eleva 1.0.0-rc.1 → 1.0.0-rc.11

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 (65) hide show
  1. package/README.md +505 -41
  2. package/dist/eleva-plugins.cjs.js +3397 -0
  3. package/dist/eleva-plugins.cjs.js.map +1 -0
  4. package/dist/eleva-plugins.esm.js +3392 -0
  5. package/dist/eleva-plugins.esm.js.map +1 -0
  6. package/dist/eleva-plugins.umd.js +3403 -0
  7. package/dist/eleva-plugins.umd.js.map +1 -0
  8. package/dist/eleva-plugins.umd.min.js +3 -0
  9. package/dist/eleva-plugins.umd.min.js.map +1 -0
  10. package/dist/eleva.cjs.js +617 -118
  11. package/dist/eleva.cjs.js.map +1 -1
  12. package/dist/eleva.d.ts +612 -75
  13. package/dist/eleva.esm.js +617 -118
  14. package/dist/eleva.esm.js.map +1 -1
  15. package/dist/eleva.umd.js +617 -118
  16. package/dist/eleva.umd.js.map +1 -1
  17. package/dist/eleva.umd.min.js +2 -2
  18. package/dist/eleva.umd.min.js.map +1 -1
  19. package/dist/plugins/attr.umd.js +232 -0
  20. package/dist/plugins/attr.umd.js.map +1 -0
  21. package/dist/plugins/attr.umd.min.js +3 -0
  22. package/dist/plugins/attr.umd.min.js.map +1 -0
  23. package/dist/plugins/props.umd.js +712 -0
  24. package/dist/plugins/props.umd.js.map +1 -0
  25. package/dist/plugins/props.umd.min.js +3 -0
  26. package/dist/plugins/props.umd.min.js.map +1 -0
  27. package/dist/plugins/router.umd.js +1808 -0
  28. package/dist/plugins/router.umd.js.map +1 -0
  29. package/dist/plugins/router.umd.min.js +3 -0
  30. package/dist/plugins/router.umd.min.js.map +1 -0
  31. package/dist/plugins/store.umd.js +685 -0
  32. package/dist/plugins/store.umd.js.map +1 -0
  33. package/dist/plugins/store.umd.min.js +3 -0
  34. package/dist/plugins/store.umd.min.js.map +1 -0
  35. package/package.json +107 -45
  36. package/src/core/Eleva.js +247 -63
  37. package/src/modules/Emitter.js +98 -8
  38. package/src/modules/Renderer.js +66 -36
  39. package/src/modules/Signal.js +85 -8
  40. package/src/modules/TemplateEngine.js +121 -13
  41. package/src/plugins/Attr.js +255 -0
  42. package/src/plugins/Props.js +593 -0
  43. package/src/plugins/Router.js +1922 -0
  44. package/src/plugins/Store.js +744 -0
  45. package/src/plugins/index.js +40 -0
  46. package/types/core/Eleva.d.ts +217 -50
  47. package/types/core/Eleva.d.ts.map +1 -1
  48. package/types/modules/Emitter.d.ts +111 -12
  49. package/types/modules/Emitter.d.ts.map +1 -1
  50. package/types/modules/Renderer.d.ts +68 -3
  51. package/types/modules/Renderer.d.ts.map +1 -1
  52. package/types/modules/Signal.d.ts +92 -10
  53. package/types/modules/Signal.d.ts.map +1 -1
  54. package/types/modules/TemplateEngine.d.ts +131 -15
  55. package/types/modules/TemplateEngine.d.ts.map +1 -1
  56. package/types/plugins/Attr.d.ts +29 -0
  57. package/types/plugins/Attr.d.ts.map +1 -0
  58. package/types/plugins/Props.d.ts +49 -0
  59. package/types/plugins/Props.d.ts.map +1 -0
  60. package/types/plugins/Router.d.ts +1000 -0
  61. package/types/plugins/Router.d.ts.map +1 -0
  62. package/types/plugins/Store.d.ts +87 -0
  63. package/types/plugins/Store.d.ts.map +1 -0
  64. package/types/plugins/index.d.ts +5 -0
  65. package/types/plugins/index.d.ts.map +1 -0
package/dist/eleva.esm.js CHANGED
@@ -1,18 +1,76 @@
1
- /*! Eleva v1.0.0-rc.1 | MIT License | https://elevajs.com */
1
+ /*! Eleva v1.0.0-rc.11 | MIT License | https://elevajs.com */
2
+ // ============================================================================
3
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
4
+ // ============================================================================
5
+
6
+ /**
7
+ * @typedef {Record<string, unknown>} TemplateData
8
+ * Data context for template interpolation
9
+ */
10
+
11
+ /**
12
+ * @typedef {string} TemplateString
13
+ * A string containing {{ expression }} interpolation markers
14
+ */
15
+
16
+ /**
17
+ * @typedef {string} Expression
18
+ * A JavaScript expression to be evaluated in the data context
19
+ */
20
+
21
+ /**
22
+ * @typedef {unknown} EvaluationResult
23
+ * The result of evaluating an expression (string, number, boolean, object, etc.)
24
+ */
25
+
2
26
  /**
3
27
  * @class 🔒 TemplateEngine
4
28
  * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
5
- * Provides a safe way to evaluate expressions in templates while preventing XSS attacks.
29
+ * Provides a way to evaluate expressions in templates.
6
30
  * All methods are static and can be called directly on the class.
7
31
  *
32
+ * Template Syntax:
33
+ * - `{{ expression }}` - Interpolate any JavaScript expression
34
+ * - `{{ variable }}` - Access data properties directly
35
+ * - `{{ object.property }}` - Access nested properties
36
+ * - `{{ condition ? a : b }}` - Ternary expressions
37
+ * - `{{ func(arg) }}` - Call functions from data context
38
+ *
8
39
  * @example
40
+ * // Basic interpolation
9
41
  * const template = "Hello, {{name}}!";
10
42
  * const data = { name: "World" };
11
- * const result = TemplateEngine.parse(template, data); // Returns: "Hello, World!"
43
+ * const result = TemplateEngine.parse(template, data);
44
+ * // Result: "Hello, World!"
45
+ *
46
+ * @example
47
+ * // Nested properties
48
+ * const template = "Welcome, {{user.name}}!";
49
+ * const data = { user: { name: "John" } };
50
+ * const result = TemplateEngine.parse(template, data);
51
+ * // Result: "Welcome, John!"
52
+ *
53
+ * @example
54
+ * // Expressions
55
+ * const template = "Status: {{active ? 'Online' : 'Offline'}}";
56
+ * const data = { active: true };
57
+ * const result = TemplateEngine.parse(template, data);
58
+ * // Result: "Status: Online"
59
+ *
60
+ * @example
61
+ * // With Signal values
62
+ * const template = "Count: {{count.value}}";
63
+ * const data = { count: { value: 42 } };
64
+ * const result = TemplateEngine.parse(template, data);
65
+ * // Result: "Count: 42"
12
66
  */
