@krizaka/ui 3.20.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +1 -0
  2. package/dist/action-bar.js +4 -4
  3. package/dist/app-shell.js +1 -1
  4. package/dist/argument-card.js +4 -4
  5. package/dist/blocks.css +1 -1
  6. package/dist/button.js +3 -2
  7. package/dist/card.js +3 -3
  8. package/dist/{chunk-5MH64NIL.js → chunk-2L67YTNV.js} +2 -2
  9. package/dist/{chunk-2NB27LM7.js → chunk-3IN6TOPM.js} +4 -4
  10. package/dist/{chunk-VDR2EP6K.js → chunk-4QN5YJ3D.js} +17 -5
  11. package/dist/chunk-4QN5YJ3D.js.map +1 -0
  12. package/dist/chunk-AQSZJJAJ.js +126 -0
  13. package/dist/chunk-AQSZJJAJ.js.map +1 -0
  14. package/dist/{chunk-3GODX3D6.js → chunk-QDXV544C.js} +2 -2
  15. package/dist/chunk-QDXV544C.js.map +1 -0
  16. package/dist/chunk-R6NK6ZNY.js +1080 -0
  17. package/dist/chunk-R6NK6ZNY.js.map +1 -0
  18. package/dist/chunk-TDG7ED2W.js +1 -0
  19. package/dist/chunk-TDG7ED2W.js.map +1 -0
  20. package/dist/{chunk-LIPK4UCS.js → chunk-YCIBJ6OQ.js} +140 -209
  21. package/dist/chunk-YCIBJ6OQ.js.map +1 -0
  22. package/dist/code-block.js +17 -5
  23. package/dist/code-block.js.map +1 -1
  24. package/dist/comparison-table.d.ts +44 -0
  25. package/dist/comparison-table.js +84 -0
  26. package/dist/comparison-table.js.map +1 -0
  27. package/dist/data-table.js +19 -1066
  28. package/dist/data-table.js.map +1 -1
  29. package/dist/date-picker.js +5 -5
  30. package/dist/date-picker.js.map +1 -1
  31. package/dist/force-update-screen.d.ts +80 -0
  32. package/dist/force-update-screen.js +38 -0
  33. package/dist/force-update-screen.js.map +1 -0
  34. package/dist/genui.d.ts +219 -0
  35. package/dist/genui.js +506 -0
  36. package/dist/genui.js.map +1 -0
  37. package/dist/index.d.ts +17 -24
  38. package/dist/index.js +1 -1
  39. package/dist/maintenance-screen.d.ts +68 -0
  40. package/dist/maintenance-screen.js +35 -0
  41. package/dist/maintenance-screen.js.map +1 -0
  42. package/dist/marketing-layout.js +1 -1
  43. package/dist/marketing-layout.js.map +1 -1
  44. package/dist/media-grid.js +44 -152
  45. package/dist/media-grid.js.map +1 -1
  46. package/dist/meta-row.d.ts +54 -0
  47. package/dist/meta-row.js +35 -0
  48. package/dist/meta-row.js.map +1 -0
  49. package/dist/native.d.ts +92 -16
  50. package/dist/native.js +873 -814
  51. package/dist/native.js.map +1 -1
  52. package/dist/nav-bar.d.ts +30 -7
  53. package/dist/nav-bar.js +71 -17
  54. package/dist/nav-bar.js.map +1 -1
  55. package/dist/page-hero.js +5 -5
  56. package/dist/radio-group.js +1 -1
  57. package/dist/settings-layout.js +1 -1
  58. package/dist/settings-layout.js.map +1 -1
  59. package/dist/site-footer.js +2 -2
  60. package/dist/skip-link.js +1 -1
  61. package/dist/step-showcase.d.ts +62 -0
  62. package/dist/step-showcase.js +227 -0
  63. package/dist/step-showcase.js.map +1 -0
  64. package/dist/tabs.js +2 -2
  65. package/dist/tabs.js.map +1 -1
  66. package/dist/update-prompt.d.ts +86 -0
  67. package/dist/update-prompt.js +41 -0
  68. package/dist/update-prompt.js.map +1 -0
  69. package/dist/video-player.d.ts +1 -1
  70. package/dist/video-player.js +16 -3
  71. package/dist/video-player.js.map +1 -1
  72. package/package.json +108 -9
  73. package/registry/achievement.json +1 -1
  74. package/registry/action-bar.json +1 -1
  75. package/registry/activity-feed.json +1 -1
  76. package/registry/alert.json +1 -1
  77. package/registry/app-shell.json +1 -1
  78. package/registry/argument-card.json +1 -1
  79. package/registry/audio-player.json +1 -1
  80. package/registry/auth-layout.json +1 -1
  81. package/registry/avatar.json +1 -1
  82. package/registry/badge.json +1 -1
  83. package/registry/button.json +1 -1
  84. package/registry/card-grid.json +1 -1
  85. package/registry/card.json +1 -1
  86. package/registry/catalogue-page.json +1 -1
  87. package/registry/changelog-page.json +1 -1
  88. package/registry/chat.json +1 -1
  89. package/registry/checkbox.json +1 -1
  90. package/registry/chip-link.json +1 -1
  91. package/registry/chip.json +1 -1
  92. package/registry/choice-grid.json +1 -1
  93. package/registry/code-block.json +1 -1
  94. package/registry/combobox.json +1 -1
  95. package/registry/command.json +1 -1
  96. package/registry/comparison-table.json +207 -0
  97. package/registry/component-page.json +1 -1
  98. package/registry/confirm-button.json +1 -1
  99. package/registry/contact-page.json +1 -1
  100. package/registry/continue-prompt.json +1 -1
  101. package/registry/countdown.json +1 -1
  102. package/registry/cta-panel.json +1 -1
  103. package/registry/dashboard-layout.json +1 -1
  104. package/registry/data-table.json +1 -1
  105. package/registry/date-picker.json +3 -3
  106. package/registry/decision-list.json +1 -1
  107. package/registry/description-list.json +1 -1
  108. package/registry/detail-layout.json +1 -1
  109. package/registry/dialog.json +1 -1
  110. package/registry/docs-article-page.json +1 -1
  111. package/registry/docs-hub-page.json +1 -1
  112. package/registry/docs-layout.json +1 -1
  113. package/registry/document-viewer.json +1 -1
  114. package/registry/dropdown-menu.json +1 -1
  115. package/registry/empty-state.json +1 -1
  116. package/registry/error-page.json +1 -1
  117. package/registry/examples/comparison-table/versus.d.ts +4 -0
  118. package/registry/examples/comparison-table/versus.js +28 -0
  119. package/registry/examples/comparison-table/versus.tsx +20 -0
  120. package/registry/examples/dialog/native/sheet.dark.png +0 -0
  121. package/registry/examples/dialog/native/sheet.light.png +0 -0
  122. package/registry/examples/force-update-screen/default.d.ts +4 -0
  123. package/registry/examples/force-update-screen/default.js +25 -0
  124. package/registry/examples/force-update-screen/default.tsx +19 -0
  125. package/registry/examples/force-update-screen/native/default.d.ts +4 -0
  126. package/registry/examples/force-update-screen/native/default.dark.png +0 -0
  127. package/registry/examples/force-update-screen/native/default.js +24 -0
  128. package/registry/examples/force-update-screen/native/default.light.png +0 -0
  129. package/registry/examples/force-update-screen/native/default.tsx +18 -0
  130. package/registry/examples/genui/streaming.d.ts +4 -0
  131. package/registry/examples/genui/streaming.js +18 -0
  132. package/registry/examples/genui/streaming.tsx +14 -0
  133. package/registry/examples/genui/table.d.ts +4 -0
  134. package/registry/examples/genui/table.js +19 -0
  135. package/registry/examples/genui/table.tsx +14 -0
  136. package/registry/examples/maintenance-screen/default.d.ts +4 -0
  137. package/registry/examples/maintenance-screen/default.js +22 -0
  138. package/registry/examples/maintenance-screen/default.tsx +16 -0
  139. package/registry/examples/maintenance-screen/native/default.d.ts +4 -0
  140. package/registry/examples/maintenance-screen/native/default.dark.png +0 -0
  141. package/registry/examples/maintenance-screen/native/default.js +21 -0
  142. package/registry/examples/maintenance-screen/native/default.light.png +0 -0
  143. package/registry/examples/maintenance-screen/native/default.tsx +15 -0
  144. package/registry/examples/meta-row/native/video.d.ts +4 -0
  145. package/registry/examples/meta-row/native/video.dark.png +0 -0
  146. package/registry/examples/meta-row/native/video.js +11 -0
  147. package/registry/examples/meta-row/native/video.light.png +0 -0
  148. package/registry/examples/meta-row/native/video.tsx +5 -0
  149. package/registry/examples/meta-row/status.d.ts +4 -0
  150. package/registry/examples/meta-row/status.js +12 -0
  151. package/registry/examples/meta-row/status.tsx +7 -0
  152. package/registry/examples/meta-row/video.d.ts +4 -0
  153. package/registry/examples/meta-row/video.js +12 -0
  154. package/registry/examples/meta-row/video.tsx +7 -0
  155. package/registry/examples/nav-bar/family-product.js +16 -13
  156. package/registry/examples/nav-bar/family-product.tsx +19 -19
  157. package/registry/examples/step-showcase/desktop.d.ts +4 -0
  158. package/registry/examples/step-showcase/desktop.js +24 -0
  159. package/registry/examples/step-showcase/desktop.tsx +19 -0
  160. package/registry/examples/step-showcase/phone.d.ts +4 -0
  161. package/registry/examples/step-showcase/phone.js +24 -0
  162. package/registry/examples/step-showcase/phone.tsx +19 -0
  163. package/registry/examples/update-prompt/default.d.ts +4 -0
  164. package/registry/examples/update-prompt/default.js +24 -0
  165. package/registry/examples/update-prompt/default.tsx +18 -0
  166. package/registry/examples/update-prompt/native/default.d.ts +4 -0
  167. package/registry/examples/update-prompt/native/default.dark.png +0 -0
  168. package/registry/examples/update-prompt/native/default.js +23 -0
  169. package/registry/examples/update-prompt/native/default.light.png +0 -0
  170. package/registry/examples/update-prompt/native/default.tsx +17 -0
  171. package/registry/field.json +1 -1
  172. package/registry/file-upload.json +1 -1
  173. package/registry/filter-bar.json +1 -1
  174. package/registry/force-update-screen.json +323 -0
  175. package/registry/form.json +1 -1
  176. package/registry/gallery-page.json +1 -1
  177. package/registry/genui.json +409 -0
  178. package/registry/glow-card.json +1 -1
  179. package/registry/goal-gauge.json +1 -1
  180. package/registry/how-it-works-page.json +1 -1
  181. package/registry/image-cropper.json +1 -1
  182. package/registry/index.json +223 -4
  183. package/registry/iso.json +1 -1
  184. package/registry/kanban.json +1 -1
  185. package/registry/kbd.json +1 -1
  186. package/registry/landing-page.json +1 -1
  187. package/registry/landscape.json +1 -1
  188. package/registry/legal-page.json +1 -1
  189. package/registry/link-card.json +1 -1
  190. package/registry/list.json +1 -1
  191. package/registry/loader.json +1 -1
  192. package/registry/longform-page.json +1 -1
  193. package/registry/maintenance-screen.json +259 -0
  194. package/registry/map.json +2 -2
  195. package/registry/markdown.json +1 -1
  196. package/registry/marketing-layout.json +1 -1
  197. package/registry/marquee.json +1 -1
  198. package/registry/mascot-quote.json +1 -1
  199. package/registry/mascot-state.json +1 -1
  200. package/registry/mascots.json +1 -1
  201. package/registry/match-dialog.json +1 -1
  202. package/registry/media-capture.json +1 -1
  203. package/registry/media-grid.json +2 -2
  204. package/registry/meta-row.json +215 -0
  205. package/registry/meter.json +1 -1
  206. package/registry/nav-bar.json +58 -25
  207. package/registry/number-ticker.json +1 -1
  208. package/registry/page-header.json +1 -1
  209. package/registry/page-hero.json +1 -1
  210. package/registry/page.json +1 -1
  211. package/registry/pagination.json +1 -1
  212. package/registry/photo-grid.json +1 -1
  213. package/registry/pointer-light.json +1 -1
  214. package/registry/popover.json +1 -1
  215. package/registry/pricing-page.json +1 -1
  216. package/registry/product-page.json +1 -1
  217. package/registry/profile-card.json +1 -1
  218. package/registry/progress.json +1 -1
  219. package/registry/promise-card.json +1 -1
  220. package/registry/radio-group.json +2 -2
  221. package/registry/recipes.json +1 -1
  222. package/registry/scroll-stage.json +1 -1
  223. package/registry/search-field.json +1 -1
  224. package/registry/section-backdrop.json +1 -1
  225. package/registry/separator.json +1 -1
  226. package/registry/settings-layout.json +2 -2
  227. package/registry/showcase.json +1 -1
  228. package/registry/site-footer.json +13 -13
  229. package/registry/sites.json +1 -1
  230. package/registry/skeleton.json +1 -1
  231. package/registry/skip-link.json +2 -2
  232. package/registry/slider.json +1 -1
  233. package/registry/slot.json +1 -1
  234. package/registry/spinner.json +1 -1
  235. package/registry/split-view.json +1 -1
  236. package/registry/stage-banner.json +1 -1
  237. package/registry/stat.json +1 -1
  238. package/registry/status-page.json +1 -1
  239. package/registry/step-progress.json +1 -1
  240. package/registry/step-showcase.json +296 -0
  241. package/registry/stepper.json +1 -1
  242. package/registry/swipe-deck.json +1 -1
  243. package/registry/switch.json +1 -1
  244. package/registry/tabs.json +3 -3
  245. package/registry/tag-input.json +1 -1
  246. package/registry/tag-selector.json +1 -1
  247. package/registry/terminal-replay.json +1 -1
  248. package/registry/theme.json +1 -1
  249. package/registry/toast.json +1 -1
  250. package/registry/toc.json +1 -1
  251. package/registry/tooltip.json +1 -1
  252. package/registry/tree-view.json +1 -1
  253. package/registry/update-prompt.json +316 -0
  254. package/registry/verified-badge.json +1 -1
  255. package/registry/vertical-video.json +1 -1
  256. package/registry/video-player.json +4 -4
  257. package/registry/video-trimmer.json +1 -1
  258. package/registry/virtual.json +1 -1
  259. package/dist/chunk-3GODX3D6.js.map +0 -1
  260. package/dist/chunk-LIPK4UCS.js.map +0 -1
  261. package/dist/chunk-VDR2EP6K.js.map +0 -1
  262. /package/dist/{chunk-5MH64NIL.js.map → chunk-2L67YTNV.js.map} +0 -0
  263. /package/dist/{chunk-2NB27LM7.js.map → chunk-3IN6TOPM.js.map} +0 -0
@@ -46,7 +46,9 @@
46
46
  }
47
47
  ],
48
48
  "bestPractices": [
49
- "Five items at most on the bar; group the rest in a menu, each link with a name and one line (`NavBar.MenuLink`).",
49
+ "Five items at most on the bar; group the rest in a menu, each link with a name and one short line (`NavBar.MenuLink`, one line, ≤ 45 characters).",
50
+ "A product's site (dev.<product>): the product's mark alone (no `NavBar.Family`, no badge — the version goes in the footer), then **Product ▾** (what it does: how it works, its features, the demo) · **Platforms ▾** (web, mobile, desktop, self-host — omitted for a single-platform product) · **Docs** · Pricing when it exists; never a lone entry that belongs to a group (no top-level Demo, Mobile or Mac). The phone panel repeats the groups with `NavBar.MobileGroup`, in the same order.",
51
+ "Light one item at most: the group that holds the page (`active` on its `NavBar.Menu`), never two.",
50
52
  "Mark the current page with `active`: the light rests on it, `aria-current=\"page\"` says it.",
51
53
  "Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.",
52
54
  "One primary action at most in `NavBar.Actions` (a `Button` `size=\"sm\"` `shape=\"pill\"`).",
@@ -77,6 +79,10 @@
77
79
  "keys": "Esc",
78
80
  "action": "Closes the menu, the family or the mobile panel and returns the focus to its trigger."
79
81
  },
