@eifi1/ui-kit 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -82
- package/dist/account/delete-account-setting.d.ts +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/admin/plan-change.js +2 -2
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/user-roster.d.ts +6 -0
- package/dist/admin/user-roster.js +4 -0
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +36 -0
- package/dist/auth/auth-errors.js +20 -0
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +26 -0
- package/dist/billing/billing-labels.js +9 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/subscription-standing.d.ts +61 -0
- package/dist/billing/subscription-standing.js +51 -0
- package/dist/billing/subscription-standing.js.map +1 -0
- package/dist/billing/usage-meter.d.ts +52 -0
- package/dist/billing/usage-meter.js +52 -0
- package/dist/billing/usage-meter.js.map +1 -0
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +4 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/alert-banner.js +2 -3
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -15
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.js +2 -1
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button.d.ts +35 -1
- package/dist/components/button.js +5 -9
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +6 -4
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/camera-button.d.ts +89 -0
- package/dist/components/camera-button.js +122 -0
- package/dist/components/camera-button.js.map +1 -0
- package/dist/components/camera-capture.d.ts +66 -0
- package/dist/components/camera-capture.js +190 -0
- package/dist/components/camera-capture.js.map +1 -0
- package/dist/components/camera-labels.d.ts +20 -0
- package/dist/components/camera-labels.js +15 -0
- package/dist/components/camera-labels.js.map +1 -0
- package/dist/components/chart-edge.d.ts +9 -0
- package/dist/components/chart-edge.js +14 -3
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-zoom-core.js +34 -29
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/checkbox-group.js +1 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/combobox-core.js +3 -2
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +2 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/consent-again-notice-labels.d.ts +25 -0
- package/dist/components/consent-again-notice-labels.js +15 -0
- package/dist/components/consent-again-notice-labels.js.map +1 -0
- package/dist/components/consent-again-notice.d.ts +85 -0
- package/dist/components/consent-again-notice.js +94 -0
- package/dist/components/consent-again-notice.js.map +1 -0
- package/dist/components/country-select.js +2 -7
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +2 -1
- package/dist/components/currency-option-list.js +10 -3
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -1
- package/dist/components/currency-select.js +2 -2
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +22 -1
- package/dist/components/danger-confirm.js +52 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-column-settings.js +2 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +2 -1
- package/dist/components/data-table-filter-popover.js +3 -2
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +33 -3
- package/dist/components/data-table-filters.js +6 -4
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-virtual.js +13 -3
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/date-field.js +2 -2
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +2 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +2 -6
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +5 -1
- package/dist/components/dialog-frame.js +6 -2
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/dropdown.js +7 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.js +2 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-parts.d.ts +108 -6
- package/dist/components/field-parts.js +59 -6
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-suggestion-labels.d.ts +2 -0
- package/dist/components/field-suggestion-labels.js +10 -0
- package/dist/components/field-suggestion-labels.js.map +1 -0
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/floating-panel.d.ts +7 -6
- package/dist/components/floating-panel.js +3 -17
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +3 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.js +3 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.d.ts +23 -5
- package/dist/components/icon-button.js +6 -8
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.js +2 -1
- package/dist/components/inline-confirm.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +2 -2
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +3 -3
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal.d.ts +13 -1
- package/dist/components/legal.js +12 -0
- package/dist/components/legal.js.map +1 -1
- package/dist/components/legended-series-chart.d.ts +80 -0
- package/dist/components/legended-series-chart.js +70 -0
- package/dist/components/legended-series-chart.js.map +1 -0
- package/dist/components/lightbox.js +2 -1
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/mini-calendar.js +16 -7
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +48 -0
- package/dist/components/modal.js +26 -6
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +1 -1
- package/dist/components/month-picker.js +6 -6
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.js +2 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.js +2 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/page-header-layout.d.ts +18 -0
- package/dist/components/page-header-layout.js +23 -0
- package/dist/components/page-header-layout.js.map +1 -0
- package/dist/components/page-header-skeleton.d.ts +41 -0
- package/dist/components/page-header-skeleton.js +52 -0
- package/dist/components/page-header-skeleton.js.map +1 -0
- package/dist/components/page-header.js +4 -17
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/popover.js +7 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/release-notes-labels.d.ts +16 -0
- package/dist/components/release-notes-labels.js +18 -0
- package/dist/components/release-notes-labels.js.map +1 -0
- package/dist/components/release-notes.d.ts +65 -0
- package/dist/components/release-notes.js +65 -0
- package/dist/components/release-notes.js.map +1 -0
- package/dist/components/select.js +2 -2
- package/dist/components/select.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +9 -0
- package/dist/components/series-chart-labels.js +3 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +5 -0
- package/dist/components/series-chart.js +89 -36
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/stat-tile.d.ts +21 -2
- package/dist/components/stat-tile.js +4 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/toggle-group.js +3 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/treemap-layout.js.map +1 -1
- package/dist/components/treemap.d.ts +17 -15
- package/dist/components/treemap.js +167 -85
- package/dist/components/treemap.js.map +1 -1
- package/dist/currencies.d.ts +16 -2
- package/dist/currencies.js +2 -1
- package/dist/currencies.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +2 -2
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-context-labels.js +2 -1
- package/dist/feedback/feedback-context-labels.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +19 -3
- package/dist/feedback/feedback-context.js +12 -1
- package/dist/feedback/feedback-context.js.map +1 -1
- package/dist/feedback/feedback-diagnostics.d.ts +17 -0
- package/dist/feedback/feedback-diagnostics.js +27 -0
- package/dist/feedback/feedback-diagnostics.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +10 -0
- package/dist/feedback/feedback-dialog.js +6 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox-page.d.ts +260 -0
- package/dist/feedback/feedback-inbox-page.js +207 -0
- package/dist/feedback/feedback-inbox-page.js.map +1 -0
- package/dist/feedback/feedback-record.d.ts +5 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +3 -0
- package/dist/feedback/feedback-row-detail.js +3 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
- package/dist/feedback/feedback-submit-dialog.js +15 -3
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +30 -2
- package/dist/feedback/feedback-submit.js +5 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.js +2 -2
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/feedback.d.ts +1 -0
- package/dist/feedback.js +6 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-debounce.d.ts +32 -5
- package/dist/hooks/use-debounce.js +6 -5
- package/dist/hooks/use-debounce.js.map +1 -1
- package/dist/hooks/use-file-picker.d.ts +3 -1
- package/dist/hooks/use-file-picker.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +19 -3
- package/dist/hooks/use-focus-trap.js +15 -5
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-has-camera.d.ts +36 -0
- package/dist/hooks/use-has-camera.js +53 -0
- package/dist/hooks/use-has-camera.js.map +1 -0
- package/dist/hooks/use-hotkey.js +2 -1
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-overlay-history.d.ts +6 -1
- package/dist/hooks/use-overlay-history.js +10 -2
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +78 -4
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +20 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +95 -5
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +74 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +75 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +77 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +77 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +74 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +22 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-labels.d.ts +8 -0
- package/dist/landing/landing-labels.js +2 -1
- package/dist/landing/landing-labels.js.map +1 -1
- package/dist/lib/changelog.d.ts +130 -0
- package/dist/lib/changelog.js +137 -0
- package/dist/lib/changelog.js.map +1 -0
- package/dist/lib/clipboard-minus.js +2 -2
- package/dist/lib/clipboard-minus.js.map +1 -1
- package/dist/lib/dates.d.ts +36 -4
- package/dist/lib/dates.js +49 -13
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/editable-content.d.ts +33 -0
- package/dist/lib/editable-content.js +15 -0
- package/dist/lib/editable-content.js.map +1 -0
- package/dist/lib/focus-left.d.ts +47 -1
- package/dist/lib/focus-left.js +11 -2
- package/dist/lib/focus-left.js.map +1 -1
- package/dist/lib/format.d.ts +37 -2
- package/dist/lib/format.js +25 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/intl-cache.d.ts +4 -2
- package/dist/lib/intl-cache.js.map +1 -1
- package/dist/lib/server-wake.d.ts +49 -9
- package/dist/lib/server-wake.js +25 -14
- package/dist/lib/server-wake.js.map +1 -1
- package/dist/lib/session-refresh.d.ts +53 -0
- package/dist/lib/session-refresh.js +27 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/translation-review.d.ts +26 -0
- package/dist/lib/translation-review.js +14 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/rhf/fields.js +3 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/shell/app-shell.d.ts +25 -1
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-menu.d.ts +3 -0
- package/dist/shell/dev-menu.js +4 -2
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/held-changes-banner-labels.d.ts +23 -0
- package/dist/shell/held-changes-banner-labels.js +14 -0
- package/dist/shell/held-changes-banner-labels.js.map +1 -0
- package/dist/shell/held-changes-banner.d.ts +75 -0
- package/dist/shell/held-changes-banner.js +106 -0
- package/dist/shell/held-changes-banner.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +27 -12
- package/dist/shell/sync-status-indicator.js +13 -4
- package/dist/shell/sync-status-indicator.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +5 -0
- package/dist/shell/top-bar-parts-labels.js +3 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +11 -2
- package/dist/sync/engine.js +13 -5
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/hooks.d.ts +3 -2
- package/dist/sync/hooks.js +4 -2
- package/dist/sync/hooks.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +2 -2
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +18 -0
- package/dist/theme/chart-palette.js +17 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +3 -3
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -4
- package/dist/theme/contrast.js.map +1 -1
- package/dist/tour/tour-overlay.js +2 -1
- package/dist/tour/tour-overlay.js.map +1 -1
- package/package.json +5 -9
- package/tokens.css +19 -54
- package/src/account/account-parts.ts +0 -47
- package/src/account/account-settings-labels.ts +0 -240
- package/src/account/account-settings.tsx +0 -698
- package/src/account/data-export-setting-labels.ts +0 -20
- package/src/account/data-export-setting.tsx +0 -200
- package/src/account/delete-account-setting-labels.ts +0 -33
- package/src/account/delete-account-setting.tsx +0 -341
- package/src/account/email-change-setting-labels.ts +0 -31
- package/src/account/email-change-setting.tsx +0 -443
- package/src/account/passkeys-setting.tsx +0 -445
- package/src/account/sessions-setting-labels.ts +0 -24
- package/src/account/sessions-setting.tsx +0 -283
- package/src/admin/admin-action-confirm-labels.ts +0 -19
- package/src/admin/admin-action-confirm.tsx +0 -509
- package/src/admin/admin-action-log-labels.ts +0 -83
- package/src/admin/admin-action-log.tsx +0 -464
- package/src/admin/admin-area-labels.ts +0 -114
- package/src/admin/admin-forbidden.tsx +0 -75
- package/src/admin/admin-overview.tsx +0 -258
- package/src/admin/admin-parts.ts +0 -68
- package/src/admin/admin-sections.tsx +0 -208
- package/src/admin/admin-tool-links.tsx +0 -90
- package/src/admin/invitations-panel-labels.ts +0 -31
- package/src/admin/invitations-panel.tsx +0 -516
- package/src/admin/mail-test-card-labels.ts +0 -19
- package/src/admin/mail-test-card.tsx +0 -189
- package/src/admin/plan-change-labels.ts +0 -5
- package/src/admin/plan-change.tsx +0 -474
- package/src/admin/reviewer-scope-editor-labels.ts +0 -16
- package/src/admin/reviewer-scope-editor.tsx +0 -224
- package/src/admin/role-select-labels.ts +0 -16
- package/src/admin/role-select.tsx +0 -282
- package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
- package/src/admin/transfer-ownership-dialog.tsx +0 -162
- package/src/admin/user-roster-labels.ts +0 -17
- package/src/admin/user-roster.tsx +0 -559
- package/src/auth/accept-invitation-labels.ts +0 -21
- package/src/auth/accept-invitation.tsx +0 -210
- package/src/auth/auth-errors-labels.ts +0 -23
- package/src/auth/auth-errors.ts +0 -370
- package/src/auth/complete-name-dialog-labels.ts +0 -16
- package/src/auth/complete-name-dialog.tsx +0 -253
- package/src/auth/email-tag.ts +0 -75
- package/src/auth/forgot-password-form-labels.ts +0 -20
- package/src/auth/forgot-password-form.tsx +0 -221
- package/src/auth/form-rules.ts +0 -40
- package/src/auth/new-password-fields.tsx +0 -85
- package/src/auth/not-found-page-labels.ts +0 -12
- package/src/auth/not-found-page.tsx +0 -95
- package/src/auth/register-form-labels.ts +0 -31
- package/src/auth/register-form.tsx +0 -456
- package/src/auth/reset-password-form-labels.ts +0 -27
- package/src/auth/reset-password-form.tsx +0 -341
- package/src/auth/sign-in-form-labels.ts +0 -44
- package/src/auth/sign-in-form.tsx +0 -858
- package/src/auth/status-parts.tsx +0 -132
- package/src/auth/verify-email-labels.ts +0 -26
- package/src/auth/verify-email.tsx +0 -445
- package/src/billing/billing-banner.tsx +0 -258
- package/src/billing/billing-labels.ts +0 -381
- package/src/billing/billing-lock.ts +0 -108
- package/src/billing/billing-overview.ts +0 -157
- package/src/billing/billing-standing.ts +0 -80
- package/src/billing/cancel-request.ts +0 -97
- package/src/billing/cancel-subscription.tsx +0 -98
- package/src/billing/checkout-processing.ts +0 -394
- package/src/billing/checkout-return.ts +0 -90
- package/src/billing/pay-page.ts +0 -330
- package/src/billing/plan-card.tsx +0 -562
- package/src/billing/plan-change-labels.ts +0 -66
- package/src/billing/plan-limit-notice.tsx +0 -148
- package/src/billing/plan-limit-toast.tsx +0 -90
- package/src/billing/plan-limit.ts +0 -52
- package/src/billing/plan-price.ts +0 -98
- package/src/billing/plan-switch.tsx +0 -216
- package/src/billing/provider-page.ts +0 -84
- package/src/billing/subscription-actions.tsx +0 -247
- package/src/billing/subscription-status-chip.tsx +0 -95
- package/src/chart.ts +0 -103
- package/src/components/account-chips-labels.ts +0 -16
- package/src/components/account-chips.tsx +0 -474
- package/src/components/alert-banner.tsx +0 -670
- package/src/components/amount-input.tsx +0 -815
- package/src/components/appearance-labels.ts +0 -63
- package/src/components/authed-image-labels.ts +0 -11
- package/src/components/authed-image.tsx +0 -209
- package/src/components/autocomplete.tsx +0 -645
- package/src/components/breadcrumbs-labels.ts +0 -10
- package/src/components/breadcrumbs.tsx +0 -205
- package/src/components/bulk-action-bar-labels.ts +0 -11
- package/src/components/bulk-action-bar.tsx +0 -443
- package/src/components/button-group.tsx +0 -223
- package/src/components/button-look.ts +0 -256
- package/src/components/button.tsx +0 -339
- package/src/components/calculator.tsx +0 -341
- package/src/components/calendar-heatmap-labels.ts +0 -15
- package/src/components/calendar-heatmap.tsx +0 -594
- package/src/components/card.tsx +0 -377
- package/src/components/chart-edge.ts +0 -106
- package/src/components/chart-figure.tsx +0 -35
- package/src/components/chart-height.ts +0 -30
- package/src/components/chart-zoom-core.tsx +0 -627
- package/src/components/chart-zoom.tsx +0 -22
- package/src/components/chart.tsx +0 -554
- package/src/components/checkbox-group.tsx +0 -247
- package/src/components/checkbox.tsx +0 -346
- package/src/components/chip-input.tsx +0 -370
- package/src/components/chip-labels.ts +0 -13
- package/src/components/chip.tsx +0 -843
- package/src/components/choice-card.tsx +0 -544
- package/src/components/column-mapper-labels.ts +0 -38
- package/src/components/column-mapper.tsx +0 -655
- package/src/components/combobox-core.tsx +0 -1321
- package/src/components/combobox-suggestions.tsx +0 -131
- package/src/components/combobox.tsx +0 -716
- package/src/components/confirm-context.ts +0 -7
- package/src/components/confirm-dialog-labels.ts +0 -13
- package/src/components/confirm-dialog.tsx +0 -341
- package/src/components/copy-button-labels.ts +0 -13
- package/src/components/copy-button.tsx +0 -192
- package/src/components/country-select-labels.ts +0 -11
- package/src/components/country-select.tsx +0 -539
- package/src/components/currency-option-list.tsx +0 -180
- package/src/components/currency-select.tsx +0 -341
- package/src/components/danger-confirm-labels.ts +0 -20
- package/src/components/danger-confirm.tsx +0 -934
- package/src/components/data-table-cells.tsx +0 -118
- package/src/components/data-table-column-settings.tsx +0 -198
- package/src/components/data-table-filter-popover.tsx +0 -281
- package/src/components/data-table-filters.ts +0 -287
- package/src/components/data-table-fit.ts +0 -301
- package/src/components/data-table-groups.tsx +0 -288
- package/src/components/data-table-head.tsx +0 -280
- package/src/components/data-table-labels.ts +0 -198
- package/src/components/data-table-leading-row.tsx +0 -58
- package/src/components/data-table-mobile.tsx +0 -1117
- package/src/components/data-table-pagination.tsx +0 -202
- package/src/components/data-table-resize.ts +0 -188
- package/src/components/data-table-row-actions.tsx +0 -302
- package/src/components/data-table-row.tsx +0 -496
- package/src/components/data-table-selection.ts +0 -71
- package/src/components/data-table-shared.tsx +0 -287
- package/src/components/data-table-sort.ts +0 -163
- package/src/components/data-table-virtual.ts +0 -576
- package/src/components/data-table.tsx +0 -2598
- package/src/components/date-field.tsx +0 -684
- package/src/components/date-format-setting.tsx +0 -64
- package/src/components/date-picker.tsx +0 -404
- package/src/components/date-range-picker.tsx +0 -518
- package/src/components/description-list-labels.ts +0 -9
- package/src/components/description-list.tsx +0 -386
- package/src/components/dialog-frame-labels.ts +0 -7
- package/src/components/dialog-frame.tsx +0 -276
- package/src/components/disclosure.tsx +0 -476
- package/src/components/dropdown.tsx +0 -461
- package/src/components/empty-state.tsx +0 -136
- package/src/components/entity-combobox.tsx +0 -422
- package/src/components/error-boundary-labels.ts +0 -20
- package/src/components/error-boundary.tsx +0 -675
- package/src/components/facing-pair.ts +0 -93
- package/src/components/field-base-labels.ts +0 -20
- package/src/components/field-base.tsx +0 -656
- package/src/components/field-parts.tsx +0 -825
- package/src/components/field-strip.tsx +0 -129
- package/src/components/field.tsx +0 -206
- package/src/components/file-button.tsx +0 -295
- package/src/components/file-dropzone.tsx +0 -559
- package/src/components/floating-panel-labels.ts +0 -10
- package/src/components/floating-panel.tsx +0 -828
- package/src/components/focus-ring.ts +0 -16
- package/src/components/form-actions-labels.ts +0 -11
- package/src/components/form-actions.tsx +0 -578
- package/src/components/full-bleed-dialog.tsx +0 -294
- package/src/components/grouped-picker.tsx +0 -184
- package/src/components/help-popover.tsx +0 -117
- package/src/components/hover-menu.tsx +0 -343
- package/src/components/iban-input-labels.ts +0 -14
- package/src/components/iban-input.tsx +0 -236
- package/src/components/icon-button.tsx +0 -959
- package/src/components/icon-picker-labels.ts +0 -13
- package/src/components/icon-picker.tsx +0 -232
- package/src/components/image-grid-labels.ts +0 -11
- package/src/components/image-grid.tsx +0 -202
- package/src/components/inline-confirm.tsx +0 -116
- package/src/components/inline-edit-field-labels.ts +0 -11
- package/src/components/inline-edit-field.tsx +0 -552
- package/src/components/inline-entity-combobox.tsx +0 -849
- package/src/components/input.tsx +0 -394
- package/src/components/jump-calendar.tsx +0 -265
- package/src/components/language-select.tsx +0 -93
- package/src/components/legal-labels.ts +0 -105
- package/src/components/legal-page.tsx +0 -711
- package/src/components/legal.tsx +0 -668
- package/src/components/lightbox-labels.ts +0 -18
- package/src/components/lightbox.tsx +0 -399
- package/src/components/line-items-labels.ts +0 -14
- package/src/components/line-items.tsx +0 -602
- package/src/components/list-labels.ts +0 -12
- package/src/components/list.tsx +0 -870
- package/src/components/loading-state.tsx +0 -78
- package/src/components/measured-grid-labels.ts +0 -25
- package/src/components/measured-grid.tsx +0 -837
- package/src/components/menu-item.tsx +0 -241
- package/src/components/mini-calendar-labels.ts +0 -17
- package/src/components/mini-calendar.tsx +0 -789
- package/src/components/modal.tsx +0 -505
- package/src/components/money-draft.ts +0 -83
- package/src/components/money-field.tsx +0 -76
- package/src/components/month-grid.tsx +0 -229
- package/src/components/month-picker-labels.ts +0 -22
- package/src/components/month-picker.tsx +0 -1087
- package/src/components/multi-entity-combobox.tsx +0 -369
- package/src/components/multi-select.tsx +0 -376
- package/src/components/nav-pills.tsx +0 -198
- package/src/components/number-field.tsx +0 -327
- package/src/components/number-input.tsx +0 -558
- package/src/components/numeric-field.ts +0 -169
- package/src/components/numpad-sheet.tsx +0 -285
- package/src/components/one-time-code-input.tsx +0 -270
- package/src/components/page-contents-labels.ts +0 -9
- package/src/components/page-contents.tsx +0 -282
- package/src/components/page-header-context.ts +0 -8
- package/src/components/page-header.tsx +0 -298
- package/src/components/password-strength-labels.ts +0 -22
- package/src/components/password-strength.tsx +0 -241
- package/src/components/phone-input-labels.ts +0 -10
- package/src/components/phone-input.tsx +0 -282
- package/src/components/picker-sheet.tsx +0 -330
- package/src/components/pie-chart-labels.ts +0 -40
- package/src/components/pie-chart.tsx +0 -480
- package/src/components/popover-labels.ts +0 -14
- package/src/components/popover.tsx +0 -268
- package/src/components/progress-bar-labels.ts +0 -10
- package/src/components/progress-bar.tsx +0 -479
- package/src/components/qr-code.tsx +0 -76
- package/src/components/reauth-dialog-labels.ts +0 -13
- package/src/components/reauth-dialog.tsx +0 -265
- package/src/components/row-actions-labels.ts +0 -12
- package/src/components/row-actions.tsx +0 -795
- package/src/components/scroll-area.tsx +0 -123
- package/src/components/search-field.tsx +0 -192
- package/src/components/select.tsx +0 -294
- package/src/components/separator.tsx +0 -33
- package/src/components/series-chart-axes.ts +0 -342
- package/src/components/series-chart-budget.ts +0 -150
- package/src/components/series-chart-labels.ts +0 -33
- package/src/components/series-chart-thin.ts +0 -180
- package/src/components/series-chart-ticks.ts +0 -194
- package/src/components/series-chart.tsx +0 -1906
- package/src/components/server-wake-labels.ts +0 -11
- package/src/components/server-wake.tsx +0 -129
- package/src/components/settings-fields.tsx +0 -237
- package/src/components/share-card-labels.ts +0 -30
- package/src/components/share-card.tsx +0 -675
- package/src/components/signature-pad-labels.ts +0 -24
- package/src/components/signature-pad.tsx +0 -747
- package/src/components/signed-amount-labels.ts +0 -10
- package/src/components/signed-amount.tsx +0 -336
- package/src/components/skeleton.tsx +0 -80
- package/src/components/slider.tsx +0 -338
- package/src/components/sparkline-labels.ts +0 -14
- package/src/components/sparkline.tsx +0 -322
- package/src/components/spinner.tsx +0 -81
- package/src/components/stat-tile-labels.ts +0 -15
- package/src/components/stat-tile.tsx +0 -725
- package/src/components/status-dot.tsx +0 -170
- package/src/components/swatch-picker-labels.ts +0 -10
- package/src/components/swatch-picker.tsx +0 -283
- package/src/components/swipeable-row.tsx +0 -386
- package/src/components/switch.tsx +0 -273
- package/src/components/table-skeleton.tsx +0 -136
- package/src/components/table.tsx +0 -647
- package/src/components/tabs-labels.ts +0 -13
- package/src/components/tabs.tsx +0 -693
- package/src/components/text-link.tsx +0 -359
- package/src/components/text.tsx +0 -150
- package/src/components/textarea.tsx +0 -330
- package/src/components/tile-radio.tsx +0 -332
- package/src/components/time-input.tsx +0 -121
- package/src/components/toast-labels.ts +0 -12
- package/src/components/toast.tsx +0 -519
- package/src/components/toggle-group.tsx +0 -752
- package/src/components/toggle-legend.tsx +0 -394
- package/src/components/tooltip.tsx +0 -1149
- package/src/components/translation-review-editor.tsx +0 -290
- package/src/components/translation-review-labels.ts +0 -181
- package/src/components/translation-review-parts.tsx +0 -272
- package/src/components/translation-review.tsx +0 -1240
- package/src/components/tree-view.tsx +0 -589
- package/src/components/treemap-labels.ts +0 -47
- package/src/components/treemap-layout.ts +0 -210
- package/src/components/treemap.tsx +0 -1184
- package/src/components/trigger-aria.ts +0 -44
- package/src/components/use-anchor-dir.ts +0 -20
- package/src/components/use-mobile-reveal.ts +0 -62
- package/src/components/use-table-state.ts +0 -753
- package/src/components/user-avatar.tsx +0 -116
- package/src/components/wizard-stepper.tsx +0 -42
- package/src/components/write-lock-labels.ts +0 -10
- package/src/components/write-lock.tsx +0 -253
- package/src/currencies.ts +0 -70
- package/src/data-table.ts +0 -42
- package/src/demo/demo-banner.tsx +0 -153
- package/src/demo/demo-countdown.ts +0 -136
- package/src/demo/demo-ended.tsx +0 -90
- package/src/demo/demo-labels.ts +0 -78
- package/src/demo/demo-session.ts +0 -32
- package/src/demo/demo-start.tsx +0 -156
- package/src/eslint/index.ts +0 -161
- package/src/eslint/role-utilities.generated.ts +0 -146
- package/src/eslint/role-utilities.ts +0 -233
- package/src/feedback/feedback-attachment-download.tsx +0 -77
- package/src/feedback/feedback-attachment-labels.ts +0 -37
- package/src/feedback/feedback-attachment.tsx +0 -1239
- package/src/feedback/feedback-capture.ts +0 -287
- package/src/feedback/feedback-comment-thread.tsx +0 -353
- package/src/feedback/feedback-context-labels.ts +0 -10
- package/src/feedback/feedback-context.tsx +0 -213
- package/src/feedback/feedback-crash.ts +0 -547
- package/src/feedback/feedback-dialog-labels.ts +0 -29
- package/src/feedback/feedback-dialog.tsx +0 -440
- package/src/feedback/feedback-entry-file.tsx +0 -150
- package/src/feedback/feedback-inbox.tsx +0 -552
- package/src/feedback/feedback-labels.ts +0 -144
- package/src/feedback/feedback-menu-labels.ts +0 -12
- package/src/feedback/feedback-menu.tsx +0 -177
- package/src/feedback/feedback-mutation.ts +0 -20
- package/src/feedback/feedback-page-link.ts +0 -90
- package/src/feedback/feedback-public-labels.ts +0 -81
- package/src/feedback/feedback-public-list.tsx +0 -524
- package/src/feedback/feedback-public-switch.tsx +0 -102
- package/src/feedback/feedback-public-types.ts +0 -101
- package/src/feedback/feedback-record.ts +0 -423
- package/src/feedback/feedback-replies-context.ts +0 -9
- package/src/feedback/feedback-replies.tsx +0 -343
- package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
- package/src/feedback/feedback-row-detail-labels.ts +0 -30
- package/src/feedback/feedback-row-detail.tsx +0 -858
- package/src/feedback/feedback-status-undo.ts +0 -109
- package/src/feedback/feedback-submit-dialog.tsx +0 -118
- package/src/feedback/feedback-submit.tsx +0 -237
- package/src/feedback/feedback-swipe.tsx +0 -192
- package/src/feedback/feedback-table-labels.ts +0 -27
- package/src/feedback/feedback-table.tsx +0 -912
- package/src/feedback/feedback-thread-labels.ts +0 -22
- package/src/feedback/feedback-thread-parts-labels.ts +0 -41
- package/src/feedback/feedback-thread.tsx +0 -905
- package/src/feedback/feedback-vocabulary.ts +0 -263
- package/src/feedback/feedback-vote-button.tsx +0 -124
- package/src/feedback.ts +0 -116
- package/src/hooks/use-anchored-panel.ts +0 -396
- package/src/hooks/use-anchored-rect.ts +0 -77
- package/src/hooks/use-announce.ts +0 -85
- package/src/hooks/use-authed-src.ts +0 -167
- package/src/hooks/use-body-scroll-lock.ts +0 -119
- package/src/hooks/use-breakpoint.ts +0 -133
- package/src/hooks/use-close-transition.ts +0 -107
- package/src/hooks/use-copy-to-clipboard.ts +0 -122
- package/src/hooks/use-debounce.ts +0 -114
- package/src/hooks/use-dismiss.ts +0 -371
- package/src/hooks/use-document-title.ts +0 -33
- package/src/hooks/use-file-drop.ts +0 -121
- package/src/hooks/use-file-picker-labels.ts +0 -28
- package/src/hooks/use-file-picker.tsx +0 -359
- package/src/hooks/use-focus-trap.ts +0 -199
- package/src/hooks/use-hotkey.ts +0 -189
- package/src/hooks/use-large-text.ts +0 -41
- package/src/hooks/use-long-press.ts +0 -140
- package/src/hooks/use-media-query.ts +0 -39
- package/src/hooks/use-mounted.ts +0 -39
- package/src/hooks/use-noindex.ts +0 -37
- package/src/hooks/use-overlay-history.ts +0 -714
- package/src/hooks/use-row-swipe.ts +0 -207
- package/src/hooks/use-search-param-state.ts +0 -434
- package/src/hooks/use-windowed-rows.ts +0 -225
- package/src/i18n/defaults.ts +0 -267
- package/src/i18n/german.ts +0 -2057
- package/src/i18n/kit-labels.tsx +0 -1003
- package/src/i18n/languages.ts +0 -349
- package/src/i18n/locales/de-CH.ts +0 -24
- package/src/i18n/locales/en.ts +0 -267
- package/src/i18n/locales/es.ts +0 -1997
- package/src/i18n/locales/fr.ts +0 -2035
- package/src/i18n/locales/hu.ts +0 -2002
- package/src/i18n/locales/it.ts +0 -2012
- package/src/i18n/locales/zh.ts +0 -1910
- package/src/i18n/plurals.ts +0 -86
- package/src/i18n/review.ts +0 -475
- package/src/i18n/shared-labels.ts +0 -138
- package/src/i18n/swiss.ts +0 -29
- package/src/index.ts +0 -1179
- package/src/landing/access.ts +0 -93
- package/src/landing/landing-actions.tsx +0 -97
- package/src/landing/landing-labels.ts +0 -59
- package/src/landing/landing-sections.tsx +0 -341
- package/src/landing/page-seo.ts +0 -261
- package/src/landing/public-header.tsx +0 -121
- package/src/landing/public-page.tsx +0 -106
- package/src/landing/routing.tsx +0 -225
- package/src/lib/broadcast.ts +0 -52
- package/src/lib/calc.ts +0 -323
- package/src/lib/chip-edge-snap.ts +0 -212
- package/src/lib/clipboard-minus.ts +0 -107
- package/src/lib/clipping.ts +0 -34
- package/src/lib/cn.ts +0 -28
- package/src/lib/column-mapping.ts +0 -234
- package/src/lib/countries.ts +0 -91
- package/src/lib/data-attributes.ts +0 -14
- package/src/lib/date-format.ts +0 -227
- package/src/lib/dates.ts +0 -266
- package/src/lib/day-format.ts +0 -96
- package/src/lib/decimal-marks.ts +0 -108
- package/src/lib/describe-failure.ts +0 -18
- package/src/lib/device-settings.ts +0 -147
- package/src/lib/direction.ts +0 -74
- package/src/lib/document-navigation.ts +0 -17
- package/src/lib/document-title.ts +0 -41
- package/src/lib/download.ts +0 -172
- package/src/lib/float-clearance.ts +0 -70
- package/src/lib/floating-stack.ts +0 -70
- package/src/lib/focus-left.ts +0 -156
- package/src/lib/format.ts +0 -522
- package/src/lib/http-status.ts +0 -38
- package/src/lib/iban.ts +0 -251
- package/src/lib/ime.ts +0 -24
- package/src/lib/intl-cache.ts +0 -25
- package/src/lib/iso-day.ts +0 -13
- package/src/lib/lazy-with-fallback.ts +0 -82
- package/src/lib/logger.ts +0 -69
- package/src/lib/minus.ts +0 -50
- package/src/lib/path-pattern.ts +0 -55
- package/src/lib/person-name.ts +0 -155
- package/src/lib/phone.ts +0 -208
- package/src/lib/plain-text.ts +0 -22
- package/src/lib/presence.ts +0 -18
- package/src/lib/qr-encode.ts +0 -444
- package/src/lib/quiet-focus.ts +0 -32
- package/src/lib/reduced-motion.ts +0 -22
- package/src/lib/refs.ts +0 -22
- package/src/lib/round.ts +0 -43
- package/src/lib/safe-storage.ts +0 -119
- package/src/lib/server-wake.ts +0 -315
- package/src/lib/session-refresh.ts +0 -472
- package/src/lib/share-format.ts +0 -18
- package/src/lib/status-tone.ts +0 -43
- package/src/lib/strip-fade.ts +0 -287
- package/src/lib/table-text.ts +0 -554
- package/src/lib/text-case.ts +0 -19
- package/src/lib/text-match.ts +0 -169
- package/src/lib/thenable.ts +0 -19
- package/src/lib/translation-review.ts +0 -717
- package/src/notifications/device-zone-report.ts +0 -67
- package/src/notifications/feedback-notification-types-labels.ts +0 -33
- package/src/notifications/feedback-notification-types.ts +0 -71
- package/src/notifications/notification-bell.tsx +0 -119
- package/src/notifications/notification-inbox.tsx +0 -309
- package/src/notifications/notification-parts.tsx +0 -81
- package/src/notifications/notification-types-setting.tsx +0 -406
- package/src/notifications/notification-types.ts +0 -89
- package/src/notifications/notifications-labels.ts +0 -287
- package/src/notifications/push-device-setting.tsx +0 -230
- package/src/notifications/push-devices-setting.tsx +0 -374
- package/src/notifications/push-worker.ts +0 -168
- package/src/notifications/quiet-hours-setting.tsx +0 -238
- package/src/notifications/unsubscribe-panel.tsx +0 -200
- package/src/notifications/use-push-device.ts +0 -448
- package/src/offline/local-first.ts +0 -100
- package/src/offline/pwa-update-prompt-labels.ts +0 -9
- package/src/offline/pwa-update-prompt.tsx +0 -78
- package/src/offline/query-persistence.ts +0 -391
- package/src/offline/service-worker-updates.ts +0 -81
- package/src/offline/shared-computer-labels.ts +0 -8
- package/src/offline/shared-computer.tsx +0 -87
- package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
- package/src/offline/sign-out-pending-dialog.tsx +0 -135
- package/src/offline.ts +0 -37
- package/src/push-worker.ts +0 -25
- package/src/rhf/fields.tsx +0 -2187
- package/src/rhf/form-core.tsx +0 -324
- package/src/rhf/form.tsx +0 -12
- package/src/rhf/line-items.tsx +0 -135
- package/src/rhf/use-rhf-wizard-step.ts +0 -104
- package/src/rhf.ts +0 -21
- package/src/search/command-palette-labels.ts +0 -16
- package/src/search/command-palette.tsx +0 -624
- package/src/search/global-search-labels.ts +0 -13
- package/src/search/global-search.tsx +0 -660
- package/src/search/search-index.ts +0 -283
- package/src/search/use-command-key.ts +0 -23
- package/src/search.ts +0 -14
- package/src/settings/device-reset-setting-labels.ts +0 -20
- package/src/settings/device-reset-setting.tsx +0 -116
- package/src/settings/settings-card-classes.ts +0 -11
- package/src/settings/settings-catalogue.ts +0 -150
- package/src/settings/settings-context.ts +0 -33
- package/src/settings/settings-heading.tsx +0 -67
- package/src/settings/settings-labels.ts +0 -87
- package/src/settings/settings-layout.tsx +0 -471
- package/src/settings/settings-section.tsx +0 -108
- package/src/settings/use-account-appearance.ts +0 -148
- package/src/settings/use-account-language.ts +0 -146
- package/src/settings/use-settings-focus.ts +0 -128
- package/src/settings/use-settings-route.ts +0 -258
- package/src/shell/account-menu-language.tsx +0 -130
- package/src/shell/account-menu-text-size.tsx +0 -80
- package/src/shell/admin-menu.tsx +0 -183
- package/src/shell/app-shell-labels.ts +0 -11
- package/src/shell/app-shell.tsx +0 -1560
- package/src/shell/auth-layout.tsx +0 -186
- package/src/shell/company-switcher-labels.ts +0 -13
- package/src/shell/company-switcher.tsx +0 -136
- package/src/shell/dev-menu.tsx +0 -257
- package/src/shell/fill-height.tsx +0 -45
- package/src/shell/language-code.ts +0 -5
- package/src/shell/option-switcher-menu.tsx +0 -84
- package/src/shell/sync-status-indicator.tsx +0 -303
- package/src/shell/top-bar-actions.tsx +0 -87
- package/src/shell/top-bar-brand.tsx +0 -78
- package/src/shell/top-bar-parts-labels.ts +0 -148
- package/src/shell/top-bar.tsx +0 -40
- package/src/shell/topbar-action-menu.tsx +0 -567
- package/src/shell/topbar-controls.tsx +0 -269
- package/src/shell/tour-menu.tsx +0 -187
- package/src/shell.ts +0 -108
- package/src/sync/db.ts +0 -129
- package/src/sync/engine.ts +0 -1011
- package/src/sync/hooks.ts +0 -98
- package/src/sync/testing.ts +0 -73
- package/src/sync/wire.ts +0 -61
- package/src/sync.ts +0 -37
- package/src/table-text.ts +0 -8
- package/src/testing/css-variables.generated.ts +0 -112
- package/src/testing/css-variables.ts +0 -189
- package/src/testing/index.ts +0 -29
- package/src/testing/react.ts +0 -58
- package/src/testing/tour.ts +0 -41
- package/src/testing/viewport.ts +0 -357
- package/src/theme/chart-palette.ts +0 -232
- package/src/theme/color.ts +0 -272
- package/src/theme/contrast-tokens.ts +0 -268
- package/src/theme/contrast.ts +0 -250
- package/src/theme/palette-derive.ts +0 -825
- package/src/theme/palette-presets.ts +0 -544
- package/src/theme/palette-store.ts +0 -108
- package/src/theme/text-size.ts +0 -289
- package/src/theme/theme-store.ts +0 -157
- package/src/tour/tour-catalogue-labels.ts +0 -75
- package/src/tour/tour-catalogue.tsx +0 -335
- package/src/tour/tour-dock.ts +0 -134
- package/src/tour/tour-engine.ts +0 -37
- package/src/tour/tour-gates.ts +0 -218
- package/src/tour/tour-guide-button.tsx +0 -64
- package/src/tour/tour-guide-labels.ts +0 -15
- package/src/tour/tour-info.ts +0 -144
- package/src/tour/tour-labels.ts +0 -23
- package/src/tour/tour-overlay.tsx +0 -1170
- package/src/tour/tour.tsx +0 -750
- package/src/tour/use-tour-progress.ts +0 -671
- package/src/tour/use-tour-url-sync.ts +0 -266
- package/src/tour.ts +0 -21
- package/src/vite-env.d.ts +0 -23
- package/src/wizard/stepper-nav.tsx +0 -360
- package/src/wizard/types.ts +0 -275
- package/src/wizard/use-wizard.ts +0 -560
- package/src/wizard/validation.ts +0 -39
- package/src/wizard/wizard-context.tsx +0 -74
- package/src/wizard/wizard-labels.ts +0 -29
- package/src/wizard/wizard-step.tsx +0 -76
- package/src/wizard/wizard-summary.tsx +0 -84
- package/src/wizard.ts +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-submit.tsx"],"sourcesContent":["import { Suspense, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { toast } from \"../components/toast\";\nimport { lazyWithFallback } from \"../lib/lazy-with-fallback\";\n// Types only, gone from the built module: the dialog's modules load with the dialog.\nimport type { FeedbackSubmitter } from \"./feedback-context\";\nimport type { FeedbackSubmitDialogProps } from \"./feedback-submit-dialog\";\nimport type { FeedbackMultipleSubmission } from \"./feedback-dialog\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport { FEEDBACK_PICKABLE_CATEGORIES, type FeedbackContext, type FeedbackEnvironment } from \"./feedback-record\";\nimport type { FeedbackCategory } from \"./feedback-vocabulary\";\n\n/**\n * The dialog, loaded when it is first opened rather than with the top bar that mounts the\n * hook (0.34, MOD-1 of the refactor audit 2026-10; see feedback-submit-dialog.tsx). A\n * LITERAL relative `import()`, as the catalogue loaders are (i18n/languages.ts): the\n * library build keeps the module its own (`bundle: false`), and a path an app's bundler\n * can read is a chunk it can split off.\n *\n * A fetch that fails — a tab left open across a deploy, offline — closes the dialog\n * rather than throwing (0.52 code review, tour.tsx): the hook lives in the top bar, above\n * the routes' error boundaries, so the rejected import took the whole shell down. The\n * next open fetches again.\n */\nconst LazySubmitDialog = lazyWithFallback(\n () => import(\"./feedback-submit-dialog\").then((module) => module.FeedbackSubmitDialog),\n SubmitDialogUnavailable,\n);\n\nfunction SubmitDialogUnavailable({ onClose }: FeedbackSubmitDialogProps) {\n useLayoutEffect(onClose, [onClose]);\n return null;\n}\n\n/**\n * `FeedbackCreate` — the body of `POST /feedback` (keksdose `schemas/feedback.py:25`, §3.4\n * of docs/feedback-harmonization.md), as {@link useFeedbackSubmit} hands it to the app's\n * `create`.\n */\nexport interface FeedbackCreatePayload {\n /** Trimmed; never blank (the dialog will not send a subject of spaces). */\n title: string;\n /** `\"\"` when the reporter wrote nothing or only whitespace — the server stores `\"\"`. */\n body: string;\n /** Never `CRASH` — the server answers that with a 422 (§3.4). */\n category: FeedbackCategory;\n /** §3.2, built by `feedbackContext` from what the context box showed. */\n context: FeedbackContext;\n /** The captured screenshot's upload URL, or `null`. */\n screenshot_url: string | null;\n /** The picked and pasted files' upload URLs, in the order they were added — `[]` when\n * none, as keksdose sends it. At most 5. */\n attachment_urls: string[];\n}\n\nexport interface UseFeedbackSubmitOptions<R = unknown> {\n /**\n * `POST /feedback/attachments` for ONE file → its URL (§3.3) — the app's authed client.\n * Every file goes up BEFORE the report is created, so a row never names a file that did\n * not arrive (keksdose `use-feedback-dialog.tsx:89`); if any upload rejects, nothing is\n * created and the failure toast says so.\n */\n upload: (file: File) => Promise<string>;\n /** `POST /feedback` with the payload. Whatever it resolves with reaches `onSubmitted`. */\n create: (payload: FeedbackCreatePayload) => Promise<R>;\n /** Who is filing — shown in the context box and sent in `context.user_*`. */\n user?: FeedbackSubmitter | null;\n /** `context.environment` — the app's own reading of which copy it is (keksdose\n * `deployEnvironment()`). */\n environment?: FeedbackEnvironment | (string & {});\n /** `context.version` — the app build (`__APP_VERSION__`). */\n version?: string;\n /** The report was filed: after the \"Thanks for the feedback!\" toast and the close. The\n * place to invalidate the app's feedback queries. */\n onSubmitted?: (result: R) => void;\n /**\n * The failure toast's text from what `upload` or `create` rejected with — the app's own\n * `extractApiErrorMessage(err, fallback)` (keksdose) / `apiErrorMessage` (Kurvenschmiede),\n * so the server's `detail` wins (§3.7: a 413, a 415, keksdose's demo-session 403). Without\n * it every failure says `feedbackToast.submitFailed`.\n */\n errorMessage?: (error: unknown, fallback: string) => string;\n /**\n * The \"Capture screenshot\" button's capture. Default {@link captureAppScreenshot} of\n * `#root`; another root is `() => captureAppScreenshot({ rootId: \"app\" })`. `false`\n * hides the button — for an app that does not install the optional peer\n * `modern-screenshot`. A rejection becomes the `captureFailed` toast.\n */\n capture?: false | (() => Promise<File | null>);\n /**\n * The app's reports are public (0.38, docs/feedback-harmonization.md §9.2 decision 6):\n * `true` shows **\"Other users will see the title and text (never your screenshots or\n * files)\"** (`feedbackDialog.publicNotice`) under the body. keksdose and Kurvenschmiede\n * pass it — for a viewer who passes the app's `public_viewer` — and kastlan, whose\n * public parts are off, does not. Default `false`.\n */\n publicNotice?: boolean;\n}\n\nexport interface FeedbackSubmit {\n /**\n * Open the dialog on `category` — a `FeedbackMenu` row's (§4.2: \"default category when\n * opened from a row: that row's\"). Without one, or with `CRASH` (never pickable), it\n * opens on `OTHER`, the server's own default (§3.4). Each open starts a fresh form,\n * reads `location.href` for the context box and ticks \"Attach current page URL\" again.\n */\n open: (category?: FeedbackCategory) => void;\n /** Close it, as Cancel does. */\n close: () => void;\n /**\n * The dialog — render it once, anywhere (it portals). `null` while closed. Since 0.34\n * its module loads when it is first opened (MOD-1), so it appears a moment after\n * `open`: a test finds it with `await screen.findBy…`, not `getBy…`.\n */\n dialog: ReactNode;\n}\n\n/**\n * The feedback submit dialog, wired (0.27.0, docs/feedback-harmonization.md §4.2): the\n * one call that replaces keksdose's `use-feedback-dialog.tsx`, kastlan's `FeedbackButton`\n * and Kurvenschmiede's `use-feedback-dialog.tsx`.\n *\n * ```tsx\n * const feedback = useFeedbackSubmit({\n * upload: (file) => feedbackApi.uploadAttachment(file),\n * create: (payload) => feedbackApi.create(payload),\n * user, environment: deployEnvironment(), version: __APP_VERSION__,\n * errorMessage: extractApiErrorMessage,\n * onSubmitted: () => qc.invalidateQueries({ queryKey: queryKeys.feedback.all }),\n * });\n * <FeedbackMenu onFile={feedback.open} isAdmin={user?.role === \"ADMIN\"} />\n * {feedback.dialog}\n * ```\n *\n * What it settles, so no app restates it:\n *\n * - `FeedbackDialog` in `attachments=\"multiple\"` with `requireBody={false}` (§7.4): a\n * subject alone is a report. One captured screenshot in its own slot plus up to 5\n * picked or pasted files, `FEEDBACK_ATTACHMENT_ACCEPT` (images, PDF, text), 10 MB each.\n * - The pickable categories — Bug, Idea, Question, Other, in the kit's words. `CRASH` is\n * filed by the error boundary and is never offered (keksdose feedback #160).\n * - The context box, built from the same values as the `context` it sends\n * ({@link FeedbackContextBox} / `feedbackContext`), the URL read when the dialog OPENS.\n * - Upload every file, then create (keksdose `use-feedback-dialog.tsx:89`): the\n * screenshot to `screenshot_url`, the rest to `attachment_urls`, all in parallel.\n * - The toasts, from `feedbackToast`: filed, failed (the server's `detail` through\n * `errorMessage`), a refused file's type, size or count, a failed capture.\n *\n * What stays the app's: the API client, the user, the environment and version, and what\n * to refresh afterwards. No TanStack in here — `create` is a plain promise, so the hook\n * runs in any app; the app's mutation, if it wants one, lives inside `create`.\n *\n * A failure keeps the dialog open with the draft, as keksdose's did. A dialog closed and\n * opened again while a send is still running is NOT closed by that send's success — the\n * new draft is somebody's next report.\n */\nexport function useFeedbackSubmit<R = unknown>(options: UseFeedbackSubmitOptions<R>): FeedbackSubmit {\n const { upload, create, user, environment, version, onSubmitted, errorMessage, capture, publicNotice } = options;\n const toastText = useFeedbackToastLabels();\n const [isOpen, setIsOpen] = useState(false);\n const [category, setCategory] = useState<FeedbackCategory>(\"OTHER\");\n const [url, setUrl] = useState(\"\");\n const [attachUrl, setAttachUrl] = useState(true);\n const [submitting, setSubmitting] = useState(false);\n // Which opening a send belongs to — see \"closed and opened again\" above.\n const opening = useRef(0);\n const inFlight = useRef(false);\n\n const open = (next?: FeedbackCategory) => {\n opening.current += 1;\n setCategory(next && FEEDBACK_PICKABLE_CATEGORIES.includes(next) ? next : \"OTHER\");\n setUrl(typeof window === \"undefined\" ? \"\" : window.location.href);\n // Fresh every time (Kurvenschmiede use-feedback-dialog.tsx:80): a reporter who left the\n // page out of one idea is not saying the bug report after it is unrelated too.\n setAttachUrl(true);\n setIsOpen(true);\n };\n const close = () => setIsOpen(false);\n\n // `context` comes from the dialog, built off the values its context box shows.\n const submit = async (data: FeedbackMultipleSubmission<FeedbackCategory>, context: FeedbackContext) => {\n if (inFlight.current) return;\n inFlight.current = true;\n setSubmitting(true);\n const from = opening.current;\n let result: R;\n try {\n const [screenshot_url, attachment_urls] = await Promise.all([\n data.screenshot ? upload(data.screenshot) : Promise.resolve(null),\n Promise.all(data.attachments.map((file) => upload(file))),\n ]);\n result = await create({\n title: data.title.trim(),\n body: data.body.trim() ? data.body : \"\",\n category: data.category,\n context,\n screenshot_url,\n attachment_urls,\n });\n } catch (error) {\n toast.error(errorMessage ? errorMessage(error, toastText.submitFailed) : toastText.submitFailed);\n return;\n } finally {\n inFlight.current = false;\n setSubmitting(false);\n }\n toast.success(toastText.submitted);\n if (opening.current === from) setIsOpen(false);\n onSubmitted?.(result);\n };\n\n // Nothing is drawn while the dialog's module is on its way — a few milliseconds, once per\n // page load; a spinner would only flash. A load that fails (a deploy replaced the chunk,\n // offline) closes the dialog (`SubmitDialogUnavailable`): the hook sits in the top bar,\n // above the routes' error boundaries, where a thrown chunk error took the shell down.\n const dialog = isOpen ? (\n <Suspense fallback={null}>\n <LazySubmitDialog\n onClose={close}\n category={category}\n onCategoryChange={setCategory}\n submitting={submitting}\n onSubmit={submit}\n user={user}\n url={url}\n attachUrl={attachUrl}\n onAttachUrlChange={setAttachUrl}\n environment={environment}\n version={version}\n capture={capture}\n publicNotice={publicNotice}\n />\n </Suspense>\n ) : null;\n\n return { open, close, dialog };\n}\n"],"mappings":";AAyNM;AAzNN,SAAS,UAAU,iBAAiB,QAAQ,gBAAgB;AAE5D,SAAS,aAAa;AACtB,SAAS,wBAAwB;AAKjC,SAAS,8BAA8B;AACvC,SAAS,oCAAoF;AAe7F,MAAM,mBAAmB;AAAA,EACvB,MAAM,OAAO,0BAA0B,EAAE,KAAK,CAAC,WAAW,OAAO,oBAAoB;AAAA,EACrF;AACF;AAEA,SAAS,wBAAwB,EAAE,QAAQ,GAA8B;AACvE,kBAAgB,SAAS,CAAC,OAAO,CAAC;AAClC,SAAO;AACT;AA4HO,SAAS,kBAA+B,SAAsD;AACnG,QAAM,EAAE,QAAQ,QAAQ,MAAM,aAAa,SAAS,aAAa,cAAc,SAAS,aAAa,IAAI;AACzG,QAAM,YAAY,uBAAuB;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,UAAU,WAAW,IAAI,SAA2B,OAAO;AAClE,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,EAAE;AACjC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAC/C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAElD,QAAM,UAAU,OAAO,CAAC;AACxB,QAAM,WAAW,OAAO,KAAK;AAE7B,QAAM,OAAO,CAAC,SAA4B;AACxC,YAAQ,WAAW;AACnB,gBAAY,QAAQ,6BAA6B,SAAS,IAAI,IAAI,OAAO,OAAO;AAChF,WAAO,OAAO,WAAW,cAAc,KAAK,OAAO,SAAS,IAAI;AAGhE,iBAAa,IAAI;AACjB,cAAU,IAAI;AAAA,EAChB;AACA,QAAM,QAAQ,MAAM,UAAU,KAAK;AAGnC,QAAM,SAAS,OAAO,MAAoD,YAA6B;AACrG,QAAI,SAAS,QAAS;AACtB,aAAS,UAAU;AACnB,kBAAc,IAAI;AAClB,UAAM,OAAO,QAAQ;AACrB,QAAI;AACJ,QAAI;AACF,YAAM,CAAC,gBAAgB,eAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC1D,KAAK,aAAa,OAAO,KAAK,UAAU,IAAI,QAAQ,QAAQ,IAAI;AAAA,QAChE,QAAQ,IAAI,KAAK,YAAY,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,CAAC;AAAA,MAC1D,CAAC;AACD,eAAS,MAAM,OAAO;AAAA,QACpB,OAAO,KAAK,MAAM,KAAK;AAAA,QACvB,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,OAAO;AAAA,QACrC,UAAU,KAAK;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,SAAS,OAAO;AACd,YAAM,MAAM,eAAe,aAAa,OAAO,UAAU,YAAY,IAAI,UAAU,YAAY;AAC/F;AAAA,IACF,UAAE;AACA,eAAS,UAAU;AACnB,oBAAc,KAAK;AAAA,IACrB;AACA,UAAM,QAAQ,UAAU,SAAS;AACjC,QAAI,QAAQ,YAAY,KAAM,WAAU,KAAK;AAC7C,kBAAc,MAAM;AAAA,EACtB;AAMA,QAAM,SAAS,SACb,oBAAC,YAAS,UAAU,MAClB;AAAA,IAAC;AAAA;AAAA,MACC,SAAS;AAAA,MACT;AAAA,MACA,kBAAkB;AAAA,MAClB;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF,GACF,IACE;AAEJ,SAAO,EAAE,MAAM,OAAO,OAAO;AAC/B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-submit.tsx"],"sourcesContent":["import { Suspense, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { toast } from \"../components/toast\";\nimport { lazyWithFallback } from \"../lib/lazy-with-fallback\";\n// Types only, gone from the built module: the dialog's modules load with the dialog.\nimport type { FeedbackSubmitter } from \"./feedback-context\";\nimport type { FeedbackSubmitDialogProps } from \"./feedback-submit-dialog\";\nimport type { FeedbackMultipleSubmission } from \"./feedback-dialog\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport { FEEDBACK_PICKABLE_CATEGORIES, type FeedbackContext, type FeedbackEnvironment } from \"./feedback-record\";\nimport type { FeedbackCategory } from \"./feedback-vocabulary\";\n\n/**\n * The dialog, loaded when it is first opened rather than with the top bar that mounts the\n * hook (0.34, MOD-1 of the refactor audit 2026-10; see feedback-submit-dialog.tsx). A\n * LITERAL relative `import()`, as the catalogue loaders are (i18n/languages.ts): the\n * library build keeps the module its own (`bundle: false`), and a path an app's bundler\n * can read is a chunk it can split off.\n *\n * A fetch that fails — a tab left open across a deploy, offline — closes the dialog\n * rather than throwing (0.52 code review, tour.tsx): the hook lives in the top bar, above\n * the routes' error boundaries, so the rejected import took the whole shell down. The\n * next open fetches again.\n */\nconst LazySubmitDialog = lazyWithFallback(\n () => import(\"./feedback-submit-dialog\").then((module) => module.FeedbackSubmitDialog),\n SubmitDialogUnavailable,\n);\n\nfunction SubmitDialogUnavailable({ onClose }: FeedbackSubmitDialogProps) {\n useLayoutEffect(onClose, [onClose]);\n return null;\n}\n\n/**\n * `FeedbackCreate` — the body of `POST /feedback` (keksdose `schemas/feedback.py:25`, §3.4\n * of docs/feedback-harmonization.md), as {@link useFeedbackSubmit} hands it to the app's\n * `create`.\n */\nexport interface FeedbackCreatePayload {\n /** Trimmed; never blank (the dialog will not send a subject of spaces). */\n title: string;\n /** `\"\"` when the reporter wrote nothing or only whitespace — the server stores `\"\"`. */\n body: string;\n /** Never `CRASH` — the server answers that with a 422 (§3.4). */\n category: FeedbackCategory;\n /** §3.2, built by `feedbackContext` from what the context box showed. */\n context: FeedbackContext;\n /** The captured screenshot's upload URL, or `null`. */\n screenshot_url: string | null;\n /** The picked and pasted files' upload URLs, in the order they were added — `[]` when\n * none, as keksdose sends it. At most 5. */\n attachment_urls: string[];\n}\n\nexport interface UseFeedbackSubmitOptions<R = unknown> {\n /**\n * `POST /feedback/attachments` for ONE file → its URL (§3.3) — the app's authed client.\n * Every file goes up BEFORE the report is created, so a row never names a file that did\n * not arrive (keksdose `use-feedback-dialog.tsx:89`); if any upload rejects, nothing is\n * created and the failure toast says so.\n */\n upload: (file: File) => Promise<string>;\n /** `POST /feedback` with the payload. Whatever it resolves with reaches `onSubmitted`. */\n create: (payload: FeedbackCreatePayload) => Promise<R>;\n /** Who is filing — shown in the context box and sent in `context.user_*`. */\n user?: FeedbackSubmitter | null;\n /** `context.environment` — the app's own reading of which copy it is (keksdose\n * `deployEnvironment()`). */\n environment?: FeedbackEnvironment | (string & {});\n /** `context.version` — the app build (`__APP_VERSION__`). */\n version?: string;\n /** The report was filed: after the \"Thanks for the feedback!\" toast and the close. The\n * place to invalidate the app's feedback queries. */\n onSubmitted?: (result: R) => void;\n /**\n * The failure toast's text from what `upload` or `create` rejected with — the app's own\n * `extractApiErrorMessage(err, fallback)` (keksdose) / `apiErrorMessage` (Kurvenschmiede),\n * so the server's `detail` wins (§3.7: a 413, a 415, keksdose's demo-session 403). Without\n * it every failure says `feedbackToast.submitFailed`.\n */\n errorMessage?: (error: unknown, fallback: string) => string;\n /**\n * The \"Capture screenshot\" button's capture. Default {@link captureAppScreenshot} of\n * `#root`; another root is `() => captureAppScreenshot({ rootId: \"app\" })`. `false`\n * hides the button — for an app that does not install the optional peer\n * `modern-screenshot`. A rejection becomes the `captureFailed` toast.\n */\n capture?: false | (() => Promise<File | null>);\n /**\n * The app's reports are public (0.38, docs/feedback-harmonization.md §9.2 decision 6):\n * `true` shows **\"Other users will see the title and text (never your screenshots or\n * files)\"** (`feedbackDialog.publicNotice`) under the body. keksdose and Kurvenschmiede\n * pass it — for a viewer who passes the app's `public_viewer` — and kastlan, whose\n * public parts are off, does not. Default `false`.\n */\n publicNotice?: boolean;\n}\n\n/**\n * `open`'s second argument (keksdose live #481: \"Report this problem\" on a receipt scan\n * that never finished, filed with the scan's bookkeeping). All of it optional, and none of\n * it outlives the opening it was handed to.\n */\nexport interface FeedbackSubmitOpenOptions {\n /**\n * What the app knows about the thing reported — keksdose's stuck scan: its id, status,\n * attempts, last error. Taken when the dialog opens (a shallow copy, so what the box\n * shows is what goes, even if the app's object changes while the reporter types), shown\n * read-only in the context box, and sent as `context.diagnostics` whatever \"Attach\n * current page URL\" says: that box withholds the page, not this. Flat, short values,\n * never the reported content itself (§3.2); the row detail shows them to the admin and\n * the author only.\n */\n diagnostics?: Readonly<Record<string, unknown>>;\n /** The subject the form starts with; the reporter can change it. Where the app's reports\n * are public (`publicNotice`), other users read it: what only the admin should see goes\n * in `diagnostics`. */\n title?: string;\n /** The body the form starts with, as `title` is the subject's. */\n body?: string;\n}\n\nexport interface FeedbackSubmit {\n /**\n * Open the dialog on `category` — a `FeedbackMenu` row's (§4.2: \"default category when\n * opened from a row: that row's\"). Without one, or with `CRASH` (never pickable), it\n * opens on `OTHER`, the server's own default (§3.4). Each open starts a fresh form,\n * reads `location.href` for the context box and ticks \"Attach current page URL\" again.\n *\n * `options` (0.54, keksdose live #481) is a screen's own report — `open(\"BUG\", {\n * diagnostics })` from \"Report this problem\": the diagnostics shown in the box and sent,\n * the subject and body prefilled. An open without them starts empty and sends none.\n */\n open: (category?: FeedbackCategory, options?: FeedbackSubmitOpenOptions) => void;\n /** Close it, as Cancel does. */\n close: () => void;\n /**\n * The dialog — render it once, anywhere (it portals). `null` while closed. Since 0.34\n * its module loads when it is first opened (MOD-1), so it appears a moment after\n * `open`: a test finds it with `await screen.findBy…`, not `getBy…`.\n */\n dialog: ReactNode;\n}\n\n/**\n * The feedback submit dialog, wired (0.27.0, docs/feedback-harmonization.md §4.2): the\n * one call that replaces keksdose's `use-feedback-dialog.tsx`, kastlan's `FeedbackButton`\n * and Kurvenschmiede's `use-feedback-dialog.tsx`.\n *\n * ```tsx\n * const feedback = useFeedbackSubmit({\n * upload: (file) => feedbackApi.uploadAttachment(file),\n * create: (payload) => feedbackApi.create(payload),\n * user, environment: deployEnvironment(), version: __APP_VERSION__,\n * errorMessage: extractApiErrorMessage,\n * onSubmitted: () => qc.invalidateQueries({ queryKey: queryKeys.feedback.all }),\n * });\n * <FeedbackMenu onFile={feedback.open} isAdmin={user?.role === \"ADMIN\"} />\n * {feedback.dialog}\n * ```\n *\n * What it settles, so no app restates it:\n *\n * - `FeedbackDialog` in `attachments=\"multiple\"` with `requireBody={false}` (§7.4): a\n * subject alone is a report. One captured screenshot in its own slot plus up to 5\n * picked or pasted files, `FEEDBACK_ATTACHMENT_ACCEPT` (images, PDF, text), 10 MB each.\n * - The pickable categories — Bug, Idea, Question, Other, in the kit's words. `CRASH` is\n * filed by the error boundary and is never offered (keksdose feedback #160).\n * - The context box, built from the same values as the `context` it sends\n * ({@link FeedbackContextBox} / `feedbackContext`), the URL read when the dialog OPENS,\n * and the diagnostics `open` was handed then (0.54, keksdose live #481).\n * - Upload every file, then create (keksdose `use-feedback-dialog.tsx:89`): the\n * screenshot to `screenshot_url`, the rest to `attachment_urls`, all in parallel.\n * - The toasts, from `feedbackToast`: filed, failed (the server's `detail` through\n * `errorMessage`), a refused file's type, size or count, a failed capture.\n *\n * What stays the app's: the API client, the user, the environment and version, and what\n * to refresh afterwards. No TanStack in here — `create` is a plain promise, so the hook\n * runs in any app; the app's mutation, if it wants one, lives inside `create`.\n *\n * A failure keeps the dialog open with the draft, as keksdose's did. A dialog closed and\n * opened again while a send is still running is NOT closed by that send's success — the\n * new draft is somebody's next report.\n */\nexport function useFeedbackSubmit<R = unknown>(options: UseFeedbackSubmitOptions<R>): FeedbackSubmit {\n const { upload, create, user, environment, version, onSubmitted, errorMessage, capture, publicNotice } = options;\n const toastText = useFeedbackToastLabels();\n const [isOpen, setIsOpen] = useState(false);\n const [category, setCategory] = useState<FeedbackCategory>(\"OTHER\");\n const [url, setUrl] = useState(\"\");\n const [attachUrl, setAttachUrl] = useState(true);\n const [opened, setOpened] = useState<FeedbackSubmitOpenOptions>({});\n const [submitting, setSubmitting] = useState(false);\n // Which opening a send belongs to — see \"closed and opened again\" above.\n const opening = useRef(0);\n const inFlight = useRef(false);\n\n const open = (next?: FeedbackCategory, given?: FeedbackSubmitOpenOptions) => {\n opening.current += 1;\n setCategory(next && FEEDBACK_PICKABLE_CATEGORIES.includes(next) ? next : \"OTHER\");\n setUrl(typeof window === \"undefined\" ? \"\" : window.location.href);\n // Fresh every time (Kurvenschmiede use-feedback-dialog.tsx:80): a reporter who left the\n // page out of one idea is not saying the bug report after it is unrelated too.\n setAttachUrl(true);\n // This opening's own, nothing of the last one's; the diagnostics copied now (#481).\n setOpened({ ...given, diagnostics: given?.diagnostics && { ...given.diagnostics } });\n setIsOpen(true);\n };\n const close = () => setIsOpen(false);\n\n // `context` comes from the dialog, built off the values its context box shows.\n const submit = async (data: FeedbackMultipleSubmission<FeedbackCategory>, context: FeedbackContext) => {\n if (inFlight.current) return;\n inFlight.current = true;\n setSubmitting(true);\n const from = opening.current;\n let result: R;\n try {\n const [screenshot_url, attachment_urls] = await Promise.all([\n data.screenshot ? upload(data.screenshot) : Promise.resolve(null),\n Promise.all(data.attachments.map((file) => upload(file))),\n ]);\n result = await create({\n title: data.title.trim(),\n body: data.body.trim() ? data.body : \"\",\n category: data.category,\n context,\n screenshot_url,\n attachment_urls,\n });\n } catch (error) {\n toast.error(errorMessage ? errorMessage(error, toastText.submitFailed) : toastText.submitFailed);\n return;\n } finally {\n inFlight.current = false;\n setSubmitting(false);\n }\n toast.success(toastText.submitted);\n if (opening.current === from) setIsOpen(false);\n onSubmitted?.(result);\n };\n\n // Nothing is drawn while the dialog's module is on its way — a few milliseconds, once per\n // page load; a spinner would only flash. A load that fails (a deploy replaced the chunk,\n // offline) closes the dialog (`SubmitDialogUnavailable`): the hook sits in the top bar,\n // above the routes' error boundaries, where a thrown chunk error took the shell down.\n const dialog = isOpen ? (\n <Suspense fallback={null}>\n <LazySubmitDialog\n onClose={close}\n category={category}\n onCategoryChange={setCategory}\n submitting={submitting}\n onSubmit={submit}\n user={user}\n url={url}\n attachUrl={attachUrl}\n onAttachUrlChange={setAttachUrl}\n environment={environment}\n version={version}\n capture={capture}\n publicNotice={publicNotice}\n opened={opened}\n />\n </Suspense>\n ) : null;\n\n return { open, close, dialog };\n}\n"],"mappings":";AAyPM;AAzPN,SAAS,UAAU,iBAAiB,QAAQ,gBAAgB;AAE5D,SAAS,aAAa;AACtB,SAAS,wBAAwB;AAKjC,SAAS,8BAA8B;AACvC,SAAS,oCAAoF;AAe7F,MAAM,mBAAmB;AAAA,EACvB,MAAM,OAAO,0BAA0B,EAAE,KAAK,CAAC,WAAW,OAAO,oBAAoB;AAAA,EACrF;AACF;AAEA,SAAS,wBAAwB,EAAE,QAAQ,GAA8B;AACvE,kBAAgB,SAAS,CAAC,OAAO,CAAC;AAClC,SAAO;AACT;AAyJO,SAAS,kBAA+B,SAAsD;AACnG,QAAM,EAAE,QAAQ,QAAQ,MAAM,aAAa,SAAS,aAAa,cAAc,SAAS,aAAa,IAAI;AACzG,QAAM,YAAY,uBAAuB;AACzC,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,UAAU,WAAW,IAAI,SAA2B,OAAO;AAClE,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,EAAE;AACjC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,IAAI;AAC/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAoC,CAAC,CAAC;AAClE,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAElD,QAAM,UAAU,OAAO,CAAC;AACxB,QAAM,WAAW,OAAO,KAAK;AAE7B,QAAM,OAAO,CAAC,MAAyB,UAAsC;AAC3E,YAAQ,WAAW;AACnB,gBAAY,QAAQ,6BAA6B,SAAS,IAAI,IAAI,OAAO,OAAO;AAChF,WAAO,OAAO,WAAW,cAAc,KAAK,OAAO,SAAS,IAAI;AAGhE,iBAAa,IAAI;AAEjB,cAAU,EAAE,GAAG,OAAO,aAAa,OAAO,eAAe,EAAE,GAAG,MAAM,YAAY,EAAE,CAAC;AACnF,cAAU,IAAI;AAAA,EAChB;AACA,QAAM,QAAQ,MAAM,UAAU,KAAK;AAGnC,QAAM,SAAS,OAAO,MAAoD,YAA6B;AACrG,QAAI,SAAS,QAAS;AACtB,aAAS,UAAU;AACnB,kBAAc,IAAI;AAClB,UAAM,OAAO,QAAQ;AACrB,QAAI;AACJ,QAAI;AACF,YAAM,CAAC,gBAAgB,eAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC1D,KAAK,aAAa,OAAO,KAAK,UAAU,IAAI,QAAQ,QAAQ,IAAI;AAAA,QAChE,QAAQ,IAAI,KAAK,YAAY,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC,CAAC;AAAA,MAC1D,CAAC;AACD,eAAS,MAAM,OAAO;AAAA,QACpB,OAAO,KAAK,MAAM,KAAK;AAAA,QACvB,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,OAAO;AAAA,QACrC,UAAU,KAAK;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,SAAS,OAAO;AACd,YAAM,MAAM,eAAe,aAAa,OAAO,UAAU,YAAY,IAAI,UAAU,YAAY;AAC/F;AAAA,IACF,UAAE;AACA,eAAS,UAAU;AACnB,oBAAc,KAAK;AAAA,IACrB;AACA,UAAM,QAAQ,UAAU,SAAS;AACjC,QAAI,QAAQ,YAAY,KAAM,WAAU,KAAK;AAC7C,kBAAc,MAAM;AAAA,EACtB;AAMA,QAAM,SAAS,SACb,oBAAC,YAAS,UAAU,MAClB;AAAA,IAAC;AAAA;AAAA,MACC,SAAS;AAAA,MACT;AAAA,MACA,kBAAkB;AAAA,MAClB;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF,GACF,IACE;AAEJ,SAAO,EAAE,MAAM,OAAO,OAAO;AAC/B;","names":[]}
|
|
@@ -70,8 +70,8 @@ const FEEDBACK_STATUS_META = {
|
|
|
70
70
|
},
|
|
71
71
|
// The one status with nowhere to move to: the vocabulary has a brand, an info, a
|
|
72
72
|
// warning and a danger, and nothing that means "succeeded". Deliberately NOT
|
|
73
|
-
// borrowed from a
|
|
74
|
-
//
|
|
73
|
+
// borrowed from a colour that means something else — a field's "saved" is a trip
|
|
74
|
+
// to the database rather than an outcome, and tokens.css's `--success` says so in
|
|
75
75
|
// as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which
|
|
76
76
|
// is why both carry a glyph and neither leans on colour alone.
|
|
77
77
|
DONE: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-vocabulary.ts"],"sourcesContent":["import {\n Ban,\n Bug,\n CheckCircle2,\n ClipboardCheck,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n RotateCcw,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\n\n/**\n * The feedback **vocabulary**: the nine statuses and five categories, the glyph and tone\n * of each, their orders, and the policy for how a report moves from status to status.\n *\n * Data and plain functions, no components. Until 0.34 this lived at the top of\n * feedback-inbox.tsx, so the feedback menu every app mounts in its top bar imported the\n * inbox — its components and the attachment field they render — for one constant (a\n * category's glyph), and only tree-shaking kept the rest out of every app's entry chunk.\n * From here the top bar's imports reach none of it (MOD-1, refactor audit 2026-10).\n * feedback-inbox.tsx re-exports every name here, so nothing an app imports has moved.\n */\n\n/** The nine states a report can be in.\n *\n * Shared value for value across the apps on purpose: it is what lets one habit,\n * and one agent prompt, work on any repo. Five are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31\n * (feedback contract §8.2): OPEN is \"filed, not yet triaged\", READY \"released for\n * implementation\", and nobody works an OPEN row. `REWORK` joined in 0.40 (§10.2\n * decisions 2, 4): \"sent back, released for work again\" — a READY that came back. */\nexport type FeedbackStatus =\n \"OPEN\" | \"READY\" | \"REWORK\" | \"IN_PROGRESS\" | \"IN_EVALUATION\" | \"NEEDS_LIVE_TEST\" | \"POSTPONED\" | \"DONE\" | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal eight long (as READY did in 0.31).\n */\nexport const FEEDBACK_STATUS_META: Record<FeedbackStatus, { icon: LucideIcon; activeBg: string; activeText: string }> =\n {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-active\",\n activeText: \"text-primary\",\n },\n // Triaged and released for implementation (feedback contract §8.2, keksdose live\n // #396): OPEN is \"filed, nobody has looked yet\", READY is \"go\". The brand tint at full\n // strength, so it reads as the go-ahead beside OPEN's neutral fill and\n // NEEDS_LIVE_TEST's muted brand.\n READY: {\n icon: ClipboardCheck,\n activeBg: \"bg-brand-soft\",\n activeText: \"text-brand\",\n },\n // Sent back by its author or an admin and released for work again (0.40, feedback\n // contract §10.2 decision 4): the Rework button's `RotateCcw`, in the danger tint so the\n // queue shows reworks apart from new reports. Not IN_PROGRESS's warning tint — that is\n // where a REWORK row moves next — and WONT_DO, the only other red, sits at the far end\n // with `Ban`.\n REWORK: {\n icon: RotateCcw,\n activeBg: \"bg-danger-soft\",\n activeText: \"text-danger\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-warning-soft\",\n activeText: \"text-warning\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-info-soft\",\n activeText: \"text-info\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-brand-soft\",\n activeText: \"text-brand-muted\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-active\",\n activeText: \"text-secondary\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a family that means something else — `--status-synced` is a\n // FIELD's trip to the database rather than an outcome, and tokens.css says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-success-soft\",\n activeText: \"text-success\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-danger-soft\",\n activeText: \"text-danger\",\n },\n };\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"READY\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. `REWORK` (0.40) is not in the chain —\n * \"advance\" from READY still goes to IN_PROGRESS — but it leads there too: a reworked\n * row is READY's twin. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n if (current === \"REWORK\") return \"IN_PROGRESS\";\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of nine icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (current === \"REWORK\") {\n // READY's twin (0.40, §10.2 decision 4): the step that picks the work up, and the\n // verdicts a released row can still get.\n set.add(\"IN_PROGRESS\");\n for (const off of OFF_CHAIN) set.add(off);\n } else if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-danger-soft\",\n badgeText: \"text-danger\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-warning-soft\",\n badgeText: \"text-warning\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-vocabulary.ts"],"sourcesContent":["import {\n Ban,\n Bug,\n CheckCircle2,\n ClipboardCheck,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n RotateCcw,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\n\n/**\n * The feedback **vocabulary**: the nine statuses and five categories, the glyph and tone\n * of each, their orders, and the policy for how a report moves from status to status.\n *\n * Data and plain functions, no components. Until 0.34 this lived at the top of\n * feedback-inbox.tsx, so the feedback menu every app mounts in its top bar imported the\n * inbox — its components and the attachment field they render — for one constant (a\n * category's glyph), and only tree-shaking kept the rest out of every app's entry chunk.\n * From here the top bar's imports reach none of it (MOD-1, refactor audit 2026-10).\n * feedback-inbox.tsx re-exports every name here, so nothing an app imports has moved.\n */\n\n/** The nine states a report can be in.\n *\n * Shared value for value across the apps on purpose: it is what lets one habit,\n * and one agent prompt, work on any repo. Five are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31\n * (feedback contract §8.2): OPEN is \"filed, not yet triaged\", READY \"released for\n * implementation\", and nobody works an OPEN row. `REWORK` joined in 0.40 (§10.2\n * decisions 2, 4): \"sent back, released for work again\" — a READY that came back. */\nexport type FeedbackStatus =\n \"OPEN\" | \"READY\" | \"REWORK\" | \"IN_PROGRESS\" | \"IN_EVALUATION\" | \"NEEDS_LIVE_TEST\" | \"POSTPONED\" | \"DONE\" | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal eight long (as READY did in 0.31).\n */\nexport const FEEDBACK_STATUS_META: Record<FeedbackStatus, { icon: LucideIcon; activeBg: string; activeText: string }> =\n {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-active\",\n activeText: \"text-primary\",\n },\n // Triaged and released for implementation (feedback contract §8.2, keksdose live\n // #396): OPEN is \"filed, nobody has looked yet\", READY is \"go\". The brand tint at full\n // strength, so it reads as the go-ahead beside OPEN's neutral fill and\n // NEEDS_LIVE_TEST's muted brand.\n READY: {\n icon: ClipboardCheck,\n activeBg: \"bg-brand-soft\",\n activeText: \"text-brand\",\n },\n // Sent back by its author or an admin and released for work again (0.40, feedback\n // contract §10.2 decision 4): the Rework button's `RotateCcw`, in the danger tint so the\n // queue shows reworks apart from new reports. Not IN_PROGRESS's warning tint — that is\n // where a REWORK row moves next — and WONT_DO, the only other red, sits at the far end\n // with `Ban`.\n REWORK: {\n icon: RotateCcw,\n activeBg: \"bg-danger-soft\",\n activeText: \"text-danger\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-warning-soft\",\n activeText: \"text-warning\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-info-soft\",\n activeText: \"text-info\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-brand-soft\",\n activeText: \"text-brand-muted\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-active\",\n activeText: \"text-secondary\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a colour that means something else — a field's \"saved\" is a trip\n // to the database rather than an outcome, and tokens.css's `--success` says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-success-soft\",\n activeText: \"text-success\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-danger-soft\",\n activeText: \"text-danger\",\n },\n };\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"READY\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. `REWORK` (0.40) is not in the chain —\n * \"advance\" from READY still goes to IN_PROGRESS — but it leads there too: a reworked\n * row is READY's twin. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n if (current === \"REWORK\") return \"IN_PROGRESS\";\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of nine icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (current === \"REWORK\") {\n // READY's twin (0.40, §10.2 decision 4): the step that picks the work up, and the\n // verdicts a released row can still get.\n set.add(\"IN_PROGRESS\");\n for (const off of OFF_CHAIN) set.add(off);\n } else if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-danger-soft\",\n badgeText: \"text-danger\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-warning-soft\",\n badgeText: \"text-warning\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-surface-2\",\n badgeText: \"text-secondary\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n"],"mappings":"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","names":[]}
|
package/dist/feedback.d.ts
CHANGED
|
@@ -20,3 +20,4 @@ export * from "./feedback/feedback-vote-button.js";
|
|
|
20
20
|
export * from "./feedback/feedback-comment-thread.js";
|
|
21
21
|
export * from "./feedback/feedback-replies.js";
|
|
22
22
|
export * from "./feedback/feedback-entry-file.js";
|
|
23
|
+
export { useFeedbackInbox, FeedbackInbox, type FeedbackInboxMutation, type FeedbackReplyPost, type FeedbackReworkPost, type UseFeedbackInboxOptions, type FeedbackInboxState, type FeedbackInboxProps, } from "./feedback/feedback-inbox-page.js";
|
package/dist/feedback.js
CHANGED
|
@@ -65,6 +65,10 @@ export * from "./feedback/feedback-vote-button.js";
|
|
|
65
65
|
export * from "./feedback/feedback-comment-thread.js";
|
|
66
66
|
export * from "./feedback/feedback-replies.js";
|
|
67
67
|
export * from "./feedback/feedback-entry-file.js";
|
|
68
|
+
import {
|
|
69
|
+
useFeedbackInbox,
|
|
70
|
+
FeedbackInbox
|
|
71
|
+
} from "./feedback/feedback-inbox-page.js";
|
|
68
72
|
export {
|
|
69
73
|
DEFAULT_ATTACHMENT_ACCEPT,
|
|
70
74
|
DEFAULT_FEEDBACK_DETAIL_LABELS,
|
|
@@ -83,6 +87,7 @@ export {
|
|
|
83
87
|
FEEDBACK_REWORKABLE_STATUSES,
|
|
84
88
|
FeedbackAwaitingToggle,
|
|
85
89
|
FeedbackEmptyState,
|
|
90
|
+
FeedbackInbox,
|
|
86
91
|
FeedbackMobileCard,
|
|
87
92
|
FeedbackPublicSwitch,
|
|
88
93
|
FeedbackReworkSection,
|
|
@@ -101,6 +106,7 @@ export {
|
|
|
101
106
|
splitBodyAttachments,
|
|
102
107
|
splitDescription,
|
|
103
108
|
useFeedbackColumns,
|
|
109
|
+
useFeedbackInbox,
|
|
104
110
|
useFeedbackPageLabels,
|
|
105
111
|
useFeedbackRenderDate,
|
|
106
112
|
useFeedbackStatusUndo
|
package/dist/feedback.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feedback.ts"],"sourcesContent":["// `@eifi1/ui-kit/feedback` — the report form and the parts an inbox is built from.\n//\n// A re-slicing of the main barrel, not a new API. The kit owns the status vocabulary,\n// the transition policy and the look; each app still wires its own API, columns,\n// strings and permissions — see the note at the top of feedback/feedback-inbox.tsx.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n// 0.27.0: the feedback harmonization (docs/feedback-harmonization.md) — one record,\n// one menu, dialog, table, detail and crash reporter for keksdose, kastlan, Kurvenschmiede.\n//\n// Named, not `*`, for six of its modules: 0.53 took the slice's share of the 55 names no app\n// reads off the surface (deprecated in 0.52, refactor audit 2026-10-30, API-4) —\n// `feedbackAttachmentLine`, `FEEDBACK_UNDO_DURATION`, `FEEDBACK_SWIPE_ACTIONS`,\n// `feedbackColumns`, `feedbackSubmitter`, `feedbackSubmitterEmail`, `feedbackEnvironment`,\n// `feedbackPagePath`, `feedbackAttachmentUrls`, `feedbackRowAccess`, `useFeedbackDetailLabels`\n// and `feedbackPublicSwitchAllowed`, which their modules keep for the kit's own use.\n// `feedbackPageHref` and `feedbackPageUrl` went whole: nothing read them.\nexport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n DEFAULT_MAX_ATTACHMENTS,\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_PICKABLE_CATEGORIES,\n FEEDBACK_REWORKABLE_STATUSES,\n FEEDBACK_QUEUE_STATUSES,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_AWAITING_STATUSES,\n FEEDBACK_DECIDED_STATUSES,\n FEEDBACK_COMMENT_MAX,\n FEEDBACK_REPLY_MAX,\n appendRework,\n reworkCount,\n feedbackReworkCount,\n splitBodyAttachments,\n splitDescription,\n replaceDescription,\n isImageAttachment,\n attachmentName,\n type FeedbackEnvironment,\n type FeedbackContext,\n type FeedbackRecord,\n} from \"./feedback/feedback-record\";\nexport * from \"./feedback/feedback-labels\";\nexport {\n useFeedbackStatusUndo,\n type FeedbackStatusPatch,\n type FeedbackStatusMutateAsync,\n type FeedbackStatusRow,\n type FeedbackStatusChange,\n type FeedbackStatusUndoOptions,\n} from \"./feedback/feedback-status-undo\";\nexport {\n DEFAULT_FEEDBACK_SWIPE,\n feedbackSwipePlan,\n feedbackSwipeOmitShown,\n type FeedbackSwipeAction,\n type FeedbackSwipeBinding,\n type FeedbackSwipeOptions,\n} from \"./feedback/feedback-swipe\";\nexport * from \"./feedback/feedback-menu\";\nexport * from \"./feedback/feedback-context\";\nexport * from \"./feedback/feedback-capture\";\nexport * from \"./feedback/feedback-submit\";\nexport {\n DEFAULT_FEEDBACK_PAGE_LABELS,\n useFeedbackPageLabels,\n isFeedbackAwaiting,\n useFeedbackRenderDate,\n FeedbackSubject,\n useFeedbackColumns,\n feedbackMobileGroupBy,\n FeedbackMobileCard,\n FeedbackAwaitingToggle,\n FeedbackEmptyState,\n type FeedbackPageLabels,\n type FeedbackTableRow,\n type FeedbackRenderDate,\n type FeedbackSubjectProps,\n type FeedbackColumnsOptions,\n type UseFeedbackColumnsOptions,\n type FeedbackMobileCardProps,\n type FeedbackAwaitingToggleProps,\n type FeedbackEmptyStateProps,\n} from \"./feedback/feedback-table\";\nexport {\n DEFAULT_FEEDBACK_DETAIL_LABELS,\n FeedbackReworkSection,\n FeedbackRowDetail,\n type FeedbackDetailLabels,\n type FeedbackRowAccess,\n type FeedbackDetailPatch,\n type FeedbackDetailUpdateAsync,\n type FeedbackUpload,\n type FeedbackReworkSend,\n type FeedbackReworkSectionProps,\n type FeedbackRowDetailProps,\n} from \"./feedback/feedback-row-detail\";\nexport * from \"./feedback/feedback-crash\";\n// 0.38.0: the public pages (§9.15) — the \"Visible to others\" switch, the list every user\n// reads and one report opened.\nexport {\n FeedbackPublicSwitch,\n type FeedbackPublicSwitchRow,\n type FeedbackPublicSwitchViewer,\n type FeedbackPublicSwitchProps,\n} from \"./feedback/feedback-public-switch\";\nexport * from \"./feedback/feedback-public-list\";\n// 0.38.0: public feedback (docs/feedback-harmonization.md §9) — the shapes, the vote, the\n// comment thread, the replies' timeline and composer.\nexport * from \"./feedback/feedback-public-types\";\nexport * from \"./feedback/feedback-vote-button\";\nexport * from \"./feedback/feedback-comment-thread\";\nexport * from \"./feedback/feedback-replies\";\nexport * from \"./feedback/feedback-entry-file\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAWd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,cAAc;AACd;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAUK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OASK;AACP,cAAc;AAGd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/feedback.ts"],"sourcesContent":["// `@eifi1/ui-kit/feedback` — the report form and the parts an inbox is built from.\n//\n// A re-slicing of the main barrel, not a new API. The kit owns the status vocabulary,\n// the transition policy and the look; each app still wires its own API, columns,\n// strings and permissions — see the note at the top of feedback/feedback-inbox.tsx.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n// 0.27.0: the feedback harmonization (docs/feedback-harmonization.md) — one record,\n// one menu, dialog, table, detail and crash reporter for keksdose, kastlan, Kurvenschmiede.\n//\n// Named, not `*`, for six of its modules: 0.53 took the slice's share of the 55 names no app\n// reads off the surface (deprecated in 0.52, refactor audit 2026-10-30, API-4) —\n// `feedbackAttachmentLine`, `FEEDBACK_UNDO_DURATION`, `FEEDBACK_SWIPE_ACTIONS`,\n// `feedbackColumns`, `feedbackSubmitter`, `feedbackSubmitterEmail`, `feedbackEnvironment`,\n// `feedbackPagePath`, `feedbackAttachmentUrls`, `feedbackRowAccess`, `useFeedbackDetailLabels`\n// and `feedbackPublicSwitchAllowed`, which their modules keep for the kit's own use.\n// `feedbackPageHref` and `feedbackPageUrl` went whole: nothing read them.\nexport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n DEFAULT_MAX_ATTACHMENTS,\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_PICKABLE_CATEGORIES,\n FEEDBACK_REWORKABLE_STATUSES,\n FEEDBACK_QUEUE_STATUSES,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_AWAITING_STATUSES,\n FEEDBACK_DECIDED_STATUSES,\n FEEDBACK_COMMENT_MAX,\n FEEDBACK_REPLY_MAX,\n appendRework,\n reworkCount,\n feedbackReworkCount,\n splitBodyAttachments,\n splitDescription,\n replaceDescription,\n isImageAttachment,\n attachmentName,\n type FeedbackEnvironment,\n type FeedbackContext,\n type FeedbackRecord,\n} from \"./feedback/feedback-record\";\nexport * from \"./feedback/feedback-labels\";\nexport {\n useFeedbackStatusUndo,\n type FeedbackStatusPatch,\n type FeedbackStatusMutateAsync,\n type FeedbackStatusRow,\n type FeedbackStatusChange,\n type FeedbackStatusUndoOptions,\n} from \"./feedback/feedback-status-undo\";\nexport {\n DEFAULT_FEEDBACK_SWIPE,\n feedbackSwipePlan,\n feedbackSwipeOmitShown,\n type FeedbackSwipeAction,\n type FeedbackSwipeBinding,\n type FeedbackSwipeOptions,\n} from \"./feedback/feedback-swipe\";\nexport * from \"./feedback/feedback-menu\";\nexport * from \"./feedback/feedback-context\";\nexport * from \"./feedback/feedback-capture\";\nexport * from \"./feedback/feedback-submit\";\nexport {\n DEFAULT_FEEDBACK_PAGE_LABELS,\n useFeedbackPageLabels,\n isFeedbackAwaiting,\n useFeedbackRenderDate,\n FeedbackSubject,\n useFeedbackColumns,\n feedbackMobileGroupBy,\n FeedbackMobileCard,\n FeedbackAwaitingToggle,\n FeedbackEmptyState,\n type FeedbackPageLabels,\n type FeedbackTableRow,\n type FeedbackRenderDate,\n type FeedbackSubjectProps,\n type FeedbackColumnsOptions,\n type UseFeedbackColumnsOptions,\n type FeedbackMobileCardProps,\n type FeedbackAwaitingToggleProps,\n type FeedbackEmptyStateProps,\n} from \"./feedback/feedback-table\";\nexport {\n DEFAULT_FEEDBACK_DETAIL_LABELS,\n FeedbackReworkSection,\n FeedbackRowDetail,\n type FeedbackDetailLabels,\n type FeedbackRowAccess,\n type FeedbackDetailPatch,\n type FeedbackDetailUpdateAsync,\n type FeedbackUpload,\n type FeedbackReworkSend,\n type FeedbackReworkSectionProps,\n type FeedbackRowDetailProps,\n} from \"./feedback/feedback-row-detail\";\nexport * from \"./feedback/feedback-crash\";\n// 0.38.0: the public pages (§9.15) — the \"Visible to others\" switch, the list every user\n// reads and one report opened.\nexport {\n FeedbackPublicSwitch,\n type FeedbackPublicSwitchRow,\n type FeedbackPublicSwitchViewer,\n type FeedbackPublicSwitchProps,\n} from \"./feedback/feedback-public-switch\";\nexport * from \"./feedback/feedback-public-list\";\n// 0.38.0: public feedback (docs/feedback-harmonization.md §9) — the shapes, the vote, the\n// comment thread, the replies' timeline and composer.\nexport * from \"./feedback/feedback-public-types\";\nexport * from \"./feedback/feedback-vote-button\";\nexport * from \"./feedback/feedback-comment-thread\";\nexport * from \"./feedback/feedback-replies\";\nexport * from \"./feedback/feedback-entry-file\";\n// 0.55.0: the inbox assembled once (kastlan reuse audit 2026-11-02, proposal 2;\n// docs/feedback-harmonization.md §13) — the wiring the three pages each wrote, as a hook\n// and the component that draws it.\nexport {\n useFeedbackInbox,\n FeedbackInbox,\n type FeedbackInboxMutation,\n type FeedbackReplyPost,\n type FeedbackReworkPost,\n type UseFeedbackInboxOptions,\n type FeedbackInboxState,\n type FeedbackInboxProps,\n} from \"./feedback/feedback-inbox-page\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAWd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,cAAc;AACd;AAAA,EACE;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAUK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OASK;AACP,cAAc;AAGd;AAAA,EACE;AAAA,OAIK;AACP,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,OAOK;","names":[]}
|
|
@@ -9,16 +9,43 @@
|
|
|
9
9
|
* The first render returns `value` itself, not `undefined`: a list filtered by the
|
|
10
10
|
* debounced query must render its initial state immediately, not after `ms`.
|
|
11
11
|
*
|
|
12
|
-
* **
|
|
13
|
-
* content. Pass a primitive — the typed query, a JSON string — or a value whose
|
|
12
|
+
* **By default it re-arms when `value`'s IDENTITY changes** (an effect on `[value, ms]`),
|
|
13
|
+
* not its content. Pass a primitive — the typed query, a JSON string — or a value whose
|
|
14
14
|
* identity is stable (state, a `useMemo`). An object or array built in render is a new
|
|
15
15
|
* one every render, so the timer is re-armed on every render and the value never
|
|
16
16
|
* settles — or, when the debounced value feeds the render that built it, settles,
|
|
17
17
|
* renders, builds another and fires again, every `ms`, with nobody typing
|
|
18
|
-
* (Kurvenschmiede's control page, KS U9).
|
|
19
|
-
*
|
|
18
|
+
* (Kurvenschmiede's control page, KS U9).
|
|
19
|
+
*
|
|
20
|
+
* **For such a value, name its content: `{ key }`** (0.55, KS U9). The timer then re-arms
|
|
21
|
+
* when the key changes and only then, and a new object under the same key is the value
|
|
22
|
+
* already on its way, not news. What comes back is the object that arrived with the key,
|
|
23
|
+
* and it keeps that identity until the key changes and settles — so it can go straight
|
|
24
|
+
* into a query key or a request without a `useMemo` round it. A key that comes back
|
|
25
|
+
* before the wait is over (A → B → A) settles on nothing new: the caller keeps the object
|
|
26
|
+
* it already had. Kurvenschmiede's own `useDebounced` (shared/lib/use-debounced.ts)
|
|
27
|
+
* settles the same way, and its four callers key one of two ways, both of which this
|
|
28
|
+
* takes:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* // A body that is small: its JSON is its content.
|
|
32
|
+
* const settled = useDebounce(geometry, 400, { key: JSON.stringify(geometry) });
|
|
33
|
+
* // A body too large to stringify on every render (twenty thousand samples twice
|
|
34
|
+
* // over): the few fields that say what the signal IS.
|
|
35
|
+
* const settled = useDebounce(request, settleMs, { key: JSON.stringify(request?.key ?? null) });
|
|
36
|
+
* ```
|
|
20
37
|
*/
|
|
21
|
-
export declare function useDebounce<T>(value: T, ms: number): T;
|
|
38
|
+
export declare function useDebounce<T>(value: T, ms: number, { key }?: DebounceOptions): T;
|
|
39
|
+
export interface DebounceOptions {
|
|
40
|
+
/**
|
|
41
|
+
* What the value IS, for a value whose identity changes on every render — a request
|
|
42
|
+
* body or a filter built in render: a string or number that changes exactly when the
|
|
43
|
+
* content does (`JSON.stringify(body)`, or the few fields that decide it). The wait
|
|
44
|
+
* restarts when the key changes, never on a new object under the same key. Without it,
|
|
45
|
+
* the value's identity is its key.
|
|
46
|
+
*/
|
|
47
|
+
key?: string | number;
|
|
48
|
+
}
|
|
22
49
|
export interface DebouncedCallbackOptions {
|
|
23
50
|
/**
|
|
24
51
|
* The longest a call may be held back, however often it is re-triggered. Without
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
function useDebounce(value, ms) {
|
|
4
|
-
const
|
|
3
|
+
function useDebounce(value, ms, { key } = {}) {
|
|
4
|
+
const id = key === void 0 ? value : key;
|
|
5
|
+
const [settled, setSettled] = useState(() => ({ id, value }));
|
|
5
6
|
useEffect(() => {
|
|
6
|
-
const timer = setTimeout(() =>
|
|
7
|
+
const timer = setTimeout(() => setSettled((prev) => Object.is(prev.id, id) ? prev : { id, value }), ms);
|
|
7
8
|
return () => clearTimeout(timer);
|
|
8
|
-
}, [
|
|
9
|
-
return
|
|
9
|
+
}, [id, ms]);
|
|
10
|
+
return settled.value;
|
|
10
11
|
}
|
|
11
12
|
function useDebouncedCallback(fn, ms, { maxWait } = {}) {
|
|
12
13
|
const fnRef = useRef(fn);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/use-debounce.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\n/**\n * A value that follows `value`, but only once it has stopped changing for `ms`.\n *\n * kastlan asked for the pair in this module because each of its search boxes carried\n * its own copy of the same six lines — a `useEffect` with a `setTimeout` — and two of\n * them forgot the cleanup, so a stale query landed AFTER the fresh one and replaced\n * its results. The cleanup is the whole point of having one implementation.\n *\n * The first render returns `value` itself, not `undefined`: a list filtered by the\n * debounced query must render its initial state immediately, not after `ms`.\n *\n * **
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/use-debounce.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\n/**\n * A value that follows `value`, but only once it has stopped changing for `ms`.\n *\n * kastlan asked for the pair in this module because each of its search boxes carried\n * its own copy of the same six lines — a `useEffect` with a `setTimeout` — and two of\n * them forgot the cleanup, so a stale query landed AFTER the fresh one and replaced\n * its results. The cleanup is the whole point of having one implementation.\n *\n * The first render returns `value` itself, not `undefined`: a list filtered by the\n * debounced query must render its initial state immediately, not after `ms`.\n *\n * **By default it re-arms when `value`'s IDENTITY changes** (an effect on `[value, ms]`),\n * not its content. Pass a primitive — the typed query, a JSON string — or a value whose\n * identity is stable (state, a `useMemo`). An object or array built in render is a new\n * one every render, so the timer is re-armed on every render and the value never\n * settles — or, when the debounced value feeds the render that built it, settles,\n * renders, builds another and fires again, every `ms`, with nobody typing\n * (Kurvenschmiede's control page, KS U9).\n *\n * **For such a value, name its content: `{ key }`** (0.55, KS U9). The timer then re-arms\n * when the key changes and only then, and a new object under the same key is the value\n * already on its way, not news. What comes back is the object that arrived with the key,\n * and it keeps that identity until the key changes and settles — so it can go straight\n * into a query key or a request without a `useMemo` round it. A key that comes back\n * before the wait is over (A → B → A) settles on nothing new: the caller keeps the object\n * it already had. Kurvenschmiede's own `useDebounced` (shared/lib/use-debounced.ts)\n * settles the same way, and its four callers key one of two ways, both of which this\n * takes:\n *\n * ```ts\n * // A body that is small: its JSON is its content.\n * const settled = useDebounce(geometry, 400, { key: JSON.stringify(geometry) });\n * // A body too large to stringify on every render (twenty thousand samples twice\n * // over): the few fields that say what the signal IS.\n * const settled = useDebounce(request, settleMs, { key: JSON.stringify(request?.key ?? null) });\n * ```\n */\nexport function useDebounce<T>(value: T, ms: number, { key }: DebounceOptions = {}): T {\n // What arms the timer: the value itself, as it always has, or the caller's name for it.\n const id: unknown = key === undefined ? value : key;\n // Held WITH the id it settled under, so a later render under the same id can be told\n // from news. Without a key the id is the value, and this is the value as it was.\n const [settled, setSettled] = useState(() => ({ id, value }));\n useEffect(() => {\n const timer = setTimeout(() => setSettled((prev) => (Object.is(prev.id, id) ? prev : { id, value })), ms);\n return () => clearTimeout(timer);\n // `value` is read as it was in the render that brought `id`: under a key, a later\n // object with the same key is the same value, and re-arming on it is KS U9's\n // metronome. Without a key `id` IS `value`, so these are today's `[value, ms]`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [id, ms]);\n return settled.value;\n}\n\nexport interface DebounceOptions {\n /**\n * What the value IS, for a value whose identity changes on every render — a request\n * body or a filter built in render: a string or number that changes exactly when the\n * content does (`JSON.stringify(body)`, or the few fields that decide it). The wait\n * restarts when the key changes, never on a new object under the same key. Without it,\n * the value's identity is its key.\n */\n key?: string | number;\n}\n\nexport interface DebouncedCallbackOptions {\n /**\n * The longest a call may be held back, however often it is re-triggered. Without\n * it a user who never stops typing never saves; with it, a draft autosave still\n * lands every `maxWait` ms while the typing goes on.\n */\n maxWait?: number;\n}\n\n/** The debounced function, plus the two escapes a caller needs around it. */\nexport type DebouncedFunction<A extends unknown[]> = ((...args: A) => void) & {\n /** Drop the pending call, if there is one. */\n cancel: () => void;\n /** Run the pending call now, if there is one — e.g. on blur or before navigating\n * away, so the last keystrokes of a form are not lost. */\n flush: () => void;\n /** Whether a call is waiting. */\n pending: () => boolean;\n};\n\n/**\n * `fn`, called only once calls have stopped arriving for `ms` — with the arguments\n * of the LAST call.\n *\n * The returned function keeps its identity across renders (so it can sit in an\n * effect's dependency list or be handed to a memoised child) and always calls the\n * LATEST `fn`, so a handler that closes over current state never runs a stale copy.\n * A pending call is cancelled on unmount: a debounced save firing into an unmounted\n * form is the bug kastlan reported, not a feature.\n */\nexport function useDebouncedCallback<A extends unknown[]>(\n fn: (...args: A) => void,\n ms: number,\n { maxWait }: DebouncedCallbackOptions = {},\n): DebouncedFunction<A> {\n const fnRef = useRef(fn);\n // Layout effect, not render-time assignment: a ref written during render is a\n // side effect React may replay or discard. Nothing can call the debounced function\n // between this commit and the layout effect, so it is never one render behind.\n useLayoutEffect(() => {\n fnRef.current = fn;\n });\n\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const maxTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const lastArgs = useRef<A | null>(null);\n\n const clear = useCallback(() => {\n if (timer.current) clearTimeout(timer.current);\n if (maxTimer.current) clearTimeout(maxTimer.current);\n timer.current = null;\n maxTimer.current = null;\n }, []);\n\n const invoke = useCallback(() => {\n const args = lastArgs.current;\n clear();\n lastArgs.current = null;\n if (args) fnRef.current(...args);\n }, [clear]);\n\n useEffect(() => clear, [clear]);\n\n return useMemo(() => {\n const debounced = ((...args: A) => {\n lastArgs.current = args;\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(invoke, ms);\n if (maxWait !== undefined && !maxTimer.current) {\n maxTimer.current = setTimeout(invoke, maxWait);\n }\n }) as DebouncedFunction<A>;\n debounced.cancel = () => {\n clear();\n lastArgs.current = null;\n };\n debounced.flush = () => {\n if (lastArgs.current) invoke();\n };\n debounced.pending = () => lastArgs.current !== null;\n return debounced;\n }, [ms, maxWait, invoke, clear]);\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,iBAAiB,SAAS,QAAQ,gBAAgB;AAuC5E,SAAS,YAAe,OAAU,IAAY,EAAE,IAAI,IAAqB,CAAC,GAAM;AAErF,QAAM,KAAc,QAAQ,SAAY,QAAQ;AAGhD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,OAAO,EAAE,IAAI,MAAM,EAAE;AAC5D,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW,MAAM,WAAW,CAAC,SAAU,OAAO,GAAG,KAAK,IAAI,EAAE,IAAI,OAAO,EAAE,IAAI,MAAM,CAAE,GAAG,EAAE;AACxG,WAAO,MAAM,aAAa,KAAK;AAAA,EAKjC,GAAG,CAAC,IAAI,EAAE,CAAC;AACX,SAAO,QAAQ;AACjB;AA2CO,SAAS,qBACd,IACA,IACA,EAAE,QAAQ,IAA8B,CAAC,GACnB;AACtB,QAAM,QAAQ,OAAO,EAAE;AAIvB,kBAAgB,MAAM;AACpB,UAAM,UAAU;AAAA,EAClB,CAAC;AAED,QAAM,QAAQ,OAA6C,IAAI;AAC/D,QAAM,WAAW,OAA6C,IAAI;AAClE,QAAM,WAAW,OAAiB,IAAI;AAEtC,QAAM,QAAQ,YAAY,MAAM;AAC9B,QAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAC7C,QAAI,SAAS,QAAS,cAAa,SAAS,OAAO;AACnD,UAAM,UAAU;AAChB,aAAS,UAAU;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,YAAY,MAAM;AAC/B,UAAM,OAAO,SAAS;AACtB,UAAM;AACN,aAAS,UAAU;AACnB,QAAI,KAAM,OAAM,QAAQ,GAAG,IAAI;AAAA,EACjC,GAAG,CAAC,KAAK,CAAC;AAEV,YAAU,MAAM,OAAO,CAAC,KAAK,CAAC;AAE9B,SAAO,QAAQ,MAAM;AACnB,UAAM,aAAa,IAAI,SAAY;AACjC,eAAS,UAAU;AACnB,UAAI,MAAM,QAAS,cAAa,MAAM,OAAO;AAC7C,YAAM,UAAU,WAAW,QAAQ,EAAE;AACrC,UAAI,YAAY,UAAa,CAAC,SAAS,SAAS;AAC9C,iBAAS,UAAU,WAAW,QAAQ,OAAO;AAAA,MAC/C;AAAA,IACF;AACA,cAAU,SAAS,MAAM;AACvB,YAAM;AACN,eAAS,UAAU;AAAA,IACrB;AACA,cAAU,QAAQ,MAAM;AACtB,UAAI,SAAS,QAAS,QAAO;AAAA,IAC/B;AACA,cAAU,UAAU,MAAM,SAAS,YAAY;AAC/C,WAAO;AAAA,EACT,GAAG,CAAC,IAAI,SAAS,QAAQ,KAAK,CAAC;AACjC;","names":[]}
|
|
@@ -125,7 +125,9 @@ export interface UseFilePickerOptions extends FileScreenOptions {
|
|
|
125
125
|
* Ask a phone for its camera instead of the file chooser: `"environment"` is the
|
|
126
126
|
* back camera, `"user"` the front. A HINT — desktop browsers ignore it, and Chrome
|
|
127
127
|
* drops it when `multiple` is also set (a camera cannot deliver a list), which is why
|
|
128
|
-
* keksdose's camera input has no `multiple`. Pass one or the other.
|
|
128
|
+
* keksdose's camera input has no `multiple`. Pass one or the other. Android Chrome may
|
|
129
|
+
* still show its Camera-or-Files sheet for it (keksdose live #292); `CameraButton` (0.55)
|
|
130
|
+
* opens the camera in the page instead and keeps this as its fallback.
|
|
129
131
|
*/
|
|
130
132
|
capture?: boolean | "user" | "environment";
|
|
131
133
|
/** The files that passed, in pick order. Never called with an empty array.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/use-file-picker.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { ReactElement } from \"react\";\nimport { useAnnounce } from \"./use-announce\";\nimport { useKitFileLabels, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_FILE_PICKER_LABELS } from \"./use-file-picker-labels\";\n\n/**\n * The file pickers' headless half: the hidden input, the screening of a pick and the\n * announcement of a refusal, which {@link FileButton}, `FileDropzone` and {@link useFileDrop}\n * are built on.\n *\n * In hooks/ since 0.34 (MOD-8, refactor audit 2026-10). It lived in\n * components/file-button.tsx, and `useFileDrop` — a hook — imported it from there, so\n * hooks/ reached up into components/. components/file-button re-exports all of it, so an\n * import from there keeps working.\n */\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the file pickers ({@link FileButton}, `FileDropzone`) render or speak.\n * Messages are functions of the file name so a translation can put it anywhere. */\nexport interface FilePickerLabels {\n /** The file's type is not in `accept`, when no `accept` is known to name (see\n * `rejectedTypeOnly`). */\n rejectedType: (name: string) => string;\n /** The file's type is not in `accept`, naming what IS accepted: `accept`'s tokens,\n * lower-cased and joined with \", \" (`\".csv\"` => \"Only .csv files\"). Used whenever\n * `accept` is set — kastlan kept a custom `isValid` + `invalidMessage` per form only\n * to say this, and `accept` already knows it. */\n rejectedTypeOnly: (accept: string, name: string) => string;\n /** The file is larger than `maxSize`; `maxSize` arrives formatted (\"5 MB\"). */\n rejectedSize: (name: string, maxSize: string) => string;\n /** The file was one too many for `maxFiles`. */\n rejectedCount: (name: string, maxFiles: number) => string;\n /** `isValid` said no and the caller gave no `invalidMessage`. */\n rejectedInvalid: (name: string) => string;\n /** Spoken instead of the per-file message when more than one file was refused. */\n rejectedMany: (count: number) => string;\n /** Spoken (and shown, on a dropzone) when `onPick` refused the WHOLE pick.\n * Receives the number of files in it. */\n rejectedPick: (count: number) => string;\n /** Spoken after a pick the component itself echoes (the dropzone). */\n selected: (count: number, firstName: string) => string;\n /** The dropzone's remove button for one file. */\n remove: (name: string) => string;\n /** The dropzone's remove-everything button in `multiple` mode. */\n clearAll: string;\n /** Spoken after a remove / clear, since the button that was pressed is gone. */\n removed: (name: string) => string;\n cleared: string;\n /** The dropzone's accessible name, when the host passes no `dropLabel`. */\n dropzone: string;\n /** The dropzone's Browse button, when the host passes no `browseLabel`. */\n browse: string;\n /** What an empty single-file dropzone says (no `emptyLabel` given). */\n empty: string;\n /** …and an empty `multiple` one. */\n emptyMultiple: string;\n /** The dropzone's second line (no `hint` given). Receives `accept` as a person\n * reads it (`\".pdf, image/png\"`), or `\"\"` when anything goes. */\n hint: (accept: string) => string;\n /** What a `busy` dropzone says in place of its body (\"Uploading…\"). */\n busy: string;\n}\n\n/* ── Screening ───────────────────────────────────────────────────────────── */\n\nexport type FileRejectionReason = \"type\" | \"size\" | \"count\" | \"invalid\";\n\n/** One file the picker refused, and why. `message` is already translated (see\n * {@link FilePickerLabels}, or the caller's `invalidMessage`), so a host that just\n * wants to show it can render `rejections[0].message` as is. */\nexport interface FileRejection {\n file: File;\n reason: FileRejectionReason;\n message: string;\n}\n\n/**\n * Does `file` satisfy an `accept` string, the way the browser's dialog reads it?\n * Comma-separated tokens: `.ext` (case-insensitive suffix of the name), `type/*`\n * (a MIME family) or an exact MIME type. An empty or absent `accept` takes anything.\n *\n * A file with no `type` — common for `.step`, `.dat`, anything the OS has no MIME\n * mapping for — can only match by extension, which is why lenkbank lists `.stp` AND\n * `model/step`: that is how `accept` has to be written for the dialog anyway.\n */\nexport function matchesAccept(file: File, accept: string | undefined): boolean {\n const tokens = (accept ?? \"\")\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean);\n if (tokens.length === 0) return true;\n const name = file.name.toLowerCase();\n // An EMPTY type is the browser saying \"I don't know\", not \"it's something else\": HEIC\n // photos on Windows without the codec arrive with `type === \"\"`. Refusing them\n // (0.6.0) refused iPhone photos the picker itself had just offered (keksdose). So a\n // missing type is inferred from the extension where that is unambiguous, and a file\n // whose type cannot be known at all is given the benefit of the doubt — nothing here\n // proves it does not match, and the server validates what it receives anyway.\n const type = (file.type || typeFromExtension(name) || \"\").toLowerCase();\n if (!type && !tokens.every((t) => t.startsWith(\".\"))) return true;\n return tokens.some((token) => {\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (!type) return false;\n if (token.endsWith(\"/*\")) return type.startsWith(token.slice(0, -1));\n return type === token;\n });\n}\n\n/** MIME types for the extensions a browser most often leaves untyped. */\nconst EXTENSION_TYPES: Record<string, string> = {\n heic: \"image/heic\",\n heif: \"image/heif\",\n avif: \"image/avif\",\n webp: \"image/webp\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n pdf: \"application/pdf\",\n csv: \"text/csv\",\n txt: \"text/plain\",\n};\n\nfunction typeFromExtension(name: string): string | undefined {\n const dot = name.lastIndexOf(\".\");\n return dot === -1 ? undefined : EXTENSION_TYPES[name.slice(dot + 1)];\n}\n\n/** @internal `\" .PDF, image/png ,\"` => `\".pdf, image/png\"` — `accept` as a person reads\n * it. Also the dropzone's default hint. */\nexport function formatAccept(accept: string): string {\n return accept\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean)\n .join(\", \");\n}\n\n/** What the pickers screen a pick with. All optional; nothing set accepts everything. */\nexport interface FileScreenOptions {\n accept?: string;\n /** Bytes. Larger files are refused with reason `\"size\"`. */\n maxSize?: number;\n /** How many files one pick may deliver. The rest are refused with reason `\"count\"`\n * — first come, first kept. For a running cap (\"at most 5 attachments\") pass what\n * is LEFT: `maxFiles={5 - attachments.length}`. */\n maxFiles?: number;\n /** The caller's own check, after `accept` and `maxSize`. */\n isValid?: (file: File) => boolean;\n /** The message for an `isValid` refusal; `labels.rejectedInvalid` otherwise. */\n invalidMessage?: string;\n}\n\n/** @internal Split a pick into the files that pass and the ones that do not. */\nexport function screenFiles(\n files: readonly File[],\n opts: FileScreenOptions,\n labels: FilePickerLabels,\n formatSize: (bytes: number) => string,\n): { accepted: File[]; rejected: FileRejection[] } {\n const accepted: File[] = [];\n const rejected: FileRejection[] = [];\n // Name what `accept` takes (\"Only .csv files\") — unless the host translated\n // `rejectedType` but not yet `rejectedTypeOnly`: its own sentence beats an English one.\n const acceptText = opts.accept ? formatAccept(opts.accept) : \"\";\n const useOnly =\n acceptText !== \"\" &&\n (labels.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||\n labels.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);\n const typeMessage = (name: string) =>\n useOnly ? labels.rejectedTypeOnly(acceptText, name) : labels.rejectedType(name);\n for (const file of files) {\n let reason: FileRejectionReason | null = null;\n if (!matchesAccept(file, opts.accept)) reason = \"type\";\n else if (opts.maxSize !== undefined && file.size > opts.maxSize) reason = \"size\";\n else if (opts.isValid && !opts.isValid(file)) reason = \"invalid\";\n // Count last, and only against files that passed everything else: a refused\n // file should not use up one of the slots a good one could have had.\n else if (opts.maxFiles !== undefined && accepted.length >= opts.maxFiles) reason = \"count\";\n if (reason === null) {\n accepted.push(file);\n continue;\n }\n const message =\n reason === \"type\"\n ? typeMessage(file.name)\n : reason === \"size\"\n ? labels.rejectedSize(file.name, formatSize(opts.maxSize ?? 0))\n : reason === \"count\"\n ? labels.rejectedCount(file.name, Math.max(0, opts.maxFiles ?? 0))\n : (opts.invalidMessage ?? labels.rejectedInvalid(file.name));\n rejected.push({ file, reason, message });\n }\n return { accepted, rejected };\n}\n\n/**\n * Judges a pick as a whole — keksdose's \"refuse the pick if any file is bad\", which\n * with `onFiles` + `onReject` alone had to be rebuilt from two calls in a microtask.\n *\n * Called ONCE per pick, after screening and before `onFiles` / `onReject`, with both\n * halves (either may be empty). Return `false` to refuse the whole pick: the accepted\n * files are not delivered, `onReject` still receives what screening refused, and one\n * sentence (`labels.rejectedPick`) is spoken for the pick instead of a per-file one.\n * Return nothing to let the pick through as usual.\n *\n * ```tsx\n * onPick={(ok, bad) => bad.length === 0 || false} // all or nothing\n * ```\n */\nexport type FilePickHandler = (accepted: File[], rejected: FileRejection[]) => boolean | void;\n\n/** @internal The shared tail of both pickers: run `onPick`, and say whether the pick\n * stands. A pick with nothing accepted has nothing to refuse, so `false` there is\n * treated as no verdict and the usual per-file messages are kept. */\nexport function judgePick(onPick: FilePickHandler | undefined, accepted: File[], rejected: FileRejection[]): boolean {\n if (!onPick) return true;\n return onPick(accepted, rejected) !== false || accepted.length === 0;\n}\n\n/** One sentence for a whole batch of refusals: the file's own message for one, a\n * count for several — reading out five sentences in a row helps nobody. */\nexport function summariseRejections(rejected: readonly FileRejection[], labels: FilePickerLabels): string {\n if (rejected.length === 0) return \"\";\n if (rejected.length === 1) return rejected[0].message;\n return labels.rejectedMany(rejected.length);\n}\n\n/* ── useFilePicker ───────────────────────────────────────────────────────── */\n\nexport interface UseFilePickerOptions extends FileScreenOptions {\n /** Let one pick deliver several files. Without it a drop of several keeps the first. */\n multiple?: boolean;\n /**\n * Ask a phone for its camera instead of the file chooser: `\"environment\"` is the\n * back camera, `\"user\"` the front. A HINT — desktop browsers ignore it, and Chrome\n * drops it when `multiple` is also set (a camera cannot deliver a list), which is why\n * keksdose's camera input has no `multiple`. Pass one or the other.\n */\n capture?: boolean | \"user\" | \"environment\";\n /** The files that passed, in pick order. Never called with an empty array.\n * Optional since 0.7 for a caller that takes the pick through `onPick` alone. */\n onFiles?: (files: File[]) => void;\n /** See {@link FilePickHandler}: the whole pick at once, with the power to refuse it. */\n onPick?: FilePickHandler;\n /** The files that did not, with a translated message each. The refusals are also\n * spoken through a live region, so this is for SHOWING them, not for a11y. */\n onReject?: (rejections: FileRejection[]) => void;\n /** `open()` and `take()` do nothing while set. */\n disabled?: boolean;\n /** Per-instance overrides of the `filePicker` label namespace. */\n labels?: LabelOverride<FilePickerLabels>;\n}\n\nexport interface UseFilePickerReturn {\n /** Open the system picker. Call it from a click handler — browsers only open a file\n * dialog in response to a user gesture. */\n open: () => void;\n /** Screen and deliver files that arrived some other way — a drop, a paste. Honours\n * `multiple` (only the first file without it) and every check. */\n take: (files: ArrayLike<File> | null | undefined) => void;\n /** The hidden input and the live region. Render it once, anywhere — it takes no\n * space and needs no positioned ancestor. */\n element: ReactElement;\n}\n\n/**\n * The headless half of {@link FileButton}: a hidden file input you can open from any\n * control, with the reset, the screening and the announcement built in.\n *\n * ```tsx\n * const picker = useFilePicker({ accept: \".pdf\", onFiles: ([f]) => upload(f) });\n * return <>{picker.element}<DetailTableCard onAdd={picker.open} … /></>;\n * ```\n */\nexport function useFilePicker({\n accept,\n multiple,\n capture,\n maxSize,\n maxFiles,\n isValid,\n invalidMessage,\n onFiles,\n onPick,\n onReject,\n disabled,\n labels: labelsProp,\n}: UseFilePickerOptions): UseFilePickerReturn {\n const inputRef = useRef<HTMLInputElement>(null);\n const labels = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS, labelsProp);\n const fileText = useKitFileLabels();\n // Assertive: a refusal is a failure the user has to hear before they move on, and\n // it is the only thing this region ever says.\n const { announce, regionProps } = useAnnounce({ politeness: \"assertive\" });\n\n const take = (list: ArrayLike<File> | null | undefined) => {\n if (disabled || !list || list.length === 0) return;\n const all = Array.from(list);\n const files = multiple ? all : all.slice(0, 1);\n const { accepted, rejected } = screenFiles(\n files,\n // A single-file picker holds one file by definition; `maxFiles` is a multi-mode\n // cap and would only ever say \"0\" there if a caller passed it by mistake.\n { accept, maxSize, maxFiles: multiple ? maxFiles : undefined, isValid, invalidMessage },\n labels,\n fileText.size,\n );\n if (!judgePick(onPick, accepted, rejected)) {\n if (rejected.length > 0) onReject?.(rejected);\n announce(labels.rejectedPick(files.length));\n return;\n }\n if (accepted.length > 0) onFiles?.(accepted);\n if (rejected.length > 0) {\n onReject?.(rejected);\n announce(summariseRejections(rejected, labels));\n }\n };\n\n const element = (\n <>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n capture={capture}\n disabled={disabled}\n // `hidden` (display: none), not `sr-only`: the control a user reaches is the\n // button, and a second, nameless tab stop for the same action is noise. A\n // display:none file input still opens on `.click()` in every current browser —\n // and, unlike `sr-only`, it cannot escape to the initial containing block and\n // stretch the page (see the note in `file-dropzone.tsx`).\n className=\"hidden\"\n tabIndex={-1}\n aria-hidden\n onChange={(e) => {\n // Copy BEFORE the reset: `files` is a live view of the input's value, and\n // clearing the value empties it.\n const picked = Array.from(e.currentTarget.files ?? []);\n e.currentTarget.value = \"\";\n take(picked);\n }}\n />\n <span {...regionProps} />\n </>\n );\n\n return {\n open: () => {\n if (!disabled) inputRef.current?.click();\n },\n take,\n element,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/use-file-picker.tsx"],"sourcesContent":["import { useRef } from \"react\";\nimport type { ReactElement } from \"react\";\nimport { useAnnounce } from \"./use-announce\";\nimport { useKitFileLabels, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_FILE_PICKER_LABELS } from \"./use-file-picker-labels\";\n\n/**\n * The file pickers' headless half: the hidden input, the screening of a pick and the\n * announcement of a refusal, which {@link FileButton}, `FileDropzone` and {@link useFileDrop}\n * are built on.\n *\n * In hooks/ since 0.34 (MOD-8, refactor audit 2026-10). It lived in\n * components/file-button.tsx, and `useFileDrop` — a hook — imported it from there, so\n * hooks/ reached up into components/. components/file-button re-exports all of it, so an\n * import from there keeps working.\n */\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the file pickers ({@link FileButton}, `FileDropzone`) render or speak.\n * Messages are functions of the file name so a translation can put it anywhere. */\nexport interface FilePickerLabels {\n /** The file's type is not in `accept`, when no `accept` is known to name (see\n * `rejectedTypeOnly`). */\n rejectedType: (name: string) => string;\n /** The file's type is not in `accept`, naming what IS accepted: `accept`'s tokens,\n * lower-cased and joined with \", \" (`\".csv\"` => \"Only .csv files\"). Used whenever\n * `accept` is set — kastlan kept a custom `isValid` + `invalidMessage` per form only\n * to say this, and `accept` already knows it. */\n rejectedTypeOnly: (accept: string, name: string) => string;\n /** The file is larger than `maxSize`; `maxSize` arrives formatted (\"5 MB\"). */\n rejectedSize: (name: string, maxSize: string) => string;\n /** The file was one too many for `maxFiles`. */\n rejectedCount: (name: string, maxFiles: number) => string;\n /** `isValid` said no and the caller gave no `invalidMessage`. */\n rejectedInvalid: (name: string) => string;\n /** Spoken instead of the per-file message when more than one file was refused. */\n rejectedMany: (count: number) => string;\n /** Spoken (and shown, on a dropzone) when `onPick` refused the WHOLE pick.\n * Receives the number of files in it. */\n rejectedPick: (count: number) => string;\n /** Spoken after a pick the component itself echoes (the dropzone). */\n selected: (count: number, firstName: string) => string;\n /** The dropzone's remove button for one file. */\n remove: (name: string) => string;\n /** The dropzone's remove-everything button in `multiple` mode. */\n clearAll: string;\n /** Spoken after a remove / clear, since the button that was pressed is gone. */\n removed: (name: string) => string;\n cleared: string;\n /** The dropzone's accessible name, when the host passes no `dropLabel`. */\n dropzone: string;\n /** The dropzone's Browse button, when the host passes no `browseLabel`. */\n browse: string;\n /** What an empty single-file dropzone says (no `emptyLabel` given). */\n empty: string;\n /** …and an empty `multiple` one. */\n emptyMultiple: string;\n /** The dropzone's second line (no `hint` given). Receives `accept` as a person\n * reads it (`\".pdf, image/png\"`), or `\"\"` when anything goes. */\n hint: (accept: string) => string;\n /** What a `busy` dropzone says in place of its body (\"Uploading…\"). */\n busy: string;\n}\n\n/* ── Screening ───────────────────────────────────────────────────────────── */\n\nexport type FileRejectionReason = \"type\" | \"size\" | \"count\" | \"invalid\";\n\n/** One file the picker refused, and why. `message` is already translated (see\n * {@link FilePickerLabels}, or the caller's `invalidMessage`), so a host that just\n * wants to show it can render `rejections[0].message` as is. */\nexport interface FileRejection {\n file: File;\n reason: FileRejectionReason;\n message: string;\n}\n\n/**\n * Does `file` satisfy an `accept` string, the way the browser's dialog reads it?\n * Comma-separated tokens: `.ext` (case-insensitive suffix of the name), `type/*`\n * (a MIME family) or an exact MIME type. An empty or absent `accept` takes anything.\n *\n * A file with no `type` — common for `.step`, `.dat`, anything the OS has no MIME\n * mapping for — can only match by extension, which is why lenkbank lists `.stp` AND\n * `model/step`: that is how `accept` has to be written for the dialog anyway.\n */\nexport function matchesAccept(file: File, accept: string | undefined): boolean {\n const tokens = (accept ?? \"\")\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean);\n if (tokens.length === 0) return true;\n const name = file.name.toLowerCase();\n // An EMPTY type is the browser saying \"I don't know\", not \"it's something else\": HEIC\n // photos on Windows without the codec arrive with `type === \"\"`. Refusing them\n // (0.6.0) refused iPhone photos the picker itself had just offered (keksdose). So a\n // missing type is inferred from the extension where that is unambiguous, and a file\n // whose type cannot be known at all is given the benefit of the doubt — nothing here\n // proves it does not match, and the server validates what it receives anyway.\n const type = (file.type || typeFromExtension(name) || \"\").toLowerCase();\n if (!type && !tokens.every((t) => t.startsWith(\".\"))) return true;\n return tokens.some((token) => {\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (!type) return false;\n if (token.endsWith(\"/*\")) return type.startsWith(token.slice(0, -1));\n return type === token;\n });\n}\n\n/** MIME types for the extensions a browser most often leaves untyped. */\nconst EXTENSION_TYPES: Record<string, string> = {\n heic: \"image/heic\",\n heif: \"image/heif\",\n avif: \"image/avif\",\n webp: \"image/webp\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n pdf: \"application/pdf\",\n csv: \"text/csv\",\n txt: \"text/plain\",\n};\n\nfunction typeFromExtension(name: string): string | undefined {\n const dot = name.lastIndexOf(\".\");\n return dot === -1 ? undefined : EXTENSION_TYPES[name.slice(dot + 1)];\n}\n\n/** @internal `\" .PDF, image/png ,\"` => `\".pdf, image/png\"` — `accept` as a person reads\n * it. Also the dropzone's default hint. */\nexport function formatAccept(accept: string): string {\n return accept\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean)\n .join(\", \");\n}\n\n/** What the pickers screen a pick with. All optional; nothing set accepts everything. */\nexport interface FileScreenOptions {\n accept?: string;\n /** Bytes. Larger files are refused with reason `\"size\"`. */\n maxSize?: number;\n /** How many files one pick may deliver. The rest are refused with reason `\"count\"`\n * — first come, first kept. For a running cap (\"at most 5 attachments\") pass what\n * is LEFT: `maxFiles={5 - attachments.length}`. */\n maxFiles?: number;\n /** The caller's own check, after `accept` and `maxSize`. */\n isValid?: (file: File) => boolean;\n /** The message for an `isValid` refusal; `labels.rejectedInvalid` otherwise. */\n invalidMessage?: string;\n}\n\n/** @internal Split a pick into the files that pass and the ones that do not. */\nexport function screenFiles(\n files: readonly File[],\n opts: FileScreenOptions,\n labels: FilePickerLabels,\n formatSize: (bytes: number) => string,\n): { accepted: File[]; rejected: FileRejection[] } {\n const accepted: File[] = [];\n const rejected: FileRejection[] = [];\n // Name what `accept` takes (\"Only .csv files\") — unless the host translated\n // `rejectedType` but not yet `rejectedTypeOnly`: its own sentence beats an English one.\n const acceptText = opts.accept ? formatAccept(opts.accept) : \"\";\n const useOnly =\n acceptText !== \"\" &&\n (labels.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||\n labels.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);\n const typeMessage = (name: string) =>\n useOnly ? labels.rejectedTypeOnly(acceptText, name) : labels.rejectedType(name);\n for (const file of files) {\n let reason: FileRejectionReason | null = null;\n if (!matchesAccept(file, opts.accept)) reason = \"type\";\n else if (opts.maxSize !== undefined && file.size > opts.maxSize) reason = \"size\";\n else if (opts.isValid && !opts.isValid(file)) reason = \"invalid\";\n // Count last, and only against files that passed everything else: a refused\n // file should not use up one of the slots a good one could have had.\n else if (opts.maxFiles !== undefined && accepted.length >= opts.maxFiles) reason = \"count\";\n if (reason === null) {\n accepted.push(file);\n continue;\n }\n const message =\n reason === \"type\"\n ? typeMessage(file.name)\n : reason === \"size\"\n ? labels.rejectedSize(file.name, formatSize(opts.maxSize ?? 0))\n : reason === \"count\"\n ? labels.rejectedCount(file.name, Math.max(0, opts.maxFiles ?? 0))\n : (opts.invalidMessage ?? labels.rejectedInvalid(file.name));\n rejected.push({ file, reason, message });\n }\n return { accepted, rejected };\n}\n\n/**\n * Judges a pick as a whole — keksdose's \"refuse the pick if any file is bad\", which\n * with `onFiles` + `onReject` alone had to be rebuilt from two calls in a microtask.\n *\n * Called ONCE per pick, after screening and before `onFiles` / `onReject`, with both\n * halves (either may be empty). Return `false` to refuse the whole pick: the accepted\n * files are not delivered, `onReject` still receives what screening refused, and one\n * sentence (`labels.rejectedPick`) is spoken for the pick instead of a per-file one.\n * Return nothing to let the pick through as usual.\n *\n * ```tsx\n * onPick={(ok, bad) => bad.length === 0 || false} // all or nothing\n * ```\n */\nexport type FilePickHandler = (accepted: File[], rejected: FileRejection[]) => boolean | void;\n\n/** @internal The shared tail of both pickers: run `onPick`, and say whether the pick\n * stands. A pick with nothing accepted has nothing to refuse, so `false` there is\n * treated as no verdict and the usual per-file messages are kept. */\nexport function judgePick(onPick: FilePickHandler | undefined, accepted: File[], rejected: FileRejection[]): boolean {\n if (!onPick) return true;\n return onPick(accepted, rejected) !== false || accepted.length === 0;\n}\n\n/** One sentence for a whole batch of refusals: the file's own message for one, a\n * count for several — reading out five sentences in a row helps nobody. */\nexport function summariseRejections(rejected: readonly FileRejection[], labels: FilePickerLabels): string {\n if (rejected.length === 0) return \"\";\n if (rejected.length === 1) return rejected[0].message;\n return labels.rejectedMany(rejected.length);\n}\n\n/* ── useFilePicker ───────────────────────────────────────────────────────── */\n\nexport interface UseFilePickerOptions extends FileScreenOptions {\n /** Let one pick deliver several files. Without it a drop of several keeps the first. */\n multiple?: boolean;\n /**\n * Ask a phone for its camera instead of the file chooser: `\"environment\"` is the\n * back camera, `\"user\"` the front. A HINT — desktop browsers ignore it, and Chrome\n * drops it when `multiple` is also set (a camera cannot deliver a list), which is why\n * keksdose's camera input has no `multiple`. Pass one or the other. Android Chrome may\n * still show its Camera-or-Files sheet for it (keksdose live #292); `CameraButton` (0.55)\n * opens the camera in the page instead and keeps this as its fallback.\n */\n capture?: boolean | \"user\" | \"environment\";\n /** The files that passed, in pick order. Never called with an empty array.\n * Optional since 0.7 for a caller that takes the pick through `onPick` alone. */\n onFiles?: (files: File[]) => void;\n /** See {@link FilePickHandler}: the whole pick at once, with the power to refuse it. */\n onPick?: FilePickHandler;\n /** The files that did not, with a translated message each. The refusals are also\n * spoken through a live region, so this is for SHOWING them, not for a11y. */\n onReject?: (rejections: FileRejection[]) => void;\n /** `open()` and `take()` do nothing while set. */\n disabled?: boolean;\n /** Per-instance overrides of the `filePicker` label namespace. */\n labels?: LabelOverride<FilePickerLabels>;\n}\n\nexport interface UseFilePickerReturn {\n /** Open the system picker. Call it from a click handler — browsers only open a file\n * dialog in response to a user gesture. */\n open: () => void;\n /** Screen and deliver files that arrived some other way — a drop, a paste. Honours\n * `multiple` (only the first file without it) and every check. */\n take: (files: ArrayLike<File> | null | undefined) => void;\n /** The hidden input and the live region. Render it once, anywhere — it takes no\n * space and needs no positioned ancestor. */\n element: ReactElement;\n}\n\n/**\n * The headless half of {@link FileButton}: a hidden file input you can open from any\n * control, with the reset, the screening and the announcement built in.\n *\n * ```tsx\n * const picker = useFilePicker({ accept: \".pdf\", onFiles: ([f]) => upload(f) });\n * return <>{picker.element}<DetailTableCard onAdd={picker.open} … /></>;\n * ```\n */\nexport function useFilePicker({\n accept,\n multiple,\n capture,\n maxSize,\n maxFiles,\n isValid,\n invalidMessage,\n onFiles,\n onPick,\n onReject,\n disabled,\n labels: labelsProp,\n}: UseFilePickerOptions): UseFilePickerReturn {\n const inputRef = useRef<HTMLInputElement>(null);\n const labels = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS, labelsProp);\n const fileText = useKitFileLabels();\n // Assertive: a refusal is a failure the user has to hear before they move on, and\n // it is the only thing this region ever says.\n const { announce, regionProps } = useAnnounce({ politeness: \"assertive\" });\n\n const take = (list: ArrayLike<File> | null | undefined) => {\n if (disabled || !list || list.length === 0) return;\n const all = Array.from(list);\n const files = multiple ? all : all.slice(0, 1);\n const { accepted, rejected } = screenFiles(\n files,\n // A single-file picker holds one file by definition; `maxFiles` is a multi-mode\n // cap and would only ever say \"0\" there if a caller passed it by mistake.\n { accept, maxSize, maxFiles: multiple ? maxFiles : undefined, isValid, invalidMessage },\n labels,\n fileText.size,\n );\n if (!judgePick(onPick, accepted, rejected)) {\n if (rejected.length > 0) onReject?.(rejected);\n announce(labels.rejectedPick(files.length));\n return;\n }\n if (accepted.length > 0) onFiles?.(accepted);\n if (rejected.length > 0) {\n onReject?.(rejected);\n announce(summariseRejections(rejected, labels));\n }\n };\n\n const element = (\n <>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n capture={capture}\n disabled={disabled}\n // `hidden` (display: none), not `sr-only`: the control a user reaches is the\n // button, and a second, nameless tab stop for the same action is noise. A\n // display:none file input still opens on `.click()` in every current browser —\n // and, unlike `sr-only`, it cannot escape to the initial containing block and\n // stretch the page (see the note in `file-dropzone.tsx`).\n className=\"hidden\"\n tabIndex={-1}\n aria-hidden\n onChange={(e) => {\n // Copy BEFORE the reset: `files` is a live view of the input's value, and\n // clearing the value empties it.\n const picked = Array.from(e.currentTarget.files ?? []);\n e.currentTarget.value = \"\";\n take(picked);\n }}\n />\n <span {...regionProps} />\n </>\n );\n\n return {\n open: () => {\n if (!disabled) inputRef.current?.click();\n },\n take,\n element,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* filtered away). Calling `.focus()` on a detached node silently drops focus to
|
|
21
21
|
* `<body>`, which for a keyboard user means losing their place in the page
|
|
22
22
|
* entirely. When the target is gone, focus falls back to the nearest thing that
|
|
23
|
-
* still exists rather than to nothing
|
|
23
|
+
* still exists rather than to nothing — the kit's one rule for it, lib/focus-left's
|
|
24
|
+
* `returnFocus` (0.52 code review 2026-10-31, held to 0.55; see the cleanup). Until
|
|
25
|
+
* then this said so and did not: a gone opener left the focus on `<body>`.
|
|
24
26
|
* - **Leaves an app's own autofocus alone** (0.33.1). React honours a child's
|
|
25
27
|
* `autoFocus` while it commits, and this hook's effect runs AFTER that commit, so
|
|
26
28
|
* focusing the container there took focus straight back off the field the app had
|
|
@@ -36,8 +38,22 @@ export { firstTabStop, tabStops } from "../lib/focus-left.js";
|
|
|
36
38
|
export interface FocusTrapOptions {
|
|
37
39
|
/** Trap only while this is true. Mount-time `true` is the common case. */
|
|
38
40
|
active?: boolean;
|
|
39
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Give the focus back as the trap is released — the kit's one rule, lib/focus-left's
|
|
43
|
+
* `returnFocus` (0.52 code review 2026-10-31, held to 0.55): only a focus that was lost
|
|
44
|
+
* or is still inside the container is moved; to the {@link opener}, else to what had
|
|
45
|
+
* the focus as the trap engaged, else to the tab stops that stood around that, nearest
|
|
46
|
+
* first; never to `<body>`. Default `true`.
|
|
47
|
+
*/
|
|
40
48
|
restoreFocus?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The control this overlay was opened from, when it knows it — its trigger. The focus
|
|
51
|
+
* goes back there first, and to what had it as the trap engaged only when the trigger
|
|
52
|
+
* cannot take it. Read as the trap is released. A Popover names its trigger: on Safari
|
|
53
|
+
* (and Firefox on macOS) a click does not focus the button it lands on, so "what had the
|
|
54
|
+
* focus" was the page, and the focus fell to `<body>` on close (0.55).
|
|
55
|
+
*/
|
|
56
|
+
opener?: () => Element | null;
|
|
41
57
|
/**
|
|
42
58
|
* What to focus when the trap engages. Default is the container itself, which is
|
|
43
59
|
* what `Modal` does and why: focusing the first FIELD pops the software keyboard on
|
|
@@ -54,4 +70,4 @@ export interface FocusTrapOptions {
|
|
|
54
70
|
*/
|
|
55
71
|
initialFocus?: "container" | "first" | (() => HTMLElement | null);
|
|
56
72
|
}
|
|
57
|
-
export declare function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, { active, restoreFocus, initialFocus }?: FocusTrapOptions): void;
|
|
73
|
+
export declare function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, { active, restoreFocus, initialFocus, opener: namedOpener }?: FocusTrapOptions): void;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import { tabStops } from "../lib/focus-left.js";
|
|
3
|
+
import { returnFocus, tabStops, tabStopsAround } from "../lib/focus-left.js";
|
|
4
4
|
import { firstTabStop, tabStops as tabStops2 } from "../lib/focus-left.js";
|
|
5
5
|
const TRAP_ATTR = "data-focus-trap";
|
|
6
6
|
function focusedElement() {
|
|
7
7
|
return typeof document === "undefined" ? null : document.activeElement;
|
|
8
8
|
}
|
|
9
|
-
function useFocusTrap(containerRef, { active = true, restoreFocus = true, initialFocus = "container" } = {}) {
|
|
10
|
-
const optsRef = useRef({ active, restoreFocus, initialFocus });
|
|
9
|
+
function useFocusTrap(containerRef, { active = true, restoreFocus = true, initialFocus = "container", opener: namedOpener } = {}) {
|
|
10
|
+
const optsRef = useRef({ active, restoreFocus, initialFocus, opener: namedOpener });
|
|
11
11
|
useLayoutEffect(() => {
|
|
12
|
-
optsRef.current = { active, restoreFocus, initialFocus };
|
|
12
|
+
optsRef.current = { active, restoreFocus, initialFocus, opener: namedOpener };
|
|
13
13
|
});
|
|
14
14
|
const [opener, setOpener] = useState(void 0);
|
|
15
15
|
if (active && opener === void 0) setOpener(focusedElement());
|
|
@@ -30,6 +30,11 @@ function useFocusTrap(containerRef, { active = true, restoreFocus = true, initia
|
|
|
30
30
|
// inner panel's last control wraps to the OUTER panel's first.
|
|
31
31
|
(el) => !inNestedTrap(el)
|
|
32
32
|
);
|
|
33
|
+
const around = [];
|
|
34
|
+
if (optsRef.current.restoreFocus && opener instanceof Element && opener.isConnected && opener !== document.body) {
|
|
35
|
+
const { after, before } = tabStopsAround(opener, 3);
|
|
36
|
+
for (const stop of [...after, ...before]) if (!container.contains(stop)) around.push(stop);
|
|
37
|
+
}
|
|
33
38
|
const { initialFocus: how } = optsRef.current;
|
|
34
39
|
const focused = document.activeElement;
|
|
35
40
|
const childHasFocus = focused !== container && container.contains(focused);
|
|
@@ -64,7 +69,12 @@ function useFocusTrap(containerRef, { active = true, restoreFocus = true, initia
|
|
|
64
69
|
container.removeAttribute(TRAP_ATTR);
|
|
65
70
|
if (container.isConnected && optsRef.current.active) return;
|
|
66
71
|
if (!optsRef.current.restoreFocus) return;
|
|
67
|
-
|
|
72
|
+
const named = optsRef.current.opener?.();
|
|
73
|
+
returnFocus({
|
|
74
|
+
opener: named ?? opener,
|
|
75
|
+
fallback: [named ? opener : null, ...around],
|
|
76
|
+
closing: container.isConnected ? container : null
|
|
77
|
+
});
|
|
68
78
|
};
|
|
69
79
|
}, [active, containerRef, opener]);
|
|
70
80
|
}
|