@seekalfred/primitives 0.7.1 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -5
- package/dist/alert/alert.d.ts +49 -4
- package/dist/alert/alert.js +66 -38
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +31 -4
- package/dist/alert-dialog/alert-dialog.js +75 -26
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +1 -1
- package/dist/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- package/dist/card/card.js.map +1 -1
- package/dist/carousel/carousel.d.ts +21 -0
- package/dist/carousel/carousel.js +34 -8
- package/dist/carousel/carousel.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/copy-button/copy-button.d.ts +37 -5
- package/dist/copy-button/copy-button.js +35 -11
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +22 -0
- package/dist/donut-chart/donut-chart.js +35 -18
- package/dist/donut-chart/donut-chart.js.map +1 -1
- package/dist/donut-chart/index.d.ts +2 -2
- package/dist/empty-state/empty-state.d.ts +21 -0
- package/dist/empty-state/empty-state.js +16 -4
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +4 -1
- package/dist/filter-bar/filter-bar.js +53 -9
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +15 -14
- package/dist/index.js +9 -9
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +7 -5
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +6 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +22 -1
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +144 -2
- package/dist/line-chart/line-chart.js +314 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +36 -1
- package/dist/money-input/money-input.js +30 -14
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +26 -1
- package/dist/morph-select/morph-select.js +28 -6
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/index.d.ts +2 -2
- package/dist/multi-select/index.js +2 -2
- package/dist/multi-select/multi-select.d.ts +74 -2
- package/dist/multi-select/multi-select.js +213 -39
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/password-field/password-field.d.ts +16 -0
- package/dist/password-field/password-field.js +37 -8
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/popover/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +39 -2
- package/dist/popover/popover.js +29 -2
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +68 -3
- package/dist/progress/progress.js +156 -39
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +109 -9
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.js +4 -3
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.d.ts +22 -1
- package/dist/search-field/search-field.js +22 -7
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +11 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +21 -9
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +65 -1
- package/dist/stat-card/stat-card.js +98 -25
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +115 -5
- package/dist/table/table.js +374 -94
- package/dist/table/table.js.map +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +3 -1
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/time-picker/time-picker.d.ts +36 -1
- package/dist/time-picker/time-picker.js +33 -5
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/usage-meter/index.d.ts +2 -2
- package/dist/usage-meter/usage-meter.d.ts +30 -0
- package/dist/usage-meter/usage-meter.js +56 -38
- package/dist/usage-meter/usage-meter.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
-
/**
|
|
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":"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"}
|
package/dist/toast/toast.d.ts
CHANGED
|
@@ -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" | "
|
|
37
|
+
tone?: "neutral" | "warning" | "success" | "info" | "danger" | 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" | "
|
|
41
|
+
tone?: "neutral" | "warning" | "success" | "info" | "danger" | 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
|
|
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(
|
|
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,
|
|
168
|
-
}, [
|
|
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:
|
|
187
|
-
value:
|
|
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(
|
|
195
|
-
if (at >
|
|
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:
|
|
222
|
-
ArrowDown:
|
|
223
|
-
ArrowLeft:
|
|
224
|
-
ArrowUp:
|
|
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} ${
|
|
237
|
-
const badge = status === "over" ? `${format(over)} over` : status === "near" ? "Almost full" : `${format(free)} free
|
|
238
|
-
const
|
|
239
|
-
const
|
|
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:
|
|
287
|
+
tone: danger ? "danger" : status === "near" ? "warning" : "neutral",
|
|
265
288
|
shape: "pill",
|
|
266
289
|
size: "lg",
|
|
267
|
-
icon:
|
|
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__ */
|
|
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" &&
|
|
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 ===
|
|
390
|
+
tabIndex: index === rove ? 0 : -1,
|
|
373
391
|
onClick: () => {
|
|
374
392
|
setRoving(index);
|
|
375
393
|
setPinned((current) => current === item.id ? null : item.id);
|