@fragments-sdk/cli 0.7.9 → 0.7.11

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 (106) hide show
  1. package/dist/bin.js +13 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-CWKQQR6C.js → chunk-57OW43NL.js} +3 -3
  4. package/dist/chunk-57OW43NL.js.map +1 -0
  5. package/dist/{chunk-AA6CAHCZ.js → chunk-7CRC46HV.js} +2 -2
  6. package/dist/chunk-7CRC46HV.js.map +1 -0
  7. package/dist/{chunk-3JPJTU25.js → chunk-CRTN6BIW.js} +5 -5
  8. package/dist/chunk-CRTN6BIW.js.map +1 -0
  9. package/dist/{chunk-LHIIBI6F.js → chunk-M42XIHPV.js} +2 -2
  10. package/dist/{chunk-2EFVPE5Q.js → chunk-TQOGBAOZ.js} +2 -2
  11. package/dist/chunk-TQOGBAOZ.js.map +1 -0
  12. package/dist/core/index.d.ts +1944 -0
  13. package/dist/{core-YAPWXDZW.js → core/index.js} +5 -5
  14. package/dist/defineFragment-C6PFzZyo.d.ts +656 -0
  15. package/dist/{generate-LEBVZCCH.js → generate-ZPERYZLF.js} +4 -4
  16. package/dist/index.d.ts +4 -159
  17. package/dist/index.js +9 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-4VXL3Q6N.js → init-GID2DXB3.js} +69 -7
  20. package/dist/init-GID2DXB3.js.map +1 -0
  21. package/dist/mcp-bin.js +3 -3
  22. package/dist/{scan-3NYSRF6G.js → scan-BSMLGBX4.js} +5 -5
  23. package/dist/{service-HL6TMP3B.js → service-QACVPR37.js} +3 -3
  24. package/dist/{static-viewer-KLD24I4R.js → static-viewer-2RQD5QLR.js} +3 -3
  25. package/dist/{test-Y7YZOJLE.js → test-36UELXTE.js} +3 -3
  26. package/dist/{tokens-M4FCJKBK.js → tokens-A3BZIQPB.js} +4 -4
  27. package/dist/{viewer-ZWQQ74FV.js → viewer-CNLZQUFO.js} +156 -32
  28. package/dist/viewer-CNLZQUFO.js.map +1 -0
  29. package/package.json +8 -2
  30. package/src/commands/add.ts +1 -1
  31. package/src/commands/init.ts +84 -4
  32. package/src/core/defineFragment.ts +1 -1
  33. package/src/core/figma.ts +1 -1
  34. package/src/core/index.ts +2 -2
  35. package/src/core/loader.ts +3 -3
  36. package/src/core/schema.ts +1 -1
  37. package/src/index.ts +6 -0
  38. package/src/migrate/converter.ts +1 -1
  39. package/src/service/snippet-validation.test.ts +5 -5
  40. package/src/service/snippet-validation.ts +0 -1
  41. package/src/viewer/__tests__/viewer-integration.test.ts +16 -23
  42. package/src/viewer/components/AccessibilityPanel.tsx +1 -1
  43. package/src/viewer/components/ActionsPanel.tsx +1 -1
  44. package/src/viewer/components/App.tsx +563 -166
  45. package/src/viewer/components/BottomPanel.tsx +1 -1
  46. package/src/viewer/components/CodePanel.naming.test.tsx +1 -2
  47. package/src/viewer/components/CodePanel.tsx +1 -2
  48. package/src/viewer/components/CommandPalette.tsx +1 -1
  49. package/src/viewer/components/ComponentGraph.tsx +1 -1
  50. package/src/viewer/components/ComponentHeader.tsx +1 -1
  51. package/src/viewer/components/ContractPanel.tsx +1 -1
  52. package/src/viewer/components/ErrorBoundary.tsx +1 -1
  53. package/src/viewer/components/HealthDashboard.tsx +1 -1
  54. package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
  55. package/src/viewer/components/InteractionsPanel.tsx +1 -1
  56. package/src/viewer/components/IsolatedRender.tsx +1 -1
  57. package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
  58. package/src/viewer/components/LandingPage.tsx +1 -1
  59. package/src/viewer/components/Layout.tsx +16 -13
  60. package/src/viewer/components/LeftSidebar.tsx +105 -18
  61. package/src/viewer/components/MultiViewportPreview.tsx +1 -1
  62. package/src/viewer/components/PreviewArea.tsx +22 -13
  63. package/src/viewer/components/PreviewFrameHost.tsx +0 -4
  64. package/src/viewer/components/PreviewToolbar.tsx +1 -1
  65. package/src/viewer/components/PropsEditor.tsx +1 -1
  66. package/src/viewer/components/PropsTable.tsx +1 -1
  67. package/src/viewer/components/RightSidebar.tsx +1 -1
  68. package/src/viewer/components/ScreenshotButton.tsx +1 -1
  69. package/src/viewer/components/SkeletonLoader.tsx +1 -1
  70. package/src/viewer/components/Toast.tsx +2 -2
  71. package/src/viewer/components/TokenStylePanel.tsx +1 -1
  72. package/src/viewer/components/VariantMatrix.tsx +1 -1
  73. package/src/viewer/components/VariantTabs.tsx +1 -1
  74. package/src/viewer/components/ViewportSelector.tsx +1 -1
  75. package/src/viewer/constants/ui.ts +14 -0
  76. package/src/viewer/entry.tsx +3 -4
  77. package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
  78. package/src/viewer/hooks/useViewSettings.ts +1 -2
  79. package/src/viewer/index.ts +1 -1
  80. package/src/viewer/preview-frame.html +6 -9
  81. package/src/viewer/server.ts +106 -9
  82. package/src/viewer/styles/globals.css +12 -51
  83. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +110 -0
  84. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +89 -0
  85. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +119 -0
  86. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +134 -0
  87. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +66 -0
  88. package/src/viewer/vendor/shared/src/docs-layout.scss +28 -0
  89. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +2 -0
  90. package/src/viewer/vendor/shared/src/index.ts +26 -0
  91. package/src/viewer/vendor/shared/src/types.ts +41 -0
  92. package/src/viewer/vite-plugin.ts +70 -9
  93. package/dist/chunk-2EFVPE5Q.js.map +0 -1
  94. package/dist/chunk-3JPJTU25.js.map +0 -1
  95. package/dist/chunk-AA6CAHCZ.js.map +0 -1
  96. package/dist/chunk-CWKQQR6C.js.map +0 -1
  97. package/dist/init-4VXL3Q6N.js.map +0 -1
  98. package/dist/viewer-ZWQQ74FV.js.map +0 -1
  99. /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
  100. /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
  101. /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
  102. /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
  103. /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
  104. /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
  105. /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
  106. /package/dist/{tokens-M4FCJKBK.js.map → tokens-A3BZIQPB.js.map} +0 -0
