@oksigenia/access-panel 0.6.0 → 0.7.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 +14 -1
- package/dist/index.d.ts +20 -8
- package/dist/index.js +165 -40
- package/dist/index.js.map +1 -1
- package/dist/web-component.js +164 -42
- package/dist/web-component.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @oksigenia/access-panel
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Two refinements to scoped panels (`scope=`):
|
|
8
|
+
|
|
9
|
+
**Window-level "global-offerable" controls.** Big cursor and the reading guide / mask can't be confined to a container, but they're benign window-level aids — so a scoped panel now offers them and applies them to the whole window, with a single shared state across every scoped instance and detached window. Toggle from any pane's pill and the cursor/guide/mask turns on window-wide while every pane's button reflects it; it persists as a window preference (synced same-document via an event and cross-window via the `storage` event), not under any one pane's `storage-key`. Grayscale and the colour-blind filter stay out of scoped panels (they destroy colour-coded data) — a scoped instance now drops only those two.
|
|
10
|
+
|
|
11
|
+
**Flat layout in scoped mode.** Curated/scoped panels looked sparse because each control category was its own grid, so controls from different categories couldn't share a row. Scoped panels now use a single flat grid; the category headings are kept screen-reader-only (out of the visual flow) so assistive tech keeps the structure, and the trailing button widens to fill any odd gap. The classic (non-scoped) panel is unchanged — it keeps its visible categories.
|
|
12
|
+
|
|
13
|
+
Both are scoped-mode only; they don't touch the classic panel or the WordPress/Moodle plugins.
|
|
14
|
+
|
|
15
|
+
## 0.6.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Make the `focus` highlight colour themeable via `--oks-focus-color` (default `#005fcc`), with an optional `--oks-focus-glow` for the ring around the current element. The hardcoded blue could vanish on a dark theme; set the variable on `:root` for the global panel, or on the scope container for a scoped one (`scope=`), to keep the highlight visible. Custom properties inherit, so it composes with `scope=`. Default behaviour is unchanged, and high-contrast mode keeps its own cyan.
|
|
20
|
+
|
|
3
21
|
## 0.6.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -102,7 +102,9 @@ By default the panel adapts the whole page — which is what a normal content si
|
|
|
102
102
|
|
|
103
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
104
|
|
|
105
|
-
A scoped instance **
|
|
105
|
+
A scoped instance **drops grayscale and the colour-blind filter** — they destroy colour-coded data, which is exactly what scoping protects. **Big cursor and the reading guide / mask** can't be confined to a container either, but they're benign window-level aids, so a scoped panel still offers them and applies them to the **whole window**, with a single shared state across every pane and detached window: toggle from any pane's button and the cursor/guide/mask turns on window-wide while every pane's button reflects it (persisted as a window preference, not per-pane).
|
|
106
|
+
|
|
107
|
+
A scoped panel uses a **flat layout** — one grid, no visible category headings (they're kept screen-reader-only) — because curated per-pane panels look sparse when each category is its own grid. The classic (non-scoped) panel keeps its categories.
|
|
106
108
|
|
|
107
109
|
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
110
|
|
|
@@ -135,6 +137,17 @@ If you need to brand the panel itself, fork the package and customize
|
|
|
135
137
|
`PANEL_CSS` — the building blocks are exported from
|
|
136
138
|
`@oksigenia/access-panel`.
|
|
137
139
|
|
|
140
|
+
### Focus highlight colour (for dark themes)
|
|
141
|
+
|
|
142
|
+
The `focus` control highlights interactive elements with a blue that can vanish on a dark background. Override it with `--oks-focus-color` (and optionally `--oks-focus-glow` for the ring around the current element). Custom properties inherit, so set it where the effect applies — on `:root` for the global panel, or on the scope container for a scoped one:
|
|
143
|
+
|
|
144
|
+
```css
|
|
145
|
+
:root { --oks-focus-color: #5ee0ff; } /* dark theme, global */
|
|
146
|
+
#map-pane { --oks-focus-color: #5ee0ff; } /* dark theme, scope= */
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Default is `#005fcc`. The persistent "all interactive" outline derives a 45% tint from this colour; high-contrast mode overrides both to cyan on its own.
|
|
150
|
+
|
|
138
151
|
## Use it in Astro
|
|
139
152
|
|
|
140
153
|
```astro
|
package/dist/index.d.ts
CHANGED
|
@@ -90,13 +90,21 @@ declare const PRESET_IDS: readonly PresetId[];
|
|
|
90
90
|
*/
|
|
91
91
|
declare function resolveEnabledControls(controlsAttr?: string | null, excludeAttr?: string | null): Set<ControlId>;
|
|
92
92
|
/**
|
|
93
|
-
* Controls that
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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
97
|
*/
|
|
98
98
|
declare const NON_SCOPABLE_CONTROLS: readonly ControlId[];
|
|
99
|
-
/**
|
|
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). */
|
|
100
108
|
declare function scopedControls(enabled: Set<ControlId>): Set<ControlId>;
|
|
101
109
|
|
|
102
110
|
type Position = 'top-left' | 'top-center' | 'top-right' | 'mid-left' | 'mid-center' | 'mid-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
@@ -114,6 +122,10 @@ interface RenderOptions {
|
|
|
114
122
|
* entirely. Defaults to true; individual presets still self-hide under the
|
|
115
123
|
* ≥2-control rule. */
|
|
116
124
|
showPresets?: boolean;
|
|
125
|
+
/** Flat layout: one grid with screen-reader-only category headings instead of
|
|
126
|
+
* visible per-category sections. Used in scoped mode, where heavily-curated
|
|
127
|
+
* panels look sparse with category breaks. Defaults to false (classic). */
|
|
128
|
+
flatLayout?: boolean;
|
|
117
129
|
}
|
|
118
130
|
/**
|
|
119
131
|
* HTML del panel completo (trigger + dialog). Pensado para inyectarse
|
|
@@ -155,8 +167,8 @@ interface PanelController {
|
|
|
155
167
|
*/
|
|
156
168
|
declare function bindPanelBehavior(root: ShadowRoot, opts?: BehaviorOptions): PanelController;
|
|
157
169
|
|
|
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";
|
|
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
|
|
170
|
+
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/* 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 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
|
+
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\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";
|
|
160
172
|
/**
|
|
161
173
|
* Scoped-mode effect CSS (#scope). Emits only the controls that can be confined
|
|
162
174
|
* to a container by selector — the same rules as EFFECT_CSS but anchored to the
|
|
@@ -175,4 +187,4 @@ declare const EFFECT_CSS = "\nhtml.oks-colorblind-1 { filter: url('#oks-filter-p
|
|
|
175
187
|
*/
|
|
176
188
|
declare function scopedEffectCss(s: string): string;
|
|
177
189
|
|
|
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 };
|
|
190
|
+
export { ALL_CONTROLS, type BehaviorOptions, COLORBLIND_FILTERS_SVG, type ControlId, DEFAULT_STATE, EFFECT_CSS, GLOBAL_OFFERABLE_CONTROLS, 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,12 +110,11 @@ function resolveEnabledControls(controlsAttr, excludeAttr) {
|
|
|
110
110
|
}
|
|
111
111
|
return enabled;
|
|
112
112
|
}
|
|
113
|
-
var NON_SCOPABLE_CONTROLS = [
|
|
114
|
-
|
|
115
|
-
"reading-guide",
|
|
116
|
-
"reading-mask",
|
|
113
|
+
var NON_SCOPABLE_CONTROLS = ["grayscale", "colorblind"];
|
|
114
|
+
var GLOBAL_OFFERABLE_CONTROLS = [
|
|
117
115
|
"big-cursor",
|
|
118
|
-
"
|
|
116
|
+
"reading-guide",
|
|
117
|
+
"reading-mask"
|
|
119
118
|
];
|
|
120
119
|
function scopedControls(enabled) {
|
|
121
120
|
const out = new Set(enabled);
|
|
@@ -145,6 +144,7 @@ function buildPanelHtml(opts) {
|
|
|
145
144
|
const enabled = opts.enabled ?? new Set(ALL_CONTROLS);
|
|
146
145
|
const showTrigger = opts.showTrigger ?? true;
|
|
147
146
|
const showPresets = opts.showPresets ?? true;
|
|
147
|
+
const flatLayout = opts.flatLayout ?? false;
|
|
148
148
|
const trig = TRIGGER_ICONS[triggerIcon];
|
|
149
149
|
const multi = (id, prefix, levels, label, icon, full = false) => {
|
|
150
150
|
if (!enabled.has(id)) return "";
|
|
@@ -179,44 +179,61 @@ function buildPanelHtml(opts) {
|
|
|
179
179
|
};
|
|
180
180
|
const presetButtons = showPresets ? PRESET_IDS.filter((id) => presetIsAvailable(id, enabled)).map((id) => preset(id, presetLabels[id])) : [];
|
|
181
181
|
const presetsBlock = presetButtons.length ? `<h4 class="oks-access-title">${escapeHtml(t.presets)}</h4><div class="oks-access-presets">${presetButtons.join("")}</div>` : "";
|
|
182
|
-
const
|
|
183
|
-
<div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
|
|
184
|
-
<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">
|
|
185
|
-
${trig}
|
|
186
|
-
</button>
|
|
187
|
-
<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>
|
|
188
|
-
</div>` : "";
|
|
189
|
-
return `${triggerBlock}
|
|
190
|
-
<div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
|
|
191
|
-
<div class="oks-access-header">
|
|
192
|
-
<h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
|
|
193
|
-
<button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="oks-access-content">
|
|
196
|
-
${presetsBlock}
|
|
197
|
-
${section(t.txt, [
|
|
182
|
+
const textBtns = [
|
|
198
183
|
multi("text-size", "oks-zoom", 4, t.size, ICON_TXT),
|
|
199
184
|
multi("line-height", "oks-lh", 3, t.lh, ICON_LH),
|
|
200
185
|
multi("text-align", "oks-align", 3, t.align, ICON_ALIGN),
|
|
201
186
|
toggle("readable-font", "oks-a11y-font", t.font, ICON_FONT),
|
|
202
187
|
toggle("dyslexia-font", "oks-dyslexia", t.dyslexia, ICON_DYSLEXIA),
|
|
203
188
|
multi("letter-spacing", "oks-ls", 3, t.ls, ICON_LS)
|
|
204
|
-
]
|
|
205
|
-
|
|
189
|
+
];
|
|
190
|
+
const visBtns = [
|
|
206
191
|
toggle("contrast", "oks-a11y-contrast", t.contrast, ICON_CONTRAST),
|
|
207
192
|
overlay("grayscale", "oks-overlay-gray", t.gray, ICON_GRAY),
|
|
208
193
|
toggle("hide-images", "oks-a11y-hide", t.hide, ICON_HIDE),
|
|
209
194
|
toggle("highlight-links", "oks-a11y-links", t.links, ICON_LINK),
|
|
210
|
-
multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND,
|
|
211
|
-
]
|
|
212
|
-
|
|
195
|
+
multi("colorblind", "oks-colorblind", 3, t.cb, ICON_COLORBLIND, !flatLayout)
|
|
196
|
+
];
|
|
197
|
+
const oriBtns = [
|
|
213
198
|
simple("reading-guide", "guide", t.guide, ICON_GUIDE),
|
|
214
199
|
simple("reading-mask", "mask", t.mask, ICON_MASK),
|
|
215
200
|
toggle("big-cursor", "oks-big-cursor", t.cursor, ICON_CURSOR),
|
|
216
201
|
toggle("big-targets", "oks-a11y-bigtargets", t.targets, ICON_TARGETS),
|
|
217
202
|
toggle("pause-anim", "oks-a11y-pause", t.pause, ICON_PAUSE),
|
|
218
203
|
toggle("focus", "oks-a11y-focus", t.focus, ICON_FOCUS)
|
|
219
|
-
]
|
|
204
|
+
];
|
|
205
|
+
const addFullWidth = (btn) => btn.replace('class="oks-access-opt', 'class="oks-access-opt full-width');
|
|
206
|
+
const flatContent = () => {
|
|
207
|
+
const groups = [
|
|
208
|
+
{ title: t.txt, items: textBtns.filter(Boolean) },
|
|
209
|
+
{ title: t.vis, items: visBtns.filter(Boolean) },
|
|
210
|
+
{ title: t.ori, items: oriBtns.filter(Boolean) }
|
|
211
|
+
].filter((g) => g.items.length > 0);
|
|
212
|
+
const total = groups.reduce((n, g) => n + g.items.length, 0);
|
|
213
|
+
if (total % 2 === 1 && groups.length > 0) {
|
|
214
|
+
const last = groups[groups.length - 1];
|
|
215
|
+
last.items[last.items.length - 1] = addFullWidth(last.items[last.items.length - 1]);
|
|
216
|
+
}
|
|
217
|
+
const inner = groups.map((g) => `<h4 class="oks-access-title oks-sr-only">${escapeHtml(g.title)}</h4>${g.items.join("")}`).join("");
|
|
218
|
+
return `<div class="oks-access-grid oks-flat">${inner}</div>`;
|
|
219
|
+
};
|
|
220
|
+
const controlsHtml = flatLayout ? flatContent() : section(t.txt, textBtns) + section(t.vis, visBtns) + section(t.ori, oriBtns);
|
|
221
|
+
const triggerBlock = showTrigger ? `
|
|
222
|
+
<div class="oks-access-wrapper" id="oks-wrapper" data-position="${opts.position}">
|
|
223
|
+
<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">
|
|
224
|
+
${trig}
|
|
225
|
+
</button>
|
|
226
|
+
<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>
|
|
227
|
+
</div>` : "";
|
|
228
|
+
return `${triggerBlock}
|
|
229
|
+
<div class="oks-access-panel" id="oks-panel" role="dialog" aria-modal="true" aria-labelledby="oks-panel-title" inert>
|
|
230
|
+
<div class="oks-access-header">
|
|
231
|
+
<h3 id="oks-panel-title">${escapeHtml(t.title)}</h3>
|
|
232
|
+
<button class="oks-access-close" id="oks-close" aria-label="${escapeAttr(t.close)}" type="button">${ICON_CLOSE}</button>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="oks-access-content">
|
|
235
|
+
${presetsBlock}
|
|
236
|
+
${controlsHtml}
|
|
220
237
|
</div>
|
|
221
238
|
<div class="oks-access-footer">
|
|
222
239
|
<button class="oks-access-reset" id="oks-reset" type="button">${escapeHtml(t.reset)}</button>
|
|
@@ -374,8 +391,69 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
374
391
|
return noopController();
|
|
375
392
|
}
|
|
376
393
|
let state = loadState(storageKey);
|
|
394
|
+
const scoped = opts.scopeEl !== void 0;
|
|
395
|
+
const GLOBAL_KEY = "oksiac::global";
|
|
396
|
+
const GLOBAL_MAP = [
|
|
397
|
+
{ flag: "bigCursor", cls: "oks-big-cursor", match: (b) => b.getAttribute("data-class") === "oks-big-cursor" },
|
|
398
|
+
{ flag: "readingGuide", cls: "oks-a11y-guide", match: (b) => b.getAttribute("data-action") === "guide" },
|
|
399
|
+
{ flag: "readingMask", cls: "oks-a11y-mask", match: (b) => b.getAttribute("data-action") === "mask" }
|
|
400
|
+
];
|
|
401
|
+
const globalOfferableOf = (b) => GLOBAL_MAP.find((x) => x.match(b));
|
|
402
|
+
const loadGlobal = () => {
|
|
403
|
+
try {
|
|
404
|
+
const r = JSON.parse(localStorage.getItem(GLOBAL_KEY) ?? "{}");
|
|
405
|
+
return { bigCursor: !!r.bigCursor, readingGuide: !!r.readingGuide, readingMask: !!r.readingMask };
|
|
406
|
+
} catch {
|
|
407
|
+
return { bigCursor: false, readingGuide: false, readingMask: false };
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const saveGlobal = (g) => {
|
|
411
|
+
try {
|
|
412
|
+
if (g.bigCursor || g.readingGuide || g.readingMask) localStorage.setItem(GLOBAL_KEY, JSON.stringify(g));
|
|
413
|
+
else localStorage.removeItem(GLOBAL_KEY);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const applyGlobal = (g) => {
|
|
418
|
+
document.body.classList.toggle("oks-big-cursor", g.bigCursor);
|
|
419
|
+
document.body.classList.toggle("oks-a11y-guide", g.readingGuide);
|
|
420
|
+
document.body.classList.toggle("oks-a11y-mask", g.readingMask);
|
|
421
|
+
for (const btn of opts$) {
|
|
422
|
+
const m = globalOfferableOf(btn);
|
|
423
|
+
if (!m) continue;
|
|
424
|
+
btn.classList.toggle("is-active", g[m.flag]);
|
|
425
|
+
btn.setAttribute("aria-pressed", g[m.flag] ? "true" : "false");
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const refreshGlobal = () => {
|
|
429
|
+
if (scoped) applyGlobal(loadGlobal());
|
|
430
|
+
};
|
|
431
|
+
const syncGlobalFromState = () => {
|
|
432
|
+
if (!scoped) return;
|
|
433
|
+
const g = loadGlobal();
|
|
434
|
+
let changed = false;
|
|
435
|
+
if (state.bigCursor) {
|
|
436
|
+
g.bigCursor = true;
|
|
437
|
+
state.bigCursor = false;
|
|
438
|
+
changed = true;
|
|
439
|
+
}
|
|
440
|
+
if (state.readingGuide) {
|
|
441
|
+
g.readingGuide = true;
|
|
442
|
+
state.readingGuide = false;
|
|
443
|
+
changed = true;
|
|
444
|
+
}
|
|
445
|
+
if (state.readingMask) {
|
|
446
|
+
g.readingMask = true;
|
|
447
|
+
state.readingMask = false;
|
|
448
|
+
changed = true;
|
|
449
|
+
}
|
|
450
|
+
if (changed) {
|
|
451
|
+
saveGlobal(g);
|
|
452
|
+
applyGlobal(g);
|
|
453
|
+
document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
|
|
454
|
+
}
|
|
455
|
+
};
|
|
377
456
|
function applyState() {
|
|
378
|
-
const scoped = opts.scopeEl !== void 0;
|
|
379
457
|
const fxRoot = scoped ? opts.scopeEl ?? null : document.body;
|
|
380
458
|
const rootEl = document.documentElement;
|
|
381
459
|
if (fxRoot) {
|
|
@@ -410,6 +488,7 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
410
488
|
}
|
|
411
489
|
function syncButtonsFromState() {
|
|
412
490
|
for (const btn of opts$) {
|
|
491
|
+
if (scoped && globalOfferableOf(btn)) continue;
|
|
413
492
|
const action = btn.getAttribute("data-action");
|
|
414
493
|
if (action === "multi" || action === "colorblind") {
|
|
415
494
|
const prefix = btn.getAttribute("data-prefix") ?? "";
|
|
@@ -444,6 +523,17 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
444
523
|
}
|
|
445
524
|
const onOptClick = (e) => {
|
|
446
525
|
const btn = e.currentTarget;
|
|
526
|
+
if (scoped) {
|
|
527
|
+
const go = globalOfferableOf(btn);
|
|
528
|
+
if (go) {
|
|
529
|
+
const g = loadGlobal();
|
|
530
|
+
g[go.flag] = !g[go.flag];
|
|
531
|
+
saveGlobal(g);
|
|
532
|
+
applyGlobal(g);
|
|
533
|
+
document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
447
537
|
const action = btn.getAttribute("data-action");
|
|
448
538
|
if (action === "multi") {
|
|
449
539
|
const prefix = btn.getAttribute("data-prefix") ?? "";
|
|
@@ -472,6 +562,7 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
472
562
|
if (id && PRESETS[id]) Object.assign(state, filterPresetForEnabled(id, enabled));
|
|
473
563
|
btn.classList.add("is-flashing");
|
|
474
564
|
setTimeout(() => btn.classList.remove("is-flashing"), 250);
|
|
565
|
+
syncGlobalFromState();
|
|
475
566
|
}
|
|
476
567
|
applyState();
|
|
477
568
|
saveState(storageKey, state);
|
|
@@ -480,6 +571,12 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
480
571
|
state = { ...DEFAULT_STATE };
|
|
481
572
|
applyState();
|
|
482
573
|
saveState(storageKey, state);
|
|
574
|
+
if (scoped) {
|
|
575
|
+
const cleared = { bigCursor: false, readingGuide: false, readingMask: false };
|
|
576
|
+
saveGlobal(cleared);
|
|
577
|
+
applyGlobal(cleared);
|
|
578
|
+
document.dispatchEvent(new CustomEvent("oksiac:globalchange"));
|
|
579
|
+
}
|
|
483
580
|
};
|
|
484
581
|
let opener = null;
|
|
485
582
|
let ignoreDocClose = false;
|
|
@@ -647,14 +744,16 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
647
744
|
saveNudge();
|
|
648
745
|
};
|
|
649
746
|
const onMove = (e) => {
|
|
650
|
-
|
|
747
|
+
const guideOn = document.body.classList.contains("oks-a11y-guide");
|
|
748
|
+
const maskOn = document.body.classList.contains("oks-a11y-mask");
|
|
749
|
+
if (!guideOn && !maskOn) return;
|
|
651
750
|
const y = e.touches?.[0]?.clientY ?? e.clientY;
|
|
652
751
|
if (typeof y !== "number") return;
|
|
653
|
-
if (
|
|
752
|
+
if (guideOn) {
|
|
654
753
|
const guide = document.getElementById("oks-reading-guide");
|
|
655
754
|
if (guide) guide.style.top = `${y}px`;
|
|
656
755
|
}
|
|
657
|
-
if (
|
|
756
|
+
if (maskOn) {
|
|
658
757
|
const mask = document.getElementById("oks-reading-mask");
|
|
659
758
|
if (mask) mask.style.setProperty("--oks-mask-y", `${y}px`);
|
|
660
759
|
}
|
|
@@ -670,9 +769,15 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
670
769
|
const onReposition = () => {
|
|
671
770
|
if (opts.scopeEl && panel.classList.contains("is-open")) positionScopedDialog();
|
|
672
771
|
};
|
|
673
|
-
|
|
772
|
+
const onGlobalChange = () => refreshGlobal();
|
|
773
|
+
const onStorage = (e) => {
|
|
774
|
+
if (e.key === GLOBAL_KEY) refreshGlobal();
|
|
775
|
+
};
|
|
776
|
+
if (scoped) {
|
|
674
777
|
window.addEventListener("scroll", onReposition, true);
|
|
675
778
|
window.addEventListener("resize", onReposition);
|
|
779
|
+
document.addEventListener("oksiac:globalchange", onGlobalChange);
|
|
780
|
+
window.addEventListener("storage", onStorage);
|
|
676
781
|
}
|
|
677
782
|
if (nudgeMax > 0 && trigger) {
|
|
678
783
|
loadNudge();
|
|
@@ -685,6 +790,7 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
685
790
|
trigger.addEventListener("keydown", onTriggerKey);
|
|
686
791
|
}
|
|
687
792
|
applyState();
|
|
793
|
+
refreshGlobal();
|
|
688
794
|
const dispose = () => {
|
|
689
795
|
trigger?.removeEventListener("click", onTriggerClick);
|
|
690
796
|
closeBtn.removeEventListener("click", closePanel);
|
|
@@ -694,9 +800,11 @@ function bindPanelBehavior(root, opts = {}) {
|
|
|
694
800
|
document.removeEventListener("keydown", onKeyDown);
|
|
695
801
|
document.removeEventListener("mousemove", onMove);
|
|
696
802
|
document.removeEventListener("touchmove", onMove);
|
|
697
|
-
if (
|
|
803
|
+
if (scoped) {
|
|
698
804
|
window.removeEventListener("scroll", onReposition, true);
|
|
699
805
|
window.removeEventListener("resize", onReposition);
|
|
806
|
+
document.removeEventListener("oksiac:globalchange", onGlobalChange);
|
|
807
|
+
window.removeEventListener("storage", onStorage);
|
|
700
808
|
}
|
|
701
809
|
trigger?.removeEventListener("pointerdown", onPointerDown);
|
|
702
810
|
document.removeEventListener("pointermove", onPointerMove);
|
|
@@ -833,6 +941,19 @@ var PANEL_CSS = `
|
|
|
833
941
|
text-transform: uppercase;
|
|
834
942
|
color: #888;
|
|
835
943
|
}
|
|
944
|
+
/* Screen-reader-only: keeps the category headings in the accessibility tree
|
|
945
|
+
(flat scoped layout) while taking them out of the grid flow so they don't
|
|
946
|
+
force a visual row break or occupy a cell. */
|
|
947
|
+
.oks-sr-only {
|
|
948
|
+
position: absolute !important;
|
|
949
|
+
width: 1px; height: 1px;
|
|
950
|
+
margin: 0; padding: 0;
|
|
951
|
+
overflow: hidden;
|
|
952
|
+
clip: rect(0 0 0 0);
|
|
953
|
+
clip-path: inset(50%);
|
|
954
|
+
white-space: nowrap;
|
|
955
|
+
border: 0;
|
|
956
|
+
}
|
|
836
957
|
.oks-access-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 8px; }
|
|
837
958
|
.oks-access-presets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
|
838
959
|
.oks-preset {
|
|
@@ -1004,18 +1125,22 @@ body.oks-ls-1 * { letter-spacing: 0.05em !important; }
|
|
|
1004
1125
|
body.oks-ls-2 * { letter-spacing: 0.10em !important; }
|
|
1005
1126
|
body.oks-ls-3 * { letter-spacing: 0.16em !important; }
|
|
1006
1127
|
|
|
1128
|
+
/* Focus highlight colour is themeable via --oks-focus-color (default #005fcc):
|
|
1129
|
+
set it on :root for a dark theme so the highlight stays visible. The
|
|
1130
|
+
persistent "all interactive" dashed derives a 45% tint from it; the glow can
|
|
1131
|
+
be overridden on its own with --oks-focus-glow. */
|
|
1007
1132
|
body.oks-a11y-focus a:not(oksigenia-access-panel):not(oksigenia-access-panel *),
|
|
1008
1133
|
body.oks-a11y-focus button:not(oksigenia-access-panel):not(oksigenia-access-panel *),
|
|
1009
1134
|
body.oks-a11y-focus input,
|
|
1010
1135
|
body.oks-a11y-focus select,
|
|
1011
1136
|
body.oks-a11y-focus textarea {
|
|
1012
|
-
outline: 2px dashed
|
|
1137
|
+
outline: 2px dashed color-mix(in srgb, var(--oks-focus-color, #005fcc) 45%, transparent) !important;
|
|
1013
1138
|
outline-offset: 2px !important;
|
|
1014
1139
|
}
|
|
1015
1140
|
body.oks-a11y-focus *:focus-visible {
|
|
1016
|
-
outline: 3px solid #005fcc !important;
|
|
1141
|
+
outline: 3px solid var(--oks-focus-color, #005fcc) !important;
|
|
1017
1142
|
outline-offset: 3px !important;
|
|
1018
|
-
box-shadow: 0 0 0 6px
|
|
1143
|
+
box-shadow: 0 0 0 6px var(--oks-focus-glow, color-mix(in srgb, var(--oks-focus-color, #005fcc) 25%, transparent)) !important;
|
|
1019
1144
|
}
|
|
1020
1145
|
|
|
1021
1146
|
body.oks-a11y-contrast.oks-a11y-focus *:focus,
|
|
@@ -1152,13 +1277,13 @@ ${s}.oks-a11y-focus button,
|
|
|
1152
1277
|
${s}.oks-a11y-focus input,
|
|
1153
1278
|
${s}.oks-a11y-focus select,
|
|
1154
1279
|
${s}.oks-a11y-focus textarea {
|
|
1155
|
-
outline: 2px dashed
|
|
1280
|
+
outline: 2px dashed color-mix(in srgb, var(--oks-focus-color, #005fcc) 45%, transparent) !important;
|
|
1156
1281
|
outline-offset: 2px !important;
|
|
1157
1282
|
}
|
|
1158
1283
|
${s}.oks-a11y-focus *:focus-visible {
|
|
1159
|
-
outline: 3px solid #005fcc !important;
|
|
1284
|
+
outline: 3px solid var(--oks-focus-color, #005fcc) !important;
|
|
1160
1285
|
outline-offset: 3px !important;
|
|
1161
|
-
box-shadow: 0 0 0 6px
|
|
1286
|
+
box-shadow: 0 0 0 6px var(--oks-focus-glow, color-mix(in srgb, var(--oks-focus-color, #005fcc) 25%, transparent)) !important;
|
|
1162
1287
|
}
|
|
1163
1288
|
|
|
1164
1289
|
${s}.oks-a11y-contrast, ${s}.oks-a11y-contrast * {
|
|
@@ -1189,6 +1314,6 @@ ${s}.oks-a11y-bigtargets summary {
|
|
|
1189
1314
|
`;
|
|
1190
1315
|
}
|
|
1191
1316
|
|
|
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 };
|
|
1317
|
+
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 };
|
|
1193
1318
|
//# sourceMappingURL=index.js.map
|
|
1194
1319
|
//# sourceMappingURL=index.js.map
|