@fragments-sdk/cli 0.5.2 → 0.6.0

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 (118) hide show
  1. package/dist/bin.js +712 -39
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/{chunk-U4GQ2JTD.js → chunk-D35RGPAG.js} +412 -35
  6. package/dist/chunk-D35RGPAG.js.map +1 -0
  7. package/dist/{chunk-XNWDI6UT.js → chunk-F7ITZPDJ.js} +5 -5
  8. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  9. package/dist/{chunk-V7YLRR4C.js → chunk-Q7GOHVOK.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-2H2JAA3U.js → chunk-SSLQXHNX.js} +3 -3
  12. package/dist/{core-DKHB7FYV.js → core-SKRPJQZG.js} +4 -4
  13. package/dist/{generate-KL24VZVD.js → generate-7AF7WRVK.js} +5 -5
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +15 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/{init-NION5S3M.js → init-WKGDPYI4.js} +5 -5
  18. package/dist/mcp-bin.js +8 -220
  19. package/dist/mcp-bin.js.map +1 -1
  20. package/dist/scan-K6JNMCGM.js +12 -0
  21. package/dist/{service-RWUMZ3EW.js → service-F3E4JJM7.js} +5 -5
  22. package/dist/static-viewer-4LQZ5AGA.js +12 -0
  23. package/dist/{test-ECPEXFDN.js → test-CJDNJTPZ.js} +4 -4
  24. package/dist/{tokens-ITADYVPF.js → tokens-JAJABYXP.js} +6 -6
  25. package/dist/viewer-R3Q6WAMJ.js +1822 -0
  26. package/dist/viewer-R3Q6WAMJ.js.map +1 -0
  27. package/package.json +5 -4
  28. package/src/bin.ts +8 -0
  29. package/src/build.ts +104 -13
  30. package/src/cli-commands.ts +18 -0
  31. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  32. package/src/commands/a11y-report.ts +625 -0
  33. package/src/commands/a11y.ts +168 -14
  34. package/src/commands/build.ts +16 -0
  35. package/src/core/auto-props.ts +464 -0
  36. package/src/core/schema.ts +2 -0
  37. package/src/core/types.ts +3 -1
  38. package/src/index.ts +4 -0
  39. package/src/mcp/server.ts +13 -220
  40. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  41. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  42. package/src/theme/contrast.test.ts +331 -0
  43. package/src/theme/contrast.ts +246 -0
  44. package/src/theme/generator.ts +213 -1
  45. package/src/theme/index.ts +16 -0
  46. package/src/theme/types.ts +51 -0
  47. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  48. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  49. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  50. package/src/viewer/components/ActionCapture.tsx +1 -1
  51. package/src/viewer/components/ActionsPanel.tsx +142 -183
  52. package/src/viewer/components/App.tsx +159 -164
  53. package/src/viewer/components/BottomPanel.tsx +40 -80
  54. package/src/viewer/components/CodePanel.tsx +9 -87
  55. package/src/viewer/components/CommandPalette.tsx +117 -74
  56. package/src/viewer/components/ComponentGraph.tsx +143 -126
  57. package/src/viewer/components/ComponentHeader.tsx +46 -43
  58. package/src/viewer/components/ContractPanel.tsx +124 -117
  59. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  60. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  61. package/src/viewer/components/FragmentEditor.tsx +126 -63
  62. package/src/viewer/components/HealthDashboard.tsx +146 -171
  63. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  64. package/src/viewer/components/Icons.tsx +99 -98
  65. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  66. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  67. package/src/viewer/components/IsolatedRender.tsx +12 -6
  68. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  69. package/src/viewer/components/LandingPage.tsx +285 -305
  70. package/src/viewer/components/Layout.tsx +7 -9
  71. package/src/viewer/components/LeftSidebar.tsx +78 -108
  72. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  73. package/src/viewer/components/PreviewArea.tsx +113 -44
  74. package/src/viewer/components/PreviewFrameHost.tsx +6 -5
  75. package/src/viewer/components/PreviewPane.tsx +2 -3
  76. package/src/viewer/components/PreviewToolbar.tsx +61 -104
  77. package/src/viewer/components/PropsEditor.tsx +154 -74
  78. package/src/viewer/components/PropsTable.tsx +95 -82
  79. package/src/viewer/components/RelationsSection.tsx +71 -40
  80. package/src/viewer/components/ResizablePanel.tsx +158 -55
  81. package/src/viewer/components/RightSidebar.tsx +46 -56
  82. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  83. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  84. package/src/viewer/components/StoryRenderer.tsx +4 -11
  85. package/src/viewer/components/Toast.tsx +3 -67
  86. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  87. package/src/viewer/components/UsageSection.tsx +26 -26
  88. package/src/viewer/components/VariantMatrix.tsx +140 -47
  89. package/src/viewer/components/VariantTabs.tsx +24 -68
  90. package/src/viewer/components/ViewportSelector.tsx +106 -110
  91. package/src/viewer/constants/ui.ts +19 -18
  92. package/src/viewer/entry.tsx +8 -3
  93. package/src/viewer/index.ts +3 -6
  94. package/src/viewer/preview-frame.html +21 -5
  95. package/src/viewer/server.ts +7 -16
  96. package/src/viewer/styles/globals.css +4 -4
  97. package/src/viewer/utils/a11y-fixes.ts +53 -30
  98. package/dist/chunk-ICAIQ57V.js.map +0 -1
  99. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  100. package/dist/scan-ESEXV7LF.js +0 -12
  101. package/dist/static-viewer-O37MJ5B6.js +0 -12
  102. package/dist/viewer-YDGFDTK5.js +0 -11104
  103. package/dist/viewer-YDGFDTK5.js.map +0 -1
  104. package/src/viewer/postcss.config.js +0 -6
  105. package/src/viewer/tailwind.config.js +0 -37
  106. /package/dist/{chunk-XNWDI6UT.js.map → chunk-F7ITZPDJ.js.map} +0 -0
  107. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  108. /package/dist/{chunk-V7YLRR4C.js.map → chunk-Q7GOHVOK.js.map} +0 -0
  109. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  110. /package/dist/{chunk-2H2JAA3U.js.map → chunk-SSLQXHNX.js.map} +0 -0
  111. /package/dist/{core-DKHB7FYV.js.map → core-SKRPJQZG.js.map} +0 -0
  112. /package/dist/{generate-KL24VZVD.js.map → generate-7AF7WRVK.js.map} +0 -0
  113. /package/dist/{init-NION5S3M.js.map → init-WKGDPYI4.js.map} +0 -0
  114. /package/dist/{scan-ESEXV7LF.js.map → scan-K6JNMCGM.js.map} +0 -0
  115. /package/dist/{service-RWUMZ3EW.js.map → service-F3E4JJM7.js.map} +0 -0
  116. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-4LQZ5AGA.js.map} +0 -0
  117. /package/dist/{test-ECPEXFDN.js.map → test-CJDNJTPZ.js.map} +0 -0
  118. /package/dist/{tokens-ITADYVPF.js.map → tokens-JAJABYXP.js.map} +0 -0
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { useState, useMemo, useEffect, useCallback, useRef } from "react";
7
7
  import type { SegmentDefinition } from "../../core/index.js";
