@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,638 @@
1
+ /**
2
+ * Main App component for the Fragments viewer.
3
+ * Refactored for better performance and maintainability.
4
+ */
5
+
6
+ import { useState, useMemo, useEffect, useCallback, useRef } from "react";
7
+ import type { SegmentDefinition } from "../../core/index.js";
8
+ import clsx from "clsx";
9
+
10
+ // Layout & Navigation
11
+ import { Layout } from "./Layout.js";
12
+ import { LeftSidebar } from "./LeftSidebar.js";
13
+ import { VariantTabs } from "./VariantTabs.js";
14
+ import { CommandPalette } from "./CommandPalette.js";
15
+ import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
16
+ import { Toast, type ToastMessage } from "./Toast.js";
17
+
18
+ // Toolbar
19
+ import { PreviewToolbar, getBackgroundStyle } from "./PreviewToolbar.js";
20
+ import { ViewportSelector } from "./ViewportSelector.js";
21
+
22
+ // Preview & Rendering
23
+ import { PreviewArea } from "./PreviewArea.js";
24
+ import { BottomPanel } from "./BottomPanel.js";
25
+ import { IsolatedRender } from "./IsolatedRender.js";
26
+ import { StoryRenderer, LoaderIndicator } from "./StoryRenderer.js";
27
+ import { HealthDashboard } from "./HealthDashboard.js";
28
+ import { useAllFigmaUrls } from "./FigmaEmbed.js";
29
+ import { ActionCapture } from "./ActionCapture.js";
30
+
31
+ // Icons
32
+ import { EmptyIcon, ExternalLinkIcon, CameraIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
33
+
34
+ // Hooks
35
+ import { useAppState } from "../hooks/useAppState.js";
36
+ import { useViewSettings } from "../hooks/useViewSettings.js";
37
+ import { useFigmaIntegration } from "../hooks/useFigmaIntegration.js";
38
+ import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
39
+ import { useActions } from "../hooks/useActions.js";
40
+ import { useUrlState, findSegmentByName, findVariantIndex } from "../hooks/useUrlState.js";
41
+ import { usePanelDock } from "./ResizablePanel.js";
42
+ import { useTheme } from "./ThemeProvider.js";
43
+
44
+ // Utilities
45
+ import { ScreenshotButton } from "./ScreenshotButton.js";
46
+
47
+ interface AppProps {
48
+ segments: Array<{ path: string; segment: SegmentDefinition }>;
49
+ }
50
+
51
+ export function App({ segments }: AppProps) {
52
+ // URL state management
53
+ const { state: urlState, setComponent: setUrlComponent, setVariant: setUrlVariant, setViewSettings: setUrlViewSettings, copyUrl } = useUrlState();
54
+
55
+ // UI state (modals, panels, view modes)
56
+ const { state: uiState, actions: uiActions } = useAppState();
57
+
58
+ // View settings (zoom, background, viewport, theme)
59
+ const viewSettings = useViewSettings({
60
+ initialState: {
61
+ zoom: urlState.zoom as any,
62
+ background: urlState.background as any,
63
+ viewport: urlState.viewport as any,
64
+ customSize: { width: urlState.customWidth, height: urlState.customHeight },
65
+ },
66
+ onZoomChange: (zoom) => setUrlViewSettings({ zoom }),
67
+ onBackgroundChange: (bg) => setUrlViewSettings({ background: bg }),
68
+ onViewportChange: (vp, size) => setUrlViewSettings({
69
+ viewport: vp,
70
+ customWidth: size?.width,
71
+ customHeight: size?.height,
72
+ }),
73
+ });
74
+
75
+ // Panel dock position
76
+ const panelDock = usePanelDock();
77
+
78
+ // Get resolved theme from ThemeProvider for iframe preview
79
+ const { resolvedTheme } = useTheme();
80
+
81
+ // Toast notifications
82
+ const [toasts, setToasts] = useState<ToastMessage[]>([]);
83
+ const addToast = useCallback((type: ToastMessage['type'], message: string, duration?: number) => {
84
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
85
+ setToasts(prev => [...prev, { id, type, message, duration }]);
86
+ }, []);
87
+ const dismissToast = useCallback((id: string) => {
88
+ setToasts(prev => prev.filter(t => t.id !== id));
89
+ }, []);
90
+
91
+ // Navigation state
92
+ const [activeSegmentPath, setActiveSegmentPath] = useState<string | null>(() => {
93
+ if (urlState.component) {
94
+ const found = findSegmentByName(segments, urlState.component);
95
+ return found?.path ?? segments[0]?.path ?? null;
96
+ }
97
+ return segments[0]?.path ?? null;
98
+ });
99
+
100
+ const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
101
+ const segment = segments.find(s => s.path === activeSegmentPath);
102
+ if (urlState.variant && segment?.segment.variants) {
103
+ return findVariantIndex(segment.segment.variants, urlState.variant);
104
+ }
105
+ return 0;
106
+ });
107
+
108
+ // Derived values
109
+ const activeSegment = useMemo(
110
+ () => segments.find((s) => s.path === activeSegmentPath),
111
+ [segments, activeSegmentPath]
112
+ );
113
+ const activeVariant = activeSegment?.segment.variants?.[activeVariantIndex];
114
+ const figmaUrl = activeVariant?.figma || activeSegment?.segment.meta.figma;
115
+
116
+ // Figma integration
117
+ const figmaIntegration = useFigmaIntegration({
118
+ figmaUrl,
119
+ showComparison: uiState.showComparison,
120
+ dependencies: [activeSegmentPath, activeVariantIndex],
121
+ });
122
+
123
+ // Actions logging
124
+ const { logs: actionLogs, logAction, clearLogs: clearActionLogs } = useActions();
125
+ const useActionsRef = useRef({ logAction });
126
+ useActionsRef.current = { logAction };
127
+
128
+ // Figma URLs for preloading
129
+ const allFigmaUrls = useAllFigmaUrls(activeSegment?.segment);
130
+
131
+ // Reset action logs on variant change
132
+ useEffect(() => {
133
+ clearActionLogs();
134
+ }, [activeSegmentPath, activeVariantIndex, clearActionLogs]);
135
+
136
+ // Extract rendered styles after component renders
137
+ useEffect(() => {
138
+ if (uiState.showComparison && activeVariant) {
139
+ const timer = setTimeout(figmaIntegration.extractRenderedStyles, 100);
140
+ return () => clearTimeout(timer);
141
+ }
142
+ }, [uiState.showComparison, activeVariant, figmaIntegration.extractRenderedStyles, uiState.previewKey]);
143
+
144
+ // Sync URL state on browser navigation
145
+ useEffect(() => {
146
+ if (urlState.component) {
147
+ const found = findSegmentByName(segments, urlState.component);
148
+ if (found && found.path !== activeSegmentPath) {
149
+ setActiveSegmentPath(found.path);
150
+ const variantIndex = findVariantIndex(found.segment.variants, urlState.variant);
151
+ setActiveVariantIndex(variantIndex);
152
+ }
153
+ }
154
+ }, [urlState.component, urlState.variant, segments, activeSegmentPath]);
155
+
156
+ // HMR toast notifications
157
+ useEffect(() => {
158
+ const hot = (import.meta as any).hot;
159
+ if (!hot) return;
160
+
161
+ const handleUpdate = (data: any) => {
162
+ if (data?.updates?.length > 0) {
163
+ const paths = data.updates.map((u: any) => u.path.split('/').pop()).join(', ');
164
+ addToast('info', `Updated: ${paths}`, 2000);
165
+ }
166
+ };
167
+
168
+ hot.on('vite:beforeUpdate', handleUpdate);
169
+ return () => hot.off?.('vite:beforeUpdate', handleUpdate);
170
+ }, [addToast]);
171
+
172
+ // Navigation handlers
173
+ const handleSelectSegment = useCallback((path: string) => {
174
+ const segment = segments.find((s) => s.path === path);
175
+ const componentName = segment?.segment.meta.name || path;
176
+ const firstVariant = segment?.segment.variants?.[0]?.name;
177
+
178
+ setActiveSegmentPath(path);
179
+ setActiveVariantIndex(0);
180
+ uiActions.setHealthDashboard(false);
181
+ setUrlComponent(componentName, firstVariant);
182
+ }, [segments, setUrlComponent, uiActions]);
183
+
184
+ const handleSelectVariant = useCallback((index: number) => {
185
+ const variantName = activeSegment?.segment.variants?.[index]?.name;
186
+ setActiveVariantIndex(index);
187
+ setUrlVariant(variantName || null);
188
+ }, [activeSegment, setUrlVariant]);
189
+
190
+ // Copy link handler
191
+ const handleCopyLink = useCallback(async () => {
192
+ const success = await copyUrl();
193
+ if (success) {
194
+ uiActions.setLinkCopied(true);
195
+ addToast('success', 'Link copied to clipboard', 2000);
196
+ setTimeout(() => uiActions.setLinkCopied(false), 2000);
197
+ }
198
+ }, [copyUrl, addToast, uiActions]);
199
+
200
+ // Sorted segment paths for keyboard navigation
201
+ const sortedSegmentPaths = useMemo(() => {
202
+ return [...segments]
203
+ .filter(s => s.segment?.meta?.name)
204
+ .sort((a, b) => a.segment.meta.name.localeCompare(b.segment.meta.name))
205
+ .map(s => s.path);
206
+ }, [segments]);
207
+
208
+ const currentSegmentIndex = sortedSegmentPaths.indexOf(activeSegmentPath || '');
209
+ const variantCount = activeSegment?.segment.variants?.length || 0;
210
+
211
+ // Keyboard shortcuts
212
+ useKeyboardShortcuts(
213
+ {
214
+ nextComponent: () => {
215
+ const nextIndex = currentSegmentIndex < sortedSegmentPaths.length - 1 ? currentSegmentIndex + 1 : 0;
216
+ handleSelectSegment(sortedSegmentPaths[nextIndex]);
217
+ },
218
+ prevComponent: () => {
219
+ const prevIndex = currentSegmentIndex > 0 ? currentSegmentIndex - 1 : sortedSegmentPaths.length - 1;
220
+ handleSelectSegment(sortedSegmentPaths[prevIndex]);
221
+ },
222
+ nextVariant: () => handleSelectVariant(activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0),
223
+ prevVariant: () => handleSelectVariant(activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1),
224
+ goToVariant: (index) => index < variantCount && handleSelectVariant(index),
225
+ toggleTheme: viewSettings.toggleTheme,
226
+ togglePanel: uiActions.togglePanel,
227
+ copyLink: handleCopyLink,
228
+ showHelp: uiActions.toggleShortcutsHelp,
229
+ openSearch: () => uiActions.setCommandPalette(true),
230
+ escape: uiActions.closeAllModals,
231
+ },
232
+ { enabled: !uiState.showShortcutsHelp, variantCount }
233
+ );
234
+
235
+ // Render variant with action logging via DOM event capture
236
+ const renderVariantWithProps = useCallback(() => {
237
+ if (!activeVariant) return null;
238
+
239
+ return (
240
+ <ActionCapture onAction={useActionsRef.current.logAction}>
241
+ <StoryRenderer variant={activeVariant}>
242
+ {(content, isLoading, error) => {
243
+ if (isLoading) return <div className="flex items-center justify-center p-8"><LoaderIndicator /></div>;
244
+ if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={activeVariant.name} hint="Check the console for the full error stack trace." />;
245
+ if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={activeVariant.name} hint="The variant's render function didn't return any JSX." />;
246
+ return content;
247
+ }}
248
+ </StoryRenderer>
249
+ </ActionCapture>
250
+ );
251
+ }, [activeVariant]);
252
+
253
+ // Check if isolated mode
254
+ const isIsolated = useMemo(() => {
255
+ const params = new URLSearchParams(window.location.search);
256
+ return params.get("isolated") === "true";
257
+ }, []);
258
+
259
+ if (isIsolated) {
260
+ return <IsolatedRender segments={segments} />;
261
+ }
262
+
263
+ return (
264
+ <>
265
+ <Toast messages={toasts} onDismiss={dismissToast} />
266
+ <KeyboardShortcutsHelp isOpen={uiState.showShortcutsHelp} onClose={() => uiActions.setShortcutsHelp(false)} />
267
+ <CommandPalette
268
+ isOpen={uiState.showCommandPalette}
269
+ onClose={() => uiActions.setCommandPalette(false)}
270
+ segments={segments}
271
+ onSelectComponent={handleSelectSegment}
272
+ onSelectVariant={(path, variantIndex) => {
273
+ handleSelectSegment(path);
274
+ setTimeout(() => handleSelectVariant(variantIndex), 0);
275
+ }}
276
+ />
277
+
278
+ <Layout
279
+ leftSidebar={
280
+ <LeftSidebar
281
+ segments={segments}
282
+ activeSegment={uiState.showHealthDashboard ? null : activeSegmentPath}
283
+ onSelect={handleSelectSegment}
284
+ showHealth={uiState.showHealthDashboard}
285
+ onHealthClick={() => {
286
+ uiActions.setHealthDashboard(true);
287
+ setActiveSegmentPath(null);
288
+ }}
289
+ />
290
+ }
291
+ >
292
+ {uiState.showHealthDashboard ? (
293
+ <div className="h-full overflow-auto bg-[--bg-primary]">
294
+ <div className="max-w-4xl mx-auto py-8 px-6">
295
+ <HealthDashboard
296
+ segments={segments}
297
+ onNavigate={(componentName) => {
298
+ const target = segments.find(s => s.segment.meta.name === componentName);
299
+ if (target) {
300
+ uiActions.setHealthDashboard(false);
301
+ handleSelectSegment(target.path);
302
+ }
303
+ }}
304
+ />
305
+ </div>
306
+ </div>
307
+ ) : activeSegment ? (
308
+ <div className={clsx("flex h-full", panelDock === "bottom" ? "flex-col" : "flex-row")}>
309
+ {/* Main Content Area */}
310
+ <div className="flex-1 flex flex-col min-w-0 min-h-0">
311
+ {/* Top Toolbar */}
312
+ <TopToolbar
313
+ segment={activeSegment}
314
+ variant={activeVariant}
315
+ viewSettings={viewSettings}
316
+ uiState={uiState}
317
+ uiActions={uiActions}
318
+ figmaUrl={figmaUrl}
319
+ linkCopied={uiState.linkCopied}
320
+ onCopyLink={handleCopyLink}
321
+ />
322
+
323
+ {/* Variant Tabs */}
324
+ {activeSegment.segment.variants && activeSegment.segment.variants.length > 0 && (
325
+ <VariantTabsBar
326
+ variants={activeSegment.segment.variants}
327
+ activeIndex={activeVariantIndex}
328
+ onSelect={handleSelectVariant}
329
+ showMatrixView={uiState.showMatrixView}
330
+ showMultiViewport={uiState.showMultiViewport}
331
+ onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
332
+ onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
333
+ />
334
+ )}
335
+
336
+ {/* Preview Area */}
337
+ <div
338
+ className="flex-1 overflow-auto relative"
339
+ style={uiState.showMatrixView ? undefined : getBackgroundStyle(viewSettings.background)}
340
+ >
341
+ {activeVariant ? (
342
+ <PreviewArea
343
+ componentName={activeSegment.segment.meta.name}
344
+ segmentPath={activeSegment.path}
345
+ variant={activeVariant}
346
+ variants={activeSegment.segment.variants}
347
+ zoom={viewSettings.zoom}
348
+ background={viewSettings.background}
349
+ viewport={viewSettings.viewport}
350
+ customSize={viewSettings.customSize}
351
+ previewTheme={resolvedTheme}
352
+ showMatrixView={uiState.showMatrixView}
353
+ showMultiViewport={uiState.showMultiViewport}
354
+ showComparison={uiState.showComparison}
355
+ figmaUrl={figmaUrl}
356
+ allFigmaUrls={allFigmaUrls}
357
+ onSelectVariant={(index) => {
358
+ uiActions.setMatrixView(false);
359
+ handleSelectVariant(index);
360
+ }}
361
+ onRetry={uiActions.incrementPreviewKey}
362
+ renderContent={renderVariantWithProps}
363
+ previewKey={`${activeSegmentPath}-${activeVariantIndex}-${uiState.previewKey}`}
364
+ />
365
+ ) : (
366
+ <NoVariantsMessage segment={activeSegment?.segment} />
367
+ )}
368
+ </div>
369
+ </div>
370
+
371
+ {/* Bottom Panel */}
372
+ {activeVariant && (
373
+ <BottomPanel
374
+ segment={activeSegment.segment}
375
+ variant={activeVariant}
376
+ segments={segments}
377
+ activePanel={uiState.activePanel}
378
+ onPanelChange={uiActions.setActivePanel}
379
+ figmaUrl={figmaUrl}
380
+ figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
381
+ renderedStyles={figmaIntegration.renderedStyles}
382
+ figmaLoading={figmaIntegration.isLoading}
383
+ figmaError={figmaIntegration.errorMessage}
384
+ onFetchFigma={figmaIntegration.fetchFigmaStyles}
385
+ onRefreshRendered={figmaIntegration.extractRenderedStyles}
386
+ actionLogs={actionLogs}
387
+ onClearActionLogs={clearActionLogs}
388
+ onNavigateToComponent={(name) => {
389
+ const target = segments.find(s => s.segment.meta.name === name);
390
+ if (target) handleSelectSegment(target.path);
391
+ }}
392
+ previewKey={uiState.previewKey}
393
+ segmentKey={`${activeSegmentPath}-${activeVariantIndex}`}
394
+ />
395
+ )}
396
+ </div>
397
+ ) : (
398
+ <div className="flex flex-col items-center justify-center h-full text-secondary">
399
+ <EmptyIcon className="w-12 h-12 mb-4 text-[--text-muted]" />
400
+ <p className="text-base font-medium text-primary">No component selected</p>
401
+ <p className="text-sm mt-1 text-tertiary">Select a component from the sidebar</p>
402
+ </div>
403
+ )}
404
+ </Layout>
405
+ </>
406
+ );
407
+ }
408
+
409
+ // Top Toolbar Component
410
+ interface TopToolbarProps {
411
+ segment: { path: string; segment: SegmentDefinition };
412
+ variant: any;
413
+ viewSettings: ReturnType<typeof useViewSettings>;
414
+ uiState: ReturnType<typeof useAppState>['state'];
415
+ uiActions: ReturnType<typeof useAppState>['actions'];
416
+ figmaUrl?: string;
417
+ linkCopied: boolean;
418
+ onCopyLink: () => void;
419
+ }
420
+
421
+ function TopToolbar({ segment, variant, viewSettings, uiState, uiActions, figmaUrl, linkCopied, onCopyLink }: TopToolbarProps) {
422
+ return (
423
+ <div className="flex items-center justify-between px-4 py-2 border-b border-[--border] bg-[--bg-secondary] flex-shrink-0">
424
+ <div className="flex items-center gap-3">
425
+ <h1 className="text-sm font-medium text-primary">{segment.segment.meta.name}</h1>
426
+ <span className="text-xs text-tertiary">{segment.segment.meta.category}</span>
427
+ </div>
428
+ <div className="flex items-center gap-2">
429
+ <PreviewToolbar
430
+ zoom={viewSettings.zoom}
431
+ background={viewSettings.background}
432
+ onZoomChange={viewSettings.setZoom}
433
+ onBackgroundChange={viewSettings.setBackground}
434
+ />
435
+ <div className="w-px h-4 bg-[--border]" />
436
+ <ViewportSelector
437
+ viewport={viewSettings.viewport}
438
+ customSize={viewSettings.customSize}
439
+ onViewportChange={viewSettings.setViewport}
440
+ onCustomSizeChange={viewSettings.setCustomSize}
441
+ />
442
+ <div className="w-px h-4 bg-[--border]" />
443
+
444
+ {figmaUrl && (
445
+ <>
446
+ <button
447
+ onClick={uiActions.toggleComparison}
448
+ className={clsx(
449
+ "p-1.5 rounded transition-colors",
450
+ uiState.showComparison
451
+ ? "text-[--color-accent] bg-[--bg-hover]"
452
+ : "text-tertiary hover:text-primary hover:bg-[--bg-hover]"
453
+ )}
454
+ title={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}
455
+ >
456
+ <CompareIcon className="w-4 h-4" />
457
+ </button>
458
+ <button
459
+ onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
460
+ className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
461
+ title="View in Figma"
462
+ >
463
+ <FigmaIcon className="w-4 h-4" />
464
+ </button>
465
+ <div className="w-px h-4 bg-[--border]" />
466
+ </>
467
+ )}
468
+
469
+ {variant && (
470
+ <>
471
+ <button
472
+ onClick={() => {
473
+ const url = new URL(window.location.href);
474
+ // Clear the hash to avoid malformed URLs
475
+ url.hash = '';
476
+ url.searchParams.set('isolated', 'true');
477
+ url.searchParams.set('component', segment.segment.meta.name);
478
+ url.searchParams.set('variant', variant.name);
479
+ if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
480
+ if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
481
+ window.open(url.toString(), '_blank', 'noopener,noreferrer');
482
+ }}
483
+ className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
484
+ title="Open in new window"
485
+ >
486
+ <ExternalLinkIcon className="w-4 h-4" />
487
+ </button>
488
+ <ScreenshotButton componentName={segment.segment.meta.name} variantName={variant.name} />
489
+ <button
490
+ onClick={onCopyLink}
491
+ className={clsx(
492
+ "p-1.5 rounded transition-colors",
493
+ linkCopied
494
+ ? "text-green-600 bg-green-100 dark:bg-green-900/30"
495
+ : "text-tertiary hover:text-primary hover:bg-[--bg-hover]"
496
+ )}
497
+ title="Copy link to share"
498
+ >
499
+ {linkCopied ? <CheckIcon className="w-4 h-4" /> : <LinkIcon className="w-4 h-4" />}
500
+ </button>
501
+ </>
502
+ )}
503
+ </div>
504
+ </div>
505
+ );
506
+ }
507
+
508
+ // Variant Tabs Bar Component
509
+ interface VariantTabsBarProps {
510
+ variants: any[];
511
+ activeIndex: number;
512
+ onSelect: (index: number) => void;
513
+ showMatrixView: boolean;
514
+ showMultiViewport: boolean;
515
+ onToggleMatrix: () => void;
516
+ onToggleMultiViewport: () => void;
517
+ }
518
+
519
+ function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showMultiViewport, onToggleMatrix, onToggleMultiViewport }: VariantTabsBarProps) {
520
+ return (
521
+ <div className="px-4 py-2 border-b border-[--border] bg-[--bg-primary] flex-shrink-0 flex items-center justify-between">
522
+ {!showMatrixView ? (
523
+ <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
524
+ ) : (
525
+ <div className="text-sm text-secondary">Showing all {variants.length} variants</div>
526
+ )}
527
+ <div className="flex items-center gap-2 ml-4">
528
+ {variants.length > 1 && (
529
+ <button
530
+ onClick={onToggleMatrix}
531
+ className={clsx(
532
+ "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors",
533
+ showMatrixView
534
+ ? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300"
535
+ : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
536
+ )}
537
+ title={showMatrixView ? "Show single variant" : "Show all variants in grid"}
538
+ >
539
+ <GridIcon className="w-4 h-4" />
540
+ {showMatrixView ? "Exit Matrix" : "Matrix"}
541
+ </button>
542
+ )}
543
+ <button
544
+ onClick={onToggleMultiViewport}
545
+ className={clsx(
546
+ "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors",
547
+ showMultiViewport
548
+ ? "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300"
549
+ : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
550
+ )}
551
+ title={showMultiViewport ? "Exit multi-viewport" : "Show at multiple screen sizes"}
552
+ >
553
+ <DevicesIcon className="w-4 h-4" />
554
+ {showMultiViewport ? "Exit Responsive" : "Responsive"}
555
+ </button>
556
+ </div>
557
+ </div>
558
+ );
559
+ }
560
+
561
+ // No variants message
562
+ interface NoVariantsMessageProps {
563
+ segment?: SegmentDefinition;
564
+ }
565
+
566
+ function NoVariantsMessage({ segment }: NoVariantsMessageProps) {
567
+ const skippedVariants = (segment?._generated as any)?.skippedVariants;
568
+
569
+ if (!skippedVariants || skippedVariants.length === 0) {
570
+ return <div className="flex items-center justify-center h-full text-secondary text-sm">No variants defined</div>;
571
+ }
572
+
573
+ return (
574
+ <div className="flex items-center justify-center h-full p-6">
575
+ <div className="p-6 bg-sky-50 dark:bg-sky-950 border border-sky-300 dark:border-sky-700 rounded-lg max-w-lg">
576
+ <div className="flex items-start gap-3">
577
+ <div className="flex-shrink-0 w-8 h-8 rounded-full bg-sky-200 dark:bg-sky-800 flex items-center justify-center">
578
+ <span className="text-sky-700 dark:text-sky-200 text-lg">ℹ</span>
579
+ </div>
580
+ <div className="flex-1 min-w-0">
581
+ <h3 className="text-sm font-semibold text-sky-900 dark:text-sky-100">
582
+ {skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
583
+ </h3>
584
+ <p className="mt-1 text-xs text-sky-800 dark:text-sky-200">
585
+ These variants couldn't be rendered because they use syntax the parser doesn't support yet:
586
+ </p>
587
+ <ul className="mt-2 space-y-1">
588
+ {skippedVariants.map((sv: any, i: number) => (
589
+ <li key={i} className="text-xs text-sky-800 dark:text-sky-200">
590
+ <span className="font-semibold">{sv.name}:</span>{' '}
591
+ <span className="text-sky-700 dark:text-sky-300">{sv.reason}</span>
592
+ </li>
593
+ ))}
594
+ </ul>
595
+ </div>
596
+ </div>
597
+ </div>
598
+ </div>
599
+ );
600
+ }
601
+
602
+ // Empty variant message
603
+ interface EmptyVariantMessageProps {
604
+ reason: string;
605
+ variantName: string;
606
+ hint?: string;
607
+ }
608
+
609
+ function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
610
+ return (
611
+ <div className="p-6 bg-amber-50 dark:bg-amber-950 border border-amber-300 dark:border-amber-700 rounded-lg max-w-md">
612
+ <div className="flex items-start gap-3">
613
+ <div className="flex-shrink-0 w-8 h-8 rounded-full bg-amber-200 dark:bg-amber-800 flex items-center justify-center">
614
+ <span className="text-amber-700 dark:text-amber-200 text-lg">⚠</span>
615
+ </div>
616
+ <div className="flex-1 min-w-0">
617
+ <h3 className="text-sm font-semibold text-amber-900 dark:text-amber-100">
618
+ Variant "{variantName}" rendered empty
619
+ </h3>
620
+ <p className="mt-1 text-xs text-amber-800 dark:text-amber-200">{reason}</p>
621
+ {hint && (
622
+ <p className="mt-2 text-xs text-amber-700 dark:text-amber-300">
623
+ <strong>Tip:</strong> {hint}
624
+ </p>
625
+ )}
626
+ <div className="mt-3 text-xs text-amber-700 dark:text-amber-300">
627
+ <strong>Common causes:</strong>
628
+ <ul className="mt-1 ml-4 list-disc space-y-0.5">
629
+ <li>Component requires props that weren't provided</li>
630
+ <li>Component renders conditionally and conditions aren't met</li>
631
+ <li>Story args reference variables that don't exist in this context</li>
632
+ </ul>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ );
638
+ }