@fragments-sdk/viewer 0.2.1

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 (141) hide show
  1. package/LICENSE +84 -0
  2. package/index.html +28 -0
  3. package/package.json +71 -0
  4. package/src/__tests__/a11y-fixes.test.ts +358 -0
  5. package/src/__tests__/jsx-parser.test.ts +502 -0
  6. package/src/__tests__/render-utils.test.ts +232 -0
  7. package/src/__tests__/style-utils.test.ts +404 -0
  8. package/src/app/index.ts +1 -0
  9. package/src/assets/fragments-logo.ts +4 -0
  10. package/src/assets/fragments_logo.png +0 -0
  11. package/src/components/AccessibilityPanel.tsx +1457 -0
  12. package/src/components/ActionCapture.tsx +172 -0
  13. package/src/components/ActionsPanel.tsx +332 -0
  14. package/src/components/AllVariantsPreview.tsx +78 -0
  15. package/src/components/App.tsx +604 -0
  16. package/src/components/BottomPanel.tsx +288 -0
  17. package/src/components/CodePanel.naming.test.tsx +59 -0
  18. package/src/components/CodePanel.tsx +118 -0
  19. package/src/components/CommandPalette.tsx +392 -0
  20. package/src/components/ComponentDocView.tsx +164 -0
  21. package/src/components/ComponentGraph.tsx +380 -0
  22. package/src/components/ComponentHeader.tsx +88 -0
  23. package/src/components/ContractPanel.tsx +241 -0
  24. package/src/components/DeviceMockup.tsx +156 -0
  25. package/src/components/EmptyVariantMessage.tsx +54 -0
  26. package/src/components/ErrorBoundary.tsx +97 -0
  27. package/src/components/FigmaEmbed.tsx +238 -0
  28. package/src/components/FragmentEditor.tsx +525 -0
  29. package/src/components/FragmentRenderer.tsx +61 -0
  30. package/src/components/HeaderSearch.tsx +24 -0
  31. package/src/components/HealthDashboard.tsx +441 -0
  32. package/src/components/HmrStatusIndicator.tsx +61 -0
  33. package/src/components/Icons.tsx +479 -0
  34. package/src/components/InteractionsPanel.tsx +757 -0
  35. package/src/components/IsolatedPreviewFrame.tsx +390 -0
  36. package/src/components/IsolatedRender.tsx +113 -0
  37. package/src/components/KeyboardShortcutsHelp.tsx +53 -0
  38. package/src/components/LandingPage.tsx +420 -0
  39. package/src/components/Layout.tsx +27 -0
  40. package/src/components/LeftSidebar.tsx +472 -0
  41. package/src/components/LoadErrorMessage.tsx +102 -0
  42. package/src/components/MultiViewportPreview.tsx +527 -0
  43. package/src/components/NoVariantsMessage.tsx +59 -0
  44. package/src/components/PanelShell.tsx +161 -0
  45. package/src/components/PerformancePanel.tsx +304 -0
  46. package/src/components/PreviewArea.tsx +254 -0
  47. package/src/components/PreviewAside.tsx +168 -0
  48. package/src/components/PreviewFrameHost.tsx +304 -0
  49. package/src/components/PreviewToolbar.tsx +80 -0
  50. package/src/components/PropsEditor.tsx +506 -0
  51. package/src/components/PropsTable.tsx +111 -0
  52. package/src/components/RelationsSection.tsx +88 -0
  53. package/src/components/ResizablePanel.tsx +271 -0
  54. package/src/components/RightSidebar.tsx +102 -0
  55. package/src/components/RuntimeToolsRegistrar.tsx +17 -0
  56. package/src/components/ScreenshotButton.tsx +90 -0
  57. package/src/components/ShadowPreview.tsx +204 -0
  58. package/src/components/Sidebar.tsx +169 -0
  59. package/src/components/SkeletonLoader.tsx +161 -0
  60. package/src/components/ThemeProvider.tsx +42 -0
  61. package/src/components/Toast.tsx +3 -0
  62. package/src/components/TokenStylePanel.tsx +699 -0
  63. package/src/components/TopToolbar.tsx +159 -0
  64. package/src/components/Untitled +1 -0
  65. package/src/components/UsageSection.tsx +95 -0
  66. package/src/components/VariantMatrix.tsx +391 -0
  67. package/src/components/VariantRenderer.tsx +131 -0
  68. package/src/components/VariantTabs.tsx +40 -0
  69. package/src/components/ViewerHeader.tsx +69 -0
  70. package/src/components/ViewerStateSync.tsx +52 -0
  71. package/src/components/ViewportSelector.tsx +172 -0
  72. package/src/components/WebMCPDevTools.tsx +503 -0
  73. package/src/components/WebMCPIntegration.tsx +47 -0
  74. package/src/components/WebMCPStatusIndicator.tsx +60 -0
  75. package/src/components/_future/CreatePage.tsx +835 -0
  76. package/src/components/viewer-utils.ts +16 -0
  77. package/src/composition-renderer.ts +381 -0
  78. package/src/constants/index.ts +1 -0
  79. package/src/constants/ui.ts +166 -0
  80. package/src/entry.tsx +335 -0
  81. package/src/hooks/index.ts +2 -0
  82. package/src/hooks/useA11yCache.ts +383 -0
  83. package/src/hooks/useA11yService.ts +364 -0
  84. package/src/hooks/useActions.ts +138 -0
  85. package/src/hooks/useAppState.ts +147 -0
  86. package/src/hooks/useCompiledFragments.ts +42 -0
  87. package/src/hooks/useFigmaIntegration.ts +132 -0
  88. package/src/hooks/useHmrStatus.ts +109 -0
  89. package/src/hooks/useKeyboardShortcuts.ts +270 -0
  90. package/src/hooks/usePreviewBridge.ts +347 -0
  91. package/src/hooks/useScrollSpy.ts +78 -0
  92. package/src/hooks/useShadowStyles.ts +221 -0
  93. package/src/hooks/useUrlState.ts +318 -0
  94. package/src/hooks/useViewSettings.ts +111 -0
  95. package/src/intelligence/healthReport.ts +505 -0
  96. package/src/intelligence/styleDrift.ts +340 -0
  97. package/src/intelligence/usageScanner.ts +309 -0
  98. package/src/jsx-parser.ts +486 -0
  99. package/src/preview-frame-entry.tsx +25 -0
  100. package/src/preview-frame.html +148 -0
  101. package/src/render-template.html +68 -0
  102. package/src/render-utils.ts +311 -0
  103. package/src/shared/ComponentDocContent.module.scss +10 -0
  104. package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
  105. package/src/shared/ComponentDocContent.tsx +274 -0
  106. package/src/shared/DocsHeaderBar.tsx +129 -0
  107. package/src/shared/DocsPageAsideHost.tsx +89 -0
  108. package/src/shared/DocsPageShell.tsx +124 -0
  109. package/src/shared/DocsSearchCommand.tsx +99 -0
  110. package/src/shared/DocsSidebarNav.tsx +66 -0
  111. package/src/shared/PropsTable.module.scss +68 -0
  112. package/src/shared/PropsTable.module.scss.d.ts +2 -0
  113. package/src/shared/PropsTable.tsx +76 -0
  114. package/src/shared/VariantPreviewCard.module.scss +114 -0
  115. package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
  116. package/src/shared/VariantPreviewCard.tsx +137 -0
  117. package/src/shared/docs-data/index.ts +32 -0
  118. package/src/shared/docs-data/mcp-configs.ts +72 -0
  119. package/src/shared/docs-data/palettes.ts +75 -0
  120. package/src/shared/docs-data/setup-examples.ts +55 -0
  121. package/src/shared/docs-layout.scss +28 -0
  122. package/src/shared/docs-layout.scss.d.ts +2 -0
  123. package/src/shared/index.ts +34 -0
  124. package/src/shared/types.ts +53 -0
  125. package/src/style-utils.ts +414 -0
  126. package/src/styles/globals.css +278 -0
  127. package/src/types/a11y.ts +197 -0
  128. package/src/utils/a11y-fixes.ts +509 -0
  129. package/src/utils/actionExport.ts +372 -0
  130. package/src/utils/colorSchemes.ts +201 -0
  131. package/src/utils/contrast.ts +246 -0
  132. package/src/utils/detectRelationships.ts +256 -0
  133. package/src/webmcp/__tests__/analytics.test.ts +108 -0
  134. package/src/webmcp/analytics.ts +165 -0
  135. package/src/webmcp/index.ts +3 -0
  136. package/src/webmcp/posthog-bridge.ts +39 -0
  137. package/src/webmcp/runtime-tools.ts +152 -0
  138. package/src/webmcp/scan-utils.ts +135 -0
  139. package/src/webmcp/use-tool-analytics.ts +69 -0
  140. package/src/webmcp/viewer-state.ts +45 -0
  141. package/tsconfig.json +20 -0
