@oksigenia/access-panel 0.4.4 → 0.5.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/CHANGELOG.md +18 -0
- package/README.md +26 -2
- package/dist/index.d.ts +60 -21
- package/dist/index.js +301 -66
- package/dist/index.js.map +1 -1
- package/dist/web-component.d.ts +14 -1
- package/dist/web-component.js +375 -74
- package/dist/web-component.js.map +1 -1
- package/package.json +1 -1
package/dist/web-component.js
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
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
|
+
function filterPresetForEnabled(id, enabled) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [k, v] of Object.entries(PRESETS[id])) {
|
|
66
|
+
if (enabled.has(STATE_KEY_CONTROL[k])) out[k] = v;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function presetIsAvailable(id, enabled) {
|
|
71
|
+
return presetControlIds(id).filter((c) => enabled.has(c)).length >= 2;
|
|
72
|
+
}
|
|
73
|
+
|
|
1
74
|
// src/icons.ts
|
|
2
75
|
var ICON_TXT = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2.5,4v3h5v12h3V7h5V4H2.5z M21.5,9h-9v3h3v7h3v-7h3V9z"/></svg>';
|
|
3
76
|
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 +89,100 @@ var ICON_COLORBLIND = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12
|
|
|
16
89
|
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
90
|
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
91
|
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>';
|
|
92
|
+
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
93
|
var TRIGGER_ICONS = {
|
|
20
94
|
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
95
|
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
96
|
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:
|
|
97
|
+
universal: UNIVERSAL_GLYPH,
|
|
98
|
+
porthole: UNIVERSAL_GLYPH
|
|
24
99
|
};
|
|
25
100
|
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
101
|
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
102
|
|
|
28
103
|
// src/render.ts
|
|
104
|
+
var PRESET_ICON = {
|
|
105
|
+
lowvision: ICON_CONTRAST,
|
|
106
|
+
dyslexia: ICON_DYSLEXIA,
|
|
107
|
+
motor: ICON_CURSOR,
|
|
108
|
+
calm: ICON_HIDE
|
|
109
|
+
};
|
|
29
110
|
function buildPanelHtml(opts) {
|
|
30
111
|
const { t, triggerIcon } = opts;
|
|
112
|
+
const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
|
|
113
|
+
const showTrigger = opts.showTrigger ?? true;
|
|
114
|
+
const showPresets = opts.showPresets ?? true;
|
|
31
115
|
const trig = TRIGGER_ICONS[triggerIcon];
|
|
32
|
-
const
|
|
33
|
-
|
|
116
|
+
const multi = (id, prefix, levels, label, icon, full = false) => {
|
|
117
|
+
if (!enabled.has(id)) return "";
|
|
118
|
+
const action = id === "colorblind" ? "colorblind" : "multi";
|
|
34
119
|
const fullClass = full ? " full-width" : "";
|
|
35
120
|
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>`;
|
|
121
|
+
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
122
|
};
|
|
38
|
-
const toggle = (klass, label, icon) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
123
|
+
const toggle = (id, klass, label, icon) => {
|
|
124
|
+
if (!enabled.has(id)) return "";
|
|
125
|
+
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>`;
|
|
126
|
+
};
|
|
127
|
+
const overlay = (id, target, label, icon) => {
|
|
128
|
+
if (!enabled.has(id)) return "";
|
|
129
|
+
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>`;
|
|
130
|
+
};
|
|
131
|
+
const simple = (id, action, label, icon) => {
|
|
132
|
+
if (!enabled.has(id)) return "";
|
|
133
|
+
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>`;
|
|
134
|
+
};
|
|
135
|
+
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>`;
|
|
136
|
+
const section = (title, rows) => {
|
|
137
|
+
const kept = rows.filter(Boolean);
|
|
138
|
+
if (kept.length === 0) return "";
|
|
139
|
+
return `<h4 class="oks-access-title">${escapeHtml(title)}</h4><div class="oks-access-grid">${kept.join("")}</div>`;
|
|
140
|
+
};
|
|
141
|
+
const presetLabels = {
|
|
142
|
+
lowvision: t.pLow,
|
|
143
|
+
dyslexia: t.pDys,
|
|
144
|
+
motor: t.pMot,
|
|
145
|
+
calm: t.pCalm
|
|
146
|
+
};
|
|
147
|
+
const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
|
|
148
|
+
const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
|
|
149
|
+
const triggerBlock = showTrigger ? `
|
|
44
150
|
<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">
|
|
151
|
+
<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
152
|
${trig}
|
|
47
153
|
</button>
|
|
48
154
|
<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
|
|
155
|
+
</div>` : "";
|
|
156
|
+
return `${triggerBlock}
|
|
50
157
|
<div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
|
|
51
158
|
<div class="oks-access-header">
|
|
52
159
|
<h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
|
|
53
160
|
<button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
|
|
54
161
|
</div>
|
|
55
162
|
<div class="oks-access-content">
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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)
|
|
163
|
+
${presetsBlock}
|
|
164
|
+
${section(t.txt, [
|
|
165
|
+
multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
|
|
166
|
+
multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
|
|
167
|
+
multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
|
|
168
|
+
toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
|
|
169
|
+
toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
|
|
170
|
+
multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
|
|
71
171
|
])}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
toggle("oks-a11y-
|
|
77
|
-
toggle("oks-a11y-links", t.links, ICON_LINK),
|
|
172
|
+
${section(t.vis, [
|
|
173
|
+
toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
|
|
174
|
+
overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
|
|
175
|
+
toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
|
|
176
|
+
toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
|
|
78
177
|
multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, true)
|
|
79
178
|
])}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
toggle("oks-
|
|
85
|
-
toggle("oks-a11y-
|
|
86
|
-
toggle("oks-a11y-
|
|
87
|
-
toggle("oks-a11y-focus", t.focus, ICON_FOCUS)
|
|
179
|
+
${section(t.ori, [
|
|
180
|
+
simple("reading-guide", "guide", t.guide, ICON_GUIDE),
|
|
181
|
+
simple("reading-mask", "mask", t.mask, ICON_MASK),
|
|
182
|
+
toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
|
|
183
|
+
toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
|
|
184
|
+
toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
|
|
185
|
+
toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
|
|
88
186
|
])}
|
|
89
187
|
</div>
|
|
90
188
|
<div class="oks-access-footer">
|
|
@@ -109,14 +207,20 @@ function positionRules(position) {
|
|
|
109
207
|
switch (position) {
|
|
110
208
|
case "top-left":
|
|
111
209
|
return { wrap: "top: 20px; left: 20px;", panel: "top: 80px; left: 20px;" };
|
|
210
|
+
case "top-center":
|
|
211
|
+
return { wrap: "top: 20px; left: 50%; transform: translateX(-50%);", panel: "top: 80px; left: 50%; transform: translateX(-50%);" };
|
|
112
212
|
case "top-right":
|
|
113
213
|
return { wrap: "top: 20px; right: 20px;", panel: "top: 80px; right: 20px;" };
|
|
114
214
|
case "mid-left":
|
|
115
215
|
return { wrap: "top: 50%; left: 20px; transform: translateY(-50%);", panel: "top: 50%; left: 90px; transform: translateY(-50%);" };
|
|
216
|
+
case "mid-center":
|
|
217
|
+
return { wrap: "top: 50%; left: 50%; transform: translate(-50%, -50%);", panel: "top: 50%; left: 50%; transform: translate(-50%, -50%);" };
|
|
116
218
|
case "mid-right":
|
|
117
219
|
return { wrap: "top: 50%; right: 20px; transform: translateY(-50%);", panel: "top: 50%; right: 90px; transform: translateY(-50%);" };
|
|
118
220
|
case "bottom-left":
|
|
119
221
|
return { wrap: "bottom: 20px; left: 20px;", panel: "bottom: 100px; left: 20px;" };
|
|
222
|
+
case "bottom-center":
|
|
223
|
+
return { wrap: "bottom: 20px; left: 50%; transform: translateX(-50%);", panel: "bottom: 100px; left: 50%; transform: translateX(-50%);" };
|
|
120
224
|
case "bottom-right":
|
|
121
225
|
return { wrap: "bottom: 20px; right: 20px;", panel: "bottom: 100px; right: 20px;" };
|
|
122
226
|
}
|
|
@@ -228,38 +332,45 @@ var TOGGLE_KEYS = {
|
|
|
228
332
|
"oks-a11y-focus": "focusOutline",
|
|
229
333
|
"oks-a11y-bigtargets": "bigTargets"
|
|
230
334
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
};
|
|
335
|
+
function noopController() {
|
|
336
|
+
return Object.assign(() => {
|
|
337
|
+
}, { open() {
|
|
338
|
+
}, close() {
|
|
339
|
+
}, toggle() {
|
|
340
|
+
} });
|
|
341
|
+
}
|
|
342
|
+
function deepActiveElement() {
|
|
343
|
+
let a = document.activeElement;
|
|
344
|
+
while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
|
|
345
|
+
return a;
|
|
346
|
+
}
|
|
237
347
|
function bindPanelBehavior(root, opts = {}) {
|
|
238
348
|
const storageKey = opts.storageKey ?? "oksiacSettings";
|
|
239
349
|
const t = getTranslation(opts.locale ?? "en");
|
|
350
|
+
const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
|
|
351
|
+
const nudgeMax = Math.max(0, opts.nudgeMax ?? 0);
|
|
240
352
|
const trigger = root.getElementById("oks-trigger");
|
|
241
353
|
const panel = root.getElementById("oks-panel");
|
|
242
354
|
const closeBtn = root.getElementById("oks-close");
|
|
243
355
|
const resetBtn = root.getElementById("oks-reset");
|
|
244
356
|
const wrapper = root.getElementById("oks-wrapper");
|
|
245
357
|
const opts$ = Array.from(root.querySelectorAll(".oks-access-opt, .oks-preset"));
|
|
246
|
-
if (!
|
|
247
|
-
return ()
|
|
248
|
-
};
|
|
358
|
+
if (!panel || !closeBtn || !resetBtn) {
|
|
359
|
+
return noopController();
|
|
249
360
|
}
|
|
250
361
|
let state = loadState(storageKey);
|
|
251
362
|
function applyState() {
|
|
252
363
|
const body = document.body;
|
|
253
|
-
const
|
|
364
|
+
const rootEl = document.documentElement;
|
|
254
365
|
for (const cls of Array.from(body.classList)) {
|
|
255
366
|
if (cls.startsWith("oks-")) body.classList.remove(cls);
|
|
256
367
|
}
|
|
257
|
-
[1, 2, 3].forEach((l) =>
|
|
368
|
+
[1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
|
|
258
369
|
if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
|
|
259
370
|
if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
|
|
260
371
|
if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
|
|
261
372
|
if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
|
|
262
|
-
if (state.colorblind > 0)
|
|
373
|
+
if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
|
|
263
374
|
if (state.font) body.classList.add("oks-a11y-font");
|
|
264
375
|
if (state.dyslexia) body.classList.add("oks-dyslexia");
|
|
265
376
|
if (state.contrast) body.classList.add("oks-a11y-contrast");
|
|
@@ -336,9 +447,8 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
336
447
|
} else if (action === "mask") {
|
|
337
448
|
state.readingMask = !state.readingMask;
|
|
338
449
|
} else if (action === "preset") {
|
|
339
|
-
const id = btn.getAttribute("data-preset")
|
|
340
|
-
|
|
341
|
-
if (preset) Object.assign(state, preset);
|
|
450
|
+
const id = btn.getAttribute("data-preset");
|
|
451
|
+
if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
|
|
342
452
|
btn.classList.add("is-flashing");
|
|
343
453
|
setTimeout(() => btn.classList.remove("is-flashing"), 250);
|
|
344
454
|
}
|
|
@@ -350,28 +460,50 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
350
460
|
applyState();
|
|
351
461
|
saveState(storageKey, state);
|
|
352
462
|
};
|
|
463
|
+
let opener = null;
|
|
464
|
+
let ignoreDocClose = false;
|
|
353
465
|
const openPanel = () => {
|
|
466
|
+
if (panel.classList.contains("is-open")) return;
|
|
467
|
+
opener = deepActiveElement();
|
|
354
468
|
panel.classList.add("is-open");
|
|
355
469
|
panel.removeAttribute("inert");
|
|
356
|
-
trigger
|
|
470
|
+
trigger?.setAttribute("aria-expanded", "true");
|
|
471
|
+
ignoreDocClose = true;
|
|
472
|
+
setTimeout(() => {
|
|
473
|
+
ignoreDocClose = false;
|
|
474
|
+
}, 0);
|
|
357
475
|
const first = panel.querySelector("button:not([disabled])");
|
|
358
476
|
first?.focus();
|
|
359
477
|
};
|
|
360
478
|
const closePanel = () => {
|
|
479
|
+
if (!panel.classList.contains("is-open")) return;
|
|
361
480
|
panel.classList.remove("is-open");
|
|
362
481
|
panel.setAttribute("inert", "");
|
|
363
|
-
trigger
|
|
364
|
-
trigger.focus();
|
|
482
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
483
|
+
if (trigger) trigger.focus();
|
|
484
|
+
else if (opener?.isConnected) opener.focus();
|
|
485
|
+
opener = null;
|
|
365
486
|
};
|
|
366
|
-
const
|
|
367
|
-
e.stopPropagation();
|
|
487
|
+
const togglePanel = () => {
|
|
368
488
|
if (panel.classList.contains("is-open")) closePanel();
|
|
369
489
|
else openPanel();
|
|
370
490
|
};
|
|
491
|
+
let suppressClick = false;
|
|
492
|
+
const onTriggerClick = (e) => {
|
|
493
|
+
e.stopPropagation();
|
|
494
|
+
if (suppressClick) {
|
|
495
|
+
suppressClick = false;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
togglePanel();
|
|
499
|
+
};
|
|
371
500
|
const onDocClick = (e) => {
|
|
372
501
|
if (!panel.classList.contains("is-open")) return;
|
|
502
|
+
if (ignoreDocClose) return;
|
|
373
503
|
const path = e.composedPath();
|
|
374
|
-
if (path.includes(panel)
|
|
504
|
+
if (path.includes(panel)) return;
|
|
505
|
+
if (trigger && path.includes(trigger)) return;
|
|
506
|
+
if (wrapper && path.includes(wrapper)) return;
|
|
375
507
|
closePanel();
|
|
376
508
|
};
|
|
377
509
|
const onKeyDown = (e) => {
|
|
@@ -398,6 +530,88 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
398
530
|
}
|
|
399
531
|
}
|
|
400
532
|
};
|
|
533
|
+
const NUDGE_KEY = `${storageKey}::pos`;
|
|
534
|
+
const clampMax = (v) => Math.max(-nudgeMax, Math.min(nudgeMax, v));
|
|
535
|
+
let nudge = { x: 0, y: 0 };
|
|
536
|
+
const loadNudge = () => {
|
|
537
|
+
if (nudgeMax <= 0) return;
|
|
538
|
+
try {
|
|
539
|
+
const raw = localStorage.getItem(NUDGE_KEY);
|
|
540
|
+
if (!raw) return;
|
|
541
|
+
const p = JSON.parse(raw);
|
|
542
|
+
if (typeof p?.x === "number" && typeof p?.y === "number") {
|
|
543
|
+
nudge = { x: clampMax(p.x), y: clampMax(p.y) };
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const saveNudge = () => {
|
|
549
|
+
try {
|
|
550
|
+
if (nudge.x === 0 && nudge.y === 0) localStorage.removeItem(NUDGE_KEY);
|
|
551
|
+
else localStorage.setItem(NUDGE_KEY, JSON.stringify(nudge));
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const applyNudge = () => {
|
|
556
|
+
if (wrapper) wrapper.style.translate = `${nudge.x}px ${nudge.y}px`;
|
|
557
|
+
};
|
|
558
|
+
const clampOffset = (x, y) => {
|
|
559
|
+
x = clampMax(x);
|
|
560
|
+
y = clampMax(y);
|
|
561
|
+
if (wrapper) {
|
|
562
|
+
const r = wrapper.getBoundingClientRect();
|
|
563
|
+
const pad = 4;
|
|
564
|
+
const baseLeft = r.left - nudge.x;
|
|
565
|
+
const baseTop = r.top - nudge.y;
|
|
566
|
+
const vw = window.innerWidth || 0;
|
|
567
|
+
const vh = window.innerHeight || 0;
|
|
568
|
+
if (r.width > 0 && vw > 0) x = Math.max(pad - baseLeft, Math.min(vw - pad - r.width - baseLeft, x));
|
|
569
|
+
if (r.height > 0 && vh > 0) y = Math.max(pad - baseTop, Math.min(vh - pad - r.height - baseTop, y));
|
|
570
|
+
}
|
|
571
|
+
return { x, y };
|
|
572
|
+
};
|
|
573
|
+
let dragStart = null;
|
|
574
|
+
let didDrag = false;
|
|
575
|
+
const DRAG_THRESHOLD = 4;
|
|
576
|
+
const onPointerDown = (e) => {
|
|
577
|
+
if (nudgeMax <= 0 || e.button !== 0) return;
|
|
578
|
+
dragStart = { px: e.clientX, py: e.clientY, ox: nudge.x, oy: nudge.y };
|
|
579
|
+
didDrag = false;
|
|
580
|
+
trigger?.setPointerCapture?.(e.pointerId);
|
|
581
|
+
};
|
|
582
|
+
const onPointerMove = (e) => {
|
|
583
|
+
if (!dragStart) return;
|
|
584
|
+
const dx = e.clientX - dragStart.px;
|
|
585
|
+
const dy = e.clientY - dragStart.py;
|
|
586
|
+
if (!didDrag && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
587
|
+
didDrag = true;
|
|
588
|
+
nudge = clampOffset(dragStart.ox + dx, dragStart.oy + dy);
|
|
589
|
+
applyNudge();
|
|
590
|
+
e.preventDefault();
|
|
591
|
+
};
|
|
592
|
+
const onPointerUp = () => {
|
|
593
|
+
if (!dragStart) return;
|
|
594
|
+
dragStart = null;
|
|
595
|
+
if (didDrag) {
|
|
596
|
+
saveNudge();
|
|
597
|
+
suppressClick = true;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const onTriggerKey = (e) => {
|
|
601
|
+
if (nudgeMax <= 0) return;
|
|
602
|
+
const step = e.shiftKey ? 1 : 10;
|
|
603
|
+
let dx = 0;
|
|
604
|
+
let dy = 0;
|
|
605
|
+
if (e.key === "ArrowLeft") dx = -step;
|
|
606
|
+
else if (e.key === "ArrowRight") dx = step;
|
|
607
|
+
else if (e.key === "ArrowUp") dy = -step;
|
|
608
|
+
else if (e.key === "ArrowDown") dy = step;
|
|
609
|
+
else return;
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
nudge = clampOffset(nudge.x + dx, nudge.y + dy);
|
|
612
|
+
applyNudge();
|
|
613
|
+
saveNudge();
|
|
614
|
+
};
|
|
401
615
|
const onMove = (e) => {
|
|
402
616
|
if (!state.readingGuide && !state.readingMask) return;
|
|
403
617
|
const y = e.touches?.[0]?.clientY ?? e.clientY;
|
|
@@ -411,7 +625,7 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
411
625
|
if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
|
|
412
626
|
}
|
|
413
627
|
};
|
|
414
|
-
trigger
|
|
628
|
+
trigger?.addEventListener("click", onTriggerClick);
|
|
415
629
|
closeBtn.addEventListener("click", closePanel);
|
|
416
630
|
resetBtn.addEventListener("click", onReset);
|
|
417
631
|
for (const btn of opts$) btn.addEventListener("click", onOptClick);
|
|
@@ -419,9 +633,19 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
419
633
|
document.addEventListener("keydown", onKeyDown);
|
|
420
634
|
document.addEventListener("mousemove", onMove);
|
|
421
635
|
document.addEventListener("touchmove", onMove, { passive: true });
|
|
636
|
+
if (nudgeMax > 0 && trigger) {
|
|
637
|
+
loadNudge();
|
|
638
|
+
applyNudge();
|
|
639
|
+
trigger.style.cursor = "grab";
|
|
640
|
+
trigger.style.touchAction = "none";
|
|
641
|
+
trigger.addEventListener("pointerdown", onPointerDown);
|
|
642
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
643
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
644
|
+
trigger.addEventListener("keydown", onTriggerKey);
|
|
645
|
+
}
|
|
422
646
|
applyState();
|
|
423
|
-
|
|
424
|
-
trigger
|
|
647
|
+
const dispose = () => {
|
|
648
|
+
trigger?.removeEventListener("click", onTriggerClick);
|
|
425
649
|
closeBtn.removeEventListener("click", closePanel);
|
|
426
650
|
resetBtn.removeEventListener("click", onReset);
|
|
427
651
|
for (const btn of opts$) btn.removeEventListener("click", onOptClick);
|
|
@@ -429,7 +653,12 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
429
653
|
document.removeEventListener("keydown", onKeyDown);
|
|
430
654
|
document.removeEventListener("mousemove", onMove);
|
|
431
655
|
document.removeEventListener("touchmove", onMove);
|
|
656
|
+
trigger?.removeEventListener("pointerdown", onPointerDown);
|
|
657
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
658
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
659
|
+
trigger?.removeEventListener("keydown", onTriggerKey);
|
|
432
660
|
};
|
|
661
|
+
return Object.assign(dispose, { open: openPanel, close: closePanel, toggle: togglePanel });
|
|
433
662
|
}
|
|
434
663
|
function ensureOverlay() {
|
|
435
664
|
let el = document.getElementById("oks-overlay-gray");
|
|
@@ -484,6 +713,12 @@ var PANEL_CSS = `
|
|
|
484
713
|
width: 60%;
|
|
485
714
|
height: 60%;
|
|
486
715
|
}
|
|
716
|
+
/* Porthole preset: a brushed-metal ring framing the standard glyph. The glyph
|
|
717
|
+
itself is untouched (discoverability), only the button gets the frame.
|
|
718
|
+
Hosts wanting a bespoke frame can target ::part(trigger) from light DOM. */
|
|
719
|
+
.oks-access-btn[data-trigger-icon="porthole"] {
|
|
720
|
+
box-shadow: 0 0 0 4px #b0bec5, 0 0 0 7px #546e7a, 0 6px 18px rgba(0,0,0,0.3);
|
|
721
|
+
}
|
|
487
722
|
.oks-active-badge {
|
|
488
723
|
position: absolute;
|
|
489
724
|
top: -2px;
|
|
@@ -836,7 +1071,19 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
|
|
|
836
1071
|
`;
|
|
837
1072
|
|
|
838
1073
|
// src/web-component.ts
|
|
839
|
-
var OBSERVED = [
|
|
1074
|
+
var OBSERVED = [
|
|
1075
|
+
"locale",
|
|
1076
|
+
"position",
|
|
1077
|
+
"position-mobile",
|
|
1078
|
+
"trigger-icon",
|
|
1079
|
+
"storage-key",
|
|
1080
|
+
"controls",
|
|
1081
|
+
"exclude",
|
|
1082
|
+
"trigger",
|
|
1083
|
+
"effects-exclude",
|
|
1084
|
+
"nudge",
|
|
1085
|
+
"presets"
|
|
1086
|
+
];
|
|
840
1087
|
var STYLE_ID = "oksigenia-access-effects";
|
|
841
1088
|
var FILTERS_ID = "oksigenia-access-filters";
|
|
842
1089
|
var GUIDE_ID = "oks-reading-guide";
|
|
@@ -871,18 +1118,23 @@ function ensureGlobalStyles() {
|
|
|
871
1118
|
}
|
|
872
1119
|
var VALID_POSITIONS = [
|
|
873
1120
|
"top-left",
|
|
1121
|
+
"top-center",
|
|
874
1122
|
"top-right",
|
|
875
1123
|
"mid-left",
|
|
1124
|
+
"mid-center",
|
|
876
1125
|
"mid-right",
|
|
877
1126
|
"bottom-left",
|
|
1127
|
+
"bottom-center",
|
|
878
1128
|
"bottom-right"
|
|
879
1129
|
];
|
|
880
|
-
var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal"];
|
|
1130
|
+
var VALID_ICONS = ["vitruvian", "wheelchair", "eye", "universal", "porthole"];
|
|
1131
|
+
var fxSeq = 0;
|
|
881
1132
|
var OksigeniaAccessPanelElement = class extends HTMLElement {
|
|
882
1133
|
static get observedAttributes() {
|
|
883
1134
|
return OBSERVED;
|
|
884
1135
|
}
|
|
885
|
-
|
|
1136
|
+
_controller = null;
|
|
1137
|
+
_fxId = `oks-access-fx-${++fxSeq}`;
|
|
886
1138
|
constructor() {
|
|
887
1139
|
super();
|
|
888
1140
|
this.attachShadow({ mode: "open" });
|
|
@@ -892,12 +1144,24 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
|
|
|
892
1144
|
this.render();
|
|
893
1145
|
}
|
|
894
1146
|
disconnectedCallback() {
|
|
895
|
-
this.
|
|
896
|
-
this.
|
|
1147
|
+
this._controller?.();
|
|
1148
|
+
this._controller = null;
|
|
1149
|
+
if (typeof document !== "undefined") document.getElementById(this._fxId)?.remove();
|
|
897
1150
|
}
|
|
898
1151
|
attributeChangedCallback() {
|
|
899
1152
|
if (this.isConnected) this.render();
|
|
900
1153
|
}
|
|
1154
|
+
/** Imperative API (#2): open / close / toggle the panel from the host.
|
|
1155
|
+
* Useful with `trigger="none"`, where the host drives its own button. */
|
|
1156
|
+
open() {
|
|
1157
|
+
this._controller?.open();
|
|
1158
|
+
}
|
|
1159
|
+
close() {
|
|
1160
|
+
this._controller?.close();
|
|
1161
|
+
}
|
|
1162
|
+
toggle() {
|
|
1163
|
+
this._controller?.toggle();
|
|
1164
|
+
}
|
|
901
1165
|
getPosition() {
|
|
902
1166
|
const attr = this.getAttribute("position") ?? "mid-left";
|
|
903
1167
|
return VALID_POSITIONS.includes(attr) ? attr : "mid-left";
|
|
@@ -913,22 +1177,59 @@ var OksigeniaAccessPanelElement = class extends HTMLElement {
|
|
|
913
1177
|
getLocale() {
|
|
914
1178
|
return this.getAttribute("locale") ?? (typeof navigator !== "undefined" ? navigator.language : "en");
|
|
915
1179
|
}
|
|
1180
|
+
/** `nudge` (bare) ⇒ default 80px cap; `nudge="50"` ⇒ 50; absent/invalid ⇒ off. */
|
|
1181
|
+
getNudgeMax() {
|
|
1182
|
+
if (!this.hasAttribute("nudge")) return 0;
|
|
1183
|
+
const raw = this.getAttribute("nudge");
|
|
1184
|
+
if (raw == null || raw.trim() === "") return 80;
|
|
1185
|
+
const n = parseInt(raw, 10);
|
|
1186
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1187
|
+
}
|
|
1188
|
+
/** Inject the per-instance effects-exclude rule (#4): under high-contrast,
|
|
1189
|
+
* drop the destructive filter from selectors the host marks as essential
|
|
1190
|
+
* media (video, canvas, scientific imagery). Grayscale is handled by not
|
|
1191
|
+
* offering the control (curation), not here. */
|
|
1192
|
+
updateEffectsExclude() {
|
|
1193
|
+
if (typeof document === "undefined") return;
|
|
1194
|
+
const sel = (this.getAttribute("effects-exclude") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1195
|
+
const existing = document.getElementById(this._fxId);
|
|
1196
|
+
if (sel.length === 0) {
|
|
1197
|
+
existing?.remove();
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const style = existing ?? document.createElement("style");
|
|
1201
|
+
if (!existing) {
|
|
1202
|
+
style.id = this._fxId;
|
|
1203
|
+
document.head.appendChild(style);
|
|
1204
|
+
}
|
|
1205
|
+
const list = sel.join(", ");
|
|
1206
|
+
style.textContent = `body.oks-a11y-contrast :is(${list}), body.oks-a11y-contrast :is(${list}) * { filter: none !important; }`;
|
|
1207
|
+
}
|
|
916
1208
|
render() {
|
|
917
1209
|
const shadow = this.shadowRoot;
|
|
918
1210
|
if (!shadow) return;
|
|
919
|
-
this.
|
|
1211
|
+
this._controller?.();
|
|
920
1212
|
const position = this.getPosition();
|
|
921
1213
|
const positionMobile = this.getPositionMobile();
|
|
1214
|
+
const enabled = resolveEnabledControls(this.getAttribute("controls"), this.getAttribute("exclude"));
|
|
1215
|
+
const showTrigger = this.getAttribute("trigger") !== "none";
|
|
1216
|
+
const showPresets = (this.getAttribute("presets") ?? "").trim().toLowerCase() !== "none";
|
|
922
1217
|
const html = buildPanelHtml({
|
|
923
1218
|
t: getTranslation(this.getLocale()),
|
|
924
1219
|
triggerIcon: this.getTriggerIcon(),
|
|
925
|
-
position
|
|
1220
|
+
position,
|
|
1221
|
+
enabled,
|
|
1222
|
+
showTrigger,
|
|
1223
|
+
showPresets
|
|
926
1224
|
});
|
|
927
1225
|
shadow.innerHTML = `<style>${PANEL_CSS}${positionCss(position, positionMobile)}</style>${html}`;
|
|
928
|
-
this.
|
|
1226
|
+
this._controller = bindPanelBehavior(shadow, {
|
|
929
1227
|
storageKey: this.getAttribute("storage-key") ?? void 0,
|
|
930
|
-
locale: this.getLocale()
|
|
1228
|
+
locale: this.getLocale(),
|
|
1229
|
+
enabled,
|
|
1230
|
+
nudgeMax: this.getNudgeMax()
|
|
931
1231
|
});
|
|
1232
|
+
this.updateEffectsExclude();
|
|
932
1233
|
}
|
|
933
1234
|
};
|
|
934
1235
|
if (typeof customElements !== "undefined" && !customElements.get("oksigenia-access-panel")) {
|