ngp-accessibility 1.0.3 → 1.0.6

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.esm.js CHANGED
@@ -2,50 +2,165 @@ import React, { createContext, useState, useEffect, useContext } from 'react';
2
2
 
3
3
  const translations = {
4
4
  en: {
5
- increaseText: 'Increase Text',
6
- decreaseText: 'Decrease Text',
7
- highContrast: 'High Contrast',
8
- negativeContrast: 'Negative Contrast',
9
- lightBackground: 'Light Background',
10
- underlineLinks: 'Underline Links',
11
- readableFont: 'Readable Font',
12
- voiceCommand: 'Voice Command',
5
+ language: "Language",
6
+ increaseText: "Increase Text",
7
+ decreaseText: "Decrease Text",
8
+ textMagnifier: "Text Magnifier",
9
+ highContrast: "High Contrast",
10
+ negativeContrast: "Negative Contrast",
11
+ lightBackground: "Light Background",
12
+ underlineLinks: "Underline Links",
13
+ highlightTitles: "Highlight Titles",
14
+ readableFont: "Readable Font",
15
+ pauseAnimations: "Pause Animations",
16
+ readingGuide: "Reading Guide",
17
+ voiceCommand: "Voice Command",
18
+ readAloud: "Read Aloud",
19
+ colorBlindMode: "Color Blind Mode",
20
+ focusIndicator: "Focus Indicator",
13
21
  },
14
22
  tl: {
15
- increaseText: 'Palakihin ang Teksto',
16
- decreaseText: 'Paliitin ang Teksto',
17
- highContrast: 'Mataas na Contrast',
18
- negativeContrast: 'Negatibong Contrast',
19
- lightBackground: 'Maliwanag na Background',
20
- underlineLinks: 'May Guhit na Links',
21
- readableFont: 'Madaling Basahin na Font',
22
- voiceCommand: 'Voice Command',
23
+ language: "Wika",
24
+ increaseText: "Palakihin ang Teksto",
25
+ decreaseText: "Paliitin ang Teksto",
26
+ textMagnifier: "Text Magnifier",
27
+ highContrast: "Mataas na Contrast",
28
+ negativeContrast: "Negatibong Contrast",
29
+ lightBackground: "Maliwanag na Background",
30
+ underlineLinks: "May Guhit na Links",
31
+ highlightTitles: "I-highlight ang Mga Pamagat",
32
+ readableFont: "Madaling Basahin na Font",
33
+ pauseAnimations: "I-pause ang mga Animation",
34
+ readingGuide: "Reading Guide",
35
+ voiceCommand: "Voice Command",
36
+ readAloud: "Basahin nang Malakas",
37
+ colorBlindMode: "Color Blind Mode",
38
+ focusIndicator: "Focus Indicator",
23
39
  },
24
40
  ceb: {
25
- increaseText: 'Padak-on ang Teksto',
26
- decreaseText: 'Pagamay-on ang Teksto',
27
- highContrast: 'Taas nga Contrast',
28
- negativeContrast: 'Negatibo nga Contrast',
29
- lightBackground: 'Hayag nga Background',
30
- underlineLinks: 'Linya sa Ubos sa Links',
31
- readableFont: 'Sayon Basahon nga Font',
32
- voiceCommand: 'Voice Command',
41
+ language: "Pinulongan",
42
+ increaseText: "Padak-on ang Teksto",
43
+ decreaseText: "Pagamay-on ang Teksto",
44
+ textMagnifier: "Text Magnifier",
45
+ highContrast: "Taas nga Contrast",
46
+ negativeContrast: "Negatibo nga Contrast",
47
+ lightBackground: "Hayag nga Background",
48
+ underlineLinks: "Linya sa Ubos sa Links",
49
+ highlightTitles: "I-highlight ang mga Ulohan",
50
+ readableFont: "Sayon Basahon nga Font",
51
+ pauseAnimations: "Ihunong ang mga Animation",
52
+ readingGuide: "Reading Guide",
53
+ voiceCommand: "Voice Command",
54
+ readAloud: "Basaha og Kusog",
55
+ colorBlindMode: "Color Blind Mode",
56
+ focusIndicator: "Focus Indicator",
33
57
  },
34
58
  };
35
59
 
60
+ const GOOGLE_TRANSLATE_COOKIE_NAME = "googtrans";
61
+ const GOOGLE_TRANSLATE_SOURCE_LANGUAGE = "en";
62
+ const getCookieDomains = () => {
63
+ const domains = new Set([window.location.hostname]);
64
+ const hostnameParts = window.location.hostname.split(".");
65
+ if (hostnameParts.length > 2) {
66
+ domains.add(hostnameParts.slice(-2).join("."));
67
+ }
68
+ return Array.from(domains);
69
+ };
70
+ const setGoogleTranslateCookie = (targetLanguage) => {
71
+ const cookieValue = `/${GOOGLE_TRANSLATE_SOURCE_LANGUAGE}/${targetLanguage}`;
72
+ document.cookie = `${GOOGLE_TRANSLATE_COOKIE_NAME}=${cookieValue}; path=/; SameSite=Lax`;
73
+ for (const domain of getCookieDomains()) {
74
+ document.cookie = `${GOOGLE_TRANSLATE_COOKIE_NAME}=${cookieValue}; path=/; domain=${domain}; SameSite=Lax`;
75
+ }
76
+ };
77
+ const clearGoogleTranslateCookie = () => {
78
+ document.cookie = `${GOOGLE_TRANSLATE_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax`;
79
+ for (const domain of getCookieDomains()) {
80
+ document.cookie = `${GOOGLE_TRANSLATE_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; SameSite=Lax`;
81
+ }
82
+ };
83
+ const dispatchNativeChangeEvent = (element) => {
84
+ element.dispatchEvent(new Event("change", { bubbles: true }));
85
+ };
86
+ const normalizeMagnifierText = (value) => value.replace(/\s+/g, " ").trim();
87
+ const magnifierSelector = "p, li, button, h1, h2, h3, h4, h5, h6, [role=heading]";
88
+ const getMagnifierElement = (target) => {
89
+ if (!(target instanceof HTMLElement))
90
+ return null;
91
+ if (target.closest(".a11y-root"))
92
+ return null;
93
+ const element = target.closest(magnifierSelector);
94
+ return element instanceof HTMLElement ? element : null;
95
+ };
96
+ const getMagnifierText = (target) => {
97
+ const element = getMagnifierElement(target);
98
+ if (!element)
99
+ return "";
100
+ const ariaLabel = normalizeMagnifierText(element.getAttribute("aria-label") || "");
101
+ if (ariaLabel)
102
+ return ariaLabel;
103
+ return normalizeMagnifierText(element.innerText || element.textContent || "");
104
+ };
36
105
  const AccessibilityContext = createContext(undefined);
