a11y-hud 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,6 +6,22 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
6
 
7
7
  var axe__default = /*#__PURE__*/_interopDefault(axe);
8
8
 
9
+ // src/bookmarklet.ts
10
+ var CDN_BASE = "https://cdn.jsdelivr.net/npm/a11y-hud";
11
+ function generateBookmarklet(version = "latest") {
12
+ const src = `${CDN_BASE}@${version}/dist/index.umd.js`;
13
+ const js = [
14
+ "(function(){",
15
+ "if(window.A11yHud){window.A11yHud.mount();return;}",
16
+ "var s=document.createElement('script');",
17
+ `s.src='${src}';`,
18
+ "s.onload=function(){window.A11yHud.mount();};",
19
+ "document.head.appendChild(s);",
20
+ "})()"
21
+ ].join("");
22
+ return `javascript:${js}`;
23
+ }
24
+
9
25
  // src/icons/alert-circle.svg
10
26
  var alert_circle_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>\n';
11
27
 
@@ -24,6 +40,9 @@ var chevron_up_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" hei
24
40
  // src/icons/copy.svg
25
41
  var copy_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>\n';
26
42
 
43
+ // src/icons/download.svg
44
+ var download_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>';
45
+
27
46
  // src/icons/filter.svg
28
47
  var filter_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>\n';
29
48
 
@@ -45,6 +64,12 @@ var refresh_cw_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" hei
45
64
  // src/icons/sun.svg
46
65
  var sun_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>\n';
47
66
 
67
+ // src/icons/trash-2.svg
68
+ var trash_2_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>';
69
+
70
+ // src/icons/upload.svg
71
+ var upload_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>';
72
+
48
73
  // src/icons/x.svg
49
74
  var x_default = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>\n';
50
75
 
