@oksigenia/access-panel 0.4.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,75 +37,185 @@ var ICON_COLORBLIND = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12
37
37
  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>';
38
38
  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>';
39
39
  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>';
40
+ 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>';
40
41
  var TRIGGER_ICONS = {
41
42
  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>',
42
43
  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>',
43
44
  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>',
44
- 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>'
45
+ universal: UNIVERSAL_GLYPH,
46
+ porthole: UNIVERSAL_GLYPH
45
47
  };
46
48
  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>';
47
49
  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>';
48
50
 
51
+ // src/controls.ts
52
+ var ALL_CONTROLS = [
53
+ "text-size",
54
+ "line-height",
55
+ "text-align",
56
+ "readable-font",
57
+ "dyslexia-font",
58
+ "letter-spacing",
59
+ "contrast",
60
+ "grayscale",
61
+ "hide-images",
62
+ "highlight-links",
63
+ "colorblind",
64
+ "reading-guide",
65
+ "reading-mask",
66
+ "big-cursor",
67
+ "big-targets",
68
+ "pause-anim",
69
+ "focus"
70
+ ];
71
+ var CONTROL_STATE_KEY = {
72
+ "text-size": "zoom",
73
+ "line-height": "lh",
74
+ "text-align": "align",
75
+ "readable-font": "font",
76
+ "dyslexia-font": "dyslexia",
77
+ "letter-spacing": "ls",
78
+ "contrast": "contrast",
79
+ "grayscale": "grayOverlay",
80
+ "hide-images": "hideImages",
81
+ "highlight-links": "highlightLinks",
82
+ "colorblind": "colorblind",
83
+ "reading-guide": "readingGuide",
84
+ "reading-mask": "readingMask",
85
+ "big-cursor": "bigCursor",
86
+ "big-targets": "bigTargets",
87
+ "pause-anim": "pauseAnim",
88
+ "focus": "focusOutline"
89
+ };
90
+ var STATE_KEY_CONTROL = Object.fromEntries(
91
+ Object.entries(CONTROL_STATE_KEY).map(([control, key]) => [key, control])
92
+ );
93
+ var PRESETS = {
94
+ lowvision: { zoom: 2, contrast: true, highlightLinks: true, bigCursor: true, focusOutline: true },
95
+ dyslexia: { dyslexia: true, lh: 2, ls: 2, readingGuide: true },
96
+ motor: { bigCursor: true, bigTargets: true, focusOutline: true },
97
+ calm: { hideImages: true, pauseAnim: true }
98
+ };
99
+ var PRESET_IDS = ["lowvision", "dyslexia", "motor", "calm"];
100
+ function presetControlIds(id) {
101
+ return Object.keys(PRESETS[id]).map((k) => STATE_KEY_CONTROL[k]);
102
+ }
103
+ function resolveEnabledControls(controlsAttr, excludeAttr) {
104
+ const valid = new Set(ALL_CONTROLS);
105
+ const parse = (raw) => (raw ?? "").split(",").map((s) => s.trim().toLowerCase()).filter((s) => valid.has(s));
106
+ const whitelist = controlsAttr != null ? parse(controlsAttr) : null;
107
+ let enabled = whitelist && whitelist.length > 0 ? new Set(whitelist) : new Set(ALL_CONTROLS);
108
+ if (excludeAttr != null) {
109
+ for (const id of parse(excludeAttr)) enabled.delete(id);
110
+ }
111
+ return enabled;
112
+ }
113
+ var NON_SCOPABLE_CONTROLS = [
114
+ "grayscale",
115
+ "reading-guide",
116
+ "reading-mask",
117
+ "big-cursor",
118
+ "colorblind"
119
+ ];
120
+ function scopedControls(enabled) {
121
+ const out = new Set(enabled);
122
+ for (const id of NON_SCOPABLE_CONTROLS) out.delete(id);
123
+ return out;
124
+ }
125
+ function filterPresetForEnabled(id, enabled) {
126
+ const out = {};
127
+ for (const [k, v] of Object.entries(PRESETS[id])) {
128
+ if (enabled.has(STATE_KEY_CONTROL[k])) out[k] = v;
129
+ }
130
+ return out;
131
+ }
132
+ function presetIsAvailable(id, enabled) {
133
+ return presetControlIds(id).filter((c) => enabled.has(c)).length >= 2;
134
+ }
135
+
49
136
  // src/render.ts
