@fragments-sdk/cli 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,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;
|