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