@oksigenia/access-panel 0.4.3 → 0.5.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.
@@ -1,3 +1,76 @@
1
+ // src/controls.ts
2
+ var ALL_CONTROLS = [
3
+ "text-size",
4
+ "line-height",
5
+ "text-align",
6
+ "readable-font",
7
+ "dyslexia-font",
8
+ "letter-spacing",
9
+ "contrast",
10
+ "grayscale",
11
+ "hide-images",
12
+ "highlight-links",
13
+ "colorblind",
14
+ "reading-guide",
15
+ "reading-mask",
16
+ "big-cursor",
17
+ "big-targets",
18
+ "pause-anim",
19
+ "focus"
20
+ ];
21
+ var CONTROL_STATE_KEY = {
22
+ "text-size": "zoom",
23
+ "line-height": "lh",
24
+ "text-align": "align",
25
+ "readable-font": "font",
26
+ "dyslexia-font": "dyslexia",
27
+ "letter-spacing": "ls",
28
+ "contrast": "contrast",
29
+ "grayscale": "grayOverlay",
30
+ "hide-images": "hideImages",
31
+ "highlight-links": "highlightLinks",
32
+ "colorblind": "colorblind",
33
+ "reading-guide": "readingGuide",
34
+ "reading-mask": "readingMask",
35
+ "big-cursor": "bigCursor",
36
+ "big-targets": "bigTargets",
37
+ "pause-anim": "pauseAnim",
38
+ "focus": "focusOutline"
39
+ };
40
+ var STATE_KEY_CONTROL = Object.fromEntries(
41
+ Object.entries(CONTROL_STATE_KEY).map(([control, key]) => [key, control])
42
+ );
43
+ var PRESETS = {
44
+ lowvision: { zoom: 2, contrast: true, highlightLinks: true, bigCursor: true, focusOutline: true },
45
+ dyslexia: { dyslexia: true, lh: 2, ls: 2, readingGuide: true },
46
+ motor: { bigCursor: true, bigTargets: true, focusOutline: true },
47
+ calm: { hideImages: true, pauseAnim: true }
48
+ };
49
+ var PRESET_IDS = ["lowvision", "dyslexia", "motor", "calm"];
50
+ function presetControlIds(id) {
51
+ return Object.keys(PRESETS[id]).map((k) => STATE_KEY_CONTROL[k]);
52
+ }
53
+ function resolveEnabledControls(controlsAttr, excludeAttr) {
54
+ const valid = new Set(ALL_CONTROLS);
55
+ const parse = (raw) => (raw ?? "").split(",").map((s) => s.trim().toLowerCase()).filter((s) => valid.has(s));
56
+ const whitelist = controlsAttr != null ? parse(controlsAttr) : null;
57
+ let enabled = whitelist && whitelist.length > 0 ? new Set(whitelist) : new Set(ALL_CONTROLS);
58
+ if (excludeAttr != null) {
59
+ for (const id of parse(excludeAttr)) enabled.delete(id);
60
+ }
61
+ return enabled;
62
+ }
63
+ function filterPresetForEnabled(id, enabled) {
64
+ const out = {};
65
+ for (const [k, v] of Object.entries(PRESETS[id])) {
66
+ if (enabled.has(STATE_KEY_CONTROL[k])) out[k] = v;
67
+ }
68
+ return out;
69
+ }
70
+ function presetIsAvailable(id, enabled) {
71
+ return presetControlIds(id).filter((c) => enabled.has(c)).length >= 2;
72
+ }
73
+
1
74
  // src/icons.ts
2
75
  var ICON_TXT = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.5,4v3h5v12h3V7h5V4H2.5z M21.5,9h-9v3h3v7h3v-7h3V9z"/></svg>';
3
76
  var ICON_LH = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6,7h2.5L5,3.5L1.5,7H4v10H1.5L5,20.5L8.5,17H6V7z M10,5v2h12V5H10z M10,19h12v-2H10V19z M10,13h12v-2H10V13z"/></svg>';
@@ -16,75 +89,100 @@ var ICON_COLORBLIND = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12
16
89
  var ICON_FOCUS = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 7V3h4v2H5v2H3zm14-4h4v4h-2V5h-2V3zm4 14v4h-4v-2h2v-2h2zM7 21H3v-4h2v2h2v2z"/></svg>';
17
90
  var ICON_MASK = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 4h20v6H2zm0 10h20v6H2zm2-9v4h16V5zm0 10v4h16v-4z" opacity=".55"/><rect x="2" y="10" width="20" height="4"/></svg>';
18
91
  var ICON_TARGETS = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h4v2H5v2H3zm14 0h4v4h-2V5h-2zm0 18v-2h2v-2h2v4zM3 21v-4h2v2h2v2zM12 8a4 4 0 100 8 4 4 0 000-8zm0 2a2 2 0 110 4 2 2 0 010-4z"/></svg>';
92
+ var UNIVERSAL_GLYPH = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z"/></svg>';
19
93
  var TRIGGER_ICONS = {
20
94
  vitruvian: '<svg viewBox="0 0 122.88 122.88" fill="currentColor"><path d="M61.44,0A61.46,61.46,0,1,1,18,18,61.21,61.21,0,0,1,61.44,0Zm-.39,74.18L52.1,98.91a4.94,4.94,0,0,1-2.58,2.83A5,5,0,0,1,42.7,95.5l6.24-17.28a26.3,26.3,0,0,0,1.17-4,40.64,40.64,0,0,0,.54-4.18c.24-2.53.41-5.27.54-7.9s.22-5.18.29-7.29c.09-2.63-.62-2.8-2.73-3.3l-.44-.1-18-3.39A5,5,0,0,1,27.08,46a5,5,0,0,1,5.05-7.74l19.34,3.63c.77.07,1.52.16,2.31.25a57.64,57.64,0,0,0,7.18.53A81.13,81.13,0,0,0,69.9,42c.9-.1,1.75-.21,2.6-.29l18.25-3.42A5,5,0,0,1,94.5,39a5,5,0,0,1,1.3,7,5,5,0,0,1-3.21,2.09L75.15,51.37c-.58.13-1.1.22-1.56.29-1.82.31-2.72.47-2.61,3.06.08,1.89.31,4.15.61,6.51.35,2.77.81,5.71,1.29,8.4.31,1.77.6,3.19,1,4.55s.79,2.75,1.39,4.42l6.11,16.9a5,5,0,0,1-6.82,6.24,4.94,4.94,0,0,1-2.58-2.83L63,74.23,62,72.4l-1,1.78Zm.39-53.52a8.83,8.83,0,1,1-6.24,2.59,8.79,8.79,0,0,1,6.24-2.59Zm36.35,4.43a51.42,51.42,0,1,0,15,36.35,51.27,51.27,0,0,0-15-36.35Z"/></svg>',
21
95
  wheelchair: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 13v-2c-1.54.02-3.09-.75-4.07-1.83l-1.29-1.43c-.17-.19-.38-.34-.61-.45-.01 0-.01-.01-.02-.01H13c-.35-.2-.75-.3-1.19-.26C10.76 7.11 10 8.04 10 9.09V15c0 1.1.9 2 2 2h5v5h2v-5.5c0-1.1-.9-2-2-2h-3v-3.45c1.29 1.07 3.25 1.94 5 1.95zm-6.17 5c-.41 1.16-1.52 2-2.83 2-1.66 0-3-1.34-3-3 0-1.31.84-2.41 2-2.83V12.1c-2.28.46-4 2.48-4 4.9 0 2.76 2.24 5 5 5 2.42 0 4.44-1.72 4.9-4h-2.07zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>',
22
96
  eye: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>',
23
- universal: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z"/></svg>'
97
+ universal: UNIVERSAL_GLYPH,
98
+ porthole: UNIVERSAL_GLYPH
24
99
  };
