@krizaka/ui 3.44.0 → 3.46.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/meter.js CHANGED
@@ -1,5 +1,32 @@
1
1
  // src/meter/meter.tsx
2
2
  import { tv } from "tailwind-variants";
3
+
4
+ // src/meter/model.ts
5
+ var MeterOptimum = { Low: "low", High: "high" };
6
+ var MeterLevel = { Ok: "ok", Success: "success", Warning: "warning", Danger: "danger" };
7
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
8
+ var percent = (ratio) => `${Math.round(ratio * 100)} %`;
9
+ function levelOf(ratio, thresholds, optimum) {
10
+ if (!thresholds) return MeterLevel.Ok;
11
+ const good = optimum === MeterOptimum.Low ? MeterLevel.Success : MeterLevel.Danger;
12
+ const bad = optimum === MeterOptimum.Low ? MeterLevel.Danger : MeterLevel.Success;
13
+ if (ratio >= thresholds.high) return bad;
14
+ return ratio >= thresholds.low ? MeterLevel.Warning : good;
15
+ }
16
+ function meterModel({ value = 0, min = 0, max = 100, valueText, thresholds, optimum = MeterOptimum.Low, parts }) {
17
+ const span = max - min || 1;
18
+ const ratio = clamp01((value - min) / span);
19
+ const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: clamp01(p.value / span), words: p.valueText ?? percent(p.value / span) }));
20
+ let start = 0;
21
+ const arcs = parts ? shares.map((p) => {
22
+ const arc = { part: p.i, len: p.ratio, start };
23
+ start += p.ratio;
24
+ return arc;
25
+ }) : [{ part: void 0, len: ratio, start: 0 }];
26
+ return { ratio, level: levelOf(ratio, thresholds, optimum), words: valueText ?? percent(ratio), shares, arcs };
27
+ }
28
+
29
+ // src/meter/meter.tsx
3
30
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
31
  var meter = tv({
5
32
  slots: {
@@ -40,11 +67,7 @@ function Meter({
40
67
  ...props
41
68
  }) {
42
69
  const s = meter({ size });
43
- const span = max - min || 1;
44
- const ratio = Math.min(1, Math.max(0, (value - min) / span));
45
- const level = !thresholds ? "ok" : ratio >= thresholds.high ? optimum === "low" ? "danger" : "success" : ratio >= thresholds.low ? "warning" : optimum === "low" ? "success" : "danger";
46
- const words = valueText ?? `${Math.round(ratio * 100)} %`;
47
- const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: Math.min(1, Math.max(0, p.value / span)), words: p.valueText ?? `${Math.round(p.value / span * 100)} %` }));
70
+ const { ratio, level, words, shares, arcs } = meterModel({ value, min, max, valueText, thresholds, optimum, parts });
48
71
  const a11y = parts ? { role: "group", "aria-label": label } : { role: "meter", "aria-label": label, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Math.min(max, Math.max(min, value)), "aria-valuetext": words };
49
72
  const legend = parts && /* @__PURE__ */ jsx("ul", { className: s.legend(), children: shares.map((p) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5", children: [
50
73
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: [s.swatch(), PART_BG[p.i % PART_BG.length]].join(" ") }),
@@ -53,31 +76,28 @@ function Meter({
53
76
  ] }, p.i)) });
