@sc4rfurryx/proteusjs 1.0.0 → 1.1.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +331 -77
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/react.d.ts +139 -0
  5. package/dist/adapters/react.esm.js +848 -0
  6. package/dist/adapters/react.esm.js.map +1 -0
  7. package/dist/adapters/svelte.d.ts +181 -0
  8. package/dist/adapters/svelte.esm.js +908 -0
  9. package/dist/adapters/svelte.esm.js.map +1 -0
  10. package/dist/adapters/vue.d.ts +205 -0
  11. package/dist/adapters/vue.esm.js +872 -0
  12. package/dist/adapters/vue.esm.js.map +1 -0
  13. package/dist/modules/a11y-audit.d.ts +39 -0
  14. package/dist/modules/a11y-audit.esm.js +509 -0
  15. package/dist/modules/a11y-audit.esm.js.map +1 -0
  16. package/dist/modules/a11y-primitives.d.ts +69 -0
  17. package/dist/modules/a11y-primitives.esm.js +445 -0
  18. package/dist/modules/a11y-primitives.esm.js.map +1 -0
  19. package/dist/modules/anchor.d.ts +29 -0
  20. package/dist/modules/anchor.esm.js +218 -0
  21. package/dist/modules/anchor.esm.js.map +1 -0
  22. package/dist/modules/container.d.ts +60 -0
  23. package/dist/modules/container.esm.js +194 -0
  24. package/dist/modules/container.esm.js.map +1 -0
  25. package/dist/modules/perf.d.ts +82 -0
  26. package/dist/modules/perf.esm.js +257 -0
  27. package/dist/modules/perf.esm.js.map +1 -0
  28. package/dist/modules/popover.d.ts +33 -0
  29. package/dist/modules/popover.esm.js +191 -0
  30. package/dist/modules/popover.esm.js.map +1 -0
  31. package/dist/modules/scroll.d.ts +43 -0
  32. package/dist/modules/scroll.esm.js +195 -0
  33. package/dist/modules/scroll.esm.js.map +1 -0
  34. package/dist/modules/transitions.d.ts +35 -0
  35. package/dist/modules/transitions.esm.js +120 -0
  36. package/dist/modules/transitions.esm.js.map +1 -0
  37. package/dist/modules/typography.d.ts +72 -0
  38. package/dist/modules/typography.esm.js +168 -0
  39. package/dist/modules/typography.esm.js.map +1 -0
  40. package/dist/proteus.cjs.js +2332 -12
  41. package/dist/proteus.cjs.js.map +1 -1
  42. package/dist/proteus.d.ts +561 -12
  43. package/dist/proteus.esm.js +2323 -12
  44. package/dist/proteus.esm.js.map +1 -1
  45. package/dist/proteus.esm.min.js +3 -3
  46. package/dist/proteus.esm.min.js.map +1 -1
  47. package/dist/proteus.js +2332 -12
  48. package/dist/proteus.js.map +1 -1
  49. package/dist/proteus.min.js +3 -3
  50. package/dist/proteus.min.js.map +1 -1
  51. package/package.json +61 -4
  52. package/src/adapters/react.ts +264 -0
  53. package/src/adapters/svelte.ts +321 -0
  54. package/src/adapters/vue.ts +268 -0
  55. package/src/index.ts +33 -6
  56. package/src/modules/a11y-audit/index.ts +608 -0
  57. package/src/modules/a11y-primitives/index.ts +554 -0
  58. package/src/modules/anchor/index.ts +257 -0
  59. package/src/modules/container/index.ts +230 -0
  60. package/src/modules/perf/index.ts +291 -0
  61. package/src/modules/popover/index.ts +238 -0
  62. package/src/modules/scroll/index.ts +251 -0
  63. package/src/modules/transitions/index.ts +145 -0
  64. package/src/modules/typography/index.ts +239 -0
  65. package/src/utils/version.ts +1 -1
