markupr 2.1.8 → 2.5.0

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 (300) hide show
  1. package/README.md +292 -15
  2. package/dist/cli/index.mjs +3593 -0
  3. package/dist/main/index.mjs +743 -220
  4. package/dist/mcp/index.mjs +4053 -0
  5. package/package.json +32 -7
  6. package/.claude/commands/review-feedback.md +0 -47
  7. package/.eslintrc.json +0 -35
  8. package/.github/CODEOWNERS +0 -16
  9. package/.github/FUNDING.yml +0 -1
  10. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -56
  11. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -54
  12. package/.github/PULL_REQUEST_TEMPLATE.md +0 -89
  13. package/.github/dependabot.yml +0 -70
  14. package/.github/workflows/ci.yml +0 -184
  15. package/.github/workflows/deploy-landing.yml +0 -134
  16. package/.github/workflows/nightly.yml +0 -288
  17. package/.github/workflows/release.yml +0 -318
  18. package/CHANGELOG.md +0 -127
  19. package/CLAUDE.md +0 -137
  20. package/CODE_OF_CONDUCT.md +0 -9
  21. package/CONTRIBUTING.md +0 -390
  22. package/PRODUCT_VISION.md +0 -277
  23. package/SECURITY.md +0 -51
  24. package/SIGNING_INSTRUCTIONS.md +0 -284
  25. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +0 -130
  26. package/assets/svg-source/dmg-background.svg +0 -70
  27. package/assets/svg-source/icon.svg +0 -20
  28. package/assets/svg-source/tray-icon-processing.svg +0 -7
  29. package/assets/svg-source/tray-icon-recording.svg +0 -7
  30. package/assets/svg-source/tray-icon.svg +0 -6
  31. package/assets/tray-complete.png +0 -0
  32. package/assets/tray-complete@2x.png +0 -0
  33. package/assets/tray-completeTemplate.png +0 -0
  34. package/assets/tray-completeTemplate@2x.png +0 -0
  35. package/assets/tray-error.png +0 -0
  36. package/assets/tray-error@2x.png +0 -0
  37. package/assets/tray-errorTemplate.png +0 -0
  38. package/assets/tray-errorTemplate@2x.png +0 -0
  39. package/assets/tray-icon-processing.png +0 -0
  40. package/assets/tray-icon-processing@2x.png +0 -0
  41. package/assets/tray-icon-processingTemplate.png +0 -0
  42. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  43. package/assets/tray-icon-recording.png +0 -0
  44. package/assets/tray-icon-recording@2x.png +0 -0
  45. package/assets/tray-icon-recordingTemplate.png +0 -0
  46. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  47. package/assets/tray-icon.png +0 -0
  48. package/assets/tray-icon@2x.png +0 -0
  49. package/assets/tray-iconTemplate.png +0 -0
  50. package/assets/tray-iconTemplate@2x.png +0 -0
  51. package/assets/tray-idle.png +0 -0
  52. package/assets/tray-idle@2x.png +0 -0
  53. package/assets/tray-idleTemplate.png +0 -0
  54. package/assets/tray-idleTemplate@2x.png +0 -0
  55. package/assets/tray-processing-0.png +0 -0
  56. package/assets/tray-processing-0@2x.png +0 -0
  57. package/assets/tray-processing-0Template.png +0 -0
  58. package/assets/tray-processing-0Template@2x.png +0 -0
  59. package/assets/tray-processing-1.png +0 -0
  60. package/assets/tray-processing-1@2x.png +0 -0
  61. package/assets/tray-processing-1Template.png +0 -0
  62. package/assets/tray-processing-1Template@2x.png +0 -0
  63. package/assets/tray-processing-2.png +0 -0
  64. package/assets/tray-processing-2@2x.png +0 -0
  65. package/assets/tray-processing-2Template.png +0 -0
  66. package/assets/tray-processing-2Template@2x.png +0 -0
  67. package/assets/tray-processing-3.png +0 -0
  68. package/assets/tray-processing-3@2x.png +0 -0
  69. package/assets/tray-processing-3Template.png +0 -0
  70. package/assets/tray-processing-3Template@2x.png +0 -0
  71. package/assets/tray-processing.png +0 -0
  72. package/assets/tray-processing@2x.png +0 -0
  73. package/assets/tray-processingTemplate.png +0 -0
  74. package/assets/tray-processingTemplate@2x.png +0 -0
  75. package/assets/tray-recording.png +0 -0
  76. package/assets/tray-recording@2x.png +0 -0
  77. package/assets/tray-recordingTemplate.png +0 -0
  78. package/assets/tray-recordingTemplate@2x.png +0 -0
  79. package/build/DMG_BACKGROUND_SPEC.md +0 -50
  80. package/build/dmg-background.png +0 -0
  81. package/build/dmg-background@2x.png +0 -0
  82. package/build/entitlements.mac.inherit.plist +0 -27
  83. package/build/entitlements.mac.plist +0 -41
  84. package/build/favicon-16.png +0 -0
  85. package/build/favicon-180.png +0 -0
  86. package/build/favicon-192.png +0 -0
  87. package/build/favicon-32.png +0 -0
  88. package/build/favicon-48.png +0 -0
  89. package/build/favicon-512.png +0 -0
  90. package/build/favicon-64.png +0 -0
  91. package/build/icon-128.png +0 -0
  92. package/build/icon-16.png +0 -0
  93. package/build/icon-24.png +0 -0
  94. package/build/icon-256.png +0 -0
  95. package/build/icon-32.png +0 -0
  96. package/build/icon-48.png +0 -0
  97. package/build/icon-64.png +0 -0
  98. package/build/icon.icns +0 -0
  99. package/build/icon.ico +0 -0
  100. package/build/icon.iconset/icon_128x128.png +0 -0
  101. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  102. package/build/icon.iconset/icon_16x16.png +0 -0
  103. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  104. package/build/icon.iconset/icon_256x256.png +0 -0
  105. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  106. package/build/icon.iconset/icon_32x32.png +0 -0
  107. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  108. package/build/icon.iconset/icon_512x512.png +0 -0
  109. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  110. package/build/icon.png +0 -0
  111. package/build/installer-header.bmp +0 -0
  112. package/build/installer-header.png +0 -0
  113. package/build/installer-sidebar.bmp +0 -0
  114. package/build/installer-sidebar.png +0 -0
  115. package/build/installer.nsh +0 -45
  116. package/build/overlay-processing.png +0 -0
  117. package/build/overlay-recording.png +0 -0
  118. package/build/toolbar-record.png +0 -0
  119. package/build/toolbar-screenshot.png +0 -0
  120. package/build/toolbar-settings.png +0 -0
  121. package/build/toolbar-stop.png +0 -0
  122. package/dist/preload/index.mjs +0 -907
  123. package/dist/renderer/assets/index-CCmUjl9K.js +0 -19495
  124. package/dist/renderer/assets/index-CUqz_Gs6.css +0 -2270
  125. package/dist/renderer/index.html +0 -27
  126. package/docs/AI_AGENT_QUICKSTART.md +0 -42
  127. package/docs/AI_PIPELINE_DESIGN.md +0 -595
  128. package/docs/API.md +0 -514
  129. package/docs/ARCHITECTURE.md +0 -460
  130. package/docs/CONFIGURATION.md +0 -336
  131. package/docs/DEVELOPMENT.md +0 -508
  132. package/docs/EXPORT_FORMATS.md +0 -451
  133. package/docs/GETTING_STARTED.md +0 -236
  134. package/docs/KEYBOARD_SHORTCUTS.md +0 -334
  135. package/docs/TROUBLESHOOTING.md +0 -418
  136. package/docs/landing/index.html +0 -672
  137. package/docs/landing/script.js +0 -342
  138. package/docs/landing/styles.css +0 -1543
  139. package/electron-builder.yml +0 -140
  140. package/electron.vite.config.ts +0 -63
  141. package/railway.json +0 -12
  142. package/scripts/build.mjs +0 -51
  143. package/scripts/generate-icons.mjs +0 -314
  144. package/scripts/generate-installer-images.cjs +0 -253
  145. package/scripts/generate-tray-icons.mjs +0 -258
  146. package/scripts/notarize.cjs +0 -180
  147. package/scripts/one-click-clean-test.sh +0 -147
  148. package/scripts/postinstall.mjs +0 -36
  149. package/scripts/setup-markupr.sh +0 -55
  150. package/setup +0 -17
  151. package/site/index.html +0 -1835
  152. package/site/package.json +0 -11
  153. package/site/railway.json +0 -12
  154. package/site/server.js +0 -31
  155. package/src/main/AutoUpdater.ts +0 -392
  156. package/src/main/CrashRecovery.ts +0 -655
  157. package/src/main/ErrorHandler.ts +0 -703
  158. package/src/main/HotkeyManager.ts +0 -399
  159. package/src/main/MenuManager.ts +0 -529
  160. package/src/main/PermissionManager.ts +0 -420
  161. package/src/main/SessionController.ts +0 -1465
  162. package/src/main/TrayManager.ts +0 -540
  163. package/src/main/ai/AIPipelineManager.ts +0 -199
  164. package/src/main/ai/ClaudeAnalyzer.ts +0 -339
  165. package/src/main/ai/ImageOptimizer.ts +0 -176
  166. package/src/main/ai/StructuredMarkdownBuilder.ts +0 -379
  167. package/src/main/ai/index.ts +0 -16
  168. package/src/main/ai/types.ts +0 -258
  169. package/src/main/analysis/ClarificationGenerator.ts +0 -385
  170. package/src/main/analysis/FeedbackAnalyzer.ts +0 -531
  171. package/src/main/analysis/index.ts +0 -19
  172. package/src/main/audio/AudioCapture.ts +0 -978
  173. package/src/main/audio/audioUtils.ts +0 -100
  174. package/src/main/audio/index.ts +0 -20
  175. package/src/main/capture/index.ts +0 -1
  176. package/src/main/index.ts +0 -1693
  177. package/src/main/ipc/captureHandlers.ts +0 -272
  178. package/src/main/ipc/index.ts +0 -45
  179. package/src/main/ipc/outputHandlers.ts +0 -302
  180. package/src/main/ipc/sessionHandlers.ts +0 -56
  181. package/src/main/ipc/settingsHandlers.ts +0 -471
  182. package/src/main/ipc/types.ts +0 -56
  183. package/src/main/ipc/windowHandlers.ts +0 -277
  184. package/src/main/output/ClipboardService.ts +0 -369
  185. package/src/main/output/ExportService.ts +0 -539
  186. package/src/main/output/FileManager.ts +0 -416
  187. package/src/main/output/MarkdownGenerator.ts +0 -791
  188. package/src/main/output/MarkdownPatcher.ts +0 -299
  189. package/src/main/output/index.ts +0 -186
  190. package/src/main/output/sessionAdapter.ts +0 -207
  191. package/src/main/output/templates/html-template.ts +0 -553
  192. package/src/main/pipeline/FrameExtractor.ts +0 -330
  193. package/src/main/pipeline/PostProcessor.ts +0 -399
  194. package/src/main/pipeline/TranscriptAnalyzer.ts +0 -226
  195. package/src/main/pipeline/index.ts +0 -36
  196. package/src/main/platform/WindowsTaskbar.ts +0 -600
  197. package/src/main/platform/index.ts +0 -16
  198. package/src/main/settings/SettingsManager.ts +0 -730
  199. package/src/main/settings/index.ts +0 -19
  200. package/src/main/transcription/ModelDownloadManager.ts +0 -494
  201. package/src/main/transcription/TierManager.ts +0 -219
  202. package/src/main/transcription/TranscriptionRecoveryService.ts +0 -340
  203. package/src/main/transcription/WhisperService.ts +0 -748
  204. package/src/main/transcription/index.ts +0 -56
  205. package/src/main/transcription/types.ts +0 -135
  206. package/src/main/windows/PopoverManager.ts +0 -284
  207. package/src/main/windows/TaskbarIntegration.ts +0 -452
  208. package/src/main/windows/index.ts +0 -23
  209. package/src/preload/index.ts +0 -1047
  210. package/src/renderer/App.tsx +0 -515
  211. package/src/renderer/AppWrapper.tsx +0 -28
  212. package/src/renderer/assets/logo-dark.svg +0 -7
  213. package/src/renderer/assets/logo.svg +0 -7
  214. package/src/renderer/audio/AudioCaptureRenderer.ts +0 -454
  215. package/src/renderer/capture/ScreenRecordingRenderer.ts +0 -492
  216. package/src/renderer/components/AnnotationOverlay.tsx +0 -836
  217. package/src/renderer/components/AudioWaveform.tsx +0 -811
  218. package/src/renderer/components/ClarificationQuestions.tsx +0 -656
  219. package/src/renderer/components/CountdownTimer.tsx +0 -495
  220. package/src/renderer/components/CrashRecoveryDialog.tsx +0 -632
  221. package/src/renderer/components/DonateButton.tsx +0 -127
  222. package/src/renderer/components/ErrorBoundary.tsx +0 -308
  223. package/src/renderer/components/ExportDialog.tsx +0 -872
  224. package/src/renderer/components/HotkeyHint.tsx +0 -261
  225. package/src/renderer/components/KeyboardShortcuts.tsx +0 -787
  226. package/src/renderer/components/ModelDownloadDialog.tsx +0 -844
  227. package/src/renderer/components/Onboarding.tsx +0 -1830
  228. package/src/renderer/components/ProcessingOverlay.tsx +0 -157
  229. package/src/renderer/components/RecordingOverlay.tsx +0 -423
  230. package/src/renderer/components/SessionHistory.tsx +0 -1746
  231. package/src/renderer/components/SessionReview.tsx +0 -1321
  232. package/src/renderer/components/SettingsPanel.tsx +0 -217
  233. package/src/renderer/components/Skeleton.tsx +0 -347
  234. package/src/renderer/components/StatusIndicator.tsx +0 -86
  235. package/src/renderer/components/ThemeProvider.tsx +0 -429
  236. package/src/renderer/components/Tooltip.tsx +0 -370
  237. package/src/renderer/components/TranscriptionPreview.tsx +0 -183
  238. package/src/renderer/components/TranscriptionTierSelector.tsx +0 -640
  239. package/src/renderer/components/UpdateNotification.tsx +0 -377
  240. package/src/renderer/components/WindowSelector.tsx +0 -947
  241. package/src/renderer/components/index.ts +0 -99
  242. package/src/renderer/components/primitives/ApiKeyInput.tsx +0 -98
  243. package/src/renderer/components/primitives/ColorPicker.tsx +0 -65
  244. package/src/renderer/components/primitives/DangerButton.tsx +0 -45
  245. package/src/renderer/components/primitives/DirectoryPicker.tsx +0 -41
  246. package/src/renderer/components/primitives/Dropdown.tsx +0 -34
  247. package/src/renderer/components/primitives/KeyRecorder.tsx +0 -117
  248. package/src/renderer/components/primitives/SettingsSection.tsx +0 -32
  249. package/src/renderer/components/primitives/Slider.tsx +0 -43
  250. package/src/renderer/components/primitives/Toggle.tsx +0 -36
  251. package/src/renderer/components/primitives/index.ts +0 -10
  252. package/src/renderer/components/settings/AdvancedTab.tsx +0 -174
  253. package/src/renderer/components/settings/AppearanceTab.tsx +0 -77
  254. package/src/renderer/components/settings/GeneralTab.tsx +0 -40
  255. package/src/renderer/components/settings/HotkeysTab.tsx +0 -79
  256. package/src/renderer/components/settings/RecordingTab.tsx +0 -84
  257. package/src/renderer/components/settings/index.ts +0 -9
  258. package/src/renderer/components/settings/settingsStyles.ts +0 -673
  259. package/src/renderer/components/settings/tabConfig.tsx +0 -85
  260. package/src/renderer/components/settings/useSettingsPanel.ts +0 -447
  261. package/src/renderer/contexts/ProcessingContext.tsx +0 -227
  262. package/src/renderer/contexts/RecordingContext.tsx +0 -683
  263. package/src/renderer/contexts/UIContext.tsx +0 -326
  264. package/src/renderer/contexts/index.ts +0 -24
  265. package/src/renderer/donateMessages.ts +0 -69
  266. package/src/renderer/hooks/index.ts +0 -75
  267. package/src/renderer/hooks/useAnimation.tsx +0 -544
  268. package/src/renderer/hooks/useTheme.ts +0 -313
  269. package/src/renderer/index.html +0 -26
  270. package/src/renderer/main.tsx +0 -52
  271. package/src/renderer/styles/animations.css +0 -1093
  272. package/src/renderer/styles/app-shell.css +0 -662
  273. package/src/renderer/styles/globals.css +0 -515
  274. package/src/renderer/styles/theme.ts +0 -578
  275. package/src/renderer/types/electron.d.ts +0 -385
  276. package/src/shared/hotkeys.ts +0 -283
  277. package/src/shared/types.ts +0 -809
  278. package/tests/clipboard.test.ts +0 -228
  279. package/tests/e2e/criticalPaths.test.ts +0 -594
  280. package/tests/feedbackAnalyzer.test.ts +0 -303
  281. package/tests/integration/sessionFlow.test.ts +0 -583
  282. package/tests/markdownGenerator.test.ts +0 -418
  283. package/tests/output.test.ts +0 -96
  284. package/tests/setup.ts +0 -486
  285. package/tests/unit/appIntegration.test.ts +0 -676
  286. package/tests/unit/appViewState.test.ts +0 -281
  287. package/tests/unit/audioIpcChannels.test.ts +0 -17
  288. package/tests/unit/exportService.test.ts +0 -492
  289. package/tests/unit/hotkeys.test.ts +0 -92
  290. package/tests/unit/navigationPreload.test.ts +0 -94
  291. package/tests/unit/onboardingFlow.test.ts +0 -345
  292. package/tests/unit/permissionManager.test.ts +0 -175
  293. package/tests/unit/permissionManagerExpanded.test.ts +0 -296
  294. package/tests/unit/screenRecordingRenderer.test.ts +0 -368
  295. package/tests/unit/sessionController.test.ts +0 -515
  296. package/tests/unit/tierManager.test.ts +0 -61
  297. package/tests/unit/tierManagerExpanded.test.ts +0 -142
  298. package/tests/unit/transcriptAnalyzer.test.ts +0 -64
  299. package/tsconfig.json +0 -25
  300. package/vitest.config.ts +0 -46