82
+ {
83
+ "keys": "Click outside",
84
+ "action": "Closes the open menu or the mobile panel."
85
+ },
80
86
  {
81
87
  "keys": "Enter / Space / ↓ (family)",
82
88
  "action": "Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover."
@@ -84,7 +90,8 @@
84
90
  ],
85
91
  "notes": [
86
92
  "A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.",
87
- "The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`.",
93
+ "The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`; a press outside closes it. `NavBar.MobileGroup` is a `group` named by its heading.",
94
+ "Menus open on click, Enter, Space or ↓, or when the pointer rests on the trigger (`delayDuration`, 200 ms) — passing over the bar opens nothing. While a menu is open the light stays on its trigger; moving on to a plain link closes the menu.",
88
95
  "The light, the chevron and the tightening do not move under `prefers-reduced-motion`; the current page is tinted before scripts load.",
89
96
  "The family is its own `nav` named by `labels.family`; its chevron is a button with the same name, `aria-expanded` and `aria-controls`. Each group is a list named by its heading, the current site says `aria-current=\"true\"` and “You are here”, a site not released says its status in words.",
90
97
  "`NavBar.Repo` is an icon link named by `label`; `NavBar.Badge` is plain text (`DEV · v1.2.3`), hidden below `sm`."
@@ -198,7 +205,7 @@
198
205
  },
199
206
  {
200
207
  "component": "NavBarItems",
201
- "description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see `NavBar.Mobile`).",
208
+ "description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n`NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n`delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\nmoving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.",
202
209
  "props": []
203
210
  },
204
211
  {
@@ -302,7 +309,7 @@
302
309
  },
303
310
  {
304
311
  "component": "NavBarMobile",
305
- "description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape and a click\non a link close it, the focus returns to the toggle.",
312
+ "description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n(`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\nto the toggle.",
306
313
  "props": [
307
314
  {
308
315
  "name": "closeLabel",
@@ -320,6 +327,19 @@
320
327
  }
321
328
  ]
322
329
  },
