@fragments-sdk/cli 0.9.0 → 0.9.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 (123) hide show
  1. package/dist/bin.js +83 -33
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/build.ts +12 -2
  37. package/src/commands/build.ts +16 -2
  38. package/src/commands/generate.ts +383 -68
  39. package/src/commands/init.ts +9 -51
  40. package/src/core/config.ts +15 -2
  41. package/src/core/generators/typescript-extractor.ts +10 -0
  42. package/src/core/index.ts +15 -0
  43. package/src/core/schema.ts +10 -2
  44. package/src/core/storyFilters.test.ts +350 -0
  45. package/src/core/storyFilters.ts +253 -0
  46. package/src/core/types.ts +22 -0
  47. package/src/migrate/converter.ts +9 -1
  48. package/src/migrate/parser.ts +2 -0
  49. package/src/migrate/types.ts +2 -0
  50. package/src/setup.ts +69 -24
  51. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  52. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  53. package/src/viewer/components/ActionsPanel.tsx +31 -29
  54. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  55. package/src/viewer/components/App.tsx +187 -740
  56. package/src/viewer/components/BottomPanel.tsx +228 -132
  57. package/src/viewer/components/CodePanel.tsx +1 -1
  58. package/src/viewer/components/CommandPalette.tsx +7 -10
  59. package/src/viewer/components/ComponentDocView.tsx +164 -0
  60. package/src/viewer/components/ComponentGraph.tsx +111 -142
  61. package/src/viewer/components/ContractPanel.tsx +6 -6
  62. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  63. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  64. package/src/viewer/components/FragmentEditor.tsx +92 -115
  65. package/src/viewer/components/HeaderSearch.tsx +24 -0
  66. package/src/viewer/components/HealthDashboard.tsx +16 -2
  67. package/src/viewer/components/Icons.tsx +9 -0
  68. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  69. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  70. package/src/viewer/components/LandingPage.tsx +3 -3
  71. package/src/viewer/components/LeftSidebar.tsx +141 -63
  72. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  73. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  74. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  75. package/src/viewer/components/PanelShell.tsx +161 -0
  76. package/src/viewer/components/PerformancePanel.tsx +31 -28
  77. package/src/viewer/components/PreviewArea.tsx +1 -1
  78. package/src/viewer/components/PreviewAside.tsx +168 -0
  79. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  80. package/src/viewer/components/PropsEditor.tsx +70 -156
  81. package/src/viewer/components/ResizablePanel.tsx +103 -263
  82. package/src/viewer/components/RightSidebar.tsx +3 -9
  83. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  84. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  85. package/src/viewer/components/TopToolbar.tsx +159 -0
  86. package/src/viewer/components/VariantMatrix.tsx +42 -86
  87. package/src/viewer/components/VariantTabs.tsx +3 -3
  88. package/src/viewer/components/ViewerHeader.tsx +69 -0
  89. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  90. package/src/viewer/components/viewer-utils.ts +16 -0
  91. package/src/viewer/entry.tsx +5 -0
  92. package/src/viewer/hooks/useAppState.ts +27 -4
  93. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  94. package/src/viewer/preview-frame.html +6 -12
  95. package/src/viewer/server.ts +169 -2
  96. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  97. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  98. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  99. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  100. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  101. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  102. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  105. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  108. package/src/viewer/vendor/shared/src/index.ts +8 -0
  109. package/src/viewer/vendor/shared/src/types.ts +12 -0
  110. package/src/viewer/vite-plugin.ts +109 -4
  111. package/dist/chunk-2JIKCJX3.js.map +0 -1
  112. package/dist/chunk-CJEGT3WD.js.map +0 -1
  113. package/dist/chunk-GOVI6COW.js.map +0 -1
  114. package/dist/generate-35OIMW4Y.js +0 -252
  115. package/dist/generate-35OIMW4Y.js.map +0 -1
  116. package/dist/init-KSAAS7X3.js.map +0 -1
  117. package/dist/viewer-SBTJDMP7.js.map +0 -1
  118. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  119. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  120. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  121. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  122. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  123. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -3,8 +3,8 @@
3
3
  * Refactored for better performance and maintainability.
4
4
  */
5
5
 
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";
6
+ import { useState, useMemo, useEffect, useCallback, useRef, type ReactNode } from "react";
7
+ import type { FragmentDefinition, FragmentVariant } from "../../core/index.js";
8
8
 
9
9
  // Layout & Navigation
10
10
  import { Layout } from "./Layout.js";
@@ -13,8 +13,9 @@ import { CommandPalette } from "./CommandPalette.js";
13
13
  import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
14
14
  import { useToast } from "./Toast.js";
15
15
 
16
- // Toolbar
17
- import { PreviewToolbar } from "./PreviewToolbar.js";
16
+ // Toolbar & Header
17
+ import { TopToolbar } from "./TopToolbar.js";
18
+ import { ViewerHeader } from "./ViewerHeader.js";
18
19
 
19
20
  // Preview & Rendering
20
21
  import { PreviewArea } from "./PreviewArea.js";
@@ -25,21 +26,21 @@ import { HealthDashboard } from "./HealthDashboard.js";
25
26
  import { useAllFigmaUrls } from "./FigmaEmbed.js";
26
27
  import { ActionCapture } from "./ActionCapture.js";
27
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
+
28
36
  // Fragments UI
29
- import { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, Input, ThemeToggle, FragmentsLogo } from "@fragments-sdk/ui";
30
- import { DeviceMobile, GridFour, Rows } from "@phosphor-icons/react";
37
+ import { Stack, Box, EmptyState } from "@fragments-sdk/ui";
31
38
 
32
39
  // Icons
33
- import { EmptyIcon, FigmaIcon, CompareIcon } from "./Icons.js";
34
-
40
+ import { EmptyIcon } from "./Icons.js";
35
41
 
36
- function GitHubIcon() {
37
- return (
38
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
39
- <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
40
- </svg>
41
- );
42
- }
42
+ // Utilities
43
+ import { getVariantSectionId } from "./viewer-utils.js";
43
44
 
44
45
  // Hooks
45
46
  import { useAppState } from "../hooks/useAppState.js";
@@ -48,9 +49,7 @@ import { useFigmaIntegration } from "../hooks/useFigmaIntegration.js";
48
49
  import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
49
50
  import { useActions } from "../hooks/useActions.js";
