@krizaka/ui 3.25.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +2 -1
  2. package/dist/chunk-3S4GHSPZ.js +109 -0
  3. package/dist/chunk-3S4GHSPZ.js.map +1 -0
  4. package/dist/{chunk-FTX66XZY.js → chunk-OUWPBSSL.js} +4 -4
  5. package/dist/{chunk-FTX66XZY.js.map → chunk-OUWPBSSL.js.map} +1 -1
  6. package/dist/{chunk-4DH2HW3D.js → chunk-TFJDIOFK.js} +2 -1
  7. package/dist/document-viewer.js +19 -3
  8. package/dist/document-viewer.js.map +1 -1
  9. package/dist/image-cropper.js +8 -4
  10. package/dist/image-cropper.js.map +1 -1
  11. package/dist/index.d.ts +3 -8
  12. package/dist/index.js +44 -48
  13. package/dist/index.js.map +1 -1
  14. package/dist/language-switch/server.d.ts +17 -0
  15. package/dist/language-switch/server.js +26 -0
  16. package/dist/language-switch/server.js.map +1 -0
  17. package/dist/language-switch.d.ts +120 -0
  18. package/dist/language-switch.js +34 -0
  19. package/dist/language-switch.js.map +1 -0
  20. package/dist/nav-bar.d.ts +52 -4
  21. package/dist/nav-bar.js +77 -14
  22. package/dist/nav-bar.js.map +1 -1
  23. package/dist/site-footer.js +2 -2
  24. package/dist/sites.js +2 -2
  25. package/dist/sites.js.map +1 -1
  26. package/package.json +32 -8
  27. package/registry/account-menu.json +1 -1
  28. package/registry/achievement.json +1 -1
  29. package/registry/action-bar.json +1 -1
  30. package/registry/activity-feed.json +1 -1
  31. package/registry/alert.json +1 -1
  32. package/registry/app-header.json +1 -1
  33. package/registry/app-shell.json +1 -1
  34. package/registry/argument-card.json +1 -1
  35. package/registry/audio-player.json +1 -1
  36. package/registry/auth-layout.json +1 -1
  37. package/registry/avatar.json +1 -1
  38. package/registry/badge.json +1 -1
  39. package/registry/button.json +1 -1
  40. package/registry/captcha.json +1 -1
  41. package/registry/card-grid.json +1 -1
  42. package/registry/card.json +1 -1
  43. package/registry/catalogue-page.json +1 -1
  44. package/registry/changelog-page.json +1 -1
  45. package/registry/chat.json +1 -1
  46. package/registry/checkbox.json +1 -1
  47. package/registry/chip-link.json +1 -1
  48. package/registry/chip.json +1 -1
  49. package/registry/choice-grid.json +1 -1
  50. package/registry/code-block.json +1 -1
  51. package/registry/combobox.json +1 -1
  52. package/registry/command.json +1 -1
  53. package/registry/comparison-table.json +1 -1
  54. package/registry/component-page.json +1 -1
  55. package/registry/confirm-button.json +1 -1
  56. package/registry/contact-page.json +1 -1
  57. package/registry/continue-prompt.json +1 -1
  58. package/registry/countdown.json +1 -1
  59. package/registry/cover-hero.json +1 -1
  60. package/registry/cta-panel.json +1 -1
  61. package/registry/dashboard-layout.json +1 -1
  62. package/registry/data-table.json +1 -1
  63. package/registry/date-picker.json +1 -1
  64. package/registry/decision-list.json +1 -1
  65. package/registry/description-list.json +1 -1
  66. package/registry/detail-layout.json +1 -1
  67. package/registry/dialog.json +1 -1
  68. package/registry/docs-article-page.json +1 -1
  69. package/registry/docs-hub-page.json +1 -1
  70. package/registry/docs-layout.json +1 -1
  71. package/registry/docs-nav.json +1 -1
  72. package/registry/document-viewer.json +2 -2
  73. package/registry/dropdown-menu.json +1 -1
  74. package/registry/empty-state.json +1 -1
  75. package/registry/error-page.json +1 -1
  76. package/registry/examples/language-switch/inline.d.ts +4 -0
  77. package/registry/examples/language-switch/inline.js +11 -0
  78. package/registry/examples/language-switch/inline.tsx +6 -0
  79. package/registry/examples/language-switch/menu.d.ts +4 -0
  80. package/registry/examples/language-switch/menu.js +11 -0
  81. package/registry/examples/language-switch/menu.tsx +6 -0
  82. package/registry/examples/nav-bar/family-product.js +18 -10
  83. package/registry/examples/nav-bar/family-product.tsx +28 -27
  84. package/registry/field.json +1 -1
  85. package/registry/file-upload.json +1 -1
  86. package/registry/filter-bar.json +1 -1
  87. package/registry/force-update-screen.json +1 -1
  88. package/registry/form.json +1 -1
  89. package/registry/gallery-page.json +1 -1
  90. package/registry/genui.json +1 -1
  91. package/registry/glow-card.json +1 -1
  92. package/registry/goal-gauge.json +1 -1
  93. package/registry/how-it-works-page.json +1 -1
  94. package/registry/image-cropper.json +2 -2
  95. package/registry/index.json +37 -5
  96. package/registry/iso.json +1 -1
  97. package/registry/kanban.json +1 -1
  98. package/registry/kbd.json +1 -1
  99. package/registry/landing-page.json +1 -1
  100. package/registry/landscape.json +1 -1
  101. package/registry/language-switch.json +250 -0
  102. package/registry/legal-page.json +1 -1
  103. package/registry/link-card.json +1 -1
  104. package/registry/list.json +1 -1
  105. package/registry/loader.json +1 -1
  106. package/registry/longform-page.json +1 -1
  107. package/registry/maintenance-screen.json +1 -1
  108. package/registry/map.json +2 -2
  109. package/registry/markdown.json +1 -1
  110. package/registry/marketing-layout.json +1 -1
  111. package/registry/marquee.json +1 -1
  112. package/registry/mascot-quote.json +1 -1
  113. package/registry/mascot-state.json +1 -1
  114. package/registry/mascots.json +1 -1
  115. package/registry/match-dialog.json +1 -1
  116. package/registry/media-capture.json +1 -1
  117. package/registry/media-gallery.json +1 -1
  118. package/registry/media-grid.json +1 -1
  119. package/registry/meta-row.json +1 -1
  120. package/registry/meter.json +1 -1
  121. package/registry/nav-bar.json +155 -10
  122. package/registry/number-ticker.json +1 -1
  123. package/registry/opening-hours.json +1 -1
  124. package/registry/page-header.json +1 -1
  125. package/registry/page-hero.json +1 -1
  126. package/registry/page.json +1 -1
  127. package/registry/pagination.json +1 -1
  128. package/registry/photo-grid.json +1 -1
  129. package/registry/pointer-light.json +1 -1
  130. package/registry/popover.json +1 -1
  131. package/registry/pricing-page.json +1 -1
  132. package/registry/product-page.json +1 -1
  133. package/registry/profile-card.json +1 -1
  134. package/registry/progress.json +1 -1
  135. package/registry/promise-card.json +1 -1
  136. package/registry/radio-group.json +1 -1
  137. package/registry/rail-nav.json +2 -2
  138. package/registry/recipes.json +1 -1
  139. package/registry/scroll-stage.json +1 -1
  140. package/registry/search-field.json +1 -1
  141. package/registry/section-backdrop.json +1 -1
  142. package/registry/separator.json +1 -1
  143. package/registry/service-card.json +1 -1
  144. package/registry/settings-layout.json +1 -1
  145. package/registry/showcase.json +1 -1
  146. package/registry/site-footer.json +2 -2
  147. package/registry/sites.json +2 -2
  148. package/registry/skeleton.json +1 -1
  149. package/registry/skip-link.json +1 -1
  150. package/registry/slider.json +1 -1
  151. package/registry/slot-picker.json +1 -1
  152. package/registry/slot.json +1 -1
  153. package/registry/spinner.json +1 -1
  154. package/registry/split-view.json +1 -1
  155. package/registry/stage-banner.json +1 -1
  156. package/registry/stat.json +1 -1
  157. package/registry/status-page.json +1 -1
  158. package/registry/step-progress.json +1 -1
  159. package/registry/step-showcase.json +1 -1
  160. package/registry/stepper.json +1 -1
  161. package/registry/sticky-action.json +1 -1
  162. package/registry/swipe-deck.json +1 -1
  163. package/registry/switch.json +1 -1
  164. package/registry/tabs.json +1 -1
  165. package/registry/tag-input.json +1 -1
  166. package/registry/tag-selector.json +1 -1
  167. package/registry/terminal-replay.json +1 -1
  168. package/registry/theme.json +1 -1
  169. package/registry/toast.json +1 -1
  170. package/registry/toc.json +1 -1
  171. package/registry/tooltip.json +1 -1
  172. package/registry/tree-view.json +1 -1
  173. package/registry/update-prompt.json +1 -1
  174. package/registry/verified-badge.json +1 -1
  175. package/registry/vertical-video.json +1 -1
  176. package/registry/video-player.json +1 -1
  177. package/registry/video-trimmer.json +1 -1
  178. package/registry/virtual.json +1 -1
  179. package/registry/waveform.json +1 -1
  180. /package/dist/{chunk-4DH2HW3D.js.map → chunk-TFJDIOFK.js.map} +0 -0
package/README.md CHANGED
@@ -147,7 +147,8 @@ light or a product's — is a set of tokens.
147
147
  | `@krizaka/ui/skip-link` | `SkipLink` (`href`, default `#content`; the label as children): the first tab stop, hidden until focused, Enter moves focus to the content (WCAG 2.4.1) | client |
148
148
  | `@krizaka/ui/kbd` | `Kbd` (`size` sm·md), `kbdVariants` | server |
149
149
  | `@krizaka/ui/tabs` | `Tabs.Root/List/Trigger/Content` (`variant` underline·segmented·pills on the root, `orientation`, `value`/`defaultValue` + `onValueChange`; one light glides to the active tab), `tabs` (the slots) | client |
