@fragments-sdk/cli 0.10.1 → 0.11.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 (149) hide show
  1. package/dist/bin.js +20 -2
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{init-NDQXUWDU.js → init-UFGK5TCN.js} +75 -4
  4. package/dist/init-UFGK5TCN.js.map +1 -0
  5. package/dist/snapshot-SV2JOFZH.js +139 -0
  6. package/dist/snapshot-SV2JOFZH.js.map +1 -0
  7. package/dist/{viewer-DNMNC5VS.js → viewer-DLLJIMCK.js} +68 -46
  8. package/dist/viewer-DLLJIMCK.js.map +1 -0
  9. package/package.json +6 -14
  10. package/src/bin.ts +30 -0
  11. package/src/commands/init.ts +76 -1
  12. package/src/commands/snapshot.ts +197 -0
  13. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  14. package/src/viewer/server.ts +37 -22
  15. package/src/viewer/vite-plugin.ts +25 -9
  16. package/dist/init-NDQXUWDU.js.map +0 -1
  17. package/dist/viewer-DNMNC5VS.js.map +0 -1
  18. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  19. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  20. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  21. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  22. package/src/viewer/assets/fragments-logo.ts +0 -4
  23. package/src/viewer/assets/fragments_logo.png +0 -0
  24. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  25. package/src/viewer/components/ActionCapture.tsx +0 -172
  26. package/src/viewer/components/ActionsPanel.tsx +0 -332
  27. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  28. package/src/viewer/components/App.tsx +0 -582
  29. package/src/viewer/components/BottomPanel.tsx +0 -288
  30. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  31. package/src/viewer/components/CodePanel.tsx +0 -118
  32. package/src/viewer/components/CommandPalette.tsx +0 -392
  33. package/src/viewer/components/ComponentDocView.tsx +0 -164
  34. package/src/viewer/components/ComponentGraph.tsx +0 -380
  35. package/src/viewer/components/ComponentHeader.tsx +0 -88
  36. package/src/viewer/components/ContractPanel.tsx +0 -241
  37. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  38. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  39. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  40. package/src/viewer/components/FragmentEditor.tsx +0 -525
  41. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  42. package/src/viewer/components/HeaderSearch.tsx +0 -24
  43. package/src/viewer/components/HealthDashboard.tsx +0 -441
  44. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  45. package/src/viewer/components/Icons.tsx +0 -479
  46. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  47. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  48. package/src/viewer/components/IsolatedRender.tsx +0 -113
  49. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  50. package/src/viewer/components/LandingPage.tsx +0 -421
  51. package/src/viewer/components/Layout.tsx +0 -27
  52. package/src/viewer/components/LeftSidebar.tsx +0 -472
  53. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  54. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  55. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  56. package/src/viewer/components/PanelShell.tsx +0 -161
  57. package/src/viewer/components/PerformancePanel.tsx +0 -304
  58. package/src/viewer/components/PreviewArea.tsx +0 -472
  59. package/src/viewer/components/PreviewAside.tsx +0 -168
  60. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  61. package/src/viewer/components/PreviewPane.tsx +0 -149
  62. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  63. package/src/viewer/components/PropsEditor.tsx +0 -506
  64. package/src/viewer/components/PropsTable.tsx +0 -111
  65. package/src/viewer/components/RelationsSection.tsx +0 -88
  66. package/src/viewer/components/ResizablePanel.tsx +0 -271
  67. package/src/viewer/components/RightSidebar.tsx +0 -102
  68. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  69. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  70. package/src/viewer/components/Sidebar.tsx +0 -169
  71. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  72. package/src/viewer/components/ThemeProvider.tsx +0 -42
  73. package/src/viewer/components/Toast.tsx +0 -3
  74. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  75. package/src/viewer/components/TopToolbar.tsx +0 -159
  76. package/src/viewer/components/UsageSection.tsx +0 -95
  77. package/src/viewer/components/VariantMatrix.tsx +0 -388
  78. package/src/viewer/components/VariantRenderer.tsx +0 -131
  79. package/src/viewer/components/VariantTabs.tsx +0 -40
  80. package/src/viewer/components/ViewerHeader.tsx +0 -69
  81. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  82. package/src/viewer/components/ViewportSelector.tsx +0 -172
  83. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  84. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  85. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  86. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  87. package/src/viewer/components/viewer-utils.ts +0 -16
  88. package/src/viewer/composition-renderer.ts +0 -381
  89. package/src/viewer/constants/index.ts +0 -1
  90. package/src/viewer/constants/ui.ts +0 -166
  91. package/src/viewer/entry.tsx +0 -335
  92. package/src/viewer/hooks/index.ts +0 -2
  93. package/src/viewer/hooks/useA11yCache.ts +0 -383
  94. package/src/viewer/hooks/useA11yService.ts +0 -364
  95. package/src/viewer/hooks/useActions.ts +0 -138
  96. package/src/viewer/hooks/useAppState.ts +0 -147
  97. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  98. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  99. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  100. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  101. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  102. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  103. package/src/viewer/hooks/useUrlState.ts +0 -318
  104. package/src/viewer/hooks/useViewSettings.ts +0 -111
  105. package/src/viewer/index.html +0 -28
  106. package/src/viewer/intelligence/healthReport.ts +0 -505
  107. package/src/viewer/intelligence/styleDrift.ts +0 -340
  108. package/src/viewer/intelligence/usageScanner.ts +0 -309
  109. package/src/viewer/jsx-parser.ts +0 -486
  110. package/src/viewer/preview-frame-entry.tsx +0 -25
  111. package/src/viewer/preview-frame.html +0 -125
  112. package/src/viewer/public/favicon.ico +0 -0
  113. package/src/viewer/render-template.html +0 -68
  114. package/src/viewer/styles/globals.css +0 -278
  115. package/src/viewer/types/a11y.ts +0 -197
  116. package/src/viewer/utils/a11y-fixes.ts +0 -509
  117. package/src/viewer/utils/actionExport.ts +0 -372
  118. package/src/viewer/utils/colorSchemes.ts +0 -201
  119. package/src/viewer/utils/detectRelationships.ts +0 -256
  120. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  121. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  122. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  123. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  124. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  125. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  126. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  127. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  128. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  129. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  130. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  131. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  132. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  133. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  134. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  135. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  136. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  137. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  138. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  139. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  140. package/src/viewer/vendor/shared/src/index.ts +0 -34
  141. package/src/viewer/vendor/shared/src/types.ts +0 -53
  142. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  143. package/src/viewer/webmcp/analytics.ts +0 -165
  144. package/src/viewer/webmcp/index.ts +0 -3
  145. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  146. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  147. package/src/viewer/webmcp/scan-utils.ts +0 -135
  148. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  149. package/src/viewer/webmcp/viewer-state.ts +0 -45
