anton-bakker-feedback 0.3.2 → 0.3.4

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.
@@ -88,6 +88,23 @@ const en = {
88
88
  "feedback.form.contentPlaceholder": "Describe the issue or suggestion in detail...",
89
89
  "feedback.form.typeLabel": "Type",
90
90
  "feedback.form.typePlaceholder": "Select a type",
91
+ // Confirm (review) form
92
+ "feedback.confirm.heading": "Review your feedback",
93
+ "feedback.confirm.titleLabel": "Title",
94
+ "feedback.confirm.descriptionLabel": "Description",
95
+ "feedback.confirm.typeLabel": "Type",
96
+ "feedback.confirm.scopeLabel": "Scope",
97
+ "feedback.confirm.sizeLabel": "Size",
98
+ "feedback.confirm.duplicateWarning": "Similar feedback already exists:",
99
+ "feedback.confirm.scopePlaceholder": "Select scope (optional)",
100
+ "feedback.confirm.scope.isolated": "Isolated",
101
+ "feedback.confirm.scope.crossCutting": "Cross-cutting",
102
+ "feedback.confirm.scope.systemic": "Systemic",
103
+ "feedback.confirm.sizePlaceholder": "Select size (optional)",
104
+ "feedback.confirm.size.trivial": "Trivial",
105
+ "feedback.confirm.size.small": "Small",
106
+ "feedback.confirm.size.medium": "Medium",
107
+ "feedback.confirm.size.large": "Large",
91
108
  // Dialogue states
92
109
  "feedback.dialogue.opening": "Thank you for your feedback. I have a few questions to help us understand better.",
93
110
  "feedback.dialogue.thinking": "Thinking...",
@@ -130,6 +147,23 @@ const nl = {
130
147
  "feedback.form.contentPlaceholder": "Beschrijf het probleem of de suggestie in detail...",
131
148
  "feedback.form.typeLabel": "Type",
132
149
  "feedback.form.typePlaceholder": "Selecteer een type",
150
+ // Confirm (review) form
151
+ "feedback.confirm.heading": "Controleer uw feedback",
152
+ "feedback.confirm.titleLabel": "Titel",
153
+ "feedback.confirm.descriptionLabel": "Omschrijving",
154
+ "feedback.confirm.typeLabel": "Type",
155
+ "feedback.confirm.scopeLabel": "Reikwijdte",
156
+ "feedback.confirm.sizeLabel": "Omvang",
157
+ "feedback.confirm.duplicateWarning": "Er bestaat al vergelijkbare feedback:",
158
+ "feedback.confirm.scopePlaceholder": "Selecteer reikwijdte (optioneel)",
159
+ "feedback.confirm.scope.isolated": "Geïsoleerd",
160
+ "feedback.confirm.scope.crossCutting": "Overkoepelend",
161
+ "feedback.confirm.scope.systemic": "Systemisch",
162
+ "feedback.confirm.sizePlaceholder": "Selecteer omvang (optioneel)",
163
+ "feedback.confirm.size.trivial": "Triviaal",
164
+ "feedback.confirm.size.small": "Klein",
165
+ "feedback.confirm.size.medium": "Middel",
166
+ "feedback.confirm.size.large": "Groot",
133
167
  // Dialogue states
134
168
  "feedback.dialogue.opening": "Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.",
135
169
  "feedback.dialogue.thinking": "Aan het nadenken...",
@@ -207,4 +241,4 @@ export {
207
241
  t,
208
242
  validateConfig as v
209
243
  };
210
- //# sourceMappingURL=i18n-Du-or3T4.js.map
244
+ //# sourceMappingURL=i18n-Cba0xHU-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-Cba0xHU-.js","sources":["../../src/core/config.ts","../../src/core/locales/en.ts","../../src/core/locales/nl.ts","../../src/core/i18n.ts"],"sourcesContent":["/**\n * Configuration schema and validation for the anton-bakker-feedback package.\n */\n\nimport type { FeedbackEnvironment } from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config Interface\n// ─────────────────────────────────────────────────────────────────\n\n/** Full configuration object for the feedback package */\nexport interface FeedbackConfig {\n /** Required — alphanumeric, hyphens, underscores only */\n appName: string;\n version?: string;\n /** Defaults to 'production' if absent */\n environment?: FeedbackEnvironment;\n\n transport: {\n /** POST endpoint — app provides its own route */\n submitUrl: string;\n /** If absent → dialogue disabled, form shown */\n dialogueUrl?: string;\n /** If absent → screenshots disabled */\n screenshotUploadUrl?: string;\n };\n\n dialogue?: {\n /** Default 3; min 1, max 10 */\n maxTurns?: number;\n /** Default 30_000 */\n timeoutMs?: number;\n };\n\n screenshots?: {\n /** Default true if screenshotUploadUrl set */\n enabled?: boolean;\n /** Default 5_242_880 (5 MB) */\n maxSizeBytes?: number;\n };\n\n ui?: {\n /** Default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default 'auto' */\n theme?: 'light' | 'dark' | 'auto';\n };\n\n observability?: {\n /** Default true */\n enabled?: boolean;\n /** Default 'Feedback/Service' */\n namespace?: string;\n /** Default ['x-trace-id', 'x-request-id'] */\n traceIdHeaders?: string[];\n };\n\n i18n?: {\n /** Default 'en' */\n locale?: string;\n customTranslations?: Record<string, Record<string, string>>;\n };\n\n signOff?: {\n /** Default 7; 0 = manual only */\n autoConfirmDays?: number;\n /** Default 3 */\n reminderDays?: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Resolved Config — all fields have values, no undefined\n// ─────────────────────────────────────────────────────────────────\n\n/** Fully-resolved config with all defaults applied — no undefined fields */\nexport interface ResolvedFeedbackConfig {\n appName: string;\n version: string;\n environment: FeedbackEnvironment;\n\n transport: {\n submitUrl: string;\n dialogueUrl: string | undefined;\n screenshotUploadUrl: string | undefined;\n };\n\n dialogue: {\n maxTurns: number;\n timeoutMs: number;\n };\n\n screenshots: {\n enabled: boolean;\n maxSizeBytes: number;\n };\n\n ui: {\n position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n theme: 'light' | 'dark' | 'auto';\n };\n\n observability: {\n enabled: boolean;\n namespace: string;\n traceIdHeaders: string[];\n };\n\n i18n: {\n locale: string;\n customTranslations: Record<string, Record<string, string>>;\n };\n\n signOff: {\n autoConfirmDays: number;\n reminderDays: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst APP_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;\nconst URL_PATTERN = /^https?:\\/\\/.+/;\n\n/** Validation result with structured errors */\nexport interface ConfigValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Validates a FeedbackConfig and returns structured errors.\n * Returns { valid: false, errors: [...] } with actionable messages.\n */\nexport function validateConfig(config: Partial<FeedbackConfig>): ConfigValidationResult {\n const errors: string[] = [];\n\n // Validate appName\n if (!config.appName || config.appName.trim() === '') {\n errors.push('appName is required and must not be empty');\n } else if (!APP_NAME_PATTERN.test(config.appName)) {\n errors.push(\n `appName \"${config.appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/ (no spaces, slashes, or special characters)`,\n );\n }\n\n // Validate transport.submitUrl\n if (!config.transport?.submitUrl || config.transport.submitUrl.trim() === '') {\n errors.push('transport.submitUrl is required and must not be empty');\n } else if (!URL_PATTERN.test(config.transport.submitUrl)) {\n errors.push(\n `transport.submitUrl \"${config.transport.submitUrl}\" must be a valid URL starting with http:// or https://`,\n );\n }\n\n // Validate dialogue.maxTurns\n if (config.dialogue?.maxTurns !== undefined) {\n const turns = config.dialogue.maxTurns;\n if (!Number.isInteger(turns) || turns < 1 || turns > 10) {\n errors.push(\n `dialogue.maxTurns must be an integer between 1 and 10 (got ${turns})`,\n );\n }\n }\n\n // Validate screenshots.maxSizeBytes\n if (config.screenshots?.maxSizeBytes !== undefined) {\n const size = config.screenshots.maxSizeBytes;\n if (!Number.isFinite(size) || size <= 0) {\n errors.push(`screenshots.maxSizeBytes must be a positive number (got ${size})`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Merges user-supplied partial config with defaults.\n * Returns a fully-resolved config with no undefined fields.\n */\nexport function resolveConfig(config: FeedbackConfig): ResolvedFeedbackConfig {\n const screenshotsEnabled =\n config.screenshots?.enabled !== undefined\n ? config.screenshots.enabled\n : !!config.transport.screenshotUploadUrl;\n\n return {\n appName: config.appName,\n version: config.version ?? '0.0.0',\n environment: config.environment ?? 'production',\n\n transport: {\n submitUrl: config.transport.submitUrl,\n dialogueUrl: config.transport.dialogueUrl,\n screenshotUploadUrl: config.transport.screenshotUploadUrl,\n },\n\n dialogue: {\n maxTurns: config.dialogue?.maxTurns ?? 3,\n timeoutMs: config.dialogue?.timeoutMs ?? 30_000,\n },\n\n screenshots: {\n enabled: screenshotsEnabled,\n maxSizeBytes: config.screenshots?.maxSizeBytes ?? 5_242_880,\n },\n\n ui: {\n position: config.ui?.position ?? 'bottom-right',\n theme: config.ui?.theme ?? 'auto',\n },\n\n observability: {\n enabled: config.observability?.enabled ?? true,\n namespace: config.observability?.namespace ?? 'Feedback/Service',\n traceIdHeaders: config.observability?.traceIdHeaders ?? [\n 'x-trace-id',\n 'x-request-id',\n ],\n },\n\n i18n: {\n locale: config.i18n?.locale ?? 'en',\n customTranslations: config.i18n?.customTranslations ?? {},\n },\n\n signOff: {\n autoConfirmDays: config.signOff?.autoConfirmDays ?? 7,\n reminderDays: config.signOff?.reminderDays ?? 3,\n },\n };\n}\n","/**\n * English (en) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const en: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Close',\n 'feedback.button.screenshot': 'Add screenshot',\n 'feedback.button.removeScreenshot': 'Remove screenshot',\n 'feedback.button.submit': 'Submit feedback',\n\n // Form labels\n 'feedback.form.contentLabel': 'What would you like to share?',\n 'feedback.form.contentPlaceholder': 'Describe the issue or suggestion in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Select a type',\n\n // Confirm (review) form\n 'feedback.confirm.heading': 'Review your feedback',\n 'feedback.confirm.titleLabel': 'Title',\n 'feedback.confirm.descriptionLabel': 'Description',\n 'feedback.confirm.typeLabel': 'Type',\n 'feedback.confirm.scopeLabel': 'Scope',\n 'feedback.confirm.sizeLabel': 'Size',\n 'feedback.confirm.duplicateWarning': 'Similar feedback already exists:',\n 'feedback.confirm.scopePlaceholder': 'Select scope (optional)',\n 'feedback.confirm.scope.isolated': 'Isolated',\n 'feedback.confirm.scope.crossCutting': 'Cross-cutting',\n 'feedback.confirm.scope.systemic': 'Systemic',\n 'feedback.confirm.sizePlaceholder': 'Select size (optional)',\n 'feedback.confirm.size.trivial': 'Trivial',\n 'feedback.confirm.size.small': 'Small',\n 'feedback.confirm.size.medium': 'Medium',\n 'feedback.confirm.size.large': 'Large',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Thank you for your feedback. I have a few questions to help us understand better.',\n 'feedback.dialogue.thinking': 'Thinking...',\n 'feedback.dialogue.error': 'Sorry, something went wrong. Please try again.',\n 'feedback.dialogue.complete': 'Thank you! Your feedback has been recorded.',\n 'feedback.dialogue.turnPrompt': 'Your response:',\n\n // Feedback types\n 'feedback.types.bug': 'Bug report',\n 'feedback.types.feature': 'Feature request',\n 'feedback.types.ux': 'UX improvement',\n 'feedback.types.other': 'Other',\n\n // Status messages\n 'feedback.status.submitting': 'Submitting your feedback...',\n 'feedback.status.success': 'Feedback submitted successfully!',\n 'feedback.status.error': 'Failed to submit feedback. Please try again.',\n 'feedback.status.retry': 'Retry',\n 'feedback.status.duplicate': 'This feedback has already been submitted.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot attached',\n 'feedback.toast.screenshotRemoved': 'Screenshot removed',\n 'feedback.toast.screenshotError': 'Failed to capture screenshot',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Mark as resolved',\n 'feedback.signoff.reopenLabel': 'Reopen',\n 'feedback.signoff.autoConfirmNotice':\n 'This issue will be marked resolved in {{days}} days unless you reopen it.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Edit',\n 'feedback.correction.reasonLabel': 'Reason for change',\n 'feedback.correction.saveLabel': 'Save changes',\n};\n","/**\n * Dutch (nl) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const nl: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Sluiten',\n 'feedback.button.screenshot': 'Screenshot toevoegen',\n 'feedback.button.removeScreenshot': 'Screenshot verwijderen',\n 'feedback.button.submit': 'Feedback versturen',\n\n // Form labels\n 'feedback.form.contentLabel': 'Wat wilt u delen?',\n 'feedback.form.contentPlaceholder': 'Beschrijf het probleem of de suggestie in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Selecteer een type',\n\n // Confirm (review) form\n 'feedback.confirm.heading': 'Controleer uw feedback',\n 'feedback.confirm.titleLabel': 'Titel',\n 'feedback.confirm.descriptionLabel': 'Omschrijving',\n 'feedback.confirm.typeLabel': 'Type',\n 'feedback.confirm.scopeLabel': 'Reikwijdte',\n 'feedback.confirm.sizeLabel': 'Omvang',\n 'feedback.confirm.duplicateWarning': 'Er bestaat al vergelijkbare feedback:',\n 'feedback.confirm.scopePlaceholder': 'Selecteer reikwijdte (optioneel)',\n 'feedback.confirm.scope.isolated': 'Geïsoleerd',\n 'feedback.confirm.scope.crossCutting': 'Overkoepelend',\n 'feedback.confirm.scope.systemic': 'Systemisch',\n 'feedback.confirm.sizePlaceholder': 'Selecteer omvang (optioneel)',\n 'feedback.confirm.size.trivial': 'Triviaal',\n 'feedback.confirm.size.small': 'Klein',\n 'feedback.confirm.size.medium': 'Middel',\n 'feedback.confirm.size.large': 'Groot',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.',\n 'feedback.dialogue.thinking': 'Aan het nadenken...',\n 'feedback.dialogue.error': 'Er is iets misgegaan. Probeer het opnieuw.',\n 'feedback.dialogue.complete': 'Bedankt! Uw feedback is geregistreerd.',\n 'feedback.dialogue.turnPrompt': 'Uw antwoord:',\n\n // Feedback types\n 'feedback.types.bug': 'Foutmelding',\n 'feedback.types.feature': 'Functiewens',\n 'feedback.types.ux': 'UX-verbetering',\n 'feedback.types.other': 'Overig',\n\n // Status messages\n 'feedback.status.submitting': 'Feedback verzenden...',\n 'feedback.status.success': 'Feedback succesvol verzonden!',\n 'feedback.status.error': 'Verzenden mislukt. Probeer het opnieuw.',\n 'feedback.status.retry': 'Opnieuw proberen',\n 'feedback.status.duplicate': 'Deze feedback is al ingediend.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot bijgevoegd',\n 'feedback.toast.screenshotRemoved': 'Screenshot verwijderd',\n 'feedback.toast.screenshotError': 'Screenshot maken mislukt',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Markeer als opgelost',\n 'feedback.signoff.reopenLabel': 'Heropenen',\n 'feedback.signoff.autoConfirmNotice':\n 'Dit probleem wordt als opgelost gemarkeerd over {{days}} dagen, tenzij u het heropent.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Bewerken',\n 'feedback.correction.reasonLabel': 'Reden voor wijziging',\n 'feedback.correction.saveLabel': 'Wijzigingen opslaan',\n};\n","/**\n * Internationalisation utilities for the feedback package.\n *\n * t() falls back gracefully:\n * - Unknown locale → English\n * - Unknown key → returns the key itself (never undefined)\n *\n * Template interpolation: {{param}} → replaced with params values\n */\n\nimport { en } from './locales/en.js';\nimport { nl } from './locales/nl.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────\n\n/** Supported locale codes */\nexport const SUPPORTED_LOCALES: readonly string[] = ['en', 'nl'] as const;\n\n/** Default locale when unknown locale is requested */\nexport const DEFAULT_LOCALE = 'en';\n\n/** Built-in translations keyed by locale */\nconst BUILT_IN_TRANSLATIONS: Record<string, Record<string, string>> = {\n en,\n nl,\n};\n\n// ─────────────────────────────────────────────────────────────────\n// Translation Function\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Looks up a translation key.\n *\n * Fallback chain:\n * 1. customTranslations[locale][key]\n * 2. BUILT_IN_TRANSLATIONS[locale][key]\n * 3. BUILT_IN_TRANSLATIONS['en'][key]\n * 4. key itself (never returns undefined or throws)\n *\n * Template interpolation: {{days}} → params.days (if params provided)\n *\n * @param key - Dot-notation key e.g. 'feedback.button.open'\n * @param locale - BCP 47 locale code e.g. 'en', 'nl'\n * @param customTranslations - Optional custom translations (overrides built-in)\n * @param params - Optional template parameters e.g. { days: 7 }\n */\nexport function t(\n key: string,\n locale: string,\n customTranslations?: Record<string, Record<string, string>>,\n params?: Record<string, string | number>,\n): string {\n // 1. Check custom translations for this locale\n const customForLocale = customTranslations?.[locale];\n if (customForLocale?.[key] !== undefined) {\n return interpolate(customForLocale[key], params);\n }\n\n // 2. Check custom translations for English fallback\n const customEn = customTranslations?.['en'];\n if (locale !== 'en' && customEn?.[key] !== undefined) {\n // Only use English custom if locale not found\n }\n\n // 3. Check built-in translations for this locale\n const builtInForLocale = BUILT_IN_TRANSLATIONS[locale];\n if (builtInForLocale?.[key] !== undefined) {\n return interpolate(builtInForLocale[key] as string, params);\n }\n\n // 4. Fall back to English built-in\n const englishBuiltIn = BUILT_IN_TRANSLATIONS['en'];\n if (englishBuiltIn?.[key] !== undefined) {\n return interpolate(englishBuiltIn[key] as string, params);\n }\n\n // 5. Fall back to English custom (if provided)\n if (customEn?.[key] !== undefined) {\n return interpolate(customEn[key] as string, params);\n }\n\n // 6. Return the key itself — never undefined\n return key;\n}\n\n/**\n * Replaces {{param}} placeholders with values from params.\n */\nfunction interpolate(\n template: string,\n params?: Record<string, string | number>,\n): string {\n if (!params) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, name: string) => {\n const value = params[name];\n return value !== undefined ? String(value) : `{{${name}}}`;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Locale Detection\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Returns the locale to use.\n * If preferredLocale is a supported locale → return it.\n * Otherwise → return DEFAULT_LOCALE ('en').\n *\n * @param preferredLocale - BCP 47 locale code or undefined\n */\nexport function detectLocale(preferredLocale?: string): string {\n if (preferredLocale && SUPPORTED_LOCALES.includes(preferredLocale)) {\n return preferredLocale;\n }\n return DEFAULT_LOCALE;\n}\n"],"names":[],"mappings":"AA2HA,MAAM,mBAAmB;AACzB,MAAM,cAAc;AAYb,SAAS,eAAe,QAAyD;AACtF,QAAM,SAAmB,CAAA;AAGzB,MAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,KAAA,MAAW,IAAI;AACnD,WAAO,KAAK,2CAA2C;AAAA,EACzD,WAAW,CAAC,iBAAiB,KAAK,OAAO,OAAO,GAAG;AACjD,WAAO;AAAA,MACL,YAAY,OAAO,OAAO;AAAA,IAAA;AAAA,EAE9B;AAGA,MAAI,CAAC,OAAO,WAAW,aAAa,OAAO,UAAU,UAAU,KAAA,MAAW,IAAI;AAC5E,WAAO,KAAK,uDAAuD;AAAA,EACrE,WAAW,CAAC,YAAY,KAAK,OAAO,UAAU,SAAS,GAAG;AACxD,WAAO;AAAA,MACL,wBAAwB,OAAO,UAAU,SAAS;AAAA,IAAA;AAAA,EAEtD;AAGA,MAAI,OAAO,UAAU,aAAa,QAAW;AAC3C,UAAM,QAAQ,OAAO,SAAS;AAC9B,QAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,IAAI;AACvD,aAAO;AAAA,QACL,8DAA8D,KAAK;AAAA,MAAA;AAAA,IAEvE;AAAA,EACF;AAGA,MAAI,OAAO,aAAa,iBAAiB,QAAW;AAClD,UAAM,OAAO,OAAO,YAAY;AAChC,QAAI,CAAC,OAAO,SAAS,IAAI,KAAK,QAAQ,GAAG;AACvC,aAAO,KAAK,2DAA2D,IAAI,GAAG;AAAA,IAChF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OAAO,OAAO,WAAW;AAAA,IACzB;AAAA,EAAA;AAEJ;AAMO,SAAS,cAAc,QAAgD;AAC5E,QAAM,qBACJ,OAAO,aAAa,YAAY,SAC5B,OAAO,YAAY,UACnB,CAAC,CAAC,OAAO,UAAU;AAEzB,SAAO;AAAA,IACL,SAAS,OAAO;AAAA,IAChB,SAAS,OAAO,WAAW;AAAA,IAC3B,aAAa,OAAO,eAAe;AAAA,IAEnC,WAAW;AAAA,MACT,WAAW,OAAO,UAAU;AAAA,MAC5B,aAAa,OAAO,UAAU;AAAA,MAC9B,qBAAqB,OAAO,UAAU;AAAA,IAAA;AAAA,IAGxC,UAAU;AAAA,MACR,UAAU,OAAO,UAAU,YAAY;AAAA,MACvC,WAAW,OAAO,UAAU,aAAa;AAAA,IAAA;AAAA,IAG3C,aAAa;AAAA,MACX,SAAS;AAAA,MACT,cAAc,OAAO,aAAa,gBAAgB;AAAA,IAAA;AAAA,IAGpD,IAAI;AAAA,MACF,UAAU,OAAO,IAAI,YAAY;AAAA,MACjC,OAAO,OAAO,IAAI,SAAS;AAAA,IAAA;AAAA,IAG7B,eAAe;AAAA,MACb,SAAS,OAAO,eAAe,WAAW;AAAA,MAC1C,WAAW,OAAO,eAAe,aAAa;AAAA,MAC9C,gBAAgB,OAAO,eAAe,kBAAkB;AAAA,QACtD;AAAA,QACA;AAAA,MAAA;AAAA,IACF;AAAA,IAGF,MAAM;AAAA,MACJ,QAAQ,OAAO,MAAM,UAAU;AAAA,MAC/B,oBAAoB,OAAO,MAAM,sBAAsB,CAAA;AAAA,IAAC;AAAA,IAG1D,SAAS;AAAA,MACP,iBAAiB,OAAO,SAAS,mBAAmB;AAAA,MACpD,cAAc,OAAO,SAAS,gBAAgB;AAAA,IAAA;AAAA,EAChD;AAEJ;ACxOO,MAAM,KAA6B;AAAA;AAAA,EAExC,wBAAwB;AAAA,EACxB,yBAAyB;AAAA,EACzB,8BAA8B;AAAA,EAC9B,oCAAoC;AAAA,EACpC,0BAA0B;AAAA;AAAA,EAG1B,8BAA8B;AAAA,EAC9B,oCAAoC;AAAA,EACpC,2BAA2B;AAAA,EAC3B,iCAAiC;AAAA;AAAA,EAGjC,4BAA4B;AAAA,EAC5B,+BAA+B;AAAA,EAC/B,qCAAqC;AAAA,EACrC,8BAA8B;AAAA,EAC9B,+BAA+B;AAAA,EAC/B,8BAA8B;AAAA,EAC9B,qCAAqC;AAAA,EACrC,qCAAqC;AAAA,EACrC,mCAAmC;AAAA,EACnC,uCAAuC;AAAA,EACvC,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,iCAAiC;AAAA,EACjC,+BAA+B;AAAA,EAC/B,gCAAgC;AAAA,EAChC,+BAA+B;AAAA;AAAA,EAG/B,6BAA6B;AAAA,EAC7B,8BAA8B;AAAA,EAC9B,2BAA2B;AAAA,EAC3B,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA;AAAA,EAGhC,sBAAsB;AAAA,EACtB,0BAA0B;AAAA,EAC1B,qBAAqB;AAAA,EACrB,wBAAwB;AAAA;AAAA,EAGxB,8BAA8B;AAAA,EAC9B,2BAA2B;AAAA,EAC3B,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,6BAA6B;AAAA;AAAA,EAG7B,kCAAkC;AAAA,EAClC,oCAAoC;AAAA,EACpC,kCAAkC;AAAA;AAAA,EAGlC,iCAAiC;AAAA,EACjC,gCAAgC;AAAA,EAChC,sCACE;AAAA;AAAA,EAGF,iCAAiC;AAAA,EACjC,mCAAmC;AAAA,EACnC,iCAAiC;AACnC;ACnEO,MAAM,KAA6B;AAAA;AAAA,EAExC,wBAAwB;AAAA,EACxB,yBAAyB;AAAA,EACzB,8BAA8B;AAAA,EAC9B,oCAAoC;AAAA,EACpC,0BAA0B;AAAA;AAAA,EAG1B,8BAA8B;AAAA,EAC9B,oCAAoC;AAAA,EACpC,2BAA2B;AAAA,EAC3B,iCAAiC;AAAA;AAAA,EAGjC,4BAA4B;AAAA,EAC5B,+BAA+B;AAAA,EAC/B,qCAAqC;AAAA,EACrC,8BAA8B;AAAA,EAC9B,+BAA+B;AAAA,EAC/B,8BAA8B;AAAA,EAC9B,qCAAqC;AAAA,EACrC,qCAAqC;AAAA,EACrC,mCAAmC;AAAA,EACnC,uCAAuC;AAAA,EACvC,mCAAmC;AAAA,EACnC,oCAAoC;AAAA,EACpC,iCAAiC;AAAA,EACjC,+BAA+B;AAAA,EAC/B,gCAAgC;AAAA,EAChC,+BAA+B;AAAA;AAAA,EAG/B,6BAA6B;AAAA,EAC7B,8BAA8B;AAAA,EAC9B,2BAA2B;AAAA,EAC3B,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA;AAAA,EAGhC,sBAAsB;AAAA,EACtB,0BAA0B;AAAA,EAC1B,qBAAqB;AAAA,EACrB,wBAAwB;AAAA;AAAA,EAGxB,8BAA8B;AAAA,EAC9B,2BAA2B;AAAA,EAC3B,yBAAyB;AAAA,EACzB,yBAAyB;AAAA,EACzB,6BAA6B;AAAA;AAAA,EAG7B,kCAAkC;AAAA,EAClC,oCAAoC;AAAA,EACpC,kCAAkC;AAAA;AAAA,EAGlC,iCAAiC;AAAA,EACjC,gCAAgC;AAAA,EAChC,sCACE;AAAA;AAAA,EAGF,iCAAiC;AAAA,EACjC,mCAAmC;AAAA,EACnC,iCAAiC;AACnC;ACrDO,MAAM,oBAAuC,CAAC,MAAM,IAAI;AAGxD,MAAM,iBAAiB;AAG9B,MAAM,wBAAgE;AAAA,EACpE;AAAA,EACA;AACF;AAsBO,SAAS,EACd,KACA,QACA,oBACA,QACQ;AAER,QAAM,kBAAkB,qBAAqB,MAAM;AACnD,MAAI,kBAAkB,GAAG,MAAM,QAAW;AACxC,WAAO,YAAY,gBAAgB,GAAG,GAAG,MAAM;AAAA,EACjD;AAGA,QAAM,WAAW,qBAAqB,IAAI;AAC1C,MAAI,WAAW,QAAQ,WAAW,GAAG,MAAM,OAAW;AAKtD,QAAM,mBAAmB,sBAAsB,MAAM;AACrD,MAAI,mBAAmB,GAAG,MAAM,QAAW;AACzC,WAAO,YAAY,iBAAiB,GAAG,GAAa,MAAM;AAAA,EAC5D;AAGA,QAAM,iBAAiB,sBAAsB,IAAI;AACjD,MAAI,iBAAiB,GAAG,MAAM,QAAW;AACvC,WAAO,YAAY,eAAe,GAAG,GAAa,MAAM;AAAA,EAC1D;AAGA,MAAI,WAAW,GAAG,MAAM,QAAW;AACjC,WAAO,YAAY,SAAS,GAAG,GAAa,MAAM;AAAA,EACpD;AAGA,SAAO;AACT;AAKA,SAAS,YACP,UACA,QACQ;AACR,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,kBAAkB,CAAC,QAAQ,SAAiB;AAClE,UAAM,QAAQ,OAAO,IAAI;AACzB,WAAO,UAAU,SAAY,OAAO,KAAK,IAAI,KAAK,IAAI;AAAA,EACxD,CAAC;AACH;AAaO,SAAS,aAAa,iBAAkC;AAC7D,MAAI,mBAAmB,kBAAkB,SAAS,eAAe,GAAG;AAClE,WAAO;AAAA,EACT;AACA,SAAO;AACT;"}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../src/core/locales/en.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiDrC,CAAC"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../src/core/locales/en.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmErC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../../src/core/locales/nl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiDrC,CAAC"}
1
+ {"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../../src/core/locales/nl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmErC,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, S, d, r, t, v } from "./chunks/i18n-Du-or3T4.js";
1
+ import { D, S, d, r, t, v } from "./chunks/i18n-Cba0xHU-.js";
2
2
  import { e, i, r as r2, a, w } from "./chunks/observability-CfwHKObh.js";
3
3
  const BASE_PRIORITY = {
4
4
  bug: 60,
@@ -4,7 +4,8 @@ import { FeedbackDetailPageProps } from './types.js';
4
4
  * Location: src/react/admin/FeedbackDetailPage.tsx
5
5
  *
6
6
  * Admin detail page for viewing a single feedback item.
7
- * Uses DetailPage + DetailSection + DetailRow from anton-bakker-tailwind-components.
7
+ * Renders structured content (bug reports, feature requests) with all fields,
8
+ * screenshot, console errors, and dialogue history.
8
9
  */
9
10
  import * as React from 'react';
10
11
  declare const FeedbackDetailPage: React.ForwardRefExoticComponent<FeedbackDetailPageProps & React.RefAttributes<HTMLDivElement>>;
@@ -1 +1 @@
1
- {"version":3,"file":"FeedbackDetailPage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG1D,QAAA,MAAM,kBAAkB,gGAoGvB,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"FeedbackDetailPage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAkC1D,QAAA,MAAM,kBAAkB,gGAuKvB,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -115,11 +115,31 @@ const FeedbackListPage = React.forwardRef(
115
115
  }
116
116
  );
117
117
  FeedbackListPage.displayName = "FeedbackListPage";
118
+ function parseStructuredContent(content) {
119
+ const lines = content.split("\n");
120
+ let header;
121
+ const fields = [];
122
+ for (const line of lines) {
123
+ const trimmed = line.trim();
124
+ if (!trimmed) continue;
125
+ const headerMatch = trimmed.match(/^\[(.+)\]$/);
126
+ if (headerMatch && headerMatch[1]) {
127
+ header = headerMatch[1];
128
+ continue;
129
+ }
130
+ const fieldMatch = trimmed.match(/^([A-Z][A-Za-z\s]+):\s*(.+)$/);
131
+ if (fieldMatch && fieldMatch[1] && fieldMatch[2]) {
132
+ fields.push({ label: fieldMatch[1], value: fieldMatch[2] });
133
+ }
134
+ }
135
+ return { header, fields, raw: content };
136
+ }
118
137
  const FeedbackDetailPage = React.forwardRef(
119
- ({ feedbackId, fetchItem, onNavigate, currentUser, className }, ref) => {
138
+ ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser, className }, ref) => {
120
139
  const [item, setItem] = React.useState(null);
121
140
  const [loading, setLoading] = React.useState(true);
122
141
  const [error, setError] = React.useState(null);
142
+ const [screenshotUrl, setScreenshotUrl] = React.useState(null);
123
143
  React.useEffect(() => {
124
144
  let cancelled = false;
125
145
  setLoading(true);
@@ -135,6 +155,11 @@ const FeedbackDetailPage = React.forwardRef(
135
155
  cancelled = true;
136
156
  };
137
157
  }, [feedbackId, fetchItem]);
158
+ React.useEffect(() => {
159
+ if (item?.hasScreenshot && item.screenshotS3Key && getScreenshotUrl) {
160
+ getScreenshotUrl(item.screenshotS3Key).then(setScreenshotUrl).catch(() => setScreenshotUrl(null));
161
+ }
162
+ }, [item?.hasScreenshot, item?.screenshotS3Key, getScreenshotUrl]);
138
163
  if (error) {
139
164
  return /* @__PURE__ */ jsx("div", { ref, className, children: /* @__PURE__ */ jsx(DetailPage, { title: "Feedback", headerActions: /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onNavigate("/feedback"), children: "Back to list" }), children: /* @__PURE__ */ jsx("div", { className: "p-6 text-destructive", children: error }) }) });
140
165
  }
@@ -142,10 +167,12 @@ const FeedbackDetailPage = React.forwardRef(
142
167
  /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onNavigate("/feedback"), children: "Back to list" }),
143
168
  /* @__PURE__ */ jsx(Button, { onClick: () => onNavigate(`/feedback/${feedbackId}/edit`), children: "Edit" })
144
169
  ] });
