wcag-scanner 1.2.65 → 1.2.67

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.
@@ -4,117 +4,264 @@ exports.WcagDevOverlay = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const browserScanner_1 = require("./browserScanner");
7
- // ─── Impact theme ──────────────────────────────────────────────────────────────
7
+ const gemini_1 = require("./gemini");
8
+ // ─── Impact colours ───────────────────────────────────────────────────────────
8
9
  const IMPACT = {
9
10
  critical: { color: '#dc2626', bg: '#fef2f2', border: '#fca5a5' },
10
11
  serious: { color: '#ea580c', bg: '#fff7ed', border: '#fdba74' },
11
12
  moderate: { color: '#ca8a04', bg: '#fefce8', border: '#fde047' },
12
13
  minor: { color: '#2563eb', bg: '#eff6ff', border: '#93c5fd' },
13
14
  };
14
- const impactTheme = (impact) => { var _a; return (_a = IMPACT[impact]) !== null && _a !== void 0 ? _a : { color: '#6b7280', bg: '#f9fafb', border: '#e5e7eb' }; };
15
+ const theme = (impact) => { var _a; return (_a = IMPACT[impact]) !== null && _a !== void 0 ? _a : { color: '#6b7280', bg: '#f9fafb', border: '#e5e7eb' }; };
15
16
  // ─── Highlight helpers ─────────────────────────────────────────────────────────
17
+ const HIGHLIGHT_ID = 'wcag-dev-highlight';
18
+ function getHighlightLayer() {
19
+ let layer = document.getElementById(HIGHLIGHT_ID);
20
+ if (layer)
21
+ return layer;
22
+ layer = document.createElement('div');
23
+ layer.id = HIGHLIGHT_ID;
24
+ layer.setAttribute('aria-hidden', 'true');
25
+ Object.assign(layer.style, {
26
+ position: 'fixed',
27
+ top: '0',
28
+ left: '0',
29
+ width: '0',
30
+ height: '0',
31
+ display: 'none',
32
+ pointerEvents: 'none',
33
+ zIndex: '2147483646',
34
+ borderRadius: '10px',
35
+ boxSizing: 'border-box',
36
+ transition: 'opacity 80ms ease-out, transform 80ms ease-out',
37
+ transform: 'translateZ(0)',
38
+ });
39
+ const label = document.createElement('div');
40
+ label.setAttribute('data-wcag-highlight-label', 'true');
41
+ Object.assign(label.style, {
42
+ position: 'absolute',
43
+ top: '-30px',
44
+ left: '0',
45
+ padding: '4px 8px',
46
+ borderRadius: '999px',
47
+ color: '#fff',
48
+ fontSize: '11px',
49
+ fontWeight: '700',
50
+ letterSpacing: '0.03em',
51
+ boxShadow: '0 6px 18px rgba(15,23,42,0.22)',
52
+ whiteSpace: 'nowrap',
53
+ maxWidth: '280px',
54
+ overflow: 'hidden',
55
+ textOverflow: 'ellipsis',
56
+ });
57
+ layer.appendChild(label);
58
+ document.body.appendChild(layer);
59
+ return layer;
60
+ }
16
61
  let _hovered = null;
17
62
  let _pinned = null;
