@eifi1/ui-kit 0.31.1 → 0.32.1
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 +51 -28
- package/dist/account/delete-account-setting.d.ts +8 -3
- package/dist/admin/admin-action-confirm.d.ts +8 -3
- package/dist/admin/admin-action-log.d.ts +1 -1
- package/dist/admin/admin-action-log.js +4 -2
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +8 -3
- package/dist/admin/invitations-panel.js +29 -26
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +8 -3
- package/dist/admin/role-select.d.ts +10 -5
- package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
- package/dist/admin/user-roster.d.ts +10 -5
- package/dist/admin/user-roster.js +8 -47
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +43 -8
- package/dist/auth/auth-errors.js +24 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/register-form.d.ts +8 -3
- package/dist/billing/billing-banner.d.ts +184 -0
- package/dist/billing/billing-banner.js +98 -0
- package/dist/billing/billing-banner.js.map +1 -0
- package/dist/billing/billing-labels.d.ts +100 -0
- package/dist/billing/billing-labels.js +76 -0
- package/dist/billing/billing-labels.js.map +1 -0
- package/dist/billing/billing-lock.d.ts +158 -0
- package/dist/billing/billing-lock.js +17 -0
- package/dist/billing/billing-lock.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +149 -0
- package/dist/billing/billing-standing.js +28 -0
- package/dist/billing/billing-standing.js.map +1 -0
- package/dist/billing/plan-card.d.ts +248 -0
- package/dist/billing/plan-card.js +248 -0
- package/dist/billing/plan-card.js.map +1 -0
- package/dist/billing/plan-limit-notice.d.ts +156 -0
- package/dist/billing/plan-limit-notice.js +76 -0
- package/dist/billing/plan-limit-notice.js.map +1 -0
- package/dist/billing/plan-limit.d.ts +34 -0
- package/dist/billing/plan-limit.js +20 -0
- package/dist/billing/plan-limit.js.map +1 -0
- package/dist/billing/plan-price.d.ts +61 -0
- package/dist/billing/plan-price.js +45 -0
- package/dist/billing/plan-price.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +133 -0
- package/dist/billing/subscription-actions.js +28 -0
- package/dist/billing/subscription-actions.js.map +1 -0
- package/dist/billing/subscription-status-chip.d.ts +149 -0
- package/dist/billing/subscription-status-chip.js +48 -0
- package/dist/billing/subscription-status-chip.js.map +1 -0
- package/dist/chart.d.ts +9 -3
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +16 -4
- package/dist/components/account-chips.js +21 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +6 -1
- package/dist/components/alert-banner.js +5 -4
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +8 -3
- package/dist/components/amount-input.js +5 -5
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/appearance-labels.d.ts +38 -0
- package/dist/components/appearance-labels.js +19 -0
- package/dist/components/appearance-labels.js.map +1 -0
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/breadcrumbs.js +3 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +2 -2
- package/dist/components/bulk-action-bar.js +7 -12
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +8 -3
- package/dist/components/button-group.js +7 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +8 -3
- package/dist/components/calculator.js +4 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +5 -5
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-height.d.ts +23 -0
- package/dist/components/chart-height.js +15 -0
- package/dist/components/chart-height.js.map +1 -0
- package/dist/components/chart-zoom.js +6 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/checkbox-group.js +2 -2
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +10 -0
- package/dist/components/checkbox.js +25 -10
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +19 -3
- package/dist/components/chip.js +54 -23
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +1 -1
- package/dist/components/choice-card.js +9 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper.js +1 -1
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +8 -7
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +8 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +8 -3
- package/dist/components/copy-button.d.ts +10 -5
- package/dist/components/country-select.js +1 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +8 -3
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +4 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-labels.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -2
- package/dist/components/data-table-pagination.js +15 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +123 -52
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +1 -1
- package/dist/components/date-picker.js +18 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.js +2 -2
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +2 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +3 -2
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -1
- package/dist/components/dropdown.js +7 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -6
- package/dist/components/facing-pair.js +3 -2
- package/dist/components/facing-pair.js.map +1 -1
- package/dist/components/field-parts.d.ts +102 -6
- package/dist/components/field-parts.js +81 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.d.ts +4 -1
- package/dist/components/field-sync.js +12 -2
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.js +2 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +8 -3
- package/dist/components/file-dropzone.d.ts +8 -3
- package/dist/components/floating-panel.js +7 -4
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.d.ts +12 -0
- package/dist/components/focus-ring.js +8 -0
- package/dist/components/focus-ring.js.map +1 -0
- package/dist/components/form-actions.d.ts +8 -3
- package/dist/components/form-actions.js +33 -19
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +11 -1
- package/dist/components/full-bleed-dialog.js +3 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/iban-input.d.ts +8 -3
- package/dist/components/image-grid.js +1 -1
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js +10 -4
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +8 -3
- package/dist/components/legal-page.d.ts +26 -5
- package/dist/components/legal-page.js +3 -1
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +8 -3
- package/dist/components/legal.js +2 -1
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js +24 -2
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +2 -1
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +5 -0
- package/dist/components/list.js +13 -9
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +3 -1
- package/dist/components/measured-grid.js +38 -20
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js +2 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +8 -4
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.d.ts +8 -3
- package/dist/components/month-picker.js +50 -39
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/nav-pills.js +1 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +8 -3
- package/dist/components/number-input.d.ts +11 -4
- package/dist/components/number-input.js +8 -6
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +8 -3
- package/dist/components/numpad-sheet.js +2 -1
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +2 -2
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +2 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/phone-input.d.ts +8 -3
- package/dist/components/phone-input.js +4 -4
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/pie-chart.d.ts +8 -2
- package/dist/components/pie-chart.js +11 -7
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +9 -1
- package/dist/components/popover.js +8 -4
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +100 -0
- package/dist/components/row-actions.js +231 -0
- package/dist/components/row-actions.js.map +1 -0
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/search-field.js +3 -2
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart-budget.d.ts +30 -7
- package/dist/components/series-chart-budget.js +12 -4
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +7 -1
- package/dist/components/series-chart-ticks.js +6 -0
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +58 -28
- package/dist/components/series-chart.js +42 -32
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +57 -8
- package/dist/components/settings-fields.js +39 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +11 -6
- package/dist/components/share-card.js +8 -2
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.d.ts +4 -0
- package/dist/components/sign-chip.js +17 -4
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.d.ts +3 -1
- package/dist/components/signature-pad.js +8 -5
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/slider.js +7 -6
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.d.ts +5 -1
- package/dist/components/sparkline.js +4 -2
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.d.ts +17 -1
- package/dist/components/stat-tile.js +48 -20
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -0
- package/dist/components/status-dot.js +9 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swipeable-row.js +10 -2
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +21 -7
- package/dist/components/switch.js +24 -11
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +8 -3
- package/dist/components/text-link.js +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +7 -2
- package/dist/components/text.js +5 -5
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +2 -1
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +8 -3
- package/dist/components/toast.d.ts +18 -2
- package/dist/components/toast.js +8 -5
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.d.ts +25 -3
- package/dist/components/toggle-group.js +40 -11
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +10 -1
- package/dist/components/toggle-legend.js +21 -7
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -1
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +4 -0
- package/dist/components/translation-review.d.ts +4 -0
- package/dist/components/translation-review.js +3 -3
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +3 -2
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +33 -16
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +8 -3
- package/dist/components/ui.js +207 -119
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +5 -1
- package/dist/components/user-avatar.js +1 -1
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.js +20 -14
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
- package/dist/demo/demo-banner.js +1 -0
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +8 -3
- package/dist/feedback/feedback-context.d.ts +8 -3
- package/dist/feedback/feedback-crash.d.ts +8 -3
- package/dist/feedback/feedback-dialog.d.ts +8 -3
- package/dist/feedback/feedback-dialog.js +3 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +8 -3
- package/dist/feedback/feedback-inbox.js +2 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +8 -3
- package/dist/feedback/feedback-menu.d.ts +9 -4
- package/dist/feedback/feedback-record.d.ts +8 -3
- package/dist/feedback/feedback-row-detail.d.ts +8 -3
- package/dist/feedback/feedback-status-undo.d.ts +8 -3
- package/dist/feedback/feedback-submit.d.ts +8 -3
- package/dist/feedback/feedback-swipe.d.ts +8 -3
- package/dist/feedback/feedback-table.d.ts +8 -3
- package/dist/feedback/feedback-thread.d.ts +8 -3
- package/dist/feedback/feedback-thread.js +3 -3
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +8 -3
- package/dist/hooks/use-anchored-panel.d.ts +32 -3
- package/dist/hooks/use-anchored-panel.js +21 -3
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-breakpoint.d.ts +88 -0
- package/dist/hooks/use-breakpoint.js +50 -0
- package/dist/hooks/use-breakpoint.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +8 -3
- package/dist/hooks/use-large-text.d.ts +29 -0
- package/dist/hooks/use-large-text.js +20 -0
- package/dist/hooks/use-large-text.js.map +1 -0
- package/dist/hooks/use-windowed-rows.d.ts +52 -8
- package/dist/hooks/use-windowed-rows.js +94 -5
- package/dist/hooks/use-windowed-rows.js.map +1 -1
- package/dist/i18n/defaults.d.ts +8 -3
- package/dist/i18n/defaults.js +8 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +8 -3
- package/dist/i18n/german.js +94 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -3
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -5
- package/dist/i18n/locales/de-CH.d.ts +8 -3
- package/dist/i18n/locales/en.d.ts +8 -3
- package/dist/i18n/locales/es.d.ts +8 -3
- package/dist/i18n/locales/es.js +89 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +8 -3
- package/dist/i18n/locales/fr.js +91 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +8 -3
- package/dist/i18n/locales/hu.js +91 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +8 -3
- package/dist/i18n/locales/it.js +92 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +8 -3
- package/dist/i18n/locales/zh.js +89 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +8 -3
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -11
- package/dist/index.js +77 -4
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +8 -3
- package/dist/landing/landing-sections.d.ts +8 -3
- package/dist/landing/landing-sections.js +7 -3
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/public-header.d.ts +9 -4
- package/dist/landing/public-header.js +3 -3
- package/dist/landing/public-header.js.map +1 -1
- package/dist/landing/routing.d.ts +8 -3
- package/dist/lib/cn.js +3 -2
- package/dist/lib/cn.js.map +1 -1
- package/dist/rhf/fields.d.ts +8 -3
- package/dist/rhf/fields.js +1 -1
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +8 -3
- package/dist/rhf/form.js +2 -2
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +8 -3
- package/dist/search/command-palette.d.ts +1 -1
- package/dist/search/command-palette.js +9 -8
- package/dist/search/command-palette.js.map +1 -1
- package/dist/settings/settings-layout.js +2 -1
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/settings-section.d.ts +8 -3
- package/dist/settings/use-account-appearance.d.ts +92 -0
- package/dist/settings/use-account-appearance.js +63 -0
- package/dist/settings/use-account-appearance.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +1 -1
- package/dist/settings/use-settings-route.js +2 -3
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.d.ts +12 -171
- package/dist/shell/app-shell.js +144 -28
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -1
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/option-switcher-menu.js +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +8 -3
- package/dist/shell/top-bar-brand.js +3 -1
- package/dist/shell/top-bar-brand.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +4 -0
- package/dist/shell/topbar-action-menu.js +3 -3
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +4 -4
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell.d.ts +9 -5
- package/dist/theme/contrast-tokens.d.ts +41 -0
- package/dist/theme/contrast-tokens.js +59 -0
- package/dist/theme/contrast-tokens.js.map +1 -0
- package/dist/theme/contrast.d.ts +110 -0
- package/dist/theme/contrast.js +128 -0
- package/dist/theme/contrast.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +12 -1
- package/dist/theme/palette-presets.js +9 -4
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.d.ts +173 -0
- package/dist/theme/text-size.js +121 -0
- package/dist/theme/text-size.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +8 -3
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +8 -3
- package/package.json +1 -1
- package/src/admin/admin-action-log.tsx +6 -1
- package/src/admin/invitations-panel.tsx +36 -29
- package/src/admin/user-roster.tsx +17 -72
- package/src/auth/auth-errors.ts +68 -7
- package/src/billing/billing-banner.tsx +205 -0
- package/src/billing/billing-labels.ts +240 -0
- package/src/billing/billing-lock.ts +75 -0
- package/src/billing/billing-standing.ts +72 -0
- package/src/billing/plan-card.tsx +458 -0
- package/src/billing/plan-limit-notice.tsx +145 -0
- package/src/billing/plan-limit.ts +53 -0
- package/src/billing/plan-price.ts +101 -0
- package/src/billing/subscription-actions.tsx +76 -0
- package/src/billing/subscription-status-chip.tsx +95 -0
- package/src/chart.ts +2 -0
- package/src/components/account-chips.tsx +36 -10
- package/src/components/alert-banner.tsx +16 -7
- package/src/components/amount-input.tsx +5 -5
- package/src/components/appearance-labels.ts +47 -0
- package/src/components/autocomplete.tsx +2 -2
- package/src/components/breadcrumbs.tsx +3 -3
- package/src/components/bulk-action-bar.tsx +11 -14
- package/src/components/button-group.tsx +21 -0
- package/src/components/calculator.tsx +4 -3
- package/src/components/calendar-heatmap.tsx +5 -5
- package/src/components/chart-height.ts +30 -0
- package/src/components/chart-zoom.tsx +6 -1
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +62 -20
- package/src/components/chip.tsx +139 -30
- package/src/components/choice-card.tsx +16 -11
- package/src/components/column-mapper.tsx +1 -1
- package/src/components/combobox-core.tsx +8 -7
- package/src/components/combobox.tsx +8 -8
- package/src/components/country-select.tsx +1 -1
- package/src/components/currency-select.tsx +3 -3
- package/src/components/data-table-filter-popover.tsx +7 -1
- package/src/components/data-table-pagination.tsx +60 -31
- package/src/components/data-table.tsx +164 -43
- package/src/components/date-picker.tsx +44 -10
- package/src/components/description-list.tsx +2 -2
- package/src/components/dialog-frame.tsx +2 -1
- package/src/components/disclosure.tsx +3 -2
- package/src/components/dropdown.tsx +10 -2
- package/src/components/facing-pair.ts +12 -5
- package/src/components/field-parts.tsx +190 -7
- package/src/components/field-sync.tsx +17 -3
- package/src/components/field.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -4
- package/src/components/focus-ring.ts +12 -0
- package/src/components/form-actions.tsx +55 -18
- package/src/components/full-bleed-dialog.tsx +18 -4
- package/src/components/image-grid.tsx +1 -1
- package/src/components/inline-edit-field.tsx +26 -4
- package/src/components/legal-page.tsx +28 -2
- package/src/components/legal.tsx +19 -1
- package/src/components/lightbox.tsx +24 -2
- package/src/components/line-items.tsx +7 -1
- package/src/components/list.tsx +36 -10
- package/src/components/measured-grid.tsx +49 -24
- package/src/components/menu-item.tsx +2 -2
- package/src/components/mini-calendar.tsx +8 -4
- package/src/components/month-picker.tsx +15 -6
- package/src/components/nav-pills.tsx +1 -1
- package/src/components/number-input.tsx +14 -7
- package/src/components/numpad-sheet.tsx +2 -2
- package/src/components/one-time-code-input.tsx +2 -2
- package/src/components/page-contents.tsx +2 -2
- package/src/components/phone-input.tsx +9 -4
- package/src/components/pie-chart.tsx +19 -9
- package/src/components/popover.tsx +22 -5
- package/src/components/row-actions.tsx +450 -0
- package/src/components/scroll-area.tsx +1 -1
- package/src/components/search-field.tsx +4 -2
- package/src/components/series-chart-budget.ts +40 -8
- package/src/components/series-chart-ticks.ts +10 -0
- package/src/components/series-chart.tsx +123 -72
- package/src/components/settings-fields.tsx +89 -5
- package/src/components/share-card.tsx +31 -2
- package/src/components/sign-chip.tsx +28 -4
- package/src/components/signature-pad.tsx +13 -7
- package/src/components/slider.tsx +22 -9
- package/src/components/sparkline.tsx +11 -3
- package/src/components/stat-tile.tsx +70 -14
- package/src/components/status-dot.tsx +25 -2
- package/src/components/swipeable-row.tsx +10 -2
- package/src/components/switch.tsx +57 -23
- package/src/components/table.tsx +2 -2
- package/src/components/text-link.tsx +1 -1
- package/src/components/text.tsx +11 -6
- package/src/components/tile-radio.tsx +4 -1
- package/src/components/toast.tsx +27 -7
- package/src/components/toggle-group.tsx +115 -14
- package/src/components/toggle-legend.tsx +53 -8
- package/src/components/translation-review-editor.tsx +1 -1
- package/src/components/translation-review.tsx +11 -5
- package/src/components/tree-view.tsx +3 -2
- package/src/components/treemap.tsx +27 -10
- package/src/components/ui.tsx +353 -153
- package/src/components/user-avatar.tsx +1 -1
- package/src/components/wizard-stepper.tsx +5 -1
- package/src/demo/demo-banner.tsx +5 -0
- package/src/feedback/feedback-dialog.tsx +3 -3
- package/src/feedback/feedback-inbox.tsx +2 -2
- package/src/feedback/feedback-thread.tsx +3 -3
- package/src/hooks/use-anchored-panel.ts +67 -5
- package/src/hooks/use-breakpoint.ts +132 -0
- package/src/hooks/use-large-text.ts +41 -0
- package/src/hooks/use-windowed-rows.ts +168 -12
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/german.ts +107 -0
- package/src/i18n/kit-labels.tsx +12 -0
- package/src/i18n/locales/es.ts +98 -0
- package/src/i18n/locales/fr.ts +102 -0
- package/src/i18n/locales/hu.ts +100 -0
- package/src/i18n/locales/it.ts +103 -0
- package/src/i18n/locales/zh.ts +94 -0
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +76 -6
- package/src/landing/landing-sections.tsx +7 -3
- package/src/landing/public-header.tsx +4 -4
- package/src/lib/cn.ts +13 -2
- package/src/rhf/fields.tsx +1 -1
- package/src/rhf/form.tsx +2 -2
- package/src/search/command-palette.tsx +10 -9
- package/src/settings/settings-layout.tsx +2 -1
- package/src/settings/use-account-appearance.ts +148 -0
- package/src/settings/use-settings-route.ts +3 -4
- package/src/shell/app-shell.tsx +278 -54
- package/src/shell/auth-layout.tsx +1 -1
- package/src/shell/option-switcher-menu.tsx +1 -1
- package/src/shell/top-bar-brand.tsx +3 -1
- package/src/shell/topbar-action-menu.tsx +3 -3
- package/src/shell/topbar-controls.tsx +4 -4
- package/src/theme/contrast-tokens.ts +112 -0
- package/src/theme/contrast.ts +225 -0
- package/src/theme/palette-presets.ts +24 -4
- package/src/theme/palette-store.ts +6 -1
- package/src/theme/text-size.ts +283 -0
- package/src/wizard/wizard-summary.tsx +4 -2
- package/tokens.css +158 -8
package/src/components/ui.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createContext, forwardRef, useContext, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
-
import { ChevronDown, Eye, EyeOff,
|
|
2
|
+
import { ChevronDown, Eye, EyeOff, Plus, X } from "lucide-react";
|
|
3
3
|
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ChangeEvent, ComponentPropsWithoutRef, CSSProperties, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { FOCUS_RING, FOCUS_RING_WIDTH } from "./focus-ring";
|
|
5
6
|
import { scrollIntoStrip, useStripFade } from "../lib/strip-fade";
|
|
6
7
|
import { horizontalStep } from "../lib/direction";
|
|
7
|
-
import {
|
|
8
|
+
import { usePhoneLayout } from "../hooks/use-breakpoint";
|
|
9
|
+
import { useLargeText } from "../hooks/use-large-text";
|
|
8
10
|
import { Tooltip, type TooltipSide } from "./tooltip";
|
|
9
11
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
10
12
|
import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
|
|
@@ -12,9 +14,13 @@ import { pickLinkRenderer, replacingClick, routerLinkNavigation } from "./text-l
|
|
|
12
14
|
import { useCommitReason } from "./write-lock";
|
|
13
15
|
import { mergeDescribedBy } from "./choice-parts";
|
|
14
16
|
import {
|
|
17
|
+
CompactControlsContext,
|
|
18
|
+
DisabledReasonLine,
|
|
15
19
|
EndHintRow,
|
|
16
20
|
FieldCaption,
|
|
17
21
|
LockedReason,
|
|
22
|
+
useDisabledReasonLine,
|
|
23
|
+
type DisabledReasonDisplay,
|
|
18
24
|
useFieldHint,
|
|
19
25
|
useLockReason,
|
|
20
26
|
FLOATING_LABEL_STATIC,
|
|
@@ -24,16 +30,35 @@ import {
|
|
|
24
30
|
|
|
25
31
|
// The static label lives in field-parts since 0.23 (it is what the field anatomy
|
|
26
32
|
// there draws, and field-parts must not import this file); public from here as before.
|
|
27
|
-
export { FieldLabel, FLOATING_LABEL_STATIC } from "./field-parts";
|
|
28
|
-
export type { FieldLabelProps } from "./field-parts";
|
|
33
|
+
export { FieldLabel, FLOATING_LABEL_STATIC, FieldHint } from "./field-parts";
|
|
34
|
+
export type { FieldLabelProps, FieldHintProps } from "./field-parts";
|
|
35
|
+
|
|
36
|
+
// The kit's focus frame (§5) lives in its own module so the parts ui.tsx itself imports
|
|
37
|
+
// (field-parts, the choice controls) can draw it too; public from here as before.
|
|
38
|
+
export { FOCUS_RING, FOCUS_RING_WIDTH } from "./focus-ring";
|
|
29
39
|
|
|
30
40
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
|
|
31
41
|
|
|
32
42
|
// Shared base ring for every button-styled element. Kept as a named const so the
|
|
33
43
|
// <Button> component and the {@link buttonClasses} helper draw from one source and
|
|
34
44
|
// can never drift apart.
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
//
|
|
46
|
+
// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus
|
|
47
|
+
// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each
|
|
48
|
+
// variant's own colour below.
|
|
49
|
+
const BUTTON_BASE = cn(
|
|
50
|
+
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed",
|
|
51
|
+
FOCUS_RING_WIDTH,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The touch target at Large and Extra large (docs/text-size-harmonization.md §4): at
|
|
56
|
+
* least 48 px, the size a finger needs whatever the type. In px on purpose, like the
|
|
57
|
+
* field floor ({@link FIELD_TOUCH_TEXT}): it is a physical threshold, not a size that
|
|
58
|
+
* follows the text. Normal keeps the kit's own heights; a `large:` variant, so it costs
|
|
59
|
+
* no render and is right on the first paint.
|
|
60
|
+
*/
|
|
61
|
+
export const TOUCH_TARGET_LARGE = "large:min-h-[48px] large:min-w-[48px]";
|
|
37
62
|
|
|
38
63
|
export type ButtonSize = "sm" | "md";
|
|
39
64
|
|
|
@@ -44,10 +69,19 @@ export type ButtonSize = "sm" | "md";
|
|
|
44
69
|
// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for
|
|
45
70
|
// a bigger text button (IconButton's `lg` is a touch target, not a text size). The
|
|
46
71
|
// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.
|
|
72
|
+
//
|
|
73
|
+
// At Large and above (§4) `sm` grows to `md`'s height — the padding that makes its
|
|
74
|
+
// 1rem line as tall as `md`'s 1.25rem one — and both reach the 48 px touch target. The
|
|
75
|
+
// type stays `sm`'s: the compact button is still the quieter one. Not on `link`, which
|
|
76
|
+
// is a word in a sentence and has no box to grow (see `buttonLook`).
|
|
47
77
|
const BUTTON_SIZES: Record<ButtonSize, string> = {
|
|
48
78
|
md: "gap-2 px-3 py-2 text-sm",
|
|
49
79
|
sm: "gap-1.5 px-2 py-1 text-xs",
|
|
50
80
|
};
|
|
81
|
+
const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
|
|
82
|
+
md: "large:min-h-[48px]",
|
|
83
|
+
sm: "large:min-h-[48px] large:py-2.5",
|
|
84
|
+
};
|
|
51
85
|
|
|
52
86
|
// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.
|
|
53
87
|
// Actions default to a warm bordered look (primary = filled warm chip, secondary =
|
|
@@ -57,24 +91,24 @@ const BUTTON_SIZES: Record<ButtonSize, string> = {
|
|
|
57
91
|
// variant's own accent: brand for the four neutral ones, danger for `danger`.
|
|
58
92
|
const buttonVariantClasses: Record<ButtonVariant, string> = {
|
|
59
93
|
primary:
|
|
60
|
-
"border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus:ring-[var(--brand)]",
|
|
94
|
+
"border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus-visible:ring-[var(--brand)]",
|
|
61
95
|
secondary:
|
|
62
|
-
"border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
|
|
96
|
+
"border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus-visible:ring-[var(--brand)]",
|
|
63
97
|
ghost:
|
|
64
|
-
"bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
|
|
98
|
+
"bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus-visible:ring-[var(--brand)]",
|
|
65
99
|
danger:
|
|
66
|
-
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border-strong)]",
|
|
100
|
+
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
67
101
|
brand:
|
|
68
|
-
"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]",
|
|
102
|
+
"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus-visible:ring-[var(--brand)]",
|
|
69
103
|
// A text link that is still a `<button>` — keksdose's six hand-rolled
|
|
70
104
|
// `<button className="text-brand underline">` sites ("Resend code", "Show all",
|
|
71
105
|
// "Undo" in a toast…), which act rather than navigate and so must not be anchors.
|
|
72
106
|
// The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but
|
|
73
|
-
// the base's
|
|
107
|
+
// the base's focus ring stays: those copies had `outline-none` and no ring, so
|
|
74
108
|
// a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that
|
|
75
109
|
// ring hugging the word instead of drawing a pill round it.
|
|
76
110
|
link:
|
|
77
|
-
"rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]",
|
|
111
|
+
"rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus-visible:ring-[var(--brand)]",
|
|
78
112
|
};
|
|
79
113
|
|
|
80
114
|
/**
|
|
@@ -109,7 +143,7 @@ const BUTTON_TONES: Record<Exclude<ButtonTone, "default">, string> = {
|
|
|
109
143
|
muted:
|
|
110
144
|
"text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:hover:text-[var(--text-secondary)]",
|
|
111
145
|
danger:
|
|
112
|
-
"text-[var(--text-secondary)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)] disabled:hover:text-[var(--text-secondary)]",
|
|
146
|
+
"text-[var(--text-secondary)] hover:text-[var(--danger)] focus-visible:ring-[var(--danger-border-strong)] disabled:hover:text-[var(--text-secondary)]",
|
|
113
147
|
};
|
|
114
148
|
|
|
115
149
|
/** Only the two transparent variants take every tone; see {@link ButtonTone}. */
|
|
@@ -120,9 +154,9 @@ const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
|
|
|
120
154
|
// the neutral ones survives the merge.
|
|
121
155
|
const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
|
|
122
156
|
secondary:
|
|
123
|
-
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
|
|
157
|
+
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
124
158
|
primary:
|
|
125
|
-
"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus:ring-[var(--danger-border-strong)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
|
|
159
|
+
"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)] hover:border-[var(--danger)] hover:bg-[var(--danger)] hover:text-[var(--danger-contrast)] focus-visible:ring-[var(--danger-border-strong)] disabled:hover:border-[var(--danger-border)] disabled:hover:bg-[var(--danger-bg)] disabled:hover:text-[var(--danger)]",
|
|
126
160
|
};
|
|
127
161
|
|
|
128
162
|
// `pressed` on a `link`: the brand colour and a heavier weight, which is exactly what
|
|
@@ -155,7 +189,13 @@ export function buttonClasses(
|
|
|
155
189
|
): string {
|
|
156
190
|
const { size = "md", className } =
|
|
157
191
|
typeof classNameOrOptions === "object" ? classNameOrOptions : { className: classNameOrOptions };
|
|
158
|
-
return cn(
|
|
192
|
+
return cn(
|
|
193
|
+
BUTTON_BASE,
|
|
194
|
+
BUTTON_SIZES[size],
|
|
195
|
+
variant !== "link" && BUTTON_SIZES_LARGE[size],
|
|
196
|
+
buttonVariantClasses[variant],
|
|
197
|
+
className,
|
|
198
|
+
);
|
|
159
199
|
}
|
|
160
200
|
|
|
161
201
|
/**
|
|
@@ -213,6 +253,12 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
213
253
|
* DangerConfirm keeps its own.
|
|
214
254
|
*/
|
|
215
255
|
disabledReason?: ReactNode;
|
|
256
|
+
/**
|
|
257
|
+
* Where `disabledReason` shows (0.32, §4 "No fact only in a tooltip"): by default a
|
|
258
|
+
* line under the button at Large and on a touch screen, the tooltip otherwise. See
|
|
259
|
+
* {@link DisabledReasonDisplay}.
|
|
260
|
+
*/
|
|
261
|
+
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
216
262
|
/**
|
|
217
263
|
* This button COMMITS — it saves, creates, deletes. Under a locked
|
|
218
264
|
* {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's
|
|
@@ -295,6 +341,7 @@ export interface ButtonLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorEle
|
|
|
295
341
|
pending?: never;
|
|
296
342
|
pressed?: never;
|
|
297
343
|
disabledReason?: never;
|
|
344
|
+
disabledReasonDisplay?: never;
|
|
298
345
|
commit?: never;
|
|
299
346
|
}
|
|
300
347
|
|
|
@@ -302,11 +349,24 @@ function hasContent(node: ReactNode): boolean {
|
|
|
302
349
|
return node !== undefined && node !== null && node !== false && node !== "";
|
|
303
350
|
}
|
|
304
351
|
|
|
352
|
+
// No fact only in a tooltip (§4): the reason line and the compact region live in
|
|
353
|
+
// field-parts, where the self-saving controls (Switch, Checkbox) reach them too.
|
|
354
|
+
export {
|
|
355
|
+
CompactControls,
|
|
356
|
+
DisabledReasonLine,
|
|
357
|
+
DISABLED_REASON_LINE_CLASS,
|
|
358
|
+
useDisabledReasonLine,
|
|
359
|
+
} from "./field-parts";
|
|
360
|
+
export type { DisabledReasonDisplay } from "./field-parts";
|
|
361
|
+
|
|
305
362
|
/** The button look shared by the `<button>` and the `<a>` form. */
|
|
306
363
|
function buttonLook(variant: ButtonVariant, size: ButtonSize, stretch: boolean | undefined, tone: ButtonTone) {
|
|
307
364
|
return cn(
|
|
308
365
|
BUTTON_BASE,
|
|
309
366
|
BUTTON_SIZES[size],
|
|
367
|
+
// §4: `sm` at `md`'s height and the 48 px target at Large — not on a `link`. In the
|
|
368
|
+
// order `buttonClasses` has it, so the two strings stay identical.
|
|
369
|
+
variant !== "link" && BUTTON_SIZES_LARGE[size],
|
|
310
370
|
// In a flex row next to a taller labelled field, `stretch` makes the button
|
|
311
371
|
// fill the field's height so the two line up (self-stretch overrides the row's
|
|
312
372
|
// align-items). No effect outside a flex row / when it's already the tallest.
|
|
@@ -422,6 +482,7 @@ function ButtonElement({
|
|
|
422
482
|
tone = "default",
|
|
423
483
|
pressed,
|
|
424
484
|
disabledReason: ownDisabledReason,
|
|
485
|
+
disabledReasonDisplay,
|
|
425
486
|
commit,
|
|
426
487
|
pending,
|
|
427
488
|
className,
|
|
@@ -486,15 +547,60 @@ function ButtonElement({
|
|
|
486
547
|
);
|
|
487
548
|
if (!locked) return button;
|
|
488
549
|
return (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
550
|
+
<ReasonFrame
|
|
551
|
+
reason={disabledReason}
|
|
552
|
+
reasonId={reasonId}
|
|
553
|
+
display={disabledReasonDisplay}
|
|
554
|
+
className={stretch ? "self-stretch" : undefined}
|
|
555
|
+
>
|
|
556
|
+
{button}
|
|
557
|
+
</ReasonFrame>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* A locked control and its reason: the line under it, or the kit {@link Tooltip} with a
|
|
563
|
+
* hidden copy for `aria-describedby` — see {@link DisabledReasonDisplay}. A component of
|
|
564
|
+
* its own so only a LOCKED control asks the text size and the pointer; the hundreds of
|
|
565
|
+
* unlocked buttons on a page subscribe to nothing.
|
|
566
|
+
*
|
|
567
|
+
* Tooltip form: the control in a FRAGMENT so Tooltip does not clone its bubble into the
|
|
568
|
+
* description as well — the hidden copy already describes it, and the same sentence
|
|
569
|
+
* twice would be read on every focus. Line form: the line is the description, and no
|
|
570
|
+
* bubble repeats what is on screen. Either way `className` (the control's `stretch`)
|
|
571
|
+
* moves to the wrapper, which is now the flex item.
|
|
572
|
+
*/
|
|
573
|
+
function ReasonFrame({
|
|
574
|
+
reason,
|
|
575
|
+
reasonId,
|
|
576
|
+
display,
|
|
577
|
+
className,
|
|
578
|
+
side,
|
|
579
|
+
portal,
|
|
580
|
+
children,
|
|
581
|
+
}: {
|
|
582
|
+
reason: ReactNode;
|
|
583
|
+
reasonId: string;
|
|
584
|
+
display: DisabledReasonDisplay | undefined;
|
|
585
|
+
className?: string;
|
|
586
|
+
side?: TooltipSide;
|
|
587
|
+
portal?: boolean;
|
|
588
|
+
children: ReactNode;
|
|
589
|
+
}) {
|
|
590
|
+
const line = useDisabledReasonLine(display);
|
|
591
|
+
if (line) {
|
|
592
|
+
return (
|
|
593
|
+
<DisabledReasonLine id={reasonId} reason={reason} className={className}>
|
|
594
|
+
{children}
|
|
595
|
+
</DisabledReasonLine>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
return (
|
|
599
|
+
<Tooltip label={reason} side={side} portal={portal} className={className}>
|
|
494
600
|
<>
|
|
495
|
-
{
|
|
601
|
+
{children}
|
|
496
602
|
<span id={reasonId} hidden>
|
|
497
|
-
{
|
|
603
|
+
{reason}
|
|
498
604
|
</span>
|
|
499
605
|
</>
|
|
500
606
|
</Tooltip>
|
|
@@ -511,8 +617,10 @@ function ButtonElement({
|
|
|
511
617
|
//
|
|
512
618
|
// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the
|
|
513
619
|
// two re-skin together with the palette.
|
|
514
|
-
const ICON_BUTTON_BASE =
|
|
515
|
-
"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none
|
|
620
|
+
const ICON_BUTTON_BASE = cn(
|
|
621
|
+
"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed",
|
|
622
|
+
FOCUS_RING_WIDTH,
|
|
623
|
+
);
|
|
516
624
|
|
|
517
625
|
export type IconButtonSize = "2xl" | "xl" | "lg" | "md" | "sm" | "xs" | "2xs";
|
|
518
626
|
|
|
@@ -548,6 +656,45 @@ const ICON_BUTTON_STRETCH_SIZES: Record<IconButtonSize, string> = {
|
|
|
548
656
|
"2xs": "w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5",
|
|
549
657
|
};
|
|
550
658
|
|
|
659
|
+
// The label as visible text beside the glyph (0.32, §4: at Large "IconButton shows its
|
|
660
|
+
// label"): the same height and glyph as the square, a width taken from the words. A
|
|
661
|
+
// map of its own rather than `w-auto` over `size-*`, which tailwind-merge cannot undo
|
|
662
|
+
// in one direction.
|
|
663
|
+
const ICON_BUTTON_TEXT_SIZES: Record<IconButtonSize, string> = {
|
|
664
|
+
"2xl": "h-16 gap-2 px-4 text-base [&_svg]:size-7",
|
|
665
|
+
xl: "h-12 gap-2 px-3.5 text-sm [&_svg]:size-6",
|
|
666
|
+
lg: "h-11 gap-2 px-3 text-sm [&_svg]:size-5",
|
|
667
|
+
md: "h-9 gap-1.5 px-2.5 text-sm [&_svg]:size-5",
|
|
668
|
+
sm: "h-8 gap-1.5 px-2 text-sm [&_svg]:size-5",
|
|
669
|
+
xs: "h-7 gap-1 rounded px-1.5 text-xs [&_svg]:size-4",
|
|
670
|
+
"2xs": "h-6 gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5",
|
|
671
|
+
};
|
|
672
|
+
const ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {
|
|
673
|
+
"2xl": "min-h-16 self-stretch gap-2 px-4 text-base [&_svg]:size-7",
|
|
674
|
+
xl: "min-h-12 self-stretch gap-2 px-3.5 text-sm [&_svg]:size-6",
|
|
675
|
+
lg: "min-h-11 self-stretch gap-2 px-3 text-sm [&_svg]:size-5",
|
|
676
|
+
md: "min-h-9 self-stretch gap-1.5 px-2.5 text-sm [&_svg]:size-5",
|
|
677
|
+
sm: "min-h-8 self-stretch gap-1.5 px-2 text-sm [&_svg]:size-5",
|
|
678
|
+
xs: "min-h-7 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-4",
|
|
679
|
+
"2xs": "min-h-6 self-stretch gap-1 rounded px-1.5 text-xs [&_svg]:size-3.5",
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// The 48 px touch target at Large (§4), per size. `lg` and up are past it already
|
|
683
|
+
// (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON
|
|
684
|
+
// a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and
|
|
685
|
+
// gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the
|
|
686
|
+
// taps a finger lands round the glyph.
|
|
687
|
+
const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
|
|
688
|
+
"2xl": "",
|
|
689
|
+
xl: "",
|
|
690
|
+
lg: "",
|
|
691
|
+
md: TOUCH_TARGET_LARGE,
|
|
692
|
+
sm: TOUCH_TARGET_LARGE,
|
|
693
|
+
xs: TOUCH_TARGET_LARGE,
|
|
694
|
+
"2xs":
|
|
695
|
+
"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']",
|
|
696
|
+
};
|
|
697
|
+
|
|
551
698
|
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
552
699
|
// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):
|
|
553
700
|
// QUIET is placeholder grey until the pointer or focus arrives, then the tone's
|
|
@@ -570,32 +717,32 @@ type ColouredTone = "danger" | "warning" | "info" | "success" | "custom";
|
|
|
570
717
|
const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {
|
|
571
718
|
danger: {
|
|
572
719
|
quiet:
|
|
573
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border-strong)]",
|
|
574
|
-
toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border-strong)]",
|
|
720
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
721
|
+
toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
575
722
|
},
|
|
576
723
|
warning: {
|
|
577
724
|
quiet:
|
|
578
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus:ring-[var(--warning-border)]",
|
|
579
|
-
toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]",
|
|
725
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus-visible:ring-[var(--warning-border)]",
|
|
726
|
+
toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus-visible:ring-[var(--warning-border)]",
|
|
580
727
|
},
|
|
581
728
|
info: {
|
|
582
729
|
quiet:
|
|
583
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
|
|
584
|
-
toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]",
|
|
730
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus-visible:ring-[var(--info-border)]",
|
|
731
|
+
toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus-visible:ring-[var(--info-border)]",
|
|
585
732
|
},
|
|
586
733
|
success: {
|
|
587
734
|
quiet:
|
|
588
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus:ring-[var(--success-border)]",
|
|
589
|
-
toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus:ring-[var(--success-border)]",
|
|
735
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus-visible:ring-[var(--success-border)]",
|
|
736
|
+
toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus-visible:ring-[var(--success-border)]",
|
|
590
737
|
},
|
|
591
738
|
// The hover fill and the ring are MIXED from the one colour, since a custom tone
|
|
592
739
|
// brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a
|
|
593
740
|
// `custom` tone with no colour set is the default look rather than invisible.
|
|
594
741
|
custom: {
|
|
595
742
|
quiet:
|
|
596
|
-
"text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
743
|
+
"text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
597
744
|
toned:
|
|
598
|
-
"text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
745
|
+
"text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus-visible:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
|
|
599
746
|
},
|
|
600
747
|
};
|
|
601
748
|
|
|
@@ -656,7 +803,7 @@ const ICON_BUTTON_PRESSED =
|
|
|
656
803
|
// themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette
|
|
657
804
|
// can re-point); the blur keeps a busy background from breaking the glyph's edge.
|
|
658
805
|
const ICON_BUTTON_OVERLAY =
|
|
659
|
-
"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus:ring-[var(--text-inverse)]";
|
|
806
|
+
"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus-visible:ring-[var(--text-inverse)]";
|
|
660
807
|
|
|
661
808
|
// `variant="shutter"`: the camera's release — keksdose's camera-capture.tsx:248 draws
|
|
662
809
|
// it by hand as a 64px circle with a thick white ring round a translucent fill. It sits
|
|
@@ -665,7 +812,7 @@ const ICON_BUTTON_OVERLAY =
|
|
|
665
812
|
// `overlay` uses would give a dark ring on a dark viewfinder in dark mode. The focus ring stands off by 2px, or it would
|
|
666
813
|
// merge into the ring that is part of the look.
|
|
667
814
|
const ICON_BUTTON_SHUTTER =
|
|
668
|
-
"rounded-full border-4 border-[var(--media-ink)] bg-[var(--media-scrim)] text-[var(--media-ink)] backdrop-blur-sm hover:bg-[var(--media-scrim-hover)] focus:ring-[var(--media-ink)] focus:ring-offset-2 focus:ring-offset-[var(--media-scrim-hover)]";
|
|
815
|
+
"rounded-full border-4 border-[var(--media-ink)] bg-[var(--media-scrim)] text-[var(--media-ink)] backdrop-blur-sm hover:bg-[var(--media-scrim-hover)] focus-visible:ring-[var(--media-ink)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--media-scrim-hover)]";
|
|
669
816
|
|
|
670
817
|
export type IconButtonVariant = ButtonVariant | "overlay" | "shutter";
|
|
671
818
|
|
|
@@ -843,9 +990,25 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
843
990
|
* unless `tooltipPortal` says otherwise.
|
|
844
991
|
*/
|
|
845
992
|
label?: string;
|
|
993
|
+
/**
|
|
994
|
+
* Show `label` as visible text beside the icon at Large and Extra large (0.32,
|
|
995
|
+
* docs/text-size-harmonization.md §4, §10.8). A reader who asked for bigger type is
|
|
996
|
+
* the one an unlabelled glyph fails, and on touch the tooltip never opens. Then there
|
|
997
|
+
* is no tooltip: the words are on the button.
|
|
998
|
+
*
|
|
999
|
+
* Default `true` — except for `variant="overlay"` and `"shutter"`, which sit ON a
|
|
1000
|
+
* picture, and inside {@link CompactControls} (AppShell's top bar, `RowActions`'
|
|
1001
|
+
* inline icons). `false`: the icon alone at every size, for an icon on content — a
|
|
1002
|
+
* receipt overlay, a map's zoom, a viewfinder, a photo thumbnail. That must be rare;
|
|
1003
|
+
* a dense row's actions belong in `RowActions`, which collapses them into a "⋯" menu
|
|
1004
|
+
* at Large instead (§10.8). Normal never shows it. `tooltip={false}` (a universally
|
|
1005
|
+
* read glyph, a dialog's ✕) keeps the icon alone too.
|
|
1006
|
+
*/
|
|
1007
|
+
labelVisible?: boolean;
|
|
846
1008
|
/** Show `label` as a tooltip. Default `true`; `false` keeps `label` as the
|
|
847
1009
|
* accessible name only — for a button whose glyph is universally read (a close ✕ in
|
|
848
|
-
* a dialog header) or that already sits under a tooltip of its own.
|
|
1010
|
+
* a dialog header) or that already sits under a tooltip of its own. It also keeps the
|
|
1011
|
+
* label from showing as text at Large (see `labelVisible`). */
|
|
849
1012
|
tooltip?: boolean;
|
|
850
1013
|
/** Where the `label` tooltip opens. See {@link Tooltip}'s `side`. */
|
|
851
1014
|
tooltipSide?: TooltipSide;
|
|
@@ -899,8 +1062,13 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
899
1062
|
* bubble shows the reason in place of the label, since the glyph already says what
|
|
900
1063
|
* the button is and the reason is the news. Shown even with `tooltip={false}`: a
|
|
901
1064
|
* reason nobody can see is not one. Wins over `disabled`, as on Button.
|
|
1065
|
+
*
|
|
1066
|
+
* 0.32: at Large and on a touch screen the reason is a line under the button rather
|
|
1067
|
+
* than a bubble — see `disabledReasonDisplay`.
|
|
902
1068
|
*/
|
|
903
1069
|
disabledReason?: ReactNode;
|
|
1070
|
+
/** Where `disabledReason` shows — {@link Button}'s `disabledReasonDisplay`. */
|
|
1071
|
+
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
904
1072
|
/** This action COMMITS — {@link Button}'s `commit`: under a locked
|
|
905
1073
|
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
906
1074
|
* reason. No effect without a lock. */
|
|
@@ -947,6 +1115,7 @@ export interface IconButtonLinkProps
|
|
|
947
1115
|
| "quiet"
|
|
948
1116
|
| "stopPropagation"
|
|
949
1117
|
| "label"
|
|
1118
|
+
| "labelVisible"
|
|
950
1119
|
| "tooltip"
|
|
951
1120
|
| "tooltipSide"
|
|
952
1121
|
| "tooltipPortal"
|
|
@@ -968,11 +1137,35 @@ export interface IconButtonLinkProps
|
|
|
968
1137
|
form?: never;
|
|
969
1138
|
pressed?: never;
|
|
970
1139
|
disabledReason?: never;
|
|
1140
|
+
disabledReasonDisplay?: never;
|
|
971
1141
|
commit?: never;
|
|
972
1142
|
disabledStyle?: never;
|
|
973
1143
|
pending?: never;
|
|
974
1144
|
}
|
|
975
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* Whether an IconButton draws its `label` as text (see `labelVisible`): at Large and
|
|
1148
|
+
* above, with a label to draw, not switched off by the caller, the variant (`overlay`,
|
|
1149
|
+
* `shutter`), `tooltip={false}` or a {@link CompactControls} region.
|
|
1150
|
+
*/
|
|
1151
|
+
function useIconButtonText(
|
|
1152
|
+
label: string | undefined,
|
|
1153
|
+
labelVisible: boolean | undefined,
|
|
1154
|
+
variant: IconButtonVariant,
|
|
1155
|
+
tooltip: boolean,
|
|
1156
|
+
): boolean {
|
|
1157
|
+
const large = useLargeText();
|
|
1158
|
+
const compact = useContext(CompactControlsContext);
|
|
1159
|
+
if (!large || label === undefined || label === "" || !tooltip) return false;
|
|
1160
|
+
const visible = labelVisible ?? (!compact && variant !== "overlay" && variant !== "shutter");
|
|
1161
|
+
return visible;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** The visible label (see `labelVisible`). */
|
|
1165
|
+
function IconButtonText({ label }: { label: string }) {
|
|
1166
|
+
return <span data-slot="icon-button-label">{label}</span>;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
976
1169
|
/**
|
|
977
1170
|
* `toneColor`: one CSS colour, or one per theme.
|
|
978
1171
|
*
|
|
@@ -1008,6 +1201,7 @@ function iconButtonLook({
|
|
|
1008
1201
|
tone,
|
|
1009
1202
|
quiet,
|
|
1010
1203
|
toneColor,
|
|
1204
|
+
withText = false,
|
|
1011
1205
|
}: {
|
|
1012
1206
|
variant: IconButtonVariant;
|
|
1013
1207
|
size: IconButtonSize;
|
|
@@ -1017,10 +1211,18 @@ function iconButtonLook({
|
|
|
1017
1211
|
tone: IconButtonTone;
|
|
1018
1212
|
quiet: boolean | undefined;
|
|
1019
1213
|
toneColor: IconButtonToneColor | undefined;
|
|
1214
|
+
/** The label is drawn beside the glyph (see `labelVisible`). */
|
|
1215
|
+
withText?: boolean;
|
|
1020
1216
|
}) {
|
|
1021
1217
|
return cn(
|
|
1022
1218
|
ICON_BUTTON_BASE,
|
|
1023
|
-
|
|
1219
|
+
withText
|
|
1220
|
+
? cn("font-medium whitespace-nowrap", stretch ? ICON_BUTTON_TEXT_STRETCH_SIZES[size] : ICON_BUTTON_TEXT_SIZES[size])
|
|
1221
|
+
: stretch
|
|
1222
|
+
? ICON_BUTTON_STRETCH_SIZES[size]
|
|
1223
|
+
: ICON_BUTTON_SIZES[size],
|
|
1224
|
+
// With text the pill is a 48 px target at any size; the square follows its size.
|
|
1225
|
+
withText ? TOUCH_TARGET_LARGE : ICON_BUTTON_TOUCH_LARGE[size],
|
|
1024
1226
|
glyphSize !== undefined && ICON_BUTTON_GLYPH_SIZES[glyphSize],
|
|
1025
1227
|
// After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
|
|
1026
1228
|
variant === "overlay"
|
|
@@ -1079,6 +1281,7 @@ function IconButtonLink({
|
|
|
1079
1281
|
shape = "square",
|
|
1080
1282
|
toneColor,
|
|
1081
1283
|
label,
|
|
1284
|
+
labelVisible,
|
|
1082
1285
|
tooltip = true,
|
|
1083
1286
|
tooltipSide,
|
|
1084
1287
|
tooltipPortal,
|
|
@@ -1091,10 +1294,19 @@ function IconButtonLink({
|
|
|
1091
1294
|
rel,
|
|
1092
1295
|
onClick,
|
|
1093
1296
|
onKeyDown,
|
|
1094
|
-
children,
|
|
1297
|
+
children: glyph,
|
|
1095
1298
|
ref,
|
|
1096
1299
|
...rest
|
|
1097
1300
|
}: IconButtonLinkProps) {
|
|
1301
|
+
const withText = useIconButtonText(label, labelVisible, variant, tooltip);
|
|
1302
|
+
const children = withText ? (
|
|
1303
|
+
<>
|
|
1304
|
+
{glyph}
|
|
1305
|
+
<IconButtonText label={label!} />
|
|
1306
|
+
</>
|
|
1307
|
+
) : (
|
|
1308
|
+
glyph
|
|
1309
|
+
);
|
|
1098
1310
|
const kitLink = useKitLink();
|
|
1099
1311
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
1100
1312
|
const tone = toneProp ?? (toneColor !== undefined ? "custom" : "default");
|
|
@@ -1116,7 +1328,7 @@ function IconButtonLink({
|
|
|
1116
1328
|
onKeyDown?.(e);
|
|
1117
1329
|
};
|
|
1118
1330
|
const cls = cn(
|
|
1119
|
-
iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor }),
|
|
1331
|
+
iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),
|
|
1120
1332
|
// BASE's `disabled:` classes never match an `<a>` — Button's disabled link look.
|
|
1121
1333
|
disabled && "pointer-events-none cursor-not-allowed opacity-50",
|
|
1122
1334
|
className,
|
|
@@ -1165,7 +1377,8 @@ function IconButtonLink({
|
|
|
1165
1377
|
);
|
|
1166
1378
|
}
|
|
1167
1379
|
control = withIconButtonBadge(control, badge, stretch);
|
|
1168
|
-
|
|
1380
|
+
// The words are on the link at Large: a bubble would only repeat them.
|
|
1381
|
+
if (!tooltip || withText || label === undefined || label === "") return control;
|
|
1169
1382
|
return (
|
|
1170
1383
|
// A fragment, as on the button: the bubble says what `aria-label` already does.
|
|
1171
1384
|
<Tooltip
|
|
@@ -1192,11 +1405,13 @@ function IconButtonElement(
|
|
|
1192
1405
|
shape = "square",
|
|
1193
1406
|
toneColor,
|
|
1194
1407
|
label,
|
|
1408
|
+
labelVisible,
|
|
1195
1409
|
tooltip = true,
|
|
1196
1410
|
tooltipSide,
|
|
1197
1411
|
tooltipPortal,
|
|
1198
1412
|
tooltipLazy = true,
|
|
1199
1413
|
disabledReason: ownDisabledReason,
|
|
1414
|
+
disabledReasonDisplay,
|
|
1200
1415
|
commit,
|
|
1201
1416
|
pending,
|
|
1202
1417
|
glyphSize,
|
|
@@ -1218,6 +1433,7 @@ function IconButtonElement(
|
|
|
1218
1433
|
) {
|
|
1219
1434
|
const tone = toneProp ?? (toneColor !== undefined ? "custom" : "default");
|
|
1220
1435
|
const keep = disabledStyle === "keep";
|
|
1436
|
+
const withText = useIconButtonText(label, labelVisible, variant, tooltip);
|
|
1221
1437
|
const reasonId = useId();
|
|
1222
1438
|
const disabledReason = useCommitReason(commit, ownDisabledReason);
|
|
1223
1439
|
const locked = hasContent(disabledReason);
|
|
@@ -1252,7 +1468,7 @@ function IconButtonElement(
|
|
|
1252
1468
|
onKeyDown?.(e);
|
|
1253
1469
|
}}
|
|
1254
1470
|
className={cn(
|
|
1255
|
-
iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor }),
|
|
1471
|
+
iconButtonLook({ variant, size, stretch, glyphSize, shape, tone, quiet, toneColor, withText }),
|
|
1256
1472
|
pressed && ICON_BUTTON_PRESSED,
|
|
1257
1473
|
// The disabled look for the focusable kind of disabled, as on Button.
|
|
1258
1474
|
locked && (keep ? "cursor-default" : "cursor-not-allowed opacity-50"),
|
|
@@ -1274,29 +1490,28 @@ function IconButtonElement(
|
|
|
1274
1490
|
) : (
|
|
1275
1491
|
children
|
|
1276
1492
|
)}
|
|
1493
|
+
{withText && <IconButtonText label={label!} />}
|
|
1277
1494
|
</button>
|
|
1278
1495
|
);
|
|
1279
1496
|
const control = withIconButtonBadge(button, badge, stretch);
|
|
1280
1497
|
if (locked) {
|
|
1281
1498
|
return (
|
|
1282
|
-
// Button's shape: the
|
|
1283
|
-
//
|
|
1284
|
-
<
|
|
1285
|
-
|
|
1499
|
+
// Button's shape: the reason as a line under it, or the bubble with a hidden copy
|
|
1500
|
+
// for the description (see ReasonFrame).
|
|
1501
|
+
<ReasonFrame
|
|
1502
|
+
reason={disabledReason}
|
|
1503
|
+
reasonId={reasonId}
|
|
1504
|
+
display={disabledReasonDisplay}
|
|
1286
1505
|
side={tooltipSide}
|
|
1287
1506
|
portal={tooltipPortal}
|
|
1288
1507
|
className={stretch ? "self-stretch" : undefined}
|
|
1289
1508
|
>
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
<span id={reasonId} hidden>
|
|
1293
|
-
{disabledReason}
|
|
1294
|
-
</span>
|
|
1295
|
-
</>
|
|
1296
|
-
</Tooltip>
|
|
1509
|
+
{control}
|
|
1510
|
+
</ReasonFrame>
|
|
1297
1511
|
);
|
|
1298
1512
|
}
|
|
1299
|
-
|
|
1513
|
+
// The words are on the button at Large: a bubble would only repeat them.
|
|
1514
|
+
if (!tooltip || withText || label === undefined || label === "") return control;
|
|
1300
1515
|
return (
|
|
1301
1516
|
// A fragment, so Tooltip leaves the button's description alone: the bubble says
|
|
1302
1517
|
// exactly what `aria-label` already does. Lazy by default — see `tooltipLazy`.
|
|
@@ -1312,8 +1527,25 @@ function IconButtonElement(
|
|
|
1312
1527
|
);
|
|
1313
1528
|
}
|
|
1314
1529
|
|
|
1530
|
+
/**
|
|
1531
|
+
* The 16 px floor on touch (docs/text-size-harmonization.md §10.1). iOS zooms the page
|
|
1532
|
+
* when a field under 16 px takes focus, and keksdose's `maximum-scale=1` — which stopped
|
|
1533
|
+
* it — goes so pinch zoom works again (WCAG 1.4.4). A zoomed page drops the keyboard
|
|
1534
|
+
* inset to 0 (§10.2), so the zoom would put a pinned editor footer behind the keyboard
|
|
1535
|
+
* (keksdose #154). So on a coarse pointer a field is at least 16 px, at every text size:
|
|
1536
|
+
* `max()` leaves Large and Extra large alone, where `text-sm` is already 17.5 / 21 px.
|
|
1537
|
+
*
|
|
1538
|
+
* In px on purpose: the threshold is the browser's, in CSS px, whatever the root size.
|
|
1539
|
+
* Font size only — the line height keeps `text-sm`'s ratio. Part of {@link FIELD_BASE};
|
|
1540
|
+
* add it to any other text-entry control whose type is below 16 px.
|
|
1541
|
+
*/
|
|
1542
|
+
export const FIELD_TOUCH_TEXT = "pointer-coarse:text-[length:max(0.875rem,16px)]";
|
|
1543
|
+
|
|
1315
1544
|
export const FIELD_BASE =
|
|
1316
1545
|
"block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-3 py-2 text-sm text-[var(--text-primary)] shadow-sm placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:ring-[var(--brand)] " +
|
|
1546
|
+
// §10.1: at least 16 px on a touch screen, or iOS zooms the page on focus.
|
|
1547
|
+
FIELD_TOUCH_TEXT +
|
|
1548
|
+
" " +
|
|
1317
1549
|
// A field the user cannot change has to LOOK settled. Without this, `disabled`
|
|
1318
1550
|
// dimmed the floating label and nothing else — FIELD_BASE's own
|
|
1319
1551
|
// `text-[var(--text-primary)]` overrides the browser's grey — so a read-only value
|
|
@@ -1375,8 +1607,12 @@ export const FIELD_INVALID =
|
|
|
1375
1607
|
|
|
1376
1608
|
export const FLOATING_INPUT_CLASS = cn(FIELD_BASE, FIELD_FLOATING_PAD, "peer placeholder:text-transparent");
|
|
1377
1609
|
|
|
1378
|
-
/** The phone breakpoint
|
|
1379
|
-
*
|
|
1610
|
+
/** The phone breakpoint at Normal text size, as a media query.
|
|
1611
|
+
*
|
|
1612
|
+
* @deprecated since 0.32 — it does not follow the text size (§3.3): at Large the CSS
|
|
1613
|
+
* `max-md:` reaches 959 px and this still stops at 767. Use `usePhoneLayout()`, which
|
|
1614
|
+
* agrees with `max-md:` at every size, or `breakpointQuery("max-md", scale)` where a
|
|
1615
|
+
* query string is needed. Kept exported, unchanged, for the apps that import it. */
|
|
1380
1616
|
export const PHONE_QUERY = "(max-width: 767px)";
|
|
1381
1617
|
|
|
1382
1618
|
/**
|
|
@@ -1385,8 +1621,9 @@ export const PHONE_QUERY = "(max-width: 767px)";
|
|
|
1385
1621
|
* about reads as the thing itself, not as another boxed row in a stack.
|
|
1386
1622
|
*
|
|
1387
1623
|
* Every control that takes `variant="display"` — {@link Input}, `NumberInput`,
|
|
1388
|
-
* `AmountInput` — means exactly the same thing by it: the treatment applies
|
|
1389
|
-
*
|
|
1624
|
+
* `AmountInput` — means exactly the same thing by it: the treatment applies in the
|
|
1625
|
+
* phone layout (`usePhoneLayout()`, `max-md:` at the text size in force) and the field
|
|
1626
|
+
* is untouched above it, so a caller never has
|
|
1390
1627
|
* to ask the viewport, and a form can't end up half-treated across breakpoints.
|
|
1391
1628
|
*
|
|
1392
1629
|
* What stays, deliberately:
|
|
@@ -1466,8 +1703,8 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
1466
1703
|
export const FLOATING_LABEL_CLASS = cn(
|
|
1467
1704
|
"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all",
|
|
1468
1705
|
"max-w-[calc(100%-1.5rem)] truncate",
|
|
1469
|
-
"peer-focus:top-1 peer-focus:text-
|
|
1470
|
-
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-
|
|
1706
|
+
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1707
|
+
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
1471
1708
|
"peer-disabled:opacity-50",
|
|
1472
1709
|
);
|
|
1473
1710
|
|
|
@@ -1485,8 +1722,8 @@ export const FLOATING_LABEL_CLASS = cn(
|
|
|
1485
1722
|
const FLOATING_ROW_CLASS = cn(
|
|
1486
1723
|
"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all",
|
|
1487
1724
|
"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]",
|
|
1488
|
-
"peer-focus:top-1 peer-focus:text-
|
|
1489
|
-
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-
|
|
1725
|
+
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1726
|
+
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
1490
1727
|
"peer-disabled:opacity-50",
|
|
1491
1728
|
);
|
|
1492
1729
|
|
|
@@ -1581,69 +1818,6 @@ export function FloatingField({
|
|
|
1581
1818
|
);
|
|
1582
1819
|
}
|
|
1583
1820
|
|
|
1584
|
-
/**
|
|
1585
|
-
* The "?" that explains a field, on the field's own label line (dev#468).
|
|
1586
|
-
*
|
|
1587
|
-
* Pass it to a labelled {@link Select} / {@link Input} as `hint`. It exists as a
|
|
1588
|
-
* component rather than as a snippet each form repeats because the previous
|
|
1589
|
-
* version was exactly that snippet — an absolutely-positioned button whose
|
|
1590
|
-
* `top-1.5` was one guess at where an 11px label sits — and the reporter's
|
|
1591
|
-
* follow-up was *"question mark is not centered. Is it part of the hoc?
|
|
1592
|
-
* positioning problems seem quite frequently."* It was not part of the HOC. Now
|
|
1593
|
-
* it is, and there is one place left where the answer can be wrong.
|
|
1594
|
-
*
|
|
1595
|
-
* A `<button>` rather than a bare icon: hover alone puts the explanation out of
|
|
1596
|
-
* reach of a keyboard and of every touch device, and the tooltip shows on focus
|
|
1597
|
-
* too. The text is also its accessible name, so a screen reader gets it without
|
|
1598
|
-
* the bubble ever opening.
|
|
1599
|
-
*/
|
|
1600
|
-
export interface FieldHintProps extends Omit<ComponentPropsWithoutRef<"button">, "children"> {
|
|
1601
|
-
/** The explanation. It is both the tooltip's text and, by default, the button's
|
|
1602
|
-
* accessible name, so a screen reader gets it without the bubble ever opening. */
|
|
1603
|
-
label: string;
|
|
1604
|
-
/** Default `"start"`: before the hint in the reading direction (the left in LTR, the
|
|
1605
|
-
* right in RTL). `left` / `right` stay physical. */
|
|
1606
|
-
side?: TooltipSide;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
export function FieldHint({
|
|
1610
|
-
label,
|
|
1611
|
-
side = "start",
|
|
1612
|
-
className,
|
|
1613
|
-
"aria-label": ariaLabel,
|
|
1614
|
-
...rest
|
|
1615
|
-
}: FieldHintProps) {
|
|
1616
|
-
return (
|
|
1617
|
-
// `tap="toggle"` (0.25): the "?" exists only to explain, so a tap on a phone shows
|
|
1618
|
-
// the bubble and the next tap hides it — the touch rule (no bubble left behind by a
|
|
1619
|
-
// tap) would otherwise make it show nothing at all.
|
|
1620
|
-
<Tooltip label={label} side={side} portal tap="toggle">
|
|
1621
|
-
<button
|
|
1622
|
-
{...rest}
|
|
1623
|
-
// `type` after the spread, not before. These render inside forms — that is the
|
|
1624
|
-
// only place a field has a label line — and a hint that defaulted to `submit`
|
|
1625
|
-
// because a caller spread a props object at it would save the form on a click
|
|
1626
|
-
// meant to explain a field.
|
|
1627
|
-
type="button"
|
|
1628
|
-
// The explanation names the button unless the caller says otherwise; passing
|
|
1629
|
-
// `aria-label` is how you shorten it for a screen reader without shortening
|
|
1630
|
-
// what the bubble shows.
|
|
1631
|
-
aria-label={ariaLabel ?? label}
|
|
1632
|
-
// Nothing to activate: the tooltip opens on hover and on focus, and a
|
|
1633
|
-
// click that did something as well would be a second, undiscoverable
|
|
1634
|
-
// behaviour on the same target.
|
|
1635
|
-
onClick={(e) => e.preventDefault()}
|
|
1636
|
-
className={cn(
|
|
1637
|
-
"flex text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]",
|
|
1638
|
-
className,
|
|
1639
|
-
)}
|
|
1640
|
-
>
|
|
1641
|
-
<HelpCircle className="size-3.5" />
|
|
1642
|
-
</button>
|
|
1643
|
-
</Tooltip>
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
1821
|
export interface LabelProps extends ComponentPropsWithoutRef<"label"> {
|
|
1648
1822
|
/** Draw the required mark after the text. The mark is `aria-hidden`: what a
|
|
1649
1823
|
* screen reader hears is the control's own `required` / `aria-required`, which
|
|
@@ -1717,7 +1891,7 @@ export function Label({ required, size = "md", disabled, className, children, ..
|
|
|
1717
1891
|
* whatever is being read at the time, on every keystroke of a form that re-validates
|
|
1718
1892
|
* as you type.
|
|
1719
1893
|
*/
|
|
1720
|
-
const FIELD_ERROR_CLASS = "mt-1 text-
|
|
1894
|
+
const FIELD_ERROR_CLASS = "mt-1 text-caption leading-tight text-[var(--danger)]";
|
|
1721
1895
|
|
|
1722
1896
|
function useFieldError(
|
|
1723
1897
|
error: ReactNode,
|
|
@@ -1921,7 +2095,7 @@ function CharacterCount({
|
|
|
1921
2095
|
<span
|
|
1922
2096
|
aria-hidden
|
|
1923
2097
|
className={cn(
|
|
1924
|
-
"shrink-0 text-
|
|
2098
|
+
"shrink-0 text-caption leading-tight tabular-nums",
|
|
1925
2099
|
zone === "limit"
|
|
1926
2100
|
? "text-[var(--danger)]"
|
|
1927
2101
|
: zone === "near"
|
|
@@ -2091,7 +2265,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
|
|
|
2091
2265
|
rest.onChange?.(e);
|
|
2092
2266
|
}
|
|
2093
2267
|
: rest.onChange;
|
|
2094
|
-
const asDisplay =
|
|
2268
|
+
const asDisplay = usePhoneLayout() && variant === "display";
|
|
2095
2269
|
// Password fields get a reveal toggle so users can check what they typed.
|
|
2096
2270
|
const isPassword = type === "password";
|
|
2097
2271
|
const [revealed, setRevealed] = useState(false);
|
|
@@ -2124,7 +2298,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
|
|
|
2124
2298
|
disabled={rest.disabled}
|
|
2125
2299
|
aria-label={revealed ? passwordText.hide : passwordText.show}
|
|
2126
2300
|
aria-pressed={revealed}
|
|
2127
|
-
className=
|
|
2301
|
+
className={cn(
|
|
2302
|
+
"absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none disabled:cursor-default disabled:opacity-50",
|
|
2303
|
+
FOCUS_RING,
|
|
2304
|
+
)}
|
|
2128
2305
|
>
|
|
2129
2306
|
{revealed ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
2130
2307
|
</button>
|
|
@@ -2346,7 +2523,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
2346
2523
|
textHint || errorEl !== null ? (
|
|
2347
2524
|
<>
|
|
2348
2525
|
{textHint && (
|
|
2349
|
-
<p id={hintId} className="mt-1 text-
|
|
2526
|
+
<p id={hintId} className="mt-1 text-caption leading-tight text-[var(--text-muted)]">
|
|
2350
2527
|
{hint}
|
|
2351
2528
|
</p>
|
|
2352
2529
|
)}
|
|
@@ -2601,9 +2778,11 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
|
|
|
2601
2778
|
* on the same ATTRIBUTE selector FIELD_BASE uses, for the reason given there. Focus and
|
|
2602
2779
|
* `invalid` change only the border and the ring, which are outside the layer, so they
|
|
2603
2780
|
* need nothing. Both themes come with the variables. The inner corners are the field's
|
|
2604
|
-
* radius (`rounded-md`,
|
|
2605
|
-
* into —
|
|
2606
|
-
* a utility uses it and the kit's token check refuses (token-vars-declared).
|
|
2781
|
+
* radius (`rounded-md`, 0.375rem) less the 1px border, so the rounded border is never
|
|
2782
|
+
* cut into — `calc(0.375rem - 1px)` rather than `var(--radius-md)`, which Tailwind emits
|
|
2783
|
+
* only where a utility uses it and the kit's token check refuses (token-vars-declared).
|
|
2784
|
+
* Since 0.32 in rem (§3.2): the field's own corner grows with the text, so a fixed 5px
|
|
2785
|
+
* left the strip's corner square of the border's at 125 %.
|
|
2607
2786
|
*
|
|
2608
2787
|
* Hidden (`display: none`) until the label floats: an empty, unfocused field has
|
|
2609
2788
|
* nothing that could scroll, and it renders exactly as it did before 0.24. Pointer
|
|
@@ -2616,7 +2795,7 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
|
|
|
2616
2795
|
* is {@link TEXTAREA_LABEL_EDGE} (0.25).
|
|
2617
2796
|
*/
|
|
2618
2797
|
const TEXTAREA_LABEL_STRIP = cn(
|
|
2619
|
-
"pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[
|
|
2798
|
+
"pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[calc(0.375rem-1px)] bg-[var(--bg-surface)]",
|
|
2620
2799
|
"peer-focus:block peer-[:not(:placeholder-shown)]:block",
|
|
2621
2800
|
"peer-disabled:bg-[var(--bg-surface-2)] peer-[[readonly]]:bg-[var(--bg-surface-2)]",
|
|
2622
2801
|
TEXTAREA_LABEL_EDGE,
|
|
@@ -2931,10 +3110,13 @@ const CARD_PADDING: Record<NonNullable<CardProps["padding"]>, string> = {
|
|
|
2931
3110
|
|
|
2932
3111
|
// The same padding one pixel short, for a 2px `toneStrength="strong"` frame: the
|
|
2933
3112
|
// border grows inwards by that pixel, so the content stays where the 1px card had it.
|
|
3113
|
+
// `calc(rem − 1px)` (0.32, §3.2): the padding grows with the text like `p-3` / `p-4`,
|
|
3114
|
+
// the border does not — a fixed 11 / 15 px stopped matching them at 125 %, and toggling
|
|
3115
|
+
// the tone shifted the content.
|
|
2934
3116
|
const CARD_PADDING_STRONG: Record<NonNullable<CardProps["padding"]>, string> = {
|
|
2935
3117
|
none: "p-0",
|
|
2936
|
-
sm: "p-[
|
|
2937
|
-
md: "p-[
|
|
3118
|
+
sm: "p-[calc(0.75rem-1px)]",
|
|
3119
|
+
md: "p-[calc(1rem-1px)]",
|
|
2938
3120
|
};
|
|
2939
3121
|
|
|
2940
3122
|
// The wash as a background IMAGE over the card's opaque `--bg-surface` — see
|
|
@@ -3036,8 +3218,14 @@ export interface CardHeaderProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
3036
3218
|
* description instead of beside them. For an action that is wider than an icon —
|
|
3037
3219
|
* a "Set up two-factor" button, a checkbox and a filter — which, kept in the
|
|
3038
3220
|
* top-end column on a 390px phone, squeezed the title into a ribbon two words wide
|
|
3039
|
-
* (keksdose's settings cards). Off by default: an icon action (the wizard
|
|
3040
|
-
* pencil) is narrow enough to stay beside the title at every width.
|
|
3221
|
+
* (keksdose's settings cards). Off by default at Normal: an icon action (the wizard
|
|
3222
|
+
* summary's pencil) is narrow enough to stay beside the title at every width.
|
|
3223
|
+
*
|
|
3224
|
+
* Unset, it stacks at Large and Extra large only (0.32.1, the 360 px Extra-large
|
|
3225
|
+
* sweep; docs/text-size-harmonization.md §4): there an IconButton shows its label as
|
|
3226
|
+
* text, so the "icon" action is a word-wide pill, and "Refresh sessions" beside
|
|
3227
|
+
* "Sessions" ran 47 px past a 360 px phone. `false` keeps the action beside the title
|
|
3228
|
+
* at every size, for an action the caller knows stays narrow (`labelVisible={false}`).
|
|
3041
3229
|
*/
|
|
3042
3230
|
stackAction?: boolean;
|
|
3043
3231
|
}
|
|
@@ -3057,7 +3245,18 @@ export type CardActionProps = ComponentPropsWithoutRef<"div">;
|
|
|
3057
3245
|
export type CardContentProps = ComponentPropsWithoutRef<"div">;
|
|
3058
3246
|
export type CardFooterProps = ComponentPropsWithoutRef<"div">;
|
|
3059
3247
|
|
|
3060
|
-
|
|
3248
|
+
/** {@link CardHeaderProps.stackAction}: one column under `sm`, and the action back into
|
|
3249
|
+
* the flow (DOM order, so after the description when it is written after it), at the
|
|
3250
|
+
* start edge. The descendant selector outranks CardAction's own placement classes. */
|
|
3251
|
+
const CARD_STACK_ACTION =
|
|
3252
|
+
"max-sm:grid-cols-1 max-sm:[&>[data-slot=card-action]]:col-start-1 max-sm:[&>[data-slot=card-action]]:row-span-1 max-sm:[&>[data-slot=card-action]]:row-start-auto max-sm:[&>[data-slot=card-action]]:justify-self-start";
|
|
3253
|
+
/** The same at Large and Extra large only — `stackAction` unset (see its note). A
|
|
3254
|
+
* `large:` class over the scaled `max-sm`, so it costs no render and is right on the
|
|
3255
|
+
* first paint. */
|
|
3256
|
+
const CARD_STACK_ACTION_AT_LARGE =
|
|
3257
|
+
"large:max-sm:grid-cols-1 large:max-sm:[&>[data-slot=card-action]]:col-start-1 large:max-sm:[&>[data-slot=card-action]]:row-span-1 large:max-sm:[&>[data-slot=card-action]]:row-start-auto large:max-sm:[&>[data-slot=card-action]]:justify-self-start";
|
|
3258
|
+
|
|
3259
|
+
export function CardHeader({ className, stackAction, ...props }: CardHeaderProps) {
|
|
3061
3260
|
const padded = useContext(CardPaddedContext);
|
|
3062
3261
|
const compact = useContext(CardDensityContext) === "compact";
|
|
3063
3262
|
return (
|
|
@@ -3079,11 +3278,8 @@ export function CardHeader({ className, stackAction = false, ...props }: CardHea
|
|
|
3079
3278
|
"grid auto-rows-min items-start gap-1.5 [--card-header-cols:1fr] has-data-[slot=card-action]:[--card-header-cols:1fr_auto] grid-cols-[var(--card-header-cols,1fr)]",
|
|
3080
3279
|
padded ? "px-0 pt-0" : "px-6 pt-6",
|
|
3081
3280
|
compact && "gap-0.5",
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
// The descendant selector outranks CardAction's own placement classes.
|
|
3085
|
-
stackAction &&
|
|
3086
|
-
"max-sm:grid-cols-1 max-sm:[&>[data-slot=card-action]]:col-start-1 max-sm:[&>[data-slot=card-action]]:row-span-1 max-sm:[&>[data-slot=card-action]]:row-start-auto max-sm:[&>[data-slot=card-action]]:justify-self-start",
|
|
3281
|
+
stackAction === true && CARD_STACK_ACTION,
|
|
3282
|
+
stackAction === undefined && CARD_STACK_ACTION_AT_LARGE,
|
|
3087
3283
|
className,
|
|
3088
3284
|
)}
|
|
3089
3285
|
{...props}
|
|
@@ -3544,7 +3740,7 @@ export interface TabsProps<T extends string>
|
|
|
3544
3740
|
* `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the
|
|
3545
3741
|
* badge moves to the row's end.
|
|
3546
3742
|
*
|
|
3547
|
-
* **On a phone (
|
|
3743
|
+
* **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**, `wrap` and
|
|
3548
3744
|
* all, rather than staying a column. A side nav only works beside its content; on
|
|
3549
3745
|
* a phone it has to go ABOVE it, and eight full-width rows there push the panel
|
|
3550
3746
|
* the user picked below the fold on every visit — the two keksdose pages both
|
|
@@ -3573,7 +3769,8 @@ const TABLIST_CLASSES =
|
|
|
3573
3769
|
// admin page's nav and the app's own sidebar mark "you are here" the same way.
|
|
3574
3770
|
const TABLIST_VERTICAL_CLASSES = "flex flex-col gap-0.5";
|
|
3575
3771
|
const TAB_VERTICAL_CLASSES =
|
|
3576
|
-
|
|
3772
|
+
FOCUS_RING_WIDTH +
|
|
3773
|
+
" flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus-visible:ring-[var(--border-strong)]";
|
|
3577
3774
|
const TAB_VERTICAL_ACTIVE_CLASSES = "bg-[var(--bg-active)] text-[var(--text-primary)]";
|
|
3578
3775
|
const TAB_VERTICAL_INACTIVE_CLASSES =
|
|
3579
3776
|
"text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]";
|
|
@@ -3586,7 +3783,8 @@ const TABLIST_WRAP_CLASSES =
|
|
|
3586
3783
|
// than the strip cannot be scrolled into view — its end was simply off screen
|
|
3587
3784
|
// (lenkbank L3). The label and detail truncate inside it instead; see `inner`.
|
|
3588
3785
|
const TAB_CLASSES =
|
|
3589
|
-
|
|
3786
|
+
FOCUS_RING_WIDTH +
|
|
3787
|
+
" max-w-full whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus-visible:ring-[var(--border-strong)]";
|
|
3590
3788
|
const TAB_ACTIVE_CLASSES = "border-[var(--text-primary)] text-[var(--text-primary)]";
|
|
3591
3789
|
const TAB_INACTIVE_CLASSES =
|
|
3592
3790
|
"border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-strong)]";
|
|
@@ -3600,7 +3798,8 @@ const TAB_INACTIVE_CLASSES =
|
|
|
3600
3798
|
// all ten have to stay readable; it is the FILL, not the text weight, that says
|
|
3601
3799
|
// which one is open.
|
|
3602
3800
|
const TAB_WRAP_CLASSES =
|
|
3603
|
-
|
|
3801
|
+
FOCUS_RING_WIDTH +
|
|
3802
|
+
" max-w-full whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm";
|
|
3604
3803
|
const TAB_WRAP_ACTIVE_CLASSES =
|
|
3605
3804
|
"bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]";
|
|
3606
3805
|
const TAB_WRAP_INACTIVE_CLASSES =
|
|
@@ -3612,7 +3811,8 @@ const TAB_WRAP_INACTIVE_CLASSES =
|
|
|
3612
3811
|
const TAB_REMOVABLE_CLASSES = "pe-8 md:pe-8";
|
|
3613
3812
|
const TAB_EMPTY_CLASSES = "-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5";
|
|
3614
3813
|
const TAB_ADD_CLASSES =
|
|
3615
|
-
|
|
3814
|
+
FOCUS_RING_WIDTH +
|
|
3815
|
+
" inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus-visible:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4";
|
|
3616
3816
|
const TAB_ADD_WRAP_CLASSES = "px-2.5 text-xs md:px-3 md:text-sm";
|
|
3617
3817
|
// `relative`: the phone's visually hidden label needs a positioned ancestor (see the
|
|
3618
3818
|
// sr-only rule in CONTRIBUTING); `max-md:px-2` squares the lone "+".
|
|
@@ -3639,7 +3839,7 @@ export function Tabs<T extends string>({
|
|
|
3639
3839
|
}: TabsProps<T>) {
|
|
3640
3840
|
const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
|
|
3641
3841
|
// See `orientation`: a vertical strip is the horizontal one on a phone.
|
|
3642
|
-
const phone =
|
|
3842
|
+
const phone = usePhoneLayout();
|
|
3643
3843
|
const vertical = orientation === "vertical" && !phone;
|
|
3644
3844
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
3645
3845
|
const fade = useStripFade(stripRef);
|
|
@@ -3836,7 +4036,7 @@ export function Tabs<T extends string>({
|
|
|
3836
4036
|
word for a screen reader ("80 km/hloop 2"). */}{" "}
|
|
3837
4037
|
<span
|
|
3838
4038
|
className={cn(
|
|
3839
|
-
"max-w-full truncate text-
|
|
4039
|
+
"max-w-full truncate text-caption font-normal leading-tight text-[var(--text-muted)]",
|
|
3840
4040
|
// On a wrapped strip's filled chip (below `md`) the muted grey
|
|
3841
4041
|
// was dark text on the brand fill — "loop 2" all but vanished
|
|
3842
4042
|
// under the one label that most needed reading. It takes the
|