pulse-js-framework 1.10.0 → 1.10.3

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 (37) hide show
  1. package/compiler/parser/_extract.js +393 -0
  2. package/compiler/parser/blocks.js +361 -0
  3. package/compiler/parser/core.js +306 -0
  4. package/compiler/parser/expressions.js +386 -0
  5. package/compiler/parser/imports.js +108 -0
  6. package/compiler/parser/index.js +47 -0
  7. package/compiler/parser/state.js +155 -0
  8. package/compiler/parser/style.js +445 -0
  9. package/compiler/parser/view.js +632 -0
  10. package/compiler/parser.js +15 -2372
  11. package/compiler/parser.js.original +2376 -0
  12. package/package.json +2 -1
  13. package/runtime/a11y/announcements.js +213 -0
  14. package/runtime/a11y/contrast.js +125 -0
  15. package/runtime/a11y/focus.js +412 -0
  16. package/runtime/a11y/index.js +35 -0
  17. package/runtime/a11y/preferences.js +121 -0
  18. package/runtime/a11y/utils.js +164 -0
  19. package/runtime/a11y/validation.js +258 -0
  20. package/runtime/a11y/widgets.js +545 -0
  21. package/runtime/a11y.js +15 -1840
  22. package/runtime/a11y.js.original +1844 -0
  23. package/runtime/graphql/cache.js +69 -0
  24. package/runtime/graphql/client.js +563 -0
  25. package/runtime/graphql/hooks.js +492 -0
  26. package/runtime/graphql/index.js +62 -0
  27. package/runtime/graphql/subscriptions.js +241 -0
  28. package/runtime/graphql.js +12 -1322
  29. package/runtime/graphql.js.original +1326 -0
  30. package/runtime/router/core.js +956 -0
  31. package/runtime/router/guards.js +90 -0
  32. package/runtime/router/history.js +204 -0
  33. package/runtime/router/index.js +36 -0
  34. package/runtime/router/lazy.js +180 -0
  35. package/runtime/router/utils.js +226 -0
  36. package/runtime/router.js +12 -1600
  37. package/runtime/router.js.original +1605 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Pulse A11y - ARIA Widgets
