@krizaka/ui 3.42.0 → 3.43.1

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 (235) hide show
  1. package/dist/RotatingWord-D9s7FXFD.d.ts +46 -0
  2. package/dist/app-shell.d.ts +9 -3
  3. package/dist/app-shell.js +6 -4
  4. package/dist/app-shell.js.map +1 -1
  5. package/dist/blocks.css +12 -0
  6. package/dist/chip.d.ts +6 -0
  7. package/dist/chip.js +7 -2
  8. package/dist/chip.js.map +1 -1
  9. package/dist/choice-grid.d.ts +5 -1
  10. package/dist/choice-grid.js +94 -30
  11. package/dist/choice-grid.js.map +1 -1
  12. package/dist/chunk-HHFWK2HH.js +163 -0
  13. package/dist/chunk-HHFWK2HH.js.map +1 -0
  14. package/dist/{chunk-AAP3U66V.js → chunk-KBQD6HE3.js} +2 -2
  15. package/dist/chunk-R3LTSMS5.js +75 -0
  16. package/dist/chunk-R3LTSMS5.js.map +1 -0
  17. package/dist/{chunk-S5LP7KKW.js → chunk-YHKTYFOQ.js} +2 -2
  18. package/dist/{chunk-WYQ277F2.js → chunk-ZQW7LF4Y.js} +2 -2
  19. package/dist/code-block.js +95 -32
  20. package/dist/code-block.js.map +1 -1
  21. package/dist/combobox.d.ts +6 -6
  22. package/dist/combobox.js +2 -2
  23. package/dist/data-table.js +3 -3
  24. package/dist/date-picker.js +1 -1
  25. package/dist/date-picker.js.map +1 -1
  26. package/dist/elephant.d.ts +57 -0
  27. package/dist/elephant.js +61 -0
  28. package/dist/elephant.js.map +1 -0
  29. package/dist/filter-bar.js +2 -2
  30. package/dist/genui.js +3 -3
  31. package/dist/index.d.ts +1 -1
  32. package/dist/map.js.map +1 -1
  33. package/dist/motion.css +44 -0
  34. package/dist/motion.d.ts +46 -39
  35. package/dist/motion.js +42 -1
  36. package/dist/motion.js.map +1 -1
  37. package/dist/native/map.js.map +1 -1
  38. package/dist/native.js.map +1 -1
  39. package/dist/nav-bar.d.ts +2 -0
  40. package/dist/nav-bar.js +3 -3
  41. package/dist/nav-bar.js.map +1 -1
  42. package/dist/popover.js +1 -1
  43. package/dist/popover.js.map +1 -1
  44. package/dist/radio-group.d.ts +5 -1
  45. package/dist/radio-group.js +5 -1
  46. package/dist/radio-group.js.map +1 -1
  47. package/dist/rail-nav.js +2 -2
  48. package/dist/rail-nav.js.map +1 -1
  49. package/dist/router.d.ts +51 -0
  50. package/dist/router.js +18 -0
  51. package/dist/router.js.map +1 -0
  52. package/dist/site-footer.d.ts +10 -5
  53. package/dist/site-footer.js +4 -3
  54. package/dist/site-footer.js.map +1 -1
  55. package/dist/tabs.d.ts +42 -3
  56. package/dist/tabs.js +52 -4
  57. package/dist/tabs.js.map +1 -1
  58. package/dist/tag-input.js +2 -2
  59. package/dist/tag-selector.js +1 -1
  60. package/package.json +32 -7
  61. package/registry/account-menu.json +1 -1
  62. package/registry/achievement.json +1 -1
  63. package/registry/action-bar.json +1 -1
  64. package/registry/activity-feed.json +1 -1
  65. package/registry/alert.json +1 -1
  66. package/registry/anatomy-plate.json +1 -1
  67. package/registry/app-header.json +1 -1
  68. package/registry/app-shell.json +11 -3
  69. package/registry/argument-card.json +1 -1
  70. package/registry/audio-player.json +1 -1
  71. package/registry/auth-layout.json +1 -1
  72. package/registry/avatar.json +1 -1
  73. package/registry/badge.json +1 -1
  74. package/registry/bird-plate.json +1 -1
  75. package/registry/button.json +1 -1
  76. package/registry/captcha.json +1 -1
  77. package/registry/card-grid.json +1 -1
  78. package/registry/card.json +1 -1
  79. package/registry/catalogue-page.json +1 -1
  80. package/registry/changelog-page.json +1 -1
  81. package/registry/chat.json +1 -1
  82. package/registry/checkbox.json +1 -1
  83. package/registry/chip-link.json +1 -1
  84. package/registry/chip-row.json +1 -1
  85. package/registry/chip.json +32 -3
  86. package/registry/choice-grid.json +32 -3
  87. package/registry/code-block.json +1 -1
  88. package/registry/combobox.json +1 -1
  89. package/registry/command.json +1 -1
  90. package/registry/comparison-table.json +1 -1
  91. package/registry/component-page.json +1 -1
  92. package/registry/confirm-button.json +1 -1
  93. package/registry/contact-page.json +1 -1
  94. package/registry/continue-prompt.json +1 -1
  95. package/registry/countdown.json +1 -1
  96. package/registry/cover-hero.json +1 -1
  97. package/registry/cta-panel.json +1 -1
  98. package/registry/dashboard-layout.json +1 -1
  99. package/registry/data-table.json +1 -1
  100. package/registry/date-picker.json +1 -1
  101. package/registry/decision-list.json +1 -1
  102. package/registry/description-list.json +1 -1
  103. package/registry/detail-layout.json +1 -1
  104. package/registry/dialog.json +1 -1
  105. package/registry/docs-article-page.json +1 -1
  106. package/registry/docs-hub-page.json +1 -1
  107. package/registry/docs-layout.json +1 -1
  108. package/registry/docs-nav.json +1 -1
  109. package/registry/document-viewer.json +1 -1
  110. package/registry/dropdown-menu.json +1 -1
  111. package/registry/elephant.json +192 -0
  112. package/registry/empty-state.json +1 -1
  113. package/registry/engine-flow.json +1 -1
  114. package/registry/error-page.json +1 -1
  115. package/registry/examples/app-shell/collapse-overflow.d.ts +4 -0
  116. package/registry/examples/app-shell/collapse-overflow.js +35 -0
  117. package/registry/examples/app-shell/collapse-overflow.tsx +28 -0
  118. package/registry/examples/elephant/closing.d.ts +4 -0
  119. package/registry/examples/elephant/closing.js +14 -0
  120. package/registry/examples/elephant/closing.tsx +11 -0
  121. package/registry/examples/elephant/poses.d.ts +4 -0
  122. package/registry/examples/elephant/poses.js +14 -0
  123. package/registry/examples/elephant/poses.tsx +11 -0
  124. package/registry/examples/router/use-route-state.d.ts +4 -0
  125. package/registry/examples/router/use-route-state.js +20 -0
  126. package/registry/examples/router/use-route-state.tsx +20 -0
  127. package/registry/examples/tabs/routes.d.ts +4 -0
  128. package/registry/examples/tabs/routes.js +16 -0
  129. package/registry/examples/tabs/routes.tsx +15 -0
  130. package/registry/field.json +1 -1
  131. package/registry/file-upload.json +1 -1
  132. package/registry/filter-bar.json +1 -1
  133. package/registry/flock.json +1 -1
  134. package/registry/footnote.json +1 -1
  135. package/registry/force-update-screen.json +1 -1
  136. package/registry/form.json +1 -1
  137. package/registry/gallery-page.json +1 -1
  138. package/registry/genui.json +1 -1
  139. package/registry/glow-card.json +1 -1
  140. package/registry/goal-gauge.json +1 -1
  141. package/registry/how-it-works-page.json +1 -1
  142. package/registry/image-cropper.json +1 -1
  143. package/registry/index.json +76 -11
  144. package/registry/iso.json +1 -1
  145. package/registry/kanban.json +1 -1
  146. package/registry/kbd.json +1 -1
  147. package/registry/landing-page.json +1 -1
  148. package/registry/landscape.json +1 -1
  149. package/registry/language-switch.json +1 -1
  150. package/registry/legal-page.json +1 -1
  151. package/registry/link-card.json +1 -1
  152. package/registry/list.json +1 -1
  153. package/registry/loader.json +1 -1
  154. package/registry/longform-page.json +1 -1
  155. package/registry/maintenance-screen.json +1 -1
  156. package/registry/map.json +2 -2
  157. package/registry/markdown.json +1 -1
  158. package/registry/marketing-layout.json +1 -1
  159. package/registry/marquee.json +1 -1
  160. package/registry/mascot-quote.json +1 -1
  161. package/registry/mascot-state.json +1 -1
  162. package/registry/mascots.json +1 -1
  163. package/registry/match-dialog.json +1 -1
  164. package/registry/media-capture.json +1 -1
  165. package/registry/media-gallery.json +1 -1
  166. package/registry/media-grid.json +1 -1
  167. package/registry/meta-row.json +1 -1
  168. package/registry/meter.json +1 -1
  169. package/registry/nav-bar.json +5 -5
  170. package/registry/number-ticker.json +1 -1
  171. package/registry/opening-hours.json +1 -1
  172. package/registry/page-header.json +1 -1
  173. package/registry/page-hero.json +1 -1
  174. package/registry/page-index.json +1 -1
  175. package/registry/page.json +1 -1
  176. package/registry/pagination.json +1 -1
  177. package/registry/photo-grid.json +1 -1
  178. package/registry/pointer-light.json +1 -1
  179. package/registry/popover.json +2 -2
  180. package/registry/pricing-page.json +1 -1
  181. package/registry/product-page.json +1 -1
  182. package/registry/profile-card.json +1 -1
  183. package/registry/progress.json +1 -1
  184. package/registry/promise-card.json +1 -1
  185. package/registry/radio-group.json +32 -3
  186. package/registry/rail-nav.json +3 -3
  187. package/registry/recipes.json +1 -1
  188. package/registry/router.json +123 -0
  189. package/registry/scroll-stage.json +1 -1
  190. package/registry/search-field.json +1 -1
  191. package/registry/section-backdrop.json +1 -1
  192. package/registry/separator.json +1 -1
  193. package/registry/service-card.json +1 -1
  194. package/registry/settings-layout.json +1 -1
  195. package/registry/showcase.json +1 -1
  196. package/registry/site-footer.json +21 -7
  197. package/registry/sites.json +1 -1
  198. package/registry/skeleton.json +1 -1
  199. package/registry/skip-link.json +1 -1
  200. package/registry/slider.json +1 -1
  201. package/registry/slot-picker.json +1 -1
  202. package/registry/slot.json +1 -1
  203. package/registry/specimen-tag.json +1 -1
  204. package/registry/spinner.json +1 -1
  205. package/registry/split-view.json +1 -1
  206. package/registry/stage-banner.json +1 -1
  207. package/registry/stat.json +1 -1
  208. package/registry/status-page.json +1 -1
  209. package/registry/step-progress.json +1 -1
  210. package/registry/step-showcase.json +1 -1
  211. package/registry/stepper.json +1 -1
  212. package/registry/sticky-action.json +1 -1
  213. package/registry/swipe-deck.json +1 -1
  214. package/registry/switch.json +1 -1
  215. package/registry/tabs.json +154 -7
  216. package/registry/tag-input.json +1 -1
  217. package/registry/tag-selector.json +1 -1
  218. package/registry/terminal-replay.json +1 -1
  219. package/registry/theme.json +1 -1
  220. package/registry/toast.json +1 -1
  221. package/registry/toc.json +1 -1
  222. package/registry/tooltip.json +1 -1
  223. package/registry/tree-view.json +1 -1
  224. package/registry/update-prompt.json +1 -1
  225. package/registry/verified-badge.json +1 -1
  226. package/registry/vertical-video.json +1 -1
  227. package/registry/video-player.json +1 -1
  228. package/registry/video-trimmer.json +1 -1
  229. package/registry/virtual.json +1 -1
  230. package/registry/waveform.json +1 -1
  231. package/dist/chunk-C3NF7V5Y.js +0 -100
  232. package/dist/chunk-C3NF7V5Y.js.map +0 -1
  233. /package/dist/{chunk-AAP3U66V.js.map → chunk-KBQD6HE3.js.map} +0 -0
  234. /package/dist/{chunk-S5LP7KKW.js.map → chunk-YHKTYFOQ.js.map} +0 -0
  235. /package/dist/{chunk-WYQ277F2.js.map → chunk-ZQW7LF4Y.js.map} +0 -0
