@eifi1/ui-kit 0.6.1 → 0.7.1
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 +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -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/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 +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -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 +5 -0
- package/dist/components/number-input.d.ts +5 -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 +5 -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/settings-fields.d.ts +2 -0
- 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/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- 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/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.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 +36 -8
- package/dist/components/ui.js +27 -19
- 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 +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -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 +45 -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 +45 -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 +45 -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 +45 -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 +45 -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 +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- 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.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- 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 +134 -85
- 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 +110 -24
- 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/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 +74 -5
- 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/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- 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 +201 -130
- 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}
|
|
@@ -246,16 +305,16 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
246
305
|
}
|
|
247
306
|
onKeyDown?.(e);
|
|
248
307
|
if (e.defaultPrevented) return;
|
|
249
|
-
|
|
308
|
+
// Disabled rows are passed over; at either end the highlight stays put.
|
|
250
309
|
if (e.key === "ArrowDown") {
|
|
251
310
|
e.preventDefault();
|
|
252
311
|
if (!open) setOpen(true);
|
|
253
|
-
else setActive((i) =>
|
|
312
|
+
else setActive((i) => stepEnabled(results, i, 1));
|
|
254
313
|
} else if (e.key === "ArrowUp") {
|
|
255
314
|
e.preventDefault();
|
|
256
315
|
if (!open) setOpen(true);
|
|
257
316
|
// From "nothing highlighted", Up goes to the bottom, as the APG has it.
|
|
258
|
-
else setActive((i) => (i < 0 ?
|
|
317
|
+
else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));
|
|
259
318
|
} else if (e.key === "Enter") {
|
|
260
319
|
// No row highlighted: the text is the answer, and a form's own submit
|
|
261
320
|
// is left alone.
|
|
@@ -270,16 +329,24 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
270
329
|
className={cn(
|
|
271
330
|
FIELD_BASE,
|
|
272
331
|
hasLabel && FIELD_FLOATING_PAD,
|
|
273
|
-
|
|
274
|
-
|
|
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"),
|
|
275
337
|
field.isInvalid && FIELD_INVALID,
|
|
276
338
|
inputClassName,
|
|
277
339
|
)}
|
|
278
340
|
/>
|
|
279
341
|
{busy && live && (
|
|
280
342
|
// Decorative: the live region below already says "Loading…".
|
|
281
|
-
<span
|
|
282
|
-
|
|
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"} />
|
|
283
350
|
</span>
|
|
284
351
|
)}
|
|
285
352
|
</div>
|
|
@@ -305,6 +372,9 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
305
372
|
// Every press inside the list keeps focus in the field — rows, the status
|
|
306
373
|
// line, the scrollbar — or the input's blur would close the list first.
|
|
307
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}
|
|
308
378
|
onMouseDown={(e) => e.preventDefault()}
|
|
309
379
|
className="fixed z-50 flex flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg"
|
|
310
380
|
style={{ top, left: rect.left, width: rect.width, maxHeight }}
|
|
@@ -340,12 +410,22 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
340
410
|
// field holds no value, so the only row that can be chosen
|
|
341
411
|
// is the one whose label the text already is.
|
|
342
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}
|
|
343
416
|
tabIndex={-1}
|
|
344
417
|
onClick={() => take(o)}
|
|
345
|
-
onMouseEnter={() =>
|
|
418
|
+
onMouseEnter={() => {
|
|
419
|
+
if (!o.disabled) setActive(i);
|
|
420
|
+
}}
|
|
346
421
|
className={cn(
|
|
347
|
-
"flex w-full items-center gap-2 px-3
|
|
348
|
-
|
|
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)]",
|
|
349
429
|
o.group != null && "ps-6",
|
|
350
430
|
)}
|
|
351
431
|
>
|
|
@@ -359,7 +439,12 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
359
439
|
{o.label}
|
|
360
440
|
</span>
|
|
361
441
|
{o.sublabel && (
|
|
362
|
-
<span
|
|
442
|
+
<span
|
|
443
|
+
className={cn(
|
|
444
|
+
"block truncate text-[var(--text-placeholder)]",
|
|
445
|
+
small ? "text-[11px]" : "text-xs",
|
|
446
|
+
)}
|
|
447
|
+
>
|
|
363
448
|
{o.sublabel}
|
|
364
449
|
</span>
|
|
365
450
|
)}
|
|
@@ -376,7 +461,7 @@ function AutocompleteInner<V extends string | number = string>(
|
|
|
376
461
|
<div
|
|
377
462
|
aria-hidden
|
|
378
463
|
className={cn(
|
|
379
|
-
"px-3 py-2 text-sm",
|
|
464
|
+
small ? "px-3 py-1.5 text-xs" : "px-3 py-2 text-sm",
|
|
380
465
|
failed && !hasStatus ? "text-[var(--danger)]" : "text-[var(--text-muted)]",
|
|
381
466
|
results.length > 0 && "border-t border-[var(--border)] text-xs",
|
|
382
467
|
)}
|
|
@@ -411,7 +496,8 @@ AutocompleteBase.displayName = "Autocomplete";
|
|
|
411
496
|
*
|
|
412
497
|
* Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted
|
|
413
498
|
* row, `aria-controls` the list, and a polite live region says what the list now
|
|
414
|
-
* holds. Keyboard: ↓/↑ move (↓ opens)
|
|
499
|
+
* holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter
|
|
500
|
+
* takes the highlighted row (with none,
|
|
415
501
|
* Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay
|
|
416
502
|
* with the caret. An Escape that closes an open list is consumed and never reaches the
|
|
417
503
|
* caller; with the list closed it does. For every other key a caller's `onKeyDown`
|
|
@@ -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);
|