@eifi1/ui-kit 0.9.0 → 0.11.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 (243) hide show
  1. package/README.md +37 -23
  2. package/dist/components/alert-banner.d.ts +57 -5
  3. package/dist/components/alert-banner.js +63 -17
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +112 -0
  10. package/dist/components/bulk-action-bar.js +196 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/button-group.d.ts +58 -3
  13. package/dist/components/button-group.js +56 -5
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/calculator.d.ts +6 -0
  16. package/dist/components/calendar-heatmap.d.ts +124 -0
  17. package/dist/components/calendar-heatmap.js +295 -0
  18. package/dist/components/calendar-heatmap.js.map +1 -0
  19. package/dist/components/chip.d.ts +31 -5
  20. package/dist/components/chip.js +85 -5
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +34 -2
  23. package/dist/components/choice-card.js +49 -0
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/copy-button.d.ts +14 -4
  26. package/dist/components/copy-button.js +17 -4
  27. package/dist/components/copy-button.js.map +1 -1
  28. package/dist/components/data-table.js +2 -0
  29. package/dist/components/data-table.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +22 -2
  31. package/dist/components/date-picker.js +11 -3
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/description-list.d.ts +43 -6
  34. package/dist/components/description-list.js +91 -11
  35. package/dist/components/description-list.js.map +1 -1
  36. package/dist/components/dialog-frame.d.ts +7 -0
  37. package/dist/components/dialog-frame.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +57 -2
  39. package/dist/components/disclosure.js +24 -12
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/field.d.ts +43 -0
  42. package/dist/components/field.js +52 -0
  43. package/dist/components/field.js.map +1 -0
  44. package/dist/components/file-button.d.ts +1 -0
  45. package/dist/components/file-dropzone.d.ts +4 -4
  46. package/dist/components/file-dropzone.js +2 -1
  47. package/dist/components/file-dropzone.js.map +1 -1
  48. package/dist/components/floating-panel.d.ts +147 -4
  49. package/dist/components/floating-panel.js +188 -28
  50. package/dist/components/floating-panel.js.map +1 -1
  51. package/dist/components/full-bleed-dialog.d.ts +23 -4
  52. package/dist/components/full-bleed-dialog.js +11 -2
  53. package/dist/components/full-bleed-dialog.js.map +1 -1
  54. package/dist/components/list.d.ts +187 -0
  55. package/dist/components/list.js +221 -0
  56. package/dist/components/list.js.map +1 -0
  57. package/dist/components/menu-item.d.ts +109 -0
  58. package/dist/components/menu-item.js +88 -0
  59. package/dist/components/menu-item.js.map +1 -0
  60. package/dist/components/mini-calendar.d.ts +70 -5
  61. package/dist/components/mini-calendar.js +166 -62
  62. package/dist/components/mini-calendar.js.map +1 -1
  63. package/dist/components/modal.d.ts +23 -1
  64. package/dist/components/modal.js +36 -10
  65. package/dist/components/modal.js.map +1 -1
  66. package/dist/components/nav-pills.d.ts +66 -0
  67. package/dist/components/nav-pills.js +65 -0
  68. package/dist/components/nav-pills.js.map +1 -0
  69. package/dist/components/number-field.d.ts +6 -0
  70. package/dist/components/number-input.d.ts +6 -0
  71. package/dist/components/numpad-sheet.d.ts +6 -0
  72. package/dist/components/page-contents.js +1 -2
  73. package/dist/components/page-contents.js.map +1 -1
  74. package/dist/components/page-header.d.ts +37 -0
  75. package/dist/components/page-header.js +36 -0
  76. package/dist/components/page-header.js.map +1 -0
  77. package/dist/components/progress-bar.d.ts +31 -2
  78. package/dist/components/progress-bar.js +73 -7
  79. package/dist/components/progress-bar.js.map +1 -1
  80. package/dist/components/series-chart-labels.d.ts +3 -0
  81. package/dist/components/series-chart-labels.js +2 -1
  82. package/dist/components/series-chart-labels.js.map +1 -1
  83. package/dist/components/series-chart.d.ts +32 -0
  84. package/dist/components/series-chart.js +141 -4
  85. package/dist/components/series-chart.js.map +1 -1
  86. package/dist/components/settings-fields.d.ts +1 -0
  87. package/dist/components/status-dot.d.ts +53 -0
  88. package/dist/components/status-dot.js +57 -0
  89. package/dist/components/status-dot.js.map +1 -0
  90. package/dist/components/table.d.ts +78 -13
  91. package/dist/components/table.js +84 -11
  92. package/dist/components/table.js.map +1 -1
  93. package/dist/components/text.d.ts +70 -0
  94. package/dist/components/text.js +24 -0
  95. package/dist/components/text.js.map +1 -0
  96. package/dist/components/time-input.d.ts +1 -0
  97. package/dist/components/toast.d.ts +167 -0
  98. package/dist/components/toast.js +226 -0
  99. package/dist/components/toast.js.map +1 -0
  100. package/dist/components/toggle-group.d.ts +44 -1
  101. package/dist/components/toggle-group.js +73 -4
  102. package/dist/components/toggle-group.js.map +1 -1
  103. package/dist/components/tooltip.d.ts +46 -15
  104. package/dist/components/tooltip.js +87 -47
  105. package/dist/components/tooltip.js.map +1 -1
  106. package/dist/components/ui.d.ts +158 -7
  107. package/dist/components/ui.js +191 -10
  108. package/dist/components/ui.js.map +1 -1
  109. package/dist/components/user-avatar.d.ts +25 -3
  110. package/dist/components/user-avatar.js +32 -3
  111. package/dist/components/user-avatar.js.map +1 -1
  112. package/dist/hooks/use-close-transition.d.ts +7 -1
  113. package/dist/hooks/use-close-transition.js +5 -2
  114. package/dist/hooks/use-close-transition.js.map +1 -1
  115. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  116. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  117. package/dist/hooks/use-file-drop.d.ts +1 -0
  118. package/dist/i18n/defaults.d.ts +6 -0
  119. package/dist/i18n/defaults.js +11 -1
  120. package/dist/i18n/defaults.js.map +1 -1
  121. package/dist/i18n/kit-labels.d.ts +11 -0
  122. package/dist/i18n/kit-labels.js.map +1 -1
  123. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  124. package/dist/i18n/locales/de-CH.d.ts +6 -0
  125. package/dist/i18n/locales/de-informal.d.ts +6 -0
  126. package/dist/i18n/locales/de.d.ts +6 -0
  127. package/dist/i18n/locales/de.js +30 -2
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +6 -0
  130. package/dist/i18n/locales/es.js +30 -2
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +6 -0
  133. package/dist/i18n/locales/fr.js +30 -2
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +6 -0
  136. package/dist/i18n/locales/hu.js +30 -2
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +6 -0
  139. package/dist/i18n/locales/it.js +30 -2
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +6 -0
  142. package/dist/i18n/locales/zh.js +30 -2
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/index.d.ts +27 -15
  145. package/dist/index.js +15 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/clipping.d.ts +25 -0
  148. package/dist/lib/clipping.js +17 -0
  149. package/dist/lib/clipping.js.map +1 -0
  150. package/dist/lib/dates.d.ts +17 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/rhf/form.d.ts +1 -0
  154. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  155. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  156. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  157. package/dist/rhf.d.ts +2 -0
  158. package/dist/rhf.js +1 -0
  159. package/dist/rhf.js.map +1 -1
  160. package/dist/search/command-palette.d.ts +19 -2
  161. package/dist/search/command-palette.js +16 -5
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/search/global-search.d.ts +26 -1
  164. package/dist/search/global-search.js +29 -13
  165. package/dist/search/global-search.js.map +1 -1
  166. package/dist/search.d.ts +1 -1
  167. package/dist/shell/topbar-action-menu.d.ts +73 -3
  168. package/dist/shell/topbar-action-menu.js +100 -27
  169. package/dist/shell/topbar-action-menu.js.map +1 -1
  170. package/dist/shell.d.ts +2 -1
  171. package/dist/wizard/stepper-nav.d.ts +1 -0
  172. package/dist/wizard/stepper-nav.js +1 -1
  173. package/dist/wizard/stepper-nav.js.map +1 -1
  174. package/dist/wizard/types.d.ts +2 -2
  175. package/dist/wizard/types.js.map +1 -1
  176. package/dist/wizard/use-wizard.js +1 -1
  177. package/dist/wizard/use-wizard.js.map +1 -1
  178. package/dist/wizard/wizard-context.d.ts +7 -1
  179. package/dist/wizard/wizard-context.js +4 -0
  180. package/dist/wizard/wizard-context.js.map +1 -1
  181. package/dist/wizard/wizard-summary.js +3 -3
  182. package/dist/wizard/wizard-summary.js.map +1 -1
  183. package/dist/wizard.d.ts +2 -1
  184. package/package.json +16 -12
  185. package/src/components/alert-banner.tsx +149 -21
  186. package/src/components/breadcrumbs.tsx +168 -0
  187. package/src/components/bulk-action-bar.tsx +362 -0
  188. package/src/components/button-group.tsx +124 -4
  189. package/src/components/calendar-heatmap.tsx +504 -0
  190. package/src/components/chip.tsx +128 -7
  191. package/src/components/choice-card.tsx +97 -1
  192. package/src/components/copy-button.tsx +29 -7
  193. package/src/components/data-table.tsx +7 -0
  194. package/src/components/date-picker.tsx +49 -1
  195. package/src/components/description-list.tsx +168 -15
  196. package/src/components/dialog-frame.tsx +7 -0
  197. package/src/components/disclosure.tsx +93 -21
  198. package/src/components/field.tsx +137 -0
  199. package/src/components/file-dropzone.tsx +9 -10
  200. package/src/components/floating-panel.tsx +366 -15
  201. package/src/components/full-bleed-dialog.tsx +42 -5
  202. package/src/components/list.tsx +443 -0
  203. package/src/components/menu-item.tsx +235 -0
  204. package/src/components/mini-calendar.tsx +245 -54
  205. package/src/components/modal.tsx +80 -17
  206. package/src/components/nav-pills.tsx +145 -0
  207. package/src/components/page-contents.tsx +4 -4
  208. package/src/components/page-header.tsx +68 -0
  209. package/src/components/progress-bar.tsx +129 -11
  210. package/src/components/series-chart-labels.ts +4 -0
  211. package/src/components/series-chart.tsx +274 -3
  212. package/src/components/status-dot.tsx +107 -0
  213. package/src/components/table.tsx +176 -17
  214. package/src/components/text.tsx +97 -0
  215. package/src/components/toast.tsx +441 -0
  216. package/src/components/toggle-group.tsx +128 -4
  217. package/src/components/tooltip.tsx +200 -100
  218. package/src/components/ui.tsx +381 -14
  219. package/src/components/user-avatar.tsx +57 -3
  220. package/src/hooks/use-close-transition.ts +14 -5
  221. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  222. package/src/i18n/defaults.ts +10 -0
  223. package/src/i18n/kit-labels.tsx +10 -0
  224. package/src/i18n/locales/de.ts +29 -0
  225. package/src/i18n/locales/es.ts +29 -0
  226. package/src/i18n/locales/fr.ts +29 -0
  227. package/src/i18n/locales/hu.ts +29 -0
  228. package/src/i18n/locales/it.ts +29 -0
  229. package/src/i18n/locales/zh.ts +29 -0
  230. package/src/index.ts +31 -0
  231. package/src/lib/clipping.ts +34 -0
  232. package/src/lib/dates.ts +37 -0
  233. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  234. package/src/rhf.ts +2 -0
  235. package/src/search/command-palette.tsx +42 -5
  236. package/src/search/global-search.tsx +59 -18
  237. package/src/shell/topbar-action-menu.tsx +239 -49
  238. package/src/wizard/stepper-nav.tsx +2 -1
  239. package/src/wizard/types.ts +2 -2
  240. package/src/wizard/use-wizard.ts +3 -3
  241. package/src/wizard/wizard-context.tsx +9 -0
  242. package/src/wizard/wizard-summary.tsx +14 -10
  243. package/tokens.css +97 -0