@@ -107,12 +107,17 @@ type SiteFooterProps = Omit<React.ComponentProps<"footer">, "children"> & {
107
107
  */
108
108
  path?: string;
109
109
  /**
110
- * Shows the family in columns (the Krizaka sites: www, ui., java.). A product is independent once deployed: pass
111
- * `family={false}` and the footer holds only its own columns, the legal links, the version and one publisher line —
112
- * “A Krizaka product”, linking to www.krizaka.com.
113
- * @default true
110
+ * Shows the family in columns. Only the hub, krizaka.com, lists the family: every other site (ui., java., the blog, the
111
+ * products) holds its own columns, the legal links, the version and one quiet publisher line — “A Krizaka project” —
112
+ * that links back to www.krizaka.com, the only way back between the sites (2026-10-22).
113
+ * @default current === "krizaka"
114
114
  */
115
115
  family?: boolean;
116
+ /**
117
+ * The hub's product list: only these products appear in the Products column (krizaka.com shows its focus products,
118
+ * and all of them once the reader asks for the whole notebook). Omit it to list every product.
119
+ */
120
+ products?: readonly SiteId[];
116
121
  /** The end of the signature line: legal links, a language switch (`SiteFooter.Link`s or any inline content). */
117
122
  legal?: React.ReactNode;
118
123
  /**
@@ -125,7 +130,7 @@ type SiteFooterProps = Omit<React.ComponentProps<"footer">, "children"> & {
125
130
  variant?: "site" | "docs" | "app";
126
131
  };
127
132
  /** The footer of a Krizaka site, one band: its own columns and the family's, then one line — signature, version, Montréal, legal. */
128
- declare function SiteFooter({ current, labels, children, version, env, path, legal, family: familyProp, variant, className, ...props }: SiteFooterProps): React.JSX.Element;
133
+ declare function SiteFooter({ current, labels, children, version, env, path, legal, family: familyProp, products, variant, className, ...props }: SiteFooterProps): React.JSX.Element;
129
134
  declare namespace SiteFooter {
130
135
  var Column: typeof SiteFooterColumn;
131
136
  var Link: typeof SiteFooterLink;
@@ -72,14 +72,15 @@ function SiteFooter({
72
72
  env,
73
73
  path,
74
74
  legal,
75
- family: familyProp = true,
75
+ family: familyProp,
76
+ products,
76
77
  variant = "site",
77
78
  className,
78
79
  ...props
79
80
  }) {
80
81
  const id = React.useId();
81
82
  const s = siteFooter({ variant });
82
- const family = variant === "site" && familyProp;
83
+ const family = variant === "site" && (familyProp ?? current === "krizaka");
83
84
  const slim = variant === "app";
84
85
  const site = siteById(current);
85
86
  const krizaka = siteById("krizaka");
@@ -90,7 +91,7 @@ function SiteFooter({
90
91
  const here = (href) => path !== void 0 && isThisPage(href, site.url, path);
91
92
  const groups = sitesByGroup();
92
93
  const columns = [
93
- ...groups.filter((g) => g.group === "products"),
94
+ ...groups.filter((g) => g.group === "products").map((g) => products ? { ...g, sites: g.sites.filter((x) => products.includes(x.id) || x.id === current) } : g),
94
95
  { group: "open-source", sites: groups.filter((g) => g.group === "open-source" || g.group === "github").flatMap((g) => g.sites) }
95
96
  ];
96
97
  return /* @__PURE__ */ jsx("footer", { className: s.root({ className }), ...props, children: /* @__PURE__ */ jsxs("div", { className: s.inner(), children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/site-footer/site-footer.tsx"],"sourcesContent":["// Client (the marks hold an id). The footer shared by every site of the Krizaka family — one calm band (Cahier, ADR-004;\n// peer review krizaka-ui#434: « too much, a double footer »): the site's own columns and the family's columns side by\n// side — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — then one line, with no\n// rule above it (a bottom bar under the columns read as a second footer, 2026-10-21): the signature (“A Krizaka product”, by the kind of the current site, to www), the version, the place, the legal links.\n// No site block (the bar already names the site), no column that lists only Krizaka, no link twice.\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 { companyLinks, 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:\n \"mx-auto grid w-full max-w-[calc(var(--kz-page-max,72rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] grid-cols-[minmax(0,1fr)] gap-10 px-[var(--kz-page-gutter,clamp(16px,3.2vw,32px))] py-12\",\n band: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-flow-col lg:auto-cols-[minmax(0,1fr)]\",\n own: \"grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-8\",\n family: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold 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=page]:font-semibold 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-sm bg-surface-3 px-1.5 py-px text-[10px] font-semibold text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 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-sm 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 variants: {\n // `site`: the full footer. `docs`: no site block, the columns in one row, on the documentation's width. `app`: one\n // slim line inside a signed-in product — signature, version, legal links.\n variant: {\n site: {},\n docs: {\n inner: \"max-w-[calc(var(--kz-page-max-docs,88rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] gap-6 py-8\",\n own: \"sm:grid-cols-4\",\n },\n app: {\n root: \"bg-transparent\",\n inner: \"gap-0 py-3\",\n },\n },\n },\n defaultVariants: { variant: \"site\" },\n});\n\nconst bare = (p: string) => p.replace(/^\\/(en|fr)(?=\\/|$)/, \"\").replace(/\\/+$/, \"\") || \"/\";\n\n/** Is `href` (an absolute family address) the page `path` of the site at `siteUrl`? Locale prefix and trailing slash do not count. */\nfunction isThisPage(href: string, siteUrl: string, path: string): boolean {\n try {\n const target = new URL(href);\n return target.host === new URL(siteUrl).host && bare(target.pathname) === bare(path.split(/[?#]/)[0]!);\n } catch {\n return false;\n }\n}\n\n/** The link to the page you are on: no reload, back to the top (the browser would otherwise keep or restore the scroll). */\nfunction backToTop(event: React.MouseEvent<HTMLAnchorElement>) {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n event.preventDefault();\n window.scrollTo({ top: 0, behavior: \"auto\" });\n}\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 * The path of the page being read on this site (`/en/story`, `/blog/…`): the company link that points to this very\n * page says so (`aria-current=\"page\"`, a visible state) and a click on it scrolls to the top instead of reloading.\n */\n path?: 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 * `site` — the full footer of a marketing or company site. `docs` — a compact one for documentation pages (no site\n * block, the columns in a row, the documentation's width; never the family). `app` — one slim line inside a signed-in\n * product: the publisher line, the version and the legal links (no columns, no family); render it once, in the\n * layout, and not at all where the app shell has a sidebar that holds them.\n * @default \"site\"\n */\n variant?: \"site\" | \"docs\" | \"app\";\n};\n\n/** The footer of a Krizaka site, one band: its own columns and the family's, then one line — signature, version, Montréal, legal. */\nexport function SiteFooter({\n current,\n labels = familyLabels.en,\n children,\n version,\n env,\n path,\n legal,\n family: familyProp = true,\n variant = \"site\",\n className,\n ...props\n}: SiteFooterProps) {\n const id = React.useId();\n const s = siteFooter({ variant });\n const family = variant === \"site\" && familyProp;\n const slim = variant === \"app\";\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n const own = React.Children.toArray(children).length > 0;\n const lang = labels.lang ?? \"en\";\n const company = labels.company ?? familyLabels.en.company!;\n const here = (href: string) => path !== undefined && isThisPage(href, site.url, path);\n // Products · Open source (GitHub, every repository, closes it) · Krizaka (the company's pages): three columns, no\n // column that lists only Krizaka.\n const groups = sitesByGroup();\n const columns = [\n ...groups.filter((g) => g.group === \"products\"),\n { group: \"open-source\" as const, sites: groups.filter((g) => g.group === \"open-source\" || g.group === \"github\").flatMap((g) => g.sites) },\n ];\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n {!slim && (own || family) && (\n <div className={s.band()}>\n {own && <div className={s.own()}>{children}</div>}\n {family && (\n <nav aria-label={labels.family} className={s.family()}>\n {columns.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 <div className={s.column()}>\n <p id={`${id}-krizaka`} className={s.title()}>\n {labels.groups.krizaka}\n </p>\n <ul aria-labelledby={`${id}-krizaka`} className={s.list()}>\n {companyLinks.map((l) => (\n <li key={l.id}>\n <a\n href={l.url[lang]}\n aria-current={here(l.url[lang]) ? \"page\" : undefined}\n onClick={here(l.url[lang]) ? backToTop : undefined}\n className={s.link()}\n >\n {company[l.id]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n </nav>\n )}\n </div>\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 {!slim && <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 // A group with no link is not a column: only a heading over nothing.\n if (React.Children.toArray(children).length === 0) return null;\n const s = siteFooter();\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.toArray(children).map((child, i) => <li key={i}>{child}</li>)}\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 s = siteFooter();\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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/site-footer/site-footer.tsx"],"sourcesContent":["// Client (the marks hold an id). The footer shared by every site of the Krizaka family — one calm band (Cahier, ADR-004;\n// peer review krizaka-ui#434: « too much, a double footer »): the site's own columns and the family's columns side by\n// side — Products · Open source (GitHub with them) · Krizaka (our story, the blog, contact) — then one line, with no\n// rule above it (a bottom bar under the columns read as a second footer, 2026-10-21): the signature (“A Krizaka product”, by the kind of the current site, to www), the version, the place, the legal links.\n// No site block (the bar already names the site), no column that lists only Krizaka, no link twice.\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 { companyLinks, 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:\n \"mx-auto grid w-full max-w-[calc(var(--kz-page-max,72rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] grid-cols-[minmax(0,1fr)] gap-10 px-[var(--kz-page-gutter,clamp(16px,3.2vw,32px))] py-12\",\n band: \"grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-flow-col lg:auto-cols-[minmax(0,1fr)]\",\n own: \"grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-8\",\n family: \"grid grid-cols-2 gap-8 sm:grid-cols-3\",\n column: \"grid min-w-0 content-start gap-3\",\n title: \"text-[11px] font-semibold 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=page]:font-semibold 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-sm bg-surface-3 px-1.5 py-px text-[10px] font-semibold text-fg-secondary\",\n bottom: \"flex flex-wrap items-center gap-x-5 gap-y-3 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-sm 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 variants: {\n // `site`: the full footer. `docs`: no site block, the columns in one row, on the documentation's width. `app`: one\n // slim line inside a signed-in product — signature, version, legal links.\n variant: {\n site: {},\n docs: {\n inner: \"max-w-[calc(var(--kz-page-max-docs,88rem)_+_2*var(--kz-page-gutter,clamp(16px,3.2vw,32px)))] gap-6 py-8\",\n own: \"sm:grid-cols-4\",\n },\n app: {\n root: \"bg-transparent\",\n inner: \"gap-0 py-3\",\n },\n },\n },\n defaultVariants: { variant: \"site\" },\n});\n\nconst bare = (p: string) => p.replace(/^\\/(en|fr)(?=\\/|$)/, \"\").replace(/\\/+$/, \"\") || \"/\";\n\n/** Is `href` (an absolute family address) the page `path` of the site at `siteUrl`? Locale prefix and trailing slash do not count. */\nfunction isThisPage(href: string, siteUrl: string, path: string): boolean {\n try {\n const target = new URL(href);\n return target.host === new URL(siteUrl).host && bare(target.pathname) === bare(path.split(/[?#]/)[0]!);\n } catch {\n return false;\n }\n}\n\n/** The link to the page you are on: no reload, back to the top (the browser would otherwise keep or restore the scroll). */\nfunction backToTop(event: React.MouseEvent<HTMLAnchorElement>) {\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;\n event.preventDefault();\n window.scrollTo({ top: 0, behavior: \"auto\" });\n}\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 * The path of the page being read on this site (`/en/story`, `/blog/…`): the company link that points to this very\n * page says so (`aria-current=\"page\"`, a visible state) and a click on it scrolls to the top instead of reloading.\n */\n path?: string;\n /**\n * Shows the family in columns. Only the hub, krizaka.com, lists the family: every other site (ui., java., the blog, the\n * products) holds its own columns, the legal links, the version and one quiet publisher line — “A Krizaka project” —\n * that links back to www.krizaka.com, the only way back between the sites (2026-10-22).\n * @default current === \"krizaka\"\n */\n family?: boolean;\n /**\n * The hub's product list: only these products appear in the Products column (krizaka.com shows its focus products,\n * and all of them once the reader asks for the whole notebook). Omit it to list every product.\n */\n products?: readonly SiteId[];\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 * `site` — the full footer of a marketing or company site. `docs` — a compact one for documentation pages (no site\n * block, the columns in a row, the documentation's width; never the family). `app` — one slim line inside a signed-in\n * product: the publisher line, the version and the legal links (no columns, no family); render it once, in the\n * layout, and not at all where the app shell has a sidebar that holds them.\n * @default \"site\"\n */\n variant?: \"site\" | \"docs\" | \"app\";\n};\n\n/** The footer of a Krizaka site, one band: its own columns and the family's, then one line — signature, version, Montréal, legal. */\nexport function SiteFooter({\n current,\n labels = familyLabels.en,\n children,\n version,\n env,\n path,\n legal,\n family: familyProp,\n products,\n variant = \"site\",\n className,\n ...props\n}: SiteFooterProps) {\n const id = React.useId();\n const s = siteFooter({ variant });\n const family = variant === \"site\" && (familyProp ?? current === \"krizaka\");\n const slim = variant === \"app\";\n const site = siteById(current);\n const krizaka = siteById(\"krizaka\");\n const tag = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean).join(\" · \");\n const own = React.Children.toArray(children).length > 0;\n const lang = labels.lang ?? \"en\";\n const company = labels.company ?? familyLabels.en.company!;\n const here = (href: string) => path !== undefined && isThisPage(href, site.url, path);\n // Products · Open source (GitHub, every repository, closes it) · Krizaka (the company's pages): three columns, no\n // column that lists only Krizaka.\n const groups = sitesByGroup();\n const columns = [\n ...groups.filter((g) => g.group === \"products\").map((g) => (products ? { ...g, sites: g.sites.filter((x) => products.includes(x.id) || x.id === current) } : g)),\n { group: \"open-source\" as const, sites: groups.filter((g) => g.group === \"open-source\" || g.group === \"github\").flatMap((g) => g.sites) },\n ];\n return (\n <footer className={s.root({ className })} {...props}>\n <div className={s.inner()}>\n {!slim && (own || family) && (\n <div className={s.band()}>\n {own && <div className={s.own()}>{children}</div>}\n {family && (\n <nav aria-label={labels.family} className={s.family()}>\n {columns.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 <div className={s.column()}>\n <p id={`${id}-krizaka`} className={s.title()}>\n {labels.groups.krizaka}\n </p>\n <ul aria-labelledby={`${id}-krizaka`} className={s.list()}>\n {companyLinks.map((l) => (\n <li key={l.id}>\n <a\n href={l.url[lang]}\n aria-current={here(l.url[lang]) ? \"page\" : undefined}\n onClick={here(l.url[lang]) ? backToTop : undefined}\n className={s.link()}\n >\n {company[l.id]}\n </a>\n </li>\n ))}\n </ul>\n </div>\n </nav>\n )}\n </div>\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 {!slim && <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 // A group with no link is not a column: only a heading over nothing.\n if (React.Children.toArray(children).length === 0) return null;\n const s = siteFooter();\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.toArray(children).map((child, i) => <li key={i}>{child}</li>)}\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 s = siteFooter();\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"],"mappings":"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","names":[]}
package/dist/tabs.d.ts CHANGED
@@ -68,19 +68,58 @@ type TabsVariants = VariantProps<typeof tabs>;
68
68
  type TabsRootProps = React.ComponentProps<typeof Tabs$1.Root> & {
69
69
  /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */
70
70
  variant?: TabsVariants["variant"];
71
+ /** Binds the active tab to this search parameter of the URL (`?tab=…`): the URL is the state. Without it the state is local — only for an ephemeral choice. */
72
+ routeKey?: string;
73
+ /** The values the parameter may hold (the tabs' `value`s); the first is the default unless `defaultValue` says otherwise. */
74
+ routeValues?: readonly string[];
71
75
  };
72
76
  /** `value`/`defaultValue` + `onValueChange`, `orientation` (horizontal by default), `variant` for the list. */
73
- declare function TabsRoot({ variant, className, ...props }: TabsRootProps): React.JSX.Element;
77
+ declare function TabsRoot({ variant, className, routeKey, routeValues, ...rest }: TabsRootProps): React.JSX.Element;
74
78
  /** The tabs. Name it (`aria-label`) when no heading says what it switches. One light glides to the active tab. */
75
79
  declare function TabsList({ className, ...props }: React.ComponentProps<typeof Tabs$1.List>): React.JSX.Element;
76
80
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof Tabs$1.Trigger>): React.JSX.Element;
77
81
  declare function TabsContent({ className, ...props }: React.ComponentProps<typeof Tabs$1.Content>): React.JSX.Element;
78
- /** `Tabs.Root/List/Trigger/Content` — `variant` underline · segmented · pills on the root. */
82
+ /**
83
+ * Is the link `href` the tab of the route `current` (a path, with its search: `/docs/api?tab=x`)? The path must equal the
84
+ * link's (a trailing slash, a locale-free comparison and the hash do not count); with `exact` false (the default) a deeper
85
+ * path of the same section counts too (`/docs/api/x` is in `/docs/api`); every search parameter of the link must be in
86
+ * `current`. The hub link (`/`) is only ever active on `/` itself.
87
+ */
88
+ declare function isTabActive(current: string, href: string, exact?: boolean): boolean;
89
+ type TabsNavProps = Omit<React.ComponentProps<"nav">, "children"> & {
90
+ /** The navigation's accessible name (« Sections »). */
91
+ label: string;
92
+ /** The current route, passed in: the path with its search (`usePathname()` + `useSearchParams()`, `/docs?tab=api`). The active tab comes from it; no local state. */
93
+ current: string;
94
+ /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */
95
+ variant?: TabsVariants["variant"];
96
+ /** The tabs: `Tabs.NavLink`. */
97
+ children: React.ReactNode;
98
+ };
99
+ /**
100
+ * Tabs as routes: a `nav` of links, the URL is the state. Per WAI-ARIA these are links, not a tablist: each tab is an
101
+ * anchor, the active one carries `aria-current="page"`, the keyboard is the page's own (Tab walks the links, Enter
102
+ * follows one) and there is no panel — the page is the panel. The same looks and the same gliding light as `Tabs.List`.
103
+ */
104
+ declare function TabsNav({ label, current, variant, className, children, ...props }: TabsNavProps): React.JSX.Element;
105
+ type TabsNavLinkProps = React.ComponentProps<"a"> & {
106
+ /** Where the tab goes (`/docs/api`, `/docs?tab=api`). */
107
+ href: string;
108
+ /** Match this path only, not the pages below it. */
109
+ exact?: boolean;
110
+ /** Renders your router's `<Link>` (pass it as the only child; the `href` goes on this part and on the link). */
111
+ asChild?: boolean;
112
+ };
113
+ /** One tab of `Tabs.Nav`: an anchor that is active when the current route is its `href`. */
114
+ declare function TabsNavLink({ href, exact, asChild, className, children, ...props }: TabsNavLinkProps): React.JSX.Element;
115
+ /** `Tabs.Root/List/Trigger/Content` — `variant` underline · segmented · pills on the root. `Tabs.Nav` + `Tabs.NavLink`: the same tabs as routes. */
79
116
  declare const Tabs: {
80
117
  Root: typeof TabsRoot;
81
118
  List: typeof TabsList;
82
119
  Trigger: typeof TabsTrigger;
83
120
  Content: typeof TabsContent;
121
+ Nav: typeof TabsNav;
122
+ NavLink: typeof TabsNavLink;
84
123
  };
85
124
 
86
- export { Tabs, TabsContent, TabsList, TabsRoot, type TabsRootProps, TabsTrigger, type TabsVariants, tabs };
125
+ export { Tabs, TabsContent, TabsList, TabsNav, TabsNavLink, type TabsNavLinkProps, type TabsNavProps, TabsRoot, type TabsRootProps, TabsTrigger, type TabsVariants, isTabActive, tabs };
package/dist/tabs.js CHANGED
@@ -2,9 +2,15 @@
2
2
  import {
3
3
  useSlidingIndicator
4
4
  } from "./chunk-4QN5YJ3D.js";
5
+ import {
6
+ useRouteBinding
7
+ } from "./chunk-R3LTSMS5.js";
8
+ import {
9
+ cn
10
+ } from "./chunk-NSLXEDIS.js";
5
11
 
6
12
  // src/tabs/tabs.tsx
7
- import { Tabs as TabsPrimitive } from "radix-ui";
13
+ import { Slot, Tabs as TabsPrimitive } from "radix-ui";
8
14
  import * as React from "react";
9
15
  import { tv } from "tailwind-variants";
10
16
  import { jsx } from "react/jsx-runtime";
@@ -15,7 +21,7 @@ var tabs = tv({
15
21
  // (server render), each trigger carries its own active style.
16
22
  list: "group/tabs relative isolate flex shrink-0 data-[orientation=vertical]:flex-col before:pointer-events-none before:absolute before:-z-10 before:opacity-0 before:transition-[translate,width,height,opacity] before:duration-300 motion-reduce:before:transition-none data-[indicator=on]:before:opacity-100",
17
23
  trigger: "inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40",
18
- content: "kz-fade min-w-0 flex-1 rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-[state=inactive]:hidden"
24
+ content: "kz-tabs-panel min-w-0 flex-1 rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-[state=inactive]:hidden"
19
25
  },
20
26
  variants: {
21
27
  variant: {
@@ -39,7 +45,8 @@ var tabs = tv({
39
45
  defaultVariants: { variant: "underline" }
40
46
  });
41
47
  var VariantContext = React.createContext("underline");
42
- function TabsRoot({ variant = "underline", className, ...props }) {
48
+ function TabsRoot({ variant = "underline", className, routeKey, routeValues, ...rest }) {
49
+ const props = useRouteBinding(routeKey, routeValues, rest);
43
50
  return /* @__PURE__ */ jsx(VariantContext.Provider, { value: variant, children: /* @__PURE__ */ jsx(TabsPrimitive.Root, { "data-variant": variant, className: tabs({ variant }).root({ className }), ...props }) });
44
51
  }
45
52
  function TabsList({ className, ...props }) {
@@ -54,13 +61,54 @@ function TabsTrigger({ className, ...props }) {
54
61
  function TabsContent({ className, ...props }) {
55
62
  return /* @__PURE__ */ jsx(TabsPrimitive.Content, { className: tabs().content({ className }), ...props });
56
63
  }
57
- var Tabs = { Root: TabsRoot, List: TabsList, Trigger: TabsTrigger, Content: TabsContent };
64
+ function isTabActive(current, href, exact = false) {
65
+ const split = (value) => {
66
+ const url = new URL(value.split("#")[0], "https://tabs.invalid");
67
+ return { path: url.pathname.replace(/\/+$/, "") || "/", search: url.searchParams };
68
+ };
69
+ const here = split(current);
70
+ const target = split(href);
71
+ const samePath = here.path === target.path;
72
+ const deeper = !exact && target.path !== "/" && here.path.startsWith(`${target.path}/`);
73
+ if (!samePath && !deeper) return false;
74
+ for (const [key, value] of target.search) if (here.search.get(key) !== value) return false;
75
+ return true;
76
+ }
77
+ var NavContext = React.createContext(null);
78
+ function TabsNav({ label, current, variant = "underline", className, children, ...props }) {
79
+ const { ref } = useSlidingIndicator({ item: "[data-tabs-link]", active: "[data-tabs-link][data-state=active]" });
80
+ const value = React.useMemo(() => ({ current, variant }), [current, variant]);
81
+ return /* @__PURE__ */ jsx(NavContext.Provider, { value, children: /* @__PURE__ */ jsx("nav", { ref, "aria-label": label, "data-variant": variant, "data-orientation": "horizontal", className: tabs({ variant }).list({ className }), ...props, children }) });
82
+ }
83
+ function TabsNavLink({ href, exact, asChild, className, children, ...props }) {
84
+ const ctx = React.useContext(NavContext);
85
+ if (!ctx) throw new Error("Tabs.NavLink must be inside <Tabs.Nav>");
86
+ const active = isTabActive(ctx.current, href, exact);
87
+ const Comp = asChild ? Slot.Root : "a";
88
+ return /* @__PURE__ */ jsx(
89
+ Comp,
90
+ {
91
+ "data-tabs-link": "",
92
+ "data-state": active ? "active" : "inactive",
93
+ "data-orientation": "horizontal",
94
+ "aria-current": active ? "page" : void 0,
95
+ ...asChild ? {} : { href },
96
+ className: cn(tabs({ variant: ctx.variant }).trigger(), "no-underline", className),
97
+ ...props,
98
+ children
99
+ }
100
+ );
101
+ }
102
+ var Tabs = { Root: TabsRoot, List: TabsList, Trigger: TabsTrigger, Content: TabsContent, Nav: TabsNav, NavLink: TabsNavLink };
58
103
  export {
59
104
  Tabs,
60
105
  TabsContent,
61
106
  TabsList,
107
+ TabsNav,
108
+ TabsNavLink,
62
109
  TabsRoot,
63
110
  TabsTrigger,
111
+ isTabActive,
64
112
  tabs
65
113
  };
66
114
  //# sourceMappingURL=tabs.js.map
package/dist/tabs.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tabs/tabs.tsx"],"sourcesContent":["// Client (a context). Radix Tabs does the roles (tablist, tab, tabpanel), the aria-selected / aria-controls wiring,\n// the roving focus (arrows, Home, End) and automatic activation: this file only holds the three looks.\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const tabs = tv({\n slots: {\n root: \"flex gap-4 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row\",\n // One light glides from the active tab to the next (`before:`, placed by useSlidingIndicator); until it is placed\n // (server render), each trigger carries its own active style.\n list:\n \"group/tabs relative isolate flex shrink-0 data-[orientation=vertical]:flex-col \" +\n \"before:pointer-events-none before:absolute before:-z-10 before:opacity-0 before:transition-[translate,width,height,opacity] before:duration-300 \" +\n \"motion-reduce:before:transition-none data-[indicator=on]:before:opacity-100\",\n trigger:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\",\n content: \"kz-fade min-w-0 flex-1 rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-[state=inactive]:hidden\",\n },\n variants: {\n variant: {\n /* The section of a page: a line under the list, the active tab underlined in the accent. */\n underline: {\n list:\n \"gap-1 border-border-default data-[orientation=horizontal]:border-b data-[orientation=vertical]:border-r before:rounded-sm before:bg-accent \" +\n \"data-[orientation=horizontal]:before:-bottom-px data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:h-0.5 \" +\n \"data-[orientation=horizontal]:before:w-(--kz-ind-w) data-[orientation=horizontal]:before:translate-x-(--kz-ind-x) \" +\n \"data-[orientation=vertical]:before:-right-px data-[orientation=vertical]:before:top-0 data-[orientation=vertical]:before:w-0.5 \" +\n \"data-[orientation=vertical]:before:h-(--kz-ind-h) data-[orientation=vertical]:before:translate-y-(--kz-ind-y)\",\n trigger:\n \"relative rounded-md px-3 py-2.5 text-sm text-fg-secondary hover:text-fg data-[state=active]:text-fg \" +\n \"after:absolute after:rounded-sm after:bg-transparent data-[state=active]:after:bg-accent \" +\n \"data-[orientation=horizontal]:after:inset-x-2 data-[orientation=horizontal]:after:-bottom-px data-[orientation=horizontal]:after:h-0.5 \" +\n \"data-[orientation=vertical]:justify-start data-[orientation=vertical]:after:inset-y-1.5 data-[orientation=vertical]:after:-right-px data-[orientation=vertical]:after:w-0.5 \" +\n \"group-data-[indicator=on]/tabs:data-[state=active]:after:bg-transparent\",\n },\n /* A view switch: equal segments in a track, the active one filled. */\n segmented: {\n list:\n \"inline-grid auto-cols-fr grid-flow-col gap-1 self-start rounded-xl border border-border-default bg-surface-1 p-1 data-[orientation=vertical]:grid-flow-row \" +\n \"before:left-0 before:top-0 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) before:rounded-lg before:bg-accent before:shadow-sm\",\n trigger:\n \"min-h-8 rounded-lg px-3 py-1.5 text-xs text-fg-secondary hover:bg-surface-2 hover:text-fg \" +\n \"data-[state=active]:bg-accent data-[state=active]:text-on-accent data-[state=active]:shadow-sm data-[state=active]:hover:bg-accent \" +\n \"transition-colors group-data-[indicator=on]/tabs:data-[state=active]:bg-transparent group-data-[indicator=on]/tabs:data-[state=active]:shadow-none\",\n },\n /* Pills that scroll sideways on a phone: a feed's sections. */\n pills: {\n list:\n \"gap-1.5 overflow-x-auto overflow-y-hidden overscroll-x-contain pt-1 -mt-1 pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"before:left-0 before:top-0 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-sm before:border before:border-accent before:bg-accent-soft\",\n trigger:\n \"h-9 rounded-sm border border-border-default bg-surface-1 px-4 text-xs text-fg-secondary hover:border-border-strong hover:text-fg \" +\n \"data-[state=active]:border-accent data-[state=active]:bg-accent-soft data-[state=active]:text-fg \" +\n \"group-data-[indicator=on]/tabs:data-[state=active]:border-transparent group-data-[indicator=on]/tabs:data-[state=active]:bg-transparent\",\n },\n },\n },\n defaultVariants: { variant: \"underline\" },\n});\n\nexport type TabsVariants = VariantProps<typeof tabs>;\ntype Variant = NonNullable<TabsVariants[\"variant\"]>;\n\nconst VariantContext = React.createContext<Variant>(\"underline\");\n\nexport type TabsRootProps = React.ComponentProps<typeof TabsPrimitive.Root> & {\n /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */\n variant?: TabsVariants[\"variant\"];\n};\n\n/** `value`/`defaultValue` + `onValueChange`, `orientation` (horizontal by default), `variant` for the list. */\nexport function TabsRoot({ variant = \"underline\", className, ...props }: TabsRootProps) {\n return (\n <VariantContext.Provider value={variant}>\n <TabsPrimitive.Root data-variant={variant} className={tabs({ variant }).root({ className })} {...props} />\n </VariantContext.Provider>\n );\n}\n\n/** The tabs. Name it (`aria-label`) when no heading says what it switches. One light glides to the active tab. */\nexport function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {\n const variant = React.useContext(VariantContext);\n const { ref } = useSlidingIndicator<HTMLDivElement>({ item: \"[role=tab]\", active: \"[role=tab][data-state=active]\" });\n return <TabsPrimitive.List ref={ref} data-variant={variant} className={tabs({ variant }).list({ className })} {...props} />;\n}\n\nexport function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n const variant = React.useContext(VariantContext);\n return <TabsPrimitive.Trigger className={tabs({ variant }).trigger({ className })} {...props} />;\n}\n\nexport function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return <TabsPrimitive.Content className={tabs().content({ className })} {...props} />;\n}\n\n/** `Tabs.Root/List/Trigger/Content` — `variant` underline · segmented · pills on the root. */\nexport const Tabs = { Root: TabsRoot, List: TabsList, Trigger: TabsTrigger, Content: TabsContent };\n"],"mappings":";;;;;;AAEA,SAAS,QAAQ,qBAAqB;AACtC,YAAY,WAAW;AACvB,SAAS,UAA6B;AA2EhC;AAvEC,IAAM,OAAO,GAAG;AAAA,EACrB,OAAO;AAAA,IACL,MAAM;AAAA;AAAA;AAAA,IAGN,MACE;AAAA,IAGF,SACE;AAAA,IAEF,SAAS;AAAA,EACX;AAAA,EACA,UAAU;AAAA,IACR,SAAS;AAAA;AAAA,MAEP,WAAW;AAAA,QACT,MACE;AAAA,QAKF,SACE;AAAA,MAKJ;AAAA;AAAA,MAEA,WAAW;AAAA,QACT,MACE;AAAA,QAEF,SACE;AAAA,MAGJ;AAAA;AAAA,MAEA,OAAO;AAAA,QACL,MACE;AAAA,QAGF,SACE;AAAA,MAGJ;AAAA,IACF;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,SAAS,YAAY;AAC1C,CAAC;AAKD,IAAM,iBAAuB,oBAAuB,WAAW;AAQxD,SAAS,SAAS,EAAE,UAAU,aAAa,WAAW,GAAG,MAAM,GAAkB;AACtF,SACE,oBAAC,eAAe,UAAf,EAAwB,OAAO,SAC9B,8BAAC,cAAc,MAAd,EAAmB,gBAAc,SAAS,WAAW,KAAK,EAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,GAAI,GAAG,OAAO,GAC1G;AAEJ;AAGO,SAAS,SAAS,EAAE,WAAW,GAAG,MAAM,GAAoD;AACjG,QAAM,UAAgB,iBAAW,cAAc;AAC/C,QAAM,EAAE,IAAI,IAAI,oBAAoC,EAAE,MAAM,cAAc,QAAQ,gCAAgC,CAAC;AACnH,SAAO,oBAAC,cAAc,MAAd,EAAmB,KAAU,gBAAc,SAAS,WAAW,KAAK,EAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,GAAI,GAAG,OAAO;AAC3H;AAEO,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAuD;AACvG,QAAM,UAAgB,iBAAW,cAAc;AAC/C,SAAO,oBAAC,cAAc,SAAd,EAAsB,WAAW,KAAK,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,GAAI,GAAG,OAAO;AAChG;AAEO,SAAS,YAAY,EAAE,WAAW,GAAG,MAAM,GAAuD;AACvG,SAAO,oBAAC,cAAc,SAAd,EAAsB,WAAW,KAAK,EAAE,QAAQ,EAAE,UAAU,CAAC,GAAI,GAAG,OAAO;AACrF;AAGO,IAAM,OAAO,EAAE,MAAM,UAAU,MAAM,UAAU,SAAS,aAAa,SAAS,YAAY;","names":[]}
1
+ {"version":3,"sources":["../src/tabs/tabs.tsx"],"sourcesContent":["// Client (a context). Radix Tabs does the roles (tablist, tab, tabpanel), the aria-selected / aria-controls wiring,\n// the roving focus (arrows, Home, End) and automatic activation: this file only holds the three looks.\nimport { Slot, Tabs as TabsPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { useRouteBinding } from \"../router\";\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const tabs = tv({\n slots: {\n root: \"flex gap-4 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row\",\n // One light glides from the active tab to the next (`before:`, placed by useSlidingIndicator); until it is placed\n // (server render), each trigger carries its own active style.\n list:\n \"group/tabs relative isolate flex shrink-0 data-[orientation=vertical]:flex-col \" +\n \"before:pointer-events-none before:absolute before:-z-10 before:opacity-0 before:transition-[translate,width,height,opacity] before:duration-300 \" +\n \"motion-reduce:before:transition-none data-[indicator=on]:before:opacity-100\",\n trigger:\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\",\n content: \"kz-tabs-panel min-w-0 flex-1 rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-[state=inactive]:hidden\",\n },\n variants: {\n variant: {\n /* The section of a page: a line under the list, the active tab underlined in the accent. */\n underline: {\n list:\n \"gap-1 border-border-default data-[orientation=horizontal]:border-b data-[orientation=vertical]:border-r before:rounded-sm before:bg-accent \" +\n \"data-[orientation=horizontal]:before:-bottom-px data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:h-0.5 \" +\n \"data-[orientation=horizontal]:before:w-(--kz-ind-w) data-[orientation=horizontal]:before:translate-x-(--kz-ind-x) \" +\n \"data-[orientation=vertical]:before:-right-px data-[orientation=vertical]:before:top-0 data-[orientation=vertical]:before:w-0.5 \" +\n \"data-[orientation=vertical]:before:h-(--kz-ind-h) data-[orientation=vertical]:before:translate-y-(--kz-ind-y)\",\n trigger:\n \"relative rounded-md px-3 py-2.5 text-sm text-fg-secondary hover:text-fg data-[state=active]:text-fg \" +\n \"after:absolute after:rounded-sm after:bg-transparent data-[state=active]:after:bg-accent \" +\n \"data-[orientation=horizontal]:after:inset-x-2 data-[orientation=horizontal]:after:-bottom-px data-[orientation=horizontal]:after:h-0.5 \" +\n \"data-[orientation=vertical]:justify-start data-[orientation=vertical]:after:inset-y-1.5 data-[orientation=vertical]:after:-right-px data-[orientation=vertical]:after:w-0.5 \" +\n \"group-data-[indicator=on]/tabs:data-[state=active]:after:bg-transparent\",\n },\n /* A view switch: equal segments in a track, the active one filled. */\n segmented: {\n list:\n \"inline-grid auto-cols-fr grid-flow-col gap-1 self-start rounded-xl border border-border-default bg-surface-1 p-1 data-[orientation=vertical]:grid-flow-row \" +\n \"before:left-0 before:top-0 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) before:rounded-lg before:bg-accent before:shadow-sm\",\n trigger:\n \"min-h-8 rounded-lg px-3 py-1.5 text-xs text-fg-secondary hover:bg-surface-2 hover:text-fg \" +\n \"data-[state=active]:bg-accent data-[state=active]:text-on-accent data-[state=active]:shadow-sm data-[state=active]:hover:bg-accent \" +\n \"transition-colors group-data-[indicator=on]/tabs:data-[state=active]:bg-transparent group-data-[indicator=on]/tabs:data-[state=active]:shadow-none\",\n },\n /* Pills that scroll sideways on a phone: a feed's sections. */\n pills: {\n list:\n \"gap-1.5 overflow-x-auto overflow-y-hidden overscroll-x-contain pt-1 -mt-1 pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"before:left-0 before:top-0 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-sm before:border before:border-accent before:bg-accent-soft\",\n trigger:\n \"h-9 rounded-sm border border-border-default bg-surface-1 px-4 text-xs text-fg-secondary hover:border-border-strong hover:text-fg \" +\n \"data-[state=active]:border-accent data-[state=active]:bg-accent-soft data-[state=active]:text-fg \" +\n \"group-data-[indicator=on]/tabs:data-[state=active]:border-transparent group-data-[indicator=on]/tabs:data-[state=active]:bg-transparent\",\n },\n },\n },\n defaultVariants: { variant: \"underline\" },\n});\n\nexport type TabsVariants = VariantProps<typeof tabs>;\ntype Variant = NonNullable<TabsVariants[\"variant\"]>;\n\nconst VariantContext = React.createContext<Variant>(\"underline\");\n\nexport type TabsRootProps = React.ComponentProps<typeof TabsPrimitive.Root> & {\n /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */\n variant?: TabsVariants[\"variant\"];\n /** Binds the active tab to this search parameter of the URL (`?tab=…`): the URL is the state. Without it the state is local — only for an ephemeral choice. */\n routeKey?: string;\n /** The values the parameter may hold (the tabs' `value`s); the first is the default unless `defaultValue` says otherwise. */\n routeValues?: readonly string[];\n};\n\n/** `value`/`defaultValue` + `onValueChange`, `orientation` (horizontal by default), `variant` for the list. */\nexport function TabsRoot({ variant = \"underline\", className, routeKey, routeValues, ...rest }: TabsRootProps) {\n const props = useRouteBinding(routeKey, routeValues, rest);\n return (\n <VariantContext.Provider value={variant}>\n <TabsPrimitive.Root data-variant={variant} className={tabs({ variant }).root({ className })} {...props} />\n </VariantContext.Provider>\n );\n}\n\n/** The tabs. Name it (`aria-label`) when no heading says what it switches. One light glides to the active tab. */\nexport function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {\n const variant = React.useContext(VariantContext);\n const { ref } = useSlidingIndicator<HTMLDivElement>({ item: \"[role=tab]\", active: \"[role=tab][data-state=active]\" });\n return <TabsPrimitive.List ref={ref} data-variant={variant} className={tabs({ variant }).list({ className })} {...props} />;\n}\n\nexport function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n const variant = React.useContext(VariantContext);\n return <TabsPrimitive.Trigger className={tabs({ variant }).trigger({ className })} {...props} />;\n}\n\nexport function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return <TabsPrimitive.Content className={tabs().content({ className })} {...props} />;\n}\n\n// ── Route mode: tabs that are links; the URL is the state (Oussama, 2026-10-22) ──────────────────────────────────────\n\n/**\n * Is the link `href` the tab of the route `current` (a path, with its search: `/docs/api?tab=x`)? The path must equal the\n * link's (a trailing slash, a locale-free comparison and the hash do not count); with `exact` false (the default) a deeper\n * path of the same section counts too (`/docs/api/x` is in `/docs/api`); every search parameter of the link must be in\n * `current`. The hub link (`/`) is only ever active on `/` itself.\n */\nexport function isTabActive(current: string, href: string, exact = false): boolean {\n const split = (value: string) => {\n const url = new URL(value.split(\"#\")[0]!, \"https://tabs.invalid\");\n return { path: url.pathname.replace(/\\/+$/, \"\") || \"/\", search: url.searchParams };\n };\n const here = split(current);\n const target = split(href);\n const samePath = here.path === target.path;\n const deeper = !exact && target.path !== \"/\" && here.path.startsWith(`${target.path}/`);\n if (!samePath && !deeper) return false;\n for (const [key, value] of target.search) if (here.search.get(key) !== value) return false;\n return true;\n}\n\ninterface NavState {\n current: string;\n variant: Variant;\n}\nconst NavContext = React.createContext<NavState | null>(null);\n\nexport type TabsNavProps = Omit<React.ComponentProps<\"nav\">, \"children\"> & {\n /** The navigation's accessible name (« Sections »). */\n label: string;\n /** The current route, passed in: the path with its search (`usePathname()` + `useSearchParams()`, `/docs?tab=api`). The active tab comes from it; no local state. */\n current: string;\n /** underline (the sections of a page) · segmented (a view switch) · pills (a feed's sections). */\n variant?: TabsVariants[\"variant\"];\n /** The tabs: `Tabs.NavLink`. */\n children: React.ReactNode;\n};\n\n/**\n * Tabs as routes: a `nav` of links, the URL is the state. Per WAI-ARIA these are links, not a tablist: each tab is an\n * anchor, the active one carries `aria-current=\"page\"`, the keyboard is the page's own (Tab walks the links, Enter\n * follows one) and there is no panel — the page is the panel. The same looks and the same gliding light as `Tabs.List`.\n */\nexport function TabsNav({ label, current, variant = \"underline\", className, children, ...props }: TabsNavProps) {\n const { ref } = useSlidingIndicator<HTMLElement>({ item: \"[data-tabs-link]\", active: \"[data-tabs-link][data-state=active]\" });\n const value = React.useMemo(() => ({ current, variant }), [current, variant]);\n return (\n <NavContext.Provider value={value}>\n <nav ref={ref} aria-label={label} data-variant={variant} data-orientation=\"horizontal\" className={tabs({ variant }).list({ className })} {...props}>\n {children}\n </nav>\n </NavContext.Provider>\n );\n}\n\nexport type TabsNavLinkProps = React.ComponentProps<\"a\"> & {\n /** Where the tab goes (`/docs/api`, `/docs?tab=api`). */\n href: string;\n /** Match this path only, not the pages below it. */\n exact?: boolean;\n /** Renders your router's `<Link>` (pass it as the only child; the `href` goes on this part and on the link). */\n asChild?: boolean;\n};\n\n/** One tab of `Tabs.Nav`: an anchor that is active when the current route is its `href`. */\nexport function TabsNavLink({ href, exact, asChild, className, children, ...props }: TabsNavLinkProps) {\n const ctx = React.useContext(NavContext);\n if (!ctx) throw new Error(\"Tabs.NavLink must be inside <Tabs.Nav>\");\n const active = isTabActive(ctx.current, href, exact);\n const Comp = asChild ? Slot.Root : \"a\";\n return (\n <Comp\n data-tabs-link=\"\"\n data-state={active ? \"active\" : \"inactive\"}\n data-orientation=\"horizontal\"\n aria-current={active ? \"page\" : undefined}\n {...(asChild ? {} : { href })}\n className={cn(tabs({ variant: ctx.variant }).trigger(), \"no-underline\", className)}\n {...props}\n >\n {children}\n </Comp>\n );\n}\n\n/** `Tabs.Root/List/Trigger/Content` — `variant` underline · segmented · pills on the root. `Tabs.Nav` + `Tabs.NavLink`: the same tabs as routes. */\nexport const Tabs = { Root: TabsRoot, List: TabsList, Trigger: TabsTrigger, Content: TabsContent, Nav: TabsNav, NavLink: TabsNavLink };\n"],"mappings":"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","names":[]}
package/dist/tag-input.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import {
3
3
  Combobox
4
- } from "./chunk-AAP3U66V.js";
5
- import "./chunk-C3NF7V5Y.js";
4
+ } from "./chunk-KBQD6HE3.js";
5
+ import "./chunk-HHFWK2HH.js";
6
6
  import "./chunk-D3T54RSL.js";
7
7
  import "./chunk-ZIXB33X7.js";
8
8
  import {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  Chip
4
- } from "./chunk-C3NF7V5Y.js";
4
+ } from "./chunk-HHFWK2HH.js";
5
5
  import "./chunk-D3T54RSL.js";
6
6
 
7
7
  // src/tag-selector/tag-selector.tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.42.0",
3
+ "version": "3.43.1",
4
4
  "description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia, Yasakia, Yororis) and the motion signature — web and /native.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -157,6 +157,10 @@
157
157
  "types": "./dist/tabs.d.ts",
158
158
  "import": "./dist/tabs.js"
159
159
  },
160
+ "./router": {
161
+ "types": "./dist/router.d.ts",
162
+ "import": "./dist/router.js"
163
+ },
160
164
  "./chip": {
161
165
  "types": "./dist/chip.d.ts",
162
166
  "import": "./dist/chip.js"
@@ -469,6 +473,10 @@
469
473
  "types": "./dist/bird-plate.d.ts",
470
474
  "import": "./dist/bird-plate.js"
471
475
  },
476
+ "./elephant": {
477
+ "types": "./dist/elephant.d.ts",
478
+ "import": "./dist/elephant.js"
479
+ },
472
480
  "./flock": {
473
481
  "types": "./dist/flock.d.ts",
474
482
  "import": "./dist/flock.js"
@@ -745,7 +753,7 @@
745
753
  ],
746
754
  "peerDependencies": {
747
755
  "@krizaka/query": ">=0.2.1",
748
- "@krizaka/tailwind": ">=3.42.0",
756
+ "@krizaka/tailwind": ">=3.43.1",
749
757
  "@maplibre/maplibre-react-native": ">=11",
750
758
  "@shopify/flash-list": ">=2",
751
759
  "expo-camera": ">=16",
@@ -805,7 +813,7 @@
805
813
  "typescript": "^5.9.0",
806
814
  "vitest": "^4.1.11",
807
815
  "zod": "^4.6.5",
808
- "@krizaka/config": "0.4.2",
816
+ "@krizaka/config": "0.5.0",
809
817
  "@krizaka/query": "0.2.1"
810
818
  },
811
819
  "engines": {
@@ -1133,10 +1141,17 @@
1133
1141
  "clsx"
1134
1142
  ]
1135
1143
  },
1144
+ {
1145
+ "name": "./router",
1146
+ "path": "dist/router.js",
1147
+ "limit": "1.2 kB",
1148
+ "gzip": true,
1149
+ "rolldown": false
1150
+ },
1136
1151
  {
1137
1152
  "name": "./tabs",
1138
1153
  "path": "dist/tabs.js",
1139
- "limit": "10.1 kB",
1154
+ "limit": "11.2 kB",
1140
1155
  "gzip": true,
1141
1156
  "ignore": [
1142
1157
  "tailwind-variants",
@@ -1947,6 +1962,16 @@
1947
1962
  "clsx"
1948
1963
  ]
1949
1964
  },
1965
+ {
1966
+ "name": "./elephant",
1967
+ "path": "dist/elephant.js",
1968
+ "limit": "1.4 kB",
1969
+ "gzip": true,
1970
+ "ignore": [
1971
+ "tailwind-variants",
1972
+ "clsx"
1973
+ ]
1974
+ },
1950
1975
  {
1951
1976
  "name": "./flock",
1952
1977
  "path": "dist/flock.js",
@@ -2128,7 +2153,7 @@
2128
2153
  {
2129
2154
  "name": "./choice-grid",
2130
2155
  "path": "dist/choice-grid.js",
2131
- "limit": "1.6 kB",
2156
+ "limit": "2.2 kB",
2132
2157
  "gzip": true,
2133
2158
  "ignore": [
2134
2159
  "tailwind-variants",
@@ -2548,9 +2573,9 @@
2548
2573
  "radix-ui": "^1.7.0",
2549
2574
  "sonner": "^2.0.8",
2550
2575
  "tailwind-variants": "^3.3.1",
2551
- "@krizaka/intl": "^0.2.1",
2552
2576
  "@krizaka/pacer": "^0.1.0",
2553
- "@krizaka/tokens": "^3.42.0"
2577
+ "@krizaka/tokens": "^3.43.1",
2578
+ "@krizaka/intl": "^0.2.1"
2554
2579
  },
2555
2580
  "scripts": {
2556
2581
  "build": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});for(const f of fs.readdirSync('registry/examples',{recursive:true}))if(/\\.(js|d\\.ts)$/.test(f))fs.rmSync('registry/examples/'+f)\" && tsup && node -e \"require('node:fs').copyFileSync('src/motion.css','dist/motion.css');require('node:fs').copyFileSync('src/blocks.css','dist/blocks.css');require('node:fs').copyFileSync('src/story.css','dist/story.css')\" && node scripts/build-registry.mjs",
@@ -179,7 +179,7 @@
179
179
  },
180
180
  {
181
181
  "path": "meta.ts",
182
- "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"flock\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
182
+ "content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"anatomy-plate\"\n | \"bird-plate\"\n | \"flock\"\n | \"elephant\"\n | \"footnote\"\n | \"page-index\"\n | \"specimen-tag\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-header\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"captcha\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"chip-row\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"comparison-table\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cover-hero\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"docs-nav\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"engine-flow\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"genui\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"maintenance-screen\"\n | \"map\"\n | \"markdown\"\n | \"meta-row\"\n | \"waveform\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"language-switch\"\n | \"media-gallery\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"opening-hours\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"router\"\n | \"rail-nav\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"service-card\"\n | \"settings-layout\"\n | \"account-menu\"\n | \"showcase\"\n | \"step-showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot-picker\"\n | \"slot\"\n | \"spinner\"\n | \"sticky-action\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
183
183
  }
184
184
  ],
185
185
  "dependencies": [],