eleva 1.0.0-rc.11 → 1.0.0-rc.13

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 (55) hide show
  1. package/README.md +15 -15
  2. package/dist/eleva-plugins.cjs.js +1763 -2160
  3. package/dist/eleva-plugins.cjs.js.map +1 -1
  4. package/dist/eleva-plugins.esm.js +1763 -2160
  5. package/dist/eleva-plugins.esm.js.map +1 -1
  6. package/dist/eleva-plugins.umd.js +1763 -2160
  7. package/dist/eleva-plugins.umd.js.map +1 -1
  8. package/dist/eleva-plugins.umd.min.js +1 -2
  9. package/dist/eleva-plugins.umd.min.js.map +1 -1
  10. package/dist/eleva.cjs.js +626 -783
  11. package/dist/eleva.cjs.js.map +1 -1
  12. package/dist/eleva.d.ts +108 -92
  13. package/dist/eleva.esm.js +626 -783
  14. package/dist/eleva.esm.js.map +1 -1
  15. package/dist/eleva.umd.js +626 -783
  16. package/dist/eleva.umd.js.map +1 -1
  17. package/dist/eleva.umd.min.js +1 -2
  18. package/dist/eleva.umd.min.js.map +1 -1
  19. package/dist/plugins/attr.umd.js +117 -153
  20. package/dist/plugins/attr.umd.js.map +1 -1
  21. package/dist/plugins/attr.umd.min.js +1 -2
  22. package/dist/plugins/attr.umd.min.js.map +1 -1
  23. package/dist/plugins/props.umd.js +346 -398
  24. package/dist/plugins/props.umd.js.map +1 -1
  25. package/dist/plugins/props.umd.min.js +1 -2
  26. package/dist/plugins/props.umd.min.js.map +1 -1
  27. package/dist/plugins/router.umd.js +889 -1114
  28. package/dist/plugins/router.umd.js.map +1 -1
  29. package/dist/plugins/router.umd.min.js +1 -2
  30. package/dist/plugins/router.umd.min.js.map +1 -1
  31. package/dist/plugins/store.umd.js +412 -496
  32. package/dist/plugins/store.umd.js.map +1 -1
  33. package/dist/plugins/store.umd.min.js +1 -2
  34. package/dist/plugins/store.umd.min.js.map +1 -1
  35. package/package.json +5 -5
  36. package/src/core/Eleva.js +24 -6
  37. package/src/modules/Emitter.js +5 -6
  38. package/src/modules/Renderer.js +182 -160
  39. package/src/modules/Signal.js +16 -28
  40. package/src/modules/TemplateEngine.js +21 -2
  41. package/src/plugins/Attr.js +7 -12
  42. package/src/plugins/Props.js +29 -20
  43. package/src/plugins/Router.js +1 -1
  44. package/src/plugins/Store.js +1 -1
  45. package/types/core/Eleva.d.ts +3 -2
  46. package/types/core/Eleva.d.ts.map +1 -1
  47. package/types/modules/Emitter.d.ts.map +1 -1
  48. package/types/modules/Renderer.d.ts +85 -88
  49. package/types/modules/Renderer.d.ts.map +1 -1
  50. package/types/modules/Signal.d.ts +11 -16
  51. package/types/modules/Signal.d.ts.map +1 -1
  52. package/types/modules/TemplateEngine.d.ts +10 -1
  53. package/types/modules/TemplateEngine.d.ts.map +1 -1
  54. package/types/plugins/Attr.d.ts.map +1 -1
  55. package/types/plugins/Props.d.ts.map +1 -1
package/dist/eleva.umd.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! Eleva v1.0.0-rc.11 | MIT License | https://elevajs.com */
1
+ /*! Eleva v1.0.0-rc.13 | MIT License | https://elevajs.com */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -8,28 +8,19 @@
8
8
  // ============================================================================
9
9
  // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
10
10
  // ============================================================================
