@fragments-sdk/viewer 0.2.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.
- package/LICENSE +84 -0
- package/index.html +28 -0
- package/package.json +71 -0
- package/src/__tests__/a11y-fixes.test.ts +358 -0
- package/src/__tests__/jsx-parser.test.ts +502 -0
- package/src/__tests__/render-utils.test.ts +232 -0
- package/src/__tests__/style-utils.test.ts +404 -0
- package/src/app/index.ts +1 -0
- package/src/assets/fragments-logo.ts +4 -0
- package/src/assets/fragments_logo.png +0 -0
- package/src/components/AccessibilityPanel.tsx +1457 -0
- package/src/components/ActionCapture.tsx +172 -0
- package/src/components/ActionsPanel.tsx +332 -0
- package/src/components/AllVariantsPreview.tsx +78 -0
- package/src/components/App.tsx +604 -0
- package/src/components/BottomPanel.tsx +288 -0
- package/src/components/CodePanel.naming.test.tsx +59 -0
- package/src/components/CodePanel.tsx +118 -0
- package/src/components/CommandPalette.tsx +392 -0
- package/src/components/ComponentDocView.tsx +164 -0
- package/src/components/ComponentGraph.tsx +380 -0
- package/src/components/ComponentHeader.tsx +88 -0
- package/src/components/ContractPanel.tsx +241 -0
- package/src/components/DeviceMockup.tsx +156 -0
- package/src/components/EmptyVariantMessage.tsx +54 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/FigmaEmbed.tsx +238 -0
- package/src/components/FragmentEditor.tsx +525 -0
- package/src/components/FragmentRenderer.tsx +61 -0
- package/src/components/HeaderSearch.tsx +24 -0
- package/src/components/HealthDashboard.tsx +441 -0
- package/src/components/HmrStatusIndicator.tsx +61 -0
- package/src/components/Icons.tsx +479 -0
- package/src/components/InteractionsPanel.tsx +757 -0
- package/src/components/IsolatedPreviewFrame.tsx +390 -0
- package/src/components/IsolatedRender.tsx +113 -0
- package/src/components/KeyboardShortcutsHelp.tsx +53 -0
- package/src/components/LandingPage.tsx +420 -0
- package/src/components/Layout.tsx +27 -0
- package/src/components/LeftSidebar.tsx +472 -0
- package/src/components/LoadErrorMessage.tsx +102 -0
- package/src/components/MultiViewportPreview.tsx +527 -0
- package/src/components/NoVariantsMessage.tsx +59 -0
- package/src/components/PanelShell.tsx +161 -0
- package/src/components/PerformancePanel.tsx +304 -0
- package/src/components/PreviewArea.tsx +254 -0
- package/src/components/PreviewAside.tsx +168 -0
- package/src/components/PreviewFrameHost.tsx +304 -0
- package/src/components/PreviewToolbar.tsx +80 -0
- package/src/components/PropsEditor.tsx +506 -0
- package/src/components/PropsTable.tsx +111 -0
- package/src/components/RelationsSection.tsx +88 -0
- package/src/components/ResizablePanel.tsx +271 -0
- package/src/components/RightSidebar.tsx +102 -0
- package/src/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/components/ScreenshotButton.tsx +90 -0
- package/src/components/ShadowPreview.tsx +204 -0
- package/src/components/Sidebar.tsx +169 -0
- package/src/components/SkeletonLoader.tsx +161 -0
- package/src/components/ThemeProvider.tsx +42 -0
- package/src/components/Toast.tsx +3 -0
- package/src/components/TokenStylePanel.tsx +699 -0
- package/src/components/TopToolbar.tsx +159 -0
- package/src/components/Untitled +1 -0
- package/src/components/UsageSection.tsx +95 -0
- package/src/components/VariantMatrix.tsx +391 -0
- package/src/components/VariantRenderer.tsx +131 -0
- package/src/components/VariantTabs.tsx +40 -0
- package/src/components/ViewerHeader.tsx +69 -0
- package/src/components/ViewerStateSync.tsx +52 -0
- package/src/components/ViewportSelector.tsx +172 -0
- package/src/components/WebMCPDevTools.tsx +503 -0
- package/src/components/WebMCPIntegration.tsx +47 -0
- package/src/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/components/_future/CreatePage.tsx +835 -0
- package/src/components/viewer-utils.ts +16 -0
- package/src/composition-renderer.ts +381 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/ui.ts +166 -0
- package/src/entry.tsx +335 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useA11yCache.ts +383 -0
- package/src/hooks/useA11yService.ts +364 -0
- package/src/hooks/useActions.ts +138 -0
- package/src/hooks/useAppState.ts +147 -0
- package/src/hooks/useCompiledFragments.ts +42 -0
- package/src/hooks/useFigmaIntegration.ts +132 -0
- package/src/hooks/useHmrStatus.ts +109 -0
- package/src/hooks/useKeyboardShortcuts.ts +270 -0
- package/src/hooks/usePreviewBridge.ts +347 -0
- package/src/hooks/useScrollSpy.ts +78 -0
- package/src/hooks/useShadowStyles.ts +221 -0
- package/src/hooks/useUrlState.ts +318 -0
- package/src/hooks/useViewSettings.ts +111 -0
- package/src/intelligence/healthReport.ts +505 -0
- package/src/intelligence/styleDrift.ts +340 -0
- package/src/intelligence/usageScanner.ts +309 -0
- package/src/jsx-parser.ts +486 -0
- package/src/preview-frame-entry.tsx +25 -0
- package/src/preview-frame.html +148 -0
- package/src/render-template.html +68 -0
- package/src/render-utils.ts +311 -0
- package/src/shared/ComponentDocContent.module.scss +10 -0
- package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/shared/ComponentDocContent.tsx +274 -0
- package/src/shared/DocsHeaderBar.tsx +129 -0
- package/src/shared/DocsPageAsideHost.tsx +89 -0
- package/src/shared/DocsPageShell.tsx +124 -0
- package/src/shared/DocsSearchCommand.tsx +99 -0
- package/src/shared/DocsSidebarNav.tsx +66 -0
- package/src/shared/PropsTable.module.scss +68 -0
- package/src/shared/PropsTable.module.scss.d.ts +2 -0
- package/src/shared/PropsTable.tsx +76 -0
- package/src/shared/VariantPreviewCard.module.scss +114 -0
- package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/shared/VariantPreviewCard.tsx +137 -0
- package/src/shared/docs-data/index.ts +32 -0
- package/src/shared/docs-data/mcp-configs.ts +72 -0
- package/src/shared/docs-data/palettes.ts +75 -0
- package/src/shared/docs-data/setup-examples.ts +55 -0
- package/src/shared/docs-layout.scss +28 -0
- package/src/shared/docs-layout.scss.d.ts +2 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/types.ts +53 -0
- package/src/style-utils.ts +414 -0
- package/src/styles/globals.css +278 -0
- package/src/types/a11y.ts +197 -0
- package/src/utils/a11y-fixes.ts +509 -0
- package/src/utils/actionExport.ts +372 -0
- package/src/utils/colorSchemes.ts +201 -0
- package/src/utils/contrast.ts +246 -0
- package/src/utils/detectRelationships.ts +256 -0
- package/src/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/webmcp/analytics.ts +165 -0
- package/src/webmcp/index.ts +3 -0
- package/src/webmcp/posthog-bridge.ts +39 -0
- package/src/webmcp/runtime-tools.ts +152 -0
- package/src/webmcp/scan-utils.ts +135 -0
- package/src/webmcp/use-tool-analytics.ts +69 -0
- package/src/webmcp/viewer-state.ts +45 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useRef, useEffect, useState, useMemo, useCallback } from "react";
|
|
2
|
+
import { FigmaIcon } from "./Icons.js";
|
|
3
|
+
import { Stack, Text, Button } from '@fragments-sdk/ui';
|
|
4
|
+
|
|
5
|
+
interface FigmaEmbedProps {
|
|
6
|
+
/** Current Figma URL to display */
|
|
7
|
+
figmaUrl: string;
|
|
8
|
+
/** All Figma URLs for the current component (for preloading) */
|
|
9
|
+
allFigmaUrls?: string[];
|
|
10
|
+
zoom?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ParsedFigmaUrl {
|
|
16
|
+
fileKey: string;
|
|
17
|
+
nodeId?: string;
|
|
18
|
+
fullUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a Figma URL to extract file key and node ID.
|
|
23
|
+
*/
|
|
24
|
+
function parseFigmaUrl(figmaUrl: string): ParsedFigmaUrl | null {
|
|
25
|
+
try {
|
|
26
|
+
const urlPattern = /figma\.com\/(?:file|design)\/([^/]+)\/[^?]*(?:\?.*node-id=([^&]+))?/i;
|
|
27
|
+
const match = figmaUrl.match(urlPattern);
|
|
28
|
+
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
|
|
31
|
+
const fileKey = match[1];
|
|
32
|
+
const nodeId = match[2] ? decodeURIComponent(match[2]) : undefined;
|
|
33
|
+
|
|
34
|
+
return { fileKey, nodeId, fullUrl: figmaUrl };
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a Figma embed URL.
|
|
42
|
+
*/
|
|
43
|
+
function buildEmbedUrl(fileKey: string, nodeId?: string): string {
|
|
44
|
+
let embedUrl = `https://embed.figma.com/design/${fileKey}?embed-host=fragments`;
|
|
45
|
+
|
|
46
|
+
if (nodeId) {
|
|
47
|
+
const embedNodeId = nodeId.replace(/:/g, "-");
|
|
48
|
+
embedUrl += `&node-id=${embedNodeId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
embedUrl += "&hide-ui=1";
|
|
52
|
+
return embedUrl;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get a unique key for a Figma URL (used for iframe identification)
|
|
57
|
+
*/
|
|
58
|
+
function getUrlKey(figmaUrl: string): string {
|
|
59
|
+
const parsed = parseFigmaUrl(figmaUrl);
|
|
60
|
+
if (!parsed) return figmaUrl;
|
|
61
|
+
return `${parsed.fileKey}:${parsed.nodeId || "default"}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* FigmaEmbed component with iframe pooling for instant variant switching.
|
|
66
|
+
*
|
|
67
|
+
* Strategy: Pre-load iframes for ALL variants of the current component.
|
|
68
|
+
* When switching variants, we just show/hide the appropriate iframe.
|
|
69
|
+
* This makes switching instantaneous since iframes are already loaded.
|
|
70
|
+
*
|
|
71
|
+
* The design embed doesn't support postMessage navigation (only prototype
|
|
72
|
+
* embeds do), so this pooling approach is the only way to achieve instant
|
|
73
|
+
* switching.
|
|
74
|
+
*/
|
|
75
|
+
export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, style }: FigmaEmbedProps) {
|
|
76
|
+
// Track loaded iframes
|
|
77
|
+
const [loadedUrls, setLoadedUrls] = useState<Set<string>>(new Set());
|
|
78
|
+
const [error, setError] = useState<string | null>(null);
|
|
79
|
+
|
|
80
|
+
// Parse the current URL
|
|
81
|
+
const currentParsed = useMemo(() => parseFigmaUrl(figmaUrl), [figmaUrl]);
|
|
82
|
+
const currentKey = useMemo(() => getUrlKey(figmaUrl), [figmaUrl]);
|
|
83
|
+
|
|
84
|
+
// Get all URLs to preload (deduplicated)
|
|
85
|
+
const urlsToPreload = useMemo(() => {
|
|
86
|
+
const urls = allFigmaUrls && allFigmaUrls.length > 0 ? allFigmaUrls : [figmaUrl];
|
|
87
|
+
const uniqueUrls = new Map<string, string>();
|
|
88
|
+
|
|
89
|
+
for (const url of urls) {
|
|
90
|
+
const key = getUrlKey(url);
|
|
91
|
+
if (!uniqueUrls.has(key)) {
|
|
92
|
+
uniqueUrls.set(key, url);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Array.from(uniqueUrls.entries()).map(([key, url]) => ({
|
|
97
|
+
key,
|
|
98
|
+
url,
|
|
99
|
+
parsed: parseFigmaUrl(url),
|
|
100
|
+
}));
|
|
101
|
+
}, [allFigmaUrls, figmaUrl]);
|
|
102
|
+
|
|
103
|
+
// Handle iframe load
|
|
104
|
+
const handleIframeLoad = useCallback((urlKey: string) => {
|
|
105
|
+
setLoadedUrls(prev => {
|
|
106
|
+
const next = new Set(prev);
|
|
107
|
+
next.add(urlKey);
|
|
108
|
+
return next;
|
|
109
|
+
});
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
// Calculate zoom transform
|
|
113
|
+
const zoomStyle: React.CSSProperties = zoom !== 100
|
|
114
|
+
? {
|
|
115
|
+
transform: `scale(${zoom / 100})`,
|
|
116
|
+
transformOrigin: "center",
|
|
117
|
+
width: `${100 / (zoom / 100)}%`,
|
|
118
|
+
height: `${100 / (zoom / 100)}%`,
|
|
119
|
+
}
|
|
120
|
+
: {};
|
|
121
|
+
|
|
122
|
+
// Check if current URL is loaded
|
|
123
|
+
const isCurrentLoaded = loadedUrls.has(currentKey);
|
|
124
|
+
|
|
125
|
+
// If we can't parse the URL, show error
|
|
126
|
+
if (!currentParsed) {
|
|
127
|
+
return (
|
|
128
|
+
<Stack className={className} align="center" justify="center" style={style}>
|
|
129
|
+
<Stack direction="column" align="center" gap="sm" style={{ color: 'var(--text-tertiary)', padding: '16px', textAlign: 'center' }}>
|
|
130
|
+
<FigmaIcon style={{ width: '24px', height: '24px' }} />
|
|
131
|
+
<Text size="xs">Unable to embed Figma design</Text>
|
|
132
|
+
<Button
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="sm"
|
|
135
|
+
onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
|
|
136
|
+
>
|
|
137
|
+
Open in Figma
|
|
138
|
+
</Button>
|
|
139
|
+
</Stack>
|
|
140
|
+
</Stack>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className={className} style={{ ...style, position: "relative", overflow: "hidden" }}>
|
|
146
|
+
{/* Loading overlay - shows while current iframe is loading */}
|
|
147
|
+
{!isCurrentLoaded && (
|
|
148
|
+
<Stack align="center" justify="center" style={{ position: 'absolute', inset: 0, backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
|
|
149
|
+
<Stack direction="column" align="center" gap="sm">
|
|
150
|
+
<FigmaIcon style={{ width: '20px', height: '20px', color: 'var(--text-tertiary)' }} />
|
|
151
|
+
<Text size="xs" color="tertiary">Loading Figma...</Text>
|
|
152
|
+
</Stack>
|
|
153
|
+
</Stack>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Error overlay */}
|
|
157
|
+
{error && (
|
|
158
|
+
<Stack align="center" justify="center" style={{ position: 'absolute', inset: 0, backgroundColor: 'var(--bg-secondary)', zIndex: 20 }}>
|
|
159
|
+
<Stack direction="column" align="center" gap="sm" style={{ color: 'var(--text-tertiary)' }}>
|
|
160
|
+
<FigmaIcon style={{ width: '24px', height: '24px' }} />
|
|
161
|
+
<Text size="xs">{error}</Text>
|
|
162
|
+
</Stack>
|
|
163
|
+
</Stack>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/*
|
|
167
|
+
Iframe pool: Pre-load all variant URLs as hidden iframes.
|
|
168
|
+
Only the current variant's iframe is visible (opacity: 1, z-index: 10).
|
|
169
|
+
Others are hidden (opacity: 0, z-index: 1) but stay loaded.
|
|
170
|
+
|
|
171
|
+
This approach works because:
|
|
172
|
+
1. Figma iframes stay active even when hidden
|
|
173
|
+
2. Switching just changes CSS visibility
|
|
174
|
+
3. No network requests when switching variants
|
|
175
|
+
*/}
|
|
176
|
+
{urlsToPreload.map(({ key, parsed }) => {
|
|
177
|
+
if (!parsed) return null;
|
|
178
|
+
|
|
179
|
+
const embedUrl = buildEmbedUrl(parsed.fileKey, parsed.nodeId);
|
|
180
|
+
const isActive = key === currentKey;
|
|
181
|
+
const isLoaded = loadedUrls.has(key);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<iframe
|
|
185
|
+
key={key}
|
|
186
|
+
src={embedUrl}
|
|
187
|
+
style={{
|
|
188
|
+
position: 'absolute',
|
|
189
|
+
inset: 0,
|
|
190
|
+
width: '100%',
|
|
191
|
+
height: '100%',
|
|
192
|
+
border: 'none',
|
|
193
|
+
transition: 'opacity 150ms',
|
|
194
|
+
...zoomStyle,
|
|
195
|
+
opacity: isActive && isLoaded ? 1 : 0,
|
|
196
|
+
zIndex: isActive ? 10 : 1,
|
|
197
|
+
pointerEvents: isActive ? "auto" : "none",
|
|
198
|
+
}}
|
|
199
|
+
onLoad={() => handleIframeLoad(key)}
|
|
200
|
+
onError={() => setError("Failed to load Figma embed")}
|
|
201
|
+
allowFullScreen
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
})}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Hook to collect all Figma URLs from a fragment's variants.
|
|
211
|
+
* This enables the FigmaEmbed to preload all variant iframes.
|
|
212
|
+
*/
|
|
213
|
+
export function useAllFigmaUrls(
|
|
214
|
+
fragment: { meta: { figma?: string }; variants?: Array<{ figma?: string }> } | undefined
|
|
215
|
+
): string[] {
|
|
216
|
+
return useMemo(() => {
|
|
217
|
+
if (!fragment) return [];
|
|
218
|
+
|
|
219
|
+
const urls: string[] = [];
|
|
220
|
+
|
|
221
|
+
// Add meta-level Figma URL
|
|
222
|
+
if (fragment.meta.figma) {
|
|
223
|
+
urls.push(fragment.meta.figma);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Add variant-level Figma URLs
|
|
227
|
+
if (fragment.variants) {
|
|
228
|
+
for (const variant of fragment.variants) {
|
|
229
|
+
if (variant.figma) {
|
|
230
|
+
urls.push(variant.figma);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Deduplicate
|
|
236
|
+
return [...new Set(urls)];
|
|
237
|
+
}, [fragment]);
|
|
238
|
+
}
|