@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,321 @@
1
+ /**
2
+ * IsolatedPreviewFrame - Parent-side iframe wrapper for CSS isolation
3
+ *
4
+ * Renders component previews inside an iframe for complete CSS isolation.
5
+ * This prevents CSS conflicts between:
6
+ * - The viewer shell and the user's component library
7
+ * - Global styles from user components
8
+ * - Theme variables with same names
9
+ */
10
+
11
+ import { memo, useRef, useEffect, useState, useCallback } from 'react';
12
+ import clsx from 'clsx';
13
+ import { usePreviewBridge, type ParentMessage } from '../hooks/usePreviewBridge.js';
14
+
15
+ /** Maximum number of retry attempts */
16
+ const MAX_RETRIES = 3;
17
+
18
+ export interface IsolatedPreviewFrameProps {
19
+ /** Segment path (file path) to render */
20
+ segmentPath: string;
21
+ /** Variant name to render */
22
+ variantName: string;
23
+ /** Props to pass to the variant render function */
24
+ props?: Record<string, unknown>;
25
+ /** Theme for the preview */
26
+ theme: 'light' | 'dark';
27
+ /** Width of the preview (CSS value) */
28
+ width?: number | string;
29
+ /** Height of the preview (CSS value) */
30
+ height?: number | string;
31
+ /** Minimum height of the preview */
32
+ minHeight?: number | string;
33
+ /** Background style for the preview container */
34
+ background?: React.CSSProperties;
35
+ /** Additional class name for the container */
36
+ className?: string;
37
+ /** Additional styles for the container */
38
+ style?: React.CSSProperties;
39
+ /** Called when content size is reported */
40
+ onContentSize?: (size: { width: number; height: number }) => void;
41
+ /** Called when an error occurs */
42
+ onError?: (error: string) => void;
43
+ /** Unique key to force re-render on variant changes */
44
+ previewKey?: string;
45
+ /** Show size indicator on hover */
46
+ showSizeIndicator?: boolean;
47
+ }
48
+
49
+ /**
50
+ * IsolatedPreviewFrame renders a component preview inside an iframe
51
+ * for complete CSS isolation from the viewer shell.
52
+ */
53
+ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
54
+ segmentPath,
55
+ variantName,
56
+ props,
57
+ theme,
58
+ width = '100%',
59
+ height = 'auto',
60
+ minHeight = 120,
61
+ background,
62
+ className = '',
63
+ style,
64
+ onContentSize,
65
+ onError,
66
+ previewKey,
67
+ showSizeIndicator = false,
68
+ }: IsolatedPreviewFrameProps) {
69
+ const iframeRef = useRef<HTMLIFrameElement>(null);
70
+ const [isLoading, setIsLoading] = useState(true);
71
+ const [frameError, setFrameError] = useState<string | null>(null);
72
+ const [retryCount, setRetryCount] = useState(0);
73
+ const [iframeKey, setIframeKey] = useState(0);
74
+ const { isReady, isRendering, lastError, contentSize, render, setTheme, clearError } = usePreviewBridge(iframeRef);
75
+ const lastRenderRef = useRef<string>('');
76
+ const isFirstLoad = useRef(true);
77
+
78
+ // Build the preview URL
79
+ const previewUrl = '/fragments/preview/';
80
+
81
+ // Handle iframe load
82
+ const handleLoad = useCallback(() => {
83
+ setIsLoading(false);
84
+ setFrameError(null);
85
+ isFirstLoad.current = false;
86
+ }, []);
87
+
88
+ // Handle iframe error
89
+ const handleError = useCallback(() => {
90
+ setIsLoading(false);
91
+ setFrameError('Failed to load preview frame');
92
+ onError?.('Failed to load preview frame');
93
+ }, [onError]);
94
+
95
+ // Handle retry
96
+ const handleRetry = useCallback(() => {
97
+ if (retryCount >= MAX_RETRIES) return;
98
+
99
+ setFrameError(null);
100
+ clearError();
101
+ setRetryCount(c => c + 1);
102
+ setIsLoading(true);
103
+ lastRenderRef.current = ''; // Force re-render
104
+ setIframeKey(k => k + 1); // Force iframe reload
105
+ }, [retryCount, clearError]);
106
+
107
+ // Send render request when ready or when render params change
108
+ useEffect(() => {
109
+ if (!isReady) return;
110
+
111
+ // Create a render key to detect changes
112
+ const renderKey = `${segmentPath}:${variantName}:${JSON.stringify(props)}:${previewKey || ''}`;
113
+ if (renderKey === lastRenderRef.current) return;
114
+ lastRenderRef.current = renderKey;
115
+
116
+ render(segmentPath, variantName, props);
117
+ }, [isReady, segmentPath, variantName, props, previewKey, render]);
118
+
119
+ // Sync theme when it changes
120
+ useEffect(() => {
121
+ if (!isReady) return;
122
+ setTheme(theme);
123
+ }, [isReady, theme, setTheme]);
124
+
125
+ // Report content size changes
126
+ useEffect(() => {
127
+ if (contentSize) {
128
+ onContentSize?.(contentSize);
129
+ }
130
+ }, [contentSize, onContentSize]);
131
+
132
+ // Report errors
133
+ useEffect(() => {
134
+ if (lastError) {
135
+ setFrameError(lastError);
136
+ onError?.(lastError);
137
+ }
138
+ }, [lastError, onError]);
139
+
140
+ // Calculate iframe dimensions
141
+ const frameWidth = typeof width === 'number' ? `${width}px` : width;
142
+ const frameHeight = typeof height === 'number' ? `${height}px` : height;
143
+ const frameMinHeight = typeof minHeight === 'number' ? `${minHeight}px` : minHeight;
144
+
145
+ // Determine if we should show skeleton vs spinner
146
+ // Skeleton for initial load, spinner for subsequent renders
147
+ const showSkeleton = isLoading && isFirstLoad.current;
148
+ const showSpinner = !showSkeleton && (isLoading || isRendering);
149
+ const showContent = isReady && !isRendering && !frameError;
150
+
151
+ return (
152
+ <div
153
+ className={clsx('isolated-preview-frame group', className)}
154
+ style={{
155
+ position: 'relative',
156
+ width: frameWidth,
157
+ height: frameHeight,
158
+ minHeight: frameMinHeight,
159
+ ...background,
160
+ ...style,
161
+ }}
162
+ >
163
+ {/* Skeleton loading overlay (initial load) */}
164
+ <div
165
+ className={clsx(
166
+ 'absolute inset-0 z-10 transition-opacity duration-150',
167
+ showSkeleton ? 'opacity-100' : 'opacity-0 pointer-events-none'
168
+ )}
169
+ style={{ background: 'rgba(255, 255, 255, 0.95)' }}
170
+ >
171
+ <PreviewSkeleton />
172
+ </div>
173
+
174
+ {/* Spinner overlay (subsequent renders) */}
175
+ <div
176
+ className={clsx(
177
+ 'absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-150',
178
+ showSpinner ? 'opacity-100' : 'opacity-0 pointer-events-none'
179
+ )}
180
+ style={{ background: 'rgba(255, 255, 255, 0.8)' }}
181
+ >
182
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6b7280', fontSize: 14 }}>
183
+ <LoadingSpinner />
184
+ <span>Rendering...</span>
185
+ </div>
186
+ </div>
187
+
188
+ {/* Error overlay */}
189
+ <div
190
+ className={clsx(
191
+ 'absolute inset-0 z-10 flex items-center justify-center p-4 transition-opacity duration-150',
192
+ frameError && !isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'
193
+ )}
194
+ style={{ background: 'rgba(254, 242, 242, 0.95)' }}
195
+ >
196
+ <div
197
+ style={{
198
+ background: 'white',
199
+ border: '1px solid #fecaca',
200
+ borderRadius: 8,
201
+ padding: 16,
202
+ maxWidth: 400,
203
+ }}
204
+ >
205
+ <div style={{ color: '#dc2626', fontWeight: 500, marginBottom: 8 }}>
206
+ Preview Error
207
+ </div>
208
+ <div style={{ color: '#991b1b', fontSize: 13, marginBottom: retryCount < MAX_RETRIES ? 12 : 0 }}>
209
+ {frameError}
210
+ </div>
211
+ {retryCount < MAX_RETRIES && (
212
+ <button
213
+ onClick={handleRetry}
214
+ style={{
215
+ padding: '6px 12px',
216
+ fontSize: 13,
217
+ fontWeight: 500,
218
+ color: 'white',
219
+ background: '#dc2626',
220
+ border: 'none',
221
+ borderRadius: 6,
222
+ cursor: 'pointer',
223
+ }}
224
+ >
225
+ Retry ({MAX_RETRIES - retryCount} remaining)
226
+ </button>
227
+ )}
228
+ </div>
229
+ </div>
230
+
231
+ {/* The iframe */}
232
+ <iframe
233
+ key={iframeKey}
234
+ ref={iframeRef}
235
+ src={previewUrl}
236
+ title={`Preview: ${variantName}`}
237
+ onLoad={handleLoad}
238
+ onError={handleError}
239
+ className={clsx(
240
+ 'transition-opacity duration-150',
241
+ showContent ? 'opacity-100' : 'opacity-0'
242
+ )}
243
+ style={{
244
+ width: '100%',
245
+ height: '100%',
246
+ border: 'none',
247
+ display: 'block',
248
+ background: 'transparent',
249
+ }}
250
+ // Security attributes
251
+ sandbox="allow-scripts allow-same-origin"
252
+ />
253
+
254
+ {/* Size indicator */}
255
+ {showSizeIndicator && contentSize && (
256
+ <div
257
+ className="absolute bottom-1 right-1 px-1.5 py-0.5 rounded font-mono
258
+ opacity-0 group-hover:opacity-100 transition-opacity duration-150"
259
+ style={{
260
+ background: 'rgba(0, 0, 0, 0.5)',
261
+ color: 'white',
262
+ fontSize: 10,
263
+ }}
264
+ >
265
+ {contentSize.width} × {contentSize.height}px
266
+ </div>
267
+ )}
268
+ </div>
269
+ );
270
+ });
271
+
272
+ /**
273
+ * Skeleton loading placeholder
274
+ */
275
+ function PreviewSkeleton() {
276
+ return (
277
+ <div className="animate-pulse p-4">
278
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-3" />
279
+ <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
280
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3" />
281
+ </div>
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Simple loading spinner component
287
+ */
288
+ function LoadingSpinner() {
289
+ return (
290
+ <svg
291
+ style={{
292
+ width: 20,
293
+ height: 20,
294
+ animation: 'spin 0.8s linear infinite',
295
+ }}
296
+ viewBox="0 0 24 24"
297
+ fill="none"
298
+ >
299
+ <style>
300
+ {`@keyframes spin { to { transform: rotate(360deg); } }`}
301
+ </style>
302
+ <circle
303
+ cx="12"
304
+ cy="12"
305
+ r="10"
306
+ stroke="#e5e7eb"
307
+ strokeWidth="2"
308
+ fill="none"
309
+ />
310
+ <path
311
+ d="M12 2a10 10 0 0 1 10 10"
312
+ stroke="#3b82f6"
313
+ strokeWidth="2"
314
+ strokeLinecap="round"
315
+ fill="none"
316
+ />
317
+ </svg>
318
+ );
319
+ }
320
+
321
+ export default IsolatedPreviewFrame;
@@ -0,0 +1,111 @@
1
+ import { useMemo, useEffect, useState } from "react";
2
+ import type { SegmentDefinition } from "../../core/index.js";
3
+ import { VariantRenderer } from "./VariantRenderer.js";
4
+ import { getBackgroundStyle, type BackgroundOption, type ZoomLevel } from "./PreviewToolbar.js";
5
+
6
+ interface IsolatedRenderProps {
7
+ segments: Array<{ path: string; segment: SegmentDefinition }>;
8
+ }
9
+
10
+ /**
11
+ * Isolated render component for screenshot capture and standalone viewing.
12
+ * Renders a single variant with minimal UI for visual testing.
13
+ * URL params: ?isolated=true&component=Name&variant=VariantName&zoom=100&bg=white&theme=light
14
+ */
15
+ export function IsolatedRender({ segments }: IsolatedRenderProps) {
16
+ const [ready, setReady] = useState(false);
17
+
18
+ // Parse query parameters
19
+ const params = useMemo(() => {
20
+ const searchParams = new URLSearchParams(window.location.search);
21
+ const zoomParam = parseInt(searchParams.get("zoom") || "100", 10);
22
+ const bgParam = searchParams.get("bg") || "white";
23
+ return {
24
+ component: searchParams.get("component"),
25
+ variant: searchParams.get("variant"),
26
+ theme: searchParams.get("theme") || "light",
27
+ zoom: [50, 75, 100, 150, 200].includes(zoomParam) ? zoomParam as ZoomLevel : 100 as ZoomLevel,
28
+ background: ["white", "black", "checkerboard", "transparent"].includes(bgParam)
29
+ ? bgParam as BackgroundOption
30
+ : "white" as BackgroundOption,
31
+ };
32
+ }, []);
33
+
34
+ // Find the matching segment and variant
35
+ const match = useMemo(() => {
36
+ if (!params.component || !params.variant) {
37
+ return null;
38
+ }
39
+
40
+ const segment = segments.find(
41
+ (s) => s.segment.meta.name === params.component
42
+ );
43
+
44
+ if (!segment) {
45
+ return null;
46
+ }
47
+
48
+ const variant = segment.segment.variants.find(
49
+ (v) => v.name === params.variant
50
+ );
51
+
52
+ if (!variant) {
53
+ return null;
54
+ }
55
+
56
+ return { segment: segment.segment, variant };
57
+ }, [segments, params]);
58
+
59
+ // Apply theme
60
+ useEffect(() => {
61
+ document.documentElement.setAttribute("data-theme", params.theme);
62
+
63
+ // Signal ready after a short delay for fonts and styles to settle
64
+ const timer = setTimeout(() => {
65
+ setReady(true);
66
+ }, 50);
67
+
68
+ return () => clearTimeout(timer);
69
+ }, [params.theme]);
70
+
71
+ // Error state - missing component or variant
72
+ if (!params.component || !params.variant) {
73
+ return (
74
+ <div className="p-4 text-red-500 font-mono text-sm">
75
+ Error: Missing component or variant parameter
76
+ <pre className="mt-2 text-xs">
77
+ Required: ?component=ComponentName&variant=VariantName
78
+ </pre>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ // Error state - component/variant not found
84
+ if (!match) {
85
+ return (
86
+ <div className="p-4 text-red-500 font-mono text-sm">
87
+ Error: Component "{params.component}" variant "{params.variant}" not
88
+ found
89
+ </div>
90
+ );
91
+ }
92
+
93
+ // Render the variant in isolation
94
+ return (
95
+ <div
96
+ id="isolated-render"
97
+ data-ready={ready}
98
+ className="min-h-screen p-8 flex items-center justify-center"
99
+ style={getBackgroundStyle(params.background)}
100
+ >
101
+ <div
102
+ style={{
103
+ transform: `scale(${params.zoom / 100})`,
104
+ transformOrigin: 'center center',
105
+ }}
106
+ >
107
+ <VariantRenderer variant={match.variant} />
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Keyboard Shortcuts Help Modal
3
+ *
4
+ * Displays all available keyboard shortcuts in a modal overlay.
5
+ */
6
+
7
+ import { useEffect } from "react";
8
+ import { SHORTCUTS } from "../hooks/useKeyboardShortcuts.js";
9
+ import { CloseIcon } from "./Icons.js";
10
+
11
+ interface KeyboardShortcutsHelpProps {
12
+ isOpen: boolean;
13
+ onClose: () => void;
14
+ }
15
+
16
+ export function KeyboardShortcutsHelp({ isOpen, onClose }: KeyboardShortcutsHelpProps) {
17
+ // Close on escape
18
+ useEffect(() => {
19
+ if (!isOpen) return;
20
+
21
+ const handleEscape = (e: KeyboardEvent) => {
22
+ if (e.key === "Escape") {
23
+ onClose();
24
+ }
25
+ };
26
+
27
+ document.addEventListener("keydown", handleEscape);
28
+ return () => document.removeEventListener("keydown", handleEscape);
29
+ }, [isOpen, onClose]);
30
+
31
+ if (!isOpen) return null;
32
+
33
+ return (
34
+ <div
35
+ className="fixed inset-0 z-50 flex items-center justify-center p-4"
36
+ onClick={onClose}
37
+ >
38
+ {/* Backdrop */}
39
+ <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
40
+
41
+ {/* Modal */}
42
+ <div
43
+ className="relative bg-[--bg-primary] border border-[--border] rounded-xl shadow-2xl max-w-md w-full"
44
+ onClick={(e) => e.stopPropagation()}
45
+ >
46
+ {/* Header */}
47
+ <div className="flex items-center justify-between px-5 py-4 border-b border-[--border]">
48
+ <h2 className="text-base font-semibold text-primary">Keyboard Shortcuts</h2>
49
+ <button
50
+ onClick={onClose}
51
+ className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded-lg transition-colors"
52
+ >
53
+ <CloseIcon className="w-4 h-4" />
54
+ </button>
55
+ </div>
56
+
57
+ {/* Shortcuts List */}
58
+ <div className="p-5 space-y-3">
59
+ {SHORTCUTS.map((shortcut, index) => (
60
+ <div key={index} className="flex items-center justify-between">
61
+ <span className="text-sm text-secondary">{shortcut.description}</span>
62
+ <div className="flex items-center gap-1.5">
63
+ {shortcut.keys.map((key, keyIndex) => (
64
+ <span key={keyIndex} className="flex items-center">
65
+ {keyIndex > 0 && (
66
+ <span className="text-xs text-tertiary mx-1">or</span>
67
+ )}
68
+ <kbd className="px-2 py-1 text-xs font-mono bg-[--bg-secondary] border border-[--border] rounded text-primary">
69
+ {key}
70
+ </kbd>
71
+ </span>
72
+ ))}
73
+ </div>
74
+ </div>
75
+ ))}
76
+ </div>
77
+
78
+ {/* Footer */}
79
+ <div className="px-5 py-3 border-t border-[--border] bg-[--bg-secondary] rounded-b-xl">
80
+ <p className="text-xs text-tertiary text-center">
81
+ Press <kbd className="px-1.5 py-0.5 text-xs font-mono bg-[--bg-primary] border border-[--border] rounded">?</kbd> to toggle this help
82
+ </p>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export default KeyboardShortcutsHelp;