@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,304 @@
1
+ /**
2
+ * PreviewFrameHost - Iframe-side component that renders components in isolation
3
+ *
4
+ * This component runs inside the preview iframe and:
5
+ * 1. Listens for render requests from the parent window
6
+ * 2. Loads and renders the requested fragment variant
7
+ * 3. Applies theme styling
8
+ * 4. Reports render status back to parent
9
+ */
10
+
11
+ import { useState, useEffect, useRef } from 'react';
12
+ import {
13
+ usePreviewVariantRuntime,
14
+ type FragmentVariant,
15
+ } from '@fragments-sdk/core';
16
+ import { useFrameBridge } from '../hooks/usePreviewBridge.js';
17
+
18
+ // Types for fragment data
19
+ interface PreviewFragmentDefinition {
20
+ meta: {
21
+ name: string;
22
+ description?: string;
23
+ category?: string;
24
+ };
25
+ variants?: FragmentVariant[];
26
+ }
27
+
28
+ interface FragmentItem {
29
+ path: string;
30
+ fragment: PreviewFragmentDefinition;
31
+ }
32
+
33
+ // Cached fragments
34
+ let cachedFragments: FragmentItem[] | null = null;
35
+ let fragmentsPromise: Promise<FragmentItem[]> | null = null;
36
+
37
+ /**
38
+ * Load fragments from the virtual module
39
+ */
40
+ async function loadFragments(): Promise<FragmentItem[]> {
41
+ if (cachedFragments) {
42
+ return cachedFragments;
43
+ }
44
+
45
+ if (fragmentsPromise) {
46
+ return fragmentsPromise;
47
+ }
48
+
49
+ fragmentsPromise = (async () => {
50
+ try {
51
+ // @ts-expect-error Virtual module
52
+ const module = await import('virtual:fragments');
53
+ if (module.fragmentsPromise) {
54
+ cachedFragments = await module.fragmentsPromise;
55
+ } else {
56
+ cachedFragments = module.fragments || [];
57
+ }
58
+ return cachedFragments!;
59
+ } catch (error) {
60
+ console.error('[PreviewFrameHost] Failed to load fragments:', error);
61
+ throw error;
62
+ }
63
+ })();
64
+
65
+ return fragmentsPromise;
66
+ }
67
+
68
+ /**
69
+ * Find a fragment by its path
70
+ */
71
+ function findFragmentByPath(fragments: FragmentItem[], path: string): FragmentItem | undefined {
72
+ return fragments.find(s => s.path === path);
73
+ }
74
+
75
+ /**
76
+ * Find a variant by name within a fragment
77
+ */
78
+ function findVariant(fragment: PreviewFragmentDefinition, variantName: string): FragmentVariant | undefined {
79
+ return fragment.variants?.find(v => v.name === variantName);
80
+ }
81
+
82
+ type PreviewMode = 'centered' | 'full-bleed';
83
+
84
+ function resolvePreviewMode(fragment: PreviewFragmentDefinition): PreviewMode {
85
+ const name = fragment.meta.name.toLowerCase();
86
+ const category = (fragment.meta.category || '').toLowerCase();
87
+
88
+ if (name.includes('appshell') || name.includes('sidebar') || name.includes('header') || name.includes('layout')) {
89
+ return 'full-bleed';
90
+ }
91
+
92
+ return 'centered';
93
+ }
94
+
95
+ /**
96
+ * Error boundary for catching render errors
97
+ */
98
+ function ErrorDisplay({ message, stack }: { message: string; stack?: string }) {
99
+ return (
100
+ <div style={{ padding: '16px', color: '#dc2626', background: 'rgba(254, 242, 242, 0.95)', borderRadius: '8px', margin: '16px' }}>
101
+ <div style={{ fontWeight: 500, marginBottom: 8 }}>Render Error</div>
102
+ <div>{message}</div>
103
+ {stack && (
104
+ <pre style={{ marginTop: 8, fontSize: 11, opacity: 0.8 }}>
105
+ {stack}
106
+ </pre>
107
+ )}
108
+ </div>
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Loading indicator
114
+ */
115
+ function LoadingIndicator() {
116
+ return (
117
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px', gap: '8px', color: '#6b7280' }}>
118
+ <div style={{ width: '16px', height: '16px', border: '2px solid #e5e7eb', borderTopColor: '#3b82f6', borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />
119
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
120
+ <span>Loading component...</span>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Variant renderer that handles async loaders
127
+ */
128
+ function VariantRenderer({
129
+ variant,
130
+ props,
131
+ mode,
132
+ onRendered,
133
+ onError,
134
+ }: {
135
+ variant: FragmentVariant;
136
+ props?: Record<string, unknown>;
137
+ mode: PreviewMode;
138
+ onRendered: (width: number, height: number) => void;
139
+ onError: (message: string, stack?: string) => void;
140
+ }) {
141
+ const { content, isLoading, error } = usePreviewVariantRuntime({
142
+ variant,
143
+ loadedData: props,
144
+ });
145
+ const containerRef = useRef<HTMLDivElement>(null);
146
+ const hasReported = useRef(false);
147
+ const lastReportedError = useRef<string | null>(null);
148
+
149
+ useEffect(() => {
150
+ hasReported.current = false;
151
+ }, [variant, props, mode]);
152
+
153
+ useEffect(() => {
154
+ if (!error) {
155
+ lastReportedError.current = null;
156
+ return;
157
+ }
158
+
159
+ const signature = `${error.message}:${error.stack ?? ''}`;
160
+ if (lastReportedError.current === signature) {
161
+ return;
162
+ }
163
+
164
+ lastReportedError.current = signature;
165
+ onError(error.message, error.stack);
166
+ }, [error, onError]);
167
+
168
+ // Report rendered size after content renders
169
+ useEffect(() => {
170
+ if (!content || hasReported.current || isLoading || error) return;
171
+
172
+ // Wait for next frame to ensure DOM has updated
173
+ requestAnimationFrame(() => {
174
+ if (containerRef.current && !hasReported.current) {
175
+ const rect = containerRef.current.getBoundingClientRect();
176
+ hasReported.current = true;
177
+ onRendered(rect.width, rect.height);
178
+ }
179
+ });
180
+ }, [content, error, isLoading, onRendered]);
181
+
182
+ if (isLoading) {
183
+ return <LoadingIndicator />;
184
+ }
185
+
186
+ if (error) {
187
+ return <ErrorDisplay message={error.message} stack={error.stack} />;
188
+ }
189
+
190
+ return (
191
+ <div
192
+ ref={containerRef}
193
+ style={{
194
+ display: 'block',
195
+ width: mode === 'full-bleed' ? '100%' : 'fit-content',
196
+ maxWidth: '100%',
197
+ minHeight: mode === 'full-bleed' ? '100%' : undefined,
198
+ transition: 'opacity 150ms',
199
+ opacity: content ? 1 : 0,
200
+ }}
201
+ >
202
+ {content}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Main PreviewFrameHost component
209
+ */
210
+ export function PreviewFrameHost() {
211
+ const { renderRequest, theme, notifyReady, notifyRendered, notifyError } = useFrameBridge();
212
+ const [fragments, setFragments] = useState<FragmentItem[] | null>(null);
213
+ const [loadError, setLoadError] = useState<string | null>(null);
214
+ const [currentVariant, setCurrentVariant] = useState<FragmentVariant | null>(null);
215
+ const [currentProps, setCurrentProps] = useState<Record<string, unknown> | undefined>(undefined);
216
+ const [previewMode, setPreviewMode] = useState<PreviewMode>('centered');
217
+
218
+ // Apply theme to document
219
+ useEffect(() => {
220
+ if (theme === 'dark') {
221
+ document.documentElement.classList.add('dark');
222
+ } else {
223
+ document.documentElement.classList.remove('dark');
224
+ }
225
+ }, [theme]);
226
+
227
+ useEffect(() => {
228
+ document.body.setAttribute('data-preview-mode', previewMode);
229
+ }, [previewMode]);
230
+
231
+ // Load fragments on mount
232
+ useEffect(() => {
233
+ loadFragments()
234
+ .then(segs => {
235
+ setFragments(segs);
236
+ notifyReady();
237
+ })
238
+ .catch(err => {
239
+ const message = err instanceof Error ? err.message : 'Failed to load fragments';
240
+ setLoadError(message);
241
+ notifyError(message);
242
+ });
243
+ }, [notifyReady, notifyError]);
244
+
245
+ // Handle render requests
246
+ useEffect(() => {
247
+ if (!renderRequest || !fragments) return;
248
+
249
+ const { fragmentPath, variantName, props } = renderRequest;
250
+
251
+ // Find fragment
252
+ const fragmentItem = findFragmentByPath(fragments, fragmentPath);
253
+ if (!fragmentItem) {
254
+ notifyError(`Fragment not found: ${fragmentPath}`);
255
+ setCurrentVariant(null);
256
+ return;
257
+ }
258
+
259
+ // Find variant
260
+ const variant = findVariant(fragmentItem.fragment, variantName);
261
+ if (!variant) {
262
+ notifyError(`Variant not found: ${variantName} in ${fragmentPath}`);
263
+ setCurrentVariant(null);
264
+ return;
265
+ }
266
+
267
+ setPreviewMode(resolvePreviewMode(fragmentItem.fragment));
268
+ setCurrentVariant(variant);
269
+ setCurrentProps(props);
270
+ }, [renderRequest, fragments, notifyError]);
271
+
272
+ // Show loading state
273
+ if (!fragments && !loadError) {
274
+ return <LoadingIndicator />;
275
+ }
276
+
277
+ // Show load error
278
+ if (loadError) {
279
+ return <ErrorDisplay message={loadError} />;
280
+ }
281
+
282
+ // Show waiting state
283
+ if (!currentVariant) {
284
+ return (
285
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px', gap: '8px', color: '#6b7280' }}>
286
+ <span>Waiting for render request...</span>
287
+ </div>
288
+ );
289
+ }
290
+
291
+ // Render the variant
292
+ return (
293
+ <VariantRenderer
294
+ key={`${renderRequest?.fragmentPath}-${renderRequest?.variantName}`}
295
+ variant={currentVariant}
296
+ props={currentProps}
297
+ mode={previewMode}
298
+ onRendered={notifyRendered}
299
+ onError={notifyError}
300
+ />
301
+ );
302
+ }
303
+
304
+ export default PreviewFrameHost;
@@ -0,0 +1,80 @@
1
+ import { useEffect, useCallback } from 'react';
2
+ import { Button, Menu, Stack } from '@fragments-sdk/ui';
3
+ import { ZOOM_LEVELS, type ZoomLevel } from '../constants/ui.js';
4
+ import { ZoomIcon, ChevronDownIcon } from './Icons.js';
5
+
6
+ // Re-export types for consumers
7
+ export type { ZoomLevel };
8
+
9
+ interface PreviewToolbarProps {
10
+ zoom: ZoomLevel;
11
+ onZoomChange: (zoom: ZoomLevel) => void;
12
+ }
13
+
14
+ export function PreviewToolbar({
15
+ zoom,
16
+ onZoomChange,
17
+ }: PreviewToolbarProps) {
18
+ // Keyboard shortcuts for zoom
19
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
20
+ // Don't handle if in input/textarea
21
+ const target = e.target as HTMLElement;
22
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
23
+ return;
24
+ }
25
+
26
+ if (e.key === '=' || e.key === '+') {
27
+ e.preventDefault();
28
+ const currentIndex = ZOOM_LEVELS.indexOf(zoom);
29
+ if (currentIndex < ZOOM_LEVELS.length - 1) {
30
+ onZoomChange(ZOOM_LEVELS[currentIndex + 1]);
31
+ }
32
+ } else if (e.key === '-') {
33
+ e.preventDefault();
34
+ const currentIndex = ZOOM_LEVELS.indexOf(zoom);
35
+ if (currentIndex > 0) {
36
+ onZoomChange(ZOOM_LEVELS[currentIndex - 1]);
37
+ }
38
+ } else if (e.key === '0') {
39
+ e.preventDefault();
40
+ onZoomChange(100);
41
+ }
42
+ }, [zoom, onZoomChange]);
43
+
44
+ useEffect(() => {
45
+ document.addEventListener('keydown', handleKeyDown);
46
+ return () => document.removeEventListener('keydown', handleKeyDown);
47
+ }, [handleKeyDown]);
48
+
49
+ return (
50
+ <Stack direction="row" gap="sm" align="center">
51
+ <Menu>
52
+ <Menu.Trigger asChild>
53
+ <Button variant="ghost" size="sm" title="Zoom level (+/-/0)">
54
+ <Stack direction="row" gap="xs" align="center">
55
+ <span style={{ display: 'inline-flex', width: '14px', height: '14px' }}>
56
+ <ZoomIcon />
57
+ </span>
58
+ <span>{zoom}%</span>
59
+ <span style={{ display: 'inline-flex', width: '12px', height: '12px' }}>
60
+ <ChevronDownIcon />
61
+ </span>
62
+ </Stack>
63
+ </Button>
64
+ </Menu.Trigger>
65
+ <Menu.Content side="bottom" align="start">
66
+ <Menu.RadioGroup
67
+ value={String(zoom)}
68
+ onValueChange={(value: string) => onZoomChange(Number(value) as ZoomLevel)}
69
+ >
70
+ {ZOOM_LEVELS.map((level) => (
71
+ <Menu.RadioItem key={level} value={String(level)}>
72
+ {level}%
73
+ </Menu.RadioItem>
74
+ ))}
75
+ </Menu.RadioGroup>
76
+ </Menu.Content>
77
+ </Menu>
78
+ </Stack>
79
+ );
80
+ }