@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/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
+ };