@ytspar/devbar 1.2.0 → 1.3.0-canary.6b0d2d9

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 (43) hide show
  1. package/dist/GlobalDevBar.d.ts +7 -2
  2. package/dist/GlobalDevBar.d.ts.map +1 -1
  3. package/dist/GlobalDevBar.js +15 -2
  4. package/dist/GlobalDevBar.js.map +1 -1
  5. package/dist/accessibility.d.ts +2 -29
  6. package/dist/accessibility.d.ts.map +1 -1
  7. package/dist/accessibility.js +2 -2
  8. package/dist/accessibility.js.map +1 -1
  9. package/dist/constants.d.ts +1 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +1 -0
  12. package/dist/constants.js.map +1 -1
  13. package/dist/modules/keyboard.d.ts.map +1 -1
  14. package/dist/modules/keyboard.js +2 -0
  15. package/dist/modules/keyboard.js.map +1 -1
  16. package/dist/modules/rendering.d.ts.map +1 -1
  17. package/dist/modules/rendering.js +791 -40
  18. package/dist/modules/rendering.js.map +1 -1
  19. package/dist/modules/screenshot.d.ts +8 -0
  20. package/dist/modules/screenshot.d.ts.map +1 -1
  21. package/dist/modules/screenshot.js +31 -0
  22. package/dist/modules/screenshot.js.map +1 -1
  23. package/dist/modules/types.d.ts +7 -1
  24. package/dist/modules/types.d.ts.map +1 -1
  25. package/dist/modules/websocket.d.ts +1 -1
  26. package/dist/modules/websocket.d.ts.map +1 -1
  27. package/dist/modules/websocket.js +184 -0
  28. package/dist/modules/websocket.js.map +1 -1
  29. package/dist/outline.d.ts +2 -10
  30. package/dist/outline.d.ts.map +1 -1
  31. package/dist/outline.js +2 -244
  32. package/dist/outline.js.map +1 -1
  33. package/dist/schema.d.ts +2 -10
  34. package/dist/schema.d.ts.map +1 -1
  35. package/dist/schema.js +2 -109
  36. package/dist/schema.js.map +1 -1
  37. package/dist/types.d.ts +1 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/ui/modals.d.ts +1 -0
  40. package/dist/ui/modals.d.ts.map +1 -1
  41. package/dist/ui/modals.js +7 -1
  42. package/dist/ui/modals.js.map +1 -1
  43. package/package.json +11 -11
@@ -6,11 +6,12 @@
6
6
  */
7
7
  import { BUTTON_COLORS, CATEGORY_COLORS, CSS_COLORS, FONT_MONO, TAILWIND_BREAKPOINTS, } from '../constants.js';
8
8
  import { extractDocumentOutline, outlineToMarkdown } from '../outline.js';
9
- import { extractPageSchema, schemaToMarkdown } from '../schema.js';
9
+ import { checkMissingTags, extractFavicons, extractPageSchema, isImageKey, schemaToMarkdown } from '../schema.js';
10
10
  import { ACCENT_COLOR_PRESETS, DEFAULT_SETTINGS, resolveSaveLocation } from '../settings.js';
11
11
  import { createEmptyMessage, createInfoBox, createModalBox, createModalContent, createModalHeader, createModalOverlay, createCloseButton, createStyledButton, createSvgIcon, getButtonStyles, } from '../ui/index.js';
12
12
  import { getResponsiveMetricVisibility } from './performance.js';