170
+ const parsed = item ? parseStructuredContent(item.content) : null;
171
+ const hasStructuredFields = parsed && parsed.fields.length > 0;
145
172
  return /* @__PURE__ */ jsx("div", { ref, className, children: /* @__PURE__ */ jsx(
146
173
  DetailPage,
147
174
  {
148
- title: item?.content?.slice(0, 50) || `Feedback ${feedbackId.slice(0, 12)}…`,
175
+ title: item?.content?.split("\n").find((l) => l.match(/^Summary:/))?.replace("Summary: ", "") || parsed?.header || `Feedback ${feedbackId.slice(0, 12)}…`,
149
176
  headerActions,
150
177
  loading,
151
178
  testId: "feedback-detail-page",
@@ -161,15 +188,34 @@ const FeedbackDetailPage = React.forwardRef(
161
188
  item.scope && /* @__PURE__ */ jsx(DetailRow, { label: "Scope", children: /* @__PURE__ */ jsx(Badge, { variant: "outline", children: item.scope }) }),
162
189
  item.size && /* @__PURE__ */ jsx(DetailRow, { label: "Size", children: /* @__PURE__ */ jsx(Badge, { variant: "outline", children: item.size }) })
163
190
  ] }),
164
- /* @__PURE__ */ jsx(DetailSection, { title: "Content", children: /* @__PURE__ */ jsx(DetailRow, { label: "Description", children: /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: item.content }) }) }),
191
+ /* @__PURE__ */ jsx(DetailSection, { title: "Content", children: hasStructuredFields ? (
192
+ // Render each structured field as a separate row
193
+ parsed.fields.map((field, i) => /* @__PURE__ */ jsx(DetailRow, { label: field.label, children: /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: field.value }) }, i))
194
+ ) : (
195
+ // Fallback: render raw content
196
+ /* @__PURE__ */ jsx(DetailRow, { label: "Description", children: /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap", children: item.content }) })
197
+ ) }),
198
+ item.hasScreenshot && /* @__PURE__ */ jsx(DetailSection, { title: "Screenshot", children: /* @__PURE__ */ jsx(DetailRow, { label: "Capture", children: screenshotUrl ? /* @__PURE__ */ jsx("a", { href: screenshotUrl, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(
199
+ "img",
200
+ {
201
+ src: screenshotUrl,
202
+ alt: "Feedback screenshot",
203
+ className: "max-w-full max-h-[400px] rounded border cursor-pointer hover:opacity-90 transition-opacity"
204
+ }
205
+ ) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: getScreenshotUrl ? "Loading screenshot…" : "Screenshot available (no viewer configured)" }) }) }),
206
+ item.context?.consoleErrors && item.context.consoleErrors.length > 0 && /* @__PURE__ */ jsx(DetailSection, { title: "Console Errors", collapsible: true, defaultOpen: true, children: /* @__PURE__ */ jsx(DetailRow, { label: `${item.context.consoleErrors.length} error(s)`, children: /* @__PURE__ */ jsx("ul", { className: "list-none space-y-1", children: item.context.consoleErrors.map((err, i) => /* @__PURE__ */ jsx("li", { className: "font-mono text-xs text-destructive bg-destructive/5 p-2 rounded", children: err }, i)) }) }) }),
165
207
  item.dialogueHistory && item.dialogueHistory.length > 0 && /* @__PURE__ */ jsx(DetailSection, { title: "Dialogue History", collapsible: true, defaultOpen: false, children: item.dialogueHistory.map((entry, i) => /* @__PURE__ */ jsxs(DetailRow, { label: entry.role === "user" ? "User" : "Assistant", children: [
166
208
  /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: entry.content }),
167
209
  entry.timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(entry.timestamp).toLocaleString() })
168
210
  ] }, i)) }),
