@eifi1/ui-kit 0.53.0 → 0.55.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 +96 -82
- package/dist/account/delete-account-setting.d.ts +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/admin/plan-change.js +2 -2
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/user-roster.d.ts +6 -0
- package/dist/admin/user-roster.js +4 -0
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +36 -0
- package/dist/auth/auth-errors.js +20 -0
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +26 -0
- package/dist/billing/billing-labels.js +9 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/subscription-standing.d.ts +61 -0
- package/dist/billing/subscription-standing.js +51 -0
- package/dist/billing/subscription-standing.js.map +1 -0
- package/dist/billing/usage-meter.d.ts +52 -0
- package/dist/billing/usage-meter.js +52 -0
- package/dist/billing/usage-meter.js.map +1 -0
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +4 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/alert-banner.js +2 -3
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -15
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +2 -1
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button.d.ts +35 -1
- package/dist/components/button.js +5 -9
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +6 -4
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/camera-button.d.ts +89 -0
- package/dist/components/camera-button.js +122 -0
- package/dist/components/camera-button.js.map +1 -0
- package/dist/components/camera-capture.d.ts +66 -0
- package/dist/components/camera-capture.js +190 -0
- package/dist/components/camera-capture.js.map +1 -0
- package/dist/components/camera-labels.d.ts +20 -0
- package/dist/components/camera-labels.js +15 -0
- package/dist/components/camera-labels.js.map +1 -0
- package/dist/components/chart-edge.d.ts +9 -0
- package/dist/components/chart-edge.js +14 -3
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-zoom-core.js +34 -29
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/combobox-core.js +3 -2
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +2 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/consent-again-notice-labels.d.ts +25 -0
- package/dist/components/consent-again-notice-labels.js +15 -0
- package/dist/components/consent-again-notice-labels.js.map +1 -0
- package/dist/components/consent-again-notice.d.ts +85 -0
- package/dist/components/consent-again-notice.js +94 -0
- package/dist/components/consent-again-notice.js.map +1 -0
- package/dist/components/country-select.js +2 -7
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +2 -1
- package/dist/components/currency-option-list.js +10 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -1
- package/dist/components/currency-select.js +2 -2
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +22 -1
- package/dist/components/danger-confirm.js +52 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-column-settings.js +2 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +2 -1
- package/dist/components/data-table-filter-popover.js +3 -2
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +33 -3
- package/dist/components/data-table-filters.js +6 -4
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +13 -3
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/date-field.js +2 -2
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +2 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +2 -6
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +5 -1
- package/dist/components/dialog-frame.js +6 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/dropdown.js +7 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.js +2 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-parts.d.ts +108 -6
- package/dist/components/field-parts.js +59 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-suggestion-labels.d.ts +2 -0
- package/dist/components/field-suggestion-labels.js +10 -0
- package/dist/components/field-suggestion-labels.js.map +1 -0
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/floating-panel.d.ts +7 -6
- package/dist/components/floating-panel.js +3 -17
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +3 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.js +3 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.d.ts +23 -5
- package/dist/components/icon-button.js +6 -8
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.js +2 -1
- package/dist/components/inline-confirm.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -2
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +3 -3
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal.d.ts +13 -1
- package/dist/components/legal.js +12 -0
- package/dist/components/legal.js.map +1 -1
- package/dist/components/legended-series-chart.d.ts +80 -0
- package/dist/components/legended-series-chart.js +70 -0
- package/dist/components/legended-series-chart.js.map +1 -0
- package/dist/components/lightbox.js +2 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/mini-calendar.js +16 -7
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +48 -0
- package/dist/components/modal.js +26 -6
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +1 -1
- package/dist/components/month-picker.js +6 -6
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js +2 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.js +2 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/page-header-layout.d.ts +18 -0
- package/dist/components/page-header-layout.js +23 -0
- package/dist/components/page-header-layout.js.map +1 -0
- package/dist/components/page-header-skeleton.d.ts +41 -0
- package/dist/components/page-header-skeleton.js +52 -0
- package/dist/components/page-header-skeleton.js.map +1 -0
- package/dist/components/page-header.js +4 -17
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/popover.js +7 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/release-notes-labels.d.ts +16 -0
- package/dist/components/release-notes-labels.js +18 -0
- package/dist/components/release-notes-labels.js.map +1 -0
- package/dist/components/release-notes.d.ts +65 -0
- package/dist/components/release-notes.js +65 -0
- package/dist/components/release-notes.js.map +1 -0
- package/dist/components/select.js +2 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +9 -0
- package/dist/components/series-chart-labels.js +3 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +5 -0
- package/dist/components/series-chart.js +89 -36
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/stat-tile.d.ts +21 -2
- package/dist/components/stat-tile.js +4 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/toggle-group.js +3 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/treemap-layout.js.map +1 -1
- package/dist/components/treemap.d.ts +17 -15
- package/dist/components/treemap.js +167 -85
- package/dist/components/treemap.js.map +1 -1
- package/dist/currencies.d.ts +16 -2
- package/dist/currencies.js +2 -1
- package/dist/currencies.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +2 -2
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-context-labels.js +2 -1
- package/dist/feedback/feedback-context-labels.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +19 -3
- package/dist/feedback/feedback-context.js +12 -1
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-diagnostics.d.ts +17 -0
- package/dist/feedback/feedback-diagnostics.js +27 -0
- package/dist/feedback/feedback-diagnostics.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +10 -0
- package/dist/feedback/feedback-dialog.js +6 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox-page.d.ts +260 -0
- package/dist/feedback/feedback-inbox-page.js +207 -0
- package/dist/feedback/feedback-inbox-page.js.map +1 -0
- package/dist/feedback/feedback-record.d.ts +5 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +3 -0
- package/dist/feedback/feedback-row-detail.js +3 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
- package/dist/feedback/feedback-submit-dialog.js +15 -3
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +30 -2
- package/dist/feedback/feedback-submit.js +5 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js +2 -2
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +6 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-debounce.d.ts +32 -5
- package/dist/hooks/use-debounce.js +6 -5
- package/dist/hooks/use-debounce.js.map +1 -1
- package/dist/hooks/use-file-picker.d.ts +3 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +19 -3
- package/dist/hooks/use-focus-trap.js +15 -5
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-has-camera.d.ts +36 -0
- package/dist/hooks/use-has-camera.js +53 -0
- package/dist/hooks/use-has-camera.js.map +1 -0
- package/dist/hooks/use-hotkey.js +2 -1
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-overlay-history.d.ts +6 -1
- package/dist/hooks/use-overlay-history.js +10 -2
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +78 -4
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +20 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +95 -5
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +74 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +75 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +77 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +77 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +74 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +22 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-labels.d.ts +8 -0
- package/dist/landing/landing-labels.js +2 -1
- package/dist/landing/landing-labels.js.map +1 -1
- package/dist/lib/changelog.d.ts +130 -0
- package/dist/lib/changelog.js +137 -0
- package/dist/lib/changelog.js.map +1 -0
- package/dist/lib/clipboard-minus.js +2 -2
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.d.ts +36 -4
- package/dist/lib/dates.js +49 -13
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/editable-content.d.ts +33 -0
- package/dist/lib/editable-content.js +15 -0
- package/dist/lib/editable-content.js.map +1 -0
- package/dist/lib/focus-left.d.ts +47 -1
- package/dist/lib/focus-left.js +11 -2
- package/dist/lib/focus-left.js.map +1 -1
- package/dist/lib/format.d.ts +37 -2
- package/dist/lib/format.js +25 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/intl-cache.d.ts +4 -2
- package/dist/lib/intl-cache.js.map +1 -1
- package/dist/lib/server-wake.d.ts +49 -9
- package/dist/lib/server-wake.js +25 -14
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/lib/session-refresh.d.ts +53 -0
- package/dist/lib/session-refresh.js +27 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/translation-review.d.ts +26 -0
- package/dist/lib/translation-review.js +14 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.js +3 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/shell/app-shell.d.ts +25 -1
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +3 -0
- package/dist/shell/dev-menu.js +4 -2
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/held-changes-banner-labels.d.ts +23 -0
- package/dist/shell/held-changes-banner-labels.js +14 -0
- package/dist/shell/held-changes-banner-labels.js.map +1 -0
- package/dist/shell/held-changes-banner.d.ts +75 -0
- package/dist/shell/held-changes-banner.js +106 -0
- package/dist/shell/held-changes-banner.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +27 -12
- package/dist/shell/sync-status-indicator.js +13 -4
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +5 -0
- package/dist/shell/top-bar-parts-labels.js +3 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +11 -2
- package/dist/sync/engine.js +13 -5
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/hooks.d.ts +3 -2
- package/dist/sync/hooks.js +4 -2
- package/dist/sync/hooks.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +2 -2
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +18 -0
- package/dist/theme/chart-palette.js +17 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +3 -3
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -4
- package/dist/theme/contrast.js.map +1 -1
- package/dist/tour/tour-overlay.js +2 -1
- package/dist/tour/tour-overlay.js.map +1 -1
- package/package.json +5 -9
- package/tokens.css +19 -54
- package/src/account/account-parts.ts +0 -47
- package/src/account/account-settings-labels.ts +0 -240
- package/src/account/account-settings.tsx +0 -698
- package/src/account/data-export-setting-labels.ts +0 -20
- package/src/account/data-export-setting.tsx +0 -200
- package/src/account/delete-account-setting-labels.ts +0 -33
- package/src/account/delete-account-setting.tsx +0 -341
- package/src/account/email-change-setting-labels.ts +0 -31
- package/src/account/email-change-setting.tsx +0 -443
- package/src/account/passkeys-setting.tsx +0 -445
- package/src/account/sessions-setting-labels.ts +0 -24
- package/src/account/sessions-setting.tsx +0 -283
- package/src/admin/admin-action-confirm-labels.ts +0 -19
- package/src/admin/admin-action-confirm.tsx +0 -509
- package/src/admin/admin-action-log-labels.ts +0 -83
- package/src/admin/admin-action-log.tsx +0 -464
- package/src/admin/admin-area-labels.ts +0 -114
- package/src/admin/admin-forbidden.tsx +0 -75
- package/src/admin/admin-overview.tsx +0 -258
- package/src/admin/admin-parts.ts +0 -68
- package/src/admin/admin-sections.tsx +0 -208
- package/src/admin/admin-tool-links.tsx +0 -90
- package/src/admin/invitations-panel-labels.ts +0 -31
- package/src/admin/invitations-panel.tsx +0 -516
- package/src/admin/mail-test-card-labels.ts +0 -19
- package/src/admin/mail-test-card.tsx +0 -189
- package/src/admin/plan-change-labels.ts +0 -5
- package/src/admin/plan-change.tsx +0 -474
- package/src/admin/reviewer-scope-editor-labels.ts +0 -16
- package/src/admin/reviewer-scope-editor.tsx +0 -224
- package/src/admin/role-select-labels.ts +0 -16
- package/src/admin/role-select.tsx +0 -282
- package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
- package/src/admin/transfer-ownership-dialog.tsx +0 -162
- package/src/admin/user-roster-labels.ts +0 -17
- package/src/admin/user-roster.tsx +0 -559
- package/src/auth/accept-invitation-labels.ts +0 -21
- package/src/auth/accept-invitation.tsx +0 -210
- package/src/auth/auth-errors-labels.ts +0 -23
- package/src/auth/auth-errors.ts +0 -370
- package/src/auth/complete-name-dialog-labels.ts +0 -16
- package/src/auth/complete-name-dialog.tsx +0 -253
- package/src/auth/email-tag.ts +0 -75
- package/src/auth/forgot-password-form-labels.ts +0 -20
- package/src/auth/forgot-password-form.tsx +0 -221
- package/src/auth/form-rules.ts +0 -40
- package/src/auth/new-password-fields.tsx +0 -85
- package/src/auth/not-found-page-labels.ts +0 -12
- package/src/auth/not-found-page.tsx +0 -95
- package/src/auth/register-form-labels.ts +0 -31
- package/src/auth/register-form.tsx +0 -456
- package/src/auth/reset-password-form-labels.ts +0 -27
- package/src/auth/reset-password-form.tsx +0 -341
- package/src/auth/sign-in-form-labels.ts +0 -44
- package/src/auth/sign-in-form.tsx +0 -858
- package/src/auth/status-parts.tsx +0 -132
- package/src/auth/verify-email-labels.ts +0 -26
- package/src/auth/verify-email.tsx +0 -445
- package/src/billing/billing-banner.tsx +0 -258
- package/src/billing/billing-labels.ts +0 -381
- package/src/billing/billing-lock.ts +0 -108
- package/src/billing/billing-overview.ts +0 -157
- package/src/billing/billing-standing.ts +0 -80
- package/src/billing/cancel-request.ts +0 -97
- package/src/billing/cancel-subscription.tsx +0 -98
- package/src/billing/checkout-processing.ts +0 -394
- package/src/billing/checkout-return.ts +0 -90
- package/src/billing/pay-page.ts +0 -330
- package/src/billing/plan-card.tsx +0 -562
- package/src/billing/plan-change-labels.ts +0 -66
- package/src/billing/plan-limit-notice.tsx +0 -148
- package/src/billing/plan-limit-toast.tsx +0 -90
- package/src/billing/plan-limit.ts +0 -52
- package/src/billing/plan-price.ts +0 -98
- package/src/billing/plan-switch.tsx +0 -216
- package/src/billing/provider-page.ts +0 -84
- package/src/billing/subscription-actions.tsx +0 -247
- package/src/billing/subscription-status-chip.tsx +0 -95
- package/src/chart.ts +0 -103
- package/src/components/account-chips-labels.ts +0 -16
- package/src/components/account-chips.tsx +0 -474
- package/src/components/alert-banner.tsx +0 -670
- package/src/components/amount-input.tsx +0 -815
- package/src/components/appearance-labels.ts +0 -63
- package/src/components/authed-image-labels.ts +0 -11
- package/src/components/authed-image.tsx +0 -209
- package/src/components/autocomplete.tsx +0 -645
- package/src/components/breadcrumbs-labels.ts +0 -10
- package/src/components/breadcrumbs.tsx +0 -205
- package/src/components/bulk-action-bar-labels.ts +0 -11
- package/src/components/bulk-action-bar.tsx +0 -443
- package/src/components/button-group.tsx +0 -223
- package/src/components/button-look.ts +0 -256
- package/src/components/button.tsx +0 -339
- package/src/components/calculator.tsx +0 -341
- package/src/components/calendar-heatmap-labels.ts +0 -15
- package/src/components/calendar-heatmap.tsx +0 -594
- package/src/components/card.tsx +0 -377
- package/src/components/chart-edge.ts +0 -106
- package/src/components/chart-figure.tsx +0 -35
- package/src/components/chart-height.ts +0 -30
- package/src/components/chart-zoom-core.tsx +0 -627
- package/src/components/chart-zoom.tsx +0 -22
- package/src/components/chart.tsx +0 -554
- package/src/components/checkbox-group.tsx +0 -247
- package/src/components/checkbox.tsx +0 -346
- package/src/components/chip-input.tsx +0 -370
- package/src/components/chip-labels.ts +0 -13
- package/src/components/chip.tsx +0 -843
- package/src/components/choice-card.tsx +0 -544
- package/src/components/column-mapper-labels.ts +0 -38
- package/src/components/column-mapper.tsx +0 -655
- package/src/components/combobox-core.tsx +0 -1321
- package/src/components/combobox-suggestions.tsx +0 -131
- package/src/components/combobox.tsx +0 -716
- package/src/components/confirm-context.ts +0 -7
- package/src/components/confirm-dialog-labels.ts +0 -13
- package/src/components/confirm-dialog.tsx +0 -341
- package/src/components/copy-button-labels.ts +0 -13
- package/src/components/copy-button.tsx +0 -192
- package/src/components/country-select-labels.ts +0 -11
- package/src/components/country-select.tsx +0 -539
- package/src/components/currency-option-list.tsx +0 -180
- package/src/components/currency-select.tsx +0 -341
- package/src/components/danger-confirm-labels.ts +0 -20
- package/src/components/danger-confirm.tsx +0 -934
- package/src/components/data-table-cells.tsx +0 -118
- package/src/components/data-table-column-settings.tsx +0 -198
- package/src/components/data-table-filter-popover.tsx +0 -281
- package/src/components/data-table-filters.ts +0 -287
- package/src/components/data-table-fit.ts +0 -301
- package/src/components/data-table-groups.tsx +0 -288
- package/src/components/data-table-head.tsx +0 -280
- package/src/components/data-table-labels.ts +0 -198
- package/src/components/data-table-leading-row.tsx +0 -58
- package/src/components/data-table-mobile.tsx +0 -1117
- package/src/components/data-table-pagination.tsx +0 -202
- package/src/components/data-table-resize.ts +0 -188
- package/src/components/data-table-row-actions.tsx +0 -302
- package/src/components/data-table-row.tsx +0 -496
- package/src/components/data-table-selection.ts +0 -71
- package/src/components/data-table-shared.tsx +0 -287
- package/src/components/data-table-sort.ts +0 -163
- package/src/components/data-table-virtual.ts +0 -576
- package/src/components/data-table.tsx +0 -2598
- package/src/components/date-field.tsx +0 -684
- package/src/components/date-format-setting.tsx +0 -64
- package/src/components/date-picker.tsx +0 -404
- package/src/components/date-range-picker.tsx +0 -518
- package/src/components/description-list-labels.ts +0 -9
- package/src/components/description-list.tsx +0 -386
- package/src/components/dialog-frame-labels.ts +0 -7
- package/src/components/dialog-frame.tsx +0 -276
- package/src/components/disclosure.tsx +0 -476
- package/src/components/dropdown.tsx +0 -461
- package/src/components/empty-state.tsx +0 -136
- package/src/components/entity-combobox.tsx +0 -422
- package/src/components/error-boundary-labels.ts +0 -20
- package/src/components/error-boundary.tsx +0 -675
- package/src/components/facing-pair.ts +0 -93
- package/src/components/field-base-labels.ts +0 -20
- package/src/components/field-base.tsx +0 -656
- package/src/components/field-parts.tsx +0 -825
- package/src/components/field-strip.tsx +0 -129
- package/src/components/field.tsx +0 -206
- package/src/components/file-button.tsx +0 -295
- package/src/components/file-dropzone.tsx +0 -559
- package/src/components/floating-panel-labels.ts +0 -10
- package/src/components/floating-panel.tsx +0 -828
- package/src/components/focus-ring.ts +0 -16
- package/src/components/form-actions-labels.ts +0 -11
- package/src/components/form-actions.tsx +0 -578
- package/src/components/full-bleed-dialog.tsx +0 -294
- package/src/components/grouped-picker.tsx +0 -184
- package/src/components/help-popover.tsx +0 -117
- package/src/components/hover-menu.tsx +0 -343
- package/src/components/iban-input-labels.ts +0 -14
- package/src/components/iban-input.tsx +0 -236
- package/src/components/icon-button.tsx +0 -959
- package/src/components/icon-picker-labels.ts +0 -13
- package/src/components/icon-picker.tsx +0 -232
- package/src/components/image-grid-labels.ts +0 -11
- package/src/components/image-grid.tsx +0 -202
- package/src/components/inline-confirm.tsx +0 -116
- package/src/components/inline-edit-field-labels.ts +0 -11
- package/src/components/inline-edit-field.tsx +0 -552
- package/src/components/inline-entity-combobox.tsx +0 -849
- package/src/components/input.tsx +0 -394
- package/src/components/jump-calendar.tsx +0 -265
- package/src/components/language-select.tsx +0 -93
- package/src/components/legal-labels.ts +0 -105
- package/src/components/legal-page.tsx +0 -711
- package/src/components/legal.tsx +0 -668
- package/src/components/lightbox-labels.ts +0 -18
- package/src/components/lightbox.tsx +0 -399
- package/src/components/line-items-labels.ts +0 -14
- package/src/components/line-items.tsx +0 -602
- package/src/components/list-labels.ts +0 -12
- package/src/components/list.tsx +0 -870
- package/src/components/loading-state.tsx +0 -78
- package/src/components/measured-grid-labels.ts +0 -25
- package/src/components/measured-grid.tsx +0 -837
- package/src/components/menu-item.tsx +0 -241
- package/src/components/mini-calendar-labels.ts +0 -17
- package/src/components/mini-calendar.tsx +0 -789
- package/src/components/modal.tsx +0 -505
- package/src/components/money-draft.ts +0 -83
- package/src/components/money-field.tsx +0 -76
- package/src/components/month-grid.tsx +0 -229
- package/src/components/month-picker-labels.ts +0 -22
- package/src/components/month-picker.tsx +0 -1087
- package/src/components/multi-entity-combobox.tsx +0 -369
- package/src/components/multi-select.tsx +0 -376
- package/src/components/nav-pills.tsx +0 -198
- package/src/components/number-field.tsx +0 -327
- package/src/components/number-input.tsx +0 -558
- package/src/components/numeric-field.ts +0 -169
- package/src/components/numpad-sheet.tsx +0 -285
- package/src/components/one-time-code-input.tsx +0 -270
- package/src/components/page-contents-labels.ts +0 -9
- package/src/components/page-contents.tsx +0 -282
- package/src/components/page-header-context.ts +0 -8
- package/src/components/page-header.tsx +0 -298
- package/src/components/password-strength-labels.ts +0 -22
- package/src/components/password-strength.tsx +0 -241
- package/src/components/phone-input-labels.ts +0 -10
- package/src/components/phone-input.tsx +0 -282
- package/src/components/picker-sheet.tsx +0 -330
- package/src/components/pie-chart-labels.ts +0 -40
- package/src/components/pie-chart.tsx +0 -480
- package/src/components/popover-labels.ts +0 -14
- package/src/components/popover.tsx +0 -268
- package/src/components/progress-bar-labels.ts +0 -10
- package/src/components/progress-bar.tsx +0 -479
- package/src/components/qr-code.tsx +0 -76
- package/src/components/reauth-dialog-labels.ts +0 -13
- package/src/components/reauth-dialog.tsx +0 -265
- package/src/components/row-actions-labels.ts +0 -12
- package/src/components/row-actions.tsx +0 -795
- package/src/components/scroll-area.tsx +0 -123
- package/src/components/search-field.tsx +0 -192
- package/src/components/select.tsx +0 -294
- package/src/components/separator.tsx +0 -33
- package/src/components/series-chart-axes.ts +0 -342
- package/src/components/series-chart-budget.ts +0 -150
- package/src/components/series-chart-labels.ts +0 -33
- package/src/components/series-chart-thin.ts +0 -180
- package/src/components/series-chart-ticks.ts +0 -194
- package/src/components/series-chart.tsx +0 -1906
- package/src/components/server-wake-labels.ts +0 -11
- package/src/components/server-wake.tsx +0 -129
- package/src/components/settings-fields.tsx +0 -237
- package/src/components/share-card-labels.ts +0 -30
- package/src/components/share-card.tsx +0 -675
- package/src/components/signature-pad-labels.ts +0 -24
- package/src/components/signature-pad.tsx +0 -747
- package/src/components/signed-amount-labels.ts +0 -10
- package/src/components/signed-amount.tsx +0 -336
- package/src/components/skeleton.tsx +0 -80
- package/src/components/slider.tsx +0 -338
- package/src/components/sparkline-labels.ts +0 -14
- package/src/components/sparkline.tsx +0 -322
- package/src/components/spinner.tsx +0 -81
- package/src/components/stat-tile-labels.ts +0 -15
- package/src/components/stat-tile.tsx +0 -725
- package/src/components/status-dot.tsx +0 -170
- package/src/components/swatch-picker-labels.ts +0 -10
- package/src/components/swatch-picker.tsx +0 -283
- package/src/components/swipeable-row.tsx +0 -386
- package/src/components/switch.tsx +0 -273
- package/src/components/table-skeleton.tsx +0 -136
- package/src/components/table.tsx +0 -647
- package/src/components/tabs-labels.ts +0 -13
- package/src/components/tabs.tsx +0 -693
- package/src/components/text-link.tsx +0 -359
- package/src/components/text.tsx +0 -150
- package/src/components/textarea.tsx +0 -330
- package/src/components/tile-radio.tsx +0 -332
- package/src/components/time-input.tsx +0 -121
- package/src/components/toast-labels.ts +0 -12
- package/src/components/toast.tsx +0 -519
- package/src/components/toggle-group.tsx +0 -752
- package/src/components/toggle-legend.tsx +0 -394
- package/src/components/tooltip.tsx +0 -1149
- package/src/components/translation-review-editor.tsx +0 -290
- package/src/components/translation-review-labels.ts +0 -181
- package/src/components/translation-review-parts.tsx +0 -272
- package/src/components/translation-review.tsx +0 -1240
- package/src/components/tree-view.tsx +0 -589
- package/src/components/treemap-labels.ts +0 -47
- package/src/components/treemap-layout.ts +0 -210
- package/src/components/treemap.tsx +0 -1184
- package/src/components/trigger-aria.ts +0 -44
- package/src/components/use-anchor-dir.ts +0 -20
- package/src/components/use-mobile-reveal.ts +0 -62
- package/src/components/use-table-state.ts +0 -753
- package/src/components/user-avatar.tsx +0 -116
- package/src/components/wizard-stepper.tsx +0 -42
- package/src/components/write-lock-labels.ts +0 -10
- package/src/components/write-lock.tsx +0 -253
- package/src/currencies.ts +0 -70
- package/src/data-table.ts +0 -42
- package/src/demo/demo-banner.tsx +0 -153
- package/src/demo/demo-countdown.ts +0 -136
- package/src/demo/demo-ended.tsx +0 -90
- package/src/demo/demo-labels.ts +0 -78
- package/src/demo/demo-session.ts +0 -32
- package/src/demo/demo-start.tsx +0 -156
- package/src/eslint/index.ts +0 -161
- package/src/eslint/role-utilities.generated.ts +0 -146
- package/src/eslint/role-utilities.ts +0 -233
- package/src/feedback/feedback-attachment-download.tsx +0 -77
- package/src/feedback/feedback-attachment-labels.ts +0 -37
- package/src/feedback/feedback-attachment.tsx +0 -1239
- package/src/feedback/feedback-capture.ts +0 -287
- package/src/feedback/feedback-comment-thread.tsx +0 -353
- package/src/feedback/feedback-context-labels.ts +0 -10
- package/src/feedback/feedback-context.tsx +0 -213
- package/src/feedback/feedback-crash.ts +0 -547
- package/src/feedback/feedback-dialog-labels.ts +0 -29
- package/src/feedback/feedback-dialog.tsx +0 -440
- package/src/feedback/feedback-entry-file.tsx +0 -150
- package/src/feedback/feedback-inbox.tsx +0 -552
- package/src/feedback/feedback-labels.ts +0 -144
- package/src/feedback/feedback-menu-labels.ts +0 -12
- package/src/feedback/feedback-menu.tsx +0 -177
- package/src/feedback/feedback-mutation.ts +0 -20
- package/src/feedback/feedback-page-link.ts +0 -90
- package/src/feedback/feedback-public-labels.ts +0 -81
- package/src/feedback/feedback-public-list.tsx +0 -524
- package/src/feedback/feedback-public-switch.tsx +0 -102
- package/src/feedback/feedback-public-types.ts +0 -101
- package/src/feedback/feedback-record.ts +0 -423
- package/src/feedback/feedback-replies-context.ts +0 -9
- package/src/feedback/feedback-replies.tsx +0 -343
- package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
- package/src/feedback/feedback-row-detail-labels.ts +0 -30
- package/src/feedback/feedback-row-detail.tsx +0 -858
- package/src/feedback/feedback-status-undo.ts +0 -109
- package/src/feedback/feedback-submit-dialog.tsx +0 -118
- package/src/feedback/feedback-submit.tsx +0 -237
- package/src/feedback/feedback-swipe.tsx +0 -192
- package/src/feedback/feedback-table-labels.ts +0 -27
- package/src/feedback/feedback-table.tsx +0 -912
- package/src/feedback/feedback-thread-labels.ts +0 -22
- package/src/feedback/feedback-thread-parts-labels.ts +0 -41
- package/src/feedback/feedback-thread.tsx +0 -905
- package/src/feedback/feedback-vocabulary.ts +0 -263
- package/src/feedback/feedback-vote-button.tsx +0 -124
- package/src/feedback.ts +0 -116
- package/src/hooks/use-anchored-panel.ts +0 -396
- package/src/hooks/use-anchored-rect.ts +0 -77
- package/src/hooks/use-announce.ts +0 -85
- package/src/hooks/use-authed-src.ts +0 -167
- package/src/hooks/use-body-scroll-lock.ts +0 -119
- package/src/hooks/use-breakpoint.ts +0 -133
- package/src/hooks/use-close-transition.ts +0 -107
- package/src/hooks/use-copy-to-clipboard.ts +0 -122
- package/src/hooks/use-debounce.ts +0 -114
- package/src/hooks/use-dismiss.ts +0 -371
- package/src/hooks/use-document-title.ts +0 -33
- package/src/hooks/use-file-drop.ts +0 -121
- package/src/hooks/use-file-picker-labels.ts +0 -28
- package/src/hooks/use-file-picker.tsx +0 -359
- package/src/hooks/use-focus-trap.ts +0 -199
- package/src/hooks/use-hotkey.ts +0 -189
- package/src/hooks/use-large-text.ts +0 -41
- package/src/hooks/use-long-press.ts +0 -140
- package/src/hooks/use-media-query.ts +0 -39
- package/src/hooks/use-mounted.ts +0 -39
- package/src/hooks/use-noindex.ts +0 -37
- package/src/hooks/use-overlay-history.ts +0 -714
- package/src/hooks/use-row-swipe.ts +0 -207
- package/src/hooks/use-search-param-state.ts +0 -434
- package/src/hooks/use-windowed-rows.ts +0 -225
- package/src/i18n/defaults.ts +0 -267
- package/src/i18n/german.ts +0 -2057
- package/src/i18n/kit-labels.tsx +0 -1003
- package/src/i18n/languages.ts +0 -349
- package/src/i18n/locales/de-CH.ts +0 -24
- package/src/i18n/locales/en.ts +0 -267
- package/src/i18n/locales/es.ts +0 -1997
- package/src/i18n/locales/fr.ts +0 -2035
- package/src/i18n/locales/hu.ts +0 -2002
- package/src/i18n/locales/it.ts +0 -2012
- package/src/i18n/locales/zh.ts +0 -1910
- package/src/i18n/plurals.ts +0 -86
- package/src/i18n/review.ts +0 -475
- package/src/i18n/shared-labels.ts +0 -138
- package/src/i18n/swiss.ts +0 -29
- package/src/index.ts +0 -1179
- package/src/landing/access.ts +0 -93
- package/src/landing/landing-actions.tsx +0 -97
- package/src/landing/landing-labels.ts +0 -59
- package/src/landing/landing-sections.tsx +0 -341
- package/src/landing/page-seo.ts +0 -261
- package/src/landing/public-header.tsx +0 -121
- package/src/landing/public-page.tsx +0 -106
- package/src/landing/routing.tsx +0 -225
- package/src/lib/broadcast.ts +0 -52
- package/src/lib/calc.ts +0 -323
- package/src/lib/chip-edge-snap.ts +0 -212
- package/src/lib/clipboard-minus.ts +0 -107
- package/src/lib/clipping.ts +0 -34
- package/src/lib/cn.ts +0 -28
- package/src/lib/column-mapping.ts +0 -234
- package/src/lib/countries.ts +0 -91
- package/src/lib/data-attributes.ts +0 -14
- package/src/lib/date-format.ts +0 -227
- package/src/lib/dates.ts +0 -266
- package/src/lib/day-format.ts +0 -96
- package/src/lib/decimal-marks.ts +0 -108
- package/src/lib/describe-failure.ts +0 -18
- package/src/lib/device-settings.ts +0 -147
- package/src/lib/direction.ts +0 -74
- package/src/lib/document-navigation.ts +0 -17
- package/src/lib/document-title.ts +0 -41
- package/src/lib/download.ts +0 -172
- package/src/lib/float-clearance.ts +0 -70
- package/src/lib/floating-stack.ts +0 -70
- package/src/lib/focus-left.ts +0 -156
- package/src/lib/format.ts +0 -522
- package/src/lib/http-status.ts +0 -38
- package/src/lib/iban.ts +0 -251
- package/src/lib/ime.ts +0 -24
- package/src/lib/intl-cache.ts +0 -25
- package/src/lib/iso-day.ts +0 -13
- package/src/lib/lazy-with-fallback.ts +0 -82
- package/src/lib/logger.ts +0 -69
- package/src/lib/minus.ts +0 -50
- package/src/lib/path-pattern.ts +0 -55
- package/src/lib/person-name.ts +0 -155
- package/src/lib/phone.ts +0 -208
- package/src/lib/plain-text.ts +0 -22
- package/src/lib/presence.ts +0 -18
- package/src/lib/qr-encode.ts +0 -444
- package/src/lib/quiet-focus.ts +0 -32
- package/src/lib/reduced-motion.ts +0 -22
- package/src/lib/refs.ts +0 -22
- package/src/lib/round.ts +0 -43
- package/src/lib/safe-storage.ts +0 -119
- package/src/lib/server-wake.ts +0 -315
- package/src/lib/session-refresh.ts +0 -472
- package/src/lib/share-format.ts +0 -18
- package/src/lib/status-tone.ts +0 -43
- package/src/lib/strip-fade.ts +0 -287
- package/src/lib/table-text.ts +0 -554
- package/src/lib/text-case.ts +0 -19
- package/src/lib/text-match.ts +0 -169
- package/src/lib/thenable.ts +0 -19
- package/src/lib/translation-review.ts +0 -717
- package/src/notifications/device-zone-report.ts +0 -67
- package/src/notifications/feedback-notification-types-labels.ts +0 -33
- package/src/notifications/feedback-notification-types.ts +0 -71
- package/src/notifications/notification-bell.tsx +0 -119
- package/src/notifications/notification-inbox.tsx +0 -309
- package/src/notifications/notification-parts.tsx +0 -81
- package/src/notifications/notification-types-setting.tsx +0 -406
- package/src/notifications/notification-types.ts +0 -89
- package/src/notifications/notifications-labels.ts +0 -287
- package/src/notifications/push-device-setting.tsx +0 -230
- package/src/notifications/push-devices-setting.tsx +0 -374
- package/src/notifications/push-worker.ts +0 -168
- package/src/notifications/quiet-hours-setting.tsx +0 -238
- package/src/notifications/unsubscribe-panel.tsx +0 -200
- package/src/notifications/use-push-device.ts +0 -448
- package/src/offline/local-first.ts +0 -100
- package/src/offline/pwa-update-prompt-labels.ts +0 -9
- package/src/offline/pwa-update-prompt.tsx +0 -78
- package/src/offline/query-persistence.ts +0 -391
- package/src/offline/service-worker-updates.ts +0 -81
- package/src/offline/shared-computer-labels.ts +0 -8
- package/src/offline/shared-computer.tsx +0 -87
- package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
- package/src/offline/sign-out-pending-dialog.tsx +0 -135
- package/src/offline.ts +0 -37
- package/src/push-worker.ts +0 -25
- package/src/rhf/fields.tsx +0 -2187
- package/src/rhf/form-core.tsx +0 -324
- package/src/rhf/form.tsx +0 -12
- package/src/rhf/line-items.tsx +0 -135
- package/src/rhf/use-rhf-wizard-step.ts +0 -104
- package/src/rhf.ts +0 -21
- package/src/search/command-palette-labels.ts +0 -16
- package/src/search/command-palette.tsx +0 -624
- package/src/search/global-search-labels.ts +0 -13
- package/src/search/global-search.tsx +0 -660
- package/src/search/search-index.ts +0 -283
- package/src/search/use-command-key.ts +0 -23
- package/src/search.ts +0 -14
- package/src/settings/device-reset-setting-labels.ts +0 -20
- package/src/settings/device-reset-setting.tsx +0 -116
- package/src/settings/settings-card-classes.ts +0 -11
- package/src/settings/settings-catalogue.ts +0 -150
- package/src/settings/settings-context.ts +0 -33
- package/src/settings/settings-heading.tsx +0 -67
- package/src/settings/settings-labels.ts +0 -87
- package/src/settings/settings-layout.tsx +0 -471
- package/src/settings/settings-section.tsx +0 -108
- package/src/settings/use-account-appearance.ts +0 -148
- package/src/settings/use-account-language.ts +0 -146
- package/src/settings/use-settings-focus.ts +0 -128
- package/src/settings/use-settings-route.ts +0 -258
- package/src/shell/account-menu-language.tsx +0 -130
- package/src/shell/account-menu-text-size.tsx +0 -80
- package/src/shell/admin-menu.tsx +0 -183
- package/src/shell/app-shell-labels.ts +0 -11
- package/src/shell/app-shell.tsx +0 -1560
- package/src/shell/auth-layout.tsx +0 -186
- package/src/shell/company-switcher-labels.ts +0 -13
- package/src/shell/company-switcher.tsx +0 -136
- package/src/shell/dev-menu.tsx +0 -257
- package/src/shell/fill-height.tsx +0 -45
- package/src/shell/language-code.ts +0 -5
- package/src/shell/option-switcher-menu.tsx +0 -84
- package/src/shell/sync-status-indicator.tsx +0 -303
- package/src/shell/top-bar-actions.tsx +0 -87
- package/src/shell/top-bar-brand.tsx +0 -78
- package/src/shell/top-bar-parts-labels.ts +0 -148
- package/src/shell/top-bar.tsx +0 -40
- package/src/shell/topbar-action-menu.tsx +0 -567
- package/src/shell/topbar-controls.tsx +0 -269
- package/src/shell/tour-menu.tsx +0 -187
- package/src/shell.ts +0 -108
- package/src/sync/db.ts +0 -129
- package/src/sync/engine.ts +0 -1011
- package/src/sync/hooks.ts +0 -98
- package/src/sync/testing.ts +0 -73
- package/src/sync/wire.ts +0 -61
- package/src/sync.ts +0 -37
- package/src/table-text.ts +0 -8
- package/src/testing/css-variables.generated.ts +0 -112
- package/src/testing/css-variables.ts +0 -189
- package/src/testing/index.ts +0 -29
- package/src/testing/react.ts +0 -58
- package/src/testing/tour.ts +0 -41
- package/src/testing/viewport.ts +0 -357
- package/src/theme/chart-palette.ts +0 -232
- package/src/theme/color.ts +0 -272
- package/src/theme/contrast-tokens.ts +0 -268
- package/src/theme/contrast.ts +0 -250
- package/src/theme/palette-derive.ts +0 -825
- package/src/theme/palette-presets.ts +0 -544
- package/src/theme/palette-store.ts +0 -108
- package/src/theme/text-size.ts +0 -289
- package/src/theme/theme-store.ts +0 -157
- package/src/tour/tour-catalogue-labels.ts +0 -75
- package/src/tour/tour-catalogue.tsx +0 -335
- package/src/tour/tour-dock.ts +0 -134
- package/src/tour/tour-engine.ts +0 -37
- package/src/tour/tour-gates.ts +0 -218
- package/src/tour/tour-guide-button.tsx +0 -64
- package/src/tour/tour-guide-labels.ts +0 -15
- package/src/tour/tour-info.ts +0 -144
- package/src/tour/tour-labels.ts +0 -23
- package/src/tour/tour-overlay.tsx +0 -1170
- package/src/tour/tour.tsx +0 -750
- package/src/tour/use-tour-progress.ts +0 -671
- package/src/tour/use-tour-url-sync.ts +0 -266
- package/src/tour.ts +0 -21
- package/src/vite-env.d.ts +0 -23
- package/src/wizard/stepper-nav.tsx +0 -360
- package/src/wizard/types.ts +0 -275
- package/src/wizard/use-wizard.ts +0 -560
- package/src/wizard/validation.ts +0 -39
- package/src/wizard/wizard-context.tsx +0 -74
- package/src/wizard/wizard-labels.ts +0 -29
- package/src/wizard/wizard-step.tsx +0 -76
- package/src/wizard/wizard-summary.tsx +0 -84
- package/src/wizard.ts +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { Spinner } from \"./spinner\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n anchoredHeadingClass,\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GroupedOptions,\n groupRuns,\n isOptionEnabled,\n stepEnabled,\n SUGGESTION_LIST_CLASS,\n suggestionRowClass,\n useActiveOptionScroll,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockedReason,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends\n Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n >,\n FieldHelpProps {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10): it was\n * `forwardRef`, cast back to a generic function, with a `displayName` line beside it. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n *\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport function Autocomplete<V extends string | number = string>(props: AutocompleteProps<V>) {\n const {\n ref,\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n help,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n // A form library's `aria-invalid` paints as well as announces (DUP-8: it only\n // announced, through the spread below).\n const field = useFieldError(error, invalid, hintParts.describedBy, rest[\"aria-invalid\"]);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = hasContent(status);\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, bottom, maxHeight, above } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n // The list is a layer on the kit's Escape stack while it shows (Wave 2, DUP-1).\n useDismissLayer(expanded, close);\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy = ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) =>\n locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"block\">\n {box}\n </LockedReason>\n ) : (\n box\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-muted\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n // At Large the label stands above the field and the strip is gone\n // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // The tour's focus on arrival (§11.8) looks nothing up: the list would\n // cover its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={(e) => {\n rest.onClick?.(e);\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={(e) => {\n quietRef.current = false;\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded && takeEscape(e)) return;\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone. Nor is a row taken by the Enter that confirms an IME\n // composition, which is the IME's (BUG-9).\n if (expanded && activeId && !isImeEnter(e)) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).\n data-kit-layer=\"\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n // The family's suggestion box (DUP-4), as a column: the list scrolls inside\n // it and the status line stays under the rows.\n className={cn(SUGGESTION_LIST_CLASS, \"fixed z-50 flex flex-col overflow-hidden py-0\")}\n // Flipped above, the list hangs from its bottom edge: three rows under a\n // 256 px cap, placed by `top`, floated ~176 px above the field (keksdose K1),\n // and the rows come and go as the user types.\n style={{ ...(above ? { bottom } : { top }), left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {/* One heading per group, a `role=\"group\"` it names, as in the core panel\n (refactor audit 2026-10-30, INP-8), drawn by the same renderer: an\n option may appear once per group. */}\n <GroupedOptions\n runs={groupRuns(results, (o) => o.group)}\n listboxId={listboxId}\n rowKey={(o) => String(o.value)}\n headingClassName={(run) => anchoredHeadingClass(run)}\n row={(o, i) => (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n // The family's suggestion row (DUP-4): it hand-rolled its\n // own with a plain `truncate`, so a long address was cut at\n // Large, where Combobox's rows wrap (text size §10.17, T1).\n // A flex row here, for the icon and the two lines.\n suggestionRowClass(i === active && !o.disabled),\n \"flex items-center gap-2\",\n small && \"py-1 text-xs\",\n o.disabled && DISABLED_ROW_CLASS,\n o.group && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-muted\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large text-primary\">{o.label}</span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate-until-large text-placeholder\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n )}\n />\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-danger\" : \"text-muted\",\n results.length > 0 && \"border-t border-subtle text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/autocomplete.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { FIELD_BASE, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { Spinner } from \"./spinner\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { takeEscape, useDismissLayer } from \"../hooks/use-dismiss\";\nimport { isImeEnter } from \"../lib/ime\";\nimport { isQuietFocus } from \"../lib/quiet-focus\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport {\n anchoredHeadingClass,\n ComboboxFieldLabel,\n DISABLED_ROW_CLASS,\n endHintRowProps,\n GroupedOptions,\n groupRuns,\n isOptionEnabled,\n stepEnabled,\n SUGGESTION_LIST_CLASS,\n suggestionRowClass,\n useActiveOptionScroll,\n useOptionSource,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport {\n EndHintRow,\n FieldCaption,\n LABEL_IN_ROW,\n LockFrame,\n mergeDescribedBy,\n StaticLabelRow,\n useFieldError,\n useFieldHint,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\n\n/**\n * `value`/`onChange` are the TEXT's, and `onSelect` is \"a suggestion was taken\" —\n * not the DOM's text-selection event — so the input's own spellings of those three\n * are omitted. Everything else reaches the `<input>`, the way it does on {@link Input}.\n */\nexport interface AutocompleteProps<V extends string | number = string>\n extends\n Omit<\n ComponentPropsWithoutRef<\"input\">,\n \"value\" | \"defaultValue\" | \"onChange\" | \"onSelect\" | \"children\" | \"type\" | \"size\"\n >,\n FieldHelpProps {\n /** What is in the field. Controlled, and never reset by the component: opening,\n * closing and a failed lookup all leave it exactly as typed. */\n value: string;\n /** Every keystroke — and, with `fillOnSelect`, the label of a taken suggestion. */\n onChange: (text: string) => void;\n /** Suggestions the caller already has (e.g. from its own query hook). Narrowed by\n * the text unless `filter={false}`. An option with `disabled: true` is listed but\n * cannot be taken — see {@link ComboOption.disabled}. */\n options?: ComboOption<V>[];\n /** Or: fetch them. Debounced (`debounceMs`), race-safe, only at `minChars` and up,\n * never while the field is idle; a rejection shows `loadErrorLabel` and empties\n * the list rather than leaving the previous text's rows under it. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** Narrow `options` by label/sublabel substring. Default `true`. Pass `false` for\n * a list a server has already searched and ranked — it is shown as given. */\n filter?: boolean;\n /** External loading flag (a caller-side fetch), OR-ed with `loadOptions`'. */\n loading?: boolean;\n /** Below this many characters nothing is offered or fetched. Default 1: an empty\n * field asks nothing — focusing it is not a search. */\n minChars?: number;\n /** Quiet time before `loadOptions` runs. Default 300 ms — an ordinary typist's\n * inter-key interval is 150–250 ms, so the pickers' 150 fires on nearly every\n * letter, which a rate-limited geocoder cannot afford. */\n debounceMs?: number;\n /**\n * Whether the list MAY show — controlled. Omit (or pass `undefined`) and the field\n * decides: open while focused, until Escape, Tab or a taken row. `false` keeps it\n * shut whatever the user does, and stops `loadOptions` asking — keksdose's address\n * search, with its confirm map up, where it used to pass `minChars={Infinity}`.\n * `true` shows it even unfocused.\n *\n * Even open, the list only APPEARS when it has something to show (rows, or a\n * status line); `aria-expanded` follows what appears, not this flag.\n */\n open?: boolean;\n /** The field wants the list open or shut — focus, typing, ↓/↑ open it; blur,\n * Escape, Tab and taking a row shut it. Called on every change, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `\"sm\"`: the 28px, 12px-type field of {@link Select}'s `size=\"sm\"`, with the icon,\n * spinner and list rows scaled to match — for a card or toolbar whose buttons are\n * small. `\"md\"` (default) is the form field. Unlabelled only, as on `Select`: a\n * floating label needs the tall box, so a labelled field ignores `\"sm\"`.\n *\n * A NUMBER is still the native `size` attribute (the width in characters), passed\n * straight through.\n */\n size?: \"sm\" | \"md\" | number;\n /** A suggestion was taken (click, or Enter on the highlighted row). */\n onSelect?: (option: ComboOption<V>) => void;\n /**\n * Put the taken suggestion's label into the field (`onChange(label)`). Default\n * `true` — the free-text case, where the text IS the value and a suggestion only\n * completes it (an address field). Pass `false` where taking a row is an ACTION\n * and the field is only the search that found it; the text then stays as typed.\n */\n fillOnSelect?: boolean;\n /** A floating label, as on {@link Combobox} — a real `<label for>` on the input.\n * Names the field (`aria-labelledby`) unless an `aria-label` is given. */\n label?: ReactNode;\n /** Leading decoration inside the field (a pin, a magnifier). Decorative: the\n * label names the field. An `<svg>` child is sized by the field — 16px, or 14px\n * at `size=\"sm\"` — so a lucide icon needs no class of its own. */\n icon?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}. */\n invalid?: boolean;\n /** What is wrong with the VALUE, as {@link Input}'s `error`: under the field, on\n * its `aria-describedby`, implies `invalid`. Not a failed lookup — that is\n * `loadErrorLabel`, shown in the list. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so this field reads like every other\n * (keksdose K4): plain TEXT is a caption under the field on the input's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the field's end edge, outside the box. Not the list's\n * `status` line, which talks about the lookup, not the field.\n *\n * Pass it — even as `undefined` — on a field whose hint comes and goes (0.24): the\n * unlabelled field then keeps its end-hint row whether or not a \"?\" is in it, so the\n * `<input>` is never re-parented, and focus and caret survive the toggle. The rule\n * the rest of the family has had since 0.23 (and the fields since 0.22).\n */\n hint?: ReactNode;\n /**\n * The caller's own line in the list, replacing the automatic one (loading / failed\n * / no results). For what only the caller knows: \"offline\", \"the address service\n * is switched off\", a privacy note, the hint a caller-fetched list needs below its\n * own minimum. While set, the list opens even with no rows. Announced politely.\n */\n status?: ReactNode;\n /** Default: `combobox.noResults` from the {@link UiKitProvider}, else English. */\n emptyLabel?: string;\n /** Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n /** Classes for the `<input>` itself; `className` styles the wrapper. */\n inputClassName?: string;\n /**\n * Why the field cannot be changed — {@link Button}'s `disabledReason`, the Combobox\n * family's write lock (keksdose G2, 0.23), for a field whose pick SAVES: an address\n * search whose taken row writes the address, a lookup that links a record.\n *\n * With a reason the input stays focusable and `aria-disabled`, is `readOnly` (the\n * settled grey FIELD_BASE gives a `[readonly]` field), opens no list and asks no\n * `loadOptions` — whatever `open` says — and nothing reaches `onChange`, `onSelect` or\n * `onOpenChange(true)`. Enter is swallowed, so a locked field does not submit the form\n * around it, and a caller's `onKeyDown` does not hear it; every other key still\n * reaches the caller (an Escape that closes its panel) and the caret. The reason is in\n * the kit {@link Tooltip} and on the input's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /**\n * This field COMMITS. Under a locked {@link WriteLockProvider} it is locked the\n * `disabledReason` way with the lock's reason (which wins over its own). A search\n * whose result is only draft state — the form's Save writes it — leaves this off and\n * stays live under the lock. No provider, or an unlocked one: no effect.\n */\n commit?: CommitScope;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /** The `<input>`. A React 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10): it was\n * `forwardRef`, cast back to a generic function, with a `displayName` line beside it. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/**\n * An inline, async-capable text field with a suggestion list under it — the\n * WAI-ARIA APG \"editable combobox with list autocomplete\".\n *\n * Two jobs, one control:\n * - **free text** (kastlan's address field): the text is the value, the list only\n * offers completions, and taking one fills the field (`fillOnSelect`, default);\n * - **search-then-act** (keksdose's address search): the text is a query, taking a\n * row calls `onSelect` and nothing is held (`fillOnSelect={false}`).\n *\n * How it differs from its siblings: {@link Combobox} is free text over a STATIC\n * string pool and opens on focus; {@link EntityCombobox} is a trigger holding an id.\n * This one keeps no value of its own, never resets the text, asks nothing below\n * `minChars`, and reports a failed lookup instead of showing stale rows.\n *\n * Focus never leaves the `<input>`: `aria-activedescendant` names the highlighted\n * row, `aria-controls` the list, and a polite live region says what the list now\n * holds. Keyboard: ↓/↑ move (↓ opens) over every row but a `disabled` one, Enter\n * takes the highlighted row (with none,\n * Enter is left to the form), Escape closes, Tab closes and moves on; Home/End stay\n * with the caret. An Escape that closes an open list is consumed and never reaches the\n * caller; with the list closed it does. For every other key a caller's `onKeyDown`\n * runs first and may `preventDefault()` to\n * keep a key for itself.\n *\n * The list is portalled and anchored under the field on every screen size: the\n * field already carries the keyboard, and `useAnchoredPanel` keeps the list clear\n * of it on a phone. There is no full-screen sheet as {@link Combobox} has.\n *\n * Generic over the option value (`ComboOption<V>`); the ref is the `<input>`'s.\n */\nexport function Autocomplete<V extends string | number = string>(props: AutocompleteProps<V>) {\n const {\n ref,\n value,\n onChange,\n options,\n loadOptions,\n filter,\n loading,\n minChars = 1,\n debounceMs = 300,\n onSelect,\n fillOnSelect = true,\n label,\n icon,\n invalid,\n error,\n hint,\n help,\n status,\n emptyLabel,\n loadErrorLabel,\n open: openProp,\n onOpenChange,\n size,\n className,\n inputClassName,\n id,\n disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\n onKeyDown,\n onFocus,\n onBlur,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n } = props;\n const generated = useId();\n const fieldId = id ?? generated;\n // Off the GENERATED id: a caller's `id` is theirs to collide with.\n const listboxId = `${generated}-listbox`;\n const labelId = `${generated}-label`;\n const optionId = (index: number) => `${generated}-option-${index}`;\n const fieldRef = useRef<HTMLDivElement>(null);\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n noResults: emptyLabel,\n loadError: loadErrorLabel,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy, { help, name: label ?? ariaLabel });\n // A form library's `aria-invalid` paints as well as announces (DUP-8: it only\n // announced, through the spread below).\n const field = useFieldError(error, invalid, hintParts.describedBy, rest[\"aria-invalid\"]);\n\n /** The user's intent: the field is focused and the list has not been dismissed.\n * Whether anything SHOWS is decided below, from what there is to show. */\n const [ownOpen, setOwnOpen] = useState(false);\n // Controlled when the caller passes a boolean. The own state is kept up to date\n // either way, so a caller that controls only SOMETIMES (`open={confirming ? false :\n // undefined}`) hands back a field that is exactly as open as focus says it is.\n const open = openProp ?? ownOpen;\n // The field took the tour's quiet focus (§11.8) and has not opened since.\n const quietRef = useRef(false);\n const setOpen = (next: boolean) => {\n setOwnOpen(next);\n if (next !== open) onOpenChange?.(next);\n };\n const [active, setActive] = useState(-1);\n // The lock: focusable, `readOnly`, no list — see `disabledReason`. Folded into `live`,\n // so a locked field asks nothing and shows nothing even with `open` held true.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked;\n const live = open && !disabled && !locked;\n const { results, busy, failed, tooShort } = useOptionSource<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n query: value,\n active: live,\n });\n\n const hasStatus = hasContent(status);\n // The visible line under the rows. Nothing below `minChars` unless the caller has\n // something to say — an empty field opening a box that says \"type more\" is noise.\n const statusLine: ReactNode = hasStatus\n ? status\n : tooShort\n ? null\n : busy && results.length === 0\n ? labels.loading\n : failed\n ? labels.loadError\n : !busy && results.length === 0\n ? labels.noResults\n : null;\n const expanded = live && (results.length > 0 || statusLine !== null);\n // A disabled row is never the keyboard's — not even when a list that changed under\n // the highlight put one there.\n const activeId = expanded && isOptionEnabled(results[active]) ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n const { rect, top, bottom, maxHeight, above } = useAnchoredPanel(fieldRef, expanded, { preferredHeight: 256 });\n const dir = useAnchorDir(fieldRef, expanded);\n\n const close = () => {\n setOpen(false);\n setActive(-1);\n };\n // The list is a layer on the kit's Escape stack while it shows (Wave 2, DUP-1).\n useDismissLayer(expanded, close);\n const take = (o: ComboOption<V>) => {\n if (o.disabled || locked) return;\n if (fillOnSelect) onChange(o.label);\n onSelect?.(o);\n close();\n };\n\n const hasLabel = label !== undefined;\n // `size=\"sm\"` is for an UNLABELLED field, as on Select: the floating label needs the\n // tall box. A number is the native attribute.\n const small = size === \"sm\" && !hasLabel;\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // The caller's name wins; else the floating label, by reference so a non-string\n // label still names the field.\n const labelledBy = ariaLabel === undefined ? (ariaLabelledBy ?? (hasLabel ? labelId : undefined)) : undefined;\n // While locked the field sits in the reason's Tooltip — in a fragment with a hidden\n // copy of the reason, Button's anatomy; `block` so a full-width field stays full width.\n const withLock = (box: ReactNode) => (\n <LockFrame lock={lock} display={disabledReasonDisplay}>\n {box}\n </LockFrame>\n );\n\n return (\n // `relative`: the floating label and the live region's `sr-only` both need a\n // local containing block.\n <div className={cn(\"relative\", className)}>\n {/* A real <label for> (lenkbank), and still `aria-labelledby`'s target, so the\n name — and the listbox's, which borrows it — is exactly what it was. */}\n {hasLabel && hintParts.labelHint === undefined && (\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId}>\n {label}\n </ComboboxFieldLabel>\n )}\n {/* `\"hint\" in props`: the family's 0.22 rule (see `endHintRowProps`) — passed at\n all, even as `undefined`, a hint keeps the unlabelled field's end-hint row, so\n a \"?\" coming and going never re-parents the <input> under the caret (0.24, the\n last of the family to get it; the siblings had it in 0.23). */}\n <EndHintRow {...endHintRowProps(\"hint\" in props, hasLabel, hintParts.labelHint)}>\n {withLock(\n <div ref={fieldRef} className={cn(\"relative\", locked && \"cursor-not-allowed\")}>\n {icon && (\n <span\n aria-hidden\n className={cn(\n \"pointer-events-none absolute flex text-muted\",\n small ? \"start-2 [&>svg]:size-3.5\" : \"start-2.5 [&>svg]:size-4\",\n // Labelled, the icon sits on the VALUE's line — the one line box below\n // FIELD_FLOATING_PAD's `pt-4` and the 1px border — not on the middle of\n // the box. Centred on the box it rose into the top strip and sat on the\n // label's first letters, which float at the same start inset. On the\n // value line it is beside what it describes, and the label keeps the\n // start edge every other label in the form's column shares.\n // At Large the label stands above the field and the strip is gone\n // (0.33, §10.17): the value's line is under FIELD_BASE's `py-2`.\n hasLabel\n ? \"top-[calc(1rem+1px)] h-5 items-center large:top-[calc(0.5rem+1px)]\"\n : \"top-1/2 -translate-y-1/2\",\n )}\n >\n {icon}\n </span>\n )}\n <input\n {...rest}\n ref={ref}\n id={fieldId}\n size={nativeSize}\n type=\"text\"\n value={value}\n // Locked: focusable, `readOnly`, `aria-disabled` — see `disabledReason`.\n disabled={locked ? undefined : disabled}\n readOnly={locked || rest.readOnly}\n aria-disabled={locked || rest[\"aria-disabled\"]}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={expanded}\n // Required by the role, and set while closed too, as on the siblings.\n aria-controls={listboxId}\n aria-activedescendant={activeId}\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(field.describedBy, locked && lock.reasonId)}\n autoComplete=\"off\"\n onFocus={(e) => {\n onFocus?.(e);\n // A locked field takes focus to say why, and opens nothing.\n if (locked) return;\n // The tour's focus on arrival (§11.8) looks nothing up: the list would\n // cover its card (0.50.1). A click, ArrowDown or typing opens it.\n if (isQuietFocus(e.currentTarget)) {\n quietRef.current = true;\n return;\n }\n // A seeded field (keksdose's `initialQuery`) is looked up as it is\n // focused; an empty one asks nothing, because of `minChars`.\n setOpen(true);\n }}\n // After a quiet focus, the person's click on the already-focused field\n // fires no `focus`: the click is their \"open\" instead.\n onClick={(e) => {\n rest.onClick?.(e);\n if (!quietRef.current || locked) return;\n quietRef.current = false;\n setOpen(true);\n }}\n onBlur={(e) => {\n quietRef.current = false;\n onBlur?.(e);\n close();\n }}\n onChange={(e) => {\n if (locked) return;\n onChange(e.target.value);\n setOpen(true);\n setActive(-1);\n }}\n onKeyDown={(e) => {\n // Locked: Enter is swallowed (no form submit, no caller's \"search now\");\n // every other key is the caller's and the caret's. No list to drive.\n if (locked) {\n if (e.key === \"Enter\") e.preventDefault();\n else onKeyDown?.(e);\n return;\n }\n // An Escape that closes the OPEN list is the list's, and is consumed before\n // the caller sees it. A caller whose Escape means \"close the panel\" (keksdose's\n // address search) otherwise closed the whole panel when the user only meant\n // to dismiss the suggestions — the opposite of what the docs promised.\n if (e.key === \"Escape\" && expanded && takeEscape(e)) return;\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n // Disabled rows are passed over; at either end the highlight stays put.\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) setOpen(true);\n else setActive((i) => stepEnabled(results, i, 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) setOpen(true);\n // From \"nothing highlighted\", Up goes to the bottom, as the APG has it.\n else setActive((i) => stepEnabled(results, i < 0 ? results.length : i, -1));\n } else if (e.key === \"Enter\") {\n // No row highlighted: the text is the answer, and a form's own submit\n // is left alone. Nor is a row taken by the Enter that confirms an IME\n // composition, which is the IME's (BUG-9).\n if (expanded && activeId && !isImeEnter(e)) {\n e.preventDefault();\n take(results[active]);\n }\n } else if (e.key === \"Tab\") {\n close();\n }\n }}\n className={cn(\n FIELD_BASE,\n hasLabel && FIELD_FLOATING_PAD,\n // Select's `SELECT_SM` box: a fixed 28px, so the caller's line height\n // cannot grow it.\n small && \"h-7 py-0 pe-2 ps-2 text-xs\",\n icon ? (small ? \"ps-7\" : \"ps-8\") : undefined,\n busy && (small ? \"pe-7\" : \"pe-9\"),\n field.isInvalid && FIELD_INVALID,\n inputClassName,\n )}\n />\n {busy && live && (\n // Decorative: the live region below already says \"Loading…\".\n <span\n className={cn(\n \"pointer-events-none absolute top-1/2 flex -translate-y-1/2\",\n small ? \"end-2\" : \"end-2.5\",\n )}\n >\n <Spinner label={null} className={small ? \"h-3.5 w-3.5\" : \"h-4 w-4\"} />\n </span>\n )}\n </div>,\n )}\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it, after the field so the \"?\"\n follows the control in the tab order (as on Input and Select). */}\n {hasLabel && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <ComboboxFieldLabel id={labelId} htmlFor={fieldId} className={LABEL_IN_ROW}>\n {label}\n </ComboboxFieldLabel>\n </StaticLabelRow>\n )}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n {/* The live region, kept mounted so a change IS an announcement: focus stays\n in the field, and without it a reader typed into a geocoder and heard only\n their own letters. The panel's visible line is aria-hidden in favour of\n this, which also carries the row count the eye reads off the list. */}\n <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {!live\n ? null\n : statusLine !== null\n ? statusLine\n : results.length > 0\n ? labels.resultCount(results.length)\n : null}\n </div>\n {expanded &&\n rect &&\n typeof document !== \"undefined\" &&\n createPortal(\n <div\n // Every press inside the list keeps focus in the field — rows, the status\n // line, the scrollbar — or the input's blur would close the list first.\n role=\"presentation\"\n // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).\n data-kit-layer=\"\"\n // Portalled out of the form's `dir`; the field's is put back. The list is\n // exactly the field's width, so `left` places it in either direction.\n dir={dir}\n onMouseDown={(e) => e.preventDefault()}\n // The family's suggestion box (DUP-4), as a column: the list scrolls inside\n // it and the status line stays under the rows.\n className={cn(SUGGESTION_LIST_CLASS, \"fixed z-50 flex flex-col overflow-hidden py-0\")}\n // Flipped above, the list hangs from its bottom edge: three rows under a\n // 256 px cap, placed by `top`, floated ~176 px above the field (keksdose K1),\n // and the rows come and go as the user types.\n style={{ ...(above ? { bottom } : { top }), left: rect.left, width: rect.width, maxHeight }}\n >\n {results.length > 0 && (\n <ul\n id={listboxId}\n role=\"listbox\"\n aria-label={ariaLabel}\n aria-labelledby={labelledBy}\n className=\"min-h-0 flex-1 overflow-y-auto py-1\"\n >\n {/* One heading per group, a `role=\"group\"` it names, as in the core panel\n (refactor audit 2026-10-30, INP-8), drawn by the same renderer: an\n option may appear once per group. */}\n <GroupedOptions\n runs={groupRuns(results, (o) => o.group)}\n listboxId={listboxId}\n rowKey={(o) => String(o.value)}\n headingClassName={(run) => anchoredHeadingClass(run)}\n row={(o, i) => (\n <li role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n role=\"option\"\n // CHOSEN, never \"highlighted\" — the family's rule. This\n // field holds no value, so the only row that can be chosen\n // is the one whose label the text already is.\n aria-selected={fillOnSelect && o.label === value}\n // Listed, announced as unavailable, never taken: `take`\n // refuses it and the arrows pass it over.\n aria-disabled={o.disabled || undefined}\n tabIndex={-1}\n onClick={() => take(o)}\n onMouseEnter={() => {\n if (!o.disabled) setActive(i);\n }}\n className={cn(\n // The family's suggestion row (DUP-4): it hand-rolled its\n // own with a plain `truncate`, so a long address was cut at\n // Large, where Combobox's rows wrap (text size §10.17, T1).\n // A flex row here, for the icon and the two lines.\n suggestionRowClass(i === active && !o.disabled),\n \"flex items-center gap-2\",\n small && \"py-1 text-xs\",\n o.disabled && DISABLED_ROW_CLASS,\n o.group && \"ps-6\",\n )}\n >\n {o.icon && (\n <span aria-hidden className=\"shrink-0 text-muted\">\n {o.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate-until-large text-primary\">{o.label}</span>\n {o.sublabel && (\n <span\n className={cn(\n \"block truncate-until-large text-placeholder\",\n small ? \"text-caption\" : \"text-xs\",\n )}\n >\n {o.sublabel}\n </span>\n )}\n </span>\n </button>\n </li>\n )}\n />\n </ul>\n )}\n {statusLine !== null && (\n // Visible, not live — the region above speaks for it.\n <div\n aria-hidden\n className={cn(\n small ? \"px-3 py-1.5 text-xs\" : \"px-3 py-2 text-sm\",\n failed && !hasStatus ? \"text-danger\" : \"text-muted\",\n results.length > 0 && \"border-t border-subtle text-xs\",\n )}\n >\n {statusLine}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { horizontalStep } from "../lib/direction.js";
|
|
7
|
+
import { isEditableContent } from "../lib/editable-content.js";
|
|
7
8
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
9
|
import { useAnnounce } from "../hooks/use-announce.js";
|
|
9
10
|
import { useBreakpoint } from "../hooks/use-breakpoint.js";
|
|
@@ -38,7 +39,7 @@ const ITEMS = "button:not([disabled]),a[href],input:not([disabled]),select:not([
|
|
|
38
39
|
function ownsArrows(el) {
|
|
39
40
|
if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;
|
|
40
41
|
if (el instanceof HTMLInputElement) return !["button", "checkbox", "radio", "submit", "reset"].includes(el.type);
|
|
41
|
-
return el
|
|
42
|
+
return isEditableContent(el);
|
|
42
43
|
}
|
|
43
44
|
function BulkActionBar({
|
|
44
45
|
count,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, FocusEvent, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport { OVERLAY_EXIT_MS, skipsExitAnimation } from \"../hooks/use-close-transition\";\nimport { focusFirst, focusIsLost, whenFocusLeftForNothing } from \"../lib/focus-left\";\nimport { hasContent } from \"../lib/presence\";\nimport { Button } from \"./button\";\nimport { IconButton } from \"./icon-button\";\nimport { DEFAULT_BULK_ACTION_BAR_LABELS } from \"./bulk-action-bar-labels\";\n\nexport interface BulkActionBarLabels {\n /** The count, shown in the bar, spoken by its live region and used as the toolbar's\n * name: \"3 selected\". */\n selected: (count: number) => string;\n /** The clear/cancel action's name — the X of the floating bar, the text button of the\n * others. */\n clear: string;\n /** Spoken when the selection empties and the bar goes. */\n cleared: string;\n}\n\n// Defined in ./bulk-action-bar-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_BULK_ACTION_BAR_LABELS };\n\n/**\n * `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the\n * home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the\n * desktop's: pinned to the top of its scroll container, over the rows it acts on.\n * `inline` — in the flow, where the caller places it.\n */\nexport type BulkActionBarVariant = \"floating\" | \"sticky\" | \"inline\";\n\n/**\n * A variant per breakpoint, mobile first — `{ base: \"floating\", md: \"sticky\" }` is the\n * phone's floating card below 768px and the desktop's sticky bar from there up. The\n * breakpoints are the ones the kit's classes use (sm 640, md 768, lg 1024, xl 1280px\n * at Normal text size; since 0.32 they move with the text size, as `md:` does). keksdose's payees selection bar (payees-selection-bar.tsx)\n * computes exactly this with its own `useMediaQuery(\"(min-width: 768px)\")`; every bar\n * that is a card on a phone and a strip on a desktop would repeat it.\n *\n * Resolved in JS rather than with `md:` classes because the variants differ in more\n * than classes: the floating bar's clear is an icon at the start, the others' a text\n * button at the end, and the floating bar is positioned by inline style.\n */\nexport interface ResponsiveBulkActionBarVariant {\n base: BulkActionBarVariant;\n sm?: BulkActionBarVariant;\n md?: BulkActionBarVariant;\n lg?: BulkActionBarVariant;\n xl?: BulkActionBarVariant;\n}\n\n/** Widest first: the first that matches and names a value wins. Each is the class's own\n * breakpoint at the text size in force (`useBreakpoint`), so `md` here is `md:` there. */\nconst BREAKPOINTS = [\"xl\", \"lg\", \"md\", \"sm\"] as const;\n\n/** The variant in force: the widest breakpoint that matches AND names one, else\n * `base`. The four queries are subscribed unconditionally — hooks cannot be skipped —\n * and a plain string variant simply ignores them. Without `matchMedia` (SSR, tests)\n * every query is false, so the bar renders its `base`, the phone's, first. */\nfunction useResolvedVariant(variant: BulkActionBarVariant | ResponsiveBulkActionBarVariant): BulkActionBarVariant {\n const matches = {\n xl: useBreakpoint(\"xl\"),\n lg: useBreakpoint(\"lg\"),\n md: useBreakpoint(\"md\"),\n sm: useBreakpoint(\"sm\"),\n };\n if (typeof variant === \"string\") return variant;\n for (const key of BREAKPOINTS) {\n const v = variant[key];\n if (matches[key] && v !== undefined) return v;\n }\n return variant.base;\n}\n\nexport interface BulkActionBarProps extends Omit<ComponentPropsWithRef<\"div\">, \"role\" | \"children\"> {\n /** How many items are selected. The bar shows only while this is above zero —\n * unless {@link open} says otherwise. */\n count: number;\n /**\n * Show the bar regardless of `count` — `true` for SELECTION MODE with nothing picked\n * yet, `false` to hide it with rows still selected. Left out, `count > 0` decides, as\n * before.\n *\n * keksdose's invoice lines (invoice-lines-bulk-bar.tsx) enter selection with a\n * \"Select\" button, and the bar must be there at \"0 selected\" to hold the way out and\n * a \"Select all\" (pass it as a child); gated on the count, the bar appeared only at\n * the first tick, and the user who pressed \"Select\" saw nothing happen. The exit plays\n * when it closes either way, and the count is still announced on every change.\n */\n open?: boolean;\n /** Clear the selection / leave selection mode. */\n onClear: () => void;\n /**\n * The actions: buttons (`IconButton size=\"lg\"` on a phone, `Button` from md up),\n * in the caller's order. They are the toolbar's arrow-key stops, after the clear.\n *\n * LINKS rove too (keksdose F8): a `<Button href>` — \"View report\", which navigates\n * rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place\n * among the buttons, not a second Tab stop beside the toolbar's one. A disabled\n * `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any\n * other focusable of your own joins with `data-toolbar-item`.\n */\n children?: ReactNode;\n /** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per\n * breakpoint, see {@link ResponsiveBulkActionBarVariant}. */\n variant?: BulkActionBarVariant | ResponsiveBulkActionBarVariant;\n /**\n * Content BELOW the toolbar row, inside the same surface — the same floating card or\n * sticky tinted strip — but OUTSIDE the toolbar: its fields keep their own Tab stops\n * and arrow keys, and the toolbar's roving focus never walks into it. keksdose's\n * desktop receipt-line bar (invoice-lines-bulk-bar.tsx) shows its `BulkFields` form —\n * a category picker, an exclude checkbox, Apply — under the count and the clear; a\n * form is not a toolbar's content (`role=\"toolbar\"` promises one arrow-key widget),\n * and set beside the bar it lost the sticky tint and scrolled away from it.\n *\n * On the floating variant the panel scrolls within 60% of the viewport's height, so a\n * long form on a phone cannot push the bar off the top of the screen.\n */\n panel?: ReactNode;\n /** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */\n labels?: LabelOverride<BulkActionBarLabels>;\n /**\n * Where the focus goes when the bar leaves while it holds it — after Clear, or an action\n * that empties the selection (0.52, refactor audit 2026-10-30, DSP-15). Without it, the\n * control the focus came into the bar from (the row's box, say), if that is still on the\n * page; with it, this first. Pass one where the selection takes that control with it — a\n * bulk delete removes the rows whose boxes were ticked: the table, the list's heading,\n * the \"Select\" button that entered selection mode. A bar that leaves without the focus\n * in it moves nothing.\n */\n returnFocusRef?: RefObject<HTMLElement | null>;\n}\n\n/** Where the floating bar sits: clear of the phone nav and of the home indicator,\n * whichever reaches higher — `FloatingPanel`'s rule, plus the bar's own 0.5rem gap.\n * Set as inline style, so reposition it through `style` (which is merged over this,\n * e.g. `style={{ insetInline: \"auto 2rem\", width: \"24rem\" }}`), not `className`. */\nconst FLOATING_STYLE: CSSProperties = {\n bottom: \"calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)\",\n // Both insets on both sides: a logical side has no one physical inset to name, and\n // the bar is symmetric, so RTL needs nothing more.\n insetInline: \"max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\",\n};\n\n/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by\n * `tabindex`, because the roving below sets every one but the current to `-1` — a\n * custom control joins with `data-toolbar-item`.\n *\n * `a[href]` is here on purpose (keksdose F8, a \"View report\" `Button href` in the bar):\n * a link in a toolbar is a toolbar item like any other, and left out it would keep its\n * own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`\n * WITHOUT one, and is skipped as a disabled button is. */\nconst ITEMS =\n \"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]\";\n\n/** Keys a text field needs for itself — the toolbar must not take its caret away. */\nfunction ownsArrows(el: Element): boolean {\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n if (el instanceof HTMLInputElement) return ![\"button\", \"checkbox\", \"radio\", \"submit\", \"reset\"].includes(el.type);\n return (el as HTMLElement).isContentEditable;\n}\n\n/**\n * The bar that appears while rows are selected: the count, a way out, and what can be\n * done to all of them at once.\n *\n * keksdose had written it three times — the transactions phone bar\n * (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars\n * (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had\n * drifted where it matters: two parked themselves with a guessed `bottom-16` that the\n * third had already found leaves a 24px gap (live #314: the nav is content-sized and\n * `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,\n * and all three were `role=\"toolbar\"` without the arrow keys that role promises.\n *\n * ## Keep it mounted\n *\n * Render it unconditionally and let `count` decide. At zero it renders only its live\n * region — which must already be in the DOM for the first \"3 selected\" to be heard —\n * and when the count drops to zero it plays its exit before it goes, which a\n * `{count > 0 && <Bar/>}` gate would cut off.\n *\n * ## The toolbar\n *\n * One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab\n * leaves — the WAI-ARIA toolbar pattern, since that is what `role=\"toolbar\"` tells a\n * screen reader to expect. The stop remembers the control last used. Arrow keys inside\n * a text field stay the field's.\n *\n * It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,\n * because tapping more rows is how a selection grows.\n */\nexport function BulkActionBar({\n count,\n onClear,\n children,\n variant: variantProp = \"floating\",\n open,\n panel,\n labels: labelsProp,\n returnFocusRef,\n className,\n style,\n onKeyDown,\n onFocus,\n onBlur,\n ...rest\n}: BulkActionBarProps) {\n const labels = useKitLabels(\"bulkActionBar\", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const barRef = useRef<HTMLDivElement>(null);\n const variant = useResolvedVariant(variantProp);\n const shown = open ?? count > 0;\n\n // The count the bar last SHOWED, so a bar lowering itself after \"clear\" still reads\n // \"3 selected\" on its way out rather than \"0 selected\". Adjusted during render\n // (`Collapse`'s pattern), as is the exit below.\n const [shownCount, setShownCount] = useState(count);\n const [leaving, setLeaving] = useState(false);\n const [prev, setPrev] = useState({ count, shown });\n if (count !== prev.count || shown !== prev.shown) {\n setPrev({ count, shown });\n if (shown) {\n setShownCount(count);\n setLeaving(false);\n } else if (prev.shown) {\n setLeaving(!skipsExitAnimation());\n }\n }\n useEffect(() => {\n if (!leaving) return;\n const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [leaving]);\n\n // Spoken on every CHANGE, not on mount: a bar mounted with rows already selected\n // was not the result of anything the user just did.\n const spoken = useRef(count);\n useEffect(() => {\n if (spoken.current === count) return;\n spoken.current = count;\n // In selection mode the bar stays at zero, so zero is a count like any other.\n announce(count > 0 || shown ? labels.selected(count) : labels.cleared);\n }, [count, shown, labels, announce]);\n\n // Roving tab stop. The controls are the caller's, so the stop is kept on the DOM\n // rather than through props: after every render the remembered control (or the\n // first) is the one with `tabindex=0`, every other one `-1`.\n const active = useRef<HTMLElement | null>(null);\n // Every render: the caller's controls come and go (a \"remove added\" that exists\n // only while some are removable), and a stop on a control that left must move on.\n useLayoutEffect(() => {\n rove(barRef.current, active);\n });\n\n // Leaving with the focus in it (refactor audit 2026-10-30, DSP-15): the bar goes `inert`,\n // which blurs the control it holds, then unmounts — and the focus fell to <body>; on a\n // phone VoiceOver's cursor jumped to the top of the page (keksdose's register). Noted on\n // the bar's outermost element, so the `panel`'s fields count: where the focus came in\n // from (the first entry since the bar showed, replaced once that control is gone), and\n // whether it is in the bar. Focus that leaves for nothing because the page took it —\n // inert, a removal — still counts as in it; a real element elsewhere takes it out, and\n // so does a click on the page that left it nowhere (0.52 code review,\n // bulk-action-bar.tsx: the note outlived it, and the bar's hide long after — the last row\n // unticked with the pointer — pulled the focus and the scroll back to the row's box).\n const cameFrom = useRef<HTMLElement | null>(null);\n const holdsFocus = useRef(false);\n const trackFocus = {\n onFocus: (e: FocusEvent<HTMLDivElement>) => {\n holdsFocus.current = true;\n const from = e.relatedTarget;\n if (from instanceof HTMLElement && !e.currentTarget.contains(from) && !cameFrom.current?.isConnected)\n cameFrom.current = from;\n },\n onBlur: (e: FocusEvent<HTMLDivElement>) => {\n onBlur?.(e);\n const to = e.relatedTarget;\n if (to instanceof Element && !e.currentTarget.contains(to)) holdsFocus.current = false;\n whenFocusLeftForNothing(e, () => {\n holdsFocus.current = false;\n });\n },\n };\n // The commit that hides the bar: the focus goes back while the control holding it is\n // still in place — before `inert` takes it, or, with no exit to play, before <body> does.\n const wasShown = useRef(shown);\n useLayoutEffect(() => {\n if (wasShown.current === shown) return;\n wasShown.current = shown;\n const from = cameFrom.current;\n const held = holdsFocus.current;\n cameFrom.current = null;\n holdsFocus.current = false;\n if (shown) return;\n // The outermost element: the toolbar, or with a `panel` the surface round both.\n const surface = barRef.current?.closest(\"[data-bulk-action-bar]\");\n const inBar = !!surface && surface.contains(document.activeElement);\n if (inBar || (held && focusIsLost())) focusFirst([returnFocusRef?.current, from]);\n }, [shown, returnFocusRef]);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented || ownsArrows(e.target as Element)) return;\n const list = itemsOf(barRef.current);\n const at = list.indexOf(document.activeElement as HTMLElement);\n if (at === -1 || list.length === 0) return;\n const step = horizontalStep(e.key, barRef.current);\n let next = -1;\n if (step !== 0) next = (at + step + list.length) % list.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = list.length - 1;\n if (next === -1) return;\n e.preventDefault();\n active.current = list[next];\n rove(barRef.current, active);\n list[next].focus();\n };\n\n if (!shown && !leaving) {\n return <span {...regionProps} />;\n }\n\n const floating = variant === \"floating\";\n const clear = floating ? (\n <IconButton size=\"lg\" onClick={onClear} aria-label={labels.clear}>\n <X />\n </IconButton>\n ) : (\n // `ms-auto`: when the actions wrap, the way out still sits at the bar's end.\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onClear} className=\"ms-auto\">\n {labels.clear}\n </Button>\n );\n\n // `hasContent` (refactor audit 2026-10-30, DUP-14): an empty-string panel drew an empty\n // bordered strip under the bar.\n const hasPanel = hasContent(panel);\n // The surface — where the bar sits and what it looks like — and the toolbar row's\n // own layout. Without a panel they are one element, exactly as before 0.11.0; with\n // one the surface wraps the row and the panel, so both share its tint and position.\n const surfaceClass = floating\n ? cn(\n \"fixed z-30 rounded-2xl border border-subtle bg-surface shadow-lg\",\n shown ? \"animate-sheet\" : \"animate-sheet-out\",\n )\n : cn(\n variant === \"sticky\" &&\n \"sticky top-0 z-20 border-b border-subtle bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur\",\n shown ? \"animate-overlay\" : \"animate-overlay-out\",\n );\n // `flex-wrap`: on a phone the count, three actions and a text \"Clear selection\" are\n // wider than the screen. Without wrapping the count (the one shrinkable item)\n // collapsed to a sliver and the rest ran out of the bar; wrapped, the actions that do\n // not fit move to a second line and every label stays whole.\n const rowClass = cn(\"flex flex-wrap items-center\", floating ? \"gap-1 px-2 py-1\" : \"gap-2 px-3 py-2\");\n const surfaceStyle = floating ? { ...FLOATING_STYLE, ...style } : style;\n\n const toolbar = (\n <div\n {...(hasPanel ? null : rest)}\n ref={barRef}\n role=\"toolbar\"\n aria-label={labels.selected(shownCount)}\n aria-orientation=\"horizontal\"\n data-bulk-action-bar={hasPanel ? undefined : variant}\n // Leaving: out of the tab order and the accessibility tree at once, while the\n // bar still animates. The selection it acted on is already gone.\n inert={(!hasPanel && !shown) || undefined}\n onKeyDown={handleKeyDown}\n onFocus={(e) => {\n onFocus?.(e);\n if (!hasPanel) trackFocus.onFocus(e);\n // Clicking a control is using it: the stop follows the pointer too.\n const hit = itemsOf(barRef.current).find((el) => el === e.target);\n if (hit) {\n active.current = hit;\n rove(barRef.current, active);\n }\n }}\n onBlur={hasPanel ? undefined : trackFocus.onBlur}\n style={hasPanel ? undefined : surfaceStyle}\n className={hasPanel ? rowClass : cn(rowClass, surfaceClass, className)}\n >\n {floating && clear}\n {/* `flex-auto`, not `flex-1`: a zero basis told the wrapping that the count needs\n no room, so it was the item squeezed to nothing instead of a line breaking.\n Its own width is its basis now; `truncate` is left for a label too long for a\n line on its own. */}\n <span className=\"min-w-0 flex-auto truncate text-sm font-medium text-primary\">{labels.selected(shownCount)}</span>\n {/* `contents`, so the caller's controls are flex items of the bar itself. */}\n <div className=\"contents\">{children}</div>\n {!floating && clear}\n </div>\n );\n\n return (\n <>\n <span {...regionProps} />\n {hasPanel ? (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the focus is\n only noted here (DSP-15): where it came from, whether it is in the bar; the\n controls are the toolbar's and the panel's own. */\n <div\n {...rest}\n onFocus={trackFocus.onFocus}\n onBlur={trackFocus.onBlur}\n data-bulk-action-bar={variant}\n inert={!shown || undefined}\n style={surfaceStyle}\n className={cn(\"flex flex-col\", surfaceClass, className)}\n >\n {toolbar}\n <div\n data-bulk-action-bar-panel=\"\"\n className={cn(\"border-t border-subtle px-3 py-3\", floating && \"max-h-[60dvh] overflow-y-auto\")}\n >\n {panel}\n </div>\n </div>\n ) : (\n toolbar\n )}\n </>\n );\n}\n\nfunction itemsOf(bar: HTMLElement | null): HTMLElement[] {\n return bar ? Array.from(bar.querySelectorAll<HTMLElement>(ITEMS)) : [];\n}\n\n/** Put the one Tab stop on the remembered control — or the first, when that one has\n * gone or was never set — and take it off every other. */\nfunction rove(bar: HTMLElement | null, active: { current: HTMLElement | null }): void {\n const list = itemsOf(bar);\n if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;\n for (const el of list) el.tabIndex = el === active.current ? 0 : -1;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, FocusEvent, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { isEditableContent } from \"../lib/editable-content\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport { OVERLAY_EXIT_MS, skipsExitAnimation } from \"../hooks/use-close-transition\";\nimport { focusFirst, focusIsLost, whenFocusLeftForNothing } from \"../lib/focus-left\";\nimport { hasContent } from \"../lib/presence\";\nimport { Button } from \"./button\";\nimport { IconButton } from \"./icon-button\";\nimport { DEFAULT_BULK_ACTION_BAR_LABELS } from \"./bulk-action-bar-labels\";\n\nexport interface BulkActionBarLabels {\n /** The count, shown in the bar, spoken by its live region and used as the toolbar's\n * name: \"3 selected\". */\n selected: (count: number) => string;\n /** The clear/cancel action's name — the X of the floating bar, the text button of the\n * others. */\n clear: string;\n /** Spoken when the selection empties and the bar goes. */\n cleared: string;\n}\n\n// Defined in ./bulk-action-bar-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_BULK_ACTION_BAR_LABELS };\n\n/**\n * `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the\n * home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the\n * desktop's: pinned to the top of its scroll container, over the rows it acts on.\n * `inline` — in the flow, where the caller places it.\n */\nexport type BulkActionBarVariant = \"floating\" | \"sticky\" | \"inline\";\n\n/**\n * A variant per breakpoint, mobile first — `{ base: \"floating\", md: \"sticky\" }` is the\n * phone's floating card below 768px and the desktop's sticky bar from there up. The\n * breakpoints are the ones the kit's classes use (sm 640, md 768, lg 1024, xl 1280px\n * at Normal text size; since 0.32 they move with the text size, as `md:` does). keksdose's payees selection bar (payees-selection-bar.tsx)\n * computes exactly this with its own `useMediaQuery(\"(min-width: 768px)\")`; every bar\n * that is a card on a phone and a strip on a desktop would repeat it.\n *\n * Resolved in JS rather than with `md:` classes because the variants differ in more\n * than classes: the floating bar's clear is an icon at the start, the others' a text\n * button at the end, and the floating bar is positioned by inline style.\n */\nexport interface ResponsiveBulkActionBarVariant {\n base: BulkActionBarVariant;\n sm?: BulkActionBarVariant;\n md?: BulkActionBarVariant;\n lg?: BulkActionBarVariant;\n xl?: BulkActionBarVariant;\n}\n\n/** Widest first: the first that matches and names a value wins. Each is the class's own\n * breakpoint at the text size in force (`useBreakpoint`), so `md` here is `md:` there. */\nconst BREAKPOINTS = [\"xl\", \"lg\", \"md\", \"sm\"] as const;\n\n/** The variant in force: the widest breakpoint that matches AND names one, else\n * `base`. The four queries are subscribed unconditionally — hooks cannot be skipped —\n * and a plain string variant simply ignores them. Without `matchMedia` (SSR, tests)\n * every query is false, so the bar renders its `base`, the phone's, first. */\nfunction useResolvedVariant(variant: BulkActionBarVariant | ResponsiveBulkActionBarVariant): BulkActionBarVariant {\n const matches = {\n xl: useBreakpoint(\"xl\"),\n lg: useBreakpoint(\"lg\"),\n md: useBreakpoint(\"md\"),\n sm: useBreakpoint(\"sm\"),\n };\n if (typeof variant === \"string\") return variant;\n for (const key of BREAKPOINTS) {\n const v = variant[key];\n if (matches[key] && v !== undefined) return v;\n }\n return variant.base;\n}\n\nexport interface BulkActionBarProps extends Omit<ComponentPropsWithRef<\"div\">, \"role\" | \"children\"> {\n /** How many items are selected. The bar shows only while this is above zero —\n * unless {@link open} says otherwise. */\n count: number;\n /**\n * Show the bar regardless of `count` — `true` for SELECTION MODE with nothing picked\n * yet, `false` to hide it with rows still selected. Left out, `count > 0` decides, as\n * before.\n *\n * keksdose's invoice lines (invoice-lines-bulk-bar.tsx) enter selection with a\n * \"Select\" button, and the bar must be there at \"0 selected\" to hold the way out and\n * a \"Select all\" (pass it as a child); gated on the count, the bar appeared only at\n * the first tick, and the user who pressed \"Select\" saw nothing happen. The exit plays\n * when it closes either way, and the count is still announced on every change.\n */\n open?: boolean;\n /** Clear the selection / leave selection mode. */\n onClear: () => void;\n /**\n * The actions: buttons (`IconButton size=\"lg\"` on a phone, `Button` from md up),\n * in the caller's order. They are the toolbar's arrow-key stops, after the clear.\n *\n * LINKS rove too (keksdose F8): a `<Button href>` — \"View report\", which navigates\n * rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place\n * among the buttons, not a second Tab stop beside the toolbar's one. A disabled\n * `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any\n * other focusable of your own joins with `data-toolbar-item`.\n */\n children?: ReactNode;\n /** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per\n * breakpoint, see {@link ResponsiveBulkActionBarVariant}. */\n variant?: BulkActionBarVariant | ResponsiveBulkActionBarVariant;\n /**\n * Content BELOW the toolbar row, inside the same surface — the same floating card or\n * sticky tinted strip — but OUTSIDE the toolbar: its fields keep their own Tab stops\n * and arrow keys, and the toolbar's roving focus never walks into it. keksdose's\n * desktop receipt-line bar (invoice-lines-bulk-bar.tsx) shows its `BulkFields` form —\n * a category picker, an exclude checkbox, Apply — under the count and the clear; a\n * form is not a toolbar's content (`role=\"toolbar\"` promises one arrow-key widget),\n * and set beside the bar it lost the sticky tint and scrolled away from it.\n *\n * On the floating variant the panel scrolls within 60% of the viewport's height, so a\n * long form on a phone cannot push the bar off the top of the screen.\n */\n panel?: ReactNode;\n /** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */\n labels?: LabelOverride<BulkActionBarLabels>;\n /**\n * Where the focus goes when the bar leaves while it holds it — after Clear, or an action\n * that empties the selection (0.52, refactor audit 2026-10-30, DSP-15). Without it, the\n * control the focus came into the bar from (the row's box, say), if that is still on the\n * page; with it, this first. Pass one where the selection takes that control with it — a\n * bulk delete removes the rows whose boxes were ticked: the table, the list's heading,\n * the \"Select\" button that entered selection mode. A bar that leaves without the focus\n * in it moves nothing.\n */\n returnFocusRef?: RefObject<HTMLElement | null>;\n}\n\n/** Where the floating bar sits: clear of the phone nav and of the home indicator,\n * whichever reaches higher — `FloatingPanel`'s rule, plus the bar's own 0.5rem gap.\n * Set as inline style, so reposition it through `style` (which is merged over this,\n * e.g. `style={{ insetInline: \"auto 2rem\", width: \"24rem\" }}`), not `className`. */\nconst FLOATING_STYLE: CSSProperties = {\n bottom: \"calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)\",\n // Both insets on both sides: a logical side has no one physical inset to name, and\n // the bar is symmetric, so RTL needs nothing more.\n insetInline: \"max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\",\n};\n\n/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by\n * `tabindex`, because the roving below sets every one but the current to `-1` — a\n * custom control joins with `data-toolbar-item`.\n *\n * `a[href]` is here on purpose (keksdose F8, a \"View report\" `Button href` in the bar):\n * a link in a toolbar is a toolbar item like any other, and left out it would keep its\n * own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`\n * WITHOUT one, and is skipped as a disabled button is. */\nconst ITEMS =\n \"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]\";\n\n/** Keys a text field needs for itself — the toolbar must not take its caret away.\n * Editable content by the kit's one rule (lib/editable-content, 0.54 follow-up):\n * `isContentEditable` alone is missing in jsdom and in any engine without it, and there\n * the arrows took the focus out of an editor in the bar. */\nfunction ownsArrows(el: Element): boolean {\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n if (el instanceof HTMLInputElement) return ![\"button\", \"checkbox\", \"radio\", \"submit\", \"reset\"].includes(el.type);\n return isEditableContent(el);\n}\n\n/**\n * The bar that appears while rows are selected: the count, a way out, and what can be\n * done to all of them at once.\n *\n * keksdose had written it three times — the transactions phone bar\n * (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars\n * (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had\n * drifted where it matters: two parked themselves with a guessed `bottom-16` that the\n * third had already found leaves a 24px gap (live #314: the nav is content-sized and\n * `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,\n * and all three were `role=\"toolbar\"` without the arrow keys that role promises.\n *\n * ## Keep it mounted\n *\n * Render it unconditionally and let `count` decide. At zero it renders only its live\n * region — which must already be in the DOM for the first \"3 selected\" to be heard —\n * and when the count drops to zero it plays its exit before it goes, which a\n * `{count > 0 && <Bar/>}` gate would cut off.\n *\n * ## The toolbar\n *\n * One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab\n * leaves — the WAI-ARIA toolbar pattern, since that is what `role=\"toolbar\"` tells a\n * screen reader to expect. The stop remembers the control last used. Arrow keys inside\n * a text field stay the field's.\n *\n * It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,\n * because tapping more rows is how a selection grows.\n */\nexport function BulkActionBar({\n count,\n onClear,\n children,\n variant: variantProp = \"floating\",\n open,\n panel,\n labels: labelsProp,\n returnFocusRef,\n className,\n style,\n onKeyDown,\n onFocus,\n onBlur,\n ...rest\n}: BulkActionBarProps) {\n const labels = useKitLabels(\"bulkActionBar\", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const barRef = useRef<HTMLDivElement>(null);\n const variant = useResolvedVariant(variantProp);\n const shown = open ?? count > 0;\n\n // The count the bar last SHOWED, so a bar lowering itself after \"clear\" still reads\n // \"3 selected\" on its way out rather than \"0 selected\". Adjusted during render\n // (`Collapse`'s pattern), as is the exit below.\n const [shownCount, setShownCount] = useState(count);\n const [leaving, setLeaving] = useState(false);\n const [prev, setPrev] = useState({ count, shown });\n if (count !== prev.count || shown !== prev.shown) {\n setPrev({ count, shown });\n if (shown) {\n setShownCount(count);\n setLeaving(false);\n } else if (prev.shown) {\n setLeaving(!skipsExitAnimation());\n }\n }\n useEffect(() => {\n if (!leaving) return;\n const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [leaving]);\n\n // Spoken on every CHANGE, not on mount: a bar mounted with rows already selected\n // was not the result of anything the user just did.\n const spoken = useRef(count);\n useEffect(() => {\n if (spoken.current === count) return;\n spoken.current = count;\n // In selection mode the bar stays at zero, so zero is a count like any other.\n announce(count > 0 || shown ? labels.selected(count) : labels.cleared);\n }, [count, shown, labels, announce]);\n\n // Roving tab stop. The controls are the caller's, so the stop is kept on the DOM\n // rather than through props: after every render the remembered control (or the\n // first) is the one with `tabindex=0`, every other one `-1`.\n const active = useRef<HTMLElement | null>(null);\n // Every render: the caller's controls come and go (a \"remove added\" that exists\n // only while some are removable), and a stop on a control that left must move on.\n useLayoutEffect(() => {\n rove(barRef.current, active);\n });\n\n // Leaving with the focus in it (refactor audit 2026-10-30, DSP-15): the bar goes `inert`,\n // which blurs the control it holds, then unmounts — and the focus fell to <body>; on a\n // phone VoiceOver's cursor jumped to the top of the page (keksdose's register). Noted on\n // the bar's outermost element, so the `panel`'s fields count: where the focus came in\n // from (the first entry since the bar showed, replaced once that control is gone), and\n // whether it is in the bar. Focus that leaves for nothing because the page took it —\n // inert, a removal — still counts as in it; a real element elsewhere takes it out, and\n // so does a click on the page that left it nowhere (0.52 code review,\n // bulk-action-bar.tsx: the note outlived it, and the bar's hide long after — the last row\n // unticked with the pointer — pulled the focus and the scroll back to the row's box).\n const cameFrom = useRef<HTMLElement | null>(null);\n const holdsFocus = useRef(false);\n const trackFocus = {\n onFocus: (e: FocusEvent<HTMLDivElement>) => {\n holdsFocus.current = true;\n const from = e.relatedTarget;\n if (from instanceof HTMLElement && !e.currentTarget.contains(from) && !cameFrom.current?.isConnected)\n cameFrom.current = from;\n },\n onBlur: (e: FocusEvent<HTMLDivElement>) => {\n onBlur?.(e);\n const to = e.relatedTarget;\n if (to instanceof Element && !e.currentTarget.contains(to)) holdsFocus.current = false;\n whenFocusLeftForNothing(e, () => {\n holdsFocus.current = false;\n });\n },\n };\n // The commit that hides the bar: the focus goes back while the control holding it is\n // still in place — before `inert` takes it, or, with no exit to play, before <body> does.\n const wasShown = useRef(shown);\n useLayoutEffect(() => {\n if (wasShown.current === shown) return;\n wasShown.current = shown;\n const from = cameFrom.current;\n const held = holdsFocus.current;\n cameFrom.current = null;\n holdsFocus.current = false;\n if (shown) return;\n // The outermost element: the toolbar, or with a `panel` the surface round both.\n const surface = barRef.current?.closest(\"[data-bulk-action-bar]\");\n const inBar = !!surface && surface.contains(document.activeElement);\n if (inBar || (held && focusIsLost())) focusFirst([returnFocusRef?.current, from]);\n }, [shown, returnFocusRef]);\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented || ownsArrows(e.target as Element)) return;\n const list = itemsOf(barRef.current);\n const at = list.indexOf(document.activeElement as HTMLElement);\n if (at === -1 || list.length === 0) return;\n const step = horizontalStep(e.key, barRef.current);\n let next = -1;\n if (step !== 0) next = (at + step + list.length) % list.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = list.length - 1;\n if (next === -1) return;\n e.preventDefault();\n active.current = list[next];\n rove(barRef.current, active);\n list[next].focus();\n };\n\n if (!shown && !leaving) {\n return <span {...regionProps} />;\n }\n\n const floating = variant === \"floating\";\n const clear = floating ? (\n <IconButton size=\"lg\" onClick={onClear} aria-label={labels.clear}>\n <X />\n </IconButton>\n ) : (\n // `ms-auto`: when the actions wrap, the way out still sits at the bar's end.\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onClear} className=\"ms-auto\">\n {labels.clear}\n </Button>\n );\n\n // `hasContent` (refactor audit 2026-10-30, DUP-14): an empty-string panel drew an empty\n // bordered strip under the bar.\n const hasPanel = hasContent(panel);\n // The surface — where the bar sits and what it looks like — and the toolbar row's\n // own layout. Without a panel they are one element, exactly as before 0.11.0; with\n // one the surface wraps the row and the panel, so both share its tint and position.\n const surfaceClass = floating\n ? cn(\n \"fixed z-30 rounded-2xl border border-subtle bg-surface shadow-lg\",\n shown ? \"animate-sheet\" : \"animate-sheet-out\",\n )\n : cn(\n variant === \"sticky\" &&\n \"sticky top-0 z-20 border-b border-subtle bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur\",\n shown ? \"animate-overlay\" : \"animate-overlay-out\",\n );\n // `flex-wrap`: on a phone the count, three actions and a text \"Clear selection\" are\n // wider than the screen. Without wrapping the count (the one shrinkable item)\n // collapsed to a sliver and the rest ran out of the bar; wrapped, the actions that do\n // not fit move to a second line and every label stays whole.\n const rowClass = cn(\"flex flex-wrap items-center\", floating ? \"gap-1 px-2 py-1\" : \"gap-2 px-3 py-2\");\n const surfaceStyle = floating ? { ...FLOATING_STYLE, ...style } : style;\n\n const toolbar = (\n <div\n {...(hasPanel ? null : rest)}\n ref={barRef}\n role=\"toolbar\"\n aria-label={labels.selected(shownCount)}\n aria-orientation=\"horizontal\"\n data-bulk-action-bar={hasPanel ? undefined : variant}\n // Leaving: out of the tab order and the accessibility tree at once, while the\n // bar still animates. The selection it acted on is already gone.\n inert={(!hasPanel && !shown) || undefined}\n onKeyDown={handleKeyDown}\n onFocus={(e) => {\n onFocus?.(e);\n if (!hasPanel) trackFocus.onFocus(e);\n // Clicking a control is using it: the stop follows the pointer too.\n const hit = itemsOf(barRef.current).find((el) => el === e.target);\n if (hit) {\n active.current = hit;\n rove(barRef.current, active);\n }\n }}\n onBlur={hasPanel ? undefined : trackFocus.onBlur}\n style={hasPanel ? undefined : surfaceStyle}\n className={hasPanel ? rowClass : cn(rowClass, surfaceClass, className)}\n >\n {floating && clear}\n {/* `flex-auto`, not `flex-1`: a zero basis told the wrapping that the count needs\n no room, so it was the item squeezed to nothing instead of a line breaking.\n Its own width is its basis now; `truncate` is left for a label too long for a\n line on its own. */}\n <span className=\"min-w-0 flex-auto truncate text-sm font-medium text-primary\">{labels.selected(shownCount)}</span>\n {/* `contents`, so the caller's controls are flex items of the bar itself. */}\n <div className=\"contents\">{children}</div>\n {!floating && clear}\n </div>\n );\n\n return (\n <>\n <span {...regionProps} />\n {hasPanel ? (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the focus is\n only noted here (DSP-15): where it came from, whether it is in the bar; the\n controls are the toolbar's and the panel's own. */\n <div\n {...rest}\n onFocus={trackFocus.onFocus}\n onBlur={trackFocus.onBlur}\n data-bulk-action-bar={variant}\n inert={!shown || undefined}\n style={surfaceStyle}\n className={cn(\"flex flex-col\", surfaceClass, className)}\n >\n {toolbar}\n <div\n data-bulk-action-bar-panel=\"\"\n className={cn(\"border-t border-subtle px-3 py-3\", floating && \"max-h-[60dvh] overflow-y-auto\")}\n >\n {panel}\n </div>\n </div>\n ) : (\n toolbar\n )}\n </>\n );\n}\n\nfunction itemsOf(bar: HTMLElement | null): HTMLElement[] {\n return bar ? Array.from(bar.querySelectorAll<HTMLElement>(ITEMS)) : [];\n}\n\n/** Put the one Tab stop on the remembered control — or the first, when that one has\n * gone or was never set — and take it off every other. */\nfunction rove(bar: HTMLElement | null, active: { current: HTMLElement | null }): void {\n const list = itemsOf(bar);\n if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;\n for (const el of list) el.tabIndex = el === active.current ? 0 : -1;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -59,12 +59,43 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
59
59
|
* bubble exists only while hovered or focused once it is portalled, and a description
|
|
60
60
|
* that comes and goes is read inconsistently. The bubble is kept visual, the way
|
|
61
61
|
* DangerConfirm keeps its own.
|
|
62
|
+
*
|
|
63
|
+
* `refused={reason}` (0.55) is the same refusal by the button's own name for it, with
|
|
64
|
+
* a form for a reason said elsewhere — see `refused`.
|
|
62
65
|
*/
|
|
63
66
|
disabledReason?: ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Refuse the action and stay where the user can learn why — no permission, a
|
|
69
|
+
* read-only plan, a held lock, an end already reached (0.52 code review 2026-10-31,
|
|
70
|
+
* held to 0.55). Shown with `disabled`, a refused action leaves the tab order and says
|
|
71
|
+
* nothing; an app's way round that was a Tooltip over the disabled button, which a
|
|
72
|
+
* keyboard never opens (Kurvenschmiede's owner-only delete,
|
|
73
|
+
* shared/lib/table-columns.tsx), and the kit's own arrows at an end hand-wrote
|
|
74
|
+
* `aria-disabled` and a guard in their `onClick`.
|
|
75
|
+
*
|
|
76
|
+
* Refused, the button is `aria-disabled="true"` — never the native `disabled`, which
|
|
77
|
+
* it wins over — and stays focusable and hoverable, with the disabled look; every
|
|
78
|
+
* activation is swallowed: the click, the Enter and Space that stand for it, and the
|
|
79
|
+
* submission a submit button would make.
|
|
80
|
+
*
|
|
81
|
+
* - **Words** (`refused={t("sharing.owner_only")}`): the reason, said the
|
|
82
|
+
* `disabledReason` way — the kit {@link Tooltip}, or a line at Large and on touch
|
|
83
|
+
* (`disabledReasonDisplay`) — and attached with `aria-describedby`, so a screen
|
|
84
|
+
* reader hears it on focus. One path with `disabledReason`; these words win over its.
|
|
85
|
+
* - **`true`**: refused with no words of its own, for a reason the screen already
|
|
86
|
+
* says — a banner over the form, a line under it — which the button's own
|
|
87
|
+
* `aria-describedby` names ({@link DisabledReasonLine} draws the line with its id),
|
|
88
|
+
* or one that needs no saying (› on the last picture). No bubble.
|
|
89
|
+
* - `false`, `null`, `undefined` or `""`: not refused, as `cond && reason` gives.
|
|
90
|
+
*
|
|
91
|
+
* Under a locked {@link WriteLockProvider} a `commit` button says the lock's reason
|
|
92
|
+
* instead, as it does over `disabledReason`.
|
|
93
|
+
*/
|
|
94
|
+
refused?: ReactNode | boolean;
|
|
64
95
|
/**
|
|
65
96
|
* Where `disabledReason` shows (0.32, §4 "No fact only in a tooltip"): by default a
|
|
66
97
|
* line under the button at Large and on a touch screen, the tooltip otherwise. See
|
|
67
|
-
* {@link DisabledReasonDisplay}.
|
|
98
|
+
* {@link DisabledReasonDisplay}. A worded `refused` shows the same way.
|
|
68
99
|
*/
|
|
69
100
|
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
70
101
|
/**
|
|
@@ -149,6 +180,9 @@ export interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorEle
|
|
|
149
180
|
pending?: never;
|
|
150
181
|
pressed?: never;
|
|
151
182
|
disabledReason?: never;
|
|
183
|
+
/** A link that explains why it cannot be followed is a refused BUTTON — see
|
|
184
|
+
* {@link ButtonProps.refused}. */
|
|
185
|
+
refused?: never;
|
|
152
186
|
disabledReasonDisplay?: never;
|
|
153
187
|
commit?: never;
|
|
154
188
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from "react";
|
|
4
3
|
import { cn } from "../lib/cn.js";
|
|
5
4
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels.js";
|
|
6
5
|
import { pickLinkRenderer, replacingClick, routerLinkNavigation } from "./text-link.js";
|
|
7
|
-
import {
|
|
8
|
-
import { hasContent } from "../lib/presence.js";
|
|
9
|
-
import { LockedReason } from "./field-parts.js";
|
|
6
|
+
import { LockedReason, useRefusal } from "./field-parts.js";
|
|
10
7
|
import { Spinner } from "./spinner.js";
|
|
11
8
|
import { BUTTON_LINK_PRESSED, ICON_BUTTON_PRESSED, RenderedButtonLink, buttonLook } from "./button-look.js";
|
|
12
9
|
function Button(props) {
|
|
@@ -91,6 +88,7 @@ function ButtonElement({
|
|
|
91
88
|
stretch,
|
|
92
89
|
tone = "default",
|
|
93
90
|
pressed,
|
|
91
|
+
refused: refusedProp,
|
|
94
92
|
disabledReason: ownDisabledReason,
|
|
95
93
|
disabledReasonDisplay,
|
|
96
94
|
commit,
|
|
@@ -105,10 +103,8 @@ function ButtonElement({
|
|
|
105
103
|
reloadDocument: _reloadDocument,
|
|
106
104
|
...rest
|
|
107
105
|
}) {
|
|
108
|
-
const reasonId =
|
|
109
|
-
const
|
|
110
|
-
const locked = hasContent(disabledReason);
|
|
111
|
-
const inert = locked || Boolean(pending);
|
|
106
|
+
const { reason: disabledReason, locked, reasonId, refused } = useRefusal(refusedProp, ownDisabledReason, commit);
|
|
107
|
+
const inert = refused || Boolean(pending);
|
|
112
108
|
const ownDescribedBy = rest["aria-describedby"];
|
|
113
109
|
const button = /* @__PURE__ */ jsx(
|
|
114
110
|
"button",
|
|
@@ -130,7 +126,7 @@ function ButtonElement({
|
|
|
130
126
|
buttonLook(variant, size, stretch, tone),
|
|
131
127
|
pressed && (variant === "link" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),
|
|
132
128
|
// The disabled look for the focusable kind of disabled.
|
|
133
|
-
|
|
129
|
+
refused && "cursor-not-allowed opacity-50",
|
|
134
130
|
// No dimming while pending: the spinner IS the state, and at half opacity it
|
|
135
131
|
// would be the faintest thing on the button. `relative` anchors it.
|
|
136
132
|
pending && "relative cursor-progress",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/button.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport { useCommitReason } from \"./write-lock\";\nimport type { CommitScope } from \"./write-lock\";\nimport { hasContent } from \"../lib/presence\";\nimport { LockedReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport { BUTTON_LINK_PRESSED, ICON_BUTTON_PRESSED, RenderedButtonLink, buttonLook } from \"./button-look\";\nimport type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-look\";\n\n// The kit's text button, and `<Button href>`, the link that looks like one. Its look —\n// the variant, size and tone maps, and `buttonClasses` — is ./button-look, which\n// IconButton shares (0.34, MOD-2, refactor audit 2026-10).\n\n/**\n * ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS\n * it unless you pass `type=\"button\"`. Kept native on purpose — a form's own submit\n * button relies on it, and changing the default would silently stop those forms from\n * submitting. Pass `type=\"button\"` for every other action in a form (keksdose had six\n * link buttons submitting their form).\n */\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** `default` (the quiet action, what a Button without `variant` draws), `accent` (the\n * strong call to action, solid brand), `secondary`, `ghost`, `danger` or `link`. See\n * {@link ButtonVariant}. */\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n /** Text colour for `link` and `ghost`: `muted` (quiet, body colour on hover) or\n * `danger` (quiet, `--danger` on hover). `danger` also makes `secondary` an\n * outlined destructive button and `default` a soft one; ignored by the other\n * variants. See {@link ButtonTone}. */\n tone?: ButtonTone;\n /**\n * Make it a toggle button, as {@link IconButton}'s `pressed` does. `true` sets\n * `aria-pressed=\"true\"` and draws the \"on\" look — on a `link`, the brand colour and a\n * medium weight; on the boxed variants, the quiet brand fill IconButton uses — and\n * `false` sets `aria-pressed=\"false\"` with the ordinary look. Left out, no\n * `aria-pressed` (or the caller's own). For lenkbank's \"All speeds\" text toggle\n * (gear/hysteresis-charts.tsx:482), which is a hand-rolled `<button aria-pressed>`\n * swapping two class strings; with `variant=\"link\" tone=\"muted\"` it is this.\n */\n pressed?: boolean;\n /**\n * Why the action is not available — the button's half of DangerConfirm's\n * `lockedReason` (kastlan handover-detail-page.tsx:197 locks a signed handover).\n *\n * A disabled button that cannot say why is a dead end: the native `disabled` takes it\n * out of the tab order, so a keyboard user never lands on it, and a pointer gets a\n * `not-allowed` cursor and nothing else. With a reason the button is `aria-disabled`\n * instead — still focusable, still hoverable — clicks (and the Enter/Space and form\n * submission they stand for) do nothing, and the reason is shown in the kit\n * {@link Tooltip} and attached with `aria-describedby`, so a screen reader hears it\n * on focus. It wins over `disabled`: passing both keeps the button reachable, which is\n * the point of giving a reason.\n *\n * The description is a `hidden` copy of the reason rather than the bubble itself: the\n * bubble exists only while hovered or focused once it is portalled, and a description\n * that comes and goes is read inconsistently. The bubble is kept visual, the way\n * DangerConfirm keeps its own.\n */\n disabledReason?: ReactNode;\n /**\n * Where `disabledReason` shows (0.32, §4 \"No fact only in a tooltip\"): by default a\n * line under the button at Large and on a touch screen, the tooltip otherwise. See\n * {@link DisabledReasonDisplay}.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This button COMMITS — it saves, creates, deletes. Under a locked\n * {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's\n * reason (which wins over a `disabledReason` of its own). No provider, or an\n * unlocked one: no effect. See {@link WriteLockProvider}; a {@link CommitScope} since\n * 0.33 (`COMMIT_EXCEPT_BILLING`: every lock but a lapsed plan's).\n */\n commit?: CommitScope;\n /**\n * Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.\n *\n * kastlan's FormActions (form-actions.tsx:50) disables its submit and swaps in a\n * `submittingLabel`, so the button jumps width mid-click and a screen reader hears\n * the name change instead of \"busy\". Here the label STAYS — it is still the\n * accessible name and still holds the button's width — and the spinner is drawn over\n * it in the text colour, so nothing beside the button moves.\n *\n * The same prop {@link FileButton} has (`pending`), with one difference: this one is\n * `aria-disabled`, not `disabled`. The button being pressed is the one that has focus,\n * and a native `disabled` drops that focus to `<body>` the moment the save starts;\n * clicks, and the Enter that submits a form through it, are swallowed instead.\n */\n pending?: boolean;\n /** Only on the link form — see {@link ButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<Button href>`: a link that looks like a button — for an action that NAVIGATES.\n *\n * kastlan paints `buttonClasses(…)` onto router Links by hand in six places (the\n * invoice / lease / unit preview dialogs' \"Open\" at invoice-preview-dialog.tsx:36,\n * platform-companies-page.tsx:111, verify-email-page.tsx:86's \"Log in\",\n * billing-history-page.tsx:85's PDF download), and has 26 `<Button onClick={() =>\n * navigate(…)}>` that are links in all but markup: no middle-click, no \"open in new\n * tab\", no URL on hover, and a screen reader announces a button that then changes the\n * page. With `href` it is an `<a>`: the provider's router link\n * ({@link UiKitProvider}'s `linkComponent`), or `renderLink` if given, else a plain one.\n *\n * `type` and `form` are typed out: they are a `<button>`'s, and a link never submits.\n */\nexport interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\"> {\n href: string;\n variant?: ButtonVariant;\n size?: ButtonSize;\n stretch?: boolean;\n tone?: ButtonTone;\n /** The in-app router link, over the provider's `linkComponent` (see\n * {@link KitLinkComponent}). Ignored when `external`. */\n renderLink?: KitLinkComponent;\n /** Leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`, never the\n * router link, and \"(opens in a new tab)\" read after the label — billing-history's\n * Stripe PDF. A `target=\"_blank\"` of your own is announced the same way. */\n external?: boolean;\n /**\n * Navigate by replacing the current history entry, so Back does not return here —\n * keksdose F1: the \"Continue\" out of a one-shot page (an emailed confirmation link, a\n * payment return URL) that must not be landed on again. Handed to the router link as\n * `replace` (see `KitLinkProps.replace` for mapping it); on a plain `<a>` a plain click\n * becomes `location.replace(href)`. Ignored when `external`.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`, so the browser loads the whole document instead\n * of the router swapping the view — keksdose F1: a route the SPA does not own (a\n * server-rendered export, a logout that must drop every in-memory cache). Wins over\n * `renderLink` and the provider's `linkComponent`.\n */\n reloadDocument?: boolean;\n /** A link cannot be `disabled`, so this renders an `<a>` with NO `href` —\n * `role=\"link\"` and `aria-disabled`, out of the tab order like a disabled button,\n * with the disabled look. The router link is not used: it needs somewhere to go. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pending?: never;\n pressed?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n}\n\n// Declared link-first so `<Button href>` picks the link props; the LAST signature is\n// what `ComponentProps<typeof Button>` reads, so wrappers (FileButton) still see the\n// `<button>` props they always did.\nexport function Button(props: ButtonLinkProps): ReactElement;\nexport function Button(props: ButtonProps): ReactElement;\nexport function Button(props: ButtonProps | ButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <ButtonLink {...(props as ButtonLinkProps)} />;\n return <ButtonElement {...(props as ButtonProps)} />;\n}\n\nfunction ButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n className,\n children,\n target,\n rel,\n onClick,\n ...rest\n}: ButtonLinkProps) {\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The same rule as every kit link (`pickLinkRenderer`): an external `href` or an\n // in-page `#anchor` is never handed to the provider's router link, even without\n // `external`.\n // `reloadDocument` is the caller saying \"not the router\" for this one link.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const cls = cn(\n buttonLook(variant, size, stretch, tone),\n // `relative` holds the sr-only note (see sr-only-containment.test).\n newTab && \"relative\",\n // BUTTON_BASE's `disabled:` classes never match an `<a>`; no pointer events also\n // keeps the hover fill from lighting up on something that refuses the click.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const body = (\n <>\n {children}\n {/* The space OUTSIDE the sr-only span: a name computation trims each child's\n text, so one inside it was lost (\"Download(opens…\"). In a flex box a bare\n space draws nothing. */}\n {newTab && \" \"}\n {newTab && <span className=\"sr-only\">({common.opensInNewTab})</span>}\n </>\n );\n if (disabled) {\n return (\n <a {...rest} role=\"link\" aria-disabled=\"true\" className={cls}>\n {body}\n </a>\n );\n }\n if (external || !Link) {\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? onClick : replacingClick(onClick, href, replace, target)}\n className={cls}\n >\n {body}\n </a>\n );\n }\n return (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...routerLinkNavigation(replace)}\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n className={cls}\n >\n {body}\n </RenderedButtonLink>\n );\n}\n\nfunction ButtonElement({\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n pressed,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n className,\n onClick,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ...rest\n}: ButtonProps) {\n const reasonId = useId();\n const disabledReason = useCommitReason(commit, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const inert = locked || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n // `preventDefault` as well as not calling through: a locked submit button must\n // not submit its form, and the browser does that after this handler returns.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n className={cn(\n buttonLook(variant, size, stretch, tone),\n pressed && (variant === \"link\" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),\n // The disabled look for the focusable kind of disabled.\n locked && \"cursor-not-allowed opacity-50\",\n // No dimming while pending: the spinner IS the state, and at half opacity it\n // would be the faintest thing on the button. `relative` anchors it.\n pending && \"relative cursor-progress\",\n className,\n )}\n >\n {pending ? (\n <>\n {/* Transparent, not hidden: it keeps the width AND stays the accessible\n name (`visibility: hidden` would drop it from the name). `gap-[inherit]`\n keeps an icon + text spaced as the button spaces them. */}\n <span className=\"inline-flex items-center justify-center gap-[inherit] opacity-0\">{children}</span>\n {/* Decorative: `aria-busy` says it, as on FileButton. */}\n <Spinner label={null} className=\"absolute inset-0 m-auto size-4 border-current/30 border-t-current\" />\n </>\n ) : (\n children\n )}\n </button>\n );\n if (!locked) return button;\n return (\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {button}\n </LockedReason>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/button.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactElement, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer, replacingClick, routerLinkNavigation } from \"./text-link\";\nimport type { CommitScope } from \"./write-lock\";\nimport { LockedReason, useRefusal, type DisabledReasonDisplay } from \"./field-parts\";\nimport { Spinner } from \"./spinner\";\nimport { BUTTON_LINK_PRESSED, ICON_BUTTON_PRESSED, RenderedButtonLink, buttonLook } from \"./button-look\";\nimport type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-look\";\n\n// The kit's text button, and `<Button href>`, the link that looks like one. Its look —\n// the variant, size and tone maps, and `buttonClasses` — is ./button-look, which\n// IconButton shares (0.34, MOD-2, refactor audit 2026-10).\n\n/**\n * ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS\n * it unless you pass `type=\"button\"`. Kept native on purpose — a form's own submit\n * button relies on it, and changing the default would silently stop those forms from\n * submitting. Pass `type=\"button\"` for every other action in a form (keksdose had six\n * link buttons submitting their form).\n */\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** `default` (the quiet action, what a Button without `variant` draws), `accent` (the\n * strong call to action, solid brand), `secondary`, `ghost`, `danger` or `link`. See\n * {@link ButtonVariant}. */\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n /** Text colour for `link` and `ghost`: `muted` (quiet, body colour on hover) or\n * `danger` (quiet, `--danger` on hover). `danger` also makes `secondary` an\n * outlined destructive button and `default` a soft one; ignored by the other\n * variants. See {@link ButtonTone}. */\n tone?: ButtonTone;\n /**\n * Make it a toggle button, as {@link IconButton}'s `pressed` does. `true` sets\n * `aria-pressed=\"true\"` and draws the \"on\" look — on a `link`, the brand colour and a\n * medium weight; on the boxed variants, the quiet brand fill IconButton uses — and\n * `false` sets `aria-pressed=\"false\"` with the ordinary look. Left out, no\n * `aria-pressed` (or the caller's own). For lenkbank's \"All speeds\" text toggle\n * (gear/hysteresis-charts.tsx:482), which is a hand-rolled `<button aria-pressed>`\n * swapping two class strings; with `variant=\"link\" tone=\"muted\"` it is this.\n */\n pressed?: boolean;\n /**\n * Why the action is not available — the button's half of DangerConfirm's\n * `lockedReason` (kastlan handover-detail-page.tsx:197 locks a signed handover).\n *\n * A disabled button that cannot say why is a dead end: the native `disabled` takes it\n * out of the tab order, so a keyboard user never lands on it, and a pointer gets a\n * `not-allowed` cursor and nothing else. With a reason the button is `aria-disabled`\n * instead — still focusable, still hoverable — clicks (and the Enter/Space and form\n * submission they stand for) do nothing, and the reason is shown in the kit\n * {@link Tooltip} and attached with `aria-describedby`, so a screen reader hears it\n * on focus. It wins over `disabled`: passing both keeps the button reachable, which is\n * the point of giving a reason.\n *\n * The description is a `hidden` copy of the reason rather than the bubble itself: the\n * bubble exists only while hovered or focused once it is portalled, and a description\n * that comes and goes is read inconsistently. The bubble is kept visual, the way\n * DangerConfirm keeps its own.\n *\n * `refused={reason}` (0.55) is the same refusal by the button's own name for it, with\n * a form for a reason said elsewhere — see `refused`.\n */\n disabledReason?: ReactNode;\n /**\n * Refuse the action and stay where the user can learn why — no permission, a\n * read-only plan, a held lock, an end already reached (0.52 code review 2026-10-31,\n * held to 0.55). Shown with `disabled`, a refused action leaves the tab order and says\n * nothing; an app's way round that was a Tooltip over the disabled button, which a\n * keyboard never opens (Kurvenschmiede's owner-only delete,\n * shared/lib/table-columns.tsx), and the kit's own arrows at an end hand-wrote\n * `aria-disabled` and a guard in their `onClick`.\n *\n * Refused, the button is `aria-disabled=\"true\"` — never the native `disabled`, which\n * it wins over — and stays focusable and hoverable, with the disabled look; every\n * activation is swallowed: the click, the Enter and Space that stand for it, and the\n * submission a submit button would make.\n *\n * - **Words** (`refused={t(\"sharing.owner_only\")}`): the reason, said the\n * `disabledReason` way — the kit {@link Tooltip}, or a line at Large and on touch\n * (`disabledReasonDisplay`) — and attached with `aria-describedby`, so a screen\n * reader hears it on focus. One path with `disabledReason`; these words win over its.\n * - **`true`**: refused with no words of its own, for a reason the screen already\n * says — a banner over the form, a line under it — which the button's own\n * `aria-describedby` names ({@link DisabledReasonLine} draws the line with its id),\n * or one that needs no saying (› on the last picture). No bubble.\n * - `false`, `null`, `undefined` or `\"\"`: not refused, as `cond && reason` gives.\n *\n * Under a locked {@link WriteLockProvider} a `commit` button says the lock's reason\n * instead, as it does over `disabledReason`.\n */\n refused?: ReactNode | boolean;\n /**\n * Where `disabledReason` shows (0.32, §4 \"No fact only in a tooltip\"): by default a\n * line under the button at Large and on a touch screen, the tooltip otherwise. See\n * {@link DisabledReasonDisplay}. A worded `refused` shows the same way.\n */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This button COMMITS — it saves, creates, deletes. Under a locked\n * {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's\n * reason (which wins over a `disabledReason` of its own). No provider, or an\n * unlocked one: no effect. See {@link WriteLockProvider}; a {@link CommitScope} since\n * 0.33 (`COMMIT_EXCEPT_BILLING`: every lock but a lapsed plan's).\n */\n commit?: CommitScope;\n /**\n * Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.\n *\n * kastlan's FormActions (form-actions.tsx:50) disables its submit and swaps in a\n * `submittingLabel`, so the button jumps width mid-click and a screen reader hears\n * the name change instead of \"busy\". Here the label STAYS — it is still the\n * accessible name and still holds the button's width — and the spinner is drawn over\n * it in the text colour, so nothing beside the button moves.\n *\n * The same prop {@link FileButton} has (`pending`), with one difference: this one is\n * `aria-disabled`, not `disabled`. The button being pressed is the one that has focus,\n * and a native `disabled` drops that focus to `<body>` the moment the save starts;\n * clicks, and the Enter that submits a form through it, are swallowed instead.\n */\n pending?: boolean;\n /** Only on the link form — see {@link ButtonLinkProps}. */\n href?: never;\n renderLink?: never;\n external?: never;\n replace?: never;\n reloadDocument?: never;\n}\n\n/**\n * `<Button href>`: a link that looks like a button — for an action that NAVIGATES.\n *\n * kastlan paints `buttonClasses(…)` onto router Links by hand in six places (the\n * invoice / lease / unit preview dialogs' \"Open\" at invoice-preview-dialog.tsx:36,\n * platform-companies-page.tsx:111, verify-email-page.tsx:86's \"Log in\",\n * billing-history-page.tsx:85's PDF download), and has 26 `<Button onClick={() =>\n * navigate(…)}>` that are links in all but markup: no middle-click, no \"open in new\n * tab\", no URL on hover, and a screen reader announces a button that then changes the\n * page. With `href` it is an `<a>`: the provider's router link\n * ({@link UiKitProvider}'s `linkComponent`), or `renderLink` if given, else a plain one.\n *\n * `type` and `form` are typed out: they are a `<button>`'s, and a link never submits.\n */\nexport interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"type\" | \"href\"> {\n href: string;\n variant?: ButtonVariant;\n size?: ButtonSize;\n stretch?: boolean;\n tone?: ButtonTone;\n /** The in-app router link, over the provider's `linkComponent` (see\n * {@link KitLinkComponent}). Ignored when `external`. */\n renderLink?: KitLinkComponent;\n /** Leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`, never the\n * router link, and \"(opens in a new tab)\" read after the label — billing-history's\n * Stripe PDF. A `target=\"_blank\"` of your own is announced the same way. */\n external?: boolean;\n /**\n * Navigate by replacing the current history entry, so Back does not return here —\n * keksdose F1: the \"Continue\" out of a one-shot page (an emailed confirmation link, a\n * payment return URL) that must not be landed on again. Handed to the router link as\n * `replace` (see `KitLinkProps.replace` for mapping it); on a plain `<a>` a plain click\n * becomes `location.replace(href)`. Ignored when `external`.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`, so the browser loads the whole document instead\n * of the router swapping the view — keksdose F1: a route the SPA does not own (a\n * server-rendered export, a logout that must drop every in-memory cache). Wins over\n * `renderLink` and the provider's `linkComponent`.\n */\n reloadDocument?: boolean;\n /** A link cannot be `disabled`, so this renders an `<a>` with NO `href` —\n * `role=\"link\"` and `aria-disabled`, out of the tab order like a disabled button,\n * with the disabled look. The router link is not used: it needs somewhere to go. */\n disabled?: boolean;\n ref?: Ref<HTMLAnchorElement>;\n type?: never;\n form?: never;\n pending?: never;\n pressed?: never;\n disabledReason?: never;\n /** A link that explains why it cannot be followed is a refused BUTTON — see\n * {@link ButtonProps.refused}. */\n refused?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n}\n\n// Declared link-first so `<Button href>` picks the link props; the LAST signature is\n// what `ComponentProps<typeof Button>` reads, so wrappers (FileButton) still see the\n// `<button>` props they always did.\nexport function Button(props: ButtonLinkProps): ReactElement;\nexport function Button(props: ButtonProps): ReactElement;\nexport function Button(props: ButtonProps | ButtonLinkProps): ReactElement {\n if (props.href !== undefined) return <ButtonLink {...(props as ButtonLinkProps)} />;\n return <ButtonElement {...(props as ButtonProps)} />;\n}\n\nfunction ButtonLink({\n href,\n renderLink,\n external,\n replace,\n reloadDocument,\n disabled,\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n className,\n children,\n target,\n rel,\n onClick,\n ...rest\n}: ButtonLinkProps) {\n const kitLink = useKitLink();\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The same rule as every kit link (`pickLinkRenderer`): an external `href` or an\n // in-page `#anchor` is never handed to the provider's router link, even without\n // `external`.\n // `reloadDocument` is the caller saying \"not the router\" for this one link.\n const Link = reloadDocument ? undefined : pickLinkRenderer<KitLinkProps>(renderLink, kitLink, href);\n const newTab = !disabled && (external || target === \"_blank\");\n const cls = cn(\n buttonLook(variant, size, stretch, tone),\n // `relative` holds the sr-only note (see sr-only-containment.test).\n newTab && \"relative\",\n // BUTTON_BASE's `disabled:` classes never match an `<a>`; no pointer events also\n // keeps the hover fill from lighting up on something that refuses the click.\n disabled && \"pointer-events-none cursor-not-allowed opacity-50\",\n className,\n );\n const body = (\n <>\n {children}\n {/* The space OUTSIDE the sr-only span: a name computation trims each child's\n text, so one inside it was lost (\"Download(opens…\"). In a flex box a bare\n space draws nothing. */}\n {newTab && \" \"}\n {newTab && <span className=\"sr-only\">({common.opensInNewTab})</span>}\n </>\n );\n if (disabled) {\n return (\n <a {...rest} role=\"link\" aria-disabled=\"true\" className={cls}>\n {body}\n </a>\n );\n }\n if (external || !Link) {\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : target}\n rel={external ? (rel ?? \"noopener noreferrer\") : rel}\n onClick={external ? onClick : replacingClick(onClick, href, replace, target)}\n className={cls}\n >\n {body}\n </a>\n );\n }\n return (\n <RenderedButtonLink\n render={Link}\n {...rest}\n {...routerLinkNavigation(replace)}\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n className={cls}\n >\n {body}\n </RenderedButtonLink>\n );\n}\n\nfunction ButtonElement({\n variant = \"default\",\n size = \"md\",\n stretch,\n tone = \"default\",\n pressed,\n refused: refusedProp,\n disabledReason: ownDisabledReason,\n disabledReasonDisplay,\n commit,\n pending,\n className,\n onClick,\n children,\n href: _href,\n renderLink: _renderLink,\n external: _external,\n replace: _replace,\n reloadDocument: _reloadDocument,\n ...rest\n}: ButtonProps) {\n // `locked`: refused WITH words, which describe it; `refused` also covers `true`.\n const { reason: disabledReason, locked, reasonId, refused } = useRefusal(refusedProp, ownDisabledReason, commit);\n const inert = refused || Boolean(pending);\n const ownDescribedBy = rest[\"aria-describedby\"];\n const button = (\n <button\n {...rest}\n disabled={inert ? undefined : rest.disabled}\n aria-disabled={inert || rest[\"aria-disabled\"]}\n aria-busy={pending || rest[\"aria-busy\"]}\n aria-describedby={locked ? (ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId) : ownDescribedBy}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n // `preventDefault` as well as not calling through: a locked submit button must\n // not submit its form, and the browser does that after this handler returns.\n if (inert) {\n e.preventDefault();\n return;\n }\n onClick?.(e);\n }}\n className={cn(\n buttonLook(variant, size, stretch, tone),\n pressed && (variant === \"link\" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),\n // The disabled look for the focusable kind of disabled.\n refused && \"cursor-not-allowed opacity-50\",\n // No dimming while pending: the spinner IS the state, and at half opacity it\n // would be the faintest thing on the button. `relative` anchors it.\n pending && \"relative cursor-progress\",\n className,\n )}\n >\n {pending ? (\n <>\n {/* Transparent, not hidden: it keeps the width AND stays the accessible\n name (`visibility: hidden` would drop it from the name). `gap-[inherit]`\n keeps an icon + text spaced as the button spaces them. */}\n <span className=\"inline-flex items-center justify-center gap-[inherit] opacity-0\">{children}</span>\n {/* Decorative: `aria-busy` says it, as on FileButton. */}\n <Spinner label={null} className=\"absolute inset-0 m-auto size-4 border-current/30 border-t-current\" />\n </>\n ) : (\n children\n )}\n </button>\n );\n if (!locked) return button;\n return (\n <LockedReason\n lock={{ reason: disabledReason, reasonId }}\n display={disabledReasonDisplay}\n className={stretch ? \"self-stretch\" : undefined}\n >\n {button}\n </LockedReason>\n );\n}\n"],"mappings":";AA8MuC,SAwCnC,UAxCmC,KA8CtB,YA9CsB;AA7MvC,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,kBAAkB;AAEhE,SAAS,kBAAkB,gBAAgB,4BAA4B;AAEvE,SAAS,cAAc,kBAA8C;AACrE,SAAS,eAAe;AACxB,SAAS,qBAAqB,qBAAqB,oBAAoB,kBAAkB;AAqMlF,SAAS,OAAO,OAAoD;AACzE,MAAI,MAAM,SAAS,OAAW,QAAO,oBAAC,cAAY,GAAI,OAA2B;AACjF,SAAO,oBAAC,iBAAe,GAAI,OAAuB;AACpD;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAK3D,QAAM,OAAO,iBAAiB,SAAY,iBAA+B,YAAY,SAAS,IAAI;AAClG,QAAM,SAAS,CAAC,aAAa,YAAY,WAAW;AACpD,QAAM,MAAM;AAAA,IACV,WAAW,SAAS,MAAM,SAAS,IAAI;AAAA;AAAA,IAEvC,UAAU;AAAA;AAAA;AAAA,IAGV,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACG;AAAA;AAAA,IAIA,UAAU;AAAA,IACV,UAAU,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,MAAE,OAAO;AAAA,MAAc;AAAA,OAAC;AAAA,KAC/D;AAEF,MAAI,UAAU;AACZ,WACE,oBAAC,OAAG,GAAG,MAAM,MAAK,QAAO,iBAAc,QAAO,WAAW,KACtD,gBACH;AAAA,EAEJ;AACA,MAAI,YAAY,CAAC,MAAM;AACrB,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA,QAAQ,WAAW,WAAW;AAAA,QAC9B,KAAK,WAAY,OAAO,wBAAyB;AAAA,QACjD,SAAS,WAAW,UAAU,eAAe,SAAS,MAAM,SAAS,MAAM;AAAA,QAC3E,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ;AAAA,MACP,GAAG;AAAA,MACH,GAAG,qBAAqB,OAAO;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,GAAG;AACL,GAAgB;AAEd,QAAM,EAAE,QAAQ,gBAAgB,QAAQ,UAAU,QAAQ,IAAI,WAAW,aAAa,mBAAmB,MAAM;AAC/G,QAAM,QAAQ,WAAW,QAAQ,OAAO;AACxC,QAAM,iBAAiB,KAAK,kBAAkB;AAC9C,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,UAAU,QAAQ,SAAY,KAAK;AAAA,MACnC,iBAAe,SAAS,KAAK,eAAe;AAAA,MAC5C,aAAW,WAAW,KAAK,WAAW;AAAA,MACtC,oBAAkB,SAAU,iBAAiB,GAAG,cAAc,IAAI,QAAQ,KAAK,WAAY;AAAA,MAC3F,gBAAc,WAAW,KAAK,cAAc;AAAA,MAC5C,SAAS,CAAC,MAAM;AAGd,YAAI,OAAO;AACT,YAAE,eAAe;AACjB;AAAA,QACF;AACA,kBAAU,CAAC;AAAA,MACb;AAAA,MACA,WAAW;AAAA,QACT,WAAW,SAAS,MAAM,SAAS,IAAI;AAAA,QACvC,YAAY,YAAY,SAAS,sBAAsB;AAAA;AAAA,QAEvD,WAAW;AAAA;AAAA;AAAA,QAGX,WAAW;AAAA,QACX;AAAA,MACF;AAAA,MAEC,oBACC,iCAIE;AAAA,4BAAC,UAAK,WAAU,mEAAmE,UAAS;AAAA,QAE5F,oBAAC,WAAQ,OAAO,MAAM,WAAU,qEAAoE;AAAA,SACtG,IAEA;AAAA;AAAA,EAEJ;AAEF,MAAI,CAAC,OAAQ,QAAO;AACpB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,EAAE,QAAQ,gBAAgB,SAAS;AAAA,MACzC,SAAS;AAAA,MACT,WAAW,UAAU,iBAAiB;AAAA,MAErC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -3,7 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { dirOf, horizontalStep } from "../lib/direction.js";
|
|
6
|
-
import { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from "../lib/dates.js";
|
|
6
|
+
import { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso, todayIso } from "../lib/dates.js";
|
|
7
|
+
import { dateFormatter } from "../lib/intl-cache.js";
|
|
7
8
|
import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels.js";
|
|
8
9
|
import { lerpHex, textOn } from "../theme/chart-palette.js";
|
|
9
10
|
import { parseHex } from "../theme/color.js";
|
|
@@ -16,9 +17,10 @@ const DAY_NAME_FORMAT = {
|
|
|
16
17
|
month: "long",
|
|
17
18
|
year: "numeric"
|
|
18
19
|
};
|
|
20
|
+
const MONTH_LABEL_FORMAT = { month: "short" };
|
|
19
21
|
const EMPTY_FILL = "color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))";
|
|
20
22
|
const weekdayIndex = (d, weekStart) => (d.getDay() - weekStart + 7) % 7;
|
|
21
|
-
function heatmapWindowEnd(from, to, anchor, latestData, today =
|
|
23
|
+
function heatmapWindowEnd(from, to, anchor, latestData, today = todayIso()) {
|
|
22
24
|
let end;
|
|
23
25
|
if (anchor === "today") end = today;
|
|
24
26
|
else if (anchor === "latest") end = latestData > today ? latestData : today;
|
|
@@ -146,7 +148,7 @@ function CalendarHeatmap({
|
|
|
146
148
|
el.removeEventListener("scroll", onScroll);
|
|
147
149
|
};
|
|
148
150
|
}, [layout, firstIso, lastIso]);
|
|
149
|
-
const formatDay = (d) =>
|
|
151
|
+
const formatDay = (d) => dateFormatter(locale, DAY_NAME_FORMAT).format(d);
|
|
150
152
|
const weekdayLabels = useMemo(() => {
|
|
151
153
|
const ref = new Date(2024, 0, 7 + weekStart);
|
|
152
154
|
const week = Array.from({ length: 7 }, (_, i) => {
|
|
@@ -261,7 +263,7 @@ function CalendarHeatmap({
|
|
|
261
263
|
for (let r = 0; r < 7; r++) {
|
|
262
264
|
const d = dayAt(w * 7 + r);
|
|
263
265
|
if (d && (d.getDate() === 1 || w === 0 && r === lead && d.getDate() <= 14)) {
|
|
264
|
-
return
|
|
266
|
+
return dateFormatter(locale, MONTH_LABEL_FORMAT).format(d);
|
|
265
267
|
}
|
|
266
268
|
}
|
|
267
269
|
return "";
|