@fragments-sdk/cli 0.5.2 → 0.7.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 (124) hide show
  1. package/dist/bin.js +996 -79
  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-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -3,9 +3,8 @@
3
3
  * Refactored for better performance and maintainability.
4
4
  */
5
5
 
6
- import { useState, useMemo, useEffect, useCallback, useRef } from "react";
7
- import type { SegmentDefinition } from "../../core/index.js";
8
- import clsx from "clsx";
6
+ import { useState, useMemo, useEffect, useCallback, useRef, type RefObject } from "react";
7
+ import { BRAND, type SegmentDefinition } from "../../core/index.js";
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,8 +27,11 @@ 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 { Header, Stack, Text, Separator, Tooltip, Button, EmptyState, Box, Alert, ScrollArea, Input } from "@fragments/ui";
32
+
31
33
  // Icons
32
- import { EmptyIcon, ExternalLinkIcon, CameraIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
34
+ import { EmptyIcon, ExternalLinkIcon, FigmaIcon, CompareIcon, CheckIcon, LinkIcon, GridIcon, DevicesIcon } from "./Icons.js";
33
35
 
34
36
  // Hooks
35
37
  import { useAppState } from "../hooks/useAppState.js";
@@ -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 { info, success } = useToast();
90
85
 
91
86
  // Navigation state
92
87
  const [activeSegmentPath, setActiveSegmentPath] = useState<string | null>(() => {
@@ -104,6 +99,8 @@ export function App({ segments }: AppProps) {
104
99
  }
105
100
  return 0;
106
101
  });
102
+ const [searchQuery, setSearchQuery] = useState('');
103
+ const searchInputRef = useRef<HTMLInputElement>(null);
107
104
 
108
105
  // Derived values
109
106
  const activeSegment = useMemo(
@@ -161,13 +158,13 @@ export function App({ segments }: AppProps) {
161
158
  const handleUpdate = (data: any) => {
162
159
  if (data?.updates?.length > 0) {
163
160
  const paths = data.updates.map((u: any) => u.path.split('/').pop()).join(', ');
164
- addToast('info', `Updated: ${paths}`, 2000);
161
+ info('HMR Update', `Updated: ${paths}`);
165
162
  }
166
163
  };
167
164
 
168
165
  hot.on('vite:beforeUpdate', handleUpdate);
169
166
  return () => hot.off?.('vite:beforeUpdate', handleUpdate);
170
- }, [addToast]);
167
+ }, [info]);
171
168
 
172
169
  // Navigation handlers
