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