@krizaka/ui 3.13.0 → 3.15.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 (71) hide show
  1. package/README.md +5 -3
  2. package/dist/avatar/defaults.d.ts +43 -0
  3. package/dist/avatar/defaults.js +76 -0
  4. package/dist/avatar/defaults.js.map +1 -0
  5. package/dist/avatar.d.ts +1 -1
  6. package/dist/avatar.js +4 -2
  7. package/dist/avatar.js.map +1 -1
  8. package/dist/chat.js +1 -1
  9. package/dist/{chunk-UB2BI3AW.js → chunk-BCVLLY3R.js} +47 -3
  10. package/dist/chunk-BCVLLY3R.js.map +1 -0
  11. package/dist/{chunk-GR5JNFGQ.js → chunk-LIPK4UCS.js} +300 -164
  12. package/dist/chunk-LIPK4UCS.js.map +1 -0
  13. package/dist/{chunk-COJ3IDE2.js → chunk-SKMJRM5K.js} +11 -4
  14. package/dist/chunk-SKMJRM5K.js.map +1 -0
  15. package/dist/{chunk-2JFARUKC.js → chunk-YIBKJ36L.js} +1 -1
  16. package/dist/chunk-YIBKJ36L.js.map +1 -0
  17. package/dist/{geometry-B2E8oFhg.d.ts → geometry-DX-W-Yyf.d.ts} +5 -2
  18. package/dist/index.d.ts +18 -2
  19. package/dist/index.js +5 -1
  20. package/dist/map.js +60 -0
  21. package/dist/map.js.map +1 -1
  22. package/dist/mascot-quote.d.ts +5 -2
  23. package/dist/mascot-quote.js +45 -1
  24. package/dist/mascot-quote.js.map +1 -1
  25. package/dist/mascot-state.d.ts +2 -2
  26. package/dist/mascot-state.js +1 -1
  27. package/dist/mascot-state.js.map +1 -1
  28. package/dist/mascots.d.ts +4 -4
  29. package/dist/mascots.js +1 -1
  30. package/dist/match-dialog.js +1 -1
  31. package/dist/native/map.js +60 -0
  32. package/dist/native/map.js.map +1 -1
  33. package/dist/native.d.ts +35 -18
  34. package/dist/native.js +1114 -912
  35. package/dist/native.js.map +1 -1
  36. package/dist/nav-bar.d.ts +1 -1
  37. package/dist/nav-bar.js +5 -4
  38. package/dist/nav-bar.js.map +1 -1
  39. package/dist/site-footer.d.ts +9 -2
  40. package/dist/site-footer.js +14 -4
  41. package/dist/site-footer.js.map +1 -1
  42. package/dist/{sites-ClGou6ou.d.ts → sites-CHp_vELn.d.ts} +4 -3
  43. package/dist/sites.d.ts +5 -4
  44. package/dist/sites.js +9 -2
  45. package/dist/sites.js.map +1 -1
  46. package/dist/vertical-video.js +1 -1
  47. package/package.json +14 -8
  48. package/registry/avatar.json +6 -2
  49. package/registry/examples/mascots/beaver.d.ts +4 -0
  50. package/registry/examples/mascots/beaver.js +19 -0
  51. package/registry/examples/mascots/beaver.tsx +18 -0
  52. package/registry/examples/mascots/flock.tsx +1 -1
  53. package/registry/examples/mascots/magpie.d.ts +4 -0
  54. package/registry/examples/mascots/magpie.js +19 -0
  55. package/registry/examples/mascots/magpie.tsx +18 -0
  56. package/registry/examples/site-footer/product.js +26 -12
  57. package/registry/examples/site-footer/product.tsx +15 -2
  58. package/registry/index.json +9 -9
  59. package/registry/map.json +1 -1
  60. package/registry/mascot-quote.json +5 -5
  61. package/registry/mascot-state.json +33 -33
  62. package/registry/mascots.json +36 -20
  63. package/registry/nav-bar.json +19 -3
  64. package/registry/permission-prompt.json +1 -1
  65. package/registry/product-page.json +2 -2
  66. package/registry/site-footer.json +42 -11
  67. package/registry/sites.json +3 -3
  68. package/dist/chunk-2JFARUKC.js.map +0 -1
  69. package/dist/chunk-COJ3IDE2.js.map +0 -1
  70. package/dist/chunk-GR5JNFGQ.js.map +0 -1
  71. package/dist/chunk-UB2BI3AW.js.map +0 -1
@@ -400,6 +400,14 @@
400
400
  "path": "family-mark.tsx",