18
- function restoreEl(saved) {
19
- saved.el.style.outline = saved.outline;
20
- saved.el.style.boxShadow = saved.shadow;
21
- }
22
- function applyHighlight(el, color) {
23
- el.style.outline = `2px solid ${color}`;
24
- el.style.boxShadow = `0 0 0 4px ${color}28`;
63
+ let _highlightRaf = 0;
64
+ let _highlightCleanup = null;
65
+ function labelForElement(el) {
66
+ const htmlEl = el;
67
+ const parts = [el.tagName.toLowerCase()];
68
+ if (htmlEl.id)
69
+ parts.push(`#${htmlEl.id}`);
70
+ const classes = typeof htmlEl.className === 'string'
71
+ ? htmlEl.className.trim().split(/\s+/).filter(Boolean).slice(0, 2)
72
+ : [];
73
+ if (classes.length)
74
+ parts.push(`.${classes.join('.')}`);
75
+ return parts.join('');
25
76
  }
26
- function hoverEl(el) {
27
- if (_hovered) {
28
- restoreEl(_hovered);
29
- _hovered = null;
77
+ function renderHighlight(el, mode) {
78
+ const rect = el.getBoundingClientRect();
79
+ const layer = getHighlightLayer();
80
+ const label = layer.querySelector('[data-wcag-highlight-label="true"]');
81
+ if (rect.width <= 0 || rect.height <= 0) {
82
+ layer.style.display = 'none';
83
+ return;
84
+ }
85
+ const color = mode === 'pinned' ? '#7c3aed' : '#0ea5e9';
86
+ const glow = mode === 'pinned' ? 'rgba(124,58,237,0.35)' : 'rgba(14,165,233,0.28)';
87
+ Object.assign(layer.style, {
88
+ display: 'block',
89
+ top: `${Math.max(rect.top - 4, 0)}px`,
90
+ left: `${Math.max(rect.left - 4, 0)}px`,
91
+ width: `${Math.max(rect.width + 8, 12)}px`,
92
+ height: `${Math.max(rect.height + 8, 12)}px`,
93
+ border: `3px solid ${color}`,
94
+ background: `${color}0d`,
95
+ boxShadow: `0 0 0 4px ${glow}, 0 14px 34px rgba(15,23,42,0.16)`,
96
+ opacity: '1',
97
+ });
98
+ if (label) {
99
+ label.textContent = mode === 'pinned' ? `Pinned ${labelForElement(el)}` : `Inspecting ${labelForElement(el)}`;
100
+ label.style.background = color;
101
+ label.style.top = rect.top < 48 ? `${rect.height + 10}px` : '-30px';
30
102
  }
31
- if (!el || (_pinned === null || _pinned === void 0 ? void 0 : _pinned.el) === el)
103
+ }
104
+ function syncHighlightPosition() {
105
+ cancelAnimationFrame(_highlightRaf);
106
+ _highlightRaf = requestAnimationFrame(() => {
107
+ if (_pinned) {
108
+ renderHighlight(_pinned, 'pinned');
109
+ return;
110
+ }
111
+ if (_hovered) {
112
+ renderHighlight(_hovered, 'hover');
113
+ return;
114
+ }
115
+ const layer = document.getElementById(HIGHLIGHT_ID);
116
+ if (layer)
117
+ layer.style.display = 'none';
118
+ });
119
+ }
120
+ function ensureHighlightTracking() {
121
+ if (_highlightCleanup)
32
122
  return;
33
- const h = el;
34
- _hovered = { el: h, outline: h.style.outline, shadow: h.style.boxShadow };
35
- applyHighlight(h, '#0ea5e9');
36
- h.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
123
+ const update = () => syncHighlightPosition();
124
+ window.addEventListener('scroll', update, true);
125
+ window.addEventListener('resize', update);
126
+ _highlightCleanup = () => {
127
+ window.removeEventListener('scroll', update, true);
128
+ window.removeEventListener('resize', update);
129
+ _highlightCleanup = null;
130
+ };
131
+ }
132
+ function maybeStopHighlightTracking() {
133
+ if (_hovered || _pinned || !_highlightCleanup)
134
+ return;
135
+ _highlightCleanup();
136
+ }
137
+ function hoverEl(el) {
138
+ _hovered = !el || _pinned === el ? null : el;
139
+ ensureHighlightTracking();
140
+ syncHighlightPosition();
37
141
  }
38
142
  function clearHover() {
39
- if (_hovered) {
40
- restoreEl(_hovered);
41
- _hovered = null;
42
- }
143
+ _hovered = null;
144
+ syncHighlightPosition();
145
+ maybeStopHighlightTracking();
43
146
  }
44
- function pinEl(el) {
45
- if ((_pinned === null || _pinned === void 0 ? void 0 : _pinned.el) === el) {
46
- restoreEl(_pinned);
147
+ /** Returns true if now pinned, false if unpinned */
148
+ function togglePin(el) {
149
+ if (_pinned === el) {
47
150
  _pinned = null;
151
+ syncHighlightPosition();
152
+ maybeStopHighlightTracking();
48
153
  return false;
49
154
  }
50
- if (_pinned)
51
- restoreEl(_pinned);
52
- if ((_hovered === null || _hovered === void 0 ? void 0 : _hovered.el) === el) {
53
- restoreEl(_hovered);
54
- _hovered = null;
55
- }
56
- const h = el;
57
- _pinned = { el: h, outline: h.style.outline, shadow: h.style.boxShadow };
58
- applyHighlight(h, '#7c3aed');
59
- h.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
155
+ _hovered = null;
156
+ _pinned = el;
157
+ ensureHighlightTracking();
158
+ syncHighlightPosition();
159
+ el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
60
160
  return true;
61
161
  }
62
162
  function clearAllHighlights() {
63
- if (_hovered) {
64
- restoreEl(_hovered);
65
- _hovered = null;
66
- }
67
- if (_pinned) {
68
- restoreEl(_pinned);
69
- _pinned = null;
70
- }
163
+ cancelAnimationFrame(_highlightRaf);
164
+ _hovered = null;
165
+ _pinned = null;
166
+ const layer = document.getElementById(HIGHLIGHT_ID);
167
+ if (layer)
168
+ layer.style.display = 'none';
169
+ maybeStopHighlightTracking();
71
170
  }
72
- const ViolationCard = ({ item, pinned, onPin }) => {
171
+ const ViolationCard = ({ item, pinned, onPin, apiKey }) => {
73
172
  const [expanded, setExpanded] = (0, react_1.useState)(false);
74
- const { color, bg, border } = impactTheme(item.impact);
75
- const clickable = !!item.domElement;
173
+ const [aiState, setAiState] = (0, react_1.useState)('idle');
174
+ const [suggestion, setSuggestion] = (0, react_1.useState)(null);
175
+ const [aiError, setAiError] = (0, react_1.useState)('');
176
+ const { color, bg, border } = theme(item.impact);
177
+ const fetchAi = async () => {
178
+ if (!apiKey || aiState === 'loading')
179
+ return;
180
+ setAiState('loading');
181
+ setAiError('');
182
+ try {
183
+ const s = await (0, gemini_1.getAiSuggestion)(apiKey, item);
184
+ setSuggestion(s);
185
+ setAiState('done');
186
+ }
187
+ catch (e) {
188
+ setAiError(e instanceof Error ? e.message : 'Unknown error');
189
+ setAiState('error');
190
+ }
191
+ };
76
192
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
77
- background: bg,
78
- borderRadius: 8,
79
- marginBottom: 6,
80
- overflow: 'hidden',
193
+ background: bg, borderRadius: 8, marginBottom: 6, overflow: 'hidden',
81
194
  outline: pinned ? `2px solid ${color}` : `1px solid ${border}`,
82
195
  outlineOffset: pinned ? 1 : 0,
83
196
  borderLeft: `3px solid ${color}`,
84
- }, children: [(0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'flex-start', gap: 8, padding: '9px 10px', cursor: clickable ? 'pointer' : 'default' }, onMouseEnter: () => hoverEl(item.domElement), onMouseLeave: () => clearHover(), onClick: () => item.domElement && onPin(item.domElement), children: [(0, jsx_runtime_1.jsx)("span", { style: {
197
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'flex-start', gap: 8, padding: '9px 10px', cursor: item.domElement ? 'pointer' : 'default' }, onMouseEnter: () => hoverEl(item.domElement), onMouseLeave: () => clearHover(), onClick: () => item.domElement && onPin(item.domElement), title: item.domElement ? (pinned ? 'Click to unpin' : 'Click to pin element') : undefined, children: [(0, jsx_runtime_1.jsx)("span", { style: {
85
198
  fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase',
86
199
  background: color, color: '#fff', borderRadius: 4, padding: '2px 5px', flexShrink: 0, marginTop: 2,
87
200
  }, children: item.impact }), (0, jsx_runtime_1.jsxs)("span", { style: { fontSize: 12, fontWeight: 500, color: '#1e293b', lineHeight: 1.5, flex: 1 }, children: [item.description, pinned && (0, jsx_runtime_1.jsx)("span", { style: { marginLeft: 5, fontSize: 10 }, children: "\uD83D\uDCCD" })] }), (0, jsx_runtime_1.jsx)("button", { style: { background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8', fontSize: 11, padding: '1px 4px', flexShrink: 0 }, onClick: e => { e.stopPropagation(); setExpanded(x => !x); }, title: expanded ? 'Collapse' : 'Show details', children: expanded ? '▲' : '▼' })] }), expanded && ((0, jsx_runtime_1.jsxs)("div", { style: { padding: '0 10px 10px', borderTop: `1px solid ${border}` }, children: [item.elementPath && ((0, jsx_runtime_1.jsx)("code", { style: {
88
201
  display: 'block', marginTop: 8, fontSize: 10, color: '#475569',
89
202
  background: '#f1f5f9', border: '1px solid #e2e8f0', borderRadius: 4,
90
- padding: '3px 7px', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
91
- }, children: item.elementPath })), item.snippet && ((0, jsx_runtime_1.jsx)("code", { style: {
203
+ padding: '3px 7px', fontFamily: 'monospace', overflow: 'hidden',
204
+ textOverflow: 'ellipsis', whiteSpace: 'nowrap',
205
+ }, title: item.elementSelector, children: item.elementPath })), item.snippet && ((0, jsx_runtime_1.jsx)("code", { style: {
92
206
  display: 'block', marginTop: 6, fontSize: 10,
93
207
  background: '#0f172a', color: '#e2e8f0', borderRadius: 5,
94
- padding: '5px 8px', fontFamily: 'monospace', whiteSpace: 'pre-wrap', wordBreak: 'break-all',
95
- maxHeight: 72, overflow: 'auto',
208
+ padding: '5px 8px', fontFamily: 'monospace', whiteSpace: 'pre-wrap',
209
+ wordBreak: 'break-all', maxHeight: 72, overflow: 'auto',
96
210
  }, children: item.snippet })), item.help && ((0, jsx_runtime_1.jsx)("p", { style: { margin: '7px 0 0', fontSize: 11, color: '#475569', fontStyle: 'italic', lineHeight: 1.5 }, children: item.help })), item.wcag && item.wcag.length > 0 && ((0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 7 }, children: item.wcag.map(w => ((0, jsx_runtime_1.jsxs)("span", { style: {
97
211
  fontSize: 10, fontWeight: 600, color: '#7c3aed',
98
212
  background: '#f5f3ff', border: '1px solid #ddd6fe', borderRadius: 4, padding: '1px 6px',
99
- }, children: ["WCAG ", w] }, w))) }))] }))] }));
213
+ }, children: ["WCAG ", w] }, w))) })), apiKey && ((0, jsx_runtime_1.jsxs)("div", { style: { marginTop: 10, borderTop: `1px solid ${border}`, paddingTop: 8 }, children: [aiState === 'idle' && ((0, jsx_runtime_1.jsx)("button", { onClick: fetchAi, style: {
214
+ fontSize: 10, fontWeight: 600, color: '#7c3aed',
215
+ background: '#f5f3ff', border: '1px solid #ddd6fe', borderRadius: 5,
216
+ padding: '3px 9px', cursor: 'pointer',
217
+ }, children: "\u2728 Get AI fix" })), aiState === 'loading' && ((0, jsx_runtime_1.jsx)("span", { style: { fontSize: 10, color: '#94a3b8' }, children: "\u23F3 Asking Gemini\u2026" })), aiState === 'error' && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("span", { style: { fontSize: 10, color: '#dc2626' }, children: ["\u26A0 ", aiError] }), (0, jsx_runtime_1.jsx)("button", { onClick: fetchAi, style: { marginLeft: 8, fontSize: 10, color: '#7c3aed', background: 'none', border: 'none', cursor: 'pointer' }, children: "Retry" })] })), aiState === 'done' && suggestion && ((0, jsx_runtime_1.jsxs)("div", { children: [suggestion.code && ((0, jsx_runtime_1.jsx)("code", { style: {
218
+ display: 'block', fontSize: 10, background: '#0f172a', color: '#86efac',
219
+ borderRadius: 5, padding: '5px 8px', fontFamily: 'monospace',
220
+ whiteSpace: 'pre-wrap', wordBreak: 'break-all', maxHeight: 100, overflow: 'auto',
221
+ }, children: suggestion.code })), suggestion.explanation && ((0, jsx_runtime_1.jsx)("p", { style: { margin: '5px 0 0', fontSize: 11, color: '#16a34a', fontStyle: 'italic' }, children: suggestion.explanation })), (0, jsx_runtime_1.jsx)("button", { onClick: () => { setSuggestion(null); setAiState('idle'); }, style: { marginTop: 4, fontSize: 10, color: '#94a3b8', background: 'none', border: 'none', cursor: 'pointer' }, children: "Clear" })] }))] }))] }))] }));
222
+ };
223
+ const SettingsPanel = ({ apiKey, preset, onSave, onPresetChange }) => {
224
+ const [draft, setDraft] = (0, react_1.useState)(apiKey);
225
+ const [saved, setSaved] = (0, react_1.useState)(false);
226
+ const save = () => {
227
+ (0, gemini_1.setStoredApiKey)(draft);
228
+ onSave(draft);
229
+ setSaved(true);
230
+ setTimeout(() => setSaved(false), 2000);
231
+ };
232
+ return ((0, jsx_runtime_1.jsxs)("div", { style: { padding: '16px 14px', flex: 1, overflowY: 'auto' }, children: [(0, jsx_runtime_1.jsxs)("p", { style: { margin: '0 0 12px', fontSize: 12, color: '#475569', lineHeight: 1.6 }, children: ["Enter your ", (0, jsx_runtime_1.jsx)("strong", { children: "Google Gemini API key" }), " to get AI-powered fix suggestions for each violation. The key is stored in ", (0, jsx_runtime_1.jsx)("code", { style: { fontSize: 11, background: '#f1f5f9', padding: '1px 4px', borderRadius: 3 }, children: "localStorage" }), " and never leaves your browser."] }), (0, jsx_runtime_1.jsx)("label", { style: { display: 'block', fontSize: 11, fontWeight: 600, color: '#374151', marginBottom: 4 }, children: "Gemini API Key" }), (0, jsx_runtime_1.jsx)("input", { type: "password", value: draft, onChange: e => setDraft(e.target.value), placeholder: "AIza\u2026", style: {
233
+ width: '100%', boxSizing: 'border-box', fontSize: 12,
234
+ border: '1px solid #d1d5db', borderRadius: 6, padding: '7px 10px',
235
+ outline: 'none', fontFamily: 'monospace',
236
+ background: '#f9fafb',
237
+ }, onKeyDown: e => { if (e.key === 'Enter')
238
+ save(); } }), (0, jsx_runtime_1.jsxs)("div", { style: { marginTop: 10, display: 'flex', gap: 8, alignItems: 'center' }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: save, style: {
239
+ background: '#7c3aed', color: '#fff', border: 'none', borderRadius: 6,
240
+ padding: '6px 14px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
241
+ }, children: saved ? '✓ Saved' : 'Save key' }), draft && ((0, jsx_runtime_1.jsx)("button", { onClick: () => { setDraft(''); (0, gemini_1.setStoredApiKey)(''); onSave(''); }, style: { fontSize: 11, color: '#94a3b8', background: 'none', border: 'none', cursor: 'pointer' }, children: "Clear" }))] }), (0, jsx_runtime_1.jsxs)("p", { style: { marginTop: 16, fontSize: 10, color: '#9ca3af', lineHeight: 1.6 }, children: ["Get a free key at", ' ', (0, jsx_runtime_1.jsx)("span", { style: { color: '#7c3aed', fontWeight: 600 }, children: "aistudio.google.com" }), ' ', "\u2192 Get API key. The free tier is sufficient for development use."] }), (0, jsx_runtime_1.jsxs)("div", { style: { marginTop: 18, paddingTop: 14, borderTop: '1px solid #e2e8f0' }, children: [(0, jsx_runtime_1.jsx)("label", { style: { display: 'block', fontSize: 11, fontWeight: 600, color: '#374151', marginBottom: 4 }, children: "Scan Preset" }), (0, jsx_runtime_1.jsxs)("select", { value: preset, onChange: e => onPresetChange(e.target.value), style: {
242
+ width: '100%', boxSizing: 'border-box', fontSize: 12,
243
+ border: '1px solid #d1d5db', borderRadius: 6, padding: '7px 10px',
244
+ outline: 'none', background: '#f9fafb', color: '#111827',
245
+ }, children: [(0, jsx_runtime_1.jsx)("option", { value: "fast", children: "Fast" }), (0, jsx_runtime_1.jsx)("option", { value: "full", children: "Full" })] }), (0, jsx_runtime_1.jsx)("p", { style: { margin: '8px 0 0', fontSize: 10, color: '#64748b', lineHeight: 1.6 }, children: "`fast` runs the default rule set. `full` also includes heavier checks like `backgroundImages`." })] })] }));
100
246
  };
