@sc4rfurryx/proteusjs 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +684 -899
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/modules/a11y-audit.d.ts +2 -10
  4. package/dist/modules/a11y-audit.esm.js +31 -476
  5. package/dist/modules/a11y-audit.esm.js.map +1 -1
  6. package/dist/modules/a11y-primitives.d.ts +9 -42
  7. package/dist/modules/a11y-primitives.esm.js +70 -401
  8. package/dist/modules/a11y-primitives.esm.js.map +1 -1
  9. package/dist/modules/anchor.d.ts +2 -1
  10. package/dist/modules/anchor.esm.js +3 -2
  11. package/dist/modules/anchor.esm.js.map +1 -1
  12. package/dist/modules/container.d.ts +1 -1
  13. package/dist/modules/container.esm.js +34 -34
  14. package/dist/modules/container.esm.js.map +1 -1
  15. package/dist/modules/perf.d.ts +1 -1
  16. package/dist/modules/perf.esm.js +2 -2
  17. package/dist/modules/popover.d.ts +1 -1
  18. package/dist/modules/popover.esm.js +2 -2
  19. package/dist/modules/scroll.d.ts +1 -1
  20. package/dist/modules/scroll.esm.js +14 -14
  21. package/dist/modules/scroll.esm.js.map +1 -1
  22. package/dist/modules/transitions.d.ts +1 -1
  23. package/dist/modules/transitions.esm.js +12 -12
  24. package/dist/modules/transitions.esm.js.map +1 -1
  25. package/dist/modules/typography.d.ts +1 -1
  26. package/dist/modules/typography.esm.js +2 -2
  27. package/dist/proteus.cjs.js +163 -941
  28. package/dist/proteus.cjs.js.map +1 -1
  29. package/dist/proteus.d.ts +23 -68
  30. package/dist/proteus.esm.js +163 -941
  31. package/dist/proteus.esm.js.map +1 -1
  32. package/dist/proteus.esm.min.js +2 -2
  33. package/dist/proteus.esm.min.js.map +1 -1
  34. package/dist/proteus.js +163 -941
  35. package/dist/proteus.js.map +1 -1
  36. package/dist/proteus.min.js +2 -2
  37. package/dist/proteus.min.js.map +1 -1
  38. package/package.json +44 -7
  39. package/src/adapters/react.ts +607 -264
  40. package/src/adapters/svelte.ts +321 -321
  41. package/src/adapters/vue.ts +268 -268
  42. package/src/core/ProteusJS.ts +6 -6
  43. package/src/index.ts +3 -3
  44. package/src/modules/a11y-audit/index.ts +84 -608
  45. package/src/modules/a11y-primitives/index.ts +152 -554
  46. package/src/modules/anchor/index.ts +259 -257
  47. package/src/modules/container/index.ts +230 -230
  48. package/src/modules/perf/index.ts +291 -291
  49. package/src/modules/popover/index.ts +238 -238
  50. package/src/modules/scroll/index.ts +251 -251
  51. package/src/modules/transitions/index.ts +145 -145
  52. package/src/modules/typography/index.ts +239 -239
  53. package/src/utils/version.ts +1 -1
  54. package/dist/adapters/react.d.ts +0 -139
  55. package/dist/adapters/react.esm.js +0 -848
  56. package/dist/adapters/react.esm.js.map +0 -1
  57. package/dist/adapters/svelte.d.ts +0 -181
  58. package/dist/adapters/svelte.esm.js +0 -908
  59. package/dist/adapters/svelte.esm.js.map +0 -1
  60. package/dist/adapters/vue.d.ts +0 -205
  61. package/dist/adapters/vue.esm.js +0 -872
  62. package/dist/adapters/vue.esm.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * ProteusJS v1.1.0
