@fragments-sdk/viewer 0.2.1 → 0.2.4

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 (60) hide show
  1. package/package.json +10 -3
  2. package/src/components/App.tsx +67 -4
  3. package/src/components/BottomPanel.tsx +31 -1
  4. package/src/components/ComponentGraph.tsx +1 -1
  5. package/src/components/EmptyVariantMessage.tsx +1 -1
  6. package/src/components/ErrorBoundary.tsx +2 -4
  7. package/src/components/FragmentRenderer.tsx +27 -4
  8. package/src/components/HeaderSearch.tsx +1 -1
  9. package/src/components/InteractionsPanel.tsx +5 -5
  10. package/src/components/LoadErrorMessage.tsx +26 -30
  11. package/src/components/NoVariantsMessage.tsx +1 -1
  12. package/src/components/PanelShell.tsx +1 -1
  13. package/src/components/PerformancePanel.tsx +4 -4
  14. package/src/components/PreviewArea.tsx +6 -0
  15. package/src/components/PropsEditor.tsx +33 -17
  16. package/src/components/SkeletonLoader.tsx +0 -1
  17. package/src/components/TokenStylePanel.tsx +3 -3
  18. package/src/components/TopToolbar.tsx +1 -1
  19. package/src/components/VariantMatrix.tsx +11 -1
  20. package/src/components/ViewerHeader.tsx +1 -1
  21. package/src/components/WebMCPDevTools.tsx +2 -2
  22. package/src/entry.tsx +4 -6
  23. package/src/hooks/useAppState.ts +1 -1
  24. package/src/preview-frame-entry.tsx +0 -2
  25. package/src/shared/DocsHeaderBar.module.scss +174 -0
  26. package/src/shared/DocsHeaderBar.tsx +149 -14
  27. package/src/shared/DocsSidebarNav.tsx +3 -3
  28. package/src/shared/index.ts +4 -1
  29. package/src/shared/types.ts +29 -3
  30. package/src/style-utils.ts +1 -1
  31. package/src/webmcp/runtime-tools.ts +1 -1
  32. package/tsconfig.json +2 -2
  33. package/src/assets/fragments-logo.ts +0 -4
  34. package/src/components/ActionsPanel.tsx +0 -332
  35. package/src/components/ComponentHeader.tsx +0 -88
  36. package/src/components/ContractPanel.tsx +0 -241
  37. package/src/components/FragmentEditor.tsx +0 -525
  38. package/src/components/HmrStatusIndicator.tsx +0 -61
  39. package/src/components/LandingPage.tsx +0 -420
  40. package/src/components/PropsTable.tsx +0 -111
  41. package/src/components/RelationsSection.tsx +0 -88
  42. package/src/components/ResizablePanel.tsx +0 -271
  43. package/src/components/RightSidebar.tsx +0 -102
  44. package/src/components/ScreenshotButton.tsx +0 -90
  45. package/src/components/Sidebar.tsx +0 -169
  46. package/src/components/UsageSection.tsx +0 -95
  47. package/src/components/VariantTabs.tsx +0 -40
  48. package/src/components/ViewportSelector.tsx +0 -172
  49. package/src/components/_future/CreatePage.tsx +0 -835
  50. package/src/composition-renderer.ts +0 -381
  51. package/src/constants/index.ts +0 -1
  52. package/src/hooks/index.ts +0 -2
  53. package/src/hooks/useHmrStatus.ts +0 -109
  54. package/src/hooks/useScrollSpy.ts +0 -78
  55. package/src/intelligence/healthReport.ts +0 -505
  56. package/src/intelligence/styleDrift.ts +0 -340
  57. package/src/intelligence/usageScanner.ts +0 -309
  58. package/src/utils/actionExport.ts +0 -372
  59. package/src/utils/colorSchemes.ts +0 -201
  60. 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.1",
3
+ "version": "0.2.4",
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
- "@fragments-sdk/core": "0.2.0",
52
- "@fragments-sdk/ui": "0.14.0"
54
+ "zod": "^4.1.11",
55
+ "@fragments-sdk/ui": "0.16.0",
56
+ "@fragments-sdk/context": "0.5.0",
57
+ "@fragments-sdk/core": "1.0.0",
58
+ "@fragments-sdk/webmcp": "2.0.0"
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",
@@ -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
- if (document.activeElement === searchInputRef.current) {
388
+ const activeSearchInput = searchInputRef.current;
389
+ if (document.activeElement === activeSearchInput) {
340
390
  if (searchQuery) {
341
391
  setSearchQuery("");
342
392
  } else {
343
- searchInputRef.current.blur();
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 variant={variant}>
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 <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>relations</Box> array to your fragment definition to visualize component connections.
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 variant="warning">
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 variant="error">
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 { ReactNode } from "react";
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={variant}>
27
- {({ content, isLoading, error }) => children(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 <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>play</Box> function to your Storybook story to enable interaction testing.
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">{`export const Default = {
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
- };`}</CodeBlock>
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" : "solid"}
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 variant={alertVariant}>
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 variant="warning">
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
- as="code"
52
- padding="sm"
53
- background="tertiary"
54
- rounded="sm"
55
- style={{
56
- display: "block",
57
- fontFamily: "monospace",
58
- fontSize: "12px",
59
- userSelect: "all",
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
- as="pre"
79
- padding="sm"
80
- background="tertiary"
81
- rounded="sm"
82
- style={{
83
- fontFamily: "monospace",
84
- fontSize: "11px",
85
- whiteSpace: "pre-wrap",
86
- wordBreak: "break-word",
87
- margin: 0,
88
- maxHeight: "200px",
89
- overflow: "auto",
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 variant="info">
28
+ <Alert severity="info">
29
29
  <Alert.Body>
30
30
  <Alert.Title>
31
31
  {skippedVariants.length} variant{skippedVariants.length === 1 ? "" : "s"} skipped
@@ -85,7 +85,7 @@ export function PanelShell({
85
85
  if (error) {
86
86
  return (
87
87
  <Box overflow="auto" padding={padding} style={{ flex: 1 }}>
88
- <Alert variant="danger">{error}</Alert>
88
+ <Alert severity="error">{error}</Alert>
89
89
  </Box>
90
90
  );
91
91
  }
@@ -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' | 'danger' {
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 'danger';
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 <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline', fontFamily: 'var(--fui-font-mono, monospace)' }}>{BRAND.cliCommand} perf</Box> to measure bundle sizes, then reload the viewer.
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 variant="danger">
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, Select, Toggle } from "@fragments-sdk/ui";
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
- <Select
245
+ <select
246
246
  value={value}
247
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}
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
- </Select>
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={(e: React.ChangeEvent<HTMLInputElement>) =>
270
- onChange(
271
- e.target.value
272
- ? Number(e.target.value)
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={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
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={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
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
- <Input
472
+ <input
467
473
  type="datetime-local"
468
474
  value={inputValue}
469
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
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
  }