101
247
  // ─── Main Overlay ──────────────────────────────────────────────────────────────
102
- const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', debounce = 750, }) => {
248
+ const WcagDevOverlay = ({ level = 'AA', preset = 'fast', rules, position = 'bottom-right', debounce = 750, }) => {
103
249
  var _a, _b, _c;
104
- const [open, setOpen] = (0, react_1.useState)(() => {
105
- try {
106
- return sessionStorage.getItem('wcag-open') === '1';
107
- }
108
- catch (_a) {
109
- return false;
110
- }
111
- });
250
+ const [open, setOpen] = (0, react_1.useState)(() => { try {
251
+ return sessionStorage.getItem('wcag-open') === '1';
252
+ }
253
+ catch (_a) {
254
+ return false;
255
+ } });
256
+ const [view, setView] = (0, react_1.useState)('list');
112
257
  const [tab, setTab] = (0, react_1.useState)('violations');
113
258
  const [filter, setFilter] = (0, react_1.useState)('all');
114
259
  const [scanning, setScanning] = (0, react_1.useState)(false);
115
260
  const [results, setResults] = (0, react_1.useState)(null);
116
261
  const [lastScan, setLastScan] = (0, react_1.useState)(null);
117
262
  const [pinnedEl, setPinnedEl] = (0, react_1.useState)(null);
263
+ const [apiKey, setApiKey] = (0, react_1.useState)(() => (0, gemini_1.getStoredApiKey)());
264
+ const [activePreset, setActivePreset] = (0, react_1.useState)(preset);
118
265
  // Drag
119
266
  const [pos, setPos] = (0, react_1.useState)(null);
120
267
  const dragging = (0, react_1.useRef)(false);
@@ -124,6 +271,8 @@ const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', deboun
124
271
  const overlayRef = (0, react_1.useRef)(null);
125
272
  const scanningRef = (0, react_1.useRef)(false);
126
273
  const cooldownRef = (0, react_1.useRef)(false);
274
+ const pendingScanRef = (0, react_1.useRef)(false);
275
+ const scanTokenRef = (0, react_1.useRef)(0);
127
276
  // ── Persist open state ────────────────────────────────────────────────────
128
277
  (0, react_1.useEffect)(() => {
129
278
  try {
@@ -135,42 +284,55 @@ const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', deboun
135
284
  }, [open]);
136
285
  // ── Scan ──────────────────────────────────────────────────────────────────
137
286
  const scan = (0, react_1.useCallback)(async () => {
138
- if (scanningRef.current)
287
+ if (timerRef.current) {
288
+ clearTimeout(timerRef.current);
289
+ timerRef.current = null;
290
+ }
291
+ if (scanningRef.current) {
292
+ pendingScanRef.current = true;
139
293
  return;
294
+ }
295
+ const token = ++scanTokenRef.current;
296
+ // Set cooldown BEFORE clearing scanningRef to close the observer gap
297
+ cooldownRef.current = true;
140
298
  scanningRef.current = true;
141
299
  setScanning(true);
142
300
  try {
143
- const res = await (0, browserScanner_1.scanBrowserPage)({ level, rules });
144
- setResults(res);
145
- setLastScan(new Date());
301
+ const res = await (0, browserScanner_1.scanBrowserPage)({ level, preset: activePreset, rules });
302
+ if (token === scanTokenRef.current) {
303
+ setResults(res);
304
+ setLastScan(new Date());
305
+ }
146
306
  }
147
307
  finally {
148
308
  scanningRef.current = false;
149
309
  setScanning(false);
150
- // Cooldown prevents the observer from immediately re-triggering after scan
151
- cooldownRef.current = true;
152
- setTimeout(() => { cooldownRef.current = false; }, 1200);
310
+ window.setTimeout(() => {
311
+ cooldownRef.current = false;
312
+ if (pendingScanRef.current) {
313
+ pendingScanRef.current = false;
314
+ void scan();
315
+ }
316
+ }, 300);
153
317
  }
154
- }, [level, rules]);
318
+ }, [activePreset, level, rules]);
155
319
  (0, react_1.useEffect)(() => { scan(); }, [scan]);
