@sc4rfurryx/proteusjs 1.1.0 → 1.1.1

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.
Files changed (40) hide show
  1. package/README.md +5 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/react.d.ts +1 -0
  4. package/dist/adapters/react.esm.js +2 -1
  5. package/dist/adapters/react.esm.js.map +1 -1
  6. package/dist/adapters/svelte.esm.js +2 -1
  7. package/dist/adapters/svelte.esm.js.map +1 -1
  8. package/dist/adapters/vue.esm.js +2 -1
  9. package/dist/adapters/vue.esm.js.map +1 -1
  10. package/dist/modules/a11y-audit.d.ts +1 -9
  11. package/dist/modules/a11y-audit.esm.js +30 -475
  12. package/dist/modules/a11y-audit.esm.js.map +1 -1
  13. package/dist/modules/a11y-primitives.d.ts +8 -41
  14. package/dist/modules/a11y-primitives.esm.js +69 -400
  15. package/dist/modules/a11y-primitives.esm.js.map +1 -1
  16. package/dist/modules/anchor.d.ts +1 -0
  17. package/dist/modules/anchor.esm.js +2 -1
  18. package/dist/modules/anchor.esm.js.map +1 -1
  19. package/dist/modules/container.esm.js +1 -1
  20. package/dist/modules/perf.esm.js +1 -1
  21. package/dist/modules/popover.esm.js +1 -1
  22. package/dist/modules/scroll.esm.js +1 -1
  23. package/dist/modules/transitions.esm.js +1 -1
  24. package/dist/modules/typography.esm.js +1 -1
  25. package/dist/proteus.cjs.js +97 -875
  26. package/dist/proteus.cjs.js.map +1 -1
  27. package/dist/proteus.d.ts +11 -56
  28. package/dist/proteus.esm.js +97 -875
  29. package/dist/proteus.esm.js.map +1 -1
  30. package/dist/proteus.esm.min.js +2 -2
  31. package/dist/proteus.esm.min.js.map +1 -1
  32. package/dist/proteus.js +97 -875
  33. package/dist/proteus.js.map +1 -1
  34. package/dist/proteus.min.js +2 -2
  35. package/dist/proteus.min.js.map +1 -1
  36. package/package.json +9 -4
  37. package/src/index.ts +1 -1
  38. package/src/modules/a11y-audit/index.ts +29 -553
  39. package/src/modules/a11y-primitives/index.ts +73 -475
  40. package/src/modules/anchor/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sc4rfurryx/proteusjs",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.",
6
6
  "main": "dist/proteus.js",
@@ -89,8 +89,13 @@
89
89
  "benchmark": "vitest run tests/benchmarks/performance-benchmark.ts",
90
90
  "accessibility": "vitest run tests/validation/accessibility-validation.test.ts",
91
91
  "docs:build": "typedoc src/index.ts",
92
- "prepublishOnly": "npm run validate && npm run build:prod",
93
- "release": "npm run validate && npm run build:prod && npm publish"
92
+ "prepublishOnly": "npm run build:prod",
93
+ "release": "npm run validate && npm run build:prod && npm publish",
94
+ "release:patch": "node scripts/release.js patch",
95
+ "release:minor": "node scripts/release.js minor",
96
+ "release:major": "node scripts/release.js major",
97
+ "release:dry": "node scripts/release.js patch --dry-run",
98
+ "publish:dev": "node scripts/publish-dev.js"
94
99
  },
95
100
  "keywords": [
96
101
  "responsive",
@@ -152,4 +157,4 @@
152
157
  "dependencies": {
153
158
  "tslib": "^2.8.1"
154
159
  }
155
- }
160
+ }
package/src/index.ts CHANGED
@@ -47,7 +47,7 @@ export type { PopoverOptions, PopoverController } from './modules/popover';
47
47
  export type { ContainerOptions } from './modules/container';
48
48
  export type { FluidTypeOptions, FluidTypeResult } from './modules/typography';
49
49
  export type { AuditOptions, AuditReport, AuditViolation } from './modules/a11y-audit';
