@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.
- package/CHANGELOG.md +13 -0
- package/README.md +62 -3
- package/dist/index.d.ts +31 -1
- package/dist/index.js +139 -23
- package/dist/index.js.map +1 -1
- package/dist/web-component.d.ts +5 -0
- package/dist/web-component.js +172 -29
- package/dist/web-component.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @oksigenia/access-panel
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `scope=` for per-container accessibility — confine a panel's effects to a host element instead of the whole page, so several panels can each adapt their own region in a multi-pane app (a data dashboard, a scientific viewer, an IDE) without clobbering each other. Opt-in: with no `scope` attribute, behaviour is unchanged.
|
|
8
|
+
|
|
9
|
+
- `<oksigenia-access-panel scope="#map-pane">` applies its effect classes to `#map-pane` (not `body`) and injects a per-instance stylesheet anchored to that selector. Multiple scoped instances coexist, each over its own region with its own `storage-key`.
|
|
10
|
+
- A scoped instance auto-excludes the controls that cannot be regionalised: the full-screen overlays (grayscale, reading guide, reading mask), the colour-blind root filter, and the window-level big cursor. A per-pane panel only offers what it can actually confine.
|
|
11
|
+
- Text size in scoped mode scales the container's `font-size`, so it scales `em`/inherited text inside the pane but not `rem` (which is always relative to `<html>`). Hosts that want per-pane text scaling size that pane's text in `em`.
|
|
12
|
+
- `effects-exclude` composes with `scope`: under a scope, the essential-media exclusion is anchored to the scope selector.
|
|
13
|
+
|
|
14
|
+
This rounds out the toolkit for embedding the panel in non-generic, multi-region platforms (alongside `effects-exclude` and `trigger="none"`). It is a component-only capability — the generic WordPress/Moodle plugins, whose sites adapt globally, neither expose nor need it.
|
|
15
|
+
|
|
3
16
|
## 0.5.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Attributes:
|
|
|
71
71
|
| `trigger` | floating | `none` renders the panel without its floating launcher; open it from your own button via `.open()`. |
|
|
72
72
|
| `effects-exclude` | none | CSS selectors kept free of the destructive high-contrast filter (e.g. `video, canvas, .no-a11y-filter`) — for surfaces where colour is information. |
|
|
73
73
|
| `nudge` | off | Present (or `nudge="50"` for a custom px cap, default 80) lets the user reposition the trigger within bounds, by drag or arrow keys, persisted per instance. |
|
|
74
|
+
| `scope` | whole page | A CSS selector (e.g. `#map-pane`) to confine the panel's effects to one container instead of `body`. For multi-pane apps where each region adapts on its own. See below. |
|
|
74
75
|
|
|
75
76
|
Control ids for `controls`/`exclude`: `text-size`, `line-height`, `text-align`, `readable-font`, `dyslexia-font`, `letter-spacing`, `contrast`, `grayscale`, `hide-images`, `highlight-links`, `colorblind`, `reading-guide`, `reading-mask`, `big-cursor`, `big-targets`, `pause-anim`, `focus`.
|
|
76
77
|
|
|
@@ -90,6 +91,21 @@ With `trigger="none"`, mount the panel and open it from anywhere:
|
|
|
90
91
|
|
|
91
92
|
`.open()`, `.close()` and `.toggle()` are available on the element. The floating trigger is also exposed as `::part(trigger)` if you want to restyle it instead of replacing it.
|
|
92
93
|
|
|
94
|
+
### Per-container accessibility (scoped mode)
|
|
95
|
+
|
|
96
|
+
By default the panel adapts the whole page — which is what a normal content site wants. In a **multi-pane app** (a data dashboard, a scientific viewer, an editor with several panels) you may want each region to adapt on its own: high contrast on the chart pane but the video untouched, bigger text in the data pane but not elsewhere. `scope` confines a panel's effects to one container:
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<div id="map-pane">…</div>
|
|
100
|
+
<oksigenia-access-panel scope="#map-pane" trigger="none" effects-exclude="canvas"></oksigenia-access-panel>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The instance applies its effect classes to `#map-pane` instead of `body` and injects a stylesheet anchored to that selector. Mount one per pane (each with its own `storage-key`) and they coexist without clobbering each other. The dialog opens **over its own pane** (anchored to the scope's box, not a shared viewport corner), so each pane's panel reads as belonging to that pane.
|
|
104
|
+
|
|
105
|
+
A scoped instance **auto-excludes** the controls that can't be confined to a container: the full-screen overlays (grayscale, reading guide, reading mask), the colour-blind root filter, and the window-level big cursor. If you need one of those globally, mount a separate non-scoped instance.
|
|
106
|
+
|
|
107
|
+
Text size in scoped mode scales the container's `font-size`, so it scales `em`/inherited text inside the pane but **not `rem`** — `rem` is always relative to `<html>` and there is no CSS way to anchor it to a container. Size that pane's text in `em` if you want per-pane text scaling.
|
|
108
|
+
|
|
93
109
|
## Theming with CSS variables
|
|
94
110
|
|
|
95
111
|
The trigger button colors are exposed as CSS custom properties on the
|
|
@@ -185,9 +201,52 @@ intentionally cover three orthogonal axes:
|
|
|
185
201
|
- **Orientation** (4 controls): reading guide, big cursor, pause animations,
|
|
186
202
|
focus outline.
|
|
187
203
|
|
|
188
|
-
You can
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
You can curate which controls appear with the `controls` / `exclude` attributes (see the attributes table above) if you want a smaller panel for your site; the default is to ship the full set.
|
|
205
|
+
|
|
206
|
+
## Scientific basis & references
|
|
207
|
+
|
|
208
|
+
Oksigenia Access is a presentation-layer tool: it lets a visitor adapt how a page is shown to them. The controls map to recognised accessibility standards and, where relevant, to peer-reviewed research. This section documents what each control is grounded in — and, just as importantly, where the evidence is weak, so nothing here claims more authority than it has. A floating panel does not make a site conformant; conformance is editorial work on the content (alt text, transcripts, semantics, source-design contrast, keyboard operability). These references are the basis for the *adaptations the panel offers*, not a conformance claim.
|
|
209
|
+
|
|
210
|
+
### Standards and legal framework
|
|
211
|
+
|
|
212
|
+
- **WCAG 2.2** — W3C Recommendation, the technical baseline most controls map to: <https://www.w3.org/TR/WCAG22/>
|
|
213
|
+
- **EN 301 549** — the European harmonised accessibility standard (references WCAG).
|
|
214
|
+
- **Directive (EU) 2016/2102** — accessibility of public-sector websites and apps: <https://eur-lex.europa.eu/eli/dir/2016/2102/oj>
|
|
215
|
+
- **Directive (EU) 2019/882 (European Accessibility Act)** — applies from 28 June 2025: <https://eur-lex.europa.eu/eli/dir/2019/882/oj>
|
|
216
|
+
- **Spain — Real Decreto 1112/2018 (BOE)** — transposes Directive 2016/2102: <https://www.boe.es/eli/es/rd/2018/09/07/1112>
|
|
217
|
+
|
|
218
|
+
### What each control is grounded in
|
|
219
|
+
|
|
220
|
+
| Control | Basis |
|
|
221
|
+
|---|---|
|
|
222
|
+
| Text size | WCAG 1.4.4 Resize Text, 1.4.10 Reflow |
|
|
223
|
+
| Line height · letter spacing · text alignment | WCAG 1.4.12 Text Spacing (line height ≥ 1.5, letter spacing ≥ 0.12em, word spacing ≥ 0.16em) |
|
|
224
|
+
| High contrast | WCAG 1.4.3 (AA), 1.4.6 (AAA), 1.4.11 Non-text Contrast |
|
|
225
|
+
| Highlight links | WCAG 1.4.1 Use of Color |
|
|
226
|
+
| Big targets | WCAG 2.5.8 Target Size Minimum (AA, 24px), 2.5.5 Enhanced (AAA, 44px) |
|
|
227
|
+
| Pause animations | WCAG 2.2.2 Pause/Stop/Hide, 2.3.3 Animation from Interactions; CSS `prefers-reduced-motion` |
|
|
228
|
+
| Focus highlight | WCAG 2.4.7 Focus Visible (AA), 2.4.11/2.4.13 Focus Appearance (2.2) |
|
|
229
|
+
| Readable font | British Dyslexia Association — Dyslexia Style Guide |
|
|
230
|
+
| Reading guide · reading mask · hide images | Cognitive-accessibility reading aids (WCAG cognitive guidance / COGA); no single Success Criterion mandates them |
|
|
231
|
+
| Colour-blind filters | Colour-vision-deficiency **simulation** — Viénot, Brettel & Mollon (1999); Brettel, Viénot & Mollon (1997); Machado, Oliveira & Fernandes (2009) |
|
|
232
|
+
|
|
233
|
+
### Honesty notes
|
|
234
|
+
|
|
235
|
+
**The colour-blind control is a _simulation_, not a _correction_.** It renders the page as a person with that deficiency would see it — a design/QA aid for people with normal vision, not an aid for a colour-blind visitor (who already sees that way). The matrices are the SVG colour-matrix filters widely used on the web; their rigorous basis is the simulation literature cited above, and the exact circulating values are approximations (for production-grade accuracy see [DaltonLens](https://daltonlens.org/cvd-simulation-svg-filters/)). Helping a colour-blind user is a design problem (colour-blind-safe palettes, shape in addition to colour), not a screen filter.
|
|
236
|
+
|
|
237
|
+
**"Dyslexia font" has mixed evidence.** Studies on special dyslexia typefaces (e.g. OpenDyslexic) are inconclusive, and several find no benefit over a good standard font. What _is_ evidence-based is spacing (WCAG 1.4.12 and the BDA guide). We offer the font as a common preference, not as a research-backed remedy.
|
|
238
|
+
|
|
239
|
+
### Sources
|
|
240
|
+
|
|
241
|
+
- W3C — *Web Content Accessibility Guidelines (WCAG) 2.2*: <https://www.w3.org/TR/WCAG22/> (per-criterion notes: <https://www.w3.org/WAI/WCAG22/Understanding/>)
|
|
242
|
+
- Directive (EU) 2016/2102: <https://eur-lex.europa.eu/eli/dir/2016/2102/oj>
|
|
243
|
+
- Directive (EU) 2019/882 (European Accessibility Act): <https://eur-lex.europa.eu/eli/dir/2019/882/oj>
|
|
244
|
+
- Real Decreto 1112/2018 (BOE): <https://www.boe.es/eli/es/rd/2018/09/07/1112>
|
|
245
|
+
- British Dyslexia Association — *Dyslexia Style Guide* (archived copy; the BDA's live document is access-restricted): <https://web.archive.org/web/20250701150153/https://www.bdadyslexia.org.uk/advice/employers/creating-a-dyslexia-friendly-workplace/dyslexia-friendly-style-guide>
|
|
246
|
+
- Viénot, F., Brettel, H., & Mollon, J. D. (1999). *Digital video colourmaps for checking the legibility of displays by dichromats.* Color Research & Application.
|
|
247
|
+
- Brettel, H., Viénot, F., & Mollon, J. D. (1997). *Computerized simulation of color appearance for dichromats.* JOSA A.
|
|
248
|
+
- Machado, G. M., Oliveira, M. M., & Fernandes, L. A. F. (2009). *A physiologically-based model for simulation of color vision deficiency.* IEEE TVCG.
|
|
249
|
+
- DaltonLens — *Accurate SVG filters for color blindness simulation*: <https://daltonlens.org/cvd-simulation-svg-filters/>
|
|
191
250
|
|
|
192
251
|
## License
|
|
193
252
|
|
package/dist/index.d.ts
CHANGED
|
@@ -89,6 +89,15 @@ declare const PRESET_IDS: readonly PresetId[];
|
|
|
89
89
|
* No attributes ⇒ all 17 (current behavior).
|
|
90
90
|
*/
|
|
91
91
|
declare function resolveEnabledControls(controlsAttr?: string | null, excludeAttr?: string | null): Set<ControlId>;
|
|
92
|
+
/**
|
|
93
|
+
* Controls that cannot be confined to a container (`scope=`): the full-screen
|
|
94
|
+
* overlays (grayscale, reading guide, reading mask), the colour-blind root
|
|
95
|
+
* `filter` on `<html>`, and the window-level big cursor. A scoped instance
|
|
96
|
+
* auto-excludes these — a per-pane panel only offers what it can regionalise.
|
|
97
|
+
*/
|
|
98
|
+
declare const NON_SCOPABLE_CONTROLS: readonly ControlId[];
|
|
99
|
+
/** Remove the non-scopable controls from an enabled set (scoped mode). */
|
|
100
|
+
declare function scopedControls(enabled: Set<ControlId>): Set<ControlId>;
|
|
92
101
|
|
|
93
102
|
type Position = 'top-left' | 'top-center' | 'top-right' | 'mid-left' | 'mid-center' | 'mid-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
94
103
|
interface RenderOptions {
|
|
@@ -126,6 +135,10 @@ interface BehaviorOptions {
|
|
|
126
135
|
/** Desplazamiento máximo del usuario para el trigger en px (#5). 0/ausente
|
|
127
136
|
* desactiva el nudge (comportamiento actual). */
|
|
128
137
|
nudgeMax?: number;
|
|
138
|
+
/** Contenedor al que confinar los efectos (`scope=`). Si se pasa, las clases
|
|
139
|
+
* scopables van a este elemento en vez de a `body`, y los efectos no
|
|
140
|
+
* regionalizables (overlays, daltonismo, cursor) no se aplican. */
|
|
141
|
+
scopeEl?: HTMLElement | null;
|
|
129
142
|
}
|
|
130
143
|
/** Dispose function with imperative open/close/toggle attached (#2). Calling
|
|
131
144
|
* the value disposes; the methods drive the panel host-side. */
|
|
@@ -144,5 +157,22 @@ declare function bindPanelBehavior(root: ShadowRoot, opts?: BehaviorOptions): Pa
|
|
|
144
157
|
|
|
145
158
|
declare const PANEL_CSS = "\n:host {\n --oks-btn-size: 55px;\n --oks-bg: #000;\n --oks-icon: #fff;\n --oks-h-bg: #fff;\n --oks-h-icon: #000;\n --oks-z: 9999999;\n}\n.oks-access-wrapper {\n position: fixed;\n z-index: var(--oks-z);\n line-height: 1;\n}\n.oks-access-btn {\n width: var(--oks-btn-size);\n height: var(--oks-btn-size);\n border-radius: 50%;\n background: var(--oks-bg);\n color: var(--oks-icon);\n border: 2px solid #fff;\n box-shadow: 0 4px 15px rgba(0,0,0,0.2);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: 0.2s;\n padding: 0;\n}\n.oks-access-btn:hover {\n background: var(--oks-h-bg);\n color: var(--oks-h-icon);\n transform: scale(1.1);\n}\n.oks-access-btn svg {\n fill: currentColor;\n display: block;\n margin: 0 auto;\n width: 60%;\n height: 60%;\n}\n/* Porthole preset: a brushed-metal ring framing the standard glyph. The glyph\n itself is untouched (discoverability), only the button gets the frame.\n Hosts wanting a bespoke frame can target ::part(trigger) from light DOM. */\n.oks-access-btn[data-trigger-icon=\"porthole\"] {\n box-shadow: 0 0 0 4px #b0bec5, 0 0 0 7px #546e7a, 0 6px 18px rgba(0,0,0,0.3);\n}\n.oks-active-badge {\n position: absolute;\n top: -2px;\n right: -2px;\n width: 20px;\n height: 20px;\n background: #25D366;\n border-radius: 50%;\n border: 2px solid #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n opacity: 0;\n pointer-events: none;\n}\n.oks-active-badge svg { width: 12px; height: 12px; }\n.oks-access-wrapper.has-active .oks-active-badge { opacity: 1; }\n\n.oks-access-panel {\n position: fixed;\n width: 340px;\n max-height: 90vh;\n background: #fff;\n border-radius: 20px;\n box-shadow: 0 20px 60px rgba(0,0,0,0.3);\n z-index: var(--oks-z);\n display: flex;\n flex-direction: column;\n opacity: 0;\n pointer-events: none;\n transition: 0.2s;\n border: 1px solid rgba(0,0,0,0.1);\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: #333;\n}\n.oks-access-panel.is-open { opacity: 1; pointer-events: all; }\n.oks-access-header {\n padding: 15px 20px;\n border-bottom: 1px solid #eee;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.oks-access-header h3 { margin: 0; font-size: 18px; color: #000; }\n.oks-access-close {\n background: #f0f0f0;\n color: #333;\n border: 1px solid #ddd;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: 0.2s;\n padding: 0;\n}\n.oks-access-close:hover { background: #e0e0e0; border-color: #ccc; }\n.oks-access-close svg { width: 24px; height: 24px; stroke-width: 2.5px; }\n.oks-access-content { padding: 0 20px 20px; overflow-y: auto; }\n.oks-access-title {\n margin: 10px 0 5px;\n font-size: 11px;\n font-weight: 800;\n text-transform: uppercase;\n color: #888;\n}\n.oks-access-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }\n.oks-access-presets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }\n.oks-preset {\n background: #f0f4f8;\n border: 2px solid transparent;\n border-radius: 8px;\n padding: 8px 4px;\n display: flex;\n flex-direction: column;\n align-items: center;\n cursor: pointer;\n font: inherit;\n color: #333;\n min-width: 0;\n transition: 0.15s;\n}\n.oks-preset:hover { background: #000; color: #fff; }\n.oks-preset:focus-visible { outline: 2px solid #000; outline-offset: 2px; }\n/* Click feedback: a 250 ms flip + slight squeeze. Pure transient \u2014 the\n button does not carry persistent active state because a preset is a\n trigger, not a mode. */\n.oks-preset.is-flashing {\n background: #000;\n color: #fff;\n transform: scale(0.96);\n}\n.oks-preset .oks-icon { font-size: 22px; margin-bottom: 4px; }\n.oks-preset .oks-icon svg { width: 20px; height: 20px; }\n.oks-preset .oks-label {\n font-size: 10px; font-weight: 700; text-transform: uppercase;\n text-align: center; line-height: 1.15;\n overflow-wrap: anywhere; word-break: break-word;\n /* Reserve room for 2 lines so 1-line labels (DISLEXIA, MOTOR) and\n 2-line labels (BAJA VISI\u00D3N, SIN DISTRAC.) end up the same height,\n centred. Without this the row looks ragged. */\n min-height: 2.3em;\n display: flex; align-items: center; justify-content: center;\n}\n.oks-access-opt {\n background: #f9f9f9;\n border: 2px solid #eee;\n border-radius: 10px;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n min-width: 0;\n min-height: 70px;\n color: #333;\n transition: 0.2s;\n font: inherit;\n}\n.oks-access-opt.full-width { grid-column: span 2; }\n.oks-access-opt.is-active {\n border-color: #000;\n background: #fff;\n box-shadow: 0 0 0 1px #000;\n}\n.oks-access-opt:hover { background: #000; color: #fff; border-color: #000; }\n.oks-icon { font-size: 28px; margin-bottom: 3px; display: block; line-height: 1; }\n.oks-icon svg { width: 24px; height: 24px; fill: currentColor; }\n.oks-label {\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n text-align: center;\n line-height: 1.2;\n overflow-wrap: anywhere;\n word-break: break-word;\n}\n.oks-levels { display: flex; gap: 3px; height: 5px; width: 50%; margin-top: 5px; }\n.oks-levels span { flex: 1; background: #ddd; border-radius: 3px; }\n.oks-access-opt[data-level=\"1\"] .oks-levels span:nth-child(1),\n.oks-access-opt[data-level=\"2\"] .oks-levels span:nth-child(-n+2),\n.oks-access-opt[data-level=\"3\"] .oks-levels span:nth-child(-n+3),\n.oks-access-opt[data-level=\"4\"] .oks-levels span:nth-child(-n+4) { background: #000; }\n.oks-access-opt:hover .oks-levels span { background: #555; }\n.oks-access-footer {\n padding: 12px 20px;\n border-top: 1px solid #eee;\n text-align: center;\n}\n.oks-access-reset {\n width: 100%;\n padding: 8px;\n border: 2px solid #000;\n color: #000;\n background: transparent;\n font-weight: 700;\n cursor: pointer;\n border-radius: 6px;\n font-size: 12px;\n}\n.oks-access-reset:hover { background: #000; color: #fff; }\n.oks-access-branding { margin-top: 12px; font-size: 12px; color: #000; font-weight: 700; }\n.oks-access-branding a { color: #000; text-decoration: none; border-bottom: 1px dotted #000; }\n\n@media (prefers-reduced-motion: reduce) {\n .oks-access-btn, .oks-access-panel, .oks-access-close,\n .oks-preset, .oks-access-opt { transition: none; }\n .oks-access-btn:hover { transform: scale(1); }\n .oks-preset.is-flashing { transform: none; }\n}\n@media (max-width: 768px) {\n .oks-access-panel {\n width: 100%; height: 100%; max-height: 100%;\n top: 0; left: 0; right: 0; bottom: 0;\n border-radius: 0;\n }\n .oks-access-opt { min-height: 72px; padding: 10px 8px; }\n .oks-icon svg { width: 26px; height: 26px; }\n .oks-label { font-size: 12px; line-height: 1.25; }\n .oks-access-grid { gap: 8px; }\n .oks-access-content { padding: 0 14px 20px; }\n .oks-access-title { font-size: 11px; margin: 10px 0 4px; }\n /* Cursor grande no aplica en t\u00E1ctil: oculto y dejo el \u00FAltimo bot\u00F3n\n de la secci\u00F3n ocupando 2 columnas para no romper la grilla par. */\n .oks-access-opt[data-class=\"oks-big-cursor\"] { display: none; }\n .oks-access-opt[data-class=\"oks-a11y-focus\"] { grid-column: span 2; }\n .oks-access-reset { padding: 14px; font-size: 14px; }\n}\n";
|
|
146
159
|
declare const EFFECT_CSS = "\nhtml.oks-colorblind-1 { filter: url('#oks-filter-protanopia'); }\nhtml.oks-colorblind-2 { filter: url('#oks-filter-deuteranopia'); }\nhtml.oks-colorblind-3 { filter: url('#oks-filter-tritanopia'); }\n\n/* Text-size levels.\n Applied to <html> via :has(), not to <body>. rem is anchored to the root\n element, so a site whose CSS sizes things in rem (most modern Astro / Tailwind\n builds) needs the root font-size to change for the scale to take effect.\n A previous version applied this to body with %, which only moved descendants\n that inherited font-size from body \u2014 anything sized in rem stayed locked to\n the 16px default of <html>. An even earlier version used the universal\n selector with em and compounded the factor at every nesting level.\n :has(body.oks-zoom-N) is the right anchor: one change at the root, rem\n descendants scale exactly once. Hard-coded px is intentionally left alone;\n browser zoom covers that case. */\nhtml:has(body.oks-zoom-1) { font-size: 110% !important; }\nhtml:has(body.oks-zoom-2) { font-size: 120% !important; }\nhtml:has(body.oks-zoom-3) { font-size: 135% !important; }\nhtml:has(body.oks-zoom-4) { font-size: 150% !important; }\n\nbody.oks-lh-1 * { line-height: 1.6 !important; }\nbody.oks-lh-2 * { line-height: 1.9 !important; }\nbody.oks-lh-3 * { line-height: 2.2 !important; }\n\nbody.oks-a11y-font { font-family: Arial, sans-serif !important; }\n\nbody.oks-dyslexia * {\n font-family: 'Comic Sans MS', 'Verdana', sans-serif !important;\n letter-spacing: 0.05em !important;\n word-spacing: 0.1em !important;\n line-height: 1.6 !important;\n}\n\nbody.oks-a11y-hide img { opacity: 0 !important; visibility: hidden !important; }\nbody.oks-a11y-links a { text-decoration: underline !important; background: #ff0 !important; color: #000 !important; }\n\nbody.oks-align-1 * { text-align: left !important; }\nbody.oks-align-2 * { text-align: center !important; }\nbody.oks-align-3 * { text-align: right !important; }\n\nbody.oks-a11y-pause * { animation: none !important; transition: none !important; }\n\nbody.oks-ls-1 * { letter-spacing: 0.05em !important; }\nbody.oks-ls-2 * { letter-spacing: 0.10em !important; }\nbody.oks-ls-3 * { letter-spacing: 0.16em !important; }\n\nbody.oks-a11y-focus a:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-focus button:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-focus input,\nbody.oks-a11y-focus select,\nbody.oks-a11y-focus textarea {\n outline: 2px dashed rgba(0, 95, 204, 0.45) !important;\n outline-offset: 2px !important;\n}\nbody.oks-a11y-focus *:focus-visible {\n outline: 3px solid #005fcc !important;\n outline-offset: 3px !important;\n box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25) !important;\n}\n\nbody.oks-a11y-contrast.oks-a11y-focus *:focus,\nbody.oks-a11y-contrast.oks-a11y-focus *:focus-visible {\n outline-color: #0ff !important;\n box-shadow: 0 0 0 6px rgba(0, 255, 255, 0.3) !important;\n}\n\n@media (pointer: fine) {\n body.oks-big-cursor, body.oks-big-cursor a, body.oks-big-cursor button {\n cursor: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 24 24'%3E%3Cpath d='M4 2L4 18L8 14L11 21L14 19.5L11 13L15.5 13Z' stroke='white' stroke-width='4' stroke-linejoin='round' fill='white'/%3E%3Cpath d='M4 2L4 18L8 14L11 21L14 19.5L11 13L15.5 13Z' fill='black'/%3E%3C/svg%3E\") 8 4, auto !important;\n }\n}\n\nbody.oks-a11y-contrast,\nbody.oks-a11y-contrast *:not(oksigenia-access-panel):not(oksigenia-access-panel *) {\n background-color: #000 !important;\n color: #ff0 !important;\n border-color: #ff0 !important;\n text-shadow: none !important;\n box-shadow: none !important;\n}\nbody.oks-a11y-contrast img { filter: grayscale(100%) contrast(120%) !important; }\nbody.oks-a11y-contrast a:not(oksigenia-access-panel *) { color: #0ff !important; text-decoration: underline !important; }\n\n/* High-contrast applies background:#000 to every descendant of body to flip\n the page to inverted colours. That selector also catches our own overlays\n (.oks-reading-guide, .oks-overlay-effect), which would then paint a solid\n black band on top of the text and defeat their purpose. Restore the\n overlay-specific values here so they keep working in high-contrast mode. */\nbody.oks-a11y-contrast .oks-reading-guide {\n background-color: rgba(255, 255, 0, 0.25) !important;\n border-top-color: #ff0 !important;\n border-bottom-color: #ff0 !important;\n}\nbody.oks-a11y-contrast .oks-overlay-effect {\n background-color: transparent !important;\n}\n\n.oks-overlay-effect {\n position: fixed; top: 0; left: 0;\n width: 100%; height: 100%;\n pointer-events: none;\n z-index: 999990;\n display: none;\n backdrop-filter: grayscale(100%);\n}\n.oks-overlay-effect.is-active { display: block; }\n\n.oks-reading-guide {\n position: fixed; left: 0;\n width: 100%; height: 50px;\n background: rgba(255, 255, 0, 0.2);\n border-top: 3px solid red;\n border-bottom: 3px solid red;\n pointer-events: none;\n z-index: 2147483647;\n display: none;\n transform: translateY(-50%);\n}\nbody.oks-a11y-guide .oks-reading-guide { display: block; }\n\n/* Reading mask: dark overlay leaving a horizontal band lit around the cursor.\n --oks-mask-y is updated from JS on mousemove; band is \u00B190px around it. */\n.oks-reading-mask {\n position: fixed; top: 0; left: 0;\n width: 100vw; height: 100vh;\n pointer-events: none;\n z-index: 2147483646;\n background: rgba(0, 0, 0, 0.75);\n display: none;\n clip-path: polygon(\n 0 0, 100% 0,\n 100% calc(var(--oks-mask-y, 50vh) - 90px),\n 0 calc(var(--oks-mask-y, 50vh) - 90px),\n 0 calc(var(--oks-mask-y, 50vh) + 90px),\n 100% calc(var(--oks-mask-y, 50vh) + 90px),\n 100% 100%, 0 100%\n );\n}\nbody.oks-a11y-mask .oks-reading-mask { display: block; }\nbody.oks-a11y-contrast .oks-reading-mask { background-color: rgba(0, 0, 0, 0.85) !important; }\n\n/* Big targets: bump interactive hit-areas to WCAG 2.5.5 (44\u00D744 minimum).\n Only adjusts padding + min-* \u2014 never display, so layouts that rely on\n inline flow or grid placement survive. Exempts our own shadow-DOM host. */\nbody.oks-a11y-bigtargets a:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-bigtargets button:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-bigtargets [role=\"button\"]:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-bigtargets input[type=\"checkbox\"]:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-bigtargets input[type=\"radio\"]:not(oksigenia-access-panel):not(oksigenia-access-panel *),\nbody.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-access-panel *) {\n min-height: 44px !important;\n min-width: 44px !important;\n padding: 8px 12px !important;\n box-sizing: border-box !important;\n}\n";
|
|
160
|
+
/**
|
|
161
|
+
* Scoped-mode effect CSS (#scope). Emits only the controls that can be confined
|
|
162
|
+
* to a container by selector — the same rules as EFFECT_CSS but anchored to the
|
|
163
|
+
* scope element instead of `body`. The non-scopable effects (colour-blind, which
|
|
164
|
+
* is a `filter` on `<html>`; the full-screen overlays grayscale / reading guide /
|
|
165
|
+
* reading mask; and big cursor) are NOT here — a scoped instance auto-excludes
|
|
166
|
+
* those controls, so they never appear in a per-pane panel.
|
|
167
|
+
*
|
|
168
|
+
* Text size scopes as the container's `font-size`, so it scales `em`/inherited
|
|
169
|
+
* text inside the pane but not `rem` (which is always relative to `<html>` —
|
|
170
|
+
* there is no CSS way to anchor rem to a container). Hosts that want per-pane
|
|
171
|
+
* text scaling size that pane's text in `em`.
|
|
172
|
+
*
|
|
173
|
+
* `s` is a host-provided CSS selector (e.g. `#map-pane`), trusted like the
|
|
174
|
+
* effects-exclude value.
|
|
175
|
+
*/
|
|
176
|
+
declare function scopedEffectCss(s: string): string;
|
|
147
177
|
|
|
148
|
-
export { ALL_CONTROLS, type BehaviorOptions, COLORBLIND_FILTERS_SVG, type ControlId, DEFAULT_STATE, EFFECT_CSS, type LocaleCode, PANEL_CSS, PRESETS, PRESET_IDS, type PanelController, type PanelState, type Position, type PresetId, type Translation, type TriggerIcon, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, supportedLocales };
|
|
178
|
+
export { ALL_CONTROLS, type BehaviorOptions, COLORBLIND_FILTERS_SVG, type ControlId, DEFAULT_STATE, EFFECT_CSS, type LocaleCode, NON_SCOPABLE_CONTROLS, PANEL_CSS, PRESETS, PRESET_IDS, type PanelController, type PanelState, type Position, type PresetId, type Translation, type TriggerIcon, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, scopedControls, scopedEffectCss, supportedLocales };
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,18 @@ function resolveEnabledControls(controlsAttr, excludeAttr) {
|
|
|
110
110
|
}
|
|
111
111
|
return enabled;
|
|
112
112
|
}
|
|
113
|
+
var NON_SCOPABLE_CONTROLS = [
|
|
114
|
+
"grayscale",
|
|
115
|
+
"reading-guide",
|
|
116
|
+
"reading-mask",
|
|
117
|
+
"big-cursor",
|
|
118
|
+
"colorblind"
|
|
119
|
+
];
|
|
120
|
+
function scopedControls(enabled) {
|
|
121
|
+
const out = new Set(enabled);
|
|
122
|
+
for (const id of NON_SCOPABLE_CONTROLS) out.delete(id);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
113
125
|
function filterPresetForEnabled(id, enabled) {
|
|
114
126
|
const out = {};
|
|
115
127
|
for (const [k, v] of Object.entries(PRESETS[id])) {
|
|
@@ -363,30 +375,36 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
363
375
|
}
|
|
364
376
|
let state = loadState(storageKey);
|
|
365
377
|
function applyState() {
|
|
366
|
-
const
|
|
378
|
+
const scoped = opts.scopeEl !== void 0;
|
|
379
|
+
const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
|
|
367
380
|
const rootEl = document.documentElement;
|
|
368
|
-
|
|
369
|
-
|
|
381
|
+
if (fxRoot) {
|
|
382
|
+
for (const cls of Array.from(fxRoot.classList)) {
|
|
383
|
+
if (cls.startsWith("oks-")) fxRoot.classList.remove(cls);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!scoped) [1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
|
|
387
|
+
if (fxRoot) {
|
|
388
|
+
if (state.zoom > 0) fxRoot.classList.add(`oks-zoom-${state.zoom}`);
|
|
389
|
+
if (state.lh > 0) fxRoot.classList.add(`oks-lh-${state.lh}`);
|
|
390
|
+
if (state.align > 0) fxRoot.classList.add(`oks-align-${state.align}`);
|
|
391
|
+
if (state.ls > 0) fxRoot.classList.add(`oks-ls-${state.ls}`);
|
|
392
|
+
if (state.font) fxRoot.classList.add("oks-a11y-font");
|
|
393
|
+
if (state.dyslexia) fxRoot.classList.add("oks-dyslexia");
|
|
394
|
+
if (state.contrast) fxRoot.classList.add("oks-a11y-contrast");
|
|
395
|
+
if (state.hideImages) fxRoot.classList.add("oks-a11y-hide");
|
|
396
|
+
if (state.highlightLinks) fxRoot.classList.add("oks-a11y-links");
|
|
397
|
+
if (state.pauseAnim) fxRoot.classList.add("oks-a11y-pause");
|
|
398
|
+
if (state.focusOutline) fxRoot.classList.add("oks-a11y-focus");
|
|
399
|
+
if (state.bigTargets) fxRoot.classList.add("oks-a11y-bigtargets");
|
|
400
|
+
}
|
|
401
|
+
if (!scoped) {
|
|
402
|
+
if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
|
|
403
|
+
if (state.bigCursor) document.body.classList.add("oks-big-cursor");
|
|
404
|
+
if (state.readingGuide) document.body.classList.add("oks-a11y-guide");
|
|
405
|
+
if (state.readingMask) document.body.classList.add("oks-a11y-mask");
|
|
406
|
+
ensureOverlay().classList.toggle("is-active", state.grayOverlay);
|
|
370
407
|
}
|
|
371
|
-
[1, 2, 3].forEach((l) => rootEl.classList.remove(`oks-colorblind-${l}`));
|
|
372
|
-
if (state.zoom > 0) body.classList.add(`oks-zoom-${state.zoom}`);
|
|
373
|
-
if (state.lh > 0) body.classList.add(`oks-lh-${state.lh}`);
|
|
374
|
-
if (state.align > 0) body.classList.add(`oks-align-${state.align}`);
|
|
375
|
-
if (state.ls > 0) body.classList.add(`oks-ls-${state.ls}`);
|
|
376
|
-
if (state.colorblind > 0) rootEl.classList.add(`oks-colorblind-${state.colorblind}`);
|
|
377
|
-
if (state.font) body.classList.add("oks-a11y-font");
|
|
378
|
-
if (state.dyslexia) body.classList.add("oks-dyslexia");
|
|
379
|
-
if (state.contrast) body.classList.add("oks-a11y-contrast");
|
|
380
|
-
if (state.hideImages) body.classList.add("oks-a11y-hide");
|
|
381
|
-
if (state.highlightLinks) body.classList.add("oks-a11y-links");
|
|
382
|
-
if (state.bigCursor) body.classList.add("oks-big-cursor");
|
|
383
|
-
if (state.pauseAnim) body.classList.add("oks-a11y-pause");
|
|
384
|
-
if (state.focusOutline) body.classList.add("oks-a11y-focus");
|
|
385
|
-
if (state.readingGuide) body.classList.add("oks-a11y-guide");
|
|
386
|
-
if (state.readingMask) body.classList.add("oks-a11y-mask");
|
|
387
|
-
if (state.bigTargets) body.classList.add("oks-a11y-bigtargets");
|
|
388
|
-
const overlay = ensureOverlay();
|
|
389
|
-
overlay.classList.toggle("is-active", state.grayOverlay);
|
|
390
408
|
syncButtonsFromState();
|
|
391
409
|
wrapper?.classList.toggle("has-active", !isStateEmpty(state));
|
|
392
410
|
}
|
|
@@ -465,11 +483,24 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
465
483
|
};
|
|
466
484
|
let opener = null;
|
|
467
485
|
let ignoreDocClose = false;
|
|
486
|
+
const positionScopedDialog = () => {
|
|
487
|
+
const el = opts.scopeEl;
|
|
488
|
+
if (!el) return;
|
|
489
|
+
const r = el.getBoundingClientRect();
|
|
490
|
+
if (r.width === 0 && r.height === 0) return;
|
|
491
|
+
const pad = 8;
|
|
492
|
+
panel.style.top = `${Math.max(pad, r.top + pad)}px`;
|
|
493
|
+
panel.style.bottom = "auto";
|
|
494
|
+
panel.style.left = `${r.left + r.width / 2}px`;
|
|
495
|
+
panel.style.right = "auto";
|
|
496
|
+
panel.style.transform = "translateX(-50%)";
|
|
497
|
+
};
|
|
468
498
|
const openPanel = () => {
|
|
469
499
|
if (panel.classList.contains("is-open")) return;
|
|
470
500
|
opener = deepActiveElement();
|
|
471
501
|
panel.classList.add("is-open");
|
|
472
502
|
panel.removeAttribute("inert");
|
|
503
|
+
if (opts.scopeEl) positionScopedDialog();
|
|
473
504
|
trigger?.setAttribute("aria-expanded", "true");
|
|
474
505
|
ignoreDocClose = true;
|
|
475
506
|
setTimeout(() => {
|
|
@@ -636,6 +667,13 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
636
667
|
document.addEventListener("keydown", onKeyDown);
|
|
637
668
|
document.addEventListener("mousemove", onMove);
|
|
638
669
|
document.addEventListener("touchmove", onMove, { passive: true });
|
|
670
|
+
const onReposition = () => {
|
|
671
|
+
if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
|
|
672
|
+
};
|
|
673
|
+
if (opts.scopeEl !== void 0) {
|
|
674
|
+
window.addEventListener("scroll", onReposition, true);
|
|
675
|
+
window.addEventListener("resize", onReposition);
|
|
676
|
+
}
|
|
639
677
|
if (nudgeMax > 0 && trigger) {
|
|
640
678
|
loadNudge();
|
|
641
679
|
applyNudge();
|
|
@@ -656,6 +694,10 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
656
694
|
document.removeEventListener("keydown", onKeyDown);
|
|
657
695
|
document.removeEventListener("mousemove", onMove);
|
|
658
696
|
document.removeEventListener("touchmove", onMove);
|
|
697
|
+
if (opts.scopeEl !== void 0) {
|
|
698
|
+
window.removeEventListener("scroll", onReposition, true);
|
|
699
|
+
window.removeEventListener("resize", onReposition);
|
|
700
|
+
}
|
|
659
701
|
trigger?.removeEventListener("pointerdown", onPointerDown);
|
|
660
702
|
document.removeEventListener("pointermove", onPointerMove);
|
|
661
703
|
document.removeEventListener("pointerup", onPointerUp);
|
|
@@ -1072,7 +1114,81 @@ body.oks-a11y-bigtargets summary:not(oksigenia-access-panel):not(oksigenia-acces
|
|
|
1072
1114
|
box-sizing: border-box !important;
|
|
1073
1115
|
}
|
|
1074
1116
|
`;
|
|
1117
|
+
function scopedEffectCss(s) {
|
|
1118
|
+
return `
|
|
1119
|
+
${s}.oks-zoom-1 { font-size: 110% !important; }
|
|
1120
|
+
${s}.oks-zoom-2 { font-size: 120% !important; }
|
|
1121
|
+
${s}.oks-zoom-3 { font-size: 135% !important; }
|
|
1122
|
+
${s}.oks-zoom-4 { font-size: 150% !important; }
|
|
1123
|
+
|
|
1124
|
+
${s}.oks-lh-1 * { line-height: 1.6 !important; }
|
|
1125
|
+
${s}.oks-lh-2 * { line-height: 1.9 !important; }
|
|
1126
|
+
${s}.oks-lh-3 * { line-height: 2.2 !important; }
|
|
1127
|
+
|
|
1128
|
+
${s}.oks-a11y-font, ${s}.oks-a11y-font * { font-family: Arial, sans-serif !important; }
|
|
1129
|
+
|
|
1130
|
+
${s}.oks-dyslexia * {
|
|
1131
|
+
font-family: 'Comic Sans MS', 'Verdana', sans-serif !important;
|
|
1132
|
+
letter-spacing: 0.05em !important;
|
|
1133
|
+
word-spacing: 0.1em !important;
|
|
1134
|
+
line-height: 1.6 !important;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
${s}.oks-a11y-hide img { opacity: 0 !important; visibility: hidden !important; }
|
|
1138
|
+
${s}.oks-a11y-links a { text-decoration: underline !important; background: #ff0 !important; color: #000 !important; }
|
|
1139
|
+
|
|
1140
|
+
${s}.oks-align-1 * { text-align: left !important; }
|
|
1141
|
+
${s}.oks-align-2 * { text-align: center !important; }
|
|
1142
|
+
${s}.oks-align-3 * { text-align: right !important; }
|
|
1143
|
+
|
|
1144
|
+
${s}.oks-a11y-pause * { animation: none !important; transition: none !important; }
|
|
1145
|
+
|
|
1146
|
+
${s}.oks-ls-1 * { letter-spacing: 0.05em !important; }
|
|
1147
|
+
${s}.oks-ls-2 * { letter-spacing: 0.10em !important; }
|
|
1148
|
+
${s}.oks-ls-3 * { letter-spacing: 0.16em !important; }
|
|
1149
|
+
|
|
1150
|
+
${s}.oks-a11y-focus a,
|
|
1151
|
+
${s}.oks-a11y-focus button,
|
|
1152
|
+
${s}.oks-a11y-focus input,
|
|
1153
|
+
${s}.oks-a11y-focus select,
|
|
1154
|
+
${s}.oks-a11y-focus textarea {
|
|
1155
|
+
outline: 2px dashed rgba(0, 95, 204, 0.45) !important;
|
|
1156
|
+
outline-offset: 2px !important;
|
|
1157
|
+
}
|
|
1158
|
+
${s}.oks-a11y-focus *:focus-visible {
|
|
1159
|
+
outline: 3px solid #005fcc !important;
|
|
1160
|
+
outline-offset: 3px !important;
|
|
1161
|
+
box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25) !important;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
${s}.oks-a11y-contrast, ${s}.oks-a11y-contrast * {
|
|
1165
|
+
background-color: #000 !important;
|
|
1166
|
+
color: #ff0 !important;
|
|
1167
|
+
border-color: #ff0 !important;
|
|
1168
|
+
text-shadow: none !important;
|
|
1169
|
+
box-shadow: none !important;
|
|
1170
|
+
}
|
|
1171
|
+
${s}.oks-a11y-contrast img { filter: grayscale(100%) contrast(120%) !important; }
|
|
1172
|
+
${s}.oks-a11y-contrast a { color: #0ff !important; text-decoration: underline !important; }
|
|
1173
|
+
${s}.oks-a11y-contrast.oks-a11y-focus *:focus-visible {
|
|
1174
|
+
outline-color: #0ff !important;
|
|
1175
|
+
box-shadow: 0 0 0 6px rgba(0, 255, 255, 0.3) !important;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
${s}.oks-a11y-bigtargets a,
|
|
1179
|
+
${s}.oks-a11y-bigtargets button,
|
|
1180
|
+
${s}.oks-a11y-bigtargets [role="button"],
|
|
1181
|
+
${s}.oks-a11y-bigtargets input[type="checkbox"],
|
|
1182
|
+
${s}.oks-a11y-bigtargets input[type="radio"],
|
|
1183
|
+
${s}.oks-a11y-bigtargets summary {
|
|
1184
|
+
min-height: 44px !important;
|
|
1185
|
+
min-width: 44px !important;
|
|
1186
|
+
padding: 8px 12px !important;
|
|
1187
|
+
box-sizing: border-box !important;
|
|
1188
|
+
}
|
|
1189
|
+
`;
|
|
1190
|
+
}
|
|
1075
1191
|
|
|
1076
|
-
export { ALL_CONTROLS, COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, PANEL_CSS, PRESETS, PRESET_IDS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, supportedLocales };
|
|
1192
|
+
export { ALL_CONTROLS, COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, NON_SCOPABLE_CONTROLS, PANEL_CSS, PRESETS, PRESET_IDS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, scopedControls, scopedEffectCss, supportedLocales };
|
|
1077
1193
|
//# sourceMappingURL=index.js.map
|
|
1078
1194
|
//# sourceMappingURL=index.js.map
|