2
+ * ProteusJS v2.0.0
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
@@ -15225,7 +15225,7 @@ function checkPassiveEventListenerSupport() {
15225
15225
  /**
15226
15226
  * Version utilities for ProteusJS
15227
15227
  */
15228
- const version = '1.1.0';
15228
+ const version = '2.0.0';
15229
15229
 
15230
15230
  /**
15231
15231
  * ProteusJS - Main library class
@@ -15992,7 +15992,7 @@ ProteusJS.instance = null;
15992
15992
  * @sc4rfurryx/proteusjs/transitions
15993
15993
  * View Transitions API wrapper with safe fallbacks
15994
15994
  *
15995
- * @version 1.1.0
15995
+ * @version 2.0.0
15996
15996
  * @author sc4rfurry
15997
15997
  * @license MIT
15998
15998
  */
@@ -16027,11 +16027,11 @@ async function transition(run, opts = {}) {
16027
16027
  // Add CSS view-transition-name if name provided
16028
16028
  if (name) {
16029
16029
  const style = document.createElement('style');
16030
- style.textContent = `
16031
- ::view-transition-old(${name}),
16032
- ::view-transition-new(${name}) {
16033
- animation-duration: ${duration}ms;
16034
- }
16030
+ style.textContent = `
16031
+ ::view-transition-old(${name}),
16032
+ ::view-transition-new(${name}) {
16033
+ animation-duration: ${duration}ms;
16034
+ }
16035
16035
  `;
16036
16036
  document.head.appendChild(style);
16037
16037
  // Clean up style after transition
@@ -16079,11 +16079,11 @@ async function navigate(url, opts = {}) {
16079
16079
  });
16080
16080
  if (name) {
16081
16081
  const style = document.createElement('style');
16082
- style.textContent = `
16083
- ::view-transition-old(${name}),
16084
- ::view-transition-new(${name}) {
16085
- animation-duration: 300ms;
16086
- }
16082
+ style.textContent = `
16083
+ ::view-transition-old(${name}),
16084
+ ::view-transition-new(${name}) {
16085
+ animation-duration: 300ms;
16086
+ }
16087
16087
  `;
16088
16088
  document.head.appendChild(style);
16089
16089
  }
@@ -16111,7 +16111,7 @@ var index$h = /*#__PURE__*/Object.freeze({
16111
16111
  * @sc4rfurryx/proteusjs/scroll
16112
16112
  * Scroll-driven animations with CSS Scroll-Linked Animations
16113
16113
  *
16114
- * @version 1.1.0
16114
+ * @version 2.0.0
16115
16115
  * @author sc4rfurry
16116
16116
  * @license MIT
16117
16117
  */
@@ -16143,18 +16143,18 @@ function scrollAnimate(target, opts) {
16143
16143
  const timelineName = `scroll-timeline-${Math.random().toString(36).substr(2, 9)}`;
16144
16144
  // Create scroll timeline
16145
16145
  const style = document.createElement('style');
16146
- style.textContent = `
16147
- @scroll-timeline ${timelineName} {
16148
- source: nearest;
16149
- orientation: ${axis};
16150
- scroll-offsets: ${start}, ${end};
16151
- }
16152
-
16153
- .scroll-animate-${timelineName} {
16154
- animation-timeline: ${timelineName};
16155
- animation-duration: 1ms; /* Required but ignored */
16156
- animation-fill-mode: both;
16157
- }
16146
+ style.textContent = `
16147
+ @scroll-timeline ${timelineName} {
16148
+ source: nearest;
16149
+ orientation: ${axis};
16150
+ scroll-offsets: ${start}, ${end};
16151
+ }
16152
+
16153
+ .scroll-animate-${timelineName} {
16154
+ animation-timeline: ${timelineName};
16155
+ animation-duration: 1ms; /* Required but ignored */
16156
+ animation-fill-mode: both;
16157
+ }
16158
16158
  `;
16159
16159
  document.head.appendChild(style);
16160
16160
  // Apply animation class
@@ -16307,7 +16307,7 @@ var index$f = /*#__PURE__*/Object.freeze({
16307
16307
  * @sc4rfurryx/proteusjs/anchor
16308
16308
  * CSS Anchor Positioning utilities with robust JS fallback
16309
16309
  *
16310
- * @version 1.1.0
16310
+ * @version 2.0.0
16311
16311
  * @author sc4rfurry
16312
16312
  * @license MIT
16313
16313
  */
@@ -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
  }
@@ -16523,7 +16524,7 @@ var index$d = /*#__PURE__*/Object.freeze({
16523
16524
  * @sc4rfurryx/proteusjs/popover
16524
16525
  * HTML Popover API wrapper with robust focus/inert handling
16525
16526
  *
16526
- * @version 1.1.0
16527
+ * @version 2.0.0
16527
16528
  * @author sc4rfurry
16528
16529
  * @license MIT
16529
16530
  */
@@ -16712,7 +16713,7 @@ var index$b = /*#__PURE__*/Object.freeze({
16712
16713
  * @sc4rfurryx/proteusjs/container
16713
16714
  * Container/Style Query helpers with visualization devtools
16714
16715
  *
16715
- * @version 1.1.0
16716
+ * @version 2.0.0
16716
16717
  * @author sc4rfurry
16717
16718
  * @license MIT
16718
16719
  */
@@ -16746,43 +16747,43 @@ function defineContainer(target, name, opts = {}) {
16746
16747
  function createDevOverlay(element, name) {
16747
16748
  const overlay = document.createElement('div');
16748
16749
  overlay.className = 'proteus-container-overlay';
16749
- overlay.style.cssText = `
16750
- position: absolute;
16751
- top: 0;
16752
- left: 0;
16753
- right: 0;
16754
- bottom: 0;
16755
- pointer-events: none;
16756
- border: 2px dashed rgba(255, 0, 255, 0.5);
16757
- background: rgba(255, 0, 255, 0.05);
16758
- z-index: 9999;
16759
- font-family: monospace;
16760
- font-size: 12px;
16761
- color: #ff00ff;
16750
+ overlay.style.cssText = `
16751
+ position: absolute;
16752
+ top: 0;
16753
+ left: 0;
16754
+ right: 0;
16755
+ bottom: 0;
16756
+ pointer-events: none;
16757
+ border: 2px dashed rgba(255, 0, 255, 0.5);
16758
+ background: rgba(255, 0, 255, 0.05);
16759
+ z-index: 9999;
16760
+ font-family: monospace;
16761
+ font-size: 12px;
16762
+ color: #ff00ff;
16762
16763
  `;
16763
16764
  const label = document.createElement('div');
16764
- label.style.cssText = `
16765
- position: absolute;
16766
- top: -20px;
16767
- left: 0;
16768
- background: rgba(255, 0, 255, 0.9);
16769
- color: white;
16770
- padding: 2px 6px;
16771
- border-radius: 3px;
16772
- font-size: 10px;
16773
- white-space: nowrap;
16765
+ label.style.cssText = `
16766
+ position: absolute;
16767
+ top: -20px;
16768
+ left: 0;
16769
+ background: rgba(255, 0, 255, 0.9);
16770
+ color: white;
16771
+ padding: 2px 6px;
16772
+ border-radius: 3px;
16773
+ font-size: 10px;
16774
+ white-space: nowrap;
16774
16775
  `;
16775
16776
  label.textContent = `Container: ${name}`;
16776
16777
  const sizeInfo = document.createElement('div');
16777
- sizeInfo.style.cssText = `
16778
- position: absolute;
16779
- bottom: 2px;
16780
- right: 2px;
16781
- background: rgba(0, 0, 0, 0.7);
16782
- color: white;
16783
- padding: 2px 4px;
16784
- border-radius: 2px;
16785
- font-size: 10px;
16778
+ sizeInfo.style.cssText = `
16779
+ position: absolute;
16780
+ bottom: 2px;
16781
+ right: 2px;
16782
+ background: rgba(0, 0, 0, 0.7);
16783
+ color: white;
16784
+ padding: 2px 4px;
16785
+ border-radius: 2px;
16786
+ font-size: 10px;
16786
16787
  `;
16787
16788
  overlay.appendChild(label);
16788
16789
  overlay.appendChild(sizeInfo);
@@ -16911,7 +16912,7 @@ var index$9 = /*#__PURE__*/Object.freeze({
16911
16912
  * @sc4rfurryx/proteusjs/typography
16912
16913
  * Fluid typography with CSS-first approach
16913
16914
  *
16914
- * @version 1.1.0
16915
+ * @version 2.0.0
16915
16916
  * @author sc4rfurry
16916
16917
  * @license MIT
16917
16918
  */
@@ -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
- * @version 1.1.0
17089
+ * @version 2.0.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
- * @version 1.1.0
17151
+ * @version 2.0.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;
17660
- };
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();
17176
+ clearTimeout(timeout);
17177
+ content.style.display = 'none';
17178
+ trigger.removeAttribute('aria-describedby');
17678
17179
  };
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);
17237
+ items[currentIndex].focus();
17935
17238
  break;
17936
- case 'Enter':
17937
- case ' ':
17938
- keyEvent.preventDefault();
17939
- if (items[currentIndex]) {
17940
- items[currentIndex].click();
17941
- }
17942
- 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
 
@@ -18041,7 +17263,7 @@ var index$3 = /*#__PURE__*/Object.freeze({
18041
17263
  * @sc4rfurryx/proteusjs/perf
18042
17264
  * Performance guardrails and CWV-friendly patterns
18043
17265
  *
18044
- * @version 1.1.0
17266
+ * @version 2.0.0
18045
17267
  * @author sc4rfurry
18046
17268
  * @license MIT
18047
17269
  */
@@ -18303,13 +17525,13 @@ var index$1 = /*#__PURE__*/Object.freeze({
18303
17525
  * ProteusJS - Native-first Web Development Primitives
18304
17526
  * Shape-shifting responsive design that adapts like the sea god himself
18305
17527
  *
18306
- * @version 1.1.0
17528
+ * @version 2.0.0
18307
17529
  * @author sc4rfurry
18308
17530
  * @license MIT
18309
17531
  */
18310
17532
  // Core exports (legacy compatibility)
18311
17533
  // Constants
18312
- const VERSION = '1.1.0';
17534
+ const VERSION = '2.0.0';
18313
17535
  const LIBRARY_NAME = 'ProteusJS';
18314
17536
 
18315
17537
  export { LIBRARY_NAME, ProteusJS, VERSION, index$5 as a11yAudit, index$3 as a11yPrimitives, index$d as anchor, index$9 as container, ProteusJS as default, isSupported$1 as isSupported, index$1 as perf, index$b as popover, index$f as scroll, index$h as transitions, index$7 as typography, version };