@fragments-sdk/cli 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -0,0 +1,625 @@
1
+ /**
2
+ * A11y HTML Report Generator
3
+ *
4
+ * Generates a standalone HTML accessibility compliance report.
5
+ * No external dependencies - everything is inlined.
6
+ */
7
+
8
+ import { BRAND } from '../core/index.js';
9
+ import type { A11ySummary, A11yComponentResult, A11yScore } from './a11y.js';
10
+ import { calculateA11yScore } from './a11y.js';
11
+
12
+ /**
13
+ * Generate a complete standalone HTML accessibility report from summary data
14
+ */
15
+ export function generateA11yReport(summary: A11ySummary): string {
16
+ const score = summary.score ?? calculateA11yScore(summary);
17
+
18
+ return `<!DOCTYPE html>
19
+ <html lang="en">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>${BRAND.name} Accessibility Report</title>
24
+ <style>
25
+ ${getStyles()}
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="container">
30
+ ${renderHeader(score)}
31
+ ${renderSummaryCards(summary, score)}
32
+ ${renderComponentGrid(summary.components)}
33
+ ${renderFooter()}
34
+ </div>
35
+ <script>
36
+ ${getScripts()}
37
+ </script>
38
+ </body>
39
+ </html>`;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Styles
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function getStyles(): string {
47
+ return `
48
+ :root {
49
+ --bg: #0a0a0a;
50
+ --bg-card: #141414;
51
+ --bg-hover: #1a1a1a;
52
+ --border: #262626;
53
+ --text: #f2f2f2;
54
+ --text-secondary: #a1a1aa;
55
+ --text-muted: #71717a;
56
+ --accent: #10b981;
57
+ --accent-light: #34d399;
58
+ --danger: #ef4444;
59
+ --warning: #eab308;
60
+ --success: #22c55e;
61
+ --radius: 12px;
62
+ }
63
+
64
+ @media (prefers-color-scheme: light) {
65
+ :root:not(.dark) {
66
+ --bg: #f8f8f8;
67
+ --bg-card: #ffffff;
68
+ --bg-hover: #f0f0f0;
69
+ --border: #e0e0e0;
70
+ --text: #171717;
71
+ --text-secondary: #525252;
72
+ --text-muted: #737373;
73
+ }
74
+ }
75
+
76
+ .dark {
77
+ --bg: #0a0a0a;
78
+ --bg-card: #141414;
79
+ --bg-hover: #1a1a1a;
80
+ --border: #262626;
81
+ --text: #f2f2f2;
82
+ --text-secondary: #a1a1aa;
83
+ --text-muted: #71717a;
84
+ }
85
+
86
+ .light {
87
+ --bg: #f8f8f8;
88
+ --bg-card: #ffffff;
89
+ --bg-hover: #f0f0f0;
90
+ --border: #e0e0e0;
91
+ --text: #171717;
92
+ --text-secondary: #525252;
93
+ --text-muted: #737373;
94
+ }
95
+
96
+ * {
97
+ margin: 0;
98
+ padding: 0;
99
+ box-sizing: border-box;
100
+ }
101
+
102
+ body {
103
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
104
+ background: var(--bg);
105
+ color: var(--text);
106
+ line-height: 1.6;
107
+ -webkit-font-smoothing: antialiased;
108
+ }
109
+
110
+ .container {
111
+ max-width: 1200px;
112
+ margin: 0 auto;
113
+ padding: 48px 24px;
114
+ }
115
+
116
+ /* Header */
117
+ .header {
118
+ display: flex;
119
+ justify-content: space-between;
120
+ align-items: flex-start;
121
+ margin-bottom: 48px;
122
+ padding-bottom: 32px;
123
+ border-bottom: 1px solid var(--border);
124
+ }
125
+
126
+ .header-left h1 {
127
+ font-size: 32px;
128
+ font-weight: 700;
129
+ margin-bottom: 8px;
130
+ background: linear-gradient(135deg, var(--text), var(--accent));
131
+ -webkit-background-clip: text;
132
+ -webkit-text-fill-color: transparent;
133
+ }
134
+
135
+ .header-left p {
136
+ color: var(--text-secondary);
137
+ font-size: 14px;
138
+ }
139
+
140
+ .header-right {
141
+ display: flex;
142
+ align-items: flex-start;
143
+ gap: 16px;
144
+ }
145
+
146
+ .theme-toggle {
147
+ background: var(--bg-card);
148
+ border: 1px solid var(--border);
149
+ border-radius: 8px;
150
+ color: var(--text-secondary);
151
+ cursor: pointer;
152
+ padding: 8px 12px;
153
+ font-size: 14px;
154
+ line-height: 1;
155
+ }
156
+
157
+ .theme-toggle:hover {
158
+ border-color: var(--accent);
159
+ color: var(--text);
160
+ }
161
+
162
+ .grade-badge {
163
+ display: flex;
164
+ flex-direction: column;
165
+ align-items: center;
166
+ padding: 24px 32px;
167
+ background: var(--bg-card);
168
+ border: 1px solid var(--border);
169
+ border-radius: var(--radius);
170
+ }
171
+
172
+ .grade-score-large {
173
+ font-size: 56px;
174
+ font-weight: 800;
175
+ line-height: 1;
176
+ }
177
+
178
+ .grade-label {
179
+ font-size: 14px;
180
+ color: var(--text-secondary);
181
+ margin-top: 8px;
182
+ }
183
+
184
+ /* Summary Cards */
185
+ .summary-grid {
186
+ display: grid;
187
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
188
+ gap: 16px;
189
+ margin-bottom: 48px;
190
+ }
191
+
192
+ .summary-card {
193
+ background: var(--bg-card);
194
+ border: 1px solid var(--border);
195
+ border-radius: var(--radius);
196
+ padding: 24px;
197
+ }
198
+
199
+ .summary-card-label {
200
+ font-size: 13px;
201
+ color: var(--text-muted);
202
+ text-transform: uppercase;
203
+ letter-spacing: 0.05em;
204
+ margin-bottom: 8px;
205
+ }
206
+
207
+ .summary-card-value {
208
+ font-size: 36px;
209
+ font-weight: 700;
210
+ }
211
+
212
+ .summary-card-sub {
213
+ font-size: 13px;
214
+ color: var(--text-secondary);
215
+ margin-top: 4px;
216
+ }
217
+
218
+ /* Section */
219
+ .section {
220
+ margin-bottom: 48px;
221
+ }
222
+
223
+ .section-title {
224
+ font-size: 20px;
225
+ font-weight: 600;
226
+ margin-bottom: 24px;
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 12px;
230
+ }
231
+
232
+ .section-title::before {
233
+ content: '';
234
+ width: 4px;
235
+ height: 24px;
236
+ background: var(--accent);
237
+ border-radius: 2px;
238
+ }
239
+
240
+ /* Component Grid */
241
+ .component-grid {
242
+ display: grid;
243
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
244
+ gap: 16px;
245
+ }
246
+
247
+ .component-card {
248
+ background: var(--bg-card);
249
+ border: 1px solid var(--border);
250
+ border-radius: var(--radius);
251
+ padding: 20px;
252
+ transition: border-color 0.2s;
253
+ }
254
+
255
+ .component-card:hover {
256
+ border-color: var(--accent);
257
+ }
258
+
259
+ .component-card-header {
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ margin-bottom: 12px;
264
+ }
265
+
266
+ .component-name {
267
+ font-weight: 600;
268
+ font-size: 16px;
269
+ }
270
+
271
+ .status-badge {
272
+ font-size: 12px;
273
+ font-weight: 700;
274
+ padding: 3px 10px;
275
+ border-radius: 6px;
276
+ text-transform: uppercase;
277
+ letter-spacing: 0.03em;
278
+ }
279
+
280
+ .status-pass { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
281
+ .status-warn { background: rgba(234, 179, 8, 0.15); color: #eab308; }
282
+ .status-fail { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
283
+
284
+ .component-stats {
285
+ display: flex;
286
+ gap: 16px;
287
+ font-size: 13px;
288
+ color: var(--text-secondary);
289
+ margin-bottom: 8px;
290
+ }
291
+
292
+ .severity-bar {
293
+ display: flex;
294
+ gap: 8px;
295
+ flex-wrap: wrap;
296
+ margin-bottom: 12px;
297
+ }
298
+
299
+ .severity-chip {
300
+ font-size: 12px;
301
+ padding: 3px 10px;
302
+ border-radius: 6px;
303
+ }
304
+
305
+ .severity-critical { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
306
+ .severity-serious { background: rgba(239, 68, 68, 0.10); color: #f87171; }
307
+ .severity-moderate { background: rgba(234, 179, 8, 0.15); color: #eab308; }
308
+ .severity-minor { background: rgba(113, 113, 122, 0.15); color: var(--text-muted); }
309
+
310
+ /* Expandable violation details */
311
+ .violation-details {
312
+ margin-top: 8px;
313
+ }
314
+
315
+ .violation-details summary {
316
+ cursor: pointer;
317
+ font-size: 13px;
318
+ color: var(--text-secondary);
319
+ padding: 4px 0;
320
+ user-select: none;
321
+ }
322
+
323
+ .violation-details summary:hover {
324
+ color: var(--text);
325
+ }
326
+
327
+ .violation-details[open] summary {
328
+ margin-bottom: 8px;
329
+ }
330
+
331
+ .violation-list {
332
+ display: flex;
333
+ flex-direction: column;
334
+ gap: 8px;
335
+ }
336
+
337
+ .violation-item {
338
+ background: var(--bg);
339
+ border-radius: 8px;
340
+ padding: 12px;
341
+ font-size: 13px;
342
+ }
343
+
344
+ .violation-item-header {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 8px;
348
+ margin-bottom: 4px;
349
+ }
350
+
351
+ .violation-rule {
352
+ font-weight: 600;
353
+ color: var(--text);
354
+ }
355
+
356
+ .violation-impact {
357
+ font-size: 11px;
358
+ font-weight: 600;
359
+ padding: 1px 6px;
360
+ border-radius: 4px;
361
+ text-transform: uppercase;
362
+ }
363
+
364
+ .violation-description {
365
+ color: var(--text-secondary);
366
+ line-height: 1.5;
367
+ }
368
+
369
+ .violation-help {
370
+ color: var(--accent);
371
+ font-size: 12px;
372
+ margin-top: 4px;
373
+ }
374
+
375
+ /* Footer */
376
+ .footer {
377
+ margin-top: 64px;
378
+ padding-top: 32px;
379
+ border-top: 1px solid var(--border);
380
+ text-align: center;
381
+ color: var(--text-muted);
382
+ font-size: 13px;
383
+ }
384
+
385
+ .footer a {
386
+ color: var(--accent);
387
+ text-decoration: none;
388
+ }
389
+
390
+ /* Responsive */
391
+ @media (max-width: 768px) {
392
+ .header {
393
+ flex-direction: column;
394
+ gap: 24px;
395
+ }
396
+
397
+ .header-right {
398
+ align-self: flex-start;
399
+ }
400
+
401
+ .component-grid {
402
+ grid-template-columns: 1fr;
403
+ }
404
+ }
405
+ `;
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Helpers
410
+ // ---------------------------------------------------------------------------
411
+
412
+ function getScoreColor(score: number): string {
413
+ if (score >= 90) return '#22c55e';
414
+ if (score >= 70) return '#eab308';
415
+ return '#ef4444';
416
+ }
417
+
418
+ function escapeHtml(str: string): string {
419
+ return str
420
+ .replace(/&/g, '&amp;')
421
+ .replace(/</g, '&lt;')
422
+ .replace(/>/g, '&gt;')
423
+ .replace(/"/g, '&quot;');
424
+ }
425
+
426
+ // ---------------------------------------------------------------------------
427
+ // Sections
428
+ // ---------------------------------------------------------------------------
429
+
430
+ function renderHeader(score: A11yScore): string {
431
+ const color = getScoreColor(score.score);
432
+ const now = new Date();
433
+ const timestamp = now.toLocaleDateString('en-US', {
434
+ weekday: 'long',
435
+ year: 'numeric',
436
+ month: 'long',
437
+ day: 'numeric',
438
+ hour: '2-digit',
439
+ minute: '2-digit',
440
+ });
441
+
442
+ return `
443
+ <header class="header">
444
+ <div class="header-left">
445
+ <h1>${BRAND.name} Accessibility Report</h1>
446
+ <p>Generated ${escapeHtml(timestamp)}</p>
447
+ </div>
448
+ <div class="header-right">
449
+ <button class="theme-toggle" id="theme-toggle" aria-label="Toggle light/dark theme">Toggle theme</button>
450
+ <div class="grade-badge">
451
+ <div class="grade-score-large" style="color: ${color}">${score.score}</div>
452
+ <div class="grade-label">/ 100</div>
453
+ </div>
454
+ </div>
455
+ </header>
456
+ `;
457
+ }
458
+
459
+ function renderSummaryCards(summary: A11ySummary, score: A11yScore): string {
460
+ return `
461
+ <div class="summary-grid">
462
+ <div class="summary-card">
463
+ <div class="summary-card-label">Components</div>
464
+ <div class="summary-card-value">${summary.totalComponents}</div>
465
+ <div class="summary-card-sub">${summary.accessibleComponents} accessible (${summary.accessiblePercent}%)</div>
466
+ </div>
467
+ <div class="summary-card">
468
+ <div class="summary-card-label">AA Compliance</div>
469
+ <div class="summary-card-value" style="color: ${getScoreColor(score.aaPercent)}">${score.aaPercent}%</div>
470
+ <div class="summary-card-sub">No critical/serious violations</div>
471
+ </div>
472
+ <div class="summary-card">
473
+ <div class="summary-card-label">AAA Compliance</div>
474
+ <div class="summary-card-value" style="color: ${getScoreColor(score.aaaPercent)}">${score.aaaPercent}%</div>
475
+ <div class="summary-card-sub">Zero violations</div>
476
+ </div>
477
+ <div class="summary-card">
478
+ <div class="summary-card-label">Total Violations</div>
479
+ <div class="summary-card-value" style="color: ${summary.totalViolations === 0 ? '#22c55e' : '#ef4444'}">${summary.totalViolations}</div>
480
+ <div class="summary-card-sub">${summary.totalCritical} critical, ${summary.totalSerious} serious, ${summary.totalModerate} moderate, ${summary.totalMinor} minor</div>
481
+ </div>
482
+ </div>
483
+ `;
484
+ }
485
+
486
+ function renderComponentCard(comp: A11yComponentResult): string {
487
+ const statusClass = comp.status === 'PASS' ? 'status-pass'
488
+ : comp.status === 'WARN' ? 'status-warn'
489
+ : 'status-fail';
490
+
491
+ const variantCount = comp.results.length || 1;
492
+
493
+ // Aggregate severity counts across all variants
494
+ let critical = 0;
495
+ let serious = 0;
496
+ let moderate = 0;
497
+ let minor = 0;
498
+ for (const r of comp.results) {
499
+ critical += r.summary.critical;
500
+ serious += r.summary.serious;
501
+ moderate += r.summary.moderate;
502
+ minor += r.summary.minor;
503
+ }
504
+
505
+ const severityChips: string[] = [];
506
+ if (critical > 0) severityChips.push(`<span class="severity-chip severity-critical">${critical} critical</span>`);
507
+ if (serious > 0) severityChips.push(`<span class="severity-chip severity-serious">${serious} serious</span>`);
508
+ if (moderate > 0) severityChips.push(`<span class="severity-chip severity-moderate">${moderate} moderate</span>`);
509
+ if (minor > 0) severityChips.push(`<span class="severity-chip severity-minor">${minor} minor</span>`);
510
+
511
+ // Build expandable violation details per variant
512
+ let violationDetailsHtml = '';
513
+ const variantsWithViolations = comp.results.filter(r => r.summary.total > 0);
514
+ if (variantsWithViolations.length > 0) {
515
+ const variantItems = variantsWithViolations.map(r => {
516
+ const counts: string[] = [];
517
+ if (r.summary.critical > 0) counts.push(`${r.summary.critical} critical`);
518
+ if (r.summary.serious > 0) counts.push(`${r.summary.serious} serious`);
519
+ if (r.summary.moderate > 0) counts.push(`${r.summary.moderate} moderate`);
520
+ if (r.summary.minor > 0) counts.push(`${r.summary.minor} minor`);
521
+ return `
522
+ <div class="violation-item">
523
+ <div class="violation-item-header">
524
+ <span class="violation-rule">${escapeHtml(r.variant)}</span>
525
+ </div>
526
+ <div class="violation-description">${counts.join(', ')}</div>
527
+ </div>
528
+ `;
529
+ }).join('');
530
+
531
+ violationDetailsHtml = `
532
+ <details class="violation-details">
533
+ <summary>View variant breakdown (${variantsWithViolations.length} variant${variantsWithViolations.length === 1 ? '' : 's'} with issues)</summary>
534
+ <div class="violation-list">
535
+ ${variantItems}
536
+ </div>
537
+ </details>
538
+ `;
539
+ }
540
+
541
+ return `
542
+ <div class="component-card">
543
+ <div class="component-card-header">
544
+ <span class="component-name">${escapeHtml(comp.component)}</span>
545
+ <span class="status-badge ${statusClass}">${comp.status}</span>
546
+ </div>
547
+ <div class="component-stats">
548
+ <span>${variantCount} variant${variantCount === 1 ? '' : 's'}</span>
549
+ <span>${comp.totalViolations} violation${comp.totalViolations === 1 ? '' : 's'}</span>
550
+ </div>
551
+ ${severityChips.length > 0 ? `<div class="severity-bar">${severityChips.join('')}</div>` : ''}
552
+ ${violationDetailsHtml}
553
+ </div>
554
+ `;
555
+ }
556
+
557
+ function renderComponentGrid(components: A11yComponentResult[]): string {
558
+ if (components.length === 0) {
559
+ return `
560
+ <section class="section">
561
+ <h2 class="section-title">Components</h2>
562
+ <p style="color: var(--text-secondary)">No components found.</p>
563
+ </section>
564
+ `;
565
+ }
566
+
567
+ // Sort: FAIL first, then WARN, then PASS
568
+ const order = { FAIL: 0, WARN: 1, PASS: 2 };
569
+ const sorted = [...components].sort((a, b) => order[a.status] - order[b.status]);
570
+
571
+ return `
572
+ <section class="section">
573
+ <h2 class="section-title">Components</h2>
574
+ <div class="component-grid">
575
+ ${sorted.map(renderComponentCard).join('')}
576
+ </div>
577
+ </section>
578
+ `;
579
+ }
580
+
581
+ function renderFooter(): string {
582
+ return `
583
+ <footer class="footer">
584
+ <p>Generated by <a href="#">${BRAND.name}</a> &mdash; AI-first design system documentation</p>
585
+ </footer>
586
+ `;
587
+ }
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // Scripts
591
+ // ---------------------------------------------------------------------------
592
+
593
+ function getScripts(): string {
594
+ return `
595
+ // Theme toggle
596
+ (function() {
597
+ var toggle = document.getElementById('theme-toggle');
598
+ var root = document.documentElement;
599
+
600
+ function setTheme(theme) {
601
+ root.classList.remove('light', 'dark');
602
+ root.classList.add(theme);
603
+ toggle.textContent = theme === 'dark' ? 'Light mode' : 'Dark mode';
604
+ }
605
+
606
+ // Default to dark
607
+ var preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
608
+ setTheme(preferred);
609
+
610
+ toggle.addEventListener('click', function() {
611
+ var current = root.classList.contains('dark') ? 'dark' : 'light';
612
+ setTheme(current === 'dark' ? 'light' : 'dark');
613
+ });
614
+ })();
615
+
616
+ // Animate bars on load
617
+ document.querySelectorAll('.bar-fill, .coverage-bar-fill').forEach(function(el) {
618
+ var width = el.style.width;
619
+ el.style.width = '0';
620
+ setTimeout(function() {
621
+ el.style.width = width;
622
+ }, 100);
623
+ });
624
+ `;
625
+ }