@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
@@ -68,38 +68,80 @@ const DeviceMockup = memo(function DeviceMockup({ type, width, children }: Devic
68
68
  const screenHeight = frameHeight - (isMobile ? 80 : 48);
69
69
 
70
70
  return (
71
- <div className="relative flex-shrink-0" style={{ width: `${frameWidth}px` }}>
71
+ <div style={{ position: 'relative', flexShrink: 0, width: `${frameWidth}px` }}>
72
72
  <div
73
- className="relative rounded-[40px] bg-[#1a1a1a] p-3 shadow-2xl"
74
73
  style={{
74
+ position: 'relative',
75
+ borderRadius: '40px',
76
+ background: '#1a1a1a',
77
+ padding: '12px',
75
78
  boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.1)',
76
79
  }}
77
80
  >
78
81
  {isMobile && (
79
82
  <>
80
- <div className="absolute -left-[3px] top-24 w-[3px] h-8 bg-[#2a2a2a] rounded-l" />
81
- <div className="absolute -left-[3px] top-36 w-[3px] h-12 bg-[#2a2a2a] rounded-l" />
82
- <div className="absolute -left-[3px] top-52 w-[3px] h-12 bg-[#2a2a2a] rounded-l" />
83
- <div className="absolute -right-[3px] top-32 w-[3px] h-16 bg-[#2a2a2a] rounded-r" />
83
+ <div style={{ position: 'absolute', left: '-3px', top: '96px', width: '3px', height: '32px', background: '#2a2a2a', borderRadius: '4px 0 0 4px' }} />
84
+ <div style={{ position: 'absolute', left: '-3px', top: '144px', width: '3px', height: '48px', background: '#2a2a2a', borderRadius: '4px 0 0 4px' }} />
85
+ <div style={{ position: 'absolute', left: '-3px', top: '208px', width: '3px', height: '48px', background: '#2a2a2a', borderRadius: '4px 0 0 4px' }} />
86
+ <div style={{ position: 'absolute', right: '-3px', top: '128px', width: '3px', height: '64px', background: '#2a2a2a', borderRadius: '0 4px 4px 0' }} />
84
87
  </>
85
88
  )}
86
89
 
87
90
  <div
88
- className="relative rounded-[32px] overflow-hidden bg-white"
89
- style={{ height: `${screenHeight}px` }}
91
+ style={{
92
+ position: 'relative',
93
+ borderRadius: '32px',
94
+ overflow: 'hidden',
95
+ background: 'white',
96
+ height: `${screenHeight}px`,
97
+ }}
90
98
  >
91
99
  {isMobile ? (
92
- <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[120px] h-[30px] bg-[#1a1a1a] rounded-b-2xl z-10 flex items-center justify-center gap-2">
93
- <div className="w-2 h-2 rounded-full bg-[#2a2a2a]" />
94
- <div className="w-12 h-1.5 rounded-full bg-[#2a2a2a]" />
100
+ <div style={{
101
+ position: 'absolute',
102
+ top: 0,
103
+ left: '50%',
104
+ transform: 'translateX(-50%)',
105
+ width: '120px',
106
+ height: '30px',
107
+ background: '#1a1a1a',
108
+ borderRadius: '0 0 16px 16px',
109
+ zIndex: 10,
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ gap: '8px',
114
+ }}>
115
+ <div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#2a2a2a' }} />
116
+ <div style={{ width: '48px', height: '6px', borderRadius: '9999px', background: '#2a2a2a' }} />
95
117
  </div>
96
118
  ) : (
97
- <div className="absolute top-2 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-[#2a2a2a] z-10" />
119
+ <div style={{
120
+ position: 'absolute',
121
+ top: '8px',
122
+ left: '50%',
123
+ transform: 'translateX(-50%)',
124
+ width: '12px',
125
+ height: '12px',
126
+ borderRadius: '50%',
127
+ background: '#2a2a2a',
128
+ zIndex: 10,
129
+ }} />
98
130
  )}
99
131
 
100
- <div className="w-full h-full overflow-auto">{children}</div>
132
+ <div style={{ width: '100%', height: '100%', overflow: 'auto' }}>{children}</div>
101
133
 
102
- <div className="absolute bottom-2 left-1/2 -translate-x-1/2 w-[100px] h-1 bg-black/20 rounded-full z-10" />
134
+ <div style={{
135
+ position: 'absolute',
136
+ bottom: '8px',
137
+ left: '50%',
138
+ transform: 'translateX(-50%)',
139
+ width: '100px',
140
+ height: '4px',
141
+ background: 'rgba(0, 0, 0, 0.2)',
142
+ borderRadius: '9999px',
143
+ zIndex: 10,
144
+ }} />
103
145
  </div>
104
146
  </div>
105
147
  </div>
@@ -119,14 +161,16 @@ const PreviewContent = memo(function PreviewContent({ zoom, previewTheme, backgr
119
161
  <div
120
162
  data-preview-container="true"
121
163
  data-theme={previewTheme}
122
- className="w-full h-full overflow-auto"
123
164
  style={{
165
+ width: '100%',
166
+ height: '100%',
167
+ overflow: 'auto',
124
168
  backgroundColor: background === 'transparent' ? 'transparent' : undefined,
125
169
  }}
126
170
  >
127
171
  <div
128
- className="p-6"
129
172
  style={{
173
+ padding: '24px',
130
174
  transform: `scale(${zoom / 100})`,
131
175
  transformOrigin: 'top left',
132
176
  width: zoom !== 100 ? `${100 / (zoom / 100)}%` : '100%',
@@ -205,11 +249,11 @@ export function PreviewArea({
205
249
 
206
250
  if (showComparison && figmaUrl) {
207
251
  return (
208
- <div className="min-h-full flex flex-col p-6">
209
- <div className="flex gap-4 flex-1">
210
- <div className="flex-1 flex flex-col">
211
- <div className="text-xs font-medium text-tertiary mb-2 text-center">Rendered</div>
212
- <div className="flex-1 flex items-center justify-center" style={backgroundStyle}>
252
+ <div style={{ minHeight: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
253
+ <div style={{ display: 'flex', gap: '16px', flex: 1 }}>
254
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
255
+ <div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)', marginBottom: '8px', textAlign: 'center' }}>Rendered</div>
256
+ <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', ...backgroundStyle }}>
213
257
  <DeviceMockup type={viewport as 'tablet' | 'mobile'} width={viewportWidth}>
214
258
  {useIframeIsolation ? (
215
259
  <IsolatedPreviewFrame
@@ -231,14 +275,19 @@ export function PreviewArea({
231
275
  </div>
232
276
  </div>
233
277
 
234
- <div className="flex-1 flex flex-col">
235
- <div className="text-xs font-medium text-tertiary mb-2 text-center">Figma Design</div>
278
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
279
+ <div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)', marginBottom: '8px', textAlign: 'center' }}>Figma Design</div>
236
280
  <FigmaEmbed
237
281
  figmaUrl={figmaUrl}
238
282
  allFigmaUrls={allFigmaUrls}
239
283
  zoom={zoom}
240
- className="flex-1 rounded-lg border border-[--border] overflow-hidden"
241
- style={backgroundStyle}
284
+ style={{
285
+ flex: 1,
286
+ borderRadius: '8px',
287
+ border: '1px solid var(--border)',
288
+ overflow: 'hidden',
289
+ ...backgroundStyle,
290
+ }}
242
291
  />
243
292
  </div>
244
293
  </div>
@@ -247,7 +296,7 @@ export function PreviewArea({
247
296
  }
248
297
 
249
298
  return (
250
- <div className="min-h-full flex items-center justify-center p-8">
299
+ <div style={{ minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
251
300
  <DeviceMockup type={viewport as 'tablet' | 'mobile'} width={viewportWidth}>
252
301
  {useIframeIsolation ? (
253
302
  <IsolatedPreviewFrame
@@ -273,13 +322,18 @@ export function PreviewArea({
273
322
  // Side-by-side comparison view
274
323
  if (showComparison && figmaUrl && variant) {
275
324
  return (
276
- <div className="min-h-full flex flex-col p-6">
277
- <div className="flex gap-4 flex-1">
278
- <div className="flex-1 flex flex-col">
279
- <div className="text-xs font-medium text-tertiary mb-2 text-center">Rendered</div>
325
+ <div style={{ minHeight: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
326
+ <div style={{ display: 'flex', gap: '16px', flex: 1 }}>
327
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
328
+ <div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)', marginBottom: '8px', textAlign: 'center' }}>Rendered</div>
280
329
  <div
281
- className="flex-1 rounded-lg border border-[--border] overflow-auto"
282
- style={backgroundStyle}
330
+ style={{
331
+ flex: 1,
332
+ borderRadius: '8px',
333
+ border: '1px solid var(--border)',
334
+ overflow: 'auto',
335
+ ...backgroundStyle,
336
+ }}
283
337
  >
284
338
  {useIframeIsolation ? (
285
339
  <IsolatedPreviewFrame
@@ -293,7 +347,12 @@ export function PreviewArea({
293
347
  />
294
348
  ) : (
295
349
  <div
296
- className="flex items-center justify-center p-8"
350
+ style={{
351
+ display: 'flex',
352
+ alignItems: 'center',
353
+ justifyContent: 'center',
354
+ padding: '32px',
355
+ }}
297
356
  data-preview-container="true"
298
357
  data-theme={previewTheme}
299
358
  >
@@ -314,14 +373,19 @@ export function PreviewArea({
314
373
  </div>
315
374
  </div>
316
375
 
317
- <div className="flex-1 flex flex-col">
318
- <div className="text-xs font-medium text-tertiary mb-2 text-center">Figma Design</div>
376
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
377
+ <div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)', marginBottom: '8px', textAlign: 'center' }}>Figma Design</div>
319
378
  <FigmaEmbed
320
379
  figmaUrl={figmaUrl}
321
380
  allFigmaUrls={allFigmaUrls}
322
381
  zoom={zoom}
323
- className="flex-1 rounded-lg border border-[--border] overflow-hidden"
324
- style={backgroundStyle}
382
+ style={{
383
+ flex: 1,
384
+ borderRadius: '8px',
385
+ border: '1px solid var(--border)',
386
+ overflow: 'hidden',
387
+ ...backgroundStyle,
388
+ }}
325
389
  />
326
390
  </div>
327
391
  </div>
@@ -336,10 +400,14 @@ export function PreviewArea({
336
400
  const isFullWidth = !viewportWidth;
337
401
 
338
402
  return (
339
- <div className={isFullWidth ? "h-full flex flex-col" : "min-h-full flex items-center justify-center p-6"}>
403
+ <div style={isFullWidth
404
+ ? { height: '100%', display: 'flex', flexDirection: 'column' }
405
+ : { minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }
406
+ }>
340
407
  <div
341
- className="relative transition-all duration-200"
342
408
  style={{
409
+ position: 'relative',
410
+ transition: 'all 200ms',
343
411
  width: viewportWidth ? `${viewportWidth}px` : '100%',
344
412
  maxWidth: viewportWidth ? undefined : '100%',
345
413
  height: isFullWidth ? '100%' : undefined,
@@ -367,12 +435,11 @@ export function PreviewArea({
367
435
 
368
436
  // Fallback: Direct rendering without iframe isolation
369
437
  return (
370
- <div className="min-h-full flex items-center justify-center p-6">
438
+ <div style={{ minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
371
439
  <div
372
- className="relative transition-all duration-200"
373
- data-preview-container="true"
374
- data-theme={previewTheme}
375
440
  style={{
441
+ position: 'relative',
442
+ transition: 'all 200ms',
376
443
  width: viewportWidth ? `${viewportWidth}px` : '100%',
377
444
  maxWidth: viewportWidth ? undefined : '100%',
378
445
  minHeight: viewportHeight ? `${viewportHeight}px` : '100%',
@@ -382,10 +449,12 @@ export function PreviewArea({
382
449
  boxShadow: '0 0 0 1px var(--border), 0 4px 12px rgba(0,0,0,0.15)',
383
450
  }),
384
451
  }}
452
+ data-preview-container="true"
453
+ data-theme={previewTheme}
385
454
  >
386
455
  <div
387
- className="p-8"
388
456
  style={{
457
+ padding: '32px',
389
458
  transform: `scale(${zoom / 100})`,
390
459
  transformOrigin: 'top left',
391
460
  width: zoom !== 100 ? `${100 / (zoom / 100)}%` : '100%',
@@ -87,7 +87,7 @@ function findVariant(segment: SegmentDefinition, variantName: string): SegmentVa
87
87
  */
88
88
  function ErrorDisplay({ message, stack }: { message: string; stack?: string }) {
89
89
  return (
90
- <div className="preview-error">
90
+ <div style={{ padding: '16px', color: '#dc2626', background: 'rgba(254, 242, 242, 0.95)', borderRadius: '8px', margin: '16px' }}>
91
91
  <div style={{ fontWeight: 500, marginBottom: 8 }}>Render Error</div>
92
92
  <div>{message}</div>
93
93
  {stack && (
@@ -104,8 +104,9 @@ function ErrorDisplay({ message, stack }: { message: string; stack?: string }) {
104
104
  */
105
105
  function LoadingIndicator() {
106
106
  return (
107
- <div className="preview-loading">
108
- <div className="spinner" />
107
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px', gap: '8px', color: '#6b7280' }}>
108
+ <div style={{ width: '16px', height: '16px', border: '2px solid #e5e7eb', borderTopColor: '#3b82f6', borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />
109
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
109
110
  <span>Loading component...</span>
110
111
  </div>
111
112
  );
@@ -206,9 +207,9 @@ function VariantRenderer({
206
207
  return (
207
208
  <div
208
209
  ref={containerRef}
209
- className="transition-opacity duration-150"
210
210
  style={{
211
211
  display: 'inline-block',
212
+ transition: 'opacity 150ms',
212
213
  opacity: content ? 1 : 0,
213
214
  }}
214
215
  >
@@ -289,7 +290,7 @@ export function PreviewFrameHost() {
289
290
  // Show waiting state
290
291
  if (!currentVariant) {
291
292
  return (
292
- <div className="preview-loading">
293
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px', gap: '8px', color: '#6b7280' }}>
293
294
  <span>Waiting for render request...</span>
294
295
  </div>
295
296
  );
@@ -130,7 +130,6 @@ export function PreviewPane({ children, className, style, includeComponentStyles
130
130
  return (
131
131
  <div
132
132
  ref={containerRef}
133
- className={className}
134
133
  style={{ minHeight: '120px', ...style }}
135
134
  data-preview-wrapper="true"
136
135
  />
@@ -141,9 +140,9 @@ export function PreviewPane({ children, className, style, includeComponentStyles
141
140
  * SimplePreviewPane - A simpler preview without Shadow DOM isolation.
142
141
  * Use this when full isolation isn't needed.
143
142
  */
144
- export function SimplePreviewPane({ children, className, style }: PreviewPaneProps) {
143
+ export function SimplePreviewPane({ children, style }: PreviewPaneProps) {
145
144
  return (
146
- <div className={className} style={style}>
145
+ <div style={style}>
147
146
  {children}
148
147
  </div>
149
148
  );
@@ -1,5 +1,5 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import clsx from 'clsx';
1
+ import { useEffect, useCallback } from 'react';
2
+ import { Button, Menu, Stack, Separator } from '@fragments/ui';
3
3
  import {
4
4
  ZOOM_LEVELS,
5
5
  type ZoomLevel,
@@ -13,10 +13,10 @@ export { getBackgroundStyle } from '../constants/ui.js';
13
13
 
14
14
  // Background options with display metadata
15
15
  const BACKGROUND_OPTIONS_UI: { value: BackgroundOption; label: string; icon: string }[] = [
16
- { value: 'white', label: 'White', icon: '' },
17
- { value: 'black', label: 'Black', icon: '' },
18
- { value: 'checkerboard', label: 'Checkerboard', icon: '' },
19
- { value: 'transparent', label: 'Transparent', icon: '' },
16
+ { value: 'white', label: 'White', icon: '\u25FB' },
17
+ { value: 'black', label: 'Black', icon: '\u25FC' },
18
+ { value: 'checkerboard', label: 'Checkerboard', icon: '\u25A6' },
19
+ { value: 'transparent', label: 'Transparent', icon: '\u25C7' },
20
20
  ];
21
21
 
22
22
  interface PreviewToolbarProps {
@@ -32,9 +32,6 @@ export function PreviewToolbar({
32
32
  onZoomChange,
33
33
  onBackgroundChange,
34
34
  }: PreviewToolbarProps) {
35
- const [zoomOpen, setZoomOpen] = useState(false);
36
- const [bgOpen, setBgOpen] = useState(false);
37
-
38
35
  // Keyboard shortcuts for zoom
39
36
  const handleKeyDown = useCallback((e: KeyboardEvent) => {
40
37
  // Don't handle if in input/textarea
@@ -66,111 +63,71 @@ export function PreviewToolbar({
66
63
  return () => document.removeEventListener('keydown', handleKeyDown);
67
64
  }, [handleKeyDown]);
68
65
 
69
- // Close dropdowns when clicking outside
70
- useEffect(() => {
71
- const handleClick = () => {
72
- setZoomOpen(false);
73
- setBgOpen(false);
74
- };
75
- if (zoomOpen || bgOpen) {
76
- document.addEventListener('click', handleClick);
77
- return () => document.removeEventListener('click', handleClick);
78
- }
79
- }, [zoomOpen, bgOpen]);
80
-
81
66
  return (
82
- <div className="flex items-center gap-2">
67
+ <Stack direction="row" gap="sm" align="center">
83
68
  {/* Zoom dropdown */}
84
- <div className="relative">
85
- <button
86
- onClick={(e) => {
87
- e.stopPropagation();
88
- setZoomOpen(!zoomOpen);
89
- setBgOpen(false);
90
- }}
91
- className={clsx(
92
- 'flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded',
93
- 'text-secondary hover:text-primary',
94
- 'hover:bg-[--bg-hover] transition-colors',
95
- 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
96
- )}
97
- title="Zoom level (+/-/0)"
98
- >
99
- <ZoomIcon className="w-3.5 h-3.5" />
100
- <span>{zoom}%</span>
101
- <ChevronDownIcon className="w-3 h-3" />
102
- </button>
103
- {zoomOpen && (
104
- <div className="absolute top-full left-0 mt-1 py-1 min-w-[80px] bg-[--bg-elevated] border border-[--border] rounded-lg shadow-lg z-50">
69
+ <Menu>
70
+ <Menu.Trigger asChild>
71
+ <Button variant="ghost" size="sm" title="Zoom level (+/-/0)">
72
+ <Stack direction="row" gap="xs" align="center">
73
+ <span style={{ display: 'inline-flex', width: '14px', height: '14px' }}>
74
+ <ZoomIcon />
75
+ </span>
76
+ <span>{zoom}%</span>
77
+ <span style={{ display: 'inline-flex', width: '12px', height: '12px' }}>
78
+ <ChevronDownIcon />
79
+ </span>
80
+ </Stack>
81
+ </Button>
82
+ </Menu.Trigger>
83
+ <Menu.Content side="bottom" align="start">
84
+ <Menu.RadioGroup
85
+ value={String(zoom)}
86
+ onValueChange={(value: string) => onZoomChange(Number(value) as ZoomLevel)}
87
+ >
105
88
  {ZOOM_LEVELS.map((level) => (
106
- <button
107
- key={level}
108
- onClick={(e) => {
109
- e.stopPropagation();
110
- onZoomChange(level);
111
- setZoomOpen(false);
112
- }}
113
- className={clsx(
114
- 'w-full px-3 py-1.5 text-xs text-left',
115
- 'hover:bg-[--bg-hover] transition-colors',
116
- level === zoom ? 'text-[--color-accent] font-medium' : 'text-secondary'
117
- )}
118
- >
89
+ <Menu.RadioItem key={level} value={String(level)}>
119
90
  {level}%
120
- </button>
91
+ </Menu.RadioItem>
121
92
  ))}
122
- </div>
123
- )}
124
- </div>
93
+ </Menu.RadioGroup>
94
+ </Menu.Content>
95
+ </Menu>
125
96
 
126
97
  {/* Divider */}
127
- <div className="w-px h-4 bg-[--border]" />
98
+ <Separator orientation="vertical" />
128
99
 
129
100
  {/* Background selector */}
130
- <div className="relative">
131
- <button
132
- onClick={(e) => {
133
- e.stopPropagation();
134
- setBgOpen(!bgOpen);
135
- setZoomOpen(false);
136
- }}
137
- className={clsx(
138
- 'flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded',
139
- 'text-secondary hover:text-primary',
140
- 'hover:bg-[--bg-hover] transition-colors',
141
- 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
142
- )}
143
- title="Background color"
144
- >
145
- <span className="text-sm">
146
- {BACKGROUND_OPTIONS_UI.find(o => o.value === background)?.icon}
147
- </span>
148
- <span className="capitalize">{background}</span>
149
- <ChevronDownIcon className="w-3 h-3" />
150
- </button>
151
- {bgOpen && (
152
- <div className="absolute top-full left-0 mt-1 py-1 min-w-[120px] bg-[--bg-elevated] border border-[--border] rounded-lg shadow-lg z-50">
101
+ <Menu>
102
+ <Menu.Trigger asChild>
103
+ <Button variant="ghost" size="sm" title="Background color">
104
+ <Stack direction="row" gap="xs" align="center">
105
+ <span style={{ fontSize: '14px' }}>
106
+ {BACKGROUND_OPTIONS_UI.find(o => o.value === background)?.icon}
107
+ </span>
108
+ <span style={{ textTransform: 'capitalize' }}>{background}</span>
109
+ <span style={{ display: 'inline-flex', width: '12px', height: '12px' }}>
110
+ <ChevronDownIcon />
111
+ </span>
112
+ </Stack>
113
+ </Button>
114
+ </Menu.Trigger>
115
+ <Menu.Content side="bottom" align="start">
116
+ <Menu.RadioGroup
117
+ value={background}
118
+ onValueChange={(value: string) => onBackgroundChange(value as BackgroundOption)}
119
+ >
153
120
  {BACKGROUND_OPTIONS_UI.map((option) => (
154
- <button
155
- key={option.value}
156
- onClick={(e) => {
157
- e.stopPropagation();
158
- onBackgroundChange(option.value);
159
- setBgOpen(false);
160
- }}
161
- className={clsx(
162
- 'w-full px-3 py-1.5 text-xs text-left flex items-center gap-2',
163
- 'hover:bg-[--bg-hover] transition-colors',
164
- option.value === background ? 'text-[--color-accent] font-medium' : 'text-secondary'
165
- )}
166
- >
167
- <span className="text-sm">{option.icon}</span>
168
- {option.label}
169
- </button>
121
+ <Menu.RadioItem key={option.value} value={option.value}>
122
+ <Stack direction="row" gap="sm" align="center">
123
+ <span style={{ fontSize: '14px' }}>{option.icon}</span>
124
+ {option.label}
125
+ </Stack>
126
+ </Menu.RadioItem>
170
127
  ))}
171
- </div>
172
- )}
173
- </div>
174
- </div>
128
+ </Menu.RadioGroup>
129
+ </Menu.Content>
130
+ </Menu>
131
+ </Stack>
175
132
  );
176
133
  }