@eifi1/ui-kit 0.51.4 → 0.51.6
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 +5 -5
- package/dist/account/account-settings-labels.js.map +1 -1
- package/dist/account/account-settings.js +11 -9
- package/dist/account/account-settings.js.map +1 -1
- package/dist/account/delete-account-setting.js +56 -65
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.js +4 -1
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/admin-overview.js +19 -11
- package/dist/admin/admin-overview.js.map +1 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/admin/admin-tool-links.d.ts +1 -1
- package/dist/admin/admin-tool-links.js +7 -1
- package/dist/admin/admin-tool-links.js.map +1 -1
- package/dist/admin/invitations-panel.js +2 -13
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/mail-test-card.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +4 -11
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/user-roster.js +1 -9
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.js +23 -31
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.js +13 -5
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.js +1 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/plan-card.js +91 -85
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-limit-toast.js.map +1 -1
- package/dist/billing/plan-limit.js.map +1 -1
- package/dist/billing/plan-price.js.map +1 -1
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/bin/eifi1-pay-page.mjs +6 -1
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.js +1 -5
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.js +29 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +1 -4
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.js +1 -7
- package/dist/components/button.js.map +1 -1
- package/dist/components/calculator.js +5 -5
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/calendar-heatmap.js +4 -10
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/card.js +1 -8
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-zoom-core.js +3 -17
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chart.js +4 -22
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/chip-input.js +4 -1
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/chip.js +10 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.js +58 -74
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/column-mapper-labels.js.map +1 -1
- package/dist/components/column-mapper.js +167 -227
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox-core.js +1 -4
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +1 -4
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.d.ts +5 -4
- package/dist/components/country-select.js +3 -19
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.js +10 -8
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.js +1 -6
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.js.map +1 -1
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.js +2 -18
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.js +3 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.js +44 -56
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.js +31 -44
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-row-actions.js +19 -16
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.js +4 -1
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.js +30 -39
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +1 -6
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/description-list.js +10 -10
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.js +9 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.js +17 -27
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +25 -35
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-base.js +5 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +1 -1
- package/dist/components/field-parts.js +10 -14
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-sync.js +8 -10
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +4 -1
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/focus-ring.js.map +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/grouped-picker.js +6 -8
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.js +1 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input-labels.js.map +1 -1
- package/dist/components/icon-button.js +4 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/icon-picker.js +2 -7
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/image-grid.js +41 -50
- package/dist/components/image-grid.js.map +1 -1
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -8
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +14 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +1 -7
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-labels.js.map +1 -1
- package/dist/components/legal-page.js +14 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.js +12 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/line-items.js +26 -36
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.js +22 -10
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.js +29 -53
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.js +13 -31
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +10 -21
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +3 -16
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/nav-pills.js +10 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.js +1 -7
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +1 -4
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/one-time-code-input.js +1 -7
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/page-contents.js +13 -22
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.js +1 -11
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/password-strength-labels.js.map +1 -1
- package/dist/components/password-strength.js +2 -8
- package/dist/components/password-strength.js.map +1 -1
- package/dist/components/phone-input.js +42 -57
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +1 -8
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.js +16 -9
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.js +1 -8
- package/dist/components/qr-code.js.map +1 -1
- package/dist/components/row-actions.js +5 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/select.js +28 -38
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.js +1 -5
- package/dist/components/separator.js.map +1 -1
- package/dist/components/series-chart-axes.js +4 -1
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-budget.js.map +1 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.js +29 -45
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.js +21 -17
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.js +4 -9
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/signature-pad.js +4 -13
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/signed-amount.js +12 -5
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/skeleton.js +2 -17
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.js +13 -36
- package/dist/components/slider.js.map +1 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -10
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.js +14 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +1 -1
- package/dist/components/swipeable-row.js +1 -8
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table-skeleton.js +4 -1
- package/dist/components/table-skeleton.js.map +1 -1
- package/dist/components/table.js +12 -9
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.js +5 -24
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/components/tile-radio.js +1 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +6 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +2 -11
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.js +2 -22
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +1 -4
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -1
- package/dist/components/translation-review-parts.js +7 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/translation-review.js +18 -7
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/tree-view.js +14 -10
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/treemap.d.ts +24 -2
- package/dist/components/treemap.js +28 -4
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/use-table-state.js +14 -11
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.js +3 -26
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/demo/demo-start.js +8 -4
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-attachment-labels.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +1 -8
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-capture.d.ts +1 -1
- package/dist/feedback/feedback-capture.js +3 -1
- package/dist/feedback/feedback-capture.js.map +1 -1
- package/dist/feedback/feedback-comment-thread.js +27 -13
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-context.js +1 -4
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +7 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-record.js +1 -5
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.js +25 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-table.js +4 -1
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +2 -13
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/hooks/use-anchored-rect.js.map +1 -1
- package/dist/hooks/use-document-title.js.map +1 -1
- package/dist/hooks/use-file-drop.js.map +1 -1
- package/dist/hooks/use-file-picker-labels.js.map +1 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-overlay-history.js +10 -5
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/hooks/use-row-swipe.d.ts +1 -1
- package/dist/hooks/use-row-swipe.js +1 -6
- package/dist/hooks/use-row-swipe.js.map +1 -1
- package/dist/i18n/defaults.js +1 -4
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.js +2 -8
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.js.map +1 -1
- package/dist/i18n/locales/en.js +4 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +4 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +4 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +4 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +4 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.js +13 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/landing/access.js +1 -4
- package/dist/landing/access.js.map +1 -1
- package/dist/landing/landing-sections.js +35 -38
- package/dist/landing/landing-sections.js.map +1 -1
- package/dist/landing/page-seo.js.map +1 -1
- package/dist/lib/calc.js.map +1 -1
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/decimal-marks.js.map +1 -1
- package/dist/lib/device-settings.js +2 -1
- package/dist/lib/device-settings.js.map +1 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/iban.js.map +1 -1
- package/dist/lib/logger.js +1 -5
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/person-name.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/qr-encode.js +344 -8
- package/dist/lib/qr-encode.js.map +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/table-text.js +15 -3
- package/dist/lib/table-text.js.map +1 -1
- package/dist/lib/text-match.d.ts +4 -0
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/notification-inbox.js +63 -44
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +6 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/push-worker.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/use-push-device.js +4 -1
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/pwa-update-prompt.d.ts +1 -1
- package/dist/offline/pwa-update-prompt.js +7 -1
- package/dist/offline/pwa-update-prompt.js.map +1 -1
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/offline.d.ts +1 -1
- package/dist/offline.js +6 -1
- package/dist/offline.js.map +1 -1
- package/dist/pay/pay.css +8 -1
- package/dist/rhf/fields.d.ts +1 -2
- package/dist/rhf/fields.js +1 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.js +1 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/search/command-palette.js +71 -62
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +34 -5
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/settings/device-reset-setting.js +1 -6
- package/dist/settings/device-reset-setting.js.map +1 -1
- package/dist/settings/settings-heading.js.map +1 -1
- package/dist/settings/settings-layout.js +9 -20
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/settings/use-settings-route.js.map +1 -1
- package/dist/shell/app-shell.js +62 -31
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.js +1 -10
- package/dist/shell/auth-layout.js.map +1 -1
- package/dist/shell/dev-menu.js +12 -6
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/sync-status-indicator.js +7 -6
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +1 -10
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.js +9 -1
- package/dist/shell/tour-menu.js.map +1 -1
- package/dist/sync/db.js.map +1 -1
- package/dist/sync/engine.js +24 -6
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +2 -2
- package/dist/sync.js +7 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/react.js.map +1 -1
- package/dist/testing/viewport.js +7 -4
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/contrast-tokens.js +13 -2
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.d.ts +1 -1
- package/dist/theme/palette-derive.js +18 -29
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +1 -3
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.js +5 -1
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-engine.js +1 -4
- package/dist/tour/tour-engine.js.map +1 -1
- package/dist/tour/tour.js +21 -12
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/wizard/stepper-nav.js +3 -24
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +24 -28
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.js.map +1 -1
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +1 -1
- package/dist/wizard/wizard-step.js +1 -8
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +5 -2
- package/src/account/account-settings-labels.ts +1 -2
- package/src/account/account-settings.tsx +11 -17
- package/src/account/delete-account-setting.tsx +1 -6
- package/src/account/email-change-setting-labels.ts +1 -2
- package/src/account/email-change-setting.tsx +3 -1
- package/src/account/passkeys-setting.tsx +1 -3
- package/src/account/sessions-setting.tsx +4 -2
- package/src/admin/admin-action-confirm.tsx +4 -7
- package/src/admin/admin-action-log.tsx +29 -29
- package/src/admin/admin-area-labels.ts +4 -1
- package/src/admin/admin-overview.tsx +8 -2
- package/src/admin/admin-sections.tsx +3 -1
- package/src/admin/admin-tool-links.tsx +7 -1
- package/src/admin/invitations-panel.tsx +5 -11
- package/src/admin/mail-test-card.tsx +1 -2
- package/src/admin/reviewer-scope-editor.tsx +4 -9
- package/src/admin/role-select.tsx +4 -2
- package/src/admin/user-roster.tsx +2 -11
- package/src/auth/accept-invitation.tsx +1 -7
- package/src/auth/auth-errors.ts +1 -6
- package/src/auth/forgot-password-form.tsx +2 -7
- package/src/auth/register-form.tsx +7 -8
- package/src/auth/reset-password-form.tsx +2 -5
- package/src/auth/sign-in-form.tsx +15 -14
- package/src/auth/verify-email.tsx +2 -7
- package/src/billing/billing-banner.tsx +3 -7
- package/src/billing/billing-labels.ts +6 -4
- package/src/billing/checkout-processing.ts +0 -1
- package/src/billing/plan-card.tsx +9 -8
- package/src/billing/plan-limit-toast.tsx +1 -2
- package/src/billing/plan-limit.ts +1 -2
- package/src/billing/plan-price.ts +1 -4
- package/src/billing/plan-switch.tsx +1 -2
- package/src/billing/subscription-actions.tsx +5 -2
- package/src/components/account-chips.tsx +1 -7
- package/src/components/alert-banner.tsx +2 -8
- package/src/components/amount-input.tsx +206 -178
- package/src/components/authed-image.tsx +7 -4
- package/src/components/autocomplete.tsx +7 -9
- package/src/components/bulk-action-bar.tsx +2 -7
- package/src/components/button-group.tsx +1 -2
- package/src/components/button-look.ts +6 -17
- package/src/components/button.tsx +1 -4
- package/src/components/calculator.tsx +6 -9
- package/src/components/calendar-heatmap.tsx +5 -10
- package/src/components/card.tsx +9 -13
- package/src/components/chart-zoom-core.tsx +6 -33
- package/src/components/chart.tsx +8 -31
- package/src/components/checkbox-group.tsx +1 -4
- package/src/components/chip-input.tsx +6 -6
- package/src/components/chip.tsx +14 -26
- package/src/components/choice-card.tsx +4 -18
- package/src/components/column-mapper-labels.ts +3 -10
- package/src/components/column-mapper.tsx +33 -122
- package/src/components/combobox-core.tsx +12 -42
- package/src/components/combobox.tsx +2 -6
- package/src/components/confirm-dialog.tsx +1 -3
- package/src/components/copy-button.tsx +2 -4
- package/src/components/country-select.tsx +9 -21
- package/src/components/currency-option-list.tsx +13 -14
- package/src/components/currency-select.tsx +2 -7
- package/src/components/danger-confirm.tsx +8 -9
- package/src/components/data-table-cells.tsx +1 -2
- package/src/components/data-table-column-settings.tsx +1 -3
- package/src/components/data-table-filter-popover.tsx +5 -19
- package/src/components/data-table-filters.ts +1 -6
- package/src/components/data-table-fit.ts +1 -3
- package/src/components/data-table-groups.tsx +3 -1
- package/src/components/data-table-head.tsx +27 -45
- package/src/components/data-table-mobile.tsx +99 -108
- package/src/components/data-table-pagination.tsx +2 -6
- package/src/components/data-table-row-actions.tsx +25 -24
- package/src/components/data-table-row.tsx +16 -17
- package/src/components/data-table-shared.tsx +10 -16
- package/src/components/data-table-sort.ts +1 -2
- package/src/components/data-table-virtual.ts +9 -4
- package/src/components/data-table.tsx +489 -501
- package/src/components/date-picker.tsx +1 -7
- package/src/components/date-range-picker.tsx +3 -13
- package/src/components/description-list.tsx +7 -13
- package/src/components/dialog-frame.tsx +12 -3
- package/src/components/disclosure.tsx +8 -12
- package/src/components/dropdown.tsx +3 -11
- package/src/components/empty-state.tsx +1 -3
- package/src/components/entity-combobox.tsx +2 -6
- package/src/components/error-boundary.tsx +7 -7
- package/src/components/field-base.tsx +8 -14
- package/src/components/field-parts.tsx +12 -23
- package/src/components/field-sync.tsx +7 -11
- package/src/components/file-button.tsx +7 -3
- package/src/components/file-dropzone.tsx +7 -12
- package/src/components/floating-panel.tsx +44 -51
- package/src/components/focus-ring.ts +1 -2
- package/src/components/form-actions.tsx +1 -4
- package/src/components/grouped-picker.tsx +6 -8
- package/src/components/hover-menu.tsx +1 -4
- package/src/components/iban-input-labels.ts +1 -2
- package/src/components/icon-button.tsx +46 -48
- package/src/components/icon-picker.tsx +4 -17
- package/src/components/image-grid.tsx +1 -6
- package/src/components/inline-edit-field.tsx +1 -5
- package/src/components/inline-entity-combobox.tsx +6 -21
- package/src/components/input.tsx +14 -2
- package/src/components/jump-calendar.tsx +2 -6
- package/src/components/legal-labels.ts +1 -2
- package/src/components/legal-page.tsx +18 -7
- package/src/components/legal.tsx +14 -5
- package/src/components/lightbox.tsx +1 -3
- package/src/components/line-items.tsx +7 -15
- package/src/components/list.tsx +29 -25
- package/src/components/measured-grid.tsx +8 -30
- package/src/components/menu-item.tsx +1 -3
- package/src/components/mini-calendar.tsx +129 -159
- package/src/components/money-field.tsx +4 -2
- package/src/components/month-picker.tsx +14 -21
- package/src/components/multi-entity-combobox.tsx +6 -26
- package/src/components/multi-select.tsx +5 -9
- package/src/components/nav-pills.tsx +10 -2
- package/src/components/number-field.tsx +18 -15
- package/src/components/number-input.tsx +3 -19
- package/src/components/numpad-sheet.tsx +9 -13
- package/src/components/one-time-code-input.tsx +6 -11
- package/src/components/page-contents.tsx +2 -9
- package/src/components/page-header.tsx +5 -10
- package/src/components/password-strength-labels.ts +1 -2
- package/src/components/password-strength.tsx +6 -9
- package/src/components/phone-input.tsx +2 -13
- package/src/components/picker-sheet.tsx +2 -5
- package/src/components/pie-chart.tsx +6 -12
- package/src/components/popover.tsx +2 -1
- package/src/components/progress-bar.tsx +16 -25
- package/src/components/qr-code.tsx +2 -9
- package/src/components/row-actions.tsx +9 -1
- package/src/components/search-field.tsx +4 -5
- package/src/components/select.tsx +2 -10
- package/src/components/separator.tsx +1 -5
- package/src/components/series-chart-axes.ts +5 -5
- package/src/components/series-chart-budget.ts +1 -4
- package/src/components/series-chart-ticks.ts +1 -4
- package/src/components/series-chart.tsx +43 -83
- package/src/components/settings-fields.tsx +20 -15
- package/src/components/share-card.tsx +6 -14
- package/src/components/sign-chip.tsx +16 -16
- package/src/components/signature-pad.tsx +21 -13
- package/src/components/signed-amount.tsx +6 -14
- package/src/components/skeleton.tsx +2 -13
- package/src/components/slider.tsx +9 -20
- package/src/components/sparkline.tsx +2 -13
- package/src/components/stat-tile.tsx +13 -24
- package/src/components/swatch-picker.tsx +8 -3
- package/src/components/swipeable-row.tsx +2 -11
- package/src/components/table-skeleton.tsx +4 -1
- package/src/components/table.tsx +12 -8
- package/src/components/tabs.tsx +24 -39
- package/src/components/text-link.tsx +6 -6
- package/src/components/text.tsx +1 -2
- package/src/components/tile-radio.tsx +6 -19
- package/src/components/time-input.tsx +4 -5
- package/src/components/toast.tsx +3 -12
- package/src/components/toggle-group.tsx +9 -9
- package/src/components/toggle-legend.tsx +4 -16
- package/src/components/tooltip.tsx +9 -48
- package/src/components/translation-review-editor.tsx +4 -4
- package/src/components/translation-review-parts.tsx +7 -1
- package/src/components/translation-review.tsx +15 -16
- package/src/components/tree-view.tsx +17 -20
- package/src/components/treemap.tsx +59 -9
- package/src/components/trigger-aria.ts +1 -4
- package/src/components/use-table-state.ts +16 -17
- package/src/components/user-avatar.tsx +4 -21
- package/src/demo/demo-start.tsx +8 -4
- package/src/feedback/feedback-attachment-labels.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +16 -29
- package/src/feedback/feedback-capture.ts +3 -1
- package/src/feedback/feedback-comment-thread.tsx +9 -6
- package/src/feedback/feedback-context.tsx +1 -4
- package/src/feedback/feedback-dialog.tsx +3 -4
- package/src/feedback/feedback-inbox.tsx +3 -9
- package/src/feedback/feedback-public-list.tsx +7 -1
- package/src/feedback/feedback-record.ts +6 -14
- package/src/feedback/feedback-replies.tsx +10 -5
- package/src/feedback/feedback-row-detail.tsx +2 -5
- package/src/feedback/feedback-status-undo.ts +1 -4
- package/src/feedback/feedback-submit-dialog.tsx +4 -4
- package/src/feedback/feedback-table.tsx +12 -8
- package/src/feedback/feedback-thread.tsx +4 -16
- package/src/feedback/feedback-vocabulary.ts +72 -82
- package/src/feedback/feedback-vote-button.tsx +9 -8
- package/src/hooks/use-anchored-rect.ts +1 -4
- package/src/hooks/use-document-title.ts +3 -1
- package/src/hooks/use-file-drop.ts +1 -3
- package/src/hooks/use-file-picker-labels.ts +1 -2
- package/src/hooks/use-file-picker.tsx +1 -5
- package/src/hooks/use-focus-trap.ts +1 -2
- package/src/hooks/use-overlay-history.ts +10 -5
- package/src/hooks/use-row-swipe.ts +1 -6
- package/src/i18n/defaults.ts +1 -4
- package/src/i18n/german.ts +33 -49
- package/src/i18n/kit-labels.tsx +5 -20
- package/src/i18n/languages.ts +2 -8
- package/src/i18n/locales/en.ts +11 -31
- package/src/i18n/locales/es.ts +35 -49
- package/src/i18n/locales/fr.ts +34 -51
- package/src/i18n/locales/hu.ts +26 -37
- package/src/i18n/locales/it.ts +35 -52
- package/src/i18n/locales/zh.ts +14 -32
- package/src/i18n/plurals.ts +4 -1
- package/src/i18n/review.ts +18 -7
- package/src/i18n/shared-labels.ts +1 -2
- package/src/index.ts +106 -32
- package/src/landing/access.ts +2 -7
- package/src/landing/landing-sections.tsx +14 -15
- package/src/landing/page-seo.ts +1 -3
- package/src/lib/calc.ts +4 -1
- package/src/lib/clipboard-minus.ts +4 -1
- package/src/lib/dates.ts +2 -10
- package/src/lib/decimal-marks.ts +1 -3
- package/src/lib/device-settings.ts +2 -1
- package/src/lib/direction.ts +1 -3
- package/src/lib/iban.ts +89 -9
- package/src/lib/logger.ts +1 -5
- package/src/lib/person-name.ts +1 -3
- package/src/lib/phone.ts +5 -8
- package/src/lib/qr-encode.ts +32 -8
- package/src/lib/session-refresh.ts +5 -6
- package/src/lib/table-text.ts +24 -6
- package/src/lib/text-match.ts +15 -3
- package/src/lib/translation-review.ts +1 -3
- package/src/notifications/notification-inbox.tsx +17 -4
- package/src/notifications/notification-parts.tsx +1 -2
- package/src/notifications/notification-types-setting.tsx +19 -18
- package/src/notifications/notifications-labels.ts +1 -2
- package/src/notifications/push-device-setting.tsx +7 -1
- package/src/notifications/push-devices-setting.tsx +7 -3
- package/src/notifications/push-worker.ts +10 -2
- package/src/notifications/quiet-hours-setting.tsx +4 -1
- package/src/notifications/use-push-device.ts +4 -1
- package/src/offline/pwa-update-prompt.tsx +7 -1
- package/src/offline/query-persistence.ts +9 -6
- package/src/offline.ts +6 -1
- package/src/rhf/fields.tsx +50 -19
- package/src/rhf/form-core.tsx +2 -6
- package/src/rhf/line-items.tsx +5 -7
- package/src/rhf/use-rhf-wizard-step.ts +3 -12
- package/src/search/command-palette.tsx +15 -23
- package/src/search/global-search.tsx +38 -8
- package/src/search/search-index.ts +5 -1
- package/src/settings/device-reset-setting.tsx +5 -8
- package/src/settings/settings-heading.tsx +3 -1
- package/src/settings/settings-layout.tsx +6 -13
- package/src/settings/use-settings-route.ts +4 -3
- package/src/shell/app-shell.tsx +48 -24
- package/src/shell/auth-layout.tsx +1 -6
- package/src/shell/dev-menu.tsx +12 -6
- package/src/shell/option-switcher-menu.tsx +1 -3
- package/src/shell/sync-status-indicator.tsx +14 -12
- package/src/shell/top-bar-parts-labels.ts +1 -3
- package/src/shell/topbar-action-menu.tsx +9 -8
- package/src/shell/topbar-controls.tsx +5 -19
- package/src/shell/tour-menu.tsx +9 -1
- package/src/sync/db.ts +5 -1
- package/src/sync/engine.ts +44 -12
- package/src/sync.ts +16 -2
- package/src/testing/css-variables.ts +1 -2
- package/src/testing/index.ts +1 -6
- package/src/testing/react.ts +1 -2
- package/src/testing/viewport.ts +9 -9
- package/src/theme/contrast-tokens.ts +13 -2
- package/src/theme/palette-derive.ts +24 -50
- package/src/theme/palette-presets.ts +0 -1
- package/src/theme/palette-store.ts +1 -4
- package/src/theme/text-size.ts +1 -1
- package/src/theme/theme-store.ts +2 -6
- package/src/tour/tour-catalogue.tsx +4 -2
- package/src/tour/tour-dock.ts +5 -1
- package/src/tour/tour-engine.ts +2 -6
- package/src/tour/tour.tsx +28 -12
- package/src/tour/use-tour-progress.ts +8 -2
- package/src/tour/use-tour-url-sync.ts +3 -1
- package/src/vite-env.d.ts +1 -4
- package/src/wizard/stepper-nav.tsx +10 -35
- package/src/wizard/types.ts +1 -4
- package/src/wizard/use-wizard.ts +28 -43
- package/src/wizard/validation.ts +1 -5
- package/src/wizard/wizard-context.tsx +2 -6
- package/src/wizard/wizard-step.tsx +1 -8
- package/src/wizard/wizard-summary.tsx +2 -6
- package/tokens.css +393 -152
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/table.tsx"],"sourcesContent":["import { Children, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { assignRef } from \"./field-parts\";\nimport { THIN_SCROLLBAR_CLASS, useScrollOverflow } from \"./scroll-area\";\n\n/** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set\n * inside a card's own padding whose columns are spaced by hand (`pl-2` on exactly the\n * columns that need it). Type stays at the body size, as `comfortable`. */\nexport type TableDensity = \"comfortable\" | \"compact\" | \"none\";\n/** CSS `table-layout`. `auto` (the browser default) sizes columns to their content;\n * `fixed` takes widths from the first row and ignores the rest — keksdose's VAT\n * summary switches to it while editing so two inputs stop bidding against the\n * figures for room. */\nexport type TableLayout = \"auto\" | \"fixed\";\nexport type TableAlign = \"start\" | \"center\" | \"end\";\n/**\n * Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a\n * head cell sits on the `bottom` (a wrapped heading ends on the line above the\n * figures), a body cell at the `top` (a wrapped note starts level with its row). A\n * table whose cells are INPUTS wants `middle`: keksdose's VAT summary\n * (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,\n * and a figure at the top of its cell sat above the middle of the field next to it —\n * so it writes `<tr className=\"align-middle\">` over raw `<th>`/`<td>`, because the\n * kit's cells each stated `align-top` and a class on the row could not reach them.\n */\nexport type TableVAlign = \"top\" | \"middle\" | \"bottom\";\n/** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */\nexport type TableHeaderCellSize = \"xs\" | \"sm\";\n/** The header cell's weight. Default `medium`. */\nexport type TableHeaderCellWeight = \"normal\" | \"medium\" | \"semibold\";\n\n/**\n * What a {@link TableRow} is in a statement-style table (0.12.0): an ordinary `row`, a\n * `group` heading (\"Current assets\", a tinted full-width label row), a `subtotal` under\n * a group, or the `total` of the whole table. kastlan's balance sheet, income statement\n * and journal entry each spelled these as `className=\"font-medium\"`, `\"font-bold\n * border-t-2\"` and `bg-muted/50` on the cell — three pages, three sets of classes for\n * the same four kinds of line.\n */\nexport type TableRowVariant = \"row\" | \"group\" | \"subtotal\" | \"total\";\n\ninterface TableContextValue {\n density: TableDensity;\n zebra: boolean;\n hover: boolean;\n rowDividers: boolean;\n captionId: string;\n registerCaption: () => () => void;\n}\n\nconst TableContext = createContext<TableContextValue>({\n density: \"comfortable\",\n zebra: false,\n hover: false,\n rowDividers: true,\n captionId: \"\",\n registerCaption: () => () => {},\n});\n\ntype TableSection = \"head\" | \"body\" | \"foot\";\n\n/** Which section a row sits in — zebra and hover apply to body rows only. `undefined`\n * outside the kit's section parts: a raw `<thead>` / `<tbody>`, which\n * {@link useSection} then reads off the DOM. */\nconst SectionContext = createContext<TableSection | undefined>(undefined);\n\nconst SECTION_OF_TAG: Record<string, TableSection> = { THEAD: \"head\", TBODY: \"body\", TFOOT: \"foot\" };\n\n/**\n * The section a row or cell sits in: the kit part's context where there is one, else the\n * nearest `<thead>` / `<tbody>` / `<tfoot>` in the DOM.\n *\n * keksdose's VAT summary writes a raw `<thead>` round kit cells in places, and a\n * `TableHeaderCell` there took itself for a BODY cell — `scope=\"row\"`, body type — with\n * nothing on the page to say why. React cannot see an ancestor's tag, so the fallback\n * reads it after mount; a layout effect, so the corrected cell is what gets painted.\n */\nfunction useSection<E extends HTMLElement>(): [TableSection, ((el: E | null) => void) | undefined] {\n const fromContext = useContext(SectionContext);\n const [node, setNode] = useState<E | null>(null);\n const [detected, setDetected] = useState<TableSection | undefined>(undefined);\n useLayoutEffect(() => {\n if (fromContext !== undefined || !node) return;\n const section = node.closest(\"thead, tbody, tfoot\");\n const found = section ? SECTION_OF_TAG[section.tagName] : undefined;\n // eslint-disable-next-line react-hooks/set-state-in-effect -- read off the DOM, which only exists after mount\n if (found !== detected) setDetected(found);\n }, [fromContext, node, detected]);\n // No ref at all under a kit section: holding the node would cost every row and cell\n // a second render on mount for an answer the context already gave.\n return [fromContext ?? detected ?? \"body\", fromContext === undefined ? setNode : undefined];\n}\n\n/**\n * One callback ref for the caller's `ref` and the part's own (0.16.0). Every part takes\n * a `ref` to its DOM element — React 19 hands it to a function component as a prop —\n * and the parts that hold the node themselves (the table, a row or a head cell reading\n * its section off the DOM) must still get it when the caller passes one too.\n */\nfunction useMergedRef<E>(own: ((el: E | null) => void) | { current: E | null } | undefined, ref: Ref<E> | undefined) {\n return useCallback(\n (node: E | null) => {\n assignRef(own as Ref<E> | undefined, node);\n assignRef(ref, node);\n },\n [own, ref],\n );\n}\n\n/** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It\n * cannot be left to CSS inheritance: every cell states its own default alignment,\n * and a class on the cell beats one on the row. */\nconst RowVAlignContext = createContext<TableVAlign | undefined>(undefined);\n\nconst VALIGN: Record<TableVAlign, string> = {\n top: \"align-top\",\n middle: \"align-middle\",\n bottom: \"align-bottom\",\n};\n\nconst HEADER_SIZE: Record<TableHeaderCellSize, string> = {\n xs: \"text-xs\",\n sm: \"text-sm\",\n};\n\nconst HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n};\n\nconst CELL_PAD: Record<TableDensity, string> = {\n comfortable: \"px-3 py-2\",\n compact: \"px-2 py-1\",\n none: \"p-0\",\n};\n\nconst ALIGN: Record<TableAlign, string> = {\n start: \"text-start\",\n center: \"text-center\",\n end: \"text-end\",\n};\n\n/**\n * The alignment a figure column wants: end-aligned (so the units line up under each\n * other in either reading direction) with tabular digits (so they line up at all).\n * Exported for a cell the kit did not render — a DataTable column, a `<td>` of the\n * app's own.\n */\nexport const NUMERIC_CELL_CLASS = \"text-end tabular-nums\";\n\n/**\n * Every part's props are `ComponentPropsWithRef` (0.16.0), so `ref` reaches the DOM\n * element: keksdose scrolled a budget row into view through `useId` + `getElementById`\n * because `<TableRow ref>` did not type-check.\n */\nexport interface TableProps extends ComponentPropsWithRef<\"table\"> {\n /** `compact` for a dense detail view (a ledger, a spec sheet). */\n density?: TableDensity;\n /** Tint every other body row. */\n zebra?: boolean;\n /** Tint the body row under the pointer. Off by default: a static table whose rows\n * light up promises a click that does nothing. With `zebra`, a tinted row hovers to\n * the card colour (0.33): the hover tint is too close to the well. */\n hover?: boolean;\n /** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */\n wrapperClassName?: string;\n /** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */\n layout?: TableLayout;\n /**\n * A rounded border round the whole table (0.12.0) — for a table standing on the page\n * or in a dialog rather than inside a card. kastlan wraps six of them in\n * `<div className=\"rounded-md border\">` by hand (payment allocation, the new-budget\n * lines, the meter readings, the deposit transactions, the unit values editor, the\n * maintenance detail), and a hand-made frame outside the scroll wrapper does not clip\n * the rows' tint to its corners.\n */\n framed?: boolean;\n /**\n * The rule between body rows. Default true. `false` for a small table whose rows are\n * spaced by their own content — keksdose's VAT summary, a block of three quiet\n * columns inside a card — which otherwise had to drop to raw `<tr>`s to lose it. One\n * row can still say otherwise with {@link TableRowProps.bordered}.\n */\n rowDividers?: boolean;\n /**\n * `\"phone\"`: below `sm` each body row becomes a block — its first cell the row's\n * title, every further cell under its column's header as a small label — and the\n * head row is visually hidden (still read by screen readers). For tables of PROSE,\n * not figures (lenkbank L5: a PID-terms table of three sentence columns wrapped into\n * two-word slivers at 390px and ran its third column off the screen). A table of\n * numbers should keep scrolling sideways, which is why this is opt-in. The labels\n * are the head row's cell texts, read from the DOM; a body cell spanning several\n * columns gets none.\n */\n stack?: \"phone\";\n /**\n * Fade out an edge of the scroll wrapper while columns are hidden behind it (0.25).\n *\n * A table wider than its box scrolls sideways inside the wrapper, and on a phone —\n * whose scrollbars are overlays that show only while you drag — nothing said there\n * was more: the last visible column simply ended at the edge, often mid-word. 0.24\n * drew a fade for ColumnRoleTable's preview (Kurvenschmiede: the fourth column's\n * role select sat off-screen with no hint) by wrapping this Table and reaching its\n * wrapper as the `<table>`'s `parentElement`; this is that fade as the Table's own,\n * so a table needs neither the reach-around nor a box of its own round it.\n *\n * It is the kit Tabs strip's fade, MEASURED rather than assumed: nothing is drawn\n * while the table fits, so a table that fits paints exactly as without the prop;\n * the end with columns behind it fades, and both ends once scrolled into the\n * middle; the ends are the reading direction's, so in RTL the hidden columns fade\n * on the left. It is a `mask-image` — no layout change, no background colour to\n * fade to, so it works on any surface in either theme — and never animated, so\n * there is nothing for reduced motion to turn off. The wrapper's own scrollbars\n * stay out of it (a fade on a classic scrollbar hid the very thing that says the\n * box scrolls).\n *\n * Focus that lands in a cell behind the fade — a link, a select, a checkbox — is\n * scrolled clear of it, cell and all where the cell fits; the browser's own scroll\n * stops at the edge, under the fade. And while the overflowing wrapper ITSELF holds\n * keyboard focus (it is a tab stop then, for arrow-key scrolling) the fade lifts, so\n * the focus outline round the box is drawn whole rather than faded out at its sides.\n *\n * The wrapper carries `data-overflow` — `\"start\"`, `\"end\"` or `\"both\"`, in\n * reading-direction terms, absent while it fits — for a caller's own affordance\n * (a scroll button) and for tests, which cannot read a mask.\n *\n * Off by default: it changes how every overflowing table looks.\n */\n edgeFade?: boolean;\n}\n\n/**\n * Below `sm` on a `stack=\"phone\"` table: rows as blocks, the head visually hidden,\n * the first cell as a title, the others labelled by `data-label` (set by the table).\n */\nconst STACK_PHONE_CLASSES = cn(\n \"max-sm:block max-sm:[&_tbody]:block max-sm:[&_tfoot]:block\",\n \"max-sm:[&_thead]:sr-only\",\n \"max-sm:[&_tbody_tr]:block max-sm:[&_tbody_tr]:py-2 max-sm:[&_tfoot_tr]:block max-sm:[&_tfoot_tr]:py-2\",\n \"max-sm:[&_tbody_td]:block max-sm:[&_tbody_td]:px-0 max-sm:[&_tbody_td]:py-0.5 max-sm:[&_tbody_td]:text-start\",\n \"max-sm:[&_tfoot_td]:block max-sm:[&_tfoot_td]:px-0 max-sm:[&_tfoot_td]:py-0.5\",\n \"max-sm:[&_tbody_td:first-child]:font-medium\",\n \"max-sm:[&_td[data-label]]:before:block max-sm:[&_td[data-label]]:before:text-caption max-sm:[&_td[data-label]]:before:font-medium max-sm:[&_td[data-label]]:before:text-muted max-sm:[&_td[data-label]]:before:content-[attr(data-label)]\",\n);\n\n/**\n * A plain, static HTML table in the kit's tokens — for the detail views that need\n * rows and columns and none of {@link DataTable}'s sorting, filtering or paging.\n *\n * kastlan renders about twenty of these on shadcn's Table parts; the part names here\n * are the same idea (`TableHead` is the `<thead>`, `TableHeaderCell` the `<th>`), so\n * the move is a rename rather than a rewrite.\n *\n * The table sits in an overflow-x wrapper, because a table cannot shrink below its\n * content and on a phone it would otherwise widen the page. While it overflows that\n * wrapper becomes a tab stop and a `role=\"region\"` named by the `TableCaption` (or the\n * table's own `aria-label`), so a keyboard user can scroll it and a reader can say\n * what it is — the same contract as {@link ScrollArea}.\n *\n * Alignment is logical throughout (`text-start` / `text-end`), so a right-to-left\n * page gets its first column on the right without a class changing.\n */\nexport function Table({\n density = \"comfortable\",\n zebra = false,\n hover = false,\n wrapperClassName,\n layout,\n framed = false,\n rowDividers = true,\n stack,\n edgeFade = false,\n className,\n \"aria-label\": ariaLabel,\n ref,\n ...rest\n}: TableProps) {\n const captionId = useId();\n const [captions, setCaptions] = useState(0);\n const wrapper = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(wrapper);\n const fade = useEdgeFade(wrapper, edgeFade);\n const tableRef = useRef<HTMLTableElement | null>(null);\n const setTableRef = useMergedRef(tableRef, ref);\n\n // `stack=\"phone\"`: label every body cell with its column's header text, and keep the\n // table's roles explicit — `display: block` on table parts drops their table\n // semantics in some engines. Re-run on any change to the rows, which the caller\n // renders and this component never sees.\n useLayoutEffect(() => {\n const table = tableRef.current;\n if (!stack || !table) return;\n const label = () => {\n const heads = Array.from(table.tHead?.rows[0]?.cells ?? []).map((cell) => cell.textContent?.trim() ?? \"\");\n table.setAttribute(\"role\", \"table\");\n for (const row of Array.from(table.rows)) {\n row.setAttribute(\"role\", \"row\");\n let column = 0;\n for (const cell of Array.from(row.cells)) {\n const span = cell.colSpan || 1;\n if (cell.tagName === \"TH\") cell.setAttribute(\"role\", cell.closest(\"thead\") ? \"columnheader\" : \"rowheader\");\n else cell.setAttribute(\"role\", \"cell\");\n const text = span === 1 && column > 0 ? heads[column] : undefined;\n if (cell.tagName === \"TD\" && text) cell.setAttribute(\"data-label\", text);\n else cell.removeAttribute(\"data-label\");\n column += span;\n }\n }\n };\n label();\n const observer = typeof MutationObserver === \"undefined\" ? null : new MutationObserver(label);\n observer?.observe(table, { childList: true, subtree: true, characterData: true });\n return () => observer?.disconnect();\n }, [stack]);\n\n const ctx = useMemo<TableContextValue>(\n () => ({\n density,\n zebra,\n hover,\n rowDividers,\n captionId,\n registerCaption: () => {\n setCaptions((n) => n + 1);\n return () => setCaptions((n) => n - 1);\n },\n }),\n [density, zebra, hover, rowDividers, captionId],\n );\n\n const regionName = captions > 0 ? { \"aria-labelledby\": captionId } : ariaLabel ? { \"aria-label\": ariaLabel } : null;\n\n return (\n <TableContext.Provider value={ctx}>\n <div\n ref={wrapper}\n // Only a region when it scrolls AND has a name: a landmark for every short\n // table on a page would bury the real ones.\n role={overflowing && regionName ? \"region\" : undefined}\n {...(overflowing ? regionName : null)}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing || undefined}\n data-clips=\"\"\n data-framed={framed || undefined}\n // `edgeFade`: the mask, `data-overflow` and the focus handling; nothing while off.\n {...fade}\n className={cn(\n // `relative`: the containing block for an `sr-only` caption.\n \"relative w-full overflow-x-auto\",\n // On the scroll wrapper itself, so its clipping keeps the rows inside the\n // rounded corners.\n framed && \"rounded-md border border-subtle\",\n THIN_SCROLLBAR_CLASS,\n \"focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-brand\",\n wrapperClassName,\n )}\n >\n <table\n {...rest}\n ref={setTableRef}\n aria-label={ariaLabel}\n className={cn(\n \"w-full caption-bottom border-collapse text-sm text-primary\",\n density === \"compact\" && \"text-xs\",\n layout === \"fixed\" && \"table-fixed\",\n layout === \"auto\" && \"table-auto\",\n stack === \"phone\" && STACK_PHONE_CLASSES,\n className,\n )}\n />\n </div>\n </TableContext.Provider>\n );\n}\n\nexport interface TableHeadProps extends ComponentPropsWithRef<\"thead\"> {\n /** The rule under the head. Default true; `false` for a quiet header over a small\n * table (keksdose's VAT summary wrote `className=\"border-b-0\"` to lose it). */\n bordered?: boolean;\n}\n\nexport function TableHead({ bordered = true, className, ...rest }: TableHeadProps) {\n return (\n <SectionContext.Provider value=\"head\">\n <thead {...rest} className={cn(bordered && \"border-b border-subtle\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport interface TableBodyProps extends ComponentPropsWithRef<\"tbody\"> {\n /**\n * Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —\n * `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-\n * writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda\n * and invitations tabs, the journal-entry and invoice detail pages, and a column\n * added later leaves every one of those counts one short.\n */\n empty?: ReactNode;\n}\n\nexport function TableBody({ className, empty, children, ...rest }: TableBodyProps) {\n // `toArray` drops null/undefined/booleans and flattens arrays, so `[[], false]` —\n // an empty map beside a conditional row that is off — is no rows.\n const hasRows = Children.toArray(children).length > 0;\n return (\n <SectionContext.Provider value=\"body\">\n <tbody {...rest} className={cn(\"[&>tr:last-child]:border-b-0\", className)}>\n {hasRows || empty === undefined || empty === null || empty === false ? (\n children\n ) : (\n <TableEmpty>{empty}</TableEmpty>\n )}\n </tbody>\n </SectionContext.Provider>\n );\n}\n\nexport interface TableEmptyProps extends Omit<ComponentPropsWithRef<\"tr\">, \"children\"> {\n children: ReactNode;\n /** Columns to span. Default: MEASURED — the widest row of the table it renders in,\n * counted after mount, so the row stays full width when a column is added. */\n colSpan?: number;\n /** Classes for the `<td>`. */\n cellClassName?: string;\n}\n\n/**\n * The \"nothing here\" row: one cell across every column, centred, muted. What\n * {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows\n * are not its direct children.\n */\nexport function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps) {\n const { density } = useContext(TableContext);\n const cell = useRef<HTMLTableCellElement | null>(null);\n const [measured, setMeasured] = useState(1);\n // Every render, deliberately: a column can appear in the head without anything this\n // row depends on changing. It cannot loop — setting the same count is a bail-out.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (colSpan !== undefined) return;\n const row = cell.current?.parentElement;\n const table = cell.current?.closest(\"table\");\n if (!row || !table) return;\n let widest = 1;\n for (const r of Array.from(table.rows)) {\n if (r === row) continue;\n let n = 0;\n for (const c of Array.from(r.cells)) n += c.colSpan || 1;\n widest = Math.max(widest, n);\n }\n setMeasured(widest);\n });\n return (\n <tr {...rest} data-table-empty=\"\" className={className}>\n <td\n ref={cell}\n colSpan={colSpan ?? measured}\n className={cn(\n // `none` keeps the message off the edges regardless: it is text, not a figure.\n density === \"none\" ? \"py-2\" : CELL_PAD[density],\n \"text-center text-muted\",\n cellClassName,\n )}\n >\n {children}\n </td>\n </tr>\n );\n}\n\nexport type TableFootProps = ComponentPropsWithRef<\"tfoot\">;\n\n/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`\n * is what tells a reader it is a total rather than one more row. */\nexport function TableFoot({ className, ...rest }: TableFootProps) {\n return (\n <SectionContext.Provider value=\"foot\">\n <tfoot {...rest} className={cn(\"border-t border-strong font-medium\", className)} />\n </SectionContext.Provider>\n );\n}\n\nconst ROW_VARIANT: Record<TableRowVariant, string | false> = {\n row: false,\n group: \"bg-surface-2 font-semibold\",\n subtotal: \"font-medium\",\n // A 2px rule over the 1px divider the row above draws on its bottom: collapsed\n // borders meet on one line, the wider wins, and the heavier rule is what says \"sum\".\n total: \"border-t-2 border-t-strong font-semibold\",\n};\n\n/** The deprecated HTML `valign` attribute is replaced by a class-backed one. */\nexport interface TableRowProps extends ComponentPropsWithRef<\"tr\"> {\n /** Vertical alignment for every cell of the row that does not set its own. See\n * {@link TableVAlign}. Left out, each cell keeps its default. */\n valign?: TableVAlign;\n /** See {@link TableRowVariant}. Default `row`. Zebra and hover tint ordinary rows\n * only: a group heading or a total is not one more record. */\n variant?: TableRowVariant;\n /** The rule under this body row. Default: the table's `rowDividers`. */\n bordered?: boolean;\n}\n\nexport function TableRow({ valign, variant = \"row\", bordered, className, ref: refProp, ...rest }: TableRowProps) {\n const { zebra, hover, rowDividers } = useContext(TableContext);\n const [section, sectionRef] = useSection<HTMLTableRowElement>();\n const ref = useMergedRef(sectionRef, refProp);\n const body = section === \"body\";\n const record = body && variant === \"row\";\n const row = (\n <tr\n ref={ref}\n {...rest}\n data-variant={variant === \"row\" ? undefined : variant}\n className={cn(\n body && (bordered ?? rowDividers) && \"border-b border-subtle\",\n record && zebra && \"even:bg-surface-2\",\n record && hover && \"transition-colors hover:bg-hover\",\n // A zebra row rests on the well, and `--bg-hover` sits only 1.00–1.02:1 from it\n // (0.33: the light well is the page colour): it hovers to the card instead, the\n // one fill that both steps 1.09–1.13:1 from the well and keeps every text role at\n // 4.5:1 (docs/colour-roles-harmonization.md §12.2). `even:hover:` outranks the\n // row's own `hover:`.\n record && zebra && hover && \"even:hover:bg-surface\",\n ROW_VARIANT[variant],\n // On the row as well, for a raw `<td>` of the caller's, which inherits it.\n valign && VALIGN[valign],\n className,\n )}\n />\n );\n return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;\n}\n\ninterface CellAlignProps {\n /** Shorthand for a figure column: end-aligned, tabular digits. */\n numeric?: boolean;\n /** Logical alignment. Default `start` (`end` when `numeric`). */\n align?: TableAlign;\n /** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and\n * `top` for a body cell. See {@link TableVAlign}. */\n valign?: TableVAlign;\n}\n\n/** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */\nexport interface TableHeaderCellProps extends Omit<ComponentPropsWithRef<\"th\">, \"align\" | \"valign\">, CellAlignProps {\n /**\n * Type size. Left out: `xs` in the head, the table's own size in the body — as\n * before. keksdose's VAT summary heads its columns at the body size and normal weight\n * (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a\n * small table of figures, and could not say so without overriding classes.\n */\n size?: TableHeaderCellSize;\n /** Default `medium`. `normal` for the quiet header above; see {@link size}. */\n weight?: TableHeaderCellWeight;\n}\n\n/**\n * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header\n * cell in a body row is the row's label (\"Net rent\" in a key-value table), and a\n * reader needs the scope to read it with each cell beside it.\n */\nexport function TableHeaderCell({\n numeric = false,\n align,\n valign,\n size,\n weight = \"medium\",\n scope,\n className,\n ref: refProp,\n ...rest\n}: TableHeaderCellProps) {\n const { density } = useContext(TableContext);\n const [section, sectionRef] = useSection<HTMLTableCellElement>();\n const ref = useMergedRef(sectionRef, refProp);\n const rowVAlign = useContext(RowVAlignContext);\n const head = section === \"head\";\n const textSize = size ?? (head ? \"xs\" : undefined);\n return (\n <th\n ref={ref}\n {...rest}\n scope={scope ?? (section === \"head\" ? \"col\" : \"row\")}\n className={cn(\n CELL_PAD[density],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums\",\n VALIGN[valign ?? rowVAlign ?? (head ? \"bottom\" : \"top\")],\n HEADER_WEIGHT[weight],\n head ? \"text-muted\" : \"text-secondary\",\n textSize && HEADER_SIZE[textSize],\n className,\n )}\n />\n );\n}\n\nexport interface TableCellProps extends Omit<ComponentPropsWithRef<\"td\">, \"align\" | \"valign\">, CellAlignProps {}\n\nexport function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {\n const { density } = useContext(TableContext);\n const rowVAlign = useContext(RowVAlignContext);\n return (\n <td\n {...rest}\n className={cn(\n CELL_PAD[density],\n VALIGN[valign ?? rowVAlign ?? \"top\"],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums whitespace-nowrap\",\n className,\n )}\n />\n );\n}\n\nexport type TableCaptionProps = ComponentPropsWithRef<\"caption\">;\n\n/**\n * The table's title. Beneath the table by default (`caption-bottom`, the shadcn\n * convention kastlan's views already follow); `className=\"caption-top\"` moves it.\n * `className=\"sr-only\"` keeps it for readers only — the wrapper is `relative`, so\n * the hidden caption stays inside it.\n *\n * It also names the scroll region when the table overflows: it registers itself\n * with the {@link Table} so the wrapper points at an id that exists, and never at\n * one that does not.\n */\nexport function TableCaption({ className, id, ...rest }: TableCaptionProps) {\n const { captionId, registerCaption } = useContext(TableContext);\n useLayoutEffect(() => (id ? undefined : registerCaption()), [id, registerCaption]);\n return (\n <caption\n {...rest}\n id={id ?? captionId}\n className={cn(\"mt-2 text-start text-sm text-muted\", className)}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/table.tsx"],"sourcesContent":["import {\n Children,\n createContext,\n useCallback,\n useContext,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ComponentPropsWithRef, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { assignRef } from \"./field-parts\";\nimport { THIN_SCROLLBAR_CLASS, useScrollOverflow } from \"./scroll-area\";\n\n/** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set\n * inside a card's own padding whose columns are spaced by hand (`pl-2` on exactly the\n * columns that need it). Type stays at the body size, as `comfortable`. */\nexport type TableDensity = \"comfortable\" | \"compact\" | \"none\";\n/** CSS `table-layout`. `auto` (the browser default) sizes columns to their content;\n * `fixed` takes widths from the first row and ignores the rest — keksdose's VAT\n * summary switches to it while editing so two inputs stop bidding against the\n * figures for room. */\nexport type TableLayout = \"auto\" | \"fixed\";\nexport type TableAlign = \"start\" | \"center\" | \"end\";\n/**\n * Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a\n * head cell sits on the `bottom` (a wrapped heading ends on the line above the\n * figures), a body cell at the `top` (a wrapped note starts level with its row). A\n * table whose cells are INPUTS wants `middle`: keksdose's VAT summary\n * (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,\n * and a figure at the top of its cell sat above the middle of the field next to it —\n * so it writes `<tr className=\"align-middle\">` over raw `<th>`/`<td>`, because the\n * kit's cells each stated `align-top` and a class on the row could not reach them.\n */\nexport type TableVAlign = \"top\" | \"middle\" | \"bottom\";\n/** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */\nexport type TableHeaderCellSize = \"xs\" | \"sm\";\n/** The header cell's weight. Default `medium`. */\nexport type TableHeaderCellWeight = \"normal\" | \"medium\" | \"semibold\";\n\n/**\n * What a {@link TableRow} is in a statement-style table (0.12.0): an ordinary `row`, a\n * `group` heading (\"Current assets\", a tinted full-width label row), a `subtotal` under\n * a group, or the `total` of the whole table. kastlan's balance sheet, income statement\n * and journal entry each spelled these as `className=\"font-medium\"`, `\"font-bold\n * border-t-2\"` and `bg-muted/50` on the cell — three pages, three sets of classes for\n * the same four kinds of line.\n */\nexport type TableRowVariant = \"row\" | \"group\" | \"subtotal\" | \"total\";\n\ninterface TableContextValue {\n density: TableDensity;\n zebra: boolean;\n hover: boolean;\n rowDividers: boolean;\n captionId: string;\n registerCaption: () => () => void;\n}\n\nconst TableContext = createContext<TableContextValue>({\n density: \"comfortable\",\n zebra: false,\n hover: false,\n rowDividers: true,\n captionId: \"\",\n registerCaption: () => () => {},\n});\n\ntype TableSection = \"head\" | \"body\" | \"foot\";\n\n/** Which section a row sits in — zebra and hover apply to body rows only. `undefined`\n * outside the kit's section parts: a raw `<thead>` / `<tbody>`, which\n * {@link useSection} then reads off the DOM. */\nconst SectionContext = createContext<TableSection | undefined>(undefined);\n\nconst SECTION_OF_TAG: Record<string, TableSection> = { THEAD: \"head\", TBODY: \"body\", TFOOT: \"foot\" };\n\n/**\n * The section a row or cell sits in: the kit part's context where there is one, else the\n * nearest `<thead>` / `<tbody>` / `<tfoot>` in the DOM.\n *\n * keksdose's VAT summary writes a raw `<thead>` round kit cells in places, and a\n * `TableHeaderCell` there took itself for a BODY cell — `scope=\"row\"`, body type — with\n * nothing on the page to say why. React cannot see an ancestor's tag, so the fallback\n * reads it after mount; a layout effect, so the corrected cell is what gets painted.\n */\nfunction useSection<E extends HTMLElement>(): [TableSection, ((el: E | null) => void) | undefined] {\n const fromContext = useContext(SectionContext);\n const [node, setNode] = useState<E | null>(null);\n const [detected, setDetected] = useState<TableSection | undefined>(undefined);\n useLayoutEffect(() => {\n if (fromContext !== undefined || !node) return;\n const section = node.closest(\"thead, tbody, tfoot\");\n const found = section ? SECTION_OF_TAG[section.tagName] : undefined;\n // eslint-disable-next-line react-hooks/set-state-in-effect -- read off the DOM, which only exists after mount\n if (found !== detected) setDetected(found);\n }, [fromContext, node, detected]);\n // No ref at all under a kit section: holding the node would cost every row and cell\n // a second render on mount for an answer the context already gave.\n return [fromContext ?? detected ?? \"body\", fromContext === undefined ? setNode : undefined];\n}\n\n/**\n * One callback ref for the caller's `ref` and the part's own (0.16.0). Every part takes\n * a `ref` to its DOM element — React 19 hands it to a function component as a prop —\n * and the parts that hold the node themselves (the table, a row or a head cell reading\n * its section off the DOM) must still get it when the caller passes one too.\n */\nfunction useMergedRef<E>(own: ((el: E | null) => void) | { current: E | null } | undefined, ref: Ref<E> | undefined) {\n return useCallback(\n (node: E | null) => {\n assignRef(own as Ref<E> | undefined, node);\n assignRef(ref, node);\n },\n [own, ref],\n );\n}\n\n/** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It\n * cannot be left to CSS inheritance: every cell states its own default alignment,\n * and a class on the cell beats one on the row. */\nconst RowVAlignContext = createContext<TableVAlign | undefined>(undefined);\n\nconst VALIGN: Record<TableVAlign, string> = {\n top: \"align-top\",\n middle: \"align-middle\",\n bottom: \"align-bottom\",\n};\n\nconst HEADER_SIZE: Record<TableHeaderCellSize, string> = {\n xs: \"text-xs\",\n sm: \"text-sm\",\n};\n\nconst HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {\n normal: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n};\n\nconst CELL_PAD: Record<TableDensity, string> = {\n comfortable: \"px-3 py-2\",\n compact: \"px-2 py-1\",\n none: \"p-0\",\n};\n\nconst ALIGN: Record<TableAlign, string> = {\n start: \"text-start\",\n center: \"text-center\",\n end: \"text-end\",\n};\n\n/**\n * The alignment a figure column wants: end-aligned (so the units line up under each\n * other in either reading direction) with tabular digits (so they line up at all).\n * Exported for a cell the kit did not render — a DataTable column, a `<td>` of the\n * app's own.\n */\nexport const NUMERIC_CELL_CLASS = \"text-end tabular-nums\";\n\n/**\n * Every part's props are `ComponentPropsWithRef` (0.16.0), so `ref` reaches the DOM\n * element: keksdose scrolled a budget row into view through `useId` + `getElementById`\n * because `<TableRow ref>` did not type-check.\n */\nexport interface TableProps extends ComponentPropsWithRef<\"table\"> {\n /** `compact` for a dense detail view (a ledger, a spec sheet). */\n density?: TableDensity;\n /** Tint every other body row. */\n zebra?: boolean;\n /** Tint the body row under the pointer. Off by default: a static table whose rows\n * light up promises a click that does nothing. With `zebra`, a tinted row hovers to\n * the card colour (0.33): the hover tint is too close to the well. */\n hover?: boolean;\n /** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */\n wrapperClassName?: string;\n /** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */\n layout?: TableLayout;\n /**\n * A rounded border round the whole table (0.12.0) — for a table standing on the page\n * or in a dialog rather than inside a card. kastlan wraps six of them in\n * `<div className=\"rounded-md border\">` by hand (payment allocation, the new-budget\n * lines, the meter readings, the deposit transactions, the unit values editor, the\n * maintenance detail), and a hand-made frame outside the scroll wrapper does not clip\n * the rows' tint to its corners.\n */\n framed?: boolean;\n /**\n * The rule between body rows. Default true. `false` for a small table whose rows are\n * spaced by their own content — keksdose's VAT summary, a block of three quiet\n * columns inside a card — which otherwise had to drop to raw `<tr>`s to lose it. One\n * row can still say otherwise with {@link TableRowProps.bordered}.\n */\n rowDividers?: boolean;\n /**\n * `\"phone\"`: below `sm` each body row becomes a block — its first cell the row's\n * title, every further cell under its column's header as a small label — and the\n * head row is visually hidden (still read by screen readers). For tables of PROSE,\n * not figures (lenkbank L5: a PID-terms table of three sentence columns wrapped into\n * two-word slivers at 390px and ran its third column off the screen). A table of\n * numbers should keep scrolling sideways, which is why this is opt-in. The labels\n * are the head row's cell texts, read from the DOM; a body cell spanning several\n * columns gets none.\n */\n stack?: \"phone\";\n /**\n * Fade out an edge of the scroll wrapper while columns are hidden behind it (0.25).\n *\n * A table wider than its box scrolls sideways inside the wrapper, and on a phone —\n * whose scrollbars are overlays that show only while you drag — nothing said there\n * was more: the last visible column simply ended at the edge, often mid-word. 0.24\n * drew a fade for ColumnRoleTable's preview (Kurvenschmiede: the fourth column's\n * role select sat off-screen with no hint) by wrapping this Table and reaching its\n * wrapper as the `<table>`'s `parentElement`; this is that fade as the Table's own,\n * so a table needs neither the reach-around nor a box of its own round it.\n *\n * It is the kit Tabs strip's fade, MEASURED rather than assumed: nothing is drawn\n * while the table fits, so a table that fits paints exactly as without the prop;\n * the end with columns behind it fades, and both ends once scrolled into the\n * middle; the ends are the reading direction's, so in RTL the hidden columns fade\n * on the left. It is a `mask-image` — no layout change, no background colour to\n * fade to, so it works on any surface in either theme — and never animated, so\n * there is nothing for reduced motion to turn off. The wrapper's own scrollbars\n * stay out of it (a fade on a classic scrollbar hid the very thing that says the\n * box scrolls).\n *\n * Focus that lands in a cell behind the fade — a link, a select, a checkbox — is\n * scrolled clear of it, cell and all where the cell fits; the browser's own scroll\n * stops at the edge, under the fade. And while the overflowing wrapper ITSELF holds\n * keyboard focus (it is a tab stop then, for arrow-key scrolling) the fade lifts, so\n * the focus outline round the box is drawn whole rather than faded out at its sides.\n *\n * The wrapper carries `data-overflow` — `\"start\"`, `\"end\"` or `\"both\"`, in\n * reading-direction terms, absent while it fits — for a caller's own affordance\n * (a scroll button) and for tests, which cannot read a mask.\n *\n * Off by default: it changes how every overflowing table looks.\n */\n edgeFade?: boolean;\n}\n\n/**\n * Below `sm` on a `stack=\"phone\"` table: rows as blocks, the head visually hidden,\n * the first cell as a title, the others labelled by `data-label` (set by the table).\n */\nconst STACK_PHONE_CLASSES = cn(\n \"max-sm:block max-sm:[&_tbody]:block max-sm:[&_tfoot]:block\",\n \"max-sm:[&_thead]:sr-only\",\n \"max-sm:[&_tbody_tr]:block max-sm:[&_tbody_tr]:py-2 max-sm:[&_tfoot_tr]:block max-sm:[&_tfoot_tr]:py-2\",\n \"max-sm:[&_tbody_td]:block max-sm:[&_tbody_td]:px-0 max-sm:[&_tbody_td]:py-0.5 max-sm:[&_tbody_td]:text-start\",\n \"max-sm:[&_tfoot_td]:block max-sm:[&_tfoot_td]:px-0 max-sm:[&_tfoot_td]:py-0.5\",\n \"max-sm:[&_tbody_td:first-child]:font-medium\",\n \"max-sm:[&_td[data-label]]:before:block max-sm:[&_td[data-label]]:before:text-caption max-sm:[&_td[data-label]]:before:font-medium max-sm:[&_td[data-label]]:before:text-muted max-sm:[&_td[data-label]]:before:content-[attr(data-label)]\",\n);\n\n/**\n * A plain, static HTML table in the kit's tokens — for the detail views that need\n * rows and columns and none of {@link DataTable}'s sorting, filtering or paging.\n *\n * kastlan renders about twenty of these on shadcn's Table parts; the part names here\n * are the same idea (`TableHead` is the `<thead>`, `TableHeaderCell` the `<th>`), so\n * the move is a rename rather than a rewrite.\n *\n * The table sits in an overflow-x wrapper, because a table cannot shrink below its\n * content and on a phone it would otherwise widen the page. While it overflows that\n * wrapper becomes a tab stop and a `role=\"region\"` named by the `TableCaption` (or the\n * table's own `aria-label`), so a keyboard user can scroll it and a reader can say\n * what it is — the same contract as {@link ScrollArea}.\n *\n * Alignment is logical throughout (`text-start` / `text-end`), so a right-to-left\n * page gets its first column on the right without a class changing.\n */\nexport function Table({\n density = \"comfortable\",\n zebra = false,\n hover = false,\n wrapperClassName,\n layout,\n framed = false,\n rowDividers = true,\n stack,\n edgeFade = false,\n className,\n \"aria-label\": ariaLabel,\n ref,\n ...rest\n}: TableProps) {\n const captionId = useId();\n const [captions, setCaptions] = useState(0);\n const wrapper = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(wrapper);\n const fade = useEdgeFade(wrapper, edgeFade);\n const tableRef = useRef<HTMLTableElement | null>(null);\n const setTableRef = useMergedRef(tableRef, ref);\n\n // `stack=\"phone\"`: label every body cell with its column's header text, and keep the\n // table's roles explicit — `display: block` on table parts drops their table\n // semantics in some engines. Re-run on any change to the rows, which the caller\n // renders and this component never sees.\n useLayoutEffect(() => {\n const table = tableRef.current;\n if (!stack || !table) return;\n const label = () => {\n const heads = Array.from(table.tHead?.rows[0]?.cells ?? []).map((cell) => cell.textContent?.trim() ?? \"\");\n table.setAttribute(\"role\", \"table\");\n for (const row of Array.from(table.rows)) {\n row.setAttribute(\"role\", \"row\");\n let column = 0;\n for (const cell of Array.from(row.cells)) {\n const span = cell.colSpan || 1;\n if (cell.tagName === \"TH\") cell.setAttribute(\"role\", cell.closest(\"thead\") ? \"columnheader\" : \"rowheader\");\n else cell.setAttribute(\"role\", \"cell\");\n const text = span === 1 && column > 0 ? heads[column] : undefined;\n if (cell.tagName === \"TD\" && text) cell.setAttribute(\"data-label\", text);\n else cell.removeAttribute(\"data-label\");\n column += span;\n }\n }\n };\n label();\n const observer = typeof MutationObserver === \"undefined\" ? null : new MutationObserver(label);\n observer?.observe(table, { childList: true, subtree: true, characterData: true });\n return () => observer?.disconnect();\n }, [stack]);\n\n const ctx = useMemo<TableContextValue>(\n () => ({\n density,\n zebra,\n hover,\n rowDividers,\n captionId,\n registerCaption: () => {\n setCaptions((n) => n + 1);\n return () => setCaptions((n) => n - 1);\n },\n }),\n [density, zebra, hover, rowDividers, captionId],\n );\n\n const regionName = captions > 0 ? { \"aria-labelledby\": captionId } : ariaLabel ? { \"aria-label\": ariaLabel } : null;\n\n return (\n <TableContext.Provider value={ctx}>\n <div\n ref={wrapper}\n // Only a region when it scrolls AND has a name: a landmark for every short\n // table on a page would bury the real ones.\n role={overflowing && regionName ? \"region\" : undefined}\n {...(overflowing ? regionName : null)}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing || undefined}\n data-clips=\"\"\n data-framed={framed || undefined}\n // `edgeFade`: the mask, `data-overflow` and the focus handling; nothing while off.\n {...fade}\n className={cn(\n // `relative`: the containing block for an `sr-only` caption.\n \"relative w-full overflow-x-auto\",\n // On the scroll wrapper itself, so its clipping keeps the rows inside the\n // rounded corners.\n framed && \"rounded-md border border-subtle\",\n THIN_SCROLLBAR_CLASS,\n \"focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-brand\",\n wrapperClassName,\n )}\n >\n <table\n {...rest}\n ref={setTableRef}\n aria-label={ariaLabel}\n className={cn(\n \"w-full caption-bottom border-collapse text-sm text-primary\",\n density === \"compact\" && \"text-xs\",\n layout === \"fixed\" && \"table-fixed\",\n layout === \"auto\" && \"table-auto\",\n stack === \"phone\" && STACK_PHONE_CLASSES,\n className,\n )}\n />\n </div>\n </TableContext.Provider>\n );\n}\n\nexport interface TableHeadProps extends ComponentPropsWithRef<\"thead\"> {\n /** The rule under the head. Default true; `false` for a quiet header over a small\n * table (keksdose's VAT summary wrote `className=\"border-b-0\"` to lose it). */\n bordered?: boolean;\n}\n\nexport function TableHead({ bordered = true, className, ...rest }: TableHeadProps) {\n return (\n <SectionContext.Provider value=\"head\">\n <thead {...rest} className={cn(bordered && \"border-b border-subtle\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport interface TableBodyProps extends ComponentPropsWithRef<\"tbody\"> {\n /**\n * Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —\n * `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-\n * writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda\n * and invitations tabs, the journal-entry and invoice detail pages, and a column\n * added later leaves every one of those counts one short.\n */\n empty?: ReactNode;\n}\n\nexport function TableBody({ className, empty, children, ...rest }: TableBodyProps) {\n // `toArray` drops null/undefined/booleans and flattens arrays, so `[[], false]` —\n // an empty map beside a conditional row that is off — is no rows.\n const hasRows = Children.toArray(children).length > 0;\n return (\n <SectionContext.Provider value=\"body\">\n <tbody {...rest} className={cn(\"[&>tr:last-child]:border-b-0\", className)}>\n {hasRows || empty === undefined || empty === null || empty === false ? (\n children\n ) : (\n <TableEmpty>{empty}</TableEmpty>\n )}\n </tbody>\n </SectionContext.Provider>\n );\n}\n\nexport interface TableEmptyProps extends Omit<ComponentPropsWithRef<\"tr\">, \"children\"> {\n children: ReactNode;\n /** Columns to span. Default: MEASURED — the widest row of the table it renders in,\n * counted after mount, so the row stays full width when a column is added. */\n colSpan?: number;\n /** Classes for the `<td>`. */\n cellClassName?: string;\n}\n\n/**\n * The \"nothing here\" row: one cell across every column, centred, muted. What\n * {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows\n * are not its direct children.\n */\nexport function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps) {\n const { density } = useContext(TableContext);\n const cell = useRef<HTMLTableCellElement | null>(null);\n const [measured, setMeasured] = useState(1);\n // Every render, deliberately: a column can appear in the head without anything this\n // row depends on changing. It cannot loop — setting the same count is a bail-out.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (colSpan !== undefined) return;\n const row = cell.current?.parentElement;\n const table = cell.current?.closest(\"table\");\n if (!row || !table) return;\n let widest = 1;\n for (const r of Array.from(table.rows)) {\n if (r === row) continue;\n let n = 0;\n for (const c of Array.from(r.cells)) n += c.colSpan || 1;\n widest = Math.max(widest, n);\n }\n setMeasured(widest);\n });\n return (\n <tr {...rest} data-table-empty=\"\" className={className}>\n <td\n ref={cell}\n colSpan={colSpan ?? measured}\n className={cn(\n // `none` keeps the message off the edges regardless: it is text, not a figure.\n density === \"none\" ? \"py-2\" : CELL_PAD[density],\n \"text-center text-muted\",\n cellClassName,\n )}\n >\n {children}\n </td>\n </tr>\n );\n}\n\nexport type TableFootProps = ComponentPropsWithRef<\"tfoot\">;\n\n/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`\n * is what tells a reader it is a total rather than one more row. */\nexport function TableFoot({ className, ...rest }: TableFootProps) {\n return (\n <SectionContext.Provider value=\"foot\">\n <tfoot {...rest} className={cn(\"border-t border-strong font-medium\", className)} />\n </SectionContext.Provider>\n );\n}\n\nconst ROW_VARIANT: Record<TableRowVariant, string | false> = {\n row: false,\n group: \"bg-surface-2 font-semibold\",\n subtotal: \"font-medium\",\n // A 2px rule over the 1px divider the row above draws on its bottom: collapsed\n // borders meet on one line, the wider wins, and the heavier rule is what says \"sum\".\n total: \"border-t-2 border-t-strong font-semibold\",\n};\n\n/** The deprecated HTML `valign` attribute is replaced by a class-backed one. */\nexport interface TableRowProps extends ComponentPropsWithRef<\"tr\"> {\n /** Vertical alignment for every cell of the row that does not set its own. See\n * {@link TableVAlign}. Left out, each cell keeps its default. */\n valign?: TableVAlign;\n /** See {@link TableRowVariant}. Default `row`. Zebra and hover tint ordinary rows\n * only: a group heading or a total is not one more record. */\n variant?: TableRowVariant;\n /** The rule under this body row. Default: the table's `rowDividers`. */\n bordered?: boolean;\n}\n\nexport function TableRow({ valign, variant = \"row\", bordered, className, ref: refProp, ...rest }: TableRowProps) {\n const { zebra, hover, rowDividers } = useContext(TableContext);\n const [section, sectionRef] = useSection<HTMLTableRowElement>();\n const ref = useMergedRef(sectionRef, refProp);\n const body = section === \"body\";\n const record = body && variant === \"row\";\n const row = (\n <tr\n ref={ref}\n {...rest}\n data-variant={variant === \"row\" ? undefined : variant}\n className={cn(\n body && (bordered ?? rowDividers) && \"border-b border-subtle\",\n record && zebra && \"even:bg-surface-2\",\n record && hover && \"transition-colors hover:bg-hover\",\n // A zebra row rests on the well, and `--bg-hover` sits only 1.00–1.02:1 from it\n // (0.33: the light well is the page colour): it hovers to the card instead, the\n // one fill that both steps 1.09–1.13:1 from the well and keeps every text role at\n // 4.5:1 (docs/colour-roles-harmonization.md §12.2). `even:hover:` outranks the\n // row's own `hover:`.\n record && zebra && hover && \"even:hover:bg-surface\",\n ROW_VARIANT[variant],\n // On the row as well, for a raw `<td>` of the caller's, which inherits it.\n valign && VALIGN[valign],\n className,\n )}\n />\n );\n return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;\n}\n\ninterface CellAlignProps {\n /** Shorthand for a figure column: end-aligned, tabular digits. */\n numeric?: boolean;\n /** Logical alignment. Default `start` (`end` when `numeric`). */\n align?: TableAlign;\n /** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and\n * `top` for a body cell. See {@link TableVAlign}. */\n valign?: TableVAlign;\n}\n\n/** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */\nexport interface TableHeaderCellProps extends Omit<ComponentPropsWithRef<\"th\">, \"align\" | \"valign\">, CellAlignProps {\n /**\n * Type size. Left out: `xs` in the head, the table's own size in the body — as\n * before. keksdose's VAT summary heads its columns at the body size and normal weight\n * (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a\n * small table of figures, and could not say so without overriding classes.\n */\n size?: TableHeaderCellSize;\n /** Default `medium`. `normal` for the quiet header above; see {@link size}. */\n weight?: TableHeaderCellWeight;\n}\n\n/**\n * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header\n * cell in a body row is the row's label (\"Net rent\" in a key-value table), and a\n * reader needs the scope to read it with each cell beside it.\n */\nexport function TableHeaderCell({\n numeric = false,\n align,\n valign,\n size,\n weight = \"medium\",\n scope,\n className,\n ref: refProp,\n ...rest\n}: TableHeaderCellProps) {\n const { density } = useContext(TableContext);\n const [section, sectionRef] = useSection<HTMLTableCellElement>();\n const ref = useMergedRef(sectionRef, refProp);\n const rowVAlign = useContext(RowVAlignContext);\n const head = section === \"head\";\n const textSize = size ?? (head ? \"xs\" : undefined);\n return (\n <th\n ref={ref}\n {...rest}\n scope={scope ?? (section === \"head\" ? \"col\" : \"row\")}\n className={cn(\n CELL_PAD[density],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums\",\n VALIGN[valign ?? rowVAlign ?? (head ? \"bottom\" : \"top\")],\n HEADER_WEIGHT[weight],\n head ? \"text-muted\" : \"text-secondary\",\n textSize && HEADER_SIZE[textSize],\n className,\n )}\n />\n );\n}\n\nexport interface TableCellProps extends Omit<ComponentPropsWithRef<\"td\">, \"align\" | \"valign\">, CellAlignProps {}\n\nexport function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {\n const { density } = useContext(TableContext);\n const rowVAlign = useContext(RowVAlignContext);\n return (\n <td\n {...rest}\n className={cn(\n CELL_PAD[density],\n VALIGN[valign ?? rowVAlign ?? \"top\"],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums whitespace-nowrap\",\n className,\n )}\n />\n );\n}\n\nexport type TableCaptionProps = ComponentPropsWithRef<\"caption\">;\n\n/**\n * The table's title. Beneath the table by default (`caption-bottom`, the shadcn\n * convention kastlan's views already follow); `className=\"caption-top\"` moves it.\n * `className=\"sr-only\"` keeps it for readers only — the wrapper is `relative`, so\n * the hidden caption stays inside it.\n *\n * It also names the scroll region when the table overflows: it registers itself\n * with the {@link Table} so the wrapper points at an id that exists, and never at\n * one that does not.\n */\nexport function TableCaption({ className, id, ...rest }: TableCaptionProps) {\n const { captionId, registerCaption } = useContext(TableContext);\n useLayoutEffect(() => (id ? undefined : registerCaption()), [id, registerCaption]);\n return <caption {...rest} id={id ?? captionId} className={cn(\"mt-2 text-start text-sm text-muted\", className)} />;\n}\n"],"mappings":"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","names":[]}
|
package/dist/components/tabs.js
CHANGED
|
@@ -179,11 +179,7 @@ function Tabs({
|
|
|
179
179
|
TAB_VERTICAL_CLASSES,
|
|
180
180
|
isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,
|
|
181
181
|
removable && "pe-8"
|
|
182
|
-
) : cn(
|
|
183
|
-
TAB_CLASSES,
|
|
184
|
-
isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES,
|
|
185
|
-
removable && TAB_REMOVABLE_CLASSES
|
|
186
|
-
)
|
|
182
|
+
) : cn(TAB_CLASSES, isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES, removable && TAB_REMOVABLE_CLASSES)
|
|
187
183
|
};
|
|
188
184
|
const inner = /* @__PURE__ */ jsxs(
|
|
189
185
|
"span",
|
|
@@ -303,25 +299,10 @@ function Tabs({
|
|
|
303
299
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-end", className), children: [
|
|
304
300
|
stripBox,
|
|
305
301
|
removeButton,
|
|
306
|
-
/* @__PURE__ */ jsx(
|
|
307
|
-
"
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
children: /* @__PURE__ */ jsxs(
|
|
311
|
-
"button",
|
|
312
|
-
{
|
|
313
|
-
type: "button",
|
|
314
|
-
onClick: onAdd,
|
|
315
|
-
disabled: busy,
|
|
316
|
-
className: cn(TAB_ADD_CLASSES, TAB_ADD_PHONE_CLASSES),
|
|
317
|
-
children: [
|
|
318
|
-
/* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
|
|
319
|
-
/* @__PURE__ */ jsx("span", { className: "max-md:sr-only", children: addLabel ?? text.add })
|
|
320
|
-
]
|
|
321
|
-
}
|
|
322
|
-
)
|
|
323
|
-
}
|
|
324
|
-
)
|
|
302
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center self-stretch border-b border-subtle", children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, disabled: busy, className: cn(TAB_ADD_CLASSES, TAB_ADD_PHONE_CLASSES), children: [
|
|
303
|
+
/* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
|
|
304
|
+
/* @__PURE__ */ jsx("span", { className: "max-md:sr-only", children: addLabel ?? text.add })
|
|
305
|
+
] }) })
|
|
325
306
|
] });
|
|
326
307
|
}
|
|
327
308
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { useLayoutEffect, useMemo, useRef } from \"react\";\nimport { ChevronLeft, ChevronRight, Plus, X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport { scrollIntoStrip, useStripFade } from \"../lib/strip-fade\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { prefersReducedMotion } from \"../lib/reduced-motion\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { mergeRefs } from \"./field-parts\";\nimport { DEFAULT_TABS_LABELS } from \"./ui-labels\";\nimport { IconButton } from \"./icon-button\";\nimport { HIT_AREA_LARGE } from \"./button-look\";\n\n/** One tab of a {@link Tabs} strip. */\nexport interface TabItem<T extends string> {\n id: T;\n /** A ReactNode so a tab can pair an icon with text. */\n label: ReactNode;\n /** Optional trailing node (e.g. a count pill). */\n badge?: ReactNode;\n /** Marks a tab that IS a route — see {@link TabsProps}. */\n href?: string;\n /** Leading decoration: an icon, or the colour swatches of the lines this tab\n * draws in a chart above the strip. Hidden from assistive tech — it repeats\n * what the label says, or it says something the label must say instead. */\n icon?: ReactNode;\n /** A second, smaller line under the label — the one fact that tells two tabs\n * with similar labels apart (\"80 km/h\", \"Level 3\"). Part of the tab's name. */\n detail?: ReactNode;\n /** A tab that exists as a slot with nothing in it yet, drawn with a dashed\n * outline so a gap in a sequence looks like a gap. Visual only: say \"empty\" in\n * the label or `detail` if a screen reader needs to know. */\n empty?: boolean;\n /** `false` keeps this one tab when the strip has `onRemove` — the one sheet a\n * workbook cannot be without. Default: removable. */\n removable?: boolean;\n /** Plain-text name for the labels the strip composes about this tab (\"Remove\n * …\"), for when `label` is not a string. Default: `label` if it is a string,\n * else the id. */\n name?: string;\n}\n\n/** The strings the strip renders on its own behalf — only when it can add or\n * remove tabs. */\nexport interface TabsLabels {\n /** The \"add\" button's visible text when the caller gives no `addLabel`. */\n add: string;\n /** Accessible name of a tab's × — \"Remove <tab>\". */\n remove: (tab: string) => string;\n /** The `overflowCue` › after the strip's end, while tabs lie past it: \"More tabs\". */\n more: string;\n /** The `overflowCue` ‹ before the strip's start, while tabs lie before it: \"Previous tabs\". */\n previous: string;\n}\n\n// Defined in ./ui-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_TABS_LABELS };\n\n/**\n * The strip's own props sit on a `<div>`: the tablist IS the root element, so anything\n * a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an\n * `aria-describedby` — lands there. `onChange` is omitted from the div's props because\n * this component's `onChange` hands over the chosen TAB ID, not a DOM event, and\n * `children` because the strip renders `tabs` — a caller who wants a PANEL wires it up\n * through `panelId`, which is the whole point of that prop.\n */\nexport interface TabsProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n /** `label` is a ReactNode so tabs can pair an icon with text; `badge` is an\n * optional trailing node (e.g. a count pill). `href` marks a tab that IS a route:\n * it renders as an anchor so it can be middle-/⌘-clicked into a new tab (Keksdose\n * feedback #451), while a plain click still goes through `onChange` and stays\n * client-side. Tabs that only flip local state leave it unset — a link to a URL\n * that does not select the tab would be worse than no link. */\n tabs: TabItem<T>[];\n active: T;\n onChange: (id: T) => void;\n className?: string;\n /**\n * Carets at the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose\n * live #410 round 4): a › after the strip's end while tabs lie past it, and a ‹ before\n * its start while tabs lie before it — each there only while something is hidden\n * past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three\n * quarters of the strip's width toward its own side. Neither ever changes direction:\n * until 0.46 one caret turned round into \"back to the first\" at the end, and read as\n * an arrow pointing the wrong way. Start and end follow the reading direction.\n *\n * The edge fade alone could read as the end of the row: the last visible tab can\n * finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).\n * Since 0.48 (live #410 rounds 5–6) the carets are bare glyphs BESIDE the strip — the ‹\n * before it, the › after it — centred on the labels, muted and primary on hover, with\n * no frame, fill or shadow; until 0.47 they were framed buttons laid over the strip's\n * last visible tabs. While the strip overflows at all it keeps a 24px gutter at both\n * ends, and each caret sits in its own, so scrolling never changes the strip's width\n * or moves its labels; a strip that fits has no gutters. Out of the tab order —\n * the keyboard walks the strip with the arrow keys, which bring the focused tab into\n * view. At every width. Ignored on a vertical strip (a side nav is a column, not a\n * row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is\n * the reference this replaces.\n */\n overflowCue?: boolean;\n /**\n * `id` of the element the caller renders the open tab's content into — the missing\n * half of `role=\"tab\"`. A tab that controls nothing is a tab in name only: a screen\n * reader announces \"tab, 2 of 5\" and then has no way to take the user to what it\n * opened, and no way back.\n *\n * **This component does not render the panel, on purpose.** `Tabs` is the STRIP; the\n * content lives wherever the caller put it — three Keksdose pages render it in a\n * sibling `<Card>`, one renders it through a router outlet, and a tab can BE a route\n * (`href`), in which case the panel is a whole page this component never sees.\n * Wrapping `children` here would mean either moving that content into the strip's\n * subtree — a layout change on every page that uses tabs — or shipping a wrapper\n * that only some callers could use. So the two halves are joined by an id instead.\n *\n * Wire the other end yourself:\n *\n * ```tsx\n * <Tabs tabs={tabs} active={active} onChange={setActive} panelId=\"report-panel\" />\n * <div id=\"report-panel\" role=\"tabpanel\" aria-labelledby=\"report-panel-tab\" tabIndex={-1}>\n * ```\n *\n * `aria-controls` goes on the OPEN tab only, and `${panelId}-tab` is its id — the\n * closed tabs' panels are not in the document, and a dangling `aria-controls`\n * describes a tab as opening something that is not there.\n */\n panelId?: string;\n /**\n * Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab\n * (only on the open one with `removeOn=\"active\"`).\n *\n * The × is on the open tab only — a row of tabs each carrying one is a row of\n * things to hit by accident, and the tab you can remove should be the one you\n * are looking at — but its room is reserved on every removable tab, so opening a\n * tab never widens it and never pushes the rest of the strip along (lenkbank\n * feedback #72). It is not a tab stop of its own: the strip stays one stop, and the\n * keyboard path is Delete (announced through `aria-keyshortcuts`), which removes\n * whichever tab has focus (see `removeOn`).\n *\n * Assistive tech meets the remove as a \"Remove <tab>\" button right AFTER the strip,\n * not as the × (0.34): a tablist may own tabs only, so the drawn × is the pointer's\n * and `aria-hidden`, and the named button beside the tablist is visually hidden.\n *\n * The strip only ASKS: it calls this, and the tab goes when the caller drops it\n * from `tabs` — after a confirmation, a request, whatever it needs. When it does,\n * focus that was on the removed tab (or its ×) moves to the open tab if the\n * removed one was open, else to its neighbour, instead of falling to the page.\n */\n onRemove?: (id: T) => void;\n /**\n * Which tabs the Delete key removes.\n *\n * - `\"every\"` (default): any focused removable tab — the ARIA pattern's reading.\n * - `\"active\"`: only the OPEN tab, the same one that wears the ×. For a strip whose\n * remove is immediate and unconfirmed (lenkbank's sheet tabs delete a measurement\n * sheet at once), so arrowing across the strip and pressing Delete cannot take\n * out a sheet the user is not looking at. Delete on a closed tab does nothing,\n * and only the open tab announces the shortcut (`aria-keyshortcuts`).\n *\n * Either way the × is drawn on the open tab only and its room stays reserved on\n * every removable tab, so the strip never relayouts when the selection moves.\n */\n removeOn?: \"every\" | \"active\";\n /** Renders an \"add\" button after the last tab, outside the tablist — it is an\n * action, not a tab, and a tablist may own only tabs. */\n onAdd?: () => void;\n /** The add button's text. Say what is added — \"Add loop\", \"Add report\" says\n * more than \"Add\". Default: `tabs.add` from the {@link UiKitProvider}. */\n addLabel?: string;\n /** Overrides for this strip's strings. See {@link TabsLabels}. */\n labels?: LabelOverride<TabsLabels>;\n /** An add or a remove is in flight: the × and the add button are disabled and\n * Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */\n busy?: boolean;\n /**\n * `\"vertical\"`: the same strip as a SIDE NAV — one item per row, the open one\n * filled, ↑/↓ walking it (Home/End as before) and `aria-orientation=\"vertical\"` on\n * the tablist. For keksdose's admin page and settings page, which each hand-built\n * the identical sidebar with none of this strip's keyboard, roving tab stop or\n * routed-`href` handling. Everything else — `href` tabs, `badge`, `panelId`,\n * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the\n * badge moves to the row's end.\n *\n * **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**, rather than\n * staying a column. A side nav only works beside its content; on\n * a phone it has to go ABOVE it, and eight full-width rows there push the panel\n * the user picked below the fold on every visit — the two keksdose pages both\n * collapsed theirs into a scrolling row by hand for exactly that reason. The\n * orientation is switched in JavaScript, not with `md:` classes, so the\n * `aria-orientation` a screen reader hears and the arrow keys that work always\n * match the layout on screen. The caller's own two-column layout has to stack at\n * the same breakpoint (`md:grid-cols-[14rem_1fr]`).\n */\n orientation?: \"horizontal\" | \"vertical\";\n /** The `role=\"tablist\"` strip, where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the strip's own ref, which scrolls the open tab into\n * view and measures the fade, so neither loses the element. */\n ref?: Ref<HTMLDivElement>;\n}\n\nconst TABLIST_CLASSES =\n \"flex gap-1 overflow-x-auto overflow-y-hidden border-b border-subtle\";\n// The side nav. A column of full-width rows with the open one FILLED rather than\n// underlined: an underline under one row of a list reads as a separator, not as a\n// selection. `--bg-active` is the kit's own selected-row fill (the sidebar's), so the\n// admin page's nav and the app's own sidebar mark \"you are here\" the same way.\nconst TABLIST_VERTICAL_CLASSES = \"flex flex-col gap-0.5\";\nconst TAB_VERTICAL_CLASSES =\n FOCUS_RING_WIDTH +\n \" 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-strong\";\nconst TAB_VERTICAL_ACTIVE_CLASSES = \"bg-active text-primary\";\nconst TAB_VERTICAL_INACTIVE_CLASSES =\n \"text-muted hover:bg-hover hover:text-primary\";\n\n// `max-w-full`: no tab outgrows its strip. A sheet's `detail` line (\"Spurstangenkraft\n// links · … · 31 Punkte\") made one tab 448px wide in a 390px strip, and a tab wider\n// than the strip cannot be scrolled into view — its end was simply off screen\n// (lenkbank L3). The label and detail truncate inside it instead; see `inner`.\nconst TAB_CLASSES =\n FOCUS_RING_WIDTH +\n \" 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-strong\";\nconst TAB_ACTIVE_CLASSES = \"border-[var(--text-primary)] text-primary\";\nconst TAB_INACTIVE_CLASSES =\n \"border-transparent text-muted hover:text-secondary hover:border-strong\";\n\n// The add/remove chrome. The × reserves its gutter on every removable tab (see\n// `onRemove`).\nconst TAB_REMOVABLE_CLASSES = \"pe-8\";\nconst TAB_EMPTY_CLASSES = \"-mx-1.5 rounded border border-dashed border-strong px-1.5\";\nconst TAB_ADD_CLASSES =\n FOCUS_RING_WIDTH +\n \" inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-muted transition-colors hover:text-primary focus:outline-none focus-visible:ring-strong disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4\";\n// `relative`: the phone's visually hidden label needs a positioned ancestor (see the\n// sr-only rule in CONTRIBUTING); `max-md:px-2` squares the lone \"+\".\nconst TAB_ADD_PHONE_CLASSES = \"relative max-md:px-2\";\n\nexport function Tabs<T extends string>({\n tabs,\n active,\n onChange,\n className,\n overflowCue = false,\n panelId,\n onRemove,\n removeOn = \"every\",\n onAdd,\n addLabel,\n labels,\n busy = false,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n ref,\n ...rest\n}: TabsProps<T>) {\n const text = useKitLabels(\"tabs\", DEFAULT_TABS_LABELS, labels);\n // See `orientation`: a vertical strip is the horizontal one on a phone.\n const phone = usePhoneLayout();\n const vertical = orientation === \"vertical\" && !phone;\n const stripRef = useRef<HTMLDivElement>(null);\n const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);\n const fade = useStripFade(stripRef);\n // See `overflowCue`: a ‹ before the strip while tabs lie before it, a › after it\n // while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.\n const cue = overflowCue && !vertical;\n const cueStart = cue && (fade.overflow === \"start\" || fade.overflow === \"both\");\n const cueEnd = cue && (fade.overflow === \"end\" || fade.overflow === \"both\");\n // Three quarters of the visible width toward `side`; an RTL strip runs leftward.\n const onCue = (side: \"start\" | \"end\") => {\n const strip = stripRef.current;\n if (!strip) return;\n const behavior: ScrollBehavior = prefersReducedMotion() ? \"auto\" : \"smooth\";\n const rtl = getComputedStyle(strip).direction === \"rtl\";\n const toward = (side === \"end\" ? 1 : -1) * (rtl ? -1 : 1);\n strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });\n };\n // In a GUTTER of its own at the strip's end (0.48, keksdose live #410 rounds 5–6): a\n // bare glyph — no frame, fill or shadow — muted until the pointer comes. Round 5 put\n // each cue in the flow beside the strip, so it took 24px the moment it appeared and the\n // labels jumped sideways at every edge the scroll crossed (\"It has problems with moving\n // to left and right especially when moving the scroll bar\"). Now the strip box keeps a\n // 24px gutter at BOTH ends for as long as the strip overflows at all (`px-[24px]`, see\n // `stripBox`), and each gutter is absolute in that padding, so a cue coming or going\n // never moves the strip, its labels or the scroll position under the thumb. A strip\n // that fits has no gutters: its width changes only when it starts or stops overflowing.\n // The gutter carries the strip's 1px rule on beneath the glyph, and lifts the glyph\n // onto the LABELS' centre rather than the box's: a tab's 2px underline (`border-b-2`)\n // sits on the rule (`-mb-px`), so the labels are centred 2px above the box's bottom —\n // the gutter's 1px rule (`border-b`) plus 1px more (`pb-px`).\n // 24px wide at every text size (KS N3): at Large `sm`'s 48px minimum made each cue\n // 48px, and at 360px the two left a two-tab strip 100px. 24 PIXELS, not `w-6`: the rem\n // grows with the text size (24 / 30 / 36px), and at Extra large on a 360px phone the two\n // gutters took 72px of a 226px strip and a sheet tab wrapped to seven lines (0.48 build\n // review 2, KS N1). The 48px touch target is an\n // invisible hit area round the glyph instead (`HIT_AREA_LARGE`, as `2xs`), over the\n // strip's faded edge, not room taken from the row. The wrapper inside the gutter is\n // ours because IconButton puts its tooltip's span around the button.\n const gutterAt = (side: \"start\" | \"end\", shown: boolean) => (\n <div\n data-tabs-cue-gutter={side}\n className={cn(\n \"absolute inset-y-0 flex w-[24px] items-center justify-center border-b border-subtle pb-px\",\n side === \"start\" ? \"start-0\" : \"end-0\",\n )}\n >\n {shown && (\n <div data-tabs-overflow-cue={side} className=\"flex\">\n <IconButton\n size=\"sm\"\n variant=\"ghost\"\n className={cn(\n \"w-[24px] large:min-w-0 bg-transparent text-muted hover:bg-transparent hover:text-primary\",\n // Over the strip's faded edge, for ‹ too.\n HIT_AREA_LARGE,\n \"large:after:z-[1]\",\n )}\n tabIndex={-1}\n type=\"button\"\n label={side === \"start\" ? text.previous : text.more}\n labelVisible={false}\n onClick={() => onCue(side)}\n >\n {side === \"start\" ? <ChevronLeft className=\"rtl:-scale-x-100\" /> : <ChevronRight className=\"rtl:-scale-x-100\" />}\n </IconButton>\n </div>\n )}\n </div>\n );\n // The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a\n // deep link to the seventh report. A selection behind the cut edge was the worst\n // case of the overflow above: the one tab the panel below belongs to, sliced to\n // \"80 km,\" under the add button. Only the strip scrolls (`scrollBy` on it, not\n // `scrollIntoView`, which would scroll the page as well), and only as far as needed\n // to clear the fade.\n useLayoutEffect(() => {\n const strip = stripRef.current;\n const tab = strip?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]');\n if (strip && tab) scrollIntoStrip(strip, tab);\n }, [active]);\n const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;\n // Whether Delete reaches this tab: every removable tab, or only the open one.\n const deletesOnKey = (tab: TabItem<T>) =>\n isRemovable(tab) && (removeOn === \"every\" || tab.id === active);\n const nameOf = (tab: TabItem<T>) =>\n tab.name ?? (typeof tab.label === \"string\" ? tab.label : tab.id);\n\n // Where focus goes once a removal the strip asked for has happened. The caller\n // owns `tabs` and may take its time (a confirmation, a request), so this waits\n // for the id to actually leave the list rather than guessing at the moment.\n const pendingFocus = useRef<{ removed: T; wasActive: boolean; neighbour?: T } | null>(null);\n const removeRef = useRef<HTMLButtonElement>(null);\n const requestRemove = (id: T) => {\n const index = tabs.findIndex((t) => t.id === id);\n pendingFocus.current = {\n removed: id,\n wasActive: id === active,\n neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id,\n };\n onRemove?.(id);\n };\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending || tabs.some((t) => t.id === pending.removed)) return;\n pendingFocus.current = null;\n const strip = stripRef.current;\n if (!strip) return;\n // Only focus that fell out of the strip with the removed tab is ours to place —\n // or that sits on the remove button after it, which a pick removed through.\n // If the user has moved on — into the panel, into a dialog — leave it there.\n const focused = document.activeElement;\n if (focused && focused !== document.body && !strip.contains(focused) && focused !== removeRef.current) return;\n // The removed tab was open: focus follows the caller's new selection, which is\n // also the strip's tab stop. It was not: its neighbour, as the ARIA pattern has it.\n const target =\n !pending.wasActive && tabs.some((t) => t.id === pending.neighbour)\n ? pending.neighbour\n : active;\n const index = tabs.findIndex((t) => t.id === target);\n strip.querySelectorAll<HTMLElement>('[role=\"tab\"]')[index]?.focus();\n }, [tabs, active]);\n\n // Arrow keys walk the strip in DOM order (ARIA tabs pattern). Home/End jump\n // to the ends. Focus only — activation stays on click/Enter/Space, because a tab\n // here can be a real route and moving focus must not navigate.\n //\n // On the TABS, not on the tablist. The container carried it, which left a `<div>`\n // wearing an interactive role and a key handler while being unfocusable —\n // `jsx-a11y/interactive-supports-focus`, and the only instance of it left in `src/`.\n // The fix that rule wants is `tabIndex` on the div; the fix the pattern wants is\n // the handler on the elements that are natively focusable and actually receive the\n // keystroke. Those are the tabs, so the warning goes away by being right rather\n // than by being satisfied.\n const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {\n // Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable\n // tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it\n // rather than letting it fall through to the page. With `removeOn=\"active\"` only\n // the open tab answers; a closed one lets the key pass untouched.\n if (e.key === \"Delete\" && deletesOnKey(tab)) {\n e.preventDefault();\n if (!busy) requestRemove(tab.id);\n return;\n }\n // Along the reading direction: the strip is a flex row, so in RTL the NEXT tab sits\n // to the left, and ArrowLeft has to reach it. A vertical strip answers ↑/↓ instead\n // — and ONLY those, as the pattern has it: ←/→ are left to the page, where a side\n // nav's neighbour (the panel) may well want them.\n const step: 1 | -1 | 0 = vertical\n ? e.key === \"ArrowDown\"\n ? 1\n : e.key === \"ArrowUp\"\n ? -1\n : 0\n : horizontalStep(e.key, e.currentTarget);\n if (step === 0 && e.key !== \"Home\" && e.key !== \"End\") return;\n const strip = e.currentTarget.closest('[role=\"tablist\"]');\n if (!strip) return;\n const items = Array.from(strip.querySelectorAll<HTMLElement>('[role=\"tab\"]'));\n const from = items.indexOf(e.currentTarget);\n if (from === -1) return;\n e.preventDefault();\n const to =\n e.key === \"Home\"\n ? 0\n : e.key === \"End\"\n ? items.length - 1\n : (from + step + items.length) % items.length;\n items[to]?.focus();\n };\n const tablist = (\n <div\n // Before the role and the name: a caller's arbitrary attribute is welcome on the\n // strip, but a `role` or an `aria-label` arriving through a spread props object\n // must not be able to unmake the tablist the tabs below are registered against.\n {...rest}\n ref={setStrip}\n role=\"tablist\"\n // Names the GROUP — every tab carries its own text — so a strip under a visible\n // heading that already says it can leave it unset.\n aria-label={ariaLabel}\n // Only when vertical: `horizontal` is the tablist's implicit value, and the\n // attribute on every existing strip would be noise in every snapshot of them.\n aria-orientation={vertical ? \"vertical\" : undefined}\n // Merged over a caller's own style rather than replacing it; see useStripFade.\n // `data-overflow` names the cut ends in reading-direction terms, for a caller's\n // own affordance (a scroll button) and for tests, which cannot read the mask.\n style={fade.mask ? { ...rest.style, maskImage: fade.mask, WebkitMaskImage: fade.mask } : rest.style}\n data-overflow={fade.overflow}\n // With an add button the strip gains an outer box, and `className` goes there\n // — it is the box a caller's margin or width is meant for.\n className={cn(\n vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,\n cue ? \"min-w-0 flex-1\" : onAdd ? \"min-w-0\" : className,\n )}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === active;\n const removable = isRemovable(tab);\n // Written once and worn by either tag below, so a routed tab and a\n // state-only tab stay indistinguishable to the eye and to a screen reader.\n const shared = {\n role: \"tab\" as const,\n \"aria-selected\": isActive,\n // Roving tabindex: the whole strip is ONE stop in the page's tab order, and\n // it is the open tab. Ten report tabs otherwise cost ten Tab presses to step\n // over on the way to the table below them (live #262's strip is the extreme\n // case, but every strip in the app paid it).\n //\n // The stop follows the SELECTION, not the focus. Arrowing is a look around —\n // activation here is manual, because a tab can be a real route — so re-\n // pointing the stop at a report the user only arrowed past would hand the\n // strip back on the next visit in a state they never chose. The cost is that\n // Tab out and back returns to the open tab rather than to the last one\n // looked at; the ARIA pattern allows either, and this one needs no second\n // piece of state shadowing `active`.\n tabIndex: isActive ? 0 : -1,\n // Only the open tab: see `panelId`.\n \"aria-controls\": isActive ? panelId : undefined,\n id: isActive && panelId ? `${panelId}-tab` : undefined,\n \"aria-keyshortcuts\": deletesOnKey(tab) ? \"Delete\" : undefined,\n onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),\n className: vertical\n ? cn(\n TAB_VERTICAL_CLASSES,\n isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,\n removable && \"pe-8\",\n )\n : cn(\n TAB_CLASSES,\n isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES,\n removable && TAB_REMOVABLE_CLASSES,\n ),\n };\n const inner = (\n <span\n className={cn(\n // In a side nav the row is the full width, so the label takes it and a\n // badge lands at the row's end, where a column of counts lines up.\n vertical ? \"flex min-w-0 flex-1 items-center gap-1.5\" : \"inline-flex max-w-full min-w-0 items-center gap-1.5\",\n tab.empty && TAB_EMPTY_CLASSES,\n )}\n >\n {tab.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 items-center gap-0.5\">\n {tab.icon}\n </span>\n )}\n {tab.detail != null ? (\n // `min-w-0` + `truncate-until-large` on each line: capped by the tab's\n // `max-w-full`, a long detail ends in an ellipsis at Normal and wraps from\n // Large up (text size §10.17; round 0.34 T1), where the tab grows a line\n // rather than cut the words. The whole text stays the tab's name.\n <span className=\"flex min-w-0 flex-col items-start text-start\">\n <span className=\"max-w-full truncate-until-large\">{tab.label}</span>\n {/* A separator for the NAME only — whitespace between flex items is\n not drawn, and without it the two lines ran together into one\n word for a screen reader (\"80 km/hloop 2\"). */}{\" \"}\n <span className=\"max-w-full truncate-until-large text-caption font-normal leading-tight text-muted\">\n {tab.detail}\n </span>\n </span>\n ) : (\n tab.label\n )}\n {tab.badge != null ? (\n vertical ? <span className=\"ms-auto flex shrink-0 items-center\">{tab.badge}</span> : tab.badge\n ) : null}\n </span>\n );\n const element = tab.href ? (\n <a\n key={tab.id}\n {...shared}\n href={tab.href}\n draggable={false}\n onClick={(e) => {\n // Middle-, ⌘/Ctrl-, Shift- and Alt-click belong to the browser\n // (feedback #451); only the plain left click is ours to cancel and\n // route through `onChange`, which keeps the switch client-side.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n onChange(tab.id);\n }}\n // A <button> tab activated on Space; an anchor does not, so becoming a\n // link (feedback #451) would have quietly cost keyboard users the tab\n // strip. Enter is deliberately untouched — it is the anchor's own\n // default and already arrives at `onChange` through the click handler\n // above, so handling it here too would switch tabs twice. Same three\n // lines the mobile row card carries, for the same reason; the rejected\n // alternative — keeping a button and faking the middle click with\n // window.open — is what the whole item is moving away from.\n //\n // Composed with the strip's arrow-key walk rather than replacing it: this\n // prop overrides the one in `shared`, and an anchor tab that lost the\n // arrows would be a strip that is navigable only where it is not a link.\n onKeyDown={(e) => {\n onTabKeyDown(e, tab);\n if (e.defaultPrevented || e.key !== \" \") return;\n e.preventDefault();\n onChange(tab.id);\n }}\n >\n {inner}\n </a>\n ) : (\n <button key={tab.id} {...shared} onClick={() => onChange(tab.id)}>\n {inner}\n </button>\n );\n if (!removable) return element;\n return (\n // The × cannot go INSIDE the tab: a button in a button is invalid HTML, and\n // a tab's children are presentational, so a reader would flatten it into the\n // tab's name. It sits beside the tab, over the gutter the tab reserves.\n <div key={tab.id} className={cn(\"relative flex max-w-full\", !vertical && \"shrink-0\")}>\n {element}\n {isActive && (\n <IconButton\n size=\"2xs\"\n tone=\"danger\"\n // The POINTER's ×, hidden from assistive tech (round 0.34, the showcase's\n // axe check): a tablist may own tabs only, and this button sat in it. The\n // named \"Remove <tab>\" a screen reader's cursor and a touch reader's swipe\n // reach is `removeButton`, after the strip. Not a tab stop either way:\n // the strip is one, and Delete is the keyboard's path.\n aria-hidden\n data-tab-remove=\"\"\n tabIndex={-1}\n type=\"button\"\n disabled={busy}\n onClick={() => requestRemove(tab.id)}\n className=\"absolute inset-y-0 end-1 my-auto\"\n >\n <X />\n </IconButton>\n )}\n </div>\n );\n })}\n </div>\n );\n // The open tab's remove action as assistive tech meets it: right after the strip,\n // OUTSIDE the tablist, which may own tabs only (round 0.34, the showcase's axe check).\n // Visually hidden, since the × drawn on the tab is the pointer's, and not a tab stop,\n // since Delete is the keyboard's. `sr-only-fixed`: it sits wherever the caller put the\n // strip, with no positioned box of the kit's around it.\n const activeTab = tabs.find((tab) => tab.id === active);\n const removeButton =\n activeTab && isRemovable(activeTab) ? (\n <button\n ref={removeRef}\n type=\"button\"\n tabIndex={-1}\n disabled={busy}\n onClick={() => requestRemove(activeTab.id)}\n className=\"sr-only-fixed\"\n >\n {text.remove(nameOf(activeTab))}\n </button>\n ) : null;\n // The box the cues sit in, ‹ in the gutter before the strip and › in the one after it\n // (see `gutterAt`). There whether or not the strip overflows right now, so a resize\n // never re-parents it; the gutters only while it overflows at all.\n const gutters = cue && fade.overflow !== undefined;\n const stripBox = cue ? (\n <div className={cn(\"relative flex min-w-0\", !onAdd && \"flex-1\", gutters && \"px-[24px]\")}>\n {tablist}\n {gutters && gutterAt(\"start\", cueStart)}\n {gutters && gutterAt(\"end\", cueEnd)}\n </div>\n ) : (\n tablist\n );\n if (!onAdd && cue) {\n // `className` goes on the row around the strip — it is the box a caller's margin or\n // width is meant for, as with an add button — and stays there at every scroll position.\n return (\n <div className={cn(\"flex items-end\", className)}>\n {stripBox}\n {removeButton}\n </div>\n );\n }\n if (!onAdd) {\n // No outer box: the strip stays the root, as callers' `className` and tests expect.\n return removeButton ? (\n <>\n {tablist}\n {removeButton}\n </>\n ) : (\n tablist\n );\n }\n if (vertical) {\n // Under the column, not beside it, and with no rule to carry on: a side nav has none.\n return (\n <div className={cn(\"flex flex-col gap-0.5\", className)}>\n {tablist}\n {removeButton}\n <button type=\"button\" onClick={onAdd} disabled={busy} className={TAB_ADD_CLASSES}>\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n );\n }\n return (\n <div className={cn(\"flex items-end\", className)}>\n {stripBox}\n {removeButton}\n {/* Outside the tablist: an action, not a tab — and a tablist may own only\n tabs. The rule under the strip carries on beneath it. */}\n <div\n className=\"flex flex-1 items-center self-stretch border-b border-subtle\"\n >\n <button\n type=\"button\"\n onClick={onAdd}\n disabled={busy}\n className={cn(TAB_ADD_CLASSES, TAB_ADD_PHONE_CLASSES)}\n >\n <Plus aria-hidden />\n {/* Below md a scrolling strip keeps the room for tabs: the label is visually\n hidden and the \"+\" carries it as its name. Beside \"+ Add loop\" a 390px strip\n showed the open tab and a sliver of the next (showcase audit). */}\n <span className=\"max-md:sr-only\">{addLabel ?? text.add}</span>\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { useLayoutEffect, useMemo, useRef } from \"react\";\nimport { ChevronLeft, ChevronRight, Plus, X } from \"lucide-react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING_WIDTH } from \"./focus-ring\";\nimport { scrollIntoStrip, useStripFade } from \"../lib/strip-fade\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { prefersReducedMotion } from \"../lib/reduced-motion\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { mergeRefs } from \"./field-parts\";\nimport { DEFAULT_TABS_LABELS } from \"./ui-labels\";\nimport { IconButton } from \"./icon-button\";\nimport { HIT_AREA_LARGE } from \"./button-look\";\n\n/** One tab of a {@link Tabs} strip. */\nexport interface TabItem<T extends string> {\n id: T;\n /** A ReactNode so a tab can pair an icon with text. */\n label: ReactNode;\n /** Optional trailing node (e.g. a count pill). */\n badge?: ReactNode;\n /** Marks a tab that IS a route — see {@link TabsProps}. */\n href?: string;\n /** Leading decoration: an icon, or the colour swatches of the lines this tab\n * draws in a chart above the strip. Hidden from assistive tech — it repeats\n * what the label says, or it says something the label must say instead. */\n icon?: ReactNode;\n /** A second, smaller line under the label — the one fact that tells two tabs\n * with similar labels apart (\"80 km/h\", \"Level 3\"). Part of the tab's name. */\n detail?: ReactNode;\n /** A tab that exists as a slot with nothing in it yet, drawn with a dashed\n * outline so a gap in a sequence looks like a gap. Visual only: say \"empty\" in\n * the label or `detail` if a screen reader needs to know. */\n empty?: boolean;\n /** `false` keeps this one tab when the strip has `onRemove` — the one sheet a\n * workbook cannot be without. Default: removable. */\n removable?: boolean;\n /** Plain-text name for the labels the strip composes about this tab (\"Remove\n * …\"), for when `label` is not a string. Default: `label` if it is a string,\n * else the id. */\n name?: string;\n}\n\n/** The strings the strip renders on its own behalf — only when it can add or\n * remove tabs. */\nexport interface TabsLabels {\n /** The \"add\" button's visible text when the caller gives no `addLabel`. */\n add: string;\n /** Accessible name of a tab's × — \"Remove <tab>\". */\n remove: (tab: string) => string;\n /** The `overflowCue` › after the strip's end, while tabs lie past it: \"More tabs\". */\n more: string;\n /** The `overflowCue` ‹ before the strip's start, while tabs lie before it: \"Previous tabs\". */\n previous: string;\n}\n\n// Defined in ./ui-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_TABS_LABELS };\n\n/**\n * The strip's own props sit on a `<div>`: the tablist IS the root element, so anything\n * a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an\n * `aria-describedby` — lands there. `onChange` is omitted from the div's props because\n * this component's `onChange` hands over the chosen TAB ID, not a DOM event, and\n * `children` because the strip renders `tabs` — a caller who wants a PANEL wires it up\n * through `panelId`, which is the whole point of that prop.\n */\nexport interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n /** `label` is a ReactNode so tabs can pair an icon with text; `badge` is an\n * optional trailing node (e.g. a count pill). `href` marks a tab that IS a route:\n * it renders as an anchor so it can be middle-/⌘-clicked into a new tab (Keksdose\n * feedback #451), while a plain click still goes through `onChange` and stays\n * client-side. Tabs that only flip local state leave it unset — a link to a URL\n * that does not select the tab would be worse than no link. */\n tabs: TabItem<T>[];\n active: T;\n onChange: (id: T) => void;\n className?: string;\n /**\n * Carets at the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose\n * live #410 round 4): a › after the strip's end while tabs lie past it, and a ‹ before\n * its start while tabs lie before it — each there only while something is hidden\n * past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three\n * quarters of the strip's width toward its own side. Neither ever changes direction:\n * until 0.46 one caret turned round into \"back to the first\" at the end, and read as\n * an arrow pointing the wrong way. Start and end follow the reading direction.\n *\n * The edge fade alone could read as the end of the row: the last visible tab can\n * finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).\n * Since 0.48 (live #410 rounds 5–6) the carets are bare glyphs BESIDE the strip — the ‹\n * before it, the › after it — centred on the labels, muted and primary on hover, with\n * no frame, fill or shadow; until 0.47 they were framed buttons laid over the strip's\n * last visible tabs. While the strip overflows at all it keeps a 24px gutter at both\n * ends, and each caret sits in its own, so scrolling never changes the strip's width\n * or moves its labels; a strip that fits has no gutters. Out of the tab order —\n * the keyboard walks the strip with the arrow keys, which bring the focused tab into\n * view. At every width. Ignored on a vertical strip (a side nav is a column, not a\n * row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is\n * the reference this replaces.\n */\n overflowCue?: boolean;\n /**\n * `id` of the element the caller renders the open tab's content into — the missing\n * half of `role=\"tab\"`. A tab that controls nothing is a tab in name only: a screen\n * reader announces \"tab, 2 of 5\" and then has no way to take the user to what it\n * opened, and no way back.\n *\n * **This component does not render the panel, on purpose.** `Tabs` is the STRIP; the\n * content lives wherever the caller put it — three Keksdose pages render it in a\n * sibling `<Card>`, one renders it through a router outlet, and a tab can BE a route\n * (`href`), in which case the panel is a whole page this component never sees.\n * Wrapping `children` here would mean either moving that content into the strip's\n * subtree — a layout change on every page that uses tabs — or shipping a wrapper\n * that only some callers could use. So the two halves are joined by an id instead.\n *\n * Wire the other end yourself:\n *\n * ```tsx\n * <Tabs tabs={tabs} active={active} onChange={setActive} panelId=\"report-panel\" />\n * <div id=\"report-panel\" role=\"tabpanel\" aria-labelledby=\"report-panel-tab\" tabIndex={-1}>\n * ```\n *\n * `aria-controls` goes on the OPEN tab only, and `${panelId}-tab` is its id — the\n * closed tabs' panels are not in the document, and a dangling `aria-controls`\n * describes a tab as opening something that is not there.\n */\n panelId?: string;\n /**\n * Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab\n * (only on the open one with `removeOn=\"active\"`).\n *\n * The × is on the open tab only — a row of tabs each carrying one is a row of\n * things to hit by accident, and the tab you can remove should be the one you\n * are looking at — but its room is reserved on every removable tab, so opening a\n * tab never widens it and never pushes the rest of the strip along (lenkbank\n * feedback #72). It is not a tab stop of its own: the strip stays one stop, and the\n * keyboard path is Delete (announced through `aria-keyshortcuts`), which removes\n * whichever tab has focus (see `removeOn`).\n *\n * Assistive tech meets the remove as a \"Remove <tab>\" button right AFTER the strip,\n * not as the × (0.34): a tablist may own tabs only, so the drawn × is the pointer's\n * and `aria-hidden`, and the named button beside the tablist is visually hidden.\n *\n * The strip only ASKS: it calls this, and the tab goes when the caller drops it\n * from `tabs` — after a confirmation, a request, whatever it needs. When it does,\n * focus that was on the removed tab (or its ×) moves to the open tab if the\n * removed one was open, else to its neighbour, instead of falling to the page.\n */\n onRemove?: (id: T) => void;\n /**\n * Which tabs the Delete key removes.\n *\n * - `\"every\"` (default): any focused removable tab — the ARIA pattern's reading.\n * - `\"active\"`: only the OPEN tab, the same one that wears the ×. For a strip whose\n * remove is immediate and unconfirmed (lenkbank's sheet tabs delete a measurement\n * sheet at once), so arrowing across the strip and pressing Delete cannot take\n * out a sheet the user is not looking at. Delete on a closed tab does nothing,\n * and only the open tab announces the shortcut (`aria-keyshortcuts`).\n *\n * Either way the × is drawn on the open tab only and its room stays reserved on\n * every removable tab, so the strip never relayouts when the selection moves.\n */\n removeOn?: \"every\" | \"active\";\n /** Renders an \"add\" button after the last tab, outside the tablist — it is an\n * action, not a tab, and a tablist may own only tabs. */\n onAdd?: () => void;\n /** The add button's text. Say what is added — \"Add loop\", \"Add report\" says\n * more than \"Add\". Default: `tabs.add` from the {@link UiKitProvider}. */\n addLabel?: string;\n /** Overrides for this strip's strings. See {@link TabsLabels}. */\n labels?: LabelOverride<TabsLabels>;\n /** An add or a remove is in flight: the × and the add button are disabled and\n * Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */\n busy?: boolean;\n /**\n * `\"vertical\"`: the same strip as a SIDE NAV — one item per row, the open one\n * filled, ↑/↓ walking it (Home/End as before) and `aria-orientation=\"vertical\"` on\n * the tablist. For keksdose's admin page and settings page, which each hand-built\n * the identical sidebar with none of this strip's keyboard, roving tab stop or\n * routed-`href` handling. Everything else — `href` tabs, `badge`, `panelId`,\n * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the\n * badge moves to the row's end.\n *\n * **On a phone (`usePhoneLayout()`) it becomes the horizontal strip**, rather than\n * staying a column. A side nav only works beside its content; on\n * a phone it has to go ABOVE it, and eight full-width rows there push the panel\n * the user picked below the fold on every visit — the two keksdose pages both\n * collapsed theirs into a scrolling row by hand for exactly that reason. The\n * orientation is switched in JavaScript, not with `md:` classes, so the\n * `aria-orientation` a screen reader hears and the arrow keys that work always\n * match the layout on screen. The caller's own two-column layout has to stack at\n * the same breakpoint (`md:grid-cols-[14rem_1fr]`).\n */\n orientation?: \"horizontal\" | \"vertical\";\n /** The `role=\"tablist\"` strip, where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the strip's own ref, which scrolls the open tab into\n * view and measures the fade, so neither loses the element. */\n ref?: Ref<HTMLDivElement>;\n}\n\nconst TABLIST_CLASSES = \"flex gap-1 overflow-x-auto overflow-y-hidden border-b border-subtle\";\n// The side nav. A column of full-width rows with the open one FILLED rather than\n// underlined: an underline under one row of a list reads as a separator, not as a\n// selection. `--bg-active` is the kit's own selected-row fill (the sidebar's), so the\n// admin page's nav and the app's own sidebar mark \"you are here\" the same way.\nconst TABLIST_VERTICAL_CLASSES = \"flex flex-col gap-0.5\";\nconst TAB_VERTICAL_CLASSES =\n FOCUS_RING_WIDTH +\n \" 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-strong\";\nconst TAB_VERTICAL_ACTIVE_CLASSES = \"bg-active text-primary\";\nconst TAB_VERTICAL_INACTIVE_CLASSES = \"text-muted hover:bg-hover hover:text-primary\";\n\n// `max-w-full`: no tab outgrows its strip. A sheet's `detail` line (\"Spurstangenkraft\n// links · … · 31 Punkte\") made one tab 448px wide in a 390px strip, and a tab wider\n// than the strip cannot be scrolled into view — its end was simply off screen\n// (lenkbank L3). The label and detail truncate inside it instead; see `inner`.\nconst TAB_CLASSES =\n FOCUS_RING_WIDTH +\n \" 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-strong\";\nconst TAB_ACTIVE_CLASSES = \"border-[var(--text-primary)] text-primary\";\nconst TAB_INACTIVE_CLASSES = \"border-transparent text-muted hover:text-secondary hover:border-strong\";\n\n// The add/remove chrome. The × reserves its gutter on every removable tab (see\n// `onRemove`).\nconst TAB_REMOVABLE_CLASSES = \"pe-8\";\nconst TAB_EMPTY_CLASSES = \"-mx-1.5 rounded border border-dashed border-strong px-1.5\";\nconst TAB_ADD_CLASSES =\n FOCUS_RING_WIDTH +\n \" inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-muted transition-colors hover:text-primary focus:outline-none focus-visible:ring-strong disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4\";\n// `relative`: the phone's visually hidden label needs a positioned ancestor (see the\n// sr-only rule in CONTRIBUTING); `max-md:px-2` squares the lone \"+\".\nconst TAB_ADD_PHONE_CLASSES = \"relative max-md:px-2\";\n\nexport function Tabs<T extends string>({\n tabs,\n active,\n onChange,\n className,\n overflowCue = false,\n panelId,\n onRemove,\n removeOn = \"every\",\n onAdd,\n addLabel,\n labels,\n busy = false,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n ref,\n ...rest\n}: TabsProps<T>) {\n const text = useKitLabels(\"tabs\", DEFAULT_TABS_LABELS, labels);\n // See `orientation`: a vertical strip is the horizontal one on a phone.\n const phone = usePhoneLayout();\n const vertical = orientation === \"vertical\" && !phone;\n const stripRef = useRef<HTMLDivElement>(null);\n const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);\n const fade = useStripFade(stripRef);\n // See `overflowCue`: a ‹ before the strip while tabs lie before it, a › after it\n // while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.\n const cue = overflowCue && !vertical;\n const cueStart = cue && (fade.overflow === \"start\" || fade.overflow === \"both\");\n const cueEnd = cue && (fade.overflow === \"end\" || fade.overflow === \"both\");\n // Three quarters of the visible width toward `side`; an RTL strip runs leftward.\n const onCue = (side: \"start\" | \"end\") => {\n const strip = stripRef.current;\n if (!strip) return;\n const behavior: ScrollBehavior = prefersReducedMotion() ? \"auto\" : \"smooth\";\n const rtl = getComputedStyle(strip).direction === \"rtl\";\n const toward = (side === \"end\" ? 1 : -1) * (rtl ? -1 : 1);\n strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });\n };\n // In a GUTTER of its own at the strip's end (0.48, keksdose live #410 rounds 5–6): a\n // bare glyph — no frame, fill or shadow — muted until the pointer comes. Round 5 put\n // each cue in the flow beside the strip, so it took 24px the moment it appeared and the\n // labels jumped sideways at every edge the scroll crossed (\"It has problems with moving\n // to left and right especially when moving the scroll bar\"). Now the strip box keeps a\n // 24px gutter at BOTH ends for as long as the strip overflows at all (`px-[24px]`, see\n // `stripBox`), and each gutter is absolute in that padding, so a cue coming or going\n // never moves the strip, its labels or the scroll position under the thumb. A strip\n // that fits has no gutters: its width changes only when it starts or stops overflowing.\n // The gutter carries the strip's 1px rule on beneath the glyph, and lifts the glyph\n // onto the LABELS' centre rather than the box's: a tab's 2px underline (`border-b-2`)\n // sits on the rule (`-mb-px`), so the labels are centred 2px above the box's bottom —\n // the gutter's 1px rule (`border-b`) plus 1px more (`pb-px`).\n // 24px wide at every text size (KS N3): at Large `sm`'s 48px minimum made each cue\n // 48px, and at 360px the two left a two-tab strip 100px. 24 PIXELS, not `w-6`: the rem\n // grows with the text size (24 / 30 / 36px), and at Extra large on a 360px phone the two\n // gutters took 72px of a 226px strip and a sheet tab wrapped to seven lines (0.48 build\n // review 2, KS N1). The 48px touch target is an\n // invisible hit area round the glyph instead (`HIT_AREA_LARGE`, as `2xs`), over the\n // strip's faded edge, not room taken from the row. The wrapper inside the gutter is\n // ours because IconButton puts its tooltip's span around the button.\n const gutterAt = (side: \"start\" | \"end\", shown: boolean) => (\n <div\n data-tabs-cue-gutter={side}\n className={cn(\n \"absolute inset-y-0 flex w-[24px] items-center justify-center border-b border-subtle pb-px\",\n side === \"start\" ? \"start-0\" : \"end-0\",\n )}\n >\n {shown && (\n <div data-tabs-overflow-cue={side} className=\"flex\">\n <IconButton\n size=\"sm\"\n variant=\"ghost\"\n className={cn(\n \"w-[24px] large:min-w-0 bg-transparent text-muted hover:bg-transparent hover:text-primary\",\n // Over the strip's faded edge, for ‹ too.\n HIT_AREA_LARGE,\n \"large:after:z-[1]\",\n )}\n tabIndex={-1}\n type=\"button\"\n label={side === \"start\" ? text.previous : text.more}\n labelVisible={false}\n onClick={() => onCue(side)}\n >\n {side === \"start\" ? (\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n ) : (\n <ChevronRight className=\"rtl:-scale-x-100\" />\n )}\n </IconButton>\n </div>\n )}\n </div>\n );\n // The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a\n // deep link to the seventh report. A selection behind the cut edge was the worst\n // case of the overflow above: the one tab the panel below belongs to, sliced to\n // \"80 km,\" under the add button. Only the strip scrolls (`scrollBy` on it, not\n // `scrollIntoView`, which would scroll the page as well), and only as far as needed\n // to clear the fade.\n useLayoutEffect(() => {\n const strip = stripRef.current;\n const tab = strip?.querySelector<HTMLElement>('[role=\"tab\"][aria-selected=\"true\"]');\n if (strip && tab) scrollIntoStrip(strip, tab);\n }, [active]);\n const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;\n // Whether Delete reaches this tab: every removable tab, or only the open one.\n const deletesOnKey = (tab: TabItem<T>) => isRemovable(tab) && (removeOn === \"every\" || tab.id === active);\n const nameOf = (tab: TabItem<T>) => tab.name ?? (typeof tab.label === \"string\" ? tab.label : tab.id);\n\n // Where focus goes once a removal the strip asked for has happened. The caller\n // owns `tabs` and may take its time (a confirmation, a request), so this waits\n // for the id to actually leave the list rather than guessing at the moment.\n const pendingFocus = useRef<{ removed: T; wasActive: boolean; neighbour?: T } | null>(null);\n const removeRef = useRef<HTMLButtonElement>(null);\n const requestRemove = (id: T) => {\n const index = tabs.findIndex((t) => t.id === id);\n pendingFocus.current = {\n removed: id,\n wasActive: id === active,\n neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id,\n };\n onRemove?.(id);\n };\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending || tabs.some((t) => t.id === pending.removed)) return;\n pendingFocus.current = null;\n const strip = stripRef.current;\n if (!strip) return;\n // Only focus that fell out of the strip with the removed tab is ours to place —\n // or that sits on the remove button after it, which a pick removed through.\n // If the user has moved on — into the panel, into a dialog — leave it there.\n const focused = document.activeElement;\n if (focused && focused !== document.body && !strip.contains(focused) && focused !== removeRef.current) return;\n // The removed tab was open: focus follows the caller's new selection, which is\n // also the strip's tab stop. It was not: its neighbour, as the ARIA pattern has it.\n const target = !pending.wasActive && tabs.some((t) => t.id === pending.neighbour) ? pending.neighbour : active;\n const index = tabs.findIndex((t) => t.id === target);\n strip.querySelectorAll<HTMLElement>('[role=\"tab\"]')[index]?.focus();\n }, [tabs, active]);\n\n // Arrow keys walk the strip in DOM order (ARIA tabs pattern). Home/End jump\n // to the ends. Focus only — activation stays on click/Enter/Space, because a tab\n // here can be a real route and moving focus must not navigate.\n //\n // On the TABS, not on the tablist. The container carried it, which left a `<div>`\n // wearing an interactive role and a key handler while being unfocusable —\n // `jsx-a11y/interactive-supports-focus`, and the only instance of it left in `src/`.\n // The fix that rule wants is `tabIndex` on the div; the fix the pattern wants is\n // the handler on the elements that are natively focusable and actually receive the\n // keystroke. Those are the tabs, so the warning goes away by being right rather\n // than by being satisfied.\n const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {\n // Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable\n // tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it\n // rather than letting it fall through to the page. With `removeOn=\"active\"` only\n // the open tab answers; a closed one lets the key pass untouched.\n if (e.key === \"Delete\" && deletesOnKey(tab)) {\n e.preventDefault();\n if (!busy) requestRemove(tab.id);\n return;\n }\n // Along the reading direction: the strip is a flex row, so in RTL the NEXT tab sits\n // to the left, and ArrowLeft has to reach it. A vertical strip answers ↑/↓ instead\n // — and ONLY those, as the pattern has it: ←/→ are left to the page, where a side\n // nav's neighbour (the panel) may well want them.\n const step: 1 | -1 | 0 = vertical\n ? e.key === \"ArrowDown\"\n ? 1\n : e.key === \"ArrowUp\"\n ? -1\n : 0\n : horizontalStep(e.key, e.currentTarget);\n if (step === 0 && e.key !== \"Home\" && e.key !== \"End\") return;\n const strip = e.currentTarget.closest('[role=\"tablist\"]');\n if (!strip) return;\n const items = Array.from(strip.querySelectorAll<HTMLElement>('[role=\"tab\"]'));\n const from = items.indexOf(e.currentTarget);\n if (from === -1) return;\n e.preventDefault();\n const to = e.key === \"Home\" ? 0 : e.key === \"End\" ? items.length - 1 : (from + step + items.length) % items.length;\n items[to]?.focus();\n };\n const tablist = (\n <div\n // Before the role and the name: a caller's arbitrary attribute is welcome on the\n // strip, but a `role` or an `aria-label` arriving through a spread props object\n // must not be able to unmake the tablist the tabs below are registered against.\n {...rest}\n ref={setStrip}\n role=\"tablist\"\n // Names the GROUP — every tab carries its own text — so a strip under a visible\n // heading that already says it can leave it unset.\n aria-label={ariaLabel}\n // Only when vertical: `horizontal` is the tablist's implicit value, and the\n // attribute on every existing strip would be noise in every snapshot of them.\n aria-orientation={vertical ? \"vertical\" : undefined}\n // Merged over a caller's own style rather than replacing it; see useStripFade.\n // `data-overflow` names the cut ends in reading-direction terms, for a caller's\n // own affordance (a scroll button) and for tests, which cannot read the mask.\n style={fade.mask ? { ...rest.style, maskImage: fade.mask, WebkitMaskImage: fade.mask } : rest.style}\n data-overflow={fade.overflow}\n // With an add button the strip gains an outer box, and `className` goes there\n // — it is the box a caller's margin or width is meant for.\n className={cn(\n vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,\n cue ? \"min-w-0 flex-1\" : onAdd ? \"min-w-0\" : className,\n )}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === active;\n const removable = isRemovable(tab);\n // Written once and worn by either tag below, so a routed tab and a\n // state-only tab stay indistinguishable to the eye and to a screen reader.\n const shared = {\n role: \"tab\" as const,\n \"aria-selected\": isActive,\n // Roving tabindex: the whole strip is ONE stop in the page's tab order, and\n // it is the open tab. Ten report tabs otherwise cost ten Tab presses to step\n // over on the way to the table below them (live #262's strip is the extreme\n // case, but every strip in the app paid it).\n //\n // The stop follows the SELECTION, not the focus. Arrowing is a look around —\n // activation here is manual, because a tab can be a real route — so re-\n // pointing the stop at a report the user only arrowed past would hand the\n // strip back on the next visit in a state they never chose. The cost is that\n // Tab out and back returns to the open tab rather than to the last one\n // looked at; the ARIA pattern allows either, and this one needs no second\n // piece of state shadowing `active`.\n tabIndex: isActive ? 0 : -1,\n // Only the open tab: see `panelId`.\n \"aria-controls\": isActive ? panelId : undefined,\n id: isActive && panelId ? `${panelId}-tab` : undefined,\n \"aria-keyshortcuts\": deletesOnKey(tab) ? \"Delete\" : undefined,\n onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),\n className: vertical\n ? cn(\n TAB_VERTICAL_CLASSES,\n isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,\n removable && \"pe-8\",\n )\n : cn(TAB_CLASSES, isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES, removable && TAB_REMOVABLE_CLASSES),\n };\n const inner = (\n <span\n className={cn(\n // In a side nav the row is the full width, so the label takes it and a\n // badge lands at the row's end, where a column of counts lines up.\n vertical\n ? \"flex min-w-0 flex-1 items-center gap-1.5\"\n : \"inline-flex max-w-full min-w-0 items-center gap-1.5\",\n tab.empty && TAB_EMPTY_CLASSES,\n )}\n >\n {tab.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 items-center gap-0.5\">\n {tab.icon}\n </span>\n )}\n {tab.detail != null ? (\n // `min-w-0` + `truncate-until-large` on each line: capped by the tab's\n // `max-w-full`, a long detail ends in an ellipsis at Normal and wraps from\n // Large up (text size §10.17; round 0.34 T1), where the tab grows a line\n // rather than cut the words. The whole text stays the tab's name.\n <span className=\"flex min-w-0 flex-col items-start text-start\">\n <span className=\"max-w-full truncate-until-large\">{tab.label}</span>\n {/* A separator for the NAME only — whitespace between flex items is\n not drawn, and without it the two lines ran together into one\n word for a screen reader (\"80 km/hloop 2\"). */}{\" \"}\n <span className=\"max-w-full truncate-until-large text-caption font-normal leading-tight text-muted\">\n {tab.detail}\n </span>\n </span>\n ) : (\n tab.label\n )}\n {tab.badge != null ? (\n vertical ? (\n <span className=\"ms-auto flex shrink-0 items-center\">{tab.badge}</span>\n ) : (\n tab.badge\n )\n ) : null}\n </span>\n );\n const element = tab.href ? (\n <a\n key={tab.id}\n {...shared}\n href={tab.href}\n draggable={false}\n onClick={(e) => {\n // Middle-, ⌘/Ctrl-, Shift- and Alt-click belong to the browser\n // (feedback #451); only the plain left click is ours to cancel and\n // route through `onChange`, which keeps the switch client-side.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n onChange(tab.id);\n }}\n // A <button> tab activated on Space; an anchor does not, so becoming a\n // link (feedback #451) would have quietly cost keyboard users the tab\n // strip. Enter is deliberately untouched — it is the anchor's own\n // default and already arrives at `onChange` through the click handler\n // above, so handling it here too would switch tabs twice. Same three\n // lines the mobile row card carries, for the same reason; the rejected\n // alternative — keeping a button and faking the middle click with\n // window.open — is what the whole item is moving away from.\n //\n // Composed with the strip's arrow-key walk rather than replacing it: this\n // prop overrides the one in `shared`, and an anchor tab that lost the\n // arrows would be a strip that is navigable only where it is not a link.\n onKeyDown={(e) => {\n onTabKeyDown(e, tab);\n if (e.defaultPrevented || e.key !== \" \") return;\n e.preventDefault();\n onChange(tab.id);\n }}\n >\n {inner}\n </a>\n ) : (\n <button key={tab.id} {...shared} onClick={() => onChange(tab.id)}>\n {inner}\n </button>\n );\n if (!removable) return element;\n return (\n // The × cannot go INSIDE the tab: a button in a button is invalid HTML, and\n // a tab's children are presentational, so a reader would flatten it into the\n // tab's name. It sits beside the tab, over the gutter the tab reserves.\n <div key={tab.id} className={cn(\"relative flex max-w-full\", !vertical && \"shrink-0\")}>\n {element}\n {isActive && (\n <IconButton\n size=\"2xs\"\n tone=\"danger\"\n // The POINTER's ×, hidden from assistive tech (round 0.34, the showcase's\n // axe check): a tablist may own tabs only, and this button sat in it. The\n // named \"Remove <tab>\" a screen reader's cursor and a touch reader's swipe\n // reach is `removeButton`, after the strip. Not a tab stop either way:\n // the strip is one, and Delete is the keyboard's path.\n aria-hidden\n data-tab-remove=\"\"\n tabIndex={-1}\n type=\"button\"\n disabled={busy}\n onClick={() => requestRemove(tab.id)}\n className=\"absolute inset-y-0 end-1 my-auto\"\n >\n <X />\n </IconButton>\n )}\n </div>\n );\n })}\n </div>\n );\n // The open tab's remove action as assistive tech meets it: right after the strip,\n // OUTSIDE the tablist, which may own tabs only (round 0.34, the showcase's axe check).\n // Visually hidden, since the × drawn on the tab is the pointer's, and not a tab stop,\n // since Delete is the keyboard's. `sr-only-fixed`: it sits wherever the caller put the\n // strip, with no positioned box of the kit's around it.\n const activeTab = tabs.find((tab) => tab.id === active);\n const removeButton =\n activeTab && isRemovable(activeTab) ? (\n <button\n ref={removeRef}\n type=\"button\"\n tabIndex={-1}\n disabled={busy}\n onClick={() => requestRemove(activeTab.id)}\n className=\"sr-only-fixed\"\n >\n {text.remove(nameOf(activeTab))}\n </button>\n ) : null;\n // The box the cues sit in, ‹ in the gutter before the strip and › in the one after it\n // (see `gutterAt`). There whether or not the strip overflows right now, so a resize\n // never re-parents it; the gutters only while it overflows at all.\n const gutters = cue && fade.overflow !== undefined;\n const stripBox = cue ? (\n <div className={cn(\"relative flex min-w-0\", !onAdd && \"flex-1\", gutters && \"px-[24px]\")}>\n {tablist}\n {gutters && gutterAt(\"start\", cueStart)}\n {gutters && gutterAt(\"end\", cueEnd)}\n </div>\n ) : (\n tablist\n );\n if (!onAdd && cue) {\n // `className` goes on the row around the strip — it is the box a caller's margin or\n // width is meant for, as with an add button — and stays there at every scroll position.\n return (\n <div className={cn(\"flex items-end\", className)}>\n {stripBox}\n {removeButton}\n </div>\n );\n }\n if (!onAdd) {\n // No outer box: the strip stays the root, as callers' `className` and tests expect.\n return removeButton ? (\n <>\n {tablist}\n {removeButton}\n </>\n ) : (\n tablist\n );\n }\n if (vertical) {\n // Under the column, not beside it, and with no rule to carry on: a side nav has none.\n return (\n <div className={cn(\"flex flex-col gap-0.5\", className)}>\n {tablist}\n {removeButton}\n <button type=\"button\" onClick={onAdd} disabled={busy} className={TAB_ADD_CLASSES}>\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n );\n }\n return (\n <div className={cn(\"flex items-end\", className)}>\n {stripBox}\n {removeButton}\n {/* Outside the tablist: an action, not a tab — and a tablist may own only\n tabs. The rule under the strip carries on beneath it. */}\n <div className=\"flex flex-1 items-center self-stretch border-b border-subtle\">\n <button type=\"button\" onClick={onAdd} disabled={busy} className={cn(TAB_ADD_CLASSES, TAB_ADD_PHONE_CLASSES)}>\n <Plus aria-hidden />\n {/* Below md a scrolling strip keeps the room for tabs: the label is visually\n hidden and the \"+\" carries it as its name. Beside \"+ Add loop\" a 390px strip\n showed the open tab and a sliver of the next (showcase audit). */}\n <span className=\"max-md:sr-only\">{addLabel ?? text.add}</span>\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAgUc,SA8TR,UA9TQ,KAoLA,YApLA;AAhUd,SAAS,iBAAiB,SAAS,cAAc;AACjD,SAAS,aAAa,cAAc,MAAM,SAAS;AAEnD,SAAS,UAAU;AACnB,SAAS,wBAAwB;AACjC,SAAS,iBAAiB,oBAAoB;AAC9C,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,oBAAwC;AACjD,SAAS,iBAAiB;AAC1B,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AA4L/B,MAAM,kBAAkB;AAKxB,MAAM,2BAA2B;AACjC,MAAM,uBACJ,mBACA;AACF,MAAM,8BAA8B;AACpC,MAAM,gCAAgC;AAMtC,MAAM,cACJ,mBACA;AACF,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAI7B,MAAM,wBAAwB;AAC9B,MAAM,oBAAoB;AAC1B,MAAM,kBACJ,mBACA;AAGF,MAAM,wBAAwB;AAEvB,SAAS,KAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,cAAc;AAAA,EACd,cAAc;AAAA,EACd;AAAA,EACA,GAAG;AACL,GAAiB;AACf,QAAM,OAAO,aAAa,QAAQ,qBAAqB,MAAM;AAE7D,QAAM,QAAQ,eAAe;AAC7B,QAAM,WAAW,gBAAgB,cAAc,CAAC;AAChD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,WAAW,QAAQ,MAAM,UAAU,UAAU,GAAG,GAAG,CAAC,GAAG,CAAC;AAC9D,QAAM,OAAO,aAAa,QAAQ;AAGlC,QAAM,MAAM,eAAe,CAAC;AAC5B,QAAM,WAAW,QAAQ,KAAK,aAAa,WAAW,KAAK,aAAa;AACxE,QAAM,SAAS,QAAQ,KAAK,aAAa,SAAS,KAAK,aAAa;AAEpE,QAAM,QAAQ,CAAC,SAA0B;AACvC,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AACZ,UAAM,WAA2B,qBAAqB,IAAI,SAAS;AACnE,UAAM,MAAM,iBAAiB,KAAK,EAAE,cAAc;AAClD,UAAM,UAAU,SAAS,QAAQ,IAAI,OAAO,MAAM,KAAK;AACvD,UAAM,SAAS,EAAE,MAAM,SAAS,KAAK,MAAM,MAAM,cAAc,IAAI,GAAG,SAAS,CAAC;AAAA,EAClF;AAsBA,QAAM,WAAW,CAAC,MAAuB,UACvC;AAAA,IAAC;AAAA;AAAA,MACC,wBAAsB;AAAA,MACtB,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU,YAAY;AAAA,MACjC;AAAA,MAEC,mBACC,oBAAC,SAAI,0BAAwB,MAAM,WAAU,QAC3C;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,WAAW;AAAA,YACT;AAAA;AAAA,YAEA;AAAA,YACA;AAAA,UACF;AAAA,UACA,UAAU;AAAA,UACV,MAAK;AAAA,UACL,OAAO,SAAS,UAAU,KAAK,WAAW,KAAK;AAAA,UAC/C,cAAc;AAAA,UACd,SAAS,MAAM,MAAM,IAAI;AAAA,UAExB,mBAAS,UACR,oBAAC,eAAY,WAAU,oBAAmB,IAE1C,oBAAC,gBAAa,WAAU,oBAAmB;AAAA;AAAA,MAE/C,GACF;AAAA;AAAA,EAEJ;AAQF,kBAAgB,MAAM;AACpB,UAAM,QAAQ,SAAS;AACvB,UAAM,MAAM,OAAO,cAA2B,oCAAoC;AAClF,QAAI,SAAS,IAAK,iBAAgB,OAAO,GAAG;AAAA,EAC9C,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,cAAc,CAAC,QAAoB,aAAa,UAAa,IAAI,cAAc;AAErF,QAAM,eAAe,CAAC,QAAoB,YAAY,GAAG,MAAM,aAAa,WAAW,IAAI,OAAO;AAClG,QAAM,SAAS,CAAC,QAAoB,IAAI,SAAS,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,IAAI;AAKjG,QAAM,eAAe,OAAiE,IAAI;AAC1F,QAAM,YAAY,OAA0B,IAAI;AAChD,QAAM,gBAAgB,CAAC,OAAU;AAC/B,UAAM,QAAQ,KAAK,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE;AAC/C,iBAAa,UAAU;AAAA,MACrB,SAAS;AAAA,MACT,WAAW,OAAO;AAAA,MAClB,YAAY,KAAK,QAAQ,CAAC,KAAK,KAAK,QAAQ,CAAC,IAAI;AAAA,IACnD;AACA,eAAW,EAAE;AAAA,EACf;AACA,kBAAgB,MAAM;AACpB,UAAM,UAAU,aAAa;AAC7B,QAAI,CAAC,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,OAAO,EAAG;AAC5D,iBAAa,UAAU;AACvB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAIZ,UAAM,UAAU,SAAS;AACzB,QAAI,WAAW,YAAY,SAAS,QAAQ,CAAC,MAAM,SAAS,OAAO,KAAK,YAAY,UAAU,QAAS;AAGvG,UAAM,SAAS,CAAC,QAAQ,aAAa,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,SAAS,IAAI,QAAQ,YAAY;AACxG,UAAM,QAAQ,KAAK,UAAU,CAAC,MAAM,EAAE,OAAO,MAAM;AACnD,UAAM,iBAA8B,cAAc,EAAE,KAAK,GAAG,MAAM;AAAA,EACpE,GAAG,CAAC,MAAM,MAAM,CAAC;AAajB,QAAM,eAAe,CAAC,GAA+B,QAAoB;AAKvE,QAAI,EAAE,QAAQ,YAAY,aAAa,GAAG,GAAG;AAC3C,QAAE,eAAe;AACjB,UAAI,CAAC,KAAM,eAAc,IAAI,EAAE;AAC/B;AAAA,IACF;AAKA,UAAM,OAAmB,WACrB,EAAE,QAAQ,cACR,IACA,EAAE,QAAQ,YACR,KACA,IACJ,eAAe,EAAE,KAAK,EAAE,aAAa;AACzC,QAAI,SAAS,KAAK,EAAE,QAAQ,UAAU,EAAE,QAAQ,MAAO;AACvD,UAAM,QAAQ,EAAE,cAAc,QAAQ,kBAAkB;AACxD,QAAI,CAAC,MAAO;AACZ,UAAM,QAAQ,MAAM,KAAK,MAAM,iBAA8B,cAAc,CAAC;AAC5E,UAAM,OAAO,MAAM,QAAQ,EAAE,aAAa;AAC1C,QAAI,SAAS,GAAI;AACjB,MAAE,eAAe;AACjB,UAAM,KAAK,EAAE,QAAQ,SAAS,IAAI,EAAE,QAAQ,QAAQ,MAAM,SAAS,KAAK,OAAO,OAAO,MAAM,UAAU,MAAM;AAC5G,UAAM,EAAE,GAAG,MAAM;AAAA,EACnB;AACA,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MAIE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,MAAK;AAAA,MAGL,cAAY;AAAA,MAGZ,oBAAkB,WAAW,aAAa;AAAA,MAI1C,OAAO,KAAK,OAAO,EAAE,GAAG,KAAK,OAAO,WAAW,KAAK,MAAM,iBAAiB,KAAK,KAAK,IAAI,KAAK;AAAA,MAC9F,iBAAe,KAAK;AAAA,MAGpB,WAAW;AAAA,QACT,WAAW,2BAA2B;AAAA,QACtC,MAAM,mBAAmB,QAAQ,YAAY;AAAA,MAC/C;AAAA,MAEC,eAAK,IAAI,CAAC,QAAQ;AACjB,cAAM,WAAW,IAAI,OAAO;AAC5B,cAAM,YAAY,YAAY,GAAG;AAGjC,cAAM,SAAS;AAAA,UACb,MAAM;AAAA,UACN,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAajB,UAAU,WAAW,IAAI;AAAA;AAAA,UAEzB,iBAAiB,WAAW,UAAU;AAAA,UACtC,IAAI,YAAY,UAAU,GAAG,OAAO,SAAS;AAAA,UAC7C,qBAAqB,aAAa,GAAG,IAAI,WAAW;AAAA,UACpD,WAAW,CAAC,MAAkC,aAAa,GAAG,GAAG;AAAA,UACjE,WAAW,WACP;AAAA,YACE;AAAA,YACA,WAAW,8BAA8B;AAAA,YACzC,aAAa;AAAA,UACf,IACA,GAAG,aAAa,WAAW,qBAAqB,sBAAsB,aAAa,qBAAqB;AAAA,QAC9G;AACA,cAAM,QACJ;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA;AAAA;AAAA,cAGT,WACI,6CACA;AAAA,cACJ,IAAI,SAAS;AAAA,YACf;AAAA,YAEC;AAAA,kBAAI,QAAQ,QACX,oBAAC,UAAK,eAAW,MAAC,WAAU,6CACzB,cAAI,MACP;AAAA,cAED,IAAI,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKb,qBAAC,UAAK,WAAU,gDACd;AAAA,sCAAC,UAAK,WAAU,mCAAmC,cAAI,OAAM;AAAA,kBAGT;AAAA,kBACpD,oBAAC,UAAK,WAAU,qFACb,cAAI,QACP;AAAA,mBACF;AAAA,kBAEA,IAAI;AAAA,cAEL,IAAI,SAAS,OACZ,WACE,oBAAC,UAAK,WAAU,sCAAsC,cAAI,OAAM,IAEhE,IAAI,QAEJ;AAAA;AAAA;AAAA,QACN;AAEF,cAAM,UAAU,IAAI,OAClB;AAAA,UAAC;AAAA;AAAA,YAEE,GAAG;AAAA,YACJ,MAAM,IAAI;AAAA,YACV,WAAW;AAAA,YACX,SAAS,CAAC,MAAM;AAId,kBAAI,EAAE,WAAW,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,OAAQ;AACxE,gBAAE,eAAe;AACjB,uBAAS,IAAI,EAAE;AAAA,YACjB;AAAA,YAaA,WAAW,CAAC,MAAM;AAChB,2BAAa,GAAG,GAAG;AACnB,kBAAI,EAAE,oBAAoB,EAAE,QAAQ,IAAK;AACzC,gBAAE,eAAe;AACjB,uBAAS,IAAI,EAAE;AAAA,YACjB;AAAA,YAEC;AAAA;AAAA,UA/BI,IAAI;AAAA,QAgCX,IAEA,oBAAC,YAAqB,GAAG,QAAQ,SAAS,MAAM,SAAS,IAAI,EAAE,GAC5D,mBADU,IAAI,EAEjB;AAEF,YAAI,CAAC,UAAW,QAAO;AACvB;AAAA;AAAA;AAAA;AAAA,UAIE,qBAAC,SAAiB,WAAW,GAAG,4BAA4B,CAAC,YAAY,UAAU,GAChF;AAAA;AAAA,YACA,YACC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,MAAK;AAAA,gBAML,eAAW;AAAA,gBACX,mBAAgB;AAAA,gBAChB,UAAU;AAAA,gBACV,MAAK;AAAA,gBACL,UAAU;AAAA,gBACV,SAAS,MAAM,cAAc,IAAI,EAAE;AAAA,gBACnC,WAAU;AAAA,gBAEV,8BAAC,KAAE;AAAA;AAAA,YACL;AAAA,eApBM,IAAI,EAsBd;AAAA;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAOF,QAAM,YAAY,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,MAAM;AACtD,QAAM,eACJ,aAAa,YAAY,SAAS,IAChC;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,MAAK;AAAA,MACL,UAAU;AAAA,MACV,UAAU;AAAA,MACV,SAAS,MAAM,cAAc,UAAU,EAAE;AAAA,MACzC,WAAU;AAAA,MAET,eAAK,OAAO,OAAO,SAAS,CAAC;AAAA;AAAA,EAChC,IACE;AAIN,QAAM,UAAU,OAAO,KAAK,aAAa;AACzC,QAAM,WAAW,MACf,qBAAC,SAAI,WAAW,GAAG,yBAAyB,CAAC,SAAS,UAAU,WAAW,WAAW,GACnF;AAAA;AAAA,IACA,WAAW,SAAS,SAAS,QAAQ;AAAA,IACrC,WAAW,SAAS,OAAO,MAAM;AAAA,KACpC,IAEA;AAEF,MAAI,CAAC,SAAS,KAAK;AAGjB,WACE,qBAAC,SAAI,WAAW,GAAG,kBAAkB,SAAS,GAC3C;AAAA;AAAA,MACA;AAAA,OACH;AAAA,EAEJ;AACA,MAAI,CAAC,OAAO;AAEV,WAAO,eACL,iCACG;AAAA;AAAA,MACA;AAAA,OACH,IAEA;AAAA,EAEJ;AACA,MAAI,UAAU;AAEZ,WACE,qBAAC,SAAI,WAAW,GAAG,yBAAyB,SAAS,GAClD;AAAA;AAAA,MACA;AAAA,MACD,qBAAC,YAAO,MAAK,UAAS,SAAS,OAAO,UAAU,MAAM,WAAW,iBAC/D;AAAA,4BAAC,QAAK,eAAW,MAAC;AAAA,QACjB,YAAY,KAAK;AAAA,SACpB;AAAA,OACF;AAAA,EAEJ;AACA,SACE,qBAAC,SAAI,WAAW,GAAG,kBAAkB,SAAS,GAC3C;AAAA;AAAA,IACA;AAAA,IAGD,oBAAC,SAAI,WAAU,gEACb,+BAAC,YAAO,MAAK,UAAS,SAAS,OAAO,UAAU,MAAM,WAAW,GAAG,iBAAiB,qBAAqB,GACxG;AAAA,0BAAC,QAAK,eAAW,MAAC;AAAA,MAIlB,oBAAC,UAAK,WAAU,kBAAkB,sBAAY,KAAK,KAAI;AAAA,OACzD,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/text-link.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent } from \"../i18n/kit-labels\";\nimport { documentNavigation } from \"../lib/document-navigation\";\n\n/* ── Choosing what draws a link ──────────────────────────────────────────── */\n\n/**\n * An `href` that leaves the app: a scheme (`https:`, `mailto:`, `tel:`) or a\n * protocol-relative `//host`. A router's link is for paths inside the app, so the\n * provider's `linkComponent` is never handed one of these — they stay a plain `<a>`.\n *\n * NOT external, and so handed to the router link: a path (`/x`, `x`, `?q=1`) and the\n * hash-router form `#/x` — that IS a route in a `HashRouter` app, which maps it to\n * `to=\"/x\"`. A plain in-page anchor (`#section`) is not external either, but it is not\n * a route: see {@link isInPageAnchor}.\n */\nexport function isExternalHref(href: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(href);\n}\n\n/**\n * An `href` that only scrolls the current page: `#section` (or a bare `#`). The\n * hash-router form `#/x` is a ROUTE, not an anchor, and is not one of these. A router\n * link must not get an anchor — under a `HashRouter` `#section` would navigate to the\n * path `/section` — so every kit link draws it as a plain `<a>`.\n */\nexport function isInPageAnchor(href: string): boolean {\n return href.startsWith(\"#\") && !href.startsWith(\"#/\");\n}\n\n/**\n * Whether the provider's `linkComponent` (or a kit component's router fallback) draws\n * `href`: an in-app path, `#/x` included — not an external `href`\n * ({@link isExternalHref}) and not an in-page `#anchor` ({@link isInPageAnchor}).\n */\nexport function isRoutableHref(href: string): boolean {\n return !isExternalHref(href) && !isInPageAnchor(href);\n}\n\n/**\n * What draws a kit link to `href`: the component's own `renderLink` when it was given\n * one (it always wins — the caller asked for it by name), else the\n * `<UiKitProvider linkComponent>` for a routable `href` ({@link isRoutableHref}), else\n * `undefined` — a plain `<a>`. The one rule every kit link follows: `https:` /\n * `mailto:` / `//host` and `#section` stay plain anchors; `/x`, `x` and a hash-router\n * `#/x` go to the router link. Every kit link props type is a subset of the anchor\n * attributes `KitLinkProps` describes, which is what makes the cast sound.\n * @internal Shared by the kit's link-bearing components; not part of the barrel.\n */\nexport function pickLinkRenderer<P>(\n own: ((props: P) => ReactElement) | undefined,\n kitLink: KitLinkComponent | undefined,\n href: string | undefined,\n): ((props: P) => ReactElement) | undefined {\n if (own) return own;\n if (!kitLink || href === undefined || !isRoutableHref(href)) return undefined;\n return kitLink as unknown as (props: P) => ReactElement;\n}\n\n/** The plain primary click a page navigates on — not a middle click, not ⌘ / Ctrl /\n * Shift / Alt (new tab, new window, download), and not one a handler already took. */\nfunction isPlainNavigationClick(event: MouseEvent<HTMLAnchorElement>): boolean {\n return (\n !event.defaultPrevented &&\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\n );\n}\n\n/**\n * The `onClick` of a PLAIN `<a>` whose kit link was asked to `replace` (keksdose F1).\n *\n * A router link replaces the history entry itself (it is handed `replace`, see\n * {@link routerLinkNavigation}); a plain `<a>` has no attribute that says so, and the\n * browser's own navigation always pushes. So a plain click is taken over — the caller's\n * `onClick` runs first and may still `preventDefault` — and becomes\n * `location.replace(href)`: the same document load, with Back skipping the one-shot URL.\n * That is the case of `replace` together with `reloadDocument`, or an app with no\n * `linkComponent`. A modified click (new tab) and a `target` other than `_self` are left\n * to the browser: they do not navigate THIS history. Without `replace`, `onClick` comes\n * back untouched.\n * @internal\n */\nexport function replacingClick(\n onClick: ((event: MouseEvent<HTMLAnchorElement>) => void) | undefined,\n href: string,\n replace: boolean | undefined,\n target?: string,\n): ((event: MouseEvent<HTMLAnchorElement>) => void) | undefined {\n if (!replace || (target !== undefined && target !== \"\" && target !== \"_self\")) return onClick;\n return (event) => {\n onClick?.(event);\n if (!isPlainNavigationClick(event)) return;\n event.preventDefault();\n documentNavigation.replace(href);\n };\n}\n\n/**\n * What a ROUTER link is handed for `replace`: `{ replace: true }` when asked, else\n * nothing at all — so a provider link that spreads its props onto a DOM `<a>` never\n * receives an unknown attribute from a link that did not ask (see `KitLinkProps.replace`).\n * @internal\n */\nexport function routerLinkNavigation(replace: boolean | undefined): { replace?: true } {\n return replace ? { replace: true } : {};\n}\n\n/**\n * Draws `props` through `render` as a component of its own, so a router link's hooks\n * belong to it and not to the kit component calling it — for the kit components that\n * call their own `renderLink` inline and reach the provider's link through here.\n * @internal\n */\nexport function RenderedKitLink<P>({ render, props }: { render: (props: P) => ReactElement; props: P }) {\n return render(props);\n}\n\n/* ── TextLink ─────────────────────────────────────────────────────────────── */\n\n/**\n * `brand` (default) for a link in running text, `muted` for footer and meta links\n * that should not compete with the content, `danger` for a destructive one (\"Leave\n * group\"), `inherit` for a link that takes its surroundings' colour.\n *\n * `primary` and `secondary` are the body and the secondary text colour, UNDERLINED\n * (keksdose F3): a link that must read as part of the text around it — a name in a\n * list row, a reference in a card's meta line — where `brand` repaints it in the accent\n * and `muted` is a step too light to read as content. `secondary` darkens to the body\n * colour under the pointer, as `muted` does; `primary` has nowhere darker to go, so its\n * underline thickens instead.\n *\n * `warning` is the kit's `--warning` text colour, underlined (kastlan 46): a link inside\n * a `<Tone tone=\"warning\">` sentence or a warning banner, which `brand` would paint in a\n * second hue and `inherit` would leave indistinguishable from the sentence. Like\n * `primary`, it keeps its colour on hover and thickens the underline.\n *\n * The three colours that sit IN the text default to `underline=\"always\"`: with the\n * colour of their surroundings, the underline is the only thing that says \"link\"\n * (WCAG 1.4.1). An explicit `underline` still wins.\n */\nexport type TextLinkTone = \"brand\" | \"muted\" | \"danger\" | \"inherit\" | \"primary\" | \"secondary\" | \"warning\";\n\n/** `hover` (the default for `brand`, `muted`, `danger`, `inherit`): underlined under\n * the pointer — the idiom all three apps draw. `always` (the default for `primary`,\n * `secondary`, `warning`) for a link inside prose, where colour alone must not be the\n * only mark (WCAG 1.4.1). `none` where the context already says \"link\" (a footer nav). */\nexport type TextLinkUnderline = \"hover\" | \"always\" | \"none\";\n\n/** The `aria-current` token. `true` on {@link TextLinkProps.current} means `page`. */\nexport type TextLinkCurrent = \"page\" | \"step\" | \"location\" | \"date\" | \"time\" | \"true\";\n\n/** What {@link TextLinkProps.renderLink} (and the provider's `linkComponent`) is\n * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface TextLinkRenderProps {\n href: string;\n /** The link's look — keep it, or the tone, underline and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: TextLinkCurrent;\n id?: string;\n title?: string;\n /** Present, as `true`, only when the link was given `replace` — see\n * `KitLinkProps.replace`. Map it to your router's replace; never onto a DOM `<a>`. */\n replace?: boolean;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface TextLinkProps\n extends Omit<ComponentPropsWithoutRef<\"a\">, \"href\" | \"children\" | \"aria-current\" | \"onClick\" | \"onKeyDown\"> {\n href: string;\n children: ReactNode;\n /** Default `brand`. */\n tone?: TextLinkTone;\n /** Default `always` for `primary`, `secondary` and `warning`, else `hover`. */\n underline?: TextLinkUnderline;\n /**\n * Navigate by replacing the current history entry, so Back does not return to this\n * URL — keksdose F1, a one-shot URL (a confirmation landing, a `?done=1` hop). Handed\n * to the router link as `replace` (see `KitLinkProps.replace` for mapping it); on a\n * plain `<a>` (no `linkComponent`, or `reloadDocument`) a plain click becomes\n * `location.replace(href)`. Ignored on an `external` link, which opens a new tab.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`: the browser loads the whole document instead of\n * the router swapping the view — keksdose F1, a route served outside the SPA (a file\n * download, a server-rendered page, a logout that must drop every in-memory cache).\n * Wins over `renderLink` and the provider's `linkComponent`: it is the caller saying\n * \"not the router\" for this one link.\n */\n reloadDocument?: boolean;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`\n * (never the router's), an external-link mark after the text (mirrored in RTL), and\n * \"opens in a new tab\" (`common.opensInNewTab`) read after it — the mark says it\n * only to the eye. kastlan's BWO / BFS source links (rent-increase-calculator-page).\n */\n external?: boolean;\n /**\n * Keep the click (and the Enter that activates the link) from reaching an ancestor —\n * a link in a DataTable row with `onRowClick` goes to its own target instead of\n * running the row's action. kastlan's `CellLink` did this by hand.\n */\n stopPropagation?: boolean;\n /** Mark the link as the current one of a set — `true` is `aria-current=\"page\"`, the\n * legal footer's link to the page you are on. Drawn in semibold. */\n current?: boolean | TextLinkCurrent;\n /** A Lucide icon before the text, at the text's size — kastlan's \"← Back\" link. */\n icon?: LucideIcon;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then a plain `<a>`. Ignored on an `external` link. */\n renderLink?: (props: TextLinkRenderProps) => ReactElement;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;\n ref?: Ref<HTMLAnchorElement>;\n}\n\nconst TONE: Record<TextLinkTone, string> = {\n // Under More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36,\n // colour-roles §13.3 decision 8): `--brand` is a mark's colour, solved to 3:1.\n brand: \"text-brand hover:text-[var(--brand-hover)] contrast:text-brand-muted contrast:hover:text-brand-muted\",\n muted: \"text-muted hover:text-primary\",\n danger: \"text-danger hover:text-[var(--danger-hover)]\",\n inherit: \"\",\n primary: \"text-primary hover:decoration-2\",\n secondary: \"text-secondary hover:text-primary\",\n warning: \"text-warning hover:decoration-2\",\n};\n\n/** The tones whose colour is their surroundings' — underlined by default; see\n * {@link TextLinkTone}. */\nconst UNDERLINED_BY_DEFAULT = new Set<TextLinkTone>([\"primary\", \"secondary\", \"warning\"]);\n\nconst UNDERLINE: Record<TextLinkUnderline, string> = {\n hover: \"no-underline hover:underline\",\n always: \"underline\",\n none: \"no-underline\",\n};\n\n/**\n * The one inline link: a real `<a href>` (middle click, \"open in new tab\", link\n * semantics) through the app's router for an in-app path, in one of four tones.\n *\n * kastlan draws this link a dozen ways by hand — `EntityLink` and `CellLink` (text-primary,\n * hover underline, one stopping the row click), the legal pages' back link and footer,\n * the feedback page's URL links, the calculator's external sources with a hand-placed\n * ↗ and no word for a screen reader — and the lease wizard's unit step (lease-unit-step)\n * used a raw `<a>` to an in-app route, which reloads the whole app. Here the router\n * link comes from `<UiKitProvider linkComponent>`, set once.\n *\n * Inline, not `inline-flex`: a link inside a sentence must wrap with it. The icons sit\n * on the text's baseline at the text's size, so they scale with whatever type the link\n * is set in. The focus ring is an outline, which follows every line of a wrapped link.\n */\nexport function TextLink({\n href,\n children,\n tone = \"brand\",\n underline: underlineProp,\n external = false,\n replace,\n reloadDocument = false,\n stopPropagation = false,\n current,\n icon: Icon,\n renderLink,\n className,\n onClick,\n onKeyDown,\n ref,\n ...rest\n}: TextLinkProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLink = useKitLink();\n const underline = underlineProp ?? (UNDERLINED_BY_DEFAULT.has(tone) ? \"always\" : \"hover\");\n const ariaCurrent: TextLinkCurrent | undefined =\n current === true ? \"page\" : current === false ? undefined : current;\n\n const look = cn(\n \"rounded-sm underline-offset-2 transition-colors\",\n \"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand\",\n TONE[tone],\n UNDERLINE[underline],\n ariaCurrent && \"font-semibold\",\n className,\n );\n\n const body = (\n <>\n {Icon && <Icon aria-hidden className=\"me-1 inline size-[1em] shrink-0 align-[-0.125em]\" />}\n {children}\n {external && (\n <>\n <ExternalLink aria-hidden className=\"ms-1 inline size-[0.85em] align-[-0.1em] rtl:-scale-x-100\" />\n {\" \"}\n <span className=\"sr-only\">({common.opensInNewTab})</span>\n </>\n )}\n </>\n );\n\n const handleClick: TextLinkRenderProps[\"onClick\"] = stopPropagation\n ? (event) => {\n event.stopPropagation();\n onClick?.(event);\n }\n : onClick;\n const props: TextLinkRenderProps = {\n ...(rest as Partial<TextLinkRenderProps>),\n ref,\n href,\n className: look,\n \"aria-current\": ariaCurrent,\n onClick: handleClick,\n onKeyDown: stopPropagation\n ? (event) => {\n if (event.key === \"Enter\") event.stopPropagation();\n onKeyDown?.(event);\n }\n : onKeyDown,\n children: body,\n };\n\n const render = external || reloadDocument ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedTextLink render={render} {...props} {...routerLinkNavigation(replace)} />;\n // A plain `<a>` never gets `replace` as an attribute; asked for, it takes the click.\n const plainClick = external ? handleClick : replacingClick(handleClick, href, replace, rest.target);\n const { children: content, ...anchor } = props;\n return (\n <a\n {...anchor}\n href={href}\n onClick={plainClick}\n {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}\n >\n {content}\n </a>\n );\n}\n\n/** Calls the link renderer as a component, so a router link's hooks are its own. */\nfunction RenderedTextLink({\n render,\n ...props\n}: TextLinkRenderProps & { render: (props: TextLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AA6Se,SAGP,UAHO,KAML,YANK;AA5Sf,SAAS,oBAAoB;AAE7B,SAAS,UAAU;AACnB,SAAS,uBAAuB,cAAc,kBAAkB;AAEhE,SAAS,0BAA0B;AAc5B,SAAS,eAAe,MAAuB;AACpD,SAAO,+BAA+B,KAAK,IAAI;AACjD;AAQO,SAAS,eAAe,MAAuB;AACpD,SAAO,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,IAAI;AACtD;AAOO,SAAS,eAAe,MAAuB;AACpD,SAAO,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe,IAAI;AACtD;AAYO,SAAS,iBACd,KACA,SACA,MAC0C;AAC1C,MAAI,IAAK,QAAO;AAChB,MAAI,CAAC,WAAW,SAAS,UAAa,CAAC,eAAe,IAAI,EAAG,QAAO;AACpE,SAAO;AACT;AAIA,SAAS,uBAAuB,OAA+C;AAC7E,SACE,CAAC,MAAM,oBACP,MAAM,WAAW,KACjB,CAAC,MAAM,WACP,CAAC,MAAM,WACP,CAAC,MAAM,YACP,CAAC,MAAM;AAEX;AAgBO,SAAS,eACd,SACA,MACA,SACA,QAC8D;AAC9D,MAAI,CAAC,WAAY,WAAW,UAAa,WAAW,MAAM,WAAW,QAAU,QAAO;AACtF,SAAO,CAAC,UAAU;AAChB,cAAU,KAAK;AACf,QAAI,CAAC,uBAAuB,KAAK,EAAG;AACpC,UAAM,eAAe;AACrB,uBAAmB,QAAQ,IAAI;AAAA,EACjC;AACF;AAQO,SAAS,qBAAqB,SAAkD;AACrF,SAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;AACxC;AAQO,SAAS,gBAAmB,EAAE,QAAQ,MAAM,GAAqD;AACtG,SAAO,OAAO,KAAK;AACrB;AA0GA,MAAM,OAAqC;AAAA;AAAA;AAAA,EAGzC,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AACX;AAIA,MAAM,wBAAwB,oBAAI,IAAkB,CAAC,WAAW,aAAa,SAAS,CAAC;AAEvF,MAAM,YAA+C;AAAA,EACnD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAiBO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AAAA,EACA,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,UAAU,WAAW;AAC3B,QAAM,YAAY,kBAAkB,sBAAsB,IAAI,IAAI,IAAI,WAAW;AACjF,QAAM,cACJ,YAAY,OAAO,SAAS,YAAY,QAAQ,SAAY;AAE9D,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,KAAK,IAAI;AAAA,IACT,UAAU,SAAS;AAAA,IACnB,eAAe;AAAA,IACf;AAAA,EACF;AAEA,QAAM,OACJ,iCACG;AAAA,YAAQ,oBAAC,QAAK,eAAW,MAAC,WAAU,oDAAmD;AAAA,IACvF;AAAA,IACA,YACC,iCACE;AAAA,0BAAC,gBAAa,eAAW,MAAC,WAAU,6DAA4D;AAAA,MAC/F;AAAA,MACD,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,QAAE,OAAO;AAAA,QAAc;AAAA,SAAC;AAAA,OACpD;AAAA,KAEJ;AAGF,QAAM,cAA8C,kBAChD,CAAC,UAAU;AACT,UAAM,gBAAgB;AACtB,cAAU,KAAK;AAAA,EACjB,IACA;AACJ,QAAM,QAA6B;AAAA,IACjC,GAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,WAAW,kBACP,CAAC,UAAU;AACT,UAAI,MAAM,QAAQ,QAAS,OAAM,gBAAgB;AACjD,kBAAY,KAAK;AAAA,IACnB,IACA;AAAA,IACJ,UAAU;AAAA,EACZ;AAEA,QAAM,SAAS,YAAY,iBAAiB,SAAY,iBAAiB,YAAY,SAAS,IAAI;AAClG,MAAI,OAAQ,QAAO,oBAAC,oBAAiB,QAAiB,GAAG,OAAQ,GAAG,qBAAqB,OAAO,GAAG;AAEnG,QAAM,aAAa,WAAW,cAAc,eAAe,aAAa,MAAM,SAAS,KAAK,MAAM;AAClG,QAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACR,GAAI,WAAW,EAAE,QAAQ,UAAU,KAAK,sBAAsB,IAAI;AAAA,MAElE;AAAA;AAAA,EACH;AAEJ;AAGA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,GAAG;AACL,GAAmF;AACjF,SAAO,OAAO,KAAK;AACrB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/text-link.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport type { KitLinkComponent } from \"../i18n/kit-labels\";\nimport { documentNavigation } from \"../lib/document-navigation\";\n\n/* ── Choosing what draws a link ──────────────────────────────────────────── */\n\n/**\n * An `href` that leaves the app: a scheme (`https:`, `mailto:`, `tel:`) or a\n * protocol-relative `//host`. A router's link is for paths inside the app, so the\n * provider's `linkComponent` is never handed one of these — they stay a plain `<a>`.\n *\n * NOT external, and so handed to the router link: a path (`/x`, `x`, `?q=1`) and the\n * hash-router form `#/x` — that IS a route in a `HashRouter` app, which maps it to\n * `to=\"/x\"`. A plain in-page anchor (`#section`) is not external either, but it is not\n * a route: see {@link isInPageAnchor}.\n */\nexport function isExternalHref(href: string): boolean {\n return /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(href);\n}\n\n/**\n * An `href` that only scrolls the current page: `#section` (or a bare `#`). The\n * hash-router form `#/x` is a ROUTE, not an anchor, and is not one of these. A router\n * link must not get an anchor — under a `HashRouter` `#section` would navigate to the\n * path `/section` — so every kit link draws it as a plain `<a>`.\n */\nexport function isInPageAnchor(href: string): boolean {\n return href.startsWith(\"#\") && !href.startsWith(\"#/\");\n}\n\n/**\n * Whether the provider's `linkComponent` (or a kit component's router fallback) draws\n * `href`: an in-app path, `#/x` included — not an external `href`\n * ({@link isExternalHref}) and not an in-page `#anchor` ({@link isInPageAnchor}).\n */\nexport function isRoutableHref(href: string): boolean {\n return !isExternalHref(href) && !isInPageAnchor(href);\n}\n\n/**\n * What draws a kit link to `href`: the component's own `renderLink` when it was given\n * one (it always wins — the caller asked for it by name), else the\n * `<UiKitProvider linkComponent>` for a routable `href` ({@link isRoutableHref}), else\n * `undefined` — a plain `<a>`. The one rule every kit link follows: `https:` /\n * `mailto:` / `//host` and `#section` stay plain anchors; `/x`, `x` and a hash-router\n * `#/x` go to the router link. Every kit link props type is a subset of the anchor\n * attributes `KitLinkProps` describes, which is what makes the cast sound.\n * @internal Shared by the kit's link-bearing components; not part of the barrel.\n */\nexport function pickLinkRenderer<P>(\n own: ((props: P) => ReactElement) | undefined,\n kitLink: KitLinkComponent | undefined,\n href: string | undefined,\n): ((props: P) => ReactElement) | undefined {\n if (own) return own;\n if (!kitLink || href === undefined || !isRoutableHref(href)) return undefined;\n return kitLink as unknown as (props: P) => ReactElement;\n}\n\n/** The plain primary click a page navigates on — not a middle click, not ⌘ / Ctrl /\n * Shift / Alt (new tab, new window, download), and not one a handler already took. */\nfunction isPlainNavigationClick(event: MouseEvent<HTMLAnchorElement>): boolean {\n return (\n !event.defaultPrevented &&\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey\n );\n}\n\n/**\n * The `onClick` of a PLAIN `<a>` whose kit link was asked to `replace` (keksdose F1).\n *\n * A router link replaces the history entry itself (it is handed `replace`, see\n * {@link routerLinkNavigation}); a plain `<a>` has no attribute that says so, and the\n * browser's own navigation always pushes. So a plain click is taken over — the caller's\n * `onClick` runs first and may still `preventDefault` — and becomes\n * `location.replace(href)`: the same document load, with Back skipping the one-shot URL.\n * That is the case of `replace` together with `reloadDocument`, or an app with no\n * `linkComponent`. A modified click (new tab) and a `target` other than `_self` are left\n * to the browser: they do not navigate THIS history. Without `replace`, `onClick` comes\n * back untouched.\n * @internal\n */\nexport function replacingClick(\n onClick: ((event: MouseEvent<HTMLAnchorElement>) => void) | undefined,\n href: string,\n replace: boolean | undefined,\n target?: string,\n): ((event: MouseEvent<HTMLAnchorElement>) => void) | undefined {\n if (!replace || (target !== undefined && target !== \"\" && target !== \"_self\")) return onClick;\n return (event) => {\n onClick?.(event);\n if (!isPlainNavigationClick(event)) return;\n event.preventDefault();\n documentNavigation.replace(href);\n };\n}\n\n/**\n * What a ROUTER link is handed for `replace`: `{ replace: true }` when asked, else\n * nothing at all — so a provider link that spreads its props onto a DOM `<a>` never\n * receives an unknown attribute from a link that did not ask (see `KitLinkProps.replace`).\n * @internal\n */\nexport function routerLinkNavigation(replace: boolean | undefined): { replace?: true } {\n return replace ? { replace: true } : {};\n}\n\n/**\n * Draws `props` through `render` as a component of its own, so a router link's hooks\n * belong to it and not to the kit component calling it — for the kit components that\n * call their own `renderLink` inline and reach the provider's link through here.\n * @internal\n */\nexport function RenderedKitLink<P>({ render, props }: { render: (props: P) => ReactElement; props: P }) {\n return render(props);\n}\n\n/* ── TextLink ─────────────────────────────────────────────────────────────── */\n\n/**\n * `brand` (default) for a link in running text, `muted` for footer and meta links\n * that should not compete with the content, `danger` for a destructive one (\"Leave\n * group\"), `inherit` for a link that takes its surroundings' colour.\n *\n * `primary` and `secondary` are the body and the secondary text colour, UNDERLINED\n * (keksdose F3): a link that must read as part of the text around it — a name in a\n * list row, a reference in a card's meta line — where `brand` repaints it in the accent\n * and `muted` is a step too light to read as content. `secondary` darkens to the body\n * colour under the pointer, as `muted` does; `primary` has nowhere darker to go, so its\n * underline thickens instead.\n *\n * `warning` is the kit's `--warning` text colour, underlined (kastlan 46): a link inside\n * a `<Tone tone=\"warning\">` sentence or a warning banner, which `brand` would paint in a\n * second hue and `inherit` would leave indistinguishable from the sentence. Like\n * `primary`, it keeps its colour on hover and thickens the underline.\n *\n * The three colours that sit IN the text default to `underline=\"always\"`: with the\n * colour of their surroundings, the underline is the only thing that says \"link\"\n * (WCAG 1.4.1). An explicit `underline` still wins.\n */\nexport type TextLinkTone = \"brand\" | \"muted\" | \"danger\" | \"inherit\" | \"primary\" | \"secondary\" | \"warning\";\n\n/** `hover` (the default for `brand`, `muted`, `danger`, `inherit`): underlined under\n * the pointer — the idiom all three apps draw. `always` (the default for `primary`,\n * `secondary`, `warning`) for a link inside prose, where colour alone must not be the\n * only mark (WCAG 1.4.1). `none` where the context already says \"link\" (a footer nav). */\nexport type TextLinkUnderline = \"hover\" | \"always\" | \"none\";\n\n/** The `aria-current` token. `true` on {@link TextLinkProps.current} means `page`. */\nexport type TextLinkCurrent = \"page\" | \"step\" | \"location\" | \"date\" | \"time\" | \"true\";\n\n/** What {@link TextLinkProps.renderLink} (and the provider's `linkComponent`) is\n * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface TextLinkRenderProps {\n href: string;\n /** The link's look — keep it, or the tone, underline and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: TextLinkCurrent;\n id?: string;\n title?: string;\n /** Present, as `true`, only when the link was given `replace` — see\n * `KitLinkProps.replace`. Map it to your router's replace; never onto a DOM `<a>`. */\n replace?: boolean;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface TextLinkProps extends Omit<\n ComponentPropsWithoutRef<\"a\">,\n \"href\" | \"children\" | \"aria-current\" | \"onClick\" | \"onKeyDown\"\n> {\n href: string;\n children: ReactNode;\n /** Default `brand`. */\n tone?: TextLinkTone;\n /** Default `always` for `primary`, `secondary` and `warning`, else `hover`. */\n underline?: TextLinkUnderline;\n /**\n * Navigate by replacing the current history entry, so Back does not return to this\n * URL — keksdose F1, a one-shot URL (a confirmation landing, a `?done=1` hop). Handed\n * to the router link as `replace` (see `KitLinkProps.replace` for mapping it); on a\n * plain `<a>` (no `linkComponent`, or `reloadDocument`) a plain click becomes\n * `location.replace(href)`. Ignored on an `external` link, which opens a new tab.\n */\n replace?: boolean;\n /**\n * A plain `<a>` for an in-app `href`: the browser loads the whole document instead of\n * the router swapping the view — keksdose F1, a route served outside the SPA (a file\n * download, a server-rendered page, a logout that must drop every in-memory cache).\n * Wins over `renderLink` and the provider's `linkComponent`: it is the caller saying\n * \"not the router\" for this one link.\n */\n reloadDocument?: boolean;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener noreferrer\">`\n * (never the router's), an external-link mark after the text (mirrored in RTL), and\n * \"opens in a new tab\" (`common.opensInNewTab`) read after it — the mark says it\n * only to the eye. kastlan's BWO / BFS source links (rent-increase-calculator-page).\n */\n external?: boolean;\n /**\n * Keep the click (and the Enter that activates the link) from reaching an ancestor —\n * a link in a DataTable row with `onRowClick` goes to its own target instead of\n * running the row's action. kastlan's `CellLink` did this by hand.\n */\n stopPropagation?: boolean;\n /** Mark the link as the current one of a set — `true` is `aria-current=\"page\"`, the\n * legal footer's link to the page you are on. Drawn in semibold. */\n current?: boolean | TextLinkCurrent;\n /** A Lucide icon before the text, at the text's size — kastlan's \"← Back\" link. */\n icon?: LucideIcon;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then a plain `<a>`. Ignored on an `external` link. */\n renderLink?: (props: TextLinkRenderProps) => ReactElement;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;\n ref?: Ref<HTMLAnchorElement>;\n}\n\nconst TONE: Record<TextLinkTone, string> = {\n // Under More contrast the brand as text, `--brand-muted`, at rest and hovered (0.36,\n // colour-roles §13.3 decision 8): `--brand` is a mark's colour, solved to 3:1.\n brand: \"text-brand hover:text-[var(--brand-hover)] contrast:text-brand-muted contrast:hover:text-brand-muted\",\n muted: \"text-muted hover:text-primary\",\n danger: \"text-danger hover:text-[var(--danger-hover)]\",\n inherit: \"\",\n primary: \"text-primary hover:decoration-2\",\n secondary: \"text-secondary hover:text-primary\",\n warning: \"text-warning hover:decoration-2\",\n};\n\n/** The tones whose colour is their surroundings' — underlined by default; see\n * {@link TextLinkTone}. */\nconst UNDERLINED_BY_DEFAULT = new Set<TextLinkTone>([\"primary\", \"secondary\", \"warning\"]);\n\nconst UNDERLINE: Record<TextLinkUnderline, string> = {\n hover: \"no-underline hover:underline\",\n always: \"underline\",\n none: \"no-underline\",\n};\n\n/**\n * The one inline link: a real `<a href>` (middle click, \"open in new tab\", link\n * semantics) through the app's router for an in-app path, in one of four tones.\n *\n * kastlan draws this link a dozen ways by hand — `EntityLink` and `CellLink` (text-primary,\n * hover underline, one stopping the row click), the legal pages' back link and footer,\n * the feedback page's URL links, the calculator's external sources with a hand-placed\n * ↗ and no word for a screen reader — and the lease wizard's unit step (lease-unit-step)\n * used a raw `<a>` to an in-app route, which reloads the whole app. Here the router\n * link comes from `<UiKitProvider linkComponent>`, set once.\n *\n * Inline, not `inline-flex`: a link inside a sentence must wrap with it. The icons sit\n * on the text's baseline at the text's size, so they scale with whatever type the link\n * is set in. The focus ring is an outline, which follows every line of a wrapped link.\n */\nexport function TextLink({\n href,\n children,\n tone = \"brand\",\n underline: underlineProp,\n external = false,\n replace,\n reloadDocument = false,\n stopPropagation = false,\n current,\n icon: Icon,\n renderLink,\n className,\n onClick,\n onKeyDown,\n ref,\n ...rest\n}: TextLinkProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const kitLink = useKitLink();\n const underline = underlineProp ?? (UNDERLINED_BY_DEFAULT.has(tone) ? \"always\" : \"hover\");\n const ariaCurrent: TextLinkCurrent | undefined = current === true ? \"page\" : current === false ? undefined : current;\n\n const look = cn(\n \"rounded-sm underline-offset-2 transition-colors\",\n \"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand\",\n TONE[tone],\n UNDERLINE[underline],\n ariaCurrent && \"font-semibold\",\n className,\n );\n\n const body = (\n <>\n {Icon && <Icon aria-hidden className=\"me-1 inline size-[1em] shrink-0 align-[-0.125em]\" />}\n {children}\n {external && (\n <>\n <ExternalLink aria-hidden className=\"ms-1 inline size-[0.85em] align-[-0.1em] rtl:-scale-x-100\" />{\" \"}\n <span className=\"sr-only\">({common.opensInNewTab})</span>\n </>\n )}\n </>\n );\n\n const handleClick: TextLinkRenderProps[\"onClick\"] = stopPropagation\n ? (event) => {\n event.stopPropagation();\n onClick?.(event);\n }\n : onClick;\n const props: TextLinkRenderProps = {\n ...(rest as Partial<TextLinkRenderProps>),\n ref,\n href,\n className: look,\n \"aria-current\": ariaCurrent,\n onClick: handleClick,\n onKeyDown: stopPropagation\n ? (event) => {\n if (event.key === \"Enter\") event.stopPropagation();\n onKeyDown?.(event);\n }\n : onKeyDown,\n children: body,\n };\n\n const render = external || reloadDocument ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedTextLink render={render} {...props} {...routerLinkNavigation(replace)} />;\n // A plain `<a>` never gets `replace` as an attribute; asked for, it takes the click.\n const plainClick = external ? handleClick : replacingClick(handleClick, href, replace, rest.target);\n const { children: content, ...anchor } = props;\n return (\n <a\n {...anchor}\n href={href}\n onClick={plainClick}\n {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}\n >\n {content}\n </a>\n );\n}\n\n/** Calls the link renderer as a component, so a router link's hooks are its own. */\nfunction RenderedTextLink({\n render,\n ...props\n}: TextLinkRenderProps & { render: (props: TextLinkRenderProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/text.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/* ── SectionLabel ─────────────────────────────────────────────────────────── */\n\n/**\n * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;\n * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings\n * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a\n * dense panel (a legend, a menu) or above a block of page content, and one size looked\n * wrong in the other place.\n *\n * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels\n * its figures (\"Assigned\", \"Activity\", \"Upcoming\", \"Available\" —\n * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line\n * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a\n * figure that size, 12px competed with it. Named `md` rather than slotted in order\n * because `sm` was already the default and renaming it would move every caller.\n *\n * `md` is also the heading over a CHART COLUMN (lenkbank P9): the small label centred\n * over each of a pair of facing charts, or over each column of a small-multiples grid.\n * There the label names a plot, not a section of the page, so `sm` (12px, the page's\n * section heading) out-ranks the card title above it, and `xs` (10px, a legend's\n * title) sinks below the axis ticks it sits beside. Rule of thumb: `xs` inside a\n * control (legend, menu), `md` over a figure or a plot, `sm` over a block of page\n * content.\n */\nexport type SectionLabelSize = \"xs\" | \"md\" | \"sm\";\n\n/**\n * The small, uppercase, tracked, muted label — as a class string, per size.\n *\n * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a\n * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better\n * reached as a string than wrapped in a component that only takes a `className`. The\n * uppercase is CSS, so a screen reader reads the text as written: write it in normal\n * case.\n *\n * The sizes are the kit's named rem steps since 0.32 (`text-micro` 0.625rem,\n * `text-caption` 0.6875rem — 10 and 11 px at Normal), so a label grows with the text\n * size like everything around it (docs/text-size-harmonization.md §3.2).\n */\nexport const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-micro font-semibold uppercase tracking-wide text-muted\",\n md: \"text-caption font-semibold uppercase tracking-wide text-muted\",\n sm: \"text-xs font-semibold uppercase tracking-wide text-muted\",\n};\n\n/**\n * `plain` (default): the label alone, as above.\n *\n * `band` (0.13, keksdose F5): the label on a full-width `surface-2` bar with a bottom\n * border — the day-group header of keksdose's mobile transaction list\n * (mobile-transaction-list.tsx:171), which separates one day's rows from the next.\n * Its type is kept as keksdose drew it — `font-medium` in the SECONDARY text colour,\n * not the plain label's semibold muted — so adopting the kit does not restyle a live\n * list: the bar already sets the label apart, and needs no extra weight. The padding (`px-4 py-2`) lines the text up with a\n * `ListItem density=\"comfortable\"` row below it; override it with `className` for a\n * list at another density. A sticky header is the caller's: add `sticky top-0`.\n */\nexport type SectionLabelVariant = \"plain\" | \"band\";\n\n/** The band's own type and box, per size — spelled out rather than merged over\n * {@link SECTION_LABEL_CLASS}, so the result does not hang on tailwind-merge telling\n * a font-size arbitrary `text-` value from a colour one. (Not spelt out as class\n * names here: Tailwind scans comments, and a literal one broke the showcase CSS.) */\nconst SECTION_LABEL_BAND_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-micro\",\n md: \"text-caption\",\n sm: \"text-xs\",\n};\nconst BAND
|
|
1
|
+
{"version":3,"sources":["../../src/components/text.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/* ── SectionLabel ─────────────────────────────────────────────────────────── */\n\n/**\n * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;\n * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings\n * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a\n * dense panel (a legend, a menu) or above a block of page content, and one size looked\n * wrong in the other place.\n *\n * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels\n * its figures (\"Assigned\", \"Activity\", \"Upcoming\", \"Available\" —\n * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line\n * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a\n * figure that size, 12px competed with it. Named `md` rather than slotted in order\n * because `sm` was already the default and renaming it would move every caller.\n *\n * `md` is also the heading over a CHART COLUMN (lenkbank P9): the small label centred\n * over each of a pair of facing charts, or over each column of a small-multiples grid.\n * There the label names a plot, not a section of the page, so `sm` (12px, the page's\n * section heading) out-ranks the card title above it, and `xs` (10px, a legend's\n * title) sinks below the axis ticks it sits beside. Rule of thumb: `xs` inside a\n * control (legend, menu), `md` over a figure or a plot, `sm` over a block of page\n * content.\n */\nexport type SectionLabelSize = \"xs\" | \"md\" | \"sm\";\n\n/**\n * The small, uppercase, tracked, muted label — as a class string, per size.\n *\n * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a\n * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better\n * reached as a string than wrapped in a component that only takes a `className`. The\n * uppercase is CSS, so a screen reader reads the text as written: write it in normal\n * case.\n *\n * The sizes are the kit's named rem steps since 0.32 (`text-micro` 0.625rem,\n * `text-caption` 0.6875rem — 10 and 11 px at Normal), so a label grows with the text\n * size like everything around it (docs/text-size-harmonization.md §3.2).\n */\nexport const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-micro font-semibold uppercase tracking-wide text-muted\",\n md: \"text-caption font-semibold uppercase tracking-wide text-muted\",\n sm: \"text-xs font-semibold uppercase tracking-wide text-muted\",\n};\n\n/**\n * `plain` (default): the label alone, as above.\n *\n * `band` (0.13, keksdose F5): the label on a full-width `surface-2` bar with a bottom\n * border — the day-group header of keksdose's mobile transaction list\n * (mobile-transaction-list.tsx:171), which separates one day's rows from the next.\n * Its type is kept as keksdose drew it — `font-medium` in the SECONDARY text colour,\n * not the plain label's semibold muted — so adopting the kit does not restyle a live\n * list: the bar already sets the label apart, and needs no extra weight. The padding (`px-4 py-2`) lines the text up with a\n * `ListItem density=\"comfortable\"` row below it; override it with `className` for a\n * list at another density. A sticky header is the caller's: add `sticky top-0`.\n */\nexport type SectionLabelVariant = \"plain\" | \"band\";\n\n/** The band's own type and box, per size — spelled out rather than merged over\n * {@link SECTION_LABEL_CLASS}, so the result does not hang on tailwind-merge telling\n * a font-size arbitrary `text-` value from a colour one. (Not spelt out as class\n * names here: Tailwind scans comments, and a literal one broke the showcase CSS.) */\nconst SECTION_LABEL_BAND_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-micro\",\n md: \"text-caption\",\n sm: \"text-xs\",\n};\nconst BAND = \"block border-b border-subtle bg-surface-2 px-4 py-2 font-medium uppercase tracking-wide text-secondary\";\n\ntype SectionLabelElement = \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"span\" | \"div\" | \"legend\";\n\nexport interface SectionLabelProps extends ComponentPropsWithoutRef<\"h3\"> {\n /**\n * The element — which is the heading LEVEL, and so is the caller's to pick: the kit\n * cannot know where in the outline the label sits. Default `h3`, which is what\n * lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a\n * label that is not a heading at all (a legend group's title).\n */\n as?: SectionLabelElement;\n size?: SectionLabelSize;\n /** `band` draws the label on a `surface-2` bar with a bottom border — a list's group\n * header. See {@link SectionLabelVariant}. */\n variant?: SectionLabelVariant;\n children: ReactNode;\n /** The element `as` names — hence `HTMLElement` (0.34, API-2, refactor audit 2026-10). */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * A section's small uppercase label. lenkbank spells\n * `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`\n * on every `<h3>` of its control page and more; keksdose's budget switcher writes the\n * 10px version on a menu row. One component, the token colour, and the level chosen\n * where the outline is known.\n */\nexport function SectionLabel({\n as = \"h3\",\n size = \"sm\",\n variant = \"plain\",\n className,\n children,\n ...rest\n}: SectionLabelProps) {\n const Tag = as as ElementType;\n const type = variant === \"band\" ? cn(SECTION_LABEL_BAND_CLASS[size], BAND) : SECTION_LABEL_CLASS[size];\n return (\n <Tag {...rest} className={cn(type, className)}>\n {children}\n </Tag>\n );\n}\n\n/* ── Caption ──────────────────────────────────────────────────────────────── */\n\n/**\n * The caption type as a class string: `text-caption` (0.6875rem, 11 px at Normal, §3.2),\n * snug, muted. lenkbank's `CAPTION`\n * (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a\n * token colour rather than `slate-500 dark:slate-400`, so it follows the palette.\n * Exported for the same reason lenkbank's is a string: every caller positions it\n * differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the\n * type, not the box.\n */\nexport const CAPTION_CLASS = \"text-caption leading-snug text-muted\";\n\nexport interface CaptionProps extends ComponentPropsWithoutRef<\"p\"> {\n /** Default `p`; `span` for a caption inside a line of other content. */\n as?: \"p\" | \"span\" | \"div\";\n children: ReactNode;\n /** The element `as` names — hence `HTMLElement` (0.34, API-2, refactor audit 2026-10). */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * The small grey sentence under a field, a table or a button — prose ABOUT the thing\n * above it. Not `FieldHint`, which hides behind a field label's \"?\" so a paragraph does\n * not make one field taller than its neighbour (lenkbank feedback #27).\n */\nexport function Caption({ as = \"p\", className, children, ...rest }: CaptionProps) {\n const Tag = as as ElementType;\n return (\n <Tag {...rest} className={cn(CAPTION_CLASS, className)}>\n {children}\n </Tag>\n );\n}\n"],"mappings":";AA8GI;AA7GJ,SAAS,UAAU;AAyCZ,MAAM,sBAAwD;AAAA,EACnE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAoBA,MAAM,2BAA6D;AAAA,EACjE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,OAAO;AA4BN,SAAS,aAAa;AAAA,EAC3B,KAAK;AAAA,EACL,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,MAAM;AACZ,QAAM,OAAO,YAAY,SAAS,GAAG,yBAAyB,IAAI,GAAG,IAAI,IAAI,oBAAoB,IAAI;AACrG,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,MAAM,SAAS,GACzC,UACH;AAEJ;AAaO,MAAM,gBAAgB;AAetB,SAAS,QAAQ,EAAE,KAAK,KAAK,WAAW,UAAU,GAAG,KAAK,GAAiB;AAChF,QAAM,MAAM;AACZ,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,eAAe,SAAS,GAClD,UACH;AAEJ;","names":[]}
|