+681
-18
deno.lock
+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
+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
+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
-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
+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
+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
+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
+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
+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
+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
+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
+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
+
});