@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.
@@ -1,3 +1,88 @@
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
+ var NON_SCOPABLE_CONTROLS = [
64
+ "grayscale",
65
+ "reading-guide",
66
+ "reading-mask",
67
+ "big-cursor",
68
+ "colorblind"
69
+ ];
70
+ function scopedControls(enabled) {
71
+ const out = new Set(enabled);
72
+ for (const id of NON_SCOPABLE_CONTROLS) out.delete(id);
73
+ return out;
74
+ }
75
+ function filterPresetForEnabled(id, enabled) {
76
+ const out = {};
77
+ for (const [k, v] of Object.entries(PRESETS[id])) {
78
+ if (enabled.has(STATE_KEY_CONTROL[k])) out[k] = v;
79
+ }
80
+ return out;
81
+ }
82
+ function presetIsAvailable(id, enabled) {
83
+ return presetControlIds(id).filter((c) => enabled.has(c)).length >= 2;
84
+ }
85
+
1
86
  // src/icons.ts
2
87
  var ICON_TXT = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.5,4v3h5v12h3V7h5V4H2.5z M21.5,9h-9v3h3v7h3v-7h3V9z"/></svg>';
3
88
  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 +101,100 @@ var ICON_COLORBLIND = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12
16
101
  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
102
  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
103
  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>';
104
+ 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
105
  var TRIGGER_ICONS = {
20
106
  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
107
  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
108
  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>'
109
+ universal: UNIVERSAL_GLYPH,
110
+ porthole: UNIVERSAL_GLYPH
24
111
  };
25
112
  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
113
  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
114
 
28
115
  // src/render.ts
116
+ var PRESET_ICON = {
117
+ lowvision: ICON_CONTRAST,
118
+ dyslexia: ICON_DYSLEXIA,
119
+ motor: ICON_CURSOR,
120
+ calm: ICON_HIDE
121
+ };
29
122
  function buildPanelHtml(opts) {
30
123
  const { t, triggerIcon } = opts;
124
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
125
+ const showTrigger = opts.showTrigger ?? true;
126
+ const showPresets = opts.showPresets ?? true;
31
127
  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) => {
128
+ const multi = (id, prefix, levels, label, icon, full = false) => {
129
+ if (!enabled.has(id)) return "";
130
+ const action = id === "colorblind" ? "colorblind" : "multi";
34
131
  const fullClass = full ? " full-width" : "";
35
132
  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>`;
133
+ 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>`;
37
134
  };
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 `
135
+ const toggle = (id, klass, label, icon) => {
136
+ if (!enabled.has(id)) return "";
137
+ 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>`;
138
+ };
139
+ const overlay = (id, target, label, icon) => {
140
+ if (!enabled.has(id)) return "";
141
+ 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>`;
142
+ };
143
+ const simple = (id, action, label, icon) => {
144
+ if (!enabled.has(id)) return "";
145
+ 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>`;
146
+ };
147
+ 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>`;
148
+ const section = (title, rows) => {
149
+ const kept = rows.filter(Boolean);
150
+ if (kept.length === 0) return "";
151
+ return `<h4 class="oks-access-title">${escapeHtml(title)}</h4><div class="oks-access-grid">${kept.join("")}</div>`;
152
+ };
153
+ const presetLabels = {
154
+ lowvision: t.pLow,
155
+ dyslexia: t.pDys,
156
+ motor: t.pMot,
157
+ calm: t.pCalm
158
+ };
159
+ const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
160
+ const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
161
+ const triggerBlock = showTrigger ? `
44
162
  <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">
163
+ <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
164
  ${trig}
47
165
  </button>
48
166
  <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>
167
+ </div>` : "";
168
+ return `${triggerBlock}
50
169
  <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
51
170
  <div class="oks-access-header">
52
171
  <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
53
172
  <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
54
173
  </div>
55
174
  <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)
175
+ ${presetsBlock}
176
+ ${section(t.txt, [
177
+ multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
178
+ multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
179
+ multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
180
+ toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
181
+ toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
182
+ multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
71
183
  ])}
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),
184
+ ${section(t.vis, [
185
+ toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
186
+ overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
187
+ toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
188
+ toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
78
189
  multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
79
190
  ])}
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)
191
+ ${section(t.ori, [
192
+ simple("reading-guide", "guide", t.guide, ICON_GUIDE),
193
+ simple("reading-mask", "mask", t.mask, ICON_MASK),
194
+ toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
195
+ toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
196
+ toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
197
+ toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
88
198
  ])}
