@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { useEffect, useLayoutEffect, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type RefObject } from \"react\";\nimport { Camera, FileText, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/ui\";\nimport { useKitFileLabels, useKitLabels } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n/** How many files `<FeedbackAttachmentField multiple>` takes when `max` is not given\n * (0.15.5). Five photos of one problem is already a lot to look through; a report\n * that needs more is a conversation, which the thread is for. */\nexport const DEFAULT_MAX_ATTACHMENTS = 5;\n\n/**\n * The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider\n * labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop\n * (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them\n * one to one. The optional heading, `attachment`, stays a prop: a note editor puts\n * the buttons straight under its textarea and wants none.\n *\n * Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app\n * that relied on its provider got \"Capture screenshot\" under a German form.\n *\n * The three keys added in 0.15.5 for `multiple` mode are optional, so a complete\n * `UiKitLabels` typed before it still compiles; they fall back to English. So is\n * `attachmentScreenshot` (0.16.0).\n */\nexport interface FeedbackAttachmentFieldLabels {\n attachmentAdd: string;\n attachmentCapture: string;\n attachmentPaste: string;\n attachmentRemove: string;\n /** `multiple` mode: the accessible name of the list of chosen files — and, since\n * 0.16.0, the heading `<FeedbackDialog attachments=\"multiple\">` puts over it. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): what the screenshot slot's chip is called — \"Screenshot\"\n * rather than the capture's file name (\"screenshot.webp\"), which says nothing a\n * reporter chose. The file name stays as the chip's second line. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button — \"Remove photo.jpg\". Named per file,\n * because five buttons all called \"Remove attachment\" say nothing about which. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown in place of the add buttons once `max` is reached. */\n attachmentLimit?: (max: number) => string;\n}\n\nexport const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {\n attachmentAdd: \"Attach image\",\n attachmentCapture: \"Capture screenshot\",\n attachmentPaste: \"…or paste a screenshot from the clipboard.\",\n attachmentRemove: \"Remove attachment\",\n attachmentList: \"Attachments\",\n attachmentScreenshot: \"Screenshot\",\n attachmentRemoveFile: (name) => `Remove ${name}`,\n attachmentLimit: (max) =>\n `Up to ${max} ${max === 1 ? \"attachment\" : \"attachments\"} — remove one to add another.`,\n};\n\n/** Why a file was turned away. `\"count\"` only ever comes from `multiple` mode: more\n * files arrived at once than `max` left room for, and the surplus was dropped. */\nexport type FeedbackAttachmentError = \"type\" | \"size\" | \"count\";\n\ninterface FeedbackAttachmentFieldBaseProps {\n /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional\n * since 0.7.0; `attachment` (the heading) is only ever read from here. */\n labels?: Partial<FeedbackAttachmentLabels>;\n accept?: string[];\n maxBytes?: number;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}\n\n/** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */\nexport interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {\n multiple?: false;\n value: File | null;\n onChange: (file: File | null) => void;\n onError?: (kind: \"type\" | \"size\") => void;\n // Multiple-mode props, refused here: without `multiple` they would be silently ignored.\n max?: never;\n screenshot?: never;\n onScreenshotChange?: never;\n}\n\n/**\n * Several files (0.15.5, keksdose dev#578: \"pasted two photos, the second overwrote\n * the first\"). Every way in ADDS — a pick of several files, a paste, a capture — up to\n * `max`, and each file is a removable chip.\n */\nexport interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachmentFieldBaseProps {\n multiple: true;\n value: File[];\n onChange: (files: File[]) => void;\n /** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The\n * add buttons and the paste hint are offered only while there is room. */\n max?: number;\n /** Per file: a rejected one is reported and the others are still added. */\n onError?: (kind: FeedbackAttachmentError) => void;\n /**\n * A separate slot for THE screenshot, outside `value` and `max`. Given\n * `onScreenshotChange`, what `onCaptureScreenshot` returns lands here instead of in\n * `value`, shown as the first chip, and the capture button is offered only while the\n * slot is empty — a second snapshot of the same view is not more evidence. This is\n * how `<FeedbackDialog attachments=\"multiple\">` keeps the screenshot apart from the\n * photos; a form built from the parts can do the same.\n */\n screenshot?: File | null;\n onScreenshotChange?: (file: File | null) => void;\n}\n\nexport type FeedbackAttachmentFieldProps =\n | FeedbackAttachmentFieldSingleProps\n | FeedbackAttachmentFieldMultipleProps;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n * With `multiple`, picking several (see {@link FeedbackAttachmentFieldMultipleProps}).\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField(props: FeedbackAttachmentFieldProps) {\n // Two components rather than one with branches: the modes hold different state\n // (one preview vs. one per chip), and a hook order that depends on a prop is a\n // crash the day a caller flips it.\n return props.multiple ? <MultipleField {...props} /> : <SingleField {...props} />;\n}\n\nfunction SingleField({\n value,\n onChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: FeedbackAttachmentFieldSingleProps) {\n // The object URL is keyed to the file it was made for, so a stale one (from the\n // previous file, or after `value` is cleared) is never shown — no reset needed.\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const preview = value && previewFor?.file === value ? previewFor.url : null;\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) return;\n const url = URL.createObjectURL(value);\n // The URL is an external resource that must be created and revoked in step\n // with the effect (a render-time `createObjectURL` would leak, and StrictMode's\n // remount would revoke a memoised one out from under the <img>).\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file: value, url });\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem);\n return;\n }\n onChange(file);\n };\n\n const onPaste = usePaste(\n (images) => {\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n const file = images[0];\n pick(new File([file], pastedName(file.type), { type: file.type }));\n },\n documentPaste,\n pasteFrom,\n );\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page.\n className={cn(\"relative\", className)}\n onPaste={onPaste}\n >\n <Heading text={labelsProp?.attachment} />\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-[var(--border)] object-cover\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{value.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {fileText.size(value.size)}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => onChange(null)}\n aria-label={text.attachmentRemove}\n className=\"rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" variant=\"secondary\" onClick={() => fileInputRef.current?.click()}>\n <Paperclip className=\"size-4\" /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton capture={onCaptureScreenshot} onFile={pick} label={text.attachmentCapture} />\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {text.attachmentPaste}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\nfunction MultipleField({\n value,\n onChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n screenshot = null,\n onScreenshotChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: FeedbackAttachmentFieldMultipleProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot!;\n\n // What an add or a remove builds on: the `value` prop, plus whatever this field has\n // already handed to `onChange` since the parent last rendered. The field is\n // controlled, so between an `onChange` and the re-render that brings it back, `value`\n // is stale — and two adds in that gap (two pastes, a paste and a pick, delivered by\n // native listeners whose updates React batches) each spread the SAME old list, and\n // the second dropped the first. Re-synced to the prop after every commit, so a\n // parent that refuses a change (or edits the list itself) is what wins.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: File[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const hasSlot = onScreenshotChange !== undefined;\n const room = Math.max(0, max - value.length);\n // The screenshot slot is its own thing: it neither counts against `max` nor is\n // closed by it, so a full set of photos still leaves the capture on offer.\n const canCapture = !!onCaptureScreenshot && (hasSlot ? !screenshot : room > 0);\n\n const add = (incoming: File[], pasted = false) => {\n const current = latest.current;\n const accepted: File[] = [];\n const taken = new Set([...current, ...(screenshot ? [screenshot] : [])].map((f) => f.name));\n for (const raw of incoming) {\n const problem = rejection(raw, accept, maxBytes);\n if (problem) {\n onError?.(problem);\n continue;\n }\n // Two pastes are two files called \"pasted.png\", and a backend that stores by\n // name keeps one of them — the very loss this mode exists to end. A picked\n // file keeps its own name: it is the user's, and they may look for it.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n taken.add(file.name);\n // The same File object twice is one file chosen twice, not two.\n if (!current.includes(file)) accepted.push(file);\n }\n if (accepted.length === 0) return;\n const space = Math.max(0, max - current.length);\n if (accepted.length > space) onError?.(\"count\");\n const kept = accepted.slice(0, space);\n if (kept.length > 0) commit([...current, ...kept]);\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom);\n\n const capture = (file: File) => {\n if (!hasSlot) {\n add([file]);\n return;\n }\n const problem = rejection(file, accept, maxBytes);\n if (problem) onError?.(problem);\n else onScreenshotChange(file);\n };\n\n // Removing a chip removes the button that had focus. Focus goes to the chip that\n // took its place — the next one — and, when it was the last, to the first action\n // button (there is room again, so \"Attach\" is back), else to the chip before it.\n // Applied once the parent has re-rendered with the shorter list: the field is\n // controlled, and focusing before that would land on the chip being removed.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target =\n removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value, screenshot]);\n\n const chips: Array<{ file: File; screenshot: boolean; name: string; remove: () => void }> = [\n ...(hasSlot && screenshot\n ? [{ file: screenshot, screenshot: true, name: screenshotLabel, remove: () => onScreenshotChange(null) }]\n : []),\n ...value.map((file) => ({\n file,\n screenshot: false,\n name: file.name,\n remove: () => commit(latest.current.filter((f) => f !== file)),\n })),\n ];\n\n return (\n <div ref={rootRef} className={cn(\"relative space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {chips.length > 0 && (\n // One chip per row, full width: wrapped chips of name-dependent widths made a\n // ragged block in a 430px dialog, and a column keeps every name and remove\n // button in the same place — the next chip's button is right under the last.\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {chips.map((chip, index) => (\n <li\n key={fileKey(chip.file)}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-1\"\n >\n <ChipPreview file={chip.file} screenshot={chip.screenshot} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{chip.name}</div>\n <div className=\"truncate text-xs text-[var(--text-muted)]\">\n {/* The screenshot's file name moves down here: still there for the\n reporter who wants to know what will be sent, not its title. */}\n {chip.screenshot ? `${chip.file.name} · ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size)}\n </div>\n </div>\n <button\n type=\"button\"\n data-attachment-remove=\"\"\n onClick={() => {\n pendingFocus.current = index;\n chip.remove();\n }}\n aria-label={removeLabel(chip.name)}\n className=\"shrink-0 rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n {(room > 0 || canCapture) && (\n <div className=\"flex flex-wrap gap-2\">\n {room > 0 && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n data-attachment-action=\"\"\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className=\"size-4\" /> {text.attachmentAdd}\n </Button>\n )}\n {canCapture && onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={capture}\n label={text.attachmentCapture}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {room > 0 ? text.attachmentPaste : limitLabel(max)}\n </p>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree: the button above is\n // the way in, and a second, unlabelled \"Choose files\" stop is noise.\n tabIndex={-1}\n aria-hidden\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\nfunction Heading({ text, className }: { text?: string; className?: string }) {\n if (!text) return null;\n return (\n <div className={cn(\"mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\", className)}>\n {text}\n </div>\n );\n}\n\nfunction CaptureButton({\n capture,\n onFile,\n label,\n ...rest\n}: {\n capture: () => Promise<File | null>;\n onFile: (file: File) => void;\n label: string;\n \"data-attachment-action\"?: string;\n}) {\n const [capturing, setCapturing] = useState(false);\n return (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing}\n {...rest}\n onClick={() => {\n if (capturing) return;\n setCapturing(true);\n void capture()\n .then((file) => {\n if (file) onFile(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" /> {capturing ? \"…\" : label}\n </Button>\n );\n}\n\n/** A chip's picture: the image itself, or a file glyph for anything that is not one.\n * Decorative (`alt=\"\"`) — the name stands beside it. Its own component so the object\n * URL lives exactly as long as the chip: removing the chip unmounts it and revokes. */\nfunction ChipPreview({ file, screenshot }: { file: File; screenshot: boolean }) {\n const isImage = file.type.startsWith(\"image/\");\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const url = previewFor?.file === file ? previewFor.url : null;\n useEffect(() => {\n if (!isImage) return;\n const next = URL.createObjectURL(file);\n // As in the single field: an external resource created and revoked with the effect.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file, url: next });\n return () => URL.revokeObjectURL(next);\n }, [file, isImage]);\n\n return (\n <span className=\"relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]\">\n {isImage && url ? (\n <img src={url} alt=\"\" className=\"size-full object-cover\" />\n ) : (\n <FileText aria-hidden className=\"size-6\" />\n )}\n {screenshot && (\n // Marks the slot apart from the photos: it is why the capture button is gone.\n <span className=\"absolute bottom-0.5 end-0.5 rounded-sm bg-[var(--bg-surface)] p-0.5 text-[var(--text-secondary)]\">\n <Camera aria-hidden className=\"size-3\" />\n </span>\n )}\n </span>\n );\n}\n\n/** A stable React key per File object — the name is not one (two pastes, two\n * \"pasted.png\" before renaming; the same photo picked from two folders). */\nconst fileKeys = new WeakMap<File, number>();\nlet nextFileKey = 0;\nfunction fileKey(file: File): number {\n let key = fileKeys.get(file);\n if (key === undefined) {\n key = nextFileKey++;\n fileKeys.set(file, key);\n }\n return key;\n}\n\nfunction rejection(file: File, accept: string[], maxBytes: number): \"type\" | \"size\" | null {\n if (!accept.includes(file.type)) return \"type\";\n if (file.size > maxBytes) return \"size\";\n return null;\n}\n\nfunction renamed(file: File, name: string): File {\n return new File([file], name, { type: file.type });\n}\n\n/** `pasted.png`, then `pasted-2.png`, `pasted-3.png`… — the first name not in `taken`. */\nfunction uniqueName(name: string, taken: Set<string>): string {\n if (!taken.has(name)) return name;\n const dot = name.lastIndexOf(\".\");\n const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, \"\"];\n let n = 2;\n while (taken.has(`${stem}-${n}${ext}`)) n++;\n return `${stem}-${n}${ext}`;\n}\n\n/**\n * The paste listener both modes share. `onImages` gets every image file on the\n * clipboard (the single field takes the first). It is registered once rather than per\n * render, so it reads the handler — which closes over props that change identity on\n * every render — out of a ref rather than out of its own dependency list. Returns the\n * `onPaste` for the field's root, or `undefined` when listening elsewhere.\n */\nfunction usePaste(\n onImages: (images: File[]) => void,\n documentPaste: boolean,\n pasteFrom: RefObject<HTMLElement | null> | undefined,\n) {\n const latest = useRef(onImages);\n useEffect(() => {\n latest.current = onImages;\n });\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImages(clipboard, () => event.preventDefault(), latest.current);\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n }, [documentPaste, pasteFrom]);\n\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n return listensElsewhere\n ? undefined\n : (event: ReactClipboardEvent) => takeImages(event.clipboardData, () => event.preventDefault(), latest.current);\n}\n\nfunction takeImages(clipboard: DataTransfer | null, stop: () => void, onImages: (images: File[]) => void) {\n const images = Array.from(clipboard?.items ?? [])\n .filter((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"))\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null);\n if (images.length === 0) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n onImages(images);\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ClipboardEvent as ReactClipboardEvent,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { Camera, FileText, Image as ImageIcon, Paperclip, X } from \"lucide-react\";\nimport { Button, IconButton, Spinner } from \"../components/ui\";\nimport { DEFAULT_FILE_PICKER_LABELS, formatAccept } from \"../components/file-button\";\nimport { useCommitReason } from \"../components/write-lock\";\nimport { useKitFileLabels, useKitLabels } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n/** How many files `<FeedbackAttachmentField multiple>` takes when `max` is not given\n * (0.15.5). Five photos of one problem is already a lot to look through; a report\n * that needs more is a conversation, which the thread is for. */\nexport const DEFAULT_MAX_ATTACHMENTS = 5;\n\n/**\n * The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider\n * labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop\n * (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them\n * one to one. The optional heading, `attachment`, stays a prop: a note editor puts\n * the buttons straight under its textarea and wants none.\n *\n * Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app\n * that relied on its provider got \"Capture screenshot\" under a German form.\n *\n * The three keys added in 0.15.5 for `multiple` mode are optional, so a complete\n * `UiKitLabels` typed before it still compiles; they fall back to English. So is\n * `attachmentScreenshot` (0.16.0).\n */\nexport interface FeedbackAttachmentFieldLabels {\n attachmentAdd: string;\n attachmentCapture: string;\n attachmentPaste: string;\n attachmentRemove: string;\n /** `multiple` mode: the accessible name of the list of chosen files — and, since\n * 0.16.0, the heading `<FeedbackDialog attachments=\"multiple\">` puts over it. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): what the screenshot slot's chip is called — \"Screenshot\"\n * rather than the capture's file name (\"screenshot.webp\"), which says nothing a\n * reporter chose. The file name stays as the chip's second line. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button — \"Remove photo.jpg\". Named per file,\n * because five buttons all called \"Remove attachment\" say nothing about which. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown in place of the add buttons once `max` is reached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0, keksdose K16): the second line of a chip whose upload is still\n * running, where an uploaded one shows its size — \"Uploading…\". Optional, like the\n * keys above, so a `UiKitLabels` typed before it still compiles. */\n attachmentUploading?: string;\n /** `refs` mode (0.23.0, keksdose G5a): {@link FeedbackAttachmentErrorInfo.message} for\n * an `\"upload\"` refusal — \"“huge.png” could not be uploaded\". The other refusals'\n * sentences are the `filePicker` namespace's, shared with FileButton. Optional, like\n * the keys above. */\n attachmentUploadFailed?: (name: string) => string;\n}\n\nexport const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {\n attachmentAdd: \"Attach image\",\n attachmentCapture: \"Capture screenshot\",\n attachmentPaste: \"…or paste a screenshot from the clipboard.\",\n attachmentRemove: \"Remove attachment\",\n attachmentList: \"Attachments\",\n attachmentScreenshot: \"Screenshot\",\n attachmentRemoveFile: (name) => `Remove ${name}`,\n attachmentLimit: (max) =>\n `Up to ${max} ${max === 1 ? \"attachment\" : \"attachments\"} — remove one to add another.`,\n attachmentUploading: \"Uploading…\",\n attachmentUploadFailed: (name) => `“${name}” could not be uploaded`,\n};\n\n/** Why a file was turned away. `\"count\"` only ever comes from `multiple` (and `refs`)\n * mode: more files arrived at once than `max` left room for, and the surplus was\n * dropped. */\nexport type FeedbackAttachmentError = \"type\" | \"size\" | \"count\";\n\n/** `refs` mode's reasons: {@link FeedbackAttachmentError}, or `\"upload\"` — `onUpload`\n * rejected, and what it rejected with is `onError`'s second argument. A type of its\n * own so a host's exhaustive switch over the File modes' reasons stays exhaustive. */\nexport type FeedbackAttachmentRefsError = FeedbackAttachmentError | \"upload\";\n\n/**\n * What `onError` knows about a refusal beyond its kind (0.23.0, keksdose G5a): the file\n * it was about, the limit that refused it, and a sentence that says both.\n *\n * keksdose's support chat toasted \"File too large\" with no name — the field's `onError`\n * carried a kind and nothing else, so a refusal could not say WHICH of three pasted\n * screenshots went, nor what the ceiling was, and its own `AttachmentPicker` (a\n * FileButton, whose `onReject` hands over `{ file, message }`) said more than the kit's\n * field would have. This is that, for every mode.\n *\n * It arrives as an EXTRA argument, after the ones `onError` already had, so a handler\n * written for 0.22 — `(kind) => …`, or `refs` mode's `(kind, error) => …` — compiles\n * and behaves as before:\n *\n * - single and `multiple`: `onError(kind, info)`;\n * - `refs`: `onError(kind, error, info)` — the second place is `onUpload`'s rejection\n * since 0.22 (`undefined` for the other kinds, as it always was), so the info comes\n * third rather than changing what a 0.22 handler reads there. It also carries that\n * `error`, so one helper `(kind, info) => …` serves all three modes.\n *\n * An object rather than more positional arguments because what a host reaches for\n * differs by kind — the bytes for `\"size\"`, `max` for `\"count\"`, the server's answer for\n * `\"upload\"` — and a field added later must not shift anyone's arguments.\n */\nexport interface FeedbackAttachmentErrorInfo {\n /** The file refused — under the name the field would have given it (a pasted image\n * is already \"pasted.png\"). For `\"count\"`, the first of the surplus; for\n * `\"upload\"`, the file whose upload failed. */\n file: File;\n /** `\"count\"`: every file of the surplus (several can arrive in one pick). Otherwise\n * `[file]`. */\n files: File[];\n /**\n * One translated sentence naming the file and the limit — the ones FileButton's\n * `onReject` hands over, from the same `filePicker` namespace: \"“huge.png” is larger\n * than 10 MB\", \"Only image/png, image/jpeg files\", \"“c.png” was not added: at most 5\n * files\" (several at once: \"3 files were not added\"). `\"upload\"`: the field's own\n * `attachmentUploadFailed`, \"“huge.png” could not be uploaded\" — a host with a better\n * sentence from the server's answer uses `error`. Ready for a toast as it is.\n */\n message: string;\n /** The field's `maxBytes` — the ceiling a `\"size\"` refusal went over, in bytes. */\n maxBytes: number;\n /** The field's `accept` — the types a `\"type\"` refusal was not among. */\n accept: string[];\n /** The field's `max` — the ceiling a `\"count\"` refusal hit. Absent in single mode,\n * which holds one file by definition. */\n max?: number;\n /** `\"upload\"` only: what `onUpload` rejected with — `onError`'s second argument. */\n error?: unknown;\n}\n\n/**\n * One uploaded attachment in `refs` mode: what the host's upload answered with. `key`\n * identifies it (a storage key, an upload id) and is what a remove goes by; `name` is\n * what the chip says. `size` (bytes) and `type` (mime) are optional — given, the chip\n * shows the size and an image icon or a file icon accordingly.\n */\nexport interface FeedbackAttachmentRef {\n key: string;\n name: string;\n size?: number;\n type?: string;\n}\n\ninterface FeedbackAttachmentFieldBaseProps {\n /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional\n * since 0.7.0; `attachment` (the heading) is only ever read from here. */\n labels?: Partial<FeedbackAttachmentLabels> &\n Pick<Partial<FeedbackAttachmentFieldLabels>, \"attachmentUploading\" | \"attachmentUploadFailed\">;\n accept?: string[];\n maxBytes?: number;\n /**\n * Take no files for now (0.23.0, keksdose G5b): the add and capture buttons, the\n * hidden file input and every chip's remove button are natively disabled, and a paste\n * is let through untouched. The chips stay as they are — what is about to be sent\n * stays readable.\n *\n * For the moment a send is in flight. keksdose's support picker is `disabled` while\n * the message goes out, because the message carries the attachments AS THEY WERE at\n * the press: a chip removed mid-send has gone with it all the same, and one added\n * mid-send looks as if it had. The field has no drop target; the paste is its drop.\n *\n * This is a pause, not a lock that explains itself — for that, `refs` mode (the mode\n * that commits) takes `commit` and `disabledReason`.\n */\n disabled?: boolean;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}\n\n/** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */\nexport interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {\n multiple?: false;\n refs?: false;\n value: File | null;\n onChange: (file: File | null) => void;\n /** A file was refused. `info` (0.23.0) names it and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n // Multiple-mode props, refused here: without `multiple` they would be silently ignored.\n max?: never;\n screenshot?: never;\n onScreenshotChange?: never;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Several files (0.15.5, keksdose dev#578: \"pasted two photos, the second overwrote\n * the first\"). Every way in ADDS — a pick of several files, a paste, a capture — up to\n * `max`, and each file is a removable chip.\n */\nexport interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachmentFieldBaseProps {\n multiple: true;\n refs?: false;\n value: File[];\n onChange: (files: File[]) => void;\n /** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The\n * add buttons and the paste hint are offered only while there is room. */\n max?: number;\n /** Per file: a rejected one is reported and the others are still added. `\"count\"` is\n * once per pick, with the whole surplus in `info.files`. `info` (0.23.0): see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * A separate slot for THE screenshot, outside `value` and `max`. Given\n * `onScreenshotChange`, what `onCaptureScreenshot` returns lands here instead of in\n * `value`, shown as the first chip, and the capture button is offered only while the\n * slot is empty — a second snapshot of the same view is not more evidence. This is\n * how `<FeedbackDialog attachments=\"multiple\">` keeps the screenshot apart from the\n * photos; a form built from the parts can do the same.\n */\n screenshot?: File | null;\n onScreenshotChange?: (file: File | null) => void;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Uploaded on pick (0.22.0, keksdose K16). keksdose's support chat uploads each file\n * the moment it is chosen and sends the message with the REFS the uploads answered —\n * a 10 MB screenshot is refused while the reporter is still typing, not when they\n * press Send — so its `AttachmentPicker` could not use this field, whose value is\n * `File`s held until the form is submitted. Here the value is\n * {@link FeedbackAttachmentRef}s: every way in (pick, paste, capture) validates the\n * file as the other modes do, then hands it to `onUpload` and shows a chip with a\n * spinner until it answers; the ref it resolves to joins `value` through `onChange`.\n * A rejection drops the chip and reports `onError(\"upload\", error)`. A chip is removed\n * BY KEY: `onChange` gets the list without it, and `onRemove` the key, for a host that\n * deletes the upload on the server too.\n *\n * Uploads still running count against `max`, so five picks in quick succession cannot\n * overshoot it; they are not in `value` until they land, so a send that reads `value`\n * sends only what has arrived — hold Send back while {@link onUploadingChange} says\n * an upload is running. Several files upload side by side and join in the order they\n * finish. The File modes are unchanged.\n */\nexport interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFieldBaseProps {\n refs: true;\n multiple?: never;\n value: FeedbackAttachmentRef[];\n onChange: (refs: FeedbackAttachmentRef[]) => void;\n /** Upload one file; resolve to its ref, or reject to refuse it. Called once per\n * accepted file, the moment it is chosen. */\n onUpload: (file: File) => Promise<FeedbackAttachmentRef>;\n /** A chip was removed — after `onChange` — with its ref's key. */\n onRemove?: (key: string) => void;\n /** Whether any upload is running: `true` when the first starts, `false` when the\n * last settles — for the host's Send, which should wait. */\n onUploadingChange?: (uploading: boolean) => void;\n /** How many refs `value` may hold, uploads in flight included. Default\n * {@link DEFAULT_MAX_ATTACHMENTS} (5). */\n max?: number;\n /** Per file. `\"upload\"` carries what `onUpload` rejected with as `error` (`undefined`\n * for the other kinds). `info` (0.23.0) comes third so a 0.22 handler's `error` is\n * still the second argument — see {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentRefsError, error: unknown, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * This field COMMITS (0.23.0): each file is uploaded the moment it is chosen, and a\n * remove may delete the upload (`onRemove`) — a save, unlike the File modes, which\n * only stage a file for a form sent later (and so stay open under a lock, like every\n * field). Under a locked {@link WriteLockProvider} it takes the `disabledReason` path\n * with the lock's reason, which wins over one of its own. No provider, or an unlocked\n * one: no effect.\n */\n commit?: boolean;\n /**\n * Why no file can be added or removed — the write lock's sentence, a quota, a thread\n * closed to replies. Unlike `disabled`, it SAYS so, the kit's commit-control way: the\n * buttons (add, capture, every chip's remove) stay focusable, `aria-disabled`, with\n * the reason in the kit Tooltip and their description; no pick, paste or remove gets\n * through; and the reason stands where the paste hint was, for a phone that never\n * hovers. The chips stay readable.\n */\n disabledReason?: ReactNode;\n screenshot?: never;\n onScreenshotChange?: never;\n}\n\nexport type FeedbackAttachmentFieldProps =\n | FeedbackAttachmentFieldSingleProps\n | FeedbackAttachmentFieldMultipleProps\n | FeedbackAttachmentFieldRefsProps;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n * With `multiple`, picking several (see {@link FeedbackAttachmentFieldMultipleProps}).\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField(props: FeedbackAttachmentFieldProps) {\n // One component per mode rather than one with branches: the modes hold different\n // state (one preview vs. one per chip vs. uploads in flight), and a hook order that\n // depends on a prop is a crash the day a caller flips it.\n if (props.refs) return <RefsField {...props} />;\n return props.multiple ? <MultipleField {...props} /> : <SingleField {...props} />;\n}\n\nfunction SingleField({\n value,\n onChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n className,\n}: FeedbackAttachmentFieldSingleProps) {\n // The object URL is keyed to the file it was made for, so a stale one (from the\n // previous file, or after `value` is cleared) is never shown — no reset needed.\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const preview = value && previewFor?.file === value ? previewFor.url : null;\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const errorInfo = useErrorInfo(accept, maxBytes, undefined, text);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) return;\n const url = URL.createObjectURL(value);\n // The URL is an external resource that must be created and revoked in step\n // with the effect (a render-time `createObjectURL` would leak, and StrictMode's\n // remount would revoke a memoised one out from under the <img>).\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file: value, url });\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n return;\n }\n onChange(file);\n };\n\n const onPaste = usePaste(\n (images) => {\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n const file = images[0];\n pick(new File([file], pastedName(file.type), { type: file.type }));\n },\n documentPaste,\n pasteFrom,\n disabled,\n );\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page.\n className={cn(\"relative\", className)}\n onPaste={onPaste}\n >\n <Heading text={labelsProp?.attachment} />\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-[var(--border)] object-cover\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{value.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {fileText.size(value.size)}\n </div>\n </div>\n <RemoveButton label={text.attachmentRemove} onClick={() => onChange(null)} disabled={disabled} />\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className=\"size-4\" /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={pick}\n label={text.attachmentCapture}\n disabled={disabled}\n />\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {text.attachmentPaste}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n disabled={disabled}\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\nfunction MultipleField({\n value,\n onChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n screenshot = null,\n onScreenshotChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n className,\n}: FeedbackAttachmentFieldMultipleProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n\n // What an add or a remove builds on: the `value` prop, plus whatever this field has\n // already handed to `onChange` since the parent last rendered. The field is\n // controlled, so between an `onChange` and the re-render that brings it back, `value`\n // is stale — and two adds in that gap (two pastes, a paste and a pick, delivered by\n // native listeners whose updates React batches) each spread the SAME old list, and\n // the second dropped the first. Re-synced to the prop after every commit, so a\n // parent that refuses a change (or edits the list itself) is what wins.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: File[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const hasSlot = onScreenshotChange !== undefined;\n const room = Math.max(0, max - value.length);\n // The screenshot slot is its own thing: it neither counts against `max` nor is\n // closed by it, so a full set of photos still leaves the capture on offer.\n const canCapture = !!onCaptureScreenshot && (hasSlot ? !screenshot : room > 0);\n\n const add = (incoming: File[], pasted = false) => {\n const current = latest.current;\n const accepted: File[] = [];\n const taken = new Set([...current, ...(screenshot ? [screenshot] : [])].map((f) => f.name));\n for (const raw of incoming) {\n // Two pastes are two files called \"pasted.png\", and a backend that stores by\n // name keeps one of them — the very loss this mode exists to end. A picked\n // file keeps its own name: it is the user's, and they may look for it. Named\n // before the check, so a refusal names it as the single field's does.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n // The same File object twice is one file chosen twice, not two.\n if (!current.includes(file)) accepted.push(file);\n }\n if (accepted.length === 0) return;\n const space = Math.max(0, max - current.length);\n if (accepted.length > space) onError?.(\"count\", errorInfo(\"count\", accepted.slice(space)));\n const kept = accepted.slice(0, space);\n if (kept.length > 0) commit([...current, ...kept]);\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, disabled);\n\n const capture = (file: File) => {\n if (!hasSlot) {\n add([file]);\n return;\n }\n const problem = rejection(file, accept, maxBytes);\n if (problem) onError?.(problem, errorInfo(problem, [file]));\n else onScreenshotChange(file);\n };\n\n // Removing a chip removes the button that had focus. Focus goes to the chip that\n // took its place — the next one — and, when it was the last, to the first action\n // button (there is room again, so \"Attach\" is back), else to the chip before it.\n // Applied once the parent has re-rendered with the shorter list: the field is\n // controlled, and focusing before that would land on the chip being removed.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target =\n removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value, screenshot]);\n\n const chips: Array<{ file: File; screenshot: boolean; name: string; remove: () => void }> = [\n ...(hasSlot && screenshot\n ? [{ file: screenshot, screenshot: true, name: screenshotLabel, remove: () => onScreenshotChange(null) }]\n : []),\n ...value.map((file) => ({\n file,\n screenshot: false,\n name: file.name,\n remove: () => commit(latest.current.filter((f) => f !== file)),\n })),\n ];\n\n return (\n <div ref={rootRef} className={cn(\"relative space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {chips.length > 0 && (\n // One chip per row, full width: wrapped chips of name-dependent widths made a\n // ragged block in a 430px dialog, and a column keeps every name and remove\n // button in the same place — the next chip's button is right under the last.\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {chips.map((chip, index) => (\n <li\n key={fileKey(chip.file)}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-1\"\n >\n <ChipPreview file={chip.file} screenshot={chip.screenshot} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{chip.name}</div>\n <div className=\"truncate text-xs text-[var(--text-muted)]\">\n {/* The screenshot's file name moves down here: still there for the\n reporter who wants to know what will be sent, not its title. */}\n {chip.screenshot ? `${chip.file.name} · ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size)}\n </div>\n </div>\n <RemoveButton\n label={removeLabel(chip.name)}\n disabled={disabled}\n onClick={() => {\n pendingFocus.current = index;\n chip.remove();\n }}\n />\n </li>\n ))}\n </ul>\n )}\n {(room > 0 || canCapture) && (\n <div className=\"flex flex-wrap gap-2\">\n {room > 0 && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n data-attachment-action=\"\"\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className=\"size-4\" /> {text.attachmentAdd}\n </Button>\n )}\n {canCapture && onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={capture}\n label={text.attachmentCapture}\n disabled={disabled}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {room > 0 ? text.attachmentPaste : limitLabel(max)}\n </p>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree: the button above is\n // the way in, and a second, unlabelled \"Choose files\" stop is noise.\n tabIndex={-1}\n aria-hidden\n disabled={disabled}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A file `refs` mode is uploading: its own id (a ref has no key yet), and the file. */\ninterface Upload {\n id: number;\n file: File;\n}\n\nlet nextUploadId = 0;\n\nfunction RefsField({\n value,\n onChange,\n onUpload,\n onRemove,\n onUploadingChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n commit: commits,\n disabledReason: ownDisabledReason,\n className,\n}: FeedbackAttachmentFieldRefsProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n // The lock's reason (under `commit`) over the field's own; with either, the controls\n // take the focusable `aria-disabled` path and say why — Button's and IconButton's\n // `disabledReason`, handed down as is.\n const disabledReason = useCommitReason(commits, ownDisabledReason);\n const locked = hasReason(disabledReason);\n const reason = locked ? disabledReason : undefined;\n const inert = disabled || locked;\n\n // As in `multiple` mode: `value` plus whatever has been handed to `onChange` since\n // the parent last rendered — two uploads landing in one tick must both survive.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: FeedbackAttachmentRef[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const [uploads, setUploads] = useState<Upload[]>([]);\n // The uploads in flight, kept in a ref as well: `add` runs several times before a\n // render when files arrive together, and each must see the others — their count\n // against `max`, their names against a pasted image's.\n const inFlight = useRef<Upload[]>([]);\n // Unmounted (the dialog closed mid-upload): a late answer is not this field's to\n // commit, and setting state on it would be a no-op at best.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const uploadingChange = useRef(onUploadingChange);\n useEffect(() => {\n uploadingChange.current = onUploadingChange;\n });\n\n const room = Math.max(0, max - value.length - uploads.length);\n\n const start = (file: File) => {\n const upload: Upload = { id: nextUploadId++, file };\n inFlight.current = [...inFlight.current, upload];\n if (inFlight.current.length === 1) uploadingChange.current?.(true);\n setUploads(inFlight.current);\n const settle = () => {\n inFlight.current = inFlight.current.filter((u) => u.id !== upload.id);\n if (!mounted.current) return;\n setUploads(inFlight.current);\n if (inFlight.current.length === 0) uploadingChange.current?.(false);\n };\n let answer: Promise<FeedbackAttachmentRef>;\n try {\n answer = Promise.resolve(onUpload(file));\n } catch (error) {\n answer = Promise.reject(error);\n }\n answer.then(\n (ref) => {\n settle();\n if (mounted.current) commit([...latest.current, ref]);\n },\n (error: unknown) => {\n settle();\n if (mounted.current) onError?.(\"upload\", error, errorInfo(\"upload\", [file], error));\n },\n );\n };\n\n const add = (incoming: File[], pasted = false) => {\n let space = Math.max(0, max - latest.current.length - inFlight.current.length);\n const taken = new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);\n const dropped: File[] = [];\n for (const raw of incoming) {\n // Pasted images are all \"image.png\" to the clipboard; as in `multiple` mode,\n // each gets a name of its own before it is checked and uploaded under it.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n // `undefined` in `error`'s place, as in 0.22: only an upload has one.\n onError?.(problem, undefined, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n if (space === 0) {\n dropped.push(file);\n continue;\n }\n space -= 1;\n start(file);\n }\n if (dropped.length > 0) onError?.(\"count\", undefined, errorInfo(\"count\", dropped));\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, inert);\n\n // Focus after a remove, as in `multiple` mode: the next chip, else the first action.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target =\n removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value]);\n\n const remove = (key: string, index: number) => {\n if (inert) return;\n pendingFocus.current = index;\n commit(latest.current.filter((r) => r.key !== key));\n onRemove?.(key);\n };\n\n const hasChips = value.length > 0 || uploads.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {hasChips && (\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {value.map((ref, index) => (\n <li\n key={`ref:${ref.key}`}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-1\"\n >\n <RefGlyph type={ref.type} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{ref.name}</div>\n {ref.size !== undefined && (\n <div className=\"truncate text-xs text-[var(--text-muted)]\">{fileText.size(ref.size)}</div>\n )}\n </div>\n <RemoveButton\n label={removeLabel(ref.name)}\n onClick={() => remove(ref.key, index)}\n disabled={disabled}\n reason={reason}\n />\n </li>\n ))}\n {uploads.map((upload) => (\n <li\n key={`upload:${upload.id}`}\n aria-busy=\"true\"\n data-attachment-uploading=\"\"\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-dashed border-[var(--border)] bg-[var(--bg-surface)] p-1\"\n >\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]\">\n <Spinner label={null} className=\"size-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{upload.file.name}</div>\n <div className=\"truncate text-xs text-[var(--text-muted)]\">{uploadingLabel}</div>\n </div>\n </li>\n ))}\n </ul>\n )}\n {room > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"secondary\"\n data-attachment-action=\"\"\n disabled={disabled}\n disabledReason={reason}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className=\"size-4\" /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={(file) => add([file])}\n label={text.attachmentCapture}\n disabled={disabled}\n disabledReason={reason}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n {locked ? (\n // Where the paste hint was: a phone never hovers, so the Tooltip alone would\n // leave a touch user with buttons that do nothing and no word why.\n <div data-attachment-reason=\"\" className=\"text-xs text-[var(--text-muted)]\">\n {disabledReason}\n </div>\n ) : (\n <p className=\"text-xs text-[var(--text-muted)]\">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n tabIndex={-1}\n aria-hidden\n disabled={inert}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A ref chip's picture. The bytes are on the server, not here, so there is nothing to\n * preview: an image glyph for an image `type`, a file glyph for anything else. */\nfunction RefGlyph({ type }: { type?: string }) {\n return (\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]\">\n {type?.startsWith(\"image/\") ? <ImageIcon aria-hidden className=\"size-6\" /> : <FileText aria-hidden className=\"size-6\" />}\n </span>\n );\n}\n\nfunction Heading({ text, className }: { text?: string; className?: string }) {\n if (!text) return null;\n return (\n <div className={cn(\"mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\", className)}>\n {text}\n </div>\n );\n}\n\nfunction CaptureButton({\n capture,\n onFile,\n label,\n disabled = false,\n disabledReason,\n ...rest\n}: {\n capture: () => Promise<File | null>;\n onFile: (file: File) => void;\n label: string;\n disabled?: boolean;\n disabledReason?: ReactNode;\n \"data-attachment-action\"?: string;\n}) {\n const [capturing, setCapturing] = useState(false);\n return (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing || disabled}\n disabledReason={disabledReason}\n {...rest}\n onClick={() => {\n if (capturing || disabled) return;\n setCapturing(true);\n void capture()\n .then((file) => {\n if (file) onFile(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" /> {capturing ? \"…\" : label}\n </Button>\n );\n}\n\n/**\n * A chip's (or the single file's) remove button: the kit's IconButton at the 28px the\n * hand-built one had, so `disabled` (0.23.0) dims and disables it the kit way and a\n * `reason` takes IconButton's `disabledReason` path — focusable, `aria-disabled`, the\n * reason in the Tooltip — rather than a third copy of either here.\n */\nfunction RemoveButton({\n label,\n onClick,\n disabled,\n reason,\n}: {\n label: string;\n onClick: () => void;\n disabled: boolean;\n reason?: ReactNode;\n}) {\n return (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n data-attachment-remove=\"\"\n aria-label={label}\n disabled={disabled}\n disabledReason={reason}\n onClick={onClick}\n className=\"shrink-0\"\n >\n <X aria-hidden />\n </IconButton>\n );\n}\n\n/** A reason worth showing: `null`, `false` and `\"\"` are a caller's \"no reason\". */\nfunction hasReason(reason: ReactNode): boolean {\n return reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n}\n\n/**\n * The {@link FeedbackAttachmentErrorInfo} builder of one field: its limits, and the\n * sentence for each refusal. The sentences are the `filePicker` namespace's — the ones\n * FileButton and FileDropzone say — so an app that translated those has these too, and\n * a refusal reads the same whichever of the kit's pickers made it. Only the upload\n * failure, which no other picker has, is this field's own (`attachmentUploadFailed`).\n */\nfunction useErrorInfo(\n accept: string[],\n maxBytes: number,\n max: number | undefined,\n text: FeedbackAttachmentFieldLabels,\n) {\n const picker = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS);\n const fileText = useKitFileLabels();\n const uploadFailed = text.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed!;\n return (kind: FeedbackAttachmentRefsError, files: File[], error?: unknown): FeedbackAttachmentErrorInfo => {\n const file = files[0];\n let message: string;\n if (kind === \"type\") {\n // FileButton's rule: name what IS accepted (\"Only image/png, image/jpeg files\"),\n // unless the app translated `rejectedType` but not yet `rejectedTypeOnly` — its\n // own sentence beats an English one.\n const acceptText = formatAccept(accept.join(\",\"));\n const useOnly =\n acceptText !== \"\" &&\n (picker.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||\n picker.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);\n message = useOnly ? picker.rejectedTypeOnly(acceptText, file.name) : picker.rejectedType(file.name);\n } else if (kind === \"size\") {\n message = picker.rejectedSize(file.name, fileText.size(maxBytes));\n } else if (kind === \"count\") {\n message = files.length === 1 ? picker.rejectedCount(file.name, max ?? 1) : picker.rejectedMany(files.length);\n } else {\n message = uploadFailed(file.name);\n }\n return {\n file,\n files,\n message,\n maxBytes,\n accept,\n ...(max !== undefined ? { max } : null),\n ...(kind === \"upload\" ? { error } : null),\n };\n };\n}\n\n/** A chip's picture: the image itself, or a file glyph for anything that is not one.\n * Decorative (`alt=\"\"`) — the name stands beside it. Its own component so the object\n * URL lives exactly as long as the chip: removing the chip unmounts it and revokes. */\nfunction ChipPreview({ file, screenshot }: { file: File; screenshot: boolean }) {\n const isImage = file.type.startsWith(\"image/\");\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const url = previewFor?.file === file ? previewFor.url : null;\n useEffect(() => {\n if (!isImage) return;\n const next = URL.createObjectURL(file);\n // As in the single field: an external resource created and revoked with the effect.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file, url: next });\n return () => URL.revokeObjectURL(next);\n }, [file, isImage]);\n\n return (\n <span className=\"relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-placeholder)]\">\n {isImage && url ? (\n <img src={url} alt=\"\" className=\"size-full object-cover\" />\n ) : (\n <FileText aria-hidden className=\"size-6\" />\n )}\n {screenshot && (\n // Marks the slot apart from the photos: it is why the capture button is gone.\n <span className=\"absolute bottom-0.5 end-0.5 rounded-sm bg-[var(--bg-surface)] p-0.5 text-[var(--text-secondary)]\">\n <Camera aria-hidden className=\"size-3\" />\n </span>\n )}\n </span>\n );\n}\n\n/** A stable React key per File object — the name is not one (two pastes, two\n * \"pasted.png\" before renaming; the same photo picked from two folders). */\nconst fileKeys = new WeakMap<File, number>();\nlet nextFileKey = 0;\nfunction fileKey(file: File): number {\n let key = fileKeys.get(file);\n if (key === undefined) {\n key = nextFileKey++;\n fileKeys.set(file, key);\n }\n return key;\n}\n\nfunction rejection(file: File, accept: string[], maxBytes: number): \"type\" | \"size\" | null {\n if (!accept.includes(file.type)) return \"type\";\n if (file.size > maxBytes) return \"size\";\n return null;\n}\n\nfunction renamed(file: File, name: string): File {\n return new File([file], name, { type: file.type });\n}\n\n/** `pasted.png`, then `pasted-2.png`, `pasted-3.png`… — the first name not in `taken`. */\nfunction uniqueName(name: string, taken: Set<string>): string {\n if (!taken.has(name)) return name;\n const dot = name.lastIndexOf(\".\");\n const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, \"\"];\n let n = 2;\n while (taken.has(`${stem}-${n}${ext}`)) n++;\n return `${stem}-${n}${ext}`;\n}\n\n/**\n * The paste listener both modes share. `onImages` gets every image file on the\n * clipboard (the single field takes the first). It is registered once rather than per\n * render, so it reads the handler — which closes over props that change identity on\n * every render — out of a ref rather than out of its own dependency list. Returns the\n * `onPaste` for the field's root, or `undefined` when listening elsewhere.\n */\nfunction usePaste(\n onImages: (images: File[]) => void,\n documentPaste: boolean,\n pasteFrom: RefObject<HTMLElement | null> | undefined,\n /** `disabled` / locked (0.23.0): the paste goes where it was going, untouched — not\n * swallowed, so a text half still lands in the box it was made in. */\n off: boolean,\n) {\n // `null` while off, so the listeners below leave the event alone entirely.\n const handler = off ? null : onImages;\n const latest = useRef(handler);\n useEffect(() => {\n latest.current = handler;\n });\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n if (!latest.current) return;\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImages(clipboard, () => event.preventDefault(), latest.current);\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n }, [documentPaste, pasteFrom]);\n\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n return listensElsewhere\n ? undefined\n : (event: ReactClipboardEvent) => {\n if (!latest.current) return;\n takeImages(event.clipboardData, () => event.preventDefault(), latest.current);\n };\n}\n\nfunction takeImages(clipboard: DataTransfer | null, stop: () => void, onImages: (images: File[]) => void) {\n const images = Array.from(clipboard?.items ?? [])\n .filter((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"))\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null);\n if (images.length === 0) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n onImages(images);\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import 'react';
|
|
2
|
-
export { b as DEFAULT_FEEDBACK_DIALOG_LABELS,
|
|
2
|
+
export { b as DEFAULT_FEEDBACK_DIALOG_LABELS, l as FeedbackAttachmentLabels, o as FeedbackCategoryOption, p as FeedbackDialog, q as FeedbackDialogLabels, r as FeedbackDialogMultipleProps, s as FeedbackDialogProps, t as FeedbackDialogSingleProps, u as FeedbackDialogTextLabels, v as FeedbackMultipleSubmission, w as FeedbackSubmission } from '../feedback-attachment-fGAzZPf0.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `attachmentList`, the one key for the heading and\n * the chip list it names. Still honoured: set, it wins over the provider's\n * `attachmentList` (but not over an `attachmentList` passed beside it). */\n attachments?: string;\n}\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /** The line beside the buttons naming the shortcut. */\n submitHint: string;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `feedbackAttachment.attachmentList`. It and this\n * key were two names for one heading (\"Attachments\" over the chips that list names\n * \"Attachments\"), and a catalogue that translated one left the other English. Still\n * honoured when set: it wins over `feedbackAttachment.attachmentList`. */\n attachments?: string;\n}\n\nexport const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {\n title: \"Send feedback\",\n category: \"Category\",\n subject: \"Subject\",\n body: \"What happened?\",\n bodyOptional: \"What happened? (optional)\",\n attachment: \"Screenshot\",\n // No `attachments`: the multiple-mode heading is `feedbackAttachment.attachmentList`,\n // and a default here would shadow a provider that translated only that one.\n submitHint: \"Ctrl/⌘ + Enter to send\",\n cancel: \"Cancel\",\n save: \"Send\",\n};\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: Partial<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n onAttachmentError?: (kind: \"type\" | \"size\") => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was dropped. */\n onAttachmentError?: (kind: FeedbackAttachmentError) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. The\n // deprecated `feedbackDialog.attachments` (prop or provider) still wins over the\n // provider's `attachmentList`, as it did before 0.16.0; an `attachmentList` prop wins\n // over both.\n const listHeading =\n labelsProp?.attachmentList ??\n labels.attachments ??\n attachmentText.attachmentList ??\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n />\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">{labels.submitHint}</div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `attachmentList`, the one key for the heading and\n * the chip list it names. Still honoured: set, it wins over the provider's\n * `attachmentList` (but not over an `attachmentList` passed beside it). */\n attachments?: string;\n}\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /** The line beside the buttons naming the shortcut. */\n submitHint: string;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `feedbackAttachment.attachmentList`. It and this\n * key were two names for one heading (\"Attachments\" over the chips that list names\n * \"Attachments\"), and a catalogue that translated one left the other English. Still\n * honoured when set: it wins over `feedbackAttachment.attachmentList`. */\n attachments?: string;\n}\n\nexport const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {\n title: \"Send feedback\",\n category: \"Category\",\n subject: \"Subject\",\n body: \"What happened?\",\n bodyOptional: \"What happened? (optional)\",\n attachment: \"Screenshot\",\n // No `attachments`: the multiple-mode heading is `feedbackAttachment.attachmentList`,\n // and a default here would shadow a provider that translated only that one.\n submitHint: \"Ctrl/⌘ + Enter to send\",\n cancel: \"Cancel\",\n save: \"Send\",\n};\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: Partial<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. The\n // deprecated `feedbackDialog.attachments` (prop or provider) still wins over the\n // provider's `attachmentList`, as it did before 0.16.0; an `attachmentList` prop wins\n // over both.\n const listHeading =\n labelsProp?.attachmentList ??\n labels.attachments ??\n attachmentText.attachmentList ??\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n />\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">{labels.submitHint}</div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { l as FeedbackAttachmentLabels, e as FeedbackAttachmentErrorInfo } from '../feedback-attachment-fGAzZPf0.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The feedback **inbox**, as the parts two apps were each writing separately.
|
|
@@ -215,7 +215,8 @@ interface FeedbackNoteAttachment {
|
|
|
215
215
|
labels: FeedbackAttachmentLabels;
|
|
216
216
|
accept?: string[];
|
|
217
217
|
maxBytes?: number;
|
|
218
|
-
|
|
218
|
+
/** `info` (0.23.0) names the refused file and the limit. */
|
|
219
|
+
onError?: (kind: "type" | "size", info: FeedbackAttachmentErrorInfo) => void;
|
|
219
220
|
onCaptureScreenshot?: () => Promise<File | null>;
|
|
220
221
|
}
|
|
221
222
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport {\n Ban,\n Bug,\n CheckCircle2,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Textarea } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { FeedbackAttachmentField } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: this owns the\n * *vocabulary*, the *policy* about how a report moves, and the *look*; the app\n * owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **What is deliberately NOT here.** The two apps store a report differently and\n * are meant to: one keeps the reporter's context in a JSON column and the\n * screenshot in an object store, the other keeps a `page_path` and the bytes in\n * the row. So there is no `Feedback` type in this file and nothing here takes\n * one. Every component takes the values it draws, and the detail panel is a\n * *shell* the app fills — which is what lets each keep the shape that suits it\n * without either of them inventing a second look for a status pill.\n */\n\n/** The seven states a report can be in.\n *\n * Shared value for value across both apps on purpose: it is what lets one habit,\n * and one agent prompt, work on either repo. Four are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. */\nexport type FeedbackStatus =\n | \"OPEN\"\n | \"IN_PROGRESS\"\n | \"IN_EVALUATION\"\n | \"NEEDS_LIVE_TEST\"\n | \"POSTPONED\"\n | \"DONE\"\n | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal seven long.\n */\nexport const FEEDBACK_STATUS_META: Record<\n FeedbackStatus,\n { icon: LucideIcon; activeBg: string; activeText: string }\n> = {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-primary)]\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-[var(--warning-bg)]\",\n activeText: \"text-[var(--warning)]\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-[var(--info-bg)]\",\n activeText: \"text-[var(--info)]\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand-muted)]\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-secondary)]\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a family that means something else — `--status-synced` is a\n // FIELD's trip to the database rather than an outcome, and tokens.css says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-[var(--success-bg)]\",\n activeText: \"text-[var(--success)]\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-[var(--danger-bg)]\",\n activeText: \"text-[var(--danger)]\",\n },\n};\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of seven icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-[var(--danger-bg)]\",\n badgeText: \"text-[var(--danger)]\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-[var(--warning-bg)]\",\n badgeText: \"text-[var(--warning)]\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* is the caller's, and for the same reason it needs the same\n * guard: pass what OTHER is called if you cannot name the value you were given.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n label: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-[11px]\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n label: ReactNode;\n className?: string;\n}) {\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called, translated by the app. */\n label: (status: FeedbackStatus) => string;\n className?: string;\n}) {\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = label(value);\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={!canEdit || active}\n onClick={() => onPick(value)}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active\n ? cn(meta.activeBg, meta.activeText)\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : \"border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among seven pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n rows = 3,\n attachment,\n resetKey,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && !pending) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {placeholder && <p className=\"text-xs text-[var(--text-muted)]\">{placeholder}</p>}\n <Textarea rows={rows} value={draft} onChange={(event) => setDraft(event.target.value)} />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"brand\" disabled={pending} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same four things {@link FeedbackAttachmentField} takes, so the\n * reply path is held to the app's own limits rather than to the defaults. */\nexport interface FeedbackNoteAttachment {\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n onError?: (kind: \"type\" | \"size\") => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {title}\n </div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children }: { children: ReactNode }) {\n return <div className=\"space-y-3 text-sm\">{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-[var(--text-placeholder)]\">{empty ?? \"—\"}</p>;\n return (\n <p className=\"whitespace-pre-wrap text-[var(--text-secondary)]\">{children}</p>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport {\n Ban,\n Bug,\n CheckCircle2,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Textarea } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: this owns the\n * *vocabulary*, the *policy* about how a report moves, and the *look*; the app\n * owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **What is deliberately NOT here.** The two apps store a report differently and\n * are meant to: one keeps the reporter's context in a JSON column and the\n * screenshot in an object store, the other keeps a `page_path` and the bytes in\n * the row. So there is no `Feedback` type in this file and nothing here takes\n * one. Every component takes the values it draws, and the detail panel is a\n * *shell* the app fills — which is what lets each keep the shape that suits it\n * without either of them inventing a second look for a status pill.\n */\n\n/** The seven states a report can be in.\n *\n * Shared value for value across both apps on purpose: it is what lets one habit,\n * and one agent prompt, work on either repo. Four are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. */\nexport type FeedbackStatus =\n | \"OPEN\"\n | \"IN_PROGRESS\"\n | \"IN_EVALUATION\"\n | \"NEEDS_LIVE_TEST\"\n | \"POSTPONED\"\n | \"DONE\"\n | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal seven long.\n */\nexport const FEEDBACK_STATUS_META: Record<\n FeedbackStatus,\n { icon: LucideIcon; activeBg: string; activeText: string }\n> = {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-primary)]\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-[var(--warning-bg)]\",\n activeText: \"text-[var(--warning)]\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-[var(--info-bg)]\",\n activeText: \"text-[var(--info)]\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand-muted)]\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-secondary)]\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a family that means something else — `--status-synced` is a\n // FIELD's trip to the database rather than an outcome, and tokens.css says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-[var(--success-bg)]\",\n activeText: \"text-[var(--success)]\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-[var(--danger-bg)]\",\n activeText: \"text-[var(--danger)]\",\n },\n};\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of seven icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-[var(--danger-bg)]\",\n badgeText: \"text-[var(--danger)]\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-[var(--warning-bg)]\",\n badgeText: \"text-[var(--warning)]\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* is the caller's, and for the same reason it needs the same\n * guard: pass what OTHER is called if you cannot name the value you were given.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n label: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-[11px]\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n label: ReactNode;\n className?: string;\n}) {\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called, translated by the app. */\n label: (status: FeedbackStatus) => string;\n className?: string;\n}) {\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = label(value);\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={!canEdit || active}\n onClick={() => onPick(value)}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active\n ? cn(meta.activeBg, meta.activeText)\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : \"border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among seven pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n rows = 3,\n attachment,\n resetKey,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && !pending) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {placeholder && <p className=\"text-xs text-[var(--text-muted)]\">{placeholder}</p>}\n <Textarea rows={rows} value={draft} onChange={(event) => setDraft(event.target.value)} />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"brand\" disabled={pending} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same four things {@link FeedbackAttachmentField} takes, so the\n * reply path is held to the app's own limits rather than to the defaults. */\nexport interface FeedbackNoteAttachment {\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n /** `info` (0.23.0) names the refused file and the limit. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {title}\n </div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children }: { children: ReactNode }) {\n return <div className=\"space-y-3 text-sm\">{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-[var(--text-placeholder)]\">{empty ?? \"—\"}</p>;\n return (\n <p className=\"whitespace-pre-wrap text-[var(--text-secondary)]\">{children}</p>\n );\n}\n"],"mappings":"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","names":[]}
|