@pigmilcom/a11y 1.0.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.
Potentially problematic release.
This version of @pigmilcom/a11y might be problematic. Click here for more details.
- package/README.md +181 -0
- package/dist/index.css +626 -0
- package/dist/index.js +319 -0
- package/dist/index.mjs +294 -0
- package/package.json +61 -0
- package/types/index.d.ts +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.js
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
A11yWidget: () => A11yWidget,
|
|
24
|
+
default: () => A11yWidget
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/widget.jsx
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
var import_react_dom = require("react-dom");
|
|
31
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
32
|
+
var STORAGE_KEY = "pgm-a11y";
|
|
33
|
+
var DEFAULTS = {
|
|
34
|
+
textSize: 0,
|
|
35
|
+
// 0=normal | 1=large | 2=x-large
|
|
36
|
+
highContrast: false,
|
|
37
|
+
invertColors: false,
|
|
38
|
+
grayscale: false,
|
|
39
|
+
reduceMotion: false,
|
|
40
|
+
highlightLinks: false,
|
|
41
|
+
textSpacing: false,
|
|
42
|
+
adhd: false,
|
|
43
|
+
dyslexia: false
|
|
44
|
+
};
|
|
45
|
+
function applyPrefs(prefs) {
|
|
46
|
+
const root = document.documentElement;
|
|
47
|
+
root.classList.remove("a11y-text-lg", "a11y-text-xl");
|
|
48
|
+
if (prefs.textSize === 1) root.classList.add("a11y-text-lg");
|
|
49
|
+
if (prefs.textSize === 2) root.classList.add("a11y-text-xl");
|
|
50
|
+
const map = {
|
|
51
|
+
"a11y-high-contrast": prefs.highContrast,
|
|
52
|
+
"a11y-invert": prefs.invertColors,
|
|
53
|
+
"a11y-grayscale": prefs.grayscale,
|
|
54
|
+
"a11y-reduce-motion": prefs.reduceMotion,
|
|
55
|
+
"a11y-highlight-links": prefs.highlightLinks,
|
|
56
|
+
"a11y-text-spacing": prefs.textSpacing,
|
|
57
|
+
"a11y-adhd": prefs.adhd,
|
|
58
|
+
"a11y-dyslexia": prefs.dyslexia
|
|
59
|
+
};
|
|
60
|
+
for (const [cls, on] of Object.entries(map)) {
|
|
61
|
+
root.classList.toggle(cls, on);
|
|
62
|
+
}
|
|
63
|
+
const filters = [];
|
|
64
|
+
if (prefs.invertColors) filters.push("invert(1) hue-rotate(180deg)");
|
|
65
|
+
if (prefs.highContrast) filters.push("contrast(1.6)");
|
|
66
|
+
if (prefs.grayscale) filters.push("grayscale(1)");
|
|
67
|
+
root.style.filter = filters.join(" ");
|
|
68
|
+
}
|
|
69
|
+
function load() {
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
72
|
+
return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS };
|
|
73
|
+
} catch {
|
|
74
|
+
return { ...DEFAULTS };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function save(prefs) {
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var TEXT_LABELS = ["Normal", "Large", "X-Large"];
|
|
84
|
+
var TOGGLES = [
|
|
85
|
+
{
|
|
86
|
+
key: "highContrast",
|
|
87
|
+
label: "High Contrast",
|
|
88
|
+
desc: "Increase colour contrast for readability",
|
|
89
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
90
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
|
|
91
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2v20" }),
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2a10 10 0 0 1 0 20", fill: "currentColor", stroke: "none" })
|
|
93
|
+
] })
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: "invertColors",
|
|
97
|
+
label: "Invert Colors",
|
|
98
|
+
desc: "Invert all page colours",
|
|
99
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
100
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3v18M3 12h18" }),
|
|
101
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3a9 9 0 0 1 0 18", fill: "currentColor", stroke: "none" })
|
|
102
|
+
] })
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "grayscale",
|
|
106
|
+
label: "Grayscale",
|
|
107
|
+
desc: "Remove all colour from the page",
|
|
108
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "9" }),
|
|
110
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "4", fill: "currentColor", stroke: "none" })
|
|
111
|
+
] })
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "reduceMotion",
|
|
115
|
+
label: "Reduce Motion",
|
|
116
|
+
desc: "Stop animations and transitions",
|
|
117
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 12h14M12 5l-7 7 7 7" }) })
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "highlightLinks",
|
|
121
|
+
label: "Highlight Links",
|
|
122
|
+
desc: "Make all links visible at a glance",
|
|
123
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
|
|
126
|
+
] })
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: "textSpacing",
|
|
130
|
+
label: "Text Spacing",
|
|
131
|
+
desc: "Increase letter, word & line spacing",
|
|
132
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 6h16M4 10h16M4 14h16M4 18h16" }) })
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "adhd",
|
|
136
|
+
label: "ADHD Friendly",
|
|
137
|
+
desc: "Remove distractions, add focus ring",
|
|
138
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
139
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
140
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2 17l10 5 10-5" }),
|
|
141
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2 12l10 5 10-5" })
|
|
142
|
+
] })
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "dyslexia",
|
|
146
|
+
label: "Dyslexia Font",
|
|
147
|
+
desc: "Switch to a high-readability typeface",
|
|
148
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 7V4h16v3" }),
|
|
150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M9 20h6" }),
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 4v16" })
|
|
152
|
+
] })
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
function A11yWidget({ className }) {
|
|
156
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
157
|
+
const [prefs, setPrefs] = (0, import_react.useState)(DEFAULTS);
|
|
158
|
+
const [mounted, setMounted] = (0, import_react.useState)(false);
|
|
159
|
+
const triggerRef = (0, import_react.useRef)(null);
|
|
160
|
+
(0, import_react.useEffect)(() => {
|
|
161
|
+
setMounted(true);
|
|
162
|
+
}, []);
|
|
163
|
+
(0, import_react.useEffect)(() => {
|
|
164
|
+
const saved = load();
|
|
165
|
+
setPrefs(saved);
|
|
166
|
+
applyPrefs(saved);
|
|
167
|
+
}, []);
|
|
168
|
+
(0, import_react.useEffect)(() => {
|
|
169
|
+
if (!open) return;
|
|
170
|
+
const onKey = (e) => {
|
|
171
|
+
if (e.key === "Escape") setOpen(false);
|
|
172
|
+
};
|
|
173
|
+
document.addEventListener("keydown", onKey);
|
|
174
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
175
|
+
}, [open]);
|
|
176
|
+
const update = (patch) => {
|
|
177
|
+
setPrefs((prev) => {
|
|
178
|
+
const next = { ...prev, ...patch };
|
|
179
|
+
applyPrefs(next);
|
|
180
|
+
save(next);
|
|
181
|
+
return next;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const reset = () => {
|
|
185
|
+
setPrefs(DEFAULTS);
|
|
186
|
+
applyPrefs(DEFAULTS);
|
|
187
|
+
save(DEFAULTS);
|
|
188
|
+
};
|
|
189
|
+
const isModified = JSON.stringify(prefs) !== JSON.stringify(DEFAULTS);
|
|
190
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
191
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
192
|
+
"button",
|
|
193
|
+
{
|
|
194
|
+
ref: triggerRef,
|
|
195
|
+
type: "button",
|
|
196
|
+
"aria-label": "Accessibility options",
|
|
197
|
+
"aria-expanded": open,
|
|
198
|
+
"aria-haspopup": "dialog",
|
|
199
|
+
onClick: () => setOpen((v) => !v),
|
|
200
|
+
className: `relative flex h-10 w-10 items-center justify-center border border-white/10 bg-[rgba(4,6,10,0.85)] text-white/60 shadow-lg transition-[border-color,color,background] duration-200 hover:border-[rgba(0,229,160,0.4)] hover:bg-[rgba(0,229,160,0.08)] hover:text-[#00e5a0] focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${className ?? ""}`,
|
|
201
|
+
children: [
|
|
202
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", className: "h-5 w-5", fill: "none", stroke: "currentColor", strokeWidth: 1.8, "aria-hidden": "true", children: [
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
204
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 8.5h14M8 8.5l1 10 3-4 3 4 1-10", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
205
|
+
] }),
|
|
206
|
+
isModified && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full bg-[#00e5a0]", "aria-hidden": "true" })
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
),
|
|
210
|
+
open && mounted && (0, import_react_dom.createPortal)(
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
212
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
213
|
+
"div",
|
|
214
|
+
{
|
|
215
|
+
className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm",
|
|
216
|
+
onClick: () => setOpen(false),
|
|
217
|
+
"aria-hidden": "true"
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
role: "dialog",
|
|
224
|
+
"aria-modal": "true",
|
|
225
|
+
"aria-label": "Accessibility settings",
|
|
226
|
+
className: "fixed left-1/2 top-1/2 z-9999 w-[clamp(280px,90vw,360px)] -translate-x-1/2 -translate-y-1/2 border border-white/10 bg-[rgba(4,6,10,0.98)] shadow-2xl max-[480px]:w-[calc(100vw-2rem)]",
|
|
227
|
+
children: [
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between border-b border-white/[0.07] px-4 py-3", children: [
|
|
229
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[0.72rem] font-semibold tracking-widest text-[#eef1f8] uppercase", children: "Accessibility" }),
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-0.5 text-[0.52rem] tracking-[0.12em] text-white/35 uppercase", children: "WCAG 2.1 \xB7 Personalise your experience" })
|
|
232
|
+
] }),
|
|
233
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
234
|
+
"button",
|
|
235
|
+
{
|
|
236
|
+
type: "button",
|
|
237
|
+
"aria-label": "Close accessibility panel",
|
|
238
|
+
onClick: () => setOpen(false),
|
|
239
|
+
className: "flex h-6 w-6 items-center justify-center text-white/30 transition-colors duration-150 hover:text-white/80 focus-visible:ring-2 focus-visible:ring-[#00e5a0]",
|
|
240
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", className: "h-3.5 w-3.5", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6 6 18M6 6l12 12", strokeLinecap: "round" }) })
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
] }),
|
|
244
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "border-b border-white/[0.07] px-4 py-3", children: [
|
|
245
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
246
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[0.6rem] tracking-[0.16em] text-white/50 uppercase", children: "Text Size" }),
|
|
247
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[0.56rem] tracking-[0.12em] text-[#00e5a0] uppercase", children: TEXT_LABELS[prefs.textSize] })
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex gap-1.5", children: TEXT_LABELS.map((lbl, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
type: "button",
|
|
253
|
+
"aria-pressed": prefs.textSize === i,
|
|
254
|
+
onClick: () => update({ textSize: i }),
|
|
255
|
+
className: `flex-1 border py-1.5 text-center text-[0.58rem] uppercase tracking-widest transition-[background,border-color,color] duration-150 focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${prefs.textSize === i ? "border-[#00e5a0] bg-[rgba(0,229,160,0.1)] text-[#00e5a0]" : "border-white/10 bg-transparent text-white/35 hover:border-white/25 hover:text-white/60"}`,
|
|
256
|
+
children: lbl
|
|
257
|
+
},
|
|
258
|
+
lbl
|
|
259
|
+
)) })
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "max-h-[min(50vh,340px)] overflow-y-auto", children: TOGGLES.map(({ key, label, desc, icon }) => {
|
|
262
|
+
const on = prefs[key];
|
|
263
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
264
|
+
"button",
|
|
265
|
+
{
|
|
266
|
+
type: "button",
|
|
267
|
+
role: "switch",
|
|
268
|
+
"aria-checked": on,
|
|
269
|
+
onClick: () => update({ [key]: !on }),
|
|
270
|
+
className: `group flex w-full items-center gap-3 px-4 py-2.5 text-left transition-[background] duration-150 hover:bg-white/3 focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${on ? "bg-[rgba(0,229,160,0.04)]" : ""}`,
|
|
271
|
+
children: [
|
|
272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `shrink-0 transition-colors duration-150 ${on ? "text-[#00e5a0]" : "text-white/30 group-hover:text-white/50"}`, children: icon }),
|
|
273
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "min-w-0 flex-1", children: [
|
|
274
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `block text-[0.68rem] font-medium leading-none tracking-[0.04em] transition-colors duration-150 ${on ? "text-[#eef1f8]" : "text-white/55"}`, children: label }),
|
|
275
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mt-0.5 block text-[0.52rem] leading-snug tracking-[0.06em] text-white/25", children: desc })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
278
|
+
"span",
|
|
279
|
+
{
|
|
280
|
+
"aria-hidden": "true",
|
|
281
|
+
className: `relative h-4 w-7 shrink-0 rounded-full border transition-[background,border-color] duration-200 ${on ? "border-[#00e5a0] bg-[rgba(0,229,160,0.2)]" : "border-white/15 bg-transparent"}`,
|
|
282
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
283
|
+
"span",
|
|
284
|
+
{
|
|
285
|
+
className: `absolute top-0.5 h-3 w-3 rounded-full transition-[left,background] duration-200 ${on ? "left-3.5 bg-[#00e5a0]" : "left-0.5 bg-white/25"}`
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
]
|
|
291
|
+
},
|
|
292
|
+
key
|
|
293
|
+
);
|
|
294
|
+
}) }),
|
|
295
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-3 border-t border-white/[0.07] px-4 py-2.5", children: [
|
|
296
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[0.5rem] uppercase tracking-widest text-white/20", children: "Changes apply instantly & persist" }),
|
|
297
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
298
|
+
"button",
|
|
299
|
+
{
|
|
300
|
+
type: "button",
|
|
301
|
+
onClick: reset,
|
|
302
|
+
disabled: !isModified,
|
|
303
|
+
className: "text-[0.56rem] uppercase tracking-[0.12em] text-white/30 underline-offset-2 transition-colors duration-150 hover:text-[#ff6b6b] disabled:cursor-not-allowed disabled:opacity-30 focus-visible:ring-2 focus-visible:ring-[#00e5a0]",
|
|
304
|
+
children: "Reset all"
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
] })
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
] }),
|
|
312
|
+
document.body
|
|
313
|
+
)
|
|
314
|
+
] });
|
|
315
|
+
}
|
|
316
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
317
|
+
0 && (module.exports = {
|
|
318
|
+
A11yWidget
|
|
319
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/widget.jsx
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
var STORAGE_KEY = "pgm-a11y";
|
|
8
|
+
var DEFAULTS = {
|
|
9
|
+
textSize: 0,
|
|
10
|
+
// 0=normal | 1=large | 2=x-large
|
|
11
|
+
highContrast: false,
|
|
12
|
+
invertColors: false,
|
|
13
|
+
grayscale: false,
|
|
14
|
+
reduceMotion: false,
|
|
15
|
+
highlightLinks: false,
|
|
16
|
+
textSpacing: false,
|
|
17
|
+
adhd: false,
|
|
18
|
+
dyslexia: false
|
|
19
|
+
};
|
|
20
|
+
function applyPrefs(prefs) {
|
|
21
|
+
const root = document.documentElement;
|
|
22
|
+
root.classList.remove("a11y-text-lg", "a11y-text-xl");
|
|
23
|
+
if (prefs.textSize === 1) root.classList.add("a11y-text-lg");
|
|
24
|
+
if (prefs.textSize === 2) root.classList.add("a11y-text-xl");
|
|
25
|
+
const map = {
|
|
26
|
+
"a11y-high-contrast": prefs.highContrast,
|
|
27
|
+
"a11y-invert": prefs.invertColors,
|
|
28
|
+
"a11y-grayscale": prefs.grayscale,
|
|
29
|
+
"a11y-reduce-motion": prefs.reduceMotion,
|
|
30
|
+
"a11y-highlight-links": prefs.highlightLinks,
|
|
31
|
+
"a11y-text-spacing": prefs.textSpacing,
|
|
32
|
+
"a11y-adhd": prefs.adhd,
|
|
33
|
+
"a11y-dyslexia": prefs.dyslexia
|
|
34
|
+
};
|
|
35
|
+
for (const [cls, on] of Object.entries(map)) {
|
|
36
|
+
root.classList.toggle(cls, on);
|
|
37
|
+
}
|
|
38
|
+
const filters = [];
|
|
39
|
+
if (prefs.invertColors) filters.push("invert(1) hue-rotate(180deg)");
|
|
40
|
+
if (prefs.highContrast) filters.push("contrast(1.6)");
|
|
41
|
+
if (prefs.grayscale) filters.push("grayscale(1)");
|
|
42
|
+
root.style.filter = filters.join(" ");
|
|
43
|
+
}
|
|
44
|
+
function load() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
47
|
+
return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS };
|
|
48
|
+
} catch {
|
|
49
|
+
return { ...DEFAULTS };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function save(prefs) {
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
var TEXT_LABELS = ["Normal", "Large", "X-Large"];
|
|
59
|
+
var TOGGLES = [
|
|
60
|
+
{
|
|
61
|
+
key: "highContrast",
|
|
62
|
+
label: "High Contrast",
|
|
63
|
+
desc: "Increase colour contrast for readability",
|
|
64
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
65
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
|
|
66
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2v20" }),
|
|
67
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2a10 10 0 0 1 0 20", fill: "currentColor", stroke: "none" })
|
|
68
|
+
] })
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "invertColors",
|
|
72
|
+
label: "Invert Colors",
|
|
73
|
+
desc: "Invert all page colours",
|
|
74
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
75
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3v18M3 12h18" }),
|
|
76
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3a9 9 0 0 1 0 18", fill: "currentColor", stroke: "none" })
|
|
77
|
+
] })
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "grayscale",
|
|
81
|
+
label: "Grayscale",
|
|
82
|
+
desc: "Remove all colour from the page",
|
|
83
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
|
|
85
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4", fill: "currentColor", stroke: "none" })
|
|
86
|
+
] })
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "reduceMotion",
|
|
90
|
+
label: "Reduce Motion",
|
|
91
|
+
desc: "Stop animations and transitions",
|
|
92
|
+
icon: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M5 12h14M12 5l-7 7 7 7" }) })
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "highlightLinks",
|
|
96
|
+
label: "Highlight Links",
|
|
97
|
+
desc: "Make all links visible at a glance",
|
|
98
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
100
|
+
/* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
|
|
101
|
+
] })
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "textSpacing",
|
|
105
|
+
label: "Text Spacing",
|
|
106
|
+
desc: "Increase letter, word & line spacing",
|
|
107
|
+
icon: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 6h16M4 10h16M4 14h16M4 18h16" }) })
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "adhd",
|
|
111
|
+
label: "ADHD Friendly",
|
|
112
|
+
desc: "Remove distractions, add focus ring",
|
|
113
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
114
|
+
/* @__PURE__ */ jsx("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
115
|
+
/* @__PURE__ */ jsx("path", { d: "M2 17l10 5 10-5" }),
|
|
116
|
+
/* @__PURE__ */ jsx("path", { d: "M2 12l10 5 10-5" })
|
|
117
|
+
] })
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "dyslexia",
|
|
121
|
+
label: "Dyslexia Font",
|
|
122
|
+
desc: "Switch to a high-readability typeface",
|
|
123
|
+
icon: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: [
|
|
124
|
+
/* @__PURE__ */ jsx("path", { d: "M4 7V4h16v3" }),
|
|
125
|
+
/* @__PURE__ */ jsx("path", { d: "M9 20h6" }),
|
|
126
|
+
/* @__PURE__ */ jsx("path", { d: "M12 4v16" })
|
|
127
|
+
] })
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
function A11yWidget({ className }) {
|
|
131
|
+
const [open, setOpen] = useState(false);
|
|
132
|
+
const [prefs, setPrefs] = useState(DEFAULTS);
|
|
133
|
+
const [mounted, setMounted] = useState(false);
|
|
134
|
+
const triggerRef = useRef(null);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
setMounted(true);
|
|
137
|
+
}, []);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const saved = load();
|
|
140
|
+
setPrefs(saved);
|
|
141
|
+
applyPrefs(saved);
|
|
142
|
+
}, []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!open) return;
|
|
145
|
+
const onKey = (e) => {
|
|
146
|
+
if (e.key === "Escape") setOpen(false);
|
|
147
|
+
};
|
|
148
|
+
document.addEventListener("keydown", onKey);
|
|
149
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
150
|
+
}, [open]);
|
|
151
|
+
const update = (patch) => {
|
|
152
|
+
setPrefs((prev) => {
|
|
153
|
+
const next = { ...prev, ...patch };
|
|
154
|
+
applyPrefs(next);
|
|
155
|
+
save(next);
|
|
156
|
+
return next;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const reset = () => {
|
|
160
|
+
setPrefs(DEFAULTS);
|
|
161
|
+
applyPrefs(DEFAULTS);
|
|
162
|
+
save(DEFAULTS);
|
|
163
|
+
};
|
|
164
|
+
const isModified = JSON.stringify(prefs) !== JSON.stringify(DEFAULTS);
|
|
165
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsxs(
|
|
167
|
+
"button",
|
|
168
|
+
{
|
|
169
|
+
ref: triggerRef,
|
|
170
|
+
type: "button",
|
|
171
|
+
"aria-label": "Accessibility options",
|
|
172
|
+
"aria-expanded": open,
|
|
173
|
+
"aria-haspopup": "dialog",
|
|
174
|
+
onClick: () => setOpen((v) => !v),
|
|
175
|
+
className: `relative flex h-10 w-10 items-center justify-center border border-white/10 bg-[rgba(4,6,10,0.85)] text-white/60 shadow-lg transition-[border-color,color,background] duration-200 hover:border-[rgba(0,229,160,0.4)] hover:bg-[rgba(0,229,160,0.08)] hover:text-[#00e5a0] focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${className ?? ""}`,
|
|
176
|
+
children: [
|
|
177
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", className: "h-5 w-5", fill: "none", stroke: "currentColor", strokeWidth: 1.8, "aria-hidden": "true", children: [
|
|
178
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
179
|
+
/* @__PURE__ */ jsx("path", { d: "M5 8.5h14M8 8.5l1 10 3-4 3 4 1-10", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
180
|
+
] }),
|
|
181
|
+
isModified && /* @__PURE__ */ jsx("span", { className: "absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full bg-[#00e5a0]", "aria-hidden": "true" })
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
open && mounted && createPortal(
|
|
186
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
"div",
|
|
189
|
+
{
|
|
190
|
+
className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm",
|
|
191
|
+
onClick: () => setOpen(false),
|
|
192
|
+
"aria-hidden": "true"
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
/* @__PURE__ */ jsxs(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
role: "dialog",
|
|
199
|
+
"aria-modal": "true",
|
|
200
|
+
"aria-label": "Accessibility settings",
|
|
201
|
+
className: "fixed left-1/2 top-1/2 z-9999 w-[clamp(280px,90vw,360px)] -translate-x-1/2 -translate-y-1/2 border border-white/10 bg-[rgba(4,6,10,0.98)] shadow-2xl max-[480px]:w-[calc(100vw-2rem)]",
|
|
202
|
+
children: [
|
|
203
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-white/[0.07] px-4 py-3", children: [
|
|
204
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
205
|
+
/* @__PURE__ */ jsx("p", { className: "text-[0.72rem] font-semibold tracking-widest text-[#eef1f8] uppercase", children: "Accessibility" }),
|
|
206
|
+
/* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[0.52rem] tracking-[0.12em] text-white/35 uppercase", children: "WCAG 2.1 \xB7 Personalise your experience" })
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ jsx(
|
|
209
|
+
"button",
|
|
210
|
+
{
|
|
211
|
+
type: "button",
|
|
212
|
+
"aria-label": "Close accessibility panel",
|
|
213
|
+
onClick: () => setOpen(false),
|
|
214
|
+
className: "flex h-6 w-6 items-center justify-center text-white/30 transition-colors duration-150 hover:text-white/80 focus-visible:ring-2 focus-visible:ring-[#00e5a0]",
|
|
215
|
+
children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "h-3.5 w-3.5", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12", strokeLinecap: "round" }) })
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
] }),
|
|
219
|
+
/* @__PURE__ */ jsxs("div", { className: "border-b border-white/[0.07] px-4 py-3", children: [
|
|
220
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
221
|
+
/* @__PURE__ */ jsx("span", { className: "text-[0.6rem] tracking-[0.16em] text-white/50 uppercase", children: "Text Size" }),
|
|
222
|
+
/* @__PURE__ */ jsx("span", { className: "text-[0.56rem] tracking-[0.12em] text-[#00e5a0] uppercase", children: TEXT_LABELS[prefs.textSize] })
|
|
223
|
+
] }),
|
|
224
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5", children: TEXT_LABELS.map((lbl, i) => /* @__PURE__ */ jsx(
|
|
225
|
+
"button",
|
|
226
|
+
{
|
|
227
|
+
type: "button",
|
|
228
|
+
"aria-pressed": prefs.textSize === i,
|
|
229
|
+
onClick: () => update({ textSize: i }),
|
|
230
|
+
className: `flex-1 border py-1.5 text-center text-[0.58rem] uppercase tracking-widest transition-[background,border-color,color] duration-150 focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${prefs.textSize === i ? "border-[#00e5a0] bg-[rgba(0,229,160,0.1)] text-[#00e5a0]" : "border-white/10 bg-transparent text-white/35 hover:border-white/25 hover:text-white/60"}`,
|
|
231
|
+
children: lbl
|
|
232
|
+
},
|
|
233
|
+
lbl
|
|
234
|
+
)) })
|
|
235
|
+
] }),
|
|
236
|
+
/* @__PURE__ */ jsx("div", { className: "max-h-[min(50vh,340px)] overflow-y-auto", children: TOGGLES.map(({ key, label, desc, icon }) => {
|
|
237
|
+
const on = prefs[key];
|
|
238
|
+
return /* @__PURE__ */ jsxs(
|
|
239
|
+
"button",
|
|
240
|
+
{
|
|
241
|
+
type: "button",
|
|
242
|
+
role: "switch",
|
|
243
|
+
"aria-checked": on,
|
|
244
|
+
onClick: () => update({ [key]: !on }),
|
|
245
|
+
className: `group flex w-full items-center gap-3 px-4 py-2.5 text-left transition-[background] duration-150 hover:bg-white/3 focus-visible:ring-2 focus-visible:ring-[#00e5a0] ${on ? "bg-[rgba(0,229,160,0.04)]" : ""}`,
|
|
246
|
+
children: [
|
|
247
|
+
/* @__PURE__ */ jsx("span", { className: `shrink-0 transition-colors duration-150 ${on ? "text-[#00e5a0]" : "text-white/30 group-hover:text-white/50"}`, children: icon }),
|
|
248
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
249
|
+
/* @__PURE__ */ jsx("span", { className: `block text-[0.68rem] font-medium leading-none tracking-[0.04em] transition-colors duration-150 ${on ? "text-[#eef1f8]" : "text-white/55"}`, children: label }),
|
|
250
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-[0.52rem] leading-snug tracking-[0.06em] text-white/25", children: desc })
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsx(
|
|
253
|
+
"span",
|
|
254
|
+
{
|
|
255
|
+
"aria-hidden": "true",
|
|
256
|
+
className: `relative h-4 w-7 shrink-0 rounded-full border transition-[background,border-color] duration-200 ${on ? "border-[#00e5a0] bg-[rgba(0,229,160,0.2)]" : "border-white/15 bg-transparent"}`,
|
|
257
|
+
children: /* @__PURE__ */ jsx(
|
|
258
|
+
"span",
|
|
259
|
+
{
|
|
260
|
+
className: `absolute top-0.5 h-3 w-3 rounded-full transition-[left,background] duration-200 ${on ? "left-3.5 bg-[#00e5a0]" : "left-0.5 bg-white/25"}`
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
key
|
|
268
|
+
);
|
|
269
|
+
}) }),
|
|
270
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-t border-white/[0.07] px-4 py-2.5", children: [
|
|
271
|
+
/* @__PURE__ */ jsx("span", { className: "text-[0.5rem] uppercase tracking-widest text-white/20", children: "Changes apply instantly & persist" }),
|
|
272
|
+
/* @__PURE__ */ jsx(
|
|
273
|
+
"button",
|
|
274
|
+
{
|
|
275
|
+
type: "button",
|
|
276
|
+
onClick: reset,
|
|
277
|
+
disabled: !isModified,
|
|
278
|
+
className: "text-[0.56rem] uppercase tracking-[0.12em] text-white/30 underline-offset-2 transition-colors duration-150 hover:text-[#ff6b6b] disabled:cursor-not-allowed disabled:opacity-30 focus-visible:ring-2 focus-visible:ring-[#00e5a0]",
|
|
279
|
+
children: "Reset all"
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
] })
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
] }),
|
|
287
|
+
document.body
|
|
288
|
+
)
|
|
289
|
+
] });
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
A11yWidget,
|
|
293
|
+
A11yWidget as default
|
|
294
|
+
};
|