89
199
  </div>
90
200
  <div class="oks-access-footer">
@@ -109,14 +219,20 @@ function positionRules(position) {
109
219
  switch (position) {
110
220
  case "top-left":
111
221
  return { wrap: "top: 20px; left: 20px;", panel: "top: 80px; left: 20px;" };
222
+ case "top-center":
223
+ return { wrap: "top: 20px; left: 50%; transform: translateX(-50%);", panel: "top: 80px; left: 50%; transform: translateX(-50%);" };
112
224
  case "top-right":
113
225
  return { wrap: "top: 20px; right: 20px;", panel: "top: 80px; right: 20px;" };
114
226
  case "mid-left":
115
227
  return { wrap: "top: 50%; left: 20px; transform: translateY(-50%);", panel: "top: 50%; left: 90px; transform: translateY(-50%);" };
228
+ case "mid-center":
229
+ return { wrap: "top: 50%; left: 50%; transform: translate(-50%, -50%);", panel: "top: 50%; left: 50%; transform: translate(-50%, -50%);" };
116
230
  case "mid-right":
117
231
  return { wrap: "top: 50%; right: 20px; transform: translateY(-50%);", panel: "top: 50%; right: 90px; transform: translateY(-50%);" };
118
232
  case "bottom-left":
119
233
  return { wrap: "bottom: 20px; left: 20px;", panel: "bottom: 100px; left: 20px;" };
234
+ case "bottom-center":
235
+ return { wrap: "bottom: 20px; left: 50%; transform: translateX(-50%);", panel: "bottom: 100px; left: 50%; transform: translateX(-50%);" };
120
236
  case "bottom-right":
121
237
  return { wrap: "bottom: 20px; right: 20px;", panel: "bottom: 100px; right: 20px;" };
122
238
  }
@@ -228,51 +344,64 @@ var TOGGLE_KEYS = {
228
344
  "oks-a11y-focus": "focusOutline",
229
345
  "oks-a11y-bigtargets": "bigTargets"
230
346
  };
231
- var PRESETS = {
232
- lowvision: { zoom: 2, contrast: true, highlightLinks: true, bigCursor: true, focusOutline: true },
233
- dyslexia: { dyslexia: true, lh: 2, ls: 2, readingGuide: true },
234
- motor: { bigCursor: true, bigTargets: true, focusOutline: true },
235
- calm: { hideImages: true, pauseAnim: true }
236
- };
347
+ function noopController() {
348
+ return Object.assign(() => {
349
+ }, { open() {
350
+ }, close() {
351
+ }, toggle() {
352
+ } });
353
+ }
354
+ function deepActiveElement() {
355
+ let a = document.activeElement;
356
+ while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
357
+ return a;
358
+ }
237
359
  function bindPanelBehavior(root, opts = {}) {
238
360
  const storageKey = opts.storageKey ?? "oksiacSettings";
239
361
  const t = getTranslation(opts.locale ?? "en");
362
+ const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
363
+ const nudgeMax = Math.max(0, opts.nudgeMax ?? 0);
240
364
  const trigger = root.getElementById("oks-trigger");
241
365
  const panel = root.getElementById("oks-panel");
242
366
  const closeBtn = root.getElementById("oks-close");
243
367
  const resetBtn = root.getElementById("oks-reset");
244
368
  const wrapper = root.getElementById("oks-wrapper");
245
369
  const opts$ = Array.from(root.querySelectorAll(".oks-access-opt, .oks-preset"));
246
- if (!trigger || !panel || !closeBtn || !resetBtn || !wrapper) {
247
- return () => {
248
- };
370
+ if (!panel || !closeBtn || !resetBtn) {
371
+ return noopController();
249
372
  }
250
373
  let state = loadState(storageKey);
251
374
  function applyState() {
252
- const body = document.body;
253
- const root2 = document.documentElement;
254
- for (const cls of Array.from(body.classList)) {
255
- if (cls.startsWith("oks-")) body.classList.remove(cls);
375
+ const scoped = opts.scopeEl !== void 0;
376
+ const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
377
+ const rootEl = document.documentElement;
378
+ if (fxRoot) {
379
+ for (const cls of Array.from(fxRoot.classList)) {
380
+ if (cls.startsWith("oks-")) fxRoot.classList.remove(cls);
381
+ }
382
+ }
383
+ if (!scoped) [1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
384
+ if (fxRoot) {
385
+ if (state.zoom > 0) fxRoot.classList.add(`oks-zoom-${state.zoom}`);
386
+ if (state.lh > 0) fxRoot.classList.add(`oks-lh-${state.lh}`);
387
+ if (state.align > 0) fxRoot.classList.add(`oks-align-${state.align}`);
388
+ if (state.ls > 0) fxRoot.classList.add(`oks-ls-${state.ls}`);
389
+ if (state.font) fxRoot.classList.add("oks-a11y-font");
390
+ if (state.dyslexia) fxRoot.classList.add("oks-dyslexia");
391
+ if (state.contrast) fxRoot.classList.add("oks-a11y-contrast");
392
+ if (state.hideImages) fxRoot.classList.add("oks-a11y-hide");
393
+ if (state.highlightLinks) fxRoot.classList.add("oks-a11y-links");
394
+ if (state.pauseAnim) fxRoot.classList.add("oks-a11y-pause");
395
+ if (state.focusOutline) fxRoot.classList.add("oks-a11y-focus");
396
+ if (state.bigTargets) fxRoot.classList.add("oks-a11y-bigtargets");
397
+ }
398
+ if (!scoped) {
399
+ if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
400
+ if (state.bigCursor) document.body.classList.add("oks-big-cursor");
401
+ if (state.readingGuide) document.body.classList.add("oks-a11y-guide");
402
+ if (state.readingMask) document.body.classList.add("oks-a11y-mask");
403
+ ensureOverlay().classList.toggle("is-active", state.grayOverlay);
256
404
  }
257
- [1, 2, 3].forEach((l) => root2.classList.remove(`oks-colorblind-${l}`));
258
- if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
259
- if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
260
- if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
261
- if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
262
- if (state.colorblind > 0) root2.classList.add(`oks-colorblind-${state.colorblind}`);
263
- if (state.font) body.classList.add("oks-a11y-font");
264
- if (state.dyslexia) body.classList.add("oks-dyslexia");
265
- if (state.contrast) body.classList.add("oks-a11y-contrast");
266
- if (state.hideImages) body.classList.add("oks-a11y-hide");
267
- if (state.highlightLinks) body.classList.add("oks-a11y-links");
268
- if (state.bigCursor) body.classList.add("oks-big-cursor");
269
- if (state.pauseAnim) body.classList.add("oks-a11y-pause");
270
- if (state.focusOutline) body.classList.add("oks-a11y-focus");
271
- if (state.readingGuide) body.classList.add("oks-a11y-guide");
272
- if (state.readingMask) body.classList.add("oks-a11y-mask");
273
- if (state.bigTargets) body.classList.add("oks-a11y-bigtargets");
274
- const overlay = ensureOverlay();
275
- overlay.classList.toggle("is-active", state.grayOverlay);
276
405
  syncButtonsFromState();
277
406
  wrapper?.classList.toggle("has-active", !isStateEmpty(state));
278
407
  }
@@ -336,9 +465,8 @@ function bindPanelBehavior(root, opts = {}) {
336
465
  } else if (action === "mask") {
337
466
  state.readingMask = !state.readingMask;
338
467
  } else if (action === "preset") {
339
- const id = btn.getAttribute("data-preset") ?? "";
340
- const preset = PRESETS[id];
341
- if (preset) Object.assign(state, preset);
468
+ const id = btn.getAttribute("data-preset");
469
+ if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
342
470
  btn.classList.add("is-flashing");
343
471
  setTimeout(() => btn.classList.remove("is-flashing"), 250);
344
472
  }
@@ -350,28 +478,63 @@ function bindPanelBehavior(root, opts = {}) {
350
478
  applyState();
351
479
  saveState(storageKey, state);
352
480
  };
481
+ let opener = null;
482
+ let ignoreDocClose = false;
483
+ const positionScopedDialog = () => {
484
+ const el = opts.scopeEl;
485
+ if (!el) return;
486
+ const r = el.getBoundingClientRect();
487
+ if (r.width === 0 && r.height === 0) return;
488
+ const pad = 8;
489
+ panel.style.top = `${Math.max(pad, r.top + pad)}px`;
490
+ panel.style.bottom = "auto";
491
+ panel.style.left = `${r.left + r.width / 2}px`;
492
+ panel.style.right = "auto";
493
+ panel.style.transform = "translateX(-50%)";
494
+ };
353
495
  const openPanel = () => {
496
+ if (panel.classList.contains("is-open")) return;
497
+ opener = deepActiveElement();
354
498
  panel.classList.add("is-open");
355
499
  panel.removeAttribute("inert");
356
- trigger.setAttribute("aria-expanded", "true");
500
+ if (opts.scopeEl) positionScopedDialog();
501
+ trigger?.setAttribute("aria-expanded", "true");
502
+ ignoreDocClose = true;
503
+ setTimeout(() => {
504
+ ignoreDocClose = false;
505
+ }, 0);
357
506
  const first = panel.querySelector("button:not([disabled])");
358
507
  first?.focus();
359
508
  };
360
509
  const closePanel = () => {
510
+ if (!panel.classList.contains("is-open")) return;
361
511
  panel.classList.remove("is-open");
362
512
  panel.setAttribute("inert", "");
363
- trigger.setAttribute("aria-expanded", "false");
364
- trigger.focus();
513
+ trigger?.setAttribute("aria-expanded", "false");
514
+ if (trigger) trigger.focus();
515
+ else if (opener?.isConnected) opener.focus();
516
+ opener = null;
365
517
  };
366
- const onTriggerClick = (e) => {
367
- e.stopPropagation();
518
+ const togglePanel = () => {
368
519
  if (panel.classList.contains("is-open")) closePanel();
369
520
  else openPanel();
370
521
  };
522
+ let suppressClick = false;
523
+ const onTriggerClick = (e) => {
524
+ e.stopPropagation();
525
+ if (suppressClick) {
526
+ suppressClick = false;
527
+ return;
528
+ }
529
+ togglePanel();
530
+ };
371
531
  const onDocClick = (e) => {
372
532
  if (!panel.classList.contains("is-open")) return;
533
+ if (ignoreDocClose) return;
373
534
  const path = e.composedPath();
374
- if (path.includes(panel) || path.includes(trigger) || path.includes(wrapper)) return;
535
+ if (path.includes(panel)) return;
536
+ if (trigger && path.includes(trigger)) return;
537
+ if (wrapper && path.includes(wrapper)) return;
375
538
  closePanel();
376
539
  };
377
540
  const onKeyDown = (e) => {
@@ -398,6 +561,88 @@ function bindPanelBehavior(root, opts = {}) {
398
561
  }
399
562
  }
400
563
  };
564
+ const NUDGE_KEY = `${storageKey}::pos`;
565
+ const clampMax = (v) => Math.max(-nudgeMax, Math.min(nudgeMax, v));
566
+ let nudge = { x: 0, y: 0 };
567
+ const loadNudge = () => {
568
+ if (nudgeMax <= 0) return;
569
+ try {
570
+ const raw = localStorage.getItem(NUDGE_KEY);
571
+ if (!raw) return;
572
+ const p = JSON.parse(raw);
573
+ if (typeof p?.x === "number" && typeof p?.y === "number") {
574
+ nudge = { x: clampMax(p.x), y: clampMax(p.y) };
575
+ }
576
+ } catch {
577
+ }
578
+ };
579
+ const saveNudge = () => {
580
+ try {
581
+ if (nudge.x === 0 && nudge.y === 0) localStorage.removeItem(NUDGE_KEY);
582
+ else localStorage.setItem(NUDGE_KEY, JSON.stringify(nudge));
583
+ } catch {
584
+ }
585
+ };
586
+ const applyNudge = () => {
587
+ if (wrapper) wrapper.style.translate = `${nudge.x}px ${nudge.y}px`;
588
+ };
589
+ const clampOffset = (x, y) => {
590
+ x = clampMax(x);
591
+ y = clampMax(y);
592
+ if (wrapper) {
593
+ const r = wrapper.getBoundingClientRect();
594
+ const pad = 4;
595
+ const baseLeft = r.left - nudge.x;
596
+ const baseTop = r.top - nudge.y;
597
+ const vw = window.innerWidth || 0;
598
+ const vh = window.innerHeight || 0;
599
+ if (r.width > 0 && vw > 0) x = Math.max(pad - baseLeft, Math.min(vw - pad - r.width - baseLeft, x));
600
+ if (r.height > 0 && vh > 0) y = Math.max(pad - baseTop, Math.min(vh - pad - r.height - baseTop, y));
601
+ }
602
+ return { x, y };
603
+ };
604
+ let dragStart = null;
605
+ let didDrag = false;
606
+ const DRAG_THRESHOLD = 4;
607
+ const onPointerDown = (e) => {
608
+ if (nudgeMax <= 0 || e.button !== 0) return;
609
+ dragStart = { px: e.clientX, py: e.clientY, ox: nudge.x, oy: nudge.y };
610
+ didDrag = false;
611
+ trigger?.setPointerCapture?.(e.pointerId);
612
+ };
613
+ const onPointerMove = (e) => {
614
+ if (!dragStart) return;
615
+ const dx = e.clientX - dragStart.px;
616
+ const dy = e.clientY - dragStart.py;
617
+ if (!didDrag && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
618
+ didDrag = true;
619
+ nudge = clampOffset(dragStart.ox + dx, dragStart.oy + dy);
620
+ applyNudge();
621
+ e.preventDefault();
622
+ };
623
+ const onPointerUp = () => {
624
+ if (!dragStart) return;
625
+ dragStart = null;
626
+ if (didDrag) {
627
+ saveNudge();
628
+ suppressClick = true;
629
+ }
630
+ };
631
+ const onTriggerKey = (e) => {
632
+ if (nudgeMax <= 0) return;
633
+ const step = e.shiftKey ? 1 : 10;
634
+ let dx = 0;
635
+ let dy = 0;
636
+ if (e.key === "ArrowLeft") dx = -step;
637
+ else if (e.key === "ArrowRight") dx = step;
638
+ else if (e.key === "ArrowUp") dy = -step;
639
+ else if (e.key === "ArrowDown") dy = step;
640
+ else return;
641
+ e.preventDefault();
642
+ nudge = clampOffset(nudge.x + dx, nudge.y + dy);
643
+ applyNudge();
644
+ saveNudge();
645
+ };
401
646
  const onMove = (e) => {
402
647
  if (!state.readingGuide && !state.readingMask) return;
403
648
  const y = e.touches?.[0]?.clientY ?? e.clientY;
@@ -411,7 +656,7 @@ function bindPanelBehavior(root, opts = {}) {
411
656
  if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
412
657
  }
413
658
  };
414
- trigger.addEventListener("click", onTriggerClick);
659
+ trigger?.addEventListener("click", onTriggerClick);
415
660
  closeBtn.addEventListener("click", closePanel);
416
661
  resetBtn.addEventListener("click", onReset);
417
662
  for (const btn of opts$) btn.addEventListener("click", onOptClick);
@@ -419,9 +664,26 @@ function bindPanelBehavior(root, opts = {}) {
419
664
  document.addEventListener("keydown", onKeyDown);
420
665
  document.addEventListener("mousemove", onMove);
421
666
  document.addEventListener("touchmove", onMove, { passive: true });
667
+ const onReposition = () => {
668
+ if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
669
+ };
670
+ if (opts.scopeEl !== void 0) {
671
+ window.addEventListener("scroll", onReposition, true);
672
+ window.addEventListener("resize", onReposition);
673
+ }
674
+ if (nudgeMax > 0 && trigger) {
675
+ loadNudge();
676
+ applyNudge();
677
+ trigger.style.cursor = "grab";
678
+ trigger.style.touchAction = "none";
679
+ trigger.addEventListener("pointerdown", onPointerDown);
680
+ document.addEventListener("pointermove", onPointerMove);
681
+ document.addEventListener("pointerup", onPointerUp);
682
+ trigger.addEventListener("keydown", onTriggerKey);
683
+ }
422
684
  applyState();
423
- return () => {
424
- trigger.removeEventListener("click", onTriggerClick);
685
+ const dispose = () => {
686
+ trigger?.removeEventListener("click", onTriggerClick);
425
687
  closeBtn.removeEventListener("click", closePanel);
426
688
  resetBtn.removeEventListener("click", onReset);
427
689
  for (const btn of opts$) btn.removeEventListener("click", onOptClick);
@@ -429,7 +691,16 @@ function bindPanelBehavior(root, opts = {}) {
429
691
  document.removeEventListener("keydown", onKeyDown);
430
692
  document.removeEventListener("mousemove", onMove);
431
693
  document.removeEventListener("touchmove", onMove);
694
+ if (opts.scopeEl !== void 0) {
695
+ window.removeEventListener("scroll", onReposition, true);
696
+ window.removeEventListener("resize", onReposition);
697
+ }
698
+ trigger?.removeEventListener("pointerdown", onPointerDown);
699
+ document.removeEventListener("pointermove", onPointerMove);
700
+ document.removeEventListener("pointerup", onPointerUp);
701
+ trigger?.removeEventListener("keydown", onTriggerKey);
432
702
  };
703
+ return Object.assign(dispose, { open: openPanel, close: closePanel, toggle: togglePanel });
433
704
  }
434
705
  function ensureOverlay() {
435
706
  let el = document.getElementById("oks-overlay-gray");
@@ -484,6 +755,12 @@ var PANEL_CSS = `
484
755
  width: 60%;
485
756
  height: 60%;
486
757
  }
758
+ /* Porthole preset: a brushed-metal ring framing the standard glyph. The glyph
759
+ itself is untouched (discoverability), only the button gets the frame.
760
+ Hosts wanting a bespoke frame can target ::part(trigger) from light DOM. */
761
+ .oks-access-btn[data-trigger-icon="porthole"] {
762
+ box-shadow: 0 0 0 4px #b0bec5, 0 0 0 7px #546e7a, 0 6px 18px rgba(0,0,0,0.3);
763
+ }
487
764
  .oks-active-badge {
488
765
  position: absolute;
489
766
  top: -2px;
@@ -834,9 +1111,96 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
834
1111
  box-sizing: border-box !important;
835
1112
  }
836
1113
  `;
1114
+ function scopedEffectCss(s) {
1115
+ return `
1116
+ ${s}.oks-zoom-1 { font-size: 110% !important; }
1117
+ ${s}.oks-zoom-2 { font-size: 120% !important; }
1118
+ ${s}.oks-zoom-3 { font-size: 135% !important; }
1119
+ ${s}.oks-zoom-4 { font-size: 150% !important; }
1120
+
1121
+ ${s}.oks-lh-1 * { line-height: 1.6 !important; }
1122
+ ${s}.oks-lh-2 * { line-height: 1.9 !important; }
1123
+ ${s}.oks-lh-3 * { line-height: 2.2 !important; }
1124
+
1125
+ ${s}.oks-a11y-font, ${s}.oks-a11y-font * { font-family: Arial, sans-serif !important; }
1126
+
1127
+ ${s}.oks-dyslexia * {
1128
+ font-family: 'Comic Sans MS', 'Verdana', sans-serif !important;
1129
+ letter-spacing: 0.05em !important;
1130
+ word-spacing: 0.1em !important;
1131
+ line-height: 1.6 !important;
1132
+ }
1133
+
1134
+ ${s}.oks-a11y-hide img { opacity: 0 !important; visibility: hidden !important; }
1135
+ ${s}.oks-a11y-links a { text-decoration: underline !important; background: #ff0 !important; color: #000 !important; }
1136
+
1137
+ ${s}.oks-align-1 * { text-align: left !important; }
1138
+ ${s}.oks-align-2 * { text-align: center !important; }
1139
+ ${s}.oks-align-3 * { text-align: right !important; }
1140
+
1141
+ ${s}.oks-a11y-pause * { animation: none !important; transition: none !important; }
1142
+
1143
+ ${s}.oks-ls-1 * { letter-spacing: 0.05em !important; }
1144
+ ${s}.oks-ls-2 * { letter-spacing: 0.10em !important; }
1145
+ ${s}.oks-ls-3 * { letter-spacing: 0.16em !important; }
1146
+
1147
+ ${s}.oks-a11y-focus a,
1148
+ ${s}.oks-a11y-focus button,
1149
+ ${s}.oks-a11y-focus input,
1150
+ ${s}.oks-a11y-focus select,
1151
+ ${s}.oks-a11y-focus textarea {
1152
+ outline: 2px dashed rgba(0, 95, 204, 0.45) !important;
1153
+ outline-offset: 2px !important;
1154
+ }
1155
+ ${s}.oks-a11y-focus *:focus-visible {
1156
+ outline: 3px solid #005fcc !important;
1157
+ outline-offset: 3px !important;
1158
+ box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25) !important;
1159
+ }
1160
+
1161
+ ${s}.oks-a11y-contrast, ${s}.oks-a11y-contrast * {
1162
+ background-color: #000 !important;
1163
+ color: #ff0 !important;
1164
+ border-color: #ff0 !important;
1165
+ text-shadow: none !important;
1166
+ box-shadow: none !important;
1167
+ }
1168
+ ${s}.oks-a11y-contrast img { filter: grayscale(100%) contrast(120%) !important; }
1169
+ ${s}.oks-a11y-contrast a { color: #0ff !important; text-decoration: underline !important; }
1170
+ ${s}.oks-a11y-contrast.oks-a11y-focus *:focus-visible {
1171
+ outline-color: #0ff !important;
1172
+ box-shadow: 0 0 0 6px rgba(0, 255, 255, 0.3) !important;
1173
+ }
1174
+
1175
+ ${s}.oks-a11y-bigtargets a,
1176
+ ${s}.oks-a11y-bigtargets button,
1177
+ ${s}.oks-a11y-bigtargets [role="button"],
1178
+ ${s}.oks-a11y-bigtargets input[type="checkbox"],
1179
+ ${s}.oks-a11y-bigtargets input[type="radio"],
1180
+ ${s}.oks-a11y-bigtargets summary {
1181
+ min-height: 44px !important;
1182
+ min-width: 44px !important;
1183
+ padding: 8px 12px !important;
1184
+ box-sizing: border-box !important;
1185
+ }
1186
+ `;
1187
+ }
837
1188
 
838
1189
  // src/web-component.ts
839
- var OBSERVED = ["locale", "position", "position-mobile", "trigger-icon", "storage-key"];
1190
+ var OBSERVED = [
1191
+ "locale",
1192
+ "position",
1193
+ "position-mobile",
1194
+ "trigger-icon",
1195
+ "storage-key",
1196
+ "controls",
1197
+ "exclude",
1198
+ "trigger",
1199
+ "effects-exclude",
1200
+ "nudge",
1201
+ "presets",
1202
+ "scope"
1203
+ ];
840
1204
  var STYLE_ID = "oksigenia-access-effects";
841
1205
  var FILTERS_ID = "oksigenia-access-filters";
842
1206
  var GUIDE_ID = "oks-reading-guide";
@@ -871,18 +1235,24 @@ function ensureGlobalStyles() {
871
1235
  }
872
1236
  var VALID_POSITIONS = [
873
1237
  "top-left",
1238
+ "top-center",
874
1239
  "top-right",
875
1240
  "mid-left",
1241
+ "mid-center",
876
1242
  "mid-right",
877
1243
  "bottom-left",
1244
+ "bottom-center",
878
1245
  "bottom-right"
879
1246
  ];
880
- var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal"];
1247
+ var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal", "porthole"];
1248
+ var fxSeq = 0;
881
1249
  var OksigeniaAccessPanelElement = class extends HTMLElement {
882
1250
  static get observedAttributes() {
883
1251
  return OBSERVED;
884
1252
  }
885
- _dispose = null;
1253
+ _controller = null;
1254
+ _fxId = `oks-access-fx-${++fxSeq}`;
1255
+ _scopeId = `oks-access-scope-${this._fxId.split("-").pop()}`;
886
1256
  constructor() {
887
1257
  super();
888
1258
  this.attachShadow({ mode: "open" });
@@ -892,12 +1262,27 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
892
1262
  this.render();
893
1263
  }
894
1264
  disconnectedCallback() {
895
- this._dispose?.();
896
- this._dispose = null;
1265
+ this._controller?.();
1266
+ this._controller = null;
1267
+ if (typeof document !== "undefined") {
1268
+ document.getElementById(this._fxId)?.remove();
1269
+ document.getElementById(this._scopeId)?.remove();
1270
+ }
897
1271
  }
898
1272
  attributeChangedCallback() {
899
1273
  if (this.isConnected) this.render();
900
1274
  }
1275
+ /** Imperative API (#2): open / close / toggle the panel from the host.
1276
+ * Useful with `trigger="none"`, where the host drives its own button. */
1277
+ open() {
1278
+ this._controller?.open();
1279
+ }
1280
+ close() {
1281
+ this._controller?.close();
1282
+ }
1283
+ toggle() {
1284
+ this._controller?.toggle();
1285
+ }
901
1286
  getPosition() {
902
1287
  const attr = this.getAttribute("position") ?? "mid-left";
903
1288
  return VALID_POSITIONS.includes(attr) ? attr : "mid-left";
@@ -913,22 +1298,81 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
913
1298
  getLocale() {
914
1299
  return this.getAttribute("locale") ?? (typeof navigator !== "undefined" ? navigator.language : "en");
915
1300
  }
1301
+ /** `nudge` (bare) ⇒ default 80px cap; `nudge="50"` ⇒ 50; absent/invalid ⇒ off. */
1302
+ getNudgeMax() {
1303
+ if (!this.hasAttribute("nudge")) return 0;
1304
+ const raw = this.getAttribute("nudge");
1305
+ if (raw == null || raw.trim() === "") return 80;
1306
+ const n = parseInt(raw, 10);
1307
+ return Number.isFinite(n) && n > 0 ? n : 0;
1308
+ }
1309
+ /** Inject the per-instance effects-exclude rule (#4): under high-contrast,
1310
+ * drop the destructive filter from selectors the host marks as essential
1311
+ * media (video, canvas, scientific imagery). Grayscale is handled by not
1312
+ * offering the control (curation), not here. */
1313
+ updateEffectsExclude(base) {
1314
+ if (typeof document === "undefined") return;
1315
+ const sel = (this.getAttribute("effects-exclude") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1316
+ const existing = document.getElementById(this._fxId);
1317
+ if (sel.length === 0) {
1318
+ existing?.remove();
1319
+ return;
1320
+ }
1321
+ const style = existing ?? document.createElement("style");
1322
+ if (!existing) {
1323
+ style.id = this._fxId;
1324
+ document.head.appendChild(style);
1325
+ }
1326
+ const list = sel.join(", ");
1327
+ style.textContent = `${base}.oks-a11y-contrast :is(${list}), ${base}.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
1328
+ }
1329
+ /** Inject the per-instance scoped effect CSS (`scope=`): the scopable effects
1330
+ * anchored to the host's container instead of `body`, so several panels
1331
+ * regionalise their own zone without clobbering each other. */
1332
+ updateScopeStyle(scope) {
1333
+ if (typeof document === "undefined") return;
1334
+ const existing = document.getElementById(this._scopeId);
1335
+ if (!scope) {
1336
+ existing?.remove();
1337
+ return;
1338
+ }
1339
+ const style = existing ?? document.createElement("style");
1340
+ if (!existing) {
1341
+ style.id = this._scopeId;
1342
+ document.head.appendChild(style);
1343
+ }
1344
+ style.textContent = scopedEffectCss(scope);
1345
+ }
916
1346
  render() {
917
1347
  const shadow = this.shadowRoot;
918
1348
  if (!shadow) return;
919
- this._dispose?.();
1349
+ this._controller?.();
920
1350
  const position = this.getPosition();
921
1351
  const positionMobile = this.getPositionMobile();
1352
+ const scope = this.getAttribute("scope")?.trim() || null;
1353
+ let enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1354
+ if (scope) enabled = scopedControls(enabled);
1355
+ const showTrigger = this.getAttribute("trigger") !== "none";
1356
+ const showPresets = (this.getAttribute("presets") ?? "").trim().toLowerCase() !== "none";
922
1357
  const html = buildPanelHtml({
923
1358
  t: getTranslation(this.getLocale()),
924
1359
  triggerIcon: this.getTriggerIcon(),
925
- position
1360
+ position,
1361
+ enabled,
1362
+ showTrigger,
1363
+ showPresets
926
1364
  });
927
1365
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
928
- this._dispose = bindPanelBehavior(shadow, {
1366
+ const scopeEl = scope ? document.querySelector(scope) : void 0;
1367
+ this._controller = bindPanelBehavior(shadow, {
929
1368
  storageKey: this.getAttribute("storage-key") ?? void 0,
930
- locale: this.getLocale()
1369
+ locale: this.getLocale(),
1370
+ enabled,
1371
+ nudgeMax: this.getNudgeMax(),
1372
+ scopeEl
931
1373
  });
1374
+ this.updateScopeStyle(scope);
1375
+ this.updateEffectsExclude(scope ?? "body");
932
1376
  }
933
1377
  };
934
1378
  if (typeof customElements !== "undefined" && !customElements.get("oksigenia-access-panel")) {