401
401
  "content": "// The mark of a site of the family (`@krizaka/ui/sites`): its brand's `ProductLogo`, still (a list of marks never\n// moves), or GitHub's, in the text colour. Shared by `NavBar.Family` and `SiteFooter` — not an entry of its own.\nimport type * as React from \"react\";\n\nimport { ProductLogo } from \"./marks/ProductLogo\";\nimport type { SiteMark } from \"./sites/sites\";\n\n/** GitHub's mark, in the text colour (both themes). */\nexport function GitHubGlyph({ size = 16, className }: { size?: number; className?: string }) {\n return (\n <svg viewBox=\"0 0 16 16\" width={size} height={size} fill=\"currentColor\" aria-hidden className={className}>\n <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" />\n </svg>\n );\n}\n\n/** A site's mark, decorative (its name is always written beside it). */\nexport function FamilyMark({ mark, size }: { mark: SiteMark; size: number }): React.ReactElement {\n if (mark === \"github\") return <GitHubGlyph size={Math.round(size * 0.8)} />;\n return <ProductLogo id={mark} size={size} animated={false} />;\n}\n"
402
402
  },
403
+ {
404
+ "path": "marks/AkinazaLogo.tsx",
405
+ "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"
406
+ },
407
+ {
408
+ "path": "marks/KachizakaLogo.tsx",
409
+ "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"
410
+ },
403
411
  {
404
412
  "path": "marks/KrizakaLogo.tsx",
405
413
  "content": "import { KRIZAKA as G } from \"./krizaka-geometry\";\nimport { a11y, type MarkProps, neutral, useIds } from \"./shared\";\n\nconst CSS = `\n.kzm-spin { transform-origin: 200px 200px; animation: kzm-spin 30s linear infinite; }\n.kzm-pulse { transform-box: fill-box; transform-origin: center; animation: kzm-pulse 3.2s ease-in-out infinite; }\n@keyframes kzm-spin { to { transform: rotate(360deg); } }\n@keyframes kzm-pulse {\n 0%, 100% { filter: drop-shadow(0 0 6px hsla(217, 92%, 53%, .25)); opacity: .9; }\n 50% { filter: drop-shadow(0 0 14px hsla(217, 92%, 53%, .55)); opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) { .kzm-spin, .kzm-pulse { animation: none; } }\n`;\n\n/**\n * The Krizaka mark: an ink hexagonal shield, open at one vertex where a blue node breaks out, around a blue core —\n * the parent of the family (Orochia's coiled serpent, Orazaka's shards). The orbit turns, the node and the core\n * breathe. The shield reads the host's text roles, so it is ink in both themes. Below 48 px it crops to the emblem.\n */\nexport function KrizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"kzm\", [\"ink\", \"blue\", \"glow\"] as const);\n const m = (cls: string) => (animated ? 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.ink} gradientUnits=\"userSpaceOnUse\" x1={G.ink.x1} y1={G.ink.y1} x2={G.ink.x2} y2={G.ink.y2}>\n <stop offset=\"0%\" stopColor={neutral(\"--kz-text-secondary\", 60)} />\n <stop offset=\"100%\" stopColor={neutral(\"--kz-text-primary\", 92)} />\n </linearGradient>\n <linearGradient id={id.blue} gradientUnits=\"userSpaceOnUse\" x1={G.blue.x1} y1={G.blue.y1} x2={G.blue.x2} y2={G.blue.y2}>\n {G.blue.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=\"200\" cy=\"200\" r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle className={m(\"kzm-spin\")} cx=\"200\" cy=\"200\" r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx=\"200\" cy=\"200\" r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g transform={`translate(${G.offset} ${G.offset})`}>\n <path d={G.shield} stroke={`url(#${id.ink})`} strokeWidth={G.stroke} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <g className={m(\"kzm-pulse\")}>\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.r} fill={`url(#${id.blue})`} />\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.inner} fill={G.highlight} />\n </g>\n <g className={m(\"kzm-pulse\")}>\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.r} fill={`url(#${id.blue})`} />\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.inner} fill={G.highlight} />\n </g>\n </g>\n </svg>\n );\n}\n"
@@ -414,7 +422,7 @@
414
422
  },
415
423
  {
416
424
  "path": "marks/ProductLogo.tsx",
417
- "content": "import { KrizakaLogo } from \"./KrizakaLogo\";\nimport { OrazakaLogo } from \"./OrazakaLogo\";\nimport { OrochiaLogo } from \"./OrochiaLogo\";\nimport type { MarkProps } from \"./shared\";\nimport { YasakiaLogo } from \"./YasakiaLogo\";\nimport { YomizakaLogo } from \"./YomizakaLogo\";\nimport { YororisLogo } from \"./YororisLogo\";\n\nexport type BrandId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\";\n\n/** The mark of a Krizaka brand by id — for navigation, cards and lists driven by data. */\nexport function ProductLogo({ id, ...props }: MarkProps & { /** The brand: `krizaka`, `orazaka`, `orochia`, `yasakia`, `yororis` or `yomizaka` (`yazakia`, deprecated: the old name of `yasakia`). */ id: BrandId | \"yazakia\" }) {\n if (id === \"krizaka\") return <KrizakaLogo {...props} />;\n if (id === \"orazaka\") return <OrazakaLogo {...props} />;\n if (id === \"yasakia\" || id === \"yazakia\") return <YasakiaLogo {...props} />;\n if (id === \"yororis\") return <YororisLogo {...props} />;\n if (id === \"yomizaka\") return <YomizakaLogo {...props} />;\n return <OrochiaLogo {...props} />;\n}\n"
425
+ "content": "import { AkinazaLogo } from \"./AkinazaLogo\";\nimport { KachizakaLogo } from \"./KachizakaLogo\";\nimport { KrizakaLogo } from \"./KrizakaLogo\";\nimport { OrazakaLogo } from \"./OrazakaLogo\";\nimport { OrochiaLogo } from \"./OrochiaLogo\";\nimport type { MarkProps } from \"./shared\";\nimport { YasakiaLogo } from \"./YasakiaLogo\";\nimport { YomizakaLogo } from \"./YomizakaLogo\";\nimport { YororisLogo } from \"./YororisLogo\";\n\nexport type BrandId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\";\n\n/** The mark of a Krizaka brand by id — for navigation, cards and lists driven by data. */\nexport function ProductLogo({ id, ...props }: MarkProps & { /** The brand: `krizaka`, `orazaka`, `orochia`, `yasakia`, `yororis`, `yomizaka`, `kachizaka` or `akinaza` (`yazakia`, deprecated: the old name of `yasakia`). */ id: BrandId | \"yazakia\" }) {\n if (id === \"krizaka\") return <KrizakaLogo {...props} />;\n if (id === \"orazaka\") return <OrazakaLogo {...props} />;\n if (id === \"yasakia\" || id === \"yazakia\") return <YasakiaLogo {...props} />;\n if (id === \"yororis\") return <YororisLogo {...props} />;\n if (id === \"yomizaka\") return <YomizakaLogo {...props} />;\n if (id === \"kachizaka\") return <KachizakaLogo {...props} />;\n if (id === \"akinaza\") return <AkinazaLogo {...props} />;\n return <OrochiaLogo {...props} />;\n}\n"
418
426
  },
419
427
  {
420
428
  "path": "marks/YasakiaLogo.tsx",
@@ -428,6 +436,14 @@
428
436
  "path": "marks/YororisLogo.tsx",
429
437
  "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"
430
438
  },
439
+ {
440
+ "path": "marks/akinaza-geometry.ts",
441
+ "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"
442
+ },
443
+ {
444
+ "path": "marks/kachizaka-geometry.ts",
445
+ "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"
446
+ },
431
447
  {
432
448
  "path": "marks/krizaka-geometry.ts",
433
449
  "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Krizaka mark's geometry — the one description drawn by the web mark (KrizakaLogo) and the native one\n * (KrizakaMark). Built on the Orochia mark's grammar (docs: packages/tokens/BRAND.md, \"Marks\"): a 360×360 artwork at\n * (20, 20) in a 400×400 view box, one bold stroke (22) drawn as an open figure, an end that breaks out as a node, a\n * core, a dashed orbit and a ring; below 48 px the view box crops to the emblem.\n *\n * Krizaka is the parent: the hexagonal shield is **ink** (the host's text roles), the node and the core carry the\n * Krizaka blue (`marks.krizaka` of @krizaka/tokens: the brand's accent hsl(217 92% 53%) → its text tint hsl(217 92% 68%)).\n */\nexport const KRIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"86 82 228 228\",\n offset: 20,\n /** The shield: a pointy-top hexagon (r 92 around 180,180), open at its upper-right edge. */\n shield: \"M259.7,134 L259.7,226 L180,272 L100.3,226 L100.3,134 L180,88 L212,106.5\",\n stroke: 22,\n ink: { x1: 70, y1: 290, x2: 290, y2: 70 },\n blue: { x1: 180, y1: 180, x2: 300, y2: 60, stops: marks.krizaka },\n /** The vertex where the shield opens: a node (the serpent's head in Orochia's grammar). */\n node: { cx: 259.7, cy: 134, r: 19, inner: 6 },\n core: { cx: 180, cy: 180, r: 24, inner: 8 },\n highlight: \"#dbeafe\",\n glow: { color: \"#196df5\", opacity: 0.16 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
@@ -470,7 +486,7 @@
470
486
  },
471
487
  {
472
488
  "path": "nav-bar/nav-bar.tsx",
473
- "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 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 <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 <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"
489
+ "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"
474
490
  },
475
491
  {
476
492
  "path": "sliding-indicator.ts",
@@ -478,7 +494,7 @@
478
494
  }
479
495
  ],
