pulse-js-framework 1.7.24 → 1.7.26

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/README.md CHANGED
@@ -48,6 +48,27 @@ npm install
48
48
  npm run dev
49
49
  ```
50
50
 
51
+ ### Or from a template
52
+
53
+ Create projects from built-in example apps:
54
+
55
+ ```bash
56
+ # E-commerce app (products, cart, checkout)
57
+ npx pulse-js-framework create my-shop --ecommerce
58
+
59
+ # Todo app (filtering, local storage)
60
+ npx pulse-js-framework create my-todos --todo
61
+
62
+ # Blog (posts, sidebar, navigation)
63
+ npx pulse-js-framework create my-blog --blog
64
+
65
+ # Chat app (messages, users, emoji picker)
66
+ npx pulse-js-framework create my-chat --chat
67
+
68
+ # Dashboard (data visualization)
69
+ npx pulse-js-framework create my-dashboard --dashboard
70
+ ```
71
+
51
72
  ### Or use directly
52
73
 
53
74
  ```javascript
@@ -139,6 +160,11 @@ See [Pulse DSL documentation](docs/pulse-dsl.md) for full syntax reference.
139
160
  # Project Creation
140
161
  pulse create <name> # Create new project
141
162
  pulse create <name> --typescript # Create TypeScript project
163
+ pulse create <name> --ecommerce # Create from E-Commerce template
164
+ pulse create <name> --todo # Create from Todo App template
165
+ pulse create <name> --blog # Create from Blog template
166
+ pulse create <name> --chat # Create from Chat template
167
+ pulse create <name> --dashboard # Create from Dashboard template
142
168
  pulse init --typescript # Initialize in current directory
143
169
 
144
170
  # Development
package/cli/help.js CHANGED
@@ -29,18 +29,29 @@ Creates a new Pulse project with a complete starter template including:
29
29
  - Project structure (src/, public/)
30
30
  - Vite configuration for development and building
31
31
  - Sample App.pulse component with counter example
32
- - Package.json with all necessary scripts`,
32
+ - Package.json with all necessary scripts
33
+
34
+ You can also create projects from built-in example templates using
35
+ the template flags (--ecommerce, --todo, --blog, --chat, --dashboard).`,
33
36
  arguments: [
34
37
  { name: '<name>', description: 'Name of the project directory to create' }
35
38
  ],
36
39
  options: [
37
40
  { flag: '--typescript, --ts', description: 'Create a TypeScript project with tsconfig.json' },
38
- { flag: '--minimal', description: 'Create minimal project structure without extras' }
41
+ { flag: '--minimal', description: 'Create minimal project structure without extras' },
42
+ { flag: '--ecommerce', description: 'Use E-Commerce template (products, cart, checkout)' },
43
+ { flag: '--todo', description: 'Use Todo App template (filtering, local storage)' },
44
+ { flag: '--blog', description: 'Use Blog template (posts, sidebar, navigation)' },
45
+ { flag: '--chat', description: 'Use Chat template (messages, users, emoji picker)' },
46
+ { flag: '--dashboard', description: 'Use Dashboard template (data visualization)' }
39
47
  ],
40
48
  examples: [
41
49
  { cmd: 'pulse create my-app', desc: 'Create a new JavaScript project' },
42
50
  { cmd: 'pulse create my-app --typescript', desc: 'Create a new TypeScript project' },
43
- { cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' }
51
+ { cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' },
52
+ { cmd: 'pulse create my-shop --ecommerce', desc: 'Create from E-Commerce template' },
53
+ { cmd: 'pulse create my-todos --todo', desc: 'Create from Todo App template' },
54
+ { cmd: 'pulse create my-blog --blog', desc: 'Create from Blog template' }
44
55
  ]
45
56
  },
46
57
 
package/cli/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname, join, resolve, relative } from 'path';
9
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch } from 'fs';
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch, cpSync, statSync } from 'fs';
10
10
  import { log } from './logger.js';
11
11
  import { findPulseFiles, parseArgs } from './utils/file-utils.js';
12
12
  import { runHelp } from './help.js';
@@ -18,6 +18,15 @@ const __dirname = dirname(__filename);
18
18
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
19
19
  const VERSION = pkg.version;
20
20
 