169
211
  item.context && /* @__PURE__ */ jsxs(DetailSection, { title: "Browser Context", collapsible: true, defaultOpen: false, children: [
170
212
  item.context.url && /* @__PURE__ */ jsx(DetailRow, { label: "URL", children: item.context.url }),
213
+ item.context.pageTitle && /* @__PURE__ */ jsx(DetailRow, { label: "Page", children: item.context.pageTitle }),
171
214
  item.context.userAgent && /* @__PURE__ */ jsx(DetailRow, { label: "User Agent", children: item.context.userAgent }),
172
- item.context.viewport && /* @__PURE__ */ jsx(DetailRow, { label: "Viewport", children: `${item.context.viewport.width}×${item.context.viewport.height}` })
215
+ item.context.viewport && /* @__PURE__ */ jsx(DetailRow, { label: "Viewport", children: `${item.context.viewport.width}×${item.context.viewport.height}` }),
216
+ item.context.locale && /* @__PURE__ */ jsx(DetailRow, { label: "Locale", children: item.context.locale }),
217
+ item.context.timezone && /* @__PURE__ */ jsx(DetailRow, { label: "Timezone", children: item.context.timezone }),
218
+ item.context.performanceMs != null && /* @__PURE__ */ jsx(DetailRow, { label: "Page Load", children: `${item.context.performanceMs}ms` })
173
219
  ] }),
