@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
@@ -7,6 +7,7 @@
7
7
  import { useMemo, useState, useCallback, useEffect } from 'react';
8
8
  import type { SegmentDefinition } from '../../core/index.js';
9
9
  import type { ImpactValue } from 'axe-core';
10
+ import { Badge, Progress, Stack, Text, Card, EmptyState, Table } from '@fragments/ui';
10
11
  import {
11
12
  getAllA11yData,
12
13
  getA11ySummary,
@@ -14,7 +15,6 @@ import {
14
15
  type ComponentA11yData,
15
16
  } from '../hooks/useA11yCache.js';
16
17
  import type { A11ySummary, CachedA11yResult } from '../types/a11y.js';
17
- import { getImpactColorClass } from '../utils/a11y-fixes.js';
18
18
 
19
19
  interface HealthDashboardProps {
20
20
  segments: Array<{ path: string; segment: SegmentDefinition }>;
@@ -70,23 +70,19 @@ function calculateCoverage(
70
70
  const cat = segment.meta.category || 'uncategorized';
71
71
  categories.add(cat);
72
72
 
73
- // Documentation
74
73
  if (segment.meta.description && segment.meta.description.trim().length > 10) {
75
74
  documented++;
76
75
  }
77
76
 
78
- // Variants
79
77
  const variantCount = segment.variants?.length || 0;
80
78
  if (variantCount > 0) {
81
79
  withVariants++;
82
80
  }
83
81
 
84
- // Usage
85
82
  if (segment.usage && (segment.usage.when.length > 0 || segment.usage.whenNot.length > 0)) {
86
83
  withUsage++;
87
84
  }
88
85
 
89
- // Figma
90
86
  if (segment.meta.figma || segment.variants?.some((v) => v.figma)) {
91
87
  figmaLinked++;
92
88
  }
@@ -99,7 +95,6 @@ function calculateCoverage(
99
95
  });
100
96
  }
101
97
 
102
- // Sort components by category then name
103
98
  components.sort((a, b) => {
104
99
  if (a.category !== b.category) return a.category.localeCompare(b.category);
105
100
  return a.name.localeCompare(b.name);
@@ -115,6 +110,16 @@ function calculateCoverage(
115
110
  return { metrics, components, categoryCount: categories.size };
116
111
  }
117
112
 
113
+ function impactToBadgeVariant(impact: ImpactValue | undefined): 'error' | 'warning' | 'info' | 'default' {
114
+ switch (impact) {
115
+ case 'critical': return 'error';
116
+ case 'serious': return 'error';
117
+ case 'moderate': return 'warning';
118
+ case 'minor': return 'info';
119
+ default: return 'default';
120
+ }
121
+ }
122
+
118
123
  export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps) {
119
124
  const { metrics, components, categoryCount } = useMemo(
120
125
  () => calculateCoverage(segments),
@@ -125,7 +130,6 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
125
130
  const [scanningComponents, setScanningComponents] = useState<Set<string>>(new Set());
126
131
  const [a11ySummary, setA11ySummary] = useState<A11ySummary | null>(null);
127
132
 
128
- // Convert cached data to component a11y format
129
133
  const convertCacheToA11y = useCallback((cached: Record<string, ComponentA11yData>): Record<string, ComponentA11yResult> => {
130
134
  const result: Record<string, ComponentA11yResult> = {};
131
135
  for (const [name, data] of Object.entries(cached)) {
@@ -145,16 +149,13 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
145
149
  return result;
146
150
  }, []);
147
151
 
148
- // Load cached a11y data on mount and listen for updates
149
152
  useEffect(() => {
150
- // Load initial cached data
151
153
  const cached = getAllA11yData();
152
154
  if (Object.keys(cached).length > 0) {
153
155
  setComponentA11y(convertCacheToA11y(cached));
154
156
  setA11ySummary(getA11ySummary());
155
157
  }
156
158
 
157
- // Listen for cache updates from AccessibilityPanel
158
159
  const handleCacheUpdate = () => {
159
160
  const updatedCache = getAllA11yData();
160
161
  setComponentA11y(convertCacheToA11y(updatedCache));
@@ -166,12 +167,10 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
166
167
  setA11ySummary(null);
167
168
  };
168
169
 
169
- // Listen for scan started events
170
170
  const handleScanStarted = (event: CustomEvent<{ componentName: string }>) => {
171
171
  setScanningComponents(prev => new Set(prev).add(event.detail.componentName));
172
172
  };
173
173
 
174
- // Listen for scan completed events
175
174
  const handleScanCompleted = (event: CustomEvent<{ componentName: string }>) => {
176
175
  setScanningComponents(prev => {
177
176
  const next = new Set(prev);
@@ -193,7 +192,6 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
193
192
  };
194
193
  }, [convertCacheToA11y]);
195
194
 
196
- // Calculate summary from component results
197
195
  const a11yResults = useMemo((): A11yResults | null => {
198
196
  const scannedComponents = Object.keys(componentA11y).length;
199
197
  if (scannedComponents === 0) {
@@ -216,13 +214,12 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
216
214
 
217
215
  if (segments.length === 0) {
218
216
  return (
219
- <div className="text-center py-12 text-tertiary">
220
- <p className="text-sm">No components loaded</p>
221
- </div>
217
+ <EmptyState>
218
+ <EmptyState.Description>No components loaded</EmptyState.Description>
219
+ </EmptyState>
222
220
  );
223
221
  }
224
222
 
225
- // Create accessibility metric if scan has been run
226
223
  const a11yMetric: CoverageMetric | null = a11yResults
227
224
  ? {
228
225
  label: 'Accessible',
@@ -232,133 +229,149 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
232
229
  : null;
233
230
 
234
231
  return (
235
- <div className="space-y-6 max-w-2xl">
232
+ <Stack direction="column" gap="lg" style={{ maxWidth: '672px' }}>
236
233
  {/* Header */}
237
234
  <div>
238
- <h1 className="text-xl font-semibold text-primary">Fragments</h1>
239
- <p className="text-sm text-tertiary mt-0.5">
235
+ <Text as="h1" size="lg" weight="semibold">Fragments</Text>
236
+ <Text size="sm" color="tertiary" style={{ marginTop: '2px' }}>
240
237
  {segments.length} component{segments.length !== 1 ? 's' : ''} · {categoryCount} categor{categoryCount !== 1 ? 'ies' : 'y'}
241
- </p>
238
+ </Text>
242
239
  </div>
243
240
 
244
241
  {/* Coverage */}
245
- <div className="rounded-lg border border-[--border-subtle] p-4 space-y-3">
246
- <h2 className="text-sm font-medium text-primary">Coverage</h2>
247
- <div className="space-y-2.5">
248
- {metrics.map((metric) => (
249
- <CoverageRow key={metric.label} metric={metric} />
250
- ))}
251
- {/* Accessibility metric */}
252
- {a11yMetric ? (
253
- <CoverageRow metric={a11yMetric} />
254
- ) : (
255
- <div className="flex items-center gap-3">
256
- <span className="text-sm text-secondary w-24 flex-shrink-0">Accessible</span>
257
- <div className="flex-1 h-2 rounded-full bg-[--bg-tertiary] overflow-hidden">
258
- <div className="h-full rounded-full bg-[--bg-tertiary]" style={{ width: '0%' }} />
259
- </div>
260
- <span className="text-xs text-tertiary w-10 text-right flex-shrink-0">-</span>
242
+ <Card>
243
+ <Card.Body>
244
+ <Stack direction="column" gap="sm">
245
+ <Text as="h2" size="sm" weight="medium">Coverage</Text>
246
+ <Stack direction="column" gap="sm">
247
+ {metrics.map((metric) => (
248
+ <CoverageRow key={metric.label} metric={metric} />
249
+ ))}
250
+ {a11yMetric ? (
251
+ <CoverageRow metric={a11yMetric} />
252
+ ) : (
253
+ <Stack direction="row" align="center" gap="sm">
254
+ <Text size="sm" color="secondary" style={{ width: '96px', flexShrink: 0 }}>Accessible</Text>
255
+ <div style={{ flex: 1 }}>
256
+ <Progress value={0} />
257
+ </div>
258
+ <Text size="xs" color="tertiary" style={{ width: '40px', textAlign: 'right', flexShrink: 0 }}>-</Text>
259
+ </Stack>
260
+ )}
261
+ </Stack>
262
+ <div style={{ paddingTop: '8px', borderTop: '1px solid var(--border-subtle)' }}>
263
+ {a11yResults ? (
264
+ a11yResults.totalViolations > 0 ? (
265
+ <Text size="xs" color="tertiary">
266
+ {a11yResults.totalViolations} violation{a11yResults.totalViolations !== 1 ? 's' : ''} found
267
+ ({a11yResults.totalCritical} critical, {a11yResults.totalSerious} serious)
268
+ </Text>
269
+ ) : (
270
+ <Text size="xs" style={{ color: 'var(--color-success)' }}>
271
+ All scanned components pass accessibility checks
272
+ </Text>
273
+ )
274
+ ) : (
275
+ <Text size="xs" color="tertiary">
276
+ Visit components to scan for accessibility issues
277
+ </Text>
278
+ )}
261
279
  </div>
262
- )}
263
- </div>
264
- {/* Show info about a11y data */}
265
- <div className="pt-2 border-t border-[--border-subtle]">
266
- {a11yResults ? (
267
- a11yResults.totalViolations > 0 ? (
268
- <p className="text-xs text-tertiary">
269
- {a11yResults.totalViolations} violation{a11yResults.totalViolations !== 1 ? 's' : ''} found
270
- ({a11yResults.totalCritical} critical, {a11yResults.totalSerious} serious)
271
- </p>
272
- ) : (
273
- <p className="text-xs text-green-600 dark:text-green-400">
274
- All scanned components pass accessibility checks
275
- </p>
276
- )
277
- ) : (
278
- <p className="text-xs text-tertiary">
279
- Visit components to scan for accessibility issues
280
- </p>
281
- )}
282
- </div>
283
- </div>
280
+ </Stack>
281
+ </Card.Body>
282
+ </Card>
284
283
 
285
284
  {/* Top Issues Section */}
286
285
  {a11ySummary && a11ySummary.topViolations.length > 0 && (
287
- <div className="rounded-lg border border-[--border-subtle] p-4 space-y-3">
288
- <h2 className="text-sm font-medium text-primary">Top Issues</h2>
289
- <p className="text-xs text-tertiary">
290
- Common accessibility violations across your components
291
- </p>
292
- <div className="space-y-2">
293
- {a11ySummary.topViolations.map((violation) => {
294
- const impactColors = getImpactColorClass(violation.impact);
295
- return (
296
- <div
297
- key={violation.ruleId}
298
- className="flex items-start gap-2 p-2 rounded bg-[--bg-secondary] border border-[--border-subtle]"
299
- >
300
- {/* Impact badge */}
301
- {violation.impact && (
302
- <span
303
- className={`text-[10px] font-semibold px-1.5 py-0.5 rounded uppercase tracking-wide flex-shrink-0 ${impactColors.bg} ${impactColors.text}`}
304
- >
305
- {violation.impact}
306
- </span>
307
- )}
308
- <div className="flex-1 min-w-0">
309
- <p className="text-xs text-primary truncate">{violation.description}</p>
310
- <p className="text-[10px] text-tertiary mt-0.5">
311
- <span className="font-mono">{violation.ruleId}</span>
312
- {' · '}
313
- {violation.affectedComponents.length} component{violation.affectedComponents.length !== 1 ? 's' : ''}
314
- </p>
315
- </div>
316
- <span className="text-xs text-tertiary flex-shrink-0">
317
- {violation.affectedComponents.length}
318
- </span>
319
- </div>
320
- );
321
- })}
322
- </div>
323
- {a11ySummary.topViolations.length >= 5 && (
324
- <p className="text-[10px] text-tertiary text-center">
325
- Showing top 5 issues
326
- </p>
327
- )}
328
- </div>
286
+ <Card>
287
+ <Card.Body>
288
+ <Stack direction="column" gap="sm">
289
+ <Text as="h2" size="sm" weight="medium">Top Issues</Text>
290
+ <Text size="xs" color="tertiary">
291
+ Common accessibility violations across your components
292
+ </Text>
293
+ <Stack direction="column" gap="sm">
294
+ {a11ySummary.topViolations.map((violation) => (
295
+ <Stack
296
+ key={violation.ruleId}
297
+ direction="row"
298
+ align="start"
299
+ gap="sm"
300
+ style={{
301
+ padding: '8px',
302
+ borderRadius: '4px',
303
+ backgroundColor: 'var(--bg-secondary)',
304
+ border: '1px solid var(--border-subtle)',
305
+ }}
306
+ >
307
+ {violation.impact && (
308
+ <Badge variant={impactToBadgeVariant(violation.impact)} size="sm">
309
+ {violation.impact}
310
+ </Badge>
311
+ )}
312
+ <div style={{ flex: 1, minWidth: 0 }}>
313
+ <Text size="xs" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
314
+ {violation.description}
315
+ </Text>
316
+ <Text size="xs" color="tertiary" style={{ marginTop: '2px' }}>
317
+ <Text as="span" size="xs" font="mono">{violation.ruleId}</Text>
318
+ {' · '}
319
+ {violation.affectedComponents.length} component{violation.affectedComponents.length !== 1 ? 's' : ''}
320
+ </Text>
321
+ </div>
322
+ <Text size="xs" color="tertiary" style={{ flexShrink: 0 }}>
323
+ {violation.affectedComponents.length}
324
+ </Text>
325
+ </Stack>
326
+ ))}
327
+ </Stack>
328
+ {a11ySummary.topViolations.length >= 5 && (
329
+ <Text size="xs" color="tertiary" style={{ textAlign: 'center' }}>
330
+ Showing top 5 issues
331
+ </Text>
332
+ )}
333
+ </Stack>
334
+ </Card.Body>
335
+ </Card>
329
336
  )}
330
337
 
331
338
  {/* Components Table */}
332
339
  <div>
333
- <h2 className="text-sm font-medium text-primary mb-2">Components</h2>
334
- <div className="rounded-lg border border-[--border-subtle] overflow-hidden">
335
- <table className="w-full text-sm">
340
+ <Text as="h2" size="sm" weight="medium" style={{ marginBottom: '8px' }}>Components</Text>
341
+ <div style={{ borderRadius: '8px', border: '1px solid var(--border-subtle)', overflow: 'hidden' }}>
342
+ <table style={{ width: '100%', fontSize: '14px', borderCollapse: 'collapse' }}>
336
343
  <thead>
337
- <tr className="border-b border-[--border-subtle] bg-[--bg-secondary]">
338
- <th className="text-left px-3 py-2 text-xs font-medium text-tertiary">Name</th>
339
- <th className="text-left px-3 py-2 text-xs font-medium text-tertiary">Category</th>
340
- <th className="text-left px-3 py-2 text-xs font-medium text-tertiary">Variants</th>
341
- <th className="text-left px-3 py-2 text-xs font-medium text-tertiary">A11y</th>
342
- <th className="text-left px-3 py-2 text-xs font-medium text-tertiary">Status</th>
344
+ <tr style={{ borderBottom: '1px solid var(--border-subtle)', backgroundColor: 'var(--bg-secondary)' }}>
345
+ <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)' }}>Name</th>
346
+ <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)' }}>Category</th>
347
+ <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)' }}>Variants</th>
348
+ <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)' }}>A11y</th>
349
+ <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: '12px', fontWeight: 500, color: 'var(--text-tertiary)' }}>Status</th>
343
350
  </tr>
344
351
  </thead>
345
352
  <tbody>
346
- {components.map((component) => {
353
+ {components.map((component, index) => {
347
354
  const a11yStatus = componentA11y[component.name];
348
355
  const isScanning = scanningComponents.has(component.name);
349
356
  return (
350
357
  <tr
351
358
  key={component.name}
352
359
  onClick={() => onNavigate?.(component.name)}
353
- className="border-b last:border-b-0 border-[--border-subtle] hover:bg-[--bg-hover] cursor-pointer transition-colors"
360
+ style={{
361
+ borderBottom: index < components.length - 1 ? '1px solid var(--border-subtle)' : 'none',
362
+ cursor: 'pointer',
363
+ transition: 'background-color 150ms',
364
+ }}
365
+ onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--bg-hover)'; }}
366
+ onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = ''; }}
354
367
  >
355
- <td className="px-3 py-2 text-primary font-medium">{component.name}</td>
356
- <td className="px-3 py-2 text-tertiary">{component.category}</td>
357
- <td className="px-3 py-2 text-tertiary">{component.variantCount}</td>
358
- <td className="px-3 py-2">
368
+ <td style={{ padding: '8px 12px' }}><Text weight="medium" size="sm">{component.name}</Text></td>
369
+ <td style={{ padding: '8px 12px' }}><Text size="sm" color="tertiary">{component.category}</Text></td>
370
+ <td style={{ padding: '8px 12px' }}><Text size="sm" color="tertiary">{component.variantCount}</Text></td>
371
+ <td style={{ padding: '8px 12px' }}>
359
372
  <A11yBadge result={a11yStatus} isScanning={isScanning} />
360
373
  </td>
361
- <td className="px-3 py-2 text-tertiary">{component.status}</td>
374
+ <td style={{ padding: '8px 12px' }}><Text size="sm" color="tertiary">{component.status}</Text></td>
362
375
  </tr>
363
376
  );
364
377
  })}
@@ -366,87 +379,49 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
366
379
  </table>
367
380
  </div>
368
381
  </div>
369
- </div>
382
+ </Stack>
370
383
  );
371
384
  }
372
385
 
373
386
  function CoverageRow({ metric }: { metric: CoverageMetric }) {
374
- const percentage = metric.total > 0 ? (metric.count / metric.total) * 100 : 0;
387
+ const percentage = metric.total > 0 ? Math.round((metric.count / metric.total) * 100) : 0;
375
388
 
376
389
  return (
377
- <div className="flex items-center gap-3">
378
- <span className="text-sm text-secondary w-24 flex-shrink-0">{metric.label}</span>
379
- <div className="flex-1 h-2 rounded-full bg-[--bg-tertiary] overflow-hidden">
380
- <div
381
- className="h-full rounded-full bg-[--color-accent] transition-all"
382
- style={{ width: `${percentage}%` }}
383
- />
390
+ <Stack direction="row" align="center" gap="sm">
391
+ <Text size="sm" color="secondary" style={{ width: '96px', flexShrink: 0 }}>{metric.label}</Text>
392
+ <div style={{ flex: 1 }}>
393
+ <Progress value={percentage} />
384
394
  </div>
385
- <span className="text-xs text-tertiary w-10 text-right flex-shrink-0">
395
+ <Text size="xs" color="tertiary" style={{ width: '40px', textAlign: 'right', flexShrink: 0 }}>
386
396
  {metric.count}/{metric.total}
387
- </span>
388
- </div>
397
+ </Text>
398
+ </Stack>
389
399
  );
390
400
  }
391
401
 
392
402
  function A11yBadge({ result, isScanning }: { result?: ComponentA11yResult; isScanning?: boolean }) {
393
- // Show scanning state
394
403
  if (isScanning) {
395
- return (
396
- <span className="inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
397
- <span className="w-2 h-2 border border-current border-t-transparent rounded-full animate-spin" />
398
- <span className="animate-pulse">Scanning</span>
399
- </span>
400
- );
401
- }
402
-
403
- if (!result) {
404
- return <span className="text-xs text-tertiary">-</span>;
404
+ return <Badge variant="warning" size="sm">Scanning</Badge>;
405
405
  }
406
406
 
407
- if (result.status === 'pending') {
408
- return <span className="text-xs text-tertiary">-</span>;
407
+ if (!result || result.status === 'pending') {
408
+ return <Text size="xs" color="tertiary">-</Text>;
409
409
  }
410
410
 
411
411
  if (result.status === 'scanning') {
412
- return (
413
- <span className="inline-flex items-center gap-1 text-xs text-tertiary">
414
- <span className="w-2 h-2 border border-current border-t-transparent rounded-full animate-spin" />
415
- </span>
416
- );
412
+ return <Badge variant="default" size="sm">...</Badge>;
417
413
  }
418
414
 
419
415
  if (result.status === 'pass') {
420
- return (
421
- <span className="inline-flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
422
- <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
423
- <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
424
- </svg>
425
- Pass
426
- </span>
427
- );
416
+ return <Badge variant="success" size="sm">Pass</Badge>;
428
417
  }
429
418
 
430
419
  if (result.status === 'fail') {
431
- return (
432
- <span className="inline-flex items-center gap-1 text-xs text-red-600 dark:text-red-400 font-medium">
433
- <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
434
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
435
- </svg>
436
- {result.violations}
437
- </span>
438
- );
420
+ return <Badge variant="error" size="sm">{result.violations}</Badge>;
439
421
  }
440
422
 
441
423
  // warn status
442
- return (
443
- <span className="inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
444
- <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
445
- <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
446
- </svg>
447
- {result.violations}
448
- </span>
449
- );
424
+ return <Badge variant="warning" size="sm">{result.violations}</Badge>;
450
425
  }
451
426
 
452
427
  export default HealthDashboard;
@@ -1,5 +1,5 @@
1
- import clsx from 'clsx';
2
- import { HMR_STATUS, type HmrStatus } from '../constants/ui.js';
1
+ import { Badge, Stack, Text } from '@fragments/ui';
2
+ import { HMR_STATUS } from '../constants/ui.js';
3
3
  import { useHmrStatus } from '../hooks/useHmrStatus.js';
4
4
  import { WifiIcon, WifiOffIcon } from './Icons.js';
5
5
 
@@ -11,43 +11,34 @@ export function HmrStatusIndicator({ className }: HmrStatusIndicatorProps) {
11
11
  const { status, lastUpdate } = useHmrStatus();
12
12
  const config = HMR_STATUS[status];
13
13
 
14
- return (
15
- <div className={clsx('flex items-center gap-1.5', className)}>
16
- {/* Status dot and label */}
17
- <div
18
- className={clsx(
19
- 'flex items-center gap-1.5 px-2 py-1 rounded-md',
20
- 'text-[10px] font-medium',
21
- status === 'connected' && 'text-emerald-600 dark:text-emerald-400',
22
- status === 'reconnecting' && 'text-amber-600 dark:text-amber-400 animate-pulse',
23
- status === 'disconnected' && 'text-red-600 dark:text-red-400'
24
- )}
25
- title={config.label}
26
- >
27
- {/* Status icon */}
28
- {status === 'disconnected' ? (
29
- <WifiOffIcon className="w-3 h-3" />
30
- ) : (
31
- <WifiIcon className="w-3 h-3" />
32
- )}
14
+ const variant = status === 'connected' ? 'success' as const
15
+ : status === 'disconnected' ? 'error' as const
16
+ : 'warning' as const;
33
17
 
34
- {/* Status dot */}
35
- <span
36
- className={clsx(
37
- 'w-1.5 h-1.5 rounded-full',
38
- config.bg,
39
- status === 'reconnecting' && 'animate-pulse'
18
+ return (
19
+ <Stack direction="row" gap="xs" align="center" className={className}>
20
+ <Badge variant={variant} size="sm" dot>
21
+ <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
22
+ {status === 'disconnected' ? (
23
+ <WifiOffIcon style={{ width: '12px', height: '12px' }} />
24
+ ) : (
25
+ <WifiIcon style={{ width: '12px', height: '12px' }} />
40
26
  )}
41
- />
42
- </div>
27
+ </span>
28
+ </Badge>
43
29
 
44
- {/* Update notification */}
45
30
  {lastUpdate && (
46
- <span className="text-[9px] text-tertiary truncate max-w-[100px]" title={lastUpdate}>
31
+ <Text
32
+ size="2xs"
33
+ color="tertiary"
34
+ truncate
35
+ style={{ maxWidth: '100px' }}
36
+ title={lastUpdate}
37
+ >
47
38
  Updated: {lastUpdate.split('/').pop()}
48
- </span>
39
+ </Text>
49
40
  )}
50
- </div>
41
+ </Stack>
51
42
  );
52
43
  }
53
44
 
@@ -58,14 +49,13 @@ export function HmrStatusDot() {
58
49
  const { status } = useHmrStatus();
59
50
  const config = HMR_STATUS[status];
60
51
 
52
+ const variant = status === 'connected' ? 'success' as const
53
+ : status === 'disconnected' ? 'error' as const
54
+ : 'warning' as const;
55
+
61
56
  return (
62
- <span
63
- className={clsx(
64
- 'w-2 h-2 rounded-full',
65
- config.bg,
66
- status === 'reconnecting' && 'animate-pulse'
67
- )}
68
- title={config.label}
69
- />
57
+ <Badge variant={variant} size="sm" dot title={config.label}>
58
+ <span />
59
+ </Badge>
70
60
  );
71
61
  }