480
496
  "dependencies": [
481
- "@krizaka/tokens@^3.13.0",
497
+ "@krizaka/tokens@^3.15.0",
482
498
  "radix-ui@^1.7.0",
483
499
  "tailwind-variants@^3.3.1"
484
500
  ],
@@ -102,7 +102,7 @@
102
102
  },
103
103
  {
104
104
  "name": "mascot",
105
- "type": "\"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\"",
105
+ "type": "\"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\" | \"magpie\" | \"beaver\"",
106
106
  "default": "falcon",
107
107
  "description": "The bird above (decorative): the falcon watches (camera), the pigeon delivers (notifications). Default falcon.",
108
108
  "required": false
@@ -134,7 +134,7 @@
134
134
  },
135
135
  {
136
136
  "name": "promise",
137
- "type": "{ bird: \"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\"; label: ReactNode; text: ReactNode; }",
137
+ "type": "{ bird: \"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\" | \"magpie\" | \"beaver\"; label: ReactNode; text: ReactNode; }",
138
138
  "default": null,
139
139
  "description": "The promise a bird keeps, under the decisions.",
140
140
  "required": false
@@ -297,7 +297,7 @@
297
297
  },
298
298
  {
299
299
  "name": "promise",
300
- "type": "{ bird: \"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\"; label: ReactNode; text: ReactNode; }",
300
+ "type": "{ bird: \"falcon\" | \"owl\" | \"duck\" | \"flamingo\" | \"pigeon\" | \"lovebird\" | \"crane\" | \"magpie\" | \"beaver\"; label: ReactNode; text: ReactNode; }",
301
301
  "default": null,
302
302
  "description": "The promise a bird keeps, under the decisions.",
303
303
  "required": false
@@ -2,14 +2,15 @@
2
2
  "name": "site-footer",
3
3
  "type": "primitive",
4
4
  "title": "Site footer",
5
- "summary": "The footer shared by every site of the Krizaka family: the site's mark and line, its own columns, the family in columns (from `@krizaka/ui/sites`), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.",
6
- "description": "The footer shared by every site of the Krizaka family: the site's mark and line, its own columns, the family in columns (from `@krizaka/ui/sites`), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.",
5
+ "summary": "The footer of a Krizaka site: the site's mark and line, its own columns, the family in columns on the Krizaka sites (from `@krizaka/ui/sites`; `family={false}` for an independent product), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.",
6
+ "description": "The footer of a Krizaka site: the site's mark and line, its own columns, the family in columns on the Krizaka sites (from `@krizaka/ui/sites`; `family={false}` for an independent product), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.",
7
7
  "status": "beta",
8
8
  "deprecated": null,
9
9
  "category": "navigation",
10
10
  "platforms": "web",
11
11
  "whenToUse": [
12
- "The bottom of every page of a Krizaka site: www, ui., java., dev.<product>.",
12
+ "The bottom of every page of a Krizaka site: www, ui., java. (the family in columns).",
13
+ "The bottom of a deployed product (dev.<product>): `family={false}` — its own columns, the legal links, the version and one publisher line, no link to another product.",
13
14
  "With `MarketingLayout`'s `footer` slot, under a bar of `@krizaka/ui/nav-bar` with `NavBar.Family`."
14
15
  ],
15
16
  "whenNotToUse": [
@@ -24,7 +25,7 @@
24
25
  ],
25
26
  "bestPractices": [
26
27
  "Set `current` to the site's id: it chooses the mark, the line, the signature (company, product, open source) and marks the site in the family.",
27
- "Two or three own columns at most (`SiteFooter.Column` + `SiteFooter.Link`), never a link to another site of the family: the family columns hold them.",
28
+ "Two or three own columns at most (`SiteFooter.Column` + `SiteFooter.Link`), never a link to another site of the family: on the Krizaka sites the family columns hold them, on a product (`family={false}`) there is none.",
28
29
  "Pass the version the site runs (`version`, and `env=\"DEV\"` on a dev environment) and the legal links in `legal`.",
29
30
  "Pass the words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`."
30
31
  ],
@@ -70,6 +71,13 @@
70
71
  "description": "The environment, when it is not production (`DEV`).",
71
72
  "required": false
72
73
  },
