@seekalfred/primitives 0.7.1 → 0.7.3

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 (101) hide show
  1. package/README.md +195 -4
  2. package/dist/alert/alert.d.ts +49 -4
  3. package/dist/alert/alert.js +66 -38
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert/index.d.ts +2 -2
  6. package/dist/alert/index.js +2 -2
  7. package/dist/alert-dialog/alert-dialog.d.ts +17 -4
  8. package/dist/alert-dialog/alert-dialog.js +26 -2
  9. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  10. package/dist/badge/badge.d.ts +1 -1
  11. package/dist/button/button.d.ts +2 -2
  12. package/dist/calendar/calendar.d.ts +1 -1
  13. package/dist/card/card.d.ts +22 -1
  14. package/dist/card/card.js +60 -7
  15. package/dist/card/card.js.map +1 -1
  16. package/dist/carousel/carousel.d.ts +21 -0
  17. package/dist/carousel/carousel.js +34 -8
  18. package/dist/carousel/carousel.js.map +1 -1
  19. package/dist/copy-button/copy-button.d.ts +18 -2
  20. package/dist/copy-button/copy-button.js +29 -8
  21. package/dist/copy-button/copy-button.js.map +1 -1
  22. package/dist/donut-chart/donut-chart.d.ts +22 -0
  23. package/dist/donut-chart/donut-chart.js +35 -18
  24. package/dist/donut-chart/donut-chart.js.map +1 -1
  25. package/dist/donut-chart/index.d.ts +2 -2
  26. package/dist/empty-state/empty-state.d.ts +21 -0
  27. package/dist/empty-state/empty-state.js +16 -4
  28. package/dist/empty-state/empty-state.js.map +1 -1
  29. package/dist/empty-state/index.d.ts +2 -2
  30. package/dist/empty-state/index.js +2 -2
  31. package/dist/filter-bar/filter-bar.d.ts +4 -1
  32. package/dist/filter-bar/filter-bar.js +53 -9
  33. package/dist/filter-bar/filter-bar.js.map +1 -1
  34. package/dist/index.d.ts +10 -10
  35. package/dist/index.js +7 -7
  36. package/dist/input/input.js +4 -4
  37. package/dist/input/input.js.map +1 -1
  38. package/dist/lib/field-shell.js +10 -3
  39. package/dist/lib/field-shell.js.map +1 -1
  40. package/dist/lib/field-surface.js +22 -1
  41. package/dist/lib/field-surface.js.map +1 -1
  42. package/dist/lib/use-copy-feedback.js +17 -3
  43. package/dist/lib/use-copy-feedback.js.map +1 -1
  44. package/dist/line-chart/index.d.ts +2 -2
  45. package/dist/line-chart/line-chart.d.ts +42 -2
  46. package/dist/line-chart/line-chart.js +143 -14
  47. package/dist/line-chart/line-chart.js.map +1 -1
  48. package/dist/money-input/money-input.d.ts +22 -1
  49. package/dist/money-input/money-input.js +13 -7
  50. package/dist/money-input/money-input.js.map +1 -1
  51. package/dist/morph-select/morph-select.d.ts +19 -1
  52. package/dist/morph-select/morph-select.js +21 -2
  53. package/dist/morph-select/morph-select.js.map +1 -1
  54. package/dist/multi-select/index.d.ts +2 -2
  55. package/dist/multi-select/index.js +2 -2
  56. package/dist/multi-select/multi-select.d.ts +43 -2
  57. package/dist/multi-select/multi-select.js +93 -36
  58. package/dist/multi-select/multi-select.js.map +1 -1
  59. package/dist/password-field/password-field.d.ts +16 -0
  60. package/dist/password-field/password-field.js +37 -8
  61. package/dist/password-field/password-field.js.map +1 -1
  62. package/dist/popover/index.d.ts +2 -2
  63. package/dist/popover/index.js +2 -2
  64. package/dist/popover/popover.d.ts +39 -2
  65. package/dist/popover/popover.js +29 -2
  66. package/dist/popover/popover.js.map +1 -1
  67. package/dist/progress/progress.d.ts +16 -2
  68. package/dist/progress/progress.js +15 -8
  69. package/dist/progress/progress.js.map +1 -1
  70. package/dist/radio-group/index.d.ts +2 -2
  71. package/dist/radio-group/index.js +2 -2
  72. package/dist/radio-group/radio-group.d.ts +33 -3
  73. package/dist/radio-group/radio-group.js +109 -9
  74. package/dist/radio-group/radio-group.js.map +1 -1
  75. package/dist/scroll-area/scroll-area.js +4 -3
  76. package/dist/scroll-area/scroll-area.js.map +1 -1
  77. package/dist/search-field/search-field.d.ts +22 -1
  78. package/dist/search-field/search-field.js +22 -7
  79. package/dist/search-field/search-field.js.map +1 -1
  80. package/dist/select/select.d.ts +1 -1
  81. package/dist/select/select.js +8 -6
  82. package/dist/select/select.js.map +1 -1
  83. package/dist/separator/separator.d.ts +21 -9
  84. package/dist/separator/separator.js +33 -11
  85. package/dist/separator/separator.js.map +1 -1
  86. package/dist/stat-card/index.d.ts +2 -2
  87. package/dist/stat-card/index.js +2 -2
  88. package/dist/stat-card/stat-card.d.ts +34 -1
  89. package/dist/stat-card/stat-card.js +51 -21
  90. package/dist/stat-card/stat-card.js.map +1 -1
  91. package/dist/styles.css +1 -1
  92. package/dist/table/table.d.ts +1 -1
  93. package/dist/time-picker/time-picker.d.ts +36 -1
  94. package/dist/time-picker/time-picker.js +33 -5
  95. package/dist/time-picker/time-picker.js.map +1 -1
  96. package/dist/toast/toast.d.ts +2 -2
  97. package/dist/usage-meter/index.d.ts +2 -2
  98. package/dist/usage-meter/usage-meter.d.ts +30 -0
  99. package/dist/usage-meter/usage-meter.js +56 -38
  100. package/dist/usage-meter/usage-meter.js.map +1 -1
  101. package/package.json +3 -3