@@ -56,6 +81,7 @@ var registry = {
56
81
  "chevron-down": chevron_down_default,
57
82
  "chevron-up": chevron_up_default,
58
83
  copy: copy_default,
84
+ download: download_default,
59
85
  filter: filter_default,
60
86
  info: info_default,
61
87
  keyboard: keyboard_default,
@@ -63,12 +89,180 @@ var registry = {
63
89
  moon: moon_default,
64
90
  "refresh-cw": refresh_cw_default,
65
91
  sun: sun_default,
92
+ "trash-2": trash_2_default,
93
+ upload: upload_default,
66
94
  x: x_default
67
95
  };
68
96
  function icon(name) {
69
97
  return registry[name] ?? "";
70
98
  }
71
99
 
100
+ // src/ignores.ts
101
+ var STORAGE_KEY = "a11y-hud:ignores";
102
+ function load() {
103
+ try {
104
+ const raw = localStorage.getItem(STORAGE_KEY);
105
+ if (raw === null) return [];
106
+ return JSON.parse(raw);
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+ function save(entries) {
112
+ try {
113
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
114
+ } catch {
115
+ }
116
+ }
117
+ function matches(a, b) {
118
+ return a.ruleId === b.ruleId && a.selector === b.selector;
119
+ }
120
+ function addIgnore(ruleId, selector) {
121
+ const entries = load();
122
+ const entry = selector !== void 0 ? { ruleId, selector } : { ruleId };
123
+ if (entries.some((e) => matches(e, entry))) return;
124
+ entries.push(entry);
125
+ save(entries);
126
+ }
127
+ function removeIgnore(ruleId, selector) {
128
+ const entry = selector !== void 0 ? { ruleId, selector } : { ruleId };
129
+ save(load().filter((e) => !matches(e, entry)));
130
+ }
131
+ function clearIgnores() {
132
+ save([]);
133
+ }
134
+ function listIgnores() {
135
+ return load();
136
+ }
137
+ function exportIgnores() {
138
+ return JSON.stringify(listIgnores(), null, 2);
139
+ }
140
+ function importIgnores(json) {
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(json);
144
+ } catch {
145
+ return;
146
+ }
147
+ if (!Array.isArray(parsed)) return;
148
+ const entries = [];
149
+ for (const item of parsed) {
150
+ if (typeof item !== "object" || item === null) continue;
151
+ const { ruleId, selector } = item;
152
+ if (typeof ruleId !== "string") continue;
153
+ if (selector !== void 0 && typeof selector !== "string") continue;
154
+ entries.push(selector !== void 0 ? { ruleId, selector } : { ruleId });
155
+ }
156
+ save(entries);
157
+ }
158
+
159
+ // src/keyboard.ts
160
+ var FOCUSABLE_SELECTOR = [
161
+ "a[href]:not([tabindex='-1'])",
162
+ "button:not([disabled]):not([tabindex='-1'])",
163
+ "input:not([disabled]):not([tabindex='-1'])",
164
+ "select:not([disabled]):not([tabindex='-1'])",
165
+ "textarea:not([disabled]):not([tabindex='-1'])",
166
+ "[tabindex]:not([tabindex='-1'])",
167
+ "details > summary:not([tabindex='-1'])",
168
+ "area[href]:not([tabindex='-1'])"
169
+ ].join(", ");
170
+ var INTERACTIVE_SELECTOR = "a[href], button, input, select, textarea, [role='button'], [role='link']";
171
+ function buildSelector(el) {
172
+ if (el.id) {
173
+ return `#${el.id}`;
174
+ }
175
+ let selector = el.tagName.toLowerCase();
176
+ if (el.className && typeof el.className === "string") {
177
+ const classes = el.className.trim().split(/\s+/).slice(0, 2);
178
+ if (classes.length > 0 && classes[0] !== "") {
179
+ selector += classes.map((c) => `.${c}`).join("");
180
+ }
181
+ }
182
+ return selector;
183
+ }
184
+ function getFocusableElements(scope) {
185
+ const all = Array.from(scope.querySelectorAll(FOCUSABLE_SELECTOR));
186
+ const positive = [];
187
+ const zero = [];
188
+ for (const el of all) {
189
+ const ti = el.tabIndex;
190
+ if (ti > 0) {
191
+ positive.push(el);
192
+ } else {
193
+ zero.push(el);
194
+ }
195
+ }
196
+ positive.sort((a, b) => a.tabIndex - b.tabIndex);
197
+ const ordered = [...positive, ...zero];
198
+ return ordered.map((element, i) => ({
199
+ element,
200
+ index: i + 1,
201
+ selector: buildSelector(element),
202
+ tabIndex: element.tabIndex
203
+ }));
204
+ }
205
+ function injectFocusOrderOverlay(elements) {
206
+ if (!document.getElementById("a11y-hud-kbd-style")) {
207
+ const style = document.createElement("style");
208
+ style.id = "a11y-hud-kbd-style";
209
+ style.textContent = [
210
+ "#a11y-hud-kbd-overlay{position:absolute;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483646}",
211
+ ".a11y-hud-kbd-badge{position:absolute;min-width:20px;height:20px;padding:0 5px;border-radius:10px;background:#7c3aed;color:#fff;font:700 11px/20px system-ui,sans-serif;text-align:center;pointer-events:none}"
212
+ ].join("");
213
+ document.head.appendChild(style);
214
+ }
215
+ const overlay = document.createElement("div");
216
+ overlay.id = "a11y-hud-kbd-overlay";
217
+ document.body.appendChild(overlay);
218
+ const scrollX = window.scrollX;
219
+ const scrollY = window.scrollY;
220
+ for (const info of elements) {
221
+ const rect = info.element.getBoundingClientRect();
222
+ const badge = document.createElement("span");
223
+ badge.className = "a11y-hud-kbd-badge";
224
+ badge.textContent = String(info.index);
225
+ badge.style.top = `${rect.top + scrollY}px`;
226
+ badge.style.left = `${rect.left + scrollX}px`;
227
+ overlay.appendChild(badge);
228
+ }
229
+ return () => {
230
+ document.getElementById("a11y-hud-kbd-overlay")?.remove();
231
+ document.getElementById("a11y-hud-kbd-style")?.remove();
232
+ };
233
+ }
234
+ function detectKeyboardViolations(elements, scope = document.body) {
235
+ const violations = [];
236
+ if (elements.length === 0) {
237
+ violations.push({
238
+ type: "no-focusable-elements",
239
+ message: "No focusable elements found in scope. Users who rely on keyboard navigation cannot reach any interactive content."
240
+ });
241
+ }
242
+ for (const info of elements) {
243
+ if (info.tabIndex > 0) {
244
+ violations.push({
245
+ type: "positive-tabindex",
246
+ element: info.element,
247
+ selector: info.selector,
248
+ message: `tabindex="${info.tabIndex}" on <${info.element.tagName.toLowerCase()}> disrupts the natural tab order. Use tabindex="0" instead.`
249
+ });
250
+ }
251
+ }
252
+ const excluded = Array.from(scope.querySelectorAll(INTERACTIVE_SELECTOR)).filter(
253
+ (el) => el.tabIndex === -1
254
+ );
255
+ for (const el of excluded) {
256
+ violations.push({
257
+ type: "interactive-excluded",
258
+ element: el,
259
+ selector: buildSelector(el),
260
+ message: `<${el.tagName.toLowerCase()}> with tabindex="-1" is removed from the tab order. Keyboard users cannot reach this element.`
261
+ });
262
+ }
263
+ return violations;
264
+ }
265
+
72
266
  // src/observer.ts
73
267
  function debounce(fn, delay) {
74
268
  let timer;
@@ -87,6 +281,7 @@ async function runScan(target = document.body, runOnly) {
87
281
  }
88
282
  const options = runOnly && runOnly.length > 0 ? { runOnly: { type: "tag", values: runOnly } } : {};
89
283
  const run = axe__default.default.run(target, options).then((results) => {
284
+ const ignores = listIgnores();
90
285
  results.violations = results.violations.map((v) => ({
91
286
  ...v,
92
287
  nodes: v.nodes.filter((node) => {
@@ -94,7 +289,19 @@ async function runScan(target = document.body, runOnly) {
94
289
  const selector = (Array.isArray(first) ? first[0] : first) ?? "";
95
290
  return !String(selector).toLowerCase().startsWith("a11y-hud");
96
291
  })
97
- })).filter((v) => v.nodes.length > 0);
292
+ })).filter((v) => v.nodes.length > 0).map((v) => {
293
+ if (ignores.some((e) => e.ruleId === v.id && e.selector === void 0)) {
294
+ return null;
295
+ }
296
+ const hasNodeIgnores = ignores.some((e) => e.ruleId === v.id && e.selector !== void 0);
297
+ if (!hasNodeIgnores) return v;
298
+ const nodes = v.nodes.filter((node) => {
299
+ const last = node.target[node.target.length - 1];
300
+ const sel = (Array.isArray(last) ? last[last.length - 1] : last) ?? "";
301
+ return !ignores.some((e) => e.ruleId === v.id && e.selector === sel);
302
+ });
303
+ return nodes.length > 0 ? { ...v, nodes } : null;
304
+ }).filter((v) => v !== null);
98
305
  return results;
99
306
  }).finally(() => {
100
307
  activeRun = null;
@@ -104,10 +311,10 @@ async function runScan(target = document.body, runOnly) {
104
311
  }
105
312
 
106
313
  // src/styles/base.css
107
- var base_default = ':host {\n --a11y-hud-bg: #1e1e2e;\n --a11y-hud-bg-elevated: #313244;\n --a11y-hud-bg-hover: #45475a;\n --a11y-hud-border: #45475a;\n --a11y-hud-text: #cdd6f4;\n --a11y-hud-text-muted: #a6adc8;\n --a11y-hud-critical: #f38ba8;\n --a11y-hud-serious: #fab387;\n --a11y-hud-moderate: #f9e2af;\n --a11y-hud-minor: #89b4fa;\n --a11y-hud-highlight: #cba6f7;\n --a11y-hud-focus-ring: #89b4fa;\n --a11y-hud-radius: 8px;\n --a11y-hud-font: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;\n --a11y-hud-z-index: 999999;\n --a11y-hud-panel-width: 380px;\n\n all: initial;\n display: block;\n position: fixed;\n inset-block-end: 16px;\n inset-inline-end: 16px;\n z-index: var(--a11y-hud-z-index);\n font-family: var(--a11y-hud-font);\n font-size: 13px;\n line-height: 1.5;\n color: var(--a11y-hud-text);\n contain: layout style;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\n.panel {\n width: var(--a11y-hud-panel-width);\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n background: var(--a11y-hud-bg);\n border: 1px solid var(--a11y-hud-border);\n border-radius: var(--a11y-hud-radius);\n overflow: hidden;\n box-shadow: 0 8px 32px rgb(0 0 0 / 40%);\n}\n\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n background: var(--a11y-hud-bg-elevated);\n border-block-end: 1px solid var(--a11y-hud-border);\n flex-shrink: 0;\n}\n\n.panel-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n letter-spacing: 0.02em;\n}\n\n.panel-title-icon {\n display: flex;\n align-items: center;\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n}\n\n.panel-title-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.panel-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding-inline: 6px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 700;\n background: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.panel-count[data-count="0"] {\n background: var(--a11y-hud-minor);\n}\n\n.panel-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.btn-icon {\n all: unset;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border-radius: 4px;\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-icon svg {\n width: 14px;\n height: 14px;\n pointer-events: none;\n}\n\n.btn-icon:hover {\n background: var(--a11y-hud-bg-hover);\n color: var(--a11y-hud-text);\n}\n\n.btn-icon:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.btn-icon[data-scanning] {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.panel-filters {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 12px;\n border-block-end: 1px solid var(--a11y-hud-border);\n flex-shrink: 0;\n}\n\n.filter-group {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n\n.filter-chip {\n all: unset;\n display: inline-flex;\n align-items: center;\n height: 22px;\n padding-inline: 8px;\n border-radius: 11px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n cursor: pointer;\n border: 1px solid var(--a11y-hud-border);\n color: var(--a11y-hud-text-muted);\n background: transparent;\n transition:\n background 120ms,\n color 120ms,\n border-color 120ms;\n}\n\n.filter-chip:hover {\n background: var(--a11y-hud-bg-hover);\n color: var(--a11y-hud-text);\n}\n\n.filter-chip:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.filter-chip[aria-pressed="true"][data-severity="critical"] {\n background: var(--a11y-hud-critical);\n border-color: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="serious"] {\n background: var(--a11y-hud-serious);\n border-color: var(--a11y-hud-serious);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="moderate"] {\n background: var(--a11y-hud-moderate);\n border-color: var(--a11y-hud-moderate);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="minor"] {\n background: var(--a11y-hud-minor);\n border-color: var(--a11y-hud-minor);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-level] {\n background: var(--a11y-hud-bg-elevated);\n border-color: var(--a11y-hud-text-muted);\n color: var(--a11y-hud-text);\n}\n\n.panel-body {\n flex: 1;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.panel-body:focus-visible {\n outline: none;\n}\n\n.violation-list {\n list-style: none;\n}\n\n.violation-item {\n border-block-end: 1px solid var(--a11y-hud-border);\n}\n\n.violation-item:last-child {\n border-block-end: none;\n}\n\n.violation-toggle {\n all: unset;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 8px 12px;\n cursor: pointer;\n text-align: start;\n transition: background 120ms;\n}\n\n.violation-toggle:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.violation-toggle:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: -2px;\n}\n\n.severity-icon {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n}\n\n.severity-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.severity-icon[data-severity="critical"] {\n color: var(--a11y-hud-critical);\n}\n\n.severity-icon[data-severity="serious"] {\n color: var(--a11y-hud-serious);\n}\n\n.severity-icon[data-severity="moderate"] {\n color: var(--a11y-hud-moderate);\n}\n\n.severity-icon[data-severity="minor"] {\n color: var(--a11y-hud-minor);\n}\n\n.violation-rule {\n flex: 1;\n font-size: 12px;\n font-weight: 500;\n word-break: break-word;\n}\n\n.violation-node-count {\n font-size: 11px;\n color: var(--a11y-hud-text-muted);\n flex-shrink: 0;\n}\n\n.chevron-icon {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n color: var(--a11y-hud-text-muted);\n transition: transform 200ms;\n}\n\n.chevron-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.violation-toggle[aria-expanded="true"] .chevron-icon {\n transform: rotate(180deg);\n}\n\n.violation-detail {\n padding: 0 12px 10px 34px;\n display: none;\n flex-direction: column;\n gap: 8px;\n}\n\n.violation-detail[data-open] {\n display: flex;\n}\n\n.violation-description {\n font-size: 12px;\n color: var(--a11y-hud-text-muted);\n line-height: 1.6;\n}\n\n.violation-nodes-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.violation-node-item {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.btn-highlight {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-highlight);\n border: 1px solid var(--a11y-hud-border);\n cursor: pointer;\n word-break: break-all;\n transition: background 120ms;\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.btn-highlight:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.btn-highlight:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.violation-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.violation-help-link {\n font-size: 11px;\n color: var(--a11y-hud-minor);\n text-decoration: none;\n}\n\n.violation-help-link:hover {\n text-decoration: underline;\n}\n\n.violation-help-link:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n border-radius: 2px;\n}\n\n.btn-copy-single {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-copy-single svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n\n.btn-copy-single:hover {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-text);\n}\n\n.btn-copy-single:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 32px 16px;\n color: var(--a11y-hud-text-muted);\n}\n\n.empty-state-icon {\n width: 32px;\n height: 32px;\n color: var(--a11y-hud-minor);\n}\n\n.empty-state-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.empty-state-title {\n font-weight: 600;\n color: var(--a11y-hud-text);\n}\n\n.empty-state-body {\n font-size: 12px;\n text-align: center;\n}\n\n.scanning-state {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 24px 16px;\n color: var(--a11y-hud-text-muted);\n font-size: 12px;\n}\n\n.scanning-spinner {\n width: 16px;\n height: 16px;\n color: var(--a11y-hud-minor);\n animation: spin 1s linear infinite;\n}\n\n.scanning-spinner svg {\n width: 100%;\n height: 100%;\n}\n\n/* \u2500\u2500 FAB (minimized state) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.fab {\n all: unset;\n display: none;\n align-items: center;\n gap: 6px;\n padding: 8px 14px;\n background: var(--a11y-hud-bg);\n border: 1px solid var(--a11y-hud-border);\n border-radius: 24px;\n color: var(--a11y-hud-text);\n cursor: pointer;\n font-family: var(--a11y-hud-font);\n font-size: 13px;\n font-weight: 600;\n box-shadow: 0 4px 16px rgb(0 0 0 / 30%);\n transition: background 120ms;\n}\n\n:host([data-minimized]) .fab {\n display: flex;\n}\n\n:host([data-minimized]) .panel {\n display: none;\n}\n\n@keyframes panel-in {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.panel {\n animation: panel-in 120ms ease-out;\n}\n\n.fab:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.fab:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 2px;\n}\n\n.fab-icon {\n display: flex;\n align-items: center;\n width: 14px;\n height: 14px;\n}\n\n.fab-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.fab-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n padding-inline: 5px;\n border-radius: 9px;\n font-size: 11px;\n font-weight: 700;\n background: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.fab-count[data-count="0"] {\n background: var(--a11y-hud-minor);\n}\n';
314
+ var base_default = ':host {\n --a11y-hud-bg: #1e1e2e;\n --a11y-hud-bg-elevated: #313244;\n --a11y-hud-bg-hover: #45475a;\n --a11y-hud-border: #45475a;\n --a11y-hud-text: #cdd6f4;\n --a11y-hud-text-muted: #a6adc8;\n --a11y-hud-critical: #f38ba8;\n --a11y-hud-serious: #fab387;\n --a11y-hud-moderate: #f9e2af;\n --a11y-hud-minor: #89b4fa;\n --a11y-hud-highlight: #cba6f7;\n --a11y-hud-tag: #94e2d5;\n --a11y-hud-focus-ring: #89b4fa;\n --a11y-hud-radius: 8px;\n --a11y-hud-font: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;\n --a11y-hud-z-index: 999999;\n --a11y-hud-panel-width: 380px;\n\n all: initial;\n display: block;\n position: fixed;\n inset-block-end: 16px;\n inset-inline-end: 16px;\n z-index: var(--a11y-hud-z-index);\n font-family: var(--a11y-hud-font);\n font-size: 13px;\n line-height: 1.5;\n color: var(--a11y-hud-text);\n contain: layout style;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\n.panel {\n width: var(--a11y-hud-panel-width);\n max-height: 70dvh;\n display: flex;\n flex-direction: column;\n background: var(--a11y-hud-bg);\n border: 1px solid var(--a11y-hud-border);\n border-radius: var(--a11y-hud-radius);\n overflow: hidden;\n box-shadow: 0 8px 32px rgb(0 0 0 / 40%);\n}\n\n.panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n background: var(--a11y-hud-bg-elevated);\n border-block-end: 1px solid var(--a11y-hud-border);\n flex-shrink: 0;\n}\n\n.btn-panel-title {\n all: unset;\n display: flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n letter-spacing: 0.02em;\n cursor: pointer;\n border-radius: 4px;\n padding: 2px 4px;\n margin: -2px -4px;\n transition: color 120ms;\n}\n\n.btn-panel-title:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 2px;\n}\n\n.panel-title-icon {\n display: flex;\n align-items: center;\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n position: relative;\n transition: color 120ms;\n}\n\n.panel-title-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.btn-panel-title[data-active] .panel-title-icon {\n color: var(--a11y-hud-highlight);\n}\n\n.panel-title-chevron {\n display: flex;\n align-items: center;\n width: 12px;\n height: 12px;\n color: var(--a11y-hud-text-muted);\n transition: transform 200ms;\n}\n\n.panel-title-chevron svg {\n width: 100%;\n height: 100%;\n}\n\n.btn-panel-title[aria-expanded="true"] .panel-title-chevron {\n transform: rotate(180deg);\n}\n\n.panel-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding-inline: 6px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 700;\n background: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.panel-count[data-count="0"] {\n background: var(--a11y-hud-minor);\n}\n\n.panel-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.btn-icon {\n all: unset;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border-radius: 4px;\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-icon svg {\n width: 14px;\n height: 14px;\n pointer-events: none;\n}\n\n.btn-icon:hover {\n background: var(--a11y-hud-bg-hover);\n color: var(--a11y-hud-text);\n}\n\n.btn-icon:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.btn-icon[aria-pressed="true"] {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-highlight);\n outline: 1px solid var(--a11y-hud-highlight);\n outline-offset: -1px;\n}\n\n.btn-icon[data-scanning] {\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n.panel-filters {\n display: none;\n flex-direction: column;\n gap: 6px;\n padding: 8px 12px;\n border-block-end: 1px solid var(--a11y-hud-border);\n flex-shrink: 0;\n}\n\n.panel-filters[data-open] {\n display: flex;\n}\n\n.filter-group {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n}\n\n.filter-chip {\n all: unset;\n display: inline-flex;\n align-items: center;\n height: 22px;\n padding-inline: 8px;\n border-radius: 11px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n cursor: pointer;\n border: 1px solid var(--a11y-hud-border);\n color: var(--a11y-hud-text-muted);\n background: transparent;\n transition:\n background 120ms,\n color 120ms,\n border-color 120ms;\n}\n\n.filter-chip:hover {\n background: var(--a11y-hud-bg-hover);\n color: var(--a11y-hud-text);\n}\n\n.filter-chip:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.filter-chip[aria-pressed="true"][data-severity="critical"] {\n background: var(--a11y-hud-critical);\n border-color: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="serious"] {\n background: var(--a11y-hud-serious);\n border-color: var(--a11y-hud-serious);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="moderate"] {\n background: var(--a11y-hud-moderate);\n border-color: var(--a11y-hud-moderate);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-severity="minor"] {\n background: var(--a11y-hud-minor);\n border-color: var(--a11y-hud-minor);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-level] {\n background: var(--a11y-hud-highlight);\n border-color: var(--a11y-hud-highlight);\n color: var(--a11y-hud-bg);\n}\n\n.filter-chip[aria-pressed="true"][data-ruleset] {\n background: var(--a11y-hud-tag);\n border-color: var(--a11y-hud-tag);\n color: var(--a11y-hud-bg);\n}\n\n.panel-body {\n flex: 1;\n overflow-x: hidden;\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.panel-body:focus-visible {\n outline: none;\n}\n\n.violation-list {\n list-style: none;\n}\n\n.violation-item {\n border-block-end: 1px solid var(--a11y-hud-border);\n}\n\n.violation-item:last-child {\n border-block-end: none;\n}\n\n.violation-toggle {\n all: unset;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 8px 12px;\n cursor: pointer;\n text-align: start;\n transition: background 120ms;\n}\n\n.violation-toggle:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.violation-toggle:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: -2px;\n}\n\n.severity-icon {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n}\n\n.severity-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.severity-icon[data-severity="critical"] {\n color: var(--a11y-hud-critical);\n}\n\n.severity-icon[data-severity="serious"] {\n color: var(--a11y-hud-serious);\n}\n\n.severity-icon[data-severity="moderate"] {\n color: var(--a11y-hud-moderate);\n}\n\n.severity-icon[data-severity="minor"] {\n color: var(--a11y-hud-minor);\n}\n\n.violation-rule {\n flex: 1;\n font-size: 12px;\n font-weight: 500;\n word-break: break-word;\n}\n\n.violation-node-count {\n font-size: 11px;\n color: var(--a11y-hud-text-muted);\n flex-shrink: 0;\n}\n\n.chevron-icon {\n display: flex;\n align-items: center;\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n color: var(--a11y-hud-text-muted);\n transition: transform 200ms;\n}\n\n.chevron-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.violation-toggle[aria-expanded="true"] .chevron-icon {\n transform: rotate(180deg);\n}\n\n.violation-detail {\n padding: 0 12px 10px 34px;\n display: none;\n flex-direction: column;\n gap: 8px;\n}\n\n.violation-detail[data-open] {\n display: flex;\n}\n\n.violation-description {\n font-size: 12px;\n color: var(--a11y-hud-text-muted);\n line-height: 1.6;\n}\n\n.violation-nodes-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.violation-node-item {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.btn-highlight {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-highlight);\n border: 1px solid var(--a11y-hud-border);\n cursor: pointer;\n word-break: break-all;\n transition: background 120ms;\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.btn-highlight:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.btn-highlight:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.violation-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.violation-help-link {\n font-size: 11px;\n color: var(--a11y-hud-minor);\n text-decoration: none;\n}\n\n.violation-help-link:hover {\n text-decoration: underline;\n}\n\n.violation-help-link:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n border-radius: 2px;\n}\n\n.btn-copy-single {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-copy-single svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n\n.btn-copy-single:hover {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-text);\n}\n\n.btn-copy-single:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.btn-ignore-rule {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-ignore-rule svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n\n.btn-ignore-rule:hover {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-critical);\n}\n\n.btn-ignore-rule:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n/* \u2500\u2500 Ignored rules section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ignored-section {\n border-top: 1px solid var(--a11y-hud-border);\n margin-top: 8px;\n}\n\n.ignored-section-toggle {\n all: unset;\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 8px 12px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n font-weight: 600;\n color: var(--a11y-hud-text-muted);\n cursor: pointer;\n gap: 6px;\n box-sizing: border-box;\n}\n\n.ignored-section-toggle:hover {\n color: var(--a11y-hud-text);\n}\n\n.ignored-section-toggle:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.ignored-section-toggle .chevron-icon {\n width: 12px;\n height: 12px;\n transition: transform 120ms;\n flex-shrink: 0;\n}\n\n.ignored-section-toggle[aria-expanded="true"] .chevron-icon {\n transform: rotate(180deg);\n}\n\n.ignored-section-body {\n display: none;\n padding: 0 12px 10px;\n}\n\n.ignored-section-body[data-open] {\n display: block;\n}\n\n.ignored-empty {\n font-size: 11px;\n color: var(--a11y-hud-text-muted);\n margin: 0 0 8px;\n}\n\n.ignored-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0 0 8px;\n padding: 0;\n}\n\n.ignored-entry {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n}\n\n.ignored-rule-id {\n font-weight: 600;\n color: var(--a11y-hud-text);\n}\n\n.ignored-selector {\n font-size: 10px;\n font-family: ui-monospace, monospace;\n color: var(--a11y-hud-text-muted);\n background: var(--a11y-hud-bg-elevated);\n padding: 1px 4px;\n border-radius: 3px;\n}\n\n.btn-remove-ignore {\n all: unset;\n display: inline-flex;\n align-items: center;\n cursor: pointer;\n color: var(--a11y-hud-text-muted);\n margin-left: auto;\n padding: 2px;\n border-radius: 3px;\n transition: color 120ms;\n}\n\n.btn-remove-ignore svg {\n width: 12px;\n height: 12px;\n pointer-events: none;\n}\n\n.btn-remove-ignore:hover {\n color: var(--a11y-hud-critical);\n}\n\n.btn-remove-ignore:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.ignored-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.btn-sm {\n all: unset;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-family: var(--a11y-hud-font);\n color: var(--a11y-hud-text-muted);\n border: 1px solid var(--a11y-hud-border);\n cursor: pointer;\n transition:\n background 120ms,\n color 120ms;\n}\n\n.btn-sm svg {\n width: 11px;\n height: 11px;\n pointer-events: none;\n}\n\n.btn-sm:hover {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-text);\n}\n\n.btn-sm:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 32px 16px;\n color: var(--a11y-hud-text-muted);\n}\n\n.empty-state-icon {\n width: 32px;\n height: 32px;\n color: var(--a11y-hud-minor);\n}\n\n.empty-state-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.empty-state-title {\n font-weight: 600;\n color: var(--a11y-hud-text);\n}\n\n.empty-state-body {\n font-size: 12px;\n text-align: center;\n}\n\n.scanning-state {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 24px 16px;\n color: var(--a11y-hud-text-muted);\n font-size: 12px;\n}\n\n.scanning-spinner {\n width: 16px;\n height: 16px;\n color: var(--a11y-hud-minor);\n animation: spin 1s linear infinite;\n}\n\n.scanning-spinner svg {\n width: 100%;\n height: 100%;\n}\n\n/* \u2500\u2500 Keyboard mode view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.panel[data-kbd-mode] .panel-filters {\n display: none;\n}\n\n.kbd-mode-view {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 8px 0;\n}\n\n.kbd-mode-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 0 12px 8px;\n border-bottom: 1px solid var(--a11y-hud-border);\n font-size: 12px;\n font-weight: 600;\n}\n\n.kbd-mode-stat {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n color: var(--a11y-hud-text-muted);\n}\n\n.kbd-mode-stat svg {\n width: 13px;\n height: 13px;\n}\n\n.kbd-mode-stat--warn {\n color: var(--a11y-hud-serious);\n}\n\n.kbd-mode-stat--ok {\n color: var(--a11y-hud-minor);\n}\n\n.kbd-violation-list,\n.kbd-element-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n padding: 0 12px;\n}\n\n.kbd-violation-item {\n display: flex;\n align-items: flex-start;\n gap: 6px;\n font-size: 11px;\n color: var(--a11y-hud-text);\n padding: 4px 0;\n border-bottom: 1px solid var(--a11y-hud-border);\n}\n\n.kbd-violation-item:last-child {\n border-bottom: none;\n}\n\n.kbd-violation-msg {\n flex: 1;\n line-height: 1.4;\n}\n\n.kbd-element-item {\n display: flex;\n align-items: center;\n}\n\n.btn-kbd-element {\n all: unset;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n font-size: 11px;\n color: var(--a11y-hud-text-muted);\n padding: 3px 6px;\n border-radius: 4px;\n cursor: pointer;\n transition: background 120ms;\n}\n\n.btn-kbd-element:hover {\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-text);\n}\n\n.btn-kbd-element:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 1px;\n}\n\n.kbd-element-index {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n border-radius: 10px;\n background: #7c3aed;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n flex-shrink: 0;\n}\n\n.kbd-element-selector {\n font-family: ui-monospace, monospace;\n font-size: 10px;\n color: var(--a11y-hud-highlight);\n}\n\n.kbd-element-tag {\n color: var(--a11y-hud-text-muted);\n font-size: 10px;\n}\n\n.kbd-tabindex-badge {\n font-size: 10px;\n font-family: ui-monospace, monospace;\n background: var(--a11y-hud-bg-elevated);\n color: var(--a11y-hud-serious);\n padding: 1px 4px;\n border-radius: 3px;\n border: 1px solid var(--a11y-hud-border);\n}\n\n.kbd-empty {\n font-size: 12px;\n color: var(--a11y-hud-text-muted);\n padding: 0 12px;\n margin: 0;\n}\n\n/* \u2500\u2500 FAB (minimized state) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.fab {\n all: unset;\n display: none;\n align-items: center;\n gap: 6px;\n padding: 8px 14px;\n background: var(--a11y-hud-bg);\n border: 1px solid var(--a11y-hud-border);\n border-radius: 24px;\n color: var(--a11y-hud-text);\n cursor: pointer;\n font-family: var(--a11y-hud-font);\n font-size: 13px;\n font-weight: 600;\n box-shadow: 0 4px 16px rgb(0 0 0 / 30%);\n transition: background 120ms;\n}\n\n:host([data-minimized]) .fab {\n display: flex;\n}\n\n:host([data-minimized]) .panel {\n display: none;\n}\n\n@keyframes panel-in {\n from {\n opacity: 0;\n transform: translateY(6px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.panel {\n animation: panel-in 120ms ease-out;\n}\n\n.fab:hover {\n background: var(--a11y-hud-bg-elevated);\n}\n\n.fab:focus-visible {\n outline: 2px solid var(--a11y-hud-focus-ring);\n outline-offset: 2px;\n}\n\n.fab-icon {\n display: flex;\n align-items: center;\n width: 14px;\n height: 14px;\n}\n\n.fab-icon svg {\n width: 100%;\n height: 100%;\n}\n\n.fab-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n padding-inline: 5px;\n border-radius: 9px;\n font-size: 11px;\n font-weight: 700;\n background: var(--a11y-hud-critical);\n color: var(--a11y-hud-bg);\n}\n\n.fab-count[data-count="0"] {\n background: var(--a11y-hud-minor);\n}\n';
108
315
 
109
316
  // src/styles/themes.css
110
- var themes_default = ':host([data-theme="light"]) {\n --a11y-hud-bg: #eff1f5;\n --a11y-hud-bg-elevated: #e6e9ef;\n --a11y-hud-bg-hover: #dce0e8;\n --a11y-hud-border: #bcc0cc;\n --a11y-hud-text: #4c4f69;\n /* #5c5f77 gives 5.56:1 on #eff1f5 (was #6c6f85 at 4.36:1) */\n --a11y-hud-text-muted: #5c5f77;\n --a11y-hud-critical: #d20f39;\n --a11y-hud-serious: #fe640b;\n --a11y-hud-moderate: #df8e1d;\n /* #1452c7 gives 5.92:1 on #eff1f5 (was #1e66f5 at 4.34:1) */\n --a11y-hud-minor: #1452c7;\n --a11y-hud-highlight: #8839ef;\n --a11y-hud-focus-ring: #1452c7;\n}\n\n/* Chip text overrides \u2014 bright severity backgrounds need dark or white text\n to meet 4.5:1 at 11 px. The base CSS uses --a11y-hud-bg (light in this\n theme) which fails on orange and yellow. */\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="critical"] {\n color: #ffffff;\n}\n\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="serious"],\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="moderate"] {\n color: #1a1a2e;\n}\n\n:host([data-theme="github-dark"]) {\n --a11y-hud-bg: #0d1117;\n --a11y-hud-bg-elevated: #161b22;\n --a11y-hud-bg-hover: #21262d;\n --a11y-hud-border: #30363d;\n --a11y-hud-text: #e6edf3;\n --a11y-hud-text-muted: #8b949e;\n --a11y-hud-critical: #ff7b72;\n --a11y-hud-serious: #ffa657;\n --a11y-hud-moderate: #e3b341;\n --a11y-hud-minor: #79c0ff;\n --a11y-hud-highlight: #d2a8ff;\n --a11y-hud-focus-ring: #2f81f7;\n}\n\n:host([data-theme="github-light"]) {\n --a11y-hud-bg: #ffffff;\n --a11y-hud-bg-elevated: #f6f8fa;\n --a11y-hud-bg-hover: #eaeef2;\n --a11y-hud-border: #d0d7de;\n --a11y-hud-text: #1f2328;\n --a11y-hud-text-muted: #636c76;\n --a11y-hud-critical: #cf222e;\n --a11y-hud-serious: #bc4c00;\n /* #8a5e00 gives 5.33:1 white-on-chip (was #9a6700 at 4.37:1 \u2014 neither black nor white passed) */\n --a11y-hud-moderate: #8a5e00;\n --a11y-hud-minor: #0550ae;\n --a11y-hud-highlight: #8250df;\n --a11y-hud-focus-ring: #0550ae;\n}\n\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="critical"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="serious"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="moderate"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="minor"] {\n color: #ffffff;\n}\n\n:host([data-theme="tokyo-night"]) {\n --a11y-hud-bg: #1a1b26;\n --a11y-hud-bg-elevated: #24283b;\n --a11y-hud-bg-hover: #2f3549;\n --a11y-hud-border: #3b4261;\n --a11y-hud-text: #c0caf5;\n /* #9aa5ce gives 7.67:1 on #1a1b26 (was #737aa2 at ~4.1:1) */\n --a11y-hud-text-muted: #9aa5ce;\n --a11y-hud-critical: #f7768e;\n --a11y-hud-serious: #ff9e64;\n --a11y-hud-moderate: #e0af68;\n --a11y-hud-minor: #7aa2f7;\n --a11y-hud-highlight: #bb9af7;\n --a11y-hud-focus-ring: #7aa2f7;\n}\n\n:host([data-theme="solarized-dark"]) {\n --a11y-hud-bg: #002b36;\n --a11y-hud-bg-elevated: #073642;\n --a11y-hud-bg-hover: #0d4456;\n --a11y-hud-border: #586e75;\n --a11y-hud-text: #eee8d5;\n --a11y-hud-text-muted: #93a1a1;\n --a11y-hud-critical: #dc322f;\n --a11y-hud-serious: #cb4b16;\n --a11y-hud-moderate: #b58900;\n /* #5ba4cf gives 5.78:1 on #002b36 for text, 5.78:1 for chip (was #268bd2 at ~3.1:1 for both) */\n --a11y-hud-minor: #5ba4cf;\n /* #f06090 gives 4.88:1 on #002b36 (was #d33682 at ~3.3:1) */\n --a11y-hud-highlight: #f06090;\n --a11y-hud-focus-ring: #268bd2;\n}\n\n/* Solarized Dark chip text overrides \u2014 canonical solarized palette falls in the\n middle-luminance zone where neither the panel bg (#002b36) nor white passes\n 4.5:1 on yellow/blue chip backgrounds. Use white text on red/orange,\n dark panel bg text on yellow/blue. */\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="critical"],\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="serious"] {\n color: #ffffff;\n}\n\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="moderate"],\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="minor"] {\n color: #002b36;\n}\n\n:host([data-theme="high-contrast"]) {\n --a11y-hud-bg: #000000;\n --a11y-hud-bg-elevated: #111111;\n --a11y-hud-bg-hover: #222222;\n --a11y-hud-border: #ffffff;\n --a11y-hud-text: #ffffff;\n --a11y-hud-text-muted: #cccccc;\n --a11y-hud-critical: #ff4444;\n --a11y-hud-serious: #ff8800;\n --a11y-hud-moderate: #ffcc00;\n --a11y-hud-minor: #66aaff;\n --a11y-hud-highlight: #ff66ff;\n --a11y-hud-focus-ring: #ffffff;\n}\n';
317
+ var themes_default = ':host([data-theme="light"]) {\n --a11y-hud-bg: #eff1f5;\n --a11y-hud-bg-elevated: #e6e9ef;\n --a11y-hud-bg-hover: #dce0e8;\n --a11y-hud-border: #bcc0cc;\n --a11y-hud-text: #4c4f69;\n /* #5c5f77 gives 5.56:1 on #eff1f5 (was #6c6f85 at 4.36:1) */\n --a11y-hud-text-muted: #5c5f77;\n --a11y-hud-critical: #d20f39;\n --a11y-hud-serious: #fe640b;\n --a11y-hud-moderate: #df8e1d;\n /* #1452c7 gives 5.92:1 on #eff1f5 (was #1e66f5 at 4.34:1) */\n --a11y-hud-minor: #1452c7;\n --a11y-hud-highlight: #8839ef;\n --a11y-hud-tag: #0d7d80;\n --a11y-hud-focus-ring: #1452c7;\n}\n\n/* Chip text overrides \u2014 bright severity backgrounds need dark or white text\n to meet 4.5:1 at 11 px. The base CSS uses --a11y-hud-bg (light in this\n theme) which fails on orange and yellow. */\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="critical"] {\n color: #ffffff;\n}\n\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="serious"],\n:host([data-theme="light"]) .filter-chip[aria-pressed="true"][data-severity="moderate"] {\n color: #1a1a2e;\n}\n\n:host([data-theme="github-dark"]) {\n --a11y-hud-bg: #0d1117;\n --a11y-hud-bg-elevated: #161b22;\n --a11y-hud-bg-hover: #21262d;\n --a11y-hud-border: #30363d;\n --a11y-hud-text: #e6edf3;\n --a11y-hud-text-muted: #8b949e;\n --a11y-hud-critical: #ff7b72;\n --a11y-hud-serious: #ffa657;\n --a11y-hud-moderate: #e3b341;\n --a11y-hud-minor: #79c0ff;\n --a11y-hud-highlight: #d2a8ff;\n --a11y-hud-tag: #56d364;\n --a11y-hud-focus-ring: #2f81f7;\n}\n\n:host([data-theme="github-light"]) {\n --a11y-hud-bg: #ffffff;\n --a11y-hud-bg-elevated: #f6f8fa;\n --a11y-hud-bg-hover: #eaeef2;\n --a11y-hud-border: #d0d7de;\n --a11y-hud-text: #1f2328;\n --a11y-hud-text-muted: #636c76;\n --a11y-hud-critical: #cf222e;\n --a11y-hud-serious: #bc4c00;\n /* #8a5e00 gives 5.33:1 white-on-chip (was #9a6700 at 4.37:1 \u2014 neither black nor white passed) */\n --a11y-hud-moderate: #8a5e00;\n --a11y-hud-minor: #0550ae;\n --a11y-hud-highlight: #8250df;\n --a11y-hud-tag: #1a7f37;\n --a11y-hud-focus-ring: #0550ae;\n}\n\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="critical"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="serious"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="moderate"],\n:host([data-theme="github-light"]) .filter-chip[aria-pressed="true"][data-severity="minor"] {\n color: #ffffff;\n}\n\n:host([data-theme="tokyo-night"]) {\n --a11y-hud-bg: #1a1b26;\n --a11y-hud-bg-elevated: #24283b;\n --a11y-hud-bg-hover: #2f3549;\n --a11y-hud-border: #3b4261;\n --a11y-hud-text: #c0caf5;\n /* #9aa5ce gives 7.67:1 on #1a1b26 (was #737aa2 at ~4.1:1) */\n --a11y-hud-text-muted: #9aa5ce;\n --a11y-hud-critical: #f7768e;\n --a11y-hud-serious: #ff9e64;\n --a11y-hud-moderate: #e0af68;\n --a11y-hud-minor: #7aa2f7;\n --a11y-hud-highlight: #bb9af7;\n --a11y-hud-tag: #73daca;\n --a11y-hud-focus-ring: #7aa2f7;\n}\n\n:host([data-theme="solarized-dark"]) {\n --a11y-hud-bg: #002b36;\n --a11y-hud-bg-elevated: #073642;\n --a11y-hud-bg-hover: #0d4456;\n --a11y-hud-border: #586e75;\n --a11y-hud-text: #eee8d5;\n --a11y-hud-text-muted: #93a1a1;\n --a11y-hud-critical: #dc322f;\n --a11y-hud-serious: #cb4b16;\n --a11y-hud-moderate: #b58900;\n /* #5ba4cf gives 5.78:1 on #002b36 for text, 5.78:1 for chip (was #268bd2 at ~3.1:1 for both) */\n --a11y-hud-minor: #5ba4cf;\n /* #f06090 gives 4.88:1 on #002b36 (was #d33682 at ~3.3:1) */\n --a11y-hud-highlight: #f06090;\n --a11y-hud-tag: #5cccbc;\n --a11y-hud-focus-ring: #268bd2;\n}\n\n/* Solarized Dark chip text overrides \u2014 canonical solarized palette falls in the\n middle-luminance zone where neither the panel bg (#002b36) nor white passes\n 4.5:1 on yellow/blue chip backgrounds. Use white text on red/orange,\n dark panel bg text on yellow/blue. */\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="critical"],\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="serious"] {\n color: #ffffff;\n}\n\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="moderate"],\n:host([data-theme="solarized-dark"]) .filter-chip[aria-pressed="true"][data-severity="minor"] {\n color: #002b36;\n}\n\n:host([data-theme="high-contrast"]) {\n --a11y-hud-bg: #000000;\n --a11y-hud-bg-elevated: #111111;\n --a11y-hud-bg-hover: #222222;\n --a11y-hud-border: #ffffff;\n --a11y-hud-text: #ffffff;\n --a11y-hud-text-muted: #cccccc;\n --a11y-hud-critical: #ff4444;\n --a11y-hud-serious: #ff8800;\n --a11y-hud-moderate: #ffcc00;\n --a11y-hud-minor: #66aaff;\n --a11y-hud-highlight: #ff66ff;\n --a11y-hud-tag: #00e5d0;\n --a11y-hud-focus-ring: #ffffff;\n}\n';
111
318
 
112
319
  // src/theme.ts
113
320
  function resolveTheme(theme) {
@@ -168,6 +375,9 @@ function ensureHighlightStyle(doc) {
168
375
  style.textContent = HIGHLIGHT_CSS;
169
376
  doc.head.appendChild(style);
170
377
  }
378
+ function escapeHtml(s) {
379
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
380
+ }
171
381
  function buildStyleSheet(css) {
172
382
  try {
173
383
  const sheet = new CSSStyleSheet();
@@ -191,10 +401,15 @@ var A11yHudElement = class extends HTMLElement {
191
401
  _observer;
192
402
  _unwatchTheme;
193
403
  _removeHighlight;
194
- _activeSeverities = /* @__PURE__ */ new Set(["critical", "serious", "moderate", "minor"]);
195
- _activeLevels = /* @__PURE__ */ new Set(["A", "AA", "AAA"]);
404
+ _activeSeverities = /* @__PURE__ */ new Set();
405
+ _activeLevels = /* @__PURE__ */ new Set();
196
406
  _runOnly = [];
197
407
  _debouncedScan;
408
+ _keyboardMode = false;
409
+ _keyboardCleanup;
410
+ _kbElements = [];
411
+ _filtersOpen = false;
412
+ _ignoredSectionOpen = false;
198
413
  constructor() {
199
414
  super();
200
415
  this._shadow = this.attachShadow({ mode: "open" });
@@ -232,6 +447,8 @@ ${themes_default}`;
232
447
  this._unwatchTheme?.();
233
448
  this._removeHighlight?.();
234
449
  this._removeHighlight = void 0;
450
+ this._keyboardCleanup?.();
451
+ this._keyboardCleanup = void 0;
235
452
  this._debouncedScan.cancel();
236
453
  }
237
454
  attributeChangedCallback(name, _old, value) {
@@ -354,9 +571,10 @@ ${themes_default}`;
354
571
  const violations = this._results?.violations ?? [];
355
572
  return violations.filter((v) => {
356
573
  const impact = v.impact;
357
- if (impact && !this._activeSeverities.has(impact)) return false;
574
+ if (this._activeSeverities.size > 0 && impact && !this._activeSeverities.has(impact))
575
+ return false;
358
576
  const level = getViolationLevel(v.tags);
359
- if (level && !this._activeLevels.has(level)) return false;
577
+ if (this._activeLevels.size > 0 && level && !this._activeLevels.has(level)) return false;
360
578
  return true;
361
579
  });
362
580
  }
@@ -381,11 +599,14 @@ ${themes_default}`;
381
599
  fabCountEl.textContent = String(total);
382
600
  fabCountEl.setAttribute("data-count", String(total));
383
601
  }
384
- const body = panel.querySelector(".panel-body");
385
- if (body) body.innerHTML = this._renderBody(filtered);
386
- this._bindHighlightEvents();
602
+ if (!this._keyboardMode) {
603
+ const body = panel.querySelector(".panel-body");
604
+ if (body) body.innerHTML = this._renderBody(filtered);
605
+ this._bindHighlightEvents();
606
+ }
387
607
  }
388
608
  _renderScanning() {
609
+ if (this._keyboardMode) return;
389
610
  const body = this._shadow.querySelector(".panel-body");
390
611
  if (body) {
391
612
  body.innerHTML = `
@@ -393,6 +614,7 @@ ${themes_default}`;
393
614
  <span class="scanning-spinner" aria-hidden="true">${icon("refresh-cw")}</span>
394
615
  Scanning\u2026
395
616
  </div>
617
+ ${this._renderIgnoredSection()}
396
618
  `;
397
619
  }
398
620
  }
@@ -400,18 +622,25 @@ ${themes_default}`;
400
622
  return `
401
623
  <div class="panel" role="complementary" aria-label="Accessibility panel">
402
624
  <div class="panel-header">
403
- <span class="panel-title">
625
+ <button class="btn-panel-title" aria-expanded="${this._filtersOpen}" aria-controls="panel-filters" aria-label="Toggle filters" title="Toggle filters">
404
626
  <span class="panel-title-icon" aria-hidden="true">${icon("filter")}</span>
405
627
  a11y
406
628
  <span class="panel-count" data-count="${total}" aria-live="polite" aria-label="${total} violations">${total}</span>
407
- </span>
629
+ <span class="panel-title-chevron" aria-hidden="true">${icon("chevron-down")}</span>
630
+ </button>
408
631
  <div class="panel-actions" role="toolbar" aria-label="Panel actions">
409
632
  <button class="btn-icon" id="btn-copy-all" aria-label="Copy all violations to clipboard" title="Copy all">
410
633
  ${icon("copy")}
411
634
  </button>
635
+ <button class="btn-icon" id="btn-export" aria-label="Export results as JSON" title="Export JSON">
636
+ ${icon("download")}
637
+ </button>
412
638
  <button class="btn-icon" id="btn-rescan" aria-label="Re-scan page" title="Re-scan">
413
639
  ${icon("refresh-cw")}
414
640
  </button>
641
+ <button class="btn-icon" id="btn-keyboard" aria-label="Toggle keyboard mode" title="Keyboard mode" aria-pressed="false">
642
+ ${icon("keyboard")}
643
+ </button>
415
644
  <button class="btn-icon" id="btn-minimize" aria-label="Minimize panel" title="Minimize">
416
645
  ${icon("minus")}
417
646
  </button>
@@ -447,7 +676,7 @@ ${themes_default}`;
447
676
  return `<button class="filter-chip" data-ruleset="${r}" aria-pressed="${active}">${r}</button>`;
448
677
  }).join("");
449
678
  return `
450
- <div class="panel-filters" aria-label="Filters">
679
+ <div id="panel-filters" class="panel-filters" aria-label="Filters" ${this._filtersOpen ? "data-open" : ""}>
451
680
  <div class="filter-group" role="group" aria-label="Filter by severity">${severityChips}</div>
452
681
  <div class="filter-group" role="group" aria-label="Filter by WCAG level">${levelChips}</div>
453
682
  <div class="filter-group" role="group" aria-label="Run only rule sets">${ruleSetChips}</div>
@@ -461,6 +690,7 @@ ${themes_default}`;
461
690
  <span class="scanning-spinner" aria-hidden="true">${icon("refresh-cw")}</span>
462
691
  <span class="empty-state-body">Running first scan\u2026</span>
463
692
  </div>
693
+ ${this._renderIgnoredSection()}
464
694
  `;
465
695
  }
466
696
  if (violations.length === 0) {
@@ -472,10 +702,44 @@ ${themes_default}`;
472
702
  ${this._results.violations.length > 0 ? "All violations filtered out." : "Great job! The scanned area has no axe-core violations."}
473
703
  </span>
474
704
  </div>
705
+ ${this._renderIgnoredSection()}
475
706
  `;
476
707
  }
477
708
  const items = violations.map((v, i) => this._renderViolationItem(v, i)).join("");
478
- return `<ul class="violation-list" role="list" aria-label="${violations.length} violation${violations.length !== 1 ? "s" : ""}">${items}</ul>`;
709
+ return `<ul class="violation-list" role="list" aria-label="${violations.length} violation${violations.length !== 1 ? "s" : ""}">${items}</ul>${this._renderIgnoredSection()}`;
710
+ }
711
+ _renderIgnoredSection() {
712
+ const entries = listIgnores();
713
+ const removeAttrs = (e) => e.selector !== void 0 ? `data-remove-rule="${e.ruleId}" data-remove-selector="${e.selector}"` : `data-remove-rule="${e.ruleId}"`;
714
+ const listHtml = entries.length === 0 ? `<p class="ignored-empty">No rules ignored yet.</p>` : `<ul class="ignored-list" aria-label="Ignored rules">${entries.map(
715
+ (e) => `
716
+ <li class="ignored-entry">
717
+ <span class="ignored-rule-id">${e.ruleId}</span>
718
+ ${e.selector !== void 0 ? `<code class="ignored-selector">${e.selector}</code>` : ""}
719
+ <button class="btn-remove-ignore" ${removeAttrs(e)} aria-label="Remove ignore for ${e.ruleId}">
720
+ ${icon("trash-2")}
721
+ </button>
722
+ </li>`
723
+ ).join("")}</ul>`;
724
+ return `
725
+ <div class="ignored-section">
726
+ <button class="ignored-section-toggle" aria-expanded="${this._ignoredSectionOpen}" aria-controls="ignored-section-body">
727
+ <span>Ignored rules (${entries.length})</span>
728
+ <span class="chevron-icon" aria-hidden="true">${icon("chevron-down")}</span>
729
+ </button>
730
+ <div id="ignored-section-body" class="ignored-section-body" ${this._ignoredSectionOpen ? "data-open" : ""}>
731
+ ${listHtml}
732
+ <div class="ignored-actions">
733
+ <button class="btn-export-ignores btn-sm" aria-label="Export ignored rules as JSON" title="Export ignores">
734
+ ${icon("download")} Export
735
+ </button>
736
+ <button class="btn-import-ignores btn-sm" aria-label="Import ignored rules from JSON" title="Import ignores">
737
+ ${icon("upload")} Import
738
+ </button>
739
+ ${entries.length > 0 ? `<button class="btn-clear-ignores btn-sm" aria-label="Clear all ignored rules" title="Clear all">${icon("trash-2")} Clear all</button>` : ""}
740
+ </div>
741
+ </div>
742
+ </div>`;
479
743
  }
480
744
  _renderViolationItem(violation, index) {
481
745
  const impact = violation.impact ?? "minor";
@@ -510,6 +774,9 @@ ${themes_default}`;
510
774
  <button class="btn-copy-single" data-copy-violation="${index}" aria-label="Copy ${violation.id} to clipboard" title="Copy violation">
511
775
  ${icon("copy")} Copy
512
776
  </button>
777
+ <button class="btn-ignore-rule" data-ignore-rule="${violation.id}" aria-label="Ignore rule ${violation.id}" title="Ignore this rule">
778
+ ${icon("trash-2")} Ignore
779
+ </button>
513
780
  </div>
514
781
  </div>
515
782
  </li>
@@ -522,11 +789,14 @@ ${themes_default}`;
522
789
  _bindEvents() {
523
790
  const panel = this._shadow.querySelector(".panel");
524
791
  if (!panel) return;
792
+ panel.querySelector(".btn-panel-title")?.addEventListener("click", () => this._toggleFilters());
525
793
  panel.querySelector("#btn-rescan")?.addEventListener("click", () => void this._runScan());
526
794
  panel.querySelector("#btn-minimize")?.addEventListener("click", () => {
527
795
  this.setAttribute("data-minimized", "");
528
796
  });
529
797
  panel.querySelector("#btn-copy-all")?.addEventListener("click", () => void this._copyAll());
798
+ panel.querySelector("#btn-export")?.addEventListener("click", () => this._exportJson());
799
+ panel.querySelector("#btn-keyboard")?.addEventListener("click", () => this._toggleKeyboardMode());
530
800
  panel.addEventListener("click", (e) => this._handlePanelClick(e));
531
801
  panel.addEventListener("keydown", (e) => this._handlePanelKeydown(e));
532
802
  this._shadow.querySelector(".fab")?.addEventListener("click", () => {
@@ -567,29 +837,84 @@ ${themes_default}`;
567
837
  void this._copySingle(vIdx);
568
838
  return;
569
839
  }
840
+ const kbdElementBtn = target.closest(".btn-kbd-element");
841
+ if (kbdElementBtn) {
842
+ const idx = Number.parseInt(kbdElementBtn.dataset.kbdElement ?? "", 10);
843
+ const info = this._kbElements.find((e2) => e2.index === idx);
844
+ if (info) {
845
+ this._removeHighlight?.();
846
+ ensureHighlightStyle(document);
847
+ const el = info.element;
848
+ el.setAttribute(HIGHLIGHT_ATTR, "");
849
+ el.scrollIntoView({ behavior: "smooth", block: "nearest" });
850
+ this._removeHighlight = () => el.removeAttribute(HIGHLIGHT_ATTR);
851
+ }
852
+ return;
853
+ }
854
+ const ignoreBtn = target.closest(".btn-ignore-rule");
855
+ if (ignoreBtn) {
856
+ const ruleId = ignoreBtn.dataset.ignoreRule;
857
+ if (ruleId) {
858
+ addIgnore(ruleId);
859
+ void this._runScan();
860
+ }
861
+ return;
862
+ }
863
+ const removeIgnoreBtn = target.closest(".btn-remove-ignore");
864
+ if (removeIgnoreBtn) {
865
+ const ruleId = removeIgnoreBtn.dataset.removeRule;
866
+ const selector = removeIgnoreBtn.dataset.removeSelector || void 0;
867
+ if (ruleId) {
868
+ removeIgnore(ruleId, selector);
869
+ void this._runScan();
870
+ }
871
+ return;
872
+ }
873
+ if (target.closest(".btn-export-ignores")) {
874
+ this._exportIgnoresJson();
875
+ return;
876
+ }
877
+ if (target.closest(".btn-import-ignores")) {
878
+ this._importIgnoresJson();
879
+ return;
880
+ }
881
+ if (target.closest(".btn-clear-ignores")) {
882
+ clearIgnores();
883
+ void this._runScan();
884
+ return;
885
+ }
886
+ const ignoredToggle = target.closest(".ignored-section-toggle");
887
+ if (ignoredToggle) {
888
+ this._ignoredSectionOpen = !this._ignoredSectionOpen;
889
+ ignoredToggle.setAttribute("aria-expanded", String(this._ignoredSectionOpen));
890
+ const bodyId = ignoredToggle.getAttribute("aria-controls");
891
+ const sectionBody = bodyId ? this._shadow.getElementById(bodyId) : null;
892
+ if (sectionBody) {
893
+ sectionBody.toggleAttribute("data-open", this._ignoredSectionOpen);
894
+ }
895
+ return;
896
+ }
570
897
  const chip = target.closest(".filter-chip");
571
898
  if (chip) {
572
899
  const severity = chip.dataset.severity;
573
900
  const level = chip.dataset.level;
574
901
  if (severity && isSeverity(severity)) {
575
902
  const active = this._activeSeverities.has(severity);
576
- if (active && this._activeSeverities.size > 1) {
903
+ if (active) {
577
904
  this._activeSeverities.delete(severity);
578
- chip.setAttribute("aria-pressed", "false");
579
- } else if (!active) {
905
+ } else {
580
906
  this._activeSeverities.add(severity);
581
- chip.setAttribute("aria-pressed", "true");
582
907
  }
908
+ chip.setAttribute("aria-pressed", String(!active));
583
909
  this._refreshBody();
584
910
  } else if (level) {
585
911
  const active = this._activeLevels.has(level);
586
- if (active && this._activeLevels.size > 1) {
912
+ if (active) {
587
913
  this._activeLevels.delete(level);
588
- chip.setAttribute("aria-pressed", "false");
589
- } else if (!active) {
914
+ } else {
590
915
  this._activeLevels.add(level);
591
- chip.setAttribute("aria-pressed", "true");
592
916
  }
917
+ chip.setAttribute("aria-pressed", String(!active));
593
918
  this._refreshBody();
594
919
  } else if (chip.dataset.ruleset) {
595
920
  const ruleset = chip.dataset.ruleset;
@@ -601,6 +926,7 @@ ${themes_default}`;
601
926
  this._runOnly = [...this._runOnly, ruleset];
602
927
  chip.setAttribute("aria-pressed", "true");
603
928
  }
929
+ this._updateFilterToggle();
604
930
  void this._runScan();
605
931
  }
606
932
  }
@@ -646,6 +972,7 @@ ${themes_default}`;
646
972
  fabCountEl.textContent = String(total);
647
973
  fabCountEl.setAttribute("data-count", String(total));
648
974
  }
975
+ this._updateFilterToggle();
649
976
  }
650
977
  _formatViolationsAsMarkdown(violations) {
651
978
  const header = `# a11y-hud scan results
@@ -694,6 +1021,155 @@ ${themes_default}`;
694
1021
  if (!violation) return;
695
1022
  await this._copyToClipboard(this._formatViolationsAsMarkdown([violation]));
696
1023
  }
1024
+ _hasActiveFilters() {
1025
+ return this._activeSeverities.size > 0 || this._activeLevels.size > 0 || this._runOnly.length > 0;
1026
+ }
1027
+ _updateFilterToggle() {
1028
+ const btn = this._shadow.querySelector(".btn-panel-title");
1029
+ if (!btn) return;
1030
+ btn.setAttribute("aria-expanded", String(this._filtersOpen));
1031
+ if (this._hasActiveFilters()) {
1032
+ btn.setAttribute("data-active", "");
1033
+ } else {
1034
+ btn.removeAttribute("data-active");
1035
+ }
1036
+ }
1037
+ _toggleFilters() {
1038
+ this._filtersOpen = !this._filtersOpen;
1039
+ const filters = this._shadow.getElementById("panel-filters");
1040
+ filters?.toggleAttribute("data-open", this._filtersOpen);
1041
+ this._updateFilterToggle();
1042
+ }
1043
+ _toggleKeyboardMode() {
1044
+ this._keyboardMode = !this._keyboardMode;
1045
+ const btn = this._shadow.querySelector("#btn-keyboard");
1046
+ if (btn) btn.setAttribute("aria-pressed", String(this._keyboardMode));
1047
+ this._shadow.querySelector(".panel")?.toggleAttribute("data-kbd-mode", this._keyboardMode);
1048
+ if (this._keyboardMode) {
1049
+ this._activateKeyboardMode();
1050
+ } else {
1051
+ this._deactivateKeyboardMode();
1052
+ }
1053
+ }
1054
+ _activateKeyboardMode() {
1055
+ const scope = this._getScopeTarget();
1056
+ this._kbElements = getFocusableElements(scope);
1057
+ const violations = detectKeyboardViolations(this._kbElements, scope);
1058
+ this._keyboardCleanup = injectFocusOrderOverlay(this._kbElements);
1059
+ const body = this._shadow.querySelector(".panel-body");
1060
+ if (body) body.innerHTML = this._renderKeyboardView(this._kbElements, violations);
1061
+ }
1062
+ _deactivateKeyboardMode() {
1063
+ this._keyboardCleanup?.();
1064
+ this._keyboardCleanup = void 0;
1065
+ this._kbElements = [];
1066
+ this._removeHighlight?.();
1067
+ this._removeHighlight = void 0;
1068
+ const body = this._shadow.querySelector(".panel-body");
1069
+ if (body) {
1070
+ body.innerHTML = this._renderBody(this._getFilteredViolations());
1071
+ this._bindHighlightEvents();
1072
+ }
1073
+ }
1074
+ _renderKeyboardView(elements, violations) {
1075
+ const elementItems = elements.map(
1076
+ (info) => `
1077
+ <li class="kbd-element-item">
1078
+ <button class="btn-kbd-element" data-kbd-element="${info.index}" aria-label="Highlight element ${info.index}: ${escapeHtml(info.selector)}">
1079
+ <span class="kbd-element-index" aria-hidden="true">${info.index}</span>
1080
+ <code class="kbd-element-selector">${escapeHtml(info.selector)}</code>
1081
+ <span class="kbd-element-tag">&lt;${info.element.tagName.toLowerCase()}&gt;</span>
1082
+ ${info.tabIndex > 0 ? `<span class="kbd-tabindex-badge">tabindex=${info.tabIndex}</span>` : ""}
1083
+ </button>
1084
+ </li>`
1085
+ ).join("");
1086
+ const violationItems = violations.map(
1087
+ (v) => `
1088
+ <li class="kbd-violation-item">
1089
+ <span class="severity-icon" data-severity="serious" aria-hidden="true">${icon("alert-circle")}</span>
1090
+ <span class="kbd-violation-msg">${escapeHtml(v.message)}</span>
1091
+ </li>`
1092
+ ).join("");
1093
+ return `
1094
+ <div class="kbd-mode-view">
1095
+ <div class="kbd-mode-header">
1096
+ <span class="kbd-mode-stat">
1097
+ <span aria-hidden="true">${icon("keyboard")}</span>
1098
+ ${elements.length} focusable element${elements.length !== 1 ? "s" : ""}
1099
+ </span>
1100
+ ${violations.length > 0 ? `<span class="kbd-mode-stat kbd-mode-stat--warn">
1101
+ <span aria-hidden="true">${icon("alert-circle")}</span>
1102
+ ${violations.length} issue${violations.length !== 1 ? "s" : ""}
1103
+ </span>` : `<span class="kbd-mode-stat kbd-mode-stat--ok">
1104
+ <span aria-hidden="true">${icon("check-circle")}</span>
1105
+ No issues
1106
+ </span>`}
1107
+ </div>
1108
+ ${violations.length > 0 ? `<ul class="kbd-violation-list" aria-label="Keyboard issues">${violationItems}</ul>` : ""}
1109
+ ${elements.length > 0 ? `<ul class="kbd-element-list" aria-label="Focusable elements in tab order">${elementItems}</ul>` : `<p class="kbd-empty">No focusable elements found in the scoped area.</p>`}
1110
+ </div>`;
1111
+ }
1112
+ _exportIgnoresJson() {
1113
+ const json = exportIgnores();
1114
+ const blob = new Blob([json], { type: "application/json" });
1115
+ const url = URL.createObjectURL(blob);
1116
+ const a = document.createElement("a");
1117
+ a.href = url;
1118
+ a.download = "a11y-hud-ignores.json";
1119
+ a.click();
1120
+ URL.revokeObjectURL(url);
1121
+ }
1122
+ _importIgnoresJson() {
1123
+ const input = document.createElement("input");
1124
+ input.type = "file";
1125
+ input.accept = ".json,application/json";
1126
+ input.onchange = () => {
1127
+ const file = input.files?.[0];
1128
+ if (!file) return;
1129
+ const reader = new FileReader();
1130
+ reader.onload = (evt) => {
1131
+ importIgnores(evt.target?.result ?? "");
1132
+ void this._runScan();
1133
+ };
1134
+ reader.readAsText(file);
1135
+ };
1136
+ input.click();
1137
+ }
1138
+ _getScopeString() {
1139
+ if (this._scopeSelector) return this._scopeSelector;
1140
+ if (this._scopeElement) {
1141
+ const el = this._scopeElement;
1142
+ if (el.id) return `#${el.id}`;
1143
+ return el.tagName.toLowerCase();
1144
+ }
1145
+ return "document.body";
1146
+ }
1147
+ _buildExport() {
1148
+ if (!this._results) return null;
1149
+ return {
1150
+ version: "1",
1151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1152
+ url: location.href,
1153
+ scope: this._getScopeString(),
1154
+ results: this._results
1155
+ };
1156
+ }
1157
+ _exportJson() {
1158
+ const data = this._buildExport();
1159
+ if (!data) return;
1160
+ const json = JSON.stringify(data, null, 2);
1161
+ const blob = new Blob([json], { type: "application/json" });
1162
+ const url = URL.createObjectURL(blob);
1163
+ const a = document.createElement("a");
1164
+ a.href = url;
1165
+ a.download = "a11y-hud-results.json";
1166
+ a.click();
1167
+ URL.revokeObjectURL(url);
1168
+ }
1169
+ exportResults() {
1170
+ const data = this._buildExport();
1171
+ return data ? JSON.stringify(data, null, 2) : null;
1172
+ }
697
1173
  _highlightNode(violationIndex, nodeIndex) {
698
1174
  this._removeHighlight?.();
699
1175
  this._removeHighlight = void 0;
@@ -749,12 +1225,49 @@ function mount(options = {}) {
749
1225
  },
750
1226
  async runScan() {
751
1227
  return el.runScan();
1228
+ },
1229
+ exportResults() {
1230
+ return el.exportResults();
1231
+ },
1232
+ ignores: {
1233
+ add(ruleId, selector) {
1234
+ addIgnore(ruleId, selector);
1235
+ void el.runScan();
1236
+ },
1237
+ remove(ruleId, selector) {
1238
+ removeIgnore(ruleId, selector);
1239
+ void el.runScan();
1240
+ },
1241
+ clear() {
1242
+ clearIgnores();
1243
+ void el.runScan();
1244
+ },
1245
+ list() {
1246
+ return listIgnores();
1247
+ },
1248
+ exportJson() {
1249
+ return exportIgnores();
1250
+ },
1251
+ importJson(json) {
1252
+ importIgnores(json);
1253
+ void el.runScan();
1254
+ }
752
1255
  }
753
1256
  };
754
1257
  }
755
1258
 
756
1259
  exports.A11yHudElement = A11yHudElement;
1260
+ exports.addIgnore = addIgnore;
1261
+ exports.clearIgnores = clearIgnores;
1262
+ exports.detectKeyboardViolations = detectKeyboardViolations;
1263
+ exports.exportIgnores = exportIgnores;
1264
+ exports.generateBookmarklet = generateBookmarklet;
1265
+ exports.getFocusableElements = getFocusableElements;
1266
+ exports.importIgnores = importIgnores;
1267
+ exports.injectFocusOrderOverlay = injectFocusOrderOverlay;
1268
+ exports.listIgnores = listIgnores;
757
1269
  exports.mount = mount;
1270
+ exports.removeIgnore = removeIgnore;
758
1271
  exports.runScan = runScan;
759
1272
  //# sourceMappingURL=index.cjs.map
760
1273
  //# sourceMappingURL=index.cjs.map