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.
- package/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markupr Keyboard Shortcuts Panel
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive cheatsheet and customization interface featuring:
|
|
5
|
+
* - Organized shortcuts by category (Recording, Navigation, Editing, Annotation)
|
|
6
|
+
* - Real-time search/filter functionality
|
|
7
|
+
* - Platform-aware display (Cmd on macOS, Ctrl on Windows)
|
|
8
|
+
* - Click-to-rebind with conflict detection (for customizable shortcuts)
|
|
9
|
+
* - Accessible modal with keyboard navigation
|
|
10
|
+
*
|
|
11
|
+
* Design: Follows macOS keyboard shortcut panel conventions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
interface Shortcut {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
keys: string;
|
|
25
|
+
category: ShortcutCategory;
|
|
26
|
+
customizable: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ShortcutCategory = 'Recording' | 'Navigation' | 'Editing' | 'Annotation' | 'Window';
|
|
30
|
+
|
|
31
|
+
interface KeyboardShortcutsProps {
|
|
32
|
+
isOpen: boolean;
|
|
33
|
+
onClose: () => void;
|
|
34
|
+
onRebind?: (shortcutId: string, newKeys: string) => void;
|
|
35
|
+
customBindings?: Partial<Record<string, string>>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Constants
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const CATEGORY_ORDER: ShortcutCategory[] = ['Recording', 'Navigation', 'Editing', 'Annotation', 'Window'];
|
|
43
|
+
|
|
44
|
+
const CATEGORY_ICONS: Record<ShortcutCategory, React.ReactNode> = {
|
|
45
|
+
Recording: (
|
|
46
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
47
|
+
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
|
48
|
+
<circle cx="8" cy="8" r="2.5" fill="currentColor" />
|
|
49
|
+
</svg>
|
|
50
|
+
),
|
|
51
|
+
Navigation: (
|
|
52
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
53
|
+
<path d="M8 2v12M2 8h12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
54
|
+
<path d="M5 5l3-3 3 3M5 11l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
55
|
+
</svg>
|
|
56
|
+
),
|
|
57
|
+
Editing: (
|
|
58
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
59
|
+
<path d="M11.5 2.5l2 2M2 14l1-4L12.5 .5l2 2L5 12l-4 1 1 1z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
60
|
+
</svg>
|
|
61
|
+
),
|
|
62
|
+
Annotation: (
|
|
63
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
64
|
+
<path d="M2 11l3-8 3 8M3.5 8h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
65
|
+
<path d="M12 6v6M10 10h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
66
|
+
</svg>
|
|
67
|
+
),
|
|
68
|
+
Window: (
|
|
69
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
70
|
+
<rect x="2" y="3" width="12" height="10" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
|
71
|
+
<path d="M2 6h12" stroke="currentColor" strokeWidth="1.5" />
|
|
72
|
+
<circle cx="4" cy="4.5" r="0.5" fill="currentColor" />
|
|
73
|
+
<circle cx="6" cy="4.5" r="0.5" fill="currentColor" />
|
|
74
|
+
<circle cx="8" cy="4.5" r="0.5" fill="currentColor" />
|
|
75
|
+
</svg>
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const DEFAULT_SHORTCUTS: Shortcut[] = [
|
|
80
|
+
// Recording
|
|
81
|
+
{
|
|
82
|
+
id: 'toggle-recording',
|
|
83
|
+
label: 'Start/Stop Recording',
|
|
84
|
+
description: 'Toggle feedback recording session',
|
|
85
|
+
keys: 'CmdOrCtrl+Shift+F',
|
|
86
|
+
category: 'Recording',
|
|
87
|
+
customizable: true,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'manual-screenshot',
|
|
91
|
+
label: 'Take Screenshot',
|
|
92
|
+
description: 'Capture current screen immediately',
|
|
93
|
+
keys: 'CmdOrCtrl+Shift+S',
|
|
94
|
+
category: 'Recording',
|
|
95
|
+
customizable: true,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'pause-resume',
|
|
99
|
+
label: 'Pause/Resume',
|
|
100
|
+
description: 'Temporarily pause recording',
|
|
101
|
+
keys: 'CmdOrCtrl+Shift+P',
|
|
102
|
+
category: 'Recording',
|
|
103
|
+
customizable: true,
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Navigation
|
|
107
|
+
{
|
|
108
|
+
id: 'open-settings',
|
|
109
|
+
label: 'Open Settings',
|
|
110
|
+
description: 'Open preferences panel',
|
|
111
|
+
keys: 'CmdOrCtrl+,',
|
|
112
|
+
category: 'Navigation',
|
|
113
|
+
customizable: false,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'open-history',
|
|
117
|
+
label: 'Session History',
|
|
118
|
+
description: 'View past recording sessions',
|
|
119
|
+
keys: 'CmdOrCtrl+H',
|
|
120
|
+
category: 'Navigation',
|
|
121
|
+
customizable: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'show-shortcuts',
|
|
125
|
+
label: 'Keyboard Shortcuts',
|
|
126
|
+
description: 'Show this panel',
|
|
127
|
+
keys: 'CmdOrCtrl+/',
|
|
128
|
+
category: 'Navigation',
|
|
129
|
+
customizable: false,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'close-dialog',
|
|
133
|
+
label: 'Close Dialog',
|
|
134
|
+
description: 'Close current modal or panel',
|
|
135
|
+
keys: 'Escape',
|
|
136
|
+
category: 'Navigation',
|
|
137
|
+
customizable: false,
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Editing
|
|
141
|
+
{
|
|
142
|
+
id: 'delete-item',
|
|
143
|
+
label: 'Delete Selected',
|
|
144
|
+
description: 'Remove selected feedback item',
|
|
145
|
+
keys: 'Backspace',
|
|
146
|
+
category: 'Editing',
|
|
147
|
+
customizable: false,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'edit-item',
|
|
151
|
+
label: 'Edit Item',
|
|
152
|
+
description: 'Open item for editing',
|
|
153
|
+
keys: 'Enter',
|
|
154
|
+
category: 'Editing',
|
|
155
|
+
customizable: false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'move-up',
|
|
159
|
+
label: 'Move Up',
|
|
160
|
+
description: 'Move item up in list',
|
|
161
|
+
keys: 'CmdOrCtrl+Up',
|
|
162
|
+
category: 'Editing',
|
|
163
|
+
customizable: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'move-down',
|
|
167
|
+
label: 'Move Down',
|
|
168
|
+
description: 'Move item down in list',
|
|
169
|
+
keys: 'CmdOrCtrl+Down',
|
|
170
|
+
category: 'Editing',
|
|
171
|
+
customizable: false,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'undo',
|
|
175
|
+
label: 'Undo',
|
|
176
|
+
description: 'Undo last action',
|
|
177
|
+
keys: 'CmdOrCtrl+Z',
|
|
178
|
+
category: 'Editing',
|
|
179
|
+
customizable: false,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'redo',
|
|
183
|
+
label: 'Redo',
|
|
184
|
+
description: 'Redo undone action',
|
|
185
|
+
keys: 'CmdOrCtrl+Shift+Z',
|
|
186
|
+
category: 'Editing',
|
|
187
|
+
customizable: false,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'select-all',
|
|
191
|
+
label: 'Select All',
|
|
192
|
+
description: 'Select all items',
|
|
193
|
+
keys: 'CmdOrCtrl+A',
|
|
194
|
+
category: 'Editing',
|
|
195
|
+
customizable: false,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Annotation Tools
|
|
199
|
+
{
|
|
200
|
+
id: 'tool-arrow',
|
|
201
|
+
label: 'Arrow Tool',
|
|
202
|
+
description: 'Draw arrows to highlight',
|
|
203
|
+
keys: '1',
|
|
204
|
+
category: 'Annotation',
|
|
205
|
+
customizable: false,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'tool-circle',
|
|
209
|
+
label: 'Circle Tool',
|
|
210
|
+
description: 'Draw circles to highlight',
|
|
211
|
+
keys: '2',
|
|
212
|
+
category: 'Annotation',
|
|
213
|
+
customizable: false,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'tool-rectangle',
|
|
217
|
+
label: 'Rectangle Tool',
|
|
218
|
+
description: 'Draw rectangles to highlight',
|
|
219
|
+
keys: '3',
|
|
220
|
+
category: 'Annotation',
|
|
221
|
+
customizable: false,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'tool-freehand',
|
|
225
|
+
label: 'Freehand Tool',
|
|
226
|
+
description: 'Draw freeform annotations',
|
|
227
|
+
keys: '4',
|
|
228
|
+
category: 'Annotation',
|
|
229
|
+
customizable: false,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'tool-text',
|
|
233
|
+
label: 'Text Tool',
|
|
234
|
+
description: 'Add text annotations',
|
|
235
|
+
keys: '5',
|
|
236
|
+
category: 'Annotation',
|
|
237
|
+
customizable: false,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'clear-annotations',
|
|
241
|
+
label: 'Clear Annotations',
|
|
242
|
+
description: 'Remove all annotations from current item',
|
|
243
|
+
keys: 'CmdOrCtrl+Backspace',
|
|
244
|
+
category: 'Annotation',
|
|
245
|
+
customizable: false,
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Window
|
|
249
|
+
{
|
|
250
|
+
id: 'minimize-window',
|
|
251
|
+
label: 'Minimize',
|
|
252
|
+
description: 'Minimize to dock',
|
|
253
|
+
keys: 'CmdOrCtrl+M',
|
|
254
|
+
category: 'Window',
|
|
255
|
+
customizable: false,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'quit-app',
|
|
259
|
+
label: 'Quit',
|
|
260
|
+
description: 'Exit markupr',
|
|
261
|
+
keys: 'CmdOrCtrl+Q',
|
|
262
|
+
category: 'Window',
|
|
263
|
+
customizable: false,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
// Key symbols for display
|
|
268
|
+
const KEY_SYMBOLS: Record<string, string> = {
|
|
269
|
+
CmdOrCtrl: '', // Handled separately
|
|
270
|
+
Cmd: '\u2318', // ⌘
|
|
271
|
+
Ctrl: '', // Handled separately
|
|
272
|
+
Control: '', // Handled separately
|
|
273
|
+
Shift: '\u21E7', // ⇧
|
|
274
|
+
Alt: '\u2325', // ⌥
|
|
275
|
+
Option: '\u2325',
|
|
276
|
+
Backspace: '\u232B', // ⌫
|
|
277
|
+
Delete: '\u2326', // ⌦
|
|
278
|
+
Enter: '\u23CE', // ⏎
|
|
279
|
+
Return: '\u23CE',
|
|
280
|
+
Escape: 'Esc',
|
|
281
|
+
Tab: '\u21E5', // ⇥
|
|
282
|
+
Up: '\u2191', // ↑
|
|
283
|
+
Down: '\u2193', // ↓
|
|
284
|
+
Left: '\u2190', // ←
|
|
285
|
+
Right: '\u2192', // →
|
|
286
|
+
Space: 'Space',
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Utility Functions
|
|
291
|
+
// ============================================================================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Detect if running on macOS
|
|
295
|
+
*/
|
|
296
|
+
function isMacOS(): boolean {
|
|
297
|
+
return typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Format key combination for display
|
|
302
|
+
*/
|
|
303
|
+
function formatKeys(keys: string, isMac: boolean): string[] {
|
|
304
|
+
const parts = keys.split('+');
|
|
305
|
+
|
|
306
|
+
return parts.map(part => {
|
|
307
|
+
const trimmed = part.trim();
|
|
308
|
+
|
|
309
|
+
// Handle CmdOrCtrl specially
|
|
310
|
+
if (trimmed === 'CmdOrCtrl' || trimmed === 'CommandOrControl') {
|
|
311
|
+
return isMac ? '\u2318' : 'Ctrl';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Handle Cmd/Ctrl
|
|
315
|
+
if (trimmed === 'Cmd' || trimmed === 'Command') {
|
|
316
|
+
return isMac ? '\u2318' : 'Ctrl';
|
|
317
|
+
}
|
|
318
|
+
if (trimmed === 'Ctrl' || trimmed === 'Control') {
|
|
319
|
+
return isMac ? '\u2303' : 'Ctrl'; // ⌃ on Mac
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check symbol map
|
|
323
|
+
if (KEY_SYMBOLS[trimmed]) {
|
|
324
|
+
return KEY_SYMBOLS[trimmed];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Return as-is (capitalize single letters)
|
|
328
|
+
return trimmed.length === 1 ? trimmed.toUpperCase() : trimmed;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// Sub-Components
|
|
334
|
+
// ============================================================================
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Individual key badge component
|
|
338
|
+
*/
|
|
339
|
+
function KeyBadge({ keyText }: { keyText: string }) {
|
|
340
|
+
const isSymbol = keyText.length === 1 && /[\u2300-\u23FF\u2190-\u21FF]/.test(keyText);
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<kbd
|
|
344
|
+
className={`
|
|
345
|
+
inline-flex items-center justify-center
|
|
346
|
+
min-w-[24px] h-6 px-1.5
|
|
347
|
+
bg-gray-800 border border-gray-600 rounded
|
|
348
|
+
font-mono text-xs text-gray-200
|
|
349
|
+
shadow-[0_1px_0_rgba(0,0,0,0.4),inset_0_1px_0_rgba(255,255,255,0.1)]
|
|
350
|
+
${isSymbol ? 'text-sm' : ''}
|
|
351
|
+
`}
|
|
352
|
+
>
|
|
353
|
+
{keyText}
|
|
354
|
+
</kbd>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Shortcut row component
|
|
360
|
+
*/
|
|
361
|
+
interface ShortcutRowProps {
|
|
362
|
+
shortcut: Shortcut;
|
|
363
|
+
isMac: boolean;
|
|
364
|
+
isEditing: boolean;
|
|
365
|
+
onStartEdit: () => void;
|
|
366
|
+
onCancelEdit: () => void;
|
|
367
|
+
onSaveEdit: (newKeys: string) => void;
|
|
368
|
+
recordedKeys: string | null;
|
|
369
|
+
conflict: string | null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function ShortcutRow({
|
|
373
|
+
shortcut,
|
|
374
|
+
isMac,
|
|
375
|
+
isEditing,
|
|
376
|
+
onStartEdit,
|
|
377
|
+
onCancelEdit,
|
|
378
|
+
onSaveEdit,
|
|
379
|
+
recordedKeys,
|
|
380
|
+
conflict,
|
|
381
|
+
}: ShortcutRowProps) {
|
|
382
|
+
const keyParts = formatKeys(recordedKeys || shortcut.keys, isMac);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div
|
|
386
|
+
className={`
|
|
387
|
+
flex items-center justify-between py-2.5 px-3 rounded-lg
|
|
388
|
+
transition-colors duration-150
|
|
389
|
+
${isEditing
|
|
390
|
+
? 'bg-blue-500/20 ring-1 ring-blue-500/50'
|
|
391
|
+
: 'hover:bg-gray-800/50'
|
|
392
|
+
}
|
|
393
|
+
${shortcut.customizable ? 'cursor-pointer' : ''}
|
|
394
|
+
`}
|
|
395
|
+
onClick={shortcut.customizable && !isEditing ? onStartEdit : undefined}
|
|
396
|
+
>
|
|
397
|
+
<div className="flex-1 min-w-0">
|
|
398
|
+
<div className="flex items-center gap-2">
|
|
399
|
+
<span className="text-white text-sm font-medium">{shortcut.label}</span>
|
|
400
|
+
{shortcut.customizable && !isEditing && (
|
|
401
|
+
<span className="text-[10px] text-gray-500 uppercase tracking-wider">
|
|
402
|
+
click to edit
|
|
403
|
+
</span>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
{shortcut.description && (
|
|
407
|
+
<p className="text-xs text-gray-400 mt-0.5 truncate">{shortcut.description}</p>
|
|
408
|
+
)}
|
|
409
|
+
{isEditing && conflict && (
|
|
410
|
+
<p className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
|
411
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
|
412
|
+
<path d="M6 0l6 11H0L6 0zm0 4.5v3m0 1.5v1" />
|
|
413
|
+
</svg>
|
|
414
|
+
Conflicts with: {conflict}
|
|
415
|
+
</p>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div className="flex items-center gap-1.5 ml-3">
|
|
420
|
+
{isEditing ? (
|
|
421
|
+
<div className="flex items-center gap-2">
|
|
422
|
+
<div className="flex items-center gap-1 px-2 py-1 bg-gray-700 rounded border border-gray-600 min-w-[80px]">
|
|
423
|
+
{recordedKeys ? (
|
|
424
|
+
keyParts.map((key, i) => (
|
|
425
|
+
<React.Fragment key={i}>
|
|
426
|
+
{i > 0 && <span className="text-gray-500 text-xs">+</span>}
|
|
427
|
+
<KeyBadge keyText={key} />
|
|
428
|
+
</React.Fragment>
|
|
429
|
+
))
|
|
430
|
+
) : (
|
|
431
|
+
<span className="text-gray-400 text-xs animate-pulse">Press keys...</span>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
<button
|
|
435
|
+
onClick={(e) => {
|
|
436
|
+
e.stopPropagation();
|
|
437
|
+
onCancelEdit();
|
|
438
|
+
}}
|
|
439
|
+
className="p-1 text-gray-400 hover:text-white"
|
|
440
|
+
title="Cancel"
|
|
441
|
+
>
|
|
442
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
443
|
+
<path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
444
|
+
</svg>
|
|
445
|
+
</button>
|
|
446
|
+
{recordedKeys && !conflict && (
|
|
447
|
+
<button
|
|
448
|
+
onClick={(e) => {
|
|
449
|
+
e.stopPropagation();
|
|
450
|
+
onSaveEdit(recordedKeys);
|
|
451
|
+
}}
|
|
452
|
+
className="p-1 text-green-400 hover:text-green-300"
|
|
453
|
+
title="Save"
|
|
454
|
+
>
|
|
455
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
456
|
+
<path d="M2 7l4 4 6-8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
457
|
+
</svg>
|
|
458
|
+
</button>
|
|
459
|
+
)}
|
|
460
|
+
</div>
|
|
461
|
+
) : (
|
|
462
|
+
keyParts.map((key, i) => (
|
|
463
|
+
<KeyBadge key={i} keyText={key} />
|
|
464
|
+
))
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// Main Component
|
|
473
|
+
// ============================================================================
|
|
474
|
+
|
|
475
|
+
export function KeyboardShortcuts({
|
|
476
|
+
isOpen,
|
|
477
|
+
onClose,
|
|
478
|
+
onRebind,
|
|
479
|
+
customBindings = {},
|
|
480
|
+
}: KeyboardShortcutsProps) {
|
|
481
|
+
const [search, setSearch] = useState('');
|
|
482
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
483
|
+
const [recordedKeys, setRecordedKeys] = useState<string | null>(null);
|
|
484
|
+
const [conflict, setConflict] = useState<string | null>(null);
|
|
485
|
+
|
|
486
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
487
|
+
const isMac = useMemo(() => isMacOS(), []);
|
|
488
|
+
|
|
489
|
+
// Merge default shortcuts with custom bindings
|
|
490
|
+
const shortcuts = useMemo(() => {
|
|
491
|
+
return DEFAULT_SHORTCUTS.map(shortcut => ({
|
|
492
|
+
...shortcut,
|
|
493
|
+
keys: customBindings[shortcut.id] || shortcut.keys,
|
|
494
|
+
}));
|
|
495
|
+
}, [customBindings]);
|
|
496
|
+
|
|
497
|
+
// Filter shortcuts by search
|
|
498
|
+
const filteredShortcuts = useMemo(() => {
|
|
499
|
+
if (!search.trim()) return shortcuts;
|
|
500
|
+
|
|
501
|
+
const searchLower = search.toLowerCase();
|
|
502
|
+
return shortcuts.filter(s =>
|
|
503
|
+
s.label.toLowerCase().includes(searchLower) ||
|
|
504
|
+
s.category.toLowerCase().includes(searchLower) ||
|
|
505
|
+
(s.description?.toLowerCase().includes(searchLower))
|
|
506
|
+
);
|
|
507
|
+
}, [shortcuts, search]);
|
|
508
|
+
|
|
509
|
+
// Group shortcuts by category
|
|
510
|
+
const groupedShortcuts = useMemo(() => {
|
|
511
|
+
const groups: Record<ShortcutCategory, Shortcut[]> = {
|
|
512
|
+
Recording: [],
|
|
513
|
+
Navigation: [],
|
|
514
|
+
Editing: [],
|
|
515
|
+
Annotation: [],
|
|
516
|
+
Window: [],
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
filteredShortcuts.forEach(shortcut => {
|
|
520
|
+
groups[shortcut.category].push(shortcut);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return groups;
|
|
524
|
+
}, [filteredShortcuts]);
|
|
525
|
+
|
|
526
|
+
// Check for key conflicts
|
|
527
|
+
const checkConflict = useCallback((newKeys: string, excludeId: string): string | null => {
|
|
528
|
+
const conflict = shortcuts.find(s =>
|
|
529
|
+
s.id !== excludeId &&
|
|
530
|
+
s.keys.toLowerCase() === newKeys.toLowerCase()
|
|
531
|
+
);
|
|
532
|
+
return conflict?.label || null;
|
|
533
|
+
}, [shortcuts]);
|
|
534
|
+
|
|
535
|
+
// Handle key recording for rebinding
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (!editingId) return;
|
|
538
|
+
|
|
539
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
e.stopPropagation();
|
|
542
|
+
|
|
543
|
+
// Build key combination
|
|
544
|
+
const parts: string[] = [];
|
|
545
|
+
|
|
546
|
+
if (e.metaKey || e.ctrlKey) {
|
|
547
|
+
parts.push('CmdOrCtrl');
|
|
548
|
+
}
|
|
549
|
+
if (e.shiftKey) {
|
|
550
|
+
parts.push('Shift');
|
|
551
|
+
}
|
|
552
|
+
if (e.altKey) {
|
|
553
|
+
parts.push('Alt');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Add the actual key
|
|
557
|
+
const key = e.key;
|
|
558
|
+
if (!['Meta', 'Control', 'Shift', 'Alt'].includes(key)) {
|
|
559
|
+
parts.push(key.length === 1 ? key.toUpperCase() : key);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (parts.length > 0 && !['Meta', 'Control', 'Shift', 'Alt'].includes(key)) {
|
|
563
|
+
const newKeys = parts.join('+');
|
|
564
|
+
setRecordedKeys(newKeys);
|
|
565
|
+
setConflict(checkConflict(newKeys, editingId));
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
570
|
+
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
|
571
|
+
}, [editingId, checkConflict]);
|
|
572
|
+
|
|
573
|
+
// Handle escape to close or cancel editing
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
576
|
+
if (e.key === 'Escape') {
|
|
577
|
+
if (editingId) {
|
|
578
|
+
setEditingId(null);
|
|
579
|
+
setRecordedKeys(null);
|
|
580
|
+
setConflict(null);
|
|
581
|
+
} else {
|
|
582
|
+
onClose();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
if (isOpen && !editingId) {
|
|
588
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
592
|
+
}, [isOpen, editingId, onClose]);
|
|
593
|
+
|
|
594
|
+
// Focus search on open
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
if (isOpen) {
|
|
597
|
+
// Small delay to allow animation
|
|
598
|
+
const timer = setTimeout(() => {
|
|
599
|
+
searchInputRef.current?.focus();
|
|
600
|
+
}, 100);
|
|
601
|
+
return () => clearTimeout(timer);
|
|
602
|
+
} else {
|
|
603
|
+
// Reset state on close
|
|
604
|
+
setSearch('');
|
|
605
|
+
setEditingId(null);
|
|
606
|
+
setRecordedKeys(null);
|
|
607
|
+
setConflict(null);
|
|
608
|
+
}
|
|
609
|
+
}, [isOpen]);
|
|
610
|
+
|
|
611
|
+
// Handlers
|
|
612
|
+
const handleStartEdit = (id: string) => {
|
|
613
|
+
setEditingId(id);
|
|
614
|
+
setRecordedKeys(null);
|
|
615
|
+
setConflict(null);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const handleCancelEdit = () => {
|
|
619
|
+
setEditingId(null);
|
|
620
|
+
setRecordedKeys(null);
|
|
621
|
+
setConflict(null);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const handleSaveEdit = (shortcutId: string, newKeys: string) => {
|
|
625
|
+
if (onRebind && !conflict) {
|
|
626
|
+
onRebind(shortcutId, newKeys);
|
|
627
|
+
}
|
|
628
|
+
setEditingId(null);
|
|
629
|
+
setRecordedKeys(null);
|
|
630
|
+
setConflict(null);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
if (!isOpen) return null;
|
|
634
|
+
|
|
635
|
+
return (
|
|
636
|
+
<div
|
|
637
|
+
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
|
|
638
|
+
onClick={onClose}
|
|
639
|
+
role="dialog"
|
|
640
|
+
aria-modal="true"
|
|
641
|
+
aria-labelledby="shortcuts-title"
|
|
642
|
+
>
|
|
643
|
+
<div
|
|
644
|
+
className="
|
|
645
|
+
bg-gray-900 border border-gray-700 rounded-2xl
|
|
646
|
+
w-full max-w-2xl mx-4
|
|
647
|
+
max-h-[80vh] overflow-hidden
|
|
648
|
+
flex flex-col
|
|
649
|
+
shadow-2xl shadow-black/50
|
|
650
|
+
animate-in fade-in zoom-in-95 duration-200
|
|
651
|
+
"
|
|
652
|
+
onClick={e => e.stopPropagation()}
|
|
653
|
+
>
|
|
654
|
+
{/* Header */}
|
|
655
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-800">
|
|
656
|
+
<h2 id="shortcuts-title" className="text-lg font-semibold text-white">
|
|
657
|
+
Keyboard Shortcuts
|
|
658
|
+
</h2>
|
|
659
|
+
<button
|
|
660
|
+
onClick={onClose}
|
|
661
|
+
className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
|
|
662
|
+
aria-label="Close"
|
|
663
|
+
>
|
|
664
|
+
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
665
|
+
<path
|
|
666
|
+
d="M4 4l10 10M14 4L4 14"
|
|
667
|
+
stroke="currentColor"
|
|
668
|
+
strokeWidth="1.5"
|
|
669
|
+
strokeLinecap="round"
|
|
670
|
+
/>
|
|
671
|
+
</svg>
|
|
672
|
+
</button>
|
|
673
|
+
</div>
|
|
674
|
+
|
|
675
|
+
{/* Search */}
|
|
676
|
+
<div className="px-6 py-3 border-b border-gray-800">
|
|
677
|
+
<div className="relative">
|
|
678
|
+
<svg
|
|
679
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
|
680
|
+
fill="none"
|
|
681
|
+
viewBox="0 0 16 16"
|
|
682
|
+
>
|
|
683
|
+
<path
|
|
684
|
+
d="M7 12A5 5 0 107 2a5 5 0 000 10zM14 14l-3.5-3.5"
|
|
685
|
+
stroke="currentColor"
|
|
686
|
+
strokeWidth="1.5"
|
|
687
|
+
strokeLinecap="round"
|
|
688
|
+
/>
|
|
689
|
+
</svg>
|
|
690
|
+
<input
|
|
691
|
+
ref={searchInputRef}
|
|
692
|
+
type="text"
|
|
693
|
+
value={search}
|
|
694
|
+
onChange={e => setSearch(e.target.value)}
|
|
695
|
+
placeholder="Search shortcuts..."
|
|
696
|
+
className="
|
|
697
|
+
w-full bg-gray-800 text-white text-sm
|
|
698
|
+
pl-10 pr-4 py-2 rounded-lg
|
|
699
|
+
border border-gray-700
|
|
700
|
+
focus:border-blue-500 focus:ring-1 focus:ring-blue-500/50
|
|
701
|
+
focus:outline-none
|
|
702
|
+
placeholder:text-gray-500
|
|
703
|
+
transition-colors
|
|
704
|
+
"
|
|
705
|
+
/>
|
|
706
|
+
{search && (
|
|
707
|
+
<button
|
|
708
|
+
onClick={() => setSearch('')}
|
|
709
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
|
|
710
|
+
>
|
|
711
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
712
|
+
<path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
713
|
+
</svg>
|
|
714
|
+
</button>
|
|
715
|
+
)}
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{/* Shortcuts List */}
|
|
720
|
+
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-6">
|
|
721
|
+
{filteredShortcuts.length === 0 ? (
|
|
722
|
+
<div className="text-center py-8">
|
|
723
|
+
<p className="text-gray-400">No shortcuts match “{search}”</p>
|
|
724
|
+
</div>
|
|
725
|
+
) : (
|
|
726
|
+
CATEGORY_ORDER.map(category => {
|
|
727
|
+
const categoryShortcuts = groupedShortcuts[category];
|
|
728
|
+
if (categoryShortcuts.length === 0) return null;
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<div key={category}>
|
|
732
|
+
<h3 className="flex items-center gap-2 text-xs font-medium text-gray-400 uppercase tracking-wider mb-2">
|
|
733
|
+
<span className="text-gray-500">{CATEGORY_ICONS[category]}</span>
|
|
734
|
+
{category}
|
|
735
|
+
</h3>
|
|
736
|
+
<div className="space-y-0.5">
|
|
737
|
+
{categoryShortcuts.map(shortcut => (
|
|
738
|
+
<ShortcutRow
|
|
739
|
+
key={shortcut.id}
|
|
740
|
+
shortcut={shortcut}
|
|
741
|
+
isMac={isMac}
|
|
742
|
+
isEditing={editingId === shortcut.id}
|
|
743
|
+
onStartEdit={() => handleStartEdit(shortcut.id)}
|
|
744
|
+
onCancelEdit={handleCancelEdit}
|
|
745
|
+
onSaveEdit={(newKeys) => handleSaveEdit(shortcut.id, newKeys)}
|
|
746
|
+
recordedKeys={editingId === shortcut.id ? recordedKeys : null}
|
|
747
|
+
conflict={editingId === shortcut.id ? conflict : null}
|
|
748
|
+
/>
|
|
749
|
+
))}
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
);
|
|
753
|
+
})
|
|
754
|
+
)}
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
{/* Footer */}
|
|
758
|
+
<div className="px-6 py-3 border-t border-gray-800 bg-gray-900/50">
|
|
759
|
+
<div className="flex items-center justify-between text-xs text-gray-400">
|
|
760
|
+
<span>
|
|
761
|
+
Press{' '}
|
|
762
|
+
<kbd className="px-1.5 py-0.5 bg-gray-800 rounded text-gray-300 border border-gray-700">
|
|
763
|
+
{isMac ? '\u2318' : 'Ctrl'}+/
|
|
764
|
+
</kbd>{' '}
|
|
765
|
+
anytime to show this panel
|
|
766
|
+
</span>
|
|
767
|
+
<span className="flex items-center gap-1">
|
|
768
|
+
{filteredShortcuts.filter(s => s.customizable).length > 0 && (
|
|
769
|
+
<>
|
|
770
|
+
<span className="inline-block w-2 h-2 rounded-full bg-blue-500/50"></span>
|
|
771
|
+
Click customizable shortcuts to rebind
|
|
772
|
+
</>
|
|
773
|
+
)}
|
|
774
|
+
</span>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ============================================================================
|
|
783
|
+
// Exports
|
|
784
|
+
// ============================================================================
|
|
785
|
+
|
|
786
|
+
export default KeyboardShortcuts;
|
|
787
|
+
export type { KeyboardShortcutsProps, Shortcut, ShortcutCategory };
|