@fragments-sdk/viewer 0.2.1 → 0.2.3
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/package.json +10 -3
- package/src/components/App.tsx +67 -4
- package/src/components/BottomPanel.tsx +31 -1
- package/src/components/ComponentGraph.tsx +1 -1
- package/src/components/EmptyVariantMessage.tsx +1 -1
- package/src/components/ErrorBoundary.tsx +2 -4
- package/src/components/FragmentRenderer.tsx +27 -4
- package/src/components/HeaderSearch.tsx +1 -1
- package/src/components/InteractionsPanel.tsx +5 -5
- package/src/components/LoadErrorMessage.tsx +26 -30
- package/src/components/NoVariantsMessage.tsx +1 -1
- package/src/components/PanelShell.tsx +1 -1
- package/src/components/PerformancePanel.tsx +4 -4
- package/src/components/PreviewArea.tsx +6 -0
- package/src/components/PropsEditor.tsx +33 -17
- package/src/components/SkeletonLoader.tsx +0 -1
- package/src/components/TokenStylePanel.tsx +3 -3
- package/src/components/TopToolbar.tsx +1 -1
- package/src/components/VariantMatrix.tsx +11 -1
- package/src/components/ViewerHeader.tsx +1 -1
- package/src/components/WebMCPDevTools.tsx +2 -2
- package/src/entry.tsx +4 -6
- package/src/hooks/useAppState.ts +1 -1
- package/src/preview-frame-entry.tsx +0 -2
- package/src/shared/DocsHeaderBar.module.scss +174 -0
- package/src/shared/DocsHeaderBar.tsx +149 -14
- package/src/shared/DocsSidebarNav.tsx +3 -3
- package/src/shared/index.ts +4 -1
- package/src/shared/types.ts +29 -3
- package/src/style-utils.ts +1 -1
- package/src/webmcp/runtime-tools.ts +1 -1
- package/tsconfig.json +2 -2
- package/src/assets/fragments-logo.ts +0 -4
- package/src/components/ActionsPanel.tsx +0 -332
- package/src/components/ComponentHeader.tsx +0 -88
- package/src/components/ContractPanel.tsx +0 -241
- package/src/components/FragmentEditor.tsx +0 -525
- package/src/components/HmrStatusIndicator.tsx +0 -61
- package/src/components/LandingPage.tsx +0 -420
- package/src/components/PropsTable.tsx +0 -111
- package/src/components/RelationsSection.tsx +0 -88
- package/src/components/ResizablePanel.tsx +0 -271
- package/src/components/RightSidebar.tsx +0 -102
- package/src/components/ScreenshotButton.tsx +0 -90
- package/src/components/Sidebar.tsx +0 -169
- package/src/components/UsageSection.tsx +0 -95
- package/src/components/VariantTabs.tsx +0 -40
- package/src/components/ViewportSelector.tsx +0 -172
- package/src/components/_future/CreatePage.tsx +0 -835
- package/src/composition-renderer.ts +0 -381
- package/src/constants/index.ts +0 -1
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useHmrStatus.ts +0 -109
- package/src/hooks/useScrollSpy.ts +0 -78
- package/src/intelligence/healthReport.ts +0 -505
- package/src/intelligence/styleDrift.ts +0 -340
- package/src/intelligence/usageScanner.ts +0 -309
- package/src/utils/actionExport.ts +0 -372
- package/src/utils/colorSchemes.ts +0 -201
- package/src/webmcp/index.ts +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/viewer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"license": "FSL-1.1-MIT",
|
|
5
5
|
"description": "Viewer shell — Storybook-like React UI for previewing Fragments components, shared doc layouts, and devtools panels",
|
|
6
6
|
"author": "Conan McNicholl",
|
|
@@ -37,25 +37,32 @@
|
|
|
37
37
|
"*.css"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@babel/parser": "^7.28.6",
|
|
41
|
+
"@babel/types": "^7.28.5",
|
|
40
42
|
"@phosphor-icons/react": "^2.1.10",
|
|
41
43
|
"@hookform/resolvers": "^5.2.2",
|
|
42
44
|
"@monaco-editor/react": "^4.7.0",
|
|
43
45
|
"@tanstack/react-virtual": "^3.13.18",
|
|
44
46
|
"axe-core": "^4.11.1",
|
|
47
|
+
"clsx": "^2.1.1",
|
|
45
48
|
"html2canvas": "^1.4.1",
|
|
46
49
|
"monaco-editor": "^0.55.1",
|
|
47
50
|
"react-colorful": "^5.6.1",
|
|
48
51
|
"react-hook-form": "^7.71.0",
|
|
49
52
|
"react-live": "^4.1.6",
|
|
50
53
|
"shiki": "^3.21.0",
|
|
51
|
-
"
|
|
52
|
-
"@fragments-sdk/
|
|
54
|
+
"zod": "^4.1.11",
|
|
55
|
+
"@fragments-sdk/webmcp": "1.0.1",
|
|
56
|
+
"@fragments-sdk/core": "0.3.0",
|
|
57
|
+
"@fragments-sdk/context": "0.4.4",
|
|
58
|
+
"@fragments-sdk/ui": "0.15.1"
|
|
53
59
|
},
|
|
54
60
|
"peerDependencies": {
|
|
55
61
|
"react": ">=18",
|
|
56
62
|
"react-dom": ">=18"
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|
|
65
|
+
"@types/node": "^22.13.10",
|
|
59
66
|
"@types/react": "^19.0.0",
|
|
60
67
|
"@types/react-dom": "^19.0.0",
|
|
61
68
|
"vite": "^6.0.0",
|
package/src/components/App.tsx
CHANGED
|
@@ -56,12 +56,30 @@ interface AppProps {
|
|
|
56
56
|
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function valuesEqual(a: unknown, b: unknown): boolean {
|
|
60
|
+
if (Object.is(a, b)) return true;
|
|
61
|
+
if (
|
|
62
|
+
a &&
|
|
63
|
+
b &&
|
|
64
|
+
typeof a === "object" &&
|
|
65
|
+
typeof b === "object"
|
|
66
|
+
) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export function App({ fragments }: AppProps) {
|
|
60
77
|
// URL state management
|
|
61
78
|
const {
|
|
62
79
|
state: urlState,
|
|
63
80
|
setComponent: setUrlComponent,
|
|
64
81
|
setVariant: setUrlVariant,
|
|
82
|
+
setProps: setUrlProps,
|
|
65
83
|
setViewSettings: setUrlViewSettings,
|
|
66
84
|
copyUrl,
|
|
67
85
|
} = useUrlState();
|
|
@@ -122,6 +140,8 @@ export function App({ fragments }: AppProps) {
|
|
|
122
140
|
const safeVariantIndex = variantCount > 0 ? Math.min(activeVariantIndex, variantCount - 1) : 0;
|
|
123
141
|
const activeVariant = variants[safeVariantIndex];
|
|
124
142
|
const figmaUrl = activeVariant?.figma || activeFragment?.fragment.meta.figma;
|
|
143
|
+
const propOverrides = urlState.props;
|
|
144
|
+
const hasPropOverrides = Object.keys(propOverrides).length > 0;
|
|
125
145
|
|
|
126
146
|
// Figma integration
|
|
127
147
|
const figmaIntegration = useFigmaIntegration({
|
|
@@ -143,6 +163,35 @@ export function App({ fragments }: AppProps) {
|
|
|
143
163
|
clearActionLogs();
|
|
144
164
|
}, [activeFragmentPath, activeVariantIndex, clearActionLogs]);
|
|
145
165
|
|
|
166
|
+
const activePropValues = useMemo(
|
|
167
|
+
() => ({
|
|
168
|
+
...(activeVariant?.args ?? {}),
|
|
169
|
+
...propOverrides,
|
|
170
|
+
}),
|
|
171
|
+
[activeVariant, propOverrides]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const handlePropValueChange = useCallback(
|
|
175
|
+
(name: string, value: unknown) => {
|
|
176
|
+
const baseValue = activeVariant?.args?.[name];
|
|
177
|
+
const propDefault = activeFragment?.fragment.props?.[name]?.default;
|
|
178
|
+
const compareTarget = baseValue !== undefined ? baseValue : propDefault;
|
|
179
|
+
|
|
180
|
+
const next = { ...propOverrides };
|
|
181
|
+
if (value === undefined || valuesEqual(value, compareTarget)) {
|
|
182
|
+
delete next[name];
|
|
183
|
+
} else {
|
|
184
|
+
next[name] = value;
|
|
185
|
+
}
|
|
186
|
+
setUrlProps(next);
|
|
187
|
+
},
|
|
188
|
+
[activeFragment, activeVariant, propOverrides, setUrlProps]
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const handleResetPropValues = useCallback(() => {
|
|
192
|
+
setUrlProps({});
|
|
193
|
+
}, [setUrlProps]);
|
|
194
|
+
|
|
146
195
|
// Extract rendered styles after component renders
|
|
147
196
|
useEffect(() => {
|
|
148
197
|
if (uiState.showComparison && activeVariant) {
|
|
@@ -336,11 +385,12 @@ export function App({ fragments }: AppProps) {
|
|
|
336
385
|
showHelp: uiActions.toggleShortcutsHelp,
|
|
337
386
|
openSearch: () => uiActions.setCommandPalette(true),
|
|
338
387
|
escape: () => {
|
|
339
|
-
|
|
388
|
+
const activeSearchInput = searchInputRef.current;
|
|
389
|
+
if (document.activeElement === activeSearchInput) {
|
|
340
390
|
if (searchQuery) {
|
|
341
391
|
setSearchQuery("");
|
|
342
392
|
} else {
|
|
343
|
-
|
|
393
|
+
activeSearchInput?.blur();
|
|
344
394
|
}
|
|
345
395
|
return;
|
|
346
396
|
}
|
|
@@ -353,10 +403,17 @@ export function App({ fragments }: AppProps) {
|
|
|
353
403
|
// Render variant with action logging via DOM event capture
|
|
354
404
|
const renderVariantWithProps = useCallback((variant: FragmentVariant | undefined) => {
|
|
355
405
|
if (!variant) return null;
|
|
406
|
+
const applyOverrides =
|
|
407
|
+
hasPropOverrides &&
|
|
408
|
+
activeVariant &&
|
|
409
|
+
variant === activeVariant;
|
|
356
410
|
|
|
357
411
|
return (
|
|
358
412
|
<ActionCapture onAction={useActionsRef.current.logAction}>
|
|
359
|
-
<FragmentRenderer
|
|
413
|
+
<FragmentRenderer
|
|
414
|
+
variant={variant}
|
|
415
|
+
argsOverrides={applyOverrides ? propOverrides : undefined}
|
|
416
|
+
>
|
|
360
417
|
{(content, isLoading, error) => {
|
|
361
418
|
if (isLoading)
|
|
362
419
|
return (
|
|
@@ -385,7 +442,7 @@ export function App({ fragments }: AppProps) {
|
|
|
385
442
|
</FragmentRenderer>
|
|
386
443
|
</ActionCapture>
|
|
387
444
|
);
|
|
388
|
-
}, []);
|
|
445
|
+
}, [activeVariant, hasPropOverrides, propOverrides]);
|
|
389
446
|
|
|
390
447
|
// Check if isolated mode
|
|
391
448
|
const isIsolated = useMemo(() => {
|
|
@@ -502,6 +559,8 @@ export function App({ fragments }: AppProps) {
|
|
|
502
559
|
showComparison={uiState.showComparison}
|
|
503
560
|
figmaUrl={figmaUrl}
|
|
504
561
|
allFigmaUrls={allFigmaUrls}
|
|
562
|
+
activeVariantIndex={safeVariantIndex}
|
|
563
|
+
variantArgsOverrides={hasPropOverrides ? propOverrides : undefined}
|
|
505
564
|
onSelectVariant={(index) => {
|
|
506
565
|
uiActions.setMatrixView(false);
|
|
507
566
|
handleSelectVariant(index);
|
|
@@ -584,6 +643,10 @@ export function App({ fragments }: AppProps) {
|
|
|
584
643
|
const target = fragments.find((s) => s.fragment.meta.name === name);
|
|
585
644
|
if (target) handleSelectFragment(target.path);
|
|
586
645
|
}}
|
|
646
|
+
propValues={activePropValues}
|
|
647
|
+
onChangePropValue={handlePropValueChange}
|
|
648
|
+
onResetPropValues={handleResetPropValues}
|
|
649
|
+
hasPropOverrides={hasPropOverrides}
|
|
587
650
|
previewKey={uiState.previewKey}
|
|
588
651
|
fragmentKey={`${activeFragmentPath}-${safeVariantIndex}`}
|
|
589
652
|
/>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BottomPanel component - the tabbed panel at the bottom of the viewer.
|
|
3
3
|
* Uses the Drawer component from @fragments-sdk/ui as a non-modal bottom sheet.
|
|
4
|
-
* Contains Styles, Accessibility, Interactions, Graph, and Performance tabs.
|
|
4
|
+
* Contains Styles, Controls, Accessibility, Interactions, Graph, and Performance tabs.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { memo, useCallback, useState, useEffect, useRef } from "react";
|
|
@@ -14,6 +14,7 @@ import { AccessibilityPanel } from "./AccessibilityPanel.js";
|
|
|
14
14
|
import { InteractionsPanel } from "./InteractionsPanel.js";
|
|
15
15
|
import { ComponentGraph } from "./ComponentGraph.js";
|
|
16
16
|
import { PerformancePanel } from "./PerformancePanel.js";
|
|
17
|
+
import { PropsEditor } from "./PropsEditor.js";
|
|
17
18
|
import type { ActivePanel } from "../hooks/useAppState.js";
|
|
18
19
|
|
|
19
20
|
// Persist panel height to localStorage
|
|
@@ -62,6 +63,10 @@ interface BottomPanelProps {
|
|
|
62
63
|
|
|
63
64
|
// Navigation
|
|
64
65
|
onNavigateToComponent: (name: string) => void;
|
|
66
|
+
propValues: Record<string, unknown>;
|
|
67
|
+
onChangePropValue: (name: string, value: unknown) => void;
|
|
68
|
+
onResetPropValues: () => void;
|
|
69
|
+
hasPropOverrides: boolean;
|
|
65
70
|
|
|
66
71
|
// Keys
|
|
67
72
|
previewKey: number;
|
|
@@ -84,6 +89,10 @@ export const BottomPanel = memo(function BottomPanel({
|
|
|
84
89
|
onFetchFigma,
|
|
85
90
|
onRefreshRendered,
|
|
86
91
|
onNavigateToComponent,
|
|
92
|
+
propValues,
|
|
93
|
+
onChangePropValue,
|
|
94
|
+
onResetPropValues,
|
|
95
|
+
hasPropOverrides,
|
|
87
96
|
previewKey,
|
|
88
97
|
fragmentKey,
|
|
89
98
|
}: BottomPanelProps) {
|
|
@@ -149,6 +158,8 @@ export const BottomPanel = memo(function BottomPanel({
|
|
|
149
158
|
[handleStylesClick, onPanelChange]
|
|
150
159
|
);
|
|
151
160
|
|
|
161
|
+
const hasControls = Object.keys(fragment.props || {}).length > 0;
|
|
162
|
+
|
|
152
163
|
return (
|
|
153
164
|
<Drawer open={open} onOpenChange={onOpenChange} modal={false}>
|
|
154
165
|
{/* Floating trigger FAB — fixed bottom-right, moves up when drawer is open */}
|
|
@@ -232,6 +243,7 @@ export const BottomPanel = memo(function BottomPanel({
|
|
|
232
243
|
<Tabs value={activePanel} onValueChange={handleTabChange}>
|
|
233
244
|
<Tabs.List variant="pills">
|
|
234
245
|
{figmaUrl && <Tabs.Tab value="styles">Styles</Tabs.Tab>}
|
|
246
|
+
{hasControls && <Tabs.Tab value="controls">Controls</Tabs.Tab>}
|
|
235
247
|
<Tabs.Tab value="accessibility">Accessibility</Tabs.Tab>
|
|
236
248
|
{variant?.hasPlayFunction && (
|
|
237
249
|
<Tabs.Tab value="interactions">Interactions</Tabs.Tab>
|
|
@@ -256,6 +268,24 @@ export const BottomPanel = memo(function BottomPanel({
|
|
|
256
268
|
/>
|
|
257
269
|
)}
|
|
258
270
|
|
|
271
|
+
{activePanel === "controls" && (
|
|
272
|
+
<div style={{ padding: "16px" }}>
|
|
273
|
+
{hasControls ? (
|
|
274
|
+
<PropsEditor
|
|
275
|
+
props={fragment.props}
|
|
276
|
+
values={propValues}
|
|
277
|
+
onChange={onChangePropValue}
|
|
278
|
+
onReset={onResetPropValues}
|
|
279
|
+
hasChanges={hasPropOverrides}
|
|
280
|
+
/>
|
|
281
|
+
) : (
|
|
282
|
+
<div style={{ color: "var(--text-secondary)" }}>
|
|
283
|
+
No documented props available for this component.
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
259
289
|
{activePanel === "accessibility" && (
|
|
260
290
|
<AccessibilityPanel
|
|
261
291
|
cacheKey={fragmentKey}
|
|
@@ -163,7 +163,7 @@ export function ComponentGraph({ fragment, allFragments, onNavigate }: Component
|
|
|
163
163
|
title: "No relationships defined",
|
|
164
164
|
description: (
|
|
165
165
|
<>
|
|
166
|
-
Add a <
|
|
166
|
+
Add a <code style={{ fontSize: '12px', display: 'inline', background: 'var(--bg-secondary)', borderRadius: '4px', padding: '2px 4px' }}>relations</code> array to your fragment definition to visualize component connections.
|
|
167
167
|
</>
|
|
168
168
|
),
|
|
169
169
|
action: (
|
|
@@ -8,7 +8,7 @@ interface EmptyVariantMessageProps {
|
|
|
8
8
|
|
|
9
9
|
export function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
|
|
10
10
|
return (
|
|
11
|
-
<Alert
|
|
11
|
+
<Alert severity="warning">
|
|
12
12
|
<Alert.Body>
|
|
13
13
|
<Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
|
|
14
14
|
<Alert.Content>
|
|
@@ -46,7 +46,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
46
46
|
const { error, errorInfo } = this.state;
|
|
47
47
|
|
|
48
48
|
return (
|
|
49
|
-
<Alert
|
|
49
|
+
<Alert severity="error">
|
|
50
50
|
<Alert.Icon>
|
|
51
51
|
<ErrorIcon style={{ width: '24px', height: '24px' }} />
|
|
52
52
|
</Alert.Icon>
|
|
@@ -71,9 +71,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
71
71
|
</Collapsible.Trigger>
|
|
72
72
|
<Collapsible.Content>
|
|
73
73
|
<div style={{ marginTop: '8px' }}>
|
|
74
|
-
<CodeBlock language="plaintext"
|
|
75
|
-
{error?.stack || errorInfo.componentStack}
|
|
76
|
-
</CodeBlock>
|
|
74
|
+
<CodeBlock language="plaintext" code={error?.stack || errorInfo.componentStack} />
|
|
77
75
|
</div>
|
|
78
76
|
</Collapsible.Content>
|
|
79
77
|
</Collapsible>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useMemo, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
PreviewVariantRuntime,
|
|
4
4
|
type FragmentVariant,
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
interface FragmentRendererProps {
|
|
8
8
|
/** The variant to render */
|
|
9
9
|
variant: FragmentVariant;
|
|
10
|
+
/** Optional args/props overrides to apply when rendering this variant */
|
|
11
|
+
argsOverrides?: Record<string, unknown>;
|
|
10
12
|
/** Children render function - receives rendered content */
|
|
11
13
|
children: (content: ReactNode | null, isLoading: boolean, error: Error | null) => ReactNode;
|
|
12
14
|
}
|
|
@@ -21,10 +23,31 @@ interface FragmentRendererProps {
|
|
|
21
23
|
*
|
|
22
24
|
* If no loaders, renders immediately.
|
|
23
25
|
*/
|
|
24
|
-
export function FragmentRenderer({ variant, children }: FragmentRendererProps) {
|
|
26
|
+
export function FragmentRenderer({ variant, argsOverrides, children }: FragmentRendererProps) {
|
|
27
|
+
const runtimeVariant = useMemo(() => {
|
|
28
|
+
if (!argsOverrides || Object.keys(argsOverrides).length === 0) {
|
|
29
|
+
return variant;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...variant,
|
|
34
|
+
render: ((options) =>
|
|
35
|
+
variant.render({
|
|
36
|
+
...options,
|
|
37
|
+
args: {
|
|
38
|
+
...(variant.args ?? {}),
|
|
39
|
+
...(options?.args ?? {}),
|
|
40
|
+
...argsOverrides,
|
|
41
|
+
},
|
|
42
|
+
})) as FragmentVariant["render"],
|
|
43
|
+
} as FragmentVariant;
|
|
44
|
+
}, [variant, argsOverrides]);
|
|
45
|
+
|
|
25
46
|
return (
|
|
26
|
-
<PreviewVariantRuntime variant={
|
|
27
|
-
{({ content, isLoading, error }) =>
|
|
47
|
+
<PreviewVariantRuntime variant={runtimeVariant}>
|
|
48
|
+
{({ content, isLoading, error }) => (
|
|
49
|
+
<>{children(content as ReactNode | null, isLoading, error)}</>
|
|
50
|
+
)}
|
|
28
51
|
</PreviewVariantRuntime>
|
|
29
52
|
);
|
|
30
53
|
}
|
|
@@ -4,7 +4,7 @@ import { Header, Input } from "@fragments-sdk/ui";
|
|
|
4
4
|
interface HeaderSearchProps {
|
|
5
5
|
value: string;
|
|
6
6
|
onChange: (value: string) => void;
|
|
7
|
-
inputRef: RefObject<HTMLInputElement>;
|
|
7
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
|
|
@@ -432,11 +432,11 @@ export function InteractionsPanel({
|
|
|
432
432
|
title: "No interactions defined",
|
|
433
433
|
description: (
|
|
434
434
|
<>
|
|
435
|
-
This variant doesn't have a play function. Add a <
|
|
435
|
+
This variant doesn't have a play function. Add a <code style={{ fontSize: '12px', display: 'inline', background: 'var(--bg-secondary)', borderRadius: '4px', padding: '2px 4px' }}>play</code> function to your Storybook story to enable interaction testing.
|
|
436
436
|
</>
|
|
437
437
|
),
|
|
438
438
|
action: (
|
|
439
|
-
<CodeBlock language="typescript"
|
|
439
|
+
<CodeBlock language="typescript" code={`export const Default = {
|
|
440
440
|
play: async ({ canvasElement, step }) => {
|
|
441
441
|
const canvas = within(canvasElement);
|
|
442
442
|
|
|
@@ -450,7 +450,7 @@ export function InteractionsPanel({
|
|
|
450
450
|
canvas.getByText('Clicked!')
|
|
451
451
|
).toBeInTheDocument();
|
|
452
452
|
}
|
|
453
|
-
};`}
|
|
453
|
+
};`} />
|
|
454
454
|
),
|
|
455
455
|
} : undefined;
|
|
456
456
|
|
|
@@ -504,7 +504,7 @@ export function InteractionsPanel({
|
|
|
504
504
|
<Button
|
|
505
505
|
onClick={() => runInteractions(debugState.mode === 'debug')}
|
|
506
506
|
disabled={result.status === "running"}
|
|
507
|
-
variant={result.status === "running" || result.status === "paused" ? "outline" : "
|
|
507
|
+
variant={result.status === "running" || result.status === "paused" ? "outline" : "primary"}
|
|
508
508
|
size="sm"
|
|
509
509
|
style={
|
|
510
510
|
result.status === "running" || result.status === "paused"
|
|
@@ -557,7 +557,7 @@ export function InteractionsPanel({
|
|
|
557
557
|
: result.status === 'paused' ? 'warning'
|
|
558
558
|
: 'info';
|
|
559
559
|
return (
|
|
560
|
-
<Alert
|
|
560
|
+
<Alert severity={alertVariant}>
|
|
561
561
|
<Alert.Body>
|
|
562
562
|
<Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
|
|
563
563
|
<Stack direction="row" align="center" gap="sm">
|
|
@@ -33,7 +33,7 @@ export function LoadErrorMessage({ error, componentName }: LoadErrorMessageProps
|
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<Stack align="center" justify="center" style={{ height: "100%", padding: "24px" }}>
|
|
36
|
-
<Alert
|
|
36
|
+
<Alert severity="warning">
|
|
37
37
|
<Alert.Body>
|
|
38
38
|
<Alert.Title>{hasMissingDeps ? "Missing Dependencies" : "Failed to Load"}</Alert.Title>
|
|
39
39
|
<Alert.Content>
|
|
@@ -47,19 +47,17 @@ export function LoadErrorMessage({ error, componentName }: LoadErrorMessageProps
|
|
|
47
47
|
<Text size="xs" weight="semibold" color="secondary">
|
|
48
48
|
Install with:
|
|
49
49
|
</Text>
|
|
50
|
-
<Box
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
>
|
|
62
|
-
{installCmd}
|
|
50
|
+
<Box padding="sm" background="tertiary" rounded="sm">
|
|
51
|
+
<code
|
|
52
|
+
style={{
|
|
53
|
+
display: "block",
|
|
54
|
+
fontFamily: "monospace",
|
|
55
|
+
fontSize: "12px",
|
|
56
|
+
userSelect: "all",
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{installCmd}
|
|
60
|
+
</code>
|
|
63
61
|
</Box>
|
|
64
62
|
<Text size="xs" color="tertiary">
|
|
65
63
|
After installing, restart the dev server.
|
|
@@ -74,22 +72,20 @@ export function LoadErrorMessage({ error, componentName }: LoadErrorMessageProps
|
|
|
74
72
|
<Text size="xs" weight="semibold" color="secondary">
|
|
75
73
|
Error:
|
|
76
74
|
</Text>
|
|
77
|
-
<Box
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
>
|
|
92
|
-
{errorMessage}
|
|
75
|
+
<Box padding="sm" background="tertiary" rounded="sm">
|
|
76
|
+
<pre
|
|
77
|
+
style={{
|
|
78
|
+
fontFamily: "monospace",
|
|
79
|
+
fontSize: "11px",
|
|
80
|
+
whiteSpace: "pre-wrap",
|
|
81
|
+
wordBreak: "break-word",
|
|
82
|
+
margin: 0,
|
|
83
|
+
maxHeight: "200px",
|
|
84
|
+
overflow: "auto",
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{errorMessage}
|
|
88
|
+
</pre>
|
|
93
89
|
</Box>
|
|
94
90
|
</>
|
|
95
91
|
)}
|
|
@@ -25,7 +25,7 @@ export function NoVariantsMessage({ fragment }: NoVariantsMessageProps) {
|
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
27
|
<Stack align="center" justify="center" style={{ height: "100%", padding: "24px" }}>
|
|
28
|
-
<Alert
|
|
28
|
+
<Alert severity="info">
|
|
29
29
|
<Alert.Body>
|
|
30
30
|
<Alert.Title>
|
|
31
31
|
{skippedVariants.length} variant{skippedVariants.length === 1 ? "" : "s"} skipped
|
|
@@ -53,11 +53,11 @@ function formatBytes(bytes: number): string {
|
|
|
53
53
|
return kb < 10 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function tierVariant(tier: string): 'success' | 'warning' | '
|
|
56
|
+
function tierVariant(tier: string): 'success' | 'warning' | 'error' {
|
|
57
57
|
switch (tier) {
|
|
58
58
|
case 'lightweight': return 'success';
|
|
59
59
|
case 'moderate': return 'warning';
|
|
60
|
-
case 'heavy': return '
|
|
60
|
+
case 'heavy': return 'error';
|
|
61
61
|
default: return 'warning';
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -220,7 +220,7 @@ export function PerformancePanel({ componentName }: PerformancePanelProps) {
|
|
|
220
220
|
title: "No performance data",
|
|
221
221
|
description: (
|
|
222
222
|
<>
|
|
223
|
-
Run <
|
|
223
|
+
Run <code style={{ fontSize: '12px', display: 'inline', fontFamily: 'var(--fui-font-mono, monospace)', background: 'var(--bg-secondary)', borderRadius: '4px', padding: '2px 4px' }}>{BRAND.cliCommand} perf</code> to measure bundle sizes, then reload the viewer.
|
|
224
224
|
</>
|
|
225
225
|
),
|
|
226
226
|
} : undefined;
|
|
@@ -233,7 +233,7 @@ export function PerformancePanel({ componentName }: PerformancePanelProps) {
|
|
|
233
233
|
<PanelShell loading={loading} empty={emptyConfig}>
|
|
234
234
|
<Stack gap="md">
|
|
235
235
|
{overBudget && (
|
|
236
|
-
<Alert
|
|
236
|
+
<Alert severity="error">
|
|
237
237
|
<strong>{componentName}</strong> exceeds its performance budget ({budgetPercent}% of allowed size).
|
|
238
238
|
Consider code splitting, lazy loading heavy dependencies, or tree-shaking unused exports.
|
|
239
239
|
</Alert>
|
|
@@ -36,6 +36,8 @@ interface PreviewAreaProps {
|
|
|
36
36
|
showComparison: boolean;
|
|
37
37
|
figmaUrl?: string;
|
|
38
38
|
allFigmaUrls: string[];
|
|
39
|
+
activeVariantIndex?: number;
|
|
40
|
+
variantArgsOverrides?: Record<string, unknown>;
|
|
39
41
|
onSelectVariant: (index: number) => void;
|
|
40
42
|
onRetry: () => void;
|
|
41
43
|
renderContent: () => ReactNode;
|
|
@@ -57,6 +59,8 @@ export function PreviewArea({
|
|
|
57
59
|
showComparison,
|
|
58
60
|
figmaUrl,
|
|
59
61
|
allFigmaUrls,
|
|
62
|
+
activeVariantIndex,
|
|
63
|
+
variantArgsOverrides,
|
|
60
64
|
onSelectVariant,
|
|
61
65
|
onRetry,
|
|
62
66
|
renderContent,
|
|
@@ -73,6 +77,8 @@ export function PreviewArea({
|
|
|
73
77
|
zoom={zoom}
|
|
74
78
|
previewTheme={previewTheme}
|
|
75
79
|
useIframeIsolation={useIframeIsolation}
|
|
80
|
+
activeVariantIndex={activeVariantIndex}
|
|
81
|
+
variantArgsOverrides={variantArgsOverrides}
|
|
76
82
|
onSelectVariant={onSelectVariant}
|
|
77
83
|
/>
|
|
78
84
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { PropDefinition } from '@fragments-sdk/core';
|
|
3
|
-
import { Box, Stack, Text, Button, Badge, Card, Input,
|
|
3
|
+
import { Box, Stack, Text, Button, Badge, Card, Input, Toggle } from "@fragments-sdk/ui";
|
|
4
4
|
import { ControlsIcon, ChevronDownIcon, RefreshIcon } from "./Icons.js";
|
|
5
5
|
|
|
6
6
|
interface PropsEditorProps {
|
|
@@ -242,16 +242,25 @@ function EnumControl({
|
|
|
242
242
|
onChange: (v: string) => void;
|
|
243
243
|
}) {
|
|
244
244
|
return (
|
|
245
|
-
<
|
|
245
|
+
<select
|
|
246
246
|
value={value}
|
|
247
|
-
onChange={(e
|
|
247
|
+
onChange={(e) => onChange(e.target.value)}
|
|
248
|
+
style={{
|
|
249
|
+
minWidth: '140px',
|
|
250
|
+
padding: '6px 8px',
|
|
251
|
+
borderRadius: '6px',
|
|
252
|
+
border: '1px solid var(--border)',
|
|
253
|
+
background: 'var(--bg-primary)',
|
|
254
|
+
color: 'var(--text-primary)',
|
|
255
|
+
fontSize: '12px',
|
|
256
|
+
}}
|
|
248
257
|
>
|
|
249
258
|
{values.map((v) => (
|
|
250
259
|
<option key={v} value={v}>
|
|
251
260
|
{v}
|
|
252
261
|
</option>
|
|
253
262
|
))}
|
|
254
|
-
</
|
|
263
|
+
</select>
|
|
255
264
|
);
|
|
256
265
|
}
|
|
257
266
|
|
|
@@ -260,19 +269,16 @@ function NumberControl({
|
|
|
260
269
|
onChange,
|
|
261
270
|
}: {
|
|
262
271
|
value: number;
|
|
263
|
-
onChange: (v: number) => void;
|
|
272
|
+
onChange: (v: number | undefined) => void;
|
|
264
273
|
}) {
|
|
265
274
|
return (
|
|
266
275
|
<Input
|
|
267
276
|
type="number"
|
|
268
|
-
value={value
|
|
269
|
-
onChange={(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
: (undefined as unknown as number)
|
|
274
|
-
)
|
|
275
|
-
}
|
|
277
|
+
value={value == null ? "" : String(value)}
|
|
278
|
+
onChange={(nextValue) => {
|
|
279
|
+
const parsedValue = nextValue ? Number(nextValue) : undefined;
|
|
280
|
+
onChange(parsedValue === undefined || Number.isNaN(parsedValue) ? undefined : parsedValue);
|
|
281
|
+
}}
|
|
276
282
|
style={{ width: '128px', fontFamily: 'monospace' }}
|
|
277
283
|
/>
|
|
278
284
|
);
|
|
@@ -291,7 +297,7 @@ function StringControl({
|
|
|
291
297
|
<Input
|
|
292
298
|
type="text"
|
|
293
299
|
value={value ?? ""}
|
|
294
|
-
onChange={
|
|
300
|
+
onChange={onChange}
|
|
295
301
|
style={compact
|
|
296
302
|
? { width: '96px', fontSize: '11px', fontFamily: 'monospace' }
|
|
297
303
|
: { width: '100%', maxWidth: '320px', fontFamily: 'monospace' }
|
|
@@ -422,7 +428,7 @@ function ColorControl({
|
|
|
422
428
|
<Input
|
|
423
429
|
type="text"
|
|
424
430
|
value={value ?? ""}
|
|
425
|
-
onChange={
|
|
431
|
+
onChange={onChange}
|
|
426
432
|
placeholder="#000000"
|
|
427
433
|
style={{ width: '96px', fontSize: '11px', fontFamily: 'monospace' }}
|
|
428
434
|
/>
|
|
@@ -463,13 +469,23 @@ function DateControl({
|
|
|
463
469
|
: "";
|
|
464
470
|
|
|
465
471
|
return (
|
|
466
|
-
<
|
|
472
|
+
<input
|
|
467
473
|
type="datetime-local"
|
|
468
474
|
value={inputValue}
|
|
469
|
-
onChange={(e
|
|
475
|
+
onChange={(e) => {
|
|
470
476
|
const date = e.target.value ? new Date(e.target.value).toISOString() : "";
|
|
471
477
|
onChange(date);
|
|
472
478
|
}}
|
|
479
|
+
style={{
|
|
480
|
+
width: '100%',
|
|
481
|
+
maxWidth: '240px',
|
|
482
|
+
padding: '6px 8px',
|
|
483
|
+
borderRadius: '6px',
|
|
484
|
+
border: '1px solid var(--border)',
|
|
485
|
+
background: 'var(--bg-primary)',
|
|
486
|
+
color: 'var(--text-primary)',
|
|
487
|
+
fontSize: '12px',
|
|
488
|
+
}}
|
|
473
489
|
/>
|
|
474
490
|
);
|
|
475
491
|
}
|