@eifi1/ui-kit 0.51.7 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -23
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +20 -8
- package/dist/billing/cancel-subscription.js +7 -5
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +19 -7
- package/dist/billing/plan-switch.js +8 -4
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -0
- package/dist/components/amount-input.js +1 -0
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +111 -2
- package/dist/components/combobox-core.js +148 -42
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +141 -130
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +12 -7
- package/dist/components/data-table-mobile.js +43 -30
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +13 -4
- package/dist/components/data-table.js +139 -36
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +148 -140
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +3 -2
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.js +3 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +3 -3
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +19 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.js +5 -14
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +29 -0
- package/dist/lib/text-match.js +20 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +13 -3
- package/dist/tour/tour.js +59 -781
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +10 -6
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +1 -5
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +4 -1
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-subscription.tsx +26 -12
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +26 -12
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +18 -7
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +293 -62
- package/src/components/combobox.tsx +183 -153
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +61 -26
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +239 -68
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +197 -153
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +10 -4
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +6 -1
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-select.tsx +21 -9
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +3 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +2 -2
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +32 -3
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +5 -17
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +57 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +108 -1202
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { horizontalStep } from "../lib/direction.js";
|
|
7
|
+
import { hasContent } from "../lib/presence.js";
|
|
7
8
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
9
|
import { useAuthedSrc } from "../hooks/use-authed-src.js";
|
|
9
10
|
import { useBreakpoint } from "../hooks/use-breakpoint.js";
|
|
@@ -22,6 +23,7 @@ function imageItemKind(item, fetchedType) {
|
|
|
22
23
|
return "image";
|
|
23
24
|
}
|
|
24
25
|
const SWIPE_MIN_PX = 50;
|
|
26
|
+
const ARROW_AT_END = "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]";
|
|
25
27
|
function Lightbox({
|
|
26
28
|
open,
|
|
27
29
|
onClose,
|
|
@@ -183,7 +185,11 @@ function Lightbox({
|
|
|
183
185
|
backCloses,
|
|
184
186
|
onKeyDown,
|
|
185
187
|
"data-lightbox": "",
|
|
186
|
-
footer:
|
|
188
|
+
footer: (
|
|
189
|
+
// `hasContent` (refactor audit 2026-10-30, DUP-14): a `""` caption drew an empty
|
|
190
|
+
// footer bar.
|
|
191
|
+
hasContent(item?.caption) ? /* @__PURE__ */ jsx("p", { className: "me-auto min-w-0 text-sm text-secondary", children: item?.caption }) : void 0
|
|
192
|
+
),
|
|
187
193
|
children: [
|
|
188
194
|
/* @__PURE__ */ jsx(
|
|
189
195
|
"div",
|
|
@@ -200,16 +206,21 @@ function Lightbox({
|
|
|
200
206
|
children: slide
|
|
201
207
|
}
|
|
202
208
|
),
|
|
203
|
-
count > 1 &&
|
|
209
|
+
count > 1 && // At an end the arrow refuses rather than turns `disabled` (refactor audit
|
|
210
|
+
// 2026-10-30, INP-4, JumpCalendar's rule): the press that reaches the last
|
|
211
|
+
// picture is made with the focus on ›, and a focused button that turns
|
|
212
|
+
// `disabled` drops the focus out of the viewer's trap to <body>. kastlan's
|
|
213
|
+
// ImageGrid and keksdose's invoice review open it without `loop`.
|
|
214
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
204
215
|
/* @__PURE__ */ jsx(
|
|
205
216
|
IconButton,
|
|
206
217
|
{
|
|
207
218
|
variant: "overlay",
|
|
208
219
|
size: "lg",
|
|
209
220
|
label: labels.previous,
|
|
210
|
-
disabled: !canPrev,
|
|
221
|
+
"aria-disabled": !canPrev || void 0,
|
|
211
222
|
onClick: () => go(-1),
|
|
212
|
-
className: "absolute start-3 top-1/2 -translate-y-1/2",
|
|
223
|
+
className: cn("absolute start-3 top-1/2 -translate-y-1/2", ARROW_AT_END),
|
|
213
224
|
tooltip: false,
|
|
214
225
|
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" })
|
|
215
226
|
}
|
|
@@ -220,9 +231,9 @@ function Lightbox({
|
|
|
220
231
|
variant: "overlay",
|
|
221
232
|
size: "lg",
|
|
222
233
|
label: labels.next,
|
|
223
|
-
disabled: !canNext,
|
|
234
|
+
"aria-disabled": !canNext || void 0,
|
|
224
235
|
onClick: () => go(1),
|
|
225
|
-
className: "absolute end-3 top-1/2 -translate-y-1/2",
|
|
236
|
+
className: cn("absolute end-3 top-1/2 -translate-y-1/2", ARROW_AT_END),
|
|
226
237
|
tooltip: false,
|
|
227
238
|
children: /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
|
|
228
239
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/lightbox.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode, TouchEvent } from \"react\";\nimport { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAuthedSrc } from \"../hooks/use-authed-src\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { IconButton } from \"./icon-button\";\nimport { buttonClasses } from \"./button-look\";\nimport { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from \"./authed-image\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport { DEFAULT_LIGHTBOX_LABELS } from \"./lightbox-labels\";\n\n/* ── The item both the grid and the lightbox show ────────────────────────── */\n\n/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —\n * a floor plan uploaded as a PDF — shown as a file card with open and download. */\nexport type ImageItemKind = \"image\" | \"file\";\n\nexport interface ImageItem {\n /** React key. Defaults to `src`. */\n key?: string | number;\n /** The full-size file — what the lightbox shows and downloads. An API path when the\n * component is given a `fetcher`. */\n src: string;\n /** A smaller rendition for the grid, when the server has one. Default: `src`. */\n thumbnailSrc?: string;\n /** Required: the picture's text alternative, and the open button's name. */\n alt: string;\n /** Shown under the thumbnail and at the foot of the lightbox. */\n caption?: ReactNode;\n /** The file's MIME type, when known (`\"application/pdf\"`). Decides {@link kind}. */\n mimeType?: string;\n /** Say it outright. Default: from {@link mimeType}, else from the file extension in\n * `src`, else — for a fetched file — from the type the server sent back. */\n kind?: ImageItemKind;\n /** The name a download is saved under, and a file card's heading. */\n fileName?: string;\n}\n\nconst FILE_EXTENSION = /\\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;\n\n/** What an item is, from what is known about it WITHOUT fetching it — see\n * {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */\nexport function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind {\n if (item.kind) return item.kind;\n if (item.mimeType) return item.mimeType.startsWith(\"image/\") ? \"image\" : \"file\";\n if (FILE_EXTENSION.test(item.src)) return \"file\";\n if (fetchedType && !fetchedType.startsWith(\"image/\")) return \"file\";\n return \"image\";\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\nexport interface LightboxLabels {\n /** The dialog's accessible name. */\n dialog: string;\n close: string;\n previous: string;\n next: string;\n /** The visible counter: \"3 / 7\". */\n counter: (index: number, count: number) => string;\n /** What a screen reader hears on each page turn: \"Image 3 of 7\". The alt follows. */\n position: (index: number, count: number) => string;\n download: string;\n /** The zoom toggle (a pressed/not-pressed button — its name does not change). */\n zoom: string;\n /** A file card's note: the file has no picture to show. */\n noPreview: string;\n /** A file card's link to the browser's own viewer (a PDF opens in its viewer). */\n openInNewTab: string;\n}\n\n// Defined in ./lightbox-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIGHTBOX_LABELS };\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport interface LightboxProps {\n open: boolean;\n /** The X, Escape and the Back gesture all call this. */\n onClose: () => void;\n items: readonly ImageItem[];\n /** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */\n index: number;\n onIndexChange: (index: number) => void;\n /** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is\n * loaded by the browser as usual. */\n fetcher?: AuthedFetcher | null;\n /** Previous from the first goes to the last, and next from the last to the first.\n * Default `false`: the buttons at the ends are disabled, which says where the set\n * ends. */\n loop?: boolean;\n /** Double-click (or double-tap) the picture, or the zoom button, to see it at full\n * size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */\n zoom?: boolean;\n /** A download link in the header. Default `true`. */\n download?: boolean;\n /**\n * Download through the app instead — kastlan's floor plans have `openDocument(id)`,\n * which knows the stored file name. Replaces the link with a button that calls this.\n */\n onDownload?: (item: ImageItem, index: number) => void;\n /** Push a history entry so Back closes the viewer. Default `true`; see\n * `FullBleedDialog`'s `backCloses` for when to turn it off. */\n backCloses?: boolean;\n labels?: LabelOverride<LightboxLabels>;\n}\n\n/** Swipes shorter than this are taps, and a drag that is mostly vertical is a scroll. */\nconst SWIPE_MIN_PX = 50;\n\n/**\n * A full-screen viewer for a set of images (and files that are not images).\n *\n * Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's\n * one implementation of everything an overlay owes the page: the focus trap, Escape,\n * the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the\n * body-scroll lock and the enter/exit motion.\n *\n * Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the\n * direction is read at the point the viewer is rendered, since the dialog itself is\n * portalled out of it), Home/End, and a horizontal swipe on a touch screen.\n *\n * Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture\n * at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —\n * the browser's own page pinch still works — because a pinch handler would have to own\n * every touch on the picture, and with it the swipe.\n *\n * A non-image (a PDF floor plan) is shown as a file card: its name, \"no preview\", a\n * link that opens it in the browser's own viewer, and the download. An `<iframe>`\n * preview was the alternative; phone browsers render a PDF in one inconsistently or\n * not at all, and a card works everywhere.\n */\nexport function Lightbox({\n open,\n onClose,\n items,\n index: indexProp,\n onIndexChange,\n fetcher,\n loop = false,\n zoom: zoomEnabled = true,\n download = true,\n onDownload,\n backCloses,\n labels: labelsProp,\n}: LightboxProps) {\n const labels = useKitLabels(\"lightbox\", DEFAULT_LIGHTBOX_LABELS, labelsProp);\n const imageLabels = useKitLabels(\"authedImage\", DEFAULT_AUTHED_IMAGE_LABELS);\n const count = items.length;\n // Clamped: the set can shrink under an open viewer (the photo on screen was deleted).\n const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);\n const item = items[index] as ImageItem | undefined;\n\n // The dialog is portalled to <body>, out of the subtree whose `dir` it would have\n // inherited; this marker stays where the viewer was rendered and reads it there.\n const anchorRef = useRef<HTMLSpanElement>(null);\n const dir = useAnchorDir(anchorRef, open);\n\n // The toolbar's words (docs/text-size-harmonization.md §4, §10.8). At Large an\n // IconButton shows its label beside the glyph; on a narrow screen the viewer's header —\n // the counter, Zoom, Download and the dialog's close — has no room for two words at\n // 150 % (a 390 px phone is 260 px of Normal type), so below `sm` the toolbar keeps its\n // icons, as the plain download link hides its word there (`max-sm:sr-only`). From `sm`\n // up, at the same breakpoint the CSS uses, the words show. Each name stays the\n // accessible name and the tooltip either way.\n const narrow = useBreakpoint(\"max-sm\");\n const toolbarLabelVisible = narrow ? false : undefined;\n\n const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });\n const kind = item ? imageItemKind(item, fetched.type) : \"image\";\n\n const [zoomedKey, setZoomedKey] = useState<string | null>(null);\n const itemKey = item ? String(item.key ?? item.src) : null;\n // Zoom belongs to the picture it was turned on for: paging on resets it.\n const zoomed = zoomEnabled && kind === \"image\" && zoomedKey !== null && zoomedKey === itemKey;\n const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n const go = (step: number) => {\n if (count === 0) return;\n let next = index + step;\n if (loop) next = (next + count) % count;\n if (next < 0 || next >= count || next === index) return;\n onIndexChange(next);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A key meant for a control stays with it: Space/Enter on a button, typing in any\n // field a caller's caption might hold.\n const target = e.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const step = horizontalStep(e.key, e.currentTarget);\n if (step !== 0) {\n e.preventDefault();\n go(step);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n if (index !== 0) onIndexChange(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n if (index !== count - 1) onIndexChange(count - 1);\n }\n };\n\n const touchStart = useRef<{ x: number; y: number } | null>(null);\n const onTouchStart = (e: TouchEvent) => {\n const t = e.touches[0];\n touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;\n };\n const onTouchEnd = (e: TouchEvent) => {\n const start = touchStart.current;\n touchStart.current = null;\n const t = e.changedTouches[0];\n // Zoomed, a drag is a PAN of the picture, not a page turn.\n if (!start || !t || zoomed) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;\n // Swiping toward the START edge brings in the next picture from the end edge.\n const towardStart = dir === \"rtl\" ? dx > 0 : dx < 0;\n go(towardStart ? 1 : -1);\n };\n\n // Paging the viewer while zoomed would leave the new picture scrolled to where the\n // last one was panned; start each at its top-left.\n const bodyRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n bodyRef.current?.scrollTo?.({ top: 0, left: 0 });\n }, [index]);\n\n const downloadHref = fetched.src;\n const downloadControl =\n download && item ? (\n onDownload ? (\n <IconButton\n size=\"sm\"\n label={labels.download}\n labelVisible={toolbarLabelVisible}\n onClick={() => onDownload(item, index)}\n >\n <Download />\n </IconButton>\n ) : downloadHref ? (\n <a\n href={downloadHref}\n // `download` names the saved file; a blob: URL has no name of its own.\n download={item.fileName ?? \"\"}\n className={buttonClasses(\"ghost\", { size: \"sm\" })}\n >\n <Download aria-hidden className=\"size-4\" />\n <span className=\"max-sm:sr-only\">{labels.download}</span>\n </a>\n ) : null\n ) : null;\n\n const header = item ? (\n <div className=\"flex min-w-0 items-center gap-2\">\n <span aria-hidden className=\"shrink-0 text-sm tabular-nums text-secondary\">\n {labels.counter(index + 1, count)}\n </span>\n {/* The page turn, said — focus does not move when the picture changes, so\n without this a reader pressing → hears nothing at all. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {`${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : \"\"}`}\n </span>\n <span className=\"min-w-0 flex-1\" />\n {zoomEnabled && kind === \"image\" && (\n <IconButton\n size=\"sm\"\n label={labels.zoom}\n labelVisible={toolbarLabelVisible}\n pressed={zoomed}\n onClick={toggleZoom}\n >\n <ZoomIn />\n </IconButton>\n )}\n {downloadControl}\n </div>\n ) : null;\n\n const slide = !item ? null : kind === \"file\" ? (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-lg border border-subtle p-6 text-center\">\n <FileText aria-hidden className=\"size-10 text-muted\" />\n <p className=\"break-all font-medium text-primary\">{item.fileName ?? item.alt}</p>\n <p className=\"text-sm text-muted\">{labels.noPreview}</p>\n {fetched.src && (\n <a\n href={fetched.src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={buttonClasses(\"secondary\", { size: \"sm\" })}\n >\n <ExternalLink aria-hidden className=\"size-4\" />\n {labels.openInNewTab}\n </a>\n )}\n </div>\n ) : (\n <ImageStates\n // Keyed by item: a new picture is a new decode, never the old one's \"loaded\".\n key={itemKey ?? undefined}\n fetched={fetched}\n alt={item.alt}\n labels={imageLabels}\n wrapperClassName={zoomed ? \"max-w-none shrink-0\" : \"flex h-full w-full items-center justify-center\"}\n className={cn(\n zoomed ? \"max-w-none cursor-zoom-out\" : \"max-h-full max-w-full object-contain\",\n !zoomed && zoomEnabled && \"cursor-zoom-in\",\n )}\n imgProps={{\n draggable: false,\n onDoubleClick: zoomEnabled ? toggleZoom : undefined,\n }}\n />\n );\n\n return (\n <>\n <span ref={anchorRef} hidden />\n <FullBleedDialog\n open={open && !!item}\n onClose={onClose}\n closeLabel={labels.close}\n aria-label={labels.dialog}\n dir={dir}\n header={header}\n backCloses={backCloses}\n onKeyDown={onKeyDown}\n data-lightbox=\"\"\n footer={\n item?.caption !== undefined && item.caption !== null && item.caption !== false ? (\n <p className=\"me-auto min-w-0 text-sm text-secondary\">{item.caption}</p>\n ) : undefined\n }\n >\n <div\n ref={bodyRef}\n data-lightbox-stage=\"\"\n data-zoomed={zoomed ? \"\" : undefined}\n onTouchStart={onTouchStart}\n onTouchEnd={onTouchEnd}\n className={cn(\n \"relative h-full min-h-48 w-full\",\n zoomed ? \"overflow-auto\" : \"flex items-center justify-center overflow-hidden\",\n )}\n >\n {slide}\n </div>\n {count > 1 && (\n <>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.previous}\n disabled={!canPrev}\n onClick={() => go(-1)}\n className=\"absolute start-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.next}\n disabled={!canNext}\n onClick={() => go(1)}\n className=\"absolute end-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </>\n )}\n </FullBleedDialog>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/lightbox.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode, TouchEvent } from \"react\";\nimport { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { hasContent } from \"../lib/presence\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useAuthedSrc } from \"../hooks/use-authed-src\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { IconButton } from \"./icon-button\";\nimport { buttonClasses } from \"./button-look\";\nimport { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from \"./authed-image\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\nimport { DEFAULT_LIGHTBOX_LABELS } from \"./lightbox-labels\";\n\n/* ── The item both the grid and the lightbox show ────────────────────────── */\n\n/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —\n * a floor plan uploaded as a PDF — shown as a file card with open and download. */\nexport type ImageItemKind = \"image\" | \"file\";\n\nexport interface ImageItem {\n /** React key. Defaults to `src`. */\n key?: string | number;\n /** The full-size file — what the lightbox shows and downloads. An API path when the\n * component is given a `fetcher`. */\n src: string;\n /** A smaller rendition for the grid, when the server has one. Default: `src`. */\n thumbnailSrc?: string;\n /** Required: the picture's text alternative, and the open button's name. */\n alt: string;\n /** Shown under the thumbnail and at the foot of the lightbox. */\n caption?: ReactNode;\n /** The file's MIME type, when known (`\"application/pdf\"`). Decides {@link kind}. */\n mimeType?: string;\n /** Say it outright. Default: from {@link mimeType}, else from the file extension in\n * `src`, else — for a fetched file — from the type the server sent back. */\n kind?: ImageItemKind;\n /** The name a download is saved under, and a file card's heading. */\n fileName?: string;\n}\n\nconst FILE_EXTENSION = /\\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;\n\n/** What an item is, from what is known about it WITHOUT fetching it — see\n * {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */\nexport function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind {\n if (item.kind) return item.kind;\n if (item.mimeType) return item.mimeType.startsWith(\"image/\") ? \"image\" : \"file\";\n if (FILE_EXTENSION.test(item.src)) return \"file\";\n if (fetchedType && !fetchedType.startsWith(\"image/\")) return \"file\";\n return \"image\";\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\nexport interface LightboxLabels {\n /** The dialog's accessible name. */\n dialog: string;\n close: string;\n previous: string;\n next: string;\n /** The visible counter: \"3 / 7\". */\n counter: (index: number, count: number) => string;\n /** What a screen reader hears on each page turn: \"Image 3 of 7\". The alt follows. */\n position: (index: number, count: number) => string;\n download: string;\n /** The zoom toggle (a pressed/not-pressed button — its name does not change). */\n zoom: string;\n /** A file card's note: the file has no picture to show. */\n noPreview: string;\n /** A file card's link to the browser's own viewer (a PDF opens in its viewer). */\n openInNewTab: string;\n}\n\n// Defined in ./lightbox-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIGHTBOX_LABELS };\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport interface LightboxProps {\n open: boolean;\n /** The X, Escape and the Back gesture all call this. */\n onClose: () => void;\n items: readonly ImageItem[];\n /** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */\n index: number;\n onIndexChange: (index: number) => void;\n /** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is\n * loaded by the browser as usual. */\n fetcher?: AuthedFetcher | null;\n /** Previous from the first goes to the last, and next from the last to the first.\n * Default `false`: the buttons at the ends refuse (`aria-disabled`, focus kept — 0.52),\n * which says where the set ends. */\n loop?: boolean;\n /** Double-click (or double-tap) the picture, or the zoom button, to see it at full\n * size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */\n zoom?: boolean;\n /** A download link in the header. Default `true`. */\n download?: boolean;\n /**\n * Download through the app instead — kastlan's floor plans have `openDocument(id)`,\n * which knows the stored file name. Replaces the link with a button that calls this.\n */\n onDownload?: (item: ImageItem, index: number) => void;\n /** Push a history entry so Back closes the viewer. Default `true`; see\n * `FullBleedDialog`'s `backCloses` for when to turn it off. */\n backCloses?: boolean;\n labels?: LabelOverride<LightboxLabels>;\n}\n\n/** Swipes shorter than this are taps, and a drag that is mostly vertical is a scroll. */\nconst SWIPE_MIN_PX = 50;\n\n/** An end arrow's look: a disabled `overlay` IconButton's (its 50% and its resting disc\n * under hover), for the focusable kind — `disabled:` cannot match `aria-disabled`. */\nconst ARROW_AT_END =\n \"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]\";\n\n/**\n * A full-screen viewer for a set of images (and files that are not images).\n *\n * Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's\n * one implementation of everything an overlay owes the page: the focus trap, Escape,\n * the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the\n * body-scroll lock and the enter/exit motion.\n *\n * Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the\n * direction is read at the point the viewer is rendered, since the dialog itself is\n * portalled out of it), Home/End, and a horizontal swipe on a touch screen.\n *\n * Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture\n * at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —\n * the browser's own page pinch still works — because a pinch handler would have to own\n * every touch on the picture, and with it the swipe.\n *\n * A non-image (a PDF floor plan) is shown as a file card: its name, \"no preview\", a\n * link that opens it in the browser's own viewer, and the download. An `<iframe>`\n * preview was the alternative; phone browsers render a PDF in one inconsistently or\n * not at all, and a card works everywhere.\n */\nexport function Lightbox({\n open,\n onClose,\n items,\n index: indexProp,\n onIndexChange,\n fetcher,\n loop = false,\n zoom: zoomEnabled = true,\n download = true,\n onDownload,\n backCloses,\n labels: labelsProp,\n}: LightboxProps) {\n const labels = useKitLabels(\"lightbox\", DEFAULT_LIGHTBOX_LABELS, labelsProp);\n const imageLabels = useKitLabels(\"authedImage\", DEFAULT_AUTHED_IMAGE_LABELS);\n const count = items.length;\n // Clamped: the set can shrink under an open viewer (the photo on screen was deleted).\n const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);\n const item = items[index] as ImageItem | undefined;\n\n // The dialog is portalled to <body>, out of the subtree whose `dir` it would have\n // inherited; this marker stays where the viewer was rendered and reads it there.\n const anchorRef = useRef<HTMLSpanElement>(null);\n const dir = useAnchorDir(anchorRef, open);\n\n // The toolbar's words (docs/text-size-harmonization.md §4, §10.8). At Large an\n // IconButton shows its label beside the glyph; on a narrow screen the viewer's header —\n // the counter, Zoom, Download and the dialog's close — has no room for two words at\n // 150 % (a 390 px phone is 260 px of Normal type), so below `sm` the toolbar keeps its\n // icons, as the plain download link hides its word there (`max-sm:sr-only`). From `sm`\n // up, at the same breakpoint the CSS uses, the words show. Each name stays the\n // accessible name and the tooltip either way.\n const narrow = useBreakpoint(\"max-sm\");\n const toolbarLabelVisible = narrow ? false : undefined;\n\n const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });\n const kind = item ? imageItemKind(item, fetched.type) : \"image\";\n\n const [zoomedKey, setZoomedKey] = useState<string | null>(null);\n const itemKey = item ? String(item.key ?? item.src) : null;\n // Zoom belongs to the picture it was turned on for: paging on resets it.\n const zoomed = zoomEnabled && kind === \"image\" && zoomedKey !== null && zoomedKey === itemKey;\n const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n const go = (step: number) => {\n if (count === 0) return;\n let next = index + step;\n if (loop) next = (next + count) % count;\n if (next < 0 || next >= count || next === index) return;\n onIndexChange(next);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A key meant for a control stays with it: Space/Enter on a button, typing in any\n // field a caller's caption might hold.\n const target = e.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const step = horizontalStep(e.key, e.currentTarget);\n if (step !== 0) {\n e.preventDefault();\n go(step);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n if (index !== 0) onIndexChange(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n if (index !== count - 1) onIndexChange(count - 1);\n }\n };\n\n const touchStart = useRef<{ x: number; y: number } | null>(null);\n const onTouchStart = (e: TouchEvent) => {\n const t = e.touches[0];\n touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;\n };\n const onTouchEnd = (e: TouchEvent) => {\n const start = touchStart.current;\n touchStart.current = null;\n const t = e.changedTouches[0];\n // Zoomed, a drag is a PAN of the picture, not a page turn.\n if (!start || !t || zoomed) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;\n // Swiping toward the START edge brings in the next picture from the end edge.\n const towardStart = dir === \"rtl\" ? dx > 0 : dx < 0;\n go(towardStart ? 1 : -1);\n };\n\n // Paging the viewer while zoomed would leave the new picture scrolled to where the\n // last one was panned; start each at its top-left.\n const bodyRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n bodyRef.current?.scrollTo?.({ top: 0, left: 0 });\n }, [index]);\n\n const downloadHref = fetched.src;\n const downloadControl =\n download && item ? (\n onDownload ? (\n <IconButton\n size=\"sm\"\n label={labels.download}\n labelVisible={toolbarLabelVisible}\n onClick={() => onDownload(item, index)}\n >\n <Download />\n </IconButton>\n ) : downloadHref ? (\n <a\n href={downloadHref}\n // `download` names the saved file; a blob: URL has no name of its own.\n download={item.fileName ?? \"\"}\n className={buttonClasses(\"ghost\", { size: \"sm\" })}\n >\n <Download aria-hidden className=\"size-4\" />\n <span className=\"max-sm:sr-only\">{labels.download}</span>\n </a>\n ) : null\n ) : null;\n\n const header = item ? (\n <div className=\"flex min-w-0 items-center gap-2\">\n <span aria-hidden className=\"shrink-0 text-sm tabular-nums text-secondary\">\n {labels.counter(index + 1, count)}\n </span>\n {/* The page turn, said — focus does not move when the picture changes, so\n without this a reader pressing → hears nothing at all. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {`${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : \"\"}`}\n </span>\n <span className=\"min-w-0 flex-1\" />\n {zoomEnabled && kind === \"image\" && (\n <IconButton\n size=\"sm\"\n label={labels.zoom}\n labelVisible={toolbarLabelVisible}\n pressed={zoomed}\n onClick={toggleZoom}\n >\n <ZoomIn />\n </IconButton>\n )}\n {downloadControl}\n </div>\n ) : null;\n\n const slide = !item ? null : kind === \"file\" ? (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-lg border border-subtle p-6 text-center\">\n <FileText aria-hidden className=\"size-10 text-muted\" />\n <p className=\"break-all font-medium text-primary\">{item.fileName ?? item.alt}</p>\n <p className=\"text-sm text-muted\">{labels.noPreview}</p>\n {fetched.src && (\n <a\n href={fetched.src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={buttonClasses(\"secondary\", { size: \"sm\" })}\n >\n <ExternalLink aria-hidden className=\"size-4\" />\n {labels.openInNewTab}\n </a>\n )}\n </div>\n ) : (\n <ImageStates\n // Keyed by item: a new picture is a new decode, never the old one's \"loaded\".\n key={itemKey ?? undefined}\n fetched={fetched}\n alt={item.alt}\n labels={imageLabels}\n wrapperClassName={zoomed ? \"max-w-none shrink-0\" : \"flex h-full w-full items-center justify-center\"}\n className={cn(\n zoomed ? \"max-w-none cursor-zoom-out\" : \"max-h-full max-w-full object-contain\",\n !zoomed && zoomEnabled && \"cursor-zoom-in\",\n )}\n imgProps={{\n draggable: false,\n onDoubleClick: zoomEnabled ? toggleZoom : undefined,\n }}\n />\n );\n\n return (\n <>\n <span ref={anchorRef} hidden />\n <FullBleedDialog\n open={open && !!item}\n onClose={onClose}\n closeLabel={labels.close}\n aria-label={labels.dialog}\n dir={dir}\n header={header}\n backCloses={backCloses}\n onKeyDown={onKeyDown}\n data-lightbox=\"\"\n footer={\n // `hasContent` (refactor audit 2026-10-30, DUP-14): a `\"\"` caption drew an empty\n // footer bar.\n hasContent(item?.caption) ? (\n <p className=\"me-auto min-w-0 text-sm text-secondary\">{item?.caption}</p>\n ) : undefined\n }\n >\n <div\n ref={bodyRef}\n data-lightbox-stage=\"\"\n data-zoomed={zoomed ? \"\" : undefined}\n onTouchStart={onTouchStart}\n onTouchEnd={onTouchEnd}\n className={cn(\n \"relative h-full min-h-48 w-full\",\n zoomed ? \"overflow-auto\" : \"flex items-center justify-center overflow-hidden\",\n )}\n >\n {slide}\n </div>\n {count > 1 && (\n // At an end the arrow refuses rather than turns `disabled` (refactor audit\n // 2026-10-30, INP-4, JumpCalendar's rule): the press that reaches the last\n // picture is made with the focus on ›, and a focused button that turns\n // `disabled` drops the focus out of the viewer's trap to <body>. kastlan's\n // ImageGrid and keksdose's invoice review open it without `loop`.\n <>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.previous}\n aria-disabled={!canPrev || undefined}\n onClick={() => go(-1)}\n className={cn(\"absolute start-3 top-1/2 -translate-y-1/2\", ARROW_AT_END)}\n tooltip={false}\n >\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.next}\n aria-disabled={!canNext || undefined}\n onClick={() => go(1)}\n className={cn(\"absolute end-3 top-1/2 -translate-y-1/2\", ARROW_AT_END)}\n tooltip={false}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </>\n )}\n </FullBleedDialog>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -42,8 +42,12 @@ export interface ListProps extends Omit<ComponentPropsWithoutRef<"ul">, "role">
|
|
|
42
42
|
* A list of `limit` rows or fewer draws them all, with no such row. Rows are counted as
|
|
43
43
|
* `Children` counts them: one per `ListItem`, a fragment as one.
|
|
44
44
|
*
|
|
45
|
-
* The row is a button with `aria-expanded`, controlling the list
|
|
46
|
-
*
|
|
45
|
+
* The row is a button with `aria-expanded`, controlling the list, in the list's last
|
|
46
|
+
* item: a reader hears "list, 6 items" for five rows and the Show all. Until 0.52 that
|
|
47
|
+
* item was `role="none"`, which left the button standing in the list itself — axe 4.13
|
|
48
|
+
* fails `aria-required-children` on it, and a `<ul>` may hold nothing but items (refactor
|
|
49
|
+
* audit 2026-10-30, found building 0.51.7; DataTable's phone list made the same call for
|
|
50
|
+
* its leading row).
|
|
47
51
|
*/
|
|
48
52
|
limit?: number;
|
|
49
53
|
}
|
package/dist/components/list.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Children, createContext, useContext, useId, useState } from "react";
|
|
4
4
|
import { ChevronDown, ExternalLink } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { hasContent } from "../lib/presence.js";
|
|
6
7
|
import { FOCUS_RING } from "./focus-ring.js";
|
|
7
8
|
import { Spinner } from "./spinner.js";
|
|
8
9
|
import { StatusDot } from "./status-dot.js";
|
|
@@ -53,7 +54,7 @@ function List({
|
|
|
53
54
|
),
|
|
54
55
|
children: [
|
|
55
56
|
folds && !open ? rows.slice(0, Math.max(0, limit)) : rows ?? children,
|
|
56
|
-
folds && /* @__PURE__ */ jsx("li", {
|
|
57
|
+
folds && /* @__PURE__ */ jsx("li", { className: MORE_PAD[density], children: /* @__PURE__ */ jsx(
|
|
57
58
|
Button,
|
|
58
59
|
{
|
|
59
60
|
type: "button",
|
|
@@ -154,7 +155,7 @@ function ListItem({
|
|
|
154
155
|
const lock = useLockReason(commit, disabledReason);
|
|
155
156
|
const locked = lock.locked && isButton && !expandable;
|
|
156
157
|
const inert = disabled || loading || locked;
|
|
157
|
-
const hasBody = children
|
|
158
|
+
const hasBody = hasContent(children);
|
|
158
159
|
const stacked = hasBody || expandable;
|
|
159
160
|
const titleId = useId();
|
|
160
161
|
const expandedId = useId();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { Children, createContext, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Spinner } from \"./spinner\";\nimport { StatusDot } from \"./status-dot\";\nimport { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { LockedReason, mergeDescribedBy, useLockReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { DEFAULT_LIST_LABELS } from \"./list-labels\";\nimport { Button } from \"./button\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n /** 0.51.4: the row under a {@link ListProps.limit}ed list that opens the rest, with\n * the WHOLE list's count — \"Show all (12)\". Optional, as every key added to a\n * namespace an app may annotate: left out, the provider's, then English. */\n showAll?: (count: number) => string;\n /** 0.51.4: the same row once the list is open, folding it back. */\n showFewer?: string;\n}\n\n// Defined in ./list-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIST_LABELS };\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n /** The `<ul>` (or `<ol>`). A React 19 ref prop since 0.34 (MOD-4, refactor audit\n * 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLUListElement>;\n /**\n * Show the first `limit` rows, then a \"Show all (n)\" row that opens the rest and turns\n * into \"Show fewer\" (0.51.4, keksdose run 89: a long \"Placed only roughly\" list on its\n * Matching report could only fold as a whole, so it opened as nothing or everything).\n * A list of `limit` rows or fewer draws them all, with no such row. Rows are counted as\n * `Children` counts them: one per `ListItem`, a fragment as one.\n *\n * The row is a button with `aria-expanded`, controlling the list; it sits in the list\n * as `role=\"none\"`, so a reader still hears \"list, 5 items\" for five rows.\n */\n limit?: number;\n}\n\n/** The show-all row's inset, in step with each density's row padding (`PAD`). */\nconst MORE_PAD: Record<ListDensity, string> = {\n compact: \"px-2 py-1\",\n default: \"px-3 py-1.5\",\n comfortable: \"px-4 py-2\",\n};\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport function List({\n ref,\n as = \"ul\",\n density = \"default\",\n separator = \"gap\",\n limit,\n className,\n children,\n ...rest\n}: ListProps) {\n const Tag = as;\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const [open, setOpen] = useState(false);\n const generatedId = useId();\n const id = rest.id ?? generatedId;\n const rows = limit === undefined ? null : Children.toArray(children);\n const folds = rows !== null && rows.length > Math.max(0, limit!);\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n id={folds ? id : rest.id}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {/* The same array folded and open: `Children.toArray` re-keys every row (`.$a`),\n so the raw `children` on one side remounted the first rows at each toggle and\n dropped their state — an opened row, a typed input (audit 2026-10-30 DSP-20). */}\n {folds && !open ? rows.slice(0, Math.max(0, limit!)) : (rows ?? children)}\n {folds && (\n <li role=\"none\" className={MORE_PAD[density]}>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto p-0\"\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen((was) => !was)}\n >\n {open\n ? (labels.showFewer ?? DEFAULT_LIST_LABELS.showFewer)\n : (labels.showAll ?? DEFAULT_LIST_LABELS.showAll!)(rows.length)}\n </Button>\n </li>\n )}\n </Tag>\n </ListContext.Provider>\n );\n}\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; leadingActions: string; gap: string; body: string }> =\n {\n compact: {\n target: \"gap-2 px-2 py-1.5\",\n actions: \"pe-1.5\",\n leadingActions: \"ps-1.5\",\n gap: \"gap-0.5\",\n body: \"px-2 pb-1.5\",\n },\n default: {\n target: \"gap-3 px-3 py-2.5\",\n actions: \"pe-2\",\n leadingActions: \"ps-2\",\n gap: \"gap-1\",\n body: \"px-3 pb-2.5\",\n },\n comfortable: {\n target: \"gap-3 px-4 py-3\",\n actions: \"pe-3\",\n leadingActions: \"ps-3\",\n gap: \"gap-1\",\n body: \"px-4 pb-3\",\n },\n };\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\n\n// The kit's focus frame (§5), inset so the row's own border and its neighbours do not\n// clip it.\nconst TARGET_RING = cn(FOCUS_RING, \"focus-visible:ring-inset\");\n\n/**\n * §4 \"ListItem: title, subtitle and meta wrap instead of truncating\" at Large and Extra\n * large (docs/text-size-harmonization.md). `large:` variants over the Normal look, so\n * a list keeps its one-height rows at Normal and costs no render to switch: a one-line\n * line is `truncate-until-large` (tokens.css, 0.33 §10.17: `truncate`, and at Large a\n * wrap that breaks a word too long for the row — the kit's one name for what this file\n * spelt as `WRAP_AT_LARGE`), a `line-clamp-2` lets go. An ellipsis hides the end of a\n * name, which is the part a reader who needs big type cannot recover by squinting.\n */\nconst UNCLAMP_AT_LARGE = \"large:line-clamp-none\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n /** The main target — the row's `<a>`, `<button>` or `<div>`, where `id` goes. A React\n * 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | ({\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is a line under the row at Large and on a touch\n * screen, else in the kit {@link Tooltip} over the row (`disabledReasonDisplay`),\n * and on its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`. A long list\n * of locked rows under a banner that already says why can keep it to the\n * tooltip with `\"tooltip\"` (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: CommitScope;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\n | ({\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it\n * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and\n * a trailing slot drops under the text when the two no longer fit side by side. The\n * rows lose their one height there; that is the trade the reader asked for.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport function ListItem({\n ref,\n title,\n titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n disabledReasonDisplay,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\n ...rest\n}: ListItemProps) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n const hasBody = children !== undefined && children !== null && children !== false;\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && <Icon className={cn(\"size-4 shrink-0 text-muted\", align === \"start\" && \"mt-0.5\")} aria-hidden />}\n {leading}\n {/* With a trailing slot, a basis at Large: below it the trailing chip or figure\n wraps under the text (the target wraps, see `target`) rather than squeezing the\n now-wrapping title into a column one word wide (§4, 240 px effective width). */}\n <span className={cn(\"flex min-w-0 flex-1 flex-col\", trailing != null && \"large:basis-40\")}>\n {overline != null && (\n <span className=\"block truncate-until-large text-caption font-medium leading-snug text-muted\">\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-primary\",\n titleLines === 1\n ? \"truncate-until-large\"\n : titleLines === 2\n ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE)\n : \"break-words\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-muted\",\n subtitleLines === 2 ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE) : \"truncate-until-large\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-caption leading-snug text-muted\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate-until-large\",\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs large:ms-auto\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external &&\n href !== undefined && <ExternalLink className=\"size-3.5 shrink-0 text-muted rtl:-scale-x-100\" aria-hidden />\n )}\n {mark}\n {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-muted transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n // §4 at Large: the trailing slot drops under the text when both no longer fit.\n trailing != null && \"large:flex-wrap\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // When stacked the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-hover\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading ? (e) => e.preventDefault() : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The frame takes the target's place in the row, so it takes its flex sizing too;\n // the button fills it (`items-stretch` in the line's column). The line takes the\n // row's body padding, so its words start where the title's do.\n main = (\n <LockedReason\n lock={lock}\n display={disabledReasonDisplay}\n className=\"flex min-w-0 flex-1 items-stretch\"\n lineClassName={pad.body}\n >\n {main}\n </LockedReason>\n );\n }\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected ? \"border-brand bg-surface-2\" : bordered ? \"border-subtle\" : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-hover\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\n );\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-subtle last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n}\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AA4GM,SAgfF,UA/dQ,KAjBN;AA5GN,SAAS,UAAU,eAAe,YAAY,OAAO,gBAAgB;AAErE,SAAS,aAAa,oBAAoB;AAE1C,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAE9B,SAAS,cAAc,kBAAkB;AACzC,SAAS,wBAAwB;AACjC,SAAS,cAAc,kBAAkB,qBAAiD;AAE1F,SAAS,2BAA2B;AACpC,SAAS,cAAc;AA+BvB,MAAM,cAAc,cAAkE;AAAA,EACpF,SAAS;AAAA,EACT,WAAW;AACb,CAAC;AA0BD,MAAM,WAAwC;AAAA,EAC5C,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AACf;AASO,SAAS,KAAK;AAAA,EACnB;AAAA,EACA,KAAK;AAAA,EACL,UAAU;AAAA,EACV,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAc;AACZ,QAAM,MAAM;AACZ,QAAM,SAAS,aAAa,QAAQ,mBAAmB;AACvD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,KAAK,MAAM;AACtB,QAAM,OAAO,UAAU,SAAY,OAAO,SAAS,QAAQ,QAAQ;AACnE,QAAM,QAAQ,SAAS,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG,KAAM;AAC/D,SACE,oBAAC,YAAY,UAAZ,EAAqB,OAAO,EAAE,SAAS,UAAU,GAChD;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,IAAI,QAAQ,KAAK,KAAK;AAAA,MACtB;AAAA,MACA,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,cAAc,UAAU,YAAY,YAAY,YAAY;AAAA,QAC5D;AAAA,MACF;AAAA,MAKC;AAAA,iBAAS,CAAC,OAAO,KAAK,MAAM,GAAG,KAAK,IAAI,GAAG,KAAM,CAAC,IAAK,QAAQ;AAAA,QAC/D,SACC,oBAAC,QAAG,MAAK,QAAO,WAAW,SAAS,OAAO,GACzC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,WAAU;AAAA,YACV,iBAAe;AAAA,YACf,iBAAe;AAAA,YACf,SAAS,MAAM,QAAQ,CAAC,QAAQ,CAAC,GAAG;AAAA,YAEnC,iBACI,OAAO,aAAa,oBAAoB,aACxC,OAAO,WAAW,oBAAoB,SAAU,KAAK,MAAM;AAAA;AAAA,QAClE,GACF;AAAA;AAAA;AAAA,EAEJ,GACF;AAEJ;AAqBA,MAAM,MACJ;AAAA,EACE,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,EACR;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,KAAK;AAAA,IACL,MAAM;AAAA,EACR;AACF;AAQF,MAAM,cAAc,GAAG,YAAY,0BAA0B;AAW7D,MAAM,mBAAmB;AA+TlB,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,KAAK;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,OAAO,WAAW,WAAW;AACnC,QAAM,SAAS,aAAa,QAAQ,mBAAmB;AACvD,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,eAAe;AAC9D,QAAM,UAAU,eAAe,KAAK;AACpC,QAAM,MAAM,IAAI,OAAO;AACvB,QAAM,aAAa,oBAAoB;AACvC,QAAM,WAAW,eAAe,gBAAgB;AAChD,QAAM,SAAS,SAAS,UAAa,CAAC;AACtC,QAAM,WAAW,CAAC,WAAW,YAAY,UAAa,eAAe,SAAS;AAC9E,QAAM,cAAc,UAAU;AAG9B,QAAM,OAAO,cAAc,QAAQ,cAAc;AACjD,QAAM,SAAS,KAAK,UAAU,YAAY,CAAC;AAC3C,QAAM,QAAQ,YAAY,WAAW;AACrC,QAAM,UAAU,aAAa,UAAa,aAAa,QAAQ,aAAa;AAG5E,QAAM,UAAU,WAAW;AAC3B,QAAM,UAAU,MAAM;AACtB,QAAM,aAAa,MAAM;AAEzB,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,QAAI,iBAAiB,OAAW,gBAAe,IAAI;AACnD,uBAAmB,IAAI;AAAA,EACzB;AAEA,QAAM,OACJ,WACC,SAAS,oBAAC,aAAU,MAAK,SAAQ,MAAK,MAAK,WAAW,UAAU,UAAU,WAAW,QAAW,IAAK;AAExG,QAAM,OACJ,iCACG;AAAA,YAAQ,oBAAC,QAAK,WAAW,GAAG,8BAA8B,UAAU,WAAW,QAAQ,GAAG,eAAW,MAAC;AAAA,IACtG;AAAA,IAID,qBAAC,UAAK,WAAW,GAAG,gCAAgC,YAAY,QAAQ,gBAAgB,GACrF;AAAA,kBAAY,QACX,oBAAC,UAAK,WAAU,+EACb,oBACH;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UAEC,IAAI,UAAU,UAAU;AAAA,UACxB,WAAW;AAAA,YACT;AAAA,YACA,eAAe,IACX,yBACA,eAAe,IACb,GAAG,4BAA4B,gBAAgB,IAC/C;AAAA,YACN,SAAS,kBAAkB;AAAA,UAC7B;AAAA,UAEC;AAAA;AAAA,YACA,UAAU,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAO;AAAA,eAAC;AAAA,YACtD,YAAY,SAAS,UAAa,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAc;AAAA,eAAC;AAAA;AAAA;AAAA,MACxF;AAAA,MACC,YAAY,QACX;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB,IAAI,GAAG,4BAA4B,gBAAgB,IAAI;AAAA,UAC3E;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MAED,QAAQ,QACP;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,WAAW,sCAAsC;AAAA,UACnD;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MAED,WAAW,QAAQ,oBAAC,UAAK,WAAU,wBAAwB,mBAAQ;AAAA,OACtE;AAAA,IACC,YAAY,QACX;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,iBAAiB,aAAa,cAAc,YAAY;AAAA,QAC1D;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAED,UACC,oBAAC,WAAQ,OAAO,MAAM,WAAU,qBAAoB,IAEpD,YACA,SAAS,UAAa,oBAAC,gBAAa,WAAU,iDAAgD,eAAW,MAAC;AAAA,IAE3G;AAAA,IACA;AAAA;AAAA,IAGC;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,UAAU,WAAW;AAAA,UACrB,YAAY;AAAA,QACd;AAAA;AAAA,IACF;AAAA,KAEJ;AAGF,QAAM,SAAS;AAAA,IACb;AAAA;AAAA,IAEA,YAAY,QAAQ;AAAA,IACpB,UAAU,UAAU,gBAAgB;AAAA,IACpC,IAAI;AAAA;AAAA;AAAA,IAGJ,UAAU,eAAe;AAAA,IACzB,WAAW,eAAe,CAAC,SAAS,CAAC,YAAY;AAAA,IACjD,eAAe;AAAA,IACf,eAAe,CAAC,SAAS;AAAA,IACzB,WAAW;AAAA,IACX,UAAU,CAAC,WAAW;AAAA,IACtB;AAAA,EACF;AAIA,QAAM,EAAE,MAAM,cAAc,GAAG,aAAa,IAAI,eAAe,CAAC;AAEhE,MAAI;AACJ,MAAI,QAAQ;AACV,UAAM,YAA+B;AAAA,MACnC,GAAI;AAAA,MACJ,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,gBAAgB,WAAW,SAAS;AAAA,MACpC,aAAa,WAAW;AAAA,MACxB,SAAS,UAAU,CAAC,MAAM,EAAE,eAAe,IAAK;AAAA,MAChD;AAAA,MACA,UAAU;AAAA,IACZ;AACA,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,UAAM,SAAS,WAAW,SAAY,iBAAiB,YAAY,SAAS,IAAI;AAChF,WAAO,SACL,oBAAC,gBAAa,QAAiB,GAAG,WAAW,IAE7C,oBAAC,OAAG,GAAG,aAAc,GAAI,WAAW,EAAE,QAAQ,UAAU,KAAK,sBAAsB,IAAI,MACpF,oBACH;AAAA,EAEJ,WAAW,UAAU;AACnB,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAI;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA,MAAK;AAAA,QAIL,UAAU,SAAS,SAAY;AAAA,QAC/B,iBAAe,WAAW,UAAU;AAAA,QACpC,aAAW,WAAW;AAAA,QACtB,gBAAc,WAAW,SAAS;AAAA,QAClC,iBAAe,aAAa,WAAW;AAAA,QACvC,iBAAe,aAAa,aAAa;AAAA,QACzC,oBACE,SACI,iBAAiB,KAAK,kBAAkB,GAAyB,KAAK,QAAQ,IAC7E,KAAK,kBAAkB;AAAA,QAE9B,SACE,WAAW,SACP,SACA,aACE,SACC;AAAA,QAET,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAEF,QAAI,QAAQ;AAIV,aACE;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,SAAS;AAAA,UACT,WAAU;AAAA,UACV,eAAe,IAAI;AAAA,UAElB;AAAA;AAAA,MACH;AAAA,IAEJ;AAAA,EACF,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAI;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA,iBAAe,YAAY;AAAA,QAC3B,aAAW,WAAW;AAAA,QACtB,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,mBACJ,kBAAkB,OAChB,oBAAC,SAAI,WAAW,GAAG,8BAA8B,IAAI,KAAK,IAAI,cAAc,GAAI,0BAAe,IAC7F;AACN,QAAM,YACJ,WAAW,OAAO,oBAAC,SAAI,WAAW,GAAG,8BAA8B,IAAI,KAAK,IAAI,OAAO,GAAI,mBAAQ,IAAS;AAE9G,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,UAAU,aAAa;AAAA;AAAA,QAEvB,aAAa;AAAA,QACb,WAAW,8BAA8B,WAAW,kBAAkB;AAAA,QACtE,CAAC,WAAW,eAAe,CAAC,SAAS,CAAC,YAAY;AAAA,SACjD,YAAY,WAAW;AAAA,QACxB;AAAA,MACF;AAAA,MAEC,oBACC,iCACE;AAAA,6BAAC,SAAI,WAAU,6BACZ;AAAA;AAAA,UACA;AAAA,UACA;AAAA,WACH;AAAA,QACC,cACC,oBAAC,YAAS,MAAM,UACd,8BAAC,SAAI,IAAI,YAAY,MAAK,SAAQ,mBAAiB,SAAS,WAAW,GAAG,WAAW,IAAI,IAAI,GAC1F,2BACH,GACF;AAAA,QAED,WACC,oBAAC,SAAI,MAAK,SAAQ,mBAAiB,SAAS,WAAW,GAAG,WAAW,IAAI,MAAM,aAAa,GACzF,UACH;AAAA,SAEJ,IAEA,iCACG;AAAA;AAAA,QACA;AAAA,QACA;AAAA,SACH;AAAA;AAAA,EAEJ;AAGF,QAAM,MAAM;AACZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT,KAAK,cAAc,aAAa;AAAA,QAChC,KAAK,cAAc,aAAa,OAAO,QAAQ;AAAA,QAC/C,aAAa;AAAA,MACf;AAAA,MAEC,sBAAY,UAAU,GAAG,IAAI;AAAA;AAAA,EAChC;AAEJ;AAEA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { Children, createContext, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Spinner } from \"./spinner\";\nimport { StatusDot } from \"./status-dot\";\nimport { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { LockedReason, mergeDescribedBy, useLockReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { DEFAULT_LIST_LABELS } from \"./list-labels\";\nimport { Button } from \"./button\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n /** 0.51.4: the row under a {@link ListProps.limit}ed list that opens the rest, with\n * the WHOLE list's count — \"Show all (12)\". Optional, as every key added to a\n * namespace an app may annotate: left out, the provider's, then English. */\n showAll?: (count: number) => string;\n /** 0.51.4: the same row once the list is open, folding it back. */\n showFewer?: string;\n}\n\n// Defined in ./list-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIST_LABELS };\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n /** The `<ul>` (or `<ol>`). A React 19 ref prop since 0.34 (MOD-4, refactor audit\n * 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLUListElement>;\n /**\n * Show the first `limit` rows, then a \"Show all (n)\" row that opens the rest and turns\n * into \"Show fewer\" (0.51.4, keksdose run 89: a long \"Placed only roughly\" list on its\n * Matching report could only fold as a whole, so it opened as nothing or everything).\n * A list of `limit` rows or fewer draws them all, with no such row. Rows are counted as\n * `Children` counts them: one per `ListItem`, a fragment as one.\n *\n * The row is a button with `aria-expanded`, controlling the list, in the list's last\n * item: a reader hears \"list, 6 items\" for five rows and the Show all. Until 0.52 that\n * item was `role=\"none\"`, which left the button standing in the list itself — axe 4.13\n * fails `aria-required-children` on it, and a `<ul>` may hold nothing but items (refactor\n * audit 2026-10-30, found building 0.51.7; DataTable's phone list made the same call for\n * its leading row).\n */\n limit?: number;\n}\n\n/** The show-all row's inset, in step with each density's row padding (`PAD`). */\nconst MORE_PAD: Record<ListDensity, string> = {\n compact: \"px-2 py-1\",\n default: \"px-3 py-1.5\",\n comfortable: \"px-4 py-2\",\n};\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport function List({\n ref,\n as = \"ul\",\n density = \"default\",\n separator = \"gap\",\n limit,\n className,\n children,\n ...rest\n}: ListProps) {\n const Tag = as;\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const [open, setOpen] = useState(false);\n const generatedId = useId();\n const id = rest.id ?? generatedId;\n const rows = limit === undefined ? null : Children.toArray(children);\n const folds = rows !== null && rows.length > Math.max(0, limit!);\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n id={folds ? id : rest.id}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {/* The same array folded and open: `Children.toArray` re-keys every row (`.$a`),\n so the raw `children` on one side remounted the first rows at each toggle and\n dropped their state — an opened row, a typed input (audit 2026-10-30 DSP-20). */}\n {folds && !open ? rows.slice(0, Math.max(0, limit!)) : (rows ?? children)}\n {folds && (\n <li className={MORE_PAD[density]}>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto p-0\"\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen((was) => !was)}\n >\n {open\n ? (labels.showFewer ?? DEFAULT_LIST_LABELS.showFewer)\n : (labels.showAll ?? DEFAULT_LIST_LABELS.showAll!)(rows.length)}\n </Button>\n </li>\n )}\n </Tag>\n </ListContext.Provider>\n );\n}\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; leadingActions: string; gap: string; body: string }> =\n {\n compact: {\n target: \"gap-2 px-2 py-1.5\",\n actions: \"pe-1.5\",\n leadingActions: \"ps-1.5\",\n gap: \"gap-0.5\",\n body: \"px-2 pb-1.5\",\n },\n default: {\n target: \"gap-3 px-3 py-2.5\",\n actions: \"pe-2\",\n leadingActions: \"ps-2\",\n gap: \"gap-1\",\n body: \"px-3 pb-2.5\",\n },\n comfortable: {\n target: \"gap-3 px-4 py-3\",\n actions: \"pe-3\",\n leadingActions: \"ps-3\",\n gap: \"gap-1\",\n body: \"px-4 pb-3\",\n },\n };\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\n\n// The kit's focus frame (§5), inset so the row's own border and its neighbours do not\n// clip it.\nconst TARGET_RING = cn(FOCUS_RING, \"focus-visible:ring-inset\");\n\n/**\n * §4 \"ListItem: title, subtitle and meta wrap instead of truncating\" at Large and Extra\n * large (docs/text-size-harmonization.md). `large:` variants over the Normal look, so\n * a list keeps its one-height rows at Normal and costs no render to switch: a one-line\n * line is `truncate-until-large` (tokens.css, 0.33 §10.17: `truncate`, and at Large a\n * wrap that breaks a word too long for the row — the kit's one name for what this file\n * spelt as `WRAP_AT_LARGE`), a `line-clamp-2` lets go. An ellipsis hides the end of a\n * name, which is the part a reader who needs big type cannot recover by squinting.\n */\nconst UNCLAMP_AT_LARGE = \"large:line-clamp-none\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n /** The main target — the row's `<a>`, `<button>` or `<div>`, where `id` goes. A React\n * 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | ({\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is a line under the row at Large and on a touch\n * screen, else in the kit {@link Tooltip} over the row (`disabledReasonDisplay`),\n * and on its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`. A long list\n * of locked rows under a banner that already says why can keep it to the\n * tooltip with `\"tooltip\"` (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: CommitScope;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\n | ({\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it\n * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and\n * a trailing slot drops under the text when the two no longer fit side by side. The\n * rows lose their one height there; that is the trade the reader asked for.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport function ListItem({\n ref,\n title,\n titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n disabledReasonDisplay,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\n ...rest\n}: ListItemProps) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n // `hasContent` (refactor audit 2026-10-30, DUP-14): a `\"\"` body opened an empty line.\n const hasBody = hasContent(children);\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && <Icon className={cn(\"size-4 shrink-0 text-muted\", align === \"start\" && \"mt-0.5\")} aria-hidden />}\n {leading}\n {/* With a trailing slot, a basis at Large: below it the trailing chip or figure\n wraps under the text (the target wraps, see `target`) rather than squeezing the\n now-wrapping title into a column one word wide (§4, 240 px effective width). */}\n <span className={cn(\"flex min-w-0 flex-1 flex-col\", trailing != null && \"large:basis-40\")}>\n {overline != null && (\n <span className=\"block truncate-until-large text-caption font-medium leading-snug text-muted\">\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-primary\",\n titleLines === 1\n ? \"truncate-until-large\"\n : titleLines === 2\n ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE)\n : \"break-words\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-muted\",\n subtitleLines === 2 ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE) : \"truncate-until-large\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-caption leading-snug text-muted\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate-until-large\",\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs large:ms-auto\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external &&\n href !== undefined && <ExternalLink className=\"size-3.5 shrink-0 text-muted rtl:-scale-x-100\" aria-hidden />\n )}\n {mark}\n {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-muted transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n // §4 at Large: the trailing slot drops under the text when both no longer fit.\n trailing != null && \"large:flex-wrap\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // When stacked the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-hover\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading ? (e) => e.preventDefault() : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The frame takes the target's place in the row, so it takes its flex sizing too;\n // the button fills it (`items-stretch` in the line's column). The line takes the\n // row's body padding, so its words start where the title's do.\n main = (\n <LockedReason\n lock={lock}\n display={disabledReasonDisplay}\n className=\"flex min-w-0 flex-1 items-stretch\"\n lineClassName={pad.body}\n >\n {main}\n </LockedReason>\n );\n }\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected ? \"border-brand bg-surface-2\" : bordered ? \"border-subtle\" : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-hover\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\n );\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-subtle last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n}\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -110,7 +110,7 @@ export interface MeasuredGridProps extends Omit<ComponentPropsWithRef<"div">, "c
|
|
|
110
110
|
/** A note under the table (which columns a layout expects, say). Also the grid's
|
|
111
111
|
* description. */
|
|
112
112
|
hint?: ReactNode;
|
|
113
|
-
columns: MeasuredGridColumn[];
|
|
113
|
+
columns: readonly MeasuredGridColumn[];
|
|
114
114
|
/** The draft — typically `useMeasuredRows().props`. */
|
|
115
115
|
cells: string[][];
|
|
116
116
|
onCells: (next: string[][]) => void;
|
|
@@ -5,6 +5,7 @@ import { ClipboardPaste, Eraser, Plus, Trash2 } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { horizontalStep } from "../lib/direction.js";
|
|
7
7
|
import { decimalMark } from "../lib/decimal-marks.js";
|
|
8
|
+
import { roundHalfAway } from "../lib/round.js";
|
|
8
9
|
import { cellNumber, isCellNumber, parseRows, splitRow } from "../lib/table-text.js";
|
|
9
10
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
10
11
|
import { useWindowedRows, WINDOWED_ROW_INDEX } from "../hooks/use-windowed-rows.js";
|
|
@@ -18,7 +19,7 @@ const BLANK = "";
|
|
|
18
19
|
const DEFAULT_ROW_HEIGHT = 28;
|
|
19
20
|
const ALL_VIEWS = ["cells", "text"];
|
|
20
21
|
const withMark = (cell, mark) => mark === "," && isCellNumber(cell) ? cell.replace(".", ",") : cell;
|
|
21
|
-
const formatCell = (value, digits, mark) => withMark(String(
|
|
22
|
+
const formatCell = (value, digits, mark) => withMark(String(roundHalfAway(value, digits)), mark);
|
|
22
23
|
const hasContent = (row) => row.some((cell) => cell.trim() !== "");
|
|
23
24
|
function useMeasuredRows(initial, options = {}) {
|
|
24
25
|
const { digits = 4 } = options;
|