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