@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.
- package/dist/GlobalDevBar.d.ts +7 -2
- package/dist/GlobalDevBar.d.ts.map +1 -1
- package/dist/GlobalDevBar.js +15 -2
- package/dist/GlobalDevBar.js.map +1 -1
- package/dist/accessibility.d.ts +2 -29
- package/dist/accessibility.d.ts.map +1 -1
- package/dist/accessibility.js +2 -2
- package/dist/accessibility.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/modules/keyboard.d.ts.map +1 -1
- package/dist/modules/keyboard.js +2 -0
- package/dist/modules/keyboard.js.map +1 -1
- package/dist/modules/rendering.d.ts.map +1 -1
- package/dist/modules/rendering.js +791 -40
- package/dist/modules/rendering.js.map +1 -1
- package/dist/modules/screenshot.d.ts +8 -0
- package/dist/modules/screenshot.d.ts.map +1 -1
- package/dist/modules/screenshot.js +31 -0
- package/dist/modules/screenshot.js.map +1 -1
- package/dist/modules/types.d.ts +7 -1
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/websocket.d.ts +1 -1
- package/dist/modules/websocket.d.ts.map +1 -1
- package/dist/modules/websocket.js +184 -0
- package/dist/modules/websocket.js.map +1 -1
- package/dist/outline.d.ts +2 -10
- package/dist/outline.d.ts.map +1 -1
- package/dist/outline.js +2 -244
- package/dist/outline.js.map +1 -1
- package/dist/schema.d.ts +2 -10
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -109
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/modals.d.ts +1 -0
- package/dist/ui/modals.d.ts.map +1 -1
- package/dist/ui/modals.js +7 -1
- package/dist/ui/modals.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
1323
|
-
borderBottom: `1px solid ${color}40`,
|
|
1324
|
-
paddingBottom: '6px',
|
|
1395
|
+
margin: '0',
|
|
1325
1396
|
});
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1464
|
+
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
|
1351
1465
|
borderRadius: '4px',
|
|
1352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1441
|
-
if (key.length > 18) {
|
|
1559
|
+
if (key.length > 18)
|
|
1442
1560
|
keyEl.title = key;
|
|
1443
|
-
}
|
|
1444
1561
|
row.appendChild(keyEl);
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
-
|
|
1452
|
-
whiteSpace: 'pre-wrap',
|
|
1862
|
+
opacity: '0.85',
|
|
1453
1863
|
});
|
|
1454
|
-
|
|
1455
|
-
row.appendChild(
|
|
1456
|
-
|
|
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
|