330
+ {
331
+ "component": "NavBarMobileGroup",
332
+ "description": "A group of the mobile panel: the links of one of the bar's menus under its name, in the same order.",
333
+ "props": [
334
+ {
335
+ "name": "label",
336
+ "type": "string",
337
+ "default": null,
338
+ "description": "The group's heading — the same words as the bar's menu (“Product”) — passed translated.",
339
+ "required": true
340
+ }
341
+ ]
342
+ },
323
343
  {
324
344
  "component": "NavBarMobileLink",
325
345
  "description": "A link of the mobile panel.",
@@ -369,10 +389,10 @@
369
389
  {
370
390
  "name": "family-product",
371
391
  "title": "dev.<product>",
372
- "description": "A product's site: its mark and the family, `DEV · v1.2.3`, How it works · Demo · Docs, and “Talk to us” on krizaka.com while sign-up is closed.",
392
+ "description": "A product's site, the standard of every Krizaka product: its mark alone, Product ▾ · Platforms ▾ · Docs with compact menus (icon · title · one line), Sign in and “Talk to us”; the phone panel groups the same entries.",
373
393
  "path": "examples/nav-bar/family-product.tsx",
374
394
  "module": "@krizaka/ui/registry/examples/nav-bar/family-product",
375
- "code": "import { OrazakaLogo } from \"@krizaka/ui\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { NavBar } from \"@krizaka/ui/nav-bar\";\nimport { familyLabels } from \"@krizaka/ui/sites\";\n\nconst links = [\n { href: \"#how\", label: \"How it works\" },\n { href: \"#demo\", label: \"Demo\" },\n { href: \"#docs\", label: \"Docs\" },\n];\n\n// dev.orazaka.com: the product's mark and the family, `DEV · v1.2.3`, three sections, and “Talk to us” on\n// krizaka.com while sign-up is closed.\nexport default function NavBarFamilyProduct() {\n return (\n <NavBar.Root label=\"Main\">\n <NavBar.Brand href=\"#home\"><OrazakaLogo size={22} />Orazaka</NavBar.Brand>\n <NavBar.Family current=\"orazaka\" labels={familyLabels.en} />\n <NavBar.Badge env=\"DEV\" version=\"1.2.3\" />\n <NavBar.Items>\n {links.map((l) => (\n <NavBar.Link key={l.href} href={l.href} active={l.href === \"#how\"}>{l.label}</NavBar.Link>\n ))}\n </NavBar.Items>\n <NavBar.Actions>\n <NavBar.Repo href=\"https://github.com/krizaka/orazaka\" label=\"Source on GitHub\" />\n <Button asChild variant=\"primary\" size=\"sm\" shape=\"pill\" className=\"hidden sm:inline-flex\">\n <a href=\"https://www.krizaka.com/contact?topic=orazaka\">Talk to us</a>\n </Button>\n <NavBar.Mobile openLabel=\"Open the menu\" closeLabel=\"Close the menu\">\n {links.map((l) => (\n <NavBar.MobileLink key={l.href} href={l.href}>{l.label}</NavBar.MobileLink>\n ))}\n <NavBar.Family current=\"orazaka\" labels={familyLabels.en} />\n </NavBar.Mobile>\n </NavBar.Actions>\n </NavBar.Root>\n );\n}\n"
395
+ "code": "import { OrazakaLogo } from \"@krizaka/ui\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { NavBar } from \"@krizaka/ui/nav-bar\";\n\n// The product-site standard: the mark alone, Product ▾ · Platforms ▾ · Docs, Sign in and one action; the phone panel\n// repeats the groups. Each menu link: icon · title · one line.\nconst groups = [\n { id: \"product\", label: \"Product\", links: [[\"#how\", \"How it works\", \"Install to first answer in three commands.\"], [\"#studios\", \"Studios\", \"Ready-made workflows, priced before you run.\"], [\"#demo\", \"Demo\", \"Real runs: prompts, images, videos.\"]] },\n { id: \"platforms\", label: \"Platforms\", links: [[\"#web\", \"Web\", \"The workspace in your browser.\"], [\"#mac\", \"Mac\", \"Orazaka Desktop for Apple Silicon.\"], [\"#self-host\", \"Self-host\", \"Install it on your own servers.\"]] },\n];\nconst icon = <svg data-kz-icon viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden><path d=\"M12 3 19.8 7.5v9L12 21l-7.8-4.5v-9Z\" /><circle data-node cx=\"12\" cy=\"12\" r=\"2.4\" fill=\"currentColor\" /></svg>;\n\nexport default function NavBarFamilyProduct() {\n return (\n <NavBar.Root label=\"Main\" floating={false}>\n <NavBar.Brand href=\"#home\"><OrazakaLogo size={22} />Orazaka</NavBar.Brand>\n <NavBar.Items>\n {groups.map((g) => (\n <NavBar.Menu key={g.id} value={g.id} label={g.label}>\n {g.links.map(([href, title, line]) => <NavBar.MenuLink key={href} href={href} title={title} description={line} icon={icon} />)}\n </NavBar.Menu>\n ))}\n <NavBar.Link href=\"#docs\">Docs</NavBar.Link>\n </NavBar.Items>\n <NavBar.Actions>\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"hidden md:inline-flex\"><a href=\"#sign-in\">Sign in</a></Button>\n <Button asChild variant=\"primary\" size=\"sm\" shape=\"cut\" className=\"hidden md:inline-flex\"><a href=\"https://www.krizaka.com/contact?topic=orazaka\">Talk to us</a></Button>\n <NavBar.Mobile openLabel=\"Open the menu\" closeLabel=\"Close the menu\">\n {groups.map((g) => (\n <NavBar.MobileGroup key={g.id} label={g.label}>\n {g.links.map(([href, title]) => <NavBar.MobileLink key={href} href={href}>{title}</NavBar.MobileLink>)}\n </NavBar.MobileGroup>\n ))}\n <NavBar.MobileLink href=\"#docs\">Docs</NavBar.MobileLink>\n </NavBar.Mobile>\n </NavBar.Actions>\n </NavBar.Root>\n );\n}\n"
376
396
  },
377
397
  {
378
398
  "name": "family-admin",
@@ -416,11 +436,11 @@
416
436
  },
417
437
  {
418
438
  "path": "marks/AkinazaLogo.tsx",
419
- "content": "import { AKINAZA as G } from \"./akinaza-geometry\";\nimport { a11y, type MarkProps, neutral, useIds, withAlpha } from \"./shared\";\n\nconst CSS = `\n.akm-orbit { transform-origin: 200px 200px; animation: akm-orbit 32s linear infinite; }\n.akm-door { animation: akm-door 3.8s ease-in-out infinite; }\n@keyframes akm-orbit { to { transform: rotate(360deg); } }\n@keyframes akm-door {\n 0%, 100% { filter: drop-shadow(0 0 4px ${withAlpha(G.pulse.color, G.pulse.low)}); }\n 50% { filter: drop-shadow(0 0 14px ${withAlpha(G.pulse.color, G.pulse.high)}); }\n}\n@media (prefers-reduced-motion: reduce) { .akm-orbit, .akm-door { animation: none; } }\n`;\n\n/**\n * The Akinaza mark, \"the open shop\": a filled awning with the Krizaka cut at its corner, scalloped, its stripes in\n * light; under it the shop front, two windows and the open door — a warm light that breathes. In the Akinaza gradient\n * (forest → copper, the brand's mark gradient of @krizaka/tokens, the same in dark and light); one node, a coin, rides\n * the dashed orbit. The family's grammar (orbit, ring, bold emblem, bright core). Below 48 px it crops to the emblem.\n */\nexport function AkinazaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"akm\", [\"gradient\", \"core\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <linearGradient id={id.core} x1=\"50%\" y1=\"100%\" x2=\"50%\" y2=\"0%\">\n {G.coreGradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={animated ? \"akm-orbit\" : undefined}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={fill} />\n </g>\n <path d={G.awning} fill={fill} />\n <path d={G.stripes} stroke={G.stripe.color} strokeOpacity={G.stripe.opacity} strokeWidth={G.stripe.width} strokeLinecap=\"round\" />\n <path d={G.front} fill={fill} />\n <path d={G.windows} fill={G.stripe.color} fillOpacity={G.stripe.opacity} />\n <rect className={animated ? \"akm-door\" : undefined} x={G.door.x} y={G.door.y} width={G.door.w} height={G.door.h} rx={G.door.rx} fill={`url(#${id.core})`} />\n </svg>\n );\n}\n"
439
+ "content": "import { AKINAZA as G } from \"./akinaza-geometry\";\nimport { a11y, familyMotion, type MarkProps, neutral, useIds } from \"./shared\";\n\nconst CSS = familyMotion(\"akm\", { ...G.motion, ...G.pulse });\n\n/**\n * The Akinaza mark, \"the cut log\": a log seen end-on in forest green, its year rings in light, cut at 45° by the beaver\n * (the Krizaka chamfer) with the fresh face in copper, and a copper heart that breathes — a shop built ring by ring.\n * One node, a coin, rides the dashed orbit. Below 48 px it crops to the emblem.\n */\nexport function AkinazaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"akm\", [\"glow\"] as const);\n const m = (cls: string) => (animated ? `akm-${cls}` : undefined);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={m(\"orbit\")}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={G.copper} />\n </g>\n <path d={G.log} fill={G.forest} />\n <path d={G.cut} fill={G.copper} />\n {G.rings.r.map((r) => (\n <circle key={r} cx={G.rings.cx} cy={G.rings.cy} r={r} stroke={G.highlight} strokeOpacity={G.rings.opacity} strokeWidth={G.rings.width} />\n ))}\n <g className={m(\"core\")}>\n <circle cx={G.heart.cx} cy={G.heart.cy} r={G.heart.r} fill={G.copper} />\n <circle cx={G.heart.cx} cy={G.heart.cy} r={G.heart.inner} fill={G.highlight} />\n </g>\n </svg>\n );\n}\n"
420
440
  },
421
441
  {
422
442
  "path": "marks/KachizakaLogo.tsx",
423
- "content": "import { KACHIZAKA as G } from \"./kachizaka-geometry\";\nimport { a11y, type MarkProps, neutral, useIds, withAlpha } from \"./shared\";\n\nconst CSS = `\n.kzm-orbit { transform-origin: 200px 200px; animation: kzm-orbit 28s linear infinite; }\n.kzm-seal { transform-box: fill-box; transform-origin: center; animation: kzm-seal 3.2s ease-in-out infinite; }\n@keyframes kzm-orbit { to { transform: rotate(360deg); } }\n@keyframes kzm-seal {\n 0%, 100% { transform: scale(1); filter: drop-shadow(0 0 4px ${withAlpha(G.pulse.color, G.pulse.low)}); }\n 50% { transform: scale(1.06); filter: drop-shadow(0 0 12px ${withAlpha(G.pulse.color, G.pulse.high)}); }\n}\n@media (prefers-reduced-motion: reduce) { .kzm-orbit, .kzm-seal { animation: none; } }\n`;\n\n/**\n * The Kachizaka mark, \"the sealed cup\": a filled trophy whose rim wears the Krizaka cut, two handles, a stem and a\n * base, in the Kachizaka gradient (cobalt → gold, the brand's mark gradient of @krizaka/tokens, the same in dark and\n * light); on the bowl a star in light — the seal of the rules, locked at kick-off — breathes; one node, the ball, rides\n * the dashed orbit. The family's grammar (orbit, ring, bold emblem, bright core). Below 48 px it crops to the emblem.\n */\nexport function KachizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"kzm\", [\"gradient\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={animated ? \"kzm-orbit\" : undefined}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={fill} />\n </g>\n <path d={G.handles} stroke={fill} strokeWidth={G.handleWidth} strokeLinecap=\"round\" />\n <path d={G.base} fill={fill} />\n <path d={G.cup} fill={fill} />\n <path className={animated ? \"kzm-seal\" : undefined} d={G.star} fill={G.starColor} />\n </svg>\n );\n}\n"
443
+ "content": "import { KACHIZAKA as G } from \"./kachizaka-geometry\";\nimport { a11y, familyMotion, type MarkProps, neutral, useIds } from \"./shared\";\n\nconst CSS = familyMotion(\"kcm\", { ...G.motion, ...G.pulse });\n\n/**\n * The Kachizaka mark, \"the podium\": three steps cut at their outer corners, the winner in gold and the others in cobalt\n * (the brand's two colours, kept apart), under a gold diamond — the family's lit node — that breathes: a friendly\n * leaderboard where only points change hands. One node, the ball, rides the dashed orbit. Below 48 px it crops to the\n * emblem.\n */\nexport function KachizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"kcm\", [\"glow\"] as const);\n const m = (cls: string) => (animated ? `kcm-${cls}` : undefined);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={m(\"orbit\")}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.ball.at[0]} cy={G.ball.at[1]} r={G.ball.r} fill={G.gold} />\n </g>\n <path d={G.steps.second} fill={G.cobalt} />\n <path d={G.steps.first} fill={G.gold} />\n <path d={G.steps.third} fill={G.cobalt} />\n <g className={m(\"core\")}>\n <path d={G.node.d} fill={G.gold} stroke={G.gold} strokeWidth={G.node.soften} strokeLinejoin=\"round\" />\n <path d={G.node.light} fill={G.highlight} />\n </g>\n </svg>\n );\n}\n"
424
444
  },
425
445
  {
426
446
  "path": "marks/KrizakaLogo.tsx",
@@ -440,23 +460,23 @@
440
460
  },
441
461
  {
442
462
  "path": "marks/YasakiaLogo.tsx",
443
- "content": "import { a11y, type MarkProps, neutral, useIds, withAlpha } from \"./shared\";\nimport { YASAKIA as G } from \"./yasakia-geometry\";\n\nconst CSS = `\n.yzm-orbit { transform-origin: 200px 200px; animation: yzm-orbit 30s linear infinite; }\n.yzm-core { transform-box: fill-box; transform-origin: center; animation: yzm-core 3.2s ease-in-out infinite; }\n@keyframes yzm-orbit { to { transform: rotate(360deg); } }\n@keyframes yzm-core {\n 0%, 100% { filter: drop-shadow(0 0 6px ${withAlpha(G.pulse.color, G.pulse.low)}); opacity: .9; }\n 50% { filter: drop-shadow(0 0 14px ${withAlpha(G.pulse.color, G.pulse.high)}); opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) { .yzm-orbit, .yzm-core { animation: none; } }\n`;\n\n/**\n * The Yasakia mark: two voices — two solid speech bubbles that overlap, the left a shade deeper — in the Yasakia\n * gradient (emerald → turquoise, the brand's mark gradient of @krizaka/tokens, the same in dark and light). Where they\n * overlap, what the two share, is the bright core: a lens lit from above that breathes. The pair, two nodes on the\n * dashed orbit, turns slowly around it — the family's grammar (orbit, ring, bold emblem, bright core). Below 48 px it\n * crops to the emblem.\n */\nexport function YasakiaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"yzm\", [\"gradient\", \"light\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <linearGradient id={id.light} gradientUnits=\"userSpaceOnUse\" x1=\"0\" y1={G.lens.y1} x2=\"0\" y2={G.lens.y2}>\n <stop offset=\"0%\" stopColor={G.highlight.color} stopOpacity={G.highlight.from} />\n <stop offset=\"100%\" stopColor={G.highlight.color} stopOpacity={G.highlight.to} />\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={animated ? \"yzm-orbit\" : undefined}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n {G.pair.at.map(([cx, cy]) => (\n <circle key={cx} cx={cx} cy={cy} r={G.pair.r} fill={fill} />\n ))}\n </g>\n {G.voices.map((v, i) => (\n <g key={v.body}>\n <path d={v.body} fill={fill} />\n <path d={v.tail} fill={fill} />\n {i === 0 ? <path d={`${v.body} ${v.tail}`} fill={G.shade.color} opacity={G.shade.opacity} /> : null}\n </g>\n ))}\n <g className={animated ? \"yzm-core\" : undefined}>\n <path d={G.lens.d} fill={fill} />\n <path d={G.lens.d} fill={`url(#${id.light})`} />\n </g>\n </svg>\n );\n}\n\n/** @deprecated Yazakia was renamed Yasakia (2026-10): use `YasakiaLogo`. Kept for one minor version. */\nexport const YazakiaLogo = YasakiaLogo;\n"
463
+ "content": "import { a11y, familyMotion, type MarkProps, neutral, useIds } from \"./shared\";\nimport { YASAKIA as G } from \"./yasakia-geometry\";\n\nconst CSS = familyMotion(\"yzm\", { ...G.motion, ...G.pulse });\n\n/**\n * The Yasakia mark, \"one conversation\": one speech bubble with a tail on each side (turned 180°, never mirrored, so it\n * can never read as a face) in the Yasakia gradient (emerald → turquoise, the same in dark and light), and the spark\n * inside it — the family's lit node — that breathes: talk first, both ways. One node rides the dashed orbit. Below 48 px\n * it crops to the emblem.\n */\nexport function YasakiaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"yzm\", [\"gradient\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n const m = (cls: string) => (animated ? `yzm-${cls}` : undefined);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={m(\"orbit\")}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={fill} />\n </g>\n <circle cx={G.bubble.cx} cy={G.bubble.cy} r={G.bubble.r} fill={fill} />\n <path d={G.tails.join(\" \")} fill={fill} />\n <g className={m(\"core\")}>\n <path d={G.spark.d} fill={G.highlight} stroke={G.highlight} strokeWidth={G.spark.soften} strokeLinejoin=\"round\" />\n </g>\n </svg>\n );\n}\n\n/** @deprecated Yazakia was renamed Yasakia (2026-10): use `YasakiaLogo`. Kept for one minor version. */\nexport const YazakiaLogo = YasakiaLogo;\n"
444
464
  },
445
465
  {
446
466
  "path": "marks/YomizakaLogo.tsx",
447
- "content": "import { a11y, type MarkProps, neutral, useIds, withAlpha } from \"./shared\";\nimport { YOMIZAKA as G } from \"./yomizaka-geometry\";\n\nconst CSS = `\n.ymm-orbit { transform-origin: 200px 200px; animation: ymm-orbit 32s linear infinite; }\n.ymm-scan { animation: ymm-scan 3.6s cubic-bezier(.45,0,.55,1) infinite; }\n.ymm-lens { transform-box: fill-box; transform-origin: center; animation: ymm-lens 3.6s ease-in-out infinite; }\n@keyframes ymm-orbit { to { transform: rotate(360deg); } }\n@keyframes ymm-scan { 0% { transform: translateY(0); opacity: 0; } 12%, 80% { opacity: 1; } 100% { transform: translateY(${G.scan.travel}px); opacity: 0; } }\n@keyframes ymm-lens {\n 0%, 100% { filter: drop-shadow(0 0 6px ${withAlpha(G.pulse.color, G.pulse.low)}); }\n 50% { filter: drop-shadow(0 0 16px ${withAlpha(G.pulse.color, G.pulse.high)}); }\n}\n@media (prefers-reduced-motion: reduce) { .ymm-orbit, .ymm-scan, .ymm-lens { animation: none; } .ymm-scan { opacity: 0; } }\n`;\n\n/**\n * The Yomizaka mark, \"the reading\": a filled page with the Krizaka cut at its corner, three lines of text in light, and\n * a lens — the eye that reads — resting on its lower half, its handle running out of the page. Page and lens in the\n * Yomizaka gradient (ink → indigo, the brand's mark gradient of @krizaka/tokens, the same in dark and light); the\n * lens' core is a pale indigo light that breathes; a line of light scans the page from top to bottom; one node, a\n * page, rides the dashed orbit. The family's grammar (orbit, ring, bold emblem, bright core). Below 48 px it crops to\n * the emblem.\n */\nexport function YomizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"ymm\", [\"gradient\", \"core\", \"glow\", \"page\"] as const);\n const fill = `url(#${id.gradient})`;\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <linearGradient id={id.core} x1=\"50%\" y1=\"100%\" x2=\"50%\" y2=\"0%\">\n {G.coreGradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n <clipPath id={id.page}>\n <path d={G.page} />\n </clipPath>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={animated ? \"ymm-orbit\" : undefined}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={fill} />\n </g>\n <path d={G.page} fill={fill} />\n <path d={G.lines} stroke={G.line.color} strokeOpacity={G.line.opacity} strokeWidth={G.line.width} strokeLinecap=\"round\" />\n {animated ? (\n <g clipPath={`url(#${id.page})`}>\n <path className=\"ymm-scan\" d={G.scan.d} stroke={G.scan.color} strokeOpacity={G.scan.opacity} strokeWidth={G.scan.width} strokeLinecap=\"round\" />\n </g>\n ) : null}\n <path d={G.handle} stroke={fill} strokeWidth={G.handleWidth} strokeLinecap=\"round\" />\n <g className={animated ? \"ymm-lens\" : undefined}>\n <circle cx={G.lens.cx} cy={G.lens.cy} r={G.lens.r} stroke={fill} strokeWidth={G.lens.width} fill={`url(#${id.core})`} />\n <circle cx={G.lens.glint.cx} cy={G.lens.glint.cy} r={G.lens.glint.r} fill={G.highlight} />\n </g>\n </svg>\n );\n}\n"
467
+ "content": "import { a11y, familyMotion, type MarkProps, neutral, useIds } from \"./shared\";\nimport { YOMIZAKA as G } from \"./yomizaka-geometry\";\n\nconst CSS = familyMotion(\"ymm\", { ...G.motion, ...G.pulse });\n\n/**\n * The Yomizaka mark, \"the watch\": the owl — two lenses at the family stroke under one calm brow, its pupils glancing\n * down the line it reads — in the Yomizaka gradient (ink → indigo, the same in dark and light). The pupils are the lit\n * cores and breathe; one node, a page, rides the dashed orbit. Below 48 px it crops to the emblem.\n */\nexport function YomizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"ymm\", [\"gradient\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n const m = (cls: string) => (animated ? `ymm-${cls}` : undefined);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={m(\"orbit\")}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={fill} />\n </g>\n <path d={G.brow} stroke={fill} strokeWidth={G.stroke} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n {G.lenses.map((l) => (\n <circle key={l.cx} cx={l.cx} cy={l.cy} r={l.r} stroke={fill} strokeWidth={G.stroke} />\n ))}\n <g className={m(\"core\")}>\n {G.pupils.map((p) => (\n <g key={p.cx}>\n <circle cx={p.cx} cy={p.cy} r={p.r} fill={fill} />\n <circle cx={p.cx} cy={p.cy} r={p.inner} fill={G.highlight} />\n </g>\n ))}\n </g>\n </svg>\n );\n}\n"
448
468
  },
449
469
  {
450
470
  "path": "marks/YororisLogo.tsx",
451
- "content": "import { a11y, type MarkProps, neutral, useIds, withAlpha } from \"./shared\";\nimport { YORORIS as G } from \"./yororis-geometry\";\n\nconst CSS = `\n.yrm-orbit { transform-origin: 200px 200px; animation: yrm-orbit 36s linear infinite; }\n.yrm-vein { animation: yrm-vein 4.8s linear infinite; }\n.yrm-core { transform-box: fill-box; transform-origin: center; animation: yrm-core 4.2s ease-in-out infinite; }\n@keyframes yrm-orbit { to { transform: rotate(360deg); } }\n@keyframes yrm-vein { to { stroke-dashoffset: 64; } }\n@keyframes yrm-core {\n 0%, 100% { filter: drop-shadow(0 0 6px ${withAlpha(G.pulse.color, G.pulse.low)}); opacity: .9; }\n 50% { filter: drop-shadow(0 0 16px ${withAlpha(G.pulse.color, G.pulse.high)}); opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) { .yrm-orbit, .yrm-vein, .yrm-core { animation: none; } }\n`;\n\n/**\n * The Yororis mark, \"the hold\": a \"Y\" whose arms are two filled leaves — open hands — rising from one stem and cupping\n * a warm core that breathes: held, given room, listened to. Leaves in the Yororis gradient (sand → sage, the brand's\n * mark gradient of @krizaka/tokens, the same in dark and light), a vein of light along each, the core in a warm sand\n * light; the moment, one node on the dashed orbit, comes round slowly. The family's grammar (orbit, ring, bold emblem,\n * bright core) with the slowest breath of the family. Below 48 px it crops to the emblem.\n */\nexport function YororisLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"yrm\", [\"gradient\", \"core\", \"glow\"] as const);\n const fill = `url(#${id.gradient})`;\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <linearGradient id={id.gradient} gradientUnits=\"userSpaceOnUse\" x1={G.gradient.x1} y1={G.gradient.y1} x2={G.gradient.x2} y2={G.gradient.y2}>\n {G.gradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <linearGradient id={id.core} x1=\"50%\" y1=\"100%\" x2=\"50%\" y2=\"0%\">\n {G.coreGradient.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={animated ? \"yrm-orbit\" : undefined}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.moment.at[0]} cy={G.moment.at[1]} r={G.moment.r} fill={fill} />\n </g>\n <path d={G.stem} stroke={fill} strokeWidth={G.stroke} strokeLinecap=\"round\" />\n {G.leaves.map((d) => (\n <path key={d} d={d} fill={fill} />\n ))}\n <path className={animated ? \"yrm-vein\" : undefined} d={G.veins} stroke={G.vein.color} strokeOpacity={G.vein.opacity} strokeWidth={G.vein.width} strokeLinecap=\"round\" strokeDasharray={G.vein.dash} />\n <g className={animated ? \"yrm-core\" : undefined}>\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.r} fill={`url(#${id.core})`} />\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.inner} fill={G.highlight} />\n </g>\n </svg>\n );\n}\n"
471
+ "content": "import { a11y, familyMotion, type MarkProps, neutral, useIds } from \"./shared\";\nimport { YORORIS as G } from \"./yororis-geometry\";\n\nconst CSS = familyMotion(\"yrm\", { ...G.motion, ...G.pulse });\n\n/**\n * The Yororis mark, \"held\": two open hands, two sage arcs of one circle, cup a warm sand core without closing on it —\n * held, given room, free to go. The core breathes, the slowest breath of the family; one node, the moment, rides the\n * dashed orbit. Below 48 px it crops to the emblem.\n */\nexport function YororisLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"yrm\", [\"glow\"] as const);\n const m = (cls: string) => (animated ? `yrm-${cls}` : undefined);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <style>{CSS}</style>\n <defs>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx={G.center} cy={G.center} r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle cx={G.center} cy={G.center} r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g className={m(\"orbit\")}>\n <circle cx={G.center} cy={G.center} r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx={G.node.at[0]} cy={G.node.at[1]} r={G.node.r} fill={G.sage} />\n </g>\n <path d={G.hands} stroke={G.sage} strokeWidth={G.stroke} strokeLinecap=\"round\" />\n <g className={m(\"core\")}>\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.r} fill={G.sand} />\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.inner} fill={G.highlight} />\n </g>\n </svg>\n );\n}\n"
452
472
  },
453
473
  {
454
474
  "path": "marks/akinaza-geometry.ts",
455
- "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Akinaza mark's geometry — drawn by the web mark (AkinazaLogo) and the native one (AkinazaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold filled emblem in the\n * brand gradient, a bright core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **the open shop** (商い, *akinai*: trade): a filled awning whose upper-right corner wears the Krizaka\n * cut, scalloped below, its stripes in light; under it the shop front and its open door — the bright core, a warm\n * light that breathes (the shop is open). The gradient is the brand's mark gradient (`marks.akinaza` of\n * @krizaka/tokens): forest at the foot → copper at the awning, mid-tones that read ≥ 3:1 on the page surfaces of both\n * themes (tested in @krizaka/tokens). One node, a coin, rides the dashed orbit.\n */\n/** The copper end of the gradient: the halo. */\nconst copper = marks.akinaza[marks.akinaza.length - 1]![1];\n\nexport const AKINAZA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n /** The awning: 124 → 276 at y 108, cut 28 at its upper-right corner, four scallops below (y 150, 19 deep). */\n awning: \"M124 108 H248 L276 136 V150 A19 19 0 0 1 238 150 A19 19 0 0 1 200 150 A19 19 0 0 1 162 150 A19 19 0 0 1 124 150 Z\",\n /** The awning's stripes, in light. */\n stripes: \"M162 116 V146 M200 116 V146 M238 116 V140\",\n stripe: { color: \"#fff7ed\", opacity: 0.55, width: 8 },\n /** The shop front: walls 140 → 260 from y 178 to the ground, the door cut out (178 → 222, from y 222). */\n front: \"M140 178 H260 V300 H222 V222 H178 V300 H140 Z\",\n /** The open door: the bright core, a warm light. */\n door: { x: 182, y: 226, w: 36, h: 74, rx: 4 },\n coreGradient: { stops: [[\"0%\", \"#fde7c7\"], [\"100%\", \"#fffaf2\"]] as const },\n /** A window each side, in light. */\n windows: \"M154 196 H168 V212 H154 Z M232 196 H246 V212 H232 Z\",\n gradient: { x1: 140, y1: 300, x2: 276, y2: 108, stops: marks.akinaza },\n /** A coin: one node on the orbit, that turns with it. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: copper, opacity: 0.16 },\n /** The door's breathing light (web), in the copper. */\n pulse: { color: copper, low: 0.3, high: 0.65 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
475
+ "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Akinaza mark's geometry — drawn by the web mark (AkinazaLogo) and the native one (AkinazaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold solid emblem, a lit core;\n * below 48 px the view box crops to the emblem.\n *\n * The emblem is **the cut log** (2026-10-17): a log seen end-on, its year rings in light, cut at 45° by the beaver —\n * the cut is the Krizaka chamfer, its fresh face in copper — and a copper heart that breathes: a shop built ring by\n * ring, year after year. Forest and copper stay **apart** (blended they turn brown): they are the two stops of the\n * brand's mark gradient (`marks.akinaza` of @krizaka/tokens), each ≥ 3:1 on the page surfaces of both themes.\n *\n * Literal geometry (no clip, so web and native draw the same thing): the log is a circle r 96 centred (194, 206) — set\n * off the centre so the cut log sits optically centred — minus the segment beyond the 45° chord at 70 from its centre;\n * the copper face is the band between that chord and a parallel one at 58, bounded by the circle.\n */\nconst forest = marks.akinaza[0]![1];\n/** The copper end: the cut, the heart, the halo and its breath. */\nconst copper = marks.akinaza[marks.akinaza.length - 1]![1];\n\nexport const AKINAZA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n forest,\n copper,\n log: \"M197 110 L290 203 A96 96 0 1 1 197 110 Z\",\n /** The fresh face of the cut: a copper band along the chord. */\n cut: \"M197 110 L290 203 A96 96 0 0 1 289.1 219.1 L180.9 110.9 A96 96 0 0 1 197 110 Z\",\n /** The year rings, in light, inside the band. */\n rings: { cx: 194, cy: 206, r: [52, 28], width: 7, opacity: 0.42 },\n heart: { cx: 194, cy: 206, r: 16, inner: 5.4 },\n highlight: \"#fff7ed\",\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.akinaza },\n /** A coin: one node on the orbit, that turns with it. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: copper, opacity: 0.16 },\n pulse: { color: copper, low: 0.3, high: 0.65 },\n motion: { spin: 32, breath: 3.8 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
456
476
  },
457
477
  {
458
478
  "path": "marks/kachizaka-geometry.ts",
459
- "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Kachizaka mark's geometry — drawn by the web mark (KachizakaLogo) and the native one (KachizakaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold filled emblem in the\n * brand gradient, a bright core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **the sealed cup**: a filled trophy whose rim wears the Krizaka cut at its upper-right corner, two\n * handles, a stem and a base; on the bowl a star in light — the seal, the rules locked at kick-off — breathes. The\n * gradient is the brand's mark gradient (`marks.kachizaka` of @krizaka/tokens): cobalt at the base → gold at the rim,\n * mid-tones that read ≥ 3:1 on the page surfaces of both themes (tested in @krizaka/tokens), so one mark serves dark\n * and light. One node, the ball, rides the dashed orbit.\n */\n/** The gold end of the gradient: the halo and the seal's breath. */\nconst gold = marks.kachizaka[marks.kachizaka.length - 1]![1];\n\nexport const KACHIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n /** The bowl: a rim 136 → 264 at y 104, cut 32 at its upper-right corner (the Krizaka chamfer), rounding down to the stem. */\n cup: \"M136 104 H232 L264 136 V164 C264 214 236 242 200 246 C164 242 136 214 136 164 Z\",\n /** The stem and the base, one shape. */\n base: \"M190 244 H210 V272 H240 C244 272 248 276 248 280 V300 H152 V280 C152 276 156 272 160 272 H190 Z\",\n /** The two handles, stroked in the gradient. */\n handles: \"M138 128 C100 124 100 196 150 202 M262 128 C300 124 300 196 250 202\",\n handleWidth: 14,\n /** The seal: a five-pointed star in light on the bowl, centred (200, 172), radii 30 / 13. */\n star: \"M200 142 L207.6 161.5 L228.5 162.7 L212.4 176 L217.6 196.3 L200 185 L182.4 196.3 L187.6 176 L171.5 162.7 L192.4 161.5 Z\",\n starColor: \"#fffbea\",\n gradient: { x1: 136, y1: 300, x2: 264, y2: 104, stops: marks.kachizaka },\n /** The ball: one node on the orbit, that turns with it. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: gold, opacity: 0.18 },\n /** The seal's breathing shadow (web), in the gold. */\n pulse: { color: gold, low: 0.35, high: 0.75 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
479
+ "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Kachizaka mark's geometry — drawn by the web mark (KachizakaLogo) and the native one (KachizakaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold solid emblem, a lit core;\n * below 48 px the view box crops to the emblem.\n *\n * The emblem is **the podium** (2026-10-17): three steps, their outer corners wearing the Krizaka cut, the winner in\n * gold and the others in cobalt, under the family's lit node (a diamond) that breathes — a friendly leaderboard where\n * only points change hands. The two colours of the brand stay **apart** (cobalt plays, gold wins): blended, cobalt →\n * gold turns grey. They are the two stops of the brand's mark gradient (`marks.kachizaka` of @krizaka/tokens), each\n * ≥ 3:1 on the page surfaces of both themes.\n */\nconst cobalt = marks.kachizaka[0]![1];\n/** The gold end: the winner, the node, the halo and its breath. */\nconst gold = marks.kachizaka[marks.kachizaka.length - 1]![1];\n\nexport const KACHIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n cobalt,\n gold,\n /** The steps: second (left, cut top-left), first (centre, both top corners cut, in gold), third (right, cut top-right). */\n steps: {\n second: \"M102 240 L122 220 H162 V296 H102 Z\",\n first: \"M170 192 L188 174 H212 L230 192 V296 H170 Z\",\n third: \"M238 248 H278 L298 268 V296 H238 Z\",\n },\n /** The lit node over the winner: a gold diamond (half-diagonal 22, softened by a 6-unit round stroke), its light inside. */\n node: { d: \"M200 102 L222 124 L200 146 L178 124 Z\", light: \"M200 116.1 L207.9 124 L200 131.9 L192.1 124 Z\", soften: 6 },\n highlight: \"#fffbea\",\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.kachizaka },\n /** The ball: one node on the orbit, that turns with it. */\n ball: { r: 9, at: [200, 40] as const },\n glow: { color: gold, opacity: 0.18 },\n pulse: { color: gold, low: 0.35, high: 0.75 },\n motion: { spin: 28, breath: 3.2 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
460
480
  },
461
481
  {
462
482
  "path": "marks/krizaka-geometry.ts",
@@ -472,43 +492,43 @@
472
492
  },
473
493
  {
474
494
  "path": "marks/shared.ts",
475
- "content": "import { useId } from \"react\";\n\nexport interface MarkProps {\n /** Width and height in pixels. */\n size?: number;\n /** Orbit, pulse and shimmer. Off with `false`; always off under prefers-reduced-motion. */\n animated?: boolean;\n /** An accessible name (the mark becomes an image); without it the mark is decorative. */\n title?: string;\n className?: string;\n}\n\n/** A neutral stroke: the host's `--kz-*` token when it has one, else the text colour at a low opacity. */\nexport const neutral = (token: string, percent: number) => `var(${token}, color-mix(in srgb, currentColor ${percent}%, transparent))`;\n\n/** Ids unique per instance, so several marks on a page never share a gradient. */\nexport function useIds<K extends string>(prefix: string, keys: readonly K[]): Record<K, string> {\n const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n return Object.fromEntries(keys.map((k) => [k, `${prefix}-${k}-${uid}`])) as Record<K, string>;\n}\n\nexport const a11y = (title?: string) => (title ? { role: \"img\" as const, \"aria-label\": title } : { \"aria-hidden\": true as const });\n\n/** `#rrggbb` + an opacity → `#rrggbbaa` (a brand colour in a CSS shadow). */\nexport const withAlpha = (hex: string, opacity: number) => `${hex}${Math.round(opacity * 255).toString(16).padStart(2, \"0\")}`;\n"
495
+ "content": "import { useId } from \"react\";\n\nexport interface MarkProps {\n /** Width and height in pixels. */\n size?: number;\n /** Orbit, pulse and shimmer. Off with `false`; always off under prefers-reduced-motion. */\n animated?: boolean;\n /** An accessible name (the mark becomes an image); without it the mark is decorative. */\n title?: string;\n className?: string;\n}\n\n/** A neutral stroke: the host's `--kz-*` token when it has one, else the text colour at a low opacity. */\nexport const neutral = (token: string, percent: number) => `var(${token}, color-mix(in srgb, currentColor ${percent}%, transparent))`;\n\n/** Ids unique per instance, so several marks on a page never share a gradient. */\nexport function useIds<K extends string>(prefix: string, keys: readonly K[]): Record<K, string> {\n const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n return Object.fromEntries(keys.map((k) => [k, `${prefix}-${k}-${uid}`])) as Record<K, string>;\n}\n\nexport const a11y = (title?: string) => (title ? { role: \"img\" as const, \"aria-label\": title } : { \"aria-hidden\": true as const });\n\n/** `#rrggbb` + an opacity → `#rrggbbaa` (a brand colour in a CSS shadow). */\nexport const withAlpha = (hex: string, opacity: number) => `${hex}${Math.round(opacity * 255).toString(16).padStart(2, \"0\")}`;\n\n/**\n * The family's motion (web), for a mark whose classes start with `prefix`: `<prefix>-orbit` turns the dashed orbit and\n * its node, `<prefix>-core` makes the lit core breathe (a shadow in the brand colour). Still under prefers-reduced-motion.\n */\nexport const familyMotion = (prefix: string, { spin, breath, color, low, high }: { spin: number; breath: number; color: string; low: number; high: number }) => `\n.${prefix}-orbit { transform-origin: 200px 200px; animation: ${prefix}-orbit ${spin}s linear infinite; }\n.${prefix}-core { transform-box: fill-box; transform-origin: center; animation: ${prefix}-core ${breath}s ease-in-out infinite; }\n@keyframes ${prefix}-orbit { to { transform: rotate(360deg); } }\n@keyframes ${prefix}-core {\n 0%, 100% { filter: drop-shadow(0 0 6px ${withAlpha(color, low)}); opacity: .92; }\n 50% { filter: drop-shadow(0 0 16px ${withAlpha(color, high)}); opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) { .${prefix}-orbit, .${prefix}-core { animation: none; } }\n`;\n"
476
496
  },
477
497
  {
478
498
  "path": "marks/yasakia-geometry.ts",
479
- "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yasakia mark's geometry — drawn by the web mark (YasakiaLogo) and the native one (YasakiaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold emblem in the brand\n * gradient, a bright core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **two voices**: two solid speech bubbles, mirrored, that overlap. Where they overlap — what the two\n * people share once they have talked — is the bright core, a lens lit from above that breathes. The left voice sits a\n * shade deeper than the right, so the pair reads as two, with depth, at 16 px as at 128 px. A thin gap (the page shows\n * through) separates each bubble from the lens: the paths are cut here, no mask, so web and native draw the same thing.\n * Two nodes, the pair, ride the dashed orbit. The gradient is the brand's mark gradient (`marks.yasakia` of\n * @krizaka/tokens): emerald → turquoise, mid-tones that read ≥ 3:1 on the page surfaces of both themes (tested in\n * @krizaka/tokens), so one mark serves dark and light.\n */\n/** The bright end of the gradient (turquoise): the halo and the core's glow. */\nconst turquoise = marks.yasakia[marks.yasakia.length - 1]![1];\n\n/*\n * How the paths are drawn (literal, so the native bundle carries no geometry code): each bubble is a circle of radius 80\n * centred at y 184, 36 on either side of x = 200 (164 and 236); they overlap in the lens (x = 200, y 112.6 → 255.4).\n * Each bubble is cut by the other's circle grown by the gap (r 87): the cut runs from (191.9, 109) to (191.9, 259) on\n * the left, mirrored on the right. Each tail is a curved point from the bubble's lower outer side, down and out to\n * (88, 294) — mirrored (312, 294).\n */\nexport const YASAKIA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n /** The two voices, left then right: each a bubble (cut around the lens) and its tail. */\n voices: [\n { body: \"M191.9 109 A80 80 0 1 0 191.9 259 A87 87 0 0 1 191.9 109 Z\", tail: \"M98 230 Q104 268 88 294 Q122 288 144 262 L130 200 Z\" },\n { body: \"M208.1 109 A80 80 0 1 1 208.1 259 A87 87 0 0 0 208.1 109 Z\", tail: \"M302 230 Q296 268 312 294 Q278 288 256 262 L270 200 Z\" },\n ] as const,\n /** The left voice, a shade deeper: the depth between the two. */\n shade: { color: \"#000\", opacity: 0.22 },\n /** Where the bubbles overlap: the core, lit from above. */\n lens: { d: \"M200 112.6 A80 80 0 0 1 200 255.4 A80 80 0 0 1 200 112.6 Z\", y1: 112.6, y2: 255.4 },\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.yasakia },\n /** The light on the lens: bright at the top, fading down. */\n highlight: { color: \"#f0fdfa\", from: 0.75, to: 0.2 },\n /** The pair: two nodes on the orbit, opposite each other, that turn with it. */\n pair: { r: 8, at: [[40, 200], [360, 200]] as const },\n glow: { color: turquoise, opacity: 0.16 },\n /** The core's breathing shadow (web), in the mark's turquoise. */\n pulse: { color: turquoise, low: 0.3, high: 0.6 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
499
+ "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yasakia mark's geometry — drawn by the web mark (YasakiaLogo) and the native one (YasakiaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold solid emblem in the\n * brand gradient, a lit core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **one conversation** (2026-10-17): one speech bubble with a tail on each side — the conversation\n * belongs to both people, talk first — and the spark inside it, the family's lit node, breathes. The tails turn about\n * the centre (180°): the mark is symmetric by rotation, never by mirror, so it can never read as a face. The gradient\n * is the brand's mark gradient (`marks.yasakia` of @krizaka/tokens): emerald → turquoise, ≥ 3:1 on both themes.\n */\n/** The turquoise end of the gradient: the halo and the spark's breath. */\nconst turquoise = marks.yasakia[marks.yasakia.length - 1]![1];\n\nexport const YASAKIA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n bubble: { cx: 200, cy: 200, r: 86 },\n /** The two tails, out of the bubble at 115°–160° and, turned 180° about the centre, at 295°–340°. */\n tails: [\n \"M163.7 277.9 Q138 294 106 304 Q124 270 119.2 229.4 Z\",\n \"M236.3 122.1 Q262 106 294 96 Q276 130 280.8 170.6 Z\",\n ],\n /** The spark: a diamond of light (half-diagonal 24, softened by a 6-unit round stroke). */\n spark: { d: \"M200 176 L224 200 L200 224 L176 200 Z\", soften: 6 },\n highlight: \"#f0fdfa\",\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.yasakia },\n /** The two people: one node on the orbit, that turns with it. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: turquoise, opacity: 0.16 },\n pulse: { color: turquoise, low: 0.3, high: 0.6 },\n motion: { spin: 30, breath: 3.2 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
480
500
  },
481
501
  {
482
502
  "path": "marks/yomizaka-geometry.ts",
483
- "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yomizaka mark's geometry — drawn by the web mark (YomizakaLogo) and the native one (YomizakaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold filled emblem in the\n * brand gradient, a bright core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **the reading**: a filled page whose upper-right corner wears the Krizaka cut, three lines of text in\n * light, and a lens — the eye that reads, the owl's — resting on its lower half, its handle running out of the page.\n * The lens is the bright core: it breathes. A thin gap (the page shows through) separates the page from the lens: the\n * page is cut here, no mask, so web and native draw the same thing. On the web a line of light scans the page from top\n * to bottom (reading); one node, a page, rides the dashed orbit. The gradient is the brand's mark gradient\n * (`marks.yomizaka` of @krizaka/tokens): ink → indigo, mid-tones that read ≥ 3:1 on the page surfaces of both themes\n * (tested in @krizaka/tokens), so one mark serves dark and light.\n */\n/** The indigo end of the gradient: the halo. */\nconst indigo = marks.yomizaka[marks.yomizaka.length - 1]![1];\n\n/*\n * How the page is drawn (literal, so the native bundle carries no geometry code): a page x 128 → 264, y 92 → 300, its\n * upper-right corner cut 36 (228, 92) → (264, 128). The lens is centred at (240, 232); the page is cut by the lens'\n * circle grown by the gap (r 52), which crosses the right edge at y 232 ± 46.13 — the large arc runs inside the page.\n */\nexport const YOMIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n /** The page, cut at its corner (the Krizaka chamfer) and around the lens. */\n page: \"M128 92 H228 L264 128 V185.87 A52 52 0 1 0 264 278.13 V300 H128 Z\",\n /** Three lines of text and the start of a fourth, in light: they wrap around the lens. */\n lines: \"M156 140 H226 M156 170 H236 M156 200 H184 M156 230 H176\",\n line: { color: \"#f4f5ff\", opacity: 0.6, width: 10 },\n /** The lens: a ring in the brand gradient, its pale core lit from above, a glint; the handle leaves the page. */\n lens: { cx: 240, cy: 232, r: 40, width: 12, core: 28, glint: { cx: 230, cy: 222, r: 8 } },\n handle: \"M276 268 L304 296\",\n handleWidth: 20,\n gradient: { x1: 128, y1: 300, x2: 304, y2: 92, stops: marks.yomizaka },\n /** The lens' core: a pale indigo light, brighter at the top. */\n coreGradient: { stops: [[\"0%\", \"#c9ceff\"], [\"100%\", \"#f4f5ff\"]] as const },\n highlight: \"#ffffff\",\n /** The reading (web): a line of light that scans the page from top to bottom, clipped to the page. */\n scan: { d: \"M136 100 H236\", travel: 192, color: \"#f4f5ff\", opacity: 0.7, width: 3 },\n /** A page: one node on the orbit, that turns with it. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: indigo, opacity: 0.18 },\n /** The lens' breathing shadow (web), in the indigo. */\n pulse: { color: indigo, low: 0.3, high: 0.6 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
503
+ "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yomizaka mark's geometry — drawn by the web mark (YomizakaLogo) and the native one (YomizakaMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold emblem at the family\n * stroke (22) in the brand gradient, a lit core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **the watch** (2026-10-17): the owl — the bird that reads documents and knows faces — drawn as two\n * lenses under one calm brow, its pupils glancing down the line it reads. The brow is a soft double curve (never a\n * sharp V: the owl is attentive, not stern). The pupils are the lit cores: they breathe. The gradient is the brand's\n * mark gradient (`marks.yomizaka` of @krizaka/tokens): ink → indigo, ≥ 3:1 on the page surfaces of both themes.\n */\n/** The indigo end of the gradient: the halo and the pupils' breath. */\nconst indigo = marks.yomizaka[marks.yomizaka.length - 1]![1];\n\nexport const YOMIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n stroke: 22,\n /** The brow: two soft arcs that meet over the beak — calm, never a sharp V. */\n brow: \"M106 153 C130 125 176 129 200 159 C224 129 270 125 294 153\",\n /** The two lenses (rings at the family stroke). */\n lenses: [\n { cx: 146, cy: 223, r: 40 },\n { cx: 254, cy: 223, r: 40 },\n ],\n /** The pupils, glancing down-right (reading): the lit cores. */\n pupils: [\n { cx: 154, cy: 229, r: 15, inner: 5.1 },\n { cx: 262, cy: 229, r: 15, inner: 5.1 },\n ],\n highlight: \"#f4f5ff\",\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.yomizaka },\n /** One node, a page, rides the orbit. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: indigo, opacity: 0.18 },\n pulse: { color: indigo, low: 0.3, high: 0.6 },\n motion: { spin: 32, breath: 3.6 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
484
504
  },
485
505
  {
486
506
  "path": "marks/yororis-geometry.ts",
487
- "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yororis mark's geometry — drawn by the web mark (YororisLogo) and the native one (YororisMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold filled emblem in the\n * brand gradient, a bright core; below 48 px the view box crops to the emblem.\n *\n * The emblem is \"the hold\": a \"Y\" whose two arms are two filled leaves — open hands — that rise from one stem and cup\n * a warm core. The core is not where the arms join, it rests *inside* them: held, given room, listened to. A vein of\n * light runs along each leaf (depth, like the Orochia serpent's scales). One node, the moment, rides the dashed orbit:\n * it comes round, then it goes (a presence that stops). The leaves wear the brand's mark gradient (`marks.yororis` of\n * @krizaka/tokens): sand → sage, mid-tones that read ≥ 3:1 on the page surfaces of both themes (tested in\n * @krizaka/tokens), so one mark serves dark and light; the core is a warm sand light that rises from the mark's sand.\n */\nconst sand = marks.yororis[0]![1];\n/** The calm end of the gradient (sage): the halo and the core's glow. */\nconst sage = marks.yororis[marks.yororis.length - 1]![1];\n\nexport const YORORIS = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n /** The two leaves — open hands, mirrored around x = 200 — filled, rising from the stem. */\n leaves: [\n \"M200 262 C142 256 104 206 110 122 C138 130 154 152 160 180 C168 218 182 238 204 246 Z\",\n \"M200 262 C258 256 296 206 290 122 C262 130 246 152 240 180 C232 218 218 238 196 246 Z\",\n ],\n /** A vein of light along each leaf (dashed, it drifts slowly when animated). */\n veins: \"M122 142 C128 196 156 232 192 248 M278 142 C272 196 244 232 208 248\",\n vein: { color: \"#fbf6ea\", opacity: 0.45, width: 3, dash: \"2 14\" },\n /** The stem, from the junction down. */\n stem: \"M200 244 L200 312\",\n stroke: 30,\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.yororis },\n /** The core's warm light: from the mark's sand (below) to a light sand (above). */\n coreGradient: { stops: [[\"0%\", sand], [\"100%\", \"#e6c993\"]] as const },\n /** The core, held in the hollow of the leaves (not on the junction): the one who is listened to. It breathes. */\n core: { cx: 200, cy: 184, r: 32, inner: 11 },\n highlight: \"#fbf6ea\",\n /** The moment: one node on the orbit, that turns with it. */\n moment: { r: 9, at: [200, 40] as const },\n glow: { color: sage, opacity: 0.18 },\n /** The core's breathing shadow (web), in the core's warm sand. */\n pulse: { color: sand, low: 0.3, high: 0.6 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
507
+ "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Yororis mark's geometry — drawn by the web mark (YororisLogo) and the native one (YororisMark). The family's\n * grammar (packages/tokens/BRAND.md, \"Marks\"): 400×400 view box, dashed orbit and ring, a bold emblem at the family\n * stroke (22), a lit core; below 48 px the view box crops to the emblem.\n *\n * The emblem is **held** (2026-10-17): two open hands, drawn as two arcs of one circle, cup a warm core without\n * closing on it — held, given room, free to go. The arcs are sage, the core sand: the two stops of the brand's mark\n * gradient (`marks.yororis` of @krizaka/tokens), each ≥ 3:1 on the page surfaces of both themes. The slowest breath of\n * the family (4.2 s).\n */\nconst sand = marks.yororis[0]![1];\n/** The sage end: the hands, the halo. */\nconst sage = marks.yororis[marks.yororis.length - 1]![1];\n\nexport const YORORIS = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"80 80 240 240\",\n center: 200,\n stroke: 22,\n sand,\n sage,\n /** The two hands: arcs of a circle r 92 centred (200, 162), from 196° to 104° and from 76° to −16°; open at the bottom and the top. */\n hands: \"M111.6 136.6 A92 92 0 0 0 177.7 251.3 M222.3 251.3 A92 92 0 0 0 288.4 136.6\",\n /** The warm core, held inside the hands. */\n core: { cx: 200, cy: 176, r: 30, inner: 10.2 },\n highlight: \"#fbf6ea\",\n gradient: { x1: 110, y1: 300, x2: 290, y2: 100, stops: marks.yororis },\n /** The moment: one node on the orbit — it comes round, then it goes. */\n node: { r: 9, at: [200, 40] as const },\n glow: { color: sage, opacity: 0.18 },\n pulse: { color: sand, low: 0.3, high: 0.6 },\n motion: { spin: 36, breath: 4.2 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
488
508
  },
489
509
  {
490
510
  "path": "meta.ts",
491
- "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
511
+ "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
492
512
  },
493
513
  {
494
514
  "path": "nav-bar/index.ts",
495
- "content": "/** @krizaka/ui/nav-bar — `NavBar` (Root, Brand, Family, Badge, Items, Link, Menu, MenuLink, Actions, Repo, Mobile, MobileLink), `navBar`. Client. */\nexport {\n NavBar,\n navBar,\n NavBarActions,\n NavBarBadge,\n type NavBarBadgeProps,\n NavBarBrand,\n type NavBarBrandProps,\n NavBarFamily,\n type NavBarFamilyProps,\n NavBarItems,\n NavBarLink,\n type NavBarLinkProps,\n NavBarMenu,\n NavBarMenuLink,\n type NavBarMenuLinkProps,\n type NavBarMenuProps,\n NavBarMobile,\n NavBarMobileLink,\n type NavBarMobileLinkProps,\n type NavBarMobileProps,\n NavBarRepo,\n type NavBarRepoProps,\n NavBarRoot,\n type NavBarRootProps,\n} from \"./nav-bar\";\n"
515
+ "content": "/** @krizaka/ui/nav-bar — `NavBar` (Root, Brand, Family, Badge, Items, Link, Menu, MenuLink, Actions, Repo, Mobile, MobileGroup, MobileLink), `navBar`. Client. */\nexport {\n NavBar,\n navBar,\n NavBarActions,\n NavBarBadge,\n type NavBarBadgeProps,\n NavBarBrand,\n type NavBarBrandProps,\n NavBarFamily,\n type NavBarFamilyProps,\n NavBarItems,\n type NavBarItemsProps,\n NavBarLink,\n type NavBarLinkProps,\n NavBarMenu,\n NavBarMenuLink,\n type NavBarMenuLinkProps,\n type NavBarMenuProps,\n NavBarMobile,\n NavBarMobileGroup,\n type NavBarMobileGroupProps,\n NavBarMobileLink,\n type NavBarMobileLinkProps,\n type NavBarMobileProps,\n NavBarRepo,\n type NavBarRepoProps,\n NavBarRoot,\n type NavBarRootProps,\n} from \"./nav-bar\";\n"
496
516
  },
497
517
  {
498
518
  "path": "nav-bar/meta.ts",
499
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Nav bar\",\n summary:\n \"The Krizaka navigation bar, the same on every site of the family: a floating pill that tightens as the page scrolls, the family switcher on the brand, one light that glides to the item under the pointer or the focus and rests on the current page, accessible menus with rich links, and a mobile panel.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n parity: { native: { status: \"n/a\", reason: \"A site's top bar; an app on a phone navigates with its tab bar.\", use: \"tab-bar\" }, desktop: { status: \"n/a\", reason: \"A public site's page, not a window of the Mac app.\" } },\n whenToUse: [\n \"The top navigation of a site or a product: a brand, a few links, one or two menus, actions on the right.\",\n \"A section bar inside an app (`floating={false}`): videos, earnings, auctions.\",\n \"Every site of the Krizaka family (www, ui., java., dev.<product>, admin.<product>): the brand and `NavBar.Family` on the left, at most four of the site's sections in the middle, the tools on the right.\",\n ],\n whenNotToUse: [\n { when: \"To switch views inside a page without changing the address.\", use: \"tabs\" },\n { when: \"For a list of actions on one object.\", use: \"dropdown-menu\" },\n { when: \"For search and commands across the app.\", use: \"command\" },\n { when: \"To link to another site of the family from the bar's middle: the family switcher holds them.\", use: \"sites\" },\n ],\n bestPractices: [\n \"Five items at most on the bar; group the rest in a menu, each link with a name and one line (`NavBar.MenuLink`).\",\n \"Mark the current page with `active`: the light rests on it, `aria-current=\\\"page\\\"` says it.\",\n \"Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.\",\n \"One primary action at most in `NavBar.Actions` (a `Button` `size=\\\"sm\\\"` `shape=\\\"pill\\\"`).\",\n \"Wrap it in a `sticky top-3` (floating) or `sticky top-0` container to keep it on screen.\",\n \"Three zones on every Krizaka site: who I am (`NavBar.Brand` + `NavBar.Family current=\\\"<site>\\\"`, then `NavBar.Badge` with the version or `DEV · v1.2.3`), this site (at most four sections, never a link to another site), tools (search when there is something to search, `NavBar.Repo`, theme, language, one primary action).\",\n \"Place a second `NavBar.Family` as the last child of `NavBar.Mobile`: below `md` the family is the panel's last group.\",\n \"Pass the family's words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Tab\", action: \"Moves between the brand, the items and the actions; leaving an open menu closes it.\" },\n { keys: \"Enter / Space / ↓\", action: \"Opens a menu and moves into it.\" },\n { keys: \"↑ / ↓ · Home / End\", action: \"Walks the links of an open menu.\" },\n { keys: \"← / →\", action: \"Moves between the items of the bar.\" },\n { keys: \"Esc\", action: \"Closes the menu, the family or the mobile panel and returns the focus to its trigger.\" },\n { keys: \"Enter / Space / ↓ (family)\", action: \"Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover.\" },\n ],\n notes: [\n \"A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.\",\n \"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`.\",\n \"The light, the chevron and the tightening do not move under `prefers-reduced-motion`; the current page is tinted before scripts load.\",\n \"The family is its own `nav` named by `labels.family`; its chevron is a button with the same name, `aria-expanded` and `aria-controls`. Each group is a list named by its heading, the current site says `aria-current=\\\"true\\\"` and “You are here”, a site not released says its status in words.\",\n \"`NavBar.Repo` is an icon link named by `label`; `NavBar.Badge` is plain text (`DEV · v1.2.3`), hidden below `sm`.\",\n ],\n },\n related: [\"sites\", \"site-footer\", \"tabs\", \"dropdown-menu\", \"command\", \"button\"],\n web: {\n imports: [\"NavBar\"],\n examples: [\n { name: \"family-www\", title: \"www.krizaka.com\", description: \"The company's bar: the mark and the family switcher, Products · Open source · Our story, the repository and Contact. Open the chevron: every site of Krizaka, grouped, the current one marked.\" },\n { name: \"family-ui\", title: \"ui.krizaka.com\", description: \"Krizaka UI: the family, the version of the packages, Get started · Foundations · Components · Templates, the repository.\" },\n { name: \"family-java\", title: \"java.krizaka.com\", description: \"Krizaka Java: the family, the version of the building blocks, Get started · Building blocks · Guides.\" },\n { name: \"family-product\", title: \"dev.<product>\", description: \"A product's site: its mark and the family, `DEV · v1.2.3`, How it works · Demo · Docs, and “Talk to us” on krizaka.com while sign-up is closed.\" },\n { name: \"family-admin\", title: \"admin.<product>\", description: \"A console: the mark, the family and `DEV`, no sections, a full-width bar.\" },\n { name: \"site\", title: \"A site's bar\", description: \"Brand, a Products menu, links, a call to action, a mobile panel — hover the items: one light glides between them.\" },\n { name: \"menu-open\", title: \"A rich menu\", description: \"Two columns of links with icons whose node lights up, the current page tinted.\" },\n { name: \"app-bar\", title: \"Inside an app\", description: \"`floating={false}`: a full-width bar with a hairline, for an app's sections.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
519
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Nav bar\",\n summary:\n \"The Krizaka navigation bar, the same on every site of the family: a floating pill that tightens as the page scrolls, the family switcher on the brand, one light that glides to the item under the pointer or the focus and rests on the current page, accessible menus with rich links, and a mobile panel.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n parity: { native: { status: \"n/a\", reason: \"A site's top bar; an app on a phone navigates with its tab bar.\", use: \"tab-bar\" }, desktop: { status: \"n/a\", reason: \"A public site's page, not a window of the Mac app.\" } },\n whenToUse: [\n \"The top navigation of a site or a product: a brand, a few links, one or two menus, actions on the right.\",\n \"A section bar inside an app (`floating={false}`): videos, earnings, auctions.\",\n \"Every site of the Krizaka family (www, ui., java., dev.<product>, admin.<product>): the brand and `NavBar.Family` on the left, at most four of the site's sections in the middle, the tools on the right.\",\n ],\n whenNotToUse: [\n { when: \"To switch views inside a page without changing the address.\", use: \"tabs\" },\n { when: \"For a list of actions on one object.\", use: \"dropdown-menu\" },\n { when: \"For search and commands across the app.\", use: \"command\" },\n { when: \"To link to another site of the family from the bar's middle: the family switcher holds them.\", use: \"sites\" },\n ],\n bestPractices: [\n \"Five items at most on the bar; group the rest in a menu, each link with a name and one short line (`NavBar.MenuLink`, one line, ≤ 45 characters).\",\n \"A product's site (dev.<product>): the product's mark alone (no `NavBar.Family`, no badge — the version goes in the footer), then **Product ▾** (what it does: how it works, its features, the demo) · **Platforms ▾** (web, mobile, desktop, self-host — omitted for a single-platform product) · **Docs** · Pricing when it exists; never a lone entry that belongs to a group (no top-level Demo, Mobile or Mac). The phone panel repeats the groups with `NavBar.MobileGroup`, in the same order.\",\n \"Light one item at most: the group that holds the page (`active` on its `NavBar.Menu`), never two.\",\n \"Mark the current page with `active`: the light rests on it, `aria-current=\\\"page\\\"` says it.\",\n \"Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.\",\n \"One primary action at most in `NavBar.Actions` (a `Button` `size=\\\"sm\\\"` `shape=\\\"pill\\\"`).\",\n \"Wrap it in a `sticky top-3` (floating) or `sticky top-0` container to keep it on screen.\",\n \"Three zones on every Krizaka site: who I am (`NavBar.Brand` + `NavBar.Family current=\\\"<site>\\\"`, then `NavBar.Badge` with the version or `DEV · v1.2.3`), this site (at most four sections, never a link to another site), tools (search when there is something to search, `NavBar.Repo`, theme, language, one primary action).\",\n \"Place a second `NavBar.Family` as the last child of `NavBar.Mobile`: below `md` the family is the panel's last group.\",\n \"Pass the family's words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Tab\", action: \"Moves between the brand, the items and the actions; leaving an open menu closes it.\" },\n { keys: \"Enter / Space / ↓\", action: \"Opens a menu and moves into it.\" },\n { keys: \"↑ / ↓ · Home / End\", action: \"Walks the links of an open menu.\" },\n { keys: \"← / →\", action: \"Moves between the items of the bar.\" },\n { keys: \"Esc\", action: \"Closes the menu, the family or the mobile panel and returns the focus to its trigger.\" },\n { keys: \"Click outside\", action: \"Closes the open menu or the mobile panel.\" },\n { keys: \"Enter / Space / ↓ (family)\", action: \"Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover.\" },\n ],\n notes: [\n \"A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.\",\n \"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`; a press outside closes it. `NavBar.MobileGroup` is a `group` named by its heading.\",\n \"Menus open on click, Enter, Space or ↓, or when the pointer rests on the trigger (`delayDuration`, 200 ms) — passing over the bar opens nothing. While a menu is open the light stays on its trigger; moving on to a plain link closes the menu.\",\n \"The light, the chevron and the tightening do not move under `prefers-reduced-motion`; the current page is tinted before scripts load.\",\n \"The family is its own `nav` named by `labels.family`; its chevron is a button with the same name, `aria-expanded` and `aria-controls`. Each group is a list named by its heading, the current site says `aria-current=\\\"true\\\"` and “You are here”, a site not released says its status in words.\",\n \"`NavBar.Repo` is an icon link named by `label`; `NavBar.Badge` is plain text (`DEV · v1.2.3`), hidden below `sm`.\",\n ],\n },\n related: [\"sites\", \"site-footer\", \"tabs\", \"dropdown-menu\", \"command\", \"button\"],\n web: {\n imports: [\"NavBar\"],\n examples: [\n { name: \"family-www\", title: \"www.krizaka.com\", description: \"The company's bar: the mark and the family switcher, Products · Open source · Our story, the repository and Contact. Open the chevron: every site of Krizaka, grouped, the current one marked.\" },\n { name: \"family-ui\", title: \"ui.krizaka.com\", description: \"Krizaka UI: the family, the version of the packages, Get started · Foundations · Components · Templates, the repository.\" },\n { name: \"family-java\", title: \"java.krizaka.com\", description: \"Krizaka Java: the family, the version of the building blocks, Get started · Building blocks · Guides.\" },\n { name: \"family-product\", title: \"dev.<product>\", description: \"A product's site, the standard of every Krizaka product: its mark alone, Product ▾ · Platforms ▾ · Docs with compact menus (icon · title · one line), Sign in and “Talk to us”; the phone panel groups the same entries.\" },\n { name: \"family-admin\", title: \"admin.<product>\", description: \"A console: the mark, the family and `DEV`, no sections, a full-width bar.\" },\n { name: \"site\", title: \"A site's bar\", description: \"Brand, a Products menu, links, a call to action, a mobile panel — hover the items: one light glides between them.\" },\n { name: \"menu-open\", title: \"A rich menu\", description: \"Two columns of links with icons whose node lights up, the current page tinted.\" },\n { name: \"app-bar\", title: \"Inside an app\", description: \"`floating={false}`: a full-width bar with a hairline, for an app's sections.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
500
520
  },
501
521
  {
502
522
  "path": "nav-bar/nav-bar.tsx",
503
- "content": "// Client (scroll, a sliding indicator, menus). Radix NavigationMenu does the menus' semantics and keyboard (Enter, Space\n// and ↓ open; ↑ ↓ Home End walk; Escape closes and returns the focus; Tab out closes); this file draws the Krizaka bar:\n// a floating pill that tightens once the page scrolls, one light that glides to the item under the pointer or the\n// focus and rests on the current page, and a mobile panel that is a plain disclosure (no focus trap, Escape closes).\nimport { NavigationMenu as Nav, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { FamilyMark, GitHubGlyph } from \"../family-mark\";\nimport { type FamilyLabels, familyLabels, type Site, type SiteId, sitesByGroup } from \"../sites/sites\";\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const navBar = tv({\n slots: {\n root:\n \"group/nav relative z-40 mx-auto flex h-14 w-full items-center gap-2 border-border-subtle bg-surface-1/75 backdrop-blur-xl backdrop-saturate-150 \" +\n \"transition-[height,background-color,border-color,box-shadow] duration-300 motion-reduce:transition-none \" +\n \"data-compact:h-12 data-compact:border-border-default data-compact:bg-surface-1/90 data-compact:shadow-lg\",\n brand:\n \"mr-2 inline-flex shrink-0 items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg no-underline \" +\n \"rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n items: \"relative hidden md:block\",\n list:\n \"group/list relative isolate flex items-center gap-0.5 \" +\n // The light: under the item that is pointed at or focused, resting on the current page.\n \"before:absolute before:left-0 before:top-0 before:-z-10 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) \" +\n \"before:rounded-full before:bg-surface-3 before:opacity-0 before:transition-[translate,width,opacity] before:duration-300 motion-reduce:before:transition-none \" +\n \"data-[indicator=on]:before:opacity-100\",\n item:\n \"inline-flex h-9 select-none items-center gap-1 rounded-full px-3.5 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:text-fg data-active:text-fg data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n // Before the light is ready (server render), the current page carries its own tint.\n \"group-not-data-[indicator=on]/list:data-active:bg-surface-3\",\n chevron: \"h-3.5 w-3.5 transition-transform duration-300 group-data-[state=open]/trigger:rotate-180 motion-reduce:transition-none\",\n viewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n panel: \"grid w-max max-w-[min(92vw,30rem)] gap-1 p-2\",\n menuLink:\n \"group/link flex items-start gap-3 rounded-xl p-3 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-active:bg-accent-soft\",\n menuIcon:\n \"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border-default bg-surface-2 text-fg-accent \" +\n \"transition-[transform,border-color] duration-300 group-hover/link:-translate-y-0.5 group-hover/link:border-accent [&>svg]:h-4.5 [&>svg]:w-4.5 motion-reduce:transform-none\",\n menuTitle: \"block text-sm font-semibold text-fg\",\n menuText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n actions: \"ml-auto flex items-center gap-2\",\n toggle:\n \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-subtle bg-surface-2/60 text-fg md:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n mobile: \"kz-pop absolute inset-x-0 top-full mt-2 grid gap-1 rounded-2xl border border-border-default bg-surface-1 p-2 shadow-lg md:hidden\",\n mobileLink:\n \"flex items-center justify-between rounded-xl px-3 py-3 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg-accent\",\n // The family switcher: a chevron attached to the brand, its panel aligned on the bar's left edge.\n family: \"hidden md:block\",\n familyTrigger:\n \"group/trigger -ml-2 mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg data-[state=open]:bg-surface-3 data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n familyViewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n familyPanel: \"grid w-[min(92vw,42rem)] gap-x-2 gap-y-3 p-3 sm:grid-cols-2\",\n familyGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n familyLink:\n \"group/link flex items-start gap-3 rounded-xl px-3 py-2.5 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n familyMark: \"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center text-fg\",\n familyName: \"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm font-semibold text-fg\",\n familyText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n familyTag: \"rounded-full px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider\",\n mobileFamily: \"mt-1 grid gap-2 border-t border-border-subtle px-1 pb-1 pt-3\",\n mobileFamilyLink:\n \"flex min-h-11 items-center gap-3 rounded-xl px-2 py-2 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n badge:\n \"hidden h-6 shrink-0 items-center gap-1 rounded-full border border-border-subtle bg-surface-2 px-2 font-mono text-[11px] font-medium text-fg-secondary sm:inline-flex\",\n repo:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n floating: {\n true: { root: \"max-w-5xl rounded-full border pl-5 pr-2 data-compact:max-w-4xl\" },\n false: { root: \"border-b px-4 md:px-6\" },\n },\n },\n defaultVariants: { floating: true },\n});\nconst s = navBar();\n\ninterface NavBarContext {\n label: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n panelId: string;\n}\nconst Context = React.createContext<NavBarContext | null>(null);\n/** True inside `NavBar.Mobile`: the family renders as the panel's last group instead of a switcher. */\nconst MobileContext = React.createContext(false);\nconst useNavBar = () => {\n const ctx = React.useContext(Context);\n if (!ctx) throw new Error(\"NavBar parts must be inside <NavBar.Root>.\");\n return ctx;\n};\n\nexport type NavBarRootProps = React.ComponentProps<\"header\"> & {\n /** The accessible name of the navigation (“Main”) — passed translated. */\n label: string;\n /**\n * Pixels of scroll after which the bar tightens (denser, lower, lifted). `false`: never.\n * @default 24\n */\n compactAfter?: number | false;\n /**\n * A floating pill (the Krizaka bar) or a full-width bar with a hairline under it.\n * @default true\n */\n floating?: boolean;\n};\n\n/** The bar. Place it in a `sticky top-3` (floating) or `sticky top-0` wrapper — or let it scroll. */\nexport function NavBarRoot({ label, compactAfter = 24, floating = true, className, children, ...props }: NavBarRootProps) {\n const [compact, setCompact] = React.useState(false);\n const [open, setOpen] = React.useState(false);\n const panelId = React.useId();\n React.useEffect(() => {\n if (compactAfter === false) return;\n const onScroll = () => setCompact(window.scrollY > compactAfter);\n onScroll();\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => window.removeEventListener(\"scroll\", onScroll);\n }, [compactAfter]);\n const ctx = React.useMemo(() => ({ label, open, setOpen, panelId }), [label, open, panelId]);\n return (\n <Context.Provider value={ctx}>\n <header data-compact={compact ? \"\" : undefined} data-floating={floating} className={navBar({ floating }).root({ className })} {...props}>\n {children}\n </header>\n </Context.Provider>\n );\n}\n\nexport type NavBarBrandProps = React.ComponentProps<\"a\"> & {\n /** Renders your link (a router `<Link>`) with the brand's look. */\n asChild?: boolean;\n};\n\n/** The brand: a mark and a name, linking home. */\nexport function NavBarBrand({ asChild, className, ...props }: NavBarBrandProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp className={s.brand({ className })} {...props} />;\n}\n\n/** The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see `NavBar.Mobile`). */\nexport function NavBarItems({ className, children, ...props }: Omit<React.ComponentProps<typeof Nav.Root>, \"orientation\">) {\n const { label } = useNavBar();\n const { ref, handlers } = useSlidingIndicator<HTMLUListElement>({ item: \"[data-nav-item]\", active: \"[data-nav-item][data-active]\", follow: true });\n return (\n <Nav.Root aria-label={label} className={s.items({ className })} delayDuration={80} {...props}>\n <Nav.List ref={ref} className={s.list()} {...handlers}>\n {children}\n </Nav.List>\n <Nav.Viewport className={s.viewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"active\"> & {\n /** The current page: `aria-current=\"page\"`, and the light rests on it. */\n active?: boolean;\n};\n\n/** A link of the bar. `asChild` renders your router's `<Link>`. */\nexport function NavBarLink({ active, className, ...props }: NavBarLinkProps) {\n return (\n <Nav.Item>\n <Nav.Link data-nav-item=\"\" data-active={active ? \"\" : undefined} active={active} aria-current={active ? \"page\" : undefined} className={s.item({ className })} {...props} />\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuProps = Omit<React.ComponentProps<typeof Nav.Item>, \"children\"> & {\n /** The trigger's words (“Products”) — passed translated. */\n label: string;\n /** The current page is inside this menu: the light rests on its trigger. */\n active?: boolean;\n /** The menu's links (`NavBar.MenuLink`), laid out in a panel; `columns` sets the grid. */\n children: React.ReactNode;\n /**\n * Columns of the panel.\n * @default 1\n */\n columns?: 1 | 2;\n};\n\n/** A menu of the bar: its trigger, and its links in a panel that opens under the bar. */\nexport function NavBarMenu({ label, active, columns = 1, children, className, ...props }: NavBarMenuProps) {\n return (\n <Nav.Item className={className} {...props}>\n <Nav.Trigger data-nav-item=\"\" data-active={active ? \"\" : undefined} className={cn(s.item(), \"group/trigger\")}>\n {label}\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content className={cn(s.panel(), columns === 2 && \"md:grid-cols-2\")}>{children}</Nav.Content>\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"title\" | \"active\"> & {\n /** The link's name — passed translated. */\n title: string;\n /** One line under it — passed translated. */\n description?: string;\n /** An icon (a signature icon of `@krizaka/icons`: its node lights up on hover). */\n icon?: React.ReactNode;\n /** The current page. */\n active?: boolean;\n};\n\n/** A link inside a menu: an icon, a name and a line of text. */\nexport function NavBarMenuLink({ title, description, icon, active, href, className, asChild, children, ...props }: NavBarMenuLinkProps) {\n const body = (\n <>\n {icon && <span className={s.menuIcon()}>{icon}</span>}\n <span className=\"min-w-0\">\n <span className={s.menuTitle()}>{title}</span>\n {description && <span className={s.menuText()}>{description}</span>}\n </span>\n </>\n );\n return (\n <Nav.Link asChild={asChild} href={asChild ? undefined : href} active={active} aria-current={active ? \"page\" : undefined} data-active={active ? \"\" : undefined} className={s.menuLink({ className })} {...props}>\n {asChild && React.isValidElement<{ children?: React.ReactNode }>(children) ? React.cloneElement(children, undefined, body) : body}\n </Nav.Link>\n );\n}\n\n/** The right side: search, theme, language, the call to action. */\nexport function NavBarActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.actions({ className })} {...props} />;\n}\n\nexport type NavBarMobileProps = React.ComponentProps<\"nav\"> & {\n /** The toggle's name when closed (“Open the menu”) — passed translated. */\n openLabel: string;\n /** The toggle's name when open (“Close the menu”) — passed translated. */\n closeLabel: string;\n};\n\n/**\n * Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape and a click\n * on a link close it, the focus returns to the toggle.\n */\nexport function NavBarMobile({ openLabel, closeLabel, className, children, ...props }: NavBarMobileProps) {\n const { label, open, setOpen, panelId } = useNavBar();\n const toggle = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n setOpen(false);\n toggle.current?.focus();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, setOpen]);\n return (\n <>\n <button ref={toggle} type=\"button\" aria-expanded={open} aria-controls={panelId} aria-label={open ? closeLabel : openLabel} onClick={() => setOpen(!open)} className={s.toggle()}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4.5 w-4.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n {open ? <path d=\"M18 6 6 18M6 6l12 12\" /> : <path d=\"M4 7h16M4 12h16M4 17h10\" />}\n </svg>\n </button>\n {open && (\n <nav\n id={panelId}\n aria-label={label}\n className={s.mobile({ className })}\n onClick={(e) => {\n if ((e.target as Element).closest(\"a\")) setOpen(false);\n }}\n {...props}\n >\n <MobileContext.Provider value>{children}</MobileContext.Provider>\n </nav>\n )}\n </>\n );\n}\n\nexport type NavBarMobileLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the mobile panel. */\nexport function NavBarMobileLink({ active, asChild, className, ...props }: NavBarMobileLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.mobileLink({ className })} {...props} />;\n}\n\n\nconst tagTone = {\n upcoming: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40\",\n construction: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40\",\n development: \"bg-surface-3 text-fg ring-1 ring-inset ring-border-strong\",\n current: \"bg-surface-3 text-fg-secondary\",\n};\n\n/** A site's name and its tags: status when not released, “You are here” on the current one. */\nfunction FamilyName({ site, current, labels }: { site: Site; current: boolean; labels: FamilyLabels }) {\n return (\n <span className={s.familyName()}>\n {site.name}\n {site.status !== \"released\" && <span className={cn(s.familyTag(), tagTone[site.status])}>{labels.status[site.status]}</span>}\n {current && <span className={cn(s.familyTag(), tagTone.current)}>{labels.current}</span>}\n </span>\n );\n}\n\nexport type NavBarFamilyProps = {\n /** The site this bar belongs to (`@krizaka/ui/sites`): marked in the family, `aria-current=\"true\"`. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** Opens the panel at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the switcher (or, inside `NavBar.Mobile`, of the family's group). */\n className?: string;\n};\n\n/**\n * The family switcher, attached to the brand (place it right after `NavBar.Brand`): a chevron that opens every site of\n * Krizaka grouped — Krizaka · Products · Open source · GitHub — the current one marked. It opens on click, Enter, Space\n * or ↓ (never on hover); ↑ ↓ walk the sites, Escape closes and returns the focus. Hidden below `md`: place a second\n * `NavBar.Family` as the last child of `NavBar.Mobile`, where it renders as the panel's last group.\n */\nexport function NavBarFamily({ current, labels = familyLabels.en, defaultOpen, className }: NavBarFamilyProps) {\n const inMobile = React.useContext(MobileContext);\n const id = React.useId();\n const groups = sitesByGroup();\n if (inMobile) {\n return (\n <div role=\"group\" aria-labelledby={`${id}-family`} className={s.mobileFamily({ className })}>\n <p id={`${id}-family`} className={s.familyGroupTitle()}>\n {labels.family}\n </p>\n {groups.map(({ group, sites }) => (\n <div key={group}>\n {/* The company's own group is named like the family itself: its heading would repeat the panel's. */}\n {group !== \"krizaka\" && (\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n )}\n <ul aria-labelledby={group !== \"krizaka\" ? `${id}-${group}` : `${id}-family`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <a href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.mobileFamilyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={24} />\n </span>\n <FamilyName site={site} current={site.id === current} labels={labels} />\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n }\n // Click only: a pointer passing over the brand never opens the family (Radix opens its menus on hover by default).\n const noHover = (e: React.PointerEvent) => e.preventDefault();\n const columns = [groups.filter((g) => g.group === \"krizaka\" || g.group === \"products\"), groups.filter((g) => g.group === \"open-source\" || g.group === \"github\")];\n return (\n <Nav.Root aria-label={labels.family} defaultValue={defaultOpen ? \"family\" : undefined} className={s.family({ className })}>\n <Nav.List className=\"flex\">\n <Nav.Item value=\"family\">\n <Nav.Trigger aria-label={labels.family} onPointerMove={noHover} onPointerLeave={noHover} className={s.familyTrigger()}>\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content onPointerLeave={noHover} className={s.familyPanel()}>\n {columns.map((column, i) => (\n <div key={i} className=\"grid content-start gap-3\">\n {column.map(({ group, sites }) => (\n <div key={group}>\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <Nav.Link href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.familyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={28} />\n </span>\n <span className=\"min-w-0\">\n <FamilyName site={site} current={site.id === current} labels={labels} />\n <span className={s.familyText()}>{labels.descriptions[site.id]}</span>\n </span>\n </Nav.Link>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n ))}\n </Nav.Content>\n </Nav.Item>\n </Nav.List>\n <Nav.Viewport onPointerLeave={noHover} className={s.familyViewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarBadgeProps = React.ComponentProps<\"span\"> & {\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n};\n\n/** A small mono badge beside the brand: the environment and the version (`DEV · v1.2.3`). Hidden below `sm`. */\nexport function NavBarBadge({ env, version, className, children, ...props }: NavBarBadgeProps) {\n const parts = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean);\n return (\n <span className={s.badge({ className })} {...props}>\n {parts.join(\" · \")}\n {children}\n </span>\n );\n}\n\nexport type NavBarRepoProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The repository the site is built from. */\n href: string;\n /** The link's accessible name (“Source on GitHub”) — passed translated. */\n label: string;\n};\n\n/** The site's repository on GitHub, an icon link of `NavBar.Actions`. */\nexport function NavBarRepo({ label, className, ...props }: NavBarRepoProps) {\n return (\n <a aria-label={label} className={s.repo({ className })} {...props}>\n <GitHubGlyph size={18} />\n </a>\n );\n}\n\n/**\n * `NavBar.Root/Brand/Family/Badge/Items/Link/Menu/MenuLink/Actions/Repo/Mobile/MobileLink` — the Krizaka navigation bar,\n * the same on every site of the family: floating, tightening on scroll, the family switcher on the brand, one light\n * gliding between its items, accessible menus, a mobile panel.\n */\nexport const NavBar = {\n Root: NavBarRoot,\n Brand: NavBarBrand,\n Family: NavBarFamily,\n Badge: NavBarBadge,\n Items: NavBarItems,\n Link: NavBarLink,\n Menu: NavBarMenu,\n MenuLink: NavBarMenuLink,\n Actions: NavBarActions,\n Repo: NavBarRepo,\n Mobile: NavBarMobile,\n MobileLink: NavBarMobileLink,\n};\n"
523
+ "content": "// Client (scroll, a sliding indicator, menus). Radix NavigationMenu does the menus' semantics and keyboard (Enter, Space\n// and ↓ open; ↑ ↓ Home End walk; Escape closes and returns the focus; Tab out closes); this file draws the Krizaka bar:\n// a floating pill that tightens once the page scrolls, one light that glides to the item under the pointer or the\n// focus and rests on the current page, and a mobile panel that is a plain disclosure (no focus trap, Escape closes).\nimport { NavigationMenu as Nav, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { FamilyMark, GitHubGlyph } from \"../family-mark\";\nimport { type FamilyLabels, familyLabels, type Site, type SiteId, sitesByGroup } from \"../sites/sites\";\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const navBar = tv({\n slots: {\n root:\n \"group/nav relative z-40 mx-auto flex h-14 w-full items-center gap-2 border-border-subtle bg-surface-1/75 backdrop-blur-xl backdrop-saturate-150 \" +\n \"transition-[height,background-color,border-color,box-shadow] duration-300 motion-reduce:transition-none \" +\n \"data-compact:h-12 data-compact:border-border-default data-compact:bg-surface-1/90 data-compact:shadow-lg\",\n brand:\n \"mr-2 inline-flex shrink-0 items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg no-underline \" +\n \"rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n items: \"relative hidden md:block\",\n list:\n \"group/list relative isolate flex items-center gap-0.5 \" +\n // The light: under the item that is pointed at or focused, resting on the current page.\n \"before:absolute before:left-0 before:top-0 before:-z-10 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) \" +\n \"before:rounded-full before:bg-surface-3 before:opacity-0 before:transition-[translate,width,opacity] before:duration-300 motion-reduce:before:transition-none \" +\n \"data-[indicator=on]:before:opacity-100\",\n item:\n \"inline-flex h-9 select-none items-center gap-1 rounded-full px-3.5 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:text-fg data-active:text-fg data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n // Before the light is ready (server render), the current page carries its own tint.\n \"group-not-data-[indicator=on]/list:data-active:bg-surface-3\",\n chevron: \"h-3.5 w-3.5 transition-transform duration-300 group-data-[state=open]/trigger:rotate-180 motion-reduce:transition-none\",\n viewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n // Compact: icon · title · one line. A sensible width (20rem, 36rem in two columns) — never a sheet over the page.\n panel: \"grid w-[min(92vw,20rem)] gap-0.5 p-1.5\",\n panelWide: \"w-[min(92vw,36rem)] md:grid-cols-2\",\n menuLink:\n \"group/link flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-active:bg-accent-soft\",\n menuIcon:\n \"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-border-default bg-surface-2 text-fg-accent \" +\n \"transition-[transform,border-color] duration-300 group-hover/link:-translate-y-0.5 group-hover/link:border-accent [&>svg]:h-4 [&>svg]:w-4 motion-reduce:transform-none\",\n menuTitle: \"block truncate text-sm font-semibold leading-5 text-fg\",\n menuText: \"block truncate text-xs leading-4 text-fg-secondary\",\n actions: \"ml-auto flex items-center gap-2\",\n toggle:\n \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-subtle bg-surface-2/60 text-fg md:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n mobile: \"kz-pop absolute inset-x-0 top-full mt-2 grid gap-1 rounded-2xl border border-border-default bg-surface-1 p-2 shadow-lg md:hidden\",\n mobileGroup: \"grid gap-0.5 [&+&]:mt-1 [&+&]:border-t [&+&]:border-border-subtle [&+&]:pt-2\",\n mobileGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n mobileLink:\n \"flex items-center justify-between rounded-xl px-3 py-3 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg-accent\",\n // The family switcher: a chevron attached to the brand, its panel aligned on the bar's left edge.\n family: \"hidden md:block\",\n familyTrigger:\n \"group/trigger -ml-2 mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg data-[state=open]:bg-surface-3 data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n familyViewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n familyPanel: \"grid w-[min(92vw,42rem)] gap-x-2 gap-y-3 p-3 sm:grid-cols-2\",\n familyGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n familyLink:\n \"group/link flex items-start gap-3 rounded-xl px-3 py-2.5 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n familyMark: \"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center text-fg\",\n familyName: \"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm font-semibold text-fg\",\n familyText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n familyTag: \"rounded-full px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider\",\n mobileFamily: \"mt-1 grid gap-2 border-t border-border-subtle px-1 pb-1 pt-3\",\n mobileFamilyLink:\n \"flex min-h-11 items-center gap-3 rounded-xl px-2 py-2 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n badge:\n \"hidden h-6 shrink-0 items-center gap-1 rounded-full border border-border-subtle bg-surface-2 px-2 font-mono text-[11px] font-medium text-fg-secondary sm:inline-flex\",\n repo:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n floating: {\n true: { root: \"max-w-5xl rounded-full border pl-5 pr-2 data-compact:max-w-4xl\" },\n false: { root: \"border-b px-4 md:px-6\" },\n },\n },\n defaultVariants: { floating: true },\n});\nconst s = navBar();\n\ninterface NavBarContext {\n label: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n panelId: string;\n}\nconst Context = React.createContext<NavBarContext | null>(null);\n/** The open menu of `NavBar.Items` (`\"\"`: none), so a plain link can close it when the pointer moves on to it. */\nconst ItemsContext = React.createContext<{ close: () => void }>({ close: () => {} });\n/** True inside `NavBar.Mobile`: the family renders as the panel's last group instead of a switcher. */\nconst MobileContext = React.createContext(false);\nconst useNavBar = () => {\n const ctx = React.useContext(Context);\n if (!ctx) throw new Error(\"NavBar parts must be inside <NavBar.Root>.\");\n return ctx;\n};\n\nexport type NavBarRootProps = React.ComponentProps<\"header\"> & {\n /** The accessible name of the navigation (“Main”) — passed translated. */\n label: string;\n /**\n * Pixels of scroll after which the bar tightens (denser, lower, lifted). `false`: never.\n * @default 24\n */\n compactAfter?: number | false;\n /**\n * A floating pill (the Krizaka bar) or a full-width bar with a hairline under it.\n * @default true\n */\n floating?: boolean;\n};\n\n/** The bar. Place it in a `sticky top-3` (floating) or `sticky top-0` wrapper — or let it scroll. */\nexport function NavBarRoot({ label, compactAfter = 24, floating = true, className, children, ...props }: NavBarRootProps) {\n const [compact, setCompact] = React.useState(false);\n const [open, setOpen] = React.useState(false);\n const panelId = React.useId();\n React.useEffect(() => {\n if (compactAfter === false) return;\n const onScroll = () => setCompact(window.scrollY > compactAfter);\n onScroll();\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => window.removeEventListener(\"scroll\", onScroll);\n }, [compactAfter]);\n const ctx = React.useMemo(() => ({ label, open, setOpen, panelId }), [label, open, panelId]);\n return (\n <Context.Provider value={ctx}>\n <header data-compact={compact ? \"\" : undefined} data-floating={floating} className={navBar({ floating }).root({ className })} {...props}>\n {children}\n </header>\n </Context.Provider>\n );\n}\n\nexport type NavBarBrandProps = React.ComponentProps<\"a\"> & {\n /** Renders your link (a router `<Link>`) with the brand's look. */\n asChild?: boolean;\n};\n\n/** The brand: a mark and a name, linking home. */\nexport function NavBarBrand({ asChild, className, ...props }: NavBarBrandProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp className={s.brand({ className })} {...props} />;\n}\n\nexport type NavBarItemsProps = Omit<React.ComponentProps<typeof Nav.Root>, \"orientation\">;\n\n/**\n * The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n * `NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n * `delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\n * moving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.\n */\nexport function NavBarItems({ className, children, value: valueProp, defaultValue, onValueChange, delayDuration = 200, ...props }: NavBarItemsProps) {\n const { label } = useNavBar();\n const [inner, setInner] = React.useState(defaultValue ?? \"\");\n const value = valueProp ?? inner;\n const setValue = React.useCallback(\n (next: string) => {\n setInner(next);\n onValueChange?.(next);\n },\n [onValueChange],\n );\n const items = React.useMemo(() => ({ close: () => setValue(\"\") }), [setValue]);\n const { ref, handlers } = useSlidingIndicator<HTMLUListElement>({\n item: \"[data-nav-item]\",\n active: \"[data-nav-item][data-active]\",\n prefer: \"[data-nav-item][data-state=open]\",\n follow: true,\n });\n return (\n <ItemsContext.Provider value={items}>\n <Nav.Root aria-label={label} className={s.items({ className })} value={value} onValueChange={setValue} delayDuration={delayDuration} {...props}>\n <Nav.List ref={ref} className={s.list()} {...handlers}>\n {children}\n </Nav.List>\n <Nav.Viewport className={s.viewport()} />\n </Nav.Root>\n </ItemsContext.Provider>\n );\n}\n\nexport type NavBarLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"active\"> & {\n /** The current page: `aria-current=\"page\"`, and the light rests on it. */\n active?: boolean;\n};\n\n/** A link of the bar. `asChild` renders your router's `<Link>`. */\nexport function NavBarLink({ active, className, onPointerEnter, ...props }: NavBarLinkProps) {\n const { close } = React.useContext(ItemsContext);\n return (\n <Nav.Item>\n <Nav.Link\n data-nav-item=\"\"\n data-active={active ? \"\" : undefined}\n active={active}\n aria-current={active ? \"page\" : undefined}\n className={s.item({ className })}\n onPointerEnter={(e) => {\n onPointerEnter?.(e);\n close();\n }}\n {...props}\n />\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuProps = Omit<React.ComponentProps<typeof Nav.Item>, \"children\"> & {\n /** The trigger's words (“Products”) — passed translated. */\n label: string;\n /** The current page is inside this menu: the light rests on its trigger. */\n active?: boolean;\n /** The menu's links (`NavBar.MenuLink`), laid out in a panel; `columns` sets the grid. */\n children: React.ReactNode;\n /**\n * Columns of the panel.\n * @default 1\n */\n columns?: 1 | 2;\n};\n\n/** A menu of the bar: its trigger, and its links in a panel that opens under the bar. */\nexport function NavBarMenu({ label, active, columns = 1, children, className, ...props }: NavBarMenuProps) {\n return (\n <Nav.Item className={className} {...props}>\n <Nav.Trigger data-nav-item=\"\" data-active={active ? \"\" : undefined} className={cn(s.item(), \"group/trigger\")}>\n {label}\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content className={cn(s.panel(), columns === 2 && s.panelWide())}>{children}</Nav.Content>\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"title\" | \"active\"> & {\n /** The link's name — passed translated. */\n title: string;\n /** One line under it — passed translated. */\n description?: string;\n /** An icon (a signature icon of `@krizaka/icons`: its node lights up on hover). */\n icon?: React.ReactNode;\n /** The current page. */\n active?: boolean;\n};\n\n/** A link inside a menu: an icon, a name and a line of text. */\nexport function NavBarMenuLink({ title, description, icon, active, href, className, asChild, children, ...props }: NavBarMenuLinkProps) {\n const body = (\n <>\n {icon && <span className={s.menuIcon()}>{icon}</span>}\n <span className=\"min-w-0\">\n <span className={s.menuTitle()}>{title}</span>\n {description && <span className={s.menuText()}>{description}</span>}\n </span>\n </>\n );\n return (\n <Nav.Link asChild={asChild} href={asChild ? undefined : href} active={active} aria-current={active ? \"page\" : undefined} data-active={active ? \"\" : undefined} className={s.menuLink({ className })} {...props}>\n {asChild && React.isValidElement<{ children?: React.ReactNode }>(children) ? React.cloneElement(children, undefined, body) : body}\n </Nav.Link>\n );\n}\n\n/** The right side: search, theme, language, the call to action. */\nexport function NavBarActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.actions({ className })} {...props} />;\n}\n\nexport type NavBarMobileProps = React.ComponentProps<\"nav\"> & {\n /** The toggle's name when closed (“Open the menu”) — passed translated. */\n openLabel: string;\n /** The toggle's name when open (“Close the menu”) — passed translated. */\n closeLabel: string;\n};\n\n/**\n * Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n * (`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\n * to the toggle.\n */\nexport function NavBarMobile({ openLabel, closeLabel, className, children, ...props }: NavBarMobileProps) {\n const { label, open, setOpen, panelId } = useNavBar();\n const toggle = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n setOpen(false);\n toggle.current?.focus();\n };\n // A press outside the panel and its toggle closes it (the page under it stays usable).\n const onPress = (e: PointerEvent) => {\n const target = e.target as Node | null;\n if (!target || toggle.current?.contains(target) || document.getElementById(panelId)?.contains(target)) return;\n setOpen(false);\n };\n document.addEventListener(\"keydown\", onKey);\n document.addEventListener(\"pointerdown\", onPress);\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.removeEventListener(\"pointerdown\", onPress);\n };\n }, [open, setOpen, panelId]);\n return (\n <>\n <button ref={toggle} type=\"button\" aria-expanded={open} aria-controls={panelId} aria-label={open ? closeLabel : openLabel} onClick={() => setOpen(!open)} className={s.toggle()}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4.5 w-4.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n {open ? <path d=\"M18 6 6 18M6 6l12 12\" /> : <path d=\"M4 7h16M4 12h16M4 17h10\" />}\n </svg>\n </button>\n {open && (\n <nav\n id={panelId}\n aria-label={label}\n className={s.mobile({ className })}\n onClick={(e) => {\n if ((e.target as Element).closest(\"a\")) setOpen(false);\n }}\n {...props}\n >\n <MobileContext.Provider value>{children}</MobileContext.Provider>\n </nav>\n )}\n </>\n );\n}\n\nexport type NavBarMobileGroupProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The group's heading — the same words as the bar's menu (“Product”) — passed translated. */\n label: string;\n};\n\n/** A group of the mobile panel: the links of one of the bar's menus under its name, in the same order. */\nexport function NavBarMobileGroup({ label, className, children, ...props }: NavBarMobileGroupProps) {\n const id = React.useId();\n return (\n <div role=\"group\" aria-labelledby={id} className={s.mobileGroup({ className })} {...props}>\n <p id={id} className={s.mobileGroupTitle()}>\n {label}\n </p>\n {children}\n </div>\n );\n}\n\nexport type NavBarMobileLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the mobile panel. */\nexport function NavBarMobileLink({ active, asChild, className, ...props }: NavBarMobileLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.mobileLink({ className })} {...props} />;\n}\n\n\nconst tagTone = {\n upcoming: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40\",\n construction: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40\",\n development: \"bg-surface-3 text-fg ring-1 ring-inset ring-border-strong\",\n current: \"bg-surface-3 text-fg-secondary\",\n};\n\n/** A site's name and its tags: status when not released, “You are here” on the current one. */\nfunction FamilyName({ site, current, labels }: { site: Site; current: boolean; labels: FamilyLabels }) {\n return (\n <span className={s.familyName()}>\n {site.name}\n {site.status !== \"released\" && <span className={cn(s.familyTag(), tagTone[site.status])}>{labels.status[site.status]}</span>}\n {current && <span className={cn(s.familyTag(), tagTone.current)}>{labels.current}</span>}\n </span>\n );\n}\n\nexport type NavBarFamilyProps = {\n /** The site this bar belongs to (`@krizaka/ui/sites`): marked in the family, `aria-current=\"true\"`. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** Opens the panel at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the switcher (or, inside `NavBar.Mobile`, of the family's group). */\n className?: string;\n};\n\n/**\n * The family switcher, attached to the brand (place it right after `NavBar.Brand`): a chevron that opens every site of\n * Krizaka grouped — Krizaka · Products · Open source · GitHub — the current one marked. It opens on click, Enter, Space\n * or ↓ (never on hover); ↑ ↓ walk the sites, Escape closes and returns the focus. Hidden below `md`: place a second\n * `NavBar.Family` as the last child of `NavBar.Mobile`, where it renders as the panel's last group.\n */\nexport function NavBarFamily({ current, labels = familyLabels.en, defaultOpen, className }: NavBarFamilyProps) {\n const inMobile = React.useContext(MobileContext);\n const id = React.useId();\n const groups = sitesByGroup();\n if (inMobile) {\n return (\n <div role=\"group\" aria-labelledby={`${id}-family`} className={s.mobileFamily({ className })}>\n <p id={`${id}-family`} className={s.familyGroupTitle()}>\n {labels.family}\n </p>\n {groups.map(({ group, sites }) => (\n <div key={group}>\n {/* The company's own group is named like the family itself: its heading would repeat the panel's. */}\n {group !== \"krizaka\" && (\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n )}\n <ul aria-labelledby={group !== \"krizaka\" ? `${id}-${group}` : `${id}-family`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <a href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.mobileFamilyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={24} />\n </span>\n <FamilyName site={site} current={site.id === current} labels={labels} />\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n }\n // Click only: a pointer passing over the brand never opens the family (Radix opens its menus on hover by default).\n const noHover = (e: React.PointerEvent) => e.preventDefault();\n const columns = [groups.filter((g) => g.group === \"krizaka\" || g.group === \"products\"), groups.filter((g) => g.group === \"open-source\" || g.group === \"github\")];\n return (\n <Nav.Root aria-label={labels.family} defaultValue={defaultOpen ? \"family\" : undefined} className={s.family({ className })}>\n <Nav.List className=\"flex\">\n <Nav.Item value=\"family\">\n <Nav.Trigger aria-label={labels.family} onPointerMove={noHover} onPointerLeave={noHover} className={s.familyTrigger()}>\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content onPointerLeave={noHover} className={s.familyPanel()}>\n {columns.map((column, i) => (\n <div key={i} className=\"grid content-start gap-3\">\n {column.map(({ group, sites }) => (\n <div key={group}>\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <Nav.Link href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.familyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={28} />\n </span>\n <span className=\"min-w-0\">\n <FamilyName site={site} current={site.id === current} labels={labels} />\n <span className={s.familyText()}>{labels.descriptions[site.id]}</span>\n </span>\n </Nav.Link>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n ))}\n </Nav.Content>\n </Nav.Item>\n </Nav.List>\n <Nav.Viewport onPointerLeave={noHover} className={s.familyViewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarBadgeProps = React.ComponentProps<\"span\"> & {\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n};\n\n/** A small mono badge beside the brand: the environment and the version (`DEV · v1.2.3`). Hidden below `sm`. */\nexport function NavBarBadge({ env, version, className, children, ...props }: NavBarBadgeProps) {\n const parts = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean);\n return (\n <span className={s.badge({ className })} {...props}>\n {parts.join(\" · \")}\n {children}\n </span>\n );\n}\n\nexport type NavBarRepoProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The repository the site is built from. */\n href: string;\n /** The link's accessible name (“Source on GitHub”) — passed translated. */\n label: string;\n};\n\n/** The site's repository on GitHub, an icon link of `NavBar.Actions`. */\nexport function NavBarRepo({ label, className, ...props }: NavBarRepoProps) {\n return (\n <a aria-label={label} className={s.repo({ className })} {...props}>\n <GitHubGlyph size={18} />\n </a>\n );\n}\n\n/**\n * `NavBar.Root/Brand/Family/Badge/Items/Link/Menu/MenuLink/Actions/Repo/Mobile/MobileGroup/MobileLink` — the Krizaka navigation bar,\n * the same on every site of the family: floating, tightening on scroll, the family switcher on the brand, one light\n * gliding between its items, accessible menus, a mobile panel.\n */\nexport const NavBar = {\n Root: NavBarRoot,\n Brand: NavBarBrand,\n Family: NavBarFamily,\n Badge: NavBarBadge,\n Items: NavBarItems,\n Link: NavBarLink,\n Menu: NavBarMenu,\n MenuLink: NavBarMenuLink,\n Actions: NavBarActions,\n Repo: NavBarRepo,\n Mobile: NavBarMobile,\n MobileGroup: NavBarMobileGroup,\n MobileLink: NavBarMobileLink,\n};\n"
504
524
  },
505
525
  {
506
526
  "path": "sliding-indicator.ts",
507
- "content": "// The shared sliding indicator (NavBar, Tabs): one element that glides from item to item instead of each item lighting\n// up on its own. It writes the target's box as CSS variables on the list (`--kz-ind-x/y/w/h`) and `data-indicator=\"on\"`\n// — no re-render, no animation frame: the glide is a CSS transition (none under reduced motion). Before hydration the\n// items keep their own active style, so nothing flashes.\nimport * as React from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nexport function useSlidingIndicator<T extends HTMLElement>({ item, active, follow = false }: { item: string; active: string; follow?: boolean }) {\n const ref = React.useRef<T>(null);\n const place = React.useCallback((target: Element | null) => {\n const list = ref.current;\n if (!list) return;\n if (!(target instanceof HTMLElement) || !list.contains(target)) {\n list.removeAttribute(\"data-indicator\");\n return;\n }\n const a = target.getBoundingClientRect();\n const b = list.getBoundingClientRect();\n list.style.setProperty(\"--kz-ind-x\", `${a.left - b.left + list.scrollLeft}px`);\n list.style.setProperty(\"--kz-ind-y\", `${a.top - b.top + list.scrollTop}px`);\n list.style.setProperty(\"--kz-ind-w\", `${a.width}px`);\n list.style.setProperty(\"--kz-ind-h\", `${a.height}px`);\n list.setAttribute(\"data-indicator\", \"on\");\n }, []);\n const rest = React.useCallback(() => place(ref.current?.querySelector(active) ?? null), [place, active]);\n\n useIsoLayoutEffect(() => {\n const list = ref.current;\n if (!list) return;\n rest();\n const resize = new ResizeObserver(rest);\n resize.observe(list);\n const changes = new MutationObserver(rest);\n changes.observe(list, { subtree: true, attributes: true, attributeFilter: [\"data-state\", \"aria-current\", \"data-active\"] });\n return () => {\n resize.disconnect();\n changes.disconnect();\n };\n }, [rest]);\n\n const handlers = follow\n ? {\n onPointerOver: (e: React.PointerEvent) => place((e.target as Element).closest(item)),\n onPointerLeave: rest,\n onFocus: (e: React.FocusEvent) => place((e.target as Element).closest(item)),\n onBlur: rest,\n }\n : {};\n return { ref, handlers };\n}\n"
527
+ "content": "// The shared sliding indicator (NavBar, Tabs): one element that glides from item to item instead of each item lighting\n// up on its own. It writes the target's box as CSS variables on the list (`--kz-ind-x/y/w/h`) and `data-indicator=\"on\"`\n// — no re-render, no animation frame: the glide is a CSS transition (none under reduced motion). Before hydration the\n// items keep their own active style, so nothing flashes.\nimport * as React from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\n/**\n * `item`: the selector of an item; `active`: of the current one, where the light rests; `prefer`: an item that, when\n * present, holds the light before the current one (the NavBar's open menu); `follow`: the light follows the pointer\n * and the focus — over anything that is not an item (an open menu's panel) it goes back to rest.\n */\nexport function useSlidingIndicator<T extends HTMLElement>({ item, active, prefer, follow = false }: { item: string; active: string; prefer?: string; follow?: boolean }) {\n const ref = React.useRef<T>(null);\n const place = React.useCallback((target: Element | null) => {\n const list = ref.current;\n if (!list) return;\n if (!(target instanceof HTMLElement) || !list.contains(target)) {\n list.removeAttribute(\"data-indicator\");\n return;\n }\n const a = target.getBoundingClientRect();\n const b = list.getBoundingClientRect();\n list.style.setProperty(\"--kz-ind-x\", `${a.left - b.left + list.scrollLeft}px`);\n list.style.setProperty(\"--kz-ind-y\", `${a.top - b.top + list.scrollTop}px`);\n list.style.setProperty(\"--kz-ind-w\", `${a.width}px`);\n list.style.setProperty(\"--kz-ind-h\", `${a.height}px`);\n list.setAttribute(\"data-indicator\", \"on\");\n }, []);\n const rest = React.useCallback(\n () => {\n const list = ref.current;\n // An item re-rendered under a resting pointer (a menu closing) keeps the light the pointer gave it.\n const hovered = follow ? list?.querySelector(`${item}:hover`) : null;\n place((prefer ? list?.querySelector(prefer) : null) ?? hovered ?? list?.querySelector(active) ?? null);\n },\n [place, active, prefer, item, follow],\n );\n const follow_ = React.useCallback((target: EventTarget) => {\n const hit = target instanceof Element ? target.closest(item) : null;\n if (hit && ref.current?.contains(hit)) place(hit);\n else rest();\n }, [item, place, rest]);\n\n useIsoLayoutEffect(() => {\n const list = ref.current;\n if (!list) return;\n rest();\n const resize = new ResizeObserver(rest);\n resize.observe(list);\n const changes = new MutationObserver(rest);\n changes.observe(list, { subtree: true, attributes: true, attributeFilter: [\"data-state\", \"aria-current\", \"data-active\"] });\n return () => {\n resize.disconnect();\n changes.disconnect();\n };\n }, [rest]);\n\n const handlers = follow\n ? {\n onPointerOver: (e: React.PointerEvent) => follow_(e.target),\n onPointerLeave: rest,\n onFocus: (e: React.FocusEvent) => follow_(e.target),\n onBlur: rest,\n }\n : {};\n return { ref, handlers };\n}\n"
508
528
  }
509
529
  ],
