diff --git a/.gitignore b/.gitignore
index dc85509..4340a41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,4 +34,5 @@ src/domain/diagnostics/
*.sln
*.sw?
Puzzlink_Assistance.js
-docs/legacy/*
\ No newline at end of file
+docs/legacy/*
+.plugin/
\ No newline at end of file
diff --git a/README.md b/README.md
index f312e5f..c5dd4c4 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,11 @@ The goal is not only to produce a final answer. The app shows each deduction ste
- Runs entirely in the browser.
- Imports Slitherlink and Masyu puzzles from puzz.link-compatible URLs and Penpa+ URLs.
-- Provides an explainable solver workspace with step replay, highlights, rule messages, and live stats.
+- Provides an explainable solver workspace with step replay, highlights, rule messages, strong-inference branch inspection, and live stats.
- Provides an editor workspace for building Slitherlink and Masyu boards, importing URLs, and loading the result into the solver.
- Provides a small Dataset page for curated examples and benchmark-oriented puzzle sets.
- Exports supported puzzle states back to puzz.link-style URLs where available.
+- Opens canonical puzz.link payloads directly in Solver through shareable deep links.
Slitherlink has the more mature rule stack. Masyu is now a first-class puzzle family with editor, import/export, rendering, replay, completion analysis, and an actively evolving solver.
@@ -44,9 +45,22 @@ pnpm benchmark:solve
## App Surfaces
-- **Solver** (`/`): import a puzzle, run one deduction at a time, jump through the replay timeline, inspect explanations, view stats, and export supported states.
+- **Solver** (`/`): import a puzzle, run one deduction at a time, jump through the replay timeline, inspect explanations and strong-inference branches, view stats, and export supported states.
- **Editor** (`/editor`): create custom Slitherlink or Masyu grids, edit clues/pearls and marks, import supported URLs, then hand the puzzle to the solver.
- **Dataset** (`/dataset`): browse curated public dataset manifests, preview puzzles, and load them into Solver or Editor.
+- **Docs** (`/docs`): browse puzzle-family rule documentation and open the explanation for any solver step.
+
+## Solver Deep Links
+
+Solver accepts canonical puzz.link-compatible payloads through the `p` query parameter:
+
+```text
+https://smilingwayne.github.io/puzzlekit-web/?p=slither/10/10/...
+https://smilingwayne.github.io/puzzlekit-web/?p=mashu/14/8/...
+```
+
+Deep links support the compact `slither/...` and `mashu/...` payload forms only. Full puzz.link,
+pzplus, pzv, and Penpa+ URLs remain supported through the existing manual URL import controls.
## Architecture Snapshot
@@ -60,6 +74,7 @@ dataset/
public/ small committed dataset manifests
private/ local-only manifests ignored by git
docs/
+ content/ in-app MDX rule documentation
techniques/ puzzle-specific technique notes
```
diff --git a/dataset/public/masyu.json b/dataset/public/masyu.json
new file mode 100644
index 0000000..4a17353
--- /dev/null
+++ b/dataset/public/masyu.json
@@ -0,0 +1,503 @@
+{
+ "schemaVersion": 1,
+ "id": "masyu",
+ "title": "Masyu Dataset",
+ "puzzleType": "masyu",
+ "items": [
+ {
+ "id": "masyu-25x25-0001",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/i0c11020i3030320006009020i9361i9c93i9i03910g50691230000110010a41e3b6aa0d0001010900c90200329940b7930006k009j66901390000109263cj6390009i0i6013003900ma0330220130990013360b7ia009i60001692009171a301399a0902010900ii",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-0002",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/i39001003216230092091006l309079931106ff090000409b9iik01093900010a00i9706i0c001193601ai3a46900b36i0i6a0030j0i003913100ic0a0a0ai633090i011303l099030f23i600630600inc00i0m090f00000b3032i01i11096b029120119i0i009000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-9899866",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/3i09210010909039900i0b20i36a401039300c311a3b00371ic09k231000ic06300ak0a00a300080ai210020a9f213i370031000a0n1000a91003bb000930a03k101c09ic03i02a001000030a111fc910c9i9021010k139f196000003032ai2f0i0919193009i0100",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-4265995",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/j00b0200019i0c079a620306900409900530001c1010c101630304000923596m9a03020i0f3110903a600b98010335022300090i002600393c33l66009800f2130i0300260id0i1103901043l00310050939334009909c01009d3091200l090c20i349k0363000300",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-1088348",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/00i0i00j1901069099l22006000900300300aii609b900006002io69620910i0109c40i4610030i12133a300030l10161000c0430222040220000200f1c6b30500a0i1ga011i063j3601000366i32109039800370i0006904223090a03902099101036b04209i0210",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-6888877",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/00909310ij929900000c10f3c4i91060030cf3097000001904m090a3300b13i020i0900b210ii3a06000991f030000c9496b50309aa30a2011023399d360020i9003601209006i6a009o600609j91963ii1930i000309g03ij03i01990004001363319203016f0o0i",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-5468112",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/ij909631000a133339330003i0j0306a0294000a06090l094007632063ai93k00030100b643402f9103022011034050a49991900005003cdf330l00100000a030j603920i9039150620i40i000920093660202000i1609b6i06161090009091703ij31132i0046000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-4003665",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/2010ij00l43i1001000020i003j03127i30060003l23004600109i102203918330i91600510a3k310c06l0301326000962902110019909031i1i60i9i9000663i030169b0io9c301a20013301001300660k0000960600370060a0i01906003i02103309i00203i0i0",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-2944455",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/00i903990300191400001b0i068279130i019c0009i4001090300il1040a9000dj0100929101cfj1040009332106i410000310360a691216b9009300a0959031a31a00100190i0i5030900i0g00064120b140010610915c4396811000i00i990100i0099i0800609i",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-9566366",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/i0i9f90a010a0a103130101793004110001330c9c0k00180i05i36100400130i304920390220f196c63060000k209393103c1b369193400000900060l60630902i0116jib0b34022003009010016200j0092010066b26399d306000i120009i1i0900i0j000b62000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-587855",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/070606i63939100000911020349c10b0a1012206i0200i002111043606100i10001200631f101106609026000611k04ki220003023011a10c0700i3k9293b0001100110m0k2k00301i9030103i169203i13929600a051001903331131m39011340331010i00921i10",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-6857349",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0000009903069609ac4906609000i03ii099i3110j010030103022i0c004c9ik004i009000k009i1ia00c9131009214302c3033006b9413909000413600b0300a321221009i02002600i009900209900069609643il315090b0i696100039010b0f01060020960700",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-3231926",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/i13000010099b129060a660d3ii000301005103o098100j03900063331630620110409i113c05b006005061300j11106210i003006k0aa923620660j900000i006i0ic0209k1060if991j060606003090240k020a26a2030900020000096236061000006i310k9000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-4088053",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0001009a3024117197200610000210a141106900i309j96g3a0i020000009i0c7aa6100j0i031000010903c6b30939a0i1a003960f93260a29960060003i330kbb9001a01302361l30c9i603061300009i303390i0003496i3101000091b6b0090f0093922103060i",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-20x20-6613546",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/20/20/i000006303j0215600c0031070269c30i0009069i200o99213300c4004c9100l000610j96i900366k3991300969a1010c200j0c009i00190000b01606j1006c0904000",
+ "width": 20,
+ "height": 20,
+ "tags": ["hard", "Medium"],
+ "source": "https://zh.puzzle-masyu.com/?size=12"
+ },
+ {
+ "id": "masyu-20x20-8268975",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/20/20/09o5020c000a410090a30f433030161091i1070000b00343l03bj3l9c0o20900000920k0756030l036221099060330300110325b11g1303c909e9l09091300ef000960",
+ "width": 20,
+ "height": 20,
+ "tags": ["hard", "Medium"],
+ "source": "https://zh.puzzle-masyu.com/?size=12"
+ },
+ {
+ "id": "masyu-25x25-5562527",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/310j2i003301220o939e0094010096201303fk11i39000300016ij00g900031i0iji0in04i33a001m0904206000d3i60399600230b1211300i039201340c39j1i32g06091a004302696712i011500007900603l191100000069ilji0c09290100a0000030i323k690",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-8964356",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/30109306i8c309300100016c1i026i00c60c00069060ai15610200200101i09l0i430aj012000000320i60021003i36600c3108939lc630091930l013310903m0333093c109f60c1i800160j001g10100d4106i1ii00k9001011001090930091010161002i191060i",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-3874630",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/00i090333709c402003010b9000f01010509ci08130i3i300940ji6969i0203i16069009600i90j030m00900di31i9b2020001a0003k06i1l60200000c3j030400003e6316f6c1031c060a0060901900j00k1022009c1a0006o031ica3003600a0a39990010090170",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-6113625",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/1006e10003209011513910ia000f02130f0200a620a30c6002090i9i0210l21010030030fa006f33ii3lc069900061909509110a207034lb6030i33116j00000904303811629d332i0i090031013j0ic00c00989040o6901600333i10199a03036430c14l00i00200",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-7453852",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/010623k3341003009003f3963093090i03339000i0i013010i63c3df3100a20002030904090000i2109ii3096i90060900960820j9a009276633b30090k010039i0904fi0i64f600406i10i400003101036001300l20i90190i142119i90020l900230c0369961000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-6744156",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/61900081000i19010062a3d1040i009039ca000a1110143fi300030100a20a0119900i6l01c0kb915935060150310c330093001l609i90k000fi0b009bc2033000000c9063i3i00k41000032610040960j0di2900396200i919100fi310602000209i210i3000i910",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-8512608",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0023096020c0a000903404i0b93i1l96i03200660b100010i0000ga300043961649901090000b9108810306090609761109090633601196910660001610i664200a3a0096003391069j0106069003k09604209200f007111301c900100i03a0009a01a0a33i0o00i0",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-879127",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/j20160902001030030330a1632051021006a9e26f00003001310100i0003111213916334133002314i99c6000410116091000005030k010ij1200i333091c011c0c9j4f1i400009030420903m00f9221i0c2090903130o00k0o03200000000a900019f607o1910060",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-5584744",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/4000002j01b905300406139330161k4322093c329k0030020339c707a21100100101700431051b0m035910000230925li4330000110060g0b1370k041133000i0i0i0335b09k6k3b0009913000i6b0000j3903002b00i7ak250i9300300230369i1a093a311102000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-8788377",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/i101013003ic1300300a9090ii900ic9i910o2012010020c002601093f072909c60093202021163b01kf90ii100011003i333o930300000i6k5610009ii02633009a01660i0010003092022c339ca4190300033043afj9c603j30600390060390026000910i001010",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-0003",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0003090019033936204i01402a1i9i1631009c2099190633a060200006020219f90i0a00i900737020009i00304b3009l0603620a300902f0c0151a0i93c01032803g003300603da03003000003ia0239i06303082033630002f0f263090k6039j0i90b0000039000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-225588",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0i60030000a0c13a3i00o3011126300il390a220300302313i0ib620163397006006a03103i6039ii93001003416133h29701i000033320ji0590ia003j01000bf0j40i9f0400900l101a013i34i0b1l16900i0030k919900b001001196013973390790c300030000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-78337",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/3110030i0690f9009934946j3003001i3313jk00993c12112a020l10ia033933c060020110900c03il00k0c6i09303040919ff62an3000f30010300j3a3i00j00i0276i0c0a390b11003192e03132093f05i099901690l0a0019295a13a10000130a9790l30i0i0k0",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-1228297",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/100311i01ia2000f32001936096f17ii013010i01504900030706360i690a003209a139c0i00800i303f006bi0300030223bii309030020c99i0a3190a0b903n3610a0a3010343903b203005310o69i50a300i3l0c9310033005c3f2029000001020600303610b2i0",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-4221753",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/000100i0010209096k0a09i006000l6i301a000i600c6930ii010i000352b0ca9160000339090ibi093o0i0009300k0faa733i06i36100i9030620204413i901040i0140630i509e033k00900010c000009001b04al00i0a000a2030ca9003001006833930jj0003i",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-988309",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/3020i23i00060010c3c006000i00e096603c0009093909j010f003304293ki1190000439930619021060900930a0c06190111441030300230k76ia3962290444050a0000c0c0000c13002i19141120106l03900a603i3b70900190i690j002009ci0300c000009600",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-2408667",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/2090903il00l000001909i90c03aik56017001000j010i02000100c900o06i0056023010i000063320g500j3i0600a0023a0199360603001660109000112212kii1000a0030a200j3221301006060c603163i15063jl02106301a6000930j0i930913903391600b00",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-9603489",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/0i0000906iic9001000003kf0041i6203100b506a0099060001j6940kb091091001216000060903k23i00k0003i3d01k39100000000026909o96301f0j1300f6091100423i1039c3006902030k34l3002900000a91060c902000ki0633kai20a0109400i0j0000030",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-5042658",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/109210103009010c39c0910999f6e6l1109i001000001ic23a6if003611260i11m099336a0c911000100a92906266910m96011102109010c91111i109c00111a062139002030923c1606391060696010j13990666i330f3000606a210006009021109613002006000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-0004",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/000k99o9301091009ca0030235019119107b0f0009101704i0ii90200i10b6im0a911a03i11130095631409o4006c17600270a00026000e1140a0fa2301011i006ca309641i0a03137309099362gia0100692004a140096200909l0i0b001ia9c400j3013036c0310",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-25x25-5809037",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/25/25/06016020j9903210090c0000i11020031022j030b130i106070c0360i0109f0200c310061209i000a000i0i603i09810160a0a032090141c210l03393i6070303021320l943010000j001aa9a02ii11f00010920a0l60i9093310jj06f1301001309c30a300010000",
+ "width": 25,
+ "height": 25,
+ "tags": ["hard", "Large"],
+ "source": "https://zh.puzzle-masyu.com/?size=18"
+ },
+ {
+ "id": "masyu-15x21-0005",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/21/15/100300030000b000i239009203036040ii006i600131090i0000b0330k2062103360996091020910ik30096002121000010i0a109",
+ "width": 21,
+ "height": 15,
+ "tags": ["easy", "Medium"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0006",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/09a1010a6c09900914690001300106ib00",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0007",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/3k6100000i0mj03381103mi090000313j0",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0008",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/0000c9ia3000cb2000030dk0100c060330",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0009",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/0060jc0000010f90c2099010a6j0000990",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0010",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/00106020b6130000211139000ka3313390",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-25x40-0011",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/40/25/13j0j0133913i0000144001960a3k3i309cc2030000093190i11g3k3j3c30c9920900009ac110i10k6k33a040g99290000331b011i26b6j0430136090000341102030lb6a3a130i1060000193940a2036303300330i103i03i0690609200a00023o030i0021210003160023112191a5200j6i66308000006000006c0i0o80116312006c00i1209j31b0020i6369i000i20i6600000140cj000030j3i9000900039209019c00i0i",
+ "width": 40,
+ "height": 25,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-25x40-0012",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/40/25/13j0j0133913i0000144001960a3k3i300cc2030000093190i1173k3j3930c9920900009ac110i10k6k33a040g99290000331b011i26b6j0430106090000341102030lb6a3a130i1060000193940a2036303300330i103i03i0690609200a00023o030i0021210003160023112191a5200j6i663080000i6000006c0i0i20116312006c02i1209j31b002006369i000i20i6600000140cj000030j3i9000900039209019c00i0i",
+ "width": 40,
+ "height": 25,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-39x49-0013",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/49/39/0000000000i000000c63k0cj04962g6a430910i06390300109i20609090i30106000300400j00i100940iib01303c0646306110306j0010900f0306409064270i30112300030900000006a000390062216j09903i606230126c93a600000114000093009j63603004000040090099l0c919j00j41000l0343902030000k10963023990i0cia390399c02069200300930613i10013j0199ib0c00000460090a000i3j6iii013i0i1232090900c06960b00i323020000209j0909900996b690006463003k090396430000219900b02091610390021300l00c61a420b039i310201003030399010210i53026b690030a061132031003262120210a0ia30i30009190i3600601990300c00i30c31k0a203c019a0000090613ii00c26b0j206i0900130300093030023i09ic3b33b10i39310ia00030090060930000000130k090",
+ "width": 49,
+ "height": 39,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x18-0014",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/18/10/09i9092000l00a19009009061cc0i0000329111300i200l600b9000c0133",
+ "width": 18,
+ "height": 10,
+ "tags": ["easy", "Medium"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-12x14-0015",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/14/12/00011c336369i9m212203333050bbf0036600ig11220333309ii9if0",
+ "width": 14,
+ "height": 12,
+ "tags": ["easy", "Medium"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0016",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/090003ii40340009080c01039960334010",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-10x10-0017",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/10/i300060b0i30003fa000124090099109i0",
+ "width": 10,
+ "height": 10,
+ "tags": ["easy", "Small"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-20x29-0018",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/29/20/3026k40306010000c200o0j3i0a00900003a307006a60300009000000126k6k6n6k00000000003i0j0jcj3i310000010060616d6a6000000000326k6k6k6k0040000000209i3i09010091030a61636963i300030002c003026k000490j00031600",
+ "width": 29,
+ "height": 20,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-13x18-0019",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/18/13/1000000a23030000006202k00002i30o601310090010c1120921601309090b0313632039009200",
+ "width": 18,
+ "height": 13,
+ "tags": ["easy", "Medium"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-40x40-0020",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/40/40/il0020010160i003ol0c3i00606900010o0203301632010100901206600900i5ef003193cii9000900106990046091191603c6i021201a69k01i23390b963i00i0630i010000o06f00af06106139302002060i0099211a1000i0j0a3ia03321562309i30016023003420000a0k90ii00190200i0fj603j06211i09190f0i9024029710i6230010000000009b0b3096f062j31300a09909060a00i04101920090626b0690020913320101iib31620200859a901b6119a901093a300a0000610310100226210k1931203605000001911030i10m23920a2010cc00200000a63i900l61c6201923i1a9036k090090j6039000a010ji0611921006i020i019ii0i04a90039c102i6000o3390000",
+ "width": 40,
+ "height": 40,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-30x30-0021",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/30/30/i00000300009133i13213a699001c3i903b3k00062633311920i30391320000313a0633621005b003i030601a9333i0i4011903060c39939002i01i0020b19193703o1c006i0109206i00049094aaj390160601090310i31021606109a5020c0i09c9000430903607c0006a901200100000i00a1a1k10263030360c21020i000l0061606c963200bk11300090403006003i000i126ii",
+ "width": 30,
+ "height": 30,
+ "tags": ["easy", "Large"],
+ "source": "example.txt"
+ },
+ {
+ "id": "masyu-15x10-0022",
+ "puzzleType": "masyu",
+ "sourceUrl": "https://puzz.link/p?mashu/10/15/39000c0966103093ibf40d3262003j31008060003l03990030",
+ "width": 10,
+ "height": 15,
+ "tags": ["easy", "Medium"],
+ "source": "example.txt"
+ }
+ ]
+}
diff --git a/docs/MASYU_AGENT_BRIEF.md b/docs/MASYU_AGENT_BRIEF.md
index 1de144a..32864d4 100644
--- a/docs/MASYU_AGENT_BRIEF.md
+++ b/docs/MASYU_AGENT_BRIEF.md
@@ -10,10 +10,10 @@ Masyu is a first-class puzzle family with:
- Penpa+ import.
- Solver rendering, replay, explanations, stats, legend/help content, and completion analysis.
- Editor support for blank custom grids, URL import, black/white pearl placement, and load-to-solver flow.
-- Deterministic rules plus bounded, explainable strong-inference rules.
+- Deterministic rules plus bounded strong-inference rules with branch replay and contradiction focus.
- Tile-color topology support for inside/outside reasoning.
-Public Masyu dataset manifests are not yet committed to `dataset/public`; use private manifests for local experiments until curated public examples are added.
+A categorized Masyu manifest is committed to `dataset/public`; use private manifests for additional local experiments.
## Canonical Model
@@ -40,7 +40,8 @@ Implemented rule areas include:
- Candidate bridge reasoning and pearl candidate pruning.
- Adjacent white pearl lookahead.
- Tile color propagation, color-line implications, color-pearl implications, and connectivity cut coloring.
-- Bounded strong inference for black pearls, white pearls, and empty cells.
+- Bounded strong inference for black pearls, confirmed-line component endpoints,
+ and white pearls.
Default expectation: deterministic rules run first, then bounded strong-inference rules reuse the deterministic rule list for local propagation.
@@ -59,7 +60,7 @@ Use these files first:
- Lookahead helpers: `src/domain/rules/masyu/rules/lookahead*.ts`
- Masyu parser/exporter: `src/domain/parsers/puzzlink/masyuPuzzlink.ts`, `src/domain/parsers/penpa/index.ts`
- Masyu editor: `src/features/editor/MasyuEditorBoard.tsx`, `src/features/editor/editorStore.ts`
-- Tests: `src/domain/rules/masyu/rules.test.ts`, `src/domain/ir/masyu.test.ts`, parser tests
+- Tests: `src/domain/rules/masyu/tests/`, `src/domain/rules/masyu/completion.test.ts`, `src/domain/ir/masyu.test.ts`, parser tests
Replay and rendering plumbing:
@@ -67,6 +68,7 @@ Replay and rendering plumbing:
- Diff application: `src/domain/rules/engine.ts`
- Solver timeline/checkpoints: `src/features/solver/solverStore.ts`
- Board rendering: `src/features/board/CanvasBoard.tsx`
+- Strong-inference inspection: `src/features/explanation/BranchInspector.tsx`
## Development Direction
@@ -74,7 +76,7 @@ Near-term Masyu work should favor small, maintainable rule improvements over bro
- Consolidate repeated line, tile, candidate, and graph primitives when duplication becomes costly.
- Prefer named deterministic rules before adding broader assumption search.
-- Keep bounded inference explainable through contradiction or common-conclusion messages.
+- Keep bounded inference explainable through structured trial traces, contradiction focus, and formal conclusions.
- Add focused fixture tests whenever a rule changes.
Historical Masyu planning notes live under `docs/legacy/`. They are reference material, not the current source of truth.
@@ -90,7 +92,7 @@ Historical Masyu planning notes live under `docs/legacy/`. They are reference ma
Useful commands:
```bash
-pnpm test:run src/domain/rules/masyu/rules.test.ts
+pnpm test:run src/domain/rules/masyu/tests
pnpm test:run src/domain/rules/engine.test.ts src/features/solver/solverStore.test.ts
pnpm build
```
diff --git a/docs/PROJECT_GUIDE_EN.md b/docs/PROJECT_GUIDE_EN.md
index 0232572..f66ebff 100644
--- a/docs/PROJECT_GUIDE_EN.md
+++ b/docs/PROJECT_GUIDE_EN.md
@@ -34,6 +34,7 @@ dataset/
scripts/
benchmark-solve.ts
docs/
+ content/ # in-app MDX rule documentation
techniques/ # puzzle-specific technique notes
legacy/ # old plans and research notes
```
@@ -57,6 +58,11 @@ Boundary rule:
This contract is central: solver output must remain deterministic, replay-safe, and explainable.
+Bounded inference steps may also store structured branch details: the base
+puzzle, initial branch diffs, trial trace, contradiction focus, and formal
+conclusion. The shared Branch Inspector replays these details for Slitherlink
+and Masyu without changing solver behavior.
+
## 4. Plugin Contract
Puzzle families are registered in `src/domain/plugins/registry.ts`.
@@ -69,6 +75,7 @@ Each `PuzzlePlugin` may provide:
- `help` for the puzzle rules popout.
- `legend` for board legend examples.
- `getStats(puzzle)` for compact board-title stats.
+- `liveStats` for puzzle-specific inference coverage series shown in the shared Live Stats panel.
- `displayOptions` for puzzle-specific board toggles.
Current families:
@@ -105,16 +112,17 @@ Implemented:
- Editor workspace for Slitherlink and Masyu custom boards, URL import, and load-to-solver flow.
- Dataset page with filters, previews, and load-to-Solver/Editor actions.
- Plugin-powered rules help, board legend, board display toggles, and compact puzzle stats.
+- In-app rule documentation generated from the production rule registry, with stable rule deep links and read-only Canvas examples.
- Slitherlink puzz.link-compatible parse/encode, Penpa+ import, deterministic rules, branch-based inference, completion analysis, editor tools, and public dataset example.
-- Masyu puzz.link-compatible parse/encode, Penpa+ import, renderer, editor, deterministic rules, completion analysis, tile-color propagation, candidate pruning, bridge reasoning, premature-loop prevention, and bounded strong inference.
+- Masyu puzz.link-compatible parse/encode, Penpa+ import, renderer, editor, deterministic rules, completion analysis, tile-color propagation, candidate pruning, bridge reasoning, premature-loop prevention, and inspectable bounded strong inference.
- Public/private benchmark manifest workflow.
- GitHub Pages release workflow for tagged builds.
Partial or planned:
-- Public Masyu dataset manifests are not yet committed to `dataset/public`.
+- The public dataset includes a categorized Masyu manifest.
- Penpa+ support is import-oriented; export completeness is not a current guarantee.
-- Puzzle-specific Live Stats wording can still be refined.
+- Live Stats uses a shared timeline, step-duration chart, and rule-usage view. Each puzzle plugin declares the meaningful inference coverage series for its own IR primitives.
- Difficulty modeling is draft-level.
- Nonogram remains planned.
diff --git a/docs/content/masyu/overview.mdx b/docs/content/masyu/overview.mdx
new file mode 100644
index 0000000..6a48b73
--- /dev/null
+++ b/docs/content/masyu/overview.mdx
@@ -0,0 +1,12 @@
+## Puzzle rules
+
+Masyu asks for one continuous loop that passes through every pearl. The loop
+turns on black pearls and continues straight in both neighboring cells. It
+passes straight through white pearls and turns in at least one neighboring
+cell.
+
+## Solver techniques
+
+PuzzleKit combines direct pearl rules, local patterns, candidate pruning,
+inside/outside tile coloring, connectivity checks, and bounded strong
+inference. Each technique below is a real rule registered in the solver.
diff --git a/docs/content/masyu/rules/black-pearl-rule.mdx b/docs/content/masyu/rules/black-pearl-rule.mdx
new file mode 100644
index 0000000..964c3df
--- /dev/null
+++ b/docs/content/masyu/rules/black-pearl-rule.mdx
@@ -0,0 +1,25 @@
+## What this technique does
+
+A black pearl must be a turn in the loop. Each of the two exits from the pearl
+must continue straight through the next cell.
+
+## Trigger conditions
+
+The technique acts when an exit is known, a direction is blocked, or only one
+valid turning pair remains.
+
+## Conclusions
+
+It can force the required straight extensions, cross out an opposite exit, or
+complete the only remaining turn around the pearl.
+
+## Why it works
+
+Using opposite exits would pass straight through the black pearl. Turning away
+immediately in a neighboring cell would violate the required straight
+continuation.
+
+## Limits and related techniques
+
+This technique checks direct pearl geometry. Black Pearl Candidate Pruning
+combines that geometry with broader local feasibility checks.
diff --git a/docs/content/masyu/rules/white-pearl-rule.mdx b/docs/content/masyu/rules/white-pearl-rule.mdx
new file mode 100644
index 0000000..8d3745e
--- /dev/null
+++ b/docs/content/masyu/rules/white-pearl-rule.mdx
@@ -0,0 +1,25 @@
+## What this technique does
+
+A white pearl must be crossed by a straight section of the loop. The loop must
+also turn in at least one of the cells immediately before or after the pearl.
+
+## Trigger conditions
+
+The technique acts when a known line, blocked direction, or impossible adjacent
+turn leaves only one valid straight axis through the pearl.
+
+## Conclusions
+
+It can force the opposite line through the pearl, cross out perpendicular exits,
+or reject a straight continuation that would make the required nearby turn
+impossible.
+
+## Why it works
+
+Any alternative would either turn on the white pearl or continue straight on
+both adjacent cells. Both outcomes violate the puzzle's white-pearl rule.
+
+## Limits and related techniques
+
+This is local deterministic reasoning. Candidate pruning and strong inference
+can resolve cases where more than one locally valid axis remains.
diff --git a/docs/content/slitherlink/overview.mdx b/docs/content/slitherlink/overview.mdx
new file mode 100644
index 0000000..ecc0088
--- /dev/null
+++ b/docs/content/slitherlink/overview.mdx
@@ -0,0 +1,12 @@
+## Puzzle rules
+
+Slitherlink asks for one continuous loop along grid edges. A numbered cell
+states how many of its four surrounding edges belong to the loop. The loop
+cannot branch, cross itself, or split into multiple loops.
+
+## Solver techniques
+
+PuzzleKit combines clue and vertex completion, local patterns, sector
+constraints, inside/outside coloring, connectivity checks, and bounded
+inference. Each technique below is linked by the same stable ID used in the
+solver trace.
diff --git a/docs/content/slitherlink/rules/vertex-degree.mdx b/docs/content/slitherlink/rules/vertex-degree.mdx
new file mode 100644
index 0000000..249718e
--- /dev/null
+++ b/docs/content/slitherlink/rules/vertex-degree.mdx
@@ -0,0 +1,25 @@
+## What this technique does
+
+Every Slitherlink vertex belongs to either zero or two loop edges. A vertex may
+never become a dead end or a branch.
+
+## Trigger conditions
+
+The technique acts when the decided edges around a vertex determine the
+remaining unknown edges.
+
+## Conclusions
+
+If two lines already meet at a vertex, every other incident edge is crossed out.
+If one line and only one unknown edge remain, that unknown edge is forced.
+If no lines and only one unknown edge remain, it is crossed out.
+
+## Why it works
+
+A single used edge creates a dead end, while three or more used edges create a
+branch. Neither can be part of one continuous loop.
+
+## Limits and related techniques
+
+This rule enforces local degree only. Prevent Premature Loop and connectivity
+rules handle larger loop structure.
diff --git a/docs/techniques/masyu.md b/docs/techniques/masyu.md
index 3b76969..8db2bf5 100644
--- a/docs/techniques/masyu.md
+++ b/docs/techniques/masyu.md
@@ -365,6 +365,14 @@ contradictions include:
- tile-color parity conflicts;
- assumptions that conflict with already decided lines.
+When a strong-inference step is selected in the reasoning panel, its Branch
+Inspector replays the rejected assumption from the base puzzle through each
+deterministic trial step to the contradiction. The inspector also provides a
+separate **Forced conclusion** tab showing the real deduction produced by the
+proof by contradiction. This conclusion tab is visual only: the solver does not
+run an additional trial branch, so existing inference budgets and behavior stay
+unchanged.
+
### Black Pearl Strong Inference
For a black pearl, the solver assumes one possible exit direction together with
@@ -375,6 +383,22 @@ crossed out in the real puzzle.
This rule determines crossed-out line segments.
+### Line Component Endpoint Strong Inference
+
+For each open component of confirmed lines, the solver examines its two
+degree-1 endpoints. Endpoints with fewer remaining directions are tested first,
+followed by endpoints belonging to longer components.
+
+For one candidate direction, the solver assumes that the component continues in
+that direction and that the endpoint's other unknown exits are crossed out. If
+deterministic propagation reaches a contradiction, that candidate direction is
+crossed out in the real puzzle. Pearl cells may be component endpoints, but the
+dedicated black-pearl strong-inference rule runs first.
+
+Because component endpoints are already high-value candidates, this rule uses a
+tighter default candidate and elapsed-time cap than the broader strong-inference
+rules.
+
### White Pearl Strong Inference
For a white pearl with exactly two feasible straight-axis candidates, the solver
@@ -385,16 +409,3 @@ forced in the real puzzle.
This rule determines the confirmed lines and crossed-out lines belonging to the
forced white-pearl axis.
-
-### Empty Cell Strong Inference
-
-For a non-pearl cell, the solver can test bounded degree choices when the local
-state has either one known line with exactly two unknown exits, or no known lines
-with exactly two unknown exits.
-
-In the degree-1 case, it assumes each possible continuation in turn. If one
-continuation reaches a contradiction, the other continuation is forced.
-
-In the degree-0 case, it tests the two local modes: both exits are used, or both
-exits are crossed out. If one mode reaches a contradiction, the other mode is
-forced in the real puzzle.
diff --git a/index.html b/index.html
index 5e3836a..4c568b2 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
-
web
+ PuzzKit Web
diff --git a/package.json b/package.json
index c2c845e..9e5711a 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"prepare": "husky"
},
"dependencies": {
+ "@mdx-js/react": "^3.1.1",
"fflate": "^0.8.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -25,6 +26,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.4",
+ "@mdx-js/rollup": "^3.1.1",
"@playwright/test": "^1.59.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 19d6246..56e89a9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@mdx-js/react':
+ specifier: ^3.1.1
+ version: 3.1.1(@types/react@19.2.14)(react@19.2.6)
fflate:
specifier: ^0.8.2
version: 0.8.2
@@ -30,6 +33,9 @@ importers:
'@eslint/js':
specifier: ^9.39.4
version: 9.39.4
+ '@mdx-js/rollup':
+ specifier: ^3.1.1
+ version: 3.1.1(rollup@4.61.1)
'@playwright/test':
specifier: ^1.59.1
version: 1.60.0
@@ -482,6 +488,20 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@mdx-js/mdx@3.1.1':
+ resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+
+ '@mdx-js/react@3.1.1':
+ resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
+ peerDependencies:
+ '@types/react': '>=16'
+ react: '>=16'
+
+ '@mdx-js/rollup@3.1.1':
+ resolution: {integrity: sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==}
+ peerDependencies:
+ rollup: '>=2'
+
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
@@ -597,6 +617,153 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
+ '@rollup/pluginutils@5.4.0':
+ resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ '@rollup/rollup-android-arm-eabi@4.61.1':
+ resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.61.1':
+ resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.61.1':
+ resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.61.1':
+ resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.61.1':
+ resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.61.1':
+ resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.61.1':
+ resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.61.1':
+ resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.61.1':
+ resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.61.1':
+ resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.61.1':
+ resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.61.1':
+ resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.61.1':
+ resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.61.1':
+ resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.61.1':
+ resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.61.1':
+ resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.61.1':
+ resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.61.1':
+ resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.61.1':
+ resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.61.1':
+ resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.61.1':
+ resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.61.1':
+ resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.61.1':
+ resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.61.1':
+ resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.61.1':
+ resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==}
+ cpu: [x64]
+ os: [win32]
+
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -638,15 +805,33 @@ packages:
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/mdx@2.0.14':
+ resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
'@types/node@24.12.4':
resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==}
@@ -658,6 +843,12 @@ packages:
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
'@typescript-eslint/eslint-plugin@8.59.3':
resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -717,6 +908,9 @@ packages:
resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@ungap/structured-clone@1.3.1':
+ resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
+
'@vitejs/plugin-react@6.0.1':
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -810,6 +1004,10 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ astring@1.9.0:
+ resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
+ hasBin: true
+
autoprefixer@10.5.0:
resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==}
engines: {node: ^10 || ^12 || >=14}
@@ -817,6 +1015,9 @@ packages:
peerDependencies:
postcss: ^8.1.0
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -851,6 +1052,9 @@ packages:
caniuse-lite@1.0.30001792:
resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -859,6 +1063,18 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@@ -867,6 +1083,9 @@ packages:
resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
engines: {node: '>=20'}
+ collapse-white-space@2.1.0:
+ resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -877,6 +1096,9 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
@@ -921,6 +1143,9 @@ packages:
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -932,6 +1157,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
@@ -955,6 +1183,12 @@ packages:
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+ esast-util-from-estree@2.0.0:
+ resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
+
+ esast-util-from-js@2.0.1:
+ resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
engines: {node: '>=18'}
@@ -1021,6 +1255,27 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-attach-comments@3.0.0:
+ resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
+
+ estree-util-build-jsx@3.0.1:
+ resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
+
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
+ estree-util-scope@1.0.0:
+ resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
+
+ estree-util-to-js@2.0.0:
+ resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
+
+ estree-util-visit@2.0.0:
+ resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -1035,6 +1290,9 @@ packages:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -1111,6 +1369,15 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ hast-util-to-estree@3.1.3:
+ resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
+
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -1146,6 +1413,18 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ inline-style-parser@0.2.7:
+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1158,6 +1437,13 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@@ -1300,6 +1586,9 @@ packages:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
lru-cache@11.3.6:
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
engines: {node: 20 || >=22}
@@ -1314,9 +1603,124 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ markdown-extensions@2.0.0:
+ resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
+ engines: {node: '>=16'}
+
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdx@3.0.0:
+ resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-mdx-expression@3.0.1:
+ resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
+
+ micromark-extension-mdx-jsx@3.0.2:
+ resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==}
+
+ micromark-extension-mdx-md@2.0.0:
+ resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
+
+ micromark-extension-mdxjs@3.0.0:
+ resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-mdx-expression@2.0.3:
+ resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-events-to-acorn@2.0.3:
+ resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
@@ -1369,6 +1773,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
parse5@8.0.1:
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
@@ -1420,6 +1827,9 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ property-information@7.2.0:
+ resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -1453,10 +1863,36 @@ packages:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'}
+ recma-build-jsx@1.0.0:
+ resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
+
+ recma-jsx@1.0.1:
+ resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ recma-parse@1.0.0:
+ resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
+
+ recma-stringify@1.0.0:
+ resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+
redent@3.0.0:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ rehype-recma@1.0.0:
+ resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+
+ remark-mdx@3.1.1:
+ resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
+
+ remark-parse@11.0.0:
+ resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
+
+ remark-rehype@11.1.2:
+ resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@@ -1480,6 +1916,11 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
+ rollup@4.61.1:
+ resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@@ -1526,6 +1967,13 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -1544,6 +1992,9 @@ packages:
resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==}
engines: {node: '>=20'}
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
@@ -1556,6 +2007,12 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ style-to-js@1.1.21:
+ resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
+
+ style-to-object@1.0.14:
+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -1596,6 +2053,12 @@ packages:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
@@ -1633,6 +2096,27 @@ packages:
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
engines: {node: '>=20.18.1'}
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position-from-estree@2.0.0:
+ resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
@@ -1642,6 +2126,12 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
vite@8.0.12:
resolution: {integrity: sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1806,6 +2296,9 @@ packages:
use-sync-external-store:
optional: true
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
snapshots:
'@adobe/css-tools@4.4.4': {}
@@ -2137,6 +2630,52 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@mdx-js/mdx@3.1.1':
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdx': 2.0.14
+ acorn: 8.16.0
+ collapse-white-space: 2.1.0
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-util-scope: 1.0.0
+ estree-walker: 3.0.3
+ hast-util-to-jsx-runtime: 2.3.6
+ markdown-extensions: 2.0.0
+ recma-build-jsx: 1.0.0
+ recma-jsx: 1.0.1(acorn@8.16.0)
+ recma-stringify: 1.0.0
+ rehype-recma: 1.0.0
+ remark-mdx: 3.1.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ source-map: 0.7.6
+ unified: 11.0.5
+ unist-util-position-from-estree: 2.0.0
+ unist-util-stringify-position: 4.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.6)':
+ dependencies:
+ '@types/mdx': 2.0.14
+ '@types/react': 19.2.14
+ react: 19.2.6
+
+ '@mdx-js/rollup@3.1.1(rollup@4.61.1)':
+ dependencies:
+ '@mdx-js/mdx': 3.1.1
+ '@rollup/pluginutils': 5.4.0(rollup@4.61.1)
+ rollup: 4.61.1
+ source-map: 0.7.6
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
@@ -2203,6 +2742,89 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.7': {}
+ '@rollup/pluginutils@5.4.0(rollup@4.61.1)':
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-walker: 2.0.2
+ picomatch: 4.0.4
+ optionalDependencies:
+ rollup: 4.61.1
+
+ '@rollup/rollup-android-arm-eabi@4.61.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.61.1':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.61.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.61.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.61.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.61.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.61.1':
+ optional: true
+
'@standard-schema/spec@1.1.0': {}
'@testing-library/dom@10.4.1':
@@ -2251,12 +2873,32 @@ snapshots:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
'@types/deep-eql@4.0.2': {}
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.9
+
'@types/estree@1.0.9': {}
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
'@types/json-schema@7.0.15': {}
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/mdx@2.0.14': {}
+
+ '@types/ms@2.1.0': {}
+
'@types/node@24.12.4':
dependencies:
undici-types: 7.16.0
@@ -2269,6 +2911,10 @@ snapshots:
dependencies:
csstype: 3.2.3
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
'@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -2360,6 +3006,8 @@ snapshots:
'@typescript-eslint/types': 8.59.3
eslint-visitor-keys: 5.0.1
+ '@ungap/structured-clone@1.3.1': {}
+
'@vitejs/plugin-react@6.0.1(vite@8.0.12(@types/node@24.12.4)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.9.0))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
@@ -2445,6 +3093,8 @@ snapshots:
assertion-error@2.0.1: {}
+ astring@1.9.0: {}
+
autoprefixer@10.5.0(postcss@8.5.14):
dependencies:
browserslist: 4.28.2
@@ -2454,6 +3104,8 @@ snapshots:
postcss: 8.5.14
postcss-value-parser: 4.2.0
+ bail@2.0.2: {}
+
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -2485,6 +3137,8 @@ snapshots:
caniuse-lite@1.0.30001792: {}
+ ccount@2.0.1: {}
+
chai@6.2.2: {}
chalk@4.1.2:
@@ -2492,6 +3146,14 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@@ -2501,6 +3163,8 @@ snapshots:
slice-ansi: 8.0.0
string-width: 8.2.1
+ collapse-white-space@2.1.0: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2509,6 +3173,8 @@ snapshots:
colorette@2.0.20: {}
+ comma-separated-tokens@2.0.3: {}
+
commander@14.0.3: {}
concat-map@0.0.1: {}
@@ -2545,12 +3211,20 @@ snapshots:
decimal.js@10.6.0: {}
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
deep-is@0.1.4: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {}
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
@@ -2565,6 +3239,20 @@ snapshots:
es-module-lexer@2.1.0: {}
+ esast-util-from-estree@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ unist-util-position-from-estree: 2.0.0
+
+ esast-util-from-js@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ acorn: 8.16.0
+ esast-util-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
esbuild@0.27.7:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
@@ -2679,6 +3367,37 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-attach-comments@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ estree-util-build-jsx@3.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-walker: 3.0.3
+
+ estree-util-is-identifier-name@3.0.0: {}
+
+ estree-util-scope@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+
+ estree-util-to-js@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ astring: 1.9.0
+ source-map: 0.7.6
+
+ estree-util-visit@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/unist': 3.0.3
+
+ estree-walker@2.0.2: {}
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
@@ -2689,6 +3408,8 @@ snapshots:
expect-type@1.3.0: {}
+ extend@3.0.2: {}
+
fast-deep-equal@3.1.3: {}
fast-json-stable-stringify@2.1.0: {}
@@ -2743,6 +3464,51 @@ snapshots:
has-flag@4.0.0: {}
+ hast-util-to-estree@3.1.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-attach-comments: 3.0.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ zwitch: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
hermes-estree@0.25.1: {}
hermes-parser@0.25.1:
@@ -2770,6 +3536,17 @@ snapshots:
indent-string@4.0.0: {}
+ inline-style-parser@0.2.7: {}
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-decimal@2.0.1: {}
+
is-extglob@2.1.1: {}
is-fullwidth-code-point@5.1.0:
@@ -2780,6 +3557,10 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-hexadecimal@2.0.1: {}
+
+ is-plain-obj@4.1.0: {}
+
is-potential-custom-element-name@1.0.1: {}
isexe@2.0.0: {}
@@ -2916,6 +3697,8 @@ snapshots:
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
+ longest-streak@3.1.0: {}
+
lru-cache@11.3.6: {}
lru-cache@5.1.1:
@@ -2928,8 +3711,315 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ markdown-extensions@2.0.0: {}
+
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx@3.0.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.1
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
mdn-data@2.27.1: {}
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-expression@3.0.1:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-jsx@3.0.2:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-extension-mdx-md@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-extension-mdxjs@3.0.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ micromark-extension-mdx-expression: 3.0.1
+ micromark-extension-mdx-jsx: 3.0.2
+ micromark-extension-mdx-md: 2.0.0
+ micromark-extension-mdxjs-esm: 3.0.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-mdx-expression@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-events-to-acorn@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
mimic-function@5.0.1: {}
min-indent@1.0.1: {}
@@ -2977,6 +4067,16 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
parse5@8.0.1:
dependencies:
entities: 8.0.0
@@ -3017,6 +4117,8 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
+ property-information@7.2.0: {}
+
punycode@2.3.1: {}
react-dom@19.2.6(react@19.2.6):
@@ -3042,11 +4144,72 @@ snapshots:
react@19.2.6: {}
+ recma-build-jsx@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-util-build-jsx: 3.0.1
+ vfile: 6.0.3
+
+ recma-jsx@1.0.1(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ estree-util-to-js: 2.0.0
+ recma-parse: 1.0.0
+ recma-stringify: 1.0.0
+ unified: 11.0.5
+
+ recma-parse@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ esast-util-from-js: 2.0.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ recma-stringify@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-util-to-js: 2.0.0
+ unified: 11.0.5
+ vfile: 6.0.3
+
redent@3.0.0:
dependencies:
indent-string: 4.0.0
strip-indent: 3.0.0
+ rehype-recma@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ '@types/hast': 3.0.4
+ hast-util-to-estree: 3.1.3
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-mdx@3.1.1:
+ dependencies:
+ mdast-util-mdx: 3.0.0
+ micromark-extension-mdxjs: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-parse@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ micromark-util-types: 2.0.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-rehype@11.1.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ mdast-util-to-hast: 13.2.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
require-from-string@2.0.2: {}
resolve-from@4.0.0: {}
@@ -3081,6 +4244,37 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.0.0
'@rolldown/binding-win32-x64-msvc': 1.0.0
+ rollup@4.61.1:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.61.1
+ '@rollup/rollup-android-arm64': 4.61.1
+ '@rollup/rollup-darwin-arm64': 4.61.1
+ '@rollup/rollup-darwin-x64': 4.61.1
+ '@rollup/rollup-freebsd-arm64': 4.61.1
+ '@rollup/rollup-freebsd-x64': 4.61.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.61.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.61.1
+ '@rollup/rollup-linux-arm64-gnu': 4.61.1
+ '@rollup/rollup-linux-arm64-musl': 4.61.1
+ '@rollup/rollup-linux-loong64-gnu': 4.61.1
+ '@rollup/rollup-linux-loong64-musl': 4.61.1
+ '@rollup/rollup-linux-ppc64-gnu': 4.61.1
+ '@rollup/rollup-linux-ppc64-musl': 4.61.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.61.1
+ '@rollup/rollup-linux-riscv64-musl': 4.61.1
+ '@rollup/rollup-linux-s390x-gnu': 4.61.1
+ '@rollup/rollup-linux-x64-gnu': 4.61.1
+ '@rollup/rollup-linux-x64-musl': 4.61.1
+ '@rollup/rollup-openbsd-x64': 4.61.1
+ '@rollup/rollup-openharmony-arm64': 4.61.1
+ '@rollup/rollup-win32-arm64-msvc': 4.61.1
+ '@rollup/rollup-win32-ia32-msvc': 4.61.1
+ '@rollup/rollup-win32-x64-gnu': 4.61.1
+ '@rollup/rollup-win32-x64-msvc': 4.61.1
+ fsevents: 2.3.3
+
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
@@ -3115,6 +4309,10 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map@0.7.6: {}
+
+ space-separated-tokens@2.0.2: {}
+
stackback@0.0.2: {}
std-env@4.1.0: {}
@@ -3132,6 +4330,11 @@ snapshots:
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
@@ -3142,6 +4345,14 @@ snapshots:
strip-json-comments@3.1.1: {}
+ style-to-js@1.1.21:
+ dependencies:
+ style-to-object: 1.0.14
+
+ style-to-object@1.0.14:
+ dependencies:
+ inline-style-parser: 0.2.7
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -3175,6 +4386,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ trim-lines@3.0.1: {}
+
+ trough@2.2.0: {}
+
ts-api-utils@2.5.0(typescript@6.0.3):
dependencies:
typescript: 6.0.3
@@ -3210,6 +4425,43 @@ snapshots:
undici@7.25.0: {}
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position-from-estree@2.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies:
browserslist: 4.28.2
@@ -3220,6 +4472,16 @@ snapshots:
dependencies:
punycode: 2.3.1
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
vite@8.0.12(@types/node@24.12.4)(esbuild@0.27.7)(tsx@4.21.0)(yaml@2.9.0):
dependencies:
lightningcss: 1.32.0
@@ -3315,3 +4577,5 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
react: 19.2.6
+
+ zwitch@2.0.4: {}
diff --git a/public/favicon.svg b/public/favicon.svg
index 6893eb1..f3d551c 100644
--- a/public/favicon.svg
+++ b/public/favicon.svg
@@ -1 +1,12 @@
-
\ No newline at end of file
+
diff --git a/src/App.tsx b/src/App.tsx
index 6b41264..1e4720f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,6 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { DatasetPage } from './app/DatasetPage'
+import { DocsPage } from './app/DocsPage'
import { EditorPage } from './app/EditorPage'
import { WorkspacePage } from './app/WorkspacePage'
@@ -8,6 +9,7 @@ function App() {
} />
} />
+ } />
} />
} />
diff --git a/src/app/AppDeepLink.test.tsx b/src/app/AppDeepLink.test.tsx
new file mode 100644
index 0000000..59a06ba
--- /dev/null
+++ b/src/app/AppDeepLink.test.tsx
@@ -0,0 +1,91 @@
+import { StrictMode } from 'react'
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { MemoryRouter } from 'react-router-dom'
+import App from '../App'
+import { createMasyuPuzzle } from '../domain/ir/masyu'
+import {
+ DEFAULT_SLITHERLINK_SAMPLE_URL,
+ useSolverStore,
+} from '../features/solver/solverStore'
+
+const SLITHER_PAYLOAD =
+ 'slither/10/10/q2111221ch6212b212611b61262cg1c6bb2121c2bcc621112bo'
+const MASYU_PAYLOAD = 'mashu/14/8/330000096960006ik00039a00010j0i0000220'
+
+const renderApp = (entry: string, strict = false) => {
+ const app = (
+
+
+
+ )
+ return render(strict ? {app} : app)
+}
+
+describe('PuzzleKit deep links', () => {
+ beforeEach(() => {
+ useSolverStore.getState().loadDefaultPuzzle()
+ })
+
+ afterEach(() => {
+ cleanup()
+ })
+
+ it('does not change the current solver puzzle when p is absent', async () => {
+ const puzzle = createMasyuPuzzle(4, 5)
+ useSolverStore.getState().loadPuzzle(puzzle, { pluginId: 'masyu', sourceUrl: 'manual' })
+
+ renderApp('/')
+
+ await waitFor(() => expect(useSolverStore.getState().pluginId).toBe('masyu'))
+ expect(useSolverStore.getState().currentPuzzle.rows).toBe(4)
+ expect(useSolverStore.getState().currentPuzzle.cols).toBe(5)
+ expect(useSolverStore.getState().sourceUrl).toBe('manual')
+ })
+
+ it.each([
+ [SLITHER_PAYLOAD, 'slitherlink', 10, 10],
+ [MASYU_PAYLOAD, 'masyu', 8, 14],
+ ])('loads canonical payload %s into the solver', async (payload, pluginId, rows, cols) => {
+ renderApp(`/?p=${encodeURIComponent(payload)}`, true)
+
+ await waitFor(() => expect(useSolverStore.getState().pluginId).toBe(pluginId))
+ const state = useSolverStore.getState()
+ expect(state.currentPuzzle.rows).toBe(rows)
+ expect(state.currentPuzzle.cols).toBe(cols)
+ expect(state.sourceUrl).toBe(`https://puzz.link/p?${payload}`)
+ expect(state.steps).toEqual([])
+ expect(state.pointer).toBe(0)
+ expect(screen.getByDisplayValue(`https://puzz.link/p?${payload}`)).toBeInTheDocument()
+ })
+
+ it('falls back to the default puzzle, reports the error, and still permits manual import', async () => {
+ renderApp('/?p=slither%2F10%2F10%2Fdsew%3F')
+
+ const dialog = await screen.findByRole('alertdialog', { name: /import failed/i })
+ expect(dialog).toBeInTheDocument()
+ expect(useSolverStore.getState().pluginId).toBe('slitherlink')
+ expect(useSolverStore.getState().sourceUrl).toBe(DEFAULT_SLITHERLINK_SAMPLE_URL)
+ expect(screen.getByText(/default puzzle has been loaded instead/i)).not.toBeVisible()
+
+ fireEvent.click(screen.getByRole('button', { name: /close/i }))
+ const input = screen.getByPlaceholderText(/paste puzz\.link/i)
+ fireEvent.change(input, { target: { value: 'https://puzz.link/p?slither/3/3/g0h' } })
+ fireEvent.click(screen.getByRole('button', { name: /import url/i }))
+
+ await waitFor(() => expect(useSolverStore.getState().currentPuzzle.rows).toBe(3))
+ expect(useSolverStore.getState().sourceUrl).toBe('https://puzz.link/p?slither/3/3/g0h')
+ expect(screen.queryByRole('alertdialog', { name: /import failed/i })).not.toBeInTheDocument()
+ })
+
+ it('ignores deep-link parameters outside the solver route', async () => {
+ const puzzle = createMasyuPuzzle(4, 5)
+ useSolverStore.getState().loadPuzzle(puzzle, { pluginId: 'masyu', sourceUrl: 'manual' })
+
+ renderApp(`/editor?p=${encodeURIComponent(SLITHER_PAYLOAD)}`)
+
+ await waitFor(() => expect(screen.getByRole('heading', { name: /puzzlekit editor/i })).toBeInTheDocument())
+ expect(useSolverStore.getState().pluginId).toBe('masyu')
+ expect(useSolverStore.getState().sourceUrl).toBe('manual')
+ })
+})
diff --git a/src/app/DatasetPage.test.tsx b/src/app/DatasetPage.test.tsx
index 03a0ea4..afdf75d 100644
--- a/src/app/DatasetPage.test.tsx
+++ b/src/app/DatasetPage.test.tsx
@@ -1,5 +1,11 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ within,
+} from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import App from '../App'
import { createSlitherPuzzle } from '../domain/ir/slither'
@@ -7,6 +13,8 @@ import { useEditorStore } from '../features/editor/editorStore'
import { useSolverStore } from '../features/solver/solverStore'
const SAMPLE_URL = 'https://puzz.link/p?slither/3/3/g0h'
+const MASYU_20_URL =
+ 'https://puzz.link/p?mashu/20/20/i000006303j0215600c0031070269c30i0009069i200o99213300c4004c9100l000610j96i900366k3991300969a1010c200j0c009i00190000b01606j1006c0904000'
const renderDataset = () =>
render(
@@ -35,58 +43,166 @@ describe('DatasetPage', () => {
it('renders dataset navigation, controls, and public manifest cards', () => {
renderDataset()
- const nav = screen.getByRole('navigation', { name: /workspace navigation/i })
- expect(screen.getByRole('heading', { name: /puzzlekit dataset/i })).toBeInTheDocument()
- expect(within(nav).getByRole('link', { name: /dataset/i })).toHaveAttribute('aria-current', 'page')
- expect(within(nav).getByRole('link', { name: /solver/i })).toHaveAttribute('href', '/')
- expect(within(nav).getByRole('link', { name: /editor/i })).toHaveAttribute('href', '/editor')
- expect(within(nav).getByRole('link', { name: /open puzzlekit web on github/i })).toHaveAttribute(
- 'href',
- 'https://github.com/SmilingWayne/puzzlekit-web',
- )
- expect(within(nav).getByRole('link', { name: /open puzzlekit web on github/i })).toHaveAttribute(
- 'target',
- '_blank',
+ const nav = screen.getByRole('navigation', {
+ name: /workspace navigation/i,
+ })
+ expect(
+ screen.getByRole('heading', { name: /puzzlekit dataset/i }),
+ ).toBeInTheDocument()
+ expect(within(nav).getByRole('link', { name: /dataset/i })).toHaveAttribute(
+ 'aria-current',
+ 'page',
)
- expect(within(nav).getByRole('link', { name: /open puzzlekit web on github/i })).toHaveAttribute(
- 'rel',
- 'noreferrer',
+ expect(within(nav).getByRole('link', { name: /solver/i })).toHaveAttribute(
+ 'href',
+ '/',
)
- expect(screen.getByRole('heading', { name: /dataset controls/i })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: 'slitherlink-10x10-0001' })).toBeInTheDocument()
- expect(screen.getByLabelText(/slitherlink-10x10-0001 dataset preview/i)).toHaveClass(
- 'dataset-preview-canvas',
+ expect(within(nav).getByRole('link', { name: /editor/i })).toHaveAttribute(
+ 'href',
+ '/editor',
)
- expect(screen.getByText(/showing 56 \/ 56 puzzles/i)).toBeInTheDocument()
+ expect(
+ within(nav).getByRole('link', { name: /open puzzlekit web on github/i }),
+ ).toHaveAttribute('href', 'https://github.com/SmilingWayne/puzzlekit-web')
+ expect(
+ within(nav).getByRole('link', { name: /open puzzlekit web on github/i }),
+ ).toHaveAttribute('target', '_blank')
+ expect(
+ within(nav).getByRole('link', { name: /open puzzlekit web on github/i }),
+ ).toHaveAttribute('rel', 'noreferrer')
+ expect(
+ screen.getByRole('heading', { name: /dataset controls/i }),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'slitherlink-10x10-0001' }),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByLabelText(/slitherlink-10x10-0001 dataset preview/i),
+ ).toHaveClass('dataset-preview-canvas')
+ expect(screen.getByText(/showing 56 \/ 111 puzzles/i)).toBeInTheDocument()
})
it('filters by search text, size, and tag', () => {
renderDataset()
- fireEvent.change(screen.getByPlaceholderText(/name, tag, size, type, or url/i), {
- target: { value: 'slitherlink-6x6-0001' },
- })
+ fireEvent.change(
+ screen.getByPlaceholderText(/name, tag, size, type, or url/i),
+ {
+ target: { value: 'slitherlink-6x6-0001' },
+ },
+ )
- expect(screen.getByRole('heading', { name: 'slitherlink-6x6-0001' })).toBeInTheDocument()
- expect(screen.queryByRole('heading', { name: 'slitherlink-10x10-0001' })).not.toBeInTheDocument()
- expect(screen.getByText(/showing 1 \/ 56 puzzles/i)).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'slitherlink-6x6-0001' }),
+ ).toBeInTheDocument()
+ expect(
+ screen.queryByRole('heading', { name: 'slitherlink-10x10-0001' }),
+ ).not.toBeInTheDocument()
+ expect(screen.getByText(/showing 1 \/ 111 puzzles/i)).toBeInTheDocument()
- fireEvent.change(screen.getByPlaceholderText(/name, tag, size, type, or url/i), {
- target: { value: '' },
+ fireEvent.change(
+ screen.getByPlaceholderText(/name, tag, size, type, or url/i),
+ {
+ target: { value: '' },
+ },
+ )
+ fireEvent.change(screen.getByLabelText(/^size$/i), {
+ target: { value: '6 x 6' },
})
- fireEvent.change(screen.getByLabelText(/^size$/i), { target: { value: '6 x 6' } })
- expect(screen.getByRole('heading', { name: 'slitherlink-6x6-0001' })).toBeInTheDocument()
- expect(screen.getByRole('heading', { name: 'slitherlink-6x6-0002' })).toBeInTheDocument()
- expect(screen.queryByRole('heading', { name: 'slitherlink-10x10-0001' })).not.toBeInTheDocument()
- expect(screen.getByText(/showing 2 \/ 56 puzzles/i)).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'slitherlink-6x6-0001' }),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'slitherlink-6x6-0002' }),
+ ).toBeInTheDocument()
+ expect(
+ screen.queryByRole('heading', { name: 'slitherlink-10x10-0001' }),
+ ).not.toBeInTheDocument()
+ expect(screen.getByText(/showing 2 \/ 111 puzzles/i)).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /auto-imported/i }))
- expect(screen.getByRole('button', { name: /auto-imported/i })).toHaveAttribute(
+ expect(
+ screen.getByRole('button', { name: /auto-imported/i }),
+ ).toHaveAttribute('data-active', 'true')
+ expect(screen.getByText(/showing 2 \/ 111 puzzles/i)).toBeInTheDocument()
+ })
+
+ it('enables Masyu datasets and resets type-specific filters', () => {
+ renderDataset()
+
+ const typeSelect = screen.getByDisplayValue('Slitherlink')
+ expect(screen.getByRole('option', { name: 'Masyu' })).toBeEnabled()
+ expect(
+ screen.getByRole('option', { name: /nonogram \(planned\)/i }),
+ ).toBeDisabled()
+
+ fireEvent.change(screen.getByLabelText(/^size$/i), {
+ target: { value: '6 x 6' },
+ })
+ fireEvent.click(screen.getByRole('button', { name: /auto-imported/i }))
+ fireEvent.change(typeSelect, { target: { value: 'masyu' } })
+
+ expect(screen.getByText(/showing 55 \/ 111 puzzles/i)).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'masyu-25x25-0001' }),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByLabelText(/masyu-25x25-0001 dataset preview/i),
+ ).toHaveClass('dataset-preview-canvas')
+ expect(screen.getByLabelText(/^size$/i)).toHaveValue('all')
+ expect(
+ screen.queryByRole('button', { name: /auto-imported/i }),
+ ).not.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /hard/i })).toHaveAttribute(
+ 'data-active',
+ 'false',
+ )
+
+ fireEvent.change(screen.getByLabelText(/^size$/i), {
+ target: { value: '20 x 20' },
+ })
+ expect(screen.getByText(/showing 2 \/ 111 puzzles/i)).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: 'masyu-20x20-6613546' }),
+ ).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: /hard/i }))
+ expect(screen.getByRole('button', { name: /hard/i })).toHaveAttribute(
'data-active',
'true',
)
- expect(screen.getByText(/showing 2 \/ 56 puzzles/i)).toBeInTheDocument()
+
+ fireEvent.change(
+ screen.getByPlaceholderText(/name, tag, size, type, or url/i),
+ {
+ target: { value: 'masyu-20x20-6613546' },
+ },
+ )
+ expect(screen.getByText(/showing 1 \/ 111 puzzles/i)).toBeInTheDocument()
+ })
+
+ it('filters Masyu datasets by difficulty and area tags', () => {
+ renderDataset()
+
+ fireEvent.change(screen.getByDisplayValue('Slitherlink'), {
+ target: { value: 'masyu' },
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: 'easy' }))
+ expect(screen.getByText(/showing 18 \/ 111 puzzles/i)).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Small' }))
+ expect(screen.getByText(/showing 7 \/ 111 puzzles/i)).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Medium' }))
+ expect(screen.getByText(/showing 7 \/ 111 puzzles/i)).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Large' }))
+ expect(screen.getByText(/showing 41 \/ 111 puzzles/i)).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'hard' }))
+ expect(screen.getByText(/showing 37 \/ 111 puzzles/i)).toBeInTheDocument()
})
it('renders compact action links for each dataset puzzle', () => {
@@ -101,16 +217,28 @@ describe('DatasetPage', () => {
)
expect(sourceLink).toHaveAttribute('target', '_blank')
expect(sourceLink).toHaveAttribute('rel', 'noreferrer')
- expect(within(card).getByRole('link', { name: 'Solver' })).toHaveAttribute('href', '/')
- expect(within(card).getByRole('link', { name: 'Editor' })).toHaveAttribute('href', '/editor')
+ expect(within(card).getByRole('link', { name: 'Solver' })).toHaveAttribute(
+ 'href',
+ '/',
+ )
+ expect(within(card).getByRole('link', { name: 'Editor' })).toHaveAttribute(
+ 'href',
+ '/editor',
+ )
})
it('loads a dataset puzzle into the solver', () => {
renderDataset()
- fireEvent.click(within(getCard('slitherlink-6x6-0001')).getByRole('link', { name: 'Solver' }))
+ fireEvent.click(
+ within(getCard('slitherlink-6x6-0001')).getByRole('link', {
+ name: 'Solver',
+ }),
+ )
- expect(screen.getByRole('heading', { name: /puzzlekit web/i })).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: /puzzlekit web/i }),
+ ).toBeInTheDocument()
expect(useSolverStore.getState().pluginId).toBe('slitherlink')
expect(useSolverStore.getState().initialPuzzle.rows).toBe(6)
expect(useSolverStore.getState().initialPuzzle.cols).toBe(6)
@@ -123,9 +251,15 @@ describe('DatasetPage', () => {
it('loads a dataset puzzle into the editor', () => {
renderDataset()
- fireEvent.click(within(getCard('slitherlink-10x10-0001')).getByRole('link', { name: 'Editor' }))
+ fireEvent.click(
+ within(getCard('slitherlink-10x10-0001')).getByRole('link', {
+ name: 'Editor',
+ }),
+ )
- expect(screen.getByRole('heading', { name: /puzzlekit editor/i })).toBeInTheDocument()
+ expect(
+ screen.getByRole('heading', { name: /puzzlekit editor/i }),
+ ).toBeInTheDocument()
expect(useEditorStore.getState().pluginId).toBe('slitherlink')
expect(useEditorStore.getState().puzzle.rows).toBe(10)
expect(useEditorStore.getState().puzzle.cols).toBe(10)
@@ -133,4 +267,34 @@ describe('DatasetPage', () => {
'https://puzz.link/p?slither/10/10/372d23djdh738adl72882dj18538ald838dhaj21d272c',
)
})
+
+ it('loads a Masyu dataset puzzle into the solver and editor', () => {
+ renderDataset()
+
+ fireEvent.change(screen.getByDisplayValue('Slitherlink'), {
+ target: { value: 'masyu' },
+ })
+ const card = getCard('masyu-20x20-6613546')
+ fireEvent.click(within(card).getByRole('link', { name: 'Solver' }))
+
+ expect(useSolverStore.getState().pluginId).toBe('masyu')
+ expect(useSolverStore.getState().initialPuzzle.rows).toBe(20)
+ expect(useSolverStore.getState().initialPuzzle.cols).toBe(20)
+ expect(useSolverStore.getState().sourceUrl).toBe(MASYU_20_URL)
+
+ renderDataset()
+ fireEvent.change(screen.getByDisplayValue('Slitherlink'), {
+ target: { value: 'masyu' },
+ })
+ fireEvent.click(
+ within(getCard('masyu-20x20-6613546')).getByRole('link', {
+ name: 'Editor',
+ }),
+ )
+
+ expect(useEditorStore.getState().pluginId).toBe('masyu')
+ expect(useEditorStore.getState().puzzle.rows).toBe(20)
+ expect(useEditorStore.getState().puzzle.cols).toBe(20)
+ expect(useEditorStore.getState().sourceUrl).toBe(MASYU_20_URL)
+ })
})
diff --git a/src/app/DatasetPage.tsx b/src/app/DatasetPage.tsx
index 66f8a91..eedc1c7 100644
--- a/src/app/DatasetPage.tsx
+++ b/src/app/DatasetPage.tsx
@@ -21,17 +21,23 @@ type ActivePuzzlePopover = 'rules' | 'legend' | null
const DATASET_PREVIEW_SIZE = 136
-const datasetCards: DatasetPuzzleCard[] = publicDatasetManifests.flatMap((manifest) =>
- manifest.items.map((item) => ({
- ...item,
- datasetId: manifest.id,
- datasetTitle: manifest.title,
- description: `${manifest.title}: ${item.height} x ${item.width} ${item.puzzleType} puzzle.`,
- })),
+const datasetCards: DatasetPuzzleCard[] = publicDatasetManifests.flatMap(
+ (manifest) =>
+ manifest.items.map((item) => ({
+ ...item,
+ datasetId: manifest.id,
+ datasetTitle: manifest.title,
+ description: `${manifest.title}: ${item.height} x ${item.width} ${item.puzzleType} puzzle.`,
+ })),
)
-const buildSizeLabel = (item: Pick): string =>
- `${item.height} x ${item.width}`
+const publicDatasetPluginIds = new Set(
+ datasetCards.map((item) => item.puzzleType),
+)
+
+const buildSizeLabel = (
+ item: Pick,
+): string => `${item.height} x ${item.width}`
const parseDatasetPuzzle = (item: BenchmarkDatasetItem) => {
const plugin = puzzleRegistry.get(item.puzzleType)
@@ -75,16 +81,24 @@ export const DatasetPage = () => {
const [query, setQuery] = useState('')
const [sizeFilter, setSizeFilter] = useState('all')
const [activeTag, setActiveTag] = useState(null)
- const [activePuzzlePopover, setActivePuzzlePopover] = useState(null)
+ const [activePuzzlePopover, setActivePuzzlePopover] =
+ useState(null)
const [actionError, setActionError] = useState('')
+ const pluginItems = useMemo(
+ () => datasetCards.filter((item) => item.puzzleType === pluginId),
+ [pluginId],
+ )
const tags = useMemo(
- () => Array.from(new Set(datasetCards.flatMap((item) => item.tags))).sort(),
- [],
+ () => Array.from(new Set(pluginItems.flatMap((item) => item.tags))).sort(),
+ [pluginItems],
)
const sizeOptions = useMemo(
- () => Array.from(new Set(datasetCards.map(buildSizeLabel))).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })),
- [],
+ () =>
+ Array.from(new Set(pluginItems.map(buildSizeLabel))).sort((a, b) =>
+ a.localeCompare(b, undefined, { numeric: true }),
+ ),
+ [pluginItems],
)
const filteredItems = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase()
@@ -102,7 +116,8 @@ export const DatasetPage = () => {
.join(' ')
.toLowerCase()
const matchesPlugin = item.puzzleType === pluginId
- const matchesQuery = normalizedQuery.length === 0 || searchableText.includes(normalizedQuery)
+ const matchesQuery =
+ normalizedQuery.length === 0 || searchableText.includes(normalizedQuery)
const matchesSize = sizeFilter === 'all' || sizeLabel === sizeFilter
const matchesTag = activeTag === null || item.tags.includes(activeTag)
return matchesPlugin && matchesQuery && matchesSize && matchesTag
@@ -142,10 +157,13 @@ export const DatasetPage = () => {
-
+
- {actionError ? {actionError}
: null}
+ {actionError ? (
+ {actionError}
+ ) : null}
{filteredItems.map((item) => (
-
+
@@ -171,7 +194,10 @@ export const DatasetPage = () => {
{tag}
))}
-
+
URL
@@ -199,7 +225,11 @@ export const DatasetPage = () => {
))}
- {filteredItems.length === 0 ?
No dataset puzzles match the current filters.
: null}
+ {filteredItems.length === 0 ? (
+
+ No dataset puzzles match the current filters.
+
+ ) : null}
@@ -215,19 +245,34 @@ export const DatasetPage = () => {
onChange={(event) => {
setActivePuzzlePopover(null)
setPluginId(event.target.value)
+ setSizeFilter('all')
+ setActiveTag(null)
}}
>
- {puzzleRegistry.all().map((plugin) => (
-
- ))}
+ {puzzleRegistry.all().map((plugin) => {
+ const hasPublicDataset = publicDatasetPluginIds.has(
+ plugin.id,
+ )
+ return (
+
+ )
+ })}
- setActivePuzzlePopover((current) => (current === 'rules' ? null : 'rules'))
+ setActivePuzzlePopover((current) =>
+ current === 'rules' ? null : 'rules',
+ )
}
onClose={() => setActivePuzzlePopover(null)}
/>
@@ -235,7 +280,9 @@ export const DatasetPage = () => {
pluginId={pluginId}
isOpen={activePuzzlePopover === 'legend'}
onToggle={() =>
- setActivePuzzlePopover((current) => (current === 'legend' ? null : 'legend'))
+ setActivePuzzlePopover((current) =>
+ current === 'legend' ? null : 'legend',
+ )
}
onClose={() => setActivePuzzlePopover(null)}
/>
@@ -252,7 +299,10 @@ export const DatasetPage = () => {
Tags
-
-
-
-
-
+ {canvasSurface}
Use the slider to zoom. Scroll to move around large grids. Highlight syncs with reasoning
steps.
diff --git a/src/features/dataset/publicDatasets.test.ts b/src/features/dataset/publicDatasets.test.ts
new file mode 100644
index 0000000..608ef59
--- /dev/null
+++ b/src/features/dataset/publicDatasets.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from 'vitest'
+import { validateBenchmarkManifest } from '../../domain/benchmark'
+import { puzzleRegistry } from '../../domain/plugins/registry'
+import { publicDatasetManifests } from './publicDatasets'
+
+describe('public datasets', () => {
+ it('provides a valid, categorized, and parseable Masyu manifest', () => {
+ const manifest = publicDatasetManifests.find(
+ (candidate) => candidate.id === 'masyu',
+ )
+
+ expect(manifest).toBeDefined()
+ const validated = validateBenchmarkManifest(manifest)
+ expect(validated.puzzleType).toBe('masyu')
+ expect(validated.title).toBe('Masyu Dataset')
+ expect(validated.items).toHaveLength(55)
+ expect(new Set(validated.items.map((item) => item.id)).size).toBe(55)
+
+ const existingItems = validated.items.slice(0, 37)
+ const newItems = validated.items.slice(37)
+ expect(existingItems.every((item) => item.tags.includes('hard'))).toBe(true)
+ expect(newItems.every((item) => item.tags.includes('easy'))).toBe(true)
+ expect(newItems.every((item) => item.source === 'example.txt')).toBe(true)
+ expect(newItems.map((item) => item.id)).toEqual([
+ 'masyu-15x21-0005',
+ 'masyu-10x10-0006',
+ 'masyu-10x10-0007',
+ 'masyu-10x10-0008',
+ 'masyu-10x10-0009',
+ 'masyu-10x10-0010',
+ 'masyu-25x40-0011',
+ 'masyu-25x40-0012',
+ 'masyu-39x49-0013',
+ 'masyu-10x18-0014',
+ 'masyu-12x14-0015',
+ 'masyu-10x10-0016',
+ 'masyu-10x10-0017',
+ 'masyu-20x29-0018',
+ 'masyu-13x18-0019',
+ 'masyu-40x40-0020',
+ 'masyu-30x30-0021',
+ 'masyu-15x10-0022',
+ ])
+
+ const plugin = puzzleRegistry.get('masyu')
+ expect(plugin).toBeDefined()
+ for (const item of validated.items) {
+ const area = item.width * item.height
+ const sizeTag = area < 150 ? 'Small' : area < 500 ? 'Medium' : 'Large'
+ const difficultyTag = item.tags.includes('hard') ? 'hard' : 'easy'
+
+ expect(item.puzzleType).toBe('masyu')
+ expect(item.tags).toEqual([difficultyTag, sizeTag])
+ expect(item.id).toMatch(/^masyu-\d+x\d+-\d+$/)
+ if (difficultyTag === 'hard') {
+ expect(item.source).toBe(
+ item.width === 25
+ ? 'https://zh.puzzle-masyu.com/?size=18'
+ : 'https://zh.puzzle-masyu.com/?size=12',
+ )
+ }
+
+ const puzzle = plugin!.parse(item.sourceUrl)
+ expect(puzzle.puzzleType).toBe('masyu')
+ expect(puzzle.cols).toBe(item.width)
+ expect(puzzle.rows).toBe(item.height)
+ }
+ })
+})
diff --git a/src/features/dataset/publicDatasets.ts b/src/features/dataset/publicDatasets.ts
index d6238c7..42525a0 100644
--- a/src/features/dataset/publicDatasets.ts
+++ b/src/features/dataset/publicDatasets.ts
@@ -1,8 +1,11 @@
import type { BenchmarkDatasetManifest } from '../../domain/benchmark/types'
+import masyuRaw from '../../../dataset/public/masyu.json?raw'
import slitherlinkExampleRaw from '../../../dataset/public/slitherlink.example.json?raw'
-const parseManifest = (raw: string): BenchmarkDatasetManifest => JSON.parse(raw) as BenchmarkDatasetManifest
+const parseManifest = (raw: string): BenchmarkDatasetManifest =>
+ JSON.parse(raw) as BenchmarkDatasetManifest
export const publicDatasetManifests: BenchmarkDatasetManifest[] = [
parseManifest(slitherlinkExampleRaw),
+ parseManifest(masyuRaw),
]
diff --git a/src/features/docs/DocsIndex.tsx b/src/features/docs/DocsIndex.tsx
new file mode 100644
index 0000000..d6e74a3
--- /dev/null
+++ b/src/features/docs/DocsIndex.tsx
@@ -0,0 +1,87 @@
+import { Link } from 'react-router-dom'
+import MasyuOverview from '../../../docs/content/masyu/overview.mdx'
+import SlitherlinkOverview from '../../../docs/content/slitherlink/overview.mdx'
+import { puzzleRegistry } from '../../domain/plugins/registry'
+import { getRuleDocPath, ruleDocEntries } from './ruleDocRegistry'
+
+const familyOverviews = {
+ masyu: MasyuOverview,
+ slitherlink: SlitherlinkOverview,
+}
+
+export const DocsIndex = ({ puzzleId }: { puzzleId?: string }) => {
+ const plugins = puzzleRegistry
+ .all()
+ .filter((plugin) => plugin.getRules().length > 0)
+ .filter((plugin) => !puzzleId || plugin.id === puzzleId)
+
+ return (
+
+
+
+ {puzzleId
+ ? `${puzzleRegistry.get(puzzleId)?.displayName ?? puzzleId} Documentation`
+ : 'Rule Documentation'}
+
+
+ Puzzle rules describe the goal. Solver techniques explain each
+ deduction that PuzzleKit can make and replay.
+
+ {puzzleId && puzzleId in familyOverviews ? (
+
+ {(() => {
+ const Overview =
+ familyOverviews[puzzleId as keyof typeof familyOverviews]
+ return
+ })()}
+
+ ) : null}
+
+ {plugins.map((plugin) => {
+ const entries = ruleDocEntries.filter(
+ (entry) => entry.puzzleId === plugin.id,
+ )
+ const categories = Array.from(
+ new Set(entries.map((entry) => entry.category)),
+ )
+ return (
+
+
+ {categories.map((category) => (
+
+
{category}
+
+ {entries
+ .filter((entry) => entry.category === category)
+ .map((entry) => (
+
+ {entry.title}
+ {entry.status}
+
+ ))}
+
+
+ ))}
+
+ )
+ })}
+
+ )
+}
diff --git a/src/features/docs/DraftRuleContent.tsx b/src/features/docs/DraftRuleContent.tsx
new file mode 100644
index 0000000..0783cd8
--- /dev/null
+++ b/src/features/docs/DraftRuleContent.tsx
@@ -0,0 +1,9 @@
+export const DraftRuleContent = () => (
+ <>
+
Documentation in progress
+
+ This solver technique is registered and linkable, but its detailed
+ explanation and example have not been written yet.
+
+ >
+)
diff --git a/src/features/docs/RuleDocPage.tsx b/src/features/docs/RuleDocPage.tsx
new file mode 100644
index 0000000..824c67e
--- /dev/null
+++ b/src/features/docs/RuleDocPage.tsx
@@ -0,0 +1,36 @@
+import { Link } from 'react-router-dom'
+import { puzzleRegistry } from '../../domain/plugins/registry'
+import { RuleExample } from './RuleExample'
+import type { RuleDocEntry } from './ruleDocRegistry'
+
+export const RuleDocPage = ({ entry }: { entry: RuleDocEntry }) => {
+ const plugin = puzzleRegistry.get(entry.puzzleId)
+ const Content = entry.content
+
+ return (
+
+
+
+
+ {entry.status}
+
+ {entry.category}
+ {entry.title}
+ {entry.summary}
+ {entry.ruleId}
+
+
+
+
+ {entry.example ? : null}
+
+ )
+}
diff --git a/src/features/docs/RuleExample.tsx b/src/features/docs/RuleExample.tsx
new file mode 100644
index 0000000..d9297ad
--- /dev/null
+++ b/src/features/docs/RuleExample.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useMemo, useState } from 'react'
+import type { PuzzleIR } from '../../domain/ir/types'
+import { applyRuleDiffs } from '../../domain/rules/engine'
+import type { InferenceFocus, RuleDiff } from '../../domain/rules/types'
+import { CanvasBoard } from '../board/CanvasBoard'
+import type { DisplaySettings } from '../solver/solverStore'
+
+type Props = {
+ puzzle: PuzzleIR
+ before?: RuleDiff[]
+ after: RuleDiff[]
+ highlights?: InferenceFocus
+ explanation?: string
+}
+
+const displaySettings: DisplaySettings = {
+ showCoordinates: false,
+ showCellColors: true,
+ showEdgeCrosses: true,
+ showSectorMarks: true,
+ showVertices: true,
+ showTiles: true,
+ showLineCrosses: true,
+ showHighlights: true,
+ showGridLabels: false,
+ showGrid: true,
+}
+
+export const RuleExample = ({
+ puzzle,
+ before = [],
+ after,
+ highlights = {},
+ explanation,
+}: Props) => {
+ const [view, setView] = useState<'before' | 'after'>('before')
+ const [isPlaying, setIsPlaying] = useState(false)
+ const beforePuzzle = useMemo(
+ () => applyRuleDiffs(puzzle, before),
+ [before, puzzle],
+ )
+ const afterPuzzle = useMemo(
+ () => applyRuleDiffs(beforePuzzle, after),
+ [after, beforePuzzle],
+ )
+
+ useEffect(() => {
+ if (!isPlaying) {
+ return
+ }
+ const timer = window.setTimeout(() => {
+ setView('after')
+ setIsPlaying(false)
+ }, 700)
+ return () => window.clearTimeout(timer)
+ }, [isPlaying])
+
+ return (
+
+
+
+ setView('before')}
+ >
+ Before
+
+ setView('after')}
+ >
+ After
+
+
+
{
+ setView('before')
+ setIsPlaying(true)
+ }}
+ >
+ {isPlaying ? 'Playing...' : 'Play deduction'}
+
+
+ undefined}
+ variant="surface"
+ inferenceDiffs={view === 'after' ? after : []}
+ inferenceDiffRole="conclusion"
+ ariaLabel="Rule documentation example"
+ />
+ {explanation ? {explanation} : null}
+
+ )
+}
diff --git a/src/features/docs/ruleDocRegistry.test.ts b/src/features/docs/ruleDocRegistry.test.ts
new file mode 100644
index 0000000..2db2eba
--- /dev/null
+++ b/src/features/docs/ruleDocRegistry.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from 'vitest'
+import { puzzleRegistry } from '../../domain/plugins/registry'
+import {
+ getRuleDocEntry,
+ getRuleDocPath,
+ ruleDocEntries,
+} from './ruleDocRegistry'
+
+describe('ruleDocRegistry', () => {
+ it('registers every production rule exactly once', () => {
+ const productionRules = puzzleRegistry
+ .all()
+ .flatMap((plugin) =>
+ plugin.getRules().map((rule) => `${plugin.id}:${rule.id}`),
+ )
+ const documentedRules = ruleDocEntries.map(
+ (entry) => `${entry.puzzleId}:${entry.ruleId}`,
+ )
+
+ expect(new Set(documentedRules).size).toBe(documentedRules.length)
+ expect(documentedRules.sort()).toEqual(productionRules.sort())
+ })
+
+ it('resolves stable rule paths by puzzle id and rule id', () => {
+ expect(getRuleDocPath('masyu', 'white-pearl-rule')).toBe(
+ '/docs/masyu/rules/white-pearl-rule',
+ )
+ expect(getRuleDocEntry('masyu', 'white-pearl-rule')?.status).toBe(
+ 'documented',
+ )
+ })
+})
diff --git a/src/features/docs/ruleDocRegistry.tsx b/src/features/docs/ruleDocRegistry.tsx
new file mode 100644
index 0000000..d00ece6
--- /dev/null
+++ b/src/features/docs/ruleDocRegistry.tsx
@@ -0,0 +1,105 @@
+import type { ComponentType } from 'react'
+import BlackPearlRuleDoc from '../../../docs/content/masyu/rules/black-pearl-rule.mdx'
+import WhitePearlRuleDoc from '../../../docs/content/masyu/rules/white-pearl-rule.mdx'
+import VertexDegreeRuleDoc from '../../../docs/content/slitherlink/rules/vertex-degree.mdx'
+import { puzzleRegistry } from '../../domain/plugins/registry'
+import type { Rule } from '../../domain/rules/types'
+import { DraftRuleContent } from './DraftRuleContent'
+import { ruleExamples, type RuleExampleData } from './ruleExamples'
+
+export type RuleDocStatus = 'documented' | 'draft' | 'missing'
+
+export type RuleDocEntry = {
+ puzzleId: string
+ ruleId: string
+ title: string
+ summary: string
+ category: string
+ content: ComponentType
+ status: RuleDocStatus
+ example?: RuleExampleData
+}
+
+const documentedContent: Record<
+ string,
+ { content: ComponentType; summary: string }
+> = {
+ 'masyu:white-pearl-rule': {
+ content: WhitePearlRuleDoc,
+ summary:
+ 'Keeps a white pearl straight while preserving its required adjacent turn.',
+ },
+ 'masyu:black-pearl-rule': {
+ content: BlackPearlRuleDoc,
+ summary:
+ 'Forces a turn on a black pearl and straight continuation after each exit.',
+ },
+ 'slitherlink:vertex-degree': {
+ content: VertexDegreeRuleDoc,
+ summary:
+ 'Maintains the loop degree of zero or two at every Slitherlink vertex.',
+ },
+}
+
+const getCategory = (rule: Rule): string => {
+ if (
+ rule.id.includes('strong') ||
+ rule.id.includes('assumption') ||
+ rule.id.includes('parity-inference')
+ ) {
+ return 'Bounded Inference'
+ }
+ if (rule.id.includes('color') || rule.id.includes('tile')) {
+ return 'Color and Region Reasoning'
+ }
+ if (rule.id.includes('sector') || rule.id.includes('vertex')) {
+ return 'Sector and Vertex Reasoning'
+ }
+ if (
+ rule.id.includes('pearl') ||
+ rule.id.includes('white') ||
+ rule.id.includes('black')
+ ) {
+ return 'Pearl Techniques'
+ }
+ if (
+ rule.id.includes('loop') ||
+ rule.id.includes('bridge') ||
+ rule.id.includes('connectivity')
+ ) {
+ return 'Loop and Connectivity'
+ }
+ return 'Core Techniques'
+}
+
+export const getRuleDocPath = (puzzleId: string, ruleId: string): string =>
+ `/docs/${encodeURIComponent(puzzleId)}/rules/${encodeURIComponent(ruleId)}`
+
+export const ruleDocEntries: RuleDocEntry[] = puzzleRegistry
+ .all()
+ .flatMap((plugin) =>
+ plugin.getRules().map((rule) => {
+ const key = `${plugin.id}:${rule.id}`
+ const documented = documentedContent[key]
+ return {
+ puzzleId: plugin.id,
+ ruleId: rule.id,
+ title: rule.name,
+ summary:
+ documented?.summary ??
+ 'A registered PuzzleKit solver technique. Detailed documentation is being prepared.',
+ category: getCategory(rule),
+ content: documented?.content ?? DraftRuleContent,
+ status: documented ? 'documented' : 'draft',
+ example: ruleExamples[key],
+ } satisfies RuleDocEntry
+ }),
+ )
+
+export const getRuleDocEntry = (
+ puzzleId: string,
+ ruleId: string,
+): RuleDocEntry | undefined =>
+ ruleDocEntries.find(
+ (entry) => entry.puzzleId === puzzleId && entry.ruleId === ruleId,
+ )
diff --git a/src/features/docs/ruleExamples.ts b/src/features/docs/ruleExamples.ts
new file mode 100644
index 0000000..ee5dddc
--- /dev/null
+++ b/src/features/docs/ruleExamples.ts
@@ -0,0 +1,81 @@
+import { cellKey, edgeKey, lineKey } from '../../domain/ir/keys'
+import { createMasyuPuzzle } from '../../domain/ir/masyu'
+import { createSlitherPuzzle } from '../../domain/ir/slither'
+import type { PuzzleIR } from '../../domain/ir/types'
+import type { InferenceFocus, RuleDiff } from '../../domain/rules/types'
+
+export type RuleExampleData = {
+ puzzle: PuzzleIR
+ before?: RuleDiff[]
+ after: RuleDiff[]
+ highlights?: InferenceFocus
+ explanation: string
+}
+
+const createWhitePearlExample = (): RuleExampleData => {
+ const puzzle = createMasyuPuzzle(3, 3)
+ puzzle.cells[cellKey(1, 1)] = { clue: { kind: 'pearl', color: 'white' } }
+ const left = lineKey([1, 0], [1, 1])
+ const right = lineKey([1, 1], [1, 2])
+ const up = lineKey([0, 1], [1, 1])
+ const down = lineKey([1, 1], [2, 1])
+ return {
+ puzzle,
+ before: [{ kind: 'line', lineKey: left, from: 'unknown', to: 'line' }],
+ after: [
+ { kind: 'line', lineKey: right, from: 'unknown', to: 'line' },
+ { kind: 'line', lineKey: up, from: 'unknown', to: 'blank' },
+ { kind: 'line', lineKey: down, from: 'unknown', to: 'blank' },
+ ],
+ highlights: { cells: [cellKey(1, 1)], lines: [right, up, down] },
+ explanation:
+ 'A known horizontal exit forces the white pearl to continue horizontally and rejects both vertical exits.',
+ }
+}
+
+const createBlackPearlExample = (): RuleExampleData => {
+ const puzzle = createMasyuPuzzle(4, 3)
+ puzzle.cells[cellKey(2, 1)] = { clue: { kind: 'pearl', color: 'black' } }
+ const up = lineKey([1, 1], [2, 1])
+ const upExtension = lineKey([0, 1], [1, 1])
+ const down = lineKey([2, 1], [3, 1])
+ return {
+ puzzle,
+ before: [{ kind: 'line', lineKey: up, from: 'unknown', to: 'line' }],
+ after: [
+ { kind: 'line', lineKey: upExtension, from: 'unknown', to: 'line' },
+ { kind: 'line', lineKey: down, from: 'unknown', to: 'blank' },
+ ],
+ highlights: { cells: [cellKey(2, 1)], lines: [upExtension, down] },
+ explanation:
+ 'Once the loop exits upward from a black pearl, it must continue straight for another cell and cannot also exit downward.',
+ }
+}
+
+const createVertexDegreeExample = (): RuleExampleData => {
+ const puzzle = createSlitherPuzzle(2, 2)
+ const top = edgeKey([0, 1], [1, 1])
+ const left = edgeKey([1, 0], [1, 1])
+ const right = edgeKey([1, 1], [1, 2])
+ const bottom = edgeKey([1, 1], [2, 1])
+ return {
+ puzzle,
+ before: [
+ { kind: 'edge', edgeKey: top, from: 'unknown', to: 'line' },
+ { kind: 'edge', edgeKey: left, from: 'unknown', to: 'line' },
+ ],
+ after: [
+ { kind: 'edge', edgeKey: right, from: 'unknown', to: 'blank' },
+ { kind: 'edge', edgeKey: bottom, from: 'unknown', to: 'blank' },
+ ],
+ highlights: { edges: [right, bottom], vertices: ['1,1'] },
+ explanation:
+ 'A loop vertex already containing two used edges cannot accept either remaining edge.',
+ }
+}
+
+export const ruleExamples: Record
= {
+ 'masyu:white-pearl-rule': createWhitePearlExample(),
+ 'masyu:black-pearl-rule': createBlackPearlExample(),
+ 'slitherlink:vertex-degree': createVertexDegreeExample(),
+}
diff --git a/src/features/explanation/BranchInspector.tsx b/src/features/explanation/BranchInspector.tsx
new file mode 100644
index 0000000..f911470
--- /dev/null
+++ b/src/features/explanation/BranchInspector.tsx
@@ -0,0 +1,187 @@
+import { useEffect, useMemo, useState } from 'react'
+import { applyRuleDiffs } from '../../domain/rules/engine'
+import type { InferenceBranch, InferenceDetails } from '../../domain/rules/types'
+import { CanvasBoard } from '../board/CanvasBoard'
+import type { DisplaySettings } from '../solver/solverStore'
+
+type Props = {
+ details: InferenceDetails
+ onClose: () => void
+}
+
+const inspectorDisplaySettings: DisplaySettings = {
+ showGridLabels: true,
+ showHighlights: true,
+ showCellColors: true,
+ showEdgeCrosses: true,
+ showSectorMarks: true,
+ showVertices: true,
+ showCoordinates: false,
+}
+
+const buildBranchPuzzle = (details: InferenceDetails, branch: InferenceBranch, pointer: number) => {
+ let puzzle = details.basePuzzle
+ if (pointer >= 1) {
+ puzzle = applyRuleDiffs(puzzle, branch.initialDiffs)
+ }
+ for (let index = 0; index < pointer - 1; index += 1) {
+ puzzle = applyRuleDiffs(puzzle, branch.traceSteps[index].diffs)
+ }
+ return puzzle
+}
+
+export const BranchInspector = ({ details, onClose }: Props) => {
+ const [branchId, setBranchId] = useState(details.defaultBranchId)
+ const [pointer, setPointer] = useState(0)
+ const branch = details.branches.find((item) => item.id === branchId) ?? details.branches[0]
+ const maxPointer = branch.traceSteps.length + 1
+ const puzzle = useMemo(
+ () => buildBranchPuzzle(details, branch, pointer),
+ [branch, details, pointer],
+ )
+ const activeTraceStep = pointer >= 2 ? branch.traceSteps[pointer - 2] : undefined
+ const showContradiction = pointer === maxPointer && branch.status === 'contradiction'
+ const isForcedConclusion = branch.role === 'forced-conclusion'
+
+ useEffect(() => {
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onClose()
+ }
+ }
+ window.addEventListener('keydown', onKeyDown)
+ return () => window.removeEventListener('keydown', onKeyDown)
+ }, [onClose])
+
+ const selectBranch = (nextBranchId: string) => {
+ setBranchId(nextBranchId)
+ setPointer(0)
+ }
+
+ const stageText =
+ pointer === 0
+ ? 'Base puzzle before the inference'
+ : pointer === 1
+ ? isForcedConclusion
+ ? 'Apply the conclusion forced by contradiction'
+ : 'Apply the branch assumption'
+ : activeTraceStep?.message ?? 'Branch result'
+
+ return (
+ event.target === event.currentTarget && onClose()}>
+
+
+
+
+ {details.branches.map((item) => (
+ selectBranch(item.id)}
+ >
+ {item.label} {item.status}
+
+ ))}
+
+
+
+
+
+ diff.kind === 'cell' ? [diff.cellKey] : [],
+ ) ?? []
+ }
+ highlightedColorTiles={activeTraceStep?.affectedTiles ?? []}
+ highlightedEdges={activeTraceStep?.affectedEdges ?? []}
+ highlightedLines={activeTraceStep?.affectedLines ?? []}
+ displaySettings={inspectorDisplaySettings}
+ onSetDisplayOption={() => {}}
+ variant="surface"
+ inferenceDiffs={pointer >= 1 ? branch.initialDiffs : []}
+ inferenceDiffRole={isForcedConclusion ? 'conclusion' : 'assumption'}
+ contradictionFocus={showContradiction ? branch.contradiction : undefined}
+ ariaLabel={`${details.basePuzzle.puzzleType === 'masyu' ? 'Masyu' : 'Slitherlink'} branch inspector canvas`}
+ />
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/features/explanation/ExplanationPanel.tsx b/src/features/explanation/ExplanationPanel.tsx
index 8aeccef..0a39284 100644
--- a/src/features/explanation/ExplanationPanel.tsx
+++ b/src/features/explanation/ExplanationPanel.tsx
@@ -1,7 +1,11 @@
import { useMemo, useState } from 'react'
+import { Link } from 'react-router-dom'
import type { RuleStep } from '../../domain/rules/types'
+import { getRuleDocPath } from '../docs/ruleDocRegistry'
+import { BranchInspector } from './BranchInspector'
type Props = {
+ pluginId: string
steps: RuleStep[]
}
@@ -19,8 +23,9 @@ const buildStepMeta = (step: RuleStep): string => {
return parts.length > 0 ? parts.join(', ') : 'edge updates: 0'
}
-export const ExplanationPanel = ({ steps }: Props) => {
+export const ExplanationPanel = ({ pluginId, steps }: Props) => {
const [showAllSteps, setShowAllSteps] = useState(false)
+ const [inspectedStep, setInspectedStep] = useState(null)
const visibleEntries = useMemo(
() =>
(showAllSteps ? steps : steps.slice(-30))
@@ -62,14 +67,40 @@ export const ExplanationPanel = ({ steps }: Props) => {
data-active={index === 0}
>
- {sequence}. {step.ruleName}
+ {sequence}.{' '}
+
+ {step.ruleName}
+
+
+
+ {step.inferenceDetails ? (
+ <>
+ setInspectedStep(step)}
+ >
+ [View details]
+ {' '}
+ >
+ ) : null}
+ {step.message}
- {step.message}
{buildStepMeta(step)}
))
)}
+ {inspectedStep?.inferenceDetails ? (
+ setInspectedStep(null)}
+ />
+ ) : null}
)
}
diff --git a/src/features/puzzlePreview/PuzzlePreviewBoard.test.tsx b/src/features/puzzlePreview/PuzzlePreviewBoard.test.tsx
new file mode 100644
index 0000000..0ae48ce
--- /dev/null
+++ b/src/features/puzzlePreview/PuzzlePreviewBoard.test.tsx
@@ -0,0 +1,43 @@
+import { cleanup, render } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cellKey } from '../../domain/ir/keys'
+import { createMasyuPuzzle } from '../../domain/ir/masyu'
+import { PuzzlePreviewBoard } from './PuzzlePreviewBoard'
+
+describe('PuzzlePreviewBoard', () => {
+ afterEach(() => {
+ cleanup()
+ vi.restoreAllMocks()
+ })
+
+ it('draws Masyu pearls in compact previews', () => {
+ const puzzle = createMasyuPuzzle(25, 25)
+ puzzle.cells[cellKey(2, 3)] = { clue: { kind: 'pearl', color: 'white' } }
+ puzzle.cells[cellKey(10, 12)] = { clue: { kind: 'pearl', color: 'black' } }
+ const arc = vi.fn()
+
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
+ clearRect: vi.fn(),
+ fillRect: vi.fn(),
+ beginPath: vi.fn(),
+ moveTo: vi.fn(),
+ lineTo: vi.fn(),
+ stroke: vi.fn(),
+ fill: vi.fn(),
+ arc,
+ } as unknown as CanvasRenderingContext2D)
+
+ render(
+ ,
+ )
+
+ expect(arc).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/src/features/puzzlePreview/PuzzlePreviewBoard.tsx b/src/features/puzzlePreview/PuzzlePreviewBoard.tsx
index c3036be..59987e8 100644
--- a/src/features/puzzlePreview/PuzzlePreviewBoard.tsx
+++ b/src/features/puzzlePreview/PuzzlePreviewBoard.tsx
@@ -74,6 +74,25 @@ const drawPuzzlePreview = (
}
}
+ if (puzzle.puzzleType === 'masyu') {
+ for (const [key, cell] of Object.entries(puzzle.cells)) {
+ if (cell.clue?.kind !== 'pearl') {
+ continue
+ }
+ const [row, col] = parseCellKey(key)
+ const centerX = offsetX + col * cellSize + cellSize / 2
+ const centerY = offsetY + row * cellSize + cellSize / 2
+ const radius = Math.max(1.2, cellSize * 0.28)
+ ctx.beginPath()
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
+ ctx.fillStyle = cell.clue.color === 'black' ? '#111827' : '#ffffff'
+ ctx.fill()
+ ctx.strokeStyle = '#111827'
+ ctx.lineWidth = Math.max(0.8, cellSize * 0.08)
+ ctx.stroke()
+ }
+ }
+
for (const [edge, state] of Object.entries(puzzle.edges)) {
const [v1, v2] = parseEdgeKey(edge)
const x1 = offsetX + v1[1] * cellSize
@@ -83,7 +102,9 @@ const drawPuzzlePreview = (
if (state.mark === 'line') {
ctx.strokeStyle = '#0284c7'
- ctx.lineWidth = isCompact ? Math.max(1.2, cellSize * 0.08) : Math.max(2, cellSize * 0.08)
+ ctx.lineWidth = isCompact
+ ? Math.max(1.2, cellSize * 0.08)
+ : Math.max(2, cellSize * 0.08)
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
@@ -91,9 +112,13 @@ const drawPuzzlePreview = (
} else if (state.mark === 'blank') {
const midX = (x1 + x2) / 2
const midY = (y1 + y2) / 2
- const crossSize = isCompact ? Math.max(1.8, cellSize * 0.16) : Math.max(3, cellSize * 0.18)
+ const crossSize = isCompact
+ ? Math.max(1.8, cellSize * 0.16)
+ : Math.max(3, cellSize * 0.18)
ctx.strokeStyle = '#94a3b8'
- ctx.lineWidth = isCompact ? Math.max(1, cellSize * 0.05) : Math.max(1.5, cellSize * 0.05)
+ ctx.lineWidth = isCompact
+ ? Math.max(1, cellSize * 0.05)
+ : Math.max(1.5, cellSize * 0.05)
ctx.beginPath()
ctx.moveTo(midX - crossSize, midY - crossSize)
ctx.lineTo(midX + crossSize, midY + crossSize)
@@ -103,7 +128,8 @@ const drawPuzzlePreview = (
}
}
- const shouldDrawVertices = !isCompact || cellSize >= 7
+ const shouldDrawVertices =
+ puzzle.puzzleType !== 'masyu' && (!isCompact || cellSize >= 7)
if (shouldDrawVertices) {
ctx.fillStyle = isCompact ? '#475569' : '#111827'
const vertexRadius = isCompact
@@ -112,7 +138,13 @@ const drawPuzzlePreview = (
for (let row = 0; row <= puzzle.rows; row += 1) {
for (let col = 0; col <= puzzle.cols; col += 1) {
ctx.beginPath()
- ctx.arc(offsetX + col * cellSize, offsetY + row * cellSize, vertexRadius, 0, Math.PI * 2)
+ ctx.arc(
+ offsetX + col * cellSize,
+ offsetY + row * cellSize,
+ vertexRadius,
+ 0,
+ Math.PI * 2,
+ )
ctx.fill()
}
}
diff --git a/src/features/solver/solverStore.test.ts b/src/features/solver/solverStore.test.ts
index a71066a..8c6f7ea 100644
--- a/src/features/solver/solverStore.test.ts
+++ b/src/features/solver/solverStore.test.ts
@@ -281,11 +281,11 @@ describe('solver timeline behavior', () => {
useSolverStore.getState().goToStep(0)
expect(useSolverStore.getState().traceStatsCache.points).toHaveLength(2)
- expect(buildTraceStatsView(useSolverStore.getState().traceStatsCache, 0).current.edgeCoverageRatio).toBe(0)
+ expect(buildTraceStatsView(useSolverStore.getState().traceStatsCache, 0).current.coverageRatios.edge).toBe(0)
useSolverStore.getState().goToStep(1)
- expect(buildTraceStatsView(useSolverStore.getState().traceStatsCache, 1).current.edgeCoverageRatio).toBe(0.25)
+ expect(buildTraceStatsView(useSolverStore.getState().traceStatsCache, 1).current.coverageRatios.edge).toBe(0.25)
})
it('jumps across a large replay trace and matches a full rebuild', () => {
diff --git a/src/features/solver/solverStore.ts b/src/features/solver/solverStore.ts
index c7473e6..27bed05 100644
--- a/src/features/solver/solverStore.ts
+++ b/src/features/solver/solverStore.ts
@@ -60,6 +60,7 @@ type SolverStore = {
terminalReport: TerminalSolveReport | null
displaySettings: DisplaySettings
loadPuzzle: (puzzle: PuzzleIR, options?: LoadPuzzleOptions) => void
+ loadDefaultPuzzle: (importError?: string) => void
importFromUrl: (url: string, pluginId?: string) => void
setSourceUrl: (url: string) => void
setPluginId: (pluginId: string) => void
@@ -316,6 +317,16 @@ export const useSolverStore = create((set, get) => ({
displaySettings: mergeDisplaySettings(nextPluginId, get().displaySettings),
})
},
+ loadDefaultPuzzle: (importError) => {
+ const puzzle = getSamplePuzzle()
+ get().loadPuzzle(puzzle, {
+ pluginId: 'slitherlink',
+ sourceUrl: DEFAULT_SLITHERLINK_SAMPLE_URL,
+ })
+ if (importError) {
+ set({ importError })
+ }
+ },
importFromUrl: (url, pluginId) => {
const activePluginId = pluginId ?? get().pluginId
const plugin = puzzleRegistry.get(activePluginId)
diff --git a/src/features/stats/StatsPanel.tsx b/src/features/stats/StatsPanel.tsx
index b45d62f..914d72d 100644
--- a/src/features/stats/StatsPanel.tsx
+++ b/src/features/stats/StatsPanel.tsx
@@ -2,10 +2,11 @@ import { useMemo, useState } from 'react'
import {
buildTraceStatsView,
type TraceStatsCache,
- type TraceChartPoint,
} from '../../domain/difficulty/traceStats'
+import { puzzleRegistry } from '../../domain/plugins/registry'
type Props = {
+ pluginId: string
traceStatsCache: TraceStatsCache
pointer: number
isRunning: boolean
@@ -18,6 +19,8 @@ type ChartSeries = {
values: number[]
}
+type ChartInterpolation = 'linear' | 'step-after'
+
const formatPercent = (ratio: number): string => `${(ratio * 100).toFixed(1)}%`
const formatDuration = (durationMs: number): string => {
@@ -48,7 +51,8 @@ const formatStepSummary = (stepNumbers: number[]): string => {
: `${stepNumbers.length} hits, steps ${first}-${last}`
}
-const clampRatio = (value: number): number => Math.min(1, Math.max(0, value))
+const clampValue = (value: number, maxValue: number): number =>
+ Math.min(maxValue, Math.max(0, value))
const MAX_RENDERED_POINTS = 400
@@ -64,11 +68,21 @@ const sampleChartValues = (values: number[]): Array<{ index: number; value: numb
return sampled.filter((item, index, arr) => index === 0 || item.index !== arr[index - 1].index)
}
+const accumulateValues = (values: number[]): number[] => {
+ let total = 0
+ return values.map((value) => {
+ total += value
+ return total
+ })
+}
+
const makePath = (
values: number[],
+ maxValue: number,
width: number,
height: number,
padding: { top: number; right: number; bottom: number; left: number },
+ interpolation: ChartInterpolation,
): string => {
if (values.length === 0) {
return ''
@@ -78,10 +92,19 @@ const makePath = (
const plotHeight = height - padding.top - padding.bottom
const xFor = (index: number): number =>
padding.left + (values.length <= 1 ? 0 : (index / (values.length - 1)) * plotWidth)
- const yFor = (value: number): number => padding.top + (1 - clampRatio(value)) * plotHeight
- return sampled
- .map(({ index, value }, sampledIndex) => `${sampledIndex === 0 ? 'M' : 'L'} ${xFor(index)} ${yFor(value)}`)
- .join(' ')
+ const yFor = (value: number): number =>
+ padding.top + (1 - clampValue(value, maxValue) / maxValue) * plotHeight
+ return sampled.reduce((path, point, sampledIndex) => {
+ const command = `${xFor(point.index)} ${yFor(point.value)}`
+ if (sampledIndex === 0) {
+ return `M ${command}`
+ }
+ if (interpolation === 'step-after') {
+ const previous = sampled[sampledIndex - 1]
+ return `${path} L ${xFor(point.index)} ${yFor(previous.value)} L ${command}`
+ }
+ return `${path} L ${command}`
+ }, '')
}
const TraceLineChart = ({
@@ -89,11 +112,19 @@ const TraceLineChart = ({
description,
series,
currentIndex,
+ maxValue,
+ formatValue,
+ interpolation = 'linear',
+ formatLegendValue,
}: {
title: string
description: string
series: ChartSeries[]
currentIndex: number
+ maxValue: number
+ formatValue: (value: number) => string
+ interpolation?: ChartInterpolation
+ formatLegendValue?: (series: ChartSeries, currentIndex: number) => string
}) => {
const width = 360
const height = 190
@@ -104,7 +135,9 @@ const TraceLineChart = ({
const plotHeight = height - padding.top - padding.bottom
const xFor = (index: number): number =>
padding.left + (maxLength <= 1 ? 0 : (index / (maxLength - 1)) * plotWidth)
- const yFor = (value: number): number => padding.top + (1 - clampRatio(value)) * plotHeight
+ const safeMaxValue = Math.max(1, maxValue)
+ const yFor = (value: number): number =>
+ padding.top + (1 - clampValue(value, safeMaxValue) / safeMaxValue) * plotHeight
return (
@@ -121,10 +154,10 @@ const TraceLineChart = ({
- 100%
+ {formatValue(maxValue)}
- 0%
+ {formatValue(0)}
0
@@ -135,7 +168,8 @@ const TraceLineChart = ({
{series.map((item) => (
(
- {item.label} {formatPercent(item.values[clampedIndex] ?? 0)}
+ {item.label}{' '}
+ {formatLegendValue?.(item, clampedIndex) ?? formatValue(item.values[clampedIndex] ?? 0)}
))}
@@ -177,32 +212,6 @@ const TraceLineChart = ({
)
}
-const getBoardProgressSeries = (points: TraceChartPoint[]): ChartSeries[] => [
- {
- label: 'Progress',
- color: '#0891b2',
- values: points.map((point) => point.boardProgressRatio),
- },
-]
-
-const getCoverageSeries = (points: TraceChartPoint[]): ChartSeries[] => [
- {
- label: 'Edge',
- color: '#2563eb',
- values: points.map((point) => point.edgeCoverageRatio),
- },
- {
- label: 'Cell',
- color: '#16a34a',
- values: points.map((point) => point.cellCoverageRatio),
- },
- {
- label: 'Vertex',
- color: '#d97706',
- values: points.map((point) => point.vertexCoverageRatio),
- },
-]
-
const MAX_RULE_BARS = 8
const RuleUsageBars = ({ rules }: { rules: ReturnType['rules'] }) => {
@@ -245,14 +254,47 @@ const RuleUsageBars = ({ rules }: { rules: ReturnType {
+export const StatsPanel = ({ pluginId, traceStatsCache, pointer, isRunning, onGoToStep }: Props) => {
const stats = useMemo(() => buildTraceStatsView(traceStatsCache, pointer), [pointer, traceStatsCache])
const [showRuleDetails, setShowRuleDetails] = useState(false)
- const boardProgressSeries = useMemo(
- () => getBoardProgressSeries(traceStatsCache.points),
+ const liveStats = puzzleRegistry.get(pluginId)?.liveStats
+ const coverageSeries = useMemo(
+ () =>
+ (liveStats?.coverageSeries ?? []).map((series) => ({
+ label: series.label,
+ color: series.color,
+ values: traceStatsCache.points.map((point) => point.coverageRatios[series.source]),
+ })),
+ [liveStats, traceStatsCache],
+ )
+ const durationIncrements = useMemo(
+ () => [
+ {
+ label: 'Full Step',
+ color: '#dc2626',
+ values: traceStatsCache.points.map((point) => point.stepDurationMs),
+ },
+ {
+ label: 'Matched Rule',
+ color: '#0891b2',
+ values: traceStatsCache.points.map((point) => point.ruleApplyMs),
+ },
+ ],
[traceStatsCache],
)
- const coverageSeries = useMemo(() => getCoverageSeries(traceStatsCache.points), [traceStatsCache])
+ const durationSeries = useMemo(
+ () =>
+ durationIncrements.map((series) => ({
+ ...series,
+ values: accumulateValues(series.values),
+ })),
+ [durationIncrements],
+ )
+ const maxDurationMs = Math.max(1, ...durationSeries.flatMap((series) => series.values))
+ const formatDurationLegend = (series: ChartSeries, currentIndex: number): string => {
+ const increments = durationIncrements.find((item) => item.label === series.label)
+ return `${formatDuration(series.values[currentIndex] ?? 0)} (+${formatDuration(increments?.values[currentIndex] ?? 0)} this step)`
+ }
return (
@@ -296,16 +338,22 @@ export const StatsPanel = ({ traceStatsCache, pointer, isRunning, onGoToStep }:
diff --git a/src/mdx.d.ts b/src/mdx.d.ts
new file mode 100644
index 0000000..c9665f3
--- /dev/null
+++ b/src/mdx.d.ts
@@ -0,0 +1,6 @@
+declare module '*.mdx' {
+ import type { ComponentType } from 'react'
+
+ const MDXContent: ComponentType
+ export default MDXContent
+}
diff --git a/tests/e2e/workspace.spec.ts b/tests/e2e/workspace.spec.ts
index 670207c..68bba91 100644
--- a/tests/e2e/workspace.spec.ts
+++ b/tests/e2e/workspace.spec.ts
@@ -2,6 +2,11 @@ import { expect, test } from '@playwright/test'
test('imports puzzle and applies next step', async ({ page }) => {
await page.goto('/puzzlekit-web/')
+ await expect(page).toHaveTitle('PuzzleKit Web')
+ await expect(page.locator('link[rel="icon"]')).toHaveAttribute(
+ 'href',
+ '/puzzlekit-web/favicon.svg',
+ )
await expect(page.getByRole('heading', { name: 'PuzzleKit Web' })).toBeVisible()
await page.getByRole('button', { name: 'Import URL' }).click()
await page.getByRole('button', { name: 'Next Step' }).click()
@@ -16,3 +21,54 @@ test('loads an editor preset into the solver', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'PuzzleKit Web' })).toBeVisible()
await expect(page.getByText('10 × 10')).toBeVisible()
})
+
+test('loads and reloads a canonical puzz.link payload deep link', async ({ page }) => {
+ const payload =
+ 'slither/10/10/q2111221ch6212b212611b61262cg1c6bb2121c2bcc621112bo'
+ const sourceUrl = `https://puzz.link/p?${payload}`
+ const urlInput = page.getByPlaceholder('Paste puzz.link, pzplus, pzv, or penpa URL')
+
+ await page.goto(`/puzzlekit-web/?p=${payload}`)
+ await expect(urlInput).toHaveValue(sourceUrl)
+ await expect(page.getByText('10 × 10')).toBeVisible()
+
+ await page.reload()
+ await expect(urlInput).toHaveValue(sourceUrl)
+ await expect(page.getByText('10 × 10')).toBeVisible()
+})
+
+test('falls back and reports an invalid deep link', async ({ page }) => {
+ await page.goto('/puzzlekit-web/?p=slither/10/10/dsew%3F')
+
+ await expect(page.getByRole('alertdialog', { name: 'Import failed' })).toBeVisible()
+ await expect(page.getByPlaceholder('Paste puzz.link, pzplus, pzv, or penpa URL')).toHaveValue(
+ /https:\/\/puzz\.link\/p\?slither\/18\/10\//,
+ )
+ await expect(page).toHaveURL(/p=slither\/10\/10\/dsew%3F/)
+})
+
+test('keeps the docs header aligned and compact across docs routes', async ({ page }) => {
+ await page.goto('/puzzlekit-web/')
+ const solverHeader = page.locator('.workspace-title')
+ const solverHeaderWidth = await solverHeader.evaluate(
+ (element) => element.getBoundingClientRect().width,
+ )
+
+ await page.goto('/puzzlekit-web/docs')
+ const docsHeader = page.locator('.workspace-title')
+ const docsIndexHeaderSize = await docsHeader.evaluate((element) => {
+ const rect = element.getBoundingClientRect()
+ return { height: rect.height, width: rect.width }
+ })
+
+ expect(docsIndexHeaderSize.width).toBeCloseTo(solverHeaderWidth, 0)
+
+ await page.goto(
+ '/puzzlekit-web/docs/slitherlink/rules/diagonal-adjacent-three-outer-corners',
+ )
+ const ruleHeaderHeight = await page
+ .locator('.workspace-title')
+ .evaluate((element) => element.getBoundingClientRect().height)
+
+ expect(ruleHeaderHeight).toBeCloseTo(docsIndexHeaderSize.height, 0)
+})
diff --git a/vite.config.ts b/vite.config.ts
index 7f70d68..3466dae 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
+import mdx from '@mdx-js/rollup'
// https://vite.dev/config/
export default defineConfig({
base: '/puzzlekit-web/',
- plugins: [react()],
+ plugins: [mdx(), react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts',