@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|