@eifi1/ui-kit 0.32.1 → 0.33.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 +16 -8
- package/dist/account/delete-account-setting.d.ts +10 -10
- package/dist/account/delete-account-setting.js +2 -1
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting.d.ts +5 -0
- package/dist/account/email-change-setting.js +4 -3
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +4 -0
- package/dist/account/sessions-setting.js +3 -2
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.d.ts +10 -10
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +10 -10
- package/dist/admin/invitations-panel.js +4 -3
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/plan-change.d.ts +100 -0
- package/dist/admin/plan-change.js +208 -0
- package/dist/admin/plan-change.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +10 -10
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.d.ts +7 -7
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
- package/dist/admin/user-roster.d.ts +7 -7
- package/dist/admin/user-roster.js +2 -2
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +13 -2
- package/dist/auth/auth-errors.js +5 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +4 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/register-form.d.ts +10 -10
- package/dist/billing/billing-banner.d.ts +13 -11
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +10 -10
- package/dist/billing/billing-labels.js +10 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-lock.d.ts +39 -12
- package/dist/billing/billing-lock.js +9 -3
- package/dist/billing/billing-lock.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +154 -0
- package/dist/billing/billing-overview.js +2 -0
- package/dist/billing/billing-overview.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +10 -10
- package/dist/billing/checkout-processing.d.ts +217 -0
- package/dist/billing/checkout-processing.js +198 -0
- package/dist/billing/checkout-processing.js.map +1 -0
- package/dist/billing/checkout-return.d.ts +51 -0
- package/dist/billing/checkout-return.js +43 -0
- package/dist/billing/checkout-return.js.map +1 -0
- package/dist/billing/pay-page.d.ts +210 -0
- package/dist/billing/pay-page.js +113 -0
- package/dist/billing/pay-page.js.map +1 -0
- package/dist/billing/plan-card.d.ts +10 -10
- package/dist/billing/plan-limit-notice.d.ts +10 -10
- package/dist/billing/plan-limit-toast.d.ts +143 -0
- package/dist/billing/plan-limit-toast.js +40 -0
- package/dist/billing/plan-limit-toast.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +38 -15
- package/dist/billing/subscription-actions.js +9 -2
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/billing/subscription-status-chip.d.ts +9 -9
- package/dist/bin/eifi1-pay-page.mjs +166 -0
- package/dist/chart.d.ts +10 -10
- package/dist/components/account-chips.d.ts +6 -1
- package/dist/components/account-settings.d.ts +4 -1
- package/dist/components/account-settings.js +2 -1
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +10 -10
- package/dist/components/autocomplete.d.ts +2 -1
- package/dist/components/autocomplete.js +3 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -10
- package/dist/components/button-group.js +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +10 -10
- package/dist/components/calculator.js +3 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +2 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +2 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +19 -17
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +3 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox.d.ts +4 -3
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +10 -10
- package/dist/components/confirm-dialog.js +6 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +11 -11
- package/dist/components/country-select.d.ts +2 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +10 -10
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +12 -1
- package/dist/components/data-table-filter-popover.d.ts +12 -1
- package/dist/components/data-table-filters.d.ts +12 -1
- package/dist/components/data-table-labels.d.ts +12 -1
- package/dist/components/data-table-pagination.d.ts +12 -1
- package/dist/components/data-table.d.ts +12 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +6 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +3 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -10
- package/dist/components/field-parts.d.ts +32 -4
- package/dist/components/field-parts.js +54 -19
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/file-button.d.ts +10 -10
- package/dist/components/file-button.js +3 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -11
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +5 -5
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +9 -9
- package/dist/components/form-actions.js +3 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +10 -10
- package/dist/components/inline-edit-field.d.ts +2 -1
- package/dist/components/inline-edit-field.js +6 -6
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +10 -10
- package/dist/components/legal-page.d.ts +10 -10
- package/dist/components/legal.d.ts +10 -10
- package/dist/components/list.d.ts +2 -1
- package/dist/components/list.js +4 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +10 -10
- package/dist/components/month-picker.js +54 -44
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +3 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +10 -10
- package/dist/components/number-input.d.ts +10 -10
- package/dist/components/numpad-sheet.d.ts +10 -10
- package/dist/components/numpad-sheet.js +2 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.js +6 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/phone-input.d.ts +10 -10
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.js +2 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +5 -5
- package/dist/components/row-actions.js +130 -38
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/series-chart.d.ts +10 -10
- package/dist/components/settings-fields.d.ts +10 -10
- package/dist/components/share-card.d.ts +7 -7
- package/dist/components/share-card.js +26 -10
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/status-dot.d.ts +1 -0
- package/dist/components/status-dot.js +2 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +2 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +88 -17
- package/dist/components/swipeable-row.js +46 -9
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +2 -1
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.d.ts +2 -1
- package/dist/components/table.js +6 -0
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +9 -9
- package/dist/components/time-input.d.ts +10 -10
- package/dist/components/toggle-group.d.ts +2 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -0
- package/dist/components/translation-review-editor.js +4 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -0
- package/dist/components/translation-review.d.ts +1 -0
- package/dist/components/translation-review.js +15 -18
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +9 -9
- package/dist/components/ui.js +48 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +12 -1
- package/dist/components/user-avatar.d.ts +1 -0
- package/dist/components/write-lock.d.ts +101 -13
- package/dist/components/write-lock.js +37 -13
- package/dist/components/write-lock.js.map +1 -1
- package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
- package/dist/data-table.d.ts +12 -1
- package/dist/{delete-account-setting-FLyJf-kZ.d.ts → delete-account-setting-DiPX4Vue.d.ts} +365 -47
- package/dist/feedback/feedback-attachment.d.ts +10 -10
- package/dist/feedback/feedback-attachment.js +7 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +10 -10
- package/dist/feedback/feedback-crash.d.ts +10 -10
- package/dist/feedback/feedback-dialog.d.ts +10 -10
- package/dist/feedback/feedback-inbox.d.ts +9 -9
- package/dist/feedback/feedback-inbox.js +7 -3
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +10 -10
- package/dist/feedback/feedback-menu.d.ts +3 -3
- package/dist/feedback/feedback-record.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +10 -10
- package/dist/feedback/feedback-submit.d.ts +10 -10
- package/dist/feedback/feedback-swipe.d.ts +10 -10
- package/dist/feedback/feedback-swipe.js +3 -6
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +10 -10
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +10 -10
- package/dist/feedback.d.ts +10 -10
- package/dist/hooks/use-file-drop.d.ts +10 -10
- package/dist/i18n/defaults.d.ts +10 -10
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -10
- package/dist/i18n/german.js +34 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -10
- package/dist/i18n/locales/de-CH.d.ts +10 -10
- package/dist/i18n/locales/en.d.ts +10 -10
- package/dist/i18n/locales/es.d.ts +10 -10
- package/dist/i18n/locales/es.js +34 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -10
- package/dist/i18n/locales/fr.js +35 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -10
- package/dist/i18n/locales/hu.js +36 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -10
- package/dist/i18n/locales/it.js +34 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -10
- package/dist/i18n/locales/zh.js +34 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -10
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +10 -10
- package/dist/landing/landing-sections.d.ts +10 -10
- package/dist/landing/public-header.d.ts +10 -10
- package/dist/landing/routing.d.ts +10 -10
- package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
- package/dist/pay/index.html +30 -0
- package/dist/pay/pay.css +126 -0
- package/dist/pay/pay.js +198 -0
- package/dist/rhf/fields.d.ts +10 -10
- package/dist/rhf/form.d.ts +10 -10
- package/dist/rhf.d.ts +10 -10
- package/dist/settings/settings-section.d.ts +10 -10
- package/dist/shell/app-shell.d.ts +9 -9
- package/dist/shell/app-shell.js +2 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +10 -10
- package/dist/shell/topbar-action-menu.d.ts +1 -0
- package/dist/shell.d.ts +10 -10
- package/dist/testing/css-variables.d.ts +74 -0
- package/dist/testing/css-variables.generated.d.ts +7 -0
- package/dist/testing/css-variables.generated.js +8 -0
- package/dist/testing/css-variables.generated.js.map +1 -0
- package/dist/testing/css-variables.js +93 -0
- package/dist/testing/css-variables.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/theme/contrast-tokens.d.ts +44 -1
- package/dist/theme/contrast-tokens.js +21 -0
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.js +60 -11
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js +45 -43
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +11 -11
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +10 -10
- package/package.json +10 -2
- package/src/account/delete-account-setting.tsx +6 -1
- package/src/account/email-change-setting.tsx +9 -3
- package/src/account/sessions-setting.tsx +7 -2
- package/src/admin/admin-action-confirm.tsx +2 -1
- package/src/admin/invitations-panel.tsx +9 -3
- package/src/admin/plan-change.tsx +459 -0
- package/src/admin/reviewer-scope-editor.tsx +2 -1
- package/src/admin/role-select.tsx +2 -1
- package/src/admin/user-roster.tsx +5 -3
- package/src/auth/auth-errors.ts +25 -2
- package/src/auth/complete-name-dialog.tsx +6 -2
- package/src/billing/billing-banner.tsx +3 -1
- package/src/billing/billing-labels.ts +45 -0
- package/src/billing/billing-lock.ts +36 -3
- package/src/billing/billing-overview.ts +58 -0
- package/src/billing/checkout-processing.ts +391 -0
- package/src/billing/checkout-return.ts +90 -0
- package/src/billing/pay-page.ts +242 -0
- package/src/billing/plan-limit-toast.tsx +91 -0
- package/src/billing/subscription-actions.tsx +41 -8
- package/src/components/account-settings.tsx +6 -2
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-group.tsx +4 -1
- package/src/components/calculator.tsx +10 -6
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +2 -1
- package/src/components/chip.tsx +37 -32
- package/src/components/choice-card.tsx +4 -2
- package/src/components/combobox.tsx +4 -4
- package/src/components/confirm-dialog.tsx +13 -6
- package/src/components/country-select.tsx +2 -2
- package/src/components/danger-confirm.tsx +2 -2
- package/src/components/data-table.tsx +20 -9
- package/src/components/date-picker.tsx +10 -7
- package/src/components/entity-combobox.tsx +3 -3
- package/src/components/field-parts.tsx +61 -14
- package/src/components/file-button.tsx +5 -4
- package/src/components/file-dropzone.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -5
- package/src/components/form-actions.tsx +12 -5
- package/src/components/inline-edit-field.tsx +13 -13
- package/src/components/list.tsx +10 -9
- package/src/components/month-picker.tsx +8 -2
- package/src/components/multi-entity-combobox.tsx +3 -3
- package/src/components/numpad-sheet.tsx +8 -4
- package/src/components/page-header.tsx +15 -1
- package/src/components/popover.tsx +4 -0
- package/src/components/row-actions.tsx +201 -50
- package/src/components/share-card.tsx +67 -13
- package/src/components/status-dot.tsx +7 -7
- package/src/components/swatch-picker.tsx +2 -2
- package/src/components/swipeable-row.tsx +138 -27
- package/src/components/switch.tsx +2 -1
- package/src/components/table.tsx +8 -1
- package/src/components/toggle-group.tsx +5 -3
- package/src/components/translation-review-editor.tsx +8 -3
- package/src/components/translation-review.tsx +18 -22
- package/src/components/ui.tsx +98 -44
- package/src/components/write-lock.tsx +150 -24
- package/src/feedback/feedback-attachment.tsx +13 -9
- package/src/feedback/feedback-inbox.tsx +10 -6
- package/src/feedback/feedback-row-detail.tsx +3 -2
- package/src/feedback/feedback-swipe.tsx +6 -11
- package/src/feedback/feedback-table.tsx +2 -1
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +38 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +34 -0
- package/src/i18n/locales/fr.ts +39 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +35 -0
- package/src/i18n/locales/zh.ts +33 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +48 -4
- package/src/shell/app-shell.tsx +11 -6
- package/src/testing/css-variables.generated.ts +112 -0
- package/src/testing/css-variables.ts +191 -0
- package/src/testing/index.ts +13 -0
- package/src/theme/contrast-tokens.ts +53 -2
- package/src/theme/palette-derive.ts +87 -10
- package/src/theme/palette-presets.ts +45 -43
- package/src/wizard/stepper-nav.tsx +2 -1
- package/tokens.css +306 -46
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-row-detail.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { MouseEvent } from \"react\";\nimport { Download, ExternalLink, FileText, 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/ui\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, useKitLocale } 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 attachmentName,\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\";\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**, **Outcome** (the admin's\n * outcome editor, Rework, Open page), **Send for rework** (only while reworking) and\n * **Status**. No comment thread (§2.2: kastlan's comments fold into the body, §7.7).\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. de-CH: \"Ergebnis\" */\n outcome: 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. de-CH: \"Ergebnis hinzufügen\" */\n outcomeAdd: string;\n /** The same button once there is one. de-CH: \"Aktualisieren\" */\n outcomeUpdate: string;\n /** The line above the outcome editor. 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 /** The link to the page the report was filed on. de-CH: \"Seite öffnen\" */\n openPage: 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}\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n resolvedAt: (date) => `Resolved: ${date}`,\n outcomeAdd: \"Add outcome\",\n outcomeUpdate: \"Update\",\n outcomePlaceholder: \"What was done, decided, or why this won't be addressed.\",\n rework: \"Rework\",\n openPage: \"Open page\",\n reworkTitle: \"Send for rework\",\n reworkSend: \"Send rework\",\n reworkPlaceholder: \"What still needs refinement? Any new constraints or change of direction.\",\n reworkUploadFailed: \"The file could not be uploaded. The rework was not sent.\",\n status: \"Status\",\n};\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: Partial<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\">,\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 ];\n return [...new Set(urls)];\n}\n\n/** The bytes behind a fetcher's answer — a `Blob` as it is, a `Response` checked. */\nasync function toBlob(answer: Blob | Response): Promise<Blob> {\n if (typeof Response !== \"undefined\" && answer instanceof Response) {\n if (!answer.ok) throw new Error(`HTTP ${answer.status}`);\n return answer.blob();\n }\n return answer as Blob;\n}\n\n/** No fetcher: the browser's own `fetch`, with the cookies a same-origin app uses. */\nconst plainFetch: AuthedFetcher = (url, { signal }) => fetch(url, { signal, credentials: \"same-origin\" });\n\n/**\n * A PDF or a text log (keksdose `FeedbackFileDownload`, dev #578): the bytes sit behind\n * an authenticated route that serves them as `attachment`, never inline (§3.5), so a\n * plain link would be a 401 the moment it was opened. Fetched through the app's\n * fetcher and handed to a synthetic anchor; a failure says so in a toast.\n */\nfunction AttachmentDownload({\n url,\n fetcher,\n labels,\n}: {\n url: string;\n fetcher: AuthedFetcher;\n labels: FeedbackDetailLabels;\n}) {\n const [busy, setBusy] = useState(false);\n const name = attachmentName(url);\n const download = async (event: MouseEvent) => {\n // The row around the detail may toggle on a click.\n event.stopPropagation();\n setBusy(true);\n try {\n const blob = await toBlob(await fetcher(url, { signal: new AbortController().signal }));\n const href = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = href;\n anchor.download = name;\n anchor.click();\n // After the click has handed the URL to the download, not before.\n setTimeout(() => URL.revokeObjectURL(href), 0);\n } catch {\n toast.error(labels.downloadFailed);\n } finally {\n setBusy(false);\n }\n };\n return (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n pending={busy}\n onClick={(event) => void download(event)}\n aria-label={labels.download(name)}\n className=\"max-w-full\"\n >\n <FileText className=\"size-3.5 shrink-0\" aria-hidden />\n <span className=\"min-w-0 max-w-[14rem] truncate\">{name}</span>\n <Download className=\"size-3.5 shrink-0\" aria-hidden />\n </Button>\n );\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 outcome editor: the admin on the inbox (`canEdit`). */\n canEditOutcome: boolean;\n /** \"Rework\": the admin or the author, on an answered row\n * ({@link FEEDBACK_REWORKABLE_STATUSES}) — §3.4, anyone else gets the server's 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: (canEdit || 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. */\n outcome?: string | null;\n status?: FeedbackStatus;\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\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 /** The PATCH — see {@link FeedbackDetailUpdate}. Called with `{ id, body }` only. */\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?: boolean;\n labels?: Partial<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter — or the admin — 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 * - **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 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 // 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 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}\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. Left out, a rework is a\n * note 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 \"Capture screenshot\" — see {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's file field, 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?: boolean;\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 labels?: Partial<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 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 [editingOutcome, setEditingOutcome] = 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 const files = feedbackAttachmentUrls(row);\n const fetchFile = fetcher ?? plainFetch;\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 const saveOutcome = (value: string) => {\n // Blank saves `null`: no outcome, rather than an empty one.\n const next = value.trim() ? value.trim() : null;\n if (next === row.outcome) {\n setEditingOutcome(false);\n return;\n }\n onUpdate({ id: row.id, outcome: next }, { onSuccess: () => setEditingOutcome(false) });\n };\n\n const showOutcomeEditor = access.canEditOutcome && editingOutcome;\n const showRework = access.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-[var(--text-primary)] [overflow-wrap:anywhere]\">\n {row.title}\n </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-[var(--border)]\"\n />\n ) : (\n <AttachmentDownload key={url} url={url} fetcher={fetchFile} labels={labels} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={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-[var(--text-muted)]\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n {showOutcomeEditor ? (\n <FeedbackNoteEditor\n key={row.id}\n initial={row.outcome ?? \"\"}\n pending={pending}\n commit={commit}\n onSave={saveOutcome}\n onCancel={() => setEditingOutcome(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={labels.outcomePlaceholder}\n />\n ) : (\n <div className=\"space-y-2\">\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n {(access.canEditOutcome || (access.canRework && !reworking) || pageHref) && (\n <div className=\"flex flex-wrap gap-2\">\n {access.canEditOutcome && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingOutcome(true);\n }}\n >\n {row.outcome ? labels.outcomeUpdate : labels.outcomeAdd}\n </Button>\n )}\n {access.canRework && !reworking && (\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 )}\n {pageHref && (\n // A real href (keksdose dev #451): middle-click and ⌘/Ctrl-click are\n // the browser's, and only a link has them.\n <Button href={pageHref} variant=\"secondary\" onClick={(event) => event.stopPropagation()}>\n <ExternalLink className=\"size-3.5\" aria-hidden /> {labels.openPage}\n </Button>\n )}\n </div>\n )}\n </div>\n )}\n </FeedbackDetailSection>\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\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 </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 { MouseEvent } from \"react\";\nimport { Download, ExternalLink, FileText, 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/ui\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, useKitLocale } 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 attachmentName,\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\";\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**, **Outcome** (the admin's\n * outcome editor, Rework, Open page), **Send for rework** (only while reworking) and\n * **Status**. No comment thread (§2.2: kastlan's comments fold into the body, §7.7).\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. de-CH: \"Ergebnis\" */\n outcome: 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. de-CH: \"Ergebnis hinzufügen\" */\n outcomeAdd: string;\n /** The same button once there is one. de-CH: \"Aktualisieren\" */\n outcomeUpdate: string;\n /** The line above the outcome editor. 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 /** The link to the page the report was filed on. de-CH: \"Seite öffnen\" */\n openPage: 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}\n\nexport const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {\n subject: \"Subject\",\n body: \"Description\",\n edit: \"Edit\",\n editDescription: \"Edit description\",\n save: \"Save\",\n cancel: \"Cancel\",\n url: \"URL\",\n copyUrl: \"Copy URL\",\n attachment: \"Attachment\",\n download: (name) => `Download ${name}`,\n downloadFailed: \"The attachment could not be downloaded.\",\n outcome: \"Outcome\",\n resolvedAt: (date) => `Resolved: ${date}`,\n outcomeAdd: \"Add outcome\",\n outcomeUpdate: \"Update\",\n outcomePlaceholder: \"What was done, decided, or why this won't be addressed.\",\n rework: \"Rework\",\n openPage: \"Open page\",\n reworkTitle: \"Send for rework\",\n reworkSend: \"Send rework\",\n reworkPlaceholder: \"What still needs refinement? Any new constraints or change of direction.\",\n reworkUploadFailed: \"The file could not be uploaded. The rework was not sent.\",\n status: \"Status\",\n};\n\n/** `feedbackDetail` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackDetailLabels(labels?: Partial<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\">,\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 ];\n return [...new Set(urls)];\n}\n\n/** The bytes behind a fetcher's answer — a `Blob` as it is, a `Response` checked. */\nasync function toBlob(answer: Blob | Response): Promise<Blob> {\n if (typeof Response !== \"undefined\" && answer instanceof Response) {\n if (!answer.ok) throw new Error(`HTTP ${answer.status}`);\n return answer.blob();\n }\n return answer as Blob;\n}\n\n/** No fetcher: the browser's own `fetch`, with the cookies a same-origin app uses. */\nconst plainFetch: AuthedFetcher = (url, { signal }) => fetch(url, { signal, credentials: \"same-origin\" });\n\n/**\n * A PDF or a text log (keksdose `FeedbackFileDownload`, dev #578): the bytes sit behind\n * an authenticated route that serves them as `attachment`, never inline (§3.5), so a\n * plain link would be a 401 the moment it was opened. Fetched through the app's\n * fetcher and handed to a synthetic anchor; a failure says so in a toast.\n */\nfunction AttachmentDownload({\n url,\n fetcher,\n labels,\n}: {\n url: string;\n fetcher: AuthedFetcher;\n labels: FeedbackDetailLabels;\n}) {\n const [busy, setBusy] = useState(false);\n const name = attachmentName(url);\n const download = async (event: MouseEvent) => {\n // The row around the detail may toggle on a click.\n event.stopPropagation();\n setBusy(true);\n try {\n const blob = await toBlob(await fetcher(url, { signal: new AbortController().signal }));\n const href = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = href;\n anchor.download = name;\n anchor.click();\n // After the click has handed the URL to the download, not before.\n setTimeout(() => URL.revokeObjectURL(href), 0);\n } catch {\n toast.error(labels.downloadFailed);\n } finally {\n setBusy(false);\n }\n };\n return (\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n pending={busy}\n onClick={(event) => void download(event)}\n aria-label={labels.download(name)}\n className=\"max-w-full\"\n >\n <FileText className=\"size-3.5 shrink-0\" aria-hidden />\n <span className=\"min-w-0 max-w-[14rem] truncate\">{name}</span>\n <Download className=\"size-3.5 shrink-0\" aria-hidden />\n </Button>\n );\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 outcome editor: the admin on the inbox (`canEdit`). */\n canEditOutcome: boolean;\n /** \"Rework\": the admin or the author, on an answered row\n * ({@link FEEDBACK_REWORKABLE_STATUSES}) — §3.4, anyone else gets the server's 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: (canEdit || 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. */\n outcome?: string | null;\n status?: FeedbackStatus;\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\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 /** The PATCH — see {@link FeedbackDetailUpdate}. Called with `{ id, body }` only. */\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?: Partial<FeedbackDetailLabels>;\n}\n\n/**\n * The rework editor (§4.4.5): the submitter — or the admin — 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 * - **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 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 // 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 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}\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. Left out, a rework is a\n * note 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 \"Capture screenshot\" — see {@link FeedbackReworkSectionProps}. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** The rework's file field, 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 labels?: Partial<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 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 [editingOutcome, setEditingOutcome] = 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 const files = feedbackAttachmentUrls(row);\n const fetchFile = fetcher ?? plainFetch;\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 const saveOutcome = (value: string) => {\n // Blank saves `null`: no outcome, rather than an empty one.\n const next = value.trim() ? value.trim() : null;\n if (next === row.outcome) {\n setEditingOutcome(false);\n return;\n }\n onUpdate({ id: row.id, outcome: next }, { onSuccess: () => setEditingOutcome(false) });\n };\n\n const showOutcomeEditor = access.canEditOutcome && editingOutcome;\n const showRework = access.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-[var(--text-primary)] [overflow-wrap:anywhere]\">\n {row.title}\n </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-[var(--border)]\"\n />\n ) : (\n <AttachmentDownload key={url} url={url} fetcher={fetchFile} labels={labels} />\n ),\n )}\n </div>\n </FeedbackDetailSection>\n )}\n\n <FeedbackDetailSection\n title={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-[var(--text-muted)]\">{labels.resolvedAt(dateText(row.resolved_at))}</span>\n ) : undefined\n }\n >\n {showOutcomeEditor ? (\n <FeedbackNoteEditor\n key={row.id}\n initial={row.outcome ?? \"\"}\n pending={pending}\n commit={commit}\n onSave={saveOutcome}\n onCancel={() => setEditingOutcome(false)}\n saveLabel={labels.save}\n cancelLabel={labels.cancel}\n placeholder={labels.outcomePlaceholder}\n />\n ) : (\n <div className=\"space-y-2\">\n <FeedbackProse>{row.outcome || undefined}</FeedbackProse>\n {(access.canEditOutcome || (access.canRework && !reworking) || pageHref) && (\n <div className=\"flex flex-wrap gap-2\">\n {access.canEditOutcome && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={(event) => {\n event.stopPropagation();\n setEditingOutcome(true);\n }}\n >\n {row.outcome ? labels.outcomeUpdate : labels.outcomeAdd}\n </Button>\n )}\n {access.canRework && !reworking && (\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 )}\n {pageHref && (\n // A real href (keksdose dev #451): middle-click and ⌘/Ctrl-click are\n // the browser's, and only a link has them.\n <Button href={pageHref} variant=\"secondary\" onClick={(event) => event.stopPropagation()}>\n <ExternalLink className=\"size-3.5\" aria-hidden /> {labels.openPage}\n </Button>\n )}\n </div>\n )}\n </div>\n )}\n </FeedbackDetailSection>\n\n {showRework && (\n <FeedbackReworkSection\n row={row}\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 </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
export { z as FEEDBACK_UNDO_DURATION, aB as FeedbackStatusChange, aD as FeedbackStatusMutate, aE as FeedbackStatusPatch, aF as FeedbackStatusRow, aH as FeedbackStatusUndoOptions, bq as useFeedbackStatusUndo } from '../delete-account-setting-
|
|
1
|
+
export { z as FEEDBACK_UNDO_DURATION, aB as FeedbackStatusChange, aD as FeedbackStatusMutate, aE as FeedbackStatusPatch, aF as FeedbackStatusRow, aH as FeedbackStatusUndoOptions, bq as useFeedbackStatusUndo } from '../delete-account-setting-DiPX4Vue.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../components/tooltip.js';
|
|
4
4
|
import '../hooks/use-anchored-rect.js';
|
|
5
5
|
import '../lib/clipping.js';
|
|
6
|
-
import '../data-table-labels-
|
|
6
|
+
import '../data-table-labels-BJ5JE_X2.js';
|
|
7
7
|
import '../components/data-table-sort.js';
|
|
8
8
|
import '../components/swipeable-row.js';
|
|
9
|
+
import '../components/status-dot.js';
|
|
10
|
+
import '../components/progress-bar.js';
|
|
11
|
+
import '../components/signed-amount.js';
|
|
12
|
+
import '../components/stat-tile.js';
|
|
13
|
+
import '../components/chip.js';
|
|
14
|
+
import 'lucide-react';
|
|
15
|
+
import '../components/field-parts.js';
|
|
16
|
+
import '../components/write-lock.js';
|
|
9
17
|
import '../components/mini-calendar.js';
|
|
10
18
|
import '../components/calendar-heatmap.js';
|
|
11
19
|
import '../components/popover.js';
|
|
12
20
|
import '../hooks/use-breakpoint.js';
|
|
13
|
-
import '../components/chip.js';
|
|
14
|
-
import 'lucide-react';
|
|
15
|
-
import '../components/field-parts.js';
|
|
16
21
|
import '../components/field-sync.js';
|
|
17
22
|
import '../wizard/types.js';
|
|
18
23
|
import '../tour/tour.js';
|
|
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
|
|
|
24
29
|
import '../components/series-chart-labels.js';
|
|
25
30
|
import '../components/pie-chart-labels.js';
|
|
26
31
|
import '../components/sparkline.js';
|
|
27
|
-
import '../components/stat-tile.js';
|
|
28
32
|
import '../components/signature-pad.js';
|
|
29
33
|
import '../components/password-strength.js';
|
|
30
34
|
import '../components/swatch-picker.js';
|
|
@@ -36,9 +40,6 @@ import '../hooks/use-search-param-state.js';
|
|
|
36
40
|
import '../components/measured-grid.js';
|
|
37
41
|
import '../components/user-avatar.js';
|
|
38
42
|
import '../lib/person-name.js';
|
|
39
|
-
import '../components/status-dot.js';
|
|
40
|
-
import '../components/progress-bar.js';
|
|
41
|
-
import '../components/signed-amount.js';
|
|
42
43
|
import '../shell/topbar-action-menu.js';
|
|
43
44
|
import '../components/menu-item.js';
|
|
44
45
|
import '../components/floating-panel.js';
|
|
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
|
|
|
54
55
|
import '../components/authed-image.js';
|
|
55
56
|
import '../components/image-grid.js';
|
|
56
57
|
import '../components/lightbox.js';
|
|
57
|
-
import '../components/write-lock.js';
|
|
58
58
|
import '../auth/sign-in-form.js';
|
|
59
59
|
import '../landing/access.js';
|
|
60
60
|
import '../landing/landing-labels.js';
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { Q as FeedbackCategory, $ as FeedbackContext, aM as FeedbackSubmitter, ah as FeedbackEnvironment } from '../delete-account-setting-
|
|
2
|
+
import { Q as FeedbackCategory, $ as FeedbackContext, aM as FeedbackSubmitter, ah as FeedbackEnvironment } from '../delete-account-setting-DiPX4Vue.js';
|
|
3
3
|
import '../components/tooltip.js';
|
|
4
4
|
import '../hooks/use-anchored-rect.js';
|
|
5
5
|
import '../lib/clipping.js';
|
|
6
|
-
import '../data-table-labels-
|
|
6
|
+
import '../data-table-labels-BJ5JE_X2.js';
|
|
7
7
|
import '../components/data-table-sort.js';
|
|
8
8
|
import '../components/swipeable-row.js';
|
|
9
|
+
import '../components/status-dot.js';
|
|
10
|
+
import '../components/progress-bar.js';
|
|
11
|
+
import '../components/signed-amount.js';
|
|
12
|
+
import '../components/stat-tile.js';
|
|
13
|
+
import '../components/chip.js';
|
|
14
|
+
import 'lucide-react';
|
|
15
|
+
import '../components/field-parts.js';
|
|
16
|
+
import '../components/write-lock.js';
|
|
9
17
|
import '../components/mini-calendar.js';
|
|
10
18
|
import '../components/calendar-heatmap.js';
|
|
11
19
|
import '../components/popover.js';
|
|
12
20
|
import '../hooks/use-breakpoint.js';
|
|
13
|
-
import '../components/chip.js';
|
|
14
|
-
import 'lucide-react';
|
|
15
|
-
import '../components/field-parts.js';
|
|
16
21
|
import '../components/field-sync.js';
|
|
17
22
|
import '../wizard/types.js';
|
|
18
23
|
import '../tour/tour.js';
|
|
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
|
|
|
24
29
|
import '../components/series-chart-labels.js';
|
|
25
30
|
import '../components/pie-chart-labels.js';
|
|
26
31
|
import '../components/sparkline.js';
|
|
27
|
-
import '../components/stat-tile.js';
|
|
28
32
|
import '../components/signature-pad.js';
|
|
29
33
|
import '../components/password-strength.js';
|
|
30
34
|
import '../components/swatch-picker.js';
|
|
@@ -36,9 +40,6 @@ import '../hooks/use-search-param-state.js';
|
|
|
36
40
|
import '../components/measured-grid.js';
|
|
37
41
|
import '../components/user-avatar.js';
|
|
38
42
|
import '../lib/person-name.js';
|
|
39
|
-
import '../components/status-dot.js';
|
|
40
|
-
import '../components/progress-bar.js';
|
|
41
|
-
import '../components/signed-amount.js';
|
|
42
43
|
import '../shell/topbar-action-menu.js';
|
|
43
44
|
import '../components/menu-item.js';
|
|
44
45
|
import '../components/floating-panel.js';
|
|
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
|
|
|
54
55
|
import '../components/authed-image.js';
|
|
55
56
|
import '../components/image-grid.js';
|
|
56
57
|
import '../components/lightbox.js';
|
|
57
|
-
import '../components/write-lock.js';
|
|
58
58
|
import '../auth/sign-in-form.js';
|
|
59
59
|
import '../landing/access.js';
|
|
60
60
|
import '../landing/landing-labels.js';
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { M as MobileSwipeActions } from '../data-table-labels-
|
|
2
|
-
import { aB as FeedbackStatusChange, aC as FeedbackStatusLabels, aF as FeedbackStatusRow } from '../delete-account-setting-
|
|
1
|
+
import { M as MobileSwipeActions } from '../data-table-labels-BJ5JE_X2.js';
|
|
2
|
+
import { aB as FeedbackStatusChange, aC as FeedbackStatusLabels, aF as FeedbackStatusRow } from '../delete-account-setting-DiPX4Vue.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '../components/data-table-sort.js';
|
|
5
5
|
import '../components/swipeable-row.js';
|
|
6
|
+
import '../components/status-dot.js';
|
|
7
|
+
import '../components/progress-bar.js';
|
|
8
|
+
import '../components/signed-amount.js';
|
|
9
|
+
import '../components/stat-tile.js';
|
|
10
|
+
import '../components/chip.js';
|
|
11
|
+
import 'lucide-react';
|
|
12
|
+
import '../components/field-parts.js';
|
|
6
13
|
import '../components/tooltip.js';
|
|
7
14
|
import '../hooks/use-anchored-rect.js';
|
|
8
15
|
import '../lib/clipping.js';
|
|
16
|
+
import '../components/write-lock.js';
|
|
9
17
|
import '../components/mini-calendar.js';
|
|
10
18
|
import '../components/calendar-heatmap.js';
|
|
11
19
|
import '../components/popover.js';
|
|
12
20
|
import '../hooks/use-breakpoint.js';
|
|
13
|
-
import '../components/chip.js';
|
|
14
|
-
import 'lucide-react';
|
|
15
|
-
import '../components/field-parts.js';
|
|
16
21
|
import '../components/field-sync.js';
|
|
17
22
|
import '../wizard/types.js';
|
|
18
23
|
import '../tour/tour.js';
|
|
@@ -24,7 +29,6 @@ import '../components/page-contents.js';
|
|
|
24
29
|
import '../components/series-chart-labels.js';
|
|
25
30
|
import '../components/pie-chart-labels.js';
|
|
26
31
|
import '../components/sparkline.js';
|
|
27
|
-
import '../components/stat-tile.js';
|
|
28
32
|
import '../components/signature-pad.js';
|
|
29
33
|
import '../components/password-strength.js';
|
|
30
34
|
import '../components/swatch-picker.js';
|
|
@@ -36,9 +40,6 @@ import '../hooks/use-search-param-state.js';
|
|
|
36
40
|
import '../components/measured-grid.js';
|
|
37
41
|
import '../components/user-avatar.js';
|
|
38
42
|
import '../lib/person-name.js';
|
|
39
|
-
import '../components/status-dot.js';
|
|
40
|
-
import '../components/progress-bar.js';
|
|
41
|
-
import '../components/signed-amount.js';
|
|
42
43
|
import '../shell/topbar-action-menu.js';
|
|
43
44
|
import '../components/menu-item.js';
|
|
44
45
|
import '../components/floating-panel.js';
|
|
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
|
|
|
54
55
|
import '../components/authed-image.js';
|
|
55
56
|
import '../components/image-grid.js';
|
|
56
57
|
import '../components/lightbox.js';
|
|
57
|
-
import '../components/write-lock.js';
|
|
58
58
|
import '../auth/sign-in-form.js';
|
|
59
59
|
import '../landing/access.js';
|
|
60
60
|
import '../landing/landing-labels.js';
|
|
@@ -27,24 +27,21 @@ function panel(id, target, row, { change, labels }) {
|
|
|
27
27
|
onCommit,
|
|
28
28
|
label: labels[target],
|
|
29
29
|
icon: /* @__PURE__ */ jsx(ChevronsRight, { className: "size-4", "aria-hidden": true }),
|
|
30
|
-
|
|
31
|
-
armedClassName: "bg-[var(--info)] text-[var(--info-contrast)]"
|
|
30
|
+
tone: "info"
|
|
32
31
|
};
|
|
33
32
|
case "done":
|
|
34
33
|
return {
|
|
35
34
|
onCommit,
|
|
36
35
|
label: labels.DONE,
|
|
37
36
|
icon: /* @__PURE__ */ jsx(Check, { className: "size-4", "aria-hidden": true }),
|
|
38
|
-
|
|
39
|
-
armedClassName: "bg-[var(--success)] text-[var(--success-contrast)]"
|
|
37
|
+
tone: "success"
|
|
40
38
|
};
|
|
41
39
|
case "wont_do":
|
|
42
40
|
return {
|
|
43
41
|
onCommit,
|
|
44
42
|
label: labels.WONT_DO,
|
|
45
43
|
icon: /* @__PURE__ */ jsx(Ban, { className: "size-4", "aria-hidden": true }),
|
|
46
|
-
|
|
47
|
-
armedClassName: "bg-[var(--danger-hover)] text-[var(--danger-contrast)]"
|
|
44
|
+
tone: "danger"
|
|
48
45
|
};
|
|
49
46
|
}
|
|
50
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-swipe.tsx"],"sourcesContent":["import { Ban, Check, ChevronsRight } from \"lucide-react\";\nimport type { SwipeAction } from \"../components/swipeable-row\";\nimport type { MobileSwipeActions } from \"../components/data-table\";\nimport { nextFeedbackStatus, type FeedbackStatus } from \"./feedback-inbox\";\nimport type { FeedbackStatusLabels } from \"./feedback-labels\";\nimport type { FeedbackStatusChange, FeedbackStatusRow } from \"./feedback-status-undo\";\n\n/**\n * Swipes on the admin inbox's phone cards — §4.5 of the feedback contract, keksdose's\n * (`feedback-page.tsx:990`, feedback #144 rework item 4: *\"it is a queue, and triaging it\n * one tap-into-the-row at a time is the slow way\"*), for every app.\n *\n * - `advance` — one step along the chain (`nextFeedbackStatus`: OPEN → READY → IN_PROGRESS →\n * IN_EVALUATION → DONE), labelled with the status it goes to;\n * - `done` — straight to DONE, for a row that plainly needs no stop on the way;\n * - `wont_do` — dismiss as WONT_DO.\n *\n * The same transitions the status column offers, so nothing is reachable by gesture that\n * is not reachable by tap. keksdose lets the user rebind them (Settings → Interaction,\n * feedback #404) and keeps that storage; kastlan and Kurvenschmiede take the default.\n */\nexport const FEEDBACK_SWIPE_ACTIONS = [\"advance\", \"done\", \"wont_do\"] as const;\n\n/** One of {@link FEEDBACK_SWIPE_ACTIONS}. */\nexport type FeedbackSwipeAction = (typeof FEEDBACK_SWIPE_ACTIONS)[number];\n\n/**\n * Which swipe does what — the same shape as 0.26's `TranslationReviewSwipeBinding`: per\n * LOGICAL side an ordered ladder, index 0 at the first threshold, index 1 at the longer\n * drag. `end` is a drag toward the reading end (right in a left-to-right page), `start`\n * the other way. keksdose stores PHYSICAL `right` / `left` ladders and hands\n * `{ end: right, start: left }`, as its translations page does\n * (`translations-page.tsx:280`).\n *\n * The ids are typed loosely on purpose: a stored binding carries keksdose's `\"none\"`\n * fillers and may carry an id from another version or another surface — they are\n * dropped (see {@link feedbackSwipePlan}), so it is passed in without a cast.\n */\nexport interface FeedbackSwipeBinding {\n /** A drag toward the reading START, nearest threshold first. */\n start?: readonly (FeedbackSwipeAction | (string & {}))[];\n /** A drag toward the reading END, nearest threshold first. */\n end?: readonly (FeedbackSwipeAction | (string & {}))[];\n}\n\n/** keksdose's shipped binding (`swipe-prefs-store.ts:99`): toward the end advance, then\n * done on the longer drag; toward the start won't do. A settings page's reset goes here. */\nexport const DEFAULT_FEEDBACK_SWIPE: FeedbackSwipeBinding = Object.freeze({\n start: Object.freeze([\"wont_do\"] as const),\n end: Object.freeze([\"advance\", \"done\"] as const),\n});\n\nexport interface FeedbackSwipeOptions {\n /** What a swipe commits through: the undoable change from `useFeedbackStatusUndo`, the\n * same one the status cell and the detail's pills use — so a swipe gets the same 8 s\n * \"Set to …\" toast with Undo as a tap (§4.5, §7.10, Marcel 2026-10-04; keksdose's\n * `changeStatusUndoably` on its swipes too). A swipe is the easiest status change to\n * make by accident. The plan calls it and nothing else, so the page owns the PATCH. */\n change: FeedbackStatusChange;\n /** The status names the panels show — `useFeedbackStatusLabels()`. The `done` panel\n * reads DONE's, `wont_do` WONT_DO's, `advance` the next status's. */\n labels: FeedbackStatusLabels;\n}\n\n/** Where an action takes a row, or null when it would do nothing from there. */\nfunction targetOf(id: string, status: FeedbackStatus): FeedbackStatus | null {\n switch (id) {\n case \"advance\":\n // Null at the chain's end (DONE) and off it (NEEDS_LIVE_TEST, POSTPONED, WONT_DO):\n // \"advance\" has no meaning there, and a threshold that commits nothing is a trap.\n return nextFeedbackStatus(status);\n case \"done\":\n return status === \"DONE\" ? null : \"DONE\";\n case \"wont_do\":\n return status === \"WONT_DO\" ? null : \"WONT_DO\";\n default:\n // \"none\", or an id this version does not know: an empty slot.\n return null;\n }\n}\n\n/** One action's panel
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-swipe.tsx"],"sourcesContent":["import { Ban, Check, ChevronsRight } from \"lucide-react\";\nimport type { SwipeAction } from \"../components/swipeable-row\";\nimport type { MobileSwipeActions } from \"../components/data-table\";\nimport { nextFeedbackStatus, type FeedbackStatus } from \"./feedback-inbox\";\nimport type { FeedbackStatusLabels } from \"./feedback-labels\";\nimport type { FeedbackStatusChange, FeedbackStatusRow } from \"./feedback-status-undo\";\n\n/**\n * Swipes on the admin inbox's phone cards — §4.5 of the feedback contract, keksdose's\n * (`feedback-page.tsx:990`, feedback #144 rework item 4: *\"it is a queue, and triaging it\n * one tap-into-the-row at a time is the slow way\"*), for every app.\n *\n * - `advance` — one step along the chain (`nextFeedbackStatus`: OPEN → READY → IN_PROGRESS →\n * IN_EVALUATION → DONE), labelled with the status it goes to;\n * - `done` — straight to DONE, for a row that plainly needs no stop on the way;\n * - `wont_do` — dismiss as WONT_DO.\n *\n * The same transitions the status column offers, so nothing is reachable by gesture that\n * is not reachable by tap. keksdose lets the user rebind them (Settings → Interaction,\n * feedback #404) and keeps that storage; kastlan and Kurvenschmiede take the default.\n */\nexport const FEEDBACK_SWIPE_ACTIONS = [\"advance\", \"done\", \"wont_do\"] as const;\n\n/** One of {@link FEEDBACK_SWIPE_ACTIONS}. */\nexport type FeedbackSwipeAction = (typeof FEEDBACK_SWIPE_ACTIONS)[number];\n\n/**\n * Which swipe does what — the same shape as 0.26's `TranslationReviewSwipeBinding`: per\n * LOGICAL side an ordered ladder, index 0 at the first threshold, index 1 at the longer\n * drag. `end` is a drag toward the reading end (right in a left-to-right page), `start`\n * the other way. keksdose stores PHYSICAL `right` / `left` ladders and hands\n * `{ end: right, start: left }`, as its translations page does\n * (`translations-page.tsx:280`).\n *\n * The ids are typed loosely on purpose: a stored binding carries keksdose's `\"none\"`\n * fillers and may carry an id from another version or another surface — they are\n * dropped (see {@link feedbackSwipePlan}), so it is passed in without a cast.\n */\nexport interface FeedbackSwipeBinding {\n /** A drag toward the reading START, nearest threshold first. */\n start?: readonly (FeedbackSwipeAction | (string & {}))[];\n /** A drag toward the reading END, nearest threshold first. */\n end?: readonly (FeedbackSwipeAction | (string & {}))[];\n}\n\n/** keksdose's shipped binding (`swipe-prefs-store.ts:99`): toward the end advance, then\n * done on the longer drag; toward the start won't do. A settings page's reset goes here. */\nexport const DEFAULT_FEEDBACK_SWIPE: FeedbackSwipeBinding = Object.freeze({\n start: Object.freeze([\"wont_do\"] as const),\n end: Object.freeze([\"advance\", \"done\"] as const),\n});\n\nexport interface FeedbackSwipeOptions {\n /** What a swipe commits through: the undoable change from `useFeedbackStatusUndo`, the\n * same one the status cell and the detail's pills use — so a swipe gets the same 8 s\n * \"Set to …\" toast with Undo as a tap (§4.5, §7.10, Marcel 2026-10-04; keksdose's\n * `changeStatusUndoably` on its swipes too). A swipe is the easiest status change to\n * make by accident. The plan calls it and nothing else, so the page owns the PATCH. */\n change: FeedbackStatusChange;\n /** The status names the panels show — `useFeedbackStatusLabels()`. The `done` panel\n * reads DONE's, `wont_do` WONT_DO's, `advance` the next status's. */\n labels: FeedbackStatusLabels;\n}\n\n/** Where an action takes a row, or null when it would do nothing from there. */\nfunction targetOf(id: string, status: FeedbackStatus): FeedbackStatus | null {\n switch (id) {\n case \"advance\":\n // Null at the chain's end (DONE) and off it (NEEDS_LIVE_TEST, POSTPONED, WONT_DO):\n // \"advance\" has no meaning there, and a threshold that commits nothing is a trap.\n return nextFeedbackStatus(status);\n case \"done\":\n return status === \"DONE\" ? null : \"DONE\";\n case \"wont_do\":\n return status === \"WONT_DO\" ? null : \"WONT_DO\";\n default:\n // \"none\", or an id this version does not know: an empty slot.\n return null;\n }\n}\n\n/** One action's panel, by tone (0.33): SwipeableRow paints the tone's soft wash idle,\n * and its fill under that fill's own foreground once armed — the same pairs in light\n * and dark, where the fills turn pastel. */\nfunction panel(\n id: FeedbackSwipeAction,\n target: FeedbackStatus,\n row: FeedbackStatusRow,\n { change, labels }: FeedbackSwipeOptions,\n): SwipeAction {\n const onCommit = () => change(row, target);\n switch (id) {\n case \"advance\":\n return {\n onCommit,\n label: labels[target],\n icon: <ChevronsRight className=\"size-4\" aria-hidden />,\n tone: \"info\",\n };\n case \"done\":\n return {\n onCommit,\n label: labels.DONE,\n icon: <Check className=\"size-4\" aria-hidden />,\n tone: \"success\",\n };\n case \"wont_do\":\n return {\n onCommit,\n label: labels.WONT_DO,\n icon: <Ban className=\"size-4\" aria-hidden />,\n tone: \"danger\",\n };\n }\n}\n\n/**\n * The binding resolved against one row — what DataTable's `mobileSwipeActions` returns\n * for it, or `null` when the row offers no swipe at all:\n *\n * ```tsx\n * const changeStatus = useFeedbackStatusUndo(update.mutate);\n * const statusLabels = useFeedbackStatusLabels();\n * <DataTable\n * mobileSwipeActions={isAdmin && !mine\n * ? (row) => feedbackSwipePlan(DEFAULT_FEEDBACK_SWIPE, row, { change: changeStatus, labels: statusLabels })\n * : undefined}\n * />\n * ```\n *\n * Modelled on 0.26's `translationReviewSwipePlan` (keksdose's `resolveSwipePlan`): an\n * action the row cannot take DROPS OUT and the ladder closes up behind it, so the second\n * stage becomes the first instead of the side going dead behind an unreachable stage:\n *\n * - `advance` on DONE and off the chain (NEEDS_LIVE_TEST, POSTPONED, WONT_DO);\n * - `done` on DONE, `wont_do` on WONT_DO;\n * - `\"none\"` and any id this version does not know;\n * - a later stage that lands where an earlier one on the same side already does — the id\n * named twice, or `done` behind `advance` on IN_EVALUATION (both go to DONE): a longer\n * drag to the same effect would arm a threshold that adds nothing.\n *\n * Only what the ROW decides. Whether the page may write at all — the admin inbox, not\n * `/my-feedback`; no write lock — is the caller's, by passing no `mobileSwipeActions`.\n */\nexport function feedbackSwipePlan(\n binding: FeedbackSwipeBinding,\n row: FeedbackStatusRow,\n options: FeedbackSwipeOptions,\n): MobileSwipeActions | null {\n const side = (ids: FeedbackSwipeBinding[\"start\"]): SwipeAction[] => {\n const reached = new Set<FeedbackStatus>();\n const out: SwipeAction[] = [];\n for (const id of ids ?? []) {\n const target = targetOf(id, row.status);\n if (target === null || reached.has(target)) continue;\n reached.add(target);\n out.push(panel(id as FeedbackSwipeAction, target, row, options));\n }\n return out;\n };\n const start = side(binding.start);\n const end = side(binding.end);\n if (start.length === 0 && end.length === 0) return null;\n return { start, end };\n}\n"],"mappings":";AAgGc;AAhGd,SAAS,KAAK,OAAO,qBAAqB;AAG1C,SAAS,0BAA+C;AAkBjD,MAAM,yBAAyB,CAAC,WAAW,QAAQ,SAAS;AA0B5D,MAAM,yBAA+C,OAAO,OAAO;AAAA,EACxE,OAAO,OAAO,OAAO,CAAC,SAAS,CAAU;AAAA,EACzC,KAAK,OAAO,OAAO,CAAC,WAAW,MAAM,CAAU;AACjD,CAAC;AAeD,SAAS,SAAS,IAAY,QAA+C;AAC3E,UAAQ,IAAI;AAAA,IACV,KAAK;AAGH,aAAO,mBAAmB,MAAM;AAAA,IAClC,KAAK;AACH,aAAO,WAAW,SAAS,OAAO;AAAA,IACpC,KAAK;AACH,aAAO,WAAW,YAAY,OAAO;AAAA,IACvC;AAEE,aAAO;AAAA,EACX;AACF;AAKA,SAAS,MACP,IACA,QACA,KACA,EAAE,QAAQ,OAAO,GACJ;AACb,QAAM,WAAW,MAAM,OAAO,KAAK,MAAM;AACzC,UAAQ,IAAI;AAAA,IACV,KAAK;AACH,aAAO;AAAA,QACL;AAAA,QACA,OAAO,OAAO,MAAM;AAAA,QACpB,MAAM,oBAAC,iBAAc,WAAU,UAAS,eAAW,MAAC;AAAA,QACpD,MAAM;AAAA,MACR;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL;AAAA,QACA,OAAO,OAAO;AAAA,QACd,MAAM,oBAAC,SAAM,WAAU,UAAS,eAAW,MAAC;AAAA,QAC5C,MAAM;AAAA,MACR;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL;AAAA,QACA,OAAO,OAAO;AAAA,QACd,MAAM,oBAAC,OAAI,WAAU,UAAS,eAAW,MAAC;AAAA,QAC1C,MAAM;AAAA,MACR;AAAA,EACJ;AACF;AA8BO,SAAS,kBACd,SACA,KACA,SAC2B;AAC3B,QAAM,OAAO,CAAC,QAAsD;AAClE,UAAM,UAAU,oBAAI,IAAoB;AACxC,UAAM,MAAqB,CAAC;AAC5B,eAAW,MAAM,OAAO,CAAC,GAAG;AAC1B,YAAM,SAAS,SAAS,IAAI,IAAI,MAAM;AACtC,UAAI,WAAW,QAAQ,QAAQ,IAAI,MAAM,EAAG;AAC5C,cAAQ,IAAI,MAAM;AAClB,UAAI,KAAK,MAAM,IAA2B,QAAQ,KAAK,OAAO,CAAC;AAAA,IACjE;AACA,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,MAAI,MAAM,WAAW,KAAK,IAAI,WAAW,EAAG,QAAO;AACnD,SAAO,EAAE,OAAO,IAAI;AACtB;","names":[]}
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import '../components/floating-panel.js';
|
|
3
|
-
import '../data-table-labels-
|
|
4
|
-
export { l as DEFAULT_FEEDBACK_PAGE_LABELS, O as FeedbackAwaitingToggle, P as FeedbackAwaitingToggleProps, U as FeedbackColumnsOptions, af as FeedbackEmptyState, ag as FeedbackEmptyStateProps, al as FeedbackMobileCard, am as FeedbackMobileCardProps, aq as FeedbackPageLabels, at as FeedbackRenderDate, aI as FeedbackSubject, aJ as FeedbackSubjectProps, aN as FeedbackTableRow, aX as UseFeedbackColumnsOptions, b1 as feedbackColumns, b3 as feedbackEnvironment, b4 as feedbackMobileGroupBy, b9 as feedbackSubmitter, ba as feedbackSubmitterEmail, bb as isFeedbackAwaiting, bl as useFeedbackColumns, bn as useFeedbackPageLabels, bo as useFeedbackRenderDate } from '../delete-account-setting-
|
|
3
|
+
import '../data-table-labels-BJ5JE_X2.js';
|
|
4
|
+
export { l as DEFAULT_FEEDBACK_PAGE_LABELS, O as FeedbackAwaitingToggle, P as FeedbackAwaitingToggleProps, U as FeedbackColumnsOptions, af as FeedbackEmptyState, ag as FeedbackEmptyStateProps, al as FeedbackMobileCard, am as FeedbackMobileCardProps, aq as FeedbackPageLabels, at as FeedbackRenderDate, aI as FeedbackSubject, aJ as FeedbackSubjectProps, aN as FeedbackTableRow, aX as UseFeedbackColumnsOptions, b1 as feedbackColumns, b3 as feedbackEnvironment, b4 as feedbackMobileGroupBy, b9 as feedbackSubmitter, ba as feedbackSubmitterEmail, bb as isFeedbackAwaiting, bl as useFeedbackColumns, bn as useFeedbackPageLabels, bo as useFeedbackRenderDate } from '../delete-account-setting-DiPX4Vue.js';
|
|
5
|
+
import '../components/write-lock.js';
|
|
5
6
|
import '../components/tooltip.js';
|
|
6
7
|
import '../hooks/use-anchored-rect.js';
|
|
7
8
|
import '../lib/clipping.js';
|
|
8
9
|
import '../components/data-table-sort.js';
|
|
9
10
|
import '../components/swipeable-row.js';
|
|
11
|
+
import '../components/status-dot.js';
|
|
12
|
+
import '../components/progress-bar.js';
|
|
13
|
+
import '../components/signed-amount.js';
|
|
14
|
+
import '../components/stat-tile.js';
|
|
15
|
+
import '../components/chip.js';
|
|
16
|
+
import 'lucide-react';
|
|
17
|
+
import '../components/field-parts.js';
|
|
10
18
|
import '../components/mini-calendar.js';
|
|
11
19
|
import '../components/calendar-heatmap.js';
|
|
12
20
|
import '../components/popover.js';
|
|
13
21
|
import '../hooks/use-breakpoint.js';
|
|
14
|
-
import '../components/chip.js';
|
|
15
|
-
import 'lucide-react';
|
|
16
|
-
import '../components/field-parts.js';
|
|
17
22
|
import '../components/field-sync.js';
|
|
18
23
|
import '../wizard/types.js';
|
|
19
24
|
import '../tour/tour.js';
|
|
@@ -25,7 +30,6 @@ import '../components/page-contents.js';
|
|
|
25
30
|
import '../components/series-chart-labels.js';
|
|
26
31
|
import '../components/pie-chart-labels.js';
|
|
27
32
|
import '../components/sparkline.js';
|
|
28
|
-
import '../components/stat-tile.js';
|
|
29
33
|
import '../components/signature-pad.js';
|
|
30
34
|
import '../components/password-strength.js';
|
|
31
35
|
import '../components/swatch-picker.js';
|
|
@@ -37,9 +41,6 @@ import '../hooks/use-search-param-state.js';
|
|
|
37
41
|
import '../components/measured-grid.js';
|
|
38
42
|
import '../components/user-avatar.js';
|
|
39
43
|
import '../lib/person-name.js';
|
|
40
|
-
import '../components/status-dot.js';
|
|
41
|
-
import '../components/progress-bar.js';
|
|
42
|
-
import '../components/signed-amount.js';
|
|
43
44
|
import '../shell/topbar-action-menu.js';
|
|
44
45
|
import '../components/menu-item.js';
|
|
45
46
|
import '../hooks/use-authed-src.js';
|
|
@@ -54,7 +55,6 @@ import '../components/error-boundary.js';
|
|
|
54
55
|
import '../components/authed-image.js';
|
|
55
56
|
import '../components/image-grid.js';
|
|
56
57
|
import '../components/lightbox.js';
|
|
57
|
-
import '../components/write-lock.js';
|
|
58
58
|
import '../auth/sign-in-form.js';
|
|
59
59
|
import '../landing/access.js';
|
|
60
60
|
import '../landing/landing-labels.js';
|