@eifi1/ui-kit 0.22.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 +11 -8
- package/dist/components/amount-input.d.ts +28 -8
- package/dist/components/amount-input.js +3 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +21 -0
- package/dist/components/autocomplete.js +128 -103
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +11 -8
- package/dist/components/calculator.d.ts +11 -8
- 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 +80 -2
- package/dist/components/combobox.js +448 -311
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +11 -8
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +8 -5
- package/dist/components/country-select.d.ts +50 -7
- package/dist/components/country-select.js +65 -9
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +11 -8
- package/dist/components/danger-confirm.js +13 -3
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/date-picker.d.ts +41 -0
- package/dist/components/date-picker.js +269 -8
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +35 -1
- package/dist/components/entity-combobox.js +148 -112
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +11 -8
- package/dist/components/field-parts.d.ts +116 -6
- package/dist/components/field-parts.js +84 -0
- package/dist/components/field-parts.js.map +1 -1
- 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 +11 -8
- package/dist/components/file-dropzone.d.ts +11 -8
- package/dist/components/form-actions.d.ts +10 -7
- package/dist/components/form-actions.js +8 -2
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +11 -8
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +12 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/language-select.d.ts +11 -8
- package/dist/components/money-field.d.ts +14 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.js +2 -1
- 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/number-field.d.ts +11 -8
- package/dist/components/number-input.d.ts +11 -8
- package/dist/components/numpad-sheet.d.ts +11 -8
- package/dist/components/phone-input.d.ts +11 -8
- 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 +11 -8
- package/dist/components/settings-fields.d.ts +11 -8
- package/dist/components/share-card.d.ts +10 -7
- package/dist/components/swatch-picker.d.ts +17 -1
- package/dist/components/swatch-picker.js +10 -4
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.d.ts +10 -7
- package/dist/components/tile-radio.d.ts +20 -0
- package/dist/components/tile-radio.js +25 -8
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +11 -8
- package/dist/components/toggle-group.js +2 -2
- 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 +8 -5
- package/dist/components/ui.js +5 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +156 -56
- 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 -287
- package/dist/feedback/feedback-thread.js +8 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-BxeQVzwq.d.ts} +405 -22
- package/dist/{feedback-attachment-NhUm7Zga.d.ts → feedback-attachment-fGAzZPf0.d.ts} +112 -10
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +11 -8
- package/dist/i18n/defaults.d.ts +11 -8
- package/dist/i18n/defaults.js +3 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +11 -8
- package/dist/i18n/german.js +36 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +11 -8
- package/dist/i18n/locales/de-CH.d.ts +11 -8
- package/dist/i18n/locales/en.d.ts +11 -8
- package/dist/i18n/locales/en.js +7 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +11 -8
- package/dist/i18n/locales/es.js +37 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +11 -8
- package/dist/i18n/locales/fr.js +36 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +11 -8
- package/dist/i18n/locales/hu.js +38 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +11 -8
- package/dist/i18n/locales/it.js +37 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +11 -8
- package/dist/i18n/locales/zh.js +34 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +11 -8
- package/dist/i18n/review.js +20 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +16 -0
- 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/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 +89 -11
- package/dist/rhf/fields.js +124 -0
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +11 -8
- package/dist/rhf.d.ts +12 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -7
- package/dist/shell/top-bar-brand.d.ts +11 -8
- 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 +10 -7
- package/dist/table-text.d.ts +1 -1
- package/dist/wizard/stepper-nav.d.ts +41 -10
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +11 -8
- package/package.json +1 -1
- package/src/components/amount-input.tsx +20 -1
- package/src/components/autocomplete.tsx +172 -111
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +614 -332
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +135 -22
- package/src/components/danger-confirm.tsx +91 -12
- package/src/components/date-picker.tsx +423 -10
- package/src/components/entity-combobox.tsx +217 -126
- package/src/components/field-parts.tsx +224 -5
- package/src/components/field-strip.tsx +149 -0
- package/src/components/form-actions.tsx +32 -4
- package/src/components/icon-picker.tsx +23 -4
- package/src/components/money-field.tsx +3 -0
- package/src/components/month-picker.tsx +2 -1
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/swatch-picker.tsx +29 -5
- package/src/components/tile-radio.tsx +74 -13
- package/src/components/toggle-group.tsx +2 -2
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +7 -33
- package/src/feedback/feedback-attachment.tsx +308 -61
- package/src/feedback/feedback-dialog.tsx +7 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +47 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +42 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/en.ts +27 -5
- package/src/i18n/locales/es.ts +41 -0
- package/src/i18n/locales/fr.ts +42 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +41 -0
- package/src/i18n/locales/zh.ts +32 -0
- package/src/i18n/review.ts +19 -0
- package/src/index.ts +18 -0
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +254 -0
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/wizard/stepper-nav.tsx +35 -1
- package/dist/components/field-anatomy.d.ts +0 -95
- package/dist/components/field-anatomy.js +0 -84
- package/dist/components/field-anatomy.js.map +0 -1
- package/src/components/field-anatomy.tsx +0 -190
|
@@ -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":";AA6RI,SASE,KATF;AA7RJ,SAAS,QAAQ,gBAAgB;AAEjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,UAAU;AACnB,SAAS,QAAQ,gBAAgB;AACjC,SAAS,eAAe;AACxB,SAAS,+BAA+B;AAuDjC,MAAM,uBAGT;AAAA,EACF,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA,EACA,aAAa;AAAA,IACX,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAIA,eAAe;AAAA,IACb,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB;AAAA,IACf,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA;AAAA,EAEA,WAAW;AAAA,IACT,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAAA,EACA,SAAS;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AACF;AAKO,MAAM,wBAAwB,OAAO,KAAK,oBAAoB;AAYrE,MAAM,YAA8B,CAAC,mBAAmB,aAAa,SAAS;AAE9E,MAAM,gBAAkC,CAAC,QAAQ,eAAe,iBAAiB,MAAM;AAIhF,SAAS,mBAAmB,SAAgD;AACjF,QAAM,KAAK,cAAc,QAAQ,OAAO;AACxC,SAAO,OAAO,MAAM,KAAK,cAAc,SAAS,IAAI,cAAc,KAAK,CAAC,IAAI;AAC9E;AAgBO,SAAS,wBAAwB,SAA2C;AACjF,QAAM,MAAM,oBAAI,IAAoB,CAAC,OAAO,CAAC;AAC7C,QAAM,KAAK,cAAc,QAAQ,OAAO;AACxC,MAAI,OAAO,IAAI;AACb,QAAI,KAAK,cAAc,SAAS,EAAG,KAAI,IAAI,cAAc,KAAK,CAAC,CAAC;AAChE,QAAI,KAAK,EAAG,KAAI,IAAI,cAAc,KAAK,CAAC,CAAC;AACzC,eAAW,OAAO,UAAW,KAAI,IAAI,GAAG;AAAA,EAC1C,WAAW,YAAY,WAAW;AAChC,QAAI,IAAI,aAAa;AACrB,QAAI,IAAI,MAAM;AACd,QAAI,IAAI,SAAS;AAAA,EACnB;AAGA,SAAO,sBAAsB,OAAO,CAAC,WAAW,IAAI,IAAI,MAAM,CAAC;AACjE;AAeO,SAAS,2BAA2B,UAA4C;AACrF,SAAO,CAAC,GAAG,qBAAqB;AAClC;AAaO,MAAM,yBAGT;AAAA,EACF,OAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AACF;AAKO,MAAM,0BAA0B,OAAO,KAAK,sBAAsB;AAKlE,SAAS,qBAAqB,UAAoC;AACvE,QAAM,KAAK,wBAAwB,QAAQ,QAAQ;AACnD,SAAO,OAAO,KAAK,wBAAwB,SAAS;AACtD;AAWO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAKG;AAGD,QAAM,QAA8D,uBAAuB,QAAQ;AACnG,QAAM,OAAO,SAAS,uBAAuB;AAC7C,QAAM,OAAO,KAAK;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,UAAU,8BAA8B;AAAA,QACxC,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,QAAK,WAAW,UAAU,WAAW,YAAY,eAAW,MAAC;AAAA,QAC7D;AAAA;AAAA;AAAA,EACH;AAEJ;AAIO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,qBAAqB,MAAM,KAAK,qBAAqB;AAClE,QAAM,OAAO,KAAK;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK;AAAA,QACL,KAAK;AAAA,QACL;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,QAAK,WAAU,UAAS,eAAW,MAAC;AAAA,QACpC;AAAA;AAAA;AAAA,EACH;AAEJ;AAiBO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,YAAY,SAAS,8BAA8B,0BAA0B,SAAS;AAAA,QAEpG,SAAS,CAAC,UAAU,MAAM,gBAAgB;AAAA,QAEzC,mBAAS,IAAI,CAAC,UAAU;AACvB,gBAAM,OAAO,qBAAqB,KAAK;AACvC,gBAAM,OAAO,KAAK;AAClB,gBAAM,SAAS,WAAW;AAC1B,gBAAM,OAAO,MAAM,KAAK;AACxB,gBAAM,SACJ;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,UAAU,CAAC,WAAW;AAAA,cACtB,SAAS,MAAM,OAAO,KAAK;AAAA,cAC3B,cAAY;AAAA,cACZ,gBAAc;AAAA,cACd,WAAW;AAAA,gBACT;AAAA,gBACA,YAAY,SACR;AAAA,kBACE;AAAA,kBACA,SACI,GAAG,KAAK,UAAU,KAAK,UAAU,IACjC;AAAA,gBACN,IACA;AAAA,kBACE;AAAA,kBACA,SACI,GAAG,KAAK,UAAU,KAAK,YAAY,oBAAoB,IACvD;AAAA,gBACN;AAAA;AAAA;AAAA;AAAA;AAAA,iBAKH,CAAC,WAAW,WAAW;AAAA,gBACxB,CAAC,WAAW;AAAA,cACd;AAAA,cAEA;AAAA,oCAAC,QAAK,WAAW,YAAY,SAAS,WAAW,YAAY,eAAW,MAAC;AAAA,gBACxE,YAAY,UAAU;AAAA;AAAA;AAAA,YA9BlB;AAAA,UA+BP;AAeF,iBAAO,YAAY,SACjB,oBAAC,WAAoB,OAAO,MAAM,MAAK,OAAM,QAAM,MAChD,oBADW,KAEd,IAEA;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH;AAAA;AAEJ;AA8BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAsBG;AACD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,OAAO;AAC1C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAKlD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,QAAQ;AACnD,MAAI,aAAa,WAAW;AAC1B,iBAAa,QAAQ;AACrB,aAAS,OAAO;AAChB,YAAQ,IAAI;AAAA,EACd;AAKA,QAAM,OAAO,OAAuB,IAAI;AACxC,QAAM,SAAS,MAAM,OAAO,OAAO,IAAI;AACvC;AAAA;AAAA;AAAA;AAAA,IAIE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAU;AAAA,QACV,WAAW,CAAC,UAAU;AACpB,eAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,WAAW,CAAC,SAAS;AACzE,kBAAM,eAAe;AACrB,mBAAO;AAAA,UACT;AAAA,QACF;AAAA,QAEC;AAAA,yBAAe,oBAAC,OAAE,WAAU,oCAAoC,uBAAY;AAAA,UAC7E,oBAAC,YAAS,MAAY,OAAO,OAAO,UAAU,CAAC,UAAU,SAAS,MAAM,OAAO,KAAK,GAAG;AAAA,UACtF,cACC;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU;AAAA,cACV,QAAQ,WAAW;AAAA,cACnB,QAAQ,WAAW;AAAA,cACnB,UAAU,WAAW;AAAA,cACrB,SAAS,WAAW;AAAA,cACpB,qBAAqB,WAAW;AAAA,cAMhC,WAAW;AAAA;AAAA,UACb;AAAA,UAEF,qBAAC,SAAI,WAAU,0BACb;AAAA,gCAAC,UAAO,SAAQ,SAAQ,SAAS,UAC9B,uBACH;AAAA,YACA,oBAAC,UAAO,SAAQ,SAAQ,UAAU,SAAS,SAAS,QACjD,qBACH;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA;AAEJ;AAuBO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACE,qBAAC,SACC;AAAA,yBAAC,SAAI,WAAU,gDACb;AAAA,0BAAC,SAAI,WAAU,wEACZ,iBACH;AAAA,MACC;AAAA,OACH;AAAA,IACC;AAAA,KACH;AAEJ;AAIO,SAAS,eAAe,EAAE,SAAS,GAA4B;AACpE,SAAO,oBAAC,SAAI,WAAU,qBAAqB,UAAS;AACtD;AAOO,SAAS,cAAc,EAAE,UAAU,MAAM,GAA6C;AAC3F,MAAI,CAAC,SAAU,QAAO,oBAAC,OAAE,WAAU,kCAAkC,mBAAS,UAAI;AAClF,SACE,oBAAC,OAAE,WAAU,oDAAoD,UAAS;AAE9E;","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":[]}
|
|
@@ -1,288 +1,69 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import 'react';
|
|
2
|
+
export { z as ChatComposer, D as ChatComposerHandle, E as ChatComposerLabels, F as ChatComposerProps, a3 as DEFAULT_FEEDBACK_COMPOSER_LABELS, a4 as DEFAULT_FEEDBACK_THREAD_LABELS, ax as FeedbackComposer, ay as FeedbackComposerAttachment, az as FeedbackComposerHandle, aA as FeedbackComposerLabels, aB as FeedbackComposerProps, aC as FeedbackThread, aD as FeedbackThreadAttachment, aE as FeedbackThreadEvent, aF as FeedbackThreadItem, aG as FeedbackThreadLabels, aH as FeedbackThreadMessage, aI as FeedbackThreadProps } from '../feedback-BxeQVzwq.js';
|
|
3
|
+
import '../feedback-attachment-fGAzZPf0.js';
|
|
4
|
+
import './feedback-inbox.js';
|
|
5
|
+
import '../components/tooltip.js';
|
|
6
|
+
import '../hooks/use-anchored-rect.js';
|
|
7
|
+
import '../lib/clipping.js';
|
|
8
|
+
import '../data-table-labels-B7OdnM0S.js';
|
|
9
|
+
import '../components/data-table-sort.js';
|
|
10
|
+
import '../components/swipeable-row.js';
|
|
11
|
+
import '../components/mini-calendar.js';
|
|
12
|
+
import '../components/calendar-heatmap.js';
|
|
13
|
+
import '../components/popover.js';
|
|
14
|
+
import '../components/chip.js';
|
|
5
15
|
import 'lucide-react';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** The shortcut line, given the platform's modifier ("Ctrl" or "⌘"). */
|
|
61
|
-
sendHint: (modifier: string) => string;
|
|
62
|
-
/** The shortcut line under `sendOn="enter"` (keksdose G5): Enter sends, so say how a
|
|
63
|
-
* new line is made. */
|
|
64
|
-
sendHintEnter: string;
|
|
65
|
-
}
|
|
66
|
-
declare const DEFAULT_FEEDBACK_COMPOSER_LABELS: FeedbackComposerLabels;
|
|
67
|
-
interface FeedbackThreadAttachment {
|
|
68
|
-
id?: string | number;
|
|
69
|
-
name: string;
|
|
70
|
-
/** Where to open it. Left out, the attachment is listed by name only. */
|
|
71
|
-
url?: string;
|
|
72
|
-
/** MIME type; an `image/*` with a `url` is shown as a thumbnail. */
|
|
73
|
-
type?: string;
|
|
74
|
-
/** Bytes, shown beside a non-image. */
|
|
75
|
-
size?: number;
|
|
76
|
-
}
|
|
77
|
-
interface FeedbackThreadMessage {
|
|
78
|
-
/** Leave out, or `"message"`. `"event"` is a {@link FeedbackThreadEvent}. */
|
|
79
|
-
kind?: "message";
|
|
80
|
-
id: string | number;
|
|
81
|
-
/** Who wrote it. Left out on an `own` message, it reads "You". */
|
|
82
|
-
author?: ReactNode;
|
|
83
|
-
/** A `Date`, an ISO string or epoch ms. */
|
|
84
|
-
createdAt: Date | string | number;
|
|
85
|
-
body: string;
|
|
86
|
-
attachments?: readonly FeedbackThreadAttachment[];
|
|
87
|
-
/** Written by whoever is reading: sits at the end side, in the brand tint. */
|
|
88
|
-
own?: boolean;
|
|
89
|
-
/** Written by the team rather than a user: gets the "Staff" badge. */
|
|
90
|
-
staff?: boolean;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* A line in the thread that nobody wrote — "Closed automatically", "Marked as
|
|
94
|
-
* resolved", "Reopened" — drawn centred and muted with its time, no author, no side,
|
|
95
|
-
* no bubble.
|
|
96
|
-
*
|
|
97
|
-
* keksdose G4: its support chat interleaves these with the messages, and as a message
|
|
98
|
-
* they came out as a bubble from nobody at the start side, which reads as the member
|
|
99
|
-
* having said "resolved". Word it for the READER, as with `own`: the app knows whether
|
|
100
|
-
* "you reopened it" or "Anna reopened it" is true for the person looking.
|
|
101
|
-
*
|
|
102
|
-
* The text is the app's, not somebody's words about their data, so it is NOT tagged
|
|
103
|
-
* `data-private`; one that names a person can wrap the name in its own
|
|
104
|
-
* `<span data-private>` (`body` is a node for that reason).
|
|
105
|
-
*/
|
|
106
|
-
interface FeedbackThreadEvent {
|
|
107
|
-
kind: "event";
|
|
108
|
-
id: string | number;
|
|
109
|
-
/** A `Date`, an ISO string or epoch ms. */
|
|
110
|
-
createdAt: Date | string | number;
|
|
111
|
-
body: ReactNode;
|
|
112
|
-
}
|
|
113
|
-
/** One row of a {@link FeedbackThread}: a message, or an event line between them. */
|
|
114
|
-
type FeedbackThreadItem = FeedbackThreadMessage | FeedbackThreadEvent;
|
|
115
|
-
interface FeedbackThreadProps {
|
|
116
|
-
/** Oldest first. Event lines ({@link FeedbackThreadEvent}) sit among the messages. */
|
|
117
|
-
messages: readonly FeedbackThreadItem[];
|
|
118
|
-
loading?: boolean;
|
|
119
|
-
/** Replaces the default "No comments yet". */
|
|
120
|
-
empty?: ReactNode;
|
|
121
|
-
/**
|
|
122
|
-
* Draw one attachment yourself — for files behind auth, which a plain `<img src>`
|
|
123
|
-
* cannot fetch (kastlan's `AuthedImage`), or a lightbox. The default is a thumbnail
|
|
124
|
-
* link for an image with a `url`, a file link otherwise.
|
|
125
|
-
*/
|
|
126
|
-
renderAttachment?: (attachment: FeedbackThreadAttachment, message: FeedbackThreadMessage) => ReactNode;
|
|
127
|
-
/** Tag bodies, authors and attachments `data-private`. Default true. */
|
|
128
|
-
redact?: boolean;
|
|
129
|
-
/**
|
|
130
|
-
* How a row's time reads. `"relative"` (default): "5 minutes ago", a date past six
|
|
131
|
-
* days — right for a report's comments, read days apart. `"clock"`: the time of day
|
|
132
|
-
* in the locale ("14:05", "2:05 PM") — the chat look, keksdose G4; pair it with
|
|
133
|
-
* `daySeparators`, which say which day that clock time is on. Either way the full
|
|
134
|
-
* date and time is in the Tooltip.
|
|
135
|
-
*/
|
|
136
|
-
timeFormat?: "relative" | "clock";
|
|
137
|
-
/**
|
|
138
|
-
* A centred day line before the first row and wherever the next row is on another
|
|
139
|
-
* calendar day (the device's zone): "Today", "Yesterday", else the date — keksdose
|
|
140
|
-
* G4. "Today" / "Yesterday" are `Intl.RelativeTimeFormat`'s own words in the kit
|
|
141
|
-
* locale, so there is no label to translate. Default false.
|
|
142
|
-
*/
|
|
143
|
-
daySeparators?: boolean;
|
|
144
|
-
/** "Now" for the relative times and the day lines — pin it in tests and stories.
|
|
145
|
-
* Default: the clock. */
|
|
146
|
-
now?: Date | number;
|
|
147
|
-
/** Overrides the provider's locale for the times. */
|
|
148
|
-
locale?: string;
|
|
149
|
-
labels?: Partial<FeedbackThreadLabels>;
|
|
150
|
-
className?: string;
|
|
151
|
-
}
|
|
152
|
-
declare function FeedbackThread({ messages, loading, empty, renderAttachment, redact, now, locale: localeProp, timeFormat, daySeparators, labels: labelsProp, className, }: FeedbackThreadProps): react.JSX.Element;
|
|
153
|
-
/** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the
|
|
154
|
-
* labels optional, since the field reads `feedbackAttachment` from the provider. */
|
|
155
|
-
type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, "labels"> & {
|
|
156
|
-
labels?: Partial<FeedbackAttachmentLabels>;
|
|
157
|
-
};
|
|
158
|
-
interface FeedbackComposerProps {
|
|
159
|
-
/**
|
|
160
|
-
* Send the comment. Return the promise: the box clears when it RESOLVES, and only of
|
|
161
|
-
* what was sent — text typed while it was in flight stays. A rejection keeps
|
|
162
|
-
* everything for a retry (surface the error yourself; the composer stays quiet).
|
|
163
|
-
*
|
|
164
|
-
* `attachment` is the built-in field's one picture. Files the host uploaded ahead
|
|
165
|
-
* (`attachmentSlot`) are the host's state, read by the host here — and `body` may be
|
|
166
|
-
* `""` when `canSend` let an attachments-only message go.
|
|
167
|
-
*/
|
|
168
|
-
onSend: (body: string, attachment: File | null) => void | Promise<unknown>;
|
|
169
|
-
/** The send is in flight: a spinner on the button and no second send. */
|
|
170
|
-
pending?: boolean;
|
|
171
|
-
/** Offer one picture with the comment (file, paste or `onCaptureScreenshot`).
|
|
172
|
-
* `true` for the defaults. Left out, the composer is text only. */
|
|
173
|
-
attachment?: boolean | FeedbackComposerAttachment;
|
|
174
|
-
/**
|
|
175
|
-
* The host's own attach control, in the row with Send where the built-in field sits
|
|
176
|
-
* (after it, if both are on) — keksdose G5: its support chat uploads up to N files
|
|
177
|
-
* AHEAD of the send and sends their refs, which one `File` in `onSend` cannot carry.
|
|
178
|
-
* The slot is the host's picker plus the chips of what is uploaded; the upload, the
|
|
179
|
-
* refs and clearing them once `onSend` resolves stay with the host. Pair it with
|
|
180
|
-
* `canSend`.
|
|
181
|
-
*/
|
|
182
|
-
attachmentSlot?: ReactNode;
|
|
183
|
-
/**
|
|
184
|
-
* Whether Send is enabled, over the composer's own rule (some text in the box) —
|
|
185
|
-
* for state the composer cannot see, i.e. `attachmentSlot`'s (keksdose G5): `true`
|
|
186
|
-
* with files ready makes an empty draft sendable (`onSend("", null)`), `false` while
|
|
187
|
-
* an upload is still running holds even a written reply back. Left out, the text
|
|
188
|
-
* decides. `pending` blocks a send either way.
|
|
189
|
-
*/
|
|
190
|
-
canSend?: boolean;
|
|
191
|
-
/**
|
|
192
|
-
* Which key sends. `"mod-enter"` (default): Ctrl/⌘+Enter, the report dialog's and
|
|
193
|
-
* note editor's shortcut, with plain Enter a new line — feedback is written in
|
|
194
|
-
* paragraphs. `"enter"`: the chat way (keksdose G5) — Enter sends, Shift+Enter is a
|
|
195
|
-
* new line, Ctrl/⌘+Enter still sends; an Enter that confirms an IME composition
|
|
196
|
-
* (Japanese, Chinese input) is the IME's, never a send.
|
|
197
|
-
*/
|
|
198
|
-
sendOn?: "mod-enter" | "enter";
|
|
199
|
-
/** Rendered INSTEAD of the composer — the thread is closed, or read-only for this
|
|
200
|
-
* reader. */
|
|
201
|
-
disabledReason?: ReactNode;
|
|
202
|
-
rows?: number;
|
|
203
|
-
/**
|
|
204
|
-
* The box's hint, over the `placeholder` label — keksdose F9: an admin answering a
|
|
205
|
-
* support ticket is asked something else ("Reply to Anna…") than a member adding to
|
|
206
|
-
* their own report, and one provider-wide label cannot say both.
|
|
207
|
-
*/
|
|
208
|
-
placeholder?: string;
|
|
209
|
-
/**
|
|
210
|
-
* The draft, controlled — for a host that keeps it itself (saves it per ticket,
|
|
211
|
-
* prefills a template). Left out, the composer keeps its own. Either way a send
|
|
212
|
-
* that resolves clears only what was sent: controlled, that is an
|
|
213
|
-
* `onValueChange("")` the host receives like any keystroke, and only if the draft
|
|
214
|
-
* is still the one that went out.
|
|
215
|
-
*/
|
|
216
|
-
value?: string;
|
|
217
|
-
/** Every change to the draft — typing, {@link FeedbackComposerHandle.insertText}, the
|
|
218
|
-
* clear after a send. Required to change a controlled `value`. */
|
|
219
|
-
onValueChange?: (value: string) => void;
|
|
220
|
-
/** {@link FeedbackComposerHandle}: put text into the draft from outside (canned-reply
|
|
221
|
-
* chips) or focus the box. */
|
|
222
|
-
ref?: Ref<FeedbackComposerHandle>;
|
|
223
|
-
labels?: Partial<FeedbackComposerLabels>;
|
|
224
|
-
className?: string;
|
|
225
|
-
/**
|
|
226
|
-
* `"sm"` for a narrow host — keksdose's assistant launcher, a small floating panel
|
|
227
|
-
* (0.22): a small Send button, and the send hint ("Enter to send, Shift + Enter for a
|
|
228
|
-
* new line") hidden from view but kept as the box's description, so a keyboard or
|
|
229
|
-
* screen-reader user still learns the shortcut. Default `"md"`.
|
|
230
|
-
*/
|
|
231
|
-
size?: "md" | "sm";
|
|
232
|
-
/**
|
|
233
|
-
* On the composer's root — the box, or the `disabledReason` line in its place —
|
|
234
|
-
* with any `data-*` attribute beside it (keksdose K17). keksdose's assistant tour
|
|
235
|
-
* points at the chat box with `[data-tour="assistant-ask"]`, and a composer that
|
|
236
|
-
* dropped every attribute it was not told about left the anchor nowhere: the
|
|
237
|
-
* assistant kept its hand-built box rather than lose the tour step.
|
|
238
|
-
*/
|
|
239
|
-
id?: string;
|
|
240
|
-
/** `data-*` attributes for the root, as `id` — a tour anchor, a test id. */
|
|
241
|
-
[dataAttribute: `data-${string}`]: string | number | boolean | undefined;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* What a {@link FeedbackComposer} `ref` holds.
|
|
245
|
-
*
|
|
246
|
-
* keksdose F9: the support admin answers the same five questions all day and wants
|
|
247
|
-
* chips above the box ("Thanks, fixed in the next release") that drop their text in
|
|
248
|
-
* where the caret is. A controlled `value` alone cannot do that well — the host sees
|
|
249
|
-
* the string, not the caret, so it could only append — so the composer, which owns
|
|
250
|
-
* the textarea, does the splice.
|
|
251
|
-
*/
|
|
252
|
-
interface FeedbackComposerHandle {
|
|
253
|
-
/**
|
|
254
|
-
* Insert `text` at the caret, replacing any selection (the caret the box last had:
|
|
255
|
-
* a click on a chip blurs it, and the selection survives the blur), then focus the
|
|
256
|
-
* box with the caret after the inserted text so the reply can go on. Nothing is
|
|
257
|
-
* added around it — put a space or a newline in `text` if the reply needs one. A
|
|
258
|
-
* no-op while `disabledReason` replaces the box.
|
|
259
|
-
*/
|
|
260
|
-
insertText: (text: string) => void;
|
|
261
|
-
focus: () => void;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* The answer box under a {@link FeedbackThread}: a textarea, an optional picture,
|
|
265
|
-
* and Send — Ctrl/⌘+Enter sends, the shortcut the report dialog and the note editor
|
|
266
|
-
* use, so the habit carries across the feature. Plain Enter is a new line: feedback is
|
|
267
|
-
* written in paragraphs, unlike a chat — which `sendOn="enter"` is for (keksdose G5).
|
|
268
|
-
*/
|
|
269
|
-
declare function FeedbackComposer({ onSend, pending, attachment, attachmentSlot, canSend: canSendProp, sendOn, disabledReason, rows, placeholder, value, onValueChange, ref, labels: labelsProp, className, id, size, ...rest }: FeedbackComposerProps): react.JSX.Element;
|
|
270
|
-
/**
|
|
271
|
-
* {@link FeedbackComposer} under a name that is not about feedback (keksdose K17).
|
|
272
|
-
*
|
|
273
|
-
* The component was never feedback-specific: a textarea, an optional picture or a
|
|
274
|
-
* host's upload slot, Send, `sendOn="enter"` for the chat way. keksdose's assistant
|
|
275
|
-
* and support chat are chats, not reports, and an import of `FeedbackComposer` there
|
|
276
|
-
* reads as a mistake to the next person. The same function, not a wrapper, so the
|
|
277
|
-
* two names cannot drift.
|
|
278
|
-
*/
|
|
279
|
-
declare const ChatComposer: typeof FeedbackComposer;
|
|
280
|
-
/** {@link FeedbackComposerProps}, for {@link ChatComposer}. */
|
|
281
|
-
type ChatComposerProps = FeedbackComposerProps;
|
|
282
|
-
/** {@link FeedbackComposerHandle}, for {@link ChatComposer}. */
|
|
283
|
-
type ChatComposerHandle = FeedbackComposerHandle;
|
|
284
|
-
/** {@link FeedbackComposerLabels}, for {@link ChatComposer} — still the `feedbackComposer`
|
|
285
|
-
* namespace of the provider. */
|
|
286
|
-
type ChatComposerLabels = FeedbackComposerLabels;
|
|
287
|
-
|
|
288
|
-
export { ChatComposer, type ChatComposerHandle, type ChatComposerLabels, type ChatComposerProps, DEFAULT_FEEDBACK_COMPOSER_LABELS, DEFAULT_FEEDBACK_THREAD_LABELS, FeedbackComposer, type FeedbackComposerAttachment, type FeedbackComposerHandle, type FeedbackComposerLabels, type FeedbackComposerProps, FeedbackThread, type FeedbackThreadAttachment, type FeedbackThreadEvent, type FeedbackThreadItem, type FeedbackThreadLabels, type FeedbackThreadMessage, type FeedbackThreadProps };
|
|
16
|
+
import '../components/field-sync.js';
|
|
17
|
+
import '../wizard/types.js';
|
|
18
|
+
import '../tour/tour.js';
|
|
19
|
+
import '../search/command-palette.js';
|
|
20
|
+
import '../search/global-search.js';
|
|
21
|
+
import '../search/search-index.js';
|
|
22
|
+
import '../components/month-picker.js';
|
|
23
|
+
import '../components/page-contents.js';
|
|
24
|
+
import '../components/series-chart-labels.js';
|
|
25
|
+
import '../components/pie-chart-labels.js';
|
|
26
|
+
import '../components/sparkline.js';
|
|
27
|
+
import '../components/stat-tile.js';
|
|
28
|
+
import '../components/signature-pad.js';
|
|
29
|
+
import '../components/password-strength.js';
|
|
30
|
+
import '../components/swatch-picker.js';
|
|
31
|
+
import '../components/tile-radio.js';
|
|
32
|
+
import '../components/icon-picker.js';
|
|
33
|
+
import '../components/dialog-frame.js';
|
|
34
|
+
import '../components/modal.js';
|
|
35
|
+
import '../hooks/use-search-param-state.js';
|
|
36
|
+
import '../components/measured-grid.js';
|
|
37
|
+
import '../components/account-settings-labels.js';
|
|
38
|
+
import '../components/floating-panel.js';
|
|
39
|
+
import '../components/bulk-action-bar.js';
|
|
40
|
+
import '../components/list.js';
|
|
41
|
+
import '../components/signed-amount.js';
|
|
42
|
+
import '../components/breadcrumbs.js';
|
|
43
|
+
import '../components/toast.js';
|
|
44
|
+
import '../components/description-list.js';
|
|
45
|
+
import '../components/line-items.js';
|
|
46
|
+
import '../components/progress-bar.js';
|
|
47
|
+
import '../components/error-boundary.js';
|
|
48
|
+
import '../components/authed-image.js';
|
|
49
|
+
import '../hooks/use-authed-src.js';
|
|
50
|
+
import '../components/image-grid.js';
|
|
51
|
+
import '../components/lightbox.js';
|
|
52
|
+
import '../components/write-lock.js';
|
|
53
|
+
import '../components/legal.js';
|
|
54
|
+
import '../components/account-chips.js';
|
|
55
|
+
import '../lib/format.js';
|
|
56
|
+
import '../components/reauth-dialog.js';
|
|
57
|
+
import '../components/server-wake.js';
|
|
58
|
+
import '../lib/server-wake.js';
|
|
59
|
+
import '../components/translation-review-labels.js';
|
|
60
|
+
import '../lib/translation-review.js';
|
|
61
|
+
import '../components/country-select.js';
|
|
62
|
+
import '../components/inline-edit-field.js';
|
|
63
|
+
import '../lib/iban.js';
|
|
64
|
+
import '../lib/phone.js';
|
|
65
|
+
import '../components/sign-chip.js';
|
|
66
|
+
import '../components/column-mapper.js';
|
|
67
|
+
import '../lib/column-mapping.js';
|
|
68
|
+
import '../lib/table-text.js';
|
|
69
|
+
import '../components/field-parts.js';
|
|
@@ -237,6 +237,9 @@ function FeedbackComposer({
|
|
|
237
237
|
sendOn = "mod-enter",
|
|
238
238
|
disabledReason,
|
|
239
239
|
rows = 3,
|
|
240
|
+
maxLength,
|
|
241
|
+
showCount,
|
|
242
|
+
countLabels,
|
|
240
243
|
placeholder,
|
|
241
244
|
value,
|
|
242
245
|
onValueChange,
|
|
@@ -294,7 +297,8 @@ function FeedbackComposer({
|
|
|
294
297
|
if (disabledReason != null && disabledReason !== false) {
|
|
295
298
|
return /* @__PURE__ */ jsx("p", { ...rootAttributes, className: cn("text-xs text-[var(--text-muted)]", className), children: disabledReason });
|
|
296
299
|
}
|
|
297
|
-
const
|
|
300
|
+
const tooLong = maxLength !== void 0 && draft.length > maxLength;
|
|
301
|
+
const canSend = (canSendProp ?? draft.trim().length > 0) && !pending && !tooLong;
|
|
298
302
|
const send = () => {
|
|
299
303
|
if (!canSend) return;
|
|
300
304
|
const sentDraft = draft;
|
|
@@ -314,6 +318,9 @@ function FeedbackComposer({
|
|
|
314
318
|
dir: "auto",
|
|
315
319
|
"aria-label": labels.field,
|
|
316
320
|
"aria-describedby": size === "sm" ? hintId : void 0,
|
|
321
|
+
maxLength,
|
|
322
|
+
...showCount !== void 0 ? { showCount } : null,
|
|
323
|
+
countLabels,
|
|
317
324
|
ref: box,
|
|
318
325
|
placeholder: placeholder ?? labels.placeholder,
|
|
319
326
|
value: draft,
|