11
-
12
11
  /**
13
12
  * @typedef {Record<string, unknown>} TemplateData
14
13
  * Data context for template interpolation
15
- */
16
-
17
- /**
14
+ */ /**
18
15
  * @typedef {string} TemplateString
19
16
  * A string containing {{ expression }} interpolation markers
20
- */
21
-
22
- /**
17
+ */ /**
23
18
  * @typedef {string} Expression
24
19
  * A JavaScript expression to be evaluated in the data context
25
- */
26
-
27
- /**
20
+ */ /**
28
21
  * @typedef {unknown} EvaluationResult
29
22
  * The result of evaluating an expression (string, number, boolean, object, etc.)
30
- */
31
-
32
- /**
23
+ */ /**
33
24
  * @class 🔒 TemplateEngine
34
25
  * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
35
26
  * Provides a way to evaluate expressions in templates.
@@ -69,19 +60,8 @@
69
60
  * const data = { count: { value: 42 } };
70
61
  * const result = TemplateEngine.parse(template, data);
71
62
  * // Result: "Count: 42"
72
- */
73
- class TemplateEngine {
74
- /**
75
- * Regular expression for matching template expressions in the format {{ expression }}
76
- * Matches: {{ anything }} with optional whitespace inside braces
77
- *
78
- * @static
79
- * @private
80
- * @type {RegExp}
81
- */
82
- static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
83
-
84
- /**
63
+ */ class TemplateEngine {
64
+ /**
85
65
  * Parses a template string, replacing expressions with their evaluated values.
86
66
  * Expressions are evaluated in the provided data context.
87
67
  *
@@ -118,13 +98,11 @@
118
98
  * online: true
119
99
  * });
120
100
  * // Result: "Status: Active"
121
- */
122
- static parse(template, data) {
123
- if (typeof template !== "string") return template;
124
- return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
125
- }
126
-
127
- /**
101
+ */ static parse(template, data) {
102
+ if (typeof template !== "string") return template;
103
+ return template.replace(this.expressionPattern, (_, expression)=>this.evaluate(expression, data));
104
+ }
105
+ /**
128
106
  * Evaluates an expression in the context of the provided data object.
129
107
  *
130
108
  * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
@@ -135,7 +113,7 @@
135
113
  * @static
136
114
  * @param {Expression|unknown} expression - The expression to evaluate.
137
115
  * @param {TemplateData} data - The data context for evaluation.
138
- * @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
116
+ * @returns {EvaluationResult} The result of the evaluation, or empty string if evaluation fails.
139
117
  *
140
118
  * @example
141
119
  * // Property access
@@ -164,46 +142,64 @@
164
142
  * // Failed evaluation returns empty string
165
143
  * TemplateEngine.evaluate("nonexistent.property", {});
166
144
  * // Result: ""
167
- */
168
- static evaluate(expression, data) {
169
- if (typeof expression !== "string") return expression;
170
- try {
171
- return new Function("data", `with(data) { return ${expression}; }`)(data);
172
- } catch {
173
- return "";
145
+ */ static evaluate(expression, data) {
146
+ if (typeof expression !== "string") return expression;
147
+ let fn = this._functionCache.get(expression);
148
+ if (!fn) {
149
+ try {
150
+ fn = new Function("data", `with(data) { return ${expression}; }`);
151
+ this._functionCache.set(expression, fn);
152
+ } catch {
153
+ return "";
154
+ }
155
+ }
156
+ try {
157
+ return fn(data);
158
+ } catch {
159
+ return "";
160
+ }
174
161
  }
175
- }
176
162
  }
163
+ /**
164
+ * Regular expression for matching template expressions in the format {{ expression }}
165
+ * Matches: {{ anything }} with optional whitespace inside braces
166
+ *
167
+ * @static
168
+ * @private
169
+ * @type {RegExp}
170
+ */ TemplateEngine.expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
171
+ /**
172
+ * Cache for compiled expression functions.
173
+ * Stores compiled Function objects keyed by expression string for O(1) lookup.
174
+ *
175
+ * @static
176
+ * @private
177
+ * @type {Map<string, Function>}
178
+ */ TemplateEngine._functionCache = new Map();
177
179
 
178
180
  // ============================================================================
179
181
  // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
180
182
  // ============================================================================
181
-
182
183
  /**
183
184
  * @template T
184
185
  * @callback SignalWatcher
185
186
  * @param {T} value - The new value of the signal
186
187
  * @returns {void}
187
- */
188
-
189
- /**
188
+ */ /**
190
189
  * @callback SignalUnsubscribe
191
190
  * @returns {boolean} True if the watcher was successfully removed
192
- */
193
-
194
- /**
191
+ */ /**
195
192
  * @template T
196
193
  * @typedef {Object} SignalLike
197
194
  * @property {T} value - The current value
198
195
  * @property {function(SignalWatcher<T>): SignalUnsubscribe} watch - Subscribe to changes
199
- */
200
-
201
- /**
196
+ */ /**
202
197
  * @class ⚡ Signal
203
198
  * @classdesc A reactive data holder that enables fine-grained reactivity in the Eleva framework.
204
- * Signals notify registered watchers when their value changes, enabling efficient DOM updates
205
- * through targeted patching rather than full re-renders.
206
- * Updates are batched using microtasks to prevent multiple synchronous notifications.
199
+ * Signals notify registered watchers synchronously when their value changes, enabling efficient
200
+ * DOM updates through targeted patching rather than full re-renders.
201
+ * Synchronous notification preserves stack traces and allows immediate value inspection.
202
+ * Render batching is handled at the component level, not the signal level.
207
203
  * The class is generic, allowing type-safe handling of any value type T.
208
204
  *
209
205
  * @template T The type of value held by this signal
@@ -229,74 +225,29 @@
229
225
  * position.value = { x: 10, y: 20 }; // Triggers watchers
230
226
  *
231
227
  * @implements {SignalLike<T>}
232
- */
233
- class Signal {
234
- /**
235
- * Creates a new Signal instance with the specified initial value.
236
- *
237
- * @public
238
- * @param {T} value - The initial value of the signal.
239
- *
240
- * @example
241
- * // Primitive types
242
- * const count = new Signal(0); // Signal<number>
243
- * const name = new Signal("John"); // Signal<string>
244
- * const active = new Signal(true); // Signal<boolean>
245
- *
246
- * @example
247
- * // Complex types (use JSDoc for type inference)
248
- * /** @type {Signal<string[]>} *\/
249
- * const items = new Signal([]);
250
- *
251
- * /** @type {Signal<{id: number, name: string} | null>} *\/
252
- * const user = new Signal(null);
253
- */
254
- constructor(value) {
228
+ */ class Signal {
255
229
  /**
256
- * Internal storage for the signal's current value
257
- * @private
258
- * @type {T}
259
- */
260
- this._value = value;
261
- /**
262
- * Collection of callback functions to be notified when value changes
263
- * @private
264
- * @type {Set<SignalWatcher<T>>}
265
- */
266
- this._watchers = new Set();
267
- /**
268
- * Flag to prevent multiple synchronous watcher notifications
269
- * @private
270
- * @type {boolean}
271
- */
272
- this._pending = false;
273
- }
274
-
275
- /**
276
230
  * Gets the current value of the signal.
277
231
  *
278
232
  * @public
279
233
  * @returns {T} The current value.
280
- */
281
- get value() {
282
- return this._value;
283
- }
284
-
285
- /**
286
- * Sets a new value for the signal and notifies all registered watchers if the value has changed.
287
- * The notification is batched using microtasks to prevent multiple synchronous updates.
234
+ */ get value() {
235
+ return this._value;
236
+ }
237
+ /**
238
+ * Sets a new value for the signal and synchronously notifies all registered watchers if the value has changed.
239
+ * Synchronous notification preserves stack traces and ensures immediate value consistency.
288
240
  *
289
241
  * @public
290
242
  * @param {T} newVal - The new value to set.
291
243
  * @returns {void}
292
- */
293
- set value(newVal) {
294
- if (this._value === newVal) return;
295
- this._value = newVal;
296
- this._notify();
297
- }
298
-
299
- /**
244
+ */ set value(newVal) {
245
+ if (this._value !== newVal) {
246
+ this._value = newVal;
247
+ this._notify();
248
+ }
249
+ }
250
+ /**
300
251
  * Registers a watcher function that will be called whenever the signal's value changes.
301
252
  * The watcher will receive the new value as its argument.
302
253
  *
@@ -317,60 +268,73 @@
317
268
  * const unsub1 = signal.watch((v) => console.log("Watcher 1:", v));
318
269
  * const unsub2 = signal.watch((v) => console.log("Watcher 2:", v));
319
270
  * signal.value = "test"; // Both watchers are called
320
- */
321
- watch(fn) {
322
- this._watchers.add(fn);
323
- return () => this._watchers.delete(fn);
324
- }
325
-
326
- /**
327
- * Notifies all registered watchers of a value change using microtask scheduling.
328
- * Uses a pending flag to batch multiple synchronous updates into a single notification.
329
- * All watcher callbacks receive the current value when executed.
271
+ */ watch(fn) {
272
+ this._watchers.add(fn);
273
+ return ()=>this._watchers.delete(fn);
274
+ }
275
+ /**
276
+ * Synchronously notifies all registered watchers of the value change.
277
+ * This preserves stack traces for debugging and ensures immediate
278
+ * value consistency. Render batching is handled at the component level.
330
279
  *
331
280
  * @private
332
281
  * @returns {void}
333
- */
334
- _notify() {
335
- if (this._pending) return;
336
- this._pending = true;
337
- queueMicrotask(() => {
338
- /** @type {(fn: (value: T) => void) => void} */
339
- this._watchers.forEach(fn => fn(this._value));
340
- this._pending = false;
341
- });
342
- }
282
+ */ _notify() {
283
+ for (const fn of this._watchers)fn(this._value);
284
+ }
285
+ /**
286
+ * Creates a new Signal instance with the specified initial value.
287
+ *
288
+ * @public
289
+ * @param {T} value - The initial value of the signal.
290
+ *
291
+ * @example
292
+ * // Primitive types
293
+ * const count = new Signal(0); // Signal<number>
294
+ * const name = new Signal("John"); // Signal<string>
295
+ * const active = new Signal(true); // Signal<boolean>
296
+ *
297
+ * @example
298
+ * // Complex types (use JSDoc for type inference)
299
+ * /** @type {Signal<string[]>} *\/
300
+ * const items = new Signal([]);
301
+ *
302
+ * /** @type {Signal<{id: number, name: string} | null>} *\/
303
+ * const user = new Signal(null);
304
+ */ constructor(value){
305
+ /**
306
+ * Internal storage for the signal's current value.
307
+ * @private
308
+ * @type {T}
309
+ */ this._value = value;
310
+ /**
311
+ * Collection of callback functions to be notified when value changes.
312
+ * @private
313
+ * @type {Set<SignalWatcher<T>>}
314
+ */ this._watchers = new Set();
315
+ }
343
316
  }
344
317
 
345
318
  // ============================================================================
346
319
  // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
347
320
  // ============================================================================
348
-
349
321
  /**
350
322
  * @template T
351
323
  * @callback EventHandler
352
324
  * @param {...T} args - Event arguments
353
325
  * @returns {void|Promise<void>}
354
- */
355
-
356
- /**
326
+ */ /**
357
327
  * @callback EventUnsubscribe
358
328
  * @returns {void}
359
- */
360
-
361
- /**
329
+ */ /**
362
330
  * @typedef {`${string}:${string}`} EventName
363
331
  * Event names follow the format 'namespace:action' (e.g., 'user:login', 'cart:update')
364
- */
365
-
366
- /**
332
+ */ /**
367
333
  * @typedef {Object} EmitterLike
368
334
  * @property {function(string, EventHandler<unknown>): EventUnsubscribe} on - Subscribe to an event
369
335
  * @property {function(string, EventHandler<unknown>=): void} off - Unsubscribe from an event
370
336
  * @property {function(string, ...unknown): void} emit - Emit an event
371
- */
372
-
373
- /**
337
+ */ /**
374
338
  * @class 📡 Emitter
375
339
  * @classdesc A robust event emitter that enables inter-component communication through a publish-subscribe pattern.
376
340
  * Components can emit events and listen for events from other components, facilitating loose coupling
@@ -413,26 +377,8 @@
413
377
  * emitter.on('router:navigate', (to, from) => {});
414
378
  *
415
379
  * @implements {EmitterLike}
416
- */
417
- class Emitter {
418
- /**
419
- * Creates a new Emitter instance.
420
- *
421
- * @public
422
- *
423
- * @example
424
- * const emitter = new Emitter();
425
- */
426
- constructor() {
380
+ */ class Emitter {
427
381
  /**
428
- * Map of event names to their registered handler functions
429
- * @private
430
- * @type {Map<string, Set<EventHandler<unknown>>>}
431
- */
432
- this._events = new Map();
433
- }
434
-
435
- /**
436
382
  * Registers an event handler for the specified event name.
437
383
  * The handler will be called with the event data when the event is emitted.
438
384
  * Event names should follow the format 'namespace:action' for consistency.
@@ -456,14 +402,13 @@
456
402
  * @example
457
403
  * // Cleanup
458
404
  * unsubscribe(); // Stops listening for the event
459
- */
460
- on(event, handler) {
461
- if (!this._events.has(event)) this._events.set(event, new Set());
462
- this._events.get(event).add(handler);
463
- return () => this.off(event, handler);
464
- }
465
-
466
- /**
405
+ */ on(event, handler) {
406
+ let h = this._events.get(event);
407
+ if (!h) this._events.set(event, h = new Set());
408
+ h.add(handler);
409
+ return ()=>this.off(event, handler);
410
+ }
411
+ /**
467
412
  * Removes an event handler for the specified event name.
468
413
  * If no handler is provided, all handlers for the event are removed.
469
414
  * Automatically cleans up empty event sets to prevent memory leaks.
@@ -483,20 +428,17 @@
483
428
  * @example
484
429
  * // Remove all handlers for an event
485
430
  * emitter.off('user:login');
486
- */
487
- off(event, handler) {
488
- if (!this._events.has(event)) return;
489
- if (handler) {
490
- const handlers = this._events.get(event);
491
- handlers.delete(handler);
492
- // Remove the event if there are no handlers left
493
- if (handlers.size === 0) this._events.delete(event);
494
- } else {
495
- this._events.delete(event);
431
+ */ off(event, handler) {
432
+ if (!this._events.has(event)) return;
433
+ if (handler) {
434
+ const handlers = this._events.get(event);
435
+ handlers.delete(handler);
436
+ if (handlers.size === 0) this._events.delete(event);
437
+ } else {
438
+ this._events.delete(event);
439
+ }
496
440
  }
497
- }
498
-
499
- /**
441
+ /**
500
442
  * Emits an event with the specified data to all registered handlers.
501
443
  * Handlers are called synchronously in the order they were registered.
502
444
  * If no handlers are registered for the event, the emission is silently ignored.
@@ -518,313 +460,326 @@
518
460
  * @example
519
461
  * // Emit without data
520
462
  * emitter.emit('app:ready');
521
- */
522
- emit(event, ...args) {
523
- if (!this._events.has(event)) return;
524
- this._events.get(event).forEach(handler => handler(...args));
525
- }
463
+ */ emit(event, ...args) {
464
+ const handlers = this._events.get(event);
465
+ if (handlers) for (const handler of handlers)handler(...args);
466
+ }
467
+ /**
468
+ * Creates a new Emitter instance.
469
+ *
470
+ * @public
471
+ *
472
+ * @example
473
+ * const emitter = new Emitter();
474
+ */ constructor(){
475
+ /**
476
+ * Map of event names to their registered handler functions
477
+ * @private
478
+ * @type {Map<string, Set<EventHandler<unknown>>>}
479
+ */ this._events = new Map();
480
+ }
526
481
  }
527
482
 
528
483
  // ============================================================================
529
484
  // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
530
485
  // ============================================================================
531
-
532
- /**
533
- * @typedef {Object} PatchOptions
534
- * @property {boolean} [preserveStyles=true]
535
- * Whether to preserve style elements with data-e-style attribute
536
- * @property {boolean} [preserveInstances=true]
537
- * Whether to preserve elements with _eleva_instance property
538
- */
539
-
540
486
  /**
541
487
  * @typedef {Map<string, Node>} KeyMap
542
- * Map of key attribute values to their corresponding DOM nodes
543
- */
544
-
545
- /**
546
- * @typedef {'ELEMENT_NODE'|'TEXT_NODE'|'COMMENT_NODE'|'DOCUMENT_FRAGMENT_NODE'} NodeTypeName
547
- * Common DOM node type names
548
- */
549
-
488
+ * Map of key attribute values to their corresponding DOM nodes for O(1) lookup
489
+ */ /**
490
+ * @typedef {Object} RendererLike
491
+ * @property {function(HTMLElement, string): void} patchDOM - Patches the DOM with new HTML
492
+ */ /**
493
+ * Properties that can diverge from attributes via user interaction.
494
+ * @private
495
+ * @type {string[]}
496
+ */ const SYNC_PROPS = [
497
+ "value",
498
+ "checked",
499
+ "selected"
500
+ ];
550
501
  /**
551
502
  * @class 🎨 Renderer
552
- * @classdesc A high-performance DOM renderer that implements an optimized direct DOM diffing algorithm.
503
+ * @classdesc A high-performance DOM renderer that implements an optimized two-pointer diffing
504
+ * algorithm with key-based node reconciliation. The renderer efficiently updates the DOM by
505
+ * computing the minimal set of operations needed to transform the current state to the desired state.
553
506
  *
554
507
  * Key features:
555
- * - Single-pass diffing algorithm for efficient DOM updates
556
- * - Key-based node reconciliation for optimal performance
557
- * - Intelligent attribute handling for ARIA, data attributes, and boolean properties
558
- * - Preservation of special Eleva-managed instances and style elements
559
- * - Memory-efficient with reusable temporary containers
560
- *
561
- * The renderer is designed to minimize DOM operations while maintaining
562
- * exact attribute synchronization and proper node identity preservation.
563
- * It's particularly optimized for frequent updates and complex DOM structures.
508
+ * - Two-pointer diffing algorithm for efficient DOM updates
509
+ * - Key-based node reconciliation for optimal list performance (O(1) lookup)
510
+ * - Preserves DOM node identity during reordering (maintains event listeners, focus, animations)
511
+ * - Intelligent attribute synchronization (skips Eleva event attributes)
512
+ * - Preservation of Eleva-managed component instances and style elements
564
513
  *
565
514
  * @example
566
515
  * // Basic usage
567
516
  * const renderer = new Renderer();
568
- * const container = document.getElementById("app");
569
- * const newHtml = "<div>Updated content</div>";
570
- * renderer.patchDOM(container, newHtml);
517
+ * renderer.patchDOM(container, '<div>Updated content</div>');
571
518
  *
572
519
  * @example
573
520
  * // With keyed elements for optimal list updates
574
- * const listHtml = `
575
- * <ul>
576
- * <li key="item-1">First</li>
577
- * <li key="item-2">Second</li>
578
- * <li key="item-3">Third</li>
579
- * </ul>
580
- * `;
581
- * renderer.patchDOM(container, listHtml);
521
+ * const html = items.map(item => `<li key="${item.id}">${item.name}</li>`).join('');
522
+ * renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
582
523
  *
583
524
  * @example
584
- * // The renderer preserves Eleva-managed elements
585
- * // Elements with _eleva_instance are not replaced during diffing
586
- * // Style elements with data-e-style are preserved
587
- */
588
- class Renderer {
589
- /**
590
- * Creates a new Renderer instance.
591
- *
592
- * @public
593
- *
594
- * @example
595
- * const renderer = new Renderer();
596
- */
597
- constructor() {
525
+ * // Keyed elements preserve DOM identity during reordering
526
+ * // Before: [A, B, C] -> After: [C, A, B]
527
+ * // The actual DOM nodes are moved, not recreated
528
+ * renderer.patchDOM(container, '<div key="C">C</div><div key="A">A</div><div key="B">B</div>');
529
+ *
530
+ * @implements {RendererLike}
531
+ */ class Renderer {
598
532
  /**
599
- * A temporary container to hold the new HTML content while diffing.
600
- * Reused across patch operations to minimize memory allocation.
601
- * @private
602
- * @type {HTMLDivElement}
603
- */
604
- this._tempContainer = document.createElement("div");
605
- }
606
-
607
- /**
608
533
  * Patches the DOM of the given container with the provided HTML string.
609
- * Uses an optimized diffing algorithm to minimize DOM operations.
534
+ * Uses an optimized two-pointer diffing algorithm to minimize DOM operations.
535
+ * The algorithm computes the minimal set of insertions, deletions, and updates
536
+ * needed to transform the current DOM state to match the new HTML.
610
537
  *
611
538
  * @public
612
539
  * @param {HTMLElement} container - The container element to patch.
613
- * @param {string} newHtml - The new HTML string.
540
+ * @param {string} newHtml - The new HTML string to render.
614
541
  * @returns {void}
615
- * @throws {TypeError} If container is not an HTMLElement or newHtml is not a string.
616
- * @throws {Error} If DOM patching fails.
617
542
  *
618
543
  * @example
619
- * // Update container content
544
+ * // Simple content update
620
545
  * renderer.patchDOM(container, '<div class="updated">New content</div>');
621
546
  *
622
547
  * @example
623
- * // Update list with keys for optimal diffing
624
- * const items = ['a', 'b', 'c'];
625
- * const html = items.map(item =>
626
- * `<li key="${item}">${item}</li>`
627
- * ).join('');
628
- * renderer.patchDOM(listContainer, `<ul>${html}</ul>`);
629
- */
630
- patchDOM(container, newHtml) {
631
- if (!(container instanceof HTMLElement)) {
632
- throw new TypeError("Container must be an HTMLElement");
633
- }
634
- if (typeof newHtml !== "string") {
635
- throw new TypeError("newHtml must be a string");
636
- }
637
- try {
638
- this._tempContainer.innerHTML = newHtml;
639
- this._diff(container, this._tempContainer);
640
- } catch (error) {
641
- throw new Error(`Failed to patch DOM: ${error.message}`);
548
+ * // List with keyed items (optimal for reordering)
549
+ * renderer.patchDOM(container, '<ul><li key="1">First</li><li key="2">Second</li></ul>');
550
+ *
551
+ * @example
552
+ * // Empty the container
553
+ * renderer.patchDOM(container, '');
554
+ */ patchDOM(container, newHtml) {
555
+ this._tempContainer.innerHTML = newHtml;
556
+ this._diff(container, this._tempContainer);
642
557
  }
643
- }
644
-
645
- /**
558
+ /**
646
559
  * Performs a diff between two DOM nodes and patches the old node to match the new node.
560
+ * Uses a two-pointer algorithm with key-based reconciliation for optimal performance.
561
+ *
562
+ * Algorithm overview:
563
+ * 1. Compare children from start using two pointers
564
+ * 2. For mismatches, build a key map lazily for O(1) lookup
565
+ * 3. Move or insert nodes as needed
566
+ * 4. Clean up remaining nodes at the end
647
567
  *
648
568
  * @private
649
- * @param {HTMLElement} oldParent - The original DOM element.
650
- * @param {HTMLElement} newParent - The new DOM element.
569
+ * @param {HTMLElement} oldParent - The original DOM element to update.
570
+ * @param {HTMLElement} newParent - The new DOM element with desired state.
651
571
  * @returns {void}
652
- */
653
- _diff(oldParent, newParent) {
654
- if (oldParent === newParent || oldParent.isEqualNode?.(newParent)) return;
655
- const oldChildren = Array.from(oldParent.childNodes);
656
- const newChildren = Array.from(newParent.childNodes);
657
- let oldStartIdx = 0,
658
- newStartIdx = 0;
659
- let oldEndIdx = oldChildren.length - 1;
660
- let newEndIdx = newChildren.length - 1;
661
- let oldKeyMap = null;
662
- while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
663
- let oldStartNode = oldChildren[oldStartIdx];
664
- let newStartNode = newChildren[newStartIdx];
665
- if (!oldStartNode) {
666
- oldStartNode = oldChildren[++oldStartIdx];
667
- } else if (this._isSameNode(oldStartNode, newStartNode)) {
668
- this._patchNode(oldStartNode, newStartNode);
669
- oldStartIdx++;
670
- newStartIdx++;
671
- } else {
672
- if (!oldKeyMap) {
673
- oldKeyMap = this._createKeyMap(oldChildren, oldStartIdx, oldEndIdx);
572
+ */ _diff(oldParent, newParent) {
573
+ // Early exit for leaf nodes (no children)
574
+ if (!oldParent.firstChild && !newParent.firstChild) return;
575
+ const oldChildren = Array.from(oldParent.childNodes);
576
+ const newChildren = Array.from(newParent.childNodes);
577
+ let oldStart = 0, newStart = 0;
578
+ let oldEnd = oldChildren.length - 1;
579
+ let newEnd = newChildren.length - 1;
580
+ let keyMap = null;
581
+ // Two-pointer algorithm with key-based reconciliation
582
+ while(oldStart <= oldEnd && newStart <= newEnd){
583
+ const oldNode = oldChildren[oldStart];
584
+ const newNode = newChildren[newStart];
585
+ if (!oldNode) {
586
+ oldStart++;
587
+ continue;
588
+ }
589
+ if (this._isSameNode(oldNode, newNode)) {
590
+ this._patchNode(oldNode, newNode);
591
+ oldStart++;
592
+ newStart++;
593
+ } else {
594
+ // Build key map lazily for O(1) lookup
595
+ if (!keyMap) {
596
+ keyMap = this._createKeyMap(oldChildren, oldStart, oldEnd);
597
+ }
598
+ const key = this._getNodeKey(newNode);
599
+ const matchedNode = key ? keyMap.get(key) : null;
600
+ // Only use matched node if tag also matches
601
+ if (matchedNode && matchedNode.nodeName === newNode.nodeName) {
602
+ // Move existing keyed node (preserves DOM identity)
603
+ this._patchNode(matchedNode, newNode);
604
+ oldParent.insertBefore(matchedNode, oldNode);
605
+ oldChildren[oldChildren.indexOf(matchedNode)] = null;
606
+ } else {
607
+ // Insert new node
608
+ oldParent.insertBefore(newNode.cloneNode(true), oldNode);
609
+ }
610
+ newStart++;
611
+ }
674
612
  }
675
- const key = this._getNodeKey(newStartNode);
676
- const oldNodeToMove = key ? oldKeyMap.get(key) : null;
677
- if (oldNodeToMove) {
678
- this._patchNode(oldNodeToMove, newStartNode);
679
- oldParent.insertBefore(oldNodeToMove, oldStartNode);
680
- oldChildren[oldChildren.indexOf(oldNodeToMove)] = null;
681
- } else {
682
- oldParent.insertBefore(newStartNode.cloneNode(true), oldStartNode);
613
+ // Add remaining new nodes
614
+ if (oldStart > oldEnd) {
615
+ const refNode = newChildren[newEnd + 1] ? oldChildren[oldStart] : null;
616
+ for(let i = newStart; i <= newEnd; i++){
617
+ if (newChildren[i]) {
618
+ oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
619
+ }
620
+ }
621
+ } else if (newStart > newEnd) {
622
+ for(let i = oldStart; i <= oldEnd; i++){
623
+ if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
624
+ }
683
625
  }
684
- newStartIdx++;
685
- }
686
- }
687
- if (oldStartIdx > oldEndIdx) {
688
- const refNode = newChildren[newEndIdx + 1] ? oldChildren[oldStartIdx] : null;
689
- for (let i = newStartIdx; i <= newEndIdx; i++) {
690
- if (newChildren[i]) oldParent.insertBefore(newChildren[i].cloneNode(true), refNode);
691
- }
692
- } else if (newStartIdx > newEndIdx) {
693
- for (let i = oldStartIdx; i <= oldEndIdx; i++) {
694
- if (oldChildren[i]) this._removeNode(oldParent, oldChildren[i]);
695
- }
696
626
  }
697
- }
698
-
699
- /**
700
- * Patches a single node.
627
+ /**
628
+ * Patches a single node, updating its content and attributes to match the new node.
629
+ * Handles text nodes by updating nodeValue, and element nodes by updating attributes
630
+ * and recursively diffing children.
631
+ *
632
+ * Skips nodes that are managed by Eleva component instances to prevent interference
633
+ * with nested component state.
701
634
  *
702
635
  * @private
703
- * @param {Node} oldNode - The original DOM node.
704
- * @param {Node} newNode - The new DOM node.
636
+ * @param {Node} oldNode - The original DOM node to update.
637
+ * @param {Node} newNode - The new DOM node with desired state.
705
638
  * @returns {void}
706
- */
707
- _patchNode(oldNode, newNode) {
708
- if (oldNode?._eleva_instance) return;
709
- if (!this._isSameNode(oldNode, newNode)) {
710
- oldNode.replaceWith(newNode.cloneNode(true));
711
- return;
712
- }
713
- if (oldNode.nodeType === Node.ELEMENT_NODE) {
714
- this._updateAttributes(oldNode, newNode);
715
- this._diff(oldNode, newNode);
716
- } else if (oldNode.nodeType === Node.TEXT_NODE && oldNode.nodeValue !== newNode.nodeValue) {
717
- oldNode.nodeValue = newNode.nodeValue;
639
+ */ _patchNode(oldNode, newNode) {
640
+ // Skip nodes managed by Eleva component instances
641
+ if (oldNode._eleva_instance) return;
642
+ if (oldNode.nodeType === 3) {
643
+ if (oldNode.nodeValue !== newNode.nodeValue) {
644
+ oldNode.nodeValue = newNode.nodeValue;
645
+ }
646
+ } else if (oldNode.nodeType === 1) {
647
+ this._updateAttributes(oldNode, newNode);
648
+ this._diff(oldNode, newNode);
649
+ }
718
650
  }
719
- }
720
-
721
- /**
722
- * Removes a node from its parent.
651
+ /**
652
+ * Removes a node from its parent, with special handling for Eleva-managed elements.
653
+ * Style elements with the `data-e-style` attribute are preserved to maintain
654
+ * component-scoped styles across re-renders.
723
655
  *
724
656
  * @private
725
- * @param {HTMLElement} parent - The parent element containing the node to remove.
657
+ * @param {HTMLElement} parent - The parent element containing the node.
726
658
  * @param {Node} node - The node to remove.
727
659
  * @returns {void}
728
- */
729
- _removeNode(parent, node) {
730
- if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
731
- parent.removeChild(node);
732
- }
733
-
734
- /**
660
+ */ _removeNode(parent, node) {
661
+ // Preserve Eleva-managed style elements
662
+ if (node.nodeName === "STYLE" && node.hasAttribute("data-e-style")) return;
663
+ parent.removeChild(node);
664
+ }
665
+ /**
735
666
  * Updates the attributes of an element to match a new element's attributes.
667
+ * Adds new attributes, updates changed values, and removes attributes no longer present.
668
+ * Also syncs DOM properties that can diverge from attributes after user interaction.
669
+ *
670
+ * Event attributes (prefixed with `@`) are skipped as they are handled separately
671
+ * by Eleva's event binding system.
736
672
  *
737
673
  * @private
738
674
  * @param {HTMLElement} oldEl - The original element to update.
739
- * @param {HTMLElement} newEl - The new element to update.
675
+ * @param {HTMLElement} newEl - The new element with target attributes.
740
676
  * @returns {void}
741
- */
742
- _updateAttributes(oldEl, newEl) {
743
- const oldAttrs = oldEl.attributes;
744
- const newAttrs = newEl.attributes;
745
-
746
- // Process new attributes
747
- for (let i = 0; i < newAttrs.length; i++) {
748
- const {
749
- name,
750
- value
751
- } = newAttrs[i];
752
-
753
- // Skip event attributes (handled by event system)
754
- if (name.startsWith("@")) continue;
755
-
756
- // Skip if attribute hasn't changed
757
- if (oldEl.getAttribute(name) === value) continue;
758
-
759
- // Basic attribute setting
760
- oldEl.setAttribute(name, value);
761
- }
762
-
763
- // Remove old attributes that are no longer present
764
- for (let i = oldAttrs.length - 1; i >= 0; i--) {
765
- const name = oldAttrs[i].name;
766
- if (!newEl.hasAttribute(name)) {
767
- oldEl.removeAttribute(name);
768
- }
677
+ */ _updateAttributes(oldEl, newEl) {
678
+ // Add/update attributes from new element
679
+ for (const attr of newEl.attributes){
680
+ // Skip event attributes (handled by Eleva's event system)
681
+ if (attr.name[0] === "@") continue;
682
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
683
+ oldEl.setAttribute(attr.name, attr.value);
684
+ }
685
+ // Sync property if it exists and is writable (handles value, checked, selected, disabled, etc.)
686
+ if (attr.name in oldEl) {
687
+ try {
688
+ const newProp = typeof oldEl[attr.name] === "boolean" ? attr.value !== "false" // Attribute presence = true, unless explicitly "false"
689
+ : attr.value;
690
+ if (oldEl[attr.name] !== newProp) oldEl[attr.name] = newProp;
691
+ } catch {
692
+ continue; // Property is readonly
693
+ }
694
+ }
695
+ }
696
+ // Remove attributes no longer present
697
+ for(let i = oldEl.attributes.length - 1; i >= 0; i--){
698
+ const name = oldEl.attributes[i].name;
699
+ if (!newEl.hasAttribute(name)) {
700
+ oldEl.removeAttribute(name);
701
+ }
702
+ }
703
+ // Sync properties that can diverge from attributes via user interaction
704
+ for (const prop of SYNC_PROPS){
705
+ if (prop in newEl && oldEl[prop] !== newEl[prop]) oldEl[prop] = newEl[prop];
706
+ }
769
707
  }
770
- }
771
-
772
- /**
773
- * Determines if two nodes are the same based on their type, name, and key attributes.
708
+ /**
709
+ * Determines if two nodes are the same for reconciliation purposes.
710
+ * Two nodes are considered the same if:
711
+ * - Both have keys: keys match AND tag names match
712
+ * - Neither has keys: node types match AND node names match
713
+ * - One has key, other doesn't: not the same
714
+ *
715
+ * This ensures keyed elements are only reused when both key and tag match,
716
+ * preventing bugs like `<div key="a">` incorrectly matching `<span key="a">`.
774
717
  *
775
718
  * @private
776
719
  * @param {Node} oldNode - The first node to compare.
777
720
  * @param {Node} newNode - The second node to compare.
778
- * @returns {boolean} True if the nodes are considered the same, false otherwise.
779
- */
780
- _isSameNode(oldNode, newNode) {
781
- if (!oldNode || !newNode) return false;
782
- const oldKey = oldNode.nodeType === Node.ELEMENT_NODE ? oldNode.getAttribute("key") : null;
783
- const newKey = newNode.nodeType === Node.ELEMENT_NODE ? newNode.getAttribute("key") : null;
784
- if (oldKey && newKey) return oldKey === newKey;
785
- return !oldKey && !newKey && oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName;
786
- }
787
-
788
- /**
789
- * Creates a key map for the children of a parent node.
790
- * Used for efficient O(1) lookup of keyed elements during diffing.
721
+ * @returns {boolean} True if the nodes are considered the same for reconciliation.
722
+ */ _isSameNode(oldNode, newNode) {
723
+ if (!oldNode || !newNode) return false;
724
+ const oldKey = this._getNodeKey(oldNode);
725
+ const newKey = this._getNodeKey(newNode);
726
+ // If both have keys, compare by key AND tag name
727
+ if (oldKey && newKey) {
728
+ return oldKey === newKey && oldNode.nodeName === newNode.nodeName;
729
+ }
730
+ // Otherwise, compare by type and name
731
+ return !oldKey && !newKey && oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName;
732
+ }
733
+ /**
734
+ * Extracts the key attribute from a node if it exists.
735
+ * Only element nodes (nodeType === 1) can have key attributes.
791
736
  *
792
737
  * @private
793
- * @param {Array<ChildNode>} children - The children of the parent node.
794
- * @param {number} start - The start index of the children.
795
- * @param {number} end - The end index of the children.
796
- * @returns {KeyMap} A key map for the children.
797
- */
798
- _createKeyMap(children, start, end) {
799
- const map = new Map();
800
- for (let i = start; i <= end; i++) {
801
- const child = children[i];
802
- const key = this._getNodeKey(child);
803
- if (key) map.set(key, child);
738
+ * @param {Node|null|undefined} node - The node to extract the key from.
739
+ * @returns {string|null} The key attribute value, or null if not an element or no key.
740
+ */ _getNodeKey(node) {
741
+ return node?.nodeType === 1 ? node.getAttribute("key") : null;
804
742
  }
805
- return map;
806
- }
807
-
808
- /**
809
- * Extracts the key attribute from a node if it exists.
743
+ /**
744
+ * Creates a key map for efficient O(1) lookup of keyed elements during diffing.
745
+ * The map is built lazily only when needed (when a mismatch occurs during diffing).
810
746
  *
811
747
  * @private
812
- * @param {Node} node - The node to extract the key from.
813
- * @returns {string|null} The key attribute value or null if not found.
814
- */
815
- _getNodeKey(node) {
816
- return node?.nodeType === Node.ELEMENT_NODE ? node.getAttribute("key") : null;
817
- }
748
+ * @param {Array<ChildNode>} children - The array of child nodes to map.
749
+ * @param {number} start - The start index (inclusive) for mapping.
750
+ * @param {number} end - The end index (inclusive) for mapping.
751
+ * @returns {KeyMap} A Map of key strings to their corresponding DOM nodes.
752
+ */ _createKeyMap(children, start, end) {
753
+ const map = new Map();
754
+ for(let i = start; i <= end; i++){
755
+ const key = this._getNodeKey(children[i]);
756
+ if (key) map.set(key, children[i]);
757
+ }
758
+ return map;
759
+ }
760
+ /**
761
+ * Creates a new Renderer instance.
762
+ *
763
+ * @public
764
+ *
765
+ * @example
766
+ * const renderer = new Renderer();
767
+ */ constructor(){
768
+ /**
769
+ * Temporary container for parsing new HTML content.
770
+ * Reused across patch operations to minimize memory allocation.
771
+ * @private
772
+ * @type {HTMLDivElement}
773
+ */ this._tempContainer = document.createElement("div");
774
+ }
818
775
  }
819
776
 
820
777
  // ============================================================================
821
778
  // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
822
779
  // ============================================================================
823
-
824
780
  // -----------------------------------------------------------------------------
825
781
  // Configuration Types
826
782
  // -----------------------------------------------------------------------------
827
-
828
783
  /**
829
784
  * @typedef {Object} ElevaConfig
830
785
  * @property {boolean} [debug=false]
@@ -833,12 +788,9 @@
833
788
  * Prefix for component style scoping
834
789
  * @property {boolean} [async=true]
835
790
  * Enable async component setup
836
- */
837
-
838
- // -----------------------------------------------------------------------------
791
+ */ // -----------------------------------------------------------------------------
839
792
  // Component Types
840
793
  // -----------------------------------------------------------------------------
841
-
842
794
  /**
843
795
  * @typedef {Object} ComponentDefinition
844
796
  * @property {SetupFunction} [setup]
@@ -849,40 +801,27 @@
849
801
  * Optional function or string that provides component-scoped CSS styles
850
802
  * @property {ChildrenMap} [children]
851
803
  * Optional object defining nested child components
852
- */
853
-
854
- /**
804
+ */ /**
855
805
  * @callback SetupFunction
856
806
  * @param {ComponentContext} ctx - The component context with props, emitter, and signal factory
857
807
  * @returns {SetupResult|Promise<SetupResult>} Reactive data and lifecycle hooks
858
- */
859
-
860
- /**
808
+ */ /**
861
809
  * @typedef {Record<string, unknown> & LifecycleHooks} SetupResult
862
810
  * Data returned from setup function, may include lifecycle hooks
863
- */
864
-
865
- /**
811
+ */ /**
866
812
  * @callback TemplateFunction
867
813
  * @param {ComponentContext} ctx - The component context
868
814
  * @returns {string|Promise<string>} HTML template string
869
- */
870
-
871
- /**
815
+ */ /**
872
816
  * @callback StyleFunction
873
817
  * @param {ComponentContext} ctx - The component context
874
818
  * @returns {string} CSS styles string
875
- */
876
-
877
- /**
819
+ */ /**
878
820
  * @typedef {Record<string, ComponentDefinition|string>} ChildrenMap
879
821
  * Map of CSS selectors to component definitions or registered component names
880
- */
881
-
882
- // -----------------------------------------------------------------------------
822
+ */ // -----------------------------------------------------------------------------
883
823
  // Context Types
884
824
  // -----------------------------------------------------------------------------
885
-
886
825
  /**
887
826
  * @typedef {Object} ComponentContext
888
827
  * @property {ComponentProps} props
@@ -891,24 +830,17 @@
891
830
  * Event emitter instance for component event handling
892
831
  * @property {SignalFactory} signal
893
832
  * Factory function to create reactive Signal instances
894
- */
895
-
896
- /**
833
+ */ /**
897
834
  * @typedef {Record<string, unknown>} ComponentProps
898
835
  * Properties passed to a component during mounting
899
- */
900
-
901
- /**
836
+ */ /**
902
837
  * @callback SignalFactory
903
838
  * @template T
904
839
  * @param {T} initialValue - The initial value for the signal
905
840
  * @returns {Signal<T>} A new Signal instance
906
- */
907
-
908
- // -----------------------------------------------------------------------------
841
+ */ // -----------------------------------------------------------------------------
909
842
  // Lifecycle Hook Types
910
843
  // -----------------------------------------------------------------------------
911
-
912
844
  /**
913
845
  * @typedef {Object} LifecycleHooks
914
846
  * @property {LifecycleHook} [onBeforeMount]
@@ -921,29 +853,21 @@
921
853
  * Hook called after component update
922
854
  * @property {UnmountHook} [onUnmount]
923
855
  * Hook called during component unmounting
924
- */
925
-
926
- /**
856
+ */ /**
927
857
  * @callback LifecycleHook
928
858
  * @param {LifecycleHookContext} ctx - Context with container and component data
929
859
  * @returns {void|Promise<void>}
930
- */
931
-
932
- /**
860
+ */ /**
933
861
  * @callback UnmountHook
934
862
  * @param {UnmountHookContext} ctx - Context with cleanup resources
935
863
  * @returns {void|Promise<void>}
936
- */
937
-
938
- /**
864
+ */ /**
939
865
  * @typedef {Object} LifecycleHookContext
940
866
  * @property {HTMLElement} container
941
867
  * The DOM element where the component is mounted
942
868
  * @property {ComponentContext & SetupResult} context
943
869
  * The component's reactive state and context data
944
- */
945
-
946
- /**
870
+ */ /**
947
871
  * @typedef {Object} UnmountHookContext
948
872
  * @property {HTMLElement} container
949
873
  * The DOM element where the component is mounted
@@ -951,9 +875,7 @@
951
875
  * The component's reactive state and context data
952
876
  * @property {CleanupResources} cleanup
953
877
  * Object containing cleanup functions and instances
954
- */
955
-
956
- /**
878
+ */ /**
957
879
  * @typedef {Object} CleanupResources
958
880
  * @property {Array<UnsubscribeFunction>} watchers
959
881
  * Signal watcher cleanup functions
@@ -961,12 +883,9 @@
961
883
  * Event listener cleanup functions
962
884
  * @property {Array<MountResult>} children
963
885
  * Child component instances
964
- */
965
-
966
- // -----------------------------------------------------------------------------
886
+ */ // -----------------------------------------------------------------------------
967
887
  // Mount Result Types
968
888
  // -----------------------------------------------------------------------------
969
-
970
889
  /**
971
890
  * @typedef {Object} MountResult
972
891
  * @property {HTMLElement} container
@@ -975,22 +894,15 @@
975
894
  * The component's reactive state and context data
976
895
  * @property {UnmountFunction} unmount
977
896
  * Function to clean up and unmount the component
978
- */
979
-
980
- /**
897
+ */ /**
981
898
  * @callback UnmountFunction
982
899
  * @returns {Promise<void>}
983
- */
984
-
985
- /**
900
+ */ /**
986
901
  * @callback UnsubscribeFunction
987
902
  * @returns {void|boolean}
988
- */
989
-
990
- // -----------------------------------------------------------------------------
903
+ */ // -----------------------------------------------------------------------------
991
904
  // Plugin Types
992
905
  // -----------------------------------------------------------------------------
993
-
994
906
  /**
995
907
  * @typedef {Object} ElevaPlugin
996
908
  * @property {PluginInstallFunction} install
@@ -999,42 +911,29 @@
999
911
  * Unique identifier name for the plugin
1000
912
  * @property {PluginUninstallFunction} [uninstall]
1001
913
  * Optional function to uninstall the plugin
1002
- */
1003
-
1004
- /**
914
+ */ /**
1005
915
  * @callback PluginInstallFunction
1006
916
  * @param {Eleva} eleva - The Eleva instance
1007
917
  * @param {PluginOptions} options - Plugin configuration options
1008
918
  * @returns {void|Eleva|unknown} Optionally returns the Eleva instance or plugin result
1009
- */
1010
-
1011
- /**
919
+ */ /**
1012
920
  * @callback PluginUninstallFunction
1013
921
  * @param {Eleva} eleva - The Eleva instance
1014
922
  * @returns {void}
1015
- */
1016
-
1017
- /**
923
+ */ /**
1018
924
  * @typedef {Record<string, unknown>} PluginOptions
1019
925
  * Configuration options passed to a plugin during installation
1020
- */
1021
-
1022
- // -----------------------------------------------------------------------------
926
+ */ // -----------------------------------------------------------------------------
1023
927
  // Event Types
1024
928
  // -----------------------------------------------------------------------------
1025
-
1026
929
  /**
1027
930
  * @callback EventHandler
1028
931
  * @param {Event} event - The DOM event object
1029
932
  * @returns {void}
1030
- */
1031
-
1032
- /**
933
+ */ /**
1033
934
  * @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
1034
935
  * Common DOM event names (prefixed with @ in templates)
1035
- */
1036
-
1037
- /**
936
+ */ /**
1038
937
  * @class 🧩 Eleva
1039
938
  * @classdesc A modern, signal-based component runtime framework that provides lifecycle hooks,
1040
939
  * scoped styles, and plugin support. Eleva manages component registration, plugin integration,
@@ -1061,50 +960,8 @@
1061
960
  * },
1062
961
  * template: `<div>Lifecycle Demo</div>`
1063
962
  * });
1064
- */
1065
- class Eleva {
1066
- /**
1067
- * Creates a new Eleva instance with the specified name and configuration.
1068
- *
1069
- * @public
1070
- * @param {string} name - The unique identifier name for this Eleva instance.
1071
- * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
1072
- * May include framework-wide settings and default behaviors.
1073
- * @throws {Error} If the name is not provided or is not a string.
1074
- * @returns {Eleva} A new Eleva instance.
1075
- *
1076
- * @example
1077
- * const app = new Eleva("myApp");
1078
- * app.component("myComponent", {
1079
- * setup: (ctx) => ({ count: ctx.signal(0) }),
1080
- * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
1081
- * });
1082
- * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
1083
- *
1084
- */
1085
- constructor(name, config = {}) {
1086
- /** @public {string} The unique identifier name for this Eleva instance */
1087
- this.name = name;
1088
- /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */
1089
- this.config = config;
1090
- /** @public {Emitter} Instance of the event emitter for handling component events */
1091
- this.emitter = new Emitter();
1092
- /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */
1093
- this.signal = Signal;
1094
- /** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */
1095
- this.templateEngine = TemplateEngine;
1096
- /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */
1097
- this.renderer = new Renderer();
1098
-
1099
- /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */
1100
- this._components = new Map();
1101
- /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */
1102
- this._plugins = new Map();
1103
- /** @private {number} Counter for generating unique component IDs */
1104
- this._componentCounter = 0;
1105
- }
1106
-
1107
- /**
963
+ */ class Eleva {
964
+ /**
1108
965
  * Integrates a plugin with the Eleva framework.
1109
966
  * The plugin's install function will be called with the Eleva instance and provided options.
1110
967
  * After installation, the plugin will be available for use by components.
@@ -1116,6 +973,7 @@
1116
973
  * @param {ElevaPlugin} plugin - The plugin object which must have an `install` function.
1117
974
  * @param {Object<string, unknown>} [options={}] - Optional configuration options for the plugin.
1118
975
  * @returns {Eleva} The Eleva instance (for method chaining).
976
+ * @throws {Error} If plugin does not have an install function.
1119
977
  * @example
1120
978
  * app.use(myPlugin, { option1: "value1" });
1121
979
  *
@@ -1126,14 +984,15 @@
1126
984
  * // Uninstall in reverse order:
1127
985
  * PluginB.uninstall(app);
1128
986
  * PluginA.uninstall(app);
1129
- */
1130
- use(plugin, options = {}) {
1131
- this._plugins.set(plugin.name, plugin);
1132
- const result = plugin.install(this, options);
1133
- return result !== undefined ? result : this;
1134
- }
1135
-
1136
- /**
987
+ */ use(plugin, options = {}) {
988
+ if (!plugin?.install || typeof plugin.install !== "function") {
989
+ throw new Error("Eleva: plugin must have an install function");
990
+ }
991
+ this._plugins.set(plugin.name, plugin);
992
+ const result = plugin.install(this, options);
993
+ return result !== undefined ? result : this;
994
+ }
995
+ /**
1137
996
  * Registers a new component with the Eleva instance.
1138
997
  * The component will be available for mounting using its registered name.
1139
998
  *
@@ -1141,20 +1000,23 @@
1141
1000
  * @param {string} name - The unique name of the component to register.
1142
1001
  * @param {ComponentDefinition} definition - The component definition including setup, template, style, and children.
1143
1002
  * @returns {Eleva} The Eleva instance (for method chaining).
1144
- * @throws {Error} If the component name is already registered.
1003
+ * @throws {Error} If name is not a non-empty string or definition has no template.
1145
1004
  * @example
1146
1005
  * app.component("myButton", {
1147
1006
  * template: (ctx) => `<button>${ctx.props.text}</button>`,
1148
1007
  * style: `button { color: blue; }`
1149
1008
  * });
1150
- */
1151
- component(name, definition) {
1152
- /** @type {Map<string, ComponentDefinition>} */
1153
- this._components.set(name, definition);
1154
- return this;
1155
- }
1156
-
1157
- /**
1009
+ */ component(name, definition) {
1010
+ if (!name || typeof name !== "string") {
1011
+ throw new Error("Eleva: component name must be a non-empty string");
1012
+ }
1013
+ if (!definition?.template) {
1014
+ throw new Error(`Eleva: component "${name}" must have a template`);
1015
+ }
1016
+ /** @type {Map<string, ComponentDefinition>} */ this._components.set(name, definition);
1017
+ return this;
1018
+ }
1019
+ /**
1158
1020
  * Mounts a registered component to a DOM element.
1159
1021
  * This will initialize the component, set up its reactive state, and render it to the DOM.
1160
1022
  *
@@ -1167,46 +1029,32 @@
1167
1029
  * - container: The mounted component's container element
1168
1030
  * - data: The component's reactive state and context
1169
1031
  * - unmount: Function to clean up and unmount the component
1170
- * @throws {Error} If the container is not found, or component is not registered.
1032
+ * @throws {Error} If container is not a DOM element or component is not registered.
1171
1033
  * @example
1172
1034
  * const instance = await app.mount(document.getElementById("app"), "myComponent", { text: "Click me" });
1173
1035
  * // Later...
1174
1036
  * instance.unmount();
1175
- */
1176
- async mount(container, compName, props = {}) {
1177
- if (!container) throw new Error(`Container not found: ${container}`);
1178
- if (container._eleva_instance) return container._eleva_instance;
1179
-
1180
- /** @type {ComponentDefinition} */
1181
- const definition = typeof compName === "string" ? this._components.get(compName) : compName;
1182
- if (!definition) throw new Error(`Component "${compName}" not registered.`);
1183
-
1184
- /** @type {string} */
1185
- const compId = `c${++this._componentCounter}`;
1186
-
1187
- /**
1037
+ */ async mount(container, compName, props = {}) {
1038
+ if (!container?.nodeType) {
1039
+ throw new Error("Eleva: container must be a DOM element");
1040
+ }
1041
+ if (container._eleva_instance) return container._eleva_instance;
1042
+ /** @type {ComponentDefinition} */ const definition = typeof compName === "string" ? this._components.get(compName) : compName;
1043
+ if (!definition) throw new Error(`Component "${compName}" not registered.`);
1044
+ /** @type {string} */ const compId = `c${++this._componentCounter}`;
1045
+ /**
1188
1046
  * Destructure the component definition to access core functionality.
1189
1047
  * - setup: Optional function for component initialization and state management
1190
1048
  * - template: Required function or string that returns the component's HTML structure
1191
1049
  * - style: Optional function or string for component-scoped CSS styles
1192
1050
  * - children: Optional object defining nested child components
1193
- */
1194
- const {
1195
- setup,
1196
- template,
1197
- style,
1198
- children
1199
- } = definition;
1200
-
1201
- /** @type {ComponentContext} */
1202
- const context = {
1203
- props,
1204
- emitter: this.emitter,
1205
- /** @type {(v: unknown) => Signal<unknown>} */
1206
- signal: v => new this.signal(v)
1207
- };
1208
-
1209
- /**
1051
+ */ const { setup, template, style, children } = definition;
1052
+ /** @type {ComponentContext} */ const context = {
1053
+ props,
1054
+ emitter: this.emitter,
1055
+ /** @type {(v: unknown) => Signal<unknown>} */ signal: (v)=>new this.signal(v)
1056
+ };
1057
+ /**
1210
1058
  * Processes the mounting of the component.
1211
1059
  * This function handles:
1212
1060
  * 1. Merging setup data with the component context
@@ -1219,132 +1067,114 @@
1219
1067
  * - container: The mounted component's container element
1220
1068
  * - data: The component's reactive state and context
1221
1069
  * - unmount: Function to clean up and unmount the component
1222
- */
1223
- const processMount = async data => {
1224
- /** @type {ComponentContext} */
1225
- const mergedContext = {
1226
- ...context,
1227
- ...data
1228
- };
1229
- /** @type {Array<() => void>} */
1230
- const watchers = [];
1231
- /** @type {Array<MountResult>} */
1232
- const childInstances = [];
1233
- /** @type {Array<() => void>} */
1234
- const listeners = [];
1235
- /** @private {boolean} Local mounted state for this component instance */
1236
- let isMounted = false;
1237
-
1238
- // ========================================================================
1239
- // Render Batching
1240
- // ========================================================================
1241
-
1242
- /** @private {boolean} Flag to prevent multiple queued renders */
1243
- let renderScheduled = false;
1244
-
1245
- /**
1246
- * Schedules a batched render on the next microtask.
1247
- * Multiple signal changes within the same synchronous block are collapsed into one render.
1070
+ */ const processMount = async (data)=>{
1071
+ /** @type {ComponentContext} */ const mergedContext = {
1072
+ ...context,
1073
+ ...data
1074
+ };
1075
+ /** @type {Array<() => void>} */ const watchers = [];
1076
+ /** @type {Array<MountResult>} */ const childInstances = [];
1077
+ /** @type {Array<() => void>} */ const listeners = [];
1078
+ /** @private {boolean} Local mounted state for this component instance */ let isMounted = false;
1079
+ // ========================================================================
1080
+ // Render Batching
1081
+ // ========================================================================
1082
+ /** @private {boolean} Flag to prevent concurrent renders */ let renderScheduled = false;
1083
+ /**
1084
+ * Schedules a render using microtask batching.
1085
+ * Since signals now notify watchers synchronously, multiple signal
1086
+ * changes in the same synchronous block will each call this function,
1087
+ * but only one render will be scheduled via queueMicrotask.
1088
+ * This separates concerns: signals handle state, components handle scheduling.
1248
1089
  * @private
1249
- */
1250
- const scheduleRender = () => {
1251
- if (renderScheduled) return;
1252
- renderScheduled = true;
1253
- queueMicrotask(async () => {
1254
- renderScheduled = false;
1255
- await render();
1256
- });
1257
- };
1258
-
1259
- /**
1090
+ */ const scheduleRender = ()=>{
1091
+ if (renderScheduled) return;
1092
+ renderScheduled = true;
1093
+ queueMicrotask(async ()=>{
1094
+ renderScheduled = false;
1095
+ await render();
1096
+ });
1097
+ };
1098
+ /**
1260
1099
  * Renders the component by:
1261
1100
  * 1. Executing lifecycle hooks
1262
1101
  * 2. Processing the template
1263
1102
  * 3. Updating the DOM
1264
1103
  * 4. Processing events, injecting styles, and mounting child components.
1265
- */
1266
- const render = async () => {
1267
- const templateResult = typeof template === "function" ? await template(mergedContext) : template;
1268
- const html = this.templateEngine.parse(templateResult, mergedContext);
1269
-
1270
- // Execute before hooks
1271
- if (!isMounted) {
1272
- await mergedContext.onBeforeMount?.({
1273
- container,
1274
- context: mergedContext
1275
- });
1276
- } else {
1277
- await mergedContext.onBeforeUpdate?.({
1278
- container,
1279
- context: mergedContext
1280
- });
1281
- }
1282
- this.renderer.patchDOM(container, html);
1283
- this._processEvents(container, mergedContext, listeners);
1284
- if (style) this._injectStyles(container, compId, style, mergedContext);
1285
- if (children) await this._mountComponents(container, children, childInstances);
1286
-
1287
- // Execute after hooks
1288
- if (!isMounted) {
1289
- await mergedContext.onMount?.({
1290
- container,
1291
- context: mergedContext
1292
- });
1293
- isMounted = true;
1294
- } else {
1295
- await mergedContext.onUpdate?.({
1296
- container,
1297
- context: mergedContext
1298
- });
1299
- }
1300
- };
1301
-
1302
- /**
1104
+ */ const render = async ()=>{
1105
+ const templateResult = typeof template === "function" ? await template(mergedContext) : template;
1106
+ const html = this.templateEngine.parse(templateResult, mergedContext);
1107
+ // Execute before hooks
1108
+ if (!isMounted) {
1109
+ await mergedContext.onBeforeMount?.({
1110
+ container,
1111
+ context: mergedContext
1112
+ });
1113
+ } else {
1114
+ await mergedContext.onBeforeUpdate?.({
1115
+ container,
1116
+ context: mergedContext
1117
+ });
1118
+ }
1119
+ this.renderer.patchDOM(container, html);
1120
+ this._processEvents(container, mergedContext, listeners);
1121
+ if (style) this._injectStyles(container, compId, style, mergedContext);
1122
+ if (children) await this._mountComponents(container, children, childInstances);
1123
+ // Execute after hooks
1124
+ if (!isMounted) {
1125
+ await mergedContext.onMount?.({
1126
+ container,
1127
+ context: mergedContext
1128
+ });
1129
+ isMounted = true;
1130
+ } else {
1131
+ await mergedContext.onUpdate?.({
1132
+ container,
1133
+ context: mergedContext
1134
+ });
1135
+ }
1136
+ };
1137
+ /**
1303
1138
  * Sets up reactive watchers for all Signal instances in the component's data.
1304
1139
  * When a Signal's value changes, a batched render is scheduled.
1305
1140
  * Multiple changes within the same frame are collapsed into one render.
1306
1141
  * Stores unsubscribe functions to clean up watchers when component unmounts.
1307
- */
1308
- for (const val of Object.values(data)) {
1309
- if (val instanceof Signal) watchers.push(val.watch(scheduleRender));
1310
- }
1311
- await render();
1312
- const instance = {
1313
- container,
1314
- data: mergedContext,
1315
- /**
1142
+ */ for (const val of Object.values(data)){
1143
+ if (val instanceof Signal) watchers.push(val.watch(scheduleRender));
1144
+ }
1145
+ await render();
1146
+ const instance = {
1147
+ container,
1148
+ data: mergedContext,
1149
+ /**
1316
1150
  * Unmounts the component, cleaning up watchers and listeners, child components, and clearing the container.
1317
1151
  *
1318
1152
  * @returns {void}
1319
- */
1320
- unmount: async () => {
1321
- /** @type {UnmountHookContext} */
1322
- await mergedContext.onUnmount?.({
1323
- container,
1324
- context: mergedContext,
1325
- cleanup: {
1326
- watchers: watchers,
1327
- listeners: listeners,
1328
- children: childInstances
1329
- }
1330
- });
1331
- for (const fn of watchers) fn();
1332
- for (const fn of listeners) fn();
1333
- for (const child of childInstances) await child.unmount();
1334
- container.innerHTML = "";
1335
- delete container._eleva_instance;
1336
- }
1337
- };
1338
- container._eleva_instance = instance;
1339
- return instance;
1340
- };
1341
-
1342
- // Handle asynchronous setup.
1343
- const setupResult = typeof setup === "function" ? await setup(context) : {};
1344
- return await processMount(setupResult);
1345
- }
1346
-
1347
- /**
1153
+ */ unmount: async ()=>{
1154
+ /** @type {UnmountHookContext} */ await mergedContext.onUnmount?.({
1155
+ container,
1156
+ context: mergedContext,
1157
+ cleanup: {
1158
+ watchers: watchers,
1159
+ listeners: listeners,
1160
+ children: childInstances
1161
+ }
1162
+ });
1163
+ for (const fn of watchers)fn();
1164
+ for (const fn of listeners)fn();
1165
+ for (const child of childInstances)await child.unmount();
1166
+ container.innerHTML = "";
1167
+ delete container._eleva_instance;
1168
+ }
1169
+ };
1170
+ container._eleva_instance = instance;
1171
+ return instance;
1172
+ };
1173
+ // Handle asynchronous setup.
1174
+ const setupResult = typeof setup === "function" ? await setup(context) : {};
1175
+ return await processMount(setupResult);
1176
+ }
1177
+ /**
1348
1178
  * Processes DOM elements for event binding based on attributes starting with "@".
1349
1179
  * This method handles the event delegation system and ensures proper cleanup of event listeners.
1350
1180
  *
@@ -1353,34 +1183,25 @@
1353
1183
  * @param {ComponentContext} context - The current component context containing event handler definitions.
1354
1184
  * @param {Array<() => void>} listeners - Array to collect cleanup functions for each event listener.
1355
1185
  * @returns {void}
1356
- */
1357
- _processEvents(container, context, listeners) {
1358
- /** @type {NodeListOf<Element>} */
1359
- const elements = container.querySelectorAll("*");
1360
- for (const el of elements) {
1361
- /** @type {NamedNodeMap} */
1362
- const attrs = el.attributes;
1363
- for (let i = 0; i < attrs.length; i++) {
1364
- /** @type {Attr} */
1365
- const attr = attrs[i];
1366
- if (!attr.name.startsWith("@")) continue;
1367
-
1368
- /** @type {keyof HTMLElementEventMap} */
1369
- const event = attr.name.slice(1);
1370
- /** @type {string} */
1371
- const handlerName = attr.value;
1372
- /** @type {(event: Event) => void} */
1373
- const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
1374
- if (typeof handler === "function") {
1375
- el.addEventListener(event, handler);
1376
- el.removeAttribute(attr.name);
1377
- listeners.push(() => el.removeEventListener(event, handler));
1186
+ */ _processEvents(container, context, listeners) {
1187
+ /** @type {NodeListOf<Element>} */ const elements = container.querySelectorAll("*");
1188
+ for (const el of elements){
1189
+ /** @type {NamedNodeMap} */ const attrs = el.attributes;
1190
+ for(let i = 0; i < attrs.length; i++){
1191
+ /** @type {Attr} */ const attr = attrs[i];
1192
+ if (!attr.name.startsWith("@")) continue;
1193
+ /** @type {keyof HTMLElementEventMap} */ const event = attr.name.slice(1);
1194
+ /** @type {string} */ const handlerName = attr.value;
1195
+ /** @type {(event: Event) => void} */ const handler = context[handlerName] || this.templateEngine.evaluate(handlerName, context);
1196
+ if (typeof handler === "function") {
1197
+ el.addEventListener(event, handler);
1198
+ el.removeAttribute(attr.name);
1199
+ listeners.push(()=>el.removeEventListener(event, handler));
1200
+ }
1201
+ }
1378
1202
  }
1379
- }
1380
1203
  }
1381
- }
1382
-
1383
- /**
1204
+ /**
1384
1205
  * Injects scoped styles into the component's container.
1385
1206
  * The styles are automatically prefixed to prevent style leakage to other components.
1386
1207
  *
@@ -1390,23 +1211,18 @@
1390
1211
  * @param {(function(ComponentContext): string)|string} styleDef - The component's style definition (function or string).
1391
1212
  * @param {ComponentContext} context - The current component context for style interpolation.
1392
1213
  * @returns {void}
1393
- */
1394
- _injectStyles(container, compId, styleDef, context) {
1395
- /** @type {string} */
1396
- const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
1397
-
1398
- /** @type {HTMLStyleElement|null} */
1399
- let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
1400
- if (styleEl && styleEl.textContent === newStyle) return;
1401
- if (!styleEl) {
1402
- styleEl = document.createElement("style");
1403
- styleEl.setAttribute("data-e-style", compId);
1404
- container.appendChild(styleEl);
1214
+ */ _injectStyles(container, compId, styleDef, context) {
1215
+ /** @type {string} */ const newStyle = typeof styleDef === "function" ? this.templateEngine.parse(styleDef(context), context) : styleDef;
1216
+ /** @type {HTMLStyleElement|null} */ let styleEl = container.querySelector(`style[data-e-style="${compId}"]`);
1217
+ if (styleEl && styleEl.textContent === newStyle) return;
1218
+ if (!styleEl) {
1219
+ styleEl = document.createElement("style");
1220
+ styleEl.setAttribute("data-e-style", compId);
1221
+ container.appendChild(styleEl);
1222
+ }
1223
+ styleEl.textContent = newStyle;
1405
1224
  }
1406
- styleEl.textContent = newStyle;
1407
- }
1408
-
1409
- /**
1225
+ /**
1410
1226
  * Extracts props from an element's attributes that start with the specified prefix.
1411
1227
  * This method is used to collect component properties from DOM elements.
1412
1228
  *
@@ -1417,23 +1233,21 @@
1417
1233
  * // For an element with attributes:
1418
1234
  * // <div :name="John" :age="25">
1419
1235
  * // Returns: { name: "John", age: "25" }
1420
- */
1421
- _extractProps(element) {
1422
- if (!element.attributes) return {};
1423
- const props = {};
1424
- const attrs = element.attributes;
1425
- for (let i = attrs.length - 1; i >= 0; i--) {
1426
- const attr = attrs[i];
1427
- if (attr.name.startsWith(":")) {
1428
- const propName = attr.name.slice(1);
1429
- props[propName] = attr.value;
1430
- element.removeAttribute(attr.name);
1431
- }
1236
+ */ _extractProps(element) {
1237
+ if (!element.attributes) return {};
1238
+ const props = {};
1239
+ const attrs = element.attributes;
1240
+ for(let i = attrs.length - 1; i >= 0; i--){
1241
+ const attr = attrs[i];
1242
+ if (attr.name.startsWith(":")) {
1243
+ const propName = attr.name.slice(1);
1244
+ props[propName] = attr.value;
1245
+ element.removeAttribute(attr.name);
1246
+ }
1247
+ }
1248
+ return props;
1432
1249
  }
1433
- return props;
1434
- }
1435
-
1436
- /**
1250
+ /**
1437
1251
  * Mounts all components within the parent component's container.
1438
1252
  * This method handles mounting of explicitly defined children components.
1439
1253
  *
@@ -1453,22 +1267,51 @@
1453
1267
  * 'UserProfile': UserProfileComponent,
1454
1268
  * '#settings-panel': "settings-panel"
1455
1269
  * };
1456
- */
1457
- async _mountComponents(container, children, childInstances) {
1458
- for (const [selector, component] of Object.entries(children)) {
1459
- if (!selector) continue;
1460
- for (const el of container.querySelectorAll(selector)) {
1461
- if (!(el instanceof HTMLElement)) continue;
1462
- /** @type {Record<string, string>} */
1463
- const props = this._extractProps(el);
1464
- /** @type {MountResult} */
1465
- const instance = await this.mount(el, component, props);
1466
- if (instance && !childInstances.includes(instance)) {
1467
- childInstances.push(instance);
1270
+ */ async _mountComponents(container, children, childInstances) {
1271
+ for (const [selector, component] of Object.entries(children)){
1272
+ if (!selector) continue;
1273
+ for (const el of container.querySelectorAll(selector)){
1274
+ if (!(el instanceof HTMLElement)) continue;
1275
+ /** @type {Record<string, string>} */ const props = this._extractProps(el);
1276
+ /** @type {MountResult} */ const instance = await this.mount(el, component, props);
1277
+ if (instance && !childInstances.includes(instance)) {
1278
+ childInstances.push(instance);
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Creates a new Eleva instance with the specified name and configuration.
1285
+ *
1286
+ * @public
1287
+ * @param {string} name - The unique identifier name for this Eleva instance.
1288
+ * @param {Record<string, unknown>} [config={}] - Optional configuration object for the instance.
1289
+ * May include framework-wide settings and default behaviors.
1290
+ * @throws {Error} If the name is not provided or is not a string.
1291
+ * @returns {Eleva} A new Eleva instance.
1292
+ *
1293
+ * @example
1294
+ * const app = new Eleva("myApp");
1295
+ * app.component("myComponent", {
1296
+ * setup: (ctx) => ({ count: ctx.signal(0) }),
1297
+ * template: (ctx) => `<div>Hello ${ctx.props.name}!</div>`
1298
+ * });
1299
+ * app.mount(document.getElementById("app"), "myComponent", { name: "World" });
1300
+ *
1301
+ */ constructor(name, config = {}){
1302
+ if (!name || typeof name !== "string") {
1303
+ throw new Error("Eleva: name must be a non-empty string");
1468
1304
  }
1469
- }
1305
+ /** @public {string} The unique identifier name for this Eleva instance */ this.name = name;
1306
+ /** @public {Object<string, unknown>} Optional configuration object for the Eleva instance */ this.config = config;
1307
+ /** @public {Emitter} Instance of the event emitter for handling component events */ this.emitter = new Emitter();
1308
+ /** @public {typeof Signal} Static reference to the Signal class for creating reactive state */ this.signal = Signal;
1309
+ /** @public {typeof TemplateEngine} Static reference to the TemplateEngine class for template parsing */ this.templateEngine = TemplateEngine;
1310
+ /** @public {Renderer} Instance of the renderer for handling DOM updates and patching */ this.renderer = new Renderer();
1311
+ /** @private {Map<string, ComponentDefinition>} Registry of all component definitions by name */ this._components = new Map();
1312
+ /** @private {Map<string, ElevaPlugin>} Collection of installed plugin instances by name */ this._plugins = new Map();
1313
+ /** @private {number} Counter for generating unique component IDs */ this._componentCounter = 0;
1470
1314
  }
1471
- }
1472
1315
  }
1473
1316
 
1474
1317
  return Eleva;