@@ -0,0 +1,604 @@
1
+ /**
2
+ * Main App component for the Fragments viewer.
3
+ * Refactored for better performance and maintainability.
4
+ */
5
+
6
+ import { useState, useMemo, useEffect, useCallback, useRef, type ReactNode } from "react";
7
+ import type { FragmentDefinition, FragmentVariant } from '@fragments-sdk/core';
8
+
9
+ // Layout & Navigation
10
+ import { Layout } from "./Layout.js";
11
+ import { LeftSidebar } from "./LeftSidebar.js";
12
+ import { CommandPalette } from "./CommandPalette.js";
13
+ import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
14
+ import { useToast } from "./Toast.js";
15
+
16
+ // Toolbar & Header
17
+ import { TopToolbar } from "./TopToolbar.js";
18
+ import { ViewerHeader } from "./ViewerHeader.js";
19
+
20
+ // Preview & Rendering
21
+ import { PreviewArea } from "./PreviewArea.js";
22
+ import { BottomPanel } from "./BottomPanel.js";
23
+ import { IsolatedRender } from "./IsolatedRender.js";
24
+ import { FragmentRenderer, LoaderIndicator } from "./FragmentRenderer.js";
25
+ import { HealthDashboard } from "./HealthDashboard.js";
26
+ import { useAllFigmaUrls } from "./FigmaEmbed.js";
27
+ import { ActionCapture } from "./ActionCapture.js";
28
+
29
+ // Extracted sub-components
30
+ import { PreviewAside } from "./PreviewAside.js";
31
+ import { AllVariantsPreview } from "./AllVariantsPreview.js";
32
+ import { ComponentDocView } from "./ComponentDocView.js";
33
+ import { NoVariantsMessage } from "./NoVariantsMessage.js";
34
+ import { EmptyVariantMessage } from "./EmptyVariantMessage.js";
35
+
36
+ // Fragments UI
37
+ import { Stack, Box, EmptyState } from "@fragments-sdk/ui";
38
+
39
+ // Icons
40
+ import { EmptyIcon } from "./Icons.js";
41
+
42
+ // Utilities
43
+ import { getVariantSectionId } from "./viewer-utils.js";
44
+
45
+ // Hooks
46
+ import { useAppState } from "../hooks/useAppState.js";
47
+ import { useViewSettings } from "../hooks/useViewSettings.js";
48
+ import { useFigmaIntegration } from "../hooks/useFigmaIntegration.js";
49
+ import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
50
+ import { useActions } from "../hooks/useActions.js";
51
+ import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useUrlState.js";
52
+ import { useTheme } from "./ThemeProvider.js";
53
+ import { ViewerStateSync } from "./ViewerStateSync.js";
54
+
55
+ interface AppProps {
56
+ fragments: Array<{ path: string; fragment: FragmentDefinition }>;
57
+ }
58
+
59
+ export function App({ fragments }: AppProps) {
60
+ // URL state management
61
+ const {
62
+ state: urlState,
63
+ setComponent: setUrlComponent,
64
+ setVariant: setUrlVariant,
65
+ setViewSettings: setUrlViewSettings,
66
+ copyUrl,
67
+ } = useUrlState();
68
+
69
+ // UI state (modals, panels, view modes)
70
+ const { state: uiState, actions: uiActions } = useAppState();
71
+
72
+ // View settings (zoom, viewport, theme)
73
+ const viewSettings = useViewSettings({
74
+ initialState: {
75
+ zoom: urlState.zoom as any,
76
+ viewport: urlState.viewport as any,
77
+ customSize: { width: urlState.customWidth, height: urlState.customHeight },
78
+ },
79
+ onZoomChange: (zoom) => setUrlViewSettings({ zoom }),
80
+ onViewportChange: (vp, size) =>
81
+ setUrlViewSettings({
82
+ viewport: vp,
83
+ customWidth: size?.width,
84
+ customHeight: size?.height,
85
+ }),
86
+ });
87
+
88
+ // Get resolved theme from ThemeProvider for iframe preview
89
+ const { resolvedTheme } = useTheme();
90
+
91
+ // Toast notifications (via Fragments UI ToastProvider)
92
+ const { info, success } = useToast();
93
+
94
+ // Navigation state
95
+ const [activeFragmentPath, setActiveFragmentPath] = useState<string | null>(() => {
96
+ if (urlState.component) {
97
+ const found = findFragmentByName(fragments, urlState.component);
98
+ return found?.path ?? fragments[0]?.path ?? null;
99
+ }
100
+ return fragments[0]?.path ?? null;
101
+ });
102
+ const activeFragmentPathRef = useRef(activeFragmentPath);
103
+ activeFragmentPathRef.current = activeFragmentPath;
104
+
105
+ const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
106
+ const fragment = fragments.find((s) => s.path === activeFragmentPath);
107
+ if (urlState.variant && fragment?.fragment.variants) {
108
+ return findVariantIndex(fragment.fragment.variants, urlState.variant);
109
+ }
110
+ return 0;
111
+ });
112
+ const [searchQuery, setSearchQuery] = useState("");
113
+ const searchInputRef = useRef<HTMLInputElement>(null);
114
+
115
+ // Derived values
116
+ const activeFragment = useMemo(
117
+ () => fragments.find((s) => s.path === activeFragmentPath),
118
+ [fragments, activeFragmentPath]
119
+ );
120
+ const variants = activeFragment?.fragment.variants ?? [];
121
+ const variantCount = variants.length;
122
+ const safeVariantIndex = variantCount > 0 ? Math.min(activeVariantIndex, variantCount - 1) : 0;
123
+ const activeVariant = variants[safeVariantIndex];
124
+ const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
125
+
126
+ // Figma integration
127
+ const figmaIntegration = useFigmaIntegration({
128
+ figmaUrl,
129
+ showComparison: uiState.showComparison,
130
+ dependencies: [activeFragmentPath, activeVariantIndex],
131
+ });
132
+
133
+ // Actions logging
134
+ const { logs: actionLogs, logAction, clearLogs: clearActionLogs } = useActions();
135
+ const useActionsRef = useRef({ logAction });
136
+ useActionsRef.current = { logAction };
137
+
138
+ // Figma URLs for preloading
139
+ const allFigmaUrls = useAllFigmaUrls(activeFragment?.fragment);
140
+
141
+ // Reset action logs on variant change
142
+ useEffect(() => {
143
+ clearActionLogs();
144
+ }, [activeFragmentPath, activeVariantIndex, clearActionLogs]);
145
+
146
+ // Extract rendered styles after component renders
147
+ useEffect(() => {
148
+ if (uiState.showComparison && activeVariant) {
149
+ const timer = setTimeout(figmaIntegration.extractRenderedStyles, 100);
150
+ return () => clearTimeout(timer);
151
+ }
152
+ }, [
153
+ uiState.showComparison,
154
+ activeVariant,
155
+ figmaIntegration.extractRenderedStyles,
156
+ uiState.previewKey,
157
+ ]);
158
+
159
+ // Keep focused variant index in range when variant lists change.
160
+ useEffect(() => {
161
+ if (variantCount === 0) {
162
+ setActiveVariantIndex(0);
163
+ return;
164
+ }
165
+ if (activeVariantIndex >= variantCount) {
166
+ setActiveVariantIndex(variantCount - 1);
167
+ }
168
+ }, [activeVariantIndex, variantCount]);
169
+
170
+ // Sync URL state on browser navigation
171
+ useEffect(() => {
172
+ if (urlState.component) {
173
+ const found = findFragmentByName(fragments, urlState.component);
174
+ if (!found) return;
175
+
176
+ const pathChanged = found.path !== activeFragmentPathRef.current;
177
+ setActiveFragmentPath(found.path);
178
+ uiActions.setHealthDashboard(false);
179
+
180
+ // Keep focused variant when entering "All" on the same component.
181
+ if (urlState.variant || pathChanged) {
182
+ const variantIndex = findVariantIndex(found.fragment.variants, urlState.variant);
183
+ setActiveVariantIndex(variantIndex);
184
+ }
185
+ }
186
+ }, [urlState.component, urlState.variant, fragments, uiActions]);
187
+
188
+ // HMR toast notifications — batched so rapid bursts show one toast
189
+ useEffect(() => {
190
+ const hot = (import.meta as any).hot;
191
+ if (!hot) return;
192
+
193
+ let pending = new Set<string>();
194
+ let timer: ReturnType<typeof setTimeout> | null = null;
195
+
196
+ const flush = () => {
197
+ timer = null;
198
+ if (pending.size === 0) return;
199
+ const unique = Array.from(pending);
200
+ pending = new Set();
201
+ const summary =
202
+ unique.length <= 3
203
+ ? unique.join(", ")
204
+ : `${unique.slice(0, 3).join(", ")} +${unique.length - 3} more`;
205
+ info("HMR Update", `Updated: ${summary}`);
206
+ };
207
+
208
+ const handleUpdate = (data: any) => {
209
+ if (data?.updates?.length > 0) {
210
+ for (const u of data.updates) {
211
+ pending.add(u.path.split("/").pop());
212
+ }
213
+ // Debounce: wait 100ms for more updates before showing toast
214
+ if (timer) clearTimeout(timer);
215
+ timer = setTimeout(flush, 100);
216
+ }
217
+ };
218
+
219
+ hot.on("vite:beforeUpdate", handleUpdate);
220
+ return () => {
221
+ hot.off?.("vite:beforeUpdate", handleUpdate);
222
+ if (timer) clearTimeout(timer);
223
+ };
224
+ }, [info]);
225
+
226
+ // Navigation handlers
227
+ const handleSelectFragment = useCallback(
228
+ (path: string) => {
229
+ const fragment = fragments.find((s) => s.path === path);
230
+ const componentName = fragment?.fragment.meta.name || path;
231
+
232
+ setActiveFragmentPath(path);
233
+ setActiveVariantIndex(0);
234
+ uiActions.setHealthDashboard(false);
235
+ setUrlComponent(componentName, null);
236
+ },
237
+ [fragments, setUrlComponent, uiActions]
238
+ );
239
+
240
+ const scrollToVariantSection = useCallback(
241
+ (index: number, behavior: ScrollBehavior = "smooth") => {
242
+ if (!activeFragment || variantCount === 0) return;
243
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
244
+ const targetVariant = variants[normalizedIndex];
245
+ if (!targetVariant) return;
246
+
247
+ const sectionId = getVariantSectionId(activeFragment.fragment.meta.name, targetVariant.name);
248
+ document.getElementById(sectionId)?.scrollIntoView({ behavior, block: "start" });
249
+ },
250
+ [activeFragment, variantCount, variants]
251
+ );
252
+
253
+ const focusVariantInAllMode = useCallback(
254
+ (index: number, shouldScroll = false) => {
255
+ if (variantCount === 0) return;
256
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
257
+ setActiveVariantIndex(normalizedIndex);
258
+ if (shouldScroll) {
259
+ scrollToVariantSection(normalizedIndex);
260
+ }
261
+ },
262
+ [variantCount, scrollToVariantSection]
263
+ );
264
+
265
+ const handleSelectVariant = useCallback(
266
+ (index: number) => {
267
+ if (variantCount === 0) return;
268
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
269
+ const variantName = variants[normalizedIndex]?.name;
270
+ setActiveVariantIndex(normalizedIndex);
271
+ setUrlVariant(variantName || null);
272
+ },
273
+ [variantCount, variants, setUrlVariant]
274
+ );
275
+
276
+ const handleSelectVariantLink = useCallback(
277
+ (index: number) => {
278
+ // Always scroll to the variant section in the docs-like view
279
+ focusVariantInAllMode(index, true);
280
+ },
281
+ [focusVariantInAllMode]
282
+ );
283
+
284
+ // Copy link handler
285
+ const handleCopyLink = useCallback(async () => {
286
+ const copied = await copyUrl();
287
+ if (copied) {
288
+ uiActions.setLinkCopied(true);
289
+ success("Copied", "Link copied to clipboard");
290
+ setTimeout(() => uiActions.setLinkCopied(false), 2000);
291
+ }
292
+ }, [copyUrl, success, uiActions]);
293
+
294
+ // Sorted fragment paths for keyboard navigation
295
+ const sortedFragmentPaths = useMemo(() => {
296
+ return [...fragments]
297
+ .filter((s) => s.fragment?.meta?.name)
298
+ .sort((a, b) => a.fragment.meta.name.localeCompare(b.fragment.meta.name))
299
+ .map((s) => s.path);
300
+ }, [fragments]);
301
+
302
+ const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || "");
303
+
304
+ // Keyboard shortcuts
305
+ useKeyboardShortcuts(
306
+ {
307
+ nextComponent: () => {
308
+ const nextIndex =
309
+ currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
310
+ handleSelectFragment(sortedFragmentPaths[nextIndex]);
311
+ },
312
+ prevComponent: () => {
313
+ const prevIndex =
314
+ currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
315
+ handleSelectFragment(sortedFragmentPaths[prevIndex]);
316
+ },
317
+ nextVariant: () => {
318
+ if (variantCount === 0) return;
319
+ const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
320
+ focusVariantInAllMode(nextIndex, true);
321
+ },
322
+ prevVariant: () => {
323
+ if (variantCount === 0) return;
324
+ const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
325
+ focusVariantInAllMode(prevIndex, true);
326
+ },
327
+ goToVariant: (index) => {
328
+ if (index >= variantCount) return;
329
+ focusVariantInAllMode(index, true);
330
+ },
331
+ toggleTheme: viewSettings.toggleTheme,
332
+ togglePanel: uiActions.togglePanel,
333
+ toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
334
+ toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
335
+ copyLink: handleCopyLink,
336
+ showHelp: uiActions.toggleShortcutsHelp,
337
+ openSearch: () => uiActions.setCommandPalette(true),
338
+ escape: () => {
339
+ if (document.activeElement === searchInputRef.current) {
340
+ if (searchQuery) {
341
+ setSearchQuery("");
342
+ } else {
343
+ searchInputRef.current.blur();
344
+ }
345
+ return;
346
+ }
347
+ uiActions.closeAllModals();
348
+ },
349
+ },
350
+ { enabled: !uiState.showShortcutsHelp, variantCount }
351
+ );
352
+
353
+ // Render variant with action logging via DOM event capture
354
+ const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
355
+ if (!variant) return null;
356
+
357
+ return (
358
+ <ActionCapture onAction={useActionsRef.current.logAction}>
359
+ <FragmentRenderer variant={variant}>
360
+ {(content, isLoading, error) => {
361
+ if (isLoading)
362
+ return (
363
+ <Stack align="center" justify="center" style={{ padding: "32px" }}>
364
+ <LoaderIndicator />
365
+ </Stack>
366
+ );
367
+ if (error)
368
+ return (
369
+ <EmptyVariantMessage
370
+ reason={`Error: ${error.message}`}
371
+ variantName={variant.name}
372
+ hint="Check the console for the full error stack trace."
373
+ />
374
+ );
375
+ if (content === null || content === undefined)
376
+ return (
377
+ <EmptyVariantMessage
378
+ reason="render() returned null or undefined"
379
+ variantName={variant.name}
380
+ hint="The variant's render function didn't return any JSX."
381
+ />
382
+ );
383
+ return content;
384
+ }}
385
+ </FragmentRenderer>
386
+ </ActionCapture>
387
+ );
388
+ }, []);
389
+
390
+ // Check if isolated mode
391
+ const isIsolated = useMemo(() => {
392
+ const params = new URLSearchParams(window.location.search);
393
+ return params.get("isolated") === "true";
394
+ }, []);
395
+
396
+ if (isIsolated) {
397
+ return <IsolatedRender fragments={fragments} />;
398
+ }
399
+
400
+ return (
401
+ <>
402
+ <ViewerStateSync fragments={fragments} activeVariantIndex={safeVariantIndex} />
403
+ <KeyboardShortcutsHelp
404
+ isOpen={uiState.showShortcutsHelp}
405
+ onClose={() => uiActions.setShortcutsHelp(false)}
406
+ />
407
+ <CommandPalette
408
+ isOpen={uiState.showCommandPalette}
409
+ onClose={() => uiActions.setCommandPalette(false)}
410
+ fragments={fragments}
411
+ onSelectComponent={handleSelectFragment}
412
+ onSelectVariant={(path, variantIndex) => {
413
+ handleSelectFragment(path);
414
+ setTimeout(() => handleSelectVariant(variantIndex), 0);
415
+ }}
416
+ />
417
+
418
+ <Layout
419
+ header={
420
+ activeFragment && !uiState.showHealthDashboard ? (
421
+ <TopToolbar
422
+ fragment={activeFragment}
423
+ viewSettings={viewSettings}
424
+ uiState={uiState}
425
+ uiActions={uiActions}
426
+ figmaUrl={figmaUrl}
427
+ searchQuery={searchQuery}
428
+ onSearchChange={setSearchQuery}
429
+ searchInputRef={searchInputRef}
430
+ />
431
+ ) : (
432
+ <ViewerHeader
433
+ showHealth={uiState.showHealthDashboard}
434
+ searchQuery={searchQuery}
435
+ onSearchChange={setSearchQuery}
436
+ searchInputRef={searchInputRef}
437
+ />
438
+ )
439
+ }
440
+ leftSidebar={
441
+ <LeftSidebar
442
+ fragments={fragments}
443
+ activeFragment={uiState.showHealthDashboard ? null : activeFragmentPath}
444
+ searchQuery={searchQuery}
445
+ onSelect={handleSelectFragment}
446
+ showHealth={uiState.showHealthDashboard}
447
+ onHealthClick={() => {
448
+ uiActions.setHealthDashboard(true);
449
+ setActiveFragmentPath(null);
450
+ }}
451
+ />
452
+ }
453
+ aside={
454
+ uiState.showAside && activeFragment && !uiState.showHealthDashboard ? (
455
+ <PreviewAside
456
+ fragment={activeFragment.fragment}
457
+ variants={variants}
458
+ focusedVariantIndex={safeVariantIndex}
459
+ activePanel={uiState.activePanel}
460
+ onSelectVariant={handleSelectVariantLink}
461
+ onCopyLink={handleCopyLink}
462
+ onShowShortcuts={uiActions.toggleShortcutsHelp}
463
+ />
464
+ ) : null
465
+ }
466
+ >
467
+ {uiState.showHealthDashboard ? (
468
+ <Box height="100%" overflow="auto" background="primary">
469
+ <Box padding="lg" style={{ maxWidth: "896px", margin: "0 auto" }}>
470
+ <HealthDashboard
471
+ fragments={fragments}
472
+ onNavigate={(componentName) => {
473
+ const target = fragments.find((s) => s.fragment.meta.name === componentName);
474
+ if (target) {
475
+ uiActions.setHealthDashboard(false);
476
+ handleSelectFragment(target.path);
477
+ }
478
+ }}
479
+ />
480
+ </Box>
481
+ </Box>
482
+ ) : activeFragment ? (
483
+ <Stack id="preview-layout" style={{ height: "100%" }}>
484
+ {/* Main Content Area */}
485
+ <Stack style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
486
+ {/* Preview Area */}
487
+ <Box id="preview-canvas" overflow="auto" style={{ flex: 1, position: "relative" }}>
488
+ {variantCount === 0 ? (
489
+ <NoVariantsMessage fragment={activeFragment?.fragment} />
490
+ ) : uiState.showMatrixView ? (
491
+ <PreviewArea
492
+ componentName={activeFragment.fragment.meta.name}
493
+ fragmentPath={activeFragment.path}
494
+ variant={activeVariant}
495
+ variants={variants}
496
+ zoom={viewSettings.zoom}
497
+ viewport={viewSettings.viewport}
498
+ customSize={viewSettings.customSize}
499
+ previewTheme={resolvedTheme}
500
+ showMatrixView={true}
501
+ showMultiViewport={false}
502
+ showComparison={uiState.showComparison}
503
+ figmaUrl={figmaUrl}
504
+ allFigmaUrls={allFigmaUrls}
505
+ onSelectVariant={(index) => {
506
+ uiActions.setMatrixView(false);
507
+ handleSelectVariant(index);
508
+ }}
509
+ onRetry={uiActions.incrementPreviewKey}
510
+ renderContent={() => renderVariantWithProps(activeVariant)}
511
+ previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
512
+ />
513
+ ) : uiState.showMultiViewport ? (
514
+ <PreviewArea
515
+ componentName={activeFragment.fragment.meta.name}
516
+ fragmentPath={activeFragment.path}
517
+ variant={activeVariant}
518
+ variants={variants}
519
+ zoom={viewSettings.zoom}
520
+ viewport={viewSettings.viewport}
521
+ customSize={viewSettings.customSize}
522
+ previewTheme={resolvedTheme}
523
+ showMatrixView={false}
524
+ showMultiViewport={true}
525
+ showComparison={uiState.showComparison}
526
+ figmaUrl={figmaUrl}
527
+ allFigmaUrls={allFigmaUrls}
528
+ onSelectVariant={(index) => {
529
+ handleSelectVariant(index);
530
+ }}
531
+ onRetry={uiActions.incrementPreviewKey}
532
+ renderContent={() => renderVariantWithProps(activeVariant)}
533
+ previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
534
+ />
535
+ ) : (
536
+ <Box
537
+ style={{
538
+ padding: "var(--fui-space-6) var(--fui-space-8)",
539
+ }}
540
+ >
541
+ <ComponentDocView
542
+ fragment={activeFragment}
543
+ fragments={fragments}
544
+ renderVariantContent={renderVariantWithProps}
545
+ onNavigateToComponent={(name) => {
546
+ const target = fragments.find((s) => s.fragment.meta.name === name);
547
+ if (target) handleSelectFragment(target.path);
548
+ }}
549
+ zoom={viewSettings.zoom}
550
+ viewport={viewSettings.viewport}
551
+ customSize={viewSettings.customSize}
552
+ previewTheme={resolvedTheme}
553
+ showComparison={uiState.showComparison}
554
+ allFigmaUrls={allFigmaUrls}
555
+ onRetry={uiActions.incrementPreviewKey}
556
+ previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
557
+ />
558
+ </Box>
559
+ )}
560
+ </Box>
561
+ </Stack>
562
+
563
+ {activeVariant && (
564
+ <BottomPanel
565
+ fragment={activeFragment.fragment}
566
+ variant={activeVariant}
567
+ fragments={fragments}
568
+ open={uiState.panelOpen}
569
+ onOpenChange={uiActions.setPanelOpen}
570
+ activePanel={uiState.activePanel}
571
+ onPanelChange={uiActions.setActivePanel}
572
+ figmaUrl={figmaUrl}
573
+ figmaStyles={
574
+ figmaIntegration.figmaStyles.status === "success"
575
+ ? figmaIntegration.figmaStyles.styles || null
576
+ : null
577
+ }
578
+ renderedStyles={figmaIntegration.renderedStyles}
579
+ figmaLoading={figmaIntegration.isLoading}
580
+ figmaError={figmaIntegration.errorMessage}
581
+ onFetchFigma={figmaIntegration.fetchFigmaStyles}
582
+ onRefreshRendered={figmaIntegration.extractRenderedStyles}
583
+ onNavigateToComponent={(name) => {
584
+ const target = fragments.find((s) => s.fragment.meta.name === name);
585
+ if (target) handleSelectFragment(target.path);
586
+ }}
587
+ previewKey={uiState.previewKey}
588
+ fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
589
+ />
590
+ )}
591
+ </Stack>
592
+ ) : (
593
+ <EmptyState style={{ height: "100%" }}>
594
+ <EmptyState.Icon>
595
+ <EmptyIcon style={{ width: "48px", height: "48px" }} />
596
+ </EmptyState.Icon>
597
+ <EmptyState.Title>No component selected</EmptyState.Title>
598
+ <EmptyState.Description>Select a component from the sidebar</EmptyState.Description>
599
+ </EmptyState>
600
+ )}
601
+ </Layout>
602
+ </>
603
+ );
604
+ }