@gtcx/accessibility 0.1.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/README.md +163 -0
- package/dist/index.d.mts +183 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +329 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +294 -0
- package/dist/tech-literacy.d.ts +99 -0
- package/dist/tech-literacy.d.ts.map +1 -0
- package/dist/tech-literacy.js +177 -0
- package/dist/tech-literacy.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
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.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
LITERACY_UI_CONFIGS: () => LITERACY_UI_CONFIGS,
|
|
24
|
+
TechLiteracyDetector: () => TechLiteracyDetector,
|
|
25
|
+
contrastRatio: () => contrastRatio,
|
|
26
|
+
detectTechLiteracy: () => detectTechLiteracy,
|
|
27
|
+
meetsWCAG: () => meetsWCAG,
|
|
28
|
+
relativeLuminance: () => relativeLuminance,
|
|
29
|
+
useAnnounce: () => useAnnounce,
|
|
30
|
+
useFocusTrap: () => useFocusTrap,
|
|
31
|
+
useReducedMotion: () => useReducedMotion
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/tech-literacy.ts
|
|
36
|
+
async function detectTechLiteracy(interactions) {
|
|
37
|
+
if (interactions.length < 3) {
|
|
38
|
+
return {
|
|
39
|
+
level: "beginner",
|
|
40
|
+
confidence: 0.3,
|
|
41
|
+
signals: ["insufficient_data"]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const signals = [];
|
|
45
|
+
let score = 50;
|
|
46
|
+
const avgActionTime = average(interactions.map((i) => i.actionTime));
|
|
47
|
+
const totalErrors = sum(interactions.map((i) => i.errorCount));
|
|
48
|
+
const shortcutUsage = interactions.filter((i) => i.usedShortcuts).length;
|
|
49
|
+
const helpRequests = interactions.filter((i) => i.requestedHelp).length;
|
|
50
|
+
const voiceUsage = interactions.filter((i) => i.usedVoice).length;
|
|
51
|
+
if (avgActionTime < 2e3) {
|
|
52
|
+
score += 15;
|
|
53
|
+
signals.push("fast_completion");
|
|
54
|
+
} else if (avgActionTime > 8e3) {
|
|
55
|
+
score -= 15;
|
|
56
|
+
signals.push("slow_completion");
|
|
57
|
+
}
|
|
58
|
+
if (totalErrors > interactions.length * 2) {
|
|
59
|
+
score -= 20;
|
|
60
|
+
signals.push("high_error_rate");
|
|
61
|
+
} else if (totalErrors === 0) {
|
|
62
|
+
score += 10;
|
|
63
|
+
signals.push("no_errors");
|
|
64
|
+
}
|
|
65
|
+
if (shortcutUsage > interactions.length * 0.3) {
|
|
66
|
+
score += 25;
|
|
67
|
+
signals.push("keyboard_shortcuts");
|
|
68
|
+
}
|
|
69
|
+
if (helpRequests > interactions.length * 0.3) {
|
|
70
|
+
score -= 15;
|
|
71
|
+
signals.push("frequent_help_requests");
|
|
72
|
+
}
|
|
73
|
+
if (voiceUsage > 0) {
|
|
74
|
+
signals.push("voice_user");
|
|
75
|
+
}
|
|
76
|
+
let level;
|
|
77
|
+
if (score >= 70) {
|
|
78
|
+
level = "advanced";
|
|
79
|
+
} else if (score >= 40) {
|
|
80
|
+
level = "intermediate";
|
|
81
|
+
} else {
|
|
82
|
+
level = "beginner";
|
|
83
|
+
}
|
|
84
|
+
const confidence = Math.min(0.95, 0.5 + interactions.length * 0.05);
|
|
85
|
+
return { level, confidence, signals };
|
|
86
|
+
}
|
|
87
|
+
var TechLiteracyDetector = class {
|
|
88
|
+
interactions = [];
|
|
89
|
+
currentLevel = "beginner";
|
|
90
|
+
onLevelChange;
|
|
91
|
+
constructor(options) {
|
|
92
|
+
this.currentLevel = options?.initialLevel || "beginner";
|
|
93
|
+
this.onLevelChange = options?.onLevelChange;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Record an interaction for analysis
|
|
97
|
+
*/
|
|
98
|
+
recordInteraction(pattern) {
|
|
99
|
+
this.interactions.push({
|
|
100
|
+
...pattern,
|
|
101
|
+
timestamp: Date.now()
|
|
102
|
+
});
|
|
103
|
+
if (this.interactions.length > 50) {
|
|
104
|
+
this.interactions = this.interactions.slice(-50);
|
|
105
|
+
}
|
|
106
|
+
if (this.interactions.length % 5 === 0) {
|
|
107
|
+
this.evaluate();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Evaluate current literacy level
|
|
112
|
+
*/
|
|
113
|
+
async evaluate() {
|
|
114
|
+
const result = await detectTechLiteracy(this.interactions);
|
|
115
|
+
if (result.level !== this.currentLevel && result.confidence > 0.7) {
|
|
116
|
+
this.currentLevel = result.level;
|
|
117
|
+
this.onLevelChange?.(result.level);
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get current assessed level
|
|
123
|
+
*/
|
|
124
|
+
getLevel() {
|
|
125
|
+
return this.currentLevel;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Manually set level (user preference)
|
|
129
|
+
*/
|
|
130
|
+
setLevel(level) {
|
|
131
|
+
this.currentLevel = level;
|
|
132
|
+
this.onLevelChange?.(level);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var LITERACY_UI_CONFIGS = {
|
|
136
|
+
beginner: {
|
|
137
|
+
buttonSize: "large",
|
|
138
|
+
// min 48px touch target
|
|
139
|
+
fontSize: "large",
|
|
140
|
+
// 18px+
|
|
141
|
+
navigation: "simple",
|
|
142
|
+
// Linear, no nested menus
|
|
143
|
+
icons: "labeled",
|
|
144
|
+
// Always show text with icons
|
|
145
|
+
animations: "minimal",
|
|
146
|
+
// Reduce motion
|
|
147
|
+
guidance: "proactive",
|
|
148
|
+
// Show help automatically
|
|
149
|
+
inputMode: "touch",
|
|
150
|
+
// Optimize for touch
|
|
151
|
+
errorHandling: "guided",
|
|
152
|
+
// Step-by-step error recovery
|
|
153
|
+
confirmations: "explicit",
|
|
154
|
+
// Confirm all actions
|
|
155
|
+
shortcuts: "hidden"
|
|
156
|
+
// Don't show keyboard shortcuts
|
|
157
|
+
},
|
|
158
|
+
intermediate: {
|
|
159
|
+
buttonSize: "medium",
|
|
160
|
+
fontSize: "medium",
|
|
161
|
+
// 16px
|
|
162
|
+
navigation: "standard",
|
|
163
|
+
// Some nesting allowed
|
|
164
|
+
icons: "tooltips",
|
|
165
|
+
// Show text on hover
|
|
166
|
+
animations: "standard",
|
|
167
|
+
guidance: "contextual",
|
|
168
|
+
// Show help when hovering
|
|
169
|
+
inputMode: "mixed",
|
|
170
|
+
// Touch + keyboard
|
|
171
|
+
errorHandling: "inline",
|
|
172
|
+
// Inline error messages
|
|
173
|
+
confirmations: "smart",
|
|
174
|
+
// Confirm destructive only
|
|
175
|
+
shortcuts: "discoverable"
|
|
176
|
+
// Show shortcuts in tooltips
|
|
177
|
+
},
|
|
178
|
+
advanced: {
|
|
179
|
+
buttonSize: "compact",
|
|
180
|
+
fontSize: "small",
|
|
181
|
+
// 14px
|
|
182
|
+
navigation: "full",
|
|
183
|
+
// All features accessible
|
|
184
|
+
icons: "minimal",
|
|
185
|
+
// Icons only, no labels
|
|
186
|
+
animations: "full",
|
|
187
|
+
guidance: "on-demand",
|
|
188
|
+
// Help only when requested
|
|
189
|
+
inputMode: "keyboard",
|
|
190
|
+
// Keyboard-first
|
|
191
|
+
errorHandling: "technical",
|
|
192
|
+
// Technical error details
|
|
193
|
+
confirmations: "minimal",
|
|
194
|
+
// Skip most confirmations
|
|
195
|
+
shortcuts: "prominent"
|
|
196
|
+
// Show all shortcuts
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
function average(arr) {
|
|
200
|
+
return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
201
|
+
}
|
|
202
|
+
function sum(arr) {
|
|
203
|
+
return arr.reduce((a, b) => a + b, 0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/contrast.ts
|
|
207
|
+
function hexToRgb(hex) {
|
|
208
|
+
const h = hex.replace("#", "");
|
|
209
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
210
|
+
return [
|
|
211
|
+
parseInt(full.slice(0, 2), 16),
|
|
212
|
+
parseInt(full.slice(2, 4), 16),
|
|
213
|
+
parseInt(full.slice(4, 6), 16)
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
function srgbToLinear(channel) {
|
|
217
|
+
const s = channel / 255;
|
|
218
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
219
|
+
}
|
|
220
|
+
function relativeLuminance(hex) {
|
|
221
|
+
const [r, g, b] = hexToRgb(hex);
|
|
222
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
223
|
+
}
|
|
224
|
+
function contrastRatio(fg, bg) {
|
|
225
|
+
const l1 = relativeLuminance(fg);
|
|
226
|
+
const l2 = relativeLuminance(bg);
|
|
227
|
+
const lighter = Math.max(l1, l2);
|
|
228
|
+
const darker = Math.min(l1, l2);
|
|
229
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
230
|
+
}
|
|
231
|
+
function meetsWCAG(fg, bg, level = "AA", large = false) {
|
|
232
|
+
const ratio = contrastRatio(fg, bg);
|
|
233
|
+
if (level === "AAA") return large ? ratio >= 4.5 : ratio >= 7;
|
|
234
|
+
return large ? ratio >= 3 : ratio >= 4.5;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/use-reduced-motion.ts
|
|
238
|
+
var import_react = require("react");
|
|
239
|
+
var QUERY = "(prefers-reduced-motion: reduce)";
|
|
240
|
+
function useReducedMotion() {
|
|
241
|
+
const [reduced, setReduced] = (0, import_react.useState)(() => {
|
|
242
|
+
if (typeof window === "undefined") return false;
|
|
243
|
+
return window.matchMedia(QUERY).matches;
|
|
244
|
+
});
|
|
245
|
+
(0, import_react.useEffect)(() => {
|
|
246
|
+
const mql = window.matchMedia(QUERY);
|
|
247
|
+
const handler = (e) => setReduced(e.matches);
|
|
248
|
+
mql.addEventListener("change", handler);
|
|
249
|
+
return () => mql.removeEventListener("change", handler);
|
|
250
|
+
}, []);
|
|
251
|
+
return reduced;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/use-focus-trap.ts
|
|
255
|
+
var import_react2 = require("react");
|
|
256
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
257
|
+
function useFocusTrap(active) {
|
|
258
|
+
const ref = (0, import_react2.useRef)(null);
|
|
259
|
+
(0, import_react2.useEffect)(() => {
|
|
260
|
+
if (!active || !ref.current) return;
|
|
261
|
+
const container = ref.current;
|
|
262
|
+
const previouslyFocused = document.activeElement;
|
|
263
|
+
const first = container.querySelector(FOCUSABLE);
|
|
264
|
+
first?.focus();
|
|
265
|
+
function handleKeyDown(e) {
|
|
266
|
+
if (e.key !== "Tab") return;
|
|
267
|
+
const focusable = container.querySelectorAll(FOCUSABLE);
|
|
268
|
+
if (focusable.length === 0) return;
|
|
269
|
+
const firstEl = focusable[0];
|
|
270
|
+
const lastEl = focusable[focusable.length - 1];
|
|
271
|
+
if (e.shiftKey && document.activeElement === firstEl) {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
lastEl.focus();
|
|
274
|
+
} else if (!e.shiftKey && document.activeElement === lastEl) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
firstEl.focus();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
280
|
+
return () => {
|
|
281
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
282
|
+
previouslyFocused?.focus();
|
|
283
|
+
};
|
|
284
|
+
}, [active]);
|
|
285
|
+
return ref;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/use-announce.ts
|
|
289
|
+
var import_react3 = require("react");
|
|
290
|
+
function useAnnounce(politeness = "polite") {
|
|
291
|
+
const ref = (0, import_react3.useRef)(null);
|
|
292
|
+
const announce = (0, import_react3.useCallback)((message) => {
|
|
293
|
+
if (!ref.current) return;
|
|
294
|
+
ref.current.textContent = "";
|
|
295
|
+
requestAnimationFrame(() => {
|
|
296
|
+
if (ref.current) ref.current.textContent = message;
|
|
297
|
+
});
|
|
298
|
+
}, []);
|
|
299
|
+
const liveRegionProps = {
|
|
300
|
+
ref,
|
|
301
|
+
role: "status",
|
|
302
|
+
"aria-live": politeness,
|
|
303
|
+
"aria-atomic": true,
|
|
304
|
+
style: {
|
|
305
|
+
position: "absolute",
|
|
306
|
+
width: 1,
|
|
307
|
+
height: 1,
|
|
308
|
+
padding: 0,
|
|
309
|
+
margin: -1,
|
|
310
|
+
overflow: "hidden",
|
|
311
|
+
clip: "rect(0, 0, 0, 0)",
|
|
312
|
+
whiteSpace: "nowrap",
|
|
313
|
+
border: 0
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
return { announce, liveRegionProps };
|
|
317
|
+
}
|
|
318
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
319
|
+
0 && (module.exports = {
|
|
320
|
+
LITERACY_UI_CONFIGS,
|
|
321
|
+
TechLiteracyDetector,
|
|
322
|
+
contrastRatio,
|
|
323
|
+
detectTechLiteracy,
|
|
324
|
+
meetsWCAG,
|
|
325
|
+
relativeLuminance,
|
|
326
|
+
useAnnounce,
|
|
327
|
+
useFocusTrap,
|
|
328
|
+
useReducedMotion
|
|
329
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iDAIyB;AAHvB,qHAAA,oBAAoB,OAAA;AACpB,mHAAA,kBAAkB,OAAA;AAClB,oHAAA,mBAAmB,OAAA;AAOrB,oCAAoC;AACpC,oDAAoD;AACpD,4DAA4D;AAC5D,sEAAsE;AACtE,uDAAuD;AACvD,gDAAgD;AAChD,oDAAoD;AACpD,yDAAyD;AACzD,gEAAgE;AAChE,+CAA+C"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/tech-literacy.ts
|
|
2
|
+
async function detectTechLiteracy(interactions) {
|
|
3
|
+
if (interactions.length < 3) {
|
|
4
|
+
return {
|
|
5
|
+
level: "beginner",
|
|
6
|
+
confidence: 0.3,
|
|
7
|
+
signals: ["insufficient_data"]
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const signals = [];
|
|
11
|
+
let score = 50;
|
|
12
|
+
const avgActionTime = average(interactions.map((i) => i.actionTime));
|
|
13
|
+
const totalErrors = sum(interactions.map((i) => i.errorCount));
|
|
14
|
+
const shortcutUsage = interactions.filter((i) => i.usedShortcuts).length;
|
|
15
|
+
const helpRequests = interactions.filter((i) => i.requestedHelp).length;
|
|
16
|
+
const voiceUsage = interactions.filter((i) => i.usedVoice).length;
|
|
17
|
+
if (avgActionTime < 2e3) {
|
|
18
|
+
score += 15;
|
|
19
|
+
signals.push("fast_completion");
|
|
20
|
+
} else if (avgActionTime > 8e3) {
|
|
21
|
+
score -= 15;
|
|
22
|
+
signals.push("slow_completion");
|
|
23
|
+
}
|
|
24
|
+
if (totalErrors > interactions.length * 2) {
|
|
25
|
+
score -= 20;
|
|
26
|
+
signals.push("high_error_rate");
|
|
27
|
+
} else if (totalErrors === 0) {
|
|
28
|
+
score += 10;
|
|
29
|
+
signals.push("no_errors");
|
|
30
|
+
}
|
|
31
|
+
if (shortcutUsage > interactions.length * 0.3) {
|
|
32
|
+
score += 25;
|
|
33
|
+
signals.push("keyboard_shortcuts");
|
|
34
|
+
}
|
|
35
|
+
if (helpRequests > interactions.length * 0.3) {
|
|
36
|
+
score -= 15;
|
|
37
|
+
signals.push("frequent_help_requests");
|
|
38
|
+
}
|
|
39
|
+
if (voiceUsage > 0) {
|
|
40
|
+
signals.push("voice_user");
|
|
41
|
+
}
|
|
42
|
+
let level;
|
|
43
|
+
if (score >= 70) {
|
|
44
|
+
level = "advanced";
|
|
45
|
+
} else if (score >= 40) {
|
|
46
|
+
level = "intermediate";
|
|
47
|
+
} else {
|
|
48
|
+
level = "beginner";
|
|
49
|
+
}
|
|
50
|
+
const confidence = Math.min(0.95, 0.5 + interactions.length * 0.05);
|
|
51
|
+
return { level, confidence, signals };
|
|
52
|
+
}
|
|
53
|
+
var TechLiteracyDetector = class {
|
|
54
|
+
interactions = [];
|
|
55
|
+
currentLevel = "beginner";
|
|
56
|
+
onLevelChange;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.currentLevel = options?.initialLevel || "beginner";
|
|
59
|
+
this.onLevelChange = options?.onLevelChange;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Record an interaction for analysis
|
|
63
|
+
*/
|
|
64
|
+
recordInteraction(pattern) {
|
|
65
|
+
this.interactions.push({
|
|
66
|
+
...pattern,
|
|
67
|
+
timestamp: Date.now()
|
|
68
|
+
});
|
|
69
|
+
if (this.interactions.length > 50) {
|
|
70
|
+
this.interactions = this.interactions.slice(-50);
|
|
71
|
+
}
|
|
72
|
+
if (this.interactions.length % 5 === 0) {
|
|
73
|
+
this.evaluate();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Evaluate current literacy level
|
|
78
|
+
*/
|
|
79
|
+
async evaluate() {
|
|
80
|
+
const result = await detectTechLiteracy(this.interactions);
|
|
81
|
+
if (result.level !== this.currentLevel && result.confidence > 0.7) {
|
|
82
|
+
this.currentLevel = result.level;
|
|
83
|
+
this.onLevelChange?.(result.level);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get current assessed level
|
|
89
|
+
*/
|
|
90
|
+
getLevel() {
|
|
91
|
+
return this.currentLevel;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Manually set level (user preference)
|
|
95
|
+
*/
|
|
96
|
+
setLevel(level) {
|
|
97
|
+
this.currentLevel = level;
|
|
98
|
+
this.onLevelChange?.(level);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var LITERACY_UI_CONFIGS = {
|
|
102
|
+
beginner: {
|
|
103
|
+
buttonSize: "large",
|
|
104
|
+
// min 48px touch target
|
|
105
|
+
fontSize: "large",
|
|
106
|
+
// 18px+
|
|
107
|
+
navigation: "simple",
|
|
108
|
+
// Linear, no nested menus
|
|
109
|
+
icons: "labeled",
|
|
110
|
+
// Always show text with icons
|
|
111
|
+
animations: "minimal",
|
|
112
|
+
// Reduce motion
|
|
113
|
+
guidance: "proactive",
|
|
114
|
+
// Show help automatically
|
|
115
|
+
inputMode: "touch",
|
|
116
|
+
// Optimize for touch
|
|
117
|
+
errorHandling: "guided",
|
|
118
|
+
// Step-by-step error recovery
|
|
119
|
+
confirmations: "explicit",
|
|
120
|
+
// Confirm all actions
|
|
121
|
+
shortcuts: "hidden"
|
|
122
|
+
// Don't show keyboard shortcuts
|
|
123
|
+
},
|
|
124
|
+
intermediate: {
|
|
125
|
+
buttonSize: "medium",
|
|
126
|
+
fontSize: "medium",
|
|
127
|
+
// 16px
|
|
128
|
+
navigation: "standard",
|
|
129
|
+
// Some nesting allowed
|
|
130
|
+
icons: "tooltips",
|
|
131
|
+
// Show text on hover
|
|
132
|
+
animations: "standard",
|
|
133
|
+
guidance: "contextual",
|
|
134
|
+
// Show help when hovering
|
|
135
|
+
inputMode: "mixed",
|
|
136
|
+
// Touch + keyboard
|
|
137
|
+
errorHandling: "inline",
|
|
138
|
+
// Inline error messages
|
|
139
|
+
confirmations: "smart",
|
|
140
|
+
// Confirm destructive only
|
|
141
|
+
shortcuts: "discoverable"
|
|
142
|
+
// Show shortcuts in tooltips
|
|
143
|
+
},
|
|
144
|
+
advanced: {
|
|
145
|
+
buttonSize: "compact",
|
|
146
|
+
fontSize: "small",
|
|
147
|
+
// 14px
|
|
148
|
+
navigation: "full",
|
|
149
|
+
// All features accessible
|
|
150
|
+
icons: "minimal",
|
|
151
|
+
// Icons only, no labels
|
|
152
|
+
animations: "full",
|
|
153
|
+
guidance: "on-demand",
|
|
154
|
+
// Help only when requested
|
|
155
|
+
inputMode: "keyboard",
|
|
156
|
+
// Keyboard-first
|
|
157
|
+
errorHandling: "technical",
|
|
158
|
+
// Technical error details
|
|
159
|
+
confirmations: "minimal",
|
|
160
|
+
// Skip most confirmations
|
|
161
|
+
shortcuts: "prominent"
|
|
162
|
+
// Show all shortcuts
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
function average(arr) {
|
|
166
|
+
return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
167
|
+
}
|
|
168
|
+
function sum(arr) {
|
|
169
|
+
return arr.reduce((a, b) => a + b, 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/contrast.ts
|
|
173
|
+
function hexToRgb(hex) {
|
|
174
|
+
const h = hex.replace("#", "");
|
|
175
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
176
|
+
return [
|
|
177
|
+
parseInt(full.slice(0, 2), 16),
|
|
178
|
+
parseInt(full.slice(2, 4), 16),
|
|
179
|
+
parseInt(full.slice(4, 6), 16)
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
function srgbToLinear(channel) {
|
|
183
|
+
const s = channel / 255;
|
|
184
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
185
|
+
}
|
|
186
|
+
function relativeLuminance(hex) {
|
|
187
|
+
const [r, g, b] = hexToRgb(hex);
|
|
188
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
189
|
+
}
|
|
190
|
+
function contrastRatio(fg, bg) {
|
|
191
|
+
const l1 = relativeLuminance(fg);
|
|
192
|
+
const l2 = relativeLuminance(bg);
|
|
193
|
+
const lighter = Math.max(l1, l2);
|
|
194
|
+
const darker = Math.min(l1, l2);
|
|
195
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
196
|
+
}
|
|
197
|
+
function meetsWCAG(fg, bg, level = "AA", large = false) {
|
|
198
|
+
const ratio = contrastRatio(fg, bg);
|
|
199
|
+
if (level === "AAA") return large ? ratio >= 4.5 : ratio >= 7;
|
|
200
|
+
return large ? ratio >= 3 : ratio >= 4.5;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/use-reduced-motion.ts
|
|
204
|
+
import { useState, useEffect } from "react";
|
|
205
|
+
var QUERY = "(prefers-reduced-motion: reduce)";
|
|
206
|
+
function useReducedMotion() {
|
|
207
|
+
const [reduced, setReduced] = useState(() => {
|
|
208
|
+
if (typeof window === "undefined") return false;
|
|
209
|
+
return window.matchMedia(QUERY).matches;
|
|
210
|
+
});
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const mql = window.matchMedia(QUERY);
|
|
213
|
+
const handler = (e) => setReduced(e.matches);
|
|
214
|
+
mql.addEventListener("change", handler);
|
|
215
|
+
return () => mql.removeEventListener("change", handler);
|
|
216
|
+
}, []);
|
|
217
|
+
return reduced;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/use-focus-trap.ts
|
|
221
|
+
import { useRef, useEffect as useEffect2 } from "react";
|
|
222
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
223
|
+
function useFocusTrap(active) {
|
|
224
|
+
const ref = useRef(null);
|
|
225
|
+
useEffect2(() => {
|
|
226
|
+
if (!active || !ref.current) return;
|
|
227
|
+
const container = ref.current;
|
|
228
|
+
const previouslyFocused = document.activeElement;
|
|
229
|
+
const first = container.querySelector(FOCUSABLE);
|
|
230
|
+
first?.focus();
|
|
231
|
+
function handleKeyDown(e) {
|
|
232
|
+
if (e.key !== "Tab") return;
|
|
233
|
+
const focusable = container.querySelectorAll(FOCUSABLE);
|
|
234
|
+
if (focusable.length === 0) return;
|
|
235
|
+
const firstEl = focusable[0];
|
|
236
|
+
const lastEl = focusable[focusable.length - 1];
|
|
237
|
+
if (e.shiftKey && document.activeElement === firstEl) {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
lastEl.focus();
|
|
240
|
+
} else if (!e.shiftKey && document.activeElement === lastEl) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
firstEl.focus();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
246
|
+
return () => {
|
|
247
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
248
|
+
previouslyFocused?.focus();
|
|
249
|
+
};
|
|
250
|
+
}, [active]);
|
|
251
|
+
return ref;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/use-announce.ts
|
|
255
|
+
import { useRef as useRef2, useCallback } from "react";
|
|
256
|
+
function useAnnounce(politeness = "polite") {
|
|
257
|
+
const ref = useRef2(null);
|
|
258
|
+
const announce = useCallback((message) => {
|
|
259
|
+
if (!ref.current) return;
|
|
260
|
+
ref.current.textContent = "";
|
|
261
|
+
requestAnimationFrame(() => {
|
|
262
|
+
if (ref.current) ref.current.textContent = message;
|
|
263
|
+
});
|
|
264
|
+
}, []);
|
|
265
|
+
const liveRegionProps = {
|
|
266
|
+
ref,
|
|
267
|
+
role: "status",
|
|
268
|
+
"aria-live": politeness,
|
|
269
|
+
"aria-atomic": true,
|
|
270
|
+
style: {
|
|
271
|
+
position: "absolute",
|
|
272
|
+
width: 1,
|
|
273
|
+
height: 1,
|
|
274
|
+
padding: 0,
|
|
275
|
+
margin: -1,
|
|
276
|
+
overflow: "hidden",
|
|
277
|
+
clip: "rect(0, 0, 0, 0)",
|
|
278
|
+
whiteSpace: "nowrap",
|
|
279
|
+
border: 0
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
return { announce, liveRegionProps };
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
LITERACY_UI_CONFIGS,
|
|
286
|
+
TechLiteracyDetector,
|
|
287
|
+
contrastRatio,
|
|
288
|
+
detectTechLiteracy,
|
|
289
|
+
meetsWCAG,
|
|
290
|
+
relativeLuminance,
|
|
291
|
+
useAnnounce,
|
|
292
|
+
useFocusTrap,
|
|
293
|
+
useReducedMotion
|
|
294
|
+
};
|