50
51
  import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useUrlState.js";
51
- import { usePanelDock } from "./ResizablePanel.js";
52
52
  import { useTheme } from "./ThemeProvider.js";
53
- import { WebMCPStatusIndicator } from "./WebMCPStatusIndicator.js";
54
53
  import { ViewerStateSync } from "./ViewerStateSync.js";
55
54
 
56
55
  interface AppProps {
@@ -59,7 +58,13 @@ interface AppProps {
59
58
 
60
59
  export function App({ fragments }: AppProps) {
61
60
  // URL state management
62
- const { state: urlState, setComponent: setUrlComponent, setVariant: setUrlVariant, setViewSettings: setUrlViewSettings, copyUrl } = useUrlState();
61
+ const {
62
+ state: urlState,
63
+ setComponent: setUrlComponent,
64
+ setVariant: setUrlVariant,
65
+ setViewSettings: setUrlViewSettings,
66
+ copyUrl,
67
+ } = useUrlState();
63
68
 
64
69
  // UI state (modals, panels, view modes)
65
70
  const { state: uiState, actions: uiActions } = useAppState();
@@ -72,16 +77,14 @@ export function App({ fragments }: AppProps) {
72
77
  customSize: { width: urlState.customWidth, height: urlState.customHeight },
73
78
  },
74
79
  onZoomChange: (zoom) => setUrlViewSettings({ zoom }),
75
- onViewportChange: (vp, size) => setUrlViewSettings({
76
- viewport: vp,
77
- customWidth: size?.width,
78
- customHeight: size?.height,
79
- }),
80
+ onViewportChange: (vp, size) =>
81
+ setUrlViewSettings({
82
+ viewport: vp,
83
+ customWidth: size?.width,
84
+ customHeight: size?.height,
85
+ }),
80
86
  });
81
87
 
82
- // Panel dock position
83
- const panelDock = usePanelDock();
84
-
85
88
  // Get resolved theme from ThemeProvider for iframe preview
86
89
  const { resolvedTheme } = useTheme();
87
90
 
@@ -100,13 +103,13 @@ export function App({ fragments }: AppProps) {
100
103
  activeFragmentPathRef.current = activeFragmentPath;
101
104
 
102
105
  const [activeVariantIndex, setActiveVariantIndex] = useState<number>(() => {
103
- const fragment = fragments.find(s => s.path === activeFragmentPath);
106
+ const fragment = fragments.find((s) => s.path === activeFragmentPath);
104
107
  if (urlState.variant && fragment?.fragment.variants) {
105
108
  return findVariantIndex(fragment.fragment.variants, urlState.variant);
106
109
  }
107
110
  return 0;
108
111
  });
109
- const [searchQuery, setSearchQuery] = useState('');
112
+ const [searchQuery, setSearchQuery] = useState("");
110
113
  const searchInputRef = useRef<HTMLInputElement>(null);
111
114
 
112
115
  // Derived values
@@ -118,7 +121,6 @@ export function App({ fragments }: AppProps) {
118
121
  const variantCount = variants.length;
119
122
  const safeVariantIndex = variantCount > 0 ? Math.min(activeVariantIndex, variantCount - 1) : 0;
120
123
  const activeVariant = variants[safeVariantIndex];
121
- const isAllVariantsMode = !urlState.variant;
122
124
  const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
123
125
 
124
126
  // Figma integration
@@ -147,7 +149,12 @@ export function App({ fragments }: AppProps) {
147
149
  const timer = setTimeout(figmaIntegration.extractRenderedStyles, 100);
148
150
  return () => clearTimeout(timer);
149
151
  }
150
- }, [uiState.showComparison, activeVariant, figmaIntegration.extractRenderedStyles, uiState.previewKey]);
152
+ }, [
153
+ uiState.showComparison,
154
+ activeVariant,
155
+ figmaIntegration.extractRenderedStyles,
156
+ uiState.previewKey,
157
+ ]);
151
158
 
152
159
  // Keep focused variant index in range when variant lists change.
