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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.10.0",
3
+ "version": "1.10.3",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -149,6 +149,7 @@
149
149
  "scripts": {
150
150
  "test": "node scripts/run-all-tests.js",
151
151
  "test:compiler": "node test/compiler.test.js",
152
+ "test:parser-coverage": "node test/parser-coverage.test.js",
152
153
  "test:sourcemap": "node test/sourcemap.test.js",
153
154
  "test:css-parsing": "node test/css-parsing.test.js",
154
155
  "test:preprocessor": "node test/preprocessor.test.js",
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Pulse A11y - Screen Reader Announcements
3
+ *
4
+ * Live region announcements for screen readers
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/announcements
7
+ */
8
+
9
+ import { pulse, effect } from '../pulse.js';
10
+
11
+ // =============================================================================
12
+ // LIVE REGIONS - Screen Reader Announcements
13
+ // =============================================================================
14
+
15
+ let liveRegionPolite = null;
16
+ let liveRegionAssertive = null;
17
+
18
+ /**
19
+ * Initialize live regions for screen reader announcements
20
+ * Called automatically on first announce
21
+ */
22
+ function ensureLiveRegions() {
23
+ if (typeof document === 'undefined') return;
24
+
25
+ if (!liveRegionPolite) {
26
+ liveRegionPolite = document.createElement('div');
27
+ liveRegionPolite.setAttribute('role', 'status');
28
+ liveRegionPolite.setAttribute('aria-live', 'polite');
29
+ liveRegionPolite.setAttribute('aria-atomic', 'true');
30
+ Object.assign(liveRegionPolite.style, {
31
+ position: 'absolute',
32
+ width: '1px',
33
+ height: '1px',
34
+ padding: '0',
35
+ margin: '-1px',
36
+ overflow: 'hidden',
37
+ clip: 'rect(0, 0, 0, 0)',
38
+ whiteSpace: 'nowrap',
39
+ border: '0'
40
+ });
41
+ liveRegionPolite.id = 'pulse-a11y-polite';
42
+ document.body.appendChild(liveRegionPolite);
43
+ }
44
+
45
+ if (!liveRegionAssertive) {
46
+ liveRegionAssertive = document.createElement('div');
47
+ liveRegionAssertive.setAttribute('role', 'alert');
48
+ liveRegionAssertive.setAttribute('aria-live', 'assertive');
49
+ liveRegionAssertive.setAttribute('aria-atomic', 'true');
50
+ Object.assign(liveRegionAssertive.style, {
51
+ position: 'absolute',
52
+ width: '1px',
53
+ height: '1px',
54
+ padding: '0',
55
+ margin: '-1px',
56
+ overflow: 'hidden',
57
+ clip: 'rect(0, 0, 0, 0)',
58
+ whiteSpace: 'nowrap',
59
+ border: '0'
60
+ });
61
+ liveRegionAssertive.id = 'pulse-a11y-assertive';
62
+ document.body.appendChild(liveRegionAssertive);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Announce a message to screen readers
68
+ * @param {string} message - Message to announce
69
+ * @param {object} options - Options
70
+ * @param {'polite'|'assertive'} options.priority - Announcement priority (default: 'polite')
71
+ * @param {number} options.clearAfter - Clear message after ms (default: 1000)
72
+ */
73
+ export function announce(message, options = {}) {
74
+ const { priority = 'polite', clearAfter = 1000 } = options;
75
+
76
+ ensureLiveRegions();
77
+
78
+ const region = priority === 'assertive' ? liveRegionAssertive : liveRegionPolite;
79
+ if (!region) return;
80
+
81
+ // Clear and set new message (needed for repeated announcements)
82
+ region.textContent = '';
83
+
84
+ // Use requestAnimationFrame to ensure the clear is processed
85
+ requestAnimationFrame(() => {
86
+ region.textContent = message;
87
+
88
+ if (clearAfter > 0) {
89
+ setTimeout(() => {
90
+ region.textContent = '';
91
+ }, clearAfter);
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Announce politely (waits for user to finish current task)
98
+ * @param {string} message - Message to announce
99
+ */
100
+ export function announcePolite(message) {
101
+ announce(message, { priority: 'polite' });
102
+ }
103
+
104
+ /**
105
+ * Announce assertively (interrupts current announcement)
106
+ * Use sparingly - only for critical updates
107
+ * @param {string} message - Message to announce
108
+ */
109
+ export function announceAssertive(message) {
110
+ announce(message, { priority: 'assertive' });
111
+ }
112
+
113
+ /**
114
+ * Create a reactive live region that announces when value changes
115
+ * @param {Function} getter - Function that returns the message
116
+ * @param {object} options - Announce options
117
+ * @returns {Function} Cleanup function
118
+ */
119
+ export function createLiveAnnouncer(getter, options = {}) {
120
+ let lastValue = null;
121
+
122
+ return effect(() => {
123
+ const value = getter();
124
+ if (value !== lastValue && value) {
125
+ announce(value, options);
126
+ lastValue = value;
127
+ }
128
+ });
129
+ }
130
+ // =============================================================================
131
+ // ANNOUNCEMENT QUEUE
132
+ // =============================================================================
133
+
134
+ /**
135
+ * Create an announcement queue that handles multiple messages in sequence
136
+ * @param {object} options - Options
137
+ * @param {number} options.minDelay - Minimum delay between announcements (ms, default: 500)
138
+ * @returns {object} Queue control object
139
+ */
140
+ export function createAnnouncementQueue(options = {}) {
141
+ const { minDelay = 500 } = options;
142
+
143
+ const queue = [];
144
+ let isProcessing = false;
145
+ let currentTimerId = null;
146
+ let aborted = false;
147
+ const queueLength = pulse(0);
148
+
149
+ const processQueue = async () => {
150
+ if (isProcessing || queue.length === 0 || aborted) return;
151
+
152
+ isProcessing = true;
153
+
154
+ while (queue.length > 0 && !aborted) {
155
+ const { message, priority, clearAfter } = queue.shift();
156
+ queueLength.set(queue.length);
157
+
158
+ announce(message, { priority, clearAfter });
159
+
160
+ // Wait for announcement to be read
161
+ await new Promise(resolve => {
162
+ currentTimerId = setTimeout(resolve,
163
+ Math.max(minDelay, clearAfter || 1000));
164
+ });
165
+ currentTimerId = null;
166
+ }
167
+
168
+ isProcessing = false;
169
+ };
170
+
171
+ const dispose = () => {
172
+ aborted = true;
173
+ if (currentTimerId !== null) {
174
+ clearTimeout(currentTimerId);
175
+ currentTimerId = null;
176
+ }
177
+ queue.length = 0;
178
+ queueLength.set(0);
179
+ isProcessing = false;
180
+ };
181
+
182
+ return {
183
+ queueLength,
184
+ /**
185
+ * Add a message to the queue
186
+ * @param {string} message - Message to announce
187
+ * @param {object} options - Announcement options (priority, clearAfter)
188
+ */
189
+ add: (message, opts = {}) => {
190
+ if (aborted) return;
191
+ queue.push({ message, ...opts });
192
+ queueLength.set(queue.length);
193
+ processQueue();
194
+ },
195
+ /**
196
+ * Clear the queue
197
+ */
198
+ clear: () => {
199
+ queue.length = 0;
200
+ queueLength.set(0);
201
+ },
202
+ /**
203
+ * Check if queue is being processed
204
+ * @returns {boolean}
205
+ */
206
+ isProcessing: () => isProcessing,
207
+ /**
208
+ * Dispose the queue, cancelling any pending timers
209
+ */
210
+ dispose
211
+ };
212
+ }
213
+
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Pulse A11y - Color Contrast
3
+ *
4
+ * Color contrast calculation and WCAG compliance checking
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/contrast
7
+ */
8
+
9
+ // =============================================================================
10
+ // COLOR CONTRAST
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Parse a color string to RGB values using canvas
15
+ * @param {string} color - CSS color string
16
+ * @returns {{r: number, g: number, b: number}|null}
17
+ */
18
+ function parseColor(color) {
19
+ if (typeof document === 'undefined') return null;
20
+
21
+ const canvas = document.createElement('canvas');
22
+ canvas.width = canvas.height = 1;
23
+ const ctx = canvas.getContext('2d');
24
+ if (!ctx) return null;
25
+
26
+ ctx.fillStyle = color;
27
+ ctx.fillRect(0, 0, 1, 1);
28
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
29
+ return { r, g, b };
30
+ }
31
+
32
+ /**
33
+ * Calculate relative luminance of a color
34
+ * @param {{r: number, g: number, b: number}} color - RGB color
35
+ * @returns {number} Luminance between 0 and 1
36
+ */
37
+ function relativeLuminance({ r, g, b }) {
38
+ const [rs, gs, bs] = [r, g, b].map(c => {
39
+ c = c / 255;
40
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
41
+ });
42
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
43
+ }
44
+
45
+ /**
46
+ * Calculate contrast ratio between two colors
47
+ * @param {string} foreground - Foreground color (any CSS color format)
48
+ * @param {string} background - Background color (any CSS color format)
49
+ * @returns {number} Contrast ratio (1 to 21)
50
+ */
51
+ export function getContrastRatio(foreground, background) {
52
+ const fg = parseColor(foreground);
53
+ const bg = parseColor(background);
54
+
55
+ if (!fg || !bg) return 1;
56
+
57
+ const l1 = relativeLuminance(fg);
58
+ const l2 = relativeLuminance(bg);
59
+
60
+ const lighter = Math.max(l1, l2);
61
+ const darker = Math.min(l1, l2);
62
+
63
+ return (lighter + 0.05) / (darker + 0.05);
64
+ }
65
+
66
+ /**
67
+ * Check if contrast meets WCAG requirements
68
+ * @param {number} ratio - Contrast ratio
69
+ * @param {'AA'|'AAA'} level - WCAG level (default: 'AA')
70
+ * @param {'normal'|'large'} textSize - Text size category (default: 'normal')
71
+ * @returns {boolean}
72
+ */
73
+ export function meetsContrastRequirement(ratio, level = 'AA', textSize = 'normal') {
74
+ const requirements = {
75
+ AA: { normal: 4.5, large: 3 },
76
+ AAA: { normal: 7, large: 4.5 }
77
+ };
78
+ return ratio >= (requirements[level]?.[textSize] ?? 4.5);
79
+ }
80
+
81
+ /**
82
+ * Get the effective background color of an element (handles transparency)
83
+ * @param {HTMLElement} element - Element to check
84
+ * @returns {string} Computed background color
85
+ */
86
+ export function getEffectiveBackgroundColor(element) {
87
+ if (!element || typeof getComputedStyle === 'undefined') return 'rgb(255, 255, 255)';
88
+
89
+ let el = element;
90
+ while (el) {
91
+ const bg = getComputedStyle(el).backgroundColor;
92
+ // Check if background is not transparent
93
+ if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') {
94
+ return bg;
95
+ }
96
+ el = el.parentElement;
97
+ }
98
+ return 'rgb(255, 255, 255)'; // Default to white
99
+ }
100
+
101
+ /**
102
+ * Check color contrast of text in an element
103
+ * @param {HTMLElement} element - Element to check
104
+ * @param {'AA'|'AAA'} level - WCAG level
105
+ * @returns {{ ratio: number, passes: boolean, foreground: string, background: string }}
106
+ */
107
+ export function checkElementContrast(element, level = 'AA') {
108
+ if (!element || typeof getComputedStyle === 'undefined') {
109
+ return { ratio: 1, passes: false, foreground: '', background: '' };
110
+ }
111
+
112
+ const style = getComputedStyle(element);
113
+ const foreground = style.color;
114
+ const background = getEffectiveBackgroundColor(element);
115
+ const ratio = getContrastRatio(foreground, background);
116
+
117
+ // Determine if text is "large" (14pt bold or 18pt+)
118
+ const fontSize = parseFloat(style.fontSize);
119
+ const fontWeight = parseInt(style.fontWeight, 10) || 400;
120
+ const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
121
+
122
+ const passes = meetsContrastRequirement(ratio, level, isLarge ? 'large' : 'normal');
123
+
124
+ return { ratio, passes, foreground, background };
125
+ }