@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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IsolatedPreviewFrame - Parent-side iframe wrapper for CSS isolation
|
|
3
|
+
*
|
|
4
|
+
* Renders component previews inside an iframe for complete CSS isolation.
|
|
5
|
+
* This prevents CSS conflicts between:
|
|
6
|
+
* - The viewer shell and the user's component library
|
|
7
|
+
* - Global styles from user components
|
|
8
|
+
* - Theme variables with same names
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { memo, useRef, useEffect, useState, useCallback } from 'react';
|
|
12
|
+
import clsx from 'clsx';
|
|
13
|
+
import { usePreviewBridge, type ParentMessage } from '../hooks/usePreviewBridge.js';
|
|
14
|
+
|
|
15
|
+
/** Maximum number of retry attempts */
|
|
16
|
+
const MAX_RETRIES = 3;
|
|
17
|
+
|
|
18
|
+
export interface IsolatedPreviewFrameProps {
|
|
19
|
+
/** Segment path (file path) to render */
|
|
20
|
+
segmentPath: string;
|
|
21
|
+
/** Variant name to render */
|
|
22
|
+
variantName: string;
|
|
23
|
+
/** Props to pass to the variant render function */
|
|
24
|
+
props?: Record<string, unknown>;
|
|
25
|
+
/** Theme for the preview */
|
|
26
|
+
theme: 'light' | 'dark';
|
|
27
|
+
/** Width of the preview (CSS value) */
|
|
28
|
+
width?: number | string;
|
|
29
|
+
/** Height of the preview (CSS value) */
|
|
30
|
+
height?: number | string;
|
|
31
|
+
/** Minimum height of the preview */
|
|
32
|
+
minHeight?: number | string;
|
|
33
|
+
/** Background style for the preview container */
|
|
34
|
+
background?: React.CSSProperties;
|
|
35
|
+
/** Additional class name for the container */
|
|
36
|
+
className?: string;
|
|
37
|
+
/** Additional styles for the container */
|
|
38
|
+
style?: React.CSSProperties;
|
|
39
|
+
/** Called when content size is reported */
|
|
40
|
+
onContentSize?: (size: { width: number; height: number }) => void;
|
|
41
|
+
/** Called when an error occurs */
|
|
42
|
+
onError?: (error: string) => void;
|
|
43
|
+
/** Unique key to force re-render on variant changes */
|
|
44
|
+
previewKey?: string;
|
|
45
|
+
/** Show size indicator on hover */
|
|
46
|
+
showSizeIndicator?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* IsolatedPreviewFrame renders a component preview inside an iframe
|
|
51
|
+
* for complete CSS isolation from the viewer shell.
|
|
52
|
+
*/
|
|
53
|
+
export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
54
|
+
segmentPath,
|
|
55
|
+
variantName,
|
|
56
|
+
props,
|
|
57
|
+
theme,
|
|
58
|
+
width = '100%',
|
|
59
|
+
height = 'auto',
|
|
60
|
+
minHeight = 120,
|
|
61
|
+
background,
|
|
62
|
+
className = '',
|
|
63
|
+
style,
|
|
64
|
+
onContentSize,
|
|
65
|
+
onError,
|
|
66
|
+
previewKey,
|
|
67
|
+
showSizeIndicator = false,
|
|
68
|
+
}: IsolatedPreviewFrameProps) {
|
|
69
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
70
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
71
|
+
const [frameError, setFrameError] = useState<string | null>(null);
|
|
72
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
73
|
+
const [iframeKey, setIframeKey] = useState(0);
|
|
74
|
+
const { isReady, isRendering, lastError, contentSize, render, setTheme, clearError } = usePreviewBridge(iframeRef);
|
|
75
|
+
const lastRenderRef = useRef<string>('');
|
|
76
|
+
const isFirstLoad = useRef(true);
|
|
77
|
+
|
|
78
|
+
// Build the preview URL
|
|
79
|
+
const previewUrl = '/fragments/preview/';
|
|
80
|
+
|
|
81
|
+
// Handle iframe load
|
|
82
|
+
const handleLoad = useCallback(() => {
|
|
83
|
+
setIsLoading(false);
|
|
84
|
+
setFrameError(null);
|
|
85
|
+
isFirstLoad.current = false;
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// Handle iframe error
|
|
89
|
+
const handleError = useCallback(() => {
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
setFrameError('Failed to load preview frame');
|
|
92
|
+
onError?.('Failed to load preview frame');
|
|
93
|
+
}, [onError]);
|
|
94
|
+
|
|
95
|
+
// Handle retry
|
|
96
|
+
const handleRetry = useCallback(() => {
|
|
97
|
+
if (retryCount >= MAX_RETRIES) return;
|
|
98
|
+
|
|
99
|
+
setFrameError(null);
|
|
100
|
+
clearError();
|
|
101
|
+
setRetryCount(c => c + 1);
|
|
102
|
+
setIsLoading(true);
|
|
103
|
+
lastRenderRef.current = ''; // Force re-render
|
|
104
|
+
setIframeKey(k => k + 1); // Force iframe reload
|
|
105
|
+
}, [retryCount, clearError]);
|
|
106
|
+
|
|
107
|
+
// Send render request when ready or when render params change
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!isReady) return;
|
|
110
|
+
|
|
111
|
+
// Create a render key to detect changes
|
|
112
|
+
const renderKey = `${segmentPath}:${variantName}:${JSON.stringify(props)}:${previewKey || ''}`;
|
|
113
|
+
if (renderKey === lastRenderRef.current) return;
|
|
114
|
+
lastRenderRef.current = renderKey;
|
|
115
|
+
|
|
116
|
+
render(segmentPath, variantName, props);
|
|
117
|
+
}, [isReady, segmentPath, variantName, props, previewKey, render]);
|
|
118
|
+
|
|
119
|
+
// Sync theme when it changes
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!isReady) return;
|
|
122
|
+
setTheme(theme);
|
|
123
|
+
}, [isReady, theme, setTheme]);
|
|
124
|
+
|
|
125
|
+
// Report content size changes
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (contentSize) {
|
|
128
|
+
onContentSize?.(contentSize);
|
|
129
|
+
}
|
|
130
|
+
}, [contentSize, onContentSize]);
|
|
131
|
+
|
|
132
|
+
// Report errors
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (lastError) {
|
|
135
|
+
setFrameError(lastError);
|
|
136
|
+
onError?.(lastError);
|
|
137
|
+
}
|
|
138
|
+
}, [lastError, onError]);
|
|
139
|
+
|
|
140
|
+
// Calculate iframe dimensions
|
|
141
|
+
const frameWidth = typeof width === 'number' ? `${width}px` : width;
|
|
142
|
+
const frameHeight = typeof height === 'number' ? `${height}px` : height;
|
|
143
|
+
const frameMinHeight = typeof minHeight === 'number' ? `${minHeight}px` : minHeight;
|
|
144
|
+
|
|
145
|
+
// Determine if we should show skeleton vs spinner
|
|
146
|
+
// Skeleton for initial load, spinner for subsequent renders
|
|
147
|
+
const showSkeleton = isLoading && isFirstLoad.current;
|
|
148
|
+
const showSpinner = !showSkeleton && (isLoading || isRendering);
|
|
149
|
+
const showContent = isReady && !isRendering && !frameError;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
className={clsx('isolated-preview-frame group', className)}
|
|
154
|
+
style={{
|
|
155
|
+
position: 'relative',
|
|
156
|
+
width: frameWidth,
|
|
157
|
+
height: frameHeight,
|
|
158
|
+
minHeight: frameMinHeight,
|
|
159
|
+
...background,
|
|
160
|
+
...style,
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{/* Skeleton loading overlay (initial load) */}
|
|
164
|
+
<div
|
|
165
|
+
className={clsx(
|
|
166
|
+
'absolute inset-0 z-10 transition-opacity duration-150',
|
|
167
|
+
showSkeleton ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
168
|
+
)}
|
|
169
|
+
style={{ background: 'rgba(255, 255, 255, 0.95)' }}
|
|
170
|
+
>
|
|
171
|
+
<PreviewSkeleton />
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Spinner overlay (subsequent renders) */}
|
|
175
|
+
<div
|
|
176
|
+
className={clsx(
|
|
177
|
+
'absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-150',
|
|
178
|
+
showSpinner ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
179
|
+
)}
|
|
180
|
+
style={{ background: 'rgba(255, 255, 255, 0.8)' }}
|
|
181
|
+
>
|
|
182
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6b7280', fontSize: 14 }}>
|
|
183
|
+
<LoadingSpinner />
|
|
184
|
+
<span>Rendering...</span>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Error overlay */}
|
|
189
|
+
<div
|
|
190
|
+
className={clsx(
|
|
191
|
+
'absolute inset-0 z-10 flex items-center justify-center p-4 transition-opacity duration-150',
|
|
192
|
+
frameError && !isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
193
|
+
)}
|
|
194
|
+
style={{ background: 'rgba(254, 242, 242, 0.95)' }}
|
|
195
|
+
>
|
|
196
|
+
<div
|
|
197
|
+
style={{
|
|
198
|
+
background: 'white',
|
|
199
|
+
border: '1px solid #fecaca',
|
|
200
|
+
borderRadius: 8,
|
|
201
|
+
padding: 16,
|
|
202
|
+
maxWidth: 400,
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<div style={{ color: '#dc2626', fontWeight: 500, marginBottom: 8 }}>
|
|
206
|
+
Preview Error
|
|
207
|
+
</div>
|
|
208
|
+
<div style={{ color: '#991b1b', fontSize: 13, marginBottom: retryCount < MAX_RETRIES ? 12 : 0 }}>
|
|
209
|
+
{frameError}
|
|
210
|
+
</div>
|
|
211
|
+
{retryCount < MAX_RETRIES && (
|
|
212
|
+
<button
|
|
213
|
+
onClick={handleRetry}
|
|
214
|
+
style={{
|
|
215
|
+
padding: '6px 12px',
|
|
216
|
+
fontSize: 13,
|
|
217
|
+
fontWeight: 500,
|
|
218
|
+
color: 'white',
|
|
219
|
+
background: '#dc2626',
|
|
220
|
+
border: 'none',
|
|
221
|
+
borderRadius: 6,
|
|
222
|
+
cursor: 'pointer',
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
Retry ({MAX_RETRIES - retryCount} remaining)
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* The iframe */}
|
|
232
|
+
<iframe
|
|
233
|
+
key={iframeKey}
|
|
234
|
+
ref={iframeRef}
|
|
235
|
+
src={previewUrl}
|
|
236
|
+
title={`Preview: ${variantName}`}
|
|
237
|
+
onLoad={handleLoad}
|
|
238
|
+
onError={handleError}
|
|
239
|
+
className={clsx(
|
|
240
|
+
'transition-opacity duration-150',
|
|
241
|
+
showContent ? 'opacity-100' : 'opacity-0'
|
|
242
|
+
)}
|
|
243
|
+
style={{
|
|
244
|
+
width: '100%',
|
|
245
|
+
height: '100%',
|
|
246
|
+
border: 'none',
|
|
247
|
+
display: 'block',
|
|
248
|
+
background: 'transparent',
|
|
249
|
+
}}
|
|
250
|
+
// Security attributes
|
|
251
|
+
sandbox="allow-scripts allow-same-origin"
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
{/* Size indicator */}
|
|
255
|
+
{showSizeIndicator && contentSize && (
|
|
256
|
+
<div
|
|
257
|
+
className="absolute bottom-1 right-1 px-1.5 py-0.5 rounded font-mono
|
|
258
|
+
opacity-0 group-hover:opacity-100 transition-opacity duration-150"
|
|
259
|
+
style={{
|
|
260
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
261
|
+
color: 'white',
|
|
262
|
+
fontSize: 10,
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
{contentSize.width} × {contentSize.height}px
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Skeleton loading placeholder
|
|
274
|
+
*/
|
|
275
|
+
function PreviewSkeleton() {
|
|
276
|
+
return (
|
|
277
|
+
<div className="animate-pulse p-4">
|
|
278
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/3 mb-3" />
|
|
279
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
|
|
280
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3" />
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Simple loading spinner component
|
|
287
|
+
*/
|
|
288
|
+
function LoadingSpinner() {
|
|
289
|
+
return (
|
|
290
|
+
<svg
|
|
291
|
+
style={{
|
|
292
|
+
width: 20,
|
|
293
|
+
height: 20,
|
|
294
|
+
animation: 'spin 0.8s linear infinite',
|
|
295
|
+
}}
|
|
296
|
+
viewBox="0 0 24 24"
|
|
297
|
+
fill="none"
|
|
298
|
+
>
|
|
299
|
+
<style>
|
|
300
|
+
{`@keyframes spin { to { transform: rotate(360deg); } }`}
|
|
301
|
+
</style>
|
|
302
|
+
<circle
|
|
303
|
+
cx="12"
|
|
304
|
+
cy="12"
|
|
305
|
+
r="10"
|
|
306
|
+
stroke="#e5e7eb"
|
|
307
|
+
strokeWidth="2"
|
|
308
|
+
fill="none"
|
|
309
|
+
/>
|
|
310
|
+
<path
|
|
311
|
+
d="M12 2a10 10 0 0 1 10 10"
|
|
312
|
+
stroke="#3b82f6"
|
|
313
|
+
strokeWidth="2"
|
|
314
|
+
strokeLinecap="round"
|
|
315
|
+
fill="none"
|
|
316
|
+
/>
|
|
317
|
+
</svg>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default IsolatedPreviewFrame;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useMemo, useEffect, useState } from "react";
|
|
2
|
+
import type { SegmentDefinition } from "../../core/index.js";
|
|
3
|
+
import { VariantRenderer } from "./VariantRenderer.js";
|
|
4
|
+
import { getBackgroundStyle, type BackgroundOption, type ZoomLevel } from "./PreviewToolbar.js";
|
|
5
|
+
|
|
6
|
+
interface IsolatedRenderProps {
|
|
7
|
+
segments: Array<{ path: string; segment: SegmentDefinition }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Isolated render component for screenshot capture and standalone viewing.
|
|
12
|
+
* Renders a single variant with minimal UI for visual testing.
|
|
13
|
+
* URL params: ?isolated=true&component=Name&variant=VariantName&zoom=100&bg=white&theme=light
|
|
14
|
+
*/
|
|
15
|
+
export function IsolatedRender({ segments }: IsolatedRenderProps) {
|
|
16
|
+
const [ready, setReady] = useState(false);
|
|
17
|
+
|
|
18
|
+
// Parse query parameters
|
|
19
|
+
const params = useMemo(() => {
|
|
20
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
21
|
+
const zoomParam = parseInt(searchParams.get("zoom") || "100", 10);
|
|
22
|
+
const bgParam = searchParams.get("bg") || "white";
|
|
23
|
+
return {
|
|
24
|
+
component: searchParams.get("component"),
|
|
25
|
+
variant: searchParams.get("variant"),
|
|
26
|
+
theme: searchParams.get("theme") || "light",
|
|
27
|
+
zoom: [50, 75, 100, 150, 200].includes(zoomParam) ? zoomParam as ZoomLevel : 100 as ZoomLevel,
|
|
28
|
+
background: ["white", "black", "checkerboard", "transparent"].includes(bgParam)
|
|
29
|
+
? bgParam as BackgroundOption
|
|
30
|
+
: "white" as BackgroundOption,
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
// Find the matching segment and variant
|
|
35
|
+
const match = useMemo(() => {
|
|
36
|
+
if (!params.component || !params.variant) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const segment = segments.find(
|
|
41
|
+
(s) => s.segment.meta.name === params.component
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!segment) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const variant = segment.segment.variants.find(
|
|
49
|
+
(v) => v.name === params.variant
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!variant) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { segment: segment.segment, variant };
|
|
57
|
+
}, [segments, params]);
|
|
58
|
+
|
|
59
|
+
// Apply theme
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
document.documentElement.setAttribute("data-theme", params.theme);
|
|
62
|
+
|
|
63
|
+
// Signal ready after a short delay for fonts and styles to settle
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
setReady(true);
|
|
66
|
+
}, 50);
|
|
67
|
+
|
|
68
|
+
return () => clearTimeout(timer);
|
|
69
|
+
}, [params.theme]);
|
|
70
|
+
|
|
71
|
+
// Error state - missing component or variant
|
|
72
|
+
if (!params.component || !params.variant) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="p-4 text-red-500 font-mono text-sm">
|
|
75
|
+
Error: Missing component or variant parameter
|
|
76
|
+
<pre className="mt-2 text-xs">
|
|
77
|
+
Required: ?component=ComponentName&variant=VariantName
|
|
78
|
+
</pre>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Error state - component/variant not found
|
|
84
|
+
if (!match) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="p-4 text-red-500 font-mono text-sm">
|
|
87
|
+
Error: Component "{params.component}" variant "{params.variant}" not
|
|
88
|
+
found
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Render the variant in isolation
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
id="isolated-render"
|
|
97
|
+
data-ready={ready}
|
|
98
|
+
className="min-h-screen p-8 flex items-center justify-center"
|
|
99
|
+
style={getBackgroundStyle(params.background)}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
transform: `scale(${params.zoom / 100})`,
|
|
104
|
+
transformOrigin: 'center center',
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<VariantRenderer variant={match.variant} />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard Shortcuts Help Modal
|
|
3
|
+
*
|
|
4
|
+
* Displays all available keyboard shortcuts in a modal overlay.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect } from "react";
|
|
8
|
+
import { SHORTCUTS } from "../hooks/useKeyboardShortcuts.js";
|
|
9
|
+
import { CloseIcon } from "./Icons.js";
|
|
10
|
+
|
|
11
|
+
interface KeyboardShortcutsHelpProps {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function KeyboardShortcutsHelp({ isOpen, onClose }: KeyboardShortcutsHelpProps) {
|
|
17
|
+
// Close on escape
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isOpen) return;
|
|
20
|
+
|
|
21
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
22
|
+
if (e.key === "Escape") {
|
|
23
|
+
onClose();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
document.addEventListener("keydown", handleEscape);
|
|
28
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
29
|
+
}, [isOpen, onClose]);
|
|
30
|
+
|
|
31
|
+
if (!isOpen) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
>
|
|
38
|
+
{/* Backdrop */}
|
|
39
|
+
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
|
40
|
+
|
|
41
|
+
{/* Modal */}
|
|
42
|
+
<div
|
|
43
|
+
className="relative bg-[--bg-primary] border border-[--border] rounded-xl shadow-2xl max-w-md w-full"
|
|
44
|
+
onClick={(e) => e.stopPropagation()}
|
|
45
|
+
>
|
|
46
|
+
{/* Header */}
|
|
47
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-[--border]">
|
|
48
|
+
<h2 className="text-base font-semibold text-primary">Keyboard Shortcuts</h2>
|
|
49
|
+
<button
|
|
50
|
+
onClick={onClose}
|
|
51
|
+
className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded-lg transition-colors"
|
|
52
|
+
>
|
|
53
|
+
<CloseIcon className="w-4 h-4" />
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{/* Shortcuts List */}
|
|
58
|
+
<div className="p-5 space-y-3">
|
|
59
|
+
{SHORTCUTS.map((shortcut, index) => (
|
|
60
|
+
<div key={index} className="flex items-center justify-between">
|
|
61
|
+
<span className="text-sm text-secondary">{shortcut.description}</span>
|
|
62
|
+
<div className="flex items-center gap-1.5">
|
|
63
|
+
{shortcut.keys.map((key, keyIndex) => (
|
|
64
|
+
<span key={keyIndex} className="flex items-center">
|
|
65
|
+
{keyIndex > 0 && (
|
|
66
|
+
<span className="text-xs text-tertiary mx-1">or</span>
|
|
67
|
+
)}
|
|
68
|
+
<kbd className="px-2 py-1 text-xs font-mono bg-[--bg-secondary] border border-[--border] rounded text-primary">
|
|
69
|
+
{key}
|
|
70
|
+
</kbd>
|
|
71
|
+
</span>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Footer */}
|
|
79
|
+
<div className="px-5 py-3 border-t border-[--border] bg-[--bg-secondary] rounded-b-xl">
|
|
80
|
+
<p className="text-xs text-tertiary text-center">
|
|
81
|
+
Press <kbd className="px-1.5 py-0.5 text-xs font-mono bg-[--bg-primary] border border-[--border] rounded">?</kbd> to toggle this help
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default KeyboardShortcutsHelp;
|