13
- import { calculateCostEstimate, closeDesignReviewConfirm, copyPathToClipboard, handleDocumentOutline, handlePageSchema, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
13
+ import { runA11yAudit, groupViolationsByImpact, getImpactColor, getViolationCounts, preloadAxe, } from '../accessibility.js';
14
+ import { calculateCostEstimate, closeDesignReviewConfirm, copyPathToClipboard, handleA11yAudit, handleDocumentOutline, handlePageSchema, handleSaveA11yAudit, handleSaveConsoleLogs, handleSaveOutline, handleSaveSchema, proceedWithDesignReview, showDesignReviewConfirmation, } from './screenshot.js';
14
15
  import { setThemeMode } from './theme.js';
15
16
  import { addTooltipTitle, attachBreakpointTooltip, attachButtonTooltip, attachClickToggleTooltip, attachInfoTooltip, attachMetricTooltip, attachTextTooltip, clearAllTooltips, } from './tooltips.js';
16
17
  /**
@@ -57,6 +58,8 @@ function createConnectionIndicator(state) {
57
58
  connIndicator.appendChild(connDot);
58
59
  return connIndicator;
59
60
  }
61
+ /** Prevents re-entrant render calls during rapid clicks */
62
+ let renderGuard = false;
60
63
  /**
61
64
  * Main render dispatch - creates container and delegates to appropriate renderer.
62
65
  */
@@ -65,12 +68,16 @@ export function render(state, consoleCaptureSingleton, customControls) {
65
68
  return;
66
69
  if (typeof document === 'undefined')
67
70
  return;
71
+ if (renderGuard)
72
+ return;
73
+ renderGuard = true;
68
74
  // Clear any orphaned tooltips from previous render
69
75
  clearAllTooltips(state);
70
76
  // Remove existing overlay if any (modals append to body, need explicit cleanup)
71
77
  if (state.overlayElement) {
72
78
  state.overlayElement.remove();
73
79
  state.overlayElement = null;
80
+ document.body.style.overflow = '';
74
81
  }
75
82
  // Remove existing container if any
76
83
  if (state.container) {
@@ -80,6 +87,8 @@ export function render(state, consoleCaptureSingleton, customControls) {
80
87
  // even if content or overlay rendering throws
81
88
  state.container = document.createElement('div');
82
89
  state.container.setAttribute('data-devbar', 'true');
90
+ state.container.setAttribute('role', 'toolbar');
91
+ state.container.setAttribute('aria-label', 'DevBar');
83
92
  document.body.appendChild(state.container);
84
93
  try {
85
94
  if (state.collapsed) {
@@ -101,6 +110,11 @@ export function render(state, consoleCaptureSingleton, customControls) {
101
110
  catch (e) {
102
111
  console.error('[GlobalDevBar] Overlay render failed:', e);
103
112
  }
113
+ // Lock body scroll while a modal overlay is open
114
+ if (state.overlayElement) {
115
+ document.body.style.overflow = 'hidden';
116
+ }
117
+ renderGuard = false;
104
118
  }
105
119
  function renderOverlays(state, consoleCaptureSingleton) {
106
120
  // Safety: only one overlay at a time. First match wins; close the rest.
@@ -108,21 +122,29 @@ function renderOverlays(state, consoleCaptureSingleton) {
108
122
  if (state.consoleFilter) {
109
123
  state.showOutlineModal = false;
110
124
  state.showSchemaModal = false;
125
+ state.showA11yModal = false;
111
126
  state.showDesignReviewConfirm = false;
112
127
  state.showSettingsPopover = false;
113
128
  renderConsolePopup(state, consoleCaptureSingleton);
114
129
  }
115
130
  else if (state.showOutlineModal) {
116
131
  state.showSchemaModal = false;
132
+ state.showA11yModal = false;
117
133
  state.showDesignReviewConfirm = false;
118
134
  state.showSettingsPopover = false;
119
135
  renderOutlineModal(state);
120
136
  }
121
137
  else if (state.showSchemaModal) {
138
+ state.showA11yModal = false;
122
139
  state.showDesignReviewConfirm = false;
123
140
  state.showSettingsPopover = false;
124
141
  renderSchemaModal(state);
125
142
  }
143
+ else if (state.showA11yModal) {
144
+ state.showDesignReviewConfirm = false;
145
+ state.showSettingsPopover = false;
146
+ renderA11yModal(state);
147
+ }
126
148
  else if (state.showDesignReviewConfirm) {
127
149
  state.showSettingsPopover = false;
128
150
  renderDesignReviewConfirmModal(state);
@@ -644,6 +666,7 @@ function renderExpanded(state, customControls) {
644
666
  actionsContainer.appendChild(createAIReviewButton(state));
645
667
  actionsContainer.appendChild(createOutlineButton(state));
646
668
  actionsContainer.appendChild(createSchemaButton(state));
669
+ actionsContainer.appendChild(createA11yButton(state));
647
670
  actionsContainer.appendChild(createSettingsButton(state));
648
671
  actionsContainer.appendChild(createCompactToggleButton(state));
649
672
  mainRow.appendChild(actionsContainer);
@@ -738,6 +761,7 @@ function createConsoleBadge(state, type, count, color) {
738
761
  function createScreenshotButton(state, accentColor) {
739
762
  const btn = document.createElement('button');
740
763
  btn.type = 'button';
764
+ btn.setAttribute('aria-label', 'Screenshot');
741
765
  const hasSuccessState = state.copiedToClipboard || state.copiedPath || state.lastScreenshot;
742
766
  const isDisabled = state.capturing;
743
767
  const effectiveSave = resolveSaveLocation(state.options.saveLocation, state.sweetlinkConnected);
@@ -874,6 +898,7 @@ function createScreenshotButton(state, accentColor) {
874
898
  function createAIReviewButton(state) {
875
899
  const btn = document.createElement('button');
876
900
  btn.type = 'button';
901
+ btn.setAttribute('aria-label', 'AI Design Review');
877
902
  const hasError = !!state.designReviewError;
878
903
  const isActive = state.designReviewInProgress || !!state.lastDesignReview || hasError;
879
904
  const isDisabled = state.designReviewInProgress || !state.sweetlinkConnected;
@@ -929,6 +954,7 @@ function createAIReviewButton(state) {
929
954
  function createOutlineButton(state) {
930
955
  const btn = document.createElement('button');
931
956
  btn.type = 'button';
957
+ btn.setAttribute('aria-label', 'Document Outline');
932
958
  const isActive = state.showOutlineModal || !!state.lastOutline;
933
959
  // Attach HTML tooltip
934
960
  attachButtonTooltip(state, btn, BUTTON_COLORS.outline, (_tooltip, h) => {
@@ -957,6 +983,7 @@ function createOutlineButton(state) {
957
983
  function createSchemaButton(state) {
958
984
  const btn = document.createElement('button');
959
985
  btn.type = 'button';
986
+ btn.setAttribute('aria-label', 'Page Schema');
960
987
  const isActive = state.showSchemaModal || !!state.lastSchema;
961
988
  // Attach HTML tooltip
962
989
  attachButtonTooltip(state, btn, BUTTON_COLORS.schema, (_tooltip, h) => {
@@ -982,6 +1009,37 @@ function createSchemaButton(state) {
982
1009
  }
983
1010
  return btn;
984
1011
  }
1012
+ function createA11yButton(state) {
1013
+ const btn = document.createElement('button');
1014
+ btn.type = 'button';
1015
+ btn.setAttribute('aria-label', 'Accessibility Audit');
1016
+ const isActive = state.showA11yModal || !!state.lastA11yAudit;
1017
+ attachButtonTooltip(state, btn, BUTTON_COLORS.a11y, (_tooltip, h) => {
1018
+ if (state.lastA11yAudit) {
1019
+ const isDownloaded = state.lastA11yAudit.endsWith('downloaded');
1020
+ h.addSuccess(isDownloaded ? 'A11y report downloaded!' : 'A11y report saved!', isDownloaded ? undefined : state.lastA11yAudit);
1021
+ return;
1022
+ }
1023
+ h.addTitle('Accessibility Audit');
1024
+ h.addDescription('Run axe-core audit to check WCAG compliance.');
1025
+ if (state.options.saveLocation === 'local' && !state.sweetlinkConnected) {
1026
+ h.addWarning('Sweetlink not connected. Switch save method to Auto or Download.');
1027
+ }
1028
+ });
1029
+ // Preload axe-core on hover
1030
+ btn.addEventListener('mouseenter', () => preloadAxe(), { once: true });
1031
+ Object.assign(btn.style, getButtonStyles(BUTTON_COLORS.a11y, isActive, false));
1032
+ btn.onclick = () => handleA11yAudit(state);
1033
+ if (state.lastA11yAudit) {
1034
+ btn.textContent = 'v';
1035
+ btn.style.fontSize = '0.5rem';
1036
+ }
1037
+ else {
1038
+ // Accessibility/shield icon
1039
+ btn.appendChild(createSvgIcon('M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z', { fill: true }));
1040
+ }
1041
+ return btn;
1042
+ }
985
1043
  /**
986
1044
  * Create the settings gear button.
987
1045
  */
@@ -989,6 +1047,7 @@ function createSettingsButton(state) {
989
1047
  const btn = document.createElement('button');
990
1048
  btn.type = 'button';
991
1049
  btn.setAttribute('data-testid', 'devbar-settings-button');
1050
+ btn.setAttribute('aria-label', 'Settings');
992
1051
  // Attach HTML tooltip
993
1052
  attachButtonTooltip(state, btn, CSS_COLORS.textSecondary, (_tooltip, h) => {
994
1053
  h.addTitle('Settings');
@@ -1048,6 +1107,7 @@ function createSettingsButton(state) {
1048
1107
  function createCompactToggleButton(state) {
1049
1108
  const btn = document.createElement('button');
1050
1109
  btn.type = 'button';
1110
+ btn.setAttribute('aria-label', state.compactMode ? 'Switch to expanded mode' : 'Switch to compact mode');
1051
1111
  const isCompact = state.compactMode;
1052
1112
  const { accentColor } = state.options;
1053
1113
  const iconColor = CSS_COLORS.textSecondary;
@@ -1131,6 +1191,7 @@ function renderConsolePopup(state, consoleCaptureSingleton) {
1131
1191
  await navigator.clipboard.writeText(lines.join('\n'));
1132
1192
  },
1133
1193
  onSave: () => handleSaveConsoleLogs(state, logs),
1194
+ onClear: () => state.clearConsoleLogs(),
1134
1195
  sweetlinkConnected: state.sweetlinkConnected,
1135
1196
  saveLocation: state.options.saveLocation,
1136
1197
  isSaving: state.savingConsoleLogs,
@@ -1290,17 +1351,25 @@ function renderSchemaModal(state) {
1290
1351
  });
1291
1352
  modal.appendChild(header);
1292
1353
  const content = createModalContent();
1354
+ const missingTags = checkMissingTags(schema);
1355
+ const favicons = extractFavicons();
1293
1356
  const hasContent = schema.jsonLd.length > 0 ||
1294
1357
  Object.keys(schema.openGraph).length > 0 ||
1295
1358
  Object.keys(schema.twitter).length > 0 ||
1296
- Object.keys(schema.metaTags).length > 0;
1359
+ Object.keys(schema.metaTags).length > 0 ||
1360
+ favicons.length > 0 ||
1361
+ missingTags.length > 0;
1297
1362
  if (!hasContent) {
1298
1363
  content.appendChild(createEmptyMessage('No structured data found on this page'));
1299
1364
  }
1300
1365
  else {
1301
- renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
1366
+ if (missingTags.length > 0)
1367
+ renderMissingTagsSection(content, missingTags);
1302
1368
  renderSchemaSection(content, 'Open Graph', schema.openGraph, CSS_COLORS.info);
1303
1369
  renderSchemaSection(content, 'Twitter Cards', schema.twitter, CSS_COLORS.cyan);
1370
+ if (favicons.length > 0)
1371
+ renderFaviconsSection(content, favicons);
1372
+ renderSchemaSection(content, 'JSON-LD', schema.jsonLd, color);
1304
1373
  renderSchemaSection(content, 'Meta Tags', schema.metaTags, CSS_COLORS.textMuted);
1305
1374
  }
1306
1375
  modal.appendChild(content);
@@ -1308,54 +1377,99 @@ function renderSchemaModal(state) {
1308
1377
  state.overlayElement = overlay;
1309
1378
  document.body.appendChild(overlay);
1310
1379
  }
1311
- function renderSchemaSection(container, title, items, color) {
1312
- const isEmpty = Array.isArray(items) ? items.length === 0 : Object.keys(items).length === 0;
1313
- if (isEmpty)
1314
- return;
1315
- const section = document.createElement('div');
1316
- section.style.marginBottom = '20px';
1317
- const sectionTitle = document.createElement('h3');
1318
- Object.assign(sectionTitle.style, {
1380
+ function renderSchemaSectionHeader(section, title, color, count) {
1381
+ const header = document.createElement('div');
1382
+ Object.assign(header.style, {
1383
+ display: 'flex',
1384
+ alignItems: 'center',
1385
+ gap: '8px',
1386
+ marginBottom: '10px',
1387
+ paddingBottom: '6px',
1388
+ borderBottom: `1px solid ${color}30`,
1389
+ });
1390
+ const titleEl = document.createElement('h3');
1391
+ Object.assign(titleEl.style, {
1319
1392
  color,
1320
1393
  fontSize: '0.8125rem',
1321
1394
  fontWeight: '600',
1322
- marginBottom: '10px',
1323
- borderBottom: `1px solid ${color}40`,
1324
- paddingBottom: '6px',
1395
+ margin: '0',
1325
1396
  });
1326
- sectionTitle.textContent = title;
1327
- section.appendChild(sectionTitle);
1397
+ titleEl.textContent = title;
1398
+ header.appendChild(titleEl);
1399
+ const badge = document.createElement('span');
1400
+ Object.assign(badge.style, {
1401
+ color: `${color}cc`,
1402
+ fontSize: '0.5625rem',
1403
+ backgroundColor: `${color}18`,
1404
+ padding: '1px 6px',
1405
+ borderRadius: '8px',
1406
+ letterSpacing: '0.03em',
1407
+ });
1408
+ badge.textContent = String(count);
1409
+ header.appendChild(badge);
1410
+ section.appendChild(header);
1411
+ }
1412
+ function renderSchemaSection(container, title, items, color) {
1413
+ const count = Array.isArray(items) ? items.length : Object.keys(items).length;
1414
+ if (count === 0)
1415
+ return;
1416
+ const section = document.createElement('div');
1417
+ section.style.marginBottom = '20px';
1418
+ renderSchemaSectionHeader(section, title, color, count);
1328
1419
  if (Array.isArray(items)) {
1329
- renderJsonLdItems(section, items);
1420
+ renderJsonLdItems(section, items, color);
1330
1421
  }
1331
1422
  else {
1332
1423
  renderKeyValueItems(section, items);
1333
1424
  }
1334
1425
  container.appendChild(section);
1335
1426
  }
1336
- function renderJsonLdItems(container, items) {
1427
+ function renderJsonLdItems(container, items, color) {
1337
1428
  items.forEach((item, i) => {
1338
1429
  const itemEl = document.createElement('div');
1339
1430
  itemEl.style.marginBottom = '10px';
1340
- const itemTitle = document.createElement('div');
1431
+ // Extract @type for a meaningful label
1432
+ const typed = item;
1433
+ const schemaType = typeof typed?.['@type'] === 'string' ? typed['@type'] : null;
1434
+ const itemHeader = document.createElement('div');
1435
+ Object.assign(itemHeader.style, {
1436
+ display: 'flex',
1437
+ alignItems: 'center',
1438
+ gap: '6px',
1439
+ marginBottom: '4px',
1440
+ });
1441
+ const itemTitle = document.createElement('span');
1341
1442
  Object.assign(itemTitle.style, {
1342
1443
  color: '#9ca3af',
1343
1444
  fontSize: '0.6875rem',
1344
- marginBottom: '4px',
1345
1445
  });
1346
1446
  itemTitle.textContent = `Schema ${i + 1}`;
1347
- itemEl.appendChild(itemTitle);
1447
+ itemHeader.appendChild(itemTitle);
1448
+ if (schemaType) {
1449
+ const typeTag = document.createElement('span');
1450
+ Object.assign(typeTag.style, {
1451
+ color: `${color}cc`,
1452
+ fontSize: '0.5625rem',
1453
+ backgroundColor: `${color}15`,
1454
+ border: `1px solid ${color}25`,
1455
+ padding: '0 5px',
1456
+ borderRadius: '3px',
1457
+ });
1458
+ typeTag.textContent = schemaType;
1459
+ itemHeader.appendChild(typeTag);
1460
+ }
1461
+ itemEl.appendChild(itemHeader);
1348
1462
  const codeEl = document.createElement('pre');
1349
1463
  Object.assign(codeEl.style, {
1350
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
1464
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
1351
1465
  borderRadius: '4px',
1352
- padding: '10px',
1466
+ borderLeft: `2px solid ${color}50`,
1467
+ padding: '10px 10px 10px 12px',
1353
1468
  overflow: 'auto',
1354
1469
  fontSize: '0.625rem',
1355
1470
  margin: '0',
1356
1471
  maxHeight: '300px',
1357
1472
  });
1358
- // Syntax highlight the JSON using DOM methods for safety
1359
1473
  appendHighlightedJson(codeEl, JSON.stringify(item, null, 2));
1360
1474
  itemEl.appendChild(codeEl);
1361
1475
  container.appendChild(itemEl);
@@ -1417,16 +1531,20 @@ function appendHighlightedJson(container, json) {
1417
1531
  }
1418
1532
  }
1419
1533
  function renderKeyValueItems(container, items) {
1420
- for (const [key, value] of Object.entries(items)) {
1534
+ const entries = Object.entries(items);
1535
+ entries.forEach(([key, value], i) => {
1536
+ const isImage = isImageKey(key);
1421
1537
  const row = document.createElement('div');
1422
1538
  Object.assign(row.style, {
1423
1539
  display: 'flex',
1424
- marginBottom: '4px',
1540
+ padding: isImage ? '6px 8px' : '3px 8px',
1425
1541
  alignItems: 'flex-start',
1542
+ borderRadius: '3px',
1543
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
1426
1544
  });
1427
1545
  const keyEl = document.createElement('span');
1428
1546
  Object.assign(keyEl.style, {
1429
- color: '#9ca3af',
1547
+ color: CSS_COLORS.textSecondary,
1430
1548
  fontSize: '0.6875rem',
1431
1549
  width: '120px',
1432
1550
  minWidth: '120px',
@@ -1435,26 +1553,659 @@ function renderKeyValueItems(container, items) {
1435
1553
  overflow: 'hidden',
1436
1554
  textOverflow: 'ellipsis',
1437
1555
  whiteSpace: 'nowrap',
1556
+ paddingTop: isImage ? '2px' : '0',
1438
1557
  });
1439
1558
  keyEl.textContent = key;
1440
- // Show full key on hover if it might be truncated
1441
- if (key.length > 18) {
1559
+ if (key.length > 18)
1442
1560
  keyEl.title = key;
1443
- }
1444
1561
  row.appendChild(keyEl);
1445
- const valueEl = document.createElement('span');
1446
- const strValue = String(value);
1447
- Object.assign(valueEl.style, {
1448
- color: '#d1d5db',
1562
+ if (isImage && value) {
1563
+ const valueCol = document.createElement('div');
1564
+ Object.assign(valueCol.style, { flex: '1', minWidth: '0' });
1565
+ // Image frame with subtle border — fixed height to prevent layout jitter
1566
+ const frame = document.createElement('div');
1567
+ Object.assign(frame.style, {
1568
+ display: 'inline-block',
1569
+ padding: '4px',
1570
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
1571
+ border: '1px solid rgba(255, 255, 255, 0.06)',
1572
+ borderRadius: '4px',
1573
+ marginBottom: '4px',
1574
+ minHeight: '60px',
1575
+ minWidth: '80px',
1576
+ });
1577
+ const thumb = document.createElement('img');
1578
+ Object.assign(thumb.style, {
1579
+ width: '200px',
1580
+ height: '120px',
1581
+ objectFit: 'contain',
1582
+ borderRadius: '2px',
1583
+ display: 'block',
1584
+ });
1585
+ thumb.src = value;
1586
+ thumb.alt = key;
1587
+ thumb.onerror = () => { frame.style.display = 'none'; };
1588
+ thumb.onload = () => {
1589
+ if (thumb.naturalWidth) {
1590
+ dimEl.textContent = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
1591
+ }
1592
+ };
1593
+ frame.appendChild(thumb);
1594
+ valueCol.appendChild(frame);
1595
+ // Reserve space for dimension text to avoid reflow
1596
+ const dimEl = document.createElement('div');
1597
+ Object.assign(dimEl.style, {
1598
+ color: CSS_COLORS.textMuted,
1599
+ fontSize: '0.5625rem',
1600
+ minHeight: '0.75rem',
1601
+ letterSpacing: '0.02em',
1602
+ });
1603
+ valueCol.appendChild(dimEl);
1604
+ const urlEl = document.createElement('div');
1605
+ Object.assign(urlEl.style, {
1606
+ color: CSS_COLORS.textMuted,
1607
+ fontSize: '0.5625rem',
1608
+ wordBreak: 'break-all',
1609
+ opacity: '0.7',
1610
+ });
1611
+ urlEl.textContent = value;
1612
+ valueCol.appendChild(urlEl);
1613
+ row.appendChild(valueCol);
1614
+ }
1615
+ else {
1616
+ const valueEl = document.createElement('span');
1617
+ Object.assign(valueEl.style, {
1618
+ color: CSS_COLORS.text,
1619
+ fontSize: '0.6875rem',
1620
+ flex: '1',
1621
+ wordBreak: 'break-word',
1622
+ whiteSpace: 'pre-wrap',
1623
+ opacity: '0.85',
1624
+ });
1625
+ valueEl.textContent = String(value);
1626
+ row.appendChild(valueEl);
1627
+ }
1628
+ container.appendChild(row);
1629
+ });
1630
+ }
1631
+ /** Derive intended device/purpose from favicon label and declared size */
1632
+ function faviconDevice(label, size) {
1633
+ const s = parseInt(size || '', 10);
1634
+ if (label.includes('apple'))
1635
+ return { text: 'Apple home screen', color: CSS_COLORS.info };
1636
+ if (size === 'any' || label.includes('svg'))
1637
+ return { text: 'Scalable (any)', color: CSS_COLORS.cyan };
1638
+ if (s >= 192)
1639
+ return { text: 'Android / PWA', color: CSS_COLORS.primary };
1640
+ if (s >= 48)
1641
+ return { text: 'Taskbar / shortcut', color: CSS_COLORS.purple };
1642
+ if (s > 0)
1643
+ return { text: 'Browser tab', color: CSS_COLORS.textSecondary };
1644
+ return { text: 'General', color: CSS_COLORS.textMuted };
1645
+ }
1646
+ function renderFaviconsSection(container, icons) {
1647
+ const color = CSS_COLORS.purple;
1648
+ const section = document.createElement('div');
1649
+ section.style.marginBottom = '20px';
1650
+ renderSchemaSectionHeader(section, 'Favicons', color, icons.length);
1651
+ icons.forEach((icon, i) => {
1652
+ const device = faviconDevice(icon.label, icon.size);
1653
+ const row = document.createElement('div');
1654
+ Object.assign(row.style, {
1655
+ display: 'flex',
1656
+ alignItems: 'center',
1657
+ padding: '6px 8px',
1658
+ gap: '10px',
1659
+ borderRadius: '3px',
1660
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
1661
+ });
1662
+ // Thumbnail frame
1663
+ const frame = document.createElement('div');
1664
+ Object.assign(frame.style, {
1665
+ width: '32px',
1666
+ height: '32px',
1667
+ display: 'flex',
1668
+ alignItems: 'center',
1669
+ justifyContent: 'center',
1670
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
1671
+ border: '1px solid rgba(255, 255, 255, 0.06)',
1672
+ borderRadius: '4px',
1673
+ flexShrink: '0',
1674
+ });
1675
+ const thumb = document.createElement('img');
1676
+ Object.assign(thumb.style, {
1677
+ width: '22px',
1678
+ height: '22px',
1679
+ objectFit: 'contain',
1680
+ });
1681
+ thumb.src = icon.url;
1682
+ thumb.alt = icon.label;
1683
+ thumb.onerror = () => { frame.style.opacity = '0.3'; };
1684
+ frame.appendChild(thumb);
1685
+ row.appendChild(frame);
1686
+ // Info column: label, device, dimensions + URL
1687
+ const infoCol = document.createElement('div');
1688
+ Object.assign(infoCol.style, {
1689
+ flex: '1',
1690
+ minWidth: '0',
1691
+ display: 'flex',
1692
+ flexDirection: 'column',
1693
+ gap: '2px',
1694
+ });
1695
+ // Top row: label + device pill
1696
+ const topRow = document.createElement('div');
1697
+ Object.assign(topRow.style, {
1698
+ display: 'flex',
1699
+ alignItems: 'center',
1700
+ gap: '6px',
1701
+ });
1702
+ const labelEl = document.createElement('span');
1703
+ Object.assign(labelEl.style, {
1704
+ color: CSS_COLORS.text,
1705
+ fontSize: '0.6875rem',
1706
+ fontWeight: '500',
1707
+ overflow: 'hidden',
1708
+ textOverflow: 'ellipsis',
1709
+ whiteSpace: 'nowrap',
1710
+ });
1711
+ labelEl.textContent = icon.label;
1712
+ if (icon.label.length > 24)
1713
+ labelEl.title = icon.label;
1714
+ topRow.appendChild(labelEl);
1715
+ const devicePill = document.createElement('span');
1716
+ Object.assign(devicePill.style, {
1717
+ color: device.color,
1718
+ fontSize: '0.5rem',
1719
+ backgroundColor: `${device.color}12`,
1720
+ padding: '1px 6px',
1721
+ borderRadius: '6px',
1722
+ letterSpacing: '0.03em',
1723
+ whiteSpace: 'nowrap',
1724
+ flexShrink: '0',
1725
+ });
1726
+ devicePill.textContent = device.text;
1727
+ topRow.appendChild(devicePill);
1728
+ infoCol.appendChild(topRow);
1729
+ // Bottom row: declared size + actual dimensions + URL
1730
+ const bottomRow = document.createElement('div');
1731
+ Object.assign(bottomRow.style, {
1732
+ display: 'flex',
1733
+ alignItems: 'center',
1734
+ gap: '6px',
1735
+ fontSize: '0.5625rem',
1736
+ color: CSS_COLORS.textMuted,
1737
+ });
1738
+ if (icon.size) {
1739
+ const declaredEl = document.createElement('span');
1740
+ declaredEl.textContent = icon.size;
1741
+ declaredEl.style.opacity = '0.8';
1742
+ bottomRow.appendChild(declaredEl);
1743
+ }
1744
+ // Actual dimensions (populated on load)
1745
+ const dimEl = document.createElement('span');
1746
+ dimEl.style.letterSpacing = '0.02em';
1747
+ bottomRow.appendChild(dimEl);
1748
+ thumb.onload = () => {
1749
+ if (thumb.naturalWidth) {
1750
+ const actual = `${thumb.naturalWidth}\u00d7${thumb.naturalHeight}`;
1751
+ if (icon.size) {
1752
+ dimEl.textContent = `\u2192 ${actual}`;
1753
+ }
1754
+ else {
1755
+ dimEl.textContent = actual;
1756
+ }
1757
+ }
1758
+ };
1759
+ const sep = document.createElement('span');
1760
+ sep.textContent = '\u00b7';
1761
+ sep.style.opacity = '0.4';
1762
+ bottomRow.appendChild(sep);
1763
+ const urlEl = document.createElement('span');
1764
+ Object.assign(urlEl.style, {
1765
+ overflow: 'hidden',
1766
+ textOverflow: 'ellipsis',
1767
+ whiteSpace: 'nowrap',
1768
+ opacity: '0.6',
1769
+ });
1770
+ urlEl.textContent = icon.url;
1771
+ urlEl.title = icon.url;
1772
+ bottomRow.appendChild(urlEl);
1773
+ infoCol.appendChild(bottomRow);
1774
+ row.appendChild(infoCol);
1775
+ section.appendChild(row);
1776
+ });
1777
+ container.appendChild(section);
1778
+ }
1779
+ function renderMissingTagsSection(container, tags) {
1780
+ const section = document.createElement('div');
1781
+ section.style.marginBottom = '20px';
1782
+ const errorCount = tags.filter((t) => t.severity === 'error').length;
1783
+ const warnCount = tags.length - errorCount;
1784
+ const hasErrors = errorCount > 0;
1785
+ const sectionColor = hasErrors ? CSS_COLORS.error : CSS_COLORS.warning;
1786
+ renderSchemaSectionHeader(section, 'Missing Tags', sectionColor, tags.length);
1787
+ // Summary pill row
1788
+ if (errorCount > 0 || warnCount > 0) {
1789
+ const summary = document.createElement('div');
1790
+ Object.assign(summary.style, {
1791
+ display: 'flex',
1792
+ gap: '8px',
1793
+ marginBottom: '8px',
1794
+ });
1795
+ if (errorCount > 0) {
1796
+ const errPill = document.createElement('span');
1797
+ Object.assign(errPill.style, {
1798
+ color: CSS_COLORS.error,
1799
+ fontSize: '0.5625rem',
1800
+ backgroundColor: `${CSS_COLORS.error}15`,
1801
+ padding: '2px 8px',
1802
+ borderRadius: '8px',
1803
+ letterSpacing: '0.03em',
1804
+ });
1805
+ errPill.textContent = `${errorCount} error${errorCount > 1 ? 's' : ''}`;
1806
+ summary.appendChild(errPill);
1807
+ }
1808
+ if (warnCount > 0) {
1809
+ const warnPill = document.createElement('span');
1810
+ Object.assign(warnPill.style, {
1811
+ color: CSS_COLORS.warning,
1812
+ fontSize: '0.5625rem',
1813
+ backgroundColor: `${CSS_COLORS.warning}15`,
1814
+ padding: '2px 8px',
1815
+ borderRadius: '8px',
1816
+ letterSpacing: '0.03em',
1817
+ });
1818
+ warnPill.textContent = `${warnCount} warning${warnCount > 1 ? 's' : ''}`;
1819
+ summary.appendChild(warnPill);
1820
+ }
1821
+ section.appendChild(summary);
1822
+ }
1823
+ tags.forEach((tag, i) => {
1824
+ const isError = tag.severity === 'error';
1825
+ const tagColor = isError ? CSS_COLORS.error : CSS_COLORS.warning;
1826
+ const row = document.createElement('div');
1827
+ Object.assign(row.style, {
1828
+ display: 'flex',
1829
+ alignItems: 'center',
1830
+ padding: '4px 8px',
1831
+ gap: '8px',
1832
+ borderRadius: '3px',
1833
+ backgroundColor: i % 2 === 0 ? 'rgba(255, 255, 255, 0.02)' : 'transparent',
1834
+ borderLeft: `2px solid ${tagColor}40`,
1835
+ });
1836
+ const icon = document.createElement('span');
1837
+ Object.assign(icon.style, {
1838
+ fontSize: '0.625rem',
1839
+ flexShrink: '0',
1840
+ width: '14px',
1841
+ textAlign: 'center',
1842
+ color: tagColor,
1843
+ });
1844
+ icon.textContent = isError ? '\u2718' : '\u26a0';
1845
+ row.appendChild(icon);
1846
+ const tagName = document.createElement('span');
1847
+ Object.assign(tagName.style, {
1848
+ color: CSS_COLORS.text,
1849
+ fontSize: '0.6875rem',
1850
+ width: '120px',
1851
+ minWidth: '120px',
1852
+ flexShrink: '0',
1853
+ fontWeight: '500',
1854
+ });
1855
+ tagName.textContent = tag.tag;
1856
+ row.appendChild(tagName);
1857
+ const hint = document.createElement('span');
1858
+ Object.assign(hint.style, {
1859
+ color: CSS_COLORS.textMuted,
1449
1860
  fontSize: '0.6875rem',
1450
1861
  flex: '1',
1451
- wordBreak: 'break-word',
1452
- whiteSpace: 'pre-wrap',
1862
+ opacity: '0.85',
1453
1863
  });
1454
- valueEl.textContent = strValue;
1455
- row.appendChild(valueEl);
1456
- container.appendChild(row);
1864
+ hint.textContent = tag.hint;
1865
+ row.appendChild(hint);
1866
+ section.appendChild(row);
1867
+ });
1868
+ container.appendChild(section);
1869
+ }
1870
+ // ============================================================================
1871
+ // Accessibility Audit Modal
1872
+ // ============================================================================
1873
+ function formatA11yMarkdown(result) {
1874
+ const counts = getViolationCounts(result.violations);
1875
+ const lines = [
1876
+ '# Accessibility Audit Report',
1877
+ '',
1878
+ `**URL:** ${result.url}`,
1879
+ `**Timestamp:** ${result.timestamp}`,
1880
+ '',
1881
+ '## Summary',
1882
+ '',
1883
+ `- **Total violations:** ${counts.total}`,
1884
+ `- Critical: ${counts.critical}`,
1885
+ `- Serious: ${counts.serious}`,
1886
+ `- Moderate: ${counts.moderate}`,
1887
+ `- Minor: ${counts.minor}`,
1888
+ `- Passes: ${result.passes.length}`,
1889
+ `- Incomplete: ${result.incomplete.length}`,
1890
+ '',
1891
+ ];
1892
+ if (result.violations.length === 0) {
1893
+ lines.push('No accessibility violations found.');
1894
+ return lines.join('\n');
1895
+ }
1896
+ const grouped = groupViolationsByImpact(result.violations);
1897
+ for (const [impact, violations] of grouped) {
1898
+ if (violations.length === 0)
1899
+ continue;
1900
+ lines.push(`## ${impact.charAt(0).toUpperCase() + impact.slice(1)} (${violations.length})`);
1901
+ lines.push('');
1902
+ for (const v of violations) {
1903
+ lines.push(`### ${v.id}`);
1904
+ lines.push('');
1905
+ lines.push(`**${v.help}**`);
1906
+ lines.push('');
1907
+ lines.push(v.description);
1908
+ lines.push('');
1909
+ lines.push(`- Help: ${v.helpUrl}`);
1910
+ lines.push(`- Elements affected: ${v.nodes.length}`);
1911
+ lines.push('');
1912
+ for (const node of v.nodes.slice(0, 10)) {
1913
+ const html = node.html.length > 120 ? `${node.html.slice(0, 120)}...` : node.html;
1914
+ lines.push(` - \`${html}\``);
1915
+ if (node.target.length > 0) {
1916
+ lines.push(` Selector: \`${node.target.join(', ')}\``);
1917
+ }
1918
+ }
1919
+ if (v.nodes.length > 10) {
1920
+ lines.push(` - ... and ${v.nodes.length - 10} more`);
1921
+ }
1922
+ lines.push('');
1923
+ }
1924
+ }
1925
+ return lines.join('\n');
1926
+ }
1927
+ function clearChildren(el) {
1928
+ while (el.firstChild) {
1929
+ el.removeChild(el.firstChild);
1930
+ }
1931
+ }
1932
+ function renderA11yModal(state) {
1933
+ const color = BUTTON_COLORS.a11y;
1934
+ const closeModal = () => {
1935
+ state.showA11yModal = false;
1936
+ state.render();
1937
+ };
1938
+ const overlay = createModalOverlay(closeModal);
1939
+ const modal = createModalBox(color);
1940
+ // Show loading state initially
1941
+ const loadingContent = createModalContent();
1942
+ const loadingMsg = document.createElement('div');
1943
+ Object.assign(loadingMsg.style, {
1944
+ textAlign: 'center',
1945
+ padding: '40px',
1946
+ color: CSS_COLORS.textSecondary,
1947
+ fontSize: '0.875rem',
1948
+ });
1949
+ loadingMsg.textContent = 'Running accessibility audit...';
1950
+ loadingMsg.style.animation = 'pulse 1.5s ease-in-out infinite';
1951
+ loadingContent.appendChild(loadingMsg);
1952
+ // Temporary header without save/copy (shown during loading)
1953
+ const loadingHeader = createModalHeader({
1954
+ color,
1955
+ title: 'Accessibility Audit',
1956
+ onClose: closeModal,
1957
+ onCopyMd: async () => { },
1958
+ sweetlinkConnected: state.sweetlinkConnected,
1959
+ saveLocation: state.options.saveLocation,
1960
+ });
1961
+ modal.appendChild(loadingHeader);
1962
+ modal.appendChild(loadingContent);
1963
+ overlay.appendChild(modal);
1964
+ state.overlayElement = overlay;
1965
+ document.body.appendChild(overlay);
1966
+ // Run the audit async and replace content when done
1967
+ runA11yAudit().then((result) => {
1968
+ // Check modal is still open
1969
+ if (!state.showA11yModal)
1970
+ return;
1971
+ const markdown = formatA11yMarkdown(result);
1972
+ // Replace modal content
1973
+ clearChildren(modal);
1974
+ const violationCount = result.violations.length;
1975
+ const titleText = violationCount === 0
1976
+ ? 'Accessibility Audit \u2014 No Issues'
1977
+ : `Accessibility Audit \u2014 ${violationCount} Violation${violationCount === 1 ? '' : 's'}`;
1978
+ const header = createModalHeader({
1979
+ color,
1980
+ title: titleText,
1981
+ onClose: closeModal,
1982
+ onCopyMd: async () => {
1983
+ await navigator.clipboard.writeText(markdown);
1984
+ },
1985
+ onSave: () => handleSaveA11yAudit(state, markdown),
1986
+ sweetlinkConnected: state.sweetlinkConnected,
1987
+ saveLocation: state.options.saveLocation,
1988
+ isSaving: state.savingA11yAudit,
1989
+ savedPath: state.lastA11yAudit,
1990
+ });
1991
+ modal.appendChild(header);
1992
+ const content = createModalContent();
1993
+ if (result.violations.length === 0) {
1994
+ const successMsg = document.createElement('div');
1995
+ Object.assign(successMsg.style, {
1996
+ textAlign: 'center',
1997
+ padding: '40px',
1998
+ color: '#10b981',
1999
+ fontSize: '0.875rem',
2000
+ });
2001
+ successMsg.textContent = 'No accessibility violations found!';
2002
+ content.appendChild(successMsg);
2003
+ // Show pass count
2004
+ if (result.passes.length > 0) {
2005
+ const passInfo = document.createElement('div');
2006
+ Object.assign(passInfo.style, {
2007
+ textAlign: 'center',
2008
+ color: CSS_COLORS.textMuted,
2009
+ fontSize: '0.75rem',
2010
+ marginTop: '8px',
2011
+ });
2012
+ passInfo.textContent = `${result.passes.length} rules passed`;
2013
+ content.appendChild(passInfo);
2014
+ }
2015
+ }
2016
+ else {
2017
+ // Summary bar
2018
+ const counts = getViolationCounts(result.violations);
2019
+ const summaryBar = document.createElement('div');
2020
+ Object.assign(summaryBar.style, {
2021
+ display: 'flex',
2022
+ gap: '12px',
2023
+ marginBottom: '16px',
2024
+ padding: '10px 12px',
2025
+ backgroundColor: `${color}10`,
2026
+ border: `1px solid ${color}30`,
2027
+ borderRadius: '6px',
2028
+ flexWrap: 'wrap',
2029
+ });
2030
+ for (const impact of ['critical', 'serious', 'moderate', 'minor']) {
2031
+ if (counts[impact] === 0)
2032
+ continue;
2033
+ const badge = document.createElement('span');
2034
+ const impactColor = getImpactColor(impact);
2035
+ Object.assign(badge.style, {
2036
+ display: 'inline-flex',
2037
+ alignItems: 'center',
2038
+ gap: '4px',
2039
+ fontSize: '0.6875rem',
2040
+ fontWeight: '600',
2041
+ color: impactColor,
2042
+ });
2043
+ const dot = document.createElement('span');
2044
+ Object.assign(dot.style, {
2045
+ width: '6px',
2046
+ height: '6px',
2047
+ borderRadius: '50%',
2048
+ backgroundColor: impactColor,
2049
+ });
2050
+ badge.appendChild(dot);
2051
+ badge.appendChild(document.createTextNode(`${counts[impact]} ${impact}`));
2052
+ summaryBar.appendChild(badge);
2053
+ }
2054
+ content.appendChild(summaryBar);
2055
+ // Grouped violations
2056
+ const grouped = groupViolationsByImpact(result.violations);
2057
+ for (const [impact, violations] of grouped) {
2058
+ if (violations.length === 0)
2059
+ continue;
2060
+ renderA11yViolationGroup(content, impact, violations);
2061
+ }
2062
+ }
2063
+ modal.appendChild(content);
2064
+ }).catch((err) => {
2065
+ if (!state.showA11yModal)
2066
+ return;
2067
+ clearChildren(modal);
2068
+ const header = createModalHeader({
2069
+ color: CSS_COLORS.error,
2070
+ title: 'Accessibility Audit \u2014 Error',
2071
+ onClose: closeModal,
2072
+ onCopyMd: async () => { },
2073
+ sweetlinkConnected: state.sweetlinkConnected,
2074
+ saveLocation: state.options.saveLocation,
2075
+ });
2076
+ modal.appendChild(header);
2077
+ const content = createModalContent();
2078
+ content.appendChild(createInfoBox(CSS_COLORS.error, 'Audit Failed', `${err instanceof Error ? err.message : 'Unknown error'}`));
2079
+ modal.appendChild(content);
2080
+ });
2081
+ }
2082
+ function renderA11yViolationGroup(container, impact, violations) {
2083
+ const impactColor = getImpactColor(impact);
2084
+ const section = document.createElement('div');
2085
+ section.style.marginBottom = '20px';
2086
+ // Section header
2087
+ const sectionTitle = document.createElement('h3');
2088
+ Object.assign(sectionTitle.style, {
2089
+ color: impactColor,
2090
+ fontSize: '0.8125rem',
2091
+ fontWeight: '600',
2092
+ marginBottom: '10px',
2093
+ borderBottom: `1px solid ${impactColor}40`,
2094
+ paddingBottom: '6px',
2095
+ textTransform: 'capitalize',
2096
+ });
2097
+ sectionTitle.textContent = `${impact} (${violations.length})`;
2098
+ section.appendChild(sectionTitle);
2099
+ for (const violation of violations) {
2100
+ const violationEl = document.createElement('div');
2101
+ Object.assign(violationEl.style, {
2102
+ marginBottom: '12px',
2103
+ padding: '10px 12px',
2104
+ backgroundColor: `${impactColor}08`,
2105
+ border: `1px solid ${impactColor}20`,
2106
+ borderRadius: '6px',
2107
+ });
2108
+ // Rule ID
2109
+ const ruleId = document.createElement('div');
2110
+ Object.assign(ruleId.style, {
2111
+ color: impactColor,
2112
+ fontSize: '0.6875rem',
2113
+ fontWeight: '600',
2114
+ marginBottom: '4px',
2115
+ });
2116
+ ruleId.textContent = violation.id;
2117
+ violationEl.appendChild(ruleId);
2118
+ // Help text
2119
+ const helpText = document.createElement('div');
2120
+ Object.assign(helpText.style, {
2121
+ color: CSS_COLORS.text,
2122
+ fontSize: '0.75rem',
2123
+ marginBottom: '4px',
2124
+ });
2125
+ helpText.textContent = violation.help;
2126
+ violationEl.appendChild(helpText);
2127
+ // Description
2128
+ const desc = document.createElement('div');
2129
+ Object.assign(desc.style, {
2130
+ color: CSS_COLORS.textSecondary,
2131
+ fontSize: '0.6875rem',
2132
+ marginBottom: '6px',
2133
+ });
2134
+ desc.textContent = violation.description;
2135
+ violationEl.appendChild(desc);
2136
+ // Node count
2137
+ const nodeCount = document.createElement('div');
2138
+ Object.assign(nodeCount.style, {
2139
+ color: CSS_COLORS.textMuted,
2140
+ fontSize: '0.625rem',
2141
+ marginBottom: '4px',
2142
+ });
2143
+ nodeCount.textContent = `${violation.nodes.length} element${violation.nodes.length === 1 ? '' : 's'} affected`;
2144
+ violationEl.appendChild(nodeCount);
2145
+ // Affected nodes (collapsed by default, show first 3)
2146
+ const nodesPreview = document.createElement('div');
2147
+ Object.assign(nodesPreview.style, {
2148
+ marginTop: '6px',
2149
+ });
2150
+ const visibleNodes = violation.nodes.slice(0, 3);
2151
+ for (const node of visibleNodes) {
2152
+ const nodeEl = document.createElement('div');
2153
+ Object.assign(nodeEl.style, {
2154
+ padding: '3px 6px',
2155
+ marginBottom: '2px',
2156
+ backgroundColor: 'rgba(0,0,0,0.2)',
2157
+ borderRadius: '3px',
2158
+ fontSize: '0.625rem',
2159
+ color: CSS_COLORS.textSecondary,
2160
+ fontFamily: 'monospace',
2161
+ whiteSpace: 'nowrap',
2162
+ overflow: 'hidden',
2163
+ textOverflow: 'ellipsis',
2164
+ });
2165
+ nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
2166
+ nodeEl.title = node.html;
2167
+ nodesPreview.appendChild(nodeEl);
2168
+ }
2169
+ if (violation.nodes.length > 3) {
2170
+ const moreBtn = document.createElement('button');
2171
+ Object.assign(moreBtn.style, {
2172
+ background: 'none',
2173
+ border: 'none',
2174
+ color: impactColor,
2175
+ fontSize: '0.625rem',
2176
+ cursor: 'pointer',
2177
+ padding: '2px 0',
2178
+ fontFamily: FONT_MONO,
2179
+ });
2180
+ moreBtn.textContent = `+ ${violation.nodes.length - 3} more`;
2181
+ moreBtn.onclick = () => {
2182
+ // Show remaining nodes
2183
+ moreBtn.remove();
2184
+ for (const node of violation.nodes.slice(3)) {
2185
+ const nodeEl = document.createElement('div');
2186
+ Object.assign(nodeEl.style, {
2187
+ padding: '3px 6px',
2188
+ marginBottom: '2px',
2189
+ backgroundColor: 'rgba(0,0,0,0.2)',
2190
+ borderRadius: '3px',
2191
+ fontSize: '0.625rem',
2192
+ color: CSS_COLORS.textSecondary,
2193
+ fontFamily: 'monospace',
2194
+ whiteSpace: 'nowrap',
2195
+ overflow: 'hidden',
2196
+ textOverflow: 'ellipsis',
2197
+ });
2198
+ nodeEl.textContent = node.html.length > 100 ? `${node.html.slice(0, 100)}...` : node.html;
2199
+ nodeEl.title = node.html;
2200
+ nodesPreview.appendChild(nodeEl);
2201
+ }
2202
+ };
2203
+ nodesPreview.appendChild(moreBtn);
2204
+ }
2205
+ violationEl.appendChild(nodesPreview);
2206
+ section.appendChild(violationEl);
1457
2207
  }
2208
+ container.appendChild(section);
1458
2209
  }
1459
2210
  // ============================================================================
1460
2211
  // Design Review Confirmation Modal