@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.
- package/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /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
|
-
<
|
|
220
|
-
<
|
|
221
|
-
</
|
|
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
|
-
<
|
|
232
|
+
<Stack direction="column" gap="lg" style={{ maxWidth: '672px' }}>
|
|
236
233
|
{/* Header */}
|
|
237
234
|
<div>
|
|
238
|
-
<h1
|
|
239
|
-
<
|
|
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
|
-
</
|
|
238
|
+
</Text>
|
|
242
239
|
</div>
|
|
243
240
|
|
|
244
241
|
{/* Coverage */}
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
</
|
|
264
|
-
|
|
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
|
-
<
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
334
|
-
<div
|
|
335
|
-
<table
|
|
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
|
|
338
|
-
<th
|
|
339
|
-
<th
|
|
340
|
-
<th
|
|
341
|
-
<th
|
|
342
|
-
<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
|
-
|
|
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
|
|
356
|
-
<td
|
|
357
|
-
<td
|
|
358
|
-
<td
|
|
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
|
|
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
|
-
</
|
|
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
|
-
<
|
|
378
|
-
<
|
|
379
|
-
<div
|
|
380
|
-
<
|
|
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
|
-
<
|
|
395
|
+
<Text size="xs" color="tertiary" style={{ width: '40px', textAlign: 'right', flexShrink: 0 }}>
|
|
386
396
|
{metric.count}/{metric.total}
|
|
387
|
-
</
|
|
388
|
-
</
|
|
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 <
|
|
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
|
|
2
|
-
import { HMR_STATUS
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
</
|
|
27
|
+
</span>
|
|
28
|
+
</Badge>
|
|
43
29
|
|
|
44
|
-
{/* Update notification */}
|
|
45
30
|
{lastUpdate && (
|
|
46
|
-
<
|
|
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
|
-
</
|
|
39
|
+
</Text>
|
|
49
40
|
)}
|
|
50
|
-
</
|
|
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
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
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
|
}
|