pulse-js-framework 1.7.8 → 1.7.10

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.
@@ -54,6 +54,13 @@ export class Transformer {
54
54
  this.importedComponents = new Map();
55
55
  this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
56
56
 
57
+ // Track a11y feature usage for conditional imports
58
+ this.usesA11y = {
59
+ srOnly: false,
60
+ trapFocus: false,
61
+ announce: false
62
+ };
63
+
57
64
  // Source map tracking
58
65
  this.sourceMap = null;
59
66
  this._currentLine = 0;
@@ -126,6 +133,40 @@ export class Transformer {
126
133
  return this._trackCode(code);
127
134
  }
128
135
 
136
+ /**
137
+ * Pre-scan AST for a11y directive usage
138
+ */
139
+ _scanA11yUsage(node) {
140
+ if (!node) return;
141
+
142
+ // Check directives for a11y usage
143
+ if (node.directives) {
144
+ for (const directive of node.directives) {
145
+ if (directive.type === 'A11yDirective') {
146
+ if (directive.attrs && directive.attrs.srOnly) {
147
+ this.usesA11y.srOnly = true;
148
+ }
149
+ } else if (directive.type === 'FocusTrapDirective') {
150
+ this.usesA11y.trapFocus = true;
151
+ }
152
+ }
153
+ }
154
+
155
+ // Recursively scan children
156
+ if (node.children) {
157
+ for (const child of node.children) {
158
+ this._scanA11yUsage(child);
159
+ }
160
+ }
161
+
162
+ // Scan view block children
163
+ if (node.type === 'ViewBlock' && node.children) {
164
+ for (const child of node.children) {
165
+ this._scanA11yUsage(child);
166
+ }
167
+ }
168
+ }
169
+
129
170
  /**
130
171
  * Transform AST to JavaScript code
131
172
  */
@@ -137,6 +178,11 @@ export class Transformer {
137
178
  extractImportedComponents(this, this.ast.imports);
138
179
  }
139
180
 
181
+ // Pre-scan for a11y usage to determine imports
182
+ if (this.ast.view) {
183
+ this._scanA11yUsage(this.ast.view);
184
+ }
185
+
140
186
  // Imports (runtime + user imports)
141
187
  parts.push(generateImports(this));
142
188
 
@@ -18,7 +18,11 @@ export const VIEW_NODE_HANDLERS = {
18
18
  [NodeType.SlotElement]: 'transformSlot',
19
19
  [NodeType.LinkDirective]: 'transformLinkDirective',
20
20
  [NodeType.OutletDirective]: 'transformOutletDirective',
21
- [NodeType.NavigateDirective]: 'transformNavigateDirective'
21
+ [NodeType.NavigateDirective]: 'transformNavigateDirective',
22
+ // Accessibility directives
23
+ [NodeType.A11yDirective]: 'transformA11yDirective',
24
+ [NodeType.LiveDirective]: 'transformLiveDirective',
25
+ [NodeType.FocusTrapDirective]: 'transformFocusTrapDirective'
22
26
  };
23
27
 
24
28
  /**
@@ -93,6 +97,12 @@ export function transformViewNode(transformer, node, indent = 0) {
93
97
  return transformOutletDirective(transformer, node, indent);
94
98
  case NodeType.NavigateDirective:
95
99
  return transformNavigateDirective(transformer, node, indent);
100
+ case NodeType.A11yDirective:
101
+ return transformA11yDirective(transformer, node, indent);
102
+ case NodeType.LiveDirective:
103
+ return transformLiveDirective(transformer, node, indent);
104
+ case NodeType.FocusTrapDirective:
105
+ return transformFocusTrapDirective(transformer, node, indent);
96
106
  default:
97
107
  return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
98
108
  }
@@ -192,6 +202,99 @@ export function transformNavigateDirective(transformer, node, indent) {
192
202
  return `${pad}router.navigate(${path}${options})`;
193
203
  }
194
204
 
205
+ /**
206
+ * Transform @a11y directive - sets ARIA attributes
207
+ * @param {Object} transformer - Transformer instance
208
+ * @param {Object} node - A11y directive node
209
+ * @param {number} indent - Indentation level
210
+ * @returns {string} JavaScript code
211
+ */
212
+ export function transformA11yDirective(transformer, node, indent) {
213
+ const pad = ' '.repeat(indent);
214
+ const attrs = node.attrs || {};
215
+
216
+ // Handle @srOnly - create visually hidden element
217
+ if (attrs.srOnly) {
218
+ return `${pad}srOnly(/* content */)`;
219
+ }
220
+
221
+ // Build ARIA attributes object
222
+ const ariaAttrs = Object.entries(attrs).map(([key, value]) => {
223
+ // Map short names to aria- attributes (role is not prefixed)
224
+ const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
225
+ const valueCode = typeof value === 'string' ? `'${value}'` : transformExpression(transformer, value);
226
+ return `'${ariaKey}': ${valueCode}`;
227
+ }).join(', ');
228
+
229
+ return `{ ${ariaAttrs} }`;
230
+ }
231
+
232
+ /**
233
+ * Build ARIA attributes object from a11y directive
234
+ * @param {Object} transformer - Transformer instance
235
+ * @param {Object} directive - A11y directive node
236
+ * @returns {Object} Object with key-value pairs for ARIA attributes
237
+ */
238
+ export function buildA11yAttributes(transformer, directive) {
239
+ const attrs = directive.attrs || {};
240
+ const result = {};
241
+
242
+ for (const [key, value] of Object.entries(attrs)) {
243
+ if (key === 'srOnly') continue;
244
+ // Map short names to aria- attributes (role is not prefixed)
245
+ const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
246
+
247
+ // Handle different value types
248
+ if (typeof value === 'string') {
249
+ result[ariaKey] = value;
250
+ } else if (typeof value === 'boolean') {
251
+ result[ariaKey] = String(value); // Convert true/false to "true"/"false"
252
+ } else if (typeof value === 'number') {
253
+ result[ariaKey] = String(value);
254
+ } else {
255
+ result[ariaKey] = transformExpression(transformer, value);
256
+ }
257
+ }
258
+
259
+ return result;
260
+ }
261
+
262
+ /**
263
+ * Transform @live directive - creates live region
264
+ * @param {Object} transformer - Transformer instance
265
+ * @param {Object} node - Live directive node
266
+ * @param {number} indent - Indentation level
267
+ * @returns {string} JavaScript code
268
+ */
269
+ export function transformLiveDirective(transformer, node, indent) {
270
+ const priority = node.priority || 'polite';
271
+ return `'${priority}'`;
272
+ }
273
+
274
+ /**
275
+ * Transform @focusTrap directive
276
+ * @param {Object} transformer - Transformer instance
277
+ * @param {Object} node - Focus trap directive node
278
+ * @param {number} indent - Indentation level
279
+ * @returns {string} JavaScript code
280
+ */
281
+ export function transformFocusTrapDirective(transformer, node, indent) {
282
+ const options = node.options || {};
283
+
284
+ if (Object.keys(options).length === 0) {
285
+ return '{}';
286
+ }
287
+
288
+ const optionsCode = Object.entries(options).map(([key, value]) => {
289
+ const valueCode = typeof value === 'boolean' ? String(value) :
290
+ typeof value === 'string' ? `'${value}'` :
291
+ transformExpression(transformer, value);
292
+ return `${key}: ${valueCode}`;
293
+ }).join(', ');
294
+
295
+ return `{ ${optionsCode} }`;
296
+ }
297
+
195
298
  /**
196
299
  * Transform element
197
300
  * @param {Object} transformer - Transformer instance
@@ -220,11 +323,77 @@ export function transformElement(transformer, node, indent) {
220
323
  selector = addScopeToSelector(transformer, selector);
221
324
  }
222
325
 
223
- // Start with el() call
224
- parts.push(`${pad}el('${selector}'`);
225
-
226
- // Add event handlers as on() chain
326
+ // Extract directives by type
227
327
  const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
328
+ const a11yDirectives = node.directives.filter(d => d.type === NodeType.A11yDirective);
329
+ const liveDirectives = node.directives.filter(d => d.type === NodeType.LiveDirective);
330
+ const focusTrapDirectives = node.directives.filter(d => d.type === NodeType.FocusTrapDirective);
331
+
332
+ // Check for @srOnly directive
333
+ const srOnlyDirective = a11yDirectives.find(d => d.attrs && d.attrs.srOnly);
334
+
335
+ // If @srOnly, wrap entire content
336
+ if (srOnlyDirective) {
337
+ transformer.usesA11y.srOnly = true;
338
+ const content = [];
339
+ for (const text of node.textContent) {
340
+ content.push(transformTextNode(transformer, text, 0).trim());
341
+ }
342
+ for (const child of node.children) {
343
+ content.push(transformViewNode(transformer, child, 0).trim());
344
+ }
345
+ const contentCode = content.length === 1 ? content[0] : `[${content.join(', ')}]`;
346
+ return `${pad}srOnly(${contentCode})`;
347
+ }
348
+
349
+ // Track focusTrap usage
350
+ if (focusTrapDirectives.length > 0) {
351
+ transformer.usesA11y.trapFocus = true;
352
+ }
353
+
354
+ // Build ARIA attributes from directives
355
+ const ariaAttrs = [];
356
+
357
+ // Process @a11y directives
358
+ for (const directive of a11yDirectives) {
359
+ const attrs = buildA11yAttributes(transformer, directive);
360
+ for (const [key, value] of Object.entries(attrs)) {
361
+ const valueCode = typeof value === 'string' ? `'${value}'` : value;
362
+ ariaAttrs.push(`'${key}': ${valueCode}`);
363
+ }
364
+ }
365
+
366
+ // Process @live directives (add aria-live and aria-atomic)
367
+ for (const directive of liveDirectives) {
368
+ const priority = directive.priority || 'polite';
369
+ ariaAttrs.push(`'aria-live': '${priority}'`);
370
+ ariaAttrs.push(`'aria-atomic': 'true'`);
371
+ }
372
+
373
+ // Build selector with inline ARIA attributes
374
+ let enhancedSelector = selector;
375
+ if (ariaAttrs.length > 0) {
376
+ // Convert ARIA attrs to selector attribute syntax where possible
377
+ // For dynamic values, we'll need to use setAriaAttributes
378
+ const staticAttrs = [];
379
+ const dynamicAttrs = [];
380
+
381
+ for (const attr of ariaAttrs) {
382
+ const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
383
+ if (match) {
384
+ // Static attribute - can embed in selector
385
+ staticAttrs.push(`[${match[1]}=${match[2]}]`);
386
+ } else {
387
+ dynamicAttrs.push(attr);
388
+ }
389
+ }
390
+
391
+ // Add static ARIA attributes to selector
392
+ enhancedSelector = selector + staticAttrs.join('');
393
+ }
394
+
395
+ // Start with el() call
396
+ parts.push(`${pad}el('${enhancedSelector}'`);
228
397
 
229
398
  // Add text content
230
399
  if (node.textContent.length > 0) {
@@ -251,6 +420,12 @@ export function transformElement(transformer, node, indent) {
251
420
  result = `on(${result}, '${handler.event}', () => { ${handlerCode}; })`;
252
421
  }
253
422
 
423
+ // Chain focus trap if present
424
+ for (const directive of focusTrapDirectives) {
425
+ const optionsCode = transformFocusTrapDirective(transformer, directive, 0);
426
+ result = `trapFocus(${result}, ${optionsCode})`;
427
+ }
428
+
254
429
  return result;
255
430
  }
256
431
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.8",
3
+ "version": "1.7.10",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -64,6 +64,14 @@
64
64
  "types": "./types/form.d.ts",
65
65
  "default": "./runtime/form.js"
66
66
  },
67
+ "./runtime/http": {
68
+ "types": "./types/http.d.ts",
69
+ "default": "./runtime/http.js"
70
+ },
71
+ "./runtime/a11y": {
72
+ "types": "./types/a11y.d.ts",
73
+ "default": "./runtime/a11y.js"
74
+ },
67
75
  "./runtime/devtools": "./runtime/devtools.js",
68
76
  "./compiler": {
69
77
  "types": "./types/index.d.ts",
@@ -93,7 +101,7 @@
93
101
  "LICENSE"
94
102
  ],
95
103
  "scripts": {
96
- "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:devtools && npm run test:native",
104
+ "test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors",
97
105
  "test:compiler": "node test/compiler.test.js",
98
106
  "test:sourcemap": "node test/sourcemap.test.js",
99
107
  "test:pulse": "node test/pulse.test.js",
@@ -110,8 +118,12 @@
110
118
  "test:docs": "node test/docs.test.js",
111
119
  "test:async": "node test/async.test.js",
112
120
  "test:form": "node test/form.test.js",
121
+ "test:http": "node test/http.test.js",
113
122
  "test:devtools": "node test/devtools.test.js",
114
123
  "test:native": "node test/native.test.js",
124
+ "test:a11y": "node test/a11y.test.js",
125
+ "test:logger": "node test/logger.test.js",
126
+ "test:errors": "node test/errors.test.js",
115
127
  "build:netlify": "node scripts/build-netlify.js",
116
128
  "version": "node scripts/sync-version.js",
117
129
  "docs": "node cli/index.js dev docs"