25
100
  var ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
26
101
  var COLORBLIND_FILTERS_SVG = '<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;overflow:hidden;" aria-hidden="true"><defs><filter id="oks-filter-protanopia"><feColorMatrix type="matrix" values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"/></filter><filter id="oks-filter-deuteranopia"><feColorMatrix type="matrix" values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"/></filter><filter id="oks-filter-tritanopia"><feColorMatrix type="matrix" values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"/></filter></defs></svg>';
27
102
 
28
103
  // src/render.ts
104
+ var PRESET_ICON = {
105
+ lowvision: ICON_CONTRAST,
106
+ dyslexia: ICON_DYSLEXIA,
107
+ motor: ICON_CURSOR,
108
+ calm: ICON_HIDE
109
+ };
29
110
  function buildPanelHtml(opts) {
30
111
  const { t, triggerIcon } = opts;
112
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
113
+ const showTrigger = opts.showTrigger ?? true;
114
+ const showPresets = opts.showPresets ?? true;
31
115
  const trig = TRIGGER_ICONS[triggerIcon];
32
- const grid = (rows) => `<div class="oks-access-grid">${rows.join("")}</div>`;
33
- const multi = (action, prefix, levels, label, icon, full = false) => {
116
+ const multi = (id, prefix, levels, label, icon, full = false) => {
117
+ if (!enabled.has(id)) return "";
118
+ const action = id === "colorblind" ? "colorblind" : "multi";
34
119
  const fullClass = full ? " full-width" : "";
35
120
  const dots = Array.from({ length: levels }, () => "<span></span>").join("");
36
- return `<button class="oks-access-opt multi-step${fullClass}" data-action="${action}" data-prefix="${prefix}" data-levels="${levels}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span><div class="oks-levels">${dots}</div></button>`;
121
+ return `<button class="oks-access-opt multi-step${fullClass}" data-control="${id}" data-action="${action}" data-prefix="${prefix}" data-levels="${levels}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span><div class="oks-levels">${dots}</div></button>`;
122
+ };
123
+ const toggle = (id, klass, label, icon) => {
124
+ if (!enabled.has(id)) return "";
125
+ return `<button class="oks-access-opt" data-control="${id}" data-action="toggle" data-class="${klass}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
126
+ };
127
+ const overlay = (id, target, label, icon) => {
128
+ if (!enabled.has(id)) return "";
129
+ return `<button class="oks-access-opt" data-control="${id}" data-action="overlay" data-target="${target}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
130
+ };
131
+ const simple = (id, action, label, icon) => {
132
+ if (!enabled.has(id)) return "";
133
+ return `<button class="oks-access-opt" data-control="${id}" data-action="${action}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
134
+ };
135
+ const preset = (id, label) => `<button class="oks-preset" data-action="preset" data-preset="${id}" type="button"><span class="oks-icon">${PRESET_ICON[id]}</span><span class="oks-label">${label}</span></button>`;
136
+ const section = (title, rows) => {
137
+ const kept = rows.filter(Boolean);
138
+ if (kept.length === 0) return "";
139
+ return `<h4 class="oks-access-title">${escapeHtml(title)}</h4><div class="oks-access-grid">${kept.join("")}</div>`;
140
+ };
141
+ const presetLabels = {
142
+ lowvision: t.pLow,
143
+ dyslexia: t.pDys,
144
+ motor: t.pMot,
145
+ calm: t.pCalm
37
146
  };
38
- const toggle = (klass, label, icon) => `<button class="oks-access-opt" data-action="toggle" data-class="${klass}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
39
- const overlay = (id, label, icon) => `<button class="oks-access-opt" data-action="overlay" data-target="${id}" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
40
- const guide = (label, icon) => `<button class="oks-access-opt" data-action="guide" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
41
- const mask = (label, icon) => `<button class="oks-access-opt" data-action="mask" aria-pressed="false" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
42
- const preset = (id, label, icon) => `<button class="oks-preset" data-action="preset" data-preset="${id}" type="button"><span class="oks-icon">${icon}</span><span class="oks-label">${label}</span></button>`;
43
- return `
147
+ const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
148
+ const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
149
+ const triggerBlock = showTrigger ? `
44
150
  <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
45
- <button class="oks-access-btn" id="oks-trigger" aria-label="${escapeAttr(t.title)}" aria-expanded="false" aria-controls="oks-panel" type="button">
151
+ <button class="oks-access-btn" id="oks-trigger" part="trigger" data-trigger-icon="${triggerIcon}" aria-label="${escapeAttr(t.title)}" aria-expanded="false" aria-controls="oks-panel" type="button">
46
152
  ${trig}
47
153
  </button>
48
154
  <span class="oks-active-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg></span>
49
- </div>
155
+ </div>` : "";
156
+ return `${triggerBlock}
50
157
  <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
51
158
  <div class="oks-access-header">
52
159
  <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
53
160
  <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
54
161
  </div>
55
162
  <div class="oks-access-content">
56
- <h4 class="oks-access-title">${escapeHtml(t.presets)}</h4>
57
- <div class="oks-access-presets">
58
- ${preset("lowvision", t.pLow, ICON_CONTRAST)}
59
- ${preset("dyslexia", t.pDys, ICON_DYSLEXIA)}
60
- ${preset("motor", t.pMot, ICON_CURSOR)}
61
- ${preset("calm", t.pCalm, ICON_HIDE)}
62
- </div>
63
- <h4 class="oks-access-title">${escapeHtml(t.txt)}</h4>
64
- ${grid([
65
- multi("multi", "oks-zoom", 4, t.size, ICON_TXT),
66
- multi("multi", "oks-lh", 3, t.lh, ICON_LH),
67
- multi("multi", "oks-align", 3, t.align, ICON_ALIGN),
68
- toggle("oks-a11y-font", t.font, ICON_FONT),
69
- toggle("oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
70
- multi("multi", "oks-ls", 3, t.ls, ICON_LS)
163
+ ${presetsBlock}
164
+ ${section(t.txt, [
165
+ multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
166
+ multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
167
+ multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
168
+ toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
169
+ toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
170
+ multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
71
171
  ])}
72
- <h4 class="oks-access-title">${escapeHtml(t.vis)}</h4>
73
- ${grid([
74
- toggle("oks-a11y-contrast", t.contrast, ICON_CONTRAST),
75
- overlay("oks-overlay-gray", t.gray, ICON_GRAY),
76
- toggle("oks-a11y-hide", t.hide, ICON_HIDE),
77
- toggle("oks-a11y-links", t.links, ICON_LINK),
172
+ ${section(t.vis, [
173
+ toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
174
+ overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
175
+ toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
176
+ toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
78
177
  multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
79
178
  ])}
80
- <h4 class="oks-access-title">${escapeHtml(t.ori)}</h4>
81
- ${grid([
82
- guide(t.guide, ICON_GUIDE),
83
- mask(t.mask, ICON_MASK),
84
- toggle("oks-big-cursor", t.cursor, ICON_CURSOR),
85
- toggle("oks-a11y-bigtargets", t.targets, ICON_TARGETS),
86
- toggle("oks-a11y-pause", t.pause, ICON_PAUSE),
87
- toggle("oks-a11y-focus", t.focus, ICON_FOCUS)
179
+ ${section(t.ori, [
180
+ simple("reading-guide", "guide", t.guide, ICON_GUIDE),
181
+ simple("reading-mask", "mask", t.mask, ICON_MASK),
182
+ toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
183
+ toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
184
+ toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
185
+ toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
88
186
  ])}
89
187
  </div>
90
188
  <div class="oks-access-footer">
@@ -109,14 +207,20 @@ function positionRules(position) {
109
207
  switch (position) {
110
208
  case "top-left":
111
209
  return { wrap: "top: 20px; left: 20px;", panel: "top: 80px; left: 20px;" };
210
+ case "top-center":
211
+ return { wrap: "top: 20px; left: 50%; transform: translateX(-50%);", panel: "top: 80px; left: 50%; transform: translateX(-50%);" };
112
212
  case "top-right":
113
213
  return { wrap: "top: 20px; right: 20px;", panel: "top: 80px; right: 20px;" };
114
214
  case "mid-left":
115
215
  return { wrap: "top: 50%; left: 20px; transform: translateY(-50%);", panel: "top: 50%; left: 90px; transform: translateY(-50%);" };
216
+ case "mid-center":
217
+ return { wrap: "top: 50%; left: 50%; transform: translate(-50%, -50%);", panel: "top: 50%; left: 50%; transform: translate(-50%, -50%);" };
116
218
  case "mid-right":
117
219
  return { wrap: "top: 50%; right: 20px; transform: translateY(-50%);", panel: "top: 50%; right: 90px; transform: translateY(-50%);" };
118
220
  case "bottom-left":
119
221
  return { wrap: "bottom: 20px; left: 20px;", panel: "bottom: 100px; left: 20px;" };
222
+ case "bottom-center":
223
+ return { wrap: "bottom: 20px; left: 50%; transform: translateX(-50%);", panel: "bottom: 100px; left: 50%; transform: translateX(-50%);" };
120
224
  case "bottom-right":
121
225
  return { wrap: "bottom: 20px; right: 20px;", panel: "bottom: 100px; right: 20px;" };
122
226
  }
@@ -184,6 +288,24 @@ function isStateEmpty(state) {
184
288
  return state.zoom === 0 && state.lh === 0 && state.align === 0 && state.ls === 0 && state.colorblind === 0 && !state.font && !state.dyslexia && !state.contrast && !state.hideImages && !state.highlightLinks && !state.bigCursor && !state.pauseAnim && !state.focusOutline && !state.grayOverlay && !state.readingGuide && !state.readingMask && !state.bigTargets;
185
289
  }
186
290
 
291
+ // src/translations.ts
292
+ var DICT = {
293
+ es: { title: "Accesibilidad", close: "Cerrar", txt: "Texto", size: "Tama\xF1o", lh: "Interlineado", align: "Alineaci\xF3n", font: "Legible", dyslexia: "Fuente Dislexia", vis: "Visual", contrast: "Contraste", gray: "Grises", hide: "Ocultar Img", links: "Links", ori: "Orientaci\xF3n", guide: "Gu\xEDa", mask: "M\xE1scara", targets: "\xC1reas Grandes", cursor: "Cursor Grande", pause: "Parar Animac.", reset: "Restablecer Todo", dev: "Desarrollado por", ls: "Espaciado", cb: "Daltonismo", focus: "Foco", presets: "Perfiles", pLow: "Baja Visi\xF3n", pDys: "Dislexia", pMot: "Motor", pCalm: "Sin Distrac.", level: (l, n, m) => `${l}, nivel ${n} de ${m}` },
294
+ en: { title: "Accessibility", close: "Close", txt: "Text", size: "Size", lh: "Line Height", align: "Align", font: "Readable Font", dyslexia: "Dyslexia Font", vis: "Visual", contrast: "Contrast", gray: "Grayscale", hide: "Hide Images", links: "Highlight Links", ori: "Orientation", guide: "Reading Guide", mask: "Reading Mask", targets: "Big Targets", cursor: "Big Cursor", pause: "Pause Anim.", reset: "Reset All", dev: "Developed by", ls: "Letter Spacing", cb: "Color Blind", focus: "Focus", presets: "Profiles", pLow: "Low Vision", pDys: "Dyslexia", pMot: "Motor", pCalm: "No Distract.", level: (l, n, m) => `${l}, level ${n} of ${m}` },
295
+ gn: { title: "Oikeha (Accesibilidad)", close: "Mboty", txt: "Mo\xF1e'\u1EBDr\xE3", size: "Tuichakue", lh: "Jei", align: "Mbojoja", font: "Letra Por\xE3", dyslexia: "Dislexia", vis: "Hechapy", contrast: "Sa'y", gray: "H\u0169 ha T\u0129", hide: "Moka\xF1y Ta'anga", links: "Joajuha", ori: "S\xE3mbyhy", guide: "S\xE3mbyhyha", mask: "Mbohov\xE1i", targets: "Tenda Guasu", cursor: "Cursor Guasu", pause: "Mboopyta", reset: "Mbojevy", dev: "Apojare", ls: "Rapykue", cb: "Sa'yvy", focus: "\xD1emoha", presets: "Tekor\xE3", pLow: "Hechapy Vai", pDys: "Dislexia", pMot: "Po-rehegua", pCalm: "Py\u02BCa Guapy", level: (l, n, m) => `${l}, ${n}/${m}` },
296
+ fr: { title: "Accessibilit\xE9", close: "Fermer", txt: "Texte", size: "Taille", lh: "Interligne", align: "Alignement", font: "Police Lisible", dyslexia: "Police Dyslexie", vis: "Visuel", contrast: "Contraste", gray: "Niveaux Gris", hide: "Masquer Img", links: "Liens", ori: "Orientation", guide: "Guide Lecture", mask: "Masque Lecture", targets: "Grandes Cibles", cursor: "Grand Curseur", pause: "Pause Anim.", reset: "R\xE9initialiser", dev: "D\xE9velopp\xE9 par", ls: "Espacement", cb: "Daltonisme", focus: "Focus", presets: "Profils", pLow: "Basse Vision", pDys: "Dyslexie", pMot: "Moteur", pCalm: "Sans Distrac.", level: (l, n, m) => `${l}, niveau ${n} sur ${m}` },
297
+ it: { title: "Accessibilit\xE0", close: "Chiudi", txt: "Testo", size: "Dimensione", lh: "Interlinea", align: "Allineamento", font: "Leggibile", dyslexia: "Font Dislessia", vis: "Visivo", contrast: "Contrasto", gray: "Scala Grig.", hide: "Nascondi Img", links: "Link", ori: "Orientamento", guide: "Guida", mask: "Maschera", targets: "Aree Grandi", cursor: "Cursore Grande", pause: "Pausa Anim.", reset: "Reimposta", dev: "Sviluppato da", ls: "Spaziatura", cb: "Daltonismo", focus: "Focus", presets: "Profili", pLow: "Ipovisione", pDys: "Dislessia", pMot: "Motorio", pCalm: "No Distraz.", level: (l, n, m) => `${l}, livello ${n} di ${m}` },
298
+ de: { title: "Barrierefreiheit", close: "Schlie\xDFen", txt: "Text", size: "Gr\xF6\xDFe", lh: "Zeilenh\xF6he", align: "Ausrichtung", font: "Lesbar", dyslexia: "Dyslexie", vis: "Visuell", contrast: "Kontrast", gray: "Graustufen", hide: "Bilder Aus", links: "Links", ori: "Orientierung", guide: "Lesehilfe", mask: "Lesemaske", targets: "Gro\xDFe Ziele", cursor: "Gro\xDFer Cursor", pause: "Anim. Stopp", reset: "Zur\xFCcksetzen", dev: "Entwickelt von", ls: "Buchst.abst.", cb: "Farbblindheit", focus: "Fokus", presets: "Profile", pLow: "Sehschw\xE4che", pDys: "Dyslexie", pMot: "Motorik", pCalm: "Ohne Ablenk.", level: (l, n, m) => `${l}, Stufe ${n} von ${m}` },
299
+ nl: { title: "Toegankelijkheid", close: "Sluiten", txt: "Tekst", size: "Grootte", lh: "Regelhoogte", align: "Uitlijning", font: "Leesbaar", dyslexia: "Dyslexie", vis: "Visueel", contrast: "Contrast", gray: "Grijstinten", hide: "Verberg Afb.", links: "Links", ori: "Ori\xEBntatie", guide: "Leesgids", mask: "Leesmasker", targets: "Grote Doelen", cursor: "Grote Cursor", pause: "Anim. Pauze", reset: "Resetten", dev: "Ontwikkeld door", ls: "Letterspatie", cb: "Kleurenblind", focus: "Focus", presets: "Profielen", pLow: "Slechtziend", pDys: "Dyslexie", pMot: "Motoriek", pCalm: "Geen Afleid.", level: (l, n, m) => `${l}, niveau ${n} van ${m}` },
300
+ sv: { title: "Tillg\xE4nglighet", close: "St\xE4ng", txt: "Text", size: "Storlek", lh: "Radh\xF6jd", align: "Justering", font: "L\xE4sbar", dyslexia: "Dyslexi", vis: "Visuell", contrast: "Kontrast", gray: "Gr\xE5skala", hide: "D\xF6lj Bilder", links: "L\xE4nkar", ori: "Orientering", guide: "L\xE4slinjal", mask: "L\xE4smask", targets: "Stora Ytor", cursor: "Stor Mark\xF6r", pause: "Pausa Anim.", reset: "\xC5terst\xE4ll", dev: "Utvecklad av", ls: "Bokstavsavst.", cb: "F\xE4rgblindhet", focus: "Fokus", presets: "Profiler", pLow: "Synneds\xE4tt.", pDys: "Dyslexi", pMot: "Motorik", pCalm: "Inga Distrak.", level: (l, n, m) => `${l}, niv\xE5 ${n} av ${m}` }
301
+ };
302
+ function getTranslation(locale) {
303
+ const lc = locale.toLowerCase();
304
+ if (lc === "gn" || lc.startsWith("gn-") || lc.startsWith("gn_")) return DICT.gn;
305
+ const base = lc.split(/[-_]/)[0];
306
+ return DICT[base] ?? DICT.en;
307
+ }
308
+
187
309
  // src/behavior.ts
188
310
  var MULTI_KEYS = {
189
311
  "oks-zoom": "zoom",
@@ -210,37 +332,45 @@ var TOGGLE_KEYS = {
210
332
  "oks-a11y-focus": "focusOutline",
211
333
  "oks-a11y-bigtargets": "bigTargets"
212
334
  };
213
- var PRESETS = {
214
- lowvision: { zoom: 2, contrast: true, highlightLinks: true, bigCursor: true, focusOutline: true },
215
- dyslexia: { dyslexia: true, lh: 2, ls: 2, readingGuide: true },
216
- motor: { bigCursor: true, bigTargets: true, focusOutline: true },
217
- calm: { hideImages: true, pauseAnim: true }
218
- };
335
+ function noopController() {
336
+ return Object.assign(() => {
337
+ }, { open() {
338
+ }, close() {
339
+ }, toggle() {
340
+ } });
341
+ }
342
+ function deepActiveElement() {
343
+ let a = document.activeElement;
344
+ while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
345
+ return a;
346
+ }
219
347
  function bindPanelBehavior(root, opts = {}) {
220
348
  const storageKey = opts.storageKey ?? "oksiacSettings";
349
+ const t = getTranslation(opts.locale ?? "en");
350
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
351
+ const nudgeMax = Math.max(0, opts.nudgeMax ?? 0);
221
352
  const trigger = root.getElementById("oks-trigger");
222
353
  const panel = root.getElementById("oks-panel");
223
354
  const closeBtn = root.getElementById("oks-close");
224
355
  const resetBtn = root.getElementById("oks-reset");
225
356
  const wrapper = root.getElementById("oks-wrapper");
226
357
  const opts$ = Array.from(root.querySelectorAll(".oks-access-opt, .oks-preset"));
227
- if (!trigger || !panel || !closeBtn || !resetBtn || !wrapper) {
228
- return () => {
229
- };
358
+ if (!panel || !closeBtn || !resetBtn) {
359
+ return noopController();
230
360
  }
231
361
  let state = loadState(storageKey);
232
362
  function applyState() {
233
363
  const body = document.body;
234
- const root2 = document.documentElement;
364
+ const rootEl = document.documentElement;
235
365
  for (const cls of Array.from(body.classList)) {
236
366
  if (cls.startsWith("oks-")) body.classList.remove(cls);
237
367
  }
238
- [1, 2, 3].forEach((l) => root2.classList.remove(`oks-colorblind-${l}`));
368
+ [1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
239
369
  if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
240
370
  if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
241
371
  if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
242
372
  if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
243
- if (state.colorblind > 0) root2.classList.add(`oks-colorblind-${state.colorblind}`);
373
+ if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
244
374
  if (state.font) body.classList.add("oks-a11y-font");
245
375
  if (state.dyslexia) body.classList.add("oks-dyslexia");
246
376
  if (state.contrast) body.classList.add("oks-a11y-contrast");
@@ -268,6 +398,10 @@ function bindPanelBehavior(root, opts = {}) {
268
398
  btn.setAttribute("data-level", String(lvl));
269
399
  btn.classList.toggle("is-active", lvl > 0);
270
400
  btn.setAttribute("aria-pressed", lvl > 0 ? "true" : "false");
401
+ const max = parseInt(btn.getAttribute("data-levels") ?? "0", 10);
402
+ const lbl = btn.querySelector(".oks-label")?.textContent?.trim() ?? "";
403
+ if (lvl > 0 && max > 0) btn.setAttribute("aria-label", t.level(lbl, lvl, max));
404
+ else btn.removeAttribute("aria-label");
271
405
  } else if (action === "toggle") {
272
406
  const klass = btn.getAttribute("data-class") ?? "";
273
407
  const key = TOGGLE_KEYS[klass];
@@ -313,9 +447,8 @@ function bindPanelBehavior(root, opts = {}) {
313
447
  } else if (action === "mask") {
314
448
  state.readingMask = !state.readingMask;
315
449
  } else if (action === "preset") {
316
- const id = btn.getAttribute("data-preset") ?? "";
317
- const preset = PRESETS[id];
318
- if (preset) Object.assign(state, preset);
450
+ const id = btn.getAttribute("data-preset");
451
+ if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
319
452
  btn.classList.add("is-flashing");
320
453
  setTimeout(() => btn.classList.remove("is-flashing"), 250);
321
454
  }
@@ -327,28 +460,50 @@ function bindPanelBehavior(root, opts = {}) {
327
460
  applyState();
328
461
  saveState(storageKey, state);
329
462
  };
463
+ let opener = null;
464
+ let ignoreDocClose = false;
330
465
  const openPanel = () => {
466
+ if (panel.classList.contains("is-open")) return;
467
+ opener = deepActiveElement();
331
468
  panel.classList.add("is-open");
332
469
  panel.removeAttribute("inert");
333
- trigger.setAttribute("aria-expanded", "true");
470
+ trigger?.setAttribute("aria-expanded", "true");
471
+ ignoreDocClose = true;
472
+ setTimeout(() => {
473
+ ignoreDocClose = false;
474
+ }, 0);
334
475
  const first = panel.querySelector("button:not([disabled])");
335
476
  first?.focus();
336
477
  };
337
478
  const closePanel = () => {
479
+ if (!panel.classList.contains("is-open")) return;
338
480
  panel.classList.remove("is-open");
339
481
  panel.setAttribute("inert", "");
340
- trigger.setAttribute("aria-expanded", "false");
341
- trigger.focus();
482
+ trigger?.setAttribute("aria-expanded", "false");
483
+ if (trigger) trigger.focus();
484
+ else if (opener?.isConnected) opener.focus();
485
+ opener = null;
342
486
  };
343
- const onTriggerClick = (e) => {
344
- e.stopPropagation();
487
+ const togglePanel = () => {
345
488
  if (panel.classList.contains("is-open")) closePanel();
346
489
  else openPanel();
347
490
  };
491
+ let suppressClick = false;
492
+ const onTriggerClick = (e) => {
493
+ e.stopPropagation();
494
+ if (suppressClick) {
495
+ suppressClick = false;
496
+ return;
497
+ }
498
+ togglePanel();
499
+ };
348
500
  const onDocClick = (e) => {
349
501
  if (!panel.classList.contains("is-open")) return;
502
+ if (ignoreDocClose) return;
350
503
  const path = e.composedPath();
351
- if (path.includes(panel) || path.includes(trigger) || path.includes(wrapper)) return;
504
+ if (path.includes(panel)) return;
505
+ if (trigger && path.includes(trigger)) return;
506
+ if (wrapper && path.includes(wrapper)) return;
352
507
  closePanel();
353
508
  };
354
509
  const onKeyDown = (e) => {
@@ -375,6 +530,88 @@ function bindPanelBehavior(root, opts = {}) {
375
530
  }
376
531
  }
377
532
  };
533
+ const NUDGE_KEY = `${storageKey}::pos`;
534
+ const clampMax = (v) => Math.max(-nudgeMax, Math.min(nudgeMax, v));
535
+ let nudge = { x: 0, y: 0 };
536
+ const loadNudge = () => {
537
+ if (nudgeMax <= 0) return;
538
+ try {
539
+ const raw = localStorage.getItem(NUDGE_KEY);
540
+ if (!raw) return;
541
+ const p = JSON.parse(raw);
542
+ if (typeof p?.x === "number" && typeof p?.y === "number") {
543
+ nudge = { x: clampMax(p.x), y: clampMax(p.y) };
544
+ }
545
+ } catch {
546
+ }
547
+ };
548
+ const saveNudge = () => {
549
+ try {
550
+ if (nudge.x === 0 && nudge.y === 0) localStorage.removeItem(NUDGE_KEY);
551
+ else localStorage.setItem(NUDGE_KEY, JSON.stringify(nudge));
552
+ } catch {
553
+ }
554
+ };
555
+ const applyNudge = () => {
556
+ if (wrapper) wrapper.style.translate = `${nudge.x}px ${nudge.y}px`;
557
+ };
558
+ const clampOffset = (x, y) => {
559
+ x = clampMax(x);
560
+ y = clampMax(y);
561
+ if (wrapper) {
562
+ const r = wrapper.getBoundingClientRect();
563
+ const pad = 4;
564
+ const baseLeft = r.left - nudge.x;
565
+ const baseTop = r.top - nudge.y;
566
+ const vw = window.innerWidth || 0;
567
+ const vh = window.innerHeight || 0;
568
+ if (r.width > 0 && vw > 0) x = Math.max(pad - baseLeft, Math.min(vw - pad - r.width - baseLeft, x));
569
+ if (r.height > 0 && vh > 0) y = Math.max(pad - baseTop, Math.min(vh - pad - r.height - baseTop, y));
570
+ }
571
+ return { x, y };
572
+ };
573
+ let dragStart = null;
574
+ let didDrag = false;
575
+ const DRAG_THRESHOLD = 4;
576
+ const onPointerDown = (e) => {
577
+ if (nudgeMax <= 0 || e.button !== 0) return;
578
+ dragStart = { px: e.clientX, py: e.clientY, ox: nudge.x, oy: nudge.y };
579
+ didDrag = false;
580
+ trigger?.setPointerCapture?.(e.pointerId);
581
+ };
582
+ const onPointerMove = (e) => {
583
+ if (!dragStart) return;
584
+ const dx = e.clientX - dragStart.px;
585
+ const dy = e.clientY - dragStart.py;
586
+ if (!didDrag && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
587
+ didDrag = true;
588
+ nudge = clampOffset(dragStart.ox + dx, dragStart.oy + dy);
589
+ applyNudge();
590
+ e.preventDefault();
591
+ };
592
+ const onPointerUp = () => {
593
+ if (!dragStart) return;
594
+ dragStart = null;
595
+ if (didDrag) {
596
+ saveNudge();
597
+ suppressClick = true;
598
+ }
599
+ };
600
+ const onTriggerKey = (e) => {
601
+ if (nudgeMax <= 0) return;
602
+ const step = e.shiftKey ? 1 : 10;
603
+ let dx = 0;
604
+ let dy = 0;
605
+ if (e.key === "ArrowLeft") dx = -step;
606
+ else if (e.key === "ArrowRight") dx = step;
607
+ else if (e.key === "ArrowUp") dy = -step;
608
+ else if (e.key === "ArrowDown") dy = step;
609
+ else return;
610
+ e.preventDefault();
611
+ nudge = clampOffset(nudge.x + dx, nudge.y + dy);
612
+ applyNudge();
613
+ saveNudge();
614
+ };
378
615
  const onMove = (e) => {
379
616
  if (!state.readingGuide && !state.readingMask) return;
380
617
  const y = e.touches?.[0]?.clientY ?? e.clientY;
@@ -388,7 +625,7 @@ function bindPanelBehavior(root, opts = {}) {
388
625
  if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
389
626
  }
390
627
  };
391
- trigger.addEventListener("click", onTriggerClick);
628
+ trigger?.addEventListener("click", onTriggerClick);
392
629
  closeBtn.addEventListener("click", closePanel);
393
630
  resetBtn.addEventListener("click", onReset);
394
631
  for (const btn of opts$) btn.addEventListener("click", onOptClick);
@@ -396,9 +633,19 @@ function bindPanelBehavior(root, opts = {}) {
396
633
  document.addEventListener("keydown", onKeyDown);
397
634
  document.addEventListener("mousemove", onMove);
398
635
  document.addEventListener("touchmove", onMove, { passive: true });
636
+ if (nudgeMax > 0 && trigger) {
637
+ loadNudge();
638
+ applyNudge();
639
+ trigger.style.cursor = "grab";
640
+ trigger.style.touchAction = "none";
641
+ trigger.addEventListener("pointerdown", onPointerDown);
642
+ document.addEventListener("pointermove", onPointerMove);
643
+ document.addEventListener("pointerup", onPointerUp);
644
+ trigger.addEventListener("keydown", onTriggerKey);
645
+ }
399
646
  applyState();
400
- return () => {
401
- trigger.removeEventListener("click", onTriggerClick);
647
+ const dispose = () => {
648
+ trigger?.removeEventListener("click", onTriggerClick);
402
649
  closeBtn.removeEventListener("click", closePanel);
403
650
  resetBtn.removeEventListener("click", onReset);
404
651
  for (const btn of opts$) btn.removeEventListener("click", onOptClick);
@@ -406,7 +653,12 @@ function bindPanelBehavior(root, opts = {}) {
406
653
  document.removeEventListener("keydown", onKeyDown);
407
654
  document.removeEventListener("mousemove", onMove);
408
655
  document.removeEventListener("touchmove", onMove);
656
+ trigger?.removeEventListener("pointerdown", onPointerDown);
657
+ document.removeEventListener("pointermove", onPointerMove);
658
+ document.removeEventListener("pointerup", onPointerUp);
659
+ trigger?.removeEventListener("keydown", onTriggerKey);
409
660
  };
661
+ return Object.assign(dispose, { open: openPanel, close: closePanel, toggle: togglePanel });
410
662
  }
411
663
  function ensureOverlay() {
412
664
  let el = document.getElementById("oks-overlay-gray");
@@ -461,6 +713,12 @@ var PANEL_CSS = `
461
713
  width: 60%;
462
714
  height: 60%;
463
715
  }
716
+ /* Porthole preset: a brushed-metal ring framing the standard glyph. The glyph
717
+ itself is untouched (discoverability), only the button gets the frame.
718
+ Hosts wanting a bespoke frame can target ::part(trigger) from light DOM. */
719
+ .oks-access-btn[data-trigger-icon="porthole"] {
720
+ box-shadow: 0 0 0 4px #b0bec5, 0 0 0 7px #546e7a, 0 6px 18px rgba(0,0,0,0.3);
721
+ }
464
722
  .oks-active-badge {
465
723
  position: absolute;
466
724
  top: -2px;
@@ -629,6 +887,12 @@ var PANEL_CSS = `
629
887
  .oks-access-branding { margin-top: 12px; font-size: 12px; color: #000; font-weight: 700; }
630
888
  .oks-access-branding a { color: #000; text-decoration: none; border-bottom: 1px dotted #000; }
631
889
 
890
+ @media (prefers-reduced-motion: reduce) {
891
+ .oks-access-btn, .oks-access-panel, .oks-access-close,
892
+ .oks-preset, .oks-access-opt { transition: none; }
893
+ .oks-access-btn:hover { transform: scale(1); }
894
+ .oks-preset.is-flashing { transform: none; }
895
+ }
632
896
  @media (max-width: 768px) {
633
897
  .oks-access-panel {
634
898
  width: 100%; height: 100%; max-height: 100%;
@@ -806,26 +1070,20 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
806
1070
  }
807
1071
  `;
808
1072
 
809
- // src/translations.ts
810
- var DICT = {
811
- es: { title: "Accesibilidad", close: "Cerrar", txt: "Texto", size: "Tama\xF1o", lh: "Interlineado", align: "Alineaci\xF3n", font: "Legible", dyslexia: "Fuente Dislexia", vis: "Visual", contrast: "Contraste", gray: "Grises", hide: "Ocultar Img", links: "Links", ori: "Orientaci\xF3n", guide: "Gu\xEDa", mask: "M\xE1scara", targets: "\xC1reas Grandes", cursor: "Cursor Grande", pause: "Parar Animac.", reset: "Restablecer Todo", dev: "Desarrollado por", ls: "Espaciado", cb: "Daltonismo", focus: "Foco", presets: "Perfiles", pLow: "Baja Visi\xF3n", pDys: "Dislexia", pMot: "Motor", pCalm: "Sin Distrac." },
812
- en: { title: "Accessibility", close: "Close", txt: "Text", size: "Size", lh: "Line Height", align: "Align", font: "Readable Font", dyslexia: "Dyslexia Font", vis: "Visual", contrast: "Contrast", gray: "Grayscale", hide: "Hide Images", links: "Highlight Links", ori: "Orientation", guide: "Reading Guide", mask: "Reading Mask", targets: "Big Targets", cursor: "Big Cursor", pause: "Pause Anim.", reset: "Reset All", dev: "Developed by", ls: "Letter Spacing", cb: "Color Blind", focus: "Focus", presets: "Profiles", pLow: "Low Vision", pDys: "Dyslexia", pMot: "Motor", pCalm: "No Distract." },
813
- gn: { title: "Oikeha (Accesibilidad)", close: "Mboty", txt: "Mo\xF1e'\u1EBDr\xE3", size: "Tuichakue", lh: "Jei", align: "Mbojoja", font: "Letra Por\xE3", dyslexia: "Dislexia", vis: "Hechapy", contrast: "Sa'y", gray: "H\u0169 ha T\u0129", hide: "Moka\xF1y Ta'anga", links: "Joajuha", ori: "S\xE3mbyhy", guide: "S\xE3mbyhyha", mask: "Mbohov\xE1i", targets: "Tenda Guasu", cursor: "Cursor Guasu", pause: "Mboopyta", reset: "Mbojevy", dev: "Apojare", ls: "Rapykue", cb: "Sa'yvy", focus: "\xD1emoha", presets: "Tekor\xE3", pLow: "Hechapy Vai", pDys: "Dislexia", pMot: "Po-rehegua", pCalm: "Py\u02BCa Guapy" },
814
- fr: { title: "Accessibilit\xE9", close: "Fermer", txt: "Texte", size: "Taille", lh: "Interligne", align: "Alignement", font: "Police Lisible", dyslexia: "Police Dyslexie", vis: "Visuel", contrast: "Contraste", gray: "Niveaux Gris", hide: "Masquer Img", links: "Liens", ori: "Orientation", guide: "Guide Lecture", mask: "Masque Lecture", targets: "Grandes Cibles", cursor: "Grand Curseur", pause: "Pause Anim.", reset: "R\xE9initialiser", dev: "D\xE9velopp\xE9 par", ls: "Espacement", cb: "Daltonisme", focus: "Focus", presets: "Profils", pLow: "Basse Vision", pDys: "Dyslexie", pMot: "Moteur", pCalm: "Sans Distrac." },
815
- it: { title: "Accessibilit\xE0", close: "Chiudi", txt: "Testo", size: "Dimensione", lh: "Interlinea", align: "Allineamento", font: "Leggibile", dyslexia: "Font Dislessia", vis: "Visivo", contrast: "Contrasto", gray: "Scala Grig.", hide: "Nascondi Img", links: "Link", ori: "Orientamento", guide: "Guida", mask: "Maschera", targets: "Aree Grandi", cursor: "Cursore Grande", pause: "Pausa Anim.", reset: "Reimposta", dev: "Sviluppato da", ls: "Spaziatura", cb: "Daltonismo", focus: "Focus", presets: "Profili", pLow: "Ipovisione", pDys: "Dislessia", pMot: "Motorio", pCalm: "No Distraz." },
816
- de: { title: "Barrierefreiheit", close: "Schlie\xDFen", txt: "Text", size: "Gr\xF6\xDFe", lh: "Zeilenh\xF6he", align: "Ausrichtung", font: "Lesbar", dyslexia: "Dyslexie", vis: "Visuell", contrast: "Kontrast", gray: "Graustufen", hide: "Bilder Aus", links: "Links", ori: "Orientierung", guide: "Lesehilfe", mask: "Lesemaske", targets: "Gro\xDFe Ziele", cursor: "Gro\xDFer Cursor", pause: "Anim. Stopp", reset: "Zur\xFCcksetzen", dev: "Entwickelt von", ls: "Buchst.abst.", cb: "Farbblindheit", focus: "Fokus", presets: "Profile", pLow: "Sehschw\xE4che", pDys: "Dyslexie", pMot: "Motorik", pCalm: "Ohne Ablenk." },
817
- nl: { title: "Toegankelijkheid", close: "Sluiten", txt: "Tekst", size: "Grootte", lh: "Regelhoogte", align: "Uitlijning", font: "Leesbaar", dyslexia: "Dyslexie", vis: "Visueel", contrast: "Contrast", gray: "Grijstinten", hide: "Verberg Afb.", links: "Links", ori: "Ori\xEBntatie", guide: "Leesgids", mask: "Leesmasker", targets: "Grote Doelen", cursor: "Grote Cursor", pause: "Anim. Pauze", reset: "Resetten", dev: "Ontwikkeld door", ls: "Letterspatie", cb: "Kleurenblind", focus: "Focus", presets: "Profielen", pLow: "Slechtziend", pDys: "Dyslexie", pMot: "Motoriek", pCalm: "Geen Afleid." },
818
- sv: { title: "Tillg\xE4nglighet", close: "St\xE4ng", txt: "Text", size: "Storlek", lh: "Radh\xF6jd", align: "Justering", font: "L\xE4sbar", dyslexia: "Dyslexi", vis: "Visuell", contrast: "Kontrast", gray: "Gr\xE5skala", hide: "D\xF6lj Bilder", links: "L\xE4nkar", ori: "Orientering", guide: "L\xE4slinjal", mask: "L\xE4smask", targets: "Stora Ytor", cursor: "Stor Mark\xF6r", pause: "Pausa Anim.", reset: "\xC5terst\xE4ll", dev: "Utvecklad av", ls: "Bokstavsavst.", cb: "F\xE4rgblindhet", focus: "Fokus", presets: "Profiler", pLow: "Synneds\xE4tt.", pDys: "Dyslexi", pMot: "Motorik", pCalm: "Inga Distrak." }
819
- };
820
- function getTranslation(locale) {
821
- const lc = locale.toLowerCase();
822
- if (lc === "gn" || lc.startsWith("gn-") || lc.startsWith("gn_")) return DICT.gn;
823
- const base = lc.split(/[-_]/)[0];
824
- return DICT[base] ?? DICT.en;
825
- }
826
-
827
1073
  // src/web-component.ts
828
- var OBSERVED = ["locale", "position", "position-mobile", "trigger-icon", "storage-key"];
1074
+ var OBSERVED = [
1075
+ "locale",
1076
+ "position",
1077
+ "position-mobile",
1078
+ "trigger-icon",
1079
+ "storage-key",
1080
+ "controls",
1081
+ "exclude",
1082
+ "trigger",
1083
+ "effects-exclude",
1084
+ "nudge",
1085
+ "presets"
1086
+ ];
829
1087
  var STYLE_ID = "oksigenia-access-effects";
830
1088
  var FILTERS_ID = "oksigenia-access-filters";
831
1089
  var GUIDE_ID = "oks-reading-guide";
@@ -860,18 +1118,23 @@ function ensureGlobalStyles() {
860
1118
  }
861
1119
  var VALID_POSITIONS = [
862
1120
  "top-left",
1121
+ "top-center",
863
1122
  "top-right",
864
1123
  "mid-left",
1124
+ "mid-center",
865
1125
  "mid-right",
866
1126
  "bottom-left",
1127
+ "bottom-center",
867
1128
  "bottom-right"
868
1129
  ];
869
- var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal"];
1130
+ var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal", "porthole"];
1131
+ var fxSeq = 0;
870
1132
  var OksigeniaAccessPanelElement = class extends HTMLElement {
871
1133
  static get observedAttributes() {
872
1134
  return OBSERVED;
873
1135
  }
874
- _dispose = null;
1136
+ _controller = null;
1137
+ _fxId = `oks-access-fx-${++fxSeq}`;
875
1138
  constructor() {
876
1139
  super();
877
1140
  this.attachShadow({ mode: "open" });
@@ -881,12 +1144,24 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
881
1144
  this.render();
882
1145
  }
883
1146
  disconnectedCallback() {
884
- this._dispose?.();
885
- this._dispose = null;
1147
+ this._controller?.();
1148
+ this._controller = null;
1149
+ if (typeof document !== "undefined") document.getElementById(this._fxId)?.remove();
886
1150
  }
887
1151
  attributeChangedCallback() {
888
1152
  if (this.isConnected) this.render();
889
1153
  }
1154
+ /** Imperative API (#2): open / close / toggle the panel from the host.
1155
+ * Useful with `trigger="none"`, where the host drives its own button. */
1156
+ open() {
1157
+ this._controller?.open();
1158
+ }
1159
+ close() {
1160
+ this._controller?.close();
1161
+ }
1162
+ toggle() {
1163
+ this._controller?.toggle();
1164
+ }
890
1165
  getPosition() {
891
1166
  const attr = this.getAttribute("position") ?? "mid-left";
892
1167
  return VALID_POSITIONS.includes(attr) ? attr : "mid-left";
@@ -902,21 +1177,59 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
902
1177
  getLocale() {
903
1178
  return this.getAttribute("locale") ?? (typeof navigator !== "undefined" ? navigator.language : "en");
904
1179
  }
1180
+ /** `nudge` (bare) ⇒ default 80px cap; `nudge="50"` ⇒ 50; absent/invalid ⇒ off. */
1181
+ getNudgeMax() {
1182
+ if (!this.hasAttribute("nudge")) return 0;
1183
+ const raw = this.getAttribute("nudge");
1184
+ if (raw == null || raw.trim() === "") return 80;
1185
+ const n = parseInt(raw, 10);
1186
+ return Number.isFinite(n) && n > 0 ? n : 0;
1187
+ }
1188
+ /** Inject the per-instance effects-exclude rule (#4): under high-contrast,
1189
+ * drop the destructive filter from selectors the host marks as essential
1190
+ * media (video, canvas, scientific imagery). Grayscale is handled by not
1191
+ * offering the control (curation), not here. */
1192
+ updateEffectsExclude() {
1193
+ if (typeof document === "undefined") return;
1194
+ const sel = (this.getAttribute("effects-exclude") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1195
+ const existing = document.getElementById(this._fxId);
1196
+ if (sel.length === 0) {
1197
+ existing?.remove();
1198
+ return;
1199
+ }
1200
+ const style = existing ?? document.createElement("style");
1201
+ if (!existing) {
1202
+ style.id = this._fxId;
1203
+ document.head.appendChild(style);
1204
+ }
1205
+ const list = sel.join(", ");
1206
+ style.textContent = `body.oks-a11y-contrast :is(${list}), body.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
1207
+ }
905
1208
  render() {
906
1209
  const shadow = this.shadowRoot;
907
1210
  if (!shadow) return;
908
- this._dispose?.();
1211
+ this._controller?.();
909
1212
  const position = this.getPosition();
910
1213
  const positionMobile = this.getPositionMobile();
1214
+ const enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1215
+ const showTrigger = this.getAttribute("trigger") !== "none";
1216
+ const showPresets = (this.getAttribute("presets") ?? "").trim().toLowerCase() !== "none";
911
1217
  const html = buildPanelHtml({
912
1218
  t: getTranslation(this.getLocale()),
913
1219
  triggerIcon: this.getTriggerIcon(),
914
- position
1220
+ position,
1221
+ enabled,
1222
+ showTrigger,
1223
+ showPresets
915
1224
  });
916
1225
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
917
- this._dispose = bindPanelBehavior(shadow, {
918
- storageKey: this.getAttribute("storage-key") ?? void 0
1226
+ this._controller = bindPanelBehavior(shadow, {
1227
+ storageKey: this.getAttribute("storage-key") ?? void 0,
1228
+ locale: this.getLocale(),
1229
+ enabled,
1230
+ nudgeMax: this.getNudgeMax()
919
1231
  });
1232
+ this.updateEffectsExclude();
920
1233
  }
921
1234
  };
922
1235
  if (typeof customElements !== "undefined" && !customElements.get("oksigenia-access-panel")) {