@oksigenia/access-panel 0.6.1 → 0.7.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,13 +60,7 @@ 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
- ];
63
+ var NON_SCOPABLE_CONTROLS = ["grayscale", "colorblind"];
70
64
  function scopedControls(enabled) {
71
65
  const out = new Set(enabled);
72
66
  for (const id of NON_SCOPABLE_CONTROLS) out.delete(id);
@@ -124,6 +118,7 @@ function buildPanelHtml(opts) {
124
118
  const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
125
119
  const showTrigger = opts.showTrigger ?? true;
126
120
  const showPresets = opts.showPresets ?? true;
121
+ const flatLayout = opts.flatLayout ?? false;
127
122
  const trig = TRIGGER_ICONS[triggerIcon];
128
123
  const multi = (id, prefix, levels, label, icon, full = false) => {
129
124
  if (!enabled.has(id)) return "";
@@ -158,44 +153,61 @@ function buildPanelHtml(opts) {
158
153
  };
159
154
  const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
160
155
  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 ? `
162
- <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
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">
164
- ${trig}
165
- </button>
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>
167
- </div>` : "";
168
- return `${triggerBlock}
169
- <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
170
- <div class="oks-access-header">
171
- <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
172
- <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
173
- </div>
174
- <div class="oks-access-content">
175
- ${presetsBlock}
176
- ${section(t.txt, [
156
+ const textBtns = [
177
157
  multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
178
158
  multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
179
159
  multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
180
160
  toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
181
161
  toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
182
162
  multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
183
- ])}
184
- ${section(t.vis, [
163
+ ];
164
+ const visBtns = [
185
165
  toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
186
166
  overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
187
167
  toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
188
168
  toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
189
- multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
190
- ])}
191
- ${section(t.ori, [
169
+ multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, !flatLayout)
170
+ ];
171
+ const oriBtns = [
192
172
  simple("reading-guide", "guide", t.guide, ICON_GUIDE),
193
173
  simple("reading-mask", "mask", t.mask, ICON_MASK),
194
174
  toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
195
175
  toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
196
176
  toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
197
177
  toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
198
- ])}
178
+ ];
179
+ const addFullWidth = (btn) => btn.replace('class="oks-access-opt', 'class="oks-access-opt full-width');
180
+ const flatContent = () => {
181
+ const groups = [
182
+ { title: t.txt, items: textBtns.filter(Boolean) },
183
+ { title: t.vis, items: visBtns.filter(Boolean) },
184
+ { title: t.ori, items: oriBtns.filter(Boolean) }
185
+ ].filter((g) => g.items.length > 0);
186
+ const total = groups.reduce((n, g) => n + g.items.length, 0);
187
+ if (total % 2 === 1 && groups.length > 0) {
188
+ const last = groups[groups.length - 1];
189
+ last.items[last.items.length - 1] = addFullWidth(last.items[last.items.length - 1]);
190
+ }
191
+ const inner = groups.map((g) => `<h4 class="oks-access-title oks-sr-only">${escapeHtml(g.title)}</h4>${g.items.join("")}`).join("");
192
+ return `<div class="oks-access-grid oks-flat">${inner}</div>`;
193
+ };
194
+ const controlsHtml = flatLayout ? flatContent() : section(t.txt, textBtns) + section(t.vis, visBtns) + section(t.ori, oriBtns);
195
+ const triggerBlock = showTrigger ? `
196
+ <div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
197
+ <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">
198
+ ${trig}
199
+ </button>
200
+ <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>
201
+ </div>` : "";
202
+ return `${triggerBlock}
203
+ <div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
204
+ <div class="oks-access-header">
205
+ <h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
206
+ <button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
207
+ </div>
208
+ <div class="oks-access-content">
209
+ ${presetsBlock}
210
+ ${controlsHtml}
199
211
  </div>
200
212
  <div class="oks-access-footer">
201
213
  <button class="oks-access-reset" id="oks-reset" type="button">${escapeHtml(t.reset)}</button>
@@ -371,8 +383,69 @@ function bindPanelBehavior(root, opts = {}) {
371
383
  return noopController();
372
384
  }
373
385
  let state = loadState(storageKey);
386
+ const scoped = opts.scopeEl !== void 0;
387
+ const GLOBAL_KEY = "oksiac::global";
388
+ const GLOBAL_MAP = [
389
+ { flag: "bigCursor", cls: "oks-big-cursor", match: (b) => b.getAttribute("data-class") === "oks-big-cursor" },
390
+ { flag: "readingGuide", cls: "oks-a11y-guide", match: (b) => b.getAttribute("data-action") === "guide" },
391
+ { flag: "readingMask", cls: "oks-a11y-mask", match: (b) => b.getAttribute("data-action") === "mask" }
392
+ ];
393
+ const globalOfferableOf = (b) => GLOBAL_MAP.find((x) => x.match(b));
394
+ const loadGlobal = () => {
395
+ try {
396
+ const r = JSON.parse(localStorage.getItem(GLOBAL_KEY) ?? "{}");
397
+ return { bigCursor: !!r.bigCursor, readingGuide: !!r.readingGuide, readingMask: !!r.readingMask };
398
+ } catch {
399
+ return { bigCursor: false, readingGuide: false, readingMask: false };
400
+ }
401
+ };
402
+ const saveGlobal = (g) => {
403
+ try {
404
+ if (g.bigCursor || g.readingGuide || g.readingMask) localStorage.setItem(GLOBAL_KEY, JSON.stringify(g));
405
+ else localStorage.removeItem(GLOBAL_KEY);
406
+ } catch {
407
+ }
408
+ };
409
+ const applyGlobal = (g) => {
410
+ document.body.classList.toggle("oks-big-cursor", g.bigCursor);
411
+ document.body.classList.toggle("oks-a11y-guide", g.readingGuide);
412
+ document.body.classList.toggle("oks-a11y-mask", g.readingMask);
413
+ for (const btn of opts$) {
414
+ const m = globalOfferableOf(btn);
415
+ if (!m) continue;
416
+ btn.classList.toggle("is-active", g[m.flag]);
417
+ btn.setAttribute("aria-pressed", g[m.flag] ? "true" : "false");
418
+ }
419
+ };
420
+ const refreshGlobal = () => {
421
+ if (scoped) applyGlobal(loadGlobal());
422
+ };
423
+ const syncGlobalFromState = () => {
424
+ if (!scoped) return;
425
+ const g = loadGlobal();
426
+ let changed = false;
427
+ if (state.bigCursor) {
428
+ g.bigCursor = true;
429
+ state.bigCursor = false;
430
+ changed = true;
431
+ }
432
+ if (state.readingGuide) {
433
+ g.readingGuide = true;
434
+ state.readingGuide = false;
435
+ changed = true;
436
+ }
437
+ if (state.readingMask) {
438
+ g.readingMask = true;
439
+ state.readingMask = false;
440
+ changed = true;
441
+ }
442
+ if (changed) {
443
+ saveGlobal(g);
444
+ applyGlobal(g);
445
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
446
+ }
447
+ };
374
448
  function applyState() {
375
- const scoped = opts.scopeEl !== void 0;
376
449
  const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
377
450
  const rootEl = document.documentElement;
378
451
  if (fxRoot) {
@@ -407,6 +480,7 @@ function bindPanelBehavior(root, opts = {}) {
407
480
  }
408
481
  function syncButtonsFromState() {
409
482
  for (const btn of opts$) {
483
+ if (scoped && globalOfferableOf(btn)) continue;
410
484
  const action = btn.getAttribute("data-action");
411
485
  if (action === "multi" || action === "colorblind") {
412
486
  const prefix = btn.getAttribute("data-prefix") ?? "";
@@ -441,6 +515,17 @@ function bindPanelBehavior(root, opts = {}) {
441
515
  }
442
516
  const onOptClick = (e) => {
443
517
  const btn = e.currentTarget;
518
+ if (scoped) {
519
+ const go = globalOfferableOf(btn);
520
+ if (go) {
521
+ const g = loadGlobal();
522
+ g[go.flag] = !g[go.flag];
523
+ saveGlobal(g);
524
+ applyGlobal(g);
525
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
526
+ return;
527
+ }
528
+ }
444
529
  const action = btn.getAttribute("data-action");
445
530
  if (action === "multi") {
446
531
  const prefix = btn.getAttribute("data-prefix") ?? "";
@@ -469,6 +554,7 @@ function bindPanelBehavior(root, opts = {}) {
469
554
  if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
470
555
  btn.classList.add("is-flashing");
471
556
  setTimeout(() => btn.classList.remove("is-flashing"), 250);
557
+ syncGlobalFromState();
472
558
  }
473
559
  applyState();
474
560
  saveState(storageKey, state);
@@ -477,6 +563,12 @@ function bindPanelBehavior(root, opts = {}) {
477
563
  state = { ...DEFAULT_STATE };
478
564
  applyState();
479
565
  saveState(storageKey, state);
566
+ if (scoped) {
567
+ const cleared = { bigCursor: false, readingGuide: false, readingMask: false };
568
+ saveGlobal(cleared);
569
+ applyGlobal(cleared);
570
+ document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
571
+ }
480
572
  };
481
573
  let opener = null;
482
574
  let ignoreDocClose = false;
@@ -644,14 +736,16 @@ function bindPanelBehavior(root, opts = {}) {
644
736
  saveNudge();
645
737
  };
646
738
  const onMove = (e) => {
647
- if (!state.readingGuide && !state.readingMask) return;
739
+ const guideOn = document.body.classList.contains("oks-a11y-guide");
740
+ const maskOn = document.body.classList.contains("oks-a11y-mask");
741
+ if (!guideOn && !maskOn) return;
648
742
  const y = e.touches?.[0]?.clientY ?? e.clientY;
649
743
  if (typeof y !== "number") return;
650
- if (state.readingGuide) {
744
+ if (guideOn) {
651
745
  const guide = document.getElementById("oks-reading-guide");
652
746
  if (guide) guide.style.top = `${y}px`;
653
747
  }
654
- if (state.readingMask) {
748
+ if (maskOn) {
655
749
  const mask = document.getElementById("oks-reading-mask");
656
750
  if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
657
751
  }
@@ -667,9 +761,15 @@ function bindPanelBehavior(root, opts = {}) {
667
761
  const onReposition = () => {
668
762
  if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
669
763
  };
670
- if (opts.scopeEl !== void 0) {
764
+ const onGlobalChange = () => refreshGlobal();
765
+ const onStorage = (e) => {
766
+ if (e.key === GLOBAL_KEY) refreshGlobal();
767
+ };
768
+ if (scoped) {
671
769
  window.addEventListener("scroll", onReposition, true);
672
770
  window.addEventListener("resize", onReposition);
771
+ document.addEventListener("oksiac:globalchange", onGlobalChange);
772
+ window.addEventListener("storage", onStorage);
673
773
  }
674
774
  if (nudgeMax > 0 && trigger) {
675
775
  loadNudge();
@@ -682,6 +782,7 @@ function bindPanelBehavior(root, opts = {}) {
682
782
  trigger.addEventListener("keydown", onTriggerKey);
683
783
  }
684
784
  applyState();
785
+ refreshGlobal();
685
786
  const dispose = () => {
686
787
  trigger?.removeEventListener("click", onTriggerClick);
687
788
  closeBtn.removeEventListener("click", closePanel);
@@ -691,9 +792,11 @@ function bindPanelBehavior(root, opts = {}) {
691
792
  document.removeEventListener("keydown", onKeyDown);
692
793
  document.removeEventListener("mousemove", onMove);
693
794
  document.removeEventListener("touchmove", onMove);
694
- if (opts.scopeEl !== void 0) {
795
+ if (scoped) {
695
796
  window.removeEventListener("scroll", onReposition, true);
696
797
  window.removeEventListener("resize", onReposition);
798
+ document.removeEventListener("oksiac:globalchange", onGlobalChange);
799
+ window.removeEventListener("storage", onStorage);
697
800
  }
698
801
  trigger?.removeEventListener("pointerdown", onPointerDown);
699
802
  document.removeEventListener("pointermove", onPointerMove);
@@ -799,6 +902,7 @@ var PANEL_CSS = `
799
902
  }
800
903
  .oks-access-panel.is-open { opacity: 1; pointer-events: all; }
801
904
  .oks-access-header {
905
+ flex: 0 0 auto;
802
906
  padding: 15px 20px;
803
907
  border-bottom: 1px solid #eee;
804
908
  display: flex;
@@ -822,7 +926,11 @@ var PANEL_CSS = `
822
926
  }
823
927
  .oks-access-close:hover { background: #e0e0e0; border-color: #ccc; }
824
928
  .oks-access-close svg { width: 24px; height: 24px; stroke-width: 2.5px; }
825
- .oks-access-content { padding: 0 20px 20px; overflow-y: auto; }
929
+ /* flex:1 1 auto + min-height:0 lets the content scroll inside the 90vh panel.
930
+ Without min-height:0 a flex child won't shrink below its content, so the
931
+ header (\xD7) and footer (Reset) get pushed off-screen on short viewports or at
932
+ high zoom \u2014 the panel becomes impossible to close. */
933
+ .oks-access-content { flex: 1 1 auto; min-height: 0; padding: 0 20px 20px; overflow-y: auto; }
826
934
  .oks-access-title {
827
935
  margin: 10px 0 5px;
828
936
  font-size: 11px;
@@ -830,6 +938,19 @@ var PANEL_CSS = `
830
938
  text-transform: uppercase;
831
939
  color: #888;
832
940
  }
941
+ /* Screen-reader-only: keeps the category headings in the accessibility tree
942
+ (flat scoped layout) while taking them out of the grid flow so they don't
943
+ force a visual row break or occupy a cell. */
944
+ .oks-sr-only {
945
+ position: absolute !important;
946
+ width: 1px; height: 1px;
947
+ margin: 0; padding: 0;
948
+ overflow: hidden;
949
+ clip: rect(0 0 0 0);
950
+ clip-path: inset(50%);
951
+ white-space: nowrap;
952
+ border: 0;
953
+ }
833
954
  .oks-access-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
834
955
  .oks-access-presets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
835
956
  .oks-preset {
@@ -910,6 +1031,7 @@ var PANEL_CSS = `
910
1031
  .oks-access-opt[data-level="4"] .oks-levels span:nth-child(-n+4) { background: #000; }
911
1032
  .oks-access-opt:hover .oks-levels span { background: #555; }
912
1033
  .oks-access-footer {
1034
+ flex: 0 0 auto;
913
1035
  padding: 12px 20px;
914
1036
  border-top: 1px solid #eee;
915
1037
  text-align: center;
@@ -1116,12 +1238,14 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
1116
1238
  }
1117
1239
  `;
1118
1240
  function scopedEffectCss(s) {
1119
- return `
1241
+ const isRoot = s === "body" || s === "html" || s === ":root";
1242
+ const zoom = isRoot ? "" : `
1120
1243
  ${s}.oks-zoom-1 { font-size: 110% !important; }
1121
1244
  ${s}.oks-zoom-2 { font-size: 120% !important; }
1122
1245
  ${s}.oks-zoom-3 { font-size: 135% !important; }
1123
1246
  ${s}.oks-zoom-4 { font-size: 150% !important; }
1124
-
1247
+ `;
1248
+ return `${zoom}
1125
1249
  ${s}.oks-lh-1 * { line-height: 1.6 !important; }
1126
1250
  ${s}.oks-lh-2 * { line-height: 1.9 !important; }
1127
1251
  ${s}.oks-lh-3 * { line-height: 2.2 !important; }
@@ -1364,7 +1488,9 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
1364
1488
  position,
1365
1489
  enabled,
1366
1490
  showTrigger,
1367
- showPresets
1491
+ showPresets,
1492
+ flatLayout: scope != null
1493
+ // scoped panels use the flat (no-category) layout
1368
1494
  });
1369
1495
  shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
1370
1496
  const scopeEl = scope ? document.querySelector(scope) : void 0;