@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ArrowUpDown, Minus, Plus } from "lucide-react";
|
|
2
|
+
import { Chip } from "./chip";
|
|
3
|
+
import { Tooltip } from "./tooltip";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
6
|
+
|
|
7
|
+
// ── Labels ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** The words a {@link SignChip} says — the `signChip` namespace of
|
|
10
|
+
* `<UiKitProvider labels>`, and per chip through its `labels` prop. */
|
|
11
|
+
export interface SignChipLabels {
|
|
12
|
+
/** The chip's text while the figure is negative — money going out. */
|
|
13
|
+
outflow: string;
|
|
14
|
+
/** The chip's text while the figure is positive — money coming in. */
|
|
15
|
+
inflow: string;
|
|
16
|
+
/** The chip's accessible name and its tooltip: the state it is in, and what a press
|
|
17
|
+
* turns it into — `current` and `next` are the two words above. The other option is
|
|
18
|
+
* nowhere on screen to be read, so the name has to say both. */
|
|
19
|
+
direction: (current: string, next: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_SIGN_CHIP_LABELS: SignChipLabels = {
|
|
23
|
+
outflow: "Outflow",
|
|
24
|
+
inflow: "Inflow",
|
|
25
|
+
direction: (current, next) => `Direction: ${current} — switch to ${next}`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface SignChipProps {
|
|
29
|
+
/** The figure is negative (an outflow) — the same flag {@link AmountInput}'s
|
|
30
|
+
* `negative` is, so one state feeds both: `<AmountInput negative={neg}
|
|
31
|
+
* onNegativeChange={setNeg} />` and `<SignChip negative={neg}
|
|
32
|
+
* onNegativeChange={setNeg} />`. */
|
|
33
|
+
negative: boolean;
|
|
34
|
+
/** A press: the other direction. */
|
|
35
|
+
onNegativeChange: (negative: boolean) => void;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
/** Per-chip overrides of the `signChip` namespace. */
|
|
38
|
+
labels?: Partial<SignChipLabels>;
|
|
39
|
+
/** On the chip. Place it in its row from here — keksdose's form passes `self-end`
|
|
40
|
+
* to sit it on the display figure's baseline strip. */
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The SIGN of an amount, as one chip that shows only the state it is in — keksdose
|
|
46
|
+
* K15, its `DirectionToggle` (feedback #417, #430, #431, dev#434) made a kit piece,
|
|
47
|
+
* so the figure and its direction come from one package and wear one palette.
|
|
48
|
+
*
|
|
49
|
+
* WHY A CHIP, NOT A SEGMENTED CONTROL. A two-option toggle spends half its width
|
|
50
|
+
* telling you what the figure is NOT, and at a phone's width it was the widest thing on
|
|
51
|
+
* the form after the figure itself (#417). This sits beside the amount and reads as one
|
|
52
|
+
* phrase with it — "−42,50 Outflow" — and a press flips it. Hiding the alternative costs
|
|
53
|
+
* discoverability, so two things pay it back: the ± glyph and the swap arrows say "this
|
|
54
|
+
* is a control", not a badge; and the name and tooltip spell out both the current state
|
|
55
|
+
* and what a press will do.
|
|
56
|
+
*
|
|
57
|
+
* NOT A PRESSED/UNPRESSED TOGGLE. A {@link Chip} toggle has a FIXED label naming its
|
|
58
|
+
* on-state, with `aria-pressed` carrying the state. Here the word IS the state, so it
|
|
59
|
+
* is an action button with no `aria-pressed`: "Direction: Inflow, not pressed" would
|
|
60
|
+
* name no on-state for "not pressed" to be the absence of. The name changing under
|
|
61
|
+
* focus is the echo.
|
|
62
|
+
*
|
|
63
|
+
* THE COLOUR is the money pairing — the chip's `expense` / `income` tones, the same
|
|
64
|
+
* `--money-expense` / `--money-income` that {@link AmountInput}'s `tone` paints the
|
|
65
|
+
* figure with (dev#434): only the text and the border carry the tint, on a neutral
|
|
66
|
+
* surface, because a filled pill next to a tinted figure reads as one smear.
|
|
67
|
+
*
|
|
68
|
+
* SIZE. `lg` on a phone — the 44px touch target beside a display figure that has no
|
|
69
|
+
* field height to match — and from `md` up exactly a labelled field's 42px, so it never
|
|
70
|
+
* stands taller than the fields in its grid row (#431).
|
|
71
|
+
*
|
|
72
|
+
* An exported chip rather than an `AmountInput signControl` slot: the slot would have
|
|
73
|
+
* to own the row the two share, and every form lays that row out differently (beside
|
|
74
|
+
* the figure on a phone, in a grid column on a desktop). The chip is the part that was
|
|
75
|
+
* being re-written; the row stays the form's.
|
|
76
|
+
*/
|
|
77
|
+
export function SignChip({ negative, onNegativeChange, disabled, labels: labelsProp, className }: SignChipProps) {
|
|
78
|
+
const labels = useKitLabels("signChip", DEFAULT_SIGN_CHIP_LABELS, labelsProp);
|
|
79
|
+
const current = negative ? labels.outflow : labels.inflow;
|
|
80
|
+
const next = negative ? labels.inflow : labels.outflow;
|
|
81
|
+
const name = labels.direction(current, next);
|
|
82
|
+
return (
|
|
83
|
+
<Tooltip label={name} lazy>
|
|
84
|
+
<Chip
|
|
85
|
+
size="lg"
|
|
86
|
+
tone={negative ? "expense" : "income"}
|
|
87
|
+
icon={negative ? Minus : Plus}
|
|
88
|
+
onClick={() => onNegativeChange(!negative)}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
aria-label={name}
|
|
91
|
+
// `gap-1.5 px-3`: the chip's narrower body, because on a 375px screen every
|
|
92
|
+
// pixel of it is taken from the figure beside it (#430). `md:py-2.5` with the
|
|
93
|
+
// hairline border is the 42px a labelled field measures (#431).
|
|
94
|
+
className={cn("shrink-0 gap-1.5 whitespace-nowrap px-3 font-medium md:min-h-0 md:py-2.5", className)}
|
|
95
|
+
>
|
|
96
|
+
{current}
|
|
97
|
+
{/* The swap arrows after the word, in the label's own row: decoration (the
|
|
98
|
+
name already says what a press does), set a little apart from the word. */}
|
|
99
|
+
<ArrowUpDown className="ms-1.5 inline size-3.5 align-[-0.2em] opacity-60" aria-hidden />
|
|
100
|
+
</Chip>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useId, useMemo } from "react";
|
|
2
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
5
|
import { TILE_SIZE, TileRadioGroup } from "./tile-radio";
|
|
6
6
|
import type { TileItem, TileSize } from "./tile-radio";
|
|
7
|
+
import { Tooltip } from "./tooltip";
|
|
8
|
+
import { useCommitReason } from "./write-lock";
|
|
9
|
+
import { hasContent, LabelStrip } from "./field-parts";
|
|
7
10
|
|
|
8
11
|
export interface SwatchPickerLabels {
|
|
9
12
|
/** The "no colour" tile's name, when `allowNone` is set. */
|
|
@@ -35,7 +38,23 @@ export interface SwatchOption<T extends string> {
|
|
|
35
38
|
/** A remark read after the name and shown under it in the bubble ("used by
|
|
36
39
|
* Groceries"). The tile wears a dot while it has one. */
|
|
37
40
|
note?: string;
|
|
41
|
+
/** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a
|
|
42
|
+
* `disabledReason` the tile stays reachable instead and says why. */
|
|
38
43
|
disabled?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Why THIS colour cannot be chosen right now — the whole picker's `disabledReason`,
|
|
46
|
+
* one tile at a time (the kit's later list after keksdose K3: a colour another
|
|
47
|
+
* category already wears, a flag only an owner may set). {@link Button}'s rule: a tile
|
|
48
|
+
* that is merely `disabled` is a dead square a keyboard never lands on, so nobody
|
|
49
|
+
* learns why it is dimmed.
|
|
50
|
+
*
|
|
51
|
+
* The tile looks disabled but is `aria-disabled`, not `disabled`: Tab and the arrow
|
|
52
|
+
* keys still reach it, its bubble shows the reason under its name, and the reason
|
|
53
|
+
* describes it. Choosing it — a click, Space or Enter, or an arrow under automatic
|
|
54
|
+
* activation — does nothing: the focus moves onto it and the checked colour stays
|
|
55
|
+
* where it was, as under the whole-picker lock. Wins over `disabled`.
|
|
56
|
+
*/
|
|
57
|
+
disabledReason?: ReactNode;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
/**
|
|
@@ -72,6 +91,31 @@ export interface SwatchPickerProps<T extends string>
|
|
|
72
91
|
tileClassName?: string;
|
|
73
92
|
disabled?: boolean;
|
|
74
93
|
labels?: Partial<SwatchPickerLabels>;
|
|
94
|
+
/**
|
|
95
|
+
* An 11px static label in a strip over the swatches, named by it (keksdose K2): the
|
|
96
|
+
* transaction row's flag picker stands between labelled fields and hand-builds this
|
|
97
|
+
* (`FieldLabel` + `pt-5`, transaction-flags.tsx:155) because the picker had no label.
|
|
98
|
+
* The label sits on the same line as a field's own (`top-1`, `inset-x-3`); the
|
|
99
|
+
* swatches start 20px down, below its whole line box — see `LABEL_STRIP_PAD`. With a
|
|
100
|
+
* label, `className` styles the wrapper, as on a labelled {@link ToggleGroup}; an
|
|
101
|
+
* explicit `aria-label` / `aria-labelledby` still names the group instead.
|
|
102
|
+
*/
|
|
103
|
+
label?: ReactNode;
|
|
104
|
+
/** A {@link FieldHint} beside the label. Only with `label`. */
|
|
105
|
+
hint?: ReactNode;
|
|
106
|
+
/**
|
|
107
|
+
* Why the colour cannot be changed right now — {@link Button}'s `disabledReason`, for
|
|
108
|
+
* a picker that saves itself on change (keksdose K3, the flag picker in a row a
|
|
109
|
+
* read-only viewer may see). The swatches stay reachable and the arrow keys still
|
|
110
|
+
* walk them, but nothing reaches `onChange`; the reason is shown in the kit
|
|
111
|
+
* {@link Tooltip} under the row (each swatch's own name bubble sits above it) and
|
|
112
|
+
* describes the group. Wins over `disabled`.
|
|
113
|
+
*/
|
|
114
|
+
disabledReason?: ReactNode;
|
|
115
|
+
/** This picker COMMITS — choosing a colour saves it. Under a locked
|
|
116
|
+
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
117
|
+
* reason, as {@link Button}'s `commit` does. */
|
|
118
|
+
commit?: boolean;
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
@@ -97,51 +141,113 @@ export function SwatchPicker<T extends string>({
|
|
|
97
141
|
tileClassName,
|
|
98
142
|
disabled = false,
|
|
99
143
|
labels,
|
|
144
|
+
label,
|
|
145
|
+
hint,
|
|
146
|
+
disabledReason: ownDisabledReason,
|
|
147
|
+
commit,
|
|
100
148
|
className,
|
|
101
149
|
...rest
|
|
102
150
|
}: SwatchPickerProps<T>) {
|
|
103
151
|
const text = useKitLabels("swatchPicker", DEFAULT_SWATCH_PICKER_LABELS, labels);
|
|
104
152
|
const mixedId = useId();
|
|
153
|
+
const labelId = useId();
|
|
154
|
+
const reasonId = useId();
|
|
155
|
+
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
156
|
+
const locked = hasContent(disabledReason);
|
|
157
|
+
const labelled = hasContent(label);
|
|
105
158
|
const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);
|
|
106
159
|
const items: TileItem<T>[] = [
|
|
107
160
|
...(allowNone ? [{ key: null, label: text.none }] : []),
|
|
108
|
-
...options.map((o) => ({
|
|
161
|
+
...options.map((o) => ({
|
|
162
|
+
key: o.value,
|
|
163
|
+
label: o.label,
|
|
164
|
+
note: o.note,
|
|
165
|
+
disabled: o.disabled,
|
|
166
|
+
disabledReason: o.disabledReason,
|
|
167
|
+
})),
|
|
109
168
|
];
|
|
110
|
-
|
|
169
|
+
const tiles = (
|
|
170
|
+
<TileRadioGroup
|
|
171
|
+
items={items}
|
|
172
|
+
checked={mixed ? undefined : value}
|
|
173
|
+
// A locked picker saves on change, so the change is swallowed — the arrow keys
|
|
174
|
+
// still move the focus (and, automatic, would choose), and nothing is chosen. A
|
|
175
|
+
// locked TILE is refused by the tiles themselves; asked again here so a reason
|
|
176
|
+
// can never become a change, whatever the tiles do.
|
|
177
|
+
onSelect={(next) => {
|
|
178
|
+
if (!locked && !hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);
|
|
179
|
+
}}
|
|
180
|
+
activation={activation}
|
|
181
|
+
// A reason wins over `disabled`: the tiles stay in the tab order.
|
|
182
|
+
disabled={disabled && !locked}
|
|
183
|
+
size={size}
|
|
184
|
+
tileClassName={
|
|
185
|
+
tileClassName || locked
|
|
186
|
+
? (_item, selected) =>
|
|
187
|
+
cn(
|
|
188
|
+
// The disabled tile's look, without its `disabled`: dimmed, a
|
|
189
|
+
// not-allowed cursor, and no hover offer on a tile that cannot be
|
|
190
|
+
// chosen. Not on the selected tile, whose frame IS its state.
|
|
191
|
+
locked && "cursor-not-allowed opacity-50",
|
|
192
|
+
locked && !selected && "hover:border-[var(--border)] hover:bg-[var(--bg-surface)]",
|
|
193
|
+
tileClassName,
|
|
194
|
+
)
|
|
195
|
+
: undefined
|
|
196
|
+
}
|
|
197
|
+
renderTile={(item) => {
|
|
198
|
+
const opt = item.key === null ? undefined : byValue.get(item.key);
|
|
199
|
+
return (
|
|
200
|
+
<span
|
|
201
|
+
aria-hidden
|
|
202
|
+
// A hairline round the dot, drawn over whatever colour it is: a pale
|
|
203
|
+
// yellow swatch on a white surface is otherwise a hole in the row.
|
|
204
|
+
className={cn(
|
|
205
|
+
"rounded-full ring-1 ring-inset ring-black/10 dark:ring-white/15",
|
|
206
|
+
TILE_SIZE[size].swatch,
|
|
207
|
+
opt?.swatchClassName,
|
|
208
|
+
)}
|
|
209
|
+
style={{ background: opt?.color }}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
const group = (
|
|
111
216
|
<div
|
|
112
217
|
{...rest}
|
|
113
218
|
role="radiogroup"
|
|
114
|
-
aria-
|
|
219
|
+
aria-labelledby={
|
|
220
|
+
labelled && rest["aria-label"] === undefined && rest["aria-labelledby"] === undefined
|
|
221
|
+
? labelId
|
|
222
|
+
: rest["aria-labelledby"]
|
|
223
|
+
}
|
|
224
|
+
aria-disabled={disabled || locked || undefined}
|
|
115
225
|
aria-describedby={
|
|
116
|
-
[rest["aria-describedby"], mixed && mixedId].filter(Boolean).join(" ") ||
|
|
226
|
+
[rest["aria-describedby"], mixed && mixedId, locked && reasonId].filter(Boolean).join(" ") ||
|
|
227
|
+
undefined
|
|
117
228
|
}
|
|
118
|
-
className={cn("flex flex-wrap items-center gap-1.5", className)}
|
|
229
|
+
className={cn("flex flex-wrap items-center gap-1.5", !labelled && className)}
|
|
119
230
|
>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
style={{ background: opt?.color }}
|
|
141
|
-
/>
|
|
142
|
-
);
|
|
143
|
-
}}
|
|
144
|
-
/>
|
|
231
|
+
{locked ? (
|
|
232
|
+
// Inside the group, round the tiles — not round the group, whose box is the
|
|
233
|
+
// caller's row item. This row inherits the group's gap, wrap and alignment, so
|
|
234
|
+
// the tiles sit exactly where they sit unlocked. Below the row: each tile's own
|
|
235
|
+
// bubble (its colour's name) is above it.
|
|
236
|
+
<Tooltip
|
|
237
|
+
label={disabledReason}
|
|
238
|
+
side="bottom"
|
|
239
|
+
className="flex min-w-0 flex-1 gap-[inherit] [align-items:inherit] [flex-wrap:inherit] [justify-content:inherit]"
|
|
240
|
+
>
|
|
241
|
+
<>
|
|
242
|
+
{tiles}
|
|
243
|
+
<span id={reasonId} hidden>
|
|
244
|
+
{disabledReason}
|
|
245
|
+
</span>
|
|
246
|
+
</>
|
|
247
|
+
</Tooltip>
|
|
248
|
+
) : (
|
|
249
|
+
tiles
|
|
250
|
+
)}
|
|
145
251
|
{/* `hidden`, not `sr-only`: a description is read from hidden text, and this
|
|
146
252
|
way it takes no room in the row. */}
|
|
147
253
|
{mixed && (
|
|
@@ -151,4 +257,10 @@ export function SwatchPicker<T extends string>({
|
|
|
151
257
|
)}
|
|
152
258
|
</div>
|
|
153
259
|
);
|
|
260
|
+
if (!labelled) return group;
|
|
261
|
+
return (
|
|
262
|
+
<LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled || locked} pad="clear" className={className}>
|
|
263
|
+
{group}
|
|
264
|
+
</LabelStrip>
|
|
265
|
+
);
|
|
154
266
|
}
|
|
@@ -2,6 +2,7 @@ import { forwardRef, useId } from "react";
|
|
|
2
2
|
import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
5
|
+
import { LockedReason, useLockReason } from "./field-parts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* An on/off switch that is still `<input type="checkbox">`, with `role="switch"`.
|
|
@@ -79,6 +80,25 @@ export interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>,
|
|
|
79
80
|
* label WITHOUT a literal "*". No mark without a label.
|
|
80
81
|
*/
|
|
81
82
|
required?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Why the switch cannot be flipped — {@link Button}'s `disabledReason`. A switch is
|
|
85
|
+
* the control that "takes effect the moment it is flipped", so it is the first one
|
|
86
|
+
* keksdose K3 names: its settings rows sat behind a `SaveGuard` that forced a native
|
|
87
|
+
* `disabled`, out of the tab order, so the reason never reached a keyboard.
|
|
88
|
+
*
|
|
89
|
+
* With a reason the switch is `aria-disabled` instead: still focusable, a click or
|
|
90
|
+
* Space flips nothing and fires no `onChange` / `onCheckedChange`, the reason is in
|
|
91
|
+
* the kit {@link Tooltip} on the track and on its `aria-describedby`, and the row
|
|
92
|
+
* fades as a disabled one does. It wins over `disabled`. Changing the lock mounts the
|
|
93
|
+
* switch anew (it moves in or out of the Tooltip), as a locked Button is.
|
|
94
|
+
*/
|
|
95
|
+
disabledReason?: ReactNode;
|
|
96
|
+
/**
|
|
97
|
+
* This switch COMMITS — flipping it saves. Under a locked {@link WriteLockProvider} it
|
|
98
|
+
* is locked the `disabledReason` way with the lock's reason (which wins over its
|
|
99
|
+
* own). No provider, or an unlocked one: no effect. Button's `commit`, for K3.
|
|
100
|
+
*/
|
|
101
|
+
commit?: boolean;
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
/**
|
|
@@ -101,39 +121,70 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(
|
|
|
101
121
|
id,
|
|
102
122
|
disabled,
|
|
103
123
|
required,
|
|
124
|
+
disabledReason,
|
|
125
|
+
commit,
|
|
126
|
+
onClick,
|
|
104
127
|
...rest
|
|
105
128
|
},
|
|
106
129
|
ref,
|
|
107
130
|
) {
|
|
108
131
|
const generated = useId();
|
|
109
132
|
const inputId = id ?? generated;
|
|
133
|
+
const lock = useLockReason(commit, disabledReason);
|
|
134
|
+
const locked = lock.locked;
|
|
135
|
+
// Locked is disabled the focusable way — the row fades and the words stop inviting a
|
|
136
|
+
// click, as for `disabled`.
|
|
137
|
+
const looksDisabled = Boolean(disabled) || locked;
|
|
110
138
|
const descriptionId = `${inputId}-description`;
|
|
111
139
|
const showDescription = hasMessage(description);
|
|
112
140
|
const bare = label === undefined && !showDescription;
|
|
113
141
|
const geometry = SIZES[size];
|
|
114
142
|
|
|
115
|
-
const
|
|
116
|
-
<span className={cn("relative inline-flex shrink-0", bare && className)}>
|
|
143
|
+
const track = (
|
|
144
|
+
<span className={cn("relative inline-flex shrink-0", bare && !locked && className)}>
|
|
117
145
|
<input
|
|
118
146
|
ref={ref}
|
|
119
147
|
id={bare ? id : inputId}
|
|
120
|
-
disabled
|
|
148
|
+
// A reason wins over `disabled`, as on Button: the switch stays reachable.
|
|
149
|
+
disabled={locked ? undefined : disabled}
|
|
121
150
|
required={required}
|
|
122
151
|
{...rest}
|
|
123
152
|
// After the spread: a caller's props object must not be able to turn this
|
|
124
153
|
// back into a plain checkbox, or into a text field.
|
|
125
154
|
type="checkbox"
|
|
126
155
|
role="switch"
|
|
127
|
-
aria-
|
|
156
|
+
aria-disabled={locked || rest["aria-disabled"] || undefined}
|
|
157
|
+
aria-describedby={mergeDescribedBy(
|
|
158
|
+
rest["aria-describedby"],
|
|
159
|
+
showDescription && descriptionId,
|
|
160
|
+
locked && lock.reasonId,
|
|
161
|
+
)}
|
|
162
|
+
// A locked switch's click is swallowed, as a locked Button's is.
|
|
163
|
+
onClick={locked ? undefined : onClick}
|
|
128
164
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
165
|
+
if (locked) {
|
|
166
|
+
// Put the thumb back and report nothing — see Checkbox for why here and not
|
|
167
|
+
// by cancelling the click. Label clicks and Space arrive as clicks too.
|
|
168
|
+
e.currentTarget.checked = !e.currentTarget.checked;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
129
171
|
onChange?.(e);
|
|
130
172
|
onCheckedChange?.(e.target.checked);
|
|
131
173
|
}}
|
|
132
|
-
className={cn(TRACK, geometry.track, inputClassName)}
|
|
174
|
+
className={cn(TRACK, geometry.track, locked && "cursor-not-allowed", inputClassName)}
|
|
133
175
|
/>
|
|
134
176
|
<span aria-hidden className={cn(THUMB, geometry.thumb)} />
|
|
135
177
|
</span>
|
|
136
178
|
);
|
|
179
|
+
// The Tooltip goes round the track, not the row — the row is the caller's layout. A
|
|
180
|
+
// bare switch hands its `className` to the wrapper, then the outermost element.
|
|
181
|
+
const control = locked ? (
|
|
182
|
+
<LockedReason lock={lock} className={cn("shrink-0", bare && className)}>
|
|
183
|
+
{track}
|
|
184
|
+
</LockedReason>
|
|
185
|
+
) : (
|
|
186
|
+
track
|
|
187
|
+
);
|
|
137
188
|
|
|
138
189
|
if (bare) return control;
|
|
139
190
|
|
|
@@ -148,7 +199,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(
|
|
|
148
199
|
htmlFor={inputId}
|
|
149
200
|
className={cn(
|
|
150
201
|
"block text-sm leading-5 text-[var(--text-primary)]",
|
|
151
|
-
|
|
202
|
+
looksDisabled ? "cursor-not-allowed" : "cursor-pointer select-none",
|
|
152
203
|
)}
|
|
153
204
|
>
|
|
154
205
|
{label}
|
|
@@ -168,7 +219,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(
|
|
|
168
219
|
);
|
|
169
220
|
|
|
170
221
|
return (
|
|
171
|
-
<div className={cn("flex items-start gap-3",
|
|
222
|
+
<div className={cn("flex items-start gap-3", looksDisabled && "opacity-60", className)}>
|
|
172
223
|
{switchPosition === "end" ? (
|
|
173
224
|
<>
|
|
174
225
|
{words}
|