@ytspar/devbar 1.7.5 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/GlobalDevBar.d.ts +4 -2
  2. package/dist/GlobalDevBar.d.ts.map +1 -1
  3. package/dist/GlobalDevBar.js +22 -9
  4. package/dist/GlobalDevBar.js.map +1 -1
  5. package/dist/accessibility.js +2 -2
  6. package/dist/accessibility.js.map +1 -1
  7. package/dist/constants.d.ts +9 -1
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +13 -1
  10. package/dist/constants.js.map +1 -1
  11. package/dist/debug.js.map +1 -1
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/modules/index.d.ts +2 -2
  17. package/dist/modules/index.d.ts.map +1 -1
  18. package/dist/modules/index.js +2 -2
  19. package/dist/modules/index.js.map +1 -1
  20. package/dist/modules/rendering/buttons.d.ts.map +1 -1
  21. package/dist/modules/rendering/buttons.js +3 -3
  22. package/dist/modules/rendering/buttons.js.map +1 -1
  23. package/dist/modules/rendering/collapsed.js +2 -2
  24. package/dist/modules/rendering/collapsed.js.map +1 -1
  25. package/dist/modules/rendering/common.d.ts +9 -0
  26. package/dist/modules/rendering/common.d.ts.map +1 -1
  27. package/dist/modules/rendering/common.js +52 -1
  28. package/dist/modules/rendering/common.js.map +1 -1
  29. package/dist/modules/rendering/compact.d.ts +2 -1
  30. package/dist/modules/rendering/compact.d.ts.map +1 -1
  31. package/dist/modules/rendering/compact.js +11 -4
  32. package/dist/modules/rendering/compact.js.map +1 -1
  33. package/dist/modules/rendering/console.d.ts.map +1 -1
  34. package/dist/modules/rendering/console.js +6 -2
  35. package/dist/modules/rendering/console.js.map +1 -1
  36. package/dist/modules/rendering/expanded.d.ts.map +1 -1
  37. package/dist/modules/rendering/expanded.js +3 -54
  38. package/dist/modules/rendering/expanded.js.map +1 -1
  39. package/dist/modules/rendering/index.d.ts.map +1 -1
  40. package/dist/modules/rendering/index.js +3 -3
  41. package/dist/modules/rendering/index.js.map +1 -1
  42. package/dist/modules/rendering/modals.d.ts.map +1 -1
  43. package/dist/modules/rendering/modals.js +264 -252
  44. package/dist/modules/rendering/modals.js.map +1 -1
  45. package/dist/modules/rendering/settings.d.ts.map +1 -1
  46. package/dist/modules/rendering/settings.js +41 -34
  47. package/dist/modules/rendering/settings.js.map +1 -1
  48. package/dist/modules/ruler.d.ts.map +1 -1
  49. package/dist/modules/ruler.js.map +1 -1
  50. package/dist/modules/screenshot.d.ts.map +1 -1
  51. package/dist/modules/screenshot.js +23 -5
  52. package/dist/modules/screenshot.js.map +1 -1
  53. package/dist/modules/theme.d.ts.map +1 -1
  54. package/dist/modules/theme.js +1 -1
  55. package/dist/modules/theme.js.map +1 -1
  56. package/dist/modules/tooltips.d.ts.map +1 -1
  57. package/dist/modules/tooltips.js +17 -5
  58. package/dist/modules/tooltips.js.map +1 -1
  59. package/dist/modules/types.d.ts +1 -1
  60. package/dist/modules/types.d.ts.map +1 -1
  61. package/dist/modules/types.js.map +1 -1
  62. package/dist/modules/websocket.d.ts.map +1 -1
  63. package/dist/modules/websocket.js +43 -8
  64. package/dist/modules/websocket.js.map +1 -1
  65. package/dist/network.js +1 -1
  66. package/dist/network.js.map +1 -1
  67. package/dist/storage.d.ts.map +1 -1
  68. package/dist/storage.js +2 -11
  69. package/dist/storage.js.map +1 -1
  70. package/dist/ui/cards.js +1 -1
  71. package/dist/ui/cards.js.map +1 -1
  72. package/dist/ui/index.d.ts +1 -1
  73. package/dist/ui/index.d.ts.map +1 -1
  74. package/dist/ui/index.js +1 -1
  75. package/dist/ui/index.js.map +1 -1
  76. package/dist/ui/modals.d.ts.map +1 -1
  77. package/dist/ui/modals.js +3 -3
  78. package/dist/ui/modals.js.map +1 -1
  79. package/dist/utils.d.ts +2 -0
  80. package/dist/utils.d.ts.map +1 -1
  81. package/dist/utils.js +10 -0
  82. package/dist/utils.js.map +1 -1
  83. package/package.json +11 -10
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Modal rendering for the DevBar: outline, schema, a11y, and design review modals.
3
3
  */
