@eifi1/ui-kit 0.6.0 → 0.7.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 +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +36 -7
- package/dist/components/autocomplete.js +68 -22
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +7 -2
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +36 -5
- package/dist/components/file-button.js +42 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +9 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +9 -0
- package/dist/components/switch.js +7 -2
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +31 -7
- package/dist/components/ui.js +26 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +43 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +43 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +43 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +43 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +43 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +43 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +43 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +133 -84
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +123 -33
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/checkbox.tsx +16 -0
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +102 -6
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/switch.tsx +16 -0
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +74 -30
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +200 -129
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -8,9 +8,14 @@ import type {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { createPortal } from "react-dom";
|
|
10
10
|
import { cn } from "../lib/cn";
|
|
11
|
-
import {
|
|
11
|
+
import { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID, Spinner } from "./ui";
|
|
12
12
|
import { useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
13
|
+
import { useAnchorDir } from "./use-anchor-dir";
|
|
13
14
|
import {
|
|
15
|
+
ComboboxFieldLabel,
|
|
16
|
+
DISABLED_ROW_CLASS,
|
|
17
|
+
isOptionEnabled,
|
|
18
|
+
stepEnabled,
|
|
14
19
|
useActiveOptionScroll,
|
|
15
20
|
useComboboxFieldError,
|
|
16
21
|
useOptionSource,
|
|
@@ -26,7 +31,7 @@ import { DEFAULT_COMBOBOX_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
|
26
31
|
export interface AutocompleteProps<V extends string | number = string>
|
|
27
32
|
extends Omit<
|
|
28
33
|
ComponentPropsWithoutRef<"input">,
|
|
29
|
-
"value" | "defaultValue" | "onChange" | "onSelect" | "children" | "type"
|
|
34
|
+
"value" | "defaultValue" | "onChange" | "onSelect" | "children" | "type" | "size"
|
|
30
35
|
> {
|
|
31
36
|
/** What is in the field. Controlled, and never reset by the component: opening,
|
|
32
37
|
* closing and a failed lookup all leave it exactly as typed. */
|
|
@@ -34,7 +39,8 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
34
39
|
/** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */
|
|
35
40
|
onChange: (text: string) => void;
|
|
36
41
|
/** Suggestions the caller already has (e.g. from its own query hook). Narrowed by
|
|
37
|
-
* the text unless `filter={false}`.
|
|
42
|
+
* the text unless `filter={false}`. An option with `disabled: true` is listed but
|
|
43
|
+
* cannot be taken — see {@link ComboOption.disabled}. */
|
|
38
44
|
options?: ComboOption<V>[];
|
|
39
45
|
/** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,
|
|
40
46
|
* never while the field is idle; a rejection shows `loadErrorLabel` and empties
|
|
@@ -52,6 +58,30 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
52
58
|
* inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every
|
|
53
59
|
* letter, which a rate-limited geocoder cannot afford. */
|
|
54
60
|
debounceMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field
|
|
63
|
+
* decides: open while focused, until Escape, Tab or a taken row. `false` keeps it
|
|
64
|
+
* shut whatever the user does, and stops `loadOptions` asking — keksdose's address
|
|
65
|
+
* search, with its confirm map up, where it used to pass `minChars={Infinity}`.
|
|
66
|
+
* `true` shows it even unfocused.
|
|
67
|
+
*
|
|
68
|
+
* Even open, the list only APPEARS when it has something to show (rows, or a
|
|
69
|
+
* status line); `aria-expanded` follows what appears, not this flag.
|
|
70
|
+
*/
|
|
71
|
+
open?: boolean;
|
|
72
|
+
/** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,
|
|
73
|
+
* Escape, Tab and taking a row shut it. Called on every change, controlled or not. */
|
|
74
|
+
onOpenChange?: (open: boolean) => void;
|
|
75
|
+
/**
|
|
76
|
+
* `"sm"`: the 28px, 12px-type field of {@link Select}'s `size="sm"`, with the icon,
|
|
77
|
+
* spinner and list rows scaled to match — for a card or toolbar whose buttons are
|
|
78
|
+
* small. `"md"` (default) is the form field. Unlabelled only, as on `Select`: a
|
|
79
|
+
* floating label needs the tall box, so a labelled field ignores `"sm"`.
|
|
80
|
+
*
|
|
81
|
+
* A NUMBER is still the native `size` attribute (the width in characters), passed
|
|
82
|
+
* straight through.
|
|
83
|
+
*/
|
|
84
|
+
size?: "sm" | "md" | number;
|
|
55
85
|
/** A suggestion was taken (click, or Enter on the highlighted row). */
|
|
56
86
|
onSelect?: (option: ComboOption<V>) => void;
|
|
57
87
|
/**
|
|
@@ -61,11 +91,12 @@ export interface AutocompleteProps<V extends string | number = string>
|
|
|
61
91
|
* and the field is only the search that found it; the text then stays as typed.
|
|
62
92
|
*/
|
|
63
93
|
fillOnSelect?: boolean;
|
|
64
|
-
/** A floating label, as on {@link Combobox}
|
|
65
|
-
* unless an `aria-label` is given. */
|
|
94
|
+
/** A floating label, as on {@link Combobox} — a real `<label for>` on the input.
|
|
95
|
+
* Names the field (`aria-labelledby`) unless an `aria-label` is given. */
|
|
66
96
|
label?: ReactNode;
|
|
67
97
|
/** Leading decoration inside the field (a pin, a magnifier). Decorative: the
|
|
68
|
-
* label names the field.
|
|
98
|
+
* label names the field. An `<svg>` child is sized by the field — 16px, or 14px
|
|
99
|
+
* at `size="sm"` — so a lucide icon needs no class of its own. */
|
|
69
100
|
icon?: ReactNode;
|
|
70
101
|
/** Required and unanswered — {@link FIELD_INVALID}. */
|
|
71
102
|
invalid?: boolean;
|
|
@@ -107,6 +138,9 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
107
138
|
status,
|
|
108
139
|
emptyLabel,
|
|
109
140
|
loadErrorLabel,
|
|
141
|
+
open: openProp,
|
|
142
|
+
onOpenChange,
|
|
143
|
+
size,
|
|
110
144
|
className,
|
|
111
145
|
inputClassName,
|
|
112
146
|
id,
|
|
@@ -136,7 +170,15 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
136
170
|
|
|
137
171
|
/** The user's intent: the field is focused and the list has not been dismissed.
|
|
138
172
|
* Whether anything SHOWS is decided below, from what there is to show. */
|
|
139
|
-
const [
|
|
173
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
174
|
+
// Controlled when the caller passes a boolean. The own state is kept up to date
|
|
175
|
+
// either way, so a caller that controls only SOMETIMES (`open={confirming ? false :
|
|
176
|
+
// undefined}`) hands back a field that is exactly as open as focus says it is.
|
|
177
|
+
const open = openProp ?? ownOpen;
|
|
178
|
+
const setOpen = (next: boolean) => {
|
|
179
|
+
setOwnOpen(next);
|
|
180
|
+
if (next !== open) onOpenChange?.(next);
|
|
181
|
+
};
|
|
140
182
|
const [active, setActive] = useState(-1);
|
|
141
183
|
const live = open && !disabled;
|
|
142
184
|
const { results, busy, failed, tooShort } = useOptionSource<V>({
|
|
@@ -165,22 +207,29 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
165
207
|
? labels.noResults
|
|
166
208
|
: null;
|
|
167
209
|
const expanded = live && (results.length > 0 || statusLine !== null);
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
// A disabled row is never the keyboard's — not even when a list that changed under
|
|
211
|
+
// the highlight put one there.
|
|
212
|
+
const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;
|
|
170
213
|
useActiveOptionScroll(activeId);
|
|
171
214
|
const { rect, top, maxHeight } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });
|
|
215
|
+
const dir = useAnchorDir(fieldRef, expanded);
|
|
172
216
|
|
|
173
217
|
const close = () => {
|
|
174
218
|
setOpen(false);
|
|
175
219
|
setActive(-1);
|
|
176
220
|
};
|
|
177
221
|
const take = (o: ComboOption<V>) => {
|
|
222
|
+
if (o.disabled) return;
|
|
178
223
|
if (fillOnSelect) onChange(o.label);
|
|
179
224
|
onSelect?.(o);
|
|
180
225
|
close();
|
|
181
226
|
};
|
|
182
227
|
|
|
183
228
|
const hasLabel = label !== undefined;
|
|
229
|
+
// `size="sm"` is for an UNLABELLED field, as on Select: the floating label needs the
|
|
230
|
+
// tall box. A number is the native attribute.
|
|
231
|
+
const small = size === "sm" && !hasLabel;
|
|
232
|
+
const nativeSize = typeof size === "number" ? size : undefined;
|
|
184
233
|
// The caller's name wins; else the floating label, by reference so a non-string
|
|
185
234
|
// label still names the field.
|
|
186
235
|
const labelledBy =
|
|
@@ -190,12 +239,21 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
190
239
|
// `relative`: the floating label and the live region's `sr-only` both need a
|
|
191
240
|
// local containing block.
|
|
192
241
|
<div className={cn("relative", className)}>
|
|
193
|
-
{
|
|
242
|
+
{/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the
|
|
243
|
+
name — and the listbox's, which borrows it — is exactly what it was. */}
|
|
244
|
+
{hasLabel && (
|
|
245
|
+
<ComboboxFieldLabel id={labelId} htmlFor={fieldId}>
|
|
246
|
+
{label}
|
|
247
|
+
</ComboboxFieldLabel>
|
|
248
|
+
)}
|
|
194
249
|
<div ref={fieldRef} className="relative">
|
|
195
250
|
{icon && (
|
|
196
251
|
<span
|
|
197
252
|
aria-hidden
|
|
198
|
-
className=
|
|
253
|
+
className={cn(
|
|
254
|
+
"pointer-events-none absolute top-1/2 flex -translate-y-1/2 text-[var(--text-muted)]",
|
|
255
|
+
small ? "start-2 [&>svg]:size-3.5" : "start-2.5 [&>svg]:size-4",
|
|
256
|
+
)}
|
|
199
257
|
>
|
|
200
258
|
{icon}
|
|
201
259
|
</span>
|
|
@@ -204,6 +262,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
204
262
|
{...rest}
|
|
205
263
|
ref={ref}
|
|
206
264
|
id={fieldId}
|
|
265
|
+
size={nativeSize}
|
|
207
266
|
type="text"
|
|
208
267
|
value={value}
|
|
209
268
|
disabled={disabled}
|
|
@@ -234,18 +293,28 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
234
293
|
setActive(-1);
|
|
235
294
|
}}
|
|
236
295
|
onKeyDown={(e) => {
|
|
296
|
+
// An Escape that closes the OPEN list is the list's, and is consumed before
|
|
297
|
+
// the caller sees it. A caller whose Escape means "close the panel" (keksdose's
|
|
298
|
+
// address search) otherwise closed the whole panel when the user only meant
|
|
299
|
+
// to dismiss the suggestions — the opposite of what the docs promised.
|
|
300
|
+
if (e.key === "Escape" && expanded) {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
close();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
237
306
|
onKeyDown?.(e);
|
|
238
307
|
if (e.defaultPrevented) return;
|
|
239
|
-
|
|
308
|
+
// Disabled rows are passed over; at either end the highlight stays put.
|
|
240
309
|
if (e.key === "ArrowDown") {
|
|
241
310
|
e.preventDefault();
|
|
242
311
|
if (!open) setOpen(true);
|
|
243
|
-
else setActive((i) =>
|
|
312
|
+
else setActive((i) => stepEnabled(results, i, 1));
|
|
244
313
|
} else if (e.key === "ArrowUp") {
|
|
245
314
|
e.preventDefault();
|
|
246
315
|
if (!open) setOpen(true);
|
|
247
316
|
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
248
|
-
else setActive((i) => (i < 0 ?
|
|
317
|
+
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
249
318
|
} else if (e.key === "Enter") {
|
|
250
319
|
// No row highlighted: the text is the answer, and a form's own submit
|
|
251
320
|
// is left alone.
|
|
@@ -253,14 +322,6 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
253
322
|
e.preventDefault();
|
|
254
323
|
take(results[active]);
|
|
255
324
|
}
|
|
256
|
-
} else if (e.key === "Escape") {
|
|
257
|
-
// Only when there is a list to close — otherwise the Escape belongs to
|
|
258
|
-
// whatever this field sits in (a dialog), and must reach it.
|
|
259
|
-
if (expanded) {
|
|
260
|
-
e.preventDefault();
|
|
261
|
-
e.stopPropagation();
|
|
262
|
-
close();
|
|
263
|
-
}
|
|
264
325
|
} else if (e.key === "Tab") {
|
|
265
326
|
close();
|
|
266
327
|
}
|
|
@@ -268,16 +329,24 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
268
329
|
className={cn(
|
|
269
330
|
FIELD_BASE,
|
|
270
331
|
hasLabel && FIELD_FLOATING_PAD,
|
|
271
|
-
|
|
272
|
-
|
|
332
|
+
// Select's `SELECT_SM` box: a fixed 28px, so the caller's line height
|
|
333
|
+
// cannot grow it.
|
|
334
|
+
small && "h-7 py-0 pe-2 ps-2 text-xs",
|
|
335
|
+
icon ? (small ? "ps-7" : "ps-8") : undefined,
|
|
336
|
+
busy && (small ? "pe-7" : "pe-9"),
|
|
273
337
|
field.isInvalid && FIELD_INVALID,
|
|
274
338
|
inputClassName,
|
|
275
339
|
)}
|
|
276
340
|
/>
|
|
277
341
|
{busy && live && (
|
|
278
342
|
// Decorative: the live region below already says "Loading…".
|
|
279
|
-
<span
|
|
280
|
-
|
|
343
|
+
<span
|
|
344
|
+
className={cn(
|
|
345
|
+
"pointer-events-none absolute top-1/2 flex -translate-y-1/2",
|
|
346
|
+
small ? "end-2" : "end-2.5",
|
|
347
|
+
)}
|
|
348
|
+
>
|
|
349
|
+
<Spinner label={null} className={small ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
|
281
350
|
</span>
|
|
282
351
|
)}
|
|
283
352
|
</div>
|
|
@@ -303,6 +372,9 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
303
372
|
// Every press inside the list keeps focus in the field — rows, the status
|
|
304
373
|
// line, the scrollbar — or the input's blur would close the list first.
|
|
305
374
|
role="presentation"
|
|
375
|
+
// Portalled out of the form's `dir`; the field's is put back. The list is
|
|
376
|
+
// exactly the field's width, so `left` places it in either direction.
|
|
377
|
+
dir={dir}
|
|
306
378
|
onMouseDown={(e) => e.preventDefault()}
|
|
307
379
|
className="fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg"
|
|
308
380
|
style={{ top, left: rect.left, width: rect.width, maxHeight }}
|
|
@@ -338,12 +410,22 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
338
410
|
// field holds no value, so the only row that can be chosen
|
|
339
411
|
// is the one whose label the text already is.
|
|
340
412
|
aria-selected={fillOnSelect && o.label === value}
|
|
413
|
+
// Listed, announced as unavailable, never taken: `take`
|
|
414
|
+
// refuses it and the arrows pass it over.
|
|
415
|
+
aria-disabled={o.disabled || undefined}
|
|
341
416
|
tabIndex={-1}
|
|
342
417
|
onClick={() => take(o)}
|
|
343
|
-
onMouseEnter={() =>
|
|
418
|
+
onMouseEnter={() => {
|
|
419
|
+
if (!o.disabled) setActive(i);
|
|
420
|
+
}}
|
|
344
421
|
className={cn(
|
|
345
|
-
"flex w-full items-center gap-2 px-3
|
|
346
|
-
|
|
422
|
+
"flex w-full items-center gap-2 px-3 text-start",
|
|
423
|
+
small ? "py-1 text-xs" : "py-1.5 text-sm",
|
|
424
|
+
o.disabled
|
|
425
|
+
? DISABLED_ROW_CLASS
|
|
426
|
+
: i === active
|
|
427
|
+
? "bg-[var(--bg-active)]"
|
|
428
|
+
: "hover:bg-[var(--bg-hover)]",
|
|
347
429
|
o.group != null && "ps-6",
|
|
348
430
|
)}
|
|
349
431
|
>
|
|
@@ -357,7 +439,12 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
357
439
|
{o.label}
|
|
358
440
|
</span>
|
|
359
441
|
{o.sublabel && (
|
|
360
|
-
<span
|
|
442
|
+
<span
|
|
443
|
+
className={cn(
|
|
444
|
+
"block truncate text-[var(--text-placeholder)]",
|
|
445
|
+
small ? "text-[11px]" : "text-xs",
|
|
446
|
+
)}
|
|
447
|
+
>
|
|
361
448
|
{o.sublabel}
|
|
362
449
|
</span>
|
|
363
450
|
)}
|
|
@@ -374,7 +461,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
374
461
|
<div
|
|
375
462
|
aria-hidden
|
|
376
463
|
className={cn(
|
|
377
|
-
"px-3 py-2 text-sm",
|
|
464
|
+
small ? "px-3 py-1.5 text-xs" : "px-3 py-2 text-sm",
|
|
378
465
|
failed && !hasStatus ? "text-[var(--danger)]" : "text-[var(--text-muted)]",
|
|
379
466
|
results.length > 0 && "border-t border-[var(--border)] text-xs",
|
|
380
467
|
)}
|
|
@@ -409,9 +496,12 @@ AutocompleteBase.displayName = "Autocomplete";
|
|
|
409
496
|
*
|
|
410
497
|
* Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted
|
|
411
498
|
* row, `aria-controls` the list, and a polite live region says what the list now
|
|
412
|
-
* holds. Keyboard: ↓/↑ move (↓ opens)
|
|
499
|
+
* holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter
|
|
500
|
+
* takes the highlighted row (with none,
|
|
413
501
|
* Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay
|
|
414
|
-
* with the caret.
|
|
502
|
+
* with the caret. An Escape that closes an open list is consumed and never reaches the
|
|
503
|
+
* caller; with the list closed it does. For every other key a caller's `onKeyDown`
|
|
504
|
+
* runs first and may `preventDefault()` to
|
|
415
505
|
* keep a key for itself.
|
|
416
506
|
*
|
|
417
507
|
* The list is portalled and anchored under the field on every screen size: the
|
|
@@ -147,9 +147,13 @@ function CalculatorPanel({
|
|
|
147
147
|
equals();
|
|
148
148
|
}
|
|
149
149
|
}}
|
|
150
|
+
// Arithmetic reads left to right in every script, and a calculator display
|
|
151
|
+
// is right-aligned in every script too — so both stay physical, pinned
|
|
152
|
+
// with `dir="ltr"` rather than mirrored in a right-to-left form.
|
|
153
|
+
dir="ltr"
|
|
150
154
|
className="block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 text-right font-mono text-sm text-[var(--text-primary)] focus:border-[var(--border-strong)] focus:ring-[var(--border-strong)]"
|
|
151
155
|
/>
|
|
152
|
-
<div className="h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]">
|
|
156
|
+
<div dir="ltr" className="h-4 pr-1 text-right font-mono text-xs text-[var(--text-placeholder)]">
|
|
153
157
|
{result !== null && formatResult(result) !== text.trim() ? `= ${formatResult(result)}` : ""}
|
|
154
158
|
</div>
|
|
155
159
|
<div className="grid grid-cols-4 gap-1.5">
|
|
@@ -337,6 +337,13 @@ export function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {
|
|
|
337
337
|
};
|
|
338
338
|
const state: ZoomState = { x: xWindow, y: ownY };
|
|
339
339
|
const zoomed = xWindow !== undefined || ownY !== null;
|
|
340
|
+
// A chart handed no rows or no series draws its empty state, not a plot — and a
|
|
341
|
+
// "Reset zoom" floating over "No data" (every line toggled off while zoomed) offers
|
|
342
|
+
// to undo something the reader cannot see. The zoom itself is KEPT: it is where they
|
|
343
|
+
// were looking, and it applies again the moment a line comes back. Only an array
|
|
344
|
+
// that is there and empty counts; a chart that does not pass its data is not known
|
|
345
|
+
// to be empty.
|
|
346
|
+
const empty = props.rows?.length === 0 || props.series?.length === 0;
|
|
340
347
|
|
|
341
348
|
const clear = () => {
|
|
342
349
|
setXWindow(undefined);
|
|
@@ -362,7 +369,7 @@ export function withChartZoom<P extends ZoomTarget>(Chart: ComponentType<P>) {
|
|
|
362
369
|
return (
|
|
363
370
|
<div className="relative">
|
|
364
371
|
<Chart {...props} zoom={binding} />
|
|
365
|
-
{zoomed && (
|
|
372
|
+
{zoomed && !empty && (
|
|
366
373
|
<button
|
|
367
374
|
type="button"
|
|
368
375
|
onClick={clear}
|
package/src/components/chart.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Tooltip as RechartsTooltip,
|
|
17
17
|
} from "recharts";
|
|
18
18
|
import { cn } from "../lib/cn";
|
|
19
|
+
import { isRtl } from "../lib/direction";
|
|
19
20
|
|
|
20
21
|
export interface ChartSeriesConfig {
|
|
21
22
|
label: ReactNode;
|
|
@@ -37,6 +38,20 @@ interface ChartContainerProps extends ComponentProps<"div"> {
|
|
|
37
38
|
children: ComponentProps<typeof ResponsiveContainer>["children"];
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The shell every chart in the kit is drawn inside.
|
|
43
|
+
*
|
|
44
|
+
* **The plot is not mirrored in RTL.** An x axis is a number line or a timeline, and
|
|
45
|
+
* both run left-to-right in Hebrew and Arabic charts as much as in English ones; a
|
|
46
|
+
* time axis flipped to run right-to-left is the surprise, not the courtesy. So no
|
|
47
|
+
* `reversed` on any axis, and the SVG is pinned to `direction: ltr` — without that it
|
|
48
|
+
* inherits the page's `rtl`, which flips what `text-anchor: end` means, and the tick
|
|
49
|
+
* labels of a left-hand y axis are drawn INTO the plot instead of beside it. Text
|
|
50
|
+
* inside the SVG still shapes by its own script; only the anchoring is physical.
|
|
51
|
+
* The HTML around the plot — legend, tooltip, the zoom reset button — follows the
|
|
52
|
+
* page's direction like any other markup. A consumer who does want a mirrored
|
|
53
|
+
* category axis passes `reversed` on its own `XAxis`.
|
|
54
|
+
*/
|
|
40
55
|
export function ChartContainer({ id, className, children, config, ...props }: ChartContainerProps) {
|
|
41
56
|
const uid = useId();
|
|
42
57
|
// The id is pasted into an UNQUOTED `[data-chart=…]` selector below, so it has to be
|
|
@@ -54,6 +69,8 @@ export function ChartContainer({ id, className, children, config, ...props }: Ch
|
|
|
54
69
|
// a horizontal drag is handed to recharts for tooltip scrubbing,
|
|
55
70
|
// instead of the page scrolling underneath the gesture (feedback #292).
|
|
56
71
|
"flex w-full touch-pan-y justify-center text-xs",
|
|
72
|
+
// physical anchoring for the unmirrored plot (see the doc comment above)
|
|
73
|
+
"[&_.recharts-surface]:[direction:ltr]",
|
|
57
74
|
// muted axis labels + gridlines; one class each, since the tokens
|
|
58
75
|
// already carry the light/dark values
|
|
59
76
|
"[&_.recharts-cartesian-axis-tick_text]:fill-[var(--text-muted)]",
|
|
@@ -101,6 +118,19 @@ const CSS_IDENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
|
101
118
|
const CSS_COLOR =
|
|
102
119
|
/^(?:#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|(?:rgba?|hsla?)\([0-9A-Za-z.,%/ -]*\)|var\(--[A-Za-z0-9_-]+\))$/;
|
|
103
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The colour an entry is painted with, or `undefined` when the style block refuses it.
|
|
123
|
+
*
|
|
124
|
+
* The one gate both sides go through: {@link ChartStyle} emits exactly these, and the
|
|
125
|
+
* legend and tooltip swatches show exactly these. They used to read `config[key].color`
|
|
126
|
+
* directly, so an entry the style block had skipped still showed its configured colour
|
|
127
|
+
* in the key — a swatch promising a colour the series was never painted in.
|
|
128
|
+
*/
|
|
129
|
+
function paintedColor(key: string, entry: ChartSeriesConfig | undefined): string | undefined {
|
|
130
|
+
if (!entry?.color) return undefined;
|
|
131
|
+
return CSS_IDENT.test(key) && CSS_COLOR.test(entry.color) ? entry.color : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
// Inject each series colour as a `--color-<key>` custom property scoped to this
|
|
105
135
|
// chart, so chart JSX can refer to `var(--color-total)` and the legend/tooltip
|
|
106
136
|
// stay in sync from one source of truth.
|
|
@@ -127,7 +157,22 @@ function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
|
|
|
127
157
|
return <style dangerouslySetInnerHTML={{ __html: css }} />;
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
|
|
160
|
+
/**
|
|
161
|
+
* recharts' `Tooltip`, with `filterNull` defaulting to **false**.
|
|
162
|
+
*
|
|
163
|
+
* recharts drops every payload entry whose value is `null`/`undefined` before the
|
|
164
|
+
* content sees it, so the missing-value path {@link ChartTooltipContent} documents —
|
|
165
|
+
* the em dash, `formatValue(undefined)` — never ran under the default: a series with a
|
|
166
|
+
* gap at the cursor simply vanished from the tooltip, which reads as "this series does
|
|
167
|
+
* not exist here" rather than "it has no value here". Kept, the entry prints "—".
|
|
168
|
+
*
|
|
169
|
+
* The other half of that filter — dropping series drawn with `hide` — is redone by
|
|
170
|
+
* `ChartTooltipContent` itself (honouring `includeHidden`), so a legend toggle still
|
|
171
|
+
* takes a series out of the tooltip. Pass `filterNull` to get recharts' behaviour back.
|
|
172
|
+
*/
|
|
173
|
+
export function ChartTooltip(props: ComponentProps<typeof RechartsTooltip>) {
|
|
174
|
+
return <RechartsTooltip filterNull={false} {...props} />;
|
|
175
|
+
}
|
|
131
176
|
export const ChartLegend = RechartsLegend;
|
|
132
177
|
|
|
133
178
|
interface TooltipPayloadItem {
|
|
@@ -135,6 +180,8 @@ interface TooltipPayloadItem {
|
|
|
135
180
|
name?: string | number;
|
|
136
181
|
value?: number | string;
|
|
137
182
|
color?: string;
|
|
183
|
+
/** A series drawn with `hide` — dropped here, see {@link ChartTooltip}. */
|
|
184
|
+
hide?: boolean;
|
|
138
185
|
payload?: Record<string, unknown>;
|
|
139
186
|
}
|
|
140
187
|
|
|
@@ -194,6 +241,8 @@ interface ChartTooltipContentProps {
|
|
|
194
241
|
* spill past the right edge of this (usually horizontally-scrolling) container
|
|
195
242
|
* — e.g. a wide chart whose rightmost bars sat off-screen (feedback #77). */
|
|
196
243
|
boundaryRef?: RefObject<HTMLElement | null>;
|
|
244
|
+
/** Injected by recharts: keep series drawn with `hide` in the tooltip. */
|
|
245
|
+
includeHidden?: boolean;
|
|
197
246
|
}
|
|
198
247
|
|
|
199
248
|
export function ChartTooltipContent({
|
|
@@ -207,17 +256,29 @@ export function ChartTooltipContent({
|
|
|
207
256
|
formatValue,
|
|
208
257
|
coordinate,
|
|
209
258
|
boundaryRef,
|
|
259
|
+
includeHidden = false,
|
|
210
260
|
}: ChartTooltipContentProps) {
|
|
211
261
|
const config = useChart();
|
|
212
262
|
const tipRef = useRef<HTMLDivElement>(null);
|
|
213
|
-
|
|
263
|
+
// `ChartTooltip` keeps null values (so they can print "—"), which also keeps the
|
|
264
|
+
// series a legend toggle switched off with `hide`; those go here instead.
|
|
265
|
+
const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
|
|
266
|
+
if (!active || !items?.length) return null;
|
|
214
267
|
// Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
|
|
215
268
|
// width; subtract the container's scrollLeft to get its on-screen x, then flip
|
|
216
269
|
// left if the tooltip's own width would run past the visible right edge.
|
|
270
|
+
//
|
|
271
|
+
// Both the cursor and the flip are PHYSICAL (the plot is never mirrored, see
|
|
272
|
+
// `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —
|
|
273
|
+
// 0 at the start, negative going left — so there the content's left edge sits
|
|
274
|
+
// `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.
|
|
217
275
|
let flip = false;
|
|
218
276
|
const boundary = boundaryRef?.current;
|
|
219
277
|
if (boundary && coordinate?.x != null) {
|
|
220
|
-
const
|
|
278
|
+
const offset = isRtl(boundary)
|
|
279
|
+
? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft
|
|
280
|
+
: boundary.scrollLeft;
|
|
281
|
+
const visibleX = coordinate.x - offset;
|
|
221
282
|
const tipWidth = tipRef.current?.offsetWidth ?? 160;
|
|
222
283
|
flip = visibleX + tipWidth + 12 > boundary.clientWidth;
|
|
223
284
|
}
|
|
@@ -233,7 +294,7 @@ export function ChartTooltipContent({
|
|
|
233
294
|
</div>
|
|
234
295
|
)}
|
|
235
296
|
<div className="grid gap-1.5">
|
|
236
|
-
{
|
|
297
|
+
{items.map((item, i) => {
|
|
237
298
|
// Bars/lines/areas identify a series by dataKey; pies/treemaps key off
|
|
238
299
|
// `name` (the nameKey value). Prefer whichever the config knows.
|
|
239
300
|
const dk = item.dataKey != null ? String(item.dataKey) : undefined;
|
|
@@ -245,7 +306,7 @@ export function ChartTooltipContent({
|
|
|
245
306
|
// or a bar with a per-cell colour, IS the colour the reader is pointing at,
|
|
246
307
|
// while `item.color` is the series-wide one — every tile's swatch came out
|
|
247
308
|
// `--chart-1` whatever colour the tile was.
|
|
248
|
-
const color = series
|
|
309
|
+
const color = paintedColor(cfgKey, series) ?? fill ?? item.color ?? "#64748b";
|
|
249
310
|
const name = series?.label ?? nm ?? cfgKey;
|
|
250
311
|
const raw = tooltipValue(item.value);
|
|
251
312
|
let shown: ReactNode;
|
|
@@ -262,7 +323,7 @@ export function ChartTooltipContent({
|
|
|
262
323
|
style={{ backgroundColor: color }}
|
|
263
324
|
/>
|
|
264
325
|
<span className="text-[var(--text-muted)]">{name}</span>
|
|
265
|
-
<span data-private className="
|
|
326
|
+
<span data-private className="ms-auto font-mono font-medium tabular-nums text-[var(--text-primary)]">
|
|
266
327
|
{shown}
|
|
267
328
|
</span>
|
|
268
329
|
</div>
|
|
@@ -313,7 +374,7 @@ export function ChartLegendContent({
|
|
|
313
374
|
const vv = item.value != null ? String(item.value) : undefined;
|
|
314
375
|
const cfgKey = dk && config[dk] ? dk : vv && config[vv] ? vv : (dk ?? vv ?? String(i));
|
|
315
376
|
const series = config[cfgKey];
|
|
316
|
-
const color = series
|
|
377
|
+
const color = paintedColor(cfgKey, series) ?? item.color ?? "#64748b";
|
|
317
378
|
const label = series?.label ?? vv ?? cfgKey;
|
|
318
379
|
const off = hidden?.has(cfgKey) ?? false;
|
|
319
380
|
const dimmed = off || (activeKey != null && activeKey !== cfgKey);
|
|
@@ -78,6 +78,15 @@ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement
|
|
|
78
78
|
* the row when there is a label, the box's own wrapper when there is not — which
|
|
79
79
|
* is where layout (`self-end pb-2` to sit on a field's baseline) belongs. */
|
|
80
80
|
inputClassName?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Must be ticked to submit — a consent, an acceptance before paying. Reaches the
|
|
83
|
+
* `<input>` as the native `required` (so a `<form>` refuses to submit without it and a
|
|
84
|
+
* screen reader announces "required"), and draws the kit's required mark after the
|
|
85
|
+
* label, the same `aria-hidden` star as {@link Label}'s: the word is announced from
|
|
86
|
+
* the control, so a star inside the name would only be read out as noise. Write the
|
|
87
|
+
* label WITHOUT a literal "*". No mark without a label.
|
|
88
|
+
*/
|
|
89
|
+
required?: boolean;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/**
|
|
@@ -103,6 +112,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
|
|
|
103
112
|
inputClassName,
|
|
104
113
|
id,
|
|
105
114
|
disabled,
|
|
115
|
+
required,
|
|
106
116
|
...rest
|
|
107
117
|
},
|
|
108
118
|
ref,
|
|
@@ -138,6 +148,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
|
|
|
138
148
|
ref={setRef}
|
|
139
149
|
id={bare ? id : inputId}
|
|
140
150
|
disabled={disabled}
|
|
151
|
+
required={required}
|
|
141
152
|
{...rest}
|
|
142
153
|
// After the spread, like Switch: a props object spread at a checkbox must not
|
|
143
154
|
// be able to turn it into something else.
|
|
@@ -203,6 +214,11 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Che
|
|
|
203
214
|
)}
|
|
204
215
|
>
|
|
205
216
|
{label}
|
|
217
|
+
{required && (
|
|
218
|
+
<span aria-hidden className="ms-0.5 text-[var(--danger)]">
|
|
219
|
+
*
|
|
220
|
+
</span>
|
|
221
|
+
)}
|
|
206
222
|
</label>
|
|
207
223
|
)}
|
|
208
224
|
{showDescription && (
|