@krizaka/ui 3.20.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-bar.js +4 -4
- package/dist/argument-card.js +1 -1
- package/dist/blocks.css +1 -1
- package/dist/button.js +3 -2
- package/dist/card.js +3 -3
- package/dist/{chunk-2NB27LM7.js → chunk-3IN6TOPM.js} +4 -4
- package/dist/{chunk-VDR2EP6K.js → chunk-4QN5YJ3D.js} +17 -5
- package/dist/chunk-4QN5YJ3D.js.map +1 -0
- package/dist/chunk-TDG7ED2W.js +1 -0
- package/dist/chunk-TDG7ED2W.js.map +1 -0
- package/dist/code-block.js +17 -5
- package/dist/code-block.js.map +1 -1
- package/dist/date-picker.js +2 -2
- package/dist/date-picker.js.map +1 -1
- package/dist/force-update-screen.d.ts +80 -0
- package/dist/force-update-screen.js +38 -0
- package/dist/force-update-screen.js.map +1 -0
- package/dist/maintenance-screen.d.ts +68 -0
- package/dist/maintenance-screen.js +35 -0
- package/dist/maintenance-screen.js.map +1 -0
- package/dist/media-grid.js +1 -1
- package/dist/media-grid.js.map +1 -1
- package/dist/native.d.ts +68 -1
- package/dist/native.js +767 -681
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.d.ts +30 -7
- package/dist/nav-bar.js +69 -15
- package/dist/nav-bar.js.map +1 -1
- package/dist/page-hero.js +3 -3
- package/dist/radio-group.js +1 -1
- package/dist/settings-layout.js +1 -1
- package/dist/settings-layout.js.map +1 -1
- package/dist/tabs.js +2 -2
- package/dist/tabs.js.map +1 -1
- package/dist/update-prompt.d.ts +86 -0
- package/dist/update-prompt.js +41 -0
- package/dist/update-prompt.js.map +1 -0
- package/dist/video-player.d.ts +1 -1
- package/dist/video-player.js +16 -3
- package/dist/video-player.js.map +1 -1
- package/package.json +51 -9
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/activity-feed.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/chat.json +1 -1
- package/registry/checkbox.json +1 -1
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +3 -3
- package/registry/decision-list.json +1 -1
- package/registry/description-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/document-viewer.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/force-update-screen/default.d.ts +4 -0
- package/registry/examples/force-update-screen/default.js +25 -0
- package/registry/examples/force-update-screen/default.tsx +19 -0
- package/registry/examples/force-update-screen/native/default.d.ts +4 -0
- package/registry/examples/force-update-screen/native/default.dark.png +0 -0
- package/registry/examples/force-update-screen/native/default.js +24 -0
- package/registry/examples/force-update-screen/native/default.light.png +0 -0
- package/registry/examples/force-update-screen/native/default.tsx +18 -0
- package/registry/examples/maintenance-screen/default.d.ts +4 -0
- package/registry/examples/maintenance-screen/default.js +22 -0
- package/registry/examples/maintenance-screen/default.tsx +16 -0
- package/registry/examples/maintenance-screen/native/default.d.ts +4 -0
- package/registry/examples/maintenance-screen/native/default.dark.png +0 -0
- package/registry/examples/maintenance-screen/native/default.js +21 -0
- package/registry/examples/maintenance-screen/native/default.light.png +0 -0
- package/registry/examples/maintenance-screen/native/default.tsx +15 -0
- package/registry/examples/nav-bar/family-product.js +16 -13
- package/registry/examples/nav-bar/family-product.tsx +19 -19
- package/registry/examples/update-prompt/default.d.ts +4 -0
- package/registry/examples/update-prompt/default.js +24 -0
- package/registry/examples/update-prompt/default.tsx +18 -0
- package/registry/examples/update-prompt/native/default.d.ts +4 -0
- package/registry/examples/update-prompt/native/default.dark.png +0 -0
- package/registry/examples/update-prompt/native/default.js +23 -0
- package/registry/examples/update-prompt/native/default.light.png +0 -0
- package/registry/examples/update-prompt/native/default.tsx +17 -0
- package/registry/field.json +1 -1
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/force-update-screen.json +323 -0
- package/registry/form.json +1 -1
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +97 -4
- package/registry/iso.json +1 -1
- package/registry/kanban.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/list.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/maintenance-screen.json +259 -0
- package/registry/map.json +2 -2
- package/registry/markdown.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +1 -1
- package/registry/mascots.json +1 -1
- package/registry/match-dialog.json +1 -1
- package/registry/media-capture.json +1 -1
- package/registry/media-grid.json +2 -2
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +47 -14
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/photo-grid.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +2 -2
- package/registry/recipes.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +2 -2
- package/registry/showcase.json +1 -1
- package/registry/site-footer.json +2 -2
- package/registry/sites.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/skip-link.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/step-progress.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/swipe-deck.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +3 -3
- package/registry/tag-input.json +1 -1
- package/registry/tag-selector.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +1 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/tree-view.json +1 -1
- package/registry/update-prompt.json +316 -0
- package/registry/verified-badge.json +1 -1
- package/registry/vertical-video.json +1 -1
- package/registry/video-player.json +4 -4
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/dist/chunk-VDR2EP6K.js.map +0 -1
- /package/dist/{chunk-2NB27LM7.js.map → chunk-3IN6TOPM.js.map} +0 -0
package/registry/nav-bar.json
CHANGED
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"bestPractices": [
|
|
49
|
-
"Five items at most on the bar; group the rest in a menu, each link with a name and one line (`NavBar.MenuLink
|
|
49
|
+
"Five items at most on the bar; group the rest in a menu, each link with a name and one short line (`NavBar.MenuLink`, one line, ≤ 45 characters).",
|
|
50
|
+
"A product's site (dev.<product>): the product's mark alone (no `NavBar.Family`, no badge — the version goes in the footer), then **Product ▾** (what it does: how it works, its features, the demo) · **Platforms ▾** (web, mobile, desktop, self-host — omitted for a single-platform product) · **Docs** · Pricing when it exists; never a lone entry that belongs to a group (no top-level Demo, Mobile or Mac). The phone panel repeats the groups with `NavBar.MobileGroup`, in the same order.",
|
|
51
|
+
"Light one item at most: the group that holds the page (`active` on its `NavBar.Menu`), never two.",
|
|
50
52
|
"Mark the current page with `active`: the light rests on it, `aria-current=\"page\"` says it.",
|
|
51
53
|
"Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.",
|
|
52
54
|
"One primary action at most in `NavBar.Actions` (a `Button` `size=\"sm\"` `shape=\"pill\"`).",
|
|
@@ -77,6 +79,10 @@
|
|
|
77
79
|
"keys": "Esc",
|
|
78
80
|
"action": "Closes the menu, the family or the mobile panel and returns the focus to its trigger."
|
|
79
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"keys": "Click outside",
|
|
84
|
+
"action": "Closes the open menu or the mobile panel."
|
|
85
|
+
},
|
|
80
86
|
{
|
|
81
87
|
"keys": "Enter / Space / ↓ (family)",
|
|
82
88
|
"action": "Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover."
|
|
@@ -84,7 +90,8 @@
|
|
|
84
90
|
],
|
|
85
91
|
"notes": [
|
|
86
92
|
"A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.",
|
|
87
|
-
"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded
|
|
93
|
+
"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`; a press outside closes it. `NavBar.MobileGroup` is a `group` named by its heading.",
|
|
94
|
+
"Menus open on click, Enter, Space or ↓, or when the pointer rests on the trigger (`delayDuration`, 200 ms) — passing over the bar opens nothing. While a menu is open the light stays on its trigger; moving on to a plain link closes the menu.",
|
|
88
95
|
"The light, the chevron and the tightening do not move under `prefers-reduced-motion`; the current page is tinted before scripts load.",
|
|
89
96
|
"The family is its own `nav` named by `labels.family`; its chevron is a button with the same name, `aria-expanded` and `aria-controls`. Each group is a list named by its heading, the current site says `aria-current=\"true\"` and “You are here”, a site not released says its status in words.",
|
|
90
97
|
"`NavBar.Repo` is an icon link named by `label`; `NavBar.Badge` is plain text (`DEV · v1.2.3`), hidden below `sm`."
|
|
@@ -198,7 +205,7 @@
|
|
|
198
205
|
},
|
|
199
206
|
{
|
|
200
207
|
"component": "NavBarItems",
|
|
201
|
-
"description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see
|
|
208
|
+
"description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n`NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n`delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\nmoving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.",
|
|
202
209
|
"props": []
|
|
203
210
|
},
|
|
204
211
|
{
|
|
@@ -302,7 +309,7 @@
|
|
|
302
309
|
},
|
|
303
310
|
{
|
|
304
311
|
"component": "NavBarMobile",
|
|
305
|
-
"description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape
|
|
312
|
+
"description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n(`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\nto the toggle.",
|
|
306
313
|
"props": [
|
|
307
314
|
{
|
|
308
315
|
"name": "closeLabel",
|
|
@@ -320,6 +327,19 @@
|
|
|
320
327
|
}
|
|
321
328
|
]
|
|
322
329
|
},
|
|
330
|
+
{
|
|
331
|
+
"component": "NavBarMobileGroup",
|
|
332
|
+
"description": "A group of the mobile panel: the links of one of the bar's menus under its name, in the same order.",
|
|
333
|
+
"props": [
|
|
334
|
+
{
|
|
335
|
+
"name": "label",
|
|
336
|
+
"type": "string",
|
|
337
|
+
"default": null,
|
|
338
|
+
"description": "The group's heading — the same words as the bar's menu (“Product”) — passed translated.",
|
|
339
|
+
"required": true
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
},
|
|
323
343
|
{
|
|
324
344
|
"component": "NavBarMobileLink",
|
|
325
345
|
"description": "A link of the mobile panel.",
|
|
@@ -369,10 +389,10 @@
|
|
|
369
389
|
{
|
|
370
390
|
"name": "family-product",
|
|
371
391
|
"title": "dev.<product>",
|
|
372
|
-
"description": "A product's site: its mark
|
|
392
|
+
"description": "A product's site, the standard of every Krizaka product: its mark alone, Product ▾ · Platforms ▾ · Docs with compact menus (icon · title · one line), Sign in and “Talk to us”; the phone panel groups the same entries.",
|
|
373
393
|
"path": "examples/nav-bar/family-product.tsx",
|
|
374
394
|
"module": "@krizaka/ui/registry/examples/nav-bar/family-product",
|
|
375
|
-
"code": "import { OrazakaLogo } from \"@krizaka/ui\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { NavBar } from \"@krizaka/ui/nav-bar\";\
|
|
395
|
+
"code": "import { OrazakaLogo } from \"@krizaka/ui\";\nimport { Button } from \"@krizaka/ui/button\";\nimport { NavBar } from \"@krizaka/ui/nav-bar\";\n\n// The product-site standard: the mark alone, Product ▾ · Platforms ▾ · Docs, Sign in and one action; the phone panel\n// repeats the groups. Each menu link: icon · title · one line.\nconst groups = [\n { id: \"product\", label: \"Product\", links: [[\"#how\", \"How it works\", \"Install to first answer in three commands.\"], [\"#studios\", \"Studios\", \"Ready-made workflows, priced before you run.\"], [\"#demo\", \"Demo\", \"Real runs: prompts, images, videos.\"]] },\n { id: \"platforms\", label: \"Platforms\", links: [[\"#web\", \"Web\", \"The workspace in your browser.\"], [\"#mac\", \"Mac\", \"Orazaka Desktop for Apple Silicon.\"], [\"#self-host\", \"Self-host\", \"Install it on your own servers.\"]] },\n];\nconst icon = <svg data-kz-icon viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" aria-hidden><path d=\"M12 3 19.8 7.5v9L12 21l-7.8-4.5v-9Z\" /><circle data-node cx=\"12\" cy=\"12\" r=\"2.4\" fill=\"currentColor\" /></svg>;\n\nexport default function NavBarFamilyProduct() {\n return (\n <NavBar.Root label=\"Main\" floating={false}>\n <NavBar.Brand href=\"#home\"><OrazakaLogo size={22} />Orazaka</NavBar.Brand>\n <NavBar.Items>\n {groups.map((g) => (\n <NavBar.Menu key={g.id} value={g.id} label={g.label}>\n {g.links.map(([href, title, line]) => <NavBar.MenuLink key={href} href={href} title={title} description={line} icon={icon} />)}\n </NavBar.Menu>\n ))}\n <NavBar.Link href=\"#docs\">Docs</NavBar.Link>\n </NavBar.Items>\n <NavBar.Actions>\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"hidden md:inline-flex\"><a href=\"#sign-in\">Sign in</a></Button>\n <Button asChild variant=\"primary\" size=\"sm\" shape=\"cut\" className=\"hidden md:inline-flex\"><a href=\"https://www.krizaka.com/contact?topic=orazaka\">Talk to us</a></Button>\n <NavBar.Mobile openLabel=\"Open the menu\" closeLabel=\"Close the menu\">\n {groups.map((g) => (\n <NavBar.MobileGroup key={g.id} label={g.label}>\n {g.links.map(([href, title]) => <NavBar.MobileLink key={href} href={href}>{title}</NavBar.MobileLink>)}\n </NavBar.MobileGroup>\n ))}\n <NavBar.MobileLink href=\"#docs\">Docs</NavBar.MobileLink>\n </NavBar.Mobile>\n </NavBar.Actions>\n </NavBar.Root>\n );\n}\n"
|
|
376
396
|
},
|
|
377
397
|
{
|
|
378
398
|
"name": "family-admin",
|
|
@@ -488,27 +508,27 @@
|
|
|
488
508
|
},
|
|
489
509
|
{
|
|
490
510
|
"path": "meta.ts",
|
|
491
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
511
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"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 | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"splash-handoff\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"update-prompt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
492
512
|
},
|
|
493
513
|
{
|
|
494
514
|
"path": "nav-bar/index.ts",
|
|
495
|
-
"content": "/** @krizaka/ui/nav-bar — `NavBar` (Root, Brand, Family, Badge, Items, Link, Menu, MenuLink, Actions, Repo, Mobile, MobileLink), `navBar`. Client. */\nexport {\n NavBar,\n navBar,\n NavBarActions,\n NavBarBadge,\n type NavBarBadgeProps,\n NavBarBrand,\n type NavBarBrandProps,\n NavBarFamily,\n type NavBarFamilyProps,\n NavBarItems,\n NavBarLink,\n type NavBarLinkProps,\n NavBarMenu,\n NavBarMenuLink,\n type NavBarMenuLinkProps,\n type NavBarMenuProps,\n NavBarMobile,\n NavBarMobileLink,\n type NavBarMobileLinkProps,\n type NavBarMobileProps,\n NavBarRepo,\n type NavBarRepoProps,\n NavBarRoot,\n type NavBarRootProps,\n} from \"./nav-bar\";\n"
|
|
515
|
+
"content": "/** @krizaka/ui/nav-bar — `NavBar` (Root, Brand, Family, Badge, Items, Link, Menu, MenuLink, Actions, Repo, Mobile, MobileGroup, MobileLink), `navBar`. Client. */\nexport {\n NavBar,\n navBar,\n NavBarActions,\n NavBarBadge,\n type NavBarBadgeProps,\n NavBarBrand,\n type NavBarBrandProps,\n NavBarFamily,\n type NavBarFamilyProps,\n NavBarItems,\n type NavBarItemsProps,\n NavBarLink,\n type NavBarLinkProps,\n NavBarMenu,\n NavBarMenuLink,\n type NavBarMenuLinkProps,\n type NavBarMenuProps,\n NavBarMobile,\n NavBarMobileGroup,\n type NavBarMobileGroupProps,\n NavBarMobileLink,\n type NavBarMobileLinkProps,\n type NavBarMobileProps,\n NavBarRepo,\n type NavBarRepoProps,\n NavBarRoot,\n type NavBarRootProps,\n} from \"./nav-bar\";\n"
|
|
496
516
|
},
|
|
497
517
|
{
|
|
498
518
|
"path": "nav-bar/meta.ts",
|
|
499
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Nav bar\",\n summary:\n \"The Krizaka navigation bar, the same on every site of the family: a floating pill that tightens as the page scrolls, the family switcher on the brand, one light that glides to the item under the pointer or the focus and rests on the current page, accessible menus with rich links, and a mobile panel.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n parity: { native: { status: \"n/a\", reason: \"A site's top bar; an app on a phone navigates with its tab bar.\", use: \"tab-bar\" }, desktop: { status: \"n/a\", reason: \"A public site's page, not a window of the Mac app.\" } },\n whenToUse: [\n \"The top navigation of a site or a product: a brand, a few links, one or two menus, actions on the right.\",\n \"A section bar inside an app (`floating={false}`): videos, earnings, auctions.\",\n \"Every site of the Krizaka family (www, ui., java., dev.<product>, admin.<product>): the brand and `NavBar.Family` on the left, at most four of the site's sections in the middle, the tools on the right.\",\n ],\n whenNotToUse: [\n { when: \"To switch views inside a page without changing the address.\", use: \"tabs\" },\n { when: \"For a list of actions on one object.\", use: \"dropdown-menu\" },\n { when: \"For search and commands across the app.\", use: \"command\" },\n { when: \"To link to another site of the family from the bar's middle: the family switcher holds them.\", use: \"sites\" },\n ],\n bestPractices: [\n \"Five items at most on the bar; group the rest in a menu, each link with a name and one line (`NavBar.MenuLink`).\",\n \"Mark the current page with `active`: the light rests on it, `aria-current=\\\"page\\\"` says it.\",\n \"Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.\",\n \"One primary action at most in `NavBar.Actions` (a `Button` `size=\\\"sm\\\"` `shape=\\\"pill\\\"`).\",\n \"Wrap it in a `sticky top-3` (floating) or `sticky top-0` container to keep it on screen.\",\n \"Three zones on every Krizaka site: who I am (`NavBar.Brand` + `NavBar.Family current=\\\"<site>\\\"`, then `NavBar.Badge` with the version or `DEV · v1.2.3`), this site (at most four sections, never a link to another site), tools (search when there is something to search, `NavBar.Repo`, theme, language, one primary action).\",\n \"Place a second `NavBar.Family` as the last child of `NavBar.Mobile`: below `md` the family is the panel's last group.\",\n \"Pass the family's words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Tab\", action: \"Moves between the brand, the items and the actions; leaving an open menu closes it.\" },\n { keys: \"Enter / Space / ↓\", action: \"Opens a menu and moves into it.\" },\n { keys: \"↑ / ↓ · Home / End\", action: \"Walks the links of an open menu.\" },\n { keys: \"← / →\", action: \"Moves between the items of the bar.\" },\n { keys: \"Esc\", action: \"Closes the menu, the family or the mobile panel and returns the focus to its trigger.\" },\n { keys: \"Enter / Space / ↓ (family)\", action: \"Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover.\" },\n ],\n notes: [\n \"A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.\",\n \"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded
|
|
519
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Nav bar\",\n summary:\n \"The Krizaka navigation bar, the same on every site of the family: a floating pill that tightens as the page scrolls, the family switcher on the brand, one light that glides to the item under the pointer or the focus and rests on the current page, accessible menus with rich links, and a mobile panel.\",\n status: \"beta\",\n category: \"navigation\",\n platforms: \"web\",\n parity: { native: { status: \"n/a\", reason: \"A site's top bar; an app on a phone navigates with its tab bar.\", use: \"tab-bar\" }, desktop: { status: \"n/a\", reason: \"A public site's page, not a window of the Mac app.\" } },\n whenToUse: [\n \"The top navigation of a site or a product: a brand, a few links, one or two menus, actions on the right.\",\n \"A section bar inside an app (`floating={false}`): videos, earnings, auctions.\",\n \"Every site of the Krizaka family (www, ui., java., dev.<product>, admin.<product>): the brand and `NavBar.Family` on the left, at most four of the site's sections in the middle, the tools on the right.\",\n ],\n whenNotToUse: [\n { when: \"To switch views inside a page without changing the address.\", use: \"tabs\" },\n { when: \"For a list of actions on one object.\", use: \"dropdown-menu\" },\n { when: \"For search and commands across the app.\", use: \"command\" },\n { when: \"To link to another site of the family from the bar's middle: the family switcher holds them.\", use: \"sites\" },\n ],\n bestPractices: [\n \"Five items at most on the bar; group the rest in a menu, each link with a name and one short line (`NavBar.MenuLink`, one line, ≤ 45 characters).\",\n \"A product's site (dev.<product>): the product's mark alone (no `NavBar.Family`, no badge — the version goes in the footer), then **Product ▾** (what it does: how it works, its features, the demo) · **Platforms ▾** (web, mobile, desktop, self-host — omitted for a single-platform product) · **Docs** · Pricing when it exists; never a lone entry that belongs to a group (no top-level Demo, Mobile or Mac). The phone panel repeats the groups with `NavBar.MobileGroup`, in the same order.\",\n \"Light one item at most: the group that holds the page (`active` on its `NavBar.Menu`), never two.\",\n \"Mark the current page with `active`: the light rests on it, `aria-current=\\\"page\\\"` says it.\",\n \"Use your router's link with `asChild` on `NavBar.Link`, `NavBar.MenuLink` and `NavBar.MobileLink`.\",\n \"One primary action at most in `NavBar.Actions` (a `Button` `size=\\\"sm\\\"` `shape=\\\"pill\\\"`).\",\n \"Wrap it in a `sticky top-3` (floating) or `sticky top-0` container to keep it on screen.\",\n \"Three zones on every Krizaka site: who I am (`NavBar.Brand` + `NavBar.Family current=\\\"<site>\\\"`, then `NavBar.Badge` with the version or `DEV · v1.2.3`), this site (at most four sections, never a link to another site), tools (search when there is something to search, `NavBar.Repo`, theme, language, one primary action).\",\n \"Place a second `NavBar.Family` as the last child of `NavBar.Mobile`: below `md` the family is the panel's last group.\",\n \"Pass the family's words translated: `labels={familyLabels[locale]}` from `@krizaka/ui/sites`.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Tab\", action: \"Moves between the brand, the items and the actions; leaving an open menu closes it.\" },\n { keys: \"Enter / Space / ↓\", action: \"Opens a menu and moves into it.\" },\n { keys: \"↑ / ↓ · Home / End\", action: \"Walks the links of an open menu.\" },\n { keys: \"← / →\", action: \"Moves between the items of the bar.\" },\n { keys: \"Esc\", action: \"Closes the menu, the family or the mobile panel and returns the focus to its trigger.\" },\n { keys: \"Click outside\", action: \"Closes the open menu or the mobile panel.\" },\n { keys: \"Enter / Space / ↓ (family)\", action: \"Opens the family from its chevron; ↑ / ↓ walk the sites. It never opens on hover.\" },\n ],\n notes: [\n \"A `nav` named by `label`, a list of links and menu triggers (Radix NavigationMenu): `aria-expanded` and `aria-controls` on each trigger.\",\n \"The mobile panel is a disclosure (no focus trap): its toggle is named by `openLabel` / `closeLabel` and says `aria-expanded`; a press outside closes it. `NavBar.MobileGroup` is a `group` named by its heading.\",\n \"Menus open on click, Enter, Space or ↓, or when the pointer rests on the trigger (`delayDuration`, 200 ms) — passing over the bar opens nothing. While a menu is open the light stays on its trigger; moving on to a plain link closes the menu.\",\n \"The light, the chevron and the tightening do not move under `prefers-reduced-motion`; the current page is tinted before scripts load.\",\n \"The family is its own `nav` named by `labels.family`; its chevron is a button with the same name, `aria-expanded` and `aria-controls`. Each group is a list named by its heading, the current site says `aria-current=\\\"true\\\"` and “You are here”, a site not released says its status in words.\",\n \"`NavBar.Repo` is an icon link named by `label`; `NavBar.Badge` is plain text (`DEV · v1.2.3`), hidden below `sm`.\",\n ],\n },\n related: [\"sites\", \"site-footer\", \"tabs\", \"dropdown-menu\", \"command\", \"button\"],\n web: {\n imports: [\"NavBar\"],\n examples: [\n { name: \"family-www\", title: \"www.krizaka.com\", description: \"The company's bar: the mark and the family switcher, Products · Open source · Our story, the repository and Contact. Open the chevron: every site of Krizaka, grouped, the current one marked.\" },\n { name: \"family-ui\", title: \"ui.krizaka.com\", description: \"Krizaka UI: the family, the version of the packages, Get started · Foundations · Components · Templates, the repository.\" },\n { name: \"family-java\", title: \"java.krizaka.com\", description: \"Krizaka Java: the family, the version of the building blocks, Get started · Building blocks · Guides.\" },\n { name: \"family-product\", title: \"dev.<product>\", description: \"A product's site, the standard of every Krizaka product: its mark alone, Product ▾ · Platforms ▾ · Docs with compact menus (icon · title · one line), Sign in and “Talk to us”; the phone panel groups the same entries.\" },\n { name: \"family-admin\", title: \"admin.<product>\", description: \"A console: the mark, the family and `DEV`, no sections, a full-width bar.\" },\n { name: \"site\", title: \"A site's bar\", description: \"Brand, a Products menu, links, a call to action, a mobile panel — hover the items: one light glides between them.\" },\n { name: \"menu-open\", title: \"A rich menu\", description: \"Two columns of links with icons whose node lights up, the current page tinted.\" },\n { name: \"app-bar\", title: \"Inside an app\", description: \"`floating={false}`: a full-width bar with a hairline, for an app's sections.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
500
520
|
},
|
|
501
521
|
{
|
|
502
522
|
"path": "nav-bar/nav-bar.tsx",
|
|
503
|
-
"content": "// Client (scroll, a sliding indicator, menus). Radix NavigationMenu does the menus' semantics and keyboard (Enter, Space\n// and ↓ open; ↑ ↓ Home End walk; Escape closes and returns the focus; Tab out closes); this file draws the Krizaka bar:\n// a floating pill that tightens once the page scrolls, one light that glides to the item under the pointer or the\n// focus and rests on the current page, and a mobile panel that is a plain disclosure (no focus trap, Escape closes).\nimport { NavigationMenu as Nav, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { FamilyMark, GitHubGlyph } from \"../family-mark\";\nimport { type FamilyLabels, familyLabels, type Site, type SiteId, sitesByGroup } from \"../sites/sites\";\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const navBar = tv({\n slots: {\n root:\n \"group/nav relative z-40 mx-auto flex h-14 w-full items-center gap-2 border-border-subtle bg-surface-1/75 backdrop-blur-xl backdrop-saturate-150 \" +\n \"transition-[height,background-color,border-color,box-shadow] duration-300 motion-reduce:transition-none \" +\n \"data-compact:h-12 data-compact:border-border-default data-compact:bg-surface-1/90 data-compact:shadow-lg\",\n brand:\n \"mr-2 inline-flex shrink-0 items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg no-underline \" +\n \"rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n items: \"relative hidden md:block\",\n list:\n \"group/list relative isolate flex items-center gap-0.5 \" +\n // The light: under the item that is pointed at or focused, resting on the current page.\n \"before:absolute before:left-0 before:top-0 before:-z-10 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) \" +\n \"before:rounded-full before:bg-surface-3 before:opacity-0 before:transition-[translate,width,opacity] before:duration-300 motion-reduce:before:transition-none \" +\n \"data-[indicator=on]:before:opacity-100\",\n item:\n \"inline-flex h-9 select-none items-center gap-1 rounded-full px-3.5 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:text-fg data-active:text-fg data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n // Before the light is ready (server render), the current page carries its own tint.\n \"group-not-data-[indicator=on]/list:data-active:bg-surface-3\",\n chevron: \"h-3.5 w-3.5 transition-transform duration-300 group-data-[state=open]/trigger:rotate-180 motion-reduce:transition-none\",\n viewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n panel: \"grid w-max max-w-[min(92vw,30rem)] gap-1 p-2\",\n menuLink:\n \"group/link flex items-start gap-3 rounded-xl p-3 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-active:bg-accent-soft\",\n menuIcon:\n \"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border-default bg-surface-2 text-fg-accent \" +\n \"transition-[transform,border-color] duration-300 group-hover/link:-translate-y-0.5 group-hover/link:border-accent [&>svg]:h-4.5 [&>svg]:w-4.5 motion-reduce:transform-none\",\n menuTitle: \"block text-sm font-semibold text-fg\",\n menuText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n actions: \"ml-auto flex items-center gap-2\",\n toggle:\n \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-subtle bg-surface-2/60 text-fg md:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n mobile: \"kz-pop absolute inset-x-0 top-full mt-2 grid gap-1 rounded-2xl border border-border-default bg-surface-1 p-2 shadow-lg md:hidden\",\n mobileLink:\n \"flex items-center justify-between rounded-xl px-3 py-3 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg-accent\",\n // The family switcher: a chevron attached to the brand, its panel aligned on the bar's left edge.\n family: \"hidden md:block\",\n familyTrigger:\n \"group/trigger -ml-2 mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg data-[state=open]:bg-surface-3 data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n familyViewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n familyPanel: \"grid w-[min(92vw,42rem)] gap-x-2 gap-y-3 p-3 sm:grid-cols-2\",\n familyGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n familyLink:\n \"group/link flex items-start gap-3 rounded-xl px-3 py-2.5 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n familyMark: \"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center text-fg\",\n familyName: \"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm font-semibold text-fg\",\n familyText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n familyTag: \"rounded-full px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider\",\n mobileFamily: \"mt-1 grid gap-2 border-t border-border-subtle px-1 pb-1 pt-3\",\n mobileFamilyLink:\n \"flex min-h-11 items-center gap-3 rounded-xl px-2 py-2 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n badge:\n \"hidden h-6 shrink-0 items-center gap-1 rounded-full border border-border-subtle bg-surface-2 px-2 font-mono text-[11px] font-medium text-fg-secondary sm:inline-flex\",\n repo:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n floating: {\n true: { root: \"max-w-5xl rounded-full border pl-5 pr-2 data-compact:max-w-4xl\" },\n false: { root: \"border-b px-4 md:px-6\" },\n },\n },\n defaultVariants: { floating: true },\n});\nconst s = navBar();\n\ninterface NavBarContext {\n label: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n panelId: string;\n}\nconst Context = React.createContext<NavBarContext | null>(null);\n/** True inside `NavBar.Mobile`: the family renders as the panel's last group instead of a switcher. */\nconst MobileContext = React.createContext(false);\nconst useNavBar = () => {\n const ctx = React.useContext(Context);\n if (!ctx) throw new Error(\"NavBar parts must be inside <NavBar.Root>.\");\n return ctx;\n};\n\nexport type NavBarRootProps = React.ComponentProps<\"header\"> & {\n /** The accessible name of the navigation (“Main”) — passed translated. */\n label: string;\n /**\n * Pixels of scroll after which the bar tightens (denser, lower, lifted). `false`: never.\n * @default 24\n */\n compactAfter?: number | false;\n /**\n * A floating pill (the Krizaka bar) or a full-width bar with a hairline under it.\n * @default true\n */\n floating?: boolean;\n};\n\n/** The bar. Place it in a `sticky top-3` (floating) or `sticky top-0` wrapper — or let it scroll. */\nexport function NavBarRoot({ label, compactAfter = 24, floating = true, className, children, ...props }: NavBarRootProps) {\n const [compact, setCompact] = React.useState(false);\n const [open, setOpen] = React.useState(false);\n const panelId = React.useId();\n React.useEffect(() => {\n if (compactAfter === false) return;\n const onScroll = () => setCompact(window.scrollY > compactAfter);\n onScroll();\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => window.removeEventListener(\"scroll\", onScroll);\n }, [compactAfter]);\n const ctx = React.useMemo(() => ({ label, open, setOpen, panelId }), [label, open, panelId]);\n return (\n <Context.Provider value={ctx}>\n <header data-compact={compact ? \"\" : undefined} data-floating={floating} className={navBar({ floating }).root({ className })} {...props}>\n {children}\n </header>\n </Context.Provider>\n );\n}\n\nexport type NavBarBrandProps = React.ComponentProps<\"a\"> & {\n /** Renders your link (a router `<Link>`) with the brand's look. */\n asChild?: boolean;\n};\n\n/** The brand: a mark and a name, linking home. */\nexport function NavBarBrand({ asChild, className, ...props }: NavBarBrandProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp className={s.brand({ className })} {...props} />;\n}\n\n/** The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see `NavBar.Mobile`). */\nexport function NavBarItems({ className, children, ...props }: Omit<React.ComponentProps<typeof Nav.Root>, \"orientation\">) {\n const { label } = useNavBar();\n const { ref, handlers } = useSlidingIndicator<HTMLUListElement>({ item: \"[data-nav-item]\", active: \"[data-nav-item][data-active]\", follow: true });\n return (\n <Nav.Root aria-label={label} className={s.items({ className })} delayDuration={80} {...props}>\n <Nav.List ref={ref} className={s.list()} {...handlers}>\n {children}\n </Nav.List>\n <Nav.Viewport className={s.viewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"active\"> & {\n /** The current page: `aria-current=\"page\"`, and the light rests on it. */\n active?: boolean;\n};\n\n/** A link of the bar. `asChild` renders your router's `<Link>`. */\nexport function NavBarLink({ active, className, ...props }: NavBarLinkProps) {\n return (\n <Nav.Item>\n <Nav.Link data-nav-item=\"\" data-active={active ? \"\" : undefined} active={active} aria-current={active ? \"page\" : undefined} className={s.item({ className })} {...props} />\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuProps = Omit<React.ComponentProps<typeof Nav.Item>, \"children\"> & {\n /** The trigger's words (“Products”) — passed translated. */\n label: string;\n /** The current page is inside this menu: the light rests on its trigger. */\n active?: boolean;\n /** The menu's links (`NavBar.MenuLink`), laid out in a panel; `columns` sets the grid. */\n children: React.ReactNode;\n /**\n * Columns of the panel.\n * @default 1\n */\n columns?: 1 | 2;\n};\n\n/** A menu of the bar: its trigger, and its links in a panel that opens under the bar. */\nexport function NavBarMenu({ label, active, columns = 1, children, className, ...props }: NavBarMenuProps) {\n return (\n <Nav.Item className={className} {...props}>\n <Nav.Trigger data-nav-item=\"\" data-active={active ? \"\" : undefined} className={cn(s.item(), \"group/trigger\")}>\n {label}\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content className={cn(s.panel(), columns === 2 && \"md:grid-cols-2\")}>{children}</Nav.Content>\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"title\" | \"active\"> & {\n /** The link's name — passed translated. */\n title: string;\n /** One line under it — passed translated. */\n description?: string;\n /** An icon (a signature icon of `@krizaka/icons`: its node lights up on hover). */\n icon?: React.ReactNode;\n /** The current page. */\n active?: boolean;\n};\n\n/** A link inside a menu: an icon, a name and a line of text. */\nexport function NavBarMenuLink({ title, description, icon, active, href, className, asChild, children, ...props }: NavBarMenuLinkProps) {\n const body = (\n <>\n {icon && <span className={s.menuIcon()}>{icon}</span>}\n <span className=\"min-w-0\">\n <span className={s.menuTitle()}>{title}</span>\n {description && <span className={s.menuText()}>{description}</span>}\n </span>\n </>\n );\n return (\n <Nav.Link asChild={asChild} href={asChild ? undefined : href} active={active} aria-current={active ? \"page\" : undefined} data-active={active ? \"\" : undefined} className={s.menuLink({ className })} {...props}>\n {asChild && React.isValidElement<{ children?: React.ReactNode }>(children) ? React.cloneElement(children, undefined, body) : body}\n </Nav.Link>\n );\n}\n\n/** The right side: search, theme, language, the call to action. */\nexport function NavBarActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.actions({ className })} {...props} />;\n}\n\nexport type NavBarMobileProps = React.ComponentProps<\"nav\"> & {\n /** The toggle's name when closed (“Open the menu”) — passed translated. */\n openLabel: string;\n /** The toggle's name when open (“Close the menu”) — passed translated. */\n closeLabel: string;\n};\n\n/**\n * Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape and a click\n * on a link close it, the focus returns to the toggle.\n */\nexport function NavBarMobile({ openLabel, closeLabel, className, children, ...props }: NavBarMobileProps) {\n const { label, open, setOpen, panelId } = useNavBar();\n const toggle = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n setOpen(false);\n toggle.current?.focus();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, setOpen]);\n return (\n <>\n <button ref={toggle} type=\"button\" aria-expanded={open} aria-controls={panelId} aria-label={open ? closeLabel : openLabel} onClick={() => setOpen(!open)} className={s.toggle()}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4.5 w-4.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n {open ? <path d=\"M18 6 6 18M6 6l12 12\" /> : <path d=\"M4 7h16M4 12h16M4 17h10\" />}\n </svg>\n </button>\n {open && (\n <nav\n id={panelId}\n aria-label={label}\n className={s.mobile({ className })}\n onClick={(e) => {\n if ((e.target as Element).closest(\"a\")) setOpen(false);\n }}\n {...props}\n >\n <MobileContext.Provider value>{children}</MobileContext.Provider>\n </nav>\n )}\n </>\n );\n}\n\nexport type NavBarMobileLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the mobile panel. */\nexport function NavBarMobileLink({ active, asChild, className, ...props }: NavBarMobileLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.mobileLink({ className })} {...props} />;\n}\n\n\nconst tagTone = {\n upcoming: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40\",\n construction: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40\",\n development: \"bg-surface-3 text-fg ring-1 ring-inset ring-border-strong\",\n current: \"bg-surface-3 text-fg-secondary\",\n};\n\n/** A site's name and its tags: status when not released, “You are here” on the current one. */\nfunction FamilyName({ site, current, labels }: { site: Site; current: boolean; labels: FamilyLabels }) {\n return (\n <span className={s.familyName()}>\n {site.name}\n {site.status !== \"released\" && <span className={cn(s.familyTag(), tagTone[site.status])}>{labels.status[site.status]}</span>}\n {current && <span className={cn(s.familyTag(), tagTone.current)}>{labels.current}</span>}\n </span>\n );\n}\n\nexport type NavBarFamilyProps = {\n /** The site this bar belongs to (`@krizaka/ui/sites`): marked in the family, `aria-current=\"true\"`. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** Opens the panel at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the switcher (or, inside `NavBar.Mobile`, of the family's group). */\n className?: string;\n};\n\n/**\n * The family switcher, attached to the brand (place it right after `NavBar.Brand`): a chevron that opens every site of\n * Krizaka grouped — Krizaka · Products · Open source · GitHub — the current one marked. It opens on click, Enter, Space\n * or ↓ (never on hover); ↑ ↓ walk the sites, Escape closes and returns the focus. Hidden below `md`: place a second\n * `NavBar.Family` as the last child of `NavBar.Mobile`, where it renders as the panel's last group.\n */\nexport function NavBarFamily({ current, labels = familyLabels.en, defaultOpen, className }: NavBarFamilyProps) {\n const inMobile = React.useContext(MobileContext);\n const id = React.useId();\n const groups = sitesByGroup();\n if (inMobile) {\n return (\n <div role=\"group\" aria-labelledby={`${id}-family`} className={s.mobileFamily({ className })}>\n <p id={`${id}-family`} className={s.familyGroupTitle()}>\n {labels.family}\n </p>\n {groups.map(({ group, sites }) => (\n <div key={group}>\n {/* The company's own group is named like the family itself: its heading would repeat the panel's. */}\n {group !== \"krizaka\" && (\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n )}\n <ul aria-labelledby={group !== \"krizaka\" ? `${id}-${group}` : `${id}-family`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <a href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.mobileFamilyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={24} />\n </span>\n <FamilyName site={site} current={site.id === current} labels={labels} />\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n }\n // Click only: a pointer passing over the brand never opens the family (Radix opens its menus on hover by default).\n const noHover = (e: React.PointerEvent) => e.preventDefault();\n const columns = [groups.filter((g) => g.group === \"krizaka\" || g.group === \"products\"), groups.filter((g) => g.group === \"open-source\" || g.group === \"github\")];\n return (\n <Nav.Root aria-label={labels.family} defaultValue={defaultOpen ? \"family\" : undefined} className={s.family({ className })}>\n <Nav.List className=\"flex\">\n <Nav.Item value=\"family\">\n <Nav.Trigger aria-label={labels.family} onPointerMove={noHover} onPointerLeave={noHover} className={s.familyTrigger()}>\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content onPointerLeave={noHover} className={s.familyPanel()}>\n {columns.map((column, i) => (\n <div key={i} className=\"grid content-start gap-3\">\n {column.map(({ group, sites }) => (\n <div key={group}>\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <Nav.Link href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.familyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={28} />\n </span>\n <span className=\"min-w-0\">\n <FamilyName site={site} current={site.id === current} labels={labels} />\n <span className={s.familyText()}>{labels.descriptions[site.id]}</span>\n </span>\n </Nav.Link>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n ))}\n </Nav.Content>\n </Nav.Item>\n </Nav.List>\n <Nav.Viewport onPointerLeave={noHover} className={s.familyViewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarBadgeProps = React.ComponentProps<\"span\"> & {\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n};\n\n/** A small mono badge beside the brand: the environment and the version (`DEV · v1.2.3`). Hidden below `sm`. */\nexport function NavBarBadge({ env, version, className, children, ...props }: NavBarBadgeProps) {\n const parts = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean);\n return (\n <span className={s.badge({ className })} {...props}>\n {parts.join(\" · \")}\n {children}\n </span>\n );\n}\n\nexport type NavBarRepoProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The repository the site is built from. */\n href: string;\n /** The link's accessible name (“Source on GitHub”) — passed translated. */\n label: string;\n};\n\n/** The site's repository on GitHub, an icon link of `NavBar.Actions`. */\nexport function NavBarRepo({ label, className, ...props }: NavBarRepoProps) {\n return (\n <a aria-label={label} className={s.repo({ className })} {...props}>\n <GitHubGlyph size={18} />\n </a>\n );\n}\n\n/**\n * `NavBar.Root/Brand/Family/Badge/Items/Link/Menu/MenuLink/Actions/Repo/Mobile/MobileLink` — the Krizaka navigation bar,\n * the same on every site of the family: floating, tightening on scroll, the family switcher on the brand, one light\n * gliding between its items, accessible menus, a mobile panel.\n */\nexport const NavBar = {\n Root: NavBarRoot,\n Brand: NavBarBrand,\n Family: NavBarFamily,\n Badge: NavBarBadge,\n Items: NavBarItems,\n Link: NavBarLink,\n Menu: NavBarMenu,\n MenuLink: NavBarMenuLink,\n Actions: NavBarActions,\n Repo: NavBarRepo,\n Mobile: NavBarMobile,\n MobileLink: NavBarMobileLink,\n};\n"
|
|
523
|
+
"content": "// Client (scroll, a sliding indicator, menus). Radix NavigationMenu does the menus' semantics and keyboard (Enter, Space\n// and ↓ open; ↑ ↓ Home End walk; Escape closes and returns the focus; Tab out closes); this file draws the Krizaka bar:\n// a floating pill that tightens once the page scrolls, one light that glides to the item under the pointer or the\n// focus and rests on the current page, and a mobile panel that is a plain disclosure (no focus trap, Escape closes).\nimport { NavigationMenu as Nav, Slot } from \"radix-ui\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { cn } from \"../cn\";\nimport { FamilyMark, GitHubGlyph } from \"../family-mark\";\nimport { type FamilyLabels, familyLabels, type Site, type SiteId, sitesByGroup } from \"../sites/sites\";\nimport { useSlidingIndicator } from \"../sliding-indicator\";\n\nexport const navBar = tv({\n slots: {\n root:\n \"group/nav relative z-40 mx-auto flex h-14 w-full items-center gap-2 border-border-subtle bg-surface-1/75 backdrop-blur-xl backdrop-saturate-150 \" +\n \"transition-[height,background-color,border-color,box-shadow] duration-300 motion-reduce:transition-none \" +\n \"data-compact:h-12 data-compact:border-border-default data-compact:bg-surface-1/90 data-compact:shadow-lg\",\n brand:\n \"mr-2 inline-flex shrink-0 items-center gap-2 font-display text-[15px] font-bold tracking-tight text-fg no-underline \" +\n \"rounded-md focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n items: \"relative hidden md:block\",\n list:\n \"group/list relative isolate flex items-center gap-0.5 \" +\n // The light: under the item that is pointed at or focused, resting on the current page.\n \"before:absolute before:left-0 before:top-0 before:-z-10 before:h-(--kz-ind-h) before:w-(--kz-ind-w) before:translate-x-(--kz-ind-x) before:translate-y-(--kz-ind-y) \" +\n \"before:rounded-full before:bg-surface-3 before:opacity-0 before:transition-[translate,width,opacity] before:duration-300 motion-reduce:before:transition-none \" +\n \"data-[indicator=on]:before:opacity-100\",\n item:\n \"inline-flex h-9 select-none items-center gap-1 rounded-full px-3.5 text-sm font-medium text-fg-secondary no-underline transition-colors \" +\n \"hover:text-fg data-active:text-fg data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n // Before the light is ready (server render), the current page carries its own tint.\n \"group-not-data-[indicator=on]/list:data-active:bg-surface-3\",\n chevron: \"h-3.5 w-3.5 transition-transform duration-300 group-data-[state=open]/trigger:rotate-180 motion-reduce:transition-none\",\n viewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n // Compact: icon · title · one line. A sensible width (20rem, 36rem in two columns) — never a sheet over the page.\n panel: \"grid w-[min(92vw,20rem)] gap-0.5 p-1.5\",\n panelWide: \"w-[min(92vw,36rem)] md:grid-cols-2\",\n menuLink:\n \"group/link flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-active:bg-accent-soft\",\n menuIcon:\n \"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-border-default bg-surface-2 text-fg-accent \" +\n \"transition-[transform,border-color] duration-300 group-hover/link:-translate-y-0.5 group-hover/link:border-accent [&>svg]:h-4 [&>svg]:w-4 motion-reduce:transform-none\",\n menuTitle: \"block truncate text-sm font-semibold leading-5 text-fg\",\n menuText: \"block truncate text-xs leading-4 text-fg-secondary\",\n actions: \"ml-auto flex items-center gap-2\",\n toggle:\n \"inline-flex h-9 w-9 items-center justify-center rounded-full border border-border-subtle bg-surface-2/60 text-fg md:hidden \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n mobile: \"kz-pop absolute inset-x-0 top-full mt-2 grid gap-1 rounded-2xl border border-border-default bg-surface-1 p-2 shadow-lg md:hidden\",\n mobileGroup: \"grid gap-0.5 [&+&]:mt-1 [&+&]:border-t [&+&]:border-border-subtle [&+&]:pt-2\",\n mobileGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n mobileLink:\n \"flex items-center justify-between rounded-xl px-3 py-3 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:text-fg-accent\",\n // The family switcher: a chevron attached to the brand, its panel aligned on the bar's left edge.\n family: \"hidden md:block\",\n familyTrigger:\n \"group/trigger -ml-2 mr-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg data-[state=open]:bg-surface-3 data-[state=open]:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n familyViewport:\n \"kz-pop absolute left-0 top-full mt-2 h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width) origin-top-left overflow-hidden \" +\n \"rounded-2xl border border-border-default bg-surface-1 shadow-lg transition-[width,height] duration-300 motion-reduce:transition-none\",\n familyPanel: \"grid w-[min(92vw,42rem)] gap-x-2 gap-y-3 p-3 sm:grid-cols-2\",\n familyGroupTitle: \"px-3 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n familyLink:\n \"group/link flex items-start gap-3 rounded-xl px-3 py-2.5 text-left no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n familyMark: \"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center text-fg\",\n familyName: \"flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm font-semibold text-fg\",\n familyText: \"mt-0.5 block text-xs leading-relaxed text-fg-secondary\",\n familyTag: \"rounded-full px-1.5 py-px text-[10px] font-semibold uppercase tracking-wider\",\n mobileFamily: \"mt-1 grid gap-2 border-t border-border-subtle px-1 pb-1 pt-3\",\n mobileFamilyLink:\n \"flex min-h-11 items-center gap-3 rounded-xl px-2 py-2 text-[15px] font-medium text-fg no-underline hover:bg-surface-2 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring data-current:bg-accent-soft\",\n badge:\n \"hidden h-6 shrink-0 items-center gap-1 rounded-full border border-border-subtle bg-surface-2 px-2 font-mono text-[11px] font-medium text-fg-secondary sm:inline-flex\",\n repo:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n },\n variants: {\n floating: {\n true: { root: \"max-w-5xl rounded-full border pl-5 pr-2 data-compact:max-w-4xl\" },\n false: { root: \"border-b px-4 md:px-6\" },\n },\n },\n defaultVariants: { floating: true },\n});\nconst s = navBar();\n\ninterface NavBarContext {\n label: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n panelId: string;\n}\nconst Context = React.createContext<NavBarContext | null>(null);\n/** The open menu of `NavBar.Items` (`\"\"`: none), so a plain link can close it when the pointer moves on to it. */\nconst ItemsContext = React.createContext<{ close: () => void }>({ close: () => {} });\n/** True inside `NavBar.Mobile`: the family renders as the panel's last group instead of a switcher. */\nconst MobileContext = React.createContext(false);\nconst useNavBar = () => {\n const ctx = React.useContext(Context);\n if (!ctx) throw new Error(\"NavBar parts must be inside <NavBar.Root>.\");\n return ctx;\n};\n\nexport type NavBarRootProps = React.ComponentProps<\"header\"> & {\n /** The accessible name of the navigation (“Main”) — passed translated. */\n label: string;\n /**\n * Pixels of scroll after which the bar tightens (denser, lower, lifted). `false`: never.\n * @default 24\n */\n compactAfter?: number | false;\n /**\n * A floating pill (the Krizaka bar) or a full-width bar with a hairline under it.\n * @default true\n */\n floating?: boolean;\n};\n\n/** The bar. Place it in a `sticky top-3` (floating) or `sticky top-0` wrapper — or let it scroll. */\nexport function NavBarRoot({ label, compactAfter = 24, floating = true, className, children, ...props }: NavBarRootProps) {\n const [compact, setCompact] = React.useState(false);\n const [open, setOpen] = React.useState(false);\n const panelId = React.useId();\n React.useEffect(() => {\n if (compactAfter === false) return;\n const onScroll = () => setCompact(window.scrollY > compactAfter);\n onScroll();\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n return () => window.removeEventListener(\"scroll\", onScroll);\n }, [compactAfter]);\n const ctx = React.useMemo(() => ({ label, open, setOpen, panelId }), [label, open, panelId]);\n return (\n <Context.Provider value={ctx}>\n <header data-compact={compact ? \"\" : undefined} data-floating={floating} className={navBar({ floating }).root({ className })} {...props}>\n {children}\n </header>\n </Context.Provider>\n );\n}\n\nexport type NavBarBrandProps = React.ComponentProps<\"a\"> & {\n /** Renders your link (a router `<Link>`) with the brand's look. */\n asChild?: boolean;\n};\n\n/** The brand: a mark and a name, linking home. */\nexport function NavBarBrand({ asChild, className, ...props }: NavBarBrandProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp className={s.brand({ className })} {...props} />;\n}\n\nexport type NavBarItemsProps = Omit<React.ComponentProps<typeof Nav.Root>, \"orientation\">;\n\n/**\n * The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n * `NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n * `delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\n * moving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.\n */\nexport function NavBarItems({ className, children, value: valueProp, defaultValue, onValueChange, delayDuration = 200, ...props }: NavBarItemsProps) {\n const { label } = useNavBar();\n const [inner, setInner] = React.useState(defaultValue ?? \"\");\n const value = valueProp ?? inner;\n const setValue = React.useCallback(\n (next: string) => {\n setInner(next);\n onValueChange?.(next);\n },\n [onValueChange],\n );\n const items = React.useMemo(() => ({ close: () => setValue(\"\") }), [setValue]);\n const { ref, handlers } = useSlidingIndicator<HTMLUListElement>({\n item: \"[data-nav-item]\",\n active: \"[data-nav-item][data-active]\",\n prefer: \"[data-nav-item][data-state=open]\",\n follow: true,\n });\n return (\n <ItemsContext.Provider value={items}>\n <Nav.Root aria-label={label} className={s.items({ className })} value={value} onValueChange={setValue} delayDuration={delayDuration} {...props}>\n <Nav.List ref={ref} className={s.list()} {...handlers}>\n {children}\n </Nav.List>\n <Nav.Viewport className={s.viewport()} />\n </Nav.Root>\n </ItemsContext.Provider>\n );\n}\n\nexport type NavBarLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"active\"> & {\n /** The current page: `aria-current=\"page\"`, and the light rests on it. */\n active?: boolean;\n};\n\n/** A link of the bar. `asChild` renders your router's `<Link>`. */\nexport function NavBarLink({ active, className, onPointerEnter, ...props }: NavBarLinkProps) {\n const { close } = React.useContext(ItemsContext);\n return (\n <Nav.Item>\n <Nav.Link\n data-nav-item=\"\"\n data-active={active ? \"\" : undefined}\n active={active}\n aria-current={active ? \"page\" : undefined}\n className={s.item({ className })}\n onPointerEnter={(e) => {\n onPointerEnter?.(e);\n close();\n }}\n {...props}\n />\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuProps = Omit<React.ComponentProps<typeof Nav.Item>, \"children\"> & {\n /** The trigger's words (“Products”) — passed translated. */\n label: string;\n /** The current page is inside this menu: the light rests on its trigger. */\n active?: boolean;\n /** The menu's links (`NavBar.MenuLink`), laid out in a panel; `columns` sets the grid. */\n children: React.ReactNode;\n /**\n * Columns of the panel.\n * @default 1\n */\n columns?: 1 | 2;\n};\n\n/** A menu of the bar: its trigger, and its links in a panel that opens under the bar. */\nexport function NavBarMenu({ label, active, columns = 1, children, className, ...props }: NavBarMenuProps) {\n return (\n <Nav.Item className={className} {...props}>\n <Nav.Trigger data-nav-item=\"\" data-active={active ? \"\" : undefined} className={cn(s.item(), \"group/trigger\")}>\n {label}\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content className={cn(s.panel(), columns === 2 && s.panelWide())}>{children}</Nav.Content>\n </Nav.Item>\n );\n}\n\nexport type NavBarMenuLinkProps = Omit<React.ComponentProps<typeof Nav.Link>, \"title\" | \"active\"> & {\n /** The link's name — passed translated. */\n title: string;\n /** One line under it — passed translated. */\n description?: string;\n /** An icon (a signature icon of `@krizaka/icons`: its node lights up on hover). */\n icon?: React.ReactNode;\n /** The current page. */\n active?: boolean;\n};\n\n/** A link inside a menu: an icon, a name and a line of text. */\nexport function NavBarMenuLink({ title, description, icon, active, href, className, asChild, children, ...props }: NavBarMenuLinkProps) {\n const body = (\n <>\n {icon && <span className={s.menuIcon()}>{icon}</span>}\n <span className=\"min-w-0\">\n <span className={s.menuTitle()}>{title}</span>\n {description && <span className={s.menuText()}>{description}</span>}\n </span>\n </>\n );\n return (\n <Nav.Link asChild={asChild} href={asChild ? undefined : href} active={active} aria-current={active ? \"page\" : undefined} data-active={active ? \"\" : undefined} className={s.menuLink({ className })} {...props}>\n {asChild && React.isValidElement<{ children?: React.ReactNode }>(children) ? React.cloneElement(children, undefined, body) : body}\n </Nav.Link>\n );\n}\n\n/** The right side: search, theme, language, the call to action. */\nexport function NavBarActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div className={s.actions({ className })} {...props} />;\n}\n\nexport type NavBarMobileProps = React.ComponentProps<\"nav\"> & {\n /** The toggle's name when closed (“Open the menu”) — passed translated. */\n openLabel: string;\n /** The toggle's name when open (“Close the menu”) — passed translated. */\n closeLabel: string;\n};\n\n/**\n * Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n * (`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\n * to the toggle.\n */\nexport function NavBarMobile({ openLabel, closeLabel, className, children, ...props }: NavBarMobileProps) {\n const { label, open, setOpen, panelId } = useNavBar();\n const toggle = React.useRef<HTMLButtonElement>(null);\n React.useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n setOpen(false);\n toggle.current?.focus();\n };\n // A press outside the panel and its toggle closes it (the page under it stays usable).\n const onPress = (e: PointerEvent) => {\n const target = e.target as Node | null;\n if (!target || toggle.current?.contains(target) || document.getElementById(panelId)?.contains(target)) return;\n setOpen(false);\n };\n document.addEventListener(\"keydown\", onKey);\n document.addEventListener(\"pointerdown\", onPress);\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.removeEventListener(\"pointerdown\", onPress);\n };\n }, [open, setOpen, panelId]);\n return (\n <>\n <button ref={toggle} type=\"button\" aria-expanded={open} aria-controls={panelId} aria-label={open ? closeLabel : openLabel} onClick={() => setOpen(!open)} className={s.toggle()}>\n <svg viewBox=\"0 0 24 24\" className=\"h-4.5 w-4.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n {open ? <path d=\"M18 6 6 18M6 6l12 12\" /> : <path d=\"M4 7h16M4 12h16M4 17h10\" />}\n </svg>\n </button>\n {open && (\n <nav\n id={panelId}\n aria-label={label}\n className={s.mobile({ className })}\n onClick={(e) => {\n if ((e.target as Element).closest(\"a\")) setOpen(false);\n }}\n {...props}\n >\n <MobileContext.Provider value>{children}</MobileContext.Provider>\n </nav>\n )}\n </>\n );\n}\n\nexport type NavBarMobileGroupProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n /** The group's heading — the same words as the bar's menu (“Product”) — passed translated. */\n label: string;\n};\n\n/** A group of the mobile panel: the links of one of the bar's menus under its name, in the same order. */\nexport function NavBarMobileGroup({ label, className, children, ...props }: NavBarMobileGroupProps) {\n const id = React.useId();\n return (\n <div role=\"group\" aria-labelledby={id} className={s.mobileGroup({ className })} {...props}>\n <p id={id} className={s.mobileGroupTitle()}>\n {label}\n </p>\n {children}\n </div>\n );\n}\n\nexport type NavBarMobileLinkProps = React.ComponentProps<\"a\"> & {\n /** The current page. */\n active?: boolean;\n /** Renders your router's `<Link>`. */\n asChild?: boolean;\n};\n\n/** A link of the mobile panel. */\nexport function NavBarMobileLink({ active, asChild, className, ...props }: NavBarMobileLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n return <Comp aria-current={active ? \"page\" : undefined} className={s.mobileLink({ className })} {...props} />;\n}\n\n\nconst tagTone = {\n upcoming: \"bg-accent-soft text-fg ring-1 ring-inset ring-accent/40\",\n construction: \"bg-warning/15 text-fg ring-1 ring-inset ring-warning/40\",\n development: \"bg-surface-3 text-fg ring-1 ring-inset ring-border-strong\",\n current: \"bg-surface-3 text-fg-secondary\",\n};\n\n/** A site's name and its tags: status when not released, “You are here” on the current one. */\nfunction FamilyName({ site, current, labels }: { site: Site; current: boolean; labels: FamilyLabels }) {\n return (\n <span className={s.familyName()}>\n {site.name}\n {site.status !== \"released\" && <span className={cn(s.familyTag(), tagTone[site.status])}>{labels.status[site.status]}</span>}\n {current && <span className={cn(s.familyTag(), tagTone.current)}>{labels.current}</span>}\n </span>\n );\n}\n\nexport type NavBarFamilyProps = {\n /** The site this bar belongs to (`@krizaka/ui/sites`): marked in the family, `aria-current=\"true\"`. */\n current: SiteId;\n /**\n * The family's words, passed translated: `familyLabels.fr` from `@krizaka/ui/sites`, or your own of the same shape.\n * @default familyLabels.en\n */\n labels?: FamilyLabels;\n /** Opens the panel at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the switcher (or, inside `NavBar.Mobile`, of the family's group). */\n className?: string;\n};\n\n/**\n * The family switcher, attached to the brand (place it right after `NavBar.Brand`): a chevron that opens every site of\n * Krizaka grouped — Krizaka · Products · Open source · GitHub — the current one marked. It opens on click, Enter, Space\n * or ↓ (never on hover); ↑ ↓ walk the sites, Escape closes and returns the focus. Hidden below `md`: place a second\n * `NavBar.Family` as the last child of `NavBar.Mobile`, where it renders as the panel's last group.\n */\nexport function NavBarFamily({ current, labels = familyLabels.en, defaultOpen, className }: NavBarFamilyProps) {\n const inMobile = React.useContext(MobileContext);\n const id = React.useId();\n const groups = sitesByGroup();\n if (inMobile) {\n return (\n <div role=\"group\" aria-labelledby={`${id}-family`} className={s.mobileFamily({ className })}>\n <p id={`${id}-family`} className={s.familyGroupTitle()}>\n {labels.family}\n </p>\n {groups.map(({ group, sites }) => (\n <div key={group}>\n {/* The company's own group is named like the family itself: its heading would repeat the panel's. */}\n {group !== \"krizaka\" && (\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n )}\n <ul aria-labelledby={group !== \"krizaka\" ? `${id}-${group}` : `${id}-family`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <a href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.mobileFamilyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={24} />\n </span>\n <FamilyName site={site} current={site.id === current} labels={labels} />\n </a>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n );\n }\n // Click only: a pointer passing over the brand never opens the family (Radix opens its menus on hover by default).\n const noHover = (e: React.PointerEvent) => e.preventDefault();\n const columns = [groups.filter((g) => g.group === \"krizaka\" || g.group === \"products\"), groups.filter((g) => g.group === \"open-source\" || g.group === \"github\")];\n return (\n <Nav.Root aria-label={labels.family} defaultValue={defaultOpen ? \"family\" : undefined} className={s.family({ className })}>\n <Nav.List className=\"flex\">\n <Nav.Item value=\"family\">\n <Nav.Trigger aria-label={labels.family} onPointerMove={noHover} onPointerLeave={noHover} className={s.familyTrigger()}>\n <svg viewBox=\"0 0 24 24\" className={s.chevron()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </Nav.Trigger>\n <Nav.Content onPointerLeave={noHover} className={s.familyPanel()}>\n {columns.map((column, i) => (\n <div key={i} className=\"grid content-start gap-3\">\n {column.map(({ group, sites }) => (\n <div key={group}>\n <p id={`${id}-${group}`} className={s.familyGroupTitle()}>\n {labels.groups[group]}\n </p>\n <ul aria-labelledby={`${id}-${group}`} className=\"grid gap-0.5\">\n {sites.map((site) => (\n <li key={site.id}>\n <Nav.Link href={site.url} data-current={site.id === current ? \"\" : undefined} aria-current={site.id === current ? \"true\" : undefined} className={s.familyLink()}>\n <span className={s.familyMark()}>\n <FamilyMark mark={site.mark} size={28} />\n </span>\n <span className=\"min-w-0\">\n <FamilyName site={site} current={site.id === current} labels={labels} />\n <span className={s.familyText()}>{labels.descriptions[site.id]}</span>\n </span>\n </Nav.Link>\n </li>\n ))}\n </ul>\n </div>\n ))}\n </div>\n ))}\n </Nav.Content>\n </Nav.Item>\n </Nav.List>\n <Nav.Viewport onPointerLeave={noHover} className={s.familyViewport()} />\n </Nav.Root>\n );\n}\n\nexport type NavBarBadgeProps = React.ComponentProps<\"span\"> & {\n /** The environment, when it is not production (`DEV`). */\n env?: string;\n /** The version the site runs (`1.2.3`, shown `v1.2.3`). */\n version?: string;\n};\n\n/** A small mono badge beside the brand: the environment and the version (`DEV · v1.2.3`). Hidden below `sm`. */\nexport function NavBarBadge({ env, version, className, children, ...props }: NavBarBadgeProps) {\n const parts = [env, version && `v${version.replace(/^v/, \"\")}`].filter(Boolean);\n return (\n <span className={s.badge({ className })} {...props}>\n {parts.join(\" · \")}\n {children}\n </span>\n );\n}\n\nexport type NavBarRepoProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n /** The repository the site is built from. */\n href: string;\n /** The link's accessible name (“Source on GitHub”) — passed translated. */\n label: string;\n};\n\n/** The site's repository on GitHub, an icon link of `NavBar.Actions`. */\nexport function NavBarRepo({ label, className, ...props }: NavBarRepoProps) {\n return (\n <a aria-label={label} className={s.repo({ className })} {...props}>\n <GitHubGlyph size={18} />\n </a>\n );\n}\n\n/**\n * `NavBar.Root/Brand/Family/Badge/Items/Link/Menu/MenuLink/Actions/Repo/Mobile/MobileGroup/MobileLink` — the Krizaka navigation bar,\n * the same on every site of the family: floating, tightening on scroll, the family switcher on the brand, one light\n * gliding between its items, accessible menus, a mobile panel.\n */\nexport const NavBar = {\n Root: NavBarRoot,\n Brand: NavBarBrand,\n Family: NavBarFamily,\n Badge: NavBarBadge,\n Items: NavBarItems,\n Link: NavBarLink,\n Menu: NavBarMenu,\n MenuLink: NavBarMenuLink,\n Actions: NavBarActions,\n Repo: NavBarRepo,\n Mobile: NavBarMobile,\n MobileGroup: NavBarMobileGroup,\n MobileLink: NavBarMobileLink,\n};\n"
|
|
504
524
|
},
|
|
505
525
|
{
|
|
506
526
|
"path": "sliding-indicator.ts",
|
|
507
|
-
"content": "// The shared sliding indicator (NavBar, Tabs): one element that glides from item to item instead of each item lighting\n// up on its own. It writes the target's box as CSS variables on the list (`--kz-ind-x/y/w/h`) and `data-indicator=\"on\"`\n// — no re-render, no animation frame: the glide is a CSS transition (none under reduced motion). Before hydration the\n// items keep their own active style, so nothing flashes.\nimport * as React from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\nexport function useSlidingIndicator<T extends HTMLElement>({ item, active, follow = false }: { item: string; active: string; follow?: boolean }) {\n const ref = React.useRef<T>(null);\n const place = React.useCallback((target: Element | null) => {\n const list = ref.current;\n if (!list) return;\n if (!(target instanceof HTMLElement) || !list.contains(target)) {\n list.removeAttribute(\"data-indicator\");\n return;\n }\n const a = target.getBoundingClientRect();\n const b = list.getBoundingClientRect();\n list.style.setProperty(\"--kz-ind-x\", `${a.left - b.left + list.scrollLeft}px`);\n list.style.setProperty(\"--kz-ind-y\", `${a.top - b.top + list.scrollTop}px`);\n list.style.setProperty(\"--kz-ind-w\", `${a.width}px`);\n list.style.setProperty(\"--kz-ind-h\", `${a.height}px`);\n list.setAttribute(\"data-indicator\", \"on\");\n }, []);\n const rest = React.useCallback(() =>
|
|
527
|
+
"content": "// The shared sliding indicator (NavBar, Tabs): one element that glides from item to item instead of each item lighting\n// up on its own. It writes the target's box as CSS variables on the list (`--kz-ind-x/y/w/h`) and `data-indicator=\"on\"`\n// — no re-render, no animation frame: the glide is a CSS transition (none under reduced motion). Before hydration the\n// items keep their own active style, so nothing flashes.\nimport * as React from \"react\";\n\nconst useIsoLayoutEffect = typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect;\n\n/**\n * `item`: the selector of an item; `active`: of the current one, where the light rests; `prefer`: an item that, when\n * present, holds the light before the current one (the NavBar's open menu); `follow`: the light follows the pointer\n * and the focus — over anything that is not an item (an open menu's panel) it goes back to rest.\n */\nexport function useSlidingIndicator<T extends HTMLElement>({ item, active, prefer, follow = false }: { item: string; active: string; prefer?: string; follow?: boolean }) {\n const ref = React.useRef<T>(null);\n const place = React.useCallback((target: Element | null) => {\n const list = ref.current;\n if (!list) return;\n if (!(target instanceof HTMLElement) || !list.contains(target)) {\n list.removeAttribute(\"data-indicator\");\n return;\n }\n const a = target.getBoundingClientRect();\n const b = list.getBoundingClientRect();\n list.style.setProperty(\"--kz-ind-x\", `${a.left - b.left + list.scrollLeft}px`);\n list.style.setProperty(\"--kz-ind-y\", `${a.top - b.top + list.scrollTop}px`);\n list.style.setProperty(\"--kz-ind-w\", `${a.width}px`);\n list.style.setProperty(\"--kz-ind-h\", `${a.height}px`);\n list.setAttribute(\"data-indicator\", \"on\");\n }, []);\n const rest = React.useCallback(\n () => {\n const list = ref.current;\n // An item re-rendered under a resting pointer (a menu closing) keeps the light the pointer gave it.\n const hovered = follow ? list?.querySelector(`${item}:hover`) : null;\n place((prefer ? list?.querySelector(prefer) : null) ?? hovered ?? list?.querySelector(active) ?? null);\n },\n [place, active, prefer, item, follow],\n );\n const follow_ = React.useCallback((target: EventTarget) => {\n const hit = target instanceof Element ? target.closest(item) : null;\n if (hit && ref.current?.contains(hit)) place(hit);\n else rest();\n }, [item, place, rest]);\n\n useIsoLayoutEffect(() => {\n const list = ref.current;\n if (!list) return;\n rest();\n const resize = new ResizeObserver(rest);\n resize.observe(list);\n const changes = new MutationObserver(rest);\n changes.observe(list, { subtree: true, attributes: true, attributeFilter: [\"data-state\", \"aria-current\", \"data-active\"] });\n return () => {\n resize.disconnect();\n changes.disconnect();\n };\n }, [rest]);\n\n const handlers = follow\n ? {\n onPointerOver: (e: React.PointerEvent) => follow_(e.target),\n onPointerLeave: rest,\n onFocus: (e: React.FocusEvent) => follow_(e.target),\n onBlur: rest,\n }\n : {};\n return { ref, handlers };\n}\n"
|
|
508
528
|
}
|
|
509
529
|
],
|
|
510
530
|
"dependencies": [
|
|
511
|
-
"@krizaka/tokens@^3.
|
|
531
|
+
"@krizaka/tokens@^3.21.0",
|
|
512
532
|
"radix-ui@^1.7.0",
|
|
513
533
|
"tailwind-variants@^3.3.1"
|
|
514
534
|
],
|
|
@@ -617,7 +637,7 @@
|
|
|
617
637
|
},
|
|
618
638
|
{
|
|
619
639
|
"component": "NavBarItems",
|
|
620
|
-
"description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see
|
|
640
|
+
"description": "The desktop navigation: links and menus, with the light that glides between them. Hidden below `md` (see\n`NavBar.Mobile`). A menu opens on click, Enter, Space or ↓ — or when the pointer rests on its trigger (hover intent,\n`delayDuration`, 200 ms): passing over the bar opens nothing. While a menu is open the light stays on its trigger;\nmoving on to a plain link closes the menu, so the bar never shows two current items. A click outside or Escape closes.",
|
|
621
641
|
"props": []
|
|
622
642
|
},
|
|
623
643
|
{
|
|
@@ -721,7 +741,7 @@
|
|
|
721
741
|
},
|
|
722
742
|
{
|
|
723
743
|
"component": "NavBarMobile",
|
|
724
|
-
"description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`). A disclosure: Escape
|
|
744
|
+
"description": "Below `md`: a toggle, and a panel under the bar with its links (`NavBar.MobileLink`), grouped like the bar's menus\n(`NavBar.MobileGroup`). A disclosure: Escape, a click on a link or a press outside close it; Escape returns the focus\nto the toggle.",
|
|
725
745
|
"props": [
|
|
726
746
|
{
|
|
727
747
|
"name": "closeLabel",
|
|
@@ -739,6 +759,19 @@
|
|
|
739
759
|
}
|
|
740
760
|
]
|
|
741
761
|
},
|
|
762
|
+
{
|
|
763
|
+
"component": "NavBarMobileGroup",
|
|
764
|
+
"description": "A group of the mobile panel: the links of one of the bar's menus under its name, in the same order.",
|
|
765
|
+
"props": [
|
|
766
|
+
{
|
|
767
|
+
"name": "label",
|
|
768
|
+
"type": "string",
|
|
769
|
+
"default": null,
|
|
770
|
+
"description": "The group's heading — the same words as the bar's menu (“Product”) — passed translated.",
|
|
771
|
+
"required": true
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
},
|
|
742
775
|
{
|
|
743
776
|
"component": "NavBarMobileLink",
|
|
744
777
|
"description": "A link of the mobile panel.",
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
},
|
|
243
243
|
{
|
|
244
244
|
"path": "meta.ts",
|
|
245
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
245
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"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 | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"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"
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
"path": "number-ticker/index.ts",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"files": [
|
|
130
130
|
{
|
|
131
131
|
"path": "meta.ts",
|
|
132
|
-
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"activity-feed\"\n | \"action-sheet\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"header\"\n | \"how-it-works-page\"\n | \"document-viewer\"\n | \"image-cropper\"\n | \"image-viewer\"\n | \"iso\"\n | \"kanban\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"map\"\n | \"markdown\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"splash-handoff\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"step-progress\"\n | \"stepper\"\n | \"swipe-deck\"\n | \"swipeable-row\"\n | \"switch\"\n | \"tab-bar\"\n | \"tabs\"\n | \"tag-input\"\n | \"tag-selector\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"tree-view\"\n | \"txt\"\n | \"verified-badge\"\n | \"vertical-video\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/**\n * Why a component is not on a platform — a decision, never a silence (the parity matrix of ui.krizaka.com/platforms):\n * - `gap` (✗): it belongs there and is not built yet; `issue` is the issue that tracks it (`krizaka/krizaka-ui#275`);\n * - `n/a`: it is not meant for that platform (a site's page block on a phone, a swipe on a desktop); `use` names the\n * component that answers there, when one does.\n */\nexport type Absence =\n | { readonly status: \"gap\"; readonly issue: string; readonly reason: string; readonly use?: undefined }\n | { readonly status: \"n/a\"; readonly reason: string; readonly use?: ComponentName; readonly issue?: undefined };\n\n/**\n * Desktop is the Orazaka Mac app (Tauri 2: the system WebView renders the web components — ADR-002). `web`: the web\n * component runs there as it is; `desktop`: it also has a desktop form (a window's title bar, a denser layout…), said in\n * its documentation; otherwise the absence and why.\n */\nexport type DesktopSupport = \"web\" | \"desktop\" | Absence;\n\n/**\n * The three platforms of Krizaka UI — web · native (React Native, Expo) · desktop — for one component. The side a\n * component has is `platforms`; the side it lacks says why here: `native` is required for (and only for) a web-only\n * component, `web` for a native-only one; `desktop` always. `scripts/registry.test.mjs` fails on a missing decision.\n */\nexport interface Parity {\n readonly web?: Absence;\n readonly native?: Absence;\n readonly desktop: DesktopSupport;\n}\n\n/**\n * `stable`: its API only changes in a major. `beta`: it may still change in a minor. `deprecated`: no product uses it and\n * no work plans to — it still works, says what replaces it, and goes in the major named by `deprecated.removedIn`.\n */\nexport type Status = \"stable\" | \"beta\" | \"deprecated\";\n\n/** What a deprecated component says (required when `status` is `deprecated`, absent otherwise). */\nexport interface Deprecation {\n /** The version that deprecated it (`3.6.0`). */\n readonly since: string;\n /** The major that removes it (`4.0.0`). */\n readonly removedIn: string;\n /** What replaces it, when a component does. */\n readonly use?: ComponentName;\n /** Why it goes: no product uses it, no planned work needs it. */\n readonly reason: string;\n}\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/** Why a reader needs a template: the seven intents the templates are grouped by (sidebar sub-headings, catalogue filter). */\nexport type TemplatePurpose = \"convince\" | \"explain\" | \"show\" | \"act\" | \"operate\" | \"converse\" | \"recover\";\n\n/** The groups of templates, in reading order: the title, and the one line that says when to come to that group. */\nexport const templatePurposes = {\n convince: { title: \"Convince\", useWhen: \"You come here when a visitor must decide: a landing, a product, a price, a comparison, a proof.\" },\n explain: { title: \"Explain\", useWhen: \"You come here when a reader must understand: how it works, a documentation hub or article, a changelog, a FAQ, a policy.\" },\n show: { title: \"Show\", useWhen: \"You come here when the work itself is the argument: a gallery, a story, a case study, an object and its details.\" },\n act: { title: \"Act\", useWhen: \"You come here when a person must do something once: reach the team, sign in or up, set up, pay.\" },\n operate: { title: \"Operate\", useWhen: \"You come here when a person works every day: a dashboard, a console with a table, settings, an app shell.\" },\n converse: { title: \"Converse\", useWhen: \"You come here when people talk: an inbox, a chat, comments, a notifications centre.\" },\n recover: { title: \"Recover\", useWhen: \"You come here when the way is blocked: an empty list, an error, a missing page, maintenance, a product still being built.\" },\n} as const satisfies Record<TemplatePurpose, { readonly title: string; readonly useWhen: string }>;\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\ninterface ExampleBase {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n}\n\n/** A plain example, or a template: a whole screen with its intent (`purpose`) and the user goal its layout serves (`why`). */\nexport type Example =\n | (ExampleBase & { readonly template?: false; readonly purpose?: undefined; readonly why?: undefined })\n | (ExampleBase & {\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its\n * phone version thought out), copied as a starting point — at most 160 lines, listed on ui.krizaka.com/templates.\n */\n readonly template: true;\n /** The intent that groups it: why you need this block. */\n readonly purpose: TemplatePurpose;\n /** Why this layout: the user goal it serves, in one sentence. */\n readonly why: string;\n });\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n /** Required for, and only for, a `deprecated` component. */\n readonly deprecated?: Deprecation;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** The platforms it is not on, and why; and its desktop form (see `Parity`). */\n readonly parity: Parity;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
132
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which ui.krizaka.com (apps/site) renders as\n// ui.krizaka.com/components/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"action-sheet\"\n | \"activity-feed\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"chat\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"description-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"document-viewer\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"fab\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"force-update-screen\"\n | \"form\"\n | \"gallery-page\"\n | \"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 | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"match-dialog\"\n | \"media-capture\"\n | \"media-grid\"\n | \"media-picker\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"onboarding\"\n | \"otp-input\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"permission-prompt\"\n | \"photo-grid\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"pull-to-refresh\"\n | \"radio-group\"\n | \"recipes\"\n | \"screen\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"showcase\"\n | \"site-footer\"\n | \"sites\"\n | \"skeleton\"\n | \"skip-link\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"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"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"path": "page-header/index.ts",
|