@@ -3,20 +3,19 @@
3
3
  * Refactored for better performance and maintainability.
4
4
  */
5
5
 
6
- import { useState, useMemo, useEffect, useCallback, useRef, type RefObject } from "react";
7
- import { BRAND, type FragmentDefinition } from "../../core/index.js";
6
+ import { useState, useMemo, useEffect, useCallback, useRef, type CSSProperties, type ReactNode, type RefObject } from "react";
7
+ import { BRAND, type FragmentDefinition, type FragmentVariant } from "../../core/index.js";
8
8
 
9
9
  // Layout & Navigation
10
10
  import { Layout } from "./Layout.js";
11
11
  import { LeftSidebar } from "./LeftSidebar.js";
12
- import { VariantTabs } from "./VariantTabs.js";
13
12
  import { CommandPalette } from "./CommandPalette.js";
14
13
  import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
15
14
  import { useToast } from "./Toast.js";
16
15
 
17
16
  // Toolbar
18
- import { PreviewToolbar, getBackgroundStyle } from "./PreviewToolbar.js";
19
- import { ViewportSelector } from "./ViewportSelector.js";
17
+ import { PreviewToolbar } from "./PreviewToolbar.js";
18
+ import { getBackgroundStyle } from "../constants/ui.js";
20
19
 
21
20
  // Preview & Rendering
22
21
  import { PreviewArea } from "./PreviewArea.js";
@@ -28,10 +27,11 @@ import { useAllFigmaUrls } from "./FigmaEmbed.js";
28
27
  import { ActionCapture } from "./ActionCapture.js";
29
28
 
30
29
  // Fragments UI
31
- import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, ScrollArea, Input, ThemeToggle } from "@fragments/ui";
30
+ import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, Input, ThemeToggle } from "@fragments-sdk/ui";
31
+ import { DeviceMobile, GridFour, Rows } from "@phosphor-icons/react";
32
32
 
33
33
  // Icons
34
- import { EmptyIcon, ExternalLinkIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
34
+ import { EmptyIcon, FigmaIcon, CompareIcon } from "./Icons.js";
35
35
 
36
36
  // Logo
37
37
  import { fragmentsLogo } from "../assets/fragments-logo.js";
@@ -54,9 +54,6 @@ import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useU
54
54
  import { usePanelDock } from "./ResizablePanel.js";
55
55
  import { useTheme } from "./ThemeProvider.js";
56
56
 
57
- // Utilities
58
- import { ScreenshotButton } from "./ScreenshotButton.js";
59
-
60
57
  interface AppProps {
61
58
  fragments: Array<{ path: string; fragment: FragmentDefinition }>;
62
59
  }
@@ -102,6 +99,8 @@ export function App({ fragments }: AppProps) {
102
99
  }
103
100
  return fragments[0]?.path ?? null;
104
101
  });
102
+ const activeFragmentPathRef = useRef(activeFragmentPath);
103
+ activeFragmentPathRef.current = activeFragmentPath;
105
104
 
106
105
  const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
107
106
  const fragment = fragments.find(s => s.path === activeFragmentPath);
@@ -118,7 +117,11 @@ export function App({ fragments }: AppProps) {
118
117
  () => fragments.find((s) => s.path === activeFragmentPath),
119
118
  [fragments, activeFragmentPath]
120
119
  );
121
- const activeVariant = activeFragment?.fragment.variants?.[activeVariantIndex];
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 isAllVariantsMode = !urlState.variant;
122
125
  const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
123
126
 
124
127
  // Figma integration
@@ -149,17 +152,34 @@ export function App({ fragments }: AppProps) {
149
152
  }
150
153
  }, [uiState.showComparison, activeVariant, figmaIntegration.extractRenderedStyles, uiState.previewKey]);
151
154
 
155
+ // Keep focused variant index in range when variant lists change.
156
+ useEffect(() => {
157
+ if (variantCount === 0) {
158
+ setActiveVariantIndex(0);
159
+ return;
160
+ }
161
+ if (activeVariantIndex >= variantCount) {
162
+ setActiveVariantIndex(variantCount - 1);
163
+ }
164
+ }, [activeVariantIndex, variantCount]);
165
+
152
166
  // Sync URL state on browser navigation
153
167
  useEffect(() => {
154
168
  if (urlState.component) {
155
169
  const found = findFragmentByName(fragments, urlState.component);
156
- if (found && found.path !== activeFragmentPath) {
157
- setActiveFragmentPath(found.path);
170
+ if (!found) return;
171
+
172
+ const pathChanged = found.path !== activeFragmentPathRef.current;
173
+ setActiveFragmentPath(found.path);
174
+ uiActions.setHealthDashboard(false);
175
+
176
+ // Keep focused variant when entering "All" on the same component.
177
+ if (urlState.variant || pathChanged) {
158
178
  const variantIndex = findVariantIndex(found.fragment.variants, urlState.variant);
159
179
  setActiveVariantIndex(variantIndex);
160
180
  }
161
181
  }
162
- }, [urlState.component, urlState.variant, fragments, activeFragmentPath]);
182
+ }, [urlState.component, urlState.variant, fragments, uiActions]);
163
183
 