174
220
  item.tags && item.tags.length > 0 && /* @__PURE__ */ jsx(DetailSection, { title: "Tags", children: /* @__PURE__ */ jsx(DetailRow, { label: "Tags", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: item.tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "outline", children: tag }, tag)) }) }) })
175
221
  ] })
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/react/admin/FeedbackListPage.tsx","../../../src/react/admin/FeedbackDetailPage.tsx","../../../src/react/admin/hooks/useStatusTransition.ts","../../../src/react/admin/FeedbackUpdatePage.tsx"],"sourcesContent":["/**\n * FeedbackListPage.tsx\n * Location: src/react/admin/FeedbackListPage.tsx\n *\n * Admin list page for browsing and filtering feedback items.\n * Thin wrapper around ListPage from anton-bakker-tailwind-components\n * for visual consistency with sagrosoft20.\n */\nimport * as React from 'react';\nimport { ListPage, Badge } from 'anton-bakker-tailwind-components';\nimport type { SimpleColumn, FilterBarProps } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType } from '../../core/types.js';\nimport type { FeedbackListPageProps } from './types.js';\n\nconst STATUS_VARIANT: Record<FeedbackStatus, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n open: 'warning',\n approved: 'default',\n deferred: 'secondary',\n resolved: 'success',\n wontfix: 'destructive',\n};\n\nconst TYPE_VARIANT: Record<FeedbackType, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n bug: 'destructive',\n feature: 'default',\n ux: 'warning',\n other: 'secondary',\n};\n\nfunction StatusBadge({ status }: { status: FeedbackStatus }) {\n return <Badge variant={STATUS_VARIANT[status] ?? 'secondary'}>{status}</Badge>;\n}\n\nfunction TypeBadge({ type }: { type: FeedbackType }) {\n return <Badge variant={TYPE_VARIANT[type] ?? 'secondary'}>{type}</Badge>;\n}\n\nconst FeedbackListPage = React.forwardRef<HTMLDivElement, FeedbackListPageProps>(\n ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser, ...rest }, ref) => {\n const [search, setSearch] = React.useState('');\n const [statusFilter, setStatusFilter] = React.useState('');\n const [typeFilter, setTypeFilter] = React.useState('');\n\n const filteredItems = React.useMemo(() => {\n let result = items;\n if (statusFilter) result = result.filter((i) => i.status === statusFilter);\n if (typeFilter) result = result.filter((i) => i.type === typeFilter);\n if (search) {\n const q = search.toLowerCase();\n result = result.filter((i) => i.content?.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));\n }\n return result;\n }, [items, statusFilter, typeFilter, search]);\n\n const columns: SimpleColumn<StoredFeedbackItem>[] = React.useMemo(() => [\n { header: 'Content', accessor: (row) => row.content?.slice(0, 60) || row.id, sortable: true },\n { header: 'Type', accessor: (row) => <TypeBadge type={row.type} />, sortable: true },\n { header: 'Status', accessor: (row) => <StatusBadge status={row.status} />, sortable: true },\n { header: 'Priority', accessor: 'priority', sortable: true },\n { header: 'Reporters', accessor: 'reporterCount', sortable: true },\n {\n header: 'Date',\n accessor: (row) => row.submittedAt ? new Date(row.submittedAt).toLocaleDateString() : '—',\n sortable: true,\n },\n ], []);\n\n const filterBar: FilterBarProps | undefined = (statusFilter || typeFilter) ? {\n filters: [\n ...(statusFilter ? [{ field: 'status', label: `Status: ${statusFilter}`, value: statusFilter }] : []),\n ...(typeFilter ? [{ field: 'type', label: `Type: ${typeFilter}`, value: typeFilter }] : []),\n ],\n onFilterRemove: (field: string) => {\n if (field === 'status') setStatusFilter('');\n if (field === 'type') setTypeFilter('');\n },\n onFiltersClear: () => { setStatusFilter(''); setTypeFilter(''); },\n } : undefined;\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by status\"\n >\n <option value=\"\">All statuses</option>\n <option value=\"open\">Open</option>\n <option value=\"approved\">Approved</option>\n <option value=\"deferred\">Deferred</option>\n <option value=\"resolved\">Resolved</option>\n <option value=\"wontfix\">Won't fix</option>\n </select>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"bug\">Bug</option>\n <option value=\"feature\">Feature</option>\n <option value=\"ux\">UX</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n );\n\n const listProps = {\n title: 'Feedback' as const,\n columns,\n items: filteredItems,\n loading,\n onRowClick: (row: StoredFeedbackItem) => onNavigate(`/feedback/${row.id}`),\n search,\n onSearchChange: setSearch,\n searchPlaceholder: 'Search feedback...',\n headerActions,\n testId: 'feedback-list-page',\n ...(filterBar ? { filterBar } : {}),\n ...(emptyStateMessage ? { emptyState: emptyStateMessage } : {}),\n ...(className ? { className } : {}),\n };\n\n return <ListPage<StoredFeedbackItem> ref={ref} {...listProps} />;\n },\n);\nFeedbackListPage.displayName = 'FeedbackListPage';\n\nexport { FeedbackListPage, StatusBadge, TypeBadge };\n","/**\n * FeedbackDetailPage.tsx\n * Location: src/react/admin/FeedbackDetailPage.tsx\n *\n * Admin detail page for viewing a single feedback item.\n * Uses DetailPage + DetailSection + DetailRow from anton-bakker-tailwind-components.\n */\nimport * as React from 'react';\nimport { DetailPage, DetailSection, DetailRow, Badge, Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem } from '../../core/types.js';\nimport type { FeedbackDetailPageProps } from './types.js';\nimport { StatusBadge, TypeBadge } from './FeedbackListPage.js';\n\nconst FeedbackDetailPage = React.forwardRef<HTMLDivElement, FeedbackDetailPageProps>(\n ({ feedbackId, fetchItem, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetchItem(feedbackId)\n .then((data) => { if (!cancelled) setItem(data); })\n .catch((err) => { if (!cancelled) setError(err?.message ?? 'Failed to load item'); })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n if (error) {\n return (\n <div ref={ref} className={className}>\n <DetailPage title=\"Feedback\" headerActions={<Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>}>\n <div className=\"p-6 text-destructive\">{error}</div>\n </DetailPage>\n </div>\n );\n }\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>\n <Button onClick={() => onNavigate(`/feedback/${feedbackId}/edit`)}>Edit</Button>\n </div>\n );\n\n return (\n <div ref={ref} className={className}>\n <DetailPage\n title={item?.content?.slice(0, 50) || `Feedback ${feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n loading={loading}\n testId=\"feedback-detail-page\"\n >\n {item && (\n <>\n <DetailSection title=\"Overview\">\n <DetailRow label=\"Status\"><StatusBadge status={item.status} /></DetailRow>\n <DetailRow label=\"Type\"><TypeBadge type={item.type} /></DetailRow>\n <DetailRow label=\"Priority\">{item.priority}</DetailRow>\n <DetailRow label=\"Environment\">{item.environment || '—'}</DetailRow>\n <DetailRow label=\"Submitted\">{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : '—'}</DetailRow>\n <DetailRow label=\"Reporters\">{item.reporterCount ?? 1}</DetailRow>\n {item.assignedTo && <DetailRow label=\"Assigned to\">{item.assignedTo}</DetailRow>}\n {item.scope && <DetailRow label=\"Scope\"><Badge variant=\"outline\">{item.scope}</Badge></DetailRow>}\n {item.size && <DetailRow label=\"Size\"><Badge variant=\"outline\">{item.size}</Badge></DetailRow>}\n </DetailSection>\n\n <DetailSection title=\"Content\">\n <DetailRow label=\"Description\">\n <p className=\"whitespace-pre-wrap\">{item.content}</p>\n </DetailRow>\n </DetailSection>\n\n {item.dialogueHistory && item.dialogueHistory.length > 0 && (\n <DetailSection title=\"Dialogue History\" collapsible defaultOpen={false}>\n {item.dialogueHistory.map((entry, i) => (\n <DetailRow key={i} label={entry.role === 'user' ? 'User' : 'Assistant'}>\n <p className=\"whitespace-pre-wrap text-sm\">{entry.content}</p>\n {entry.timestamp && (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(entry.timestamp).toLocaleString()}\n </span>\n )}\n </DetailRow>\n ))}\n </DetailSection>\n )}\n\n {item.context && (\n <DetailSection title=\"Browser Context\" collapsible defaultOpen={false}>\n {item.context.url && <DetailRow label=\"URL\">{item.context.url}</DetailRow>}\n {item.context.userAgent && <DetailRow label=\"User Agent\">{item.context.userAgent}</DetailRow>}\n {item.context.viewport && <DetailRow label=\"Viewport\">{`${item.context.viewport.width}×${item.context.viewport.height}`}</DetailRow>}\n </DetailSection>\n )}\n\n {item.tags && item.tags.length > 0 && (\n <DetailSection title=\"Tags\">\n <DetailRow label=\"Tags\">\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n </DetailRow>\n </DetailSection>\n )}\n </>\n )}\n </DetailPage>\n </div>\n );\n },\n);\nFeedbackDetailPage.displayName = 'FeedbackDetailPage';\n\nexport { FeedbackDetailPage };\n","/**\n * useStatusTransition — pure hook encoding the FeedbackStatus transition table.\n *\n * Returns the set of valid target statuses reachable from the current status.\n * The current status is excluded from allowedTargets (can't transition to self).\n *\n * No side effects — same inputs always produce same outputs.\n */\nimport type { FeedbackStatus } from '../../../core/types.js';\n\n/**\n * Valid transitions for each FeedbackStatus.\n * open → resolved is deliberately excluded (must go through approved first).\n */\nconst ALLOWED_TRANSITIONS: Record<FeedbackStatus, FeedbackStatus[]> = {\n open: ['approved', 'deferred', 'wontfix'],\n approved: ['open', 'resolved', 'deferred'],\n deferred: ['open', 'approved', 'wontfix'],\n resolved: ['open', 'approved'],\n wontfix: ['open', 'deferred'],\n};\n\nexport interface UseStatusTransitionResult {\n /** Statuses reachable from the current status (current status excluded) */\n allowedTargets: FeedbackStatus[];\n}\n\n/**\n * Returns the allowed target statuses from the given current status.\n *\n * @param currentStatus - The status the item currently has\n * @returns Object with `allowedTargets` — statuses the item can transition to\n *\n * @example\n * const { allowedTargets } = useStatusTransition('open');\n * // allowedTargets → ['approved', 'deferred', 'wontfix']\n */\nexport function useStatusTransition(currentStatus: FeedbackStatus): UseStatusTransitionResult {\n const allowedTargets = ALLOWED_TRANSITIONS[currentStatus] ?? [];\n return { allowedTargets };\n}\n","/**\n * FeedbackUpdatePage.tsx\n * Location: src/react/admin/FeedbackUpdatePage.tsx\n *\n * Admin update page for editing mutable fields of a feedback item.\n * Uses UpdatePage + EnumField + TextField + TextareaField from anton-bakker-tailwind-components.\n */\nimport * as React from 'react';\nimport { UpdatePage, EnumField, TextField, TextareaField, Button, SaveFooter, Badge } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize } from '../../core/types.js';\nimport type { FeedbackUpdatePageProps } from './types.js';\nimport { useStatusTransition } from './hooks/useStatusTransition.js';\n\nconst STATUS_OPTIONS = [\n { value: 'open', label: 'Open' },\n { value: 'approved', label: 'Approved' },\n { value: 'deferred', label: 'Deferred' },\n { value: 'resolved', label: 'Resolved' },\n { value: 'wontfix', label: \"Won't Fix\" },\n];\n\nconst TYPE_OPTIONS = [\n { value: 'bug', label: 'Bug' },\n { value: 'feature', label: 'Feature' },\n { value: 'ux', label: 'UX Issue' },\n { value: 'other', label: 'Other' },\n];\n\nconst SCOPE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'component', label: 'Component' },\n { value: 'page', label: 'Page' },\n { value: 'workflow', label: 'Workflow' },\n];\n\nconst SIZE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'trivial', label: 'Trivial' },\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n];\n\nconst FeedbackUpdatePage = React.forwardRef<HTMLDivElement, FeedbackUpdatePageProps>(\n ({ feedbackId, fetchItem, updateItem, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [saving, setSaving] = React.useState(false);\n\n // Form state\n const [status, setStatus] = React.useState<FeedbackStatus>('open');\n const [type, setType] = React.useState<FeedbackType>('bug');\n const [scope, setScope] = React.useState<string>('');\n const [size, setSize] = React.useState<string>('');\n const [assignedTo, setAssignedTo] = React.useState('');\n const [tags, setTags] = React.useState('');\n const [correctionNote, setCorrectionNote] = React.useState('');\n\n // Load item\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n fetchItem(feedbackId)\n .then((data) => {\n if (cancelled) return;\n setItem(data);\n setStatus(data.status);\n setType(data.type);\n setScope(data.scope ?? '');\n setSize(data.size ?? '');\n setAssignedTo(data.assignedTo ?? '');\n setTags(data.tags?.join(', ') ?? '');\n })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n const { allowedTargets } = useStatusTransition(status);\n const statusOptions = STATUS_OPTIONS.filter(\n (opt) => opt.value === status || allowedTargets.includes(opt.value as FeedbackStatus),\n );\n\n // Dirty detection\n const classificationChanged = item && (type !== item.type || scope !== (item.scope ?? '') || size !== (item.size ?? ''));\n const isDirty = item && (\n status !== item.status ||\n type !== item.type ||\n scope !== (item.scope ?? '') ||\n size !== (item.size ?? '') ||\n assignedTo !== (item.assignedTo ?? '') ||\n tags !== (item.tags?.join(', ') ?? '')\n );\n\n const handleSave = async () => {\n if (!item) return;\n setSaving(true);\n try {\n const patch: Partial<StoredFeedbackItem> = { lastModifiedAt: new Date().toISOString() };\n if (status !== item.status) patch.status = status;\n if (type !== item.type) patch.type = type;\n if (scope !== (item.scope ?? '')) (patch as Record<string, unknown>).scope = scope || undefined;\n if (size !== (item.size ?? '')) (patch as Record<string, unknown>).size = size || undefined;\n if (assignedTo !== (item.assignedTo ?? '')) (patch as Record<string, unknown>).assignedTo = assignedTo || undefined;\n const newTags = tags.split(',').map((t) => t.trim()).filter(Boolean);\n if (JSON.stringify(newTags) !== JSON.stringify(item.tags ?? [])) patch.tags = newTags;\n\n if (status === 'resolved' && item.status !== 'resolved') {\n patch.resolvedAt = new Date().toISOString();\n patch.resolvedBy = currentUser?.userId ?? 'unknown';\n }\n\n if (classificationChanged && correctionNote) {\n patch.corrections = [\n ...(item.corrections ?? []),\n {\n field: [type !== item.type && 'type', scope !== (item.scope ?? '') && 'scope', size !== (item.size ?? '') && 'size'].filter(Boolean).join(', '),\n before: { type: item.type, scope: item.scope, size: item.size },\n after: { type, scope: scope || undefined, size: size || undefined },\n correctedBy: currentUser?.userId ?? 'unknown',\n correctedAt: new Date().toISOString(),\n reason: correctionNote,\n },\n ];\n }\n\n await updateItem(feedbackId, patch);\n onNavigate(`/feedback/${feedbackId}`);\n } finally {\n setSaving(false);\n }\n };\n\n const headerActions = (\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>\n Cancel\n </Button>\n );\n\n return (\n <div ref={ref} className={className}>\n <UpdatePage\n title={`Edit: ${item?.content?.slice(0, 40) || feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n dataLoading={loading}\n actions={\n <SaveFooter dirty={!!isDirty} saving={saving}>\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>Cancel</Button>\n <Button\n onClick={handleSave}\n disabled={saving || !isDirty || (!!classificationChanged && !correctionNote)}\n >\n {saving ? 'Saving…' : 'Save changes'}\n </Button>\n </SaveFooter>\n }\n testId=\"feedback-update-page\"\n >\n {item && (\n <div className=\"space-y-6\">\n {/* Read-only context */}\n <div className=\"rounded-lg border border-border p-4 bg-muted/30\">\n <p className=\"text-sm text-muted-foreground mb-2\">Original submission</p>\n <p className=\"whitespace-pre-wrap text-sm\">{item.content}</p>\n <div className=\"flex gap-3 mt-3 text-xs text-muted-foreground\">\n <span>{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : ''}</span>\n <span>{item.environment}</span>\n <span>{item.reporterCount ?? 1} reporter(s)</span>\n </div>\n </div>\n\n {/* Editable fields */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <EnumField\n label=\"Status\"\n options={statusOptions}\n value={status}\n onChange={(v) => setStatus(v as FeedbackStatus)}\n />\n <EnumField\n label=\"Type\"\n options={TYPE_OPTIONS}\n value={type}\n onChange={(v) => setType(v as FeedbackType)}\n />\n <EnumField\n label=\"Scope\"\n options={SCOPE_OPTIONS}\n value={scope}\n onChange={setScope}\n />\n <EnumField\n label=\"Size\"\n options={SIZE_OPTIONS}\n value={size}\n onChange={setSize}\n />\n <TextField\n label=\"Assigned to\"\n value={assignedTo}\n onChange={(e) => setAssignedTo(e.target.value)}\n placeholder={currentUser?.name ?? currentUser?.userId ?? ''}\n {...(currentUser ? { description: `You: ${currentUser.name ?? currentUser.userId}` } : {})}\n />\n <TextField\n label=\"Tags\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n placeholder=\"Comma-separated tags\"\n description=\"Separate multiple tags with commas\"\n />\n </div>\n\n {/* Correction note (required when classification changes) */}\n {classificationChanged && (\n <TextareaField\n label=\"Correction note\"\n value={correctionNote}\n onChange={(e) => setCorrectionNote(e.target.value)}\n required\n {...(!correctionNote ? { error: 'Required when changing type, scope, or size' } : {})}\n placeholder=\"Explain why this classification is being changed...\"\n />\n )}\n\n {/* Existing tags display */}\n {item.tags && item.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n )}\n </div>\n )}\n </UpdatePage>\n </div>\n );\n },\n);\nFeedbackUpdatePage.displayName = 'FeedbackUpdatePage';\n\nexport { FeedbackUpdatePage };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/react/admin/FeedbackListPage.tsx","../../../src/react/admin/FeedbackDetailPage.tsx","../../../src/react/admin/hooks/useStatusTransition.ts","../../../src/react/admin/FeedbackUpdatePage.tsx"],"sourcesContent":["/**\n * FeedbackListPage.tsx\n * Location: src/react/admin/FeedbackListPage.tsx\n *\n * Admin list page for browsing and filtering feedback items.\n * Thin wrapper around ListPage from anton-bakker-tailwind-components\n * for visual consistency with sagrosoft20.\n */\nimport * as React from 'react';\nimport { ListPage, Badge } from 'anton-bakker-tailwind-components';\nimport type { SimpleColumn, FilterBarProps } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType } from '../../core/types.js';\nimport type { FeedbackListPageProps } from './types.js';\n\nconst STATUS_VARIANT: Record<FeedbackStatus, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n open: 'warning',\n approved: 'default',\n deferred: 'secondary',\n resolved: 'success',\n wontfix: 'destructive',\n};\n\nconst TYPE_VARIANT: Record<FeedbackType, 'default' | 'success' | 'warning' | 'destructive' | 'secondary'> = {\n bug: 'destructive',\n feature: 'default',\n ux: 'warning',\n other: 'secondary',\n};\n\nfunction StatusBadge({ status }: { status: FeedbackStatus }) {\n return <Badge variant={STATUS_VARIANT[status] ?? 'secondary'}>{status}</Badge>;\n}\n\nfunction TypeBadge({ type }: { type: FeedbackType }) {\n return <Badge variant={TYPE_VARIANT[type] ?? 'secondary'}>{type}</Badge>;\n}\n\nconst FeedbackListPage = React.forwardRef<HTMLDivElement, FeedbackListPageProps>(\n ({ items, loading = false, onNavigate, className, emptyStateMessage, currentUser, ...rest }, ref) => {\n const [search, setSearch] = React.useState('');\n const [statusFilter, setStatusFilter] = React.useState('');\n const [typeFilter, setTypeFilter] = React.useState('');\n\n const filteredItems = React.useMemo(() => {\n let result = items;\n if (statusFilter) result = result.filter((i) => i.status === statusFilter);\n if (typeFilter) result = result.filter((i) => i.type === typeFilter);\n if (search) {\n const q = search.toLowerCase();\n result = result.filter((i) => i.content?.toLowerCase().includes(q) || i.id.toLowerCase().includes(q));\n }\n return result;\n }, [items, statusFilter, typeFilter, search]);\n\n const columns: SimpleColumn<StoredFeedbackItem>[] = React.useMemo(() => [\n { header: 'Content', accessor: (row) => row.content?.slice(0, 60) || row.id, sortable: true },\n { header: 'Type', accessor: (row) => <TypeBadge type={row.type} />, sortable: true },\n { header: 'Status', accessor: (row) => <StatusBadge status={row.status} />, sortable: true },\n { header: 'Priority', accessor: 'priority', sortable: true },\n { header: 'Reporters', accessor: 'reporterCount', sortable: true },\n {\n header: 'Date',\n accessor: (row) => row.submittedAt ? new Date(row.submittedAt).toLocaleDateString() : '—',\n sortable: true,\n },\n ], []);\n\n const filterBar: FilterBarProps | undefined = (statusFilter || typeFilter) ? {\n filters: [\n ...(statusFilter ? [{ field: 'status', label: `Status: ${statusFilter}`, value: statusFilter }] : []),\n ...(typeFilter ? [{ field: 'type', label: `Type: ${typeFilter}`, value: typeFilter }] : []),\n ],\n onFilterRemove: (field: string) => {\n if (field === 'status') setStatusFilter('');\n if (field === 'type') setTypeFilter('');\n },\n onFiltersClear: () => { setStatusFilter(''); setTypeFilter(''); },\n } : undefined;\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by status\"\n >\n <option value=\"\">All statuses</option>\n <option value=\"open\">Open</option>\n <option value=\"approved\">Approved</option>\n <option value=\"deferred\">Deferred</option>\n <option value=\"resolved\">Resolved</option>\n <option value=\"wontfix\">Won't fix</option>\n </select>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value)}\n className=\"h-8 rounded-md border border-border bg-transparent px-2 text-sm\"\n aria-label=\"Filter by type\"\n >\n <option value=\"\">All types</option>\n <option value=\"bug\">Bug</option>\n <option value=\"feature\">Feature</option>\n <option value=\"ux\">UX</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n );\n\n const listProps = {\n title: 'Feedback' as const,\n columns,\n items: filteredItems,\n loading,\n onRowClick: (row: StoredFeedbackItem) => onNavigate(`/feedback/${row.id}`),\n search,\n onSearchChange: setSearch,\n searchPlaceholder: 'Search feedback...',\n headerActions,\n testId: 'feedback-list-page',\n ...(filterBar ? { filterBar } : {}),\n ...(emptyStateMessage ? { emptyState: emptyStateMessage } : {}),\n ...(className ? { className } : {}),\n };\n\n return <ListPage<StoredFeedbackItem> ref={ref} {...listProps} />;\n },\n);\nFeedbackListPage.displayName = 'FeedbackListPage';\n\nexport { FeedbackListPage, StatusBadge, TypeBadge };\n","/**\n * FeedbackDetailPage.tsx\n * Location: src/react/admin/FeedbackDetailPage.tsx\n *\n * Admin detail page for viewing a single feedback item.\n * Renders structured content (bug reports, feature requests) with all fields,\n * screenshot, console errors, and dialogue history.\n */\nimport * as React from 'react';\nimport { DetailPage, DetailSection, DetailRow, Badge, Button } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem } from '../../core/types.js';\nimport type { FeedbackDetailPageProps } from './types.js';\nimport { StatusBadge, TypeBadge } from './FeedbackListPage.js';\n\n/**\n * Parse structured content (produced by serializeManualForm or AI dialogue)\n * into labeled key-value pairs.\n * Format: \"Label: value\" lines, with optional [Bug Report] or [Feature Request] header.\n */\nfunction parseStructuredContent(content: string): { header: string | undefined; fields: { label: string; value: string }[]; raw: string } {\n const lines = content.split('\\n');\n let header: string | undefined;\n const fields: { label: string; value: string }[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n // Check for header line like [Bug Report] or [Feature Request]\n const headerMatch = trimmed.match(/^\\[(.+)\\]$/);\n if (headerMatch && headerMatch[1]) {\n header = headerMatch[1];\n continue;\n }\n\n // Check for \"Label: value\" pattern\n const fieldMatch = trimmed.match(/^([A-Z][A-Za-z\\s]+):\\s*(.+)$/);\n if (fieldMatch && fieldMatch[1] && fieldMatch[2]) {\n fields.push({ label: fieldMatch[1], value: fieldMatch[2] });\n }\n }\n\n return { header, fields, raw: content };\n}\n\nconst FeedbackDetailPage = React.forwardRef<HTMLDivElement, FeedbackDetailPageProps>(\n ({ feedbackId, fetchItem, getScreenshotUrl, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n const [screenshotUrl, setScreenshotUrl] = React.useState<string | null>(null);\n\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n setError(null);\n fetchItem(feedbackId)\n .then((data) => { if (!cancelled) setItem(data); })\n .catch((err) => { if (!cancelled) setError(err?.message ?? 'Failed to load item'); })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n // Load screenshot URL when item has one\n React.useEffect(() => {\n if (item?.hasScreenshot && item.screenshotS3Key && getScreenshotUrl) {\n getScreenshotUrl(item.screenshotS3Key)\n .then(setScreenshotUrl)\n .catch(() => setScreenshotUrl(null));\n }\n }, [item?.hasScreenshot, item?.screenshotS3Key, getScreenshotUrl]);\n\n if (error) {\n return (\n <div ref={ref} className={className}>\n <DetailPage title=\"Feedback\" headerActions={<Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>}>\n <div className=\"p-6 text-destructive\">{error}</div>\n </DetailPage>\n </div>\n );\n }\n\n const headerActions = (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"outline\" onClick={() => onNavigate('/feedback')}>Back to list</Button>\n <Button onClick={() => onNavigate(`/feedback/${feedbackId}/edit`)}>Edit</Button>\n </div>\n );\n\n // Parse structured content\n const parsed = item ? parseStructuredContent(item.content) : null;\n const hasStructuredFields = parsed && parsed.fields.length > 0;\n\n return (\n <div ref={ref} className={className}>\n <DetailPage\n title={item?.content?.split('\\n').find(l => l.match(/^Summary:/))?.replace('Summary: ', '') || parsed?.header || `Feedback ${feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n loading={loading}\n testId=\"feedback-detail-page\"\n >\n {item && (\n <>\n <DetailSection title=\"Overview\">\n <DetailRow label=\"Status\"><StatusBadge status={item.status} /></DetailRow>\n <DetailRow label=\"Type\"><TypeBadge type={item.type} /></DetailRow>\n <DetailRow label=\"Priority\">{item.priority}</DetailRow>\n <DetailRow label=\"Environment\">{item.environment || '—'}</DetailRow>\n <DetailRow label=\"Submitted\">{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : '—'}</DetailRow>\n <DetailRow label=\"Reporters\">{item.reporterCount ?? 1}</DetailRow>\n {item.assignedTo && <DetailRow label=\"Assigned to\">{item.assignedTo}</DetailRow>}\n {item.scope && <DetailRow label=\"Scope\"><Badge variant=\"outline\">{item.scope}</Badge></DetailRow>}\n {item.size && <DetailRow label=\"Size\"><Badge variant=\"outline\">{item.size}</Badge></DetailRow>}\n </DetailSection>\n\n <DetailSection title=\"Content\">\n {hasStructuredFields ? (\n // Render each structured field as a separate row\n parsed.fields.map((field, i) => (\n <DetailRow key={i} label={field.label}>\n <p className=\"whitespace-pre-wrap\">{field.value}</p>\n </DetailRow>\n ))\n ) : (\n // Fallback: render raw content\n <DetailRow label=\"Description\">\n <p className=\"whitespace-pre-wrap\">{item.content}</p>\n </DetailRow>\n )}\n </DetailSection>\n\n {/* Screenshot */}\n {item.hasScreenshot && (\n <DetailSection title=\"Screenshot\">\n <DetailRow label=\"Capture\">\n {screenshotUrl ? (\n <a href={screenshotUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <img\n src={screenshotUrl}\n alt=\"Feedback screenshot\"\n className=\"max-w-full max-h-[400px] rounded border cursor-pointer hover:opacity-90 transition-opacity\"\n />\n </a>\n ) : (\n <span className=\"text-muted-foreground text-sm\">\n {getScreenshotUrl ? 'Loading screenshot…' : 'Screenshot available (no viewer configured)'}\n </span>\n )}\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Console errors */}\n {item.context?.consoleErrors && item.context.consoleErrors.length > 0 && (\n <DetailSection title=\"Console Errors\" collapsible defaultOpen>\n <DetailRow label={`${item.context.consoleErrors.length} error(s)`}>\n <ul className=\"list-none space-y-1\">\n {item.context.consoleErrors.map((err, i) => (\n <li key={i} className=\"font-mono text-xs text-destructive bg-destructive/5 p-2 rounded\">\n {err}\n </li>\n ))}\n </ul>\n </DetailRow>\n </DetailSection>\n )}\n\n {/* Dialogue history */}\n {item.dialogueHistory && item.dialogueHistory.length > 0 && (\n <DetailSection title=\"Dialogue History\" collapsible defaultOpen={false}>\n {item.dialogueHistory.map((entry, i) => (\n <DetailRow key={i} label={entry.role === 'user' ? 'User' : 'Assistant'}>\n <p className=\"whitespace-pre-wrap text-sm\">{entry.content}</p>\n {entry.timestamp && (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(entry.timestamp).toLocaleString()}\n </span>\n )}\n </DetailRow>\n ))}\n </DetailSection>\n )}\n\n {/* Browser context */}\n {item.context && (\n <DetailSection title=\"Browser Context\" collapsible defaultOpen={false}>\n {item.context.url && <DetailRow label=\"URL\">{item.context.url}</DetailRow>}\n {item.context.pageTitle && <DetailRow label=\"Page\">{item.context.pageTitle}</DetailRow>}\n {item.context.userAgent && <DetailRow label=\"User Agent\">{item.context.userAgent}</DetailRow>}\n {item.context.viewport && <DetailRow label=\"Viewport\">{`${item.context.viewport.width}×${item.context.viewport.height}`}</DetailRow>}\n {item.context.locale && <DetailRow label=\"Locale\">{item.context.locale}</DetailRow>}\n {item.context.timezone && <DetailRow label=\"Timezone\">{item.context.timezone}</DetailRow>}\n {item.context.performanceMs != null && <DetailRow label=\"Page Load\">{`${item.context.performanceMs}ms`}</DetailRow>}\n </DetailSection>\n )}\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <DetailSection title=\"Tags\">\n <DetailRow label=\"Tags\">\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n </DetailRow>\n </DetailSection>\n )}\n </>\n )}\n </DetailPage>\n </div>\n );\n },\n);\nFeedbackDetailPage.displayName = 'FeedbackDetailPage';\n\nexport { FeedbackDetailPage };\n","/**\n * useStatusTransition — pure hook encoding the FeedbackStatus transition table.\n *\n * Returns the set of valid target statuses reachable from the current status.\n * The current status is excluded from allowedTargets (can't transition to self).\n *\n * No side effects — same inputs always produce same outputs.\n */\nimport type { FeedbackStatus } from '../../../core/types.js';\n\n/**\n * Valid transitions for each FeedbackStatus.\n * open → resolved is deliberately excluded (must go through approved first).\n */\nconst ALLOWED_TRANSITIONS: Record<FeedbackStatus, FeedbackStatus[]> = {\n open: ['approved', 'deferred', 'wontfix'],\n approved: ['open', 'resolved', 'deferred'],\n deferred: ['open', 'approved', 'wontfix'],\n resolved: ['open', 'approved'],\n wontfix: ['open', 'deferred'],\n};\n\nexport interface UseStatusTransitionResult {\n /** Statuses reachable from the current status (current status excluded) */\n allowedTargets: FeedbackStatus[];\n}\n\n/**\n * Returns the allowed target statuses from the given current status.\n *\n * @param currentStatus - The status the item currently has\n * @returns Object with `allowedTargets` — statuses the item can transition to\n *\n * @example\n * const { allowedTargets } = useStatusTransition('open');\n * // allowedTargets → ['approved', 'deferred', 'wontfix']\n */\nexport function useStatusTransition(currentStatus: FeedbackStatus): UseStatusTransitionResult {\n const allowedTargets = ALLOWED_TRANSITIONS[currentStatus] ?? [];\n return { allowedTargets };\n}\n","/**\n * FeedbackUpdatePage.tsx\n * Location: src/react/admin/FeedbackUpdatePage.tsx\n *\n * Admin update page for editing mutable fields of a feedback item.\n * Uses UpdatePage + EnumField + TextField + TextareaField from anton-bakker-tailwind-components.\n */\nimport * as React from 'react';\nimport { UpdatePage, EnumField, TextField, TextareaField, Button, SaveFooter, Badge } from 'anton-bakker-tailwind-components';\nimport type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize } from '../../core/types.js';\nimport type { FeedbackUpdatePageProps } from './types.js';\nimport { useStatusTransition } from './hooks/useStatusTransition.js';\n\nconst STATUS_OPTIONS = [\n { value: 'open', label: 'Open' },\n { value: 'approved', label: 'Approved' },\n { value: 'deferred', label: 'Deferred' },\n { value: 'resolved', label: 'Resolved' },\n { value: 'wontfix', label: \"Won't Fix\" },\n];\n\nconst TYPE_OPTIONS = [\n { value: 'bug', label: 'Bug' },\n { value: 'feature', label: 'Feature' },\n { value: 'ux', label: 'UX Issue' },\n { value: 'other', label: 'Other' },\n];\n\nconst SCOPE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'component', label: 'Component' },\n { value: 'page', label: 'Page' },\n { value: 'workflow', label: 'Workflow' },\n];\n\nconst SIZE_OPTIONS = [\n { value: '', label: '— not set —' },\n { value: 'trivial', label: 'Trivial' },\n { value: 'small', label: 'Small' },\n { value: 'medium', label: 'Medium' },\n { value: 'large', label: 'Large' },\n];\n\nconst FeedbackUpdatePage = React.forwardRef<HTMLDivElement, FeedbackUpdatePageProps>(\n ({ feedbackId, fetchItem, updateItem, onNavigate, currentUser, className }, ref) => {\n const [item, setItem] = React.useState<StoredFeedbackItem | null>(null);\n const [loading, setLoading] = React.useState(true);\n const [saving, setSaving] = React.useState(false);\n\n // Form state\n const [status, setStatus] = React.useState<FeedbackStatus>('open');\n const [type, setType] = React.useState<FeedbackType>('bug');\n const [scope, setScope] = React.useState<string>('');\n const [size, setSize] = React.useState<string>('');\n const [assignedTo, setAssignedTo] = React.useState('');\n const [tags, setTags] = React.useState('');\n const [correctionNote, setCorrectionNote] = React.useState('');\n\n // Load item\n React.useEffect(() => {\n let cancelled = false;\n setLoading(true);\n fetchItem(feedbackId)\n .then((data) => {\n if (cancelled) return;\n setItem(data);\n setStatus(data.status);\n setType(data.type);\n setScope(data.scope ?? '');\n setSize(data.size ?? '');\n setAssignedTo(data.assignedTo ?? '');\n setTags(data.tags?.join(', ') ?? '');\n })\n .finally(() => { if (!cancelled) setLoading(false); });\n return () => { cancelled = true; };\n }, [feedbackId, fetchItem]);\n\n const { allowedTargets } = useStatusTransition(status);\n const statusOptions = STATUS_OPTIONS.filter(\n (opt) => opt.value === status || allowedTargets.includes(opt.value as FeedbackStatus),\n );\n\n // Dirty detection\n const classificationChanged = item && (type !== item.type || scope !== (item.scope ?? '') || size !== (item.size ?? ''));\n const isDirty = item && (\n status !== item.status ||\n type !== item.type ||\n scope !== (item.scope ?? '') ||\n size !== (item.size ?? '') ||\n assignedTo !== (item.assignedTo ?? '') ||\n tags !== (item.tags?.join(', ') ?? '')\n );\n\n const handleSave = async () => {\n if (!item) return;\n setSaving(true);\n try {\n const patch: Partial<StoredFeedbackItem> = { lastModifiedAt: new Date().toISOString() };\n if (status !== item.status) patch.status = status;\n if (type !== item.type) patch.type = type;\n if (scope !== (item.scope ?? '')) (patch as Record<string, unknown>).scope = scope || undefined;\n if (size !== (item.size ?? '')) (patch as Record<string, unknown>).size = size || undefined;\n if (assignedTo !== (item.assignedTo ?? '')) (patch as Record<string, unknown>).assignedTo = assignedTo || undefined;\n const newTags = tags.split(',').map((t) => t.trim()).filter(Boolean);\n if (JSON.stringify(newTags) !== JSON.stringify(item.tags ?? [])) patch.tags = newTags;\n\n if (status === 'resolved' && item.status !== 'resolved') {\n patch.resolvedAt = new Date().toISOString();\n patch.resolvedBy = currentUser?.userId ?? 'unknown';\n }\n\n if (classificationChanged && correctionNote) {\n patch.corrections = [\n ...(item.corrections ?? []),\n {\n field: [type !== item.type && 'type', scope !== (item.scope ?? '') && 'scope', size !== (item.size ?? '') && 'size'].filter(Boolean).join(', '),\n before: { type: item.type, scope: item.scope, size: item.size },\n after: { type, scope: scope || undefined, size: size || undefined },\n correctedBy: currentUser?.userId ?? 'unknown',\n correctedAt: new Date().toISOString(),\n reason: correctionNote,\n },\n ];\n }\n\n await updateItem(feedbackId, patch);\n onNavigate(`/feedback/${feedbackId}`);\n } finally {\n setSaving(false);\n }\n };\n\n const headerActions = (\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>\n Cancel\n </Button>\n );\n\n return (\n <div ref={ref} className={className}>\n <UpdatePage\n title={`Edit: ${item?.content?.slice(0, 40) || feedbackId.slice(0, 12)}…`}\n headerActions={headerActions}\n dataLoading={loading}\n actions={\n <SaveFooter dirty={!!isDirty} saving={saving}>\n <Button variant=\"outline\" onClick={() => onNavigate(`/feedback/${feedbackId}`)}>Cancel</Button>\n <Button\n onClick={handleSave}\n disabled={saving || !isDirty || (!!classificationChanged && !correctionNote)}\n >\n {saving ? 'Saving…' : 'Save changes'}\n </Button>\n </SaveFooter>\n }\n testId=\"feedback-update-page\"\n >\n {item && (\n <div className=\"space-y-6\">\n {/* Read-only context */}\n <div className=\"rounded-lg border border-border p-4 bg-muted/30\">\n <p className=\"text-sm text-muted-foreground mb-2\">Original submission</p>\n <p className=\"whitespace-pre-wrap text-sm\">{item.content}</p>\n <div className=\"flex gap-3 mt-3 text-xs text-muted-foreground\">\n <span>{item.submittedAt ? new Date(item.submittedAt).toLocaleString() : ''}</span>\n <span>{item.environment}</span>\n <span>{item.reporterCount ?? 1} reporter(s)</span>\n </div>\n </div>\n\n {/* Editable fields */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <EnumField\n label=\"Status\"\n options={statusOptions}\n value={status}\n onChange={(v) => setStatus(v as FeedbackStatus)}\n />\n <EnumField\n label=\"Type\"\n options={TYPE_OPTIONS}\n value={type}\n onChange={(v) => setType(v as FeedbackType)}\n />\n <EnumField\n label=\"Scope\"\n options={SCOPE_OPTIONS}\n value={scope}\n onChange={setScope}\n />\n <EnumField\n label=\"Size\"\n options={SIZE_OPTIONS}\n value={size}\n onChange={setSize}\n />\n <TextField\n label=\"Assigned to\"\n value={assignedTo}\n onChange={(e) => setAssignedTo(e.target.value)}\n placeholder={currentUser?.name ?? currentUser?.userId ?? ''}\n {...(currentUser ? { description: `You: ${currentUser.name ?? currentUser.userId}` } : {})}\n />\n <TextField\n label=\"Tags\"\n value={tags}\n onChange={(e) => setTags(e.target.value)}\n placeholder=\"Comma-separated tags\"\n description=\"Separate multiple tags with commas\"\n />\n </div>\n\n {/* Correction note (required when classification changes) */}\n {classificationChanged && (\n <TextareaField\n label=\"Correction note\"\n value={correctionNote}\n onChange={(e) => setCorrectionNote(e.target.value)}\n required\n {...(!correctionNote ? { error: 'Required when changing type, scope, or size' } : {})}\n placeholder=\"Explain why this classification is being changed...\"\n />\n )}\n\n {/* Existing tags display */}\n {item.tags && item.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {item.tags.map((tag) => <Badge key={tag} variant=\"outline\">{tag}</Badge>)}\n </div>\n )}\n </div>\n )}\n </UpdatePage>\n </div>\n );\n },\n);\nFeedbackUpdatePage.displayName = 'FeedbackUpdatePage';\n\nexport { FeedbackUpdatePage };\n"],"names":[],"mappings":"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"}
@@ -30,6 +30,8 @@ export interface FeedbackListPageProps extends AdminPageProps {
30
30
  export interface FeedbackDetailPageProps extends AdminPageProps {
31
31
  feedbackId: string;
32
32
  fetchItem: (id: string) => Promise<StoredFeedbackItem>;
33
+ /** Optional callback to get a displayable URL for the screenshot (e.g. presigned S3 URL) */
34
+ getScreenshotUrl?: (s3Key: string) => Promise<string>;
33
35
  }
34
36
  /**
35
37
  * Props for the FeedbackUpdatePage component.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/react/admin/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/react/admin/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACvD;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E"}
package/dist/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import React__default, { createContext, lazy, useMemo, useReducer, useEffect, useCallback, Suspense, useContext, useState, useRef } from "react";
3
- import { v as validateConfig, r as resolveConfig, d as detectLocale, t } from "./chunks/i18n-Du-or3T4.js";
3
+ import { v as validateConfig, r as resolveConfig, d as detectLocale, t } from "./chunks/i18n-Cba0xHU-.js";
4
4
  import { c } from "./chunks/submitResolver-DsfkrUHY.js";
5
5
  const FeedbackContext = createContext(null);
6
6
  FeedbackContext.displayName = "FeedbackContext";
@@ -975,9 +975,9 @@ function FeedbackConfirm({ className }) {
975
975
  "aria-label": "Submit feedback",
976
976
  noValidate: true,
977
977
  children: [
978
- /* @__PURE__ */ jsx("h2", { id: "feedback-confirm-title", className: "feedback-confirm__title", children: "Review your feedback" }),
978
+ /* @__PURE__ */ jsx("h2", { id: "feedback-confirm-title", className: "feedback-confirm__title", children: t("feedback.confirm.heading", locale, customTranslations) }),
979
979
  duplicates.length > 0 && /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__duplicate-warning", role: "alert", children: [
980
- /* @__PURE__ */ jsx("strong", { children: "Similar feedback already exists:" }),
980
+ /* @__PURE__ */ jsx("strong", { children: t("feedback.confirm.duplicateWarning", locale, customTranslations) }),
981
981
  /* @__PURE__ */ jsx("ul", { children: duplicates.map((d) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: `#${d.id}`, className: "feedback-confirm__duplicate-link", children: [
982
982
  d.title,
983
983
  " (",
@@ -987,7 +987,7 @@ function FeedbackConfirm({ className }) {
987
987
  ] }),
988
988
  /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__field", children: [
989
989
  /* @__PURE__ */ jsxs("label", { htmlFor: "feedback-confirm-title-input", className: "feedback-confirm__label", children: [
990
- t("feedback.confirm.titleLabel", locale, customTranslations) || "Title",
990
+ t("feedback.confirm.titleLabel", locale, customTranslations),
991
991
  " ",
992
992
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "feedback-confirm__required", children: "*" }),
993
993
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: "(required)" })
@@ -1016,7 +1016,7 @@ function FeedbackConfirm({ className }) {
1016
1016
  )
1017
1017
  ] }),
