@fragments-sdk/cli 0.2.2
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 +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized UI state management for the App component.
|
|
3
|
+
* Uses useReducer for predictable state updates and better performance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useReducer, useCallback, useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
export type ActivePanel = 'code' | 'styles' | 'accessibility' | 'interactions' | 'actions' | 'graph' | 'contract';
|
|
9
|
+
|
|
10
|
+
interface AppUIState {
|
|
11
|
+
activePanel: ActivePanel;
|
|
12
|
+
panelOpen: boolean;
|
|
13
|
+
showHealthDashboard: boolean;
|
|
14
|
+
showComparison: boolean;
|
|
15
|
+
showShortcutsHelp: boolean;
|
|
16
|
+
showMatrixView: boolean;
|
|
17
|
+
showCommandPalette: boolean;
|
|
18
|
+
showMultiViewport: boolean;
|
|
19
|
+
linkCopied: boolean;
|
|
20
|
+
previewKey: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type AppUIAction =
|
|
24
|
+
| { type: 'SET_ACTIVE_PANEL'; payload: ActivePanel }
|
|
25
|
+
| { type: 'TOGGLE_PANEL' }
|
|
26
|
+
| { type: 'SET_PANEL_OPEN'; payload: boolean }
|
|
27
|
+
| { type: 'SET_HEALTH_DASHBOARD'; payload: boolean }
|
|
28
|
+
| { type: 'TOGGLE_COMPARISON' }
|
|
29
|
+
| { type: 'SET_COMPARISON'; payload: boolean }
|
|
30
|
+
| { type: 'TOGGLE_SHORTCUTS_HELP' }
|
|
31
|
+
| { type: 'SET_SHORTCUTS_HELP'; payload: boolean }
|
|
32
|
+
| { type: 'SET_MATRIX_VIEW'; payload: boolean }
|
|
33
|
+
| { type: 'SET_COMMAND_PALETTE'; payload: boolean }
|
|
34
|
+
| { type: 'SET_MULTI_VIEWPORT'; payload: boolean }
|
|
35
|
+
| { type: 'SET_LINK_COPIED'; payload: boolean }
|
|
36
|
+
| { type: 'INCREMENT_PREVIEW_KEY' }
|
|
37
|
+
| { type: 'CLOSE_ALL_MODALS' };
|
|
38
|
+
|
|
39
|
+
const initialState: AppUIState = {
|
|
40
|
+
activePanel: 'code',
|
|
41
|
+
panelOpen: true,
|
|
42
|
+
showHealthDashboard: false,
|
|
43
|
+
showComparison: true,
|
|
44
|
+
showShortcutsHelp: false,
|
|
45
|
+
showMatrixView: false,
|
|
46
|
+
showCommandPalette: false,
|
|
47
|
+
showMultiViewport: false,
|
|
48
|
+
linkCopied: false,
|
|
49
|
+
previewKey: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function appUIReducer(state: AppUIState, action: AppUIAction): AppUIState {
|
|
53
|
+
switch (action.type) {
|
|
54
|
+
case 'SET_ACTIVE_PANEL':
|
|
55
|
+
return { ...state, activePanel: action.payload };
|
|
56
|
+
case 'TOGGLE_PANEL':
|
|
57
|
+
return { ...state, panelOpen: !state.panelOpen };
|
|
58
|
+
case 'SET_PANEL_OPEN':
|
|
59
|
+
return { ...state, panelOpen: action.payload };
|
|
60
|
+
case 'SET_HEALTH_DASHBOARD':
|
|
61
|
+
return { ...state, showHealthDashboard: action.payload };
|
|
62
|
+
case 'TOGGLE_COMPARISON':
|
|
63
|
+
return { ...state, showComparison: !state.showComparison };
|
|
64
|
+
case 'SET_COMPARISON':
|
|
65
|
+
return { ...state, showComparison: action.payload };
|
|
66
|
+
case 'TOGGLE_SHORTCUTS_HELP':
|
|
67
|
+
return { ...state, showShortcutsHelp: !state.showShortcutsHelp };
|
|
68
|
+
case 'SET_SHORTCUTS_HELP':
|
|
69
|
+
return { ...state, showShortcutsHelp: action.payload };
|
|
70
|
+
case 'SET_MATRIX_VIEW':
|
|
71
|
+
// When enabling matrix view, disable multi-viewport
|
|
72
|
+
return {
|
|
73
|
+
...state,
|
|
74
|
+
showMatrixView: action.payload,
|
|
75
|
+
showMultiViewport: action.payload ? false : state.showMultiViewport,
|
|
76
|
+
};
|
|
77
|
+
case 'SET_COMMAND_PALETTE':
|
|
78
|
+
return { ...state, showCommandPalette: action.payload };
|
|
79
|
+
case 'SET_MULTI_VIEWPORT':
|
|
80
|
+
// When enabling multi-viewport, disable matrix view
|
|
81
|
+
return {
|
|
82
|
+
...state,
|
|
83
|
+
showMultiViewport: action.payload,
|
|
84
|
+
showMatrixView: action.payload ? false : state.showMatrixView,
|
|
85
|
+
};
|
|
86
|
+
case 'SET_LINK_COPIED':
|
|
87
|
+
return { ...state, linkCopied: action.payload };
|
|
88
|
+
case 'INCREMENT_PREVIEW_KEY':
|
|
89
|
+
return { ...state, previewKey: state.previewKey + 1 };
|
|
90
|
+
case 'CLOSE_ALL_MODALS':
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
showShortcutsHelp: false,
|
|
94
|
+
showCommandPalette: false,
|
|
95
|
+
};
|
|
96
|
+
default:
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useAppState() {
|
|
102
|
+
const [state, dispatch] = useReducer(appUIReducer, initialState);
|
|
103
|
+
|
|
104
|
+
const actions = useMemo(() => ({
|
|
105
|
+
setActivePanel: (panel: ActivePanel) => dispatch({ type: 'SET_ACTIVE_PANEL', payload: panel }),
|
|
106
|
+
togglePanel: () => dispatch({ type: 'TOGGLE_PANEL' }),
|
|
107
|
+
setPanelOpen: (open: boolean) => dispatch({ type: 'SET_PANEL_OPEN', payload: open }),
|
|
108
|
+
setHealthDashboard: (show: boolean) => dispatch({ type: 'SET_HEALTH_DASHBOARD', payload: show }),
|
|
109
|
+
toggleComparison: () => dispatch({ type: 'TOGGLE_COMPARISON' }),
|
|
110
|
+
setComparison: (show: boolean) => dispatch({ type: 'SET_COMPARISON', payload: show }),
|
|
111
|
+
toggleShortcutsHelp: () => dispatch({ type: 'TOGGLE_SHORTCUTS_HELP' }),
|
|
112
|
+
setShortcutsHelp: (show: boolean) => dispatch({ type: 'SET_SHORTCUTS_HELP', payload: show }),
|
|
113
|
+
setMatrixView: (show: boolean) => dispatch({ type: 'SET_MATRIX_VIEW', payload: show }),
|
|
114
|
+
setCommandPalette: (show: boolean) => dispatch({ type: 'SET_COMMAND_PALETTE', payload: show }),
|
|
115
|
+
setMultiViewport: (show: boolean) => dispatch({ type: 'SET_MULTI_VIEWPORT', payload: show }),
|
|
116
|
+
setLinkCopied: (copied: boolean) => dispatch({ type: 'SET_LINK_COPIED', payload: copied }),
|
|
117
|
+
incrementPreviewKey: () => dispatch({ type: 'INCREMENT_PREVIEW_KEY' }),
|
|
118
|
+
closeAllModals: () => dispatch({ type: 'CLOSE_ALL_MODALS' }),
|
|
119
|
+
}), []);
|
|
120
|
+
|
|
121
|
+
return { state, actions };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type { AppUIState, AppUIAction };
|
|
@@ -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('/segments/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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Shortcuts Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides global keyboard navigation for the viewer:
|
|
5
|
+
* - j/k or ↓/↑: Navigate components
|
|
6
|
+
* - [/] or ←/→: Navigate variants
|
|
7
|
+
* - 1-9: Jump to variant by number
|
|
8
|
+
* - t: Toggle preview theme
|
|
9
|
+
* - p: Toggle panel
|
|
10
|
+
* - cmd+shift+c: Copy link
|
|
11
|
+
* - ?: Show shortcuts help
|
|
12
|
+
* - Escape: Close modals/clear search
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useCallback } from "react";
|
|
16
|
+
|
|
17
|
+
export interface ShortcutActions {
|
|
18
|
+
/** Navigate to next component */
|
|
19
|
+
nextComponent: () => void;
|
|
20
|
+
/** Navigate to previous component */
|
|
21
|
+
prevComponent: () => void;
|
|
22
|
+
/** Navigate to next variant */
|
|
23
|
+
nextVariant: () => void;
|
|
24
|
+
/** Navigate to previous variant */
|
|
25
|
+
prevVariant: () => void;
|
|
26
|
+
/** Jump to variant by index (0-based) */
|
|
27
|
+
goToVariant: (index: number) => void;
|
|
28
|
+
/** Toggle preview theme */
|
|
29
|
+
toggleTheme: () => void;
|
|
30
|
+
/** Toggle panel open/closed */
|
|
31
|
+
togglePanel: () => void;
|
|
32
|
+
/** Copy shareable link */
|
|
33
|
+
copyLink: () => void;
|
|
34
|
+
/** Show shortcuts help */
|
|
35
|
+
showHelp: () => void;
|
|
36
|
+
/** Open search/command palette */
|
|
37
|
+
openSearch: () => void;
|
|
38
|
+
/** Close any open modal/dialog */
|
|
39
|
+
escape: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ShortcutConfig {
|
|
43
|
+
/** Whether shortcuts are enabled */
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
/** Number of variants available */
|
|
46
|
+
variantCount?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if an element is an input that should capture keyboard events
|
|
51
|
+
*/
|
|
52
|
+
function isInputElement(element: EventTarget | null): boolean {
|
|
53
|
+
if (!element || !(element instanceof HTMLElement)) return false;
|
|
54
|
+
|
|
55
|
+
const tagName = element.tagName.toLowerCase();
|
|
56
|
+
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (element.isContentEditable) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if inside Monaco Editor
|
|
65
|
+
if (element.closest('.monaco-editor')) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hook to register keyboard shortcuts
|
|
74
|
+
*/
|
|
75
|
+
export function useKeyboardShortcuts(
|
|
76
|
+
actions: Partial<ShortcutActions>,
|
|
77
|
+
config: ShortcutConfig = {}
|
|
78
|
+
) {
|
|
79
|
+
const { enabled = true, variantCount = 0 } = config;
|
|
80
|
+
|
|
81
|
+
const handleKeyDown = useCallback(
|
|
82
|
+
(event: KeyboardEvent) => {
|
|
83
|
+
if (!enabled) return;
|
|
84
|
+
|
|
85
|
+
// Don't capture events from input elements (except for specific shortcuts)
|
|
86
|
+
const isInput = isInputElement(event.target);
|
|
87
|
+
|
|
88
|
+
// Global shortcuts that work even in inputs
|
|
89
|
+
if (event.key === "Escape") {
|
|
90
|
+
actions.escape?.();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// cmd+shift+c: Copy link (works everywhere)
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === "c") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
actions.copyLink?.();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// cmd+k: Open search/command palette (works everywhere)
|
|
102
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
actions.openSearch?.();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Skip other shortcuts if in input
|
|
109
|
+
if (isInput) return;
|
|
110
|
+
|
|
111
|
+
// "/" also opens search (when not in input)
|
|
112
|
+
if (event.key === "/") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
actions.openSearch?.();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Navigation shortcuts
|
|
119
|
+
switch (event.key) {
|
|
120
|
+
// Component navigation
|
|
121
|
+
case "j":
|
|
122
|
+
case "ArrowDown":
|
|
123
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
actions.nextComponent?.();
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "k":
|
|
130
|
+
case "ArrowUp":
|
|
131
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
actions.prevComponent?.();
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
// Variant navigation
|
|
138
|
+
case "[":
|
|
139
|
+
case "ArrowLeft":
|
|
140
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
actions.prevVariant?.();
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "]":
|
|
147
|
+
case "ArrowRight":
|
|
148
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
actions.nextVariant?.();
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
// Number keys 1-9 for variant selection
|
|
155
|
+
case "1":
|
|
156
|
+
case "2":
|
|
157
|
+
case "3":
|
|
158
|
+
case "4":
|
|
159
|
+
case "5":
|
|
160
|
+
case "6":
|
|
161
|
+
case "7":
|
|
162
|
+
case "8":
|
|
163
|
+
case "9":
|
|
164
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
165
|
+
const index = parseInt(event.key, 10) - 1;
|
|
166
|
+
if (index < variantCount) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
actions.goToVariant?.(index);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
// Theme toggle
|
|
174
|
+
case "t":
|
|
175
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
actions.toggleTheme?.();
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
// Panel toggle
|
|
182
|
+
case "p":
|
|
183
|
+
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
actions.togglePanel?.();
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
// Help
|
|
190
|
+
case "?":
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
actions.showHelp?.();
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
[enabled, actions, variantCount]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
201
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
202
|
+
}, [handleKeyDown]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Keyboard shortcuts data for help display
|
|
207
|
+
*/
|
|
208
|
+
export const SHORTCUTS = [
|
|
209
|
+
{ keys: ["j", "↓"], description: "Next component" },
|
|
210
|
+
{ keys: ["k", "↑"], description: "Previous component" },
|
|
211
|
+
{ keys: ["[", "←"], description: "Previous variant" },
|
|
212
|
+
{ keys: ["]", "→"], description: "Next variant" },
|
|
213
|
+
{ keys: ["1-9"], description: "Go to variant" },
|
|
214
|
+
{ keys: ["t"], description: "Toggle preview theme" },
|
|
215
|
+
{ keys: ["p"], description: "Toggle panel" },
|
|
216
|
+
{ keys: ["⌘⇧C"], description: "Copy link" },
|
|
217
|
+
{ keys: ["/", "⌘K"], description: "Search" },
|
|
218
|
+
{ keys: ["?"], description: "Show shortcuts" },
|
|
219
|
+
{ keys: ["Esc"], description: "Close / Clear" },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
export default useKeyboardShortcuts;
|