@krizaka/ui 3.33.0 → 3.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/motion.css CHANGED
@@ -294,7 +294,7 @@ svg[data-kz-icon="ExternalIcon"] {
294
294
  display: flex;
295
295
  flex-direction: column;
296
296
  transform: translateY(calc(var(--kz-digit, 0) * -10%));
297
- transition: transform 0.9s var(--kz-ease);
297
+ transition: transform var(--kz-count-duration, 900ms) var(--kz-count-ease, var(--kz-ease));
298
298
  }
299
299
  .kz-count-digit > span > span {
300
300
  height: 1lh;
@@ -690,8 +690,7 @@ svg[data-kz-icon="ExternalIcon"] {
690
690
  /* ─── Arrival and growth (the data demos of Krizaka Motion) — CSS only, held by the conductor like every loop ───
691
691
  `[data-kz-arrive]` on an element (or on a cell: its table row, `tr:has(…)`, its list item) plays an arrival once when it
692
692
  is added: `slide` (default) falls into place, `fade`, `flash` (an accent wash that fades) — chosen on any ancestor by
693
- `data-kz-arrival`. `--kz-arrive-duration` sets the length. `.kz-grow` around a Sparkline (or any SVG with a stroke and
694
- a fill) draws its stroke over `--kz-grow-duration` with `--kz-grow-ease`, and raises the fill. No movement of layout:
693
+ `data-kz-arrival`. `--kz-arrive-duration` sets the length. `.kz-grow` around a Sparkline (or any SVG chart) wipes it in from the left over `--kz-grow-duration` with `--kz-grow-ease`. No movement of layout:
695
694
  `translate`/`scale`/`opacity` only. Off screen they pause (`[data-offscreen]`), under reduced motion they rest done. */
696
695
  [data-kz-arrive], tr:has([data-kz-arrive]), li:has(> [data-kz-arrive]) {
697
696
  animation: kz-arrive-slide var(--kz-arrive-duration, 520ms) var(--kz-ease) both;
@@ -701,18 +700,11 @@ svg[data-kz-icon="ExternalIcon"] {
701
700
  @keyframes kz-arrive-slide { from { opacity: 0; translate: 0 -14px; } to { opacity: 1; translate: 0 0; } }
702
701
  @keyframes kz-arrive-fade { from { opacity: 0; } to { opacity: 1; } }
703
702
  @keyframes kz-arrive-flash { from { background: color-mix(in oklab, var(--kz-accent) 24%, transparent); } to { background: transparent; } }
704
- .kz-grow svg path[stroke]:not([stroke="none"]) {
705
- stroke-dasharray: 1;
706
- stroke-dashoffset: 1;
707
- animation: kz-grow-draw var(--kz-grow-duration, 900ms) var(--kz-grow-ease, var(--kz-ease)) forwards;
703
+ .kz-grow svg {
704
+ animation: kz-grow-wipe var(--kz-grow-duration, 900ms) var(--kz-grow-ease, var(--kz-ease)) backwards;
708
705
  }
709
- .kz-grow svg path[fill]:not([fill="none"]) {
710
- transform-box: fill-box;
711
- transform-origin: bottom;
712
- animation: kz-grow-rise var(--kz-grow-duration, 900ms) var(--kz-grow-ease, var(--kz-ease)) backwards;
713
- }
714
- @keyframes kz-grow-draw { to { stroke-dashoffset: 0; } }
715
- @keyframes kz-grow-rise { from { opacity: 0; scale: 1 0.2; } to { opacity: 1; scale: 1 1; } }
706
+ /* A wipe from the left: the whole chart (line, area, bars) is revealed as one — no per-path trick, so it works on any SVG chart. */
707
+ @keyframes kz-grow-wipe { from { clip-path: inset(-4px 100% -4px -4px); } to { clip-path: inset(-4px -4px -4px -4px); } }
716
708
 
717
709
  @media (prefers-reduced-motion: reduce) {
718
710
  .kz-page, .kz-word, .kz-gradient-text, .kz-marquee, .kz-backdrop-drift, .kz-overlay, .kz-dialog, .kz-pop, .kz-fade, .kz-progress,
@@ -730,6 +722,5 @@ svg[data-kz-icon="ExternalIcon"] {
730
722
  .kz-count-digit > span { transition: none; }
731
723
  /* The still frame is a composed picture, not a cut animation: particles rest along their link, labels show. */
732
724
  .kz-iso-item > .kz-iso-lift, .kz-iso-tip { transition: none; }
733
- [data-kz-arrive], tr:has([data-kz-arrive]), li:has(> [data-kz-arrive]), .kz-grow svg path { animation: none !important; }
734
- .kz-grow svg path[stroke]:not([stroke="none"]) { stroke-dashoffset: 0; }
725
+ [data-kz-arrive], tr:has([data-kz-arrive]), li:has(> [data-kz-arrive]), .kz-grow svg { animation: none !important; }
735
726
  }
@@ -14,10 +14,14 @@ interface NumberTickerProps extends Omit<React.ComponentProps<"span">, "children
14
14
  */
15
15
  trigger?: "view" | "change";
16
16
  /**
17
- * Milliseconds of the climb.
18
- * @default 1100
17
+ * Milliseconds of the climb (`view`) or of each digit's roll (`change`). Unset: 1100 ms to climb, 900 ms to roll.
19
18
  */
20
19
  duration?: number;
20
+ /**
21
+ * The timing function of the climb and of the roll: `ease-out` (the climb's own curve), `linear`, `ease-in-out`, or any
22
+ * CSS `<easing-function>`. Unset: the platform's curve (`--kz-ease`).
23
+ */
24
+ easing?: "ease-out" | "linear" | "ease-in-out" | (string & {});
21
25
  /** Announce each new value politely (`trigger="change"`): a live total the reader should hear. */
22
26
  announce?: boolean;
23
27
  /**
@@ -27,6 +31,6 @@ interface NumberTickerProps extends Omit<React.ComponentProps<"span">, "children
27
31
  align?: "start" | "end";
28
32
  }
29
33
  /** A figure that climbs into view, or rolls digit by digit to each new value. */
30
- declare function NumberTicker({ value, locale, format, trigger, duration, announce, align, className, ...props }: NumberTickerProps): React.JSX.Element;
34
+ declare function NumberTicker({ value, locale, format, trigger, duration, announce, align, easing, className, style, ...props }: NumberTickerProps): React.JSX.Element;
31
35
 
32
36
  export { NumberTicker, type NumberTickerProps };
@@ -7,7 +7,10 @@ import {
7
7
  import * as React from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  var easeOut = (t) => 1 - Math.pow(1 - t, 4);
10
- function NumberTicker({ value, locale, format, trigger = "view", duration = 1100, announce, align = "end", className, ...props }) {
10
+ var curves = { linear: (t) => t, "ease-in-out": (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2 };
11
+ function NumberTicker({ value, locale, format, trigger = "view", duration, announce, align = "end", easing, className, style, ...props }) {
12
+ const climb = duration ?? 1100;
13
+ const curve = easing && curves[easing] || easeOut;
11
14
  const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format]);
12
15
  const text = formatter.format(value);
13
16
  const live = React.useRef(null);
@@ -31,8 +34,8 @@ function NumberTicker({ value, locale, format, trigger = "view", duration = 1100
31
34
  if (wasFirst) return;
32
35
  const start = performance.now();
33
36
  const tick = (now) => {
34
- const p = Math.min(1, (now - start) / duration);
35
- el.textContent = formatter.format(value * easeOut(p));
37
+ const p = Math.min(1, (now - start) / climb);
38
+ el.textContent = formatter.format(value * curve(p));
36
39
  if (p < 1) frame = requestAnimationFrame(tick);
37
40
  };
38
41
  el.textContent = formatter.format(0);
@@ -46,14 +49,24 @@ function NumberTicker({ value, locale, format, trigger = "view", duration = 1100
46
49
  cancelAnimationFrame(frame);
47
50
  el.textContent = formatter.format(value);
48
51
  };
49
- }, [trigger, value, duration, formatter]);
50
- return /* @__PURE__ */ jsxs("span", { "data-align": align, "data-trend": trigger === "change" ? trend : void 0, className: cx("kz-count", className), ...props, children: [
51
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "kz-count-ghost", children: text }),
52
- trigger === "change" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "kz-count-roll", children: [...text].map(
53
- (char, i, all) => /\d/.test(char) ? /* @__PURE__ */ jsx("span", { className: "kz-count-digit", style: { "--kz-digit": char }, children: /* @__PURE__ */ jsx("span", { children: "0123456789".split("").map((d) => /* @__PURE__ */ jsx("span", { children: d }, d)) }) }, all.length - i) : /* @__PURE__ */ jsx("span", { children: char }, all.length - i)
54
- ) }) : /* @__PURE__ */ jsx("span", { "aria-hidden": true, ref: live, children: text }),
55
- /* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": announce ? "polite" : void 0, children: text })
56
- ] });
52
+ }, [trigger, value, climb, curve, formatter]);
53
+ return /* @__PURE__ */ jsxs(
54
+ "span",
55
+ {
56
+ "data-align": align,
57
+ "data-trend": trigger === "change" ? trend : void 0,
58
+ className: cx("kz-count", className),
59
+ style: { ...duration !== void 0 ? { "--kz-count-duration": `${duration}ms` } : null, ...easing ? { "--kz-count-ease": easing } : null, ...style },
60
+ ...props,
61
+ children: [
62
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "kz-count-ghost", children: text }),
63
+ trigger === "change" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "kz-count-roll", children: [...text].map(
64
+ (char, i, all) => /\d/.test(char) ? /* @__PURE__ */ jsx("span", { className: "kz-count-digit", style: { "--kz-digit": char }, children: /* @__PURE__ */ jsx("span", { children: "0123456789".split("").map((d) => /* @__PURE__ */ jsx("span", { children: d }, d)) }) }, all.length - i) : /* @__PURE__ */ jsx("span", { children: char }, all.length - i)
65
+ ) }) : /* @__PURE__ */ jsx("span", { "aria-hidden": true, ref: live, children: text }),
66
+ /* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": announce ? "polite" : void 0, children: text })
67
+ ]
68
+ }
69
+ );
57
70
  }
58
71
  export {
59
72
  NumberTicker
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/number-ticker/number-ticker.tsx"],"sourcesContent":["// Client (an observer, a short count). A figure that climbs — without the faults of the usual counters: the server\n// renders the final value (crawlers, screen readers, no-JS and reduced-motion readers all read it), an invisible copy\n// holds its width from the first paint (no layout shift), a figure already on screen at load does not drop to 0 in\n// front of the reader, the count is a bounded loop (~1 s) that stops for good, and assistive technology reads only the\n// final value. On `trigger=\"change\"` each digit rolls to its new value (a live total: tips, bids) and the change can be\n// announced.\nimport * as React from \"react\";\n\nimport { cx } from \"../cx\";\n\nexport interface NumberTickerProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** The value to show. */\n value: number;\n /** The locale of the digits and separators (`\"fr\"`: 1 204,5) — the app's locale. */\n locale?: string;\n /** How to write it (`{ style: \"currency\", currency: \"EUR\" }`, `{ maximumFractionDigits: 1 }`…): Intl.NumberFormat options. */\n format?: Intl.NumberFormatOptions;\n /**\n * `view`: climbs from 0 once, the first time it scrolls into view. `change`: each digit rolls to the new value\n * whenever `value` changes (a live total).\n * @default \"view\"\n */\n trigger?: \"view\" | \"change\";\n /**\n * Milliseconds of the climb.\n * @default 1100\n */\n duration?: number;\n /** Announce each new value politely (`trigger=\"change\"`): a live total the reader should hear. */\n announce?: boolean;\n /**\n * Where the figure sits in the box its final width reserves.\n * @default \"end\"\n */\n align?: \"start\" | \"end\";\n}\n\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 4);\n\n/** A figure that climbs into view, or rolls digit by digit to each new value. */\nexport function NumberTicker({ value, locale, format, trigger = \"view\", duration = 1100, announce, align = \"end\", className, ...props }: NumberTickerProps) {\n const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format]);\n const text = formatter.format(value);\n const live = React.useRef<HTMLSpanElement>(null);\n // The previous value, kept in state (React's pattern for “adjusting state when a prop changes”): the trend of the last change.\n const [previous, setPrevious] = React.useState(value);\n const [trend, setTrend] = React.useState<\"up\" | \"down\" | undefined>(undefined);\n if (value !== previous) {\n setPrevious(value);\n setTrend(value > previous ? \"up\" : \"down\");\n }\n\n React.useEffect(() => {\n const el = live.current;\n if (trigger !== \"view\" || !el || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n let first = true;\n let frame = 0;\n const io = new IntersectionObserver(\n ([entry]) => {\n const wasFirst = first;\n first = false;\n if (!entry?.isIntersecting) return;\n io.disconnect();\n if (wasFirst) return; // already on screen at load: it stays, nothing drops to 0 in front of the reader\n const start = performance.now();\n const tick = (now: number) => {\n const p = Math.min(1, (now - start) / duration);\n el.textContent = formatter.format(value * easeOut(p));\n if (p < 1) frame = requestAnimationFrame(tick);\n };\n el.textContent = formatter.format(0);\n frame = requestAnimationFrame(tick);\n },\n { threshold: 0.6 },\n );\n io.observe(el);\n return () => {\n io.disconnect();\n cancelAnimationFrame(frame);\n el.textContent = formatter.format(value);\n };\n }, [trigger, value, duration, formatter]);\n\n return (\n <span data-align={align} data-trend={trigger === \"change\" ? trend : undefined} className={cx(\"kz-count\", className)} {...props}>\n <span aria-hidden className=\"kz-count-ghost\">\n {text}\n </span>\n {trigger === \"change\" ? (\n <span aria-hidden className=\"kz-count-roll\">\n {/* Keyed from the right: when a digit is added (999 → 1,000), the units stay the units. */}\n {[...text].map((char, i, all) =>\n /\\d/.test(char) ? (\n <span key={all.length - i} className=\"kz-count-digit\" style={{ \"--kz-digit\": char } as React.CSSProperties}>\n <span>\n {\"0123456789\".split(\"\").map((d) => (\n <span key={d}>{d}</span>\n ))}\n </span>\n </span>\n ) : (\n <span key={all.length - i}>{char}</span>\n ),\n )}\n </span>\n ) : (\n <span aria-hidden ref={live}>\n {text}\n </span>\n )}\n <span className=\"sr-only\" aria-live={announce ? \"polite\" : undefined}>\n {text}\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;;AAMA,YAAY,WAAW;AA8EnB,SACE,KADF;AA/CJ,IAAM,UAAU,CAAC,MAAc,IAAI,KAAK,IAAI,IAAI,GAAG,CAAC;AAG7C,SAAS,aAAa,EAAE,OAAO,QAAQ,QAAQ,UAAU,QAAQ,WAAW,MAAM,UAAU,QAAQ,OAAO,WAAW,GAAG,MAAM,GAAsB;AAC1J,QAAM,YAAkB,cAAQ,MAAM,IAAI,KAAK,aAAa,QAAQ,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;AAC7F,QAAM,OAAO,UAAU,OAAO,KAAK;AACnC,QAAM,OAAa,aAAwB,IAAI;AAE/C,QAAM,CAAC,UAAU,WAAW,IAAU,eAAS,KAAK;AACpD,QAAM,CAAC,OAAO,QAAQ,IAAU,eAAoC,MAAS;AAC7E,MAAI,UAAU,UAAU;AACtB,gBAAY,KAAK;AACjB,aAAS,QAAQ,WAAW,OAAO,MAAM;AAAA,EAC3C;AAEA,EAAM,gBAAU,MAAM;AACpB,UAAM,KAAK,KAAK;AAChB,QAAI,YAAY,UAAU,CAAC,MAAM,OAAO,WAAW,kCAAkC,EAAE,QAAS;AAChG,QAAI,QAAQ;AACZ,QAAI,QAAQ;AACZ,UAAM,KAAK,IAAI;AAAA,MACb,CAAC,CAAC,KAAK,MAAM;AACX,cAAM,WAAW;AACjB,gBAAQ;AACR,YAAI,CAAC,OAAO,eAAgB;AAC5B,WAAG,WAAW;AACd,YAAI,SAAU;AACd,cAAM,QAAQ,YAAY,IAAI;AAC9B,cAAM,OAAO,CAAC,QAAgB;AAC5B,gBAAM,IAAI,KAAK,IAAI,IAAI,MAAM,SAAS,QAAQ;AAC9C,aAAG,cAAc,UAAU,OAAO,QAAQ,QAAQ,CAAC,CAAC;AACpD,cAAI,IAAI,EAAG,SAAQ,sBAAsB,IAAI;AAAA,QAC/C;AACA,WAAG,cAAc,UAAU,OAAO,CAAC;AACnC,gBAAQ,sBAAsB,IAAI;AAAA,MACpC;AAAA,MACA,EAAE,WAAW,IAAI;AAAA,IACnB;AACA,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM;AACX,SAAG,WAAW;AACd,2BAAqB,KAAK;AAC1B,SAAG,cAAc,UAAU,OAAO,KAAK;AAAA,IACzC;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,UAAU,SAAS,CAAC;AAExC,SACE,qBAAC,UAAK,cAAY,OAAO,cAAY,YAAY,WAAW,QAAQ,QAAW,WAAW,GAAG,YAAY,SAAS,GAAI,GAAG,OACvH;AAAA,wBAAC,UAAK,eAAW,MAAC,WAAU,kBACzB,gBACH;AAAA,IACC,YAAY,WACX,oBAAC,UAAK,eAAW,MAAC,WAAU,iBAEzB,WAAC,GAAG,IAAI,EAAE;AAAA,MAAI,CAAC,MAAM,GAAG,QACvB,KAAK,KAAK,IAAI,IACZ,oBAAC,UAA0B,WAAU,kBAAiB,OAAO,EAAE,cAAc,KAAK,GAChF,8BAAC,UACE,uBAAa,MAAM,EAAE,EAAE,IAAI,CAAC,MAC3B,oBAAC,UAAc,eAAJ,CAAM,CAClB,GACH,KALS,IAAI,SAAS,CAMxB,IAEA,oBAAC,UAA2B,kBAAjB,IAAI,SAAS,CAAS;AAAA,IAErC,GACF,IAEA,oBAAC,UAAK,eAAW,MAAC,KAAK,MACpB,gBACH;AAAA,IAEF,oBAAC,UAAK,WAAU,WAAU,aAAW,WAAW,WAAW,QACxD,gBACH;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/number-ticker/number-ticker.tsx"],"sourcesContent":["// Client (an observer, a short count). A figure that climbs — without the faults of the usual counters: the server\n// renders the final value (crawlers, screen readers, no-JS and reduced-motion readers all read it), an invisible copy\n// holds its width from the first paint (no layout shift), a figure already on screen at load does not drop to 0 in\n// front of the reader, the count is a bounded loop (~1 s) that stops for good, and assistive technology reads only the\n// final value. On `trigger=\"change\"` each digit rolls to its new value (a live total: tips, bids) and the change can be\n// announced.\nimport * as React from \"react\";\n\nimport { cx } from \"../cx\";\n\nexport interface NumberTickerProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n /** The value to show. */\n value: number;\n /** The locale of the digits and separators (`\"fr\"`: 1 204,5) — the app's locale. */\n locale?: string;\n /** How to write it (`{ style: \"currency\", currency: \"EUR\" }`, `{ maximumFractionDigits: 1 }`…): Intl.NumberFormat options. */\n format?: Intl.NumberFormatOptions;\n /**\n * `view`: climbs from 0 once, the first time it scrolls into view. `change`: each digit rolls to the new value\n * whenever `value` changes (a live total).\n * @default \"view\"\n */\n trigger?: \"view\" | \"change\";\n /**\n * Milliseconds of the climb (`view`) or of each digit's roll (`change`). Unset: 1100 ms to climb, 900 ms to roll.\n */\n duration?: number;\n /**\n * The timing function of the climb and of the roll: `ease-out` (the climb's own curve), `linear`, `ease-in-out`, or any\n * CSS `<easing-function>`. Unset: the platform's curve (`--kz-ease`).\n */\n easing?: \"ease-out\" | \"linear\" | \"ease-in-out\" | (string & {});\n /** Announce each new value politely (`trigger=\"change\"`): a live total the reader should hear. */\n announce?: boolean;\n /**\n * Where the figure sits in the box its final width reserves.\n * @default \"end\"\n */\n align?: \"start\" | \"end\";\n}\n\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 4);\nconst curves: Record<string, (t: number) => number> = { linear: (t) => t, \"ease-in-out\": (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2) };\n\n/** A figure that climbs into view, or rolls digit by digit to each new value. */\nexport function NumberTicker({ value, locale, format, trigger = \"view\", duration, announce, align = \"end\", easing, className, style, ...props }: NumberTickerProps) {\n const climb = duration ?? 1100;\n const curve = (easing && curves[easing]) || easeOut;\n const formatter = React.useMemo(() => new Intl.NumberFormat(locale, format), [locale, format]);\n const text = formatter.format(value);\n const live = React.useRef<HTMLSpanElement>(null);\n // The previous value, kept in state (React's pattern for “adjusting state when a prop changes”): the trend of the last change.\n const [previous, setPrevious] = React.useState(value);\n const [trend, setTrend] = React.useState<\"up\" | \"down\" | undefined>(undefined);\n if (value !== previous) {\n setPrevious(value);\n setTrend(value > previous ? \"up\" : \"down\");\n }\n\n React.useEffect(() => {\n const el = live.current;\n if (trigger !== \"view\" || !el || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n let first = true;\n let frame = 0;\n const io = new IntersectionObserver(\n ([entry]) => {\n const wasFirst = first;\n first = false;\n if (!entry?.isIntersecting) return;\n io.disconnect();\n if (wasFirst) return; // already on screen at load: it stays, nothing drops to 0 in front of the reader\n const start = performance.now();\n const tick = (now: number) => {\n const p = Math.min(1, (now - start) / climb);\n el.textContent = formatter.format(value * curve(p));\n if (p < 1) frame = requestAnimationFrame(tick);\n };\n el.textContent = formatter.format(0);\n frame = requestAnimationFrame(tick);\n },\n { threshold: 0.6 },\n );\n io.observe(el);\n return () => {\n io.disconnect();\n cancelAnimationFrame(frame);\n el.textContent = formatter.format(value);\n };\n }, [trigger, value, climb, curve, formatter]);\n\n return (\n <span\n data-align={align}\n data-trend={trigger === \"change\" ? trend : undefined}\n className={cx(\"kz-count\", className)}\n style={{ ...(duration !== undefined ? { \"--kz-count-duration\": `${duration}ms` } : null), ...(easing ? { \"--kz-count-ease\": easing } : null), ...style } as React.CSSProperties}\n {...props}\n >\n <span aria-hidden className=\"kz-count-ghost\">\n {text}\n </span>\n {trigger === \"change\" ? (\n <span aria-hidden className=\"kz-count-roll\">\n {/* Keyed from the right: when a digit is added (999 → 1,000), the units stay the units. */}\n {[...text].map((char, i, all) =>\n /\\d/.test(char) ? (\n <span key={all.length - i} className=\"kz-count-digit\" style={{ \"--kz-digit\": char } as React.CSSProperties}>\n <span>\n {\"0123456789\".split(\"\").map((d) => (\n <span key={d}>{d}</span>\n ))}\n </span>\n </span>\n ) : (\n <span key={all.length - i}>{char}</span>\n ),\n )}\n </span>\n ) : (\n <span aria-hidden ref={live}>\n {text}\n </span>\n )}\n <span className=\"sr-only\" aria-live={announce ? \"polite\" : undefined}>\n {text}\n </span>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
@@ -51,6 +51,11 @@ type TerminalReplayProps = Omit<React.ComponentProps<"figure">, "children" | "ti
51
51
  * @default 900
52
52
  */
53
53
  stepDelay?: number;
54
+ /**
55
+ * A multiplier on every delay (the motion vocabulary's `speed`): 2 plays twice as fast, 0.5 half as fast.
56
+ * @default 1
57
+ */
58
+ speed?: number;
54
59
  /**
55
60
  * The screen's height, in lines; longer sessions scroll.
56
61
  * @default 18
@@ -63,6 +68,6 @@ type TerminalReplayProps = Omit<React.ComponentProps<"figure">, "children" | "ti
63
68
  autoPlay?: boolean;
64
69
  };
65
70
  /** A terminal session played back: commands typed at the prompt, their output line by line — or the whole session at rest. */
66
- declare function TerminalReplay({ steps, label, title, prompt, playLabel, pauseLabel, replayLabel, copyLabel, copiedLabel, typingSpeed, lineDelay, stepDelay, rows, autoPlay, className, style, ...props }: TerminalReplayProps): React.JSX.Element;
71
+ declare function TerminalReplay({ steps, label, title, prompt, playLabel, pauseLabel, replayLabel, copyLabel, copiedLabel, typingSpeed, lineDelay, stepDelay, speed, rows, autoPlay, className, style, ...props }: TerminalReplayProps): React.JSX.Element;
67
72
 
68
73
  export { type TerminalLine, TerminalReplay, type TerminalReplayProps, type TerminalStep, type TerminalTone };
@@ -45,6 +45,7 @@ function TerminalReplay({
45
45
  typingSpeed = 34,
46
46
  lineDelay = 70,
47
47
  stepDelay = 900,
48
+ speed = 1,
48
49
  rows = 18,
49
50
  autoPlay = true,
50
51
  className,
@@ -72,15 +73,17 @@ function TerminalReplay({
72
73
  io.observe(el);
73
74
  return () => io.disconnect();
74
75
  }, [autoPlay]);
75
- const ended = phase === "playing" && !advance(steps, pos, { typing: typingSpeed, line: lineDelay, step: stepDelay });
76
+ const rate = Math.max(0.1, speed);
77
+ const timing = React.useMemo(() => ({ typing: typingSpeed / rate, line: lineDelay / rate, step: stepDelay / rate }), [typingSpeed, lineDelay, stepDelay, rate]);
78
+ const ended = phase === "playing" && !advance(steps, pos, timing);
76
79
  const state = ended ? "done" : phase;
77
80
  React.useEffect(() => {
78
81
  if (phase !== "playing" || !visible) return;
79
- const next = advance(steps, pos, { typing: typingSpeed, line: lineDelay, step: stepDelay });
82
+ const next = advance(steps, pos, timing);
80
83
  if (!next) return;
81
84
  const timer = setTimeout(() => setPos(next[0]), next[1]);
82
85
  return () => clearTimeout(timer);
83
- }, [phase, visible, pos, steps, typingSpeed, lineDelay, stepDelay]);
86
+ }, [phase, visible, pos, steps, timing]);
84
87
  React.useEffect(() => {
85
88
  const el = screenRef.current;
86
89
  if (el && state === "playing") el.scrollTop = el.scrollHeight;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/terminal-replay/terminal-replay.tsx"],"sourcesContent":["// Client (a timer, an observer). A terminal session played back: each command is typed at the prompt, its output\n// arrives line by line, the screen follows the last line like a real terminal. What a reader sees without the motion\n// is the session itself: the server renders it whole (no JavaScript, reduced motion and assistive technology all get\n// the full transcript), the screen has a fixed height (no layout shift while it plays), the replay starts when it\n// scrolls into view, waits while it is off screen, and pauses or replays on demand (WCAG 2.2.2). A bounded chain of\n// timeouts, never a permanent animation frame.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { CheckGlyph, PauseGlyph, PlayGlyph, RetryGlyph } from \"../glyphs\";\n\n/** The colour of an output line — the tokens' text roles, all AA on the screen: secondary, accent, success, warning, danger. */\nexport type TerminalTone = \"muted\" | \"accent\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface TerminalLine {\n /** The line as the program printed it (no colour codes). */\n text: string;\n /** Its colour, when the program coloured it. */\n tone?: TerminalTone;\n}\n\nexport interface TerminalStep {\n /** What is typed at the prompt (`npx orazaka install`). */\n command: string;\n /** What it printed, line by line. */\n output?: readonly (string | TerminalLine)[];\n}\n\nexport type TerminalReplayProps = Omit<React.ComponentProps<\"figure\">, \"children\" | \"title\"> & {\n /** The session, in order. */\n steps: readonly TerminalStep[];\n /** The accessible name of the screen (“Terminal session: install, start, dev”) — passed translated. */\n label: string;\n /** The window's title, shown in its bar (`~/projects — zsh`). */\n title?: React.ReactNode;\n /**\n * What the prompt shows before each command.\n * @default \"$\"\n */\n prompt?: string;\n /** The play button's words (the replay is waiting or paused). */\n playLabel: string;\n /** The pause button's words (the replay is running). */\n pauseLabel: string;\n /** The replay button's words (the session is shown whole). */\n replayLabel: string;\n /** The copy button's words: it copies the commands, one per line. Without it, no copy button. */\n copyLabel?: string;\n /** What the copy button says once the commands are copied (announced politely). */\n copiedLabel?: string;\n /**\n * Milliseconds per typed character.\n * @default 34\n */\n typingSpeed?: number;\n /**\n * Milliseconds between two output lines.\n * @default 70\n */\n lineDelay?: number;\n /**\n * Milliseconds between a command's last line and the next command.\n * @default 900\n */\n stepDelay?: number;\n /**\n * The screen's height, in lines; longer sessions scroll.\n * @default 18\n */\n rows?: number;\n /**\n * Plays the first time it scrolls into view. `false`: the session rests whole until the reader presses play.\n * @default true\n */\n autoPlay?: boolean;\n};\n\ntype Phase = \"final\" | \"idle\" | \"playing\" | \"paused\" | \"done\";\ninterface Position {\n step: number;\n chars: number;\n lines: number;\n}\n\nconst TONE: Record<TerminalTone, string> = {\n muted: \"text-fg-secondary\",\n accent: \"text-fg-accent\",\n success: \"text-fg-success\",\n warning: \"text-fg-warning\",\n danger: \"text-fg-danger\",\n};\n\nconst line = (entry: string | TerminalLine): TerminalLine => (typeof entry === \"string\" ? { text: entry } : entry);\n\n/** The next position of the replay and how long to wait for it; null at the end of the session. */\nfunction advance(steps: readonly TerminalStep[], pos: Position, timing: { typing: number; line: number; step: number }): [Position, number] | null {\n const current = steps[pos.step];\n if (!current) return null;\n const output = current.output ?? [];\n if (pos.chars < current.command.length) {\n // A space is a beat longer: the rhythm of someone typing, not of a printer.\n const pause = current.command[pos.chars] === \" \" ? timing.typing * 2 : timing.typing;\n return [{ ...pos, chars: pos.chars + 1 }, pause];\n }\n if (pos.lines < output.length) return [{ ...pos, lines: pos.lines + 1 }, pos.lines === 0 ? timing.line * 4 : timing.line];\n if (pos.step < steps.length - 1) return [{ step: pos.step + 1, chars: 0, lines: 0 }, timing.step];\n return null;\n}\n\n/** A terminal session played back: commands typed at the prompt, their output line by line — or the whole session at rest. */\nexport function TerminalReplay({\n steps,\n label,\n title,\n prompt = \"$\",\n playLabel,\n pauseLabel,\n replayLabel,\n copyLabel,\n copiedLabel,\n typingSpeed = 34,\n lineDelay = 70,\n stepDelay = 900,\n rows = 18,\n autoPlay = true,\n className,\n style,\n ...props\n}: TerminalReplayProps) {\n const root = React.useRef<HTMLElement>(null);\n const screenRef = React.useRef<HTMLDivElement>(null);\n // The server renders the session whole; the client takes it over unless the reader asked for less motion.\n const [phase, setPhase] = React.useState<Phase>(\"final\");\n const [pos, setPos] = React.useState<Position>({ step: 0, chars: 0, lines: 0 });\n const [visible, setVisible] = React.useState(false);\n const [copied, setCopied] = React.useState(false);\n\n React.useEffect(() => {\n const el = root.current;\n if (!el || !autoPlay || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n setPhase(\"idle\");\n // Starts the first time it is seen; off screen, it waits where it is.\n const io = new IntersectionObserver(\n ([entry]) => {\n const seen = Boolean(entry?.isIntersecting);\n setVisible(seen);\n if (seen) setPhase((current) => (current === \"idle\" ? \"playing\" : current));\n },\n { threshold: 0.35 },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [autoPlay]);\n\n const ended = phase === \"playing\" && !advance(steps, pos, { typing: typingSpeed, line: lineDelay, step: stepDelay });\n // The end of the session is where the replay stands, not an event: derived, never set from an effect.\n const state: Phase = ended ? \"done\" : phase;\n\n React.useEffect(() => {\n if (phase !== \"playing\" || !visible) return;\n const next = advance(steps, pos, { typing: typingSpeed, line: lineDelay, step: stepDelay });\n if (!next) return;\n const timer = setTimeout(() => setPos(next[0]), next[1]);\n return () => clearTimeout(timer);\n }, [phase, visible, pos, steps, typingSpeed, lineDelay, stepDelay]);\n\n // The screen follows the last line while it plays, like a terminal.\n React.useEffect(() => {\n const el = screenRef.current;\n if (el && state === \"playing\") el.scrollTop = el.scrollHeight;\n }, [pos, state]);\n\n React.useEffect(() => {\n if (!copied) return;\n const timer = setTimeout(() => setCopied(false), 1800);\n return () => clearTimeout(timer);\n }, [copied]);\n\n const whole = state === \"final\" || state === \"done\";\n const shown = whole ? steps.length - 1 : pos.step;\n const toggle = () => {\n if (state === \"playing\") setPhase(\"paused\");\n else if (state === \"paused\" || state === \"idle\") setPhase(\"playing\");\n else {\n // Played on demand — also under reduced motion: the reader asked for it. Off screen it would wait: it is on screen.\n setVisible(true);\n setPos({ step: 0, chars: 0, lines: 0 });\n setPhase(\"playing\");\n }\n };\n const control =\n state === \"playing\"\n ? { text: pauseLabel, Glyph: PauseGlyph }\n : state === \"done\"\n ? { text: replayLabel, Glyph: RetryGlyph }\n : { text: playLabel, Glyph: PlayGlyph };\n const commands = steps.map((s) => s.command).join(\"\\n\");\n const transcript = steps.map((s) => [`${prompt} ${s.command}`, ...(s.output ?? []).map((o) => line(o).text)].join(\"\\n\")).join(\"\\n\\n\");\n\n return (\n <figure\n ref={root}\n data-state={state}\n data-autoplay={autoPlay ? \"\" : undefined}\n className={cn(\"kz-replay overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-md\", className)}\n style={{ \"--kz-replay-rows\": rows, ...style } as React.CSSProperties}\n {...props}\n >\n <figcaption className=\"flex items-center gap-3 border-b border-border-subtle bg-surface-2 px-3 py-2 text-xs text-fg-secondary\">\n <span aria-hidden className=\"flex shrink-0 gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n </span>\n <span className=\"min-w-0 flex-1 truncate font-mono\">{title}</span>\n <button\n type=\"button\"\n onClick={toggle}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 font-medium text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <control.Glyph className=\"h-3.5 w-3.5\" />\n {control.text}\n </button>\n {copyLabel && (\n <button\n type=\"button\"\n onClick={() => navigator.clipboard?.writeText(commands).then(() => setCopied(true))}\n data-copied={copied ? \"\" : undefined}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 font-medium text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg focus-visible:outline-2 focus-visible:outline-ring\"\n >\n {copied ? <CheckGlyph className=\"h-3.5 w-3.5\" /> : <CopyGlyph />}\n <span aria-live=\"polite\">{copied ? (copiedLabel ?? copyLabel) : copyLabel}</span>\n </button>\n )}\n </figcaption>\n <div\n ref={screenRef}\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n className=\"kz-replay-screen relative overflow-auto bg-surface-0 px-4 py-3 font-mono text-[0.8125rem] leading-relaxed text-fg focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n >\n <div aria-hidden className=\"kz-replay-live\">\n {steps.slice(0, state === \"idle\" ? 0 : shown + 1).map((step, i) => {\n const current = !whole && i === shown;\n const typed = current ? step.command.slice(0, pos.chars) : step.command;\n const output = (step.output ?? []).slice(0, current ? pos.lines : undefined);\n const typing = current && pos.chars < step.command.length;\n return (\n <div key={i} className={i > 0 ? \"mt-3\" : undefined}>\n <div className=\"whitespace-pre\">\n <span className=\"text-fg-accent select-none\">{prompt} </span>\n <span className=\"font-medium\">{typed}</span>\n {typing && <span className=\"kz-replay-caret\" />}\n </div>\n {output.map((entry, j) => {\n const { text, tone } = line(entry);\n return (\n <div key={j} className={cn(\"min-h-[1lh] whitespace-pre\", tone ? TONE[tone] : \"text-fg\")}>\n {text}\n </div>\n );\n })}\n </div>\n );\n })}\n {(state === \"idle\" || whole) && (\n <div className={cn(\"whitespace-pre\", state !== \"idle\" && \"mt-3\")}>\n <span className=\"text-fg-accent select-none\">{prompt} </span>\n <span className=\"kz-replay-caret\" />\n </div>\n )}\n </div>\n <pre className=\"sr-only\">{transcript}</pre>\n </div>\n </figure>\n );\n}\n\n/** Two sheets, one over the other: copy. Drawn here, like the other glyphs of the primitives (decorative). */\nfunction CopyGlyph() {\n return (\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden focusable=\"false\">\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M5 15V6a2 2 0 0 1 2-2h8\" />\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/terminal-replay/terminal-replay.tsx"],"sourcesContent":["// Client (a timer, an observer). A terminal session played back: each command is typed at the prompt, its output\n// arrives line by line, the screen follows the last line like a real terminal. What a reader sees without the motion\n// is the session itself: the server renders it whole (no JavaScript, reduced motion and assistive technology all get\n// the full transcript), the screen has a fixed height (no layout shift while it plays), the replay starts when it\n// scrolls into view, waits while it is off screen, and pauses or replays on demand (WCAG 2.2.2). A bounded chain of\n// timeouts, never a permanent animation frame.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { CheckGlyph, PauseGlyph, PlayGlyph, RetryGlyph } from \"../glyphs\";\n\n/** The colour of an output line — the tokens' text roles, all AA on the screen: secondary, accent, success, warning, danger. */\nexport type TerminalTone = \"muted\" | \"accent\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface TerminalLine {\n /** The line as the program printed it (no colour codes). */\n text: string;\n /** Its colour, when the program coloured it. */\n tone?: TerminalTone;\n}\n\nexport interface TerminalStep {\n /** What is typed at the prompt (`npx orazaka install`). */\n command: string;\n /** What it printed, line by line. */\n output?: readonly (string | TerminalLine)[];\n}\n\nexport type TerminalReplayProps = Omit<React.ComponentProps<\"figure\">, \"children\" | \"title\"> & {\n /** The session, in order. */\n steps: readonly TerminalStep[];\n /** The accessible name of the screen (“Terminal session: install, start, dev”) — passed translated. */\n label: string;\n /** The window's title, shown in its bar (`~/projects — zsh`). */\n title?: React.ReactNode;\n /**\n * What the prompt shows before each command.\n * @default \"$\"\n */\n prompt?: string;\n /** The play button's words (the replay is waiting or paused). */\n playLabel: string;\n /** The pause button's words (the replay is running). */\n pauseLabel: string;\n /** The replay button's words (the session is shown whole). */\n replayLabel: string;\n /** The copy button's words: it copies the commands, one per line. Without it, no copy button. */\n copyLabel?: string;\n /** What the copy button says once the commands are copied (announced politely). */\n copiedLabel?: string;\n /**\n * Milliseconds per typed character.\n * @default 34\n */\n typingSpeed?: number;\n /**\n * Milliseconds between two output lines.\n * @default 70\n */\n lineDelay?: number;\n /**\n * Milliseconds between a command's last line and the next command.\n * @default 900\n */\n stepDelay?: number;\n /**\n * A multiplier on every delay (the motion vocabulary's `speed`): 2 plays twice as fast, 0.5 half as fast.\n * @default 1\n */\n speed?: number;\n /**\n * The screen's height, in lines; longer sessions scroll.\n * @default 18\n */\n rows?: number;\n /**\n * Plays the first time it scrolls into view. `false`: the session rests whole until the reader presses play.\n * @default true\n */\n autoPlay?: boolean;\n};\n\ntype Phase = \"final\" | \"idle\" | \"playing\" | \"paused\" | \"done\";\ninterface Position {\n step: number;\n chars: number;\n lines: number;\n}\n\nconst TONE: Record<TerminalTone, string> = {\n muted: \"text-fg-secondary\",\n accent: \"text-fg-accent\",\n success: \"text-fg-success\",\n warning: \"text-fg-warning\",\n danger: \"text-fg-danger\",\n};\n\nconst line = (entry: string | TerminalLine): TerminalLine => (typeof entry === \"string\" ? { text: entry } : entry);\n\n/** The next position of the replay and how long to wait for it; null at the end of the session. */\nfunction advance(steps: readonly TerminalStep[], pos: Position, timing: { typing: number; line: number; step: number }): [Position, number] | null {\n const current = steps[pos.step];\n if (!current) return null;\n const output = current.output ?? [];\n if (pos.chars < current.command.length) {\n // A space is a beat longer: the rhythm of someone typing, not of a printer.\n const pause = current.command[pos.chars] === \" \" ? timing.typing * 2 : timing.typing;\n return [{ ...pos, chars: pos.chars + 1 }, pause];\n }\n if (pos.lines < output.length) return [{ ...pos, lines: pos.lines + 1 }, pos.lines === 0 ? timing.line * 4 : timing.line];\n if (pos.step < steps.length - 1) return [{ step: pos.step + 1, chars: 0, lines: 0 }, timing.step];\n return null;\n}\n\n/** A terminal session played back: commands typed at the prompt, their output line by line — or the whole session at rest. */\nexport function TerminalReplay({\n steps,\n label,\n title,\n prompt = \"$\",\n playLabel,\n pauseLabel,\n replayLabel,\n copyLabel,\n copiedLabel,\n typingSpeed = 34,\n lineDelay = 70,\n stepDelay = 900,\n speed = 1,\n rows = 18,\n autoPlay = true,\n className,\n style,\n ...props\n}: TerminalReplayProps) {\n const root = React.useRef<HTMLElement>(null);\n const screenRef = React.useRef<HTMLDivElement>(null);\n // The server renders the session whole; the client takes it over unless the reader asked for less motion.\n const [phase, setPhase] = React.useState<Phase>(\"final\");\n const [pos, setPos] = React.useState<Position>({ step: 0, chars: 0, lines: 0 });\n const [visible, setVisible] = React.useState(false);\n const [copied, setCopied] = React.useState(false);\n\n React.useEffect(() => {\n const el = root.current;\n if (!el || !autoPlay || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n setPhase(\"idle\");\n // Starts the first time it is seen; off screen, it waits where it is.\n const io = new IntersectionObserver(\n ([entry]) => {\n const seen = Boolean(entry?.isIntersecting);\n setVisible(seen);\n if (seen) setPhase((current) => (current === \"idle\" ? \"playing\" : current));\n },\n { threshold: 0.35 },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [autoPlay]);\n\n const rate = Math.max(0.1, speed);\n const timing = React.useMemo(() => ({ typing: typingSpeed / rate, line: lineDelay / rate, step: stepDelay / rate }), [typingSpeed, lineDelay, stepDelay, rate]);\n const ended = phase === \"playing\" && !advance(steps, pos, timing);\n // The end of the session is where the replay stands, not an event: derived, never set from an effect.\n const state: Phase = ended ? \"done\" : phase;\n\n React.useEffect(() => {\n if (phase !== \"playing\" || !visible) return;\n const next = advance(steps, pos, timing);\n if (!next) return;\n const timer = setTimeout(() => setPos(next[0]), next[1]);\n return () => clearTimeout(timer);\n }, [phase, visible, pos, steps, timing]);\n\n // The screen follows the last line while it plays, like a terminal.\n React.useEffect(() => {\n const el = screenRef.current;\n if (el && state === \"playing\") el.scrollTop = el.scrollHeight;\n }, [pos, state]);\n\n React.useEffect(() => {\n if (!copied) return;\n const timer = setTimeout(() => setCopied(false), 1800);\n return () => clearTimeout(timer);\n }, [copied]);\n\n const whole = state === \"final\" || state === \"done\";\n const shown = whole ? steps.length - 1 : pos.step;\n const toggle = () => {\n if (state === \"playing\") setPhase(\"paused\");\n else if (state === \"paused\" || state === \"idle\") setPhase(\"playing\");\n else {\n // Played on demand — also under reduced motion: the reader asked for it. Off screen it would wait: it is on screen.\n setVisible(true);\n setPos({ step: 0, chars: 0, lines: 0 });\n setPhase(\"playing\");\n }\n };\n const control =\n state === \"playing\"\n ? { text: pauseLabel, Glyph: PauseGlyph }\n : state === \"done\"\n ? { text: replayLabel, Glyph: RetryGlyph }\n : { text: playLabel, Glyph: PlayGlyph };\n const commands = steps.map((s) => s.command).join(\"\\n\");\n const transcript = steps.map((s) => [`${prompt} ${s.command}`, ...(s.output ?? []).map((o) => line(o).text)].join(\"\\n\")).join(\"\\n\\n\");\n\n return (\n <figure\n ref={root}\n data-state={state}\n data-autoplay={autoPlay ? \"\" : undefined}\n className={cn(\"kz-replay overflow-hidden rounded-xl border border-border-default bg-surface-1 shadow-md\", className)}\n style={{ \"--kz-replay-rows\": rows, ...style } as React.CSSProperties}\n {...props}\n >\n <figcaption className=\"flex items-center gap-3 border-b border-border-subtle bg-surface-2 px-3 py-2 text-xs text-fg-secondary\">\n <span aria-hidden className=\"flex shrink-0 gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-border-strong\" />\n </span>\n <span className=\"min-w-0 flex-1 truncate font-mono\">{title}</span>\n <button\n type=\"button\"\n onClick={toggle}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 font-medium text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <control.Glyph className=\"h-3.5 w-3.5\" />\n {control.text}\n </button>\n {copyLabel && (\n <button\n type=\"button\"\n onClick={() => navigator.clipboard?.writeText(commands).then(() => setCopied(true))}\n data-copied={copied ? \"\" : undefined}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 font-medium text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg focus-visible:outline-2 focus-visible:outline-ring\"\n >\n {copied ? <CheckGlyph className=\"h-3.5 w-3.5\" /> : <CopyGlyph />}\n <span aria-live=\"polite\">{copied ? (copiedLabel ?? copyLabel) : copyLabel}</span>\n </button>\n )}\n </figcaption>\n <div\n ref={screenRef}\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n className=\"kz-replay-screen relative overflow-auto bg-surface-0 px-4 py-3 font-mono text-[0.8125rem] leading-relaxed text-fg focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring\"\n >\n <div aria-hidden className=\"kz-replay-live\">\n {steps.slice(0, state === \"idle\" ? 0 : shown + 1).map((step, i) => {\n const current = !whole && i === shown;\n const typed = current ? step.command.slice(0, pos.chars) : step.command;\n const output = (step.output ?? []).slice(0, current ? pos.lines : undefined);\n const typing = current && pos.chars < step.command.length;\n return (\n <div key={i} className={i > 0 ? \"mt-3\" : undefined}>\n <div className=\"whitespace-pre\">\n <span className=\"text-fg-accent select-none\">{prompt} </span>\n <span className=\"font-medium\">{typed}</span>\n {typing && <span className=\"kz-replay-caret\" />}\n </div>\n {output.map((entry, j) => {\n const { text, tone } = line(entry);\n return (\n <div key={j} className={cn(\"min-h-[1lh] whitespace-pre\", tone ? TONE[tone] : \"text-fg\")}>\n {text}\n </div>\n );\n })}\n </div>\n );\n })}\n {(state === \"idle\" || whole) && (\n <div className={cn(\"whitespace-pre\", state !== \"idle\" && \"mt-3\")}>\n <span className=\"text-fg-accent select-none\">{prompt} </span>\n <span className=\"kz-replay-caret\" />\n </div>\n )}\n </div>\n <pre className=\"sr-only\">{transcript}</pre>\n </div>\n </figure>\n );\n}\n\n/** Two sheets, one over the other: copy. Drawn here, like the other glyphs of the primitives (decorative). */\nfunction CopyGlyph() {\n return (\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden focusable=\"false\">\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M5 15V6a2 2 0 0 1 2-2h8\" />\n </svg>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAMA,YAAY,WAAW;AAmNf,SACE,KADF;AAhIR,IAAM,OAAqC;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV;AAEA,IAAM,OAAO,CAAC,UAAgD,OAAO,UAAU,WAAW,EAAE,MAAM,MAAM,IAAI;AAG5G,SAAS,QAAQ,OAAgC,KAAe,QAAmF;AACjJ,QAAM,UAAU,MAAM,IAAI,IAAI;AAC9B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAAS,QAAQ,UAAU,CAAC;AAClC,MAAI,IAAI,QAAQ,QAAQ,QAAQ,QAAQ;AAEtC,UAAM,QAAQ,QAAQ,QAAQ,IAAI,KAAK,MAAM,MAAM,OAAO,SAAS,IAAI,OAAO;AAC9E,WAAO,CAAC,EAAE,GAAG,KAAK,OAAO,IAAI,QAAQ,EAAE,GAAG,KAAK;AAAA,EACjD;AACA,MAAI,IAAI,QAAQ,OAAO,OAAQ,QAAO,CAAC,EAAE,GAAG,KAAK,OAAO,IAAI,QAAQ,EAAE,GAAG,IAAI,UAAU,IAAI,OAAO,OAAO,IAAI,OAAO,IAAI;AACxH,MAAI,IAAI,OAAO,MAAM,SAAS,EAAG,QAAO,CAAC,EAAE,MAAM,IAAI,OAAO,GAAG,OAAO,GAAG,OAAO,EAAE,GAAG,OAAO,IAAI;AAChG,SAAO;AACT;AAGO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAwB;AACtB,QAAM,OAAa,aAAoB,IAAI;AAC3C,QAAM,YAAkB,aAAuB,IAAI;AAEnD,QAAM,CAAC,OAAO,QAAQ,IAAU,eAAgB,OAAO;AACvD,QAAM,CAAC,KAAK,MAAM,IAAU,eAAmB,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EAAE,CAAC;AAC9E,QAAM,CAAC,SAAS,UAAU,IAAU,eAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAU,eAAS,KAAK;AAEhD,EAAM,gBAAU,MAAM;AACpB,UAAM,KAAK,KAAK;AAChB,QAAI,CAAC,MAAM,CAAC,YAAY,OAAO,WAAW,kCAAkC,EAAE,QAAS;AACvF,aAAS,MAAM;AAEf,UAAM,KAAK,IAAI;AAAA,MACb,CAAC,CAAC,KAAK,MAAM;AACX,cAAM,OAAO,QAAQ,OAAO,cAAc;AAC1C,mBAAW,IAAI;AACf,YAAI,KAAM,UAAS,CAAC,YAAa,YAAY,SAAS,YAAY,OAAQ;AAAA,MAC5E;AAAA,MACA,EAAE,WAAW,KAAK;AAAA,IACpB;AACA,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,OAAO,KAAK,IAAI,KAAK,KAAK;AAChC,QAAM,SAAe,cAAQ,OAAO,EAAE,QAAQ,cAAc,MAAM,MAAM,YAAY,MAAM,MAAM,YAAY,KAAK,IAAI,CAAC,aAAa,WAAW,WAAW,IAAI,CAAC;AAC9J,QAAM,QAAQ,UAAU,aAAa,CAAC,QAAQ,OAAO,KAAK,MAAM;AAEhE,QAAM,QAAe,QAAQ,SAAS;AAEtC,EAAM,gBAAU,MAAM;AACpB,QAAI,UAAU,aAAa,CAAC,QAAS;AACrC,UAAM,OAAO,QAAQ,OAAO,KAAK,MAAM;AACvC,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,WAAW,MAAM,OAAO,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;AACvD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,OAAO,SAAS,KAAK,OAAO,MAAM,CAAC;AAGvC,EAAM,gBAAU,MAAM;AACpB,UAAM,KAAK,UAAU;AACrB,QAAI,MAAM,UAAU,UAAW,IAAG,YAAY,GAAG;AAAA,EACnD,GAAG,CAAC,KAAK,KAAK,CAAC;AAEf,EAAM,gBAAU,MAAM;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,QAAQ,WAAW,MAAM,UAAU,KAAK,GAAG,IAAI;AACrD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ,UAAU,WAAW,UAAU;AAC7C,QAAM,QAAQ,QAAQ,MAAM,SAAS,IAAI,IAAI;AAC7C,QAAM,SAAS,MAAM;AACnB,QAAI,UAAU,UAAW,UAAS,QAAQ;AAAA,aACjC,UAAU,YAAY,UAAU,OAAQ,UAAS,SAAS;AAAA,SAC9D;AAEH,iBAAW,IAAI;AACf,aAAO,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EAAE,CAAC;AACtC,eAAS,SAAS;AAAA,IACpB;AAAA,EACF;AACA,QAAM,UACJ,UAAU,YACN,EAAE,MAAM,YAAY,OAAO,WAAW,IACtC,UAAU,SACR,EAAE,MAAM,aAAa,OAAO,WAAW,IACvC,EAAE,MAAM,WAAW,OAAO,UAAU;AAC5C,QAAM,WAAW,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,IAAI;AACtD,QAAM,aAAa,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,EAAE,OAAO,IAAI,IAAI,EAAE,UAAU,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,MAAM;AAEpI,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,cAAY;AAAA,MACZ,iBAAe,WAAW,KAAK;AAAA,MAC/B,WAAW,GAAG,4FAA4F,SAAS;AAAA,MACnH,OAAO,EAAE,oBAAoB,MAAM,GAAG,MAAM;AAAA,MAC3C,GAAG;AAAA,MAEJ;AAAA,6BAAC,gBAAW,WAAU,0GACpB;AAAA,+BAAC,UAAK,eAAW,MAAC,WAAU,yBAC1B;AAAA,gCAAC,UAAK,WAAU,6CAA4C;AAAA,YAC5D,oBAAC,UAAK,WAAU,6CAA4C;AAAA,YAC5D,oBAAC,UAAK,WAAU,6CAA4C;AAAA,aAC9D;AAAA,UACA,oBAAC,UAAK,WAAU,qCAAqC,iBAAM;AAAA,UAC3D;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cAEV;AAAA,oCAAC,QAAQ,OAAR,EAAc,WAAU,eAAc;AAAA,gBACtC,QAAQ;AAAA;AAAA;AAAA,UACX;AAAA,UACC,aACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,UAAU,WAAW,UAAU,QAAQ,EAAE,KAAK,MAAM,UAAU,IAAI,CAAC;AAAA,cAClF,eAAa,SAAS,KAAK;AAAA,cAC3B,WAAU;AAAA,cAET;AAAA,yBAAS,oBAAC,cAAW,WAAU,eAAc,IAAK,oBAAC,aAAU;AAAA,gBAC9D,oBAAC,UAAK,aAAU,UAAU,mBAAU,eAAe,YAAa,WAAU;AAAA;AAAA;AAAA,UAC5E;AAAA,WAEJ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,cAAY;AAAA,YACZ,UAAU;AAAA,YACV,WAAU;AAAA,YAEV;AAAA,mCAAC,SAAI,eAAW,MAAC,WAAU,kBACxB;AAAA,sBAAM,MAAM,GAAG,UAAU,SAAS,IAAI,QAAQ,CAAC,EAAE,IAAI,CAAC,MAAM,MAAM;AACjE,wBAAM,UAAU,CAAC,SAAS,MAAM;AAChC,wBAAM,QAAQ,UAAU,KAAK,QAAQ,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK;AAChE,wBAAM,UAAU,KAAK,UAAU,CAAC,GAAG,MAAM,GAAG,UAAU,IAAI,QAAQ,MAAS;AAC3E,wBAAM,SAAS,WAAW,IAAI,QAAQ,KAAK,QAAQ;AACnD,yBACE,qBAAC,SAAY,WAAW,IAAI,IAAI,SAAS,QACvC;AAAA,yCAAC,SAAI,WAAU,kBACb;AAAA,2CAAC,UAAK,WAAU,8BAA8B;AAAA;AAAA,wBAAO;AAAA,yBAAC;AAAA,sBACtD,oBAAC,UAAK,WAAU,eAAe,iBAAM;AAAA,sBACpC,UAAU,oBAAC,UAAK,WAAU,mBAAkB;AAAA,uBAC/C;AAAA,oBACC,OAAO,IAAI,CAAC,OAAO,MAAM;AACxB,4BAAM,EAAE,MAAM,KAAK,IAAI,KAAK,KAAK;AACjC,6BACE,oBAAC,SAAY,WAAW,GAAG,8BAA8B,OAAO,KAAK,IAAI,IAAI,SAAS,GACnF,kBADO,CAEV;AAAA,oBAEJ,CAAC;AAAA,uBAbO,CAcV;AAAA,gBAEJ,CAAC;AAAA,iBACC,UAAU,UAAU,UACpB,qBAAC,SAAI,WAAW,GAAG,kBAAkB,UAAU,UAAU,MAAM,GAC7D;AAAA,uCAAC,UAAK,WAAU,8BAA8B;AAAA;AAAA,oBAAO;AAAA,qBAAC;AAAA,kBACtD,oBAAC,UAAK,WAAU,mBAAkB;AAAA,mBACpC;AAAA,iBAEJ;AAAA,cACA,oBAAC,SAAI,WAAU,WAAW,sBAAW;AAAA;AAAA;AAAA,QACvC;AAAA;AAAA;AAAA,EACF;AAEJ;AAGA,SAAS,YAAY;AACnB,SACE,qBAAC,SAAI,SAAQ,aAAY,WAAU,eAAc,MAAK,QAAO,QAAO,gBAAe,aAAY,KAAI,eAAc,SAAQ,gBAAe,SAAQ,eAAW,MAAC,WAAU,SACpK;AAAA,wBAAC,UAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,IAAG,KAAI;AAAA,IAChD,oBAAC,UAAK,GAAE,2BAA0B;AAAA,KACpC;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.33.0",
3
+ "version": "3.34.0",
4
4
  "description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia, Yasakia, Yororis) and the motion signature — web and /native.",
5
5
  "keywords": [
6
6
  "krizaka",
@@ -721,7 +721,7 @@
721
721
  ],
722
722
  "peerDependencies": {
723
723
  "@krizaka/query": ">=0.2.1",
724
- "@krizaka/tailwind": ">=3.33.0",
724
+ "@krizaka/tailwind": ">=3.34.0",
725
725
  "@maplibre/maplibre-react-native": ">=11",
726
726
  "@shopify/flash-list": ">=2",
727
727
  "expo-camera": ">=16",
@@ -2464,9 +2464,9 @@
2464
2464
  "radix-ui": "^1.7.0",
2465
2465
  "sonner": "^2.0.8",
2466
2466
  "tailwind-variants": "^3.3.1",
2467
- "@krizaka/intl": "^0.2.1",
2468
2467
  "@krizaka/pacer": "^0.1.0",
2469
- "@krizaka/tokens": "^3.33.0"
2468
+ "@krizaka/tokens": "^3.34.0",
2469
+ "@krizaka/intl": "^0.2.1"
2470
2470
  },
2471
2471
  "scripts": {
2472
2472
  "build": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});for(const f of fs.readdirSync('registry/examples',{recursive:true}))if(/\\.(js|d\\.ts)$/.test(f))fs.rmSync('registry/examples/'+f)\" && tsup && node -e \"require('node:fs').copyFileSync('src/motion.css','dist/motion.css');require('node:fs').copyFileSync('src/blocks.css','dist/blocks.css');require('node:fs').copyFileSync('src/story.css','dist/story.css')\" && node scripts/build-registry.mjs",
@@ -339,7 +339,7 @@
339
339
  "description": "`sm`, `md` (default) and `lg`.",
340
340
  "path": "examples/button/sizes.tsx",
341
341
  "module": "@krizaka/ui/registry/examples/button/sizes",
342
- "code": "import { Button } from \"@krizaka/ui/button\";\n\nexport default function ButtonSizes() {\n return (\n <div className=\"flex items-center gap-3\">\n <Button variant=\"primary\" size=\"sm\">\n Continue\n </Button>\n <Button variant=\"primary\" size=\"md\">\n Continue\n </Button>\n <Button variant=\"primary\" size=\"lg\">\n Continue\n </Button>\n </div>\n );\n}\n"
342
+ "code": "import { Button } from \"@krizaka/ui/button\";\n\nexport default function ButtonSizes() {\n return (\n <div className=\"flex flex-wrap items-center gap-3\">\n <Button variant=\"primary\" size=\"sm\">\n Continue\n </Button>\n <Button variant=\"primary\" size=\"md\">\n Continue\n </Button>\n <Button variant=\"primary\" size=\"lg\">\n Continue\n </Button>\n </div>\n );\n}\n"
343
343
  },
344
344
  {
345
345
  "name": "pill",
@@ -1467,7 +1467,7 @@
1467
1467
  },
1468
1468
  {
1469
1469
  "path": "date-picker/day-strip.tsx",
1470
- "content": "// Client. The days ahead as a strip that scrolls sideways — the agenda of the next two weeks, a delivery day, a\n// session. A radio group: one tab stop, the arrows move and pick (Home / End: the first and last day), each day named by\n// its full date; a marked day (it has something) says so in its name. The buttons at the ends page by a screen.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\n\nimport { ChevronGlyph } from \"../glyphs\";\nimport { useControllable, usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { addDays, type IsoDate, toIsoDate, toLocalDate } from \"./calendar\";\n\nexport const dayStrip = tv({\n slots: {\n root: \"flex w-full min-w-0 flex-col gap-2\",\n header: \"flex items-center justify-between gap-2\",\n title: \"text-sm font-semibold capitalize text-fg\",\n navs: \"flex gap-1.5\",\n nav:\n \"inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 bg-sheen text-fg-secondary inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring disabled:pointer-events-none disabled:opacity-35\",\n track:\n \"-mx-1 flex snap-x snap-mandatory gap-2 overflow-x-auto overflow-y-hidden scroll-smooth px-1 py-1.5 [scrollbar-width:none] motion-reduce:scroll-auto [&::-webkit-scrollbar]:hidden\",\n day:\n \"relative flex h-[4.5rem] w-14 shrink-0 snap-start flex-col items-center justify-center gap-0.5 rounded-2xl border border-border-default bg-surface-2 bg-sheen text-fg inset-shadow-edge \" +\n \"transition-[background-color,border-color,box-shadow,transform] duration-150 hover:border-border-strong active:scale-95 \" +\n \"motion-reduce:transition-none motion-reduce:active:scale-100 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"aria-checked:border-transparent aria-checked:bg-accent aria-checked:bg-fill-shade aria-checked:text-on-accent aria-checked:shadow-glow aria-checked:inset-shadow-edge-fill \" +\n \"disabled:cursor-not-allowed disabled:border-dashed disabled:bg-transparent disabled:bg-none disabled:text-fg-muted disabled:shadow-none\",\n weekday: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary group-aria-checked:text-on-accent group-disabled:text-fg-muted\",\n number: \"text-lg font-semibold tabular-nums leading-none\",\n // Today: the node of the marks above the number; a marked day: a dot under it.\n node: \"absolute top-1.5 h-1 w-1 rotate-45 rounded-[1px] bg-accent group-aria-checked:bg-on-accent\",\n mark: \"h-1.5 w-1.5 rounded-full bg-fg-secondary group-aria-checked:bg-on-accent\",\n noMark: \"h-1.5 w-1.5\",\n },\n});\n\nexport type DayStripLabels = {\n /** The button that shows the earlier days (\"Earlier days\"). */\n previous: string;\n /** The button that shows the later days (\"Later days\"). */\n next: string;\n /** Added to the name of a marked day (\"has events\"). */\n marked: string;\n};\n\nexport const dayStripLabels: DayStripLabels = { previous: \"Earlier days\", next: \"Later days\", marked: \"has events\" };\n\nexport type DayStripProps = {\n /** The group's accessible name (\"Delivery day\"), passed translated. */\n label: string;\n /** The BCP 47 locale of the names and the dates: always explicit. */\n locale: string;\n /** The chosen day, ISO (controlled); `null` for none. */\n value?: IsoDate | null;\n /** The chosen day at first, uncontrolled. */\n defaultValue?: IsoDate | null;\n /** Called with the day picked. */\n onValueChange?: (value: IsoDate) => void;\n /** The first day of the strip. Default: today. */\n start?: IsoDate;\n /** How many days. Default 14. */\n days?: number;\n /** Refuses some days (closed, full): drawn dashed, skipped by the arrows. */\n isDateDisabled?: (date: IsoDate) => boolean;\n /** Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name. */\n isDateMarked?: (date: IsoDate) => boolean;\n /** Today (marked by the node): inject it for a stable render or a test. Default: the device's date. */\n today?: IsoDate;\n /** Shows the month of the chosen day (or of the first) and the page buttons above the strip. */\n showHeader?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<DayStripLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** The days ahead in a row that scrolls sideways: one picked, today and the marked days shown. */\nexport function DayStrip({ label, locale, value: valueProp, defaultValue = null, onValueChange, start, days = 14, isDateDisabled, isDateMarked, today: todayProp, showHeader = true, labels: given, className }: DayStripProps) {\n const labels = { ...dayStripLabels, ...given };\n const s = dayStrip();\n const today = todayProp ?? toIsoDate(new Date());\n const first = start ?? today;\n const list = React.useMemo(() => Array.from({ length: days }, (_, i) => addDays(first, i)), [first, days]);\n const [value, setValue] = useControllable<IsoDate | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const enabled = list.filter((d) => !isDateDisabled?.(d));\n const tabStop = value && enabled.includes(value) ? value : enabled[0];\n const track = React.useRef<HTMLDivElement>(null);\n const reduce = usePrefersReducedMotion();\n const weekday = getDateTimeFormat(locale, { weekday: \"short\" });\n const monthTitle = getDateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(toLocalDate(value ?? first));\n\n const reveal = (iso: IsoDate, focus: boolean) => {\n const el = track.current?.querySelector<HTMLButtonElement>(`[data-date=\"${iso}\"]`);\n if (!el) return;\n if (focus) el.focus({ preventScroll: true });\n el.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\", behavior: reduce ? \"auto\" : \"smooth\" });\n };\n // The chosen day is in view when the strip appears.\n React.useEffect(() => {\n if (value) track.current?.querySelector<HTMLElement>(`[data-date=\"${value}\"]`)?.scrollIntoView?.({ block: \"nearest\", inline: \"center\" });\n // Only once: afterwards the person scrolls.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const i = enabled.indexOf(tabStop ?? \"\");\n const moves: Record<string, number> = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: enabled.length - 1 };\n const to = moves[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = enabled[Math.min(enabled.length - 1, Math.max(0, to))];\n if (!next) return;\n setValue(next);\n reveal(next, true);\n };\n const page = (by: number) => {\n const el = track.current;\n if (!el) return;\n el.scrollBy?.({ left: by * el.clientWidth * 0.8, behavior: reduce ? \"auto\" : \"smooth\" });\n };\n\n return (\n <div className={s.root({ className })}>\n {showHeader ? (\n <div className={s.header()}>\n <span className={s.title()} aria-hidden>\n {monthTitle}\n </span>\n <span className={s.navs()}>\n <button type=\"button\" className={s.nav()} aria-label={labels.previous} onClick={() => page(-1)}>\n <ChevronGlyph direction=\"left\" className=\"h-4 w-4\" />\n </button>\n <button type=\"button\" className={s.nav()} aria-label={labels.next} onClick={() => page(1)}>\n <ChevronGlyph direction=\"right\" className=\"h-4 w-4\" />\n </button>\n </span>\n </div>\n ) : null}\n <div ref={track} role=\"radiogroup\" aria-label={label} className={s.track()} onKeyDown={onKeyDown}>\n {list.map((iso) => {\n const marked = Boolean(isDateMarked?.(iso));\n const name = formatDate(toLocalDate(iso), { locale, style: \"long\" });\n return (\n <button\n key={iso}\n type=\"button\"\n role=\"radio\"\n data-date={iso}\n aria-checked={iso === value}\n aria-current={iso === today ? \"date\" : undefined}\n aria-label={marked ? `${name}, ${labels.marked}` : name}\n tabIndex={iso === tabStop ? 0 : -1}\n disabled={Boolean(isDateDisabled?.(iso))}\n className={s.day({ className: \"group\" })}\n onClick={() => setValue(iso)}\n >\n {iso === today ? <span aria-hidden className={s.node()} /> : null}\n <span aria-hidden className={s.weekday()}>\n {weekday.format(toLocalDate(iso)).replace(/\\.$/, \"\")}\n </span>\n <span aria-hidden className={s.number()}>\n {Number(iso.slice(8))}\n </span>\n <span aria-hidden className={marked ? s.mark() : s.noMark()} />\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"
1470
+ "content": "// Client. The days ahead as a strip that scrolls sideways — the agenda of the next two weeks, a delivery day, a\n// session. A radio group: one tab stop, the arrows move and pick (Home / End: the first and last day), each day named by\n// its full date; a marked day (it has something) says so in its name. The buttons at the ends page by a screen.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\n\nimport { ChevronGlyph } from \"../glyphs\";\nimport { useControllable, usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { addDays, type IsoDate, toIsoDate, toLocalDate } from \"./calendar\";\n\nexport const dayStrip = tv({\n slots: {\n root: \"flex w-full min-w-0 flex-col gap-2\",\n header: \"flex items-center justify-between gap-2\",\n title: \"text-sm font-semibold capitalize text-fg\",\n navs: \"flex gap-1.5\",\n nav:\n \"inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 bg-sheen text-fg-secondary inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring disabled:pointer-events-none disabled:opacity-35\",\n track:\n \"-mx-1 flex snap-x snap-mandatory gap-2 overflow-x-auto overflow-y-hidden scroll-smooth px-1 py-1.5 [scrollbar-width:none] motion-reduce:scroll-auto [&::-webkit-scrollbar]:hidden\",\n day:\n \"relative flex h-[4.5rem] w-14 shrink-0 snap-start flex-col items-center justify-center gap-0.5 rounded-2xl border border-border-default bg-surface-2 bg-sheen text-fg inset-shadow-edge \" +\n \"transition-[background-color,border-color,box-shadow,transform] duration-150 hover:border-border-strong active:scale-95 \" +\n \"motion-reduce:transition-none motion-reduce:active:scale-100 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"aria-checked:border-transparent aria-checked:bg-accent aria-checked:bg-fill-shade aria-checked:text-on-accent aria-checked:shadow-glow aria-checked:inset-shadow-edge-fill \" +\n \"disabled:cursor-not-allowed disabled:border-dashed disabled:bg-transparent disabled:bg-none disabled:text-fg-muted disabled:shadow-none\",\n weekday: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary group-aria-checked:text-on-accent group-disabled:text-fg-muted\",\n number: \"text-lg font-semibold tabular-nums leading-none\",\n // Today: the node of the marks above the number; a marked day: a dot under it.\n node: \"absolute top-1.5 h-1 w-1 rotate-45 rounded-[1px] bg-accent group-aria-checked:bg-on-accent\",\n mark: \"h-1.5 w-1.5 rounded-full bg-fg-secondary group-aria-checked:bg-on-accent\",\n noMark: \"h-1.5 w-1.5\",\n },\n});\n\nexport type DayStripLabels = {\n /** The button that shows the earlier days (\"Earlier days\"). */\n previous: string;\n /** The button that shows the later days (\"Later days\"). */\n next: string;\n /** Added to the name of a marked day (\"has events\"). */\n marked: string;\n};\n\nexport const dayStripLabels: DayStripLabels = { previous: \"Earlier days\", next: \"Later days\", marked: \"has events\" };\n\nexport type DayStripProps = {\n /** The group's accessible name (\"Delivery day\"), passed translated. */\n label: string;\n /** The BCP 47 locale of the names and the dates: always explicit. */\n locale: string;\n /** The chosen day, ISO (controlled); `null` for none. */\n value?: IsoDate | null;\n /** The chosen day at first, uncontrolled. */\n defaultValue?: IsoDate | null;\n /** Called with the day picked. */\n onValueChange?: (value: IsoDate) => void;\n /** The first day of the strip. Default: today. */\n start?: IsoDate;\n /** How many days. Default 14. */\n days?: number;\n /** Refuses some days (closed, full): drawn dashed, skipped by the arrows. */\n isDateDisabled?: (date: IsoDate) => boolean;\n /** Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name. */\n isDateMarked?: (date: IsoDate) => boolean;\n /** Today (marked by the node): inject it for a stable render or a test. Default: the device's date. */\n today?: IsoDate;\n /** Shows the month of the chosen day (or of the first) and the page buttons above the strip. */\n showHeader?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<DayStripLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** The days ahead in a row that scrolls sideways: one picked, today and the marked days shown. */\nexport function DayStrip({ label, locale, value: valueProp, defaultValue = null, onValueChange, start, days = 14, isDateDisabled, isDateMarked, today: todayProp, showHeader = true, labels: given, className }: DayStripProps) {\n const labels = { ...dayStripLabels, ...given };\n const s = dayStrip();\n const today = todayProp ?? toIsoDate(new Date());\n const first = start ?? today;\n const list = React.useMemo(() => Array.from({ length: days }, (_, i) => addDays(first, i)), [first, days]);\n const [value, setValue] = useControllable<IsoDate | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const enabled = list.filter((d) => !isDateDisabled?.(d));\n const tabStop = value && enabled.includes(value) ? value : enabled[0];\n const track = React.useRef<HTMLDivElement>(null);\n const reduce = usePrefersReducedMotion();\n const weekday = getDateTimeFormat(locale, { weekday: \"short\" });\n const monthTitle = getDateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(toLocalDate(value ?? first));\n\n // A day is brought into view by scrolling the strip sideways — never with scrollIntoView, which also scrolls the page\n // (a strip low on a page would pull the whole page down to it when it appears).\n const scrollTrackTo = (el: HTMLElement, where: \"center\" | \"nearest\", smooth: boolean) => {\n const strip = track.current;\n if (!strip) return;\n const box = el.getBoundingClientRect();\n const frame = strip.getBoundingClientRect();\n const offset = box.left - frame.left;\n let left = strip.scrollLeft;\n if (where === \"center\") left += offset - (strip.clientWidth - box.width) / 2;\n else if (offset < 0) left += offset;\n else if (offset + box.width > strip.clientWidth) left += offset + box.width - strip.clientWidth;\n if (left === strip.scrollLeft) return;\n if (typeof strip.scrollTo === \"function\") strip.scrollTo({ left, behavior: smooth ? \"smooth\" : \"auto\" });\n else strip.scrollLeft = left;\n };\n const reveal = (iso: IsoDate, focus: boolean) => {\n const el = track.current?.querySelector<HTMLButtonElement>(`[data-date=\"${iso}\"]`);\n if (!el) return;\n if (focus) el.focus({ preventScroll: true });\n scrollTrackTo(el, \"nearest\", !reduce);\n };\n // The chosen day is in view when the strip appears.\n React.useEffect(() => {\n const el = value ? track.current?.querySelector<HTMLElement>(`[data-date=\"${value}\"]`) : null;\n if (el) scrollTrackTo(el, \"center\", false);\n // Only once: afterwards the person scrolls.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const i = enabled.indexOf(tabStop ?? \"\");\n const moves: Record<string, number> = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: enabled.length - 1 };\n const to = moves[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = enabled[Math.min(enabled.length - 1, Math.max(0, to))];\n if (!next) return;\n setValue(next);\n reveal(next, true);\n };\n const page = (by: number) => {\n const el = track.current;\n if (!el) return;\n el.scrollBy?.({ left: by * el.clientWidth * 0.8, behavior: reduce ? \"auto\" : \"smooth\" });\n };\n\n return (\n <div className={s.root({ className })}>\n {showHeader ? (\n <div className={s.header()}>\n <span className={s.title()} aria-hidden>\n {monthTitle}\n </span>\n <span className={s.navs()}>\n <button type=\"button\" className={s.nav()} aria-label={labels.previous} onClick={() => page(-1)}>\n <ChevronGlyph direction=\"left\" className=\"h-4 w-4\" />\n </button>\n <button type=\"button\" className={s.nav()} aria-label={labels.next} onClick={() => page(1)}>\n <ChevronGlyph direction=\"right\" className=\"h-4 w-4\" />\n </button>\n </span>\n </div>\n ) : null}\n <div ref={track} role=\"radiogroup\" aria-label={label} className={s.track()} onKeyDown={onKeyDown}>\n {list.map((iso) => {\n const marked = Boolean(isDateMarked?.(iso));\n const name = formatDate(toLocalDate(iso), { locale, style: \"long\" });\n return (\n <button\n key={iso}\n type=\"button\"\n role=\"radio\"\n data-date={iso}\n aria-checked={iso === value}\n aria-current={iso === today ? \"date\" : undefined}\n aria-label={marked ? `${name}, ${labels.marked}` : name}\n tabIndex={iso === tabStop ? 0 : -1}\n disabled={Boolean(isDateDisabled?.(iso))}\n className={s.day({ className: \"group\" })}\n onClick={() => setValue(iso)}\n >\n {iso === today ? <span aria-hidden className={s.node()} /> : null}\n <span aria-hidden className={s.weekday()}>\n {weekday.format(toLocalDate(iso)).replace(/\\.$/, \"\")}\n </span>\n <span aria-hidden className={s.number()}>\n {Number(iso.slice(8))}\n </span>\n <span aria-hidden className={marked ? s.mark() : s.noMark()} />\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"
1471
1471
  },
1472
1472
  {
1473
1473
  "path": "date-picker/index.ts",
@@ -4,7 +4,7 @@
4
4
  import { Button } from "@krizaka/ui/button";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  function ButtonSizes() {
7
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
7
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
8
8
  /* @__PURE__ */ jsx(Button, { variant: "primary", size: "sm", children: "Continue" }),
9
9
  /* @__PURE__ */ jsx(Button, { variant: "primary", size: "md", children: "Continue" }),
10
10
  /* @__PURE__ */ jsx(Button, { variant: "primary", size: "lg", children: "Continue" })
@@ -2,7 +2,7 @@ import { Button } from "@krizaka/ui/button";
2
2
 
3
3
  export default function ButtonSizes() {
4
4
  return (
5
- <div className="flex items-center gap-3">
5
+ <div className="flex flex-wrap items-center gap-3">
6
6
  <Button variant="primary" size="sm">
7
7
  Continue
8
8
  </Button>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.33.0",
3
+ "version": "3.34.0",
4
4
  "templatePurposes": {
5
5
  "convince": {
6
6
  "title": "Convince",
@@ -3017,7 +3017,7 @@
3017
3017
  "native": 2
3018
3018
  },
3019
3019
  "dependencies": [
3020
- "@krizaka/tokens@^3.33.0",
3020
+ "@krizaka/tokens@^3.34.0",
3021
3021
  "@protomaps/basemaps@5.7.2",
3022
3022
  "tailwind-variants@^3.3.1"
3023
3023
  ],
@@ -3456,7 +3456,7 @@
3456
3456
  "native": 0
3457
3457
  },
3458
3458
  "dependencies": [
3459
- "@krizaka/tokens@^3.33.0",
3459
+ "@krizaka/tokens@^3.34.0",
3460
3460
  "radix-ui@^1.7.0",
3461
3461
  "tailwind-variants@^3.3.1"
3462
3462
  ],
@@ -4088,7 +4088,7 @@
4088
4088
  "native": 0
4089
4089
  },
4090
4090
  "dependencies": [
4091
- "@krizaka/tokens@^3.33.0",
4091
+ "@krizaka/tokens@^3.34.0",
4092
4092
  "radix-ui@^1.7.0",
4093
4093
  "tailwind-variants@^3.3.1"
4094
4094
  ],
@@ -4431,7 +4431,7 @@
4431
4431
  "native": 0
4432
4432
  },
4433
4433
  "dependencies": [
4434
- "@krizaka/tokens@^3.33.0",
4434
+ "@krizaka/tokens@^3.34.0",
4435
4435
  "radix-ui@^1.7.0",
4436
4436
  "tailwind-variants@^3.3.1"
4437
4437
  ],