@krizaka/ui 3.33.1 → 3.35.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;
package/dist/nav-bar.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as tailwind_variants from 'tailwind-variants';
2
2
  import { NavigationMenu } from 'radix-ui';
3
3
  import * as React from 'react';
4
4
  import { LanguageSwitchProps } from './language-switch.js';
5
- import { S as SiteId, F as FamilyLabels } from './sites-B4prB793.js';
5
+ import { S as SiteId, F as FamilyLabels } from './sites-B4sYlfPQ.js';
6
6
 
7
7
  declare const navBar: tailwind_variants.TVReturnType<{
8
8
  floating: {
package/dist/nav-bar.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  GitHubGlyph,
5
5
  familyLabels,
6
6
  sitesByGroup
7
- } from "./chunk-2AJ4NJUF.js";
7
+ } from "./chunk-IANSFIVC.js";
8
8
  import {
9
9
  LanguageSwitch
10
10
  } from "./chunk-3S4GHSPZ.js";
@@ -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":[]}
@@ -1,6 +1,6 @@
1
1
  import * as tailwind_variants from 'tailwind-variants';
2
2
  import * as React from 'react';
3
- import { S as SiteId, F as FamilyLabels } from './sites-B4prB793.js';
3
+ import { S as SiteId, F as FamilyLabels } from './sites-B4sYlfPQ.js';
4
4
 
