@fragments-sdk/viewer 0.2.1
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/LICENSE +84 -0
- package/index.html +28 -0
- package/package.json +71 -0
- package/src/__tests__/a11y-fixes.test.ts +358 -0
- package/src/__tests__/jsx-parser.test.ts +502 -0
- package/src/__tests__/render-utils.test.ts +232 -0
- package/src/__tests__/style-utils.test.ts +404 -0
- package/src/app/index.ts +1 -0
- package/src/assets/fragments-logo.ts +4 -0
- package/src/assets/fragments_logo.png +0 -0
- package/src/components/AccessibilityPanel.tsx +1457 -0
- package/src/components/ActionCapture.tsx +172 -0
- package/src/components/ActionsPanel.tsx +332 -0
- package/src/components/AllVariantsPreview.tsx +78 -0
- package/src/components/App.tsx +604 -0
- package/src/components/BottomPanel.tsx +288 -0
- package/src/components/CodePanel.naming.test.tsx +59 -0
- package/src/components/CodePanel.tsx +118 -0
- package/src/components/CommandPalette.tsx +392 -0
- package/src/components/ComponentDocView.tsx +164 -0
- package/src/components/ComponentGraph.tsx +380 -0
- package/src/components/ComponentHeader.tsx +88 -0
- package/src/components/ContractPanel.tsx +241 -0
- package/src/components/DeviceMockup.tsx +156 -0
- package/src/components/EmptyVariantMessage.tsx +54 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/FigmaEmbed.tsx +238 -0
- package/src/components/FragmentEditor.tsx +525 -0
- package/src/components/FragmentRenderer.tsx +61 -0
- package/src/components/HeaderSearch.tsx +24 -0
- package/src/components/HealthDashboard.tsx +441 -0
- package/src/components/HmrStatusIndicator.tsx +61 -0
- package/src/components/Icons.tsx +479 -0
- package/src/components/InteractionsPanel.tsx +757 -0
- package/src/components/IsolatedPreviewFrame.tsx +390 -0
- package/src/components/IsolatedRender.tsx +113 -0
- package/src/components/KeyboardShortcutsHelp.tsx +53 -0
- package/src/components/LandingPage.tsx +420 -0
- package/src/components/Layout.tsx +27 -0
- package/src/components/LeftSidebar.tsx +472 -0
- package/src/components/LoadErrorMessage.tsx +102 -0
- package/src/components/MultiViewportPreview.tsx +527 -0
- package/src/components/NoVariantsMessage.tsx +59 -0
- package/src/components/PanelShell.tsx +161 -0
- package/src/components/PerformancePanel.tsx +304 -0
- package/src/components/PreviewArea.tsx +254 -0
- package/src/components/PreviewAside.tsx +168 -0
- package/src/components/PreviewFrameHost.tsx +304 -0
- package/src/components/PreviewToolbar.tsx +80 -0
- package/src/components/PropsEditor.tsx +506 -0
- package/src/components/PropsTable.tsx +111 -0
- package/src/components/RelationsSection.tsx +88 -0
- package/src/components/ResizablePanel.tsx +271 -0
- package/src/components/RightSidebar.tsx +102 -0
- package/src/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/components/ScreenshotButton.tsx +90 -0
- package/src/components/ShadowPreview.tsx +204 -0
- package/src/components/Sidebar.tsx +169 -0
- package/src/components/SkeletonLoader.tsx +161 -0
- package/src/components/ThemeProvider.tsx +42 -0
- package/src/components/Toast.tsx +3 -0
- package/src/components/TokenStylePanel.tsx +699 -0
- package/src/components/TopToolbar.tsx +159 -0
- package/src/components/Untitled +1 -0
- package/src/components/UsageSection.tsx +95 -0
- package/src/components/VariantMatrix.tsx +391 -0
- package/src/components/VariantRenderer.tsx +131 -0
- package/src/components/VariantTabs.tsx +40 -0
- package/src/components/ViewerHeader.tsx +69 -0
- package/src/components/ViewerStateSync.tsx +52 -0
- package/src/components/ViewportSelector.tsx +172 -0
- package/src/components/WebMCPDevTools.tsx +503 -0
- package/src/components/WebMCPIntegration.tsx +47 -0
- package/src/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/components/_future/CreatePage.tsx +835 -0
- package/src/components/viewer-utils.ts +16 -0
- package/src/composition-renderer.ts +381 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/ui.ts +166 -0
- package/src/entry.tsx +335 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useA11yCache.ts +383 -0
- package/src/hooks/useA11yService.ts +364 -0
- package/src/hooks/useActions.ts +138 -0
- package/src/hooks/useAppState.ts +147 -0
- package/src/hooks/useCompiledFragments.ts +42 -0
- package/src/hooks/useFigmaIntegration.ts +132 -0
- package/src/hooks/useHmrStatus.ts +109 -0
- package/src/hooks/useKeyboardShortcuts.ts +270 -0
- package/src/hooks/usePreviewBridge.ts +347 -0
- package/src/hooks/useScrollSpy.ts +78 -0
- package/src/hooks/useShadowStyles.ts +221 -0
- package/src/hooks/useUrlState.ts +318 -0
- package/src/hooks/useViewSettings.ts +111 -0
- package/src/intelligence/healthReport.ts +505 -0
- package/src/intelligence/styleDrift.ts +340 -0
- package/src/intelligence/usageScanner.ts +309 -0
- package/src/jsx-parser.ts +486 -0
- package/src/preview-frame-entry.tsx +25 -0
- package/src/preview-frame.html +148 -0
- package/src/render-template.html +68 -0
- package/src/render-utils.ts +311 -0
- package/src/shared/ComponentDocContent.module.scss +10 -0
- package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/shared/ComponentDocContent.tsx +274 -0
- package/src/shared/DocsHeaderBar.tsx +129 -0
- package/src/shared/DocsPageAsideHost.tsx +89 -0
- package/src/shared/DocsPageShell.tsx +124 -0
- package/src/shared/DocsSearchCommand.tsx +99 -0
- package/src/shared/DocsSidebarNav.tsx +66 -0
- package/src/shared/PropsTable.module.scss +68 -0
- package/src/shared/PropsTable.module.scss.d.ts +2 -0
- package/src/shared/PropsTable.tsx +76 -0
- package/src/shared/VariantPreviewCard.module.scss +114 -0
- package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/shared/VariantPreviewCard.tsx +137 -0
- package/src/shared/docs-data/index.ts +32 -0
- package/src/shared/docs-data/mcp-configs.ts +72 -0
- package/src/shared/docs-data/palettes.ts +75 -0
- package/src/shared/docs-data/setup-examples.ts +55 -0
- package/src/shared/docs-layout.scss +28 -0
- package/src/shared/docs-layout.scss.d.ts +2 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/types.ts +53 -0
- package/src/style-utils.ts +414 -0
- package/src/styles/globals.css +278 -0
- package/src/types/a11y.ts +197 -0
- package/src/utils/a11y-fixes.ts +509 -0
- package/src/utils/actionExport.ts +372 -0
- package/src/utils/colorSchemes.ts +201 -0
- package/src/utils/contrast.ts +246 -0
- package/src/utils/detectRelationships.ts +256 -0
- package/src/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/webmcp/analytics.ts +165 -0
- package/src/webmcp/index.ts +3 -0
- package/src/webmcp/posthog-bridge.ts +39 -0
- package/src/webmcp/runtime-tools.ts +152 -0
- package/src/webmcp/scan-utils.ts +135 -0
- package/src/webmcp/use-tool-analytics.ts +69 -0
- package/src/webmcp/viewer-state.ts +45 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma integration hook.
|
|
3
|
+
* Handles fetching Figma styles and extracting rendered component styles.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
interface FigmaStylesState {
|
|
9
|
+
status: 'idle' | 'loading' | 'success' | 'error';
|
|
10
|
+
styles?: Record<string, string>;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseFigmaIntegrationOptions {
|
|
15
|
+
figmaUrl?: string;
|
|
16
|
+
showComparison?: boolean;
|
|
17
|
+
dependencies?: unknown[]; // Dependencies that should reset styles when changed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useFigmaIntegration(options: UseFigmaIntegrationOptions = {}) {
|
|
21
|
+
const { figmaUrl, showComparison = false, dependencies = [] } = options;
|
|
22
|
+
|
|
23
|
+
const [figmaStyles, setFigmaStyles] = useState<FigmaStylesState>({ status: 'idle' });
|
|
24
|
+
const [renderedStyles, setRenderedStyles] = useState<Record<string, string> | null>(null);
|
|
25
|
+
|
|
26
|
+
// Fetch Figma styles from API
|
|
27
|
+
const fetchFigmaStyles = useCallback(async () => {
|
|
28
|
+
if (!figmaUrl) return;
|
|
29
|
+
|
|
30
|
+
setFigmaStyles({ status: 'loading' });
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch('/fragments/figma-styles', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ figmaUrl }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = await response.json();
|
|
40
|
+
|
|
41
|
+
if (result.error) {
|
|
42
|
+
setFigmaStyles({ status: 'error', error: result.error });
|
|
43
|
+
} else {
|
|
44
|
+
setFigmaStyles({ status: 'success', styles: result.styles });
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
setFigmaStyles({ status: 'error', error: 'Failed to fetch Figma styles' });
|
|
48
|
+
}
|
|
49
|
+
}, [figmaUrl]);
|
|
50
|
+
|
|
51
|
+
// Extract computed styles from rendered component
|
|
52
|
+
const extractRenderedStyles = useCallback(() => {
|
|
53
|
+
const container = document.querySelector('[data-preview-container="true"]');
|
|
54
|
+
if (!container) return;
|
|
55
|
+
|
|
56
|
+
const candidates = container.querySelectorAll('*');
|
|
57
|
+
let bestElement: HTMLElement | null = null;
|
|
58
|
+
let bestScore = -1;
|
|
59
|
+
|
|
60
|
+
const isVisibleColor = (color: string | undefined): boolean => {
|
|
61
|
+
if (!color) return false;
|
|
62
|
+
if (color === 'transparent' || color === 'rgba(0, 0, 0, 0)') return false;
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const el of candidates) {
|
|
67
|
+
const htmlEl = el as HTMLElement;
|
|
68
|
+
const styles = window.getComputedStyle(htmlEl);
|
|
69
|
+
let score = 0;
|
|
70
|
+
|
|
71
|
+
if (isVisibleColor(styles.backgroundColor)) score += 10;
|
|
72
|
+
if (styles.borderWidth && styles.borderWidth !== '0px') score += 3;
|
|
73
|
+
if (styles.boxShadow && styles.boxShadow !== 'none') score += 3;
|
|
74
|
+
|
|
75
|
+
const tagName = htmlEl.tagName.toLowerCase();
|
|
76
|
+
if (['button', 'a', 'input', 'select', 'textarea'].includes(tagName)) score += 5;
|
|
77
|
+
if (htmlEl.getAttribute('role') === 'button') score += 5;
|
|
78
|
+
|
|
79
|
+
const rect = htmlEl.getBoundingClientRect();
|
|
80
|
+
if (rect.width < 10 || rect.height < 10) score -= 10;
|
|
81
|
+
if (rect.width > 500 || rect.height > 500) score -= 3;
|
|
82
|
+
|
|
83
|
+
if (score > bestScore) {
|
|
84
|
+
bestScore = score;
|
|
85
|
+
bestElement = htmlEl;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!bestElement) return;
|
|
90
|
+
|
|
91
|
+
const styles = window.getComputedStyle(bestElement);
|
|
92
|
+
const relevantProps = [
|
|
93
|
+
'backgroundColor', 'borderColor', 'borderWidth', 'borderRadius',
|
|
94
|
+
'fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing',
|
|
95
|
+
'textAlign', 'boxShadow', 'padding', 'gap', 'opacity'
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const result: Record<string, string> = {};
|
|
99
|
+
for (const prop of relevantProps) {
|
|
100
|
+
const cssKey = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
101
|
+
const value = styles.getPropertyValue(cssKey);
|
|
102
|
+
if (value) result[prop] = value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setRenderedStyles(result);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Reset styles when dependencies change
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
setFigmaStyles({ status: 'idle' });
|
|
111
|
+
setRenderedStyles(null);
|
|
112
|
+
}, dependencies);
|
|
113
|
+
|
|
114
|
+
// Auto-fetch Figma styles when comparison is shown
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (showComparison && figmaUrl && figmaStyles.status === 'idle') {
|
|
117
|
+
fetchFigmaStyles();
|
|
118
|
+
}
|
|
119
|
+
}, [showComparison, figmaUrl, figmaStyles.status, fetchFigmaStyles]);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
figmaStyles,
|
|
123
|
+
renderedStyles,
|
|
124
|
+
fetchFigmaStyles,
|
|
125
|
+
extractRenderedStyles,
|
|
126
|
+
isLoading: figmaStyles.status === 'loading',
|
|
127
|
+
hasError: figmaStyles.status === 'error',
|
|
128
|
+
errorMessage: figmaStyles.error,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type { FigmaStylesState };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import type { HmrStatus } from '../constants/ui.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to track Vite HMR connection status.
|
|
6
|
+
* Returns the current connection status and any recent file changes.
|
|
7
|
+
*/
|
|
8
|
+
export function useHmrStatus() {
|
|
9
|
+
const [status, setStatus] = useState<HmrStatus>('connected');
|
|
10
|
+
const [lastUpdate, setLastUpdate] = useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Check if we're in a Vite environment
|
|
14
|
+
if (typeof import.meta.hot === 'undefined') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hot = import.meta.hot;
|
|
19
|
+
|
|
20
|
+
// Listen for HMR connection status
|
|
21
|
+
// Vite emits these events on the WebSocket connection
|
|
22
|
+
const handleConnect = () => {
|
|
23
|
+
setStatus('connected');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleDisconnect = () => {
|
|
27
|
+
setStatus('disconnected');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleReconnecting = () => {
|
|
31
|
+
setStatus('reconnecting');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Listen for module updates
|
|
35
|
+
const handleUpdate = (data: { type: string; path?: string }) => {
|
|
36
|
+
if (data.path) {
|
|
37
|
+
setLastUpdate(data.path);
|
|
38
|
+
// Clear the update notification after 3 seconds
|
|
39
|
+
setTimeout(() => setLastUpdate(null), 3000);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Vite's HMR API
|
|
44
|
+
// @ts-expect-error Vite internal events
|
|
45
|
+
hot.on('vite:beforeUpdate', handleUpdate);
|
|
46
|
+
|
|
47
|
+
// Listen for WebSocket events via custom events
|
|
48
|
+
// These are dispatched by Vite's client
|
|
49
|
+
window.addEventListener('vite:ws-connect', handleConnect);
|
|
50
|
+
window.addEventListener('vite:ws-disconnect', handleDisconnect);
|
|
51
|
+
|
|
52
|
+
// For Vite 5+, we can use the connection status directly
|
|
53
|
+
// Check current status
|
|
54
|
+
try {
|
|
55
|
+
// @ts-expect-error Vite internal
|
|
56
|
+
if (hot.connection?.socket?.readyState === 1) {
|
|
57
|
+
setStatus('connected');
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore - may not be available
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener('vite:ws-connect', handleConnect);
|
|
65
|
+
window.removeEventListener('vite:ws-disconnect', handleDisconnect);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Manual check for connection status every 5 seconds
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const checkConnection = () => {
|
|
72
|
+
if (typeof import.meta.hot === 'undefined') {
|
|
73
|
+
setStatus('disconnected');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// @ts-expect-error Vite internal
|
|
79
|
+
const socket = import.meta.hot.connection?.socket;
|
|
80
|
+
if (socket) {
|
|
81
|
+
switch (socket.readyState) {
|
|
82
|
+
case 0: // CONNECTING
|
|
83
|
+
setStatus('reconnecting');
|
|
84
|
+
break;
|
|
85
|
+
case 1: // OPEN
|
|
86
|
+
setStatus('connected');
|
|
87
|
+
break;
|
|
88
|
+
case 2: // CLOSING
|
|
89
|
+
case 3: // CLOSED
|
|
90
|
+
setStatus('disconnected');
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Ignore errors - HMR may not be available
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Initial check
|
|
100
|
+
checkConnection();
|
|
101
|
+
|
|
102
|
+
// Periodic check
|
|
103
|
+
const interval = setInterval(checkConnection, 5000);
|
|
104
|
+
|
|
105
|
+
return () => clearInterval(interval);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
return { status, lastUpdate };
|
|
109
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Shortcuts Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides global keyboard navigation for the viewer:
|
|
5
|
+
* - ⌘↓/⌘↑: Navigate components
|
|
6
|
+
* - ⌘←/⌘→: Navigate variants
|
|
7
|
+
* - j/k or ↓/↑: Navigate components (legacy)
|
|
8
|
+
* - [/] or ←/→: Navigate variants (legacy)
|
|
9
|
+
* - 1-9: Jump to variant by number
|
|
10
|
+
* - t: Toggle preview theme
|
|
11
|
+
* - p: Toggle panel
|
|
12
|
+
* - m: Toggle matrix view
|
|
13
|
+
* - r: Toggle responsive view
|
|
14
|
+
* - cmd+shift+c: Copy link
|
|
15
|
+
* - ?: Show shortcuts help
|
|
16
|
+
* - Escape: Close modals/clear search
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useCallback } from "react";
|
|
20
|
+
|
|
21
|
+
export interface ShortcutActions {
|
|
22
|
+
/** Navigate to next component */
|
|
23
|
+
nextComponent: () => void;
|
|
24
|
+
/** Navigate to previous component */
|
|
25
|
+
prevComponent: () => void;
|
|
26
|
+
/** Navigate to next variant */
|
|
27
|
+
nextVariant: () => void;
|
|
28
|
+
/** Navigate to previous variant */
|
|
29
|
+
prevVariant: () => void;
|
|
30
|
+
/** Jump to variant by index (0-based) */
|
|
31
|
+
goToVariant: (index: number) => void;
|
|
32
|
+
/** Toggle preview theme */
|
|
33
|
+
toggleTheme: () => void;
|
|
34
|
+
/** Toggle panel open/closed */
|
|
35
|
+
togglePanel: () => void;
|
|
36
|
+
/** Toggle matrix view */
|
|
37
|
+
toggleMatrix: () => void;
|
|
38
|
+
/** Toggle responsive/multi-viewport view */
|
|
39
|
+
toggleResponsive: () => void;
|
|
40
|
+
/** Copy shareable link */
|
|
41
|
+
copyLink: () => void;
|
|
42
|
+
/** Show shortcuts help */
|
|
43
|
+
showHelp: () => void;
|
|
44
|
+
/** Open search/command palette */
|
|
45
|
+
openSearch: () => void;
|
|
46
|
+
/** Close any open modal/dialog */
|
|
47
|
+
escape: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ShortcutConfig {
|
|
51
|
+
/** Whether shortcuts are enabled */
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
/** Number of variants available */
|
|
54
|
+
variantCount?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if an element is an input that should capture keyboard events
|
|
59
|
+
*/
|
|
60
|
+
function isInputElement(element: EventTarget | null): boolean {
|
|
61
|
+
if (!element || !(element instanceof HTMLElement)) return false;
|
|
62
|
+
|
|
63
|
+
const tagName = element.tagName.toLowerCase();
|
|
64
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (element.isContentEditable) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if inside Monaco Editor
|
|
73
|
+
if (element.closest('.monaco-editor')) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hook to register keyboard shortcuts
|
|
82
|
+
*/
|
|
83
|
+
export function useKeyboardShortcuts(
|
|
84
|
+
actions: Partial<ShortcutActions>,
|
|
85
|
+
config: ShortcutConfig = {}
|
|
86
|
+
) {
|
|
87
|
+
const { enabled = true, variantCount = 0 } = config;
|
|
88
|
+
|
|
89
|
+
const handleKeyDown = useCallback(
|
|
90
|
+
(event: KeyboardEvent) => {
|
|
91
|
+
if (!enabled) return;
|
|
92
|
+
|
|
93
|
+
// Don't capture events from input elements (except for specific shortcuts)
|
|
94
|
+
const isInput = isInputElement(event.target);
|
|
95
|
+
const isMeta = event.metaKey || event.ctrlKey;
|
|
96
|
+
|
|
97
|
+
// Global shortcuts that work even in inputs
|
|
98
|
+
if (event.key === "Escape") {
|
|
99
|
+
actions.escape?.();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// cmd+shift+c: Copy link (works everywhere)
|
|
104
|
+
if (isMeta && event.shiftKey && event.key === "c") {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
actions.copyLink?.();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// cmd+k: Open search/command palette (works everywhere)
|
|
111
|
+
if (isMeta && event.key === "k") {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
actions.openSearch?.();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// cmd+arrow: Navigation (works everywhere)
|
|
118
|
+
if (isMeta && event.key === "ArrowDown") {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
actions.nextComponent?.();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (isMeta && event.key === "ArrowUp") {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
actions.prevComponent?.();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (isMeta && event.key === "ArrowRight") {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
actions.nextVariant?.();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isMeta && event.key === "ArrowLeft") {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
actions.prevVariant?.();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Skip other shortcuts if in input
|
|
140
|
+
if (isInput) return;
|
|
141
|
+
|
|
142
|
+
// "/" also opens search (when not in input)
|
|
143
|
+
if (event.key === "/") {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
actions.openSearch?.();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Navigation shortcuts
|
|
150
|
+
switch (event.key) {
|
|
151
|
+
// Component navigation
|
|
152
|
+
case "j":
|
|
153
|
+
case "ArrowDown":
|
|
154
|
+
if (!isMeta && !event.altKey) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
actions.nextComponent?.();
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "k":
|
|
161
|
+
case "ArrowUp":
|
|
162
|
+
if (!isMeta && !event.altKey) {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
actions.prevComponent?.();
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
// Variant navigation
|
|
169
|
+
case "[":
|
|
170
|
+
case "ArrowLeft":
|
|
171
|
+
if (!isMeta && !event.altKey) {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
actions.prevVariant?.();
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case "]":
|
|
178
|
+
case "ArrowRight":
|
|
179
|
+
if (!isMeta && !event.altKey) {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
actions.nextVariant?.();
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
// Number keys 1-9 for variant selection
|
|
186
|
+
case "1":
|
|
187
|
+
case "2":
|
|
188
|
+
case "3":
|
|
189
|
+
case "4":
|
|
190
|
+
case "5":
|
|
191
|
+
case "6":
|
|
192
|
+
case "7":
|
|
193
|
+
case "8":
|
|
194
|
+
case "9":
|
|
195
|
+
if (!isMeta && !event.altKey) {
|
|
196
|
+
const index = parseInt(event.key, 10) - 1;
|
|
197
|
+
if (index < variantCount) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
actions.goToVariant?.(index);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
// Theme toggle
|
|
205
|
+
case "t":
|
|
206
|
+
if (!isMeta && !event.altKey) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
actions.toggleTheme?.();
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
// Panel toggle
|
|
213
|
+
case "p":
|
|
214
|
+
if (!isMeta && !event.altKey) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
actions.togglePanel?.();
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
// Matrix view toggle
|
|
221
|
+
case "m":
|
|
222
|
+
if (!isMeta && !event.altKey) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
actions.toggleMatrix?.();
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
// Responsive view toggle
|
|
229
|
+
case "v":
|
|
230
|
+
if (!isMeta && !event.altKey) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
actions.toggleResponsive?.();
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
// Help
|
|
237
|
+
case "?":
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
actions.showHelp?.();
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
[enabled, actions, variantCount]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
248
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
249
|
+
}, [handleKeyDown]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Keyboard shortcuts data for help display
|
|
254
|
+
*/
|
|
255
|
+
export const SHORTCUTS = [
|
|
256
|
+
{ keys: ["⌘↓"], description: "Next component" },
|
|
257
|
+
{ keys: ["⌘↑"], description: "Previous component" },
|
|
258
|
+
{ keys: ["⌘→"], description: "Next variant" },
|
|
259
|
+
{ keys: ["⌘←"], description: "Previous variant" },
|
|
260
|
+
{ keys: ["1-9"], description: "Go to variant" },
|
|
261
|
+
{ keys: ["t"], description: "Toggle preview theme" },
|
|
262
|
+
{ keys: ["p"], description: "Toggle addons panel" },
|
|
263
|
+
{ keys: ["m"], description: "Matrix view" },
|
|
264
|
+
{ keys: ["v"], description: "Responsive view" },
|
|
265
|
+
{ keys: ["/", "⌘K"], description: "Command palette" },
|
|
266
|
+
{ keys: ["?"], description: "Show shortcuts" },
|
|
267
|
+
{ keys: ["Esc"], description: "Close / Clear" },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export default useKeyboardShortcuts;
|