@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. 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;