@oksigenia/access-panel 0.5.0 → 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.
@@ -60,6 +60,18 @@ function resolveEnabledControls(controlsAttr, excludeAttr) {
60
60
  }
61
61
  return enabled;
62
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
+ }
63
75
  function filterPresetForEnabled(id, enabled) {
64
76
  const out = {};
65
77
  for (const [k, v] of Object.entries(PRESETS[id])) {
@@ -360,30 +372,36 @@ function bindPanelBehavior(root, opts = {}) {
360
372
  }
361
373
  let state = loadState(storageKey);
362
374
  function applyState() {
363
- const body = document.body;
375
+ const scoped = opts.scopeEl !== void 0;
376
+ const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
364
377
  const rootEl = document.documentElement;
365
- for (const cls of Array.from(body.classList)) {
366
- if (cls.startsWith("oks-")) body.classList.remove(cls);
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);
367
404
  }
368
- [1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
369
- if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
370
- if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
371
- if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
372
- if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
373
- if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
374
- if (state.font) body.classList.add("oks-a11y-font");
375
- if (state.dyslexia) body.classList.add("oks-dyslexia");
376
- if (state.contrast) body.classList.add("oks-a11y-contrast");
377
- if (state.hideImages) body.classList.add("oks-a11y-hide");
378
- if (state.highlightLinks) body.classList.add("oks-a11y-links");
379
- if (state.bigCursor) body.classList.add("oks-big-cursor");
380
- if (state.pauseAnim) body.classList.add("oks-a11y-pause");
381
- if (state.focusOutline) body.classList.add("oks-a11y-focus");
382
- if (state.readingGuide) body.classList.add("oks-a11y-guide");
383
- if (state.readingMask) body.classList.add("oks-a11y-mask");
384
- if (state.bigTargets) body.classList.add("oks-a11y-bigtargets");
385
- const overlay = ensureOverlay();
386
- overlay.classList.toggle("is-active", state.grayOverlay);
387
405
  syncButtonsFromState();
388
406
  wrapper?.classList.toggle("has-active", !isStateEmpty(state));
389
407
  }
@@ -462,11 +480,24 @@ function bindPanelBehavior(root, opts = {}) {
462
480
  };
463
481
  let opener = null;
464
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
+ };
465
495
  const openPanel = () => {
466
496
  if (panel.classList.contains("is-open")) return;
467
497
  opener = deepActiveElement();
468
498
  panel.classList.add("is-open");
469
499
  panel.removeAttribute("inert");
500
+ if (opts.scopeEl) positionScopedDialog();
470
501
  trigger?.setAttribute("aria-expanded", "true");
471
502
  ignoreDocClose = true;
472
503
  setTimeout(() => {
@@ -633,6 +664,13 @@ function bindPanelBehavior(root, opts = {}) {
633
664
  document.addEventListener("keydown", onKeyDown);
634
665
  document.addEventListener("mousemove", onMove);
635
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
+ }
636
674
  if (nudgeMax > 0 && trigger) {
637
675
  loadNudge();
638
676
  applyNudge();
@@ -653,6 +691,10 @@ function bindPanelBehavior(root, opts = {}) {
653
691
  document.removeEventListener("keydown", onKeyDown);
654
692
  document.removeEventListener("mousemove", onMove);
655
693
  document.removeEventListener("touchmove", onMove);
694
+ if (opts.scopeEl !== void 0) {
695
+ window.removeEventListener("scroll", onReposition, true);
696
+ window.removeEventListener("resize", onReposition);
697
+ }
656
698
  trigger?.removeEventListener("pointerdown", onPointerDown);
657
699
  document.removeEventListener("pointermove", onPointerMove);
658
700
  document.removeEventListener("pointerup", onPointerUp);
@@ -1069,6 +1111,80 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
1069
1111
  box-sizing: border-box !important;
1070
1112
  }
1071
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
+ }
1072
1188
 
1073
1189
  // src/web-component.ts
1074
1190
  var OBSERVED = [
@@ -1082,7 +1198,8 @@ var OBSERVED = [
1082
1198
  "trigger",
1083
1199
  "effects-exclude",
1084
1200
  "nudge",
1085
- "presets"
1201
+ "presets",
1202
+ "scope"
1086
1203
  ];
1087
1204
  var STYLE_ID = "oksigenia-access-effects";
1088
1205
  var FILTERS_ID = "oksigenia-access-filters";
@@ -1135,6 +1252,7 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1135
1252
  }
1136
1253
  _controller = null;
1137
1254
  _fxId = `oks-access-fx-${++fxSeq}`;
1255
+ _scopeId = `oks-access-scope-${this._fxId.split("-").pop()}`;
1138
1256
  constructor() {
1139
1257
  super();
1140
1258
  this.attachShadow({ mode: "open" });
@@ -1146,7 +1264,10 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1146
1264
  disconnectedCallback() {
1147
1265
  this._controller?.();
1148
1266
  this._controller = null;
1149
- if (typeof document !== "undefined") document.getElementById(this._fxId)?.remove();
1267
+ if (typeof document !== "undefined") {
1268
+ document.getElementById(this._fxId)?.remove();
1269
+ document.getElementById(this._scopeId)?.remove();
1270
+ }
1150
1271
  }
1151
1272
  attributeChangedCallback() {
1152
1273
  if (this.isConnected) this.render();
@@ -1189,7 +1310,7 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1189
1310
  * drop the destructive filter from selectors the host marks as essential
1190
1311
  * media (video, canvas, scientific imagery). Grayscale is handled by not
1191
1312
  * offering the control (curation), not here. */
1192
- updateEffectsExclude() {
1313
+ updateEffectsExclude(base) {
1193
1314
  if (typeof document === "undefined") return;
1194
1315
  const sel = (this.getAttribute("effects-exclude") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1195
1316
  const existing = document.getElementById(this._fxId);
@@ -1203,7 +1324,24 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1203
1324
  document.head.appendChild(style);
1204
1325
  }
1205
1326
  const list = sel.join(", ");
1206
- style.textContent = `body.oks-a11y-contrast :is(${list}), body.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
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);
1207
1345
  }
1208
1346
  render() {
1209
1347
  const shadow = this.shadowRoot;
@@ -1211,7 +1349,9 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1211
1349
  this._controller?.();
1212
1350
  const position = this.getPosition();
1213
1351
  const positionMobile = this.getPositionMobile();
1214
- const enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1352
+ const scope = this.getAttribute("scope")?.trim() || null;
1353
+ let enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1354
+ if (scope) enabled = scopedControls(enabled);
1215
1355
  const showTrigger = this.getAttribute("trigger") !== "none";
1216
1356
  const showPresets = (this.getAttribute("presets") ?? "").trim().toLowerCase() !== "none";
1217
1357
  const html = buildPanelHtml({
@@ -1223,13 +1363,16 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1223
1363
  showPresets
1224
1364
  });
1225
1365
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
1366
+ const scopeEl = scope ? document.querySelector(scope) : void 0;
1226
1367
  this._controller = bindPanelBehavior(shadow, {
1227
1368
  storageKey: this.getAttribute("storage-key") ?? void 0,
1228
1369
  locale: this.getLocale(),
1229
1370
  enabled,
1230
- nudgeMax: this.getNudgeMax()
1371
+ nudgeMax: this.getNudgeMax(),
1372
+ scopeEl
1231
1373
  });
1232
- this.updateEffectsExclude();
1374
+ this.updateScopeStyle(scope);
1375
+ this.updateEffectsExclude(scope ?? "body");
1233
1376
  }
1234
1377
  };
1235
1378
  if (typeof customElements !== "undefined" && !customElements.get("oksigenia-access-panel")) {