3
+ *
4
+ * ARIA widget implementations (modal, tabs, accordion, etc.)
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/widgets
7
+ */
8
+
9
+ import { pulse, effect } from '../pulse.js';
10
+ import { generateId, makeInert } from './utils.js';
11
+ import { trapFocus, onEscapeKey, createRovingTabindex } from './focus.js';
12
+
13
+ // =============================================================================
14
+ // ARIA HELPERS
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Set multiple ARIA attributes on an element
19
+ * @param {HTMLElement} element - Target element
20
+ * @param {object} attrs - ARIA attributes (without 'aria-' prefix)
21
+ */
22
+ export function setAriaAttributes(element, attrs) {
23
+ Object.entries(attrs).forEach(([key, value]) => {
24
+ if (value === null || value === undefined) {
25
+ element.removeAttribute(`aria-${key}`);
26
+ } else {
27
+ element.setAttribute(`aria-${key}`, String(value));
28
+ }
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Create an ARIA-compliant disclosure widget
34
+ * @param {HTMLElement} trigger - Button that toggles disclosure
35
+ * @param {HTMLElement} content - Content to show/hide
36
+ * @param {object} options - Options
37
+ * @returns {object} Control object with toggle, open, close methods
38
+ */
39
+ export function createDisclosure(trigger, content, options = {}) {
40
+ const { defaultOpen = false, onToggle = null } = options;
41
+
42
+ const expanded = pulse(defaultOpen);
43
+ const id = content.id || `pulse-disclosure-${Date.now()}`;
44
+
45
+ content.id = id;
46
+ trigger.setAttribute('aria-controls', id);
47
+ trigger.setAttribute('aria-expanded', String(expanded.get()));
48
+
49
+ // Update visibility
50
+ effect(() => {
51
+ const isOpen = expanded.get();
52
+ trigger.setAttribute('aria-expanded', String(isOpen));
53
+ content.hidden = !isOpen;
54
+ if (onToggle) onToggle(isOpen);
55
+ });
56
+
57
+ // Handle click
58
+ trigger.addEventListener('click', () => {
59
+ expanded.update(v => !v);
60
+ });
61
+
62
+ // Handle keyboard
63
+ trigger.addEventListener('keydown', (e) => {
64
+ if (e.key === 'Enter' || e.key === ' ') {
65
+ e.preventDefault();
66
+ expanded.update(v => !v);
67
+ }
68
+ });
69
+
70
+ return {
71
+ expanded,
72
+ toggle: () => expanded.update(v => !v),
73
+ open: () => expanded.set(true),
74
+ close: () => expanded.set(false)
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Create ARIA-compliant tabs
80
+ * @param {HTMLElement} tablist - Container with role="tablist"
81
+ * @param {object} options - Options
82
+ * @returns {object} Control object
83
+ */
84
+ export function createTabs(tablist, options = {}) {
85
+ const { defaultIndex = 0, orientation = 'horizontal', onSelect = null } = options;
86
+
87
+ const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
88
+ const panels = tabs.map(tab => {
89
+ const panelId = tab.getAttribute('aria-controls');
90
+ return document.getElementById(panelId);
91
+ });
92
+
93
+ const selectedIndex = pulse(defaultIndex);
94
+
95
+ tablist.setAttribute('aria-orientation', orientation);
96
+
97
+ // Update selection
98
+ effect(() => {
99
+ const index = selectedIndex.get();
100
+
101
+ tabs.forEach((tab, i) => {
102
+ const isSelected = i === index;
103
+ tab.setAttribute('aria-selected', String(isSelected));
104
+ tab.setAttribute('tabindex', isSelected ? '0' : '-1');
105
+ });
106
+
107
+ panels.forEach((panel, i) => {
108
+ if (panel) {
109
+ panel.hidden = i !== index;
110
+ }
111
+ });
112
+
113
+ if (onSelect) onSelect(index);
114
+ });
115
+
116
+ // Handle click
117
+ tabs.forEach((tab, i) => {
118
+ tab.addEventListener('click', () => {
119
+ selectedIndex.set(i);
120
+ tab.focus();
121
+ });
122
+ });
123
+
124
+ // Handle keyboard navigation
125
+ tablist.addEventListener('keydown', (e) => {
126
+ const currentIndex = selectedIndex.get();
127
+ let newIndex = currentIndex;
128
+
129
+ const isHorizontal = orientation === 'horizontal';
130
+ const prevKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';
131
+ const nextKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';
132
+
133
+ switch (e.key) {
134
+ case prevKey:
135
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
136
+ break;
137
+ case nextKey:
138
+ newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
139
+ break;
140
+ case 'Home':
141
+ newIndex = 0;
142
+ break;
143
+ case 'End':
144
+ newIndex = tabs.length - 1;
145
+ break;
146
+ default:
147
+ return;
148
+ }
149
+
150
+ e.preventDefault();
151
+ selectedIndex.set(newIndex);
152
+ tabs[newIndex].focus();
153
+ });
154
+
155
+ return {
156
+ selectedIndex,
157
+ select: (index) => selectedIndex.set(index),
158
+ tabs,
159
+ panels
160
+ };
161
+ }
162
+ // =============================================================================
163
+ // ARIA WIDGETS
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Create an accessible modal dialog
168
+ * Composes trapFocus, onEscapeKey, and proper ARIA attributes
169
+ * @param {HTMLElement} dialog - Dialog element
170
+ * @param {object} options - Options
171
+ * @param {HTMLElement} options.triggerElement - Element that triggered the dialog
172
+ * @param {string} options.labelledBy - ID of element labeling the dialog
173
+ * @param {string} options.describedBy - ID of element describing the dialog
174
+ * @param {HTMLElement} options.initialFocus - Element to focus initially
175
+ * @param {Function} options.onClose - Callback when dialog should close
176
+ * @param {boolean} options.closeOnBackdropClick - Close on backdrop click (default: true)
177
+ * @param {boolean} options.inertBackground - Make background inert (default: true)
178
+ * @returns {object} Control object with open, close methods and isOpen pulse
179
+ */
180
+ export function createModal(dialog, options = {}) {
181
+ const {
182
+ labelledBy = null,
183
+ describedBy = null,
184
+ initialFocus = null,
185
+ onClose = null,
186
+ closeOnBackdropClick = true,
187
+ inertBackground = true
188
+ } = options;
189
+
190
+ const isOpen = pulse(false);
191
+ let releaseFocusTrap = null;
192
+ let removeEscapeHandler = null;
193
+ let restoreInertFns = null;
194
+ let backdropHandler = null;
195
+
196
+ // Set ARIA attributes
197
+ dialog.setAttribute('role', 'dialog');
198
+ dialog.setAttribute('aria-modal', 'true');
199
+ if (labelledBy) dialog.setAttribute('aria-labelledby', labelledBy);
200
+ if (describedBy) dialog.setAttribute('aria-describedby', describedBy);
201
+
202
+ const open = () => {
203
+ if (isOpen.get()) return;
204
+
205
+ dialog.hidden = false;
206
+ isOpen.set(true);
207
+
208
+ // Make background inert
209
+ if (inertBackground && typeof document !== 'undefined') {
210
+ const siblings = Array.from(document.body.children)
211
+ .filter(el => el !== dialog && !el.hasAttribute('inert'));
212
+ restoreInertFns = siblings.map(el => makeInert(el));
213
+ }
214
+
215
+ // Trap focus
216
+ releaseFocusTrap = trapFocus(dialog, {
217
+ autoFocus: true,
218
+ returnFocus: true,
219
+ initialFocus
220
+ });
221
+
222
+ // Handle escape key
223
+ removeEscapeHandler = onEscapeKey(dialog, close);
224
+
225
+ // Handle backdrop click
226
+ if (closeOnBackdropClick) {
227
+ backdropHandler = (e) => {
228
+ if (e.target === dialog) close();
229
+ };
230
+ dialog.addEventListener('click', backdropHandler);
231
+ }
232
+
233
+ // Announce to screen readers
234
+ announce('Dialog opened');
235
+ };
236
+
237
+ const close = () => {
238
+ if (!isOpen.get()) return;
239
+
240
+ dialog.hidden = true;
241
+ isOpen.set(false);
242
+
243
+ // Clean up
244
+ if (releaseFocusTrap) {
245
+ releaseFocusTrap();
246
+ releaseFocusTrap = null;
247
+ }
248
+ if (removeEscapeHandler) {
249
+ removeEscapeHandler();
250
+ removeEscapeHandler = null;
251
+ }
252
+ if (restoreInertFns) {
253
+ restoreInertFns.forEach(restore => restore());
254
+ restoreInertFns = null;
255
+ }
256
+ if (backdropHandler) {
257
+ dialog.removeEventListener('click', backdropHandler);
258
+ backdropHandler = null;
259
+ }
260
+
261
+ if (onClose) onClose();
262
+ announce('Dialog closed');
263
+ };
264
+
265
+ return { isOpen, open, close };
266
+ }
267
+
268
+ /**
269
+ * Create an accessible tooltip
270
+ * Manages aria-describedby and visibility
271
+ * @param {HTMLElement} trigger - Element that triggers tooltip
272
+ * @param {HTMLElement} tooltip - Tooltip element
273
+ * @param {object} options - Options
274
+ * @param {number} options.showDelay - Delay before showing (ms, default: 500)
275
+ * @param {number} options.hideDelay - Delay before hiding (ms, default: 100)
276
+ * @returns {object} Control object with show, hide methods and isVisible pulse
277
+ */
278
+ export function createTooltip(trigger, tooltip, options = {}) {
279
+ const {
280
+ showDelay = 500,
281
+ hideDelay = 100
282
+ } = options;
283
+
284
+ const isVisible = pulse(false);
285
+ let showTimer = null;
286
+ let hideTimer = null;
287
+
288
+ // Generate ID if needed
289
+ const tooltipId = tooltip.id || generateId('tooltip');
290
+ tooltip.id = tooltipId;
291
+
292
+ // Set ARIA attributes
293
+ tooltip.setAttribute('role', 'tooltip');
294
+ trigger.setAttribute('aria-describedby', tooltipId);
295
+ tooltip.hidden = true;
296
+
297
+ const show = () => {
298
+ clearTimeout(hideTimer);
299
+ showTimer = setTimeout(() => {
300
+ tooltip.hidden = false;
301
+ isVisible.set(true);
302
+ }, showDelay);
303
+ };
304
+
305
+ const hide = () => {
306
+ clearTimeout(showTimer);
307
+ hideTimer = setTimeout(() => {
308
+ tooltip.hidden = true;
309
+ isVisible.set(false);
310
+ }, hideDelay);
311
+ };
312
+
313
+ const showImmediate = () => {
314
+ clearTimeout(hideTimer);
315
+ clearTimeout(showTimer);
316
+ tooltip.hidden = false;
317
+ isVisible.set(true);
318
+ };
319
+
320
+ const hideImmediate = () => {
321
+ clearTimeout(hideTimer);
322
+ clearTimeout(showTimer);
323
+ tooltip.hidden = true;
324
+ isVisible.set(false);
325
+ };
326
+
327
+ const handleEscapeKey = (e) => {
328
+ if (e.key === 'Escape') hideImmediate();
329
+ };
330
+
331
+ // Event listeners
332
+ trigger.addEventListener('mouseenter', show);
333
+ trigger.addEventListener('mouseleave', hide);
334
+ trigger.addEventListener('focus', showImmediate);
335
+ trigger.addEventListener('blur', hideImmediate);
336
+ trigger.addEventListener('keydown', handleEscapeKey);
337
+
338
+ const cleanup = () => {
339
+ clearTimeout(showTimer);
340
+ clearTimeout(hideTimer);
341
+ trigger.removeEventListener('mouseenter', show);
342
+ trigger.removeEventListener('mouseleave', hide);
343
+ trigger.removeEventListener('focus', showImmediate);
344
+ trigger.removeEventListener('blur', hideImmediate);
345
+ trigger.removeEventListener('keydown', handleEscapeKey);
346
+ trigger.removeAttribute('aria-describedby');
347
+ };
348
+
349
+ return { isVisible, show: showImmediate, hide: hideImmediate, cleanup };
350
+ }
351
+
352
+ /**
353
+ * Create an accessible accordion (composed of disclosures)
354
+ * @param {HTMLElement} container - Accordion container
355
+ * @param {object} options - Options
356
+ * @param {string} options.triggerSelector - Selector for accordion triggers
357
+ * @param {string} options.panelSelector - Selector for accordion panels
358
+ * @param {boolean} options.allowMultiple - Allow multiple panels open (default: false)
359
+ * @param {number} options.defaultOpen - Index of initially open panel (-1 for none)
360
+ * @param {Function} options.onToggle - Callback (index, isOpen) => void
361
+ * @returns {object} Control object
362
+ */
363
+ export function createAccordion(container, options = {}) {
364
+ const {
365
+ triggerSelector = '[data-accordion-trigger]',
366
+ panelSelector = '[data-accordion-panel]',
367
+ allowMultiple = false,
368
+ defaultOpen = -1,
369
+ onToggle = null
370
+ } = options;
371
+
372
+ const triggers = Array.from(container.querySelectorAll(triggerSelector));
373
+ const panels = Array.from(container.querySelectorAll(panelSelector));
374
+ const disclosures = [];
375
+ const openIndices = pulse(defaultOpen >= 0 ? [defaultOpen] : []);
376
+
377
+ triggers.forEach((trigger, index) => {
378
+ const panel = panels[index];
379
+ if (!panel) return;
380
+
381
+ const disclosure = createDisclosure(trigger, panel, {
382
+ defaultOpen: index === defaultOpen,
383
+ onToggle: (isExpanded) => {
384
+ if (isExpanded) {
385
+ if (allowMultiple) {
386
+ openIndices.update(arr => arr.includes(index) ? arr : [...arr, index]);
387
+ } else {
388
+ // Close other panels
389
+ disclosures.forEach((d, i) => {
390
+ if (i !== index && d.expanded.get()) d.close();
391
+ });
392
+ openIndices.set([index]);
393
+ }
394
+ } else {
395
+ openIndices.update(arr => arr.filter(i => i !== index));
396
+ }
397
+ if (onToggle) onToggle(index, isExpanded);
398
+ }
399
+ });
400
+
401
+ disclosures.push(disclosure);
402
+ });
403
+
404
+ return {
405
+ openIndices,
406
+ disclosures,
407
+ openAll: () => {
408
+ if (allowMultiple) {
409
+ disclosures.forEach(d => d.open());
410
+ }
411
+ },
412
+ closeAll: () => {
413
+ disclosures.forEach(d => d.close());
414
+ },
415
+ open: (index) => {
416
+ if (disclosures[index]) disclosures[index].open();
417
+ },
418
+ close: (index) => {
419
+ if (disclosures[index]) disclosures[index].close();
420
+ },
421
+ toggle: (index) => {
422
+ if (disclosures[index]) disclosures[index].toggle();
423
+ }
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Create an accessible dropdown menu
429
+ * @param {HTMLElement} button - Menu button
430
+ * @param {HTMLElement} menu - Menu container
431
+ * @param {object} options - Options
432
+ * @param {string} options.itemSelector - Selector for menu items (default: '[role="menuitem"]')
433
+ * @param {Function} options.onSelect - Callback when item is selected
434
+ * @param {boolean} options.closeOnSelect - Close menu on item selection (default: true)
435
+ * @returns {object} Control object with open, close, toggle methods and isOpen pulse
436
+ */
437
+ export function createMenu(button, menu, options = {}) {
438
+ const {
439
+ itemSelector = '[role="menuitem"]',
440
+ onSelect = null,
441
+ closeOnSelect = true
442
+ } = options;
443
+
444
+ const isOpen = pulse(false);
445
+ const menuId = menu.id || generateId('menu');
446
+ let rovingCleanup = null;
447
+ let documentClickHandler = null;
448
+
449
+ // Set ARIA attributes
450
+ menu.id = menuId;
451
+ menu.setAttribute('role', 'menu');
452
+ button.setAttribute('aria-haspopup', 'menu');
453
+ button.setAttribute('aria-controls', menuId);
454
+ button.setAttribute('aria-expanded', 'false');
455
+ menu.hidden = true;
456
+
457
+ const open = () => {
458
+ if (isOpen.get()) return;
459
+
460
+ menu.hidden = false;
461
+ button.setAttribute('aria-expanded', 'true');
462
+ isOpen.set(true);
463
+
464
+ // Setup roving tabindex for menu items
465
+ rovingCleanup = createRovingTabindex(menu, {
466
+ selector: itemSelector,
467
+ orientation: 'vertical',
468
+ onSelect: (el, index) => {
469
+ if (onSelect) onSelect(el, index);
470
+ if (closeOnSelect) close();
471
+ }
472
+ });
473
+
474
+ // Focus first item
475
+ const firstItem = menu.querySelector(itemSelector);
476
+ if (firstItem) firstItem.focus();
477
+
478
+ // Close on click outside (delay to avoid immediate close)
479
+ setTimeout(() => {
480
+ documentClickHandler = (e) => {
481
+ if (!button.contains(e.target) && !menu.contains(e.target)) {
482
+ close();
483
+ }
484
+ };
485
+ document.addEventListener('click', documentClickHandler);
486
+ }, 0);
487
+ };
488
+
489
+ const close = () => {
490
+ if (!isOpen.get()) return;
491
+
492
+ menu.hidden = true;
493
+ button.setAttribute('aria-expanded', 'false');
494
+ isOpen.set(false);
495
+
496
+ if (rovingCleanup) {
497
+ rovingCleanup();
498
+ rovingCleanup = null;
499
+ }
500
+
501
+ if (documentClickHandler) {
502
+ document.removeEventListener('click', documentClickHandler);
503
+ documentClickHandler = null;
504
+ }
505
+
506
+ button.focus();
507
+ };
508
+
509
+ const toggle = () => isOpen.get() ? close() : open();
510
+
511
+ // Button click
512
+ button.addEventListener('click', toggle);
513
+
514
+ // Keyboard navigation on button
515
+ const handleButtonKeyDown = (e) => {
516
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
517
+ e.preventDefault();
518
+ open();
519
+ }
520
+ };
521
+ button.addEventListener('keydown', handleButtonKeyDown);
522
+
523
+ // Close on escape
524
+ const handleMenuKeyDown = (e) => {
525
+ if (e.key === 'Escape') {
526
+ e.stopPropagation();
527
+ close();
528
+ }
529
+ };
530
+ menu.addEventListener('keydown', handleMenuKeyDown);
531
+
532
+ const cleanup = () => {
533
+ button.removeEventListener('click', toggle);
534
+ button.removeEventListener('keydown', handleButtonKeyDown);
535
+ menu.removeEventListener('keydown', handleMenuKeyDown);
536
+ if (documentClickHandler) {
537
+ document.removeEventListener('click', documentClickHandler);
538
+ }
539
+ if (rovingCleanup) {
540
+ rovingCleanup();
541
+ }
542
+ };
543
+
544
+ return { isOpen, open, close, toggle, cleanup };
545
+ }