@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.
Files changed (141) hide show
  1. package/LICENSE +84 -0
  2. package/index.html +28 -0
  3. package/package.json +71 -0
  4. package/src/__tests__/a11y-fixes.test.ts +358 -0
  5. package/src/__tests__/jsx-parser.test.ts +502 -0
  6. package/src/__tests__/render-utils.test.ts +232 -0
  7. package/src/__tests__/style-utils.test.ts +404 -0
  8. package/src/app/index.ts +1 -0
  9. package/src/assets/fragments-logo.ts +4 -0
  10. package/src/assets/fragments_logo.png +0 -0
  11. package/src/components/AccessibilityPanel.tsx +1457 -0
  12. package/src/components/ActionCapture.tsx +172 -0
  13. package/src/components/ActionsPanel.tsx +332 -0
  14. package/src/components/AllVariantsPreview.tsx +78 -0
  15. package/src/components/App.tsx +604 -0
  16. package/src/components/BottomPanel.tsx +288 -0
  17. package/src/components/CodePanel.naming.test.tsx +59 -0
  18. package/src/components/CodePanel.tsx +118 -0
  19. package/src/components/CommandPalette.tsx +392 -0
  20. package/src/components/ComponentDocView.tsx +164 -0
  21. package/src/components/ComponentGraph.tsx +380 -0
  22. package/src/components/ComponentHeader.tsx +88 -0
  23. package/src/components/ContractPanel.tsx +241 -0
  24. package/src/components/DeviceMockup.tsx +156 -0
  25. package/src/components/EmptyVariantMessage.tsx +54 -0
  26. package/src/components/ErrorBoundary.tsx +97 -0
  27. package/src/components/FigmaEmbed.tsx +238 -0
  28. package/src/components/FragmentEditor.tsx +525 -0
  29. package/src/components/FragmentRenderer.tsx +61 -0
  30. package/src/components/HeaderSearch.tsx +24 -0
  31. package/src/components/HealthDashboard.tsx +441 -0
  32. package/src/components/HmrStatusIndicator.tsx +61 -0
  33. package/src/components/Icons.tsx +479 -0
  34. package/src/components/InteractionsPanel.tsx +757 -0
  35. package/src/components/IsolatedPreviewFrame.tsx +390 -0
  36. package/src/components/IsolatedRender.tsx +113 -0
  37. package/src/components/KeyboardShortcutsHelp.tsx +53 -0
  38. package/src/components/LandingPage.tsx +420 -0
  39. package/src/components/Layout.tsx +27 -0
  40. package/src/components/LeftSidebar.tsx +472 -0
  41. package/src/components/LoadErrorMessage.tsx +102 -0
  42. package/src/components/MultiViewportPreview.tsx +527 -0
  43. package/src/components/NoVariantsMessage.tsx +59 -0
  44. package/src/components/PanelShell.tsx +161 -0
  45. package/src/components/PerformancePanel.tsx +304 -0
  46. package/src/components/PreviewArea.tsx +254 -0
  47. package/src/components/PreviewAside.tsx +168 -0
  48. package/src/components/PreviewFrameHost.tsx +304 -0
  49. package/src/components/PreviewToolbar.tsx +80 -0
  50. package/src/components/PropsEditor.tsx +506 -0
  51. package/src/components/PropsTable.tsx +111 -0
  52. package/src/components/RelationsSection.tsx +88 -0
  53. package/src/components/ResizablePanel.tsx +271 -0
  54. package/src/components/RightSidebar.tsx +102 -0
  55. package/src/components/RuntimeToolsRegistrar.tsx +17 -0
  56. package/src/components/ScreenshotButton.tsx +90 -0
  57. package/src/components/ShadowPreview.tsx +204 -0
  58. package/src/components/Sidebar.tsx +169 -0
  59. package/src/components/SkeletonLoader.tsx +161 -0
  60. package/src/components/ThemeProvider.tsx +42 -0
  61. package/src/components/Toast.tsx +3 -0
  62. package/src/components/TokenStylePanel.tsx +699 -0
  63. package/src/components/TopToolbar.tsx +159 -0
  64. package/src/components/Untitled +1 -0
  65. package/src/components/UsageSection.tsx +95 -0
  66. package/src/components/VariantMatrix.tsx +391 -0
  67. package/src/components/VariantRenderer.tsx +131 -0
  68. package/src/components/VariantTabs.tsx +40 -0
  69. package/src/components/ViewerHeader.tsx +69 -0
  70. package/src/components/ViewerStateSync.tsx +52 -0
  71. package/src/components/ViewportSelector.tsx +172 -0
  72. package/src/components/WebMCPDevTools.tsx +503 -0
  73. package/src/components/WebMCPIntegration.tsx +47 -0
  74. package/src/components/WebMCPStatusIndicator.tsx +60 -0
  75. package/src/components/_future/CreatePage.tsx +835 -0
  76. package/src/components/viewer-utils.ts +16 -0
  77. package/src/composition-renderer.ts +381 -0
  78. package/src/constants/index.ts +1 -0
  79. package/src/constants/ui.ts +166 -0
  80. package/src/entry.tsx +335 -0
  81. package/src/hooks/index.ts +2 -0
  82. package/src/hooks/useA11yCache.ts +383 -0
  83. package/src/hooks/useA11yService.ts +364 -0
  84. package/src/hooks/useActions.ts +138 -0
  85. package/src/hooks/useAppState.ts +147 -0
  86. package/src/hooks/useCompiledFragments.ts +42 -0
  87. package/src/hooks/useFigmaIntegration.ts +132 -0
  88. package/src/hooks/useHmrStatus.ts +109 -0
  89. package/src/hooks/useKeyboardShortcuts.ts +270 -0
  90. package/src/hooks/usePreviewBridge.ts +347 -0
  91. package/src/hooks/useScrollSpy.ts +78 -0
  92. package/src/hooks/useShadowStyles.ts +221 -0
  93. package/src/hooks/useUrlState.ts +318 -0
  94. package/src/hooks/useViewSettings.ts +111 -0
  95. package/src/intelligence/healthReport.ts +505 -0
  96. package/src/intelligence/styleDrift.ts +340 -0
  97. package/src/intelligence/usageScanner.ts +309 -0
  98. package/src/jsx-parser.ts +486 -0
  99. package/src/preview-frame-entry.tsx +25 -0
  100. package/src/preview-frame.html +148 -0
  101. package/src/render-template.html +68 -0
  102. package/src/render-utils.ts +311 -0
  103. package/src/shared/ComponentDocContent.module.scss +10 -0
  104. package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
  105. package/src/shared/ComponentDocContent.tsx +274 -0
  106. package/src/shared/DocsHeaderBar.tsx +129 -0
  107. package/src/shared/DocsPageAsideHost.tsx +89 -0
  108. package/src/shared/DocsPageShell.tsx +124 -0
  109. package/src/shared/DocsSearchCommand.tsx +99 -0
  110. package/src/shared/DocsSidebarNav.tsx +66 -0
  111. package/src/shared/PropsTable.module.scss +68 -0
  112. package/src/shared/PropsTable.module.scss.d.ts +2 -0
  113. package/src/shared/PropsTable.tsx +76 -0
  114. package/src/shared/VariantPreviewCard.module.scss +114 -0
  115. package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
  116. package/src/shared/VariantPreviewCard.tsx +137 -0
  117. package/src/shared/docs-data/index.ts +32 -0
  118. package/src/shared/docs-data/mcp-configs.ts +72 -0
  119. package/src/shared/docs-data/palettes.ts +75 -0
  120. package/src/shared/docs-data/setup-examples.ts +55 -0
  121. package/src/shared/docs-layout.scss +28 -0
  122. package/src/shared/docs-layout.scss.d.ts +2 -0
  123. package/src/shared/index.ts +34 -0
  124. package/src/shared/types.ts +53 -0
  125. package/src/style-utils.ts +414 -0
  126. package/src/styles/globals.css +278 -0
  127. package/src/types/a11y.ts +197 -0
  128. package/src/utils/a11y-fixes.ts +509 -0
  129. package/src/utils/actionExport.ts +372 -0
  130. package/src/utils/colorSchemes.ts +201 -0
  131. package/src/utils/contrast.ts +246 -0
  132. package/src/utils/detectRelationships.ts +256 -0
  133. package/src/webmcp/__tests__/analytics.test.ts +108 -0
  134. package/src/webmcp/analytics.ts +165 -0
  135. package/src/webmcp/index.ts +3 -0
  136. package/src/webmcp/posthog-bridge.ts +39 -0
  137. package/src/webmcp/runtime-tools.ts +152 -0
  138. package/src/webmcp/scan-utils.ts +135 -0
  139. package/src/webmcp/use-tool-analytics.ts +69 -0
  140. package/src/webmcp/viewer-state.ts +45 -0
  141. 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
+ }