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,515 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import type { SessionState } from '../shared/types';
3
+ import {
4
+ CrashRecoveryDialog,
5
+ Onboarding,
6
+ SettingsPanel,
7
+ UpdateNotification,
8
+ CountdownTimer,
9
+ CompactAudioIndicator,
10
+ DonateButton,
11
+ KeyboardShortcuts,
12
+ ExportDialog,
13
+ SessionReview,
14
+ RecordingOverlay,
15
+ ProcessingOverlay,
16
+ } from './components';
17
+ import { SessionHistory } from './components/SessionHistory';
18
+ import { ToggleRecordingHint, ManualScreenshotHint, PauseResumeHint } from './components/HotkeyHint';
19
+ import StatusIndicator from './components/StatusIndicator';
20
+ import {
21
+ useRecording,
22
+ useProcessing,
23
+ useUI,
24
+ PROCESSING_BASELINE_PERCENT,
25
+ PROCESSING_DOT_FRAMES,
26
+ formatProcessingStep,
27
+ } from './contexts';
28
+ import './styles/app-shell.css';
29
+
30
+ // ============================================================================
31
+ // Helpers
32
+ // ============================================================================
33
+
34
+ function formatDuration(durationMs: number): string {
35
+ const seconds = Math.floor(durationMs / 1000);
36
+ const mins = Math.floor(seconds / 60);
37
+ const secs = seconds % 60;
38
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
39
+ }
40
+
41
+ function formatRelativeTime(timestamp: number): string {
42
+ const now = Date.now();
43
+ const diff = now - timestamp;
44
+ const minute = 60_000;
45
+ const hour = 60 * minute;
46
+ const day = 24 * hour;
47
+
48
+ if (diff < minute) return 'just now';
49
+ if (diff < hour) return `${Math.floor(diff / minute)}m ago`;
50
+ if (diff < day) return `${Math.floor(diff / hour)}h ago`;
51
+ return `${Math.floor(diff / day)}d ago`;
52
+ }
53
+
54
+ function formatCaptureTrigger(trigger?: 'pause' | 'manual' | 'voice-command'): string {
55
+ switch (trigger) {
56
+ case 'manual':
57
+ return 'Manual Shot Marker';
58
+ case 'voice-command':
59
+ return 'Voice Cue Marker';
60
+ default:
61
+ return 'AI Frame Marker';
62
+ }
63
+ }
64
+
65
+ function mapPopoverState(state: SessionState): 'idle' | 'recording' | 'processing' | 'complete' | 'error' {
66
+ if (state === 'recording' || state === 'starting') return 'recording';
67
+ if (state === 'stopping' || state === 'processing') return 'processing';
68
+ if (state === 'complete') return 'complete';
69
+ if (state === 'error') return 'error';
70
+ return 'idle';
71
+ }
72
+
73
+ // ============================================================================
74
+ // App Component
75
+ // ============================================================================
76
+
77
+ const App: React.FC = () => {
78
+ const recording = useRecording();
79
+ const processing = useProcessing();
80
+ const ui = useUI();
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // HUD mode body class
84
+ // ---------------------------------------------------------------------------
85
+ useEffect(() => {
86
+ const bodyClass = 'markupr-hud-mode';
87
+ const htmlClass = 'markupr-hud-mode';
88
+ document.documentElement.classList.toggle(htmlClass, ui.isHudMode);
89
+ document.body.classList.toggle(bodyClass, ui.isHudMode);
90
+ return () => {
91
+ document.documentElement.classList.remove(htmlClass);
92
+ document.body.classList.remove(bodyClass);
93
+ };
94
+ }, [ui.isHudMode]);
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Primary action (orchestrates countdown + start/stop)
98
+ // ---------------------------------------------------------------------------
99
+ const handlePrimaryAction = useCallback(async () => {
100
+ if (ui.primaryActionDisabled) return;
101
+
102
+ if (recording.state === 'idle' && ui.countdownDuration > 0) {
103
+ ui.setShowCountdown(true);
104
+ return;
105
+ }
106
+
107
+ if (recording.state === 'recording') {
108
+ await recording.stopSession();
109
+ } else {
110
+ await recording.startSession();
111
+ }
112
+ }, [ui.primaryActionDisabled, ui.countdownDuration, ui.setShowCountdown, recording.state, recording.stopSession, recording.startSession]);
113
+
114
+ const handleCountdownComplete = useCallback(async () => {
115
+ ui.setShowCountdown(false);
116
+ await recording.startSession();
117
+ }, [ui.setShowCountdown, recording.startSession]);
118
+
119
+ const handleCountdownSkip = useCallback(() => {
120
+ ui.setShowCountdown(false);
121
+ }, [ui.setShowCountdown]);
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Render
125
+ // ---------------------------------------------------------------------------
126
+ return (
127
+ <div className={`ff-shell ff-shell--${recording.state}${ui.isHudMode ? ' ff-shell--hud' : ''}`}>
128
+ {/* === Global overlays === */}
129
+ <UpdateNotification />
130
+
131
+ {recording.incompleteSession && !recording.isCheckingRecovery && (
132
+ <CrashRecoveryDialog
133
+ session={recording.incompleteSession}
134
+ onRecover={recording.recoverSession}
135
+ onDiscard={recording.discardSession}
136
+ />
137
+ )}
138
+
139
+ {ui.showOnboarding && (
140
+ <Onboarding
141
+ onComplete={ui.handleOnboardingComplete}
142
+ onSkip={ui.handleOnboardingSkip}
143
+ />
144
+ )}
145
+
146
+ {ui.showCountdown && ui.countdownDuration > 0 && (
147
+ <CountdownTimer
148
+ duration={ui.countdownDuration as 3 | 5}
149
+ onComplete={handleCountdownComplete}
150
+ onSkip={handleCountdownSkip}
151
+ />
152
+ )}
153
+
154
+ <SettingsPanel
155
+ isOpen={ui.currentView === 'settings'}
156
+ onClose={ui.closeOverlay}
157
+ />
158
+
159
+ <SessionHistory
160
+ isOpen={ui.currentView === 'history'}
161
+ onClose={ui.closeOverlay}
162
+ onOpenSession={recording.openRecent}
163
+ />
164
+
165
+ <KeyboardShortcuts
166
+ isOpen={ui.currentView === 'shortcuts'}
167
+ onClose={ui.closeOverlay}
168
+ />
169
+
170
+ {ui.showExportDialog && (
171
+ <ExportDialog
172
+ session={{ id: '', startTime: Date.now(), feedbackItems: [] }}
173
+ isOpen={ui.showExportDialog}
174
+ onClose={() => ui.setShowExportDialog(false)}
175
+ onExport={ui.handleExport}
176
+ />
177
+ )}
178
+
179
+ {/* === Main Card === */}
180
+ <main className={`ff-shell__card${ui.isHudMode ? ' ff-shell__card--hud' : ''}`}>
181
+ {ui.showRecordingStatus && (
182
+ <RecordingOverlay
183
+ duration={Math.floor(recording.duration / 1000)}
184
+ screenshotCount={recording.screenshotCount}
185
+ onStop={() => { void handlePrimaryAction(); }}
186
+ audioLevel={recording.audioLevel}
187
+ isVoiceActive={recording.isVoiceActive}
188
+ isPaused={recording.isPaused}
189
+ manualShortcut={ui.settings?.hotkeys?.manualScreenshot}
190
+ toggleShortcut={ui.settings?.hotkeys?.toggleRecording}
191
+ pauseShortcut={ui.settings?.hotkeys?.pauseResume}
192
+ />
193
+ )}
194
+
195
+ {ui.showProcessingProgress && ui.isHudMode && (
196
+ <ProcessingOverlay
197
+ percent={processing.processingProgress?.percent ?? PROCESSING_BASELINE_PERCENT}
198
+ step={processing.processingProgress?.step || formatProcessingStep('preparing')}
199
+ onHide={() => { void window.markupr.window.hide(); }}
200
+ />
201
+ )}
202
+
203
+ {!ui.isHudMode && (
204
+ <>
205
+ <header className="ff-shell__header">
206
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
207
+ <StatusIndicator
208
+ status={mapPopoverState(recording.state)}
209
+ error={recording.errorMessage}
210
+ />
211
+ <div>
212
+ <p className="ff-shell__eyebrow">markupr</p>
213
+ <h1 className="ff-shell__title">{ui.statusCopy.title}</h1>
214
+ </div>
215
+ </div>
216
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
217
+ <button
218
+ className="ff-shell__quiet-btn"
219
+ onClick={() => ui.setCurrentView('settings')}
220
+ type="button"
221
+ aria-label="Open Settings"
222
+ title="Settings"
223
+ style={{ fontSize: 16 }}
224
+ >
225
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
226
+ <path d="M8 10a2 2 0 100-4 2 2 0 000 4z" />
227
+ <path d="M13.178 9.689a1.2 1.2 0 00.24 1.324l.043.044a1.455 1.455 0 11-2.058 2.058l-.044-.044a1.2 1.2 0 00-1.324-.24 1.2 1.2 0 00-.727 1.098v.122a1.455 1.455 0 01-2.91 0v-.065a1.2 1.2 0 00-.785-1.097 1.2 1.2 0 00-1.324.24l-.044.043a1.455 1.455 0 11-2.058-2.058l.044-.044a1.2 1.2 0 00.24-1.324 1.2 1.2 0 00-1.098-.727h-.122a1.455 1.455 0 010-2.91h.065a1.2 1.2 0 001.097-.785 1.2 1.2 0 00-.24-1.324l-.043-.044A1.455 1.455 0 114.187 1.84l.044.044a1.2 1.2 0 001.324.24h.058a1.2 1.2 0 00.727-1.098V.904a1.455 1.455 0 012.91 0v.065a1.2 1.2 0 00.727 1.097 1.2 1.2 0 001.324-.24l.044-.043a1.455 1.455 0 112.058 2.058l-.044.044a1.2 1.2 0 00-.24 1.324v.058a1.2 1.2 0 001.098.727h.122a1.455 1.455 0 010 2.91h-.065a1.2 1.2 0 00-1.097.727z" />
228
+ </svg>
229
+ </button>
230
+ <button
231
+ className="ff-shell__quiet-btn"
232
+ onClick={() => ui.setCurrentView('history')}
233
+ type="button"
234
+ aria-label="Open Session History"
235
+ title="Session History"
236
+ style={{ fontSize: 16 }}
237
+ >
238
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
239
+ <path d="M8 1.455A6.545 6.545 0 1014.545 8" />
240
+ <path d="M8 4v4l2.5 2.5" />
241
+ </svg>
242
+ </button>
243
+ <button
244
+ className="ff-shell__quiet-btn"
245
+ onClick={() => window.markupr.window.hide()}
246
+ type="button"
247
+ >
248
+ Hide
249
+ </button>
250
+ </div>
251
+ </header>
252
+
253
+ <p className="ff-shell__subtitle">{ui.statusCopy.detail}</p>
254
+
255
+ <section className="ff-shell__controls">
256
+ <button
257
+ className={`ff-shell__primary-btn ${recording.state === 'recording' ? 'is-live' : ''}`}
258
+ type="button"
259
+ onClick={handlePrimaryAction}
260
+ disabled={ui.primaryActionDisabled}
261
+ >
262
+ {recording.state === 'processing' || recording.state === 'stopping' ? 'Processing\u2026' : ui.primaryActionLabel}
263
+ </button>
264
+
265
+ <button
266
+ className="ff-shell__secondary-btn"
267
+ type="button"
268
+ onClick={recording.togglePause}
269
+ disabled={ui.pauseActionDisabled}
270
+ >
271
+ {recording.isPaused ? 'Resume Session' : 'Pause Session'} (<PauseResumeHint inline />)
272
+ </button>
273
+
274
+ <button
275
+ className="ff-shell__secondary-btn"
276
+ type="button"
277
+ onClick={recording.manualCapture}
278
+ disabled={ui.manualCaptureDisabled}
279
+ >
280
+ Capture Screenshot (<ManualScreenshotHint inline />)
281
+ </button>
282
+ </section>
283
+
284
+ <section className="ff-shell__meta">
285
+ <span>{formatDuration(recording.duration)}</span>
286
+ <span>{recording.screenshotCount} shots marked</span>
287
+ <span className={recording.hasTranscriptionCapability ? 'is-ready' : 'is-optional'}>
288
+ {recording.hasTranscriptionCapability ? 'Transcript Ready' : 'Add OpenAI Key'}
289
+ </span>
290
+ {recording.lastCapture && (
291
+ <span title={new Date(recording.lastCapture.timestamp).toLocaleString()}>
292
+ {formatCaptureTrigger(recording.lastCapture.trigger)}
293
+ </span>
294
+ )}
295
+ </section>
296
+
297
+ {recording.state === 'idle' && ui.hasRequiredByokKeys === false && (
298
+ <section className="ff-shell__byok-cta">
299
+ <p className="ff-shell__byok-title">BYOK setup required for full reports</p>
300
+ <p className="ff-shell__byok-detail">
301
+ Add your OpenAI and Anthropic API keys in Settings {'>'} Advanced.
302
+ </p>
303
+ <button
304
+ type="button"
305
+ className="ff-shell__byok-btn"
306
+ onClick={() => ui.setCurrentView('settings')}
307
+ >
308
+ Open BYOK Setup
309
+ </button>
310
+ </section>
311
+ )}
312
+
313
+ {ui.showRecordingStatus && (
314
+ <section className="ff-shell__transcript">
315
+ <p className="ff-shell__transcript-label">Recording Active</p>
316
+ <div
317
+ style={{
318
+ display: 'flex',
319
+ alignItems: 'center',
320
+ justifyContent: 'space-between',
321
+ gap: 10,
322
+ padding: '8px 10px',
323
+ borderRadius: 10,
324
+ background: 'rgba(16, 22, 32, 0.72)',
325
+ border: '1px solid rgba(145, 160, 186, 0.26)',
326
+ }}
327
+ >
328
+ <span className="ff-shell__transcript-line">
329
+ {recording.isPaused
330
+ ? 'Session paused'
331
+ : recording.isVoiceActive
332
+ ? 'Mic is active'
333
+ : 'Listening for narration'}
334
+ </span>
335
+ {ui.settings?.showAudioWaveform !== false && (
336
+ <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
337
+ <CompactAudioIndicator
338
+ audioLevel={recording.audioLevel}
339
+ isVoiceActive={recording.isVoiceActive}
340
+ accentColor="#0a84ff"
341
+ inactiveColor="#c7c7cc"
342
+ barCount={7}
343
+ />
344
+ <span
345
+ style={{
346
+ minWidth: 44,
347
+ textAlign: 'right',
348
+ fontSize: 11,
349
+ color: '#98a7c0',
350
+ fontVariantNumeric: 'tabular-nums',
351
+ }}
352
+ >
353
+ {Math.round(Math.max(0, Math.min(1, recording.audioLevel)) * 100)}%
354
+ </span>
355
+ </div>
356
+ )}
357
+ </div>
358
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 8 }}>
359
+ <span className="ff-shell__meta-pill">
360
+ Mark Shot: <ManualScreenshotHint inline />
361
+ </span>
362
+ <span className="ff-shell__meta-pill">
363
+ Stop: <ToggleRecordingHint inline />
364
+ </span>
365
+ <span className="ff-shell__meta-pill">
366
+ Pause: <PauseResumeHint inline />
367
+ </span>
368
+ </div>
369
+ <p className="ff-shell__transcript-placeholder" style={{ marginTop: 8 }}>
370
+ Manual shots are confirmed instantly above. After stop, AI analyzes your transcript + screen frames and assembles an AI-ready report.
371
+ </p>
372
+ </section>
373
+ )}
374
+
375
+ {ui.showProcessingProgress && (
376
+ <section className="ff-shell__processing">
377
+ <p className="ff-shell__processing-label">
378
+ Processing your recording
379
+ <span className="ff-shell__processing-dots" aria-hidden="true">
380
+ {PROCESSING_DOT_FRAMES[processing.processingDotFrame] || PROCESSING_DOT_FRAMES[0]}
381
+ </span>
382
+ </p>
383
+ <div className="ff-shell__processing-bar-track">
384
+ <div
385
+ className="ff-shell__processing-bar-fill"
386
+ style={{ width: `${processing.processingProgress?.percent ?? 0}%` }}
387
+ />
388
+ </div>
389
+ <div className="ff-shell__processing-info">
390
+ <span className="ff-shell__processing-percent">
391
+ {processing.processingProgress?.percent ?? 0}%
392
+ </span>
393
+ <span className="ff-shell__processing-step">
394
+ {processing.processingProgress?.step || 'Preparing...'}
395
+ </span>
396
+ </div>
397
+ </section>
398
+ )}
399
+
400
+ {recording.state === 'complete' && recording.reviewSession && recording.showReviewEditor && (
401
+ <SessionReview
402
+ session={recording.reviewSession}
403
+ onSave={recording.reviewSave}
404
+ onCopy={recording.copyReportPath}
405
+ onOpenFolder={recording.openReportFolder}
406
+ onClose={recording.reviewClose}
407
+ />
408
+ )}
409
+
410
+ {recording.reportPath && (!recording.reviewSession || !recording.showReviewEditor) && (
411
+ <section className="ff-shell__report">
412
+ <p className="ff-shell__report-label">Latest Report Path</p>
413
+ <code className="ff-shell__path">{recording.reportPath}</code>
414
+ <div className="ff-shell__report-actions">
415
+ <button type="button" onClick={recording.copyReportPath}>
416
+ Copy Path
417
+ </button>
418
+ <button type="button" onClick={recording.openReportFolder}>
419
+ Open Folder
420
+ </button>
421
+ {recording.reviewSession && (
422
+ <button type="button" onClick={() => recording.setShowReviewEditor(true)}>
423
+ Open Review Editor
424
+ </button>
425
+ )}
426
+ </div>
427
+ {recording.recordingPath && (
428
+ <>
429
+ <p className="ff-shell__report-label">Session Recording</p>
430
+ <code className="ff-shell__path">{recording.recordingPath}</code>
431
+ <div className="ff-shell__report-actions">
432
+ <button type="button" onClick={recording.copyRecordingPath}>
433
+ Copy Recording Path
434
+ </button>
435
+ <button type="button" onClick={recording.openReportFolder}>
436
+ Open Folder
437
+ </button>
438
+ </div>
439
+ </>
440
+ )}
441
+ {recording.audioPath && (
442
+ <>
443
+ <p className="ff-shell__report-label">Narration Audio</p>
444
+ <code className="ff-shell__path">{recording.audioPath}</code>
445
+ <div className="ff-shell__report-actions">
446
+ <button type="button" onClick={recording.copyAudioPath}>
447
+ Copy Audio Path
448
+ </button>
449
+ <button type="button" onClick={recording.openReportFolder}>
450
+ Open Folder
451
+ </button>
452
+ </div>
453
+ </>
454
+ )}
455
+ </section>
456
+ )}
457
+
458
+ {recording.errorMessage && recording.state === 'error' && (
459
+ <section className="ff-shell__error">
460
+ <p>{recording.errorMessage}</p>
461
+ </section>
462
+ )}
463
+
464
+ <section className="ff-shell__recent">
465
+ <div className="ff-shell__recent-header">
466
+ <h2>Recent Captures</h2>
467
+ <button type="button" onClick={recording.loadRecentSessions}>
468
+ Refresh
469
+ </button>
470
+ </div>
471
+
472
+ {recording.recentSessions.length === 0 ? (
473
+ <p className="ff-shell__empty">No captures yet. Run a session and it will appear here.</p>
474
+ ) : (
475
+ <ul className="ff-shell__recent-list">
476
+ {recording.recentSessions.map((session) => (
477
+ <li key={session.id} className="ff-shell__recent-item">
478
+ <button
479
+ className="ff-shell__recent-open"
480
+ type="button"
481
+ onClick={() => recording.openRecent(session)}
482
+ >
483
+ <span>{session.sourceName || 'Feedback Session'}</span>
484
+ <span>{formatRelativeTime(session.startTime)}</span>
485
+ </button>
486
+ <div className="ff-shell__recent-meta">
487
+ <span>{session.itemCount} items</span>
488
+ <span>{session.screenshotCount} shots</span>
489
+ <button type="button" onClick={() => recording.copyRecentPath(session)}>
490
+ Copy File Path
491
+ </button>
492
+ </div>
493
+ </li>
494
+ ))}
495
+ </ul>
496
+ )}
497
+ </section>
498
+
499
+ <footer className="ff-shell__footer">
500
+ <p>
501
+ Mic activity is monitored live; narration is transcribed after recording completes.
502
+ </p>
503
+ <p>
504
+ Global hotkey: <ToggleRecordingHint inline /> starts or stops the loop.
505
+ </p>
506
+ <DonateButton className="ff-shell__donate" />
507
+ </footer>
508
+ </>
509
+ )}
510
+ </main>
511
+ </div>
512
+ );
513
+ };
514
+
515
+ export default App;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * AppWrapper.tsx
3
+ *
4
+ * Root wrapper that composes all context providers around the App component.
5
+ * Provider order matters: RecordingProvider > ProcessingProvider > UIProvider.
6
+ */
7
+
8
+ import React from 'react';
9
+ import App from './App';
10
+ import { ErrorBoundary } from './components';
11
+ import { ThemeProvider } from './components/ThemeProvider';
12
+ import { RecordingProvider, ProcessingProvider, UIProvider } from './contexts';
13
+
14
+ export const AppWrapper: React.FC = () => (
15
+ <ErrorBoundary>
16
+ <ThemeProvider>
17
+ <RecordingProvider>
18
+ <ProcessingProvider>
19
+ <UIProvider>
20
+ <App />
21
+ </UIProvider>
22
+ </ProcessingProvider>
23
+ </RecordingProvider>
24
+ </ThemeProvider>
25
+ </ErrorBoundary>
26
+ );
27
+
28
+ export default AppWrapper;
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="220" height="48" viewBox="0 0 220 48" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <rect x="4" y="6" width="36" height="36" rx="10" fill="#1D2431" stroke="#3A4352"/>
4
+ <path d="M12 30V17L22 25L32 17V30" stroke="#F8FAFC" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <circle cx="32" cy="30" r="2.25" fill="#F8FAFC"/>
6
+ <text x="52" y="31" font-family="-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',sans-serif" font-size="24" font-weight="650" fill="#F3F4F6">markupr</text>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="220" height="48" viewBox="0 0 220 48" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <rect x="4" y="6" width="36" height="36" rx="10" fill="#F5F6F8" stroke="#D1D5DB"/>
4
+ <path d="M12 30V17L22 25L32 17V30" stroke="#111827" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <circle cx="32" cy="30" r="2.25" fill="#111827"/>
6
+ <text x="52" y="31" font-family="-apple-system,BlinkMacSystemFont,'SF Pro Display','Segoe UI',sans-serif" font-size="24" font-weight="650" fill="#111827">markupr</text>
7
+ </svg>