@@ -1,159 +0,0 @@
1
- import type { RefObject } from "react";
2
- import type { FragmentDefinition } from "../../core/index.js";
3
- import type { useViewSettings } from "../hooks/useViewSettings.js";
4
- import type { useAppState } from "../hooks/useAppState.js";
5
- import {
6
- Header,
7
- Stack,
8
- Text,
9
- Separator,
10
- Tooltip,
11
- Button,
12
- ThemeToggle,
13
- FragmentsLogo,
14
- } from "@fragments-sdk/ui";
15
- import { DeviceMobile, GridFour, SidebarSimple } from "@phosphor-icons/react";
16
- import { GitHubIcon, FigmaIcon, CompareIcon } from "./Icons.js";
17
- import { PreviewToolbar } from "./PreviewToolbar.js";
18
- import { HeaderSearch } from "./HeaderSearch.js";
19
- import { useTheme } from "./ThemeProvider.js";
20
- import { WebMCPStatusIndicator } from "./WebMCPStatusIndicator.js";
21
-
22
- /** Normalize category to Title Case for display */
23
- function titleCase(str: string): string {
24
- return str.replace(/\b\w/g, (c) => c.toUpperCase());
25
- }
26
-
27
- interface TopToolbarProps {
28
- fragment: { path: string; fragment: FragmentDefinition };
29
- viewSettings: ReturnType<typeof useViewSettings>;
30
- uiState: ReturnType<typeof useAppState>["state"];
31
- uiActions: ReturnType<typeof useAppState>["actions"];
32
- figmaUrl?: string;
33
- searchQuery: string;
34
- onSearchChange: (value: string) => void;
35
- searchInputRef: RefObject<HTMLInputElement>;
36
- }
37
-
38
- export function TopToolbar({
39
- fragment,
40
- viewSettings,
41
- uiState,
42
- uiActions,
43
- figmaUrl,
44
- searchQuery,
45
- onSearchChange,
46
- searchInputRef,
47
- }: TopToolbarProps) {
48
- const { setTheme, resolvedTheme } = useTheme();
49
- return (
50
- <Header aria-label="Component preview toolbar">
51
- <Header.Trigger />
52
- <Header.Brand>
53
- <Stack direction="row" align="center" gap="sm">
54
- <FragmentsLogo size={20} />
55
- <Text weight="medium" size="sm">
56
- {fragment.fragment.meta.name}
57
- </Text>
58
- <Text size="xs" color="tertiary">
59
- {titleCase(fragment.fragment.meta.category || '')}
60
- </Text>
61
- </Stack>
62
- </Header.Brand>
63
- <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
64
- <Header.Spacer />
65
- <Header.Actions>
66
- <PreviewToolbar zoom={viewSettings.zoom} onZoomChange={viewSettings.setZoom} />
67
- <Separator orientation="vertical" style={{ height: "16px" }} />
68
- <Tooltip content={uiState.showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
69
- <Button
70
- variant={uiState.showMatrixView ? "secondary" : "ghost"}
71
- size="sm"
72
- icon
73
- aria-pressed={uiState.showMatrixView}
74
- aria-label="Toggle matrix view"
75
- onClick={() => uiActions.setMatrixView(!uiState.showMatrixView)}
76
- >
77
- <GridFour size={16} />
78
- </Button>
79
- </Tooltip>
80
- <Tooltip
81
- content={uiState.showMultiViewport ? "Disable responsive view" : "Enable responsive view"}
82
- >
83
- <Button
84
- variant={uiState.showMultiViewport ? "secondary" : "ghost"}
85
- size="sm"
86
- icon
87
- aria-pressed={uiState.showMultiViewport}
88
- aria-label="Toggle responsive view"
89
- onClick={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
90
- >
91
- <DeviceMobile size={16} />
92
- </Button>
93
- </Tooltip>
94
- <Separator orientation="vertical" style={{ height: "16px" }} />
95
- {figmaUrl && (
96
- <>
97
- <Tooltip
98
- content={
99
- uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"
100
- }
101
- >
102
- <Button
103
- variant={uiState.showComparison ? "secondary" : "ghost"}
104
- size="sm"
105
- icon
106
- onClick={uiActions.toggleComparison}
107
- >
108
- <CompareIcon style={{ width: "16px", height: "16px" }} />
109
- </Button>
110
- </Tooltip>
111
- <Tooltip content="View in Figma">
112
- <Button
113
- onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
114
- variant="ghost"
115
- size="sm"
116
- icon
117
- >
118
- <FigmaIcon style={{ width: "16px", height: "16px" }} />
119
- </Button>
120
- </Tooltip>
121
- <Separator orientation="vertical" style={{ height: "16px" }} />
122
- </>
123
- )}
124
- <WebMCPStatusIndicator />
125
- <Tooltip content={uiState.showAside ? "Hide side panel" : "Show side panel"}>
126
- <Button
127
- variant={uiState.showAside ? "secondary" : "ghost"}
128
- size="sm"
129
- icon
130
- aria-pressed={uiState.showAside}
131
- aria-label="Toggle side panel"
132
- onClick={uiActions.toggleAside}
133
- >
134
- <SidebarSimple size={16} style={{ transform: "scaleX(-1)" }} />
135
- </Button>
136
- </Tooltip>
137
- <Separator orientation="vertical" style={{ height: "16px" }} />
138
- <ThemeToggle
139
- size="sm"
140
- value={resolvedTheme}
141
- onValueChange={(value) => setTheme(value)}
142
- aria-label={`Theme: ${resolvedTheme}`}
143
- />
144
- <Button
145
- as="a"
146
- variant="ghost"
147
- size="sm"
148
- icon
149
- href="https://github.com/ConanMcN/fragments"
150
- target="_blank"
151
- rel="noopener noreferrer"
152
- aria-label="View on GitHub"
153
- >
154
- <GitHubIcon />
155
- </Button>
156
- </Header.Actions>
157
- </Header>
158
- );
159
- }
@@ -1,95 +0,0 @@
1
- import type { FragmentUsage } from '../../core/index.js';
2
- import { CheckIcon, XIcon, AccessibilityIcon } from './Icons.js';
3
-
4
- interface UsageSectionProps {
5
- usage: FragmentUsage;
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" style={{ scrollMarginTop: '96px' }}>
20
- <h2 style={{ fontSize: '16px', fontWeight: 600, color: 'var(--text-primary)', marginBottom: '20px' }}>Usage</h2>
21
-
22
- {/* When to use / When not to use */}
23
- {(hasWhen || hasWhenNot) && (
24
- <div style={{ display: 'grid', gridTemplateColumns: hasWhen && hasWhenNot ? '1fr 1fr' : '1fr', gap: '32px', marginBottom: '32px' }}>
25
- {hasWhen && (
26
- <div style={{ padding: '16px', borderRadius: '12px', background: 'var(--color-success-bg)', border: '1px solid rgba(16, 163, 127, 0.2)' }}>
27
- <h3 style={{ fontSize: '13px', fontWeight: 500, color: 'var(--color-success)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
28
- <CheckIcon style={{ width: '16px', height: '16px' }} />
29
- When to use
30
- </h3>
31
- <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
32
- {usage.when!.map((item, index) => (
33
- <li key={index} style={{ fontSize: '13px', color: 'var(--text-primary)', lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
34
- <span style={{ color: 'var(--color-success)', marginTop: '6px', fontSize: '12px' }}>&#8226;</span>
35
- <span>{item}</span>
36
- </li>
37
- ))}
38
- </ul>
39
- </div>
40
- )}
41
-
42
- {hasWhenNot && (
43
- <div style={{ padding: '16px', borderRadius: '12px', background: 'var(--color-danger-bg)', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
44
- <h3 style={{ fontSize: '13px', fontWeight: 500, color: 'var(--color-danger)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
45
- <XIcon style={{ width: '16px', height: '16px' }} />
46
- When not to use
47
- </h3>
48
- <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
49
- {usage.whenNot!.map((item, index) => (
50
- <li key={index} style={{ fontSize: '13px', color: 'var(--text-primary)', lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
51
- <span style={{ color: 'var(--color-danger)', marginTop: '6px', fontSize: '12px' }}>&#8226;</span>
52
- <span>{item}</span>
53
- </li>
54
- ))}
55
- </ul>
56
- </div>
57
- )}
58
- </div>
59
- )}
60
-
61
- {/* Guidelines */}
62
- {hasGuidelines && (
63
- <div style={{ marginBottom: '24px' }}>
64
- <h3 style={{ fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)', marginBottom: '12px' }}>Guidelines</h3>
65
- <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
66
- {usage.guidelines!.map((item, index) => (
67
- <li key={index} style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
68
- <span style={{ color: 'var(--color-accent)', marginTop: '6px', fontSize: '12px' }}>&#8226;</span>
69
- <span>{item}</span>
70
- </li>
71
- ))}
72
- </ul>
73
- </div>
74
- )}
75
-
76
- {/* Accessibility */}
77
- {hasAccessibility && (
78
- <div style={{ padding: '16px', borderRadius: '12px', border: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
79
- <h3 style={{ fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)', marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
80
- <AccessibilityIcon style={{ width: '16px', height: '16px', color: 'var(--text-secondary)' }} />
81
- Accessibility
82
- </h3>
83
- <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
84
- {usage.accessibility!.map((item, index) => (
85
- <li key={index} style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: 1.6, display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
86
- <span style={{ color: 'var(--text-tertiary)', marginTop: '6px', fontSize: '12px' }}>&#8226;</span>
87
- <span>{item}</span>
88
- </li>
89
- ))}
90
- </ul>
91
- </div>
92
- )}
93
- </section>
94
- );
95
- }
@@ -1,388 +0,0 @@
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 { Box, Stack, Text, Button, Badge, EmptyState } from "@fragments-sdk/ui";
16
- import type { FragmentVariant } from "../../core/index.js";
17
- import { ErrorBoundary } from "./ErrorBoundary.js";
18
- import { FragmentRenderer, LoaderIndicator } from "./FragmentRenderer.js";
19
- import { IsolatedPreviewFrame } from "./IsolatedPreviewFrame.js";
20
- import { ChevronDownIcon } from "./Icons.js";
21
-
22
- interface VariantMatrixProps {
23
- /** All variants to display */
24
- variants: FragmentVariant[];
25
- /** Component name for error display */
26
- componentName: string;
27
- /** Fragment path for iframe rendering */
28
- fragmentPath: string;
29
- /** Current zoom level */
30
- zoom: number;
31
- /** Preview theme */
32
- previewTheme: "light" | "dark";
33
- /** Whether to use iframe isolation */
34
- useIframeIsolation?: boolean;
35
- /** Callback when a variant is clicked to focus on it */
36
- onSelectVariant?: (index: number) => void;
37
- }
38
-
39
- type GridSize = "small" | "medium" | "large";
40
-
41
- interface GridConfig {
42
- gridTemplateColumns: string;
43
- minHeight: string;
44
- heightPx: number; // For virtualization
45
- scale: number;
46
- colCount: number; // Default column count for virtualization
47
- }
48
-
49
- const GRID_SIZES: Record<GridSize, GridConfig> = {
50
- small: { gridTemplateColumns: "repeat(4, 1fr)", minHeight: "150px", heightPx: 150, scale: 0.5, colCount: 4 },
51
- medium: { gridTemplateColumns: "repeat(3, 1fr)", minHeight: "200px", heightPx: 200, scale: 0.75, colCount: 3 },
52
- large: { gridTemplateColumns: "repeat(2, 1fr)", minHeight: "300px", heightPx: 300, scale: 1, colCount: 2 },
53
- };
54
-
55
- /** Threshold for enabling virtualization */
56
- const VIRTUALIZATION_THRESHOLD = 12;
57
-
58
- export function VariantMatrix({
59
- variants,
60
- componentName,
61
- fragmentPath,
62
- zoom,
63
- previewTheme,
64
- useIframeIsolation = true,
65
- onSelectVariant,
66
- }: VariantMatrixProps) {
67
- const [gridSize, setGridSize] = useState<GridSize>("medium");
68
- const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
69
- const scrollRef = useRef<HTMLDivElement>(null);
70
-
71
- const gridConfig = GRID_SIZES[gridSize];
72
- const effectiveScale = (zoom / 100) * gridConfig.scale;
73
-
74
- // Determine if we should use virtualization
75
- const useVirtualization = variants.length > VIRTUALIZATION_THRESHOLD;
76
-
77
- // Calculate number of rows for virtualization
78
- const columns = gridConfig.colCount;
79
- const rowCount = Math.ceil(variants.length / columns);
80
-
81
- // Row height includes card height + gap
82
- const rowHeight = gridConfig.heightPx + 16; // 16px gap
83
-
84
- const rowVirtualizer = useVirtualizer({
85
- count: rowCount,
86
- getScrollElement: () => scrollRef.current,
87
- estimateSize: () => rowHeight,
88
- overscan: 2, // Render 2 extra rows above/below for smooth scrolling
89
- });
90
-
91
- if (variants.length === 0) {
92
- return (
93
- <EmptyState style={{ height: '100%' }}>
94
- <EmptyState.Title>No variants to display</EmptyState.Title>
95
- </EmptyState>
96
- );
97
- }
98
-
99
- return (
100
- <Stack style={{ height: '100%' }}>
101
- {/* Toolbar */}
102
- <Box paddingX="md" paddingY="sm" borderBottom background="secondary" style={{ flexShrink: 0 }}>
103
- <Stack direction="row" align="center" justify="between">
104
- <Text size="sm" color="secondary">
105
- {variants.length} variant{variants.length !== 1 ? "s" : ""}
106
- {useVirtualization && <Text as="span" size="xs" color="tertiary"> (virtualized)</Text>}
107
- </Text>
108
- <Stack direction="row" align="center" gap="sm">
109
- <Text size="xs" color="tertiary">Grid size:</Text>
110
- <Stack direction="row" style={{ borderRadius: 6, border: '1px solid var(--border)', overflow: 'hidden' }}>
111
- {(["small", "medium", "large"] as GridSize[]).map((size) => (
112
- <Button
113
- key={size}
114
- variant={gridSize === size ? "secondary" : "ghost"}
115
- size="sm"
116
- onClick={() => setGridSize(size)}
117
- style={{ textTransform: 'capitalize', borderRadius: 0 }}
118
- >
119
- {size}
120
- </Button>
121
- ))}
122
- </Stack>
123
- </Stack>
124
- </Stack>
125
- </Box>
126
-
127
- {/* Grid - Virtualized or Regular */}
128
- {useVirtualization ? (
129
- <Box ref={scrollRef} overflow="auto" padding="md" style={{ flex: 1 }}>
130
- <div
131
- style={{
132
- height: `${rowVirtualizer.getTotalSize()}px`,
133
- width: "100%",
134
- position: "relative",
135
- }}
136
- >
137
- {rowVirtualizer.getVirtualItems().map((virtualRow) => {
138
- const startIndex = virtualRow.index * columns;
139
- const rowVariants = variants.slice(startIndex, startIndex + columns);
140
-
141
- return (
142
- <div
143
- key={virtualRow.key}
144
- style={{
145
- position: "absolute",
146
- top: 0,
147
- left: 0,
148
- width: "100%",
149
- height: `${virtualRow.size}px`,
150
- transform: `translateY(${virtualRow.start}px)`,
151
- }}
152
- >
153
- <div style={{
154
- display: 'grid',
155
- gap: 16,
156
- gridTemplateColumns: gridConfig.gridTemplateColumns,
157
- height: gridConfig.minHeight,
158
- }}>
159
- {rowVariants.map((variant, colIndex) => {
160
- const index = startIndex + colIndex;
161
- return (
162
- <VariantCard
163
- key={variant.name}
164
- variant={variant}
165
- index={index}
166
- componentName={componentName}
167
- fragmentPath={fragmentPath}
168
- scale={effectiveScale}
169
- minHeight={gridConfig.minHeight}
170
- previewTheme={previewTheme}
171
- useIframeIsolation={useIframeIsolation}
172
- isHovered={hoveredIndex === index}
173
- onHover={() => setHoveredIndex(index)}
174
- onLeave={() => setHoveredIndex(null)}
175
- onClick={() => onSelectVariant?.(index)}
176
- />
177
- );
178
- })}
179
- </div>
180
- </div>
181
- );
182
- })}
183
- </div>
184
- </Box>
185
- ) : (
186
- <Box overflow="auto" padding="md" style={{ flex: 1 }}>
187
- <div style={{
188
- display: 'grid',
189
- gap: 16,
190
- gridTemplateColumns: gridConfig.gridTemplateColumns,
191
- }}>
192
- {variants.map((variant, index) => (
193
- <VariantCard
194
- key={variant.name}
195
- variant={variant}
196
- index={index}
197
- componentName={componentName}
198
- fragmentPath={fragmentPath}
199
- scale={effectiveScale}
200
- minHeight={gridConfig.minHeight}
201
- previewTheme={previewTheme}
202
- useIframeIsolation={useIframeIsolation}
203
- isHovered={hoveredIndex === index}
204
- onHover={() => setHoveredIndex(index)}
205
- onLeave={() => setHoveredIndex(null)}
206
- onClick={() => onSelectVariant?.(index)}
207
- />
208
- ))}
209
- </div>
210
- </Box>
211
- )}
212
- </Stack>
213
- );
214
- }
215
-
216
- interface VariantCardProps {
217
- variant: FragmentVariant;
218
- index: number;
219
- componentName: string;
220
- fragmentPath: string;
221
- scale: number;
222
- minHeight: string;
223
- previewTheme: "light" | "dark";
224
- useIframeIsolation: boolean;
225
- isHovered: boolean;
226
- onHover: () => void;
227
- onLeave: () => void;
228
- onClick: () => void;
229
- }
230
-
231
- function VariantCard({
232
- variant,
233
- index,
234
- componentName,
235
- fragmentPath,
236
- scale,
237
- minHeight,
238
- previewTheme,
239
- useIframeIsolation,
240
- isHovered,
241
- onHover,
242
- onLeave,
243
- onClick,
244
- }: VariantCardProps) {
245
- return (
246
- <div
247
- style={{
248
- position: 'relative',
249
- borderRadius: 8,
250
- border: isHovered
251
- ? '2px solid #3b82f6'
252
- : '1px solid var(--border)',
253
- overflow: 'hidden',
254
- transition: 'border-color 0.2s, box-shadow 0.2s',
255
- cursor: 'pointer',
256
- minHeight,
257
- boxShadow: isHovered
258
- ? '0 10px 15px -3px rgba(0,0,0,0.1), 0 0 0 3px rgba(59,130,246,0.2)'
259
- : 'none',
260
- }}
261
- onMouseEnter={onHover}
262
- onMouseLeave={onLeave}
263
- onClick={onClick}
264
- >
265
- {/* Header overlay - keep inline styles (CSS art) */}
266
- <div style={{
267
- position: 'absolute',
268
- top: 0,
269
- left: 0,
270
- right: 0,
271
- zIndex: 10,
272
- padding: '4px 8px',
273
- background: 'linear-gradient(to bottom, rgba(0,0,0,0.6), transparent)',
274
- }}>
275
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
276
- <span style={{
277
- fontSize: 12,
278
- fontWeight: 500,
279
- color: '#ffffff',
280
- overflow: 'hidden',
281
- textOverflow: 'ellipsis',
282
- whiteSpace: 'nowrap',
283
- }}>
284
- {variant.name}
285
- </span>
286
- <span style={{ fontSize: 10, color: 'rgba(255,255,255,0.7)' }}>
287
- #{index + 1}
288
- </span>
289
- </div>
290
- </div>
291
-
292
- {/* Click to view overlay */}
293
- <div
294
- style={{
295
- position: 'absolute',
296
- inset: 0,
297
- zIndex: 10,
298
- display: 'flex',
299
- alignItems: 'center',
300
- justifyContent: 'center',
301
- background: 'rgba(0,0,0,0.4)',
302
- transition: 'opacity 0.2s',
303
- opacity: isHovered ? 1 : 0,
304
- pointerEvents: isHovered ? 'auto' : 'none',
305
- }}
306
- >
307
- <span style={{
308
- padding: '6px 12px',
309
- background: '#2563eb',
310
- color: '#ffffff',
311
- fontSize: 12,
312
- fontWeight: 500,
313
- borderRadius: 9999,
314
- boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
315
- }}>
316
- Click to focus
317
- </span>
318
- </div>
319
-
320
- {/* Preview content */}
321
- <div
322
- data-theme={previewTheme}
323
- style={{
324
- height: '100%',
325
- width: '100%',
326
- overflow: 'hidden',
327
- display: 'flex',
328
- alignItems: 'center',
329
- justifyContent: 'center',
330
- }}
331
- >
332
- {useIframeIsolation ? (
333
- <IsolatedPreviewFrame
334
- fragmentPath={fragmentPath}
335
- variantName={variant.name}
336
- theme={previewTheme}
337
- width="100%"
338
- height="100%"
339
- minHeight={minHeight}
340
- />
341
- ) : (
342
- <div
343
- style={{
344
- padding: 16,
345
- transform: `scale(${scale})`,
346
- }}
347
- >
348
- <ErrorBoundary
349
- componentName={componentName}
350
- fallback={
351
- <Text size="xs" style={{ color: 'var(--color-danger)' }}>
352
- Error rendering variant
353
- </Text>
354
- }
355
- >
356
- <FragmentRenderer variant={variant}>
357
- {(content, isLoading, error) => {
358
- if (isLoading) {
359
- return (
360
- <Stack align="center" justify="center" style={{ padding: 16 }}>
361
- <LoaderIndicator />
362
- </Stack>
363
- );
364
- }
365
- if (error) {
366
- return (
367
- <Text size="xs" style={{ color: 'var(--color-danger)', padding: 8 }}>
368
- {error.message}
369
- </Text>
370
- );
371
- }
372
- return content;
373
- }}
374
- </FragmentRenderer>
375
- </ErrorBoundary>
376
- </div>
377
- )}
378
- </div>
379
-
380
- {/* Tags/badges */}
381
- {variant.hasPlayFunction && (
382
- <div style={{ position: 'absolute', bottom: 8, right: 8, zIndex: 10 }}>
383
- <Badge variant="info" size="sm">play</Badge>
384
- </div>
385
- )}
386
- </div>
387
- );
388
- }