510
530
  "dependencies": [
511
- "@krizaka/tokens@^3.20.0",
531
+ "@krizaka/tokens@^3.22.0",
512
532
  "radix-ui@^1.7.0",
513
533
  "tailwind-variants@^3.3.1"
514
534
  ],
@@ -617,7 +637,7 @@
617
637
  },
618
638
  {
619
639
  "component": "NavBarItems",
620
- "description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see `NavBar.Mobile`).",
640
+ "description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n`NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n`delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\nmoving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.",
621
641
  "props": []
622
642
  },
623
643
  {
@@ -721,7 +741,7 @@
721
741
  },
722
742
  {
723
743
  "component": "NavBarMobile",
724
- "description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape and a click\non a link close it, the focus returns to the toggle.",
744
+ "description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n(`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\nto the toggle.",
725
745
  "props": [
726
746
  {
727
747
  "name": "closeLabel",
@@ -739,6 +759,19 @@
739
759
  }
740
760
  ]
741
761
  },
762
+ {
763
+ "component": "NavBarMobileGroup",
764
+ "description": "A group of the mobile panel: the links of one of the bar's menus under its name, in the same order.",
765
+ "props": [
766
+ {
767
+ "name": "label",
768
+ "type": "string",
769
+ "default": null,
770
+ "description": "The group's heading — the same words as the bar's menu (“Product”) — passed translated.",
771
+ "required": true
772
+ }
773
+ ]
774
+ },
742
775
  {
743
776
  "component": "NavBarMobileLink",
744
777
  "description": "A link of the mobile panel.",