5
5
  declare const siteFooter: tailwind_variants.TVReturnType<{
6
6
  variant: {
@@ -4,7 +4,7 @@ import {
4
4
  familyLabels,
5
5
  siteById,
6
6
  sitesByGroup
7
- } from "./chunk-2AJ4NJUF.js";
7
+ } from "./chunk-IANSFIVC.js";
8
8
  import {
9
9
  KrizakaLogo
10
10
  } from "./chunk-4E5IUZHR.js";
@@ -1,5 +1,5 @@
1
1
  /** A site of the family. */
2
- type SiteId = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza" | "ui" | "java" | "github";
2
+ type SiteId = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza" | "ui" | "java" | "motion" | "github";
3
3
  /** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */
4
4
  type SiteKind = "company" | "product" | "open-source";
5
5
  /**
package/dist/sites.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** A site of the family. */
2
- type SiteId = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza" | "ui" | "java" | "github";
2
+ type SiteId = "krizaka" | "orazaka" | "orochia" | "yasakia" | "yororis" | "yomizaka" | "kachizaka" | "akinaza" | "ui" | "java" | "motion" | "github";
3
3
  /** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */
4
4
  type SiteKind = "company" | "product" | "open-source";
5
5
  /**
package/dist/sites.js CHANGED
@@ -11,6 +11,8 @@ var sites = [
11
11
  site({ id: "akinaza", name: "Akinaza", url: "https://dev.akinaza.com", kind: "product", group: "products", status: "development", mark: "akinaza", repo: "https://github.com/krizaka/akinaza" }),
12
12
  site({ id: "ui", name: "Krizaka UI", url: "https://ui.krizaka.com", kind: "open-source", group: "open-source", status: "released", mark: "krizaka", repo: "https://github.com/krizaka/krizaka-ui" }),
13
13
  site({ id: "java", name: "Krizaka Java", url: "https://java.krizaka.com", kind: "open-source", group: "open-source", status: "released", mark: "krizaka", repo: "https://github.com/krizaka/krizaka-build" }),
14
+ // The motion lab: built in the open on GitHub Pages; what proves itself there moves into Krizaka UI.
15
+ site({ id: "motion", name: "Krizaka Motion", url: "https://krizaka.github.io/krizaka-motion", kind: "open-source", group: "open-source", status: "development", mark: "krizaka", repo: "https://github.com/krizaka/krizaka-motion" }),
14
16
  site({ id: "github", name: "GitHub", url: "https://github.com/krizaka", kind: "open-source", group: "github", status: "released", mark: "github", repo: "https://github.com/krizaka" })
15
17
  ];
16
18
  var siteGroups = ["krizaka", "products", "open-source", "github"];
@@ -39,6 +41,7 @@ var familyLabels = {
39
41
  akinaza: "Running a shop in Qu\xE9bec, for less \u2014 Loi 25 by design.",
40
42
  ui: "Components, tokens, icons and charts \u2014 web and React Native.",
41
43
  java: "Spring Boot building blocks on Maven Central.",
44
+ motion: "The motion lab: the kit's motion, played live with its knobs.",
42
45
  github: "Every repository, in the open."
43
46
  },
44
47
  signature: { company: "A Krizaka company", product: "A Krizaka product", "open-source": "A Krizaka open-source project" },
@@ -61,6 +64,7 @@ var familyLabels = {
61
64
  akinaza: "Tenir une boutique au Qu\xE9bec, pour moins cher \u2014 Loi 25 d\xE8s la conception.",
62
65
  ui: "Composants, jetons, ic\xF4nes et graphiques \u2014 web et React Native.",
63
66
  java: "Des briques Spring Boot sur Maven Central.",
67
+ motion: "Le labo du mouvement : le mouvement du kit, jou\xE9 en direct, avec ses r\xE9glages.",
64
68
  github: "Tous les d\xE9p\xF4ts, au grand jour."
65
69
  },
66
70
  signature: { company: "Une compagnie Krizaka", product: "Un produit Krizaka", "open-source": "Un projet open source de Krizaka" },
package/dist/sites.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sites/sites.ts"],"sourcesContent":["// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing offered (a QA-only app may run; the public page says so). `development`: being built in the\n * open, with a dev site (said \"In development\"). A site with none yet would use its repository as its address.\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://dev.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://dev.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://dev.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://dev.yororis.com\", kind: \"product\", group: \"products\", status: \"construction\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://dev.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://dev.kachizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://dev.akinaza.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI on your own servers: chat, documents, images and video — and the engine under the other products.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"Meet real people: introductions, and photos checked before you see them.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Yomizaka reads your leases, contracts and invoices.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine sur vos propres serveurs : conversation, documents, images et vidéo — et le moteur des autres produits.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"Rencontrez de vraies personnes : des présentations, et des photos vérifiées avant que vous les voyiez.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Yomizaka lit vos baux, contrats et factures.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sites/sites.ts"],"sourcesContent":["// Server-safe: plain data. The Krizaka family — every site Krizaka runs, once. The family switcher of the bar\n// (`NavBar.Family`) and the common footer (`SiteFooter`) both read it, so adding a site is one release of this package:\n// every bar and every footer of the family follow.\n//\n// The words of the family (a site's one line, the groups, the statuses, the footer's signature) are brand data, written\n// here in English and French (`familyLabels`) — a site passes the set of its locale, or its own translation.\n\n/** A site of the family. */\nexport type SiteId = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"ui\" | \"java\" | \"motion\" | \"github\";\n\n/** What a site is: the company, a product, or an open-source project. It chooses the footer's signature line. */\nexport type SiteKind = \"company\" | \"product\" | \"open-source\";\n\n/**\n * `released`: open to visit. `upcoming`: announced, not released yet (said \"Coming soon\"). `construction`: under\n * construction — nothing offered (a QA-only app may run; the public page says so). `development`: being built in the\n * open, with a dev site (said \"In development\"). A site with none yet would use its repository as its address.\n */\nexport type SiteStatus = \"released\" | \"upcoming\" | \"construction\" | \"development\";\n\n/** The groups of the family, in reading order: Krizaka · Products · Open source · GitHub. */\nexport type SiteGroup = \"krizaka\" | \"products\" | \"open-source\" | \"github\";\n\n/** The mark a site wears: a brand of `ProductLogo` (Krizaka UI and Krizaka Java wear Krizaka's), or GitHub's. */\nexport type SiteMark = \"krizaka\" | \"orazaka\" | \"orochia\" | \"yasakia\" | \"yororis\" | \"yomizaka\" | \"kachizaka\" | \"akinaza\" | \"github\";\n\nexport interface Site {\n readonly id: SiteId;\n /** The name, never translated. */\n readonly name: string;\n /** The address of the site (its home). */\n readonly url: string;\n /** The address without the scheme, as people say it (`ui.krizaka.com`). */\n readonly host: string;\n readonly kind: SiteKind;\n readonly group: SiteGroup;\n readonly status: SiteStatus;\n readonly mark: SiteMark;\n /** The repository the site is built from. */\n readonly repo: string;\n}\n\nconst site = (s: Omit<Site, \"host\">): Site => ({ ...s, host: s.url.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\") });\n\n/** Every site of the family, in reading order (the group order, then the order inside each group). */\nexport const sites: readonly Site[] = [\n site({ id: \"krizaka\", name: \"Krizaka\", url: \"https://www.krizaka.com\", kind: \"company\", group: \"krizaka\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-com\" }),\n site({ id: \"orazaka\", name: \"Orazaka\", url: \"https://dev.orazaka.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orazaka\", repo: \"https://github.com/krizaka/orazaka\" }),\n site({ id: \"orochia\", name: \"Orochia\", url: \"https://dev.orochia.com\", kind: \"product\", group: \"products\", status: \"released\", mark: \"orochia\", repo: \"https://github.com/krizaka/orochia\" }),\n site({ id: \"yasakia\", name: \"Yasakia\", url: \"https://dev.yasakia.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yasakia\", repo: \"https://github.com/krizaka/yasakia\" }),\n site({ id: \"yororis\", name: \"Yororis\", url: \"https://dev.yororis.com\", kind: \"product\", group: \"products\", status: \"construction\", mark: \"yororis\", repo: \"https://github.com/krizaka/yororis\" }),\n site({ id: \"yomizaka\", name: \"Yomizaka\", url: \"https://dev.yomizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"yomizaka\", repo: \"https://github.com/krizaka/yomizaka\" }),\n site({ id: \"kachizaka\", name: \"Kachizaka\", url: \"https://dev.kachizaka.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"kachizaka\", repo: \"https://github.com/krizaka/kachizaka\" }),\n site({ id: \"akinaza\", name: \"Akinaza\", url: \"https://dev.akinaza.com\", kind: \"product\", group: \"products\", status: \"development\", mark: \"akinaza\", repo: \"https://github.com/krizaka/akinaza\" }),\n site({ id: \"ui\", name: \"Krizaka UI\", url: \"https://ui.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-ui\" }),\n site({ id: \"java\", name: \"Krizaka Java\", url: \"https://java.krizaka.com\", kind: \"open-source\", group: \"open-source\", status: \"released\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-build\" }),\n // The motion lab: built in the open on GitHub Pages; what proves itself there moves into Krizaka UI.\n site({ id: \"motion\", name: \"Krizaka Motion\", url: \"https://krizaka.github.io/krizaka-motion\", kind: \"open-source\", group: \"open-source\", status: \"development\", mark: \"krizaka\", repo: \"https://github.com/krizaka/krizaka-motion\" }),\n site({ id: \"github\", name: \"GitHub\", url: \"https://github.com/krizaka\", kind: \"open-source\", group: \"github\", status: \"released\", mark: \"github\", repo: \"https://github.com/krizaka\" }),\n];\n\n/** The groups, in reading order. */\nexport const siteGroups: readonly SiteGroup[] = [\"krizaka\", \"products\", \"open-source\", \"github\"];\n\n/** A site by id. */\nexport function siteById(id: SiteId): Site {\n const found = sites.find((s) => s.id === id);\n if (!found) throw new Error(`Unknown Krizaka site \"${id}\".`);\n return found;\n}\n\n/** The family grouped, in reading order: `[{ group, sites }]`, empty groups left out. */\nexport function sitesByGroup(): { group: SiteGroup; sites: Site[] }[] {\n return siteGroups.map((group) => ({ group, sites: sites.filter((s) => s.group === group) })).filter((g) => g.sites.length > 0);\n}\n\n/** The languages the family's words are written in. */\nexport type SiteLocale = \"en\" | \"fr\";\n\n/** The words of the family: the switcher, the footer, each site's one line. */\nexport interface FamilyLabels {\n /** The family's name: the switcher's navigation and its button, the footer's navigation (“Krizaka family”). */\n family: string;\n /** The heading of each group. */\n groups: Record<SiteGroup, string>;\n /** The badge of a site that is not released. */\n status: Record<Exclude<SiteStatus, \"released\">, string>;\n /** Says which site is the current one (“You are here”). */\n current: string;\n /** One line per site: what it is. */\n descriptions: Record<SiteId, string>;\n /** The footer's signature, by the kind of the current site (“A Krizaka product”). */\n signature: Record<SiteKind, string>;\n /** The footer's place line (“Made in Montréal”). */\n place: string;\n /** The accessible name of the version (“Version”). */\n version: string;\n}\n\n/** The family's words in English and French. Pass `familyLabels[locale]`, or your own translation of the same shape. */\nexport const familyLabels: Record<SiteLocale, FamilyLabels> = {\n en: {\n family: \"Krizaka family\",\n groups: { krizaka: \"Krizaka\", products: \"Products\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Coming soon\", construction: \"Under construction\", development: \"In development\" },\n current: \"You are here\",\n descriptions: {\n krizaka: \"The company: our products, our open source, our story.\",\n orazaka: \"Sovereign AI on your own servers: chat, documents, images and video — and the engine under the other products.\",\n orochia: \"A video platform for creators.\",\n yasakia: \"Meet real people: introductions, and photos checked before you see them.\",\n yororis: \"A listening space with a face and a voice.\",\n yomizaka: \"Yomizaka reads your leases, contracts and invoices.\",\n kachizaka: \"Predictions among friends — live data, in real time.\",\n akinaza: \"Running a shop in Québec, for less — Loi 25 by design.\",\n ui: \"Components, tokens, icons and charts — web and React Native.\",\n java: \"Spring Boot building blocks on Maven Central.\",\n motion: \"The motion lab: the kit's motion, played live with its knobs.\",\n github: \"Every repository, in the open.\",\n },\n signature: { company: \"A Krizaka company\", product: \"A Krizaka product\", \"open-source\": \"A Krizaka open-source project\" },\n place: \"Made in Montréal\",\n version: \"Version\",\n },\n fr: {\n family: \"Famille Krizaka\",\n groups: { krizaka: \"Krizaka\", products: \"Produits\", \"open-source\": \"Open source\", github: \"GitHub\" },\n status: { upcoming: \"Bientôt\", construction: \"En construction\", development: \"En développement\" },\n current: \"Vous êtes ici\",\n descriptions: {\n krizaka: \"La compagnie : nos produits, notre open source, notre histoire.\",\n orazaka: \"L’IA souveraine sur vos propres serveurs : conversation, documents, images et vidéo — et le moteur des autres produits.\",\n orochia: \"Une plateforme vidéo pour les créateurs.\",\n yasakia: \"Rencontrez de vraies personnes : des présentations, et des photos vérifiées avant que vous les voyiez.\",\n yororis: \"Un espace d’écoute avec un visage et une voix.\",\n yomizaka: \"Yomizaka lit vos baux, contrats et factures.\",\n kachizaka: \"Les pronostics entre amis — des données en direct.\",\n akinaza: \"Tenir une boutique au Québec, pour moins cher — Loi 25 dès la conception.\",\n ui: \"Composants, jetons, icônes et graphiques — web et React Native.\",\n java: \"Des briques Spring Boot sur Maven Central.\",\n motion: \"Le labo du mouvement : le mouvement du kit, joué en direct, avec ses réglages.\",\n github: \"Tous les dépôts, au grand jour.\",\n },\n signature: { company: \"Une compagnie Krizaka\", product: \"Un produit Krizaka\", \"open-source\": \"Un projet open source de Krizaka\" },\n place: \"Fait à Montréal\",\n version: \"Version\",\n },\n};\n"],"mappings":";AA0CA,IAAM,OAAO,CAAC,OAAiC,EAAE,GAAG,GAAG,MAAM,EAAE,IAAI,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE,EAAE;AAG3G,IAAM,QAAyB;AAAA,EACpC,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,WAAW,QAAQ,YAAY,MAAM,WAAW,MAAM,yCAAyC,CAAC;AAAA,EAC/L,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,YAAY,QAAQ,YAAY,MAAM,WAAW,MAAM,qCAAqC,CAAC;AAAA,EAC5L,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,YAAY,QAAQ,YAAY,MAAM,WAAW,MAAM,qCAAqC,CAAC;AAAA,EAC5L,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,YAAY,QAAQ,eAAe,MAAM,WAAW,MAAM,qCAAqC,CAAC;AAAA,EAC/L,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,YAAY,QAAQ,gBAAgB,MAAM,WAAW,MAAM,qCAAqC,CAAC;AAAA,EAChM,KAAK,EAAE,IAAI,YAAY,MAAM,YAAY,KAAK,4BAA4B,MAAM,WAAW,OAAO,YAAY,QAAQ,eAAe,MAAM,YAAY,MAAM,sCAAsC,CAAC;AAAA,EACpM,KAAK,EAAE,IAAI,aAAa,MAAM,aAAa,KAAK,6BAA6B,MAAM,WAAW,OAAO,YAAY,QAAQ,eAAe,MAAM,aAAa,MAAM,uCAAuC,CAAC;AAAA,EACzM,KAAK,EAAE,IAAI,WAAW,MAAM,WAAW,KAAK,2BAA2B,MAAM,WAAW,OAAO,YAAY,QAAQ,eAAe,MAAM,WAAW,MAAM,qCAAqC,CAAC;AAAA,EAC/L,KAAK,EAAE,IAAI,MAAM,MAAM,cAAc,KAAK,0BAA0B,MAAM,eAAe,OAAO,eAAe,QAAQ,YAAY,MAAM,WAAW,MAAM,wCAAwC,CAAC;AAAA,EACnM,KAAK,EAAE,IAAI,QAAQ,MAAM,gBAAgB,KAAK,4BAA4B,MAAM,eAAe,OAAO,eAAe,QAAQ,YAAY,MAAM,WAAW,MAAM,2CAA2C,CAAC;AAAA;AAAA,EAE5M,KAAK,EAAE,IAAI,UAAU,MAAM,kBAAkB,KAAK,4CAA4C,MAAM,eAAe,OAAO,eAAe,QAAQ,eAAe,MAAM,WAAW,MAAM,4CAA4C,CAAC;AAAA,EACpO,KAAK,EAAE,IAAI,UAAU,MAAM,UAAU,KAAK,8BAA8B,MAAM,eAAe,OAAO,UAAU,QAAQ,YAAY,MAAM,UAAU,MAAM,6BAA6B,CAAC;AACxL;AAGO,IAAM,aAAmC,CAAC,WAAW,YAAY,eAAe,QAAQ;AAGxF,SAAS,SAAS,IAAkB;AACzC,QAAM,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAC3C,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,yBAAyB,EAAE,IAAI;AAC3D,SAAO;AACT;AAGO,SAAS,eAAsD;AACpE,SAAO,WAAW,IAAI,CAAC,WAAW,EAAE,OAAO,OAAO,MAAM,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC;AAC/H;AA0BO,IAAM,eAAiD;AAAA,EAC5D,IAAI;AAAA,IACF,QAAQ;AAAA,IACR,QAAQ,EAAE,SAAS,WAAW,UAAU,YAAY,eAAe,eAAe,QAAQ,SAAS;AAAA,IACnG,QAAQ,EAAE,UAAU,eAAe,cAAc,sBAAsB,aAAa,iBAAiB;AAAA,IACrG,SAAS;AAAA,IACT,cAAc;AAAA,MACZ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAS;AAAA,MACT,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,IACA,WAAW,EAAE,SAAS,qBAAqB,SAAS,qBAAqB,eAAe,gCAAgC;AAAA,IACxH,OAAO;AAAA,IACP,SAAS;AAAA,EACX;AAAA,EACA,IAAI;AAAA,IACF,QAAQ;AAAA,IACR,QAAQ,EAAE,SAAS,WAAW,UAAU,YAAY,eAAe,eAAe,QAAQ,SAAS;AAAA,IACnG,QAAQ,EAAE,UAAU,cAAW,cAAc,mBAAmB,aAAa,sBAAmB;AAAA,IAChG,SAAS;AAAA,IACT,cAAc;AAAA,MACZ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAS;AAAA,MACT,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,IACA,WAAW,EAAE,SAAS,yBAAyB,SAAS,sBAAsB,eAAe,mCAAmC;AAAA,IAChI,OAAO;AAAA,IACP,SAAS;AAAA,EACX;AACF;","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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.33.1",
3
+ "version": "3.35.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.1",
724
+ "@krizaka/tailwind": ">=3.35.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/tokens": "^3.33.1",
2468
2467
  "@krizaka/intl": "^0.2.1",
2469
- "@krizaka/pacer": "^0.1.0"
2468
+ "@krizaka/pacer": "^0.1.0",
2469
+ "@krizaka/tokens": "^3.35.0"
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",
@@ -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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krizaka/ui",
3
- "version": "3.33.1",
3
+ "version": "3.35.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.1",
3020
+ "@krizaka/tokens@^3.35.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.1",
3459
+ "@krizaka/tokens@^3.35.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.1",
4091
+ "@krizaka/tokens@^3.35.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.1",
4434
+ "@krizaka/tokens@^3.35.0",
4435
4435
  "radix-ui@^1.7.0",
4436
4436
  "tailwind-variants@^3.3.1"
4437
4437
  ],