eleva 1.0.0-alpha → 1.0.0-rc.10

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