@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/dist/proteus.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * ProteusJS v1.1.0
2
+ * ProteusJS v1.1.1
3
3
  * Shape-shifting responsive design that adapts like the sea god himself
4
4
  * (c) 2025 sc4rfurry
5
5
  * Released under the MIT License
@@ -16511,6 +16511,7 @@
16511
16511
  setupJSFallback();
16512
16512
  }
16513
16513
  return {
16514
+ update: updatePosition,
16514
16515
  destroy
16515
16516
  };
16516
16517
  }
@@ -17089,504 +17090,59 @@
17089
17090
 
17090
17091
  /**
17091
17092
  * @sc4rfurryx/proteusjs/a11y-audit
17092
- * Accessibility audits for development (dev-only)
17093
+ * Lightweight accessibility audits for development
17093
17094
  *
17094
17095
  * @version 1.1.0
17095
17096
  * @author sc4rfurry
17096
17097
  * @license MIT
17097
17098
  */
17098
- /**
17099
- * Run accessibility audits with actionable output
17100
- * DEV-ONLY: This module should be tree-shaken in production
17101
- */
17102
17099
  async function audit(target = document, options = {}) {
17103
- // Ensure this only runs in development
17104
- if (process.env['NODE_ENV'] === 'production') {
17105
- console.warn('a11y-audit should not be used in production');
17106
- return {
17107
- violations: [],
17108
- passes: 0,
17109
- incomplete: 0,
17110
- timestamp: Date.now(),
17111
- url: window.location.href
17112
- };
17100
+ if (typeof window === 'undefined' || process.env['NODE_ENV'] === 'production') {
17101
+ return { violations: [], passes: 0, timestamp: Date.now(), url: '' };
17113
17102
  }
17114
- const { rules = ['color-contrast', 'heading-order', 'image-alt', 'label', 'link-name', 'button-name'], format = 'console', openInBrowser = false } = options;
17103
+ const { rules = ['images', 'headings', 'forms'], format = 'console' } = options;
17115
17104
  const violations = [];
17116
17105
  let passes = 0;
17117
- let incomplete = 0;
17118
- // Basic accessibility checks
17119
- const checks = {
17120
- 'color-contrast': checkColorContrast,
17121
- 'heading-order': checkHeadingOrder,
17122
- 'image-alt': checkImageAlt,
17123
- 'label': checkFormLabels,
17124
- 'link-name': checkLinkNames,
17125
- 'button-name': checkButtonNames,
17126
- 'focus-visible': checkFocusVisible,
17127
- 'aria-labels': checkAriaLabels,
17128
- 'landmark-roles': checkLandmarkRoles,
17129
- 'skip-links': checkSkipLinks
17130
- };
17131
- // Run selected checks
17132
- for (const ruleId of rules) {
17133
- if (checks[ruleId]) {
17134
- try {
17135
- const result = await checks[ruleId](target);
17136
- if (result.violations.length > 0) {
17137
- violations.push(...result.violations);
17138
- }
17139
- else {
17140
- passes++;
17141
- }
17142
- }
17143
- catch (error) {
17144
- incomplete++;
17145
- console.warn(`Failed to run accessibility check: ${ruleId}`, error);
17146
- }
17147
- }
17148
- }
17149
- const report = {
17150
- violations,
17151
- passes,
17152
- incomplete,
17153
- timestamp: Date.now(),
17154
- url: window.location.href
17155
- };
17156
- // Output results
17157
- if (format === 'console') {
17158
- outputToConsole(report);
17159
- }
17160
- if (openInBrowser) {
17161
- openReportInBrowser(report);
17162
- }
17163
- return report;
17164
- }
17165
- // Individual check functions
17166
- async function checkColorContrast(target) {
17167
- const violations = [];
17168
- const elements = target.querySelectorAll('*');
17169
- elements.forEach(element => {
17170
- const style = getComputedStyle(element);
17171
- const color = style.color;
17172
- const backgroundColor = style.backgroundColor;
17173
- // Simple contrast check (would need more sophisticated implementation)
17174
- if (color && backgroundColor && color !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'rgba(0, 0, 0, 0)') {
17175
- const contrast = calculateContrast(color, backgroundColor);
17176
- if (contrast < 4.5) {
17177
- violations.push({
17178
- id: 'color-contrast',
17179
- impact: 'serious',
17180
- nodes: 1,
17181
- help: 'Elements must have sufficient color contrast',
17182
- fix: `Increase contrast ratio to at least 4.5:1. Current: ${contrast.toFixed(2)}:1`,
17183
- elements: [element]
17184
- });
17185
- }
17186
- }
17187
- });
17188
- return { violations };
17189
- }
17190
- async function checkHeadingOrder(target) {
17191
- const violations = [];
17192
- const headings = target.querySelectorAll('h1, h2, h3, h4, h5, h6');
17193
- let lastLevel = 0;
17194
- headings.forEach(heading => {
17195
- const level = parseInt(heading.tagName.charAt(1));
17196
- if (level > lastLevel + 1) {
17106
+ if (rules.includes('images')) {
17107
+ const imgs = target.querySelectorAll('img:not([alt])');
17108
+ if (imgs.length > 0) {
17197
17109
  violations.push({
17198
- id: 'heading-order',
17199
- impact: 'moderate',
17200
- nodes: 1,
17201
- help: 'Heading levels should only increase by one',
17202
- fix: `Change ${heading.tagName} to H${lastLevel + 1} or add intermediate headings`,
17203
- elements: [heading]
17110
+ id: 'image-alt', impact: 'critical', nodes: imgs.length, help: 'Images need alt text'
17204
17111
  });
17205
17112
  }
17206
- lastLevel = level;
17207
- });
17208
- return { violations };
17209
- }
17210
- async function checkImageAlt(target) {
17211
- const violations = [];
17212
- const images = target.querySelectorAll('img');
17213
- images.forEach(img => {
17214
- if (!img.hasAttribute('alt')) {
17215
- violations.push({
17216
- id: 'image-alt',
17217
- impact: 'critical',
17218
- nodes: 1,
17219
- help: 'Images must have alternative text',
17220
- fix: 'Add alt attribute with descriptive text or alt="" for decorative images',
17221
- elements: [img]
17222
- });
17223
- }
17224
- });
17225
- return { violations };
17226
- }
17227
- async function checkFormLabels(target) {
17228
- const violations = [];
17229
- const inputs = target.querySelectorAll('input, select, textarea');
17230
- inputs.forEach(input => {
17231
- const hasLabel = input.hasAttribute('aria-label') ||
17232
- input.hasAttribute('aria-labelledby') ||
17233
- target.querySelector(`label[for="${input.id}"]`) ||
17234
- input.closest('label');
17235
- if (!hasLabel) {
17236
- violations.push({
17237
- id: 'label',
17238
- impact: 'critical',
17239
- nodes: 1,
17240
- help: 'Form elements must have labels',
17241
- fix: 'Add a label element, aria-label, or aria-labelledby attribute',
17242
- elements: [input]
17243
- });
17244
- }
17245
- });
17246
- return { violations };
17247
- }
17248
- async function checkLinkNames(target) {
17249
- const violations = [];
17250
- const links = target.querySelectorAll('a[href]');
17251
- links.forEach(link => {
17252
- const text = link.textContent?.trim();
17253
- const ariaLabel = link.getAttribute('aria-label');
17254
- if (!text && !ariaLabel) {
17255
- violations.push({
17256
- id: 'link-name',
17257
- impact: 'serious',
17258
- nodes: 1,
17259
- help: 'Links must have discernible text',
17260
- fix: 'Add descriptive text content or aria-label attribute',
17261
- elements: [link]
17262
- });
17263
- }
17264
- });
17265
- return { violations };
17266
- }
17267
- async function checkButtonNames(target) {
17268
- const violations = [];
17269
- const buttons = target.querySelectorAll('button, input[type="button"], input[type="submit"]');
17270
- buttons.forEach(button => {
17271
- const text = button.textContent?.trim();
17272
- const ariaLabel = button.getAttribute('aria-label');
17273
- const value = button.getAttribute('value');
17274
- if (!text && !ariaLabel && !value) {
17275
- violations.push({
17276
- id: 'button-name',
17277
- impact: 'serious',
17278
- nodes: 1,
17279
- help: 'Buttons must have discernible text',
17280
- fix: 'Add text content, aria-label, or value attribute',
17281
- elements: [button]
17282
- });
17283
- }
17284
- });
17285
- return { violations };
17286
- }
17287
- async function checkFocusVisible(target) {
17288
- const violations = [];
17289
- const focusableElements = target.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
17290
- focusableElements.forEach(element => {
17291
- const styles = getComputedStyle(element);
17292
- const hasVisibleFocus = styles.outline !== 'none' && styles.outline !== '0px' &&
17293
- styles.outline !== '0' && styles.outlineWidth !== '0px';
17294
- // Check if element has custom focus styles
17295
- const hasCustomFocus = styles.boxShadow.includes('inset') ||
17296
- styles.border !== styles.borderColor ||
17297
- element.hasAttribute('data-focus-visible');
17298
- if (!hasVisibleFocus && !hasCustomFocus) {
17299
- violations.push({
17300
- id: 'focus-visible',
17301
- impact: 'serious',
17302
- nodes: 1,
17303
- help: 'Interactive elements must have visible focus indicators',
17304
- fix: 'Add outline, box-shadow, or other visible focus styles',
17305
- elements: [element]
17306
- });
17307
- }
17308
- });
17309
- return { violations };
17310
- }
17311
- async function checkAriaLabels(target) {
17312
- const violations = [];
17313
- // Check for aria-labelledby pointing to non-existent elements
17314
- const elementsWithLabelledBy = target.querySelectorAll('[aria-labelledby]');
17315
- elementsWithLabelledBy.forEach(element => {
17316
- const labelledBy = element.getAttribute('aria-labelledby');
17317
- if (labelledBy) {
17318
- const labelIds = labelledBy.split(' ');
17319
- const missingIds = labelIds.filter(id => !target.querySelector(`#${id}`));
17320
- if (missingIds.length > 0) {
17321
- violations.push({
17322
- id: 'aria-labelledby-invalid',
17323
- impact: 'serious',
17324
- nodes: 1,
17325
- help: 'aria-labelledby must reference existing elements',
17326
- fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
17327
- elements: [element]
17328
- });
17329
- }
17330
- }
17331
- });
17332
- // Check for aria-describedby pointing to non-existent elements
17333
- const elementsWithDescribedBy = target.querySelectorAll('[aria-describedby]');
17334
- elementsWithDescribedBy.forEach(element => {
17335
- const describedBy = element.getAttribute('aria-describedby');
17336
- if (describedBy) {
17337
- const descriptionIds = describedBy.split(' ');
17338
- const missingIds = descriptionIds.filter(id => !target.querySelector(`#${id}`));
17339
- if (missingIds.length > 0) {
17340
- violations.push({
17341
- id: 'aria-describedby-invalid',
17342
- impact: 'moderate',
17343
- nodes: 1,
17344
- help: 'aria-describedby must reference existing elements',
17345
- fix: `Fix or remove references to missing IDs: ${missingIds.join(', ')}`,
17346
- elements: [element]
17347
- });
17348
- }
17349
- }
17350
- });
17351
- return { violations };
17352
- }
17353
- async function checkLandmarkRoles(target) {
17354
- const violations = [];
17355
- // Check for missing main landmark
17356
- const mainElements = target.querySelectorAll('main, [role="main"]');
17357
- if (mainElements.length === 0) {
17358
- violations.push({
17359
- id: 'landmark-main-missing',
17360
- impact: 'moderate',
17361
- nodes: 0,
17362
- help: 'Page should have a main landmark',
17363
- fix: 'Add a <main> element or role="main" to identify the main content area'
17364
- });
17365
- }
17366
- else if (mainElements.length > 1) {
17367
- violations.push({
17368
- id: 'landmark-main-multiple',
17369
- impact: 'moderate',
17370
- nodes: mainElements.length,
17371
- help: 'Page should have only one main landmark',
17372
- fix: 'Ensure only one main element or role="main" exists per page',
17373
- elements: Array.from(mainElements)
17374
- });
17375
- }
17376
- // Check for navigation landmarks without labels when multiple exist
17377
- const navElements = target.querySelectorAll('nav, [role="navigation"]');
17378
- if (navElements.length > 1) {
17379
- navElements.forEach(nav => {
17380
- const hasLabel = nav.hasAttribute('aria-label') ||
17381
- nav.hasAttribute('aria-labelledby') ||
17382
- nav.querySelector('h1, h2, h3, h4, h5, h6');
17383
- if (!hasLabel) {
17384
- violations.push({
17385
- id: 'landmark-nav-unlabeled',
17386
- impact: 'moderate',
17387
- nodes: 1,
17388
- help: 'Multiple navigation landmarks should be labeled',
17389
- fix: 'Add aria-label or aria-labelledby to distinguish navigation areas',
17390
- elements: [nav]
17391
- });
17392
- }
17393
- });
17113
+ passes += target.querySelectorAll('img[alt]').length;
17394
17114
  }
17395
- return { violations };
17396
- }
17397
- async function checkSkipLinks(target) {
17398
- const violations = [];
17399
- // Check for skip links in documents with navigation
17400
- const navElements = target.querySelectorAll('nav, [role="navigation"]');
17401
- const mainElement = target.querySelector('main, [role="main"]');
17402
- if (navElements.length > 0 && mainElement) {
17403
- const skipLinks = target.querySelectorAll('a[href^="#"]');
17404
- const hasSkipToMain = Array.from(skipLinks).some(link => {
17405
- const href = link.getAttribute('href');
17406
- return href && (href === '#main' ||
17407
- href === `#${mainElement.id}` ||
17408
- link.textContent?.toLowerCase().includes('skip to main') ||
17409
- link.textContent?.toLowerCase().includes('skip to content'));
17410
- });
17411
- if (!hasSkipToMain) {
17115
+ if (rules.includes('headings')) {
17116
+ const h1s = target.querySelectorAll('h1');
17117
+ if (h1s.length !== 1) {
17412
17118
  violations.push({
17413
- id: 'skip-link-missing',
17414
- impact: 'moderate',
17415
- nodes: 0,
17416
- help: 'Page with navigation should have skip links',
17417
- fix: 'Add a skip link to the main content area for keyboard users'
17119
+ id: 'heading-structure', impact: 'moderate', nodes: h1s.length, help: 'Page should have exactly one h1'
17418
17120
  });
17419
17121
  }
17122
+ else
17123
+ passes++;
17420
17124
  }
17421
- // Check that skip links are properly positioned (should be first focusable element)
17422
- const firstFocusable = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
17423
- if (firstFocusable && firstFocusable.tagName === 'A') {
17424
- const href = firstFocusable.getAttribute('href');
17425
- if (!href?.startsWith('#')) {
17125
+ if (rules.includes('forms')) {
17126
+ const unlabeled = target.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
17127
+ if (unlabeled.length > 0) {
17426
17128
  violations.push({
17427
- id: 'skip-link-not-first',
17428
- impact: 'minor',
17429
- nodes: 1,
17430
- help: 'Skip links should be the first focusable elements',
17431
- fix: 'Move skip links to the beginning of the document',
17432
- elements: [firstFocusable]
17129
+ id: 'form-labels', impact: 'critical', nodes: unlabeled.length, help: 'Form inputs need labels'
17433
17130
  });
17434
17131
  }
17132
+ passes += target.querySelectorAll('input[aria-label], input[aria-labelledby]').length;
17435
17133
  }
17436
- return { violations };
17437
- }
17438
- // Utility functions
17439
- function calculateContrast(color1, color2) {
17440
- // Convert colors to RGB values
17441
- const rgb1 = parseColor(color1);
17442
- const rgb2 = parseColor(color2);
17443
- if (!rgb1 || !rgb2)
17444
- return 4.5; // Fallback if parsing fails
17445
- // Calculate relative luminance
17446
- const l1 = getRelativeLuminance(rgb1);
17447
- const l2 = getRelativeLuminance(rgb2);
17448
- // Calculate contrast ratio
17449
- const lighter = Math.max(l1, l2);
17450
- const darker = Math.min(l1, l2);
17451
- return (lighter + 0.05) / (darker + 0.05);
17452
- }
17453
- function parseColor(color) {
17454
- // Handle rgb() format
17455
- const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
17456
- if (rgbMatch && rgbMatch[1] && rgbMatch[2] && rgbMatch[3]) {
17457
- return {
17458
- r: parseInt(rgbMatch[1], 10),
17459
- g: parseInt(rgbMatch[2], 10),
17460
- b: parseInt(rgbMatch[3], 10)
17461
- };
17462
- }
17463
- // Handle hex format
17464
- const hexMatch = color.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
17465
- if (hexMatch && hexMatch[1] && hexMatch[2] && hexMatch[3]) {
17466
- return {
17467
- r: parseInt(hexMatch[1], 16),
17468
- g: parseInt(hexMatch[2], 16),
17469
- b: parseInt(hexMatch[3], 16)
17470
- };
17471
- }
17472
- // Handle named colors (basic set)
17473
- const namedColors = {
17474
- 'black': { r: 0, g: 0, b: 0 },
17475
- 'white': { r: 255, g: 255, b: 255 },
17476
- 'red': { r: 255, g: 0, b: 0 },
17477
- 'green': { r: 0, g: 128, b: 0 },
17478
- 'blue': { r: 0, g: 0, b: 255 }
17134
+ const report = {
17135
+ violations, passes, timestamp: Date.now(),
17136
+ url: typeof window !== 'undefined' ? window.location.href : ''
17479
17137
  };
17480
- return namedColors[color.toLowerCase()] || null;
17481
- }
17482
- function getRelativeLuminance(rgb) {
17483
- const { r, g, b } = rgb;
17484
- // Convert to sRGB
17485
- const rsRGB = r / 255;
17486
- const gsRGB = g / 255;
17487
- const bsRGB = b / 255;
17488
- // Apply gamma correction
17489
- const rLinear = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
17490
- const gLinear = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
17491
- const bLinear = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
17492
- // Calculate relative luminance
17493
- return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
17494
- }
17495
- function outputToConsole(report) {
17496
- console.group('🔍 Accessibility Audit Results');
17497
- if (report.violations.length === 0) {
17498
- console.log('✅ No accessibility violations found!');
17499
- }
17500
- else {
17501
- console.log(`❌ Found ${report.violations.length} accessibility violations:`);
17502
- report.violations.forEach(violation => {
17503
- const emoji = violation.impact === 'critical' ? '🚨' :
17504
- violation.impact === 'serious' ? '⚠️' :
17505
- violation.impact === 'moderate' ? '⚡' : 'ℹ️';
17506
- console.group(`${emoji} ${violation.help}`);
17507
- console.log(`Impact: ${violation.impact}`);
17508
- console.log(`Fix: ${violation.fix}`);
17509
- if (violation.elements) {
17510
- console.log('Elements:', violation.elements);
17511
- }
17512
- console.groupEnd();
17513
- });
17514
- }
17515
- console.log(`✅ ${report.passes} checks passed`);
17516
- if (report.incomplete > 0) {
17517
- console.log(`⚠️ ${report.incomplete} checks incomplete`);
17138
+ if (format === 'console' && violations.length > 0) {
17139
+ console.group('🔍 A11y Audit Results');
17140
+ violations.forEach(v => console.warn(`${v.impact}: ${v.help}`));
17141
+ console.groupEnd();
17518
17142
  }
17519
- console.groupEnd();
17520
- }
17521
- function openReportInBrowser(report) {
17522
- const html = generateHTMLReport(report);
17523
- const blob = new Blob([html], { type: 'text/html' });
17524
- const url = URL.createObjectURL(blob);
17525
- const newWindow = window.open(url, '_blank');
17526
- if (!newWindow) {
17527
- console.warn('Could not open report in new window. Please check popup blocker settings.');
17528
- // Fallback: download the report
17529
- const link = document.createElement('a');
17530
- link.href = url;
17531
- link.download = `proteus-a11y-report-${Date.now()}.html`;
17532
- link.click();
17533
- }
17534
- // Clean up the blob URL after a delay
17535
- setTimeout(() => URL.revokeObjectURL(url), 1000);
17536
- }
17537
- function generateHTMLReport(report) {
17538
- const violationsList = report.violations.map(violation => `
17539
- <div class="violation violation--${violation.impact}">
17540
- <h3>${violation.help}</h3>
17541
- <p><strong>Impact:</strong> ${violation.impact}</p>
17542
- <p><strong>Fix:</strong> ${violation.fix}</p>
17543
- <p><strong>Affected elements:</strong> ${violation.nodes}</p>
17544
- </div>
17545
- `).join('');
17546
- return `
17547
- <!DOCTYPE html>
17548
- <html lang="en">
17549
- <head>
17550
- <meta charset="UTF-8">
17551
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
17552
- <title>ProteusJS Accessibility Report</title>
17553
- <style>
17554
- body { font-family: system-ui, sans-serif; margin: 2rem; line-height: 1.6; }
17555
- .header { border-bottom: 2px solid #e5e7eb; padding-bottom: 1rem; margin-bottom: 2rem; }
17556
- .summary { background: #f3f4f6; padding: 1rem; border-radius: 8px; margin-bottom: 2rem; }
17557
- .violation { border-left: 4px solid #ef4444; padding: 1rem; margin-bottom: 1rem; background: #fef2f2; }
17558
- .violation--critical { border-color: #dc2626; background: #fef2f2; }
17559
- .violation--serious { border-color: #ea580c; background: #fff7ed; }
17560
- .violation--moderate { border-color: #d97706; background: #fffbeb; }
17561
- .violation--minor { border-color: #65a30d; background: #f7fee7; }
17562
- .violation h3 { margin-top: 0; color: #374151; }
17563
- .no-violations { text-align: center; color: #059669; font-size: 1.25rem; padding: 2rem; }
17564
- </style>
17565
- </head>
17566
- <body>
17567
- <div class="header">
17568
- <h1>🌊 ProteusJS Accessibility Report</h1>
17569
- <p>Generated on ${new Date(report.timestamp).toLocaleString()}</p>
17570
- <p>URL: ${report.url}</p>
17571
- </div>
17572
-
17573
- <div class="summary">
17574
- <h2>Summary</h2>
17575
- <p><strong>Violations:</strong> ${report.violations.length}</p>
17576
- <p><strong>Checks passed:</strong> ${report.passes}</p>
17577
- <p><strong>Incomplete checks:</strong> ${report.incomplete}</p>
17578
- </div>
17579
-
17580
- ${report.violations.length === 0 ?
17581
- '<div class="no-violations">✅ No accessibility violations found!</div>' :
17582
- `<h2>Violations</h2>${violationsList}`}
17583
- </body>
17584
- </html>`;
17143
+ return report;
17585
17144
  }
17586
- // Export default object for convenience
17587
- var index$4 = {
17588
- audit
17589
- };
17145
+ var index$4 = { audit };
17590
17146
 
17591
17147
  var index$5 = /*#__PURE__*/Object.freeze({
17592
17148
  __proto__: null,
@@ -17596,450 +17152,116 @@
17596
17152
 
17597
17153
  /**
17598
17154
  * @sc4rfurryx/proteusjs/a11y-primitives
17599
- * Headless accessibility patterns (no styles)
17155
+ * Lightweight accessibility patterns
17600
17156
  *
17601
17157
  * @version 1.1.0
17602
17158
  * @author sc4rfurry
17603
17159
  * @license MIT
17604
17160
  */
17605
- /**
17606
- * Dialog primitive with proper ARIA and focus management
17607
- */
17608
17161
  function dialog(root, opts = {}) {
17609
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17610
- if (!rootEl)
17611
- throw new Error('Dialog root element not found');
17162
+ const el = typeof root === 'string' ? document.querySelector(root) : root;
17163
+ if (!el)
17164
+ throw new Error('Dialog element not found');
17612
17165
  const { modal = true, restoreFocus = true } = opts;
17613
- let isOpen = false;
17614
- const setup = () => {
17615
- rootEl.setAttribute('role', 'dialog');
17616
- if (modal) {
17617
- rootEl.setAttribute('aria-modal', 'true');
17618
- }
17619
- // Ensure dialog is initially hidden
17620
- if (!rootEl.hasAttribute('hidden')) {
17621
- rootEl.setAttribute('hidden', '');
17622
- }
17623
- };
17624
- const handleKeyDown = (e) => {
17625
- if (e.key === 'Escape' && isOpen) ;
17626
- };
17627
- setup();
17628
- document.addEventListener('keydown', handleKeyDown);
17629
- return {
17630
- destroy: () => {
17631
- document.removeEventListener('keydown', handleKeyDown);
17632
- }
17166
+ const close = () => {
17633
17167
  };
17168
+ return { destroy: () => close() };
17634
17169
  }
17635
- /**
17636
- * Tooltip primitive with delay and proper ARIA
17637
- */
17638
- function tooltip(trigger, panel, opts = {}) {
17639
- const triggerEl = typeof trigger === 'string' ? document.querySelector(trigger) : trigger;
17640
- const panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
17641
- if (!triggerEl || !panelEl) {
17642
- throw new Error('Both trigger and panel elements must exist');
17643
- }
17644
- const { delay = 500 } = opts;
17645
- let timeoutId = null;
17646
- let isVisible = false;
17647
- const setup = () => {
17648
- const tooltipId = panelEl.id || `tooltip-${Math.random().toString(36).substring(2, 11)}`;
17649
- panelEl.id = tooltipId;
17650
- panelEl.setAttribute('role', 'tooltip');
17651
- triggerEl.setAttribute('aria-describedby', tooltipId);
17652
- // Initially hidden
17653
- panelEl.style.display = 'none';
17654
- };
17170
+ function tooltip(trigger, content, opts = {}) {
17171
+ const { delay = 300 } = opts;
17172
+ let timeout;
17655
17173
  const show = () => {
17656
- if (isVisible)
17657
- return;
17658
- panelEl.style.display = 'block';
17659
- isVisible = true;
17174
+ clearTimeout(timeout);
17175
+ timeout = window.setTimeout(() => {
17176
+ content.setAttribute('role', 'tooltip');
17177
+ trigger.setAttribute('aria-describedby', content.id || 'tooltip');
17178
+ content.style.display = 'block';
17179
+ }, delay);
17660
17180
  };
17661
17181
  const hide = () => {
17662
- if (!isVisible)
17663
- return;
17664
- panelEl.style.display = 'none';
17665
- isVisible = false;
17182
+ clearTimeout(timeout);
17183
+ content.style.display = 'none';
17184
+ trigger.removeAttribute('aria-describedby');
17666
17185
  };
17667
- const handleMouseEnter = () => {
17668
- if (timeoutId)
17669
- clearTimeout(timeoutId);
17670
- timeoutId = window.setTimeout(show, delay);
17671
- };
17672
- const handleMouseLeave = () => {
17673
- if (timeoutId) {
17674
- clearTimeout(timeoutId);
17675
- timeoutId = null;
17676
- }
17677
- hide();
17678
- };
17679
- const handleFocus = () => {
17680
- show();
17681
- };
17682
- const handleBlur = () => {
17683
- hide();
17684
- };
17685
- setup();
17686
- triggerEl.addEventListener('mouseenter', handleMouseEnter);
17687
- triggerEl.addEventListener('mouseleave', handleMouseLeave);
17688
- triggerEl.addEventListener('focus', handleFocus);
17689
- triggerEl.addEventListener('blur', handleBlur);
17186
+ trigger.addEventListener('mouseenter', show);
17187
+ trigger.addEventListener('mouseleave', hide);
17188
+ trigger.addEventListener('focus', show);
17189
+ trigger.addEventListener('blur', hide);
17690
17190
  return {
17691
17191
  destroy: () => {
17692
- if (timeoutId)
17693
- clearTimeout(timeoutId);
17694
- triggerEl.removeEventListener('mouseenter', handleMouseEnter);
17695
- triggerEl.removeEventListener('mouseleave', handleMouseLeave);
17696
- triggerEl.removeEventListener('focus', handleFocus);
17697
- triggerEl.removeEventListener('blur', handleBlur);
17698
- hide();
17192
+ clearTimeout(timeout);
17193
+ trigger.removeEventListener('mouseenter', show);
17194
+ trigger.removeEventListener('mouseleave', hide);
17195
+ trigger.removeEventListener('focus', show);
17196
+ trigger.removeEventListener('blur', hide);
17699
17197
  }
17700
17198
  };
17701
17199
  }
17702
- /**
17703
- * Listbox primitive with keyboard navigation
17704
- */
17705
- function listbox(root, opts = {}) {
17706
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17707
- if (!rootEl)
17708
- throw new Error('Listbox root element not found');
17709
- const { multiselect = false } = opts;
17710
- let currentIndex = -1;
17711
- const setup = () => {
17712
- rootEl.setAttribute('role', 'listbox');
17713
- if (multiselect) {
17714
- rootEl.setAttribute('aria-multiselectable', 'true');
17715
- }
17716
- // Set up options
17717
- const options = rootEl.querySelectorAll('[role="option"]');
17718
- options.forEach((option, _index) => {
17719
- option.setAttribute('aria-selected', 'false');
17720
- option.setAttribute('tabindex', '-1');
17721
- });
17722
- if (options.length > 0) {
17723
- options[0]?.setAttribute('tabindex', '0');
17724
- currentIndex = 0;
17725
- }
17726
- };
17727
- const getOptions = () => rootEl.querySelectorAll('[role="option"]');
17728
- const setCurrentIndex = (index) => {
17729
- const options = getOptions();
17730
- if (index < 0 || index >= options.length)
17731
- return;
17732
- // Remove tabindex from all options
17733
- options.forEach(option => option.setAttribute('tabindex', '-1'));
17734
- // Set current option
17735
- currentIndex = index;
17736
- options[currentIndex]?.setAttribute('tabindex', '0');
17737
- options[currentIndex]?.focus();
17738
- };
17739
- const selectOption = (index) => {
17740
- const options = getOptions();
17741
- if (index < 0 || index >= options.length)
17742
- return;
17743
- if (multiselect) {
17744
- const isSelected = options[index]?.getAttribute('aria-selected') === 'true';
17745
- options[index]?.setAttribute('aria-selected', (!isSelected).toString());
17746
- }
17747
- else {
17748
- // Single select - clear all others
17749
- options.forEach(option => option.setAttribute('aria-selected', 'false'));
17750
- options[index]?.setAttribute('aria-selected', 'true');
17751
- }
17752
- };
17753
- const handleKeyDown = (e) => {
17754
- const keyEvent = e;
17755
- const options = getOptions();
17756
- switch (keyEvent.key) {
17757
- case 'ArrowDown':
17758
- keyEvent.preventDefault();
17759
- setCurrentIndex(Math.min(currentIndex + 1, options.length - 1));
17760
- break;
17761
- case 'ArrowUp':
17762
- keyEvent.preventDefault();
17763
- setCurrentIndex(Math.max(currentIndex - 1, 0));
17764
- break;
17765
- case 'Home':
17766
- keyEvent.preventDefault();
17767
- setCurrentIndex(0);
17768
- break;
17769
- case 'End':
17770
- keyEvent.preventDefault();
17771
- setCurrentIndex(options.length - 1);
17772
- break;
17773
- case 'Enter':
17774
- case ' ':
17775
- keyEvent.preventDefault();
17776
- selectOption(currentIndex);
17777
- break;
17778
- }
17779
- };
17780
- const handleClick = (e) => {
17781
- const target = e.target;
17782
- const option = target.closest('[role="option"]');
17783
- if (!option)
17200
+ function focusTrap(container) {
17201
+ const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
17202
+ const activate = () => {
17203
+ const elements = container.querySelectorAll(focusable);
17204
+ if (elements.length === 0)
17784
17205
  return;
17785
- const options = Array.from(getOptions());
17786
- const index = options.indexOf(option);
17787
- if (index >= 0) {
17788
- setCurrentIndex(index);
17789
- selectOption(index);
17790
- }
17791
- };
17792
- setup();
17793
- rootEl.addEventListener('keydown', handleKeyDown);
17794
- rootEl.addEventListener('click', handleClick);
17795
- return {
17796
- destroy: () => {
17797
- rootEl.removeEventListener('keydown', handleKeyDown);
17798
- rootEl.removeEventListener('click', handleClick);
17799
- }
17800
- };
17801
- }
17802
- /**
17803
- * Combobox primitive with filtering and multiselect
17804
- */
17805
- function combobox(root, opts = {}) {
17806
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17807
- if (!rootEl)
17808
- throw new Error('Combobox root element not found');
17809
- const { multiselect = false, filtering: _filtering } = opts;
17810
- let isOpen = false;
17811
- const setup = () => {
17812
- rootEl.setAttribute('role', 'combobox');
17813
- rootEl.setAttribute('aria-expanded', 'false');
17814
- if (multiselect) {
17815
- rootEl.setAttribute('aria-multiselectable', 'true');
17816
- }
17817
- };
17818
- const handleKeyDown = (e) => {
17819
- const keyEvent = e;
17820
- switch (keyEvent.key) {
17821
- case 'ArrowDown':
17822
- keyEvent.preventDefault();
17823
- if (!isOpen) {
17824
- isOpen = true;
17825
- rootEl.setAttribute('aria-expanded', 'true');
17826
- }
17827
- // Navigate options logic would go here
17828
- break;
17829
- case 'Escape':
17830
- keyEvent.preventDefault();
17831
- isOpen = false;
17832
- rootEl.setAttribute('aria-expanded', 'false');
17833
- break;
17834
- }
17835
- };
17836
- setup();
17837
- rootEl.addEventListener('keydown', handleKeyDown);
17838
- return {
17839
- destroy: () => {
17840
- rootEl.removeEventListener('keydown', handleKeyDown);
17841
- }
17842
- };
17843
- }
17844
- /**
17845
- * Tabs primitive with keyboard navigation
17846
- */
17847
- function tabs(root) {
17848
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17849
- if (!rootEl)
17850
- throw new Error('Tabs root element not found');
17851
- let currentIndex = 0;
17852
- const setup = () => {
17853
- const tabList = rootEl.querySelector('[role="tablist"]');
17854
- const tabs = rootEl.querySelectorAll('[role="tab"]');
17855
- const panels = rootEl.querySelectorAll('[role="tabpanel"]');
17856
- if (!tabList) {
17857
- rootEl.setAttribute('role', 'tablist');
17858
- }
17859
- tabs.forEach((tab, index) => {
17860
- tab.setAttribute('tabindex', index === 0 ? '0' : '-1');
17861
- tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
17862
- });
17863
- panels.forEach((panel, index) => {
17864
- panel.setAttribute('hidden', index === 0 ? '' : 'true');
17865
- });
17866
- };
17867
- const handleKeyDown = (e) => {
17868
- const keyEvent = e;
17869
- const tabs = Array.from(rootEl.querySelectorAll('[role="tab"]'));
17870
- switch (keyEvent.key) {
17871
- case 'ArrowRight':
17872
- keyEvent.preventDefault();
17873
- currentIndex = (currentIndex + 1) % tabs.length;
17874
- activateTab(currentIndex);
17875
- break;
17876
- case 'ArrowLeft':
17877
- keyEvent.preventDefault();
17878
- currentIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;
17879
- activateTab(currentIndex);
17880
- break;
17881
- }
17882
- };
17883
- const activateTab = (index) => {
17884
- const tabs = rootEl.querySelectorAll('[role="tab"]');
17885
- const panels = rootEl.querySelectorAll('[role="tabpanel"]');
17886
- tabs.forEach((tab, i) => {
17887
- tab.setAttribute('tabindex', i === index ? '0' : '-1');
17888
- tab.setAttribute('aria-selected', i === index ? 'true' : 'false');
17889
- if (i === index) {
17890
- tab.focus();
17891
- }
17892
- });
17893
- panels.forEach((panel, i) => {
17894
- if (i === index) {
17895
- panel.removeAttribute('hidden');
17206
+ const first = elements[0];
17207
+ const last = elements[elements.length - 1];
17208
+ const handleTab = (e) => {
17209
+ if (e.key !== 'Tab')
17210
+ return;
17211
+ if (e.shiftKey && document.activeElement === first) {
17212
+ e.preventDefault();
17213
+ last.focus();
17896
17214
  }
17897
- else {
17898
- panel.setAttribute('hidden', 'true');
17215
+ else if (!e.shiftKey && document.activeElement === last) {
17216
+ e.preventDefault();
17217
+ first.focus();
17899
17218
  }
17900
- });
17219
+ };
17220
+ container.addEventListener('keydown', handleTab);
17221
+ first.focus();
17222
+ return () => container.removeEventListener('keydown', handleTab);
17901
17223
  };
17902
- setup();
17903
- rootEl.addEventListener('keydown', handleKeyDown);
17224
+ let deactivate = () => { };
17904
17225
  return {
17905
- destroy: () => {
17906
- rootEl.removeEventListener('keydown', handleKeyDown);
17907
- }
17226
+ activate: () => { deactivate = activate() || (() => { }); },
17227
+ deactivate: () => deactivate()
17908
17228
  };
17909
17229
  }
17910
- /**
17911
- * Menu primitive with keyboard navigation
17912
- */
17913
- function menu(root) {
17914
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17915
- if (!rootEl)
17916
- throw new Error('Menu root element not found');
17917
- let currentIndex = -1;
17918
- const setup = () => {
17919
- rootEl.setAttribute('role', 'menu');
17920
- const items = rootEl.querySelectorAll('[role="menuitem"]');
17921
- items.forEach((item, index) => {
17922
- item.setAttribute('tabindex', index === 0 ? '0' : '-1');
17923
- });
17924
- if (items.length > 0) {
17925
- currentIndex = 0;
17926
- }
17927
- };
17928
- const handleKeyDown = (e) => {
17929
- const keyEvent = e;
17930
- const items = Array.from(rootEl.querySelectorAll('[role="menuitem"]'));
17931
- switch (keyEvent.key) {
17230
+ function menu(container) {
17231
+ const items = container.querySelectorAll('[role="menuitem"]');
17232
+ let currentIndex = 0;
17233
+ const navigate = (e) => {
17234
+ switch (e.key) {
17932
17235
  case 'ArrowDown':
17933
- keyEvent.preventDefault();
17236
+ e.preventDefault();
17934
17237
  currentIndex = (currentIndex + 1) % items.length;
17935
- setCurrentItem(currentIndex);
17238
+ items[currentIndex].focus();
17936
17239
  break;
17937
17240
  case 'ArrowUp':
17938
- keyEvent.preventDefault();
17241
+ e.preventDefault();
17939
17242
  currentIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
17940
- setCurrentItem(currentIndex);
17941
- break;
17942
- case 'Enter':
17943
- case ' ':
17944
- keyEvent.preventDefault();
17945
- if (items[currentIndex]) {
17946
- items[currentIndex].click();
17947
- }
17243
+ items[currentIndex].focus();
17948
17244
  break;
17949
- }
17950
- };
17951
- const setCurrentItem = (index) => {
17952
- const items = rootEl.querySelectorAll('[role="menuitem"]');
17953
- items.forEach((item, i) => {
17954
- item.setAttribute('tabindex', i === index ? '0' : '-1');
17955
- if (i === index) {
17956
- item.focus();
17957
- }
17958
- });
17959
- };
17960
- setup();
17961
- rootEl.addEventListener('keydown', handleKeyDown);
17962
- return {
17963
- destroy: () => {
17964
- rootEl.removeEventListener('keydown', handleKeyDown);
17965
- }
17966
- };
17967
- }
17968
- /**
17969
- * Focus trap utility
17970
- */
17971
- function focusTrap(root) {
17972
- const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
17973
- if (!rootEl)
17974
- throw new Error('Focus trap root element not found');
17975
- let isActive = false;
17976
- let focusableElements = [];
17977
- const updateFocusableElements = () => {
17978
- const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
17979
- focusableElements = Array.from(rootEl.querySelectorAll(selector));
17980
- };
17981
- const handleKeyDown = (e) => {
17982
- if (!isActive || e.key !== 'Tab')
17983
- return;
17984
- updateFocusableElements();
17985
- if (focusableElements.length === 0)
17986
- return;
17987
- const firstElement = focusableElements[0];
17988
- const lastElement = focusableElements[focusableElements.length - 1];
17989
- if (e.shiftKey) {
17990
- if (document.activeElement === firstElement) {
17991
- e.preventDefault();
17992
- lastElement.focus();
17993
- }
17994
- }
17995
- else {
17996
- if (document.activeElement === lastElement) {
17245
+ case 'Escape':
17997
17246
  e.preventDefault();
17998
- firstElement.focus();
17999
- }
18000
- }
18001
- };
18002
- const activate = () => {
18003
- if (isActive)
18004
- return;
18005
- isActive = true;
18006
- updateFocusableElements();
18007
- if (focusableElements.length > 0) {
18008
- focusableElements[0].focus();
17247
+ container.dispatchEvent(new CustomEvent('menu:close'));
17248
+ break;
18009
17249
  }
18010
- document.addEventListener('keydown', handleKeyDown);
18011
- };
18012
- const deactivate = () => {
18013
- if (!isActive)
18014
- return;
18015
- isActive = false;
18016
- document.removeEventListener('keydown', handleKeyDown);
18017
17250
  };
17251
+ container.setAttribute('role', 'menu');
17252
+ container.addEventListener('keydown', navigate);
18018
17253
  return {
18019
- activate,
18020
- deactivate
17254
+ destroy: () => container.removeEventListener('keydown', navigate)
18021
17255
  };
18022
17256
  }
18023
- // Export all functions
18024
- var index$2 = {
18025
- dialog,
18026
- tooltip,
18027
- combobox,
18028
- listbox,
18029
- tabs,
18030
- menu,
18031
- focusTrap
18032
- };
17257
+ var index$2 = { dialog, tooltip, focusTrap, menu };
18033
17258
 
18034
17259
  var index$3 = /*#__PURE__*/Object.freeze({
18035
17260
  __proto__: null,
18036
- combobox: combobox,
18037
17261
  default: index$2,
18038
17262
  dialog: dialog,
18039
17263
  focusTrap: focusTrap,
18040
- listbox: listbox,
18041
17264
  menu: menu,
18042
- tabs: tabs,
18043
17265
  tooltip: tooltip
18044
17266
  });
18045
17267