1018
1018
  /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__field", children: [
1019
- /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-description", className: "feedback-confirm__label", children: t("feedback.confirm.descriptionLabel", locale, customTranslations) || "Description" }),
1019
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-description", className: "feedback-confirm__label", children: t("feedback.confirm.descriptionLabel", locale, customTranslations) }),
1020
1020
  /* @__PURE__ */ jsx(
1021
1021
  "textarea",
1022
1022
  {
@@ -1030,7 +1030,7 @@ function FeedbackConfirm({ className }) {
1030
1030
  ] }),
1031
1031
  /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__field", children: [
1032
1032
  /* @__PURE__ */ jsxs("label", { htmlFor: "feedback-confirm-type", className: "feedback-confirm__label", children: [
1033
- t("feedback.confirm.typeLabel", locale, customTranslations) || "Type",
1033
+ t("feedback.confirm.typeLabel", locale, customTranslations),
1034
1034
  " ",
1035
1035
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "feedback-confirm__required", children: "*" }),
1036
1036
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: "(required)" })
@@ -1065,7 +1065,7 @@ function FeedbackConfirm({ className }) {
1065
1065
  )
1066
1066
  ] }),
1067
1067
  /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__field", children: [
1068
- /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-scope", className: "feedback-confirm__label", children: t("feedback.confirm.scopeLabel", locale, customTranslations) || "Scope" }),
1068
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-scope", className: "feedback-confirm__label", children: t("feedback.confirm.scopeLabel", locale, customTranslations) }),
1069
1069
  /* @__PURE__ */ jsxs(
1070
1070
  "select",
1071
1071
  {
@@ -1074,16 +1074,16 @@ function FeedbackConfirm({ className }) {
1074
1074
  value: values.scope,
1075
1075
  onChange: (e) => handleChange("scope", e.target.value),
1076
1076
  children: [
1077
- /* @__PURE__ */ jsx("option", { value: "", children: "Select scope (optional)" }),
1078
- /* @__PURE__ */ jsx("option", { value: "isolated", children: "Isolated" }),
1079
- /* @__PURE__ */ jsx("option", { value: "cross-cutting", children: "Cross-cutting" }),
1080
- /* @__PURE__ */ jsx("option", { value: "systemic", children: "Systemic" })
1077
+ /* @__PURE__ */ jsx("option", { value: "", children: t("feedback.confirm.scopePlaceholder", locale, customTranslations) }),
1078
+ /* @__PURE__ */ jsx("option", { value: "isolated", children: t("feedback.confirm.scope.isolated", locale, customTranslations) }),
1079
+ /* @__PURE__ */ jsx("option", { value: "cross-cutting", children: t("feedback.confirm.scope.crossCutting", locale, customTranslations) }),
1080
+ /* @__PURE__ */ jsx("option", { value: "systemic", children: t("feedback.confirm.scope.systemic", locale, customTranslations) })
1081
1081
  ]
1082
1082
  }
1083
1083
  )
1084
1084
  ] }),