50
- export type { Controller, DialogOptions, TooltipOptions, ComboboxOptions, ListboxOptions, FocusTrapController } from './modules/a11y-primitives';
50
+ export type { Controller, DialogOptions, TooltipOptions, FocusTrapController } from './modules/a11y-primitives';
51
51
  export type { SpeculationOptions, ContentVisibilityOptions } from './modules/perf';
52
52
 
53
53
  // Utility exports
@@ -1,6 +1,6 @@
1
- /**
1
+ īģŋ/**
2
2
  * @sc4rfurryx/proteusjs/a11y-audit
3
- * Accessibility audits for development (dev-only)
3
+ * Lightweight accessibility audits for development
4
4
  *
5
5
  * @version 1.1.0
6
6
  * @author sc4rfurry
@@ -10,7 +10,6 @@
10
10
  export interface AuditOptions {
11
11
  rules?: string[];
12
12
  format?: 'console' | 'json';
13
- openInBrowser?: boolean;
14
13
  }
15
14
 
16
15
  export interface AuditViolation {
@@ -18,591 +17,68 @@ export interface AuditViolation {
18
17
  impact: 'minor' | 'moderate' | 'serious' | 'critical';
19
18
  nodes: number;
20
19
  help: string;
21
- fix: string;
22
- elements?: Element[];
23
20
  }
24
21
 
25
22
  export interface AuditReport {
26
23
  violations: AuditViolation[];
27
24
  passes: number;
28
- incomplete: number;
29
25
  timestamp: number;
30
26
  url: string;
31
27
  }
32
28
 
33
- /**
34
- * Run accessibility audits with actionable output
35
- * DEV-ONLY: This module should be tree-shaken in production
36
- */
37
29
  export async function audit(
38
30
  target: Document | Element = document,
39
31
  options: AuditOptions = {}
40
32
  ): Promise<AuditReport> {
41
- // Ensure this only runs in development
42
- if (process.env['NODE_ENV'] === 'production') {
43
- console.warn('a11y-audit should not be used in production');
44
- return {
45
- violations: [],
46
- passes: 0,
47
- incomplete: 0,
48
- timestamp: Date.now(),
49
- url: window.location.href
50
- };
33
+ if (typeof window === 'undefined' || process.env['NODE_ENV'] === 'production') {
34
+ return { violations: [], passes: 0, timestamp: Date.now(), url: '' };
51
35
  }
52
36
 
53
- const {
54
- rules = ['color-contrast', 'heading-order', 'image-alt', 'label', 'link-name', 'button-name'],
55
- format = 'console',
56
- openInBrowser = false
57
- } = options;
58
-
37
+ const { rules = ['images', 'headings', 'forms'], format = 'console' } = options;
59
38
  const violations: AuditViolation[] = [];
60
39
  let passes = 0;
61
- let incomplete = 0;
62
-
63
- // Basic accessibility checks
64
- const checks = {
65
- 'color-contrast': checkColorContrast,
66
- 'heading-order': checkHeadingOrder,
67
- 'image-alt': checkImageAlt,
68
- 'label': checkFormLabels,
69
- 'link-name': checkLinkNames,
70
- 'button-name': checkButtonNames,
71
- 'focus-visible': checkFocusVisible,
72
- 'aria-labels': checkAriaLabels,
73
- 'landmark-roles': checkLandmarkRoles,
74
- 'skip-links': checkSkipLinks
75
- };
76
-
77
- // Run selected checks
78
- for (const ruleId of rules) {
79
- if (checks[ruleId as keyof typeof checks]) {
80
- try {
81
- const result = await checks[ruleId as keyof typeof checks](target);
82
- if (result.violations.length > 0) {
83
- violations.push(...result.violations);
84
- } else {
85
- passes++;
86
- }
87
- } catch (error) {
88
- incomplete++;
89
- console.warn(`Failed to run accessibility check: ${ruleId}`, error);
90
- }
91
- }
92
- }
93
-
94
- const report: AuditReport = {
95
- violations,
96
- passes,
97
- incomplete,
98
- timestamp: Date.now(),
99
- url: window.location.href
100
- };
101
-
102
- // Output results
103
- if (format === 'console') {
104
- outputToConsole(report);
105
- }
106
-
107
- if (openInBrowser) {
108
- openReportInBrowser(report);
109
- }
110
-
111
- return report;
112
- }
113
-
114
- // Individual check functions
115
- async function checkColorContrast(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
116
- const violations: AuditViolation[] = [];
117
- const elements = target.querySelectorAll('*');
118
-
119
- elements.forEach(element => {
120
- const style = getComputedStyle(element);
121
- const color = style.color;
122
- const backgroundColor = style.backgroundColor;
123
-
124
- // Simple contrast check (would need more sophisticated implementation)
125
- if (color && backgroundColor && color !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'rgba(0, 0, 0, 0)') {
126
- const contrast = calculateContrast(color, backgroundColor);
127
- if (contrast < 4.5) {
128
- violations.push({
129
- id: 'color-contrast',
130
- impact: 'serious',
131
- nodes: 1,
132
- help: 'Elements must have sufficient color contrast',
133
- fix: `Increase contrast ratio to at least 4.5:1. Current: ${contrast.toFixed(2)}:1`,
134
- elements: [element]
135
- });
136
- }
137
- }
138
- });
139
-
140
- return { violations };
141
- }
142
40
 
143
- async function checkHeadingOrder(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
144
- const violations: AuditViolation[] = [];
145
- const headings = target.querySelectorAll('h1, h2, h3, h4, h5, h6');
146
-
147
- let lastLevel = 0;
148
- headings.forEach(heading => {
149
- const level = parseInt(heading.tagName.charAt(1));
150
- if (level > lastLevel + 1) {
41
+ if (rules.includes('images')) {
42
+ const imgs = target.querySelectorAll('img:not([alt])');
43
+ if (imgs.length > 0) {
151
44
  violations.push({
152
- id: 'heading-order',
153
- impact: 'moderate',
154
- nodes: 1,
155
- help: 'Heading levels should only increase by one',
156
- fix: `Change ${heading.tagName} to H${lastLevel + 1} or add intermediate headings`,
157
- elements: [heading]
45
+ id: 'image-alt', impact: 'critical', nodes: imgs.length, help: 'Images need alt text'
158
46
  });
159
47
  }
160
- lastLevel = level;
161
- });
162
-
163
- return { violations };
164
- }
165
-
166
- async function checkImageAlt(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
167
- const violations: AuditViolation[] = [];
168
- const images = target.querySelectorAll('img');
169
-
170
- images.forEach(img => {
171
- if (!img.hasAttribute('alt')) {
172
- violations.push({
173
- id: 'image-alt',
174
- impact: 'critical',
175
- nodes: 1,
176
- help: 'Images must have alternative text',
177
- fix: 'Add alt attribute with descriptive text or alt="" for decorative images',
178
- elements: [img]
179
- });
180
- }
181
- });
182
-
183
- return { violations };
184
- }
185
-
186
- async function checkFormLabels(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
187
- const violations: AuditViolation[] = [];
188
- const inputs = target.querySelectorAll('input, select, textarea');
189
-
190
- inputs.forEach(input => {
191
- const hasLabel = input.hasAttribute('aria-label') ||
192
- input.hasAttribute('aria-labelledby') ||
193
- target.querySelector(`label[for="${input.id}"]`) ||
194
- input.closest('label');
195
-
196
- if (!hasLabel) {
197
- violations.push({
198
- id: 'label',
199
- impact: 'critical',
200
- nodes: 1,
201
- help: 'Form elements must have labels',
202
- fix: 'Add a label element, aria-label, or aria-labelledby attribute',
203
- elements: [input]
204
- });
205
- }
206
- });
207
-
208
- return { violations };
209
- }
210
-
211
- async function checkLinkNames(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
212
- const violations: AuditViolation[] = [];
213
- const links = target.querySelectorAll('a[href]');
214
-
215
- links.forEach(link => {
216
- const text = link.textContent?.trim();
217
- const ariaLabel = link.getAttribute('aria-label');
218
-
219
- if (!text && !ariaLabel) {
220
- violations.push({
221
- id: 'link-name',
222
- impact: 'serious',
223
- nodes: 1,
224
- help: 'Links must have discernible text',
225
- fix: 'Add descriptive text content or aria-label attribute',
226
- elements: [link]
227
- });
228
- }
229
- });
230
-
231
- return { violations };
232
- }
233
-
234
- async function checkButtonNames(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
235
- const violations: AuditViolation[] = [];
236
- const buttons = target.querySelectorAll('button, input[type="button"], input[type="submit"]');
237
-
238
- buttons.forEach(button => {
239
- const text = button.textContent?.trim();
240
- const ariaLabel = button.getAttribute('aria-label');
241
- const value = button.getAttribute('value');
242
-
243
- if (!text && !ariaLabel && !value) {
244
- violations.push({
245
- id: 'button-name',
246
- impact: 'serious',
247
- nodes: 1,
248
- help: 'Buttons must have discernible text',
249
- fix: 'Add text content, aria-label, or value attribute',
250
- elements: [button]
251
- });
252
- }
253
- });
254
-
255
- return { violations };
256
- }
257
-
258
- async function checkFocusVisible(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
259
- const violations: AuditViolation[] = [];
260
- const focusableElements = target.querySelectorAll(
261
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
262
- );
263
-
264
- focusableElements.forEach(element => {
265
- const styles = getComputedStyle(element as HTMLElement);
266
- const hasVisibleFocus = styles.outline !== 'none' && styles.outline !== '0px' &&
267
- styles.outline !== '0' && styles.outlineWidth !== '0px';
268
-
269
- // Check if element has custom focus styles
270
- const hasCustomFocus = styles.boxShadow.includes('inset') ||
271
- styles.border !== styles.borderColor ||
272
- element.hasAttribute('data-focus-visible');
273
-
274
- if (!hasVisibleFocus && !hasCustomFocus) {
275
- violations.push({
276
- id: 'focus-visible',
277
- impact: 'serious',
278
- nodes: 1,
279
- help: 'Interactive elements must have visible focus indicators',
280
- fix: 'Add outline, box-shadow, or other visible focus styles',
281
- elements: [element]
282
- });
283
- }
284
- });
285
-
286
- return { violations };
287
- }
288
-
289
- async function checkAriaLabels(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
290
- const violations: AuditViolation[] = [];
291
-
292
- // Check for aria-labelledby pointing to non-existent elements
293
- const elementsWithLabelledBy = target.querySelectorAll('[aria-labelledby]');
294
- elementsWithLabelledBy.forEach(element => {
295
- const labelledBy = element.getAttribute('aria-labelledby');
296
- if (labelledBy) {
297
- const labelIds = labelledBy.split(' ');
298
- const missingIds = labelIds.filter(id => !target.querySelector(`#${id}`));
299
-
300
- if (missingIds.length > 0) {
301
- violations.push({
302
- id: 'aria-labelledby-invalid',
303
- impact: 'serious',
304
- nodes: 1,
305
- help: 'aria-labelledby must reference existing elements',
306
- fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
307
- elements: [element]
308
- });
309
- }
310
- }
311
- });
312
-
313
- // Check for aria-describedby pointing to non-existent elements
314
- const elementsWithDescribedBy = target.querySelectorAll('[aria-describedby]');
315
- elementsWithDescribedBy.forEach(element => {
316
- const describedBy = element.getAttribute('aria-describedby');
317
- if (describedBy) {
318
- const descriptionIds = describedBy.split(' ');
319
- const missingIds = descriptionIds.filter(id => !target.querySelector(`#${id}`));
320
-
321
- if (missingIds.length > 0) {
322
- violations.push({
323
- id: 'aria-describedby-invalid',
324
- impact: 'moderate',
325
- nodes: 1,
326
- help: 'aria-describedby must reference existing elements',
327
- fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
328
- elements: [element]
329
- });
330
- }
331
- }
332
- });
333
-
334
- return { violations };
335
- }
336
-
337
- async function checkLandmarkRoles(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
338
- const violations: AuditViolation[] = [];
339
-
340
- // Check for missing main landmark
341
- const mainElements = target.querySelectorAll('main, [role="main"]');
342
- if (mainElements.length === 0) {
343
- violations.push({
344
- id: 'landmark-main-missing',
345
- impact: 'moderate',
346
- nodes: 0,
347
- help: 'Page should have a main landmark',
348
- fix: 'Add a <main> element or role="main" to identify the main content area'
349
- });
350
- } else if (mainElements.length > 1) {
351
- violations.push({
352
- id: 'landmark-main-multiple',
353
- impact: 'moderate',
354
- nodes: mainElements.length,
355
- help: 'Page should have only one main landmark',
356
- fix: 'Ensure only one main element or role="main" exists per page',
357
- elements: Array.from(mainElements)
358
- });
48
+ passes += target.querySelectorAll('img[alt]').length;
359
49
  }
360
50
 
361
- // Check for navigation landmarks without labels when multiple exist
362
- const navElements = target.querySelectorAll('nav, [role="navigation"]');
363
- if (navElements.length > 1) {
364
- navElements.forEach(nav => {
365
- const hasLabel = nav.hasAttribute('aria-label') ||
366
- nav.hasAttribute('aria-labelledby') ||
367
- nav.querySelector('h1, h2, h3, h4, h5, h6');
368
-
369
- if (!hasLabel) {
370
- violations.push({
371
- id: 'landmark-nav-unlabeled',
372
- impact: 'moderate',
373
- nodes: 1,
374
- help: 'Multiple navigation landmarks should be labeled',
375
- fix: 'Add aria-label or aria-labelledby to distinguish navigation areas',
376
- elements: [nav]
377
- });
378
- }
379
- });
380
- }
381
-
382
- return { violations };
383
- }
384
-
385
- async function checkSkipLinks(target: Document | Element): Promise<{ violations: AuditViolation[] }> {
386
- const violations: AuditViolation[] = [];
387
-
388
- // Check for skip links in documents with navigation
389
- const navElements = target.querySelectorAll('nav, [role="navigation"]');
390
- const mainElement = target.querySelector('main, [role="main"]');
391
-
392
- if (navElements.length > 0 && mainElement) {
393
- const skipLinks = target.querySelectorAll('a[href^="#"]');
394
- const hasSkipToMain = Array.from(skipLinks).some(link => {
395
- const href = link.getAttribute('href');
396
- return href && (
397
- href === '#main' ||
398
- href === `#${mainElement.id}` ||
399
- link.textContent?.toLowerCase().includes('skip to main') ||
400
- link.textContent?.toLowerCase().includes('skip to content')
401
- );
402
- });
403
-
404
- if (!hasSkipToMain) {
51
+ if (rules.includes('headings')) {
52
+ const h1s = target.querySelectorAll('h1');
53
+ if (h1s.length !== 1) {
405
54
  violations.push({
406
- id: 'skip-link-missing',
407
- impact: 'moderate',
408
- nodes: 0,
409
- help: 'Page with navigation should have skip links',
410
- fix: 'Add a skip link to the main content area for keyboard users'
55
+ id: 'heading-structure', impact: 'moderate', nodes: h1s.length, help: 'Page should have exactly one h1'
411
56
  });
412
- }
57
+ } else passes++;
413
58
  }
414
59
 
415
- // Check that skip links are properly positioned (should be first focusable element)
416
- const firstFocusable = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
417
- if (firstFocusable && firstFocusable.tagName === 'A') {
418
- const href = firstFocusable.getAttribute('href');
419
- if (!href?.startsWith('#')) {
60
+ if (rules.includes('forms')) {
61
+ const unlabeled = target.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
62
+ if (unlabeled.length > 0) {
420
63
  violations.push({
421
- id: 'skip-link-not-first',
422
- impact: 'minor',
423
- nodes: 1,
424
- help: 'Skip links should be the first focusable elements',
425
- fix: 'Move skip links to the beginning of the document',
426
- elements: [firstFocusable]
64
+ id: 'form-labels', impact: 'critical', nodes: unlabeled.length, help: 'Form inputs need labels'
427
65
  });
428
66
  }
67
+ passes += target.querySelectorAll('input[aria-label], input[aria-labelledby]').length;
429
68
  }
430
69
 
431
- return { violations };
432
- }
433
-
434
- // Utility functions
435
- function calculateContrast(color1: string, color2: string): number {
436
- // Convert colors to RGB values
437
- const rgb1 = parseColor(color1);
438
- const rgb2 = parseColor(color2);
439
-
440
- if (!rgb1 || !rgb2) return 4.5; // Fallback if parsing fails
441
-
442
- // Calculate relative luminance
443
- const l1 = getRelativeLuminance(rgb1);
444
- const l2 = getRelativeLuminance(rgb2);
445
-
446
- // Calculate contrast ratio
447
- const lighter = Math.max(l1, l2);
448
- const darker = Math.min(l1, l2);
449
-
450
- return (lighter + 0.05) / (darker + 0.05);
451
- }
452
-
453
- function parseColor(color: string): { r: number; g: number; b: number } | null {
454
- // Handle rgb() format
455
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
456
- if (rgbMatch && rgbMatch[1] && rgbMatch[2] && rgbMatch[3]) {
457
- return {
458
- r: parseInt(rgbMatch[1], 10),
459
- g: parseInt(rgbMatch[2], 10),
460
- b: parseInt(rgbMatch[3], 10)
461
- };
462
- }
463
-
464
- // Handle hex format
465
- const hexMatch = color.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
466
- if (hexMatch && hexMatch[1] && hexMatch[2] && hexMatch[3]) {
467
- return {
468
- r: parseInt(hexMatch[1], 16),
469
- g: parseInt(hexMatch[2], 16),
470
- b: parseInt(hexMatch[3], 16)
471
- };
472
- }
473
-
474
- // Handle named colors (basic set)
475
- const namedColors: Record<string, { r: number; g: number; b: number }> = {
476
- 'black': { r: 0, g: 0, b: 0 },
477
- 'white': { r: 255, g: 255, b: 255 },
478
- 'red': { r: 255, g: 0, b: 0 },
479
- 'green': { r: 0, g: 128, b: 0 },
480
- 'blue': { r: 0, g: 0, b: 255 }
70
+ const report: AuditReport = {
71
+ violations, passes, timestamp: Date.now(),
72
+ url: typeof window !== 'undefined' ? window.location.href : ''
481
73
  };
482
74
 
483
- return namedColors[color.toLowerCase()] || null;
484
- }
485
-
486
- function getRelativeLuminance(rgb: { r: number; g: number; b: number }): number {
487
- const { r, g, b } = rgb;
488
-
489
- // Convert to sRGB
490
- const rsRGB = r / 255;
491
- const gsRGB = g / 255;
492
- const bsRGB = b / 255;
493
-
494
- // Apply gamma correction
495
- const rLinear = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
496
- const gLinear = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
497
- const bLinear = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
498
-
499
- // Calculate relative luminance
500
- return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
501
- }
502
-
503
- function outputToConsole(report: AuditReport): void {
504
- console.group('🔍 Accessibility Audit Results');
505
-
506
- if (report.violations.length === 0) {
507
- console.log('✅ No accessibility violations found!');
508
- } else {
509
- console.log(`❌ Found ${report.violations.length} accessibility violations:`);
510
-
511
- report.violations.forEach(violation => {
512
- const emoji = violation.impact === 'critical' ? '🚨' :
513
- violation.impact === 'serious' ? 'âš ī¸' :
514
- violation.impact === 'moderate' ? '⚡' : 'â„šī¸';
515
-
516
- console.group(`${emoji} ${violation.help}`);
517
- console.log(`Impact: ${violation.impact}`);
518
- console.log(`Fix: ${violation.fix}`);
519
- if (violation.elements) {
520
- console.log('Elements:', violation.elements);
521
- }
522
- console.groupEnd();
523
- });
524
- }
525
-
526
- console.log(`✅ ${report.passes} checks passed`);
527
- if (report.incomplete > 0) {
528
- console.log(`âš ī¸ ${report.incomplete} checks incomplete`);
529
- }
530
-
531
- console.groupEnd();
532
- }
533
-
534
- function openReportInBrowser(report: AuditReport): void {
535
- const html = generateHTMLReport(report);
536
- const blob = new Blob([html], { type: 'text/html' });
537
- const url = URL.createObjectURL(blob);
538
-
539
- const newWindow = window.open(url, '_blank');
540
- if (!newWindow) {
541
- console.warn('Could not open report in new window. Please check popup blocker settings.');
542
- // Fallback: download the report
543
- const link = document.createElement('a');
544
- link.href = url;
545
- link.download = `proteus-a11y-report-${Date.now()}.html`;
546
- link.click();
75
+ if (format === 'console' && violations.length > 0) {
76
+ console.group('🔍 A11y Audit Results');
77
+ violations.forEach(v => console.warn(`${v.impact}: ${v.help}`));
78
+ console.groupEnd();
547
79
  }
548
80
 
549
- // Clean up the blob URL after a delay
550
- setTimeout(() => URL.revokeObjectURL(url), 1000);
551
- }
552
-
553
- function generateHTMLReport(report: AuditReport): string {
554
- const violationsList = report.violations.map(violation => `
555
- <div class="violation violation--${violation.impact}">
556
- <h3>${violation.help}</h3>
557
- <p><strong>Impact:</strong> ${violation.impact}</p>
558
- <p><strong>Fix:</strong> ${violation.fix}</p>
559
- <p><strong>Affected elements:</strong> ${violation.nodes}</p>
560
- </div>
561
- `).join('');
562
-
563
- return `
564
- <!DOCTYPE html>
565
- <html lang="en">
566
- <head>
567
- <meta charset="UTF-8">
568
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
569
- <title>ProteusJS Accessibility Report</title>
570
- <style>
571
- body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.6; }
572
- .header { border-bottom: 2px solid #e5e7eb; padding-bottom: 1rem; margin-bottom: 2rem; }
573
- .summary { background: #f3f4f6; padding: 1rem; border-radius: 8px; margin-bottom: 2rem; }
574
- .violation { border-left: 4px solid #ef4444; padding: 1rem; margin-bottom: 1rem; background: #fef2f2; }
575
- .violation--critical { border-color: #dc2626; background: #fef2f2; }
576
- .violation--serious { border-color: #ea580c; background: #fff7ed; }
577
- .violation--moderate { border-color: #d97706; background: #fffbeb; }
578
- .violation--minor { border-color: #65a30d; background: #f7fee7; }
579
- .violation h3 { margin-top: 0; color: #374151; }
580
- .no-violations { text-align: center; color: #059669; font-size: 1.25rem; padding: 2rem; }
581
- </style>
582
- </head>
583
- <body>
584
- <div class="header">
585
- <h1>🌊 ProteusJS Accessibility Report</h1>
586
- <p>Generated on ${new Date(report.timestamp).toLocaleString()}</p>
587
- <p>URL: ${report.url}</p>
588
- </div>
589
-
590
- <div class="summary">
591
- <h2>Summary</h2>
592
- <p><strong>Violations:</strong> ${report.violations.length}</p>
593
- <p><strong>Checks passed:</strong> ${report.passes}</p>
594
- <p><strong>Incomplete checks:</strong> ${report.incomplete}</p>
595
- </div>
596
-
597
- ${report.violations.length === 0 ?
598
- '<div class="no-violations">✅ No accessibility violations found!</div>' :
599
- `<h2>Violations</h2>${violationsList}`
600
- }
601
- </body>
602
- </html>`;
81
+ return report;
603
82
  }
604
83
 
605
- // Export default object for convenience
606
- export default {
607
- audit
608
- };
84
+ export default { audit };