164
184
  // HMR toast notifications
165
185
  useEffect(() => {
@@ -181,19 +201,64 @@ export function App({ fragments }: AppProps) {
181
201
  const handleSelectFragment = useCallback((path: string) => {
182
202
  const fragment = fragments.find((s) => s.path === path);
183
203
  const componentName = fragment?.fragment.meta.name || path;
184
- const firstVariant = fragment?.fragment.variants?.[0]?.name;
185
204
 
186
205
  setActiveFragmentPath(path);
187
206
  setActiveVariantIndex(0);
188
207
  uiActions.setHealthDashboard(false);
189
- setUrlComponent(componentName, firstVariant);
208
+ setUrlComponent(componentName, null);
190
209
  }, [fragments, setUrlComponent, uiActions]);
191
210
 
211
+ const scrollToVariantSection = useCallback(
212
+ (index: number, behavior: ScrollBehavior = "smooth") => {
213
+ if (!activeFragment || variantCount === 0) return;
214
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
215
+ const targetVariant = variants[normalizedIndex];
216
+ if (!targetVariant) return;
217
+
218
+ const sectionId = getVariantSectionId(activeFragment.fragment.meta.name, targetVariant.name);
219
+ document.getElementById(sectionId)?.scrollIntoView({ behavior, block: "start" });
220
+ },
221
+ [activeFragment, variantCount, variants]
222
+ );
223
+
224
+ const focusVariantInAllMode = useCallback(
225
+ (index: number, shouldScroll = false) => {
226
+ if (variantCount === 0) return;
227
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
228
+ setActiveVariantIndex(normalizedIndex);
229
+ if (shouldScroll) {
230
+ scrollToVariantSection(normalizedIndex);
231
+ }
232
+ },
233
+ [variantCount, scrollToVariantSection]
234
+ );
235
+
192
236
  const handleSelectVariant = useCallback((index: number) => {
193
- const variantName = activeFragment?.fragment.variants?.[index]?.name;
194
- setActiveVariantIndex(index);
237
+ if (variantCount === 0) return;
238
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
239
+ const variantName = variants[normalizedIndex]?.name;
240
+ setActiveVariantIndex(normalizedIndex);
195
241
  setUrlVariant(variantName || null);
196
- }, [activeFragment, setUrlVariant]);
242
+ }, [variantCount, variants, setUrlVariant]);
243
+
244
+ const handleSelectAllVariants = useCallback(() => {
245
+ setUrlVariant(null);
246
+ requestAnimationFrame(() => {
247
+ const previewCanvas = document.getElementById("preview-canvas");
248
+ if (previewCanvas instanceof HTMLElement) {
249
+ previewCanvas.scrollTo({ top: 0, behavior: "smooth" });
250
+ }
251
+ });
252
+ }, [setUrlVariant]);
253
+
254
+ const handleSelectVariantLink = useCallback((index: number) => {
255
+ if (isAllVariantsMode) {
256
+ // In All mode, selecting a variant link exits All and opens that single variant.
257
+ handleSelectVariant(index);
258
+ return;
259
+ }
260
+ handleSelectVariant(index);
261
+ }, [handleSelectVariant, isAllVariantsMode]);
197
262
 
198
263
  // Copy link handler
199
264
  const handleCopyLink = useCallback(async () => {
@@ -219,7 +284,6 @@ export function App({ fragments }: AppProps) {
219
284
  }, [fragments]);
220
285
 
221
286
  const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || '');
222
- const variantCount = activeFragment?.fragment.variants?.length || 0;
223
287
 
224
288
  // Keyboard shortcuts
225
289
  useKeyboardShortcuts(
@@ -232,11 +296,36 @@ export function App({ fragments }: AppProps) {
232
296
  const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
233
297
  handleSelectFragment(sortedFragmentPaths[prevIndex]);
234
298
  },
235
- nextVariant: () => handleSelectVariant(activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0),
236
- prevVariant: () => handleSelectVariant(activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1),
237
- goToVariant: (index) => index < variantCount && handleSelectVariant(index),
299
+ nextVariant: () => {
300
+ if (variantCount === 0) return;
301
+ const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
302
+ if (isAllVariantsMode) {
303
+ focusVariantInAllMode(nextIndex, true);
304
+ return;
305
+ }
306
+ handleSelectVariant(nextIndex);
307
+ },
308
+ prevVariant: () => {
309
+ if (variantCount === 0) return;
310
+ const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
311
+ if (isAllVariantsMode) {
312
+ focusVariantInAllMode(prevIndex, true);
313
+ return;
314
+ }
315
+ handleSelectVariant(prevIndex);
316
+ },
317
+ goToVariant: (index) => {
318
+ if (index >= variantCount) return;
319
+ if (isAllVariantsMode) {
320
+ focusVariantInAllMode(index, true);
321
+ return;
322
+ }
323
+ handleSelectVariant(index);
324
+ },
238
325
  toggleTheme: viewSettings.toggleTheme,
239
326
  togglePanel: uiActions.togglePanel,
327
+ toggleMatrix: () => uiActions.setMatrixView(!uiState.showMatrixView),
328
+ toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
240
329
  copyLink: handleCopyLink,
241
330
  showHelp: uiActions.toggleShortcutsHelp,
242
331
  openSearch: focusSearchInput,
@@ -256,22 +345,22 @@ export function App({ fragments }: AppProps) {
256
345
  );
257
346
 
258
347
  // Render variant with action logging via DOM event capture
259
- const renderVariantWithProps = useCallback(() => {
260
- if (!activeVariant) return null;
348
+ const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
349
+ if (!variant) return null;
261
350
 
262
351
  return (
263
352
  <ActionCapture onAction={useActionsRef.current.logAction}>
264
- <StoryRenderer variant={activeVariant}>
353
+ <StoryRenderer variant={variant}>
265
354
  {(content, isLoading, error) => {
266
355
  if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
267
- if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={activeVariant.name} hint="Check the console for the full error stack trace." />;
268
- if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={activeVariant.name} hint="The variant's render function didn't return any JSX." />;
356
+ if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={variant.name} hint="Check the console for the full error stack trace." />;
357
+ if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={variant.name} hint="The variant's render function didn't return any JSX." />;
269
358
  return content;
270
359
  }}
271
360
  </StoryRenderer>
272
361
  </ActionCapture>
273
362
  );
274
- }, [activeVariant]);
363
+ }, []);
275
364
 
