@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,404 @@
1
+ /**
2
+ * PreviewArea component - handles rendering the component preview.
3
+ * Extracted from App.tsx for better organization and performance.
4
+ *
5
+ * Now supports iframe-based isolation for CSS isolation between
6
+ * the viewer shell and user's component library.
7
+ */
8
+
9
+ import { memo, type ReactNode } from 'react';
10
+ import type { SegmentVariant } from '../../core/index.js';
11
+ import { ErrorBoundary } from './ErrorBoundary.js';
12
+ import { FigmaEmbed } from './FigmaEmbed.js';
13
+ import { VariantMatrix } from './VariantMatrix.js';
14
+ import { MultiViewportPreview } from './MultiViewportPreview.js';
15
+ import { IsolatedPreviewFrame } from './IsolatedPreviewFrame.js';
16
+ import { getBackgroundStyle, type ZoomLevel, type BackgroundOption } from './PreviewToolbar.js';
17
+ import type { PreviewTheme } from '../hooks/useViewSettings.js';
18
+ import { getViewportWidth, type ViewportPreset, type ViewportSize } from './ViewportSelector.js';
19
+
20
+ interface PreviewAreaProps {
21
+ // Component data
22
+ componentName: string;
23
+ segmentPath: string;
24
+ variant: SegmentVariant | undefined;
25
+ variants: SegmentVariant[] | undefined;
26
+
27
+ // View settings
28
+ zoom: ZoomLevel;
29
+ background: BackgroundOption;
30
+ viewport: ViewportPreset;
31
+ customSize: ViewportSize;
32
+ previewTheme: PreviewTheme;
33
+
34
+ // Display modes
35
+ showMatrixView: boolean;
36
+ showMultiViewport: boolean;
37
+ showComparison: boolean;
38
+
39
+ // Figma
40
+ figmaUrl?: string;
41
+ allFigmaUrls: string[];
42
+
43
+ // Callbacks
44
+ onSelectVariant: (index: number) => void;
45
+ onRetry: () => void;
46
+
47
+ // Render function for variant content (used as fallback and for matrix/multi-viewport)
48
+ renderContent: () => ReactNode;
49
+
50
+ // Keys for forcing re-renders
51
+ previewKey: string;
52
+
53
+ // Whether to use iframe isolation (default: true)
54
+ useIframeIsolation?: boolean;
55
+ }
56
+
57
+ // Device mockup component for tablet/mobile preview
58
+ interface DeviceMockupProps {
59
+ type: 'tablet' | 'mobile';
60
+ width: number;
61
+ children: React.ReactNode;
62
+ }
63
+
64
+ const DeviceMockup = memo(function DeviceMockup({ type, width, children }: DeviceMockupProps) {
65
+ const isMobile = type === 'mobile';
66
+ const frameWidth = width + 24;
67
+ const frameHeight = isMobile ? 720 : 1024;
68
+ const screenHeight = frameHeight - (isMobile ? 80 : 48);
69
+
70
+ return (
71
+ <div className="relative flex-shrink-0" style={{ width: `${frameWidth}px` }}>
72
+ <div
73
+ className="relative rounded-[40px] bg-[#1a1a1a] p-3 shadow-2xl"
74
+ style={{
75
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.1)',
76
+ }}
77
+ >
78
+ {isMobile && (
79
+ <>
80
+ <div className="absolute -left-[3px] top-24 w-[3px] h-8 bg-[#2a2a2a] rounded-l" />
81
+ <div className="absolute -left-[3px] top-36 w-[3px] h-12 bg-[#2a2a2a] rounded-l" />
82
+ <div className="absolute -left-[3px] top-52 w-[3px] h-12 bg-[#2a2a2a] rounded-l" />
83
+ <div className="absolute -right-[3px] top-32 w-[3px] h-16 bg-[#2a2a2a] rounded-r" />
84
+ </>
85
+ )}
86
+
87
+ <div
88
+ className="relative rounded-[32px] overflow-hidden bg-white"
89
+ style={{ height: `${screenHeight}px` }}
90
+ >
91
+ {isMobile ? (
92
+ <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[120px] h-[30px] bg-[#1a1a1a] rounded-b-2xl z-10 flex items-center justify-center gap-2">
93
+ <div className="w-2 h-2 rounded-full bg-[#2a2a2a]" />
94
+ <div className="w-12 h-1.5 rounded-full bg-[#2a2a2a]" />
95
+ </div>
96
+ ) : (
97
+ <div className="absolute top-2 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-[#2a2a2a] z-10" />
98
+ )}
99
+
100
+ <div className="w-full h-full overflow-auto">{children}</div>
101
+
102
+ <div className="absolute bottom-2 left-1/2 -translate-x-1/2 w-[100px] h-1 bg-black/20 rounded-full z-10" />
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ });
108
+
109
+ // Inner preview content with zoom transform
110
+ interface PreviewContentProps {
111
+ zoom: ZoomLevel;
112
+ previewTheme: PreviewTheme;
113
+ background: BackgroundOption;
114
+ children: ReactNode;
115
+ }
116
+
117
+ const PreviewContent = memo(function PreviewContent({ zoom, previewTheme, background, children }: PreviewContentProps) {
118
+ return (
119
+ <div
120
+ data-preview-container="true"
121
+ data-theme={previewTheme}
122
+ className="w-full h-full overflow-auto"
123
+ style={{
124
+ backgroundColor: background === 'transparent' ? 'transparent' : undefined,
125
+ }}
126
+ >
127
+ <div
128
+ className="p-6"
129
+ style={{
130
+ transform: `scale(${zoom / 100})`,
131
+ transformOrigin: 'top left',
132
+ width: zoom !== 100 ? `${100 / (zoom / 100)}%` : '100%',
133
+ color: '#1f2937',
134
+ }}
135
+ >
136
+ {children}
137
+ </div>
138
+ </div>
139
+ );
140
+ });
141
+
142
+ export function PreviewArea({
143
+ componentName,
144
+ segmentPath,
145
+ variant,
146
+ variants,
147
+ zoom,
148
+ background,
149
+ viewport,
150
+ customSize,
151
+ previewTheme,
152
+ showMatrixView,
153
+ showMultiViewport,
154
+ showComparison,
155
+ figmaUrl,
156
+ allFigmaUrls,
157
+ onSelectVariant,
158
+ onRetry,
159
+ renderContent,
160
+ previewKey,
161
+ useIframeIsolation = true,
162
+ }: PreviewAreaProps) {
163
+ // Matrix view
164
+ if (showMatrixView && variants) {
165
+ return (
166
+ <VariantMatrix
167
+ variants={variants}
168
+ componentName={componentName}
169
+ segmentPath={segmentPath}
170
+ zoom={zoom}
171
+ previewTheme={previewTheme}
172
+ background={background}
173
+ useIframeIsolation={useIframeIsolation}
174
+ onSelectVariant={(index) => {
175
+ onSelectVariant(index);
176
+ }}
177
+ />
178
+ );
179
+ }
180
+
181
+ // Multi-viewport view
182
+ if (showMultiViewport && variant) {
183
+ return (
184
+ <MultiViewportPreview
185
+ componentName={componentName}
186
+ segmentPath={segmentPath}
187
+ variantName={variant.name}
188
+ renderContent={renderContent}
189
+ previewTheme={previewTheme}
190
+ background={background}
191
+ zoom={zoom}
192
+ useIframeIsolation={useIframeIsolation}
193
+ />
194
+ );
195
+ }
196
+
197
+ const viewportWidth = getViewportWidth(viewport, customSize);
198
+ const viewportHeight = viewport === 'custom' ? customSize.height : null;
199
+ const isDevice = viewport === 'tablet' || viewport === 'mobile';
200
+ const backgroundStyle = getBackgroundStyle(background);
201
+
202
+ // Device mockup view (tablet/mobile)
203
+ if (isDevice && viewportWidth && variant) {
204
+ const deviceScreenHeight = viewport === 'mobile' ? 640 : 976;
205
+
206
+ if (showComparison && figmaUrl) {
207
+ return (
208
+ <div className="min-h-full flex flex-col p-6">
209
+ <div className="flex gap-4 flex-1">
210
+ <div className="flex-1 flex flex-col">
211
+ <div className="text-xs font-medium text-tertiary mb-2 text-center">Rendered</div>
212
+ <div className="flex-1 flex items-center justify-center" style={backgroundStyle}>
213
+ <DeviceMockup type={viewport as 'tablet' | 'mobile'} width={viewportWidth}>
214
+ {useIframeIsolation ? (
215
+ <IsolatedPreviewFrame
216
+ segmentPath={segmentPath}
217
+ variantName={variant.name}
218
+ theme={previewTheme}
219
+ width={viewportWidth}
220
+ height={deviceScreenHeight}
221
+ previewKey={previewKey}
222
+ />
223
+ ) : (
224
+ <PreviewContent zoom={zoom} previewTheme={previewTheme} background={background}>
225
+ <ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
226
+ {renderContent()}
227
+ </ErrorBoundary>
228
+ </PreviewContent>
229
+ )}
230
+ </DeviceMockup>
231
+ </div>
232
+ </div>
233
+
234
+ <div className="flex-1 flex flex-col">
235
+ <div className="text-xs font-medium text-tertiary mb-2 text-center">Figma Design</div>
236
+ <FigmaEmbed
237
+ figmaUrl={figmaUrl}
238
+ allFigmaUrls={allFigmaUrls}
239
+ zoom={zoom}
240
+ className="flex-1 rounded-lg border border-[--border] overflow-hidden"
241
+ style={backgroundStyle}
242
+ />
243
+ </div>
244
+ </div>
245
+ </div>
246
+ );
247
+ }
248
+
249
+ return (
250
+ <div className="min-h-full flex items-center justify-center p-8">
251
+ <DeviceMockup type={viewport as 'tablet' | 'mobile'} width={viewportWidth}>
252
+ {useIframeIsolation ? (
253
+ <IsolatedPreviewFrame
254
+ segmentPath={segmentPath}
255
+ variantName={variant.name}
256
+ theme={previewTheme}
257
+ width={viewportWidth}
258
+ height={deviceScreenHeight}
259
+ previewKey={previewKey}
260
+ />
261
+ ) : (
262
+ <PreviewContent zoom={zoom} previewTheme={previewTheme} background={background}>
263
+ <ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
264
+ {renderContent()}
265
+ </ErrorBoundary>
266
+ </PreviewContent>
267
+ )}
268
+ </DeviceMockup>
269
+ </div>
270
+ );
271
+ }
272
+
273
+ // Side-by-side comparison view
274
+ if (showComparison && figmaUrl && variant) {
275
+ return (
276
+ <div className="min-h-full flex flex-col p-6">
277
+ <div className="flex gap-4 flex-1">
278
+ <div className="flex-1 flex flex-col">
279
+ <div className="text-xs font-medium text-tertiary mb-2 text-center">Rendered</div>
280
+ <div
281
+ className="flex-1 rounded-lg border border-[--border] overflow-auto"
282
+ style={backgroundStyle}
283
+ >
284
+ {useIframeIsolation ? (
285
+ <IsolatedPreviewFrame
286
+ segmentPath={segmentPath}
287
+ variantName={variant.name}
288
+ theme={previewTheme}
289
+ width="100%"
290
+ height="100%"
291
+ minHeight={300}
292
+ previewKey={previewKey}
293
+ />
294
+ ) : (
295
+ <div
296
+ className="flex items-center justify-center p-8"
297
+ data-preview-container="true"
298
+ data-theme={previewTheme}
299
+ >
300
+ <div
301
+ style={{
302
+ transform: `scale(${zoom / 100})`,
303
+ transformOrigin: 'top left',
304
+ width: zoom !== 100 ? `${100 / (zoom / 100)}%` : '100%',
305
+ color: '#1f2937',
306
+ }}
307
+ >
308
+ <ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
309
+ {renderContent()}
310
+ </ErrorBoundary>
311
+ </div>
312
+ </div>
313
+ )}
314
+ </div>
315
+ </div>
316
+
317
+ <div className="flex-1 flex flex-col">
318
+ <div className="text-xs font-medium text-tertiary mb-2 text-center">Figma Design</div>
319
+ <FigmaEmbed
320
+ figmaUrl={figmaUrl}
321
+ allFigmaUrls={allFigmaUrls}
322
+ zoom={zoom}
323
+ className="flex-1 rounded-lg border border-[--border] overflow-hidden"
324
+ style={backgroundStyle}
325
+ />
326
+ </div>
327
+ </div>
328
+ </div>
329
+ );
330
+ }
331
+
332
+ // Regular preview (responsive or desktop)
333
+ // Use iframe isolation when enabled for complete CSS isolation
334
+ if (useIframeIsolation && variant) {
335
+ // When no specific viewport width, fill the container
336
+ const isFullWidth = !viewportWidth;
337
+
338
+ return (
339
+ <div className={isFullWidth ? "h-full flex flex-col" : "min-h-full flex items-center justify-center p-6"}>
340
+ <div
341
+ className="relative transition-all duration-200"
342
+ style={{
343
+ width: viewportWidth ? `${viewportWidth}px` : '100%',
344
+ maxWidth: viewportWidth ? undefined : '100%',
345
+ height: isFullWidth ? '100%' : undefined,
346
+ minHeight: viewportHeight ? `${viewportHeight}px` : (isFullWidth ? undefined : '200px'),
347
+ ...(viewportWidth && {
348
+ backgroundColor: 'var(--bg-primary)',
349
+ borderRadius: '8px',
350
+ boxShadow: '0 0 0 1px var(--border), 0 4px 12px rgba(0,0,0,0.15)',
351
+ }),
352
+ }}
353
+ >
354
+ <IsolatedPreviewFrame
355
+ segmentPath={segmentPath}
356
+ variantName={variant.name}
357
+ theme={previewTheme}
358
+ width="100%"
359
+ height="100%"
360
+ minHeight={viewportHeight || 200}
361
+ previewKey={previewKey}
362
+ />
363
+ </div>
364
+ </div>
365
+ );
366
+ }
367
+
368
+ // Fallback: Direct rendering without iframe isolation
369
+ return (
370
+ <div className="min-h-full flex items-center justify-center p-6">
371
+ <div
372
+ className="relative transition-all duration-200"
373
+ data-preview-container="true"
374
+ data-theme={previewTheme}
375
+ style={{
376
+ width: viewportWidth ? `${viewportWidth}px` : '100%',
377
+ maxWidth: viewportWidth ? undefined : '100%',
378
+ minHeight: viewportHeight ? `${viewportHeight}px` : '100%',
379
+ ...(viewportWidth && {
380
+ backgroundColor: 'var(--bg-primary)',
381
+ borderRadius: '8px',
382
+ boxShadow: '0 0 0 1px var(--border), 0 4px 12px rgba(0,0,0,0.15)',
383
+ }),
384
+ }}
385
+ >
386
+ <div
387
+ className="p-8"
388
+ style={{
389
+ transform: `scale(${zoom / 100})`,
390
+ transformOrigin: 'top left',
391
+ width: zoom !== 100 ? `${100 / (zoom / 100)}%` : '100%',
392
+ color: '#1f2937',
393
+ }}
394
+ >
395
+ <ErrorBoundary key={previewKey} componentName={componentName} onRetry={onRetry}>
396
+ {renderContent()}
397
+ </ErrorBoundary>
398
+ </div>
399
+ </div>
400
+ </div>
401
+ );
402
+ }
403
+
404
+ export { DeviceMockup };
@@ -0,0 +1,310 @@
1
+ /**
2
+ * PreviewFrameHost - Iframe-side component that renders components in isolation
3
+ *
4
+ * This component runs inside the preview iframe and:
5
+ * 1. Listens for render requests from the parent window
6
+ * 2. Loads and renders the requested segment variant
7
+ * 3. Applies theme styling
8
+ * 4. Reports render status back to parent
9
+ */
10
+
11
+ import { useState, useEffect, useRef, type ReactNode } from 'react';
12
+ import { useFrameBridge } from '../hooks/usePreviewBridge.js';
13
+
14
+ // Types for segment data
15
+ interface SegmentVariant {
16
+ name: string;
17
+ render: (options?: { loadedData?: Record<string, unknown> }) => ReactNode;
18
+ loaders?: Array<() => Promise<Record<string, unknown>>>;
19
+ props?: Record<string, unknown>;
20
+ hasPlayFunction?: boolean;
21
+ }
22
+
23
+ interface SegmentDefinition {
24
+ meta: {
25
+ name: string;
26
+ description?: string;
27
+ };
28
+ variants?: SegmentVariant[];
29
+ }
30
+
31
+ interface SegmentItem {
32
+ path: string;
33
+ segment: SegmentDefinition;
34
+ }
35
+
36
+ // Cached segments
37
+ let cachedSegments: SegmentItem[] | null = null;
38
+ let segmentsPromise: Promise<SegmentItem[]> | null = null;
39
+
40
+ /**
41
+ * Load segments from the virtual module
42
+ */
43
+ async function loadSegments(): Promise<SegmentItem[]> {
44
+ if (cachedSegments) {
45
+ return cachedSegments;
46
+ }
47
+
48
+ if (segmentsPromise) {
49
+ return segmentsPromise;
50
+ }
51
+
52
+ segmentsPromise = (async () => {
53
+ try {
54
+ // @ts-expect-error Virtual module
55
+ const module = await import('virtual:fragments');
56
+ if (module.segmentsPromise) {
57
+ cachedSegments = await module.segmentsPromise;
58
+ } else {
59
+ cachedSegments = module.segments || [];
60
+ }
61
+ return cachedSegments!;
62
+ } catch (error) {
63
+ console.error('[PreviewFrameHost] Failed to load segments:', error);
64
+ throw error;
65
+ }
66
+ })();
67
+
68
+ return segmentsPromise;
69
+ }
70
+
71
+ /**
72
+ * Find a segment by its path
73
+ */
74
+ function findSegmentByPath(segments: SegmentItem[], path: string): SegmentItem | undefined {
75
+ return segments.find(s => s.path === path);
76
+ }
77
+
78
+ /**
79
+ * Find a variant by name within a segment
80
+ */
81
+ function findVariant(segment: SegmentDefinition, variantName: string): SegmentVariant | undefined {
82
+ return segment.variants?.find(v => v.name === variantName);
83
+ }
84
+
85
+ /**
86
+ * Error boundary for catching render errors
87
+ */
88
+ function ErrorDisplay({ message, stack }: { message: string; stack?: string }) {
89
+ return (
90
+ <div className="preview-error">
91
+ <div style={{ fontWeight: 500, marginBottom: 8 }}>Render Error</div>
92
+ <div>{message}</div>
93
+ {stack && (
94
+ <pre style={{ marginTop: 8, fontSize: 11, opacity: 0.8 }}>
95
+ {stack}
96
+ </pre>
97
+ )}
98
+ </div>
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Loading indicator
104
+ */
105
+ function LoadingIndicator() {
106
+ return (
107
+ <div className="preview-loading">
108
+ <div className="spinner" />
109
+ <span>Loading component...</span>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Variant renderer that handles async loaders
116
+ */
117
+ function VariantRenderer({
118
+ variant,
119
+ props,
120
+ onRendered,
121
+ onError,
122
+ }: {
123
+ variant: SegmentVariant;
124
+ props?: Record<string, unknown>;
125
+ onRendered: (width: number, height: number) => void;
126
+ onError: (message: string, stack?: string) => void;
127
+ }) {
128
+ const [content, setContent] = useState<ReactNode | null>(null);
129
+ const [isLoading, setIsLoading] = useState(false);
130
+ const [error, setError] = useState<{ message: string; stack?: string } | null>(null);
131
+ const containerRef = useRef<HTMLDivElement>(null);
132
+ const hasReported = useRef(false);
133
+
134
+ useEffect(() => {
135
+ hasReported.current = false;
136
+ setContent(null);
137
+ setError(null);
138
+
139
+ const hasLoaders = variant.loaders && variant.loaders.length > 0;
140
+
141
+ if (!hasLoaders) {
142
+ // No loaders - render immediately
143
+ try {
144
+ const rendered = variant.render({ loadedData: props });
145
+ setContent(rendered);
146
+ } catch (err) {
147
+ const errorObj = err instanceof Error ? err : new Error(String(err));
148
+ setError({ message: errorObj.message, stack: errorObj.stack });
149
+ onError(errorObj.message, errorObj.stack);
150
+ }
151
+ return;
152
+ }
153
+
154
+ // Has loaders - execute them first
155
+ setIsLoading(true);
156
+ let cancelled = false;
157
+
158
+ async function executeLoaders() {
159
+ try {
160
+ const results = await Promise.all(variant.loaders!.map(loader => loader()));
161
+ if (cancelled) return;
162
+
163
+ const merged = results.reduce((acc, result) => ({ ...acc, ...result }), {});
164
+ const rendered = variant.render({ loadedData: { ...merged, ...props } });
165
+
166
+ setContent(rendered);
167
+ setIsLoading(false);
168
+ } catch (err) {
169
+ if (cancelled) return;
170
+ const errorObj = err instanceof Error ? err : new Error(String(err));
171
+ setError({ message: errorObj.message, stack: errorObj.stack });
172
+ setIsLoading(false);
173
+ onError(errorObj.message, errorObj.stack);
174
+ }
175
+ }
176
+
177
+ executeLoaders();
178
+
179
+ return () => {
180
+ cancelled = true;
181
+ };
182
+ }, [variant, props, onError]);
183
+
184
+ // Report rendered size after content renders
185
+ useEffect(() => {
186
+ if (!content || hasReported.current) return;
187
+
188
+ // Wait for next frame to ensure DOM has updated
189
+ requestAnimationFrame(() => {
190
+ if (containerRef.current && !hasReported.current) {
191
+ const rect = containerRef.current.getBoundingClientRect();
192
+ hasReported.current = true;
193
+ onRendered(rect.width, rect.height);
194
+ }
195
+ });
196
+ }, [content, onRendered]);
197
+
198
+ if (isLoading) {
199
+ return <LoadingIndicator />;
200
+ }
201
+
202
+ if (error) {
203
+ return <ErrorDisplay message={error.message} stack={error.stack} />;
204
+ }
205
+
206
+ return (
207
+ <div
208
+ ref={containerRef}
209
+ className="transition-opacity duration-150"
210
+ style={{
211
+ display: 'inline-block',
212
+ opacity: content ? 1 : 0,
213
+ }}
214
+ >
215
+ {content}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Main PreviewFrameHost component
222
+ */
223
+ export function PreviewFrameHost() {
224
+ const { renderRequest, theme, notifyReady, notifyRendered, notifyError } = useFrameBridge();
225
+ const [segments, setSegments] = useState<SegmentItem[] | null>(null);
226
+ const [loadError, setLoadError] = useState<string | null>(null);
227
+ const [currentVariant, setCurrentVariant] = useState<SegmentVariant | null>(null);
228
+ const [currentProps, setCurrentProps] = useState<Record<string, unknown> | undefined>(undefined);
229
+
230
+ // Apply theme to document
231
+ useEffect(() => {
232
+ if (theme === 'dark') {
233
+ document.documentElement.classList.add('dark');
234
+ } else {
235
+ document.documentElement.classList.remove('dark');
236
+ }
237
+ }, [theme]);
238
+
239
+ // Load segments on mount
240
+ useEffect(() => {
241
+ loadSegments()
242
+ .then(segs => {
243
+ setSegments(segs);
244
+ notifyReady();
245
+ })
246
+ .catch(err => {
247
+ const message = err instanceof Error ? err.message : 'Failed to load segments';
248
+ setLoadError(message);
249
+ notifyError(message);
250
+ });
251
+ }, [notifyReady, notifyError]);
252
+
253
+ // Handle render requests
254
+ useEffect(() => {
255
+ if (!renderRequest || !segments) return;
256
+
257
+ const { segmentPath, variantName, props } = renderRequest;
258
+
259
+ // Find segment
260
+ const segmentItem = findSegmentByPath(segments, segmentPath);
261
+ if (!segmentItem) {
262
+ notifyError(`Segment not found: ${segmentPath}`);
263
+ setCurrentVariant(null);
264
+ return;
265
+ }
266
+
267
+ // Find variant
268
+ const variant = findVariant(segmentItem.segment, variantName);
269
+ if (!variant) {
270
+ notifyError(`Variant not found: ${variantName} in ${segmentPath}`);
271
+ setCurrentVariant(null);
272
+ return;
273
+ }
274
+
275
+ setCurrentVariant(variant);
276
+ setCurrentProps(props);
277
+ }, [renderRequest, segments, notifyError]);
278
+
279
+ // Show loading state
280
+ if (!segments && !loadError) {
281
+ return <LoadingIndicator />;
282
+ }
283
+
284
+ // Show load error
285
+ if (loadError) {
286
+ return <ErrorDisplay message={loadError} />;
287
+ }
288
+
289
+ // Show waiting state
290
+ if (!currentVariant) {
291
+ return (
292
+ <div className="preview-loading">
293
+ <span>Waiting for render request...</span>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ // Render the variant
299
+ return (
300
+ <VariantRenderer
301
+ key={`${renderRequest?.segmentPath}-${renderRequest?.variantName}`}
302
+ variant={currentVariant}
303
+ props={currentProps}
304
+ onRendered={notifyRendered}
305
+ onError={notifyError}
306
+ />
307
+ );
308
+ }
309
+
310
+ export default PreviewFrameHost;