@fragments-sdk/cli 0.5.2 → 0.6.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 (118) hide show
  1. package/dist/bin.js +712 -39
  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-U4GQ2JTD.js → chunk-D35RGPAG.js} +412 -35
  6. package/dist/chunk-D35RGPAG.js.map +1 -0
  7. package/dist/{chunk-XNWDI6UT.js → chunk-F7ITZPDJ.js} +5 -5
  8. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  9. package/dist/{chunk-V7YLRR4C.js → chunk-Q7GOHVOK.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-2H2JAA3U.js → chunk-SSLQXHNX.js} +3 -3
  12. package/dist/{core-DKHB7FYV.js → core-SKRPJQZG.js} +4 -4
  13. package/dist/{generate-KL24VZVD.js → generate-7AF7WRVK.js} +5 -5
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +15 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/{init-NION5S3M.js → init-WKGDPYI4.js} +5 -5
  18. package/dist/mcp-bin.js +8 -220
  19. package/dist/mcp-bin.js.map +1 -1
  20. package/dist/scan-K6JNMCGM.js +12 -0
  21. package/dist/{service-RWUMZ3EW.js → service-F3E4JJM7.js} +5 -5
  22. package/dist/static-viewer-4LQZ5AGA.js +12 -0
  23. package/dist/{test-ECPEXFDN.js → test-CJDNJTPZ.js} +4 -4
  24. package/dist/{tokens-ITADYVPF.js → tokens-JAJABYXP.js} +6 -6
  25. package/dist/viewer-R3Q6WAMJ.js +1822 -0
  26. package/dist/viewer-R3Q6WAMJ.js.map +1 -0
  27. package/package.json +5 -4
  28. package/src/bin.ts +8 -0
  29. package/src/build.ts +104 -13
  30. package/src/cli-commands.ts +18 -0
  31. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  32. package/src/commands/a11y-report.ts +625 -0
  33. package/src/commands/a11y.ts +168 -14
  34. package/src/commands/build.ts +16 -0
  35. package/src/core/auto-props.ts +464 -0
  36. package/src/core/schema.ts +2 -0
  37. package/src/core/types.ts +3 -1
  38. package/src/index.ts +4 -0
  39. package/src/mcp/server.ts +13 -220
  40. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  41. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  42. package/src/theme/contrast.test.ts +331 -0
  43. package/src/theme/contrast.ts +246 -0
  44. package/src/theme/generator.ts +213 -1
  45. package/src/theme/index.ts +16 -0
  46. package/src/theme/types.ts +51 -0
  47. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  48. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  49. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  50. package/src/viewer/components/ActionCapture.tsx +1 -1
  51. package/src/viewer/components/ActionsPanel.tsx +142 -183
  52. package/src/viewer/components/App.tsx +159 -164
  53. package/src/viewer/components/BottomPanel.tsx +40 -80
  54. package/src/viewer/components/CodePanel.tsx +9 -87
  55. package/src/viewer/components/CommandPalette.tsx +117 -74
  56. package/src/viewer/components/ComponentGraph.tsx +143 -126
  57. package/src/viewer/components/ComponentHeader.tsx +46 -43
  58. package/src/viewer/components/ContractPanel.tsx +124 -117
  59. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  60. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  61. package/src/viewer/components/FragmentEditor.tsx +126 -63
  62. package/src/viewer/components/HealthDashboard.tsx +146 -171
  63. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  64. package/src/viewer/components/Icons.tsx +99 -98
  65. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  66. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  67. package/src/viewer/components/IsolatedRender.tsx +12 -6
  68. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  69. package/src/viewer/components/LandingPage.tsx +285 -305
  70. package/src/viewer/components/Layout.tsx +7 -9
  71. package/src/viewer/components/LeftSidebar.tsx +78 -108
  72. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  73. package/src/viewer/components/PreviewArea.tsx +113 -44
  74. package/src/viewer/components/PreviewFrameHost.tsx +6 -5
  75. package/src/viewer/components/PreviewPane.tsx +2 -3
  76. package/src/viewer/components/PreviewToolbar.tsx +61 -104
  77. package/src/viewer/components/PropsEditor.tsx +154 -74
  78. package/src/viewer/components/PropsTable.tsx +95 -82
  79. package/src/viewer/components/RelationsSection.tsx +71 -40
  80. package/src/viewer/components/ResizablePanel.tsx +158 -55
  81. package/src/viewer/components/RightSidebar.tsx +46 -56
  82. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  83. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  84. package/src/viewer/components/StoryRenderer.tsx +4 -11
  85. package/src/viewer/components/Toast.tsx +3 -67
  86. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  87. package/src/viewer/components/UsageSection.tsx +26 -26
  88. package/src/viewer/components/VariantMatrix.tsx +140 -47
  89. package/src/viewer/components/VariantTabs.tsx +24 -68
  90. package/src/viewer/components/ViewportSelector.tsx +106 -110
  91. package/src/viewer/constants/ui.ts +19 -18
  92. package/src/viewer/entry.tsx +8 -3
  93. package/src/viewer/index.ts +3 -6
  94. package/src/viewer/preview-frame.html +21 -5
  95. package/src/viewer/server.ts +7 -16
  96. package/src/viewer/styles/globals.css +4 -4
  97. package/src/viewer/utils/a11y-fixes.ts +53 -30
  98. package/dist/chunk-ICAIQ57V.js.map +0 -1
  99. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  100. package/dist/scan-ESEXV7LF.js +0 -12
  101. package/dist/static-viewer-O37MJ5B6.js +0 -12
  102. package/dist/viewer-YDGFDTK5.js +0 -11104
  103. package/dist/viewer-YDGFDTK5.js.map +0 -1
  104. package/src/viewer/postcss.config.js +0 -6
  105. package/src/viewer/tailwind.config.js +0 -37
  106. /package/dist/{chunk-XNWDI6UT.js.map → chunk-F7ITZPDJ.js.map} +0 -0
  107. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  108. /package/dist/{chunk-V7YLRR4C.js.map → chunk-Q7GOHVOK.js.map} +0 -0
  109. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  110. /package/dist/{chunk-2H2JAA3U.js.map → chunk-SSLQXHNX.js.map} +0 -0
  111. /package/dist/{core-DKHB7FYV.js.map → core-SKRPJQZG.js.map} +0 -0
  112. /package/dist/{generate-KL24VZVD.js.map → generate-7AF7WRVK.js.map} +0 -0
  113. /package/dist/{init-NION5S3M.js.map → init-WKGDPYI4.js.map} +0 -0
  114. /package/dist/{scan-ESEXV7LF.js.map → scan-K6JNMCGM.js.map} +0 -0
  115. /package/dist/{service-RWUMZ3EW.js.map → service-F3E4JJM7.js.map} +0 -0
  116. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-4LQZ5AGA.js.map} +0 -0
  117. /package/dist/{test-ECPEXFDN.js.map → test-CJDNJTPZ.js.map} +0 -0
  118. /package/dist/{tokens-ITADYVPF.js.map → tokens-JAJABYXP.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
+ }