@krizaka/ui 3.26.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.
@@ -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";
@@ -92,4 +92,4 @@ export {
92
92
  sitesByGroup,
93
93
  familyLabels
94
94
  };
95
- //# sourceMappingURL=chunk-BCGJZ5PG.js.map
95
+ //# sourceMappingURL=chunk-OUWPBSSL.js.map
@@ -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":"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","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;
package/dist/index.js CHANGED
@@ -18,63 +18,59 @@ import {
18
18
  YomizakaLogo,
19
19
  YororisLogo,
20
20
  a11y,
21
- neutral
22
- } from "./chunk-4DH2HW3D.js";
21
+ neutral,
22
+ useIds
23
+ } from "./chunk-TFJDIOFK.js";
23
24
 
24
25
  // src/marks/wu-sama-geometry.ts
25
- var round = (n) => Math.round(n * 10) / 10;
26
- var center = 200;
27
- var radius = 150;
28
- var u = radius * Math.sqrt(3) / 2;
29
- var gap = 10;
30
- var hexagon = [
31
- [center, center - radius],
32
- [round(center + u), center - radius / 2],
33
- [round(center + u), center + radius / 2],
34
- [center, center + radius],
35
- [round(center - u), center + radius / 2],
36
- [round(center - u), center - radius / 2]
37
- ];
38
- var [T, UR, LR, B, LL, UL] = hexagon;
39
- var onEdge = (a, b, x) => [x, round(a[1] + (x - a[0]) / (b[0] - a[0]) * (b[1] - a[1]))];
40
- var pt = ([x, y]) => `${x},${y}`;
41
- var left = [onEdge(T, UL, center - gap), UL, LL, onEdge(B, LL, center - gap)];
42
- var right = [onEdge(T, UR, center + gap), UR, LR, onEdge(B, LR, center + gap)];
43
- var wPoints = [
44
- [round(center - 0.63 * u), round(center - 0.4 * radius)],
45
- [round(center - 0.37 * u), round(center + 0.55 * radius)],
46
- [center, round(center - 0.03 * radius)],
47
- [round(center + 0.37 * u), round(center + 0.55 * radius)],
48
- [round(center + 0.63 * u), round(center - 0.4 * radius)]
49
- ];
50
- var peak = wPoints[2];
26
+ import { marks } from "@krizaka/tokens/native";
51
27
  var WU_SAMA = {
52
28
  viewBox: "0 0 400 400",
53
- croppedViewBox: "34 34 332 332",
54
- center,
55
- radius,
56
- gap,
57
- hexagon,
58
- /** The two halves of the hexagon (open at the top and bottom vertices), stroked in ink. */
59
- shell: { left: `M${left.map(pt).join(" L")}`, right: `M${right.map(pt).join(" L")}`, stroke: 22 },
60
- /** The W, stroked in gold. */
61
- w: `M${wPoints.map(pt).join(" L")}`,
62
- wStroke: 26,
63
- /** The node: the family's lit node, a small diamond at the W's middle summit, in ink. */
64
- node: `M${center},${round(peak[1] - 24)} L${center + 18},${peak[1]} L${center},${round(peak[1] + 24)} L${center - 18},${peak[1]}Z`,
65
- /** One mid-tone amber, hsl(40 94% 37%): 3.3:1 on white and 5.8:1 on the dark surface. */
66
- gold: "hsl(40 94% 37%)"
29
+ croppedViewBox: "86 82 228 228",
30
+ offset: 20,
31
+ /** The shield: the Krizaka pointy-top hexagon (r 92 around 180,180), open at its upper-right edge. */
32
+ shield: "M259.7,134 L259.7,226 L180,272 L100.3,226 L100.3,134 L180,88 L212,106.5",
33
+ stroke: 22,
34
+ ink: { x1: 70, y1: 290, x2: 290, y2: 70 },
35
+ blue: { x1: 180, y1: 180, x2: 300, y2: 60, stops: marks.krizaka },
36
+ /** The vertex where the shield opens: a lit node in Krizaka blue. */
37
+ node: { cx: 259.7, cy: 134, r: 19, inner: 6 },
38
+ /** The W: five straight legs in Krizaka blue. */
39
+ w: "M132,142 L154,216 L180,166 L206,216 L228,142",
40
+ wStroke: 18,
41
+ highlight: "#dbeafe",
42
+ glow: { color: "#196df5", opacity: 0.16 },
43
+ orbit: { r: 160, dash: "8 8", width: 1.5 },
44
+ ring: { r: 134, width: 1 }
67
45
  };
