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,164 @@
1
+ /**
2
+ * Pulse A11y - Utilities
3
+ *
4
+ * Utility functions for accessibility features
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/utils
7
+ */
8
+
9
+ // =============================================================================
10
+ // UTILITIES
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Generate a unique ID for ARIA relationships
15
+ * @param {string} prefix - ID prefix
16
+ * @returns {string} Unique ID
17
+ */
18
+ export function generateId(prefix = 'pulse') {
19
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
20
+ }
21
+
22
+ /**
23
+ * Compute the accessible name of an element
24
+ * Follows simplified ARIA accessible name computation algorithm
25
+ * @param {HTMLElement} element - Element to get name for
26
+ * @returns {string} The accessible name
27
+ */
28
+ export function getAccessibleName(element) {
29
+ if (!element) return '';
30
+
31
+ // 1. aria-labelledby takes precedence
32
+ const labelledBy = element.getAttribute('aria-labelledby');
33
+ if (labelledBy) {
34
+ const ids = labelledBy.split(/\s+/);
35
+ const names = ids
36
+ .map(id => document.getElementById(id))
37
+ .filter(Boolean)
38
+ .map(el => el.textContent?.trim() || '');
39
+ if (names.length > 0) {
40
+ return names.join(' ');
41
+ }
42
+ }
43
+
44
+ // 2. aria-label
45
+ const ariaLabel = element.getAttribute('aria-label');
46
+ if (ariaLabel && ariaLabel.trim()) {
47
+ return ariaLabel.trim();
48
+ }
49
+
50
+ // 3. Native label association (for form controls)
51
+ if (element.labels && element.labels.length > 0) {
52
+ return Array.from(element.labels)
53
+ .map(label => label.textContent?.trim() || '')
54
+ .join(' ');
55
+ }
56
+
57
+ // 4. title attribute
58
+ const title = element.getAttribute('title');
59
+ if (title && title.trim()) {
60
+ return title.trim();
61
+ }
62
+
63
+ // 5. Placeholder (for inputs)
64
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
65
+ const placeholder = element.getAttribute('placeholder');
66
+ if (placeholder && placeholder.trim()) {
67
+ return placeholder.trim();
68
+ }
69
+ }
70
+
71
+ // 6. alt attribute (for images)
72
+ if (element.tagName === 'IMG') {
73
+ const alt = element.getAttribute('alt');
74
+ if (alt) return alt;
75
+ }
76
+
77
+ // 7. Text content (for buttons, links)
78
+ const textContent = element.textContent?.trim();
79
+ if (textContent) {
80
+ return textContent;
81
+ }
82
+
83
+ // 8. value attribute (for inputs with type=button/submit)
84
+ const type = element.getAttribute('type');
85
+ if (element.tagName === 'INPUT' && (type === 'button' || type === 'submit')) {
86
+ return element.value || '';
87
+ }
88
+
89
+ return '';
90
+ }
91
+
92
+ /**
93
+ * Check if an element is hidden from accessibility tree
94
+ * Considers aria-hidden, display:none, visibility:hidden, and inert
95
+ * @param {HTMLElement} element - Element to check
96
+ * @returns {boolean} True if element is hidden from a11y
97
+ */
98
+ export function isAccessiblyHidden(element) {
99
+ if (!element) return true;
100
+
101
+ // Check aria-hidden
102
+ if (element.getAttribute('aria-hidden') === 'true') return true;
103
+
104
+ // Check CSS
105
+ const style = getComputedStyle(element);
106
+ if (style.display === 'none') return true;
107
+ if (style.visibility === 'hidden') return true;
108
+
109
+ // Check inert
110
+ if (element.hasAttribute('inert')) return true;
111
+
112
+ // Check ancestors
113
+ let parent = element.parentElement;
114
+ while (parent) {
115
+ if (parent.getAttribute('aria-hidden') === 'true') return true;
116
+ if (parent.hasAttribute('inert')) return true;
117
+ parent = parent.parentElement;
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Make an element and its descendants inert (non-interactive)
125
+ * Sets both inert attribute and aria-hidden="true"
126
+ * @param {HTMLElement} element - Element to make inert
127
+ * @returns {Function} Restore function to undo inert state
128
+ */
129
+ export function makeInert(element) {
130
+ const wasInert = element.hasAttribute('inert');
131
+ element.setAttribute('inert', '');
132
+ element.setAttribute('aria-hidden', 'true');
133
+
134
+ return () => {
135
+ if (!wasInert) {
136
+ element.removeAttribute('inert');
137
+ }
138
+ element.removeAttribute('aria-hidden');
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Create a visually hidden element (screen reader only)
144
+ * Uses the "sr-only" pattern: visible to assistive tech, hidden visually
145
+ * @param {string} text - Text content for screen readers
146
+ * @returns {HTMLElement} The sr-only element
147
+ */
148
+ export function srOnly(text) {
149
+ const span = document.createElement('span');
150
+ span.textContent = text;
151
+ span.className = 'sr-only';
152
+ span.style.cssText = `
153
+ position: absolute;
154
+ width: 1px;
155
+ height: 1px;
156
+ padding: 0;
157
+ margin: -1px;
158
+ overflow: hidden;
159
+ clip: rect(0, 0, 0, 0);
160
+ white-space: nowrap;
161
+ border: 0;
162
+ `;
163
+ return span;
164
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Pulse A11y - Accessibility Validation
3
+ *
4
+ * A11y validation and auditing tools
5
+ *
6
+ * @module pulse-js-framework/runtime/a11y/validation
7
+ */
8
+
9
+ import { pulse } from '../pulse.js';
10
+
11
+ // =============================================================================
12
+ // VALIDATION & AUDITING
13
+ // =============================================================================
14
+
15
+ /**
16
+ * A11y issues found during validation
17
+ * @typedef {object} A11yIssue
18
+ * @property {'error'|'warning'} severity - Issue severity
19
+ * @property {string} rule - Rule identifier
20
+ * @property {string} message - Human-readable message
21
+ * @property {HTMLElement} element - The element with the issue
22
+ */
23
+
24
+ /**
25
+ * Validate accessibility of a container
26
+ * @param {HTMLElement} container - Container to validate (default: document.body)
27
+ * @returns {A11yIssue[]} Array of issues found
28
+ */
29
+ export function validateA11y(container = document.body) {
30
+ const issues = [];
31
+
32
+ const addIssue = (severity, rule, message, element) => {
33
+ issues.push({ severity, rule, message, element });
34
+ };
35
+
36
+ // Check images for alt text
37
+ container.querySelectorAll('img').forEach(img => {
38
+ if (!img.hasAttribute('alt')) {
39
+ addIssue('error', 'img-alt', 'Image missing alt attribute', img);
40
+ } else if (img.alt === '') {
41
+ // Empty alt is OK for decorative images, but warn
42
+ if (!img.getAttribute('role')?.includes('presentation')) {
43
+ addIssue('warning', 'img-alt-empty', 'Image has empty alt - ensure it is decorative', img);
44
+ }
45
+ }
46
+ });
47
+
48
+ // Check buttons for accessible names
49
+ container.querySelectorAll('button').forEach(button => {
50
+ const hasText = button.textContent.trim().length > 0;
51
+ const hasAriaLabel = button.hasAttribute('aria-label');
52
+ const hasAriaLabelledBy = button.hasAttribute('aria-labelledby');
53
+ const hasTitle = button.hasAttribute('title');
54
+
55
+ if (!hasText && !hasAriaLabel && !hasAriaLabelledBy && !hasTitle) {
56
+ addIssue('error', 'button-name', 'Button has no accessible name', button);
57
+ }
58
+ });
59
+
60
+ // Check links for accessible names
61
+ container.querySelectorAll('a[href]').forEach(link => {
62
+ const hasText = link.textContent.trim().length > 0;
63
+ const hasAriaLabel = link.hasAttribute('aria-label');
64
+ const hasImg = link.querySelector('img[alt]');
65
+
66
+ if (!hasText && !hasAriaLabel && !hasImg) {
67
+ addIssue('error', 'link-name', 'Link has no accessible name', link);
68
+ }
69
+ });
70
+
71
+ // Check form inputs for labels
72
+ container.querySelectorAll('input, select, textarea').forEach(input => {
73
+ if (input.type === 'hidden' || input.type === 'submit' || input.type === 'button') return;
74
+
75
+ const id = input.id;
76
+ const hasLabel = id && container.querySelector(`label[for="${id}"]`);
77
+ const hasAriaLabel = input.hasAttribute('aria-label');
78
+ const hasAriaLabelledBy = input.hasAttribute('aria-labelledby');
79
+ const isWrappedByLabel = input.closest('label');
80
+ const hasPlaceholder = input.hasAttribute('placeholder');
81
+
82
+ if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy && !isWrappedByLabel) {
83
+ const msg = hasPlaceholder
84
+ ? 'Form input uses placeholder but missing label (placeholder is not a label substitute)'
85
+ : 'Form input missing associated label';
86
+ addIssue('error', 'input-label', msg, input);
87
+ }
88
+ });
89
+
90
+ // Check for positive tabindex (anti-pattern)
91
+ container.querySelectorAll('[tabindex]').forEach(el => {
92
+ const tabindex = parseInt(el.getAttribute('tabindex'), 10);
93
+ if (tabindex > 0) {
94
+ addIssue('warning', 'tabindex-positive', 'Avoid positive tabindex values - use DOM order instead', el);
95
+ }
96
+ });
97
+
98
+ // Check for click handlers on non-interactive elements
99
+ container.querySelectorAll('div[onclick], span[onclick]').forEach(el => {
100
+ if (!el.hasAttribute('role') && !el.hasAttribute('tabindex')) {
101
+ addIssue('warning', 'click-non-interactive', 'Click handler on non-interactive element - consider using button', el);
102
+ }
103
+ });
104
+
105
+ // Check headings hierarchy
106
+ const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6');
107
+ let lastLevel = 0;
108
+ headings.forEach(heading => {
109
+ const level = parseInt(heading.tagName[1], 10);
110
+ if (level > lastLevel + 1 && lastLevel !== 0) {
111
+ addIssue('warning', 'heading-order', `Heading level skipped (h${lastLevel} to h${level})`, heading);
112
+ }
113
+ lastLevel = level;
114
+ });
115
+
116
+ // Check for autoplay media
117
+ container.querySelectorAll('video[autoplay], audio[autoplay]').forEach(media => {
118
+ if (!media.hasAttribute('muted')) {
119
+ addIssue('warning', 'media-autoplay', 'Autoplaying media should be muted', media);
120
+ }
121
+ });
122
+
123
+ // Check for duplicate IDs
124
+ const idMap = new Map();
125
+ container.querySelectorAll('[id]').forEach(el => {
126
+ const id = el.id;
127
+ if (id) {
128
+ if (idMap.has(id)) {
129
+ addIssue('error', 'duplicate-id', `Duplicate ID "${id}" found`, el);
130
+ } else {
131
+ idMap.set(id, el);
132
+ }
133
+ }
134
+ });
135
+
136
+ // Check for landmark regions (main, nav, etc.)
137
+ if (typeof container.querySelector === 'function' && container === document.body) {
138
+ const hasMain = container.querySelector('main, [role="main"]');
139
+ if (!hasMain) {
140
+ addIssue('warning', 'missing-main', 'Page should have a <main> landmark', document.body);
141
+ }
142
+ }
143
+
144
+ // Check for nested interactive elements
145
+ container.querySelectorAll('a, button').forEach(el => {
146
+ if (typeof el.querySelector === 'function') {
147
+ const nestedInteractive = el.querySelector('a, button, input, select, textarea');
148
+ if (nestedInteractive) {
149
+ addIssue('error', 'nested-interactive',
150
+ 'Interactive elements should not be nested inside other interactive elements', el);
151
+ }
152
+ }
153
+ });
154
+
155
+ // Check for missing html lang attribute
156
+ if (container === document.body && typeof document !== 'undefined' && document.documentElement) {
157
+ const lang = document.documentElement.getAttribute?.('lang');
158
+ if (!lang) {
159
+ addIssue('warning', 'missing-lang',
160
+ 'Document should have a lang attribute on <html>', document.documentElement);
161
+ }
162
+ }
163
+
164
+ // Check for touch target sizes (WCAG 2.2 - 24x24px minimum)
165
+ if (typeof getComputedStyle === 'function') {
166
+ container.querySelectorAll('a, button, input, select, [role="button"], [role="link"]').forEach(el => {
167
+ if (typeof el.getBoundingClientRect === 'function') {
168
+ const rect = el.getBoundingClientRect();
169
+ if (rect.width > 0 && rect.height > 0 && (rect.width < 24 || rect.height < 24)) {
170
+ // Only flag if element is visible
171
+ const style = getComputedStyle(el);
172
+ if (style.display !== 'none' && style.visibility !== 'hidden') {
173
+ addIssue('warning', 'touch-target-size',
174
+ `Touch target (${Math.round(rect.width)}x${Math.round(rect.height)}px) smaller than 24x24px minimum`,
175
+ el);
176
+ }
177
+ }
178
+ }
179
+ });
180
+ }
181
+
182
+ return issues;
183
+ }
184
+
185
+ /**
186
+ * Log validation results to console
187
+ * @param {A11yIssue[]} issues - Issues from validateA11y
188
+ */
189
+ export function logA11yIssues(issues) {
190
+ if (issues.length === 0) {
191
+ console.log('%c✓ No accessibility issues found', 'color: green; font-weight: bold');
192
+ return;
193
+ }
194
+
195
+ const errors = issues.filter(i => i.severity === 'error');
196
+ const warnings = issues.filter(i => i.severity === 'warning');
197
+
198
+ console.group(`%cAccessibility Issues (${errors.length} errors, ${warnings.length} warnings)`,
199
+ 'color: red; font-weight: bold');
200
+
201
+ issues.forEach(issue => {
202
+ const icon = issue.severity === 'error' ? '❌' : '⚠️';
203
+ const color = issue.severity === 'error' ? 'color: red' : 'color: orange';
204
+ console.log(`%c${icon} [${issue.rule}] ${issue.message}`, color, issue.element);
205
+ });
206
+
207
+ console.groupEnd();
208
+ }
209
+
210
+ /**
211
+ * Highlight elements with accessibility issues in the DOM
212
+ * @param {A11yIssue[]} issues - Issues from validateA11y
213
+ * @returns {Function} Cleanup function to remove highlights
214
+ */
215
+ export function highlightA11yIssues(issues) {
216
+ const highlights = [];
217
+
218
+ issues.forEach(issue => {
219
+ const el = issue.element;
220
+ const rect = el.getBoundingClientRect();
221
+
222
+ const highlight = document.createElement('div');
223
+ highlight.className = 'pulse-a11y-highlight';
224
+ highlight.style.cssText = `
225
+ position: fixed;
226
+ top: ${rect.top}px;
227
+ left: ${rect.left}px;
228
+ width: ${rect.width}px;
229
+ height: ${rect.height}px;
230
+ border: 2px solid ${issue.severity === 'error' ? 'red' : 'orange'};
231
+ background: ${issue.severity === 'error' ? 'rgba(255,0,0,0.1)' : 'rgba(255,165,0,0.1)'};
232
+ pointer-events: none;
233
+ z-index: 99999;
234
+ `;
235
+
236
+ const label = document.createElement('div');
237
+ label.style.cssText = `
238
+ position: absolute;
239
+ top: -20px;
240
+ left: 0;
241
+ background: ${issue.severity === 'error' ? 'red' : 'orange'};
242
+ color: white;
243
+ font-size: 10px;
244
+ padding: 2px 4px;
245
+ border-radius: 2px;
246
+ white-space: nowrap;
247
+ `;
248
+ label.textContent = issue.rule;
249
+ highlight.appendChild(label);
250
+
251
+ document.body.appendChild(highlight);
252
+ highlights.push(highlight);
253
+ });
254
+
255
+ return () => {
256
+ highlights.forEach(h => h.remove());
257
+ };
258
+ }