@eifi1/ui-kit 0.51.7 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -23
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +20 -8
- package/dist/billing/cancel-subscription.js +7 -5
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +19 -7
- package/dist/billing/plan-switch.js +8 -4
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -0
- package/dist/components/amount-input.js +1 -0
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +111 -2
- package/dist/components/combobox-core.js +148 -42
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +141 -130
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +12 -7
- package/dist/components/data-table-mobile.js +43 -30
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +13 -4
- package/dist/components/data-table.js +139 -36
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +148 -140
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +3 -2
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.js +3 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +3 -3
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +19 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.js +5 -14
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +29 -0
- package/dist/lib/text-match.js +20 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +13 -3
- package/dist/tour/tour.js +59 -781
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +10 -6
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +1 -5
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +4 -1
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-subscription.tsx +26 -12
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +26 -12
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +18 -7
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +293 -62
- package/src/components/combobox.tsx +183 -153
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +61 -26
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +239 -68
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +197 -153
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +10 -4
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +6 -1
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-select.tsx +21 -9
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +3 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +2 -2
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +32 -3
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +5 -17
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +57 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +108 -1202
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -7,8 +7,8 @@ import { CopyButton } from "../components/copy-button.js";
|
|
|
7
7
|
import { TextLink } from "../components/text-link.js";
|
|
8
8
|
import { toast } from "../components/toast.js";
|
|
9
9
|
import { Button } from "../components/button.js";
|
|
10
|
-
import { useKitLabels
|
|
11
|
-
import {
|
|
10
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
11
|
+
import { useDayFormatter } from "../lib/day-format.js";
|
|
12
12
|
import { DEFAULT_MAX_ATTACHMENT_BYTES } from "./feedback-attachment.js";
|
|
13
13
|
import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "./feedback-dialog.js";
|
|
14
14
|
import {
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
splitBodyAttachments,
|
|
33
33
|
splitDescription
|
|
34
34
|
} from "./feedback-record.js";
|
|
35
|
+
import { runMutation } from "./feedback-mutation.js";
|
|
36
|
+
import { feedbackPage, feedbackPageLinkTarget } from "./feedback-page-link.js";
|
|
35
37
|
import { useFeedbackStatusUndo } from "./feedback-status-undo.js";
|
|
36
38
|
import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels.js";
|
|
37
39
|
import { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from "./feedback-replies.js";
|
|
@@ -42,28 +44,14 @@ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels
|
|
|
42
44
|
function useFeedbackDetailLabels(labels) {
|
|
43
45
|
return useKitLabels("feedbackDetail", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);
|
|
44
46
|
}
|
|
45
|
-
function parsePage(context) {
|
|
46
|
-
const raw = (context?.url || context?.route || "").trim();
|
|
47
|
-
if (!raw) return null;
|
|
48
|
-
try {
|
|
49
|
-
const url = new URL(raw, "http://localhost");
|
|
50
|
-
return url.protocol === "http:" || url.protocol === "https:" ? url : null;
|
|
51
|
-
} catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const localPath = (pathname) => pathname.replace(/^\/{2,}/, "/");
|
|
56
47
|
function feedbackPageHref(context) {
|
|
57
|
-
|
|
58
|
-
return url ? `${localPath(url.pathname)}${url.search}${url.hash}` : null;
|
|
48
|
+
return feedbackPage(context)?.href ?? null;
|
|
59
49
|
}
|
|
60
50
|
function feedbackPagePath(context) {
|
|
61
|
-
|
|
62
|
-
return url ? localPath(url.pathname) : null;
|
|
51
|
+
return feedbackPage(context)?.path ?? null;
|
|
63
52
|
}
|
|
64
53
|
function feedbackPageUrl(context) {
|
|
65
|
-
|
|
66
|
-
return (context?.url || context?.route || "").trim();
|
|
54
|
+
return feedbackPage(context)?.url ?? null;
|
|
67
55
|
}
|
|
68
56
|
function feedbackAttachmentUrls(row) {
|
|
69
57
|
const urls = [
|
|
@@ -155,7 +143,7 @@ function FeedbackReworkSection({
|
|
|
155
143
|
return;
|
|
156
144
|
}
|
|
157
145
|
if (body === null) return;
|
|
158
|
-
onUpdate
|
|
146
|
+
runMutation(onUpdate, { id: row.id, body }, () => onSent?.());
|
|
159
147
|
};
|
|
160
148
|
return /* @__PURE__ */ jsx(FeedbackDetailSection, { title: labels.reworkTitle, children: /* @__PURE__ */ jsx(
|
|
161
149
|
FeedbackNoteEditor,
|
|
@@ -194,6 +182,7 @@ function FeedbackRowDetail({
|
|
|
194
182
|
onCaptureScreenshot,
|
|
195
183
|
reworkAttachment,
|
|
196
184
|
renderDate,
|
|
185
|
+
pageLinkExclude,
|
|
197
186
|
commit,
|
|
198
187
|
showSubject = true,
|
|
199
188
|
onReply,
|
|
@@ -207,7 +196,6 @@ function FeedbackRowDetail({
|
|
|
207
196
|
}) {
|
|
208
197
|
const labels = useFeedbackDetailLabels(labelsProp);
|
|
209
198
|
const dialogText = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS);
|
|
210
|
-
const locale = useKitLocale();
|
|
211
199
|
const ownStatusChange = useFeedbackStatusUndo(onUpdate);
|
|
212
200
|
const changeStatus = statusChange ?? ownStatusChange;
|
|
213
201
|
const [editingBody, setEditingBody] = useState(false);
|
|
@@ -215,17 +203,18 @@ function FeedbackRowDetail({
|
|
|
215
203
|
const access = feedbackRowAccess(row, { canEdit, viewerId });
|
|
216
204
|
const bodyText = splitBodyAttachments(row.body).text;
|
|
217
205
|
const { description } = splitDescription(row.body);
|
|
218
|
-
const
|
|
219
|
-
const
|
|
206
|
+
const page = feedbackPage(row.context);
|
|
207
|
+
const pageLink = page && feedbackPageLinkTarget(page, pageLinkExclude);
|
|
220
208
|
const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: void 0 } : row);
|
|
221
209
|
const fetchFile = fetcher ?? plainFeedbackFetch;
|
|
222
|
-
const
|
|
210
|
+
const day = useDayFormatter(renderDate);
|
|
211
|
+
const dateText = (iso) => day(iso);
|
|
223
212
|
const saveDescription = (value) => {
|
|
224
213
|
if (value.trim() === description.trim()) {
|
|
225
214
|
setEditingBody(false);
|
|
226
215
|
return;
|
|
227
216
|
}
|
|
228
|
-
onUpdate
|
|
217
|
+
runMutation(onUpdate, { id: row.id, body: replaceDescription(row.body, value) }, () => setEditingBody(false));
|
|
229
218
|
};
|
|
230
219
|
const replies = row.replies;
|
|
231
220
|
const commentLabels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS);
|
|
@@ -271,10 +260,23 @@ function FeedbackRowDetail({
|
|
|
271
260
|
) : /* @__PURE__ */ jsx(FeedbackProse, { children: bodyText.trim() ? bodyText : void 0 })
|
|
272
261
|
}
|
|
273
262
|
),
|
|
274
|
-
|
|
263
|
+
page && // The page it was filed on, with the full URL a click away (keksdose #278). Only
|
|
264
|
+
// the path is a link; the query and hash, the reporter's to write, are text — and
|
|
265
|
+
// a one-shot path is text too (FLW-7, 0.52 code review: see feedback-page-link).
|
|
275
266
|
/* @__PURE__ */ jsx(FeedbackDetailSection, { title: labels.url, children: /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
276
|
-
/* @__PURE__ */
|
|
277
|
-
|
|
267
|
+
/* @__PURE__ */ jsxs(
|
|
268
|
+
"code",
|
|
269
|
+
{
|
|
270
|
+
className: "min-w-0 break-all text-xs text-secondary",
|
|
271
|
+
"data-feedback-page": "",
|
|
272
|
+
"data-feedback-page-unlinked": pageLink ? void 0 : "",
|
|
273
|
+
children: [
|
|
274
|
+
pageLink ? /* @__PURE__ */ jsx(TextLink, { href: pageLink, tone: "secondary", underline: "hover", stopPropagation: true, children: page.path }) : page.path,
|
|
275
|
+
page.href.slice(page.path.length)
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
),
|
|
279
|
+
/* @__PURE__ */ jsx(CopyButton, { size: "xs", className: "shrink-0", text: page.url, label: labels.copyUrl, stopPropagation: true })
|
|
278
280
|
] }) }),
|
|
279
281
|
files.length > 0 && /* @__PURE__ */ jsx(FeedbackDetailSection, { title: labels.attachment, children: /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-wrap items-start gap-2", children: files.map(
|
|
280
282
|
(url) => isImageAttachment(url) ? /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-row-detail.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { AuthedImage } from \"../components/authed-image\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { TextLink } from \"../components/text-link\";\nimport { toast } from \"../components/toast\";\nimport { Button } from \"../components/button\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { formatDate } from \"../lib/format\";\nimport { DEFAULT_MAX_ATTACHMENT_BYTES } from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackNoteEditor,\n FeedbackProse,\n FeedbackStatusTransitions,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-inbox\";\nimport type { FeedbackNoteAttachment, FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_REWORKABLE_STATUSES,\n appendRework,\n isImageAttachment,\n replaceDescription,\n splitBodyAttachments,\n splitDescription,\n} from \"./feedback-record\";\nimport type { FeedbackContext, FeedbackRecord } from \"./feedback-record\";\nimport { useFeedbackStatusUndo } from \"./feedback-status-undo\";\nimport type { FeedbackStatusChange } from \"./feedback-status-undo\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\nimport { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from \"./feedback-replies\";\nimport { FeedbackAttachmentDownload, plainFeedbackFetch } from \"./feedback-attachment-download\";\nimport { FeedbackPublicSwitch } from \"./feedback-public-switch\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * One report, opened — the panel a feedback table unfolds under its row (§4.4 of\n * docs/feedback-harmonization.md), lifted from keksdose's `FeedbackRow`\n * (`frontend/src/features/feedback/feedback-page.tsx:329-612`) with the contract's\n * changes applied.\n *\n * Until 0.27 each app wrote this panel itself out of the kit's shell\n * (`FeedbackDetail` / `FeedbackDetailSection` / `FeedbackNoteEditor`), and the three\n * copies had drifted in exactly the places a user notices:\n *\n * - **the description editor held the raw body** — keksdose `initial={fb.body}`\n * (`:420`), kastlan the same (`pages/feedback-page.tsx:226`) — so an author fixing a\n * typo could delete the `--- REWORK … ---` rounds and the `[screenshot]` file lines\n * with it, and the history was gone. Here the editor holds the ORIGINAL description\n * only ({@link splitDescription}) and saves through {@link replaceDescription}, which\n * puts every appended block and file line back unchanged;\n * - **kastlan's rework sent `status: \"OPEN\"`** from the client (`:189-195`) and offered it\n * on three statuses; keksdose sends the body alone and lets the server reopen the row\n * on five (live #331). The contract is keksdose's (§3.4) — {@link appendRework}, one\n * file, the server reopens;\n * - **kastlan showed only `screenshot_url`** (`:265-276`) and no downloads; keksdose the\n * screenshot, the other files and the rework pictures, a PDF as a download (dev #578);\n * - **the URL fell away on Kurvenschmiede's rows**, whose backfilled `context.url` is a\n * path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section\n * vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).\n *\n * The sections, in the contract's order: **Subject** (the category and the title, §8.1;\n * `showSubject={false}` leaves it out), **Description** (the body; the author's\n * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Replies** (0.38,\n * §9.7: the timeline, the admin's reply composer with its status picker and the nudge,\n * Rework — on a row without `replies`, a server before 0.38, the frozen\n * **Outcome** read-only in its place), **Comments** (0.38: the app's\n * `FeedbackCommentThread`, when passed), **Send for rework** (only while reworking) and\n * **Status** (with the **Visible to others** switch when `onPublicChange` is passed).\n *\n * The app keeps its data and its API: the row it renders, the PATCH (`onUpdate`), the\n * upload (`onUpload`), the authenticated GET (`fetcher`) and who is looking (`canEdit`,\n * `viewerId`). Every word is the kit's `feedbackDetail` namespace.\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/**\n * `feedbackDetail` — the row detail's words (§4.4). English: keksdose's `en.json`\n * (`feedback.*`, `common.*`, `more.copy_url`) with the contract's changes; the de-CH\n * canon (keksdose `de-CH.json`, ss never ß, \"Nacharbeit\" throughout — §7.14) on each key.\n */\nexport interface FeedbackDetailLabels {\n /** The first section's heading: what the report is called (feedback contract §8.1,\n * keksdose live #395 — with the row expanded and the table scrolled, nothing said\n * which entry was being answered). de-CH: \"Betreff\" */\n subject: string;\n /** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks\n * \"What happened? (optional)\"; the description, once filed, is headed as what it is\n * — and as the author's \"Edit description\" calls it. de-CH: \"Beschreibung\" */\n body: string;\n /** The author's button on that heading. de-CH: \"Bearbeiten\" */\n edit: string;\n /** Its accessible name — says WHAT is edited; contains the visible word.\n * de-CH: \"Beschreibung bearbeiten\" */\n editDescription: string;\n /** The editors' save button (description, outcome). de-CH: \"Speichern\" */\n save: string;\n /** Every editor's cancel button. de-CH: \"Abbrechen\" */\n cancel: string;\n /** Section 2's heading. de-CH: \"URL\" */\n url: string;\n /** The copy button beside the link — copies the FULL `context.url`.\n * de-CH: \"URL kopieren\" */\n copyUrl: string;\n /** Section 3's heading, and each picture's alt text. de-CH: \"Anhang\" */\n attachment: string;\n /** A PDF's / text file's download button, given the file's name.\n * de-CH: \"{{name}} herunterladen\" */\n download: (name: string) => string;\n /** The toast when that download failed. de-CH: \"Der Anhang konnte nicht\n * heruntergeladen werden.\" */\n downloadFailed: string;\n /** Section 4's heading on a row without `replies` (a server before 0.38), where the\n * frozen outcome is shown read-only. de-CH: \"Ergebnis\" */\n outcome: string;\n /** Section 4's heading since 0.38: the replies' timeline (§9.7). de-CH: \"Antworten\" */\n replies: string;\n /** On section 4's heading line once the row is settled, given the formatted\n * `resolved_at` date. de-CH: \"Erledigt am: {{date}}\" */\n resolvedAt: (date: string) => string;\n /** The admin's button while there is no outcome. **Unused since 0.38** (the outcome is\n * frozen, §9.7; a reply is written instead); kept for one release.\n * de-CH: \"Ergebnis hinzufügen\" */\n outcomeAdd: string;\n /** The same button once there is one. Unused since 0.38, as `outcomeAdd`.\n * de-CH: \"Aktualisieren\" */\n outcomeUpdate: string;\n /** The line above the outcome editor. Unused since 0.38, as `outcomeAdd`. de-CH: \"Was wurde umgesetzt, entschieden oder\n * warum nicht.\" */\n outcomePlaceholder: string;\n /** The button that opens section 5 on an answered row. de-CH: \"Nacharbeit\" */\n rework: string;\n /** Section 5's heading. de-CH: \"Zur Nacharbeit senden\" */\n reworkTitle: string;\n /** Section 5's send button. de-CH: \"Nacharbeit senden\" */\n reworkSend: string;\n /** The line above the rework note. de-CH: \"Was muss noch angepasst werden? Neue\n * Anforderungen oder Richtungswechsel.\" */\n reworkPlaceholder: string;\n /** The toast when the rework's file did not upload — and so nothing was sent. Names\n * no screenshot: it fires for a PDF or a log too (§7.14; keksdose's said\n * \"screenshot\"). de-CH: \"Die Datei konnte nicht hochgeladen werden. Die Nacharbeit\n * wurde nicht gesendet.\" */\n reworkUploadFailed: string;\n /** Section 6's heading. de-CH: \"Status\" */\n status: string;\n /** A `FeedbackNoteEditor`'s box, named when no `placeholder` line names it and the\n * caller gives no `label` — without it the box was read out as just \"edit text\"\n * (round 0.34, the showcase's axe check). de-CH: \"Notiz\" */\n note: string;\n}\n\n// Defined in ./feedback-row-detail-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DETAIL_LABELS };\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: LabelOverride<FeedbackDetailLabels>): FeedbackDetailLabels {\n return useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);\n}\n\n/* ── The page a report was filed on (§3.2, §4.4.2) ────────────────────────── */\n\n/** `context.url`, else `context.route` — a parsed URL, or null when neither is set or\n * the value is no http(s) address or path (a `javascript:` value is not a page). */\nfunction parsePage(context: FeedbackContext | null | undefined): URL | null {\n const raw = (context?.url || context?.route || \"\").trim();\n if (!raw) return null;\n try {\n // A base, so a PATH-ONLY value parses too: Kurvenschmiede backfills its old rows\n // with `url = page_path + search` (§3.2), and keksdose's `new URL(raw)` threw on\n // those and dropped the section.\n const url = new URL(raw, \"http://localhost\");\n return url.protocol === \"http:\" || url.protocol === \"https:\" ? url : null;\n } catch {\n return null;\n }\n}\n\n/** Collapses a leading `//` run: `/\\/\\/evil.example/` as a path would be a\n * protocol-relative link off the app. */\nconst localPath = (pathname: string) => pathname.replace(/^\\/{2,}/, \"/\");\n\n/**\n * The in-app link to the page a report was filed on — path + query + hash (keksdose\n * `localHref`: the query and hash carry the state worth restoring, `?tab=import`, a\n * drill-down). Read from `context.url`, else `context.route`; a path-only `url` works.\n * Always a path on THIS app, never another origin's address, so it is safe as an\n * `href` whatever the row says. `null` when there is nothing to link.\n */\nexport function feedbackPageHref(context: FeedbackContext | null | undefined): string | null {\n const url = parsePage(context);\n return url ? `${localPath(url.pathname)}${url.search}${url.hash}` : null;\n}\n\n/** The page's path alone — the compact text a URL column shows (§4.3). */\nexport function feedbackPagePath(context: FeedbackContext | null | undefined): string | null {\n const url = parsePage(context);\n return url ? localPath(url.pathname) : null;\n}\n\n/**\n * What \"Copy URL\" copies: the FULL `context.url` as the reporter's browser had it —\n * origin included, which is the one part that says which copy of the app it was\n * (feedback #278) — else `context.route`. `null` when there is no page.\n */\nexport function feedbackPageUrl(context: FeedbackContext | null | undefined): string | null {\n if (!parsePage(context)) return null;\n return (context?.url || context?.route || \"\").trim();\n}\n\n/* ── Attachments (§3.5, §4.4.3) ───────────────────────────────────────────── */\n\n/**\n * Every file a report carries, in the order the detail shows them (§4.4.3): the\n * screenshot it was filed with, then its other files, then whatever the rework rounds\n * brought, in body order (keksdose dev #578, live #291). One list rather than three —\n * a reader looking for \"the picture\" should not have to know at which point in the\n * conversation it was taken.\n *\n * Each URL once: keksdose and Kurvenschmiede content-address their keys, so the same\n * screenshot pasted twice is one URL — shown twice it is the same picture twice (and,\n * keyed by URL, a React key clash keksdose had).\n */\nexport function feedbackAttachmentUrls(\n row: Pick<FeedbackRecord, \"screenshot_url\" | \"attachment_urls\" | \"body\"> & Partial<Pick<FeedbackRecord, \"replies\">>,\n): string[] {\n const urls = [\n ...(row.screenshot_url ? [row.screenshot_url] : []),\n // `?? []`: NULL on rows filed before the column, and no key at all from a server\n // that predates it (keksdose dev #578).\n ...(row.attachment_urls ?? []),\n ...splitBodyAttachments(row.body).urls,\n // 0.40 (§10.3): a rework entry's one file, in entry order — where the rework\n // pictures of the body were before.\n ...(row.replies ?? []).flatMap((reply) =>\n reply.kind === \"rework\" && reply.attachment_url ? [reply.attachment_url] : [],\n ),\n ];\n return [...new Set(urls)];\n}\n\n/* ── Who may do what (§3.4, §4.3, §4.4) ───────────────────────────────────── */\n\n/** What the person looking at a row may do with it — {@link feedbackRowAccess}. */\nexport interface FeedbackRowAccess {\n /** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */\n isAuthor: boolean;\n /** \"Edit description\": the author, while the row is OPEN, READY or IN_PROGRESS — on either\n * page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */\n canEditDescription: boolean;\n /** The admin on the inbox (`canEdit`): since 0.38 the reply composer (the outcome\n * itself is frozen, §9.7). */\n canEditOutcome: boolean;\n /** \"Rework\": the author, on an answered row ({@link FEEDBACK_REWORKABLE_STATUSES}).\n * Since 0.40 (§10.2 decision 6) the author's only — an admin sends a report back with\n * a reply whose status is `REWORK`; the server answers anyone else 403. */\n canRework: boolean;\n /** The status pills: the admin on the inbox; everyone else sees them read-only. */\n canChangeStatus: boolean;\n}\n\n/**\n * The detail's permission rules in one place — the same answers the server gives\n * (§3.4), so the panel never offers what the PATCH would refuse.\n *\n * `canEdit` is the app's \"admin on `/feedback`\": an admin on `/my-feedback` edits no\n * status and no outcome there (§4.3), but as the author still edits the description\n * and sends for rework. `viewerId` is the signed-in user's id.\n */\nexport function feedbackRowAccess(\n row: Pick<FeedbackRecord, \"user_id\" | \"status\">,\n { canEdit, viewerId }: { canEdit: boolean; viewerId?: number | null },\n): FeedbackRowAccess {\n // `user_id: null` is an erased author (keksdose live #275): nobody is that author,\n // least of all a viewer whose id happens to be missing too.\n const isAuthor = row.user_id !== null && viewerId !== undefined && viewerId !== null && viewerId === row.user_id;\n return {\n isAuthor,\n canEditDescription: isAuthor && FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status),\n canEditOutcome: canEdit,\n canRework: isAuthor && FEEDBACK_REWORKABLE_STATUSES.includes(row.status),\n canChangeStatus: canEdit,\n };\n}\n\n/* ── The PATCH ─────────────────────────────────────────────────────────────── */\n\n/** What the detail sends as `PATCH /feedback/{id}` (§3.4): the body (a description\n * edit, a rework append) or the outcome — and, through {@link useFeedbackStatusUndo}\n * when no `statusChange` is given, the status. Never two at once: a rework PATCH must\n * carry the body ALONE for the server to read it as an append (keksdose live #331). */\nexport interface FeedbackDetailPatch {\n id: number;\n body?: string;\n /** `null` clears it — a real edit. **Frozen since 0.38** (§9.7): server-kit 0.11\n * answers 422; the detail no longer sends it (a reply is written instead). */\n outcome?: string | null;\n status?: FeedbackStatus;\n /** 0.38.1: the **Visible to others** flip (§9.3) — what `onPublicChange`'s\n * `PATCH /feedback/{id} {\"public\": next}` carries, alone, so an app can send it\n * through the same `onUpdate` mutation. */\n public?: boolean;\n}\n\n/**\n * The page's update mutation. **TanStack Query's `mutate` fits as it stands** — pass\n * `update.mutate` (and `pending={update.isPending}`), the same function\n * `useFeedbackStatusUndo` takes. A hand-written one sends the PATCH and calls\n * `onSuccess` once it has landed, and only then: that is when an editor closes. A\n * failure is the mutation's own to toast — the server's `detail`, else\n * `feedbackToast.updateFailed` (§4.4) — and leaves the editor open with its draft.\n */\nexport type FeedbackDetailUpdate = (patch: FeedbackDetailPatch, callbacks: { onSuccess: () => void }) => void;\n\n/** `POST /feedback/attachments` (§3.3): one file up, its URL\n * (`/api/v1/feedback/attachments/<key>`) back. Reject on failure. */\nexport type FeedbackUpload = (file: File) => Promise<string>;\n\n/* ── Send for rework (§4.4.5) ─────────────────────────────────────────────── */\n\n/** 0.40 (§10.3): `POST /feedback/{id}/rework` — the trimmed note and the uploaded file's\n * URL. Return the promise: the section closes when it resolves and keeps the note when\n * it rejects (the toast is the app's: the server's `detail`, else\n * `feedbackToast.updateFailed`). */\nexport type FeedbackReworkSend = (note: string, attachmentUrl?: string) => void | Promise<unknown>;\n\nexport interface FeedbackReworkSectionProps {\n /** The row being sent back: its id, and the body the round is appended to. */\n row: Pick<FeedbackRecord, \"id\" | \"body\">;\n /** 0.40: the rework as an entry of its own — see {@link FeedbackReworkSend}. With it,\n * `onUpdate` is not called. */\n onRework?: FeedbackReworkSend;\n /** The PATCH — see {@link FeedbackDetailUpdate}. Called with `{ id, body }` only, and\n * only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still\n * turns into an entry for one release. Optional since 0.40; pass one of the two. */\n onUpdate?: FeedbackDetailUpdate;\n /** The PATCH is in flight (`update.isPending`). The upload adds its own. */\n pending?: boolean;\n /** The upload. Left out, the note goes without a file field. */\n onUpload?: FeedbackUpload;\n /** Snapshot the app view as a File — the kit's `captureAppScreenshot`, or the app's\n * own. A \"Capture screenshot\" button appears only with it; a rejection (no\n * `modern-screenshot`, a canvas that would not draw) toasts\n * `feedbackToast.captureFailed` and adds nothing. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The round landed — the server has the note and, on an answered row, reopened it. */\n onSent?: () => void;\n onCancel: () => void;\n /** Over the field's defaults: the contract's types and 10 MB (§3.5), the\n * `feedbackToast` refusals, the `feedbackAttachment` words. */\n attachment?: Partial<FeedbackNoteAttachment>;\n /** The send COMMITS — see `FeedbackNoteEditor`'s `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter sends an answered report\n * back with a note and at most one file, and the server reopens it (§3.4, keksdose\n * `submitRework`, `feedback-page.tsx:350`).\n *\n * - **The note is required** and trimmed: Send stays disabled on a blank box (a file\n * alone cannot be sent; keksdose `:577` dropped that click silently).\n * - **One file**, added, captured or pasted — the dialog's types and 10 MB. It is\n * uploaded FIRST and awaited: a body naming a file that never arrived would be a\n * rework whose evidence is missing, and the upload is the half that fails on its\n * own. If it fails nothing is sent and the toast says so; the note stays in the box.\n * - **0.40: an entry of its own** (§10.3) — with `onRework`, the note and the file's URL\n * go to `POST /feedback/{id}/rework` and the body is not touched. Without it, the\n * legacy append below, which server-kit 0.13 still converts (one minor).\n * - **The body alone goes in the PATCH** — {@link appendRework}'s strict extension of\n * the current body, which is how the server recognises the append and reopens the\n * row. Never a `status` (kastlan's client sent `OPEN`; an author may not write one,\n * so the server refused the whole PATCH, note and all — keksdose live #331).\n *\n * Exported on its own for an app that offers rework somewhere other than the row\n * detail. Kurvenschmiede's author verdict (\"That is it, done\") is NOT here: an answered\n * item stays answered unless its author sends it back (§7.2).\n */\nexport function FeedbackReworkSection({\n row,\n onRework,\n onUpdate,\n pending = false,\n onUpload,\n onCaptureScreenshot,\n onSent,\n onCancel,\n attachment,\n commit,\n labels: labelsProp,\n}: FeedbackReworkSectionProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n const toasts = useFeedbackToastLabels();\n const [uploading, setUploading] = useState(false);\n const [sending, setSending] = useState(false);\n // The body as it is NOW, not as it was when Send was pressed: the list can refresh\n // while the file uploads, and an append to a stale body is no longer a strict\n // extension of the stored one — the server would read it as an edit, and refuse it.\n const latestBody = useRef(row.body);\n useEffect(() => {\n latestBody.current = row.body;\n }, [row.body]);\n\n const capture = onCaptureScreenshot\n ? async () => {\n try {\n return await onCaptureScreenshot();\n } catch {\n toast.error(toasts.captureFailed);\n return null;\n }\n }\n : undefined;\n\n const send = async (value: string, file?: File | null) => {\n const note = value.trim();\n if (!note) return;\n let url: string | null = null;\n if (file && onUpload) {\n setUploading(true);\n try {\n url = await onUpload(file);\n } catch {\n toast.error(labels.reworkUploadFailed);\n return;\n } finally {\n setUploading(false);\n }\n }\n if (onRework) {\n // 0.40 (§10.3): the entry, not the body — the server checks the URL (a 422 outside\n // the app's attachment prefix).\n setSending(true);\n try {\n await onRework(note, url ?? undefined);\n } catch {\n return; // The app's toast; the note stays in the box.\n } finally {\n setSending(false);\n }\n onSent?.();\n return;\n }\n if (!onUpdate) return;\n let body: string | null;\n try {\n body = appendRework(latestBody.current, note, url);\n } catch {\n // The upload answered with something that is not an attachment URL of this\n // contract — written anyway it would sit in the body as prose no reader knows to\n // be a file. As far as the person is concerned, the file did not arrive.\n toast.error(labels.reworkUploadFailed);\n return;\n }\n if (body === null) return;\n onUpdate({ id: row.id, body }, { onSuccess: () => onSent?.() });\n };\n\n return (\n <FeedbackDetailSection title={labels.reworkTitle}>\n <FeedbackNoteEditor\n key={row.id}\n initial=\"\"\n required\n commit={commit}\n pending={pending || uploading || sending}\n onSave={(value, file) => void send(value, file)}\n onCancel={onCancel}\n saveLabel={labels.reworkSend}\n cancelLabel={labels.cancel}\n placeholder={labels.reworkPlaceholder}\n attachment={\n onUpload\n ? {\n accept: FEEDBACK_ATTACHMENT_ACCEPT,\n maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,\n onError: (kind) => {\n toast.error(kind === \"type\" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);\n },\n onCaptureScreenshot: capture,\n ...attachment,\n }\n : undefined\n }\n />\n </FeedbackDetailSection>\n );\n}\n\n/* ── The detail (§4.4) ─────────────────────────────────────────────────────── */\n\nexport interface FeedbackRowDetailProps {\n /** The report — the list's own row, as `GET /feedback` / `GET /feedback/my` sent it. */\n row: FeedbackRecord;\n /** The admin on the inbox (`/feedback`): status pills and the outcome editor. `false`\n * on `/my-feedback`, for an admin too (§4.3). */\n canEdit: boolean;\n /** The signed-in user's id — who the AUTHOR is: the description editor and rework. */\n viewerId?: number | null;\n /** The page's PATCH — `update.mutate` (see {@link FeedbackDetailUpdate}). */\n onUpdate: FeedbackDetailUpdate;\n /** `update.isPending`: the editors' save buttons wait for it. */\n pending?: boolean;\n /** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with\n * `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go\n * through it, since the attachment route wants the bearer token (§3.5). Left out,\n * the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /**\n * The undoable status change — what `useFeedbackStatusUndo(update.mutate)` returns,\n * the SAME one the table's status cell and the swipes use (§4.5). Left out, the\n * detail builds one from `onUpdate`, which is equivalent as long as `onUpdate` is the\n * page's mutation (its observer outlives the row a status filter removes).\n */\n statusChange?: FeedbackStatusChange;\n /** For the rework's and (0.47, with `replyAttachments`) the reply composer's \"Capture screenshot\" — see\n * {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,\n * over the contract's defaults. */\n reworkAttachment?: Partial<FeedbackNoteAttachment>;\n /** How the `resolved_at` date reads — the app's date cell, as a string. Default: the\n * kit's `formatDate` in the provider's locale, `medium` style. */\n renderDate?: (iso: string) => string;\n /**\n * The detail's saves COMMIT (description, outcome, rework, status): under a locked\n * `WriteLockProvider` each is `aria-disabled` with the lock's reason, and nothing\n * reaches `onUpdate`. Off by default — keksdose's shell lock is a read-only demo\n * BUDGET, and a person viewing one still owns their own reports.\n */\n commit?: CommitScope;\n /**\n * Open with the subject: the category badge and the title (feedback contract §8.1,\n * keksdose live #395). On by default; `false` for a page that already shows the title\n * beside the detail.\n */\n showSubject?: boolean;\n /**\n * 0.38: `POST /feedback/{id}/replies` (§9.7) — the body and the status picked\n * (`undefined`: \"No change\"). With `canEdit` and a row that carries `replies`, the\n * reply composer stands under the timeline. Return the promise: the box clears when it\n * resolves. A failure is the app's to toast — the server's `detail`, else\n * `feedbackToast.replyFailed` (0.38.1).\n *\n * 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the\n * composer takes one file as the rework note does — pasted into the box, captured or\n * picked — uploaded first, and its URL is the third argument (`attachment_url` in the\n * body; server-kit 0.18).\n */\n onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;\n /**\n * 0.47 (§12): the reply composer takes one file — through `onUpload`,\n * `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:\n * `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of\n * `(body, status)` does not grow a file field whose file its reply would drop (round\n * 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes\n * `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an\n * older server answers the field with a 422).\n */\n replyAttachments?: boolean;\n /** 0.38: the reply is in flight. */\n replying?: boolean;\n /**\n * 0.40 (§10.3): `POST /feedback/{id}/rework` — the author's rework as an entry of its\n * own (see {@link FeedbackReworkSend}). The **Rework** button shows only to the author\n * on an answered row, and only where the reply composer is not shown (§10.2 decision 6:\n * an admin answering a report sends it back with a reply whose status is `REWORK`).\n * Left out, the rework goes through `onUpdate` as the legacy body append — for one\n * minor, while the server converts it.\n */\n onRework?: FeedbackReworkSend;\n /** 0.38: the report's comments — the app's `FeedbackCommentThread` — in a section of\n * their own under the replies. A thread here draws the comments alone (0.38.1): the\n * Replies section above has the replies, so whatever `replies` the thread is handed\n * are dropped, and nothing is drawn twice. */\n comments?: ReactNode;\n /** 0.38: `PATCH /feedback/{id} {\"public\": next}` — draws the **Visible to others**\n * switch (`FeedbackPublicSwitch`) in the Status section, for the admin (`canEdit`)\n * and, within §9.3's window, the author. The page's own mutation fits:\n * `onPublicChange={(next) => update.mutate({ id: row.id, public: next })}` —\n * {@link FeedbackDetailPatch}'s `public` (0.38.1). */\n onPublicChange?: (next: boolean) => void;\n labels?: LabelOverride<FeedbackDetailLabels>;\n className?: string;\n}\n\n/**\n * The expanded row of a feedback table (§4.4): what was reported, where, with which\n * files, what the answer was, and where it stands — with the edits each viewer may\n * make. Hand it to `DataTable`'s `expandedRow`:\n *\n * ```tsx\n * const update = useMutation({ mutationFn: ({ id, ...patch }) => api.updateFeedback(id, patch),\n * onSuccess: invalidate, onError: (e) => toast.error(detailOf(e) ?? toastLabels.updateFailed) });\n * const changeStatus = useFeedbackStatusUndo(update.mutate);\n * <DataTable\n * isExpanded={(row) => expandedId === row.id}\n * expandedRow={(row) => (\n * <FeedbackRowDetail row={row} canEdit={isAdmin && !mine} viewerId={user?.id}\n * onUpdate={update.mutate} pending={update.isPending} statusChange={changeStatus}\n * onUpload={api.uploadFeedbackAttachment} fetcher={authedImageFetcher}\n * onCaptureScreenshot={captureAppScreenshot} />\n * )} … />\n * ```\n *\n * Mounted only while its row is expanded, so which editor is open lives here, and\n * collapsing the row closes it. Each editor is keyed by the row's id, so a draft is\n * never thrown away by a list refresh under it.\n */\nexport function FeedbackRowDetail({\n row,\n canEdit,\n viewerId,\n onUpdate,\n pending = false,\n onUpload,\n fetcher,\n statusChange,\n onCaptureScreenshot,\n reworkAttachment,\n renderDate,\n commit,\n showSubject = true,\n onReply,\n replyAttachments = false,\n replying = false,\n onRework,\n comments,\n onPublicChange,\n labels: labelsProp,\n className,\n}: FeedbackRowDetailProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n // \"placeholder = the dialog's body label\" (§4.4.1): read from the dialog's own\n // namespace, so the two can never say different things.\n const dialogText = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS);\n const locale = useKitLocale();\n const ownStatusChange = useFeedbackStatusUndo(onUpdate);\n const changeStatus = statusChange ?? ownStatusChange;\n const [editingBody, setEditingBody] = useState(false);\n const [reworking, setReworking] = useState(false);\n\n const access = feedbackRowAccess(row, { canEdit, viewerId });\n // The body as written, without its file lines (they are shown as files below) — the\n // rework rounds stay in the prose: they are what was said after the answer.\n const bodyText = splitBodyAttachments(row.body).text;\n const { description } = splitDescription(row.body);\n const pageHref = feedbackPageHref(row.context);\n const pageUrl = feedbackPageUrl(row.context);\n // 0.47 (§12): with the entries, each entry's file is drawn inline in the Replies\n // timeline, so the Attachment section holds the report's own files.\n const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: undefined } : row);\n const fetchFile = fetcher ?? plainFeedbackFetch;\n const dateText = (iso: string) => (renderDate ? renderDate(iso) : formatDate(iso, \"medium\", { locale }));\n\n const saveDescription = (value: string) => {\n // Only a real change makes a round trip; clearing it is one (\"\" — the description\n // is optional, §7.4), and the rounds and file lines go back unchanged.\n if (value.trim() === description.trim()) {\n setEditingBody(false);\n return;\n }\n onUpdate({ id: row.id, body: replaceDescription(row.body, value) }, { onSuccess: () => setEditingBody(false) });\n };\n\n // 0.38 (§9.7): the replies replace the outcome; a row without them is a server before\n // 0.38, whose frozen outcome is shown as it is, read-only.\n const replies = row.replies;\n const commentLabels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS);\n // 0.40 (§10.2 decision 6): whoever has the reply composer answers with it — REWORK is\n // one of its statuses — so the Rework button is for an author without it.\n const composerShown = Boolean(replies && access.canEditOutcome && onReply);\n const canRework = access.canRework && !composerShown;\n const showRework = canRework && reworking;\n\n return (\n <FeedbackDetail className={className}>\n {showSubject && (\n <FeedbackDetailSection title={labels.subject}>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm\" data-feedback-subject=\"\">\n <FeedbackCategoryBadge category={row.category} />\n <span className=\"min-w-0 font-medium text-primary [overflow-wrap:anywhere]\">{row.title}</span>\n </p>\n </FeedbackDetailSection>\n )}\n <FeedbackDetailSection\n title={labels.body}\n action={\n access.canEditDescription && !editingBody ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingBody(true);\n }}\n aria-label={labels.editDescription}\n >\n {labels.edit}\n </Button>\n ) : undefined\n }\n >\n {access.canEditDescription && editingBody ? (\n <FeedbackNoteEditor\n key={row.id}\n // The ORIGINAL description only — see the module note.\n initial={description}\n pending={pending}\n commit={commit}\n onSave={saveDescription}\n onCancel={() => setEditingBody(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={dialogText.bodyOptional ?? dialogText.body}\n />\n ) : (\n <FeedbackProse>{bodyText.trim() ? bodyText : undefined}</FeedbackProse>\n )}\n </FeedbackDetailSection>\n\n {pageHref && pageUrl && (\n // The page it was filed on, with the full URL a click away (keksdose #278).\n <FeedbackDetailSection title={labels.url}>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <TextLink href={pageHref} tone=\"secondary\" underline=\"hover\" stopPropagation className=\"min-w-0\">\n <code className=\"break-all text-xs\">{pageHref}</code>\n </TextLink>\n <CopyButton size=\"xs\" className=\"shrink-0\" text={pageUrl} label={labels.copyUrl} stopPropagation />\n </div>\n </FeedbackDetailSection>\n )}\n\n {files.length > 0 && (\n <FeedbackDetailSection title={labels.attachment}>\n <div className=\"flex min-w-0 flex-wrap items-start gap-2\">\n {files.map((url) =>\n isImageAttachment(url) ? (\n <AuthedImage\n key={url}\n src={url}\n alt={labels.attachment}\n fetcher={fetchFile}\n link\n stopPropagation\n // A broken attachment is simply not in the row (keksdose's\n // AuthedImage default) rather than a grey tile per failure.\n errorFallback={null}\n wrapperClassName=\"inline-block max-w-full\"\n className=\"max-h-64 max-w-full rounded border border-subtle\"\n />\n ) : (\n <FeedbackAttachmentDownload key={url} url={url} fetcher={fetchFile} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={replies ? labels.replies : labels.outcome}\n // When the row entered DONE / WONT_DO (keksdose feedback #96).\n action={\n row.resolved_at ? (\n <span className=\"text-xs text-muted\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n <div className=\"space-y-3\">\n {replies ? (\n <FeedbackReplyTimeline replies={replies} fetcher={fetchFile} />\n ) : (\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n )}\n {composerShown && replies && onReply && (\n <FeedbackReplyComposer\n key={row.id}\n status={row.status}\n onReply={onReply}\n pending={replying}\n onUpload={replyAttachments ? onUpload : undefined}\n onCaptureScreenshot={replyAttachments ? onCaptureScreenshot : undefined}\n attachment={replyAttachments ? reworkAttachment : undefined}\n suggestedStatus={feedbackReplyNudge(row)}\n commit={commit}\n />\n )}\n {/* No \"Open page\" button here since 0.46 (keksdose live #449): the URL section\n above already links the page, with the path on screen. */}\n {canRework && !reworking && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setReworking(true);\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden /> {labels.rework}\n </Button>\n </div>\n )}\n </div>\n </FeedbackDetailSection>\n\n {comments != null && (\n <FeedbackDetailSection title={commentLabels.comments}>\n <FeedbackRepliesDrawnAboveContext.Provider value>{comments}</FeedbackRepliesDrawnAboveContext.Provider>\n </FeedbackDetailSection>\n )}\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\n onRework={onRework}\n onUpdate={onUpdate}\n pending={pending}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n onSent={() => setReworking(false)}\n onCancel={() => setReworking(false)}\n attachment={reworkAttachment}\n commit={commit}\n labels={labelsProp}\n />\n )}\n\n <FeedbackDetailSection title={labels.status}>\n {/* Expanded: the whole enum in one click for the admin (keksdose feedback #421);\n everyone else the narrow set, read-only — a picture of the workflow. */}\n <FeedbackStatusTransitions\n status={row.status}\n statuses={\n access.canChangeStatus ? selectableFeedbackStatuses(row.status) : visibleFeedbackStatuses(row.status)\n }\n canEdit={access.canChangeStatus}\n commit={commit}\n onPick={(status) => changeStatus(row, status)}\n variant=\"pill\"\n />\n {onPublicChange && (\n <FeedbackPublicSwitch\n row={row}\n isAdmin={canEdit}\n viewerId={viewerId}\n onChange={onPublicChange}\n pending={pending}\n commit={commit}\n className=\"mt-3\"\n />\n )}\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-row-detail.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { RotateCcw } from \"lucide-react\";\nimport { AuthedImage } from \"../components/authed-image\";\nimport { CopyButton } from \"../components/copy-button\";\nimport { TextLink } from \"../components/text-link\";\nimport { toast } from \"../components/toast\";\nimport { Button } from \"../components/button\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport type { PathPattern } from \"../landing/routing\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport { DEFAULT_MAX_ATTACHMENT_BYTES } from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog\";\nimport {\n FeedbackCategoryBadge,\n FeedbackDetail,\n FeedbackDetailSection,\n FeedbackNoteEditor,\n FeedbackProse,\n FeedbackStatusTransitions,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-inbox\";\nimport type { FeedbackNoteAttachment, FeedbackStatus } from \"./feedback-inbox\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_AUTHOR_EDITABLE_STATUSES,\n FEEDBACK_REWORKABLE_STATUSES,\n appendRework,\n isImageAttachment,\n replaceDescription,\n splitBodyAttachments,\n splitDescription,\n} from \"./feedback-record\";\nimport type { FeedbackContext, FeedbackRecord } from \"./feedback-record\";\nimport { runMutation, type FeedbackMutationCall } from \"./feedback-mutation\";\nimport { feedbackPage, feedbackPageLinkTarget } from \"./feedback-page-link\";\nimport { useFeedbackStatusUndo } from \"./feedback-status-undo\";\nimport type { FeedbackStatusChange } from \"./feedback-status-undo\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\nimport { FeedbackReplyComposer, FeedbackReplyTimeline, feedbackReplyNudge } from \"./feedback-replies\";\nimport { FeedbackAttachmentDownload, plainFeedbackFetch } from \"./feedback-attachment-download\";\nimport { FeedbackPublicSwitch } from \"./feedback-public-switch\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * One report, opened — the panel a feedback table unfolds under its row (§4.4 of\n * docs/feedback-harmonization.md), lifted from keksdose's `FeedbackRow`\n * (`frontend/src/features/feedback/feedback-page.tsx:329-612`) with the contract's\n * changes applied.\n *\n * Until 0.27 each app wrote this panel itself out of the kit's shell\n * (`FeedbackDetail` / `FeedbackDetailSection` / `FeedbackNoteEditor`), and the three\n * copies had drifted in exactly the places a user notices:\n *\n * - **the description editor held the raw body** — keksdose `initial={fb.body}`\n * (`:420`), kastlan the same (`pages/feedback-page.tsx:226`) — so an author fixing a\n * typo could delete the `--- REWORK … ---` rounds and the `[screenshot]` file lines\n * with it, and the history was gone. Here the editor holds the ORIGINAL description\n * only ({@link splitDescription}) and saves through {@link replaceDescription}, which\n * puts every appended block and file line back unchanged;\n * - **kastlan's rework sent `status: \"OPEN\"`** from the client (`:189-195`) and offered it\n * on three statuses; keksdose sends the body alone and lets the server reopen the row\n * on five (live #331). The contract is keksdose's (§3.4) — {@link appendRework}, one\n * file, the server reopens;\n * - **kastlan showed only `screenshot_url`** (`:265-276`) and no downloads; keksdose the\n * screenshot, the other files and the rework pictures, a PDF as a download (dev #578);\n * - **the URL fell away on Kurvenschmiede's rows**, whose backfilled `context.url` is a\n * path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section\n * vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).\n *\n * The sections, in the contract's order: **Subject** (the category and the title, §8.1;\n * `showSubject={false}` leaves it out), **Description** (the body; the author's\n * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Replies** (0.38,\n * §9.7: the timeline, the admin's reply composer with its status picker and the nudge,\n * Rework — on a row without `replies`, a server before 0.38, the frozen\n * **Outcome** read-only in its place), **Comments** (0.38: the app's\n * `FeedbackCommentThread`, when passed), **Send for rework** (only while reworking) and\n * **Status** (with the **Visible to others** switch when `onPublicChange` is passed).\n *\n * The app keeps its data and its API: the row it renders, the PATCH (`onUpdate`), the\n * upload (`onUpload`), the authenticated GET (`fetcher`) and who is looking (`canEdit`,\n * `viewerId`). Every word is the kit's `feedbackDetail` namespace.\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/**\n * `feedbackDetail` — the row detail's words (§4.4). English: keksdose's `en.json`\n * (`feedback.*`, `common.*`, `more.copy_url`) with the contract's changes; the de-CH\n * canon (keksdose `de-CH.json`, ss never ß, \"Nacharbeit\" throughout — §7.14) on each key.\n */\nexport interface FeedbackDetailLabels {\n /** The first section's heading: what the report is called (feedback contract §8.1,\n * keksdose live #395 — with the row expanded and the table scrolled, nothing said\n * which entry was being answered). de-CH: \"Betreff\" */\n subject: string;\n /** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks\n * \"What happened? (optional)\"; the description, once filed, is headed as what it is\n * — and as the author's \"Edit description\" calls it. de-CH: \"Beschreibung\" */\n body: string;\n /** The author's button on that heading. de-CH: \"Bearbeiten\" */\n edit: string;\n /** Its accessible name — says WHAT is edited; contains the visible word.\n * de-CH: \"Beschreibung bearbeiten\" */\n editDescription: string;\n /** The editors' save button (description, outcome). de-CH: \"Speichern\" */\n save: string;\n /** Every editor's cancel button. de-CH: \"Abbrechen\" */\n cancel: string;\n /** Section 2's heading. de-CH: \"URL\" */\n url: string;\n /** The copy button beside the link — copies the FULL `context.url`.\n * de-CH: \"URL kopieren\" */\n copyUrl: string;\n /** Section 3's heading, and each picture's alt text. de-CH: \"Anhang\" */\n attachment: string;\n /** A PDF's / text file's download button, given the file's name.\n * de-CH: \"{{name}} herunterladen\" */\n download: (name: string) => string;\n /** The toast when that download failed. de-CH: \"Der Anhang konnte nicht\n * heruntergeladen werden.\" */\n downloadFailed: string;\n /** Section 4's heading on a row without `replies` (a server before 0.38), where the\n * frozen outcome is shown read-only. de-CH: \"Ergebnis\" */\n outcome: string;\n /** Section 4's heading since 0.38: the replies' timeline (§9.7). de-CH: \"Antworten\" */\n replies: string;\n /** On section 4's heading line once the row is settled, given the formatted\n * `resolved_at` date. de-CH: \"Erledigt am: {{date}}\" */\n resolvedAt: (date: string) => string;\n // Deprecated on these members, the ones an app's code reads and overrides through: the\n // tags sat on the English default's keys, and reached nothing — the default is typed by\n // this interface (0.52 code review, feedback-row-detail-labels.ts; refactor audit\n // 2026-10-30, DOC-9).\n /** The admin's button while there is no outcome — read by nothing since 0.38 (the\n * outcome is frozen, §9.7; a reply is written instead). de-CH: \"Ergebnis hinzufügen\"\n * @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */\n outcomeAdd: string;\n /** The same button once there is one. Unused since 0.38, as `outcomeAdd`.\n * de-CH: \"Aktualisieren\"\n * @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */\n outcomeUpdate: string;\n /** The line above the outcome editor. Unused since 0.38, as `outcomeAdd`.\n * de-CH: \"Was wurde umgesetzt, entschieden oder warum nicht.\"\n * @deprecated 0.52 — a reply: the outcome is frozen since 0.38 (§9.7); removed in 0.53. */\n outcomePlaceholder: string;\n /** The button that opens section 5 on an answered row. de-CH: \"Nacharbeit\" */\n rework: string;\n /** Section 5's heading. de-CH: \"Zur Nacharbeit senden\" */\n reworkTitle: string;\n /** Section 5's send button. de-CH: \"Nacharbeit senden\" */\n reworkSend: string;\n /** The line above the rework note. de-CH: \"Was muss noch angepasst werden? Neue\n * Anforderungen oder Richtungswechsel.\" */\n reworkPlaceholder: string;\n /** The toast when the rework's file did not upload — and so nothing was sent. Names\n * no screenshot: it fires for a PDF or a log too (§7.14; keksdose's said\n * \"screenshot\"). de-CH: \"Die Datei konnte nicht hochgeladen werden. Die Nacharbeit\n * wurde nicht gesendet.\" */\n reworkUploadFailed: string;\n /** Section 6's heading. de-CH: \"Status\" */\n status: string;\n /** A `FeedbackNoteEditor`'s box, named when no `placeholder` line names it and the\n * caller gives no `label` — without it the box was read out as just \"edit text\"\n * (round 0.34, the showcase's axe check). de-CH: \"Notiz\" */\n note: string;\n}\n\n// Defined in ./feedback-row-detail-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DETAIL_LABELS };\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: LabelOverride<FeedbackDetailLabels>): FeedbackDetailLabels {\n return useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS, labels);\n}\n\n/* ── The page a report was filed on (§3.2, §4.4.2) ────────────────────────── */\n\n/**\n * The page a report was filed on as the reporter had it — path + query + hash (keksdose\n * `localHref`: the query and hash carry the state, `?tab=import`, a drill-down). Read from\n * `context.url`, else `context.route`; a path-only `url` works. `null` when there is no\n * page.\n *\n * Always a path on THIS app, never another origin's address — but the path and query\n * are the reporter's to write, so it is TEXT, not a link target: `/join?token=…` acts on\n * load, and `/login?next=…` forwards a signed-in admin to whatever its query names\n * (refactor audit 2026-10-30, FLW-7; 0.52 code review). The kit's URL cell and section\n * link {@link feedbackPagePath} alone, and not even that for a one-shot route\n * (`pageLinkExclude`) — `FeedbackRowDetail` and `useFeedbackColumns` draw the page that\n * way; an app's own cell should not link this.\n */\nexport function feedbackPageHref(context: FeedbackContext | null | undefined): string | null {\n return feedbackPage(context)?.href ?? null;\n}\n\n/** The page's path alone — the compact text a URL column shows (§4.3), and the only part\n * of the page the kit links. */\nexport function feedbackPagePath(context: FeedbackContext | null | undefined): string | null {\n return feedbackPage(context)?.path ?? null;\n}\n\n/**\n * What \"Copy URL\" copies: the FULL `context.url` as the reporter's browser had it —\n * origin included, which is the one part that says which copy of the app it was\n * (feedback #278) — else `context.route`. `null` when there is no page.\n */\nexport function feedbackPageUrl(context: FeedbackContext | null | undefined): string | null {\n return feedbackPage(context)?.url ?? null;\n}\n\n/* ── Attachments (§3.5, §4.4.3) ───────────────────────────────────────────── */\n\n/**\n * Every file a report carries, in the order the detail shows them (§4.4.3): the\n * screenshot it was filed with, then its other files, then whatever the rework rounds\n * brought, in body order (keksdose dev #578, live #291). One list rather than three —\n * a reader looking for \"the picture\" should not have to know at which point in the\n * conversation it was taken.\n *\n * Each URL once: keksdose and Kurvenschmiede content-address their keys, so the same\n * screenshot pasted twice is one URL — shown twice it is the same picture twice (and,\n * keyed by URL, a React key clash keksdose had).\n */\nexport function feedbackAttachmentUrls(\n row: Pick<FeedbackRecord, \"screenshot_url\" | \"attachment_urls\" | \"body\"> & Partial<Pick<FeedbackRecord, \"replies\">>,\n): string[] {\n const urls = [\n ...(row.screenshot_url ? [row.screenshot_url] : []),\n // `?? []`: NULL on rows filed before the column, and no key at all from a server\n // that predates it (keksdose dev #578).\n ...(row.attachment_urls ?? []),\n ...splitBodyAttachments(row.body).urls,\n // 0.40 (§10.3): a rework entry's one file, in entry order — where the rework\n // pictures of the body were before.\n ...(row.replies ?? []).flatMap((reply) =>\n reply.kind === \"rework\" && reply.attachment_url ? [reply.attachment_url] : [],\n ),\n ];\n return [...new Set(urls)];\n}\n\n/* ── Who may do what (§3.4, §4.3, §4.4) ───────────────────────────────────── */\n\n/** What the person looking at a row may do with it — {@link feedbackRowAccess}. */\nexport interface FeedbackRowAccess {\n /** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */\n isAuthor: boolean;\n /** \"Edit description\": the author, while the row is OPEN, READY or IN_PROGRESS — on either\n * page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */\n canEditDescription: boolean;\n /** The admin on the inbox (`canEdit`): since 0.38 the reply composer (the outcome\n * itself is frozen, §9.7). */\n canEditOutcome: boolean;\n /** \"Rework\": the author, on an answered row ({@link FEEDBACK_REWORKABLE_STATUSES}).\n * Since 0.40 (§10.2 decision 6) the author's only — an admin sends a report back with\n * a reply whose status is `REWORK`; the server answers anyone else 403. */\n canRework: boolean;\n /** The status pills: the admin on the inbox; everyone else sees them read-only. */\n canChangeStatus: boolean;\n}\n\n/**\n * The detail's permission rules in one place — the same answers the server gives\n * (§3.4), so the panel never offers what the PATCH would refuse.\n *\n * `canEdit` is the app's \"admin on `/feedback`\": an admin on `/my-feedback` edits no\n * status and no outcome there (§4.3), but as the author still edits the description\n * and sends for rework. `viewerId` is the signed-in user's id.\n */\nexport function feedbackRowAccess(\n row: Pick<FeedbackRecord, \"user_id\" | \"status\">,\n { canEdit, viewerId }: { canEdit: boolean; viewerId?: number | null },\n): FeedbackRowAccess {\n // `user_id: null` is an erased author (keksdose live #275): nobody is that author,\n // least of all a viewer whose id happens to be missing too.\n const isAuthor = row.user_id !== null && viewerId !== undefined && viewerId !== null && viewerId === row.user_id;\n return {\n isAuthor,\n canEditDescription: isAuthor && FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status),\n canEditOutcome: canEdit,\n canRework: isAuthor && FEEDBACK_REWORKABLE_STATUSES.includes(row.status),\n canChangeStatus: canEdit,\n };\n}\n\n/* ── The PATCH ─────────────────────────────────────────────────────────────── */\n\n/** What the detail sends as `PATCH /feedback/{id}` (§3.4): the body (a description\n * edit, a rework append) or the outcome — and, through {@link useFeedbackStatusUndo}\n * when no `statusChange` is given, the status. Never two at once: a rework PATCH must\n * carry the body ALONE for the server to read it as an append (keksdose live #331). */\nexport interface FeedbackDetailPatch {\n id: number;\n body?: string;\n /** `null` clears it — a real edit. **Frozen since 0.38** (§9.7): server-kit 0.11\n * answers 422; the detail no longer sends it (a reply is written instead). */\n outcome?: string | null;\n status?: FeedbackStatus;\n /** 0.38.1: the **Visible to others** flip (§9.3) — what `onPublicChange`'s\n * `PATCH /feedback/{id} {\"public\": next}` carries, alone, so an app can send it\n * through the same `onUpdate` mutation. */\n public?: boolean;\n}\n\n/**\n * The page's update mutation (0.52): **TanStack Query's `mutateAsync`** — pass\n * `update.mutateAsync` (and `pending={update.isPending}`), the same function\n * `useFeedbackStatusUndo` takes. An editor closes when the promise resolves, which it does\n * for every call — also when a status pill was pressed while the save was in flight\n * (refactor audit 2026-10-30, FLW-3). A hand-written one resolves once the PATCH has\n * landed. A failure is the mutation's own to toast — the server's `detail`, else\n * `feedbackToast.updateFailed` (§4.4) — and leaves the editor open with its draft.\n */\nexport type FeedbackDetailUpdateAsync = (patch: FeedbackDetailPatch) => Promise<unknown>;\n\n/**\n * The callback form: TanStack Query's `mutate`, or a function that calls `onSuccess` once\n * the PATCH has landed. TanStack calls a per-call `onSuccess` for the LATEST `mutate()`\n * only, so an editor whose save was followed by another PATCH before it answered stayed\n * open, and the earlier status change lost its Undo.\n *\n * @deprecated 0.52 — pass `update.mutateAsync` ({@link FeedbackDetailUpdateAsync}); removed\n * in 0.53\n */\nexport type FeedbackDetailUpdate = (patch: FeedbackDetailPatch, callbacks: { onSuccess: () => void }) => void;\n\n/** `POST /feedback/attachments` (§3.3): one file up, its URL\n * (`/api/v1/feedback/attachments/<key>`) back. Reject on failure. */\nexport type FeedbackUpload = (file: File) => Promise<string>;\n\n/* ── Send for rework (§4.4.5) ─────────────────────────────────────────────── */\n\n/** 0.40 (§10.3): `POST /feedback/{id}/rework` — the trimmed note and the uploaded file's\n * URL. Return the promise: the section closes when it resolves and keeps the note when\n * it rejects (the toast is the app's: the server's `detail`, else\n * `feedbackToast.updateFailed`). */\nexport type FeedbackReworkSend = (note: string, attachmentUrl?: string) => void | Promise<unknown>;\n\nexport interface FeedbackReworkSectionProps {\n /** The row being sent back: its id, and the body the round is appended to. */\n row: Pick<FeedbackRecord, \"id\" | \"body\">;\n /** 0.40: the rework as an entry of its own — see {@link FeedbackReworkSend}. With it,\n * `onUpdate` is not called. */\n onRework?: FeedbackReworkSend;\n /** The PATCH — see {@link FeedbackDetailUpdateAsync}. Called with `{ id, body }` only,\n * and only without `onRework`: the legacy append (§3.4), which server-kit 0.13 still\n * turns into an entry for one release. Optional since 0.40; pass one of the two. */\n onUpdate?: FeedbackMutationCall<FeedbackDetailPatch>;\n /** The PATCH is in flight (`update.isPending`). The upload adds its own. */\n pending?: boolean;\n /** The upload. Left out, the note goes without a file field. */\n onUpload?: FeedbackUpload;\n /** Snapshot the app view as a File — the kit's `captureAppScreenshot`, or the app's\n * own. A \"Capture screenshot\" button appears only with it; a rejection (no\n * `modern-screenshot`, a canvas that would not draw) toasts\n * `feedbackToast.captureFailed` and adds nothing. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The round landed — the server has the note and, on an answered row, reopened it. */\n onSent?: () => void;\n onCancel: () => void;\n /** Over the field's defaults: the contract's types and 10 MB (§3.5), the\n * `feedbackToast` refusals, the `feedbackAttachment` words. */\n attachment?: Partial<FeedbackNoteAttachment>;\n /** The send COMMITS — see `FeedbackNoteEditor`'s `commit`. */\n commit?: CommitScope;\n labels?: LabelOverride<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter sends an answered report\n * back with a note and at most one file, and the server reopens it (§3.4, keksdose\n * `submitRework`, `feedback-page.tsx:350`).\n *\n * - **The note is required** and trimmed: Send stays disabled on a blank box (a file\n * alone cannot be sent; keksdose `:577` dropped that click silently).\n * - **One file**, added, captured or pasted — the dialog's types and 10 MB. It is\n * uploaded FIRST and awaited: a body naming a file that never arrived would be a\n * rework whose evidence is missing, and the upload is the half that fails on its\n * own. If it fails nothing is sent and the toast says so; the note stays in the box.\n * - **0.40: an entry of its own** (§10.3) — with `onRework`, the note and the file's URL\n * go to `POST /feedback/{id}/rework` and the body is not touched. Without it, the\n * legacy append below, which server-kit 0.13 still converts (one minor).\n * - **The body alone goes in the PATCH** — {@link appendRework}'s strict extension of\n * the current body, which is how the server recognises the append and reopens the\n * row. Never a `status` (kastlan's client sent `OPEN`; an author may not write one,\n * so the server refused the whole PATCH, note and all — keksdose live #331).\n *\n * Exported on its own for an app that offers rework somewhere other than the row\n * detail. Kurvenschmiede's author verdict (\"That is it, done\") is NOT here: an answered\n * item stays answered unless its author sends it back (§7.2).\n */\nexport function FeedbackReworkSection({\n row,\n onRework,\n onUpdate,\n pending = false,\n onUpload,\n onCaptureScreenshot,\n onSent,\n onCancel,\n attachment,\n commit,\n labels: labelsProp,\n}: FeedbackReworkSectionProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n const toasts = useFeedbackToastLabels();\n const [uploading, setUploading] = useState(false);\n const [sending, setSending] = useState(false);\n // The body as it is NOW, not as it was when Send was pressed: the list can refresh\n // while the file uploads, and an append to a stale body is no longer a strict\n // extension of the stored one — the server would read it as an edit, and refuse it.\n const latestBody = useRef(row.body);\n useEffect(() => {\n latestBody.current = row.body;\n }, [row.body]);\n\n const capture = onCaptureScreenshot\n ? async () => {\n try {\n return await onCaptureScreenshot();\n } catch {\n toast.error(toasts.captureFailed);\n return null;\n }\n }\n : undefined;\n\n const send = async (value: string, file?: File | null) => {\n const note = value.trim();\n if (!note) return;\n let url: string | null = null;\n if (file && onUpload) {\n setUploading(true);\n try {\n url = await onUpload(file);\n } catch {\n toast.error(labels.reworkUploadFailed);\n return;\n } finally {\n setUploading(false);\n }\n }\n if (onRework) {\n // 0.40 (§10.3): the entry, not the body — the server checks the URL (a 422 outside\n // the app's attachment prefix).\n setSending(true);\n try {\n await onRework(note, url ?? undefined);\n } catch {\n return; // The app's toast; the note stays in the box.\n } finally {\n setSending(false);\n }\n onSent?.();\n return;\n }\n if (!onUpdate) return;\n let body: string | null;\n try {\n body = appendRework(latestBody.current, note, url);\n } catch {\n // The upload answered with something that is not an attachment URL of this\n // contract — written anyway it would sit in the body as prose no reader knows to\n // be a file. As far as the person is concerned, the file did not arrive.\n toast.error(labels.reworkUploadFailed);\n return;\n }\n if (body === null) return;\n runMutation(onUpdate, { id: row.id, body }, () => onSent?.());\n };\n\n return (\n <FeedbackDetailSection title={labels.reworkTitle}>\n <FeedbackNoteEditor\n key={row.id}\n initial=\"\"\n required\n commit={commit}\n pending={pending || uploading || sending}\n onSave={(value, file) => void send(value, file)}\n onCancel={onCancel}\n saveLabel={labels.reworkSend}\n cancelLabel={labels.cancel}\n placeholder={labels.reworkPlaceholder}\n attachment={\n onUpload\n ? {\n accept: FEEDBACK_ATTACHMENT_ACCEPT,\n maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,\n onError: (kind) => {\n toast.error(kind === \"type\" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);\n },\n onCaptureScreenshot: capture,\n ...attachment,\n }\n : undefined\n }\n />\n </FeedbackDetailSection>\n );\n}\n\n/* ── The detail (§4.4) ─────────────────────────────────────────────────────── */\n\nexport interface FeedbackRowDetailProps {\n /** The report — the list's own row, as `GET /feedback` / `GET /feedback/my` sent it. */\n row: FeedbackRecord;\n /** The admin on the inbox (`/feedback`): status pills and the outcome editor. `false`\n * on `/my-feedback`, for an admin too (§4.3). */\n canEdit: boolean;\n /** The signed-in user's id — who the AUTHOR is: the description editor and rework. */\n viewerId?: number | null;\n /** The page's PATCH — `update.mutateAsync` (see {@link FeedbackDetailUpdateAsync}). The\n * callback form ({@link FeedbackDetailUpdate}, `update.mutate`) still fits, deprecated\n * since 0.52; the prop's one signature takes both. */\n onUpdate: FeedbackMutationCall<FeedbackDetailPatch>;\n /** `update.isPending`: the editors' save buttons wait for it. */\n pending?: boolean;\n /** `POST /feedback/attachments` for the rework's one file and, since 0.47 (§12) with\n * `replyAttachments`, the admin reply's. Left out, a rework or a reply is text alone. */\n onUpload?: FeedbackUpload;\n /** The app's authenticated GET (`AuthedImage`'s) — the pictures and the downloads go\n * through it, since the attachment route wants the bearer token (§3.5). Left out,\n * the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /**\n * The undoable status change — what `useFeedbackStatusUndo(update.mutateAsync)` returns,\n * the SAME one the table's status cell and the swipes use (§4.5). Left out, the\n * detail builds one from `onUpdate`, which is equivalent as long as `onUpdate` is the\n * page's mutation (its observer outlives the row a status filter removes).\n */\n statusChange?: FeedbackStatusChange;\n /** For the rework's and (0.47, with `replyAttachments`) the reply composer's \"Capture screenshot\" — see\n * {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's and (0.47, with `replyAttachments`) the reply composer's file field,\n * over the contract's defaults. */\n reworkAttachment?: Partial<FeedbackNoteAttachment>;\n /** How the `resolved_at` date reads — the app's date cell, as a string. Default\n * (0.52): the `<UiKitProvider formatDate>` when the app set one, else the kit's\n * `formatDate` in the provider's locale and time zone, `medium` style. */\n renderDate?: (iso: string) => string;\n /**\n * 0.52 (FLW-7): the app's one-shot routes, ON TOP of the kit's — `/join`,\n * `/register`, `/verify-email`, `/reset-password` (a string is that path and everything\n * under it, a RegExp is tested on the path, as `useLastVisitedPage`'s `exclude`). The\n * URL section shows such a page as text with Copy, never as a link: it acts on load,\n * and a report's page is the reporter's to write. Any other page links its PATH alone;\n * the query, where tokens and `?next=` targets travel, is text. keksdose:\n * `[\"/banks/callback\", \"/share-target\"]`. Keep it stable (module scope).\n */\n pageLinkExclude?: readonly PathPattern[];\n /**\n * The detail's saves COMMIT (description, outcome, rework, status): under a locked\n * `WriteLockProvider` each is `aria-disabled` with the lock's reason, and nothing\n * reaches `onUpdate`. Off by default — keksdose's shell lock is a read-only demo\n * BUDGET, and a person viewing one still owns their own reports.\n */\n commit?: CommitScope;\n /**\n * Open with the subject: the category badge and the title (feedback contract §8.1,\n * keksdose live #395). On by default; `false` for a page that already shows the title\n * beside the detail.\n */\n showSubject?: boolean;\n /**\n * 0.38: `POST /feedback/{id}/replies` (§9.7) — the body and the status picked\n * (`undefined`: \"No change\"). With `canEdit` and a row that carries `replies`, the\n * reply composer stands under the timeline. Return the promise: the box clears when it\n * resolves. A failure is the app's to toast — the server's `detail`, else\n * `feedbackToast.replyFailed` (0.38.1).\n *\n * 0.47 (§12, keksdose live #447): with `replyAttachments` (and `onUpload`) the\n * composer takes one file as the rework note does — pasted into the box, captured or\n * picked — uploaded first, and its URL is the third argument (`attachment_url` in the\n * body; server-kit 0.18).\n */\n onReply?: (body: string, status?: FeedbackStatus, attachmentUrl?: string) => void | Promise<unknown>;\n /**\n * 0.47 (§12): the reply composer takes one file — through `onUpload`,\n * `onCaptureScreenshot` and `reworkAttachment`, the rework note's. Off by default:\n * `onUpload` alone stays the rework's, so a page that passes it and an `onReply` of\n * `(body, status)` does not grow a file field whose file its reply would drop (round\n * 0.47 build reviews, keksdose and kastlan B1). Turn it on in the commit that makes\n * `onReply` send its third argument as `attachment_url`, with server-kit 0.18 (an\n * older server answers the field with a 422).\n */\n replyAttachments?: boolean;\n /** 0.38: the reply is in flight. */\n replying?: boolean;\n /**\n * 0.40 (§10.3): `POST /feedback/{id}/rework` — the author's rework as an entry of its\n * own (see {@link FeedbackReworkSend}). The **Rework** button shows only to the author\n * on an answered row, and only where the reply composer is not shown (§10.2 decision 6:\n * an admin answering a report sends it back with a reply whose status is `REWORK`).\n * Left out, the rework goes through `onUpdate` as the legacy body append — for one\n * minor, while the server converts it.\n */\n onRework?: FeedbackReworkSend;\n /** 0.38: the report's comments — the app's `FeedbackCommentThread` — in a section of\n * their own under the replies. A thread here draws the comments alone (0.38.1): the\n * Replies section above has the replies, so whatever `replies` the thread is handed\n * are dropped, and nothing is drawn twice. */\n comments?: ReactNode;\n /** 0.38: `PATCH /feedback/{id} {\"public\": next}` — draws the **Visible to others**\n * switch (`FeedbackPublicSwitch`) in the Status section, for the admin (`canEdit`)\n * and, within §9.3's window, the author. The page's own mutation fits:\n * `onPublicChange={(next) => update.mutate({ id: row.id, public: next })}` —\n * {@link FeedbackDetailPatch}'s `public` (0.38.1). */\n onPublicChange?: (next: boolean) => void;\n labels?: LabelOverride<FeedbackDetailLabels>;\n className?: string;\n}\n\n/**\n * The expanded row of a feedback table (§4.4): what was reported, where, with which\n * files, what the answer was, and where it stands — with the edits each viewer may\n * make. Hand it to `DataTable`'s `expandedRow`:\n *\n * ```tsx\n * const update = useMutation({ mutationFn: ({ id, ...patch }) => api.updateFeedback(id, patch),\n * onSuccess: invalidate, onError: (e) => toast.error(detailOf(e) ?? toastLabels.updateFailed) });\n * const changeStatus = useFeedbackStatusUndo(update.mutateAsync);\n * <DataTable\n * isExpanded={(row) => expandedId === row.id}\n * expandedRow={(row) => (\n * <FeedbackRowDetail row={row} canEdit={isAdmin && !mine} viewerId={user?.id}\n * onUpdate={update.mutateAsync} pending={update.isPending} statusChange={changeStatus}\n * onUpload={api.uploadFeedbackAttachment} fetcher={authedImageFetcher}\n * onCaptureScreenshot={captureAppScreenshot} />\n * )} … />\n * ```\n *\n * Mounted only while its row is expanded, so which editor is open lives here, and\n * collapsing the row closes it. Each editor is keyed by the row's id, so a draft is\n * never thrown away by a list refresh under it.\n */\nexport function FeedbackRowDetail({\n row,\n canEdit,\n viewerId,\n onUpdate,\n pending = false,\n onUpload,\n fetcher,\n statusChange,\n onCaptureScreenshot,\n reworkAttachment,\n renderDate,\n pageLinkExclude,\n commit,\n showSubject = true,\n onReply,\n replyAttachments = false,\n replying = false,\n onRework,\n comments,\n onPublicChange,\n labels: labelsProp,\n className,\n}: FeedbackRowDetailProps) {\n const labels = useFeedbackDetailLabels(labelsProp);\n // \"placeholder = the dialog's body label\" (§4.4.1): read from the dialog's own\n // namespace, so the two can never say different things.\n const dialogText = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS);\n const ownStatusChange = useFeedbackStatusUndo(onUpdate);\n const changeStatus = statusChange ?? ownStatusChange;\n const [editingBody, setEditingBody] = useState(false);\n const [reworking, setReworking] = useState(false);\n\n const access = feedbackRowAccess(row, { canEdit, viewerId });\n // The body as written, without its file lines (they are shown as files below) — the\n // rework rounds stay in the prose: they are what was said after the answer.\n const bodyText = splitBodyAttachments(row.body).text;\n const { description } = splitDescription(row.body);\n const page = feedbackPage(row.context);\n const pageLink = page && feedbackPageLinkTarget(page, pageLinkExclude);\n // 0.47 (§12): with the entries, each entry's file is drawn inline in the Replies\n // timeline, so the Attachment section holds the report's own files.\n const files = feedbackAttachmentUrls(row.replies ? { ...row, replies: undefined } : row);\n const fetchFile = fetcher ?? plainFeedbackFetch;\n // A date in a sentence (\"Resolved: …\"), so no weekday — through the kit's one day\n // ladder, which reaches the provider's `formatDate` (0.52, refactor audit 2026-10-30,\n // DUP-2).\n const day = useDayFormatter<string>(renderDate);\n const dateText = (iso: string) => day(iso);\n\n const saveDescription = (value: string) => {\n // Only a real change makes a round trip; clearing it is one (\"\" — the description\n // is optional, §7.4), and the rounds and file lines go back unchanged.\n if (value.trim() === description.trim()) {\n setEditingBody(false);\n return;\n }\n runMutation(onUpdate, { id: row.id, body: replaceDescription(row.body, value) }, () => setEditingBody(false));\n };\n\n // 0.38 (§9.7): the replies replace the outcome; a row without them is a server before\n // 0.38, whose frozen outcome is shown as it is, read-only.\n const replies = row.replies;\n const commentLabels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS);\n // 0.40 (§10.2 decision 6): whoever has the reply composer answers with it — REWORK is\n // one of its statuses — so the Rework button is for an author without it.\n const composerShown = Boolean(replies && access.canEditOutcome && onReply);\n const canRework = access.canRework && !composerShown;\n const showRework = canRework && reworking;\n\n return (\n <FeedbackDetail className={className}>\n {showSubject && (\n <FeedbackDetailSection title={labels.subject}>\n <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm\" data-feedback-subject=\"\">\n <FeedbackCategoryBadge category={row.category} />\n <span className=\"min-w-0 font-medium text-primary [overflow-wrap:anywhere]\">{row.title}</span>\n </p>\n </FeedbackDetailSection>\n )}\n <FeedbackDetailSection\n title={labels.body}\n action={\n access.canEditDescription && !editingBody ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingBody(true);\n }}\n aria-label={labels.editDescription}\n >\n {labels.edit}\n </Button>\n ) : undefined\n }\n >\n {access.canEditDescription && editingBody ? (\n <FeedbackNoteEditor\n key={row.id}\n // The ORIGINAL description only — see the module note.\n initial={description}\n pending={pending}\n commit={commit}\n onSave={saveDescription}\n onCancel={() => setEditingBody(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={dialogText.bodyOptional ?? dialogText.body}\n />\n ) : (\n <FeedbackProse>{bodyText.trim() ? bodyText : undefined}</FeedbackProse>\n )}\n </FeedbackDetailSection>\n\n {page && (\n // The page it was filed on, with the full URL a click away (keksdose #278). Only\n // the path is a link; the query and hash, the reporter's to write, are text — and\n // a one-shot path is text too (FLW-7, 0.52 code review: see feedback-page-link).\n <FeedbackDetailSection title={labels.url}>\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <code\n className=\"min-w-0 break-all text-xs text-secondary\"\n data-feedback-page=\"\"\n data-feedback-page-unlinked={pageLink ? undefined : \"\"}\n >\n {pageLink ? (\n <TextLink href={pageLink} tone=\"secondary\" underline=\"hover\" stopPropagation>\n {page.path}\n </TextLink>\n ) : (\n page.path\n )}\n {page.href.slice(page.path.length)}\n </code>\n <CopyButton size=\"xs\" className=\"shrink-0\" text={page.url} label={labels.copyUrl} stopPropagation />\n </div>\n </FeedbackDetailSection>\n )}\n\n {files.length > 0 && (\n <FeedbackDetailSection title={labels.attachment}>\n <div className=\"flex min-w-0 flex-wrap items-start gap-2\">\n {files.map((url) =>\n isImageAttachment(url) ? (\n <AuthedImage\n key={url}\n src={url}\n alt={labels.attachment}\n fetcher={fetchFile}\n link\n stopPropagation\n // A broken attachment is simply not in the row (keksdose's\n // AuthedImage default) rather than a grey tile per failure.\n errorFallback={null}\n wrapperClassName=\"inline-block max-w-full\"\n className=\"max-h-64 max-w-full rounded border border-subtle\"\n />\n ) : (\n <FeedbackAttachmentDownload key={url} url={url} fetcher={fetchFile} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={replies ? labels.replies : labels.outcome}\n // When the row entered DONE / WONT_DO (keksdose feedback #96).\n action={\n row.resolved_at ? (\n <span className=\"text-xs text-muted\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n <div className=\"space-y-3\">\n {replies ? (\n <FeedbackReplyTimeline replies={replies} fetcher={fetchFile} />\n ) : (\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n )}\n {composerShown && replies && onReply && (\n <FeedbackReplyComposer\n key={row.id}\n status={row.status}\n onReply={onReply}\n pending={replying}\n onUpload={replyAttachments ? onUpload : undefined}\n onCaptureScreenshot={replyAttachments ? onCaptureScreenshot : undefined}\n attachment={replyAttachments ? reworkAttachment : undefined}\n suggestedStatus={feedbackReplyNudge(row)}\n commit={commit}\n />\n )}\n {/* No \"Open page\" button here since 0.46 (keksdose live #449): the URL section\n above already links the page, with the path on screen. */}\n {canRework && !reworking && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setReworking(true);\n }}\n >\n <RotateCcw className=\"size-3.5\" aria-hidden /> {labels.rework}\n </Button>\n </div>\n )}\n </div>\n </FeedbackDetailSection>\n\n {comments != null && (\n <FeedbackDetailSection title={commentLabels.comments}>\n <FeedbackRepliesDrawnAboveContext.Provider value>{comments}</FeedbackRepliesDrawnAboveContext.Provider>\n </FeedbackDetailSection>\n )}\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\n onRework={onRework}\n onUpdate={onUpdate}\n pending={pending}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n onSent={() => setReworking(false)}\n onCancel={() => setReworking(false)}\n attachment={reworkAttachment}\n commit={commit}\n labels={labelsProp}\n />\n )}\n\n <FeedbackDetailSection title={labels.status}>\n {/* Expanded: the whole enum in one click for the admin (keksdose feedback #421);\n everyone else the narrow set, read-only — a picture of the workflow. */}\n <FeedbackStatusTransitions\n status={row.status}\n statuses={\n access.canChangeStatus ? selectableFeedbackStatuses(row.status) : visibleFeedbackStatuses(row.status)\n }\n canEdit={access.canChangeStatus}\n commit={commit}\n onPick={(status) => changeStatus(row, status)}\n variant=\"pill\"\n />\n {onPublicChange && (\n <FeedbackPublicSwitch\n row={row}\n isAdmin={canEdit}\n viewerId={viewerId}\n onChange={onPublicChange}\n pending={pending}\n commit={commit}\n className=\"mt-3\"\n />\n )}\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,6 +2,7 @@ import type { FeedbackStatus } from "./feedback-vocabulary.js";
|
|
|
2
2
|
import { type FeedbackStatusLabels, type FeedbackToastLabels } from "./feedback-labels.js";
|
|
3
3
|
import type { FeedbackRecord } from "./feedback-record.js";
|
|
4
4
|
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
5
|
+
import { type FeedbackMutationCall } from "./feedback-mutation.js";
|
|
5
6
|
/**
|
|
6
7
|
* A status change that can be taken back — §4.5 of the feedback contract, lifted from
|
|
7
8
|
* keksdose's `changeStatusUndoably` (`frontend/src/features/feedback/feedback-page.tsx`,
|
|
@@ -20,12 +21,24 @@ export interface FeedbackStatusPatch {
|
|
|
20
21
|
status: FeedbackStatus;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
|
-
* The page's status mutation. **TanStack Query's `
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
24
|
+
* The page's status mutation (0.52): **TanStack Query's `mutateAsync`** — pass
|
|
25
|
+
* `update.mutateAsync`. The toast comes when the promise resolves, which it does for every
|
|
26
|
+
* call (refactor audit 2026-10-30, FLW-3). A hand-written one returns a promise that
|
|
27
|
+
* resolves once the PATCH has landed and rejects when it failed.
|
|
26
28
|
*
|
|
27
29
|
* Failure is the mutation's own business: its `onError` toasts the server's `detail`,
|
|
28
|
-
* else `feedbackToast.updateFailed` (§4.4) — this hook adds no second error toast
|
|
30
|
+
* else `feedbackToast.updateFailed` (§4.4) — this hook adds no second error toast, and
|
|
31
|
+
* catches the rejection.
|
|
32
|
+
*/
|
|
33
|
+
export type FeedbackStatusMutateAsync = (patch: FeedbackStatusPatch) => Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* The callback form: TanStack Query's `mutate`, or a function that calls `onSuccess` once
|
|
36
|
+
* the PATCH has landed. TanStack calls a per-call `onSuccess` for the LATEST `mutate()` on
|
|
37
|
+
* its observer only, so a second change sent before the first answered — another swipe,
|
|
38
|
+
* another pill, the Undo itself — lost the first change's Undo toast.
|
|
39
|
+
*
|
|
40
|
+
* @deprecated 0.52 — pass `update.mutateAsync` ({@link FeedbackStatusMutateAsync}), whose
|
|
41
|
+
* promise settles for every call; removed in 0.53
|
|
29
42
|
*/
|
|
30
43
|
export type FeedbackStatusMutate = (patch: FeedbackStatusPatch, callbacks: {
|
|
31
44
|
onSuccess: () => void;
|
|
@@ -51,16 +64,22 @@ export declare const FEEDBACK_UNDO_DURATION = 8000;
|
|
|
51
64
|
*
|
|
52
65
|
* ```tsx
|
|
53
66
|
* const update = useMutation({ mutationFn: api.updateFeedback, onError: … });
|
|
54
|
-
* const changeStatus = useFeedbackStatusUndo(update.
|
|
67
|
+
* const changeStatus = useFeedbackStatusUndo(update.mutateAsync);
|
|
55
68
|
* <FeedbackStatusTransitions … onPick={(status) => changeStatus(row, status)} />
|
|
56
69
|
* ```
|
|
57
70
|
*
|
|
58
|
-
* Give it the PAGE's mutation, not one a row owns:
|
|
59
|
-
*
|
|
60
|
-
* status filters out unmounts before the PATCH answers — the toast would never come.
|
|
71
|
+
* Give it the PAGE's mutation, not one a row owns: a row the new status filters out
|
|
72
|
+
* unmounts before the PATCH answers.
|
|
61
73
|
*
|
|
62
|
-
* The flow: PATCH `{ id, status }` →
|
|
74
|
+
* The flow: PATCH `{ id, status }` → once it has landed a success toast
|
|
63
75
|
* `statusChanged(newName, title)` with the action `statusUndo`, 8 s → Undo PATCHes
|
|
64
|
-
* `{ id, status: previous }` →
|
|
76
|
+
* `{ id, status: previous }` → once that has landed `statusRestored(previousName, title)`.
|
|
77
|
+
* Each change waits for its OWN answer, so two changes in flight each get their toast.
|
|
78
|
+
*
|
|
79
|
+
* `update.mutate` ({@link FeedbackStatusMutate}) still fits, deprecated since 0.52: with it
|
|
80
|
+
* a second change before the first answered loses the first one's Undo. One signature
|
|
81
|
+
* takes both rather than two overloads: TypeScript types an inline callback's parameters
|
|
82
|
+
* from the FIRST overload, so a hand-written `(patch, { onSuccess }) => …` — the form this
|
|
83
|
+
* hook documented until 0.52 — would no longer compile.
|
|
65
84
|
*/
|
|
66
|
-
export declare function useFeedbackStatusUndo(mutate:
|
|
85
|
+
export declare function useFeedbackStatusUndo(mutate: FeedbackMutationCall<FeedbackStatusPatch>, { labels, statusLabels }?: FeedbackStatusUndoOptions): FeedbackStatusChange;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useFeedbackStatusLabels,
|
|
6
6
|
useFeedbackToastLabels
|
|
7
7
|
} from "./feedback-labels.js";
|
|
8
|
+
import { runMutation } from "./feedback-mutation.js";
|
|
8
9
|
const FEEDBACK_UNDO_DURATION = 8e3;
|
|
9
10
|
function useFeedbackStatusUndo(mutate, { labels, statusLabels } = {}) {
|
|
10
11
|
const words = useFeedbackToastLabels(labels);
|
|
@@ -14,21 +15,15 @@ function useFeedbackStatusUndo(mutate, { labels, statusLabels } = {}) {
|
|
|
14
15
|
const { id, title, status: previous } = row;
|
|
15
16
|
if (status === previous) return;
|
|
16
17
|
const nameOf = (value) => names[value] ?? value;
|
|
17
|
-
mutate(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{ onSuccess: () => void toast.success(words.statusRestored(nameOf(previous), title)) }
|
|
27
|
-
)
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
);
|
|
18
|
+
runMutation(mutate, { id, status }, () => {
|
|
19
|
+
toast.undo(words.statusChanged(nameOf(status), title), {
|
|
20
|
+
label: words.statusUndo,
|
|
21
|
+
duration: FEEDBACK_UNDO_DURATION,
|
|
22
|
+
onUndo: () => runMutation(mutate, { id, status: previous }, () => {
|
|
23
|
+
toast.success(words.statusRestored(nameOf(previous), title));
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
});
|
|
32
27
|
},
|
|
33
28
|
[mutate, words, names]
|
|
34
29
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-status-undo.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { toast } from \"../components/toast\";\nimport type { FeedbackStatus } from \"./feedback-vocabulary\";\nimport {\n useFeedbackStatusLabels,\n useFeedbackToastLabels,\n type FeedbackStatusLabels,\n type FeedbackToastLabels,\n} from \"./feedback-labels\";\nimport type { FeedbackRecord } from \"./feedback-record\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * A status change that can be taken back — §4.5 of the feedback contract, lifted from\n * keksdose's `changeStatusUndoably` (`frontend/src/features/feedback/feedback-page.tsx`,\n * dev #587: *\"Just clicked somewhere and a feedback row disappeared without knowing what\n * I set as new status.\"*).\n *\n * An inbox is usually filtered by status, so a row whose new status falls outside the\n * filter leaves the table the moment the change lands, with nothing on screen saying what\n * it became. The toast says it, names the report, and offers the old status back for 8 s\n * (the window keksdose's transactions Undo uses, feedback #184). Undo PATCHes the old\n * status and confirms that too. kastlan and Kurvenschmiede had no Undo at all.\n */\n\n/** The one PATCH a status change is: `PATCH /feedback/{id}` with `{ status }`. */\nexport interface FeedbackStatusPatch {\n id: number;\n status: FeedbackStatus;\n}\n\n/**\n * The page's status mutation. **TanStack Query's `
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-status-undo.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { toast } from \"../components/toast\";\nimport type { FeedbackStatus } from \"./feedback-vocabulary\";\nimport {\n useFeedbackStatusLabels,\n useFeedbackToastLabels,\n type FeedbackStatusLabels,\n type FeedbackToastLabels,\n} from \"./feedback-labels\";\nimport type { FeedbackRecord } from \"./feedback-record\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { runMutation, type FeedbackMutationCall } from \"./feedback-mutation\";\n\n/**\n * A status change that can be taken back — §4.5 of the feedback contract, lifted from\n * keksdose's `changeStatusUndoably` (`frontend/src/features/feedback/feedback-page.tsx`,\n * dev #587: *\"Just clicked somewhere and a feedback row disappeared without knowing what\n * I set as new status.\"*).\n *\n * An inbox is usually filtered by status, so a row whose new status falls outside the\n * filter leaves the table the moment the change lands, with nothing on screen saying what\n * it became. The toast says it, names the report, and offers the old status back for 8 s\n * (the window keksdose's transactions Undo uses, feedback #184). Undo PATCHes the old\n * status and confirms that too. kastlan and Kurvenschmiede had no Undo at all.\n */\n\n/** The one PATCH a status change is: `PATCH /feedback/{id}` with `{ status }`. */\nexport interface FeedbackStatusPatch {\n id: number;\n status: FeedbackStatus;\n}\n\n/**\n * The page's status mutation (0.52): **TanStack Query's `mutateAsync`** — pass\n * `update.mutateAsync`. The toast comes when the promise resolves, which it does for every\n * call (refactor audit 2026-10-30, FLW-3). A hand-written one returns a promise that\n * resolves once the PATCH has landed and rejects when it failed.\n *\n * Failure is the mutation's own business: its `onError` toasts the server's `detail`,\n * else `feedbackToast.updateFailed` (§4.4) — this hook adds no second error toast, and\n * catches the rejection.\n */\nexport type FeedbackStatusMutateAsync = (patch: FeedbackStatusPatch) => Promise<unknown>;\n\n/**\n * The callback form: TanStack Query's `mutate`, or a function that calls `onSuccess` once\n * the PATCH has landed. TanStack calls a per-call `onSuccess` for the LATEST `mutate()` on\n * its observer only, so a second change sent before the first answered — another swipe,\n * another pill, the Undo itself — lost the first change's Undo toast.\n *\n * @deprecated 0.52 — pass `update.mutateAsync` ({@link FeedbackStatusMutateAsync}), whose\n * promise settles for every call; removed in 0.53\n */\nexport type FeedbackStatusMutate = (patch: FeedbackStatusPatch, callbacks: { onSuccess: () => void }) => void;\n\n/** What the change needs to know about the row: which one, what it is called in the\n * toast, and where it stood (the status Undo goes back to). */\nexport type FeedbackStatusRow = Pick<FeedbackRecord, \"id\" | \"title\" | \"status\">;\n\n/** Set `row` to `status`, with the Undo toast. A no-op when it is already there. */\nexport type FeedbackStatusChange = (row: FeedbackStatusRow, status: FeedbackStatus) => void;\n\nexport interface FeedbackStatusUndoOptions {\n /** Over the `feedbackToast` namespace (the toast's words). */\n labels?: LabelOverride<FeedbackToastLabels>;\n /** Over the `feedbackStatus` namespace (the status names inside them). */\n statusLabels?: Partial<FeedbackStatusLabels>;\n}\n\n/** How long the Undo stays reachable: 8 s, keksdose's window (feedback #184). */\nexport const FEEDBACK_UNDO_DURATION = 8000;\n\n/**\n * The undoable status change — for the table's status cell, the row detail's pills and\n * the phone swipes alike (§4.5, §7.10: swipes included, Marcel 2026-10-04 — keksdose's\n * swipe commits go through its `changeStatusUndoably` too). Wire every one of them\n * through the function this returns:\n *\n * ```tsx\n * const update = useMutation({ mutationFn: api.updateFeedback, onError: … });\n * const changeStatus = useFeedbackStatusUndo(update.mutateAsync);\n * <FeedbackStatusTransitions … onPick={(status) => changeStatus(row, status)} />\n * ```\n *\n * Give it the PAGE's mutation, not one a row owns: a row the new status filters out\n * unmounts before the PATCH answers.\n *\n * The flow: PATCH `{ id, status }` → once it has landed a success toast\n * `statusChanged(newName, title)` with the action `statusUndo`, 8 s → Undo PATCHes\n * `{ id, status: previous }` → once that has landed `statusRestored(previousName, title)`.\n * Each change waits for its OWN answer, so two changes in flight each get their toast.\n *\n * `update.mutate` ({@link FeedbackStatusMutate}) still fits, deprecated since 0.52: with it\n * a second change before the first answered loses the first one's Undo. One signature\n * takes both rather than two overloads: TypeScript types an inline callback's parameters\n * from the FIRST overload, so a hand-written `(patch, { onSuccess }) => …` — the form this\n * hook documented until 0.52 — would no longer compile.\n */\nexport function useFeedbackStatusUndo(\n mutate: FeedbackMutationCall<FeedbackStatusPatch>,\n { labels, statusLabels }: FeedbackStatusUndoOptions = {},\n): FeedbackStatusChange {\n const words = useFeedbackToastLabels(labels);\n const names = useFeedbackStatusLabels(statusLabels);\n return useCallback<FeedbackStatusChange>(\n (row, status) => {\n const { id, title, status: previous } = row;\n if (status === previous) return;\n const nameOf = (value: FeedbackStatus) => names[value] ?? value;\n runMutation(mutate, { id, status }, () => {\n toast.undo(words.statusChanged(nameOf(status), title), {\n label: words.statusUndo,\n duration: FEEDBACK_UNDO_DURATION,\n onUndo: () =>\n runMutation(mutate, { id, status: previous }, () => {\n toast.success(words.statusRestored(nameOf(previous), title));\n }),\n });\n });\n },\n [mutate, words, names],\n );\n}\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AAEtB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAGP,SAAS,mBAA8C;AA2DhD,MAAM,yBAAyB;AA4B/B,SAAS,sBACd,QACA,EAAE,QAAQ,aAAa,IAA+B,CAAC,GACjC;AACtB,QAAM,QAAQ,uBAAuB,MAAM;AAC3C,QAAM,QAAQ,wBAAwB,YAAY;AAClD,SAAO;AAAA,IACL,CAAC,KAAK,WAAW;AACf,YAAM,EAAE,IAAI,OAAO,QAAQ,SAAS,IAAI;AACxC,UAAI,WAAW,SAAU;AACzB,YAAM,SAAS,CAAC,UAA0B,MAAM,KAAK,KAAK;AAC1D,kBAAY,QAAQ,EAAE,IAAI,OAAO,GAAG,MAAM;AACxC,cAAM,KAAK,MAAM,cAAc,OAAO,MAAM,GAAG,KAAK,GAAG;AAAA,UACrD,OAAO,MAAM;AAAA,UACb,UAAU;AAAA,UACV,QAAQ,MACN,YAAY,QAAQ,EAAE,IAAI,QAAQ,SAAS,GAAG,MAAM;AAClD,kBAAM,QAAQ,MAAM,eAAe,OAAO,QAAQ,GAAG,KAAK,CAAC;AAAA,UAC7D,CAAC;AAAA,QACL,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IACA,CAAC,QAAQ,OAAO,KAAK;AAAA,EACvB;AACF;","names":[]}
|
|
@@ -24,7 +24,7 @@ export interface FeedbackSubmitDialogProps {
|
|
|
24
24
|
onCategoryChange: (category: FeedbackCategory) => void;
|
|
25
25
|
submitting: boolean;
|
|
26
26
|
/** The form, and the `context` built from the same values the box showed. */
|
|
27
|
-
onSubmit: (data: FeedbackMultipleSubmission
|
|
27
|
+
onSubmit: (data: FeedbackMultipleSubmission<FeedbackCategory>, context: FeedbackContext) => void | Promise<void>;
|
|
28
28
|
user?: FeedbackSubmitter | null;
|
|
29
29
|
/** `location.href` when the dialog opened. */
|
|
30
30
|
url: string;
|
|
@@ -45,7 +45,7 @@ function FeedbackSubmitDialog({
|
|
|
45
45
|
onClose,
|
|
46
46
|
categories: FEEDBACK_PICKABLE_CATEGORIES.map((value) => ({ value, label: categoryLabels[value] })),
|
|
47
47
|
category,
|
|
48
|
-
onCategoryChange
|
|
48
|
+
onCategoryChange,
|
|
49
49
|
submitting,
|
|
50
50
|
attachments: "multiple",
|
|
51
51
|
maxAttachments: DEFAULT_MAX_ATTACHMENTS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-submit-dialog.tsx"],"sourcesContent":["import { toast } from \"../components/toast\";\nimport { captureAppScreenshot } from \"./feedback-capture\";\nimport { FeedbackContextBox, feedbackContext, type FeedbackSubmitter } from \"./feedback-context\";\nimport { FeedbackDialog, type FeedbackMultipleSubmission } from \"./feedback-dialog\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useFeedbackCategoryLabels, useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n DEFAULT_MAX_ATTACHMENTS,\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_PICKABLE_CATEGORIES,\n type FeedbackContext,\n type FeedbackEnvironment,\n} from \"./feedback-record\";\nimport type { FeedbackCategory } from \"./feedback-vocabulary\";\n\n/**\n * The open half of `useFeedbackSubmit`: the dialog it draws, with the context box and\n * everything that only exists while somebody is filing a report.\n *\n * A module of its own so it can load LATE (0.34, MOD-1 of the refactor audit 2026-10).\n * Every app mounts the hook in its top bar; this — the attachment field, the modal, the\n * context box, the capture — was ~11 kB gzip of each app's entry chunk, for a dialog most\n * sessions never open. feedback-submit.tsx reaches it only through a literal relative\n * `import()`, so the library build keeps it a module of its own (`bundle: false`) and an\n * app's bundler makes it a chunk of its own.\n *\n * Internal, and not in the barrel: nothing but that `import()` should reach it. The hook\n * keeps the state — open, category, the URL and whether it goes along, the send in\n * flight — and this draws it.\n */\n\n/** What `useFeedbackSubmit` hands its lazily loaded dialog. */\nexport interface FeedbackSubmitDialogProps {\n onClose: () => void;\n category: FeedbackCategory;\n onCategoryChange: (category: FeedbackCategory) => void;\n submitting: boolean;\n /** The form, and the `context` built from the same values the box showed. */\n onSubmit: (data: FeedbackMultipleSubmission
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-submit-dialog.tsx"],"sourcesContent":["import { toast } from \"../components/toast\";\nimport { captureAppScreenshot } from \"./feedback-capture\";\nimport { FeedbackContextBox, feedbackContext, type FeedbackSubmitter } from \"./feedback-context\";\nimport { FeedbackDialog, type FeedbackMultipleSubmission } from \"./feedback-dialog\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useFeedbackCategoryLabels, useFeedbackToastLabels } from \"./feedback-labels\";\nimport {\n DEFAULT_MAX_ATTACHMENTS,\n FEEDBACK_ATTACHMENT_ACCEPT,\n FEEDBACK_PICKABLE_CATEGORIES,\n type FeedbackContext,\n type FeedbackEnvironment,\n} from \"./feedback-record\";\nimport type { FeedbackCategory } from \"./feedback-vocabulary\";\n\n/**\n * The open half of `useFeedbackSubmit`: the dialog it draws, with the context box and\n * everything that only exists while somebody is filing a report.\n *\n * A module of its own so it can load LATE (0.34, MOD-1 of the refactor audit 2026-10).\n * Every app mounts the hook in its top bar; this — the attachment field, the modal, the\n * context box, the capture — was ~11 kB gzip of each app's entry chunk, for a dialog most\n * sessions never open. feedback-submit.tsx reaches it only through a literal relative\n * `import()`, so the library build keeps it a module of its own (`bundle: false`) and an\n * app's bundler makes it a chunk of its own.\n *\n * Internal, and not in the barrel: nothing but that `import()` should reach it. The hook\n * keeps the state — open, category, the URL and whether it goes along, the send in\n * flight — and this draws it.\n */\n\n/** What `useFeedbackSubmit` hands its lazily loaded dialog. */\nexport interface FeedbackSubmitDialogProps {\n onClose: () => void;\n category: FeedbackCategory;\n onCategoryChange: (category: FeedbackCategory) => void;\n submitting: boolean;\n /** The form, and the `context` built from the same values the box showed. */\n onSubmit: (data: FeedbackMultipleSubmission<FeedbackCategory>, context: FeedbackContext) => void | Promise<void>;\n user?: FeedbackSubmitter | null;\n /** `location.href` when the dialog opened. */\n url: string;\n attachUrl: boolean;\n onAttachUrlChange: (attachUrl: boolean) => void;\n environment?: FeedbackEnvironment | (string & {});\n version?: string;\n /** `UseFeedbackSubmitOptions.capture`, as the app passed it. */\n capture?: false | (() => Promise<File | null>);\n /** `UseFeedbackSubmitOptions.publicNotice` (0.38): the line under the body. */\n publicNotice?: boolean;\n}\n\n/** `FeedbackDialog` as `useFeedbackSubmit` settles it — see the hook for what it settles. */\nexport function FeedbackSubmitDialog({\n onClose,\n category,\n onCategoryChange,\n submitting,\n onSubmit,\n user,\n url,\n attachUrl,\n onAttachUrlChange,\n environment,\n version,\n capture,\n publicNotice = false,\n}: FeedbackSubmitDialogProps) {\n const toastText = useFeedbackToastLabels();\n const dialogText = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS);\n const categoryLabels = useFeedbackCategoryLabels();\n\n const onCaptureScreenshot =\n capture === false\n ? undefined\n : async () => {\n try {\n return await (capture ?? (() => captureAppScreenshot()))();\n } catch {\n toast.error(toastText.captureFailed);\n return null;\n }\n };\n\n return (\n <FeedbackDialog\n open\n onClose={onClose}\n categories={FEEDBACK_PICKABLE_CATEGORIES.map((value) => ({ value, label: categoryLabels[value] }))}\n category={category}\n onCategoryChange={onCategoryChange}\n submitting={submitting}\n attachments=\"multiple\"\n maxAttachments={DEFAULT_MAX_ATTACHMENTS}\n requireBody={false}\n attachmentAccept={FEEDBACK_ATTACHMENT_ACCEPT}\n onAttachmentError={(kind, info) =>\n toast.error(\n kind === \"type\"\n ? toastText.attachmentUnsupported\n : kind === \"size\"\n ? toastText.attachmentTooLarge\n : // More files at once than there was room for: the surplus was dropped, the\n // rest kept. The count is how many fit, as keksdose says it.\n toastText.attachmentTooMany(info.max ?? DEFAULT_MAX_ATTACHMENTS),\n )\n }\n onCaptureScreenshot={onCaptureScreenshot}\n publicNotice={publicNotice ? (dialogText.publicNotice ?? DEFAULT_FEEDBACK_DIALOG_LABELS.publicNotice) : undefined}\n // The context is read off the very values the box below shows (§3.2).\n onSubmit={(data) => onSubmit(data, feedbackContext({ user, attachUrl, url, environment, version }))}\n contextSlot={\n <FeedbackContextBox user={user} url={url} attachUrl={attachUrl} onAttachUrlChange={onAttachUrlChange} />\n }\n />\n );\n}\n"],"mappings":";AAiHQ;AAjHR,SAAS,aAAa;AACtB,SAAS,4BAA4B;AACrC,SAAS,oBAAoB,uBAA+C;AAC5E,SAAS,sBAAuD;AAChE,SAAS,sCAAsC;AAC/C,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B,8BAA8B;AAClE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAyCA,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AACjB,GAA8B;AAC5B,QAAM,YAAY,uBAAuB;AACzC,QAAM,aAAa,aAAa,kBAAkB,8BAA8B;AAChF,QAAM,iBAAiB,0BAA0B;AAEjD,QAAM,sBACJ,YAAY,QACR,SACA,YAAY;AACV,QAAI;AACF,aAAO,OAAO,YAAY,MAAM,qBAAqB,IAAI;AAAA,IAC3D,QAAQ;AACN,YAAM,MAAM,UAAU,aAAa;AACnC,aAAO;AAAA,IACT;AAAA,EACF;AAEN,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ;AAAA,MACA,YAAY,6BAA6B,IAAI,CAAC,WAAW,EAAE,OAAO,OAAO,eAAe,KAAK,EAAE,EAAE;AAAA,MACjG;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,kBAAkB;AAAA,MAClB,mBAAmB,CAAC,MAAM,SACxB,MAAM;AAAA,QACJ,SAAS,SACL,UAAU,wBACV,SAAS,SACP,UAAU;AAAA;AAAA;AAAA,UAGV,UAAU,kBAAkB,KAAK,OAAO,uBAAuB;AAAA;AAAA,MACvE;AAAA,MAEF;AAAA,MACA,cAAc,eAAgB,WAAW,gBAAgB,+BAA+B,eAAgB;AAAA,MAExG,UAAU,CAAC,SAAS,SAAS,MAAM,gBAAgB,EAAE,MAAM,WAAW,KAAK,aAAa,QAAQ,CAAC,CAAC;AAAA,MAClG,aACE,oBAAC,sBAAmB,MAAY,KAAU,WAAsB,mBAAsC;AAAA;AAAA,EAE1G;AAEJ;","names":[]}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { Suspense, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { toast } from "../components/toast.js";
|
|
5
|
+
import { lazyWithFallback } from "../lib/lazy-with-fallback.js";
|
|
5
6
|
import { useFeedbackToastLabels } from "./feedback-labels.js";
|
|
6
7
|
import { FEEDBACK_PICKABLE_CATEGORIES } from "./feedback-record.js";
|
|
7
|
-
const LazySubmitDialog =
|
|
8
|
-
() => import("./feedback-submit-dialog.js").then((module) =>
|
|
8
|
+
const LazySubmitDialog = lazyWithFallback(
|
|
9
|
+
() => import("./feedback-submit-dialog.js").then((module) => module.FeedbackSubmitDialog),
|
|
10
|
+
SubmitDialogUnavailable
|
|
9
11
|
);
|
|
12
|
+
function SubmitDialogUnavailable({ onClose }) {
|
|
13
|
+
useLayoutEffect(onClose, [onClose]);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
10
16
|
function useFeedbackSubmit(options) {
|
|
11
17
|
const { upload, create, user, environment, version, onSubmitted, errorMessage, capture, publicNotice } = options;
|
|
12
18
|
const toastText = useFeedbackToastLabels();
|