slicejs-web-framework 3.3.7 → 3.4.0

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.
package/Slice/Slice.js CHANGED
@@ -1,619 +1,633 @@
1
-
2
- /**
3
- * Main Slice.js runtime.
4
- */
5
- export default class Slice {
6
- /**
7
- * @param {Object} sliceConfig
8
- */
9
- constructor(sliceConfig, frameworkClasses = null) {
10
- this.frameworkClasses = frameworkClasses;
11
- const ControllerClass = frameworkClasses?.Controller;
12
- const StylesManagerClass = frameworkClasses?.StylesManager;
13
-
14
- this.controller = ControllerClass ? new ControllerClass() : null;
15
- this.stylesManager = StylesManagerClass ? new StylesManagerClass() : null;
16
- this.paths = sliceConfig.paths;
17
- this.themeConfig = sliceConfig.themeManager;
18
- this.stylesConfig = sliceConfig.stylesManager;
19
- this.loggerConfig = sliceConfig.logger;
20
- this.debuggerConfig = sliceConfig.debugger;
21
- this.loadingConfig = sliceConfig.loading;
22
- this.eventsConfig = sliceConfig.events;
23
-
24
- // Default to production until init() resolves the actual mode.
25
- // Safe to call isProduction() before init() completes.
26
- this._mode = 'production';
27
- this._publicEnv = {};
28
-
29
- // 📦 Bundle system is initialized automatically via import in index.js
30
- }
31
-
32
- /**
33
- * Dynamically import a module and return its default export.
34
- * @param {string} module
35
- * @returns {Promise<any>}
36
- */
37
- async getClass(module) {
38
- try {
39
- const { default: myClass } = await import(module);
40
- return await myClass;
41
- } catch (error) {
42
- this.logger.logError('Slice', `Error loading class ${module}`, error);
1
+
2
+ /**
3
+ * Main Slice.js runtime.
4
+ */
5
+ export default class Slice {
6
+ /**
7
+ * @param {Object} sliceConfig
8
+ */
9
+ constructor(sliceConfig, frameworkClasses = null) {
10
+ this.frameworkClasses = frameworkClasses;
11
+ const ControllerClass = frameworkClasses?.Controller;
12
+ const StylesManagerClass = frameworkClasses?.StylesManager;
13
+
14
+ this.controller = ControllerClass ? new ControllerClass() : null;
15
+ this.stylesManager = StylesManagerClass ? new StylesManagerClass() : null;
16
+ this.paths = sliceConfig.paths;
17
+ this.themeConfig = sliceConfig.themeManager;
18
+ this.stylesConfig = sliceConfig.stylesManager;
19
+ this.loggerConfig = sliceConfig.logger;
20
+ this.debuggerConfig = sliceConfig.debugger;
21
+ this.loadingConfig = sliceConfig.loading;
22
+ this.eventsConfig = sliceConfig.events;
23
+
24
+ // Default to production until init() resolves the actual mode.
25
+ // Safe to call isProduction() before init() completes.
26
+ this._mode = 'production';
27
+ this._publicEnv = {};
28
+
29
+ // 📦 Bundle system is initialized automatically via import in index.js
30
+ }
31
+
32
+ /**
33
+ * Dynamically import a module and return its default export.
34
+ * @param {string} module
35
+ * @returns {Promise<any>}
36
+ */
37
+ async getClass(module) {
38
+ try {
39
+ const { default: myClass } = await import(module);
40
+ return await myClass;
41
+ } catch (error) {
42
+ this.logger.error('Slice', `Error loading class ${module}`, error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Returns true when running in production mode.
49
+ * Reliable after init() has completed.
50
+ * @returns {boolean}
51
+ */
52
+ isProduction() {
53
+ return this._mode === 'production';
54
+ }
55
+
56
+ setPublicEnv(envPayload = {}) {
57
+ const normalized = {};
58
+
59
+ for (const [key, value] of Object.entries(envPayload || {})) {
60
+ if (!key.startsWith('SLICE_PUBLIC_')) continue;
61
+ normalized[key] = String(value ?? '');
62
+ }
63
+
64
+ this._publicEnv = normalized;
65
+ }
66
+
67
+ getEnv(name, fallbackValue = undefined) {
68
+ if (!name || typeof name !== 'string') {
69
+ return fallbackValue;
70
+ }
71
+
72
+ if (Object.prototype.hasOwnProperty.call(this._publicEnv, name)) {
73
+ return this._publicEnv[name];
74
+ }
75
+
76
+ return fallbackValue;
77
+ }
78
+
79
+ getPublicEnv() {
80
+ return { ...this._publicEnv };
81
+ }
82
+
83
+ /**
84
+ * Typed accessors over the public env, so callers stop re-parsing strings.
85
+ * slice.env.get('SLICE_PUBLIC_API_URL', '')
86
+ * slice.env.bool('SLICE_PUBLIC_AUTH_ENABLED') // '1'|'true'|'yes'|'on' → true
87
+ * slice.env.int('SLICE_PUBLIC_TIMEOUT', 5000)
88
+ * slice.env.list('SLICE_PUBLIC_MODELS') // 'a, b' → ['a','b']
89
+ * slice.env.has('X') / slice.env.all()
90
+ * @returns {{ get: Function, has: Function, all: Function, bool: Function, int: Function, list: Function }}
91
+ */
92
+ get env() {
93
+ const read = (name) =>
94
+ Object.prototype.hasOwnProperty.call(this._publicEnv, name) ? this._publicEnv[name] : undefined;
95
+ const present = (v) => v !== undefined && String(v).trim() !== '';
96
+
97
+ return {
98
+ get: (name, fallback = undefined) => this.getEnv(name, fallback),
99
+ has: (name) => Object.prototype.hasOwnProperty.call(this._publicEnv, name),
100
+ all: () => this.getPublicEnv(),
101
+ bool: (name, fallback = false) => {
102
+ const v = read(name);
103
+ return present(v) ? ['1', 'true', 'yes', 'on'].includes(String(v).trim().toLowerCase()) : fallback;
104
+ },
105
+ int: (name, fallback = 0) => {
106
+ const v = read(name);
107
+ const n = parseInt(v, 10);
108
+ return Number.isNaN(n) ? fallback : n;
109
+ },
110
+ list: (name, fallback = []) => {
111
+ const v = read(name);
112
+ if (!present(v)) return fallback;
113
+ return String(v)
114
+ .split(',')
115
+ .map((s) => s.trim())
116
+ .filter(Boolean);
117
+ },
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get a component instance by sliceId.
123
+ * @param {string} componentSliceId
124
+ * @returns {HTMLElement|undefined}
125
+ */
126
+ getComponent(componentSliceId) {
127
+ return this.controller.activeComponents.get(componentSliceId);
128
+ }
129
+
130
+ /**
131
+ * Build a component instance and run init.
132
+ *
133
+ * Pass `{ singleton: true }` to get-or-create one shared instance keyed by
134
+ * `sliceId` (defaults to `componentName`). Concurrent singleton builds of the
135
+ * same id share a single in-flight build, so they never race on a duplicate
136
+ * sliceId. Singletons are only supported for Service components — for app-wide
137
+ * UI build a Provider Service that manages the Visual (see ToastProvider /
138
+ * ToolTipProvider), because a DOM node can only live in one place.
139
+ *
140
+ * Note: `props` only apply on the first (creating) call; later calls return
141
+ * the existing instance and ignore them.
142
+ *
143
+ * @param {string} componentName
144
+ * @param {Object} [props]
145
+ * @param {boolean} [props.singleton] Reuse a single instance per sliceId.
146
+ * @returns {Promise<HTMLElement|Object|null>}
147
+ */
148
+ async build(componentName, props = {}) {
149
+ if (!props || props.singleton !== true) {
150
+ return this._build(componentName, props);
151
+ }
152
+
153
+ const { singleton, ...rest } = props;
154
+ const sliceId = rest.sliceId || componentName;
155
+
156
+ // Singletons are allowed for any category whose *type* is 'Service' — not
157
+ // only the built-in 'Service' category. Custom categories declared in
158
+ // sliceConfig with `"type": "Service"` (e.g. AppServices) are services too.
159
+ const category = this.controller.componentCategories.get(componentName);
160
+ const categoryType = slice.paths?.components?.[category]?.type;
161
+ if (categoryType !== 'Service') {
162
+ this.logger.logError(
163
+ 'Slice',
164
+ `singleton:true is only supported for Service-type components ('${componentName}' is in category '${category || 'unknown'}', type '${categoryType || 'unknown'}'). ` +
165
+ `For app-wide UI build a Provider Service that manages the Visual (see ToastProvider/ToolTipProvider).`
166
+ );
167
+ return null;
168
+ }
169
+
170
+ return this.controller.getOrCreate(sliceId, () =>
171
+ this._build(componentName, { ...rest, sliceId })
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Internal build: load resources, construct, run init, register. Always
177
+ * creates a new instance. Public `build` delegates here (and wraps it with
178
+ * get-or-create when `singleton:true`).
179
+ * @param {string} componentName
180
+ * @param {Object} [props]
181
+ * @returns {Promise<HTMLElement|Object|null>}
182
+ */
183
+ async _build(componentName, props = {}) {
184
+ if (!componentName) {
185
+ this.logger.error('Slice', 'Component name is required to build a component');
186
+ return null;
187
+ }
188
+
189
+ if (typeof componentName !== 'string') {
190
+ this.logger.error('Slice', 'Component name must be a string');
191
+ return null;
192
+ }
193
+
194
+ // 📦 Try to load from bundles first
195
+ const bundleName = this.controller.getBundleForComponent(componentName);
196
+ if (bundleName && !this.controller.loadedBundles.has(bundleName)) {
197
+ await this.controller.loadBundle(bundleName);
198
+ }
199
+
200
+ if (!this.controller.componentCategories.has(componentName) && !this.controller.classes.has(componentName)) {
201
+ this.logger.error('Slice', `Component ${componentName} not found in components.js file`);
202
+ return null;
203
+ }
204
+
205
+ let componentCategory = this.controller.componentCategories.get(componentName);
206
+ if (!componentCategory && this.controller.classes.has(componentName)) {
207
+ componentCategory = 'AppComponents';
208
+ }
209
+
210
+ // 📦 Check if component is already available from loaded bundles
211
+ const isFromBundle = this.controller.isComponentFromBundle(componentName);
212
+
213
+ if (componentCategory === 'Structural') {
214
+ this.logger.error('Slice', `Component ${componentName} is a Structural component and cannot be built`);
215
+ return null;
216
+ }
217
+
218
+ let isVisual = slice.paths.components[componentCategory]?.type === 'Visual';
219
+ let modulePath = `${slice.paths.components[componentCategory].path}/${componentName}/${componentName}.js`;
220
+ const isJsOnlyVisualComponent = isVisual && (componentName === 'MultiRoute' || componentName === 'Route');
221
+
222
+ // Load template, class, and CSS concurrently if needed
223
+ try {
224
+ // 📦 Skip individual loading if component is available from bundles
225
+ const loadTemplate =
226
+ isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.templates.has(componentName)
227
+ ? Promise.resolve(null)
228
+ : this.controller.fetchText(componentName, 'html', componentCategory);
229
+
230
+ const loadClass =
231
+ isFromBundle || this.controller.classes.has(componentName)
232
+ ? Promise.resolve(null)
233
+ : this.getClass(modulePath);
234
+
235
+ const loadCSS =
236
+ isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.requestedStyles.has(componentName)
237
+ ? Promise.resolve(null)
238
+ : this.controller.fetchText(componentName, 'css', componentCategory);
239
+
240
+ const [html, ComponentClass, css] = await Promise.all([loadTemplate, loadClass, loadCSS]);
241
+
242
+ // 📦 If component is from bundle but not in cache, it should have been registered by registerBundle
243
+ if (isFromBundle) {
244
+ this.logger.logInfo('Slice', `📦 Using bundled component: ${componentName}`);
245
+ }
246
+
247
+ if (html || html === '') {
248
+ const template = document.createElement('template');
249
+ template.innerHTML = html;
250
+ this.controller.templates.set(componentName, template);
251
+ this.logger.logInfo('Slice', `Template ${componentName} loaded`);
252
+ }
253
+
254
+ if (ComponentClass) {
255
+ this.controller.classes.set(componentName, ComponentClass);
256
+ this.logger.logInfo('Slice', `Class ${componentName} loaded`);
257
+ }
258
+
259
+ if (css) {
260
+ this.stylesManager.registerComponentStyles(componentName, css);
261
+ this.logger.logInfo('Slice', `CSS ${componentName} loaded`);
262
+ }
263
+ } catch (error) {
264
+ this.logger.logError('Slice', `Error loading resources for ${componentName}`, error);
265
+ return null;
266
+ }
267
+
268
+ // Create instance
269
+ try {
270
+ let componentIds = {};
271
+ if (props.id) componentIds.id = props.id;
272
+ if (props.sliceId) componentIds.sliceId = props.sliceId;
273
+
274
+ delete props.id;
275
+ delete props.sliceId;
276
+ // `singleton` is a build directive (handled in the public build()
277
+ // wrapper). Strip it here too so it is consistently reserved and never
278
+ // leaks into a component's props on the non-singleton path.
279
+ delete props.singleton;
280
+
281
+ const ComponentClass = this.controller.classes.get(componentName);
282
+ this.logger.logInfo(
283
+ 'Slice',
284
+ `🔎 Build component: ${componentName} (classType: ${typeof ComponentClass}, isFunction: ${typeof ComponentClass === 'function'})`
285
+ );
286
+ const componentInstance = new ComponentClass(props);
287
+
288
+ if (componentIds.id && isVisual) componentInstance.id = componentIds.id;
289
+ if (componentIds.sliceId) componentInstance.sliceId = componentIds.sliceId;
290
+
291
+ if (!this.controller.verifyComponentIds(componentInstance)) {
292
+ this.logger.logError('Slice', `Error registering instance ${componentName} ${componentInstance.sliceId}`);
293
+ return null;
294
+ }
295
+
296
+ if (componentInstance.init) await componentInstance.init();
297
+
298
+ if (slice.debuggerConfig.enabled && isVisual) {
299
+ this.debugger.attachDebugMode(componentInstance);
300
+ }
301
+
302
+ this.controller.registerComponent(componentInstance);
303
+ if (isVisual) {
304
+ this.controller.registerComponentsRecursively(componentInstance);
305
+ }
306
+
307
+ this.logger.logInfo('Slice', `Instance ${componentInstance.sliceId} created`);
308
+ return componentInstance;
309
+ } catch (error) {
310
+ this.logger.logError('Slice', `Error creating instance ${componentName}`, error);
311
+ return null;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Apply a theme by name.
317
+ * @param {string} themeName
318
+ * @returns {Promise<void>}
319
+ */
320
+ async setTheme(themeName) {
321
+ await this.stylesManager.themeManager.applyTheme(themeName);
322
+ }
323
+
324
+ /**
325
+ * Current theme name.
326
+ * @returns {string|null}
327
+ */
328
+ get theme() {
329
+ return this.stylesManager.themeManager.currentTheme;
330
+ }
331
+
332
+ /**
333
+ * Attach HTML template to a component instance.
334
+ * @param {HTMLElement} componentInstance
335
+ * @returns {void}
336
+ */
337
+ attachTemplate(componentInstance) {
338
+ this.controller.loadTemplateToComponent(componentInstance);
339
+ }
340
+ }
341
+
342
+ async function loadConfig() {
343
+ try {
344
+ const response = await fetch('/sliceConfig.json');
345
+ if (!response.ok) throw new Error('Error loading sliceConfig.json');
346
+ const json = await response.json();
347
+ return json;
348
+ } catch (error) {
349
+ console.error('Error loading config file:', error);
350
+ return null;
351
+ }
352
+ }
353
+
354
+ async function init() {
355
+ const sliceConfig = await loadConfig();
356
+ if (!sliceConfig) {
357
+ console.error('%c\u26A0\uFE0F Error loading Slice configuration', 'color: red; font-size: 20px;');
358
+ alert('Error loading Slice configuration');
359
+ throw new Error('Slice initialization failed: unable to load sliceConfig.json');
360
+ }
361
+
362
+ // 1+2. Fetch mode endpoint and bundle config in parallel — both are independent.
363
+ // In production, /slice-env.json returns 404 (catch is expected and normal).
364
+ // bundleConfigJson.production serves as a mode fallback when env endpoint is absent.
365
+ let frameworkClasses = null;
366
+ const [envResult, configResult] = await Promise.all([
367
+ fetch('/slice-env.json', { cache: 'no-store' })
368
+ .then(r => r.ok ? r.json() : null)
369
+ .catch(error => { console.error('[Slice.js] Error fetching /slice-env.json:', error); return null; }),
370
+ fetch('/bundles/bundle.config.json', { cache: 'no-store' })
371
+ .then(r => r.ok ? r.json() : null)
372
+ .catch(error => { console.error('[Slice.js] Error fetching bundle.config.json:', error); return null; })
373
+ ]);
374
+ const envMode = envResult?.mode ?? null;
375
+ const bundleConfigJson = configResult;
376
+
377
+ // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
378
+ let resolvedMode;
379
+ if (envMode) {
380
+ resolvedMode = envMode;
381
+ } else if (bundleConfigJson?.production) {
382
+ resolvedMode = 'production';
383
+ } else {
384
+ resolvedMode = 'development';
385
+ }
386
+
387
+ // 4. Load framework classes.
388
+ // In production the bundler generates slice-bundle.framework.js which
389
+ // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
390
+ // imports so the live /Slice/ source is served directly without bundles.
391
+ if (resolvedMode === 'production' && bundleConfigJson?.bundles?.framework?.file) {
392
+ try {
393
+ await import(`/bundles/${bundleConfigJson.bundles.framework.file}`);
394
+ if (window.SLICE_FRAMEWORK_CLASSES) {
395
+ frameworkClasses = window.SLICE_FRAMEWORK_CLASSES;
396
+ }
397
+ } catch (e) {
398
+ console.error('[Slice.js] framework bundle import failed, falling through to individual imports:', e);
399
+ }
400
+ }
401
+
402
+ if (!frameworkClasses) {
403
+ try {
404
+ const imports = await Promise.all([
405
+ import('./Components/Structural/Controller/Controller.js'),
406
+ import('./Components/Structural/StylesManager/StylesManager.js')
407
+ ]);
408
+ frameworkClasses = {
409
+ Controller: imports[0].default,
410
+ StylesManager: imports[1].default
411
+ };
412
+ } catch (e) {
413
+ console.error('[Slice.js] individual imports fallback failed:', e);
414
+ throw e;
415
+ }
416
+ }
417
+
418
+ // 5. Create Slice instance and set resolved mode
419
+ window.slice = new Slice(sliceConfig, frameworkClasses);
420
+ window.slice._mode = resolvedMode;
421
+ window.slice.setPublicEnv(envResult?.env || {});
422
+
423
+ const createBundlingInitError = (step, error) => {
424
+ const detail = error instanceof Error ? error.message : String(error);
425
+ return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
426
+ };
427
+
428
+ // Initialize bundles before building components.
429
+ // Only in production — dev mode loads each component individually from source.
430
+ // bundleConfigJson was already fetched above (step 2); reuse it.
431
+ if (resolvedMode === 'production' && bundleConfigJson) {
432
+ window.slice.controller.bundleConfig = bundleConfigJson;
433
+ }
434
+
435
+ if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
436
+ const config = window.slice.controller.bundleConfig;
437
+ if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
438
+ window.__SLICE_SHARED_DEPS__ = {};
439
+ }
440
+ const criticalFile = config?.bundles?.critical?.file;
441
+ if (criticalFile) {
442
+ try {
443
+ await window.slice.controller.loadBundle('critical');
444
+ } catch (error) {
445
+ throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
446
+ }
447
+ }
448
+
449
+ const routeBundles = config?.routeBundles || {};
450
+ const initialPath = window.location.pathname || '/';
451
+ const bundlesForRoute = routeBundles[initialPath] || [];
452
+
453
+ const loadRouteBundles = async () => {
454
+ for (const bundleName of bundlesForRoute) {
455
+ if (bundleName === 'critical') continue;
456
+ const bundleInfo = config?.bundles?.routes?.[bundleName];
457
+ if (!bundleInfo?.file) continue;
458
+ await window.slice.controller.loadBundle(bundleName);
459
+ }
460
+ };
461
+
462
+ const preloadRouteBundles = () => {
463
+ loadRouteBundles().catch((error) => {
464
+ window.slice?.logger?.error('Slice', `Idle route preload failed for "${initialPath}"`, error);
465
+ });
466
+ };
467
+
468
+ const safePreload = () => {
469
+ try {
470
+ preloadRouteBundles();
471
+ } catch (error) {
472
+ window.slice?.logger?.error('Slice', 'Error in route preload callback', error);
473
+ }
474
+ };
475
+
476
+ if (typeof requestIdleCallback === 'function') {
477
+ requestIdleCallback(() => safePreload());
478
+ } else {
479
+ setTimeout(() => safePreload(), 0);
480
+ }
481
+ }
482
+
483
+ slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
484
+
485
+ if (sliceConfig.logger.enabled) {
486
+ const LoggerModule = window.slice.frameworkClasses?.Logger
487
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/Logger.js`);
488
+ window.slice.logger = new LoggerModule();
489
+ } else {
490
+ const noop = () => {};
491
+ window.slice.logger = {
492
+ error: noop, warn: noop, info: noop, debug: noop,
493
+ logError: noop, logWarning: noop, logInfo: noop,
494
+ };
495
+ }
496
+
497
+ if (sliceConfig.debugger.enabled) {
498
+ const DebuggerModule = window.slice.frameworkClasses?.Debugger
499
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`);
500
+ window.slice.debugger = new DebuggerModule();
501
+ await window.slice.debugger.enableDebugMode();
502
+ document.body.appendChild(window.slice.debugger);
503
+ }
504
+
505
+ if (sliceConfig.events?.ui?.enabled) {
506
+ const EventsDebuggerModule = window.slice.frameworkClasses?.EventManagerDebugger
507
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManagerDebugger.js`);
508
+ window.slice.eventsDebugger = new EventsDebuggerModule();
509
+ await window.slice.eventsDebugger.init();
510
+ document.body.appendChild(window.slice.eventsDebugger);
511
+ }
512
+
513
+ if (sliceConfig.context?.ui?.enabled) {
514
+ const ContextDebuggerModule = window.slice.frameworkClasses?.ContextManagerDebugger
515
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManagerDebugger.js`);
516
+ window.slice.contextDebugger = new ContextDebuggerModule();
517
+ await window.slice.contextDebugger.init();
518
+ document.body.appendChild(window.slice.contextDebugger);
519
+ }
520
+
521
+ if (sliceConfig.events?.enabled) {
522
+ const EventManagerModule = window.slice.frameworkClasses?.EventManager
523
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`);
524
+ window.slice.events = new EventManagerModule();
525
+ if (typeof window.slice.events.init === 'function') {
526
+ await window.slice.events.init();
527
+ }
528
+ } else {
529
+ window.slice.events = {
530
+ subscribe: () => null,
531
+ subscribeOnce: () => null,
532
+ unsubscribe: () => false,
533
+ emit: () => {},
534
+ bind: () => ({
535
+ subscribe: () => null,
536
+ subscribeOnce: () => null,
537
+ emit: () => {},
538
+ }),
539
+ cleanupComponent: () => 0,
540
+ hasSubscribers: () => false,
541
+ subscriberCount: () => 0,
542
+ clear: () => {},
543
+ };
544
+ window.slice.logger.logError('Slice', 'EventManager disabled');
545
+ }
546
+
547
+ if (sliceConfig.context?.enabled) {
548
+ const ContextManagerModule = window.slice.frameworkClasses?.ContextManager
549
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManager.js`);
550
+ window.slice.context = new ContextManagerModule();
551
+ if (typeof window.slice.context.init === 'function') {
552
+ await window.slice.context.init();
553
+ }
554
+ } else {
555
+ window.slice.context = {
556
+ create: () => false,
557
+ getState: () => null,
558
+ setState: () => {},
559
+ watch: () => null,
560
+ has: () => false,
561
+ destroy: () => false,
562
+ list: () => [],
563
+ };
564
+ window.slice.logger.logError('Slice', 'ContextManager disabled');
565
+ }
566
+
567
+ if (sliceConfig.logger?.ui?.enabled) {
568
+ try {
569
+ const LogViewerModule = window.slice.frameworkClasses?.LogViewer
570
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/LogViewer/LogViewer.js`);
571
+ const logViewer = new LogViewerModule();
572
+ window.slice.logViewer = logViewer;
573
+ logViewer.style.display = 'none';
574
+ document.body.appendChild(logViewer);
575
+ if (typeof logViewer.init === 'function') logViewer.init();
576
+ } catch (e) {
577
+ window.slice.logger?.warn?.('Slice', 'Could not load LogViewer component', e);
578
+ }
43
579
  }
44
- }
45
-
46
- /**
47
- * Returns true when running in production mode.
48
- * Reliable after init() has completed.
49
- * @returns {boolean}
50
- */
51
- isProduction() {
52
- return this._mode === 'production';
53
- }
54
-
55
- setPublicEnv(envPayload = {}) {
56
- const normalized = {};
57
-
58
- for (const [key, value] of Object.entries(envPayload || {})) {
59
- if (!key.startsWith('SLICE_PUBLIC_')) continue;
60
- normalized[key] = String(value ?? '');
61
- }
62
-
63
- this._publicEnv = normalized;
64
- }
65
-
66
- getEnv(name, fallbackValue = undefined) {
67
- if (!name || typeof name !== 'string') {
68
- return fallbackValue;
69
- }
70
-
71
- if (Object.prototype.hasOwnProperty.call(this._publicEnv, name)) {
72
- return this._publicEnv[name];
73
- }
74
-
75
- return fallbackValue;
76
- }
77
-
78
- getPublicEnv() {
79
- return { ...this._publicEnv };
80
- }
81
-
82
- /**
83
- * Typed accessors over the public env, so callers stop re-parsing strings.
84
- * slice.env.get('SLICE_PUBLIC_API_URL', '')
85
- * slice.env.bool('SLICE_PUBLIC_AUTH_ENABLED') // '1'|'true'|'yes'|'on' → true
86
- * slice.env.int('SLICE_PUBLIC_TIMEOUT', 5000)
87
- * slice.env.list('SLICE_PUBLIC_MODELS') // 'a, b' → ['a','b']
88
- * slice.env.has('X') / slice.env.all()
89
- * @returns {{ get: Function, has: Function, all: Function, bool: Function, int: Function, list: Function }}
90
- */
91
- get env() {
92
- const read = (name) =>
93
- Object.prototype.hasOwnProperty.call(this._publicEnv, name) ? this._publicEnv[name] : undefined;
94
- const present = (v) => v !== undefined && String(v).trim() !== '';
95
-
96
- return {
97
- get: (name, fallback = undefined) => this.getEnv(name, fallback),
98
- has: (name) => Object.prototype.hasOwnProperty.call(this._publicEnv, name),
99
- all: () => this.getPublicEnv(),
100
- bool: (name, fallback = false) => {
101
- const v = read(name);
102
- return present(v) ? ['1', 'true', 'yes', 'on'].includes(String(v).trim().toLowerCase()) : fallback;
103
- },
104
- int: (name, fallback = 0) => {
105
- const v = read(name);
106
- const n = parseInt(v, 10);
107
- return Number.isNaN(n) ? fallback : n;
108
- },
109
- list: (name, fallback = []) => {
110
- const v = read(name);
111
- if (!present(v)) return fallback;
112
- return String(v)
113
- .split(',')
114
- .map((s) => s.trim())
115
- .filter(Boolean);
116
- },
117
- };
118
- }
119
-
120
- /**
121
- * Get a component instance by sliceId.
122
- * @param {string} componentSliceId
123
- * @returns {HTMLElement|undefined}
124
- */
125
- getComponent(componentSliceId) {
126
- return this.controller.activeComponents.get(componentSliceId);
127
- }
128
-
129
- /**
130
- * Build a component instance and run init.
131
- *
132
- * Pass `{ singleton: true }` to get-or-create one shared instance keyed by
133
- * `sliceId` (defaults to `componentName`). Concurrent singleton builds of the
134
- * same id share a single in-flight build, so they never race on a duplicate
135
- * sliceId. Singletons are only supported for Service components — for app-wide
136
- * UI build a Provider Service that manages the Visual (see ToastProvider /
137
- * ToolTipProvider), because a DOM node can only live in one place.
138
- *
139
- * Note: `props` only apply on the first (creating) call; later calls return
140
- * the existing instance and ignore them.
141
- *
142
- * @param {string} componentName
143
- * @param {Object} [props]
144
- * @param {boolean} [props.singleton] Reuse a single instance per sliceId.
145
- * @returns {Promise<HTMLElement|Object|null>}
146
- */
147
- async build(componentName, props = {}) {
148
- if (!props || props.singleton !== true) {
149
- return this._build(componentName, props);
150
- }
151
-
152
- const { singleton, ...rest } = props;
153
- const sliceId = rest.sliceId || componentName;
154
-
155
- // Singletons are allowed for any category whose *type* is 'Service' — not
156
- // only the built-in 'Service' category. Custom categories declared in
157
- // sliceConfig with `"type": "Service"` (e.g. AppServices) are services too.
158
- const category = this.controller.componentCategories.get(componentName);
159
- const categoryType = slice.paths?.components?.[category]?.type;
160
- if (categoryType !== 'Service') {
161
- this.logger.logError(
162
- 'Slice',
163
- `singleton:true is only supported for Service-type components ('${componentName}' is in category '${category || 'unknown'}', type '${categoryType || 'unknown'}'). ` +
164
- `For app-wide UI build a Provider Service that manages the Visual (see ToastProvider/ToolTipProvider).`
165
- );
166
- return null;
167
- }
168
-
169
- return this.controller.getOrCreate(sliceId, () =>
170
- this._build(componentName, { ...rest, sliceId })
171
- );
172
- }
173
-
174
- /**
175
- * Internal build: load resources, construct, run init, register. Always
176
- * creates a new instance. Public `build` delegates here (and wraps it with
177
- * get-or-create when `singleton:true`).
178
- * @param {string} componentName
179
- * @param {Object} [props]
180
- * @returns {Promise<HTMLElement|Object|null>}
181
- */
182
- async _build(componentName, props = {}) {
183
- if (!componentName) {
184
- this.logger.logError('Slice', null, `Component name is required to build a component`);
185
- return null;
186
- }
187
-
188
- if (typeof componentName !== 'string') {
189
- this.logger.logError('Slice', null, `Component name must be a string`);
190
- return null;
191
- }
192
-
193
- // 📦 Try to load from bundles first
194
- const bundleName = this.controller.getBundleForComponent(componentName);
195
- if (bundleName && !this.controller.loadedBundles.has(bundleName)) {
196
- await this.controller.loadBundle(bundleName);
197
- }
198
-
199
- // After bundle loading attempt, allow build when class is already available
200
- // even if components map has no category entry (stale/components.js mismatch).
201
- if (!this.controller.componentCategories.has(componentName) && !this.controller.classes.has(componentName)) {
202
- this.logger.logError('Slice', null, `Component ${componentName} not found in components.js file`);
203
- return null;
204
- }
205
-
206
- let componentCategory = this.controller.componentCategories.get(componentName);
207
- if (!componentCategory && this.controller.classes.has(componentName)) {
208
- componentCategory = 'AppComponents';
209
- }
210
-
211
- // 📦 Check if component is already available from loaded bundles
212
- const isFromBundle = this.controller.isComponentFromBundle(componentName);
213
-
214
- if (componentCategory === 'Structural') {
215
- this.logger.logError(
216
- 'Slice',
217
- null,
218
- `Component ${componentName} is a Structural component and cannot be built`
219
- );
220
- return null;
221
- }
222
-
223
- let isVisual = slice.paths.components[componentCategory]?.type === 'Visual';
224
- let modulePath = `${slice.paths.components[componentCategory].path}/${componentName}/${componentName}.js`;
225
- const isJsOnlyVisualComponent = isVisual && (componentName === 'MultiRoute' || componentName === 'Route');
226
-
227
- // Load template, class, and CSS concurrently if needed
228
- try {
229
- // 📦 Skip individual loading if component is available from bundles
230
- const loadTemplate =
231
- isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.templates.has(componentName)
232
- ? Promise.resolve(null)
233
- : this.controller.fetchText(componentName, 'html', componentCategory);
234
-
235
- const loadClass =
236
- isFromBundle || this.controller.classes.has(componentName)
237
- ? Promise.resolve(null)
238
- : this.getClass(modulePath);
239
-
240
- const loadCSS =
241
- isFromBundle || !isVisual || isJsOnlyVisualComponent || this.controller.requestedStyles.has(componentName)
242
- ? Promise.resolve(null)
243
- : this.controller.fetchText(componentName, 'css', componentCategory);
244
-
245
- const [html, ComponentClass, css] = await Promise.all([loadTemplate, loadClass, loadCSS]);
246
-
247
- // 📦 If component is from bundle but not in cache, it should have been registered by registerBundle
248
- if (isFromBundle) {
249
- this.logger.logInfo('Slice', `📦 Using bundled component: ${componentName}`);
250
- }
251
-
252
- if (html || html === '') {
253
- const template = document.createElement('template');
254
- template.innerHTML = html;
255
- this.controller.templates.set(componentName, template);
256
- this.logger.logInfo('Slice', `Template ${componentName} loaded`);
257
- }
258
-
259
- if (ComponentClass) {
260
- this.controller.classes.set(componentName, ComponentClass);
261
- this.logger.logInfo('Slice', `Class ${componentName} loaded`);
262
- }
263
-
264
- if (css) {
265
- this.stylesManager.registerComponentStyles(componentName, css);
266
- this.logger.logInfo('Slice', `CSS ${componentName} loaded`);
267
- }
268
- } catch (error) {
269
- this.logger.logError('Slice', `Error loading resources for ${componentName}`, error);
270
- return null;
271
- }
272
-
273
- // Create instance
274
- try {
275
- let componentIds = {};
276
- if (props.id) componentIds.id = props.id;
277
- if (props.sliceId) componentIds.sliceId = props.sliceId;
278
-
279
- delete props.id;
280
- delete props.sliceId;
281
- // `singleton` is a build directive (handled in the public build()
282
- // wrapper). Strip it here too so it is consistently reserved and never
283
- // leaks into a component's props on the non-singleton path.
284
- delete props.singleton;
285
-
286
- const ComponentClass = this.controller.classes.get(componentName);
287
- this.logger.logInfo(
288
- 'Slice',
289
- `🔎 Build component: ${componentName} (classType: ${typeof ComponentClass}, isFunction: ${typeof ComponentClass === 'function'})`
290
- );
291
- const componentInstance = new ComponentClass(props);
292
-
293
- if (componentIds.id && isVisual) componentInstance.id = componentIds.id;
294
- if (componentIds.sliceId) componentInstance.sliceId = componentIds.sliceId;
295
-
296
- if (!this.controller.verifyComponentIds(componentInstance)) {
297
- this.logger.logError('Slice', `Error registering instance ${componentName} ${componentInstance.sliceId}`);
298
- return null;
299
- }
300
-
301
- if (componentInstance.init) await componentInstance.init();
302
-
303
- if (slice.debuggerConfig.enabled && isVisual) {
304
- this.debugger.attachDebugMode(componentInstance);
305
- }
306
-
307
- this.controller.registerComponent(componentInstance);
308
- if (isVisual) {
309
- this.controller.registerComponentsRecursively(componentInstance);
310
- }
311
-
312
- this.logger.logInfo('Slice', `Instance ${componentInstance.sliceId} created`);
313
- return componentInstance;
314
- } catch (error) {
315
- this.logger.logError('Slice', `Error creating instance ${componentName}`, error);
316
- return null;
317
- }
318
- }
319
-
320
- /**
321
- * Apply a theme by name.
322
- * @param {string} themeName
323
- * @returns {Promise<void>}
324
- */
325
- async setTheme(themeName) {
326
- await this.stylesManager.themeManager.applyTheme(themeName);
327
- }
328
-
329
- /**
330
- * Current theme name.
331
- * @returns {string|null}
332
- */
333
- get theme() {
334
- return this.stylesManager.themeManager.currentTheme;
335
- }
336
-
337
- /**
338
- * Attach HTML template to a component instance.
339
- * @param {HTMLElement} componentInstance
340
- * @returns {void}
341
- */
342
- attachTemplate(componentInstance) {
343
- this.controller.loadTemplateToComponent(componentInstance);
344
- }
345
- }
346
-
347
- async function loadConfig() {
348
- try {
349
- const response = await fetch('/sliceConfig.json'); // 🔹 Express lo sirve desde `src/`
350
- if (!response.ok) throw new Error('Error loading sliceConfig.json');
351
- const json = await response.json();
352
- return json;
353
- } catch (error) {
354
- console.error(`Error loading config file: ${error.message}`);
355
- return null;
356
- }
357
- }
358
-
359
- async function init() {
360
- const sliceConfig = await loadConfig();
361
- if (!sliceConfig) {
362
- //Display error message in console with colors and alert in english
363
- console.error('%c⛔️ Error loading Slice configuration ⛔️', 'color: red; font-size: 20px;');
364
- alert('Error loading Slice configuration');
365
- return;
366
- }
367
-
368
- // 1+2. Fetch mode endpoint and bundle config in parallel — both are independent.
369
- // In production, /slice-env.json returns 404 (catch is expected and normal).
370
- // bundleConfigJson.production serves as a mode fallback when env endpoint is absent.
371
- let frameworkClasses = null;
372
- const [envResult, configResult] = await Promise.all([
373
- fetch('/slice-env.json', { cache: 'no-store' })
374
- .then(r => r.ok ? r.json() : null)
375
- .catch(() => null),
376
- fetch('/bundles/bundle.config.json', { cache: 'no-store' })
377
- .then(r => r.ok ? r.json() : null)
378
- .catch(() => null)
379
- ]);
380
- const envMode = envResult?.mode ?? null;
381
- const bundleConfigJson = configResult;
382
-
383
- // 3. Determine canonical mode: env endpoint takes precedence, then bundle config
384
- let resolvedMode;
385
- if (envMode) {
386
- resolvedMode = envMode;
387
- } else if (bundleConfigJson?.production) {
388
- resolvedMode = 'production';
389
- } else {
390
- resolvedMode = 'development';
391
- }
392
-
393
- // 4. Load framework classes.
394
- // In production the bundler generates slice-bundle.framework.js which
395
- // sets window.SLICE_FRAMEWORK_CLASSES. In dev mode always use individual
396
- // imports so the live /Slice/ source is served directly without bundles.
397
- if (resolvedMode === 'production' && bundleConfigJson?.bundles?.framework?.file) {
398
- try {
399
- await import(`/bundles/${bundleConfigJson.bundles.framework.file}`);
400
- if (window.SLICE_FRAMEWORK_CLASSES) {
401
- frameworkClasses = window.SLICE_FRAMEWORK_CLASSES;
402
- }
403
- } catch (e) {
404
- // framework bundle failed — fall through to individual imports
405
- console.error('[Slice.js] framework bundle import failed:', e?.message || e);
406
- }
407
- }
408
-
409
- if (!frameworkClasses) {
410
- try {
411
- const imports = await Promise.all([
412
- import('./Components/Structural/Controller/Controller.js'),
413
- import('./Components/Structural/StylesManager/StylesManager.js')
414
- ]);
415
- frameworkClasses = {
416
- Controller: imports[0].default,
417
- StylesManager: imports[1].default
418
- };
419
- } catch (e) {
420
- console.error('[Slice.js] individual imports fallback failed:', e?.message || e);
421
- throw e;
422
- }
423
- }
424
-
425
- // 5. Create Slice instance and set resolved mode
426
- window.slice = new Slice(sliceConfig, frameworkClasses);
427
- window.slice._mode = resolvedMode;
428
- window.slice.setPublicEnv(envResult?.env || {});
429
-
430
- const createBundlingInitError = (step, error) => {
431
- const detail = error instanceof Error ? error.message : String(error);
432
- return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
433
- };
434
-
435
- // Initialize bundles before building components.
436
- // Only in production — dev mode loads each component individually from source.
437
- // bundleConfigJson was already fetched above (step 2); reuse it.
438
- if (resolvedMode === 'production' && bundleConfigJson) {
439
- window.slice.controller.bundleConfig = bundleConfigJson;
440
- }
441
-
442
- if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
443
- const config = window.slice.controller.bundleConfig;
444
- if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
445
- window.__SLICE_SHARED_DEPS__ = {};
446
- }
447
- const criticalFile = config?.bundles?.critical?.file;
448
- if (criticalFile) {
449
- try {
450
- await window.slice.controller.loadBundle('critical');
451
- } catch (error) {
452
- throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
453
- }
454
- }
455
-
456
- const routeBundles = config?.routeBundles || {};
457
- const initialPath = window.location.pathname || '/';
458
- const bundlesForRoute = routeBundles[initialPath] || [];
459
-
460
- const loadRouteBundles = async () => {
461
- for (const bundleName of bundlesForRoute) {
462
- if (bundleName === 'critical') continue;
463
- const bundleInfo = config?.bundles?.routes?.[bundleName];
464
- if (!bundleInfo?.file) continue;
465
- await window.slice.controller.loadBundle(bundleName);
466
- }
467
- };
468
-
469
- const preloadRouteBundles = () => {
470
- loadRouteBundles().catch((error) => {
471
- const bundlingError = createBundlingInitError(
472
- `idle route preload "${initialPath}"`,
473
- error
474
- );
475
- queueMicrotask(() => {
476
- throw bundlingError;
477
- });
478
- });
479
- };
480
-
481
- if (typeof requestIdleCallback === 'function') {
482
- requestIdleCallback(() => preloadRouteBundles());
483
- } else {
484
- setTimeout(() => preloadRouteBundles(), 0);
485
- }
486
- }
487
-
488
- slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
489
-
490
- if (sliceConfig.logger.enabled) {
491
- const LoggerModule = window.slice.frameworkClasses?.Logger
492
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/Logger.js`);
493
- window.slice.logger = new LoggerModule();
494
- } else {
495
- window.slice.logger = {
496
- logError: () => {},
497
- logWarning: () => {},
498
- logInfo: () => {},
499
- };
500
- }
501
-
502
- if (sliceConfig.debugger.enabled) {
503
- const DebuggerModule = window.slice.frameworkClasses?.Debugger
504
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`);
505
- window.slice.debugger = new DebuggerModule();
506
- await window.slice.debugger.enableDebugMode();
507
- document.body.appendChild(window.slice.debugger);
508
- }
509
-
510
- if (sliceConfig.events?.ui?.enabled) {
511
- const EventsDebuggerModule = window.slice.frameworkClasses?.EventManagerDebugger
512
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManagerDebugger.js`);
513
- window.slice.eventsDebugger = new EventsDebuggerModule();
514
- await window.slice.eventsDebugger.init();
515
- document.body.appendChild(window.slice.eventsDebugger);
516
- }
517
-
518
- if (sliceConfig.context?.ui?.enabled) {
519
- const ContextDebuggerModule = window.slice.frameworkClasses?.ContextManagerDebugger
520
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManagerDebugger.js`);
521
- window.slice.contextDebugger = new ContextDebuggerModule();
522
- await window.slice.contextDebugger.init();
523
- document.body.appendChild(window.slice.contextDebugger);
524
- }
525
-
526
- if (sliceConfig.events?.enabled) {
527
- const EventManagerModule = window.slice.frameworkClasses?.EventManager
528
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/EventManager/EventManager.js`);
529
- window.slice.events = new EventManagerModule();
530
- if (typeof window.slice.events.init === 'function') {
531
- await window.slice.events.init();
532
- }
533
- } else {
534
- window.slice.events = {
535
- subscribe: () => null,
536
- subscribeOnce: () => null,
537
- unsubscribe: () => false,
538
- emit: () => {},
539
- bind: () => ({
540
- subscribe: () => null,
541
- subscribeOnce: () => null,
542
- emit: () => {},
543
- }),
544
- cleanupComponent: () => 0,
545
- hasSubscribers: () => false,
546
- subscriberCount: () => 0,
547
- clear: () => {},
548
- };
549
- window.slice.logger.logError('Slice', 'EventManager disabled');
550
- }
551
-
552
- if (sliceConfig.context?.enabled) {
553
- const ContextManagerModule = window.slice.frameworkClasses?.ContextManager
554
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/ContextManager/ContextManager.js`);
555
- window.slice.context = new ContextManagerModule();
556
- if (typeof window.slice.context.init === 'function') {
557
- await window.slice.context.init();
558
- }
559
- } else {
560
- window.slice.context = {
561
- create: () => false,
562
- getState: () => null,
563
- setState: () => {},
564
- watch: () => null,
565
- has: () => false,
566
- destroy: () => false,
567
- list: () => [],
568
- };
569
- window.slice.logger.logError('Slice', 'ContextManager disabled');
570
- }
571
-
572
- if (sliceConfig.loading.enabled) {
573
- const loading = await window.slice.build('Loading', {});
574
- window.slice.loading = loading;
575
- if (typeof loading?.start === 'function') {
576
- loading.start();
577
- }
578
- }
579
-
580
- const stylesInitPromise = window.slice.stylesManager.init();
581
- const routesModulePromise = import(slice.paths.routesFile);
582
-
583
- if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut) {
584
- const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
585
- const toKey = (event) => {
586
- const parts = [];
587
- if (event.ctrlKey) parts.push('ctrl');
588
- if (event.shiftKey) parts.push('shift');
589
- if (event.altKey) parts.push('alt');
590
- if (event.metaKey) parts.push('meta');
591
- const key = event.key?.toLowerCase();
592
- if (key && !['control', 'shift', 'alt', 'meta'].includes(key)) {
593
- parts.push(key);
594
- }
595
- return parts.join('+');
596
- };
597
-
598
- const handlers = {
599
- [normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
600
- [normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
601
- };
602
-
603
- document.addEventListener('keydown', (event) => {
604
- const key = toKey(event);
605
- if (!key || !handlers[key]) return;
606
- event.preventDefault();
607
- handlers[key]();
608
- });
609
- }
610
-
611
- const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise]);
612
- const routes = routesModule.default;
613
- const RouterModule = window.slice.frameworkClasses?.Router
614
- || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
615
- window.slice.router = new RouterModule(routes);
616
- await window.slice.router.init();
617
- }
618
-
619
- await init();
580
+
581
+ if (sliceConfig.loading.enabled) {
582
+ const loading = await window.slice.build('Loading', {});
583
+ window.slice.loading = loading;
584
+ if (typeof loading?.start === 'function') {
585
+ loading.start();
586
+ }
587
+ }
588
+
589
+ const stylesInitPromise = window.slice.stylesManager.init();
590
+ const routesModulePromise = import(slice.paths.routesFile);
591
+
592
+ if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut) {
593
+ const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
594
+ const toKey = (event) => {
595
+ const parts = [];
596
+ if (event.ctrlKey) parts.push('ctrl');
597
+ if (event.shiftKey) parts.push('shift');
598
+ if (event.altKey) parts.push('alt');
599
+ if (event.metaKey) parts.push('meta');
600
+ const key = event.key?.toLowerCase();
601
+ if (key && !['control', 'shift', 'alt', 'meta'].includes(key)) {
602
+ parts.push(key);
603
+ }
604
+ return parts.join('+');
605
+ };
606
+
607
+ const handlers = {
608
+ [normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
609
+ [normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
610
+ [normalize(sliceConfig.logger?.ui?.shortcut)]: () => window.slice.logViewer?.toggle?.(),
611
+ };
612
+
613
+ document.addEventListener('keydown', (event) => {
614
+ const key = toKey(event);
615
+ if (!key || !handlers[key]) return;
616
+ event.preventDefault();
617
+ handlers[key]();
618
+ });
619
+ }
620
+
621
+ const [, routesModule] = await Promise.all([stylesInitPromise, routesModulePromise]);
622
+ const routes = routesModule.default;
623
+ const RouterModule = window.slice.frameworkClasses?.Router
624
+ || await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Router/Router.js`);
625
+ window.slice.router = new RouterModule(routes);
626
+ await window.slice.router.init();
627
+ }
628
+
629
+ try {
630
+ await init();
631
+ } catch (initError) {
632
+ console.error('[Slice.js] Initialization failed:', initError);
633
+ }