@sc4rfurryx/proteusjs 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.
- package/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,444 @@
|
|
1
|
+
/**
|
2
|
+
* ScreenReaderSupport - Enhanced screen reader compatibility
|
3
|
+
* Provides intelligent announcements, live regions, and ARIA management
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface AnnouncementConfig {
|
7
|
+
priority: 'polite' | 'assertive' | 'off';
|
8
|
+
delay?: number;
|
9
|
+
clear?: boolean;
|
10
|
+
interrupt?: boolean;
|
11
|
+
}
|
12
|
+
|
13
|
+
export interface LiveRegionConfig {
|
14
|
+
type: 'status' | 'alert' | 'log' | 'marquee' | 'timer';
|
15
|
+
atomic: boolean;
|
16
|
+
relevant: 'additions' | 'removals' | 'text' | 'all';
|
17
|
+
busy: boolean;
|
18
|
+
}
|
19
|
+
|
20
|
+
export interface AriaLabelConfig {
|
21
|
+
label?: string;
|
22
|
+
labelledBy?: string;
|
23
|
+
describedBy?: string;
|
24
|
+
role?: string;
|
25
|
+
expanded?: boolean;
|
26
|
+
selected?: boolean;
|
27
|
+
checked?: boolean;
|
28
|
+
disabled?: boolean;
|
29
|
+
hidden?: boolean;
|
30
|
+
live?: 'polite' | 'assertive' | 'off';
|
31
|
+
}
|
32
|
+
|
33
|
+
export class ScreenReaderSupport {
|
34
|
+
private liveRegions: Map<string, HTMLElement> = new Map();
|
35
|
+
private announcementQueue: Array<{ message: string; config: AnnouncementConfig }> = [];
|
36
|
+
private isProcessingQueue: boolean = false;
|
37
|
+
private screenReaderDetected: boolean = false;
|
38
|
+
private lastAnnouncement: string = '';
|
39
|
+
private lastAnnouncementTime: number = 0;
|
40
|
+
|
41
|
+
constructor() {
|
42
|
+
this.detectScreenReader();
|
43
|
+
this.setupDefaultLiveRegions();
|
44
|
+
this.setupKeyboardDetection();
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Announce message to screen readers
|
49
|
+
*/
|
50
|
+
public announce(message: string, config: AnnouncementConfig = { priority: 'polite' }): void {
|
51
|
+
if (!message.trim()) return;
|
52
|
+
|
53
|
+
// Avoid duplicate announcements within 1 second
|
54
|
+
const now = Date.now();
|
55
|
+
if (this.lastAnnouncement === message && now - this.lastAnnouncementTime < 1000) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
this.lastAnnouncement = message;
|
60
|
+
this.lastAnnouncementTime = now;
|
61
|
+
|
62
|
+
if (config.interrupt) {
|
63
|
+
this.clearQueue();
|
64
|
+
}
|
65
|
+
|
66
|
+
this.announcementQueue.push({ message, config });
|
67
|
+
this.processQueue();
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* Create or update live region
|
72
|
+
*/
|
73
|
+
public createLiveRegion(id: string, config: LiveRegionConfig): HTMLElement {
|
74
|
+
let region = this.liveRegions.get(id);
|
75
|
+
|
76
|
+
if (!region) {
|
77
|
+
region = document.createElement('div');
|
78
|
+
region.id = `proteus-live-${id}`;
|
79
|
+
region.className = 'proteus-sr-only';
|
80
|
+
this.applyScreenReaderOnlyStyles(region);
|
81
|
+
document.body.appendChild(region);
|
82
|
+
this.liveRegions.set(id, region);
|
83
|
+
}
|
84
|
+
|
85
|
+
// Update ARIA attributes
|
86
|
+
region.setAttribute('aria-live', this.mapLiveRegionType(config.type));
|
87
|
+
region.setAttribute('aria-atomic', config.atomic.toString());
|
88
|
+
region.setAttribute('aria-relevant', config.relevant);
|
89
|
+
region.setAttribute('aria-busy', config.busy.toString());
|
90
|
+
region.setAttribute('role', config.type === 'alert' ? 'alert' : 'status');
|
91
|
+
|
92
|
+
return region;
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Update live region content
|
97
|
+
*/
|
98
|
+
public updateLiveRegion(id: string, content: string): void {
|
99
|
+
const region = this.liveRegions.get(id);
|
100
|
+
if (region) {
|
101
|
+
region.textContent = content;
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Apply comprehensive ARIA labels to element
|
107
|
+
*/
|
108
|
+
public applyAriaLabels(element: Element, config: AriaLabelConfig): void {
|
109
|
+
const htmlElement = element as HTMLElement;
|
110
|
+
|
111
|
+
// Label attributes
|
112
|
+
if (config.label) {
|
113
|
+
htmlElement.setAttribute('aria-label', config.label);
|
114
|
+
}
|
115
|
+
if (config.labelledBy) {
|
116
|
+
htmlElement.setAttribute('aria-labelledby', config.labelledBy);
|
117
|
+
}
|
118
|
+
if (config.describedBy) {
|
119
|
+
htmlElement.setAttribute('aria-describedby', config.describedBy);
|
120
|
+
}
|
121
|
+
|
122
|
+
// Role
|
123
|
+
if (config.role) {
|
124
|
+
htmlElement.setAttribute('role', config.role);
|
125
|
+
}
|
126
|
+
|
127
|
+
// State attributes
|
128
|
+
if (config.expanded !== undefined) {
|
129
|
+
htmlElement.setAttribute('aria-expanded', config.expanded.toString());
|
130
|
+
}
|
131
|
+
if (config.selected !== undefined) {
|
132
|
+
htmlElement.setAttribute('aria-selected', config.selected.toString());
|
133
|
+
}
|
134
|
+
if (config.checked !== undefined) {
|
135
|
+
htmlElement.setAttribute('aria-checked', config.checked.toString());
|
136
|
+
}
|
137
|
+
if (config.disabled !== undefined) {
|
138
|
+
htmlElement.setAttribute('aria-disabled', config.disabled.toString());
|
139
|
+
}
|
140
|
+
if (config.hidden !== undefined) {
|
141
|
+
htmlElement.setAttribute('aria-hidden', config.hidden.toString());
|
142
|
+
}
|
143
|
+
if (config.live) {
|
144
|
+
htmlElement.setAttribute('aria-live', config.live);
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Announce container changes for responsive layouts
|
150
|
+
*/
|
151
|
+
public announceContainerChange(element: Element, breakpoint: string, width: number): void {
|
152
|
+
const elementName = this.getElementDescription(element);
|
153
|
+
const message = `${elementName} layout changed to ${breakpoint} breakpoint at ${width} pixels wide`;
|
154
|
+
|
155
|
+
this.announce(message, { priority: 'polite', delay: 500 });
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Announce typography changes
|
160
|
+
*/
|
161
|
+
public announceTypographyChange(element: Element, fontSize: string): void {
|
162
|
+
const elementName = this.getElementDescription(element);
|
163
|
+
const message = `${elementName} text size adjusted to ${fontSize}`;
|
164
|
+
|
165
|
+
this.announce(message, { priority: 'polite', delay: 300 });
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Create skip links for navigation
|
170
|
+
*/
|
171
|
+
public createSkipLinks(targets: Array<{ id: string; label: string }>): HTMLElement {
|
172
|
+
const skipContainer = document.createElement('div');
|
173
|
+
skipContainer.className = 'proteus-skip-links';
|
174
|
+
skipContainer.setAttribute('role', 'navigation');
|
175
|
+
skipContainer.setAttribute('aria-label', 'Skip links');
|
176
|
+
|
177
|
+
targets.forEach(target => {
|
178
|
+
const link = document.createElement('a');
|
179
|
+
link.href = `#${target.id}`;
|
180
|
+
link.textContent = target.label;
|
181
|
+
link.className = 'proteus-skip-link';
|
182
|
+
|
183
|
+
// Style skip link
|
184
|
+
this.applySkipLinkStyles(link);
|
185
|
+
|
186
|
+
skipContainer.appendChild(link);
|
187
|
+
});
|
188
|
+
|
189
|
+
// Insert at beginning of body
|
190
|
+
document.body.insertBefore(skipContainer, document.body.firstChild);
|
191
|
+
|
192
|
+
return skipContainer;
|
193
|
+
}
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Manage focus for dynamic content
|
197
|
+
*/
|
198
|
+
public manageFocus(element: Element, reason: string): void {
|
199
|
+
const htmlElement = element as HTMLElement;
|
200
|
+
|
201
|
+
// Ensure element is focusable
|
202
|
+
if (!htmlElement.hasAttribute('tabindex')) {
|
203
|
+
htmlElement.setAttribute('tabindex', '-1');
|
204
|
+
}
|
205
|
+
|
206
|
+
// Focus the element
|
207
|
+
htmlElement.focus();
|
208
|
+
|
209
|
+
// Announce focus change
|
210
|
+
const elementName = this.getElementDescription(element);
|
211
|
+
this.announce(`Focus moved to ${elementName}. ${reason}`, { priority: 'assertive' });
|
212
|
+
}
|
213
|
+
|
214
|
+
/**
|
215
|
+
* Check if screen reader is active
|
216
|
+
*/
|
217
|
+
public isScreenReaderActive(): boolean {
|
218
|
+
return this.screenReaderDetected;
|
219
|
+
}
|
220
|
+
|
221
|
+
/**
|
222
|
+
* Clean up resources
|
223
|
+
*/
|
224
|
+
public destroy(): void {
|
225
|
+
// Remove live regions
|
226
|
+
this.liveRegions.forEach(region => {
|
227
|
+
if (region.parentNode) {
|
228
|
+
region.parentNode.removeChild(region);
|
229
|
+
}
|
230
|
+
});
|
231
|
+
this.liveRegions.clear();
|
232
|
+
|
233
|
+
// Clear queue
|
234
|
+
this.clearQueue();
|
235
|
+
}
|
236
|
+
|
237
|
+
/**
|
238
|
+
* Detect screen reader presence
|
239
|
+
*/
|
240
|
+
private detectScreenReader(): void {
|
241
|
+
// Check for common screen reader indicators
|
242
|
+
const indicators = [
|
243
|
+
() => navigator.userAgent.includes('NVDA'),
|
244
|
+
() => navigator.userAgent.includes('JAWS'),
|
245
|
+
() => navigator.userAgent.includes('VoiceOver'),
|
246
|
+
() => navigator.userAgent.includes('Orca'),
|
247
|
+
() => window.speechSynthesis && window.speechSynthesis.getVoices().length > 0,
|
248
|
+
() => 'speechSynthesis' in window,
|
249
|
+
() => document.querySelector('[aria-live]') !== null
|
250
|
+
];
|
251
|
+
|
252
|
+
this.screenReaderDetected = indicators.some(check => {
|
253
|
+
try {
|
254
|
+
return check();
|
255
|
+
} catch {
|
256
|
+
return false;
|
257
|
+
}
|
258
|
+
});
|
259
|
+
|
260
|
+
// Also check for keyboard-only navigation
|
261
|
+
this.setupKeyboardDetection();
|
262
|
+
}
|
263
|
+
|
264
|
+
/**
|
265
|
+
* Setup keyboard navigation detection
|
266
|
+
*/
|
267
|
+
private setupKeyboardDetection(): void {
|
268
|
+
let keyboardUser = false;
|
269
|
+
|
270
|
+
document.addEventListener('keydown', (e) => {
|
271
|
+
if (e.key === 'Tab') {
|
272
|
+
keyboardUser = true;
|
273
|
+
document.body.classList.add('proteus-keyboard-user');
|
274
|
+
}
|
275
|
+
});
|
276
|
+
|
277
|
+
document.addEventListener('mousedown', () => {
|
278
|
+
if (keyboardUser) {
|
279
|
+
keyboardUser = false;
|
280
|
+
document.body.classList.remove('proteus-keyboard-user');
|
281
|
+
}
|
282
|
+
});
|
283
|
+
}
|
284
|
+
|
285
|
+
/**
|
286
|
+
* Setup default live regions
|
287
|
+
*/
|
288
|
+
private setupDefaultLiveRegions(): void {
|
289
|
+
// Status region for general announcements
|
290
|
+
this.createLiveRegion('status', {
|
291
|
+
type: 'status',
|
292
|
+
atomic: false,
|
293
|
+
relevant: 'additions',
|
294
|
+
busy: false
|
295
|
+
});
|
296
|
+
|
297
|
+
// Alert region for important announcements
|
298
|
+
this.createLiveRegion('alert', {
|
299
|
+
type: 'alert',
|
300
|
+
atomic: true,
|
301
|
+
relevant: 'all',
|
302
|
+
busy: false
|
303
|
+
});
|
304
|
+
}
|
305
|
+
|
306
|
+
/**
|
307
|
+
* Process announcement queue
|
308
|
+
*/
|
309
|
+
private async processQueue(): Promise<void> {
|
310
|
+
if (this.isProcessingQueue || this.announcementQueue.length === 0) {
|
311
|
+
return;
|
312
|
+
}
|
313
|
+
|
314
|
+
this.isProcessingQueue = true;
|
315
|
+
|
316
|
+
while (this.announcementQueue.length > 0) {
|
317
|
+
const { message, config } = this.announcementQueue.shift()!;
|
318
|
+
|
319
|
+
if (config.clear) {
|
320
|
+
this.clearLiveRegions();
|
321
|
+
}
|
322
|
+
|
323
|
+
const regionId = config.priority === 'assertive' ? 'alert' : 'status';
|
324
|
+
this.updateLiveRegion(regionId, message);
|
325
|
+
|
326
|
+
if (config.delay) {
|
327
|
+
await this.delay(config.delay);
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
this.isProcessingQueue = false;
|
332
|
+
}
|
333
|
+
|
334
|
+
/**
|
335
|
+
* Clear announcement queue
|
336
|
+
*/
|
337
|
+
private clearQueue(): void {
|
338
|
+
this.announcementQueue = [];
|
339
|
+
this.isProcessingQueue = false;
|
340
|
+
}
|
341
|
+
|
342
|
+
/**
|
343
|
+
* Clear all live regions
|
344
|
+
*/
|
345
|
+
private clearLiveRegions(): void {
|
346
|
+
this.liveRegions.forEach(region => {
|
347
|
+
region.textContent = '';
|
348
|
+
});
|
349
|
+
}
|
350
|
+
|
351
|
+
/**
|
352
|
+
* Map live region type to aria-live value
|
353
|
+
*/
|
354
|
+
private mapLiveRegionType(type: LiveRegionConfig['type']): 'polite' | 'assertive' | 'off' {
|
355
|
+
switch (type) {
|
356
|
+
case 'alert':
|
357
|
+
return 'assertive';
|
358
|
+
case 'status':
|
359
|
+
case 'log':
|
360
|
+
case 'marquee':
|
361
|
+
case 'timer':
|
362
|
+
return 'polite';
|
363
|
+
default:
|
364
|
+
return 'polite';
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
368
|
+
/**
|
369
|
+
* Get descriptive name for element
|
370
|
+
*/
|
371
|
+
private getElementDescription(element: Element): string {
|
372
|
+
// Try aria-label first
|
373
|
+
const ariaLabel = element.getAttribute('aria-label');
|
374
|
+
if (ariaLabel) return ariaLabel;
|
375
|
+
|
376
|
+
// Try aria-labelledby
|
377
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
378
|
+
if (labelledBy) {
|
379
|
+
const labelElement = document.getElementById(labelledBy);
|
380
|
+
if (labelElement) return labelElement.textContent || 'element';
|
381
|
+
}
|
382
|
+
|
383
|
+
// Try text content
|
384
|
+
const textContent = element.textContent?.trim();
|
385
|
+
if (textContent && textContent.length < 50) return textContent;
|
386
|
+
|
387
|
+
// Try tag name and class
|
388
|
+
const tagName = element.tagName.toLowerCase();
|
389
|
+
const className = element.className ? ` with class ${element.className}` : '';
|
390
|
+
|
391
|
+
return `${tagName}${className}`;
|
392
|
+
}
|
393
|
+
|
394
|
+
/**
|
395
|
+
* Apply screen reader only styles
|
396
|
+
*/
|
397
|
+
private applyScreenReaderOnlyStyles(element: HTMLElement): void {
|
398
|
+
element.style.cssText = `
|
399
|
+
position: absolute !important;
|
400
|
+
width: 1px !important;
|
401
|
+
height: 1px !important;
|
402
|
+
padding: 0 !important;
|
403
|
+
margin: -1px !important;
|
404
|
+
overflow: hidden !important;
|
405
|
+
clip: rect(0, 0, 0, 0) !important;
|
406
|
+
white-space: nowrap !important;
|
407
|
+
border: 0 !important;
|
408
|
+
`;
|
409
|
+
}
|
410
|
+
|
411
|
+
/**
|
412
|
+
* Apply skip link styles
|
413
|
+
*/
|
414
|
+
private applySkipLinkStyles(element: HTMLElement): void {
|
415
|
+
element.style.cssText = `
|
416
|
+
position: absolute;
|
417
|
+
top: -40px;
|
418
|
+
left: 6px;
|
419
|
+
background: #000;
|
420
|
+
color: #fff;
|
421
|
+
padding: 8px;
|
422
|
+
text-decoration: none;
|
423
|
+
border-radius: 4px;
|
424
|
+
z-index: 10000;
|
425
|
+
transition: top 0.3s;
|
426
|
+
`;
|
427
|
+
|
428
|
+
// Show on focus
|
429
|
+
element.addEventListener('focus', () => {
|
430
|
+
element.style.top = '6px';
|
431
|
+
});
|
432
|
+
|
433
|
+
element.addEventListener('blur', () => {
|
434
|
+
element.style.top = '-40px';
|
435
|
+
});
|
436
|
+
}
|
437
|
+
|
438
|
+
/**
|
439
|
+
* Utility delay function
|
440
|
+
*/
|
441
|
+
private delay(ms: number): Promise<void> {
|
442
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
443
|
+
}
|
444
|
+
}
|