54
77
  if (variant === "ring") {
55
78
  const r = 44;
56
- let offset = 0;
57
- const arcs = parts ? shares.map((p) => {
58
- const arc = { cls: PART_STROKE[p.i % PART_STROKE.length], len: p.ratio * 100, start: offset };
59
- offset += arc.len;
60
- return arc;
61
- }) : [{ cls: STROKE[level], len: ratio * 100, start: 0 }];
62
79
  return /* @__PURE__ */ jsxs("div", { ...a11y, "data-level": parts ? void 0 : level, className: ["inline-flex flex-col items-center gap-2", className].filter(Boolean).join(" "), ...props, children: [
63
80
  /* @__PURE__ */ jsxs("div", { className: s.ring(), children: [
64
81
  /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 100 100", className: "h-full w-full -rotate-90", "aria-hidden": true, children: [
65
82
  /* @__PURE__ */ jsx("circle", { cx: "50", cy: "50", r, fill: "none", strokeWidth: "9", className: "stroke-border-default" }),
66
- arcs.map((a, i) => /* @__PURE__ */ jsx(
67
- "circle",
68
- {
69
- cx: "50",
70
- cy: "50",
71
- r,
72
- fill: "none",
73
- strokeWidth: "9",
74
- pathLength: 100,
75
- strokeDasharray: `${Math.max(0, a.len - (parts ? 1 : 0))} ${100 - Math.max(0, a.len - (parts ? 1 : 0))}`,
76
- strokeDashoffset: -a.start,
77
- className: [a.cls, "transition-[stroke-dasharray] duration-700 motion-reduce:transition-none"].join(" ")
78
- },
79
- i
80
- ))
83
+ arcs.map((arc, i) => {
84
+ const length = Math.max(0, arc.len * 100 - (parts ? 1 : 0));
85
+ return /* @__PURE__ */ jsx(
86
+ "circle",
87
+ {
88
+ cx: "50",
89
+ cy: "50",
90
+ r,
91
+ fill: "none",
92
+ strokeWidth: "9",
93
+ pathLength: 100,
94
+ strokeDasharray: `${length} ${100 - length}`,
95
+ strokeDashoffset: -arc.start * 100,
96
+ className: [arc.part === void 0 ? STROKE[level] : PART_STROKE[arc.part % PART_STROKE.length], "transition-[stroke-dasharray] duration-700 motion-reduce:transition-none"].join(" ")
97
+ },
98
+ i
99
+ );
100
+ })
81
101
  ] }),
82
102
  children !== void 0 && /* @__PURE__ */ jsx("div", { className: s.center(), children })
83
103
  ] }),
package/dist/meter.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/meter/meter.tsx"],"sourcesContent":["// Server-safe. A measure within a known range — a quota, a balance, a share — as opposed to the progress of a task.\n// `role=\"meter\"`, its value in words; thresholds turn it to warning then danger (or the reverse when high is good);\n// split into parts (90 % to the creator, 10 % to the platform) it becomes a stacked bar or ring with its legend: the\n// text version of what it draws.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nexport const meter = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n header: \"flex items-baseline gap-2 text-xs\",\n caption: \"min-w-0 flex-1 truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n track: \"relative flex h-2 w-full gap-0.5 overflow-hidden rounded-sm bg-surface-3\",\n fill: \"h-full rounded-sm transition-[width] duration-700 motion-reduce:transition-none\",\n legend: \"mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-fg-secondary\",\n swatch: \"inline-block h-2 w-2 shrink-0 rounded-full\",\n ring: \"relative inline-flex shrink-0 items-center justify-center\",\n center: \"absolute inset-0 flex flex-col items-center justify-center text-center\",\n },\n variants: {\n size: { sm: { track: \"h-1.5\", ring: \"h-14 w-14\" }, md: { ring: \"h-24 w-24\" }, lg: { track: \"h-3\", ring: \"h-36 w-36\" } },\n },\n defaultVariants: { size: \"md\" },\n});\n\n/** The colour of a fill: by tone (thresholds) or by part (the accent, a neutral, accent-2, a lighter neutral). */\nconst BG = { ok: \"bg-accent\", success: \"bg-success\", warning: \"bg-warning\", danger: \"bg-danger\" } as const;\nconst STROKE = { ok: \"stroke-accent\", success: \"stroke-success\", warning: \"stroke-warning\", danger: \"stroke-danger\" } as const;\nconst PART_BG = [\"bg-accent\", \"bg-fg-secondary\", \"bg-accent-2\", \"bg-border-strong\"];\nconst PART_STROKE = [\"stroke-accent\", \"stroke-fg-secondary\", \"stroke-accent-2\", \"stroke-border-strong\"];\n\nexport interface MeterPart {\n /** Its name (“To the creator”) — passed translated. */\n label: string;\n /** Its value, in the meter's unit. */\n value: number;\n /** Its value in words (“€9.00”), in the legend and for assistive technology; default: its percentage. */\n valueText?: string;\n}\n\nexport type MeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n /** The accessible name (“Storage”, “Split of a tip”) — passed translated; shown with `showLabel`. */\n label: string;\n /** The value, between `min` and `max` (ignored with `parts`). */\n value?: number;\n /**\n * The bottom of the range.\n * @default 0\n */\n min?: number;\n /**\n * The top of the range.\n * @default 100\n */\n max?: number;\n /** The value in words (“42 GB of 50 GB”) — passed translated; default: the percentage. */\n valueText?: string;\n /**\n * Ratios (0–1) where the tone changes: past `high` it is in danger, past `low` in warning — or the reverse with\n * `optimum=\"high\"` (a balance: the lower, the worse). Without thresholds the fill is the brand accent.\n */\n thresholds?: { readonly low: number; readonly high: number };\n /**\n * Which end is good.\n * @default \"low\"\n */\n optimum?: \"low\" | \"high\";\n /** Split into parts that add up to `max`: a stacked bar or ring, with its legend. */\n parts?: readonly MeterPart[];\n /**\n * bar · ring.\n * @default \"bar\"\n */\n variant?: \"bar\" | \"ring\";\n /**\n * sm · md · lg: the bar's thickness, the ring's diameter.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Shows the label and the value above the bar. */\n showLabel?: boolean;\n /** A ring's centre: the value, a word. */\n children?: React.ReactNode;\n};\n\n/** A measure within a range — a quota, a balance, a share — toned by thresholds or split into parts. */\nexport function Meter({\n label,\n value = 0,\n min = 0,\n max = 100,\n valueText,\n thresholds,\n optimum = \"low\",\n parts,\n variant = \"bar\",\n size = \"md\",\n showLabel,\n className,\n children,\n ...props\n}: MeterProps) {\n const s = meter({ size });\n const span = max - min || 1;\n const ratio = Math.min(1, Math.max(0, (value - min) / span));\n const level = !thresholds ? \"ok\" : ratio >= thresholds.high ? (optimum === \"low\" ? \"danger\" : \"success\") : ratio >= thresholds.low ? \"warning\" : optimum === \"low\" ? \"success\" : \"danger\";\n const words = valueText ?? `${Math.round(ratio * 100)} %`;\n const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: Math.min(1, Math.max(0, p.value / span)), words: p.valueText ?? `${Math.round((p.value / span) * 100)} %` }));\n const a11y = parts\n ? { role: \"group\" as const, \"aria-label\": label }\n : { role: \"meter\" as const, \"aria-label\": label, \"aria-valuemin\": min, \"aria-valuemax\": max, \"aria-valuenow\": Math.min(max, Math.max(min, value)), \"aria-valuetext\": words };\n const legend = parts && (\n <ul className={s.legend()}>\n {shares.map((p) => (\n <li key={p.i} className=\"flex items-center gap-1.5\">\n <span aria-hidden className={[s.swatch(), PART_BG[p.i % PART_BG.length]].join(\" \")} />\n <span className=\"text-fg\">{p.label}</span>\n <span className=\"tabular-nums\">{p.words}</span>\n </li>\n ))}\n </ul>\n );\n\n if (variant === \"ring\") {\n const r = 44;\n let offset = 0;\n const arcs = parts\n ? shares.map((p) => {\n const arc = { cls: PART_STROKE[p.i % PART_STROKE.length]!, len: p.ratio * 100, start: offset };\n offset += arc.len;\n return arc;\n })\n : [{ cls: STROKE[level], len: ratio * 100, start: 0 }];\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={[\"inline-flex flex-col items-center gap-2\", className].filter(Boolean).join(\" \")} {...props}>\n <div className={s.ring()}>\n <svg viewBox=\"0 0 100 100\" className=\"h-full w-full -rotate-90\" aria-hidden>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth=\"9\" className=\"stroke-border-default\" />\n {arcs.map((a, i) => (\n <circle\n key={i}\n cx=\"50\"\n cy=\"50\"\n r={r}\n fill=\"none\"\n strokeWidth=\"9\"\n pathLength={100}\n strokeDasharray={`${Math.max(0, a.len - (parts ? 1 : 0))} ${100 - Math.max(0, a.len - (parts ? 1 : 0))}`}\n strokeDashoffset={-a.start}\n className={[a.cls, \"transition-[stroke-dasharray] duration-700 motion-reduce:transition-none\"].join(\" \")}\n />\n ))}\n </svg>\n {children !== undefined && <div className={s.center()}>{children}</div>}\n </div>\n {legend}\n </div>\n );\n }\n\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={s.root({ className })} {...props}>\n {showLabel && (\n <div aria-hidden className={s.header()}>\n <span className={s.caption()}>{label}</span>\n {!parts && <span className={s.figure()}>{words}</span>}\n </div>\n )}\n <div aria-hidden className={s.track()}>\n {parts ? (\n shares.map((p) => <span key={p.i} className={[s.fill(), PART_BG[p.i % PART_BG.length]].join(\" \")} style={{ width: `${p.ratio * 100}%` }} />)\n ) : (\n <span className={[s.fill(), BG[level]].join(\" \")} style={{ width: `${ratio * 100}%` }} />\n )}\n </div>\n {legend}\n </div>\n );\n}\n\nexport interface MeterValueProps {\n /** The figure. */\n value: React.ReactNode;\n /** A word under it. */\n caption?: React.ReactNode;\n}\n\n/** For the ring's centre when it shows a figure and a word. */\nexport function MeterValue({ value, caption }: MeterValueProps) {\n return (\n <>\n <span className=\"font-display text-lg font-semibold text-fg tabular-nums\">{value}</span>\n {caption && <span className=\"text-[10px] text-fg-secondary\">{caption}</span>}\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/meter/meter.tsx","../src/meter/model.ts"],"sourcesContent":["// Server-safe. A measure within a known range — a quota, a balance, a share — as opposed to the progress of a task.\n// `role=\"meter\"`, its value in words; thresholds turn it to warning then danger (or the reverse when high is good);\n// split into parts (90 % to the creator, 10 % to the platform) it becomes a stacked bar or ring with its legend: the\n// text version of what it draws.\nimport type * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { type MeterLevel, meterModel, type MeterPart, type MeterThresholds } from \"./model\";\n\nexport type { MeterPart };\n\nexport const meter = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n header: \"flex items-baseline gap-2 text-xs\",\n caption: \"min-w-0 flex-1 truncate font-medium text-fg\",\n figure: \"shrink-0 tabular-nums text-fg-secondary\",\n track: \"relative flex h-2 w-full gap-0.5 overflow-hidden rounded-sm bg-surface-3\",\n fill: \"h-full rounded-sm transition-[width] duration-700 motion-reduce:transition-none\",\n legend: \"mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs text-fg-secondary\",\n swatch: \"inline-block h-2 w-2 shrink-0 rounded-full\",\n ring: \"relative inline-flex shrink-0 items-center justify-center\",\n center: \"absolute inset-0 flex flex-col items-center justify-center text-center\",\n },\n variants: {\n size: { sm: { track: \"h-1.5\", ring: \"h-14 w-14\" }, md: { ring: \"h-24 w-24\" }, lg: { track: \"h-3\", ring: \"h-36 w-36\" } },\n },\n defaultVariants: { size: \"md\" },\n});\n\n/** The colour of a fill: by tone (thresholds) or by part (the accent, a neutral, accent-2, a lighter neutral). */\nconst BG: Record<MeterLevel, string> = { ok: \"bg-accent\", success: \"bg-success\", warning: \"bg-warning\", danger: \"bg-danger\" };\nconst STROKE: Record<MeterLevel, string> = { ok: \"stroke-accent\", success: \"stroke-success\", warning: \"stroke-warning\", danger: \"stroke-danger\" };\nconst PART_BG = [\"bg-accent\", \"bg-fg-secondary\", \"bg-accent-2\", \"bg-border-strong\"];\nconst PART_STROKE = [\"stroke-accent\", \"stroke-fg-secondary\", \"stroke-accent-2\", \"stroke-border-strong\"];\n\nexport type MeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n /** The accessible name (“Storage”, “Split of a tip”) — passed translated; shown with `showLabel`. */\n label: string;\n /** The value, between `min` and `max` (ignored with `parts`). */\n value?: number;\n /**\n * The bottom of the range.\n * @default 0\n */\n min?: number;\n /**\n * The top of the range.\n * @default 100\n */\n max?: number;\n /** The value in words (“42 GB of 50 GB”) — passed translated; default: the percentage. */\n valueText?: string;\n /**\n * Ratios (0–1) where the tone changes: past `high` it is in danger, past `low` in warning — or the reverse with\n * `optimum=\"high\"` (a balance: the lower, the worse). Without thresholds the fill is the brand accent.\n */\n thresholds?: MeterThresholds;\n /**\n * Which end is good.\n * @default \"low\"\n */\n optimum?: \"low\" | \"high\";\n /** Split into parts that add up to `max`: a stacked bar or ring, with its legend. */\n parts?: readonly MeterPart[];\n /**\n * bar · ring.\n * @default \"bar\"\n */\n variant?: \"bar\" | \"ring\";\n /**\n * sm · md · lg: the bar's thickness, the ring's diameter.\n * @default \"md\"\n */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Shows the label and the value above the bar. */\n showLabel?: boolean;\n /** A ring's centre: the value, a word. */\n children?: React.ReactNode;\n};\n\n/** A measure within a range — a quota, a balance, a share — toned by thresholds or split into parts. */\nexport function Meter({\n label,\n value = 0,\n min = 0,\n max = 100,\n valueText,\n thresholds,\n optimum = \"low\",\n parts,\n variant = \"bar\",\n size = \"md\",\n showLabel,\n className,\n children,\n ...props\n}: MeterProps) {\n const s = meter({ size });\n const { ratio, level, words, shares, arcs } = meterModel({ value, min, max, valueText, thresholds, optimum, parts });\n const a11y = parts\n ? { role: \"group\" as const, \"aria-label\": label }\n : { role: \"meter\" as const, \"aria-label\": label, \"aria-valuemin\": min, \"aria-valuemax\": max, \"aria-valuenow\": Math.min(max, Math.max(min, value)), \"aria-valuetext\": words };\n const legend = parts && (\n <ul className={s.legend()}>\n {shares.map((p) => (\n <li key={p.i} className=\"flex items-center gap-1.5\">\n <span aria-hidden className={[s.swatch(), PART_BG[p.i % PART_BG.length]].join(\" \")} />\n <span className=\"text-fg\">{p.label}</span>\n <span className=\"tabular-nums\">{p.words}</span>\n </li>\n ))}\n </ul>\n );\n\n if (variant === \"ring\") {\n const r = 44;\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={[\"inline-flex flex-col items-center gap-2\", className].filter(Boolean).join(\" \")} {...props}>\n <div className={s.ring()}>\n <svg viewBox=\"0 0 100 100\" className=\"h-full w-full -rotate-90\" aria-hidden>\n <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" strokeWidth=\"9\" className=\"stroke-border-default\" />\n {arcs.map((arc, i) => {\n const length = Math.max(0, arc.len * 100 - (parts ? 1 : 0));\n return (\n <circle\n key={i}\n cx=\"50\"\n cy=\"50\"\n r={r}\n fill=\"none\"\n strokeWidth=\"9\"\n pathLength={100}\n strokeDasharray={`${length} ${100 - length}`}\n strokeDashoffset={-arc.start * 100}\n className={[arc.part === undefined ? STROKE[level] : PART_STROKE[arc.part % PART_STROKE.length], \"transition-[stroke-dasharray] duration-700 motion-reduce:transition-none\"].join(\" \")}\n />\n );\n })}\n </svg>\n {children !== undefined && <div className={s.center()}>{children}</div>}\n </div>\n {legend}\n </div>\n );\n }\n\n return (\n <div {...a11y} data-level={parts ? undefined : level} className={s.root({ className })} {...props}>\n {showLabel && (\n <div aria-hidden className={s.header()}>\n <span className={s.caption()}>{label}</span>\n {!parts && <span className={s.figure()}>{words}</span>}\n </div>\n )}\n <div aria-hidden className={s.track()}>\n {parts ? (\n shares.map((p) => <span key={p.i} className={[s.fill(), PART_BG[p.i % PART_BG.length]].join(\" \")} style={{ width: `${p.ratio * 100}%` }} />)\n ) : (\n <span className={[s.fill(), BG[level]].join(\" \")} style={{ width: `${ratio * 100}%` }} />\n )}\n </div>\n {legend}\n </div>\n );\n}\n\nexport interface MeterValueProps {\n /** The figure. */\n value: React.ReactNode;\n /** A word under it. */\n caption?: React.ReactNode;\n}\n\n/** For the ring's centre when it shows a figure and a word. */\nexport function MeterValue({ value, caption }: MeterValueProps) {\n return (\n <>\n <span className=\"font-display text-lg font-semibold text-fg tabular-nums\">{value}</span>\n {caption && <span className=\"text-[10px] text-fg-secondary\">{caption}</span>}\n </>\n );\n}\n","// Headless: the arithmetic of a Meter, read by the web (`meter.tsx`) and the native (`native/meter.tsx`) alike.\n\nexport interface MeterPart {\n /** Its name (“To the creator”) — passed translated. */\n label: string;\n /** Its value, in the meter's unit. */\n value: number;\n /** Its value in words (“€9.00”), in the legend and for assistive technology; default: its percentage. */\n valueText?: string;\n}\n\n/** Which end of the range is good: `low` (a quota) or `high` (a score, a balance). */\nexport const MeterOptimum = { Low: \"low\", High: \"high\" } as const;\nexport type MeterOptimum = (typeof MeterOptimum)[keyof typeof MeterOptimum];\n\n/** The tone of a fill: the accent without thresholds, otherwise good, warning or bad. */\nexport const MeterLevel = { Ok: \"ok\", Success: \"success\", Warning: \"warning\", Danger: \"danger\" } as const;\nexport type MeterLevel = (typeof MeterLevel)[keyof typeof MeterLevel];\n\nexport interface MeterThresholds {\n readonly low: number;\n readonly high: number;\n}\n\nexport interface MeterInput {\n value?: number;\n min?: number;\n max?: number;\n valueText?: string;\n thresholds?: MeterThresholds;\n optimum?: MeterOptimum;\n parts?: readonly MeterPart[];\n}\n\nexport interface MeterShare extends MeterPart {\n i: number;\n /** Its share of the range, 0–1. */\n ratio: number;\n /** Its value in words. */\n words: string;\n}\n\n/** One arc of a ring, as fractions of the circle: the part it draws (none for the whole value), its length, where it starts. */\nexport interface MeterArc {\n part: number | undefined;\n len: number;\n start: number;\n}\n\nexport interface MeterModel {\n ratio: number;\n level: MeterLevel;\n words: string;\n shares: MeterShare[];\n arcs: MeterArc[];\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\nconst percent = (ratio: number) => `${Math.round(ratio * 100)} %`;\n\nfunction levelOf(ratio: number, thresholds: MeterThresholds | undefined, optimum: MeterOptimum): MeterLevel {\n if (!thresholds) return MeterLevel.Ok;\n const good = optimum === MeterOptimum.Low ? MeterLevel.Success : MeterLevel.Danger;\n const bad = optimum === MeterOptimum.Low ? MeterLevel.Danger : MeterLevel.Success;\n if (ratio >= thresholds.high) return bad;\n return ratio >= thresholds.low ? MeterLevel.Warning : good;\n}\n\n/** The ratio, tone, words, parts and ring arcs of a meter. */\nexport function meterModel({ value = 0, min = 0, max = 100, valueText, thresholds, optimum = MeterOptimum.Low, parts }: MeterInput): MeterModel {\n const span = max - min || 1;\n const ratio = clamp01((value - min) / span);\n const shares = (parts ?? []).map((p, i) => ({ ...p, i, ratio: clamp01(p.value / span), words: p.valueText ?? percent(p.value / span) }));\n let start = 0;\n const arcs = parts\n ? shares.map((p) => {\n const arc = { part: p.i, len: p.ratio, start };\n start += p.ratio;\n return arc;\n })\n : [{ part: undefined, len: ratio, start: 0 }];\n return { ratio, level: levelOf(ratio, thresholds, optimum), words: valueText ?? percent(ratio), shares, arcs };\n}\n"],"mappings":"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","names":[]}
package/dist/motion.css CHANGED
@@ -755,3 +755,16 @@ svg[data-kz-icon="ExternalIcon"] {
755
755
  @media (prefers-reduced-motion: reduce) {
756
756
  .kz-overlay, .kz-dialog, .kz-popover, .kz-tabs-panel { animation: none; transition: none; }
757
757
  }
758
+
759
+ /* ── PAGE TRANSITIONS ───────────────────────────────────────────────────────────────────────────────────────────────
760
+ <PageTransition> (@krizaka/ui/motion) gives the page content the view-transition-class .kz-page: the old page leaves
761
+ quickly, the new one cross-fades in with a short rise. Tokens only. Reduced motion: the page changes at once, and every
762
+ view transition of the document (shared elements included) loses its animation. */
763
+ ::view-transition-old(.kz-page) { animation: kz-page-out var(--kz-motion-duration-fast) var(--kz-motion-ease-in) both; }
764
+ ::view-transition-new(.kz-page) { animation: kz-page-in var(--kz-motion-duration-base) var(--kz-motion-ease-out) both; }
765
+ @keyframes kz-page-out { from { opacity: 1; } to { opacity: 0; } }
766
+ @keyframes kz-page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
767
+
768
+ @media (prefers-reduced-motion: reduce) {
769
+ ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
770
+ }
package/dist/motion.d.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  export { M as MotionObserver, a as MotionObserverProps, R as RotatingWord, b as RotatingWordProps } from './RotatingWord-D9s7FXFD.js';
2
- import 'react';
2
+ import * as React from 'react';
3
+ import { ReactNode } from 'react';
4
+
5
+ /** The `view-transition-class` the page change carries (styled in motion.css, « PAGE TRANSITIONS »). */
6
+ declare const PAGE_TRANSITION_CLASS = "kz-page";
7
+ interface PageTransitionProps {
8
+ children: ReactNode;
9
+ }
10
+ /**
11
+ * Wrap the page content (the root layout's `children`) once: a navigation then cross-fades and rises over the
12
+ * `--kz-motion-*` tokens, through the platform's View Transitions. Where the runtime has no `ViewTransition` it renders
13
+ * its children untouched; under `prefers-reduced-motion` motion.css makes the change instant. The DOM always updates.
14
+ */
15
+ declare function PageTransition({ children }: PageTransitionProps): React.JSX.Element;
3
16
 
4
17
  /**
5
18
  * Page transitions on the platform: `document.startViewTransition` with a cross-fade fallback and an instant change
@@ -50,4 +63,4 @@ declare function sharedElementStyle(name: string): {
50
63
  viewTransitionName: string;
51
64
  };
52
65
 
53
- export { type CrossFadeOptions, type ViewTransitionEnv, type ViewTransitionLike, canViewTransition, crossFallback, runViewTransition, sharedElementStyle, transitionPage };
66
+ export { type CrossFadeOptions, PAGE_TRANSITION_CLASS, PageTransition, type PageTransitionProps, type ViewTransitionEnv, type ViewTransitionLike, canViewTransition, crossFallback, runViewTransition, sharedElementStyle, transitionPage };
package/dist/motion.js CHANGED
@@ -4,6 +4,21 @@ import {
4
4
  RotatingWord
5
5
  } from "./chunk-4JO5GSSD.js";
6
6
 
7
+ // src/motion/PageTransition.tsx
8
+ import * as React from "react";
9
+ import { Fragment, jsx } from "react/jsx-runtime";
10
+ var PAGE_TRANSITION_CLASS = "kz-page";
11
+ var RUNTIME_KEYS = ["ViewTransition", "unstable_ViewTransition"];
12
+ function runtimeViewTransition() {
13
+ const runtime = React;
14
+ return RUNTIME_KEYS.map((key) => runtime[key]).find(Boolean);
15
+ }
16
+ var ViewTransition = runtimeViewTransition();
17
+ function PageTransition({ children }) {
18
+ if (!ViewTransition) return /* @__PURE__ */ jsx(Fragment, { children });
19
+ return /* @__PURE__ */ jsx(ViewTransition, { default: PAGE_TRANSITION_CLASS, children });
20
+ }
21
+
7
22
  // src/motion/view-transition.ts
8
23
  var IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
9
24
  function canViewTransition(doc) {
@@ -41,6 +56,8 @@ function sharedElementStyle(name) {
41
56
  }
42
57
  export {
43
58
  MotionObserver,
59
+ PAGE_TRANSITION_CLASS,
60
+ PageTransition,
44
61
  RotatingWord,
45
62
  canViewTransition,
46
63
  crossFallback,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/motion/view-transition.ts"],"sourcesContent":["/**\n * Page transitions on the platform: `document.startViewTransition` with a cross-fade fallback and an instant change\n * under reduced motion. Pure functions over an injected document, so they are testable without a browser.\n *\n * The contract (docs/direction-2027.md): the DOM always updates; motion is only the way it gets there.\n */\n\nexport interface ViewTransitionLike {\n readonly finished: Promise<void>;\n}\n\nexport type StartViewTransition = (update: () => void) => ViewTransitionLike;\n\nexport interface ViewTransitionEnv {\n /** The document whose transition runs. Defaults to the global one when the call happens in a browser. */\n readonly doc?: {\n startViewTransition?: StartViewTransition;\n documentElement?: { animate?: (frames: Keyframe[], options: KeyframeAnimationOptions) => unknown };\n };\n /** `prefers-reduced-motion: reduce`. */\n readonly reduced?: boolean;\n}\n\nexport interface CrossFadeOptions {\n /** Milliseconds of the fallback cross-fade. */\n readonly duration?: number;\n readonly easing?: string;\n}\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/** True when the browser can run a native view transition. */\nexport function canViewTransition(doc: ViewTransitionEnv[\"doc\"]): boolean {\n return typeof doc?.startViewTransition === \"function\";\n}\n\n/**\n * Runs `update` inside a view transition when the platform has one and motion is allowed; otherwise it runs `update`\n * at once. Resolves when the transition has finished (or straight away when there is none). It never rejects: a\n * skipped or aborted transition still leaves the DOM in its new state.\n */\nexport function runViewTransition(update: () => void, env: ViewTransitionEnv = {}): Promise<void> {\n if (env.reduced || !canViewTransition(env.doc)) {\n update();\n return Promise.resolve();\n }\n const transition = env.doc!.startViewTransition!(update);\n return transition.finished.then(\n () => undefined,\n () => undefined,\n );\n}\n\n/**\n * The fallback for a browser without view transitions: the change is applied, then the root cross-fades from\n * transparent. Reduced motion means no fade at all. Returns the animation when one was started, otherwise `null`.\n */\nexport function crossFallback(update: () => void, env: ViewTransitionEnv = {}, options: CrossFadeOptions = {}): unknown {\n update();\n if (env.reduced || canViewTransition(env.doc)) return null;\n const root = env.doc?.documentElement;\n if (typeof root?.animate !== \"function\") return null;\n return root.animate([{ opacity: 0 }, { opacity: 1 }], {\n duration: options.duration ?? 200,\n easing: options.easing ?? \"ease-out\",\n });\n}\n\n/** One call for a change of page or step: a view transition where it exists, a cross-fade otherwise, instant under reduced motion. */\nexport function transitionPage(update: () => void, env: ViewTransitionEnv = {}, options: CrossFadeOptions = {}): Promise<void> {\n if (canViewTransition(env.doc) && !env.reduced) return runViewTransition(update, env);\n crossFallback(update, env, options);\n return Promise.resolve();\n}\n\n/**\n * The `view-transition-name` for a shared element: the element morphs between the two states wherever it lands. The\n * name must be one CSS identifier and unique on the page while the transition runs.\n */\nexport function sharedElementStyle(name: string): { viewTransitionName: string } {\n if (!IDENT.test(name)) throw new RangeError(`view-transition-name must be one identifier, got \"${name}\"`);\n return { viewTransitionName: name };\n}\n"],"mappings":";;;;;;;AA6BA,IAAM,QAAQ;AAGP,SAAS,kBAAkB,KAAwC;AACxE,SAAO,OAAO,KAAK,wBAAwB;AAC7C;AAOO,SAAS,kBAAkB,QAAoB,MAAyB,CAAC,GAAkB;AAChG,MAAI,IAAI,WAAW,CAAC,kBAAkB,IAAI,GAAG,GAAG;AAC9C,WAAO;AACP,WAAO,QAAQ,QAAQ;AAAA,EACzB;AACA,QAAM,aAAa,IAAI,IAAK,oBAAqB,MAAM;AACvD,SAAO,WAAW,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACF;AAMO,SAAS,cAAc,QAAoB,MAAyB,CAAC,GAAG,UAA4B,CAAC,GAAY;AACtH,SAAO;AACP,MAAI,IAAI,WAAW,kBAAkB,IAAI,GAAG,EAAG,QAAO;AACtD,QAAM,OAAO,IAAI,KAAK;AACtB,MAAI,OAAO,MAAM,YAAY,WAAY,QAAO;AAChD,SAAO,KAAK,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,GAAG;AAAA,IACpD,UAAU,QAAQ,YAAY;AAAA,IAC9B,QAAQ,QAAQ,UAAU;AAAA,EAC5B,CAAC;AACH;AAGO,SAAS,eAAe,QAAoB,MAAyB,CAAC,GAAG,UAA4B,CAAC,GAAkB;AAC7H,MAAI,kBAAkB,IAAI,GAAG,KAAK,CAAC,IAAI,QAAS,QAAO,kBAAkB,QAAQ,GAAG;AACpF,gBAAc,QAAQ,KAAK,OAAO;AAClC,SAAO,QAAQ,QAAQ;AACzB;AAMO,SAAS,mBAAmB,MAA8C;AAC/E,MAAI,CAAC,MAAM,KAAK,IAAI,EAAG,OAAM,IAAI,WAAW,qDAAqD,IAAI,GAAG;AACxG,SAAO,EAAE,oBAAoB,KAAK;AACpC;","names":[]}
1
+ {"version":3,"sources":["../src/motion/PageTransition.tsx","../src/motion/view-transition.ts"],"sourcesContent":["import type { ComponentType, ReactNode } from \"react\";\nimport * as React from \"react\";\n\n/** The `view-transition-class` the page change carries (styled in motion.css, « PAGE TRANSITIONS »). */\nexport const PAGE_TRANSITION_CLASS = \"kz-page\";\n\ntype ViewTransitionComponent = ComponentType<{ children?: ReactNode; default?: string }>;\n\n/** Next.js ships React canary, where it is `ViewTransition`; earlier canaries called it `unstable_ViewTransition`. */\nconst RUNTIME_KEYS = [\"ViewTransition\", \"unstable_ViewTransition\"] as const;\n\n// Read through a variable key: a plain React build has neither export, and a static `React.ViewTransition` would make\n// the bundler warn about a missing export.\nfunction runtimeViewTransition(): ViewTransitionComponent | undefined {\n const runtime = React as unknown as Record<string, ViewTransitionComponent | undefined>;\n return RUNTIME_KEYS.map((key) => runtime[key]).find(Boolean);\n}\n\nconst ViewTransition = runtimeViewTransition();\n\nexport interface PageTransitionProps {\n children: ReactNode;\n}\n\n/**\n * Wrap the page content (the root layout's `children`) once: a navigation then cross-fades and rises over the\n * `--kz-motion-*` tokens, through the platform's View Transitions. Where the runtime has no `ViewTransition` it renders\n * its children untouched; under `prefers-reduced-motion` motion.css makes the change instant. The DOM always updates.\n */\nexport function PageTransition({ children }: PageTransitionProps) {\n if (!ViewTransition) return <>{children}</>;\n return <ViewTransition default={PAGE_TRANSITION_CLASS}>{children}</ViewTransition>;\n}\n","/**\n * Page transitions on the platform: `document.startViewTransition` with a cross-fade fallback and an instant change\n * under reduced motion. Pure functions over an injected document, so they are testable without a browser.\n *\n * The contract (docs/direction-2027.md): the DOM always updates; motion is only the way it gets there.\n */\n\nexport interface ViewTransitionLike {\n readonly finished: Promise<void>;\n}\n\nexport type StartViewTransition = (update: () => void) => ViewTransitionLike;\n\nexport interface ViewTransitionEnv {\n /** The document whose transition runs. Defaults to the global one when the call happens in a browser. */\n readonly doc?: {\n startViewTransition?: StartViewTransition;\n documentElement?: { animate?: (frames: Keyframe[], options: KeyframeAnimationOptions) => unknown };\n };\n /** `prefers-reduced-motion: reduce`. */\n readonly reduced?: boolean;\n}\n\nexport interface CrossFadeOptions {\n /** Milliseconds of the fallback cross-fade. */\n readonly duration?: number;\n readonly easing?: string;\n}\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\n/** True when the browser can run a native view transition. */\nexport function canViewTransition(doc: ViewTransitionEnv[\"doc\"]): boolean {\n return typeof doc?.startViewTransition === \"function\";\n}\n\n/**\n * Runs `update` inside a view transition when the platform has one and motion is allowed; otherwise it runs `update`\n * at once. Resolves when the transition has finished (or straight away when there is none). It never rejects: a\n * skipped or aborted transition still leaves the DOM in its new state.\n */\nexport function runViewTransition(update: () => void, env: ViewTransitionEnv = {}): Promise<void> {\n if (env.reduced || !canViewTransition(env.doc)) {\n update();\n return Promise.resolve();\n }\n const transition = env.doc!.startViewTransition!(update);\n return transition.finished.then(\n () => undefined,\n () => undefined,\n );\n}\n\n/**\n * The fallback for a browser without view transitions: the change is applied, then the root cross-fades from\n * transparent. Reduced motion means no fade at all. Returns the animation when one was started, otherwise `null`.\n */\nexport function crossFallback(update: () => void, env: ViewTransitionEnv = {}, options: CrossFadeOptions = {}): unknown {\n update();\n if (env.reduced || canViewTransition(env.doc)) return null;\n const root = env.doc?.documentElement;\n if (typeof root?.animate !== \"function\") return null;\n return root.animate([{ opacity: 0 }, { opacity: 1 }], {\n duration: options.duration ?? 200,\n easing: options.easing ?? \"ease-out\",\n });\n}\n\n/** One call for a change of page or step: a view transition where it exists, a cross-fade otherwise, instant under reduced motion. */\nexport function transitionPage(update: () => void, env: ViewTransitionEnv = {}, options: CrossFadeOptions = {}): Promise<void> {\n if (canViewTransition(env.doc) && !env.reduced) return runViewTransition(update, env);\n crossFallback(update, env, options);\n return Promise.resolve();\n}\n\n/**\n * The `view-transition-name` for a shared element: the element morphs between the two states wherever it lands. The\n * name must be one CSS identifier and unique on the page while the transition runs.\n */\nexport function sharedElementStyle(name: string): { viewTransitionName: string } {\n if (!IDENT.test(name)) throw new RangeError(`view-transition-name must be one identifier, got \"${name}\"`);\n return { viewTransitionName: name };\n}\n"],"mappings":";;;;;;;AACA,YAAY,WAAW;AA6BO;AA1BvB,IAAM,wBAAwB;AAKrC,IAAM,eAAe,CAAC,kBAAkB,yBAAyB;AAIjE,SAAS,wBAA6D;AACpE,QAAM,UAAU;AAChB,SAAO,aAAa,IAAI,CAAC,QAAQ,QAAQ,GAAG,CAAC,EAAE,KAAK,OAAO;AAC7D;AAEA,IAAM,iBAAiB,sBAAsB;AAWtC,SAAS,eAAe,EAAE,SAAS,GAAwB;AAChE,MAAI,CAAC,eAAgB,QAAO,gCAAG,UAAS;AACxC,SAAO,oBAAC,kBAAe,SAAS,uBAAwB,UAAS;AACnE;;;ACHA,IAAM,QAAQ;AAGP,SAAS,kBAAkB,KAAwC;AACxE,SAAO,OAAO,KAAK,wBAAwB;AAC7C;AAOO,SAAS,kBAAkB,QAAoB,MAAyB,CAAC,GAAkB;AAChG,MAAI,IAAI,WAAW,CAAC,kBAAkB,IAAI,GAAG,GAAG;AAC9C,WAAO;AACP,WAAO,QAAQ,QAAQ;AAAA,EACzB;AACA,QAAM,aAAa,IAAI,IAAK,oBAAqB,MAAM;AACvD,SAAO,WAAW,SAAS;AAAA,IACzB,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACF;AAMO,SAAS,cAAc,QAAoB,MAAyB,CAAC,GAAG,UAA4B,CAAC,GAAY;AACtH,SAAO;AACP,MAAI,IAAI,WAAW,kBAAkB,IAAI,GAAG,EAAG,QAAO;AACtD,QAAM,OAAO,IAAI,KAAK;AACtB,MAAI,OAAO,MAAM,YAAY,WAAY,QAAO;AAChD,SAAO,KAAK,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,GAAG;AAAA,IACpD,UAAU,QAAQ,YAAY;AAAA,IAC9B,QAAQ,QAAQ,UAAU;AAAA,EAC5B,CAAC;AACH;AAGO,SAAS,eAAe,QAAoB,MAAyB,CAAC,GAAG,UAA4B,CAAC,GAAkB;AAC7H,MAAI,kBAAkB,IAAI,GAAG,KAAK,CAAC,IAAI,QAAS,QAAO,kBAAkB,QAAQ,GAAG;AACpF,gBAAc,QAAQ,KAAK,OAAO;AAClC,SAAO,QAAQ,QAAQ;AACzB;AAMO,SAAS,mBAAmB,MAA8C;AAC/E,MAAI,CAAC,MAAM,KAAK,IAAI,EAAG,OAAM,IAAI,WAAW,qDAAqD,IAAI,GAAG;AACxG,SAAO,EAAE,oBAAoB,KAAK;AACpC;","names":[]}
@@ -175,6 +175,34 @@ interface KrizakaFieldArray<T = unknown> {
175
175
  declare function useKrizakaFieldArray<T = unknown>(name: string): KrizakaFieldArray<T>;
176
176
  /** The value of a field, kept up to date (`form.Watch`): show a part of the form only when a box is ticked. */
177
177
  declare function useFormValue<T = unknown>(name: string): T;
178
+ type FormFieldProps<T = unknown> = {
179
+ /** The field's name in the schema (typed by the form). */
180
+ name: string;
181
+ /** Checks the value on the server as it changes. */
182
+ validateAsync?: FieldCheck<T>;
183
+ /** The wait, in ms, before `validateAsync`. */
184
+ asyncDebounceMs?: number;
185
+ /** Draws the field: its value, setter, error (and the ids, on the web) (`useKrizakaField`). */
186
+ children: (field: KrizakaField<T>) => React.ReactNode;
187
+ };
188
+ /** A field drawn by the app (a control the package does not have): the form's value and errors, any markup. */
189
+ declare function FormField({ name, validateAsync, asyncDebounceMs, children }: FormFieldProps): React.JSX.Element;
190
+ type FieldArrayProps<T = unknown> = {
191
+ /** The array's name in the schema (typed by the form). */
192
+ name: string;
193
+ /** Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, "url")` for the fields of an item. */
194
+ children: (array: KrizakaFieldArray<T>) => React.ReactNode;
195
+ };
196
+ /** A list of fields that grows and shrinks (links, tiers, guests): each item's fields named `links[0].url`. */
197
+ declare function FieldArray({ name, children }: FieldArrayProps): React.JSX.Element;
198
+ type WatchProps<T = unknown> = {
199
+ /** The field to follow (typed by the form). */
200
+ name: string;
201
+ /** Draws from its current value: show a part of the form only when a choice is made. */
202
+ children: (value: T) => React.ReactNode;
203
+ };
204
+ /** Renders from a field's current value (a part shown only when a choice is made, a total). */
205
+ declare function Watch({ name, children }: WatchProps): React.JSX.Element;
178
206
  /** The state of the submit, for a button: sending, and whether the last try succeeded. */
179
207
  declare function useFormStatus(): {
180
208
  submitting: boolean;
@@ -371,34 +399,6 @@ type MoneyProps = Base & {
371
399
  };
372
400
  /** An amount of the form, stored as integer minor units (cents), typed on the decimal keypad the locale's way. */
373
401
  declare function Money({ name, label, hint, disabled, style, currency, placeholder }: MoneyProps): React.JSX.Element;
374
- type FormFieldProps<T = unknown> = {
375
- /** The field's name in the schema (typed by the form). */
376
- name: string;
377
- /** Checks the value on the server as it changes. */
378
- validateAsync?: FieldCheck<T>;
379
- /** The wait, in ms, before `validateAsync`. */
380
- asyncDebounceMs?: number;
381
- /** Draws the field: its value, setter, error (`useKrizakaField`). */
382
- children: (field: KrizakaField<T>) => React.ReactNode;
383
- };
384
- /** A field drawn by the app (a control the package does not have). */
385
- declare function FormField({ name, validateAsync, asyncDebounceMs, children }: FormFieldProps): React.JSX.Element;
386
- type FieldArrayProps<T = unknown> = {
387
- /** The array's name in the schema (typed by the form). */
388
- name: string;
389
- /** Draws the items: `items`, `push`, `remove`, `move`, `fieldName(i, "url")`. */
390
- children: (array: KrizakaFieldArray<T>) => React.ReactNode;
391
- };
392
- /** A list of fields that grows and shrinks: each item's fields named `links[0].url`. */
393
- declare function FieldArray({ name, children }: FieldArrayProps): React.JSX.Element;
394
- type WatchProps<T = unknown> = {
395
- /** The field to follow (typed by the form). */
396
- name: string;
397
- /** Draws from its current value. */
398
- children: (value: T) => React.ReactNode;
399
- };
400
- /** Renders from a field's current value (a part shown only when a choice is made). */
401
- declare function Watch({ name, children }: WatchProps): React.JSX.Element;
402
402
  /** A form, defined once — the same schema and defaults as the web (`packages/core` of a product) — on native fields. */
403
403
  declare const createKrizakaForm: <S extends FormSchema>(definition: KrizakaFormDefinition<S>) => KrizakaFormFactory<S, {
404
404
  TextField: typeof TextField;
@@ -141,7 +141,7 @@ function problemToErrors(problem, i18n = {}, known, codeFields = {}) {
141
141
  }
142
142
 
143
143
  // src/form/core.tsx
144
- import { jsx } from "react/jsx-runtime";
144
+ import { Fragment, jsx } from "react/jsx-runtime";
145
145
  var FormContext = React.createContext(null);
146
146
  function useFormContext() {
147
147
  const ctx = React.useContext(FormContext);
@@ -286,6 +286,15 @@ function useFormValue(name) {
286
286
  const ctx = useFormContext();
287
287
  return useStore(ctx.api.store, (state) => getPath(state.values, name));
288
288
  }
289
+ function FormField({ name, validateAsync, asyncDebounceMs, children }) {
290
+ return /* @__PURE__ */ jsx(Fragment, { children: children(useKrizakaField(name, { validateAsync, asyncDebounceMs })) });
291
+ }
292
+ function FieldArray({ name, children }) {
293
+ return /* @__PURE__ */ jsx(Fragment, { children: children(useKrizakaFieldArray(name)) });
294
+ }
295
+ function Watch({ name, children }) {
296
+ return /* @__PURE__ */ jsx(Fragment, { children: children(useFormValue(name)) });
297
+ }
289
298
  function useFormStatus() {
290
299
  const ctx = useFormContext();
291
300
  const submitting = useStore(ctx.api.store, (s) => s.isSubmitting);
@@ -411,7 +420,7 @@ function latestBirthDate(age, today = /* @__PURE__ */ new Date()) {
411
420
  }
412
421
 
413
422
  // src/native/form.tsx
414
- import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
423
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
415
424
  var ChainContext = React2.createContext(null);
416
425
  function useChain(ctx) {
417
426
  const entries = React2.useRef([]);
@@ -662,15 +671,6 @@ function Money({ name, label, hint, disabled, style, currency, placeholder }) {
662
671
  }
663
672
  );
664
673
  }
665
- function FormField({ name, validateAsync, asyncDebounceMs, children }) {
666
- return /* @__PURE__ */ jsx2(Fragment, { children: children(useKrizakaField(name, { validateAsync, asyncDebounceMs })) });
667
- }
668
- function FieldArray({ name, children }) {
669
- return /* @__PURE__ */ jsx2(Fragment, { children: children(useKrizakaFieldArray(name)) });
670
- }
671
- function Watch({ name, children }) {
672
- return /* @__PURE__ */ jsx2(Fragment, { children: children(useFormValue(name)) });
673
- }
674
674
  var fields = { TextField, TextArea, Checkbox, Switch, RadioGroup, Select, Combobox, DateField, Money, Slider, Field: FormField, FieldArray, Watch, Submit, FormError };
675
675
  var createKrizakaForm = makeCreateKrizakaForm(fields, FormRoot);
676
676
  var styles = StyleSheet.create({