@fragments-sdk/cli 0.7.14 → 0.7.16
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/dist/bin.js +7 -7
- package/dist/{chunk-CRTN6BIW.js → chunk-QLTLLQBI.js} +2 -2
- package/dist/{chunk-TQOGBAOZ.js → chunk-WLXFE6XW.js} +91 -2
- package/dist/chunk-WLXFE6XW.js.map +1 -0
- package/dist/core/index.d.ts +44 -3
- package/dist/core/index.js +11 -3
- package/dist/{defineFragment-C6PFzZyo.d.ts → defineFragment-BI9KoPrs.d.ts} +1 -1
- package/dist/{generate-ZPERYZLF.js → generate-ICIPKCKV.js} +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/init-DIZ6UNBL.js +806 -0
- package/dist/init-DIZ6UNBL.js.map +1 -0
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-BSMLGBX4.js → scan-X3DI2X5G.js} +2 -2
- package/dist/{service-QACVPR37.js → service-JEWWTSKI.js} +2 -2
- package/dist/{static-viewer-2RQD5QLR.js → static-viewer-JIWCYKVK.js} +2 -2
- package/dist/{tokens-A3BZIQPB.js → tokens-K2AGUUOJ.js} +2 -2
- package/dist/{viewer-CNLZQUFO.js → viewer-QKIAPTPG.js} +126 -15
- package/dist/viewer-QKIAPTPG.js.map +1 -0
- package/package.json +3 -2
- package/src/commands/init-framework.ts +414 -0
- package/src/commands/init.ts +41 -1
- package/src/core/__tests__/preview-runtime.test.tsx +111 -0
- package/src/core/index.ts +13 -0
- package/src/core/preview-runtime.tsx +144 -0
- package/src/viewer/components/App.tsx +8 -3
- package/src/viewer/components/FragmentRenderer.tsx +61 -0
- package/src/viewer/components/HealthDashboard.tsx +1 -1
- package/src/viewer/components/IsolatedPreviewFrame.tsx +10 -8
- package/src/viewer/components/PreviewFrameHost.tsx +27 -60
- package/src/viewer/components/PropsTable.tsx +2 -2
- package/src/viewer/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/viewer/components/SkeletonLoader.tsx +114 -125
- package/src/viewer/components/VariantMatrix.tsx +3 -3
- package/src/viewer/components/ViewerStateSync.tsx +52 -0
- package/src/viewer/components/WebMCPDevTools.tsx +509 -0
- package/src/viewer/components/WebMCPIntegration.tsx +47 -0
- package/src/viewer/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/viewer/entry.tsx +32 -5
- package/src/viewer/hooks/useA11yService.ts +1 -135
- package/src/viewer/hooks/useCompiledFragments.ts +42 -0
- package/src/viewer/index.html +1 -1
- package/src/viewer/public/favicon.ico +0 -0
- package/src/viewer/server.ts +59 -3
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +19 -0
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +1 -1
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +69 -104
- package/src/viewer/vite-plugin.ts +76 -1
- package/src/viewer/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/viewer/webmcp/analytics.ts +165 -0
- package/src/viewer/webmcp/index.ts +3 -0
- package/src/viewer/webmcp/posthog-bridge.ts +39 -0
- package/src/viewer/webmcp/runtime-tools.ts +152 -0
- package/src/viewer/webmcp/scan-utils.ts +135 -0
- package/src/viewer/webmcp/use-tool-analytics.ts +69 -0
- package/src/viewer/webmcp/viewer-state.ts +45 -0
- package/dist/chunk-TQOGBAOZ.js.map +0 -1
- package/dist/init-GID2DXB3.js +0 -498
- package/dist/init-GID2DXB3.js.map +0 -1
- package/dist/viewer-CNLZQUFO.js.map +0 -1
- package/src/viewer/components/StoryRenderer.tsx +0 -121
- /package/dist/{chunk-CRTN6BIW.js.map → chunk-QLTLLQBI.js.map} +0 -0
- /package/dist/{generate-ZPERYZLF.js.map → generate-ICIPKCKV.js.map} +0 -0
- /package/dist/{scan-BSMLGBX4.js.map → scan-X3DI2X5G.js.map} +0 -0
- /package/dist/{service-QACVPR37.js.map → service-JEWWTSKI.js.map} +0 -0
- /package/dist/{static-viewer-2RQD5QLR.js.map → static-viewer-JIWCYKVK.js.map} +0 -0
- /package/dist/{tokens-A3BZIQPB.js.map → tokens-K2AGUUOJ.js.map} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
import type { VariantLoader, VariantRenderOptions } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal contract for rendering a preview variant.
|
|
6
|
+
* Compatible with fragment variants and Storybook-adapted variants.
|
|
7
|
+
*/
|
|
8
|
+
export interface PreviewVariantLike {
|
|
9
|
+
render: (options?: VariantRenderOptions) => ReactNode;
|
|
10
|
+
loaders?: VariantLoader[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreviewRuntimeState {
|
|
14
|
+
content: ReactNode | null;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
loadedData?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PreviewRuntimeOptions {
|
|
21
|
+
variant?: PreviewVariantLike | null;
|
|
22
|
+
loadedData?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMPTY_STATE: PreviewRuntimeState = {
|
|
26
|
+
content: null,
|
|
27
|
+
isLoading: false,
|
|
28
|
+
error: null,
|
|
29
|
+
loadedData: undefined,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function toError(error: unknown): Error {
|
|
33
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute all variant loaders and merge their payloads.
|
|
38
|
+
* `loadedData` is applied last so host-level overrides win.
|
|
39
|
+
*/
|
|
40
|
+
export async function executeVariantLoaders(
|
|
41
|
+
loaders: VariantLoader[] | undefined,
|
|
42
|
+
loadedData?: Record<string, unknown>,
|
|
43
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
44
|
+
const hasLoaders = !!loaders && loaders.length > 0;
|
|
45
|
+
if (!hasLoaders) {
|
|
46
|
+
return loadedData;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const results = await Promise.all(loaders.map((loader) => loader()));
|
|
50
|
+
const mergedFromLoaders = results.reduce<Record<string, unknown>>(
|
|
51
|
+
(acc, result) => ({ ...acc, ...result }),
|
|
52
|
+
{},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return loadedData ? { ...mergedFromLoaders, ...loadedData } : mergedFromLoaders;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a full runtime state (loader execution + render) in one async call.
|
|
60
|
+
* This is useful for testing and for hook/component orchestration.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolvePreviewRuntimeState(
|
|
63
|
+
options: PreviewRuntimeOptions,
|
|
64
|
+
): Promise<PreviewRuntimeState> {
|
|
65
|
+
const { variant, loadedData } = options;
|
|
66
|
+
if (!variant) {
|
|
67
|
+
return EMPTY_STATE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const mergedLoadedData = await executeVariantLoaders(variant.loaders, loadedData);
|
|
72
|
+
const content = variant.render({ loadedData: mergedLoadedData });
|
|
73
|
+
return {
|
|
74
|
+
content,
|
|
75
|
+
isLoading: false,
|
|
76
|
+
error: null,
|
|
77
|
+
loadedData: mergedLoadedData,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
content: null,
|
|
82
|
+
isLoading: false,
|
|
83
|
+
error: toError(error),
|
|
84
|
+
loadedData: undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hook for rendering a preview variant with loader support.
|
|
91
|
+
*/
|
|
92
|
+
export function usePreviewVariantRuntime(
|
|
93
|
+
options: PreviewRuntimeOptions,
|
|
94
|
+
): PreviewRuntimeState {
|
|
95
|
+
const { variant, loadedData } = options;
|
|
96
|
+
const [state, setState] = useState<PreviewRuntimeState>(EMPTY_STATE);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
|
|
101
|
+
if (!variant) {
|
|
102
|
+
setState(EMPTY_STATE);
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasLoaders = !!variant.loaders && variant.loaders.length > 0;
|
|
109
|
+
setState({
|
|
110
|
+
content: null,
|
|
111
|
+
isLoading: hasLoaders,
|
|
112
|
+
error: null,
|
|
113
|
+
loadedData: undefined,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
resolvePreviewRuntimeState({ variant, loadedData }).then((nextState) => {
|
|
117
|
+
if (!cancelled) {
|
|
118
|
+
setState(nextState);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [variant, loadedData]);
|
|
126
|
+
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface PreviewVariantRuntimeProps extends PreviewRuntimeOptions {
|
|
131
|
+
children: (state: PreviewRuntimeState) => ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render-prop component wrapper around `usePreviewVariantRuntime`.
|
|
136
|
+
*/
|
|
137
|
+
export function PreviewVariantRuntime({
|
|
138
|
+
variant,
|
|
139
|
+
loadedData,
|
|
140
|
+
children,
|
|
141
|
+
}: PreviewVariantRuntimeProps) {
|
|
142
|
+
const state = usePreviewVariantRuntime({ variant, loadedData });
|
|
143
|
+
return <>{children(state)}</>;
|
|
144
|
+
}
|
|
@@ -20,7 +20,7 @@ import { PreviewToolbar } from "./PreviewToolbar.js";
|
|
|
20
20
|
import { PreviewArea } from "./PreviewArea.js";
|
|
21
21
|
import { BottomPanel } from "./BottomPanel.js";
|
|
22
22
|
import { IsolatedRender } from "./IsolatedRender.js";
|
|
23
|
-
import {
|
|
23
|
+
import { FragmentRenderer, LoaderIndicator } from "./FragmentRenderer.js";
|
|
24
24
|
import { HealthDashboard } from "./HealthDashboard.js";
|
|
25
25
|
import { useAllFigmaUrls } from "./FigmaEmbed.js";
|
|
26
26
|
import { ActionCapture } from "./ActionCapture.js";
|
|
@@ -50,6 +50,8 @@ import { useActions } from "../hooks/useActions.js";
|
|
|
50
50
|
import { useUrlState, findFragmentByName, findVariantIndex } from "../hooks/useUrlState.js";
|
|
51
51
|
import { usePanelDock } from "./ResizablePanel.js";
|
|
52
52
|
import { useTheme } from "./ThemeProvider.js";
|
|
53
|
+
import { WebMCPStatusIndicator } from "./WebMCPStatusIndicator.js";
|
|
54
|
+
import { ViewerStateSync } from "./ViewerStateSync.js";
|
|
53
55
|
|
|
54
56
|
interface AppProps {
|
|
55
57
|
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
@@ -340,14 +342,14 @@ export function App({ fragments }: AppProps) {
|
|
|
340
342
|
|
|
341
343
|
return (
|
|
342
344
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
343
|
-
<
|
|
345
|
+
<FragmentRenderer variant={variant}>
|
|
344
346
|
{(content, isLoading, error) => {
|
|
345
347
|
if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
|
|
346
348
|
if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={variant.name} hint="Check the console for the full error stack trace." />;
|
|
347
349
|
if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={variant.name} hint="The variant's render function didn't return any JSX." />;
|
|
348
350
|
return content;
|
|
349
351
|
}}
|
|
350
|
-
</
|
|
352
|
+
</FragmentRenderer>
|
|
351
353
|
</ActionCapture>
|
|
352
354
|
);
|
|
353
355
|
}, []);
|
|
@@ -364,6 +366,7 @@ export function App({ fragments }: AppProps) {
|
|
|
364
366
|
|
|
365
367
|
return (
|
|
366
368
|
<>
|
|
369
|
+
<ViewerStateSync fragments={fragments} activeVariantIndex={safeVariantIndex} />
|
|
367
370
|
<KeyboardShortcutsHelp isOpen={uiState.showShortcutsHelp} onClose={() => uiActions.setShortcutsHelp(false)} />
|
|
368
371
|
<CommandPalette
|
|
369
372
|
isOpen={uiState.showCommandPalette}
|
|
@@ -611,6 +614,7 @@ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef
|
|
|
611
614
|
<HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
|
|
612
615
|
<Header.Spacer />
|
|
613
616
|
<Header.Actions>
|
|
617
|
+
<WebMCPStatusIndicator />
|
|
614
618
|
<ThemeToggle
|
|
615
619
|
size="sm"
|
|
616
620
|
value={resolvedTheme}
|
|
@@ -833,6 +837,7 @@ function TopToolbar({
|
|
|
833
837
|
<Separator orientation="vertical" style={{ height: '16px' }} />
|
|
834
838
|
</>
|
|
835
839
|
)}
|
|
840
|
+
<WebMCPStatusIndicator />
|
|
836
841
|
<ThemeToggle
|
|
837
842
|
size="sm"
|
|
838
843
|
value={resolvedTheme}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
PreviewVariantRuntime,
|
|
4
|
+
type FragmentVariant,
|
|
5
|
+
} from "../../core/index.js";
|
|
6
|
+
|
|
7
|
+
interface FragmentRendererProps {
|
|
8
|
+
/** The variant to render */
|
|
9
|
+
variant: FragmentVariant;
|
|
10
|
+
/** Children render function - receives rendered content */
|
|
11
|
+
children: (content: ReactNode | null, isLoading: boolean, error: Error | null) => ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Component that handles async loader execution before rendering a fragment variant.
|
|
16
|
+
*
|
|
17
|
+
* If the variant has loaders:
|
|
18
|
+
* 1. Shows loading state while loaders execute
|
|
19
|
+
* 2. Merges all loaded data
|
|
20
|
+
* 3. Passes loaded data to render function
|
|
21
|
+
*
|
|
22
|
+
* If no loaders, renders immediately.
|
|
23
|
+
*/
|
|
24
|
+
export function FragmentRenderer({ variant, children }: FragmentRendererProps) {
|
|
25
|
+
return (
|
|
26
|
+
<PreviewVariantRuntime variant={variant}>
|
|
27
|
+
{({ content, isLoading, error }) => children(content, isLoading, error)}
|
|
28
|
+
</PreviewVariantRuntime>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Loading indicator for fragment variants with loaders
|
|
34
|
+
*/
|
|
35
|
+
export function LoaderIndicator() {
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-secondary)', fontSize: '14px' }}>
|
|
38
|
+
<svg
|
|
39
|
+
style={{ animation: 'spin 1s linear infinite', width: '16px', height: '16px' }}
|
|
40
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
41
|
+
fill="none"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
>
|
|
44
|
+
<circle
|
|
45
|
+
style={{ opacity: 0.25 }}
|
|
46
|
+
cx="12"
|
|
47
|
+
cy="12"
|
|
48
|
+
r="10"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
strokeWidth="4"
|
|
51
|
+
/>
|
|
52
|
+
<path
|
|
53
|
+
style={{ opacity: 0.75 }}
|
|
54
|
+
fill="currentColor"
|
|
55
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
56
|
+
/>
|
|
57
|
+
</svg>
|
|
58
|
+
<span>Running loaders...</span>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
8
8
|
import type { FragmentDefinition } from '../../core/index.js';
|
|
9
9
|
import type { ImpactValue } from 'axe-core';
|
|
10
|
-
import { Badge, Progress, Stack, Text, Card, EmptyState
|
|
10
|
+
import { Badge, Progress, Stack, Text, Card, EmptyState } from '@fragments-sdk/ui';
|
|
11
11
|
import {
|
|
12
12
|
getAllA11yData,
|
|
13
13
|
getA11ySummary,
|
|
@@ -70,9 +70,9 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
70
70
|
const [frameError, setFrameError] = useState<string | null>(null);
|
|
71
71
|
const [retryCount, setRetryCount] = useState(0);
|
|
72
72
|
const [iframeKey, setIframeKey] = useState(0);
|
|
73
|
+
const [hasRenderedOnce, setHasRenderedOnce] = useState(false);
|
|
73
74
|
const { isReady, isRendering, lastError, contentSize, render, setTheme, clearError } = usePreviewBridge(iframeRef);
|
|
74
75
|
const lastRenderRef = useRef<string>('');
|
|
75
|
-
const isFirstLoad = useRef(true);
|
|
76
76
|
const [isHovered, setIsHovered] = useState(false);
|
|
77
77
|
|
|
78
78
|
// Build the preview URL
|
|
@@ -82,7 +82,6 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
82
82
|
const handleLoad = useCallback(() => {
|
|
83
83
|
setIsLoading(false);
|
|
84
84
|
setFrameError(null);
|
|
85
|
-
isFirstLoad.current = false;
|
|
86
85
|
}, []);
|
|
87
86
|
|
|
88
87
|
// Handle iframe error
|
|
@@ -100,6 +99,7 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
100
99
|
clearError();
|
|
101
100
|
setRetryCount(c => c + 1);
|
|
102
101
|
setIsLoading(true);
|
|
102
|
+
setHasRenderedOnce(false);
|
|
103
103
|
lastRenderRef.current = ''; // Force re-render
|
|
104
104
|
setIframeKey(k => k + 1); // Force iframe reload
|
|
105
105
|
}, [retryCount, clearError]);
|
|
@@ -125,6 +125,7 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
125
125
|
// Report content size changes
|
|
126
126
|
useEffect(() => {
|
|
127
127
|
if (contentSize) {
|
|
128
|
+
setHasRenderedOnce(true);
|
|
128
129
|
onContentSize?.(contentSize);
|
|
129
130
|
}
|
|
130
131
|
}, [contentSize, onContentSize]);
|
|
@@ -142,11 +143,12 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
142
143
|
const frameHeight = typeof height === 'number' ? `${height}px` : height;
|
|
143
144
|
const frameMinHeight = typeof minHeight === 'number' ? `${minHeight}px` : minHeight;
|
|
144
145
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
146
|
+
// Keep first-load skeleton visible until the first successful render signal.
|
|
147
|
+
// After first render, preserve content visibility and show spinner overlays
|
|
148
|
+
// for later renders to avoid white/blank flashes.
|
|
149
|
+
const showSkeleton = !hasRenderedOnce && !frameError;
|
|
150
|
+
const showSpinner = hasRenderedOnce && !frameError && (isLoading || isRendering);
|
|
151
|
+
const showContent = !frameError && (hasRenderedOnce || (isReady && !isRendering));
|
|
150
152
|
|
|
151
153
|
return (
|
|
152
154
|
<div
|
|
@@ -259,7 +261,7 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
259
261
|
border: 'none',
|
|
260
262
|
display: 'block',
|
|
261
263
|
background: 'transparent',
|
|
262
|
-
transition: 'opacity
|
|
264
|
+
transition: 'opacity 120ms ease',
|
|
263
265
|
opacity: showContent ? 1 : 0,
|
|
264
266
|
}}
|
|
265
267
|
// Security attributes
|
|
@@ -8,19 +8,15 @@
|
|
|
8
8
|
* 4. Reports render status back to parent
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useEffect, useRef
|
|
11
|
+
import { useState, useEffect, useRef } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
usePreviewVariantRuntime,
|
|
14
|
+
type FragmentVariant,
|
|
15
|
+
} from '../../core/index.js';
|
|
12
16
|
import { useFrameBridge } from '../hooks/usePreviewBridge.js';
|
|
13
17
|
|
|
14
18
|
// Types for fragment data
|
|
15
|
-
interface
|
|
16
|
-
name: string;
|
|
17
|
-
render: (options?: { loadedData?: Record<string, unknown> }) => ReactNode;
|
|
18
|
-
loaders?: Array<() => Promise<Record<string, unknown>>>;
|
|
19
|
-
props?: Record<string, unknown>;
|
|
20
|
-
hasPlayFunction?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface FragmentDefinition {
|
|
19
|
+
interface PreviewFragmentDefinition {
|
|
24
20
|
meta: {
|
|
25
21
|
name: string;
|
|
26
22
|
description?: string;
|
|
@@ -31,7 +27,7 @@ interface FragmentDefinition {
|
|
|
31
27
|
|
|
32
28
|
interface FragmentItem {
|
|
33
29
|
path: string;
|
|
34
|
-
fragment:
|
|
30
|
+
fragment: PreviewFragmentDefinition;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
// Cached fragments
|
|
@@ -79,13 +75,13 @@ function findFragmentByPath(fragments: FragmentItem[], path: string): FragmentIt
|
|
|
79
75
|
/**
|
|
80
76
|
* Find a variant by name within a fragment
|
|
81
77
|
*/
|
|
82
|
-
function findVariant(fragment:
|
|
78
|
+
function findVariant(fragment: PreviewFragmentDefinition, variantName: string): FragmentVariant | undefined {
|
|
83
79
|
return fragment.variants?.find(v => v.name === variantName);
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
type PreviewMode = 'centered' | 'full-bleed';
|
|
87
83
|
|
|
88
|
-
function resolvePreviewMode(fragment:
|
|
84
|
+
function resolvePreviewMode(fragment: PreviewFragmentDefinition): PreviewMode {
|
|
89
85
|
const name = fragment.meta.name.toLowerCase();
|
|
90
86
|
const category = (fragment.meta.category || '').toLowerCase();
|
|
91
87
|
|
|
@@ -142,65 +138,36 @@ function VariantRenderer({
|
|
|
142
138
|
onRendered: (width: number, height: number) => void;
|
|
143
139
|
onError: (message: string, stack?: string) => void;
|
|
144
140
|
}) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const { content, isLoading, error } = usePreviewVariantRuntime({
|
|
142
|
+
variant,
|
|
143
|
+
loadedData: props,
|
|
144
|
+
});
|
|
148
145
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
149
146
|
const hasReported = useRef(false);
|
|
147
|
+
const lastReportedError = useRef<string | null>(null);
|
|
150
148
|
|
|
151
149
|
useEffect(() => {
|
|
152
150
|
hasReported.current = false;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!hasLoaders) {
|
|
159
|
-
// No loaders - render immediately
|
|
160
|
-
try {
|
|
161
|
-
const rendered = variant.render({ loadedData: props });
|
|
162
|
-
setContent(rendered);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
165
|
-
setError({ message: errorObj.message, stack: errorObj.stack });
|
|
166
|
-
onError(errorObj.message, errorObj.stack);
|
|
167
|
-
}
|
|
151
|
+
}, [variant, props, mode]);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!error) {
|
|
155
|
+
lastReportedError.current = null;
|
|
168
156
|
return;
|
|
169
157
|
}
|
|
170
158
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
async function executeLoaders() {
|
|
176
|
-
try {
|
|
177
|
-
const results = await Promise.all(variant.loaders!.map(loader => loader()));
|
|
178
|
-
if (cancelled) return;
|
|
179
|
-
|
|
180
|
-
const merged = results.reduce((acc, result) => ({ ...acc, ...result }), {});
|
|
181
|
-
const rendered = variant.render({ loadedData: { ...merged, ...props } });
|
|
182
|
-
|
|
183
|
-
setContent(rendered);
|
|
184
|
-
setIsLoading(false);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
if (cancelled) return;
|
|
187
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
188
|
-
setError({ message: errorObj.message, stack: errorObj.stack });
|
|
189
|
-
setIsLoading(false);
|
|
190
|
-
onError(errorObj.message, errorObj.stack);
|
|
191
|
-
}
|
|
159
|
+
const signature = `${error.message}:${error.stack ?? ''}`;
|
|
160
|
+
if (lastReportedError.current === signature) {
|
|
161
|
+
return;
|
|
192
162
|
}
|
|
193
163
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
cancelled = true;
|
|
198
|
-
};
|
|
199
|
-
}, [variant, props, onError]);
|
|
164
|
+
lastReportedError.current = signature;
|
|
165
|
+
onError(error.message, error.stack);
|
|
166
|
+
}, [error, onError]);
|
|
200
167
|
|
|
201
168
|
// Report rendered size after content renders
|
|
202
169
|
useEffect(() => {
|
|
203
|
-
if (!content || hasReported.current) return;
|
|
170
|
+
if (!content || hasReported.current || isLoading || error) return;
|
|
204
171
|
|
|
205
172
|
// Wait for next frame to ensure DOM has updated
|
|
206
173
|
requestAnimationFrame(() => {
|
|
@@ -210,7 +177,7 @@ function VariantRenderer({
|
|
|
210
177
|
onRendered(rect.width, rect.height);
|
|
211
178
|
}
|
|
212
179
|
});
|
|
213
|
-
}, [content, onRendered]);
|
|
180
|
+
}, [content, error, isLoading, onRendered]);
|
|
214
181
|
|
|
215
182
|
if (isLoading) {
|
|
216
183
|
return <LoadingIndicator />;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { PropDefinition } from '../../core/index.js';
|
|
3
|
-
import {
|
|
3
|
+
import { DataTable, createColumns, Badge, Text, Stack } from '@fragments-sdk/ui';
|
|
4
4
|
import { WarningIcon } from './Icons.js';
|
|
5
5
|
|
|
6
6
|
interface PropsTableProps {
|
|
@@ -99,7 +99,7 @@ export function PropsTable({ props }: PropsTableProps) {
|
|
|
99
99
|
return (
|
|
100
100
|
<section id="props" style={{ scrollMarginTop: '96px' }}>
|
|
101
101
|
<Text as="h2" size="md" weight="semibold" style={{ marginBottom: '16px' }}>Props</Text>
|
|
102
|
-
<
|
|
102
|
+
<DataTable
|
|
103
103
|
columns={columns}
|
|
104
104
|
data={data}
|
|
105
105
|
size="sm"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useWebMCPTools } from '@fragments-sdk/webmcp/react';
|
|
3
|
+
import { useCompiledFragments } from '../hooks/useCompiledFragments.js';
|
|
4
|
+
import { createRuntimeWebMCPTools } from '../webmcp/runtime-tools.js';
|
|
5
|
+
|
|
6
|
+
export function RuntimeToolsRegistrar() {
|
|
7
|
+
const { data } = useCompiledFragments();
|
|
8
|
+
|
|
9
|
+
const tools = useMemo(() => {
|
|
10
|
+
if (!data) return [];
|
|
11
|
+
return createRuntimeWebMCPTools({ compiledData: data });
|
|
12
|
+
}, [data]);
|
|
13
|
+
|
|
14
|
+
useWebMCPTools(tools);
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|