4
- import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, withAlpha } from '../../constants.js';
4
+ import { a11yToMarkdown, getImpactColor, getViolationCounts, groupViolationsByImpact, runA11yAudit, } from '../../accessibility.js';
5
+ import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, withAlpha, } from '../../constants.js';
5
6
  import { extractDocumentOutline, outlineToMarkdown } from '../../outline.js';
6
- import { checkMissingTags, extractFavicons, extractPageSchema, isImageKey, schemaToMarkdown } from '../../schema.js';
7
+ import { checkMissingTags, extractFavicons, extractPageSchema, isImageKey, schemaToMarkdown, } from '../../schema.js';
7
8
  import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createStyledButton, } from '../../ui/index.js';
8
- import { a11yToMarkdown, runA11yAudit, groupViolationsByImpact, getImpactColor, getViolationCounts, } from '../../accessibility.js';
9
9
  import { calculateCostEstimate, closeDesignReviewConfirm, handleSaveA11yAudit, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, } from '../screenshot.js';
10
10
  import { clearChildren } from './common.js';
11
11
  // ============================================================================
@@ -91,7 +91,7 @@ function renderOutlineNodes(nodes, parentEl, depth, headingTracker) {
91
91
  }
92
92
  const textSpan = document.createElement('span');
93
93
  Object.assign(textSpan.style, {
94
- color: '#d1d5db',
94
+ color: CSS_COLORS.textSecondary,
95
95
  fontSize: '0.6875rem',
96
96
  marginLeft: '8px',
97
97
  });
@@ -375,7 +375,9 @@ function renderKeyValueItems(container, items) {
375
375
  });
376
376
  thumb.src = value;
377
377
  thumb.alt = key;
