@bauer-group/accessibility-widget 1.0.3

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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/accessibility-widget-core.min.js +6 -0
  4. package/dist/accessibility-widget-core.min.js.map +7 -0
  5. package/dist/accessibility-widget-loader.min.js +14 -0
  6. package/dist/accessibility-widget-loader.min.js.map +7 -0
  7. package/dist/accessibility-widget.min.css +7 -0
  8. package/dist/accessibility-widget.min.css.map +7 -0
  9. package/dist/config.d.ts +33 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/core.d.ts +31 -0
  12. package/dist/core.d.ts.map +1 -0
  13. package/dist/features/apply.d.ts +3 -0
  14. package/dist/features/apply.d.ts.map +1 -0
  15. package/dist/features/profile.d.ts +3 -0
  16. package/dist/features/profile.d.ts.map +1 -0
  17. package/dist/features/reading-guide.d.ts +2 -0
  18. package/dist/features/reading-guide.d.ts.map +1 -0
  19. package/dist/features/reading-mask.d.ts +2 -0
  20. package/dist/features/reading-mask.d.ts.map +1 -0
  21. package/dist/features/structure-nav.d.ts +2 -0
  22. package/dist/features/structure-nav.d.ts.map +1 -0
  23. package/dist/features/tts.d.ts +7 -0
  24. package/dist/features/tts.d.ts.map +1 -0
  25. package/dist/focus-trap.d.ts +6 -0
  26. package/dist/focus-trap.d.ts.map +1 -0
  27. package/dist/i18n/ar.d.ts +3 -0
  28. package/dist/i18n/ar.d.ts.map +1 -0
  29. package/dist/i18n/bn.d.ts +3 -0
  30. package/dist/i18n/bn.d.ts.map +1 -0
  31. package/dist/i18n/cs.d.ts +3 -0
  32. package/dist/i18n/cs.d.ts.map +1 -0
  33. package/dist/i18n/de.d.ts +3 -0
  34. package/dist/i18n/de.d.ts.map +1 -0
  35. package/dist/i18n/el.d.ts +3 -0
  36. package/dist/i18n/el.d.ts.map +1 -0
  37. package/dist/i18n/en.d.ts +3 -0
  38. package/dist/i18n/en.d.ts.map +1 -0
  39. package/dist/i18n/es.d.ts +3 -0
  40. package/dist/i18n/es.d.ts.map +1 -0
  41. package/dist/i18n/fa.d.ts +3 -0
  42. package/dist/i18n/fa.d.ts.map +1 -0
  43. package/dist/i18n/fr.d.ts +3 -0
  44. package/dist/i18n/fr.d.ts.map +1 -0
  45. package/dist/i18n/he.d.ts +3 -0
  46. package/dist/i18n/he.d.ts.map +1 -0
  47. package/dist/i18n/hi.d.ts +3 -0
  48. package/dist/i18n/hi.d.ts.map +1 -0
  49. package/dist/i18n/hu.d.ts +3 -0
  50. package/dist/i18n/hu.d.ts.map +1 -0
  51. package/dist/i18n/id.d.ts +3 -0
  52. package/dist/i18n/id.d.ts.map +1 -0
  53. package/dist/i18n/index.d.ts +6 -0
  54. package/dist/i18n/index.d.ts.map +1 -0
  55. package/dist/i18n/it.d.ts +3 -0
  56. package/dist/i18n/it.d.ts.map +1 -0
  57. package/dist/i18n/ja.d.ts +3 -0
  58. package/dist/i18n/ja.d.ts.map +1 -0
  59. package/dist/i18n/ko.d.ts +3 -0
  60. package/dist/i18n/ko.d.ts.map +1 -0
  61. package/dist/i18n/nl.d.ts +3 -0
  62. package/dist/i18n/nl.d.ts.map +1 -0
  63. package/dist/i18n/pl.d.ts +3 -0
  64. package/dist/i18n/pl.d.ts.map +1 -0
  65. package/dist/i18n/pt.d.ts +3 -0
  66. package/dist/i18n/pt.d.ts.map +1 -0
  67. package/dist/i18n/ro.d.ts +3 -0
  68. package/dist/i18n/ro.d.ts.map +1 -0
  69. package/dist/i18n/ru.d.ts +3 -0
  70. package/dist/i18n/ru.d.ts.map +1 -0
  71. package/dist/i18n/sv.d.ts +3 -0
  72. package/dist/i18n/sv.d.ts.map +1 -0
  73. package/dist/i18n/th.d.ts +3 -0
  74. package/dist/i18n/th.d.ts.map +1 -0
  75. package/dist/i18n/tr.d.ts +3 -0
  76. package/dist/i18n/tr.d.ts.map +1 -0
  77. package/dist/i18n/types.d.ts +44 -0
  78. package/dist/i18n/types.d.ts.map +1 -0
  79. package/dist/i18n/uk.d.ts +3 -0
  80. package/dist/i18n/uk.d.ts.map +1 -0
  81. package/dist/i18n/ur.d.ts +3 -0
  82. package/dist/i18n/ur.d.ts.map +1 -0
  83. package/dist/i18n/vi.d.ts +3 -0
  84. package/dist/i18n/vi.d.ts.map +1 -0
  85. package/dist/i18n/zh.d.ts +3 -0
  86. package/dist/i18n/zh.d.ts.map +1 -0
  87. package/dist/index.d.ts +10 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/integrity.json +9 -0
  90. package/dist/integrity.txt +12 -0
  91. package/dist/loader.d.ts +2 -0
  92. package/dist/loader.d.ts.map +1 -0
  93. package/dist/panel/drag.d.ts +34 -0
  94. package/dist/panel/drag.d.ts.map +1 -0
  95. package/dist/panel/panel.d.ts +23 -0
  96. package/dist/panel/panel.d.ts.map +1 -0
  97. package/dist/state.d.ts +18 -0
  98. package/dist/state.d.ts.map +1 -0
  99. package/dist/styles/critical.d.ts +16 -0
  100. package/dist/styles/critical.d.ts.map +1 -0
  101. package/dist/types/index.d.ts +3 -0
  102. package/dist/types/index.d.ts.map +1 -0
  103. package/dist/types/locale.d.ts +11 -0
  104. package/dist/types/locale.d.ts.map +1 -0
  105. package/dist/types/widget.d.ts +207 -0
  106. package/dist/types/widget.d.ts.map +1 -0
  107. package/dist/util/debug.d.ts +8 -0
  108. package/dist/util/debug.d.ts.map +1 -0
  109. package/dist/util/dom.d.ts +19 -0
  110. package/dist/util/dom.d.ts.map +1 -0
  111. package/dist/util/events.d.ts +38 -0
  112. package/dist/util/events.d.ts.map +1 -0
  113. package/dist/util/feature-icons.d.ts +33 -0
  114. package/dist/util/feature-icons.d.ts.map +1 -0
  115. package/dist/util/language-names.d.ts +12 -0
  116. package/dist/util/language-names.d.ts.map +1 -0
  117. package/dist/util/svg.d.ts +38 -0
  118. package/dist/util/svg.d.ts.map +1 -0
  119. package/package.json +67 -0
  120. package/src/config.ts +213 -0
  121. package/src/core.ts +173 -0
  122. package/src/features/apply.ts +37 -0
  123. package/src/features/profile.ts +18 -0
  124. package/src/features/reading-guide.ts +25 -0
  125. package/src/features/reading-mask.ts +25 -0
  126. package/src/features/structure-nav.ts +43 -0
  127. package/src/features/tts.ts +73 -0
  128. package/src/focus-trap.ts +35 -0
  129. package/src/globals.d.ts +63 -0
  130. package/src/i18n/ar.ts +48 -0
  131. package/src/i18n/bn.ts +48 -0
  132. package/src/i18n/cs.ts +48 -0
  133. package/src/i18n/de.ts +65 -0
  134. package/src/i18n/el.ts +48 -0
  135. package/src/i18n/en.ts +65 -0
  136. package/src/i18n/es.ts +48 -0
  137. package/src/i18n/fa.ts +48 -0
  138. package/src/i18n/fr.ts +48 -0
  139. package/src/i18n/he.ts +48 -0
  140. package/src/i18n/hi.ts +48 -0
  141. package/src/i18n/hu.ts +48 -0
  142. package/src/i18n/id.ts +48 -0
  143. package/src/i18n/index.ts +70 -0
  144. package/src/i18n/it.ts +48 -0
  145. package/src/i18n/ja.ts +48 -0
  146. package/src/i18n/ko.ts +48 -0
  147. package/src/i18n/nl.ts +48 -0
  148. package/src/i18n/pl.ts +48 -0
  149. package/src/i18n/pt.ts +48 -0
  150. package/src/i18n/ro.ts +48 -0
  151. package/src/i18n/ru.ts +48 -0
  152. package/src/i18n/sv.ts +48 -0
  153. package/src/i18n/th.ts +48 -0
  154. package/src/i18n/tr.ts +48 -0
  155. package/src/i18n/types.ts +36 -0
  156. package/src/i18n/uk.ts +48 -0
  157. package/src/i18n/ur.ts +48 -0
  158. package/src/i18n/vi.ts +48 -0
  159. package/src/i18n/zh.ts +48 -0
  160. package/src/index.ts +9 -0
  161. package/src/loader.ts +533 -0
  162. package/src/panel/drag.ts +210 -0
  163. package/src/panel/panel.ts +617 -0
  164. package/src/state.ts +91 -0
  165. package/src/styles/critical.ts +56 -0
  166. package/src/styles/widget.css +739 -0
  167. package/src/types/index.ts +2 -0
  168. package/src/types/locale.ts +55 -0
  169. package/src/types/widget.ts +300 -0
  170. package/src/util/debug.ts +12 -0
  171. package/src/util/dom.ts +68 -0
  172. package/src/util/events.ts +54 -0
  173. package/src/util/feature-icons.ts +163 -0
  174. package/src/util/language-names.ts +41 -0
  175. package/src/util/svg.ts +93 -0
