@oksigenia/access-panel 0.7.3 → 0.8.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 +10 -0
- package/README.md +17 -0
- package/dist/behavior-CEe_YuMU.d.ts +118 -0
- package/dist/index.d.ts +4 -98
- package/dist/index.js +49 -7
- package/dist/index.js.map +1 -1
- package/dist/web-component.d.ts +8 -0
- package/dist/web-component.js +55 -7
- package/dist/web-component.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @oksigenia/access-panel
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
Nothing changes for sites that don't use the two additions below.
|
|
8
|
+
|
|
9
|
+
- **`oksiac:change` event.** Fired on the element after every change the visitor makes (a control, a profile, Reset), once the state is saved, with the full state in `detail.state`. It bubbles out of the shadow root and doesn't fire on load, so a host can store the settings on its side (a user account, say) without loops.
|
|
10
|
+
- **`initial-state` attribute.** A JSON state the panel starts from, winning over localStorage and saved there. Read on the first render only; invalid JSON is ignored and localStorage stands. Together with the event, it lets the settings follow a signed-in user across devices.
|
|
11
|
+
- **`sanitizeState` and `parseState`** exported. They keep only the known keys with valid values. `loadState` now goes through them too, so stray keys or out-of-range levels in localStorage are dropped instead of carried along.
|
|
12
|
+
|
|
3
13
|
## 0.7.3
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Attributes:
|
|
|
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
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. |
|
|
75
|
+
| `initial-state` | none | A JSON state the panel starts from, winning over localStorage (and saved there). Read on the first render only. Invalid JSON is ignored and localStorage stands. For hosts that keep the visitor's settings on their side; see below. |
|
|
75
76
|
|
|
76
77
|
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`.
|
|
77
78
|
|
|
@@ -91,6 +92,22 @@ With `trigger="none"`, mount the panel and open it from anywhere:
|
|
|
91
92
|
|
|
92
93
|
`.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.
|
|
93
94
|
|
|
95
|
+
### Keeping the settings on your side
|
|
96
|
+
|
|
97
|
+
By default the visitor's settings live in their browser. If your site has accounts and you want the settings to follow the user across devices, listen for `oksiac:change` and save `detail.state` wherever you keep user data, then hand it back with `initial-state` when you render the page:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<oksigenia-access-panel id="a11y" initial-state='{"zoom":2,"dyslexia":true}'></oksigenia-access-panel>
|
|
101
|
+
<script type="module">
|
|
102
|
+
import '@oksigenia/access-panel/web-component';
|
|
103
|
+
document.getElementById('a11y').addEventListener('oksiac:change', (e) => {
|
|
104
|
+
fetch('/my-account/a11y', { method: 'POST', body: JSON.stringify(e.detail.state) });
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`oksiac:change` fires after every change the visitor makes (a control, a profile, Reset), once the state is saved, and bubbles out of the element. It doesn't fire on load or when `initial-state` is applied, so saving it back can't loop. In scoped mode, the window-level controls (big cursor, reading guide and mask) keep their own shared state and don't fire it. `initial-state` takes the same keys as `detail.state`; unknown keys and out-of-range values are dropped.
|
|
110
|
+
|
|
94
111
|
### Per-container accessibility (scoped mode)
|
|
95
112
|
|
|
96
113
|
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:
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
interface PanelState {
|
|
2
|
+
/** Niveles 1..4 → 0 desactivado. */
|
|
3
|
+
zoom: number;
|
|
4
|
+
/** Niveles 1..3 → 0 desactivado. */
|
|
5
|
+
lh: number;
|
|
6
|
+
/** Niveles 1..3 → 0 desactivado. */
|
|
7
|
+
align: number;
|
|
8
|
+
/** Niveles 1..3 → 0 desactivado. */
|
|
9
|
+
ls: number;
|
|
10
|
+
/** Niveles 1..3 (1=protanopia, 2=deuteranopia, 3=tritanopia). 0 desactivado. */
|
|
11
|
+
colorblind: number;
|
|
12
|
+
/** Toggles. */
|
|
13
|
+
font: boolean;
|
|
14
|
+
dyslexia: boolean;
|
|
15
|
+
contrast: boolean;
|
|
16
|
+
hideImages: boolean;
|
|
17
|
+
highlightLinks: boolean;
|
|
18
|
+
bigCursor: boolean;
|
|
19
|
+
pauseAnim: boolean;
|
|
20
|
+
focusOutline: boolean;
|
|
21
|
+
/** Overlay escala de grises (excluyente con contrast). */
|
|
22
|
+
grayOverlay: boolean;
|
|
23
|
+
/** Guía horizontal de lectura. */
|
|
24
|
+
readingGuide: boolean;
|
|
25
|
+
/** Máscara de lectura: oscurece todo menos una banda alrededor del cursor. */
|
|
26
|
+
readingMask: boolean;
|
|
27
|
+
/** Aumenta el hit-area de interactivos a 44×44 mínimo (WCAG 2.5.5/2.5.8). */
|
|
28
|
+
bigTargets: boolean;
|
|
29
|
+
}
|
|
30
|
+
declare const DEFAULT_STATE: Readonly<PanelState>;
|
|
31
|
+
/**
|
|
32
|
+
* Turns anything parsed from JSON into a clean PanelState: only the known
|
|
33
|
+
* keys, levels as integers within range, toggles as booleans. Unknown keys
|
|
34
|
+
* and out-of-range values fall back to the default. Returns null when the
|
|
35
|
+
* input isn't a plain object at all.
|
|
36
|
+
*
|
|
37
|
+
* Toggles also accept 1, the way older WordPress builds stored them.
|
|
38
|
+
*/
|
|
39
|
+
declare function sanitizeState(input: unknown): PanelState | null;
|
|
40
|
+
/** Parses a JSON string (e.g. the `initial-state` attribute) into a clean
|
|
41
|
+
* PanelState, or null when it isn't valid JSON for a state object. */
|
|
42
|
+
declare function parseState(json: string | null | undefined): PanelState | null;
|
|
43
|
+
declare function loadState(key: string): PanelState;
|
|
44
|
+
declare function saveState(key: string, state: PanelState): void;
|
|
45
|
+
declare function isStateEmpty(state: PanelState): boolean;
|
|
46
|
+
|
|
47
|
+
type ControlId = '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';
|
|
48
|
+
/** Render order (also the order `exclude=`/`controls=` are validated against). */
|
|
49
|
+
declare const ALL_CONTROLS: readonly ControlId[];
|
|
50
|
+
type PresetId = 'lowvision' | 'dyslexia' | 'motor' | 'calm';
|
|
51
|
+
/** Preset profiles: set the listed flags, leave the rest as-is.
|
|
52
|
+
* Apply is additive — pressing two presets unions their flags. */
|
|
53
|
+
declare const PRESETS: Readonly<Record<PresetId, Partial<PanelState>>>;
|
|
54
|
+
declare const PRESET_IDS: readonly PresetId[];
|
|
55
|
+
/**
|
|
56
|
+
* Resolve which controls an instance offers from its `controls=` (whitelist)
|
|
57
|
+
* and `exclude=` (blacklist) attributes. Both optional; unknown ids ignored.
|
|
58
|
+
* No attributes ⇒ all 17 (current behavior).
|
|
59
|
+
*/
|
|
60
|
+
declare function resolveEnabledControls(controlsAttr?: string | null, excludeAttr?: string | null): Set<ControlId>;
|
|
61
|
+
/**
|
|
62
|
+
* Controls that can't be confined to a container AND shouldn't even be offered
|
|
63
|
+
* in a scoped panel: grayscale (a full-screen overlay that destroys colour
|
|
64
|
+
* data) and the colour-blind root `filter` (destroys data + the open
|
|
65
|
+
* simulation-vs-correction question). A scoped instance drops these entirely.
|
|
66
|
+
*/
|
|
67
|
+
declare const NON_SCOPABLE_CONTROLS: readonly ControlId[];
|
|
68
|
+
/**
|
|
69
|
+
* Controls that can't be *scoped* but ARE benign window-level aids, so a scoped
|
|
70
|
+
* panel still offers them and applies them globally (to the whole window) with
|
|
71
|
+
* a shared, cross-instance state: big cursor and the reading guide / mask.
|
|
72
|
+
* Toggling from any pane affects the whole window and every pane's button
|
|
73
|
+
* reflects it. (Unlike grayscale/colour-blind, these don't falsify data.)
|
|
74
|
+
*/
|
|
75
|
+
declare const GLOBAL_OFFERABLE_CONTROLS: readonly ControlId[];
|
|
76
|
+
/** Remove the controls a scoped instance doesn't offer at all (grayscale, colour-blind). */
|
|
77
|
+
declare function scopedControls(enabled: Set<ControlId>): Set<ControlId>;
|
|
78
|
+
|
|
79
|
+
interface BehaviorOptions {
|
|
80
|
+
/** Llave localStorage. Default oksiacSettings. */
|
|
81
|
+
storageKey?: string;
|
|
82
|
+
/** Locale para los aria-label dinámicos (nivel de los multinivel). Default en. */
|
|
83
|
+
locale?: string;
|
|
84
|
+
/** Controles que ofrece esta instancia (#1). Default: los 17. Filtra qué
|
|
85
|
+
* flags puede aplicar un perfil. */
|
|
86
|
+
enabled?: Set<ControlId>;
|
|
87
|
+
/** Desplazamiento máximo del usuario para el trigger en px (#5). 0/ausente
|
|
88
|
+
* desactiva el nudge (comportamiento actual). */
|
|
89
|
+
nudgeMax?: number;
|
|
90
|
+
/** Contenedor al que confinar los efectos (`scope=`). Si se pasa, las clases
|
|
91
|
+
* scopables van a este elemento en vez de a `body`, y los efectos no
|
|
92
|
+
* regionalizables (overlays, daltonismo, cursor) no se aplican. */
|
|
93
|
+
scopeEl?: HTMLElement | null;
|
|
94
|
+
/** Starting state that wins over localStorage (`initial-state=`), e.g. the
|
|
95
|
+
* visitor's settings kept in their account on the host. It is written to
|
|
96
|
+
* localStorage too, and does not fire `oksiac:change`. Already sanitised. */
|
|
97
|
+
initialState?: PanelState | null;
|
|
98
|
+
}
|
|
99
|
+
/** Detail of the `oksiac:change` event: the full state after the change. */
|
|
100
|
+
interface PanelChangeDetail {
|
|
101
|
+
state: PanelState;
|
|
102
|
+
}
|
|
103
|
+
/** Dispose function with imperative open/close/toggle attached (#2). Calling
|
|
104
|
+
* the value disposes; the methods drive the panel host-side. */
|
|
105
|
+
interface PanelController {
|
|
106
|
+
(): void;
|
|
107
|
+
open(): void;
|
|
108
|
+
close(): void;
|
|
109
|
+
toggle(): void;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Engancha toda la lógica al panel ya renderizado dentro de `root`
|
|
113
|
+
* (que es el shadowRoot del custom element). Devuelve un PanelController:
|
|
114
|
+
* invocarlo limpia; sus métodos abren/cierran el panel.
|
|
115
|
+
*/
|
|
116
|
+
declare function bindPanelBehavior(root: ShadowRoot, opts?: BehaviorOptions): PanelController;
|
|
117
|
+
|
|
118
|
+
export { ALL_CONTROLS as A, type BehaviorOptions as B, type ControlId as C, DEFAULT_STATE as D, GLOBAL_OFFERABLE_CONTROLS as G, NON_SCOPABLE_CONTROLS as N, PRESETS as P, PRESET_IDS as a, type PanelChangeDetail as b, type PanelController as c, type PanelState as d, type PresetId as e, bindPanelBehavior as f, saveState as g, scopedControls as h, isStateEmpty as i, loadState as l, parseState as p, resolveEnabledControls as r, sanitizeState as s };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { C as ControlId } from './behavior-CEe_YuMU.js';
|
|
2
|
+
export { A as ALL_CONTROLS, B as BehaviorOptions, D as DEFAULT_STATE, G as GLOBAL_OFFERABLE_CONTROLS, N as NON_SCOPABLE_CONTROLS, P as PRESETS, a as PRESET_IDS, b as PanelChangeDetail, c as PanelController, d as PanelState, e as PresetId, f as bindPanelBehavior, i as isStateEmpty, l as loadState, p as parseState, r as resolveEnabledControls, s as sanitizeState, g as saveState, h as scopedControls } from './behavior-CEe_YuMU.js';
|
|
3
|
+
|
|
1
4
|
type LocaleCode = 'es' | 'gn' | 'en' | 'fr' | 'it' | 'de' | 'nl' | 'sv';
|
|
2
5
|
interface Translation {
|
|
3
6
|
title: string;
|
|
@@ -41,72 +44,6 @@ declare function supportedLocales(): readonly LocaleCode[];
|
|
|
41
44
|
type TriggerIcon = 'vitruvian' | 'wheelchair' | 'eye' | 'universal' | 'porthole';
|
|
42
45
|
declare const 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>";
|
|
43
46
|
|
|
44
|
-
interface PanelState {
|
|
45
|
-
/** Niveles 1..4 → 0 desactivado. */
|
|
46
|
-
zoom: number;
|
|
47
|
-
/** Niveles 1..3 → 0 desactivado. */
|
|
48
|
-
lh: number;
|
|
49
|
-
/** Niveles 1..3 → 0 desactivado. */
|
|
50
|
-
align: number;
|
|
51
|
-
/** Niveles 1..3 → 0 desactivado. */
|
|
52
|
-
ls: number;
|
|
53
|
-
/** Niveles 1..3 (1=protanopia, 2=deuteranopia, 3=tritanopia). 0 desactivado. */
|
|
54
|
-
colorblind: number;
|
|
55
|
-
/** Toggles. */
|
|
56
|
-
font: boolean;
|
|
57
|
-
dyslexia: boolean;
|
|
58
|
-
contrast: boolean;
|
|
59
|
-
hideImages: boolean;
|
|
60
|
-
highlightLinks: boolean;
|
|
61
|
-
bigCursor: boolean;
|
|
62
|
-
pauseAnim: boolean;
|
|
63
|
-
focusOutline: boolean;
|
|
64
|
-
/** Overlay escala de grises (excluyente con contrast). */
|
|
65
|
-
grayOverlay: boolean;
|
|
66
|
-
/** Guía horizontal de lectura. */
|
|
67
|
-
readingGuide: boolean;
|
|
68
|
-
/** Máscara de lectura: oscurece todo menos una banda alrededor del cursor. */
|
|
69
|
-
readingMask: boolean;
|
|
70
|
-
/** Aumenta el hit-area de interactivos a 44×44 mínimo (WCAG 2.5.5/2.5.8). */
|
|
71
|
-
bigTargets: boolean;
|
|
72
|
-
}
|
|
73
|
-
declare const DEFAULT_STATE: Readonly<PanelState>;
|
|
74
|
-
declare function loadState(key: string): PanelState;
|
|
75
|
-
declare function saveState(key: string, state: PanelState): void;
|
|
76
|
-
declare function isStateEmpty(state: PanelState): boolean;
|
|
77
|
-
|
|
78
|
-
type ControlId = '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';
|
|
79
|
-
/** Render order (also the order `exclude=`/`controls=` are validated against). */
|
|
80
|
-
declare const ALL_CONTROLS: readonly ControlId[];
|
|
81
|
-
type PresetId = 'lowvision' | 'dyslexia' | 'motor' | 'calm';
|
|
82
|
-
/** Preset profiles: set the listed flags, leave the rest as-is.
|
|
83
|
-
* Apply is additive — pressing two presets unions their flags. */
|
|
84
|
-
declare const PRESETS: Readonly<Record<PresetId, Partial<PanelState>>>;
|
|
85
|
-
declare const PRESET_IDS: readonly PresetId[];
|
|
86
|
-
/**
|
|
87
|
-
* Resolve which controls an instance offers from its `controls=` (whitelist)
|
|
88
|
-
* and `exclude=` (blacklist) attributes. Both optional; unknown ids ignored.
|
|
89
|
-
* No attributes ⇒ all 17 (current behavior).
|
|
90
|
-
*/
|
|
91
|
-
declare function resolveEnabledControls(controlsAttr?: string | null, excludeAttr?: string | null): Set<ControlId>;
|
|
92
|
-
/**
|
|
93
|
-
* Controls that can't be confined to a container AND shouldn't even be offered
|
|
94
|
-
* in a scoped panel: grayscale (a full-screen overlay that destroys colour
|
|
95
|
-
* data) and the colour-blind root `filter` (destroys data + the open
|
|
96
|
-
* simulation-vs-correction question). A scoped instance drops these entirely.
|
|
97
|
-
*/
|
|
98
|
-
declare const NON_SCOPABLE_CONTROLS: readonly ControlId[];
|
|
99
|
-
/**
|
|
100
|
-
* Controls that can't be *scoped* but ARE benign window-level aids, so a scoped
|
|
101
|
-
* panel still offers them and applies them globally (to the whole window) with
|
|
102
|
-
* a shared, cross-instance state: big cursor and the reading guide / mask.
|
|
103
|
-
* Toggling from any pane affects the whole window and every pane's button
|
|
104
|
-
* reflects it. (Unlike grayscale/colour-blind, these don't falsify data.)
|
|
105
|
-
*/
|
|
106
|
-
declare const GLOBAL_OFFERABLE_CONTROLS: readonly ControlId[];
|
|
107
|
-
/** Remove the controls a scoped instance doesn't offer at all (grayscale, colour-blind). */
|
|
108
|
-
declare function scopedControls(enabled: Set<ControlId>): Set<ControlId>;
|
|
109
|
-
|
|
110
47
|
type Position = 'top-left' | 'top-center' | 'top-right' | 'mid-left' | 'mid-center' | 'mid-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
111
48
|
interface RenderOptions {
|
|
112
49
|
t: Translation;
|
|
@@ -136,37 +73,6 @@ declare function buildPanelHtml(opts: RenderOptions): string;
|
|
|
136
73
|
* `mobile` opcional aplica una posición distinta del wrapper en viewport ≤768px. */
|
|
137
74
|
declare function positionCss(position: Position, mobile?: Position): string;
|
|
138
75
|
|
|
139
|
-
interface BehaviorOptions {
|
|
140
|
-
/** Llave localStorage. Default oksiacSettings. */
|
|
141
|
-
storageKey?: string;
|
|
142
|
-
/** Locale para los aria-label dinámicos (nivel de los multinivel). Default en. */
|
|
143
|
-
locale?: string;
|
|
144
|
-
/** Controles que ofrece esta instancia (#1). Default: los 17. Filtra qué
|
|
145
|
-
* flags puede aplicar un perfil. */
|
|
146
|
-
enabled?: Set<ControlId>;
|
|
147
|
-
/** Desplazamiento máximo del usuario para el trigger en px (#5). 0/ausente
|
|
148
|
-
* desactiva el nudge (comportamiento actual). */
|
|
149
|
-
nudgeMax?: number;
|
|
150
|
-
/** Contenedor al que confinar los efectos (`scope=`). Si se pasa, las clases
|
|
151
|
-
* scopables van a este elemento en vez de a `body`, y los efectos no
|
|
152
|
-
* regionalizables (overlays, daltonismo, cursor) no se aplican. */
|
|
153
|
-
scopeEl?: HTMLElement | null;
|
|
154
|
-
}
|
|
155
|
-
/** Dispose function with imperative open/close/toggle attached (#2). Calling
|
|
156
|
-
* the value disposes; the methods drive the panel host-side. */
|
|
157
|
-
interface PanelController {
|
|
158
|
-
(): void;
|
|
159
|
-
open(): void;
|
|
160
|
-
close(): void;
|
|
161
|
-
toggle(): void;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Engancha toda la lógica al panel ya renderizado dentro de `root`
|
|
165
|
-
* (que es el shadowRoot del custom element). Devuelve un PanelController:
|
|
166
|
-
* invocarlo limpia; sus métodos abren/cierran el panel.
|
|
167
|
-
*/
|
|
168
|
-
declare function bindPanelBehavior(root: ShadowRoot, opts?: BehaviorOptions): PanelController;
|
|
169
|
-
|
|
170
76
|
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 flex: 0 0 auto;\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/* flex:1 1 auto + min-height:0 lets the content scroll inside the 90vh panel.\n Without min-height:0 a flex child won't shrink below its content, so the\n header (\u00D7) and footer (Reset) get pushed off-screen on short viewports or at\n high zoom \u2014 the panel becomes impossible to close. */\n.oks-access-content { flex: 1 1 auto; min-height: 0; 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/* Screen-reader-only: keeps the category headings in the accessibility tree\n (flat scoped layout) while taking them out of the grid flow so they don't\n force a visual row break or occupy a cell. */\n.oks-sr-only {\n position: absolute !important;\n width: 1px; height: 1px;\n margin: 0; padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\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 flex: 0 0 auto;\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";
|
|
171
77
|
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 *:not(.fa, [class*=\"fa-\"], .icon, [class^=\"icon-\"], [class*=\" icon-\"], .dashicons, [class*=\"dashicons-\"], .material-icons, [class^=\"material-symbols\"], .glyphicon, [class^=\"bi-\"], [class*=\" bi-\"]) {\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\n/* Focus highlight colour is themeable via --oks-focus-color (default #005fcc):\n set it on :root for a dark theme so the highlight stays visible. The\n persistent \"all interactive\" dashed derives a 45% tint from it; the glow can\n be overridden on its own with --oks-focus-glow. */\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 color-mix(in srgb, var(--oks-focus-color, #005fcc) 45%, transparent) !important;\n outline-offset: 2px !important;\n}\nbody.oks-a11y-focus *:focus-visible {\n outline: 3px solid var(--oks-focus-color, #005fcc) !important;\n outline-offset: 3px !important;\n box-shadow: 0 0 0 6px var(--oks-focus-glow, color-mix(in srgb, var(--oks-focus-color, #005fcc) 25%, transparent)) !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";
|
|
172
78
|
/**
|
|
@@ -187,4 +93,4 @@ declare const EFFECT_CSS = "\nhtml.oks-colorblind-1 { filter: url('#oks-filter-p
|
|
|
187
93
|
*/
|
|
188
94
|
declare function scopedEffectCss(s: string): string;
|
|
189
95
|
|
|
190
|
-
export {
|
|
96
|
+
export { COLORBLIND_FILTERS_SVG, ControlId, EFFECT_CSS, type LocaleCode, PANEL_CSS, type Position, type Translation, type TriggerIcon, buildPanelHtml, getTranslation, positionCss, scopedEffectCss, supportedLocales };
|
package/dist/index.js
CHANGED
|
@@ -309,14 +309,42 @@ var DEFAULT_STATE = Object.freeze({
|
|
|
309
309
|
readingMask: false,
|
|
310
310
|
bigTargets: false
|
|
311
311
|
});
|
|
312
|
+
var LEVEL_MAX = Object.freeze({
|
|
313
|
+
zoom: 4,
|
|
314
|
+
lh: 3,
|
|
315
|
+
align: 3,
|
|
316
|
+
ls: 3,
|
|
317
|
+
colorblind: 3
|
|
318
|
+
});
|
|
319
|
+
function sanitizeState(input) {
|
|
320
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
321
|
+
const src = input;
|
|
322
|
+
const out = { ...DEFAULT_STATE };
|
|
323
|
+
for (const key of Object.keys(DEFAULT_STATE)) {
|
|
324
|
+
const v = src[key];
|
|
325
|
+
if (v === void 0) continue;
|
|
326
|
+
if (key in LEVEL_MAX) {
|
|
327
|
+
if (typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= LEVEL_MAX[key]) {
|
|
328
|
+
out[key] = v;
|
|
329
|
+
}
|
|
330
|
+
} else if (v === true || v === 1) {
|
|
331
|
+
out[key] = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function parseState(json) {
|
|
337
|
+
if (json == null || json.trim() === "") return null;
|
|
338
|
+
try {
|
|
339
|
+
return sanitizeState(JSON.parse(json));
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
312
344
|
function loadState(key) {
|
|
313
345
|
try {
|
|
314
346
|
if (typeof localStorage === "undefined") return { ...DEFAULT_STATE };
|
|
315
|
-
|
|
316
|
-
if (!raw) return { ...DEFAULT_STATE };
|
|
317
|
-
const parsed = JSON.parse(raw);
|
|
318
|
-
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_STATE };
|
|
319
|
-
return { ...DEFAULT_STATE, ...parsed };
|
|
347
|
+
return parseState(localStorage.getItem(key)) ?? { ...DEFAULT_STATE };
|
|
320
348
|
} catch {
|
|
321
349
|
return { ...DEFAULT_STATE };
|
|
322
350
|
}
|
|
@@ -393,7 +421,19 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
393
421
|
if (!panel || !closeBtn || !resetBtn) {
|
|
394
422
|
return noopController();
|
|
395
423
|
}
|
|
396
|
-
let state
|
|
424
|
+
let state;
|
|
425
|
+
if (opts.initialState) {
|
|
426
|
+
state = { ...opts.initialState };
|
|
427
|
+
saveState(storageKey, state);
|
|
428
|
+
} else {
|
|
429
|
+
state = loadState(storageKey);
|
|
430
|
+
}
|
|
431
|
+
const emitChange = () => {
|
|
432
|
+
const host = root.host;
|
|
433
|
+
if (!host) return;
|
|
434
|
+
const detail = { state: { ...state } };
|
|
435
|
+
host.dispatchEvent(new CustomEvent("oksiac:change", { bubbles: true, composed: true, detail }));
|
|
436
|
+
};
|
|
397
437
|
const scoped = opts.scopeEl !== void 0;
|
|
398
438
|
const GLOBAL_KEY = "oksiac::global";
|
|
399
439
|
const GLOBAL_MAP = [
|
|
@@ -569,11 +609,13 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
569
609
|
}
|
|
570
610
|
applyState();
|
|
571
611
|
saveState(storageKey, state);
|
|
612
|
+
emitChange();
|
|
572
613
|
};
|
|
573
614
|
const onReset = () => {
|
|
574
615
|
state = { ...DEFAULT_STATE };
|
|
575
616
|
applyState();
|
|
576
617
|
saveState(storageKey, state);
|
|
618
|
+
emitChange();
|
|
577
619
|
if (scoped) {
|
|
578
620
|
const cleared = { bigCursor: false, readingGuide: false, readingMask: false };
|
|
579
621
|
saveGlobal(cleared);
|
|
@@ -1326,6 +1368,6 @@ ${s}.oks-a11y-bigtargets summary {
|
|
|
1326
1368
|
`;
|
|
1327
1369
|
}
|
|
1328
1370
|
|
|
1329
|
-
export { ALL_CONTROLS, COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, GLOBAL_OFFERABLE_CONTROLS, NON_SCOPABLE_CONTROLS, PANEL_CSS, PRESETS, PRESET_IDS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, positionCss, resolveEnabledControls, saveState, scopedControls, scopedEffectCss, supportedLocales };
|
|
1371
|
+
export { ALL_CONTROLS, COLORBLIND_FILTERS_SVG, DEFAULT_STATE, EFFECT_CSS, GLOBAL_OFFERABLE_CONTROLS, NON_SCOPABLE_CONTROLS, PANEL_CSS, PRESETS, PRESET_IDS, bindPanelBehavior, buildPanelHtml, getTranslation, isStateEmpty, loadState, parseState, positionCss, resolveEnabledControls, sanitizeState, saveState, scopedControls, scopedEffectCss, supportedLocales };
|
|
1330
1372
|
//# sourceMappingURL=index.js.map
|
|
1331
1373
|
//# sourceMappingURL=index.js.map
|