276
365
  // Check if isolated mode
277
366
  const isIsolated = useMemo(() => {
@@ -302,13 +391,9 @@ export function App({ fragments }: AppProps) {
302
391
  activeFragment && !uiState.showHealthDashboard ? (
303
392
  <TopToolbar
304
393
  fragment={activeFragment}
305
- variant={activeVariant}
306
- viewSettings={viewSettings}
307
394
  uiState={uiState}
308
395
  uiActions={uiActions}
309
396
  figmaUrl={figmaUrl}
310
- linkCopied={uiState.linkCopied}
311
- onCopyLink={handleCopyLink}
312
397
  searchQuery={searchQuery}
313
398
  onSearchChange={setSearchQuery}
314
399
  searchInputRef={searchInputRef}
@@ -335,6 +420,21 @@ export function App({ fragments }: AppProps) {
335
420
  }}
336
421
  />
337
422
  }
423
+ aside={
424
+ activeFragment && !uiState.showHealthDashboard ? (
425
+ <PreviewAside
426
+ fragment={activeFragment.fragment}
427
+ variants={variants}
428
+ focusedVariantIndex={safeVariantIndex}
429
+ isAllVariantsMode={isAllVariantsMode}
430
+ activePanel={uiState.activePanel}
431
+ onSelectAllVariants={handleSelectAllVariants}
432
+ onSelectVariant={handleSelectVariantLink}
433
+ onCopyLink={handleCopyLink}
434
+ onShowShortcuts={uiActions.toggleShortcutsHelp}
435
+ />
436
+ ) : null
437
+ }
338
438
  >
339
439
  {uiState.showHealthDashboard ? (
340
440
  <div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
@@ -352,24 +452,25 @@ export function App({ fragments }: AppProps) {
352
452
  </Box>
353
453
  </div>
354
454
  ) : activeFragment ? (
355
- <div style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
455
+ <div id="preview-layout" style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
356
456
  {/* Main Content Area */}
357
457
  <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
358
- {/* Variant Tabs */}
359
- {activeFragment.fragment.variants && activeFragment.fragment.variants.length > 0 && (
360
- <VariantTabsBar
361
- variants={activeFragment.fragment.variants}
362
- activeIndex={activeVariantIndex}
363
- onSelect={handleSelectVariant}
364
- showMatrixView={uiState.showMatrixView}
365
- showMultiViewport={uiState.showMultiViewport}
366
- onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
367
- onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
368
- />
369
- )}
458
+ <PreviewControlsBar
459
+ zoom={viewSettings.zoom}
460
+ background={viewSettings.background}
461
+ onZoomChange={viewSettings.setZoom}
462
+ onBackgroundChange={viewSettings.setBackground}
463
+ showMatrixView={uiState.showMatrixView}
464
+ showMultiViewport={uiState.showMultiViewport}
465
+ panelOpen={uiState.panelOpen}
466
+ onToggleMatrix={() => uiActions.setMatrixView(!uiState.showMatrixView)}
467
+ onToggleMultiViewport={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
468
+ onTogglePanel={uiActions.togglePanel}
469
+ />
370
470
 
371
471
  {/* Preview Area */}
372
472
  <div
473
+ id="preview-canvas"
373
474
  style={{
374
475
  flex: 1,
375
476
  overflow: 'auto',
@@ -377,12 +478,32 @@ export function App({ fragments }: AppProps) {
377
478
  ...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
378
479
  }}
379
480
  >
380
- {activeVariant ? (
481
+ {variantCount === 0 ? (
482
+ <NoVariantsMessage fragment={activeFragment?.fragment} />
483
+ ) : isAllVariantsMode && !uiState.showMatrixView && !uiState.showMultiViewport ? (
484
+ <AllVariantsPreview
485
+ componentName={activeFragment.fragment.meta.name}
486
+ fragmentPath={activeFragment.path}
487
+ variants={variants}
488
+ focusedVariantIndex={safeVariantIndex}
489
+ zoom={viewSettings.zoom}
490
+ background={viewSettings.background}
491
+ viewport={viewSettings.viewport}
492
+ customSize={viewSettings.customSize}
493
+ previewTheme={resolvedTheme}
494
+ showComparison={uiState.showComparison}
495
+ allFigmaUrls={allFigmaUrls}
496
+ fallbackFigmaUrl={activeFragment.fragment.meta.figma}
497
+ onRetry={uiActions.incrementPreviewKey}
498
+ renderVariantContent={renderVariantWithProps}
499
+ previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
500
+ />
501
+ ) : (
381
502
  <PreviewArea
382
503
  componentName={activeFragment.fragment.meta.name}
383
504
  fragmentPath={activeFragment.path}
384
505
  variant={activeVariant}
385
- variants={activeFragment.fragment.variants}
506
+ variants={variants}
386
507
  zoom={viewSettings.zoom}
387
508
  background={viewSettings.background}
388
509
  viewport={viewSettings.viewport}
@@ -398,40 +519,40 @@ export function App({ fragments }: AppProps) {
398
519
  handleSelectVariant(index);
399
520
  }}
400
521
  onRetry={uiActions.incrementPreviewKey}
401
- renderContent={renderVariantWithProps}
402
- previewKey={`${activeFragmentPath}-${activeVariantIndex}-${uiState.previewKey}`}
522
+ renderContent={() => renderVariantWithProps(activeVariant)}
523
+ previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
403
524
  />
404
- ) : (
405
- <NoVariantsMessage fragment={activeFragment?.fragment} />
406
525
  )}
407
526
  </div>
408
527
  </div>
409
528
 
410
529
  {/* Bottom Panel */}
411
- {activeVariant && (
412
- <BottomPanel
413
- fragment={activeFragment.fragment}
414
- variant={activeVariant}
415
- fragments={fragments}
416
- activePanel={uiState.activePanel}
417
- onPanelChange={uiActions.setActivePanel}
418
- figmaUrl={figmaUrl}
419
- figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
420
- renderedStyles={figmaIntegration.renderedStyles}
421
- figmaLoading={figmaIntegration.isLoading}
422
- figmaError={figmaIntegration.errorMessage}
423
- onFetchFigma={figmaIntegration.fetchFigmaStyles}
424
- onRefreshRendered={figmaIntegration.extractRenderedStyles}
425
- actionLogs={actionLogs}
426
- onClearActionLogs={clearActionLogs}
427
- onNavigateToComponent={(name) => {
428
- const target = fragments.find(s => s.fragment.meta.name === name);
429
- if (target) handleSelectFragment(target.path);
430
- }}
431
- previewKey={uiState.previewKey}
432
- fragmentKey={`${activeFragmentPath}-${activeVariantIndex}`}
433
- />
434
- )}
530
+ <div id="preview-tools">
531
+ {uiState.panelOpen && activeVariant && (
532
+ <BottomPanel
533
+ fragment={activeFragment.fragment}
534
+ variant={activeVariant}
535
+ fragments={fragments}
536
+ activePanel={uiState.activePanel}
537
+ onPanelChange={uiActions.setActivePanel}
538
+ figmaUrl={figmaUrl}
539
+ figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
540
+ renderedStyles={figmaIntegration.renderedStyles}
541
+ figmaLoading={figmaIntegration.isLoading}
542
+ figmaError={figmaIntegration.errorMessage}
543
+ onFetchFigma={figmaIntegration.fetchFigmaStyles}
544
+ onRefreshRendered={figmaIntegration.extractRenderedStyles}
545
+ actionLogs={actionLogs}
546
+ onClearActionLogs={clearActionLogs}
547
+ onNavigateToComponent={(name) => {
548
+ const target = fragments.find(s => s.fragment.meta.name === name);
549
+ if (target) handleSelectFragment(target.path);
550
+ }}
551
+ previewKey={uiState.previewKey}
552
+ fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
553
+ />
554
+ )}
555
+ </div>
435
556
  </div>
436
557
  ) : (
437
558
  <EmptyState style={{ height: '100%' }}>
@@ -450,13 +571,9 @@ export function App({ fragments }: AppProps) {
450
571
  // Top Toolbar Component
451
572
  interface TopToolbarProps {
452
573
  fragment: { path: string; fragment: FragmentDefinition };
453
- variant: any;
454
- viewSettings: ReturnType<typeof useViewSettings>;
455
574
  uiState: ReturnType<typeof useAppState>['state'];
456
575
  uiActions: ReturnType<typeof useAppState>['actions'];
457
576
  figmaUrl?: string;
458
- linkCopied: boolean;
459
- onCopyLink: () => void;
460
577
  searchQuery: string;
461
578
  onSearchChange: (value: string) => void;
462
579
  searchInputRef: RefObject<HTMLInputElement>;
@@ -475,6 +592,18 @@ interface HeaderSearchProps {
475
592
  inputRef: RefObject<HTMLInputElement>;
476
593
  }
477
594
 
595
+ interface PreviewAsideProps {
596
+ fragment: FragmentDefinition;
597
+ variants: FragmentVariant[];
598
+ focusedVariantIndex: number;
599
+ isAllVariantsMode: boolean;
600
+ activePanel: string;
601
+ onSelectAllVariants: () => void;
602
+ onSelectVariant: (index: number) => void;
603
+ onCopyLink: () => void;
604
+ onShowShortcuts: () => void;
605
+ }
606
+
478
607
  function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
479
608
  return (
480
609
  <Header.Search expandable>
@@ -536,15 +665,113 @@ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef
536
665
  );
537
666
  }
538
667
 
668
+ function PreviewAside({
669
+ fragment,
670
+ variants,
671
+ focusedVariantIndex,
672
+ isAllVariantsMode,
673
+ activePanel,
674
+ onSelectAllVariants,
675
+ onSelectVariant,
676
+ onCopyLink,
677
+ onShowShortcuts,
678
+ }: PreviewAsideProps) {
679
+ const focusedVariant = variants[focusedVariantIndex] || null;
680
+
681
+ const baseLinkStyle: CSSProperties = {
682
+ color: 'var(--text-secondary)',
683
+ textDecoration: 'none',
684
+ fontSize: '13px',
685
+ borderRadius: '6px',
686
+ padding: '4px 8px',
687
+ display: 'block',
688
+ };
689
+
690
+ const getLinkStyle = (isActive = false): CSSProperties => (
691
+ isActive
692
+ ? { ...baseLinkStyle, color: 'var(--text-primary)', backgroundColor: 'var(--bg-hover)' }
693
+ : baseLinkStyle
694
+ );
695
+
696
+ return (
697
+ <Box padding="md" style={{ position: 'sticky', top: '80px' }}>
698
+ <Stack gap="md">
699
+ <Stack gap="xs">
700
+ <Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em' }}>
701
+ On this page
702
+ </Text>
703
+ <a href="#preview-canvas" style={baseLinkStyle}>
704
+ Preview
705
+ </a>
706
+ <a href="#preview-tools" style={baseLinkStyle}>
707
+ Panels
708
+ </a>
709
+ {variants.length > 0 && (
710
+ <>
711
+ <Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '8px' }}>
712
+ Variants
713
+ </Text>
714
+ <a
715
+ href="#preview-canvas"
716
+ style={getLinkStyle(isAllVariantsMode)}
717
+ onClick={(event) => {
718
+ event.preventDefault();
719
+ onSelectAllVariants();
720
+ }}
721
+ >
722
+ All
723
+ </a>
724
+ {variants.map((variant, index) => {
725
+ const active = index === focusedVariantIndex;
726
+
727
+ return (
728
+ <a
729
+ key={variant.name}
730
+ href="#preview-canvas"
731
+ style={getLinkStyle(active)}
732
+ onClick={(event) => {
733
+ event.preventDefault();
734
+ onSelectVariant(index);
735
+ }}
736
+ >
737
+ {variant.name}
738
+ </a>
739
+ );
740
+ })}
741
+ </>
742
+ )}
743
+ </Stack>
744
+ <Separator />
745
+ <Stack gap="xs">
746
+ <Text size="sm" weight="medium">{fragment.meta.name}</Text>
747
+ <Text size="xs" color="secondary">
748
+ {isAllVariantsMode
749
+ ? `Variant: All${focusedVariant ? ` (focused: ${focusedVariant.name})` : ''}`
750
+ : focusedVariant ? `Variant: ${focusedVariant.name}` : 'Select a variant'}
751
+ </Text>
752
+ <Text size="xs" color="tertiary">
753
+ Active panel: {activePanel}
754
+ </Text>
755
+ </Stack>
756
+ <Separator />
757
+ <Stack gap="xs">
758
+ <Button variant="ghost" size="sm" onClick={onCopyLink}>
759
+ Copy Link
760
+ </Button>
761
+ <Button variant="ghost" size="sm" onClick={onShowShortcuts}>
762
+ Keyboard Shortcuts
763
+ </Button>
764
+ </Stack>
765
+ </Stack>
766
+ </Box>
767
+ );
768
+ }
769
+
539
770
  function TopToolbar({
540
771
  fragment,
541
- variant,
542
- viewSettings,
543
772
  uiState,
544
773
  uiActions,
545
774
  figmaUrl,
546
- linkCopied,
547
- onCopyLink,
548
775
  searchQuery,
549
776
  onSearchChange,
550
777
  searchInputRef,
@@ -563,21 +790,6 @@ function TopToolbar({
563
790
  <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
564
791
  <Header.Spacer />
565
792
  <Header.Actions>
566
- <PreviewToolbar
567
- zoom={viewSettings.zoom}
568
- background={viewSettings.background}
569
- onZoomChange={viewSettings.setZoom}
570
- onBackgroundChange={viewSettings.setBackground}
571
- />
572
- <Separator orientation="vertical" style={{ height: '16px' }} />
573
- <ViewportSelector
574
- viewport={viewSettings.viewport}
575
- customSize={viewSettings.customSize}
576
- onViewportChange={viewSettings.setViewport}
577
- onCustomSizeChange={viewSettings.setCustomSize}
578
- />
579
- <Separator orientation="vertical" style={{ height: '16px' }} />
580
-
581
793
  {figmaUrl && (
582
794
  <>
583
795
  <Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
@@ -602,41 +814,6 @@ function TopToolbar({
602
814
  <Separator orientation="vertical" style={{ height: '16px' }} />
603
815
  </>
604
816
  )}
605
-
606
- {variant && (
607
- <>
608
- <Tooltip content="Open in new window">
609
- <Button
610
- onClick={() => {
611
- const url = new URL(window.location.href);
612
- url.hash = '';
613
- url.searchParams.set('isolated', 'true');
614
- url.searchParams.set('component', fragment.fragment.meta.name);
615
- url.searchParams.set('variant', variant.name);
616
- if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
617
- if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
618
- window.open(url.toString(), '_blank', 'noopener,noreferrer');
619
- }}
620
- variant="ghost"
621
- size="sm"
622
- >
623
- <ExternalLinkIcon style={{ width: '16px', height: '16px' }} />
624
- </Button>
625
- </Tooltip>
626
- <ScreenshotButton componentName={fragment.fragment.meta.name} variantName={variant.name} />
627
- <Tooltip content="Copy link to share">
628
- <Button
629
- onClick={onCopyLink}
630
- variant="ghost"
631
- size="sm"
632
- style={linkCopied ? { color: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.1)' } : {}}
633
- >
634
- {linkCopied ? <CheckIcon style={{ width: '16px', height: '16px' }} /> : <LinkIcon style={{ width: '16px', height: '16px' }} />}
635
- </Button>
636
- </Tooltip>
637
- </>
638
- )}
639
- <Separator orientation="vertical" style={{ height: '16px' }} />
640
817
  <ThemeToggle
641
818
  size="sm"
642
819
  value={resolvedTheme}
@@ -666,52 +843,180 @@ function TopToolbar({
666
843
  );
667
844
  }
668
845
 
669
- // Variant Tabs Bar Component
670
- interface VariantTabsBarProps {
671
- variants: any[];
672
- activeIndex: number;
673
- onSelect: (index: number) => void;
846
+ function normalizeAnchorSegment(value: string): string {
847
+ const normalized = value
848
+ .toLowerCase()
849
+ .trim()
850
+ .replace(/[^a-z0-9]+/g, "-")
851
+ .replace(/^-+|-+$/g, "");
852
+ return normalized || "variant";
853
+ }
854
+
855
+ function getVariantSectionId(componentName: string, variantName: string): string {
856
+ return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
857
+ }
858
+
859
+ interface PreviewControlsBarProps {
860
+ zoom: ReturnType<typeof useViewSettings>["zoom"];
861
+ background: ReturnType<typeof useViewSettings>["background"];
862
+ onZoomChange: ReturnType<typeof useViewSettings>["setZoom"];
863
+ onBackgroundChange: ReturnType<typeof useViewSettings>["setBackground"];
674
864
  showMatrixView: boolean;
675
865
  showMultiViewport: boolean;
866
+ panelOpen: boolean;
676
867
  onToggleMatrix: () => void;
677
868
  onToggleMultiViewport: () => void;
869
+ onTogglePanel: () => void;
678
870
  }
679
871
 
680
- function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showMultiViewport, onToggleMatrix, onToggleMultiViewport }: VariantTabsBarProps) {
872
+ function PreviewControlsBar({
873
+ zoom,
874
+ background,
875
+ onZoomChange,
876
+ onBackgroundChange,
877
+ showMatrixView,
878
+ showMultiViewport,
879
+ panelOpen,
880
+ onToggleMatrix,
881
+ onToggleMultiViewport,
882
+ onTogglePanel,
883
+ }: PreviewControlsBarProps) {
884
+ const toggleButtonStyle = (active: boolean): CSSProperties => (
885
+ active ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}
886
+ );
887
+
681
888
  return (
682
- <Stack direction="row" align="center" justify="between" style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
683
- {!showMatrixView ? (
684
- <ScrollArea orientation="horizontal" showFades style={{ flex: 1, minWidth: 0 }}>
685
- <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
686
- </ScrollArea>
687
- ) : (
688
- <Text size="sm" color="secondary">Showing all {variants.length} variants</Text>
689
- )}
690
- <Stack direction="row" align="center" gap="sm" style={{ marginLeft: '16px', flexShrink: 0 }}>
691
- {variants.length > 1 && (
889
+ <div style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
890
+ <Stack direction="row" gap="sm" align="center" justify="end">
891
+ <PreviewToolbar
892
+ zoom={zoom}
893
+ background={background}
894
+ onZoomChange={onZoomChange}
895
+ onBackgroundChange={onBackgroundChange}
896
+ />
897
+ <Separator orientation="vertical" style={{ height: '16px' }} />
898
+ <Tooltip content={showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
692
899
  <Button
900
+ variant="ghost"
901
+ size="sm"
902
+ aria-pressed={showMatrixView}
903
+ aria-label="Toggle matrix view"
693
904
  onClick={onToggleMatrix}
905
+ style={toggleButtonStyle(showMatrixView)}
906
+ >
907
+ <GridFour size={16} />
908
+ </Button>
909
+ </Tooltip>
910
+ <Tooltip content={showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
911
+ <Button
694
912
  variant="ghost"
695
913
  size="sm"
696
- title={showMatrixView ? "Show single variant" : "Show all variants in grid"}
697
- style={showMatrixView ? { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: 'var(--color-accent)' } : {}}
914
+ aria-pressed={showMultiViewport}
915
+ aria-label="Toggle responsive view"
916
+ onClick={onToggleMultiViewport}
917
+ style={toggleButtonStyle(showMultiViewport)}
698
918
  >
699
- <GridIcon style={{ width: '16px', height: '16px' }} />
700
- {showMatrixView ? "Exit Matrix" : "Matrix"}
919
+ <DeviceMobile size={16} />
701
920
  </Button>
702
- )}
703
- <Button
704
- onClick={onToggleMultiViewport}
705
- variant="ghost"
706
- size="sm"
707
- title={showMultiViewport ? "Exit multi-viewport" : "Show at multiple screen sizes"}
708
- style={showMultiViewport ? { backgroundColor: 'rgba(34, 197, 94, 0.1)', color: '#16a34a' } : {}}
709
- >
710
- <DevicesIcon style={{ width: '16px', height: '16px' }} />
711
- {showMultiViewport ? "Exit Responsive" : "Responsive"}
712
- </Button>
921
+ </Tooltip>
922
+ <Tooltip content={panelOpen ? "Hide addons panel" : "Show addons panel"}>
923
+ <Button
924
+ variant="ghost"
925
+ size="sm"
926
+ aria-pressed={panelOpen}
927
+ aria-label="Toggle addons panel"
928
+ onClick={onTogglePanel}
929
+ style={toggleButtonStyle(panelOpen)}
930
+ >
931
+ <Rows size={16} />
932
+ </Button>
933
+ </Tooltip>
713
934
  </Stack>
714
- </Stack>
935
+ </div>
936
+ );
937
+ }
938
+
939
+ interface AllVariantsPreviewProps {
940
+ componentName: string;
941
+ fragmentPath: string;
942
+ variants: FragmentVariant[];
943
+ focusedVariantIndex: number;
944
+ zoom: ReturnType<typeof useViewSettings>["zoom"];
945
+ background: ReturnType<typeof useViewSettings>["background"];
946
+ viewport: ReturnType<typeof useViewSettings>["viewport"];
947
+ customSize: ReturnType<typeof useViewSettings>["customSize"];
948
+ previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
949
+ showComparison: boolean;
950
+ allFigmaUrls: string[];
951
+ fallbackFigmaUrl?: string;
952
+ onRetry: () => void;
953
+ renderVariantContent: (variant: FragmentVariant) => ReactNode;
954
+ previewKeyBase: string;
955
+ }
956
+
957
+ function AllVariantsPreview({
958
+ componentName,
959
+ fragmentPath,
960
+ variants,
961
+ focusedVariantIndex,
962
+ zoom,
963
+ background,
964
+ viewport,
965
+ customSize,
966
+ previewTheme,
967
+ showComparison,
968
+ allFigmaUrls,
969
+ fallbackFigmaUrl,
970
+ onRetry,
971
+ renderVariantContent,
972
+ previewKeyBase,
973
+ }: AllVariantsPreviewProps) {
974
+ return (
975
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
976
+ {variants.map((variant, index) => {
977
+ const isFocused = index === focusedVariantIndex;
978
+
979
+ return (
980
+ <section
981
+ id={getVariantSectionId(componentName, variant.name)}
982
+ key={variant.name}
983
+ style={{
984
+ border: '1px solid var(--border)',
985
+ borderColor: isFocused ? 'var(--color-accent)' : 'var(--border)',
986
+ borderRadius: '10px',
987
+ overflow: 'hidden',
988
+ backgroundColor: 'var(--bg-primary)',
989
+ boxShadow: isFocused ? '0 0 0 1px color-mix(in srgb, var(--color-accent) 40%, transparent)' : undefined,
990
+ }}
991
+ >
992
+ <Stack direction="row" align="baseline" gap="sm" style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
993
+ <Text size="sm" weight="medium">{variant.name}</Text>
994
+ <Text size="xs" color="secondary">{variant.description}</Text>
995
+ </Stack>
996
+ <PreviewArea
997
+ componentName={componentName}
998
+ fragmentPath={fragmentPath}
999
+ variant={variant}
1000
+ variants={variants}
1001
+ zoom={zoom}
1002
+ background={background}
1003
+ viewport={viewport}
1004
+ customSize={customSize}
1005
+ previewTheme={previewTheme}
1006
+ showMatrixView={false}
1007
+ showMultiViewport={false}
1008
+ showComparison={showComparison}
1009
+ figmaUrl={variant.figma || fallbackFigmaUrl}
1010
+ allFigmaUrls={allFigmaUrls}
1011
+ onSelectVariant={() => {}}
1012
+ onRetry={onRetry}
1013
+ renderContent={() => renderVariantContent(variant)}
1014
+ previewKey={`${previewKeyBase}-${index}`}
1015
+ />
1016
+ </section>
1017
+ );
1018
+ })}
1019
+ </div>
715
1020
  );
716
1021
  }
717
1022
 
@@ -721,6 +1026,12 @@ interface NoVariantsMessageProps {
721
1026
  }
722
1027
 
723
1028
  function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
1029
+ // Check for load error (missing dependencies, schema errors, etc.)
1030
+ const loadError = (fragment as any)?._loadError;
1031
+ if (loadError) {
1032
+ return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
1033
+ }
1034
+
724
1035
  const skippedVariants = (fragment?._generated as any)?.skippedVariants;
725
1036
 
726
1037
  if (!skippedVariants || skippedVariants.length === 0) {
@@ -761,6 +1072,92 @@ function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
761
1072
  );
762
1073
  }
763
1074
 
1075
+ // Load error message — shown when a fragment failed to import (missing deps, schema errors, etc.)
1076
+ function LoadErrorMessage({ error, componentName }: { error: { message: string; dependencies: string[] }; componentName?: string }) {
1077
+ const deps = error.dependencies || [];
1078
+ const errorMessage = error.message || 'Unknown error';
1079
+
1080
+ // Determine if the error is a missing module/dependency issue
1081
+ const isModuleError = /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(errorMessage);
1082
+
1083
+ // Only suggest packages if the error is actually about missing modules
1084
+ let suggestedPackages: string[] = [];
1085
+ if (isModuleError) {
1086
+ if (deps.length > 0) {
1087
+ suggestedPackages = [...deps];
1088
+ } else {
1089
+ const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
1090
+ if (match) {
1091
+ suggestedPackages = [match[1]];
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ const hasMissingDeps = suggestedPackages.length > 0;
1097
+ const installCmd = `pnpm add ${suggestedPackages.join(' ')}`;
1098
+
1099
+ return (
1100
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
1101
+ <Alert variant="warning">
1102
+ <Alert.Body>
1103
+ <Alert.Title>
1104
+ {hasMissingDeps ? 'Missing Dependencies' : 'Failed to Load'}
1105
+ </Alert.Title>
1106
+ <Alert.Content>
1107
+ <Stack direction="column" gap="sm">
1108
+ {hasMissingDeps ? (
1109
+ <>
1110
+ <Text size="xs" color="secondary">
1111
+ {componentName ? `${componentName} requires` : 'This component requires'} packages that are not installed in your project.
1112
+ </Text>
1113
+ <Text size="xs" weight="semibold" color="secondary">Install with:</Text>
1114
+ <code style={{
1115
+ display: 'block',
1116
+ padding: '8px 12px',
1117
+ borderRadius: '6px',
1118
+ backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
1119
+ fontFamily: 'monospace',
1120
+ fontSize: '12px',
1121
+ color: 'var(--text-primary, #111827)',
1122
+ userSelect: 'all',
1123
+ }}>
1124
+ {installCmd}
1125
+ </code>
1126
+ <Text size="xs" color="tertiary">
1127
+ After installing, restart the dev server.
1128
+ </Text>
1129
+ </>
1130
+ ) : (
1131
+ <>
1132
+ <Text size="xs" color="secondary">
1133
+ {componentName ? `${componentName} couldn't` : 'This component couldn\'t'} be loaded. This may be due to a schema validation error or missing imports.
1134
+ </Text>
1135
+ <Text size="xs" weight="semibold" color="secondary">Error:</Text>
1136
+ <pre style={{
1137
+ padding: '8px 12px',
1138
+ borderRadius: '6px',
1139
+ backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
1140
+ fontFamily: 'monospace',
1141
+ fontSize: '11px',
1142
+ color: 'var(--text-secondary, #374151)',
1143
+ whiteSpace: 'pre-wrap',
1144
+ wordBreak: 'break-word',
1145
+ margin: 0,
1146
+ maxHeight: '200px',
1147
+ overflow: 'auto',
1148
+ }}>
1149
+ {errorMessage}
1150
+ </pre>
1151
+ </>
1152
+ )}
1153
+ </Stack>
1154
+ </Alert.Content>
1155
+ </Alert.Body>
1156
+ </Alert>
1157
+ </div>
1158
+ );
1159
+ }
1160
+
764
1161
  // Empty variant message
765
1162
  interface EmptyVariantMessageProps {
766
1163
  reason: string;