37
- const AccessibilityProvider = ({ children, translations: customTranslations, }) => {
106
+ const AccessibilityProvider = ({ children, translations: customTranslations, googleTranslateTargetSelector, translationTargetSelector, }) => {
107
+ var _a;
38
108
  const [state, setState] = useState({
39
109
  language: "en",
40
110
  textSize: 100,
111
+ textMagnifier: false,
112
+ pauseAnimations: false,
113
+ readingGuide: false,
41
114
  highContrast: false,
42
115
  negativeContrast: false,
43
116
  lightBackground: false,
44
117
  underlineLinks: false,
118
+ highlightTitles: false,
45
119
  readableFont: false,
46
120
  voiceEnabled: false,
121
+ readAloud: false,
122
+ colorBlindMode: "none",
123
+ focusIndicator: false,
47
124
  });
48
125
  const allTranslations = customTranslations || translations;
126
+ const defaultUiTranslations = translations.en;
127
+ const manualTranslationsForLanguage = translations[state.language];
128
+ const hasManualTranslations = Boolean(manualTranslationsForLanguage);
129
+ const resolvedTranslationTargetSelector = (_a = googleTranslateTargetSelector !== null && googleTranslateTargetSelector !== void 0 ? googleTranslateTargetSelector : translationTargetSelector) !== null && _a !== void 0 ? _a : "main";
130
+ useEffect(() => {
131
+ const target = document.querySelector(resolvedTranslationTargetSelector);
132
+ if (!target) {
133
+ console.warn(`[NGP Accessibility] Google translation target selector "${resolvedTranslationTargetSelector}" did not match any element. Falling back to page-wide translation behavior.`);
134
+ return;
135
+ }
136
+ const markedElements = new Set();
137
+ let current = target;
138
+ while (current && current !== document.body) {
139
+ const parentElement = current.parentElement;
140
+ if (!parentElement)
141
+ break;
142
+ for (const sibling of Array.from(parentElement.children)) {
143
+ if (sibling === current || !(sibling instanceof HTMLElement))
144
+ continue;
145
+ if (sibling.id === "a11y-google-translate-element")
146
+ continue;
147
+ sibling.classList.add("notranslate");
148
+ sibling.setAttribute("translate", "no");
149
+ sibling.dataset.a11yTranslationScoped = "true";
150
+ markedElements.add(sibling);
151
+ }
152
+ current = parentElement;
153
+ }
154
+ return () => {
155
+ for (const element of markedElements) {
156
+ if (element.dataset.a11yTranslationScoped !== "true")
157
+ continue;
158
+ element.classList.remove("notranslate");
159
+ element.removeAttribute("translate");
160
+ delete element.dataset.a11yTranslationScoped;
161
+ }
162
+ };
163
+ }, [resolvedTranslationTargetSelector]);
49
164
  useEffect(() => {
50
165
  const root = document.documentElement;
51
166
  root.style.fontSize = `${state.textSize}%`;
@@ -53,8 +168,276 @@ const AccessibilityProvider = ({ children, translations: customTranslations, })
53
168
  root.classList.toggle("negative-contrast", state.negativeContrast);
54
169
  root.classList.toggle("light-background", state.lightBackground);
55
170
  root.classList.toggle("underline-links", state.underlineLinks);
171
+ root.classList.toggle("highlight-titles", state.highlightTitles);
56
172
  root.classList.toggle("readable-font", state.readableFont);
173
+ root.classList.toggle("pause-animations", state.pauseAnimations);
174
+ root.classList.toggle("focus-indicator", state.focusIndicator);
175
+ const body = document.body;
176
+ if (state.colorBlindMode !== "none") {
177
+ body.style.filter = `url(#a11y-${state.colorBlindMode})`;
178
+ }
179
+ else {
180
+ body.style.filter = "";
181
+ }
57
182
  }, [state]);
183
+ useEffect(() => {
184
+ const svgId = "a11y-colorblind-filters";
185
+ if (document.getElementById(svgId))
186
+ return;
187
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
188
+ svg.id = svgId;
189
+ svg.setAttribute("aria-hidden", "true");
190
+ svg.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;";
191
+ svg.innerHTML = `
192
+ <defs>
193
+ <filter id="a11y-protanopia">
194
+ <feColorMatrix type="matrix" values="
195
+ 0.567 0.433 0 0 0
196
+ 0.558 0.442 0 0 0
197
+ 0 0.242 0.758 0 0
198
+ 0 0 0 1 0"/>
199
+ </filter>
200
+ <filter id="a11y-deuteranopia">
201
+ <feColorMatrix type="matrix" values="
202
+ 0.625 0.375 0 0 0
203
+ 0.7 0.3 0 0 0
204
+ 0 0.3 0.7 0 0
205
+ 0 0 0 1 0"/>
206
+ </filter>
207
+ <filter id="a11y-tritanopia">
208
+ <feColorMatrix type="matrix" values="
209
+ 0.95 0.05 0 0 0
210
+ 0 0.433 0.567 0 0
211
+ 0 0.475 0.525 0 0
212
+ 0 0 0 1 0"/>
213
+ </filter>
214
+ </defs>
215
+ `;
216
+ document.body.prepend(svg);
217
+ return () => {
218
+ var _a;
219
+ (_a = document.getElementById(svgId)) === null || _a === void 0 ? void 0 : _a.remove();
220
+ };
221
+ }, []);
222
+ useEffect(() => {
223
+ if (!state.readAloud || !("speechSynthesis" in window))
224
+ return;
225
+ const readableSelector = "p, li, button, a, h1, h2, h3, h4, h5, h6, label, td, th, [role='button'], [role='heading'], [role='link']";
226
+ const speak = (text) => {
227
+ window.speechSynthesis.cancel();
228
+ const utterance = new SpeechSynthesisUtterance(text);
229
+ utterance.lang =
230
+ state.language === "tl"
231
+ ? "tl-PH"
232
+ : state.language === "ceb"
233
+ ? "fil-PH"
234
+ : "en-US";
235
+ utterance.rate = 0.95;
236
+ window.speechSynthesis.speak(utterance);
237
+ };
238
+ const getReadableText = (el) => normalizeMagnifierText(el.getAttribute("aria-label") ||
239
+ el.getAttribute("placeholder") ||
240
+ el.getAttribute("title") ||
241
+ el.innerText ||
242
+ el.textContent ||
243
+ "");
244
+ const handleMouseOver = (event) => {
245
+ const target = event.target;
246
+ if (!target)
247
+ return;
248
+ const el = target.closest(readableSelector);
249
+ if (!el || el.closest(".a11y-root"))
250
+ return;
251
+ const text = getReadableText(el);
252
+ if (text)
253
+ speak(text);
254
+ };
255
+ const handleFocus = (event) => {
256
+ const el = event.target;
257
+ if (!el || el.closest(".a11y-root"))
258
+ return;
259
+ const text = getReadableText(el);
260
+ if (text)
261
+ speak(text);
262
+ };
263
+ document.addEventListener("mouseover", handleMouseOver, true);
264
+ document.addEventListener("focusin", handleFocus, true);
265
+ return () => {
266
+ document.removeEventListener("mouseover", handleMouseOver, true);
267
+ document.removeEventListener("focusin", handleFocus, true);
268
+ window.speechSynthesis.cancel();
269
+ };
270
+ }, [state.readAloud, state.language]);
271
+ useEffect(() => {
272
+ if (!state.readingGuide)
273
+ return;
274
+ const guide = document.createElement("div");
275
+ guide.className = "a11y-reading-guide";
276
+ guide.setAttribute("aria-hidden", "true");
277
+ guide.innerHTML = `
278
+ <div class="a11y-reading-guide-top"></div>
279
+ <div class="a11y-reading-guide-focus"></div>
280
+ <div class="a11y-reading-guide-line"></div>
281
+ <div class="a11y-reading-guide-bottom"></div>
282
+ `;
283
+ document.body.appendChild(guide);
284
+ const topMask = guide.querySelector(".a11y-reading-guide-top");
285
+ const focusBand = guide.querySelector(".a11y-reading-guide-focus");
286
+ const focusLine = guide.querySelector(".a11y-reading-guide-line");
287
+ const bottomMask = guide.querySelector(".a11y-reading-guide-bottom");
288
+ const bandHeight = 72;
289
+ const halfBandHeight = bandHeight / 2;
290
+ const updateGuide = (clientY) => {
291
+ const safeY = Math.min(Math.max(halfBandHeight + 12, clientY), window.innerHeight - halfBandHeight - 12);
292
+ const top = Math.max(0, safeY - halfBandHeight);
293
+ const bottom = Math.min(window.innerHeight, safeY + halfBandHeight);
294
+ topMask.style.height = `${top}px`;
295
+ focusBand.style.top = `${top}px`;
296
+ focusBand.style.height = `${bottom - top}px`;
297
+ focusLine.style.top = `${safeY}px`;
298
+ bottomMask.style.top = `${bottom}px`;
299
+ bottomMask.style.height = `${Math.max(0, window.innerHeight - bottom)}px`;
300
+ };
301
+ const resetGuide = () => updateGuide(window.innerHeight / 2);
302
+ const handleMouseMove = (event) => updateGuide(event.clientY);
303
+ const handleTouchStart = (event) => {
304
+ const touch = event.touches[0];
305
+ if (touch)
306
+ updateGuide(touch.clientY);
307
+ };
308
+ const handleTouchMove = (event) => {
309
+ const touch = event.touches[0];
310
+ if (touch)
311
+ updateGuide(touch.clientY);
312
+ };
313
+ resetGuide();
314
+ document.addEventListener("mousemove", handleMouseMove, true);
315
+ document.addEventListener("touchstart", handleTouchStart, true);
316
+ document.addEventListener("touchmove", handleTouchMove, true);
317
+ window.addEventListener("resize", resetGuide);
318
+ return () => {
319
+ document.removeEventListener("mousemove", handleMouseMove, true);
320
+ document.removeEventListener("touchstart", handleTouchStart, true);
321
+ document.removeEventListener("touchmove", handleTouchMove, true);
322
+ window.removeEventListener("resize", resetGuide);
323
+ guide.remove();
324
+ };
325
+ }, [state.readingGuide]);
326
+ useEffect(() => {
327
+ if (!state.textMagnifier)
328
+ return;
329
+ const tooltip = document.createElement("div");
330
+ tooltip.className = "a11y-text-magnifier-tooltip";
331
+ tooltip.setAttribute("aria-hidden", "true");
332
+ document.body.appendChild(tooltip);
333
+ const hideTooltip = () => {
334
+ tooltip.removeAttribute("data-visible");
335
+ tooltip.textContent = "";
336
+ };
337
+ const showTooltip = (text) => {
338
+ tooltip.textContent = text;
339
+ tooltip.setAttribute("data-visible", "true");
340
+ };
341
+ const updateTooltipPosition = (clientX, clientY) => {
342
+ const tooltipWidth = tooltip.offsetWidth || 320;
343
+ const tooltipHeight = tooltip.offsetHeight || 56;
344
+ const left = Math.min(Math.max(12, clientX + 18), window.innerWidth - tooltipWidth - 12);
345
+ const top = clientY + tooltipHeight + 18 > window.innerHeight
346
+ ? Math.max(12, clientY - tooltipHeight - 18)
347
+ : clientY + 18;
348
+ tooltip.style.left = `${left}px`;
349
+ tooltip.style.top = `${top}px`;
350
+ };
351
+ const handleMouseMove = (event) => {
352
+ const text = getMagnifierText(event.target);
353
+ if (!text) {
354
+ hideTooltip();
355
+ return;
356
+ }
357
+ showTooltip(text);
358
+ updateTooltipPosition(event.clientX, event.clientY);
359
+ };
360
+ let touchTimer = null;
361
+ let activeTouchId = null;
362
+ let activeText = "";
363
+ let pressPoint = null;
364
+ const clearTouchTimer = () => {
365
+ if (touchTimer !== null) {
366
+ window.clearTimeout(touchTimer);
367
+ touchTimer = null;
368
+ }
369
+ };
370
+ const resetTouchState = () => {
371
+ clearTouchTimer();
372
+ activeTouchId = null;
373
+ activeText = "";
374
+ pressPoint = null;
375
+ hideTooltip();
376
+ };
377
+ const getTrackedTouch = (touches) => {
378
+ if (activeTouchId === null)
379
+ return null;
380
+ for (const touch of Array.from(touches)) {
381
+ if (touch.identifier === activeTouchId)
382
+ return touch;
383
+ }
384
+ return null;
385
+ };
386
+ const handleTouchStart = (event) => {
387
+ const touch = event.changedTouches[0];
388
+ if (!touch)
389
+ return;
390
+ const text = getMagnifierText(event.target);
391
+ if (!text) {
392
+ resetTouchState();
393
+ return;
394
+ }
395
+ clearTouchTimer();
396
+ activeTouchId = touch.identifier;
397
+ activeText = text;
398
+ pressPoint = { x: touch.clientX, y: touch.clientY };
399
+ touchTimer = window.setTimeout(() => {
400
+ showTooltip(activeText);
401
+ updateTooltipPosition(touch.clientX, touch.clientY);
402
+ touchTimer = null;
403
+ }, 450);
404
+ };
405
+ const handleTouchMove = (event) => {
406
+ const touch = getTrackedTouch(event.touches);
407
+ if (!touch)
408
+ return;
409
+ if (!tooltip.hasAttribute("data-visible")) {
410
+ if (pressPoint &&
411
+ (Math.abs(touch.clientX - pressPoint.x) > 10 ||
412
+ Math.abs(touch.clientY - pressPoint.y) > 10)) {
413
+ resetTouchState();
414
+ }
415
+ return;
416
+ }
417
+ updateTooltipPosition(touch.clientX, touch.clientY);
418
+ };
419
+ const handleTouchEnd = () => resetTouchState();
420
+ const handleTouchCancel = () => resetTouchState();
421
+ const handlePointerLeave = () => hideTooltip();
422
+ document.addEventListener("mousemove", handleMouseMove, true);
423
+ document.addEventListener("touchstart", handleTouchStart, true);
424
+ document.addEventListener("touchmove", handleTouchMove, true);
425
+ document.addEventListener("touchend", handleTouchEnd, true);
426
+ document.addEventListener("touchcancel", handleTouchCancel, true);
427
+ document.addEventListener("scroll", hideTooltip, true);
428
+ window.addEventListener("blur", handlePointerLeave);
429
+ return () => {
430
+ document.removeEventListener("mousemove", handleMouseMove, true);
431
+ document.removeEventListener("touchstart", handleTouchStart, true);
432
+ document.removeEventListener("touchmove", handleTouchMove, true);
433
+ document.removeEventListener("touchend", handleTouchEnd, true);
434
+ document.removeEventListener("touchcancel", handleTouchCancel, true);
435
+ document.removeEventListener("scroll", hideTooltip, true);
436
+ window.removeEventListener("blur", handlePointerLeave);
437
+ clearTouchTimer();
438
+ tooltip.remove();
439
+ };
440
+ }, [state.textMagnifier]);
58
441
  useEffect(() => {
59
442
  if (!state.voiceEnabled ||
60
443
  !("webkitSpeechRecognition" in window || "SpeechRecognition" in window))
@@ -87,6 +470,15 @@ const AccessibilityProvider = ({ children, translations: customTranslations, })
87
470
  toggleLightBackground();
88
471
  if (transcript.includes("underline") || transcript.includes("guhit"))
89
472
  toggleUnderlineLinks();
473
+ if (transcript.includes("highlight titles") ||
474
+ transcript.includes("title highlight"))
475
+ toggleHighlightTitles();
476
+ if (transcript.includes("pause animations") ||
477
+ transcript.includes("stop animations"))
478
+ togglePauseAnimations();
479
+ if (transcript.includes("reading guide") ||
480
+ transcript.includes("reading line"))
481
+ toggleReadingGuide();
90
482
  if (transcript.includes("readable font") ||
91
483
  transcript.includes("madaling"))
92
484
  toggleReadableFont();
@@ -97,23 +489,108 @@ const AccessibilityProvider = ({ children, translations: customTranslations, })
97
489
  const setLanguage = (lang) => setState((s) => (Object.assign(Object.assign({}, s), { language: lang })));
98
490
  const increaseText = () => setState((s) => (Object.assign(Object.assign({}, s), { textSize: Math.min(s.textSize + 10, 200) })));
99
491
  const decreaseText = () => setState((s) => (Object.assign(Object.assign({}, s), { textSize: Math.max(s.textSize - 10, 80) })));
492
+ const toggleTextMagnifier = () => setState((s) => (Object.assign(Object.assign({}, s), { textMagnifier: !s.textMagnifier })));
493
+ const togglePauseAnimations = () => setState((s) => (Object.assign(Object.assign({}, s), { pauseAnimations: !s.pauseAnimations })));
494
+ const toggleReadingGuide = () => setState((s) => (Object.assign(Object.assign({}, s), { readingGuide: !s.readingGuide })));
100
495
  const toggleHighContrast = () => setState((s) => (Object.assign(Object.assign({}, s), { highContrast: !s.highContrast, negativeContrast: false })));
101
496
  const toggleNegativeContrast = () => setState((s) => (Object.assign(Object.assign({}, s), { negativeContrast: !s.negativeContrast, highContrast: false })));
102
497
  const toggleLightBackground = () => setState((s) => (Object.assign(Object.assign({}, s), { lightBackground: !s.lightBackground })));
103
498
  const toggleUnderlineLinks = () => setState((s) => (Object.assign(Object.assign({}, s), { underlineLinks: !s.underlineLinks })));
499
+ const toggleHighlightTitles = () => setState((s) => (Object.assign(Object.assign({}, s), { highlightTitles: !s.highlightTitles })));
104
500
  const toggleReadableFont = () => setState((s) => (Object.assign(Object.assign({}, s), { readableFont: !s.readableFont })));
105
501
  const toggleVoice = () => setState((s) => (Object.assign(Object.assign({}, s), { voiceEnabled: !s.voiceEnabled })));
106
- const translate = (key) => translations[state.language][key];
107
- const t = (key) => { var _a; return ((_a = allTranslations[state.language]) === null || _a === void 0 ? void 0 : _a[key]) || key; };
502
+ const toggleReadAloud = () => setState((s) => (Object.assign(Object.assign({}, s), { readAloud: !s.readAloud })));
503
+ const setColorBlindMode = (mode) => setState((s) => (Object.assign(Object.assign({}, s), { colorBlindMode: mode })));
504
+ const toggleFocusIndicator = () => setState((s) => (Object.assign(Object.assign({}, s), { focusIndicator: !s.focusIndicator })));
505
+ // Google Translate: auto-translate page content when language changes
506
+ useEffect(() => {
507
+ const GTRANSLATE_SCRIPT_ID = "a11y-google-translate-script";
508
+ const GTRANSLATE_CONTAINER_ID = "a11y-google-translate-element";
509
+ const loadGoogleTranslateScript = () => {
510
+ if (document.getElementById(GTRANSLATE_SCRIPT_ID))
511
+ return;
512
+ // Create hidden container for the widget
513
+ const container = document.createElement("div");
514
+ container.id = GTRANSLATE_CONTAINER_ID;
515
+ container.style.display = "none";
516
+ document.body.appendChild(container);
517
+ // Define the init callback
518
+ window.googleTranslateElementInit = () => {
519
+ new window.google.translate.TranslateElement({
520
+ pageLanguage: "en",
521
+ autoDisplay: false,
522
+ }, GTRANSLATE_CONTAINER_ID);
523
+ };
524
+ const script = document.createElement("script");
525
+ script.id = GTRANSLATE_SCRIPT_ID;
526
+ script.src =
527
+ "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
528
+ script.async = true;
529
+ document.body.appendChild(script);
530
+ };
531
+ const triggerTranslation = (langCode) => {
532
+ setGoogleTranslateCookie(langCode);
533
+ const select = document.querySelector(".goog-te-combo");
534
+ if (select) {
535
+ select.value = langCode;
536
+ dispatchNativeChangeEvent(select);
537
+ }
538
+ };
539
+ const resetTranslation = () => {
540
+ const select = document.querySelector(".goog-te-combo");
541
+ if (select) {
542
+ select.value = GOOGLE_TRANSLATE_SOURCE_LANGUAGE;
543
+ dispatchNativeChangeEvent(select);
544
+ }
545
+ clearGoogleTranslateCookie();
546
+ };
547
+ const langMap = {
548
+ en: GOOGLE_TRANSLATE_SOURCE_LANGUAGE,
549
+ tl: "tl",
550
+ ceb: "ceb",
551
+ ja: "ja",
552
+ es: "es",
553
+ fr: "fr",
554
+ de: "de",
555
+ zh: "zh-CN",
556
+ ar: "ar",
557
+ };
558
+ const googleLang = langMap[state.language] || state.language;
559
+ if (googleLang === GOOGLE_TRANSLATE_SOURCE_LANGUAGE) {
560
+ resetTranslation();
561
+ return;
562
+ }
563
+ loadGoogleTranslateScript();
564
+ setGoogleTranslateCookie(googleLang);
565
+ // Wait for Google Translate widget to be ready, then trigger
566
+ const interval = window.setInterval(() => {
567
+ const select = document.querySelector(".goog-te-combo");
568
+ if (select) {
569
+ window.clearInterval(interval);
570
+ triggerTranslation(googleLang);
571
+ }
572
+ }, 300);
573
+ return () => window.clearInterval(interval);
574
+ }, [state.language]);
575
+ const translate = (key) => (manualTranslationsForLanguage === null || manualTranslationsForLanguage === void 0 ? void 0 : manualTranslationsForLanguage[key]) || defaultUiTranslations[key];
576
+ const t = (key) => { var _a, _b; return ((_a = allTranslations[state.language]) === null || _a === void 0 ? void 0 : _a[key]) || ((_b = allTranslations.en) === null || _b === void 0 ? void 0 : _b[key]) || key; };
108
577
  return (React.createElement(AccessibilityContext.Provider, { value: Object.assign(Object.assign({}, state), { setLanguage,
109
578
  increaseText,
110
579
  decreaseText,
580
+ toggleTextMagnifier,
581
+ togglePauseAnimations,
582
+ toggleReadingGuide,
111
583
  toggleHighContrast,
112
584
  toggleNegativeContrast,
113
585
  toggleLightBackground,
114
586
  toggleUnderlineLinks,
587
+ toggleHighlightTitles,
115
588
  toggleReadableFont,
116
589
  toggleVoice,
590
+ toggleReadAloud,
591
+ setColorBlindMode,
592
+ toggleFocusIndicator,
593
+ hasManualTranslations,
117
594
  translate,
118
595
  t }) }, children));
119
596
  };
@@ -126,8 +603,8 @@ const useAccessibility = () => {
126
603
 
127
604
  const cn$1 = (...parts) => parts.filter(Boolean).join(" ");
128
605
  const AccessibilityToolbar = ({ className, style, classes, }) => {
129
- const { language, setLanguage, increaseText, decreaseText, toggleHighContrast, toggleNegativeContrast, toggleLightBackground, toggleUnderlineLinks, toggleReadableFont, toggleVoice, translate, highContrast, negativeContrast, lightBackground, underlineLinks, readableFont, voiceEnabled, } = useAccessibility();
130
- return (React.createElement("div", { className: cn$1("accessibility-toolbar", classes === null || classes === void 0 ? void 0 : classes.root, className), style: style },
606
+ const { language, setLanguage, increaseText, decreaseText, toggleTextMagnifier, togglePauseAnimations, toggleReadingGuide, toggleHighContrast, toggleNegativeContrast, toggleLightBackground, toggleUnderlineLinks, toggleHighlightTitles, toggleReadableFont, toggleVoice, toggleReadAloud, setColorBlindMode, toggleFocusIndicator, translate, textMagnifier, pauseAnimations, readingGuide, highContrast, negativeContrast, lightBackground, underlineLinks, highlightTitles, readableFont, voiceEnabled, readAloud, colorBlindMode, focusIndicator, } = useAccessibility();
607
+ return (React.createElement("div", { className: cn$1("accessibility-toolbar notranslate", classes === null || classes === void 0 ? void 0 : classes.root, className), style: style, translate: "no" },
131
608
  React.createElement("select", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.select), value: language, onChange: (e) => setLanguage(e.target.value) },
132
609
  React.createElement("option", { value: "en" }, "English"),
133
610
  React.createElement("option", { value: "tl" }, "Tagalog"),
@@ -138,50 +615,88 @@ const AccessibilityToolbar = ({ className, style, classes, }) => {
138
615
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button), onClick: decreaseText },
139
616
  "- ",
140
617
  translate("decreaseText")),
618
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, textMagnifier && "active", textMagnifier && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": textMagnifier, onClick: toggleTextMagnifier }, translate("textMagnifier")),
141
619
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, highContrast && "active", highContrast && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": highContrast, onClick: toggleHighContrast }, translate("highContrast")),
142
620
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, negativeContrast && "active", negativeContrast && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": negativeContrast, onClick: toggleNegativeContrast }, translate("negativeContrast")),
143
621
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, lightBackground && "active", lightBackground && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": lightBackground, onClick: toggleLightBackground }, translate("lightBackground")),
144
622
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, underlineLinks && "active", underlineLinks && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": underlineLinks, onClick: toggleUnderlineLinks }, translate("underlineLinks")),
623
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, highlightTitles && "active", highlightTitles && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": highlightTitles, onClick: toggleHighlightTitles }, translate("highlightTitles")),
145
624
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, readableFont && "active", readableFont && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": readableFont, onClick: toggleReadableFont }, translate("readableFont")),
625
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, pauseAnimations && "active", pauseAnimations && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": pauseAnimations, onClick: togglePauseAnimations }, translate("pauseAnimations")),
626
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, readingGuide && "active", readingGuide && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": readingGuide, onClick: toggleReadingGuide }, translate("readingGuide")),
146
627
  React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, classes === null || classes === void 0 ? void 0 : classes.voiceButton, voiceEnabled && "active", voiceEnabled && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": voiceEnabled, onClick: toggleVoice },
147
628
  "\uD83C\uDFA4 ",
148
- translate("voiceCommand"))));
629
+ translate("voiceCommand")),
630
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, readAloud && "active", readAloud && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": readAloud, onClick: toggleReadAloud },
631
+ "\uD83D\uDD0A ",
632
+ translate("readAloud")),
633
+ React.createElement("button", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.button, focusIndicator && "active", focusIndicator && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": focusIndicator, onClick: toggleFocusIndicator }, translate("focusIndicator")),
634
+ React.createElement("select", { className: cn$1(classes === null || classes === void 0 ? void 0 : classes.select), value: colorBlindMode, onChange: (e) => setColorBlindMode(e.target.value), "aria-label": translate("colorBlindMode") },
635
+ React.createElement("option", { value: "none" }, translate("colorBlindMode")),
636
+ React.createElement("option", { value: "protanopia" }, "Protanopia (Red)"),
637
+ React.createElement("option", { value: "deuteranopia" }, "Deuteranopia (Green)"),
638
+ React.createElement("option", { value: "tritanopia" }, "Tritanopia (Blue)"))));
149
639
  };