153
160
  useEffect(() => {
@@ -185,25 +192,28 @@ export function App({ fragments }: AppProps) {
185
192
 
186
193
  const handleUpdate = (data: any) => {
187
194
  if (data?.updates?.length > 0) {
188
- const paths = data.updates.map((u: any) => u.path.split('/').pop()).join(', ');
189
- info('HMR Update', `Updated: ${paths}`);
195
+ const paths = data.updates.map((u: any) => u.path.split("/").pop()).join(", ");
196
+ info("HMR Update", `Updated: ${paths}`);
190
197
  }
191
198
  };
192
199
 
193
- hot.on('vite:beforeUpdate', handleUpdate);
194
- return () => hot.off?.('vite:beforeUpdate', handleUpdate);
200
+ hot.on("vite:beforeUpdate", handleUpdate);
201
+ return () => hot.off?.("vite:beforeUpdate", handleUpdate);
195
202
  }, [info]);
196
203
 
197
204
  // Navigation handlers
198
- const handleSelectFragment = useCallback((path: string) => {
199
- const fragment = fragments.find((s) => s.path === path);
200
- const componentName = fragment?.fragment.meta.name || path;
205
+ const handleSelectFragment = useCallback(
206
+ (path: string) => {
207
+ const fragment = fragments.find((s) => s.path === path);
208
+ const componentName = fragment?.fragment.meta.name || path;
201
209
 
202
- setActiveFragmentPath(path);
203
- setActiveVariantIndex(0);
204
- uiActions.setHealthDashboard(false);
205
- setUrlComponent(componentName, null);
206
- }, [fragments, setUrlComponent, uiActions]);
210
+ setActiveFragmentPath(path);
211
+ setActiveVariantIndex(0);
212
+ uiActions.setHealthDashboard(false);
213
+ setUrlComponent(componentName, null);
214
+ },
215
+ [fragments, setUrlComponent, uiActions]
216
+ );
207
217
 
208
218
  const scrollToVariantSection = useCallback(
209
219
  (index: number, behavior: ScrollBehavior = "smooth") => {
@@ -230,39 +240,31 @@ export function App({ fragments }: AppProps) {
230
240
  [variantCount, scrollToVariantSection]
231
241
  );
232
242
 
233
- const handleSelectVariant = useCallback((index: number) => {
234
- if (variantCount === 0) return;
235
- const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
236
- const variantName = variants[normalizedIndex]?.name;
237
- setActiveVariantIndex(normalizedIndex);
238
- setUrlVariant(variantName || null);
239
- }, [variantCount, variants, setUrlVariant]);
240
-
241
- const handleSelectAllVariants = useCallback(() => {
242
- setUrlVariant(null);
243
- requestAnimationFrame(() => {
244
- const previewCanvas = document.getElementById("preview-canvas");
245
- if (previewCanvas instanceof HTMLElement) {
246
- previewCanvas.scrollTo({ top: 0, behavior: "smooth" });
247
- }
248
- });
249
- }, [setUrlVariant]);
243
+ const handleSelectVariant = useCallback(
244
+ (index: number) => {
245
+ if (variantCount === 0) return;
246
+ const normalizedIndex = ((index % variantCount) + variantCount) % variantCount;
247
+ const variantName = variants[normalizedIndex]?.name;
248
+ setActiveVariantIndex(normalizedIndex);
249
+ setUrlVariant(variantName || null);
250
+ },
251
+ [variantCount, variants, setUrlVariant]
252
+ );
250
253
 
251
- const handleSelectVariantLink = useCallback((index: number) => {
252
- if (isAllVariantsMode) {
253
- // In All mode, selecting a variant link exits All and opens that single variant.
254
- handleSelectVariant(index);
255
- return;
256
- }
257
- handleSelectVariant(index);
258
- }, [handleSelectVariant, isAllVariantsMode]);
254
+ const handleSelectVariantLink = useCallback(
255
+ (index: number) => {
256
+ // Always scroll to the variant section in the docs-like view
257
+ focusVariantInAllMode(index, true);
258
+ },
259
+ [focusVariantInAllMode]
260
+ );
259
261
 
260
262
  // Copy link handler
261
263
  const handleCopyLink = useCallback(async () => {
262
264
  const copied = await copyUrl();
263
265
  if (copied) {
264
266
  uiActions.setLinkCopied(true);
265
- success('Copied', 'Link copied to clipboard');
267
+ success("Copied", "Link copied to clipboard");
266
268
  setTimeout(() => uiActions.setLinkCopied(false), 2000);
267
269
  }
268
270
  }, [copyUrl, success, uiActions]);
@@ -270,49 +272,39 @@ export function App({ fragments }: AppProps) {
270
272
  // Sorted fragment paths for keyboard navigation
271
273
  const sortedFragmentPaths = useMemo(() => {
272
274
  return [...fragments]
273
- .filter(s => s.fragment?.meta?.name)
275
+ .filter((s) => s.fragment?.meta?.name)
274
276
  .sort((a, b) => a.fragment.meta.name.localeCompare(b.fragment.meta.name))
275
- .map(s => s.path);
277
+ .map((s) => s.path);
276
278
  }, [fragments]);
277
279
 
278
- const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || '');
280
+ const currentFragmentIndex = sortedFragmentPaths.indexOf(activeFragmentPath || "");
279
281
 
280
282
  // Keyboard shortcuts
281
283
  useKeyboardShortcuts(
282
284
  {
283
285
  nextComponent: () => {
284
- const nextIndex = currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
286
+ const nextIndex =
287
+ currentFragmentIndex < sortedFragmentPaths.length - 1 ? currentFragmentIndex + 1 : 0;
285
288
  handleSelectFragment(sortedFragmentPaths[nextIndex]);
286
289
  },
287
290
  prevComponent: () => {
288
- const prevIndex = currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
291
+ const prevIndex =
292
+ currentFragmentIndex > 0 ? currentFragmentIndex - 1 : sortedFragmentPaths.length - 1;
289
293
  handleSelectFragment(sortedFragmentPaths[prevIndex]);
290
294
  },
291
295
  nextVariant: () => {
292
296
  if (variantCount === 0) return;
293
297
  const nextIndex = activeVariantIndex < variantCount - 1 ? activeVariantIndex + 1 : 0;
294
- if (isAllVariantsMode) {
295
- focusVariantInAllMode(nextIndex, true);
296
- return;
297
- }
298
- handleSelectVariant(nextIndex);
298
+ focusVariantInAllMode(nextIndex, true);
299
299
  },
300
300
  prevVariant: () => {
301
301
  if (variantCount === 0) return;
302
302
  const prevIndex = activeVariantIndex > 0 ? activeVariantIndex - 1 : variantCount - 1;
303
- if (isAllVariantsMode) {
304
- focusVariantInAllMode(prevIndex, true);
305
- return;
306
- }
307
- handleSelectVariant(prevIndex);
303
+ focusVariantInAllMode(prevIndex, true);
308
304
  },
309
305
  goToVariant: (index) => {
310
306
  if (index >= variantCount) return;
311
- if (isAllVariantsMode) {
312
- focusVariantInAllMode(index, true);
313
- return;
314
- }
315
- handleSelectVariant(index);
307
+ focusVariantInAllMode(index, true);
316
308
  },
317
309
  toggleTheme: viewSettings.toggleTheme,
318
310
  togglePanel: uiActions.togglePanel,
@@ -324,7 +316,7 @@ export function App({ fragments }: AppProps) {
324
316
  escape: () => {
325
317
  if (document.activeElement === searchInputRef.current) {
326
318
  if (searchQuery) {
327
- setSearchQuery('');
319
+ setSearchQuery("");
328
320
  } else {
329
321
  searchInputRef.current.blur();
330
322
  }
@@ -344,9 +336,28 @@ export function App({ fragments }: AppProps) {
344
336
  <ActionCapture onAction={useActionsRef.current.logAction}>
345
337
  <FragmentRenderer variant={variant}>
346
338
  {(content, isLoading, error) => {
347
- if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
348
- if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={variant.name} hint="Check the console for the full error stack trace." />;
349
- 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." />;
339
+ if (isLoading)
340
+ return (
341
+ <Stack align="center" justify="center" style={{ padding: "32px" }}>
342
+ <LoaderIndicator />
343
+ </Stack>
344
+ );
345
+ if (error)
346
+ return (
347
+ <EmptyVariantMessage
348
+ reason={`Error: ${error.message}`}
349
+ variantName={variant.name}
350
+ hint="Check the console for the full error stack trace."
351
+ />
352
+ );
353
+ if (content === null || content === undefined)
354
+ return (
355
+ <EmptyVariantMessage
356
+ reason="render() returned null or undefined"
357
+ variantName={variant.name}
358
+ hint="The variant's render function didn't return any JSX."
359
+ />
360
+ );
350
361
  return content;
351
362
  }}
352
363
  </FragmentRenderer>
@@ -367,7 +378,10 @@ export function App({ fragments }: AppProps) {
367
378
  return (
368
379
  <>
369
380
  <ViewerStateSync fragments={fragments} activeVariantIndex={safeVariantIndex} />
370
- <KeyboardShortcutsHelp isOpen={uiState.showShortcutsHelp} onClose={() => uiActions.setShortcutsHelp(false)} />
381
+ <KeyboardShortcutsHelp
382
+ isOpen={uiState.showShortcutsHelp}
383
+ onClose={() => uiActions.setShortcutsHelp(false)}
384
+ />
371
385
  <CommandPalette
372
386
  isOpen={uiState.showCommandPalette}
373
387
  onClose={() => uiActions.setCommandPalette(false)}
@@ -415,14 +429,12 @@ export function App({ fragments }: AppProps) {
415
429
  />
416
430
  }
417
431
  aside={
418
- activeFragment && !uiState.showHealthDashboard ? (
432
+ uiState.showAside && activeFragment && !uiState.showHealthDashboard ? (
419
433
  <PreviewAside
420
434
  fragment={activeFragment.fragment}
421
435
  variants={variants}
422
436
  focusedVariantIndex={safeVariantIndex}
423
- isAllVariantsMode={isAllVariantsMode}
424
437
  activePanel={uiState.activePanel}
425
- onSelectAllVariants={handleSelectAllVariants}
426
438
  onSelectVariant={handleSelectVariantLink}
427
439
  onCopyLink={handleCopyLink}
428
440
  onShowShortcuts={uiActions.toggleShortcutsHelp}
@@ -431,12 +443,12 @@ export function App({ fragments }: AppProps) {
431
443
  }
432
444
  >
433
445
  {uiState.showHealthDashboard ? (
434
- <div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
435
- <Box padding="lg" style={{ maxWidth: '896px', margin: '0 auto' }}>
446
+ <Box height="100%" overflow="auto" background="primary">
447
+ <Box padding="lg" style={{ maxWidth: "896px", margin: "0 auto" }}>
436
448
  <HealthDashboard
437
449
  fragments={fragments}
438
450
  onNavigate={(componentName) => {
439
- const target = fragments.find(s => s.fragment.meta.name === componentName);
451
+ const target = fragments.find((s) => s.fragment.meta.name === componentName);
440
452
  if (target) {
441
453
  uiActions.setHealthDashboard(false);
442
454
  handleSelectFragment(target.path);
@@ -444,40 +456,39 @@ export function App({ fragments }: AppProps) {
444
456
  }}
445
457
  />
446
458
  </Box>
447
- </div>
459
+ </Box>
448
460
  ) : activeFragment ? (
449
- <div id="preview-layout" style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
461
+ <Stack id="preview-layout" style={{ height: "100%" }}>
450
462
  {/* Main Content Area */}
451
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
463
+ <Stack style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
452
464
  {/* Preview Area */}
453
- <div
454
- id="preview-canvas"
455
- style={{
456
- flex: 1,
457
- overflow: 'auto',
458
- position: 'relative',
459
- }}
460
- >
465
+ <Box id="preview-canvas" overflow="auto" style={{ flex: 1, position: "relative" }}>
461
466
  {variantCount === 0 ? (
462
467
  <NoVariantsMessage fragment={activeFragment?.fragment} />
463
- ) : isAllVariantsMode && !uiState.showMatrixView && !uiState.showMultiViewport ? (
464
- <AllVariantsPreview
468
+ ) : uiState.showMatrixView ? (
469
+ <PreviewArea
465
470
  componentName={activeFragment.fragment.meta.name}
466
471
  fragmentPath={activeFragment.path}
472
+ variant={activeVariant}
467
473
  variants={variants}
468
- focusedVariantIndex={safeVariantIndex}
469
474
  zoom={viewSettings.zoom}
470
475
  viewport={viewSettings.viewport}
471
476
  customSize={viewSettings.customSize}
472
477
  previewTheme={resolvedTheme}
478
+ showMatrixView={true}
479
+ showMultiViewport={false}
473
480
  showComparison={uiState.showComparison}
481
+ figmaUrl={figmaUrl}
474
482
  allFigmaUrls={allFigmaUrls}
475
- fallbackFigmaUrl={activeFragment.fragment.meta.figma}
483
+ onSelectVariant={(index) => {
484
+ uiActions.setMatrixView(false);
485
+ handleSelectVariant(index);
486
+ }}
476
487
  onRetry={uiActions.incrementPreviewKey}
477
- renderVariantContent={renderVariantWithProps}
478
- previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
488
+ renderContent={() => renderVariantWithProps(activeVariant)}
489
+ previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
479
490
  />
480
- ) : (
491
+ ) : uiState.showMultiViewport ? (
481
492
  <PreviewArea
482
493
  componentName={activeFragment.fragment.meta.name}
483
494
  fragmentPath={activeFragment.path}
@@ -487,55 +498,79 @@ export function App({ fragments }: AppProps) {
487
498
  viewport={viewSettings.viewport}
488
499
  customSize={viewSettings.customSize}
489
500
  previewTheme={resolvedTheme}
490
- showMatrixView={uiState.showMatrixView}
491
- showMultiViewport={uiState.showMultiViewport}
501
+ showMatrixView={false}
502
+ showMultiViewport={true}
492
503
  showComparison={uiState.showComparison}
493
504
  figmaUrl={figmaUrl}
494
505
  allFigmaUrls={allFigmaUrls}
495
506
  onSelectVariant={(index) => {
496
- uiActions.setMatrixView(false);
497
507
  handleSelectVariant(index);
498
508
  }}
499
509
  onRetry={uiActions.incrementPreviewKey}
500
510
  renderContent={() => renderVariantWithProps(activeVariant)}
501
511
  previewKey={`${activeFragmentPath}-${safeVariantIndex}-${uiState.previewKey}`}
502
512
  />
513
+ ) : (
514
+ <Box
515
+ style={{
516
+ padding: "var(--fui-space-6) var(--fui-space-8)",
517
+ }}
518
+ >
519
+ <ComponentDocView
520
+ fragment={activeFragment}
521
+ fragments={fragments}
522
+ renderVariantContent={renderVariantWithProps}
523
+ onNavigateToComponent={(name) => {
524
+ const target = fragments.find((s) => s.fragment.meta.name === name);
525
+ if (target) handleSelectFragment(target.path);
526
+ }}
527
+ zoom={viewSettings.zoom}
528
+ viewport={viewSettings.viewport}
529
+ customSize={viewSettings.customSize}
530
+ previewTheme={resolvedTheme}
531
+ showComparison={uiState.showComparison}
532
+ allFigmaUrls={allFigmaUrls}
533
+ onRetry={uiActions.incrementPreviewKey}
534
+ previewKeyBase={`${activeFragmentPath}-${uiState.previewKey}`}
535
+ />
536
+ </Box>
503
537
  )}
504
- </div>
505
- </div>
538
+ </Box>
539
+ </Stack>
506
540
 
507
- {/* Bottom Panel */}
508
- <div id="preview-tools">
509
- {uiState.panelOpen && activeVariant && (
510
- <BottomPanel
511
- fragment={activeFragment.fragment}
512
- variant={activeVariant}
513
- fragments={fragments}
514
- activePanel={uiState.activePanel}
515
- onPanelChange={uiActions.setActivePanel}
516
- figmaUrl={figmaUrl}
517
- figmaStyles={figmaIntegration.figmaStyles.status === 'success' ? figmaIntegration.figmaStyles.styles || null : null}
518
- renderedStyles={figmaIntegration.renderedStyles}
519
- figmaLoading={figmaIntegration.isLoading}
520
- figmaError={figmaIntegration.errorMessage}
521
- onFetchFigma={figmaIntegration.fetchFigmaStyles}
522
- onRefreshRendered={figmaIntegration.extractRenderedStyles}
523
- actionLogs={actionLogs}
524
- onClearActionLogs={clearActionLogs}
525
- onNavigateToComponent={(name) => {
526
- const target = fragments.find(s => s.fragment.meta.name === name);
527
- if (target) handleSelectFragment(target.path);
528
- }}
529
- previewKey={uiState.previewKey}
530
- fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
531
- />
532
- )}
533
- </div>
534
- </div>
541
+ {activeVariant && (
542
+ <BottomPanel
543
+ fragment={activeFragment.fragment}
544
+ variant={activeVariant}
545
+ fragments={fragments}
546
+ open={uiState.panelOpen}
547
+ onOpenChange={uiActions.setPanelOpen}
548
+ activePanel={uiState.activePanel}
549
+ onPanelChange={uiActions.setActivePanel}
550
+ figmaUrl={figmaUrl}
551
+ figmaStyles={
552
+ figmaIntegration.figmaStyles.status === "success"
553
+ ? figmaIntegration.figmaStyles.styles || null
554
+ : null
555
+ }
556
+ renderedStyles={figmaIntegration.renderedStyles}
557
+ figmaLoading={figmaIntegration.isLoading}
558
+ figmaError={figmaIntegration.errorMessage}
559
+ onFetchFigma={figmaIntegration.fetchFigmaStyles}
560
+ onRefreshRendered={figmaIntegration.extractRenderedStyles}
561
+ onNavigateToComponent={(name) => {
562
+ const target = fragments.find((s) => s.fragment.meta.name === name);
563
+ if (target) handleSelectFragment(target.path);
564
+ }}
565
+ previewKey={uiState.previewKey}
566
+ fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
567
+ />
568
+ )}
569
+ </Stack>
535
570
  ) : (
536
- <EmptyState style={{ height: '100%' }}>
571
+ <EmptyState style={{ height: "100%" }}>
537
572
  <EmptyState.Icon>
538
- <EmptyIcon style={{ width: '48px', height: '48px' }} />
573
+ <EmptyIcon style={{ width: "48px", height: "48px" }} />
539
574
  </EmptyState.Icon>
540
575
  <EmptyState.Title>No component selected</EmptyState.Title>
541
576
  <EmptyState.Description>Select a component from the sidebar</EmptyState.Description>
@@ -545,591 +580,3 @@ export function App({ fragments }: AppProps) {
545
580
  </>
546
581
  );
547
582
  }
548
-
549
- // Top Toolbar Component
550
- interface TopToolbarProps {
551
- fragment: { path: string; fragment: FragmentDefinition };
552
- viewSettings: ReturnType<typeof useViewSettings>;
553
- uiState: ReturnType<typeof useAppState>['state'];
554
- uiActions: ReturnType<typeof useAppState>['actions'];
555
- figmaUrl?: string;
556
- searchQuery: string;
557
- onSearchChange: (value: string) => void;
558
- searchInputRef: RefObject<HTMLInputElement>;
559
- }
560
-
561
- interface ViewerHeaderProps {
562
- showHealth: boolean;
563
- searchQuery: string;
564
- onSearchChange: (value: string) => void;
565
- searchInputRef: RefObject<HTMLInputElement>;
566
- }
567
-
568
- interface HeaderSearchProps {
569
- value: string;
570
- onChange: (value: string) => void;
571
- inputRef: RefObject<HTMLInputElement>;
572
- }
573
-
574
- interface PreviewAsideProps {
575
- fragment: FragmentDefinition;
576
- variants: FragmentVariant[];
577
- focusedVariantIndex: number;
578
- isAllVariantsMode: boolean;
579
- activePanel: string;
580
- onSelectAllVariants: () => void;
581
- onSelectVariant: (index: number) => void;
582
- onCopyLink: () => void;
583
- onShowShortcuts: () => void;
584
- }
585
-
586
- function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
587
- return (
588
- <Header.Search expandable>
589
- <Input
590
- ref={inputRef}
591
- value={value}
592
- onChange={onChange}
593
- placeholder="Search components"
594
- aria-label="Search components"
595
- size="sm"
596
- style={{ width: '240px' }}
597
- />
598
- </Header.Search>
599
- );
600
- }
601
-
602
- function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef }: ViewerHeaderProps) {
603
- const { setTheme, resolvedTheme } = useTheme();
604
- return (
605
- <Header aria-label="Fragments viewer header">
606
- <Header.Trigger />
607
- <Header.Brand>
608
- <Stack direction="row" gap="sm" align="center">
609
- <FragmentsLogo size={20} />
610
- <Text weight="medium" size="sm">{BRAND.name}</Text>
611
- <Text size="xs" color="tertiary">{showHealth ? 'health dashboard' : 'preview'}</Text>
612
- </Stack>
613
- </Header.Brand>
614
- <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
615
- <Header.Spacer />
616
- <Header.Actions>
617
- <WebMCPStatusIndicator />
618
- <ThemeToggle
619
- size="sm"
620
- value={resolvedTheme}
621
- onValueChange={(value) => setTheme(value)}
622
- aria-label={`Theme: ${resolvedTheme}`}
623
- />
624
- <a
625
- href="https://github.com/ConanMcN/fragments"
626
- target="_blank"
627
- rel="noopener noreferrer"
628
- style={{
629
- display: 'flex',
630
- alignItems: 'center',
631
- justifyContent: 'center',
632
- width: '32px',
633
- height: '32px',
634
- borderRadius: 'var(--radius-md, 6px)',
635
- color: 'var(--text-secondary)',
636
- transition: 'background-color 150ms ease, color 150ms ease',
637
- }}
638
- aria-label="View on GitHub"
639
- >
640
- <GitHubIcon />
641
- </a>
642
- </Header.Actions>
643
- </Header>
644
- );
645
- }
646
-
647
- function PreviewAside({
648
- fragment,
649
- variants,
650
- focusedVariantIndex,
651
- isAllVariantsMode,
652
- activePanel,
653
- onSelectAllVariants,
654
- onSelectVariant,
655
- onCopyLink,
656
- onShowShortcuts,
657
- }: PreviewAsideProps) {
658
- const focusedVariant = variants[focusedVariantIndex] || null;
659
-
660
- const baseLinkStyle: CSSProperties = {
661
- color: 'var(--text-secondary)',
662
- textDecoration: 'none',
663
- fontSize: '13px',
664
- borderRadius: '6px',
665
- padding: '4px 8px',
666
- display: 'block',
667
- };
668
-
669
- const getLinkStyle = (isActive = false): CSSProperties => (
670
- isActive
671
- ? { ...baseLinkStyle, color: 'var(--text-primary)', backgroundColor: 'var(--bg-hover)' }
672
- : baseLinkStyle
673
- );
674
-
675
- return (
676
- <Box padding="md" style={{ position: 'sticky', top: '80px' }}>
677
- <Stack gap="md">
678
- <Stack gap="xs">
679
- <Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em' }}>
680
- On this page
681
- </Text>
682
- <a href="#preview-canvas" style={baseLinkStyle}>
683
- Preview
684
- </a>
685
- <a href="#preview-tools" style={baseLinkStyle}>
686
- Panels
687
- </a>
688
- {variants.length > 0 && (
689
- <>
690
- <Text size="xs" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '8px' }}>
691
- Variants
692
- </Text>
693
- <a
694
- href="#preview-canvas"
695
- style={getLinkStyle(isAllVariantsMode)}
696
- onClick={(event) => {
697
- event.preventDefault();
698
- onSelectAllVariants();
699
- }}
700
- >
701
- All
702
- </a>
703
- {variants.map((variant, index) => {
704
- const active = index === focusedVariantIndex;
705
- const anchorId = getVariantSectionId(fragment.meta.name, variant.name);
706
-
707
- return (
708
- <a
709
- key={variant.name}
710
- href={`#${anchorId}`}
711
- style={getLinkStyle(active)}
712
- onClick={(event) => {
713
- event.preventDefault();
714
- onSelectVariant(index);
715
- }}
716
- >
717
- {variant.name}
718
- </a>
719
- );
720
- })}
721
- </>
722
- )}
723
- </Stack>
724
- <Separator />
725
- <Stack gap="xs">
726
- <Text size="sm" weight="medium">{fragment.meta.name}</Text>
727
- <Text size="xs" color="secondary">
728
- {isAllVariantsMode
729
- ? `Variant: All${focusedVariant ? ` (focused: ${focusedVariant.name})` : ''}`
730
- : focusedVariant ? `Variant: ${focusedVariant.name}` : 'Select a variant'}
731
- </Text>
732
- <Text size="xs" color="tertiary">
733
- Active panel: {activePanel}
734
- </Text>
735
- </Stack>
736
- <Separator />
737
- <Stack gap="xs">
738
- <Button variant="ghost" size="sm" onClick={onCopyLink}>
739
- Copy Link
740
- </Button>
741
- <Button variant="ghost" size="sm" onClick={onShowShortcuts}>
742
- Keyboard Shortcuts
743
- </Button>
744
- </Stack>
745
- </Stack>
746
- </Box>
747
- );
748
- }
749
-
750
- function TopToolbar({
751
- fragment,
752
- viewSettings,
753
- uiState,
754
- uiActions,
755
- figmaUrl,
756
- searchQuery,
757
- onSearchChange,
758
- searchInputRef,
759
- }: TopToolbarProps) {
760
- const { setTheme, resolvedTheme } = useTheme();
761
- return (
762
- <Header aria-label="Component preview toolbar">
763
- <Header.Trigger />
764
- <Header.Brand>
765
- <Stack direction="row" align="center" gap="sm">
766
- <FragmentsLogo size={20} />
767
- <Text weight="medium" size="sm">{fragment.fragment.meta.name}</Text>
768
- <Text size="xs" color="tertiary">{fragment.fragment.meta.category}</Text>
769
- </Stack>
770
- </Header.Brand>
771
- <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
772
- <Header.Spacer />
773
- <Header.Actions>
774
- <PreviewToolbar
775
- zoom={viewSettings.zoom}
776
- onZoomChange={viewSettings.setZoom}
777
- />
778
- <Separator orientation="vertical" style={{ height: '16px' }} />
779
- <Tooltip content={uiState.showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
780
- <Button
781
- variant="ghost"
782
- size="sm"
783
- aria-pressed={uiState.showMatrixView}
784
- aria-label="Toggle matrix view"
785
- onClick={() => uiActions.setMatrixView(!uiState.showMatrixView)}
786
- style={uiState.showMatrixView ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
787
- >
788
- <GridFour size={16} />
789
- </Button>
790
- </Tooltip>
791
- <Tooltip content={uiState.showMultiViewport ? "Disable responsive view" : "Enable responsive view"}>
792
- <Button
793
- variant="ghost"
794
- size="sm"
795
- aria-pressed={uiState.showMultiViewport}
796
- aria-label="Toggle responsive view"
797
- onClick={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
798
- style={uiState.showMultiViewport ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
799
- >
800
- <DeviceMobile size={16} />
801
- </Button>
802
- </Tooltip>
803
- <Tooltip content={uiState.panelOpen ? "Hide addons panel" : "Show addons panel"}>
804
- <Button
805
- variant="ghost"
806
- size="sm"
807
- aria-pressed={uiState.panelOpen}
808
- aria-label="Toggle addons panel"
809
- onClick={uiActions.togglePanel}
810
- style={uiState.panelOpen ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
811
- >
812
- <Rows size={16} />
813
- </Button>
814
- </Tooltip>
815
- <Separator orientation="vertical" style={{ height: '16px' }} />
816
- {figmaUrl && (
817
- <>
818
- <Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
819
- <Button
820
- onClick={uiActions.toggleComparison}
821
- variant="ghost"
822
- size="sm"
823
- style={uiState.showComparison ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
824
- >
825
- <CompareIcon style={{ width: '16px', height: '16px' }} />
826
- </Button>
827
- </Tooltip>
828
- <Tooltip content="View in Figma">
829
- <Button
830
- onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
831
- variant="ghost"
832
- size="sm"
833
- >
834
- <FigmaIcon style={{ width: '16px', height: '16px' }} />
835
- </Button>
836
- </Tooltip>
837
- <Separator orientation="vertical" style={{ height: '16px' }} />
838
- </>
839
- )}
840
- <WebMCPStatusIndicator />
841
- <ThemeToggle
842
- size="sm"
843
- value={resolvedTheme}
844
- onValueChange={(value) => setTheme(value)}
845
- aria-label={`Theme: ${resolvedTheme}`}
846
- />
847
- <a
848
- href="https://github.com/ConanMcN/fragments"
849
- target="_blank"
850
- rel="noopener noreferrer"
851
- style={{
852
- display: 'flex',
853
- alignItems: 'center',
854
- justifyContent: 'center',
855
- width: '32px',
856
- height: '32px',
857
- borderRadius: 'var(--radius-md, 6px)',
858
- color: 'var(--text-secondary)',
859
- transition: 'background-color 150ms ease, color 150ms ease',
860
- }}
861
- aria-label="View on GitHub"
862
- >
863
- <GitHubIcon />
864
- </a>
865
- </Header.Actions>
866
- </Header>
867
- );
868
- }
869
-
870
- function normalizeAnchorSegment(value: string): string {
871
- const normalized = value
872
- .toLowerCase()
873
- .trim()
874
- .replace(/[^a-z0-9]+/g, "-")
875
- .replace(/^-+|-+$/g, "");
876
- return normalized || "variant";
877
- }
878
-
879
- function getVariantSectionId(componentName: string, variantName: string): string {
880
- return `preview-${normalizeAnchorSegment(componentName)}-${normalizeAnchorSegment(variantName)}`;
881
- }
882
-
883
- interface AllVariantsPreviewProps {
884
- componentName: string;
885
- fragmentPath: string;
886
- variants: FragmentVariant[];
887
- focusedVariantIndex: number;
888
- zoom: ReturnType<typeof useViewSettings>["zoom"];
889
- viewport: ReturnType<typeof useViewSettings>["viewport"];
890
- customSize: ReturnType<typeof useViewSettings>["customSize"];
891
- previewTheme: ReturnType<typeof useTheme>["resolvedTheme"];
892
- showComparison: boolean;
893
- allFigmaUrls: string[];
894
- fallbackFigmaUrl?: string;
895
- onRetry: () => void;
896
- renderVariantContent: (variant: FragmentVariant) => ReactNode;
897
- previewKeyBase: string;
898
- }
899
-
900
- function AllVariantsPreview({
901
- componentName,
902
- fragmentPath,
903
- variants,
904
- focusedVariantIndex,
905
- zoom,
906
- viewport,
907
- customSize,
908
- previewTheme,
909
- showComparison,
910
- allFigmaUrls,
911
- fallbackFigmaUrl,
912
- onRetry,
913
- renderVariantContent,
914
- previewKeyBase,
915
- }: AllVariantsPreviewProps) {
916
- return (
917
- <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
918
- {variants.map((variant, index) => {
919
- const isFocused = index === focusedVariantIndex;
920
-
921
- return (
922
- <section
923
- id={getVariantSectionId(componentName, variant.name)}
924
- key={variant.name}
925
- style={{
926
- border: '1px solid var(--border)',
927
- borderColor: isFocused ? 'var(--color-accent)' : 'var(--border)',
928
- borderRadius: '10px',
929
- overflow: 'hidden',
930
- backgroundColor: 'var(--bg-primary)',
931
- boxShadow: isFocused ? '0 0 0 1px color-mix(in srgb, var(--color-accent) 40%, transparent)' : undefined,
932
- }}
933
- >
934
- <Stack direction="row" align="baseline" gap="sm" style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
935
- <Text size="sm" weight="medium">{variant.name}</Text>
936
- <Text size="xs" color="secondary">{variant.description}</Text>
937
- </Stack>
938
- <PreviewArea
939
- componentName={componentName}
940
- fragmentPath={fragmentPath}
941
- variant={variant}
942
- variants={variants}
943
- zoom={zoom}
944
- viewport={viewport}
945
- customSize={customSize}
946
- previewTheme={previewTheme}
947
- showMatrixView={false}
948
- showMultiViewport={false}
949
- showComparison={showComparison}
950
- figmaUrl={variant.figma || fallbackFigmaUrl}
951
- allFigmaUrls={allFigmaUrls}
952
- onSelectVariant={() => {}}
953
- onRetry={onRetry}
954
- renderContent={() => renderVariantContent(variant)}
955
- previewKey={`${previewKeyBase}-${index}`}
956
- />
957
- </section>
958
- );
959
- })}
960
- </div>
961
- );
962
- }
963
-
964
- // No variants message
965
- interface NoVariantsMessageProps {
966
- fragment?: FragmentDefinition;
967
- }
968
-
969
- function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
970
- // Check for load error (missing dependencies, schema errors, etc.)
971
- const loadError = (fragment as any)?._loadError;
972
- if (loadError) {
973
- return <LoadErrorMessage error={loadError} componentName={fragment?.meta?.name} />;
974
- }
975
-
976
- const skippedVariants = (fragment?._generated as any)?.skippedVariants;
977
-
978
- if (!skippedVariants || skippedVariants.length === 0) {
979
- return (
980
- <EmptyState style={{ height: '100%' }}>
981
- <EmptyState.Description>No variants defined</EmptyState.Description>
982
- </EmptyState>
983
- );
984
- }
985
-
986
- return (
987
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
988
- <Alert variant="info">
989
- <Alert.Body>
990
- <Alert.Title>
991
- {skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
992
- </Alert.Title>
993
- <Alert.Content>
994
- <Stack direction="column" gap="sm">
995
- <Text size="xs" color="secondary">
996
- These variants couldn't be rendered because they use syntax the parser doesn't support yet:
997
- </Text>
998
- <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
999
- {skippedVariants.map((sv: any, i: number) => (
1000
- <li key={i}>
1001
- <Text size="xs" color="secondary">
1002
- <Text as="span" size="xs" weight="semibold">{sv.name}:</Text>{' '}
1003
- <Text as="span" size="xs" color="tertiary">{sv.reason}</Text>
1004
- </Text>
1005
- </li>
1006
- ))}
1007
- </ul>
1008
- </Stack>
1009
- </Alert.Content>
1010
- </Alert.Body>
1011
- </Alert>
1012
- </div>
1013
- );
1014
- }
1015
-
1016
- // Load error message — shown when a fragment failed to import (missing deps, schema errors, etc.)
1017
- function LoadErrorMessage({ error, componentName }: { error: { message: string; dependencies: string[] }; componentName?: string }) {
1018
- const deps = error.dependencies || [];
1019
- const errorMessage = error.message || 'Unknown error';
1020
-
1021
- // Determine if the error is a missing module/dependency issue
1022
- const isModuleError = /Failed to resolve import|Cannot find module|Module not found|does not provide an export/.test(errorMessage);
1023
-
1024
- // Only suggest packages if the error is actually about missing modules
1025
- let suggestedPackages: string[] = [];
1026
- if (isModuleError) {
1027
- if (deps.length > 0) {
1028
- suggestedPackages = [...deps];
1029
- } else {
1030
- const match = errorMessage.match(/Failed to resolve import ["']([^"']+)["']/);
1031
- if (match) {
1032
- suggestedPackages = [match[1]];
1033
- }
1034
- }
1035
- }
1036
-
1037
- const hasMissingDeps = suggestedPackages.length > 0;
1038
- const installCmd = `pnpm add ${suggestedPackages.join(' ')}`;
1039
-
1040
- return (
1041
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
1042
- <Alert variant="warning">
1043
- <Alert.Body>
1044
- <Alert.Title>
1045
- {hasMissingDeps ? 'Missing Dependencies' : 'Failed to Load'}
1046
- </Alert.Title>
1047
- <Alert.Content>
1048
- <Stack direction="column" gap="sm">
1049
- {hasMissingDeps ? (
1050
- <>
1051
- <Text size="xs" color="secondary">
1052
- {componentName ? `${componentName} requires` : 'This component requires'} packages that are not installed in your project.
1053
- </Text>
1054
- <Text size="xs" weight="semibold" color="secondary">Install with:</Text>
1055
- <code style={{
1056
- display: 'block',
1057
- padding: '8px 12px',
1058
- borderRadius: '6px',
1059
- backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
1060
- fontFamily: 'monospace',
1061
- fontSize: '12px',
1062
- color: 'var(--text-primary, #111827)',
1063
- userSelect: 'all',
1064
- }}>
1065
- {installCmd}
1066
- </code>
1067
- <Text size="xs" color="tertiary">
1068
- After installing, restart the dev server.
1069
- </Text>
1070
- </>
1071
- ) : (
1072
- <>
1073
- <Text size="xs" color="secondary">
1074
- {componentName ? `${componentName} couldn't` : 'This component couldn\'t'} be loaded. This may be due to a schema validation error or missing imports.
1075
- </Text>
1076
- <Text size="xs" weight="semibold" color="secondary">Error:</Text>
1077
- <pre style={{
1078
- padding: '8px 12px',
1079
- borderRadius: '6px',
1080
- backgroundColor: 'var(--bg-tertiary, #f3f4f6)',
1081
- fontFamily: 'monospace',
1082
- fontSize: '11px',
1083
- color: 'var(--text-secondary, #374151)',
1084
- whiteSpace: 'pre-wrap',
1085
- wordBreak: 'break-word',
1086
- margin: 0,
1087
- maxHeight: '200px',
1088
- overflow: 'auto',
1089
- }}>
1090
- {errorMessage}
1091
- </pre>
1092
- </>
1093
- )}
1094
- </Stack>
1095
- </Alert.Content>
1096
- </Alert.Body>
1097
- </Alert>
1098
- </div>
1099
- );
1100
- }
1101
-
1102
- // Empty variant message
1103
- interface EmptyVariantMessageProps {
1104
- reason: string;
1105
- variantName: string;
1106
- hint?: string;
1107
- }
1108
-
1109
- function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
1110
- return (
1111
- <Alert variant="warning">
1112
- <Alert.Body>
1113
- <Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
1114
- <Alert.Content>
1115
- <Stack direction="column" gap="sm">
1116
- <Text size="xs" color="secondary">{reason}</Text>
1117
- {hint && (
1118
- <Text size="xs" color="tertiary">
1119
- <Text as="span" size="xs" weight="semibold">Tip:</Text> {hint}
1120
- </Text>
1121
- )}
1122
- <div>
1123
- <Text size="xs" color="tertiary" weight="semibold">Common causes:</Text>
1124
- <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
1125
- <li><Text size="xs" color="secondary">Component requires props that weren't provided</Text></li>
1126
- <li><Text size="xs" color="secondary">Component renders conditionally and conditions aren't met</Text></li>
1127
- <li><Text size="xs" color="secondary">Story args reference variables that don't exist in this context</Text></li>
1128
- </ul>
1129
- </div>
1130
- </Stack>
1131
- </Alert.Content>
1132
- </Alert.Body>
1133
- </Alert>
1134
- );
1135
- }