@@ -17,6 +17,21 @@ type ProgressBarTone = "brand" | "neutral" | "success" | "warning" | "danger" |
17
17
  * line of text where 8px outweighs the text.
18
18
  */
19
19
  type ProgressBarSize = "sm" | "slim" | "md" | "lg";
20
+ /** One part of a stacked bar. See {@link ProgressBarProps.segments}. */
21
+ interface ProgressBarSegment {
22
+ /** Its size, on the bar's own `min`–`max` scale. */
23
+ value: number;
24
+ /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */
25
+ tone?: ProgressBarTone;
26
+ /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */
27
+ color?: string;
28
+ /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */
29
+ className?: string;
30
+ /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */
31
+ label?: ReactNode;
32
+ /** A stable React key, when `label` is not a string. */
33
+ key?: string;
34
+ }
20
35
  interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
21
36
  /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
22
37
  * and nobody knows how much is left (an import that reports no count). */
@@ -45,6 +60,20 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
45
60
  tone?: ProgressBarTone;
46
61
  size?: ProgressBarSize;
47
62
  locale?: string;
63
+ /**
64
+ * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap
65
+ * between them — keksdose's discretionary-spending bar (cut-card:133), a whole
66
+ * split into the steps of a scale. Forces `variant="meter"`: parts of a whole are
67
+ * never "busy". `aria-valuenow` is the sum; `aria-valuetext` names every part
68
+ * ("Rent: 40%, Food: 25%"), each part's value through `formatValue` and joined in the
69
+ * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader
70
+ * altogether, leaving the legend as the only way in. The sum is clamped to `max`,
71
+ * and parts past it are cut off at the track's end.
72
+ */
73
+ segments?: ProgressBarSegment[];
74
+ /** With `segments`: a list under the bar naming each part with its swatch and value
75
+ * — the table view of the bar, in text tokens rather than the segment colour. */
76
+ legend?: boolean;
48
77
  }