378
- thumb.onerror = () => { frame.style.display = 'none'; };
378
+ thumb.onerror = () => {
379
+ frame.style.display = 'none';
380
+ };
379
381
  thumb.onload = () => {
380
382
  if (thumb.naturalWidth) {
381
383
  dimEl.textContent = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
@@ -434,139 +436,180 @@ function faviconDevice(label, size) {
434
436
  return { text: 'Browser tab', color: CSS_COLORS.textSecondary };
435
437
  return { text: 'General', color: CSS_COLORS.textMuted };
436
438
  }
439
+ /** Create a single favicon card row with thumbnail, label, device pill, dimensions, and URL. */
440
+ function createFaviconCard(icon, index) {
441
+ const device = faviconDevice(icon.label, icon.size);
442
+ const row = document.createElement('div');
443
+ Object.assign(row.style, {
444
+ display: 'flex',
445
+ alignItems: 'center',
446
+ padding: '6px 8px',
447
+ gap: '10px',
448
+ borderRadius: '3px',
449
+ backgroundColor: index % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
450
+ });
451
+ // Thumbnail frame
452
+ const frame = document.createElement('div');
453
+ Object.assign(frame.style, {
454
+ width: '32px',
455
+ height: '32px',
456
+ display: 'flex',
457
+ alignItems: 'center',
458
+ justifyContent: 'center',
459
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
460
+ border: '1px solid rgba(255, 255, 255, 0.06)',
461
+ borderRadius: '4px',
462
+ flexShrink: '0',
463
+ });
464
+ const thumb = document.createElement('img');
465
+ Object.assign(thumb.style, {
466
+ width: '22px',
467
+ height: '22px',
468
+ objectFit: 'contain',
469
+ });
470
+ thumb.src = icon.url;
471
+ thumb.alt = icon.label;
472
+ thumb.onerror = () => {
473
+ frame.style.opacity = '0.3';
474
+ };
475
+ frame.appendChild(thumb);
476
+ row.appendChild(frame);
477
+ // Info column: label, device, dimensions + URL
478
+ const infoCol = document.createElement('div');
479
+ Object.assign(infoCol.style, {
480
+ flex: '1',
481
+ minWidth: '0',
482
+ display: 'flex',
483
+ flexDirection: 'column',
484
+ gap: '2px',
485
+ });
486
+ // Top row: label + device pill
487
+ const topRow = document.createElement('div');
488
+ Object.assign(topRow.style, {
489
+ display: 'flex',
490
+ alignItems: 'center',
491
+ gap: '6px',
492
+ });
493
+ const labelEl = document.createElement('span');
494
+ Object.assign(labelEl.style, {
495
+ color: CSS_COLORS.text,
496
+ fontSize: '0.6875rem',
497
+ fontWeight: '500',
498
+ overflow: 'hidden',
499
+ textOverflow: 'ellipsis',
500
+ whiteSpace: 'nowrap',
501
+ });
502
+ labelEl.textContent = icon.label;
503
+ if (icon.label.length > 24)
504
+ labelEl.title = icon.label;
505
+ topRow.appendChild(labelEl);
506
+ const devicePill = document.createElement('span');
507
+ Object.assign(devicePill.style, {
508
+ color: device.color,
509
+ fontSize: '0.5rem',
510
+ backgroundColor: withAlpha(device.color, 7),
511
+ padding: '1px 6px',
512
+ borderRadius: '6px',
513
+ letterSpacing: '0.03em',
514
+ whiteSpace: 'nowrap',
515
+ flexShrink: '0',
516
+ });
517
+ devicePill.textContent = device.text;
518
+ topRow.appendChild(devicePill);
519
+ infoCol.appendChild(topRow);
520
+ // Bottom row: declared size + actual dimensions + URL
521
+ const bottomRow = document.createElement('div');
522
+ Object.assign(bottomRow.style, {
523
+ display: 'flex',
524
+ alignItems: 'center',
525
+ gap: '6px',
526
+ fontSize: '0.5625rem',
527
+ color: CSS_COLORS.textMuted,
528
+ });
529
+ if (icon.size) {
530
+ const declaredEl = document.createElement('span');
531
+ declaredEl.textContent = icon.size;
532
+ declaredEl.style.opacity = '0.8';
533
+ bottomRow.appendChild(declaredEl);
534
+ }
535
+ // Actual dimensions (populated on load)
536
+ const dimEl = document.createElement('span');
537
+ dimEl.style.letterSpacing = '0.02em';
538
+ bottomRow.appendChild(dimEl);
539
+ thumb.onload = () => {
540
+ if (thumb.naturalWidth) {
541
+ const actual = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
542
+ if (icon.size) {
543
+ dimEl.textContent = `\u2192 ${actual}`;
544
+ }
545
+ else {
546
+ dimEl.textContent = actual;
547
+ }
548
+ }
549
+ };
550
+ const sep = document.createElement('span');
551
+ sep.textContent = '\u00b7';
552
+ sep.style.opacity = '0.4';
553
+ bottomRow.appendChild(sep);
554
+ const urlEl = document.createElement('span');
555
+ Object.assign(urlEl.style, {
556
+ overflow: 'hidden',
557
+ textOverflow: 'ellipsis',
558
+ whiteSpace: 'nowrap',
559
+ opacity: '0.6',
560
+ });
561
+ urlEl.textContent = icon.url;
562
+ urlEl.title = icon.url;
563
+ bottomRow.appendChild(urlEl);
564
+ infoCol.appendChild(bottomRow);
565
+ row.appendChild(infoCol);
566
+ return row;
567
+ }
437
568
  function renderFaviconsSection(container, icons) {
438
569
  const color = CSS_COLORS.purple;
439
570
  const section = document.createElement('div');
440
571
  section.style.marginBottom = '20px';
441
572
  renderSchemaSectionHeader(section, 'Favicons', color, icons.length);
442
573
  icons.forEach((icon, i) => {
443
- const device = faviconDevice(icon.label, icon.size);
444
- const row = document.createElement('div');
445
- Object.assign(row.style, {
446
- display: 'flex',
447
- alignItems: 'center',
448
- padding: '6px 8px',
449
- gap: '10px',
450
- borderRadius: '3px',
451
- backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
452
- });
453
- // Thumbnail frame
454
- const frame = document.createElement('div');
455
- Object.assign(frame.style, {
456
- width: '32px',
457
- height: '32px',
458
- display: 'flex',
459
- alignItems: 'center',
460
- justifyContent: 'center',
461
- backgroundColor: 'rgba(0, 0, 0, 0.25)',
462
- border: '1px solid rgba(255, 255, 255, 0.06)',
463
- borderRadius: '4px',
464
- flexShrink: '0',
465
- });
466
- const thumb = document.createElement('img');
467
- Object.assign(thumb.style, {
468
- width: '22px',
469
- height: '22px',
470
- objectFit: 'contain',
471
- });
472
- thumb.src = icon.url;
473
- thumb.alt = icon.label;
474
- thumb.onerror = () => { frame.style.opacity = '0.3'; };
475
- frame.appendChild(thumb);
476
- row.appendChild(frame);
477
- // Info column: label, device, dimensions + URL
478
- const infoCol = document.createElement('div');
479
- Object.assign(infoCol.style, {
480
- flex: '1',
481
- minWidth: '0',
482
- display: 'flex',
483
- flexDirection: 'column',
484
- gap: '2px',
485
- });
486
- // Top row: label + device pill
487
- const topRow = document.createElement('div');
488
- Object.assign(topRow.style, {
489
- display: 'flex',
490
- alignItems: 'center',
491
- gap: '6px',
492
- });
493
- const labelEl = document.createElement('span');
494
- Object.assign(labelEl.style, {
495
- color: CSS_COLORS.text,
496
- fontSize: '0.6875rem',
497
- fontWeight: '500',
498
- overflow: 'hidden',
499
- textOverflow: 'ellipsis',
500
- whiteSpace: 'nowrap',
501
- });
502
- labelEl.textContent = icon.label;
503
- if (icon.label.length > 24)
504
- labelEl.title = icon.label;
505
- topRow.appendChild(labelEl);
506
- const devicePill = document.createElement('span');
507
- Object.assign(devicePill.style, {
508
- color: device.color,
509
- fontSize: '0.5rem',
510
- backgroundColor: withAlpha(device.color, 7),
511
- padding: '1px 6px',
512
- borderRadius: '6px',
513
- letterSpacing: '0.03em',
514
- whiteSpace: 'nowrap',
515
- flexShrink: '0',
516
- });
517
- devicePill.textContent = device.text;
518
- topRow.appendChild(devicePill);
519
- infoCol.appendChild(topRow);
520
- // Bottom row: declared size + actual dimensions + URL
521
- const bottomRow = document.createElement('div');
522
- Object.assign(bottomRow.style, {
523
- display: 'flex',
524
- alignItems: 'center',
525
- gap: '6px',
526
- fontSize: '0.5625rem',
527
- color: CSS_COLORS.textMuted,
528
- });
529
- if (icon.size) {
530
- const declaredEl = document.createElement('span');
531
- declaredEl.textContent = icon.size;
532
- declaredEl.style.opacity = '0.8';
533
- bottomRow.appendChild(declaredEl);
534
- }
535
- // Actual dimensions (populated on load)
536
- const dimEl = document.createElement('span');
537
- dimEl.style.letterSpacing = '0.02em';
538
- bottomRow.appendChild(dimEl);
539
- thumb.onload = () => {
540
- if (thumb.naturalWidth) {
541
- const actual = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
542
- if (icon.size) {
543
- dimEl.textContent = `\u2192 ${actual}`;
544
- }
545
- else {
546
- dimEl.textContent = actual;
547
- }
548
- }
549
- };
550
- const sep = document.createElement('span');
551
- sep.textContent = '\u00b7';
552
- sep.style.opacity = '0.4';
553
- bottomRow.appendChild(sep);
554
- const urlEl = document.createElement('span');
555
- Object.assign(urlEl.style, {
556
- overflow: 'hidden',
557
- textOverflow: 'ellipsis',
558
- whiteSpace: 'nowrap',
559
- opacity: '0.6',
560
- });
561
- urlEl.textContent = icon.url;
562
- urlEl.title = icon.url;
563
- bottomRow.appendChild(urlEl);
564
- infoCol.appendChild(bottomRow);
565
- row.appendChild(infoCol);
566
- section.appendChild(row);
574
+ section.appendChild(createFaviconCard(icon, i));
567
575
  });
568
576
  container.appendChild(section);
569
577
  }
578
+ /** Create a colored summary pill (e.g. "3 errors", "2 warnings"). */
579
+ function createSummaryPill(text, color) {
580
+ const pill = document.createElement('span');
581
+ Object.assign(pill.style, {
582
+ color,
583
+ fontSize: '0.5625rem',
584
+ backgroundColor: withAlpha(color, 8),
585
+ padding: '2px 8px',
586
+ borderRadius: '8px',
587
+ letterSpacing: '0.03em',
588
+ });
589
+ pill.textContent = text;
590
+ return pill;
591
+ }
592
+ /** Create a striped row with alternating background and optional left border. */
593
+ function createStripedRow(index, borderColor) {
594
+ const row = document.createElement('div');
595
+ Object.assign(row.style, {
596
+ display: 'flex',
597
+ alignItems: 'center',
598
+ padding: '4px 8px',
599
+ gap: '8px',
600
+ borderRadius: '3px',
601
+ backgroundColor: index % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
602
+ ...(borderColor ? { borderLeft: `2px solid ${withAlpha(borderColor, 25)}` } : {}),
603
+ });
604
+ return row;
605
+ }
606
+ /** Create a styled text span for modal info items. */
607
+ function createInfoSpan(text, styles) {
608
+ const el = document.createElement('span');
609
+ Object.assign(el.style, styles);
610
+ el.textContent = text;
611
+ return el;
612
+ }
570
613
  function renderMissingTagsSection(container, tags) {
571
614
  const section = document.createElement('div');
572
615
  section.style.marginBottom = '20px';
@@ -584,76 +627,39 @@ function renderMissingTagsSection(container, tags) {
584
627
  marginBottom: '8px',
585
628
  });
586
629
  if (errorCount > 0) {
587
- const errPill = document.createElement('span');
588
- Object.assign(errPill.style, {
589
- color: CSS_COLORS.error,
590
- fontSize: '0.5625rem',
591
- backgroundColor: withAlpha(CSS_COLORS.error, 8),
592
- padding: '2px 8px',
593
- borderRadius: '8px',
594
- letterSpacing: '0.03em',
595
- });
596
- errPill.textContent = `${errorCount} error${errorCount > 1 ? 's' : ''}`;
597
- summary.appendChild(errPill);
630
+ summary.appendChild(createSummaryPill(`${errorCount} error${errorCount > 1 ? 's' : ''}`, CSS_COLORS.error));
598
631
  }
599
632
  if (warnCount > 0) {
600
- const warnPill = document.createElement('span');
601
- Object.assign(warnPill.style, {
602
- color: CSS_COLORS.warning,
603
- fontSize: '0.5625rem',
604
- backgroundColor: withAlpha(CSS_COLORS.warning, 8),
605
- padding: '2px 8px',
606
- borderRadius: '8px',
607
- letterSpacing: '0.03em',
608
- });
609
- warnPill.textContent = `${warnCount} warning${warnCount > 1 ? 's' : ''}`;
610
- summary.appendChild(warnPill);
633
+ summary.appendChild(createSummaryPill(`${warnCount} warning${warnCount > 1 ? 's' : ''}`, CSS_COLORS.warning));
611
634
  }
612
635
  section.appendChild(summary);
613
636
  }
614
637
  tags.forEach((tag, i) => {
615
638
  const isError = tag.severity === 'error';
616
639
  const tagColor = isError ? CSS_COLORS.error : CSS_COLORS.warning;
617
- const row = document.createElement('div');
618
- Object.assign(row.style, {
619
- display: 'flex',
620
- alignItems: 'center',
621
- padding: '4px 8px',
622
- gap: '8px',
623
- borderRadius: '3px',
624
- backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
625
- borderLeft: `2px solid ${withAlpha(tagColor, 25)}`,
626
- });
627
- const icon = document.createElement('span');
628
- Object.assign(icon.style, {
640
+ const row = createStripedRow(i, tagColor);
641
+ const icon = createInfoSpan(isError ? '\u2718' : '\u26a0', {
629
642
  fontSize: '0.625rem',
630
643
  flexShrink: '0',
631
644
  width: '14px',
632
645
  textAlign: 'center',
633
646
  color: tagColor,
634
647
  });
635
- icon.textContent = isError ? '\u2718' : '\u26a0';
636
648
  row.appendChild(icon);
637
- const tagName = document.createElement('span');
638
- Object.assign(tagName.style, {
649
+ row.appendChild(createInfoSpan(tag.tag, {
639
650
  color: CSS_COLORS.text,
640
651
  fontSize: '0.6875rem',
641
652
  width: '120px',
642
653
  minWidth: '120px',
643
654
  flexShrink: '0',
644
655
  fontWeight: '500',
645
- });
646
- tagName.textContent = tag.tag;
647
- row.appendChild(tagName);
648
- const hint = document.createElement('span');
649
- Object.assign(hint.style, {
656
+ }));
657
+ row.appendChild(createInfoSpan(tag.hint, {
650
658
  color: CSS_COLORS.textMuted,
651
659
  fontSize: '0.6875rem',
652
660
  flex: '1',
653
661
  opacity: '0.85',
654
- });
655
- hint.textContent = tag.hint;
656
- row.appendChild(hint);
662
+ }));
657
663
  section.appendChild(row);
658
664
  });
659
665
  container.appendChild(section);
@@ -674,7 +680,8 @@ export function renderA11yModal(state) {
674
680
  state.overlayElement = overlay;
675
681
  document.body.appendChild(overlay);
676
682
  // Run the audit async and replace content when done
677
- runA11yAudit().then((result) => {
683
+ runA11yAudit()
684
+ .then((result) => {
678
685
  // Check modal is still open
679
686
  if (!state.showA11yModal)
680
687
  return;
@@ -702,7 +709,8 @@ export function renderA11yModal(state) {
702
709
  const content = createModalContent();
703
710
  renderA11ySuccessContent(content, result, color);
704
711
  modal.appendChild(content);
705
- }).catch((err) => {
712
+ })
713
+ .catch((err) => {
706
714
  if (!state.showA11yModal)
707
715
  return;
708
716
  clearChildren(modal);
@@ -836,6 +844,85 @@ function createViolationNodeEl(node) {
836
844
  el.title = node.html;
837
845
  return el;
838
846
  }
847
+ /** Create a single violation card with rule ID, help text, description, and affected nodes. */
848
+ function createViolationCard(violation, impactColor) {
849
+ const violationEl = document.createElement('div');
850
+ Object.assign(violationEl.style, {
851
+ marginBottom: '12px',
852
+ padding: '10px 12px',
853
+ backgroundColor: withAlpha(impactColor, 3),
854
+ border: `1px solid ${withAlpha(impactColor, 13)}`,
855
+ borderRadius: '6px',
856
+ });
857
+ // Rule ID
858
+ const ruleId = document.createElement('div');
859
+ Object.assign(ruleId.style, {
860
+ color: impactColor,
861
+ fontSize: '0.6875rem',
862
+ fontWeight: '600',
863
+ marginBottom: '4px',
864
+ });
865
+ ruleId.textContent = violation.id;
866
+ violationEl.appendChild(ruleId);
867
+ // Help text
868
+ const helpText = document.createElement('div');
869
+ Object.assign(helpText.style, {
870
+ color: CSS_COLORS.text,
871
+ fontSize: '0.75rem',
872
+ marginBottom: '4px',
873
+ });
874
+ helpText.textContent = violation.help;
875
+ violationEl.appendChild(helpText);
876
+ // Description
877
+ const desc = document.createElement('div');
878
+ Object.assign(desc.style, {
879
+ color: CSS_COLORS.textSecondary,
880
+ fontSize: '0.6875rem',
881
+ marginBottom: '6px',
882
+ });
883
+ desc.textContent = violation.description;
884
+ violationEl.appendChild(desc);
885
+ // Node count
886
+ const nodeCount = document.createElement('div');
887
+ Object.assign(nodeCount.style, {
888
+ color: CSS_COLORS.textMuted,
889
+ fontSize: '0.625rem',
890
+ marginBottom: '4px',
891
+ });
892
+ nodeCount.textContent = `${violation.nodes.length} element${violation.nodes.length === 1 ? '' : 's'} affected`;
893
+ violationEl.appendChild(nodeCount);
894
+ // Affected nodes (collapsed by default, show first 3)
895
+ const nodesPreview = document.createElement('div');
896
+ Object.assign(nodesPreview.style, {
897
+ marginTop: '6px',
898
+ });
899
+ const visibleNodes = violation.nodes.slice(0, 3);
900
+ for (const node of visibleNodes) {
901
+ nodesPreview.appendChild(createViolationNodeEl(node));
902
+ }
903
+ if (violation.nodes.length > 3) {
904
+ const moreBtn = document.createElement('button');
905
+ Object.assign(moreBtn.style, {
906
+ background: 'none',
907
+ border: 'none',
908
+ color: impactColor,
909
+ fontSize: '0.625rem',
910
+ cursor: 'pointer',
911
+ padding: '2px 0',
912
+ fontFamily: FONT_MONO,
913
+ });
914
+ moreBtn.textContent = `+ ${violation.nodes.length - 3} more`;
915
+ moreBtn.onclick = () => {
916
+ moreBtn.remove();
917
+ for (const node of violation.nodes.slice(3)) {
918
+ nodesPreview.appendChild(createViolationNodeEl(node));
919
+ }
920
+ };
921
+ nodesPreview.appendChild(moreBtn);
922
+ }
923
+ violationEl.appendChild(nodesPreview);
924
+ return violationEl;
925
+ }
839
926
  function renderA11yViolationGroup(container, impact, violations) {
840
927
  const impactColor = getImpactColor(impact);
841
928
  const section = document.createElement('div');
@@ -854,82 +941,7 @@ function renderA11yViolationGroup(container, impact, violations) {
854
941
  sectionTitle.textContent = `${impact} (${violations.length})`;
855
942
  section.appendChild(sectionTitle);
856
943
  for (const violation of violations) {
857
- const violationEl = document.createElement('div');
858
- Object.assign(violationEl.style, {
859
- marginBottom: '12px',
860
- padding: '10px 12px',
861
- backgroundColor: withAlpha(impactColor, 3),
862
- border: `1px solid ${withAlpha(impactColor, 13)}`,
863
- borderRadius: '6px',
864
- });
865
- // Rule ID
866
- const ruleId = document.createElement('div');
867
- Object.assign(ruleId.style, {
868
- color: impactColor,
869
- fontSize: '0.6875rem',
870
- fontWeight: '600',
871
- marginBottom: '4px',
872
- });
873
- ruleId.textContent = violation.id;
874
- violationEl.appendChild(ruleId);
875
- // Help text
876
- const helpText = document.createElement('div');
877
- Object.assign(helpText.style, {
878
- color: CSS_COLORS.text,
879
- fontSize: '0.75rem',
880
- marginBottom: '4px',
881
- });
882
- helpText.textContent = violation.help;
883
- violationEl.appendChild(helpText);
884
- // Description
885
- const desc = document.createElement('div');
886
- Object.assign(desc.style, {
887
- color: CSS_COLORS.textSecondary,
888
- fontSize: '0.6875rem',
889
- marginBottom: '6px',
890
- });
891
- desc.textContent = violation.description;
892
- violationEl.appendChild(desc);
893
- // Node count
894
- const nodeCount = document.createElement('div');
895
- Object.assign(nodeCount.style, {
896
- color: CSS_COLORS.textMuted,
897
- fontSize: '0.625rem',
898
- marginBottom: '4px',
899
- });
900
- nodeCount.textContent = `${violation.nodes.length} element${violation.nodes.length === 1 ? '' : 's'} affected`;
901
- violationEl.appendChild(nodeCount);
902
- // Affected nodes (collapsed by default, show first 3)
903
- const nodesPreview = document.createElement('div');
904
- Object.assign(nodesPreview.style, {
905
- marginTop: '6px',
906
- });
907
- const visibleNodes = violation.nodes.slice(0, 3);
908
- for (const node of visibleNodes) {
909
- nodesPreview.appendChild(createViolationNodeEl(node));
910
- }
911
- if (violation.nodes.length > 3) {
912
- const moreBtn = document.createElement('button');
913
- Object.assign(moreBtn.style, {
914
- background: 'none',
915
- border: 'none',
916
- color: impactColor,
917
- fontSize: '0.625rem',
918
- cursor: 'pointer',
919
- padding: '2px 0',
920
- fontFamily: FONT_MONO,
921
- });
922
- moreBtn.textContent = `+ ${violation.nodes.length - 3} more`;
923
- moreBtn.onclick = () => {
924
- moreBtn.remove();
925
- for (const node of violation.nodes.slice(3)) {
926
- nodesPreview.appendChild(createViolationNodeEl(node));
927
- }
928
- };
929
- nodesPreview.appendChild(moreBtn);
930
- }
931
- violationEl.appendChild(nodesPreview);
932
- section.appendChild(violationEl);
944
+ section.appendChild(createViolationCard(violation, impactColor));
933
945
  }
934
946
  container.appendChild(section);
935
947
  }