@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
@@ -12,7 +12,6 @@
12
12
 
13
13
  import { useState, useMemo, useRef, useCallback } from "react";
14
14
  import { useVirtualizer } from "@tanstack/react-virtual";
15
- import clsx from "clsx";
16
15
  import type { SegmentVariant } from "../../core/index.js";
17
16
  import { ErrorBoundary } from "./ErrorBoundary.js";
18
17
  import { StoryRenderer, LoaderIndicator } from "./StoryRenderer.js";
@@ -42,7 +41,7 @@ interface VariantMatrixProps {
42
41
  type GridSize = "small" | "medium" | "large";
43
42
 
44
43
  interface GridConfig {
45
- cols: string;
44
+ gridTemplateColumns: string;
46
45
  minHeight: string;
47
46
  heightPx: number; // For virtualization
48
47
  scale: number;
@@ -50,9 +49,9 @@ interface GridConfig {
50
49
  }
51
50
 
52
51
  const GRID_SIZES: Record<GridSize, GridConfig> = {
53
- small: { cols: "grid-cols-4 lg:grid-cols-5 xl:grid-cols-6", minHeight: "150px", heightPx: 150, scale: 0.5, colCount: 4 },
54
- medium: { cols: "grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", minHeight: "200px", heightPx: 200, scale: 0.75, colCount: 3 },
55
- large: { cols: "grid-cols-1 lg:grid-cols-2 xl:grid-cols-3", minHeight: "300px", heightPx: 300, scale: 1, colCount: 2 },
52
+ small: { gridTemplateColumns: "repeat(4, 1fr)", minHeight: "150px", heightPx: 150, scale: 0.5, colCount: 4 },
53
+ medium: { gridTemplateColumns: "repeat(3, 1fr)", minHeight: "200px", heightPx: 200, scale: 0.75, colCount: 3 },
54
+ large: { gridTemplateColumns: "repeat(2, 1fr)", minHeight: "300px", heightPx: 300, scale: 1, colCount: 2 },
56
55
  };
57
56
 
58
57
  /** Threshold for enabling virtualization */
@@ -70,6 +69,7 @@ export function VariantMatrix({
70
69
  }: VariantMatrixProps) {
71
70
  const [gridSize, setGridSize] = useState<GridSize>("medium");
72
71
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
72
+ const [hoveredButton, setHoveredButton] = useState<GridSize | null>(null);
73
73
  const scrollRef = useRef<HTMLDivElement>(null);
74
74
 
75
75
  const gridConfig = GRID_SIZES[gridSize];
@@ -94,35 +94,68 @@ export function VariantMatrix({
94
94
 
95
95
  if (variants.length === 0) {
96
96
  return (
97
- <div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
97
+ <div style={{
98
+ display: 'flex',
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ height: '100%',
102
+ color: 'var(--text-muted)',
103
+ }}>
98
104
  No variants to display
99
105
  </div>
100
106
  );
101
107
  }
102
108
 
103
109
  return (
104
- <div className="h-full flex flex-col">
110
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
105
111
  {/* Toolbar */}
106
- <div className="flex-shrink-0 px-4 py-2 border-b border-[--border] bg-[--bg-secondary] flex items-center justify-between">
107
- <div className="text-sm text-secondary">
112
+ <div style={{
113
+ flexShrink: 0,
114
+ padding: '8px 16px',
115
+ borderBottom: '1px solid var(--border)',
116
+ background: 'var(--bg-secondary)',
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ justifyContent: 'space-between',
120
+ }}>
121
+ <div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>
108
122
  {variants.length} variant{variants.length !== 1 ? "s" : ""}
109
123
  {useVirtualization && (
110
- <span className="ml-2 text-xs text-tertiary">(virtualized)</span>
124
+ <span style={{ marginLeft: 8, fontSize: 12, color: 'var(--text-tertiary)' }}>(virtualized)</span>
111
125
  )}
112
126
  </div>
113
- <div className="flex items-center gap-2">
114
- <span className="text-xs text-tertiary">Grid size:</span>
115
- <div className="flex rounded-md border border-[--border] overflow-hidden">
127
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
128
+ <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Grid size:</span>
129
+ <div style={{
130
+ display: 'flex',
131
+ borderRadius: 6,
132
+ border: '1px solid var(--border)',
133
+ overflow: 'hidden',
134
+ }}>
116
135
  {(["small", "medium", "large"] as GridSize[]).map((size) => (
117
136
  <button
118
137
  key={size}
119
138
  onClick={() => setGridSize(size)}
120
- className={clsx(
121
- "px-2 py-1 text-xs capitalize transition-colors",
122
- gridSize === size
123
- ? "bg-[--bg-hover] text-primary"
124
- : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
125
- )}
139
+ onMouseEnter={() => setHoveredButton(size)}
140
+ onMouseLeave={() => setHoveredButton(null)}
141
+ style={{
142
+ padding: '4px 8px',
143
+ fontSize: 12,
144
+ textTransform: 'capitalize',
145
+ transition: 'background-color 0.15s, color 0.15s',
146
+ border: 'none',
147
+ cursor: 'pointer',
148
+ background: gridSize === size
149
+ ? 'var(--bg-hover)'
150
+ : hoveredButton === size
151
+ ? 'var(--bg-hover)'
152
+ : 'transparent',
153
+ color: gridSize === size
154
+ ? 'var(--text-primary)'
155
+ : hoveredButton === size
156
+ ? 'var(--text-secondary)'
157
+ : 'var(--text-tertiary)',
158
+ }}
126
159
  >
127
160
  {size}
128
161
  </button>
@@ -133,7 +166,7 @@ export function VariantMatrix({
133
166
 
134
167
  {/* Grid - Virtualized or Regular */}
135
168
  {useVirtualization ? (
136
- <div ref={scrollRef} className="flex-1 overflow-auto p-4">
169
+ <div ref={scrollRef} style={{ flex: 1, overflow: 'auto', padding: 16 }}>
137
170
  <div
138
171
  style={{
139
172
  height: `${rowVirtualizer.getTotalSize()}px`,
@@ -157,7 +190,12 @@ export function VariantMatrix({
157
190
  transform: `translateY(${virtualRow.start}px)`,
158
191
  }}
159
192
  >
160
- <div className={clsx("grid gap-4", gridConfig.cols)} style={{ height: gridConfig.minHeight }}>
193
+ <div style={{
194
+ display: 'grid',
195
+ gap: 16,
196
+ gridTemplateColumns: gridConfig.gridTemplateColumns,
197
+ height: gridConfig.minHeight,
198
+ }}>
161
199
  {rowVariants.map((variant, colIndex) => {
162
200
  const index = startIndex + colIndex;
163
201
  return (
@@ -186,8 +224,12 @@ export function VariantMatrix({
186
224
  </div>
187
225
  </div>
188
226
  ) : (
189
- <div className="flex-1 overflow-auto p-4">
190
- <div className={clsx("grid gap-4", gridConfig.cols)}>
227
+ <div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
228
+ <div style={{
229
+ display: 'grid',
230
+ gap: 16,
231
+ gridTemplateColumns: gridConfig.gridTemplateColumns,
232
+ }}>
191
233
  {variants.map((variant, index) => (
192
234
  <VariantCard
193
235
  key={variant.name}
@@ -248,24 +290,46 @@ function VariantCard({
248
290
 
249
291
  return (
250
292
  <div
251
- className={clsx(
252
- "group relative rounded-lg border overflow-hidden transition-all cursor-pointer",
253
- isHovered
254
- ? "border-blue-500 shadow-lg ring-2 ring-blue-500/20"
255
- : "border-[--border] hover:border-blue-300 dark:hover:border-blue-700"
256
- )}
257
- style={{ minHeight }}
293
+ style={{
294
+ position: 'relative',
295
+ borderRadius: 8,
296
+ border: isHovered
297
+ ? '2px solid #3b82f6'
298
+ : '1px solid var(--border)',
299
+ overflow: 'hidden',
300
+ transition: 'border-color 0.2s, box-shadow 0.2s',
301
+ cursor: 'pointer',
302
+ minHeight,
303
+ boxShadow: isHovered
304
+ ? '0 10px 15px -3px rgba(0,0,0,0.1), 0 0 0 3px rgba(59,130,246,0.2)'
305
+ : 'none',
306
+ }}
258
307
  onMouseEnter={onHover}
259
308
  onMouseLeave={onLeave}
260
309
  onClick={onClick}
261
310
  >
262
311
  {/* Header */}
263
- <div className="absolute top-0 left-0 right-0 z-10 px-2 py-1 bg-gradient-to-b from-black/60 to-transparent">
264
- <div className="flex items-center justify-between">
265
- <span className="text-xs font-medium text-white truncate">
312
+ <div style={{
313
+ position: 'absolute',
314
+ top: 0,
315
+ left: 0,
316
+ right: 0,
317
+ zIndex: 10,
318
+ padding: '4px 8px',
319
+ background: 'linear-gradient(to bottom, rgba(0,0,0,0.6), transparent)',
320
+ }}>
321
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
322
+ <span style={{
323
+ fontSize: 12,
324
+ fontWeight: 500,
325
+ color: '#ffffff',
326
+ overflow: 'hidden',
327
+ textOverflow: 'ellipsis',
328
+ whiteSpace: 'nowrap',
329
+ }}>
266
330
  {variant.name}
267
331
  </span>
268
- <span className="text-[10px] text-white/70">
332
+ <span style={{ fontSize: 10, color: 'rgba(255,255,255,0.7)' }}>
269
333
  #{index + 1}
270
334
  </span>
271
335
  </div>
@@ -273,21 +337,44 @@ function VariantCard({
273
337
 
274
338
  {/* Click to view overlay */}
275
339
  <div
276
- className={clsx(
277
- "absolute inset-0 z-10 flex items-center justify-center bg-black/40 transition-opacity",
278
- isHovered ? "opacity-100" : "opacity-0"
279
- )}
340
+ style={{
341
+ position: 'absolute',
342
+ inset: 0,
343
+ zIndex: 10,
344
+ display: 'flex',
345
+ alignItems: 'center',
346
+ justifyContent: 'center',
347
+ background: 'rgba(0,0,0,0.4)',
348
+ transition: 'opacity 0.2s',
349
+ opacity: isHovered ? 1 : 0,
350
+ pointerEvents: isHovered ? 'auto' : 'none',
351
+ }}
280
352
  >
281
- <span className="px-3 py-1.5 bg-blue-600 text-white text-xs font-medium rounded-full shadow-lg">
353
+ <span style={{
354
+ padding: '6px 12px',
355
+ background: '#2563eb',
356
+ color: '#ffffff',
357
+ fontSize: 12,
358
+ fontWeight: 500,
359
+ borderRadius: 9999,
360
+ boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)',
361
+ }}>
282
362
  Click to focus
283
363
  </span>
284
364
  </div>
285
365
 
286
366
  {/* Preview content */}
287
367
  <div
288
- className="h-full w-full overflow-hidden flex items-center justify-center"
289
368
  data-theme={previewTheme}
290
- style={backgroundStyle}
369
+ style={{
370
+ height: '100%',
371
+ width: '100%',
372
+ overflow: 'hidden',
373
+ display: 'flex',
374
+ alignItems: 'center',
375
+ justifyContent: 'center',
376
+ ...backgroundStyle,
377
+ }}
291
378
  >
292
379
  {useIframeIsolation ? (
293
380
  <IsolatedPreviewFrame
@@ -300,15 +387,15 @@ function VariantCard({
300
387
  />
301
388
  ) : (
302
389
  <div
303
- className="p-4"
304
390
  style={{
391
+ padding: 16,
305
392
  transform: `scale(${scale})`,
306
393
  }}
307
394
  >
308
395
  <ErrorBoundary
309
396
  componentName={componentName}
310
397
  fallback={
311
- <div className="text-xs text-red-500 p-2">
398
+ <div style={{ fontSize: 12, color: '#ef4444', padding: 8 }}>
312
399
  Error rendering variant
313
400
  </div>
314
401
  }
@@ -317,14 +404,14 @@ function VariantCard({
317
404
  {(content, isLoading, error) => {
318
405
  if (isLoading) {
319
406
  return (
320
- <div className="flex items-center justify-center p-4">
407
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
321
408
  <LoaderIndicator />
322
409
  </div>
323
410
  );
324
411
  }
325
412
  if (error) {
326
413
  return (
327
- <div className="text-xs text-red-500 p-2">
414
+ <div style={{ fontSize: 12, color: '#ef4444', padding: 8 }}>
328
415
  {error.message}
329
416
  </div>
330
417
  );
@@ -339,8 +426,14 @@ function VariantCard({
339
426
 
340
427
  {/* Tags/badges */}
341
428
  {variant.hasPlayFunction && (
342
- <div className="absolute bottom-2 right-2 z-10">
343
- <span className="px-1.5 py-0.5 text-[10px] bg-purple-600 text-white rounded">
429
+ <div style={{ position: 'absolute', bottom: 8, right: 8, zIndex: 10 }}>
430
+ <span style={{
431
+ padding: '2px 6px',
432
+ fontSize: 10,
433
+ background: '#9333ea',
434
+ color: '#ffffff',
435
+ borderRadius: 4,
436
+ }}>
344
437
  play
345
438
  </span>
346
439
  </div>
@@ -1,6 +1,5 @@
1
- import { useRef, useEffect, useCallback } from 'react';
1
+ import { Tabs } from '@fragments/ui';
2
2
  import type { SegmentVariant } from '../../core/index.js';
3
- import clsx from 'clsx';
4
3
  import { PlayIcon } from './Icons.js';
5
4
 
6
5
  interface VariantTabsProps {
@@ -10,75 +9,32 @@ interface VariantTabsProps {
10
9
  }
11
10
 
12
11
  export function VariantTabs({ variants, activeIndex, onSelect }: VariantTabsProps) {
13
- const containerRef = useRef<HTMLDivElement>(null);
14
- const buttonRefs = useRef<Map<number, HTMLButtonElement>>(new Map());
15
-
16
- // Scroll active tab into view when it changes
17
- useEffect(() => {
18
- const button = buttonRefs.current.get(activeIndex);
19
- if (button && containerRef.current) {
20
- button.scrollIntoView({
21
- behavior: 'smooth',
22
- block: 'nearest',
23
- inline: 'center',
24
- });
25
- }
26
- }, [activeIndex]);
27
-
28
- // Keyboard navigation for variant tabs
29
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
30
- if (e.key === 'ArrowLeft') {
31
- e.preventDefault();
32
- const newIndex = activeIndex > 0 ? activeIndex - 1 : variants.length - 1;
33
- onSelect(newIndex);
34
- } else if (e.key === 'ArrowRight') {
35
- e.preventDefault();
36
- const newIndex = activeIndex < variants.length - 1 ? activeIndex + 1 : 0;
37
- onSelect(newIndex);
38
- }
39
- }, [activeIndex, variants.length, onSelect]);
40
-
41
12
  if (variants.length === 0) return null;
42
13
 
14
+ const activeValue = variants[activeIndex]?.name ?? '';
15
+
43
16
  return (
44
- <div
45
- ref={containerRef}
46
- className="flex items-center gap-1 overflow-x-auto max-w-full scrollbar-thin"
47
- onKeyDown={handleKeyDown}
48
- role="tablist"
49
- aria-label="Component variants"
17
+ <Tabs
18
+ value={activeValue}
19
+ onValueChange={(value: string | number) => {
20
+ const index = variants.findIndex(v => v.name === value);
21
+ if (index >= 0) onSelect(index);
22
+ }}
50
23
  >
51
- {variants.map((variant, index) => {
52
- const isActive = activeIndex === index;
53
- return (
54
- <button
55
- key={index}
56
- ref={(el) => {
57
- if (el) buttonRefs.current.set(index, el);
58
- else buttonRefs.current.delete(index);
59
- }}
60
- onClick={() => onSelect(index)}
61
- onKeyDown={handleKeyDown}
62
- role="tab"
63
- aria-selected={isActive}
64
- tabIndex={isActive ? 0 : -1}
65
- className={clsx(
66
- 'px-3 py-1 text-xs font-medium rounded whitespace-nowrap flex-shrink-0',
67
- 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]',
68
- 'flex items-center gap-1.5',
69
- isActive
70
- ? 'text-primary bg-[--bg-hover]'
71
- : 'text-tertiary hover:text-secondary'
72
- )}
73
- title={variant.hasPlayFunction ? `${variant.name} (has interaction test)` : variant.name}
74
- >
75
- {variant.name}
76
- {variant.hasPlayFunction && (
77
- <PlayIcon className="w-3 h-3 text-[--color-accent]" />
78
- )}
79
- </button>
80
- );
81
- })}
82
- </div>
24
+ <Tabs.List variant="pills">
25
+ {variants.map((variant) => (
26
+ <Tabs.Tab key={variant.name} value={variant.name}>
27
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
28
+ {variant.name}
29
+ {variant.hasPlayFunction && (
30
+ <span style={{ display: 'inline-flex', width: '12px', height: '12px', color: 'var(--color-accent)' }}>
31
+ <PlayIcon />
32
+ </span>
33
+ )}
34
+ </span>
35
+ </Tabs.Tab>
36
+ ))}
37
+ </Tabs.List>
38
+ </Tabs>
83
39
  );
84
40
  }