@@ -0,0 +1,554 @@
1
+ /**
2
+ * @sc4rfurryx/proteusjs/a11y-primitives
3
+ * Headless accessibility patterns (no styles)
4
+ *
5
+ * @version 1.1.0
6
+ * @author sc4rfurry
7
+ * @license MIT
8
+ */
9
+
10
+ export interface Controller {
11
+ destroy(): void;
12
+ }
13
+
14
+ export interface DialogOptions {
15
+ modal?: boolean;
16
+ restoreFocus?: boolean;
17
+ }
18
+
19
+ export interface TooltipOptions {
20
+ delay?: number;
21
+ }
22
+
23
+ export interface ComboboxOptions {
24
+ multiselect?: boolean;
25
+ filtering?: (query: string) => Promise<unknown[]> | unknown[];
26
+ }
27
+
28
+ export interface ListboxOptions {
29
+ multiselect?: boolean;
30
+ }
31
+
32
+ export interface FocusTrapController {
33
+ activate(): void;
34
+ deactivate(): void;
35
+ }
36
+
37
+ /**
38
+ * Dialog primitive with proper ARIA and focus management
39
+ */
40
+ export function dialog(root: Element | string, opts: DialogOptions = {}): Controller {
41
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
42
+ if (!rootEl) throw new Error('Dialog root element not found');
43
+
44
+ const { modal = true, restoreFocus = true } = opts;
45
+ let previousFocus: Element | null = null;
46
+ let isOpen = false;
47
+
48
+ const setup = () => {
49
+ rootEl.setAttribute('role', 'dialog');
50
+ if (modal) {
51
+ rootEl.setAttribute('aria-modal', 'true');
52
+ }
53
+
54
+ // Ensure dialog is initially hidden
55
+ if (!rootEl.hasAttribute('hidden')) {
56
+ rootEl.setAttribute('hidden', '');
57
+ }
58
+ };
59
+
60
+ const open = () => {
61
+ if (restoreFocus) {
62
+ previousFocus = document.activeElement;
63
+ }
64
+
65
+ rootEl.removeAttribute('hidden');
66
+ isOpen = true;
67
+
68
+ // Focus first focusable element
69
+ const focusable = rootEl.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
70
+ if (focusable) {
71
+ (focusable as HTMLElement).focus();
72
+ }
73
+ };
74
+
75
+ const close = () => {
76
+ rootEl.setAttribute('hidden', '');
77
+ isOpen = false;
78
+
79
+ if (restoreFocus && previousFocus) {
80
+ (previousFocus as HTMLElement).focus();
81
+ }
82
+ };
83
+
84
+ const handleKeyDown = (e: KeyboardEvent) => {
85
+ if (e.key === 'Escape' && isOpen) {
86
+ close();
87
+ }
88
+ };
89
+
90
+ setup();
91
+ document.addEventListener('keydown', handleKeyDown);
92
+
93
+ return {
94
+ destroy: () => {
95
+ document.removeEventListener('keydown', handleKeyDown);
96
+ if (isOpen) close();
97
+ }
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Tooltip primitive with delay and proper ARIA
103
+ */
104
+ export function tooltip(trigger: Element | string, panel: Element | string, opts: TooltipOptions = {}): Controller {
105
+ const triggerEl = typeof trigger === 'string' ? document.querySelector(trigger) : trigger;
106
+ const panelEl = typeof panel === 'string' ? document.querySelector(panel) : panel;
107
+
108
+ if (!triggerEl || !panelEl) {
109
+ throw new Error('Both trigger and panel elements must exist');
110
+ }
111
+
112
+ const { delay = 500 } = opts;
113
+ let timeoutId: number | null = null;
114
+ let isVisible = false;
115
+
116
+ const setup = () => {
117
+ const tooltipId = panelEl.id || `tooltip-${Math.random().toString(36).substring(2, 11)}`;
118
+ panelEl.id = tooltipId;
119
+ panelEl.setAttribute('role', 'tooltip');
120
+ triggerEl.setAttribute('aria-describedby', tooltipId);
121
+
122
+ // Initially hidden
123
+ (panelEl as HTMLElement).style.display = 'none';
124
+ };
125
+
126
+ const show = () => {
127
+ if (isVisible) return;
128
+ (panelEl as HTMLElement).style.display = 'block';
129
+ isVisible = true;
130
+ };
131
+
132
+ const hide = () => {
133
+ if (!isVisible) return;
134
+ (panelEl as HTMLElement).style.display = 'none';
135
+ isVisible = false;
136
+ };
137
+
138
+ const handleMouseEnter = () => {
139
+ if (timeoutId) clearTimeout(timeoutId);
140
+ timeoutId = window.setTimeout(show, delay);
141
+ };
142
+
143
+ const handleMouseLeave = () => {
144
+ if (timeoutId) {
145
+ clearTimeout(timeoutId);
146
+ timeoutId = null;
147
+ }
148
+ hide();
149
+ };
150
+
151
+ const handleFocus = () => {
152
+ show();
153
+ };
154
+
155
+ const handleBlur = () => {
156
+ hide();
157
+ };
158
+
159
+ setup();
160
+ triggerEl.addEventListener('mouseenter', handleMouseEnter);
161
+ triggerEl.addEventListener('mouseleave', handleMouseLeave);
162
+ triggerEl.addEventListener('focus', handleFocus);
163
+ triggerEl.addEventListener('blur', handleBlur);
164
+
165
+ return {
166
+ destroy: () => {
167
+ if (timeoutId) clearTimeout(timeoutId);
168
+ triggerEl.removeEventListener('mouseenter', handleMouseEnter);
169
+ triggerEl.removeEventListener('mouseleave', handleMouseLeave);
170
+ triggerEl.removeEventListener('focus', handleFocus);
171
+ triggerEl.removeEventListener('blur', handleBlur);
172
+ hide();
173
+ }
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Listbox primitive with keyboard navigation
179
+ */
180
+ export function listbox(root: Element | string, opts: ListboxOptions = {}): Controller {
181
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
182
+ if (!rootEl) throw new Error('Listbox root element not found');
183
+
184
+ const { multiselect = false } = opts;
185
+ let currentIndex = -1;
186
+
187
+ const setup = () => {
188
+ rootEl.setAttribute('role', 'listbox');
189
+ if (multiselect) {
190
+ rootEl.setAttribute('aria-multiselectable', 'true');
191
+ }
192
+
193
+ // Set up options
194
+ const options = rootEl.querySelectorAll('[role="option"]');
195
+ options.forEach((option, _index) => {
196
+ option.setAttribute('aria-selected', 'false');
197
+ option.setAttribute('tabindex', '-1');
198
+ });
199
+
200
+ if (options.length > 0) {
201
+ options[0]?.setAttribute('tabindex', '0');
202
+ currentIndex = 0;
203
+ }
204
+ };
205
+
206
+ const getOptions = () => rootEl.querySelectorAll('[role="option"]');
207
+
208
+ const setCurrentIndex = (index: number) => {
209
+ const options = getOptions();
210
+ if (index < 0 || index >= options.length) return;
211
+
212
+ // Remove tabindex from all options
213
+ options.forEach(option => option.setAttribute('tabindex', '-1'));
214
+
215
+ // Set current option
216
+ currentIndex = index;
217
+ options[currentIndex]?.setAttribute('tabindex', '0');
218
+ (options[currentIndex] as HTMLElement)?.focus();
219
+ };
220
+
221
+ const selectOption = (index: number) => {
222
+ const options = getOptions();
223
+ if (index < 0 || index >= options.length) return;
224
+
225
+ if (multiselect) {
226
+ const isSelected = options[index]?.getAttribute('aria-selected') === 'true';
227
+ options[index]?.setAttribute('aria-selected', (!isSelected).toString());
228
+ } else {
229
+ // Single select - clear all others
230
+ options.forEach(option => option.setAttribute('aria-selected', 'false'));
231
+ options[index]?.setAttribute('aria-selected', 'true');
232
+ }
233
+ };
234
+
235
+ const handleKeyDown = (e: Event) => {
236
+ const keyEvent = e as KeyboardEvent;
237
+ const options = getOptions();
238
+
239
+ switch (keyEvent.key) {
240
+ case 'ArrowDown':
241
+ keyEvent.preventDefault();
242
+ setCurrentIndex(Math.min(currentIndex + 1, options.length - 1));
243
+ break;
244
+ case 'ArrowUp':
245
+ keyEvent.preventDefault();
246
+ setCurrentIndex(Math.max(currentIndex - 1, 0));
247
+ break;
248
+ case 'Home':
249
+ keyEvent.preventDefault();
250
+ setCurrentIndex(0);
251
+ break;
252
+ case 'End':
253
+ keyEvent.preventDefault();
254
+ setCurrentIndex(options.length - 1);
255
+ break;
256
+ case 'Enter':
257
+ case ' ':
258
+ keyEvent.preventDefault();
259
+ selectOption(currentIndex);
260
+ break;
261
+ }
262
+ };
263
+
264
+ const handleClick = (e: Event) => {
265
+ const target = e.target as Element;
266
+ const option = target.closest('[role="option"]');
267
+ if (!option) return;
268
+
269
+ const options = Array.from(getOptions());
270
+ const index = options.indexOf(option);
271
+ if (index >= 0) {
272
+ setCurrentIndex(index);
273
+ selectOption(index);
274
+ }
275
+ };
276
+
277
+ setup();
278
+ rootEl.addEventListener('keydown', handleKeyDown);
279
+ rootEl.addEventListener('click', handleClick);
280
+
281
+ return {
282
+ destroy: () => {
283
+ rootEl.removeEventListener('keydown', handleKeyDown);
284
+ rootEl.removeEventListener('click', handleClick);
285
+ }
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Combobox primitive with filtering and multiselect
291
+ */
292
+ export function combobox(
293
+ root: Element | string,
294
+ opts: ComboboxOptions = {}
295
+ ): Controller {
296
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
297
+ if (!rootEl) throw new Error('Combobox root element not found');
298
+
299
+ const { multiselect = false, filtering: _filtering } = opts;
300
+ let isOpen = false;
301
+
302
+ const setup = () => {
303
+ rootEl.setAttribute('role', 'combobox');
304
+ rootEl.setAttribute('aria-expanded', 'false');
305
+ if (multiselect) {
306
+ rootEl.setAttribute('aria-multiselectable', 'true');
307
+ }
308
+ };
309
+
310
+ const handleKeyDown = (e: Event) => {
311
+ const keyEvent = e as KeyboardEvent;
312
+
313
+ switch (keyEvent.key) {
314
+ case 'ArrowDown':
315
+ keyEvent.preventDefault();
316
+ if (!isOpen) {
317
+ isOpen = true;
318
+ rootEl.setAttribute('aria-expanded', 'true');
319
+ }
320
+ // Navigate options logic would go here
321
+ break;
322
+ case 'Escape':
323
+ keyEvent.preventDefault();
324
+ isOpen = false;
325
+ rootEl.setAttribute('aria-expanded', 'false');
326
+ break;
327
+ }
328
+ };
329
+
330
+ setup();
331
+ rootEl.addEventListener('keydown', handleKeyDown);
332
+
333
+ return {
334
+ destroy: () => {
335
+ rootEl.removeEventListener('keydown', handleKeyDown);
336
+ }
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Tabs primitive with keyboard navigation
342
+ */
343
+ export function tabs(root: Element | string): Controller {
344
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
345
+ if (!rootEl) throw new Error('Tabs root element not found');
346
+
347
+ let currentIndex = 0;
348
+
349
+ const setup = () => {
350
+ const tabList = rootEl.querySelector('[role="tablist"]');
351
+ const tabs = rootEl.querySelectorAll('[role="tab"]');
352
+ const panels = rootEl.querySelectorAll('[role="tabpanel"]');
353
+
354
+ if (!tabList) {
355
+ rootEl.setAttribute('role', 'tablist');
356
+ }
357
+
358
+ tabs.forEach((tab, index) => {
359
+ tab.setAttribute('tabindex', index === 0 ? '0' : '-1');
360
+ tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
361
+ });
362
+
363
+ panels.forEach((panel, index) => {
364
+ panel.setAttribute('hidden', index === 0 ? '' : 'true');
365
+ });
366
+ };
367
+
368
+ const handleKeyDown = (e: Event) => {
369
+ const keyEvent = e as KeyboardEvent;
370
+ const tabs = Array.from(rootEl.querySelectorAll('[role="tab"]'));
371
+
372
+ switch (keyEvent.key) {
373
+ case 'ArrowRight':
374
+ keyEvent.preventDefault();
375
+ currentIndex = (currentIndex + 1) % tabs.length;
376
+ activateTab(currentIndex);
377
+ break;
378
+ case 'ArrowLeft':
379
+ keyEvent.preventDefault();
380
+ currentIndex = currentIndex === 0 ? tabs.length - 1 : currentIndex - 1;
381
+ activateTab(currentIndex);
382
+ break;
383
+ }
384
+ };
385
+
386
+ const activateTab = (index: number) => {
387
+ const tabs = rootEl.querySelectorAll('[role="tab"]');
388
+ const panels = rootEl.querySelectorAll('[role="tabpanel"]');
389
+
390
+ tabs.forEach((tab, i) => {
391
+ tab.setAttribute('tabindex', i === index ? '0' : '-1');
392
+ tab.setAttribute('aria-selected', i === index ? 'true' : 'false');
393
+ if (i === index) {
394
+ (tab as HTMLElement).focus();
395
+ }
396
+ });
397
+
398
+ panels.forEach((panel, i) => {
399
+ if (i === index) {
400
+ panel.removeAttribute('hidden');
401
+ } else {
402
+ panel.setAttribute('hidden', 'true');
403
+ }
404
+ });
405
+ };
406
+
407
+ setup();
408
+ rootEl.addEventListener('keydown', handleKeyDown);
409
+
410
+ return {
411
+ destroy: () => {
412
+ rootEl.removeEventListener('keydown', handleKeyDown);
413
+ }
414
+ };
415
+ }
416
+
417
+ /**
418
+ * Menu primitive with keyboard navigation
419
+ */
420
+ export function menu(root: Element | string): Controller {
421
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
422
+ if (!rootEl) throw new Error('Menu root element not found');
423
+
424
+ let currentIndex = -1;
425
+
426
+ const setup = () => {
427
+ rootEl.setAttribute('role', 'menu');
428
+
429
+ const items = rootEl.querySelectorAll('[role="menuitem"]');
430
+ items.forEach((item, index) => {
431
+ item.setAttribute('tabindex', index === 0 ? '0' : '-1');
432
+ });
433
+
434
+ if (items.length > 0) {
435
+ currentIndex = 0;
436
+ }
437
+ };
438
+
439
+ const handleKeyDown = (e: Event) => {
440
+ const keyEvent = e as KeyboardEvent;
441
+ const items = Array.from(rootEl.querySelectorAll('[role="menuitem"]'));
442
+
443
+ switch (keyEvent.key) {
444
+ case 'ArrowDown':
445
+ keyEvent.preventDefault();
446
+ currentIndex = (currentIndex + 1) % items.length;
447
+ setCurrentItem(currentIndex);
448
+ break;
449
+ case 'ArrowUp':
450
+ keyEvent.preventDefault();
451
+ currentIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
452
+ setCurrentItem(currentIndex);
453
+ break;
454
+ case 'Enter':
455
+ case ' ':
456
+ keyEvent.preventDefault();
457
+ if (items[currentIndex]) {
458
+ (items[currentIndex] as HTMLElement).click();
459
+ }
460
+ break;
461
+ }
462
+ };
463
+
464
+ const setCurrentItem = (index: number) => {
465
+ const items = rootEl.querySelectorAll('[role="menuitem"]');
466
+ items.forEach((item, i) => {
467
+ item.setAttribute('tabindex', i === index ? '0' : '-1');
468
+ if (i === index) {
469
+ (item as HTMLElement).focus();
470
+ }
471
+ });
472
+ };
473
+
474
+ setup();
475
+ rootEl.addEventListener('keydown', handleKeyDown);
476
+
477
+ return {
478
+ destroy: () => {
479
+ rootEl.removeEventListener('keydown', handleKeyDown);
480
+ }
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Focus trap utility
486
+ */
487
+ export function focusTrap(root: Element | string): FocusTrapController {
488
+ const rootEl = typeof root === 'string' ? document.querySelector(root) : root;
489
+ if (!rootEl) throw new Error('Focus trap root element not found');
490
+
491
+ let isActive = false;
492
+ let focusableElements: Element[] = [];
493
+
494
+ const updateFocusableElements = () => {
495
+ const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
496
+ focusableElements = Array.from(rootEl.querySelectorAll(selector));
497
+ };
498
+
499
+ const handleKeyDown = (e: KeyboardEvent) => {
500
+ if (!isActive || e.key !== 'Tab') return;
501
+
502
+ updateFocusableElements();
503
+ if (focusableElements.length === 0) return;
504
+
505
+ const firstElement = focusableElements[0] as HTMLElement;
506
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
507
+
508
+ if (e.shiftKey) {
509
+ if (document.activeElement === firstElement) {
510
+ e.preventDefault();
511
+ lastElement.focus();
512
+ }
513
+ } else {
514
+ if (document.activeElement === lastElement) {
515
+ e.preventDefault();
516
+ firstElement.focus();
517
+ }
518
+ }
519
+ };
520
+
521
+ const activate = () => {
522
+ if (isActive) return;
523
+ isActive = true;
524
+ updateFocusableElements();
525
+
526
+ if (focusableElements.length > 0) {
527
+ (focusableElements[0] as HTMLElement).focus();
528
+ }
529
+
530
+ document.addEventListener('keydown', handleKeyDown);
531
+ };
532
+
533
+ const deactivate = () => {
534
+ if (!isActive) return;
535
+ isActive = false;
536
+ document.removeEventListener('keydown', handleKeyDown);
537
+ };
538
+
539
+ return {
540
+ activate,
541
+ deactivate
542
+ };
543
+ }
544
+
545
+ // Export all functions
546
+ export default {
547
+ dialog,
548
+ tooltip,
549
+ combobox,
550
+ listbox,
551
+ tabs,
552
+ menu,
553
+ focusTrap
554
+ };