markupr 2.1.8

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.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,1321 @@
1
+ /**
2
+ * SessionReview - Premium Feedback Review Experience
3
+ *
4
+ * A document-editor-style interface for reviewing and editing feedback before export.
5
+ *
6
+ * Features:
7
+ * - Thumbnail grid with drag-to-reorder
8
+ * - Inline transcript editing
9
+ * - Delete with undo (5 second toast)
10
+ * - Split view: items list (60%) / Markdown preview (40%)
11
+ * - Category/severity tags (clickable to change)
12
+ * - Save/Copy/Open Folder actions
13
+ * - Full keyboard navigation (Up/Down, Delete, Enter)
14
+ */
15
+
16
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
17
+ import type {
18
+ ReviewSession as Session,
19
+ ReviewFeedbackItem as FeedbackItem,
20
+ ReviewFeedbackCategory as FeedbackCategory,
21
+ ReviewFeedbackSeverity as FeedbackSeverity,
22
+ } from '../../shared/types';
23
+ import { useTheme } from '../hooks/useTheme';
24
+
25
+ // ============================================================================
26
+ // Types
27
+ // ============================================================================
28
+
29
+ interface SessionReviewProps {
30
+ session: Session;
31
+ onSave: (session: Session) => void;
32
+ onCopy: () => void;
33
+ onOpenFolder: () => void;
34
+ onClose: () => void;
35
+ }
36
+
37
+ interface DeletedItem {
38
+ item: FeedbackItem;
39
+ index: number;
40
+ timeoutId: NodeJS.Timeout;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Constants
45
+ // ============================================================================
46
+
47
+ const CATEGORIES: FeedbackCategory[] = ['Bug', 'UX Issue', 'Suggestion', 'Performance', 'Question', 'General'];
48
+ const SEVERITIES: FeedbackSeverity[] = ['Critical', 'High', 'Medium', 'Low'];
49
+
50
+ // Category and severity color maps are now created inside components using useTheme()
51
+ // to support dynamic theme switching.
52
+
53
+ const UNDO_DURATION_MS = 5000;
54
+
55
+ // ============================================================================
56
+ // Sub-Components
57
+ // ============================================================================
58
+
59
+ /**
60
+ * FeedbackItemCard - Draggable, editable feedback item
61
+ */
62
+ interface FeedbackItemCardProps {
63
+ item: FeedbackItem;
64
+ index: number;
65
+ isSelected: boolean;
66
+ isEditing: boolean;
67
+ isDragging: boolean;
68
+ dragOverIndex: number | null;
69
+ onSelect: () => void;
70
+ onStartEdit: () => void;
71
+ onSaveEdit: (newText: string) => void;
72
+ onCancelEdit: () => void;
73
+ onDelete: () => void;
74
+ onCategoryChange: (category: FeedbackCategory) => void;
75
+ onSeverityChange: (severity: FeedbackSeverity) => void;
76
+ onDragStart: (e: React.DragEvent, index: number) => void;
77
+ onDragOver: (e: React.DragEvent, index: number) => void;
78
+ onDragEnd: () => void;
79
+ onThumbnailClick: (imagePath: string) => void;
80
+ }
81
+
82
+ const FeedbackItemCard: React.FC<FeedbackItemCardProps> = ({
83
+ item,
84
+ index,
85
+ isSelected,
86
+ isEditing,
87
+ isDragging,
88
+ dragOverIndex,
89
+ onSelect,
90
+ onStartEdit,
91
+ onSaveEdit,
92
+ onCancelEdit,
93
+ onDelete,
94
+ onCategoryChange,
95
+ onSeverityChange,
96
+ onDragStart,
97
+ onDragOver,
98
+ onDragEnd,
99
+ onThumbnailClick,
100
+ }) => {
101
+ const [editText, setEditText] = useState(item.transcription);
102
+ const [showCategoryDropdown, setShowCategoryDropdown] = useState(false);
103
+ const [showSeverityDropdown, setShowSeverityDropdown] = useState(false);
104
+ const [isHovered, setIsHovered] = useState(false);
105
+ const editInputRef = useRef<HTMLTextAreaElement>(null);
106
+ const { colors } = useTheme();
107
+
108
+ const CATEGORY_COLORS = useMemo((): Record<FeedbackCategory, string> => ({
109
+ Bug: colors.status.error,
110
+ 'UX Issue': colors.status.warning,
111
+ Suggestion: colors.accent.default,
112
+ Performance: colors.status.success,
113
+ Question: colors.status.info,
114
+ General: colors.text.tertiary,
115
+ }), [colors]);
116
+
117
+ const SEVERITY_COLORS = useMemo((): Record<FeedbackSeverity, string> => ({
118
+ Critical: colors.status.error,
119
+ High: colors.status.warning,
120
+ Medium: colors.status.warning,
121
+ Low: colors.status.success,
122
+ }), [colors]);
123
+
124
+ useEffect(() => {
125
+ if (isEditing && editInputRef.current) {
126
+ editInputRef.current.focus();
127
+ editInputRef.current.select();
128
+ }
129
+ }, [isEditing]);
130
+
131
+ useEffect(() => {
132
+ setEditText(item.transcription);
133
+ }, [item.transcription]);
134
+
135
+ const handleKeyDown = (e: React.KeyboardEvent) => {
136
+ if (e.key === 'Enter' && !e.shiftKey) {
137
+ e.preventDefault();
138
+ onSaveEdit(editText);
139
+ } else if (e.key === 'Escape') {
140
+ onCancelEdit();
141
+ setEditText(item.transcription);
142
+ }
143
+ };
144
+
145
+ const category = item.category || 'General';
146
+ const severity = item.severity || 'Medium';
147
+ const isDropTarget = dragOverIndex === index && !isDragging;
148
+
149
+ return (
150
+ <div
151
+ draggable={!isEditing}
152
+ onDragStart={(e) => onDragStart(e, index)}
153
+ onDragOver={(e) => onDragOver(e, index)}
154
+ onDragEnd={onDragEnd}
155
+ onClick={onSelect}
156
+ onDoubleClick={onStartEdit}
157
+ onMouseEnter={() => setIsHovered(true)}
158
+ onMouseLeave={() => {
159
+ setIsHovered(false);
160
+ setShowCategoryDropdown(false);
161
+ setShowSeverityDropdown(false);
162
+ }}
163
+ style={{
164
+ ...styles.card,
165
+ backgroundColor: isSelected ? colors.accent.subtle : colors.surface.inset,
166
+ borderColor: isSelected ? colors.accent.muted : colors.border.subtle,
167
+ transform: isDragging ? 'scale(0.98) rotate(1deg)' : isDropTarget ? 'translateY(4px)' : 'none',
168
+ opacity: isDragging ? 0.6 : 1,
169
+ boxShadow: isDropTarget
170
+ ? `0 -2px 0 0 ${colors.accent.default}, 0 8px 16px -4px rgba(0, 0, 0, 0.3)`
171
+ : isSelected
172
+ ? `0 8px 16px -4px ${colors.accent.subtle}`
173
+ : 'none',
174
+ }}
175
+ >
176
+ {/* Drag Handle */}
177
+ <div style={styles.dragHandle}>
178
+ <svg width="12" height="20" viewBox="0 0 12 20" fill="none">
179
+ <circle cx="3" cy="4" r="1.5" fill="currentColor" />
180
+ <circle cx="9" cy="4" r="1.5" fill="currentColor" />
181
+ <circle cx="3" cy="10" r="1.5" fill="currentColor" />
182
+ <circle cx="9" cy="10" r="1.5" fill="currentColor" />
183
+ <circle cx="3" cy="16" r="1.5" fill="currentColor" />
184
+ <circle cx="9" cy="16" r="1.5" fill="currentColor" />
185
+ </svg>
186
+ </div>
187
+
188
+ {/* Content Area */}
189
+ <div style={styles.cardContent}>
190
+ {/* Header Row: ID + Tags */}
191
+ <div style={styles.cardHeader}>
192
+ <span style={styles.itemId}>FB-{(index + 1).toString().padStart(3, '0')}</span>
193
+
194
+ {/* Category Tag */}
195
+ <div style={{ position: 'relative' }}>
196
+ <button
197
+ onClick={(e) => {
198
+ e.stopPropagation();
199
+ setShowCategoryDropdown(!showCategoryDropdown);
200
+ setShowSeverityDropdown(false);
201
+ }}
202
+ style={{
203
+ ...styles.tag,
204
+ backgroundColor: `${CATEGORY_COLORS[category]}20`,
205
+ color: CATEGORY_COLORS[category],
206
+ borderColor: `${CATEGORY_COLORS[category]}40`,
207
+ }}
208
+ >
209
+ {category}
210
+ </button>
211
+ {showCategoryDropdown && (
212
+ <div style={styles.dropdown}>
213
+ {CATEGORIES.map((cat) => (
214
+ <button
215
+ key={cat}
216
+ onClick={(e) => {
217
+ e.stopPropagation();
218
+ onCategoryChange(cat);
219
+ setShowCategoryDropdown(false);
220
+ }}
221
+ style={{
222
+ ...styles.dropdownItem,
223
+ backgroundColor: cat === category ? colors.accent.subtle : 'transparent',
224
+ }}
225
+ >
226
+ <span
227
+ style={{
228
+ width: 8,
229
+ height: 8,
230
+ borderRadius: '50%',
231
+ backgroundColor: CATEGORY_COLORS[cat],
232
+ marginRight: 8,
233
+ }}
234
+ />
235
+ {cat}
236
+ </button>
237
+ ))}
238
+ </div>
239
+ )}
240
+ </div>
241
+
242
+ {/* Severity Tag */}
243
+ <div style={{ position: 'relative' }}>
244
+ <button
245
+ onClick={(e) => {
246
+ e.stopPropagation();
247
+ setShowSeverityDropdown(!showSeverityDropdown);
248
+ setShowCategoryDropdown(false);
249
+ }}
250
+ style={{
251
+ ...styles.tag,
252
+ backgroundColor: `${SEVERITY_COLORS[severity]}20`,
253
+ color: SEVERITY_COLORS[severity],
254
+ borderColor: `${SEVERITY_COLORS[severity]}40`,
255
+ }}
256
+ >
257
+ {severity}
258
+ </button>
259
+ {showSeverityDropdown && (
260
+ <div style={styles.dropdown}>
261
+ {SEVERITIES.map((sev) => (
262
+ <button
263
+ key={sev}
264
+ onClick={(e) => {
265
+ e.stopPropagation();
266
+ onSeverityChange(sev);
267
+ setShowSeverityDropdown(false);
268
+ }}
269
+ style={{
270
+ ...styles.dropdownItem,
271
+ backgroundColor: sev === severity ? colors.accent.subtle : 'transparent',
272
+ }}
273
+ >
274
+ <span
275
+ style={{
276
+ width: 8,
277
+ height: 8,
278
+ borderRadius: '50%',
279
+ backgroundColor: SEVERITY_COLORS[sev],
280
+ marginRight: 8,
281
+ }}
282
+ />
283
+ {sev}
284
+ </button>
285
+ ))}
286
+ </div>
287
+ )}
288
+ </div>
289
+
290
+ {/* Action Buttons (on hover) */}
291
+ {isHovered && !isEditing && (
292
+ <div style={styles.cardActions}>
293
+ <button
294
+ onClick={(e) => {
295
+ e.stopPropagation();
296
+ onStartEdit();
297
+ }}
298
+ style={styles.actionButton}
299
+ title="Edit (Enter)"
300
+ >
301
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
302
+ <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" />
303
+ <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" />
304
+ </svg>
305
+ </button>
306
+ <button
307
+ onClick={(e) => {
308
+ e.stopPropagation();
309
+ onDelete();
310
+ }}
311
+ style={{ ...styles.actionButton, color: 'var(--status-error)' }}
312
+ title="Delete (Del)"
313
+ >
314
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
315
+ <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
316
+ </svg>
317
+ </button>
318
+ </div>
319
+ )}
320
+ </div>
321
+
322
+ {/* Transcription */}
323
+ {isEditing ? (
324
+ <textarea
325
+ ref={editInputRef}
326
+ value={editText}
327
+ onChange={(e) => setEditText(e.target.value)}
328
+ onKeyDown={handleKeyDown}
329
+ onBlur={() => onSaveEdit(editText)}
330
+ style={styles.editTextarea}
331
+ placeholder="Enter feedback text..."
332
+ />
333
+ ) : (
334
+ <p style={styles.transcription}>{item.transcription}</p>
335
+ )}
336
+
337
+ {/* Screenshot Thumbnails */}
338
+ {item.screenshots.length > 0 && (
339
+ <div style={styles.thumbnailRow}>
340
+ {item.screenshots.map((screenshot, ssIndex) => (
341
+ <button
342
+ key={screenshot.id}
343
+ onClick={(e) => {
344
+ e.stopPropagation();
345
+ onThumbnailClick(screenshot.imagePath);
346
+ }}
347
+ style={styles.thumbnail}
348
+ title="Click to view full size"
349
+ >
350
+ {screenshot.base64 ? (
351
+ <img
352
+ src={`data:image/png;base64,${screenshot.base64}`}
353
+ alt={`Screenshot ${ssIndex + 1}`}
354
+ style={styles.thumbnailImage}
355
+ />
356
+ ) : (
357
+ <div style={styles.thumbnailPlaceholder}>
358
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
359
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
360
+ <circle cx="8.5" cy="8.5" r="1.5" />
361
+ <polyline points="21 15 16 10 5 21" />
362
+ </svg>
363
+ </div>
364
+ )}
365
+ </button>
366
+ ))}
367
+ </div>
368
+ )}
369
+ </div>
370
+ </div>
371
+ );
372
+ };
373
+
374
+ /**
375
+ * MarkdownPreview - Live preview of the generated output
376
+ */
377
+ interface MarkdownPreviewProps {
378
+ session: Session;
379
+ projectName?: string;
380
+ }
381
+
382
+ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({ session, projectName = 'Project' }) => {
383
+ const markdown = useMemo(() => {
384
+ const items = session.feedbackItems;
385
+ const duration = session.endTime
386
+ ? formatDuration(session.endTime - session.startTime)
387
+ : 'In Progress';
388
+ const timestamp = new Date(session.endTime || Date.now()).toLocaleString();
389
+
390
+ let content = `# ${projectName} Feedback Report\n`;
391
+ content += `> Generated by markupr on ${timestamp}\n`;
392
+ content += `> Duration: ${duration} | Items: ${items.length}\n\n`;
393
+ content += `---\n\n`;
394
+ content += `## Feedback Items\n\n`;
395
+
396
+ items.forEach((item, index) => {
397
+ const id = `FB-${(index + 1).toString().padStart(3, '0')}`;
398
+ const category = item.category || 'General';
399
+ const severity = item.severity || 'Medium';
400
+
401
+ content += `### ${id}: ${item.transcription.slice(0, 50)}${item.transcription.length > 50 ? '...' : ''}\n`;
402
+ content += `**Type:** ${category} | **Severity:** ${severity}\n\n`;
403
+ content += `> ${item.transcription}\n\n`;
404
+
405
+ if (item.screenshots.length > 0) {
406
+ content += `*${item.screenshots.length} screenshot(s) attached*\n\n`;
407
+ }
408
+
409
+ content += `---\n\n`;
410
+ });
411
+
412
+ return content;
413
+ }, [session, projectName]);
414
+
415
+ return (
416
+ <div style={styles.previewContainer}>
417
+ <div style={styles.previewHeader}>
418
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
419
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
420
+ <polyline points="14 2 14 8 20 8" />
421
+ <line x1="16" y1="13" x2="8" y2="13" />
422
+ <line x1="16" y1="17" x2="8" y2="17" />
423
+ <polyline points="10 9 9 9 8 9" />
424
+ </svg>
425
+ <span style={styles.previewTitle}>Markdown Preview</span>
426
+ </div>
427
+ <pre style={styles.previewContent}>{markdown}</pre>
428
+ </div>
429
+ );
430
+ };
431
+
432
+ /**
433
+ * DeleteUndoToast - Toast notification with undo action
434
+ */
435
+ interface DeleteUndoToastProps {
436
+ itemId: string;
437
+ onUndo: () => void;
438
+ progress: number;
439
+ }
440
+
441
+ const DeleteUndoToast: React.FC<DeleteUndoToastProps> = ({ itemId, onUndo, progress }) => {
442
+ return (
443
+ <div style={styles.toast}>
444
+ <div style={styles.toastContent}>
445
+ <span style={styles.toastText}>Deleted {itemId}</span>
446
+ <button onClick={onUndo} style={styles.undoButton}>
447
+ Undo
448
+ </button>
449
+ </div>
450
+ <div style={styles.toastProgress}>
451
+ <div
452
+ style={{
453
+ ...styles.toastProgressBar,
454
+ width: `${progress}%`,
455
+ }}
456
+ />
457
+ </div>
458
+ </div>
459
+ );
460
+ };
461
+
462
+ /**
463
+ * ActionToolbar - Save, Copy, Open Folder, Close actions
464
+ */
465
+ interface ActionToolbarProps {
466
+ onSave: () => void;
467
+ onCopy: () => void;
468
+ onOpenFolder: () => void;
469
+ onClose: () => void;
470
+ itemCount: number;
471
+ hasChanges: boolean;
472
+ }
473
+
474
+ const ActionToolbar: React.FC<ActionToolbarProps> = ({
475
+ onSave,
476
+ onCopy,
477
+ onOpenFolder,
478
+ onClose,
479
+ itemCount,
480
+ hasChanges,
481
+ }) => {
482
+ return (
483
+ <div style={styles.toolbar}>
484
+ <div style={styles.toolbarLeft}>
485
+ <span style={styles.itemCount}>{itemCount} items</span>
486
+ {hasChanges && <span style={styles.unsavedBadge}>Unsaved changes</span>}
487
+ </div>
488
+ <div style={styles.toolbarRight}>
489
+ <button onClick={onOpenFolder} style={styles.toolbarButton} title="Open folder">
490
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
491
+ <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
492
+ </svg>
493
+ <span>Open Folder</span>
494
+ </button>
495
+ <button onClick={onCopy} style={styles.toolbarButton} title="Copy to clipboard">
496
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
497
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
498
+ <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
499
+ </svg>
500
+ <span>Copy</span>
501
+ </button>
502
+ <button
503
+ onClick={onSave}
504
+ style={{
505
+ ...styles.toolbarButton,
506
+ ...styles.primaryButton,
507
+ }}
508
+ title="Save changes"
509
+ >
510
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
511
+ <path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z" />
512
+ <polyline points="17 21 17 13 7 13 7 21" />
513
+ <polyline points="7 3 7 8 15 8" />
514
+ </svg>
515
+ <span>Save</span>
516
+ </button>
517
+ <button onClick={onClose} style={styles.closeButton} title="Close">
518
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
519
+ <line x1="18" y1="6" x2="6" y2="18" />
520
+ <line x1="6" y1="6" x2="18" y2="18" />
521
+ </svg>
522
+ </button>
523
+ </div>
524
+ </div>
525
+ );
526
+ };
527
+
528
+ /**
529
+ * ImageLightbox - Full-size image viewer
530
+ */
531
+ interface ImageLightboxProps {
532
+ imagePath: string;
533
+ onClose: () => void;
534
+ }
535
+
536
+ const ImageLightbox: React.FC<ImageLightboxProps> = ({ imagePath, onClose }) => {
537
+ useEffect(() => {
538
+ const handleKeyDown = (e: KeyboardEvent) => {
539
+ if (e.key === 'Escape') {
540
+ onClose();
541
+ }
542
+ };
543
+ document.addEventListener('keydown', handleKeyDown);
544
+ return () => document.removeEventListener('keydown', handleKeyDown);
545
+ }, [onClose]);
546
+
547
+ return (
548
+ <div style={styles.lightboxOverlay} onClick={onClose}>
549
+ <button style={styles.lightboxClose} onClick={onClose}>
550
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
551
+ <line x1="18" y1="6" x2="6" y2="18" />
552
+ <line x1="6" y1="6" x2="18" y2="18" />
553
+ </svg>
554
+ </button>
555
+ <img
556
+ src={imagePath}
557
+ alt="Screenshot"
558
+ style={styles.lightboxImage}
559
+ onClick={(e) => e.stopPropagation()}
560
+ />
561
+ </div>
562
+ );
563
+ };
564
+
565
+ // ============================================================================
566
+ // Helper Functions
567
+ // ============================================================================
568
+
569
+ function formatDuration(ms: number): string {
570
+ const totalSeconds = Math.floor(ms / 1000);
571
+ const mins = Math.floor(totalSeconds / 60);
572
+ const secs = totalSeconds % 60;
573
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
574
+ }
575
+
576
+ // ============================================================================
577
+ // Main Component
578
+ // ============================================================================
579
+
580
+ const SessionReview: React.FC<SessionReviewProps> = ({
581
+ session,
582
+ onSave,
583
+ onCopy,
584
+ onOpenFolder,
585
+ onClose,
586
+ }) => {
587
+ // State
588
+ const [items, setItems] = useState<FeedbackItem[]>(session.feedbackItems);
589
+ const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
590
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
591
+ const [deletedItems, setDeletedItems] = useState<DeletedItem[]>([]);
592
+ const [dragIndex, setDragIndex] = useState<number | null>(null);
593
+ const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
594
+ const [lightboxImage, setLightboxImage] = useState<string | null>(null);
595
+ const [hasChanges, setHasChanges] = useState(false);
596
+ const [undoProgress, setUndoProgress] = useState(100);
597
+
598
+ const containerRef = useRef<HTMLDivElement>(null);
599
+
600
+ // Sync items back to session
601
+ const currentSession = useMemo(
602
+ (): Session => ({
603
+ ...session,
604
+ feedbackItems: items,
605
+ }),
606
+ [session, items]
607
+ );
608
+
609
+ // Undo progress timer
610
+ useEffect(() => {
611
+ if (deletedItems.length === 0) {
612
+ setUndoProgress(100);
613
+ return;
614
+ }
615
+
616
+ const interval = setInterval(() => {
617
+ setUndoProgress((prev) => Math.max(0, prev - 2));
618
+ }, UNDO_DURATION_MS / 50);
619
+
620
+ return () => clearInterval(interval);
621
+ }, [deletedItems]);
622
+
623
+ // Handlers
624
+ const handleDelete = useCallback(
625
+ (index: number) => {
626
+ const itemToDelete = items[index];
627
+ const timeoutId = setTimeout(() => {
628
+ setDeletedItems((prev) => prev.filter((d) => d.item.id !== itemToDelete.id));
629
+ }, UNDO_DURATION_MS);
630
+
631
+ setDeletedItems((prev) => [...prev, { item: itemToDelete, index, timeoutId }]);
632
+ setItems((prev) => prev.filter((_, i) => i !== index));
633
+ setHasChanges(true);
634
+ setUndoProgress(100);
635
+
636
+ if (selectedIndex === index) {
637
+ setSelectedIndex(null);
638
+ } else if (selectedIndex !== null && selectedIndex > index) {
639
+ setSelectedIndex(selectedIndex - 1);
640
+ }
641
+ },
642
+ [items, selectedIndex]
643
+ );
644
+
645
+ // Handle keyboard navigation (must be after handleDelete is defined)
646
+ useEffect(() => {
647
+ const handleKeyDown = (e: KeyboardEvent) => {
648
+ // Don't handle if editing
649
+ if (editingIndex !== null) return;
650
+
651
+ switch (e.key) {
652
+ case 'ArrowUp':
653
+ e.preventDefault();
654
+ setSelectedIndex((prev) =>
655
+ prev === null ? items.length - 1 : Math.max(0, prev - 1)
656
+ );
657
+ break;
658
+ case 'ArrowDown':
659
+ e.preventDefault();
660
+ setSelectedIndex((prev) =>
661
+ prev === null ? 0 : Math.min(items.length - 1, prev + 1)
662
+ );
663
+ break;
664
+ case 'Delete':
665
+ case 'Backspace':
666
+ if (selectedIndex !== null && !e.metaKey && !e.ctrlKey) {
667
+ e.preventDefault();
668
+ handleDelete(selectedIndex);
669
+ }
670
+ break;
671
+ case 'Enter':
672
+ if (selectedIndex !== null) {
673
+ e.preventDefault();
674
+ setEditingIndex(selectedIndex);
675
+ }
676
+ break;
677
+ case 'Escape':
678
+ setSelectedIndex(null);
679
+ setEditingIndex(null);
680
+ break;
681
+ }
682
+ };
683
+
684
+ window.addEventListener('keydown', handleKeyDown);
685
+ return () => window.removeEventListener('keydown', handleKeyDown);
686
+ }, [items.length, selectedIndex, editingIndex, handleDelete]);
687
+
688
+ const handleUndo = useCallback(
689
+ (deletedItem: DeletedItem) => {
690
+ clearTimeout(deletedItem.timeoutId);
691
+ setDeletedItems((prev) => prev.filter((d) => d.item.id !== deletedItem.item.id));
692
+ setItems((prev) => {
693
+ const newItems = [...prev];
694
+ newItems.splice(deletedItem.index, 0, deletedItem.item);
695
+ return newItems;
696
+ });
697
+ },
698
+ []
699
+ );
700
+
701
+ const handleSaveEdit = useCallback(
702
+ (index: number, newText: string) => {
703
+ setItems((prev) =>
704
+ prev.map((item, i) =>
705
+ i === index ? { ...item, transcription: newText } : item
706
+ )
707
+ );
708
+ setEditingIndex(null);
709
+ setHasChanges(true);
710
+ },
711
+ []
712
+ );
713
+
714
+ const handleCategoryChange = useCallback(
715
+ (index: number, category: FeedbackCategory) => {
716
+ setItems((prev) =>
717
+ prev.map((item, i) => (i === index ? { ...item, category } : item))
718
+ );
719
+ setHasChanges(true);
720
+ },
721
+ []
722
+ );
723
+
724
+ const handleSeverityChange = useCallback(
725
+ (index: number, severity: FeedbackSeverity) => {
726
+ setItems((prev) =>
727
+ prev.map((item, i) => (i === index ? { ...item, severity } : item))
728
+ );
729
+ setHasChanges(true);
730
+ },
731
+ []
732
+ );
733
+
734
+ const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
735
+ e.dataTransfer.effectAllowed = 'move';
736
+ setDragIndex(index);
737
+ }, []);
738
+
739
+ const handleDragOver = useCallback(
740
+ (e: React.DragEvent, index: number) => {
741
+ e.preventDefault();
742
+ e.dataTransfer.dropEffect = 'move';
743
+ if (dragIndex !== null && dragIndex !== index) {
744
+ setDragOverIndex(index);
745
+ }
746
+ },
747
+ [dragIndex]
748
+ );
749
+
750
+ const handleDragEnd = useCallback(() => {
751
+ if (dragIndex !== null && dragOverIndex !== null && dragIndex !== dragOverIndex) {
752
+ setItems((prev) => {
753
+ const newItems = [...prev];
754
+ const [draggedItem] = newItems.splice(dragIndex, 1);
755
+ newItems.splice(dragOverIndex, 0, draggedItem);
756
+ return newItems;
757
+ });
758
+ setHasChanges(true);
759
+ }
760
+ setDragIndex(null);
761
+ setDragOverIndex(null);
762
+ }, [dragIndex, dragOverIndex]);
763
+
764
+ const handleSave = useCallback(() => {
765
+ onSave(currentSession);
766
+ setHasChanges(false);
767
+ }, [currentSession, onSave]);
768
+
769
+ return (
770
+ <div ref={containerRef} style={styles.container}>
771
+ {/* toastSlideIn, pageFadeIn, pulseBorder keyframes provided by animations.css; scrollbar styles below */}
772
+ <style>
773
+ {`
774
+ .markupr-scrollbar::-webkit-scrollbar {
775
+ width: 8px;
776
+ }
777
+
778
+ .markupr-scrollbar::-webkit-scrollbar-track {
779
+ background: rgba(31, 41, 55, 0.3);
780
+ border-radius: 4px;
781
+ }
782
+
783
+ .markupr-scrollbar::-webkit-scrollbar-thumb {
784
+ background: rgba(107, 114, 128, 0.5);
785
+ border-radius: 4px;
786
+ }
787
+
788
+ .markupr-scrollbar::-webkit-scrollbar-thumb:hover {
789
+ background: rgba(107, 114, 128, 0.7);
790
+ }
791
+ `}
792
+ </style>
793
+
794
+ {/* Toolbar */}
795
+ <ActionToolbar
796
+ onSave={handleSave}
797
+ onCopy={onCopy}
798
+ onOpenFolder={onOpenFolder}
799
+ onClose={onClose}
800
+ itemCount={items.length}
801
+ hasChanges={hasChanges}
802
+ />
803
+
804
+ {/* Main Content */}
805
+ <div style={styles.mainContent}>
806
+ {/* Items List (60%) */}
807
+ <div style={styles.itemsPane} className="markupr-scrollbar">
808
+ {items.length === 0 ? (
809
+ <div style={styles.emptyState}>
810
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
811
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
812
+ <polyline points="14 2 14 8 20 8" />
813
+ </svg>
814
+ <p style={styles.emptyText}>No feedback items</p>
815
+ <p style={styles.emptySubtext}>Start a new recording to capture feedback</p>
816
+ </div>
817
+ ) : (
818
+ items.map((item, index) => (
819
+ <div
820
+ key={item.id}
821
+ className="ff-list-item-enter"
822
+ style={{ animationDelay: `${index * 50}ms` }}
823
+ >
824
+ <FeedbackItemCard
825
+ item={item}
826
+ index={index}
827
+ isSelected={selectedIndex === index}
828
+ isEditing={editingIndex === index}
829
+ isDragging={dragIndex === index}
830
+ dragOverIndex={dragOverIndex}
831
+ onSelect={() => setSelectedIndex(index)}
832
+ onStartEdit={() => setEditingIndex(index)}
833
+ onSaveEdit={(newText) => handleSaveEdit(index, newText)}
834
+ onCancelEdit={() => setEditingIndex(null)}
835
+ onDelete={() => handleDelete(index)}
836
+ onCategoryChange={(cat) => handleCategoryChange(index, cat)}
837
+ onSeverityChange={(sev) => handleSeverityChange(index, sev)}
838
+ onDragStart={handleDragStart}
839
+ onDragOver={handleDragOver}
840
+ onDragEnd={handleDragEnd}
841
+ onThumbnailClick={setLightboxImage}
842
+ />
843
+ </div>
844
+ ))
845
+ )}
846
+ </div>
847
+
848
+ {/* Preview Pane (40%) */}
849
+ <div style={styles.previewPane}>
850
+ <MarkdownPreview session={currentSession} projectName={session.metadata?.sourceName} />
851
+ </div>
852
+ </div>
853
+
854
+ {/* Delete Undo Toast */}
855
+ {deletedItems.length > 0 && (
856
+ <div style={styles.toastContainer} className="ff-toast-enter">
857
+ {deletedItems.map((deleted) => (
858
+ <DeleteUndoToast
859
+ key={deleted.item.id}
860
+ itemId={`FB-${(deleted.index + 1).toString().padStart(3, '0')}`}
861
+ onUndo={() => handleUndo(deleted)}
862
+ progress={undoProgress}
863
+ />
864
+ ))}
865
+ </div>
866
+ )}
867
+
868
+ {/* Image Lightbox */}
869
+ {lightboxImage && (
870
+ <div className="ff-dialog-enter">
871
+ <ImageLightbox imagePath={lightboxImage} onClose={() => setLightboxImage(null)} />
872
+ </div>
873
+ )}
874
+
875
+ {/* Keyboard Shortcuts Help */}
876
+ <div style={styles.shortcutsHint}>
877
+ <span style={styles.shortcutKey}>Arrow</span> Navigate
878
+ <span style={styles.shortcutKey}>Enter</span> Edit
879
+ <span style={styles.shortcutKey}>Del</span> Remove
880
+ <span style={styles.shortcutKey}>Drag</span> Reorder
881
+ </div>
882
+ </div>
883
+ );
884
+ };
885
+
886
+ // ============================================================================
887
+ // Styles
888
+ // ============================================================================
889
+
890
+ type ExtendedCSSProperties = React.CSSProperties & {
891
+ WebkitAppRegion?: 'drag' | 'no-drag';
892
+ };
893
+
894
+ const styles: Record<string, ExtendedCSSProperties> = {
895
+ container: {
896
+ width: '100%',
897
+ height: '100%',
898
+ display: 'flex',
899
+ flexDirection: 'column',
900
+ backgroundColor: 'var(--bg-primary)',
901
+ backgroundImage: 'linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-primary) 100%)',
902
+ color: 'var(--text-primary)',
903
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
904
+ position: 'relative',
905
+ overflow: 'hidden',
906
+ },
907
+
908
+ // Toolbar
909
+ toolbar: {
910
+ display: 'flex',
911
+ alignItems: 'center',
912
+ justifyContent: 'space-between',
913
+ padding: '12px 20px',
914
+ borderBottom: '1px solid var(--border-default)',
915
+ backgroundColor: 'var(--surface-glass)',
916
+ backdropFilter: 'blur(12px)',
917
+ WebkitAppRegion: 'drag',
918
+ },
919
+ toolbarLeft: {
920
+ display: 'flex',
921
+ alignItems: 'center',
922
+ gap: 12,
923
+ WebkitAppRegion: 'no-drag',
924
+ },
925
+ toolbarRight: {
926
+ display: 'flex',
927
+ alignItems: 'center',
928
+ gap: 8,
929
+ WebkitAppRegion: 'no-drag',
930
+ },
931
+ itemCount: {
932
+ fontSize: 13,
933
+ color: 'var(--text-secondary)',
934
+ fontWeight: 500,
935
+ },
936
+ unsavedBadge: {
937
+ fontSize: 11,
938
+ color: 'var(--status-warning)',
939
+ backgroundColor: 'var(--status-warning-subtle)',
940
+ padding: '2px 8px',
941
+ borderRadius: 10,
942
+ fontWeight: 500,
943
+ },
944
+ toolbarButton: {
945
+ display: 'flex',
946
+ alignItems: 'center',
947
+ gap: 6,
948
+ padding: '8px 12px',
949
+ backgroundColor: 'var(--surface-inset)',
950
+ border: '1px solid var(--border-strong)',
951
+ borderRadius: 8,
952
+ color: 'var(--text-primary)',
953
+ fontSize: 13,
954
+ fontWeight: 500,
955
+ cursor: 'pointer',
956
+ transition: 'all 0.15s ease',
957
+ },
958
+ primaryButton: {
959
+ backgroundColor: 'var(--accent-default)',
960
+ borderColor: 'var(--accent-muted)',
961
+ },
962
+ closeButton: {
963
+ display: 'flex',
964
+ alignItems: 'center',
965
+ justifyContent: 'center',
966
+ width: 32,
967
+ height: 32,
968
+ padding: 0,
969
+ backgroundColor: 'transparent',
970
+ border: 'none',
971
+ borderRadius: 6,
972
+ color: 'var(--text-secondary)',
973
+ cursor: 'pointer',
974
+ transition: 'all 0.15s ease',
975
+ },
976
+
977
+ // Main Content
978
+ mainContent: {
979
+ display: 'flex',
980
+ flex: 1,
981
+ overflow: 'hidden',
982
+ },
983
+ itemsPane: {
984
+ width: '60%',
985
+ padding: 16,
986
+ overflowY: 'auto',
987
+ display: 'flex',
988
+ flexDirection: 'column',
989
+ gap: 12,
990
+ },
991
+ previewPane: {
992
+ width: '40%',
993
+ borderLeft: '1px solid var(--border-default)',
994
+ backgroundColor: 'var(--surface-glass)',
995
+ overflow: 'hidden',
996
+ },
997
+
998
+ // Card
999
+ card: {
1000
+ display: 'flex',
1001
+ gap: 12,
1002
+ padding: 16,
1003
+ borderRadius: 12,
1004
+ border: '1px solid',
1005
+ cursor: 'pointer',
1006
+ transition: 'all 0.2s ease',
1007
+ position: 'relative',
1008
+ },
1009
+ dragHandle: {
1010
+ display: 'flex',
1011
+ alignItems: 'center',
1012
+ justifyContent: 'center',
1013
+ width: 20,
1014
+ cursor: 'grab',
1015
+ opacity: 0.5,
1016
+ color: 'var(--text-tertiary)',
1017
+ transition: 'opacity 0.15s ease',
1018
+ },
1019
+ cardContent: {
1020
+ flex: 1,
1021
+ display: 'flex',
1022
+ flexDirection: 'column',
1023
+ gap: 10,
1024
+ minWidth: 0,
1025
+ },
1026
+ cardHeader: {
1027
+ display: 'flex',
1028
+ alignItems: 'center',
1029
+ gap: 8,
1030
+ flexWrap: 'wrap',
1031
+ },
1032
+ itemId: {
1033
+ fontSize: 12,
1034
+ fontWeight: 600,
1035
+ color: 'var(--text-tertiary)',
1036
+ fontFamily: 'ui-monospace, SFMono-Regular, monospace',
1037
+ },
1038
+ tag: {
1039
+ display: 'inline-flex',
1040
+ alignItems: 'center',
1041
+ padding: '2px 8px',
1042
+ fontSize: 11,
1043
+ fontWeight: 500,
1044
+ borderRadius: 6,
1045
+ border: '1px solid',
1046
+ cursor: 'pointer',
1047
+ transition: 'all 0.15s ease',
1048
+ backgroundColor: 'transparent',
1049
+ },
1050
+ dropdown: {
1051
+ position: 'absolute',
1052
+ top: '100%',
1053
+ left: 0,
1054
+ marginTop: 4,
1055
+ backgroundColor: 'var(--bg-elevated)',
1056
+ border: '1px solid var(--border-strong)',
1057
+ borderRadius: 8,
1058
+ padding: 4,
1059
+ zIndex: 100,
1060
+ boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.4)',
1061
+ minWidth: 120,
1062
+ },
1063
+ dropdownItem: {
1064
+ display: 'flex',
1065
+ alignItems: 'center',
1066
+ width: '100%',
1067
+ padding: '8px 12px',
1068
+ backgroundColor: 'transparent',
1069
+ border: 'none',
1070
+ borderRadius: 4,
1071
+ color: 'var(--text-primary)',
1072
+ fontSize: 12,
1073
+ cursor: 'pointer',
1074
+ transition: 'background-color 0.15s ease',
1075
+ textAlign: 'left',
1076
+ },
1077
+ cardActions: {
1078
+ marginLeft: 'auto',
1079
+ display: 'flex',
1080
+ gap: 4,
1081
+ },
1082
+ actionButton: {
1083
+ display: 'flex',
1084
+ alignItems: 'center',
1085
+ justifyContent: 'center',
1086
+ width: 28,
1087
+ height: 28,
1088
+ padding: 0,
1089
+ backgroundColor: 'var(--surface-inset)',
1090
+ border: 'none',
1091
+ borderRadius: 6,
1092
+ color: 'var(--text-secondary)',
1093
+ cursor: 'pointer',
1094
+ transition: 'all 0.15s ease',
1095
+ },
1096
+ transcription: {
1097
+ margin: 0,
1098
+ fontSize: 14,
1099
+ lineHeight: 1.6,
1100
+ color: 'var(--text-primary)',
1101
+ wordBreak: 'break-word',
1102
+ },
1103
+ editTextarea: {
1104
+ width: '100%',
1105
+ minHeight: 80,
1106
+ padding: 12,
1107
+ backgroundColor: 'var(--surface-inset)',
1108
+ border: '2px solid var(--border-focus)',
1109
+ borderRadius: 8,
1110
+ color: 'var(--text-primary)',
1111
+ fontSize: 14,
1112
+ lineHeight: 1.6,
1113
+ resize: 'vertical',
1114
+ outline: 'none',
1115
+ fontFamily: 'inherit',
1116
+ animation: 'pulseBorder 1.5s ease-in-out infinite',
1117
+ },
1118
+ thumbnailRow: {
1119
+ display: 'flex',
1120
+ gap: 8,
1121
+ marginTop: 4,
1122
+ flexWrap: 'wrap',
1123
+ },
1124
+ thumbnail: {
1125
+ width: 60,
1126
+ height: 45,
1127
+ borderRadius: 6,
1128
+ overflow: 'hidden',
1129
+ border: '1px solid var(--border-default)',
1130
+ backgroundColor: 'var(--surface-inset)',
1131
+ cursor: 'pointer',
1132
+ transition: 'all 0.15s ease',
1133
+ padding: 0,
1134
+ },
1135
+ thumbnailImage: {
1136
+ width: '100%',
1137
+ height: '100%',
1138
+ objectFit: 'cover',
1139
+ },
1140
+ thumbnailPlaceholder: {
1141
+ width: '100%',
1142
+ height: '100%',
1143
+ display: 'flex',
1144
+ alignItems: 'center',
1145
+ justifyContent: 'center',
1146
+ },
1147
+
1148
+ // Preview
1149
+ previewContainer: {
1150
+ height: '100%',
1151
+ display: 'flex',
1152
+ flexDirection: 'column',
1153
+ },
1154
+ previewHeader: {
1155
+ display: 'flex',
1156
+ alignItems: 'center',
1157
+ gap: 8,
1158
+ padding: '12px 16px',
1159
+ borderBottom: '1px solid var(--border-default)',
1160
+ },
1161
+ previewTitle: {
1162
+ fontSize: 12,
1163
+ fontWeight: 500,
1164
+ color: 'var(--text-secondary)',
1165
+ textTransform: 'uppercase',
1166
+ letterSpacing: '0.05em',
1167
+ },
1168
+ previewContent: {
1169
+ flex: 1,
1170
+ padding: 16,
1171
+ margin: 0,
1172
+ fontSize: 12,
1173
+ lineHeight: 1.6,
1174
+ color: 'var(--text-secondary)',
1175
+ fontFamily: 'ui-monospace, SFMono-Regular, monospace',
1176
+ whiteSpace: 'pre-wrap',
1177
+ overflowY: 'auto',
1178
+ backgroundColor: 'transparent',
1179
+ },
1180
+
1181
+ // Toast
1182
+ toastContainer: {
1183
+ position: 'fixed',
1184
+ bottom: 60,
1185
+ left: '50%',
1186
+ transform: 'translateX(-50%)',
1187
+ display: 'flex',
1188
+ flexDirection: 'column',
1189
+ gap: 8,
1190
+ zIndex: 1000,
1191
+ },
1192
+ toast: {
1193
+ backgroundColor: 'var(--bg-elevated)',
1194
+ border: '1px solid var(--border-strong)',
1195
+ borderRadius: 12,
1196
+ padding: 0,
1197
+ overflow: 'hidden',
1198
+ boxShadow: '0 20px 40px -10px rgba(0, 0, 0, 0.5)',
1199
+ animation: 'toastSlideIn 0.3s ease-out',
1200
+ minWidth: 240,
1201
+ },
1202
+ toastContent: {
1203
+ display: 'flex',
1204
+ alignItems: 'center',
1205
+ justifyContent: 'space-between',
1206
+ padding: '12px 16px',
1207
+ },
1208
+ toastText: {
1209
+ fontSize: 13,
1210
+ color: 'var(--text-primary)',
1211
+ fontWeight: 500,
1212
+ },
1213
+ undoButton: {
1214
+ padding: '6px 12px',
1215
+ backgroundColor: 'transparent',
1216
+ border: '1px solid var(--accent-muted)',
1217
+ borderRadius: 6,
1218
+ color: 'var(--text-link)',
1219
+ fontSize: 12,
1220
+ fontWeight: 600,
1221
+ cursor: 'pointer',
1222
+ transition: 'all 0.15s ease',
1223
+ },
1224
+ toastProgress: {
1225
+ height: 3,
1226
+ backgroundColor: 'var(--surface-inset)',
1227
+ },
1228
+ toastProgressBar: {
1229
+ height: '100%',
1230
+ backgroundColor: 'var(--status-error)',
1231
+ transition: 'width 0.1s linear',
1232
+ },
1233
+
1234
+ // Lightbox
1235
+ lightboxOverlay: {
1236
+ position: 'fixed',
1237
+ inset: 0,
1238
+ backgroundColor: 'rgba(0, 0, 0, 0.9)',
1239
+ display: 'flex',
1240
+ alignItems: 'center',
1241
+ justifyContent: 'center',
1242
+ zIndex: 2000,
1243
+ animation: 'pageFadeIn 0.2s ease-out',
1244
+ cursor: 'zoom-out',
1245
+ },
1246
+ lightboxClose: {
1247
+ position: 'absolute',
1248
+ top: 20,
1249
+ right: 20,
1250
+ width: 40,
1251
+ height: 40,
1252
+ display: 'flex',
1253
+ alignItems: 'center',
1254
+ justifyContent: 'center',
1255
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
1256
+ border: 'none',
1257
+ borderRadius: '50%',
1258
+ color: 'var(--text-inverse)',
1259
+ cursor: 'pointer',
1260
+ transition: 'background-color 0.15s ease',
1261
+ },
1262
+ lightboxImage: {
1263
+ maxWidth: '90vw',
1264
+ maxHeight: '90vh',
1265
+ objectFit: 'contain',
1266
+ borderRadius: 8,
1267
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
1268
+ cursor: 'default',
1269
+ },
1270
+
1271
+ // Empty State
1272
+ emptyState: {
1273
+ flex: 1,
1274
+ display: 'flex',
1275
+ flexDirection: 'column',
1276
+ alignItems: 'center',
1277
+ justifyContent: 'center',
1278
+ padding: 40,
1279
+ textAlign: 'center',
1280
+ },
1281
+ emptyText: {
1282
+ marginTop: 16,
1283
+ fontSize: 16,
1284
+ fontWeight: 500,
1285
+ color: 'var(--text-secondary)',
1286
+ },
1287
+ emptySubtext: {
1288
+ marginTop: 4,
1289
+ fontSize: 13,
1290
+ color: 'var(--text-tertiary)',
1291
+ },
1292
+
1293
+ // Keyboard Shortcuts Hint
1294
+ shortcutsHint: {
1295
+ position: 'absolute',
1296
+ bottom: 12,
1297
+ left: 20,
1298
+ display: 'flex',
1299
+ alignItems: 'center',
1300
+ gap: 12,
1301
+ fontSize: 11,
1302
+ color: 'var(--text-tertiary)',
1303
+ },
1304
+ shortcutKey: {
1305
+ display: 'inline-flex',
1306
+ alignItems: 'center',
1307
+ justifyContent: 'center',
1308
+ minWidth: 20,
1309
+ padding: '2px 6px',
1310
+ marginRight: 4,
1311
+ backgroundColor: 'var(--surface-inset)',
1312
+ border: '1px solid var(--border-strong)',
1313
+ borderRadius: 4,
1314
+ fontSize: 10,
1315
+ fontWeight: 500,
1316
+ color: 'var(--text-secondary)',
1317
+ },
1318
+ };
1319
+
1320
+ export { SessionReview };
1321
+ export type { SessionReviewProps };