a11y-hud 0.1.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.js ADDED
@@ -0,0 +1,694 @@
1
+ import axe from 'axe-core';
2
+
3
+ // src/scan.ts
4
+ async function runScan(target = document.body) {
5
+ const results = await axe.run(target);
6
+ results.violations = results.violations.map((v) => ({
7
+ ...v,
8
+ nodes: v.nodes.filter((node) => {
9
+ const first = node.target[0];
10
+ const selector = (Array.isArray(first) ? first[0] : first) ?? "";
11
+ return !String(selector).toLowerCase().startsWith("a11y-hud");
12
+ })
13
+ })).filter((v) => v.nodes.length > 0);
14
+ return results;
15
+ }
16
+
17
+ // src/mount.ts
18
+ function mount(options = {}) {
19
+ let el = document.querySelector("a11y-hud");
20
+ if (!el) {
21
+ el = document.createElement("a11y-hud");
22
+ document.body.appendChild(el);
23
+ }
24
+ if (options.theme !== void 0) el.setAttribute("theme", options.theme);
25
+ if (options.scope !== void 0) {
26
+ if (typeof options.scope === "string") {
27
+ el.setAttribute("scope", options.scope);
28
+ } else {
29
+ el.scopeElement = options.scope;
30
+ }
31
+ }
32
+ if (options.autoScan === false) {
33
+ el.removeAttribute("auto-scan");
34
+ } else if (options.autoScan === true) {
35
+ el.setAttribute("auto-scan", "");
36
+ }
37
+ if (options.debounce !== void 0) {
38
+ el.setAttribute("debounce", String(options.debounce));
39
+ }
40
+ return {
41
+ unmount() {
42
+ el.remove();
43
+ },
44
+ setTheme(theme) {
45
+ el.setTheme(theme);
46
+ },
47
+ async runScan() {
48
+ return el.triggerScan();
49
+ }
50
+ };
51
+ }
52
+
53
+ // src/icons/alert-circle.svg
54
+ 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';
55
+
56
+ // src/icons/alert-triangle.svg
57
+ var alert_triangle_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.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>\n';
58
+
59
+ // src/icons/check-circle.svg
60
+ var check_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"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>\n';
61
+
62
+ // src/icons/chevron-down.svg
63
+ var chevron_down_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="m6 9 6 6 6-6"/></svg>\n';
64
+
65
+ // src/icons/chevron-up.svg
66
+ var chevron_up_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 15-6-6-6 6"/></svg>\n';
67
+
68
+ // src/icons/copy.svg
69
+ 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';
70
+
71
+ // src/icons/filter.svg
72
+ 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';
73
+
74
+ // src/icons/info.svg
75
+ var info_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"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>\n';
76
+
77
+ // src/icons/keyboard.svg
78
+ var keyboard_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="20" height="16" x="2" y="4" rx="2" ry="2"/><path d="M6 8h.001"/><path d="M10 8h.001"/><path d="M14 8h.001"/><path d="M18 8h.001"/><path d="M8 12h.001"/><path d="M12 12h.001"/><path d="M16 12h.001"/><path d="M7 16h10"/></svg>\n';
79
+
80
+ // src/icons/minus.svg
81
+ var minus_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="M5 12h14"/></svg>\n';
82
+
83
+ // src/icons/moon.svg
84
+ var moon_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="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>\n';
85
+
86
+ // src/icons/refresh-cw.svg
87
+ var refresh_cw_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 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>\n';
88
+
89
+ // src/icons/sun.svg
90
+ 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';
91
+
92
+ // src/icons/x.svg
93
+ 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';
94
+
95
+ // src/icons/index.ts
96
+ var registry = {
97
+ "alert-circle": alert_circle_default,
98
+ "alert-triangle": alert_triangle_default,
99
+ "check-circle": check_circle_default,
100
+ "chevron-down": chevron_down_default,
101
+ "chevron-up": chevron_up_default,
102
+ copy: copy_default,
103
+ filter: filter_default,
104
+ info: info_default,
105
+ keyboard: keyboard_default,
106
+ minus: minus_default,
107
+ moon: moon_default,
108
+ "refresh-cw": refresh_cw_default,
109
+ sun: sun_default,
110
+ x: x_default
111
+ };
112
+ function icon(name) {
113
+ return registry[name] ?? "";
114
+ }
115
+
116
+ // src/observer.ts
117
+ function debounce(fn, delay) {
118
+ let timer;
119
+ const debounced = (...args) => {
120
+ clearTimeout(timer);
121
+ timer = setTimeout(() => fn(...args), delay);
122
+ };
123
+ debounced.cancel = () => clearTimeout(timer);
124
+ return debounced;
125
+ }
126
+
127
+ // src/styles/base.css
128
+ 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: background 120ms, 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: background 120ms, color 120ms, 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: background 120ms, 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';
129
+
130
+ // src/styles/themes.css
131
+ 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="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';
132
+
133
+ // src/theme.ts
134
+ function resolveTheme(theme) {
135
+ if (matchMedia("(prefers-contrast: more)").matches) return "high-contrast";
136
+ if (theme === "auto")
137
+ return matchMedia("(prefers-color-scheme: dark)").matches ? "default" : "light";
138
+ return theme;
139
+ }
140
+ function watchTheme(getTheme, callback) {
141
+ const colorScheme = matchMedia("(prefers-color-scheme: dark)");
142
+ const contrast = matchMedia("(prefers-contrast: more)");
143
+ const handler = () => callback(resolveTheme(getTheme()));
144
+ colorScheme.addEventListener("change", handler);
145
+ contrast.addEventListener("change", handler);
146
+ return () => {
147
+ colorScheme.removeEventListener("change", handler);
148
+ contrast.removeEventListener("change", handler);
149
+ };
150
+ }
151
+
152
+ // src/element.ts
153
+ var HIGHLIGHT_ATTR = "data-a11y-hud-highlighted";
154
+ var HIGHLIGHT_STYLE_ID = "a11y-hud-highlight-style";
155
+ var HIGHLIGHT_CSS = `[${HIGHLIGHT_ATTR}]{outline:3px solid #7c3aed!important;outline-offset:3px!important;scroll-margin:8px!important;}`;
156
+ var WCAG_LEVEL_TAGS = {
157
+ A: ["wcag2a", "wcag21a"],
158
+ AA: ["wcag2aa", "wcag21aa", "wcag22aa"],
159
+ AAA: ["wcag2aaa"]
160
+ };
161
+ var SEVERITY_ICON = {
162
+ critical: "alert-triangle",
163
+ serious: "alert-circle",
164
+ moderate: "alert-circle",
165
+ minor: "info"
166
+ };
167
+ function isSeverity(value) {
168
+ return value === "critical" || value === "serious" || value === "moderate" || value === "minor";
169
+ }
170
+ function getViolationLevel(tags) {
171
+ for (const [level, levelTags] of Object.entries(WCAG_LEVEL_TAGS)) {
172
+ if (tags.some((t) => levelTags.includes(t))) return level;
173
+ }
174
+ return "";
175
+ }
176
+ function findElement(target) {
177
+ try {
178
+ const last = target[target.length - 1];
179
+ const selector = Array.isArray(last) ? last[last.length - 1] : last;
180
+ return selector ? document.querySelector(selector) : null;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ function ensureHighlightStyle(doc) {
186
+ if (doc.getElementById(HIGHLIGHT_STYLE_ID)) return;
187
+ const style = doc.createElement("style");
188
+ style.id = HIGHLIGHT_STYLE_ID;
189
+ style.textContent = HIGHLIGHT_CSS;
190
+ doc.head.appendChild(style);
191
+ }
192
+ function buildStyleSheet(css) {
193
+ try {
194
+ const sheet = new CSSStyleSheet();
195
+ sheet.replaceSync(css);
196
+ return sheet;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ var A11yHudElement = class extends HTMLElement {
202
+ static observedAttributes = ["theme", "scope", "auto-scan", "debounce"];
203
+ _shadow;
204
+ _theme = "auto";
205
+ _scopeSelector;
206
+ _scopeElement;
207
+ _autoScan = true;
208
+ _debounceMs = 500;
209
+ _results;
210
+ _scanning = false;
211
+ _mounted = false;
212
+ _observer;
213
+ _unwatchTheme;
214
+ _removeHighlight;
215
+ _activeSeverities = /* @__PURE__ */ new Set(["critical", "serious", "moderate", "minor"]);
216
+ _activeLevels = /* @__PURE__ */ new Set(["A", "AA", "AAA"]);
217
+ _debouncedScan;
218
+ constructor() {
219
+ super();
220
+ this._shadow = this.attachShadow({ mode: "open" });
221
+ this._debouncedScan = debounce(() => void this._runScan(), this._debounceMs);
222
+ this._applyStyles();
223
+ }
224
+ _applyStyles() {
225
+ const combined = `${base_default}
226
+ ${themes_default}`;
227
+ const sheet = buildStyleSheet(combined);
228
+ if (sheet) {
229
+ this._shadow.adoptedStyleSheets = [sheet];
230
+ } else {
231
+ const style = document.createElement("style");
232
+ style.textContent = combined;
233
+ this._shadow.appendChild(style);
234
+ }
235
+ }
236
+ connectedCallback() {
237
+ this._mounted = true;
238
+ this._applyResolvedTheme();
239
+ this._render();
240
+ this._unwatchTheme = watchTheme(
241
+ () => this._theme,
242
+ (resolved) => {
243
+ this.dataset.theme = resolved;
244
+ }
245
+ );
246
+ if (this._autoScan) this._startObserver();
247
+ void this._runScan();
248
+ }
249
+ disconnectedCallback() {
250
+ this._mounted = false;
251
+ this._observer?.disconnect();
252
+ this._unwatchTheme?.();
253
+ this._removeHighlight?.();
254
+ this._removeHighlight = void 0;
255
+ this._debouncedScan.cancel();
256
+ }
257
+ attributeChangedCallback(name, _old, value) {
258
+ switch (name) {
259
+ case "theme":
260
+ this._theme = value ?? "auto";
261
+ if (this._mounted) this._applyResolvedTheme();
262
+ break;
263
+ case "scope":
264
+ this._scopeSelector = value ?? void 0;
265
+ this._scopeElement = void 0;
266
+ break;
267
+ case "auto-scan":
268
+ this._autoScan = value !== null;
269
+ if (this._mounted) {
270
+ if (this._autoScan) {
271
+ this._startObserver();
272
+ } else {
273
+ this._observer?.disconnect();
274
+ this._observer = void 0;
275
+ }
276
+ }
277
+ break;
278
+ case "debounce": {
279
+ const parsed = Number.parseInt(value ?? "", 10);
280
+ this._debounceMs = Number.isNaN(parsed) ? 500 : parsed;
281
+ this._debouncedScan.cancel();
282
+ this._debouncedScan = debounce(() => void this._runScan(), this._debounceMs);
283
+ break;
284
+ }
285
+ }
286
+ }
287
+ get scopeElement() {
288
+ return this._scopeElement;
289
+ }
290
+ set scopeElement(el) {
291
+ this._scopeElement = el;
292
+ this._scopeSelector = void 0;
293
+ }
294
+ setTheme(theme) {
295
+ this._theme = theme;
296
+ this.setAttribute("theme", theme);
297
+ this._applyResolvedTheme();
298
+ }
299
+ async triggerScan() {
300
+ return this._runScan();
301
+ }
302
+ _getScopeTarget() {
303
+ if (this._scopeElement) return this._scopeElement;
304
+ if (this._scopeSelector) {
305
+ return document.querySelector(this._scopeSelector) ?? document.body;
306
+ }
307
+ return document.body;
308
+ }
309
+ _applyResolvedTheme() {
310
+ this.dataset.theme = resolveTheme(this._theme);
311
+ }
312
+ _startObserver() {
313
+ this._observer?.disconnect();
314
+ const target = this._getScopeTarget();
315
+ const debouncedScan = this._debouncedScan;
316
+ this._observer = new MutationObserver((records) => {
317
+ if (records.every((r) => r.type === "attributes" && r.attributeName === HIGHLIGHT_ATTR))
318
+ return;
319
+ debouncedScan();
320
+ });
321
+ this._observer.observe(target, {
322
+ childList: true,
323
+ subtree: true,
324
+ attributes: true,
325
+ characterData: false
326
+ });
327
+ }
328
+ async _runScan() {
329
+ if (this._scanning) return this._results ?? { violations: [] };
330
+ this._scanning = true;
331
+ const rescanBtn = this._shadow.querySelector("#btn-rescan");
332
+ if (rescanBtn) {
333
+ rescanBtn.setAttribute("data-scanning", "");
334
+ rescanBtn.disabled = true;
335
+ }
336
+ if (!this._results) this._renderScanning();
337
+ await new Promise((resolve) => setTimeout(resolve, 0));
338
+ try {
339
+ const target = this._getScopeTarget();
340
+ const results = await runScan(target);
341
+ if (!this._mounted) return results;
342
+ this._results = results;
343
+ this._render();
344
+ return results;
345
+ } finally {
346
+ this._scanning = false;
347
+ const btn = this._shadow.querySelector("#btn-rescan");
348
+ if (btn) {
349
+ btn.removeAttribute("data-scanning");
350
+ btn.disabled = false;
351
+ }
352
+ }
353
+ }
354
+ _getFilteredViolations() {
355
+ const violations = this._results?.violations ?? [];
356
+ return violations.filter((v) => {
357
+ const impact = v.impact;
358
+ if (impact && !this._activeSeverities.has(impact)) return false;
359
+ const level = getViolationLevel(v.tags);
360
+ if (level && !this._activeLevels.has(level)) return false;
361
+ return true;
362
+ });
363
+ }
364
+ _render() {
365
+ const filtered = this._getFilteredViolations();
366
+ const total = this._results?.violations.length ?? 0;
367
+ const panel = this._shadow.querySelector(".panel");
368
+ if (!panel) {
369
+ const template = document.createElement("template");
370
+ template.innerHTML = this._renderPanel(filtered, total);
371
+ this._shadow.appendChild(template.content);
372
+ this._bindEvents();
373
+ return;
374
+ }
375
+ const countEl = panel.querySelector(".panel-count");
376
+ if (countEl) {
377
+ countEl.textContent = String(total);
378
+ countEl.setAttribute("data-count", String(total));
379
+ }
380
+ const fabCountEl = this._shadow.querySelector(".fab-count");
381
+ if (fabCountEl) {
382
+ fabCountEl.textContent = String(total);
383
+ fabCountEl.setAttribute("data-count", String(total));
384
+ }
385
+ const body = panel.querySelector(".panel-body");
386
+ if (body) body.innerHTML = this._renderBody(filtered);
387
+ this._bindHighlightEvents();
388
+ }
389
+ _renderScanning() {
390
+ const body = this._shadow.querySelector(".panel-body");
391
+ if (body) {
392
+ body.innerHTML = `
393
+ <div class="scanning-state" aria-live="polite" aria-label="Scanning for accessibility issues">
394
+ <span class="scanning-spinner" aria-hidden="true">${icon("refresh-cw")}</span>
395
+ Scanning\u2026
396
+ </div>
397
+ `;
398
+ }
399
+ }
400
+ _renderPanel(violations, total) {
401
+ return `
402
+ <div class="panel" role="complementary" aria-label="Accessibility panel">
403
+ <div class="panel-header">
404
+ <span class="panel-title">
405
+ <span class="panel-title-icon" aria-hidden="true">${icon("filter")}</span>
406
+ a11y
407
+ <span class="panel-count" data-count="${total}" aria-live="polite" aria-label="${total} violations">${total}</span>
408
+ </span>
409
+ <div class="panel-actions" role="toolbar" aria-label="Panel actions">
410
+ <button class="btn-icon" id="btn-copy-all" aria-label="Copy all violations to clipboard" title="Copy all">
411
+ ${icon("copy")}
412
+ </button>
413
+ <button class="btn-icon" id="btn-rescan" aria-label="Re-scan page" title="Re-scan">
414
+ ${icon("refresh-cw")}
415
+ </button>
416
+ <button class="btn-icon" id="btn-minimize" aria-label="Minimize panel" title="Minimize">
417
+ ${icon("minus")}
418
+ </button>
419
+ </div>
420
+ </div>
421
+ ${this._renderFilters()}
422
+ <div class="panel-body" role="region" aria-label="Violations">
423
+ ${this._renderBody(violations)}
424
+ </div>
425
+ </div>
426
+ <button class="fab" aria-label="Open accessibility panel" title="Open a11y panel">
427
+ <span class="fab-icon" aria-hidden="true">${icon("filter")}</span>
428
+ a11y
429
+ <span class="fab-count" data-count="${total}">${total}</span>
430
+ </button>
431
+ `;
432
+ }
433
+ _renderFilters() {
434
+ const severities = ["critical", "serious", "moderate", "minor"];
435
+ const levels = ["A", "AA", "AAA"];
436
+ const severityChips = severities.map((s) => {
437
+ const active = this._activeSeverities.has(s);
438
+ const label = s.charAt(0).toUpperCase() + s.slice(1);
439
+ return `<button class="filter-chip" data-severity="${s}" aria-pressed="${active}">${label}</button>`;
440
+ }).join("");
441
+ const levelChips = levels.map((l) => {
442
+ const active = this._activeLevels.has(l);
443
+ return `<button class="filter-chip" data-level="${l}" aria-pressed="${active}">${l}</button>`;
444
+ }).join("");
445
+ return `
446
+ <div class="panel-filters" aria-label="Filters">
447
+ <div class="filter-group" role="group" aria-label="Filter by severity">${severityChips}</div>
448
+ <div class="filter-group" role="group" aria-label="Filter by WCAG level">${levelChips}</div>
449
+ </div>
450
+ `;
451
+ }
452
+ _renderBody(violations) {
453
+ if (!this._results) {
454
+ return `
455
+ <div class="empty-state">
456
+ <span class="scanning-spinner" aria-hidden="true">${icon("refresh-cw")}</span>
457
+ <span class="empty-state-body">Running first scan\u2026</span>
458
+ </div>
459
+ `;
460
+ }
461
+ if (violations.length === 0) {
462
+ return `
463
+ <div class="empty-state">
464
+ <span class="empty-state-icon" aria-hidden="true">${icon("check-circle")}</span>
465
+ <span class="empty-state-title">No violations found</span>
466
+ <span class="empty-state-body">
467
+ ${this._results.violations.length > 0 ? "All violations filtered out." : "Great job! The scanned area has no axe-core violations."}
468
+ </span>
469
+ </div>
470
+ `;
471
+ }
472
+ const items = violations.map((v, i) => this._renderViolationItem(v, i)).join("");
473
+ return `<ul class="violation-list" role="list" aria-label="${violations.length} violation${violations.length !== 1 ? "s" : ""}">${items}</ul>`;
474
+ }
475
+ _renderViolationItem(violation, index) {
476
+ const impact = violation.impact ?? "minor";
477
+ const iconName = SEVERITY_ICON[impact];
478
+ const nodeCount = violation.nodes.length;
479
+ const nodeLabel = `${nodeCount} node${nodeCount !== 1 ? "s" : ""}`;
480
+ const nodes = violation.nodes.map((node, ni) => {
481
+ const selector = this._nodeSelector(node.target);
482
+ return `
483
+ <li class="violation-node-item">
484
+ <button class="btn-highlight" data-violation="${index}" data-node="${ni}" aria-label="Highlight: ${selector}">
485
+ ${selector}
486
+ </button>
487
+ </li>
488
+ `;
489
+ }).join("");
490
+ return `
491
+ <li class="violation-item">
492
+ <button class="violation-toggle" aria-expanded="false" aria-controls="violation-detail-${index}">
493
+ <span class="severity-icon" data-severity="${impact}" aria-hidden="true">${icon(iconName)}</span>
494
+ <span class="violation-rule">${violation.id}</span>
495
+ <span class="violation-node-count" aria-label="${nodeLabel}">${nodeLabel}</span>
496
+ <span class="chevron-icon" aria-hidden="true">${icon("chevron-down")}</span>
497
+ </button>
498
+ <div class="violation-detail" id="violation-detail-${index}" role="region" aria-label="${violation.id} details">
499
+ <p class="violation-description">${violation.description}</p>
500
+ <ul class="violation-nodes-list" aria-label="Affected elements">${nodes}</ul>
501
+ <div class="violation-footer">
502
+ <a class="violation-help-link" href="${violation.helpUrl}" target="_blank" rel="noopener noreferrer">
503
+ Learn more \u2197
504
+ </a>
505
+ <button class="btn-copy-single" data-copy-violation="${index}" aria-label="Copy ${violation.id} to clipboard" title="Copy violation">
506
+ ${icon("copy")} Copy
507
+ </button>
508
+ </div>
509
+ </div>
510
+ </li>
511
+ `;
512
+ }
513
+ _nodeSelector(target) {
514
+ const last = target[target.length - 1];
515
+ return (Array.isArray(last) ? last[last.length - 1] : last) ?? "";
516
+ }
517
+ _bindEvents() {
518
+ const panel = this._shadow.querySelector(".panel");
519
+ if (!panel) return;
520
+ panel.querySelector("#btn-rescan")?.addEventListener("click", () => void this._runScan());
521
+ panel.querySelector("#btn-minimize")?.addEventListener("click", () => {
522
+ this.setAttribute("data-minimized", "");
523
+ });
524
+ panel.querySelector("#btn-copy-all")?.addEventListener("click", () => void this._copyAll());
525
+ panel.addEventListener("click", (e) => this._handlePanelClick(e));
526
+ panel.addEventListener("keydown", (e) => this._handlePanelKeydown(e));
527
+ this._shadow.querySelector(".fab")?.addEventListener("click", () => {
528
+ this.removeAttribute("data-minimized");
529
+ });
530
+ this._bindHighlightEvents();
531
+ }
532
+ _bindHighlightEvents() {
533
+ for (const btn of this._shadow.querySelectorAll(".btn-highlight")) {
534
+ btn.addEventListener("click", (e) => {
535
+ const target = e.currentTarget;
536
+ const vIdx = Number.parseInt(target.dataset.violation ?? "", 10);
537
+ const nIdx = Number.parseInt(target.dataset.node ?? "", 10);
538
+ this._highlightNode(vIdx, nIdx);
539
+ });
540
+ }
541
+ }
542
+ _handlePanelClick(e) {
543
+ const target = e.target;
544
+ const toggle = target.closest(".violation-toggle");
545
+ if (toggle) {
546
+ const expanded = toggle.getAttribute("aria-expanded") === "true";
547
+ toggle.setAttribute("aria-expanded", String(!expanded));
548
+ const detailId = toggle.getAttribute("aria-controls");
549
+ const detail = detailId ? this._shadow.getElementById(detailId) : null;
550
+ if (detail) {
551
+ if (expanded) {
552
+ detail.removeAttribute("data-open");
553
+ } else {
554
+ detail.setAttribute("data-open", "");
555
+ }
556
+ }
557
+ return;
558
+ }
559
+ const copyBtn = target.closest(".btn-copy-single");
560
+ if (copyBtn) {
561
+ const vIdx = Number.parseInt(copyBtn.dataset.copyViolation ?? "", 10);
562
+ void this._copySingle(vIdx);
563
+ return;
564
+ }
565
+ const chip = target.closest(".filter-chip");
566
+ if (chip) {
567
+ const severity = chip.dataset.severity;
568
+ const level = chip.dataset.level;
569
+ if (severity && isSeverity(severity)) {
570
+ const active = this._activeSeverities.has(severity);
571
+ if (active && this._activeSeverities.size > 1) {
572
+ this._activeSeverities.delete(severity);
573
+ chip.setAttribute("aria-pressed", "false");
574
+ } else if (!active) {
575
+ this._activeSeverities.add(severity);
576
+ chip.setAttribute("aria-pressed", "true");
577
+ }
578
+ this._refreshBody();
579
+ } else if (level) {
580
+ const active = this._activeLevels.has(level);
581
+ if (active && this._activeLevels.size > 1) {
582
+ this._activeLevels.delete(level);
583
+ chip.setAttribute("aria-pressed", "false");
584
+ } else if (!active) {
585
+ this._activeLevels.add(level);
586
+ chip.setAttribute("aria-pressed", "true");
587
+ }
588
+ this._refreshBody();
589
+ }
590
+ }
591
+ }
592
+ _handlePanelKeydown(e) {
593
+ if (e.key === "Escape") {
594
+ this.setAttribute("data-minimized", "");
595
+ return;
596
+ }
597
+ const list = this._shadow.querySelector(".violation-list");
598
+ if (!list) return;
599
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
600
+ const toggles = Array.from(list.querySelectorAll(".violation-toggle"));
601
+ const current = this._shadow.activeElement;
602
+ const idx = current ? toggles.indexOf(current) : -1;
603
+ const next = e.key === "ArrowDown" ? toggles[idx + 1] ?? toggles[0] : toggles[idx - 1] ?? toggles[toggles.length - 1];
604
+ if (next) {
605
+ e.preventDefault();
606
+ next.focus();
607
+ }
608
+ }
609
+ }
610
+ _refreshBody() {
611
+ const body = this._shadow.querySelector(".panel-body");
612
+ if (body) {
613
+ body.innerHTML = this._renderBody(this._getFilteredViolations());
614
+ this._bindHighlightEvents();
615
+ }
616
+ const total = this._results?.violations.length ?? 0;
617
+ const countEl = this._shadow.querySelector(".panel-count");
618
+ if (countEl) {
619
+ countEl.textContent = String(total);
620
+ countEl.setAttribute("data-count", String(total));
621
+ }
622
+ const fabCountEl = this._shadow.querySelector(".fab-count");
623
+ if (fabCountEl) {
624
+ fabCountEl.textContent = String(total);
625
+ fabCountEl.setAttribute("data-count", String(total));
626
+ }
627
+ }
628
+ _formatViolationsAsMarkdown(violations) {
629
+ const header = `# a11y-hud scan results
630
+ **URL:** ${location.href}
631
+ **Time:** ${(/* @__PURE__ */ new Date()).toISOString()}
632
+
633
+ `;
634
+ const body = violations.map((v) => {
635
+ const nodes = v.nodes.map((n) => {
636
+ const lines = ["```html", n.html, "```"];
637
+ if (n.failureSummary) lines.push(`*Failure:* ${n.failureSummary}`);
638
+ return lines.join("\n");
639
+ }).join("\n\n");
640
+ return [
641
+ "---",
642
+ `### [${(v.impact ?? "minor").toUpperCase()}] ${v.id}`,
643
+ `**Help:** ${v.help}`,
644
+ `**Description:** ${v.description}`,
645
+ `**Affected elements (${v.nodes.length}):**`,
646
+ nodes,
647
+ `**More info:** ${v.helpUrl}`
648
+ ].join("\n");
649
+ }).join("\n\n");
650
+ return header + body;
651
+ }
652
+ async _copyToClipboard(text) {
653
+ try {
654
+ await navigator.clipboard.writeText(text);
655
+ } catch {
656
+ const ta = document.createElement("textarea");
657
+ ta.value = text;
658
+ ta.style.cssText = "position:fixed;opacity:0;pointer-events:none";
659
+ document.body.appendChild(ta);
660
+ ta.select();
661
+ document.execCommand("copy");
662
+ ta.remove();
663
+ }
664
+ }
665
+ async _copyAll() {
666
+ const violations = this._results?.violations;
667
+ if (!violations?.length) return;
668
+ await this._copyToClipboard(this._formatViolationsAsMarkdown(violations));
669
+ }
670
+ async _copySingle(violationIndex) {
671
+ const violation = this._getFilteredViolations()[violationIndex];
672
+ if (!violation) return;
673
+ await this._copyToClipboard(this._formatViolationsAsMarkdown([violation]));
674
+ }
675
+ _highlightNode(violationIndex, nodeIndex) {
676
+ this._removeHighlight?.();
677
+ this._removeHighlight = void 0;
678
+ const violation = this._getFilteredViolations()[violationIndex];
679
+ if (!violation) return;
680
+ const node = violation.nodes[nodeIndex];
681
+ if (!node) return;
682
+ const el = findElement(node.target);
683
+ if (!el) return;
684
+ ensureHighlightStyle(document);
685
+ el.setAttribute(HIGHLIGHT_ATTR, "");
686
+ el.scrollIntoView({ behavior: "smooth", block: "nearest" });
687
+ this._removeHighlight = () => el.removeAttribute(HIGHLIGHT_ATTR);
688
+ }
689
+ };
690
+ customElements.define("a11y-hud", A11yHudElement);
691
+
692
+ export { A11yHudElement, mount, runScan };
693
+ //# sourceMappingURL=index.js.map
694
+ //# sourceMappingURL=index.js.map