@fragments-sdk/cli 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,95 @@
1
+ import type { SegmentUsage } from '../../core/index.js';
2
+ import { CheckIcon, XIcon, AccessibilityIcon } from './Icons.js';
3
+
4
+ interface UsageSectionProps {
5
+ usage: SegmentUsage;
6
+ }
7
+
8
+ export function UsageSection({ usage }: UsageSectionProps) {
9
+ const hasWhen = usage.when && usage.when.length > 0;
10
+ const hasWhenNot = usage.whenNot && usage.whenNot.length > 0;
11
+ const hasGuidelines = usage.guidelines && usage.guidelines.length > 0;
12
+ const hasAccessibility = usage.accessibility && usage.accessibility.length > 0;
13
+
14
+ if (!hasWhen && !hasWhenNot && !hasGuidelines && !hasAccessibility) {
15
+ return null;
16
+ }
17
+
18
+ return (
19
+ <section id="usage" className="scroll-mt-24">
20
+ <h2 className="text-base font-semibold text-primary mb-5">Usage</h2>
21
+
22
+ {/* When to use / When not to use */}
23
+ {(hasWhen || hasWhenNot) && (
24
+ <div className="grid md:grid-cols-2 gap-8 mb-8">
25
+ {hasWhen && (
26
+ <div className="p-4 rounded-xl bg-[--color-success-bg] border border-[--color-success]/20">
27
+ <h3 className="text-[13px] font-medium text-[--color-success] mb-3 flex items-center gap-2">
28
+ <CheckIcon className="w-4 h-4" />
29
+ When to use
30
+ </h3>
31
+ <ul className="space-y-2">
32
+ {usage.when!.map((item, index) => (
33
+ <li key={index} className="text-[13px] text-primary leading-relaxed flex items-start gap-2">
34
+ <span className="text-[--color-success] mt-1.5 text-xs">•</span>
35
+ <span>{item}</span>
36
+ </li>
37
+ ))}
38
+ </ul>
39
+ </div>
40
+ )}
41
+
42
+ {hasWhenNot && (
43
+ <div className="p-4 rounded-xl bg-[--color-danger-bg] border border-[--color-danger]/20">
44
+ <h3 className="text-[13px] font-medium text-[--color-danger] mb-3 flex items-center gap-2">
45
+ <XIcon className="w-4 h-4" />
46
+ When not to use
47
+ </h3>
48
+ <ul className="space-y-2">
49
+ {usage.whenNot!.map((item, index) => (
50
+ <li key={index} className="text-[13px] text-primary leading-relaxed flex items-start gap-2">
51
+ <span className="text-[--color-danger] mt-1.5 text-xs">•</span>
52
+ <span>{item}</span>
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ </div>
57
+ )}
58
+ </div>
59
+ )}
60
+
61
+ {/* Guidelines */}
62
+ {hasGuidelines && (
63
+ <div className="mb-6">
64
+ <h3 className="text-[13px] font-medium text-primary mb-3">Guidelines</h3>
65
+ <ul className="space-y-2">
66
+ {usage.guidelines!.map((item, index) => (
67
+ <li key={index} className="text-[13px] text-secondary leading-relaxed flex items-start gap-2">
68
+ <span className="text-[--color-accent] mt-1.5 text-xs">•</span>
69
+ <span>{item}</span>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+ )}
75
+
76
+ {/* Accessibility */}
77
+ {hasAccessibility && (
78
+ <div className="p-4 rounded-xl border border-[--border] bg-[--bg-secondary]">
79
+ <h3 className="text-[13px] font-medium text-primary mb-3 flex items-center gap-2">
80
+ <AccessibilityIcon className="w-4 h-4 text-secondary" />
81
+ Accessibility
82
+ </h3>
83
+ <ul className="space-y-2">
84
+ {usage.accessibility!.map((item, index) => (
85
+ <li key={index} className="text-[13px] text-secondary leading-relaxed flex items-start gap-2">
86
+ <span className="text-tertiary mt-1.5 text-xs">•</span>
87
+ <span>{item}</span>
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ </div>
92
+ )}
93
+ </section>
94
+ );
95
+ }
@@ -0,0 +1,350 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Variant Matrix View - Display all variants in a grid
4
+ *
5
+ * Shows all variants of a component simultaneously, making it easy to:
6
+ * - Compare states/variants at a glance
7
+ * - Spot visual inconsistencies
8
+ * - Review all component states quickly
9
+ *
10
+ * Uses virtualization to only render visible variants for better performance.
11
+ */
12
+
13
+ import { useState, useMemo, useRef, useCallback } from "react";
14
+ import { useVirtualizer } from "@tanstack/react-virtual";
15
+ import clsx from "clsx";
16
+ import type { SegmentVariant } from "../../core/index.js";
17
+ import { ErrorBoundary } from "./ErrorBoundary.js";
18
+ import { StoryRenderer, LoaderIndicator } from "./StoryRenderer.js";
19
+ import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
20
+ import { ChevronDownIcon } from "./Icons.js";
21
+ import { getBackgroundStyle, type BackgroundOption } from "./PreviewToolbar.js";
22
+
23
+ interface VariantMatrixProps {
24
+ /** All variants to display */
25
+ variants: SegmentVariant[];
26
+ /** Component name for error display */
27
+ componentName: string;
28
+ /** Segment path for iframe rendering */
29
+ segmentPath: string;
30
+ /** Current zoom level */
31
+ zoom: number;
32
+ /** Preview theme */
33
+ previewTheme: "light" | "dark";
34
+ /** Background option */
35
+ background: BackgroundOption;
36
+ /** Whether to use iframe isolation */
37
+ useIframeIsolation?: boolean;
38
+ /** Callback when a variant is clicked to focus on it */
39
+ onSelectVariant?: (index: number) => void;
40
+ }
41
+
42
+ type GridSize = "small" | "medium" | "large";
43
+
44
+ interface GridConfig {
45
+ cols: string;
46
+ minHeight: string;
47
+ heightPx: number; // For virtualization
48
+ scale: number;
49
+ colCount: number; // Default column count for virtualization
50
+ }
51
+
52
+ const GRID_SIZES: Record<GridSize, GridConfig> = {
53
+ small: { cols: "grid-cols-4 lg:grid-cols-5 xl:grid-cols-6", minHeight: "150px", heightPx: 150, scale: 0.5, colCount: 4 },
54
+ medium: { cols: "grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", minHeight: "200px", heightPx: 200, scale: 0.75, colCount: 3 },
55
+ large: { cols: "grid-cols-1 lg:grid-cols-2 xl:grid-cols-3", minHeight: "300px", heightPx: 300, scale: 1, colCount: 2 },
56
+ };
57
+
58
+ /** Threshold for enabling virtualization */
59
+ const VIRTUALIZATION_THRESHOLD = 12;
60
+
61
+ export function VariantMatrix({
62
+ variants,
63
+ componentName,
64
+ segmentPath,
65
+ zoom,
66
+ previewTheme,
67
+ background,
68
+ useIframeIsolation = true,
69
+ onSelectVariant,
70
+ }: VariantMatrixProps) {
71
+ const [gridSize, setGridSize] = useState<GridSize>("medium");
72
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
73
+ const scrollRef = useRef<HTMLDivElement>(null);
74
+
75
+ const gridConfig = GRID_SIZES[gridSize];
76
+ const effectiveScale = (zoom / 100) * gridConfig.scale;
77
+
78
+ // Determine if we should use virtualization
79
+ const useVirtualization = variants.length > VIRTUALIZATION_THRESHOLD;
80
+
81
+ // Calculate number of rows for virtualization
82
+ const columns = gridConfig.colCount;
83
+ const rowCount = Math.ceil(variants.length / columns);
84
+
85
+ // Row height includes card height + gap
86
+ const rowHeight = gridConfig.heightPx + 16; // 16px gap
87
+
88
+ const rowVirtualizer = useVirtualizer({
89
+ count: rowCount,
90
+ getScrollElement: () => scrollRef.current,
91
+ estimateSize: () => rowHeight,
92
+ overscan: 2, // Render 2 extra rows above/below for smooth scrolling
93
+ });
94
+
95
+ if (variants.length === 0) {
96
+ return (
97
+ <div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
98
+ No variants to display
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div className="h-full flex flex-col">
105
+ {/* Toolbar */}
106
+ <div className="flex-shrink-0 px-4 py-2 border-b border-[--border] bg-[--bg-secondary] flex items-center justify-between">
107
+ <div className="text-sm text-secondary">
108
+ {variants.length} variant{variants.length !== 1 ? "s" : ""}
109
+ {useVirtualization && (
110
+ <span className="ml-2 text-xs text-tertiary">(virtualized)</span>
111
+ )}
112
+ </div>
113
+ <div className="flex items-center gap-2">
114
+ <span className="text-xs text-tertiary">Grid size:</span>
115
+ <div className="flex rounded-md border border-[--border] overflow-hidden">
116
+ {(["small", "medium", "large"] as GridSize[]).map((size) => (
117
+ <button
118
+ key={size}
119
+ onClick={() => setGridSize(size)}
120
+ className={clsx(
121
+ "px-2 py-1 text-xs capitalize transition-colors",
122
+ gridSize === size
123
+ ? "bg-[--bg-hover] text-primary"
124
+ : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
125
+ )}
126
+ >
127
+ {size}
128
+ </button>
129
+ ))}
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Grid - Virtualized or Regular */}
135
+ {useVirtualization ? (
136
+ <div ref={scrollRef} className="flex-1 overflow-auto p-4">
137
+ <div
138
+ style={{
139
+ height: `${rowVirtualizer.getTotalSize()}px`,
140
+ width: "100%",
141
+ position: "relative",
142
+ }}
143
+ >
144
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
145
+ const startIndex = virtualRow.index * columns;
146
+ const rowVariants = variants.slice(startIndex, startIndex + columns);
147
+
148
+ return (
149
+ <div
150
+ key={virtualRow.key}
151
+ style={{
152
+ position: "absolute",
153
+ top: 0,
154
+ left: 0,
155
+ width: "100%",
156
+ height: `${virtualRow.size}px`,
157
+ transform: `translateY(${virtualRow.start}px)`,
158
+ }}
159
+ >
160
+ <div className={clsx("grid gap-4", gridConfig.cols)} style={{ height: gridConfig.minHeight }}>
161
+ {rowVariants.map((variant, colIndex) => {
162
+ const index = startIndex + colIndex;
163
+ return (
164
+ <VariantCard
165
+ key={variant.name}
166
+ variant={variant}
167
+ index={index}
168
+ componentName={componentName}
169
+ segmentPath={segmentPath}
170
+ scale={effectiveScale}
171
+ minHeight={gridConfig.minHeight}
172
+ previewTheme={previewTheme}
173
+ background={background}
174
+ useIframeIsolation={useIframeIsolation}
175
+ isHovered={hoveredIndex === index}
176
+ onHover={() => setHoveredIndex(index)}
177
+ onLeave={() => setHoveredIndex(null)}
178
+ onClick={() => onSelectVariant?.(index)}
179
+ />
180
+ );
181
+ })}
182
+ </div>
183
+ </div>
184
+ );
185
+ })}
186
+ </div>
187
+ </div>
188
+ ) : (
189
+ <div className="flex-1 overflow-auto p-4">
190
+ <div className={clsx("grid gap-4", gridConfig.cols)}>
191
+ {variants.map((variant, index) => (
192
+ <VariantCard
193
+ key={variant.name}
194
+ variant={variant}
195
+ index={index}
196
+ componentName={componentName}
197
+ segmentPath={segmentPath}
198
+ scale={effectiveScale}
199
+ minHeight={gridConfig.minHeight}
200
+ previewTheme={previewTheme}
201
+ background={background}
202
+ useIframeIsolation={useIframeIsolation}
203
+ isHovered={hoveredIndex === index}
204
+ onHover={() => setHoveredIndex(index)}
205
+ onLeave={() => setHoveredIndex(null)}
206
+ onClick={() => onSelectVariant?.(index)}
207
+ />
208
+ ))}
209
+ </div>
210
+ </div>
211
+ )}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ interface VariantCardProps {
217
+ variant: SegmentVariant;
218
+ index: number;
219
+ componentName: string;
220
+ segmentPath: string;
221
+ scale: number;
222
+ minHeight: string;
223
+ previewTheme: "light" | "dark";
224
+ background: BackgroundOption;
225
+ useIframeIsolation: boolean;
226
+ isHovered: boolean;
227
+ onHover: () => void;
228
+ onLeave: () => void;
229
+ onClick: () => void;
230
+ }
231
+
232
+ function VariantCard({
233
+ variant,
234
+ index,
235
+ componentName,
236
+ segmentPath,
237
+ scale,
238
+ minHeight,
239
+ previewTheme,
240
+ background,
241
+ useIframeIsolation,
242
+ isHovered,
243
+ onHover,
244
+ onLeave,
245
+ onClick,
246
+ }: VariantCardProps) {
247
+ const backgroundStyle = getBackgroundStyle(background);
248
+
249
+ return (
250
+ <div
251
+ className={clsx(
252
+ "group relative rounded-lg border overflow-hidden transition-all cursor-pointer",
253
+ isHovered
254
+ ? "border-blue-500 shadow-lg ring-2 ring-blue-500/20"
255
+ : "border-[--border] hover:border-blue-300 dark:hover:border-blue-700"
256
+ )}
257
+ style={{ minHeight }}
258
+ onMouseEnter={onHover}
259
+ onMouseLeave={onLeave}
260
+ onClick={onClick}
261
+ >
262
+ {/* Header */}
263
+ <div className="absolute top-0 left-0 right-0 z-10 px-2 py-1 bg-gradient-to-b from-black/60 to-transparent">
264
+ <div className="flex items-center justify-between">
265
+ <span className="text-xs font-medium text-white truncate">
266
+ {variant.name}
267
+ </span>
268
+ <span className="text-[10px] text-white/70">
269
+ #{index + 1}
270
+ </span>
271
+ </div>
272
+ </div>
273
+
274
+ {/* Click to view overlay */}
275
+ <div
276
+ className={clsx(
277
+ "absolute inset-0 z-10 flex items-center justify-center bg-black/40 transition-opacity",
278
+ isHovered ? "opacity-100" : "opacity-0"
279
+ )}
280
+ >
281
+ <span className="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded-full shadow-lg">
282
+ Click to focus
283
+ </span>
284
+ </div>
285
+
286
+ {/* Preview content */}
287
+ <div
288
+ className="h-full w-full overflow-hidden flex items-center justify-center"
289
+ data-theme={previewTheme}
290
+ style={backgroundStyle}
291
+ >
292
+ {useIframeIsolation ? (
293
+ <IsolatedPreviewFrame
294
+ segmentPath={segmentPath}
295
+ variantName={variant.name}
296
+ theme={previewTheme}
297
+ width="100%"
298
+ height="100%"
299
+ minHeight={minHeight}
300
+ />
301
+ ) : (
302
+ <div
303
+ className="p-4"
304
+ style={{
305
+ transform: `scale(${scale})`,
306
+ }}
307
+ >
308
+ <ErrorBoundary
309
+ componentName={componentName}
310
+ fallback={
311
+ <div className="text-xs text-red-500 p-2">
312
+ Error rendering variant
313
+ </div>
314
+ }
315
+ >
316
+ <StoryRenderer variant={variant}>
317
+ {(content, isLoading, error) => {
318
+ if (isLoading) {
319
+ return (
320
+ <div className="flex items-center justify-center p-4">
321
+ <LoaderIndicator />
322
+ </div>
323
+ );
324
+ }
325
+ if (error) {
326
+ return (
327
+ <div className="text-xs text-red-500 p-2">
328
+ {error.message}
329
+ </div>
330
+ );
331
+ }
332
+ return content;
333
+ }}
334
+ </StoryRenderer>
335
+ </ErrorBoundary>
336
+ </div>
337
+ )}
338
+ </div>
339
+
340
+ {/* Tags/badges */}
341
+ {variant.hasPlayFunction && (
342
+ <div className="absolute bottom-2 right-2 z-10">
343
+ <span className="px-1.5 py-0.5 text-[10px] bg-purple-600 text-white rounded">
344
+ play
345
+ </span>
346
+ </div>
347
+ )}
348
+ </div>
349
+ );
350
+ }
@@ -0,0 +1,131 @@
1
+ // @ts-nocheck
2
+ import React, { type ReactNode, Suspense } from 'react';
3
+ import type { SegmentVariant } from '../../core/index.js';
4
+ import { ErrorBoundary } from './ErrorBoundary.js';
5
+
6
+ interface VariantRendererProps {
7
+ /** The variant to render */
8
+ variant: SegmentVariant;
9
+
10
+ /** Optional loading fallback for async components */
11
+ loadingFallback?: ReactNode;
12
+
13
+ /** Optional error fallback */
14
+ errorFallback?: ReactNode;
15
+
16
+ /** Callback when variant render throws */
17
+ onError?: (error: Error) => void;
18
+ }
19
+
20
+ /**
21
+ * Renders a single variant with error boundary and suspense support.
22
+ */
23
+ export function VariantRenderer({
24
+ variant,
25
+ loadingFallback,
26
+ errorFallback,
27
+ onError,
28
+ }: VariantRendererProps): React.ReactElement {
29
+ return (
30
+ <ErrorBoundary
31
+ fallback={errorFallback}
32
+ onError={(error) => onError?.(error)}
33
+ >
34
+ <Suspense
35
+ fallback={
36
+ loadingFallback ?? (
37
+ <div
38
+ style={{
39
+ padding: '16px',
40
+ color: '#6b7280',
41
+ fontFamily: 'system-ui, sans-serif',
42
+ }}
43
+ >
44
+ Loading...
45
+ </div>
46
+ )
47
+ }
48
+ >
49
+ {variant.render()}
50
+ </Suspense>
51
+ </ErrorBoundary>
52
+ );
53
+ }
54
+
55
+ interface VariantGridProps {
56
+ /** Variants to render */
57
+ variants: SegmentVariant[];
58
+
59
+ /** Number of columns in the grid */
60
+ columns?: number;
61
+
62
+ /** Gap between variants in pixels */
63
+ gap?: number;
64
+
65
+ /** Optional loading fallback */
66
+ loadingFallback?: ReactNode;
67
+
68
+ /** Optional error fallback */
69
+ errorFallback?: ReactNode;
70
+ }
71
+
72
+ /**
73
+ * Renders multiple variants in a grid layout.
74
+ */
75
+ export function VariantGrid({
76
+ variants,
77
+ columns = 2,
78
+ gap = 24,
79
+ loadingFallback,
80
+ errorFallback,
81
+ }: VariantGridProps): React.ReactElement {
82
+ return (
83
+ <div
84
+ style={{
85
+ display: 'grid',
86
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
87
+ gap: `${gap}px`,
88
+ }}
89
+ >
90
+ {variants.map((variant) => (
91
+ <div key={variant.name}>
92
+ <div
93
+ style={{
94
+ marginBottom: '8px',
95
+ fontWeight: 600,
96
+ fontSize: '14px',
97
+ color: '#374151',
98
+ fontFamily: 'system-ui, sans-serif',
99
+ }}
100
+ >
101
+ {variant.name}
102
+ </div>
103
+ <div
104
+ style={{
105
+ marginBottom: '8px',
106
+ fontSize: '13px',
107
+ color: '#6b7280',
108
+ fontFamily: 'system-ui, sans-serif',
109
+ }}
110
+ >
111
+ {variant.description}
112
+ </div>
113
+ <div
114
+ style={{
115
+ padding: '16px',
116
+ border: '1px solid #e5e7eb',
117
+ borderRadius: '8px',
118
+ background: '#ffffff',
119
+ }}
120
+ >
121
+ <VariantRenderer
122
+ variant={variant}
123
+ loadingFallback={loadingFallback}
124
+ errorFallback={errorFallback}
125
+ />
126
+ </div>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,84 @@
1
+ import { useRef, useEffect, useCallback } from 'react';
2
+ import type { SegmentVariant } from '../../core/index.js';
3
+ import clsx from 'clsx';
4
+ import { PlayIcon } from './Icons.js';
5
+
6
+ interface VariantTabsProps {
7
+ variants: SegmentVariant[];
8
+ activeIndex: number;
9
+ onSelect: (index: number) => void;
10
+ }
11
+
12
+ export function VariantTabs({ variants, activeIndex, onSelect }: VariantTabsProps) {
13
+ const containerRef = useRef<HTMLDivElement>(null);
14
+ const buttonRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
15
+
16
+ // Scroll active tab into view when it changes
17
+ useEffect(() => {
18
+ const button = buttonRefs.current.get(activeIndex);
19
+ if (button && containerRef.current) {
20
+ button.scrollIntoView({
21
+ behavior: 'smooth',
22
+ block: 'nearest',
23
+ inline: 'center',
24
+ });
25
+ }
26
+ }, [activeIndex]);
27
+
28
+ // Keyboard navigation for variant tabs
29
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
30
+ if (e.key === 'ArrowLeft') {
31
+ e.preventDefault();
32
+ const newIndex = activeIndex > 0 ? activeIndex - 1 : variants.length - 1;
33
+ onSelect(newIndex);
34
+ } else if (e.key === 'ArrowRight') {
35
+ e.preventDefault();
36
+ const newIndex = activeIndex < variants.length - 1 ? activeIndex + 1 : 0;
37
+ onSelect(newIndex);
38
+ }
39
+ }, [activeIndex, variants.length, onSelect]);
40
+
41
+ if (variants.length === 0) return null;
42
+
43
+ return (
44
+ <div
45
+ ref={containerRef}
46
+ className="flex items-center gap-1 overflow-x-auto max-w-full scrollbar-thin"
47
+ onKeyDown={handleKeyDown}
48
+ role="tablist"
49
+ aria-label="Component variants"
50
+ >
51
+ {variants.map((variant, index) => {
52
+ const isActive = activeIndex === index;
53
+ return (
54
+ <button
55
+ key={index}
56
+ ref={(el) => {
57
+ if (el) buttonRefs.current.set(index, el);
58
+ else buttonRefs.current.delete(index);
59
+ }}
60
+ onClick={() => onSelect(index)}
61
+ onKeyDown={handleKeyDown}
62
+ role="tab"
63
+ aria-selected={isActive}
64
+ tabIndex={isActive ? 0 : -1}
65
+ className={clsx(
66
+ 'px-3 py-1 text-xs font-medium rounded whitespace-nowrap flex-shrink-0',
67
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]',
68
+ 'flex items-center gap-1.5',
69
+ isActive
70
+ ? 'text-primary bg-[--bg-hover]'
71
+ : 'text-tertiary hover:text-secondary'
72
+ )}
73
+ title={variant.hasPlayFunction ? `${variant.name} (has interaction test)` : variant.name}
74
+ >
75
+ {variant.name}
76
+ {variant.hasPlayFunction && (
77
+ <PlayIcon className="w-3 h-3 text-[--color-accent]" />
78
+ )}
79
+ </button>
80
+ );
81
+ })}
82
+ </div>
83
+ );
84
+ }