@@ -119,7 +119,7 @@ export declare const tableCellVariants: (props?: ({
119
119
  * marks where the frozen column ends.
120
120
  */
121
121
  export declare const tableStickyCellVariants: (props?: ({
122
- part?: "header" | "body" | "bare" | null | undefined;
122
+ part?: "body" | "header" | "bare" | null | undefined;
123
123
  striped?: boolean | null | undefined;
124
124
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
125
125
  /**
@@ -1,3 +1,4 @@
1
+ import { FocusEvent, ReactNode } from "react";
1
2
  import { TimeValue } from "react-aria-components";
2
3
  //#region src/time-picker/time-picker.d.ts
3
4
  /**
@@ -26,7 +27,12 @@ export interface TimePickerProps {
26
27
  description?: string;
27
28
  /** Shown while no time is chosen. @default "Select a time" */
28
29
  placeholder?: string;
29
- /** The chosen time. Controlled: pass `null` for none. */
30
+ /**
31
+ * The chosen time. Controlled: pass `null` for none. `undefined` means
32
+ * UNCONTROLLED, so going from a time to `undefined` keeps the last time on
33
+ * screen: to clear a controlled picker (a form reset) pass `null`, for example
34
+ * `value={field.value ?? null}`.
35
+ */
30
36
  value?: TimeValue | null;
31
37
  /** The starting time when uncontrolled. @default null */
32
38
  defaultValue?: TimeValue | null;
@@ -40,6 +46,29 @@ export interface TimePickerProps {
40
46
  size?: "sm" | "md" | "lg";
41
47
  /** Disable the control. */
42
48
  isDisabled?: boolean;
49
+ /**
50
+ * The trigger's `id`, so a `<label htmlFor>` outside the picker (or a form
51
+ * library that focuses a field by id) reaches the real control.
52
+ */
53
+ id?: string;
54
+ /**
55
+ * Submits the time with a native form, as "HH:MM" in 24-hour time (the same
56
+ * string an `<input type="time">` submits). While no time is chosen the field
57
+ * submits an empty string under this name, as an empty `<input type="time">` does.
58
+ */
59
+ name?: string;
60
+ /**
61
+ * Marks the time as wrong: the trigger takes the invalid border. Pair it with
62
+ * `errorMessage`, or the state is shown in colour only.
63
+ */
64
+ isInvalid?: boolean;
65
+ /**
66
+ * Why the time is wrong, under the control. It shows only while `isInvalid`
67
+ * is set, and React Aria names it in the trigger's `aria-describedby`.
68
+ */
69
+ errorMessage?: ReactNode;
70
+ /** Called when focus leaves the trigger, such as react-hook-form's `field.onBlur`. */
71
+ onBlur?: (event: FocusEvent<Element>) => void;
43
72
  /** Extra classes for the wrapper. Merged last. */
44
73
  className?: string;
45
74
  }
@@ -48,6 +77,12 @@ export interface TimePickerProps {
48
77
  *
49
78
  * @example
50
79
  * <TimePicker label="Start time" value={time} onChange={setTime} minuteStep={30} />
80
+ *
81
+ * @example
82
+ * // With react-hook-form's Controller
83
+ * <TimePicker label="Sync time" id={field.name} name={field.name} value={field.value ?? null}
84
+ * onChange={field.onChange} onBlur={field.onBlur}
85
+ * isInvalid={!!fieldState.error} errorMessage={fieldState.error?.message} />
51
86
  */
52
87
  export declare const TimePicker: import("react").ForwardRefExoticComponent<TimePickerProps & import("react").RefAttributes<HTMLButtonElement>>;
53
88
  //#endregion
@@ -3,10 +3,10 @@ import { cn, cnAll } from "../lib/cn.js";
3
3
  import { useIsomorphicLayoutEffect } from "../lib/motion.js";
4
4
  import { labelVariants } from "../label/label.js";
5
5
  import { RollingValue } from "../lib/rolling-value.js";
6
- import { selectTriggerVariants, selectVariants } from "../select/select.js";
6
+ import { selectErrorClass, selectTriggerVariants, selectVariants } from "../select/select.js";
7
7
  import { cva } from "class-variance-authority";
8
8
  import { forwardRef, useMemo, useRef, useState } from "react";
9
- import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue, Text, useLocale } from "react-aria-components";
9
+ import { Button, FieldError, Label, ListBox, ListBoxItem, Popover, Select, SelectValue, Text, useLocale } from "react-aria-components";
10
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { Time } from "@internationalized/date";
12
12
  //#region src/time-picker/time-picker.tsx
@@ -31,6 +31,19 @@ const timePickerListVariants = cva([
31
31
  "bg-card text-card-foreground shadow-floating",
32
32
  "box-border font-sans text-sm leading-(--leading-normal)"
33
33
  ]);
34
+ /**
35
+ * Keyboard focus on an INVALID trigger. The shared field surface shows focus by
36
+ * darkening the border, but an invalid field keeps its red border while focused,
37
+ * so a keyboard user tabbing onto an invalid picker saw no change at all
38
+ * (WCAG 2.4.7). The red border stays (it is the state) and the focus ring is
39
+ * added outside it. Only while invalid: a valid trigger is unchanged.
40
+ */
41
+ const timePickerInvalidFocusClass = [
42
+ "group-data-[invalid]:data-[focus-visible]:outline-solid",
43
+ "group-data-[invalid]:data-[focus-visible]:outline-(length:--focus-ring-width)",
44
+ "group-data-[invalid]:data-[focus-visible]:outline-offset-(--focus-ring-offset)",
45
+ "group-data-[invalid]:data-[focus-visible]:outline-(--color-ring)"
46
+ ].join(" ");
34
47
  /** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */
35
48
  const timePickerItemVariants = cva([
36
49
  "relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2",
@@ -102,8 +115,14 @@ function CenteredListBox({ children }) {
102
115
  *
103
116
  * @example
104
117
  * <TimePicker label="Start time" value={time} onChange={setTime} minuteStep={30} />
118
+ *
119
+ * @example
120
+ * // With react-hook-form's Controller
121
+ * <TimePicker label="Sync time" id={field.name} name={field.name} value={field.value ?? null}
122
+ * onChange={field.onChange} onBlur={field.onBlur}
123
+ * isInvalid={!!fieldState.error} errorMessage={fieldState.error?.message} />
105
124
  */
106
- const TimePicker = forwardRef(function TimePicker({ label, labelHidden, description, placeholder = "Select a time", value, defaultValue = null, onChange, minuteStep = 15, hourCycle = 12, size = "md", isDisabled, className }, ref) {
125
+ const TimePicker = forwardRef(function TimePicker({ label, labelHidden, description, placeholder = "Select a time", value, defaultValue = null, onChange, minuteStep = 15, hourCycle = 12, size = "md", isDisabled, id, name, isInvalid, errorMessage, onBlur, className }, ref) {
107
126
  const { locale } = useLocale();
108
127
  const [inner, setInner] = useState(defaultValue);
109
128
  const current = value === void 0 ? inner : value;
@@ -140,6 +159,10 @@ const TimePicker = forwardRef(function TimePicker({ label, labelHidden, descript
140
159
  onChange?.(time);
141
160
  },
142
161
  isDisabled,
162
+ id,
163
+ name,
164
+ isInvalid,
165
+ onBlur,
143
166
  placeholder,
144
167
  className: cn(selectVariants(), className),
145
168
  children: [
@@ -151,7 +174,7 @@ const TimePicker = forwardRef(function TimePicker({ label, labelHidden, descript
151
174
  /* @__PURE__ */ jsxs(Button, {
152
175
  ref,
153
176
  "data-slot": "time-picker-trigger",
154
- className: selectTriggerVariants({ size }),
177
+ className: cnAll(selectTriggerVariants({ size }), timePickerInvalidFocusClass),
155
178
  children: [
156
179
  /* @__PURE__ */ jsx(ClockGlyph, {}),
157
180
  /* @__PURE__ */ jsx(RollingValue, {
@@ -177,6 +200,11 @@ const TimePicker = forwardRef(function TimePicker({ label, labelHidden, descript
177
200
  className: descriptionClass,
178
201
  children: description
179
202
  }) : null,
203
+ errorMessage == null ? null : /* @__PURE__ */ jsx(FieldError, {
204
+ "data-slot": "time-picker-error",
205
+ className: selectErrorClass,
206
+ children: errorMessage
207
+ }),
180
208
  /* @__PURE__ */ jsx(Popover, {
181
209
  "data-slot": "time-picker-popover",
182
210
  className: timePickerListVariants(),
@@ -196,6 +224,6 @@ const TimePicker = forwardRef(function TimePicker({ label, labelHidden, descript
196
224
  });
197
225
  });
198
226
  //#endregion
199
- export { TimePicker, timePickerItemVariants, timePickerListVariants };
227
+ export { TimePicker, timePickerInvalidFocusClass, timePickerItemVariants, timePickerListVariants };
200
228
 
201
229
  //# sourceMappingURL=time-picker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"time-picker.js","names":["AriaListBox","AriaSelect","AriaLabel","AriaButton","AriaSelectValue","AriaText","AriaPopover","AriaListBoxItem"],"sources":["../../src/time-picker/time-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { Time } from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useMemo, useRef, useState } from \"react\";\nimport {\n Button as AriaButton,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectValue as AriaSelectValue,\n Text as AriaText,\n useLocale,\n type TimeValue,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\nimport { selectTriggerVariants, selectVariants } from \"../select/select.js\";\n\n/**\n * TimePicker, a field that shows a time and opens a list of times to pick from.\n *\n * Values are `TimeValue` objects from `@internationalized/date`, as Calendar and\n * DatePicker use `DateValue` (ADR 0006), not \"HH:MM\" strings: a time of day has\n * no time zone or format of its own. Unlike those two, this one builds a `Time`\n * at runtime to hand back, so the package is a real (peer) dependency here and\n * not only a type.\n *\n * The control is a React Aria `Select`, so the list is a listbox with typeahead,\n * arrow keys and a roving highlight; the motion (the value rolling like a clock\n * face, the chevron, the list growing from the field) sits on top of it.\n */\n\n/** The list: it grows from the field, and the highlight follows the pointer and the arrows. */\nexport const timePickerListVariants = cva([\n \"picker-pop w-(--trigger-width) origin-top overflow-hidden\",\n // A little less offset than a date picker: this list sits close under its field.\n \"[--pop-in-y:-6px] [--pop-in-scale:0.97] [--pop-out-y:-4px] [--pop-out-scale:0.98] [--pop-out-duration:120ms]\",\n \"rounded-panel border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\n/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */\nexport const timePickerItemVariants = cva([\n \"relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2\",\n \"rounded-control px-3 py-1.5 tabular-nums outline-none\",\n \"text-sm leading-(--leading-normal) text-card-foreground\",\n \"data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-80 motion-safe:ease-settle\",\n \"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\nconst descriptionClass = \"text-sm leading-(--leading-normal) text-muted-foreground\";\n\nconst pad = (value: number) => String(value).padStart(2, \"0\");\nconst keyOf = (minutes: number) => `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;\nconst minutesOf = (value: TimeValue) => value.hour * 60 + value.minute;\n\nfunction ClockGlyph() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground\"\n >\n <circle cx=\"8\" cy=\"8\" r=\"5.5\" />\n <path d=\"M8 5v3l2 1.5\" />\n </svg>\n );\n}\n\n/** The chevron turns half a turn while the list is open, on the gentle spring. */\nfunction Chevron() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n );\n}\n\n/**\n * The list of times. It opens centred on the chosen time (or at the top with\n * none), before the first frame, so there is no scroll to watch; the arrow keys\n * then keep the highlight in view, which React Aria does.\n */\nfunction CenteredListBox({ children }: { children: React.ReactNode }) {\n const ref = useRef<HTMLDivElement>(null);\n useIsomorphicLayoutEffect(() => {\n const list = ref.current;\n const chosen = list?.querySelector<HTMLElement>('[role=\"option\"][aria-selected=\"true\"]');\n if (!list || !chosen) return;\n list.scrollTop = chosen.offsetTop - (list.clientHeight - chosen.offsetHeight) / 2;\n }, []);\n return (\n <AriaListBox\n ref={ref}\n data-slot=\"time-picker-list\"\n className=\"relative max-h-62.5 overflow-y-auto overscroll-contain outline-none\"\n >\n {children}\n </AriaListBox>\n );\n}\n\nexport interface TimePickerProps {\n /** What is being chosen, and the control's accessible name. Required, as on Select. */\n label: string;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Help text under the control, named in `aria-describedby`. */\n description?: string;\n /** Shown while no time is chosen. @default \"Select a time\" */\n placeholder?: string;\n /** The chosen time. Controlled: pass `null` for none. */\n value?: TimeValue | null;\n /** The starting time when uncontrolled. @default null */\n defaultValue?: TimeValue | null;\n /** Called with the chosen time, as a `Time`. */\n onChange?: (value: TimeValue) => void;\n /** Minutes between the times in the list. @default 15 */\n minuteStep?: 1 | 5 | 10 | 15 | 30;\n /** 12-hour (9:00 AM) or 24-hour (09:00) times. @default 12 */\n hourCycle?: 12 | 24;\n /** Control scale of the trigger. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disable the control. */\n isDisabled?: boolean;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * A time picker.\n *\n * @example\n * <TimePicker label=\"Start time\" value={time} onChange={setTime} minuteStep={30} />\n */\nexport const TimePicker = forwardRef<HTMLButtonElement, TimePickerProps>(function TimePicker(\n {\n label,\n labelHidden,\n description,\n placeholder = \"Select a time\",\n value,\n defaultValue = null,\n onChange,\n minuteStep = 15,\n hourCycle = 12,\n size = \"md\",\n isDisabled,\n className,\n },\n ref,\n) {\n const { locale } = useLocale();\n const [inner, setInner] = useState<TimeValue | null>(defaultValue);\n const current = value === undefined ? inner : value;\n const minutes = current ? minutesOf(current) : null;\n const key = minutes === null ? null : keyOf(minutes);\n\n const display = useMemo(() => {\n const format = new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hourCycle: hourCycle === 24 ? \"h23\" : \"h12\",\n timeZone: \"UTC\",\n });\n return (total: number) =>\n format\n .format(new Date(Date.UTC(2000, 0, 1, Math.floor(total / 60), total % 60)))\n .replace(/\\u202f/g, \" \");\n }, [locale, hourCycle]);\n\n // The list: every step of the day, plus the current time if it falls between steps.\n const options = useMemo(() => {\n const all = Array.from({ length: Math.ceil(1440 / minuteStep) }, (_, i) => i * minuteStep);\n if (minutes !== null && !all.includes(minutes)) all.push(minutes);\n return all.sort((a, b) => a - b);\n }, [minuteStep, minutes]);\n\n // Which way the time moved, so the shown value rolls like a clock face.\n const [previous, setPrevious] = useState(minutes);\n const [dir, setDir] = useState(1);\n if (previous !== minutes) {\n setPrevious(minutes);\n setDir(minutes !== null && previous !== null && minutes < previous ? -1 : 1);\n }\n\n return (\n <AriaSelect\n data-slot=\"time-picker\"\n selectedKey={key}\n onSelectionChange={(next) => {\n if (next == null) return;\n const [hour = 0, minute = 0] = String(next).split(\":\").map(Number);\n const time = new Time(hour, minute);\n if (value === undefined) setInner(time);\n onChange?.(time);\n }}\n isDisabled={isDisabled}\n placeholder={placeholder}\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"time-picker-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <AriaButton ref={ref} data-slot=\"time-picker-trigger\" className={selectTriggerVariants({ size })}>\n <ClockGlyph />\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"time-picker-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={key ?? \"placeholder\"}\n dir={key === null ? -1 : dir}\n >\n {minutes === null ? (\n <span data-slot=\"time-picker-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n display(minutes)\n )}\n </RollingValue>\n <Chevron />\n </AriaButton>\n {description ? (\n <AriaText slot=\"description\" data-slot=\"time-picker-description\" className={descriptionClass}>\n {description}\n </AriaText>\n ) : null}\n <AriaPopover data-slot=\"time-picker-popover\" className={timePickerListVariants()}>\n <CenteredListBox>\n {options.map((total) => (\n <AriaListBoxItem\n key={keyOf(total)}\n id={keyOf(total)}\n textValue={display(total)}\n data-slot=\"time-picker-item\"\n className={timePickerItemVariants()}\n >\n {({ isSelected }) => (\n <>\n {display(total)}\n {isSelected ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"time-picker-dot\"\n className=\"size-1.5 rounded-full bg-foreground\"\n />\n ) : null}\n </>\n )}\n </AriaListBoxItem>\n ))}\n </CenteredListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"time-picker.js","names":["AriaListBox","AriaSelect","AriaLabel","AriaButton","AriaSelectValue","AriaText","AriaFieldError","AriaPopover","AriaListBoxItem"],"sources":["../../src/time-picker/time-picker.tsx"],"sourcesContent":["\"use client\";\n\nimport { Time } from \"@internationalized/date\";\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useMemo, useRef, useState, type FocusEvent, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n FieldError as AriaFieldError,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectValue as AriaSelectValue,\n Text as AriaText,\n useLocale,\n type TimeValue,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { RollingValue } from \"../lib/rolling-value.js\";\nimport { selectErrorClass, selectTriggerVariants, selectVariants } from \"../select/select.js\";\n\n/**\n * TimePicker, a field that shows a time and opens a list of times to pick from.\n *\n * Values are `TimeValue` objects from `@internationalized/date`, as Calendar and\n * DatePicker use `DateValue` (ADR 0006), not \"HH:MM\" strings: a time of day has\n * no time zone or format of its own. Unlike those two, this one builds a `Time`\n * at runtime to hand back, so the package is a real (peer) dependency here and\n * not only a type.\n *\n * The control is a React Aria `Select`, so the list is a listbox with typeahead,\n * arrow keys and a roving highlight; the motion (the value rolling like a clock\n * face, the chevron, the list growing from the field) sits on top of it.\n */\n\n/** The list: it grows from the field, and the highlight follows the pointer and the arrows. */\nexport const timePickerListVariants = cva([\n \"picker-pop w-(--trigger-width) origin-top overflow-hidden\",\n // A little less offset than a date picker: this list sits close under its field.\n \"[--pop-in-y:-6px] [--pop-in-scale:0.97] [--pop-out-y:-4px] [--pop-out-scale:0.98] [--pop-out-duration:120ms]\",\n \"rounded-panel border-(length:--border-width) border-border p-1\",\n \"bg-card text-card-foreground shadow-floating\",\n \"box-border font-sans text-sm leading-(--leading-normal)\",\n]);\n\n/**\n * Keyboard focus on an INVALID trigger. The shared field surface shows focus by\n * darkening the border, but an invalid field keeps its red border while focused,\n * so a keyboard user tabbing onto an invalid picker saw no change at all\n * (WCAG 2.4.7). The red border stays (it is the state) and the focus ring is\n * added outside it. Only while invalid: a valid trigger is unchanged.\n */\nexport const timePickerInvalidFocusClass = [\n \"group-data-[invalid]:data-[focus-visible]:outline-solid\",\n \"group-data-[invalid]:data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"group-data-[invalid]:data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"group-data-[invalid]:data-[focus-visible]:outline-(--color-ring)\",\n].join(\" \");\n\n/** A time. The highlight changes almost at once (the arrows move it often); a dot marks the chosen one. */\nexport const timePickerItemVariants = cva([\n \"relative flex min-h-9 cursor-pointer select-none items-center justify-between gap-2\",\n \"rounded-control px-3 py-1.5 tabular-nums outline-none\",\n \"text-sm leading-(--leading-normal) text-card-foreground\",\n \"data-[focused]:bg-muted\",\n \"motion-safe:transition-colors motion-safe:duration-80 motion-safe:ease-settle\",\n \"data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-(--opacity-disabled)\",\n]);\n\nconst descriptionClass = \"text-sm leading-(--leading-normal) text-muted-foreground\";\n\nconst pad = (value: number) => String(value).padStart(2, \"0\");\nconst keyOf = (minutes: number) => `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;\nconst minutesOf = (value: TimeValue) => value.hour * 60 + value.minute;\n\nfunction ClockGlyph() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground\"\n >\n <circle cx=\"8\" cy=\"8\" r=\"5.5\" />\n <path d=\"M8 5v3l2 1.5\" />\n </svg>\n );\n}\n\n/** The chevron turns half a turn while the list is open, on the gentle spring. */\nfunction Chevron() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n );\n}\n\n/**\n * The list of times. It opens centred on the chosen time (or at the top with\n * none), before the first frame, so there is no scroll to watch; the arrow keys\n * then keep the highlight in view, which React Aria does.\n */\nfunction CenteredListBox({ children }: { children: React.ReactNode }) {\n const ref = useRef<HTMLDivElement>(null);\n useIsomorphicLayoutEffect(() => {\n const list = ref.current;\n const chosen = list?.querySelector<HTMLElement>('[role=\"option\"][aria-selected=\"true\"]');\n if (!list || !chosen) return;\n list.scrollTop = chosen.offsetTop - (list.clientHeight - chosen.offsetHeight) / 2;\n }, []);\n return (\n <AriaListBox\n ref={ref}\n data-slot=\"time-picker-list\"\n className=\"relative max-h-62.5 overflow-y-auto overscroll-contain outline-none\"\n >\n {children}\n </AriaListBox>\n );\n}\n\nexport interface TimePickerProps {\n /** What is being chosen, and the control's accessible name. Required, as on Select. */\n label: string;\n /** Render the label for screen readers only. */\n labelHidden?: boolean;\n /** Help text under the control, named in `aria-describedby`. */\n description?: string;\n /** Shown while no time is chosen. @default \"Select a time\" */\n placeholder?: string;\n /**\n * The chosen time. Controlled: pass `null` for none. `undefined` means\n * UNCONTROLLED, so going from a time to `undefined` keeps the last time on\n * screen: to clear a controlled picker (a form reset) pass `null`, for example\n * `value={field.value ?? null}`.\n */\n value?: TimeValue | null;\n /** The starting time when uncontrolled. @default null */\n defaultValue?: TimeValue | null;\n /** Called with the chosen time, as a `Time`. */\n onChange?: (value: TimeValue) => void;\n /** Minutes between the times in the list. @default 15 */\n minuteStep?: 1 | 5 | 10 | 15 | 30;\n /** 12-hour (9:00 AM) or 24-hour (09:00) times. @default 12 */\n hourCycle?: 12 | 24;\n /** Control scale of the trigger. @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Disable the control. */\n isDisabled?: boolean;\n /**\n * The trigger's `id`, so a `<label htmlFor>` outside the picker (or a form\n * library that focuses a field by id) reaches the real control.\n */\n id?: string;\n /**\n * Submits the time with a native form, as \"HH:MM\" in 24-hour time (the same\n * string an `<input type=\"time\">` submits). While no time is chosen the field\n * submits an empty string under this name, as an empty `<input type=\"time\">` does.\n */\n name?: string;\n /**\n * Marks the time as wrong: the trigger takes the invalid border. Pair it with\n * `errorMessage`, or the state is shown in colour only.\n */\n isInvalid?: boolean;\n /**\n * Why the time is wrong, under the control. It shows only while `isInvalid`\n * is set, and React Aria names it in the trigger's `aria-describedby`.\n */\n errorMessage?: ReactNode;\n /** Called when focus leaves the trigger, such as react-hook-form's `field.onBlur`. */\n onBlur?: (event: FocusEvent<Element>) => void;\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/**\n * A time picker.\n *\n * @example\n * <TimePicker label=\"Start time\" value={time} onChange={setTime} minuteStep={30} />\n *\n * @example\n * // With react-hook-form's Controller\n * <TimePicker label=\"Sync time\" id={field.name} name={field.name} value={field.value ?? null}\n * onChange={field.onChange} onBlur={field.onBlur}\n * isInvalid={!!fieldState.error} errorMessage={fieldState.error?.message} />\n */\nexport const TimePicker = forwardRef<HTMLButtonElement, TimePickerProps>(function TimePicker(\n {\n label,\n labelHidden,\n description,\n placeholder = \"Select a time\",\n value,\n defaultValue = null,\n onChange,\n minuteStep = 15,\n hourCycle = 12,\n size = \"md\",\n isDisabled,\n id,\n name,\n isInvalid,\n errorMessage,\n onBlur,\n className,\n },\n ref,\n) {\n const { locale } = useLocale();\n const [inner, setInner] = useState<TimeValue | null>(defaultValue);\n const current = value === undefined ? inner : value;\n const minutes = current ? minutesOf(current) : null;\n const key = minutes === null ? null : keyOf(minutes);\n\n const display = useMemo(() => {\n const format = new Intl.DateTimeFormat(locale, {\n hour: \"numeric\",\n minute: \"2-digit\",\n hourCycle: hourCycle === 24 ? \"h23\" : \"h12\",\n timeZone: \"UTC\",\n });\n return (total: number) =>\n format\n .format(new Date(Date.UTC(2000, 0, 1, Math.floor(total / 60), total % 60)))\n .replace(/\\u202f/g, \" \");\n }, [locale, hourCycle]);\n\n // The list: every step of the day, plus the current time if it falls between steps.\n const options = useMemo(() => {\n const all = Array.from({ length: Math.ceil(1440 / minuteStep) }, (_, i) => i * minuteStep);\n if (minutes !== null && !all.includes(minutes)) all.push(minutes);\n return all.sort((a, b) => a - b);\n }, [minuteStep, minutes]);\n\n // Which way the time moved, so the shown value rolls like a clock face.\n const [previous, setPrevious] = useState(minutes);\n const [dir, setDir] = useState(1);\n if (previous !== minutes) {\n setPrevious(minutes);\n setDir(minutes !== null && previous !== null && minutes < previous ? -1 : 1);\n }\n\n return (\n <AriaSelect\n data-slot=\"time-picker\"\n selectedKey={key}\n onSelectionChange={(next) => {\n if (next == null) return;\n const [hour = 0, minute = 0] = String(next).split(\":\").map(Number);\n const time = new Time(hour, minute);\n if (value === undefined) setInner(time);\n onChange?.(time);\n }}\n isDisabled={isDisabled}\n id={id}\n name={name}\n isInvalid={isInvalid}\n onBlur={onBlur}\n placeholder={placeholder}\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"time-picker-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <AriaButton\n ref={ref}\n data-slot=\"time-picker-trigger\"\n className={cnAll(selectTriggerVariants({ size }), timePickerInvalidFocusClass)}\n >\n <ClockGlyph />\n <RollingValue\n srValue={\n <AriaSelectValue data-slot=\"time-picker-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n }\n swapKey={key ?? \"placeholder\"}\n dir={key === null ? -1 : dir}\n >\n {minutes === null ? (\n <span data-slot=\"time-picker-placeholder\" className=\"text-muted-foreground\">\n {placeholder}\n </span>\n ) : (\n display(minutes)\n )}\n </RollingValue>\n <Chevron />\n </AriaButton>\n {description ? (\n <AriaText slot=\"description\" data-slot=\"time-picker-description\" className={descriptionClass}>\n {description}\n </AriaText>\n ) : null}\n {errorMessage == null ? null : (\n // Select's pattern: React Aria renders FieldError only while the field is invalid.\n <AriaFieldError data-slot=\"time-picker-error\" className={selectErrorClass}>\n {errorMessage}\n </AriaFieldError>\n )}\n <AriaPopover data-slot=\"time-picker-popover\" className={timePickerListVariants()}>\n <CenteredListBox>\n {options.map((total) => (\n <AriaListBoxItem\n key={keyOf(total)}\n id={keyOf(total)}\n textValue={display(total)}\n data-slot=\"time-picker-item\"\n className={timePickerItemVariants()}\n >\n {({ isSelected }) => (\n <>\n {display(total)}\n {isSelected ? (\n <span\n aria-hidden=\"true\"\n data-slot=\"time-picker-dot\"\n className=\"size-1.5 rounded-full bg-foreground\"\n />\n ) : null}\n </>\n )}\n </AriaListBoxItem>\n ))}\n </CenteredListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAa,yBAAyB,IAAI;CACxC;CAEA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;AASD,MAAa,8BAA8B;CACzC;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;AAGV,MAAa,yBAAyB,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,mBAAmB;AAEzB,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;AAC5D,MAAM,SAAS,YAAoB,GAAG,IAAI,KAAK,MAAM,UAAU,EAAE,CAAC,EAAE,GAAG,IAAI,UAAU,EAAE;AACvF,MAAM,aAAa,UAAqB,MAAM,OAAO,KAAK,MAAM;AAEhE,SAAS,aAAa;CACpB,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;EACd,gBAAe;EACf,WAAU;EARZ,UAAA,CAUE,oBAAC,UAAD;GAAQ,IAAG;GAAI,IAAG;GAAI,GAAE;EAAO,CAAA,GAC/B,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA,CACrB;;AAET;;AAGA,SAAS,UAAU;CACjB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;EACd,gBAAe;EACf,WAAU;EAEV,UAAA,oBAAC,QAAD,EAAM,GAAE,eAAgB,CAAA;CACrB,CAAA;AAET;;;;;;AAOA,SAAS,gBAAgB,EAAE,YAA2C;CACpE,MAAM,MAAM,OAAuB,IAAI;CACvC,gCAAgC;EAC9B,MAAM,OAAO,IAAI;EACjB,MAAM,SAAS,MAAM,cAA2B,2CAAuC;EACvF,IAAI,CAAC,QAAQ,CAAC,QAAQ;EACtB,KAAK,YAAY,OAAO,aAAa,KAAK,eAAe,OAAO,gBAAgB;CAClF,GAAG,CAAC,CAAC;CACL,OACE,oBAACA,SAAD;EACO;EACL,aAAU;EACV,WAAU;EAET;CACU,CAAA;AAEjB;;;;;;;;;;;;;AAqEA,MAAa,aAAa,WAA+C,SAAS,WAChF,EACE,OACA,aACA,aACA,cAAc,iBACd,OACA,eAAe,MACf,UACA,aAAa,IACb,YAAY,IACZ,OAAO,MACP,YACA,IACA,MACA,WACA,cACA,QACA,aAEF,KACA;CACA,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,CAAC,OAAO,YAAY,SAA2B,YAAY;CACjE,MAAM,UAAU,UAAU,KAAA,IAAY,QAAQ;CAC9C,MAAM,UAAU,UAAU,UAAU,OAAO,IAAI;CAC/C,MAAM,MAAM,YAAY,OAAO,OAAO,MAAM,OAAO;CAEnD,MAAM,UAAU,cAAc;EAC5B,MAAM,SAAS,IAAI,KAAK,eAAe,QAAQ;GAC7C,MAAM;GACN,QAAQ;GACR,WAAW,cAAc,KAAK,QAAQ;GACtC,UAAU;EACZ,CAAC;EACD,QAAQ,UACN,OACG,OAAO,IAAI,KAAK,KAAK,IAAI,KAAM,GAAG,GAAG,KAAK,MAAM,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,CAC1E,QAAQ,WAAW,GAAG;CAC7B,GAAG,CAAC,QAAQ,SAAS,CAAC;CAGtB,MAAM,UAAU,cAAc;EAC5B,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,KAAK,KAAK,OAAO,UAAU,EAAE,IAAI,GAAG,MAAM,IAAI,UAAU;EACzF,IAAI,YAAY,QAAQ,CAAC,IAAI,SAAS,OAAO,GAAG,IAAI,KAAK,OAAO;EAChE,OAAO,IAAI,MAAM,GAAG,MAAM,IAAI,CAAC;CACjC,GAAG,CAAC,YAAY,OAAO,CAAC;CAGxB,MAAM,CAAC,UAAU,eAAe,SAAS,OAAO;CAChD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC;CAChC,IAAI,aAAa,SAAS;EACxB,YAAY,OAAO;EACnB,OAAO,YAAY,QAAQ,aAAa,QAAQ,UAAU,WAAW,KAAK,CAAC;CAC7E;CAEA,OACE,qBAACC,QAAD;EACE,aAAU;EACV,aAAa;EACb,oBAAoB,SAAS;GAC3B,IAAI,QAAQ,MAAM;GAClB,MAAM,CAAC,OAAO,GAAG,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;GACjE,MAAM,OAAO,IAAI,KAAK,MAAM,MAAM;GAClC,IAAI,UAAU,KAAA,GAAW,SAAS,IAAI;GACtC,WAAW,IAAI;EACjB;EACY;EACR;EACE;EACK;EACH;EACK;EACb,WAAW,GAAG,eAAe,GAAG,SAAS;EAhB3C,UAAA;GAkBE,oBAACC,OAAD;IACE,aAAU;IACV,WAAW,MAAM,cAAc,GAAG,eAAe,SAAS;IAEzD,UAAA;GACQ,CAAA;GACX,qBAACC,QAAD;IACO;IACL,aAAU;IACV,WAAW,MAAM,sBAAsB,EAAE,KAAK,CAAC,GAAG,2BAA2B;IAH/E,UAAA;KAKE,oBAAC,YAAD,CAAa,CAAA;KACb,oBAAC,cAAD;MACE,SACE,oBAACC,aAAD;OAAiB,aAAU;OAAoB,WAAU;OACrD,WAAA,EAAE,eAAe,mBAAoB,gBAAgB,cAAc;MACtD,CAAA;MAEnB,SAAS,OAAO;MAChB,KAAK,QAAQ,OAAO,KAAK;MAExB,UAAA,YAAY,OACX,oBAAC,QAAD;OAAM,aAAU;OAA0B,WAAU;OACjD,UAAA;MACG,CAAA,IAEN,QAAQ,OAAO;KAEL,CAAA;KACd,oBAAC,SAAD,CAAU,CAAA;IACA;;GACX,cACC,oBAACC,MAAD;IAAU,MAAK;IAAc,aAAU;IAA0B,WAAW;IACzE,UAAA;GACO,CAAA,IACR;GACH,gBAAgB,OAAO,OAEtB,oBAACC,YAAD;IAAgB,aAAU;IAAoB,WAAW;IACtD,UAAA;GACa,CAAA;GAElB,oBAACC,SAAD;IAAa,aAAU;IAAsB,WAAW,uBAAuB;IAC7E,UAAA,oBAAC,iBAAD,EAAA,UACG,QAAQ,KAAK,UACZ,oBAACC,aAAD;KAEE,IAAI,MAAM,KAAK;KACf,WAAW,QAAQ,KAAK;KACxB,aAAU;KACV,WAAW,uBAAuB;KAEhC,WAAA,EAAE,iBACF,qBAAA,YAAA,EAAA,UAAA,CACG,QAAQ,KAAK,GACb,aACC,oBAAC,QAAD;MACE,eAAY;MACZ,aAAU;MACV,WAAU;KACX,CAAA,IACC,IACJ,EAAA,CAAA;IAEW,GAlBV,MAAM,KAAK,CAkBD,CAClB,EACc,CAAA;GACN,CAAA;EACH;;AAEhB,CAAC"}
@@ -34,11 +34,11 @@ import { UNSTABLE_ToastQueue } from "react-aria-components";
34
34
  /** Where the region sits. Fixed, above everything, out of the way of content. */
35
35
  export declare const toastRegionVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
36
36
  export declare const toastVariants: (props?: ({
37
- tone?: "neutral" | "success" | "warning" | "danger" | "info" | null | undefined;
37
+ tone?: "success" | "warning" | "danger" | "neutral" | "info" | null | undefined;
38
38
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
39
39
  /** The status icon's circle: the tone's light fill with its AA text colour. */
40
40
  export declare const toastIconVariants: (props?: ({
41
- tone?: "neutral" | "success" | "warning" | "danger" | "info" | null | undefined;
41
+ tone?: "success" | "warning" | "danger" | "neutral" | "info" | null | undefined;
42
42
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
43
43
  export declare const toastCloseVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
44
44
  /** What a toast carries. `title` is required: an unnamed toast announces nothing. */
@@ -1,2 +1,2 @@
1
- import { UsageMeter, UsageMeterProps, UsageMeterSegment } from "./usage-meter.js";
2
- export { UsageMeter, type UsageMeterProps, type UsageMeterSegment };
1
+ import { UsageMeter, UsageMeterProps, UsageMeterSegment, UsageMeterStatus } from "./usage-meter.js";
2
+ export { UsageMeter, type UsageMeterProps, type UsageMeterSegment, type UsageMeterStatus };
@@ -44,7 +44,37 @@ export interface UsageMeterProps extends Omit<ComponentPropsWithoutRef<"div">, "
44
44
  overLabel?: string;
45
45
  /** Share of the limit at which the meter warns that it is almost full. @default 0.9 */
46
46
  warnAt?: number;
47
+ /**
48
+ * The state as the caller (usually the backend) reads it, overriding the one
49
+ * `warnAt` and the limit would give. `ok` and `warn` look as the meter's own
50
+ * under-limit and almost-full states; `full` is the allowance used up
51
+ * ("Limit reached"); `exhausted` is nothing left to draw on at all ("None
52
+ * left"). Left out, the meter works it out itself.
53
+ *
54
+ * The bar and legend agree with it: under `full` or `exhausted` the free
55
+ * share is shown as none (the bar fills, the free item reads 0), and past
56
+ * the limit `ok` and `warn` give way to the meter's own over state, since
57
+ * the hatch already says over.
58
+ */
59
+ status?: UsageMeterStatus;
60
+ /**
61
+ * The product's words for the status, such as "Credits paused". They replace
62
+ * the badge's words (including "Unlimited"), the status in the bar's name and
63
+ * the announcement; the tone and icon still follow the status.
64
+ */
65
+ statusLabel?: string;
66
+ /**
67
+ * No limit: the bar shows each category's share of what is used, with no
68
+ * free share, limit marker or overflow, and the badge reads "Unlimited"
69
+ * unless `statusLabel` is set. `limit`, `warnAt` and `status` are ignored.
70
+ */
71
+ unlimited?: boolean;
47
72
  }
73
+ /**
74
+ * A status the caller can impose: under the limit, almost full, allowance used
75
+ * up, and nothing left at all.
76
+ */
77
+ export type UsageMeterStatus = "ok" | "warn" | "full" | "exhausted";
48
78
  export declare const UsageMeter: import("react").ForwardRefExoticComponent<UsageMeterProps & import("react").RefAttributes<HTMLDivElement>>;
49
79
  //#endregion
50
80
  //# sourceMappingURL=usage-meter.d.ts.map
@@ -10,6 +10,13 @@ import { useSvgId } from "../lib/svg-id.js";
10
10
  import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
11
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
12
  //#region src/usage-meter/usage-meter.tsx
13
+ /** The imposed status as `data-status` names it: `warn` is the meter's own `near`. */
14
+ const IMPOSED = {
15
+ ok: "ok",
16
+ warn: "near",
17
+ full: "full",
18
+ exhausted: "exhausted"
19
+ };
13
20
  /** A segment stops two pixels short of the next, so neighbours read apart without an outline. */
14
21
  const GAP = 2;
15
22
  /** Stagger of each segment's first growth. */
@@ -62,7 +69,7 @@ function AlertTriangleIcon() {
62
69
  children: [/* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }), /* @__PURE__ */ jsx("path", { d: "M12 9v4M12 17h.01" })]
63
70
  });
64
71
  }
65
- const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit = "", decimals = 1, freeLabel = "Free", overLabel = "Over limit", warnAt = .9, className, ...props }, ref) {
72
+ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit = "", decimals = 1, freeLabel = "Free", overLabel = "Over limit", warnAt = .9, status: imposed, statusLabel, unlimited = false, className, ...props }, ref) {
66
73
  const root = useRef(null);
67
74
  const refs = useMemo(() => mergeRefs(ref, root), [ref]);
68
75
  const bar = useRef(null);
@@ -74,9 +81,13 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
74
81
  const hatchId = `hatch${useSvgId()}`;
75
82
  const round = (value) => Math.round(value * 10 ** decimals) / 10 ** decimals;
76
83
  const total = round(segments.reduce((sum, segment) => sum + segment.value, 0));
77
- const free = round(Math.max(0, limit - total));
78
84
  const over = round(Math.max(0, total - limit));
79
- const status = total > limit ? "over" : total >= limit * warnAt ? "near" : "ok";
85
+ const past = !unlimited && total > limit;
86
+ const status = unlimited ? "ok" : imposed && !(past && (imposed === "ok" || imposed === "warn")) ? IMPOSED[imposed] : past ? "over" : total >= limit * warnAt ? "near" : "ok";
87
+ const capped = !unlimited && !past && (status === "full" || status === "exhausted");
88
+ const ceiling = capped ? total : limit;
89
+ const free = round(Math.max(0, ceiling - total));
90
+ const danger = status === "over" || status === "full" || status === "exhausted";
80
91
  const format = (value) => `${numberFormat(decimals).format(value)}${unit ? ` ${unit}` : ""}`;
81
92
  const limitText = format(limit).replace(/\.0+(?= |$)/, "");
82
93
  const [ready, setReady] = useState(false);
@@ -95,8 +106,12 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
95
106
  return () => observer.disconnect();
96
107
  }, []);
97
108
  const [amounts] = useState(() => /* @__PURE__ */ new Map());
98
- const shownLimit = useRef(live(limit));
109
+ const shownLimit = useRef(live(unlimited ? 0 : ceiling));
99
110
  const width = useRef(0);
111
+ const bounded = useRef(!unlimited && !capped);
112
+ useIsomorphicLayoutEffect(() => {
113
+ bounded.current = !unlimited && !capped;
114
+ });
100
115
  /** One writer: every segment's place and length, the limit marker and the overflow hatch. */
101
116
  const paint = useCallback(() => {
102
117
  const frame = track.current;
@@ -121,7 +136,7 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
121
136
  if (marker.current) marker.current.style.translate = `${limitX}px 0`;
122
137
  if (overflow.current) overflow.current.style.clipPath = `inset(0 0 0 ${limitX + GAP}px)`;
123
138
  if (bar.current) {
124
- if (used > limitNow) bar.current.setAttribute("data-over", "");
139
+ if (bounded.current && used > limitNow) bar.current.setAttribute("data-over", "");
125
140
  else bar.current.removeAttribute("data-over");
126
141
  }
127
142
  }, [amounts]);
@@ -164,8 +179,12 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
164
179
  amounts
165
180
  ]);
166
181
  useEffect(() => {
167
- retarget(shownLimit.current, limit, SMOOTH, paint);
168
- }, [limit, paint]);
182
+ retarget(shownLimit.current, unlimited ? 0 : ceiling, SMOOTH, paint);
183
+ }, [
184
+ ceiling,
185
+ unlimited,
186
+ paint
187
+ ]);
169
188
  useEffect(() => {
170
189
  const bound = shownLimit.current;
171
190
  return () => {
@@ -181,24 +200,25 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
181
200
  id: segment.id,
182
201
  label: segment.label,
183
202
  value: segment.value
184
- })), {
203
+ })), ...unlimited ? [] : [{
185
204
  id: "free",
186
- label: status === "over" ? overLabel : freeLabel,
187
- value: status === "over" ? over : free
188
- }];
205
+ label: past ? overLabel : freeLabel,
206
+ value: past ? over : free
207
+ }]];
208
+ const rove = Math.min(roving, Math.max(0, items.length - 1));
189
209
  const active = hovered ?? focused ?? pinned;
190
210
  const activeItem = items.find((item) => item.id === active) ?? null;
191
211
  const idAt = (clientX) => {
192
212
  const rect = track.current?.getBoundingClientRect();
193
213
  if (!rect?.width) return null;
194
- const at = (clientX - rect.left) / rect.width * Math.max(limit, total);
195
- if (at > limit) return "free";
214
+ const at = (clientX - rect.left) / rect.width * (unlimited ? total : Math.max(ceiling, total));
215
+ if (!unlimited && at > ceiling) return "free";
196
216
  let end = 0;
197
217
  for (const segment of segments) {
198
218
  end += segment.value;
199
219
  if (at <= end) return segment.id;
200
220
  }
201
- return "free";
221
+ return unlimited ? null : "free";
202
222
  };
203
223
  const onTrackMove = (event) => {
204
224
  if (event.pointerType === "mouse") setHovered(idAt(event.clientX));
@@ -218,32 +238,34 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
218
238
  return;
219
239
  }
220
240
  const next = {
221
- ArrowRight: roving + 1,
222
- ArrowDown: roving + 1,
223
- ArrowLeft: roving - 1,
224
- ArrowUp: roving - 1,
241
+ ArrowRight: rove + 1,
242
+ ArrowDown: rove + 1,
243
+ ArrowLeft: rove - 1,
244
+ ArrowUp: rove - 1,
225
245
  Home: 0,
226
246
  End: items.length - 1
227
247
  }[event.key];
228
- if (next === void 0) return;
248
+ if (next === void 0 || !items.length) return;
229
249
  event.preventDefault();
230
250
  const index = (next + items.length) % items.length;
231
251
  setRoving(index);
232
252
  legend.current?.querySelectorAll("button")[index]?.focus();
233
253
  };
234
254
  const headline = ready ? activeItem ? activeItem.value : total : 0;
235
- const share = activeItem ? Math.round(activeItem.value / limit * 100) : 0;
236
- const caption = !activeItem ? `${unit} of ${limitText} used` : activeItem.id === "free" ? `${unit} ${status === "over" ? "over the limit" : "free"}` : `${unit} in ${activeItem.label} · ${share}% of plan`;
237
- const badge = status === "over" ? `${format(over)} over` : status === "near" ? "Almost full" : `${format(free)} free`;
238
- const summary = `${label}: ${format(total)} of ${limitText} used. ${segments.map((segment) => `${segment.label} ${format(segment.value)}`).join(", ")}. ${status === "over" ? `Over the limit by ${format(over)}.` : `${format(free)} free${status === "near" ? ", almost full" : ""}.`}`;
239
- const notice = status === "over" ? `over:${over}` : status;
255
+ const share = activeItem && !unlimited && limit > 0 ? Math.round(activeItem.value / limit * 100) : 0;
256
+ const caption = !activeItem ? unlimited ? `${unit} used · no limit` : `${unit} of ${limitText} used` : activeItem.id === "free" ? `${unit} ${past ? "over the limit" : "free"}` : unlimited ? `${unit} in ${activeItem.label}` : `${unit} in ${activeItem.label} · ${share}% of plan`;
257
+ const badge = statusLabel ?? (unlimited ? "Unlimited" : status === "over" ? `${format(over)} over` : status === "near" ? "Almost full" : status === "full" ? "Limit reached" : status === "exhausted" ? "None left" : `${format(free)} free`);
258
+ const breakdown = segments.map((segment) => `${segment.label} ${format(segment.value)}`).join(", ");
259
+ const labelled = statusLabel ? ` ${statusLabel}.` : "";
260
+ const summary = unlimited ? `${label}: ${format(total)} used, no limit. ${breakdown}.${labelled}` : `${label}: ${format(total)} of ${limitText} used. ${breakdown}. ${status === "over" ? `Over the limit by ${format(over)}.` : `${format(free)} free${statusLabel ? "" : status === "near" ? ", almost full" : status === "full" ? ", limit reached" : status === "exhausted" ? ", none left" : ""}.`}${labelled}`;
261
+ const notice = `${unlimited ? "unlimited" : status === "over" ? `over:${over}` : status}|${statusLabel ?? ""}`;
240
262
  const [announced, setAnnounced] = useState({
241
263
  notice,
242
264
  message: ""
243
265
  });
244
266
  if (announced.notice !== notice) setAnnounced({
245
267
  notice,
246
- message: status === "over" ? `Over the plan limit by ${format(over)}.` : status === "near" ? `Almost full. ${format(free)} free.` : `Under the limit. ${format(free)} free.`
268
+ message: unlimited ? `No limit.${labelled}` : status === "over" ? `Over the plan limit by ${format(over)}.${labelled}` : statusLabel ? `${statusLabel}. ${format(free)} free.` : status === "near" ? `Almost full. ${format(free)} free.` : status === "full" ? "Limit reached." : status === "exhausted" ? "None left." : `Under the limit. ${format(free)} free.`
247
269
  });
248
270
  return /* @__PURE__ */ jsxs("div", {
249
271
  ref: refs,
@@ -252,6 +274,7 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
252
274
  "aria-labelledby": titleId,
253
275
  "data-slot": "usage-meter",
254
276
  "data-status": status,
277
+ "data-unlimited": unlimited || void 0,
255
278
  className: cn("usage-meter", className),
256
279
  children: [
257
280
  /* @__PURE__ */ jsxs("div", {
@@ -261,24 +284,19 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
261
284
  className: "min-w-0 text-base font-medium text-foreground text-balance",
262
285
  children: label
263
286
  }), /* @__PURE__ */ jsx(Badge, {
264
- tone: status === "over" ? "danger" : status === "near" ? "warning" : "neutral",
287
+ tone: danger ? "danger" : status === "near" ? "warning" : "neutral",
265
288
  shape: "pill",
266
289
  size: "lg",
267
- icon: status === "over" ? /* @__PURE__ */ jsx(AlertTriangleIcon, {}) : status === "near" ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
290
+ icon: danger ? /* @__PURE__ */ jsx(AlertTriangleIcon, {}) : status === "near" ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
268
291
  children: badge
269
292
  })]
270
293
  }),
271
294
  /* @__PURE__ */ jsxs("p", {
272
295
  className: "m-0 flex min-w-0 items-baseline gap-2",
273
296
  children: [
274
- /* @__PURE__ */ jsxs("span", {
297
+ /* @__PURE__ */ jsx("span", {
275
298
  className: "sr-only",
276
- children: [
277
- format(total),
278
- " of ",
279
- limitText,
280
- " used"
281
- ]
299
+ children: unlimited ? `${format(total)} used, no limit` : `${format(total)} of ${limitText} used`
282
300
  }),
283
301
  /* @__PURE__ */ jsx("span", {
284
302
  "data-slot": "usage-meter-amount",
@@ -318,7 +336,7 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
318
336
  "data-index": index,
319
337
  "data-highlight": active ? active === segment.id ? "on" : "off" : void 0,
320
338
  className: "usage-meter-segment"
321
- }, segment.id)), /* @__PURE__ */ jsxs("svg", {
339
+ }, segment.id)), unlimited ? null : /* @__PURE__ */ jsxs("svg", {
322
340
  ref: overflow,
323
341
  "aria-hidden": "true",
324
342
  focusable: "false",
@@ -349,7 +367,7 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
349
367
  })
350
368
  ]
351
369
  })]
352
- }), /* @__PURE__ */ jsx("span", {
370
+ }), unlimited ? null : /* @__PURE__ */ jsx("span", {
353
371
  ref: marker,
354
372
  "aria-hidden": "true",
355
373
  className: "usage-meter-limit"
@@ -362,14 +380,14 @@ const UsageMeter = forwardRef(function UsageMeter({ label, segments, limit, unit
362
380
  onKeyDown: onLegendKey,
363
381
  className: "usage-meter-legend",
364
382
  children: items.map((item, index) => {
365
- const warning = item.id === "free" && status === "over";
383
+ const warning = item.id === "free" && past;
366
384
  return /* @__PURE__ */ jsxs("button", {
367
385
  type: "button",
368
386
  "data-slot": "usage-meter-item",
369
387
  "data-highlight": active ? active === item.id ? "on" : "off" : void 0,
370
388
  "aria-pressed": pinned === item.id,
371
389
  "aria-label": `${item.label}, ${format(item.value)}`,
372
- tabIndex: index === roving ? 0 : -1,
390
+ tabIndex: index === rove ? 0 : -1,
373
391
  onClick: () => {
374
392
  setRoving(index);
375
393
  setPinned((current) => current === item.id ? null : item.id);