49
78
  /**
50
79
  * A horizontal bar that fills toward `max`.
@@ -63,6 +92,6 @@ interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "childr
63
92
  * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an
64
93
  * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.
65
94
  */
66
- declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
95
+ declare function ProgressBar({ value, min, max, variant, label, showValue, formatValue, tone, size, locale, segments, legend, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...rest }: ProgressBarProps): react.JSX.Element;
67
96
 
68
- export { ProgressBar, type ProgressBarProps, type ProgressBarSize, type ProgressBarTone };
97
+ export { ProgressBar, type ProgressBarProps, type ProgressBarSegment, type ProgressBarSize, type ProgressBarTone };
@@ -14,6 +14,25 @@ const FILL = {
14
14
  expense: "bg-[var(--money-expense)]"
15
15
  };
16
16
  const TRACK_HEIGHT = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
17
+ const SERIES_FILL = [
18
+ "bg-[var(--chart-1)]",
19
+ "bg-[var(--chart-2)]",
20
+ "bg-[var(--chart-3)]",
21
+ "bg-[var(--chart-4)]",
22
+ "bg-[var(--chart-5)]",
23
+ "bg-[var(--chart-6)]",
24
+ "bg-[var(--chart-7)]",
25
+ "bg-[var(--chart-8)]",
26
+ "bg-[var(--chart-9)]"
27
+ ];
28
+ function segmentFills(segments) {
29
+ let next = 0;
30
+ return segments.map((seg) => {
31
+ if (seg.className) return seg.className;
32
+ if (seg.tone) return FILL[seg.tone];
33
+ return seg.color ? void 0 : SERIES_FILL[next++ % SERIES_FILL.length];
34
+ });
35
+ }
17
36
  function ProgressBar({
18
37
  value,
19
38
  min = 0,
@@ -25,6 +44,8 @@ function ProgressBar({
25
44
  tone = "brand",
26
45
  size = "md",
27
46
  locale,
47
+ segments,
48
+ legend = false,
28
49
  className,
29
50
  "aria-label": ariaLabel,
30
51
  "aria-labelledby": ariaLabelledBy,
@@ -33,13 +54,25 @@ function ProgressBar({
33
54
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
34
55
  const kitLocale = useKitLocale(locale);
35
56
  const labelId = useId();
36
- const meter = variant === "meter";
37
- const current = value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value;
38
- const indeterminate = current === void 0;
57
+ const stacked = segments !== void 0;
58
+ const meter = stacked || variant === "meter";
39
59
  const span = max - min;
60
+ const partValue = (v) => Number.isFinite(v) ? Math.max(0, v - min) : 0;
61
+ const fills = stacked ? segmentFills(segments) : [];
62
+ const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : void 0;
63
+ const format = (v) => formatValue ? formatValue(v, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
64
+ span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span
65
+ );
66
+ const partTexts = stacked ? segments.map((seg) => {
67
+ const text = format(min + partValue(seg.value));
68
+ return typeof seg.label === "string" || typeof seg.label === "number" ? common.fieldValue(String(seg.label), text) : text;
69
+ }) : [];
70
+ const current = partTotal ?? (value === void 0 || !Number.isFinite(value) ? meter ? min : void 0 : value);
71
+ const indeterminate = current === void 0;
40
72
  const clamped = indeterminate ? void 0 : Math.min(max, Math.max(min, current));
41
73
  const fraction = clamped === void 0 || span <= 0 ? 0 : (clamped - min) / span;
42
- const valueText = clamped === void 0 ? void 0 : formatValue ? formatValue(clamped, max, min) : new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(fraction);
74
+ const valueText = clamped === void 0 ? void 0 : format(clamped);
75
+ const ariaValueText = stacked && partTexts.length > 0 ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts) : valueText;
43
76
  const labelledBy = ariaLabelledBy ?? (label != null ? labelId : void 0);
44
77
  const name = labelledBy ? void 0 : ariaLabel ?? (indeterminate ? common.loading : void 0);
45
78
  return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), "data-state": indeterminate ? "indeterminate" : "determinate", children: [
@@ -59,10 +92,28 @@ function ProgressBar({
59
92
  "aria-valuemin": indeterminate ? void 0 : min,
60
93
  "aria-valuemax": indeterminate ? void 0 : max,
61
94
  "aria-valuenow": clamped,
62
- "aria-valuetext": valueText,
95
+ "aria-valuetext": ariaValueText,
63
96
  "aria-busy": !meter && indeterminate ? true : void 0,
64
97
  className: cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size]),
65
- children: indeterminate ? /* @__PURE__ */ jsx(
98
+ children: stacked ? (
99
+ // The parts in a flex row with a 2px gap, so two neighbouring steps of one
100
+ // hue are parted by the track rather than by their difference in lightness.
101
+ /* @__PURE__ */ jsx("div", { className: "flex h-full w-full gap-0.5", children: segments.map((seg, i) => /* @__PURE__ */ jsx(
102
+ "div",
103
+ {
104
+ "data-part": "segment",
105
+ className: cn(
106
+ "h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
107
+ fills[i]
108
+ ),
109
+ style: {
110
+ width: `${span <= 0 ? 0 : partValue(seg.value) / span * 100}%`,
111
+ backgroundColor: seg.className ? void 0 : seg.color
112
+ }
113
+ },
114
+ seg.key ?? i
115
+ )) })
116
+ ) : indeterminate ? /* @__PURE__ */ jsx(
66
117
  "div",
67
118
  {
68
119
  "data-part": "fill",
@@ -87,7 +138,22 @@ function ProgressBar({
87
138
  }
88
139
  )
89
140
  }
90
- )
141
+ ),
142
+ stacked && legend && segments.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-1 text-sm", "data-part": "legend", children: segments.map((seg, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2", children: [
143
+ /* @__PURE__ */ jsx(
144
+ "span",
145
+ {
146
+ "aria-hidden": true,
147
+ className: cn(
148
+ "size-2.5 shrink-0 rounded-sm",
149
+ fills[i]
150
+ ),
151
+ style: { backgroundColor: seg.className ? void 0 : seg.color }
152
+ }
153
+ ),
154
+ seg.label != null && /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[var(--text-secondary)]", children: seg.label }),
155
+ /* @__PURE__ */ jsx("span", { className: "ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]", children: format(min + partValue(seg.value)) })
156
+ ] }, seg.key ?? i)) })
91
157
  ] });
