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