@oksigenia/access-panel 0.5.0 → 0.6.1

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);
@@ -959,18 +1001,22 @@ body.oks-ls-1 * { letter-spacing: 0.05em !important; }
959
1001
  body.oks-ls-2 * { letter-spacing: 0.10em !important; }
960
1002
  body.oks-ls-3 * { letter-spacing: 0.16em !important; }
961
1003
 
1004
+ /* Focus highlight colour is themeable via --oks-focus-color (default #005fcc):
1005
+ set it on :root for a dark theme so the highlight stays visible. The
1006
+ persistent "all interactive" dashed derives a 45% tint from it; the glow can
1007
+ be overridden on its own with --oks-focus-glow. */
962
1008
  body.oks-a11y-focus a:not(oksigenia-access-panel):not(oksigenia-access-panel *),
963
1009
  body.oks-a11y-focus button:not(oksigenia-access-panel):not(oksigenia-access-panel *),
964
1010
  body.oks-a11y-focus input,
965
1011
  body.oks-a11y-focus select,
966
1012
  body.oks-a11y-focus textarea {
967
- outline: 2px dashed rgba(0, 95, 204, 0.45) !important;
1013
+ outline: 2px dashed color-mix(in srgb, var(--oks-focus-color, #005fcc) 45%, transparent) !important;
968
1014
  outline-offset: 2px !important;
969
1015
  }
970
1016
  body.oks-a11y-focus *:focus-visible {
971
- outline: 3px solid #005fcc !important;
1017
+ outline: 3px solid var(--oks-focus-color, #005fcc) !important;
972
1018
  outline-offset: 3px !important;
973
- box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25) !important;
1019
+ box-shadow: 0 0 0 6px var(--oks-focus-glow, color-mix(in srgb, var(--oks-focus-color, #005fcc) 25%, transparent)) !important;
974
1020
  }
975
1021
 
976
1022
  body.oks-a11y-contrast.oks-a11y-focus *:focus,
@@ -1069,6 +1115,80 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
1069
1115
  box-sizing: border-box !important;
1070
1116
  }
1071
1117
  `;
1118
+ function scopedEffectCss(s) {
1119
+ return `
1120
+ ${s}.oks-zoom-1 { font-size: 110% !important; }
1121
+ ${s}.oks-zoom-2 { font-size: 120% !important; }
1122
+ ${s}.oks-zoom-3 { font-size: 135% !important; }
1123
+ ${s}.oks-zoom-4 { font-size: 150% !important; }
1124
+
1125
+ ${s}.oks-lh-1 * { line-height: 1.6 !important; }
1126
+ ${s}.oks-lh-2 * { line-height: 1.9 !important; }
1127
+ ${s}.oks-lh-3 * { line-height: 2.2 !important; }
1128
+
1129
+ ${s}.oks-a11y-font, ${s}.oks-a11y-font * { font-family: Arial, sans-serif !important; }
1130
+
1131
+ ${s}.oks-dyslexia * {
1132
+ font-family: 'Comic Sans MS', 'Verdana', sans-serif !important;
1133
+ letter-spacing: 0.05em !important;
1134
+ word-spacing: 0.1em !important;
1135
+ line-height: 1.6 !important;
1136
+ }
1137
+
1138
+ ${s}.oks-a11y-hide img { opacity: 0 !important; visibility: hidden !important; }
1139
+ ${s}.oks-a11y-links a { text-decoration: underline !important; background: #ff0 !important; color: #000 !important; }
1140
+
1141
+ ${s}.oks-align-1 * { text-align: left !important; }
1142
+ ${s}.oks-align-2 * { text-align: center !important; }
1143
+ ${s}.oks-align-3 * { text-align: right !important; }
1144
+
1145
+ ${s}.oks-a11y-pause * { animation: none !important; transition: none !important; }
1146
+
1147
+ ${s}.oks-ls-1 * { letter-spacing: 0.05em !important; }
1148
+ ${s}.oks-ls-2 * { letter-spacing: 0.10em !important; }
1149
+ ${s}.oks-ls-3 * { letter-spacing: 0.16em !important; }
1150
+
1151
+ ${s}.oks-a11y-focus a,
1152
+ ${s}.oks-a11y-focus button,
1153
+ ${s}.oks-a11y-focus input,
1154
+ ${s}.oks-a11y-focus select,
1155
+ ${s}.oks-a11y-focus textarea {
1156
+ outline: 2px dashed color-mix(in srgb, var(--oks-focus-color, #005fcc) 45%, transparent) !important;
1157
+ outline-offset: 2px !important;
1158
+ }
1159
+ ${s}.oks-a11y-focus *:focus-visible {
1160
+ outline: 3px solid var(--oks-focus-color, #005fcc) !important;
1161
+ outline-offset: 3px !important;
1162
+ box-shadow: 0 0 0 6px var(--oks-focus-glow, color-mix(in srgb, var(--oks-focus-color, #005fcc) 25%, transparent)) !important;
1163
+ }
1164
+
1165
+ ${s}.oks-a11y-contrast, ${s}.oks-a11y-contrast * {
1166
+ background-color: #000 !important;
1167
+ color: #ff0 !important;
1168
+ border-color: #ff0 !important;
1169
+ text-shadow: none !important;
1170
+ box-shadow: none !important;
1171
+ }
1172
+ ${s}.oks-a11y-contrast img { filter: grayscale(100%) contrast(120%) !important; }
1173
+ ${s}.oks-a11y-contrast a { color: #0ff !important; text-decoration: underline !important; }
1174
+ ${s}.oks-a11y-contrast.oks-a11y-focus *:focus-visible {
1175
+ outline-color: #0ff !important;
1176
+ box-shadow: 0 0 0 6px rgba(0, 255, 255, 0.3) !important;
1177
+ }
1178
+
1179
+ ${s}.oks-a11y-bigtargets a,
1180
+ ${s}.oks-a11y-bigtargets button,
1181
+ ${s}.oks-a11y-bigtargets [role="button"],
1182
+ ${s}.oks-a11y-bigtargets input[type="checkbox"],
1183
+ ${s}.oks-a11y-bigtargets input[type="radio"],
1184
+ ${s}.oks-a11y-bigtargets summary {
1185
+ min-height: 44px !important;
1186
+ min-width: 44px !important;
1187
+ padding: 8px 12px !important;
1188
+ box-sizing: border-box !important;
1189
+ }
1190
+ `;
1191
+ }
1072
1192
 
1073
1193
  // src/web-component.ts
1074
1194
  var OBSERVED = [
@@ -1082,7 +1202,8 @@ var OBSERVED = [
1082
1202
  "trigger",
1083
1203
  "effects-exclude",
1084
1204
  "nudge",
1085
- "presets"
1205
+ "presets",
1206
+ "scope"
1086
1207
  ];
1087
1208
  var STYLE_ID = "oksigenia-access-effects";
1088
1209
  var FILTERS_ID = "oksigenia-access-filters";
@@ -1135,6 +1256,7 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1135
1256
  }
1136
1257
  _controller = null;
1137
1258
  _fxId = `oks-access-fx-${++fxSeq}`;
1259
+ _scopeId = `oks-access-scope-${this._fxId.split("-").pop()}`;
1138
1260
  constructor() {
1139
1261
  super();
1140
1262
  this.attachShadow({ mode: "open" });
@@ -1146,7 +1268,10 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1146
1268
  disconnectedCallback() {
1147
1269
  this._controller?.();
1148
1270
  this._controller = null;
1149
- if (typeof document !== "undefined") document.getElementById(this._fxId)?.remove();
1271
+ if (typeof document !== "undefined") {
1272
+ document.getElementById(this._fxId)?.remove();
1273
+ document.getElementById(this._scopeId)?.remove();
1274
+ }
1150
1275
  }
1151
1276
  attributeChangedCallback() {
1152
1277
  if (this.isConnected) this.render();
@@ -1189,7 +1314,7 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1189
1314
  * drop the destructive filter from selectors the host marks as essential
1190
1315
  * media (video, canvas, scientific imagery). Grayscale is handled by not
1191
1316
  * offering the control (curation), not here. */
1192
- updateEffectsExclude() {
1317
+ updateEffectsExclude(base) {
1193
1318
  if (typeof document === "undefined") return;
1194
1319
  const sel = (this.getAttribute("effects-exclude") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1195
1320
  const existing = document.getElementById(this._fxId);
@@ -1203,7 +1328,24 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1203
1328
  document.head.appendChild(style);
1204
1329
  }
1205
1330
  const list = sel.join(", ");
1206
- style.textContent = `body.oks-a11y-contrast :is(${list}), body.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
1331
+ style.textContent = `${base}.oks-a11y-contrast :is(${list}), ${base}.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
1332
+ }
1333
+ /** Inject the per-instance scoped effect CSS (`scope=`): the scopable effects
1334
+ * anchored to the host's container instead of `body`, so several panels
1335
+ * regionalise their own zone without clobbering each other. */
1336
+ updateScopeStyle(scope) {
1337
+ if (typeof document === "undefined") return;
1338
+ const existing = document.getElementById(this._scopeId);
1339
+ if (!scope) {
1340
+ existing?.remove();
1341
+ return;
1342
+ }
1343
+ const style = existing ?? document.createElement("style");
1344
+ if (!existing) {
1345
+ style.id = this._scopeId;
1346
+ document.head.appendChild(style);
1347
+ }
1348
+ style.textContent = scopedEffectCss(scope);
1207
1349
  }
1208
1350
  render() {
1209
1351
  const shadow = this.shadowRoot;
@@ -1211,7 +1353,9 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1211
1353
  this._controller?.();
1212
1354
  const position = this.getPosition();
1213
1355
  const positionMobile = this.getPositionMobile();
1214
- const enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1356
+ const scope = this.getAttribute("scope")?.trim() || null;
1357
+ let enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
1358
+ if (scope) enabled = scopedControls(enabled);
1215
1359
  const showTrigger = this.getAttribute("trigger") !== "none";
1216
1360
  const showPresets = (this.getAttribute("presets") ?? "").trim().toLowerCase() !== "none";
1217
1361
  const html = buildPanelHtml({
@@ -1223,13 +1367,16 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1223
1367
  showPresets
1224
1368
  });
1225
1369
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
1370
+ const scopeEl = scope ? document.querySelector(scope) : void 0;
1226
1371
  this._controller = bindPanelBehavior(shadow, {
1227
1372
  storageKey: this.getAttribute("storage-key") ?? void 0,
1228
1373
  locale: this.getLocale(),
1229
1374
  enabled,
1230
- nudgeMax: this.getNudgeMax()
1375
+ nudgeMax: this.getNudgeMax(),
1376
+ scopeEl
1231
1377
  });
1232
- this.updateEffectsExclude();
1378
+ this.updateScopeStyle(scope);
1379
+ this.updateEffectsExclude(scope ?? "body");
1233
1380
  }
1234
1381
  };
1235
1382
  if (typeof customElements !== "undefined" && !customElements.get("oksigenia-access-panel")) {