21
+ // Available example templates
22
+ const TEMPLATES = {
23
+ ecommerce: { name: 'E-Commerce', description: 'Shopping cart with products, checkout, and cart management' },
24
+ todo: { name: 'Todo App', description: 'Classic todo list with filtering and local storage' },
25
+ blog: { name: 'Blog', description: 'Blogging platform with posts, sidebar, and navigation' },
26
+ chat: { name: 'Chat', description: 'Real-time chat with messages, users, and emoji picker' },
27
+ dashboard: { name: 'Dashboard', description: 'Analytics dashboard with data visualization' }
28
+ };
29
+
21
30
  // Command handlers
22
31
  const commands = {
23
32
  help: showHelp,
@@ -191,6 +200,87 @@ function showVersion() {
191
200
  log.info(`Pulse Framework v${VERSION}`);
192
201
  }
193
202
 
203
+ /**
204
+ * Copy example template to project directory
205
+ * Transforms imports from /runtime/index.js to pulse-js-framework/runtime
206
+ * @param {string} templateName - Name of the template (ecommerce, todo, blog, chat, dashboard)
207
+ * @param {string} projectPath - Destination project path
208
+ * @param {string} projectName - Name of the project
209
+ */
210
+ function copyExampleTemplate(templateName, projectPath, projectName) {
211
+ const examplesDir = join(__dirname, '..', 'examples');
212
+ const templateDir = join(examplesDir, templateName);
213
+
214
+ if (!existsSync(templateDir)) {
215
+ throw new Error(`Template "${templateName}" not found at ${templateDir}`);
216
+ }
217
+
218
+ /**
219
+ * Recursively copy directory, transforming JS files
220
+ */
221
+ function copyDir(src, dest) {
222
+ if (!existsSync(dest)) {
223
+ mkdirSync(dest, { recursive: true });
224
+ }
225
+
226
+ const entries = readdirSync(src, { withFileTypes: true });
227
+
228
+ for (const entry of entries) {
229
+ const srcPath = join(src, entry.name);
230
+ const destPath = join(dest, entry.name);
231
+
232
+ // Skip node_modules and dist directories
233
+ if (entry.name === 'node_modules' || entry.name === 'dist') {
234
+ continue;
235
+ }
236
+
237
+ if (entry.isDirectory()) {
238
+ copyDir(srcPath, destPath);
239
+ } else {
240
+ // Read file content
241
+ let content = readFileSync(srcPath, 'utf-8');
242
+
243
+ // Transform imports in JS files
244
+ if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
245
+ // Transform /runtime/index.js imports to pulse-js-framework/runtime
246
+ content = content.replace(
247
+ /from\s+['"]\/runtime\/index\.js['"]/g,
248
+ "from 'pulse-js-framework/runtime'"
249
+ );
250
+ content = content.replace(
251
+ /from\s+['"]\/runtime['"]/g,
252
+ "from 'pulse-js-framework/runtime'"
253
+ );
254
+ // Transform other runtime submodule imports
255
+ content = content.replace(
256
+ /from\s+['"]\/runtime\/([^'"]+)['"]/g,
257
+ "from 'pulse-js-framework/runtime/$1'"
258
+ );
259
+ }
260
+
261
+ writeFileSync(destPath, content);
262
+ }
263
+ }
264
+ }
265
+
266
+ // Copy src directory
267
+ const srcDir = join(templateDir, 'src');
268
+ if (existsSync(srcDir)) {
269
+ copyDir(srcDir, join(projectPath, 'src'));
270
+ }
271
+
272
+ // Copy index.html if exists
273
+ const indexHtml = join(templateDir, 'index.html');
274
+ if (existsSync(indexHtml)) {
275
+ let content = readFileSync(indexHtml, 'utf-8');
276
+ // Update title to project name
277
+ content = content.replace(/<title>[^<]*<\/title>/, `<title>${projectName}</title>`);
278
+ writeFileSync(join(projectPath, 'index.html'), content);
279
+ }
280
+
281
+ return true;
282
+ }
283
+
194
284
  /**
195
285
  * Create a new project
196
286
  */
@@ -200,7 +290,16 @@ async function createProject(args) {
200
290
 
201
291
  if (!projectName) {
202
292
  log.error('Please provide a project name.');
203
- log.info('Usage: pulse create <project-name>');
293
+ log.info('Usage: pulse create <project-name> [options]');
294
+ log.info('');
295
+ log.info('Options:');
296
+ log.info(' --typescript, --ts Create TypeScript project');
297
+ log.info(' --minimal Create minimal project');
298
+ log.info('');
299
+ log.info('Templates (use existing example apps):');
300
+ for (const [key, info] of Object.entries(TEMPLATES)) {
301
+ log.info(` --${key.padEnd(12)} ${info.description}`);
302
+ }
204
303
  process.exit(1);
205
304
  }
206
305
 
@@ -214,6 +313,93 @@ async function createProject(args) {
214
313
  const useTypescript = options.typescript || options.ts || false;
215
314
  const minimal = options.minimal || false;
216
315
 
316
+ // Check for template options
317
+ let selectedTemplate = null;
318
+ for (const templateName of Object.keys(TEMPLATES)) {
319
+ if (options[templateName]) {
320
+ selectedTemplate = templateName;
321
+ break;
322
+ }
323
+ }
324
+
325
+ // If a template is selected, use the template-based creation
326
+ if (selectedTemplate) {
327
+ const templateInfo = TEMPLATES[selectedTemplate];
328
+ log.info(`Creating new Pulse project: ${projectName} (${templateInfo.name} template)`);
329
+
330
+ // Create project directory
331
+ mkdirSync(projectPath);
332
+ mkdirSync(join(projectPath, 'public'));
333
+
334
+ // Copy template files
335
+ try {
336
+ copyExampleTemplate(selectedTemplate, projectPath, projectName);
337
+ log.info(` ✓ Copied ${templateInfo.name} template files`);
338
+ } catch (err) {
339
+ log.error(`Failed to copy template: ${err.message}`);
340
+ process.exit(1);
341
+ }
342
+
343
+ // Create package.json for template project
344
+ const packageJson = {
345
+ name: projectName,
346
+ version: '0.1.0',
347
+ type: 'module',
348
+ scripts: {
349
+ dev: 'pulse dev',
350
+ build: 'pulse build',
351
+ preview: 'vite preview',
352
+ test: 'pulse test',
353
+ lint: 'pulse lint'
354
+ },
355
+ dependencies: {
356
+ 'pulse-js-framework': '^1.0.0'
357
+ },
358
+ devDependencies: {
359
+ vite: '^5.0.0'
360
+ }
361
+ };
362
+
363
+ writeFileSync(
364
+ join(projectPath, 'package.json'),
365
+ JSON.stringify(packageJson, null, 2)
366
+ );
367
+
368
+ // Create vite.config.js
369
+ const viteConfig = `import { defineConfig } from 'vite';
370
+ import pulse from 'pulse-js-framework/vite';
371
+
372
+ export default defineConfig({
373
+ plugins: [pulse()]
374
+ });
375
+ `;
376
+ writeFileSync(join(projectPath, 'vite.config.js'), viteConfig);
377
+
378
+ // Create .gitignore
379
+ const gitignore = `node_modules
380
+ dist
381
+ .DS_Store
382
+ *.local
383
+ `;
384
+ writeFileSync(join(projectPath, '.gitignore'), gitignore);
385
+
386
+ log.info(`
387
+ Project created successfully!
388
+
389
+ Next steps:
390
+ cd ${projectName}
391
+ npm install
392
+ npm run dev
393
+
394
+ Template: ${templateInfo.name}
395
+ ${templateInfo.description}
396
+
397
+ Happy coding with Pulse!
398
+ `);
399
+ return;
400
+ }
401
+
402
+ // Standard project creation (no template)
217
403
  log.info(`Creating new Pulse project: ${projectName}${useTypescript ? ' (TypeScript)' : ''}`);
218
404
 
219
405
  // Create project structure
@@ -25,10 +25,14 @@ export function transformExpression(transformer, node) {
25
25
 
26
26
  switch (node.type) {
27
27
  case NodeType.Identifier:
28
+ // Props take precedence over state (props are destructured in render scope)
29
+ if (transformer.propVars.has(node.name)) {
30
+ return node.name;
31
+ }
28
32
  if (transformer.stateVars.has(node.name)) {
29
33
  return `${node.name}.get()`;
30
34
  }
31
- // Props are accessed directly (already destructured)
35
+ // Other identifiers (actions, imports, etc.) accessed directly
32
36
  return node.name;
33
37
 
34
38
  case NodeType.Literal:
@@ -153,8 +157,13 @@ export function transformExpression(transformer, node) {
153
157
  */
154
158
  export function transformExpressionString(transformer, exprStr) {
155
159
  // Simple transformation: wrap state vars with .get()
160
+ // Props take precedence - don't wrap props with .get()
156
161
  let result = exprStr;
157
162
  for (const stateVar of transformer.stateVars) {
163
+ // Skip if this var name is also a prop (props shadow state in render scope)
164
+ if (transformer.propVars.has(stateVar)) {
165
+ continue;
166
+ }
158
167
  result = result.replace(
159
168
  new RegExp(`\\b${stateVar}\\b`, 'g'),
160
169
  `${stateVar}.get()`
@@ -298,34 +298,165 @@ export function transformFocusTrapDirective(transformer, node, indent) {
298
298
  return `{ ${optionsCode} }`;
299
299
  }
300
300
 
301
+ /**
302
+ * Parse a balanced expression starting from an opening brace
303
+ * Handles nested braces and string literals correctly
304
+ * @param {string} str - The string to parse
305
+ * @param {number} start - Index of the opening brace
306
+ * @returns {Object} { expr: string, end: number } or null if invalid
307
+ */
308
+ function parseBalancedExpression(str, start) {
309
+ if (str[start] !== '{') return null;
310
+
311
+ let depth = 0;
312
+ let inString = false;
313
+ let stringChar = '';
314
+ let i = start;
315
+
316
+ while (i < str.length) {
317
+ const char = str[i];
318
+ const prevChar = i > 0 ? str[i - 1] : '';
319
+
320
+ // Handle string literals
321
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
322
+ inString = true;
323
+ stringChar = char;
324
+ } else if (inString && char === stringChar && prevChar !== '\\') {
325
+ inString = false;
326
+ stringChar = '';
327
+ }
328
+
329
+ // Count braces only outside strings
330
+ if (!inString) {
331
+ if (char === '{') {
332
+ depth++;
333
+ } else if (char === '}') {
334
+ depth--;
335
+ if (depth === 0) {
336
+ // Found the matching closing brace
337
+ return {
338
+ expr: str.slice(start + 1, i),
339
+ end: i
340
+ };
341
+ }
342
+ }
343
+ }
344
+
345
+ i++;
346
+ }
347
+
348
+ return null; // Unbalanced braces
349
+ }
350
+
301
351
  /**
302
352
  * Extract dynamic attributes from a selector
303
353
  * Returns { cleanSelector, dynamicAttrs } where dynamicAttrs is an array of { name, expr }
354
+ * Handles complex expressions including ternaries, nested braces, and string literals
304
355
  * @param {string} selector - CSS selector with potential dynamic attributes
305
356
  * @returns {Object} { cleanSelector, dynamicAttrs }
306
357
  */
358
+ /**
359
+ * Extract all attributes (static and dynamic) from a selector string.
360
+ * Static attributes become part of the attrs object passed to el().
361
+ * Dynamic attributes (with {expr} values) are bound via bind().
362
+ *
363
+ * @param {string} selector - Element selector like "div.class[href=url][value={expr}]"
364
+ * @returns {{cleanSelector: string, staticAttrs: Array<{name: string, value: string}>, dynamicAttrs: Array<{name: string, expr: string}>}}
365
+ */
307
366
  function extractDynamicAttributes(selector) {
308
367
  const dynamicAttrs = [];
309
- // Match attributes with {expression} values: [name={expr}] or [name="{expr}"]
310
- const attrPattern = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*\{([^}]+)\}\]/g;
311
- const attrPatternQuoted = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"\{([^}]+)\}"\]/g;
312
-
313
- let cleanSelector = selector;
314
-
315
- // Extract unquoted dynamic attributes: [value={expr}]
316
- let match;
317
- while ((match = attrPattern.exec(selector)) !== null) {
318
- dynamicAttrs.push({ name: match[1], expr: match[2] });
319
- }
320
- cleanSelector = cleanSelector.replace(attrPattern, '');
368
+ const staticAttrs = [];
369
+ let cleanSelector = '';
370
+ let i = 0;
371
+
372
+ while (i < selector.length) {
373
+ // Look for attribute start: [
374
+ if (selector[i] === '[') {
375
+ i++; // Skip [
376
+
377
+ // Parse attribute name
378
+ let attrName = '';
379
+ while (i < selector.length && /[a-zA-Z0-9-_]/.test(selector[i])) {
380
+ attrName += selector[i];
381
+ i++;
382
+ }
321
383
 
322
- // Extract quoted dynamic attributes: [value="{expr}"]
323
- while ((match = attrPatternQuoted.exec(selector)) !== null) {
324
- dynamicAttrs.push({ name: match[1], expr: match[2] });
384
+ // Skip whitespace
385
+ while (i < selector.length && /\s/.test(selector[i])) i++;
386
+
387
+ // Check for =
388
+ if (i < selector.length && selector[i] === '=') {
389
+ i++; // Skip =
390
+
391
+ // Skip whitespace
392
+ while (i < selector.length && /\s/.test(selector[i])) i++;
393
+
394
+ // Determine quote character (or none)
395
+ let quoteChar = null;
396
+ if (selector[i] === '"' || selector[i] === "'") {
397
+ quoteChar = selector[i];
398
+ i++;
399
+ }
400
+
401
+ // Check for dynamic expression {
402
+ if (selector[i] === '{') {
403
+ const result = parseBalancedExpression(selector, i);
404
+ if (result) {
405
+ dynamicAttrs.push({ name: attrName, expr: result.expr });
406
+ i = result.end + 1; // Skip past closing }
407
+
408
+ // Skip optional closing quote
409
+ if (quoteChar && selector[i] === quoteChar) i++;
410
+
411
+ // Skip closing ]
412
+ if (selector[i] === ']') i++;
413
+
414
+ // Don't add this attribute to cleanSelector
415
+ continue;
416
+ }
417
+ }
418
+
419
+ // Static attribute - parse the value
420
+ let attrValue = '';
421
+ if (quoteChar) {
422
+ // Quoted value - read until closing quote
423
+ while (i < selector.length && selector[i] !== quoteChar) {
424
+ attrValue += selector[i];
425
+ i++;
426
+ }
427
+ // Skip closing quote
428
+ if (selector[i] === quoteChar) i++;
429
+ } else {
430
+ // Unquoted value - read until ]
431
+ while (i < selector.length && selector[i] !== ']') {
432
+ attrValue += selector[i];
433
+ i++;
434
+ }
435
+ }
436
+
437
+ // Skip closing ]
438
+ if (selector[i] === ']') i++;
439
+
440
+ // Add to static attrs (don't put in selector)
441
+ staticAttrs.push({ name: attrName, value: attrValue });
442
+ continue;
443
+ } else {
444
+ // Boolean attribute (no value) like [disabled]
445
+ // Skip to closing ]
446
+ while (i < selector.length && selector[i] !== ']') i++;
447
+ if (selector[i] === ']') i++;
448
+
449
+ // Add as boolean attribute
450
+ staticAttrs.push({ name: attrName, value: '' });
451
+ continue;
452
+ }
453
+ } else {
454
+ cleanSelector += selector[i];
455
+ i++;
456
+ }
325
457
  }
326
- cleanSelector = cleanSelector.replace(attrPatternQuoted, '');
327
458
 
328
- return { cleanSelector, dynamicAttrs };
459
+ return { cleanSelector, staticAttrs, dynamicAttrs };
329
460
  }
330
461
 
331
462
  /**
@@ -350,8 +481,8 @@ export function transformElement(transformer, node, indent) {
350
481
  return transformComponentCall(transformer, node, indent);
351
482
  }
352
483
 
353
- // Extract dynamic attributes from selector (e.g., [value={searchQuery}])
354
- let { cleanSelector, dynamicAttrs } = extractDynamicAttributes(node.selector);
484
+ // Extract all attributes from selector (static and dynamic)
485
+ let { cleanSelector, staticAttrs, dynamicAttrs } = extractDynamicAttributes(node.selector);
355
486
 
356
487
  // Add scoped class to selector if CSS scoping is enabled
357
488
  let selector = cleanSelector;
@@ -388,51 +519,46 @@ export function transformElement(transformer, node, indent) {
388
519
  transformer.usesA11y.trapFocus = true;
389
520
  }
390
521
 
391
- // Build ARIA attributes from directives
392
- const ariaAttrs = [];
522
+ // Collect all static attributes for the el() attrs object
523
+ const allStaticAttrs = [];
393
524
 
394
- // Process @a11y directives
525
+ // Add attributes extracted from selector
526
+ for (const attr of staticAttrs) {
527
+ // Escape single quotes in values
528
+ const escapedValue = attr.value.replace(/'/g, "\\'");
529
+ if (attr.value === '') {
530
+ // Boolean attribute
531
+ allStaticAttrs.push(`'${attr.name}': true`);
532
+ } else {
533
+ allStaticAttrs.push(`'${attr.name}': '${escapedValue}'`);
534
+ }
535
+ }
536
+
537
+ // Process @a11y directives - add to static attrs
395
538
  for (const directive of a11yDirectives) {
396
539
  const attrs = buildA11yAttributes(transformer, directive);
397
540
  for (const [key, value] of Object.entries(attrs)) {
398
541
  const valueCode = typeof value === 'string' ? `'${value}'` : value;
399
- ariaAttrs.push(`'${key}': ${valueCode}`);
542
+ allStaticAttrs.push(`'${key}': ${valueCode}`);
400
543
  }
401
544
  }
402
545
 
403
546
  // Process @live directives (add aria-live and aria-atomic)
404
547
  for (const directive of liveDirectives) {
405
548
  const priority = directive.priority || 'polite';
406
- ariaAttrs.push(`'aria-live': '${priority}'`);
407
- ariaAttrs.push(`'aria-atomic': 'true'`);
408
- }
409
-
410
- // Build selector with inline ARIA attributes
411
- let enhancedSelector = selector;
412
- if (ariaAttrs.length > 0) {
413
- // Convert ARIA attrs to selector attribute syntax where possible
414
- // For dynamic values, we'll need to use setAriaAttributes
415
- const staticAttrs = [];
416
- const dynamicAttrs = [];
417
-
418
- for (const attr of ariaAttrs) {
419
- const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
420
- if (match) {
421
- // Static attribute - can embed in selector
422
- staticAttrs.push(`[${match[1]}=${match[2]}]`);
423
- } else {
424
- dynamicAttrs.push(attr);
425
- }
426
- }
427
-
428
- // Add static ARIA attributes to selector
429
- enhancedSelector = selector + staticAttrs.join('');
549
+ allStaticAttrs.push(`'aria-live': '${priority}'`);
550
+ allStaticAttrs.push(`'aria-atomic': 'true'`);
430
551
  }
431
552
 
432
553
  // Start with el() call - escape single quotes in selector
433
- const escapedSelector = enhancedSelector.replace(/'/g, "\\'");
554
+ const escapedSelector = selector.replace(/'/g, "\\'");
434
555
  parts.push(`${pad}el('${escapedSelector}'`);
435
556
 
557
+ // Add attributes object if we have any static attributes
558
+ if (allStaticAttrs.length > 0) {
559
+ parts.push(`, { ${allStaticAttrs.join(', ')} }`);
560
+ }
561
+
436
562
  // Add text content
437
563
  if (node.textContent.length > 0) {
438
564
  for (const text of node.textContent) {
@@ -577,6 +703,18 @@ export function transformComponentCall(transformer, node, indent) {
577
703
  * @param {number} indent - Indentation level
578
704
  * @returns {string} JavaScript code
579
705
  */
706
+ /**
707
+ * Escape a string for use in a template literal
708
+ * @param {string} str - String to escape
709
+ * @returns {string} Escaped string
710
+ */
711
+ function escapeTemplateString(str) {
712
+ return str
713
+ .replace(/\\/g, '\\\\') // Escape backslashes first
714
+ .replace(/`/g, '\\`') // Escape backticks
715
+ .replace(/\$/g, '\\$'); // Escape dollar signs to prevent ${} interpretation
716
+ }
717
+
580
718
  export function transformTextNode(transformer, node, indent) {
581
719
  const pad = ' '.repeat(indent);
582
720
  const parts = node.parts;
@@ -589,7 +727,8 @@ export function transformTextNode(transformer, node, indent) {
589
727
  // Has interpolations - use text() with a function
590
728
  const textParts = parts.map(part => {
591
729
  if (typeof part === 'string') {
592
- return JSON.stringify(part);
730
+ // Escape for template literal (not JSON.stringify which adds quotes)
731
+ return escapeTemplateString(part);
593
732
  }
594
733
  // Interpolation
595
734
  const expr = transformExpressionString(transformer, part.expression);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.24",
3
+ "version": "1.7.26",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",