@@ -0,0 +1,55 @@
1
+ export const SUPPORTED_LOCALES = [
2
+ // Original set (≥ 8M speakers, EU focus)
3
+ 'de',
4
+ 'en',
5
+ 'fr',
6
+ 'es',
7
+ 'it',
8
+ 'pl',
9
+ 'tr',
10
+ 'ar',
11
+ // Added: all remaining languages with ≥ 8M speakers whose scripts render
12
+ // reliably with standard system fonts on current browsers.
13
+ 'zh', // Mandarin Chinese (Simplified)
14
+ 'hi', // Hindi
15
+ 'pt', // Portuguese
16
+ 'bn', // Bengali
17
+ 'ru', // Russian
18
+ 'ja', // Japanese
19
+ 'ko', // Korean
20
+ 'vi', // Vietnamese
21
+ 'fa', // Persian / Farsi (RTL)
22
+ 'ur', // Urdu (RTL)
23
+ 'th', // Thai
24
+ 'id', // Indonesian (covers Malay speakers via Bahasa)
25
+ 'he', // Hebrew (RTL)
26
+ 'nl', // Dutch
27
+ 'sv', // Swedish
28
+ 'cs', // Czech
29
+ 'el', // Greek
30
+ 'hu', // Hungarian
31
+ 'ro', // Romanian
32
+ 'uk', // Ukrainian
33
+ ] as const;
34
+
35
+ export type Locale = (typeof SUPPORTED_LOCALES)[number];
36
+
37
+ /**
38
+ * Right-to-left locales. Used by the panel renderer to set `dir="rtl"`.
39
+ * Keep in sync with the list of locales we ship translations for.
40
+ */
41
+ export const RTL_LOCALES: readonly Locale[] = ['ar', 'fa', 'ur', 'he'];
42
+
43
+ export function isRtl(locale: Locale): boolean {
44
+ return (RTL_LOCALES as readonly string[]).includes(locale);
45
+ }
46
+
47
+ export function isLocale(value: unknown): value is Locale {
48
+ return typeof value === 'string' && (SUPPORTED_LOCALES as readonly string[]).includes(value);
49
+ }
50
+
51
+ export function normalizeLocale(input: string | undefined | null, fallback: Locale = 'de'): Locale {
52
+ if (!input) return fallback;
53
+ const prefix = input.toLowerCase().split(/[-_]/)[0];
54
+ return prefix && isLocale(prefix) ? prefix : fallback;
55
+ }
@@ -0,0 +1,300 @@
1
+ import type { Locale } from './locale.js';
2
+
3
+ export const FEATURE_IDS = [
4
+ 'fontSize',
5
+ 'lineHeight',
6
+ 'letterSpacing',
7
+ 'contrast',
8
+ 'grayscale',
9
+ 'invertColors',
10
+ 'dyslexiaFont',
11
+ 'highlightLinks',
12
+ 'pauseAnimations',
13
+ 'bigCursor',
14
+ 'focusOutline',
15
+ 'readingMask',
16
+ 'readingGuide',
17
+ 'tts',
18
+ 'structureNav',
19
+ ] as const;
20
+
21
+ export type FeatureId = (typeof FEATURE_IDS)[number];
22
+
23
+ export const CONTRAST_MODES = ['off', 'high', 'dark', 'invert'] as const;
24
+ export type ContrastMode = (typeof CONTRAST_MODES)[number];
25
+
26
+ export const PROFILE_IDS = [
27
+ 'visionImpaired',
28
+ 'motor',
29
+ 'cognitive',
30
+ 'seizureSafe',
31
+ 'adhd',
32
+ 'blind',
33
+ ] as const;
34
+ export type ProfileId = (typeof PROFILE_IDS)[number];
35
+
36
+ export const POSITIONS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'] as const;
37
+ export type Position = (typeof POSITIONS)[number];
38
+
39
+ export interface WidgetState {
40
+ features: Record<FeatureId, boolean>;
41
+ fontSizeLevel: number;
42
+ lineHeightLevel: number;
43
+ letterSpacingLevel: number;
44
+ contrastMode: ContrastMode;
45
+ /** Enlarges panel controls & tap targets — independent from font-size feature. */
46
+ oversized?: boolean;
47
+ /**
48
+ * Custom FAB position set by user drag (only populated when `draggableFab`
49
+ * is enabled and the user actually moved the button). Coordinates are
50
+ * viewport-pixel distances from the **top-left** corner. When absent, the
51
+ * FAB uses the configured `position` + `offset` anchor.
52
+ */
53
+ fabPosition?: { x: number; y: number } | null;
54
+ /**
55
+ * User-chosen locale override. When set, wins over `config.locale`
56
+ * on the next panel open and gets applied on reload by the loader.
57
+ * Populated by the in-panel language dropdown and the public
58
+ * `setLocale()` runtime API.
59
+ */
60
+ locale?: string;
61
+ }
62
+
63
+ /**
64
+ * Host-supplied configuration for the widget loader.
65
+ *
66
+ * Set via `window.AccessibilityWidgetConfig = { … }` **before** the loader
67
+ * script executes. All fields are optional — the widget ships with sensible
68
+ * defaults. Invalid values are coerced to defaults; with `debug: true` they
69
+ * emit console warnings so mis-configurations are discoverable.
70
+ *
71
+ * @example Minimal override
72
+ * ```ts
73
+ * window.AccessibilityWidgetConfig = { locale: 'de', primaryColor: '#0058a3' };
74
+ * ```
75
+ *
76
+ * @example Enterprise deployment
77
+ * ```ts
78
+ * window.AccessibilityWidgetConfig = {
79
+ * corePath: '/assets/accessibility-widget-core.min.js',
80
+ * cssPath: '/assets/accessibility-widget.min.css',
81
+ * coreIntegrity: 'sha384-…',
82
+ * cssIntegrity: 'sha384-…',
83
+ * position: 'bottom-left',
84
+ * offset: { x: 24, y: 96 }, // clear the chat widget
85
+ * zIndex: 9999,
86
+ * primaryColor: '#0058a3',
87
+ * storageKey: 'mycorp-a11y',
88
+ * statementUrl: '/accessibility-statement',
89
+ * disabledFeatures: ['tts'],
90
+ * initialFeatures: { focusOutline: true },
91
+ * debug: false,
92
+ * };
93
+ * ```
94
+ */
95
+ export interface WidgetConfig {
96
+ // ─── asset loading ────────────────────────────────────────────────
97
+ /** URL to the on-demand core bundle. Default: `/accessibility-widget/accessibility-widget-core.min.js`. */
98
+ corePath?: string;
99
+ /** URL to the widget stylesheet. Default: `/accessibility-widget/accessibility-widget.min.css`. */
100
+ cssPath?: string;
101
+ /** SRI hash for the core bundle (`sha384-…`). Matches the value printed in `dist/integrity.txt`. */
102
+ coreIntegrity?: string | null;
103
+ /** SRI hash for the stylesheet. Matches the value printed in `dist/integrity.txt`. */
104
+ cssIntegrity?: string | null;
105
+
106
+ // ─── localization ─────────────────────────────────────────────────
107
+ /**
108
+ * Widget locale. `'auto'` (default) picks from `document.documentElement.lang`
109
+ * or `navigator.language`, falling back to `de`. Set explicitly to override.
110
+ */
111
+ locale?: Locale | 'auto';
112
+
113
+ // ─── UI / branding ────────────────────────────────────────────────
114
+ /** FAB anchor corner. Default: `bottom-right`. */
115
+ position?: Position;
116
+ /**
117
+ * Pixel offset of the FAB from its anchor corner.
118
+ * Useful when other fixed elements (chat widgets, cookie banners) would collide.
119
+ * Default: `{ x: 20, y: 20 }`.
120
+ */
121
+ offset?: { x?: number; y?: number };
122
+ /**
123
+ * Override the FAB z-index. Default: `2147483646` (one below max int32).
124
+ * Lower values allow in-page dialogs to stack on top of the FAB.
125
+ */
126
+ zIndex?: number;
127
+ /** FAB background color. Default: `#0058a3` (BAUER GROUP blue). Must be a valid CSS color. */
128
+ primaryColor?: string;
129
+ /** Override the FAB `aria-label`. `null` → use the localized default for the active locale. */
130
+ buttonLabel?: string | null;
131
+
132
+ // ─── persistence ──────────────────────────────────────────────────
133
+ /**
134
+ * localStorage key for user preferences. Default: `accessibility-widget`.
135
+ * Change this to namespace the widget on multi-tenant platforms where
136
+ * different sub-brands need isolated preferences.
137
+ */
138
+ storageKey?: string;
139
+
140
+ // ─── initial experience ───────────────────────────────────────────
141
+ /**
142
+ * Features turned ON for a first-time visitor (no persisted state yet).
143
+ * Only applies once — once the user has modified anything in the panel,
144
+ * the persisted state wins.
145
+ *
146
+ * @example Public-sector site that wants a visible focus ring by default
147
+ * ```ts
148
+ * { initialFeatures: { focusOutline: true, highlightLinks: true } }
149
+ * ```
150
+ */
151
+ initialFeatures?: Partial<Record<FeatureId, boolean>>;
152
+
153
+ // ─── feature gating ───────────────────────────────────────────────
154
+ /**
155
+ * Features hidden from the panel UI. Useful when certain features don't
156
+ * make sense in context (e.g. `tts` on a site with no text content).
157
+ * Hidden features are neither toggle-able nor activated by profile presets.
158
+ */
159
+ disabledFeatures?: readonly FeatureId[];
160
+
161
+ // ─── legal / compliance ───────────────────────────────────────────
162
+ /**
163
+ * URL of the site's accessibility statement. When set, a link is rendered
164
+ * in the panel footer (label is localized — see `Translation.statementLink`).
165
+ * Recommended for BFSG § 14 / EN 301 549 § 12.1.1 compliance.
166
+ *
167
+ * Absolute URLs (`http://`, `https://`, protocol-relative `//…`) open in a
168
+ * new tab with `rel="noopener noreferrer"`; relative paths stay same-tab.
169
+ */
170
+ statementUrl?: string;
171
+
172
+ /**
173
+ * Optional free-form disclaimer text rendered in the panel footer above
174
+ * the "Powered by" line. No default — if you want the footer to carry
175
+ * guidance ("Feedback zur Barrierefreiheit bitte an die Hotline …"), set
176
+ * it per host. Kept as plain text, not HTML; line breaks render as
177
+ * whitespace.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * { disclaimer: 'Feedback an barrierefreiheit@example.com' }
182
+ * ```
183
+ */
184
+ disclaimer?: string;
185
+
186
+ /**
187
+ * Suppress the "Powered by BAUER GROUP Accessibility-Widget" footer line.
188
+ * Intended for white-label deployments — the default (`false`) shows a
189
+ * small localised attribution with a link back to the product page.
190
+ */
191
+ hidePoweredBy?: boolean;
192
+
193
+ // ─── behavior ─────────────────────────────────────────────────────
194
+ /**
195
+ * When true, end users can drag the FAB to a custom position (pointer or
196
+ * keyboard). The chosen position persists under `storageKey` as part of
197
+ * the widget state and is restored on the next visit. Default: `false`.
198
+ *
199
+ * Keyboard: focus the FAB, hold Shift + Arrow keys to move in 10 px steps.
200
+ */
201
+ draggableFab?: boolean;
202
+ /**
203
+ * Keyboard shortcut that opens the widget from anywhere on the page.
204
+ *
205
+ * - `string` — a combo like `'ctrl+alt+a'`, `'alt+shift+a'`, `'f2'`. Tokens
206
+ * are case-insensitive and joined with `+`. Supported modifiers:
207
+ * `alt`, `ctrl`, `shift`, `meta`. Exactly one non-modifier key is required.
208
+ * - `false` — disable the shortcut entirely. Useful when the default clashes
209
+ * with a host's own A11y hotkey or a browser / extension binding.
210
+ *
211
+ * Invalid strings emit a `console.warn` when `debug: true` and the shortcut
212
+ * is disabled (no fallback to another combo — the host's choice is respected).
213
+ *
214
+ * Default: `'ctrl+alt+a'` — aligned with the prevailing convention among
215
+ * A11y widgets (Userway and others), rarely clashes with browser chrome.
216
+ *
217
+ * @example Function-key style
218
+ * ```ts
219
+ * { keyboardShortcut: 'f2' }
220
+ * ```
221
+ *
222
+ * @example Disable completely
223
+ * ```ts
224
+ * { keyboardShortcut: false }
225
+ * ```
226
+ */
227
+ keyboardShortcut?: string | false;
228
+ /** When true, features that add motion respect `prefers-reduced-motion`. Default: `true`. */
229
+ respectReducedMotion?: boolean;
230
+ /** When true, the FAB is hidden in print media (`@media print`). Default: `true`. */
231
+ hideOnPrint?: boolean;
232
+
233
+ // ─── debug ────────────────────────────────────────────────────────
234
+ /**
235
+ * When true, normally-silent failures (localStorage quota, malformed
236
+ * persisted state, failed core fetch) emit `console.warn`. Production
237
+ * bundles should keep this `false` (default) to avoid noise.
238
+ */
239
+ debug?: boolean;
240
+ }
241
+
242
+ export interface ProfilePreset {
243
+ features?: Partial<Record<FeatureId, boolean>>;
244
+ fontSizeLevel?: number;
245
+ lineHeightLevel?: number;
246
+ letterSpacingLevel?: number;
247
+ contrastMode?: ContrastMode;
248
+ }
249
+
250
+ export const DEFAULT_STATE: WidgetState = {
251
+ features: FEATURE_IDS.reduce(
252
+ (acc, id) => {
253
+ acc[id] = false;
254
+ return acc;
255
+ },
256
+ {} as Record<FeatureId, boolean>,
257
+ ),
258
+ fontSizeLevel: 1,
259
+ lineHeightLevel: 1.5,
260
+ letterSpacingLevel: 0,
261
+ contrastMode: 'off',
262
+ oversized: false,
263
+ fabPosition: null,
264
+ };
265
+
266
+ export const PROFILES: Record<ProfileId, ProfilePreset> = {
267
+ visionImpaired: {
268
+ features: {
269
+ fontSize: true,
270
+ contrast: true,
271
+ highlightLinks: true,
272
+ focusOutline: true,
273
+ bigCursor: true,
274
+ },
275
+ fontSizeLevel: 1.4,
276
+ contrastMode: 'high',
277
+ },
278
+ motor: {
279
+ features: { bigCursor: true, focusOutline: true, pauseAnimations: true },
280
+ },
281
+ cognitive: {
282
+ features: {
283
+ dyslexiaFont: true,
284
+ lineHeight: true,
285
+ letterSpacing: true,
286
+ readingGuide: true,
287
+ },
288
+ lineHeightLevel: 1.8,
289
+ letterSpacingLevel: 0.05,
290
+ },
291
+ seizureSafe: {
292
+ features: { pauseAnimations: true, grayscale: true },
293
+ },
294
+ adhd: {
295
+ features: { readingMask: true, pauseAnimations: true, focusOutline: true },
296
+ },
297
+ blind: {
298
+ features: { structureNav: true, highlightLinks: true, focusOutline: true },
299
+ },
300
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Emits a console warning only when debug mode is enabled on
3
+ * `window.AccessibilityWidgetConfig.debug`. Silent by default so production
4
+ * bundles don't leak noise, but makes genuine failures discoverable when the
5
+ * developer opts in.
6
+ */
7
+ export function warnIfDebug(message: string, error?: unknown): void {
8
+ if (typeof window === 'undefined') return;
9
+ if (!window.AccessibilityWidgetConfig?.debug) return;
10
+
11
+ console.warn(`[aw] ${message}`, error);
12
+ }
@@ -0,0 +1,68 @@
1
+ export type AttrMap = Record<string, string | number | boolean | null | undefined>;
2
+ export type EventMap = Record<string, EventListener>;
3
+
4
+ export interface MakeOptions {
5
+ class?: string;
6
+ text?: string;
7
+ attrs?: AttrMap;
8
+ on?: EventMap;
9
+ children?: (Node | null | undefined)[];
10
+ }
11
+
12
+ /**
13
+ * Type-safe DOM builder. Intentionally does NOT accept raw HTML strings —
14
+ * all content is set via textContent or pre-built child nodes. This keeps
15
+ * the widget XSS-safe even if a future config field leaks untrusted input.
16
+ */
17
+ export function make<K extends keyof HTMLElementTagNameMap>(
18
+ tag: K,
19
+ opts: MakeOptions = {},
20
+ ): HTMLElementTagNameMap[K] {
21
+ const el = document.createElement(tag);
22
+ if (opts.class) el.className = opts.class;
23
+ if (opts.text !== undefined) el.textContent = opts.text;
24
+ if (opts.attrs) {
25
+ for (const [k, v] of Object.entries(opts.attrs)) {
26
+ if (v === null || v === undefined || v === false) continue;
27
+ el.setAttribute(k, v === true ? '' : String(v));
28
+ }
29
+ }
30
+ if (opts.on) {
31
+ for (const [evt, handler] of Object.entries(opts.on)) {
32
+ el.addEventListener(evt, handler);
33
+ }
34
+ }
35
+ if (opts.children) {
36
+ for (const child of opts.children) {
37
+ if (child) el.appendChild(child);
38
+ }
39
+ }
40
+ return el;
41
+ }
42
+
43
+ export function toggleAttr(
44
+ el: Element,
45
+ name: string,
46
+ value: string | number | null | undefined | false,
47
+ ): void {
48
+ if (value === null || value === undefined || value === false || value === '') {
49
+ el.removeAttribute(name);
50
+ } else {
51
+ el.setAttribute(name, String(value));
52
+ }
53
+ }
54
+
55
+ export function prefersReducedMotion(): boolean {
56
+ return typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
57
+ }
58
+
59
+ export function onceIdle(cb: () => void, timeout = 2000): void {
60
+ const win = window as unknown as {
61
+ requestIdleCallback?: (cb: () => void, o?: { timeout: number }) => number;
62
+ };
63
+ if (typeof win.requestIdleCallback === 'function') {
64
+ win.requestIdleCallback(cb, { timeout });
65
+ } else {
66
+ setTimeout(cb, Math.min(timeout, 1000));
67
+ }
68
+ }
@@ -0,0 +1,54 @@
1
+ import type { Locale, ProfileId, WidgetState } from '../types/index.js';
2
+
3
+ /**
4
+ * Public event map. Events are dispatched as `CustomEvent` on `document`
5
+ * under the prefixed type name (`accessibility-widget:<name>`). Using
6
+ * the DOM event system keeps loader and core decoupled — neither needs
7
+ * to share a listener registry.
8
+ */
9
+ export interface WidgetEventMap {
10
+ stateChange: { state: WidgetState };
11
+ open: { trigger?: HTMLElement | null };
12
+ close: Record<string, never>;
13
+ profileApplied: { profile: ProfileId; state: WidgetState };
14
+ localeChanged: { locale: Locale };
15
+ reset: Record<string, never>;
16
+ }
17
+
18
+ export type WidgetEventName = keyof WidgetEventMap;
19
+
20
+ const PREFIX = 'accessibility-widget:';
21
+
22
+ export function dispatchWidgetEvent<K extends WidgetEventName>(
23
+ name: K,
24
+ detail: WidgetEventMap[K],
25
+ ): void {
26
+ if (typeof document === 'undefined') return;
27
+ try {
28
+ document.dispatchEvent(new CustomEvent(PREFIX + name, { detail }));
29
+ } catch {
30
+ /* CustomEvent not supported on ancient browsers — widget drops this event */
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Subscribe to a widget event. Returns an unsubscribe function.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const off = onWidgetEvent('stateChange', (e) => analytics.track('a11y', e.state));
40
+ * // later
41
+ * off();
42
+ * ```
43
+ */
44
+ export function onWidgetEvent<K extends WidgetEventName>(
45
+ name: K,
46
+ handler: (detail: WidgetEventMap[K]) => void,
47
+ ): () => void {
48
+ if (typeof document === 'undefined') return () => {};
49
+ const listener = (e: Event): void => {
50
+ handler((e as CustomEvent<WidgetEventMap[K]>).detail);
51
+ };
52
+ document.addEventListener(PREFIX + name, listener);
53
+ return () => document.removeEventListener(PREFIX + name, listener);
54
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Lucide-based pictograms for widget features and UI controls.
3
+ *
4
+ * All paths are taken from Lucide (https://lucide.dev, MIT license) and
5
+ * rendered via our programmatic buildIcon() — never innerHTML — so there is
6
+ * no XSS surface. Only the raw `d` data is shipped, no runtime dependency.
7
+ *
8
+ * Convention: every icon uses the shared 24x24 Lucide viewBox and
9
+ * `stroke: true`, which lets <svg> itself carry the stroke defaults
10
+ * (fill:none, stroke:currentColor, stroke-width:2, round caps) — path
11
+ * definitions therefore stay minimal and compress well under gzip.
12
+ *
13
+ * This module is imported ONLY from the panel (core bundle).
14
+ * The loader bundle never pulls it in, keeping the initial FAB payload tiny.
15
+ */
16
+ import type { FeatureId } from '../types/index.js';
17
+ import type { SvgIconOptions } from './svg.js';
18
+
19
+ export const FEATURE_ICONS: Record<FeatureId, SvgIconOptions> = {
20
+ fontSize: {
21
+ stroke: true,
22
+ paths: [{ d: 'M4 7V4h16v3M9 20h6M12 4v16' }],
23
+ },
24
+ lineHeight: {
25
+ stroke: true,
26
+ paths: [{ d: 'M3 6h18M3 12h18M3 18h18' }],
27
+ },
28
+ letterSpacing: {
29
+ stroke: true,
30
+ paths: [{ d: 'M18 8l4 4-4 4M2 12h20M6 8l-4 4 4 4' }],
31
+ },
32
+ contrast: {
33
+ stroke: true,
34
+ paths: [
35
+ { d: 'M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0z' },
36
+ { d: 'M12 2a10 10 0 0 1 0 20z', fill: 'currentColor' },
37
+ ],
38
+ },
39
+ grayscale: {
40
+ stroke: true,
41
+ paths: [
42
+ {
43
+ d: 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5-2 1.6-3 3.5-3 5.5a7 7 0 0 0 7 7z',
44
+ },
45
+ ],
46
+ },
47
+ invertColors: {
48
+ stroke: true,
49
+ paths: [{ d: 'M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9z' }],
50
+ },
51
+ dyslexiaFont: {
52
+ stroke: true,
53
+ paths: [
54
+ { d: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z' },
55
+ { d: 'M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z' },
56
+ ],
57
+ },
58
+ highlightLinks: {
59
+ stroke: true,
60
+ paths: [
61
+ { d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' },
62
+ { d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' },
63
+ ],
64
+ },
65
+ pauseAnimations: {
66
+ stroke: true,
67
+ paths: [{ d: 'M6 4h4v16H6zM14 4h4v16h-4z' }],
68
+ },
69
+ bigCursor: {
70
+ stroke: true,
71
+ paths: [{ d: 'M4 4l7.07 17 2.51-7.39L21 11.07z' }],
72
+ },
73
+ focusOutline: {
74
+ stroke: true,
75
+ paths: [
76
+ { d: 'M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z' },
77
+ {
78
+ d: 'M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2',
79
+ },
80
+ ],
81
+ },
82
+ readingMask: {
83
+ stroke: true,
84
+ paths: [
85
+ {
86
+ d: 'M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2M7 12h10',
87
+ },
88
+ ],
89
+ },
90
+ readingGuide: {
91
+ stroke: true,
92
+ paths: [{ d: 'M21 12H3M6 8v8M18 8v8' }],
93
+ },
94
+ tts: {
95
+ stroke: true,
96
+ paths: [
97
+ {
98
+ d: 'M11 4.7a.7.7 0 0 0-1.2-.5L6.4 7.6A1.4 1.4 0 0 1 5.4 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.4a1.4 1.4 0 0 1 1 .4l3.4 3.4A.7.7 0 0 0 11 19.3z',
99
+ },
100
+ { d: 'M16 9a5 5 0 0 1 0 6' },
101
+ { d: 'M19.36 18.36a9 9 0 0 0 0-12.72' },
102
+ ],
103
+ },
104
+ structureNav: {
105
+ stroke: true,
106
+ paths: [{ d: 'M21 12h-8M21 6H8M21 18h-8M3 6v4c0 1.1.9 2 2 2h3M3 10v6c0 1.1.9 2 2 2h3' }],
107
+ },
108
+ };
109
+
110
+ /** Reset / undo (Lucide: rotate-ccw). */
111
+ export const ICON_RESET: SvgIconOptions = {
112
+ stroke: true,
113
+ paths: [{ d: 'M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5' }],
114
+ };
115
+
116
+ /** Info / tooltip trigger (Lucide: info). */
117
+ export const ICON_INFO: SvgIconOptions = {
118
+ stroke: true,
119
+ paths: [
120
+ { d: 'M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0z' },
121
+ { d: 'M12 16v-4' },
122
+ { d: 'M12 8h.01' },
123
+ ],
124
+ };
125
+
126
+ /** Globe / language switcher (Lucide: globe). */
127
+ export const ICON_GLOBE: SvgIconOptions = {
128
+ stroke: true,
129
+ paths: [
130
+ { d: 'M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0z' },
131
+ { d: 'M12 2a14.5 14.5 0 0 0 0 20M12 2a14.5 14.5 0 0 1 0 20M2 12h20' },
132
+ ],
133
+ };
134
+
135
+ /** Oversized mode (Lucide: maximize-2). */
136
+ export const ICON_MAXIMIZE: SvgIconOptions = {
137
+ stroke: true,
138
+ paths: [{ d: 'M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7' }],
139
+ };
140
+
141
+ /** Collapsible section chevron (Lucide: chevron-down). */
142
+ export const ICON_CHEVRON: SvgIconOptions = {
143
+ stroke: true,
144
+ paths: [{ d: 'm6 9 6 6 6-6' }],
145
+ };
146
+
147
+ /** Drag handle (Lucide: grip-horizontal) — filled dots. */
148
+ export const ICON_GRIP: SvgIconOptions = {
149
+ circles: [
150
+ { cx: 9, cy: 5, r: 1 },
151
+ { cx: 9, cy: 12, r: 1 },
152
+ { cx: 9, cy: 19, r: 1 },
153
+ { cx: 15, cy: 5, r: 1 },
154
+ { cx: 15, cy: 12, r: 1 },
155
+ { cx: 15, cy: 19, r: 1 },
156
+ ],
157
+ };
158
+
159
+ /** Active state check mark (Lucide: check). */
160
+ export const ICON_CHECK: SvgIconOptions = {
161
+ stroke: true,
162
+ paths: [{ d: 'M20 6 9 17l-5-5' }],
163
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Native language labels for the runtime language switcher.
3
+ *
4
+ * Each entry is the language's endonym (its own native name), not a localized
5
+ * translation. Showing "Deutsch" to a Japanese reader is more useful than
6
+ * "German" — the user who needs German will recognise their own word for it.
7
+ *
8
+ * The autonym pattern also sidesteps N×N translation matrices.
9
+ */
10
+ import type { Locale } from '../types/index.js';
11
+
12
+ export const LANGUAGE_NAMES: Record<Locale, string> = {
13
+ de: 'Deutsch',
14
+ en: 'English',
15
+ fr: 'Français',
16
+ es: 'Español',
17
+ it: 'Italiano',
18
+ pl: 'Polski',
19
+ tr: 'Türkçe',
20
+ ar: 'العربية',
21
+ zh: '中文',
22
+ hi: 'हिन्दी',
23
+ pt: 'Português',
24
+ bn: 'বাংলা',
25
+ ru: 'Русский',
26
+ ja: '日本語',
27
+ ko: '한국어',
28
+ vi: 'Tiếng Việt',
29
+ fa: 'فارسی',
30
+ ur: 'اردو',
31
+ th: 'ไทย',
32
+ id: 'Bahasa Indonesia',
33
+ he: 'עברית',
34
+ nl: 'Nederlands',
35
+ sv: 'Svenska',
36
+ cs: 'Čeština',
37
+ el: 'Ελληνικά',
38
+ hu: 'Magyar',
39
+ ro: 'Română',
40
+ uk: 'Українська',
41
+ };