8
- import clsx from "clsx";
9
8
 
10
9
  // Layout & Navigation
11
10
  import { Layout } from "./Layout.js";
@@ -13,7 +12,7 @@ import { LeftSidebar } from "./LeftSidebar.js";
13
12
  import { VariantTabs } from "./VariantTabs.js";
14
13
  import { CommandPalette } from "./CommandPalette.js";
15
14
  import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp.js";
16
- import { Toast, type ToastMessage } from "./Toast.js";
15
+ import { useToast } from "./Toast.js";
17
16
 
18
17
  // Toolbar
19
18
  import { PreviewToolbar, getBackgroundStyle } from "./PreviewToolbar.js";
@@ -28,6 +27,9 @@ import { HealthDashboard } from "./HealthDashboard.js";
28
27
  import { useAllFigmaUrls } from "./FigmaEmbed.js";
29
28
  import { ActionCapture } from "./ActionCapture.js";
30
29
 
30
+ // Fragments UI
31
+ import { Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert } from "@fragments/ui";
32
+
31
33
  // Icons
32
34
  import { EmptyIcon, ExternalLinkIcon, CameraIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
33
35
 
@@ -78,15 +80,8 @@ export function App({ segments }: AppProps) {
78
80
  // Get resolved theme from ThemeProvider for iframe preview
79
81
  const { resolvedTheme } = useTheme();
80
82
 
81
- // Toast notifications
82
- const [toasts, setToasts] = useState<ToastMessage[]>([]);
83
- const addToast = useCallback((type: ToastMessage['type'], message: string, duration?: number) => {
84
- const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
85
- setToasts(prev => [...prev, { id, type, message, duration }]);
86
- }, []);
87
- const dismissToast = useCallback((id: string) => {
88
- setToasts(prev => prev.filter(t => t.id !== id));
89
- }, []);
83
+ // Toast notifications (via Fragments UI ToastProvider)
84
+ const { toast, info, success } = useToast();
90
85
 
91
86
  // Navigation state
92
87
  const [activeSegmentPath, setActiveSegmentPath] = useState<string | null>(() => {
@@ -161,13 +156,13 @@ export function App({ segments }: AppProps) {
161
156
  const handleUpdate = (data: any) => {
162
157
  if (data?.updates?.length > 0) {
163
158
  const paths = data.updates.map((u: any) => u.path.split('/').pop()).join(', ');
164
- addToast('info', `Updated: ${paths}`, 2000);
159
+ info('HMR Update', `Updated: ${paths}`);
165
160
  }
166
161
  };
167
162
 
168
163
  hot.on('vite:beforeUpdate', handleUpdate);
169
164
  return () => hot.off?.('vite:beforeUpdate', handleUpdate);
170
- }, [addToast]);
165
+ }, [info]);
171
166
 
172
167
  // Navigation handlers
173
168
  const handleSelectSegment = useCallback((path: string) => {
@@ -189,13 +184,13 @@ export function App({ segments }: AppProps) {
189
184
 
190
185
  // Copy link handler
191
186
  const handleCopyLink = useCallback(async () => {
192
- const success = await copyUrl();
193
- if (success) {
187
+ const copied = await copyUrl();
188
+ if (copied) {
194
189
  uiActions.setLinkCopied(true);
195
- addToast('success', 'Link copied to clipboard', 2000);
190
+ success('Copied', 'Link copied to clipboard');
196
191
  setTimeout(() => uiActions.setLinkCopied(false), 2000);
197
192
  }
198
- }, [copyUrl, addToast, uiActions]);
193
+ }, [copyUrl, success, uiActions]);
199
194
 
200
195
  // Sorted segment paths for keyboard navigation
201
196
  const sortedSegmentPaths = useMemo(() => {
@@ -240,7 +235,7 @@ export function App({ segments }: AppProps) {
240
235
  <ActionCapture onAction={useActionsRef.current.logAction}>
241
236
  <StoryRenderer variant={activeVariant}>
242
237
  {(content, isLoading, error) => {
243
- if (isLoading) return <div className="flex items-center justify-center p-8"><LoaderIndicator /></div>;
238
+ if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
244
239
  if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={activeVariant.name} hint="Check the console for the full error stack trace." />;
245
240
  if (content === null || content === undefined) return <EmptyVariantMessage reason="render() returned null or undefined" variantName={activeVariant.name} hint="The variant's render function didn't return any JSX." />;
246
241
  return content;
@@ -262,7 +257,6 @@ export function App({ segments }: AppProps) {
262
257
 
263
258
  return (
264
259
  <>
265
- <Toast messages={toasts} onDismiss={dismissToast} />
266
260
  <KeyboardShortcutsHelp isOpen={uiState.showShortcutsHelp} onClose={() => uiActions.setShortcutsHelp(false)} />
267
261
  <CommandPalette
268
262
  isOpen={uiState.showCommandPalette}
@@ -290,8 +284,8 @@ export function App({ segments }: AppProps) {
290
284
  }
291
285
  >
292
286
  {uiState.showHealthDashboard ? (
293
- <div className="h-full overflow-auto bg-[--bg-primary]">
294
- <div className="max-w-4xl mx-auto py-8 px-6">
287
+ <div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
288
+ <Box padding="lg" style={{ maxWidth: '896px', margin: '0 auto' }}>
295
289
  <HealthDashboard
296
290
  segments={segments}
297
291
  onNavigate={(componentName) => {
@@ -302,12 +296,12 @@ export function App({ segments }: AppProps) {
302
296
  }
303
297
  }}
304
298
  />
305
- </div>
299
+ </Box>
306
300
  </div>
307
301
  ) : activeSegment ? (
308
- <div className={clsx("flex h-full", panelDock === "bottom" ? "flex-col" : "flex-row")}>
302
+ <div style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
309
303
  {/* Main Content Area */}
310
- <div className="flex-1 flex flex-col min-w-0 min-h-0">
304
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
311
305
  {/* Top Toolbar */}
312
306
  <TopToolbar
313
307
  segment={activeSegment}
@@ -335,8 +329,12 @@ export function App({ segments }: AppProps) {
335
329
 
336
330
  {/* Preview Area */}
337
331
  <div
338
- className="flex-1 overflow-auto relative"
339
- style={uiState.showMatrixView ? undefined : getBackgroundStyle(viewSettings.background)}
332
+ style={{
333
+ flex: 1,
334
+ overflow: 'auto',
335
+ position: 'relative',
336
+ ...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
337
+ }}
340
338
  >
341
339
  {activeVariant ? (
342
340
  <PreviewArea
@@ -395,11 +393,13 @@ export function App({ segments }: AppProps) {
395
393
  )}
396
394
  </div>
397
395
  ) : (
398
- <div className="flex flex-col items-center justify-center h-full text-secondary">
399
- <EmptyIcon className="w-12 h-12 mb-4 text-[--text-muted]" />
400
- <p className="text-base font-medium text-primary">No component selected</p>
401
- <p className="text-sm mt-1 text-tertiary">Select a component from the sidebar</p>
402
- </div>
396
+ <EmptyState style={{ height: '100%' }}>
397
+ <EmptyState.Icon>
398
+ <EmptyIcon style={{ width: '48px', height: '48px' }} />
399
+ </EmptyState.Icon>
400
+ <EmptyState.Title>No component selected</EmptyState.Title>
401
+ <EmptyState.Description>Select a component from the sidebar</EmptyState.Description>
402
+ </EmptyState>
403
403
  )}
404
404
  </Layout>
405
405
  </>
@@ -420,88 +420,87 @@ interface TopToolbarProps {
420
420
 
421
421
  function TopToolbar({ segment, variant, viewSettings, uiState, uiActions, figmaUrl, linkCopied, onCopyLink }: TopToolbarProps) {
422
422
  return (
423
- <div className="flex items-center justify-between px-4 py-2 border-b border-[--border] bg-[--bg-secondary] flex-shrink-0">
424
- <div className="flex items-center gap-3">
425
- <h1 className="text-sm font-medium text-primary">{segment.segment.meta.name}</h1>
426
- <span className="text-xs text-tertiary">{segment.segment.meta.category}</span>
427
- </div>
428
- <div className="flex items-center gap-2">
423
+ <Stack direction="row" align="center" justify="between" style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-secondary)', flexShrink: 0 }}>
424
+ <Stack direction="row" align="center" gap="sm">
425
+ <Text weight="medium" size="sm">{segment.segment.meta.name}</Text>
426
+ <Text size="xs" color="tertiary">{segment.segment.meta.category}</Text>
427
+ </Stack>
428
+ <Stack direction="row" align="center" gap="sm">
429
429
  <PreviewToolbar
430
430
  zoom={viewSettings.zoom}
431
431
  background={viewSettings.background}
432
432
  onZoomChange={viewSettings.setZoom}
433
433
  onBackgroundChange={viewSettings.setBackground}
434
434
  />
435
- <div className="w-px h-4 bg-[--border]" />
435
+ <Separator orientation="vertical" style={{ height: '16px' }} />
436
436
  <ViewportSelector
437
437
  viewport={viewSettings.viewport}
438
438
  customSize={viewSettings.customSize}
439
439
  onViewportChange={viewSettings.setViewport}
440
440
  onCustomSizeChange={viewSettings.setCustomSize}
441
441
  />
442
- <div className="w-px h-4 bg-[--border]" />
442
+ <Separator orientation="vertical" style={{ height: '16px' }} />
443
443
 
444
444
  {figmaUrl && (
445
445
  <>
446
- <button
447
- onClick={uiActions.toggleComparison}
448
- className={clsx(
449
- "p-1.5 rounded transition-colors",
450
- uiState.showComparison
451
- ? "text-[--color-accent] bg-[--bg-hover]"
452
- : "text-tertiary hover:text-primary hover:bg-[--bg-hover]"
453
- )}
454
- title={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}
455
- >
456
- <CompareIcon className="w-4 h-4" />
457
- </button>
458
- <button
459
- onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
460
- className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
461
- title="View in Figma"
462
- >
463
- <FigmaIcon className="w-4 h-4" />
464
- </button>
465
- <div className="w-px h-4 bg-[--border]" />
446
+ <Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
447
+ <Button
448
+ onClick={uiActions.toggleComparison}
449
+ variant="ghost"
450
+ size="sm"
451
+ style={uiState.showComparison ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
452
+ >
453
+ <CompareIcon style={{ width: '16px', height: '16px' }} />
454
+ </Button>
455
+ </Tooltip>
456
+ <Tooltip content="View in Figma">
457
+ <Button
458
+ onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
459
+ variant="ghost"
460
+ size="sm"
461
+ >
462
+ <FigmaIcon style={{ width: '16px', height: '16px' }} />
463
+ </Button>
464
+ </Tooltip>
465
+ <Separator orientation="vertical" style={{ height: '16px' }} />
466
466
  </>
467
467
  )}
468
468
 
469
469
  {variant && (
470
470
  <>
471
- <button
472
- onClick={() => {
473
- const url = new URL(window.location.href);
474
- // Clear the hash to avoid malformed URLs
475
- url.hash = '';
476
- url.searchParams.set('isolated', 'true');
477
- url.searchParams.set('component', segment.segment.meta.name);
478
- url.searchParams.set('variant', variant.name);
479
- if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
480
- if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
481
- window.open(url.toString(), '_blank', 'noopener,noreferrer');
482
- }}
483
- className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
484
- title="Open in new window"
485
- >
486
- <ExternalLinkIcon className="w-4 h-4" />
487
- </button>
471
+ <Tooltip content="Open in new window">
472
+ <Button
473
+ onClick={() => {
474
+ const url = new URL(window.location.href);
475
+ url.hash = '';
476
+ url.searchParams.set('isolated', 'true');
477
+ url.searchParams.set('component', segment.segment.meta.name);
478
+ url.searchParams.set('variant', variant.name);
479
+ if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
480
+ if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
481
+ window.open(url.toString(), '_blank', 'noopener,noreferrer');
482
+ }}
483
+ variant="ghost"
484
+ size="sm"
485
+ >
486
+ <ExternalLinkIcon style={{ width: '16px', height: '16px' }} />
487
+ </Button>
488
+ </Tooltip>
488
489
  <ScreenshotButton componentName={segment.segment.meta.name} variantName={variant.name} />
489
- <button
490
- onClick={onCopyLink}
491
- className={clsx(
492
- "p-1.5 rounded transition-colors",
493
- linkCopied
494
- ? "text-green-600 bg-green-100 dark:bg-green-900/30"
495
- : "text-tertiary hover:text-primary hover:bg-[--bg-hover]"
496
- )}
497
- title="Copy link to share"
498
- >
499
- {linkCopied ? <CheckIcon className="w-4 h-4" /> : <LinkIcon className="w-4 h-4" />}
500
- </button>
490
+ <Tooltip content="Copy link to share">
491
+ <Button
492
+ onClick={onCopyLink}
493
+ variant="ghost"
494
+ size="sm"
495
+ style={linkCopied ? { color: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.1)' } : {}}
496
+ >
497
+ {linkCopied ? <CheckIcon style={{ width: '16px', height: '16px' }} /> : <LinkIcon style={{ width: '16px', height: '16px' }} />}
498
+ </Button>
499
+ </Tooltip>
501
500
  </>
502
501
  )}
503
- </div>
504
- </div>
502
+ </Stack>
503
+ </Stack>
505
504
  );
506
505
  }
507
506
 
@@ -518,43 +517,37 @@ interface VariantTabsBarProps {
518
517
 
519
518
  function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showMultiViewport, onToggleMatrix, onToggleMultiViewport }: VariantTabsBarProps) {
520
519
  return (
521
- <div className="px-4 py-2 border-b border-[--border] bg-[--bg-primary] flex-shrink-0 flex items-center justify-between">
520
+ <Stack direction="row" align="center" justify="between" style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
522
521
  {!showMatrixView ? (
523
522
  <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
524
523
  ) : (
525
- <div className="text-sm text-secondary">Showing all {variants.length} variants</div>
524
+ <Text size="sm" color="secondary">Showing all {variants.length} variants</Text>
526
525
  )}
527
- <div className="flex items-center gap-2 ml-4">
526
+ <Stack direction="row" align="center" gap="sm" style={{ marginLeft: '16px' }}>
528
527
  {variants.length > 1 && (
529
- <button
528
+ <Button
530
529
  onClick={onToggleMatrix}
531
- className={clsx(
532
- "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors",
533
- showMatrixView
534
- ? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300"
535
- : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
536
- )}
530
+ variant="ghost"
531
+ size="sm"
537
532
  title={showMatrixView ? "Show single variant" : "Show all variants in grid"}
533
+ style={showMatrixView ? { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: 'var(--color-accent)' } : {}}
538
534
  >
539
- <GridIcon className="w-4 h-4" />
535
+ <GridIcon style={{ width: '16px', height: '16px' }} />
540
536
  {showMatrixView ? "Exit Matrix" : "Matrix"}
541
- </button>
537
+ </Button>
542
538
  )}
543
- <button
539
+ <Button
544
540
  onClick={onToggleMultiViewport}
545
- className={clsx(
546
- "flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors",
547
- showMultiViewport
548
- ? "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300"
549
- : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
550
- )}
541
+ variant="ghost"
542
+ size="sm"
551
543
  title={showMultiViewport ? "Exit multi-viewport" : "Show at multiple screen sizes"}
544
+ style={showMultiViewport ? { backgroundColor: 'rgba(34, 197, 94, 0.1)', color: '#16a34a' } : {}}
552
545
  >
553
- <DevicesIcon className="w-4 h-4" />
546
+ <DevicesIcon style={{ width: '16px', height: '16px' }} />
554
547
  {showMultiViewport ? "Exit Responsive" : "Responsive"}
555
- </button>
556
- </div>
557
- </div>
548
+ </Button>
549
+ </Stack>
550
+ </Stack>
558
551
  );
559
552
  }
560
553
 
@@ -567,34 +560,39 @@ function NoVariantsMessage({ segment }: NoVariantsMessageProps) {
567
560
  const skippedVariants = (segment?._generated as any)?.skippedVariants;
568
561
 
569
562
  if (!skippedVariants || skippedVariants.length === 0) {
570
- return <div className="flex items-center justify-center h-full text-secondary text-sm">No variants defined</div>;
563
+ return (
564
+ <EmptyState style={{ height: '100%' }}>
565
+ <EmptyState.Description>No variants defined</EmptyState.Description>
566
+ </EmptyState>
567
+ );
571
568
  }
572
569
 
573
570
  return (
574
- <div className="flex items-center justify-center h-full p-6">
575
- <div className="p-6 bg-sky-50 dark:bg-sky-950 border border-sky-300 dark:border-sky-700 rounded-lg max-w-lg">
576
- <div className="flex items-start gap-3">
577
- <div className="flex-shrink-0 w-8 h-8 rounded-full bg-sky-200 dark:bg-sky-800 flex items-center justify-center">
578
- <span className="text-sky-700 dark:text-sky-200 text-lg">ℹ</span>
579
- </div>
580
- <div className="flex-1 min-w-0">
581
- <h3 className="text-sm font-semibold text-sky-900 dark:text-sky-100">
582
- {skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
583
- </h3>
584
- <p className="mt-1 text-xs text-sky-800 dark:text-sky-200">
585
- These variants couldn't be rendered because they use syntax the parser doesn't support yet:
586
- </p>
587
- <ul className="mt-2 space-y-1">
588
- {skippedVariants.map((sv: any, i: number) => (
589
- <li key={i} className="text-xs text-sky-800 dark:text-sky-200">
590
- <span className="font-semibold">{sv.name}:</span>{' '}
591
- <span className="text-sky-700 dark:text-sky-300">{sv.reason}</span>
592
- </li>
593
- ))}
594
- </ul>
595
- </div>
596
- </div>
597
- </div>
571
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
572
+ <Alert variant="info">
573
+ <Alert.Body>
574
+ <Alert.Title>
575
+ {skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
576
+ </Alert.Title>
577
+ <Alert.Content>
578
+ <Stack direction="column" gap="sm">
579
+ <Text size="xs" color="secondary">
580
+ These variants couldn't be rendered because they use syntax the parser doesn't support yet:
581
+ </Text>
582
+ <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
583
+ {skippedVariants.map((sv: any, i: number) => (
584
+ <li key={i}>
585
+ <Text size="xs" color="secondary">
586
+ <Text as="span" size="xs" weight="semibold">{sv.name}:</Text>{' '}
587
+ <Text as="span" size="xs" color="tertiary">{sv.reason}</Text>
588
+ </Text>
589
+ </li>
590
+ ))}
591
+ </ul>
592
+ </Stack>
593
+ </Alert.Content>
594
+ </Alert.Body>
595
+ </Alert>
598
596
  </div>
599
597
  );
600
598
  }
@@ -608,31 +606,28 @@ interface EmptyVariantMessageProps {
608
606
 
609
607
  function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
610
608
  return (
611
- <div className="p-6 bg-amber-50 dark:bg-amber-950 border border-amber-300 dark:border-amber-700 rounded-lg max-w-md">
612
- <div className="flex items-start gap-3">
613
- <div className="flex-shrink-0 w-8 h-8 rounded-full bg-amber-200 dark:bg-amber-800 flex items-center justify-center">
614
- <span className="text-amber-700 dark:text-amber-200 text-lg">⚠</span>
615
- </div>
616
- <div className="flex-1 min-w-0">
617
- <h3 className="text-sm font-semibold text-amber-900 dark:text-amber-100">
618
- Variant "{variantName}" rendered empty
619
- </h3>
620
- <p className="mt-1 text-xs text-amber-800 dark:text-amber-200">{reason}</p>
621
- {hint && (
622
- <p className="mt-2 text-xs text-amber-700 dark:text-amber-300">
623
- <strong>Tip:</strong> {hint}
624
- </p>
625
- )}
626
- <div className="mt-3 text-xs text-amber-700 dark:text-amber-300">
627
- <strong>Common causes:</strong>
628
- <ul className="mt-1 ml-4 list-disc space-y-0.5">
629
- <li>Component requires props that weren't provided</li>
630
- <li>Component renders conditionally and conditions aren't met</li>
631
- <li>Story args reference variables that don't exist in this context</li>
632
- </ul>
633
- </div>
634
- </div>
635
- </div>
636
- </div>
609
+ <Alert variant="warning">
610
+ <Alert.Body>
611
+ <Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
612
+ <Alert.Content>
613
+ <Stack direction="column" gap="sm">
614
+ <Text size="xs" color="secondary">{reason}</Text>
615
+ {hint && (
616
+ <Text size="xs" color="tertiary">
617
+ <Text as="span" size="xs" weight="semibold">Tip:</Text> {hint}
618
+ </Text>
619
+ )}
620
+ <div>
621
+ <Text size="xs" color="tertiary" weight="semibold">Common causes:</Text>
622
+ <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
623
+ <li><Text size="xs" color="secondary">Component requires props that weren't provided</Text></li>
624
+ <li><Text size="xs" color="secondary">Component renders conditionally and conditions aren't met</Text></li>
625
+ <li><Text size="xs" color="secondary">Story args reference variables that don't exist in this context</Text></li>
626
+ </ul>
627
+ </div>
628
+ </Stack>
629
+ </Alert.Content>
630
+ </Alert.Body>
631
+ </Alert>
637
632
  );
638
633
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { memo, useCallback } from 'react';
7
7
  import type { SegmentDefinition, SegmentVariant } from '../../core/index.js';
8
- import clsx from 'clsx';
8
+ import { Tabs, Badge } from '@fragments/ui';
9
9
  import { ResizablePanel } from './ResizablePanel.js';
10
10
  import { CodePanel } from './CodePanel.js';
11
11
  import { TokenStylePanel } from './TokenStylePanel.js';
@@ -48,35 +48,6 @@ interface BottomPanelProps {
48
48
  segmentKey: string;
49
49
  }
50
50
 
51
- // Tab button component
52
- interface TabButtonProps {
53
- active: boolean;
54
- onClick: () => void;
55
- children: React.ReactNode;
56
- badge?: number;
57
- }
58
-
59
- const TabButton = memo(function TabButton({ active, onClick, children, badge }: TabButtonProps) {
60
- return (
61
- <button
62
- onClick={onClick}
63
- className={clsx(
64
- "px-3 py-1 text-xs font-medium rounded relative",
65
- active
66
- ? "text-primary bg-[--bg-hover]"
67
- : "text-tertiary hover:text-secondary"
68
- )}
69
- >
70
- {children}
71
- {badge !== undefined && badge > 0 && (
72
- <span className="absolute -top-1 -right-1 w-4 h-4 text-[10px] bg-blue-500 text-white rounded-full flex items-center justify-center">
73
- {badge > 99 ? '99+' : badge}
74
- </span>
75
- )}
76
- </button>
77
- );
78
- });
79
-
80
51
  export const BottomPanel = memo(function BottomPanel({
81
52
  segment,
82
53
  variant,
@@ -102,63 +73,52 @@ export const BottomPanel = memo(function BottomPanel({
102
73
  setTimeout(onRefreshRendered, 100);
103
74
  }, [onPanelChange, onFetchFigma, onRefreshRendered]);
104
75
 
76
+ // Build tab change handler that also triggers side effects
77
+ const handleTabChange = useCallback((value: string | number) => {
78
+ const panel = String(value);
79
+ if (panel === 'styles') {
80
+ handleStylesClick();
81
+ } else {
82
+ onPanelChange(panel as ActivePanel);
83
+ }
84
+ }, [handleStylesClick, onPanelChange]);
85
+
105
86
  return (
106
87
  <ResizablePanel
107
88
  visible={true}
108
89
  header={
109
- <div className="flex items-center gap-1">
110
- <TabButton
111
- active={activePanel === 'code'}
112
- onClick={() => onPanelChange('code')}
113
- >
114
- Code
115
- </TabButton>
116
- {figmaUrl && (
117
- <TabButton
118
- active={activePanel === 'styles'}
119
- onClick={handleStylesClick}
120
- >
121
- Styles
122
- </TabButton>
123
- )}
124
- <TabButton
125
- active={activePanel === 'accessibility'}
126
- onClick={() => onPanelChange('accessibility')}
127
- >
128
- Accessibility
129
- </TabButton>
130
- {variant?.hasPlayFunction && (
131
- <TabButton
132
- active={activePanel === 'interactions'}
133
- onClick={() => onPanelChange('interactions')}
134
- >
135
- Interactions
136
- </TabButton>
137
- )}
138
- <TabButton
139
- active={activePanel === 'actions'}
140
- onClick={() => onPanelChange('actions')}
141
- badge={actionLogs.length}
142
- >
143
- Actions
144
- </TabButton>
145
- <TabButton
146
- active={activePanel === 'graph'}
147
- onClick={() => onPanelChange('graph')}
148
- >
149
- Graph
150
- </TabButton>
151
- <TabButton
152
- active={activePanel === 'contract'}
153
- onClick={() => onPanelChange('contract')}
154
- >
155
- Contract
156
- </TabButton>
157
- </div>
90
+ <Tabs value={activePanel} onValueChange={handleTabChange}>
91
+ <Tabs.List variant="pills">
92
+ <Tabs.Tab value="code">Code</Tabs.Tab>
93
+ {figmaUrl && <Tabs.Tab value="styles">Styles</Tabs.Tab>}
94
+ <Tabs.Tab value="accessibility">Accessibility</Tabs.Tab>
95
+ {variant?.hasPlayFunction && <Tabs.Tab value="interactions">Interactions</Tabs.Tab>}
96
+ <Tabs.Tab value="actions">
97
+ <span style={{ position: 'relative' }}>
98
+ Actions
99
+ {actionLogs.length > 0 && (
100
+ <Badge
101
+ variant="info"
102
+ size="sm"
103
+ style={{
104
+ position: 'absolute',
105
+ top: '-8px',
106
+ right: '-16px',
107
+ }}
108
+ >
109
+ {actionLogs.length > 99 ? '99+' : actionLogs.length}
110
+ </Badge>
111
+ )}
112
+ </span>
113
+ </Tabs.Tab>
114
+ <Tabs.Tab value="graph">Graph</Tabs.Tab>
115
+ <Tabs.Tab value="contract">Contract</Tabs.Tab>
116
+ </Tabs.List>
117
+ </Tabs>
158
118
  }
159
119
  >
160
120
  {activePanel === 'code' && (
161
- <div className="p-4">
121
+ <div style={{ padding: '16px' }}>
162
122
  <CodePanel
163
123
  variant={variant}
164
124
  componentName={segment.meta.name}