@a.nemreen/a11y 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.cjs ADDED
@@ -0,0 +1,1238 @@
1
+ 'use strict';
2
+
3
+ // src/controller.ts
4
+ var DEFAULT_STORAGE_KEY = "dga-a11y";
5
+ var FONT_SCALES = [1, 1.15, 1.3, 1.5];
6
+ var FONT_STEPS = [0, 1, 2, 3];
7
+ var TEXT_ALIGNS = ["default", "end", "start", "justify"];
8
+ var LINE_HEIGHTS = ["normal", "1.6", "1.8", "2.0"];
9
+ var LETTER_SPACINGS = ["0", "0.04em", "0.08em", "0.12em"];
10
+ var WORD_SPACINGS = ["0", "0.16em", "0.32em", "0.48em"];
11
+ var A11Y_BUNDLES = [
12
+ "epilepsy-safe",
13
+ "visually-impaired",
14
+ "cognitive-disability",
15
+ "motor-impaired",
16
+ "colorblind",
17
+ "dyslexia-friendly",
18
+ "adhd-friendly"
19
+ ];
20
+ var BUNDLE_PRIMITIVES = {
21
+ "epilepsy-safe": ["reduce-motion"],
22
+ "visually-impaired": ["high-contrast"],
23
+ "cognitive-disability": ["highlight-titles", "reduce-motion"],
24
+ "motor-impaired": ["motor-targets"],
25
+ colorblind: [],
26
+ "dyslexia-friendly": ["dyslexia", "highlight-links"],
27
+ "adhd-friendly": ["reduce-motion", "highlight-titles", "reading-mask"]
28
+ };
29
+ var A11yController = class {
30
+ constructor(options = {}) {
31
+ this.listeners = /* @__PURE__ */ new Set();
32
+ this.liveRegion = null;
33
+ this.opener = null;
34
+ this.mqMotion = null;
35
+ this.mqContrast = null;
36
+ this.localeObserver = null;
37
+ this.localeOverride = null;
38
+ this.destroyed = false;
39
+ this.isOpen = false;
40
+ this.modes = /* @__PURE__ */ new Set();
41
+ this.visualFilter = "none";
42
+ this.fontStep = 0;
43
+ this.textAlign = "default";
44
+ this.lineHeight = "normal";
45
+ this.letterSpacing = "0";
46
+ this.wordSpacing = "0";
47
+ this.maskBand = 60;
48
+ this.maskY = 0;
49
+ this.locale = "ar";
50
+ this.onOsChange = () => {
51
+ this.modes = new Set(this.modes);
52
+ if (this.mqMotion?.matches) this.modes.add("reduce-motion");
53
+ if (this.mqContrast?.matches) this.modes.add("high-contrast");
54
+ this.notify();
55
+ };
56
+ this.doc = options.document ?? document;
57
+ this.storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
58
+ if (options.locale && options.locale !== "auto") {
59
+ this.localeOverride = options.locale;
60
+ this.locale = options.locale;
61
+ } else {
62
+ this.locale = this.readDomLocale();
63
+ }
64
+ this.hydrate();
65
+ this.bindOsPreferences();
66
+ this.bindLocaleObserver();
67
+ }
68
+ subscribe(listener) {
69
+ this.listeners.add(listener);
70
+ return () => this.listeners.delete(listener);
71
+ }
72
+ emit() {
73
+ for (const fn of this.listeners) fn();
74
+ }
75
+ notify() {
76
+ this.applyToDocument();
77
+ this.ensureDyslexiaFont(this.hasEffectiveMode("dyslexia"));
78
+ this.persist();
79
+ this.emit();
80
+ }
81
+ effectiveModes() {
82
+ const effective = new Set(this.modes);
83
+ for (const b of A11Y_BUNDLES) {
84
+ if (this.modes.has(b)) {
85
+ for (const p of BUNDLE_PRIMITIVES[b]) effective.add(p);
86
+ }
87
+ }
88
+ return effective;
89
+ }
90
+ hasEffectiveMode(mode) {
91
+ return this.effectiveModes().has(mode);
92
+ }
93
+ open(opener) {
94
+ if (opener) this.opener = opener;
95
+ this.isOpen = true;
96
+ this.emit();
97
+ }
98
+ close() {
99
+ this.isOpen = false;
100
+ const el = this.opener;
101
+ this.opener = null;
102
+ this.emit();
103
+ queueMicrotask(() => el?.focus?.());
104
+ }
105
+ toggle(opener) {
106
+ if (this.isOpen) this.close();
107
+ else this.open(opener);
108
+ }
109
+ hasMode(mode) {
110
+ return this.modes.has(mode);
111
+ }
112
+ isBundle(mode) {
113
+ return A11Y_BUNDLES.includes(mode);
114
+ }
115
+ toggleMode(mode) {
116
+ this.setMode(mode, !this.modes.has(mode));
117
+ }
118
+ setMode(mode, enabled) {
119
+ if (enabled) {
120
+ if (this.modes.has(mode)) return;
121
+ this.modes = new Set(this.modes);
122
+ this.modes.add(mode);
123
+ if (this.isBundle(mode)) this.applyBundleExtras(mode, true);
124
+ this.notify();
125
+ return;
126
+ }
127
+ const bundlesToDisable = [];
128
+ if (this.isBundle(mode)) {
129
+ if (!this.modes.has(mode)) return;
130
+ bundlesToDisable.push(mode);
131
+ } else {
132
+ for (const b of A11Y_BUNDLES) {
133
+ if (this.modes.has(b) && BUNDLE_PRIMITIVES[b].includes(mode)) {
134
+ bundlesToDisable.push(b);
135
+ }
136
+ }
137
+ if (!this.modes.has(mode) && bundlesToDisable.length === 0) return;
138
+ }
139
+ this.modes = new Set(this.modes);
140
+ this.modes.delete(mode);
141
+ for (const b of bundlesToDisable) this.modes.delete(b);
142
+ for (const b of bundlesToDisable) {
143
+ this.applyBundleExtras(b, false);
144
+ }
145
+ this.notify();
146
+ }
147
+ setVisualFilter(filter) {
148
+ this.visualFilter = this.visualFilter === filter ? "none" : filter;
149
+ this.notify();
150
+ }
151
+ stepFontStep(delta) {
152
+ const len = FONT_STEPS.length;
153
+ this.fontStep = ((this.fontStep + delta) % len + len) % len;
154
+ this.notify();
155
+ }
156
+ stepTextAlign(delta) {
157
+ const len = TEXT_ALIGNS.length;
158
+ const i = TEXT_ALIGNS.indexOf(this.textAlign);
159
+ this.textAlign = TEXT_ALIGNS[((i + delta) % len + len) % len];
160
+ this.notify();
161
+ }
162
+ stepLineHeight(delta) {
163
+ const len = LINE_HEIGHTS.length;
164
+ const i = LINE_HEIGHTS.indexOf(this.lineHeight);
165
+ this.lineHeight = LINE_HEIGHTS[((i + delta) % len + len) % len];
166
+ this.notify();
167
+ }
168
+ stepLetterSpacing(delta) {
169
+ const len = LETTER_SPACINGS.length;
170
+ const i = LETTER_SPACINGS.indexOf(this.letterSpacing);
171
+ this.letterSpacing = LETTER_SPACINGS[((i + delta) % len + len) % len];
172
+ this.notify();
173
+ }
174
+ stepWordSpacing(delta) {
175
+ const len = WORD_SPACINGS.length;
176
+ const i = WORD_SPACINGS.indexOf(this.wordSpacing);
177
+ this.wordSpacing = WORD_SPACINGS[((i + delta) % len + len) % len];
178
+ this.notify();
179
+ }
180
+ adjustMaskBand(delta) {
181
+ this.maskBand = Math.max(20, Math.min(160, this.maskBand + delta));
182
+ this.notify();
183
+ }
184
+ setMaskY(y) {
185
+ const vh = this.doc.defaultView?.innerHeight ?? 800;
186
+ this.maskY = Math.max(0, Math.min(vh, y));
187
+ this.notify();
188
+ }
189
+ reset() {
190
+ this.modes = /* @__PURE__ */ new Set();
191
+ this.visualFilter = "none";
192
+ this.fontStep = 0;
193
+ this.textAlign = "default";
194
+ this.lineHeight = "normal";
195
+ this.letterSpacing = "0";
196
+ this.wordSpacing = "0";
197
+ this.maskBand = 60;
198
+ const vh = this.doc.defaultView?.innerHeight ?? 800;
199
+ this.maskY = Math.round(vh / 2);
200
+ this.notify();
201
+ }
202
+ announce(msg) {
203
+ const region = this.ensureLiveRegion();
204
+ region.textContent = "";
205
+ requestAnimationFrame(() => {
206
+ region.textContent = msg;
207
+ });
208
+ }
209
+ toggleLocale() {
210
+ const next = this.locale === "ar" ? "en" : "ar";
211
+ const root = this.doc.documentElement;
212
+ root.lang = next;
213
+ root.dir = next === "ar" ? "rtl" : "ltr";
214
+ root.dataset["locale"] = next;
215
+ this.localeOverride = next;
216
+ this.locale = next;
217
+ this.emit();
218
+ return next;
219
+ }
220
+ activeModeCount(section) {
221
+ if (section === "modes") {
222
+ return A11Y_BUNDLES.filter((b) => this.modes.has(b)).length;
223
+ }
224
+ if (section === "readable") {
225
+ let n = 0;
226
+ if (this.fontStep > 0) n++;
227
+ if (this.modes.has("dyslexia")) n++;
228
+ if (this.modes.has("highlight-titles")) n++;
229
+ if (this.modes.has("highlight-links")) n++;
230
+ if (this.modes.has("reading-mask")) n++;
231
+ if (this.modes.has("reduce-motion")) n++;
232
+ if (this.textAlign !== "default") n++;
233
+ if (this.lineHeight !== "normal") n++;
234
+ if (this.letterSpacing !== "0") n++;
235
+ if (this.wordSpacing !== "0") n++;
236
+ return n;
237
+ }
238
+ return this.visualFilter === "none" ? 0 : 1;
239
+ }
240
+ activeSettingCount() {
241
+ return this.activeModeCount("modes") + this.activeModeCount("readable") + this.activeModeCount("visual");
242
+ }
243
+ destroy() {
244
+ if (this.destroyed) return;
245
+ this.destroyed = true;
246
+ this.localeObserver?.disconnect();
247
+ this.localeObserver = null;
248
+ this.mqMotion?.removeEventListener?.("change", this.onOsChange);
249
+ this.mqContrast?.removeEventListener?.("change", this.onOsChange);
250
+ this.listeners.clear();
251
+ this.liveRegion?.remove();
252
+ this.liveRegion = null;
253
+ this.doc.getElementById("dga-a11y-opendyslexic")?.remove();
254
+ }
255
+ applyBundleExtras(bundle, enable) {
256
+ const extras = BUNDLE_PRIMITIVES[bundle];
257
+ this.modes = new Set(this.modes);
258
+ for (const p of extras) {
259
+ if (enable) {
260
+ this.modes.add(p);
261
+ } else {
262
+ const stillNeeded = A11Y_BUNDLES.some(
263
+ (b) => b !== bundle && this.modes.has(b) && BUNDLE_PRIMITIVES[b].includes(p)
264
+ );
265
+ if (!stillNeeded) this.modes.delete(p);
266
+ }
267
+ }
268
+ if (bundle === "epilepsy-safe") {
269
+ if (enable) this.visualFilter = "low-saturation";
270
+ else if (this.visualFilter === "low-saturation") this.visualFilter = "none";
271
+ }
272
+ if (bundle === "colorblind") {
273
+ if (enable) this.visualFilter = "deuteranopia";
274
+ else if (this.visualFilter === "deuteranopia") this.visualFilter = "none";
275
+ }
276
+ if (bundle === "visually-impaired") {
277
+ if (enable) this.fontStep = 2;
278
+ else if (this.fontStep === 2) this.fontStep = 0;
279
+ }
280
+ if (bundle === "dyslexia-friendly") {
281
+ if (enable) {
282
+ this.lineHeight = "1.8";
283
+ this.letterSpacing = "0.08em";
284
+ this.wordSpacing = "0.16em";
285
+ } else {
286
+ if (this.lineHeight === "1.8") this.lineHeight = "normal";
287
+ if (this.letterSpacing === "0.08em") this.letterSpacing = "0";
288
+ if (this.wordSpacing === "0.16em") this.wordSpacing = "0";
289
+ }
290
+ }
291
+ }
292
+ ensureDyslexiaFont(needed) {
293
+ const id = "dga-a11y-opendyslexic";
294
+ const existing = this.doc.getElementById(id);
295
+ if (!needed) {
296
+ existing?.remove();
297
+ return;
298
+ }
299
+ if (existing) return;
300
+ const style = this.doc.createElement("style");
301
+ style.id = id;
302
+ style.textContent = `
303
+ @font-face {
304
+ font-family: 'OpenDyslexic';
305
+ font-style: normal;
306
+ font-weight: 400;
307
+ font-display: swap;
308
+ src: url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.2.5/latin-400-normal.woff2') format('woff2');
309
+ }
310
+ @font-face {
311
+ font-family: 'OpenDyslexic';
312
+ font-style: normal;
313
+ font-weight: 700;
314
+ font-display: swap;
315
+ src: url('https://cdn.jsdelivr.net/fontsource/fonts/opendyslexic@5.2.5/latin-700-normal.woff2') format('woff2');
316
+ }`;
317
+ this.doc.head.appendChild(style);
318
+ }
319
+ applyToDocument() {
320
+ const root = this.doc.documentElement;
321
+ const modes = this.modes;
322
+ const tokens = /* @__PURE__ */ new Set();
323
+ for (const m of modes) tokens.add(m);
324
+ for (const b of A11Y_BUNDLES) {
325
+ if (modes.has(b)) {
326
+ for (const p of BUNDLE_PRIMITIVES[b]) tokens.add(p);
327
+ }
328
+ }
329
+ if (this.fontStep > 0) tokens.add(`font-step-${this.fontStep}`);
330
+ if (this.textAlign !== "default") tokens.add(`text-align-${this.textAlign}`);
331
+ if (this.lineHeight !== "normal") tokens.add("has-line-height");
332
+ if (this.letterSpacing !== "0") tokens.add("has-letter-spacing");
333
+ if (this.wordSpacing !== "0") tokens.add("has-word-spacing");
334
+ if (tokens.size) {
335
+ root.setAttribute("data-a11y", [...tokens].join(" "));
336
+ } else {
337
+ root.removeAttribute("data-a11y");
338
+ }
339
+ if (this.visualFilter === "none") {
340
+ root.removeAttribute("data-a11y-filter");
341
+ } else {
342
+ root.setAttribute("data-a11y-filter", this.visualFilter);
343
+ }
344
+ const scale = FONT_SCALES[this.fontStep] ?? 1;
345
+ root.style.setProperty("--dga-font-scale", String(scale));
346
+ root.style.setProperty("--user-font-scale", String(scale));
347
+ if (this.lineHeight !== "normal") {
348
+ root.style.setProperty("--user-line-height", this.lineHeight);
349
+ } else {
350
+ root.style.removeProperty("--user-line-height");
351
+ }
352
+ if (this.letterSpacing !== "0") {
353
+ root.style.setProperty("--user-letter-spacing", this.letterSpacing);
354
+ } else {
355
+ root.style.removeProperty("--user-letter-spacing");
356
+ }
357
+ if (this.wordSpacing !== "0") {
358
+ root.style.setProperty("--user-word-spacing", this.wordSpacing);
359
+ } else {
360
+ root.style.removeProperty("--user-word-spacing");
361
+ }
362
+ root.style.setProperty("--a11y-mask-band", `${this.maskBand}px`);
363
+ root.style.setProperty("--a11y-mask-y", `${this.maskY}px`);
364
+ }
365
+ persist() {
366
+ try {
367
+ const payload = {
368
+ modes: [...this.modes],
369
+ visualFilter: this.visualFilter,
370
+ fontStep: this.fontStep,
371
+ textAlign: this.textAlign,
372
+ lineHeight: this.lineHeight,
373
+ letterSpacing: this.letterSpacing,
374
+ wordSpacing: this.wordSpacing,
375
+ maskBand: this.maskBand,
376
+ maskY: this.maskY
377
+ };
378
+ localStorage.setItem(this.storageKey, JSON.stringify(payload));
379
+ } catch {
380
+ }
381
+ }
382
+ hydrate() {
383
+ const vh = this.doc.defaultView?.innerHeight ?? 800;
384
+ this.maskY = Math.round(vh / 2);
385
+ try {
386
+ const raw = localStorage.getItem(this.storageKey);
387
+ if (!raw) {
388
+ this.applyToDocument();
389
+ return;
390
+ }
391
+ const data = JSON.parse(raw);
392
+ if (Array.isArray(data.modes)) this.modes = new Set(data.modes);
393
+ if (data.visualFilter) this.visualFilter = data.visualFilter;
394
+ if (typeof data.fontStep === "number") {
395
+ this.fontStep = Math.max(0, Math.min(3, Math.floor(data.fontStep)));
396
+ }
397
+ if (data.textAlign) this.textAlign = data.textAlign;
398
+ if (data.lineHeight) this.lineHeight = data.lineHeight;
399
+ if (data.letterSpacing) this.letterSpacing = data.letterSpacing;
400
+ if (data.wordSpacing) this.wordSpacing = data.wordSpacing;
401
+ if (typeof data.maskBand === "number") {
402
+ this.maskBand = Math.max(20, Math.min(160, data.maskBand));
403
+ }
404
+ if (typeof data.maskY === "number") this.maskY = data.maskY;
405
+ } catch {
406
+ }
407
+ this.applyToDocument();
408
+ this.ensureDyslexiaFont(this.hasEffectiveMode("dyslexia"));
409
+ }
410
+ bindOsPreferences() {
411
+ const win = this.doc.defaultView;
412
+ if (!win?.matchMedia) return;
413
+ this.mqMotion = win.matchMedia("(prefers-reduced-motion: reduce)");
414
+ this.mqContrast = win.matchMedia("(prefers-contrast: more)");
415
+ this.onOsChange();
416
+ this.mqMotion.addEventListener?.("change", this.onOsChange);
417
+ this.mqContrast.addEventListener?.("change", this.onOsChange);
418
+ }
419
+ readDomLocale() {
420
+ const root = this.doc.documentElement;
421
+ const data = root.dataset["locale"]?.toLowerCase();
422
+ if (data === "en" || data === "ar") return data;
423
+ const lang = root.lang?.toLowerCase() ?? "ar";
424
+ return lang.startsWith("en") ? "en" : "ar";
425
+ }
426
+ bindLocaleObserver() {
427
+ if (this.localeOverride) return;
428
+ const root = this.doc.documentElement;
429
+ if (typeof MutationObserver === "undefined") return;
430
+ this.localeObserver = new MutationObserver(() => {
431
+ const next = this.readDomLocale();
432
+ if (next === this.locale) return;
433
+ this.locale = next;
434
+ this.emit();
435
+ });
436
+ this.localeObserver.observe(root, {
437
+ attributes: true,
438
+ attributeFilter: ["lang", "data-locale"]
439
+ });
440
+ }
441
+ ensureLiveRegion() {
442
+ if (this.liveRegion?.isConnected) return this.liveRegion;
443
+ const el = this.doc.createElement("div");
444
+ el.id = "dga-a11y-live";
445
+ el.setAttribute("role", "status");
446
+ el.setAttribute("aria-live", "polite");
447
+ el.setAttribute("aria-atomic", "true");
448
+ el.className = "a11y-sr-only";
449
+ this.doc.body.appendChild(el);
450
+ this.liveRegion = el;
451
+ return el;
452
+ }
453
+ };
454
+
455
+ // src/skip-link.ts
456
+ function createSkipLink(options = {}) {
457
+ const a = document.createElement("a");
458
+ a.href = options.href ?? "#main";
459
+ a.className = "a11y-skip-link";
460
+ a.textContent = options.label ?? "Skip to content";
461
+ a.setAttribute("data-a11y-ui", "");
462
+ const styleId = "a11y-skip-link-style";
463
+ if (!document.getElementById(styleId)) {
464
+ const style = document.createElement("style");
465
+ style.id = styleId;
466
+ style.textContent = `
467
+ .a11y-skip-link {
468
+ position: absolute;
469
+ inset-inline-start: 1rem;
470
+ top: 1rem;
471
+ z-index: 10000;
472
+ padding: 0.5rem 1rem;
473
+ border-radius: 0.375rem;
474
+ background: #fff;
475
+ color: #1b8354;
476
+ font: inherit;
477
+ font-size: 0.875rem;
478
+ font-weight: 600;
479
+ text-decoration: none;
480
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
481
+ transform: translateY(-200%);
482
+ transition: transform 0.15s ease;
483
+ }
484
+ .a11y-skip-link:focus,
485
+ .a11y-skip-link:focus-visible {
486
+ transform: translateY(0);
487
+ outline: 2px solid currentColor;
488
+ outline-offset: 2px;
489
+ }`;
490
+ document.head.appendChild(style);
491
+ }
492
+ const root = options.root ?? document.body;
493
+ root.prepend(a);
494
+ return a;
495
+ }
496
+
497
+ // src/i18n.ts
498
+ var AR = {
499
+ open: "\u0641\u062A\u062D \u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0625\u0645\u0643\u0627\u0646\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644",
500
+ close: "\u0625\u063A\u0644\u0627\u0642 \u0644\u0648\u062D\u0629 \u0625\u0645\u0643\u0627\u0646\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644",
501
+ panelLabel: "\u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0625\u0645\u0643\u0627\u0646\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644",
502
+ title: "\u0623\u062F\u0648\u0627\u062A \u0625\u0645\u0643\u0627\u0646\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644",
503
+ sectionActive: "\u0645\u0641\u0639\u0651\u0644 \u0627\u0644\u0622\u0646",
504
+ sectionProfiles: "\u0623\u0648\u0636\u0627\u0639 \u0633\u0631\u064A\u0639\u0629",
505
+ sectionAdjust: "\u0636\u0628\u0637",
506
+ sectionFilters: "\u0645\u0631\u0634\u062D\u0627\u062A \u0628\u0635\u0631\u064A\u0629",
507
+ profilesHint: "\u064A\u0645\u0643\u0646 \u062A\u0641\u0639\u064A\u0644 \u0623\u0643\u062B\u0631 \u0645\u0646 \u0648\u0636\u0639\u061B \u0642\u062F \u062A\u062A\u062F\u0627\u062E\u0644 \u0628\u0639\u0636 \u0627\u0644\u062A\u0623\u062B\u064A\u0631\u0627\u062A.",
508
+ chipJump: "\u0627\u0644\u0627\u0646\u062A\u0642\u0627\u0644 \u0625\u0644\u0649",
509
+ stepDecrease: "\u0625\u0646\u0642\u0627\u0635",
510
+ stepIncrease: "\u0632\u064A\u0627\u062F\u0629",
511
+ reset: "\u0625\u0639\u0627\u062F\u0629 \u062A\u0639\u064A\u064A\u0646 \u0627\u0644\u0625\u0639\u062F\u0627\u062F\u0627\u062A",
512
+ resetConfirm: "\u062A\u0623\u0643\u064A\u062F \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u0639\u064A\u064A\u0646",
513
+ announcedReset: "\u062A\u0645\u062A \u0625\u0639\u0627\u062F\u0629 \u062A\u0639\u064A\u064A\u0646 \u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0625\u0645\u0643\u0627\u0646\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644",
514
+ fontSize: "\u062D\u062C\u0645 \u0627\u0644\u062E\u0637",
515
+ dyslexia: "\u062E\u0637 \u0645\u0646\u0627\u0633\u0628 \u0644\u0639\u0633\u0631 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
516
+ highlightTitles: "\u062A\u0645\u064A\u064A\u0632 \u0627\u0644\u0639\u0646\u0627\u0648\u064A\u0646",
517
+ highlightLinks: "\u062A\u0645\u064A\u064A\u0632 \u0627\u0644\u0631\u0648\u0627\u0628\u0637",
518
+ readingMask: "\u0642\u0646\u0627\u0639 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
519
+ reduceMotion: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u062D\u0631\u0643\u0629",
520
+ textAlign: "\u0645\u062D\u0627\u0630\u0627\u0629 \u0627\u0644\u0646\u0635",
521
+ lineHeight: "\u0627\u0631\u062A\u0641\u0627\u0639 \u0627\u0644\u0633\u0637\u0631",
522
+ letterSpacing: "\u062A\u0628\u0627\u0639\u062F \u0627\u0644\u062D\u0631\u0648\u0641",
523
+ wordSpacing: "\u062A\u0628\u0627\u0639\u062F \u0627\u0644\u0643\u0644\u0645\u0627\u062A",
524
+ filterBoost: "\u062A\u0639\u0632\u064A\u0632 \u0627\u0644\u062A\u0628\u0627\u064A\u0646",
525
+ filterMono: "\u0623\u0628\u064A\u0636 \u0648\u0623\u0633\u0648\u062F",
526
+ filterHigh: "\u062A\u0628\u0627\u064A\u0646 \u0639\u0627\u0644\u064D",
527
+ filterHighSat: "\u062A\u0634\u0628\u0639 \u0639\u0627\u0644\u064D",
528
+ filterLowSat: "\u062A\u0634\u0628\u0639 \u0645\u0646\u062E\u0641\u0636",
529
+ filterDeutan: "\u0645\u062D\u0627\u0643\u0627\u0629 \u0639\u0645\u0649 \u0627\u0644\u0623\u062E\u0636\u0631",
530
+ maskToolbar: "\u0623\u062F\u0648\u0627\u062A \u0642\u0646\u0627\u0639 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
531
+ maskSmaller: "\u062A\u0635\u063A\u064A\u0631 \u0627\u0644\u0634\u0631\u064A\u0637",
532
+ maskLarger: "\u062A\u0643\u0628\u064A\u0631 \u0627\u0644\u0634\u0631\u064A\u0637",
533
+ maskDrag: "\u0633\u062D\u0628",
534
+ maskClose: "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0642\u0646\u0627\u0639",
535
+ stateOn: "\u0645\u0641\u0639\u0651\u0644",
536
+ stateOff: "\u0625\u064A\u0642\u0627\u0641",
537
+ defaultValue: "\u0627\u0641\u062A\u0631\u0627\u0636\u064A",
538
+ fontLevels: ["\u0627\u0641\u062A\u0631\u0627\u0636\u064A", "\u0643\u0628\u064A\u0631", "\u0623\u0643\u0628\u0631", "\u0627\u0644\u0623\u0643\u0628\u0631"],
539
+ alignLevels: ["\u0627\u0641\u062A\u0631\u0627\u0636\u064A", "\u0646\u0647\u0627\u064A\u0629", "\u0628\u062F\u0627\u064A\u0629", "\u0636\u0628\u0637"],
540
+ lineLevels: ["\u0627\u0641\u062A\u0631\u0627\u0636\u064A", "\u0661\u066B\u0666", "\u0661\u066B\u0668", "\u0662\u066B\u0660"],
541
+ letterLevels: ["\u0627\u0641\u062A\u0631\u0627\u0636\u064A", "\u0636\u064A\u0642", "\u0645\u062A\u0648\u0633\u0637", "\u0648\u0627\u0633\u0639"],
542
+ wordLevels: ["\u0627\u0641\u062A\u0631\u0627\u0636\u064A", "\u0636\u064A\u0642", "\u0645\u062A\u0648\u0633\u0637", "\u0648\u0627\u0633\u0639"],
543
+ bundles: {
544
+ "epilepsy-safe": {
545
+ label: "\u0648\u0636\u0639 \u0622\u0645\u0646 \u0644\u0644\u0635\u0631\u0639",
546
+ desc: "\u064A\u0648\u0642\u0641 \u0627\u0644\u062D\u0631\u0643\u0629 \u0648\u064A\u062E\u0641\u0641 \u0643\u062B\u0627\u0641\u0629 \u0627\u0644\u0623\u0644\u0648\u0627\u0646"
547
+ },
548
+ "visually-impaired": {
549
+ label: "\u0648\u0636\u0639 \u0636\u0639\u0641 \u0627\u0644\u0628\u0635\u0631",
550
+ desc: "\u064A\u0643\u0628\u0651\u0631 \u0627\u0644\u0646\u0635 \u0648\u064A\u0639\u0632\u0632 \u0627\u0644\u062A\u0628\u0627\u064A\u0646 \u0644\u0644\u0642\u0631\u0627\u0621\u0629 \u0623\u0648\u0636\u062D"
551
+ },
552
+ "cognitive-disability": {
553
+ label: "\u0648\u0636\u0639 \u0627\u0644\u0625\u0639\u0627\u0642\u0629 \u0627\u0644\u0625\u062F\u0631\u0627\u0643\u064A\u0629",
554
+ desc: "\u064A\u0645\u064A\u0651\u0632 \u0627\u0644\u0639\u0646\u0627\u0648\u064A\u0646 \u0648\u064A\u0648\u0642\u0641 \u0627\u0644\u062D\u0631\u0643\u0629 \u0644\u062A\u0642\u0644\u064A\u0644 \u0627\u0644\u062A\u0634\u062A\u064A\u062A"
555
+ },
556
+ "motor-impaired": {
557
+ label: "\u0648\u0636\u0639 \u0636\u0639\u0641 \u0627\u0644\u062D\u0631\u0643\u0629",
558
+ desc: "\u064A\u0643\u0628\u0651\u0631 \u0627\u0644\u0623\u0632\u0631\u0627\u0631 \u0648\u0627\u0644\u062D\u0642\u0648\u0644\u060C \u064A\u0648\u0633\u0651\u0639 \u0645\u0646\u0637\u0642\u0629 \u0627\u0644\u0646\u0642\u0631\u060C \u0648\u064A\u064F\u0638\u0647\u0631 \u0625\u0637\u0627\u0631 \u062A\u0631\u0643\u064A\u0632 \u0623\u0648\u0636\u062D"
559
+ },
560
+ colorblind: {
561
+ label: "\u0648\u0636\u0639 \u0639\u0645\u0649 \u0627\u0644\u0623\u0644\u0648\u0627\u0646",
562
+ desc: "\u064A\u0636\u0628\u0637 \u0627\u0644\u0623\u0644\u0648\u0627\u0646 \u0644\u0644\u062A\u0645\u064A\u064A\u0632 \u0628\u064A\u0646 \u0627\u0644\u0623\u062D\u0645\u0631 \u0648\u0627\u0644\u0623\u062E\u0636\u0631"
563
+ },
564
+ "dyslexia-friendly": {
565
+ label: "\u0648\u0636\u0639 \u0645\u0646\u0627\u0633\u0628 \u0644\u0639\u0633\u0631 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
566
+ desc: "\u064A\u0628\u062F\u0651\u0644 \u0625\u0644\u0649 \u062E\u0637 \u0623\u0648\u0636\u062D \u0644\u0644\u0642\u0631\u0627\u0621\u0629 \u0648\u064A\u0648\u0633\u0651\u0639 \u062A\u0628\u0627\u0639\u062F \u0627\u0644\u0623\u0633\u0637\u0631 \u0648\u0627\u0644\u062D\u0631\u0648\u0641 \u0648\u0627\u0644\u0643\u0644\u0645\u0627\u062A"
567
+ },
568
+ "adhd-friendly": {
569
+ label: "\u0648\u0636\u0639 \u0645\u0646\u0627\u0633\u0628 \u0644\u0627\u0636\u0637\u0631\u0627\u0628 \u0641\u0631\u0637 \u0627\u0644\u062D\u0631\u0643\u0629",
570
+ desc: "\u064A\u0648\u0642\u0641 \u0627\u0644\u062D\u0631\u0643\u0629\u060C \u064A\u0645\u064A\u0651\u0632 \u0627\u0644\u0639\u0646\u0627\u0648\u064A\u0646\u060C \u0648\u064A\u0641\u0639\u0651\u0644 \u0642\u0646\u0627\u0639 \u0627\u0644\u0642\u0631\u0627\u0621\u0629"
571
+ }
572
+ }
573
+ };
574
+ var EN = {
575
+ open: "Open accessibility settings",
576
+ close: "Close accessibility panel",
577
+ panelLabel: "Accessibility settings",
578
+ title: "Accessibility Tools",
579
+ sectionActive: "Active now",
580
+ sectionProfiles: "Quick profiles",
581
+ sectionAdjust: "Adjust",
582
+ sectionFilters: "Visual filters",
583
+ profilesHint: "You can enable more than one profile; some effects may overlap.",
584
+ chipJump: "Jump to",
585
+ stepDecrease: "Decrease",
586
+ stepIncrease: "Increase",
587
+ reset: "Reset Settings",
588
+ resetConfirm: "Confirm reset",
589
+ announcedReset: "Accessibility settings reset",
590
+ fontSize: "Font size",
591
+ dyslexia: "Dyslexia friendly",
592
+ highlightTitles: "Highlight titles",
593
+ highlightLinks: "Highlight links",
594
+ readingMask: "Reading mask",
595
+ reduceMotion: "Pause motion",
596
+ textAlign: "Text alignment",
597
+ lineHeight: "Line height",
598
+ letterSpacing: "Letter spacing",
599
+ wordSpacing: "Word spacing",
600
+ filterBoost: "Boost contrast",
601
+ filterMono: "Monochrome",
602
+ filterHigh: "High contrast",
603
+ filterHighSat: "High saturation",
604
+ filterLowSat: "Low saturation",
605
+ filterDeutan: "Deuteranopia",
606
+ maskToolbar: "Reading mask toolbar",
607
+ maskSmaller: "Decrease band height",
608
+ maskLarger: "Increase band height",
609
+ maskDrag: "Drag",
610
+ maskClose: "Close mask",
611
+ stateOn: "On",
612
+ stateOff: "Off",
613
+ defaultValue: "Default",
614
+ fontLevels: ["Default", "Large", "Larger", "Largest"],
615
+ alignLevels: ["Default", "End", "Start", "Justify"],
616
+ lineLevels: ["Default", "1.6", "1.8", "2.0"],
617
+ letterLevels: ["Default", "Tight", "Medium", "Wide"],
618
+ wordLevels: ["Default", "Tight", "Medium", "Wide"],
619
+ bundles: {
620
+ "epilepsy-safe": {
621
+ label: "Epilepsy safe",
622
+ desc: "Stops motion and dampens color intensity"
623
+ },
624
+ "visually-impaired": {
625
+ label: "Visually impaired",
626
+ desc: "Enlarges text and boosts contrast for clearer reading"
627
+ },
628
+ "cognitive-disability": {
629
+ label: "Cognitive disability",
630
+ desc: "Highlights titles and stops motion to reduce distraction"
631
+ },
632
+ "motor-impaired": {
633
+ label: "Motor impaired",
634
+ desc: "Makes buttons and fields larger, expands click areas, and shows a thicker focus ring"
635
+ },
636
+ colorblind: {
637
+ label: "Colorblind",
638
+ desc: "Adjusts colors to distinguish red and green clearly"
639
+ },
640
+ "dyslexia-friendly": {
641
+ label: "Dyslexia friendly",
642
+ desc: "Switches to a clearer reading font and widens line, letter, and word spacing"
643
+ },
644
+ "adhd-friendly": {
645
+ label: "ADHD friendly",
646
+ desc: "Stops motion, highlights titles, and enables the reading mask"
647
+ }
648
+ }
649
+ };
650
+ function getCopy(locale) {
651
+ return locale === "ar" ? AR : EN;
652
+ }
653
+
654
+ // src/icons.ts
655
+ var ICONS = {
656
+ access: '<path d="M12 2a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm-1 7h2l1.5 4H16l-1 2h-1.2L12.5 11h-1L10.2 15H9l-1-2h1.5L11 9Zm-5.5 9.5L8 14l1.2 1.6L12 12l2.8 3.6L16 14l2.5 4.5H5.5Z"/>',
657
+ close: '<path d="M6.7 6.7a1 1 0 0 1 1.4 0L12 10.6l3.9-3.9a1 1 0 1 1 1.4 1.4L13.4 12l3.9 3.9a1 1 0 0 1-1.4 1.4L12 13.4l-3.9 3.9a1 1 0 0 1-1.4-1.4L10.6 12 6.7 8.1a1 1 0 0 1 0-1.4Z"/>',
658
+ flash: '<path d="M13 2 4 14h7l-1 8 10-14h-7l1-6Z"/>',
659
+ eye: '<path d="M12 5c-5 0-9 4.5-10 7 1 2.5 5 7 10 7s9-4.5 10-7c-1-2.5-5-7-10-7Zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/>',
660
+ brain: '<path d="M9 3a3 3 0 0 0-3 3v1a3 3 0 0 0-1 5.8V15a3 3 0 0 0 3 3h1v1a2 2 0 0 0 4 0v-1h1a3 3 0 0 0 3-3v-2.2A3 3 0 0 0 18 7V6a3 3 0 0 0-3-3h-1a3 3 0 0 0-5 0H9Z"/>',
661
+ touch: '<path d="M9 11V6a1.5 1.5 0 0 1 3 0v5h1V8a1.5 1.5 0 0 1 3 0v5h1V10a1.5 1.5 0 0 1 3 0v6a5 5 0 0 1-5 5h-2a7 7 0 0 1-7-7v-3a1.5 1.5 0 0 1 3 0v2"/>',
662
+ colors: '<path d="M12 3a9 9 0 0 0 0 18h1a3 3 0 0 0 0-6h-1a1 1 0 1 1 0-2 3 3 0 1 0 0-6 5 5 0 0 1 4.9 4"/>',
663
+ glasses: '<path d="M2 12h3a4 4 0 0 0 8 0h2a4 4 0 0 0 8 0h-1M10 12a2 2 0 0 1 4 0"/>',
664
+ mask: '<path d="M3 10c0-2 2-4 4-4h10c2 0 4 2 4 4v2c0 4-4 7-9 7s-9-3-9-7v-2Zm4 1h2m6 0h2"/>',
665
+ heading: '<path d="M6 5v14M18 5v14M6 12h12"/>',
666
+ linkAlt: '<path d="M10 13a5 5 0 0 0 7.5.5l2-2a5 5 0 0 0-7-7l-1 1M14 11a5 5 0 0 0-7.5-.5l-2 2a5 5 0 0 0 7 7l1-1"/>',
667
+ pause: '<path d="M8 5h3v14H8V5Zm5 0h3v14h-3V5Z"/>',
668
+ textFont: '<path d="M5 19h3l1-3h6l1 3h3L13 5h-2L5 19Zm5.5-6L12 8l1.5 5h-3Z"/>',
669
+ textAlign: '<path d="M4 6h16M4 10h10M4 14h16M4 18h10"/>',
670
+ textUnderline: '<path d="M6 5v6a6 6 0 0 0 12 0V5M5 19h14"/>',
671
+ letterSpacing: '<path d="M4 18V6M20 18V6M8 14l2-8h1l2 8M9 11h3M14.5 14 16 6h1l1.5 8"/>',
672
+ maximize: '<path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"/>',
673
+ minus: '<path d="M5 12h14"/>',
674
+ plus: '<path d="M12 5v14M5 12h14"/>',
675
+ droplet: '<path d="M12 3s6 7 6 11a6 6 0 1 1-12 0c0-4 6-11 6-11Z"/>',
676
+ sun: '<path d="M12 4V2m0 20v-2m8-8h2M2 12h2m13.7 5.7 1.4 1.4M4.9 4.9l1.4 1.4m0 11.4-1.4 1.4M19.1 4.9l-1.4 1.4M12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z"/>',
677
+ colorPicker: '<path d="M12 3a7 7 0 0 1 0 14h-1l-4 4v-5a7 7 0 0 1 5-13Z"/>'
678
+ };
679
+ function iconSvg(name, size = 18) {
680
+ const path = ICONS[name] ?? ICONS.access;
681
+ return `<svg class="a11y-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">${path}</svg>`;
682
+ }
683
+
684
+ // src/widget.ts
685
+ var BUNDLE_ICONS = {
686
+ "epilepsy-safe": "flash",
687
+ "visually-impaired": "eye",
688
+ "cognitive-disability": "brain",
689
+ "motor-impaired": "touch",
690
+ colorblind: "colors",
691
+ "dyslexia-friendly": "glasses",
692
+ "adhd-friendly": "mask"
693
+ };
694
+ var TOGGLE_ROWS = [
695
+ { id: "dyslexia", mode: "dyslexia", icon: "glasses", labelKey: "dyslexia" },
696
+ { id: "highlight-titles", mode: "highlight-titles", icon: "heading", labelKey: "highlightTitles" },
697
+ { id: "highlight-links", mode: "highlight-links", icon: "linkAlt", labelKey: "highlightLinks" },
698
+ { id: "reading-mask", mode: "reading-mask", icon: "mask", labelKey: "readingMask" },
699
+ { id: "reduce-motion", mode: "reduce-motion", icon: "pause", labelKey: "reduceMotion" }
700
+ ];
701
+ var FILTERS = [
702
+ { value: "boost-contrast", labelKey: "filterBoost", icon: "maximize" },
703
+ { value: "monochrome", labelKey: "filterMono", icon: "droplet" },
704
+ { value: "high-contrast", labelKey: "filterHigh", icon: "sun" },
705
+ { value: "high-saturation", labelKey: "filterHighSat", icon: "colors" },
706
+ { value: "low-saturation", labelKey: "filterLowSat", icon: "colorPicker" },
707
+ { value: "deuteranopia", labelKey: "filterDeutan", icon: "eye" }
708
+ ];
709
+ var PANEL_ID = "a11y-accessibility-panel";
710
+ var A11yWidget = class {
711
+ constructor(ctrl, options = {}) {
712
+ this.rootEl = null;
713
+ this.unsub = null;
714
+ this.resetArmed = false;
715
+ this.resetTimer = null;
716
+ this.draggingMask = false;
717
+ this.onKeyDown = (e) => this.handleKey(e);
718
+ this.onPointerMove = (e) => {
719
+ if (!this.draggingMask) return;
720
+ this.ctrl.setMaskY(e.clientY);
721
+ };
722
+ this.onPointerUp = () => {
723
+ this.draggingMask = false;
724
+ };
725
+ this.ctrl = ctrl;
726
+ this.position = options.position ?? "end";
727
+ this.stack = Math.max(0, options.stack ?? 0);
728
+ this.mountRoot = options.root ?? document.body;
729
+ }
730
+ mount() {
731
+ if (this.rootEl) return;
732
+ this.rootEl = document.createElement("div");
733
+ this.rootEl.className = "a11y-root";
734
+ this.rootEl.setAttribute("data-a11y-ui", "");
735
+ this.mountRoot.appendChild(this.rootEl);
736
+ this.unsub = this.ctrl.subscribe(() => this.render());
737
+ document.addEventListener("keydown", this.onKeyDown);
738
+ document.addEventListener("pointermove", this.onPointerMove);
739
+ document.addEventListener("pointerup", this.onPointerUp);
740
+ this.render();
741
+ }
742
+ destroy() {
743
+ this.unsub?.();
744
+ this.unsub = null;
745
+ document.removeEventListener("keydown", this.onKeyDown);
746
+ document.removeEventListener("pointermove", this.onPointerMove);
747
+ document.removeEventListener("pointerup", this.onPointerUp);
748
+ if (this.resetTimer) clearTimeout(this.resetTimer);
749
+ this.rootEl?.remove();
750
+ this.rootEl = null;
751
+ }
752
+ copy() {
753
+ return getCopy(this.ctrl.locale);
754
+ }
755
+ fabBottom() {
756
+ return `calc(var(--a11y-fab-edge, 2rem) + ${this.stack} * var(--a11y-fab-stack-step, 4.25rem))`;
757
+ }
758
+ activeChips() {
759
+ const c = this.copy();
760
+ const chips = [];
761
+ const covered = /* @__PURE__ */ new Set();
762
+ for (const b of A11Y_BUNDLES) {
763
+ if (!this.ctrl.hasMode(b)) continue;
764
+ chips.push({ id: b, label: c.bundles[b].label, controlId: `profile-${b}` });
765
+ for (const p of BUNDLE_PRIMITIVES[b]) covered.add(p);
766
+ }
767
+ for (const row of TOGGLE_ROWS) {
768
+ if (!this.ctrl.hasEffectiveMode(row.mode)) continue;
769
+ if (covered.has(row.mode) && !this.ctrl.hasMode(row.mode)) continue;
770
+ if (covered.has(row.mode) && A11Y_BUNDLES.some((b) => this.ctrl.hasMode(b) && BUNDLE_PRIMITIVES[b].includes(row.mode))) {
771
+ const ownedByActiveBundle = A11Y_BUNDLES.some(
772
+ (b) => this.ctrl.hasMode(b) && BUNDLE_PRIMITIVES[b].includes(row.mode)
773
+ );
774
+ if (ownedByActiveBundle && !this.ctrl.hasMode(row.mode)) continue;
775
+ if (ownedByActiveBundle) continue;
776
+ }
777
+ if (A11Y_BUNDLES.some((b) => this.ctrl.hasMode(b) && BUNDLE_PRIMITIVES[b].includes(row.mode))) {
778
+ continue;
779
+ }
780
+ chips.push({
781
+ id: row.id,
782
+ label: String(c[row.labelKey]),
783
+ controlId: row.id
784
+ });
785
+ }
786
+ if (this.ctrl.fontStep > 0) {
787
+ chips.push({ id: "font", label: c.fontSize, controlId: "font" });
788
+ }
789
+ if (this.ctrl.textAlign !== "default") {
790
+ chips.push({ id: "align", label: c.textAlign, controlId: "align" });
791
+ }
792
+ if (this.ctrl.lineHeight !== "normal") {
793
+ chips.push({ id: "line", label: c.lineHeight, controlId: "line" });
794
+ }
795
+ if (this.ctrl.letterSpacing !== "0") {
796
+ chips.push({ id: "letter", label: c.letterSpacing, controlId: "letter" });
797
+ }
798
+ if (this.ctrl.wordSpacing !== "0") {
799
+ chips.push({ id: "word", label: c.wordSpacing, controlId: "word" });
800
+ }
801
+ if (this.ctrl.visualFilter !== "none") {
802
+ const f = FILTERS.find((x) => x.value === this.ctrl.visualFilter);
803
+ chips.push({
804
+ id: "filters",
805
+ label: f ? String(c[f.labelKey]) : c.sectionFilters,
806
+ controlId: "filters"
807
+ });
808
+ }
809
+ return chips;
810
+ }
811
+ render() {
812
+ if (!this.rootEl) return;
813
+ const c = this.copy();
814
+ const chips = this.activeChips();
815
+ const badge = chips.length;
816
+ const open = this.ctrl.isOpen;
817
+ const readingMask = this.ctrl.hasEffectiveMode("reading-mask");
818
+ this.rootEl.innerHTML = `
819
+ <svg class="a11y-deuteranopia-svg" aria-hidden="true" focusable="false">
820
+ <filter id="a11y-deuteranopia">
821
+ <feColorMatrix type="matrix" values="0.625 0.375 0 0 0
822
+ 0.7 0.3 0 0 0
823
+ 0 0.3 0.7 0 0
824
+ 0 0 0 1 0"/>
825
+ </filter>
826
+ </svg>
827
+
828
+ <button
829
+ type="button"
830
+ class="a11y-fab"
831
+ data-edge="${this.position}"
832
+ data-stack="${this.stack}"
833
+ style="bottom: ${this.fabBottom()}"
834
+ aria-expanded="${open}"
835
+ aria-controls="${PANEL_ID}"
836
+ aria-label="${escapeAttr(c.open)}"
837
+ data-a11y-action="toggle"
838
+ >
839
+ <span class="a11y-fab__icon">${iconSvg("access", 22)}</span>
840
+ ${badge > 0 ? `<span class="a11y-fab__badge" aria-hidden="true">${badge > 9 ? "9+" : badge}</span>` : ""}
841
+ </button>
842
+
843
+ ${open ? this.renderPanel(c, chips) : ""}
844
+ ${readingMask ? this.renderMask(c) : ""}
845
+ `;
846
+ this.bindEvents();
847
+ if (open) {
848
+ const closeBtn = this.rootEl.querySelector('[data-a11y-action="close"]');
849
+ queueMicrotask(() => closeBtn?.focus());
850
+ }
851
+ }
852
+ renderPanel(c, chips) {
853
+ const profileCount = this.ctrl.activeModeCount("modes");
854
+ return `
855
+ <div class="a11y-layer" role="presentation">
856
+ <div class="a11y-backdrop" data-a11y-action="close" aria-hidden="true"></div>
857
+ <aside
858
+ id="${PANEL_ID}"
859
+ class="a11y-panel a11y-panel--${this.position}"
860
+ role="dialog"
861
+ aria-modal="true"
862
+ aria-label="${escapeAttr(c.panelLabel)}"
863
+ >
864
+ <div class="a11y-panel__header">
865
+ <h2 class="a11y-panel__title">${escapeHtml(c.title)}</h2>
866
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-action="close" aria-label="${escapeAttr(c.close)}">
867
+ ${iconSvg("close", 18)}
868
+ </button>
869
+ </div>
870
+ <div class="a11y-panel__body">
871
+ ${chips.length ? `<section aria-labelledby="a11y-active-heading">
872
+ <h3 id="a11y-active-heading" class="a11y-section-title">${escapeHtml(c.sectionActive)}</h3>
873
+ <div class="a11y-chips">
874
+ ${chips.map(
875
+ (chip) => `
876
+ <button type="button" class="a11y-chip" data-a11y-focus="${escapeAttr(chip.controlId)}" aria-label="${escapeAttr(c.chipJump + ": " + chip.label)}">
877
+ <span>${escapeHtml(chip.label)}</span>
878
+ </button>`
879
+ ).join("")}
880
+ </div>
881
+ </section>` : ""}
882
+
883
+ <section aria-labelledby="a11y-profiles-heading">
884
+ <h3 id="a11y-profiles-heading" class="a11y-section-title">${escapeHtml(c.sectionProfiles)}</h3>
885
+ ${profileCount >= 2 ? `<p class="a11y-hint">${escapeHtml(c.profilesHint)}</p>` : ""}
886
+ <div class="a11y-list">
887
+ ${A11Y_BUNDLES.map((id) => {
888
+ const on = this.ctrl.hasMode(id);
889
+ const meta = c.bundles[id];
890
+ return `
891
+ <div class="a11y-row ${on ? "is-active" : ""}" data-a11y-control="profile-${id}" title="${escapeAttr(meta.desc)}">
892
+ <div class="a11y-row__main">
893
+ <span class="a11y-row__icon">${iconSvg(BUNDLE_ICONS[id], 18)}</span>
894
+ <div>
895
+ <div class="a11y-row__label">${escapeHtml(meta.label)}</div>
896
+ <span class="a11y-sr-only">${escapeHtml(meta.desc)}</span>
897
+ </div>
898
+ </div>
899
+ <button
900
+ type="button"
901
+ class="a11y-switch"
902
+ role="switch"
903
+ aria-checked="${on}"
904
+ aria-label="${escapeAttr(meta.label)}"
905
+ data-a11y-bundle="${id}"
906
+ ></button>
907
+ </div>`;
908
+ }).join("")}
909
+ </div>
910
+ </section>
911
+
912
+ <section aria-labelledby="a11y-adjust-heading">
913
+ <h3 id="a11y-adjust-heading" class="a11y-section-title">${escapeHtml(c.sectionAdjust)}</h3>
914
+ <div class="a11y-divide">
915
+ ${TOGGLE_ROWS.map((row) => {
916
+ const on = this.ctrl.hasEffectiveMode(row.mode);
917
+ const label = String(c[row.labelKey]);
918
+ return `
919
+ <div class="a11y-row" data-a11y-control="${row.id}">
920
+ <div class="a11y-row__main">
921
+ ${iconSvg(row.icon, 18)}
922
+ <span class="a11y-row__label">${escapeHtml(label)}</span>
923
+ </div>
924
+ <button
925
+ type="button"
926
+ class="a11y-switch"
927
+ role="switch"
928
+ aria-checked="${on}"
929
+ aria-label="${escapeAttr(label)}"
930
+ data-a11y-toggle="${row.mode}"
931
+ ></button>
932
+ </div>`;
933
+ }).join("")}
934
+
935
+ ${this.renderSteppers(c)}
936
+
937
+ <div class="a11y-filters" data-a11y-control="filters">
938
+ <div class="a11y-filters__head">
939
+ ${iconSvg("colors", 18)}
940
+ <span>${escapeHtml(c.sectionFilters)}</span>
941
+ </div>
942
+ <div class="a11y-filter-chips" role="group" aria-label="${escapeAttr(c.sectionFilters)}">
943
+ ${FILTERS.map((f) => {
944
+ const active = this.ctrl.visualFilter === f.value;
945
+ const label = String(c[f.labelKey]);
946
+ return `
947
+ <button
948
+ type="button"
949
+ class="a11y-filter-chip ${active ? "is-active" : ""}"
950
+ aria-pressed="${active}"
951
+ data-a11y-filter="${f.value}"
952
+ >
953
+ ${iconSvg(f.icon, 14)}
954
+ ${escapeHtml(label)}
955
+ </button>`;
956
+ }).join("")}
957
+ </div>
958
+ </div>
959
+ </div>
960
+ </section>
961
+ </div>
962
+ <div class="a11y-panel__footer">
963
+ <button type="button" class="a11y-btn a11y-btn--outline" data-a11y-action="reset">
964
+ ${escapeHtml(this.resetArmed ? c.resetConfirm : c.reset)}
965
+ </button>
966
+ </div>
967
+ </aside>
968
+ </div>`;
969
+ }
970
+ renderSteppers(c) {
971
+ const hideLetter = this.ctrl.locale === "ar";
972
+ const fontLabel = c.fontLevels[this.ctrl.fontStep] ?? c.defaultValue;
973
+ const alignLevel = this.ctrl.textAlign === "default" ? 0 : this.ctrl.textAlign === "end" ? 1 : this.ctrl.textAlign === "start" ? 2 : 3;
974
+ const lineLevel = this.ctrl.lineHeight === "normal" ? 0 : this.ctrl.lineHeight === "1.6" ? 1 : this.ctrl.lineHeight === "1.8" ? 2 : 3;
975
+ const letterLevel = this.ctrl.letterSpacing === "0" ? 0 : this.ctrl.letterSpacing === "0.04em" ? 1 : this.ctrl.letterSpacing === "0.08em" ? 2 : 3;
976
+ const wordLevel = this.ctrl.wordSpacing === "0" ? 0 : this.ctrl.wordSpacing === "0.16em" ? 1 : this.ctrl.wordSpacing === "0.32em" ? 2 : 3;
977
+ const steppers = [
978
+ { id: "font", icon: "textFont", label: c.fontSize, value: fontLabel, active: this.ctrl.fontStep > 0 },
979
+ {
980
+ id: "align",
981
+ icon: "textAlign",
982
+ label: c.textAlign,
983
+ value: c.alignLevels[alignLevel],
984
+ active: this.ctrl.textAlign !== "default"
985
+ },
986
+ {
987
+ id: "line",
988
+ icon: "textUnderline",
989
+ label: c.lineHeight,
990
+ value: c.lineLevels[lineLevel],
991
+ active: this.ctrl.lineHeight !== "normal"
992
+ }
993
+ ];
994
+ if (!hideLetter) {
995
+ steppers.push({
996
+ id: "letter",
997
+ icon: "letterSpacing",
998
+ label: c.letterSpacing,
999
+ value: c.letterLevels[letterLevel],
1000
+ active: this.ctrl.letterSpacing !== "0"
1001
+ });
1002
+ }
1003
+ steppers.push({
1004
+ id: "word",
1005
+ icon: "maximize",
1006
+ label: c.wordSpacing,
1007
+ value: c.wordLevels[wordLevel],
1008
+ active: this.ctrl.wordSpacing !== "0"
1009
+ });
1010
+ return steppers.map(
1011
+ (row) => `
1012
+ <div class="a11y-row" data-a11y-control="${row.id}">
1013
+ <div class="a11y-row__main">
1014
+ ${iconSvg(row.icon, 18)}
1015
+ <span class="a11y-row__label">${escapeHtml(row.label)}</span>
1016
+ </div>
1017
+ <div class="a11y-stepper" role="group" aria-label="${escapeAttr(row.label)}">
1018
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-step="${row.id}" data-delta="-1" aria-label="${escapeAttr(c.stepDecrease + ": " + row.label)}">
1019
+ ${iconSvg("minus", 14)}
1020
+ </button>
1021
+ <span class="a11y-stepper__value" aria-live="polite">${escapeHtml(row.value)}</span>
1022
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-step="${row.id}" data-delta="1" aria-label="${escapeAttr(c.stepIncrease + ": " + row.label)}">
1023
+ ${iconSvg("plus", 14)}
1024
+ </button>
1025
+ </div>
1026
+ </div>`
1027
+ ).join("");
1028
+ }
1029
+ renderMask(c) {
1030
+ return `
1031
+ <div class="a11y-mask-overlay" aria-hidden="true">
1032
+ <div class="a11y-mask-shade" style="top:0;height:calc(var(--a11y-mask-y) - var(--a11y-mask-band))"></div>
1033
+ <div class="a11y-mask-shade" style="top:calc(var(--a11y-mask-y) + var(--a11y-mask-band));bottom:0"></div>
1034
+ </div>
1035
+ <div
1036
+ class="a11y-mask-toolbar"
1037
+ role="toolbar"
1038
+ aria-label="${escapeAttr(c.maskToolbar)}"
1039
+ style="top:calc(var(--a11y-mask-y) + var(--a11y-mask-band) + 8px)"
1040
+ >
1041
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-action="mask-smaller" aria-label="${escapeAttr(c.maskSmaller)}">
1042
+ ${iconSvg("minus", 14)}
1043
+ </button>
1044
+ <button type="button" class="a11y-mask-drag" data-a11y-action="mask-drag">${escapeHtml(c.maskDrag)}</button>
1045
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-action="mask-larger" aria-label="${escapeAttr(c.maskLarger)}">
1046
+ ${iconSvg("plus", 14)}
1047
+ </button>
1048
+ <button type="button" class="a11y-btn a11y-btn--subtle" data-a11y-action="mask-close" aria-label="${escapeAttr(c.maskClose)}">
1049
+ ${iconSvg("close", 14)}
1050
+ </button>
1051
+ </div>`;
1052
+ }
1053
+ bindEvents() {
1054
+ if (!this.rootEl) return;
1055
+ this.rootEl.querySelectorAll("[data-a11y-action]").forEach((el) => {
1056
+ el.addEventListener("click", (e) => {
1057
+ const action = el.getAttribute("data-a11y-action");
1058
+ if (action === "toggle") {
1059
+ this.ctrl.toggle(el);
1060
+ } else if (action === "close") {
1061
+ this.ctrl.close();
1062
+ } else if (action === "reset") {
1063
+ this.onReset();
1064
+ } else if (action === "mask-smaller") {
1065
+ this.ctrl.adjustMaskBand(-20);
1066
+ } else if (action === "mask-larger") {
1067
+ this.ctrl.adjustMaskBand(20);
1068
+ } else if (action === "mask-close") {
1069
+ this.ctrl.setMode("reading-mask", false);
1070
+ } else if (action === "mask-drag") {
1071
+ e.preventDefault();
1072
+ this.draggingMask = true;
1073
+ }
1074
+ });
1075
+ });
1076
+ this.rootEl.querySelectorAll("[data-a11y-bundle]").forEach((el) => {
1077
+ el.addEventListener("click", () => {
1078
+ const id = el.getAttribute("data-a11y-bundle");
1079
+ this.ctrl.setMode(id, !this.ctrl.hasMode(id));
1080
+ });
1081
+ });
1082
+ this.rootEl.querySelectorAll("[data-a11y-toggle]").forEach((el) => {
1083
+ el.addEventListener("click", () => {
1084
+ const mode = el.getAttribute("data-a11y-toggle");
1085
+ this.ctrl.setMode(mode, !this.ctrl.hasEffectiveMode(mode));
1086
+ });
1087
+ });
1088
+ this.rootEl.querySelectorAll("[data-a11y-filter]").forEach((el) => {
1089
+ el.addEventListener("click", () => {
1090
+ const filter = el.getAttribute("data-a11y-filter");
1091
+ this.ctrl.setVisualFilter(filter);
1092
+ });
1093
+ });
1094
+ this.rootEl.querySelectorAll("[data-a11y-step]").forEach((el) => {
1095
+ el.addEventListener("click", () => {
1096
+ const id = el.getAttribute("data-a11y-step");
1097
+ const delta = Number(el.getAttribute("data-delta") || "1");
1098
+ if (id === "font") this.ctrl.stepFontStep(delta);
1099
+ else if (id === "align") this.ctrl.stepTextAlign(delta);
1100
+ else if (id === "line") this.ctrl.stepLineHeight(delta);
1101
+ else if (id === "letter") this.ctrl.stepLetterSpacing(delta);
1102
+ else if (id === "word") this.ctrl.stepWordSpacing(delta);
1103
+ });
1104
+ });
1105
+ this.rootEl.querySelectorAll("[data-a11y-focus]").forEach((el) => {
1106
+ el.addEventListener("click", () => {
1107
+ const id = el.getAttribute("data-a11y-focus");
1108
+ const target = this.rootEl?.querySelector(`[data-a11y-control="${id}"]`);
1109
+ target?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1110
+ const focusable = target?.querySelector("button, [tabindex]");
1111
+ focusable?.focus();
1112
+ });
1113
+ });
1114
+ }
1115
+ onReset() {
1116
+ if (!this.resetArmed) {
1117
+ this.resetArmed = true;
1118
+ this.render();
1119
+ if (this.resetTimer) clearTimeout(this.resetTimer);
1120
+ this.resetTimer = setTimeout(() => {
1121
+ this.resetArmed = false;
1122
+ this.render();
1123
+ }, 5e3);
1124
+ return;
1125
+ }
1126
+ this.resetArmed = false;
1127
+ if (this.resetTimer) clearTimeout(this.resetTimer);
1128
+ this.ctrl.reset();
1129
+ this.ctrl.announce(this.copy().announcedReset);
1130
+ }
1131
+ handleKey(e) {
1132
+ if (e.key === "Escape") {
1133
+ if (this.ctrl.isOpen) {
1134
+ e.preventDefault();
1135
+ this.ctrl.close();
1136
+ return;
1137
+ }
1138
+ if (this.ctrl.hasEffectiveMode("reading-mask")) {
1139
+ e.preventDefault();
1140
+ this.ctrl.setMode("reading-mask", false);
1141
+ }
1142
+ return;
1143
+ }
1144
+ if (!this.ctrl.hasEffectiveMode("reading-mask") || this.ctrl.isOpen) return;
1145
+ const vh = window.innerHeight;
1146
+ if (e.key === "ArrowUp") {
1147
+ e.preventDefault();
1148
+ this.ctrl.setMaskY(this.ctrl.maskY - 20);
1149
+ } else if (e.key === "ArrowDown") {
1150
+ e.preventDefault();
1151
+ this.ctrl.setMaskY(this.ctrl.maskY + 20);
1152
+ } else if (e.key === "PageUp") {
1153
+ e.preventDefault();
1154
+ this.ctrl.setMaskY(this.ctrl.maskY - 100);
1155
+ } else if (e.key === "PageDown") {
1156
+ e.preventDefault();
1157
+ this.ctrl.setMaskY(this.ctrl.maskY + 100);
1158
+ } else if (e.key === "Home") {
1159
+ e.preventDefault();
1160
+ this.ctrl.setMaskY(0);
1161
+ } else if (e.key === "End") {
1162
+ e.preventDefault();
1163
+ this.ctrl.setMaskY(vh);
1164
+ }
1165
+ }
1166
+ };
1167
+ function escapeHtml(s) {
1168
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
1169
+ }
1170
+ function escapeAttr(s) {
1171
+ return escapeHtml(s).replaceAll("'", "&#39;");
1172
+ }
1173
+
1174
+ // src/index.ts
1175
+ var instance = null;
1176
+ function ensureStyles() {
1177
+ if (typeof document === "undefined") return;
1178
+ if (document.querySelector("link[data-a11y-styles], style[data-a11y-styles]")) return;
1179
+ }
1180
+ function mount(options = {}) {
1181
+ if (instance) return instance;
1182
+ ensureStyles();
1183
+ const controller = new A11yController({
1184
+ storageKey: options.storageKey,
1185
+ locale: options.locale
1186
+ });
1187
+ let widget = null;
1188
+ if (!options.headless) {
1189
+ widget = new A11yWidget(controller, {
1190
+ position: options.position,
1191
+ stack: options.stack,
1192
+ root: options.root
1193
+ });
1194
+ widget.mount();
1195
+ }
1196
+ instance = {
1197
+ controller,
1198
+ widget,
1199
+ open: () => controller.open(),
1200
+ close: () => controller.close(),
1201
+ toggle: () => controller.toggle(),
1202
+ toggleMode: (mode) => controller.toggleMode(mode),
1203
+ setMode: (mode, enabled) => controller.setMode(mode, enabled),
1204
+ setVisualFilter: (filter) => controller.setVisualFilter(filter),
1205
+ reset: () => controller.reset(),
1206
+ announce: (msg) => controller.announce(msg),
1207
+ toggleLocale: () => controller.toggleLocale(),
1208
+ destroy: () => {
1209
+ widget?.destroy();
1210
+ controller.destroy();
1211
+ instance = null;
1212
+ }
1213
+ };
1214
+ return instance;
1215
+ }
1216
+ function getInstance() {
1217
+ return instance;
1218
+ }
1219
+ function destroy() {
1220
+ instance?.destroy();
1221
+ }
1222
+ var Accessibility = {
1223
+ mount,
1224
+ destroy,
1225
+ getInstance,
1226
+ createSkipLink
1227
+ };
1228
+
1229
+ exports.A11Y_BUNDLES = A11Y_BUNDLES;
1230
+ exports.A11yController = A11yController;
1231
+ exports.A11yWidget = A11yWidget;
1232
+ exports.Accessibility = Accessibility;
1233
+ exports.BUNDLE_PRIMITIVES = BUNDLE_PRIMITIVES;
1234
+ exports.createSkipLink = createSkipLink;
1235
+ exports.destroy = destroy;
1236
+ exports.getCopy = getCopy;
1237
+ exports.getInstance = getInstance;
1238
+ exports.mount = mount;