@eifi1/ui-kit 0.51.6 → 0.52.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 +146 -26
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +3 -7
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +3 -2
- package/dist/auth/complete-name-dialog.js +3 -5
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +11 -2
- package/dist/billing/cancel-request.js +15 -3
- package/dist/billing/cancel-request.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +22 -10
- package/dist/billing/cancel-subscription.js +8 -11
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +21 -9
- package/dist/billing/plan-switch.js +10 -8
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +22 -4
- package/dist/components/amount-input.js +3 -3
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.d.ts +21 -2
- package/dist/components/calculator.js +53 -14
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +119 -7
- package/dist/components/combobox-core.js +151 -46
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +142 -131
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -7
- package/dist/components/data-table-mobile.js +63 -31
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +25 -8
- package/dist/components/data-table.js +154 -25
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/entity-combobox.js +4 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +150 -141
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +4 -3
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +24 -0
- package/dist/components/modal.js +12 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/money-draft.js +7 -5
- package/dist/components/money-draft.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -2
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +6 -4
- package/dist/components/number-field.js +3 -2
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +4 -1
- package/dist/components/number-input.js +3 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +4 -5
- package/dist/components/reauth-dialog.js +1 -4
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +10 -3
- package/dist/components/series-chart-axes.js +1 -0
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +13 -11
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +7 -1
- package/dist/hooks/use-overlay-history.js +35 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/calc.d.ts +7 -1
- package/dist/lib/calc.js +3 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +2 -2
- package/dist/lib/clipboard-minus.js +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +7 -1
- package/dist/lib/decimal-marks.js +5 -2
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.d.ts +5 -3
- package/dist/lib/format.js +16 -13
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +38 -0
- package/dist/lib/text-match.js +24 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +16 -4
- package/dist/tour/tour.js +62 -778
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js +2 -2
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +12 -8
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +5 -10
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +9 -8
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-request.ts +28 -6
- package/src/billing/cancel-subscription.tsx +29 -22
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +31 -20
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +32 -14
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/calculator.tsx +98 -17
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +303 -69
- package/src/components/combobox.tsx +184 -155
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +110 -27
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +267 -41
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/entity-combobox.tsx +4 -1
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +198 -152
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +14 -5
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +45 -4
- package/src/components/money-draft.ts +16 -5
- package/src/components/money-field.tsx +2 -2
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-entity-combobox.tsx +1 -0
- package/src/components/multi-select.tsx +21 -9
- package/src/components/number-field.tsx +14 -7
- package/src/components/number-input.tsx +9 -3
- package/src/components/numpad-sheet.tsx +4 -3
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/reauth-dialog.tsx +6 -10
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +11 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +35 -19
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-dismiss.ts +3 -2
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +70 -5
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/calc.ts +13 -2
- package/src/lib/clipboard-minus.ts +14 -4
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/decimal-marks.ts +12 -3
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +34 -19
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +69 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +116 -1193
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/tour/use-tour-url-sync.ts +7 -3
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode, RefObject } from "react";
|
|
2
2
|
import { type LabelOverride } from "../i18n/kit-labels.js";
|
|
3
3
|
import { DEFAULT_BULK_ACTION_BAR_LABELS } from "./bulk-action-bar-labels.js";
|
|
4
4
|
export interface BulkActionBarLabels {
|
|
@@ -85,6 +85,16 @@ export interface BulkActionBarProps extends Omit<ComponentPropsWithRef<"div">, "
|
|
|
85
85
|
panel?: ReactNode;
|
|
86
86
|
/** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */
|
|
87
87
|
labels?: LabelOverride<BulkActionBarLabels>;
|
|
88
|
+
/**
|
|
89
|
+
* Where the focus goes when the bar leaves while it holds it — after Clear, or an action
|
|
90
|
+
* that empties the selection (0.52, refactor audit 2026-10-30, DSP-15). Without it, the
|
|
91
|
+
* control the focus came into the bar from (the row's box, say), if that is still on the
|
|
92
|
+
* page; with it, this first. Pass one where the selection takes that control with it — a
|
|
93
|
+
* bulk delete removes the rows whose boxes were ticked: the table, the list's heading,
|
|
94
|
+
* the "Select" button that entered selection mode. A bar that leaves without the focus
|
|
95
|
+
* in it moves nothing.
|
|
96
|
+
*/
|
|
97
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
88
98
|
}
|
|
89
99
|
/**
|
|
90
100
|
* The bar that appears while rows are selected: the count, a way out, and what can be
|
|
@@ -115,4 +125,4 @@ export interface BulkActionBarProps extends Omit<ComponentPropsWithRef<"div">, "
|
|
|
115
125
|
* It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,
|
|
116
126
|
* because tapping more rows is how a selection grows.
|
|
117
127
|
*/
|
|
118
|
-
export declare function BulkActionBar({ count, onClear, children, variant: variantProp, open, panel, labels: labelsProp, className, style, onKeyDown, onFocus, ...rest }: BulkActionBarProps): import("react").JSX.Element;
|
|
128
|
+
export declare function BulkActionBar({ count, onClear, children, variant: variantProp, open, panel, labels: labelsProp, returnFocusRef, className, style, onKeyDown, onFocus, onBlur, ...rest }: BulkActionBarProps): import("react").JSX.Element;
|
|
@@ -8,6 +8,8 @@ import { useKitLabels } from "../i18n/kit-labels.js";
|
|
|
8
8
|
import { useAnnounce } from "../hooks/use-announce.js";
|
|
9
9
|
import { useBreakpoint } from "../hooks/use-breakpoint.js";
|
|
10
10
|
import { OVERLAY_EXIT_MS, skipsExitAnimation } from "../hooks/use-close-transition.js";
|
|
11
|
+
import { focusFirst, focusIsLost, whenFocusLeftForNothing } from "../lib/focus-left.js";
|
|
12
|
+
import { hasContent } from "../lib/presence.js";
|
|
11
13
|
import { Button } from "./button.js";
|
|
12
14
|
import { IconButton } from "./icon-button.js";
|
|
13
15
|
import { DEFAULT_BULK_ACTION_BAR_LABELS } from "./bulk-action-bar-labels.js";
|
|
@@ -46,10 +48,12 @@ function BulkActionBar({
|
|
|
46
48
|
open,
|
|
47
49
|
panel,
|
|
48
50
|
labels: labelsProp,
|
|
51
|
+
returnFocusRef,
|
|
49
52
|
className,
|
|
50
53
|
style,
|
|
51
54
|
onKeyDown,
|
|
52
55
|
onFocus,
|
|
56
|
+
onBlur,
|
|
53
57
|
...rest
|
|
54
58
|
}) {
|
|
55
59
|
const labels = useKitLabels("bulkActionBar", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);
|
|
@@ -84,6 +88,37 @@ function BulkActionBar({
|
|
|
84
88
|
useLayoutEffect(() => {
|
|
85
89
|
rove(barRef.current, active);
|
|
86
90
|
});
|
|
91
|
+
const cameFrom = useRef(null);
|
|
92
|
+
const holdsFocus = useRef(false);
|
|
93
|
+
const trackFocus = {
|
|
94
|
+
onFocus: (e) => {
|
|
95
|
+
holdsFocus.current = true;
|
|
96
|
+
const from = e.relatedTarget;
|
|
97
|
+
if (from instanceof HTMLElement && !e.currentTarget.contains(from) && !cameFrom.current?.isConnected)
|
|
98
|
+
cameFrom.current = from;
|
|
99
|
+
},
|
|
100
|
+
onBlur: (e) => {
|
|
101
|
+
onBlur?.(e);
|
|
102
|
+
const to = e.relatedTarget;
|
|
103
|
+
if (to instanceof Element && !e.currentTarget.contains(to)) holdsFocus.current = false;
|
|
104
|
+
whenFocusLeftForNothing(e, () => {
|
|
105
|
+
holdsFocus.current = false;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const wasShown = useRef(shown);
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
if (wasShown.current === shown) return;
|
|
112
|
+
wasShown.current = shown;
|
|
113
|
+
const from = cameFrom.current;
|
|
114
|
+
const held = holdsFocus.current;
|
|
115
|
+
cameFrom.current = null;
|
|
116
|
+
holdsFocus.current = false;
|
|
117
|
+
if (shown) return;
|
|
118
|
+
const surface = barRef.current?.closest("[data-bulk-action-bar]");
|
|
119
|
+
const inBar = !!surface && surface.contains(document.activeElement);
|
|
120
|
+
if (inBar || held && focusIsLost()) focusFirst([returnFocusRef?.current, from]);
|
|
121
|
+
}, [shown, returnFocusRef]);
|
|
87
122
|
const handleKeyDown = (e) => {
|
|
88
123
|
onKeyDown?.(e);
|
|
89
124
|
if (e.defaultPrevented || ownsArrows(e.target)) return;
|
|
@@ -109,7 +144,7 @@ function BulkActionBar({
|
|
|
109
144
|
// `ms-auto`: when the actions wrap, the way out still sits at the bar's end.
|
|
110
145
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onClear, className: "ms-auto", children: labels.clear })
|
|
111
146
|
);
|
|
112
|
-
const hasPanel = panel
|
|
147
|
+
const hasPanel = hasContent(panel);
|
|
113
148
|
const surfaceClass = floating ? cn(
|
|
114
149
|
"fixed z-30 rounded-2xl border border-subtle bg-surface shadow-lg",
|
|
115
150
|
shown ? "animate-sheet" : "animate-sheet-out"
|
|
@@ -132,12 +167,14 @@ function BulkActionBar({
|
|
|
132
167
|
onKeyDown: handleKeyDown,
|
|
133
168
|
onFocus: (e) => {
|
|
134
169
|
onFocus?.(e);
|
|
170
|
+
if (!hasPanel) trackFocus.onFocus(e);
|
|
135
171
|
const hit = itemsOf(barRef.current).find((el) => el === e.target);
|
|
136
172
|
if (hit) {
|
|
137
173
|
active.current = hit;
|
|
138
174
|
rove(barRef.current, active);
|
|
139
175
|
}
|
|
140
176
|
},
|
|
177
|
+
onBlur: hasPanel ? void 0 : trackFocus.onBlur,
|
|
141
178
|
style: hasPanel ? void 0 : surfaceStyle,
|
|
142
179
|
className: hasPanel ? rowClass : cn(rowClass, surfaceClass, className),
|
|
143
180
|
children: [
|
|
@@ -150,26 +187,33 @@ function BulkActionBar({
|
|
|
150
187
|
);
|
|
151
188
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
152
189
|
/* @__PURE__ */ jsx("span", { ...regionProps }),
|
|
153
|
-
hasPanel ?
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
hasPanel ? (
|
|
191
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the focus is
|
|
192
|
+
only noted here (DSP-15): where it came from, whether it is in the bar; the
|
|
193
|
+
controls are the toolbar's and the panel's own. */
|
|
194
|
+
/* @__PURE__ */ jsxs(
|
|
195
|
+
"div",
|
|
196
|
+
{
|
|
197
|
+
...rest,
|
|
198
|
+
onFocus: trackFocus.onFocus,
|
|
199
|
+
onBlur: trackFocus.onBlur,
|
|
200
|
+
"data-bulk-action-bar": variant,
|
|
201
|
+
inert: !shown || void 0,
|
|
202
|
+
style: surfaceStyle,
|
|
203
|
+
className: cn("flex flex-col", surfaceClass, className),
|
|
204
|
+
children: [
|
|
205
|
+
toolbar,
|
|
206
|
+
/* @__PURE__ */ jsx(
|
|
207
|
+
"div",
|
|
208
|
+
{
|
|
209
|
+
"data-bulk-action-bar-panel": "",
|
|
210
|
+
className: cn("border-t border-subtle px-3 py-3", floating && "max-h-[60dvh] overflow-y-auto"),
|
|
211
|
+
children: panel
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
)
|
|
173
217
|
) : toolbar
|
|
174
218
|
] });
|
|
175
219
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } 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 { 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\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 className,\n style,\n onKeyDown,\n onFocus,\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 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 const hasPanel = panel !== undefined && panel !== null && panel !== false;\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 // 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 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 <div\n {...rest}\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 { 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":[]}
|
|
@@ -6,8 +6,8 @@ import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels.js";
|
|
|
6
6
|
* and its router link. Both now draw from this module, which renders nothing, so neither
|
|
7
7
|
* imports the other.
|
|
8
8
|
*
|
|
9
|
-
* Public through
|
|
10
|
-
* The other exports are the two buttons' shared parts (`@internal`).
|
|
9
|
+
* Public through the barrel (src/index.ts) by name: `buttonClasses`, `TOUCH_TARGET_LARGE`
|
|
10
|
+
* and the four types. The other exports are the two buttons' shared parts (`@internal`).
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
13
|
* A button's look.
|
|
@@ -20,9 +20,9 @@ import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels.js";
|
|
|
20
20
|
* (theme/palette-derive.ts), which no button paints.
|
|
21
21
|
* - `secondary`: an outline. `ghost`: text or glyph only. `danger`: solid `--danger`,
|
|
22
22
|
* for an action that destroys something. `link`: a text link that acts (a `<button>`).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* 0.49's `primary` and `brand` (aliases of `default` and `accent`) were removed in 0.51;
|
|
25
|
+
* `primary` will not come back with another meaning.
|
|
26
26
|
*/
|
|
27
27
|
export type ButtonVariant = "default" | "accent" | "secondary" | "ghost" | "danger" | "link";
|
|
28
28
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/button-look.ts"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\n\n/**\n * The button look, which {@link Button} and {@link IconButton} share (0.34, MOD-2, refactor\n * audit 2026-10). In ui.tsx it was the one knot in an otherwise one-way graph: Button drew\n * IconButton's pressed look, and IconButton drew Button's variant colours, its hover ink\n * and its router link. Both now draw from this module, which renders nothing, so neither\n * imports the other.\n *\n * Public through ./ui by name: `buttonClasses`, `TOUCH_TARGET_LARGE` and the four types.\n * The other exports are the two buttons' shared parts (`@internal`).\n */\n\n/**\n * A button's look.\n *\n * - `default`: the quiet action — a warm filled chip with a hairline border. What a\n * Button with no `variant` draws.\n * - `accent`: the strong one, solid `--brand` with `--brand-contrast` text — the one\n * call to action a view leads with. \"Accent\" is the button's role here, painted in\n * `--brand`; it is not the palette's second anchor colour, also called `accent`\n * (theme/palette-derive.ts), which no button paints.\n * - `secondary`: an outline. `ghost`: text or glyph only. `danger`: solid `--danger`,\n * for an action that destroys something. `link`: a text link that acts (a `<button>`).\n * - `primary` and `brand` are **deprecated** aliases of `default` and `accent` (0.49,\n * same look; a development build warns once per name). They are removed in 0.51, and\n * `primary` will not come back with another meaning.\n */\nexport type ButtonVariant = \"default\" | \"accent\" | \"secondary\" | \"ghost\" | \"danger\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\n//\n// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus\n// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each\n// variant's own colour below.\n//\n// `[&_svg]:shrink-0` (0.32.2, kastlan's 0.32.1 report): an icon beside a label keeps its\n// size when the label wraps at Large. Without it the svg was a flex item like the text,\n// and a two-line label squeezed it to a sliver (\"New invoice\" in a page header at 360 px\n// / Extra large).\nconst BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:shrink-0\",\n FOCUS_RING_WIDTH,\n);\n\n/**\n * The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at\n * least 48 px, the size a finger needs whatever the type. In px on purpose, like the\n * field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that\n * follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs\n * no render and is right on the first paint.\n */\nexport const TOUCH_TARGET_LARGE = \"large:min-h-[48px] large:min-w-[48px]\";\n\n/** The 48 px target at Large for a control whose BOX cannot grow (a chip's or a tab's\n * action, IconButton `2xs`; Tabs' overflow cues, 0.48 KS N3): an invisible `::after`\n * centred on it takes the taps a finger lands round the glyph. Not public. */\nexport const HIT_AREA_LARGE =\n \"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\n//\n// At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its\n// 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The\n// type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which\n// is a word in a sentence and has no box to grow (see `buttonLook`).\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\nconst BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {\n md: \"large:min-h-[48px]\",\n sm: \"large:min-h-[48px] large:py-2.5\",\n};\n\n/**\n * The hover of a control that may sit on the page, a card or a well (0.33, B′ —\n * docs/colour-roles-harmonization.md §12.1, §12.10): the body ink at 7 %, TRANSLUCENT,\n * so it darkens (in dark, lightens) whatever is under it by the same step — 1.11:1 or\n * more on all three surfaces, both themes, with the label at 7:1 and up. It was\n * `--bg-surface-2`, which since 0.33 is the page itself in light: a ghost button on the\n * page would not have moved at all. Not `--bg-hover` either, which is mixed against the\n * CARD and stands only 1.04:1 from the page. IconButton's `custom` tone uses the same\n * recipe at 12 %.\n */\nexport const HOVER_INK = \"hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\";\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (`default` = filled warm chip, secondary =\n// outline); `accent` is the solid brand fill for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nexport const buttonVariantClasses: Record<ButtonVariant, string> = {\n default:\n // Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the\n // border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.\n \"border border-subtle bg-surface-2 text-primary hover:bg-active focus-visible:ring-brand\",\n secondary: `border border-subtle bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n ghost: `bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n danger: \"bg-danger text-danger-contrast hover:bg-danger-hover focus-visible:ring-danger-strong\",\n accent: \"bg-brand text-brand-contrast hover:bg-brand-hover focus-visible:ring-brand\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's focus ring stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n //\n // Under More contrast the word reads `--brand-muted`, the brand AS TEXT (0.36,\n // colour-roles §13.3 decision 8): `--brand` is solved as a mark (3:1), and on More\n // contrast's darker light well Moss's read 3.68:1; the derived dark brands sit at 3.0\n // as text too. Both modes, since `contrast:` does not split them.\n link: \"rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand contrast:text-brand-muted\",\n};\n\n/**\n * The text colour of a `link` or `ghost` button, over the variant's own.\n *\n * `muted` is the QUIET link keksdose hand-rolls in six places (tours-page:321's \"Mark\n * undone\", transaction-fields:176/215's \"Add line\" / \"Fill total\",\n * invoice-lines-table:376, invoice-review:464): secondary text that darkens to the\n * body colour under the pointer, for an action that must be findable but not compete\n * with the brand-coloured one beside it. `danger` is the same quiet look turning\n * `--danger` on hover — transaction-editor:401's \"Remove split\", which is a text\n * action that destroys something.\n *\n * A `tone` rather than a `link-muted` variant, because it is the axis IconButton\n * already has (`tone=\"muted\"`, `tone=\"danger\"`, same quiet-until-hover meaning), and\n * because the same two looks are wanted on `ghost`.\n *\n * `danger` also reaches the two NEUTRAL boxed variants, where the box carries it:\n * `secondary` becomes an outlined destructive button (`--danger` text and a\n * `--danger-border` outline, the quiet danger fill on hover) and `default` a soft one\n * (the quiet fill at rest, solid `--danger` on hover). kastlan's meeting-invitations-\n * tab.tsx \"Remove all\" is `variant=\"secondary\" className=\"text-destructive\"` — a\n * destructive action that must sit in a row of secondary buttons without shouting\n * like the solid `variant=\"danger\"` would, and whose red text alone left the border\n * and hover saying \"neutral\". `muted` stays a text look and does nothing on a box, and\n * `danger`, `accent` and `link`'s own colours are left alone: a tone that recoloured a\n * solid danger or accent button would be a second variant under another name.\n */\nexport type ButtonTone = \"default\" | \"muted\" | \"danger\";\n\n// `contrast:text-secondary` restates the tone's own colour under the `contrast:` variant,\n// so tailwind-merge drops the link's `contrast:text-brand-muted` (0.36): a quiet link\n// stays quiet under More contrast. On `ghost` it reads the same either way.\nconst BUTTON_TONES: Record<Exclude<ButtonTone, \"default\">, string> = {\n muted: \"text-secondary hover:text-primary disabled:hover:text-secondary contrast:text-secondary\",\n danger:\n \"text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary contrast:text-secondary\",\n};\n\n/** Only the two transparent variants take every tone; see {@link ButtonTone}. */\nconst TONED_VARIANTS = new Set<ButtonVariant>([\"link\", \"ghost\"]);\n\n// `tone=\"danger\"` on the neutral boxed variants — see {@link ButtonTone}. Each string\n// restates every colour the variant sets (border, fill, text, hover, ring) so none of\n// the neutral ones survives the merge.\nconst BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {\n // Not boxed, but its hover is a fill: the danger wash under the danger text, as\n // IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1\n // over a light page.\n ghost: \"hover:bg-danger-soft\",\n secondary: \"border-danger bg-transparent text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n default:\n \"border-danger bg-danger-soft text-danger hover:border-[var(--danger)] hover:bg-danger hover:text-danger-contrast focus-visible:ring-danger-strong disabled:hover:border-danger disabled:hover:bg-danger-soft disabled:hover:text-danger\",\n};\n\n// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what\n// lenkbank's \"All speeds\" toggle paints by hand (gear/hysteresis-charts.tsx:482). After\n// the tone, so a pressed muted link is brand, and an unpressed one is quiet grey. Under\n// More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36, as the\n// `link` variant), whatever the tone.\nexport const BUTTON_LINK_PRESSED =\n \"font-medium text-brand hover:text-brand disabled:hover:text-brand contrast:text-brand-muted contrast:hover:text-brand-muted contrast:disabled:hover:text-brand-muted\";\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"default\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n buttonVariantClasses[variant],\n className,\n );\n}\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey. On hover\n// the glyph turns brand-muted: `--brand` on `--brand-bg-hover` is 2.69–2.80 in four dark\n// presets, under an icon's 3:1 (colour-roles §12.12). Button's `pressed` draws it on its\n// boxed variants too, which is why it lives here and not in ./icon-button.\nexport const ICON_BUTTON_PRESSED =\n \"bg-brand-soft text-brand hover:bg-brand-soft-hover hover:text-brand-muted disabled:hover:bg-brand-soft disabled:hover:text-brand\";\n\n/** @internal The button look shared by the `<button>` and the `<a>` form. */\nexport function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the\n // order `buttonClasses` has it, so the two strings stay identical.\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n tone !== \"default\" && TONED_VARIANTS.has(variant) && BUTTON_TONES[tone],\n tone === \"danger\" && BUTTON_BOXED_DANGER[variant],\n );\n}\n\n/** @internal Calls a `renderLink` / provider link as a function, as ListItem and Chip\n * do — an inline `renderLink` arrow would otherwise be a new component type every\n * render. Both buttons' link forms render through it. */\nexport function RenderedButtonLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {\n return render(props);\n}\n"],"mappings":";AAAA,SAAS,UAAU;AACnB,SAAS,wBAAwB;AA2CjC,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AACF;AASO,MAAM,qBAAqB;AAK3B,MAAM,iBACX;AAgBF,MAAM,eAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,qBAAiD;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AACN;AAYO,MAAM,YAAY;AAQlB,MAAM,uBAAsD;AAAA,EACjE;AAAA;AAAA;AAAA,IAGE;AAAA;AAAA,EACF,WAAW,oDAAoD,SAAS;AAAA,EACxE,OAAO,+BAA+B,SAAS;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,MAAM;AACR;AAiCA,MAAM,eAA+D;AAAA,EACnE,OAAO;AAAA,EACP,QACE;AACJ;AAGA,MAAM,iBAAiB,oBAAI,IAAmB,CAAC,QAAQ,OAAO,CAAC;AAK/D,MAAM,sBAA8D;AAAA;AAAA;AAAA;AAAA,EAIlE,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SACE;AACJ;AAOO,MAAM,sBACX;AAoBK,SAAS,cACd,UAAyB,WACzB,oBACQ;AACR,QAAM,EAAE,OAAO,MAAM,UAAU,IAC7B,OAAO,uBAAuB,WAAW,qBAAqB,EAAE,WAAW,mBAAmB;AAChG,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA,IACjB,YAAY,UAAU,mBAAmB,IAAI;AAAA,IAC7C,qBAAqB,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAQO,MAAM,sBACX;AAGK,SAAS,WAAW,SAAwB,MAAkB,SAA8B,MAAkB;AACnH,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA;AAAA;AAAA,IAGjB,YAAY,UAAU,mBAAmB,IAAI;AAAA;AAAA;AAAA;AAAA,IAI7C,WAAW;AAAA,IACX,qBAAqB,OAAO;AAAA,IAC5B,SAAS,aAAa,eAAe,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,IACtE,SAAS,YAAY,oBAAoB,OAAO;AAAA,EAClD;AACF;AAKO,SAAS,mBAAmB,EAAE,QAAQ,GAAG,MAAM,GAAgD;AACpG,SAAO,OAAO,KAAK;AACrB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/button-look.ts"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport type { KitLinkComponent, KitLinkProps } from \"../i18n/kit-labels\";\n\n/**\n * The button look, which {@link Button} and {@link IconButton} share (0.34, MOD-2, refactor\n * audit 2026-10). In ui.tsx it was the one knot in an otherwise one-way graph: Button drew\n * IconButton's pressed look, and IconButton drew Button's variant colours, its hover ink\n * and its router link. Both now draw from this module, which renders nothing, so neither\n * imports the other.\n *\n * Public through the barrel (src/index.ts) by name: `buttonClasses`, `TOUCH_TARGET_LARGE`\n * and the four types. The other exports are the two buttons' shared parts (`@internal`).\n */\n\n/**\n * A button's look.\n *\n * - `default`: the quiet action — a warm filled chip with a hairline border. What a\n * Button with no `variant` draws.\n * - `accent`: the strong one, solid `--brand` with `--brand-contrast` text — the one\n * call to action a view leads with. \"Accent\" is the button's role here, painted in\n * `--brand`; it is not the palette's second anchor colour, also called `accent`\n * (theme/palette-derive.ts), which no button paints.\n * - `secondary`: an outline. `ghost`: text or glyph only. `danger`: solid `--danger`,\n * for an action that destroys something. `link`: a text link that acts (a `<button>`).\n *\n * 0.49's `primary` and `brand` (aliases of `default` and `accent`) were removed in 0.51;\n * `primary` will not come back with another meaning.\n */\nexport type ButtonVariant = \"default\" | \"accent\" | \"secondary\" | \"ghost\" | \"danger\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\n//\n// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus\n// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each\n// variant's own colour below.\n//\n// `[&_svg]:shrink-0` (0.32.2, kastlan's 0.32.1 report): an icon beside a label keeps its\n// size when the label wraps at Large. Without it the svg was a flex item like the text,\n// and a two-line label squeezed it to a sliver (\"New invoice\" in a page header at 360 px\n// / Extra large).\nconst BUTTON_BASE = cn(\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:shrink-0\",\n FOCUS_RING_WIDTH,\n);\n\n/**\n * The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at\n * least 48 px, the size a finger needs whatever the type. In px on purpose, like the\n * field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that\n * follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs\n * no render and is right on the first paint.\n */\nexport const TOUCH_TARGET_LARGE = \"large:min-h-[48px] large:min-w-[48px]\";\n\n/** The 48 px target at Large for a control whose BOX cannot grow (a chip's or a tab's\n * action, IconButton `2xs`; Tabs' overflow cues, 0.48 KS N3): an invisible `::after`\n * centred on it takes the taps a finger lands round the glyph. Not public. */\nexport const HIT_AREA_LARGE =\n \"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\n//\n// At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its\n// 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The\n// type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which\n// is a word in a sentence and has no box to grow (see `buttonLook`).\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\nconst BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {\n md: \"large:min-h-[48px]\",\n sm: \"large:min-h-[48px] large:py-2.5\",\n};\n\n/**\n * The hover of a control that may sit on the page, a card or a well (0.33, B′ —\n * docs/colour-roles-harmonization.md §12.1, §12.10): the body ink at 7 %, TRANSLUCENT,\n * so it darkens (in dark, lightens) whatever is under it by the same step — 1.11:1 or\n * more on all three surfaces, both themes, with the label at 7:1 and up. It was\n * `--bg-surface-2`, which since 0.33 is the page itself in light: a ghost button on the\n * page would not have moved at all. Not `--bg-hover` either, which is mixed against the\n * CARD and stands only 1.04:1 from the page. IconButton's `custom` tone uses the same\n * recipe at 12 %.\n */\nexport const HOVER_INK = \"hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\";\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (`default` = filled warm chip, secondary =\n// outline); `accent` is the solid brand fill for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nexport const buttonVariantClasses: Record<ButtonVariant, string> = {\n default:\n // Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the\n // border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.\n \"border border-subtle bg-surface-2 text-primary hover:bg-active focus-visible:ring-brand\",\n secondary: `border border-subtle bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n ghost: `bg-transparent text-primary ${HOVER_INK} focus-visible:ring-brand`,\n danger: \"bg-danger text-danger-contrast hover:bg-danger-hover focus-visible:ring-danger-strong\",\n accent: \"bg-brand text-brand-contrast hover:bg-brand-hover focus-visible:ring-brand\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's focus ring stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n //\n // Under More contrast the word reads `--brand-muted`, the brand AS TEXT (0.36,\n // colour-roles §13.3 decision 8): `--brand` is solved as a mark (3:1), and on More\n // contrast's darker light well Moss's read 3.68:1; the derived dark brands sit at 3.0\n // as text too. Both modes, since `contrast:` does not split them.\n link: \"rounded-sm p-0 bg-transparent text-brand underline-offset-4 hover:underline focus-visible:ring-brand contrast:text-brand-muted\",\n};\n\n/**\n * The text colour of a `link` or `ghost` button, over the variant's own.\n *\n * `muted` is the QUIET link keksdose hand-rolls in six places (tours-page:321's \"Mark\n * undone\", transaction-fields:176/215's \"Add line\" / \"Fill total\",\n * invoice-lines-table:376, invoice-review:464): secondary text that darkens to the\n * body colour under the pointer, for an action that must be findable but not compete\n * with the brand-coloured one beside it. `danger` is the same quiet look turning\n * `--danger` on hover — transaction-editor:401's \"Remove split\", which is a text\n * action that destroys something.\n *\n * A `tone` rather than a `link-muted` variant, because it is the axis IconButton\n * already has (`tone=\"muted\"`, `tone=\"danger\"`, same quiet-until-hover meaning), and\n * because the same two looks are wanted on `ghost`.\n *\n * `danger` also reaches the two NEUTRAL boxed variants, where the box carries it:\n * `secondary` becomes an outlined destructive button (`--danger` text and a\n * `--danger-border` outline, the quiet danger fill on hover) and `default` a soft one\n * (the quiet fill at rest, solid `--danger` on hover). kastlan's meeting-invitations-\n * tab.tsx \"Remove all\" is `variant=\"secondary\" className=\"text-destructive\"` — a\n * destructive action that must sit in a row of secondary buttons without shouting\n * like the solid `variant=\"danger\"` would, and whose red text alone left the border\n * and hover saying \"neutral\". `muted` stays a text look and does nothing on a box, and\n * `danger`, `accent` and `link`'s own colours are left alone: a tone that recoloured a\n * solid danger or accent button would be a second variant under another name.\n */\nexport type ButtonTone = \"default\" | \"muted\" | \"danger\";\n\n// `contrast:text-secondary` restates the tone's own colour under the `contrast:` variant,\n// so tailwind-merge drops the link's `contrast:text-brand-muted` (0.36): a quiet link\n// stays quiet under More contrast. On `ghost` it reads the same either way.\nconst BUTTON_TONES: Record<Exclude<ButtonTone, \"default\">, string> = {\n muted: \"text-secondary hover:text-primary disabled:hover:text-secondary contrast:text-secondary\",\n danger:\n \"text-secondary hover:text-danger focus-visible:ring-danger-strong disabled:hover:text-secondary contrast:text-secondary\",\n};\n\n/** Only the two transparent variants take every tone; see {@link ButtonTone}. */\nconst TONED_VARIANTS = new Set<ButtonVariant>([\"link\", \"ghost\"]);\n\n// `tone=\"danger\"` on the neutral boxed variants — see {@link ButtonTone}. Each string\n// restates every colour the variant sets (border, fill, text, hover, ring) so none of\n// the neutral ones survives the merge.\nconst BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {\n // Not boxed, but its hover is a fill: the danger wash under the danger text, as\n // IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1\n // over a light page.\n ghost: \"hover:bg-danger-soft\",\n secondary: \"border-danger bg-transparent text-danger hover:bg-danger-soft focus-visible:ring-danger-strong\",\n default:\n \"border-danger bg-danger-soft text-danger hover:border-[var(--danger)] hover:bg-danger hover:text-danger-contrast focus-visible:ring-danger-strong disabled:hover:border-danger disabled:hover:bg-danger-soft disabled:hover:text-danger\",\n};\n\n// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what\n// lenkbank's \"All speeds\" toggle paints by hand (gear/hysteresis-charts.tsx:482). After\n// the tone, so a pressed muted link is brand, and an unpressed one is quiet grey. Under\n// More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36, as the\n// `link` variant), whatever the tone.\nexport const BUTTON_LINK_PRESSED =\n \"font-medium text-brand hover:text-brand disabled:hover:text-brand contrast:text-brand-muted contrast:hover:text-brand-muted contrast:disabled:hover:text-brand-muted\";\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"default\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n buttonVariantClasses[variant],\n className,\n );\n}\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey. On hover\n// the glyph turns brand-muted: `--brand` on `--brand-bg-hover` is 2.69–2.80 in four dark\n// presets, under an icon's 3:1 (colour-roles §12.12). Button's `pressed` draws it on its\n// boxed variants too, which is why it lives here and not in ./icon-button.\nexport const ICON_BUTTON_PRESSED =\n \"bg-brand-soft text-brand hover:bg-brand-soft-hover hover:text-brand-muted disabled:hover:bg-brand-soft disabled:hover:text-brand\";\n\n/** @internal The button look shared by the `<button>` and the `<a>` form. */\nexport function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {\n return cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the\n // order `buttonClasses` has it, so the two strings stay identical.\n variant !== \"link\" && BUTTON_SIZES_LARGE[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n tone !== \"default\" && TONED_VARIANTS.has(variant) && BUTTON_TONES[tone],\n tone === \"danger\" && BUTTON_BOXED_DANGER[variant],\n );\n}\n\n/** @internal Calls a `renderLink` / provider link as a function, as ListItem and Chip\n * do — an inline `renderLink` arrow would otherwise be a new component type every\n * render. Both buttons' link forms render through it. */\nexport function RenderedButtonLink({ render, ...props }: KitLinkProps & { render: KitLinkComponent }) {\n return render(props);\n}\n"],"mappings":";AAAA,SAAS,UAAU;AACnB,SAAS,wBAAwB;AA2CjC,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AACF;AASO,MAAM,qBAAqB;AAK3B,MAAM,iBACX;AAgBF,MAAM,eAA2C;AAAA,EAC/C,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,qBAAiD;AAAA,EACrD,IAAI;AAAA,EACJ,IAAI;AACN;AAYO,MAAM,YAAY;AAQlB,MAAM,uBAAsD;AAAA,EACjE;AAAA;AAAA;AAAA,IAGE;AAAA;AAAA,EACF,WAAW,oDAAoD,SAAS;AAAA,EACxE,OAAO,+BAA+B,SAAS;AAAA,EAC/C,QAAQ;AAAA,EACR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaR,MAAM;AACR;AAiCA,MAAM,eAA+D;AAAA,EACnE,OAAO;AAAA,EACP,QACE;AACJ;AAGA,MAAM,iBAAiB,oBAAI,IAAmB,CAAC,QAAQ,OAAO,CAAC;AAK/D,MAAM,sBAA8D;AAAA;AAAA;AAAA;AAAA,EAIlE,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SACE;AACJ;AAOO,MAAM,sBACX;AAoBK,SAAS,cACd,UAAyB,WACzB,oBACQ;AACR,QAAM,EAAE,OAAO,MAAM,UAAU,IAC7B,OAAO,uBAAuB,WAAW,qBAAqB,EAAE,WAAW,mBAAmB;AAChG,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA,IACjB,YAAY,UAAU,mBAAmB,IAAI;AAAA,IAC7C,qBAAqB,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAQO,MAAM,sBACX;AAGK,SAAS,WAAW,SAAwB,MAAkB,SAA8B,MAAkB;AACnH,SAAO;AAAA,IACL;AAAA,IACA,aAAa,IAAI;AAAA;AAAA;AAAA,IAGjB,YAAY,UAAU,mBAAmB,IAAI;AAAA;AAAA;AAAA;AAAA,IAI7C,WAAW;AAAA,IACX,qBAAqB,OAAO;AAAA,IAC5B,SAAS,aAAa,eAAe,IAAI,OAAO,KAAK,aAAa,IAAI;AAAA,IACtE,SAAS,YAAY,oBAAoB,OAAO;AAAA,EAClD;AACF;AAKO,SAAS,mBAAmB,EAAE,QAAQ,GAAG,MAAM,GAAgD;AACpG,SAAO,OAAO,KAAK;AACrB;","names":[]}
|
|
@@ -12,8 +12,8 @@ import type { ButtonSize, ButtonTone, ButtonVariant } from "./button-look.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
14
14
|
/** `default` (the quiet action, what a Button without `variant` draws), `accent` (the
|
|
15
|
-
* strong call to action, solid brand), `secondary`, `ghost`, `danger` or `link`.
|
|
16
|
-
*
|
|
15
|
+
* strong call to action, solid brand), `secondary`, `ghost`, `danger` or `link`. See
|
|
16
|
+
* {@link ButtonVariant}. */
|
|
17
17
|
variant?: ButtonVariant;
|
|
18
18
|
/** `md` (default) is the page's action button. `sm` is the compact one — 12px text
|
|
19
19
|
* and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a
|
|
@@ -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`.\n * `primary` / `brand` are deprecated aliases of `default` / `accent`. See {@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":";AA+KuC,SAwCnC,UAxCmC,KA8CtB,YA9CsB;AA/KvC,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,kBAAkB;AAEhE,SAAS,kBAAkB,gBAAgB,4BAA4B;AACvE,SAAS,uBAAuB;AAEhC,SAAS,kBAAkB;AAC3B,SAAS,oBAAgD;AACzD,SAAS,eAAe;AACxB,SAAS,qBAAqB,qBAAqB,oBAAoB,kBAAkB;AAmKlF,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,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;AACd,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,gBAAgB,QAAQ,iBAAiB;AAChE,QAAM,SAAS,WAAW,cAAc;AACxC,QAAM,QAAQ,UAAU,QAAQ,OAAO;AACvC,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,UAAU;AAAA;AAAA;AAAA,QAGV,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":[]}
|
|
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":[]}
|
|
@@ -21,6 +21,24 @@ interface CalculatorButtonProps {
|
|
|
21
21
|
* the one argument and ignore this.
|
|
22
22
|
*/
|
|
23
23
|
onChange: (value: string, expression?: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Called ONCE per calculator session, with the result the host should keep: on "="
|
|
26
|
+
* (the key, or Enter in the display), and when the panel closes — Escape, a press
|
|
27
|
+
* outside, the trigger — on a result `onChange` reported after the last commit. Never
|
|
28
|
+
* per keystroke: `onChange` is the live channel, this is the one to save on.
|
|
29
|
+
*
|
|
30
|
+
* Not called when the button goes away with its panel open (a lock or `disabled`
|
|
31
|
+
* arriving, the field unmounting): those say the value must not move, and the result
|
|
32
|
+
* stays where `onChange` put it.
|
|
33
|
+
*
|
|
34
|
+
* Refactor audit 2026-10-30, INP-1: the pad had no commit point of its own, and the
|
|
35
|
+
* two field engines guessed in opposite directions. NumberInput never committed a
|
|
36
|
+
* result, so Kurvenschmiede's dialogs saved the number from before the calculation;
|
|
37
|
+
* AmountInput committed every keystroke that resolved, so "100+50" saved 105 on its way
|
|
38
|
+
* to 150, and keksdose's threshold field, `disabled` while it saves, lost its pad to the
|
|
39
|
+
* first of them.
|
|
40
|
+
*/
|
|
41
|
+
onCommit?: (value: string) => void;
|
|
24
42
|
className?: string;
|
|
25
43
|
/** The trigger's accessible name; wins over `calculator.open` from the provider. */
|
|
26
44
|
"aria-label"?: string;
|
|
@@ -33,7 +51,8 @@ interface CalculatorButtonProps {
|
|
|
33
51
|
* A calculator icon that opens a small keypad popover anchored to its host
|
|
34
52
|
* field. Designed as a trailing-edge adornment: render it inside the field's
|
|
35
53
|
* `relative` wrapper, positioned absolutely. The keypad seeds from the current
|
|
36
|
-
* field value and writes evaluated results straight back through `onChange
|
|
54
|
+
* field value and writes evaluated results straight back through `onChange`, and
|
|
55
|
+
* commits the session once through `onCommit`.
|
|
37
56
|
*/
|
|
38
|
-
export declare function CalculatorButton({ value, onChange, className, "aria-label": ariaLabel, labels: labelsProp, }: CalculatorButtonProps): import("react").JSX.Element;
|
|
57
|
+
export declare function CalculatorButton({ value, onChange, onCommit, className, "aria-label": ariaLabel, labels: labelsProp, }: CalculatorButtonProps): import("react").JSX.Element;
|
|
39
58
|
export {};
|