150
640
 
151
641
  const cn = (...parts) => parts.filter(Boolean).join(" ");
152
642
  const AccessibilityDropdown = ({ className, style, classes, triggerLabel = "♿ Accessibility", renderTrigger, }) => {
153
643
  const [isOpen, setIsOpen] = useState(false);
154
- const { language, setLanguage, increaseText, decreaseText, toggleHighContrast, toggleNegativeContrast, toggleLightBackground, toggleUnderlineLinks, toggleReadableFont, toggleVoice, translate, highContrast, negativeContrast, lightBackground, underlineLinks, readableFont, voiceEnabled, } = useAccessibility();
644
+ const { language, setLanguage, textSize, increaseText, decreaseText, toggleTextMagnifier, togglePauseAnimations, toggleReadingGuide, toggleHighContrast, toggleNegativeContrast, toggleLightBackground, toggleUnderlineLinks, toggleHighlightTitles, toggleReadableFont, toggleVoice, translate, textMagnifier, pauseAnimations, readingGuide, highContrast, negativeContrast, lightBackground, underlineLinks, highlightTitles, readableFont, voiceEnabled, } = useAccessibility();
155
645
  const toggle = () => setIsOpen(!isOpen);
156
- return (React.createElement("div", { className: cn("accessibility-dropdown", classes === null || classes === void 0 ? void 0 : classes.root, className), style: style },
646
+ const closePanel = () => setIsOpen(false);
647
+ const textScaleDelta = textSize - 100;
648
+ const textScaleLabel = textScaleDelta === 0
649
+ ? "Default"
650
+ : `${textScaleDelta > 0 ? "+" : ""}${textScaleDelta}%`;
651
+ return (React.createElement("div", { className: cn("a11y-root", classes === null || classes === void 0 ? void 0 : classes.root, className), style: style },
157
652
  renderTrigger ? (renderTrigger({
158
653
  isOpen,
159
654
  toggle,
160
- className: cn("accessibility-dropdown-toggle", classes === null || classes === void 0 ? void 0 : classes.toggle),
161
- })) : (React.createElement("button", { className: cn("accessibility-dropdown-toggle", classes === null || classes === void 0 ? void 0 : classes.toggle), onClick: toggle }, triggerLabel)),
162
- isOpen && (React.createElement("div", { className: cn("accessibility-dropdown-menu", classes === null || classes === void 0 ? void 0 : classes.menu) },
163
- React.createElement("div", { className: cn("accessibility-dropdown-section", classes === null || classes === void 0 ? void 0 : classes.section) },
164
- React.createElement("label", { className: cn(classes === null || classes === void 0 ? void 0 : classes.label) }, "Language:"),
165
- React.createElement("select", { className: cn(classes === null || classes === void 0 ? void 0 : classes.select), value: language, onChange: (e) => setLanguage(e.target.value) },
655
+ className: cn("a11y-trigger", classes === null || classes === void 0 ? void 0 : classes.trigger),
656
+ })) : (React.createElement("button", { className: cn("a11y-trigger", classes === null || classes === void 0 ? void 0 : classes.trigger), onClick: toggle }, triggerLabel)),
657
+ isOpen && (React.createElement("div", { className: cn("a11y-panel", classes === null || classes === void 0 ? void 0 : classes.panel) },
658
+ React.createElement("div", { className: cn("a11y-panel-header", classes === null || classes === void 0 ? void 0 : classes.panelHeader) },
659
+ React.createElement("div", { className: "a11y-panel-heading" },
660
+ React.createElement("span", { className: "a11y-panel-eyebrow" }, "NGP"),
661
+ React.createElement("label", { className: cn("a11y-panel-title", classes === null || classes === void 0 ? void 0 : classes.title) }, "Accessibility Options")),
662
+ React.createElement("button", { type: "button", className: cn("a11y-panel-close", classes === null || classes === void 0 ? void 0 : classes.closeButton), onClick: closePanel, "aria-label": "Close accessibility panel" }, "\u00D7")),
663
+ React.createElement("div", { className: cn("a11y-language", classes === null || classes === void 0 ? void 0 : classes.section) },
664
+ React.createElement("label", { htmlFor: "a11y-language-select", className: cn("a11y-language-label", classes === null || classes === void 0 ? void 0 : classes.title) }, translate("language")),
665
+ React.createElement("select", { id: "a11y-language-select", value: language, onChange: (e) => setLanguage(e.target.value) },
166
666
  React.createElement("option", { value: "en" }, "English"),
167
667
  React.createElement("option", { value: "tl" }, "Tagalog"),
168
- React.createElement("option", { value: "ceb" }, "Cebuano"))),
169
- React.createElement("div", { className: cn("accessibility-dropdown-section", classes === null || classes === void 0 ? void 0 : classes.section) },
170
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button), onClick: increaseText },
171
- "+ ",
172
- translate("increaseText")),
173
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button), onClick: decreaseText },
174
- "- ",
175
- translate("decreaseText"))),
176
- React.createElement("div", { className: cn("accessibility-dropdown-section", classes === null || classes === void 0 ? void 0 : classes.section) },
177
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, highContrast && "active", highContrast && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": highContrast, onClick: toggleHighContrast }, translate("highContrast")),
178
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, negativeContrast && "active", negativeContrast && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": negativeContrast, onClick: toggleNegativeContrast }, translate("negativeContrast")),
179
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, lightBackground && "active", lightBackground && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": lightBackground, onClick: toggleLightBackground }, translate("lightBackground")),
180
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, underlineLinks && "active", underlineLinks && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": underlineLinks, onClick: toggleUnderlineLinks }, translate("underlineLinks")),
181
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, readableFont && "active", readableFont && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": readableFont, onClick: toggleReadableFont }, translate("readableFont")),
182
- React.createElement("button", { className: cn(classes === null || classes === void 0 ? void 0 : classes.button, classes === null || classes === void 0 ? void 0 : classes.voiceButton, voiceEnabled && "active", voiceEnabled && (classes === null || classes === void 0 ? void 0 : classes.activeButton)), "data-active": voiceEnabled, onClick: toggleVoice },
183
- "\uD83C\uDFA4 ",
184
- translate("voiceCommand")))))));
668
+ React.createElement("option", { value: "ceb" }, "Cebuano"),
669
+ React.createElement("option", { value: "ja" }, "Japanese"),
670
+ React.createElement("option", { value: "es" }, "Spanish"),
671
+ React.createElement("option", { value: "fr" }, "French"),
672
+ React.createElement("option", { value: "de" }, "German"),
673
+ React.createElement("option", { value: "zh" }, "Chinese"),
674
+ React.createElement("option", { value: "ar" }, "Arabic"))),
675
+ React.createElement("section", { className: cn("a11y-section", classes === null || classes === void 0 ? void 0 : classes.section) },
676
+ React.createElement("h3", { className: cn("a11y-section-title", classes === null || classes === void 0 ? void 0 : classes.title) }, "Content Adjustments"),
677
+ React.createElement("div", { className: "a11y-card" },
678
+ React.createElement("span", { className: cn("a11y-card-title", classes === null || classes === void 0 ? void 0 : classes.title) }, "Content Scaling"),
679
+ React.createElement("div", { className: "a11y-stepper" },
680
+ React.createElement("button", { onClick: decreaseText }, "\u2212"),
681
+ React.createElement("span", { className: "a11y-stepper-value" }, textScaleLabel),
682
+ React.createElement("button", { onClick: increaseText }, "+"))),
683
+ React.createElement("div", { className: cn("a11y-grid", classes === null || classes === void 0 ? void 0 : classes.section) },
684
+ React.createElement("button", { className: cn("a11y-card", textMagnifier && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleTextMagnifier }, translate("textMagnifier")),
685
+ React.createElement("button", { className: cn("a11y-card", readableFont && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleReadableFont }, "Readable Font"),
686
+ React.createElement("button", { className: cn("a11y-card", underlineLinks && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleUnderlineLinks }, "Highlight Links"),
687
+ React.createElement("button", { className: cn("a11y-card", highlightTitles && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleHighlightTitles }, "Highlight Titles"))),
688
+ React.createElement("section", { className: cn("a11y-section", classes === null || classes === void 0 ? void 0 : classes.section) },
689
+ React.createElement("h3", { className: cn("a11y-section-title", classes === null || classes === void 0 ? void 0 : classes.title) }, "Color Adjustments"),
690
+ React.createElement("div", { className: cn("a11y-grid", classes === null || classes === void 0 ? void 0 : classes.section) },
691
+ React.createElement("button", { className: cn("a11y-card", highContrast && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleHighContrast }, translate("highContrast")),
692
+ React.createElement("button", { className: cn("a11y-card", negativeContrast && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleNegativeContrast }, translate("negativeContrast")),
693
+ React.createElement("button", { className: cn("a11y-card", lightBackground && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleLightBackground }, translate("lightBackground")))),
694
+ React.createElement("section", { className: cn("a11y-section", classes === null || classes === void 0 ? void 0 : classes.section) },
695
+ React.createElement("h3", { className: cn("a11y-section-title", classes === null || classes === void 0 ? void 0 : classes.title) }, "Accessibility Tools"),
696
+ React.createElement("div", { className: cn("a11y-grid", classes === null || classes === void 0 ? void 0 : classes.section) },
697
+ React.createElement("button", { className: cn("a11y-card", pauseAnimations && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: togglePauseAnimations }, translate("pauseAnimations")),
698
+ React.createElement("button", { className: cn("a11y-card", readingGuide && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleReadingGuide }, translate("readingGuide")),
699
+ React.createElement("button", { className: cn("a11y-card", voiceEnabled && "active", classes === null || classes === void 0 ? void 0 : classes.button), onClick: toggleVoice }, translate("voiceCommand"))))))));
185
700
  };
186
701
 
187
702
  const T = ({ k, children }) => {