@@ -1,1746 +0,0 @@
1
- /**
2
- * SessionHistory - Professional Data Management Experience
3
- *
4
- * A comprehensive session browser for viewing, searching, and managing past feedback sessions.
5
- *
6
- * Features:
7
- * - List all sessions with thumbnails and metadata
8
- * - Search by content (transcription text, project name)
9
- * - Sort/Filter by date, name, item count
10
- * - Quick preview on hover
11
- * - Actions: Open, delete, export, copy, open folder
12
- * - Bulk actions: Select multiple, delete/export batch
13
- * - Virtual scrolling for large lists
14
- * - Full keyboard navigation
15
- * - Context menu support
16
- */
17
-
18
- import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
19
- import { Skeleton, SkeletonText } from './Skeleton';
20
- import { useTheme } from '../hooks/useTheme';
21
-
22
- // ============================================================================
23
- // Types
24
- // ============================================================================
25
-
26
- /**
27
- * Re-export SessionMetadata for external usage
28
- * The actual interface is defined in electron.d.ts as SessionHistoryItem
29
- */
30
- export interface SessionMetadata {
31
- id: string;
32
- startTime: number;
33
- endTime: number;
34
- itemCount: number;
35
- screenshotCount: number;
36
- sourceName: string;
37
- firstThumbnail?: string;
38
- folder: string;
39
- transcriptionPreview?: string;
40
- }
41
-
42
- interface SessionHistoryProps {
43
- isOpen: boolean;
44
- onClose: () => void;
45
- onOpenSession: (session: SessionMetadata) => void;
46
- }
47
-
48
- type SortOption = 'date' | 'name' | 'items' | 'duration';
49
- type SortDirection = 'asc' | 'desc';
50
-
51
- interface ContextMenuState {
52
- visible: boolean;
53
- x: number;
54
- y: number;
55
- sessionId: string | null;
56
- }
57
-
58
- // ============================================================================
59
- // Helper Functions
60
- // ============================================================================
61
-
62
- function formatDuration(ms: number): string {
63
- const totalSeconds = Math.floor(ms / 1000);
64
- const mins = Math.floor(totalSeconds / 60);
65
- const secs = totalSeconds % 60;
66
- if (mins === 0) {
67
- return `${secs}s`;
68
- }
69
- return `${mins}:${secs.toString().padStart(2, '0')}`;
70
- }
71
-
72
- function formatRelativeDate(timestamp: number): string {
73
- const date = new Date(timestamp);
74
- const now = new Date();
75
- const diffMs = now.getTime() - date.getTime();
76
- const diffMins = Math.floor(diffMs / (1000 * 60));
77
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
78
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
79
-
80
- if (diffMins < 1) return 'Just now';
81
- if (diffMins < 60) return `${diffMins}m ago`;
82
- if (diffHours < 24) return `${diffHours}h ago`;
83
- if (diffDays < 7) return `${diffDays}d ago`;
84
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
85
- }
86
-
87
- // ============================================================================
88
- // Sub-Components
89
- // ============================================================================
90
-
91
- /**
92
- * Session Card - Individual session item
93
- */
94
- interface SessionCardProps {
95
- session: SessionMetadata;
96
- isSelected: boolean;
97
- isFocused: boolean;
98
- onSelect: (shift: boolean, ctrl: boolean) => void;
99
- onOpen: () => void;
100
- onDelete: () => void;
101
- onExport: () => void;
102
- onOpenFolder: () => void;
103
- onContextMenu: (e: React.MouseEvent) => void;
104
- }
105
-
106
- const SessionCard: React.FC<SessionCardProps> = ({
107
- session,
108
- isSelected,
109
- isFocused,
110
- onSelect,
111
- onOpen,
112
- onDelete,
113
- onExport,
114
- onOpenFolder,
115
- onContextMenu,
116
- }) => {
117
- const [isHovered, setIsHovered] = useState(false);
118
- const { colors } = useTheme();
119
- const duration = session.endTime - session.startTime;
120
-
121
- const handleClick = useCallback(
122
- (e: React.MouseEvent) => {
123
- e.stopPropagation();
124
- onSelect(e.shiftKey, e.metaKey || e.ctrlKey);
125
- },
126
- [onSelect]
127
- );
128
-
129
- const handleDoubleClick = useCallback(
130
- (e: React.MouseEvent) => {
131
- e.stopPropagation();
132
- onOpen();
133
- },
134
- [onOpen]
135
- );
136
-
137
- const handleKeyDown = useCallback(
138
- (e: React.KeyboardEvent) => {
139
- if (e.key === 'Enter') {
140
- e.preventDefault();
141
- onOpen();
142
- } else if (e.key === 'Delete' || e.key === 'Backspace') {
143
- e.preventDefault();
144
- onDelete();
145
- }
146
- },
147
- [onOpen, onDelete]
148
- );
149
-
150
- return (
151
- <div
152
- role="row"
153
- tabIndex={0}
154
- onClick={handleClick}
155
- onDoubleClick={handleDoubleClick}
156
- onContextMenu={onContextMenu}
157
- onKeyDown={handleKeyDown}
158
- onMouseEnter={() => setIsHovered(true)}
159
- onMouseLeave={() => setIsHovered(false)}
160
- style={{
161
- ...styles.sessionCard,
162
- backgroundColor: isSelected
163
- ? colors.accent.subtle
164
- : isHovered
165
- ? colors.bg.subtle
166
- : colors.surface.inset,
167
- borderColor: isSelected
168
- ? colors.accent.muted
169
- : isFocused
170
- ? colors.border.focus
171
- : colors.border.subtle,
172
- boxShadow: isSelected ? `0 4px 12px -2px ${colors.accent.subtle}` : 'none',
173
- }}
174
- >
175
- {/* Checkbox */}
176
- <div
177
- style={{
178
- ...styles.checkbox,
179
- backgroundColor: isSelected ? colors.accent.default : 'transparent',
180
- borderColor: isSelected ? colors.accent.default : colors.text.tertiary,
181
- }}
182
- >
183
- {isSelected && (
184
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
185
- <path
186
- d="M2.5 6l2.5 2.5 4.5-4.5"
187
- stroke={colors.text.inverse}
188
- strokeWidth="2"
189
- strokeLinecap="round"
190
- strokeLinejoin="round"
191
- />
192
- </svg>
193
- )}
194
- </div>
195
-
196
- {/* Thumbnail */}
197
- <div style={styles.thumbnail}>
198
- {session.firstThumbnail ? (
199
- <img
200
- src={`file://${session.firstThumbnail}`}
201
- alt="Session thumbnail"
202
- style={styles.thumbnailImage}
203
- onError={(e) => {
204
- (e.target as HTMLImageElement).style.display = 'none';
205
- }}
206
- />
207
- ) : (
208
- <div style={styles.thumbnailPlaceholder}>
209
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ color: 'var(--text-tertiary)' }}>
210
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
211
- <circle cx="8.5" cy="8.5" r="1.5" />
212
- <polyline points="21 15 16 10 5 21" />
213
- </svg>
214
- </div>
215
- )}
216
- </div>
217
-
218
- {/* Content */}
219
- <div style={styles.sessionContent}>
220
- <div style={styles.sessionHeader}>
221
- <span style={styles.sessionName}>{session.sourceName || 'Untitled Session'}</span>
222
- <span style={styles.sessionDate}>{formatRelativeDate(session.startTime)}</span>
223
- </div>
224
- <div style={styles.sessionMeta}>
225
- <span style={styles.metaItem}>
226
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
227
- <path d="M12 2v10l4 2" />
228
- <circle cx="12" cy="12" r="10" />
229
- </svg>
230
- {formatDuration(duration)}
231
- </span>
232
- <span style={styles.metaItem}>
233
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
234
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
235
- <circle cx="8.5" cy="8.5" r="1.5" />
236
- <polyline points="21 15 16 10 5 21" />
237
- </svg>
238
- {session.screenshotCount}
239
- </span>
240
- <span style={styles.metaItem}>
241
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
242
- <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
243
- <polyline points="14 2 14 8 20 8" />
244
- <line x1="16" y1="13" x2="8" y2="13" />
245
- <line x1="16" y1="17" x2="8" y2="17" />
246
- </svg>
247
- {session.itemCount} items
248
- </span>
249
- </div>
250
- {session.transcriptionPreview && (
251
- <p style={styles.transcriptionPreview}>
252
- {session.transcriptionPreview.slice(0, 80)}
253
- {session.transcriptionPreview.length > 80 ? '...' : ''}
254
- </p>
255
- )}
256
- </div>
257
-
258
- {/* Actions (on hover) */}
259
- {isHovered && (
260
- <div style={styles.actionButtons}>
261
- <button
262
- style={styles.actionButton}
263
- onClick={(e) => {
264
- e.stopPropagation();
265
- onOpen();
266
- }}
267
- title="Open Session"
268
- >
269
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
270
- <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
271
- <polyline points="15 3 21 3 21 9" />
272
- <line x1="10" y1="14" x2="21" y2="3" />
273
- </svg>
274
- </button>
275
- <button
276
- style={styles.actionButton}
277
- onClick={(e) => {
278
- e.stopPropagation();
279
- onOpenFolder();
280
- }}
281
- title="Open Folder"
282
- >
283
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
284
- <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
285
- </svg>
286
- </button>
287
- <button
288
- style={styles.actionButton}
289
- onClick={(e) => {
290
- e.stopPropagation();
291
- onExport();
292
- }}
293
- title="Export"
294
- >
295
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
296
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
297
- <polyline points="7 10 12 15 17 10" />
298
- <line x1="12" y1="15" x2="12" y2="3" />
299
- </svg>
300
- </button>
301
- <button
302
- style={{ ...styles.actionButton, color: colors.status.error }}
303
- onClick={(e) => {
304
- e.stopPropagation();
305
- onDelete();
306
- }}
307
- title="Delete"
308
- >
309
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
310
- <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
311
- </svg>
312
- </button>
313
- </div>
314
- )}
315
- </div>
316
- );
317
- };
318
-
319
- /**
320
- * Search Input with clear button
321
- */
322
- const SearchInput: React.FC<{
323
- value: string;
324
- onChange: (value: string) => void;
325
- placeholder?: string;
326
- }> = ({ value, onChange, placeholder = 'Search sessions...' }) => {
327
- const inputRef = useRef<HTMLInputElement>(null);
328
-
329
- return (
330
- <div style={styles.searchContainer}>
331
- <svg
332
- width="16"
333
- height="16"
334
- viewBox="0 0 24 24"
335
- fill="none"
336
- stroke="currentColor"
337
- strokeWidth="2"
338
- style={{ ...styles.searchIcon, color: 'var(--text-tertiary)' }}
339
- >
340
- <circle cx="11" cy="11" r="8" />
341
- <path d="M21 21l-4.35-4.35" />
342
- </svg>
343
- <input
344
- ref={inputRef}
345
- type="text"
346
- value={value}
347
- onChange={(e) => onChange(e.target.value)}
348
- placeholder={placeholder}
349
- style={styles.searchInput}
350
- />
351
- {value && (
352
- <button
353
- style={styles.clearButton}
354
- onClick={() => {
355
- onChange('');
356
- inputRef.current?.focus();
357
- }}
358
- >
359
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
360
- <circle cx="12" cy="12" r="10" />
361
- <line x1="15" y1="9" x2="9" y2="15" />
362
- <line x1="9" y1="9" x2="15" y2="15" />
363
- </svg>
364
- </button>
365
- )}
366
- </div>
367
- );
368
- };
369
-
370
- /**
371
- * Sort Dropdown
372
- */
373
- const SortDropdown: React.FC<{
374
- sortBy: SortOption;
375
- direction: SortDirection;
376
- onSortChange: (sort: SortOption) => void;
377
- onDirectionToggle: () => void;
378
- }> = ({ sortBy, direction, onSortChange, onDirectionToggle }) => {
379
- const [isOpen, setIsOpen] = useState(false);
380
- const dropdownRef = useRef<HTMLDivElement>(null);
381
-
382
- const sortOptions: { value: SortOption; label: string }[] = [
383
- { value: 'date', label: 'Date' },
384
- { value: 'name', label: 'Name' },
385
- { value: 'items', label: 'Item Count' },
386
- { value: 'duration', label: 'Duration' },
387
- ];
388
-
389
- const currentLabel = sortOptions.find((opt) => opt.value === sortBy)?.label || 'Date';
390
-
391
- useEffect(() => {
392
- const handleClickOutside = (e: MouseEvent) => {
393
- if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
394
- setIsOpen(false);
395
- }
396
- };
397
- document.addEventListener('mousedown', handleClickOutside);
398
- return () => document.removeEventListener('mousedown', handleClickOutside);
399
- }, []);
400
-
401
- return (
402
- <div ref={dropdownRef} style={styles.sortDropdown}>
403
- <button style={styles.sortButton} onClick={() => setIsOpen(!isOpen)}>
404
- <span>Sort: {currentLabel}</span>
405
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
406
- <path
407
- d="M3 5l3 3 3-3"
408
- stroke="currentColor"
409
- strokeWidth="1.5"
410
- strokeLinecap="round"
411
- strokeLinejoin="round"
412
- />
413
- </svg>
414
- </button>
415
- <button
416
- style={{
417
- ...styles.directionButton,
418
- transform: direction === 'asc' ? 'rotate(180deg)' : 'none',
419
- }}
420
- onClick={onDirectionToggle}
421
- title={direction === 'desc' ? 'Newest first' : 'Oldest first'}
422
- >
423
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
424
- <polyline points="6 9 12 15 18 9" />
425
- </svg>
426
- </button>
427
- {isOpen && (
428
- <div style={styles.sortDropdownMenu}>
429
- {sortOptions.map((option) => (
430
- <button
431
- key={option.value}
432
- style={{
433
- ...styles.sortDropdownItem,
434
- backgroundColor: sortBy === option.value ? 'var(--accent-subtle)' : 'transparent',
435
- }}
436
- onClick={() => {
437
- onSortChange(option.value);
438
- setIsOpen(false);
439
- }}
440
- >
441
- {option.label}
442
- {sortBy === option.value && (
443
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent-default)" strokeWidth="2">
444
- <polyline points="20 6 9 17 4 12" />
445
- </svg>
446
- )}
447
- </button>
448
- ))}
449
- </div>
450
- )}
451
- </div>
452
- );
453
- };
454
-
455
- /**
456
- * Context Menu
457
- */
458
- const ContextMenu: React.FC<{
459
- state: ContextMenuState;
460
- onClose: () => void;
461
- onOpen: () => void;
462
- onOpenFolder: () => void;
463
- onExport: () => void;
464
- onDelete: () => void;
465
- onSelectAll: () => void;
466
- }> = ({ state, onClose, onOpen, onOpenFolder, onExport, onDelete, onSelectAll }) => {
467
- const menuRef = useRef<HTMLDivElement>(null);
468
-
469
- useEffect(() => {
470
- const handleClickOutside = (e: MouseEvent) => {
471
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
472
- onClose();
473
- }
474
- };
475
- document.addEventListener('mousedown', handleClickOutside);
476
- return () => document.removeEventListener('mousedown', handleClickOutside);
477
- }, [onClose]);
478
-
479
- useEffect(() => {
480
- const handleKeyDown = (e: KeyboardEvent) => {
481
- if (e.key === 'Escape') {
482
- onClose();
483
- }
484
- };
485
- document.addEventListener('keydown', handleKeyDown);
486
- return () => document.removeEventListener('keydown', handleKeyDown);
487
- }, [onClose]);
488
-
489
- if (!state.visible) return null;
490
-
491
- return (
492
- <div
493
- ref={menuRef}
494
- style={{
495
- ...styles.contextMenu,
496
- top: state.y,
497
- left: state.x,
498
- }}
499
- >
500
- <button style={styles.contextMenuItem} onClick={onOpen}>
501
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
502
- <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
503
- <polyline points="15 3 21 3 21 9" />
504
- <line x1="10" y1="14" x2="21" y2="3" />
505
- </svg>
506
- Open
507
- </button>
508
- <button style={styles.contextMenuItem} onClick={onOpenFolder}>
509
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
510
- <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
511
- </svg>
512
- Open Folder
513
- </button>
514
- <div style={styles.contextMenuDivider} />
515
- <button style={styles.contextMenuItem} onClick={onExport}>
516
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
517
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
518
- <polyline points="7 10 12 15 17 10" />
519
- <line x1="12" y1="15" x2="12" y2="3" />
520
- </svg>
521
- Export
522
- </button>
523
- <button style={styles.contextMenuItem} onClick={onSelectAll}>
524
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
525
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
526
- <path d="M9 9h6v6H9z" />
527
- </svg>
528
- Select All
529
- </button>
530
- <div style={styles.contextMenuDivider} />
531
- <button style={{ ...styles.contextMenuItem, color: 'var(--status-error)' }} onClick={onDelete}>
532
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
533
- <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
534
- </svg>
535
- Delete
536
- </button>
537
- </div>
538
- );
539
- };
540
-
541
- /**
542
- * Delete Confirmation Dialog
543
- */
544
- const DeleteConfirmDialog: React.FC<{
545
- isOpen: boolean;
546
- count: number;
547
- onConfirm: () => void;
548
- onCancel: () => void;
549
- }> = ({ isOpen, count, onConfirm, onCancel }) => {
550
- if (!isOpen) return null;
551
-
552
- return (
553
- <div style={styles.dialogOverlay}>
554
- <div style={styles.dialogBackdrop} onClick={onCancel} />
555
- <div style={styles.dialog}>
556
- <div style={styles.dialogIcon}>
557
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--status-error)" strokeWidth="1.5">
558
- <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
559
- <line x1="10" y1="11" x2="10" y2="17" />
560
- <line x1="14" y1="11" x2="14" y2="17" />
561
- </svg>
562
- </div>
563
- <h3 style={styles.dialogTitle}>
564
- Delete {count} session{count > 1 ? 's' : ''}?
565
- </h3>
566
- <p style={styles.dialogMessage}>
567
- This will permanently delete the session{count > 1 ? 's' : ''} and all associated screenshots. This action
568
- cannot be undone.
569
- </p>
570
- <div style={styles.dialogButtons}>
571
- <button style={styles.dialogCancelButton} onClick={onCancel}>
572
- Cancel
573
- </button>
574
- <button style={styles.dialogDeleteButton} onClick={onConfirm}>
575
- Delete
576
- </button>
577
- </div>
578
- </div>
579
- </div>
580
- );
581
- };
582
-
583
- /**
584
- * Empty State
585
- */
586
- const EmptyState: React.FC<{ hasSearch: boolean; onClear: () => void }> = ({ hasSearch, onClear }) => (
587
- <div style={styles.emptyState}>
588
- <div style={styles.emptyIcon}>
589
- <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" style={{ color: 'var(--text-tertiary)' }}>
590
- {hasSearch ? (
591
- <>
592
- <circle cx="11" cy="11" r="8" />
593
- <path d="M21 21l-4.35-4.35" />
594
- <path d="M8 11h6" strokeWidth="1.5" />
595
- </>
596
- ) : (
597
- <>
598
- <path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
599
- <path d="M12 11v6M9 14h6" strokeWidth="1.5" strokeLinecap="round" />
600
- </>
601
- )}
602
- </svg>
603
- </div>
604
- <h3 style={styles.emptyTitle}>{hasSearch ? 'No sessions found' : 'No sessions yet'}</h3>
605
- <p style={styles.emptyMessage}>
606
- {hasSearch
607
- ? "Try adjusting your search terms or clear the filter to see all sessions."
608
- : 'Start recording feedback to see your sessions here.'}
609
- </p>
610
- {hasSearch && (
611
- <button style={styles.emptyClearButton} onClick={onClear}>
612
- Clear Search
613
- </button>
614
- )}
615
- </div>
616
- );
617
-
618
- /**
619
- * Loading State
620
- */
621
- const LoadingState: React.FC = () => (
622
- <div style={styles.loadingContainer}>
623
- {Array.from({ length: 5 }).map((_, index) => (
624
- <div
625
- key={index}
626
- style={{
627
- ...styles.skeletonCard,
628
- animationDelay: `${index * 100}ms`,
629
- }}
630
- className="ff-list-item-enter"
631
- >
632
- <Skeleton width={20} height={20} rounded={4} />
633
- <Skeleton width={80} height={56} rounded={8} />
634
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
635
- <Skeleton width="60%" height={16} />
636
- <Skeleton width="40%" height={12} />
637
- <SkeletonText lines={1} animation="shimmer" />
638
- </div>
639
- </div>
640
- ))}
641
- </div>
642
- );
643
-
644
- // ============================================================================
645
- // Main Component
646
- // ============================================================================
647
-
648
- export function SessionHistory({ isOpen, onClose, onOpenSession }: SessionHistoryProps) {
649
- // State
650
- const [sessions, setSessions] = useState<SessionMetadata[]>([]);
651
- const [search, setSearch] = useState('');
652
- const [sortBy, setSortBy] = useState<SortOption>('date');
653
- const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
654
- const [selected, setSelected] = useState<Set<string>>(new Set());
655
- const [focusedIndex, setFocusedIndex] = useState<number>(-1);
656
- const [isLoading, setIsLoading] = useState(true);
657
- const [contextMenu, setContextMenu] = useState<ContextMenuState>({
658
- visible: false,
659
- x: 0,
660
- y: 0,
661
- sessionId: null,
662
- });
663
- const [deleteConfirm, setDeleteConfirm] = useState<{
664
- isOpen: boolean;
665
- sessionIds: string[];
666
- }>({ isOpen: false, sessionIds: [] });
667
-
668
- const containerRef = useRef<HTMLDivElement>(null);
669
- const listRef = useRef<HTMLDivElement>(null);
670
-
671
- // Load sessions on mount/open
672
- useEffect(() => {
673
- if (!isOpen) return;
674
-
675
- async function loadSessions() {
676
- setIsLoading(true);
677
- try {
678
- // Call the IPC API to list sessions
679
- // The API is typed in electron.d.ts as window.markupr.output.listSessions()
680
- if (window.markupr?.output?.listSessions) {
681
- const list = await window.markupr.output.listSessions();
682
- setSessions(list);
683
- } else {
684
- // Fallback for development/testing without full IPC wiring
685
- console.warn('[SessionHistory] listSessions API not available');
686
- setSessions([]);
687
- }
688
- } catch (error) {
689
- console.error('Failed to load sessions:', error);
690
- setSessions([]);
691
- } finally {
692
- setIsLoading(false);
693
- }
694
- }
695
-
696
- loadSessions();
697
- setSelected(new Set());
698
- setFocusedIndex(-1);
699
- }, [isOpen]);
700
-
701
- // Filter and sort sessions
702
- const filteredSessions = useMemo(() => {
703
- let result = sessions;
704
-
705
- // Filter by search
706
- if (search) {
707
- const searchLower = search.toLowerCase();
708
- result = result.filter(
709
- (s) =>
710
- s.sourceName.toLowerCase().includes(searchLower) ||
711
- (s.transcriptionPreview && s.transcriptionPreview.toLowerCase().includes(searchLower))
712
- );
713
- }
714
-
715
- // Sort
716
- result = [...result].sort((a, b) => {
717
- let comparison = 0;
718
-
719
- switch (sortBy) {
720
- case 'date':
721
- comparison = b.startTime - a.startTime;
722
- break;
723
- case 'name':
724
- comparison = (a.sourceName || '').localeCompare(b.sourceName || '');
725
- break;
726
- case 'items':
727
- comparison = b.itemCount - a.itemCount;
728
- break;
729
- case 'duration':
730
- comparison = (b.endTime - b.startTime) - (a.endTime - a.startTime);
731
- break;
732
- }
733
-
734
- return sortDirection === 'desc' ? comparison : -comparison;
735
- });
736
-
737
- return result;
738
- }, [sessions, search, sortBy, sortDirection]);
739
-
740
- // Handlers
741
- const handleSelectSession = useCallback(
742
- (sessionId: string, shift: boolean, ctrl: boolean) => {
743
- setSelected((prev) => {
744
- const newSet = new Set(prev);
745
-
746
- if (shift && focusedIndex >= 0) {
747
- // Range selection
748
- const currentIndex = filteredSessions.findIndex((s) => s.id === sessionId);
749
- const start = Math.min(focusedIndex, currentIndex);
750
- const end = Math.max(focusedIndex, currentIndex);
751
-
752
- for (let i = start; i <= end; i++) {
753
- newSet.add(filteredSessions[i].id);
754
- }
755
- } else if (ctrl) {
756
- // Toggle selection
757
- if (newSet.has(sessionId)) {
758
- newSet.delete(sessionId);
759
- } else {
760
- newSet.add(sessionId);
761
- }
762
- } else {
763
- // Single selection
764
- newSet.clear();
765
- newSet.add(sessionId);
766
- }
767
-
768
- return newSet;
769
- });
770
-
771
- // Update focused index
772
- const index = filteredSessions.findIndex((s) => s.id === sessionId);
773
- if (index >= 0) {
774
- setFocusedIndex(index);
775
- }
776
- },
777
- [filteredSessions, focusedIndex]
778
- );
779
-
780
- const handleOpenSession = useCallback(
781
- (session: SessionMetadata) => {
782
- onOpenSession(session);
783
- onClose();
784
- },
785
- [onOpenSession, onClose]
786
- );
787
-
788
- const handleDeleteSessions = useCallback((sessionIds: string[]) => {
789
- setDeleteConfirm({ isOpen: true, sessionIds });
790
- }, []);
791
-
792
- const handleConfirmDelete = useCallback(async () => {
793
- const { sessionIds } = deleteConfirm;
794
-
795
- try {
796
- // Call the IPC API to delete sessions
797
- if (window.markupr?.output?.deleteSessions) {
798
- const result = await window.markupr.output.deleteSessions(sessionIds);
799
- if (result.success) {
800
- // Remove successfully deleted sessions from state
801
- setSessions((prev) => prev.filter((s) => !result.deleted.includes(s.id)));
802
- setSelected((prev) => {
803
- const newSet = new Set(prev);
804
- result.deleted.forEach((id) => newSet.delete(id));
805
- return newSet;
806
- });
807
- }
808
- if (result.failed.length > 0) {
809
- console.warn('Some sessions failed to delete:', result.failed);
810
- }
811
- } else {
812
- // Fallback: just remove from local state
813
- setSessions((prev) => prev.filter((s) => !sessionIds.includes(s.id)));
814
- setSelected((prev) => {
815
- const newSet = new Set(prev);
816
- sessionIds.forEach((id) => newSet.delete(id));
817
- return newSet;
818
- });
819
- }
820
- } catch (error) {
821
- console.error('Failed to delete sessions:', error);
822
- }
823
-
824
- setDeleteConfirm({ isOpen: false, sessionIds: [] });
825
- }, [deleteConfirm]);
826
-
827
- const handleExportSessions = useCallback(async (sessionIds: string[]) => {
828
- try {
829
- // Call the IPC API to export sessions
830
- if (window.markupr?.output?.exportSessions) {
831
- const result = await window.markupr.output.exportSessions(sessionIds);
832
- if (result.success && result.path) {
833
- console.log('Sessions exported to:', result.path);
834
- // Optionally open the folder containing the export
835
- await window.markupr.output.openFolder(result.path);
836
- } else if (result.error) {
837
- console.error('Export failed:', result.error);
838
- }
839
- } else {
840
- console.warn('[SessionHistory] exportSessions API not available');
841
- }
842
- } catch (error) {
843
- console.error('Failed to export sessions:', error);
844
- }
845
- }, []);
846
-
847
- const handleOpenFolder = useCallback(async (session: SessionMetadata) => {
848
- try {
849
- await window.markupr.output.openFolder(session.folder);
850
- } catch (error) {
851
- console.error('Failed to open folder:', error);
852
- }
853
- }, []);
854
-
855
- const handleSelectAll = useCallback(() => {
856
- setSelected(new Set(filteredSessions.map((s) => s.id)));
857
- }, [filteredSessions]);
858
-
859
- const handleDeselectAll = useCallback(() => {
860
- setSelected(new Set());
861
- }, []);
862
-
863
- const handleContextMenu = useCallback((e: React.MouseEvent, sessionId: string) => {
864
- e.preventDefault();
865
- setContextMenu({
866
- visible: true,
867
- x: e.clientX,
868
- y: e.clientY,
869
- sessionId,
870
- });
871
- }, []);
872
-
873
- // Keyboard navigation
874
- useEffect(() => {
875
- if (!isOpen) return;
876
-
877
- const handleKeyDown = (e: KeyboardEvent) => {
878
- // Close on Escape
879
- if (e.key === 'Escape') {
880
- if (contextMenu.visible) {
881
- setContextMenu((prev) => ({ ...prev, visible: false }));
882
- } else if (deleteConfirm.isOpen) {
883
- setDeleteConfirm({ isOpen: false, sessionIds: [] });
884
- } else {
885
- onClose();
886
- }
887
- return;
888
- }
889
-
890
- // Navigation
891
- if (e.key === 'ArrowDown') {
892
- e.preventDefault();
893
- setFocusedIndex((prev) => Math.min(prev + 1, filteredSessions.length - 1));
894
- } else if (e.key === 'ArrowUp') {
895
- e.preventDefault();
896
- setFocusedIndex((prev) => Math.max(prev - 1, 0));
897
- } else if (e.key === ' ' && focusedIndex >= 0) {
898
- e.preventDefault();
899
- const session = filteredSessions[focusedIndex];
900
- handleSelectSession(session.id, e.shiftKey, true);
901
- } else if (e.key === 'Enter' && focusedIndex >= 0) {
902
- e.preventDefault();
903
- const session = filteredSessions[focusedIndex];
904
- handleOpenSession(session);
905
- } else if (e.key === 'Delete' || e.key === 'Backspace') {
906
- if (selected.size > 0 && !e.metaKey && !e.ctrlKey) {
907
- e.preventDefault();
908
- handleDeleteSessions(Array.from(selected));
909
- }
910
- } else if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
911
- e.preventDefault();
912
- handleSelectAll();
913
- }
914
- };
915
-
916
- window.addEventListener('keydown', handleKeyDown);
917
- return () => window.removeEventListener('keydown', handleKeyDown);
918
- }, [
919
- isOpen,
920
- filteredSessions,
921
- focusedIndex,
922
- selected,
923
- contextMenu.visible,
924
- deleteConfirm.isOpen,
925
- handleSelectSession,
926
- handleOpenSession,
927
- handleDeleteSessions,
928
- handleSelectAll,
929
- onClose,
930
- ]);
931
-
932
- // Scroll focused item into view
933
- useEffect(() => {
934
- if (focusedIndex >= 0 && listRef.current) {
935
- const item = listRef.current.children[focusedIndex] as HTMLElement;
936
- if (item) {
937
- item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
938
- }
939
- }
940
- }, [focusedIndex]);
941
-
942
- if (!isOpen) return null;
943
-
944
- return (
945
- <div style={styles.overlay}>
946
- {/* dialogEnter keyframe provided by animations.css; scrollbar styles below */}
947
- <style>
948
- {`
949
- .markupr-history-scrollbar::-webkit-scrollbar {
950
- width: 8px;
951
- }
952
-
953
- .markupr-history-scrollbar::-webkit-scrollbar-track {
954
- background: var(--surface-inset);
955
- border-radius: 4px;
956
- }
957
-
958
- .markupr-history-scrollbar::-webkit-scrollbar-thumb {
959
- background: var(--border-strong);
960
- border-radius: 4px;
961
- }
962
-
963
- .markupr-history-scrollbar::-webkit-scrollbar-thumb:hover {
964
- background: var(--text-tertiary);
965
- }
966
- `}
967
- </style>
968
-
969
- <div style={styles.backdrop} onClick={onClose} />
970
-
971
- <div ref={containerRef} style={styles.panel} className="ff-dialog-enter">
972
- {/* Header */}
973
- <div style={styles.header}>
974
- <div style={styles.headerLeft}>
975
- <h2 style={styles.headerTitle}>Session History</h2>
976
- {!isLoading && (
977
- <span style={styles.sessionCount}>
978
- {filteredSessions.length} session{filteredSessions.length !== 1 ? 's' : ''}
979
- </span>
980
- )}
981
- </div>
982
- <SearchInput value={search} onChange={setSearch} />
983
- <button style={styles.closeButton} onClick={onClose} aria-label="Close">
984
- <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
985
- <path d="M5 5l10 10M15 5l-10 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
986
- </svg>
987
- </button>
988
- </div>
989
-
990
- {/* Toolbar */}
991
- <div style={styles.toolbar}>
992
- <div style={styles.toolbarLeft}>
993
- <SortDropdown
994
- sortBy={sortBy}
995
- direction={sortDirection}
996
- onSortChange={setSortBy}
997
- onDirectionToggle={() => setSortDirection((d) => (d === 'desc' ? 'asc' : 'desc'))}
998
- />
999
- {selected.size > 0 && (
1000
- <span style={styles.selectedCount}>
1001
- {selected.size} selected
1002
- <button style={styles.deselectButton} onClick={handleDeselectAll}>
1003
- Clear
1004
- </button>
1005
- </span>
1006
- )}
1007
- </div>
1008
- <div style={styles.toolbarRight}>
1009
- {selected.size > 0 && (
1010
- <>
1011
- <button
1012
- style={styles.bulkButton}
1013
- onClick={() => handleExportSessions(Array.from(selected))}
1014
- title="Export selected"
1015
- >
1016
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1017
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
1018
- <polyline points="7 10 12 15 17 10" />
1019
- <line x1="12" y1="15" x2="12" y2="3" />
1020
- </svg>
1021
- Export
1022
- </button>
1023
- <button
1024
- style={{ ...styles.bulkButton, ...styles.deleteButton }}
1025
- onClick={() => handleDeleteSessions(Array.from(selected))}
1026
- title="Delete selected"
1027
- >
1028
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1029
- <path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
1030
- </svg>
1031
- Delete
1032
- </button>
1033
- </>
1034
- )}
1035
- </div>
1036
- </div>
1037
-
1038
- {/* Content */}
1039
- <div ref={listRef} style={styles.content} className="markupr-history-scrollbar" role="grid">
1040
- {isLoading ? (
1041
- <LoadingState />
1042
- ) : filteredSessions.length === 0 ? (
1043
- <EmptyState hasSearch={!!search} onClear={() => setSearch('')} />
1044
- ) : (
1045
- filteredSessions.map((session, index) => (
1046
- <SessionCard
1047
- key={session.id}
1048
- session={session}
1049
- isSelected={selected.has(session.id)}
1050
- isFocused={focusedIndex === index}
1051
- onSelect={(shift, ctrl) => handleSelectSession(session.id, shift, ctrl)}
1052
- onOpen={() => handleOpenSession(session)}
1053
- onDelete={() => handleDeleteSessions([session.id])}
1054
- onExport={() => handleExportSessions([session.id])}
1055
- onOpenFolder={() => handleOpenFolder(session)}
1056
- onContextMenu={(e) => handleContextMenu(e, session.id)}
1057
- />
1058
- ))
1059
- )}
1060
- </div>
1061
-
1062
- {/* Footer */}
1063
- <div style={styles.footer}>
1064
- <div style={styles.footerLeft}>
1065
- <span style={styles.footerHint}>
1066
- <kbd style={styles.kbd}>Arrow</kbd> Navigate
1067
- <kbd style={styles.kbd}>Space</kbd> Select
1068
- <kbd style={styles.kbd}>Enter</kbd> Open
1069
- <kbd style={styles.kbd}>Del</kbd> Delete
1070
- </span>
1071
- </div>
1072
- <button style={styles.closeFooterButton} onClick={onClose}>
1073
- Close
1074
- </button>
1075
- </div>
1076
- </div>
1077
-
1078
- {/* Context Menu */}
1079
- <ContextMenu
1080
- state={contextMenu}
1081
- onClose={() => setContextMenu((prev) => ({ ...prev, visible: false }))}
1082
- onOpen={() => {
1083
- const session = filteredSessions.find((s) => s.id === contextMenu.sessionId);
1084
- if (session) handleOpenSession(session);
1085
- setContextMenu((prev) => ({ ...prev, visible: false }));
1086
- }}
1087
- onOpenFolder={() => {
1088
- const session = filteredSessions.find((s) => s.id === contextMenu.sessionId);
1089
- if (session) handleOpenFolder(session);
1090
- setContextMenu((prev) => ({ ...prev, visible: false }));
1091
- }}
1092
- onExport={() => {
1093
- if (contextMenu.sessionId) handleExportSessions([contextMenu.sessionId]);
1094
- setContextMenu((prev) => ({ ...prev, visible: false }));
1095
- }}
1096
- onDelete={() => {
1097
- if (contextMenu.sessionId) handleDeleteSessions([contextMenu.sessionId]);
1098
- setContextMenu((prev) => ({ ...prev, visible: false }));
1099
- }}
1100
- onSelectAll={() => {
1101
- handleSelectAll();
1102
- setContextMenu((prev) => ({ ...prev, visible: false }));
1103
- }}
1104
- />
1105
-
1106
- {/* Delete Confirmation Dialog */}
1107
- <DeleteConfirmDialog
1108
- isOpen={deleteConfirm.isOpen}
1109
- count={deleteConfirm.sessionIds.length}
1110
- onConfirm={handleConfirmDelete}
1111
- onCancel={() => setDeleteConfirm({ isOpen: false, sessionIds: [] })}
1112
- />
1113
- </div>
1114
- );
1115
- }
1116
-
1117
- // ============================================================================
1118
- // Styles
1119
- // ============================================================================
1120
-
1121
- type ExtendedCSSProperties = React.CSSProperties & {
1122
- WebkitAppRegion?: 'drag' | 'no-drag';
1123
- };
1124
-
1125
- const styles: Record<string, ExtendedCSSProperties> = {
1126
- // Overlay & Panel
1127
- overlay: {
1128
- position: 'fixed',
1129
- inset: 0,
1130
- display: 'flex',
1131
- alignItems: 'center',
1132
- justifyContent: 'center',
1133
- zIndex: 100,
1134
- padding: 24,
1135
- },
1136
-
1137
- backdrop: {
1138
- position: 'absolute',
1139
- inset: 0,
1140
- backgroundColor: 'var(--bg-overlay)',
1141
- backdropFilter: 'blur(4px)',
1142
- },
1143
-
1144
- panel: {
1145
- position: 'relative',
1146
- width: '100%',
1147
- maxWidth: 900,
1148
- maxHeight: '90vh',
1149
- backgroundColor: 'var(--surface-glass)',
1150
- borderRadius: 16,
1151
- boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--border-default)',
1152
- display: 'flex',
1153
- flexDirection: 'column',
1154
- overflow: 'hidden',
1155
- },
1156
-
1157
- // Header
1158
- header: {
1159
- display: 'flex',
1160
- alignItems: 'center',
1161
- gap: 16,
1162
- padding: '20px 24px',
1163
- borderBottom: '1px solid var(--border-default)',
1164
- WebkitAppRegion: 'drag',
1165
- },
1166
-
1167
- headerLeft: {
1168
- display: 'flex',
1169
- alignItems: 'baseline',
1170
- gap: 12,
1171
- WebkitAppRegion: 'no-drag',
1172
- },
1173
-
1174
- headerTitle: {
1175
- fontSize: 18,
1176
- fontWeight: 600,
1177
- color: 'var(--text-primary)',
1178
- margin: 0,
1179
- },
1180
-
1181
- sessionCount: {
1182
- fontSize: 13,
1183
- color: 'var(--text-tertiary)',
1184
- fontWeight: 400,
1185
- },
1186
-
1187
- closeButton: {
1188
- width: 32,
1189
- height: 32,
1190
- display: 'flex',
1191
- alignItems: 'center',
1192
- justifyContent: 'center',
1193
- backgroundColor: 'transparent',
1194
- border: 'none',
1195
- borderRadius: 8,
1196
- color: 'var(--text-secondary)',
1197
- cursor: 'pointer',
1198
- transition: 'all 0.2s ease',
1199
- WebkitAppRegion: 'no-drag',
1200
- marginLeft: 8,
1201
- },
1202
-
1203
- // Search
1204
- searchContainer: {
1205
- flex: 1,
1206
- maxWidth: 280,
1207
- position: 'relative',
1208
- display: 'flex',
1209
- alignItems: 'center',
1210
- WebkitAppRegion: 'no-drag',
1211
- },
1212
-
1213
- searchIcon: {
1214
- position: 'absolute',
1215
- left: 12,
1216
- pointerEvents: 'none',
1217
- },
1218
-
1219
- searchInput: {
1220
- width: '100%',
1221
- padding: '8px 36px 8px 38px',
1222
- backgroundColor: 'var(--surface-inset)',
1223
- border: '1px solid var(--border-strong)',
1224
- borderRadius: 8,
1225
- color: 'var(--text-primary)',
1226
- fontSize: 13,
1227
- outline: 'none',
1228
- transition: 'border-color 0.2s ease',
1229
- },
1230
-
1231
- clearButton: {
1232
- position: 'absolute',
1233
- right: 8,
1234
- width: 24,
1235
- height: 24,
1236
- display: 'flex',
1237
- alignItems: 'center',
1238
- justifyContent: 'center',
1239
- backgroundColor: 'transparent',
1240
- border: 'none',
1241
- borderRadius: 4,
1242
- color: 'var(--text-tertiary)',
1243
- cursor: 'pointer',
1244
- },
1245
-
1246
- // Toolbar
1247
- toolbar: {
1248
- display: 'flex',
1249
- alignItems: 'center',
1250
- justifyContent: 'space-between',
1251
- padding: '12px 24px',
1252
- borderBottom: '1px solid var(--border-subtle)',
1253
- backgroundColor: 'var(--surface-glass)',
1254
- },
1255
-
1256
- toolbarLeft: {
1257
- display: 'flex',
1258
- alignItems: 'center',
1259
- gap: 16,
1260
- },
1261
-
1262
- toolbarRight: {
1263
- display: 'flex',
1264
- alignItems: 'center',
1265
- gap: 8,
1266
- },
1267
-
1268
- selectedCount: {
1269
- display: 'flex',
1270
- alignItems: 'center',
1271
- gap: 8,
1272
- fontSize: 13,
1273
- color: 'var(--text-link)',
1274
- fontWeight: 500,
1275
- },
1276
-
1277
- deselectButton: {
1278
- padding: '2px 8px',
1279
- backgroundColor: 'transparent',
1280
- border: '1px solid var(--accent-subtle)',
1281
- borderRadius: 4,
1282
- color: 'var(--text-link)',
1283
- fontSize: 11,
1284
- cursor: 'pointer',
1285
- },
1286
-
1287
- // Sort Dropdown
1288
- sortDropdown: {
1289
- position: 'relative',
1290
- display: 'flex',
1291
- alignItems: 'center',
1292
- gap: 4,
1293
- },
1294
-
1295
- sortButton: {
1296
- display: 'flex',
1297
- alignItems: 'center',
1298
- gap: 6,
1299
- padding: '6px 10px',
1300
- backgroundColor: 'var(--surface-inset)',
1301
- border: '1px solid var(--border-strong)',
1302
- borderRadius: 6,
1303
- color: 'var(--text-secondary)',
1304
- fontSize: 12,
1305
- fontWeight: 500,
1306
- cursor: 'pointer',
1307
- transition: 'all 0.15s ease',
1308
- },
1309
-
1310
- directionButton: {
1311
- width: 28,
1312
- height: 28,
1313
- display: 'flex',
1314
- alignItems: 'center',
1315
- justifyContent: 'center',
1316
- backgroundColor: 'var(--surface-inset)',
1317
- border: '1px solid var(--border-strong)',
1318
- borderRadius: 6,
1319
- color: 'var(--text-secondary)',
1320
- cursor: 'pointer',
1321
- transition: 'all 0.15s ease',
1322
- },
1323
-
1324
- sortDropdownMenu: {
1325
- position: 'absolute',
1326
- top: '100%',
1327
- left: 0,
1328
- marginTop: 4,
1329
- minWidth: 140,
1330
- backgroundColor: 'var(--bg-elevated)',
1331
- border: '1px solid var(--border-strong)',
1332
- borderRadius: 8,
1333
- padding: 4,
1334
- zIndex: 50,
1335
- boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.4)',
1336
- },
1337
-
1338
- sortDropdownItem: {
1339
- display: 'flex',
1340
- alignItems: 'center',
1341
- justifyContent: 'space-between',
1342
- width: '100%',
1343
- padding: '8px 12px',
1344
- backgroundColor: 'transparent',
1345
- border: 'none',
1346
- borderRadius: 4,
1347
- color: 'var(--text-primary)',
1348
- fontSize: 13,
1349
- textAlign: 'left',
1350
- cursor: 'pointer',
1351
- transition: 'background-color 0.15s ease',
1352
- },
1353
-
1354
- // Bulk Actions
1355
- bulkButton: {
1356
- display: 'flex',
1357
- alignItems: 'center',
1358
- gap: 6,
1359
- padding: '6px 12px',
1360
- backgroundColor: 'var(--surface-inset)',
1361
- border: '1px solid var(--border-strong)',
1362
- borderRadius: 6,
1363
- color: 'var(--text-secondary)',
1364
- fontSize: 12,
1365
- fontWeight: 500,
1366
- cursor: 'pointer',
1367
- transition: 'all 0.15s ease',
1368
- },
1369
-
1370
- deleteButton: {
1371
- backgroundColor: 'var(--status-error-subtle)',
1372
- borderColor: 'var(--status-error)',
1373
- color: 'var(--status-error)',
1374
- },
1375
-
1376
- // Content
1377
- content: {
1378
- flex: 1,
1379
- padding: 16,
1380
- overflowY: 'auto',
1381
- display: 'flex',
1382
- flexDirection: 'column',
1383
- gap: 8,
1384
- },
1385
-
1386
- // Session Card
1387
- sessionCard: {
1388
- display: 'flex',
1389
- alignItems: 'center',
1390
- gap: 12,
1391
- padding: 12,
1392
- borderRadius: 10,
1393
- border: '1px solid',
1394
- cursor: 'pointer',
1395
- transition: 'all 0.15s ease',
1396
- outline: 'none',
1397
- },
1398
-
1399
- checkbox: {
1400
- width: 20,
1401
- height: 20,
1402
- borderRadius: 4,
1403
- border: '2px solid',
1404
- display: 'flex',
1405
- alignItems: 'center',
1406
- justifyContent: 'center',
1407
- flexShrink: 0,
1408
- transition: 'all 0.15s ease',
1409
- },
1410
-
1411
- thumbnail: {
1412
- width: 80,
1413
- height: 56,
1414
- borderRadius: 6,
1415
- overflow: 'hidden',
1416
- backgroundColor: 'var(--surface-inset)',
1417
- flexShrink: 0,
1418
- },
1419
-
1420
- thumbnailImage: {
1421
- width: '100%',
1422
- height: '100%',
1423
- objectFit: 'cover',
1424
- },
1425
-
1426
- thumbnailPlaceholder: {
1427
- width: '100%',
1428
- height: '100%',
1429
- display: 'flex',
1430
- alignItems: 'center',
1431
- justifyContent: 'center',
1432
- backgroundColor: 'var(--surface-inset)',
1433
- },
1434
-
1435
- sessionContent: {
1436
- flex: 1,
1437
- minWidth: 0,
1438
- display: 'flex',
1439
- flexDirection: 'column',
1440
- gap: 4,
1441
- },
1442
-
1443
- sessionHeader: {
1444
- display: 'flex',
1445
- alignItems: 'center',
1446
- justifyContent: 'space-between',
1447
- gap: 8,
1448
- },
1449
-
1450
- sessionName: {
1451
- fontSize: 14,
1452
- fontWeight: 500,
1453
- color: 'var(--text-primary)',
1454
- whiteSpace: 'nowrap',
1455
- overflow: 'hidden',
1456
- textOverflow: 'ellipsis',
1457
- },
1458
-
1459
- sessionDate: {
1460
- fontSize: 11,
1461
- color: 'var(--text-tertiary)',
1462
- whiteSpace: 'nowrap',
1463
- flexShrink: 0,
1464
- },
1465
-
1466
- sessionMeta: {
1467
- display: 'flex',
1468
- alignItems: 'center',
1469
- gap: 12,
1470
- },
1471
-
1472
- metaItem: {
1473
- display: 'flex',
1474
- alignItems: 'center',
1475
- gap: 4,
1476
- fontSize: 12,
1477
- color: 'var(--text-secondary)',
1478
- },
1479
-
1480
- transcriptionPreview: {
1481
- margin: 0,
1482
- fontSize: 12,
1483
- color: 'var(--text-tertiary)',
1484
- lineHeight: 1.4,
1485
- overflow: 'hidden',
1486
- textOverflow: 'ellipsis',
1487
- whiteSpace: 'nowrap',
1488
- },
1489
-
1490
- actionButtons: {
1491
- display: 'flex',
1492
- alignItems: 'center',
1493
- gap: 4,
1494
- marginLeft: 8,
1495
- flexShrink: 0,
1496
- },
1497
-
1498
- actionButton: {
1499
- width: 32,
1500
- height: 32,
1501
- display: 'flex',
1502
- alignItems: 'center',
1503
- justifyContent: 'center',
1504
- backgroundColor: 'var(--surface-inset)',
1505
- border: 'none',
1506
- borderRadius: 6,
1507
- color: 'var(--text-secondary)',
1508
- cursor: 'pointer',
1509
- transition: 'all 0.15s ease',
1510
- },
1511
-
1512
- // Context Menu
1513
- contextMenu: {
1514
- position: 'fixed',
1515
- minWidth: 160,
1516
- backgroundColor: 'var(--bg-elevated)',
1517
- border: '1px solid var(--border-strong)',
1518
- borderRadius: 8,
1519
- padding: 4,
1520
- zIndex: 200,
1521
- boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.5)',
1522
- },
1523
-
1524
- contextMenuItem: {
1525
- display: 'flex',
1526
- alignItems: 'center',
1527
- gap: 10,
1528
- width: '100%',
1529
- padding: '8px 12px',
1530
- backgroundColor: 'transparent',
1531
- border: 'none',
1532
- borderRadius: 4,
1533
- color: 'var(--text-primary)',
1534
- fontSize: 13,
1535
- textAlign: 'left',
1536
- cursor: 'pointer',
1537
- transition: 'background-color 0.15s ease',
1538
- },
1539
-
1540
- contextMenuDivider: {
1541
- height: 1,
1542
- backgroundColor: 'var(--border-subtle)',
1543
- margin: '4px 0',
1544
- },
1545
-
1546
- // Delete Dialog
1547
- dialogOverlay: {
1548
- position: 'fixed',
1549
- inset: 0,
1550
- display: 'flex',
1551
- alignItems: 'center',
1552
- justifyContent: 'center',
1553
- zIndex: 300,
1554
- },
1555
-
1556
- dialogBackdrop: {
1557
- position: 'absolute',
1558
- inset: 0,
1559
- backgroundColor: 'var(--bg-overlay)',
1560
- },
1561
-
1562
- dialog: {
1563
- position: 'relative',
1564
- width: 320,
1565
- backgroundColor: 'var(--bg-elevated)',
1566
- borderRadius: 12,
1567
- padding: 24,
1568
- textAlign: 'center',
1569
- boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
1570
- },
1571
-
1572
- dialogIcon: {
1573
- width: 56,
1574
- height: 56,
1575
- margin: '0 auto 16px',
1576
- display: 'flex',
1577
- alignItems: 'center',
1578
- justifyContent: 'center',
1579
- backgroundColor: 'var(--status-error-subtle)',
1580
- borderRadius: '50%',
1581
- },
1582
-
1583
- dialogTitle: {
1584
- fontSize: 16,
1585
- fontWeight: 600,
1586
- color: 'var(--text-primary)',
1587
- margin: '0 0 8px',
1588
- },
1589
-
1590
- dialogMessage: {
1591
- fontSize: 13,
1592
- color: 'var(--text-secondary)',
1593
- margin: '0 0 20px',
1594
- lineHeight: 1.5,
1595
- },
1596
-
1597
- dialogButtons: {
1598
- display: 'flex',
1599
- gap: 8,
1600
- },
1601
-
1602
- dialogCancelButton: {
1603
- flex: 1,
1604
- padding: '10px 16px',
1605
- backgroundColor: 'var(--surface-inset)',
1606
- border: '1px solid var(--border-strong)',
1607
- borderRadius: 8,
1608
- color: 'var(--text-secondary)',
1609
- fontSize: 13,
1610
- fontWeight: 500,
1611
- cursor: 'pointer',
1612
- transition: 'all 0.15s ease',
1613
- },
1614
-
1615
- dialogDeleteButton: {
1616
- flex: 1,
1617
- padding: '10px 16px',
1618
- backgroundColor: 'var(--status-error)',
1619
- border: 'none',
1620
- borderRadius: 8,
1621
- color: 'var(--text-inverse)',
1622
- fontSize: 13,
1623
- fontWeight: 500,
1624
- cursor: 'pointer',
1625
- transition: 'all 0.15s ease',
1626
- },
1627
-
1628
- // Empty State
1629
- emptyState: {
1630
- flex: 1,
1631
- display: 'flex',
1632
- flexDirection: 'column',
1633
- alignItems: 'center',
1634
- justifyContent: 'center',
1635
- padding: 40,
1636
- textAlign: 'center',
1637
- },
1638
-
1639
- emptyIcon: {
1640
- width: 80,
1641
- height: 80,
1642
- marginBottom: 16,
1643
- display: 'flex',
1644
- alignItems: 'center',
1645
- justifyContent: 'center',
1646
- backgroundColor: 'var(--surface-inset)',
1647
- borderRadius: '50%',
1648
- },
1649
-
1650
- emptyTitle: {
1651
- fontSize: 16,
1652
- fontWeight: 500,
1653
- color: 'var(--text-secondary)',
1654
- margin: '0 0 8px',
1655
- },
1656
-
1657
- emptyMessage: {
1658
- fontSize: 13,
1659
- color: 'var(--text-tertiary)',
1660
- margin: '0 0 16px',
1661
- maxWidth: 280,
1662
- lineHeight: 1.5,
1663
- },
1664
-
1665
- emptyClearButton: {
1666
- padding: '8px 16px',
1667
- backgroundColor: 'var(--accent-subtle)',
1668
- border: '1px solid var(--accent-muted)',
1669
- borderRadius: 6,
1670
- color: 'var(--text-link)',
1671
- fontSize: 13,
1672
- fontWeight: 500,
1673
- cursor: 'pointer',
1674
- transition: 'all 0.15s ease',
1675
- },
1676
-
1677
- // Loading
1678
- loadingContainer: {
1679
- display: 'flex',
1680
- flexDirection: 'column',
1681
- gap: 8,
1682
- },
1683
-
1684
- skeletonCard: {
1685
- display: 'flex',
1686
- alignItems: 'center',
1687
- gap: 12,
1688
- padding: 12,
1689
- backgroundColor: 'var(--surface-inset)',
1690
- borderRadius: 10,
1691
- border: '1px solid var(--border-subtle)',
1692
- },
1693
-
1694
- // Footer
1695
- footer: {
1696
- display: 'flex',
1697
- alignItems: 'center',
1698
- justifyContent: 'space-between',
1699
- padding: '12px 24px',
1700
- borderTop: '1px solid var(--border-default)',
1701
- backgroundColor: 'var(--surface-glass)',
1702
- },
1703
-
1704
- footerLeft: {
1705
- display: 'flex',
1706
- alignItems: 'center',
1707
- },
1708
-
1709
- footerHint: {
1710
- display: 'flex',
1711
- alignItems: 'center',
1712
- gap: 12,
1713
- fontSize: 11,
1714
- color: 'var(--text-tertiary)',
1715
- },
1716
-
1717
- kbd: {
1718
- display: 'inline-flex',
1719
- alignItems: 'center',
1720
- justifyContent: 'center',
1721
- minWidth: 20,
1722
- padding: '2px 6px',
1723
- marginRight: 4,
1724
- backgroundColor: 'var(--surface-inset)',
1725
- border: '1px solid var(--border-strong)',
1726
- borderRadius: 4,
1727
- fontSize: 10,
1728
- fontWeight: 500,
1729
- color: 'var(--text-secondary)',
1730
- fontFamily: 'ui-monospace, SFMono-Regular, monospace',
1731
- },
1732
-
1733
- closeFooterButton: {
1734
- padding: '8px 20px',
1735
- backgroundColor: 'var(--surface-inset)',
1736
- border: '1px solid var(--border-strong)',
1737
- borderRadius: 8,
1738
- color: 'var(--text-secondary)',
1739
- fontSize: 13,
1740
- fontWeight: 500,
1741
- cursor: 'pointer',
1742
- transition: 'all 0.15s ease',
1743
- },
1744
- };
1745
-
1746
- export default SessionHistory;