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,872 @@
1
+ /**
2
+ * ExportDialog - Premium Export Experience for markupr
3
+ *
4
+ * A beautiful modal dialog for selecting export format and options.
5
+ *
6
+ * Features:
7
+ * - Format cards with descriptions and icons
8
+ * - Live preview panel (Markdown/HTML/JSON)
9
+ * - Theme toggle for HTML/PDF
10
+ * - Include images option
11
+ * - Custom filename input
12
+ * - Export progress indicator
13
+ */
14
+
15
+ import React, { useState, useMemo, useCallback, useEffect } from 'react';
16
+ import type { ReviewSession as Session } from '../../shared/types';
17
+ import { useTheme } from '../hooks/useTheme';
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ type ExportFormat = 'markdown' | 'pdf' | 'html' | 'json';
24
+
25
+ interface ExportDialogProps {
26
+ session: Session;
27
+ isOpen: boolean;
28
+ onClose: () => void;
29
+ onExport: (options: ExportOptions) => Promise<void>;
30
+ defaultProjectName?: string;
31
+ }
32
+
33
+ interface ExportOptions {
34
+ format: ExportFormat;
35
+ projectName: string;
36
+ includeImages: boolean;
37
+ theme: 'dark' | 'light';
38
+ }
39
+
40
+ interface FormatCardData {
41
+ format: ExportFormat;
42
+ name: string;
43
+ description: string;
44
+ icon: React.ReactNode;
45
+ extension: string;
46
+ features: string[];
47
+ }
48
+
49
+ // ============================================================================
50
+ // Icons
51
+ // ============================================================================
52
+
53
+ const MarkdownIcon = () => (
54
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
55
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
56
+ <polyline points="14 2 14 8 20 8" />
57
+ <line x1="9" y1="15" x2="15" y2="15" />
58
+ <line x1="9" y1="11" x2="15" y2="11" />
59
+ </svg>
60
+ );
61
+
62
+ const PdfIcon = () => (
63
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
64
+ <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
65
+ <polyline points="14 2 14 8 20 8" />
66
+ <path d="M9 15v-2a1 1 0 011-1h1a1 1 0 011 1v0a1 1 0 01-1 1h-1" />
67
+ <path d="M14 15v-4h1.5a1.5 1.5 0 010 3H14" />
68
+ </svg>
69
+ );
70
+
71
+ const HtmlIcon = () => (
72
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
73
+ <polyline points="16 18 22 12 16 6" />
74
+ <polyline points="8 6 2 12 8 18" />
75
+ <line x1="12" y1="2" x2="12" y2="22" strokeDasharray="2 2" />
76
+ </svg>
77
+ );
78
+
79
+ const JsonIcon = () => (
80
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
81
+ <path d="M8 3H7a2 2 0 00-2 2v5a2 2 0 01-2 2 2 2 0 012 2v5a2 2 0 002 2h1" />
82
+ <path d="M16 21h1a2 2 0 002-2v-5a2 2 0 012-2 2 2 0 01-2-2V5a2 2 0 00-2-2h-1" />
83
+ </svg>
84
+ );
85
+
86
+ const CloseIcon = () => (
87
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
88
+ <line x1="18" y1="6" x2="6" y2="18" />
89
+ <line x1="6" y1="6" x2="18" y2="18" />
90
+ </svg>
91
+ );
92
+
93
+ const CheckIcon = () => (
94
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
95
+ <polyline points="20 6 9 17 4 12" />
96
+ </svg>
97
+ );
98
+
99
+ const SpinnerIcon = () => (
100
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
101
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
102
+ </svg>
103
+ );
104
+
105
+ // ============================================================================
106
+ // Format Data
107
+ // ============================================================================
108
+
109
+ const FORMAT_DATA: FormatCardData[] = [
110
+ {
111
+ format: 'markdown',
112
+ name: 'Markdown',
113
+ description: 'AI-ready format for Claude, ChatGPT, and other assistants',
114
+ icon: <MarkdownIcon />,
115
+ extension: '.md',
116
+ features: ['Structured headings', 'Image references', 'Summary table'],
117
+ },
118
+ {
119
+ format: 'pdf',
120
+ name: 'PDF',
121
+ description: 'Beautiful document for sharing and printing',
122
+ icon: <PdfIcon />,
123
+ extension: '.pdf',
124
+ features: ['Embedded images', 'Print-ready', 'Professional layout'],
125
+ },
126
+ {
127
+ format: 'html',
128
+ name: 'HTML',
129
+ description: 'Standalone web page with no dependencies',
130
+ icon: <HtmlIcon />,
131
+ extension: '.html',
132
+ features: ['Self-contained', 'Dark/Light themes', 'Mobile responsive'],
133
+ },
134
+ {
135
+ format: 'json',
136
+ name: 'JSON',
137
+ description: 'Machine-readable for integrations and APIs',
138
+ icon: <JsonIcon />,
139
+ extension: '.json',
140
+ features: ['Structured data', 'API-friendly', 'Full metadata'],
141
+ },
142
+ ];
143
+
144
+ // ============================================================================
145
+ // Sub-Components
146
+ // ============================================================================
147
+
148
+ interface FormatCardProps {
149
+ data: FormatCardData;
150
+ isSelected: boolean;
151
+ onSelect: () => void;
152
+ }
153
+
154
+ const FormatCard: React.FC<FormatCardProps> = ({ data, isSelected, onSelect }) => {
155
+ const { colors } = useTheme();
156
+ return (
157
+ <button
158
+ onClick={onSelect}
159
+ style={{
160
+ ...styles.formatCard,
161
+ borderColor: isSelected ? `${colors.accent.default}b3` : 'rgba(51, 65, 85, 0.5)',
162
+ backgroundColor: isSelected ? `${colors.accent.default}1a` : 'rgba(31, 41, 55, 0.5)',
163
+ boxShadow: isSelected ? `0 0 0 2px ${colors.accent.default}4d` : 'none',
164
+ }}
165
+ >
166
+ <div style={styles.formatIcon}>{data.icon}</div>
167
+ <div style={styles.formatInfo}>
168
+ <div style={styles.formatHeader}>
169
+ <span style={styles.formatName}>{data.name}</span>
170
+ <span style={styles.formatExtension}>{data.extension}</span>
171
+ </div>
172
+ <p style={styles.formatDescription}>{data.description}</p>
173
+ <div style={styles.formatFeatures}>
174
+ {data.features.map((feature) => (
175
+ <span key={feature} style={styles.featureTag}>
176
+ {feature}
177
+ </span>
178
+ ))}
179
+ </div>
180
+ </div>
181
+ {isSelected && (
182
+ <div style={styles.selectedBadge}>
183
+ <CheckIcon />
184
+ </div>
185
+ )}
186
+ </button>
187
+ );
188
+ };
189
+
190
+ interface PreviewPanelProps {
191
+ session: Session;
192
+ format: ExportFormat;
193
+ projectName: string;
194
+ }
195
+
196
+ const PreviewPanel: React.FC<PreviewPanelProps> = ({ session, format, projectName }) => {
197
+ const preview = useMemo(() => {
198
+ const items = session.feedbackItems.slice(0, 3); // Show first 3 items
199
+
200
+ switch (format) {
201
+ case 'markdown': {
202
+ let md = `# ${projectName} Feedback Report\n\n`;
203
+ md += `## Feedback Items\n\n`;
204
+ items.forEach((item, i) => {
205
+ md += `### FB-${(i + 1).toString().padStart(3, '0')}: ${item.transcription.slice(0, 40)}...\n`;
206
+ md += `**Type:** ${item.category || 'General'}\n\n`;
207
+ md += `> ${item.transcription.slice(0, 100)}...\n\n`;
208
+ });
209
+ if (session.feedbackItems.length > 3) {
210
+ md += `\n*...and ${session.feedbackItems.length - 3} more items*`;
211
+ }
212
+ return md;
213
+ }
214
+
215
+ case 'html':
216
+ return `<!DOCTYPE html>
217
+ <html>
218
+ <head>
219
+ <title>${projectName} - Feedback</title>
220
+ </head>
221
+ <body>
222
+ <h1>${projectName} Feedback Report</h1>
223
+ <p>${session.feedbackItems.length} items</p>
224
+ <!-- Full content in exported file -->
225
+ </body>
226
+ </html>`;
227
+
228
+ case 'json':
229
+ return JSON.stringify(
230
+ {
231
+ version: '1.0',
232
+ session: {
233
+ id: session.id,
234
+ items: items.map((item, i) => ({
235
+ id: `FB-${(i + 1).toString().padStart(3, '0')}`,
236
+ transcription: item.transcription.slice(0, 50) + '...',
237
+ category: item.category,
238
+ })),
239
+ },
240
+ '...': session.feedbackItems.length > 3 ? `${session.feedbackItems.length - 3} more items` : undefined,
241
+ },
242
+ null,
243
+ 2
244
+ );
245
+
246
+ case 'pdf':
247
+ return `[PDF Preview]
248
+
249
+ ${projectName} Feedback Report
250
+ ${'='.repeat(40)}
251
+
252
+ This will generate a beautifully formatted
253
+ PDF document with:
254
+
255
+ - Embedded screenshots
256
+ - Professional typography
257
+ - Print-ready layout
258
+ - ${session.feedbackItems.length} feedback items
259
+
260
+ Export to see the full PDF.`;
261
+
262
+ default:
263
+ return '';
264
+ }
265
+ }, [session, format, projectName]);
266
+
267
+ return (
268
+ <div style={styles.previewPanel}>
269
+ <div style={styles.previewHeader}>
270
+ <span style={styles.previewTitle}>Preview</span>
271
+ <span style={styles.previewFormat}>{format.toUpperCase()}</span>
272
+ </div>
273
+ <pre style={styles.previewContent}>{preview}</pre>
274
+ </div>
275
+ );
276
+ };
277
+
278
+ // ============================================================================
279
+ // Main Component
280
+ // ============================================================================
281
+
282
+ const ExportDialog: React.FC<ExportDialogProps> = ({
283
+ session,
284
+ isOpen,
285
+ onClose,
286
+ onExport,
287
+ defaultProjectName,
288
+ }) => {
289
+ const { colors } = useTheme();
290
+ const [format, setFormat] = useState<ExportFormat>('markdown');
291
+ const [projectName, setProjectName] = useState(
292
+ defaultProjectName || session.metadata?.sourceName || 'Feedback Report'
293
+ );
294
+ const [includeImages, setIncludeImages] = useState(true);
295
+ const [theme, setTheme] = useState<'dark' | 'light'>('dark');
296
+ const [isExporting, setIsExporting] = useState(false);
297
+ const [exportSuccess, setExportSuccess] = useState(false);
298
+
299
+ // Reset state when dialog opens
300
+ useEffect(() => {
301
+ if (isOpen) {
302
+ setExportSuccess(false);
303
+ setIsExporting(false);
304
+ }
305
+ }, [isOpen]);
306
+
307
+ // Handle escape key
308
+ useEffect(() => {
309
+ const handleKeyDown = (e: KeyboardEvent) => {
310
+ if (e.key === 'Escape' && isOpen && !isExporting) {
311
+ onClose();
312
+ }
313
+ };
314
+ window.addEventListener('keydown', handleKeyDown);
315
+ return () => window.removeEventListener('keydown', handleKeyDown);
316
+ }, [isOpen, isExporting, onClose]);
317
+
318
+ const handleExport = useCallback(async () => {
319
+ setIsExporting(true);
320
+ try {
321
+ await onExport({
322
+ format,
323
+ projectName,
324
+ includeImages,
325
+ theme,
326
+ });
327
+ setExportSuccess(true);
328
+ setTimeout(() => {
329
+ onClose();
330
+ }, 1500);
331
+ } catch (error) {
332
+ console.error('Export failed:', error);
333
+ } finally {
334
+ setIsExporting(false);
335
+ }
336
+ }, [format, projectName, includeImages, theme, onExport, onClose]);
337
+
338
+ if (!isOpen) return null;
339
+
340
+ const selectedFormat = FORMAT_DATA.find((f) => f.format === format)!;
341
+ const showThemeOption = format === 'html' || format === 'pdf';
342
+ const showImagesOption = format !== 'json';
343
+
344
+ return (
345
+ <div style={styles.overlay} onClick={onClose}>
346
+ {/* dialogEnter, spin, successPop keyframes provided by animations.css */}
347
+
348
+ <div
349
+ style={styles.dialog}
350
+ onClick={(e) => e.stopPropagation()}
351
+ >
352
+ {/* Header */}
353
+ <div style={styles.header}>
354
+ <h2 style={styles.title}>Export Feedback</h2>
355
+ <button onClick={onClose} style={styles.closeButton} disabled={isExporting}>
356
+ <CloseIcon />
357
+ </button>
358
+ </div>
359
+
360
+ {/* Content */}
361
+ <div style={styles.content}>
362
+ {/* Left: Format Selection */}
363
+ <div style={styles.leftPane}>
364
+ <div style={styles.sectionTitle}>Choose Format</div>
365
+ <div style={styles.formatGrid}>
366
+ {FORMAT_DATA.map((data) => (
367
+ <FormatCard
368
+ key={data.format}
369
+ data={data}
370
+ isSelected={format === data.format}
371
+ onSelect={() => setFormat(data.format)}
372
+ />
373
+ ))}
374
+ </div>
375
+
376
+ {/* Options */}
377
+ <div style={styles.optionsSection}>
378
+ <div style={styles.sectionTitle}>Options</div>
379
+
380
+ {/* Project Name */}
381
+ <div style={styles.optionRow}>
382
+ <label style={styles.optionLabel}>Project Name</label>
383
+ <input
384
+ type="text"
385
+ value={projectName}
386
+ onChange={(e) => setProjectName(e.target.value)}
387
+ style={styles.textInput}
388
+ placeholder="Enter project name..."
389
+ />
390
+ </div>
391
+
392
+ {/* Include Images */}
393
+ {showImagesOption && (
394
+ <div style={styles.optionRow}>
395
+ <label style={styles.optionLabel}>Include Images</label>
396
+ <button
397
+ onClick={() => setIncludeImages(!includeImages)}
398
+ style={{
399
+ ...styles.toggleButton,
400
+ backgroundColor: includeImages
401
+ ? `${colors.accent.default}cc`
402
+ : 'rgba(51, 65, 85, 0.5)',
403
+ }}
404
+ >
405
+ <div
406
+ style={{
407
+ ...styles.toggleKnob,
408
+ transform: includeImages ? 'translateX(16px)' : 'translateX(0)',
409
+ }}
410
+ />
411
+ </button>
412
+ </div>
413
+ )}
414
+
415
+ {/* Theme Toggle */}
416
+ {showThemeOption && (
417
+ <div style={styles.optionRow}>
418
+ <label style={styles.optionLabel}>Theme</label>
419
+ <div style={styles.themeToggle}>
420
+ <button
421
+ onClick={() => setTheme('dark')}
422
+ style={{
423
+ ...styles.themeButton,
424
+ backgroundColor: theme === 'dark' ? `${colors.accent.default}cc` : 'transparent',
425
+ color: theme === 'dark' ? colors.text.inverse : colors.text.secondary,
426
+ }}
427
+ >
428
+ Dark
429
+ </button>
430
+ <button
431
+ onClick={() => setTheme('light')}
432
+ style={{
433
+ ...styles.themeButton,
434
+ backgroundColor: theme === 'light' ? `${colors.accent.default}cc` : 'transparent',
435
+ color: theme === 'light' ? colors.text.inverse : colors.text.secondary,
436
+ }}
437
+ >
438
+ Light
439
+ </button>
440
+ </div>
441
+ </div>
442
+ )}
443
+ </div>
444
+ </div>
445
+
446
+ {/* Right: Preview */}
447
+ <div style={styles.rightPane}>
448
+ <PreviewPanel session={session} format={format} projectName={projectName} />
449
+ </div>
450
+ </div>
451
+
452
+ {/* Footer */}
453
+ <div style={styles.footer}>
454
+ <div style={styles.footerInfo}>
455
+ <span style={styles.footerItemCount}>
456
+ {session.feedbackItems.length} items
457
+ </span>
458
+ <span style={styles.footerDot}>*</span>
459
+ <span style={styles.footerFormat}>
460
+ {selectedFormat.name} ({selectedFormat.extension})
461
+ </span>
462
+ </div>
463
+
464
+ <div style={styles.footerActions}>
465
+ <button onClick={onClose} style={styles.cancelButton} disabled={isExporting}>
466
+ Cancel
467
+ </button>
468
+ <button
469
+ onClick={handleExport}
470
+ style={{
471
+ ...styles.exportButton,
472
+ opacity: isExporting ? 0.7 : 1,
473
+ }}
474
+ disabled={isExporting}
475
+ >
476
+ {isExporting ? (
477
+ <>
478
+ <span style={{ animation: 'spin 1s linear infinite', display: 'inline-flex' }}>
479
+ <SpinnerIcon />
480
+ </span>
481
+ <span>Exporting...</span>
482
+ </>
483
+ ) : exportSuccess ? (
484
+ <>
485
+ <span style={{ animation: 'successPop 0.3s ease-out' }}>
486
+ <CheckIcon />
487
+ </span>
488
+ <span>Exported!</span>
489
+ </>
490
+ ) : (
491
+ <>
492
+ <span>Export as {selectedFormat.name}</span>
493
+ </>
494
+ )}
495
+ </button>
496
+ </div>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ );
501
+ };
502
+
503
+ // ============================================================================
504
+ // Styles
505
+ // ============================================================================
506
+
507
+ const styles: Record<string, React.CSSProperties> = {
508
+ overlay: {
509
+ position: 'fixed',
510
+ inset: 0,
511
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
512
+ display: 'flex',
513
+ alignItems: 'center',
514
+ justifyContent: 'center',
515
+ zIndex: 1000,
516
+ backdropFilter: 'blur(4px)',
517
+ },
518
+
519
+ dialog: {
520
+ width: '90%',
521
+ maxWidth: 900,
522
+ maxHeight: '85vh',
523
+ backgroundColor: 'var(--bg-primary)',
524
+ borderRadius: 16,
525
+ border: '1px solid rgba(51, 65, 85, 0.5)',
526
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
527
+ display: 'flex',
528
+ flexDirection: 'column',
529
+ overflow: 'hidden',
530
+ animation: 'dialogEnter 0.2s ease-out',
531
+ },
532
+
533
+ // Header
534
+ header: {
535
+ display: 'flex',
536
+ alignItems: 'center',
537
+ justifyContent: 'space-between',
538
+ padding: '16px 20px',
539
+ borderBottom: '1px solid rgba(51, 65, 85, 0.5)',
540
+ },
541
+
542
+ title: {
543
+ fontSize: 18,
544
+ fontWeight: 600,
545
+ color: 'var(--text-inverse)',
546
+ margin: 0,
547
+ },
548
+
549
+ closeButton: {
550
+ display: 'flex',
551
+ alignItems: 'center',
552
+ justifyContent: 'center',
553
+ width: 32,
554
+ height: 32,
555
+ padding: 0,
556
+ backgroundColor: 'transparent',
557
+ border: 'none',
558
+ borderRadius: 8,
559
+ color: 'var(--text-secondary)',
560
+ cursor: 'pointer',
561
+ transition: 'all 0.15s ease',
562
+ },
563
+
564
+ // Content
565
+ content: {
566
+ display: 'flex',
567
+ flex: 1,
568
+ overflow: 'hidden',
569
+ },
570
+
571
+ leftPane: {
572
+ width: '55%',
573
+ padding: 20,
574
+ overflowY: 'auto',
575
+ borderRight: '1px solid rgba(51, 65, 85, 0.5)',
576
+ },
577
+
578
+ rightPane: {
579
+ width: '45%',
580
+ padding: 20,
581
+ backgroundColor: 'rgba(15, 23, 42, 0.5)',
582
+ overflow: 'hidden',
583
+ display: 'flex',
584
+ flexDirection: 'column',
585
+ },
586
+
587
+ sectionTitle: {
588
+ fontSize: 12,
589
+ fontWeight: 600,
590
+ color: 'var(--text-tertiary)',
591
+ textTransform: 'uppercase',
592
+ letterSpacing: '0.05em',
593
+ marginBottom: 12,
594
+ },
595
+
596
+ // Format Grid
597
+ formatGrid: {
598
+ display: 'grid',
599
+ gridTemplateColumns: 'repeat(2, 1fr)',
600
+ gap: 12,
601
+ marginBottom: 24,
602
+ },
603
+
604
+ formatCard: {
605
+ display: 'flex',
606
+ alignItems: 'flex-start',
607
+ gap: 12,
608
+ padding: 14,
609
+ borderRadius: 12,
610
+ border: '1px solid',
611
+ cursor: 'pointer',
612
+ textAlign: 'left',
613
+ transition: 'all 0.15s ease',
614
+ position: 'relative',
615
+ },
616
+
617
+ formatIcon: {
618
+ flexShrink: 0,
619
+ width: 40,
620
+ height: 40,
621
+ display: 'flex',
622
+ alignItems: 'center',
623
+ justifyContent: 'center',
624
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
625
+ borderRadius: 10,
626
+ color: 'var(--text-link)',
627
+ },
628
+
629
+ formatInfo: {
630
+ flex: 1,
631
+ minWidth: 0,
632
+ },
633
+
634
+ formatHeader: {
635
+ display: 'flex',
636
+ alignItems: 'center',
637
+ gap: 6,
638
+ marginBottom: 4,
639
+ },
640
+
641
+ formatName: {
642
+ fontSize: 14,
643
+ fontWeight: 600,
644
+ color: 'var(--text-inverse)',
645
+ },
646
+
647
+ formatExtension: {
648
+ fontSize: 11,
649
+ color: 'var(--text-tertiary)',
650
+ fontFamily: 'ui-monospace, monospace',
651
+ },
652
+
653
+ formatDescription: {
654
+ fontSize: 12,
655
+ color: 'var(--text-secondary)',
656
+ margin: 0,
657
+ lineHeight: 1.4,
658
+ marginBottom: 8,
659
+ },
660
+
661
+ formatFeatures: {
662
+ display: 'flex',
663
+ flexWrap: 'wrap',
664
+ gap: 4,
665
+ },
666
+
667
+ featureTag: {
668
+ fontSize: 10,
669
+ color: 'var(--text-tertiary)',
670
+ backgroundColor: 'rgba(51, 65, 85, 0.5)',
671
+ padding: '2px 6px',
672
+ borderRadius: 4,
673
+ },
674
+
675
+ selectedBadge: {
676
+ position: 'absolute',
677
+ top: 8,
678
+ right: 8,
679
+ width: 20,
680
+ height: 20,
681
+ display: 'flex',
682
+ alignItems: 'center',
683
+ justifyContent: 'center',
684
+ backgroundColor: 'var(--accent-default)',
685
+ borderRadius: '50%',
686
+ color: 'var(--text-inverse)',
687
+ },
688
+
689
+ // Options
690
+ optionsSection: {
691
+ marginTop: 8,
692
+ },
693
+
694
+ optionRow: {
695
+ display: 'flex',
696
+ alignItems: 'center',
697
+ justifyContent: 'space-between',
698
+ padding: '12px 0',
699
+ borderBottom: '1px solid rgba(51, 65, 85, 0.3)',
700
+ },
701
+
702
+ optionLabel: {
703
+ fontSize: 13,
704
+ fontWeight: 500,
705
+ color: 'var(--text-primary)',
706
+ },
707
+
708
+ textInput: {
709
+ width: 200,
710
+ padding: '8px 12px',
711
+ backgroundColor: 'rgba(31, 41, 55, 0.5)',
712
+ border: '1px solid rgba(51, 65, 85, 0.5)',
713
+ borderRadius: 8,
714
+ color: 'var(--text-primary)',
715
+ fontSize: 13,
716
+ outline: 'none',
717
+ transition: 'border-color 0.15s ease',
718
+ },
719
+
720
+ toggleButton: {
721
+ width: 44,
722
+ height: 24,
723
+ borderRadius: 12,
724
+ border: 'none',
725
+ cursor: 'pointer',
726
+ position: 'relative',
727
+ transition: 'background-color 0.2s ease',
728
+ padding: 2,
729
+ },
730
+
731
+ toggleKnob: {
732
+ width: 20,
733
+ height: 20,
734
+ backgroundColor: 'var(--text-inverse)',
735
+ borderRadius: '50%',
736
+ transition: 'transform 0.2s ease',
737
+ boxShadow: '0 1px 3px rgba(0, 0, 0, 0.2)',
738
+ },
739
+
740
+ themeToggle: {
741
+ display: 'flex',
742
+ backgroundColor: 'rgba(31, 41, 55, 0.5)',
743
+ borderRadius: 8,
744
+ padding: 2,
745
+ },
746
+
747
+ themeButton: {
748
+ padding: '6px 12px',
749
+ borderRadius: 6,
750
+ border: 'none',
751
+ fontSize: 12,
752
+ fontWeight: 500,
753
+ cursor: 'pointer',
754
+ transition: 'all 0.15s ease',
755
+ },
756
+
757
+ // Preview
758
+ previewPanel: {
759
+ flex: 1,
760
+ display: 'flex',
761
+ flexDirection: 'column',
762
+ backgroundColor: 'rgba(31, 41, 55, 0.3)',
763
+ borderRadius: 12,
764
+ border: '1px solid rgba(51, 65, 85, 0.5)',
765
+ overflow: 'hidden',
766
+ },
767
+
768
+ previewHeader: {
769
+ display: 'flex',
770
+ alignItems: 'center',
771
+ justifyContent: 'space-between',
772
+ padding: '10px 14px',
773
+ borderBottom: '1px solid rgba(51, 65, 85, 0.5)',
774
+ },
775
+
776
+ previewTitle: {
777
+ fontSize: 12,
778
+ fontWeight: 500,
779
+ color: 'var(--text-tertiary)',
780
+ textTransform: 'uppercase',
781
+ letterSpacing: '0.05em',
782
+ },
783
+
784
+ previewFormat: {
785
+ fontSize: 10,
786
+ fontWeight: 600,
787
+ color: 'var(--text-link)',
788
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
789
+ padding: '2px 8px',
790
+ borderRadius: 4,
791
+ },
792
+
793
+ previewContent: {
794
+ flex: 1,
795
+ padding: 14,
796
+ margin: 0,
797
+ fontSize: 11,
798
+ lineHeight: 1.5,
799
+ color: 'var(--text-secondary)',
800
+ fontFamily: 'ui-monospace, SFMono-Regular, monospace',
801
+ whiteSpace: 'pre-wrap',
802
+ overflowY: 'auto',
803
+ overflowX: 'hidden',
804
+ wordBreak: 'break-word',
805
+ },
806
+
807
+ // Footer
808
+ footer: {
809
+ display: 'flex',
810
+ alignItems: 'center',
811
+ justifyContent: 'space-between',
812
+ padding: '14px 20px',
813
+ borderTop: '1px solid rgba(51, 65, 85, 0.5)',
814
+ backgroundColor: 'rgba(15, 23, 42, 0.5)',
815
+ },
816
+
817
+ footerInfo: {
818
+ display: 'flex',
819
+ alignItems: 'center',
820
+ gap: 8,
821
+ fontSize: 12,
822
+ color: 'var(--text-tertiary)',
823
+ },
824
+
825
+ footerItemCount: {
826
+ fontWeight: 500,
827
+ },
828
+
829
+ footerDot: {
830
+ opacity: 0.5,
831
+ },
832
+
833
+ footerFormat: {
834
+ color: 'var(--text-secondary)',
835
+ },
836
+
837
+ footerActions: {
838
+ display: 'flex',
839
+ alignItems: 'center',
840
+ gap: 10,
841
+ },
842
+
843
+ cancelButton: {
844
+ padding: '10px 16px',
845
+ backgroundColor: 'transparent',
846
+ border: '1px solid rgba(51, 65, 85, 0.5)',
847
+ borderRadius: 8,
848
+ color: 'var(--text-secondary)',
849
+ fontSize: 13,
850
+ fontWeight: 500,
851
+ cursor: 'pointer',
852
+ transition: 'all 0.15s ease',
853
+ },
854
+
855
+ exportButton: {
856
+ display: 'flex',
857
+ alignItems: 'center',
858
+ gap: 8,
859
+ padding: '10px 20px',
860
+ backgroundColor: 'var(--accent-default)',
861
+ border: 'none',
862
+ borderRadius: 8,
863
+ color: 'var(--text-inverse)',
864
+ fontSize: 13,
865
+ fontWeight: 600,
866
+ cursor: 'pointer',
867
+ transition: 'all 0.15s ease',
868
+ },
869
+ };
870
+
871
+ export { ExportDialog };
872
+ export type { ExportDialogProps, ExportOptions, ExportFormat };