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,412 @@
1
+ /**
2
+ * Pulse A11y - Focus Management
3
+ *
4
+ * Focus management, skip links, and keyboard navigation
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/focus
7
+ */
8
+
9
+ import { pulse, effect } from '../pulse.js';
10
+
11
+ // =============================================================================
12
+ // FOCUS MANAGEMENT
13
+ // =============================================================================
14
+
15
+ const focusStack = [];
16
+
17
+ /** Focusable element selector */
18
+ const FOCUSABLE_SELECTOR = [
19
+ 'a[href]',
20
+ 'button:not([disabled])',
21
+ 'input:not([disabled]):not([type="hidden"])',
22
+ 'select:not([disabled])',
23
+ 'textarea:not([disabled])',
24
+ '[tabindex]:not([tabindex="-1"])',
25
+ '[contenteditable="true"]',
26
+ 'audio[controls]',
27
+ 'video[controls]',
28
+ 'details > summary',
29
+ 'iframe'
30
+ ].join(', ');
31
+
32
+ /**
33
+ * Get all focusable elements within a container
34
+ * @param {HTMLElement} container - Container element
35
+ * @returns {HTMLElement[]} Array of focusable elements
36
+ */
37
+ export function getFocusableElements(container) {
38
+ if (!container) return [];
39
+ const elements = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
40
+ return elements.filter(el => {
41
+ // Check visibility
42
+ const style = getComputedStyle(el);
43
+ return style.display !== 'none' &&
44
+ style.visibility !== 'hidden' &&
45
+ el.offsetParent !== null;
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Focus the first focusable element in a container
51
+ * @param {HTMLElement} container - Container element
52
+ * @returns {HTMLElement|null} The focused element or null
53
+ */
54
+ export function focusFirst(container) {
55
+ const focusable = getFocusableElements(container);
56
+ if (focusable.length > 0) {
57
+ focusable[0].focus();
58
+ return focusable[0];
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Focus the last focusable element in a container
65
+ * @param {HTMLElement} container - Container element
66
+ * @returns {HTMLElement|null} The focused element or null
67
+ */
68
+ export function focusLast(container) {
69
+ const focusable = getFocusableElements(container);
70
+ if (focusable.length > 0) {
71
+ focusable[focusable.length - 1].focus();
72
+ return focusable[focusable.length - 1];
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Trap focus within a container (for modals, dialogs)
79
+ * @param {HTMLElement} container - Container to trap focus in
80
+ * @param {object} options - Options
81
+ * @param {boolean} options.autoFocus - Auto focus first element (default: true)
82
+ * @param {boolean} options.returnFocus - Return focus on release (default: true)
83
+ * @param {HTMLElement} options.initialFocus - Element to focus initially
84
+ * @returns {Function} Release function to remove trap
85
+ */
86
+ export function trapFocus(container, options = {}) {
87
+ const { autoFocus = true, returnFocus = true, initialFocus = null } = options;
88
+
89
+ if (!container) return () => {};
90
+
91
+ // Save current focus
92
+ const previouslyFocused = document.activeElement;
93
+ if (returnFocus) {
94
+ focusStack.push(previouslyFocused);
95
+ }
96
+
97
+ // Handle Tab key
98
+ const handleKeyDown = (e) => {
99
+ if (e.key !== 'Tab') return;
100
+
101
+ const focusable = getFocusableElements(container);
102
+ if (focusable.length === 0) {
103
+ e.preventDefault();
104
+ return;
105
+ }
106
+
107
+ const firstFocusable = focusable[0];
108
+ const lastFocusable = focusable[focusable.length - 1];
109
+
110
+ if (e.shiftKey) {
111
+ // Shift + Tab: going backwards
112
+ if (document.activeElement === firstFocusable) {
113
+ e.preventDefault();
114
+ lastFocusable.focus();
115
+ }
116
+ } else {
117
+ // Tab: going forwards
118
+ if (document.activeElement === lastFocusable) {
119
+ e.preventDefault();
120
+ firstFocusable.focus();
121
+ }
122
+ }
123
+ };
124
+
125
+ // Handle focus leaving container
126
+ const handleFocusOut = (e) => {
127
+ if (!container.contains(e.relatedTarget)) {
128
+ // Focus is leaving container, bring it back
129
+ const focusable = getFocusableElements(container);
130
+ if (focusable.length > 0) {
131
+ focusable[0].focus();
132
+ }
133
+ }
134
+ };
135
+
136
+ container.addEventListener('keydown', handleKeyDown);
137
+ container.addEventListener('focusout', handleFocusOut);
138
+
139
+ // Set initial focus
140
+ if (autoFocus) {
141
+ requestAnimationFrame(() => {
142
+ if (initialFocus && container.contains(initialFocus)) {
143
+ initialFocus.focus();
144
+ } else {
145
+ focusFirst(container);
146
+ }
147
+ });
148
+ }
149
+
150
+ // Return release function
151
+ return function releaseFocusTrap() {
152
+ container.removeEventListener('keydown', handleKeyDown);
153
+ container.removeEventListener('focusout', handleFocusOut);
154
+
155
+ if (returnFocus && focusStack.length > 0) {
156
+ const toFocus = focusStack.pop();
157
+ if (toFocus && typeof toFocus.focus === 'function') {
158
+ toFocus.focus();
159
+ }
160
+ }
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Save current focus to stack
166
+ */
167
+ export function saveFocus() {
168
+ focusStack.push(document.activeElement);
169
+ }
170
+
171
+ /**
172
+ * Restore focus from stack
173
+ */
174
+ export function restoreFocus() {
175
+ if (focusStack.length > 0) {
176
+ const element = focusStack.pop();
177
+ if (element && typeof element.focus === 'function') {
178
+ element.focus();
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Clear focus stack
185
+ */
186
+ export function clearFocusStack() {
187
+ focusStack.length = 0;
188
+ }
189
+
190
+ /**
191
+ * Add escape key handler for dismissing modals/dialogs
192
+ * @param {HTMLElement} container - Container element
193
+ * @param {Function} onEscape - Callback when escape is pressed
194
+ * @param {object} options - Options
195
+ * @param {boolean} options.stopPropagation - Stop event propagation (default: true)
196
+ * @returns {Function} Cleanup function to remove handler
197
+ */
198
+ export function onEscapeKey(container, onEscape, options = {}) {
199
+ const { stopPropagation = true } = options;
200
+
201
+ if (!container) return () => {};
202
+
203
+ const handleKeyDown = (e) => {
204
+ if (e.key === 'Escape' || e.key === 'Esc') {
205
+ if (stopPropagation) {
206
+ e.stopPropagation();
207
+ }
208
+ onEscape(e);
209
+ }
210
+ };
211
+
212
+ container.addEventListener('keydown', handleKeyDown);
213
+
214
+ return () => {
215
+ container.removeEventListener('keydown', handleKeyDown);
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Track whether the user is navigating with keyboard
221
+ * Useful for implementing :focus-visible behavior
222
+ * @returns {{ isKeyboardUser: object, cleanup: Function }} isKeyboardUser is a pulse
223
+ */
224
+ export function createFocusVisibleTracker() {
225
+ const isKeyboardUser = pulse(false);
226
+
227
+ if (typeof document === 'undefined') {
228
+ return { isKeyboardUser, cleanup: () => {} };
229
+ }
230
+
231
+ const handleKeyDown = (e) => {
232
+ if (e.key === 'Tab' || e.key === 'ArrowUp' || e.key === 'ArrowDown' ||
233
+ e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
234
+ isKeyboardUser.set(true);
235
+ }
236
+ };
237
+
238
+ const handleMouseDown = () => {
239
+ isKeyboardUser.set(false);
240
+ };
241
+
242
+ document.addEventListener('keydown', handleKeyDown, true);
243
+ document.addEventListener('mousedown', handleMouseDown, true);
244
+
245
+ return {
246
+ isKeyboardUser,
247
+ cleanup: () => {
248
+ document.removeEventListener('keydown', handleKeyDown, true);
249
+ document.removeEventListener('mousedown', handleMouseDown, true);
250
+ }
251
+ };
252
+ }
253
+
254
+ // =============================================================================
255
+ // SKIP LINKS
256
+ // =============================================================================
257
+
258
+ /**
259
+ * Create a skip link for keyboard navigation
260
+ * @param {string} targetId - ID of target element to skip to
261
+ * @param {string} text - Link text (default: 'Skip to main content')
262
+ * @param {object} options - Options
263
+ * @returns {HTMLElement} The skip link element
264
+ */
265
+ export function createSkipLink(targetId, text = 'Skip to main content', options = {}) {
266
+ const { className = 'pulse-skip-link' } = options;
267
+
268
+ const link = document.createElement('a');
269
+ link.href = `#${targetId}`;
270
+ link.textContent = text;
271
+ link.className = className;
272
+
273
+ // Visually hidden but focusable styles
274
+ Object.assign(link.style, {
275
+ position: 'absolute',
276
+ top: '-40px',
277
+ left: '0',
278
+ padding: '8px 16px',
279
+ background: '#000',
280
+ color: '#fff',
281
+ textDecoration: 'none',
282
+ zIndex: '10000',
283
+ transition: 'top 0.2s'
284
+ });
285
+
286
+ // Show on focus
287
+ link.addEventListener('focus', () => {
288
+ link.style.top = '0';
289
+ });
290
+
291
+ link.addEventListener('blur', () => {
292
+ link.style.top = '-40px';
293
+ });
294
+
295
+ // Handle click
296
+ link.addEventListener('click', (e) => {
297
+ e.preventDefault();
298
+ const target = document.getElementById(targetId);
299
+ if (target) {
300
+ target.setAttribute('tabindex', '-1');
301
+ target.focus();
302
+ target.removeAttribute('tabindex');
303
+ }
304
+ });
305
+
306
+ return link;
307
+ }
308
+
309
+ /**
310
+ * Install skip links at the beginning of the document
311
+ * @param {Array<{target: string, text: string}>} links - Skip link definitions
312
+ */
313
+ export function installSkipLinks(links) {
314
+ const container = document.createElement('nav');
315
+ container.setAttribute('aria-label', 'Skip links');
316
+ container.className = 'pulse-skip-links';
317
+
318
+ links.forEach(({ target, text }) => {
319
+ container.appendChild(createSkipLink(target, text));
320
+ });
321
+
322
+ document.body.insertBefore(container, document.body.firstChild);
323
+ return container;
324
+ }
325
+ // =============================================================================
326
+ // KEYBOARD NAVIGATION
327
+ // =============================================================================
328
+
329
+ /**
330
+ * Handle arrow key navigation within a container (roving tabindex)
331
+ * @param {HTMLElement} container - Container element
332
+ * @param {object} options - Options
333
+ * @returns {Function} Cleanup function
334
+ */
335
+ export function createRovingTabindex(container, options = {}) {
336
+ const {
337
+ selector = '[role="option"], [role="menuitem"], [role="treeitem"]',
338
+ orientation = 'vertical',
339
+ loop = true,
340
+ onSelect = null
341
+ } = options;
342
+
343
+ const getItems = () => Array.from(container.querySelectorAll(selector))
344
+ .filter(el => !el.hasAttribute('disabled') && el.getAttribute('aria-disabled') !== 'true');
345
+
346
+ const items = getItems();
347
+ if (items.length === 0) return () => {};
348
+
349
+ // Initialize tabindex
350
+ items.forEach((item, i) => {
351
+ item.setAttribute('tabindex', i === 0 ? '0' : '-1');
352
+ });
353
+
354
+ const handleKeyDown = (e) => {
355
+ const items = getItems();
356
+ const currentIndex = items.findIndex(item => item === document.activeElement);
357
+ if (currentIndex === -1) return;
358
+
359
+ const isVertical = orientation === 'vertical';
360
+ const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
361
+ const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
362
+
363
+ let newIndex = currentIndex;
364
+
365
+ switch (e.key) {
366
+ case prevKey:
367
+ if (loop) {
368
+ newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
369
+ } else {
370
+ newIndex = Math.max(0, currentIndex - 1);
371
+ }
372
+ break;
373
+ case nextKey:
374
+ if (loop) {
375
+ newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
376
+ } else {
377
+ newIndex = Math.min(items.length - 1, currentIndex + 1);
378
+ }
379
+ break;
380
+ case 'Home':
381
+ newIndex = 0;
382
+ break;
383
+ case 'End':
384
+ newIndex = items.length - 1;
385
+ break;
386
+ case 'Enter':
387
+ case ' ':
388
+ if (onSelect) {
389
+ e.preventDefault();
390
+ onSelect(items[currentIndex], currentIndex);
391
+ }
392
+ return;
393
+ default:
394
+ return;
395
+ }
396
+
397
+ e.preventDefault();
398
+
399
+ // Update tabindex
400
+ items.forEach((item, i) => {
401
+ item.setAttribute('tabindex', i === newIndex ? '0' : '-1');
402
+ });
403
+
404
+ items[newIndex].focus();
405
+ };
406
+
407
+ container.addEventListener('keydown', handleKeyDown);
408
+
409
+ return () => {
410
+ container.removeEventListener('keydown', handleKeyDown);
411
+ };
412
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pulse A11y - Main Entry Point
3
+ *
4
+ * Barrel export for all accessibility modules
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y
7
+ */
8
+
9
+ // Export all from sub-modules
10
+ export * from './announcements.js';
11
+ export * from './focus.js';
12
+ export * from './preferences.js';
13
+ export * from './widgets.js';
14
+ export * from './validation.js';
15
+ export * from './contrast.js';
16
+ export * from './utils.js';
17
+
18
+ // Default export for backward compatibility
19
+ import * as announcements from './announcements.js';
20
+ import * as focus from './focus.js';
21
+ import * as preferences from './preferences.js';
22
+ import * as widgets from './widgets.js';
23
+ import * as validation from './validation.js';
24
+ import * as contrast from './contrast.js';
25
+ import * as utils from './utils.js';
26
+
27
+ export default {
28
+ ...announcements,
29
+ ...focus,
30
+ ...preferences,
31
+ ...widgets,
32
+ ...validation,
33
+ ...contrast,
34
+ ...utils
35
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pulse A11y - User Preferences
3
+ *
4
+ * User preference detection (reduced motion, color scheme, etc.)
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/preferences
7
+ */
8
+
9
+ import { pulse } from '../pulse.js';
10
+
11
+ // =============================================================================
12
+ // USER PREFERENCES
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Check if user prefers reduced motion
17
+ * @returns {boolean}
18
+ */
19
+ export function prefersReducedMotion() {
20
+ if (typeof window === 'undefined') return false;
21
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
22
+ }
23
+
24
+ /**
25
+ * Check user's preferred color scheme
26
+ * @returns {'light'|'dark'|'no-preference'}
27
+ */
28
+ export function prefersColorScheme() {
29
+ if (typeof window === 'undefined') return 'no-preference';
30
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
31
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) return 'light';
32
+ return 'no-preference';
33
+ }
34
+
35
+ /**
36
+ * Check if user prefers high contrast
37
+ * @returns {boolean}
38
+ */
39
+ export function prefersHighContrast() {
40
+ if (typeof window === 'undefined') return false;
41
+ return window.matchMedia('(prefers-contrast: more)').matches;
42
+ }
43
+
44
+ /**
45
+ * Check if user prefers reduced transparency
46
+ * @returns {boolean}
47
+ */
48
+ export function prefersReducedTransparency() {
49
+ if (typeof window === 'undefined') return false;
50
+ return window.matchMedia('(prefers-reduced-transparency: reduce)').matches;
51
+ }
52
+
53
+ /**
54
+ * Check if forced-colors mode is active (Windows High Contrast)
55
+ * @returns {'none'|'active'}
56
+ */
57
+ export function forcedColorsMode() {
58
+ if (typeof window === 'undefined') return 'none';
59
+ if (window.matchMedia('(forced-colors: active)').matches) return 'active';
60
+ return 'none';
61
+ }
62
+
63
+ /**
64
+ * Check user's contrast preference (more detailed than prefersHighContrast)
65
+ * @returns {'no-preference'|'more'|'less'|'custom'}
66
+ */
67
+ export function prefersContrast() {
68
+ if (typeof window === 'undefined') return 'no-preference';
69
+ if (window.matchMedia('(prefers-contrast: more)').matches) return 'more';
70
+ if (window.matchMedia('(prefers-contrast: less)').matches) return 'less';
71
+ if (window.matchMedia('(prefers-contrast: custom)').matches) return 'custom';
72
+ return 'no-preference';
73
+ }
74
+
75
+ /**
76
+ * Create reactive user preferences pulse
77
+ * @returns {object} Object with reactive preference pulses
78
+ */
79
+ export function createPreferences() {
80
+ const reducedMotion = pulse(prefersReducedMotion());
81
+ const colorScheme = pulse(prefersColorScheme());
82
+ const highContrast = pulse(prefersHighContrast());
83
+ const reducedTransparency = pulse(prefersReducedTransparency());
84
+ const forcedColors = pulse(forcedColorsMode());
85
+ const contrast = pulse(prefersContrast());
86
+
87
+ const listeners = [];
88
+
89
+ if (typeof window !== 'undefined') {
90
+ const track = (query, handler) => {
91
+ const mql = window.matchMedia(query);
92
+ mql.addEventListener('change', handler);
93
+ listeners.push({ mql, handler });
94
+ };
95
+
96
+ track('(prefers-reduced-motion: reduce)', (e) => reducedMotion.set(e.matches));
97
+ track('(prefers-color-scheme: dark)', (e) => colorScheme.set(e.matches ? 'dark' : 'light'));
98
+ track('(prefers-contrast: more)', (e) => highContrast.set(e.matches));
99
+ track('(prefers-reduced-transparency: reduce)', (e) => reducedTransparency.set(e.matches));
100
+ track('(forced-colors: active)', (e) => forcedColors.set(e.matches ? 'active' : 'none'));
101
+ track('(prefers-contrast: more)', () => contrast.set(prefersContrast()));
102
+ track('(prefers-contrast: less)', () => contrast.set(prefersContrast()));
103
+ }
104
+
105
+ const cleanup = () => {
106
+ for (const { mql, handler } of listeners) {
107
+ mql.removeEventListener('change', handler);
108
+ }
109
+ listeners.length = 0;
110
+ };
111
+
112
+ return {
113
+ reducedMotion,
114
+ colorScheme,
115
+ highContrast,
116
+ reducedTransparency,
117
+ forcedColors,
118
+ contrast,
119
+ cleanup
120
+ };
121
+ }