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
@@ -0,0 +1,3392 @@
1
+ /*! Eleva Plugins v1.0.0-rc.11 | MIT License | https://elevajs.com */
2
+ /**
3
+ * A regular expression to match hyphenated lowercase letters.
4
+ * @private
5
+ * @type {RegExp}
6
+ */
7
+ const CAMEL_RE = /-([a-z])/g;
8
+
9
+ /**
10
+ * @class 🎯 AttrPlugin
11
+ * @classdesc A plugin that provides advanced attribute handling for Eleva components.
12
+ * This plugin extends the renderer with sophisticated attribute processing including:
13
+ * - ARIA attribute handling with proper property mapping
14
+ * - Data attribute management
15
+ * - Boolean attribute processing
16
+ * - Dynamic property detection and mapping
17
+ * - Attribute cleanup and removal
18
+ *
19
+ * @example
20
+ * // Install the plugin
21
+ * const app = new Eleva("myApp");
22
+ * app.use(AttrPlugin);
23
+ *
24
+ * // Use advanced attributes in components
25
+ * app.component("myComponent", {
26
+ * template: (ctx) => `
27
+ * <button
28
+ * aria-expanded="${ctx.isExpanded.value}"
29
+ * data-user-id="${ctx.userId.value}"
30
+ * disabled="${ctx.isLoading.value}"
31
+ * class="btn ${ctx.variant.value}"
32
+ * >
33
+ * ${ctx.text.value}
34
+ * </button>
35
+ * `
36
+ * });
37
+ */
38
+ const AttrPlugin = {
39
+ /**
40
+ * Unique identifier for the plugin
41
+ * @type {string}
42
+ */
43
+ name: "attr",
44
+ /**
45
+ * Plugin version
46
+ * @type {string}
47
+ */
48
+ version: "1.0.0-rc.11",
49
+ /**
50
+ * Plugin description
51
+ * @type {string}
52
+ */
53
+ description: "Advanced attribute handling for Eleva components",
54
+ /**
55
+ * Installs the plugin into the Eleva instance
56
+ *
57
+ * @param {Object} eleva - The Eleva instance
58
+ * @param {Object} options - Plugin configuration options
59
+ * @param {boolean} [options.enableAria=true] - Enable ARIA attribute handling
60
+ * @param {boolean} [options.enableData=true] - Enable data attribute handling
61
+ * @param {boolean} [options.enableBoolean=true] - Enable boolean attribute handling
62
+ * @param {boolean} [options.enableDynamic=true] - Enable dynamic property detection
63
+ */
64
+ install(eleva, options = {}) {
65
+ const {
66
+ enableAria = true,
67
+ enableData = true,
68
+ enableBoolean = true,
69
+ enableDynamic = true
70
+ } = options;
71
+
72
+ /**
73
+ * Updates the attributes of an element to match a new element's attributes.
74
+ * This method provides sophisticated attribute processing including:
75
+ * - ARIA attribute handling with proper property mapping
76
+ * - Data attribute management
77
+ * - Boolean attribute processing
78
+ * - Dynamic property detection and mapping
79
+ * - Attribute cleanup and removal
80
+ *
81
+ * @param {HTMLElement} oldEl - The original element to update
82
+ * @param {HTMLElement} newEl - The new element to update
83
+ * @returns {void}
84
+ */
85
+ const updateAttributes = (oldEl, newEl) => {
86
+ const oldAttrs = oldEl.attributes;
87
+ const newAttrs = newEl.attributes;
88
+
89
+ // Process new attributes
90
+ for (let i = 0; i < newAttrs.length; i++) {
91
+ const {
92
+ name,
93
+ value
94
+ } = newAttrs[i];
95
+
96
+ // Skip event attributes (handled by event system)
97
+ if (name.startsWith("@")) continue;
98
+
99
+ // Skip if attribute hasn't changed
100
+ if (oldEl.getAttribute(name) === value) continue;
101
+
102
+ // Handle ARIA attributes
103
+ if (enableAria && name.startsWith("aria-")) {
104
+ const prop = "aria" + name.slice(5).replace(CAMEL_RE, (_, l) => l.toUpperCase());
105
+ oldEl[prop] = value;
106
+ oldEl.setAttribute(name, value);
107
+ }
108
+ // Handle data attributes
109
+ else if (enableData && name.startsWith("data-")) {
110
+ oldEl.dataset[name.slice(5)] = value;
111
+ oldEl.setAttribute(name, value);
112
+ }
113
+ // Handle other attributes
114
+ else {
115
+ let prop = name.replace(CAMEL_RE, (_, l) => l.toUpperCase());
116
+
117
+ // Dynamic property detection
118
+ if (enableDynamic && !(prop in oldEl) && !Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop)) {
119
+ const elementProps = Object.getOwnPropertyNames(Object.getPrototypeOf(oldEl));
120
+ const matchingProp = elementProps.find(p => p.toLowerCase() === name.toLowerCase() || p.toLowerCase().includes(name.toLowerCase()) || name.toLowerCase().includes(p.toLowerCase()));
121
+ if (matchingProp) {
122
+ prop = matchingProp;
123
+ }
124
+ }
125
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldEl), prop);
126
+ const hasProperty = prop in oldEl || descriptor;
127
+ if (hasProperty) {
128
+ // Boolean attribute handling
129
+ if (enableBoolean) {
130
+ const isBoolean = typeof oldEl[prop] === "boolean" || descriptor?.get && typeof descriptor.get.call(oldEl) === "boolean";
131
+ if (isBoolean) {
132
+ const boolValue = value !== "false" && (value === "" || value === prop || value === "true");
133
+ oldEl[prop] = boolValue;
134
+ if (boolValue) {
135
+ oldEl.setAttribute(name, "");
136
+ } else {
137
+ oldEl.removeAttribute(name);
138
+ }
139
+ } else {
140
+ oldEl[prop] = value;
141
+ oldEl.setAttribute(name, value);
142
+ }
143
+ } else {
144
+ oldEl[prop] = value;
145
+ oldEl.setAttribute(name, value);
146
+ }
147
+ } else {
148
+ oldEl.setAttribute(name, value);
149
+ }
150
+ }
151
+ }
152
+
153
+ // Remove old attributes that are no longer present
154
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
155
+ const name = oldAttrs[i].name;
156
+ if (!newEl.hasAttribute(name)) {
157
+ oldEl.removeAttribute(name);
158
+ }
159
+ }
160
+ };
161
+
162
+ // Extend the renderer with the advanced attribute handler
163
+ if (eleva.renderer) {
164
+ eleva.renderer.updateAttributes = updateAttributes;
165
+
166
+ // Store the original _patchNode method
167
+ const originalPatchNode = eleva.renderer._patchNode;
168
+ eleva.renderer._originalPatchNode = originalPatchNode;
169
+
170
+ // Override the _patchNode method to use our attribute handler
171
+ eleva.renderer._patchNode = function (oldNode, newNode) {
172
+ if (oldNode?._eleva_instance) return;
173
+ if (!this._isSameNode(oldNode, newNode)) {
174
+ oldNode.replaceWith(newNode.cloneNode(true));
175
+ return;
176
+ }
177
+ if (oldNode.nodeType === Node.ELEMENT_NODE) {
178
+ updateAttributes(oldNode, newNode);
179
+ this._diff(oldNode, newNode);
180
+ } else if (oldNode.nodeType === Node.TEXT_NODE && oldNode.nodeValue !== newNode.nodeValue) {
181
+ oldNode.nodeValue = newNode.nodeValue;
182
+ }
183
+ };
184
+ }
185
+
186
+ // Add plugin metadata to the Eleva instance
187
+ if (!eleva.plugins) {
188
+ eleva.plugins = new Map();
189
+ }
190
+ eleva.plugins.set(this.name, {
191
+ name: this.name,
192
+ version: this.version,
193
+ description: this.description,
194
+ options
195
+ });
196
+
197
+ // Add utility methods for manual attribute updates
198
+ eleva.updateElementAttributes = updateAttributes;
199
+ },
200
+ /**
201
+ * Uninstalls the plugin from the Eleva instance
202
+ *
203
+ * @param {Object} eleva - The Eleva instance
204
+ */
205
+ uninstall(eleva) {
206
+ // Restore original _patchNode method if it exists
207
+ if (eleva.renderer && eleva.renderer._originalPatchNode) {
208
+ eleva.renderer._patchNode = eleva.renderer._originalPatchNode;
209
+ delete eleva.renderer._originalPatchNode;
210
+ }
211
+
212
+ // Remove plugin metadata
213
+ if (eleva.plugins) {
214
+ eleva.plugins.delete(this.name);
215
+ }
216
+
217
+ // Remove utility methods
218
+ delete eleva.updateElementAttributes;
219
+ }
220
+ };
221
+
222
+ /**
223
+ * @typedef {import('eleva').Eleva} Eleva
224
+ * @typedef {import('eleva').Signal} Signal
225
+ * @typedef {import('eleva').ComponentDefinition} ComponentDefinition
226
+ * @typedef {import('eleva').Emitter} Emitter
227
+ * @typedef {import('eleva').MountResult} MountResult
228
+ */
229
+
230
+ // ============================================
231
+ // Core Type Definitions
232
+ // ============================================
233
+
234
+ /**
235
+ * @typedef {'hash' | 'history' | 'query'} RouterMode
236
+ * The routing mode determines how the router manages URL state.
237
+ * - `hash`: Uses URL hash (e.g., `/#/path`) - works without server config
238
+ * - `history`: Uses HTML5 History API (e.g., `/path`) - requires server config
239
+ * - `query`: Uses query parameters (e.g., `?view=/path`) - useful for embedded apps
240
+ */
241
+
242
+ /**
243
+ * @typedef {Object} RouterOptions
244
+ * @property {RouterMode} [mode='hash'] - The routing mode to use.
245
+ * @property {string} [queryParam='view'] - Query parameter name for 'query' mode.
246
+ * @property {string} [viewSelector='root'] - Selector for the view container element.
247
+ * @property {string} mount - CSS selector for the mount point element.
248
+ * @property {RouteDefinition[]} routes - Array of route definitions.
249
+ * @property {string | ComponentDefinition} [globalLayout] - Default layout for all routes.
250
+ * @property {NavigationGuard} [onBeforeEach] - Global navigation guard.
251
+ * @description Configuration options for the Router plugin.
252
+ */
253
+
254
+ /**
255
+ * @typedef {Object} NavigationTarget
256
+ * @property {string} path - The target path (can include params like '/users/:id').
257
+ * @property {Record<string, string>} [params] - Route parameters to inject into the path.
258
+ * @property {Record<string, string>} [query] - Query parameters to append.
259
+ * @property {boolean} [replace=false] - Whether to replace current history entry.
260
+ * @property {Record<string, any>} [state] - State object to pass to history.
261
+ * @description Object describing a navigation target for `router.navigate()`.
262
+ */
263
+
264
+ /**
265
+ * @typedef {Object} ScrollPosition
266
+ * @property {number} x - Horizontal scroll position.
267
+ * @property {number} y - Vertical scroll position.
268
+ * @description Represents a saved scroll position.
269
+ */
270
+
271
+ /**
272
+ * @typedef {Object} RouteSegment
273
+ * @property {'static' | 'param'} type - The segment type.
274
+ * @property {string} value - The segment value (for static) or empty string (for param).
275
+ * @property {string} [name] - The parameter name (for param segments).
276
+ * @description Internal representation of a parsed route path segment.
277
+ * @private
278
+ */
279
+
280
+ /**
281
+ * @typedef {Object} RouteMatch
282
+ * @property {RouteDefinition} route - The matched route definition.
283
+ * @property {Record<string, string>} params - The extracted route parameters.
284
+ * @description Result of matching a path against route definitions.
285
+ * @private
286
+ */
287
+
288
+ /**
289
+ * @typedef {Record<string, any>} RouteMeta
290
+ * @description Arbitrary metadata attached to routes for use in guards and components.
291
+ * Common properties include:
292
+ * - `requiresAuth: boolean` - Whether the route requires authentication
293
+ * - `title: string` - Page title for the route
294
+ * - `roles: string[]` - Required user roles
295
+ * @example
296
+ * {
297
+ * path: '/admin',
298
+ * component: AdminPage,
299
+ * meta: { requiresAuth: true, roles: ['admin'], title: 'Admin Dashboard' }
300
+ * }
301
+ */
302
+
303
+ /**
304
+ * @typedef {Object} RouterErrorHandler
305
+ * @property {(error: Error, context: string, details?: Record<string, any>) => void} handle - Throws a formatted error.
306
+ * @property {(message: string, details?: Record<string, any>) => void} warn - Logs a warning.
307
+ * @property {(message: string, error: Error, details?: Record<string, any>) => void} log - Logs an error without throwing.
308
+ * @description Interface for the router's error handling system.
309
+ */
310
+
311
+ // ============================================
312
+ // Event Callback Type Definitions
313
+ // ============================================
314
+
315
+ /**
316
+ * @callback NavigationContextCallback
317
+ * @param {NavigationContext} context - The navigation context (can be modified to block/redirect).
318
+ * @returns {void | Promise<void>}
319
+ * @description Callback for `router:beforeEach` event. Modify context to control navigation.
320
+ */
321
+
322
+ /**
323
+ * @callback ResolveContextCallback
324
+ * @param {ResolveContext} context - The resolve context (can be modified to block/redirect).
325
+ * @returns {void | Promise<void>}
326
+ * @description Callback for `router:beforeResolve` and `router:afterResolve` events.
327
+ */
328
+
329
+ /**
330
+ * @callback RenderContextCallback
331
+ * @param {RenderContext} context - The render context.
332
+ * @returns {void | Promise<void>}
333
+ * @description Callback for `router:beforeRender` and `router:afterRender` events.
334
+ */
335
+
336
+ /**
337
+ * @callback ScrollContextCallback
338
+ * @param {ScrollContext} context - The scroll context with saved position info.
339
+ * @returns {void | Promise<void>}
340
+ * @description Callback for `router:scroll` event. Use to implement scroll behavior.
341
+ */
342
+
343
+ /**
344
+ * @callback RouteChangeCallback
345
+ * @param {RouteLocation} to - The target route location.
346
+ * @param {RouteLocation | null} from - The source route location.
347
+ * @returns {void | Promise<void>}
348
+ * @description Callback for `router:afterEnter`, `router:afterLeave`, `router:afterEach` events.
349
+ */
350
+
351
+ /**
352
+ * @callback RouterErrorCallback
353
+ * @param {Error} error - The error that occurred.
354
+ * @param {RouteLocation} [to] - The target route (if available).
355
+ * @param {RouteLocation | null} [from] - The source route (if available).
356
+ * @returns {void | Promise<void>}
357
+ * @description Callback for `router:onError` event.
358
+ */
359
+
360
+ /**
361
+ * @callback RouterReadyCallback
362
+ * @param {Router} router - The router instance.
363
+ * @returns {void | Promise<void>}
364
+ * @description Callback for `router:ready` event.
365
+ */
366
+
367
+ /**
368
+ * @callback RouteAddedCallback
369
+ * @param {RouteDefinition} route - The added route definition.
370
+ * @returns {void | Promise<void>}
371
+ * @description Callback for `router:routeAdded` event.
372
+ */
373
+
374
+ /**
375
+ * @callback RouteRemovedCallback
376
+ * @param {RouteDefinition} route - The removed route definition.
377
+ * @returns {void | Promise<void>}
378
+ * @description Callback for `router:routeRemoved` event.
379
+ */
380
+
381
+ // ============================================
382
+ // Core Type Definitions (continued)
383
+ // ============================================
384
+
385
+ /**
386
+ * Simple error handler for the core router.
387
+ * Can be overridden by error handling plugins.
388
+ * Provides consistent error formatting and logging for router operations.
389
+ * @private
390
+ */
391
+ const CoreErrorHandler = {
392
+ /**
393
+ * Handles router errors with basic formatting.
394
+ * @param {Error} error - The error to handle.
395
+ * @param {string} context - The context where the error occurred.
396
+ * @param {Object} details - Additional error details.
397
+ * @throws {Error} The formatted error.
398
+ */
399
+ handle(error, context, details = {}) {
400
+ const message = `[ElevaRouter] ${context}: ${error.message}`;
401
+ const formattedError = new Error(message);
402
+
403
+ // Preserve original error details
404
+ formattedError.originalError = error;
405
+ formattedError.context = context;
406
+ formattedError.details = details;
407
+ console.error(message, {
408
+ error,
409
+ context,
410
+ details
411
+ });
412
+ throw formattedError;
413
+ },
414
+ /**
415
+ * Logs a warning without throwing an error.
416
+ * @param {string} message - The warning message.
417
+ * @param {Object} details - Additional warning details.
418
+ */
419
+ warn(message, details = {}) {
420
+ console.warn(`[ElevaRouter] ${message}`, details);
421
+ },
422
+ /**
423
+ * Logs an error without throwing.
424
+ * @param {string} message - The error message.
425
+ * @param {Error} error - The original error.
426
+ * @param {Object} details - Additional error details.
427
+ */
428
+ log(message, error, details = {}) {
429
+ console.error(`[ElevaRouter] ${message}`, {
430
+ error,
431
+ details
432
+ });
433
+ }
434
+ };
435
+
436
+ /**
437
+ * @typedef {Object} RouteLocation
438
+ * @property {string} path - The path of the route (e.g., '/users/123').
439
+ * @property {Record<string, string>} query - Query parameters as key-value pairs.
440
+ * @property {string} fullUrl - The complete URL including hash, path, and query string.
441
+ * @property {Record<string, string>} params - Dynamic route parameters (e.g., `{ id: '123' }`).
442
+ * @property {RouteMeta} meta - Metadata associated with the matched route.
443
+ * @property {string} [name] - The optional name of the matched route.
444
+ * @property {RouteDefinition} matched - The raw route definition object that was matched.
445
+ * @description Represents the current or target location in the router.
446
+ */
447
+
448
+ /**
449
+ * @typedef {boolean | string | NavigationTarget | void} NavigationGuardResult
450
+ * The return value of a navigation guard.
451
+ * - `true` or `undefined/void`: Allow navigation
452
+ * - `false`: Abort navigation
453
+ * - `string`: Redirect to path
454
+ * - `NavigationTarget`: Redirect with options
455
+ */
456
+
457
+ /**
458
+ * @callback NavigationGuard
459
+ * @param {RouteLocation} to - The target route location.
460
+ * @param {RouteLocation | null} from - The source route location (null on initial navigation).
461
+ * @returns {NavigationGuardResult | Promise<NavigationGuardResult>}
462
+ * @description A function that controls navigation flow. Runs before navigation is confirmed.
463
+ * @example
464
+ * // Simple auth guard
465
+ * const authGuard = (to, from) => {
466
+ * if (to.meta.requiresAuth && !isLoggedIn()) {
467
+ * return '/login'; // Redirect
468
+ * }
469
+ * // Allow navigation (implicit return undefined)
470
+ * };
471
+ */
472
+
473
+ /**
474
+ * @callback NavigationHook
475
+ * @param {RouteLocation} to - The target route location.
476
+ * @param {RouteLocation | null} from - The source route location.
477
+ * @returns {void | Promise<void>}
478
+ * @description A lifecycle hook for side effects. Does not affect navigation flow.
479
+ * @example
480
+ * // Analytics hook
481
+ * const analyticsHook = (to, from) => {
482
+ * analytics.trackPageView(to.path);
483
+ * };
484
+ */
485
+
486
+ /**
487
+ * @typedef {Object} RouterPlugin
488
+ * @property {string} name - Unique plugin identifier.
489
+ * @property {string} [version] - Plugin version (recommended to match router version).
490
+ * @property {(router: Router, options?: Record<string, any>) => void} install - Installation function.
491
+ * @property {(router: Router) => void | Promise<void>} [destroy] - Cleanup function called on router.destroy().
492
+ * @description Interface for router plugins. Plugins can extend router functionality.
493
+ * @example
494
+ * const AnalyticsPlugin = {
495
+ * name: 'analytics',
496
+ * version: '1.0.0',
497
+ * install(router, options) {
498
+ * router.emitter.on('router:afterEach', (to, from) => {
499
+ * analytics.track(to.path);
500
+ * });
501
+ * }
502
+ * };
503
+ */
504
+
505
+ /**
506
+ * @typedef {Object} NavigationContext
507
+ * @property {RouteLocation} to - The target route location.
508
+ * @property {RouteLocation | null} from - The source route location.
509
+ * @property {boolean} cancelled - Whether navigation has been cancelled.
510
+ * @property {string | {path: string} | null} redirectTo - Redirect target if navigation should redirect.
511
+ * @description A context object passed to navigation events that plugins can modify to control navigation flow.
512
+ */
513
+
514
+ /**
515
+ * @typedef {Object} ResolveContext
516
+ * @property {RouteLocation} to - The target route location.
517
+ * @property {RouteLocation | null} from - The source route location.
518
+ * @property {RouteDefinition} route - The matched route definition.
519
+ * @property {ComponentDefinition | null} layoutComponent - The resolved layout component (available in afterResolve).
520
+ * @property {ComponentDefinition | null} pageComponent - The resolved page component (available in afterResolve).
521
+ * @property {boolean} cancelled - Whether navigation has been cancelled.
522
+ * @property {string | {path: string} | null} redirectTo - Redirect target if navigation should redirect.
523
+ * @description A context object passed to component resolution events.
524
+ */
525
+
526
+ /**
527
+ * @typedef {Object} RenderContext
528
+ * @property {RouteLocation} to - The target route location.
529
+ * @property {RouteLocation | null} from - The source route location.
530
+ * @property {ComponentDefinition | null} layoutComponent - The layout component being rendered.
531
+ * @property {ComponentDefinition} pageComponent - The page component being rendered.
532
+ * @description A context object passed to render events.
533
+ */
534
+
535
+ /**
536
+ * @typedef {Object} ScrollContext
537
+ * @property {RouteLocation} to - The target route location.
538
+ * @property {RouteLocation | null} from - The source route location.
539
+ * @property {{x: number, y: number} | null} savedPosition - The saved scroll position (if navigating via back/forward).
540
+ * @description A context object passed to scroll events for plugins to handle scroll behavior.
541
+ */
542
+
543
+ /**
544
+ * @typedef {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} RouteComponent
545
+ * A component that can be rendered for a route.
546
+ * - `string`: Name of a registered component
547
+ * - `ComponentDefinition`: Inline component definition
548
+ * - `() => Promise<{default: ComponentDefinition}>`: Lazy-loaded component (e.g., `() => import('./Page.js')`)
549
+ */
550
+
551
+ /**
552
+ * @typedef {Object} RouteDefinition
553
+ * @property {string} path - URL path pattern. Supports:
554
+ * - Static: `'/about'`
555
+ * - Dynamic params: `'/users/:id'`
556
+ * - Wildcard: `'*'` (catch-all, must be last)
557
+ * @property {RouteComponent} component - The component to render for this route.
558
+ * @property {RouteComponent} [layout] - Optional layout component to wrap the route component.
559
+ * @property {string} [name] - Optional route name for programmatic navigation.
560
+ * @property {RouteMeta} [meta] - Optional metadata (auth flags, titles, etc.).
561
+ * @property {NavigationGuard} [beforeEnter] - Route-specific guard before entering.
562
+ * @property {NavigationHook} [afterEnter] - Hook after entering and component is mounted.
563
+ * @property {NavigationGuard} [beforeLeave] - Guard before leaving this route.
564
+ * @property {NavigationHook} [afterLeave] - Hook after leaving and component is unmounted.
565
+ * @property {RouteSegment[]} [segments] - Internal: parsed path segments (added by router).
566
+ * @description Defines a route in the application.
567
+ * @example
568
+ * // Static route
569
+ * { path: '/about', component: AboutPage }
570
+ *
571
+ * // Dynamic route with params
572
+ * { path: '/users/:id', component: UserPage, meta: { requiresAuth: true } }
573
+ *
574
+ * // Lazy-loaded route with layout
575
+ * {
576
+ * path: '/dashboard',
577
+ * component: () => import('./Dashboard.js'),
578
+ * layout: DashboardLayout,
579
+ * beforeEnter: (to, from) => isLoggedIn() || '/login'
580
+ * }
581
+ *
582
+ * // Catch-all 404 route (must be last)
583
+ * { path: '*', component: NotFoundPage }
584
+ */
585
+
586
+ /**
587
+ * @class Router
588
+ * @classdesc A powerful, reactive, and flexible Router Plugin for Eleva.
589
+ * This class manages all routing logic, including state, navigation, and rendering.
590
+ *
591
+ * ## Features
592
+ * - Multiple routing modes (hash, history, query)
593
+ * - Reactive route state via Signals
594
+ * - Navigation guards and lifecycle hooks
595
+ * - Lazy-loaded components
596
+ * - Layout system
597
+ * - Plugin architecture
598
+ * - Scroll position management
599
+ *
600
+ * ## Events Reference
601
+ * | Event | Callback Type | Can Block | Description |
602
+ * |-------|--------------|-----------|-------------|
603
+ * | `router:ready` | {@link RouterReadyCallback} | No | Router initialized |
604
+ * | `router:beforeEach` | {@link NavigationContextCallback} | Yes | Before guards run |
605
+ * | `router:beforeResolve` | {@link ResolveContextCallback} | Yes | Before component loading |
606
+ * | `router:afterResolve` | {@link ResolveContextCallback} | No | After components loaded |
607
+ * | `router:afterLeave` | {@link RouteChangeCallback} | No | After leaving route |
608
+ * | `router:beforeRender` | {@link RenderContextCallback} | No | Before DOM update |
609
+ * | `router:afterRender` | {@link RenderContextCallback} | No | After DOM update |
610
+ * | `router:scroll` | {@link ScrollContextCallback} | No | For scroll behavior |
611
+ * | `router:afterEnter` | {@link RouteChangeCallback} | No | After entering route |
612
+ * | `router:afterEach` | {@link RouteChangeCallback} | No | Navigation complete |
613
+ * | `router:onError` | {@link RouterErrorCallback} | No | Navigation error |
614
+ * | `router:routeAdded` | {@link RouteAddedCallback} | No | Dynamic route added |
615
+ * | `router:routeRemoved` | {@link RouteRemovedCallback} | No | Dynamic route removed |
616
+ *
617
+ * ## Reactive Signals
618
+ * - `currentRoute: Signal<RouteLocation | null>` - Current route info
619
+ * - `previousRoute: Signal<RouteLocation | null>` - Previous route info
620
+ * - `currentParams: Signal<Record<string, string>>` - Current route params
621
+ * - `currentQuery: Signal<Record<string, string>>` - Current query params
622
+ * - `currentLayout: Signal<MountResult | null>` - Mounted layout instance
623
+ * - `currentView: Signal<MountResult | null>` - Mounted view instance
624
+ * - `isReady: Signal<boolean>` - Router readiness state
625
+ *
626
+ * @note Internal API Access Policy:
627
+ * As a core Eleva plugin, the Router may access internal Eleva APIs (prefixed with _)
628
+ * such as `eleva._components`. This is intentional and these internal APIs are
629
+ * considered stable for official plugins. Third-party plugins should avoid
630
+ * accessing internal APIs as they may change without notice.
631
+ *
632
+ * @example
633
+ * // Basic setup
634
+ * const router = new Router(eleva, {
635
+ * mode: 'hash',
636
+ * mount: '#app',
637
+ * routes: [
638
+ * { path: '/', component: HomePage },
639
+ * { path: '/users/:id', component: UserPage },
640
+ * { path: '*', component: NotFoundPage }
641
+ * ]
642
+ * });
643
+ *
644
+ * // Start router
645
+ * await router.start();
646
+ *
647
+ * // Navigate programmatically
648
+ * const success = await router.navigate('/users/123');
649
+ *
650
+ * // Watch for route changes
651
+ * router.currentRoute.watch((route) => {
652
+ * document.title = route?.meta?.title || 'My App';
653
+ * });
654
+ *
655
+ * @private
656
+ */
657
+ class Router {
658
+ /**
659
+ * Creates an instance of the Router.
660
+ * @param {Eleva} eleva - The Eleva framework instance.
661
+ * @param {RouterOptions} options - The configuration options for the router.
662
+ */
663
+ constructor(eleva, options = {}) {
664
+ /** @type {Eleva} The Eleva framework instance. */
665
+ this.eleva = eleva;
666
+
667
+ /** @type {RouterOptions} The merged router options. */
668
+ this.options = {
669
+ mode: "hash",
670
+ queryParam: "view",
671
+ viewSelector: "root",
672
+ ...options
673
+ };
674
+
675
+ /** @private @type {RouteDefinition[]} The processed list of route definitions. */
676
+ this.routes = this._processRoutes(options.routes || []);
677
+
678
+ /** @private @type {import('eleva').Emitter} The shared Eleva event emitter for global hooks. */
679
+ this.emitter = this.eleva.emitter;
680
+
681
+ /** @private @type {boolean} A flag indicating if the router has been started. */
682
+ this.isStarted = false;
683
+
684
+ /** @private @type {boolean} A flag to prevent navigation loops from history events. */
685
+ this._isNavigating = false;
686
+
687
+ /** @private @type {number} Counter for tracking navigation operations to prevent race conditions. */
688
+ this._navigationId = 0;
689
+
690
+ /** @private @type {Array<() => void>} A collection of cleanup functions for event listeners. */
691
+ this.eventListeners = [];
692
+
693
+ /** @type {Signal<RouteLocation | null>} A reactive signal holding the current route's information. */
694
+ this.currentRoute = new this.eleva.signal(null);
695
+
696
+ /** @type {Signal<RouteLocation | null>} A reactive signal holding the previous route's information. */
697
+ this.previousRoute = new this.eleva.signal(null);
698
+
699
+ /** @type {Signal<Object<string, string>>} A reactive signal holding the current route's parameters. */
700
+ this.currentParams = new this.eleva.signal({});
701
+
702
+ /** @type {Signal<Object<string, string>>} A reactive signal holding the current route's query parameters. */
703
+ this.currentQuery = new this.eleva.signal({});
704
+
705
+ /** @type {Signal<import('eleva').MountResult | null>} A reactive signal for the currently mounted layout instance. */
706
+ this.currentLayout = new this.eleva.signal(null);
707
+
708
+ /** @type {Signal<import('eleva').MountResult | null>} A reactive signal for the currently mounted view (page) instance. */
709
+ this.currentView = new this.eleva.signal(null);
710
+
711
+ /** @type {Signal<boolean>} A reactive signal indicating if the router is ready (started and initial navigation complete). */
712
+ this.isReady = new this.eleva.signal(false);
713
+
714
+ /** @private @type {Map<string, RouterPlugin>} Map of registered plugins by name. */
715
+ this.plugins = new Map();
716
+
717
+ /** @private @type {Array<NavigationGuard>} Array of global before-each navigation guards. */
718
+ this._beforeEachGuards = [];
719
+
720
+ // If onBeforeEach was provided in options, add it to the guards array
721
+ if (options.onBeforeEach) {
722
+ this._beforeEachGuards.push(options.onBeforeEach);
723
+ }
724
+
725
+ /** @type {Object} The error handler instance. Can be overridden by plugins. */
726
+ this.errorHandler = CoreErrorHandler;
727
+
728
+ /** @private @type {Map<string, {x: number, y: number}>} Saved scroll positions by route path. */
729
+ this._scrollPositions = new Map();
730
+ this._validateOptions();
731
+ }
732
+
733
+ /**
734
+ * Validates the provided router options.
735
+ * @private
736
+ * @throws {Error} If the routing mode is invalid.
737
+ */
738
+ _validateOptions() {
739
+ if (!["hash", "query", "history"].includes(this.options.mode)) {
740
+ this.errorHandler.handle(new Error(`Invalid routing mode: ${this.options.mode}. Must be "hash", "query", or "history".`), "Configuration validation failed");
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Pre-processes route definitions to parse their path segments for efficient matching.
746
+ * @private
747
+ * @param {RouteDefinition[]} routes - The raw route definitions.
748
+ * @returns {RouteDefinition[]} The processed routes.
749
+ */
750
+ _processRoutes(routes) {
751
+ const processedRoutes = [];
752
+ for (const route of routes) {
753
+ try {
754
+ processedRoutes.push({
755
+ ...route,
756
+ segments: this._parsePathIntoSegments(route.path)
757
+ });
758
+ } catch (error) {
759
+ this.errorHandler.warn(`Invalid path in route definition "${route.path || "undefined"}": ${error.message}`, {
760
+ route,
761
+ error
762
+ });
763
+ }
764
+ }
765
+ return processedRoutes;
766
+ }
767
+
768
+ /**
769
+ * Parses a route path string into an array of static and parameter segments.
770
+ * @private
771
+ * @param {string} path - The path pattern to parse.
772
+ * @returns {Array<{type: 'static' | 'param', value?: string, name?: string}>} An array of segment objects.
773
+ * @throws {Error} If the route path is not a valid string.
774
+ */
775
+ _parsePathIntoSegments(path) {
776
+ if (!path || typeof path !== "string") {
777
+ this.errorHandler.handle(new Error("Route path must be a non-empty string"), "Path parsing failed", {
778
+ path
779
+ });
780
+ }
781
+ const normalizedPath = path.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
782
+ if (normalizedPath === "/") {
783
+ return [];
784
+ }
785
+ return normalizedPath.split("/").filter(Boolean).map(segment => {
786
+ if (segment.startsWith(":")) {
787
+ const paramName = segment.substring(1);
788
+ if (!paramName) {
789
+ this.errorHandler.handle(new Error(`Invalid parameter segment: ${segment}`), "Path parsing failed", {
790
+ segment,
791
+ path
792
+ });
793
+ }
794
+ return {
795
+ type: "param",
796
+ name: paramName
797
+ };
798
+ }
799
+ return {
800
+ type: "static",
801
+ value: segment
802
+ };
803
+ });
804
+ }
805
+
806
+ /**
807
+ * Finds the view element within a container using multiple selector strategies.
808
+ * @private
809
+ * @param {HTMLElement} container - The parent element to search within.
810
+ * @returns {HTMLElement} The found view element or the container itself as a fallback.
811
+ */
812
+ _findViewElement(container) {
813
+ const selector = this.options.viewSelector;
814
+ return container.querySelector(`#${selector}`) || container.querySelector(`.${selector}`) || container.querySelector(`[data-${selector}]`) || container.querySelector(selector) || container;
815
+ }
816
+
817
+ /**
818
+ * Starts the router, initializes event listeners, and performs the initial navigation.
819
+ * @returns {Promise<Router>} The router instance for method chaining.
820
+ *
821
+ * @example
822
+ * // Basic usage
823
+ * await router.start();
824
+ *
825
+ * // Method chaining
826
+ * await router.start().then(r => r.navigate('/home'));
827
+ *
828
+ * // Reactive readiness
829
+ * router.isReady.watch((ready) => {
830
+ * if (ready) console.log('Router is ready!');
831
+ * });
832
+ */
833
+ async start() {
834
+ if (this.isStarted) {
835
+ this.errorHandler.warn("Router is already started");
836
+ return this;
837
+ }
838
+ if (typeof window === "undefined") {
839
+ this.errorHandler.warn("Router start skipped: `window` object not available (SSR environment)");
840
+ return this;
841
+ }
842
+ if (typeof document !== "undefined" && !document.querySelector(this.options.mount)) {
843
+ this.errorHandler.warn(`Mount element "${this.options.mount}" was not found in the DOM. The router will not start.`, {
844
+ mountSelector: this.options.mount
845
+ });
846
+ return this;
847
+ }
848
+ const handler = () => this._handleRouteChange();
849
+ if (this.options.mode === "hash") {
850
+ window.addEventListener("hashchange", handler);
851
+ this.eventListeners.push(() => window.removeEventListener("hashchange", handler));
852
+ } else {
853
+ window.addEventListener("popstate", handler);
854
+ this.eventListeners.push(() => window.removeEventListener("popstate", handler));
855
+ }
856
+ this.isStarted = true;
857
+ // Initial navigation is not a popstate event
858
+ await this._handleRouteChange(false);
859
+ // Set isReady to true after initial navigation completes
860
+ this.isReady.value = true;
861
+ await this.emitter.emit("router:ready", this);
862
+ return this;
863
+ }
864
+
865
+ /**
866
+ * Stops the router and cleans up all event listeners and mounted components.
867
+ * @returns {Promise<void>}
868
+ */
869
+ async destroy() {
870
+ if (!this.isStarted) return;
871
+
872
+ // Clean up plugins
873
+ for (const plugin of this.plugins.values()) {
874
+ if (typeof plugin.destroy === "function") {
875
+ try {
876
+ await plugin.destroy(this);
877
+ } catch (error) {
878
+ this.errorHandler.log(`Plugin ${plugin.name} destroy failed`, error);
879
+ }
880
+ }
881
+ }
882
+ this.eventListeners.forEach(cleanup => cleanup());
883
+ this.eventListeners = [];
884
+ if (this.currentLayout.value) {
885
+ await this.currentLayout.value.unmount();
886
+ }
887
+ this.isStarted = false;
888
+ this.isReady.value = false;
889
+ }
890
+
891
+ /**
892
+ * Alias for destroy(). Stops the router and cleans up all resources.
893
+ * Provided for semantic consistency (start/stop pattern).
894
+ * @returns {Promise<void>}
895
+ *
896
+ * @example
897
+ * await router.start();
898
+ * // ... later
899
+ * await router.stop();
900
+ */
901
+ async stop() {
902
+ return this.destroy();
903
+ }
904
+
905
+ /**
906
+ * Programmatically navigates to a new route.
907
+ * @param {string | NavigationTarget} location - The target location as a path string or navigation target object.
908
+ * @param {Record<string, string>} [params] - Route parameters (only used when location is a string).
909
+ * @returns {Promise<boolean>} True if navigation succeeded, false if blocked by guards or failed.
910
+ *
911
+ * @example
912
+ * // Basic navigation
913
+ * await router.navigate('/users/123');
914
+ *
915
+ * // Check if navigation succeeded
916
+ * const success = await router.navigate('/protected');
917
+ * if (!success) {
918
+ * console.log('Navigation was blocked by a guard');
919
+ * }
920
+ *
921
+ * // Navigate with options
922
+ * await router.navigate({
923
+ * path: '/users/:id',
924
+ * params: { id: '123' },
925
+ * query: { tab: 'profile' },
926
+ * replace: true
927
+ * });
928
+ */
929
+ async navigate(location, params = {}) {
930
+ try {
931
+ const target = typeof location === "string" ? {
932
+ path: location,
933
+ params
934
+ } : location;
935
+ let path = this._buildPath(target.path, target.params || {});
936
+ const query = target.query || {};
937
+ if (Object.keys(query).length > 0) {
938
+ const queryString = new URLSearchParams(query).toString();
939
+ if (queryString) path += `?${queryString}`;
940
+ }
941
+ if (this._isSameRoute(path, target.params, query)) {
942
+ return true; // Already at this route, consider it successful
943
+ }
944
+ const navigationSuccessful = await this._proceedWithNavigation(path);
945
+ if (navigationSuccessful) {
946
+ // Increment navigation ID and capture it for this navigation
947
+ const currentNavId = ++this._navigationId;
948
+ this._isNavigating = true;
949
+ const state = target.state || {};
950
+ const replace = target.replace || false;
951
+ const historyMethod = replace ? "replaceState" : "pushState";
952
+ if (this.options.mode === "hash") {
953
+ if (replace) {
954
+ const newUrl = `${window.location.pathname}${window.location.search}#${path}`;
955
+ window.history.replaceState(state, "", newUrl);
956
+ } else {
957
+ window.location.hash = path;
958
+ }
959
+ } else {
960
+ const url = this.options.mode === "query" ? this._buildQueryUrl(path) : path;
961
+ history[historyMethod](state, "", url);
962
+ }
963
+
964
+ // Only reset the flag if no newer navigation has started
965
+ queueMicrotask(() => {
966
+ if (this._navigationId === currentNavId) {
967
+ this._isNavigating = false;
968
+ }
969
+ });
970
+ }
971
+ return navigationSuccessful;
972
+ } catch (error) {
973
+ this.errorHandler.log("Navigation failed", error);
974
+ await this.emitter.emit("router:onError", error);
975
+ return false;
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Builds a URL for query mode.
981
+ * @private
982
+ * @param {string} path - The path to set as the query parameter.
983
+ * @returns {string} The full URL with the updated query string.
984
+ */
985
+ _buildQueryUrl(path) {
986
+ const urlParams = new URLSearchParams(window.location.search);
987
+ urlParams.set(this.options.queryParam, path.split("?")[0]);
988
+ return `${window.location.pathname}?${urlParams.toString()}`;
989
+ }
990
+
991
+ /**
992
+ * Checks if the target route is identical to the current route.
993
+ * @private
994
+ * @param {string} path - The target path with query string.
995
+ * @param {object} params - The target params.
996
+ * @param {object} query - The target query.
997
+ * @returns {boolean} - True if the routes are the same.
998
+ */
999
+ _isSameRoute(path, params, query) {
1000
+ const current = this.currentRoute.value;
1001
+ if (!current) return false;
1002
+ const [targetPath, queryString] = path.split("?");
1003
+ const targetQuery = query || this._parseQuery(queryString || "");
1004
+ return current.path === targetPath && JSON.stringify(current.params) === JSON.stringify(params || {}) && JSON.stringify(current.query) === JSON.stringify(targetQuery);
1005
+ }
1006
+
1007
+ /**
1008
+ * Injects dynamic parameters into a path string.
1009
+ * @private
1010
+ */
1011
+ _buildPath(path, params) {
1012
+ let result = path;
1013
+ for (const [key, value] of Object.entries(params)) {
1014
+ // Fix: Handle special characters and ensure proper encoding
1015
+ const encodedValue = encodeURIComponent(String(value));
1016
+ result = result.replace(new RegExp(`:${key}\\b`, "g"), encodedValue);
1017
+ }
1018
+ return result;
1019
+ }
1020
+
1021
+ /**
1022
+ * The handler for browser-initiated route changes (e.g., back/forward buttons).
1023
+ * @private
1024
+ * @param {boolean} [isPopState=true] - Whether this is a popstate event (back/forward navigation).
1025
+ */
1026
+ async _handleRouteChange(isPopState = true) {
1027
+ if (this._isNavigating) return;
1028
+ try {
1029
+ const from = this.currentRoute.value;
1030
+ const toLocation = this._getCurrentLocation();
1031
+ const navigationSuccessful = await this._proceedWithNavigation(toLocation.fullUrl, isPopState);
1032
+
1033
+ // If navigation was blocked by a guard, revert the URL change
1034
+ if (!navigationSuccessful && from) {
1035
+ this.navigate({
1036
+ path: from.path,
1037
+ query: from.query,
1038
+ replace: true
1039
+ });
1040
+ }
1041
+ } catch (error) {
1042
+ this.errorHandler.log("Route change handling failed", error, {
1043
+ currentUrl: typeof window !== "undefined" ? window.location.href : ""
1044
+ });
1045
+ await this.emitter.emit("router:onError", error);
1046
+ }
1047
+ }
1048
+
1049
+ /**
1050
+ * Manages the core navigation lifecycle. Runs guards before committing changes.
1051
+ * Emits lifecycle events that plugins can hook into:
1052
+ * - router:beforeEach - Before guards run (can block/redirect via context)
1053
+ * - router:beforeResolve - Before component resolution (can block/redirect)
1054
+ * - router:afterResolve - After components are resolved
1055
+ * - router:beforeRender - Before DOM rendering
1056
+ * - router:afterRender - After DOM rendering
1057
+ * - router:scroll - After render, for scroll behavior
1058
+ * - router:afterEnter - After entering a route
1059
+ * - router:afterLeave - After leaving a route
1060
+ * - router:afterEach - After navigation completes
1061
+ *
1062
+ * @private
1063
+ * @param {string} fullPath - The full path (e.g., '/users/123?foo=bar') to navigate to.
1064
+ * @param {boolean} [isPopState=false] - Whether this navigation was triggered by popstate (back/forward).
1065
+ * @returns {Promise<boolean>} - `true` if navigation succeeded, `false` if aborted.
1066
+ */
1067
+ async _proceedWithNavigation(fullPath, isPopState = false) {
1068
+ const from = this.currentRoute.value;
1069
+ const [path, queryString] = (fullPath || "/").split("?");
1070
+ const toLocation = {
1071
+ path: path.startsWith("/") ? path : `/${path}`,
1072
+ query: this._parseQuery(queryString),
1073
+ fullUrl: fullPath
1074
+ };
1075
+ let toMatch = this._matchRoute(toLocation.path);
1076
+ if (!toMatch) {
1077
+ const notFoundRoute = this.routes.find(route => route.path === "*");
1078
+ if (notFoundRoute) {
1079
+ toMatch = {
1080
+ route: notFoundRoute,
1081
+ params: {
1082
+ pathMatch: decodeURIComponent(toLocation.path.substring(1))
1083
+ }
1084
+ };
1085
+ } else {
1086
+ await this.emitter.emit("router:onError", new Error(`Route not found: ${toLocation.path}`), toLocation, from);
1087
+ return false;
1088
+ }
1089
+ }
1090
+ const to = {
1091
+ ...toLocation,
1092
+ params: toMatch.params,
1093
+ meta: toMatch.route.meta || {},
1094
+ name: toMatch.route.name,
1095
+ matched: toMatch.route
1096
+ };
1097
+ try {
1098
+ // 1. Run all *pre-navigation* guards.
1099
+ const canNavigate = await this._runGuards(to, from, toMatch.route);
1100
+ if (!canNavigate) return false;
1101
+
1102
+ // 2. Save current scroll position before navigating away
1103
+ if (from && typeof window !== "undefined") {
1104
+ this._scrollPositions.set(from.path, {
1105
+ x: window.scrollX || window.pageXOffset || 0,
1106
+ y: window.scrollY || window.pageYOffset || 0
1107
+ });
1108
+ }
1109
+
1110
+ // 3. Emit beforeResolve event - plugins can show loading indicators
1111
+ /** @type {ResolveContext} */
1112
+ const resolveContext = {
1113
+ to,
1114
+ from,
1115
+ route: toMatch.route,
1116
+ layoutComponent: null,
1117
+ pageComponent: null,
1118
+ cancelled: false,
1119
+ redirectTo: null
1120
+ };
1121
+ await this.emitter.emit("router:beforeResolve", resolveContext);
1122
+
1123
+ // Check if resolution was cancelled or redirected
1124
+ if (resolveContext.cancelled) return false;
1125
+ if (resolveContext.redirectTo) {
1126
+ this.navigate(resolveContext.redirectTo);
1127
+ return false;
1128
+ }
1129
+
1130
+ // 4. Resolve async components *before* touching the DOM.
1131
+ const {
1132
+ layoutComponent,
1133
+ pageComponent
1134
+ } = await this._resolveComponents(toMatch.route);
1135
+
1136
+ // 5. Emit afterResolve event - plugins can hide loading indicators
1137
+ resolveContext.layoutComponent = layoutComponent;
1138
+ resolveContext.pageComponent = pageComponent;
1139
+ await this.emitter.emit("router:afterResolve", resolveContext);
1140
+
1141
+ // 6. Unmount the previous view/layout.
1142
+ if (from) {
1143
+ const toLayout = toMatch.route.layout || this.options.globalLayout;
1144
+ const fromLayout = from.matched.layout || this.options.globalLayout;
1145
+ const tryUnmount = async instance => {
1146
+ if (!instance) return;
1147
+ try {
1148
+ await instance.unmount();
1149
+ } catch (error) {
1150
+ this.errorHandler.warn("Error during component unmount", {
1151
+ error,
1152
+ instance
1153
+ });
1154
+ }
1155
+ };
1156
+ if (toLayout !== fromLayout) {
1157
+ await tryUnmount(this.currentLayout.value);
1158
+ this.currentLayout.value = null;
1159
+ } else {
1160
+ await tryUnmount(this.currentView.value);
1161
+ this.currentView.value = null;
1162
+ }
1163
+
1164
+ // Call `afterLeave` hook *after* the old component has been unmounted.
1165
+ if (from.matched.afterLeave) {
1166
+ await from.matched.afterLeave(to, from);
1167
+ }
1168
+ await this.emitter.emit("router:afterLeave", to, from);
1169
+ }
1170
+
1171
+ // 7. Update reactive state.
1172
+ this.previousRoute.value = from;
1173
+ this.currentRoute.value = to;
1174
+ this.currentParams.value = to.params || {};
1175
+ this.currentQuery.value = to.query || {};
1176
+
1177
+ // 8. Emit beforeRender event - plugins can add transitions
1178
+ /** @type {RenderContext} */
1179
+ const renderContext = {
1180
+ to,
1181
+ from,
1182
+ layoutComponent,
1183
+ pageComponent
1184
+ };
1185
+ await this.emitter.emit("router:beforeRender", renderContext);
1186
+
1187
+ // 9. Render the new components.
1188
+ await this._render(layoutComponent, pageComponent, to);
1189
+
1190
+ // 10. Emit afterRender event - plugins can trigger animations
1191
+ await this.emitter.emit("router:afterRender", renderContext);
1192
+
1193
+ // 11. Emit scroll event - plugins can handle scroll restoration
1194
+ /** @type {ScrollContext} */
1195
+ const scrollContext = {
1196
+ to,
1197
+ from,
1198
+ savedPosition: isPopState ? this._scrollPositions.get(to.path) || null : null
1199
+ };
1200
+ await this.emitter.emit("router:scroll", scrollContext);
1201
+
1202
+ // 12. Run post-navigation hooks.
1203
+ if (toMatch.route.afterEnter) {
1204
+ await toMatch.route.afterEnter(to, from);
1205
+ }
1206
+ await this.emitter.emit("router:afterEnter", to, from);
1207
+ await this.emitter.emit("router:afterEach", to, from);
1208
+ return true;
1209
+ } catch (error) {
1210
+ this.errorHandler.log("Error during navigation", error, {
1211
+ to,
1212
+ from
1213
+ });
1214
+ await this.emitter.emit("router:onError", error, to, from);
1215
+ return false;
1216
+ }
1217
+ }
1218
+
1219
+ /**
1220
+ * Executes all applicable navigation guards for a transition in order.
1221
+ * Guards are executed in the following order:
1222
+ * 1. Global beforeEach event (emitter-based, can block via context)
1223
+ * 2. Global beforeEach guards (registered via onBeforeEach)
1224
+ * 3. Route-specific beforeLeave guard (from the route being left)
1225
+ * 4. Route-specific beforeEnter guard (from the route being entered)
1226
+ *
1227
+ * @private
1228
+ * @param {RouteLocation} to - The target route location.
1229
+ * @param {RouteLocation | null} from - The current route location (null on initial navigation).
1230
+ * @param {RouteDefinition} route - The matched route definition.
1231
+ * @returns {Promise<boolean>} - `false` if navigation should be aborted.
1232
+ */
1233
+ async _runGuards(to, from, route) {
1234
+ // Create navigation context that plugins can modify to block navigation
1235
+ /** @type {NavigationContext} */
1236
+ const navContext = {
1237
+ to,
1238
+ from,
1239
+ cancelled: false,
1240
+ redirectTo: null
1241
+ };
1242
+
1243
+ // Emit beforeEach event with context - plugins can block by modifying context
1244
+ await this.emitter.emit("router:beforeEach", navContext);
1245
+
1246
+ // Check if navigation was cancelled or redirected by event listeners
1247
+ if (navContext.cancelled) return false;
1248
+ if (navContext.redirectTo) {
1249
+ this.navigate(navContext.redirectTo);
1250
+ return false;
1251
+ }
1252
+
1253
+ // Collect all guards in execution order
1254
+ const guards = [...this._beforeEachGuards, ...(from && from.matched.beforeLeave ? [from.matched.beforeLeave] : []), ...(route.beforeEnter ? [route.beforeEnter] : [])];
1255
+ for (const guard of guards) {
1256
+ const result = await guard(to, from);
1257
+ if (result === false) return false;
1258
+ if (typeof result === "string" || typeof result === "object") {
1259
+ this.navigate(result);
1260
+ return false;
1261
+ }
1262
+ }
1263
+ return true;
1264
+ }
1265
+
1266
+ /**
1267
+ * Resolves a string component definition to a component object.
1268
+ * @private
1269
+ * @param {string} def - The component name to resolve.
1270
+ * @returns {ComponentDefinition} The resolved component.
1271
+ * @throws {Error} If the component is not registered.
1272
+ *
1273
+ * @note Core plugins (Router, Attr, Props, Store) may access eleva._components
1274
+ * directly. This is intentional and stable for official Eleva plugins shipped
1275
+ * with the framework. Third-party plugins should use eleva.component() for
1276
+ * registration and avoid direct access to internal APIs.
1277
+ */
1278
+ _resolveStringComponent(def) {
1279
+ const componentDef = this.eleva._components.get(def);
1280
+ if (!componentDef) {
1281
+ this.errorHandler.handle(new Error(`Component "${def}" not registered.`), "Component resolution failed", {
1282
+ componentName: def,
1283
+ availableComponents: Array.from(this.eleva._components.keys())
1284
+ });
1285
+ }
1286
+ return componentDef;
1287
+ }
1288
+
1289
+ /**
1290
+ * Resolves a function component definition to a component object.
1291
+ * @private
1292
+ * @param {Function} def - The function to resolve.
1293
+ * @returns {Promise<ComponentDefinition>} The resolved component.
1294
+ * @throws {Error} If the function fails to load the component.
1295
+ */
1296
+ async _resolveFunctionComponent(def) {
1297
+ try {
1298
+ const funcStr = def.toString();
1299
+ const isAsyncImport = funcStr.includes("import(") || funcStr.startsWith("() =>");
1300
+ const result = await def();
1301
+ return isAsyncImport ? result.default || result : result;
1302
+ } catch (error) {
1303
+ this.errorHandler.handle(new Error(`Failed to load async component: ${error.message}`), "Component resolution failed", {
1304
+ function: def.toString(),
1305
+ error
1306
+ });
1307
+ }
1308
+ }
1309
+
1310
+ /**
1311
+ * Validates a component definition object.
1312
+ * @private
1313
+ * @param {any} def - The component definition to validate.
1314
+ * @returns {ComponentDefinition} The validated component.
1315
+ * @throws {Error} If the component definition is invalid.
1316
+ */
1317
+ _validateComponentDefinition(def) {
1318
+ if (!def || typeof def !== "object") {
1319
+ this.errorHandler.handle(new Error(`Invalid component definition: ${typeof def}`), "Component validation failed", {
1320
+ definition: def
1321
+ });
1322
+ }
1323
+ if (typeof def.template !== "function" && typeof def.template !== "string") {
1324
+ this.errorHandler.handle(new Error("Component missing template property"), "Component validation failed", {
1325
+ definition: def
1326
+ });
1327
+ }
1328
+ return def;
1329
+ }
1330
+
1331
+ /**
1332
+ * Resolves a component definition to a component object.
1333
+ * @private
1334
+ * @param {any} def - The component definition to resolve.
1335
+ * @returns {Promise<ComponentDefinition | null>} The resolved component or null.
1336
+ */
1337
+ async _resolveComponent(def) {
1338
+ if (def === null || def === undefined) {
1339
+ return null;
1340
+ }
1341
+ if (typeof def === "string") {
1342
+ return this._resolveStringComponent(def);
1343
+ }
1344
+ if (typeof def === "function") {
1345
+ return await this._resolveFunctionComponent(def);
1346
+ }
1347
+ if (def && typeof def === "object") {
1348
+ return this._validateComponentDefinition(def);
1349
+ }
1350
+ this.errorHandler.handle(new Error(`Invalid component definition: ${typeof def}`), "Component resolution failed", {
1351
+ definition: def
1352
+ });
1353
+ }
1354
+
1355
+ /**
1356
+ * Asynchronously resolves the layout and page components for a route.
1357
+ * @private
1358
+ * @param {RouteDefinition} route - The route to resolve components for.
1359
+ * @returns {Promise<{layoutComponent: ComponentDefinition | null, pageComponent: ComponentDefinition}>}
1360
+ */
1361
+ async _resolveComponents(route) {
1362
+ const effectiveLayout = route.layout || this.options.globalLayout;
1363
+ try {
1364
+ const [layoutComponent, pageComponent] = await Promise.all([this._resolveComponent(effectiveLayout), this._resolveComponent(route.component)]);
1365
+ if (!pageComponent) {
1366
+ this.errorHandler.handle(new Error(`Page component is null or undefined for route: ${route.path}`), "Component resolution failed", {
1367
+ route: route.path
1368
+ });
1369
+ }
1370
+ return {
1371
+ layoutComponent,
1372
+ pageComponent
1373
+ };
1374
+ } catch (error) {
1375
+ this.errorHandler.log(`Error resolving components for route ${route.path}`, error, {
1376
+ route: route.path
1377
+ });
1378
+ throw error;
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Renders the components for the current route into the DOM.
1384
+ * @private
1385
+ * @param {ComponentDefinition | null} layoutComponent - The pre-loaded layout component.
1386
+ * @param {ComponentDefinition} pageComponent - The pre-loaded page component.
1387
+ */
1388
+ async _render(layoutComponent, pageComponent) {
1389
+ const mountEl = document.querySelector(this.options.mount);
1390
+ if (!mountEl) {
1391
+ this.errorHandler.handle(new Error(`Mount element "${this.options.mount}" not found.`), {
1392
+ mountSelector: this.options.mount
1393
+ });
1394
+ }
1395
+ if (layoutComponent) {
1396
+ const layoutInstance = await this.eleva.mount(mountEl, this._wrapComponentWithChildren(layoutComponent));
1397
+ this.currentLayout.value = layoutInstance;
1398
+ const viewEl = this._findViewElement(layoutInstance.container);
1399
+ const viewInstance = await this.eleva.mount(viewEl, this._wrapComponentWithChildren(pageComponent));
1400
+ this.currentView.value = viewInstance;
1401
+ } else {
1402
+ const viewInstance = await this.eleva.mount(mountEl, this._wrapComponentWithChildren(pageComponent));
1403
+ this.currentView.value = viewInstance;
1404
+ this.currentLayout.value = null;
1405
+ }
1406
+ }
1407
+
1408
+ /**
1409
+ * Creates a getter function for router context properties.
1410
+ * @private
1411
+ * @param {string} property - The property name to access.
1412
+ * @param {any} defaultValue - The default value if property is undefined.
1413
+ * @returns {Function} A getter function.
1414
+ */
1415
+ _createRouteGetter(property, defaultValue) {
1416
+ return () => this.currentRoute.value?.[property] ?? defaultValue;
1417
+ }
1418
+
1419
+ /**
1420
+ * Wraps a component definition to inject router-specific context into its setup function.
1421
+ * @private
1422
+ * @param {ComponentDefinition} component - The component to wrap.
1423
+ * @returns {ComponentDefinition} The wrapped component definition.
1424
+ */
1425
+ _wrapComponent(component) {
1426
+ const originalSetup = component.setup;
1427
+ const self = this;
1428
+ return {
1429
+ ...component,
1430
+ async setup(ctx) {
1431
+ ctx.router = {
1432
+ navigate: self.navigate.bind(self),
1433
+ current: self.currentRoute,
1434
+ previous: self.previousRoute,
1435
+ // Route property getters
1436
+ get params() {
1437
+ return self._createRouteGetter("params", {})();
1438
+ },
1439
+ get query() {
1440
+ return self._createRouteGetter("query", {})();
1441
+ },
1442
+ get path() {
1443
+ return self._createRouteGetter("path", "/")();
1444
+ },
1445
+ get fullUrl() {
1446
+ return self._createRouteGetter("fullUrl", window.location.href)();
1447
+ },
1448
+ get meta() {
1449
+ return self._createRouteGetter("meta", {})();
1450
+ }
1451
+ };
1452
+ return originalSetup ? await originalSetup(ctx) : {};
1453
+ }
1454
+ };
1455
+ }
1456
+
1457
+ /**
1458
+ * Recursively wraps all child components to ensure they have access to router context.
1459
+ * @private
1460
+ * @param {ComponentDefinition | string} component - The component to wrap (can be a definition object or a registered component name).
1461
+ * @returns {ComponentDefinition | string} The wrapped component definition or the original string reference.
1462
+ */
1463
+ _wrapComponentWithChildren(component) {
1464
+ // If the component is a string (registered component name), return as-is
1465
+ // The router context will be injected when the component is resolved during mounting
1466
+ if (typeof component === "string") {
1467
+ return component;
1468
+ }
1469
+
1470
+ // If not a valid component object, return as-is
1471
+ if (!component || typeof component !== "object") {
1472
+ return component;
1473
+ }
1474
+ const wrappedComponent = this._wrapComponent(component);
1475
+
1476
+ // If the component has children, wrap them too
1477
+ if (wrappedComponent.children && typeof wrappedComponent.children === "object") {
1478
+ const wrappedChildren = {};
1479
+ for (const [selector, childComponent] of Object.entries(wrappedComponent.children)) {
1480
+ wrappedChildren[selector] = this._wrapComponentWithChildren(childComponent);
1481
+ }
1482
+ wrappedComponent.children = wrappedChildren;
1483
+ }
1484
+ return wrappedComponent;
1485
+ }
1486
+
1487
+ /**
1488
+ * Gets the current location information from the browser's window object.
1489
+ * @private
1490
+ * @returns {Omit<RouteLocation, 'params' | 'meta' | 'name' | 'matched'>}
1491
+ */
1492
+ _getCurrentLocation() {
1493
+ if (typeof window === "undefined") return {
1494
+ path: "/",
1495
+ query: {},
1496
+ fullUrl: ""
1497
+ };
1498
+ let path, queryString, fullUrl;
1499
+ switch (this.options.mode) {
1500
+ case "hash":
1501
+ fullUrl = window.location.hash.slice(1) || "/";
1502
+ [path, queryString] = fullUrl.split("?");
1503
+ break;
1504
+ case "query":
1505
+ const urlParams = new URLSearchParams(window.location.search);
1506
+ path = urlParams.get(this.options.queryParam) || "/";
1507
+ queryString = window.location.search.slice(1);
1508
+ fullUrl = path;
1509
+ break;
1510
+ default:
1511
+ // 'history' mode
1512
+ path = window.location.pathname || "/";
1513
+ queryString = window.location.search.slice(1);
1514
+ fullUrl = `${path}${queryString ? "?" + queryString : ""}`;
1515
+ }
1516
+ return {
1517
+ path: path.startsWith("/") ? path : `/${path}`,
1518
+ query: this._parseQuery(queryString),
1519
+ fullUrl
1520
+ };
1521
+ }
1522
+
1523
+ /**
1524
+ * Parses a query string into a key-value object.
1525
+ * @private
1526
+ */
1527
+ _parseQuery(queryString) {
1528
+ const query = {};
1529
+ if (queryString) {
1530
+ new URLSearchParams(queryString).forEach((value, key) => {
1531
+ query[key] = value;
1532
+ });
1533
+ }
1534
+ return query;
1535
+ }
1536
+
1537
+ /**
1538
+ * Matches a given path against the registered routes.
1539
+ * @private
1540
+ * @param {string} path - The path to match.
1541
+ * @returns {{route: RouteDefinition, params: Object<string, string>} | null} The matched route and its params, or null.
1542
+ */
1543
+ _matchRoute(path) {
1544
+ const pathSegments = path.split("/").filter(Boolean);
1545
+ for (const route of this.routes) {
1546
+ // Handle the root path as a special case.
1547
+ if (route.path === "/") {
1548
+ if (pathSegments.length === 0) return {
1549
+ route,
1550
+ params: {}
1551
+ };
1552
+ continue;
1553
+ }
1554
+ if (route.segments.length !== pathSegments.length) continue;
1555
+ const params = {};
1556
+ let isMatch = true;
1557
+ for (let i = 0; i < route.segments.length; i++) {
1558
+ const routeSegment = route.segments[i];
1559
+ const pathSegment = pathSegments[i];
1560
+ if (routeSegment.type === "param") {
1561
+ params[routeSegment.name] = decodeURIComponent(pathSegment);
1562
+ } else if (routeSegment.value !== pathSegment) {
1563
+ isMatch = false;
1564
+ break;
1565
+ }
1566
+ }
1567
+ if (isMatch) return {
1568
+ route,
1569
+ params
1570
+ };
1571
+ }
1572
+ return null;
1573
+ }
1574
+
1575
+ // ============================================
1576
+ // Dynamic Route Management API
1577
+ // ============================================
1578
+
1579
+ /**
1580
+ * Adds a new route dynamically at runtime.
1581
+ * The route will be processed and available for navigation immediately.
1582
+ *
1583
+ * @param {RouteDefinition} route - The route definition to add.
1584
+ * @param {RouteDefinition} [parentRoute] - Optional parent route to add as a child (not yet implemented).
1585
+ * @returns {() => void} A function to remove the added route.
1586
+ *
1587
+ * @example
1588
+ * // Add a route dynamically
1589
+ * const removeRoute = router.addRoute({
1590
+ * path: '/dynamic',
1591
+ * component: DynamicPage,
1592
+ * meta: { title: 'Dynamic Page' }
1593
+ * });
1594
+ *
1595
+ * // Later, remove the route
1596
+ * removeRoute();
1597
+ */
1598
+ addRoute(route, parentRoute = null) {
1599
+ if (!route || !route.path) {
1600
+ this.errorHandler.warn("Invalid route definition: missing path", {
1601
+ route
1602
+ });
1603
+ return () => {};
1604
+ }
1605
+
1606
+ // Check if route already exists
1607
+ if (this.hasRoute(route.path)) {
1608
+ this.errorHandler.warn(`Route "${route.path}" already exists`, {
1609
+ route
1610
+ });
1611
+ return () => {};
1612
+ }
1613
+
1614
+ // Process the route (parse segments)
1615
+ const processedRoute = {
1616
+ ...route,
1617
+ segments: this._parsePathIntoSegments(route.path)
1618
+ };
1619
+
1620
+ // Add to routes array (before wildcard if exists)
1621
+ const wildcardIndex = this.routes.findIndex(r => r.path === "*");
1622
+ if (wildcardIndex !== -1) {
1623
+ this.routes.splice(wildcardIndex, 0, processedRoute);
1624
+ } else {
1625
+ this.routes.push(processedRoute);
1626
+ }
1627
+
1628
+ // Emit event for plugins
1629
+ this.emitter.emit("router:routeAdded", processedRoute);
1630
+
1631
+ // Return removal function
1632
+ return () => this.removeRoute(route.path);
1633
+ }
1634
+
1635
+ /**
1636
+ * Removes a route by its path.
1637
+ *
1638
+ * @param {string} path - The path of the route to remove.
1639
+ * @returns {boolean} True if the route was removed, false if not found.
1640
+ *
1641
+ * @example
1642
+ * router.removeRoute('/dynamic');
1643
+ */
1644
+ removeRoute(path) {
1645
+ const index = this.routes.findIndex(r => r.path === path);
1646
+ if (index === -1) {
1647
+ return false;
1648
+ }
1649
+ const [removedRoute] = this.routes.splice(index, 1);
1650
+
1651
+ // Emit event for plugins
1652
+ this.emitter.emit("router:routeRemoved", removedRoute);
1653
+ return true;
1654
+ }
1655
+
1656
+ /**
1657
+ * Checks if a route with the given path exists.
1658
+ *
1659
+ * @param {string} path - The path to check.
1660
+ * @returns {boolean} True if the route exists.
1661
+ *
1662
+ * @example
1663
+ * if (router.hasRoute('/users/:id')) {
1664
+ * console.log('User route exists');
1665
+ * }
1666
+ */
1667
+ hasRoute(path) {
1668
+ return this.routes.some(r => r.path === path);
1669
+ }
1670
+
1671
+ /**
1672
+ * Gets all registered routes.
1673
+ *
1674
+ * @returns {RouteDefinition[]} A copy of the routes array.
1675
+ *
1676
+ * @example
1677
+ * const routes = router.getRoutes();
1678
+ * console.log('Available routes:', routes.map(r => r.path));
1679
+ */
1680
+ getRoutes() {
1681
+ return [...this.routes];
1682
+ }
1683
+
1684
+ /**
1685
+ * Gets a route by its path.
1686
+ *
1687
+ * @param {string} path - The path of the route to get.
1688
+ * @returns {RouteDefinition | undefined} The route definition or undefined.
1689
+ *
1690
+ * @example
1691
+ * const route = router.getRoute('/users/:id');
1692
+ * if (route) {
1693
+ * console.log('Route meta:', route.meta);
1694
+ * }
1695
+ */
1696
+ getRoute(path) {
1697
+ return this.routes.find(r => r.path === path);
1698
+ }
1699
+
1700
+ // ============================================
1701
+ // Hook Registration Methods
1702
+ // ============================================
1703
+
1704
+ /**
1705
+ * Registers a global pre-navigation guard.
1706
+ * Multiple guards can be registered and will be executed in order.
1707
+ * Guards can also be registered via the emitter using `router:beforeEach` event.
1708
+ *
1709
+ * @param {NavigationGuard} guard - The guard function to register.
1710
+ * @returns {() => void} A function to unregister the guard.
1711
+ *
1712
+ * @example
1713
+ * // Register a guard
1714
+ * const unregister = router.onBeforeEach((to, from) => {
1715
+ * if (to.meta.requiresAuth && !isAuthenticated()) {
1716
+ * return '/login';
1717
+ * }
1718
+ * });
1719
+ *
1720
+ * // Later, unregister the guard
1721
+ * unregister();
1722
+ */
1723
+ onBeforeEach(guard) {
1724
+ this._beforeEachGuards.push(guard);
1725
+ return () => {
1726
+ const index = this._beforeEachGuards.indexOf(guard);
1727
+ if (index > -1) {
1728
+ this._beforeEachGuards.splice(index, 1);
1729
+ }
1730
+ };
1731
+ }
1732
+ /**
1733
+ * Registers a global hook that runs after a new route component has been mounted.
1734
+ * @param {NavigationHook} hook - The hook function to register.
1735
+ * @returns {() => void} A function to unregister the hook.
1736
+ */
1737
+ onAfterEnter(hook) {
1738
+ return this.emitter.on("router:afterEnter", hook);
1739
+ }
1740
+
1741
+ /**
1742
+ * Registers a global hook that runs after a route component has been unmounted.
1743
+ * @param {NavigationHook} hook - The hook function to register.
1744
+ * @returns {() => void} A function to unregister the hook.
1745
+ */
1746
+ onAfterLeave(hook) {
1747
+ return this.emitter.on("router:afterLeave", hook);
1748
+ }
1749
+
1750
+ /**
1751
+ * Registers a global hook that runs after a navigation has been confirmed and all hooks have completed.
1752
+ * @param {NavigationHook} hook - The hook function to register.
1753
+ * @returns {() => void} A function to unregister the hook.
1754
+ */
1755
+ onAfterEach(hook) {
1756
+ return this.emitter.on("router:afterEach", hook);
1757
+ }
1758
+
1759
+ /**
1760
+ * Registers a global error handler for navigation errors.
1761
+ * @param {(error: Error, to?: RouteLocation, from?: RouteLocation) => void} handler - The error handler function.
1762
+ * @returns {() => void} A function to unregister the handler.
1763
+ */
1764
+ onError(handler) {
1765
+ return this.emitter.on("router:onError", handler);
1766
+ }
1767
+
1768
+ /**
1769
+ * Registers a plugin with the router.
1770
+ * @param {RouterPlugin} plugin - The plugin to register.
1771
+ */
1772
+ use(plugin, options = {}) {
1773
+ if (typeof plugin.install !== "function") {
1774
+ this.errorHandler.handle(new Error("Plugin must have an install method"), "Plugin registration failed", {
1775
+ plugin
1776
+ });
1777
+ }
1778
+
1779
+ // Check if plugin is already registered
1780
+ if (this.plugins.has(plugin.name)) {
1781
+ this.errorHandler.warn(`Plugin "${plugin.name}" is already registered`, {
1782
+ existingPlugin: this.plugins.get(plugin.name)
1783
+ });
1784
+ return;
1785
+ }
1786
+ this.plugins.set(plugin.name, plugin);
1787
+ plugin.install(this, options);
1788
+ }
1789
+
1790
+ /**
1791
+ * Gets all registered plugins.
1792
+ * @returns {RouterPlugin[]} Array of registered plugins.
1793
+ */
1794
+ getPlugins() {
1795
+ return Array.from(this.plugins.values());
1796
+ }
1797
+
1798
+ /**
1799
+ * Gets a plugin by name.
1800
+ * @param {string} name - The plugin name.
1801
+ * @returns {RouterPlugin | undefined} The plugin or undefined.
1802
+ */
1803
+ getPlugin(name) {
1804
+ return this.plugins.get(name);
1805
+ }
1806
+
1807
+ /**
1808
+ * Removes a plugin from the router.
1809
+ * @param {string} name - The plugin name.
1810
+ * @returns {boolean} True if the plugin was removed.
1811
+ */
1812
+ removePlugin(name) {
1813
+ const plugin = this.plugins.get(name);
1814
+ if (!plugin) return false;
1815
+
1816
+ // Call destroy if available
1817
+ if (typeof plugin.destroy === "function") {
1818
+ try {
1819
+ plugin.destroy(this);
1820
+ } catch (error) {
1821
+ this.errorHandler.log(`Plugin ${name} destroy failed`, error);
1822
+ }
1823
+ }
1824
+ return this.plugins.delete(name);
1825
+ }
1826
+
1827
+ /**
1828
+ * Sets a custom error handler. Used by error handling plugins.
1829
+ * @param {Object} errorHandler - The error handler object with handle, warn, and log methods.
1830
+ */
1831
+ setErrorHandler(errorHandler) {
1832
+ if (errorHandler && typeof errorHandler.handle === "function" && typeof errorHandler.warn === "function" && typeof errorHandler.log === "function") {
1833
+ this.errorHandler = errorHandler;
1834
+ } else {
1835
+ console.warn("[ElevaRouter] Invalid error handler provided. Must have handle, warn, and log methods.");
1836
+ }
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * @typedef {Object} RouterOptions
1842
+ * @property {string} mount - A CSS selector for the main element where the app is mounted.
1843
+ * @property {RouteDefinition[]} routes - An array of route definitions.
1844
+ * @property {'hash' | 'query' | 'history'} [mode='hash'] - The routing mode.
1845
+ * @property {string} [queryParam='page'] - The query parameter to use in 'query' mode.
1846
+ * @property {string} [viewSelector='view'] - The selector for the view element within a layout.
1847
+ * @property {boolean} [autoStart=true] - Whether to start the router automatically.
1848
+ * @property {NavigationGuard} [onBeforeEach] - A global guard executed before every navigation.
1849
+ * @property {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} [globalLayout] - A global layout for all routes. Can be overridden by a route's specific layout.
1850
+ */
1851
+
1852
+ /**
1853
+ * @class 🚀 RouterPlugin
1854
+ * @classdesc A powerful, reactive, and flexible Router Plugin for Eleva applications.
1855
+ * This plugin provides comprehensive client-side routing functionality including:
1856
+ * - Multiple routing modes (hash, history, query)
1857
+ * - Navigation guards and lifecycle hooks
1858
+ * - Reactive state management
1859
+ * - Component resolution and lazy loading
1860
+ * - Layout and page component separation
1861
+ * - Plugin system for extensibility
1862
+ * - Advanced error handling
1863
+ *
1864
+ * @example
1865
+ * // Install the plugin
1866
+ * const app = new Eleva("myApp");
1867
+ *
1868
+ * const HomePage = { template: () => `<h1>Home</h1>` };
1869
+ * const AboutPage = { template: () => `<h1>About Us</h1>` };
1870
+ * const UserPage = {
1871
+ * template: (ctx) => `<h1>User: ${ctx.router.params.id}</h1>`
1872
+ * };
1873
+ *
1874
+ * app.use(RouterPlugin, {
1875
+ * mount: '#app',
1876
+ * mode: 'hash',
1877
+ * routes: [
1878
+ * { path: '/', component: HomePage },
1879
+ * { path: '/about', component: AboutPage },
1880
+ * { path: '/users/:id', component: UserPage }
1881
+ * ]
1882
+ * });
1883
+ */
1884
+ const RouterPlugin = {
1885
+ /**
1886
+ * Unique identifier for the plugin
1887
+ * @type {string}
1888
+ */
1889
+ name: "router",
1890
+ /**
1891
+ * Plugin version
1892
+ * @type {string}
1893
+ */
1894
+ version: "1.0.0-rc.11",
1895
+ /**
1896
+ * Plugin description
1897
+ * @type {string}
1898
+ */
1899
+ description: "Client-side routing for Eleva applications",
1900
+ /**
1901
+ * Installs the RouterPlugin into an Eleva instance.
1902
+ *
1903
+ * @param {Eleva} eleva - The Eleva instance
1904
+ * @param {RouterOptions} options - Router configuration options
1905
+ * @param {string} options.mount - A CSS selector for the main element where the app is mounted
1906
+ * @param {RouteDefinition[]} options.routes - An array of route definitions
1907
+ * @param {'hash' | 'query' | 'history'} [options.mode='hash'] - The routing mode
1908
+ * @param {string} [options.queryParam='page'] - The query parameter to use in 'query' mode
1909
+ * @param {string} [options.viewSelector='view'] - The selector for the view element within a layout
1910
+ * @param {boolean} [options.autoStart=true] - Whether to start the router automatically
1911
+ * @param {NavigationGuard} [options.onBeforeEach] - A global guard executed before every navigation
1912
+ * @param {string | ComponentDefinition | (() => Promise<{default: ComponentDefinition}>)} [options.globalLayout] - A global layout for all routes
1913
+ *
1914
+ * @example
1915
+ * // main.js
1916
+ * import Eleva from 'eleva';
1917
+ * import { RouterPlugin } from './plugins/RouterPlugin.js';
1918
+ *
1919
+ * const app = new Eleva('myApp');
1920
+ *
1921
+ * const HomePage = { template: () => `<h1>Home</h1>` };
1922
+ * const AboutPage = { template: () => `<h1>About Us</h1>` };
1923
+ *
1924
+ * app.use(RouterPlugin, {
1925
+ * mount: '#app',
1926
+ * routes: [
1927
+ * { path: '/', component: HomePage },
1928
+ * { path: '/about', component: AboutPage }
1929
+ * ]
1930
+ * });
1931
+ */
1932
+ install(eleva, options = {}) {
1933
+ if (!options.mount) {
1934
+ throw new Error("[RouterPlugin] 'mount' option is required");
1935
+ }
1936
+ if (!options.routes || !Array.isArray(options.routes)) {
1937
+ throw new Error("[RouterPlugin] 'routes' option must be an array");
1938
+ }
1939
+
1940
+ /**
1941
+ * Registers a component definition with the Eleva instance.
1942
+ * This method handles both inline component objects and pre-registered component names.
1943
+ *
1944
+ * @param {any} def - The component definition to register
1945
+ * @param {string} type - The type of component for naming (e.g., "Route", "Layout")
1946
+ * @returns {string | null} The registered component name or null if no definition provided
1947
+ */
1948
+ const register = (def, type) => {
1949
+ if (!def) return null;
1950
+ if (typeof def === "object" && def !== null && !def.name) {
1951
+ const name = `Eleva${type}Component_${Math.random().toString(36).slice(2, 11)}`;
1952
+ try {
1953
+ eleva.component(name, def);
1954
+ return name;
1955
+ } catch (error) {
1956
+ throw new Error(`[RouterPlugin] Failed to register ${type} component: ${error.message}`);
1957
+ }
1958
+ }
1959
+ return def;
1960
+ };
1961
+ if (options.globalLayout) {
1962
+ options.globalLayout = register(options.globalLayout, "GlobalLayout");
1963
+ }
1964
+ (options.routes || []).forEach(route => {
1965
+ route.component = register(route.component, "Route");
1966
+ if (route.layout) {
1967
+ route.layout = register(route.layout, "RouteLayout");
1968
+ }
1969
+ });
1970
+ const router = new Router(eleva, options);
1971
+ eleva.router = router;
1972
+ if (options.autoStart !== false) {
1973
+ queueMicrotask(() => router.start());
1974
+ }
1975
+
1976
+ // Add plugin metadata to the Eleva instance
1977
+ if (!eleva.plugins) {
1978
+ eleva.plugins = new Map();
1979
+ }
1980
+ eleva.plugins.set(this.name, {
1981
+ name: this.name,
1982
+ version: this.version,
1983
+ description: this.description,
1984
+ options
1985
+ });
1986
+
1987
+ // Add utility methods for manual router access
1988
+ eleva.navigate = router.navigate.bind(router);
1989
+ eleva.getCurrentRoute = () => router.currentRoute.value;
1990
+ eleva.getRouteParams = () => router.currentParams.value;
1991
+ eleva.getRouteQuery = () => router.currentQuery.value;
1992
+ return router;
1993
+ },
1994
+ /**
1995
+ * Uninstalls the plugin from the Eleva instance
1996
+ *
1997
+ * @param {Eleva} eleva - The Eleva instance
1998
+ */
1999
+ async uninstall(eleva) {
2000
+ if (eleva.router) {
2001
+ await eleva.router.destroy();
2002
+ delete eleva.router;
2003
+ }
2004
+
2005
+ // Remove plugin metadata
2006
+ if (eleva.plugins) {
2007
+ eleva.plugins.delete(this.name);
2008
+ }
2009
+
2010
+ // Remove utility methods
2011
+ delete eleva.navigate;
2012
+ delete eleva.getCurrentRoute;
2013
+ delete eleva.getRouteParams;
2014
+ delete eleva.getRouteQuery;
2015
+ }
2016
+ };
2017
+
2018
+ // ============================================================================
2019
+ // TYPE DEFINITIONS - TypeScript-friendly JSDoc types for IDE support
2020
+ // ============================================================================
2021
+
2022
+ /**
2023
+ * @typedef {Record<string, unknown>} TemplateData
2024
+ * Data context for template interpolation
2025
+ */
2026
+
2027
+ /**
2028
+ * @typedef {string} TemplateString
2029
+ * A string containing {{ expression }} interpolation markers
2030
+ */
2031
+
2032
+ /**
2033
+ * @typedef {string} Expression
2034
+ * A JavaScript expression to be evaluated in the data context
2035
+ */
2036
+
2037
+ /**
2038
+ * @typedef {unknown} EvaluationResult
2039
+ * The result of evaluating an expression (string, number, boolean, object, etc.)
2040
+ */
2041
+
2042
+ /**
2043
+ * @class 🔒 TemplateEngine
2044
+ * @classdesc A secure template engine that handles interpolation and dynamic attribute parsing.
2045
+ * Provides a way to evaluate expressions in templates.
2046
+ * All methods are static and can be called directly on the class.
2047
+ *
2048
+ * Template Syntax:
2049
+ * - `{{ expression }}` - Interpolate any JavaScript expression
2050
+ * - `{{ variable }}` - Access data properties directly
2051
+ * - `{{ object.property }}` - Access nested properties
2052
+ * - `{{ condition ? a : b }}` - Ternary expressions
2053
+ * - `{{ func(arg) }}` - Call functions from data context
2054
+ *
2055
+ * @example
2056
+ * // Basic interpolation
2057
+ * const template = "Hello, {{name}}!";
2058
+ * const data = { name: "World" };
2059
+ * const result = TemplateEngine.parse(template, data);
2060
+ * // Result: "Hello, World!"
2061
+ *
2062
+ * @example
2063
+ * // Nested properties
2064
+ * const template = "Welcome, {{user.name}}!";
2065
+ * const data = { user: { name: "John" } };
2066
+ * const result = TemplateEngine.parse(template, data);
2067
+ * // Result: "Welcome, John!"
2068
+ *
2069
+ * @example
2070
+ * // Expressions
2071
+ * const template = "Status: {{active ? 'Online' : 'Offline'}}";
2072
+ * const data = { active: true };
2073
+ * const result = TemplateEngine.parse(template, data);
2074
+ * // Result: "Status: Online"
2075
+ *
2076
+ * @example
2077
+ * // With Signal values
2078
+ * const template = "Count: {{count.value}}";
2079
+ * const data = { count: { value: 42 } };
2080
+ * const result = TemplateEngine.parse(template, data);
2081
+ * // Result: "Count: 42"
2082
+ */
2083
+ class TemplateEngine {
2084
+ /**
2085
+ * Regular expression for matching template expressions in the format {{ expression }}
2086
+ * Matches: {{ anything }} with optional whitespace inside braces
2087
+ *
2088
+ * @static
2089
+ * @private
2090
+ * @type {RegExp}
2091
+ */
2092
+ static expressionPattern = /\{\{\s*(.*?)\s*\}\}/g;
2093
+
2094
+ /**
2095
+ * Parses a template string, replacing expressions with their evaluated values.
2096
+ * Expressions are evaluated in the provided data context.
2097
+ *
2098
+ * @public
2099
+ * @static
2100
+ * @param {TemplateString|unknown} template - The template string to parse.
2101
+ * @param {TemplateData} data - The data context for evaluating expressions.
2102
+ * @returns {string} The parsed template with expressions replaced by their values.
2103
+ *
2104
+ * @example
2105
+ * // Simple variables
2106
+ * TemplateEngine.parse("Hello, {{name}}!", { name: "World" });
2107
+ * // Result: "Hello, World!"
2108
+ *
2109
+ * @example
2110
+ * // Nested properties
2111
+ * TemplateEngine.parse("{{user.name}} is {{user.age}} years old", {
2112
+ * user: { name: "John", age: 30 }
2113
+ * });
2114
+ * // Result: "John is 30 years old"
2115
+ *
2116
+ * @example
2117
+ * // Multiple expressions
2118
+ * TemplateEngine.parse("{{greeting}}, {{name}}! You have {{count}} messages.", {
2119
+ * greeting: "Hello",
2120
+ * name: "User",
2121
+ * count: 5
2122
+ * });
2123
+ * // Result: "Hello, User! You have 5 messages."
2124
+ *
2125
+ * @example
2126
+ * // With conditionals
2127
+ * TemplateEngine.parse("Status: {{online ? 'Active' : 'Inactive'}}", {
2128
+ * online: true
2129
+ * });
2130
+ * // Result: "Status: Active"
2131
+ */
2132
+ static parse(template, data) {
2133
+ if (typeof template !== "string") return template;
2134
+ return template.replace(this.expressionPattern, (_, expression) => this.evaluate(expression, data));
2135
+ }
2136
+
2137
+ /**
2138
+ * Evaluates an expression in the context of the provided data object.
2139
+ *
2140
+ * Note: This does not provide a true sandbox and evaluated expressions may access global scope.
2141
+ * The use of the `with` statement is necessary for expression evaluation but has security implications.
2142
+ * Only use with trusted templates. User input should never be directly interpolated.
2143
+ *
2144
+ * @public
2145
+ * @static
2146
+ * @param {Expression|unknown} expression - The expression to evaluate.
2147
+ * @param {TemplateData} data - The data context for evaluation.
2148
+ * @returns {EvaluationResult} The result of the evaluation, or an empty string if evaluation fails.
2149
+ *
2150
+ * @example
2151
+ * // Property access
2152
+ * TemplateEngine.evaluate("user.name", { user: { name: "John" } });
2153
+ * // Result: "John"
2154
+ *
2155
+ * @example
2156
+ * // Numeric values
2157
+ * TemplateEngine.evaluate("user.age", { user: { age: 30 } });
2158
+ * // Result: 30
2159
+ *
2160
+ * @example
2161
+ * // Expressions
2162
+ * TemplateEngine.evaluate("items.length > 0", { items: [1, 2, 3] });
2163
+ * // Result: true
2164
+ *
2165
+ * @example
2166
+ * // Function calls
2167
+ * TemplateEngine.evaluate("formatDate(date)", {
2168
+ * date: new Date(),
2169
+ * formatDate: (d) => d.toISOString()
2170
+ * });
2171
+ * // Result: "2024-01-01T00:00:00.000Z"
2172
+ *
2173
+ * @example
2174
+ * // Failed evaluation returns empty string
2175
+ * TemplateEngine.evaluate("nonexistent.property", {});
2176
+ * // Result: ""
2177
+ */
2178
+ static evaluate(expression, data) {
2179
+ if (typeof expression !== "string") return expression;
2180
+ try {
2181
+ return new Function("data", `with(data) { return ${expression}; }`)(data);
2182
+ } catch {
2183
+ return "";
2184
+ }
2185
+ }
2186
+ }
2187
+
2188
+ /**
2189
+ * @class 🎯 PropsPlugin
2190
+ * @classdesc A plugin that extends Eleva's props data handling to support any type of data structure
2191
+ * with automatic type detection, parsing, and reactive prop updates. This plugin enables seamless
2192
+ * passing of complex data types from parent to child components without manual parsing.
2193
+ *
2194
+ * Core Features:
2195
+ * - Automatic type detection and parsing (strings, numbers, booleans, objects, arrays, dates, etc.)
2196
+ * - Support for complex data structures including nested objects and arrays
2197
+ * - Reactive props that automatically update when parent data changes
2198
+ * - Comprehensive error handling with custom error callbacks
2199
+ * - Simple configuration with minimal setup required
2200
+ *
2201
+ * @example
2202
+ * // Install the plugin
2203
+ * const app = new Eleva("myApp");
2204
+ * app.use(PropsPlugin, {
2205
+ * enableAutoParsing: true,
2206
+ * enableReactivity: true,
2207
+ * onError: (error, value) => {
2208
+ * console.error('Props parsing error:', error, value);
2209
+ * }
2210
+ * });
2211
+ *
2212
+ * // Use complex props in components
2213
+ * app.component("UserCard", {
2214
+ * template: (ctx) => `
2215
+ * <div class="user-info-container"
2216
+ * :user='${JSON.stringify(ctx.user.value)}'
2217
+ * :permissions='${JSON.stringify(ctx.permissions.value)}'
2218
+ * :settings='${JSON.stringify(ctx.settings.value)}'>
2219
+ * </div>
2220
+ * `,
2221
+ * children: {
2222
+ * '.user-info-container': 'UserInfo'
2223
+ * }
2224
+ * });
2225
+ *
2226
+ * app.component("UserInfo", {
2227
+ * setup({ props }) {
2228
+ * return {
2229
+ * user: props.user, // Automatically parsed object
2230
+ * permissions: props.permissions, // Automatically parsed array
2231
+ * settings: props.settings // Automatically parsed object
2232
+ * };
2233
+ * }
2234
+ * });
2235
+ */
2236
+ const PropsPlugin = {
2237
+ /**
2238
+ * Unique identifier for the plugin
2239
+ * @type {string}
2240
+ */
2241
+ name: "props",
2242
+ /**
2243
+ * Plugin version
2244
+ * @type {string}
2245
+ */
2246
+ version: "1.0.0-rc.11",
2247
+ /**
2248
+ * Plugin description
2249
+ * @type {string}
2250
+ */
2251
+ description: "Advanced props data handling for complex data structures with automatic type detection and reactivity",
2252
+ /**
2253
+ * Installs the plugin into the Eleva instance
2254
+ *
2255
+ * @param {Object} eleva - The Eleva instance
2256
+ * @param {Object} options - Plugin configuration options
2257
+ * @param {boolean} [options.enableAutoParsing=true] - Enable automatic type detection and parsing
2258
+ * @param {boolean} [options.enableReactivity=true] - Enable reactive prop updates using Eleva's signal system
2259
+ * @param {Function} [options.onError=null] - Error handler function called when parsing fails
2260
+ *
2261
+ * @example
2262
+ * // Basic installation
2263
+ * app.use(PropsPlugin);
2264
+ *
2265
+ * // Installation with custom options
2266
+ * app.use(PropsPlugin, {
2267
+ * enableAutoParsing: true,
2268
+ * enableReactivity: false,
2269
+ * onError: (error, value) => {
2270
+ * console.error('Props parsing error:', error, value);
2271
+ * }
2272
+ * });
2273
+ */
2274
+ install(eleva, options = {}) {
2275
+ const {
2276
+ enableAutoParsing = true,
2277
+ enableReactivity = true,
2278
+ onError = null
2279
+ } = options;
2280
+
2281
+ /**
2282
+ * Detects the type of a given value
2283
+ * @private
2284
+ * @param {any} value - The value to detect type for
2285
+ * @returns {string} The detected type ('string', 'number', 'boolean', 'object', 'array', 'date', 'map', 'set', 'function', 'null', 'undefined', 'unknown')
2286
+ *
2287
+ * @example
2288
+ * detectType("hello") // → "string"
2289
+ * detectType(42) // → "number"
2290
+ * detectType(true) // → "boolean"
2291
+ * detectType([1, 2, 3]) // → "array"
2292
+ * detectType({}) // → "object"
2293
+ * detectType(new Date()) // → "date"
2294
+ * detectType(null) // → "null"
2295
+ */
2296
+ const detectType = value => {
2297
+ if (value === null) return "null";
2298
+ if (value === undefined) return "undefined";
2299
+ if (typeof value === "boolean") return "boolean";
2300
+ if (typeof value === "number") return "number";
2301
+ if (typeof value === "string") return "string";
2302
+ if (typeof value === "function") return "function";
2303
+ if (value instanceof Date) return "date";
2304
+ if (value instanceof Map) return "map";
2305
+ if (value instanceof Set) return "set";
2306
+ if (Array.isArray(value)) return "array";
2307
+ if (typeof value === "object") return "object";
2308
+ return "unknown";
2309
+ };
2310
+
2311
+ /**
2312
+ * Parses a prop value with automatic type detection
2313
+ * @private
2314
+ * @param {any} value - The value to parse
2315
+ * @returns {any} The parsed value with appropriate type
2316
+ *
2317
+ * @description
2318
+ * This function automatically detects and parses different data types from string values:
2319
+ * - Special strings: "true" → true, "false" → false, "null" → null, "undefined" → undefined
2320
+ * - JSON objects/arrays: '{"key": "value"}' → {key: "value"}, '[1, 2, 3]' → [1, 2, 3]
2321
+ * - Boolean-like strings: "1" → true, "0" → false, "" → true
2322
+ * - Numeric strings: "42" → 42, "3.14" → 3.14
2323
+ * - Date strings: "2023-01-01T00:00:00.000Z" → Date object
2324
+ * - Other strings: returned as-is
2325
+ *
2326
+ * @example
2327
+ * parsePropValue("true") // → true
2328
+ * parsePropValue("42") // → 42
2329
+ * parsePropValue('{"key": "val"}') // → {key: "val"}
2330
+ * parsePropValue('[1, 2, 3]') // → [1, 2, 3]
2331
+ * parsePropValue("hello") // → "hello"
2332
+ */
2333
+ const parsePropValue = value => {
2334
+ try {
2335
+ // Handle non-string values - return as-is
2336
+ if (typeof value !== "string") {
2337
+ return value;
2338
+ }
2339
+
2340
+ // Handle special string patterns first
2341
+ if (value === "true") return true;
2342
+ if (value === "false") return false;
2343
+ if (value === "null") return null;
2344
+ if (value === "undefined") return undefined;
2345
+
2346
+ // Try to parse as JSON (for objects and arrays)
2347
+ // This handles complex data structures like objects and arrays
2348
+ if (value.startsWith("{") || value.startsWith("[")) {
2349
+ try {
2350
+ return JSON.parse(value);
2351
+ } catch (e) {
2352
+ // Not valid JSON, throw error to trigger error handler
2353
+ throw new Error(`Invalid JSON: ${value}`);
2354
+ }
2355
+ }
2356
+
2357
+ // Handle boolean-like strings (including "1" and "0")
2358
+ // These are common in HTML attributes and should be treated as booleans
2359
+ if (value === "1") return true;
2360
+ if (value === "0") return false;
2361
+ if (value === "") return true; // Empty string is truthy in HTML attributes
2362
+
2363
+ // Handle numeric strings (after boolean check to avoid conflicts)
2364
+ // This ensures "0" is treated as boolean false, not number 0
2365
+ if (!isNaN(value) && value !== "" && !isNaN(parseFloat(value))) {
2366
+ return Number(value);
2367
+ }
2368
+
2369
+ // Handle date strings (ISO format)
2370
+ // Recognizes standard ISO date format and converts to Date object
2371
+ if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
2372
+ const date = new Date(value);
2373
+ if (!isNaN(date.getTime())) {
2374
+ return date;
2375
+ }
2376
+ }
2377
+
2378
+ // Return as string if no other parsing applies
2379
+ // This is the fallback for regular text strings
2380
+ return value;
2381
+ } catch (error) {
2382
+ // Call error handler if provided
2383
+ if (onError) {
2384
+ onError(error, value);
2385
+ }
2386
+ // Fallback to original value to prevent breaking the application
2387
+ return value;
2388
+ }
2389
+ };
2390
+
2391
+ /**
2392
+ * Enhanced props extraction with automatic type detection
2393
+ * @private
2394
+ * @param {HTMLElement} element - The DOM element to extract props from
2395
+ * @returns {Object} Object containing parsed props with appropriate types
2396
+ *
2397
+ * @description
2398
+ * Extracts props from DOM element attributes that start with ":" and automatically
2399
+ * parses them to their appropriate types. Removes the attributes from the element
2400
+ * after extraction.
2401
+ *
2402
+ * @example
2403
+ * // HTML: <div :name="John" :age="30" :active="true" :data='{"key": "value"}'></div>
2404
+ * const props = extractProps(element);
2405
+ * // Result: { name: "John", age: 30, active: true, data: {key: "value"} }
2406
+ */
2407
+ const extractProps = element => {
2408
+ const props = {};
2409
+ const attrs = element.attributes;
2410
+
2411
+ // Iterate through attributes in reverse order to handle removal correctly
2412
+ for (let i = attrs.length - 1; i >= 0; i--) {
2413
+ const attr = attrs[i];
2414
+ // Only process attributes that start with ":" (prop attributes)
2415
+ if (attr.name.startsWith(":")) {
2416
+ const propName = attr.name.slice(1); // Remove the ":" prefix
2417
+ // Parse the value if auto-parsing is enabled, otherwise use as-is
2418
+ const parsedValue = enableAutoParsing ? parsePropValue(attr.value) : attr.value;
2419
+ props[propName] = parsedValue;
2420
+ // Remove the attribute from the DOM element after extraction
2421
+ element.removeAttribute(attr.name);
2422
+ }
2423
+ }
2424
+ return props;
2425
+ };
2426
+
2427
+ /**
2428
+ * Creates reactive props using Eleva's signal system
2429
+ * @private
2430
+ * @param {Object} props - The props object to make reactive
2431
+ * @returns {Object} Object containing reactive props (Eleva signals)
2432
+ *
2433
+ * @description
2434
+ * Converts regular prop values into Eleva signals for reactive updates.
2435
+ * If a value is already a signal, it's passed through unchanged.
2436
+ *
2437
+ * @example
2438
+ * const props = { name: "John", age: 30, active: true };
2439
+ * const reactiveProps = createReactiveProps(props);
2440
+ * // Result: {
2441
+ * // name: Signal("John"),
2442
+ * // age: Signal(30),
2443
+ * // active: Signal(true)
2444
+ * // }
2445
+ */
2446
+ const createReactiveProps = props => {
2447
+ const reactiveProps = {};
2448
+
2449
+ // Convert each prop value to a reactive signal
2450
+ Object.entries(props).forEach(([key, value]) => {
2451
+ // Check if value is already a signal (has 'value' and 'watch' properties)
2452
+ if (value && typeof value === "object" && "value" in value && "watch" in value) {
2453
+ // Value is already a signal, use it as-is
2454
+ reactiveProps[key] = value;
2455
+ } else {
2456
+ // Create new signal for the prop value to make it reactive
2457
+ reactiveProps[key] = new eleva.signal(value);
2458
+ }
2459
+ });
2460
+ return reactiveProps;
2461
+ };
2462
+
2463
+ // Override Eleva's internal _extractProps method with our enhanced version
2464
+ eleva._extractProps = extractProps;
2465
+
2466
+ // Override Eleva's mount method to apply enhanced prop handling
2467
+ const originalMount = eleva.mount;
2468
+ eleva.mount = async (container, compName, props = {}) => {
2469
+ // Create reactive props if reactivity is enabled
2470
+ const enhancedProps = enableReactivity ? createReactiveProps(props) : props;
2471
+
2472
+ // Call the original mount method with enhanced props
2473
+ return await originalMount.call(eleva, container, compName, enhancedProps);
2474
+ };
2475
+
2476
+ // Override Eleva's _mountComponents method to enable signal reference passing
2477
+ const originalMountComponents = eleva._mountComponents;
2478
+
2479
+ // Cache to store parent contexts by container element
2480
+ const parentContextCache = new WeakMap();
2481
+ // Store child instances that need signal linking
2482
+ const pendingSignalLinks = new Set();
2483
+ eleva._mountComponents = async (container, children, childInstances) => {
2484
+ for (const [selector, component] of Object.entries(children)) {
2485
+ if (!selector) continue;
2486
+ for (const el of container.querySelectorAll(selector)) {
2487
+ if (!(el instanceof HTMLElement)) continue;
2488
+
2489
+ // Extract props from DOM attributes
2490
+ const extractedProps = eleva._extractProps(el);
2491
+
2492
+ // Get parent context to check for signal references
2493
+ let enhancedProps = extractedProps;
2494
+
2495
+ // Try to find parent context by looking up the DOM tree
2496
+ let parentContext = parentContextCache.get(container);
2497
+ if (!parentContext) {
2498
+ let currentElement = container;
2499
+ while (currentElement && !parentContext) {
2500
+ if (currentElement._eleva_instance && currentElement._eleva_instance.data) {
2501
+ parentContext = currentElement._eleva_instance.data;
2502
+ // Cache the parent context for future use
2503
+ parentContextCache.set(container, parentContext);
2504
+ break;
2505
+ }
2506
+ currentElement = currentElement.parentElement;
2507
+ }
2508
+ }
2509
+ if (enableReactivity && parentContext) {
2510
+ const signalProps = {};
2511
+
2512
+ // Check each extracted prop to see if there's a matching signal in parent context
2513
+ Object.keys(extractedProps).forEach(propName => {
2514
+ if (parentContext[propName] && parentContext[propName] instanceof eleva.signal) {
2515
+ // Found a signal in parent context with the same name as the prop
2516
+ // Pass the signal reference instead of creating a new one
2517
+ signalProps[propName] = parentContext[propName];
2518
+ }
2519
+ });
2520
+
2521
+ // Merge signal props with regular props (signal props take precedence)
2522
+ enhancedProps = {
2523
+ ...extractedProps,
2524
+ ...signalProps
2525
+ };
2526
+ }
2527
+
2528
+ // Create reactive props for non-signal props only
2529
+ let finalProps = enhancedProps;
2530
+ if (enableReactivity) {
2531
+ // Only create reactive props for values that aren't already signals
2532
+ const nonSignalProps = {};
2533
+ Object.entries(enhancedProps).forEach(([key, value]) => {
2534
+ if (!(value && typeof value === "object" && "value" in value && "watch" in value)) {
2535
+ // This is not a signal, create a reactive prop for it
2536
+ nonSignalProps[key] = value;
2537
+ }
2538
+ });
2539
+
2540
+ // Create reactive props only for non-signal values
2541
+ const reactiveNonSignalProps = createReactiveProps(nonSignalProps);
2542
+
2543
+ // Merge signal props with reactive non-signal props
2544
+ finalProps = {
2545
+ ...reactiveNonSignalProps,
2546
+ ...enhancedProps // Signal props take precedence
2547
+ };
2548
+ }
2549
+
2550
+ /** @type {MountResult} */
2551
+ const instance = await eleva.mount(el, component, finalProps);
2552
+ if (instance && !childInstances.includes(instance)) {
2553
+ childInstances.push(instance);
2554
+
2555
+ // If we have extracted props but no parent context yet, mark for later signal linking
2556
+ if (enableReactivity && Object.keys(extractedProps).length > 0 && !parentContext) {
2557
+ pendingSignalLinks.add({
2558
+ instance,
2559
+ extractedProps,
2560
+ container,
2561
+ component
2562
+ });
2563
+ }
2564
+ }
2565
+ }
2566
+ }
2567
+
2568
+ // After mounting all children, try to link signals for pending instances
2569
+ if (enableReactivity && pendingSignalLinks.size > 0) {
2570
+ for (const pending of pendingSignalLinks) {
2571
+ const {
2572
+ instance,
2573
+ extractedProps,
2574
+ container,
2575
+ component
2576
+ } = pending;
2577
+
2578
+ // Try to find parent context again
2579
+ let parentContext = parentContextCache.get(container);
2580
+ if (!parentContext) {
2581
+ let currentElement = container;
2582
+ while (currentElement && !parentContext) {
2583
+ if (currentElement._eleva_instance && currentElement._eleva_instance.data) {
2584
+ parentContext = currentElement._eleva_instance.data;
2585
+ parentContextCache.set(container, parentContext);
2586
+ break;
2587
+ }
2588
+ currentElement = currentElement.parentElement;
2589
+ }
2590
+ }
2591
+ if (parentContext) {
2592
+ const signalProps = {};
2593
+
2594
+ // Check each extracted prop to see if there's a matching signal in parent context
2595
+ Object.keys(extractedProps).forEach(propName => {
2596
+ if (parentContext[propName] && parentContext[propName] instanceof eleva.signal) {
2597
+ signalProps[propName] = parentContext[propName];
2598
+ }
2599
+ });
2600
+
2601
+ // Update the child instance's data with signal references
2602
+ if (Object.keys(signalProps).length > 0) {
2603
+ Object.assign(instance.data, signalProps);
2604
+
2605
+ // Set up signal watchers for the newly linked signals
2606
+ Object.keys(signalProps).forEach(propName => {
2607
+ const signal = signalProps[propName];
2608
+ if (signal && typeof signal.watch === "function") {
2609
+ signal.watch(newValue => {
2610
+ // Trigger a re-render of the child component when the signal changes
2611
+ const childComponent = eleva._components.get(component) || component;
2612
+ if (childComponent && childComponent.template) {
2613
+ const templateResult = typeof childComponent.template === "function" ? childComponent.template(instance.data) : childComponent.template;
2614
+ const newHtml = TemplateEngine.parse(templateResult, instance.data);
2615
+ eleva.renderer.patchDOM(instance.container, newHtml);
2616
+ }
2617
+ });
2618
+ }
2619
+ });
2620
+
2621
+ // Initial re-render to show the correct signal values
2622
+ const childComponent = eleva._components.get(component) || component;
2623
+ if (childComponent && childComponent.template) {
2624
+ const templateResult = typeof childComponent.template === "function" ? childComponent.template(instance.data) : childComponent.template;
2625
+ const newHtml = TemplateEngine.parse(templateResult, instance.data);
2626
+ eleva.renderer.patchDOM(instance.container, newHtml);
2627
+ }
2628
+ }
2629
+
2630
+ // Remove from pending list
2631
+ pendingSignalLinks.delete(pending);
2632
+ }
2633
+ }
2634
+ }
2635
+ };
2636
+
2637
+ /**
2638
+ * Expose utility methods on the Eleva instance
2639
+ * @namespace eleva.props
2640
+ */
2641
+ eleva.props = {
2642
+ /**
2643
+ * Parse a single value with automatic type detection
2644
+ * @param {any} value - The value to parse
2645
+ * @returns {any} The parsed value with appropriate type
2646
+ *
2647
+ * @example
2648
+ * app.props.parse("42") // → 42
2649
+ * app.props.parse("true") // → true
2650
+ * app.props.parse('{"key": "val"}') // → {key: "val"}
2651
+ */
2652
+ parse: value => {
2653
+ // Return value as-is if auto parsing is disabled
2654
+ if (!enableAutoParsing) {
2655
+ return value;
2656
+ }
2657
+ // Use our enhanced parsing function
2658
+ return parsePropValue(value);
2659
+ },
2660
+ /**
2661
+ * Detect the type of a value
2662
+ * @param {any} value - The value to detect type for
2663
+ * @returns {string} The detected type
2664
+ *
2665
+ * @example
2666
+ * app.props.detectType("hello") // → "string"
2667
+ * app.props.detectType(42) // → "number"
2668
+ * app.props.detectType([1, 2, 3]) // → "array"
2669
+ */
2670
+ detectType
2671
+ };
2672
+
2673
+ // Store original methods for uninstall
2674
+ eleva._originalExtractProps = eleva._extractProps;
2675
+ eleva._originalMount = originalMount;
2676
+ eleva._originalMountComponents = originalMountComponents;
2677
+ },
2678
+ /**
2679
+ * Uninstalls the plugin from the Eleva instance
2680
+ *
2681
+ * @param {Object} eleva - The Eleva instance
2682
+ *
2683
+ * @description
2684
+ * Restores the original Eleva methods and removes all plugin-specific
2685
+ * functionality. This method should be called when the plugin is no
2686
+ * longer needed.
2687
+ *
2688
+ * @example
2689
+ * // Uninstall the plugin
2690
+ * PropsPlugin.uninstall(app);
2691
+ */
2692
+ uninstall(eleva) {
2693
+ // Restore original _extractProps method
2694
+ if (eleva._originalExtractProps) {
2695
+ eleva._extractProps = eleva._originalExtractProps;
2696
+ delete eleva._originalExtractProps;
2697
+ }
2698
+
2699
+ // Restore original mount method
2700
+ if (eleva._originalMount) {
2701
+ eleva.mount = eleva._originalMount;
2702
+ delete eleva._originalMount;
2703
+ }
2704
+
2705
+ // Restore original _mountComponents method
2706
+ if (eleva._originalMountComponents) {
2707
+ eleva._mountComponents = eleva._originalMountComponents;
2708
+ delete eleva._originalMountComponents;
2709
+ }
2710
+
2711
+ // Remove plugin utility methods
2712
+ if (eleva.props) {
2713
+ delete eleva.props;
2714
+ }
2715
+ }
2716
+ };
2717
+
2718
+ /**
2719
+ * @class 🏪 StorePlugin
2720
+ * @classdesc A powerful reactive state management plugin for Eleva that enables sharing
2721
+ * reactive data across the entire application. The Store plugin provides a centralized,
2722
+ * reactive data store that can be accessed from any component's setup function.
2723
+ *
2724
+ * Core Features:
2725
+ * - Centralized reactive state management using Eleva's signal system
2726
+ * - Global state accessibility through component setup functions
2727
+ * - Namespace support for organizing store modules
2728
+ * - Built-in persistence with localStorage/sessionStorage support
2729
+ * - Action-based state mutations with validation
2730
+ * - Subscription system for reactive updates
2731
+ * - DevTools integration for debugging
2732
+ * - Plugin architecture for extensibility
2733
+ *
2734
+ * @example
2735
+ * // Install the plugin
2736
+ * const app = new Eleva("myApp");
2737
+ * app.use(StorePlugin, {
2738
+ * state: {
2739
+ * user: { name: "John", email: "john@example.com" },
2740
+ * counter: 0,
2741
+ * todos: []
2742
+ * },
2743
+ * actions: {
2744
+ * increment: (state) => state.counter.value++,
2745
+ * addTodo: (state, todo) => state.todos.value.push(todo),
2746
+ * setUser: (state, user) => state.user.value = user
2747
+ * },
2748
+ * persistence: {
2749
+ * enabled: true,
2750
+ * key: "myApp-store",
2751
+ * storage: "localStorage"
2752
+ * }
2753
+ * });
2754
+ *
2755
+ * // Use store in components
2756
+ * app.component("Counter", {
2757
+ * setup({ store }) {
2758
+ * return {
2759
+ * count: store.state.counter,
2760
+ * increment: () => store.dispatch("increment"),
2761
+ * user: store.state.user
2762
+ * };
2763
+ * },
2764
+ * template: (ctx) => `
2765
+ * <div>
2766
+ * <p>Hello ${ctx.user.value.name}!</p>
2767
+ * <p>Count: ${ctx.count.value}</p>
2768
+ * <button onclick="ctx.increment()">+</button>
2769
+ * </div>
2770
+ * `
2771
+ * });
2772
+ */
2773
+ const StorePlugin = {
2774
+ /**
2775
+ * Unique identifier for the plugin
2776
+ * @type {string}
2777
+ */
2778
+ name: "store",
2779
+ /**
2780
+ * Plugin version
2781
+ * @type {string}
2782
+ */
2783
+ version: "1.0.0-rc.11",
2784
+ /**
2785
+ * Plugin description
2786
+ * @type {string}
2787
+ */
2788
+ description: "Reactive state management for sharing data across the entire Eleva application",
2789
+ /**
2790
+ * Installs the plugin into the Eleva instance
2791
+ *
2792
+ * @param {Object} eleva - The Eleva instance
2793
+ * @param {Object} options - Plugin configuration options
2794
+ * @param {Object} [options.state={}] - Initial state object
2795
+ * @param {Object} [options.actions={}] - Action functions for state mutations
2796
+ * @param {Object} [options.namespaces={}] - Namespaced modules for organizing store
2797
+ * @param {Object} [options.persistence] - Persistence configuration
2798
+ * @param {boolean} [options.persistence.enabled=false] - Enable state persistence
2799
+ * @param {string} [options.persistence.key="eleva-store"] - Storage key
2800
+ * @param {"localStorage" | "sessionStorage"} [options.persistence.storage="localStorage"] - Storage type
2801
+ * @param {Array<string>} [options.persistence.include] - State keys to persist (if not provided, all state is persisted)
2802
+ * @param {Array<string>} [options.persistence.exclude] - State keys to exclude from persistence
2803
+ * @param {boolean} [options.devTools=false] - Enable development tools integration
2804
+ * @param {Function} [options.onError=null] - Error handler function
2805
+ *
2806
+ * @example
2807
+ * // Basic installation
2808
+ * app.use(StorePlugin, {
2809
+ * state: { count: 0, user: null },
2810
+ * actions: {
2811
+ * increment: (state) => state.count.value++,
2812
+ * setUser: (state, user) => state.user.value = user
2813
+ * }
2814
+ * });
2815
+ *
2816
+ * // Advanced installation with persistence and namespaces
2817
+ * app.use(StorePlugin, {
2818
+ * state: { theme: "light" },
2819
+ * namespaces: {
2820
+ * auth: {
2821
+ * state: { user: null, token: null },
2822
+ * actions: {
2823
+ * login: (state, { user, token }) => {
2824
+ * state.user.value = user;
2825
+ * state.token.value = token;
2826
+ * },
2827
+ * logout: (state) => {
2828
+ * state.user.value = null;
2829
+ * state.token.value = null;
2830
+ * }
2831
+ * }
2832
+ * }
2833
+ * },
2834
+ * persistence: {
2835
+ * enabled: true,
2836
+ * include: ["theme", "auth.user"]
2837
+ * }
2838
+ * });
2839
+ */
2840
+ install(eleva, options = {}) {
2841
+ const {
2842
+ state = {},
2843
+ actions = {},
2844
+ namespaces = {},
2845
+ persistence = {},
2846
+ devTools = false,
2847
+ onError = null
2848
+ } = options;
2849
+
2850
+ /**
2851
+ * Store instance that manages all state and provides the API
2852
+ * @private
2853
+ */
2854
+ class Store {
2855
+ constructor() {
2856
+ this.state = {};
2857
+ this.actions = {};
2858
+ this.subscribers = new Set();
2859
+ this.mutations = [];
2860
+ this.persistence = {
2861
+ enabled: false,
2862
+ key: "eleva-store",
2863
+ storage: "localStorage",
2864
+ include: null,
2865
+ exclude: null,
2866
+ ...persistence
2867
+ };
2868
+ this.devTools = devTools;
2869
+ this.onError = onError;
2870
+ this._initializeState(state, actions);
2871
+ this._initializeNamespaces(namespaces);
2872
+ this._loadPersistedState();
2873
+ this._setupDevTools();
2874
+ }
2875
+
2876
+ /**
2877
+ * Initializes the root state and actions
2878
+ * @private
2879
+ */
2880
+ _initializeState(initialState, initialActions) {
2881
+ // Create reactive signals for each state property
2882
+ Object.entries(initialState).forEach(([key, value]) => {
2883
+ this.state[key] = new eleva.signal(value);
2884
+ });
2885
+
2886
+ // Set up actions
2887
+ this.actions = {
2888
+ ...initialActions
2889
+ };
2890
+ }
2891
+
2892
+ /**
2893
+ * Initializes namespaced modules
2894
+ * @private
2895
+ */
2896
+ _initializeNamespaces(namespaces) {
2897
+ Object.entries(namespaces).forEach(([namespace, module]) => {
2898
+ const {
2899
+ state: moduleState = {},
2900
+ actions: moduleActions = {}
2901
+ } = module;
2902
+
2903
+ // Create namespace object if it doesn't exist
2904
+ if (!this.state[namespace]) {
2905
+ this.state[namespace] = {};
2906
+ }
2907
+ if (!this.actions[namespace]) {
2908
+ this.actions[namespace] = {};
2909
+ }
2910
+
2911
+ // Initialize namespaced state
2912
+ Object.entries(moduleState).forEach(([key, value]) => {
2913
+ this.state[namespace][key] = new eleva.signal(value);
2914
+ });
2915
+
2916
+ // Set up namespaced actions
2917
+ this.actions[namespace] = {
2918
+ ...moduleActions
2919
+ };
2920
+ });
2921
+ }
2922
+
2923
+ /**
2924
+ * Loads persisted state from storage
2925
+ * @private
2926
+ */
2927
+ _loadPersistedState() {
2928
+ if (!this.persistence.enabled || typeof window === "undefined") {
2929
+ return;
2930
+ }
2931
+ try {
2932
+ const storage = window[this.persistence.storage];
2933
+ const persistedData = storage.getItem(this.persistence.key);
2934
+ if (persistedData) {
2935
+ const data = JSON.parse(persistedData);
2936
+ this._applyPersistedData(data);
2937
+ }
2938
+ } catch (error) {
2939
+ if (this.onError) {
2940
+ this.onError(error, "Failed to load persisted state");
2941
+ } else {
2942
+ console.warn("[StorePlugin] Failed to load persisted state:", error);
2943
+ }
2944
+ }
2945
+ }
2946
+
2947
+ /**
2948
+ * Applies persisted data to the current state
2949
+ * @private
2950
+ */
2951
+ _applyPersistedData(data, currentState = this.state, path = "") {
2952
+ Object.entries(data).forEach(([key, value]) => {
2953
+ const fullPath = path ? `${path}.${key}` : key;
2954
+ if (this._shouldPersist(fullPath)) {
2955
+ if (currentState[key] && typeof currentState[key] === "object" && "value" in currentState[key]) {
2956
+ // This is a signal, update its value
2957
+ currentState[key].value = value;
2958
+ } else if (typeof value === "object" && value !== null && currentState[key]) {
2959
+ // This is a nested object, recurse
2960
+ this._applyPersistedData(value, currentState[key], fullPath);
2961
+ }
2962
+ }
2963
+ });
2964
+ }
2965
+
2966
+ /**
2967
+ * Determines if a state path should be persisted
2968
+ * @private
2969
+ */
2970
+ _shouldPersist(path) {
2971
+ const {
2972
+ include,
2973
+ exclude
2974
+ } = this.persistence;
2975
+ if (include && include.length > 0) {
2976
+ return include.some(includePath => path.startsWith(includePath));
2977
+ }
2978
+ if (exclude && exclude.length > 0) {
2979
+ return !exclude.some(excludePath => path.startsWith(excludePath));
2980
+ }
2981
+ return true;
2982
+ }
2983
+
2984
+ /**
2985
+ * Saves current state to storage
2986
+ * @private
2987
+ */
2988
+ _saveState() {
2989
+ if (!this.persistence.enabled || typeof window === "undefined") {
2990
+ return;
2991
+ }
2992
+ try {
2993
+ const storage = window[this.persistence.storage];
2994
+ const dataToSave = this._extractPersistedData();
2995
+ storage.setItem(this.persistence.key, JSON.stringify(dataToSave));
2996
+ } catch (error) {
2997
+ if (this.onError) {
2998
+ this.onError(error, "Failed to save state");
2999
+ } else {
3000
+ console.warn("[StorePlugin] Failed to save state:", error);
3001
+ }
3002
+ }
3003
+ }
3004
+
3005
+ /**
3006
+ * Extracts data that should be persisted
3007
+ * @private
3008
+ */
3009
+ _extractPersistedData(currentState = this.state, path = "") {
3010
+ const result = {};
3011
+ Object.entries(currentState).forEach(([key, value]) => {
3012
+ const fullPath = path ? `${path}.${key}` : key;
3013
+ if (this._shouldPersist(fullPath)) {
3014
+ if (value && typeof value === "object" && "value" in value) {
3015
+ // This is a signal, extract its value
3016
+ result[key] = value.value;
3017
+ } else if (typeof value === "object" && value !== null) {
3018
+ // This is a nested object, recurse
3019
+ const nestedData = this._extractPersistedData(value, fullPath);
3020
+ if (Object.keys(nestedData).length > 0) {
3021
+ result[key] = nestedData;
3022
+ }
3023
+ }
3024
+ }
3025
+ });
3026
+ return result;
3027
+ }
3028
+
3029
+ /**
3030
+ * Sets up development tools integration
3031
+ * @private
3032
+ */
3033
+ _setupDevTools() {
3034
+ if (!this.devTools || typeof window === "undefined" || !window.__ELEVA_DEVTOOLS__) {
3035
+ return;
3036
+ }
3037
+ window.__ELEVA_DEVTOOLS__.registerStore(this);
3038
+ }
3039
+
3040
+ /**
3041
+ * Dispatches an action to mutate the state
3042
+ * @param {string} actionName - The name of the action to dispatch (supports namespaced actions like "auth.login")
3043
+ * @param {any} payload - The payload to pass to the action
3044
+ * @returns {Promise<any>} The result of the action
3045
+ */
3046
+ async dispatch(actionName, payload) {
3047
+ try {
3048
+ const action = this._getAction(actionName);
3049
+ if (!action) {
3050
+ const error = new Error(`Action "${actionName}" not found`);
3051
+ if (this.onError) {
3052
+ this.onError(error, actionName);
3053
+ }
3054
+ throw error;
3055
+ }
3056
+ const mutation = {
3057
+ type: actionName,
3058
+ payload,
3059
+ timestamp: Date.now()
3060
+ };
3061
+
3062
+ // Record mutation for devtools
3063
+ this.mutations.push(mutation);
3064
+ if (this.mutations.length > 100) {
3065
+ this.mutations.shift(); // Keep only last 100 mutations
3066
+ }
3067
+
3068
+ // Execute the action
3069
+ const result = await action.call(null, this.state, payload);
3070
+
3071
+ // Save state if persistence is enabled
3072
+ this._saveState();
3073
+
3074
+ // Notify subscribers
3075
+ this.subscribers.forEach(callback => {
3076
+ try {
3077
+ callback(mutation, this.state);
3078
+ } catch (error) {
3079
+ if (this.onError) {
3080
+ this.onError(error, "Subscriber callback failed");
3081
+ }
3082
+ }
3083
+ });
3084
+
3085
+ // Notify devtools
3086
+ if (this.devTools && typeof window !== "undefined" && window.__ELEVA_DEVTOOLS__) {
3087
+ window.__ELEVA_DEVTOOLS__.notifyMutation(mutation, this.state);
3088
+ }
3089
+ return result;
3090
+ } catch (error) {
3091
+ if (this.onError) {
3092
+ this.onError(error, `Action dispatch failed: ${actionName}`);
3093
+ }
3094
+ throw error;
3095
+ }
3096
+ }
3097
+
3098
+ /**
3099
+ * Gets an action by name (supports namespaced actions)
3100
+ * @private
3101
+ */
3102
+ _getAction(actionName) {
3103
+ const parts = actionName.split(".");
3104
+ let current = this.actions;
3105
+ for (const part of parts) {
3106
+ if (current[part] === undefined) {
3107
+ return null;
3108
+ }
3109
+ current = current[part];
3110
+ }
3111
+ return typeof current === "function" ? current : null;
3112
+ }
3113
+
3114
+ /**
3115
+ * Subscribes to store mutations
3116
+ * @param {Function} callback - Callback function to call on mutations
3117
+ * @returns {Function} Unsubscribe function
3118
+ */
3119
+ subscribe(callback) {
3120
+ if (typeof callback !== "function") {
3121
+ throw new Error("Subscribe callback must be a function");
3122
+ }
3123
+ this.subscribers.add(callback);
3124
+
3125
+ // Return unsubscribe function
3126
+ return () => {
3127
+ this.subscribers.delete(callback);
3128
+ };
3129
+ }
3130
+
3131
+ /**
3132
+ * Gets a deep copy of the current state values (not signals)
3133
+ * @returns {Object} The current state values
3134
+ */
3135
+ getState() {
3136
+ return this._extractPersistedData();
3137
+ }
3138
+
3139
+ /**
3140
+ * Replaces the entire state (useful for testing or state hydration)
3141
+ * @param {Object} newState - The new state object
3142
+ */
3143
+ replaceState(newState) {
3144
+ this._applyPersistedData(newState);
3145
+ this._saveState();
3146
+ }
3147
+
3148
+ /**
3149
+ * Clears persisted state from storage
3150
+ */
3151
+ clearPersistedState() {
3152
+ if (!this.persistence.enabled || typeof window === "undefined") {
3153
+ return;
3154
+ }
3155
+ try {
3156
+ const storage = window[this.persistence.storage];
3157
+ storage.removeItem(this.persistence.key);
3158
+ } catch (error) {
3159
+ if (this.onError) {
3160
+ this.onError(error, "Failed to clear persisted state");
3161
+ }
3162
+ }
3163
+ }
3164
+
3165
+ /**
3166
+ * Registers a new namespaced module at runtime
3167
+ * @param {string} namespace - The namespace for the module
3168
+ * @param {Object} module - The module definition
3169
+ * @param {Object} module.state - The module's initial state
3170
+ * @param {Object} module.actions - The module's actions
3171
+ */
3172
+ registerModule(namespace, module) {
3173
+ if (this.state[namespace] || this.actions[namespace]) {
3174
+ console.warn(`[StorePlugin] Module "${namespace}" already exists`);
3175
+ return;
3176
+ }
3177
+
3178
+ // Initialize the module
3179
+ this.state[namespace] = {};
3180
+ this.actions[namespace] = {};
3181
+ const namespaces = {
3182
+ [namespace]: module
3183
+ };
3184
+ this._initializeNamespaces(namespaces);
3185
+ this._saveState();
3186
+ }
3187
+
3188
+ /**
3189
+ * Unregisters a namespaced module
3190
+ * @param {string} namespace - The namespace to unregister
3191
+ */
3192
+ unregisterModule(namespace) {
3193
+ if (!this.state[namespace] && !this.actions[namespace]) {
3194
+ console.warn(`[StorePlugin] Module "${namespace}" does not exist`);
3195
+ return;
3196
+ }
3197
+ delete this.state[namespace];
3198
+ delete this.actions[namespace];
3199
+ this._saveState();
3200
+ }
3201
+
3202
+ /**
3203
+ * Creates a new reactive state property at runtime
3204
+ * @param {string} key - The state key
3205
+ * @param {*} initialValue - The initial value
3206
+ * @returns {Object} The created signal
3207
+ */
3208
+ createState(key, initialValue) {
3209
+ if (this.state[key]) {
3210
+ return this.state[key]; // Return existing state
3211
+ }
3212
+ this.state[key] = new eleva.signal(initialValue);
3213
+ this._saveState();
3214
+ return this.state[key];
3215
+ }
3216
+
3217
+ /**
3218
+ * Creates a new action at runtime
3219
+ * @param {string} name - The action name
3220
+ * @param {Function} actionFn - The action function
3221
+ */
3222
+ createAction(name, actionFn) {
3223
+ if (typeof actionFn !== "function") {
3224
+ throw new Error("Action must be a function");
3225
+ }
3226
+ this.actions[name] = actionFn;
3227
+ }
3228
+ }
3229
+
3230
+ // Create the store instance
3231
+ const store = new Store();
3232
+
3233
+ // Store the original mount method to override it
3234
+ const originalMount = eleva.mount;
3235
+
3236
+ /**
3237
+ * Override the mount method to inject store context into components
3238
+ */
3239
+ eleva.mount = async (container, compName, props = {}) => {
3240
+ // Get the component definition
3241
+ const componentDef = typeof compName === "string" ? eleva._components.get(compName) || compName : compName;
3242
+ if (!componentDef) {
3243
+ return await originalMount.call(eleva, container, compName, props);
3244
+ }
3245
+
3246
+ // Create a wrapped component that injects store into setup
3247
+ const wrappedComponent = {
3248
+ ...componentDef,
3249
+ async setup(ctx) {
3250
+ // Inject store into the context with enhanced API
3251
+ ctx.store = {
3252
+ // Core store functionality
3253
+ state: store.state,
3254
+ dispatch: store.dispatch.bind(store),
3255
+ subscribe: store.subscribe.bind(store),
3256
+ getState: store.getState.bind(store),
3257
+ // Module management
3258
+ registerModule: store.registerModule.bind(store),
3259
+ unregisterModule: store.unregisterModule.bind(store),
3260
+ // Utilities for dynamic state/action creation
3261
+ createState: store.createState.bind(store),
3262
+ createAction: store.createAction.bind(store),
3263
+ // Access to signal constructor for manual state creation
3264
+ signal: eleva.signal
3265
+ };
3266
+
3267
+ // Call original setup if it exists
3268
+ const originalSetup = componentDef.setup;
3269
+ const result = originalSetup ? await originalSetup(ctx) : {};
3270
+ return result;
3271
+ }
3272
+ };
3273
+
3274
+ // Call original mount with wrapped component
3275
+ return await originalMount.call(eleva, container, wrappedComponent, props);
3276
+ };
3277
+
3278
+ // Override _mountComponents to ensure child components also get store context
3279
+ const originalMountComponents = eleva._mountComponents;
3280
+ eleva._mountComponents = async (container, children, childInstances) => {
3281
+ // Create wrapped children with store injection
3282
+ const wrappedChildren = {};
3283
+ for (const [selector, childComponent] of Object.entries(children)) {
3284
+ const componentDef = typeof childComponent === "string" ? eleva._components.get(childComponent) || childComponent : childComponent;
3285
+ if (componentDef && typeof componentDef === "object") {
3286
+ wrappedChildren[selector] = {
3287
+ ...componentDef,
3288
+ async setup(ctx) {
3289
+ // Inject store into the context with enhanced API
3290
+ ctx.store = {
3291
+ // Core store functionality
3292
+ state: store.state,
3293
+ dispatch: store.dispatch.bind(store),
3294
+ subscribe: store.subscribe.bind(store),
3295
+ getState: store.getState.bind(store),
3296
+ // Module management
3297
+ registerModule: store.registerModule.bind(store),
3298
+ unregisterModule: store.unregisterModule.bind(store),
3299
+ // Utilities for dynamic state/action creation
3300
+ createState: store.createState.bind(store),
3301
+ createAction: store.createAction.bind(store),
3302
+ // Access to signal constructor for manual state creation
3303
+ signal: eleva.signal
3304
+ };
3305
+
3306
+ // Call original setup if it exists
3307
+ const originalSetup = componentDef.setup;
3308
+ const result = originalSetup ? await originalSetup(ctx) : {};
3309
+ return result;
3310
+ }
3311
+ };
3312
+ } else {
3313
+ wrappedChildren[selector] = childComponent;
3314
+ }
3315
+ }
3316
+
3317
+ // Call original _mountComponents with wrapped children
3318
+ return await originalMountComponents.call(eleva, container, wrappedChildren, childInstances);
3319
+ };
3320
+
3321
+ // Expose store instance and utilities on the Eleva instance
3322
+ eleva.store = store;
3323
+
3324
+ /**
3325
+ * Expose utility methods on the Eleva instance
3326
+ * @namespace eleva.store
3327
+ */
3328
+ eleva.createAction = (name, actionFn) => {
3329
+ store.actions[name] = actionFn;
3330
+ };
3331
+ eleva.dispatch = (actionName, payload) => {
3332
+ return store.dispatch(actionName, payload);
3333
+ };
3334
+ eleva.getState = () => {
3335
+ return store.getState();
3336
+ };
3337
+ eleva.subscribe = callback => {
3338
+ return store.subscribe(callback);
3339
+ };
3340
+
3341
+ // Store original methods for cleanup
3342
+ eleva._originalMount = originalMount;
3343
+ eleva._originalMountComponents = originalMountComponents;
3344
+ },
3345
+ /**
3346
+ * Uninstalls the plugin from the Eleva instance
3347
+ *
3348
+ * @param {Object} eleva - The Eleva instance
3349
+ *
3350
+ * @description
3351
+ * Restores the original Eleva methods and removes all plugin-specific
3352
+ * functionality. This method should be called when the plugin is no
3353
+ * longer needed.
3354
+ *
3355
+ * @example
3356
+ * // Uninstall the plugin
3357
+ * StorePlugin.uninstall(app);
3358
+ */
3359
+ uninstall(eleva) {
3360
+ // Restore original mount method
3361
+ if (eleva._originalMount) {
3362
+ eleva.mount = eleva._originalMount;
3363
+ delete eleva._originalMount;
3364
+ }
3365
+
3366
+ // Restore original _mountComponents method
3367
+ if (eleva._originalMountComponents) {
3368
+ eleva._mountComponents = eleva._originalMountComponents;
3369
+ delete eleva._originalMountComponents;
3370
+ }
3371
+
3372
+ // Remove store instance and utility methods
3373
+ if (eleva.store) {
3374
+ delete eleva.store;
3375
+ }
3376
+ if (eleva.createAction) {
3377
+ delete eleva.createAction;
3378
+ }
3379
+ if (eleva.dispatch) {
3380
+ delete eleva.dispatch;
3381
+ }
3382
+ if (eleva.getState) {
3383
+ delete eleva.getState;
3384
+ }
3385
+ if (eleva.subscribe) {
3386
+ delete eleva.subscribe;
3387
+ }
3388
+ }
3389
+ };
3390
+
3391
+ export { AttrPlugin as Attr, PropsPlugin as Props, RouterPlugin as Router, StorePlugin as Store };
3392
+ //# sourceMappingURL=eleva-plugins.esm.js.map