156
320
  // ── MutationObserver ──────────────────────────────────────────────────────
157
321
  (0, react_1.useEffect)(() => {
158
322
  observerRef.current = new MutationObserver((mutations) => {
159
323
  if (scanningRef.current || cooldownRef.current)
160
324
  return;
161
- if (overlayRef.current &&
162
- mutations.every(m => overlayRef.current.contains(m.target)))
325
+ if (overlayRef.current && mutations.every(m => overlayRef.current.contains(m.target)))
163
326
  return;
164
327
  if (timerRef.current)
165
328
  clearTimeout(timerRef.current);
166
- timerRef.current = setTimeout(scan, debounce);
329
+ timerRef.current = setTimeout(() => {
330
+ timerRef.current = null;
331
+ void scan();
332
+ }, debounce);
167
333
  });
168
334
  observerRef.current.observe(document.body, {
169
- childList: true,
170
- subtree: true,
171
- attributes: true,
172
- // 'style' excluded — our highlight helper modifies inline styles on
173
- // page elements which would otherwise cause an infinite rescan loop
335
+ childList: true, subtree: true, attributes: true,
174
336
  attributeFilter: ['class', 'hidden', 'aria-hidden', 'role', 'alt', 'src', 'href'],
175
337
  });
176
338
  return () => {
@@ -181,33 +343,30 @@ const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', deboun
181
343
  clearAllHighlights();
182
344
  };
183
345
  }, [scan, debounce]);