74
+ {
75
+ "name": "family",
76
+ "type": "boolean",
77
+ "default": "true",
78
+ "description": "Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass\n`family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —\n“A Krizaka product”, linking to www.krizaka.com.",
79
+ "required": false
80
+ },
73
81
  {
74
82
  "name": "labels",
75
83
  "type": "FamilyLabels",
@@ -164,10 +172,10 @@
164
172
  {
165
173
  "name": "product",
166
174
  "title": "A product",
167
- "description": "dev.orazaka.com: the product's columns, the family, “A Krizaka product”, `DEV · v1.2.3`.",
175
+ "description": "dev.orazaka.com (`family={false}`): the product's columns, the legal links, “A Krizaka product” → www.krizaka.com, `DEV · v1.2.3` — no family columns.",
168
176
  "path": "examples/site-footer/product.tsx",
169
177
  "module": "@krizaka/ui/registry/examples/site-footer/product",
170
- "code": "import { SiteFooter } from \"@krizaka/ui/site-footer\";\nimport { familyLabels } from \"@krizaka/ui/sites\";\n\n// A product's footer (dev.orazaka.com): its own columns, the family, “A Krizaka product”, the version it runs.\nexport default function SiteFooterProduct() {\n return (\n <SiteFooter current=\"orazaka\" labels={familyLabels.en} env=\"DEV\" version=\"1.2.3\">\n <SiteFooter.Column title=\"Orazaka\">\n <SiteFooter.Link href=\"#how\">How it works</SiteFooter.Link>\n <SiteFooter.Link href=\"#demo\">Demo</SiteFooter.Link>\n <SiteFooter.Link href=\"#docs\">Docs</SiteFooter.Link>\n </SiteFooter.Column>\n <SiteFooter.Column title=\"Talk to us\">\n <SiteFooter.Link href=\"https://www.krizaka.com/contact?topic=orazaka\">Contact</SiteFooter.Link>\n <SiteFooter.Link href=\"https://github.com/krizaka/orazaka\">Source</SiteFooter.Link>\n </SiteFooter.Column>\n </SiteFooter>\n );\n}\n"
178
+ "code": "import { SiteFooter } from \"@krizaka/ui/site-footer\";\nimport { familyLabels } from \"@krizaka/ui/sites\";\n\n// A product's footer (dev.orazaka.com): independent once deployed — its own columns, the legal links, the version it\n// runs, and one publisher line “A Krizaka product” (→ www.krizaka.com). No family columns, no link to another product.\nexport default function SiteFooterProduct() {\n return (\n <SiteFooter\n current=\"orazaka\"\n family={false}\n labels={familyLabels.en}\n env=\"DEV\"\n version=\"1.2.3\"\n legal={\n <>\n <SiteFooter.Link href=\"#privacy\">Privacy</SiteFooter.Link>\n <SiteFooter.Link href=\"#terms\">Terms</SiteFooter.Link>\n </>\n }\n >\n <SiteFooter.Column title=\"Orazaka\">\n <SiteFooter.Link href=\"#how\">How it works</SiteFooter.Link>\n <SiteFooter.Link href=\"#demo\">Demo</SiteFooter.Link>\n <SiteFooter.Link href=\"#docs\">Docs</SiteFooter.Link>\n </SiteFooter.Column>\n <SiteFooter.Column title=\"Talk to us\">\n <SiteFooter.Link href=\"https://www.krizaka.com/contact?topic=orazaka\">Contact</SiteFooter.Link>\n <SiteFooter.Link href=\"https://github.com/krizaka/orazaka\">Source</SiteFooter.Link>\n </SiteFooter.Column>\n </SiteFooter>\n );\n}\n"
171
179
  },
172
180
  {
173
181
  "name": "company",
@@ -193,6 +201,14 @@
193
201
  "path": "family-mark.tsx",
194
202
  "content": "// The mark of a site of the family (`@krizaka/ui/sites`): its brand's `ProductLogo`, still (a list of marks never\n// moves), or GitHub's, in the text colour. Shared by `NavBar.Family` and `SiteFooter` — not an entry of its own.\nimport type * as React from \"react\";\n\nimport { ProductLogo } from \"./marks/ProductLogo\";\nimport type { SiteMark } from \"./sites/sites\";\n\n/** GitHub's mark, in the text colour (both themes). */\nexport function GitHubGlyph({ size = 16, className }: { size?: number; className?: string }) {\n return (\n <svg viewBox=\"0 0 16 16\" width={size} height={size} fill=\"currentColor\" aria-hidden className={className}>\n <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" />\n </svg>\n );\n}\n\n/** A site's mark, decorative (its name is always written beside it). */\nexport function FamilyMark({ mark, size }: { mark: SiteMark; size: number }): React.ReactElement {\n if (mark === \"github\") return <GitHubGlyph size={Math.round(size * 0.8)} />;\n return <ProductLogo id={mark} size={size} animated={false} />;\n}\n"
195
203
  },
204
+ {
205
+ "path": "marks/AkinazaLogo.tsx",
206
+ "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"
207
+ },
208
+ {
209
+ "path": "marks/KachizakaLogo.tsx",
210
+ "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"
211
+ },
196
212
  {
197
213
  "path": "marks/KrizakaLogo.tsx",
198
214
  "content": "import { KRIZAKA as G } from \"./krizaka-geometry\";\nimport { a11y, type MarkProps, neutral, useIds } from \"./shared\";\n\nconst CSS = `\n.kzm-spin { transform-origin: 200px 200px; animation: kzm-spin 30s linear infinite; }\n.kzm-pulse { transform-box: fill-box; transform-origin: center; animation: kzm-pulse 3.2s ease-in-out infinite; }\n@keyframes kzm-spin { to { transform: rotate(360deg); } }\n@keyframes kzm-pulse {\n 0%, 100% { filter: drop-shadow(0 0 6px hsla(217, 92%, 53%, .25)); opacity: .9; }\n 50% { filter: drop-shadow(0 0 14px hsla(217, 92%, 53%, .55)); opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) { .kzm-spin, .kzm-pulse { animation: none; } }\n`;\n\n/**\n * The Krizaka mark: an ink hexagonal shield, open at one vertex where a blue node breaks out, around a blue core —\n * the parent of the family (Orochia's coiled serpent, Orazaka's shards). The orbit turns, the node and the core\n * breathe. The shield reads the host's text roles, so it is ink in both themes. Below 48 px it crops to the emblem.\n */\nexport function KrizakaLogo({ size = 32, animated = true, title, className }: MarkProps) {\n const id = useIds(\"kzm\", [\"ink\", \"blue\", \"glow\"] as const);\n const m = (cls: string) => (animated ? 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.ink} gradientUnits=\"userSpaceOnUse\" x1={G.ink.x1} y1={G.ink.y1} x2={G.ink.x2} y2={G.ink.y2}>\n <stop offset=\"0%\" stopColor={neutral(\"--kz-text-secondary\", 60)} />\n <stop offset=\"100%\" stopColor={neutral(\"--kz-text-primary\", 92)} />\n </linearGradient>\n <linearGradient id={id.blue} gradientUnits=\"userSpaceOnUse\" x1={G.blue.x1} y1={G.blue.y1} x2={G.blue.x2} y2={G.blue.y2}>\n {G.blue.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=\"200\" cy=\"200\" r=\"190\" fill={`url(#${id.glow})`} /> : null /* cropped: the halo would show its square */}\n <circle className={m(\"kzm-spin\")} cx=\"200\" cy=\"200\" r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx=\"200\" cy=\"200\" r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n <g transform={`translate(${G.offset} ${G.offset})`}>\n <path d={G.shield} stroke={`url(#${id.ink})`} strokeWidth={G.stroke} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <g className={m(\"kzm-pulse\")}>\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.r} fill={`url(#${id.blue})`} />\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.inner} fill={G.highlight} />\n </g>\n <g className={m(\"kzm-pulse\")}>\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.r} fill={`url(#${id.blue})`} />\n <circle cx={G.core.cx} cy={G.core.cy} r={G.core.inner} fill={G.highlight} />\n </g>\n </g>\n </svg>\n );\n}\n"
@@ -207,7 +223,7 @@
207
223
  },
208
224
  {
209
225
  "path": "marks/ProductLogo.tsx",
210
- "content": "import { KrizakaLogo } from \"./KrizakaLogo\";\nimport { OrazakaLogo } from \"./OrazakaLogo\";\nimport { OrochiaLogo } from \"./OrochiaLogo\";\nimport type { MarkProps } from \"./shared\";\nimport { YasakiaLogo } from \"./YasakiaLogo\";\nimport { YomizakaLogo } from \"./YomizakaLogo\";\nimport { YororisLogo } from \"./YororisLogo\";\n\nexport type BrandId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\";\n\n/** The mark of a Krizaka brand by id — for navigation, cards and lists driven by data. */\nexport function ProductLogo({ id, ...props }: MarkProps & { /** The brand: `krizaka`, `orazaka`, `orochia`, `yasakia`, `yororis` or `yomizaka` (`yazakia`, deprecated: the old name of `yasakia`). */ id: BrandId | \"yazakia\" }) {\n if (id === \"krizaka\") return <KrizakaLogo {...props} />;\n if (id === \"orazaka\") return <OrazakaLogo {...props} />;\n if (id === \"yasakia\" || id === \"yazakia\") return <YasakiaLogo {...props} />;\n if (id === \"yororis\") return <YororisLogo {...props} />;\n if (id === \"yomizaka\") return <YomizakaLogo {...props} />;\n return <OrochiaLogo {...props} />;\n}\n"
226
+ "content": "import { AkinazaLogo } from \"./AkinazaLogo\";\nimport { KachizakaLogo } from \"./KachizakaLogo\";\nimport { KrizakaLogo } from \"./KrizakaLogo\";\nimport { OrazakaLogo } from \"./OrazakaLogo\";\nimport { OrochiaLogo } from \"./OrochiaLogo\";\nimport type { MarkProps } from \"./shared\";\nimport { YasakiaLogo } from \"./YasakiaLogo\";\nimport { YomizakaLogo } from \"./YomizakaLogo\";\nimport { YororisLogo } from \"./YororisLogo\";\n\nexport type BrandId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\";\n\n/** The mark of a Krizaka brand by id — for navigation, cards and lists driven by data. */\nexport function ProductLogo({ id, ...props }: MarkProps & { /** The brand: `krizaka`, `orazaka`, `orochia`, `yasakia`, `yororis`, `yomizaka`, `kachizaka` or `akinaza` (`yazakia`, deprecated: the old name of `yasakia`). */ id: BrandId | \"yazakia\" }) {\n if (id === \"krizaka\") return <KrizakaLogo {...props} />;\n if (id === \"orazaka\") return <OrazakaLogo {...props} />;\n if (id === \"yasakia\" || id === \"yazakia\") return <YasakiaLogo {...props} />;\n if (id === \"yororis\") return <YororisLogo {...props} />;\n if (id === \"yomizaka\") return <YomizakaLogo {...props} />;\n if (id === \"kachizaka\") return <KachizakaLogo {...props} />;\n if (id === \"akinaza\") return <AkinazaLogo {...props} />;\n return <OrochiaLogo {...props} />;\n}\n"
211
227
  },
212
228
  {
213
229
  "path": "marks/YasakiaLogo.tsx",
@@ -221,6 +237,14 @@
221
237
  "path": "marks/YororisLogo.tsx",
222
238
  "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"
223
239
  },
240
+ {
241
+ "path": "marks/akinaza-geometry.ts",
242
+ "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"
243
+ },
244
+ {
245
+ "path": "marks/kachizaka-geometry.ts",
246
+ "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"
247
+ },
224
248
  {
225
249
  "path": "marks/krizaka-geometry.ts",
226
250
  "content": "import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Krizaka mark's geometry — the one description drawn by the web mark (KrizakaLogo) and the native one\n * (KrizakaMark). Built on the Orochia mark's grammar (docs: packages/tokens/BRAND.md, \"Marks\"): a 360×360 artwork at\n * (20, 20) in a 400×400 view box, one bold stroke (22) drawn as an open figure, an end that breaks out as a node, a\n * core, a dashed orbit and a ring; below 48 px the view box crops to the emblem.\n *\n * Krizaka is the parent: the hexagonal shield is **ink** (the host's text roles), the node and the core carry the\n * Krizaka blue (`marks.krizaka` of @krizaka/tokens: the brand's accent hsl(217 92% 53%) → its text tint hsl(217 92% 68%)).\n */\nexport const KRIZAKA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"86 82 228 228\",\n offset: 20,\n /** The shield: a pointy-top hexagon (r 92 around 180,180), open at its upper-right edge. */\n shield: \"M259.7,134 L259.7,226 L180,272 L100.3,226 L100.3,134 L180,88 L212,106.5\",\n stroke: 22,\n ink: { x1: 70, y1: 290, x2: 290, y2: 70 },\n blue: { x1: 180, y1: 180, x2: 300, y2: 60, stops: marks.krizaka },\n /** The vertex where the shield opens: a node (the serpent's head in Orochia's grammar). */\n node: { cx: 259.7, cy: 134, r: 19, inner: 6 },\n core: { cx: 180, cy: 180, r: 24, inner: 8 },\n highlight: \"#dbeafe\",\n glow: { color: \"#196df5\", opacity: 0.16 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n"
@@ -259,15 +283,15 @@
259
283
  },
260
284
  {
261
285
  "path": "site-footer/meta.ts",
262
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Site footer\",\n summary:\n \"The footer shared by every site of the Krizaka family: the site's mark and line, its own columns, the family in columns (from `@krizaka/ui/sites`), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n whenToUse: [\n \"The bottom of every page of a Krizaka site: www, ui., java., dev.<product>.\",\n \"With `MarketingLayout`'s `footer` slot, under a bar of `@krizaka/ui/nav-bar` with `NavBar.Family`.\",\n ],\n whenNotToUse: [\n { when: \"Inside an application screen or a console: the app shell holds its own navigation.\", use: \"app-shell\" },\n { when: \"To list the family in the bar.\", use: \"nav-bar\" },\n ],\n bestPractices: [\n \"Set `current` to the site's id: it chooses the mark, the line, the signature (company, product, open source) and marks the site in the family.\",\n \"Two or three own columns at most (`SiteFooter.Column` + `SiteFooter.Link`), never a link to another site of the family: the family columns hold them.\",\n \"Pass the version the site runs (`version`, and `env=\\\"DEV\\\"` on a dev environment) and the legal links in `legal`.\",\n \"Pass the words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"A `footer` landmark; the family is a `nav` named by `labels.family`, each column a list named by its heading.\",\n \"The current site says `aria-current=\\\"true\\\"` and “You are here” to screen readers; a status is written, never only a colour.\",\n \"The marks are decorative: every site's name is written beside its mark.\",\n ],\n },\n related: [\"sites\", \"nav-bar\", \"marketing-layout\"],\n web: {\n imports: [\"SiteFooter\", \"SiteFooterColumn\", \"SiteFooterLink\"],\n examples: [\n { name: \"product\", title: \"A product\", description: \"dev.orazaka.com: the product's columns, the family, “A Krizaka product”, `DEV · v1.2.3`.\" },\n { name: \"company\", title: \"The company\", description: \"www.krizaka.com: “A Krizaka company”, the company's column, the legal links at the end of the signature line.\" },\n { name: \"open-source-fr\", title: \"Open source, in French\", description: \"ui.krizaka.com in French: « Un projet open source de Krizaka », the version of the packages.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
286
+ "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Site footer\",\n summary:\n \"The footer of a Krizaka site: the site's mark and line, its own columns, the family in columns on the Krizaka sites (from `@krizaka/ui/sites`; `family={false}` for an independent product), and the signature line — “A Krizaka company / product / open-source project” — with the version and Montréal.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n whenToUse: [\n \"The bottom of every page of a Krizaka site: www, ui., java. (the family in columns).\",\n \"The bottom of a deployed product (dev.<product>): `family={false}` — its own columns, the legal links, the version and one publisher line, no link to another product.\",\n \"With `MarketingLayout`'s `footer` slot, under a bar of `@krizaka/ui/nav-bar` with `NavBar.Family`.\",\n ],\n whenNotToUse: [\n { when: \"Inside an application screen or a console: the app shell holds its own navigation.\", use: \"app-shell\" },\n { when: \"To list the family in the bar.\", use: \"nav-bar\" },\n ],\n bestPractices: [\n \"Set `current` to the site's id: it chooses the mark, the line, the signature (company, product, open source) and marks the site in the family.\",\n \"Two or three own columns at most (`SiteFooter.Column` + `SiteFooter.Link`), never a link to another site of the family: on the Krizaka sites the family columns hold them, on a product (`family={false}`) there is none.\",\n \"Pass the version the site runs (`version`, and `env=\\\"DEV\\\"` on a dev environment) and the legal links in `legal`.\",\n \"Pass the words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"A `footer` landmark; the family is a `nav` named by `labels.family`, each column a list named by its heading.\",\n \"The current site says `aria-current=\\\"true\\\"` and “You are here” to screen readers; a status is written, never only a colour.\",\n \"The marks are decorative: every site's name is written beside its mark.\",\n ],\n },\n related: [\"sites\", \"nav-bar\", \"marketing-layout\"],\n web: {\n imports: [\"SiteFooter\", \"SiteFooterColumn\", \"SiteFooterLink\"],\n examples: [\n { name: \"product\", title: \"A product\", description: \"dev.orazaka.com (`family={false}`): the product's columns, the legal links, “A Krizaka product” → www.krizaka.com, `DEV · v1.2.3` — no family columns.\" },\n { name: \"company\", title: \"The company\", description: \"www.krizaka.com: “A Krizaka company”, the company's column, the legal links at the end of the signature line.\" },\n { name: \"open-source-fr\", title: \"Open source, in French\", description: \"ui.krizaka.com in French: « Un projet open source de Krizaka », the version of the packages.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
263
287
  },
264
288
  {
265
289
  "path": "site-footer/site-footer.tsx",
266
- "content": "// Client (the marks hold an id). The footer shared by every site of the Krizaka family: the site's own columns, then the\n// family in columns from `@krizaka/ui/sites` (the same groups as the bar's switcher), then the signature line — “A\n// Krizaka product” by the kind of the current site — the version and the place.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner: \"mx-auto grid w-full max-w-(--kz-page-max,72rem) grid-cols-[minmax(0,1fr)] gap-10 px-(--kz-page-gutter,16px) py-12\",\n top: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]\",\n site: \"grid content-start gap-3\",\n siteName: \"inline-flex items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg\",\n siteText: \"max-w-xs text-sm leading-relaxed text-fg-secondary\",\n own: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n family: \"grid grid-cols-2 gap-8 border-t border-border-subtle pt-8 md:grid-cols-4\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n list: \"grid min-w-0 gap-2\",\n link:\n \"inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-full bg-surface-3 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version: \"rounded-full border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n});\nconst s = siteFooter();\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\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 /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n};\n\n/** The footer of a Krizaka site: its own columns, the family in columns, the signature, the version, Montréal. */\nexport function SiteFooter({ current, labels = familyLabels.en, children, version, env, legal, className, ...props }: SiteFooterProps) {\n const id = React.useId();\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n <div className={s.top()}>\n <div className={s.site()}>\n <a href={site.url} className={s.siteName()}>\n <FamilyMark mark={site.mark} size={24} />\n {site.name}\n </a>\n <p className={s.siteText()}>{labels.descriptions[site.id]}</p>\n </div>\n {children && <div className={s.own()}>{children}</div>}\n </div>\n <nav aria-label={labels.family} className={s.family()}>\n {sitesByGroup().map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </nav>\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n <span>{labels.place}</span>\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.map(children, (child) => (child ? <li>{child}</li> : null))}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"
290
+ "content": "// Client (the marks hold an id). The footer shared by every site of the Krizaka family: the site's own columns, then the\n// family in columns from `@krizaka/ui/sites` (the same groups as the bar's switcher), then the signature line — “A\n// Krizaka product” by the kind of the current site — the version and the place.\nimport { Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { FamilyMark } from \"../family-mark\";\nimport { KrizakaLogo } from \"../marks/KrizakaLogo\";\nimport { type FamilyLabels, familyLabels, siteById, type SiteId, sitesByGroup } from \"../sites/sites\";\n\nexport const siteFooter = tv({\n slots: {\n root: \"w-full border-t border-border-subtle bg-surface-0 text-fg\",\n inner: \"mx-auto grid w-full max-w-(--kz-page-max,72rem) grid-cols-[minmax(0,1fr)] gap-10 px-(--kz-page-gutter,16px) py-12\",\n top: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]\",\n site: \"grid content-start gap-3\",\n siteName: \"inline-flex items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg\",\n siteText: \"max-w-xs text-sm leading-relaxed text-fg-secondary\",\n own: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n family: \"grid grid-cols-2 gap-8 border-t border-border-subtle pt-8 md:grid-cols-4\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n list: \"grid min-w-0 gap-2\",\n link:\n \"inline-flex min-h-6 min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 rounded-md text-sm [overflow-wrap:anywhere] text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg aria-[current=true]:text-fg aria-[current=true]:font-semibold\",\n mark: \"flex h-4 w-4 shrink-0 items-center justify-center text-fg\",\n tag: \"rounded-full bg-surface-3 px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-border-subtle pt-6 text-xs text-fg-secondary\",\n signature:\n \"inline-flex items-center gap-2 rounded-md font-medium text-fg no-underline hover:text-fg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n version:\n \"rounded-full border border-border-subtle bg-surface-2 px-2 py-0.5 font-mono text-[11px] text-fg-secondary\",\n legal: \"flex flex-wrap items-center gap-x-4 gap-y-2 sm:ml-auto\",\n },\n});\nconst s = siteFooter();\n\nexport type SiteFooterProps = Omit<React.ComponentProps<\"footer\">, \"children\"> & {\n /** The site this footer belongs to (`@krizaka/ui/sites`): its mark and line, its signature, marked in the family. */\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 /** The site's own columns (`SiteFooter.Column`), before the family. */\n children?: React.ReactNode;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /**\n * Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass\n * `family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —\n * “A Krizaka product”, linking to www.krizaka.com.\n * @default true\n */\n family?: boolean;\n /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */\n legal?: React.ReactNode;\n};\n\n/** The footer of a Krizaka site: its own columns, the family in columns, the signature, the version, Montréal. */\nexport function SiteFooter({\n current,\n labels = familyLabels.en,\n children,\n version,\n env,\n legal,\n family = true,\n className,\n ...props\n}: SiteFooterProps) {\n const id = React.useId();\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n <div className={s.top()}>\n <div className={s.site()}>\n <a href={site.url} className={s.siteName()}>\n <FamilyMark mark={site.mark} size={24} />\n {site.name}\n </a>\n <p className={s.siteText()}>{labels.descriptions[site.id]}</p>\n </div>\n {children && <div className={s.own()}>{children}</div>}\n </div>\n {family && (\n <nav aria-label={labels.family} className={s.family()}>\n {sitesByGroup().map(({ group, sites }) => (\n <div key={group} className={s.column()}>\n <p id={`${id}-${group}`} className={s.title()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className={s.list()}>\n {sites.map((item) => (\n <li key={item.id}>\n <a href={item.url} aria-current={item.id === current ? \"true\" : undefined} className={s.link()}>\n <span className={s.mark()}>\n <FamilyMark mark={item.mark} size={16} />\n </span>\n {item.name}\n {item.status !== \"released\" && <span className={s.tag()}>{labels.status[item.status]}</span>}\n {item.id === current && <span className=\"sr-only\">({labels.current})</span>}\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </nav>\n )}\n <div className={s.bottom()}>\n <a href={krizaka.url} className={s.signature()}>\n <KrizakaLogo size={18} animated={false} />\n {labels.signature[site.kind]}\n </a>\n {tag && (\n <span className={s.version()}>\n <span className=\"sr-only\">{labels.version} </span>\n {tag}\n </span>\n )}\n <span>{labels.place}</span>\n {legal && <div className={s.legal()}>{legal}</div>}\n </div>\n </div>\n </footer>\n );\n}\n\nexport type SiteFooterColumnProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The column's heading (“Docs”) — passed translated. */\n title: string;\n /** Its links (`SiteFooter.Link`). */\n children: React.ReactNode;\n};\n\n/** One of the site's own columns: a heading and its links. */\nexport function SiteFooterColumn({ title, children, className, ...props }: SiteFooterColumnProps) {\n const id = React.useId();\n return (\n <div className={s.column({ className })} {...props}>\n <p id={id} className={s.title()}>\n {title}\n </p>\n <ul aria-labelledby={id} className={s.list()}>\n {React.Children.map(children, (child) => (child ? <li>{child}</li> : null))}\n </ul>\n </div>\n );\n}\n\nexport type SiteFooterLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page: `aria-current=\"page\"`. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the footer. */\nexport function SiteFooterLink({ active, asChild, className, ...props }: SiteFooterLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.link({ className })} {...props} />;\n}\n\nSiteFooter.Column = SiteFooterColumn;\nSiteFooter.Link = SiteFooterLink;\n"
267
291
  }
268
292
  ],