92
158
  }
93
159
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const meter = variant === \"meter\";\n const current = value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value;\n const indeterminate = current === undefined;\n const span = max - min;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText =\n clamped === undefined\n ? undefined\n : formatValue\n ? formatValue(clamped, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(fraction);\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={valueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";AAkIQ,SAEI,KAFJ;AAlIR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,oBAAoB;AA2BlE,MAAM,OAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,MAAM,eAAgD,EAAE,IAAI,OAAO,MAAM,SAAS,IAAI,OAAO,IAAI,MAAM;AAiDhG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,mBAAmB;AAAA,EACnB,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,aAAa,MAAM;AACrC,QAAM,UAAU,MAAM;AAEtB,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,IAAK,QAAQ,MAAM,SAAa;AAC7F,QAAM,gBAAgB,YAAY;AAClC,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,gBAAgB,SAAY,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,OAAO,CAAC;AAChF,QAAM,WAAW,YAAY,UAAa,QAAQ,IAAI,KAAK,UAAU,OAAO;AAE5E,QAAM,YACJ,YAAY,SACR,SACA,cACE,YAAY,SAAS,KAAK,GAAG,IAC7B,IAAI,KAAK,aAAa,WAAW,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC,EAAE,OAAO,QAAQ;AAExG,QAAM,aAAa,mBAAmB,SAAS,OAAO,UAAU;AAChE,QAAM,OAAO,aAAa,SAAa,cAAc,gBAAgB,OAAO,UAAU;AAEtF,SACE,qBAAC,SAAI,WAAW,GAAG,WAAW,SAAS,GAAG,cAAY,gBAAgB,kBAAkB,eACpF;AAAA,cAAS,QAAS,aAAa,cAAc,WAC7C,qBAAC,SAAI,WAAU,0CACZ;AAAA,eAAS,QACR,oBAAC,UAAK,IAAI,SAAS,WAAU,wCAC1B,iBACH;AAAA,MAED,aAAa,cAAc;AAAA;AAAA,MAG1B,oBAAC,UAAK,eAAW,MAAC,WAAU,0DACzB,qBACH;AAAA,OAEJ;AAAA,IAEF;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,MAAM,QAAQ,UAAU;AAAA,QACxB,cAAY;AAAA,QACZ,mBAAiB;AAAA,QACjB,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe,gBAAgB,SAAY;AAAA,QAC3C,iBAAe;AAAA,QACf,kBAAgB;AAAA,QAChB,aAAW,CAAC,SAAS,gBAAgB,OAAO;AAAA,QAG5C,WAAW,GAAG,sEAAsE,aAAa,IAAI,CAAC;AAAA,QAErG,0BACC;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAMT;AAAA,cACA;AAAA,cACA;AAAA,cACA,KAAK,IAAI;AAAA,YACX;AAAA;AAAA,QACF,IAEA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YAGV,WAAW,GAAG,qFAAqF,KAAK,IAAI,CAAC;AAAA,YAC7G,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI;AAAA;AAAA,QACvC;AAAA;AAAA,IAEJ;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/progress-bar.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the\n * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what\n * came in or went out is money, and painting it `success` / `warning` said \"good\" and\n * \"careful\" about a figure that is neither. keksdose's report meters (food-group-card,\n * drilldown-modal) sit beside amounts that are already in the money colours.\n */\nexport type ProgressBarTone =\n | \"brand\"\n | \"neutral\"\n | \"success\"\n | \"warning\"\n | \"danger\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/**\n * Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a\n * letter because the letters are taken in order — an `xs` at 6px would be THICKER than\n * `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's\n * track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a\n * line of text where 8px outweighs the text.\n */\nexport type ProgressBarSize = \"sm\" | \"slim\" | \"md\" | \"lg\";\n\nconst FILL: Record<ProgressBarTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: \"h-1\", slim: \"h-1.5\", md: \"h-2\", lg: \"h-3\" };\n\n// A segment with no tone of its own takes the next categorical chart colour, so a\n// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.\nconst SERIES_FILL = [\n \"bg-[var(--chart-1)]\",\n \"bg-[var(--chart-2)]\",\n \"bg-[var(--chart-3)]\",\n \"bg-[var(--chart-4)]\",\n \"bg-[var(--chart-5)]\",\n \"bg-[var(--chart-6)]\",\n \"bg-[var(--chart-7)]\",\n \"bg-[var(--chart-8)]\",\n \"bg-[var(--chart-9)]\",\n];\n\n/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)\n * the next chart colour. \"Next\" counts the UNCOLOURED segments only — a segment with\n * its own `className`/`tone`/`color` does not use up `--chart-1`, so an \"Other\" in grey\n * ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */\nfunction segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {\n let next = 0;\n return segments.map((seg) => {\n if (seg.className) return seg.className;\n if (seg.tone) return FILL[seg.tone];\n return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];\n });\n}\n\n/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */\nexport interface ProgressBarSegment {\n /** Its size, on the bar's own `min`–`max` scale. */\n value: number;\n /** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */\n tone?: ProgressBarTone;\n /** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */\n color?: string;\n /** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */\n className?: string;\n /** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */\n label?: ReactNode;\n /** A stable React key, when `label` is not a string. */\n key?: string;\n}\n\nexport interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way\n * and nobody knows how much is left (an import that reports no count). */\n value?: number;\n min?: number;\n max?: number;\n /**\n * `progress` (default): a task moving toward completion — `role=\"progressbar\"`.\n * `meter`: a share of a known whole that is not going anywhere (budget used, disk\n * space, one category's slice of spending) — `role=\"meter\"`. keksdose's share bars\n * were progress bars to a screen reader, which reads \"busy\" into a figure that is\n * simply a figure. A meter is never indeterminate; a missing value reads as `min`.\n */\n variant?: \"progress\" | \"meter\";\n /** Visible label above the bar, and its accessible name. Without it, pass\n * `aria-label` (an indeterminate bar with neither is named `common.loading`). */\n label?: ReactNode;\n /** Show the value text at the end of the label row. */\n showValue?: boolean;\n /**\n * The value as words — both the visible `showValue` text and `aria-valuetext`.\n * Default: the fraction as a percentage in the kit's locale (\"42 %\" in French).\n * Pass one for anything that is not a percentage: `(v, max) => \\`${v} of ${max} files\\``.\n */\n formatValue?: (value: number, max: number, min: number) => string;\n tone?: ProgressBarTone;\n size?: ProgressBarSize;\n locale?: string;\n /**\n * Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap\n * between them — keksdose's discretionary-spending bar (cut-card:133), a whole\n * split into the steps of a scale. Forces `variant=\"meter\"`: parts of a whole are\n * never \"busy\". `aria-valuenow` is the sum; `aria-valuetext` names every part\n * (\"Rent: 40%, Food: 25%\"), each part's value through `formatValue` and joined in the\n * kit's locale — the words keksdose's hand-drawn bar had to hide from a reader\n * altogether, leaving the legend as the only way in. The sum is clamped to `max`,\n * and parts past it are cut off at the track's end.\n */\n segments?: ProgressBarSegment[];\n /** With `segments`: a list under the bar naming each part with its swatch and value\n * — the table view of the bar, in text tokens rather than the segment colour. */\n legend?: boolean;\n}\n\n/**\n * A horizontal bar that fills toward `max`.\n *\n * keksdose (import progress, budget share bars) and kastlan (upload and job\n * progress) each drew their own div-in-a-div. None had a role, so a reader heard\n * nothing at all; the share bars that did have one claimed to be progress.\n *\n * `aria-valuetext` is always set when there is a value: a bare `aria-valuenow=\"37\"`\n * is read as \"37\" by some readers and \"37 percent\" by others, and neither is right\n * when `max` is 212 files.\n *\n * The indeterminate sweep stops under `prefers-reduced-motion`. It becomes a still,\n * half-strength bar across the whole track — still visibly \"something is happening\",\n * with nothing moving. The sweep is a Tailwind arbitrary `animate-[…]` rather than\n * tokens.css's `.animate-indeterminate` class because that one is UNLAYERED, and an\n * unlayered rule beats every layered utility, `motion-reduce:animate-none` included.\n */\nexport function ProgressBar({\n value,\n min = 0,\n max = 100,\n variant = \"progress\",\n label,\n showValue = false,\n formatValue,\n tone = \"brand\",\n size = \"md\",\n locale,\n segments,\n legend = false,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...rest\n}: ProgressBarProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLocale = useKitLocale(locale);\n const labelId = useId();\n\n const stacked = segments !== undefined;\n const meter = stacked || variant === \"meter\";\n const span = max - min;\n const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);\n const fills = stacked ? segmentFills(segments) : [];\n const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;\n const format = (v: number) =>\n formatValue\n ? formatValue(v, max, min)\n : new Intl.NumberFormat(kitLocale, { style: \"percent\", maximumFractionDigits: 0 }).format(\n span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,\n );\n const partTexts = stacked\n ? segments.map((seg) => {\n const text = format(min + partValue(seg.value));\n return typeof seg.label === \"string\" || typeof seg.label === \"number\"\n ? common.fieldValue(String(seg.label), text)\n : text;\n })\n : [];\n const current =\n partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);\n const indeterminate = current === undefined;\n const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));\n const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;\n\n const valueText = clamped === undefined ? undefined : format(clamped);\n const ariaValueText =\n stacked && partTexts.length > 0\n ? new Intl.ListFormat(kitLocale, { style: \"short\", type: \"unit\" }).format(partTexts)\n : valueText;\n\n const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);\n const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));\n\n return (\n <div className={cn(\"min-w-0\", className)} data-state={indeterminate ? \"indeterminate\" : \"determinate\"}>\n {(label != null || (showValue && valueText !== undefined)) && (\n <div className=\"mb-1 flex items-baseline gap-2 text-sm\">\n {label != null && (\n <span id={labelId} className=\"min-w-0 text-[var(--text-secondary)]\">\n {label}\n </span>\n )}\n {showValue && valueText !== undefined && (\n // Hidden: the same words are the bar's aria-valuetext, and reading them\n // twice in a row is noise.\n <span aria-hidden className=\"ms-auto shrink-0 tabular-nums text-[var(--text-muted)]\">\n {valueText}\n </span>\n )}\n </div>\n )}\n <div\n {...rest}\n role={meter ? \"meter\" : \"progressbar\"}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-valuemin={indeterminate ? undefined : min}\n aria-valuemax={indeterminate ? undefined : max}\n aria-valuenow={clamped}\n aria-valuetext={ariaValueText}\n aria-busy={!meter && indeterminate ? true : undefined}\n // `relative` + `overflow-hidden`: the indeterminate segment is positioned in\n // here and must not paint past the rounded ends.\n className={cn(\"relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]\", TRACK_HEIGHT[size])}\n >\n {stacked ? (\n // The parts in a flex row with a 2px gap, so two neighbouring steps of one\n // hue are parted by the track rather than by their difference in lightness.\n <div className=\"flex h-full w-full gap-0.5\">\n {segments.map((seg, i) => (\n <div\n key={seg.key ?? i}\n data-part=\"segment\"\n className={cn(\n \"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none\",\n fills[i],\n )}\n style={{\n width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,\n backgroundColor: seg.className ? undefined : seg.color,\n }}\n />\n ))}\n </div>\n ) : indeterminate ? (\n <div\n data-part=\"fill\"\n className={cn(\n // `left-0`, deliberately physical: the keyframes (tokens.css\n // `indeterminate-sweep`) translate along the physical x axis, and under\n // RTL the same keyframes are run in REVERSE — from the right edge back\n // to the left. A logical `start-0` would park the segment at the right\n // in RTL and the reversed sweep would then start off-screen.\n \"absolute inset-y-0 left-0 w-1/3 rounded-full\",\n \"animate-[indeterminate-sweep_1.4s_ease-in-out_infinite] rtl:[animation-direction:reverse]\",\n \"motion-reduce:w-full motion-reduce:animate-none motion-reduce:opacity-50\",\n FILL[tone],\n )}\n />\n ) : (\n <div\n data-part=\"fill\"\n // Block flow starts at the inline start, so a plain width fills from the\n // right under `dir=\"rtl\"` with nothing extra.\n className={cn(\"h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none\", FILL[tone])}\n style={{ width: `${fraction * 100}%` }}\n />\n )}\n </div>\n {stacked && legend && segments.length > 0 && (\n <ul className=\"mt-2 space-y-1 text-sm\" data-part=\"legend\">\n {segments.map((seg, i) => (\n <li key={seg.key ?? i} className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={cn(\n \"size-2.5 shrink-0 rounded-sm\",\n fills[i],\n )}\n style={{ backgroundColor: seg.className ? undefined : seg.color }}\n />\n {seg.label != null && (\n <span className=\"min-w-0 flex-1 truncate text-[var(--text-secondary)]\">{seg.label}</span>\n )}\n <span className=\"ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]\">\n {format(min + partValue(seg.value))}\n </span>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -8,6 +8,9 @@ interface SeriesChartLabels {
8
8
  empty: string;