184
- // ── Keyboard shortcut Alt+Shift+W ─────────────────────────────────────────
346
+ // ── Keyboard Alt+Shift+W ──────────────────────────────────────────────────
185
347
  (0, react_1.useEffect)(() => {
186
- const handler = (e) => {
187
- if (e.altKey && e.shiftKey && e.key === 'W')
188
- setOpen(o => !o);
189
- };
190
- window.addEventListener('keydown', handler);
191
- return () => window.removeEventListener('keydown', handler);
348
+ const h = (e) => { if (e.altKey && e.shiftKey && e.key === 'W')
349
+ setOpen(o => !o); };
350
+ window.addEventListener('keydown', h);
351
+ return () => window.removeEventListener('keydown', h);
192
352
  }, []);
193
- // ── Pin handler ───────────────────────────────────────────────────────────
353
+ // ── Pin ───────────────────────────────────────────────────────────────────
194
354
  const handlePin = (0, react_1.useCallback)((el) => {
195
- const nowPinned = pinEl(el);
355
+ const nowPinned = togglePin(el);
196
356
  setPinnedEl(nowPinned ? el : null);
197
357
  }, []);
198
358
  // ── Drag ──────────────────────────────────────────────────────────────────
199
359
  const onDragStart = (e) => {
200
360
  var _a, _b, _c;
201
361
  dragging.current = true;
202
- const rect = (_a = overlayRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
203
- dragOffset.current = { x: e.clientX - ((_b = rect === null || rect === void 0 ? void 0 : rect.left) !== null && _b !== void 0 ? _b : 0), y: e.clientY - ((_c = rect === null || rect === void 0 ? void 0 : rect.top) !== null && _c !== void 0 ? _c : 0) };
362
+ const r = (_a = overlayRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
363
+ dragOffset.current = { x: e.clientX - ((_b = r === null || r === void 0 ? void 0 : r.left) !== null && _b !== void 0 ? _b : 0), y: e.clientY - ((_c = r === null || r === void 0 ? void 0 : r.top) !== null && _c !== void 0 ? _c : 0) };
204
364
  e.preventDefault();
205
365
  };
206
366
  (0, react_1.useEffect)(() => {
207
367
  const onMove = (e) => {
208
- if (!dragging.current)
209
- return;
210
- setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
368
+ if (dragging.current)
369
+ setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
211
370
  };
212
371
  const onUp = () => { dragging.current = false; };
213
372
  window.addEventListener('mousemove', onMove);
@@ -230,58 +389,10 @@ const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', deboun
230
389
  const anchorStyle = pos
231
390
  ? { position: 'fixed', top: pos.y, left: pos.x, zIndex: 2147483647, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' }
232
391
  : { position: 'fixed', bottom: 20, [isLeft ? 'left' : 'right']: 20, zIndex: 2147483647, fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' };
233
- const btnColor = scanning ? '#64748b' : vCount > 0 ? '#7c3aed' : '#16a34a';
234
- const s = {
235
- toggle: {
236
- display: 'flex', alignItems: 'center', gap: 6,
237
- background: btnColor, color: '#fff', border: 'none', borderRadius: 22,
238
- padding: '8px 14px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
239
- boxShadow: '0 2px 12px rgba(0,0,0,0.2)', userSelect: 'none',
240
- },
241
- badge: {
242
- background: 'rgba(255,255,255,0.22)', borderRadius: 9,
243
- padding: '1px 6px', fontSize: 11, fontWeight: 700,
244
- },
245
- panel: {
246
- position: 'absolute',
247
- bottom: pos ? undefined : 48,
248
- top: pos ? 48 : undefined,
249
- [isLeft ? 'left' : 'right']: 0,
250
- width: 400,
251
- maxHeight: '74vh',
252
- background: '#ffffff',
253
- borderRadius: 10,
254
- boxShadow: '0 8px 40px rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.07)',
255
- display: 'flex',
256
- flexDirection: 'column',
257
- overflow: 'hidden',
258
- },
259
- header: {
260
- display: 'flex', alignItems: 'center', gap: 8,
261
- padding: '10px 12px', background: '#0f172a',
262
- cursor: 'grab', userSelect: 'none', flexShrink: 0,
263
- },
264
- headerBtn: {
265
- background: 'none', border: 'none', cursor: 'pointer',
266
- color: '#64748b', fontSize: 14, padding: '2px 5px', lineHeight: 1, borderRadius: 4,
267
- },
268
- summary: {
269
- display: 'flex', gap: 6, padding: '8px 12px',
270
- borderBottom: '1px solid #f1f5f9', background: '#f8fafc', flexShrink: 0,
271
- },
272
- toolbar: {
273
- display: 'flex', alignItems: 'center',
274
- borderBottom: '1px solid #f1f5f9', padding: '0 12px',
275
- gap: 2, flexShrink: 0,
276
- },
277
- list: {
278
- flex: 1, overflowY: 'auto', padding: '10px 10px',
279
- },
280
- footer: {
281
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
282
- padding: '7px 12px', borderTop: '1px solid #f1f5f9',
283
- background: '#f8fafc', fontSize: 10, color: '#94a3b8', flexShrink: 0,
284
- },
392
+ const btnBg = scanning ? '#64748b' : vCount > 0 ? '#7c3aed' : '#16a34a';
393
+ const iconBtnStyle = {
394
+ background: 'none', border: 'none', cursor: 'pointer',
395
+ color: '#64748b', fontSize: 14, padding: '2px 5px', lineHeight: 1, borderRadius: 4,
285
396
  };
286
397
  const chip = (color, count) => ({
287
398
  display: 'flex', alignItems: 'center', gap: 3, fontSize: 11, fontWeight: 600,
@@ -302,17 +413,34 @@ const WcagDevOverlay = ({ level = 'AA', rules, position = 'bottom-right', deboun
302
413
  padding: '4px 10px', fontSize: 10, fontWeight: 600,
303
414
  cursor: scanning ? 'wait' : 'pointer', opacity: scanning ? 0.55 : 1,
304
415
  };
305
- return ((0, jsx_runtime_1.jsxs)("div", { ref: overlayRef, style: anchorStyle, "data-wcag-overlay": "true", children: [open && ((0, jsx_runtime_1.jsxs)("div", { style: s.panel, role: "dialog", "aria-label": "WCAG Dev Inspector", children: [(0, jsx_runtime_1.jsxs)("div", { style: s.header, onMouseDown: onDragStart, children: [(0, jsx_runtime_1.jsx)("span", { style: { fontSize: 14 }, children: "\u267F" }), (0, jsx_runtime_1.jsx)("span", { style: { fontWeight: 700, fontSize: 13, flex: 1, color: '#f1f5f9' }, children: "WCAG Inspector" }), (0, jsx_runtime_1.jsx)("span", { style: {
416
+ const viewActive = (v) => ({
417
+ ...iconBtnStyle,
418
+ color: view === v ? '#7c3aed' : '#64748b',
419
+ background: view === v ? '#f5f3ff' : 'none',
420
+ });
421
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: overlayRef, style: anchorStyle, "data-wcag-overlay": "true", children: [open && ((0, jsx_runtime_1.jsxs)("div", { style: {
422
+ position: 'absolute', bottom: pos ? undefined : 48, top: pos ? 48 : undefined,
423
+ [isLeft ? 'left' : 'right']: 0, width: 410, maxHeight: '75vh',
424
+ background: '#fff', borderRadius: 10,
425
+ boxShadow: '0 8px 40px rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.07)',
426
+ display: 'flex', flexDirection: 'column', overflow: 'hidden',
427
+ }, role: "dialog", "aria-label": "WCAG Dev Inspector", children: [(0, jsx_runtime_1.jsxs)("div", { style: {
428
+ display: 'flex', alignItems: 'center', gap: 8,
429
+ padding: '10px 12px', background: '#0f172a',
430
+ cursor: 'grab', userSelect: 'none', flexShrink: 0,
431
+ }, onMouseDown: onDragStart, children: [(0, jsx_runtime_1.jsx)("span", { style: { fontSize: 14 }, children: "\u267F" }), (0, jsx_runtime_1.jsx)("span", { style: { fontWeight: 700, fontSize: 13, flex: 1, color: '#f1f5f9' }, children: "WCAG Inspector" }), (0, jsx_runtime_1.jsx)("span", { style: {
306
432
  fontSize: 9, fontWeight: 700, color: '#a78bfa',
307
433
  background: '#4c1d95', borderRadius: 4, padding: '2px 6px', letterSpacing: '0.05em',
308
- }, children: level }), (0, jsx_runtime_1.jsx)("button", { onClick: scan, disabled: scanning, title: "Rescan", style: s.headerBtn, children: scanning ? '⏳' : '↻' }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setOpen(false), title: "Close (Alt+Shift+W)", style: s.headerBtn, children: "\u2715" })] }), (0, jsx_runtime_1.jsxs)("div", { style: s.summary, children: [(0, jsx_runtime_1.jsxs)("span", { style: chip('#dc2626', vCount), children: ["\u2717 ", vCount, " violation", vCount !== 1 ? 's' : ''] }), (0, jsx_runtime_1.jsxs)("span", { style: chip('#ea580c', wCount), children: ["\u26A0 ", wCount, " warning", wCount !== 1 ? 's' : ''] }), (0, jsx_runtime_1.jsxs)("span", { style: chip('#16a34a', pCount), children: ["\u2713 ", pCount, " pass", pCount !== 1 ? 'es' : ''] })] }), (0, jsx_runtime_1.jsxs)("div", { style: s.toolbar, children: [(0, jsx_runtime_1.jsxs)("button", { style: tabBtn(tab === 'violations'), onClick: () => setTab('violations'), children: ["Violations (", vCount, ")"] }), (0, jsx_runtime_1.jsxs)("button", { style: tabBtn(tab === 'warnings'), onClick: () => setTab('warnings'), children: ["Warnings (", wCount, ")"] }), (0, jsx_runtime_1.jsxs)("select", { style: {
309
- marginLeft: 'auto', fontSize: 10, border: '1px solid #e2e8f0',
310
- borderRadius: 5, padding: '3px 6px', color: '#475569', background: '#fff', cursor: 'pointer', outline: 'none',
311
- }, value: filter, onChange: e => setFilter(e.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "all", children: "All" }), (0, jsx_runtime_1.jsx)("option", { value: "critical", children: "Critical" }), (0, jsx_runtime_1.jsx)("option", { value: "serious", children: "Serious" }), (0, jsx_runtime_1.jsx)("option", { value: "moderate", children: "Moderate" }), (0, jsx_runtime_1.jsx)("option", { value: "minor", children: "Minor" })] })] }), (0, jsx_runtime_1.jsxs)("div", { style: s.list, children: [scanning && !results && ((0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', color: '#94a3b8', padding: '28px 0', fontSize: 12 }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontSize: 22, marginBottom: 8 }, children: "\u23F3" }), "Scanning\u2026"] })), !scanning && isEmpty && ((0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', padding: '28px 0' }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontSize: 28, marginBottom: 8 }, children: tab === 'violations' ? '✅' : '🔕' }), (0, jsx_runtime_1.jsxs)("div", { style: { color: '#64748b', fontSize: 12, fontWeight: 500 }, children: ["No ", tab, " found"] }), filter !== 'all' && ((0, jsx_runtime_1.jsx)("button", { style: { marginTop: 6, fontSize: 11, color: '#7c3aed', background: 'none', border: 'none', cursor: 'pointer' }, onClick: () => setFilter('all'), children: "Clear filter" }))] })), items === null || items === void 0 ? void 0 : items.map((item, i) => ((0, jsx_runtime_1.jsx)(ViolationCard, { item: item, pinned: pinnedEl === item.domElement && item.domElement != null, onPin: handlePin }, `${item.rule}-${i}`)))] }), (0, jsx_runtime_1.jsxs)("div", { style: s.footer, children: [(0, jsx_runtime_1.jsxs)("span", { children: [pinnedEl ? '📍 pinned · ' : '', "Scanned ", elapsed, results ? ` · ${results.duration}ms` : ''] }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [pinnedEl && ((0, jsx_runtime_1.jsx)("button", { style: { ...rescanBtn, background: '#64748b' }, onClick: () => { clearAllHighlights(); setPinnedEl(null); }, children: "Unpin" })), (0, jsx_runtime_1.jsx)("button", { style: rescanBtn, onClick: scan, disabled: scanning, children: scanning ? 'Scanning…' : 'Rescan' })] })] })] })), (0, jsx_runtime_1.jsxs)("button", { style: s.toggle, onClick: () => setOpen(o => !o), title: "Toggle WCAG Inspector (Alt+Shift+W)", "aria-expanded": open, children: [(0, jsx_runtime_1.jsx)("span", { style: { fontSize: 14 }, children: "\u267F" }), scanning
434
+ }, children: level }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setView(v => v === 'settings' ? 'list' : 'settings'), style: viewActive('settings'), title: "Settings", children: "\u2699" }), (0, jsx_runtime_1.jsx)("button", { onClick: scan, disabled: scanning, title: "Rescan", style: iconBtnStyle, children: scanning ? '⏳' : '↻' }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setOpen(false), title: "Close (Alt+Shift+W)", style: iconBtnStyle, children: "\u2715" })] }), view === 'settings' ? ((0, jsx_runtime_1.jsx)(SettingsPanel, { apiKey: apiKey, preset: activePreset, onSave: k => setApiKey(k), onPresetChange: setActivePreset })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: 6, padding: '8px 12px', borderBottom: '1px solid #f1f5f9', background: '#f8fafc', flexShrink: 0 }, children: [(0, jsx_runtime_1.jsxs)("span", { style: chip('#dc2626', vCount), children: ["\u2717 ", vCount, " violation", vCount !== 1 ? 's' : ''] }), (0, jsx_runtime_1.jsxs)("span", { style: chip('#ea580c', wCount), children: ["\u26A0 ", wCount, " warning", wCount !== 1 ? 's' : ''] }), (0, jsx_runtime_1.jsxs)("span", { style: chip('#16a34a', pCount), children: ["\u2713 ", pCount, " pass", pCount !== 1 ? 'es' : ''] }), apiKey && (0, jsx_runtime_1.jsx)("span", { style: { ...chip('#7c3aed', 1), marginLeft: 'auto' }, title: "AI suggestions enabled", children: "\u2728 AI" })] }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', borderBottom: '1px solid #f1f5f9', padding: '0 12px', gap: 2, flexShrink: 0 }, children: [(0, jsx_runtime_1.jsxs)("button", { style: tabBtn(tab === 'violations'), onClick: () => setTab('violations'), children: ["Violations (", vCount, ")"] }), (0, jsx_runtime_1.jsxs)("button", { style: tabBtn(tab === 'warnings'), onClick: () => setTab('warnings'), children: ["Warnings (", wCount, ")"] }), (0, jsx_runtime_1.jsxs)("select", { style: { marginLeft: 'auto', fontSize: 10, border: '1px solid #e2e8f0', borderRadius: 5, padding: '3px 6px', color: '#475569', background: '#fff', cursor: 'pointer', outline: 'none' }, value: filter, onChange: e => setFilter(e.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "all", children: "All" }), (0, jsx_runtime_1.jsx)("option", { value: "critical", children: "Critical" }), (0, jsx_runtime_1.jsx)("option", { value: "serious", children: "Serious" }), (0, jsx_runtime_1.jsx)("option", { value: "moderate", children: "Moderate" }), (0, jsx_runtime_1.jsx)("option", { value: "minor", children: "Minor" })] })] }), (0, jsx_runtime_1.jsxs)("div", { style: { flex: 1, overflowY: 'auto', padding: '10px' }, children: [scanning && !results && ((0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', color: '#94a3b8', padding: '28px 0', fontSize: 12 }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontSize: 22, marginBottom: 8 }, children: "\u23F3" }), "Scanning\u2026"] })), !scanning && isEmpty && ((0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', padding: '28px 0' }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontSize: 28, marginBottom: 8 }, children: tab === 'violations' ? '✅' : '🔕' }), (0, jsx_runtime_1.jsxs)("div", { style: { color: '#64748b', fontSize: 12, fontWeight: 500 }, children: ["No ", tab, " found"] }), filter !== 'all' && ((0, jsx_runtime_1.jsx)("button", { style: { marginTop: 6, fontSize: 11, color: '#7c3aed', background: 'none', border: 'none', cursor: 'pointer' }, onClick: () => setFilter('all'), children: "Clear filter" }))] })), items === null || items === void 0 ? void 0 : items.map((item, i) => ((0, jsx_runtime_1.jsx)(ViolationCard, { item: item, pinned: pinnedEl === item.domElement && item.domElement != null, onPin: handlePin, apiKey: apiKey }, `${item.rule}-${i}`)))] }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '7px 12px', borderTop: '1px solid #f1f5f9', background: '#f8fafc', fontSize: 10, color: '#94a3b8', flexShrink: 0 }, children: [(0, jsx_runtime_1.jsxs)("span", { children: [pinnedEl ? '📍 pinned · ' : '', "Scanned ", elapsed, results ? ` · ${results.duration}ms` : ''] }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [pinnedEl && ((0, jsx_runtime_1.jsx)("button", { style: { ...rescanBtn, background: '#64748b' }, onClick: () => { clearAllHighlights(); setPinnedEl(null); }, children: "Unpin" })), (0, jsx_runtime_1.jsx)("button", { style: rescanBtn, onClick: scan, disabled: scanning, children: scanning ? 'Scanning…' : 'Rescan' })] })] })] }))] })), (0, jsx_runtime_1.jsxs)("button", { style: {
435
+ display: 'flex', alignItems: 'center', gap: 6,
436
+ background: btnBg, color: '#fff', border: 'none', borderRadius: 22,
437
+ padding: '8px 14px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
438
+ boxShadow: '0 2px 12px rgba(0,0,0,0.2)', userSelect: 'none',
439
+ }, onClick: () => setOpen(o => !o), title: "Toggle WCAG Inspector (Alt+Shift+W)", "aria-expanded": open, children: [(0, jsx_runtime_1.jsx)("span", { style: { fontSize: 14 }, children: "\u267F" }), scanning
312
440
  ? (0, jsx_runtime_1.jsx)("span", { style: { opacity: 0.8, fontSize: 11 }, children: "scanning\u2026" })
313
441
  : vCount > 0
314
- ? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { style: s.badge, children: vCount }), (0, jsx_runtime_1.jsx)("span", { style: { fontSize: 11 }, children: "issues" })] })
315
- : (0, jsx_runtime_1.jsx)("span", { style: s.badge, children: "\u2713" })] })] }));
442
+ ? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { style: { background: 'rgba(255,255,255,0.22)', borderRadius: 9, padding: '1px 6px', fontSize: 11, fontWeight: 700 }, children: vCount }), (0, jsx_runtime_1.jsx)("span", { style: { fontSize: 11 }, children: " issues" })] })
443
+ : (0, jsx_runtime_1.jsx)("span", { style: { background: 'rgba(255,255,255,0.22)', borderRadius: 9, padding: '1px 6px', fontSize: 11, fontWeight: 700 }, children: "\u2713" })] })] }));
316
444
  };
