slicejs-web-framework 2.3.0 → 2.3.2

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.
@@ -1,4 +1,46 @@
1
+ /**
2
+ * @typedef {Object} RouteConfig
3
+ * @property {string} path
4
+ * @property {string} component
5
+ * @property {RouteConfig[]} [children]
6
+ * @property {Object} [metadata]
7
+ * @property {string} [fullPath]
8
+ * @property {string|null} [parentPath]
9
+ * @property {RouteConfig|null} [parentRoute]
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} RouteInfo
14
+ * @property {string} path
15
+ * @property {string} component
16
+ * @property {Object} params
17
+ * @property {Object} query
18
+ * @property {Object} metadata
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} GuardRedirect
23
+ * @property {string} path
24
+ * @property {boolean} [replace]
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} RouteMatch
29
+ * @property {RouteConfig|null} route
30
+ * @property {Object} params
31
+ * @property {RouteConfig} [childRoute]
32
+ */
33
+
34
+ /**
35
+ * @callback RouterNext
36
+ * @param {void|false|string|{ path: string, replace?: boolean }} [arg]
37
+ * @returns {void}
38
+ */
39
+
1
40
  export default class Router {
41
+ /**
42
+ * @param {RouteConfig[]} routes
43
+ */
2
44
  constructor(routes) {
3
45
  this.routes = routes;
4
46
  this.activeRoute = null;
@@ -23,7 +65,8 @@ export default class Router {
23
65
 
24
66
  /**
25
67
  * Inicializa el router
26
- * Si el usuario no llama start() manualmente, se auto-inicia después de un delay
68
+ * Si el usuario no llama start() manualmente, se auto-inicia despues de un delay
69
+ * @returns {void}
27
70
  */
28
71
  init() {
29
72
  window.addEventListener('popstate', this.onRouteChange.bind(this));
@@ -41,7 +84,8 @@ export default class Router {
41
84
  /**
42
85
  * Inicia el router y carga la ruta inicial
43
86
  * OPCIONAL: Solo necesario si usas guards (beforeEach/afterEach)
44
- * Si no lo llamas, el router se auto-inicia después de 50ms
87
+ * Si no lo llamas, el router se auto-inicia despues de 50ms
88
+ * @returns {Promise<void>}
45
89
  */
46
90
  async start() {
47
91
  // Prevenir múltiples llamadas
@@ -65,9 +109,10 @@ export default class Router {
65
109
  // ============================================
66
110
 
67
111
  /**
68
- * Registra un guard que se ejecuta ANTES de cada navegación
69
- * Puede bloquear o redirigir la navegación
70
- * @param {Function} guard - Función (to, from, next) => {}
112
+ * Registra un guard que se ejecuta ANTES de cada navegacion.
113
+ * Puede bloquear o redirigir la navegacion mediante next().
114
+ * @param {(to: RouteInfo, from: RouteInfo, next: RouterNext) => void|Promise<void>} guard
115
+ * @returns {void}
71
116
  */
72
117
  beforeEach(guard) {
73
118
  if (typeof guard !== 'function') {
@@ -78,9 +123,10 @@ export default class Router {
78
123
  }
79
124
 
80
125
  /**
81
- * Registra un guard que se ejecuta DESPUÉS de cada navegación
82
- * No puede bloquear la navegación
83
- * @param {Function} guard - Función (to, from) => {}
126
+ * Registra un guard que se ejecuta DESPUES de cada navegacion.
127
+ * No puede bloquear la navegacion.
128
+ * @param {(to: RouteInfo, from: RouteInfo) => void} guard
129
+ * @returns {void}
84
130
  */
85
131
  afterEach(guard) {
86
132
  if (typeof guard !== 'function') {
@@ -97,6 +143,13 @@ export default class Router {
97
143
  * @param {String} requestedPath - Path original solicitado
98
144
  * @returns {Object} Objeto con path, component, params, query, metadata
99
145
  */
146
+ /**
147
+ * Build route info used by guards and events.
148
+ * @param {RouteConfig|null} route
149
+ * @param {Object} [params]
150
+ * @param {string|null} [requestedPath]
151
+ * @returns {RouteInfo}
152
+ */
100
153
  _createRouteInfo(route, params = {}, requestedPath = null) {
101
154
  if (!route) {
102
155
  return {
@@ -121,6 +174,10 @@ export default class Router {
121
174
  * Parsea los query parameters de la URL actual
122
175
  * @returns {Object} Objeto con los query params
123
176
  */
177
+ /**
178
+ * Parse query params from current URL.
179
+ * @returns {Object}
180
+ */
124
181
  _parseQueryParams() {
125
182
  const queryString = window.location.search;
126
183
  if (!queryString) return {};
@@ -141,6 +198,12 @@ export default class Router {
141
198
  * @param {Object} from - Información de ruta origen
142
199
  * @returns {Object|null} Objeto con redirectPath y options, o null si continúa
143
200
  */
201
+ /**
202
+ * Execute beforeEach guard if defined.
203
+ * @param {RouteInfo} to
204
+ * @param {RouteInfo} from
205
+ * @returns {Promise<{ path: string|false, options: { replace?: boolean } }|null>}
206
+ */
144
207
  async _executeBeforeEachGuard(to, from) {
145
208
  if (!this._beforeEachGuard) {
146
209
  return null;
@@ -212,6 +275,12 @@ export default class Router {
212
275
  * @param {Object} to - Información de ruta destino
213
276
  * @param {Object} from - Información de ruta origen
214
277
  */
278
+ /**
279
+ * Execute afterEach guard if defined.
280
+ * @param {RouteInfo} to
281
+ * @param {RouteInfo} from
282
+ * @returns {void}
283
+ */
215
284
  _executeAfterEachGuard(to, from) {
216
285
  if (!this._afterEachGuard) {
217
286
  return;
@@ -228,6 +297,13 @@ export default class Router {
228
297
  // ROUTING CORE (MODIFICADO CON GUARDS)
229
298
  // ============================================
230
299
 
300
+ /**
301
+ * Navigate to a route path with guards support. Add replace to do router.replace() instead of push.
302
+ * @param {string} path
303
+ * @param {string[]} [_redirectChain]
304
+ * @param {{ replace?: boolean }} [_options]
305
+ * @returns {Promise<void>}
306
+ */
231
307
  async navigate(path, _redirectChain = [], _options = {}) {
232
308
  const currentPath = window.location.pathname;
233
309
 
@@ -282,6 +358,12 @@ export default class Router {
282
358
  * @param {Object} to - Información de ruta destino
283
359
  * @param {Object} from - Información de ruta origen
284
360
  */
361
+ /**
362
+ * Perform navigation after guards.
363
+ * @param {RouteInfo} to
364
+ * @param {RouteInfo} from
365
+ * @returns {Promise<void>}
366
+ */
285
367
  async _performNavigation(to, from) {
286
368
  // Renderizar la nueva ruta
287
369
  await this.onRouteChange();
@@ -293,6 +375,10 @@ export default class Router {
293
375
  this._emitRouteChange(to, from);
294
376
  }
295
377
 
378
+ /**
379
+ * React to URL changes and render routes.
380
+ * @returns {Promise<void>}
381
+ */
296
382
  async onRouteChange() {
297
383
  // Cancelar el timeout anterior si existe
298
384
  if (this.routeChangeTimeout) {
@@ -315,6 +401,12 @@ export default class Router {
315
401
  }, 10);
316
402
  }
317
403
 
404
+ /**
405
+ * Build or update the active route component.
406
+ * @param {RouteConfig} route
407
+ * @param {Object} params
408
+ * @returns {Promise<void>}
409
+ */
318
410
  async handleRoute(route, params) {
319
411
  const targetElement = document.querySelector('#app');
320
412
 
@@ -357,6 +449,10 @@ export default class Router {
357
449
  slice.router.activeRoute = route;
358
450
  }
359
451
 
452
+ /**
453
+ * Load initial route and run guards.
454
+ * @returns {Promise<void>}
455
+ */
360
456
  async loadInitialRoute() {
361
457
  const path = window.location.pathname;
362
458
  const { route, params } = this.matchRoute(path);
@@ -392,6 +488,12 @@ export default class Router {
392
488
  * @param {Object} to
393
489
  * @param {Object} from
394
490
  */
491
+ /**
492
+ * Emit route change event.
493
+ * @param {RouteInfo} to
494
+ * @param {RouteInfo} from
495
+ * @returns {void}
496
+ */
395
497
  _emitRouteChange(to, from) {
396
498
  const payload = { to, from };
397
499
 
@@ -475,6 +577,11 @@ export default class Router {
475
577
  return pathToRouteMap;
476
578
  }
477
579
 
580
+ /**
581
+ * Render any Route/MultiRoute components in a container.
582
+ * @param {Document|HTMLElement} [searchContainer]
583
+ * @returns {Promise<boolean>}
584
+ */
478
585
  async renderRoutesComponentsInPage(searchContainer = document) {
479
586
  let routerContainersFlag = false;
480
587
  const routeContainers = this.getCachedRouteContainers(searchContainer);
@@ -534,6 +641,11 @@ export default class Router {
534
641
  return routeContainers;
535
642
  }
536
643
 
644
+ /**
645
+ * Render route containers inside a component.
646
+ * @param {HTMLElement} component
647
+ * @returns {Promise<boolean>}
648
+ */
537
649
  async renderRoutesInComponent(component) {
538
650
  if (!component) {
539
651
  slice.logger.logWarning('Router', 'No component provided for route rendering');
@@ -543,6 +655,11 @@ export default class Router {
543
655
  return await this.renderRoutesComponentsInPage(component);
544
656
  }
545
657
 
658
+ /**
659
+ * Match a path to a configured route.
660
+ * @param {string} path
661
+ * @returns {RouteMatch}
662
+ */
546
663
  matchRoute(path) {
547
664
  const exactMatch = this.pathToRouteMap.get(path);
548
665
  if (exactMatch) {
@@ -583,6 +700,11 @@ export default class Router {
583
700
  return { route: notFoundRoute, params: {} };
584
701
  }
585
702
 
703
+ /**
704
+ * Compile a path pattern with ${param} segments.
705
+ * @param {string} pattern
706
+ * @returns {{ regex: RegExp, paramNames: string[] }}
707
+ */
586
708
  compilePathPattern(pattern) {
587
709
  const paramNames = [];
588
710
  const regexPattern =
@@ -1,49 +1,64 @@
1
-
2
- export default class StylesManager {
3
- constructor() {
4
- this.componentStyles = document.createElement('style');
5
- this.componentStyles.id = 'slice-component-styles';
6
- document.head.appendChild(this.componentStyles);
7
-
8
- }
9
-
10
- async init() {
11
- for (let i = 0; i < slice.stylesConfig.requestedStyles.length; i++) {
12
- const styles = await slice.controller.fetchText(slice.stylesConfig.requestedStyles[i], 'styles');
13
- this.componentStyles.innerText += styles;
14
- slice.logger.logInfo('StylesManager', `${slice.stylesConfig.requestedStyles[i]} styles loaded`);
15
- }
16
-
17
- if (slice.themeConfig.enabled) {
18
- const module = await import(`${slice.paths.structuralComponentFolderPath}/StylesManager/ThemeManager/ThemeManager.js`);
19
-
20
- this.themeManager = new module.default();
21
- let theme;
22
-
23
- if (slice.themeConfig.saveThemeLocally) {
24
- theme = localStorage.getItem('sliceTheme');
25
- }
26
-
27
- if (!theme) {
28
- theme = slice.themeConfig.defaultTheme;
29
- }
30
-
31
- if (slice.themeConfig.useBrowserTheme) {
32
- const browserTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'Dark' : 'Light';
33
- theme = browserTheme;
34
- }
35
-
36
- await this.themeManager.applyTheme(theme);
37
- }
38
- }
39
-
40
- //add a method that will add css as text to the componentStyles element
41
- appendComponentStyles(cssText) {
42
- this.componentStyles.appendChild(document.createTextNode(cssText));
43
- }
44
-
45
- registerComponentStyles(componentName, cssText) {
46
- slice.controller.requestedStyles.add(componentName);
47
- this.appendComponentStyles(cssText);
48
- }
49
- }
1
+
2
+ export default class StylesManager {
3
+ constructor() {
4
+ this.componentStyles = document.createElement('style');
5
+ this.componentStyles.id = 'slice-component-styles';
6
+ document.head.appendChild(this.componentStyles);
7
+
8
+ }
9
+
10
+ /**
11
+ * Load global styles and initialize ThemeManager if enabled.
12
+ * @returns {Promise<void>}
13
+ */
14
+ async init() {
15
+ for (let i = 0; i < slice.stylesConfig.requestedStyles.length; i++) {
16
+ const styles = await slice.controller.fetchText(slice.stylesConfig.requestedStyles[i], 'styles');
17
+ this.componentStyles.innerText += styles;
18
+ slice.logger.logInfo('StylesManager', `${slice.stylesConfig.requestedStyles[i]} styles loaded`);
19
+ }
20
+
21
+ if (slice.themeConfig.enabled) {
22
+ const module = await import(`${slice.paths.structuralComponentFolderPath}/StylesManager/ThemeManager/ThemeManager.js`);
23
+
24
+ this.themeManager = new module.default();
25
+ let theme;
26
+
27
+ if (slice.themeConfig.saveThemeLocally) {
28
+ theme = localStorage.getItem('sliceTheme');
29
+ }
30
+
31
+ if (!theme) {
32
+ theme = slice.themeConfig.defaultTheme;
33
+ }
34
+
35
+ if (slice.themeConfig.useBrowserTheme) {
36
+ const browserTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'Dark' : 'Light';
37
+ theme = browserTheme;
38
+ }
39
+
40
+ await this.themeManager.applyTheme(theme);
41
+ }
42
+ }
43
+
44
+ //add a method that will add css as text to the componentStyles element
45
+ /**
46
+ * Append raw CSS to the global component style tag.
47
+ * @param {string} cssText
48
+ * @returns {void}
49
+ */
50
+ appendComponentStyles(cssText) {
51
+ this.componentStyles.appendChild(document.createTextNode(cssText));
52
+ }
53
+
54
+ /**
55
+ * Register CSS for a component.
56
+ * @param {string} componentName
57
+ * @param {string} cssText
58
+ * @returns {void}
59
+ */
60
+ registerComponentStyles(componentName, cssText) {
61
+ slice.controller.requestedStyles.add(componentName);
62
+ this.appendComponentStyles(cssText);
63
+ }
64
+ }
@@ -1,56 +1,84 @@
1
- export default class ThemeManager {
2
- constructor() {
3
- this.themeStyles = new Map();
4
- this.currentTheme = null;
5
- this.themeStyle = document.createElement('style');
6
- document.head.appendChild(this.themeStyle);
7
- }
8
-
9
- async applyTheme(themeName) {
10
- if (!themeName) {
11
- slice.logger.logError('ThemeManager', 'Invalid theme name');
12
- return;
13
- }
14
-
15
- if (!this.themeStyles.has(themeName)) {
16
- await this.loadThemeCSS(themeName);
17
- } else {
18
- this.setThemeStyle(themeName);
19
- this.saveThemeLocally(themeName, this.themeStyles.get(themeName));
20
- }
21
- }
22
-
23
- async loadThemeCSS(themeName) {
24
- let themeContent =
25
- localStorage.getItem(`sliceTheme-${themeName}`) || (await slice.controller.fetchText(themeName, 'theme'));
26
-
27
- if (!themeContent) {
28
- slice.logger.logError('ThemeManager', `Failed to load theme: ${themeName}`);
29
- return;
30
- }
31
-
32
- this.themeStyles.set(themeName, themeContent);
33
- this.setThemeStyle(themeName);
34
- this.saveThemeLocally(themeName, themeContent);
35
- }
36
-
37
- saveThemeLocally(themeName, themeContent) {
38
- if (slice.themeConfig.saveThemeLocally) {
39
- localStorage.setItem('sliceTheme', themeName);
40
- localStorage.setItem(`sliceTheme-${themeName}`, themeContent);
41
- slice.logger.logInfo('ThemeManager', `Theme ${themeName} saved locally`);
42
- }
43
- }
44
-
45
- removeCurrentTheme() {
46
- if (this.currentTheme) {
47
- this.themeStyle.textContent = '';
48
- }
49
- }
50
-
51
- setThemeStyle(themeName) {
52
- this.themeStyle.textContent = this.themeStyles.get(themeName);
53
- this.currentTheme = themeName;
54
- slice.logger.logInfo('ThemeManager', `Theme ${themeName} applied`);
55
- }
56
- }
1
+ /**
2
+ * Manages theme CSS loading and persistence.
3
+ */
4
+ export default class ThemeManager {
5
+ constructor() {
6
+ this.themeStyles = new Map();
7
+ this.currentTheme = null;
8
+ this.themeStyle = document.createElement('style');
9
+ document.head.appendChild(this.themeStyle);
10
+ }
11
+
12
+ /**
13
+ * Apply a theme by name.
14
+ * @param {string} themeName
15
+ * @returns {Promise<void>}
16
+ */
17
+ async applyTheme(themeName) {
18
+ if (!themeName) {
19
+ slice.logger.logError('ThemeManager', 'Invalid theme name');
20
+ return;
21
+ }
22
+
23
+ if (!this.themeStyles.has(themeName)) {
24
+ await this.loadThemeCSS(themeName);
25
+ } else {
26
+ this.setThemeStyle(themeName);
27
+ this.saveThemeLocally(themeName, this.themeStyles.get(themeName));
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Load theme CSS and cache it.
33
+ * @param {string} themeName
34
+ * @returns {Promise<void>}
35
+ */
36
+ async loadThemeCSS(themeName) {
37
+ let themeContent =
38
+ localStorage.getItem(`sliceTheme-${themeName}`) || (await slice.controller.fetchText(themeName, 'theme'));
39
+
40
+ if (!themeContent) {
41
+ slice.logger.logError('ThemeManager', `Failed to load theme: ${themeName}`);
42
+ return;
43
+ }
44
+
45
+ this.themeStyles.set(themeName, themeContent);
46
+ this.setThemeStyle(themeName);
47
+ this.saveThemeLocally(themeName, themeContent);
48
+ }
49
+
50
+ /**
51
+ * Persist a theme in localStorage when enabled.
52
+ * @param {string} themeName
53
+ * @param {string} themeContent
54
+ * @returns {void}
55
+ */
56
+ saveThemeLocally(themeName, themeContent) {
57
+ if (slice.themeConfig.saveThemeLocally) {
58
+ localStorage.setItem('sliceTheme', themeName);
59
+ localStorage.setItem(`sliceTheme-${themeName}`, themeContent);
60
+ slice.logger.logInfo('ThemeManager', `Theme ${themeName} saved locally`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Clear currently applied theme styles.
66
+ * @returns {void}
67
+ */
68
+ removeCurrentTheme() {
69
+ if (this.currentTheme) {
70
+ this.themeStyle.textContent = '';
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Set theme style text and mark current theme.
76
+ * @param {string} themeName
77
+ * @returns {void}
78
+ */
79
+ setThemeStyle(themeName) {
80
+ this.themeStyle.textContent = this.themeStyles.get(themeName);
81
+ this.currentTheme = themeName;
82
+ slice.logger.logInfo('ThemeManager', `Theme ${themeName} applied`);
83
+ }
84
+ }
package/Slice/Slice.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import Controller from './Components/Structural/Controller/Controller.js';
2
2
  import StylesManager from './Components/Structural/StylesManager/StylesManager.js';
3
3
 
4
+ /**
5
+ * Main Slice.js runtime.
6
+ */
4
7
  export default class Slice {
8
+ /**
9
+ * @param {Object} sliceConfig
10
+ */
5
11
  constructor(sliceConfig) {
6
12
  this.controller = new Controller();
7
13
  this.stylesManager = new StylesManager();
@@ -16,6 +22,11 @@ export default class Slice {
16
22
  // 📦 Bundle system is initialized automatically via import in index.js
17
23
  }
18
24
 
25
+ /**
26
+ * Dynamically import a module and return its default export.
27
+ * @param {string} module
28
+ * @returns {Promise<any>}
29
+ */
19
30
  async getClass(module) {
20
31
  try {
21
32
  const { default: myClass } = await import(module);
@@ -25,14 +36,29 @@ export default class Slice {
25
36
  }
26
37
  }
27
38
 
39
+ /**
40
+ * Flag for production behavior (override in builds).
41
+ * @returns {boolean}
42
+ */
28
43
  isProduction() {
29
44
  return true;
30
45
  }
31
46
 
47
+ /**
48
+ * Get a component instance by sliceId.
49
+ * @param {string} componentSliceId
50
+ * @returns {HTMLElement|undefined}
51
+ */
32
52
  getComponent(componentSliceId) {
33
53
  return this.controller.activeComponents.get(componentSliceId);
34
54
  }
35
55
 
56
+ /**
57
+ * Build a component instance and run init.
58
+ * @param {string} componentName
59
+ * @param {Object} [props]
60
+ * @returns {Promise<HTMLElement|Object|null>}
61
+ */
36
62
  async build(componentName, props = {}) {
37
63
  if (!componentName) {
38
64
  this.logger.logError('Slice', null, `Component name is required to build a component`);
@@ -159,14 +185,28 @@ export default class Slice {
159
185
  }
160
186
  }
161
187
 
188
+ /**
189
+ * Apply a theme by name.
190
+ * @param {string} themeName
191
+ * @returns {Promise<void>}
192
+ */
162
193
  async setTheme(themeName) {
163
194
  await this.stylesManager.themeManager.applyTheme(themeName);
164
195
  }
165
196
 
197
+ /**
198
+ * Current theme name.
199
+ * @returns {string|null}
200
+ */
166
201
  get theme() {
167
202
  return this.stylesManager.themeManager.currentTheme;
168
203
  }
169
204
 
205
+ /**
206
+ * Attach HTML template to a component instance.
207
+ * @param {HTMLElement} componentInstance
208
+ * @returns {void}
209
+ */
170
210
  attachTemplate(componentInstance) {
171
211
  this.controller.loadTemplateToComponent(componentInstance);
172
212
  }
@@ -210,14 +250,32 @@ async function init() {
210
250
  }
211
251
 
212
252
  if (sliceConfig.debugger.enabled) {
213
- const DebuggerModule = await window.slice.getClass(
214
- `${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`
215
- );
216
- window.slice.debugger = new DebuggerModule();
217
- await window.slice.debugger.enableDebugMode();
218
- document.body.appendChild(window.slice.debugger);
253
+ const DebuggerModule = await window.slice.getClass(
254
+ `${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`
255
+ );
256
+ window.slice.debugger = new DebuggerModule();
257
+ await window.slice.debugger.enableDebugMode();
258
+ document.body.appendChild(window.slice.debugger);
219
259
  }
220
260
 
261
+ if (sliceConfig.events?.ui?.enabled) {
262
+ const EventsDebuggerModule = await window.slice.getClass(
263
+ `${slice.paths.structuralComponentFolderPath}/EventManager/EventManagerDebugger.js`
264
+ );
265
+ window.slice.eventsDebugger = new EventsDebuggerModule();
266
+ await window.slice.eventsDebugger.init();
267
+ document.body.appendChild(window.slice.eventsDebugger);
268
+ }
269
+
270
+ if (sliceConfig.context?.ui?.enabled) {
271
+ const ContextDebuggerModule = await window.slice.getClass(
272
+ `${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManagerDebugger.js`
273
+ );
274
+ window.slice.contextDebugger = new ContextDebuggerModule();
275
+ await window.slice.contextDebugger.init();
276
+ document.body.appendChild(window.slice.contextDebugger);
277
+ }
278
+
221
279
  if (sliceConfig.events?.enabled) {
222
280
  const EventManagerModule = await window.slice.getClass(
223
281
  `${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`
@@ -275,6 +333,34 @@ async function init() {
275
333
 
276
334
  await window.slice.stylesManager.init();
277
335
 
336
+ if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut) {
337
+ const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
338
+ const toKey = (event) => {
339
+ const parts = [];
340
+ if (event.ctrlKey) parts.push('ctrl');
341
+ if (event.shiftKey) parts.push('shift');
342
+ if (event.altKey) parts.push('alt');
343
+ if (event.metaKey) parts.push('meta');
344
+ const key = event.key?.toLowerCase();
345
+ if (key && !['control', 'shift', 'alt', 'meta'].includes(key)) {
346
+ parts.push(key);
347
+ }
348
+ return parts.join('+');
349
+ };
350
+
351
+ const handlers = {
352
+ [normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
353
+ [normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
354
+ };
355
+
356
+ document.addEventListener('keydown', (event) => {
357
+ const key = toKey(event);
358
+ if (!key || !handlers[key]) return;
359
+ event.preventDefault();
360
+ handlers[key]();
361
+ });
362
+ }
363
+
278
364
  const routesModule = await import(slice.paths.routesFile);
279
365
  const routes = routesModule.default;
280
366
  const RouterModule = await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slicejs-web-framework",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=20"