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