13
67
  class TemplateEngine {
14
68
  /**
15
- * @private {RegExp} Regular expression for matching template expressions in the format {{ expression }}
69
+ * Regular expression for matching template expressions in the format {{ expression }}
70
+ * Matches: {{ anything }} with optional whitespace inside braces
71
+ *
72
+ * @static
73
+ * @private
16
74
  * @type {RegExp}
17
75
  */
18
76
  static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
@@ -23,13 +81,37 @@ class TemplateEngine {
23
81
  *
24
82
  * @public
25
83
  * @static
26
- * @param {string} template - The template string to parse.
27
- * @param {Record<string, unknown>} data - The data context for evaluating expressions.
84
+ * @param {TemplateString|unknown} template - The template string to parse.
85
+ * @param {TemplateData} data - The data context for evaluating expressions.
28
86
  * @returns {string} The parsed template with expressions replaced by their values.
87
+ *
29
88
  * @example
30
- * const result = TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
89
+ * // Simple variables
90
+ * TemplateEngine.parse("Hello, {{name}}!", { name: "World" });
91
+ * // Result: "Hello, World!"
92
+ *
93
+ * @example
94
+ * // Nested properties
95
+ * TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
31
96
  * user: { name: "John", age: 30 }
32
- * }); // Returns: "John is 30 years old"
97
+ * });
98
+ * // Result: "John is 30 years old"
99
+ *
100
+ * @example
101
+ * // Multiple expressions
102
+ * TemplateEngine.parse("{{greeting}}, {{name}}! You have {{count}} messages.", {
103
+ * greeting: "Hello",
104
+ * name: "User",
105
+ * count: 5
106
+ * });
107
+ * // Result: "Hello, User! You have 5 messages."
108
+ *
109
+ * @example
110
+ * // With conditionals
111
+ * TemplateEngine.parse("Status: {{online ? 'Active' : 'Inactive'}}", {
112
+ * online: true
113
+ * });
114
+ * // Result: "Status: Active"
33
115
  */
