@dowel-ui/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +187 -0
  3. package/dist/components/ai-action-ledger/ai-action-ledger.d.ts +77 -0
  4. package/dist/components/ai-action-ledger/ai-action-ledger.d.ts.map +1 -0
  5. package/dist/components/ai-action-ledger/ai-action-ledger.js +224 -0
  6. package/dist/components/ai-action-ledger/ai-action-ledger.js.map +1 -0
  7. package/dist/components/ai-action-ledger/index.d.ts +2 -0
  8. package/dist/components/ai-action-ledger/index.js +2 -0
  9. package/dist/components/ai-action-ledger/meta.js +17 -0
  10. package/dist/components/ai-action-ledger/meta.js.map +1 -0
  11. package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
  12. package/dist/components/ai-inline-completion/ai-inline-completion.d.ts +45 -0
  13. package/dist/components/ai-inline-completion/ai-inline-completion.d.ts.map +1 -0
  14. package/dist/components/ai-inline-completion/ai-inline-completion.js +110 -0
  15. package/dist/components/ai-inline-completion/ai-inline-completion.js.map +1 -0
  16. package/dist/components/ai-inline-completion/index.d.ts +2 -0
  17. package/dist/components/ai-inline-completion/index.js +2 -0
  18. package/dist/components/ai-inline-completion/meta.js +17 -0
  19. package/dist/components/ai-inline-completion/meta.js.map +1 -0
  20. package/dist/components/ai-structured-output/ai-structured-output.d.ts +75 -0
  21. package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -0
  22. package/dist/components/ai-structured-output/ai-structured-output.js +126 -0
  23. package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -0
  24. package/dist/components/ai-structured-output/index.d.ts +2 -0
  25. package/dist/components/ai-structured-output/index.js +2 -0
  26. package/dist/components/ai-structured-output/meta.js +17 -0
  27. package/dist/components/ai-structured-output/meta.js.map +1 -0
  28. package/dist/components/ai-tool/ai-tool.d.ts +1 -1
  29. package/dist/components/alert/alert.d.ts +1 -1
  30. package/dist/components/avatar/avatar.d.ts +1 -1
  31. package/dist/components/badge/badge.d.ts +1 -1
  32. package/dist/components/button/button.d.ts +1 -1
  33. package/dist/components/meter/index.d.ts +2 -0
  34. package/dist/components/meter/index.js +2 -0
  35. package/dist/components/meter/meta.js +17 -0
  36. package/dist/components/meter/meta.js.map +1 -0
  37. package/dist/components/meter/meter.d.ts +69 -0
  38. package/dist/components/meter/meter.d.ts.map +1 -0
  39. package/dist/components/meter/meter.js +144 -0
  40. package/dist/components/meter/meter.js.map +1 -0
  41. package/dist/components/metric-delta/index.d.ts +2 -0
  42. package/dist/components/metric-delta/index.js +2 -0
  43. package/dist/components/metric-delta/meta.js +17 -0
  44. package/dist/components/metric-delta/meta.js.map +1 -0
  45. package/dist/components/metric-delta/metric-delta.d.ts +59 -0
  46. package/dist/components/metric-delta/metric-delta.d.ts.map +1 -0
  47. package/dist/components/metric-delta/metric-delta.js +82 -0
  48. package/dist/components/metric-delta/metric-delta.js.map +1 -0
  49. package/dist/components/progress/progress.d.ts +1 -1
  50. package/dist/components/record-diff/index.d.ts +2 -0
  51. package/dist/components/record-diff/index.js +2 -0
  52. package/dist/components/record-diff/meta.js +17 -0
  53. package/dist/components/record-diff/meta.js.map +1 -0
  54. package/dist/components/record-diff/record-diff.d.ts +56 -0
  55. package/dist/components/record-diff/record-diff.d.ts.map +1 -0
  56. package/dist/components/record-diff/record-diff.js +199 -0
  57. package/dist/components/record-diff/record-diff.js.map +1 -0
  58. package/dist/components/spinner/spinner.d.ts +1 -1
  59. package/dist/components/toast/toast.d.ts +1 -1
  60. package/dist/index.d.ts +13 -1
  61. package/dist/index.js +7 -1
  62. package/dist/registry/components.d.ts.map +1 -1
  63. package/dist/registry/components.js +57 -45
  64. package/dist/registry/components.js.map +1 -1
  65. package/package.json +40 -5
  66. package/src/components/ai-action-ledger/ai-action-ledger.tsx +381 -0
  67. package/src/components/ai-action-ledger/index.ts +14 -0
  68. package/src/components/ai-action-ledger/meta.ts +19 -0
  69. package/src/components/ai-inline-completion/ai-inline-completion.tsx +214 -0
  70. package/src/components/ai-inline-completion/index.ts +1 -0
  71. package/src/components/ai-inline-completion/meta.ts +20 -0
  72. package/src/components/ai-structured-output/ai-structured-output.tsx +244 -0
  73. package/src/components/ai-structured-output/index.ts +10 -0
  74. package/src/components/ai-structured-output/meta.ts +18 -0
  75. package/src/components/meter/index.ts +8 -0
  76. package/src/components/meter/meta.ts +20 -0
  77. package/src/components/meter/meter.tsx +219 -0
  78. package/src/components/metric-delta/index.ts +6 -0
  79. package/src/components/metric-delta/meta.ts +18 -0
  80. package/src/components/metric-delta/metric-delta.tsx +180 -0
  81. package/src/components/record-diff/index.ts +10 -0
  82. package/src/components/record-diff/meta.ts +19 -0
  83. package/src/components/record-diff/record-diff.tsx +341 -0
  84. package/src/index.ts +6 -0
  85. package/src/registry/components.ts +12 -0
  86. package/src/blocks/admin-users/admin-users.stories.tsx +0 -83
  87. package/src/blocks/admin-users/admin-users.test.tsx +0 -159
  88. package/src/blocks/ai-chat/ai-chat.stories.tsx +0 -149
  89. package/src/blocks/ai-chat/ai-chat.test.tsx +0 -166
  90. package/src/blocks/dashboard/dashboard.stories.tsx +0 -106
  91. package/src/blocks/dashboard/dashboard.test.tsx +0 -94
  92. package/src/blocks/forgot-password/forgot-password.stories.tsx +0 -41
  93. package/src/blocks/forgot-password/forgot-password.test.tsx +0 -62
  94. package/src/blocks/login/login.stories.tsx +0 -46
  95. package/src/blocks/login/login.test.tsx +0 -100
  96. package/src/blocks/pricing/pricing.stories.tsx +0 -62
  97. package/src/blocks/pricing/pricing.test.tsx +0 -101
  98. package/src/blocks/settings/settings.stories.tsx +0 -94
  99. package/src/blocks/settings/settings.test.tsx +0 -167
  100. package/src/blocks/signup/signup.stories.tsx +0 -27
  101. package/src/blocks/signup/signup.test.tsx +0 -97
  102. package/src/components/accordion/accordion.stories.tsx +0 -90
  103. package/src/components/accordion/accordion.test.tsx +0 -156
  104. package/src/components/activity-feed/activity-feed.stories.tsx +0 -163
  105. package/src/components/activity-feed/activity-feed.test.tsx +0 -125
  106. package/src/components/ai-agent-status/ai-agent-status.stories.tsx +0 -86
  107. package/src/components/ai-agent-status/ai-agent-status.test.tsx +0 -66
  108. package/src/components/ai-conversation/ai-conversation.stories.tsx +0 -126
  109. package/src/components/ai-conversation/ai-conversation.test.tsx +0 -144
  110. package/src/components/ai-message/ai-message.stories.tsx +0 -106
  111. package/src/components/ai-message/ai-message.test.tsx +0 -160
  112. package/src/components/ai-model-selector/ai-model-selector.stories.tsx +0 -66
  113. package/src/components/ai-model-selector/ai-model-selector.test.tsx +0 -120
  114. package/src/components/ai-prompt-input/ai-prompt-input.stories.tsx +0 -145
  115. package/src/components/ai-prompt-input/ai-prompt-input.test.tsx +0 -273
  116. package/src/components/ai-reasoning/ai-reasoning.stories.tsx +0 -60
  117. package/src/components/ai-reasoning/ai-reasoning.test.tsx +0 -95
  118. package/src/components/ai-response/ai-response.stories.tsx +0 -75
  119. package/src/components/ai-response/ai-response.test.tsx +0 -96
  120. package/src/components/ai-sources/ai-sources.stories.tsx +0 -93
  121. package/src/components/ai-sources/ai-sources.test.tsx +0 -102
  122. package/src/components/ai-token-usage/ai-token-usage.stories.tsx +0 -59
  123. package/src/components/ai-token-usage/ai-token-usage.test.tsx +0 -83
  124. package/src/components/ai-tool/ai-tool.stories.tsx +0 -96
  125. package/src/components/ai-tool/ai-tool.test.tsx +0 -137
  126. package/src/components/alert/alert.stories.tsx +0 -101
  127. package/src/components/alert/alert.test.tsx +0 -94
  128. package/src/components/avatar/avatar.stories.tsx +0 -58
  129. package/src/components/avatar/avatar.test.tsx +0 -64
  130. package/src/components/badge/badge.stories.tsx +0 -68
  131. package/src/components/badge/badge.test.tsx +0 -61
  132. package/src/components/button/button.stories.tsx +0 -128
  133. package/src/components/button/button.test.tsx +0 -224
  134. package/src/components/calendar/calendar.stories.tsx +0 -105
  135. package/src/components/calendar/calendar.test.tsx +0 -255
  136. package/src/components/card/card.stories.tsx +0 -61
  137. package/src/components/card/card.test.tsx +0 -66
  138. package/src/components/checkbox/checkbox.stories.tsx +0 -124
  139. package/src/components/checkbox/checkbox.test.tsx +0 -121
  140. package/src/components/code-block/code-block.stories.tsx +0 -62
  141. package/src/components/code-block/code-block.test.tsx +0 -139
  142. package/src/components/combobox/combobox.stories.tsx +0 -171
  143. package/src/components/combobox/combobox.test.tsx +0 -429
  144. package/src/components/command/command.stories.tsx +0 -152
  145. package/src/components/command/command.test.tsx +0 -380
  146. package/src/components/data-table/data-table.stories.tsx +0 -151
  147. package/src/components/data-table/data-table.test.tsx +0 -288
  148. package/src/components/date-picker/date-picker.stories.tsx +0 -103
  149. package/src/components/date-picker/date-picker.test.tsx +0 -205
  150. package/src/components/dialog/dialog.stories.tsx +0 -140
  151. package/src/components/dialog/dialog.test.tsx +0 -186
  152. package/src/components/drawer/drawer.stories.tsx +0 -111
  153. package/src/components/drawer/drawer.test.tsx +0 -187
  154. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +0 -128
  155. package/src/components/dropdown-menu/dropdown-menu.test.tsx +0 -321
  156. package/src/components/empty-state/empty-state.stories.tsx +0 -70
  157. package/src/components/empty-state/empty-state.test.tsx +0 -97
  158. package/src/components/form/form.stories.tsx +0 -144
  159. package/src/components/form/form.test.tsx +0 -203
  160. package/src/components/input/input.stories.tsx +0 -84
  161. package/src/components/input/input.test.tsx +0 -108
  162. package/src/components/label/label.stories.tsx +0 -51
  163. package/src/components/label/label.test.tsx +0 -55
  164. package/src/components/pagination/pagination.stories.tsx +0 -129
  165. package/src/components/pagination/pagination.test.tsx +0 -119
  166. package/src/components/popover/popover.stories.tsx +0 -84
  167. package/src/components/popover/popover.test.tsx +0 -163
  168. package/src/components/progress/progress.stories.tsx +0 -90
  169. package/src/components/progress/progress.test.tsx +0 -78
  170. package/src/components/radio-group/radio-group.stories.tsx +0 -102
  171. package/src/components/radio-group/radio-group.test.tsx +0 -152
  172. package/src/components/select/select.stories.tsx +0 -137
  173. package/src/components/select/select.test.tsx +0 -209
  174. package/src/components/separator/separator.stories.tsx +0 -42
  175. package/src/components/separator/separator.test.tsx +0 -46
  176. package/src/components/sheet/sheet.stories.tsx +0 -107
  177. package/src/components/sheet/sheet.test.tsx +0 -152
  178. package/src/components/skeleton/skeleton.stories.tsx +0 -46
  179. package/src/components/skeleton/skeleton.test.tsx +0 -57
  180. package/src/components/slider/slider.stories.tsx +0 -87
  181. package/src/components/slider/slider.test.tsx +0 -148
  182. package/src/components/spinner/spinner.stories.tsx +0 -47
  183. package/src/components/spinner/spinner.test.tsx +0 -41
  184. package/src/components/switch/switch.stories.tsx +0 -70
  185. package/src/components/switch/switch.test.tsx +0 -101
  186. package/src/components/table/table.stories.tsx +0 -131
  187. package/src/components/table/table.test.tsx +0 -153
  188. package/src/components/tabs/tabs.stories.tsx +0 -139
  189. package/src/components/tabs/tabs.test.tsx +0 -148
  190. package/src/components/toast/toast.stories.tsx +0 -167
  191. package/src/components/toast/toast.test.tsx +0 -306
  192. package/src/components/tooltip/tooltip.stories.tsx +0 -89
  193. package/src/components/tooltip/tooltip.test.tsx +0 -120
  194. package/src/lib/utils.test.ts +0 -32
  195. package/src/registry/meta.test.ts +0 -234
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { createContext, useContext, useMemo } from "react";
5
+ //#region src/components/meter/meter.tsx
6
+ /**
7
+ * A measurement against a known capacity: storage against a quota, seats
8
+ * against a plan, spend against a budget.
9
+ *
10
+ * Not a Progress. Progress reports how far along a task is and is expected to
11
+ * reach the end; a meter reports a level that has no reason to move at all, and
12
+ * where being near the top is the thing worth noticing. They carry different
13
+ * ARIA roles for exactly that reason, and screen readers phrase them
14
+ * differently.
15
+ *
16
+ * The bar is a single `role="meter"` and the segments inside it are decoration.
17
+ * Making each segment its own widget would put five unlabelled meters in the
18
+ * tab order to describe one quantity; the legend carries the per-segment detail
19
+ * as text instead, where it can actually be read.
20
+ */
21
+ const TONES = {
22
+ primary: "bg-primary",
23
+ info: "bg-info",
24
+ success: "bg-success",
25
+ warning: "bg-warning",
26
+ destructive: "bg-destructive",
27
+ neutral: "bg-border-strong"
28
+ };
29
+ const MeterContext = createContext(null);
30
+ function useMeterContext(component) {
31
+ const context = useContext(MeterContext);
32
+ if (!context) throw new Error(`${component} must be rendered inside <Meter>.`);
33
+ return context;
34
+ }
35
+ function defaultFormat(value) {
36
+ return new Intl.NumberFormat().format(value);
37
+ }
38
+ function Meter({ className, segments, max, label, warnAt = .9, threshold, format = defaultFormat, children, ...props }) {
39
+ const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);
40
+ const over = total > max;
41
+ const strained = max > 0 && total / max >= warnAt;
42
+ const denominator = over ? total : max;
43
+ const context = useMemo(() => ({
44
+ segments,
45
+ total,
46
+ max,
47
+ format,
48
+ over
49
+ }), [
50
+ segments,
51
+ total,
52
+ max,
53
+ format,
54
+ over
55
+ ]);
56
+ return /* @__PURE__ */ jsx(MeterContext.Provider, {
57
+ value: context,
58
+ children: /* @__PURE__ */ jsxs("div", {
59
+ "data-slot": "meter",
60
+ "data-over": over || void 0,
61
+ "data-strained": strained || void 0,
62
+ className: cn("flex w-full flex-col gap-1.5", className),
63
+ ...props,
64
+ children: [/* @__PURE__ */ jsxs("div", {
65
+ role: "meter",
66
+ "aria-label": label,
67
+ "aria-valuenow": total,
68
+ "aria-valuemin": 0,
69
+ "aria-valuemax": max,
70
+ "aria-valuetext": `${format(total)} of ${format(max)}${over ? ", over capacity" : ""}`,
71
+ "data-slot": "meter-track",
72
+ className: "relative h-2 w-full overflow-hidden rounded-full bg-muted",
73
+ children: [/* @__PURE__ */ jsx("div", {
74
+ className: "flex h-full w-full",
75
+ children: segments.map((segment) => {
76
+ const value = Math.max(0, segment.value);
77
+ if (value === 0 || denominator === 0) return null;
78
+ return /* @__PURE__ */ jsx("div", {
79
+ "data-slot": "meter-segment",
80
+ "data-tone": segment.tone ?? "primary",
81
+ className: cn("h-full first:rounded-l-full last:rounded-r-full", "transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]", TONES[segment.tone ?? "primary"]),
82
+ style: { width: `${String(value / denominator * 100)}%` }
83
+ }, segment.id);
84
+ })
85
+ }), threshold && max > 0 && threshold.value > 0 && threshold.value < max ? /* @__PURE__ */ jsx("div", {
86
+ "data-slot": "meter-threshold",
87
+ "aria-hidden": "true",
88
+ className: "absolute inset-y-0 w-px bg-foreground/45",
89
+ style: { left: `${String(threshold.value / denominator * 100)}%` }
90
+ }) : null]
91
+ }), children]
92
+ })
93
+ });
94
+ }
95
+ /**
96
+ * The per-segment breakdown, as text.
97
+ *
98
+ * This is where the detail lives. The bar communicates one number; anyone who
99
+ * needs to know that images account for most of it reads it here.
100
+ */
101
+ function MeterLegend({ className, ...props }) {
102
+ const { segments, format } = useMeterContext("MeterLegend");
103
+ return /* @__PURE__ */ jsx("ul", {
104
+ "data-slot": "meter-legend",
105
+ className: cn("flex flex-wrap gap-x-4 gap-y-1 text-xs", className),
106
+ ...props,
107
+ children: segments.map((segment) => /* @__PURE__ */ jsxs("li", {
108
+ className: "flex items-center gap-1.5",
109
+ children: [
110
+ /* @__PURE__ */ jsx("span", {
111
+ "aria-hidden": "true",
112
+ className: cn("size-2 shrink-0 rounded-full", TONES[segment.tone ?? "primary"])
113
+ }),
114
+ /* @__PURE__ */ jsx("span", {
115
+ className: "text-muted-foreground",
116
+ children: segment.label
117
+ }),
118
+ /* @__PURE__ */ jsx("span", {
119
+ className: "tabular-nums",
120
+ children: format(segment.value)
121
+ })
122
+ ]
123
+ }, segment.id))
124
+ });
125
+ }
126
+ /** The headline "4.2 GB of 10 GB" line, with an over-capacity state. */
127
+ function MeterValue({ className, ...props }) {
128
+ const { total, max, format, over } = useMeterContext("MeterValue");
129
+ return /* @__PURE__ */ jsxs("p", {
130
+ "data-slot": "meter-value",
131
+ className: cn("text-xs tabular-nums", over ? "text-destructive" : "text-muted-foreground", className),
132
+ ...props,
133
+ children: [
134
+ format(total),
135
+ " of ",
136
+ format(max),
137
+ over ? " — over capacity" : null
138
+ ]
139
+ });
140
+ }
141
+ //#endregion
142
+ export { Meter, MeterLegend, MeterValue };
143
+
144
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","names":[],"sources":["../../../src/components/meter/meter.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useMemo, type ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A measurement against a known capacity: storage against a quota, seats\n * against a plan, spend against a budget.\n *\n * Not a Progress. Progress reports how far along a task is and is expected to\n * reach the end; a meter reports a level that has no reason to move at all, and\n * where being near the top is the thing worth noticing. They carry different\n * ARIA roles for exactly that reason, and screen readers phrase them\n * differently.\n *\n * The bar is a single `role=\"meter\"` and the segments inside it are decoration.\n * Making each segment its own widget would put five unlabelled meters in the\n * tab order to describe one quantity; the legend carries the per-segment detail\n * as text instead, where it can actually be read.\n */\n\nconst TONES = {\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n neutral: \"bg-border-strong\",\n} as const;\n\nexport type MeterTone = keyof typeof TONES;\n\nexport interface MeterSegment {\n /** Stable identity, so a re-order does not re-animate every segment. */\n id: string;\n /** Named in the legend and in the accessible summary. */\n label: string;\n value: number;\n tone?: MeterTone;\n}\n\ninterface MeterContextValue {\n segments: MeterSegment[];\n total: number;\n max: number;\n format: (value: number) => string;\n over: boolean;\n}\n\nconst MeterContext = createContext<MeterContextValue | null>(null);\n\nfunction useMeterContext(component: string): MeterContextValue {\n const context = useContext(MeterContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Meter>.`);\n }\n return context;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nexport interface MeterProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** Parts making up the measurement. One segment is the ordinary case. */\n segments: MeterSegment[];\n /** The capacity being measured against. */\n max: number;\n /** Names the meter. Required — an unnamed measurement is unreadable. */\n label: string;\n /**\n * Fraction of `max` past which the meter reports strain, between 0 and 1.\n * Presentation only: it never changes the reported value.\n */\n warnAt?: number;\n /** Optional marker on the track, for a soft limit or an alert threshold. */\n threshold?: { value: number; label: string };\n /** Formats every number that reaches the reader. */\n format?: (value: number) => string;\n /** Shown after the bar. Compose `MeterLegend` here, or your own summary. */\n children?: React.ReactNode;\n}\n\nexport function Meter({\n className,\n segments,\n max,\n label,\n warnAt = 0.9,\n threshold,\n format = defaultFormat,\n children,\n ...props\n}: MeterProps) {\n const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);\n const over = total > max;\n const strained = max > 0 && total / max >= warnAt;\n\n // Widths are a share of capacity, not of the total, so a half-full meter\n // renders half full. Dividing by the total instead would make every meter\n // look completely full, which is the usual bug in hand-rolled versions.\n const denominator = over ? total : max;\n\n const context = useMemo<MeterContextValue>(\n () => ({ segments, total, max, format, over }),\n [segments, total, max, format, over],\n );\n\n return (\n <MeterContext.Provider value={context}>\n <div\n data-slot=\"meter\"\n data-over={over || undefined}\n data-strained={strained || undefined}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuenow={total}\n aria-valuemin={0}\n aria-valuemax={max}\n // Without this a screen reader announces a bare number with no unit\n // and no capacity — \"4\" tells the reader nothing about 4 of what.\n aria-valuetext={`${format(total)} of ${format(max)}${over ? \", over capacity\" : \"\"}`}\n data-slot=\"meter-track\"\n className=\"relative h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"flex h-full w-full\">\n {segments.map((segment) => {\n const value = Math.max(0, segment.value);\n // A zero segment renders nothing at all. A one-pixel sliver reads\n // as a small amount rather than as none.\n if (value === 0 || denominator === 0) return null;\n\n return (\n <div\n key={segment.id}\n data-slot=\"meter-segment\"\n data-tone={segment.tone ?? \"primary\"}\n className={cn(\n \"h-full first:rounded-l-full last:rounded-r-full\",\n \"transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n TONES[segment.tone ?? \"primary\"],\n )}\n style={{ width: `${String((value / denominator) * 100)}%` }}\n />\n );\n })}\n </div>\n\n {threshold && max > 0 && threshold.value > 0 && threshold.value < max ? (\n <div\n data-slot=\"meter-threshold\"\n // The marker repeats what the legend states in words, so it is\n // decoration. Announcing it would interrupt the value.\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 w-px bg-foreground/45\"\n style={{ left: `${String((threshold.value / denominator) * 100)}%` }}\n />\n ) : null}\n </div>\n\n {children}\n </div>\n </MeterContext.Provider>\n );\n}\n\n/**\n * The per-segment breakdown, as text.\n *\n * This is where the detail lives. The bar communicates one number; anyone who\n * needs to know that images account for most of it reads it here.\n */\nexport function MeterLegend({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n const { segments, format } = useMeterContext(\"MeterLegend\");\n\n return (\n <ul\n data-slot=\"meter-legend\"\n className={cn(\"flex flex-wrap gap-x-4 gap-y-1 text-xs\", className)}\n {...props}\n >\n {segments.map((segment) => (\n <li key={segment.id} className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className={cn(\"size-2 shrink-0 rounded-full\", TONES[segment.tone ?? \"primary\"])}\n />\n <span className=\"text-muted-foreground\">{segment.label}</span>\n <span className=\"tabular-nums\">{format(segment.value)}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** The headline \"4.2 GB of 10 GB\" line, with an over-capacity state. */\nexport function MeterValue({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { total, max, format, over } = useMeterContext(\"MeterValue\");\n\n return (\n <p\n data-slot=\"meter-value\"\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n {format(total)} of {format(max)}\n {over ? \" — over capacity\" : null}\n </p>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { MetricDelta, MetricDeltaProps, MetricPolarity, deltaVariants } from "./metric-delta.js";
2
+ export { MetricDelta, type MetricDeltaProps, type MetricPolarity, deltaVariants };
@@ -0,0 +1,2 @@
1
+ import { MetricDelta, deltaVariants } from "./metric-delta.js";
2
+ export { MetricDelta, deltaVariants };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/metric-delta/meta.ts
3
+ const meta = defineMeta({
4
+ name: "metric-delta",
5
+ title: "Metric Delta",
6
+ description: "A headline number with a change indicator that knows which direction is good.",
7
+ category: "data",
8
+ status: "stable",
9
+ dependencies: ["class-variance-authority"],
10
+ registryDependencies: [],
11
+ files: ["metric-delta.tsx"],
12
+ a11y: "The meaning is never carried by colour and an arrow alone, which fails WCAG 1.4.1 — the direction and the comparison are always stated in text. The tile reads as one sentence rather than three fragments: the visible number and delta are aria-hidden and a single screen-reader description carries value, direction, comparison and, where given, sample size. A zero baseline is announced as having no percentage rather than as an infinite one."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/metric-delta/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"metric-delta\",\n title: \"Metric Delta\",\n description: \"A headline number with a change indicator that knows which direction is good.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [\"class-variance-authority\"],\n registryDependencies: [],\n files: [\"metric-delta.tsx\"],\n a11y:\n \"The meaning is never carried by colour and an arrow alone, which fails WCAG 1.4.1 — the \" +\n \"direction and the comparison are always stated in text. The tile reads as one sentence \" +\n \"rather than three fragments: the visible number and delta are aria-hidden and a single \" +\n \"screen-reader description carries value, direction, comparison and, where given, sample \" +\n \"size. A zero baseline is announced as having no percentage rather than as an infinite one.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC,0BAA0B;CACzC,sBAAsB,CAAC;CACvB,OAAO,CAAC,kBAAkB;CAC1B,MACE;AAKJ,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { VariantProps } from "class-variance-authority";
2
+ import { ComponentPropsWithRef } from "react";
3
+ //#region src/components/metric-delta/metric-delta.d.ts
4
+ /**
5
+ * A headline number with an honest change indicator.
6
+ *
7
+ * The visual is a commodity; the correctness is not. Three things go wrong in
8
+ * almost every hand-rolled version, and all three are handled here:
9
+ *
10
+ * 1. Direction is assumed to be good news. Rising churn, rising latency and
11
+ * rising cost are all bad, and painting them green is worse than showing no
12
+ * colour at all. `polarity` decides.
13
+ * 2. A zero baseline produces `+∞%` or `NaN`. There is no percentage change
14
+ * from nothing, so this says so in words instead of inventing one.
15
+ * 3. Meaning is carried by colour and an arrow alone, which fails WCAG 1.4.1
16
+ * and is unreadable to anyone who does not already know the convention. The
17
+ * direction is always stated in text.
18
+ *
19
+ * What this deliberately does NOT do is claim statistical significance. A
20
+ * component holding two numbers cannot know whether a change is significant —
21
+ * that needs the sample size and a test. `insignificantBelow` is a presentation
22
+ * threshold and is described as one; passing `sampleSize` only adds it to the
23
+ * accessible description, it does not license a stronger claim.
24
+ */
25
+ type MetricPolarity = "higher-is-better" | "lower-is-better" | "neutral";
26
+ declare const deltaVariants: (props?: ({
27
+ sentiment?: "good" | "bad" | "neutral" | null | undefined;
28
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
+ interface MetricDeltaProps extends Omit<ComponentPropsWithRef<"div">, "children">, VariantProps<typeof deltaVariants> {
30
+ /** The current value. */
31
+ value: number;
32
+ /** The value being compared against. Omit when there is nothing to compare. */
33
+ previous?: number;
34
+ /** Names the metric. */
35
+ label: string;
36
+ /**
37
+ * Whether a rise is good news. Churn, latency, error rate and cost are all
38
+ * `lower-is-better`, and getting this wrong paints bad news green.
39
+ */
40
+ polarity?: MetricPolarity;
41
+ /**
42
+ * Relative change below which the delta is shown without sentiment, as a
43
+ * fraction. Presentation only — this is not a significance test.
44
+ */
45
+ insignificantBelow?: number;
46
+ /**
47
+ * Included in the accessible description when given, so a reader can judge
48
+ * the change for themselves. Never used to compute significance.
49
+ */
50
+ sampleSize?: number;
51
+ /** What the previous value represents, e.g. "vs last week". */
52
+ comparisonLabel?: string;
53
+ /** Formats the headline value. */
54
+ format?: (value: number) => string;
55
+ }
56
+ declare function MetricDelta({ className, value, previous, label, polarity, insignificantBelow, sampleSize, comparisonLabel, format, ...props }: MetricDeltaProps): import("react").JSX.Element;
57
+ //#endregion
58
+ export { MetricDelta, MetricDeltaProps, MetricPolarity, deltaVariants };
59
+ //# sourceMappingURL=metric-delta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric-delta.d.ts","names":[],"sources":["../../../src/components/metric-delta/metric-delta.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KA6BY;cAEN,gBAAa;;6CASjB;UAae,yBACP,KAAK,2CAA2C,oBAAoB;;EAE5E;;EAEA;;EAEA;;;;;EAKA,WAAW;;;;;EAKX;;;;;EAKA;;EAEA;;EAEA,UAAU;;iBAaI,cACd,WACA,OACA,UACA,OACA,UACA,oBACA,YACA,iBACA,WACG,SACF,mCAAgB,IAAA"}
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { cva } from "class-variance-authority";
5
+ //#region src/components/metric-delta/metric-delta.tsx
6
+ const deltaVariants = cva("inline-flex items-center gap-1 text-xs font-medium tabular-nums", {
7
+ variants: { sentiment: {
8
+ good: "text-success",
9
+ bad: "text-destructive",
10
+ neutral: "text-muted-foreground"
11
+ } },
12
+ defaultVariants: { sentiment: "neutral" }
13
+ });
14
+ /** What the change means, once polarity is applied. */
15
+ function sentimentOf(change, polarity, meaningful) {
16
+ if (!meaningful || change === 0 || polarity === "neutral") return "neutral";
17
+ return (polarity === "higher-is-better" ? change > 0 : change < 0) ? "good" : "bad";
18
+ }
19
+ function defaultFormat(value) {
20
+ return new Intl.NumberFormat().format(value);
21
+ }
22
+ const percentFormatter = new Intl.NumberFormat(void 0, {
23
+ style: "percent",
24
+ maximumFractionDigits: 1,
25
+ signDisplay: "exceptZero"
26
+ });
27
+ function MetricDelta({ className, value, previous, label, polarity = "higher-is-better", insignificantBelow = 0, sampleSize, comparisonLabel = "vs previous period", format = defaultFormat, ...props }) {
28
+ const hasComparison = previous !== void 0;
29
+ const absoluteChange = hasComparison ? value - previous : 0;
30
+ const relativeChange = hasComparison && previous !== 0 ? absoluteChange / Math.abs(previous) : null;
31
+ const meaningful = relativeChange === null ? absoluteChange !== 0 : Math.abs(relativeChange) >= insignificantBelow;
32
+ const sentiment = hasComparison ? sentimentOf(absoluteChange, polarity, meaningful) : "neutral";
33
+ const direction = absoluteChange > 0 ? "up" : absoluteChange < 0 ? "down" : "flat";
34
+ const changeText = !hasComparison ? null : relativeChange !== null ? percentFormatter.format(relativeChange) : `${absoluteChange > 0 ? "+" : ""}${format(absoluteChange)}`;
35
+ const description = [
36
+ `${label}: ${format(value)}`,
37
+ hasComparison ? relativeChange !== null ? `${direction === "flat" ? "unchanged" : direction} ${changeText ?? ""} ${comparisonLabel}` : `${direction === "flat" ? "unchanged" : direction} by ${format(Math.abs(absoluteChange))} ${comparisonLabel}, no percentage available from a zero baseline` : null,
38
+ hasComparison && !meaningful && direction !== "flat" ? "not a meaningful change" : null,
39
+ sampleSize !== void 0 ? `sample size ${format(sampleSize)}` : null
40
+ ].filter(Boolean).join(", ");
41
+ return /* @__PURE__ */ jsxs("div", {
42
+ "data-slot": "metric-delta",
43
+ "data-direction": direction,
44
+ "data-sentiment": sentiment,
45
+ className: cn("flex flex-col gap-1", className),
46
+ ...props,
47
+ children: [
48
+ /* @__PURE__ */ jsx("p", {
49
+ className: "text-xs text-muted-foreground",
50
+ children: label
51
+ }),
52
+ /* @__PURE__ */ jsx("p", {
53
+ className: "sr-only",
54
+ children: description
55
+ }),
56
+ /* @__PURE__ */ jsx("p", {
57
+ "aria-hidden": "true",
58
+ className: "text-2xl leading-tight font-semibold tracking-tight tabular-nums",
59
+ children: format(value)
60
+ }),
61
+ hasComparison ? /* @__PURE__ */ jsxs("p", {
62
+ "aria-hidden": "true",
63
+ className: "flex flex-wrap items-center gap-x-1.5 gap-y-0.5",
64
+ children: [/* @__PURE__ */ jsxs("span", {
65
+ className: cn(deltaVariants({ sentiment })),
66
+ "data-slot": "metric-delta-change",
67
+ children: [/* @__PURE__ */ jsx("span", {
68
+ "aria-hidden": "true",
69
+ children: direction === "up" ? "↑" : direction === "down" ? "↓" : "→"
70
+ }), changeText]
71
+ }), /* @__PURE__ */ jsx("span", {
72
+ className: "text-xs text-muted-foreground",
73
+ children: comparisonLabel
74
+ })]
75
+ }) : null
76
+ ]
77
+ });
78
+ }
79
+ //#endregion
80
+ export { MetricDelta, deltaVariants };
81
+
82
+ //# sourceMappingURL=metric-delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metric-delta.js","names":[],"sources":["../../../src/components/metric-delta/metric-delta.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A headline number with an honest change indicator.\n *\n * The visual is a commodity; the correctness is not. Three things go wrong in\n * almost every hand-rolled version, and all three are handled here:\n *\n * 1. Direction is assumed to be good news. Rising churn, rising latency and\n * rising cost are all bad, and painting them green is worse than showing no\n * colour at all. `polarity` decides.\n * 2. A zero baseline produces `+∞%` or `NaN`. There is no percentage change\n * from nothing, so this says so in words instead of inventing one.\n * 3. Meaning is carried by colour and an arrow alone, which fails WCAG 1.4.1\n * and is unreadable to anyone who does not already know the convention. The\n * direction is always stated in text.\n *\n * What this deliberately does NOT do is claim statistical significance. A\n * component holding two numbers cannot know whether a change is significant —\n * that needs the sample size and a test. `insignificantBelow` is a presentation\n * threshold and is described as one; passing `sampleSize` only adds it to the\n * accessible description, it does not license a stronger claim.\n */\n\nexport type MetricPolarity = \"higher-is-better\" | \"lower-is-better\" | \"neutral\";\n\nconst deltaVariants = cva(\"inline-flex items-center gap-1 text-xs font-medium tabular-nums\", {\n variants: {\n sentiment: {\n good: \"text-success\",\n bad: \"text-destructive\",\n neutral: \"text-muted-foreground\",\n },\n },\n defaultVariants: { sentiment: \"neutral\" },\n});\n\n/** What the change means, once polarity is applied. */\nfunction sentimentOf(\n change: number,\n polarity: MetricPolarity,\n meaningful: boolean,\n): \"good\" | \"bad\" | \"neutral\" {\n if (!meaningful || change === 0 || polarity === \"neutral\") return \"neutral\";\n const improved = polarity === \"higher-is-better\" ? change > 0 : change < 0;\n return improved ? \"good\" : \"bad\";\n}\n\nexport interface MetricDeltaProps\n extends Omit<ComponentPropsWithRef<\"div\">, \"children\">, VariantProps<typeof deltaVariants> {\n /** The current value. */\n value: number;\n /** The value being compared against. Omit when there is nothing to compare. */\n previous?: number;\n /** Names the metric. */\n label: string;\n /**\n * Whether a rise is good news. Churn, latency, error rate and cost are all\n * `lower-is-better`, and getting this wrong paints bad news green.\n */\n polarity?: MetricPolarity;\n /**\n * Relative change below which the delta is shown without sentiment, as a\n * fraction. Presentation only — this is not a significance test.\n */\n insignificantBelow?: number;\n /**\n * Included in the accessible description when given, so a reader can judge\n * the change for themselves. Never used to compute significance.\n */\n sampleSize?: number;\n /** What the previous value represents, e.g. \"vs last week\". */\n comparisonLabel?: string;\n /** Formats the headline value. */\n format?: (value: number) => string;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nconst percentFormatter = new Intl.NumberFormat(undefined, {\n style: \"percent\",\n maximumFractionDigits: 1,\n signDisplay: \"exceptZero\",\n});\n\nexport function MetricDelta({\n className,\n value,\n previous,\n label,\n polarity = \"higher-is-better\",\n insignificantBelow = 0,\n sampleSize,\n comparisonLabel = \"vs previous period\",\n format = defaultFormat,\n ...props\n}: MetricDeltaProps) {\n const hasComparison = previous !== undefined;\n const absoluteChange = hasComparison ? value - previous : 0;\n\n // A percentage change from zero is undefined, not infinite. Reporting it as\n // a number at all is the lie; the component says \"no prior value\" instead.\n const canComputeRelative = hasComparison && previous !== 0;\n const relativeChange = canComputeRelative ? absoluteChange / Math.abs(previous) : null;\n\n const meaningful =\n relativeChange === null\n ? absoluteChange !== 0\n : Math.abs(relativeChange) >= insignificantBelow;\n\n const sentiment = hasComparison\n ? sentimentOf(absoluteChange, polarity, meaningful)\n : \"neutral\";\n\n const direction = absoluteChange > 0 ? \"up\" : absoluteChange < 0 ? \"down\" : \"flat\";\n\n const changeText = !hasComparison\n ? null\n : relativeChange !== null\n ? percentFormatter.format(relativeChange)\n : `${absoluteChange > 0 ? \"+\" : \"\"}${format(absoluteChange)}`;\n\n // Everything a screen reader needs, in one sentence, in the order it matters.\n const description = [\n `${label}: ${format(value)}`,\n hasComparison\n ? relativeChange !== null\n ? `${direction === \"flat\" ? \"unchanged\" : direction} ${changeText ?? \"\"} ${comparisonLabel}`\n : `${direction === \"flat\" ? \"unchanged\" : direction} by ${format(Math.abs(absoluteChange))} ${comparisonLabel}, no percentage available from a zero baseline`\n : null,\n hasComparison && !meaningful && direction !== \"flat\" ? \"not a meaningful change\" : null,\n sampleSize !== undefined ? `sample size ${format(sampleSize)}` : null,\n ]\n .filter(Boolean)\n .join(\", \");\n\n return (\n <div\n data-slot=\"metric-delta\"\n data-direction={direction}\n data-sentiment={sentiment}\n className={cn(\"flex flex-col gap-1\", className)}\n {...props}\n >\n <p className=\"text-xs text-muted-foreground\">{label}</p>\n\n {/* One accessible sentence for the whole tile. The parts below are hidden\n so the reader gets the meaning once, not as three fragments. */}\n <p className=\"sr-only\">{description}</p>\n\n <p\n aria-hidden=\"true\"\n className=\"text-2xl leading-tight font-semibold tracking-tight tabular-nums\"\n >\n {format(value)}\n </p>\n\n {hasComparison ? (\n <p aria-hidden=\"true\" className=\"flex flex-wrap items-center gap-x-1.5 gap-y-0.5\">\n <span className={cn(deltaVariants({ sentiment }))} data-slot=\"metric-delta-change\">\n <span aria-hidden=\"true\">\n {direction === \"up\" ? \"↑\" : direction === \"down\" ? \"↓\" : \"→\"}\n </span>\n {changeText}\n </span>\n <span className=\"text-xs text-muted-foreground\">{comparisonLabel}</span>\n </p>\n ) : null}\n </div>\n );\n}\n\nexport { deltaVariants };\n"],"mappings":";;;;;AA+BA,MAAM,gBAAgB,IAAI,mEAAmE;CAC3F,UAAU,EACR,WAAW;EACT,MAAM;EACN,KAAK;EACL,SAAS;CACX,EACF;CACA,iBAAiB,EAAE,WAAW,UAAU;AAC1C,CAAC;;AAGD,SAAS,YACP,QACA,UACA,YAC4B;CAC5B,IAAI,CAAC,cAAc,WAAW,KAAK,aAAa,WAAW,OAAO;CAElE,QADiB,aAAa,qBAAqB,SAAS,IAAI,SAAS,KACvD,SAAS;AAC7B;AA+BA,SAAS,cAAc,OAAuB;CAC5C,OAAO,IAAI,KAAK,aAAa,CAAC,CAAC,OAAO,KAAK;AAC7C;AAEA,MAAM,mBAAmB,IAAI,KAAK,aAAa,KAAA,GAAW;CACxD,OAAO;CACP,uBAAuB;CACvB,aAAa;AACf,CAAC;AAED,SAAgB,YAAY,EAC1B,WACA,OACA,UACA,OACA,WAAW,oBACX,qBAAqB,GACrB,YACA,kBAAkB,sBAClB,SAAS,eACT,GAAG,SACgB;CACnB,MAAM,gBAAgB,aAAa,KAAA;CACnC,MAAM,iBAAiB,gBAAgB,QAAQ,WAAW;CAK1D,MAAM,iBADqB,iBAAiB,aAAa,IACb,iBAAiB,KAAK,IAAI,QAAQ,IAAI;CAElF,MAAM,aACJ,mBAAmB,OACf,mBAAmB,IACnB,KAAK,IAAI,cAAc,KAAK;CAElC,MAAM,YAAY,gBACd,YAAY,gBAAgB,UAAU,UAAU,IAChD;CAEJ,MAAM,YAAY,iBAAiB,IAAI,OAAO,iBAAiB,IAAI,SAAS;CAE5E,MAAM,aAAa,CAAC,gBAChB,OACA,mBAAmB,OACjB,iBAAiB,OAAO,cAAc,IACtC,GAAG,iBAAiB,IAAI,MAAM,KAAK,OAAO,cAAc;CAG9D,MAAM,cAAc;EAClB,GAAG,MAAM,IAAI,OAAO,KAAK;EACzB,gBACI,mBAAmB,OACjB,GAAG,cAAc,SAAS,cAAc,UAAU,GAAG,cAAc,GAAG,GAAG,oBACzE,GAAG,cAAc,SAAS,cAAc,UAAU,MAAM,OAAO,KAAK,IAAI,cAAc,CAAC,EAAE,GAAG,gBAAgB,kDAC9G;EACJ,iBAAiB,CAAC,cAAc,cAAc,SAAS,4BAA4B;EACnF,eAAe,KAAA,IAAY,eAAe,OAAO,UAAU,MAAM;CACnE,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,IAAI;CAEZ,OACE,qBAAC,OAAD;EACE,aAAU;EACV,kBAAgB;EAChB,kBAAgB;EAChB,WAAW,GAAG,uBAAuB,SAAS;EAC9C,GAAI;EALN,UAAA;GAOE,oBAAC,KAAD;IAAG,WAAU;IAAiC,UAAA;GAAS,CAAA;GAIvD,oBAAC,KAAD;IAAG,WAAU;IAAW,UAAA;GAAe,CAAA;GAEvC,oBAAC,KAAD;IACE,eAAY;IACZ,WAAU;IAET,UAAA,OAAO,KAAK;GACZ,CAAA;GAEF,gBACC,qBAAC,KAAD;IAAG,eAAY;IAAO,WAAU;IAAhC,UAAA,CACE,qBAAC,QAAD;KAAM,WAAW,GAAG,cAAc,EAAE,UAAU,CAAC,CAAC;KAAG,aAAU;KAA7D,UAAA,CACE,oBAAC,QAAD;MAAM,eAAY;MACf,UAAA,cAAc,OAAO,MAAM,cAAc,SAAS,MAAM;KACrD,CAAA,GACL,UACG;IACN,CAAA,GAAA,oBAAC,QAAD;KAAM,WAAU;KAAiC,UAAA;IAAsB,CAAA,CACtE;GACD,CAAA,IAAA;EACD;;AAET"}
@@ -4,7 +4,7 @@ import { ComponentPropsWithRef } from "react";
4
4
  //#region src/components/progress/progress.d.ts
5
5
  declare const progressVariants: (props?: ({
6
6
  size?: "sm" | "md" | "lg" | null | undefined;
7
- tone?: "destructive" | "success" | "warning" | "primary" | null | undefined;
7
+ tone?: "primary" | "destructive" | "success" | "warning" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  interface ProgressProps extends ComponentPropsWithRef<typeof Progress.Root>, VariantProps<typeof progressVariants> {}
10
10
  /**
@@ -0,0 +1,2 @@
1
+ import { FieldChange, RecordDiff, RecordDiffProps, RecordDiffSummary, RecordDiffTable, RecordDiffTableProps, RecordField, diffRecords } from "./record-diff.js";
2
+ export { type FieldChange, RecordDiff, type RecordDiffProps, RecordDiffSummary, RecordDiffTable, type RecordDiffTableProps, type RecordField, diffRecords };
@@ -0,0 +1,2 @@
1
+ import { RecordDiff, RecordDiffSummary, RecordDiffTable, diffRecords } from "./record-diff.js";
2
+ export { RecordDiff, RecordDiffSummary, RecordDiffTable, diffRecords };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/record-diff/meta.ts
3
+ const meta = defineMeta({
4
+ name: "record-diff",
5
+ title: "Record Diff",
6
+ description: "Field-level before and after, for audit entries, settings history and revisions.",
7
+ category: "data",
8
+ status: "stable",
9
+ dependencies: [],
10
+ registryDependencies: [],
11
+ files: ["record-diff.tsx"],
12
+ a11y: "A real table, not a grid of divs. Each field name is a th with scope=\"row\", so a value is announced with the field it belongs to rather than as a loose cell. The kind of change is never carried by row colour alone — every row states added, removed or changed in text for screen readers, satisfying WCAG 1.4.1. The unchanged-field toggle is a real button with aria-expanded. Redacted values never reach the DOM at all."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/record-diff/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"record-diff\",\n title: \"Record Diff\",\n description:\n \"Field-level before and after, for audit entries, settings history and revisions.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"record-diff.tsx\"],\n a11y:\n 'A real table, not a grid of divs. Each field name is a th with scope=\"row\", so a value is ' +\n \"announced with the field it belongs to rather than as a loose cell. The kind of change is \" +\n \"never carried by row colour alone — every row states added, removed or changed in text for \" +\n \"screen readers, satisfying WCAG 1.4.1. The unchanged-field toggle is a real button with \" +\n \"aria-expanded. Redacted values never reach the DOM at all.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,iBAAiB;CACzB,MACE;AAKJ,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { ComponentPropsWithRef } from "react";
2
+ //#region src/components/record-diff/record-diff.d.ts
3
+ /**
4
+ * What changed about a record, field by field.
5
+ *
6
+ * Every diff package on npm compares lines of text. That is the wrong shape for
7
+ * an audit entry, a settings revision or a config history, where the change is
8
+ * "role went from viewer to admin" and rendering it as two lines of JSON makes
9
+ * the reader do the comparison themselves.
10
+ *
11
+ * Scope is deliberately flat records. Nested objects and arrays are formatted as
12
+ * values rather than recursed into, because a recursive differ is a different
13
+ * and much larger component, and pretending otherwise would produce a shallow
14
+ * one that quietly lies about deep changes. `formatValue` is the escape hatch.
15
+ *
16
+ * Redaction is by key pattern and applied before formatting, so a secret is
17
+ * never handed to a formatter that might log it.
18
+ */
19
+ type FieldChange = "added" | "removed" | "changed" | "unchanged";
20
+ interface RecordField {
21
+ key: string;
22
+ label: string;
23
+ change: FieldChange;
24
+ before: unknown;
25
+ after: unknown;
26
+ redacted: boolean;
27
+ }
28
+ interface RecordDiffProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
29
+ before: Record<string, unknown>;
30
+ after: Record<string, unknown>;
31
+ /** Overrides the humanised key. */
32
+ labels?: Record<string, string>;
33
+ /** Keys to render as redacted. Defaults to common secret-shaped names. */
34
+ redact?: RegExp;
35
+ /** Only these keys, in this order. Anything else is ignored. */
36
+ fields?: string[];
37
+ /** Hides unchanged fields behind a toggle. */
38
+ collapseUnchanged?: boolean;
39
+ formatValue?: (value: unknown, key: string) => string;
40
+ children?: React.ReactNode;
41
+ }
42
+ declare function diffRecords(before: Record<string, unknown>, after: Record<string, unknown>, options?: {
43
+ labels?: Record<string, string>;
44
+ redact?: RegExp;
45
+ fields?: string[];
46
+ }): RecordField[];
47
+ declare function RecordDiff({ className, before, after, labels, redact, fields: fieldKeys, collapseUnchanged, formatValue, children, ...props }: RecordDiffProps): import("react").JSX.Element;
48
+ /** "3 fields changed" — the thing a reviewer scanning a log needs first. */
49
+ declare function RecordDiffSummary({ className, ...props }: ComponentPropsWithRef<"p">): import("react").JSX.Element;
50
+ interface RecordDiffTableProps extends ComponentPropsWithRef<"table"> {
51
+ collapseUnchanged?: boolean;
52
+ }
53
+ declare function RecordDiffTable({ className, collapseUnchanged, ...props }: RecordDiffTableProps): import("react").JSX.Element;
54
+ //#endregion
55
+ export { FieldChange, RecordDiff, RecordDiffProps, RecordDiffSummary, RecordDiffTable, RecordDiffTableProps, RecordField, diffRecords };
56
+ //# sourceMappingURL=record-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-diff.d.ts","names":[],"sources":["../../../src/components/record-diff/record-diff.tsx"],"mappings":";;;;;;;;;;;;;;;;;;KA6BY;UAEK;EACf;EACA;EACA,QAAQ;EACR;EACA;EACA;;UAiDe,wBAAwB,KAAK;EAC5C,QAAQ;EACR,OAAO;;EAEP,SAAS;;EAET,SAAS;;EAET;;EAEA;EACA,eAAe,gBAAgB;EAC/B,WAAW,MAAM;;iBA4BH,YACd,QAAQ,yBACR,OAAO,yBACP;EAAW,SAAS;EAAwB,SAAS;EAAQ;IAC5D;iBAwBa,aACd,WACA,QACA,OACA,QACA,QACA,QAAQ,WACR,mBACA,aACA,aACG,SACF,kCAAe,IAAA;;iBAiCF,oBAAoB,cAAc,SAAS,6CAA0B,IAAA;UAgBpE,6BAA6B;EAC5C;;iBAGc,kBACd,WACA,sBACG,SACF,uCAAoB,IAAA"}