Highly ambitious ATProtocol AppView service and sdks

some progress updating docs, new code blocks, use marked instead of custom md parser

+1
.gitignore
··· 1 1 .env* 2 2 !.env.example 3 3 node_modules 4 + .playwright-mcp
+681 -18
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@rendermaid/core@*": "0.6.0", 5 + "jsr:@rendermaid/core@0.6.0": "0.6.0", 4 6 "jsr:@shikijs/shiki@*": "3.7.0", 5 7 "jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3", 6 8 "jsr:@std/assert@*": "1.0.14", ··· 28 30 "npm:@takumi-rs/helpers@~0.29.8": "0.29.8", 29 31 "npm:clsx@^2.1.1": "2.1.1", 30 32 "npm:lucide-preact@0.544": "0.544.0_preact@10.27.1", 33 + "npm:marked-highlight@*": "2.2.2_marked@16.1.1", 34 + "npm:marked@*": "16.1.1", 35 + "npm:mermaid@10.6.1": "10.6.1_cytoscape@3.33.1", 31 36 "npm:pg@^8.16.3": "8.16.3", 32 37 "npm:preact-render-to-string@^6.5.13": "6.6.1_preact@10.27.1", 33 38 "npm:preact@^10.27.1": "10.27.1", 34 39 "npm:shiki@^3.7.0": "3.13.0", 35 40 "npm:tailwind-merge@^2.5.5": "2.6.0", 36 41 "npm:ts-morph@26.0.0": "26.0.0", 42 + "npm:ts-pattern@5.0.5": "5.0.5", 43 + "npm:ts-pattern@^5.7.1": "5.8.0", 37 44 "npm:typed-htmx@~0.3.1": "0.3.1" 38 45 }, 39 46 "jsr": { 47 + "@rendermaid/core@0.6.0": { 48 + "integrity": "057f87e6a57c24352051643d32a0f3c8ff7573db35fad8c8b287fb752ccf2b17", 49 + "dependencies": [ 50 + "npm:ts-pattern@5.0.5", 51 + "npm:ts-pattern@^5.7.1" 52 + ] 53 + }, 40 54 "@shikijs/shiki@3.7.0": { 41 55 "integrity": "6afb828d7d26efc521ef4ca16a7ef7245aca8e83dceaf58cc5cc64d3a4a4a895", 42 56 "dependencies": [ ··· 108 122 } 109 123 }, 110 124 "npm": { 125 + "@braintree/sanitize-url@6.0.4": { 126 + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" 127 + }, 111 128 "@isaacs/balanced-match@4.0.1": { 112 129 "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==" 113 130 }, ··· 238 255 "path-browserify" 239 256 ] 240 257 }, 258 + "@types/d3-scale-chromatic@3.1.0": { 259 + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" 260 + }, 261 + "@types/d3-scale@4.0.9": { 262 + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", 263 + "dependencies": [ 264 + "@types/d3-time" 265 + ] 266 + }, 267 + "@types/d3-time@3.0.4": { 268 + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" 269 + }, 270 + "@types/debug@4.1.12": { 271 + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", 272 + "dependencies": [ 273 + "@types/ms" 274 + ] 275 + }, 241 276 "@types/hast@3.0.4": { 242 277 "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 243 278 "dependencies": [ 244 - "@types/unist" 279 + "@types/unist@3.0.3" 280 + ] 281 + }, 282 + "@types/mdast@3.0.15": { 283 + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", 284 + "dependencies": [ 285 + "@types/unist@2.0.11" 245 286 ] 246 287 }, 247 288 "@types/mdast@4.0.4": { 248 289 "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 249 290 "dependencies": [ 250 - "@types/unist" 291 + "@types/unist@3.0.3" 251 292 ] 293 + }, 294 + "@types/ms@2.1.0": { 295 + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" 296 + }, 297 + "@types/trusted-types@2.0.7": { 298 + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" 299 + }, 300 + "@types/unist@2.0.11": { 301 + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" 252 302 }, 253 303 "@types/unist@3.0.3": { 254 304 "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" ··· 271 321 "character-entities-legacy@3.0.0": { 272 322 "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" 273 323 }, 324 + "character-entities@2.0.2": { 325 + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==" 326 + }, 274 327 "clsx@2.1.1": { 275 328 "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" 276 329 }, ··· 280 333 "comma-separated-tokens@2.0.3": { 281 334 "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" 282 335 }, 336 + "commander@7.2.0": { 337 + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" 338 + }, 339 + "cose-base@1.0.3": { 340 + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", 341 + "dependencies": [ 342 + "layout-base@1.0.2" 343 + ] 344 + }, 345 + "cose-base@2.2.0": { 346 + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", 347 + "dependencies": [ 348 + "layout-base@2.0.1" 349 + ] 350 + }, 351 + "cytoscape-cose-bilkent@4.1.0_cytoscape@3.33.1": { 352 + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", 353 + "dependencies": [ 354 + "cose-base@1.0.3", 355 + "cytoscape" 356 + ] 357 + }, 358 + "cytoscape-fcose@2.2.0_cytoscape@3.33.1": { 359 + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", 360 + "dependencies": [ 361 + "cose-base@2.2.0", 362 + "cytoscape" 363 + ] 364 + }, 365 + "cytoscape@3.33.1": { 366 + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==" 367 + }, 368 + "d3-array@2.12.1": { 369 + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", 370 + "dependencies": [ 371 + "internmap@1.0.1" 372 + ] 373 + }, 374 + "d3-array@3.2.4": { 375 + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", 376 + "dependencies": [ 377 + "internmap@2.0.3" 378 + ] 379 + }, 380 + "d3-axis@3.0.0": { 381 + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" 382 + }, 383 + "d3-brush@3.0.0_d3-selection@3.0.0": { 384 + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", 385 + "dependencies": [ 386 + "d3-dispatch", 387 + "d3-drag", 388 + "d3-interpolate", 389 + "d3-selection", 390 + "d3-transition" 391 + ] 392 + }, 393 + "d3-chord@3.0.1": { 394 + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", 395 + "dependencies": [ 396 + "d3-path@3.1.0" 397 + ] 398 + }, 399 + "d3-color@3.1.0": { 400 + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" 401 + }, 402 + "d3-contour@4.0.2": { 403 + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", 404 + "dependencies": [ 405 + "d3-array@3.2.4" 406 + ] 407 + }, 408 + "d3-delaunay@6.0.4": { 409 + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", 410 + "dependencies": [ 411 + "delaunator" 412 + ] 413 + }, 414 + "d3-dispatch@3.0.1": { 415 + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" 416 + }, 417 + "d3-drag@3.0.0": { 418 + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", 419 + "dependencies": [ 420 + "d3-dispatch", 421 + "d3-selection" 422 + ] 423 + }, 424 + "d3-dsv@3.0.1": { 425 + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", 426 + "dependencies": [ 427 + "commander", 428 + "iconv-lite", 429 + "rw" 430 + ], 431 + "bin": true 432 + }, 433 + "d3-ease@3.0.1": { 434 + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" 435 + }, 436 + "d3-fetch@3.0.1": { 437 + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", 438 + "dependencies": [ 439 + "d3-dsv" 440 + ] 441 + }, 442 + "d3-force@3.0.0": { 443 + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", 444 + "dependencies": [ 445 + "d3-dispatch", 446 + "d3-quadtree", 447 + "d3-timer" 448 + ] 449 + }, 450 + "d3-format@3.1.0": { 451 + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" 452 + }, 453 + "d3-geo@3.1.1": { 454 + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", 455 + "dependencies": [ 456 + "d3-array@3.2.4" 457 + ] 458 + }, 459 + "d3-hierarchy@3.1.2": { 460 + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" 461 + }, 462 + "d3-interpolate@3.0.1": { 463 + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", 464 + "dependencies": [ 465 + "d3-color" 466 + ] 467 + }, 468 + "d3-path@1.0.9": { 469 + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" 470 + }, 471 + "d3-path@3.1.0": { 472 + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" 473 + }, 474 + "d3-polygon@3.0.1": { 475 + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" 476 + }, 477 + "d3-quadtree@3.0.1": { 478 + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" 479 + }, 480 + "d3-random@3.0.1": { 481 + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" 482 + }, 483 + "d3-sankey@0.12.3": { 484 + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", 485 + "dependencies": [ 486 + "d3-array@2.12.1", 487 + "d3-shape@1.3.7" 488 + ] 489 + }, 490 + "d3-scale-chromatic@3.1.0": { 491 + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", 492 + "dependencies": [ 493 + "d3-color", 494 + "d3-interpolate" 495 + ] 496 + }, 497 + "d3-scale@4.0.2": { 498 + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", 499 + "dependencies": [ 500 + "d3-array@3.2.4", 501 + "d3-format", 502 + "d3-interpolate", 503 + "d3-time", 504 + "d3-time-format" 505 + ] 506 + }, 507 + "d3-selection@3.0.0": { 508 + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" 509 + }, 510 + "d3-shape@1.3.7": { 511 + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", 512 + "dependencies": [ 513 + "d3-path@1.0.9" 514 + ] 515 + }, 516 + "d3-shape@3.2.0": { 517 + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", 518 + "dependencies": [ 519 + "d3-path@3.1.0" 520 + ] 521 + }, 522 + "d3-time-format@4.1.0": { 523 + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", 524 + "dependencies": [ 525 + "d3-time" 526 + ] 527 + }, 528 + "d3-time@3.1.0": { 529 + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", 530 + "dependencies": [ 531 + "d3-array@3.2.4" 532 + ] 533 + }, 534 + "d3-timer@3.0.1": { 535 + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" 536 + }, 537 + "d3-transition@3.0.1_d3-selection@3.0.0": { 538 + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", 539 + "dependencies": [ 540 + "d3-color", 541 + "d3-dispatch", 542 + "d3-ease", 543 + "d3-interpolate", 544 + "d3-selection", 545 + "d3-timer" 546 + ] 547 + }, 548 + "d3-zoom@3.0.0_d3-selection@3.0.0": { 549 + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", 550 + "dependencies": [ 551 + "d3-dispatch", 552 + "d3-drag", 553 + "d3-interpolate", 554 + "d3-selection", 555 + "d3-transition" 556 + ] 557 + }, 558 + "d3@7.9.0_d3-selection@3.0.0": { 559 + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", 560 + "dependencies": [ 561 + "d3-array@3.2.4", 562 + "d3-axis", 563 + "d3-brush", 564 + "d3-chord", 565 + "d3-color", 566 + "d3-contour", 567 + "d3-delaunay", 568 + "d3-dispatch", 569 + "d3-drag", 570 + "d3-dsv", 571 + "d3-ease", 572 + "d3-fetch", 573 + "d3-force", 574 + "d3-format", 575 + "d3-geo", 576 + "d3-hierarchy", 577 + "d3-interpolate", 578 + "d3-path@3.1.0", 579 + "d3-polygon", 580 + "d3-quadtree", 581 + "d3-random", 582 + "d3-scale", 583 + "d3-scale-chromatic", 584 + "d3-selection", 585 + "d3-shape@3.2.0", 586 + "d3-time", 587 + "d3-time-format", 588 + "d3-timer", 589 + "d3-transition", 590 + "d3-zoom" 591 + ] 592 + }, 593 + "dagre-d3-es@7.0.10": { 594 + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", 595 + "dependencies": [ 596 + "d3", 597 + "lodash-es" 598 + ] 599 + }, 600 + "dayjs@1.11.18": { 601 + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==" 602 + }, 603 + "debug@4.4.1": { 604 + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 605 + "dependencies": [ 606 + "ms" 607 + ] 608 + }, 609 + "decode-named-character-reference@1.2.0": { 610 + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", 611 + "dependencies": [ 612 + "character-entities" 613 + ] 614 + }, 615 + "delaunator@5.0.1": { 616 + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", 617 + "dependencies": [ 618 + "robust-predicates" 619 + ] 620 + }, 283 621 "dequal@2.0.3": { 284 622 "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" 285 623 }, ··· 289 627 "dequal" 290 628 ] 291 629 }, 630 + "diff@5.2.0": { 631 + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" 632 + }, 633 + "dompurify@3.2.7": { 634 + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", 635 + "optionalDependencies": [ 636 + "@types/trusted-types" 637 + ] 638 + }, 639 + "elkjs@0.8.2": { 640 + "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==" 641 + }, 292 642 "fast-glob@3.3.3": { 293 643 "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 294 644 "dependencies": [ ··· 321 671 "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 322 672 "dependencies": [ 323 673 "@types/hast", 324 - "@types/unist", 674 + "@types/unist@3.0.3", 325 675 "ccount", 326 676 "comma-separated-tokens", 327 677 "hast-util-whitespace", ··· 342 692 "html-void-elements@3.0.0": { 343 693 "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" 344 694 }, 695 + "iconv-lite@0.6.3": { 696 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 697 + "dependencies": [ 698 + "safer-buffer" 699 + ] 700 + }, 701 + "internmap@1.0.1": { 702 + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" 703 + }, 704 + "internmap@2.0.3": { 705 + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" 706 + }, 345 707 "is-extglob@2.1.1": { 346 708 "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" 347 709 }, ··· 354 716 "is-number@7.0.0": { 355 717 "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 356 718 }, 719 + "khroma@2.1.0": { 720 + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" 721 + }, 722 + "kleur@4.1.5": { 723 + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" 724 + }, 725 + "layout-base@1.0.2": { 726 + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" 727 + }, 728 + "layout-base@2.0.1": { 729 + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" 730 + }, 731 + "lodash-es@4.17.21": { 732 + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" 733 + }, 357 734 "lucide-preact@0.544.0_preact@10.27.1": { 358 735 "integrity": "sha512-1OYqlRfxlQ6fQ8/e39kiY1btdKGCljwDmYKgF/GnB0ytVYV+PZE5EXmKdA3/Pknqs5A5QQKX+sK9TD7knUzwuw==", 359 736 "dependencies": [ 360 737 "preact" 361 738 ] 362 739 }, 740 + "marked-highlight@2.2.2_marked@16.1.1": { 741 + "integrity": "sha512-KlHOP31DatbtPPXPaI8nx1KTrG3EW0Z5zewCwpUj65swbtKOTStteK3sNAjBqV75Pgo3fNEVNHeptg18mDuWgw==", 742 + "dependencies": [ 743 + "marked" 744 + ] 745 + }, 746 + "marked@16.1.1": { 747 + "integrity": "sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==", 748 + "bin": true 749 + }, 750 + "mdast-util-from-markdown@1.3.1": { 751 + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", 752 + "dependencies": [ 753 + "@types/mdast@3.0.15", 754 + "@types/unist@2.0.11", 755 + "decode-named-character-reference", 756 + "mdast-util-to-string", 757 + "micromark", 758 + "micromark-util-decode-numeric-character-reference", 759 + "micromark-util-decode-string", 760 + "micromark-util-normalize-identifier", 761 + "micromark-util-symbol@1.1.0", 762 + "micromark-util-types@1.1.0", 763 + "unist-util-stringify-position@3.0.3", 764 + "uvu" 765 + ] 766 + }, 363 767 "mdast-util-to-hast@13.2.0": { 364 768 "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", 365 769 "dependencies": [ 366 770 "@types/hast", 367 - "@types/mdast", 771 + "@types/mdast@4.0.4", 368 772 "@ungap/structured-clone", 369 773 "devlop", 370 - "micromark-util-sanitize-uri", 774 + "micromark-util-sanitize-uri@2.0.1", 371 775 "trim-lines", 372 776 "unist-util-position", 373 777 "unist-util-visit", 374 778 "vfile" 375 779 ] 376 780 }, 781 + "mdast-util-to-string@3.2.0": { 782 + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", 783 + "dependencies": [ 784 + "@types/mdast@3.0.15" 785 + ] 786 + }, 377 787 "merge2@1.4.1": { 378 788 "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" 379 789 }, 790 + "mermaid@10.6.1_cytoscape@3.33.1": { 791 + "integrity": "sha512-Hky0/RpOw/1il9X8AvzOEChfJtVvmXm+y7JML5C//ePYMy0/9jCEmW1E1g86x9oDfW9+iVEdTV/i+M6KWRNs4A==", 792 + "dependencies": [ 793 + "@braintree/sanitize-url", 794 + "@types/d3-scale", 795 + "@types/d3-scale-chromatic", 796 + "cytoscape", 797 + "cytoscape-cose-bilkent", 798 + "cytoscape-fcose", 799 + "d3", 800 + "d3-sankey", 801 + "dagre-d3-es", 802 + "dayjs", 803 + "dompurify", 804 + "elkjs", 805 + "khroma", 806 + "lodash-es", 807 + "mdast-util-from-markdown", 808 + "non-layered-tidy-tree-layout", 809 + "stylis", 810 + "ts-dedent", 811 + "uuid", 812 + "web-worker" 813 + ] 814 + }, 815 + "micromark-core-commonmark@1.1.0": { 816 + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", 817 + "dependencies": [ 818 + "decode-named-character-reference", 819 + "micromark-factory-destination", 820 + "micromark-factory-label", 821 + "micromark-factory-space", 822 + "micromark-factory-title", 823 + "micromark-factory-whitespace", 824 + "micromark-util-character@1.2.0", 825 + "micromark-util-chunked", 826 + "micromark-util-classify-character", 827 + "micromark-util-html-tag-name", 828 + "micromark-util-normalize-identifier", 829 + "micromark-util-resolve-all", 830 + "micromark-util-subtokenize", 831 + "micromark-util-symbol@1.1.0", 832 + "micromark-util-types@1.1.0", 833 + "uvu" 834 + ] 835 + }, 836 + "micromark-factory-destination@1.1.0": { 837 + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", 838 + "dependencies": [ 839 + "micromark-util-character@1.2.0", 840 + "micromark-util-symbol@1.1.0", 841 + "micromark-util-types@1.1.0" 842 + ] 843 + }, 844 + "micromark-factory-label@1.1.0": { 845 + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", 846 + "dependencies": [ 847 + "micromark-util-character@1.2.0", 848 + "micromark-util-symbol@1.1.0", 849 + "micromark-util-types@1.1.0", 850 + "uvu" 851 + ] 852 + }, 853 + "micromark-factory-space@1.1.0": { 854 + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", 855 + "dependencies": [ 856 + "micromark-util-character@1.2.0", 857 + "micromark-util-types@1.1.0" 858 + ] 859 + }, 860 + "micromark-factory-title@1.1.0": { 861 + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", 862 + "dependencies": [ 863 + "micromark-factory-space", 864 + "micromark-util-character@1.2.0", 865 + "micromark-util-symbol@1.1.0", 866 + "micromark-util-types@1.1.0" 867 + ] 868 + }, 869 + "micromark-factory-whitespace@1.1.0": { 870 + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", 871 + "dependencies": [ 872 + "micromark-factory-space", 873 + "micromark-util-character@1.2.0", 874 + "micromark-util-symbol@1.1.0", 875 + "micromark-util-types@1.1.0" 876 + ] 877 + }, 878 + "micromark-util-character@1.2.0": { 879 + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", 880 + "dependencies": [ 881 + "micromark-util-symbol@1.1.0", 882 + "micromark-util-types@1.1.0" 883 + ] 884 + }, 380 885 "micromark-util-character@2.1.1": { 381 886 "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 382 887 "dependencies": [ 383 - "micromark-util-symbol", 384 - "micromark-util-types" 888 + "micromark-util-symbol@2.0.1", 889 + "micromark-util-types@2.0.2" 890 + ] 891 + }, 892 + "micromark-util-chunked@1.1.0": { 893 + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", 894 + "dependencies": [ 895 + "micromark-util-symbol@1.1.0" 896 + ] 897 + }, 898 + "micromark-util-classify-character@1.1.0": { 899 + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", 900 + "dependencies": [ 901 + "micromark-util-character@1.2.0", 902 + "micromark-util-symbol@1.1.0", 903 + "micromark-util-types@1.1.0" 385 904 ] 386 905 }, 906 + "micromark-util-combine-extensions@1.1.0": { 907 + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", 908 + "dependencies": [ 909 + "micromark-util-chunked", 910 + "micromark-util-types@1.1.0" 911 + ] 912 + }, 913 + "micromark-util-decode-numeric-character-reference@1.1.0": { 914 + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", 915 + "dependencies": [ 916 + "micromark-util-symbol@1.1.0" 917 + ] 918 + }, 919 + "micromark-util-decode-string@1.1.0": { 920 + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", 921 + "dependencies": [ 922 + "decode-named-character-reference", 923 + "micromark-util-character@1.2.0", 924 + "micromark-util-decode-numeric-character-reference", 925 + "micromark-util-symbol@1.1.0" 926 + ] 927 + }, 928 + "micromark-util-encode@1.1.0": { 929 + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==" 930 + }, 387 931 "micromark-util-encode@2.0.1": { 388 932 "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" 389 933 }, 934 + "micromark-util-html-tag-name@1.2.0": { 935 + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==" 936 + }, 937 + "micromark-util-normalize-identifier@1.1.0": { 938 + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", 939 + "dependencies": [ 940 + "micromark-util-symbol@1.1.0" 941 + ] 942 + }, 943 + "micromark-util-resolve-all@1.1.0": { 944 + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", 945 + "dependencies": [ 946 + "micromark-util-types@1.1.0" 947 + ] 948 + }, 949 + "micromark-util-sanitize-uri@1.2.0": { 950 + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", 951 + "dependencies": [ 952 + "micromark-util-character@1.2.0", 953 + "micromark-util-encode@1.1.0", 954 + "micromark-util-symbol@1.1.0" 955 + ] 956 + }, 390 957 "micromark-util-sanitize-uri@2.0.1": { 391 958 "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 392 959 "dependencies": [ 393 - "micromark-util-character", 394 - "micromark-util-encode", 395 - "micromark-util-symbol" 960 + "micromark-util-character@2.1.1", 961 + "micromark-util-encode@2.0.1", 962 + "micromark-util-symbol@2.0.1" 963 + ] 964 + }, 965 + "micromark-util-subtokenize@1.1.0": { 966 + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", 967 + "dependencies": [ 968 + "micromark-util-chunked", 969 + "micromark-util-symbol@1.1.0", 970 + "micromark-util-types@1.1.0", 971 + "uvu" 396 972 ] 397 973 }, 974 + "micromark-util-symbol@1.1.0": { 975 + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==" 976 + }, 398 977 "micromark-util-symbol@2.0.1": { 399 978 "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" 979 + }, 980 + "micromark-util-types@1.1.0": { 981 + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==" 400 982 }, 401 983 "micromark-util-types@2.0.2": { 402 984 "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 403 985 }, 986 + "micromark@3.2.0": { 987 + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", 988 + "dependencies": [ 989 + "@types/debug", 990 + "debug", 991 + "decode-named-character-reference", 992 + "micromark-core-commonmark", 993 + "micromark-factory-space", 994 + "micromark-util-character@1.2.0", 995 + "micromark-util-chunked", 996 + "micromark-util-combine-extensions", 997 + "micromark-util-decode-numeric-character-reference", 998 + "micromark-util-encode@1.1.0", 999 + "micromark-util-normalize-identifier", 1000 + "micromark-util-resolve-all", 1001 + "micromark-util-sanitize-uri@1.2.0", 1002 + "micromark-util-subtokenize", 1003 + "micromark-util-symbol@1.1.0", 1004 + "micromark-util-types@1.1.0", 1005 + "uvu" 1006 + ] 1007 + }, 404 1008 "micromatch@4.0.8": { 405 1009 "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 406 1010 "dependencies": [ ··· 413 1017 "dependencies": [ 414 1018 "@isaacs/brace-expansion" 415 1019 ] 1020 + }, 1021 + "mri@1.2.0": { 1022 + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" 1023 + }, 1024 + "ms@2.1.3": { 1025 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1026 + }, 1027 + "non-layered-tidy-tree-layout@2.0.2": { 1028 + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" 416 1029 }, 417 1030 "oniguruma-parser@0.12.1": { 418 1031 "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==" ··· 526 1139 "reusify@1.1.0": { 527 1140 "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" 528 1141 }, 1142 + "robust-predicates@3.0.2": { 1143 + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" 1144 + }, 529 1145 "run-parallel@1.2.0": { 530 1146 "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 531 1147 "dependencies": [ 532 1148 "queue-microtask" 533 1149 ] 534 1150 }, 1151 + "rw@1.3.3": { 1152 + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" 1153 + }, 1154 + "sade@1.8.1": { 1155 + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", 1156 + "dependencies": [ 1157 + "mri" 1158 + ] 1159 + }, 1160 + "safer-buffer@2.1.2": { 1161 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1162 + }, 535 1163 "shiki@3.13.0": { 536 1164 "integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==", 537 1165 "dependencies": [ ··· 557 1185 "character-entities-html4", 558 1186 "character-entities-legacy" 559 1187 ] 1188 + }, 1189 + "stylis@4.3.6": { 1190 + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" 560 1191 }, 561 1192 "tailwind-merge@2.6.0": { 562 1193 "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==" ··· 570 1201 "trim-lines@3.0.1": { 571 1202 "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" 572 1203 }, 1204 + "ts-dedent@2.2.0": { 1205 + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==" 1206 + }, 573 1207 "ts-morph@26.0.0": { 574 1208 "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", 575 1209 "dependencies": [ ··· 577 1211 "code-block-writer" 578 1212 ] 579 1213 }, 1214 + "ts-pattern@5.0.5": { 1215 + "integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA==" 1216 + }, 1217 + "ts-pattern@5.8.0": { 1218 + "integrity": "sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==" 1219 + }, 580 1220 "typed-html@3.0.1": { 581 1221 "integrity": "sha512-JKCM9zTfPDuPqQqdGZBWSEiItShliKkBFg5c6yOR8zth43v763XkAzTWaOlVqc0Y6p9ee8AaAbipGfUnCsYZUA==" 582 1222 }, ··· 589 1229 "unist-util-is@6.0.0": { 590 1230 "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", 591 1231 "dependencies": [ 592 - "@types/unist" 1232 + "@types/unist@3.0.3" 593 1233 ] 594 1234 }, 595 1235 "unist-util-position@5.0.0": { 596 1236 "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 597 1237 "dependencies": [ 598 - "@types/unist" 1238 + "@types/unist@3.0.3" 1239 + ] 1240 + }, 1241 + "unist-util-stringify-position@3.0.3": { 1242 + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", 1243 + "dependencies": [ 1244 + "@types/unist@2.0.11" 599 1245 ] 600 1246 }, 601 1247 "unist-util-stringify-position@4.0.0": { 602 1248 "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 603 1249 "dependencies": [ 604 - "@types/unist" 1250 + "@types/unist@3.0.3" 605 1251 ] 606 1252 }, 607 1253 "unist-util-visit-parents@6.0.1": { 608 1254 "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", 609 1255 "dependencies": [ 610 - "@types/unist", 1256 + "@types/unist@3.0.3", 611 1257 "unist-util-is" 612 1258 ] 613 1259 }, 614 1260 "unist-util-visit@5.0.0": { 615 1261 "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 616 1262 "dependencies": [ 617 - "@types/unist", 1263 + "@types/unist@3.0.3", 618 1264 "unist-util-is", 619 1265 "unist-util-visit-parents" 620 1266 ] 621 1267 }, 1268 + "uuid@9.0.1": { 1269 + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", 1270 + "bin": true 1271 + }, 1272 + "uvu@0.5.6": { 1273 + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", 1274 + "dependencies": [ 1275 + "dequal", 1276 + "diff", 1277 + "kleur", 1278 + "sade" 1279 + ], 1280 + "bin": true 1281 + }, 622 1282 "vfile-message@4.0.3": { 623 1283 "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", 624 1284 "dependencies": [ 625 - "@types/unist", 626 - "unist-util-stringify-position" 1285 + "@types/unist@3.0.3", 1286 + "unist-util-stringify-position@4.0.0" 627 1287 ] 628 1288 }, 629 1289 "vfile@6.0.3": { 630 1290 "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 631 1291 "dependencies": [ 632 - "@types/unist", 1292 + "@types/unist@3.0.3", 633 1293 "vfile-message" 634 1294 ] 1295 + }, 1296 + "web-worker@1.5.0": { 1297 + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==" 635 1298 }, 636 1299 "xtend@4.0.2": { 637 1300 "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
+38 -34
docs/api-reference.md
··· 4 4 5 5 ## Base URL 6 6 7 - ``` 7 + ```bash 8 8 https://api.slices.network/xrpc/ 9 9 ``` 10 10 ··· 36 36 - `limit` (number, optional): Maximum records (default: 50) 37 37 - `cursor` (string, optional): Pagination cursor 38 38 - `where` (object, optional): Filter conditions using field-specific queries 39 - - `sortBy` (array, optional): Sort specification with field and direction objects 39 + - `sortBy` (array, optional): Sort specification with field and direction 40 + objects 40 41 41 42 ### `[collection].getRecord` 42 43 ··· 63 64 64 65 **Response**: 65 66 66 - ```json 67 + ```json Code 67 68 { 68 69 "count": 150 69 70 } ··· 79 80 80 81 **Body**: 81 82 82 - ```json 83 + ```json Code 83 84 { 84 85 "slice": "at://your-slice-uri", 85 86 "record": { ··· 104 105 105 106 **Body**: 106 107 107 - ```json 108 + ```json Code 108 109 { 109 110 "slice": "at://your-slice-uri", 110 111 "rkey": "3xyz789abc", ··· 129 130 130 131 **Body**: 131 132 132 - ```json 133 + ```json Code 133 134 { 134 135 "rkey": "3abc123xyz" 135 136 } ··· 150 151 - `limit` (number, optional): Maximum records to return (default: 50) 151 152 - `cursor` (string, optional): Pagination cursor 152 153 - `where` (object, optional): Filter conditions using field-specific queries 153 - - `sortBy` (array, optional): Sort specification with field and direction objects 154 + - `sortBy` (array, optional): Sort specification with field and direction 155 + objects 154 156 155 157 **Response**: 156 158 157 - ```json 159 + ```json Code 158 160 { 159 161 "records": [ 160 162 { ··· 196 198 197 199 **Body**: 198 200 199 - ```json 201 + ```json Code 200 202 { 201 203 "slice": "at://your-slice-uri", 202 204 "record": { ··· 211 213 212 214 **Response**: 213 215 214 - ```json 216 + ```json Code 215 217 { 216 218 "uri": "at://did:plc:abc/network.slices.slice/xyz", 217 219 "cid": "bafyrei..." ··· 228 230 229 231 **Body**: 230 232 231 - ```json 233 + ```json Code 232 234 { 233 235 "slice": "at://your-slice-uri" 234 236 } ··· 236 238 237 239 **Response**: 238 240 239 - ```json 241 + ```json Code 240 242 { 241 243 "success": true, 242 244 "collections": ["com.recordcollector.album", "com.recordcollector.review"], ··· 262 264 263 265 **Body**: 264 266 265 - ```json 267 + ```json Code 266 268 { 267 269 "slice": "at://your-slice-uri", 268 270 "collections": ["com.recordcollector.album", "com.recordcollector.review"], ··· 274 276 275 277 **Response**: 276 278 277 - ```json 279 + ```json Code 278 280 { 279 281 "success": true, 280 282 "records": [ ··· 299 301 300 302 **Body**: 301 303 302 - ```json 304 + ```json Code 303 305 { 304 306 "slice": "at://your-slice-uri", 305 307 "collections": ["com.recordcollector.album", "com.recordcollector.review"], ··· 312 314 313 315 **Response**: 314 316 315 - ```json 317 + ```json Code 316 318 { 317 319 "success": true, 318 320 "records": [ ··· 339 341 340 342 **Body**: 341 343 342 - ```json 344 + ```json Code 343 345 { 344 346 "slice": "at://your-slice-uri", 345 347 "timeoutSeconds": 30 ··· 348 350 349 351 **Response**: 350 352 351 - ```json 353 + ```json Code 352 354 { 353 355 "success": true, 354 356 "reposProcessed": 1, ··· 368 370 369 371 **Body**: 370 372 371 - ```json 373 + ```json Code 372 374 { 373 375 "slice": "at://your-slice-uri", 374 376 "collections": ["com.recordcollector.album"], ··· 380 382 381 383 **Response**: 382 384 383 - ```json 385 + ```json Code 384 386 { 385 387 "success": true, 386 388 "jobId": "job-uuid", ··· 396 398 397 399 **Body**: 398 400 399 - ```json 401 + ```json Code 400 402 { 401 403 "target": "typescript", 402 404 "slice": "at://your-slice-uri" ··· 405 407 406 408 **Response**: 407 409 408 - ```json 410 + ```json Code 409 411 { 410 412 "success": true, 411 413 "generatedCode": "// Generated TypeScript client code..." ··· 432 434 433 435 **Response**: 434 436 435 - ```json 437 + ```json Code 436 438 { 437 439 "count": 10 438 440 } ··· 448 450 449 451 **Body**: 450 452 451 - ```json 453 + ```json Code 452 454 { 453 455 "slice": "at://your-slice-uri", 454 456 "record": { ··· 479 481 480 482 **Response**: 481 483 482 - ```json 484 + ```json Code 483 485 { 484 486 "actors": [ 485 487 { ··· 511 513 512 514 **Response**: 513 515 514 - ```json 516 + ```json Code 515 517 { 516 518 "blob": { 517 519 "$type": "blob", ··· 526 528 527 529 All endpoints may return error responses: 528 530 529 - ```json 531 + ```json Code 530 532 { 531 533 "error": "InvalidRequest", 532 534 "message": "Detailed error message" ··· 564 566 565 567 ## Filtering 566 568 567 - List endpoints support filtering using the `where` parameter with field-specific query operators: 569 + List endpoints support filtering using the `where` parameter with field-specific 570 + query operators: 568 571 569 572 ### Filter Operators 570 573 ··· 576 579 577 580 **Exact match filtering:** 578 581 579 - ```json 582 + ```json Code 580 583 { 581 584 "where": { 582 585 "artist": { "eq": "Nirvana" }, ··· 587 590 588 591 **Text search filtering:** 589 592 590 - ```json 593 + ```json Code 591 594 { 592 595 "where": { 593 596 "title": { "contains": "nevermind" }, ··· 598 601 599 602 **Array filtering:** 600 603 601 - ```json 604 + ```json Code 602 605 { 603 606 "where": { 604 607 "condition": { "in": ["Mint", "Near Mint", "Very Good Plus"] }, ··· 609 612 610 613 **Global search across all fields:** 611 614 612 - ```json 615 + ```json Code 613 616 { 614 617 "where": { 615 618 "json": { "contains": "grunge" } ··· 621 624 622 625 Sort parameter uses an array format with field and direction: 623 626 624 - ```json 627 + ```json Code 625 628 { 626 629 "sortBy": [ 627 630 { "field": "releaseDate", "direction": "desc" }, ··· 634 637 635 638 - `[{ "field": "releaseDate", "direction": "desc" }]` - Newest releases first 636 639 - `[{ "field": "artist", "direction": "asc" }]` - Alphabetical by artist 637 - - `[{ "field": "releaseDate", "direction": "desc" }, { "field": "title", "direction": "asc" }]` - Newest first, then alphabetical by title 640 + - `[{ "field": "releaseDate", "direction": "desc" }, { "field": "title", "direction": "asc" }]` - 641 + Newest first, then alphabetical by title 638 642 639 643 ## Next Steps 640 644
+6 -5
docs/concepts.md
··· 31 31 32 32 ### Lexicon Structure 33 33 34 - ```json 34 + ```json Code 35 35 { 36 36 "lexicon": 1, 37 37 "id": "com.recordcollector.album", ··· 78 78 ### Primary Collections 79 79 80 80 Collections that match your slice's domain namespace. For example, if your slice 81 - domain is `com.recordcollector`, then `com.recordcollector.album` would be a primary collection. 81 + domain is `com.recordcollector`, then `com.recordcollector.album` would be a 82 + primary collection. 82 83 83 84 ### External Collections 84 85 ··· 251 252 252 253 ### Using Generated SDKs 253 254 254 - ```typescript 255 + ```typescript Code 255 256 // Initialize client 256 257 const client = new AtProtoClient(apiUrl, sliceUri, oauthClient); 257 258 ··· 283 284 284 285 ### Blob Structure 285 286 286 - ```json 287 + ```json Code 287 288 { 288 289 "$type": "blob", 289 290 "ref": { "$link": "bafkreig5bcb..." }, ··· 296 297 297 298 Convert blob references to CDN URLs using Bluesky's CDN: 298 299 299 - ```typescript 300 + ```typescript Code 300 301 recordBlobToCdnUrl(record, blobRef, "avatar"); 301 302 // -> https://cdn.bsky.app/img/avatar/plain/did:plc:abc/bafkrei...@jpeg 302 303 ```
+2 -113
docs/getting-started.md
··· 2 2 3 3 This guide will help you set up Slices and create your first slice. 4 4 5 - ## Prerequisites 6 - 7 - - Docker and Docker Compose 8 - - PostgreSQL (or use Docker) 9 - - Deno (for frontend) 10 - - Rust and Cargo (for API development) 11 - - An AT Protocol account (for OAuth) 12 - 13 - ## Initial Setup 14 - 15 - ### 1. Clone the Repository 16 - 17 - ```bash 18 - git clone https://tangled.sh/@slices.network/slices 19 - cd slice 20 - ``` 21 - 22 - ### 2. Set Up the Database 23 - 24 - Start PostgreSQL using Docker: 25 - 26 - ```bash 27 - docker-compose up -d postgres 28 - ``` 29 - 30 - Or use an existing PostgreSQL instance and create a database: 31 - 32 - ```sql 33 - CREATE DATABASE slices; 34 - ``` 35 - 36 - ### 3. Configure Environment Variables 37 - 38 - Create `.env` files for both API and frontend: 39 - 40 - **API (`/api/.env`)**: 41 - 42 - ```bash 43 - DATABASE_URL=postgres://user:password@localhost:5432/slices 44 - AUTH_BASE_URL=https://aip.your-domain.com 45 - PORT=3000 46 - ``` 47 - 48 - **Frontend (`/frontend/.env`)**: 49 - 50 - ```bash 51 - OAUTH_CLIENT_ID=your-client-id 52 - OAUTH_CLIENT_SECRET=your-client-secret 53 - OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback 54 - OAUTH_AIP_BASE_URL=https://aip.your-domain.com 55 - SESSION_ENCRYPTION_KEY=your-32-char-key 56 - API_URL=http://localhost:3000 57 - SLICE_URI=at://did:plc:your-did/network.slices.slice/your-slice-id 58 - DATABASE_URL=slices.db 59 - ``` 60 - 61 - ### 4. Register OAuth Client 62 - 63 - Register your application with the AIP server: 64 - 65 - ```bash 66 - cd frontend 67 - ./scripts/register-oauth-client.sh 68 - ``` 69 - 70 - Save the client ID and secret to your `.env` file. 71 - 72 - ### 5. Start the Services 73 - 74 - Start the API server: 75 - 76 - ```bash 77 - cd api 78 - cargo run 79 - ``` 80 - 81 - Start the frontend: 82 - 83 - ```bash 84 - cd frontend 85 - deno task dev 86 - ``` 87 - 88 - Visit `http://localhost:8000` to access the web interface. 89 - 90 5 ## Creating Your First Slice 91 6 92 7 ### 1. Log In ··· 105 20 Navigate to your slice and go to the Lexicon tab. Create a lexicon for your 106 21 first record type: 107 22 108 - ```json 23 + ```json Code 109 24 { 110 25 "lexicon": 1, 111 26 "id": "com.recordcollector.album", ··· 161 76 162 77 In your application: 163 78 164 - ```typescript 79 + ```typescript Code 165 80 import { AtProtoClient } from "./generated-client.ts"; 166 81 167 82 const client = new AtProtoClient( ··· 218 133 - [API Reference](./api-reference.md) - Explore available endpoints 219 134 - [SDK Usage](./sdk-usage.md) - Advanced SDK patterns 220 135 - [Examples](./examples/) - Sample applications 221 - 222 - ## Troubleshooting 223 - 224 - ### Database Connection Issues 225 - 226 - - Verify PostgreSQL is running: `docker ps` 227 - - Check DATABASE_URL format 228 - - Ensure database exists 229 - 230 - ### OAuth Errors 231 - 232 - - Verify client ID and secret 233 - - Check redirect URI matches configuration 234 - - Ensure AIP server is accessible 235 - 236 - ### Sync Not Working 237 - 238 - - Check user has necessary permissions 239 - - Verify lexicons are valid 240 - - Check API server logs for errors 241 - 242 - ### Generated Client Issues 243 - 244 - - Regenerate client after lexicon changes 245 - - Ensure API server is running 246 - - Check for TypeScript compilation errors
+118
docs/introduction.md
··· 1 + # Introduction 2 + 3 + Slices is an open source platform for building structured data applications on 4 + the AT Protocol network. 5 + 6 + ## What is Slices? 7 + 8 + Slices lets you define custom data schemas and build applications that store, 9 + query, and sync structured records across the decentralized AT Protocol network. 10 + Think of it as a schema-first backend that automatically handles data 11 + validation, indexing, and cross-network synchronization. 12 + 13 + ## How Slices Works on AT Protocol 14 + 15 + ```mermaid 16 + flowchart LR 17 + Users[Users<br/>Create/Update Records] --> PDS[PDS Nodes<br/>Store user data] 18 + PDS --> Firehose[Firehose<br/>Stream of all events] 19 + Firehose --> SlicesNetwork[Slices Network - AppView<br/>• Monitors all AT Protocol data<br/>• Routes to relevant slices] 20 + SlicesNetwork --> SliceA[Slice A<br/>• Blog lexicons<br/>• Post records<br/>• Comment queries] 21 + SlicesNetwork --> SliceB[Slice B<br/>• Music lexicons<br/>• Album records<br/>• Playlist queries] 22 + SliceA --> ClientA[Application<br/>Client<br/>• Read/write data] 23 + SliceB --> ClientB[Application<br/>Client<br/>• Read/write data] 24 + ``` 25 + 26 + **Flow:** 27 + 28 + 1. Users create records on their Personal Data Server (PDS) 29 + 2. The Firehose streams all network events in real-time 30 + 3. The Slices Network monitors the firehose and routes data to relevant slices 31 + 4. Each slice indexes only records matching its specific lexicons 32 + 5. Application clients connect to specific slices to read/write data 33 + 34 + ## Quick Start 35 + 36 + Get started in under a minute: 37 + 38 + ```bash 39 + # Initialize a new slice project 40 + deno run -A jsr:@slices/cli init 41 + 42 + # Follow the prompts to: 43 + # 1. Name your slice 44 + # 2. Define your first lexicon 45 + # 3. Deploy to the network 46 + ``` 47 + 48 + ## Simple Example 49 + 50 + Define a schema for a blog post: 51 + 52 + ```json lexicons/com/myblog/post.json 53 + { 54 + "lexicon": 1, 55 + "id": "com.myblog.post", 56 + "defs": { 57 + "main": { 58 + "type": "record", 59 + "record": { 60 + "type": "object", 61 + "required": ["title", "content", "createdAt"], 62 + "properties": { 63 + "title": { "type": "string", "maxLength": 200 }, 64 + "content": { "type": "string", "maxLength": 10000 }, 65 + "tags": { "type": "array", "items": { "type": "string" } }, 66 + "createdAt": { "type": "string", "format": "datetime" } 67 + } 68 + } 69 + } 70 + } 71 + } 72 + ``` 73 + 74 + Deploy your lexicon and generate the TypeScript client: 75 + 76 + ```bash 77 + # Push your lexicon to the slice 78 + deno run -A jsr:@slices/cli lexicon push 79 + 80 + # Generate TypeScript SDK from your lexicons 81 + deno run -A jsr:@slices/cli codegen 82 + ``` 83 + 84 + Query your data using the auto-generated API: 85 + 86 + ```typescript 87 + // Get all posts with a specific tag 88 + const posts = await client.com.myblog.post.getRecords({ 89 + slice: "at://your-slice-uri", 90 + where: { tags: { contains: "javascript" } }, 91 + sortBy: [{ field: "createdAt", direction: "desc" }], 92 + }); 93 + ``` 94 + 95 + ## Key Features 96 + 97 + - **Schema Validation**: Define lexicons that enforce data structure and constraints 98 + - **Auto-generated APIs**: REST endpoints created automatically from your schemas 99 + - **TypeScript SDKs**: Type-safe clients generated from your lexicons 100 + - **Real-time Sync**: Automatic synchronization across the AT Protocol network 101 + - **Advanced Querying**: Filter, sort, and paginate records with a powerful query API 102 + - **OAuth Built-in**: Authentication with any AT Protocol account 103 + 104 + ## When to Use Slices 105 + 106 + Slices is ideal for: 107 + 108 + - **Social Applications**: Build specialized communities, forums, or social features 109 + - **Content Platforms**: Create blogs, documentation sites, or media libraries 110 + - **SaaS Products**: Develop collaborative tools with structured data needs 111 + - **Web APIs**: Design REST APIs with automatic validation and documentation 112 + - **Decentralized Apps**: Build on AT Protocol without managing infrastructure 113 + 114 + ## Next Steps 115 + 116 + - [Getting Started](./getting-started.md) - Set up your first slice 117 + - [Core Concepts](./concepts.md) - Understand lexicons and collections 118 + - [API Reference](./api-reference.md) - Explore the full API
+104
docs/plugins.md
··· 1 + # Plugins 2 + 3 + Enhance your Slices development workflow with plugins. 4 + 5 + ## Lexicon IntelliSense 6 + 7 + A VS Code extension that provides real-time validation and IntelliSense support 8 + for AT Protocol lexicon files. 9 + 10 + [View on VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=SlicesNetwork.lexicon-intellisense) 11 + 12 + ### Features 13 + 14 + - **Real-time validation**: Validates lexicon files as you type using the 15 + `@slices/lexicon` validator 16 + - **JSON Schema support**: Provides autocomplete and validation for lexicon 17 + structure 18 + - **Cross-lexicon validation**: Validates references between lexicon files in 19 + your workspace 20 + - **Error diagnostics**: Shows validation errors directly in the editor 21 + 22 + ### Installation 23 + 24 + Install from the VS Code marketplace: 25 + 26 + 1. Open VS Code Extensions (Cmd+Shift+X on Mac, Ctrl+Shift+X on Windows/Linux) 27 + 2. Search for "Lexicon IntelliSense" by SlicesNetwork 28 + 3. Click Install 29 + 30 + ### Configuration 31 + 32 + Configure the extension in VS Code settings: 33 + 34 + ```json .vscode/settings.json 35 + { 36 + "lexiconIntelliSense.enableValidation": true, 37 + "lexiconIntelliSense.lexiconDirectory": "lexicons" 38 + } 39 + ``` 40 + 41 + **Settings:** 42 + 43 + - `lexiconIntelliSense.enableValidation`: Enable/disable validation (default: 44 + `true`) 45 + - `lexiconIntelliSense.lexiconDirectory`: Directory containing lexicon files 46 + relative to workspace root (default: `"lexicons"`) 47 + 48 + ### Commands 49 + 50 + Access these commands via the Command Palette (Cmd+Shift+P): 51 + 52 + - **Lexicon: Validate Current File** - Validates the currently open lexicon file 53 + - **Lexicon: Validate Workspace** - Validates all lexicon files in the workspace 54 + 55 + ### Usage 56 + 57 + The extension automatically activates for: 58 + 59 + - JSON files in directories containing "lexicons" in the path 60 + - JSON files with lexicon structure (containing `id` and `defs` fields) 61 + 62 + It validates: 63 + 64 + - **Structure**: Required `id` and `defs` fields 65 + - **Types**: Correct definition types (record, query, procedure, subscription) 66 + - **References**: Resolvable references to other lexicons 67 + - **Formats**: String formats like datetime, uri, nsid, at-uri, did 68 + 69 + ### Example 70 + 71 + When editing a lexicon file like `lexicons/com/example/post.json`: 72 + 73 + ```json lexicons/com/example/post.json 74 + { 75 + "lexicon": 1, 76 + "id": "com.example.post", 77 + "defs": { 78 + "main": { 79 + "type": "record", 80 + "description": "A blog post", 81 + "record": { 82 + "type": "object", 83 + "required": ["title", "content"], 84 + "properties": { 85 + "title": { 86 + "type": "string", 87 + "maxLength": 200 88 + }, 89 + "content": { 90 + "type": "string" 91 + } 92 + } 93 + } 94 + } 95 + } 96 + } 97 + ``` 98 + 99 + The extension provides: 100 + 101 + - Autocomplete for property types and fields 102 + - Validation errors for missing required fields 103 + - Warnings for invalid references 104 + - IntelliSense for lexicon-specific properties
+27 -27
docs/sdk-usage.md
··· 7 7 After generating your TypeScript client, you can use it directly in your 8 8 project: 9 9 10 - ```typescript 10 + ```typescript Code 11 11 import { AtProtoClient } from "./generated_client.ts"; 12 12 import { OAuthClient } from "@slices/oauth"; 13 13 ``` ··· 16 16 17 17 ### Without Authentication (Read-Only) 18 18 19 - ```typescript 19 + ```typescript Code 20 20 const client = new AtProtoClient( 21 21 "https://api.your-domain.com", 22 22 "at://did:plc:abc/network.slices.slice/your-slice-rkey", ··· 28 28 29 29 ### With Authentication (Full Access) 30 30 31 - ```typescript 31 + ```typescript Code 32 32 import { OAuthClient } from "@slices/oauth"; 33 33 34 34 // Set up OAuth client ··· 54 54 55 55 The SDK uses `getRecords` for retrieving records: 56 56 57 - ```typescript 57 + ```typescript Code 58 58 // Get all vinyl records 59 59 const albums = await client.com.recordcollector.album.getRecords(); 60 60 ··· 142 142 The `countRecords` method allows you to count records without fetching them, 143 143 using the same filtering parameters as `getRecords`: 144 144 145 - ```typescript 145 + ```typescript Code 146 146 // Count all records 147 147 const total = await client.com.recordcollector.album.countRecords(); 148 148 console.log(`Total albums: ${total.count}`); ··· 186 186 187 187 ### Getting a Single Record 188 188 189 - ```typescript 189 + ```typescript Code 190 190 const album = await client.com.recordcollector.album.getRecord({ 191 191 uri: "at://did:plc:abc/com.recordcollector.album/3jklmno456", 192 192 }); ··· 197 197 198 198 ### Creating Records 199 199 200 - ```typescript 200 + ```typescript Code 201 201 // Create with auto-generated key 202 202 const newAlbum = await client.com.recordcollector.album.createRecord({ 203 203 title: "In Utero", ··· 221 221 222 222 ### Updating Records 223 223 224 - ```typescript 224 + ```typescript Code 225 225 // Get the record key from the URI 226 226 const uri = "at://did:plc:abc/com.recordcollector.album/3jklmno456"; 227 227 const rkey = uri.split("/").pop(); // '3jklmno456' ··· 241 241 242 242 ### Deleting Records 243 243 244 - ```typescript 244 + ```typescript Code 245 245 const rkey = "3jklmno456"; 246 246 await client.com.recordcollector.album.deleteRecord(rkey); 247 247 ``` ··· 250 250 251 251 Access synced external collections like Bluesky profiles: 252 252 253 - ```typescript 253 + ```typescript Code 254 254 // Get Bluesky profiles in your slice 255 255 const profiles = await client.app.bsky.actor.profile.getRecords(); 256 256 ··· 268 268 269 269 ### Uploading Blobs 270 270 271 - ```typescript 271 + ```typescript Code 272 272 // Read file as ArrayBuffer 273 273 const file = await Deno.readFile("./nevermind-cover.jpg"); 274 274 ··· 291 291 292 292 ### Converting Blobs to CDN URLs 293 293 294 - ```typescript 294 + ```typescript Code 295 295 import { recordBlobToCdnUrl } from "./generated-client.ts"; 296 296 297 297 // Get a record with a blob ··· 320 320 321 321 ### Get Slice Statistics 322 322 323 - ```typescript 323 + ```typescript Code 324 324 const stats = await client.network.slices.slice.stats({ 325 325 slice: "at://your-slice-uri", 326 326 }); ··· 338 338 The `getActors` method retrieves actors (users) within a slice with powerful 339 339 filtering and sorting capabilities: 340 340 341 - ```typescript 341 + ```typescript Code 342 342 // Get all actors in the slice 343 343 const actors = await client.network.slices.slice.getActors(); 344 344 ··· 384 384 385 385 The `getSliceRecords` method uses the same `where` clause approach: 386 386 387 - ```typescript 387 + ```typescript Code 388 388 // Get records from specific collections 389 389 const records = await client.network.slices.slice.getSliceRecords({ 390 390 where: { ··· 439 439 440 440 Search within specific fields of your records: 441 441 442 - ```typescript 442 + ```typescript Code 443 443 // Search in title field only 444 444 const titleSearch = await client.com.recordcollector.album.getRecords({ 445 445 where: { ··· 459 459 460 460 Use the special `json` field to search across **all fields** in a record: 461 461 462 - ```typescript 462 + ```typescript Code 463 463 // Finds records containing "grunge" anywhere in their data 464 464 const globalSearch = await client.com.recordcollector.album.getRecords({ 465 465 where: { ··· 479 479 480 480 When using `getSliceRecords`, you can search across multiple collections: 481 481 482 - ```typescript 482 + ```typescript Code 483 483 // Search for "seattle" across all collections 484 484 const crossCollectionSearch = await client.network.slices.slice.getSliceRecords( 485 485 { ··· 506 506 using the separate `orWhere` parameter. This provides clean type safety and 507 507 autocomplete for field names: 508 508 509 - ```typescript 509 + ```typescript Code 510 510 // Find albums by either Nirvana OR Alice in Chains 511 511 const albums = await client.com.recordcollector.album.getRecords({ 512 512 orWhere: { ··· 562 562 563 563 ### Sync User Collections 564 564 565 - ```typescript 565 + ```typescript Code 566 566 // Sync current user's data (requires auth) 567 567 const syncResult = await client.network.slices.slice.syncUserCollections({ 568 568 timeoutSeconds: 30, ··· 573 573 574 574 ## Error Handling 575 575 576 - ```typescript 576 + ```typescript Code 577 577 try { 578 578 const post = await client.com.example.post.getRecord({ 579 579 uri: "at://invalid-uri", ··· 593 593 594 594 ### 1. Initialize OAuth 595 595 596 - ```typescript 596 + ```typescript Code 597 597 const oauthClient = new OAuthClient({ 598 598 clientId: process.env.OAUTH_CLIENT_ID, 599 599 clientSecret: process.env.OAUTH_CLIENT_SECRET, ··· 604 604 605 605 ### 2. Start Authorization 606 606 607 - ```typescript 607 + ```typescript Code 608 608 const authResult = await oauthClient.authorize({ 609 609 loginHint: "user.bsky.social", 610 610 }); ··· 615 615 616 616 ### 3. Handle Callback 617 617 618 - ```typescript 618 + ```typescript Code 619 619 // In your callback handler 620 620 const urlParams = new URLSearchParams(window.location.search); 621 621 const code = urlParams.get("code"); ··· 626 626 627 627 ### 4. Use Authenticated Client 628 628 629 - ```typescript 629 + ```typescript Code 630 630 const client = new AtProtoClient(apiUrl, sliceUri, oauthClient); 631 631 632 632 // OAuth tokens are automatically managed ··· 640 640 641 641 The generated SDK provides full TypeScript type safety: 642 642 643 - ```typescript 643 + ```typescript Code 644 644 // TypeScript knows the shape of your records 645 645 const album = await client.com.recordcollector.album.getRecord({ uri }); 646 646 ··· 669 669 670 670 ### Batch Operations 671 671 672 - ```typescript 672 + ```typescript Code 673 673 // Process records in batches 674 674 async function* getAllAlbums() { 675 675 let cursor: string | undefined;
+88
docs/self-hosting.md
··· 1 + # Self-Hosting Slices 2 + 3 + This guide covers how to set up and run your own Slices instance. 4 + 5 + ## Prerequisites 6 + 7 + - Docker and Docker Compose 8 + - PostgreSQL (or use Docker) 9 + - Deno (for frontend) 10 + - Rust and Cargo (for API development) 11 + - An AT Protocol account (for OAuth) 12 + 13 + ## Initial Setup 14 + 15 + ### 1. Clone the Repository 16 + 17 + ```bash 18 + git clone https://tangled.sh/@slices.network/slices 19 + cd slice 20 + ``` 21 + 22 + ### 2. Set Up the Database 23 + 24 + Start PostgreSQL using Docker: 25 + 26 + ```bash 27 + docker-compose up -d postgres 28 + ``` 29 + 30 + Or use an existing PostgreSQL instance and create a database: 31 + 32 + ```sql 33 + CREATE DATABASE slices; 34 + ``` 35 + 36 + ### 3. Configure Environment Variables 37 + 38 + Create `.env` files for both API and frontend: 39 + 40 + **API (`/api/.env`)**: 41 + 42 + ```bash 43 + DATABASE_URL=postgres://user:password@localhost:5432/slices 44 + AUTH_BASE_URL=https://aip.your-domain.com 45 + PORT=3000 46 + ``` 47 + 48 + **Frontend (`/frontend/.env`)**: 49 + 50 + ```bash 51 + OAUTH_CLIENT_ID=your-client-id 52 + OAUTH_CLIENT_SECRET=your-client-secret 53 + OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback 54 + OAUTH_AIP_BASE_URL=https://aip.your-domain.com 55 + SESSION_ENCRYPTION_KEY=your-32-char-key 56 + API_URL=http://localhost:3000 57 + SLICE_URI=at://did:plc:your-did/network.slices.slice/your-slice-id 58 + DATABASE_URL=slices.db 59 + ``` 60 + 61 + ### 4. Register OAuth Client 62 + 63 + Register your application with the AIP server: 64 + 65 + ```bash 66 + cd frontend 67 + ./scripts/register-oauth-client.sh 68 + ``` 69 + 70 + Save the client ID and secret to your `.env` file. 71 + 72 + ### 5. Start the Services 73 + 74 + Start the API server: 75 + 76 + ```bash 77 + cd api 78 + cargo run 79 + ``` 80 + 81 + Start the frontend: 82 + 83 + ```bash 84 + cd frontend 85 + deno task dev 86 + ``` 87 + 88 + Visit `http://localhost:8000` to access the web interface.
+217 -144
frontend/src/features/docs/handlers.tsx
··· 4 4 import { codeToHtml } from "jsr:@shikijs/shiki"; 5 5 import { DocsPage } from "./templates/DocsPage.tsx"; 6 6 import { DocsIndexPage } from "./templates/DocsIndexPage.tsx"; 7 + import { render } from "preact-render-to-string"; 8 + import { CodeBlock } from "./templates/fragments/CodeBlock.tsx"; 9 + import { marked } from "npm:marked"; 10 + import type { Tokens } from "npm:marked"; 11 + import { markedHighlight } from "npm:marked-highlight"; 7 12 8 - // List of available docs 9 - const AVAILABLE_DOCS = [ 13 + // Categorized documentation structure 14 + const DOCS_CATEGORIES = [ 10 15 { 11 - slug: "getting-started", 12 - title: "Getting Started", 13 - description: "Learn how to set up and use Slices", 16 + category: "Getting Started", 17 + docs: [ 18 + { 19 + slug: "introduction", 20 + title: "Introduction", 21 + description: "Overview of Slices platform", 22 + }, 23 + { 24 + slug: "getting-started", 25 + title: "Quick Start", 26 + description: "Learn how to set up and use Slices", 27 + }, 28 + ], 14 29 }, 15 30 { 16 - slug: "concepts", 17 - title: "Core Concepts", 18 - description: "Understand slices, lexicons, and collections", 31 + category: "Core Concepts", 32 + docs: [ 33 + { 34 + slug: "concepts", 35 + title: "Core Concepts", 36 + description: "Understand slices, lexicons, and collections", 37 + }, 38 + ], 19 39 }, 20 40 { 21 - slug: "api-reference", 22 - title: "API Reference", 23 - description: "Complete endpoint documentation", 41 + category: "Reference", 42 + docs: [ 43 + { 44 + slug: "api-reference", 45 + title: "API Reference", 46 + description: "Complete endpoint documentation", 47 + }, 48 + { 49 + slug: "sdk-usage", 50 + title: "SDK Usage", 51 + description: "Advanced client patterns and examples", 52 + }, 53 + ], 24 54 }, 25 55 { 26 - slug: "sdk-usage", 27 - title: "SDK Usage", 28 - description: "Advanced client patterns and examples", 56 + category: "Extensions", 57 + docs: [ 58 + { 59 + slug: "plugins", 60 + title: "Plugins", 61 + description: "Extensions and tools for development", 62 + }, 63 + ], 29 64 }, 30 65 ]; 66 + 67 + // Flatten for backward compatibility 68 + const AVAILABLE_DOCS = DOCS_CATEGORIES.flatMap(category => category.docs); 31 69 32 70 const DOCS_PATH = Deno.env.get("DOCS_PATH") || "../docs"; 33 71 ··· 42 80 } 43 81 } 44 82 45 - // Markdown to HTML converter with Shiki syntax highlighting 83 + // Extract headers for table of contents 84 + function extractHeaders(html: string) { 85 + const headerRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/g; 86 + const headers: Array<{ level: number; text: string; id: string }> = []; 87 + 88 + let match; 89 + while ((match = headerRegex.exec(html)) !== null) { 90 + const level = parseInt(match[1]); 91 + const text = match[2].replace(/<[^>]+>/g, ""); // Strip HTML tags 92 + const id = text 93 + .toLowerCase() 94 + .replace(/[^\w\s-]/g, "") // Remove special characters 95 + .replace(/\s+/g, "-") // Replace spaces with hyphens 96 + .trim(); 97 + 98 + headers.push({ level, text, id }); 99 + } 100 + 101 + return headers; 102 + } 103 + 104 + // Markdown to HTML converter using marked with custom renderer and Shiki 46 105 async function markdownToHtml(markdown: string): Promise<string> { 47 - // First, extract and process code blocks with Shiki 48 - const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; 49 - const codeBlocks: { placeholder: string; replacement: string }[] = []; 106 + // Configure marked with highlight extension for code blocks 107 + marked.use( 108 + markedHighlight({ 109 + async: true, 110 + async highlight(code, lang, info) { 111 + // Parse filename from info string (e.g., "javascript filename.js") 112 + const parts = (info || "").split(/\s+/); 113 + const filename = 114 + parts.length > 1 ? parts.slice(1).join(" ") : undefined; 115 + 116 + // Handle Mermaid diagrams - client-side rendering 117 + if (lang === "mermaid") { 118 + return ` 119 + <div class="my-8 w-full overflow-x-auto"> 120 + <div class="mermaid w-full" style="font-size: 16px;">${code}</div> 121 + </div> 122 + `; 123 + } 50 124 51 - let html = markdown; 52 - let blockIndex = 0; 53 - let match; 54 - while ((match = codeBlockRegex.exec(markdown)) !== null) { 55 - const [fullMatch, lang, code] = match; 56 - const placeholder = `__CODE_BLOCK_${blockIndex}__`; 125 + const highlightedCode = await codeToHtml(code, { 126 + lang: lang || "text", 127 + themes: { 128 + light: "github-light", 129 + dark: "github-dark", 130 + }, 131 + }); 57 132 58 - try { 59 - const highlightedCode = await codeToHtml(code.trim(), { 60 - lang: lang || "text", 61 - themes: { 62 - light: "github-light", 63 - dark: "github-dark", 64 - }, 65 - }); 133 + // Generate the complete code block using composition 134 + if (filename) { 135 + return render( 136 + <CodeBlock> 137 + <CodeBlock.Header> 138 + <span>{filename}</span> 139 + <CodeBlock.CopyButton /> 140 + </CodeBlock.Header> 141 + <CodeBlock.Code highlightedCode={highlightedCode} /> 142 + </CodeBlock> 143 + ); 144 + } else { 145 + return render(<CodeBlock.Code highlightedCode={highlightedCode} />); 146 + } 147 + }, 148 + langPrefix: "language-", 149 + }) 150 + ); 151 + 152 + // Configure marked with custom renderer 153 + const renderer = new marked.Renderer(); 154 + 155 + // Custom inline code renderer 156 + renderer.codespan = function (token: Tokens.Codespan) { 157 + return `<code class="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 px-1.5 py-0.5 rounded text-sm font-mono font-normal">${token.text}</code>`; 158 + }; 159 + 160 + // Custom header renderer with IDs and styling 161 + renderer.heading = function (token: Tokens.Heading) { 162 + const text = this.parser.parseInline(token.tokens); 163 + const level = token.depth; 164 + const id = text 165 + .toLowerCase() 166 + .replace(/<[^>]+>/g, "") // Strip HTML tags 167 + .replace(/[^\w\s-]/g, "") // Remove special characters 168 + .replace(/\s+/g, "-") // Replace spaces with hyphens 169 + .trim(); 170 + 171 + const styles = { 172 + 1: "text-2xl font-bold text-zinc-900 dark:text-white mt-10 mb-6", 173 + 2: "text-xl font-bold text-zinc-900 dark:text-white mt-10 mb-4", 174 + 3: "text-lg font-semibold text-zinc-900 dark:text-white mt-8 mb-4", 175 + 4: "text-base font-semibold text-zinc-900 dark:text-white mt-6 mb-3", 176 + 5: "text-sm font-semibold text-zinc-900 dark:text-white mt-4 mb-2", 177 + 6: "text-xs font-semibold text-zinc-900 dark:text-white mt-4 mb-2", 178 + }; 179 + 180 + return `<h${level} id="${id}" class="${ 181 + styles[level as keyof typeof styles] 182 + }">${text}</h${level}>`; 183 + }; 66 184 67 - // Wrap in a container with proper styling 68 - const styledCode = 69 - `<div class="my-4 [&_pre]:p-4 [&_pre]:rounded-md [&_pre]:overflow-x-auto [&_pre]:text-sm">${highlightedCode}</div>`; 185 + // Custom link renderer to handle .md links 186 + renderer.link = function (token: Tokens.Link) { 187 + let href = token.href; 188 + const title = token.title; 189 + const text = this.parser.parseInline(token.tokens); 70 190 71 - codeBlocks.push({ 72 - placeholder, 73 - replacement: styledCode, 74 - }); 75 - } catch (error) { 76 - // Fallback to simple code block if Shiki fails 77 - console.warn("Shiki highlighting failed:", error); 78 - const fallback = 79 - `<pre class="bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md p-4 overflow-x-auto my-4"><code class="text-sm text-zinc-900 dark:text-zinc-100">${code.trim()}</code></pre>`; 80 - codeBlocks.push({ 81 - placeholder, 82 - replacement: fallback, 83 - }); 191 + // Convert relative .md links to docs routes 192 + if (href.endsWith(".md") && !href.startsWith("http")) { 193 + const slug = href.replace(/^\.\//, "").replace(/\.md$/, ""); 194 + href = `/docs/${slug}`; 84 195 } 85 196 86 - // Replace the code block with placeholder 87 - html = html.replace(fullMatch, placeholder); 88 - blockIndex++; 89 - } 197 + const titleAttr = title ? ` title="${title}"` : ""; 198 + return `<a href="${href}"${titleAttr} class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 underline">${text}</a>`; 199 + }; 90 200 91 - // Process other markdown elements 92 - html = html 93 - // Headers with inline code (process these first to handle backticks in headers) 94 - .replace( 95 - /^#### `([^`]+)`$/gm, 96 - '<h4 class="text-base font-semibold text-zinc-900 dark:text-white mt-6 mb-3"><code class="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 px-2 py-1 rounded text-sm font-mono font-normal">$1</code></h4>', 97 - ) 98 - .replace( 99 - /^### `([^`]+)`$/gm, 100 - '<h3 class="text-lg font-semibold text-zinc-900 dark:text-white mt-8 mb-4"><code class="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 px-2 py-1 rounded font-mono font-normal">$1</code></h3>', 101 - ) 102 - .replace( 103 - /^## `([^`]+)`$/gm, 104 - '<h2 class="text-xl font-bold text-zinc-900 dark:text-white mt-10 mb-4"><code class="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 px-2 py-1 rounded font-mono font-normal">$1</code></h2>', 105 - ) 106 - // Regular headers (without backticks) 107 - .replace( 108 - /^#### (.*$)/gm, 109 - '<h4 class="text-base font-semibold text-zinc-900 dark:text-white mt-6 mb-3">$1</h4>', 110 - ) 111 - .replace( 112 - /^### (.*$)/gm, 113 - '<h3 class="text-lg font-semibold text-zinc-900 dark:text-white mt-8 mb-4">$1</h3>', 114 - ) 115 - .replace( 116 - /^## (.*$)/gm, 117 - '<h2 class="text-xl font-bold text-zinc-900 dark:text-white mt-10 mb-4">$1</h2>', 118 - ) 119 - .replace( 120 - /^# (.*$)/gm, 121 - '<h1 class="text-2xl font-bold text-zinc-900 dark:text-white mt-10 mb-6">$1</h1>', 122 - ) 123 - // Inline code (for non-header text) 124 - .replace( 125 - /`([^`]+)`/g, 126 - '<code class="bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 px-1.5 py-0.5 rounded text-sm font-mono font-normal">$1</code>', 127 - ) 128 - // Bold 129 - .replace(/\*\*(.*?)\*\*/g, '<strong class="font-semibold text-zinc-900 dark:text-white">$1</strong>') 130 - // Lists (handle both - and * syntax, process before italic to avoid conflicts) 131 - .replace( 132 - /^[\-\*] (.*$)/gm, 133 - '<li class="mb-1" data-type="unordered">$1</li>', 134 - ) 135 - // Numbered lists 136 - .replace(/^\d+\. (.*$)/gm, '<li class="mb-1" data-type="ordered">$1</li>') 137 - // Italic (use word boundaries to avoid matching list items) 138 - .replace(/(?<!\*)\*(?!\*)([^\*]+)\*(?!\*)/g, '<em class="italic">$1</em>') 139 - // Links (convert .md links to docs routes) 140 - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => { 141 - // Convert relative .md links to docs routes 142 - if (url.endsWith(".md") && !url.startsWith("http")) { 143 - const slug = url.replace(/^\.\//, "").replace(/\.md$/, ""); 144 - return `<a href="/docs/${slug}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 underline">${text}</a>`; 145 - } 146 - return `<a href="${url}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 underline">${text}</a>`; 147 - }); 201 + // Custom paragraph renderer 202 + renderer.paragraph = function (token: Tokens.Paragraph) { 203 + const text = this.parser.parseInline(token.tokens); 204 + return `<p class="mb-4 leading-relaxed text-zinc-700 dark:text-zinc-300">${text}</p>`; 205 + }; 148 206 149 - // Group consecutive list items into ul/ol elements 150 - html = html.replace( 151 - /(<li[^>]*data-type="unordered"[^>]*>.*?<\/li>\s*)+/gs, 152 - (match) => { 153 - const cleanedMatch = match.replace(/data-type="unordered"/g, ""); 154 - return `<ul class="list-disc list-inside my-4 text-zinc-700 dark:text-zinc-300">${cleanedMatch}</ul>`; 155 - }, 156 - ); 207 + // Custom list renderers 208 + renderer.list = function (token: Tokens.List) { 209 + const ordered = token.ordered; 210 + const body = token.items 211 + .map((item: Tokens.ListItem) => { 212 + const text = this.parser.parseInline(item.tokens); 213 + return `<li class="mb-1">${text}</li>`; 214 + }) 215 + .join(""); 216 + const tag = ordered ? "ol" : "ul"; 217 + const listStyle = ordered ? "list-decimal" : "list-disc"; 218 + return `<${tag} class="${listStyle} list-inside my-4 text-zinc-700 dark:text-zinc-300">${body}</${tag}>`; 219 + }; 157 220 158 - html = html.replace( 159 - /(<li[^>]*data-type="ordered"[^>]*>.*?<\/li>\s*)+/gs, 160 - (match) => { 161 - const cleanedMatch = match.replace(/data-type="ordered"/g, ""); 162 - return `<ol class="list-decimal list-inside my-4 text-zinc-700 dark:text-zinc-300">${cleanedMatch}</ol>`; 163 - }, 164 - ); 221 + renderer.listitem = function (token: Tokens.ListItem) { 222 + const text = this.parser.parseInline(token.tokens); 223 + return `<li class="mb-1">${text}</li>`; 224 + }; 165 225 166 - // Process paragraphs 167 - html = html.split("\n\n") 168 - .map((paragraph) => { 169 - const trimmed = paragraph.trim(); 170 - if (!trimmed) return ""; 171 - if (trimmed.startsWith("<") || trimmed.startsWith("__CODE_BLOCK_")) { 172 - return trimmed; // Already HTML or placeholder 173 - } 174 - return `<p class="mb-4 leading-relaxed text-zinc-700 dark:text-zinc-300">${trimmed}</p>`; 175 - }) 176 - .join("\n"); 226 + // Custom strong/bold renderer 227 + renderer.strong = function (token: Tokens.Strong) { 228 + const text = this.parser.parseInline(token.tokens); 229 + return `<strong class="font-semibold text-zinc-900 dark:text-white">${text}</strong>`; 230 + }; 231 + 232 + // Custom emphasis/italic renderer 233 + renderer.em = function (token: Tokens.Em) { 234 + const text = this.parser.parseInline(token.tokens); 235 + return `<em class="italic">${text}</em>`; 236 + }; 237 + 238 + // Custom code block renderer - return content as-is since marked-highlight handles it 239 + renderer.code = function (token: Tokens.Code) { 240 + return token.text; // marked-highlight has already processed this 241 + }; 242 + 243 + // Set options and use the custom renderer 244 + marked.setOptions({ 245 + renderer: renderer, 246 + gfm: true, 247 + breaks: false, // This helps with multi-line list items 248 + pedantic: false, 249 + }); 177 250 178 - // Finally, restore code blocks from placeholders 179 - for (const { placeholder, replacement } of codeBlocks) { 180 - html = html.replace(placeholder, replacement); 181 - } 251 + // Parse markdown to HTML using marked (this handles multi-line list items properly) 252 + const html = await marked.parse(markdown); 182 253 183 254 return html; 184 255 } ··· 186 257 async function handleDocsIndex(request: Request): Promise<Response> { 187 258 const { currentUser } = await withAuth(request); 188 259 return renderHTML( 189 - <DocsIndexPage 190 - docs={AVAILABLE_DOCS} 191 - currentUser={currentUser} 192 - />, 260 + <DocsIndexPage docs={AVAILABLE_DOCS} categories={DOCS_CATEGORIES} currentUser={currentUser} /> 193 261 ); 194 262 } 195 263 ··· 221 289 // Convert to HTML with Shiki syntax highlighting 222 290 const htmlContent = await markdownToHtml(markdownContent); 223 291 292 + // Extract headers for table of contents 293 + const headers = extractHeaders(htmlContent); 294 + 224 295 return renderHTML( 225 296 <DocsPage 226 297 title={docInfo.title} 227 298 content={htmlContent} 299 + headers={headers} 228 300 docs={AVAILABLE_DOCS} 301 + categories={DOCS_CATEGORIES} 229 302 currentSlug={slug} 230 303 currentUser={currentUser} 231 - />, 304 + /> 232 305 ); 233 306 } 234 307
+39 -25
frontend/src/features/docs/templates/DocsIndexPage.tsx
··· 1 1 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 2 2 import { Layout } from "../../../shared/fragments/Layout.tsx"; 3 - import { Card } from "../../../shared/fragments/Card.tsx"; 4 3 import { Text } from "../../../shared/fragments/Text.tsx"; 5 4 6 5 interface DocItem { ··· 9 8 description: string; 10 9 } 11 10 11 + interface DocCategory { 12 + category: string; 13 + docs: DocItem[]; 14 + } 15 + 12 16 interface DocsIndexPageProps { 13 17 docs: DocItem[]; 18 + categories: DocCategory[]; 14 19 currentUser?: AuthenticatedUser; 15 20 } 16 21 17 - export function DocsIndexPage({ docs, currentUser }: DocsIndexPageProps) { 22 + export function DocsIndexPage({ docs, categories, currentUser }: DocsIndexPageProps) { 18 23 return ( 19 24 <Layout title="Documentation - Slices" currentUser={currentUser}> 20 - <div className="py-8 px-4"> 21 - <div className="mb-8"> 22 - <Text as="h1" size="3xl" className="font-bold mb-2"> 23 - Documentation 25 + <div className="py-8 px-4 max-w-6xl mx-auto"> 26 + <div className="mb-12"> 27 + <Text as="h1" size="3xl" className="font-bold mb-4"> 28 + Slices Documentation 24 29 </Text> 25 - <Text as="p" variant="secondary"> 26 - Learn how to build AT Protocol applications with Slices 30 + <Text as="p" size="lg" variant="secondary" className="leading-relaxed"> 31 + Learn how to build AT Protocol applications with Slices. These guides cover everything from basic concepts to advanced usage patterns. 27 32 </Text> 28 33 </div> 29 34 30 - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> 31 - {docs.map((doc) => ( 32 - <a 33 - key={doc.slug} 34 - href={`/docs/${doc.slug}`} 35 - className="block" 36 - > 37 - <Card padding="md" variant="hover"> 38 - <Text as="h2" size="xl" className="font-semibold mb-2"> 39 - {doc.title} 40 - </Text> 41 - <Text as="p" variant="secondary"> 42 - {doc.description} 43 - </Text> 44 - </Card> 45 - </a> 35 + <div className="space-y-16"> 36 + {categories.map((category) => ( 37 + <section key={category.category}> 38 + <Text as="h2" size="2xl" className="font-bold mb-8 text-zinc-900 dark:text-white"> 39 + {category.category} 40 + </Text> 41 + <div className="space-y-6"> 42 + {category.docs.map((doc) => ( 43 + <div key={doc.slug} className="border-b border-zinc-200 dark:border-zinc-700 pb-6"> 44 + <a 45 + href={`/docs/${doc.slug}`} 46 + className="block group hover:no-underline" 47 + > 48 + <Text as="h3" size="lg" className="font-semibold mb-3 text-blue-600 dark:text-blue-400 group-hover:text-blue-700 dark:group-hover:text-blue-300 transition-colors underline decoration-blue-600 dark:decoration-blue-400"> 49 + {doc.title} 50 + </Text> 51 + <Text as="p" variant="secondary" className="leading-relaxed text-base"> 52 + {doc.description} 53 + </Text> 54 + </a> 55 + </div> 56 + ))} 57 + </div> 58 + </section> 46 59 ))} 47 60 </div> 61 + 48 62 </div> 49 63 </Layout> 50 64 ); 51 - } 65 + }
+154 -61
frontend/src/features/docs/templates/DocsPage.tsx
··· 1 1 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 2 2 import { Layout } from "../../../shared/fragments/Layout.tsx"; 3 3 import { Text } from "../../../shared/fragments/Text.tsx"; 4 + import { Breadcrumb } from "../../../shared/fragments/Breadcrumb.tsx"; 4 5 5 6 interface DocItem { 6 7 slug: string; ··· 8 9 description: string; 9 10 } 10 11 12 + interface DocCategory { 13 + category: string; 14 + docs: DocItem[]; 15 + } 16 + 17 + interface HeaderItem { 18 + level: number; 19 + text: string; 20 + id: string; 21 + } 22 + 11 23 interface DocsPageProps { 12 24 title: string; 13 25 content: string; 26 + headers: HeaderItem[]; 14 27 docs: DocItem[]; 28 + categories: DocCategory[]; 15 29 currentSlug: string; 16 30 currentUser?: AuthenticatedUser; 17 31 } 18 32 19 - export function DocsPage( 20 - { title, content, docs, currentSlug, currentUser }: DocsPageProps, 21 - ) { 33 + export function DocsPage({ 34 + title, 35 + content, 36 + headers, 37 + docs, 38 + categories, 39 + currentSlug, 40 + currentUser, 41 + }: DocsPageProps) { 22 42 return ( 23 - <Layout title={`${title} - Slices`} currentUser={currentUser}> 24 - <div className="py-4 sm:py-8 px-4"> 25 - {/* Mobile navigation dropdown */} 26 - <div className="sm:hidden mb-6"> 27 - <label 28 - htmlFor="docs-nav" 29 - className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2" 30 - > 31 - Navigate to 32 - </label> 33 - <select 34 - id="docs-nav" 35 - className="block w-full px-3 py-2 text-base border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent" 36 - value={currentSlug} 37 - /* @ts-ignore - Hyperscript attribute */ 38 - _="on change set window.location to `/docs/${me.value}`" 39 - > 40 - {docs.map((doc) => ( 41 - <option 42 - key={doc.slug} 43 - value={doc.slug} 44 - selected={doc.slug === currentSlug} 45 - > 46 - {doc.title} 47 - </option> 48 - ))} 49 - </select> 50 - </div> 51 - 52 - <div className="flex gap-8"> 53 - {/* Desktop Sidebar */} 54 - <nav className="hidden sm:block w-64 flex-shrink-0"> 55 - <div className="sticky sm:top-[5rem]"> 56 - <Text as="h2" size="sm" className="font-semibold mb-4"> 57 - Documentation 58 - </Text> 59 - <ul className="space-y-1"> 60 - {docs.map((doc) => ( 61 - <li key={doc.slug}> 62 - <a 63 - href={`/docs/${doc.slug}`} 64 - className={`block px-3 py-2 text-sm rounded-md transition-colors ${ 65 - doc.slug === currentSlug 66 - ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white font-medium" 67 - : "text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-50 dark:hover:bg-zinc-800" 68 - }`} 69 - > 70 - {doc.title} 71 - </a> 72 - </li> 73 - ))} 74 - </ul> 75 - </div> 76 - </nav> 43 + <Layout 44 + title={`${title} - Slices`} 45 + currentUser={currentUser} 46 + > 47 + <div className="py-8 px-4 max-w-6xl mx-auto relative"> 48 + {/* Breadcrumb */} 49 + <Breadcrumb 50 + items={[ 51 + { label: "Documentation", href: "/docs" }, 52 + { label: title } 53 + ]} 54 + /> 77 55 78 - {/* Content */} 79 - <main className="flex-1 min-w-0 overflow-x-hidden"> 80 - <article className="prose prose-zinc dark:prose-invert max-w-none prose-sm sm:prose-base"> 56 + {/* Two-column layout */} 57 + <div className="flex gap-12"> 58 + {/* Main Content */} 59 + <main className="flex-1 min-w-0"> 60 + <article className="prose prose-zinc dark:prose-invert max-w-none"> 81 61 <div 82 - className="docs-content [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_pre]:border [&_pre]:border-zinc-200 dark:[&_pre]:border-zinc-700 [&_pre]:rounded-md [&_pre>code]:border-0 [&_:not(pre)>code]:border [&_:not(pre)>code]:border-zinc-200 dark:[&_:not(pre)>code]:border-zinc-700 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:px-1" 62 + className="docs-content [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_pre]:border [&_pre]:border-zinc-200 dark:[&_pre]:border-zinc-700 [&_pre]:rounded-lg [&_pre>code]:border-0 [&_:not(pre)>code]:bg-zinc-100 dark:[&_:not(pre)>code]:bg-zinc-800 [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:text-sm" 83 63 dangerouslySetInnerHTML={{ __html: content }} 84 64 /> 85 65 </article> 86 66 </main> 67 + 68 + {/* Right Sidebar - Table of Contents */} 69 + {headers.length > 0 && ( 70 + <aside className="hidden lg:flex w-64 flex-shrink-0 relative"> 71 + <div className="sticky top-1/2 -translate-y-1/2 w-64 max-h-[60vh] overflow-y-auto bg-zinc-50 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 shadow-sm"> 72 + <Text as="h3" size="sm" className="font-semibold mb-4 text-zinc-900 dark:text-white"> 73 + On This Page 74 + </Text> 75 + <nav> 76 + <ul className="space-y-1 text-sm" id="toc-nav"> 77 + {headers.map((header) => ( 78 + <li key={header.id}> 79 + <a 80 + href={`#${header.id}`} 81 + data-target={header.id} 82 + /* @ts-ignore - Hyperscript attribute */ 83 + _="on click call updateActiveTocLink(me) then on load call updateActiveTocOnScroll()" 84 + className={`block py-1.5 px-3 -mx-3 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800/50 ${ 85 + header.level === 1 86 + ? "text-zinc-900 dark:text-white font-medium" 87 + : header.level === 2 88 + ? "text-zinc-700 dark:text-zinc-300" 89 + : "text-zinc-600 dark:text-zinc-400 ml-2" 90 + } [&.active]:bg-blue-50 dark:[&.active]:bg-blue-950/50 [&.active]:text-blue-600 dark:[&.active]:text-blue-400`} 91 + > 92 + {header.text} 93 + </a> 94 + </li> 95 + ))} 96 + </ul> 97 + </nav> 98 + </div> 99 + </aside> 100 + )} 87 101 </div> 88 102 </div> 103 + 104 + {/* Add scroll tracking script */} 105 + <script 106 + dangerouslySetInnerHTML={{ 107 + __html: ` 108 + function updateActiveTocLink(clickedLink) { 109 + // Remove active from all TOC links 110 + document.querySelectorAll('#toc-nav a').forEach(link => { 111 + link.classList.remove('active'); 112 + }); 113 + // Add active to clicked link 114 + clickedLink.classList.add('active'); 115 + } 116 + 117 + function updateActiveTocOnScroll() { 118 + const headers = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')); 119 + const tocLinks = document.querySelectorAll('#toc-nav a[data-target]'); 120 + 121 + function updateActiveHeader() { 122 + let activeHeader = null; 123 + 124 + // Find the header that's currently in view 125 + for (let i = headers.length - 1; i >= 0; i--) { 126 + const header = headers[i]; 127 + const rect = header.getBoundingClientRect(); 128 + 129 + // If header is above the top quarter of the viewport, it's the active one 130 + if (rect.top <= window.innerHeight / 4) { 131 + activeHeader = header; 132 + break; 133 + } 134 + } 135 + 136 + // Update TOC links 137 + tocLinks.forEach(link => { 138 + link.classList.remove('active'); 139 + if (activeHeader && link.dataset.target === activeHeader.id) { 140 + link.classList.add('active'); 141 + 142 + // Scroll the active link into view within the TOC container 143 + const tocNav = document.querySelector('#toc-nav'); 144 + const tocContainer = tocNav.closest('.overflow-y-auto'); 145 + 146 + if (tocContainer && tocContainer.scrollHeight > tocContainer.clientHeight) { 147 + // Get the position of the active link relative to the scrollable container 148 + const containerRect = tocContainer.getBoundingClientRect(); 149 + const linkRect = link.getBoundingClientRect(); 150 + 151 + const isAbove = linkRect.top < containerRect.top + 40; // 40px buffer from top 152 + const isBelow = linkRect.bottom > containerRect.bottom - 40; // 40px buffer from bottom 153 + 154 + if (isAbove || isBelow) { 155 + // Calculate scroll position to center the link 156 + const linkOffsetTop = link.offsetTop; 157 + const containerHeight = tocContainer.clientHeight; 158 + const targetScrollTop = linkOffsetTop - (containerHeight / 2); 159 + 160 + tocContainer.scrollTo({ 161 + top: Math.max(0, targetScrollTop), 162 + behavior: 'smooth' 163 + }); 164 + } 165 + } 166 + } 167 + }); 168 + } 169 + 170 + // Update on scroll 171 + window.addEventListener('scroll', updateActiveHeader); 172 + 173 + // Update on page load 174 + updateActiveHeader(); 175 + } 176 + 177 + // Initialize scroll tracking when page loads 178 + document.addEventListener('DOMContentLoaded', updateActiveTocOnScroll); 179 + `, 180 + }} 181 + /> 89 182 </Layout> 90 183 ); 91 184 }
+96
frontend/src/features/docs/templates/fragments/CodeBlock.tsx
··· 1 + import { Copy } from "lucide-preact"; 2 + import { ComponentChildren, VNode, FunctionComponent } from "preact"; 3 + 4 + interface CodeBlockProps { 5 + children: ComponentChildren; 6 + } 7 + 8 + interface CodeBlockHeaderProps { 9 + children: ComponentChildren; 10 + } 11 + 12 + interface CodeBlockCodeProps { 13 + highlightedCode: string; 14 + } 15 + 16 + // No props needed for CopyButton, it finds the code automatically 17 + 18 + function CodeBlockRoot({ children }: CodeBlockProps) { 19 + // Check if there's a header in the children 20 + const childrenArray = Array.isArray(children) ? children : [children]; 21 + 22 + const hasHeader = childrenArray.some((child) => { 23 + const vnode = child as VNode; 24 + const component = vnode?.type as FunctionComponent; 25 + return ( 26 + component === CodeBlockHeader || component?.name === "CodeBlockHeader" 27 + ); 28 + }); 29 + 30 + // Clone children and pass hasHeader context to CodeBlockCode components 31 + const enhancedChildren = childrenArray.map((child) => { 32 + const vnode = child as VNode; 33 + const component = vnode?.type as FunctionComponent; 34 + 35 + if (component === CodeBlockCode || component?.name === "CodeBlockCode") { 36 + return { 37 + ...vnode, 38 + props: { 39 + ...vnode.props, 40 + _hasHeader: hasHeader, 41 + }, 42 + }; 43 + } 44 + return child; 45 + }); 46 + 47 + return ( 48 + <div class="my-4 border border-zinc-200 dark:border-zinc-700 rounded-md"> 49 + {enhancedChildren} 50 + </div> 51 + ); 52 + } 53 + 54 + function CodeBlockHeader({ children }: CodeBlockHeaderProps) { 55 + return ( 56 + <div class="bg-zinc-100 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 rounded-t-md font-mono flex justify-between items-center"> 57 + {children} 58 + </div> 59 + ); 60 + } 61 + 62 + function CodeBlockCopyButton() { 63 + return ( 64 + <button 65 + type="button" 66 + // @ts-ignore Hyperscript attribute 67 + _="on click writeText(the textContent of the first <pre/> in the nextElementSibling of my parentElement) to the navigator's clipboard" 68 + class="text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 p-1 rounded transition-colors" 69 + title="Copy code" 70 + > 71 + <Copy size={16} /> 72 + </button> 73 + ); 74 + } 75 + 76 + function CodeBlockCode({ 77 + highlightedCode, 78 + _hasHeader, 79 + }: CodeBlockCodeProps & { _hasHeader?: boolean }) { 80 + const className = _hasHeader 81 + ? "[&_pre]:pt-4 [&_pre]:pb-4 [&_pre]:px-4 [&_pre]:rounded-none [&_pre]:rounded-b-md [&_pre]:overflow-x-auto [&_pre]:text-sm [&_pre]:border-0" 82 + : "my-4 [&_pre]:p-4 [&_pre]:rounded-md [&_pre]:overflow-x-auto [&_pre]:text-sm"; 83 + 84 + return ( 85 + <div 86 + class={className} 87 + dangerouslySetInnerHTML={{ __html: highlightedCode }} 88 + /> 89 + ); 90 + } 91 + 92 + export const CodeBlock = Object.assign(CodeBlockRoot, { 93 + Header: CodeBlockHeader, 94 + Code: CodeBlockCode, 95 + CopyButton: CodeBlockCopyButton, 96 + });
+118 -2
frontend/src/shared/fragments/Layout.tsx
··· 56 56 <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> 57 57 <script src="https://cdn.tailwindcss.com/3.4.1"></script> 58 58 <script src="https://unpkg.com/lucide@latest"></script> 59 + <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> 59 60 {Deno.env.get("DENO_ENV") === "production" && ( 60 - <script data-goatcounter="https://slices.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script> 61 + <script 62 + data-goatcounter="https://slices.goatcounter.com/count" 63 + async 64 + src="//gc.zgo.at/count.js" 65 + ></script> 61 66 )} 62 67 <style 63 68 dangerouslySetInnerHTML={{ 64 69 __html: ` 70 + /* Scroll padding for anchor links to account for sticky navigation */ 71 + html { 72 + scroll-padding-top: 5rem; 73 + } 65 74 66 75 .htmx-indicator { 67 76 display: none; ··· 76 85 } 77 86 78 87 /* Shiki dual theme support */ 88 + .shiki { 89 + background-color: #fafafa !important; /* zinc-50 */ 90 + } 91 + 79 92 @media (prefers-color-scheme: dark) { 80 93 .shiki, 81 94 .shiki span { ··· 86 99 `, 87 100 }} 88 101 /> 102 + <script 103 + dangerouslySetInnerHTML={{ 104 + __html: ` 105 + let isClickScrolling = false; 106 + 107 + function updateActiveTocLink(clickedLink) { 108 + // Remove active class from all TOC links 109 + const tocLinks = document.querySelectorAll('#toc-nav a'); 110 + tocLinks.forEach(link => link.classList.remove('active')); 111 + 112 + // Add active class to clicked link 113 + clickedLink.classList.add('active'); 114 + 115 + // Prevent scroll observer from interfering for a short time 116 + isClickScrolling = true; 117 + setTimeout(() => { 118 + isClickScrolling = false; 119 + }, 1000); 120 + } 121 + 122 + // Set up scroll tracking when page loads 123 + document.addEventListener('DOMContentLoaded', function() { 124 + const tocNav = document.querySelector('#toc-nav'); 125 + if (!tocNav) return; 126 + 127 + function updateActiveHeaderOnScroll() { 128 + if (isClickScrolling) return; // Don't update if user just clicked 129 + 130 + const headers = document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'); 131 + let activeHeader = null; 132 + 133 + // Find the header that's currently most prominent in the viewport 134 + for (const header of headers) { 135 + const rect = header.getBoundingClientRect(); 136 + // If header is above the fold but close to top, or visible in upper part of viewport 137 + if (rect.top <= 150) { 138 + activeHeader = header; 139 + } else { 140 + break; // Stop at the first header that's too far down 141 + } 142 + } 143 + 144 + if (activeHeader) { 145 + const tocLinks = document.querySelectorAll('#toc-nav a'); 146 + tocLinks.forEach(link => link.classList.remove('active')); 147 + const activeLink = document.querySelector('#toc-nav a[data-target="' + activeHeader.id + '"]'); 148 + if (activeLink) activeLink.classList.add('active'); 149 + } 150 + } 151 + 152 + // Listen to scroll events 153 + window.addEventListener('scroll', updateActiveHeaderOnScroll, { passive: true }); 154 + 155 + // Set initial active header 156 + setTimeout(updateActiveHeaderOnScroll, 100); 157 + }); 158 + 159 + // Initialize Mermaid diagrams with theme support 160 + if (typeof mermaid !== 'undefined') { 161 + // Detect if dark mode is preferred 162 + const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 163 + 164 + if (isDarkMode) { 165 + mermaid.initialize({ 166 + startOnLoad: true, 167 + theme: 'dark', 168 + themeVariables: { 169 + primaryColor: '#3b82f6', 170 + primaryTextColor: '#f9fafb', 171 + primaryBorderColor: '#6b7280', 172 + lineColor: '#9ca3af', 173 + secondaryColor: '#374151', 174 + tertiaryColor: '#1f2937', 175 + background: '#111827', 176 + mainBkg: '#1f2937', 177 + secondBkg: '#374151', 178 + tertiaryBkg: '#3b82f6' 179 + } 180 + }); 181 + } else { 182 + mermaid.initialize({ 183 + startOnLoad: true, 184 + theme: 'default', 185 + themeVariables: { 186 + primaryColor: '#3b82f6', 187 + primaryTextColor: '#1f2937', 188 + primaryBorderColor: '#6b7280', 189 + lineColor: '#6b7280', 190 + secondaryColor: '#f3f4f6', 191 + tertiaryColor: '#f9fafb', 192 + background: '#ffffff', 193 + mainBkg: '#f9fafb', 194 + secondBkg: '#f3f4f6', 195 + tertiaryBkg: '#dbeafe' 196 + } 197 + }); 198 + } 199 + } 200 + 201 + `, 202 + }} 203 + /> 89 204 </head> 90 205 <body className="min-h-screen bg-white dark:bg-zinc-900 dark:text-white"> 91 206 {showNavigation && <Navigation currentUser={currentUser} />} ··· 121 236 <footer className="bg-white dark:bg-zinc-900 py-6 px-6"> 122 237 <div className="flex items-center justify-center gap-4 opacity-20 hover:opacity-100 transition-opacity duration-300"> 123 238 <Text variant="secondary" size="sm"> 124 - {new Date().getFullYear()} Slices Network. All rights reserved. 239 + {new Date().getFullYear()} Slices Network. All rights 240 + reserved. 125 241 </Text> 126 242 {currentUser?.isAuthenticated && ( 127 243 <>
+3 -6
frontend/src/shared/fragments/Text.tsx
··· 28 28 | "h5" 29 29 | "h6" 30 30 | "dt" 31 - | "dd"; 31 + | "dd" 32 + | "strong"; 32 33 33 34 interface TextProps { 34 35 variant?: TextVariant; ··· 84 85 className 85 86 ); 86 87 87 - return ( 88 - <Component className={classes}> 89 - {children} 90 - </Component> 91 - ); 88 + return <Component className={classes}>{children}</Component>; 92 89 }