34
116
  static parse(template, data) {
35
117
  if (typeof template !== "string") return template;
@@ -38,18 +120,44 @@ class TemplateEngine {
38
120
 
39
121
  /**
40
122
  * Evaluates an expression in the context of the provided data object.
123
+ *
41
124
  * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
42
125
  * The use of the `with` statement is necessary for expression evaluation but has security implications.
43
- * Expressions should be carefully validated before evaluation.
126
+ * Only use with trusted templates. User input should never be directly interpolated.
44
127
  *
45
128
  * @public
46
129
  * @static
47
- * @param {string} expression - The expression to evaluate.
48
- * @param {Record<string, unknown>} data - The data context for evaluation.
49
- * @returns {unknown} The result of the evaluation, or an empty string if evaluation fails.
130
+ * @param {Expression|unknown} expression - The expression to evaluate.
131
+ * @param {TemplateData} data - The data context for evaluation.
132
+ * @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
133
+ *
134
+ * @example
135
+ * // Property access
136
+ * TemplateEngine.evaluate("user.name", { user: { name: "John" } });
137
+ * // Result: "John"
138
+ *
139
+ * @example
140
+ * // Numeric values
141
+ * TemplateEngine.evaluate("user.age", { user: { age: 30 } });
142
+ * // Result: 30
143
+ *
50
144
  * @example
51
- * const result = TemplateEngine.evaluate("user.name", { user: { name: "John" } }); // Returns: "John"
52
- * const age = TemplateEngine.evaluate("user.age", { user: { age: 30 } }); // Returns: 30
145
+ * // Expressions
146
+ * TemplateEngine.evaluate("items.length > 0", { items: [1, 2, 3] });
147
+ * // Result: true
148
+ *
149
+ * @example
150
+ * // Function calls
151
+ * TemplateEngine.evaluate("formatDate(date)", {
152
+ * date: new Date(),
153
+ * formatDate: (d) => d.toISOString()
154
+ * });
155
+ * // Result: "2024-01-01T00:00:00.000Z"
156
+ *
157
+ * @example
158
+ * // Failed evaluation returns empty string
159
+ * TemplateEngine.evaluate("nonexistent.property", {});
160
+ * // Result: ""
53
161
  */
54
162
  static evaluate(expression, data) {
55
163
  if (typeof expression !== "string") return expression;
@@ -61,6 +169,29 @@ class TemplateEngine {
61
169
  }
62
170
  }
63
171
 
172
+ // ============================================================================
173
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
174
+ // ============================================================================
175
+
176
+ /**
177
+ * @template T
178
+ * @callback SignalWatcher
179
+ * @param {T} value - The new value of the signal
180
+ * @returns {void}
181
+ */
182
+
183
+ /**
184
+ * @callback SignalUnsubscribe
185
+ * @returns {boolean} True if the watcher was successfully removed
186
+ */
187
+
188
+ /**
189
+ * @template T
190
+ * @typedef {Object} SignalLike
191
+ * @property {T} value - The current value
192
+ * @property {function(SignalWatcher<T>): SignalUnsubscribe} watch - Subscribe to changes
193
+ */
194
+
64
195
  /**
65
196
  * @class ⚡ Signal
66
197
  * @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
@@ -69,11 +200,29 @@ class TemplateEngine {
69
200
  * Updates are batched using microtasks to prevent multiple synchronous notifications.
70
201
  * The class is generic, allowing type-safe handling of any value type T.
71
202
  *
203
+ * @template T The type of value held by this signal
204
+ *
72
205
  * @example
206
+ * // Basic usage
73
207
  * const count = new Signal(0);
74
208
  * count.watch((value) => console.log(`Count changed to: ${value}`));
75
209
  * count.value = 1; // Logs: "Count changed to: 1"
76
- * @template T
210
+ *
211
+ * @example
212
+ * // With unsubscribe
213
+ * const name = new Signal("John");
214
+ * const unsubscribe = name.watch((value) => console.log(value));
215
+ * name.value = "Jane"; // Logs: "Jane"
216
+ * unsubscribe(); // Stop watching
217
+ * name.value = "Bob"; // No log output
218
+ *
219
+ * @example
220
+ * // With objects
221
+ * /** @type {Signal<{x: number, y: number}>} *\/
222
+ * const position = new Signal({ x: 0, y: 0 });
223
+ * position.value = { x: 10, y: 20 }; // Triggers watchers
224
+ *
225
+ * @implements {SignalLike<T>}
77
226
  */
78
227
  class Signal {
79
228
  /**
@@ -81,13 +230,39 @@ class Signal {
81
230
  *
82
231
  * @public
83
232
  * @param {T} value - The initial value of the signal.
233
+ *
234
+ * @example
235
+ * // Primitive types
236
+ * const count = new Signal(0); // Signal<number>
237
+ * const name = new Signal("John"); // Signal<string>
238
+ * const active = new Signal(true); // Signal<boolean>
239
+ *
240
+ * @example
241
+ * // Complex types (use JSDoc for type inference)
242
+ * /** @type {Signal<string[]>} *\/
243
+ * const items = new Signal([]);
244
+ *
245
+ * /** @type {Signal<{id: number, name: string} | null>} *\/
246
+ * const user = new Signal(null);
84
247
  */
85
248
  constructor(value) {
86
- /** @private {T} Internal storage for the signal's current value */
249
+ /**
250
+ * Internal storage for the signal's current value
251
+ * @private
252
+ * @type {T}
253
+ */
87
254
  this._value = value;
88
- /** @private {Set<(value: T) => void>} Collection of callback functions to be notified when value changes */
255
+ /**
256
+ * Collection of callback functions to be notified when value changes
257
+ * @private
258
+ * @type {Set<SignalWatcher<T>>}
259
+ */
89
260
  this._watchers = new Set();
90
- /** @private {boolean} Flag to prevent multiple synchronous watcher notifications and batch updates into microtasks */
261
+ /**
262
+ * Flag to prevent multiple synchronous watcher notifications
263
+ * @private
264
+ * @type {boolean}
265
+ */
91
266
  this._pending = false;
92
267
  }
93
268
 
@@ -120,12 +295,22 @@ class Signal {
120
295
  * The watcher will receive the new value as its argument.
121
296
  *
122
297
  * @public
123
- * @param {(value: T) => void} fn - The callback function to invoke on value change.
124
- * @returns {() => boolean} A function to unsubscribe the watcher.
298
+ * @param {SignalWatcher<T>} fn - The callback function to invoke on value change.
299
+ * @returns {SignalUnsubscribe} A function to unsubscribe the watcher.
300
+ *
125
301
  * @example
302
+ * // Basic watching
126
303
  * const unsubscribe = signal.watch((value) => console.log(value));
127
- * // Later...
128
- * unsubscribe(); // Stops watching for changes
304
+ *
305
+ * @example
306
+ * // Stop watching
307
+ * unsubscribe(); // Returns true if watcher was removed
308
+ *
309
+ * @example
310
+ * // Multiple watchers
311
+ * const unsub1 = signal.watch((v) => console.log("Watcher 1:", v));
312
+ * const unsub2 = signal.watch((v) => console.log("Watcher 2:", v));
313
+ * signal.value = "test"; // Both watchers are called
129
314
  */
130
315
  watch(fn) {
131
316
  this._watchers.add(fn);
@@ -151,6 +336,34 @@ class Signal {
151
336
  }
152
337
  }
153
338
 
339
+ // ============================================================================
340
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
341
+ // ============================================================================
342
+
343
+ /**
344
+ * @template T
345
+ * @callback EventHandler
346
+ * @param {...T} args - Event arguments
347
+ * @returns {void|Promise<void>}
348
+ */
349
+
350
+ /**
351
+ * @callback EventUnsubscribe
352
+ * @returns {void}
353
+ */
354
+
355
+ /**
356
+ * @typedef {`${string}:${string}`} EventName
357
+ * Event names follow the format 'namespace:action' (e.g., 'user:login', 'cart:update')
358
+ */
359
+
360
+ /**
361
+ * @typedef {Object} EmitterLike
362
+ * @property {function(string, EventHandler<unknown>): EventUnsubscribe} on - Subscribe to an event
363
+ * @property {function(string, EventHandler<unknown>=): void} off - Unsubscribe from an event
364
+ * @property {function(string, ...unknown): void} emit - Emit an event
365
+ */
366
+
154
367
  /**
155
368
  * @class 📡 Emitter
156
369
  * @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
@@ -158,21 +371,58 @@ class Signal {
158
371
  * and reactive updates across the application.
159
372
  * Events are handled synchronously in the order they were registered, with proper cleanup
160
373
  * of unsubscribed handlers.
161
- * Event names should follow the format 'namespace:action' (e.g., 'user:login', 'cart:update').
374
+ *
375
+ * Event names should follow the format 'namespace:action' for consistency and organization.
162
376
  *
163
377
  * @example
378
+ * // Basic usage
164
379
  * const emitter = new Emitter();
165
380
  * emitter.on('user:login', (user) => console.log(`User logged in: ${user.name}`));
166
381
  * emitter.emit('user:login', { name: 'John' }); // Logs: "User logged in: John"
382
+ *
383
+ * @example
384
+ * // With unsubscribe
385
+ * const unsub = emitter.on('cart:update', (items) => {
386
+ * console.log(`Cart has ${items.length} items`);
387
+ * });
388
+ * emitter.emit('cart:update', [{ id: 1, name: 'Book' }]); // Logs: "Cart has 1 items"
389
+ * unsub(); // Stop listening
390
+ * emitter.emit('cart:update', []); // No log output
391
+ *
392
+ * @example
393
+ * // Multiple arguments
394
+ * emitter.on('order:placed', (orderId, amount, currency) => {
395
+ * console.log(`Order ${orderId}: ${amount} ${currency}`);
396
+ * });
397
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
398
+ *
399
+ * @example
400
+ * // Common event patterns
401
+ * // Lifecycle events
402
+ * emitter.on('component:mount', (component) => {});
403
+ * emitter.on('component:unmount', (component) => {});
404
+ * // State events
405
+ * emitter.on('state:change', (newState, oldState) => {});
406
+ * // Navigation events
407
+ * emitter.on('router:navigate', (to, from) => {});
408
+ *
409
+ * @implements {EmitterLike}
167
410
  */
168
411
  class Emitter {
169
412
  /**
170
413
  * Creates a new Emitter instance.
171
414
  *
172
415
  * @public
416
+ *
417
+ * @example
418
+ * const emitter = new Emitter();
173
419
  */
174
420
  constructor() {
175
- /** @private {Map<string, Set<(data: unknown) => void>>} Map of event names to their registered handler functions */
421
+ /**
422
+ * Map of event names to their registered handler functions
423
+ * @private
424
+ * @type {Map<string, Set<EventHandler<unknown>>>}
425
+ */
176
426
  this._events = new Map();
177
427
  }
178
428
 
@@ -182,12 +432,23 @@ class Emitter {
182
432
  * Event names should follow the format 'namespace:action' for consistency.
183
433
  *
184
434
  * @public
435
+ * @template T
185
436
  * @param {string} event - The name of the event to listen for (e.g., 'user:login').
186
- * @param {(data: unknown) => void} handler - The callback function to invoke when the event occurs.
187
- * @returns {() => void} A function to unsubscribe the event handler.
437
+ * @param {EventHandler<T>} handler - The callback function to invoke when the event occurs.
438
+ * @returns {EventUnsubscribe} A function to unsubscribe the event handler.
439
+ *
188
440
  * @example
441
+ * // Basic subscription
189
442
  * const unsubscribe = emitter.on('user:login', (user) => console.log(user));
190
- * // Later...
443
+ *
444
+ * @example
445
+ * // Typed handler
446
+ * emitter.on('user:update', (/** @type {{id: number, name: string}} *\/ user) => {
447
+ * console.log(`User ${user.id}: ${user.name}`);
448
+ * });
449
+ *
450
+ * @example
451
+ * // Cleanup
191
452
  * unsubscribe(); // Stops listening for the event
192
453
  */
193
454
  on(event, handler) {
@@ -202,12 +463,18 @@ class Emitter {
202
463
  * Automatically cleans up empty event sets to prevent memory leaks.
203
464
  *
204
465
  * @public
466
+ * @template T
205
467
  * @param {string} event - The name of the event to remove handlers from.
206
- * @param {(data: unknown) => void} [handler] - The specific handler function to remove.
468
+ * @param {EventHandler<T>} [handler] - The specific handler function to remove.
207
469
  * @returns {void}
470
+ *
208
471
  * @example
209
472
  * // Remove a specific handler
473
+ * const loginHandler = (user) => console.log(user);
474
+ * emitter.on('user:login', loginHandler);
210
475
  * emitter.off('user:login', loginHandler);
476
+ *
477
+ * @example
211
478
  * // Remove all handlers for an event
212
479
  * emitter.off('user:login');
213
480
  */
@@ -229,14 +496,22 @@ class Emitter {
229
496
  * If no handlers are registered for the event, the emission is silently ignored.
230
497
  *
231
498
  * @public
499
+ * @template T
232
500
  * @param {string} event - The name of the event to emit.
233
- * @param {...unknown} args - Optional arguments to pass to the event handlers.
501
+ * @param {...T} args - Optional arguments to pass to the event handlers.
234
502
  * @returns {void}
503
+ *
235
504
  * @example
236
505
  * // Emit an event with data
237
506
  * emitter.emit('user:login', { name: 'John', role: 'admin' });
507
+ *
508
+ * @example
238
509
  * // Emit an event with multiple arguments
239
- * emitter.emit('cart:update', { items: [] }, { total: 0 });
510
+ * emitter.emit('order:placed', 'ORD-123', 99.99, 'USD');
511
+ *
512
+ * @example
513
+ * // Emit without data
514
+ * emitter.emit('app:ready');
240
515
  */
241
516
  emit(event, ...args) {
242
517
  if (!this._events.has(event)) return;
@@ -244,12 +519,27 @@ class Emitter {
244
519
  }
245
520
  }
246
521
 
522
+ // ============================================================================
523
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
524
+ // ============================================================================
525
+
526
+ /**
527
+ * @typedef {Object} PatchOptions
528
+ * @property {boolean} [preserveStyles=true]
529
+ * Whether to preserve style elements with data-e-style attribute
530
+ * @property {boolean} [preserveInstances=true]
531
+ * Whether to preserve elements with _eleva_instance property
532
+ */
533
+
534
+ /**
535
+ * @typedef {Map<string, Node>} KeyMap
536
+ * Map of key attribute values to their corresponding DOM nodes
537
+ */
538
+
247
539
  /**
248
- * A regular expression to match hyphenated lowercase letters.
249
- * @private
250
- * @type {RegExp}
540
+ * @typedef {'ELEMENT_NODE'|'TEXT_NODE'|'COMMENT_NODE'|'DOCUMENT_FRAGMENT_NODE'} NodeTypeName
541
+ * Common DOM node type names
251
542
  */
252
- const CAMEL_RE = /-([a-z])/g;
253
543
 
254
544
  /**
255
545
  * @class 🎨 Renderer
@@ -267,27 +557,50 @@ const CAMEL_RE = /-([a-z])/g;
267
557
  * It's particularly optimized for frequent updates and complex DOM structures.
268
558
  *
269
559
  * @example
560
+ * // Basic usage
270
561
  * const renderer = new Renderer();
271
562
  * const container = document.getElementById("app");
272
563
  * const newHtml = "<div>Updated content</div>";
273
564
  * renderer.patchDOM(container, newHtml);
565
+ *
566
+ * @example
567
+ * // With keyed elements for optimal list updates
568
+ * const listHtml = `
569
+ * <ul>
570
+ * <li key="item-1">First</li>
571
+ * <li key="item-2">Second</li>
572
+ * <li key="item-3">Third</li>
573
+ * </ul>
574
+ * `;
575
+ * renderer.patchDOM(container, listHtml);
576
+ *
577
+ * @example
578
+ * // The renderer preserves Eleva-managed elements
579
+ * // Elements with _eleva_instance are not replaced during diffing
580
+ * // Style elements with data-e-style are preserved
274
581
  */
275
582
  class Renderer {
276
583
  /**
277
584
  * Creates a new Renderer instance.
585
+ *
278
586
  * @public
587
+ *
588
+ * @example
589
+ * const renderer = new Renderer();
279
590
  */
280
591
  constructor() {
281
592
  /**
282
593
  * A temporary container to hold the new HTML content while diffing.
594
+ * Reused across patch operations to minimize memory allocation.
283
595
  * @private
284
- * @type {HTMLElement}
596
+ * @type {HTMLDivElement}
285
597
  */
286
598
  this._tempContainer = document.createElement("div");
287
599
  }
288
600
 
289
601
  /**
290
602
  * Patches the DOM of the given container with the provided HTML string.
603
+ * Uses an optimized diffing algorithm to minimize DOM operations.
291
604
  *
292
605
  * @public
293
606
  * @param {HTMLElement} container - The container element to patch.
@@ -295,6 +608,18 @@ class Renderer {
295
608
  * @returns {void}
296
609
  * @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
297
610
  * @throws {Error} If DOM patching fails.
611
+ *
612
+ * @example
613
+ * // Update container content
614
+ * renderer.patchDOM(container, '<div class="updated">New content</div>');
615
+ *
616
+ * @example
617
+ * // Update list with keys for optimal diffing
618
+ * const items = ['a', 'b', 'c'];
619
+ * const html = items.map(item =>
620
+ * `<li key="${item}">${item}</li>`
621
+ * ).join('');
622
+ * renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
298
623
  */
299
624
  patchDOM(container, newHtml) {
300
625
  if (!(container instanceof HTMLElement)) {
@@ -412,35 +737,24 @@ class Renderer {
412
737
  const oldAttrs = oldEl.attributes;
413
738
  const newAttrs = newEl.attributes;
414
739
 
415
- // Single pass for new/updated attributes
740
+ // Process new attributes
416
741
  for (let i = 0; i < newAttrs.length; i++) {
417
742
  const {
418
743
  name,
419
744
  value
420
745
  } = newAttrs[i];
746
+
747
+ // Skip event attributes (handled by event system)
421
748
  if (name.startsWith("@")) continue;
749
+
750
+ // Skip if attribute hasn't changed
422
751
  if (oldEl.getAttribute(name) === value) continue;
752
+
753
+ // Basic attribute setting
423
754
  oldEl.setAttribute(name, value);
424
- if (name.startsWith("aria-")) {
425
- const prop = "aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
426
- oldEl[prop] = value;
427
- } else if (name.startsWith("data-")) {
428
- oldEl.dataset[name.slice(5)] = value;
429
- } else {
430
- const prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
431
- if (prop in oldEl) {
432
- const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop);
433
- const isBoolean = typeof oldEl[prop] === "boolean" || descriptor?.get && typeof descriptor.get.call(oldEl) === "boolean";
434
- if (isBoolean) {
435
- oldEl[prop] = value !== "false" && (value === "" || value === prop || value === "true");
436
- } else {
437
- oldEl[prop] = value;
438
- }
439
- }
440
- }
441
755
  }
442
756
 
443
- // Remove any attributes no longer present
757
+ // Remove old attributes that are no longer present
444
758
  for (let i = oldAttrs.length - 1; i >= 0; i--) {
445
759
  const name = oldAttrs[i].name;
446
760
  if (!newEl.hasAttribute(name)) {
@@ -467,12 +781,13 @@ class Renderer {
467
781
 
468
782
  /**
469
783
  * Creates a key map for the children of a parent node.
784
+ * Used for efficient O(1) lookup of keyed elements during diffing.
470
785
  *
471
786
  * @private
472
- * @param {Array<Node>} children - The children of the parent node.
787
+ * @param {Array<ChildNode>} children - The children of the parent node.
473
788
  * @param {number} start - The start index of the children.
474
789
  * @param {number} end - The end index of the children.
475
- * @returns {Map<string, Node>} A key map for the children.
790
+ * @returns {KeyMap} A key map for the children.
476
791
  */
477
792
  _createKeyMap(children, start, end) {
478
793
  const map = new Map();
@@ -496,43 +811,129 @@ class Renderer {
496
811
  }
497
812
  }
498
813
 
814
+ // ============================================================================
815
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
816
+ // ============================================================================
817
+
818
+ // -----------------------------------------------------------------------------
819
+ // Configuration Types
820
+ // -----------------------------------------------------------------------------
821
+
822
+ /**
823
+ * @typedef {Object} ElevaConfig
824
+ * @property {boolean} [debug=false]
825
+ * Enable debug mode for verbose logging
826
+ * @property {string} [prefix='e']
827
+ * Prefix for component style scoping
828
+ * @property {boolean} [async=true]
829
+ * Enable async component setup
830
+ */
831
+
832
+ // -----------------------------------------------------------------------------
833
+ // Component Types
834
+ // -----------------------------------------------------------------------------
835
+
499
836
  /**
500
837
  * @typedef {Object} ComponentDefinition
501
- * @property {function(ComponentContext): (Record<string, unknown>|Promise<Record<string, unknown>>)} [setup]
838
+ * @property {SetupFunction} [setup]
502
839
  * Optional setup function that initializes the component's state and returns reactive data
503
- * @property {(function(ComponentContext): string|Promise<string>)} template
504
- * Required function that defines the component's HTML structure
505
- * @property {(function(ComponentContext): string)|string} [style]
840
+ * @property {TemplateFunction|string} template
841
+ * Required function or string that defines the component's HTML structure
842
+ * @property {StyleFunction|string} [style]
506
843
  * Optional function or string that provides component-scoped CSS styles
507
- * @property {Record<string, ComponentDefinition>} [children]
844
+ * @property {ChildrenMap} [children]
508
845
  * Optional object defining nested child components
509
846
  */
510
847
 
848
+ /**
849
+ * @callback SetupFunction
850
+ * @param {ComponentContext} ctx - The component context with props, emitter, and signal factory
851
+ * @returns {SetupResult|Promise<SetupResult>} Reactive data and lifecycle hooks
852
+ */
853
+
854
+ /**
855
+ * @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
856
+ * Data returned from setup function, may include lifecycle hooks
857
+ */
858
+
859
+ /**
860
+ * @callback TemplateFunction
861
+ * @param {ComponentContext} ctx - The component context
862
+ * @returns {string|Promise<string>} HTML template string
863
+ */
864
+
865
+ /**
866
+ * @callback StyleFunction
867
+ * @param {ComponentContext} ctx - The component context
868
+ * @returns {string} CSS styles string
869
+ */
870
+
871
+ /**
872
+ * @typedef {Record<string, ComponentDefinition|string>} ChildrenMap
873
+ * Map of CSS selectors to component definitions or registered component names
874
+ */
875
+
876
+ // -----------------------------------------------------------------------------
877
+ // Context Types
878
+ // -----------------------------------------------------------------------------
879
+
511
880
  /**
512
881
  * @typedef {Object} ComponentContext
513
- * @property {Record<string, unknown>} props
882
+ * @property {ComponentProps} props
514
883
  * Component properties passed during mounting
515
884
  * @property {Emitter} emitter
516
885
  * Event emitter instance for component event handling
517
- * @property {function<T>(value: T): Signal<T>} signal
886
+ * @property {SignalFactory} signal
518
887
  * Factory function to create reactive Signal instances
519
- * @property {function(LifecycleHookContext): Promise<void>} [onBeforeMount]
888
+ */
889
+
890
+ /**
891
+ * @typedef {Record<string, unknown>} ComponentProps
892
+ * Properties passed to a component during mounting
893
+ */
894
+
895
+ /**
896
+ * @callback SignalFactory
897
+ * @template T
898
+ * @param {T} initialValue - The initial value for the signal
899
+ * @returns {Signal<T>} A new Signal instance
900
+ */
901
+
902
+ // -----------------------------------------------------------------------------
903
+ // Lifecycle Hook Types
904
+ // -----------------------------------------------------------------------------
905
+
906
+ /**
907
+ * @typedef {Object} LifecycleHooks
908
+ * @property {LifecycleHook} [onBeforeMount]
520
909
  * Hook called before component mounting
521
- * @property {function(LifecycleHookContext): Promise<void>} [onMount]
910
+ * @property {LifecycleHook} [onMount]
522
911
  * Hook called after component mounting
523
- * @property {function(LifecycleHookContext): Promise<void>} [onBeforeUpdate]
912
+ * @property {LifecycleHook} [onBeforeUpdate]
524
913
  * Hook called before component update
525
- * @property {function(LifecycleHookContext): Promise<void>} [onUpdate]
914
+ * @property {LifecycleHook} [onUpdate]
526
915
  * Hook called after component update
527
- * @property {function(UnmountHookContext): Promise<void>} [onUnmount]
916
+ * @property {UnmountHook} [onUnmount]
528
917
  * Hook called during component unmounting
529
918
  */
530
919
 
920
+ /**
921
+ * @callback LifecycleHook
922
+ * @param {LifecycleHookContext} ctx - Context with container and component data
923
+ * @returns {void|Promise<void>}
924
+ */
925
+
926
+ /**
927
+ * @callback UnmountHook
928
+ * @param {UnmountHookContext} ctx - Context with cleanup resources
929
+ * @returns {void|Promise<void>}
930
+ */
931
+
531
932
  /**
532
933
  * @typedef {Object} LifecycleHookContext
533
934
  * @property {HTMLElement} container
534
935
  * The DOM element where the component is mounted
535
- * @property {ComponentContext} context
936
+ * @property {ComponentContext & SetupResult} context
536
937
  * The component's reactive state and context data
537
938
  */
538
939
 
@@ -540,32 +941,91 @@ class Renderer {
540
941
  * @typedef {Object} UnmountHookContext
541
942
  * @property {HTMLElement} container
542
943
  * The DOM element where the component is mounted
543
- * @property {ComponentContext} context
944
+ * @property {ComponentContext & SetupResult} context
544
945
  * The component's reactive state and context data
545
- * @property {{
546
- * watchers: Array<() => void>, // Signal watcher cleanup functions
547
- * listeners: Array<() => void>, // Event listener cleanup functions
548
- * children: Array<MountResult> // Child component instances
549
- * }} cleanup
946
+ * @property {CleanupResources} cleanup
550
947
  * Object containing cleanup functions and instances
551
948
  */
552
949
 
950
+ /**
951
+ * @typedef {Object} CleanupResources
952
+ * @property {Array<UnsubscribeFunction>} watchers
953
+ * Signal watcher cleanup functions
954
+ * @property {Array<UnsubscribeFunction>} listeners
955
+ * Event listener cleanup functions
956
+ * @property {Array<MountResult>} children
957
+ * Child component instances
958
+ */
959
+
960
+ // -----------------------------------------------------------------------------
961
+ // Mount Result Types
962
+ // -----------------------------------------------------------------------------
963
+
553
964
  /**
554
965
  * @typedef {Object} MountResult
555
966
  * @property {HTMLElement} container
556
967
  * The DOM element where the component is mounted
557
- * @property {ComponentContext} data
968
+ * @property {ComponentContext & SetupResult} data
558
969
  * The component's reactive state and context data
559
- * @property {function(): Promise<void>} unmount
970
+ * @property {UnmountFunction} unmount
560
971
  * Function to clean up and unmount the component
561
972
  */
562
973
 
974
+ /**
975
+ * @callback UnmountFunction
976
+ * @returns {Promise<void>}
977
+ */
978
+
979
+ /**
980
+ * @callback UnsubscribeFunction
981
+ * @returns {void|boolean}
982
+ */
983
+
984
+ // -----------------------------------------------------------------------------
985
+ // Plugin Types
986
+ // -----------------------------------------------------------------------------
987
+
563
988
  /**
564
989
  * @typedef {Object} ElevaPlugin
565
- * @property {function(Eleva, Record<string, unknown>): void} install
990
+ * @property {PluginInstallFunction} install
566
991
  * Function that installs the plugin into the Eleva instance
567
992
  * @property {string} name
568
993
  * Unique identifier name for the plugin
994
+ * @property {PluginUninstallFunction} [uninstall]
995
+ * Optional function to uninstall the plugin
996
+ */
997
+
998
+ /**
999
+ * @callback PluginInstallFunction
1000
+ * @param {Eleva} eleva - The Eleva instance
1001
+ * @param {PluginOptions} options - Plugin configuration options
1002
+ * @returns {void|Eleva|unknown} Optionally returns the Eleva instance or plugin result
1003
+ */
1004
+
1005
+ /**
1006
+ * @callback PluginUninstallFunction
1007
+ * @param {Eleva} eleva - The Eleva instance
1008
+ * @returns {void}
1009
+ */
1010
+
1011
+ /**
1012
+ * @typedef {Record<string, unknown>} PluginOptions
1013
+ * Configuration options passed to a plugin during installation
1014
+ */
1015
+
1016
+ // -----------------------------------------------------------------------------
1017
+ // Event Types
1018
+ // -----------------------------------------------------------------------------
1019
+
1020
+ /**
1021
+ * @callback EventHandler
1022
+ * @param {Event} event - The DOM event object
1023
+ * @returns {void}
1024
+ */
1025
+
1026
+ /**
1027
+ * @typedef {'click'|'submit'|'input'|'change'|'focus'|'blur'|'keydown'|'keyup'|'keypress'|'mouseenter'|'mouseleave'|'mouseover'|'mouseout'|'mousedown'|'mouseup'|'touchstart'|'touchend'|'touchmove'|'scroll'|'resize'|'load'|'error'|string} DOMEventName
1028
+ * Common DOM event names (prefixed with @ in templates)
569
1029
  */
570
1030
 
571
1031
  /**
@@ -625,6 +1085,8 @@ class Eleva {
625
1085
  this.emitter = new Emitter();
626
1086
  /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
627
1087
  this.signal = Signal;
1088
+ /** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */
1089
+ this.templateEngine = TemplateEngine;
628
1090
  /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
629
1091
  this.renderer = new Renderer();
630
1092
 
@@ -632,8 +1094,8 @@ class Eleva {
632
1094
  this._components = new Map();
633
1095
  /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
634
1096
  this._plugins = new Map();
635
- /** @private {boolean} Flag indicating if the root component is currently mounted */
636
- this._isMounted = false;
1097
+ /** @private {number} Counter for generating unique component IDs */
1098
+ this._componentCounter = 0;
637
1099
  }
638
1100
 
639
1101
  /**
@@ -641,17 +1103,28 @@ class Eleva {
641
1103
  * The plugin's install function will be called with the Eleva instance and provided options.
642
1104
  * After installation, the plugin will be available for use by components.
643
1105
  *
1106
+ * Note: Plugins that wrap core methods (e.g., mount) must be uninstalled in reverse order
1107
+ * of installation (LIFO - Last In, First Out) to avoid conflicts.
1108
+ *
644
1109
  * @public
645
1110
  * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
646
1111
  * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
647
1112
  * @returns {Eleva} The Eleva instance (for method chaining).
648
1113
  * @example
649
1114
  * app.use(myPlugin, { option1: "value1" });
1115
+ *
1116
+ * @example
1117
+ * // Correct uninstall order (LIFO)
1118
+ * app.use(PluginA);
1119
+ * app.use(PluginB);
1120
+ * // Uninstall in reverse order:
1121
+ * PluginB.uninstall(app);
1122
+ * PluginA.uninstall(app);
650
1123
  */
651
1124
  use(plugin, options = {}) {
652
- plugin.install(this, options);
653
1125
  this._plugins.set(plugin.name, plugin);
654
- return this;
1126
+ const result = plugin.install(this, options);
1127
+ return result !== undefined ? result : this;
655
1128
  }
656
1129
 
657
1130
  /**
@@ -702,6 +1175,9 @@ class Eleva {
702
1175
  const definition = typeof compName === "string" ? this._components.get(compName) : compName;
703
1176
  if (!definition) throw new Error(`Component "${compName}" not registered.`);
704
1177
 
1178
+ /** @type {string} */
1179
+ const compId = `c${++this._componentCounter}`;
1180
+
705
1181
  /**
706
1182
  * Destructure the component definition to access core functionality.
707
1183
  * - setup: Optional function for component initialization and state management
@@ -750,44 +1226,66 @@ class Eleva {
750
1226
  const childInstances = [];
751
1227
  /** @type {Array<() => void>} */
752
1228
  const listeners = [];
1229
+ /** @private {boolean} Local mounted state for this component instance */
1230
+ let isMounted = false;
753
1231
 
754
- // Execute before hooks
755
- if (!this._isMounted) {
756
- /** @type {LifecycleHookContext} */
757
- await mergedContext.onBeforeMount?.({
758
- container,
759
- context: mergedContext
760
- });
761
- } else {
762
- /** @type {LifecycleHookContext} */
763
- await mergedContext.onBeforeUpdate?.({
764
- container,
765
- context: mergedContext
1232
+ // ========================================================================
1233
+ // Render Batching
1234
+ // ========================================================================
1235
+
1236
+ /** @private {boolean} Flag to prevent multiple queued renders */
1237
+ let renderScheduled = false;
1238
+
1239
+ /**
1240
+ * Schedules a batched render on the next microtask.
1241
+ * Multiple signal changes within the same synchronous block are collapsed into one render.
1242
+ * @private
1243
+ */
1244
+ const scheduleRender = () => {
1245
+ if (renderScheduled) return;
1246
+ renderScheduled = true;
1247
+ queueMicrotask(async () => {
1248
+ renderScheduled = false;
1249
+ await render();
766
1250
  });
767
- }
1251
+ };
768
1252
 
769
1253
  /**
770
1254
  * Renders the component by:
771
- * 1. Processing the template
772
- * 2. Updating the DOM
773
- * 3. Processing events, injecting styles, and mounting child components.
1255
+ * 1. Executing lifecycle hooks
1256
+ * 2. Processing the template
1257
+ * 3. Updating the DOM
1258
+ * 4. Processing events, injecting styles, and mounting child components.
774
1259
  */
775
1260
  const render = async () => {
776
1261
  const templateResult = typeof template === "function" ? await template(mergedContext) : template;
777
- const newHtml = TemplateEngine.parse(templateResult, mergedContext);
778
- this.renderer.patchDOM(container, newHtml);
1262
+ const html = this.templateEngine.parse(templateResult, mergedContext);
1263
+
1264
+ // Execute before hooks
1265
+ if (!isMounted) {
1266
+ await mergedContext.onBeforeMount?.({
1267
+ container,
1268
+ context: mergedContext
1269
+ });
1270
+ } else {
1271
+ await mergedContext.onBeforeUpdate?.({
1272
+ container,
1273
+ context: mergedContext
1274
+ });
1275
+ }
1276
+ this.renderer.patchDOM(container, html);
779
1277
  this._processEvents(container, mergedContext, listeners);
780
- if (style) this._injectStyles(container, compName, style, mergedContext);
1278
+ if (style) this._injectStyles(container, compId, style, mergedContext);
781
1279
  if (children) await this._mountComponents(container, children, childInstances);
782
- if (!this._isMounted) {
783
- /** @type {LifecycleHookContext} */
1280
+
1281
+ // Execute after hooks
1282
+ if (!isMounted) {
784
1283
  await mergedContext.onMount?.({
785
1284
  container,
786
1285
  context: mergedContext
787
1286
  });
788
- this._isMounted = true;
1287
+ isMounted = true;
789
1288
  } else {
790
- /** @type {LifecycleHookContext} */
791
1289
  await mergedContext.onUpdate?.({
792
1290
  container,
793
1291
  context: mergedContext
@@ -797,11 +1295,12 @@ class Eleva {
797
1295
 
798
1296
  /**
799
1297
  * Sets up reactive watchers for all Signal instances in the component's data.
800
- * When a Signal's value changes, the component will re-render to reflect the updates.
1298
+ * When a Signal's value changes, a batched render is scheduled.
1299
+ * Multiple changes within the same frame are collapsed into one render.
801
1300
  * Stores unsubscribe functions to clean up watchers when component unmounts.
802
1301
  */
803
1302
  for (const val of Object.values(data)) {
804
- if (val instanceof Signal) watchers.push(val.watch(render));
1303
+ if (val instanceof Signal) watchers.push(val.watch(scheduleRender));
805
1304
  }
806
1305
  await render();
807
1306
  const instance = {
@@ -865,7 +1364,7 @@ class Eleva {
865
1364
  /** @type {string} */
866
1365
  const handlerName = attr.value;
867
1366
  /** @type {(event: Event) => void} */
868
- const handler = context[handlerName] || TemplateEngine.evaluate(handlerName, context);
1367
+ const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
869
1368
  if (typeof handler === "function") {
870
1369
  el.addEventListener(event, handler);
871
1370
  el.removeAttribute(attr.name);
@@ -881,20 +1380,21 @@ class Eleva {
881
1380
  *
882
1381
  * @private
883
1382
  * @param {HTMLElement} container - The container element where styles should be injected.
884
- * @param {string} compName - The component name used to identify the style element.
1383
+ * @param {string} compId - The component ID used to identify the style element.
885
1384
  * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
886
1385
  * @param {ComponentContext} context - The current component context for style interpolation.
887
1386
  * @returns {void}
888
1387
  */
889
- _injectStyles(container, compName, styleDef, context) {
1388
+ _injectStyles(container, compId, styleDef, context) {
890
1389
  /** @type {string} */
891
- const newStyle = typeof styleDef === "function" ? TemplateEngine.parse(styleDef(context), context) : styleDef;
1390
+ const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
1391
+
892
1392
  /** @type {HTMLStyleElement|null} */
893
- let styleEl = container.querySelector(`style[data-e-style="${compName}"]`);
1393
+ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
894
1394
  if (styleEl && styleEl.textContent === newStyle) return;
895
1395
  if (!styleEl) {
896
1396
  styleEl = document.createElement("style");
897
- styleEl.setAttribute("data-e-style", compName);
1397
+ styleEl.setAttribute("data-e-style", compId);
898
1398
  container.appendChild(styleEl);
899
1399
  }
900
1400
  styleEl.textContent = newStyle;
@@ -906,21 +1406,20 @@ class Eleva {
906
1406
  *
907
1407
  * @private
908
1408
  * @param {HTMLElement} element - The DOM element to extract props from
909
- * @param {string} prefix - The prefix to look for in attributes
910
1409
  * @returns {Record<string, string>} An object containing the extracted props
911
1410
  * @example
912
1411
  * // For an element with attributes:
913
1412
  * // <div :name="John" :age="25">
914
1413
  * // Returns: { name: "John", age: "25" }
915
1414
  */
916
- _extractProps(element, prefix) {
1415
+ _extractProps(element) {
917
1416
  if (!element.attributes) return {};
918
1417
  const props = {};
919
1418
  const attrs = element.attributes;
920
1419
  for (let i = attrs.length - 1; i >= 0; i--) {
921
1420
  const attr = attrs[i];
922
- if (attr.name.startsWith(prefix)) {
923
- const propName = attr.name.slice(prefix.length);
1421
+ if (attr.name.startsWith(":")) {
1422
+ const propName = attr.name.slice(1);
924
1423
  props[propName] = attr.value;
925
1424
  element.removeAttribute(attr.name);
926
1425
  }
@@ -955,7 +1454,7 @@ class Eleva {
955
1454
  for (const el of container.querySelectorAll(selector)) {
956
1455
  if (!(el instanceof HTMLElement)) continue;
957
1456
  /** @type {Record<string, string>} */
958
- const props = this._extractProps(el, ":");
1457
+ const props = this._extractProps(el);
959
1458
  /** @type {MountResult} */
960
1459
  const instance = await this.mount(el, component, props);
961
1460
  if (instance && !childInstances.includes(instance)) {