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.
- package/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +2 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- 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.
|
|
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
|
+
}
|