1085
1085
  /* @__PURE__ */ jsxs("div", { className: "feedback-confirm__field", children: [
1086
- /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-size", className: "feedback-confirm__label", children: t("feedback.confirm.sizeLabel", locale, customTranslations) || "Size" }),
1086
+ /* @__PURE__ */ jsx("label", { htmlFor: "feedback-confirm-size", className: "feedback-confirm__label", children: t("feedback.confirm.sizeLabel", locale, customTranslations) }),
1087
1087
  /* @__PURE__ */ jsxs(
1088
1088
  "select",
1089
1089
  {
@@ -1092,11 +1092,11 @@ function FeedbackConfirm({ className }) {
1092
1092
  value: values.size,
1093
1093
  onChange: (e) => handleChange("size", e.target.value),
1094
1094
  children: [
1095
- /* @__PURE__ */ jsx("option", { value: "", children: "Select size (optional)" }),
1096
- /* @__PURE__ */ jsx("option", { value: "trivial", children: "Trivial" }),
1097
- /* @__PURE__ */ jsx("option", { value: "small", children: "Small" }),
1098
- /* @__PURE__ */ jsx("option", { value: "medium", children: "Medium" }),
1099
- /* @__PURE__ */ jsx("option", { value: "large", children: "Large" })
1095
+ /* @__PURE__ */ jsx("option", { value: "", children: t("feedback.confirm.sizePlaceholder", locale, customTranslations) }),
1096
+ /* @__PURE__ */ jsx("option", { value: "trivial", children: t("feedback.confirm.size.trivial", locale, customTranslations) }),
1097
+ /* @__PURE__ */ jsx("option", { value: "small", children: t("feedback.confirm.size.small", locale, customTranslations) }),
1098
+ /* @__PURE__ */ jsx("option", { value: "medium", children: t("feedback.confirm.size.medium", locale, customTranslations) }),
1099
+ /* @__PURE__ */ jsx("option", { value: "large", children: t("feedback.confirm.size.large", locale, customTranslations) })
1100
1100
  ]
1101
1101
  }
1102
1102
  )