173
170
  const handleSelectSegment = useCallback((path: string) => {
@@ -189,13 +186,18 @@ export function App({ segments }: AppProps) {
189
186
 
190
187
  // Copy link handler
191
188
  const handleCopyLink = useCallback(async () => {
192
- const success = await copyUrl();
193
- if (success) {
189
+ const copied = await copyUrl();
190
+ if (copied) {
194
191
  uiActions.setLinkCopied(true);
195
- addToast('success', 'Link copied to clipboard', 2000);
192
+ success('Copied', 'Link copied to clipboard');
196
193
  setTimeout(() => uiActions.setLinkCopied(false), 2000);
197
194
  }
198
- }, [copyUrl, addToast, uiActions]);
195
+ }, [copyUrl, success, uiActions]);
196
+
197
+ const focusSearchInput = useCallback(() => {
198
+ searchInputRef.current?.focus();
199
+ searchInputRef.current?.select();
200
+ }, []);
199
201
 
200
202
  // Sorted segment paths for keyboard navigation
201
203
  const sortedSegmentPaths = useMemo(() => {
@@ -226,8 +228,18 @@ export function App({ segments }: AppProps) {
226
228
  togglePanel: uiActions.togglePanel,
227
229
  copyLink: handleCopyLink,
228
230
  showHelp: uiActions.toggleShortcutsHelp,
229
- openSearch: () => uiActions.setCommandPalette(true),
230
- escape: uiActions.closeAllModals,
231
+ openSearch: focusSearchInput,
232
+ escape: () => {
233
+ if (document.activeElement === searchInputRef.current) {
234
+ if (searchQuery) {
235
+ setSearchQuery('');
236
+ } else {
237
+ searchInputRef.current.blur();
238
+ }
239
+ return;
240
+ }
241
+ uiActions.closeAllModals();
242
+ },
231
243
  },
232
244
  { enabled: !uiState.showShortcutsHelp, variantCount }
233
245
  );
@@ -240,7 +252,7 @@ export function App({ segments }: AppProps) {
240
252
  <ActionCapture onAction={useActionsRef.current.logAction}>
241
253
  <StoryRenderer variant={activeVariant}>
242
254
  {(content, isLoading, error) => {
243
- if (isLoading) return <div className="flex items-center justify-center p-8"><LoaderIndicator /></div>;
255
+ if (isLoading) return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}><LoaderIndicator /></div>;
244
256
  if (error) return <EmptyVariantMessage reason={`Error: ${error.message}`} variantName={activeVariant.name} hint="Check the console for the full error stack trace." />;
245
257
  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
258
  return content;
@@ -262,7 +274,6 @@ export function App({ segments }: AppProps) {
262
274
 
263
275
  return (
264
276
  <>
265
- <Toast messages={toasts} onDismiss={dismissToast} />
266
277
  <KeyboardShortcutsHelp isOpen={uiState.showShortcutsHelp} onClose={() => uiActions.setShortcutsHelp(false)} />
267
278
  <CommandPalette
268
279
  isOpen={uiState.showCommandPalette}
@@ -276,10 +287,35 @@ export function App({ segments }: AppProps) {
276
287
  />
277
288
 
278
289
  <Layout
290
+ header={
291
+ activeSegment && !uiState.showHealthDashboard ? (
292
+ <TopToolbar
293
+ segment={activeSegment}
294
+ variant={activeVariant}
295
+ viewSettings={viewSettings}
296
+ uiState={uiState}
297
+ uiActions={uiActions}
298
+ figmaUrl={figmaUrl}
299
+ linkCopied={uiState.linkCopied}
300
+ onCopyLink={handleCopyLink}
301
+ searchQuery={searchQuery}
302
+ onSearchChange={setSearchQuery}
303
+ searchInputRef={searchInputRef}
304
+ />
305
+ ) : (
306
+ <ViewerHeader
307
+ showHealth={uiState.showHealthDashboard}
308
+ searchQuery={searchQuery}
309
+ onSearchChange={setSearchQuery}
310
+ searchInputRef={searchInputRef}
311
+ />
312
+ )
313
+ }
279
314
  leftSidebar={
280
315
  <LeftSidebar
281
316
  segments={segments}
282
317
  activeSegment={uiState.showHealthDashboard ? null : activeSegmentPath}
318
+ searchQuery={searchQuery}
283
319
  onSelect={handleSelectSegment}
284
320
  showHealth={uiState.showHealthDashboard}
285
321
  onHealthClick={() => {
@@ -290,8 +326,8 @@ export function App({ segments }: AppProps) {
290
326
  }
291
327
  >
292
328
  {uiState.showHealthDashboard ? (
293
- <div className="h-full overflow-auto bg-[--bg-primary]">
294
- <div className="max-w-4xl mx-auto py-8 px-6">
329
+ <div style={{ height: '100%', overflow: 'auto', backgroundColor: 'var(--bg-primary)' }}>
330
+ <Box padding="lg" style={{ maxWidth: '896px', margin: '0 auto' }}>
295
331
  <HealthDashboard
296
332
  segments={segments}
297
333
  onNavigate={(componentName) => {
@@ -302,24 +338,12 @@ export function App({ segments }: AppProps) {
302
338
  }
303
339
  }}
304
340
  />
305
- </div>
341
+ </Box>
306
342
  </div>
307
343
  ) : activeSegment ? (
308
- <div className={clsx("flex h-full", panelDock === "bottom" ? "flex-col" : "flex-row")}>
344
+ <div style={{ display: 'flex', height: '100%', flexDirection: panelDock === "bottom" ? 'column' : 'row' }}>
309
345
  {/* Main Content Area */}
310
- <div className="flex-1 flex flex-col min-w-0 min-h-0">
311
- {/* Top Toolbar */}
312
- <TopToolbar
313
- segment={activeSegment}
314
- variant={activeVariant}
315
- viewSettings={viewSettings}
316
- uiState={uiState}
317
- uiActions={uiActions}
318
- figmaUrl={figmaUrl}
319
- linkCopied={uiState.linkCopied}
320
- onCopyLink={handleCopyLink}
321
- />
322
-
346
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
323
347
  {/* Variant Tabs */}
324
348
  {activeSegment.segment.variants && activeSegment.segment.variants.length > 0 && (
325
349
  <VariantTabsBar
@@ -335,8 +359,12 @@ export function App({ segments }: AppProps) {
335
359
 
336
360
  {/* Preview Area */}
337
361
  <div
338
- className="flex-1 overflow-auto relative"
339
- style={uiState.showMatrixView ? undefined : getBackgroundStyle(viewSettings.background)}
362
+ style={{
363
+ flex: 1,
364
+ overflow: 'auto',
365
+ position: 'relative',
366
+ ...(uiState.showMatrixView ? {} : getBackgroundStyle(viewSettings.background)),
367
+ }}
340
368
  >
341
369
  {activeVariant ? (
342
370
  <PreviewArea
@@ -395,11 +423,13 @@ export function App({ segments }: AppProps) {
395
423
  )}
396
424
  </div>
397
425
  ) : (
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>
426
+ <EmptyState style={{ height: '100%' }}>
427
+ <EmptyState.Icon>
428
+ <EmptyIcon style={{ width: '48px', height: '48px' }} />
429
+ </EmptyState.Icon>
430
+ <EmptyState.Title>No component selected</EmptyState.Title>
431
+ <EmptyState.Description>Select a component from the sidebar</EmptyState.Description>
432
+ </EmptyState>
403
433
  )}
404
434
  </Layout>
405
435
  </>
@@ -416,92 +446,157 @@ interface TopToolbarProps {
416
446
  figmaUrl?: string;
417
447
  linkCopied: boolean;
418
448
  onCopyLink: () => void;
449
+ searchQuery: string;
450
+ onSearchChange: (value: string) => void;
451
+ searchInputRef: RefObject<HTMLInputElement>;
452
+ }
453
+
454
+ interface ViewerHeaderProps {
455
+ showHealth: boolean;
456
+ searchQuery: string;
457
+ onSearchChange: (value: string) => void;
458
+ searchInputRef: RefObject<HTMLInputElement>;
459
+ }
460
+
461
+ interface HeaderSearchProps {
462
+ value: string;
463
+ onChange: (value: string) => void;
464
+ inputRef: RefObject<HTMLInputElement>;
465
+ }
466
+
467
+ function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
468
+ return (
469
+ <Header.Search expandable>
470
+ <Input
471
+ ref={inputRef}
472
+ value={value}
473
+ onChange={onChange}
474
+ placeholder="Search components"
475
+ aria-label="Search components"
476
+ size="sm"
477
+ shortcut="⌘K"
478
+ style={{ width: '220px' }}
479
+ />
480
+ </Header.Search>
481
+ );
482
+ }
483
+
484
+ function ViewerHeader({ showHealth, searchQuery, onSearchChange, searchInputRef }: ViewerHeaderProps) {
485
+ return (
486
+ <Header aria-label="Fragments viewer header">
487
+ <Header.Trigger />
488
+ <Header.Brand>
489
+ <Stack direction="row" gap="sm" align="center">
490
+ <Text weight="medium" size="sm">{BRAND.name}</Text>
491
+ <Text size="xs" color="tertiary">{showHealth ? 'health dashboard' : 'preview'}</Text>
492
+ </Stack>
493
+ </Header.Brand>
494
+ <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
495
+ <Header.Spacer />
496
+ </Header>
497
+ );
419
498
  }
420
499
 
421
- function TopToolbar({ segment, variant, viewSettings, uiState, uiActions, figmaUrl, linkCopied, onCopyLink }: TopToolbarProps) {
500
+ function TopToolbar({
501
+ segment,
502
+ variant,
503
+ viewSettings,
504
+ uiState,
505
+ uiActions,
506
+ figmaUrl,
507
+ linkCopied,
508
+ onCopyLink,
509
+ searchQuery,
510
+ onSearchChange,
511
+ searchInputRef,
512
+ }: TopToolbarProps) {
422
513
  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">
514
+ <Header aria-label="Component preview toolbar">
515
+ <Header.Trigger />
516
+ <Header.Brand>
517
+ <Stack direction="row" align="center" gap="sm">
518
+ <Text weight="medium" size="sm">{segment.segment.meta.name}</Text>
519
+ <Text size="xs" color="tertiary">{segment.segment.meta.category}</Text>
520
+ </Stack>
521
+ </Header.Brand>
522
+ <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
523
+ <Header.Spacer />
524
+ <Header.Actions>
429
525
  <PreviewToolbar
430
526
  zoom={viewSettings.zoom}
431
527
  background={viewSettings.background}
432
528
  onZoomChange={viewSettings.setZoom}
433
529
  onBackgroundChange={viewSettings.setBackground}
434
530
  />
435
- <div className="w-px h-4 bg-[--border]" />
531
+ <Separator orientation="vertical" style={{ height: '16px' }} />
436
532
  <ViewportSelector
437
533
  viewport={viewSettings.viewport}
438
534
  customSize={viewSettings.customSize}
439
535
  onViewportChange={viewSettings.setViewport}
440
536
  onCustomSizeChange={viewSettings.setCustomSize}
441
537
  />
442
- <div className="w-px h-4 bg-[--border]" />
538
+ <Separator orientation="vertical" style={{ height: '16px' }} />
443
539
 
444
540
  {figmaUrl && (
445
541
  <>
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]" />
542
+ <Tooltip content={uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"}>
543
+ <Button
544
+ onClick={uiActions.toggleComparison}
545
+ variant="ghost"
546
+ size="sm"
547
+ style={uiState.showComparison ? { color: 'var(--color-accent)', backgroundColor: 'var(--bg-hover)' } : {}}
548
+ >
549
+ <CompareIcon style={{ width: '16px', height: '16px' }} />
550
+ </Button>
551
+ </Tooltip>
552
+ <Tooltip content="View in Figma">
553
+ <Button
554
+ onClick={() => window.open(figmaUrl, '_blank', 'noopener,noreferrer')}
555
+ variant="ghost"
556
+ size="sm"
557
+ >
558
+ <FigmaIcon style={{ width: '16px', height: '16px' }} />
559
+ </Button>
560
+ </Tooltip>
561
+ <Separator orientation="vertical" style={{ height: '16px' }} />
466
562
  </>
467
563
  )}
468
564
 
469
565
  {variant && (
470
566
  <>
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>
567
+ <Tooltip content="Open in new window">
568
+ <Button
569
+ onClick={() => {
570
+ const url = new URL(window.location.href);
571
+ url.hash = '';
572
+ url.searchParams.set('isolated', 'true');
573
+ url.searchParams.set('component', segment.segment.meta.name);
574
+ url.searchParams.set('variant', variant.name);
575
+ if (viewSettings.zoom !== 100) url.searchParams.set('zoom', String(viewSettings.zoom));
576
+ if (viewSettings.background !== 'transparent') url.searchParams.set('bg', viewSettings.background);
577
+ window.open(url.toString(), '_blank', 'noopener,noreferrer');
578
+ }}
579
+ variant="ghost"
580
+ size="sm"
581
+ >
582
+ <ExternalLinkIcon style={{ width: '16px', height: '16px' }} />
583
+ </Button>
584
+ </Tooltip>
488
585
  <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>
586
+ <Tooltip content="Copy link to share">
587
+ <Button
588
+ onClick={onCopyLink}
589
+ variant="ghost"
590
+ size="sm"
591
+ style={linkCopied ? { color: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.1)' } : {}}
592
+ >
593
+ {linkCopied ? <CheckIcon style={{ width: '16px', height: '16px' }} /> : <LinkIcon style={{ width: '16px', height: '16px' }} />}
594
+ </Button>
595
+ </Tooltip>
501
596
  </>
502
597
  )}
503
- </div>
504
- </div>
598
+ </Header.Actions>
599
+ </Header>
505
600
  );
506
601
  }
507
602
 
@@ -518,43 +613,39 @@ interface VariantTabsBarProps {
518
613
 
519
614
  function VariantTabsBar({ variants, activeIndex, onSelect, showMatrixView, showMultiViewport, onToggleMatrix, onToggleMultiViewport }: VariantTabsBarProps) {
520
615
  return (
521
- <div className="px-4 py-2 border-b border-[--border] bg-[--bg-primary] flex-shrink-0 flex items-center justify-between">
616
+ <Stack direction="row" align="center" justify="between" style={{ padding: '8px 16px', borderBottom: '1px solid var(--border)', backgroundColor: 'var(--bg-primary)', flexShrink: 0 }}>
522
617
  {!showMatrixView ? (
523
- <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
618
+ <ScrollArea orientation="horizontal" showFades style={{ flex: 1, minWidth: 0 }}>
619
+ <VariantTabs variants={variants} activeIndex={activeIndex} onSelect={onSelect} />
620
+ </ScrollArea>
524
621
  ) : (
525
- <div className="text-sm text-secondary">Showing all {variants.length} variants</div>
622
+ <Text size="sm" color="secondary">Showing all {variants.length} variants</Text>
526
623
  )}
527
- <div className="flex items-center gap-2 ml-4">
624
+ <Stack direction="row" align="center" gap="sm" style={{ marginLeft: '16px', flexShrink: 0 }}>
528
625
  {variants.length > 1 && (
529
- <button
626
+ <Button
530
627
  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
- )}
628
+ variant="ghost"
629
+ size="sm"
537
630
  title={showMatrixView ? "Show single variant" : "Show all variants in grid"}
631
+ style={showMatrixView ? { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: 'var(--color-accent)' } : {}}
538
632
  >
539
- <GridIcon className="w-4 h-4" />
633
+ <GridIcon style={{ width: '16px', height: '16px' }} />
540
634
  {showMatrixView ? "Exit Matrix" : "Matrix"}
541
- </button>
635
+ </Button>
542
636
  )}
543
- <button
637
+ <Button
544
638
  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
- )}
639
+ variant="ghost"
640
+ size="sm"
551
641
  title={showMultiViewport ? "Exit multi-viewport" : "Show at multiple screen sizes"}
642
+ style={showMultiViewport ? { backgroundColor: 'rgba(34, 197, 94, 0.1)', color: '#16a34a' } : {}}
552
643
  >
553
- <DevicesIcon className="w-4 h-4" />
644
+ <DevicesIcon style={{ width: '16px', height: '16px' }} />
554
645
  {showMultiViewport ? "Exit Responsive" : "Responsive"}
555
- </button>
556
- </div>
557
- </div>
646
+ </Button>
647
+ </Stack>
648
+ </Stack>
558
649
  );
559
650
  }
560
651
 
@@ -567,34 +658,39 @@ function NoVariantsMessage({ segment }: NoVariantsMessageProps) {
567
658
  const skippedVariants = (segment?._generated as any)?.skippedVariants;
568
659
 
569
660
  if (!skippedVariants || skippedVariants.length === 0) {
570
- return <div className="flex items-center justify-center h-full text-secondary text-sm">No variants defined</div>;
661
+ return (
662
+ <EmptyState style={{ height: '100%' }}>
663
+ <EmptyState.Description>No variants defined</EmptyState.Description>
664
+ </EmptyState>
665
+ );
571
666
  }
572
667
 
573
668
  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>
669
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', padding: '24px' }}>
670
+ <Alert variant="info">
671
+ <Alert.Body>
672
+ <Alert.Title>
673
+ {skippedVariants.length} variant{skippedVariants.length === 1 ? '' : 's'} skipped
674
+ </Alert.Title>
675
+ <Alert.Content>
676
+ <Stack direction="column" gap="sm">
677
+ <Text size="xs" color="secondary">
678
+ These variants couldn't be rendered because they use syntax the parser doesn't support yet:
679
+ </Text>
680
+ <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
681
+ {skippedVariants.map((sv: any, i: number) => (
682
+ <li key={i}>
683
+ <Text size="xs" color="secondary">
684
+ <Text as="span" size="xs" weight="semibold">{sv.name}:</Text>{' '}
685
+ <Text as="span" size="xs" color="tertiary">{sv.reason}</Text>
686
+ </Text>
687
+ </li>
688
+ ))}
689
+ </ul>
690
+ </Stack>
691
+ </Alert.Content>
692
+ </Alert.Body>
693
+ </Alert>
598
694
  </div>
599
695
  );
600
696
  }
@@ -608,31 +704,28 @@ interface EmptyVariantMessageProps {
608
704
 
609
705
  function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
610
706
  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>
707
+ <Alert variant="warning">
708
+ <Alert.Body>
709
+ <Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
710
+ <Alert.Content>
711
+ <Stack direction="column" gap="sm">
712
+ <Text size="xs" color="secondary">{reason}</Text>
713
+ {hint && (
714
+ <Text size="xs" color="tertiary">
715
+ <Text as="span" size="xs" weight="semibold">Tip:</Text> {hint}
716
+ </Text>
717
+ )}
718
+ <div>
719
+ <Text size="xs" color="tertiary" weight="semibold">Common causes:</Text>
720
+ <ul style={{ marginTop: '4px', marginLeft: '16px', listStyleType: 'disc' }}>
721
+ <li><Text size="xs" color="secondary">Component requires props that weren't provided</Text></li>
722
+ <li><Text size="xs" color="secondary">Component renders conditionally and conditions aren't met</Text></li>
723
+ <li><Text size="xs" color="secondary">Story args reference variables that don't exist in this context</Text></li>
724
+ </ul>
725
+ </div>
726
+ </Stack>
727
+ </Alert.Content>
728
+ </Alert.Body>
729
+ </Alert>
637
730
  );
638
731
  }