@eifi1/ui-kit 0.11.0 → 0.13.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 +59 -34
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +40 -4
- package/dist/components/amount-input.js +50 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +125 -27
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -4
- package/dist/components/list.js +35 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +42 -20
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +5 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +122 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +164 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +161 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +161 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +161 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +161 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +283 -0
- package/dist/rhf/fields.js +631 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +82 -19
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -6
- package/tokens.css +12 -0
|
@@ -57,6 +57,15 @@ interface NumberInputProps {
|
|
|
57
57
|
hint?: ReactNode;
|
|
58
58
|
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
59
59
|
invalid?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
|
|
62
|
+
* `{(ids) => <NumberInput {...ids} … />}` — is typed and reaches the input: the hint
|
|
63
|
+
* and error are described, a required number is announced as required, and
|
|
64
|
+
* `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.
|
|
65
|
+
*/
|
|
66
|
+
"aria-describedby"?: string;
|
|
67
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
68
|
+
"aria-required"?: boolean | "true" | "false";
|
|
60
69
|
/**
|
|
61
70
|
* A static unit shown at the field's right edge — "%", "kg", "km/h".
|
|
62
71
|
*
|
|
@@ -156,7 +165,10 @@ export function NumberInput({
|
|
|
156
165
|
calculator = true,
|
|
157
166
|
variant = "field",
|
|
158
167
|
hint,
|
|
159
|
-
invalid,
|
|
168
|
+
invalid: invalidProp,
|
|
169
|
+
"aria-describedby": ariaDescribedBy,
|
|
170
|
+
"aria-invalid": ariaInvalid,
|
|
171
|
+
"aria-required": ariaRequired,
|
|
160
172
|
suffix,
|
|
161
173
|
step,
|
|
162
174
|
min,
|
|
@@ -164,6 +176,7 @@ export function NumberInput({
|
|
|
164
176
|
}: NumberInputProps) {
|
|
165
177
|
const generatedId = useId();
|
|
166
178
|
const fieldId = id ?? generatedId;
|
|
179
|
+
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
167
180
|
const labelled = label !== undefined;
|
|
168
181
|
// On phones we suppress the OS keyboard (inputMode="none" below) for our own
|
|
169
182
|
// calculator numpad, so the desktop popover trigger is hidden (feedback #334).
|
|
@@ -219,6 +232,7 @@ export function NumberInput({
|
|
|
219
232
|
// keeps focus/caret); desktop keeps the native decimal keypad.
|
|
220
233
|
inputMode={isMobile ? "none" : "decimal"}
|
|
221
234
|
autoComplete="off"
|
|
235
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
222
236
|
autoFocus={autoFocus}
|
|
223
237
|
disabled={disabled}
|
|
224
238
|
// Display mode has no floating label to feed the blank-placeholder trick,
|
|
@@ -234,6 +248,8 @@ export function NumberInput({
|
|
|
234
248
|
}}
|
|
235
249
|
onKeyDown={onKeyDown}
|
|
236
250
|
aria-invalid={invalid || undefined}
|
|
251
|
+
aria-describedby={ariaDescribedBy}
|
|
252
|
+
aria-required={ariaRequired}
|
|
237
253
|
// The end padding sits last so it always wins over an inputClassName that
|
|
238
254
|
// sets its own px. Both trailing controls can be on at once, so the room
|
|
239
255
|
// they need is reserved together rather than by whichever happens to render:
|
|
@@ -99,11 +99,16 @@ export function NumberPadSheet({
|
|
|
99
99
|
onDone,
|
|
100
100
|
label,
|
|
101
101
|
labels,
|
|
102
|
+
decimalMark = ".",
|
|
102
103
|
}: {
|
|
103
104
|
value: string;
|
|
104
105
|
/** Fired with the raw (sanitised) field text on every key — same contract as
|
|
105
106
|
* the host field's own `onChange`. */
|
|
106
107
|
onChange: (value: string) => void;
|
|
108
|
+
/** The glyph on the decimal key — "," where the host shows a comma (AmountInput
|
|
109
|
+
* in fr-CH, kastlan 40). The key still inserts a dot; the host's text is dot-form
|
|
110
|
+
* underneath and `sanitizeLive` reads either. */
|
|
111
|
+
decimalMark?: "." | ",";
|
|
107
112
|
/** Fired on "Done": the host blurs the input, which commits (evaluates) and
|
|
108
113
|
* unmounts the sheet via its existing blur handler. */
|
|
109
114
|
onDone: () => void;
|
|
@@ -231,7 +236,7 @@ export function NumberPadSheet({
|
|
|
231
236
|
onClick={() => insert(key.ins)}
|
|
232
237
|
className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}
|
|
233
238
|
>
|
|
234
|
-
{key.label}
|
|
239
|
+
{key.kind === "ins" && key.name === "decimal" ? decimalMark : key.label}
|
|
235
240
|
</button>
|
|
236
241
|
),
|
|
237
242
|
)}
|
|
@@ -2,21 +2,44 @@ import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
import { SECTION_LABEL_CLASS } from "./text";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a
|
|
7
|
+
* phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).
|
|
8
|
+
*
|
|
9
|
+
* `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at
|
|
10
|
+
* EVERY width — budgets, rules, accounts, transactions and budget pages all write
|
|
11
|
+
* exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to
|
|
12
|
+
* `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's
|
|
13
|
+
* titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is
|
|
14
|
+
* one: the letters are taken in order, and there is none between `sm` and `md` — an
|
|
15
|
+
* `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is
|
|
16
|
+
* not.
|
|
17
|
+
*/
|
|
18
|
+
export type PageHeaderSize = "sm" | "compact" | "md";
|
|
8
19
|
|
|
9
20
|
const TITLE: Record<PageHeaderSize, string> = {
|
|
10
21
|
sm: "text-lg font-semibold",
|
|
22
|
+
compact: "text-xl font-semibold",
|
|
11
23
|
md: "text-xl font-semibold tracking-tight sm:text-2xl",
|
|
12
24
|
};
|
|
13
25
|
|
|
26
|
+
/**
|
|
27
|
+
* How the title block and the actions sit below the `sm` breakpoint. From `sm` up both
|
|
28
|
+
* layouts are one row: title block at the start, actions at the end.
|
|
29
|
+
*/
|
|
30
|
+
export type PageHeaderMobileLayout = "stacked" | "inline";
|
|
31
|
+
|
|
32
|
+
const ROW: Record<PageHeaderMobileLayout, string> = {
|
|
33
|
+
stacked: "flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
|
|
34
|
+
inline: "flex-row items-center justify-between gap-2 sm:gap-4",
|
|
35
|
+
};
|
|
36
|
+
|
|
14
37
|
export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
15
38
|
title: ReactNode;
|
|
16
39
|
/** A sentence under the title. */
|
|
17
40
|
description?: ReactNode;
|
|
18
41
|
/** Buttons for the page — "New", "Export". At the title's end from `sm` up; under it,
|
|
19
|
-
* wrapping, on a phone
|
|
42
|
+
* wrapping, on a phone — unless {@link mobileLayout} is `inline`. */
|
|
20
43
|
actions?: ReactNode;
|
|
21
44
|
/** A `Breadcrumbs` trail, drawn above everything else. */
|
|
22
45
|
breadcrumbs?: ReactNode;
|
|
@@ -27,6 +50,19 @@ export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
27
50
|
* header inside a page that already has its `h1` (a tab's own header). */
|
|
28
51
|
as?: "h1" | "h2" | "h3";
|
|
29
52
|
size?: PageHeaderSize;
|
|
53
|
+
/**
|
|
54
|
+
* `stacked` (default): on a phone the actions drop under the title and wrap, so a
|
|
55
|
+
* row of three buttons never pushes the title off the screen.
|
|
56
|
+
*
|
|
57
|
+
* `inline`: the actions stay beside the title on a phone too, vertically centred on
|
|
58
|
+
* it — keksdose's headers (transactions-page.tsx:515, `flex items-center
|
|
59
|
+
* justify-between gap-2`), which carry one or two icon-sized controls and were tuned
|
|
60
|
+
* for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the
|
|
61
|
+
* row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is
|
|
62
|
+
* for a header whose actions are known to fit beside a phone-width title (an icon
|
|
63
|
+
* button, a month picker); a long title wraps to make room for them instead.
|
|
64
|
+
*/
|
|
65
|
+
mobileLayout?: PageHeaderMobileLayout;
|
|
30
66
|
}
|
|
31
67
|
|
|
32
68
|
/**
|
|
@@ -48,6 +84,7 @@ export function PageHeader({
|
|
|
48
84
|
eyebrow,
|
|
49
85
|
as = "h1",
|
|
50
86
|
size = "md",
|
|
87
|
+
mobileLayout = "stacked",
|
|
51
88
|
className,
|
|
52
89
|
...rest
|
|
53
90
|
}: PageHeaderProps) {
|
|
@@ -55,7 +92,7 @@ export function PageHeader({
|
|
|
55
92
|
return (
|
|
56
93
|
<div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
57
94
|
{breadcrumbs}
|
|
58
|
-
<div className="flex min-w-0
|
|
95
|
+
<div className={cn("flex min-w-0", ROW[mobileLayout])}>
|
|
59
96
|
<div className="min-w-0 flex-1">
|
|
60
97
|
{eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
|
|
61
98
|
<Heading className={cn("break-words text-[var(--text-primary)]", TITLE[size])}>{title}</Heading>
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
|
+
import { KeyRound } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { useKitLocale } from "../i18n/kit-labels";
|
|
6
|
+
import { Button, Card, EmptyState, Input } from "./ui";
|
|
7
|
+
import { Skeleton } from "./skeleton";
|
|
8
|
+
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
9
|
+
import type { PasskeysSettingLabels } from "./account-settings-labels";
|
|
10
|
+
|
|
11
|
+
/** One registered passkey, as the app's API lists it. */
|
|
12
|
+
export interface PasskeyItem {
|
|
13
|
+
id: string | number;
|
|
14
|
+
name: string;
|
|
15
|
+
/** A `Date`, an ISO string or epoch ms. */
|
|
16
|
+
createdAt?: Date | string | number | null;
|
|
17
|
+
/** Null (or left out) reads as "Never used". */
|
|
18
|
+
lastUsedAt?: Date | string | number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PasskeysSettingProps {
|
|
22
|
+
/** `undefined` while the first load is in flight (or pass `loading`). */
|
|
23
|
+
passkeys: readonly PasskeyItem[] | undefined;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Run the WebAuthn ceremony and register the passkey — app-side, since the options
|
|
27
|
+
* and the attestation are the app's server's business. Called with the trimmed name
|
|
28
|
+
* field, which may be EMPTY: pick your own default ("Passkey", the device name).
|
|
29
|
+
* Return the promise and the field clears when it resolves; a rejection (including
|
|
30
|
+
* the user cancelling the OS prompt) keeps what was typed.
|
|
31
|
+
*/
|
|
32
|
+
onAdd: (name: string) => void | Promise<unknown>;
|
|
33
|
+
/** The ceremony is running. The add button says so and does not start a second one. */
|
|
34
|
+
adding?: boolean;
|
|
35
|
+
/** Offer "Rename" on each row. Return a promise to leave edit mode only on success. */
|
|
36
|
+
onRename?: (id: PasskeyItem["id"], name: string) => void | Promise<unknown>;
|
|
37
|
+
/** Offer "Delete" on each row — always behind an inline confirmation. */
|
|
38
|
+
onDelete?: (id: PasskeyItem["id"]) => void | Promise<unknown>;
|
|
39
|
+
/** A row whose rename or delete is in flight; its buttons are disabled. */
|
|
40
|
+
busyId?: PasskeyItem["id"] | null;
|
|
41
|
+
/** Show the name field above the add button. Default true. */
|
|
42
|
+
nameField?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Shown INSTEAD of the list and the add button — passkeys are not supported by this
|
|
45
|
+
* browser, or the page is not a secure context. Its wording is the app's: only the
|
|
46
|
+
* app knows which of those it detected.
|
|
47
|
+
*/
|
|
48
|
+
unavailable?: ReactNode;
|
|
49
|
+
/** A date for the "Added …" / "Last used …" lines. Default: the locale's medium date. */
|
|
50
|
+
formatDate?: (date: Date) => string;
|
|
51
|
+
/** Overrides the provider's locale for the default date format. */
|
|
52
|
+
locale?: string;
|
|
53
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { passkeys } }}>` > English. */
|
|
54
|
+
labels?: Partial<PasskeysSettingLabels>;
|
|
55
|
+
className?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The passkeys section of an account page: what is registered, when it was last used,
|
|
60
|
+
* rename and delete, and "Add passkey" — the fourth card beside `ProfileSetting`,
|
|
61
|
+
* `PasswordSetting` and `TwoFactorSetting`, and built the same way (a self-contained
|
|
62
|
+
* `<Card>` owning its transient form state; the app owns the data and the API).
|
|
63
|
+
*
|
|
64
|
+
* Lifted from kastlan's passkeys-card. **The WebAuthn ceremony stays app-side**:
|
|
65
|
+
* `navigator.credentials.create` needs options minted by the app's server and hands
|
|
66
|
+
* back an attestation only that server can verify, so the kit calls `onAdd` and waits.
|
|
67
|
+
*
|
|
68
|
+
* **Delete confirms inline**, in the row, rather than through `useConfirm`: this card
|
|
69
|
+
* must work in an app without a `ConfirmProvider`, and the question belongs next to
|
|
70
|
+
* the passkey it names. Focus moves to Cancel, as in every destructive confirm in the
|
|
71
|
+
* kit, so the Enter that pressed Delete cannot also answer it.
|
|
72
|
+
*/
|
|
73
|
+
export function PasskeysSetting({
|
|
74
|
+
passkeys,
|
|
75
|
+
loading,
|
|
76
|
+
onAdd,
|
|
77
|
+
adding = false,
|
|
78
|
+
onRename,
|
|
79
|
+
onDelete,
|
|
80
|
+
busyId,
|
|
81
|
+
nameField = true,
|
|
82
|
+
unavailable,
|
|
83
|
+
formatDate,
|
|
84
|
+
locale: localeProp,
|
|
85
|
+
labels: labelsProp,
|
|
86
|
+
className,
|
|
87
|
+
}: PasskeysSettingProps) {
|
|
88
|
+
const labels = useAccountSettingsLabels("passkeys", labelsProp);
|
|
89
|
+
const locale = useKitLocale(localeProp);
|
|
90
|
+
const [name, setName] = useState("");
|
|
91
|
+
|
|
92
|
+
const date = (value: PasskeyItem["createdAt"]): string | null => {
|
|
93
|
+
if (value === null || value === undefined || value === "") return null;
|
|
94
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
95
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
96
|
+
return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const add = () => {
|
|
100
|
+
if (adding) return;
|
|
101
|
+
const typed = name;
|
|
102
|
+
Promise.resolve(onAdd(typed.trim()))
|
|
103
|
+
// Clear only what was sent: the field stays editable while the OS prompt is up.
|
|
104
|
+
.then(() => setName((current) => (current === typed ? "" : current)))
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const isLoading = loading || passkeys === undefined;
|
|
109
|
+
const items = passkeys ?? [];
|
|
110
|
+
|
|
111
|
+
const body = (() => {
|
|
112
|
+
if (unavailable != null && unavailable !== false) {
|
|
113
|
+
return <EmptyState variant="inline" size="sm" icon={<KeyRound />} title={unavailable} />;
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
{isLoading ? (
|
|
118
|
+
<Skeleton lines={2} label={labels.loading} />
|
|
119
|
+
) : items.length === 0 ? (
|
|
120
|
+
<EmptyState variant="inline" size="sm" icon={<KeyRound />} title={labels.empty} />
|
|
121
|
+
) : (
|
|
122
|
+
<ul aria-label={labels.list} className="divide-y divide-[var(--border)]">
|
|
123
|
+
{items.map((item) => (
|
|
124
|
+
<PasskeyRow
|
|
125
|
+
key={item.id}
|
|
126
|
+
item={item}
|
|
127
|
+
created={date(item.createdAt)}
|
|
128
|
+
lastUsed={date(item.lastUsedAt)}
|
|
129
|
+
busy={busyId !== undefined && busyId !== null && busyId === item.id}
|
|
130
|
+
onRename={onRename}
|
|
131
|
+
onDelete={onDelete}
|
|
132
|
+
labels={labels}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
)}
|
|
137
|
+
<div className="space-y-2 pt-1">
|
|
138
|
+
{nameField && (
|
|
139
|
+
<Input
|
|
140
|
+
label={labels.nameLabel}
|
|
141
|
+
placeholder={labels.namePlaceholder}
|
|
142
|
+
value={name}
|
|
143
|
+
maxLength={120}
|
|
144
|
+
onChange={(e) => setName(e.target.value)}
|
|
145
|
+
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
|
146
|
+
if (e.key === "Enter") {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
add();
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
<Button onClick={add} disabled={adding} aria-busy={adding || undefined}>
|
|
154
|
+
<KeyRound className="size-4" aria-hidden />
|
|
155
|
+
{adding ? labels.adding : labels.add}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
})();
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Card className={cn("p-4 space-y-3", className)}>
|
|
164
|
+
<div>
|
|
165
|
+
<div className="text-sm font-medium">{labels.title}</div>
|
|
166
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
167
|
+
{labels.description}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
{body}
|
|
171
|
+
</Card>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type RowMode = "view" | "rename" | "delete";
|
|
176
|
+
|
|
177
|
+
function PasskeyRow({
|
|
178
|
+
item,
|
|
179
|
+
created,
|
|
180
|
+
lastUsed,
|
|
181
|
+
busy,
|
|
182
|
+
onRename,
|
|
183
|
+
onDelete,
|
|
184
|
+
labels,
|
|
185
|
+
}: {
|
|
186
|
+
item: PasskeyItem;
|
|
187
|
+
created: string | null;
|
|
188
|
+
lastUsed: string | null;
|
|
189
|
+
busy: boolean;
|
|
190
|
+
onRename?: PasskeysSettingProps["onRename"];
|
|
191
|
+
onDelete?: PasskeysSettingProps["onDelete"];
|
|
192
|
+
labels: PasskeysSettingLabels;
|
|
193
|
+
}) {
|
|
194
|
+
const [mode, setMode] = useState<RowMode>("view");
|
|
195
|
+
const [draft, setDraft] = useState(item.name);
|
|
196
|
+
// Which button opened the sub-mode, so leaving it puts focus back there — the
|
|
197
|
+
// confirm or the field unmounts under the focus, which otherwise falls to <body>.
|
|
198
|
+
const [returnTo, setReturnTo] = useState<"rename" | "delete" | null>(null);
|
|
199
|
+
const cancelDeleteRef = useRef<HTMLButtonElement>(null);
|
|
200
|
+
const renameRef = useRef<HTMLInputElement>(null);
|
|
201
|
+
const renameButtonRef = useRef<HTMLButtonElement>(null);
|
|
202
|
+
const deleteButtonRef = useRef<HTMLButtonElement>(null);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (mode === "delete") cancelDeleteRef.current?.focus();
|
|
206
|
+
else if (mode === "rename") renameRef.current?.select();
|
|
207
|
+
else if (returnTo) (returnTo === "rename" ? renameButtonRef : deleteButtonRef).current?.focus();
|
|
208
|
+
}, [mode, returnTo]);
|
|
209
|
+
|
|
210
|
+
const open = (next: "rename" | "delete") => {
|
|
211
|
+
setReturnTo(next);
|
|
212
|
+
setDraft(item.name);
|
|
213
|
+
setMode(next);
|
|
214
|
+
};
|
|
215
|
+
const leave = () => setMode("view");
|
|
216
|
+
const escapeLeaves = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
217
|
+
if (e.key === "Escape") {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
leave();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const saveRename = () => {
|
|
225
|
+
const next = draft.trim();
|
|
226
|
+
if (!onRename || !next || busy) return;
|
|
227
|
+
if (next === item.name) {
|
|
228
|
+
leave();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
Promise.resolve(onRename(item.id, next))
|
|
232
|
+
.then(leave)
|
|
233
|
+
.catch(() => {});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const confirmDelete = () => {
|
|
237
|
+
if (!onDelete || busy) return;
|
|
238
|
+
// The row unmounts once the list refreshes; on a failure it stays, confirm open.
|
|
239
|
+
Promise.resolve(onDelete(item.id))
|
|
240
|
+
.then(() => setMode("view"))
|
|
241
|
+
.catch(() => {});
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed]
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join(" · ");
|
|
247
|
+
|
|
248
|
+
if (mode === "rename") {
|
|
249
|
+
return (
|
|
250
|
+
<li className="flex flex-wrap items-center gap-2 py-2">
|
|
251
|
+
<Input
|
|
252
|
+
ref={renameRef}
|
|
253
|
+
aria-label={labels.renameField(item.name)}
|
|
254
|
+
className="min-w-0 flex-1"
|
|
255
|
+
value={draft}
|
|
256
|
+
maxLength={120}
|
|
257
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
258
|
+
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
|
259
|
+
if (e.key === "Enter") {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
saveRename();
|
|
262
|
+
} else if (e.key === "Escape") {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
leave();
|
|
266
|
+
}
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
<Button size="sm" onClick={saveRename} disabled={busy || !draft.trim()}>
|
|
270
|
+
{labels.save}
|
|
271
|
+
</Button>
|
|
272
|
+
<Button size="sm" variant="ghost" onClick={leave}>
|
|
273
|
+
{labels.cancel}
|
|
274
|
+
</Button>
|
|
275
|
+
</li>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (mode === "delete") {
|
|
280
|
+
return (
|
|
281
|
+
<li className="space-y-2 py-2">
|
|
282
|
+
<p role="alert" className="text-sm">
|
|
283
|
+
{labels.deleteConfirm(item.name)}
|
|
284
|
+
</p>
|
|
285
|
+
<div className="flex flex-wrap gap-2">
|
|
286
|
+
<Button size="sm" variant="danger" onClick={confirmDelete} onKeyDown={escapeLeaves} disabled={busy}>
|
|
287
|
+
{labels.confirmDelete}
|
|
288
|
+
</Button>
|
|
289
|
+
<Button ref={cancelDeleteRef} size="sm" variant="ghost" onClick={leave} onKeyDown={escapeLeaves}>
|
|
290
|
+
{labels.cancel}
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
</li>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<li className="flex items-center gap-3 py-2">
|
|
299
|
+
<KeyRound className="size-4 shrink-0 text-[var(--text-muted)]" aria-hidden />
|
|
300
|
+
<div className="min-w-0 flex-1">
|
|
301
|
+
<div className="truncate text-sm font-medium">{item.name}</div>
|
|
302
|
+
<div className="text-xs text-[var(--text-muted)]">{meta}</div>
|
|
303
|
+
</div>
|
|
304
|
+
{onRename && (
|
|
305
|
+
<Button
|
|
306
|
+
ref={renameButtonRef}
|
|
307
|
+
size="sm"
|
|
308
|
+
variant="ghost"
|
|
309
|
+
disabled={busy}
|
|
310
|
+
onClick={() => open("rename")}
|
|
311
|
+
aria-label={labels.renameItem(item.name)}
|
|
312
|
+
>
|
|
313
|
+
{labels.rename}
|
|
314
|
+
</Button>
|
|
315
|
+
)}
|
|
316
|
+
{onDelete && (
|
|
317
|
+
<Button
|
|
318
|
+
ref={deleteButtonRef}
|
|
319
|
+
size="sm"
|
|
320
|
+
variant="ghost"
|
|
321
|
+
tone="danger"
|
|
322
|
+
disabled={busy}
|
|
323
|
+
onClick={() => open("delete")}
|
|
324
|
+
aria-label={labels.deleteItem(item.name)}
|
|
325
|
+
>
|
|
326
|
+
{labels.delete}
|
|
327
|
+
</Button>
|
|
328
|
+
)}
|
|
329
|
+
</li>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
|
|
|
3
3
|
import { X } from "lucide-react";
|
|
4
4
|
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
|
+
import { CLIPS_ATTRIBUTE } from "../lib/clipping";
|
|
6
7
|
import { DropdownSearchHeader } from "./dropdown";
|
|
7
8
|
import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
|
|
8
9
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
@@ -153,6 +154,9 @@ export function PickerSheet({
|
|
|
153
154
|
|
|
154
155
|
if (!open || typeof document === "undefined") return null;
|
|
155
156
|
return createPortal(
|
|
157
|
+
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the dialog
|
|
158
|
+
container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for whatever
|
|
159
|
+
inside it holds focus, and `onMouseDown` only stops propagation (live #328). */
|
|
156
160
|
<div
|
|
157
161
|
// Spread FIRST, and every attribute below it deliberately wins: `style`,
|
|
158
162
|
// `onMouseDown` and `onKeyDown` here are not styling choices, they are live
|
|
@@ -237,7 +241,12 @@ export function PickerSheet({
|
|
|
237
241
|
/>
|
|
238
242
|
{/* min-h-0 so the LIST scrolls rather than the dialog growing past the
|
|
239
243
|
viewport — a flex child defaults to min-height:auto. */}
|
|
240
|
-
|
|
244
|
+
{/* Marked as the scroller it is (`CLIPS_ATTRIBUTE`), so a Tooltip in a row —
|
|
245
|
+
`Combobox`'s `optionAdornment` — portals out under jsdom as it does in the
|
|
246
|
+
browser rather than joining the option's name (keksdose E8). */}
|
|
247
|
+
<div {...{ [CLIPS_ATTRIBUTE]: "" }} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
248
|
+
{children}
|
|
249
|
+
</div>
|
|
241
250
|
</div>,
|
|
242
251
|
document.body,
|
|
243
252
|
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// All user-facing strings for `PieChart`, as the `pieChart` namespace of
|
|
2
|
+
// `<UiKitProvider labels>`, and per chart the `labels` prop over that.
|
|
3
|
+
//
|
|
4
|
+
// A module of its own, with no recharts in it, for the reason `series-chart-labels`
|
|
5
|
+
// is one: the label registry (src/i18n) imports the defaults, and must not pull the
|
|
6
|
+
// chart library in behind them.
|
|
7
|
+
|
|
8
|
+
export interface PieChartLabels {
|
|
9
|
+
/** Accessible name of the group of keyboard stops laid over the slices — one tab
|
|
10
|
+
* stop, arrow keys between them. */
|
|
11
|
+
slices: string;
|
|
12
|
+
/**
|
|
13
|
+
* One slice's accessible name. `value` arrives through `formatValue`, `percent` as
|
|
14
|
+
* the slice's share of what is drawn, already formatted in the locale ("42 %" in
|
|
15
|
+
* German, "42%" in English) — the function only arranges them.
|
|
16
|
+
*/
|
|
17
|
+
slice: (label: string, value: string, percent: string) => string;
|
|
18
|
+
/** The caption over the donut's centre figure. */
|
|
19
|
+
total: string;
|
|
20
|
+
/** What a chart with nothing to draw says, when the caller passes no `empty`. */
|
|
21
|
+
empty: string;
|
|
22
|
+
/** Accessible name of the chart's legend (a group of switches, one per slice). */
|
|
23
|
+
legend: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {
|
|
27
|
+
slices: "Chart slices",
|
|
28
|
+
slice: (label, value, percent) => `${label}: ${value} (${percent})`,
|
|
29
|
+
total: "Total",
|
|
30
|
+
empty: "No data",
|
|
31
|
+
legend: "Categories",
|
|
32
|
+
};
|