9
9
  /** Accessible name of a `ToggleLegend` — a group of switches, one per series. */
10
10
  legend: string;
11
+ /** Accessible name of the group of keyboard stops a clickable chart (`onPointClick`)
12
+ * lays over its bars or periods — one tab stop, arrow keys between them. */
13
+ points: string;
11
14
  }
12
15
  declare const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels;
13
16
 
@@ -3,7 +3,8 @@ const DEFAULT_SERIES_CHART_LABELS = {
3
3
  resetZoom: "Reset zoom",
4
4
  zoomHint: "Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.",
5
5
  empty: "No data",
6
- legend: "Series"
6
+ legend: "Series",
7
+ points: "Chart values"
7
8
  };
8
9
  export {
9
10
  DEFAULT_SERIES_CHART_LABELS
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/series-chart-labels.ts"],"sourcesContent":["// All user-facing strings for the series chart family — `SeriesChart`, its drag-to-zoom\n// layer and `ToggleLegend` — as the `seriesChart` namespace of `<UiKitProvider labels>`,\n// and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, so the label registry (src/i18n) can\n// import the defaults without pulling the chart library in behind them.\n\nexport interface SeriesChartLabels {\n /** The button that puts a zoomed chart back to its full extent. Visible text. */\n resetZoom: string;\n /** Accessible name of the drag surface laid over the plot. It is the only place the\n * gesture is explained, so it says how the drag is read and how to undo it. */\n zoomHint: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of a `ToggleLegend` — a group of switches, one per series. */\n legend: string;\n}\n\nexport const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {\n resetZoom: \"Reset zoom\",\n zoomHint:\n \"Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.\",\n empty: \"No data\",\n legend: \"Series\",\n};\n"],"mappings":";AAmBO,MAAM,8BAAiD;AAAA,EAC5D,WAAW;AAAA,EACX,UACE;AAAA,EACF,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
1
+ {"version":3,"sources":["../../src/components/series-chart-labels.ts"],"sourcesContent":["// All user-facing strings for the series chart family — `SeriesChart`, its drag-to-zoom\n// layer and `ToggleLegend` — as the `seriesChart` namespace of `<UiKitProvider labels>`,\n// and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, so the label registry (src/i18n) can\n// import the defaults without pulling the chart library in behind them.\n\nexport interface SeriesChartLabels {\n /** The button that puts a zoomed chart back to its full extent. Visible text. */\n resetZoom: string;\n /** Accessible name of the drag surface laid over the plot. It is the only place the\n * gesture is explained, so it says how the drag is read and how to undo it. */\n zoomHint: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of a `ToggleLegend` — a group of switches, one per series. */\n legend: string;\n /** Accessible name of the group of keyboard stops a clickable chart (`onPointClick`)\n * lays over its bars or periods — one tab stop, arrow keys between them. */\n points: string;\n}\n\nexport const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {\n resetZoom: \"Reset zoom\",\n zoomHint:\n \"Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.\",\n empty: \"No data\",\n legend: \"Series\",\n points: \"Chart values\",\n};\n"],"mappings":";AAsBO,MAAM,8BAAiD;AAAA,EAC5D,WAAW;AAAA,EACX,UACE;AAAA,EACF,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AACV;","names":[]}
@@ -75,6 +75,18 @@ interface SeriesChartSeries {
75
75
  activeDot?: boolean | {
76
76
  r?: number;
77
77
  };
78
+ /**
79
+ * The shortest a bar of this series is drawn, in px — recharts' `minPointSize`. A
80
+ * zero or a value too small to see keeps a visible stub, so an empty slot still reads
81
+ * as a slot with a value rather than a gap: keksdose's weekday pattern, where a
82
+ * Sunday with no spending drew a 2% stub by hand. Only the DRAWING changes: the axis
83
+ * is fitted to the values, and the tooltip, the keyboard stops and every accessible
84
+ * name still say 0. A hole (no value in the row) stays a hole. Overrides the chart's
85
+ * {@link SeriesChartProps.minBarLength}; `0` turns it off for this series. Meant for
86
+ * unstacked bars — in a `stack` the stub is drawn over the layer above it. Lines and
87
+ * areas ignore it.
88
+ */
89
+ minBarLength?: number;
78
90
  }
79
91
  /** What a series draws. See {@link SeriesChartSeries.type}. */
80
92
  type SeriesChartType = "line" | "bar" | "area";
@@ -144,6 +156,22 @@ interface SeriesChartAxisShape {
144
156
  * a hairline). A pinned `domain` is left alone either way.
145
157
  */
146
158
  includeZero?: boolean;
159
+ /**
160
+ * Tick only on whole numbers — the y counterpart of `SeriesChartX.integerTicks`, and
161
+ * what keksdose's user-count axis (`metrics-panel.tsx`, `wholeTicks`) and its runway
162
+ * in days (`cash-buffer-chart.tsx`, `wholeDayTicks`) each wrote by hand: a band of
163
+ * [0, 3.1] users is ticked 0 1 2 3, not 0 0.5 1 … 3, and a short runway is not ticked
164
+ * 0 / 0.5 / 1 under a rounding formatter that prints "1" twice. A (zoom) window that
165
+ * holds no whole number falls back to the ordinary ticks rather than to none.
166
+ *
167
+ * AUTOMATIC by default — on when every value this axis carries is a whole number
168
+ * (counts), because that is where a half tick is a value no sample can have, and it
169
+ * changes nothing on a span wider than about eight, which already ticks on whole
170
+ * steps. `true` forces it over fractional data (days of runway computed as 43.7);
171
+ * `false` keeps fractional ticks for a continuous quantity that merely happens to be
172
+ * whole in this sample. `tickValues` wins over either.
173
+ */
174
+ integerTicks?: boolean;
147
175
  }
