@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
|
@@ -146,35 +146,40 @@ export function useFieldSync<T>({
|
|
|
146
146
|
};
|
|
147
147
|
}, []);
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
149
|
+
// A named inner function so the settle path can start the next round by calling it
|
|
150
|
+
// again; a `useCallback` cannot refer to its own binding while it is being built.
|
|
151
|
+
const flush = useCallback(() => {
|
|
152
|
+
const run = async (): Promise<void> => {
|
|
153
|
+
if (inFlightRef.current) return; // the settle path below picks the new draft up
|
|
154
|
+
const next = draftRef.current;
|
|
155
|
+
if (equalsRef.current(next, savedRef.current)) return;
|
|
156
|
+
|
|
157
|
+
inFlightRef.current = true;
|
|
158
|
+
setState("pending");
|
|
159
|
+
setError(null);
|
|
160
|
+
try {
|
|
161
|
+
await onSaveRef.current(next);
|
|
162
|
+
savedRef.current = next;
|
|
163
|
+
inFlightRef.current = false;
|
|
164
|
+
if (!mountedRef.current) return;
|
|
165
|
+
// The draft may have moved while this was in the air. Compare against what is
|
|
166
|
+
// on screen NOW, not against what was sent.
|
|
167
|
+
if (equalsRef.current(draftRef.current, next)) {
|
|
168
|
+
setState("synced");
|
|
169
|
+
} else {
|
|
170
|
+
setState("edited");
|
|
171
|
+
void run();
|
|
172
|
+
}
|
|
173
|
+
} catch (cause) {
|
|
174
|
+
inFlightRef.current = false;
|
|
175
|
+
const err = toError(cause);
|
|
176
|
+
onErrorRef.current?.(err);
|
|
177
|
+
if (!mountedRef.current) return;
|
|
178
|
+
setState("error");
|
|
179
|
+
setError(err);
|
|
169
180
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const err = toError(cause);
|
|
173
|
-
onErrorRef.current?.(err);
|
|
174
|
-
if (!mountedRef.current) return;
|
|
175
|
-
setState("error");
|
|
176
|
-
setError(err);
|
|
177
|
-
}
|
|
181
|
+
};
|
|
182
|
+
return run();
|
|
178
183
|
}, []);
|
|
179
184
|
|
|
180
185
|
const setValue = useCallback(
|
|
@@ -396,23 +401,26 @@ export function FieldSyncRow<T>({
|
|
|
396
401
|
const retry = onRetry ?? sync.retry;
|
|
397
402
|
|
|
398
403
|
// The saved confirmation: on only for the moment after a save lands. Keyed on the
|
|
399
|
-
// TRANSITION pending → synced, so a field that starts synced shows nothing.
|
|
400
|
-
|
|
404
|
+
// TRANSITION pending → synced, so a field that starts synced shows nothing. The
|
|
405
|
+
// transition is seen while rendering, against the previous state; the effect only
|
|
406
|
+
// owns the timer that turns it off again.
|
|
407
|
+
const [previous, setPrevious] = useState(state);
|
|
401
408
|
const [justSaved, setJustSaved] = useState(false);
|
|
409
|
+
if (state !== previous) {
|
|
410
|
+
setPrevious(state);
|
|
411
|
+
setJustSaved(state === "synced" && previous === "pending");
|
|
412
|
+
}
|
|
402
413
|
useEffect(() => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
setJustSaved(true);
|
|
408
|
-
const t = setTimeout(() => setJustSaved(false), savedMs);
|
|
409
|
-
return () => clearTimeout(t);
|
|
410
|
-
}
|
|
411
|
-
if (state !== "synced") setJustSaved(false);
|
|
412
|
-
}, [state, savedMs]);
|
|
414
|
+
if (!justSaved) return;
|
|
415
|
+
const t = setTimeout(() => setJustSaved(false), savedMs);
|
|
416
|
+
return () => clearTimeout(t);
|
|
417
|
+
}, [justSaved, savedMs]);
|
|
413
418
|
const shown: FieldSyncState | null = state === "synced" ? (justSaved ? "synced" : null) : state;
|
|
414
419
|
|
|
415
420
|
return (
|
|
421
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- not an interaction:
|
|
422
|
+
`onBlur` listens for focus leaving the row's own controls (which are the
|
|
423
|
+
interactive elements) to save; the wrapper itself is never focused. */
|
|
416
424
|
<div
|
|
417
425
|
data-field-sync={state}
|
|
418
426
|
className={cn(
|
package/src/components/field.tsx
CHANGED
|
@@ -55,6 +55,40 @@ export interface FieldControlProps {
|
|
|
55
55
|
"aria-required": true | undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The second argument of a `Field`'s render-prop children: what a GROUP needs that
|
|
60
|
+
* a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing
|
|
61
|
+
* at a `role="radiogroup"` div names nothing — so a group (a {@link ToggleGroup})
|
|
62
|
+
* takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}
|
|
63
|
+
* so the spread every existing control takes is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export interface FieldRenderMeta {
|
|
66
|
+
/** The label element's id, or `undefined` when the field has no label. */
|
|
67
|
+
labelId: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where the label is visible. `"visible"` (default); `"sr-only"`: never drawn, still
|
|
72
|
+
* the control's name; `"below-sm"` / `"below-md"` / `"below-lg"`: drawn only under
|
|
73
|
+
* that breakpoint and screen-reader-only from it up.
|
|
74
|
+
*
|
|
75
|
+
* For table-like editors — kastlan's journal entry lines
|
|
76
|
+
* (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop
|
|
77
|
+
* row sits under column headers that already say "Account" and "Amount", but the
|
|
78
|
+
* phone layout stacks each line as a card and needs the labels back. The name stays
|
|
79
|
+
* on the control at every width; only the ink changes.
|
|
80
|
+
*/
|
|
81
|
+
export type FieldLabelVisibility = "visible" | "sr-only" | "below-sm" | "below-md" | "below-lg";
|
|
82
|
+
|
|
83
|
+
// Literal class strings so Tailwind's scanner sees each one.
|
|
84
|
+
const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
|
|
85
|
+
visible: undefined,
|
|
86
|
+
"sr-only": "sr-only",
|
|
87
|
+
"below-sm": "sm:sr-only",
|
|
88
|
+
"below-md": "md:sr-only",
|
|
89
|
+
"below-lg": "lg:sr-only",
|
|
90
|
+
};
|
|
91
|
+
|
|
58
92
|
export interface FieldProps {
|
|
59
93
|
/** The label above the control. Omit it for a group whose control is labelled
|
|
60
94
|
* some other way; the hint and error still render. */
|
|
@@ -74,10 +108,13 @@ export interface FieldProps {
|
|
|
74
108
|
disabled?: boolean;
|
|
75
109
|
/** The label's size: `sm` for a dense row, `md` otherwise. */
|
|
76
110
|
labelSize?: "sm" | "md";
|
|
111
|
+
/** See {@link FieldLabelVisibility}. */
|
|
112
|
+
labelVisibility?: FieldLabelVisibility;
|
|
77
113
|
/** Wrapper className. */
|
|
78
114
|
className?: string;
|
|
79
|
-
/** The control. A function receives {@link FieldControlProps} to spread on it
|
|
80
|
-
|
|
115
|
+
/** The control. A function receives {@link FieldControlProps} to spread on it, and
|
|
116
|
+
* {@link FieldRenderMeta} (the label's id, for a group). */
|
|
117
|
+
children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
const isShown = (node: ReactNode) =>
|
|
@@ -91,6 +128,7 @@ export function Field({
|
|
|
91
128
|
htmlFor,
|
|
92
129
|
disabled,
|
|
93
130
|
labelSize,
|
|
131
|
+
labelVisibility = "visible",
|
|
94
132
|
className,
|
|
95
133
|
children,
|
|
96
134
|
}: FieldProps) {
|
|
@@ -98,6 +136,7 @@ export function Field({
|
|
|
98
136
|
const id = htmlFor ?? `${generated}-control`;
|
|
99
137
|
const hintId = `${generated}-hint`;
|
|
100
138
|
const errorId = `${generated}-error`;
|
|
139
|
+
const labelId = `${generated}-label`;
|
|
101
140
|
const hasHint = isShown(hint);
|
|
102
141
|
const hasError = isShown(error);
|
|
103
142
|
const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
|
|
@@ -108,20 +147,32 @@ export function Field({
|
|
|
108
147
|
"aria-required": required || undefined,
|
|
109
148
|
};
|
|
110
149
|
return (
|
|
111
|
-
<div
|
|
150
|
+
<div
|
|
151
|
+
data-slot="field"
|
|
152
|
+
className={cn(
|
|
153
|
+
"grid gap-1.5",
|
|
154
|
+
// A hidden label is `position: absolute`; `relative` keeps its containing
|
|
155
|
+
// block here rather than the page's (see sr-only-containment.test).
|
|
156
|
+
labelVisibility !== "visible" && "relative",
|
|
157
|
+
className,
|
|
158
|
+
)}
|
|
159
|
+
>
|
|
112
160
|
{label !== undefined && (
|
|
113
161
|
<Label
|
|
162
|
+
id={labelId}
|
|
114
163
|
htmlFor={id}
|
|
115
164
|
required={required}
|
|
116
165
|
disabled={disabled}
|
|
117
166
|
size={labelSize}
|
|
118
167
|
data-error={hasError || undefined}
|
|
119
|
-
className="data-[error=true]:text-[var(--danger)]"
|
|
168
|
+
className={cn("data-[error=true]:text-[var(--danger)]", LABEL_VISIBILITY[labelVisibility])}
|
|
120
169
|
>
|
|
121
170
|
{label}
|
|
122
171
|
</Label>
|
|
123
172
|
)}
|
|
124
|
-
{typeof children === "function"
|
|
173
|
+
{typeof children === "function"
|
|
174
|
+
? children(control, { labelId: label !== undefined ? labelId : undefined })
|
|
175
|
+
: children}
|
|
125
176
|
{hasHint && (
|
|
126
177
|
<p id={hintId} className="text-[11px] leading-tight text-[var(--text-muted)]">
|
|
127
178
|
{hint}
|
|
@@ -15,7 +15,8 @@ import { X } from "lucide-react";
|
|
|
15
15
|
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
17
|
import { dirOf, type Direction } from "../lib/direction";
|
|
18
|
-
import { useKitLabels } from "../i18n/kit-labels";
|
|
18
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
19
|
+
import { pickLinkRenderer } from "./text-link";
|
|
19
20
|
import { Tooltip, type TooltipSide } from "./tooltip";
|
|
20
21
|
|
|
21
22
|
export interface FloatingPanelLabels {
|
|
@@ -588,8 +589,17 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
|
|
|
588
589
|
label: string;
|
|
589
590
|
icon: ReactNode;
|
|
590
591
|
/** Make it a toggle: `aria-pressed` and the brand "on" look (the brand glyph on the
|
|
591
|
-
* quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.
|
|
592
|
+
* quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.
|
|
593
|
+
* For the state without the tint, see {@link pressedStyle}. */
|
|
592
594
|
pressed?: boolean;
|
|
595
|
+
/**
|
|
596
|
+
* How `pressed` looks. `tint` (default): the brand "on" look. `plain`: the member
|
|
597
|
+
* keeps its quiet look in both states and only `aria-pressed` changes — for a toggle
|
|
598
|
+
* whose icon already says which state it is in (keksdose's /transactions "Upcoming"
|
|
599
|
+
* swaps two icons; a tint on top said it twice). The same as passing `aria-pressed`
|
|
600
|
+
* without `pressed`, which also rides through untinted.
|
|
601
|
+
*/
|
|
602
|
+
pressedStyle?: "tint" | "plain";
|
|
593
603
|
/**
|
|
594
604
|
* `primary`: the brand-filled member, for the group's main action (the /transactions
|
|
595
605
|
* "+"). Default: the quiet one.
|
|
@@ -611,8 +621,8 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
|
|
|
611
621
|
tooltip?: boolean;
|
|
612
622
|
/** Where the tooltip opens. Default `top` — the group sits at the bottom of the screen. */
|
|
613
623
|
tooltipSide?: TooltipSide;
|
|
614
|
-
/** Make it a link — the /transactions invoices member.
|
|
615
|
-
* `
|
|
624
|
+
/** Make it a link — the /transactions invoices member. Drawn by `renderLink`, else
|
|
625
|
+
* the `<UiKitProvider linkComponent>` (an in-app `href`), else a plain `<a>`. */
|
|
616
626
|
href?: string;
|
|
617
627
|
/** Render the link through your router's `Link` — see {@link FloatingActionLinkProps}. */
|
|
618
628
|
renderLink?: (props: FloatingActionLinkProps) => ReactElement;
|
|
@@ -637,6 +647,7 @@ export function FloatingAction({
|
|
|
637
647
|
label,
|
|
638
648
|
icon,
|
|
639
649
|
pressed,
|
|
650
|
+
pressedStyle = "tint",
|
|
640
651
|
variant = "default",
|
|
641
652
|
badge,
|
|
642
653
|
badgeLabel,
|
|
@@ -652,6 +663,7 @@ export function FloatingAction({
|
|
|
652
663
|
...rest
|
|
653
664
|
}: FloatingActionProps) {
|
|
654
665
|
const labels = useKitLabels("floatingPanel", DEFAULT_FLOATING_PANEL_LABELS);
|
|
666
|
+
const kitLink = useKitLink();
|
|
655
667
|
const counted = badge !== undefined && badge > 0;
|
|
656
668
|
const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;
|
|
657
669
|
const isLink = href !== undefined;
|
|
@@ -661,7 +673,7 @@ export function FloatingAction({
|
|
|
661
673
|
"disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-5",
|
|
662
674
|
variant === "primary"
|
|
663
675
|
? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
|
|
664
|
-
: pressed && !isLink
|
|
676
|
+
: pressed && !isLink && pressedStyle === "tint"
|
|
665
677
|
? "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]"
|
|
666
678
|
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
|
|
667
679
|
className,
|
|
@@ -697,8 +709,9 @@ export function FloatingAction({
|
|
|
697
709
|
children: body,
|
|
698
710
|
};
|
|
699
711
|
const { children: content, ...anchor } = linkProps;
|
|
700
|
-
|
|
701
|
-
|
|
712
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
713
|
+
member = render ? (
|
|
714
|
+
<RenderedFloatingLink render={render} {...linkProps} />
|
|
702
715
|
) : (
|
|
703
716
|
<a {...anchor}>{content}</a>
|
|
704
717
|
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isValidElement } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { Button, Spinner, type ButtonVariant } from "./ui";
|
|
6
|
+
|
|
7
|
+
/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */
|
|
8
|
+
export interface FormActionsLabels {
|
|
9
|
+
save: string;
|
|
10
|
+
cancel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {
|
|
14
|
+
save: "Save",
|
|
15
|
+
cancel: "Cancel",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FormActionsAlign = "start" | "center" | "end" | "between";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where the row sits:
|
|
22
|
+
* - `inline` (default): under the form's last field, `pt-4` above it.
|
|
23
|
+
* - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface
|
|
24
|
+
* colour with a rule above, clear of the phone's home indicator — for a long form
|
|
25
|
+
* whose Save should not be a scroll away.
|
|
26
|
+
* - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,
|
|
27
|
+
* FullBleedDialog's `footer`), which already pads and rules it.
|
|
28
|
+
*/
|
|
29
|
+
export type FormActionsPlacement = "inline" | "sticky" | "dialog";
|
|
30
|
+
|
|
31
|
+
/** The start-side action of {@link FormActionsProps.destructive}, as data. */
|
|
32
|
+
export interface FormActionsDestructive {
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** See Button's `disabledReason`. */
|
|
37
|
+
disabledReason?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ALIGN_CLASS: Record<FormActionsAlign, string> = {
|
|
41
|
+
start: "justify-start",
|
|
42
|
+
center: "justify-center",
|
|
43
|
+
end: "justify-end",
|
|
44
|
+
between: "justify-between",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {
|
|
48
|
+
inline: "pt-4",
|
|
49
|
+
sticky: "sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3",
|
|
50
|
+
dialog: "",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
54
|
+
/** Called by Cancel. Omitted: no Cancel button (a form that only saves). */
|
|
55
|
+
onCancel?: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Given: the save button is `type="button"` and calls this — for a dialog or a row
|
|
58
|
+
* that saves from a handler. Omitted: it is `type="submit"` and submits the
|
|
59
|
+
* enclosing `<form>` (or the one named by {@link form}).
|
|
60
|
+
*/
|
|
61
|
+
onSubmit?: () => void;
|
|
62
|
+
/** The save button's text. Default `form.save` from the provider, else "Save". */
|
|
63
|
+
submitLabel?: ReactNode;
|
|
64
|
+
/** The cancel button's text. Default `form.cancel` from the provider, else "Cancel". */
|
|
65
|
+
cancelLabel?: ReactNode;
|
|
66
|
+
/** The save button's text while `pending` ("Saving…"). Default: `submitLabel`. */
|
|
67
|
+
pendingLabel?: ReactNode;
|
|
68
|
+
/**
|
|
69
|
+
* The save is running: a spinner in the save button, `aria-busy` on it, and the
|
|
70
|
+
* button disabled, so a second click cannot send the form twice. Cancel stays
|
|
71
|
+
* usable.
|
|
72
|
+
*/
|
|
73
|
+
pending?: boolean;
|
|
74
|
+
/** Disable save for a reason of the form's own (an unbalanced entry). */
|
|
75
|
+
submitDisabled?: boolean;
|
|
76
|
+
/** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */
|
|
77
|
+
submitDisabledReason?: ReactNode;
|
|
78
|
+
/** The save button's variant. Default `brand`; `danger` for a save that destroys
|
|
79
|
+
* (kastlan's `destructive` flag on its own FormActions). */
|
|
80
|
+
submitVariant?: ButtonVariant;
|
|
81
|
+
/**
|
|
82
|
+
* A destructive action at the START of the row, apart from Save and Cancel — the
|
|
83
|
+
* "Delete" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger
|
|
84
|
+
* button, or an element of your own (a {@link DangerConfirm}, say). With it the row
|
|
85
|
+
* is `between`-aligned unless `align` says otherwise.
|
|
86
|
+
*/
|
|
87
|
+
destructive?: ReactNode | FormActionsDestructive;
|
|
88
|
+
/** Horizontal alignment. Default `end` (`between` with a `destructive` action). */
|
|
89
|
+
align?: FormActionsAlign;
|
|
90
|
+
/** See {@link FormActionsPlacement}. Default `inline`. */
|
|
91
|
+
placement?: FormActionsPlacement;
|
|
92
|
+
/** The `id` of the `<form>` the save button submits, when the row is rendered
|
|
93
|
+
* outside it (a dialog's footer slot). */
|
|
94
|
+
form?: string;
|
|
95
|
+
/** Extra actions, placed before Cancel. */
|
|
96
|
+
children?: ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isDestructiveData(value: unknown): value is FormActionsDestructive {
|
|
100
|
+
return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's
|
|
105
|
+
* `FormActions`, with the labels from the provider, a pending state and a place for
|
|
106
|
+
* the destructive action.
|
|
107
|
+
*
|
|
108
|
+
* ```tsx
|
|
109
|
+
* <form onSubmit={form.handleSubmit(save)}>
|
|
110
|
+
* …
|
|
111
|
+
* <FormActions onCancel={close} pending={mutation.isPending} />
|
|
112
|
+
* </form>
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`
|
|
116
|
+
* so the button's name stays its text; `aria-busy` is what says it is working.
|
|
117
|
+
*/
|
|
118
|
+
export function FormActions({
|
|
119
|
+
onCancel,
|
|
120
|
+
onSubmit,
|
|
121
|
+
submitLabel,
|
|
122
|
+
cancelLabel,
|
|
123
|
+
pendingLabel,
|
|
124
|
+
pending = false,
|
|
125
|
+
submitDisabled = false,
|
|
126
|
+
submitDisabledReason,
|
|
127
|
+
submitVariant = "brand",
|
|
128
|
+
destructive,
|
|
129
|
+
align,
|
|
130
|
+
placement = "inline",
|
|
131
|
+
form,
|
|
132
|
+
children,
|
|
133
|
+
className,
|
|
134
|
+
style,
|
|
135
|
+
...rest
|
|
136
|
+
}: FormActionsProps) {
|
|
137
|
+
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
138
|
+
const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;
|
|
139
|
+
const justify = align ?? (hasDestructive ? "between" : "end");
|
|
140
|
+
const start = !hasDestructive ? null : isDestructiveData(destructive) ? (
|
|
141
|
+
<Button
|
|
142
|
+
type="button"
|
|
143
|
+
variant="ghost"
|
|
144
|
+
tone="danger"
|
|
145
|
+
onClick={destructive.onClick}
|
|
146
|
+
disabled={destructive.disabled}
|
|
147
|
+
disabledReason={destructive.disabledReason}
|
|
148
|
+
>
|
|
149
|
+
{destructive.label}
|
|
150
|
+
</Button>
|
|
151
|
+
) : (
|
|
152
|
+
destructive
|
|
153
|
+
);
|
|
154
|
+
const saveLabel = submitLabel ?? labels.save;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
data-slot="form-actions"
|
|
159
|
+
data-placement={placement}
|
|
160
|
+
{...rest}
|
|
161
|
+
style={
|
|
162
|
+
placement === "sticky"
|
|
163
|
+
? { paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))", ...style }
|
|
164
|
+
: style
|
|
165
|
+
}
|
|
166
|
+
className={cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}
|
|
167
|
+
>
|
|
168
|
+
{start && <div className="me-auto flex items-center gap-2">{start}</div>}
|
|
169
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
170
|
+
{children}
|
|
171
|
+
{onCancel && (
|
|
172
|
+
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
173
|
+
{cancelLabel ?? labels.cancel}
|
|
174
|
+
</Button>
|
|
175
|
+
)}
|
|
176
|
+
<Button
|
|
177
|
+
type={onSubmit ? "button" : "submit"}
|
|
178
|
+
form={form}
|
|
179
|
+
onClick={onSubmit}
|
|
180
|
+
variant={submitVariant}
|
|
181
|
+
disabled={pending || submitDisabled}
|
|
182
|
+
disabledReason={pending ? undefined : submitDisabledReason}
|
|
183
|
+
aria-busy={pending || undefined}
|
|
184
|
+
>
|
|
185
|
+
{pending && <Spinner label={null} className="size-4" />}
|
|
186
|
+
{pending && pendingLabel !== undefined ? pendingLabel : saveLabel}
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -169,6 +169,10 @@ export function FullBleedDialog({
|
|
|
169
169
|
if (!open || typeof document === "undefined") return null;
|
|
170
170
|
|
|
171
171
|
return createPortal(
|
|
172
|
+
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the dialog
|
|
173
|
+
container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for whatever
|
|
174
|
+
inside it holds focus, and the mouse handlers are the backdrop press (Escape and the
|
|
175
|
+
close button are its keyboard path). */
|
|
172
176
|
<div
|
|
173
177
|
// `animate-overlay` on the backdrop and `animate-sheet` on the panel (live #320):
|
|
174
178
|
// the backdrop fades, the panel rises from the bottom edge it is anchored to.
|
|
@@ -103,22 +103,24 @@ export function HoverMenu({
|
|
|
103
103
|
// close what was just opened (feedback #19). Toggling closed on click stays
|
|
104
104
|
// for keyboard/touch activation, which reaches "open" without going through
|
|
105
105
|
// the hover path.
|
|
106
|
-
|
|
106
|
+
//
|
|
107
|
+
// State rather than a ref: `close` is handed to the caller's render prop, and a
|
|
108
|
+
// function that writes a ref must not be reachable from render.
|
|
109
|
+
const [openedByHover, setOpenedByHover] = useState(false);
|
|
107
110
|
|
|
108
111
|
const close = useCallback(() => {
|
|
109
|
-
|
|
112
|
+
setOpenedByHover(false);
|
|
110
113
|
setPhase("closed");
|
|
111
114
|
}, []);
|
|
112
115
|
|
|
113
|
-
const toggle = useCallback(
|
|
114
|
-
()
|
|
115
|
-
setPhase(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
116
|
+
const toggle = useCallback(() => {
|
|
117
|
+
if (open) {
|
|
118
|
+
if (!openedByHover) setPhase("closed");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
setOpenedByHover(false);
|
|
122
|
+
setPhase("open");
|
|
123
|
+
}, [open, openedByHover]);
|
|
122
124
|
|
|
123
125
|
const handleMouseEnter = useCallback(
|
|
124
126
|
() => setPhase((p) => (p === "closing" ? "open" : p === "closed" ? "opening" : p)),
|
|
@@ -134,7 +136,7 @@ export function HoverMenu({
|
|
|
134
136
|
useEffect(() => {
|
|
135
137
|
if (phase === "opening") {
|
|
136
138
|
const id = setTimeout(() => {
|
|
137
|
-
|
|
139
|
+
setOpenedByHover(true);
|
|
138
140
|
setPhase("open");
|
|
139
141
|
}, OPEN_DELAY_MS);
|
|
140
142
|
return () => clearTimeout(id);
|
|
@@ -217,7 +219,7 @@ export function HoverMenu({
|
|
|
217
219
|
(edge === "last" ? list[list.length - 1] : list[0])?.focus();
|
|
218
220
|
} else {
|
|
219
221
|
focusOnOpenRef.current = edge;
|
|
220
|
-
|
|
222
|
+
setOpenedByHover(false);
|
|
221
223
|
setPhase("open");
|
|
222
224
|
}
|
|
223
225
|
}
|
|
@@ -247,11 +249,14 @@ export function HoverMenu({
|
|
|
247
249
|
// subtracted first so the measurement is against the natural position.
|
|
248
250
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
249
251
|
const [shiftX, setShiftX] = useState(0);
|
|
252
|
+
// Back to 0 on close, adjusted while rendering against the previous `open`.
|
|
253
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
254
|
+
if (open !== wasOpen) {
|
|
255
|
+
setWasOpen(open);
|
|
256
|
+
if (!open) setShiftX(0);
|
|
257
|
+
}
|
|
250
258
|
useLayoutEffect(() => {
|
|
251
|
-
if (!open)
|
|
252
|
-
setShiftX(0);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
259
|
+
if (!open) return;
|
|
255
260
|
const el = panelRef.current;
|
|
256
261
|
if (!el) return;
|
|
257
262
|
const rect = el.getBoundingClientRect();
|
|
@@ -267,6 +272,10 @@ export function HoverMenu({
|
|
|
267
272
|
}, [open]);
|
|
268
273
|
|
|
269
274
|
return (
|
|
275
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
|
|
276
|
+
itself: the mouse handlers are the hover-to-open convenience, and `onKeyDown`
|
|
277
|
+
catches keys bubbling from the caller's trigger button and the menuitems, which
|
|
278
|
+
are the keyboard path (Enter/Space/arrows open, arrows move, Escape closes). */
|
|
270
279
|
<div
|
|
271
280
|
// `...rest` first: the hover handlers below are the whole component, and a caller
|
|
272
281
|
// passing one of its own must not silently replace them.
|