68
46
 
69
47
  // src/marks/WuSamaMark.tsx
70
- import { jsx, jsxs } from "react/jsx-runtime";
48
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
71
49
  function WuSamaMark({ size = 32, title, className }) {
72
- const ink = neutral("--kz-text-primary", 92);
50
+ const id = useIds("wsm", ["ink", "blue", "glow"]);
73
51
  return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: size < 48 ? WU_SAMA.croppedViewBox : WU_SAMA.viewBox, fill: "none", xmlns: "http://www.w3.org/2000/svg", className, ...a11y(title), children: [
74
- /* @__PURE__ */ jsx("path", { d: WU_SAMA.shell.left, stroke: ink, strokeWidth: WU_SAMA.shell.stroke, strokeLinejoin: "miter", strokeLinecap: "butt" }),
75
- /* @__PURE__ */ jsx("path", { d: WU_SAMA.shell.right, stroke: ink, strokeWidth: WU_SAMA.shell.stroke, strokeLinejoin: "miter", strokeLinecap: "butt" }),
76
- /* @__PURE__ */ jsx("path", { d: WU_SAMA.w, stroke: WU_SAMA.gold, strokeWidth: WU_SAMA.wStroke, strokeLinejoin: "miter", strokeMiterlimit: 4, strokeLinecap: "butt" }),
77
- /* @__PURE__ */ jsx("path", { d: WU_SAMA.node, fill: ink })
52
+ /* @__PURE__ */ jsxs("defs", { children: [
53
+ /* @__PURE__ */ jsxs("linearGradient", { id: id.ink, gradientUnits: "userSpaceOnUse", x1: WU_SAMA.ink.x1, y1: WU_SAMA.ink.y1, x2: WU_SAMA.ink.x2, y2: WU_SAMA.ink.y2, children: [
54
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: neutral("--kz-text-secondary", 60) }),
55
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: neutral("--kz-text-primary", 92) })
56
+ ] }),
57
+ /* @__PURE__ */ jsx("linearGradient", { id: id.blue, gradientUnits: "userSpaceOnUse", x1: WU_SAMA.blue.x1, y1: WU_SAMA.blue.y1, x2: WU_SAMA.blue.x2, y2: WU_SAMA.blue.y2, children: WU_SAMA.blue.stops.map(([offset, color]) => /* @__PURE__ */ jsx("stop", { offset, stopColor: color }, offset)) }),
58
+ /* @__PURE__ */ jsxs("radialGradient", { id: id.glow, cx: "50%", cy: "50%", r: "50%", children: [
59
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: WU_SAMA.glow.color, stopOpacity: WU_SAMA.glow.opacity }),
60
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: WU_SAMA.glow.color, stopOpacity: "0" })
61
+ ] })
62
+ ] }),
63
+ size >= 48 ? /* @__PURE__ */ jsx("circle", { cx: "200", cy: "200", r: "190", fill: `url(#${id.glow})` }) : null,
64
+ size >= 48 ? /* @__PURE__ */ jsxs(Fragment, { children: [
65
+ /* @__PURE__ */ jsx("circle", { cx: "200", cy: "200", r: WU_SAMA.orbit.r, stroke: neutral("--kz-border-strong", 40), strokeWidth: WU_SAMA.orbit.width, strokeDasharray: WU_SAMA.orbit.dash, opacity: ".6" }),
66
+ /* @__PURE__ */ jsx("circle", { cx: "200", cy: "200", r: WU_SAMA.ring.r, stroke: neutral("--kz-border-default", 30), strokeWidth: WU_SAMA.ring.width, opacity: ".35" })
67
+ ] }) : null,
68
+ /* @__PURE__ */ jsxs("g", { transform: `translate(${WU_SAMA.offset} ${WU_SAMA.offset})`, children: [
69
+ /* @__PURE__ */ jsx("path", { d: WU_SAMA.shield, stroke: `url(#${id.ink})`, strokeWidth: WU_SAMA.stroke, strokeLinecap: "round", strokeLinejoin: "round" }),
70
+ /* @__PURE__ */ jsx("circle", { cx: WU_SAMA.node.cx, cy: WU_SAMA.node.cy, r: WU_SAMA.node.r, fill: `url(#${id.blue})` }),
71
+ /* @__PURE__ */ jsx("circle", { cx: WU_SAMA.node.cx, cy: WU_SAMA.node.cy, r: WU_SAMA.node.inner, fill: WU_SAMA.highlight }),
72
+ /* @__PURE__ */ jsx("path", { d: WU_SAMA.w, stroke: `url(#${id.blue})`, strokeWidth: WU_SAMA.wStroke, strokeLinecap: "round", strokeLinejoin: "round" })
73
+ ] })
78
74
  ] });