148
176
  /**
149
177
  * A y axis — and a VISIBLE one says what it measures.
@@ -406,6 +434,10 @@ interface SeriesChartProps {
406
434
  locale?: string;
407
435
  /** Onto the chart's root (`ChartContainer`) — e.g. an app's own grid ink. */
408
436
  className?: string;
437
+ /** The shortest bar drawn, in px, for every bar series that does not set its own
438
+ * {@link SeriesChartSeries.minBarLength}. Drawing only: domain, tooltip and names
439
+ * keep the real value. Default: none — a zero draws nothing. */
440
+ minBarLength?: number;
409
441
  }
410
442
  /** How much of an axis band the rotated title takes, in px. Reserved rather than
411
443
  * shared with the ticks: a title merely offset inside the band prints on top of a
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useMemo } from "react";
3
+ import { useMemo, useRef, useState } from "react";
4
4
  import {
5
5
  Area,
6
6
  Bar,
@@ -10,8 +10,11 @@ import {
10
10
  Line,
11
11
  ReferenceDot,
12
12
  ReferenceLine,
13
+ Rectangle,
13
14
  XAxis,
14
- YAxis
15
+ YAxis,
16
+ usePlotArea,
17
+ useXAxisScale
15
18
  } from "recharts";
16
19
  import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
17
20
  import {
@@ -196,6 +199,77 @@ const TIME_TICK_FORMAT = {
196
199
  const LINE_WIDTH = 2;
197
200
  const STEP_WIDTH = 1.5;
198
201
  const BAR_RADIUS = [3, 3, 0, 0];
202
+ function focusedBarShape(index) {
203
+ return function FocusedBar(props) {
204
+ const focused = props.originalDataIndex === index;
205
+ return /* @__PURE__ */ jsx(Rectangle, { ...props, ...focused ? { stroke: "var(--text-primary)", strokeWidth: 2 } : {} });
206
+ };
207
+ }
208
+ function PointKeys({
209
+ stops,
210
+ current,
211
+ label,
212
+ onFocusStop,
213
+ onBlurStops,
214
+ onPick
215
+ }) {
216
+ const plot = usePlotArea();
217
+ const scale = useXAxisScale();
218
+ const refs = useRef([]);
219
+ if (!plot || !scale || plot.width <= 0 || plot.height <= 0) return null;
220
+ const move = (at) => {
221
+ const next = Math.max(0, Math.min(stops.length - 1, at));
222
+ refs.current[next]?.focus();
223
+ };
224
+ const onKeyDown = (at) => (event) => {
225
+ const stop = stops[at];
226
+ const inSlot = (step) => stops[at + step]?.index === stop.index ? at + step : at;
227
+ const target = event.key === "ArrowRight" ? at + 1 : event.key === "ArrowLeft" ? at - 1 : event.key === "ArrowDown" ? inSlot(1) : event.key === "ArrowUp" ? inSlot(-1) : event.key === "Home" ? 0 : event.key === "End" ? stops.length - 1 : void 0;
228
+ if (target !== void 0) {
229
+ event.preventDefault();
230
+ move(target);
231
+ } else if (event.key === "Enter" || event.key === " ") {
232
+ event.preventDefault();
233
+ onPick(stop);
234
+ }
235
+ };
236
+ const left = plot.x;
237
+ const right = plot.x + plot.width;
238
+ return /* @__PURE__ */ jsx("g", { role: "group", "aria-label": label, children: stops.map((stop, at) => {
239
+ const a = finite(scale(stop.from)) ?? left;
240
+ const b = finite(scale(stop.to)) ?? right;
241
+ const x0 = Math.max(left, Math.min(a, b));
242
+ const x1 = Math.min(right, Math.max(a, b));
243
+ return /* @__PURE__ */ jsx(
244
+ "rect",
245
+ {
246
+ ref: (node) => {
247
+ refs.current[at] = node;
248
+ },
249
+ role: "button",
250
+ tabIndex: at === current ? 0 : -1,
251
+ "aria-label": stop.name,
252
+ "data-point-key": stop.key,
253
+ x: x0,
254
+ y: plot.y,
255
+ width: Math.max(0, x1 - x0),
256
+ height: plot.height,
257
+ fill: "transparent",
258
+ pointerEvents: "none",
259
+ strokeWidth: 2,
260
+ className: cn(
261
+ "outline-none focus-visible:fill-[var(--brand)]/10",
262
+ // A bar outlines ITSELF (see `FOCUSED_BAR`); a slot has nothing else to.
263
+ stop.key === void 0 && "focus-visible:stroke-[var(--brand)]"
264
+ ),
265
+ onFocus: () => onFocusStop(at),
266
+ onBlur: onBlurStops,
267
+ onKeyDown: onKeyDown(at)
268
+ },
269
+ `${stop.index}:${stop.key ?? ""}`
270
+ );
271
+ }) });
272
+ }
199
273
  function SeriesPlot({
200
274
  model,
201
275
  xAxis,
@@ -214,11 +288,14 @@ function SeriesPlot({
214
288
  zoom,
215
289
  labels: labelsProp,
216
290
  locale: localeProp,
217
- className
291
+ className,
292
+ minBarLength
218
293
  }) {
219
294
  const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
220
295
  const locale = useKitLocale(localeProp);
221
296
  const number = useDefaultFormat(localeProp);
297
+ const [keyStop, setKeyStop] = useState(0);
298
+ const [keyFocus, setKeyFocus] = useState(void 0);
222
299
  const heightClass = typeof height === "string" ? height : void 0;
223
300
  const heightStyle = typeof height === "number" ? { height } : void 0;
224
301
  if (!model.source.length || !series.length) {
@@ -300,6 +377,12 @@ function SeriesPlot({
300
377
  const anchored = (axis) => axis.includeZero ?? series.some(
301
378
  (entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id && (entry.type ?? "line") !== "line"
302
379
  );
380
+ const wholeOnly = (axis) => axis.integerTicks ?? series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id).every(
381
+ (entry) => plotted.every((row) => {
382
+ const value = finite(row[entry.key]);
383
+ return value === void 0 || Number.isInteger(value);
384
+ })
385
+ );
303
386
  const onAxis = (axisId) => (ref) => (ref.axis ?? DEFAULT_Y_AXIS) === axisId;
304
387
  const fittedY = (axis) => {
305
388
  const own = [
@@ -317,6 +400,38 @@ function SeriesPlot({
317
400
  const row = rows[index];
318
401
  return row ? { index, row, x: xOf(index), key } : void 0;
319
402
  };
403
+ const stops = [];
404
+ if (onPointClick) {
405
+ const valueText = valueFormat ?? number;
406
+ const nameOf = (entry) => typeof entry.label === "string" || typeof entry.label === "number" ? String(entry.label) : entry.key;
407
+ const said = (entry, value) => {
408
+ const name = nameOf(entry);
409
+ return name ? `${name}: ${valueText(value)}` : valueText(value);
410
+ };
411
+ const bars = series.filter((entry) => entry.type === "bar");
412
+ const [low, high] = fittedX ?? [-Infinity, Infinity];
413
+ const placed = plotted.map((row, index) => ({ index, at: model.kind === "category" ? index : finite(row[xKey]) })).filter((slot) => slot.at !== void 0 && slot.at >= low && slot.at <= high).sort((a, b) => a.at - b.at);
414
+ placed.forEach(({ index, at }, k) => {
415
+ const from = k > 0 ? (placed[k - 1].at + at) / 2 : low;
416
+ const to = k < placed.length - 1 ? (placed[k + 1].at + at) / 2 : high;
417
+ const heading = xLabel(at);
418
+ const where = typeof heading === "string" && heading ? heading : xFormat(at);
419
+ const valued = (bars.length ? bars : series).map((entry) => ({ entry, value: finite(rows[index]?.[entry.key]) })).filter((cell) => cell.value !== void 0);
420
+ if (bars.length) {
421
+ for (const { entry, value } of valued) {
422
+ stops.push({ index, key: entry.key, from, to, name: `${where} \u2014 ${said(entry, value)}` });
423
+ }
424
+ } else if (valued.length) {
425
+ stops.push({
426
+ index,
427
+ from,
428
+ to,
429
+ name: `${where} \u2014 ${valued.map(({ entry, value }) => said(entry, value)).join(", ")}`
430
+ });
431
+ }
432
+ });
433
+ }
434
+ const focusedStop = keyFocus === void 0 ? void 0 : stops[keyFocus];
320
435
  const dotFor = (entry) => {
321
436
  const want = entry.dot;
322
437
  if (!want) return false;
@@ -371,6 +486,7 @@ function SeriesPlot({
371
486
  data: plotted,
372
487
  margin,
373
488
  stackOffset: "sign",
489
+ accessibilityLayer: !onPointClick,
374
490
  onClick: onPointClick ? (state) => {
375
491
  const index = Number(state?.activeTooltipIndex ?? state?.activeIndex);
376
492
  const hit = Number.isInteger(index) ? hitAt(index) : void 0;
@@ -413,7 +529,7 @@ function SeriesPlot({
413
529
  yAxisId: axis.id,
414
530
  hide: axis.hide,
415
531
  domain,
416
- ticks: resolveTicks(axis.tickValues, domain, finite) ?? niceTicks(domain),
532
+ ticks: resolveTicks(axis.tickValues, domain, finite) ?? (wholeOnly(axis) ? integerTicks(domain) ?? niceTicks(domain) : niceTicks(domain)),
417
533
  allowDataOverflow: zoomed,
418
534
  orientation: axis.orientation ?? "left",
419
535
  width: axisBandWidth(axis.width, Boolean(axis.title) && !axis.hide),
@@ -441,6 +557,7 @@ function SeriesPlot({
441
557
  {
442
558
  ...tooltip?.allowEscapeViewBox || tooltip?.boundary ? { allowEscapeViewBox: tooltip.allowEscapeViewBox ?? { x: true } } : {},
443
559
  ...tooltip?.boundary ? { offset: 0 } : {},
560
+ defaultIndex: focusedStop?.index,
444
561
  content: /* @__PURE__ */ jsx(
445
562
  ChartTooltipContent,
446
563
  {
@@ -460,6 +577,7 @@ function SeriesPlot({
460
577
  const curve = entry.step ? "stepAfter" : entry.curve ?? "monotone";
461
578
  const width = entry.strokeWidth ?? (entry.step ? STEP_WIDTH : LINE_WIDTH);
462
579
  if (type === "bar") {
580
+ const stub = entry.minBarLength ?? minBarLength;
463
581
  return /* @__PURE__ */ jsx(
464
582
  Bar,
465
583
  {
@@ -469,6 +587,8 @@ function SeriesPlot({
469
587
  fill: color,
470
588
  fillOpacity: entry.fillOpacity,
471
589
  radius: entry.stack === void 0 ? BAR_RADIUS : 0,
590
+ ...stub ? { minPointSize: (_value, index) => finite(plotted[index]?.[entry.key]) === void 0 ? 0 : stub } : {},
591
+ ...focusedStop?.key === entry.key ? { shape: focusedBarShape(focusedStop.index) } : {},
472
592
  onClick: onPointClick ? (_bar, index, event) => {
473
593
  event.stopPropagation();
474
594
  const hit = hitAt(index, entry.key);
@@ -581,6 +701,23 @@ function SeriesPlot({
581
701
  marker.key ?? `marker-${index}`
582
702
  );
583
703
  }),
704
+ stops.length > 0 && /* @__PURE__ */ jsx(
705
+ PointKeys,
706
+ {
707
+ stops,
708
+ current: Math.min(keyStop, stops.length - 1),
709
+ label: labels.points,
710
+ onFocusStop: (at) => {
711
+ setKeyStop(at);
712
+ setKeyFocus(at);
713
+ },
714
+ onBlurStops: () => setKeyFocus(void 0),
715
+ onPick: (stop) => {
716
+ const hit = hitAt(stop.index, stop.key);
717
+ if (hit) onPointClick?.(hit);
718
+ }
719
+ }
720
+ ),
584
721
  zoom?.layer
585
722
  ]
586
723
  }