@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|