79
75
  }
80
76
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/marks/wu-sama-geometry.ts","../src/marks/WuSamaMark.tsx"],"sourcesContent":["/**\n * The Wu-sama mark's geometry — the founder's byline mark. **Original mark, inspired by, not derived from**: it is the\n * Krizaka hexagon (a pointy-top regular hexagon in the 400×400 frame of the family), split at its top and bottom\n * vertices into two halves like the Krizaka shield that opens at one vertex, around a W of five straight legs whose\n * points are measured on that hexagon (`u`, its half-width, and `radius`) — see `wu-sama-geometry` tests. It is not\n * the Wu-Tang Clan's mark: no bat-wing silhouette, no curve, no black-and-yellow composition. The gold is one fixed\n * mid-tone amber (≥ 3:1 on the page surfaces of both themes), a hip-hop nod and not that clan's yellow.\n */\nconst round = (n: number) => Math.round(n * 10) / 10;\nconst center = 200;\nconst radius = 150;\n/** Half the hexagon's width. */\nconst u = (radius * Math.sqrt(3)) / 2;\nconst gap = 10;\n\n/** T, UR, LR, B, LL, UL. */\nconst hexagon = [\n [center, center - radius],\n [round(center + u), center - radius / 2],\n [round(center + u), center + radius / 2],\n [center, center + radius],\n [round(center - u), center + radius / 2],\n [round(center - u), center - radius / 2],\n] as const;\nconst [T, UR, LR, B, LL, UL] = hexagon;\n\n/** The point of the edge a→b whose x is `x`. */\nconst onEdge = (a: readonly [number, number], b: readonly [number, number], x: number) => [x, round(a[1] + ((x - a[0]) / (b[0] - a[0])) * (b[1] - a[1]))] as const;\nconst pt = ([x, y]: readonly [number, number]) => `${x},${y}`;\n\nconst left = [onEdge(T, UL, center - gap), UL, LL, onEdge(B, LL, center - gap)] as const;\nconst right = [onEdge(T, UR, center + gap), UR, LR, onEdge(B, LR, center + gap)] as const;\n\n/** The W: tips and valleys at fixed fractions of the hexagon's half-width and radius; the middle summit on the axis. */\nconst wPoints = [\n [round(center - 0.63 * u), round(center - 0.4 * radius)],\n [round(center - 0.37 * u), round(center + 0.55 * radius)],\n [center, round(center - 0.03 * radius)],\n [round(center + 0.37 * u), round(center + 0.55 * radius)],\n [round(center + 0.63 * u), round(center - 0.4 * radius)],\n] as const;\nconst peak = wPoints[2];\n\nexport const WU_SAMA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"34 34 332 332\",\n center,\n radius,\n gap,\n hexagon,\n /** The two halves of the hexagon (open at the top and bottom vertices), stroked in ink. */\n shell: { left: `M${left.map(pt).join(\" L\")}`, right: `M${right.map(pt).join(\" L\")}`, stroke: 22 },\n /** The W, stroked in gold. */\n w: `M${wPoints.map(pt).join(\" L\")}`,\n wStroke: 26,\n /** The node: the family's lit node, a small diamond at the W's middle summit, in ink. */\n node: `M${center},${round(peak[1] - 24)} L${center + 18},${peak[1]} L${center},${round(peak[1] + 24)} L${center - 18},${peak[1]}Z`,\n /** One mid-tone amber, hsl(40 94% 37%): 3.3:1 on white and 5.8:1 on the dark surface. */\n gold: \"hsl(40 94% 37%)\",\n} as const;\n","import { a11y, type MarkProps, neutral } from \"./shared\";\nimport { WU_SAMA as G } from \"./wu-sama-geometry\";\n\n/**\n * The Wu-sama mark — the founder's byline mark, for the person and not the company (the company keeps\n * `KrizakaLogo`). Oussama's nickname is Wu-sama: a W of five straight legs in gold inside the Krizaka hexagon, split\n * at its top and bottom vertices, a small lit node at the middle summit. Still: no orbit, no pulse. The hexagon and the\n * 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.\n *\n * **Original mark, inspired by, not derived from**: it nods to the culture its name comes from but is built from the\n * Krizaka hexagon's own measures (see `wu-sama-geometry.ts` and its tests), never from the Wu-Tang Clan's registered\n * logo — no bat-wing W, no curve, no yellow-on-black copy. Below 48 px it crops to the emblem.\n */\nexport function WuSamaMark({ size = 32, title, className }: Omit<MarkProps, \"animated\"> & { animated?: boolean }) {\n const ink = neutral(\"--kz-text-primary\", 92);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <path d={G.shell.left} stroke={ink} strokeWidth={G.shell.stroke} strokeLinejoin=\"miter\" strokeLinecap=\"butt\" />\n <path d={G.shell.right} stroke={ink} strokeWidth={G.shell.stroke} strokeLinejoin=\"miter\" strokeLinecap=\"butt\" />\n <path d={G.w} stroke={G.gold} strokeWidth={G.wStroke} strokeLinejoin=\"miter\" strokeMiterlimit={4} strokeLinecap=\"butt\" />\n <path d={G.node} fill={ink} />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAQA,IAAM,QAAQ,CAAC,MAAc,KAAK,MAAM,IAAI,EAAE,IAAI;AAClD,IAAM,SAAS;AACf,IAAM,SAAS;AAEf,IAAM,IAAK,SAAS,KAAK,KAAK,CAAC,IAAK;AACpC,IAAM,MAAM;AAGZ,IAAM,UAAU;AAAA,EACd,CAAC,QAAQ,SAAS,MAAM;AAAA,EACxB,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,SAAS,CAAC;AAAA,EACvC,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,SAAS,CAAC;AAAA,EACvC,CAAC,QAAQ,SAAS,MAAM;AAAA,EACxB,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,SAAS,CAAC;AAAA,EACvC,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,SAAS,CAAC;AACzC;AACA,IAAM,CAAC,GAAG,IAAI,IAAI,GAAG,IAAI,EAAE,IAAI;AAG/B,IAAM,SAAS,CAAC,GAA8B,GAA8B,MAAc,CAAC,GAAG,MAAM,EAAE,CAAC,KAAM,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,MAAO,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;AACxJ,IAAM,KAAK,CAAC,CAAC,GAAG,CAAC,MAAiC,GAAG,CAAC,IAAI,CAAC;AAE3D,IAAM,OAAO,CAAC,OAAO,GAAG,IAAI,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,GAAG,IAAI,SAAS,GAAG,CAAC;AAC9E,IAAM,QAAQ,CAAC,OAAO,GAAG,IAAI,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,GAAG,IAAI,SAAS,GAAG,CAAC;AAG/E,IAAM,UAAU;AAAA,EACd,CAAC,MAAM,SAAS,OAAO,CAAC,GAAG,MAAM,SAAS,MAAM,MAAM,CAAC;AAAA,EACvD,CAAC,MAAM,SAAS,OAAO,CAAC,GAAG,MAAM,SAAS,OAAO,MAAM,CAAC;AAAA,EACxD,CAAC,QAAQ,MAAM,SAAS,OAAO,MAAM,CAAC;AAAA,EACtC,CAAC,MAAM,SAAS,OAAO,CAAC,GAAG,MAAM,SAAS,OAAO,MAAM,CAAC;AAAA,EACxD,CAAC,MAAM,SAAS,OAAO,CAAC,GAAG,MAAM,SAAS,MAAM,MAAM,CAAC;AACzD;AACA,IAAM,OAAO,QAAQ,CAAC;AAEf,IAAM,UAAU;AAAA,EACrB,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC,IAAI,OAAO,IAAI,MAAM,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC,IAAI,QAAQ,GAAG;AAAA;AAAA,EAEhG,GAAG,IAAI,QAAQ,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,EACjC,SAAS;AAAA;AAAA,EAET,MAAM,IAAI,MAAM,IAAI,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,EAAE,IAAI,KAAK,CAAC,CAAC,KAAK,MAAM,IAAI,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,EAAE,IAAI,KAAK,CAAC,CAAC;AAAA;AAAA,EAE/H,MAAM;AACR;;;AC3CI,SACE,KADF;AAHG,SAAS,WAAW,EAAE,OAAO,IAAI,OAAO,UAAU,GAAyD;AAChH,QAAM,MAAM,QAAQ,qBAAqB,EAAE;AAC3C,SACE,qBAAC,SAAI,OAAO,MAAM,QAAQ,MAAM,SAAS,OAAO,KAAK,QAAE,iBAAiB,QAAE,SAAS,MAAK,QAAO,OAAM,8BAA6B,WAAuB,GAAG,KAAK,KAAK,GACpK;AAAA,wBAAC,UAAK,GAAG,QAAE,MAAM,MAAM,QAAQ,KAAK,aAAa,QAAE,MAAM,QAAQ,gBAAe,SAAQ,eAAc,QAAO;AAAA,IAC7G,oBAAC,UAAK,GAAG,QAAE,MAAM,OAAO,QAAQ,KAAK,aAAa,QAAE,MAAM,QAAQ,gBAAe,SAAQ,eAAc,QAAO;AAAA,IAC9G,oBAAC,UAAK,GAAG,QAAE,GAAG,QAAQ,QAAE,MAAM,aAAa,QAAE,SAAS,gBAAe,SAAQ,kBAAkB,GAAG,eAAc,QAAO;AAAA,IACvH,oBAAC,UAAK,GAAG,QAAE,MAAM,MAAM,KAAK;AAAA,KAC9B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/marks/wu-sama-geometry.ts","../src/marks/WuSamaMark.tsx"],"sourcesContent":["import { marks } from \"@krizaka/tokens/native\";\n\n/**\n * The Wu-sama mark's geometry — the founder's byline mark.\n * It shares the Krizaka shield contour (pointy-top hexagonal shield with open vertex and lit node)\n * and brand colors, holding a W of five straight legs in Krizaka blue at its center.\n */\nexport const WU_SAMA = {\n viewBox: \"0 0 400 400\",\n croppedViewBox: \"86 82 228 228\",\n offset: 20,\n /** The shield: the Krizaka pointy-top hexagon (r 92 around 180,180), open at its upper-right edge. */\n shield: \"M259.7,134 L259.7,226 L180,272 L100.3,226 L100.3,134 L180,88 L212,106.5\",\n stroke: 22,\n ink: { x1: 70, y1: 290, x2: 290, y2: 70 },\n blue: { x1: 180, y1: 180, x2: 300, y2: 60, stops: marks.krizaka },\n /** The vertex where the shield opens: a lit node in Krizaka blue. */\n node: { cx: 259.7, cy: 134, r: 19, inner: 6 },\n /** The W: five straight legs in Krizaka blue. */\n w: \"M132,142 L154,216 L180,166 L206,216 L228,142\",\n wStroke: 18,\n highlight: \"#dbeafe\",\n glow: { color: \"#196df5\", opacity: 0.16 },\n orbit: { r: 160, dash: \"8 8\", width: 1.5 },\n ring: { r: 134, width: 1 },\n} as const;\n","import { a11y, type MarkProps, neutral, useIds } from \"./shared\";\nimport { WU_SAMA as G } from \"./wu-sama-geometry\";\n\n/**\n * The Wu-sama mark — the founder's byline mark, for the person and not the company (the company keeps `KrizakaLogo`).\n * Oussama's nickname is Wu-sama: it shares Krizaka's hexagonal shield contour and color palette (ink shield, open\n * vertex with lit blue node), with the W rendered in Krizaka blue at the center. Below 48 px it crops to the emblem.\n */\nexport function WuSamaMark({ size = 32, title, className }: Omit<MarkProps, \"animated\"> & { animated?: boolean }) {\n const id = useIds(\"wsm\", [\"ink\", \"blue\", \"glow\"] as const);\n return (\n <svg width={size} height={size} viewBox={size < 48 ? G.croppedViewBox : G.viewBox} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className={className} {...a11y(title)}>\n <defs>\n <linearGradient id={id.ink} gradientUnits=\"userSpaceOnUse\" x1={G.ink.x1} y1={G.ink.y1} x2={G.ink.x2} y2={G.ink.y2}>\n <stop offset=\"0%\" stopColor={neutral(\"--kz-text-secondary\", 60)} />\n <stop offset=\"100%\" stopColor={neutral(\"--kz-text-primary\", 92)} />\n </linearGradient>\n <linearGradient id={id.blue} gradientUnits=\"userSpaceOnUse\" x1={G.blue.x1} y1={G.blue.y1} x2={G.blue.x2} y2={G.blue.y2}>\n {G.blue.stops.map(([offset, color]) => (\n <stop key={offset} offset={offset} stopColor={color} />\n ))}\n </linearGradient>\n <radialGradient id={id.glow} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <stop offset=\"0%\" stopColor={G.glow.color} stopOpacity={G.glow.opacity} />\n <stop offset=\"100%\" stopColor={G.glow.color} stopOpacity=\"0\" />\n </radialGradient>\n </defs>\n {size >= 48 ? <circle cx=\"200\" cy=\"200\" r=\"190\" fill={`url(#${id.glow})`} /> : null}\n {size >= 48 ? (\n <>\n <circle cx=\"200\" cy=\"200\" r={G.orbit.r} stroke={neutral(\"--kz-border-strong\", 40)} strokeWidth={G.orbit.width} strokeDasharray={G.orbit.dash} opacity=\".6\" />\n <circle cx=\"200\" cy=\"200\" r={G.ring.r} stroke={neutral(\"--kz-border-default\", 30)} strokeWidth={G.ring.width} opacity=\".35\" />\n </>\n ) : null}\n <g transform={`translate(${G.offset} ${G.offset})`}>\n <path d={G.shield} stroke={`url(#${id.ink})`} strokeWidth={G.stroke} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.r} fill={`url(#${id.blue})`} />\n <circle cx={G.node.cx} cy={G.node.cy} r={G.node.inner} fill={G.highlight} />\n <path d={G.w} stroke={`url(#${id.blue})`} strokeWidth={G.wStroke} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </g>\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,17 @@
1
+ /** A language the site speaks: its code (`fr`), its name in its own language (`Français`) and the address of the page in it. */
2
+ interface Language {
3
+ code: string;
4
+ name: string;
5
+ href: string;
6
+ }
7
+ /** The languages of the family, each named in its own language — the switch never shows “French” to a French reader. */
8
+ declare const languageNames: Readonly<Record<string, string>>;
9
+ /**
10
+ * The same page in another language: `/fr/baux?x=1#a` → `/en/baux?x=1#a`. The first segment is the language when it is
11
+ * one of `locales`; an address without a language prefix gets one. Query and hash are kept.
12
+ */
13
+ declare function languageHref(path: string, to: string, locales?: readonly string[]): string;
14
+ /** The `languages` of a switch for the page at `path`, in the order of `locales`, each named in its own language. */
15
+ declare function languagesFor(path: string, locales?: readonly string[]): Language[];
16
+
17
+ export { type Language, languageHref, languageNames, languagesFor };
@@ -0,0 +1,26 @@
1
+ // src/language-switch/href.ts
2
+ var languageNames = {
3
+ en: "English",
4
+ fr: "Fran\xE7ais",
5
+ es: "Espa\xF1ol",
6
+ de: "Deutsch",
7
+ it: "Italiano",
8
+ pt: "Portugu\xEAs"
9
+ };
10
+ function languageHref(path, to, locales = ["en", "fr"]) {
11
+ const splitIndex = path.search(/[?#]/);
12
+ const pathname = splitIndex === -1 ? path : path.slice(0, splitIndex);
13
+ const rest = splitIndex === -1 ? "" : path.slice(splitIndex);
14
+ const segments = (pathname || "/").split("/").filter(Boolean);
15
+ if (segments[0] && locales.includes(segments[0])) segments.shift();
16
+ return `/${[to, ...segments].join("/")}${rest}`;
17
+ }
18
+ function languagesFor(path, locales = ["en", "fr"]) {
19
+ return locales.map((code) => ({ code, name: languageNames[code] ?? code.toUpperCase(), href: languageHref(path, code, locales) }));
20
+ }
21
+ export {
22
+ languageHref,
23
+ languageNames,
24
+ languagesFor
25
+ };
26
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/language-switch/href.ts"],"sourcesContent":["/** A language the site speaks: its code (`fr`), its name in its own language (`Français`) and the address of the page in it. */\nexport interface Language {\n code: string;\n name: string;\n href: string;\n}\n\n/** The languages of the family, each named in its own language — the switch never shows “French” to a French reader. */\nexport const languageNames: Readonly<Record<string, string>> = {\n en: \"English\",\n fr: \"Français\",\n es: \"Español\",\n de: \"Deutsch\",\n it: \"Italiano\",\n pt: \"Português\",\n};\n\n/**\n * The same page in another language: `/fr/baux?x=1#a` → `/en/baux?x=1#a`. The first segment is the language when it is\n * one of `locales`; an address without a language prefix gets one. Query and hash are kept.\n */\nexport function languageHref(path: string, to: string, locales: readonly string[] = [\"en\", \"fr\"]): string {\n const splitIndex = path.search(/[?#]/);\n const pathname = splitIndex === -1 ? path : path.slice(0, splitIndex);\n const rest = splitIndex === -1 ? \"\" : path.slice(splitIndex);\n const segments = (pathname || \"/\").split(\"/\").filter(Boolean);\n if (segments[0] && locales.includes(segments[0])) segments.shift();\n return `/${[to, ...segments].join(\"/\")}${rest}`;\n}\n\n/** The `languages` of a switch for the page at `path`, in the order of `locales`, each named in its own language. */\nexport function languagesFor(path: string, locales: readonly string[] = [\"en\", \"fr\"]): Language[] {\n return locales.map((code) => ({ code, name: languageNames[code] ?? code.toUpperCase(), href: languageHref(path, code, locales) }));\n}\n"],"mappings":";AAQO,IAAM,gBAAkD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAMO,SAAS,aAAa,MAAc,IAAY,UAA6B,CAAC,MAAM,IAAI,GAAW;AACxG,QAAM,aAAa,KAAK,OAAO,MAAM;AACrC,QAAM,WAAW,eAAe,KAAK,OAAO,KAAK,MAAM,GAAG,UAAU;AACpE,QAAM,OAAO,eAAe,KAAK,KAAK,KAAK,MAAM,UAAU;AAC3D,QAAM,YAAY,YAAY,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC5D,MAAI,SAAS,CAAC,KAAK,QAAQ,SAAS,SAAS,CAAC,CAAC,EAAG,UAAS,MAAM;AACjE,SAAO,IAAI,CAAC,IAAI,GAAG,QAAQ,EAAE,KAAK,GAAG,CAAC,GAAG,IAAI;AAC/C;AAGO,SAAS,aAAa,MAAc,UAA6B,CAAC,MAAM,IAAI,GAAe;AAChG,SAAO,QAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,cAAc,IAAI,KAAK,KAAK,YAAY,GAAG,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE,EAAE;AACnI;","names":[]}
@@ -14,8 +14,10 @@ var languageNames = {
14
14
  pt: "Portugu\xEAs"
15
15
  };
16
16
  function languageHref(path, to, locales = ["en", "fr"]) {
17
- const [, pathname = "/", rest = ""] = /^([^?#]*)(.*)$/.exec(path) ?? [];
18
- const segments = pathname.split("/").filter(Boolean);
17
+ const splitIndex = path.search(/[?#]/);
18
+ const pathname = splitIndex === -1 ? path : path.slice(0, splitIndex);
19
+ const rest = splitIndex === -1 ? "" : path.slice(splitIndex);
20
+ const segments = (pathname || "/").split("/").filter(Boolean);
19
21
  if (segments[0] && locales.includes(segments[0])) segments.shift();
20
22
  return `/${[to, ...segments].join("/")}${rest}`;
21
23
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/language-switch/href.ts"],"sourcesContent":["/** A language the site speaks: its code (`fr`), its name in its own language (`Français`) and the address of the page in it. */\nexport interface Language {\n code: string;\n name: string;\n href: string;\n}\n\n/** The languages of the family, each named in its own language — the switch never shows “French” to a French reader. */\nexport const languageNames: Readonly<Record<string, string>> = {\n en: \"English\",\n fr: \"Français\",\n es: \"Español\",\n de: \"Deutsch\",\n it: \"Italiano\",\n pt: \"Português\",\n};\n\n/**\n * The same page in another language: `/fr/baux?x=1#a` → `/en/baux?x=1#a`. The first segment is the language when it is\n * one of `locales`; an address without a language prefix gets one. Query and hash are kept.\n */\nexport function languageHref(path: string, to: string, locales: readonly string[] = [\"en\", \"fr\"]): string {\n const [, pathname = \"/\", rest = \"\"] = /^([^?#]*)(.*)$/.exec(path) ?? [];\n const segments = pathname.split(\"/\").filter(Boolean);\n if (segments[0] && locales.includes(segments[0])) segments.shift();\n return `/${[to, ...segments].join(\"/\")}${rest}`;\n}\n\n/** The `languages` of a switch for the page at `path`, in the order of `locales`, each named in its own language. */\nexport function languagesFor(path: string, locales: readonly string[] = [\"en\", \"fr\"]): Language[] {\n return locales.map((code) => ({ code, name: languageNames[code] ?? code.toUpperCase(), href: languageHref(path, code, locales) }));\n}\n"],"mappings":";;;;;;;AAQO,IAAM,gBAAkD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAMO,SAAS,aAAa,MAAc,IAAY,UAA6B,CAAC,MAAM,IAAI,GAAW;AACxG,QAAM,CAAC,EAAE,WAAW,KAAK,OAAO,EAAE,IAAI,iBAAiB,KAAK,IAAI,KAAK,CAAC;AACtE,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,MAAI,SAAS,CAAC,KAAK,QAAQ,SAAS,SAAS,CAAC,CAAC,EAAG,UAAS,MAAM;AACjE,SAAO,IAAI,CAAC,IAAI,GAAG,QAAQ,EAAE,KAAK,GAAG,CAAC,GAAG,IAAI;AAC/C;AAGO,SAAS,aAAa,MAAc,UAA6B,CAAC,MAAM,IAAI,GAAe;AAChG,SAAO,QAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,cAAc,IAAI,KAAK,KAAK,YAAY,GAAG,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE,EAAE;AACnI;","names":[]}
1
+ {"version":3,"sources":["../src/language-switch/href.ts"],"sourcesContent":["/** A language the site speaks: its code (`fr`), its name in its own language (`Français`) and the address of the page in it. */\nexport interface Language {\n code: string;\n name: string;\n href: string;\n}\n\n/** The languages of the family, each named in its own language — the switch never shows “French” to a French reader. */\nexport const languageNames: Readonly<Record<string, string>> = {\n en: \"English\",\n fr: \"Français\",\n es: \"Español\",\n de: \"Deutsch\",\n it: \"Italiano\",\n pt: \"Português\",\n};\n\n/**\n * The same page in another language: `/fr/baux?x=1#a` → `/en/baux?x=1#a`. The first segment is the language when it is\n * one of `locales`; an address without a language prefix gets one. Query and hash are kept.\n */\nexport function languageHref(path: string, to: string, locales: readonly string[] = [\"en\", \"fr\"]): string {\n const splitIndex = path.search(/[?#]/);\n const pathname = splitIndex === -1 ? path : path.slice(0, splitIndex);\n const rest = splitIndex === -1 ? \"\" : path.slice(splitIndex);\n const segments = (pathname || \"/\").split(\"/\").filter(Boolean);\n if (segments[0] && locales.includes(segments[0])) segments.shift();\n return `/${[to, ...segments].join(\"/\")}${rest}`;\n}\n\n/** The `languages` of a switch for the page at `path`, in the order of `locales`, each named in its own language. */\nexport function languagesFor(path: string, locales: readonly string[] = [\"en\", \"fr\"]): Language[] {\n return locales.map((code) => ({ code, name: languageNames[code] ?? code.toUpperCase(), href: languageHref(path, code, locales) }));\n}\n"],"mappings":";;;;;;;AAQO,IAAM,gBAAkD;AAAA,EAC7D,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAMO,SAAS,aAAa,MAAc,IAAY,UAA6B,CAAC,MAAM,IAAI,GAAW;AACxG,QAAM,aAAa,KAAK,OAAO,MAAM;AACrC,QAAM,WAAW,eAAe,KAAK,OAAO,KAAK,MAAM,GAAG,UAAU;AACpE,QAAM,OAAO,eAAe,KAAK,KAAK,KAAK,MAAM,UAAU;AAC3D,QAAM,YAAY,YAAY,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC5D,MAAI,SAAS,CAAC,KAAK,QAAQ,SAAS,SAAS,CAAC,CAAC,EAAG,UAAS,MAAM;AACjE,SAAO,IAAI,CAAC,IAAI,GAAG,QAAQ,EAAE,KAAK,GAAG,CAAC,GAAG,IAAI;AAC/C;AAGO,SAAS,aAAa,MAAc,UAA6B,CAAC,MAAM,IAAI,GAAe;AAChG,SAAO,QAAQ,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,cAAc,IAAI,KAAK,KAAK,YAAY,GAAG,MAAM,aAAa,MAAM,MAAM,OAAO,EAAE,EAAE;AACnI;","names":[]}