317
445
  exports.WcagDevOverlay = WcagDevOverlay;
318
446
  exports.default = exports.WcagDevOverlay;
@@ -2,10 +2,13 @@ import { ScannerOptions, Violation, Warning, Pass } from '../types';
2
2
  export interface AnnotatedViolation extends Violation {
3
3
  domElement?: Element;
4
4
  elementPath?: string;
5
+ /** Precise nth-child CSS selector — use for querySelector to find element */
6
+ elementSelector?: string;
5
7
  }
6
8
  export interface AnnotatedWarning extends Warning {
7
9
  domElement?: Element;
8
10
  elementPath?: string;
11
+ elementSelector?: string;
9
12
  }
10
13
  export interface BrowserScanResults {
11
14
  violations: AnnotatedViolation[];
@@ -14,5 +17,12 @@ export interface BrowserScanResults {
14
17
  duration: number;
15
18
  }
16
19
  export declare function scanBrowserPage(options?: ScannerOptions): Promise<BrowserScanResults>;
17
- /** Build a readable CSS-selector-style breadcrumb for an element. */
20
+ /**
21
+ * Build a precise nth-child CSS selector path for an element.
22
+ * This is unambiguous and always finds the exact element.
23
+ */
24
+ export declare function getNthChildSelector(el: Element): string;
25
+ /** Build a human-readable breadcrumb label for an element. */
18
26
  export declare function getElementPath(el: Element): string;
27
+ export declare function findElement(item: Violation | Warning, doc: Document): Element | null;
28
+ export declare function findBySnippet(snippet: string, doc: Document): Element | null;