@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Dashboard Component
3
+ * Minimalist overview of design system coverage and components
4
+ * with enhanced accessibility insights and top issues aggregation
5
+ */
6
+
7
+ import { useMemo, useState, useCallback, useEffect } from 'react';
8
+ import type { SegmentDefinition } from '../../core/index.js';
9
+ import type { ImpactValue } from 'axe-core';
10
+ import {
11
+ getAllA11yData,
12
+ getA11ySummary,
13
+ getAllA11yResults,
14
+ type ComponentA11yData,
15
+ } from '../hooks/useA11yCache.js';
16
+ import type { A11ySummary, CachedA11yResult } from '../types/a11y.js';
17
+ import { getImpactColorClass } from '../utils/a11y-fixes.js';
18
+
19
+ interface HealthDashboardProps {
20
+ segments: Array<{ path: string; segment: SegmentDefinition }>;
21
+ onNavigate?: (componentName: string) => void;
22
+ }
23
+
24
+ interface CoverageMetric {
25
+ label: string;
26
+ count: number;
27
+ total: number;
28
+ }
29
+
30
+ interface A11yResults {
31
+ accessibleComponents: number;
32
+ totalComponents: number;
33
+ totalViolations: number;
34
+ totalCritical: number;
35
+ totalSerious: number;
36
+ }
37
+
38
+ interface ComponentA11yResult {
39
+ violations: number;
40
+ critical: number;
41
+ serious: number;
42
+ status: 'pending' | 'scanning' | 'pass' | 'warn' | 'fail';
43
+ }
44
+
45
+ interface ComponentRow {
46
+ name: string;
47
+ category: string;
48
+ variantCount: number;
49
+ status: string;
50
+ }
51
+
52
+ function calculateCoverage(
53
+ segments: Array<{ path: string; segment: SegmentDefinition }>
54
+ ): { metrics: CoverageMetric[]; components: ComponentRow[]; categoryCount: number } {
55
+ const total = segments.length;
56
+
57
+ if (total === 0) {
58
+ return { metrics: [], components: [], categoryCount: 0 };
59
+ }
60
+
61
+ const categories = new Set<string>();
62
+ const components: ComponentRow[] = [];
63
+
64
+ let documented = 0;
65
+ let withVariants = 0;
66
+ let withUsage = 0;
67
+ let figmaLinked = 0;
68
+
69
+ for (const { segment } of segments) {
70
+ const cat = segment.meta.category || 'uncategorized';
71
+ categories.add(cat);
72
+
73
+ // Documentation
74
+ if (segment.meta.description && segment.meta.description.trim().length > 10) {
75
+ documented++;
76
+ }
77
+
78
+ // Variants
79
+ const variantCount = segment.variants?.length || 0;
80
+ if (variantCount > 0) {
81
+ withVariants++;
82
+ }
83
+
84
+ // Usage
85
+ if (segment.usage && (segment.usage.when.length > 0 || segment.usage.whenNot.length > 0)) {
86
+ withUsage++;
87
+ }
88
+
89
+ // Figma
90
+ if (segment.meta.figma || segment.variants?.some((v) => v.figma)) {
91
+ figmaLinked++;
92
+ }
93
+
94
+ components.push({
95
+ name: segment.meta.name,
96
+ category: cat,
97
+ variantCount,
98
+ status: segment.meta.status || 'stable',
99
+ });
100
+ }
101
+
102
+ // Sort components by category then name
103
+ components.sort((a, b) => {
104
+ if (a.category !== b.category) return a.category.localeCompare(b.category);
105
+ return a.name.localeCompare(b.name);
106
+ });
107
+
108
+ const metrics: CoverageMetric[] = [
109
+ { label: 'Documented', count: documented, total },
110
+ { label: 'Variants', count: withVariants, total },
111
+ { label: 'Usage', count: withUsage, total },
112
+ { label: 'Figma', count: figmaLinked, total },
113
+ ];
114
+
115
+ return { metrics, components, categoryCount: categories.size };
116
+ }
117
+
118
+ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps) {
119
+ const { metrics, components, categoryCount } = useMemo(
120
+ () => calculateCoverage(segments),
121
+ [segments]
122
+ );
123
+
124
+ const [componentA11y, setComponentA11y] = useState<Record<string, ComponentA11yResult>>({});
125
+ const [scanningComponents, setScanningComponents] = useState<Set<string>>(new Set());
126
+ const [a11ySummary, setA11ySummary] = useState<A11ySummary | null>(null);
127
+
128
+ // Convert cached data to component a11y format
129
+ const convertCacheToA11y = useCallback((cached: Record<string, ComponentA11yData>): Record<string, ComponentA11yResult> => {
130
+ const result: Record<string, ComponentA11yResult> = {};
131
+ for (const [name, data] of Object.entries(cached)) {
132
+ let status: ComponentA11yResult['status'] = 'pass';
133
+ if (data.critical > 0 || data.serious > 0) {
134
+ status = 'fail';
135
+ } else if (data.violations > 0) {
136
+ status = 'warn';
137
+ }
138
+ result[name] = {
139
+ violations: data.violations,
140
+ critical: data.critical,
141
+ serious: data.serious,
142
+ status,
143
+ };
144
+ }
145
+ return result;
146
+ }, []);
147
+
148
+ // Load cached a11y data on mount and listen for updates
149
+ useEffect(() => {
150
+ // Load initial cached data
151
+ const cached = getAllA11yData();
152
+ if (Object.keys(cached).length > 0) {
153
+ setComponentA11y(convertCacheToA11y(cached));
154
+ setA11ySummary(getA11ySummary());
155
+ }
156
+
157
+ // Listen for cache updates from AccessibilityPanel
158
+ const handleCacheUpdate = () => {
159
+ const updatedCache = getAllA11yData();
160
+ setComponentA11y(convertCacheToA11y(updatedCache));
161
+ setA11ySummary(getA11ySummary());
162
+ };
163
+
164
+ const handleCacheCleared = () => {
165
+ setComponentA11y({});
166
+ setA11ySummary(null);
167
+ };
168
+
169
+ // Listen for scan started events
170
+ const handleScanStarted = (event: CustomEvent<{ componentName: string }>) => {
171
+ setScanningComponents(prev => new Set(prev).add(event.detail.componentName));
172
+ };
173
+
174
+ // Listen for scan completed events
175
+ const handleScanCompleted = (event: CustomEvent<{ componentName: string }>) => {
176
+ setScanningComponents(prev => {
177
+ const next = new Set(prev);
178
+ next.delete(event.detail.componentName);
179
+ return next;
180
+ });
181
+ };
182
+
183
+ window.addEventListener('a11y-cache-updated', handleCacheUpdate);
184
+ window.addEventListener('a11y-cache-cleared', handleCacheCleared);
185
+ window.addEventListener('a11y-scan-started', handleScanStarted as EventListener);
186
+ window.addEventListener('a11y-scan-completed', handleScanCompleted as EventListener);
187
+
188
+ return () => {
189
+ window.removeEventListener('a11y-cache-updated', handleCacheUpdate);
190
+ window.removeEventListener('a11y-cache-cleared', handleCacheCleared);
191
+ window.removeEventListener('a11y-scan-started', handleScanStarted as EventListener);
192
+ window.removeEventListener('a11y-scan-completed', handleScanCompleted as EventListener);
193
+ };
194
+ }, [convertCacheToA11y]);
195
+
196
+ // Calculate summary from component results
197
+ const a11yResults = useMemo((): A11yResults | null => {
198
+ const scannedComponents = Object.keys(componentA11y).length;
199
+ if (scannedComponents === 0) {
200
+ return null;
201
+ }
202
+
203
+ const summary = a11ySummary || getA11ySummary();
204
+ return {
205
+ accessibleComponents: summary.accessibleComponents,
206
+ totalComponents: summary.totalComponents,
207
+ totalViolations:
208
+ summary.violationsByImpact.critical +
209
+ summary.violationsByImpact.serious +
210
+ summary.violationsByImpact.moderate +
211
+ summary.violationsByImpact.minor,
212
+ totalCritical: summary.violationsByImpact.critical,
213
+ totalSerious: summary.violationsByImpact.serious,
214
+ };
215
+ }, [componentA11y, a11ySummary]);
216
+
217
+ if (segments.length === 0) {
218
+ return (
219
+ <div className="text-center py-12 text-tertiary">
220
+ <p className="text-sm">No components loaded</p>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ // Create accessibility metric if scan has been run
226
+ const a11yMetric: CoverageMetric | null = a11yResults
227
+ ? {
228
+ label: 'Accessible',
229
+ count: a11yResults.accessibleComponents,
230
+ total: a11yResults.totalComponents,
231
+ }
232
+ : null;
233
+
234
+ return (
235
+ <div className="space-y-6 max-w-2xl">
236
+ {/* Header */}
237
+ <div>
238
+ <h1 className="text-xl font-semibold text-primary">Fragments</h1>
239
+ <p className="text-sm text-tertiary mt-0.5">
240
+ {segments.length} component{segments.length !== 1 ? 's' : ''} · {categoryCount} categor{categoryCount !== 1 ? 'ies' : 'y'}
241
+ </p>
242
+ </div>
243
+
244
+ {/* 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>
261
+ </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>
284
+
285
+ {/* Top Issues Section */}
286
+ {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>
329
+ )}
330
+
331
+ {/* Components Table */}
332
+ <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">
336
+ <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>
343
+ </tr>
344
+ </thead>
345
+ <tbody>
346
+ {components.map((component) => {
347
+ const a11yStatus = componentA11y[component.name];
348
+ const isScanning = scanningComponents.has(component.name);
349
+ return (
350
+ <tr
351
+ key={component.name}
352
+ onClick={() => onNavigate?.(component.name)}
353
+ className="border-b last:border-b-0 border-[--border-subtle] hover:bg-[--bg-hover] cursor-pointer transition-colors"
354
+ >
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">
359
+ <A11yBadge result={a11yStatus} isScanning={isScanning} />
360
+ </td>
361
+ <td className="px-3 py-2 text-tertiary">{component.status}</td>
362
+ </tr>
363
+ );
364
+ })}
365
+ </tbody>
366
+ </table>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ );
371
+ }
372
+
373
+ function CoverageRow({ metric }: { metric: CoverageMetric }) {
374
+ const percentage = metric.total > 0 ? (metric.count / metric.total) * 100 : 0;
375
+
376
+ 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
+ />
384
+ </div>
385
+ <span className="text-xs text-tertiary w-10 text-right flex-shrink-0">
386
+ {metric.count}/{metric.total}
387
+ </span>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ function A11yBadge({ result, isScanning }: { result?: ComponentA11yResult; isScanning?: boolean }) {
393
+ // Show scanning state
394
+ 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>;
405
+ }
406
+
407
+ if (result.status === 'pending') {
408
+ return <span className="text-xs text-tertiary">-</span>;
409
+ }
410
+
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
+ );
417
+ }
418
+
419
+ 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
+ );
428
+ }
429
+
430
+ 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
+ );
439
+ }
440
+
441
+ // 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
+ );
450
+ }
451
+
452
+ export default HealthDashboard;
@@ -0,0 +1,71 @@
1
+ import clsx from 'clsx';
2
+ import { HMR_STATUS, type HmrStatus } from '../constants/ui.js';
3
+ import { useHmrStatus } from '../hooks/useHmrStatus.js';
4
+ import { WifiIcon, WifiOffIcon } from './Icons.js';
5
+
6
+ interface HmrStatusIndicatorProps {
7
+ className?: string;
8
+ }
9
+
10
+ export function HmrStatusIndicator({ className }: HmrStatusIndicatorProps) {
11
+ const { status, lastUpdate } = useHmrStatus();
12
+ const config = HMR_STATUS[status];
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
+ )}
33
+
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'
40
+ )}
41
+ />
42
+ </div>
43
+
44
+ {/* Update notification */}
45
+ {lastUpdate && (
46
+ <span className="text-[9px] text-tertiary truncate max-w-[100px]" title={lastUpdate}>
47
+ Updated: {lastUpdate.split('/').pop()}
48
+ </span>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Compact version that just shows the status dot.
56
+ */
57
+ export function HmrStatusDot() {
58
+ const { status } = useHmrStatus();
59
+ const config = HMR_STATUS[status];
60
+
61
+ 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
+ />
70
+ );
71
+ }