150
- | `@krizaka/ui/nav-bar` | `NavBar.Root` (`label`, `compactAfter`, `floating`) `/Brand` `/Family` (`current`, `labels`, `defaultOpen`: the family switcher on the brand, the mobile panel's last group inside `Mobile`) `/Badge` (`env`, `version`) `/Items/Link` (`active`) `/Menu` (`label`, `columns`) `/MenuLink` (`title`, `description`, `icon`) `/Actions` `/Repo` (`href`, `label`) `/Mobile` (`openLabel`, `closeLabel`) `/MobileLink`, `navBar` (the slots) | client |
150
+ | `@krizaka/ui/language-switch` | `LanguageSwitch` (`label`, `current`, `languages`, `variant`, `linkAs`), `languagesFor(path)`, `languageHref(path, to)` | client |
151
+ | `@krizaka/ui/nav-bar` | `NavBar.Root` (`label`, `compactAfter`, `floating`) `/Brand` `/Family` (`current`, `labels`, `defaultOpen`: the family switcher on the brand, the mobile panel's last group inside `Mobile`) `/Badge` (`env`, `version`) `/Items/Link` (`active`) `/Menu` (`label`, `columns`) `/MenuLink` (`title`, `description`, `icon`) `/Actions` `/Repo` (`href`, `label`) `/Language` (the language switch: a menu in the bar, a row in the mobile panel) `/Mobile` (`openLabel`, `closeLabel`) `/MobilePreferences` (`label`) `/MobileLink`, `navBar` (the slots) | client |
151
152
  | `@krizaka/ui/sites` | The Krizaka family as data: `sites` (id, name, url, host, kind, group, status, mark, repo), `siteGroups`, `siteById`, `sitesByGroup`, `familyLabels` (`en`, `fr`) | server |
152
153
  | `@krizaka/ui/site-footer` | `SiteFooter` (`current`, `labels`, `version`, `env`, `legal`; own columns as children) `.Column` (`title`) `.Link` (`active`, `asChild`): the footer of every Krizaka site — the family in columns, “A Krizaka company / product / open-source project”, the version, Montréal | client |
153
154
  | `@krizaka/ui/chip` | `Chip` (`selected`/`defaultSelected` + `onSelectedChange`, `size` sm·md; `removable` + `removeLabel` + `onRemove`), `Chip.Group` (`type` single·multiple, `label`, `required` for single), `chip` (the slots) | client |
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ // src/language-switch/language-switch.tsx
4
+ import * as React from "react";
5
+ import { tv } from "tailwind-variants";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ var languageSwitch = tv({
8
+ slots: {
9
+ root: "relative inline-flex",
10
+ trigger: "group/language inline-flex h-9 shrink-0 select-none items-center gap-1.5 rounded-full px-2.5 font-mono text-xs font-semibold uppercase tracking-wide text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg aria-expanded:bg-surface-3 aria-expanded:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
11
+ glyph: "h-4 w-4 shrink-0",
12
+ panel: "kz-pop absolute right-0 top-full z-50 mt-2 grid min-w-44 max-w-[calc(100vw-2rem)] gap-0.5 rounded-xl border border-border-default bg-surface-1 p-1.5 shadow-lg",
13
+ heading: "px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary",
14
+ item: "flex min-h-9 items-center justify-between gap-6 rounded-lg px-2.5 py-2 text-sm font-medium text-fg no-underline outline-hidden transition-colors hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-accent-soft",
15
+ check: "h-4 w-4 shrink-0 text-fg-accent",
16
+ inline: "inline-flex items-center gap-1 rounded-full border border-border-subtle bg-surface-2/60 p-0.5",
17
+ inlineLink: "inline-flex min-h-9 items-center rounded-full px-3 text-sm font-medium text-fg-secondary no-underline transition-colors hover:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-surface-3 aria-[current=true]:text-fg"
18
+ }
19
+ });
20
+ var s = languageSwitch();
21
+ var Globe = ({ className }) => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className, fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
22
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
23
+ /* @__PURE__ */ jsx("path", { d: "M3 12h18M12 3c2.5 2.6 3.8 5.6 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3Z" })
24
+ ] });
25
+ function LanguageSwitch({ label, current, languages, variant = "menu", linkAs, defaultOpen = false, className }) {
26
+ const Link = linkAs ?? "a";
27
+ const id = React.useId();
28
+ const [open, setOpen] = React.useState(defaultOpen);
29
+ const root = React.useRef(null);
30
+ const trigger = React.useRef(null);
31
+ const active = languages.find((l) => l.code === current);
32
+ React.useEffect(() => {
33
+ if (!open) return;
34
+ const onPress = (e) => {
35
+ if (e.target instanceof Node && !root.current?.contains(e.target)) setOpen(false);
36
+ };
37
+ document.addEventListener("pointerdown", onPress);
38
+ return () => document.removeEventListener("pointerdown", onPress);
39
+ }, [open]);
40
+ if (variant === "inline") {
41
+ return /* @__PURE__ */ jsx("div", { role: "group", "aria-label": label, className: s.inline({ className }), children: languages.map((l) => /* @__PURE__ */ jsx(Link, { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": l.code === current ? "true" : void 0, className: s.inlineLink(), children: l.name }, l.code)) });
42
+ }
43
+ const links = () => [...root.current?.querySelectorAll("[data-language]") ?? []];
44
+ const close = (focus) => {
45
+ setOpen(false);
46
+ if (focus) trigger.current?.focus();
47
+ };
48
+ return /* @__PURE__ */ jsxs(
49
+ "div",
50
+ {
51
+ ref: root,
52
+ className: s.root({ className }),
53
+ onKeyDown: (e) => {
54
+ if (e.key === "Escape" && open) {
55
+ e.stopPropagation();
56
+ close(true);
57
+ }
58
+ if (open && (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End")) {
59
+ e.preventDefault();
60
+ const all = links();
61
+ const at = all.indexOf(document.activeElement);
62
+ const next = e.key === "Home" ? 0 : e.key === "End" ? all.length - 1 : (at + (e.key === "ArrowDown" ? 1 : -1) + all.length) % all.length;
63
+ all[next]?.focus();
64
+ }
65
+ },
66
+ onBlur: (e) => {
67
+ if (open && !root.current?.contains(e.relatedTarget)) setOpen(false);
68
+ },
69
+ children: [
70
+ /* @__PURE__ */ jsxs(
71
+ "button",
72
+ {
73
+ ref: trigger,
74
+ type: "button",
75
+ "aria-label": active ? `${label}: ${active.name}` : label,
76
+ "aria-expanded": open,
77
+ "aria-controls": id,
78
+ onClick: () => setOpen(!open),
79
+ onKeyDown: (e) => {
80
+ if (e.key === "ArrowDown") {
81
+ e.preventDefault();
82
+ setOpen(true);
83
+ requestAnimationFrame(() => (links().find((l) => l.getAttribute("aria-current") === "true") ?? links()[0])?.focus());
84
+ }
85
+ },
86
+ className: s.trigger(),
87
+ children: [
88
+ /* @__PURE__ */ jsx(Globe, { className: s.glyph() }),
89
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-code": current, className: "after:content-[attr(data-code)]" })
90
+ ]
91
+ }
92
+ ),
93
+ open && /* @__PURE__ */ jsxs("div", { id, role: "group", "aria-label": label, className: s.panel(), children: [
94
+ /* @__PURE__ */ jsx("p", { "aria-hidden": true, className: s.heading(), children: label }),
95
+ languages.map((l) => /* @__PURE__ */ jsxs(Link, { "data-language": "", href: l.href, hrefLang: l.code, lang: l.code, "aria-current": l.code === current ? "true" : void 0, className: s.item(), children: [
96
+ l.name,
97
+ l.code === current && /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: s.check(), fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) })
98
+ ] }, l.code))
99
+ ] })
100
+ ]
101
+ }
102
+ );
103
+ }
104
+
105
+ export {
106
+ languageSwitch,
107
+ LanguageSwitch
108
+ };
109
+ //# sourceMappingURL=chunk-3S4GHSPZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/language-switch/language-switch.tsx"],"sourcesContent":["// Client (a disclosure). The one language switch of the family: a globe and the current code (FR), opening a short list of\n// the languages by their own names — links with `hreflang` that keep the page. A disclosure, not a Radix menu: the items\n// are navigation, and the bar must not pay for a menu engine (Escape, outside press, focus-out and the arrows are here).\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport type { Language } from \"./href\";\n\nexport const languageSwitch = tv({\n slots: {\n root: \"relative inline-flex\",\n trigger:\n \"group/language inline-flex h-9 shrink-0 select-none items-center gap-1.5 rounded-full px-2.5 font-mono text-xs font-semibold uppercase tracking-wide text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg aria-expanded:bg-surface-3 aria-expanded:text-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n glyph: \"h-4 w-4 shrink-0\",\n panel:\n \"kz-pop absolute right-0 top-full z-50 mt-2 grid min-w-44 max-w-[calc(100vw-2rem)] gap-0.5 rounded-xl border border-border-default bg-surface-1 p-1.5 shadow-lg\",\n heading: \"px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n item:\n \"flex min-h-9 items-center justify-between gap-6 rounded-lg px-2.5 py-2 text-sm font-medium text-fg no-underline outline-hidden transition-colors \" +\n \"hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-accent-soft\",\n check: \"h-4 w-4 shrink-0 text-fg-accent\",\n inline: \"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-surface-2/60 p-0.5\",\n inlineLink:\n \"inline-flex min-h-9 items-center rounded-full px-3 text-sm font-medium text-fg-secondary no-underline transition-colors hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-[current=true]:bg-surface-3 aria-[current=true]:text-fg\",\n },\n});\nconst s = languageSwitch();\n\nexport type LanguageSwitchProps = {\n /** The control's accessible name (“Language”, “Langue”) — passed translated. */\n label: string;\n /** The code of the page's language (`fr`). */\n current: string;\n /** The languages offered, each with its name in its own language and the address of the same page (`languagesFor`). */\n languages: readonly Language[];\n /**\n * `menu`: a globe and the code, opening a menu (the bar). `inline`: every language in a row (a mobile panel, a settings page).\n * @default \"menu\"\n */\n variant?: \"menu\" | \"inline\";\n /** Your router's `<Link>` (it receives `href`, `hrefLang`, `lang`). A plain `<a>` otherwise. */\n linkAs?: React.ElementType;\n /** Opens the menu at first render (a story, a screenshot). */\n defaultOpen?: boolean;\n /** Classes of the control. */\n className?: string;\n};\n\nconst Globe = ({ className }: { className?: string }) => (\n <svg viewBox=\"0 0 24 24\" className={className} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M3 12h18M12 3c2.5 2.6 3.8 5.6 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3Z\" />\n </svg>\n);\n\n/**\n * The language switch, the same on every Krizaka site and app: a compact control — a globe and the current code\n * (`FR`) — that opens a menu of the languages named in their own language (Français · English), each a link with\n * `hreflang` to the same page. It shows the language you are in, never the “other” one. `variant=\"inline\"` lays the\n * languages in a row for a phone's panel or a settings page.\n */\nexport function LanguageSwitch({ label, current, languages, variant = \"menu\", linkAs, defaultOpen = false, className }: LanguageSwitchProps) {\n const Link = linkAs ?? \"a\";\n const id = React.useId();\n const [open, setOpen] = React.useState(defaultOpen);\n const root = React.useRef<HTMLDivElement>(null);\n const trigger = React.useRef<HTMLButtonElement>(null);\n const active = languages.find((l) => l.code === current);\n\n React.useEffect(() => {\n if (!open) return;\n // A press outside closes it (the page under it stays usable).\n const onPress = (e: PointerEvent) => {\n if (e.target instanceof Node && !root.current?.contains(e.target)) setOpen(false);\n };\n document.addEventListener(\"pointerdown\", onPress);\n return () => document.removeEventListener(\"pointerdown\", onPress);\n }, [open]);\n\n if (variant === \"inline\") {\n return (\n <div role=\"group\" aria-label={label} className={s.inline({ className })}>\n {languages.map((l) => (\n <Link key={l.code} href={l.href} hrefLang={l.code} lang={l.code} aria-current={l.code === current ? \"true\" : undefined} className={s.inlineLink()}>\n {l.name}\n </Link>\n ))}\n </div>\n );\n }\n\n const links = () => [...(root.current?.querySelectorAll<HTMLElement>(\"[data-language]\") ?? [])];\n const close = (focus: boolean) => {\n setOpen(false);\n if (focus) trigger.current?.focus();\n };\n return (\n <div\n ref={root}\n className={s.root({ className })}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && open) {\n e.stopPropagation();\n close(true);\n }\n if (open && (e.key === \"ArrowDown\" || e.key === \"ArrowUp\" || e.key === \"Home\" || e.key === \"End\")) {\n e.preventDefault();\n const all = links();\n const at = all.indexOf(document.activeElement as HTMLElement);\n const next = e.key === \"Home\" ? 0 : e.key === \"End\" ? all.length - 1 : (at + (e.key === \"ArrowDown\" ? 1 : -1) + all.length) % all.length;\n all[next]?.focus();\n }\n }}\n // Tab out of the switch closes it.\n onBlur={(e) => {\n if (open && !root.current?.contains(e.relatedTarget as Node | null)) setOpen(false);\n }}\n >\n <button\n ref={trigger}\n type=\"button\"\n aria-label={active ? `${label}: ${active.name}` : label}\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen(!open)}\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setOpen(true);\n requestAnimationFrame(() => (links().find((l) => l.getAttribute(\"aria-current\") === \"true\") ?? links()[0])?.focus());\n }\n }}\n className={s.trigger()}\n >\n <Globe className={s.glyph()} />\n {/* Drawn by CSS: the code is no text of the button, so its name (`Language: English`) never mismatches what is visible. */}\n <span aria-hidden data-code={current} className=\"after:content-[attr(data-code)]\" />\n </button>\n {open && (\n <div id={id} role=\"group\" aria-label={label} className={s.panel()}>\n <p aria-hidden className={s.heading()}>\n {label}\n </p>\n {languages.map((l) => (\n <Link key={l.code} data-language=\"\" href={l.href} hrefLang={l.code} lang={l.code} aria-current={l.code === current ? \"true\" : undefined} className={s.item()}>\n {l.name}\n {l.code === current && (\n <svg viewBox=\"0 0 24 24\" className={s.check()} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n )}\n </Link>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  ProductLogo
4
- } from "./chunk-4DH2HW3D.js";
4
+ } from "./chunk-TFJDIOFK.js";
5
5
 
6
6
  // src/family-mark.tsx
7
7
  import { jsx } from "react/jsx-runtime";
@@ -50,7 +50,7 @@ var familyLabels = {
50
50
  orochia: "A video platform for creators.",
51
51
  yasakia: "The dating app where you talk first.",
52
52
  yororis: "A listening space with a face and a voice.",
53
- yomizaka: "Your documents, read and checked by Orazaka.",
53
+ yomizaka: "Yomizaka reads your leases, contracts and invoices.",
54
54
  kachizaka: "Predictions among friends \u2014 live data, in real time.",
55
55
  akinaza: "Running a shop in Qu\xE9bec, for less \u2014 Loi 25 by design.",
56
56
  ui: "Components, tokens, icons and charts \u2014 web and React Native.",
@@ -72,7 +72,7 @@ var familyLabels = {
72
72
  orochia: "Une plateforme vid\xE9o pour les cr\xE9ateurs.",
73
73
  yasakia: "L\u2019app de rencontre o\xF9 l\u2019on se parle d\u2019abord.",
74
74
  yororis: "Un espace d\u2019\xE9coute avec un visage et une voix.",
75
- yomizaka: "Vos documents, lus et v\xE9rifi\xE9s par Orazaka.",
75
+ yomizaka: "Yomizaka lit vos baux, contrats et factures.",
76
76
  kachizaka: "Les pronostics entre amis \u2014 des donn\xE9es en direct.",
77
77
  akinaza: "Tenir une boutique au Qu\xE9bec, pour moins cher \u2014 Loi 25 d\xE8s la conception.",
78
78
  ui: "Composants, jetons, ic\xF4nes et graphiques \u2014 web et React Native.",
@@ -92,4 +92,4 @@ export {
92
92
  sitesByGroup,
93
93
  familyLabels
94
94
  };
95
- //# sourceMappingURL=chunk-FTX66XZY.js.map
95
+ //# sourceMappingURL=chunk-OUWPBSSL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/family-mark.tsx","../src/sites/sites.ts"],"sourcesContent":["// The mark of a site of the family (`@krizaka/ui/sites`): its brand's `ProductLogo`, still (a list of marks never\n// moves), or GitHub's, in the text colour. Shared by `NavBar.Family` and `SiteFooter` — not an entry of its own.\nimport type * as React from \"react\";\n\nimport { ProductLogo } from \"./marks/ProductLogo\";\nimport type { SiteMark } from \"./sites/sites\";\n\n/** GitHub's mark, in the text colour (both themes). */\nexport function GitHubGlyph({ size = 16, className }: { size?: number; className?: string }) {\n return (\n <svg viewBox=\"0 0 16 16\" width={size} height={size} fill=\"currentColor\" aria-hidden className={className}>\n <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" />\n </svg>\n );\n}\n\n/** A site's mark, decorative (its name is always written beside it). */\nexport function FamilyMark({ mark, size }: { mark: SiteMark; size: number }): React.ReactElement {\n if (mark === \"github\") return <GitHubGlyph size={Math.round(size * 0.8)} />;\n return <ProductLogo id={mark} size={size} animated={false} />;\n}\n","// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing deployed beyond its page, nothing offered. `development`: being built in the open, nothing\n * deployed yet — its address is its repository (said \"In development\").\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://dev.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://dev.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://dev.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://dev.yororis.com\", kind: \"product\", group: \"products\", status: \"construction\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://dev.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n // In development: nothing deployed yet, so the address is the repository (never a dead link to an unborn dev site).\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://github.com/krizaka/kachizaka\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://github.com/krizaka/akinaza\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI, on your own servers.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"The dating app where you talk first.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Your documents, read and checked by Orazaka.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine, sur vos propres serveurs.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"L’app de rencontre où l’on se parle d’abord.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Vos documents, lus et vérifiés par Orazaka.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/family-mark.tsx","../src/sites/sites.ts"],"sourcesContent":["// The mark of a site of the family (`@krizaka/ui/sites`): its brand's `ProductLogo`, still (a list of marks never\n// moves), or GitHub's, in the text colour. Shared by `NavBar.Family` and `SiteFooter` — not an entry of its own.\nimport type * as React from \"react\";\n\nimport { ProductLogo } from \"./marks/ProductLogo\";\nimport type { SiteMark } from \"./sites/sites\";\n\n/** GitHub's mark, in the text colour (both themes). */\nexport function GitHubGlyph({ size = 16, className }: { size?: number; className?: string }) {\n return (\n <svg viewBox=\"0 0 16 16\" width={size} height={size} fill=\"currentColor\" aria-hidden className={className}>\n <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8Z\" />\n </svg>\n );\n}\n\n/** A site's mark, decorative (its name is always written beside it). */\nexport function FamilyMark({ mark, size }: { mark: SiteMark; size: number }): React.ReactElement {\n if (mark === \"github\") return <GitHubGlyph size={Math.round(size * 0.8)} />;\n return <ProductLogo id={mark} size={size} animated={false} />;\n}\n","// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing deployed beyond its page, nothing offered. `development`: being built in the open, nothing\n * deployed yet — its address is its repository (said \"In development\").\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://dev.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://dev.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://dev.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://dev.yororis.com\", kind: \"product\", group: \"products\", status: \"construction\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://dev.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n // In development: nothing deployed yet, so the address is the repository (never a dead link to an unborn dev site).\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://github.com/krizaka/kachizaka\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://github.com/krizaka/akinaza\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI, on your own servers.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"The dating app where you talk first.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Yomizaka reads your leases, contracts and invoices.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine, sur vos propres serveurs.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"L’app de rencontre où l’on se parle d’abord.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Yomizaka lit vos baux, contrats et factures.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n },\n};\n"],"mappings":"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","names":[]}
@@ -511,6 +511,7 @@ function ProductLogo({ id, ...props }) {
511
511
 
512
512
  export {
513
513
  neutral,
514
+ useIds,
514
515
  a11y,
515
516
  AkinazaLogo,
516
517
  KachizakaLogo,
@@ -523,4 +524,4 @@ export {
523
524
  YororisLogo,
524
525
  ProductLogo
525
526
  };
526
- //# sourceMappingURL=chunk-4DH2HW3D.js.map
527
+ //# sourceMappingURL=chunk-TFJDIOFK.js.map
@@ -32,6 +32,23 @@ function indexPage(items) {
32
32
  });
33
33
  return { text, at };
34
34
  }
35
+ var ADVANCE = "278 278 355 556 556 889 667 191 333 333 389 584 278 333 278 278 556 556 556 556 556 556 556 556 556 556 278 278 584 584 584 556 1015 667 667 722 722 667 611 778 722 278 500 667 556 833 722 778 667 778 722 667 611 722 667 944 667 667 611 278 278 278 469 556 333 556 556 500 556 556 278 556 556 222 222 500 222 833 556 556 556 556 333 500 278 556 500 722 500 500 500";
36
+ var ADVANCES = ADVANCE.split(" ").map(Number);
37
+ function advance(ch) {
38
+ const base = ch.normalize("NFD")[0] ?? ch;
39
+ const code = base.charCodeAt(0);
40
+ return code >= 32 && code <= 126 ? ADVANCES[code - 32] ?? 556 : 556;
41
+ }
42
+ function shareBefore(text, n) {
43
+ let before = 0;
44
+ let total = 0;
45
+ for (let i = 0; i < text.length; i++) {
46
+ const a = advance(text[i]);
47
+ if (i < n) before += a;
48
+ total += a;
49
+ }
50
+ return total > 0 ? before / total : n / Math.max(1, text.length);
51
+ }
35
52
  function boxesOf(page, index, from, to) {
36
53
  const spans = /* @__PURE__ */ new Map();
37
54
  for (let k = from; k < to; k++) {
@@ -42,10 +59,9 @@ function boxesOf(page, index, from, to) {
42
59
  const raw = [];
43
60
  for (const [i, [c0, c1]] of [...spans].sort((a, b) => a[0] - b[0])) {
44
61
  const item = page.items[i];
45
- const len = Math.max(1, item.str.length);
46
62
  const size = item.height || Math.abs(item.transform[3] ?? 0) || 10;
47
- const x = (item.transform[4] ?? 0) + item.width * c0 / len;
48
- const w = item.width * (c1 - c0) / len;
63
+ const x = (item.transform[4] ?? 0) + item.width * shareBefore(item.str, c0);
64
+ const w = item.width * (shareBefore(item.str, c1) - shareBefore(item.str, c0));
49
65
  const top = page.height - ((item.transform[5] ?? 0) + size * 0.9);
50
66
  raw.push({ x: x / page.width, y: top / page.height, width: w / page.width, height: size * 1.15 / page.height });
51
67
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/document-viewer/document-viewer.tsx","../src/document-viewer/locate.ts"],"sourcesContent":["// Client. A PDF drawn page by page (pdf.js), and passages of it lit where they sit. The app hands over the engine\n// (`loadPdfjs={() => import(\"pdfjs-dist\")}` — an optional peer, loaded only on a page that shows a document) and the\n// worker's address; it hands over the passages as quotes (`highlights`), and the viewer finds their place from the\n// PDF's own text positions (`locateQuote`): one box per line, in fractions of the page, so they follow any zoom.\n// `activeId` (controlled) scrolls to a passage and strengthens it; a click on a box calls `onActiveChange`. A passage\n// the text layer cannot place is never drawn at a guessed place: the viewer goes to its page when it is known and shows\n// the quote in a bar — and tells the app (`onLocate`) so its list can say \"page 3\" or \"not found\".\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { useLatest, usePrefersReducedMotion } from \"../media\";\nimport { type Located, locateQuote, type PageText } from \"./locate\";\n\nexport const documentViewer = tv({\n slots: {\n root: \"relative flex min-w-0 flex-col gap-3\",\n pages: \"flex max-h-(--kz-document-viewer-height,70vh) flex-col gap-3 overflow-auto rounded-lg border border-border-default bg-surface-2 p-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\",\n page: \"relative mx-auto w-full max-w-(--kz-document-viewer-page,56rem) shrink-0 overflow-hidden rounded-sm bg-white shadow-sm\",\n canvas: \"block h-auto w-full\",\n caption: \"absolute top-1 left-1 rounded-sm bg-surface-1/90 px-1.5 py-0.5 text-xs text-fg-secondary\",\n box: \"absolute cursor-pointer rounded-xs border border-warning/70 bg-warning/25 mix-blend-multiply transition-colors data-[active=true]:border-2 data-[active=true]:border-danger data-[active=true]:bg-danger/25 motion-reduce:transition-none\",\n bar: \"rounded-lg border border-border-default bg-surface-1 px-3 py-2 text-sm text-fg-secondary\",\n status: \"p-6 text-center text-sm text-fg-secondary\",\n },\n});\n\n/** The words of the viewer, passed translated. `{page}` and `{total}` are replaced. */\nexport type DocumentViewerLabels = {\n /** The accessible name of the pages' region. */\n region: string;\n loading: string;\n failed: string;\n /** A page's caption and accessible name: \"Page {page} of {total}\". */\n page: string;\n /** The bar above the pages when the active passage could only be placed on a page: \"Passage on page {page}:\". */\n onPage: string;\n /** The bar when the active passage is not in the text layer. */\n notFound: string;\n};\n\nexport const documentViewerLabels: DocumentViewerLabels = {\n region: \"Document pages\",\n loading: \"Loading the document…\",\n failed: \"The document could not be displayed.\",\n page: \"Page {page} of {total}\",\n onPage: \"Passage on page {page}:\",\n notFound: \"This passage is not in the document's text layer:\",\n};\n\n/** A passage to light: its quote (as the text layer reads it), the page if the app knows it, a name for screen readers. */\nexport type DocumentHighlight = { id: string; quote: string; page?: number; label?: string };\n\n/** What the viewer found for a passage. */\nexport type { Located };\n\n/** The members of pdf.js the viewer uses (`pdfjs-dist` ≥ 5), typed here so the package does not depend on it. */\nexport type PdfjsModule = {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(source: { url?: string; data?: Uint8Array }): { promise: Promise<PdfDocument>; destroy?: () => Promise<void> | void };\n};\nexport type PdfDocument = { numPages: number; getPage(n: number): Promise<PdfPage>; destroy?: () => Promise<void> | void };\nexport type PdfPage = {\n rotate: number;\n view: number[];\n getViewport(options: { scale: number }): { width: number; height: number };\n getTextContent(): Promise<{ items: readonly unknown[] }>;\n render(options: { canvas: HTMLCanvasElement; canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }): { promise: Promise<void>; cancel?: () => void };\n cleanup?: () => void;\n};\n\nexport type DocumentViewerProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> & {\n /** The PDF: an address (same origin or CORS) or its bytes. */\n src: string | Uint8Array;\n /** Loads pdf.js: `() => import(\"pdfjs-dist\")`. An optional peer, never in a bundle without a document. */\n loadPdfjs: () => Promise<PdfjsModule>;\n /** The address of pdf.js's worker (`new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url).toString()`). */\n workerSrc: string;\n /** The passages to light. */\n highlights?: readonly DocumentHighlight[];\n /** The passage in focus: the viewer scrolls to it. */\n activeId?: string | null;\n /** A box was clicked (the id) or the focus was released (`null`). */\n onActiveChange?: (id: string | null) => void;\n /** Where each passage was found (`null`: nowhere) — once the text layer is read, and again if `highlights` change. */\n onLocate?: (found: Record<string, Located | null>) => void;\n /** Called when the document is open, with its page count. */\n onLoad?: (info: { pages: number }) => void;\n /** The words of the viewer, translated (English by default). */\n labels?: Partial<DocumentViewerLabels>;\n};\n\nconst fill = (template: string, vars: Record<string, string | number>) => template.replace(/\\{(\\w+)\\}/g, (m, k: string) => (k in vars ? String(vars[k]) : m));\n\ntype Loaded = { doc: PdfDocument; texts: PageText[]; sizes: { width: number; height: number }[] };\ntype Phase = { state: \"loading\" } | { state: \"error\"; key: string } | ({ state: \"ready\"; key: string } & Loaded);\n\nconst isItem = (x: unknown): x is PageText[\"items\"][number] => typeof x === \"object\" && x !== null && typeof (x as { str?: unknown }).str === \"string\" && Array.isArray((x as { transform?: unknown }).transform);\n\nfunction PageCanvas({ page, doc, caption, onReady, className }: { page: number; doc: PdfDocument; caption: string; onReady?: () => void; className: string }) {\n const ref = React.useRef<HTMLCanvasElement>(null);\n const wrapper = React.useRef<HTMLDivElement>(null);\n const [visible, setVisible] = React.useState(() => typeof IntersectionObserver === \"undefined\");\n const ready = useLatest(onReady);\n React.useEffect(() => {\n const node = wrapper.current?.parentElement;\n if (!node) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && setVisible(true), { rootMargin: \"600px 0px\" });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n React.useEffect(() => {\n if (!visible) return;\n let task: { promise: Promise<void>; cancel?: () => void } | undefined;\n let cancelled = false;\n void (async () => {\n const p = await doc.getPage(page);\n const canvas = ref.current;\n const ctx = canvas?.getContext(\"2d\");\n if (cancelled || !canvas || !ctx) return;\n const base = p.getViewport({ scale: 1 });\n const dpr = typeof window === \"undefined\" ? 1 : Math.min(2, window.devicePixelRatio || 1);\n const scale = Math.min(2.5, Math.max(1, (canvas.parentElement?.clientWidth || base.width) / base.width)) * dpr;\n const viewport = p.getViewport({ scale });\n canvas.width = Math.floor(viewport.width);\n canvas.height = Math.floor(viewport.height);\n task = p.render({ canvas, canvasContext: ctx, viewport });\n await task.promise.catch(() => undefined);\n if (!cancelled) ready.current?.();\n })().catch(() => undefined);\n return () => {\n cancelled = true;\n task?.cancel?.();\n };\n }, [visible, doc, page, ready]);\n return (\n <div ref={wrapper} className=\"contents\">\n <canvas ref={ref} aria-hidden=\"true\" className={className} />\n <span className=\"sr-only\">{caption}</span>\n </div>\n );\n}\n\n/** A PDF with its passages lit — see the file's header. Client. */\nexport function DocumentViewer({ src, loadPdfjs, workerSrc, highlights = [], activeId = null, onActiveChange, onLocate, onLoad, labels, className, ...props }: DocumentViewerProps) {\n const words = { ...documentViewerLabels, ...labels };\n const s = documentViewer();\n const reduce = usePrefersReducedMotion();\n const [loaded, setPhase] = React.useState<Phase>({ state: \"loading\" });\n const callbacks = useLatest({ onLoad, onLocate });\n const containerRef = React.useRef<HTMLDivElement>(null);\n const bytes = typeof src === \"string\" ? src : src.byteLength + \":\" + src[0] + \":\" + src[src.length - 1];\n // A result belongs to the document it was read from: another `src` is loading again, with no reset inside the effect.\n const phase: Phase = loaded.state !== \"loading\" && loaded.key !== bytes ? { state: \"loading\" } : loaded;\n\n // Open the document and read every page's text layer (positions only; nothing is sent anywhere).\n React.useEffect(() => {\n let cancelled = false;\n let opened: { destroy?: () => Promise<void> | void } | undefined;\n void (async () => {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;\n const loading = pdfjs.getDocument(typeof src === \"string\" ? { url: src } : { data: src.slice() });\n opened = loading;\n const doc = await loading.promise;\n const texts: PageText[] = [];\n const sizes: Loaded[\"sizes\"] = [];\n for (let n = 1; n <= doc.numPages; n++) {\n const page = await doc.getPage(n);\n const vp = page.getViewport({ scale: 1 });\n sizes.push({ width: vp.width, height: vp.height });\n const content = await page.getTextContent();\n // A rotated page has no reliable boxes: it keeps its size and an empty text layer (the app's page hint still works).\n texts.push({ page: n, width: vp.width, height: vp.height, items: page.rotate % 360 === 0 ? content.items.filter(isItem) : [] });\n page.cleanup?.();\n }\n if (cancelled) return;\n setPhase({ state: \"ready\", key: bytes, doc, texts, sizes });\n callbacks.current.onLoad?.({ pages: doc.numPages });\n })().catch(() => !cancelled && setPhase({ state: \"error\", key: bytes }));\n return () => {\n cancelled = true;\n void opened?.destroy?.();\n };\n // `src` is compared by content (`bytes`) so a re-render with the same bytes does not reopen the file.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bytes, loadPdfjs, workerSrc, callbacks]);\n\n const key = highlights.map((h) => `${h.id}\\u0000${h.page ?? \"\"}\\u0000${h.quote}`).join(\"\\u0001\");\n const found = React.useMemo(() => {\n const out: Record<string, Located | null> = {};\n if (phase.state === \"ready\") for (const h of highlights) out[h.id] = locateQuote(phase.texts, h.quote, h.page);\n return out;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [phase, key]);\n React.useEffect(() => {\n if (phase.state === \"ready\") callbacks.current.onLocate?.(found);\n }, [found, phase.state, callbacks]);\n\n // Go to the active passage.\n const active = activeId ? highlights.find((h) => h.id === activeId) : undefined;\n const where = activeId ? found[activeId] : undefined;\n React.useEffect(() => {\n if (phase.state !== \"ready\" || !where) return;\n const root = containerRef.current;\n const target = root?.querySelector<HTMLElement>(where.boxes.length > 0 ? `[data-highlight=\"${CSS.escape(activeId!)}\"]` : `[data-page=\"${where.page}\"]`) ?? null;\n target?.scrollIntoView?.({ block: where.boxes.length > 0 ? \"center\" : \"start\", behavior: reduce ? \"auto\" : \"smooth\" });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [activeId, where, phase.state]);\n\n return (\n <div className={s.root({ className })} {...props}>\n {active && where !== undefined && where?.match !== \"exact\" && where?.match !== \"start\" ? (\n <p className={s.bar()} data-testid=\"document-viewer-bar\">\n {where?.match === \"page\" ? fill(words.onPage, { page: where.page }) : words.notFound} <q>{active.quote}</q>\n </p>\n ) : null}\n <div ref={containerRef} className={s.pages()} role=\"region\" aria-label={words.region} tabIndex={0}>\n {phase.state === \"loading\" ? <p className={s.status()}>{words.loading}</p> : null}\n {phase.state === \"error\" ? <p role=\"alert\" className={s.status()}>{words.failed}</p> : null}\n {phase.state === \"ready\"\n ? phase.texts.map((t, i) => {\n const size = phase.sizes[i]!;\n const caption = fill(words.page, { page: t.page, total: phase.doc.numPages });\n return (\n <div key={t.page} className={s.page()} data-page={t.page} style={{ aspectRatio: `${size.width} / ${size.height}` }}>\n <PageCanvas page={t.page} doc={phase.doc} caption={caption} className={s.canvas()} />\n <span className={s.caption()} aria-hidden=\"true\">{caption}</span>\n {highlights.flatMap((h) => {\n const f = found[h.id];\n if (!f || f.page !== t.page) return [];\n return f.boxes.map((b, k) => (\n <div\n key={`${h.id}:${k}`}\n aria-hidden=\"true\"\n data-highlight={h.id}\n data-active={h.id === activeId}\n className={s.box()}\n style={{ left: `${b.x * 100}%`, top: `${b.y * 100}%`, width: `${b.width * 100}%`, height: `${b.height * 100}%` }}\n onClick={() => onActiveChange?.(h.id === activeId ? null : h.id)}\n />\n ));\n })}\n </div>\n );\n })\n : null}\n </div>\n </div>\n );\n}\n","// Pure: where a quoted passage sits on the pages of a PDF, from pdf.js's text positions. No DOM, no pdf.js import —\n// the viewer feeds it what `page.getTextContent()` returned, a test feeds it a fixture.\n\n/** A text item as pdf.js returns it (`TextItem`): the string, its matrix (`[a, b, c, d, x, baseline]`) and its size. */\nexport type TextItem = { str: string; transform: number[]; width: number; height: number };\n\n/** A rectangle in fractions of the page (0–1), origin top-left — independent of the zoom the page is drawn at. */\nexport type Box = { x: number; y: number; width: number; height: number };\n\n/** The text layer of one page: its size in PDF units (rotation 0) and its items in reading order. */\nexport type PageText = { page: number; width: number; height: number; items: readonly TextItem[] };\n\nexport type Located = {\n /** 1-based page where the passage starts. */\n page: number;\n /** One box per line of the passage (merged along a line); empty when only the page is known. */\n boxes: Box[];\n /** `exact`: the whole quote was found; `start`: only its opening words were (a quote cut by a different layout). */\n match: \"exact\" | \"start\" | \"page\";\n};\n\nconst FOLD: Record<string, string> = { \"’\": \"'\", \"‘\": \"'\", \"“\": '\"', \"”\": '\"', \"«\": '\"', \"»\": '\"', \"–\": \"-\", \"—\": \"-\", \" \": \" \", \" \": \" \", \"fi\": \"fi\", \"fl\": \"fl\" };\n\n/** The comparable form of a text: no whitespace, no soft hyphen, straight quotes, case and accents folded. */\nexport function fold(text: string): string {\n let out = \"\";\n for (const ch of text.normalize(\"NFKD\")) {\n if (/\\p{M}/u.test(ch) || /\\s/u.test(ch) || ch === \"­\" || ch === \"​\") continue;\n out += (FOLD[ch] ?? ch).toLowerCase();\n }\n return out;\n}\n\ntype Index = { text: string; at: { item: number; char: number }[] };\n\nfunction indexPage(items: readonly TextItem[]): Index {\n let text = \"\";\n const at: Index[\"at\"] = [];\n items.forEach((item, i) => {\n for (let c = 0; c < item.str.length; c++) {\n const piece = fold(item.str[c]!);\n for (let k = 0; k < piece.length; k++) {\n text += piece[k];\n at.push({ item: i, char: c });\n }\n }\n });\n return { text, at };\n}\n\nfunction boxesOf(page: PageText, index: Index, from: number, to: number): Box[] {\n const spans = new Map<number, [number, number]>();\n for (let k = from; k < to; k++) {\n const { item, char } = index.at[k]!;\n const s = spans.get(item);\n spans.set(item, s ? [Math.min(s[0], char), Math.max(s[1], char + 1)] : [char, char + 1]);\n }\n const raw: Box[] = [];\n for (const [i, [c0, c1]] of [...spans].sort((a, b) => a[0] - b[0])) {\n const item = page.items[i]!;\n const len = Math.max(1, item.str.length);\n const size = item.height || Math.abs(item.transform[3] ?? 0) || 10;\n const x = (item.transform[4] ?? 0) + (item.width * c0) / len;\n const w = (item.width * (c1 - c0)) / len;\n const top = page.height - ((item.transform[5] ?? 0) + size * 0.9);\n raw.push({ x: x / page.width, y: top / page.height, width: w / page.width, height: (size * 1.15) / page.height });\n }\n // Merge the pieces that sit on one line and touch (several items of one sentence) into one rectangle.\n const merged: Box[] = [];\n for (const b of raw) {\n const last = merged[merged.length - 1];\n const sameLine = last && Math.abs(last.y - b.y) < last.height * 0.5 && b.x - (last.x + last.width) < last.height * 1.5 && b.x >= last.x - 1e-9;\n if (last && sameLine) {\n const right = Math.max(last.x + last.width, b.x + b.width);\n last.y = Math.min(last.y, b.y);\n last.height = Math.max(last.height, b.height);\n last.width = right - last.x;\n } else merged.push({ ...b });\n }\n return merged.map((b) => ({ x: clamp01(b.x), y: clamp01(b.y), width: Math.min(b.width, 1 - clamp01(b.x)), height: Math.min(b.height, 1 - clamp01(b.y)) }));\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\n/** The shortest opening worth highlighting alone: a few words, not a stray letter. */\nconst MIN_START = 24;\n\n/**\n * Where `quote` sits in the pages: the first page (starting with `hintPage`, if any) whose text contains it, and the\n * boxes of its lines. A quote that a different layout cut (a column break, a hyphen) is looked for by its opening\n * words (`match: \"start\"`); one that is nowhere gives `null`, or the hinted page alone (`match: \"page\"`). Never a guess:\n * a rotated page has no boxes, only its page.\n */\nexport function locateQuote(pages: readonly PageText[], quote: string, hintPage?: number): Located | null {\n const needle = fold(quote);\n const ordered = hintPage ? [...pages].sort((a, b) => Number(b.page === hintPage) - Number(a.page === hintPage)) : pages;\n if (needle.length > 0) {\n for (const mode of [\"exact\", \"start\"] as const) {\n const wanted = mode === \"exact\" ? needle : needle.slice(0, Math.max(MIN_START, Math.min(40, Math.floor(needle.length * 0.6))));\n if (mode === \"start\" && (needle.length <= wanted.length || wanted.length < MIN_START)) break;\n for (const page of ordered) {\n const index = indexPage(page.items);\n const found = index.text.indexOf(wanted);\n if (found < 0) continue;\n return { page: page.page, boxes: boxesOf(page, index, found, found + wanted.length), match: mode };\n }\n }\n }\n return hintPage && pages.some((p) => p.page === hintPage) ? { page: hintPage, boxes: [], match: \"page\" } : null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/document-viewer/document-viewer.tsx","../src/document-viewer/locate.ts"],"sourcesContent":["// Client. A PDF drawn page by page (pdf.js), and passages of it lit where they sit. The app hands over the engine\n// (`loadPdfjs={() => import(\"pdfjs-dist\")}` — an optional peer, loaded only on a page that shows a document) and the\n// worker's address; it hands over the passages as quotes (`highlights`), and the viewer finds their place from the\n// PDF's own text positions (`locateQuote`): one box per line, in fractions of the page, so they follow any zoom.\n// `activeId` (controlled) scrolls to a passage and strengthens it; a click on a box calls `onActiveChange`. A passage\n// the text layer cannot place is never drawn at a guessed place: the viewer goes to its page when it is known and shows\n// the quote in a bar — and tells the app (`onLocate`) so its list can say \"page 3\" or \"not found\".\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { useLatest, usePrefersReducedMotion } from \"../media\";\nimport { type Located, locateQuote, type PageText } from \"./locate\";\n\nexport const documentViewer = tv({\n slots: {\n root: \"relative flex min-w-0 flex-col gap-3\",\n pages: \"flex max-h-(--kz-document-viewer-height,70vh) flex-col gap-3 overflow-auto rounded-lg border border-border-default bg-surface-2 p-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\",\n page: \"relative mx-auto w-full max-w-(--kz-document-viewer-page,56rem) shrink-0 overflow-hidden rounded-sm bg-white shadow-sm\",\n canvas: \"block h-auto w-full\",\n caption: \"absolute top-1 left-1 rounded-sm bg-surface-1/90 px-1.5 py-0.5 text-xs text-fg-secondary\",\n box: \"absolute cursor-pointer rounded-xs border border-warning/70 bg-warning/25 mix-blend-multiply transition-colors data-[active=true]:border-2 data-[active=true]:border-danger data-[active=true]:bg-danger/25 motion-reduce:transition-none\",\n bar: \"rounded-lg border border-border-default bg-surface-1 px-3 py-2 text-sm text-fg-secondary\",\n status: \"p-6 text-center text-sm text-fg-secondary\",\n },\n});\n\n/** The words of the viewer, passed translated. `{page}` and `{total}` are replaced. */\nexport type DocumentViewerLabels = {\n /** The accessible name of the pages' region. */\n region: string;\n loading: string;\n failed: string;\n /** A page's caption and accessible name: \"Page {page} of {total}\". */\n page: string;\n /** The bar above the pages when the active passage could only be placed on a page: \"Passage on page {page}:\". */\n onPage: string;\n /** The bar when the active passage is not in the text layer. */\n notFound: string;\n};\n\nexport const documentViewerLabels: DocumentViewerLabels = {\n region: \"Document pages\",\n loading: \"Loading the document…\",\n failed: \"The document could not be displayed.\",\n page: \"Page {page} of {total}\",\n onPage: \"Passage on page {page}:\",\n notFound: \"This passage is not in the document's text layer:\",\n};\n\n/** A passage to light: its quote (as the text layer reads it), the page if the app knows it, a name for screen readers. */\nexport type DocumentHighlight = { id: string; quote: string; page?: number; label?: string };\n\n/** What the viewer found for a passage. */\nexport type { Located };\n\n/** The members of pdf.js the viewer uses (`pdfjs-dist` ≥ 5), typed here so the package does not depend on it. */\nexport type PdfjsModule = {\n GlobalWorkerOptions: { workerSrc: string };\n getDocument(source: { url?: string; data?: Uint8Array }): { promise: Promise<PdfDocument>; destroy?: () => Promise<void> | void };\n};\nexport type PdfDocument = { numPages: number; getPage(n: number): Promise<PdfPage>; destroy?: () => Promise<void> | void };\nexport type PdfPage = {\n rotate: number;\n view: number[];\n getViewport(options: { scale: number }): { width: number; height: number };\n getTextContent(): Promise<{ items: readonly unknown[] }>;\n render(options: { canvas: HTMLCanvasElement; canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }): { promise: Promise<void>; cancel?: () => void };\n cleanup?: () => void;\n};\n\nexport type DocumentViewerProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"onChange\"> & {\n /** The PDF: an address (same origin or CORS) or its bytes. */\n src: string | Uint8Array;\n /** Loads pdf.js: `() => import(\"pdfjs-dist\")`. An optional peer, never in a bundle without a document. */\n loadPdfjs: () => Promise<PdfjsModule>;\n /** The address of pdf.js's worker (`new URL(\"pdfjs-dist/build/pdf.worker.min.mjs\", import.meta.url).toString()`). */\n workerSrc: string;\n /** The passages to light. */\n highlights?: readonly DocumentHighlight[];\n /** The passage in focus: the viewer scrolls to it. */\n activeId?: string | null;\n /** A box was clicked (the id) or the focus was released (`null`). */\n onActiveChange?: (id: string | null) => void;\n /** Where each passage was found (`null`: nowhere) — once the text layer is read, and again if `highlights` change. */\n onLocate?: (found: Record<string, Located | null>) => void;\n /** Called when the document is open, with its page count. */\n onLoad?: (info: { pages: number }) => void;\n /** The words of the viewer, translated (English by default). */\n labels?: Partial<DocumentViewerLabels>;\n};\n\nconst fill = (template: string, vars: Record<string, string | number>) => template.replace(/\\{(\\w+)\\}/g, (m, k: string) => (k in vars ? String(vars[k]) : m));\n\ntype Loaded = { doc: PdfDocument; texts: PageText[]; sizes: { width: number; height: number }[] };\ntype Phase = { state: \"loading\" } | { state: \"error\"; key: string } | ({ state: \"ready\"; key: string } & Loaded);\n\nconst isItem = (x: unknown): x is PageText[\"items\"][number] => typeof x === \"object\" && x !== null && typeof (x as { str?: unknown }).str === \"string\" && Array.isArray((x as { transform?: unknown }).transform);\n\nfunction PageCanvas({ page, doc, caption, onReady, className }: { page: number; doc: PdfDocument; caption: string; onReady?: () => void; className: string }) {\n const ref = React.useRef<HTMLCanvasElement>(null);\n const wrapper = React.useRef<HTMLDivElement>(null);\n const [visible, setVisible] = React.useState(() => typeof IntersectionObserver === \"undefined\");\n const ready = useLatest(onReady);\n React.useEffect(() => {\n const node = wrapper.current?.parentElement;\n if (!node) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && setVisible(true), { rootMargin: \"600px 0px\" });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n React.useEffect(() => {\n if (!visible) return;\n let task: { promise: Promise<void>; cancel?: () => void } | undefined;\n let cancelled = false;\n void (async () => {\n const p = await doc.getPage(page);\n const canvas = ref.current;\n const ctx = canvas?.getContext(\"2d\");\n if (cancelled || !canvas || !ctx) return;\n const base = p.getViewport({ scale: 1 });\n const dpr = typeof window === \"undefined\" ? 1 : Math.min(2, window.devicePixelRatio || 1);\n const scale = Math.min(2.5, Math.max(1, (canvas.parentElement?.clientWidth || base.width) / base.width)) * dpr;\n const viewport = p.getViewport({ scale });\n canvas.width = Math.floor(viewport.width);\n canvas.height = Math.floor(viewport.height);\n task = p.render({ canvas, canvasContext: ctx, viewport });\n await task.promise.catch(() => undefined);\n if (!cancelled) ready.current?.();\n })().catch(() => undefined);\n return () => {\n cancelled = true;\n task?.cancel?.();\n };\n }, [visible, doc, page, ready]);\n return (\n <div ref={wrapper} className=\"contents\">\n <canvas ref={ref} aria-hidden=\"true\" className={className} />\n <span className=\"sr-only\">{caption}</span>\n </div>\n );\n}\n\n/** A PDF with its passages lit — see the file's header. Client. */\nexport function DocumentViewer({ src, loadPdfjs, workerSrc, highlights = [], activeId = null, onActiveChange, onLocate, onLoad, labels, className, ...props }: DocumentViewerProps) {\n const words = { ...documentViewerLabels, ...labels };\n const s = documentViewer();\n const reduce = usePrefersReducedMotion();\n const [loaded, setPhase] = React.useState<Phase>({ state: \"loading\" });\n const callbacks = useLatest({ onLoad, onLocate });\n const containerRef = React.useRef<HTMLDivElement>(null);\n const bytes = typeof src === \"string\" ? src : src.byteLength + \":\" + src[0] + \":\" + src[src.length - 1];\n // A result belongs to the document it was read from: another `src` is loading again, with no reset inside the effect.\n const phase: Phase = loaded.state !== \"loading\" && loaded.key !== bytes ? { state: \"loading\" } : loaded;\n\n // Open the document and read every page's text layer (positions only; nothing is sent anywhere).\n React.useEffect(() => {\n let cancelled = false;\n let opened: { destroy?: () => Promise<void> | void } | undefined;\n void (async () => {\n const pdfjs = await loadPdfjs();\n pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;\n const loading = pdfjs.getDocument(typeof src === \"string\" ? { url: src } : { data: src.slice() });\n opened = loading;\n const doc = await loading.promise;\n const texts: PageText[] = [];\n const sizes: Loaded[\"sizes\"] = [];\n for (let n = 1; n <= doc.numPages; n++) {\n const page = await doc.getPage(n);\n const vp = page.getViewport({ scale: 1 });\n sizes.push({ width: vp.width, height: vp.height });\n const content = await page.getTextContent();\n // A rotated page has no reliable boxes: it keeps its size and an empty text layer (the app's page hint still works).\n texts.push({ page: n, width: vp.width, height: vp.height, items: page.rotate % 360 === 0 ? content.items.filter(isItem) : [] });\n page.cleanup?.();\n }\n if (cancelled) return;\n setPhase({ state: \"ready\", key: bytes, doc, texts, sizes });\n callbacks.current.onLoad?.({ pages: doc.numPages });\n })().catch(() => !cancelled && setPhase({ state: \"error\", key: bytes }));\n return () => {\n cancelled = true;\n void opened?.destroy?.();\n };\n // `src` is compared by content (`bytes`) so a re-render with the same bytes does not reopen the file.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bytes, loadPdfjs, workerSrc, callbacks]);\n\n const key = highlights.map((h) => `${h.id}\\u0000${h.page ?? \"\"}\\u0000${h.quote}`).join(\"\\u0001\");\n const found = React.useMemo(() => {\n const out: Record<string, Located | null> = {};\n if (phase.state === \"ready\") for (const h of highlights) out[h.id] = locateQuote(phase.texts, h.quote, h.page);\n return out;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [phase, key]);\n React.useEffect(() => {\n if (phase.state === \"ready\") callbacks.current.onLocate?.(found);\n }, [found, phase.state, callbacks]);\n\n // Go to the active passage.\n const active = activeId ? highlights.find((h) => h.id === activeId) : undefined;\n const where = activeId ? found[activeId] : undefined;\n React.useEffect(() => {\n if (phase.state !== \"ready\" || !where) return;\n const root = containerRef.current;\n const target = root?.querySelector<HTMLElement>(where.boxes.length > 0 ? `[data-highlight=\"${CSS.escape(activeId!)}\"]` : `[data-page=\"${where.page}\"]`) ?? null;\n target?.scrollIntoView?.({ block: where.boxes.length > 0 ? \"center\" : \"start\", behavior: reduce ? \"auto\" : \"smooth\" });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [activeId, where, phase.state]);\n\n return (\n <div className={s.root({ className })} {...props}>\n {active && where !== undefined && where?.match !== \"exact\" && where?.match !== \"start\" ? (\n <p className={s.bar()} data-testid=\"document-viewer-bar\">\n {where?.match === \"page\" ? fill(words.onPage, { page: where.page }) : words.notFound} <q>{active.quote}</q>\n </p>\n ) : null}\n <div ref={containerRef} className={s.pages()} role=\"region\" aria-label={words.region} tabIndex={0}>\n {phase.state === \"loading\" ? <p className={s.status()}>{words.loading}</p> : null}\n {phase.state === \"error\" ? <p role=\"alert\" className={s.status()}>{words.failed}</p> : null}\n {phase.state === \"ready\"\n ? phase.texts.map((t, i) => {\n const size = phase.sizes[i]!;\n const caption = fill(words.page, { page: t.page, total: phase.doc.numPages });\n return (\n <div key={t.page} className={s.page()} data-page={t.page} style={{ aspectRatio: `${size.width} / ${size.height}` }}>\n <PageCanvas page={t.page} doc={phase.doc} caption={caption} className={s.canvas()} />\n <span className={s.caption()} aria-hidden=\"true\">{caption}</span>\n {highlights.flatMap((h) => {\n const f = found[h.id];\n if (!f || f.page !== t.page) return [];\n return f.boxes.map((b, k) => (\n <div\n key={`${h.id}:${k}`}\n aria-hidden=\"true\"\n data-highlight={h.id}\n data-active={h.id === activeId}\n className={s.box()}\n style={{ left: `${b.x * 100}%`, top: `${b.y * 100}%`, width: `${b.width * 100}%`, height: `${b.height * 100}%` }}\n onClick={() => onActiveChange?.(h.id === activeId ? null : h.id)}\n />\n ));\n })}\n </div>\n );\n })\n : null}\n </div>\n </div>\n );\n}\n","// Pure: where a quoted passage sits on the pages of a PDF, from pdf.js's text positions. No DOM, no pdf.js import —\n// the viewer feeds it what `page.getTextContent()` returned, a test feeds it a fixture.\n\n/** A text item as pdf.js returns it (`TextItem`): the string, its matrix (`[a, b, c, d, x, baseline]`) and its size. */\nexport type TextItem = { str: string; transform: number[]; width: number; height: number };\n\n/** A rectangle in fractions of the page (0–1), origin top-left — independent of the zoom the page is drawn at. */\nexport type Box = { x: number; y: number; width: number; height: number };\n\n/** The text layer of one page: its size in PDF units (rotation 0) and its items in reading order. */\nexport type PageText = { page: number; width: number; height: number; items: readonly TextItem[] };\n\nexport type Located = {\n /** 1-based page where the passage starts. */\n page: number;\n /** One box per line of the passage (merged along a line); empty when only the page is known. */\n boxes: Box[];\n /** `exact`: the whole quote was found; `start`: only its opening words were (a quote cut by a different layout). */\n match: \"exact\" | \"start\" | \"page\";\n};\n\nconst FOLD: Record<string, string> = { \"’\": \"'\", \"‘\": \"'\", \"“\": '\"', \"”\": '\"', \"«\": '\"', \"»\": '\"', \"–\": \"-\", \"—\": \"-\", \" \": \" \", \" \": \" \", \"fi\": \"fi\", \"fl\": \"fl\" };\n\n/** The comparable form of a text: no whitespace, no soft hyphen, straight quotes, case and accents folded. */\nexport function fold(text: string): string {\n let out = \"\";\n for (const ch of text.normalize(\"NFKD\")) {\n if (/\\p{M}/u.test(ch) || /\\s/u.test(ch) || ch === \"­\" || ch === \"​\") continue;\n out += (FOLD[ch] ?? ch).toLowerCase();\n }\n return out;\n}\n\ntype Index = { text: string; at: { item: number; char: number }[] };\n\nfunction indexPage(items: readonly TextItem[]): Index {\n let text = \"\";\n const at: Index[\"at\"] = [];\n items.forEach((item, i) => {\n for (let c = 0; c < item.str.length; c++) {\n const piece = fold(item.str[c]!);\n for (let k = 0; k < piece.length; k++) {\n text += piece[k];\n at.push({ item: i, char: c });\n }\n }\n });\n return { text, at };\n}\n\n// Proportional faces: \"locataire\" is not as wide as nine capitals. pdf.js gives one width per item, so a position inside an\n// item is shared out by the Helvetica advance widths (per 1000 em) — close for any sans text, far better than a flat share.\nconst ADVANCE =\n \"278 278 355 556 556 889 667 191 333 333 389 584 278 333 278 278 556 556 556 556 556 556 556 556 556 556 278 278 584 584 584 556 1015 \" +\n \"667 667 722 722 667 611 778 722 278 500 667 556 833 722 778 667 778 722 667 611 722 667 944 667 667 611 278 278 278 469 556 333 \" +\n \"556 556 500 556 556 278 556 556 222 222 500 222 833 556 556 556 556 333 500 278 556 500 722 500 500 500\";\nconst ADVANCES = ADVANCE.split(\" \").map(Number);\nfunction advance(ch: string): number {\n const base = ch.normalize(\"NFD\")[0] ?? ch;\n const code = base.charCodeAt(0);\n return code >= 32 && code <= 126 ? (ADVANCES[code - 32] ?? 556) : 556;\n}\n/** The share (0–1) of an item's width that lies before character `n` of `text`. */\nfunction shareBefore(text: string, n: number): number {\n let before = 0;\n let total = 0;\n for (let i = 0; i < text.length; i++) {\n const a = advance(text[i]!);\n if (i < n) before += a;\n total += a;\n }\n return total > 0 ? before / total : n / Math.max(1, text.length);\n}\n\nfunction boxesOf(page: PageText, index: Index, from: number, to: number): Box[] {\n const spans = new Map<number, [number, number]>();\n for (let k = from; k < to; k++) {\n const { item, char } = index.at[k]!;\n const s = spans.get(item);\n spans.set(item, s ? [Math.min(s[0], char), Math.max(s[1], char + 1)] : [char, char + 1]);\n }\n const raw: Box[] = [];\n for (const [i, [c0, c1]] of [...spans].sort((a, b) => a[0] - b[0])) {\n const item = page.items[i]!;\n const size = item.height || Math.abs(item.transform[3] ?? 0) || 10;\n const x = (item.transform[4] ?? 0) + item.width * shareBefore(item.str, c0);\n const w = item.width * (shareBefore(item.str, c1) - shareBefore(item.str, c0));\n const top = page.height - ((item.transform[5] ?? 0) + size * 0.9);\n raw.push({ x: x / page.width, y: top / page.height, width: w / page.width, height: (size * 1.15) / page.height });\n }\n // Merge the pieces that sit on one line and touch (several items of one sentence) into one rectangle.\n const merged: Box[] = [];\n for (const b of raw) {\n const last = merged[merged.length - 1];\n const sameLine = last && Math.abs(last.y - b.y) < last.height * 0.5 && b.x - (last.x + last.width) < last.height * 1.5 && b.x >= last.x - 1e-9;\n if (last && sameLine) {\n const right = Math.max(last.x + last.width, b.x + b.width);\n last.y = Math.min(last.y, b.y);\n last.height = Math.max(last.height, b.height);\n last.width = right - last.x;\n } else merged.push({ ...b });\n }\n return merged.map((b) => ({ x: clamp01(b.x), y: clamp01(b.y), width: Math.min(b.width, 1 - clamp01(b.x)), height: Math.min(b.height, 1 - clamp01(b.y)) }));\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\n/** The shortest opening worth highlighting alone: a few words, not a stray letter. */\nconst MIN_START = 24;\n\n/**\n * Where `quote` sits in the pages: the first page (starting with `hintPage`, if any) whose text contains it, and the\n * boxes of its lines. A quote that a different layout cut (a column break, a hyphen) is looked for by its opening\n * words (`match: \"start\"`); one that is nowhere gives `null`, or the hinted page alone (`match: \"page\"`). Never a guess:\n * a rotated page has no boxes, only its page.\n */\nexport function locateQuote(pages: readonly PageText[], quote: string, hintPage?: number): Located | null {\n const needle = fold(quote);\n const ordered = hintPage ? [...pages].sort((a, b) => Number(b.page === hintPage) - Number(a.page === hintPage)) : pages;\n if (needle.length > 0) {\n for (const mode of [\"exact\", \"start\"] as const) {\n const wanted = mode === \"exact\" ? needle : needle.slice(0, Math.max(MIN_START, Math.min(40, Math.floor(needle.length * 0.6))));\n if (mode === \"start\" && (needle.length <= wanted.length || wanted.length < MIN_START)) break;\n for (const page of ordered) {\n const index = indexPage(page.items);\n const found = index.text.indexOf(wanted);\n if (found < 0) continue;\n return { page: page.page, boxes: boxesOf(page, index, found, found + wanted.length), match: mode };\n }\n }\n }\n return hintPage && pages.some((p) => p.page === hintPage) ? { page: hintPage, boxes: [], match: \"page\" } : null;\n}\n"],"mappings":"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","names":[]}
@@ -73,11 +73,12 @@ function ImageCropper({
73
73
  };
74
74
  const x = clamp(value.x, halfX, 1 - halfX);
75
75
  const y = clamp(value.y, halfY, 1 - halfY);
76
+ const pct = (n) => `${Math.round(n * 1e4) / 1e4}%`;
76
77
  const style = {
77
- width: `${widthPct}%`,
78
- height: `${heightPct}%`,
79
- left: `${50 - x * widthPct}%`,
80
- top: `${50 - y * heightPct}%`
78
+ width: pct(widthPct),
79
+ height: pct(heightPct),
80
+ left: pct(50 - x * widthPct),
81
+ top: pct(50 - y * heightPct)
81
82
  };
82
83
  return /* @__PURE__ */ jsxs("div", { className: s.root({ className }), children: [
83
84
  /* @__PURE__ */ jsxs(
@@ -137,6 +138,9 @@ function ImageCropper({
137
138
  draggable: false,
138
139
  className: s.media(),
139
140
  style,
141
+ ref: (img) => {
142
+ if (img?.complete && img.naturalWidth > 0) setLoaded((was) => was?.width === img.naturalWidth && was.height === img.naturalHeight ? was : { width: img.naturalWidth, height: img.naturalHeight });
143
+ },
140
144
  onLoad: (event) => setLoaded({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight })
141
145
  }
142
146
  ) : null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/image-cropper/image-cropper.tsx"],"sourcesContent":["// Client. A fixed frame (a profile picture's square, a vertical 9:16, a banner) over a picture or a video the person\n// drags and zooms to choose what stays in. The value is a focus point (0–1 on each axis) and a zoom — independent of\n// the screen, the same on the server — and `cropRect` turns it into pixels of the source. Keyboard: the frame takes the\n// focus, the arrows move (Shift: faster), + / − zoom; the zoom is also a slider. No motion but the person's own.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { clamp } from \"../media\";\nimport { Slider } from \"../slider\";\n\nexport const imageCropper = tv({\n slots: {\n root: \"flex w-full flex-col gap-3\",\n frame:\n \"relative w-full cursor-grab touch-none overflow-hidden rounded-xl bg-media active:cursor-grabbing \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0\",\n media: \"pointer-events-none absolute max-w-none select-none\",\n grid:\n \"pointer-events-none absolute inset-0 rounded-xl border-2 border-fg-on-media/80 \" +\n \"bg-[linear-gradient(to_right,transparent_33.2%,var(--kz-text-on-media)_33.3%,transparent_33.5%,transparent_66.5%,var(--kz-text-on-media)_66.6%,transparent_66.8%),linear-gradient(to_bottom,transparent_33.2%,var(--kz-text-on-media)_33.3%,transparent_33.5%,transparent_66.5%,var(--kz-text-on-media)_66.6%,transparent_66.8%)] opacity-40\",\n round: \"pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_var(--kz-scrim-strong)]\",\n },\n});\n\n/** Where the frame looks: the focus point (0–1 from the left and the top of the source) and the zoom (1 = covers). */\nexport type CropValue = { x: number; y: number; zoom: number };\n\n/** The crop in pixels of the source: what to cut from an image of `width` × `height` for a frame of `aspect`. */\nexport function cropRect(value: CropValue, source: { width: number; height: number }, aspect: number) {\n const mediaAspect = source.width / source.height;\n const width = (mediaAspect > aspect ? source.height * aspect : source.width) / value.zoom;\n const height = width / aspect;\n const x = clamp(value.x * source.width - width / 2, 0, source.width - width);\n const y = clamp(value.y * source.height - height / 2, 0, source.height - height);\n return { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };\n}\n\nexport type ImageCropperLabels = {\n /** The frame's accessible name (\"Crop area\"). */\n frame: string;\n /** How to use it with the keyboard, read with the frame (\"Use the arrow keys to move the picture, + and − to zoom.\"). */\n instructions: string;\n /** The zoom slider (\"Zoom\"). */\n zoom: string;\n};\n\nexport const imageCropperLabels: ImageCropperLabels = {\n frame: \"Crop area\",\n instructions: \"Drag or use the arrow keys to move the picture, + and − to zoom.\",\n zoom: \"Zoom\",\n};\n\nexport type ImageCropperProps = {\n /** The picture to crop. Or `children`: a `<video>` (with the class it is given through `mediaClassName`). */\n src?: string;\n /** The picture's alternative text, passed translated (empty when the frame's name says enough). */\n alt?: string;\n /** A media element instead of `src` — a `<video>` to choose the frame of a clip. Its natural size is in `naturalSize`. */\n children?: React.ReactElement<{ className?: string; style?: React.CSSProperties }>;\n /** The source's size when `children` is a video: `{ width: video.videoWidth, height: video.videoHeight }`. */\n naturalSize?: { width: number; height: number };\n /** The frame's width / height: 1 (a square), 9 / 16 (vertical), 3 (a banner). */\n aspect: number;\n /** A round mask over the square frame (a profile picture). */\n round?: boolean;\n /** The crop (controlled). */\n value?: CropValue;\n /** The crop at first, uncontrolled. Default: centred, no zoom. */\n defaultValue?: CropValue;\n /** Called on every move. */\n onValueChange?: (value: CropValue) => void;\n /** The largest zoom. Default 4. */\n maxZoom?: number;\n /** Shows the zoom slider under the frame. Default `true`. */\n showZoom?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<ImageCropperLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Chooses what a fixed frame keeps of a picture or a video: drag, zoom, keyboard; a focus point and a zoom out. */\nexport function ImageCropper({\n src,\n alt = \"\",\n children,\n naturalSize,\n aspect,\n round,\n value: valueProp,\n defaultValue = { x: 0.5, y: 0.5, zoom: 1 },\n onValueChange,\n maxZoom = 4,\n showZoom = true,\n labels: given,\n className,\n}: ImageCropperProps) {\n const labels = { ...imageCropperLabels, ...given };\n const s = imageCropper();\n const frame = React.useRef<HTMLDivElement>(null);\n const drag = React.useRef<{ x: number; y: number; from: CropValue } | null>(null);\n const [inner, setInner] = React.useState(defaultValue);\n const [loaded, setLoaded] = React.useState<{ width: number; height: number } | null>(null);\n const value = valueProp ?? inner;\n const id = React.useId();\n const size = naturalSize ?? loaded;\n const mediaAspect = size ? size.width / size.height : aspect;\n\n // The media's box, in % of the frame: it covers the frame at zoom 1.\n const widthPct = (mediaAspect > aspect ? (mediaAspect / aspect) * 100 : 100) * value.zoom;\n const heightPct = (mediaAspect > aspect ? 100 : (aspect / mediaAspect) * 100) * value.zoom;\n // The focus point's limits: the media never leaves a gap in the frame.\n const halfX = 50 / widthPct;\n const halfY = 50 / heightPct;\n\n const set = (next: CropValue) => {\n const zoom = clamp(next.zoom, 1, maxZoom);\n const wPct = (mediaAspect > aspect ? (mediaAspect / aspect) * 100 : 100) * zoom;\n const hPct = (mediaAspect > aspect ? 100 : (aspect / mediaAspect) * 100) * zoom;\n const fixed = { x: clamp(next.x, 50 / wPct, 1 - 50 / wPct), y: clamp(next.y, 50 / hPct, 1 - 50 / hPct), zoom };\n if (!valueProp) setInner(fixed);\n onValueChange?.(fixed);\n };\n\n const x = clamp(value.x, halfX, 1 - halfX);\n const y = clamp(value.y, halfY, 1 - halfY);\n const style: React.CSSProperties = {\n width: `${widthPct}%`,\n height: `${heightPct}%`,\n left: `${50 - x * widthPct}%`,\n top: `${50 - y * heightPct}%`,\n };\n\n return (\n <div className={s.root({ className })}>\n <div\n ref={frame}\n role=\"group\"\n tabIndex={0}\n aria-label={labels.frame}\n aria-describedby={`${id}-help`}\n className={s.frame()}\n style={{ aspectRatio: String(aspect) }}\n onPointerDown={(event) => {\n frame.current?.setPointerCapture?.(event.pointerId);\n drag.current = { x: event.clientX, y: event.clientY, from: { x, y, zoom: value.zoom } };\n }}\n onPointerMove={(event) => {\n const start = drag.current;\n const box = frame.current?.getBoundingClientRect();\n if (!start || !box?.width) return;\n set({\n x: start.from.x - (event.clientX - start.x) / ((box.width * widthPct) / 100),\n y: start.from.y - (event.clientY - start.y) / ((box.height * heightPct) / 100),\n zoom: value.zoom,\n });\n }}\n onPointerUp={() => {\n drag.current = null;\n }}\n onWheel={(event) => {\n if (!event.ctrlKey && !event.metaKey) return;\n event.preventDefault();\n set({ x, y, zoom: value.zoom - event.deltaY * 0.01 });\n }}\n onKeyDown={(event) => {\n const nudge = event.shiftKey ? 0.1 : 0.02;\n const moves: Record<string, CropValue> = {\n ArrowLeft: { x: x - nudge, y, zoom: value.zoom },\n ArrowRight: { x: x + nudge, y, zoom: value.zoom },\n ArrowUp: { x, y: y - nudge, zoom: value.zoom },\n ArrowDown: { x, y: y + nudge, zoom: value.zoom },\n \"+\": { x, y, zoom: value.zoom + 0.1 },\n \"=\": { x, y, zoom: value.zoom + 0.1 },\n \"-\": { x, y, zoom: value.zoom - 0.1 },\n };\n const next = moves[event.key];\n if (!next) return;\n event.preventDefault();\n set(next);\n }}\n >\n {children ? (\n React.cloneElement(children, { className: s.media({ className: children.props.className }), style: { ...children.props.style, ...style } })\n ) : src ? (\n <img\n src={src}\n alt={alt}\n draggable={false}\n className={s.media()}\n style={style}\n onLoad={(event) => setLoaded({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight })}\n />\n ) : null}\n <span className={round ? s.round() : s.grid()} aria-hidden />\n <span id={`${id}-help`} className=\"sr-only\">\n {labels.instructions}\n </span>\n </div>\n {showZoom && (\n <Slider\n label={labels.zoom}\n min={1}\n max={maxZoom}\n step={0.05}\n value={value.zoom}\n onValueChange={(zoom) => set({ x, y, zoom })}\n formatValue={(v) => `${Math.round(v * 100)} %`}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/image-cropper/image-cropper.tsx"],"sourcesContent":["// Client. A fixed frame (a profile picture's square, a vertical 9:16, a banner) over a picture or a video the person\n// drags and zooms to choose what stays in. The value is a focus point (0–1 on each axis) and a zoom — independent of\n// the screen, the same on the server — and `cropRect` turns it into pixels of the source. Keyboard: the frame takes the\n// focus, the arrows move (Shift: faster), + / − zoom; the zoom is also a slider. No motion but the person's own.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { clamp } from \"../media\";\nimport { Slider } from \"../slider\";\n\nexport const imageCropper = tv({\n slots: {\n root: \"flex w-full flex-col gap-3\",\n frame:\n \"relative w-full cursor-grab touch-none overflow-hidden rounded-xl bg-media active:cursor-grabbing \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0\",\n media: \"pointer-events-none absolute max-w-none select-none\",\n grid:\n \"pointer-events-none absolute inset-0 rounded-xl border-2 border-fg-on-media/80 \" +\n \"bg-[linear-gradient(to_right,transparent_33.2%,var(--kz-text-on-media)_33.3%,transparent_33.5%,transparent_66.5%,var(--kz-text-on-media)_66.6%,transparent_66.8%),linear-gradient(to_bottom,transparent_33.2%,var(--kz-text-on-media)_33.3%,transparent_33.5%,transparent_66.5%,var(--kz-text-on-media)_66.6%,transparent_66.8%)] opacity-40\",\n round: \"pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_var(--kz-scrim-strong)]\",\n },\n});\n\n/** Where the frame looks: the focus point (0–1 from the left and the top of the source) and the zoom (1 = covers). */\nexport type CropValue = { x: number; y: number; zoom: number };\n\n/** The crop in pixels of the source: what to cut from an image of `width` × `height` for a frame of `aspect`. */\nexport function cropRect(value: CropValue, source: { width: number; height: number }, aspect: number) {\n const mediaAspect = source.width / source.height;\n const width = (mediaAspect > aspect ? source.height * aspect : source.width) / value.zoom;\n const height = width / aspect;\n const x = clamp(value.x * source.width - width / 2, 0, source.width - width);\n const y = clamp(value.y * source.height - height / 2, 0, source.height - height);\n return { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };\n}\n\nexport type ImageCropperLabels = {\n /** The frame's accessible name (\"Crop area\"). */\n frame: string;\n /** How to use it with the keyboard, read with the frame (\"Use the arrow keys to move the picture, + and − to zoom.\"). */\n instructions: string;\n /** The zoom slider (\"Zoom\"). */\n zoom: string;\n};\n\nexport const imageCropperLabels: ImageCropperLabels = {\n frame: \"Crop area\",\n instructions: \"Drag or use the arrow keys to move the picture, + and − to zoom.\",\n zoom: \"Zoom\",\n};\n\nexport type ImageCropperProps = {\n /** The picture to crop. Or `children`: a `<video>` (with the class it is given through `mediaClassName`). */\n src?: string;\n /** The picture's alternative text, passed translated (empty when the frame's name says enough). */\n alt?: string;\n /** A media element instead of `src` — a `<video>` to choose the frame of a clip. Its natural size is in `naturalSize`. */\n children?: React.ReactElement<{ className?: string; style?: React.CSSProperties }>;\n /** The source's size when `children` is a video: `{ width: video.videoWidth, height: video.videoHeight }`. */\n naturalSize?: { width: number; height: number };\n /** The frame's width / height: 1 (a square), 9 / 16 (vertical), 3 (a banner). */\n aspect: number;\n /** A round mask over the square frame (a profile picture). */\n round?: boolean;\n /** The crop (controlled). */\n value?: CropValue;\n /** The crop at first, uncontrolled. Default: centred, no zoom. */\n defaultValue?: CropValue;\n /** Called on every move. */\n onValueChange?: (value: CropValue) => void;\n /** The largest zoom. Default 4. */\n maxZoom?: number;\n /** Shows the zoom slider under the frame. Default `true`. */\n showZoom?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<ImageCropperLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Chooses what a fixed frame keeps of a picture or a video: drag, zoom, keyboard; a focus point and a zoom out. */\nexport function ImageCropper({\n src,\n alt = \"\",\n children,\n naturalSize,\n aspect,\n round,\n value: valueProp,\n defaultValue = { x: 0.5, y: 0.5, zoom: 1 },\n onValueChange,\n maxZoom = 4,\n showZoom = true,\n labels: given,\n className,\n}: ImageCropperProps) {\n const labels = { ...imageCropperLabels, ...given };\n const s = imageCropper();\n const frame = React.useRef<HTMLDivElement>(null);\n const drag = React.useRef<{ x: number; y: number; from: CropValue } | null>(null);\n const [inner, setInner] = React.useState(defaultValue);\n const [loaded, setLoaded] = React.useState<{ width: number; height: number } | null>(null);\n const value = valueProp ?? inner;\n const id = React.useId();\n const size = naturalSize ?? loaded;\n const mediaAspect = size ? size.width / size.height : aspect;\n\n // The media's box, in % of the frame: it covers the frame at zoom 1.\n const widthPct = (mediaAspect > aspect ? (mediaAspect / aspect) * 100 : 100) * value.zoom;\n const heightPct = (mediaAspect > aspect ? 100 : (aspect / mediaAspect) * 100) * value.zoom;\n // The focus point's limits: the media never leaves a gap in the frame.\n const halfX = 50 / widthPct;\n const halfY = 50 / heightPct;\n\n const set = (next: CropValue) => {\n const zoom = clamp(next.zoom, 1, maxZoom);\n const wPct = (mediaAspect > aspect ? (mediaAspect / aspect) * 100 : 100) * zoom;\n const hPct = (mediaAspect > aspect ? 100 : (aspect / mediaAspect) * 100) * zoom;\n const fixed = { x: clamp(next.x, 50 / wPct, 1 - 50 / wPct), y: clamp(next.y, 50 / hPct, 1 - 50 / hPct), zoom };\n if (!valueProp) setInner(fixed);\n onValueChange?.(fixed);\n };\n\n const x = clamp(value.x, halfX, 1 - halfX);\n const y = clamp(value.y, halfY, 1 - halfY);\n // Rounded: float noise (5e-15) is not a length every parser takes, and a dropped `top` is a gap.\n const pct = (n: number) => `${Math.round(n * 1e4) / 1e4}%`;\n const style: React.CSSProperties = {\n width: pct(widthPct),\n height: pct(heightPct),\n left: pct(50 - x * widthPct),\n top: pct(50 - y * heightPct),\n };\n\n return (\n <div className={s.root({ className })}>\n <div\n ref={frame}\n role=\"group\"\n tabIndex={0}\n aria-label={labels.frame}\n aria-describedby={`${id}-help`}\n className={s.frame()}\n style={{ aspectRatio: String(aspect) }}\n onPointerDown={(event) => {\n frame.current?.setPointerCapture?.(event.pointerId);\n drag.current = { x: event.clientX, y: event.clientY, from: { x, y, zoom: value.zoom } };\n }}\n onPointerMove={(event) => {\n const start = drag.current;\n const box = frame.current?.getBoundingClientRect();\n if (!start || !box?.width) return;\n set({\n x: start.from.x - (event.clientX - start.x) / ((box.width * widthPct) / 100),\n y: start.from.y - (event.clientY - start.y) / ((box.height * heightPct) / 100),\n zoom: value.zoom,\n });\n }}\n onPointerUp={() => {\n drag.current = null;\n }}\n onWheel={(event) => {\n if (!event.ctrlKey && !event.metaKey) return;\n event.preventDefault();\n set({ x, y, zoom: value.zoom - event.deltaY * 0.01 });\n }}\n onKeyDown={(event) => {\n const nudge = event.shiftKey ? 0.1 : 0.02;\n const moves: Record<string, CropValue> = {\n ArrowLeft: { x: x - nudge, y, zoom: value.zoom },\n ArrowRight: { x: x + nudge, y, zoom: value.zoom },\n ArrowUp: { x, y: y - nudge, zoom: value.zoom },\n ArrowDown: { x, y: y + nudge, zoom: value.zoom },\n \"+\": { x, y, zoom: value.zoom + 0.1 },\n \"=\": { x, y, zoom: value.zoom + 0.1 },\n \"-\": { x, y, zoom: value.zoom - 0.1 },\n };\n const next = moves[event.key];\n if (!next) return;\n event.preventDefault();\n set(next);\n }}\n >\n {children ? (\n React.cloneElement(children, { className: s.media({ className: children.props.className }), style: { ...children.props.style, ...style } })\n ) : src ? (\n <img\n src={src}\n alt={alt}\n draggable={false}\n className={s.media()}\n style={style}\n // A picture already in the cache (or rendered on the server) is complete before React attaches: no load event.\n ref={(img) => {\n if (img?.complete && img.naturalWidth > 0) setLoaded((was) => (was?.width === img.naturalWidth && was.height === img.naturalHeight ? was : { width: img.naturalWidth, height: img.naturalHeight }));\n }}\n onLoad={(event) => setLoaded({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight })}\n />\n ) : null}\n <span className={round ? s.round() : s.grid()} aria-hidden />\n <span id={`${id}-help`} className=\"sr-only\">\n {labels.instructions}\n </span>\n </div>\n {showZoom && (\n <Slider\n label={labels.zoom}\n min={1}\n max={maxZoom}\n step={0.05}\n value={value.zoom}\n onValueChange={(zoom) => set({ x, y, zoom })}\n formatValue={(v) => `${Math.round(v * 100)} %`}\n />\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAIA,YAAY,WAAW;AACvB,SAAS,UAAU;AAoIb,SAkDI,KAlDJ;AA/HC,IAAM,eAAe,GAAG;AAAA,EAC7B,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OACE;AAAA,IAEF,OAAO;AAAA,IACP,MACE;AAAA,IAEF,OAAO;AAAA,EACT;AACF,CAAC;AAMM,SAAS,SAAS,OAAkB,QAA2C,QAAgB;AACpG,QAAM,cAAc,OAAO,QAAQ,OAAO;AAC1C,QAAM,SAAS,cAAc,SAAS,OAAO,SAAS,SAAS,OAAO,SAAS,MAAM;AACrF,QAAM,SAAS,QAAQ;AACvB,QAAM,IAAI,MAAM,MAAM,IAAI,OAAO,QAAQ,QAAQ,GAAG,GAAG,OAAO,QAAQ,KAAK;AAC3E,QAAM,IAAI,MAAM,MAAM,IAAI,OAAO,SAAS,SAAS,GAAG,GAAG,OAAO,SAAS,MAAM;AAC/E,SAAO,EAAE,GAAG,KAAK,MAAM,CAAC,GAAG,GAAG,KAAK,MAAM,CAAC,GAAG,OAAO,KAAK,MAAM,KAAK,GAAG,QAAQ,KAAK,MAAM,MAAM,EAAE;AACpG;AAWO,IAAM,qBAAyC;AAAA,EACpD,OAAO;AAAA,EACP,cAAc;AAAA,EACd,MAAM;AACR;AAgCO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,eAAe,EAAE,GAAG,KAAK,GAAG,KAAK,MAAM,EAAE;AAAA,EACzC;AAAA,EACA,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AACF,GAAsB;AACpB,QAAM,SAAS,EAAE,GAAG,oBAAoB,GAAG,MAAM;AACjD,QAAM,IAAI,aAAa;AACvB,QAAM,QAAc,aAAuB,IAAI;AAC/C,QAAM,OAAa,aAAyD,IAAI;AAChF,QAAM,CAAC,OAAO,QAAQ,IAAU,eAAS,YAAY;AACrD,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAmD,IAAI;AACzF,QAAM,QAAQ,aAAa;AAC3B,QAAM,KAAW,YAAM;AACvB,QAAM,OAAO,eAAe;AAC5B,QAAM,cAAc,OAAO,KAAK,QAAQ,KAAK,SAAS;AAGtD,QAAM,YAAY,cAAc,SAAU,cAAc,SAAU,MAAM,OAAO,MAAM;AACrF,QAAM,aAAa,cAAc,SAAS,MAAO,SAAS,cAAe,OAAO,MAAM;AAEtF,QAAM,QAAQ,KAAK;AACnB,QAAM,QAAQ,KAAK;AAEnB,QAAM,MAAM,CAAC,SAAoB;AAC/B,UAAM,OAAO,MAAM,KAAK,MAAM,GAAG,OAAO;AACxC,UAAM,QAAQ,cAAc,SAAU,cAAc,SAAU,MAAM,OAAO;AAC3E,UAAM,QAAQ,cAAc,SAAS,MAAO,SAAS,cAAe,OAAO;AAC3E,UAAM,QAAQ,EAAE,GAAG,MAAM,KAAK,GAAG,KAAK,MAAM,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,KAAK,GAAG,KAAK,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK;AAC7G,QAAI,CAAC,UAAW,UAAS,KAAK;AAC9B,oBAAgB,KAAK;AAAA,EACvB;AAEA,QAAM,IAAI,MAAM,MAAM,GAAG,OAAO,IAAI,KAAK;AACzC,QAAM,IAAI,MAAM,MAAM,GAAG,OAAO,IAAI,KAAK;AAEzC,QAAM,MAAM,CAAC,MAAc,GAAG,KAAK,MAAM,IAAI,GAAG,IAAI,GAAG;AACvD,QAAM,QAA6B;AAAA,IACjC,OAAO,IAAI,QAAQ;AAAA,IACnB,QAAQ,IAAI,SAAS;AAAA,IACrB,MAAM,IAAI,KAAK,IAAI,QAAQ;AAAA,IAC3B,KAAK,IAAI,KAAK,IAAI,SAAS;AAAA,EAC7B;AAEA,SACE,qBAAC,SAAI,WAAW,EAAE,KAAK,EAAE,UAAU,CAAC,GAClC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,UAAU;AAAA,QACV,cAAY,OAAO;AAAA,QACnB,oBAAkB,GAAG,EAAE;AAAA,QACvB,WAAW,EAAE,MAAM;AAAA,QACnB,OAAO,EAAE,aAAa,OAAO,MAAM,EAAE;AAAA,QACrC,eAAe,CAAC,UAAU;AACxB,gBAAM,SAAS,oBAAoB,MAAM,SAAS;AAClD,eAAK,UAAU,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,SAAS,MAAM,EAAE,GAAG,GAAG,MAAM,MAAM,KAAK,EAAE;AAAA,QACxF;AAAA,QACA,eAAe,CAAC,UAAU;AACxB,gBAAM,QAAQ,KAAK;AACnB,gBAAM,MAAM,MAAM,SAAS,sBAAsB;AACjD,cAAI,CAAC,SAAS,CAAC,KAAK,MAAO;AAC3B,cAAI;AAAA,YACF,GAAG,MAAM,KAAK,KAAK,MAAM,UAAU,MAAM,MAAO,IAAI,QAAQ,WAAY;AAAA,YACxE,GAAG,MAAM,KAAK,KAAK,MAAM,UAAU,MAAM,MAAO,IAAI,SAAS,YAAa;AAAA,YAC1E,MAAM,MAAM;AAAA,UACd,CAAC;AAAA,QACH;AAAA,QACA,aAAa,MAAM;AACjB,eAAK,UAAU;AAAA,QACjB;AAAA,QACA,SAAS,CAAC,UAAU;AAClB,cAAI,CAAC,MAAM,WAAW,CAAC,MAAM,QAAS;AACtC,gBAAM,eAAe;AACrB,cAAI,EAAE,GAAG,GAAG,MAAM,MAAM,OAAO,MAAM,SAAS,KAAK,CAAC;AAAA,QACtD;AAAA,QACA,WAAW,CAAC,UAAU;AACpB,gBAAM,QAAQ,MAAM,WAAW,MAAM;AACrC,gBAAM,QAAmC;AAAA,YACvC,WAAW,EAAE,GAAG,IAAI,OAAO,GAAG,MAAM,MAAM,KAAK;AAAA,YAC/C,YAAY,EAAE,GAAG,IAAI,OAAO,GAAG,MAAM,MAAM,KAAK;AAAA,YAChD,SAAS,EAAE,GAAG,GAAG,IAAI,OAAO,MAAM,MAAM,KAAK;AAAA,YAC7C,WAAW,EAAE,GAAG,GAAG,IAAI,OAAO,MAAM,MAAM,KAAK;AAAA,YAC/C,KAAK,EAAE,GAAG,GAAG,MAAM,MAAM,OAAO,IAAI;AAAA,YACpC,KAAK,EAAE,GAAG,GAAG,MAAM,MAAM,OAAO,IAAI;AAAA,YACpC,KAAK,EAAE,GAAG,GAAG,MAAM,MAAM,OAAO,IAAI;AAAA,UACtC;AACA,gBAAM,OAAO,MAAM,MAAM,GAAG;AAC5B,cAAI,CAAC,KAAM;AACX,gBAAM,eAAe;AACrB,cAAI,IAAI;AAAA,QACV;AAAA,QAEC;AAAA,qBACO,mBAAa,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,SAAS,MAAM,UAAU,CAAC,GAAG,OAAO,EAAE,GAAG,SAAS,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,IACxI,MACF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,WAAW,EAAE,MAAM;AAAA,cACnB;AAAA,cAEA,KAAK,CAAC,QAAQ;AACZ,oBAAI,KAAK,YAAY,IAAI,eAAe,EAAG,WAAU,CAAC,QAAS,KAAK,UAAU,IAAI,gBAAgB,IAAI,WAAW,IAAI,gBAAgB,MAAM,EAAE,OAAO,IAAI,cAAc,QAAQ,IAAI,cAAc,CAAE;AAAA,cACpM;AAAA,cACA,QAAQ,CAAC,UAAU,UAAU,EAAE,OAAO,MAAM,cAAc,cAAc,QAAQ,MAAM,cAAc,cAAc,CAAC;AAAA;AAAA,UACrH,IACE;AAAA,UACJ,oBAAC,UAAK,WAAW,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,GAAG,eAAW,MAAC;AAAA,UAC3D,oBAAC,UAAK,IAAI,GAAG,EAAE,SAAS,WAAU,WAC/B,iBAAO,cACV;AAAA;AAAA;AAAA,IACF;AAAA,IACC,YACC;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,OAAO;AAAA,QACd,KAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM;AAAA,QACN,OAAO,MAAM;AAAA,QACb,eAAe,CAAC,SAAS,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC;AAAA,QAC3C,aAAa,CAAC,MAAM,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC;AAAA;AAAA,IAC5C;AAAA,KAEJ;AAEJ;","names":[]}
package/dist/index.d.ts CHANGED
@@ -56,14 +56,9 @@ declare function ProductLogo({ id, ...props }: MarkProps & {
56
56
  }): React.JSX.Element;
57
57
 
58
58
  /**
59
- * The Wu-sama mark — the founder's byline mark, for the person and not the company (the company keeps
60
- * `KrizakaLogo`). Oussama's nickname is Wu-sama: a W of five straight legs in gold inside the Krizaka hexagon, split
61
- * at its top and bottom vertices, a small lit node at the middle summit. Still: no orbit, no pulse. The hexagon and the
62
- * node read the host's text role, so they are ink in both themes; the gold is one mid-tone amber that reads on both.
63
- *
64
- * **Original mark, inspired by, not derived from**: it nods to the culture its name comes from but is built from the
65
- * Krizaka hexagon's own measures (see `wu-sama-geometry.ts` and its tests), never from the Wu-Tang Clan's registered
66
- * logo — no bat-wing W, no curve, no yellow-on-black copy. Below 48 px it crops to the emblem.
59
+ * The Wu-sama mark — the founder's byline mark, for the person and not the company (the company keeps `KrizakaLogo`).
60
+ * Oussama's nickname is Wu-sama: it shares Krizaka's hexagonal shield contour and color palette (ink shield, open
61
+ * vertex with lit blue node), with the W rendered in Krizaka blue at the center. Below 48 px it crops to the emblem.
67
62
  */
68
63
  declare function WuSamaMark({ size, title, className }: Omit<MarkProps, "animated"> & {
69
64
  animated?: boolean;