269
293
  "dependencies": [
270
- "@krizaka/tokens@^3.13.0",
294
+ "@krizaka/tokens@^3.15.0",
271
295
  "radix-ui@^1.7.0",
272
296
  "tailwind-variants@^3.3.1"
273
297
  ],
@@ -276,7 +300,7 @@
276
300
  ],
277
301
  "demo": {
278
302
  "path": "examples/site-footer/product.tsx",
279
- "content": "import { SiteFooter } from \"@krizaka/ui/site-footer\";\nimport { familyLabels } from \"@krizaka/ui/sites\";\n\n// A product's footer (dev.orazaka.com): its own columns, the family, “A Krizaka product”, the version it runs.\nexport default function SiteFooterProduct() {\n return (\n <SiteFooter current=\"orazaka\" labels={familyLabels.en} env=\"DEV\" version=\"1.2.3\">\n <SiteFooter.Column title=\"Orazaka\">\n <SiteFooter.Link href=\"#how\">How it works</SiteFooter.Link>\n <SiteFooter.Link href=\"#demo\">Demo</SiteFooter.Link>\n <SiteFooter.Link href=\"#docs\">Docs</SiteFooter.Link>\n </SiteFooter.Column>\n <SiteFooter.Column title=\"Talk to us\">\n <SiteFooter.Link href=\"https://www.krizaka.com/contact?topic=orazaka\">Contact</SiteFooter.Link>\n <SiteFooter.Link href=\"https://github.com/krizaka/orazaka\">Source</SiteFooter.Link>\n </SiteFooter.Column>\n </SiteFooter>\n );\n}\n"
303
+ "content": "import { SiteFooter } from \"@krizaka/ui/site-footer\";\nimport { familyLabels } from \"@krizaka/ui/sites\";\n\n// A product's footer (dev.orazaka.com): independent once deployed — its own columns, the legal links, the version it\n// runs, and one publisher line “A Krizaka product” (→ www.krizaka.com). No family columns, no link to another product.\nexport default function SiteFooterProduct() {\n return (\n <SiteFooter\n current=\"orazaka\"\n family={false}\n labels={familyLabels.en}\n env=\"DEV\"\n version=\"1.2.3\"\n legal={\n <>\n <SiteFooter.Link href=\"#privacy\">Privacy</SiteFooter.Link>\n <SiteFooter.Link href=\"#terms\">Terms</SiteFooter.Link>\n </>\n }\n >\n <SiteFooter.Column title=\"Orazaka\">\n <SiteFooter.Link href=\"#how\">How it works</SiteFooter.Link>\n <SiteFooter.Link href=\"#demo\">Demo</SiteFooter.Link>\n <SiteFooter.Link href=\"#docs\">Docs</SiteFooter.Link>\n </SiteFooter.Column>\n <SiteFooter.Column title=\"Talk to us\">\n <SiteFooter.Link href=\"https://www.krizaka.com/contact?topic=orazaka\">Contact</SiteFooter.Link>\n <SiteFooter.Link href=\"https://github.com/krizaka/orazaka\">Source</SiteFooter.Link>\n </SiteFooter.Column>\n </SiteFooter>\n );\n}\n"
280
304
  },
281
305
  "props": [
282
306
  {
@@ -304,6 +328,13 @@
304
328
  "description": "The environment, when it is not production (`DEV`).",
305
329
  "required": false
306
330
  },
331
+ {
332
+ "name": "family",
333
+ "type": "boolean",
334
+ "default": "true",
335
+ "description": "Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass\n`family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —\n“A Krizaka product”, linking to www.krizaka.com.",
336
+ "required": false
337
+ },
307
338
  {
308
339
  "name": "labels",
309
340
  "type": "FamilyLabels",