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,370 @@
1
+ /**
2
+ * Premium Tooltip Component
3
+ *
4
+ * Animated tooltip with multiple placement options and arrow indicators.
5
+ * Uses CSS animations for smooth enter/exit transitions.
6
+ */
7
+
8
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
15
+
16
+ export interface TooltipProps {
17
+ /** Tooltip content */
18
+ content: React.ReactNode;
19
+ /** Trigger element */
20
+ children: React.ReactElement;
21
+ /** Placement relative to trigger */
22
+ placement?: TooltipPlacement;
23
+ /** Delay before showing (ms) */
24
+ showDelay?: number;
25
+ /** Delay before hiding (ms) */
26
+ hideDelay?: number;
27
+ /** Offset from trigger (px) */
28
+ offset?: number;
29
+ /** Disable the tooltip */
30
+ disabled?: boolean;
31
+ /** Show arrow indicator */
32
+ showArrow?: boolean;
33
+ /** Maximum width */
34
+ maxWidth?: number;
35
+ /** Custom z-index */
36
+ zIndex?: number;
37
+ }
38
+
39
+ // ============================================================================
40
+ // Tooltip Component
41
+ // ============================================================================
42
+
43
+ export const Tooltip: React.FC<TooltipProps> = ({
44
+ content,
45
+ children,
46
+ placement = 'top',
47
+ showDelay = 200,
48
+ hideDelay = 0,
49
+ offset = 8,
50
+ disabled = false,
51
+ showArrow = true,
52
+ maxWidth = 240,
53
+ zIndex = 1000,
54
+ }) => {
55
+ const [isVisible, setIsVisible] = useState(false);
56
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false);
57
+ const [position, setPosition] = useState({ top: 0, left: 0 });
58
+
59
+ const triggerRef = useRef<HTMLElement>(null);
60
+ const tooltipRef = useRef<HTMLDivElement>(null);
61
+ const showTimeoutRef = useRef<NodeJS.Timeout>();
62
+ const hideTimeoutRef = useRef<NodeJS.Timeout>();
63
+
64
+ // Calculate tooltip position
65
+ const updatePosition = useCallback(() => {
66
+ if (!triggerRef.current || !tooltipRef.current) return;
67
+
68
+ const triggerRect = triggerRef.current.getBoundingClientRect();
69
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
70
+
71
+ let top = 0;
72
+ let left = 0;
73
+
74
+ switch (placement) {
75
+ case 'top':
76
+ top = triggerRect.top - tooltipRect.height - offset;
77
+ left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
78
+ break;
79
+ case 'bottom':
80
+ top = triggerRect.bottom + offset;
81
+ left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
82
+ break;
83
+ case 'left':
84
+ top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
85
+ left = triggerRect.left - tooltipRect.width - offset;
86
+ break;
87
+ case 'right':
88
+ top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
89
+ left = triggerRect.right + offset;
90
+ break;
91
+ }
92
+
93
+ // Keep tooltip within viewport
94
+ const padding = 8;
95
+ left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
96
+ top = Math.max(padding, Math.min(top, window.innerHeight - tooltipRect.height - padding));
97
+
98
+ setPosition({ top, left });
99
+ }, [placement, offset]);
100
+
101
+ // Show tooltip
102
+ const handleShow = useCallback(() => {
103
+ if (disabled) return;
104
+
105
+ if (hideTimeoutRef.current) {
106
+ clearTimeout(hideTimeoutRef.current);
107
+ }
108
+
109
+ showTimeoutRef.current = setTimeout(() => {
110
+ setIsAnimatingOut(false);
111
+ setIsVisible(true);
112
+ }, showDelay);
113
+ }, [disabled, showDelay]);
114
+
115
+ // Hide tooltip
116
+ const handleHide = useCallback(() => {
117
+ if (showTimeoutRef.current) {
118
+ clearTimeout(showTimeoutRef.current);
119
+ }
120
+
121
+ hideTimeoutRef.current = setTimeout(() => {
122
+ setIsAnimatingOut(true);
123
+ setTimeout(() => {
124
+ setIsVisible(false);
125
+ setIsAnimatingOut(false);
126
+ }, 100); // Match exit animation duration
127
+ }, hideDelay);
128
+ }, [hideDelay]);
129
+
130
+ // Update position when visible
131
+ useEffect(() => {
132
+ if (isVisible) {
133
+ updatePosition();
134
+ // Update on scroll/resize
135
+ window.addEventListener('scroll', updatePosition, true);
136
+ window.addEventListener('resize', updatePosition);
137
+ return () => {
138
+ window.removeEventListener('scroll', updatePosition, true);
139
+ window.removeEventListener('resize', updatePosition);
140
+ };
141
+ }
142
+ }, [isVisible, updatePosition]);
143
+
144
+ // Cleanup timeouts
145
+ useEffect(() => {
146
+ return () => {
147
+ if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
148
+ if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
149
+ };
150
+ }, []);
151
+
152
+ // Get arrow styles based on placement
153
+ const getArrowStyles = (): React.CSSProperties => {
154
+ const base: React.CSSProperties = {
155
+ position: 'absolute',
156
+ width: 8,
157
+ height: 8,
158
+ backgroundColor: 'rgba(17, 24, 39, 0.95)',
159
+ transform: 'rotate(45deg)',
160
+ };
161
+
162
+ switch (placement) {
163
+ case 'top':
164
+ return { ...base, bottom: -4, left: '50%', marginLeft: -4 };
165
+ case 'bottom':
166
+ return { ...base, top: -4, left: '50%', marginLeft: -4 };
167
+ case 'left':
168
+ return { ...base, right: -4, top: '50%', marginTop: -4 };
169
+ case 'right':
170
+ return { ...base, left: -4, top: '50%', marginTop: -4 };
171
+ }
172
+ };
173
+
174
+ // Clone child with event handlers
175
+ const trigger = React.cloneElement(children, {
176
+ ref: triggerRef,
177
+ onMouseEnter: (e: React.MouseEvent) => {
178
+ handleShow();
179
+ children.props.onMouseEnter?.(e);
180
+ },
181
+ onMouseLeave: (e: React.MouseEvent) => {
182
+ handleHide();
183
+ children.props.onMouseLeave?.(e);
184
+ },
185
+ onFocus: (e: React.FocusEvent) => {
186
+ handleShow();
187
+ children.props.onFocus?.(e);
188
+ },
189
+ onBlur: (e: React.FocusEvent) => {
190
+ handleHide();
191
+ children.props.onBlur?.(e);
192
+ },
193
+ });
194
+
195
+ return (
196
+ <>
197
+ {trigger}
198
+ {isVisible && (
199
+ <div
200
+ ref={tooltipRef}
201
+ role="tooltip"
202
+ style={{
203
+ ...styles.tooltip,
204
+ top: position.top,
205
+ left: position.left,
206
+ maxWidth,
207
+ zIndex,
208
+ }}
209
+ className={isAnimatingOut ? 'ff-tooltip-exit' : `ff-tooltip-enter${placement === 'bottom' ? '' : '-top'}`}
210
+ >
211
+ {content}
212
+ {showArrow && <div style={getArrowStyles()} />}
213
+ </div>
214
+ )}
215
+ </>
216
+ );
217
+ };
218
+
219
+ // ============================================================================
220
+ // Hotkey Tooltip (for keyboard shortcuts)
221
+ // ============================================================================
222
+
223
+ export interface HotkeyTooltipProps extends Omit<TooltipProps, 'content'> {
224
+ /** Action description */
225
+ action: string;
226
+ /** Keyboard shortcut keys */
227
+ keys: string[];
228
+ }
229
+
230
+ export const HotkeyTooltip: React.FC<HotkeyTooltipProps> = ({
231
+ action,
232
+ keys,
233
+ children,
234
+ ...props
235
+ }) => {
236
+ const content = (
237
+ <div style={styles.hotkeyContent}>
238
+ <span style={styles.hotkeyAction}>{action}</span>
239
+ <div style={styles.hotkeyKeys}>
240
+ {keys.map((key, index) => (
241
+ <React.Fragment key={key}>
242
+ <kbd style={styles.kbd}>{key}</kbd>
243
+ {index < keys.length - 1 && <span style={styles.keySeparator}>+</span>}
244
+ </React.Fragment>
245
+ ))}
246
+ </div>
247
+ </div>
248
+ );
249
+
250
+ return (
251
+ <Tooltip content={content} {...props}>
252
+ {children}
253
+ </Tooltip>
254
+ );
255
+ };
256
+
257
+ // ============================================================================
258
+ // Status Tooltip (colored based on status)
259
+ // ============================================================================
260
+
261
+ export type TooltipStatus = 'info' | 'success' | 'warning' | 'error';
262
+
263
+ export interface StatusTooltipProps extends Omit<TooltipProps, 'content'> {
264
+ /** Tooltip message */
265
+ message: string;
266
+ /** Status type for color */
267
+ status?: TooltipStatus;
268
+ }
269
+
270
+ const STATUS_COLORS: Record<TooltipStatus, string> = {
271
+ info: 'var(--status-info)',
272
+ success: 'var(--status-success)',
273
+ warning: 'var(--status-warning)',
274
+ error: 'var(--status-error)',
275
+ };
276
+
277
+ export const StatusTooltip: React.FC<StatusTooltipProps> = ({
278
+ message,
279
+ status = 'info',
280
+ children,
281
+ ...props
282
+ }) => {
283
+ const content = (
284
+ <div style={styles.statusContent}>
285
+ <div
286
+ style={{
287
+ ...styles.statusDot,
288
+ backgroundColor: STATUS_COLORS[status],
289
+ }}
290
+ />
291
+ <span>{message}</span>
292
+ </div>
293
+ );
294
+
295
+ return (
296
+ <Tooltip content={content} {...props}>
297
+ {children}
298
+ </Tooltip>
299
+ );
300
+ };
301
+
302
+ // ============================================================================
303
+ // Styles
304
+ // ============================================================================
305
+
306
+ const styles: Record<string, React.CSSProperties> = {
307
+ tooltip: {
308
+ position: 'fixed',
309
+ padding: '8px 12px',
310
+ backgroundColor: 'rgba(17, 24, 39, 0.95)',
311
+ color: 'var(--text-primary)',
312
+ fontSize: 13,
313
+ fontWeight: 500,
314
+ borderRadius: 8,
315
+ boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1)',
316
+ backdropFilter: 'blur(8px)',
317
+ WebkitBackdropFilter: 'blur(8px)',
318
+ pointerEvents: 'none',
319
+ whiteSpace: 'nowrap',
320
+ },
321
+ hotkeyContent: {
322
+ display: 'flex',
323
+ alignItems: 'center',
324
+ gap: 12,
325
+ },
326
+ hotkeyAction: {
327
+ color: 'var(--text-secondary)',
328
+ },
329
+ hotkeyKeys: {
330
+ display: 'flex',
331
+ alignItems: 'center',
332
+ gap: 4,
333
+ },
334
+ kbd: {
335
+ display: 'inline-flex',
336
+ alignItems: 'center',
337
+ justifyContent: 'center',
338
+ minWidth: 20,
339
+ padding: '2px 6px',
340
+ backgroundColor: 'rgba(55, 65, 81, 0.8)',
341
+ borderRadius: 4,
342
+ fontSize: 11,
343
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
344
+ fontWeight: 600,
345
+ color: 'var(--text-primary)',
346
+ border: '1px solid rgba(75, 85, 99, 0.5)',
347
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',
348
+ },
349
+ keySeparator: {
350
+ color: 'var(--text-tertiary)',
351
+ fontSize: 10,
352
+ },
353
+ statusContent: {
354
+ display: 'flex',
355
+ alignItems: 'center',
356
+ gap: 8,
357
+ },
358
+ statusDot: {
359
+ width: 8,
360
+ height: 8,
361
+ borderRadius: '50%',
362
+ flexShrink: 0,
363
+ },
364
+ };
365
+
366
+ // ============================================================================
367
+ // Exports
368
+ // ============================================================================
369
+
370
+ export default Tooltip;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Transcript Results Viewer
3
+ *
4
+ * Displays completed transcript segments after post-processing.
5
+ * This replaces the previous real-time subtitle overlay with a static
6
+ * display of finalized transcript content.
7
+ *
8
+ * Features:
9
+ * - Static display of completed transcript segments
10
+ * - Scrollable container for long transcripts
11
+ * - Position options retained for layout flexibility
12
+ * - Premium typography with optional dark mode
13
+ */
14
+
15
+ import React, { useMemo } from 'react';
16
+ import { useTheme } from '../hooks/useTheme';
17
+
18
+ const MAX_RENDER_SEGMENTS = 100;
19
+
20
+ export interface TranscriptSegment {
21
+ /** Unique identifier for the segment */
22
+ id: string;
23
+ /** The transcribed text */
24
+ text: string;
25
+ /** Start time in milliseconds (relative to session start) */
26
+ startTime: number;
27
+ /** End time in milliseconds (relative to session start) */
28
+ endTime: number;
29
+ /** Transcription confidence (0-1) */
30
+ confidence: number;
31
+ }
32
+
33
+ export interface TranscriptionPreviewProps {
34
+ /** Completed transcript segments to display */
35
+ segments: TranscriptSegment[];
36
+ /** Whether the component is visible */
37
+ isVisible: boolean;
38
+ /** Callback when user toggles visibility */
39
+ onToggle?: () => void;
40
+ /** Whether to use dark mode styling */
41
+ isDarkMode?: boolean;
42
+ /** Maximum height before scrolling (px) */
43
+ maxHeight?: number;
44
+ }
45
+
46
+ function formatTimestamp(ms: number): string {
47
+ const totalSeconds = Math.floor(ms / 1000);
48
+ const minutes = Math.floor(totalSeconds / 60);
49
+ const seconds = totalSeconds % 60;
50
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
51
+ }
52
+
53
+ export const TranscriptionPreview: React.FC<TranscriptionPreviewProps> = ({
54
+ segments,
55
+ isVisible,
56
+ onToggle,
57
+ isDarkMode = false,
58
+ maxHeight = 300,
59
+ }) => {
60
+ const { colors } = useTheme();
61
+ const visibleSegments = useMemo(
62
+ () => (segments.length > MAX_RENDER_SEGMENTS ? segments.slice(-MAX_RENDER_SEGMENTS) : segments),
63
+ [segments]
64
+ );
65
+
66
+ const theme = useMemo(
67
+ () => ({
68
+ bg: isDarkMode ? 'rgba(0, 0, 0, 0.80)' : 'rgba(255, 255, 255, 0.94)',
69
+ text: colors.text.primary,
70
+ textMuted: colors.text.secondary,
71
+ border: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(60, 60, 67, 0.15)',
72
+ timestampBg: isDarkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(120, 120, 128, 0.08)',
73
+ }),
74
+ [isDarkMode]
75
+ );
76
+
77
+ if (!isVisible || segments.length === 0) return null;
78
+
79
+ return (
80
+ <div
81
+ style={{
82
+ borderRadius: 12,
83
+ border: `1px solid ${theme.border}`,
84
+ backgroundColor: theme.bg,
85
+ overflow: 'hidden',
86
+ fontFamily:
87
+ '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
88
+ }}
89
+ role="region"
90
+ aria-label="Transcript results"
91
+ >
92
+ <div
93
+ style={{
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'space-between',
97
+ padding: '10px 14px',
98
+ borderBottom: `1px solid ${theme.border}`,
99
+ }}
100
+ >
101
+ <span
102
+ style={{
103
+ fontSize: 11,
104
+ fontWeight: 600,
105
+ textTransform: 'uppercase',
106
+ letterSpacing: '0.06em',
107
+ color: theme.textMuted,
108
+ }}
109
+ >
110
+ Transcript ({segments.length} segment{segments.length !== 1 ? 's' : ''})
111
+ {segments.length > visibleSegments.length && ` · showing latest ${visibleSegments.length}`}
112
+ </span>
113
+ {onToggle && (
114
+ <button
115
+ type="button"
116
+ onClick={onToggle}
117
+ style={{
118
+ fontSize: 11,
119
+ color: theme.textMuted,
120
+ background: 'none',
121
+ border: 'none',
122
+ cursor: 'pointer',
123
+ padding: '2px 6px',
124
+ }}
125
+ >
126
+ Hide
127
+ </button>
128
+ )}
129
+ </div>
130
+
131
+ <div
132
+ style={{
133
+ maxHeight,
134
+ overflowY: 'auto',
135
+ padding: '8px 14px 12px',
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ gap: 8,
139
+ }}
140
+ >
141
+ {visibleSegments.map((segment) => (
142
+ <div key={segment.id} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
143
+ <span
144
+ style={{
145
+ fontSize: 10,
146
+ fontWeight: 500,
147
+ color: theme.textMuted,
148
+ fontVariantNumeric: 'tabular-nums',
149
+ whiteSpace: 'nowrap',
150
+ padding: '3px 6px',
151
+ borderRadius: 4,
152
+ backgroundColor: theme.timestampBg,
153
+ flexShrink: 0,
154
+ marginTop: 2,
155
+ }}
156
+ >
157
+ {formatTimestamp(segment.startTime)}
158
+ </span>
159
+ <p
160
+ style={{
161
+ fontSize: 13,
162
+ lineHeight: 1.45,
163
+ color: theme.text,
164
+ margin: 0,
165
+ }}
166
+ >
167
+ {segment.text}
168
+ </p>
169
+ </div>
170
+ ))}
171
+ </div>
172
+ </div>
173
+ );
174
+ };
175
+
176
+ /**
177
+ * Legacy alias - TranscriptionPreviewAnimated is no longer used.
178
+ * Kept as a re-export of the static viewer for backwards compatibility
179
+ * with any imports that reference it.
180
+ */
181
+ export const TranscriptionPreviewAnimated = TranscriptionPreview;
182
+
183
+ export default TranscriptionPreview;