137
+ var PRESET_ICON = {
138
+ lowvision: ICON_CONTRAST,
139
+ dyslexia: ICON_DYSLEXIA,
140
+ motor: ICON_CURSOR,
141
+ calm: ICON_HIDE
142
+ };
50
143
  function buildPanelHtml(opts) {
51
144
  const { t, triggerIcon } = opts;
145
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
146
+ const showTrigger = opts.showTrigger ?? true;
147
+ const showPresets = opts.showPresets ?? true;
52
148
  const trig = TRIGGER_ICONS[triggerIcon];
53
- const grid = (rows) => `<div class="oks-access-grid">${rows.join("")}</div>`;
54
- const multi = (action, prefix, levels, label, icon, full = false) => {
149
+ const multi = (id, prefix, levels, label, icon, full = false) => {
150
+ if (!enabled.has(id)) return "";
151
+ const action = id === "colorblind" ? "colorblind" : "multi";
55
152
  const fullClass = full ? " full-width" : "";
56
153
  const dots = Array.from({ length: levels }, () => "<span></span>").join("");
57
- 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>`;
154
+ 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>`;
58
155
  };
59
- 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>`;
60
- 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>`;
61
- 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>`;
62
- 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>`;
63
- 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>`;
64
- return `
156
+ const toggle = (id, klass, label, icon) => {
157
+ if (!enabled.has(id)) return "";
158
+ 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>`;
159
+ };
160
+ const overlay = (id, target, label, icon) => {
161
+ if (!enabled.has(id)) return "";
162
+ 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>`;
163
+ };
164
+ const simple = (id, action, label, icon) => {
165
+ if (!enabled.has(id)) return "";
166
+ 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>`;
167
+ };
168
+ 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>`;
169
+ const section = (title, rows) => {
170
+ const kept = rows.filter(Boolean);
171
+ if (kept.length === 0) return "";
172
+ return `<h4 class="oks-access-title">${escapeHtml(title)}</h4><div class="oks-access-grid">${kept.join("")}</div>`;
173
+ };
174
+ const presetLabels = {
175
+ lowvision: t.pLow,
176
+ dyslexia: t.pDys,
177
+ motor: t.pMot,
178
+ calm: t.pCalm
179
+ };
180
+ const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
181
+ const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
182
+ const triggerBlock = showTrigger ? `
65
183
  <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
66
- <button class="oks-access-btn" id="oks-trigger" aria-label="${escapeAttr(t.title)}" aria-expanded="false" aria-controls="oks-panel" type="button">
184
+ <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">
67
185
  ${trig}
68
186
  </button>
69
187
  <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>
70
- </div>
188
+ </div>` : "";
189
+ return `${triggerBlock}
71
190
  <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
72
191
  <div class="oks-access-header">
73
192
  <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
74
193
  <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
75
194
  </div>
76
195
  <div class="oks-access-content">
77
- <h4 class="oks-access-title">${escapeHtml(t.presets)}</h4>
78
- <div class="oks-access-presets">
79
- ${preset("lowvision", t.pLow, ICON_CONTRAST)}
80
- ${preset("dyslexia", t.pDys, ICON_DYSLEXIA)}
81
- ${preset("motor", t.pMot, ICON_CURSOR)}
82
- ${preset("calm", t.pCalm, ICON_HIDE)}
83
- </div>
84
- <h4 class="oks-access-title">${escapeHtml(t.txt)}</h4>
85
- ${grid([
86
- multi("multi", "oks-zoom", 4, t.size, ICON_TXT),
87
- multi("multi", "oks-lh", 3, t.lh, ICON_LH),
88
- multi("multi", "oks-align", 3, t.align, ICON_ALIGN),
89
- toggle("oks-a11y-font", t.font, ICON_FONT),
90
- toggle("oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
91
- multi("multi", "oks-ls", 3, t.ls, ICON_LS)
196
+ ${presetsBlock}
197
+ ${section(t.txt, [
198
+ multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
199
+ multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
200
+ multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
201
+ toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
202
+ toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
203
+ multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
92
204
  ])}
93
- <h4 class="oks-access-title">${escapeHtml(t.vis)}</h4>
94
- ${grid([
95
- toggle("oks-a11y-contrast", t.contrast, ICON_CONTRAST),
96
- overlay("oks-overlay-gray", t.gray, ICON_GRAY),
97
- toggle("oks-a11y-hide", t.hide, ICON_HIDE),
98
- toggle("oks-a11y-links", t.links, ICON_LINK),
205
+ ${section(t.vis, [
206
+ toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
207
+ overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
208
+ toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
209
+ toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
99
210
  multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
100
211
  ])}
101
- <h4 class="oks-access-title">${escapeHtml(t.ori)}</h4>
102
- ${grid([
103
- guide(t.guide, ICON_GUIDE),
104
- mask(t.mask, ICON_MASK),
105
- toggle("oks-big-cursor", t.cursor, ICON_CURSOR),
106
- toggle("oks-a11y-bigtargets", t.targets, ICON_TARGETS),
107
- toggle("oks-a11y-pause", t.pause, ICON_PAUSE),
108
- toggle("oks-a11y-focus", t.focus, ICON_FOCUS)
212
+ ${section(t.ori, [
213
+ simple("reading-guide", "guide", t.guide, ICON_GUIDE),
214
+ simple("reading-mask", "mask", t.mask, ICON_MASK),
215
+ toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
216
+ toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
217
+ toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
218
+ toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
109
219
  ])}
110
220
  </div>
111
221
  <div class="oks-access-footer">
@@ -130,14 +240,20 @@ function positionRules(position) {
130
240
  switch (position) {
131
241
  case "top-left":
132
242
  return { wrap: "top: 20px; left: 20px;", panel: "top: 80px; left: 20px;" };
243
+ case "top-center":
244
+ return { wrap: "top: 20px; left: 50%; transform: translateX(-50%);", panel: "top: 80px; left: 50%; transform: translateX(-50%);" };
133
245
  case "top-right":
134
246
  return { wrap: "top: 20px; right: 20px;", panel: "top: 80px; right: 20px;" };
135
247
  case "mid-left":
136
248
  return { wrap: "top: 50%; left: 20px; transform: translateY(-50%);", panel: "top: 50%; left: 90px; transform: translateY(-50%);" };
249
+ case "mid-center":
250
+ return { wrap: "top: 50%; left: 50%; transform: translate(-50%, -50%);", panel: "top: 50%; left: 50%; transform: translate(-50%, -50%);" };
137
251
  case "mid-right":
138
252
  return { wrap: "top: 50%; right: 20px; transform: translateY(-50%);", panel: "top: 50%; right: 90px; transform: translateY(-50%);" };
139
253
  case "bottom-left":
140
254
  return { wrap: "bottom: 20px; left: 20px;", panel: "bottom: 100px; left: 20px;" };
255
+ case "bottom-center":
256
+ return { wrap: "bottom: 20px; left: 50%; transform: translateX(-50%);", panel: "bottom: 100px; left: 50%; transform: translateX(-50%);" };
141
257
  case "bottom-right":
142
258
  return { wrap: "bottom: 20px; right: 20px;", panel: "bottom: 100px; right: 20px;" };
143
259
  }
@@ -231,51 +347,64 @@ var TOGGLE_KEYS = {
231
347
  "oks-a11y-focus": "focusOutline",
232
348
  "oks-a11y-bigtargets": "bigTargets"
233
349
  };
234
- var PRESETS = {
235
- lowvision: { zoom: 2, contrast: true, highlightLinks: true, bigCursor: true, focusOutline: true },
236
- dyslexia: { dyslexia: true, lh: 2, ls: 2, readingGuide: true },
237
- motor: { bigCursor: true, bigTargets: true, focusOutline: true },
238
- calm: { hideImages: true, pauseAnim: true }
239
- };
350
+ function noopController() {
351
+ return Object.assign(() => {
352
+ }, { open() {
353
+ }, close() {
354
+ }, toggle() {
355
+ } });
356
+ }
357
+ function deepActiveElement() {
358
+ let a = document.activeElement;
359
+ while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
360
+ return a;
361
+ }
240
362
  function bindPanelBehavior(root, opts = {}) {
241
363
  const storageKey = opts.storageKey ?? "oksiacSettings";
242
364
  const t = getTranslation(opts.locale ?? "en");
365
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
366
+ const nudgeMax = Math.max(0, opts.nudgeMax ?? 0);
243
367
  const trigger = root.getElementById("oks-trigger");
244
368
  const panel = root.getElementById("oks-panel");
245
369
  const closeBtn = root.getElementById("oks-close");
246
370
  const resetBtn = root.getElementById("oks-reset");
247
371
  const wrapper = root.getElementById("oks-wrapper");
248
372
  const opts$ = Array.from(root.querySelectorAll(".oks-access-opt, .oks-preset"));
249
- if (!trigger || !panel || !closeBtn || !resetBtn || !wrapper) {
250
- return () => {
251
- };
373
+ if (!panel || !closeBtn || !resetBtn) {
374
+ return noopController();
252
375
  }
253
376
  let state = loadState(storageKey);
254
377
  function applyState() {
255
- const body = document.body;
256
- const root2 = document.documentElement;
257
- for (const cls of Array.from(body.classList)) {
258
- if (cls.startsWith("oks-")) body.classList.remove(cls);
378
+ const scoped = opts.scopeEl !== void 0;
379
+ const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
380
+ const rootEl = document.documentElement;
381
+ if (fxRoot) {
382
+ for (const cls of Array.from(fxRoot.classList)) {
383
+ if (cls.startsWith("oks-")) fxRoot.classList.remove(cls);
384
+ }
385
+ }
386
+ if (!scoped) [1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
387
+ if (fxRoot) {
388
+ if (state.zoom > 0) fxRoot.classList.add(`oks-zoom-${state.zoom}`);
389
+ if (state.lh > 0) fxRoot.classList.add(`oks-lh-${state.lh}`);
390
+ if (state.align > 0) fxRoot.classList.add(`oks-align-${state.align}`);
391
+ if (state.ls > 0) fxRoot.classList.add(`oks-ls-${state.ls}`);
392
+ if (state.font) fxRoot.classList.add("oks-a11y-font");
393
+ if (state.dyslexia) fxRoot.classList.add("oks-dyslexia");
394
+ if (state.contrast) fxRoot.classList.add("oks-a11y-contrast");
395
+ if (state.hideImages) fxRoot.classList.add("oks-a11y-hide");
396
+ if (state.highlightLinks) fxRoot.classList.add("oks-a11y-links");
397
+ if (state.pauseAnim) fxRoot.classList.add("oks-a11y-pause");
398
+ if (state.focusOutline) fxRoot.classList.add("oks-a11y-focus");
399
+ if (state.bigTargets) fxRoot.classList.add("oks-a11y-bigtargets");
400
+ }
401
+ if (!scoped) {
402
+ if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
403
+ if (state.bigCursor) document.body.classList.add("oks-big-cursor");
404
+ if (state.readingGuide) document.body.classList.add("oks-a11y-guide");
405
+ if (state.readingMask) document.body.classList.add("oks-a11y-mask");
406
+ ensureOverlay().classList.toggle("is-active", state.grayOverlay);
259
407
  }
260
- [1, 2, 3].forEach((l) => root2.classList.remove(`oks-colorblind-${l}`));
261
- if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
262
- if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
263
- if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
264
- if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
265
- if (state.colorblind > 0) root2.classList.add(`oks-colorblind-${state.colorblind}`);
266
- if (state.font) body.classList.add("oks-a11y-font");
267
- if (state.dyslexia) body.classList.add("oks-dyslexia");
268
- if (state.contrast) body.classList.add("oks-a11y-contrast");
269
- if (state.hideImages) body.classList.add("oks-a11y-hide");
270
- if (state.highlightLinks) body.classList.add("oks-a11y-links");
271
- if (state.bigCursor) body.classList.add("oks-big-cursor");
272
- if (state.pauseAnim) body.classList.add("oks-a11y-pause");
273
- if (state.focusOutline) body.classList.add("oks-a11y-focus");
274
- if (state.readingGuide) body.classList.add("oks-a11y-guide");
275
- if (state.readingMask) body.classList.add("oks-a11y-mask");
276
- if (state.bigTargets) body.classList.add("oks-a11y-bigtargets");
277
- const overlay = ensureOverlay();
278
- overlay.classList.toggle("is-active", state.grayOverlay);
279
408
  syncButtonsFromState();
280
409
  wrapper?.classList.toggle("has-active", !isStateEmpty(state));
281
410
  }
@@ -339,9 +468,8 @@ function bindPanelBehavior(root, opts = {}) {
339
468
  } else if (action === "mask") {
340
469
  state.readingMask = !state.readingMask;
341
470
  } else if (action === "preset") {
342
- const id = btn.getAttribute("data-preset") ?? "";
343
- const preset = PRESETS[id];
344
- if (preset) Object.assign(state, preset);
471
+ const id = btn.getAttribute("data-preset");
472
+ if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
345
473
  btn.classList.add("is-flashing");
346
474
  setTimeout(() => btn.classList.remove("is-flashing"), 250);
347
475
  }
@@ -353,28 +481,63 @@ function bindPanelBehavior(root, opts = {}) {
353
481
  applyState();
354
482
  saveState(storageKey, state);
355
483
  };
484
+ let opener = null;
485
+ let ignoreDocClose = false;
486
+ const positionScopedDialog = () => {
487
+ const el = opts.scopeEl;
488
+ if (!el) return;
489
+ const r = el.getBoundingClientRect();
490
+ if (r.width === 0 && r.height === 0) return;
491
+ const pad = 8;
492
+ panel.style.top = `${Math.max(pad, r.top + pad)}px`;
493
+ panel.style.bottom = "auto";
494
+ panel.style.left = `${r.left + r.width / 2}px`;
495
+ panel.style.right = "auto";
496
+ panel.style.transform = "translateX(-50%)";
497
+ };
356
498
  const openPanel = () => {
499
+ if (panel.classList.contains("is-open")) return;
500
+ opener = deepActiveElement();
357
501
  panel.classList.add("is-open");
358
502
  panel.removeAttribute("inert");
359
- trigger.setAttribute("aria-expanded", "true");
503
+ if (opts.scopeEl) positionScopedDialog();
504
+ trigger?.setAttribute("aria-expanded", "true");
505
+ ignoreDocClose = true;
506
+ setTimeout(() => {
507
+ ignoreDocClose = false;
508
+ }, 0);
360
509
  const first = panel.querySelector("button:not([disabled])");
361
510
  first?.focus();
362
511
  };
363
512
  const closePanel = () => {
513
+ if (!panel.classList.contains("is-open")) return;
364
514
  panel.classList.remove("is-open");
365
515
  panel.setAttribute("inert", "");
366
- trigger.setAttribute("aria-expanded", "false");
367
- trigger.focus();
516
+ trigger?.setAttribute("aria-expanded", "false");
517
+ if (trigger) trigger.focus();
518
+ else if (opener?.isConnected) opener.focus();
519
+ opener = null;
368
520
  };
369
- const onTriggerClick = (e) => {
370
- e.stopPropagation();
521
+ const togglePanel = () => {
371
522
  if (panel.classList.contains("is-open")) closePanel();
372
523
  else openPanel();
373
524
  };
525
+ let suppressClick = false;
526
+ const onTriggerClick = (e) => {
527
+ e.stopPropagation();
528
+ if (suppressClick) {
529
+ suppressClick = false;
530
+ return;
531
+ }
532
+ togglePanel();
533
+ };
374
534
  const onDocClick = (e) => {
375
535
  if (!panel.classList.contains("is-open")) return;
536
+ if (ignoreDocClose) return;
376
537
  const path = e.composedPath();
377
- if (path.includes(panel) || path.includes(trigger) || path.includes(wrapper)) return;
538
+ if (path.includes(panel)) return;
539
+ if (trigger && path.includes(trigger)) return;
540
+ if (wrapper && path.includes(wrapper)) return;
378
541
  closePanel();
379
542
  };
380
543
  const onKeyDown = (e) => {
@@ -401,6 +564,88 @@ function bindPanelBehavior(root, opts = {}) {
401
564
  }
402
565
  }
403
566
  };
567
+ const NUDGE_KEY = `${storageKey}::pos`;
568
+ const clampMax = (v) => Math.max(-nudgeMax, Math.min(nudgeMax, v));
569
+ let nudge = { x: 0, y: 0 };
570
+ const loadNudge = () => {
571
+ if (nudgeMax <= 0) return;
572
+ try {
573
+ const raw = localStorage.getItem(NUDGE_KEY);
574
+ if (!raw) return;
575
+ const p = JSON.parse(raw);
576
+ if (typeof p?.x === "number" && typeof p?.y === "number") {
577
+ nudge = { x: clampMax(p.x), y: clampMax(p.y) };
578
+ }
579
+ } catch {
580
+ }
581
+ };
582
+ const saveNudge = () => {
583
+ try {
584
+ if (nudge.x === 0 && nudge.y === 0) localStorage.removeItem(NUDGE_KEY);
585
+ else localStorage.setItem(NUDGE_KEY, JSON.stringify(nudge));
586
+ } catch {
587
+ }
588
+ };
589
+ const applyNudge = () => {
590
+ if (wrapper) wrapper.style.translate = `${nudge.x}px ${nudge.y}px`;
591
+ };
592
+ const clampOffset = (x, y) => {
593
+ x = clampMax(x);
594
+ y = clampMax(y);
595
+ if (wrapper) {
596
+ const r = wrapper.getBoundingClientRect();
597
+ const pad = 4;
598
+ const baseLeft = r.left - nudge.x;
599
+ const baseTop = r.top - nudge.y;
600
+ const vw = window.innerWidth || 0;
601
+ const vh = window.innerHeight || 0;
602
+ if (r.width > 0 && vw > 0) x = Math.max(pad - baseLeft, Math.min(vw - pad - r.width - baseLeft, x));
603
+ if (r.height > 0 && vh > 0) y = Math.max(pad - baseTop, Math.min(vh - pad - r.height - baseTop, y));
604
+ }
605
+ return { x, y };
606
+ };
607
+ let dragStart = null;
608
+ let didDrag = false;
609
+ const DRAG_THRESHOLD = 4;
610
+ const onPointerDown = (e) => {
611
+ if (nudgeMax <= 0 || e.button !== 0) return;
612
+ dragStart = { px: e.clientX, py: e.clientY, ox: nudge.x, oy: nudge.y };
613
+ didDrag = false;
614
+ trigger?.setPointerCapture?.(e.pointerId);
615
+ };
616
+ const onPointerMove = (e) => {
617
+ if (!dragStart) return;
618
+ const dx = e.clientX - dragStart.px;
619
+ const dy = e.clientY - dragStart.py;
620
+ if (!didDrag && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
621
+ didDrag = true;
622
+ nudge = clampOffset(dragStart.ox + dx, dragStart.oy + dy);
623
+ applyNudge();
624
+ e.preventDefault();
625
+ };
626
+ const onPointerUp = () => {
627
+ if (!dragStart) return;
628
+ dragStart = null;
629
+ if (didDrag) {
630
+ saveNudge();
631
+ suppressClick = true;
632
+ }
633
+ };
634
+ const onTriggerKey = (e) => {
635
+ if (nudgeMax <= 0) return;
636
+ const step = e.shiftKey ? 1 : 10;
637
+ let dx = 0;
638
+ let dy = 0;
639
+ if (e.key === "ArrowLeft") dx = -step;
640
+ else if (e.key === "ArrowRight") dx = step;
641
+ else if (e.key === "ArrowUp") dy = -step;
642
+ else if (e.key === "ArrowDown") dy = step;
643
+ else return;
644
+ e.preventDefault();
645
+ nudge = clampOffset(nudge.x + dx, nudge.y + dy);
646
+ applyNudge();
647
+ saveNudge();
648
+ };
404
649
  const onMove = (e) => {
405
650
  if (!state.readingGuide && !state.readingMask) return;
406
651
  const y = e.touches?.[0]?.clientY ?? e.clientY;
@@ -414,7 +659,7 @@ function bindPanelBehavior(root, opts = {}) {
414
659
  if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
415
660
  }
416
661
  };
417
- trigger.addEventListener("click", onTriggerClick);
662
+ trigger?.addEventListener("click", onTriggerClick);
418
663
  closeBtn.addEventListener("click", closePanel);
419
664
  resetBtn.addEventListener("click", onReset);
420
665
  for (const btn of opts$) btn.addEventListener("click", onOptClick);
@@ -422,9 +667,26 @@ function bindPanelBehavior(root, opts = {}) {
422
667
  document.addEventListener("keydown", onKeyDown);
423
668
  document.addEventListener("mousemove", onMove);
424
669
  document.addEventListener("touchmove", onMove, { passive: true });
670
+ const onReposition = () => {
671
+ if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
672
+ };
673
+ if (opts.scopeEl !== void 0) {
674
+ window.addEventListener("scroll", onReposition, true);
675
+ window.addEventListener("resize", onReposition);
676
+ }
677
+ if (nudgeMax > 0 && trigger) {
678
+ loadNudge();
679
+ applyNudge();
680
+ trigger.style.cursor = "grab";
681
+ trigger.style.touchAction = "none";
682
+ trigger.addEventListener("pointerdown", onPointerDown);
683
+ document.addEventListener("pointermove", onPointerMove);
684
+ document.addEventListener("pointerup", onPointerUp);
685
+ trigger.addEventListener("keydown", onTriggerKey);
686
+ }
425
687
  applyState();
426
- return () => {
427
- trigger.removeEventListener("click", onTriggerClick);
688
+ const dispose = () => {
689
+ trigger?.removeEventListener("click", onTriggerClick);
428
690
  closeBtn.removeEventListener("click", closePanel);
429
691
  resetBtn.removeEventListener("click", onReset);
430
692
  for (const btn of opts$) btn.removeEventListener("click", onOptClick);
@@ -432,7 +694,16 @@ function bindPanelBehavior(root, opts = {}) {
432
694
  document.removeEventListener("keydown", onKeyDown);
433
695
  document.removeEventListener("mousemove", onMove);
434
696
  document.removeEventListener("touchmove", onMove);
697
+ if (opts.scopeEl !== void 0) {
698
+ window.removeEventListener("scroll", onReposition, true);
699
+ window.removeEventListener("resize", onReposition);
700
+ }
701
+ trigger?.removeEventListener("pointerdown", onPointerDown);
702
+ document.removeEventListener("pointermove", onPointerMove);
703
+ document.removeEventListener("pointerup", onPointerUp);
704
+ trigger?.removeEventListener("keydown", onTriggerKey);
435
705
  };
706
+ return Object.assign(dispose, { open: openPanel, close: closePanel, toggle: togglePanel });
436
707
  }
437
708
  function ensureOverlay() {
438
709
  let el = document.getElementById("oks-overlay-gray");
@@ -487,6 +758,12 @@ var PANEL_CSS = `
487
758
  width: 60%;
488
759
  height: 60%;
489
760
  }
761
+ /* Porthole preset: a brushed-metal ring framing the standard glyph. The glyph
762
+ itself is untouched (discoverability), only the button gets the frame.
763
+ Hosts wanting a bespoke frame can target ::part(trigger) from light DOM. */
764
+ .oks-access-btn[data-trigger-icon="porthole"] {
765
+ box-shadow: 0 0 0 4px #b0bec5, 0 0 0 7px #546e7a, 0 6px 18px rgba(0,0,0,0.3);
766
+ }
490
767
  .oks-active-badge {
491
768
  position: absolute;
492
769
  top: -2px;
@@ -837,7 +1114,81 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
837
1114
  box-sizing: border-box !important;
838
1115
  }
839
1116
  `;
1117
+ function scopedEffectCss(s) {
1118
+ return `
1119
+ ${s}.oks-zoom-1 { font-size: 110% !important; }
1120
+ ${s}.oks-zoom-2 { font-size: 120% !important; }
1121
+ ${s}.oks-zoom-3 { font-size: 135% !important; }
1122
+ ${s}.oks-zoom-4 { font-size: 150% !important; }
1123
+
1124
+ ${s}.oks-lh-1 * { line-height: 1.6 !important; }
1125
+ ${s}.oks-lh-2 * { line-height: 1.9 !important; }
1126
+ ${s}.oks-lh-3 * { line-height: 2.2 !important; }
1127
+
1128
+ ${s}.oks-a11y-font, ${s}.oks-a11y-font * { font-family: Arial, sans-serif !important; }
1129
+
1130
+ ${s}.oks-dyslexia * {
1131
+ font-family: 'Comic Sans MS', 'Verdana', sans-serif !important;
1132
+ letter-spacing: 0.05em !important;
1133
+ word-spacing: 0.1em !important;
1134
+ line-height: 1.6 !important;
1135
+ }
1136
+
1137
+ ${s}.oks-a11y-hide img { opacity: 0 !important; visibility: hidden !important; }
1138
+ ${s}.oks-a11y-links a { text-decoration: underline !important; background: #ff0 !important; color: #000 !important; }
1139
+
1140
+ ${s}.oks-align-1 * { text-align: left !important; }
1141
+ ${s}.oks-align-2 * { text-align: center !important; }
1142
+ ${s}.oks-align-3 * { text-align: right !important; }
1143
+
1144
+ ${s}.oks-a11y-pause * { animation: none !important; transition: none !important; }
1145
+
1146
+ ${s}.oks-ls-1 * { letter-spacing: 0.05em !important; }
1147
+ ${s}.oks-ls-2 * { letter-spacing: 0.10em !important; }
1148
+ ${s}.oks-ls-3 * { letter-spacing: 0.16em !important; }
1149
+
1150
+ ${s}.oks-a11y-focus a,
1151
+ ${s}.oks-a11y-focus button,
1152
+ ${s}.oks-a11y-focus input,
1153
+ ${s}.oks-a11y-focus select,
1154
+ ${s}.oks-a11y-focus textarea {
1155
+ outline: 2px dashed rgba(0, 95, 204, 0.45) !important;
1156
+ outline-offset: 2px !important;
1157
+ }
1158
+ ${s}.oks-a11y-focus *:focus-visible {
1159
+ outline: 3px solid #005fcc !important;
1160
+ outline-offset: 3px !important;
1161
+ box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25) !important;
1162
+ }
1163
+
1164
+ ${s}.oks-a11y-contrast, ${s}.oks-a11y-contrast * {
1165
+ background-color: #000 !important;
1166
+ color: #ff0 !important;
1167
+ border-color: #ff0 !important;
1168
+ text-shadow: none !important;
1169
+ box-shadow: none !important;
1170
+ }
1171
+ ${s}.oks-a11y-contrast img { filter: grayscale(100%) contrast(120%) !important; }
1172
+ ${s}.oks-a11y-contrast a { color: #0ff !important; text-decoration: underline !important; }
1173
+ ${s}.oks-a11y-contrast.oks-a11y-focus *:focus-visible {
1174
+ outline-color: #0ff !important;
1175
+ box-shadow: 0 0 0 6px rgba(0, 255, 255, 0.3) !important;
1176
+ }
1177
+
1178
+ ${s}.oks-a11y-bigtargets a,
1179
+ ${s}.oks-a11y-bigtargets button,
1180
+ ${s}.oks-a11y-bigtargets [role="button"],
1181
+ ${s}.oks-a11y-bigtargets input[type="checkbox"],
1182
+ ${s}.oks-a11y-bigtargets input[type="radio"],
1183
+ ${s}.oks-a11y-bigtargets summary {
1184
+ min-height: 44px !important;
1185
+ min-width: 44px !important;
1186
+ padding: 8px 12px !important;
1187
+ box-sizing: border-box !important;
1188
+ }
1189
+ `;
1190
+ }
840
1191
 
841
- export { COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, PANEL_CSS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, saveState, supportedLocales };
1192
+ export { ALL_CONTROLS, COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, NON_SCOPABLE_CONTROLS, PANEL_CSS, PRESETS, PRESET_IDS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, scopedControls, scopedEffectCss, supportedLocales };
842
1193
  //# sourceMappingURL=index.js.map
843
1194
  //# sourceMappingURL=index.js.map