slicejs-web-framework 4.0.1 → 4.0.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.
Files changed (71) hide show
  1. package/.opencode/opencode.json +14 -14
  2. package/LICENSE +21 -21
  3. package/README.md +97 -97
  4. package/Slice/Components/Structural/ContextManager/ContextManager.js +422 -422
  5. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +470 -470
  6. package/Slice/Components/Structural/Controller/Controller.js +1406 -1406
  7. package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -52
  8. package/Slice/Components/Structural/Debugger/Debugger.html +72 -72
  9. package/Slice/Components/Structural/Debugger/Debugger.js +1505 -1505
  10. package/Slice/Components/Structural/EventManager/EventManager.js +651 -651
  11. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +985 -985
  12. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -296
  13. package/Slice/Components/Structural/Logger/Log.js +10 -10
  14. package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +662 -662
  15. package/Slice/Components/Structural/Logger/Logger.js +225 -225
  16. package/Slice/Components/Structural/Router/Router.js +810 -793
  17. package/Slice/Components/Structural/StylesManager/StylesManager.js +82 -82
  18. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +175 -162
  19. package/Slice/Slice.js +708 -685
  20. package/Slice/tests/build-bundled-component-without-category.test.js +103 -103
  21. package/Slice/tests/build-js-only-visual-components.test.js +144 -144
  22. package/Slice/tests/build-singleton.test.js +244 -244
  23. package/Slice/tests/bundle-v2-runtime-contract.test.js +747 -738
  24. package/Slice/tests/context-patch-use.test.js +95 -95
  25. package/Slice/tests/destroy-cascade.test.js +215 -215
  26. package/Slice/tests/event-emitters.test.js +79 -79
  27. package/Slice/tests/event-registry.test.js +141 -141
  28. package/Slice/tests/fixtures/real-runtime-loader.mjs +21 -21
  29. package/Slice/tests/leak-orphans.test.js +131 -131
  30. package/Slice/tests/props-allowed-values-validation.test.js +119 -119
  31. package/Slice/tests/public-env-runtime-accessors.test.js +44 -44
  32. package/Slice/tests/public-env-typed-accessors.test.js +66 -66
  33. package/Slice/tests/router-loading-finally.test.js +68 -68
  34. package/api/framework/devDepsOptimizer.js +335 -335
  35. package/api/framework/express.js +3 -3
  36. package/api/framework/precompressedStatic.js +93 -93
  37. package/api/framework/publicEnvResolver.js +123 -123
  38. package/api/framework/securityMiddleware.js +197 -197
  39. package/api/framework/server.js +411 -411
  40. package/api/index.js +7 -7
  41. package/api/tests/dev-deps-optimizer.test.js +137 -137
  42. package/api/tests/dev-server-external.test.js +56 -56
  43. package/api/tests/precompressed-static.test.js +119 -119
  44. package/api/tests/public-env-resolver.test.js +193 -193
  45. package/package.json +49 -49
  46. package/sliceConfig.schema.json +122 -122
  47. package/src/App/index.html +69 -69
  48. package/src/App/index.js +20 -20
  49. package/src/App/style.css +11 -11
  50. package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -9
  51. package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -8
  52. package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -12
  53. package/src/Components/AppComponents/AppShell/AppShell.css +10 -10
  54. package/src/Components/AppComponents/AppShell/AppShell.html +4 -4
  55. package/src/Components/AppComponents/AppShell/AppShell.js +36 -36
  56. package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -20
  57. package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -10
  58. package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -19
  59. package/src/Components/components.js +8 -8
  60. package/src/public/Styles/sliceStyles.css +34 -34
  61. package/src/public/Themes/Dark.css +42 -42
  62. package/src/public/Themes/Light.css +31 -31
  63. package/src/public/Themes/Slice.css +47 -47
  64. package/src/public/manifest.json +15 -15
  65. package/src/public/robots.txt +4 -4
  66. package/src/public/sitemap.xml +8 -8
  67. package/src/routes.js +9 -9
  68. package/src/sliceConfig.json +72 -72
  69. package/tests/router-build-failure.test.js +125 -0
  70. package/tests/router-emit-ordering.test.js +63 -63
  71. package/types/index.d.ts +219 -219
@@ -1,422 +1,422 @@
1
- /**
2
- * ContextManager - Sistema de contextos compartidos para Slice.js
3
- * Ubicación: /Slice/Components/Structural/ContextManager/ContextManager.js
4
- *
5
- * Características:
6
- * - Estado compartido entre componentes
7
- * - Usa EventManager internamente para notificaciones
8
- * - Selectores para observar campos específicos
9
- * - Auto-limpieza cuando componentes se destruyen
10
- * - Persistencia opcional en localStorage
11
- */
12
- /**
13
- * @typedef {Object} ContextOptions
14
- * @property {boolean} [persist]
15
- * @property {string} [storageKey]
16
- */
17
-
18
- export default class ContextManager {
19
- constructor() {
20
- // Map<contextName, { state, options }>
21
- this.contexts = new Map();
22
- }
23
-
24
- init() {
25
- return true;
26
- }
27
-
28
- // ============================================
29
- // CREAR CONTEXTO
30
- // ============================================
31
-
32
- /**
33
- * Crear un nuevo contexto
34
- * @param {string} name - Nombre unico del contexto
35
- * @param {Object} [initialState] - Estado inicial
36
- * @param {ContextOptions} [options] - Opciones: { persist, storageKey }
37
- * @returns {boolean}
38
- *
39
- * @example
40
- * slice.context.create('auth', {
41
- * isLoggedIn: false,
42
- * user: null
43
- * });
44
- *
45
- * // Con persistencia en localStorage
46
- * slice.context.create('preferences', {
47
- * theme: 'light',
48
- * language: 'es'
49
- * }, { persist: true });
50
- */
51
- create(name, initialState = {}, options = {}) {
52
- if (this.contexts.has(name)) {
53
- slice.logger.logWarning('ContextManager', `El contexto "${name}" ya existe`);
54
- return false;
55
- }
56
-
57
- // Cargar estado persistido si existe
58
- let state = initialState;
59
- if (options.persist) {
60
- const storageKey = options.storageKey || `slice_context_${name}`;
61
- const persisted = this._loadFromStorage(storageKey);
62
- if (persisted !== null) {
63
- state = persisted;
64
- }
65
- }
66
-
67
- this.contexts.set(name, {
68
- state,
69
- options: {
70
- persist: options.persist || false,
71
- storageKey: options.storageKey || `slice_context_${name}`,
72
- },
73
- });
74
-
75
- slice.events.emit('context:__created', { name, state });
76
-
77
- slice.logger.logInfo('ContextManager', `Contexto "${name}" creado`);
78
-
79
- return true;
80
- }
81
-
82
- // ============================================
83
- // LEER ESTADO
84
- // ============================================
85
-
86
- /**
87
- * Obtener el estado actual de un contexto
88
- * @param {string} name - Nombre del contexto
89
- * @returns {any|null} Estado actual o null si no existe
90
- *
91
- * @example
92
- * const auth = slice.context.getState('auth');
93
- * console.log(auth.user.name);
94
- */
95
- getState(name) {
96
- if (!this.contexts.has(name)) {
97
- slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
98
- return null;
99
- }
100
-
101
- return this.contexts.get(name).state;
102
- }
103
-
104
- // ============================================
105
- // ACTUALIZAR ESTADO
106
- // ============================================
107
-
108
- /**
109
- * Actualizar el estado de un contexto
110
- * @param {string} name - Nombre del contexto
111
- * @param {Object|Function} updater - Nuevo estado o funcion (prevState) => newState
112
- * @returns {void}
113
- *
114
- * @example
115
- * // Reemplazar con objeto
116
- * slice.context.setState('auth', {
117
- * isLoggedIn: true,
118
- * user: { name: 'Juan' }
119
- * });
120
- *
121
- * // Usar funcion para acceder al estado anterior
122
- * slice.context.setState('cart', (prev) => ({
123
- * ...prev,
124
- * items: [...prev.items, nuevoProducto],
125
- * total: prev.total + nuevoProducto.price
126
- * }));
127
- */
128
- setState(name, updater) {
129
- if (!this.contexts.has(name)) {
130
- slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
131
- return;
132
- }
133
-
134
- const context = this.contexts.get(name);
135
- const prevState = context.state;
136
-
137
- // Calcular nuevo estado
138
- let newState;
139
- if (typeof updater === 'function') {
140
- newState = updater(prevState);
141
- } else {
142
- newState = updater;
143
- }
144
-
145
- // Guardar nuevo estado
146
- context.state = newState;
147
-
148
- // Persistir si está habilitado
149
- if (context.options.persist) {
150
- this._saveToStorage(name, newState, context.options.storageKey);
151
- }
152
-
153
- // Emitir evento para notificar a los watchers
154
- slice.events.emit(`context:${name}`, newState, prevState);
155
-
156
- slice.logger.logInfo('ContextManager', `Contexto "${name}" actualizado`);
157
- }
158
-
159
- /**
160
- * Merge a partial object into the context's state (first-level merge).
161
- * `setState` REPLACES the whole state; `patch` keeps the existing fields and
162
- * overrides only the keys you pass — the common "update one field" case.
163
- * @param {string} name - Nombre del contexto
164
- * @param {Object} partial - Campos a fusionar sobre el estado actual
165
- * @returns {void}
166
- *
167
- * @example
168
- * slice.context.patch('cart', { discount: 0.1 }); // items/total se conservan
169
- */
170
- patch(name, partial) {
171
- if (!this.contexts.has(name)) {
172
- slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
173
- return;
174
- }
175
- if (!partial || typeof partial !== 'object' || Array.isArray(partial)) {
176
- slice.logger.logError('ContextManager', `patch("${name}") requiere un objeto parcial`);
177
- return;
178
- }
179
- return this.setState(name, (prev) => ({ ...prev, ...partial }));
180
- }
181
-
182
- /**
183
- * Return a handle bound to a single context so you call its methods without
184
- * repeating the name on every call.
185
- * @param {string} name - Nombre del contexto
186
- * @returns {{ get: Function, set: Function, patch: Function, watch: Function, bind: Function, has: Function, destroy: Function }}
187
- *
188
- * @example
189
- * const cart = slice.context.use('cart');
190
- * cart.get(); // estado actual
191
- * cart.patch({ discount: 0.1 }); // merge (conserva el resto)
192
- * cart.watch(this, (s) => this.render(s));
193
- * // watch + render inicial en una línea:
194
- * cart.bind(this, (count) => { this.$badge.textContent = count; }, (s) => s.items.length);
195
- */
196
- use(name) {
197
- return {
198
- get: () => this.getState(name),
199
- set: (updater) => this.setState(name, updater),
200
- patch: (partial) => this.patch(name, partial),
201
- watch: (component, callback, selector = null) => this.watch(name, component, callback, selector),
202
- // watch + an immediate call with the current value (initial render in one line)
203
- bind: (component, callback, selector = null) => {
204
- const state = this.getState(name);
205
- callback(selector ? selector(state) : state);
206
- return this.watch(name, component, callback, selector);
207
- },
208
- has: () => this.has(name),
209
- destroy: () => this.destroy(name),
210
- };
211
- }
212
-
213
- // ============================================
214
- // WATCH (OBSERVAR CAMBIOS)
215
- // ============================================
216
-
217
- /**
218
- * Observar cambios en un contexto
219
- * @param {string} name - Nombre del contexto
220
- * @param {HTMLElement} component - Componente Slice para auto-cleanup
221
- * @param {(value: any) => void} callback - Funcion a ejecutar cuando cambia
222
- * @param {(state: any) => any} [selector] - Opcional. Funcion para seleccionar campos
223
- * @returns {string|null} subscriptionId
224
- *
225
- * @example
226
- * // Observar todo el estado
227
- * slice.context.watch('auth', this, (state) => {
228
- * this.render(state);
229
- * });
230
- *
231
- * // Observar un campo especifico
232
- * slice.context.watch('auth', this, (name) => {
233
- * this.$name.textContent = name;
234
- * }, state => state.user.name);
235
- *
236
- * // Observar multiples campos
237
- * slice.context.watch('auth', this, (data) => {
238
- * this.$name.textContent = data.name;
239
- * this.$avatar.src = data.avatar;
240
- * }, state => ({
241
- * name: state.user.name,
242
- * avatar: state.user.avatar
243
- * }));
244
- *
245
- * // Valores computados
246
- * slice.context.watch('cart', this, (total) => {
247
- * this.$total.textContent = `${total}`;
248
- * }, state => state.items.reduce((sum, i) => sum + i.price, 0));
249
- */
250
- watch(name, component, callback, selector = null) {
251
- if (!this.contexts.has(name)) {
252
- slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
253
- return null;
254
- }
255
-
256
- if (!component?.sliceId) {
257
- slice.logger.logError('ContextManager', 'watch() requiere un componente Slice válido');
258
- return null;
259
- }
260
-
261
- if (typeof callback !== 'function') {
262
- slice.logger.logError('ContextManager', 'El callback debe ser una función');
263
- return null;
264
- }
265
-
266
- // Guardar el valor anterior del selector para comparar
267
- let lastSelectedValue = selector ? this._getSelectedValue(name, selector) : this.getState(name);
268
-
269
- // Crear wrapper que aplica el selector y compara
270
- const wrappedCallback = (newState, prevState) => {
271
- if (selector) {
272
- const newSelectedValue = this._applySelector(newState, selector);
273
- const prevSelectedValue = this._applySelector(prevState, selector);
274
-
275
- // Solo ejecutar si el valor seleccionado cambió
276
- if (!this._isEqual(newSelectedValue, prevSelectedValue)) {
277
- lastSelectedValue = newSelectedValue;
278
- callback(newSelectedValue);
279
- }
280
- } else {
281
- // Sin selector, siempre ejecutar
282
- callback(newState);
283
- }
284
- };
285
-
286
- // Suscribirse al evento del contexto
287
- const subscriptionId = slice.events.subscribe(`context:${name}`, wrappedCallback, { component });
288
-
289
- slice.logger.logInfo('ContextManager', `Watch registrado en "${name}" para ${component.sliceId}`);
290
-
291
- return subscriptionId;
292
- }
293
-
294
- // ============================================
295
- // UTILIDADES
296
- // ============================================
297
-
298
- /**
299
- * Verificar si un contexto existe
300
- * @param {string} name - Nombre del contexto
301
- * @returns {boolean}
302
- */
303
- has(name) {
304
- return this.contexts.has(name);
305
- }
306
-
307
- /**
308
- * Eliminar un contexto
309
- * @param {string} name - Nombre del contexto
310
- * @returns {boolean}
311
- */
312
- destroy(name) {
313
- if (!this.contexts.has(name)) {
314
- return false;
315
- }
316
-
317
- const context = this.contexts.get(name);
318
-
319
- // Limpiar storage si existe
320
- if (context.options.persist) {
321
- this._removeFromStorage(context.options.storageKey);
322
- }
323
-
324
- this.contexts.delete(name);
325
-
326
- slice.logger.logInfo('ContextManager', `Contexto "${name}" eliminado`);
327
-
328
- return true;
329
- }
330
-
331
- /**
332
- * Obtener lista de todos los contextos
333
- * @returns {string[]}
334
- */
335
- list() {
336
- return Array.from(this.contexts.keys());
337
- }
338
-
339
- // ============================================
340
- // MÉTODOS PRIVADOS
341
- // ============================================
342
-
343
- /**
344
- * Aplicar selector al estado
345
- */
346
- _applySelector(state, selector) {
347
- try {
348
- return selector(state);
349
- } catch (error) {
350
- slice.logger.error('ContextManager', 'Error applying selector', error);
351
- throw error;
352
- }
353
- }
354
-
355
- /**
356
- * Obtener valor seleccionado del estado actual
357
- */
358
- _getSelectedValue(name, selector) {
359
- const state = this.getState(name);
360
- return this._applySelector(state, selector);
361
- }
362
-
363
- /**
364
- * Comparar dos valores (shallow)
365
- */
366
- _isEqual(a, b) {
367
- // Mismo valor primitivo o referencia
368
- if (a === b) return true;
369
-
370
- // Si alguno es null/undefined
371
- if (a == null || b == null) return false;
372
-
373
- // Si no son objetos, ya sabemos que son diferentes
374
- if (typeof a !== 'object' || typeof b !== 'object') return false;
375
-
376
- // Comparación shallow de objetos
377
- const keysA = Object.keys(a);
378
- const keysB = Object.keys(b);
379
-
380
- if (keysA.length !== keysB.length) return false;
381
-
382
- for (const key of keysA) {
383
- if (a[key] !== b[key]) return false;
384
- }
385
-
386
- return true;
387
- }
388
-
389
- /**
390
- * Cargar estado desde localStorage
391
- */
392
- _loadFromStorage(storageKey) {
393
- try {
394
- const data = localStorage.getItem(storageKey);
395
- if (data) {
396
- return JSON.parse(data);
397
- }
398
- } catch (error) {
399
- slice.logger.error('ContextManager', `Error loading data from localStorage "${storageKey}"`, error);
400
- }
401
- return null;
402
- }
403
-
404
- _saveToStorage(name, state, storageKey) {
405
- try {
406
- localStorage.setItem(storageKey, JSON.stringify(state));
407
- } catch (error) {
408
- slice.logger.error('ContextManager', `Error saving "${name}" to localStorage`, error);
409
- }
410
- }
411
-
412
- /**
413
- * Eliminar estado de localStorage
414
- */
415
- _removeFromStorage(storageKey) {
416
- try {
417
- localStorage.removeItem(storageKey);
418
- } catch (error) {
419
- slice.logger.warn('ContextManager', `Error removing "${storageKey}" from localStorage`, error);
420
- }
421
- }
422
- }
1
+ /**
2
+ * ContextManager - Sistema de contextos compartidos para Slice.js
3
+ * Ubicación: /Slice/Components/Structural/ContextManager/ContextManager.js
4
+ *
5
+ * Características:
6
+ * - Estado compartido entre componentes
7
+ * - Usa EventManager internamente para notificaciones
8
+ * - Selectores para observar campos específicos
9
+ * - Auto-limpieza cuando componentes se destruyen
10
+ * - Persistencia opcional en localStorage
11
+ */
12
+ /**
13
+ * @typedef {Object} ContextOptions
14
+ * @property {boolean} [persist]
15
+ * @property {string} [storageKey]
16
+ */
17
+
18
+ export default class ContextManager {
19
+ constructor() {
20
+ // Map<contextName, { state, options }>
21
+ this.contexts = new Map();
22
+ }
23
+
24
+ init() {
25
+ return true;
26
+ }
27
+
28
+ // ============================================
29
+ // CREAR CONTEXTO
30
+ // ============================================
31
+
32
+ /**
33
+ * Crear un nuevo contexto
34
+ * @param {string} name - Nombre unico del contexto
35
+ * @param {Object} [initialState] - Estado inicial
36
+ * @param {ContextOptions} [options] - Opciones: { persist, storageKey }
37
+ * @returns {boolean}
38
+ *
39
+ * @example
40
+ * slice.context.create('auth', {
41
+ * isLoggedIn: false,
42
+ * user: null
43
+ * });
44
+ *
45
+ * // Con persistencia en localStorage
46
+ * slice.context.create('preferences', {
47
+ * theme: 'light',
48
+ * language: 'es'
49
+ * }, { persist: true });
50
+ */
51
+ create(name, initialState = {}, options = {}) {
52
+ if (this.contexts.has(name)) {
53
+ slice.logger.logWarning('ContextManager', `El contexto "${name}" ya existe`);
54
+ return false;
55
+ }
56
+
57
+ // Cargar estado persistido si existe
58
+ let state = initialState;
59
+ if (options.persist) {
60
+ const storageKey = options.storageKey || `slice_context_${name}`;
61
+ const persisted = this._loadFromStorage(storageKey);
62
+ if (persisted !== null) {
63
+ state = persisted;
64
+ }
65
+ }
66
+
67
+ this.contexts.set(name, {
68
+ state,
69
+ options: {
70
+ persist: options.persist || false,
71
+ storageKey: options.storageKey || `slice_context_${name}`,
72
+ },
73
+ });
74
+
75
+ slice.events.emit('context:__created', { name, state });
76
+
77
+ slice.logger.logInfo('ContextManager', `Contexto "${name}" creado`);
78
+
79
+ return true;
80
+ }
81
+
82
+ // ============================================
83
+ // LEER ESTADO
84
+ // ============================================
85
+
86
+ /**
87
+ * Obtener el estado actual de un contexto
88
+ * @param {string} name - Nombre del contexto
89
+ * @returns {any|null} Estado actual o null si no existe
90
+ *
91
+ * @example
92
+ * const auth = slice.context.getState('auth');
93
+ * console.log(auth.user.name);
94
+ */
95
+ getState(name) {
96
+ if (!this.contexts.has(name)) {
97
+ slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
98
+ return null;
99
+ }
100
+
101
+ return this.contexts.get(name).state;
102
+ }
103
+
104
+ // ============================================
105
+ // ACTUALIZAR ESTADO
106
+ // ============================================
107
+
108
+ /**
109
+ * Actualizar el estado de un contexto
110
+ * @param {string} name - Nombre del contexto
111
+ * @param {Object|Function} updater - Nuevo estado o funcion (prevState) => newState
112
+ * @returns {void}
113
+ *
114
+ * @example
115
+ * // Reemplazar con objeto
116
+ * slice.context.setState('auth', {
117
+ * isLoggedIn: true,
118
+ * user: { name: 'Juan' }
119
+ * });
120
+ *
121
+ * // Usar funcion para acceder al estado anterior
122
+ * slice.context.setState('cart', (prev) => ({
123
+ * ...prev,
124
+ * items: [...prev.items, nuevoProducto],
125
+ * total: prev.total + nuevoProducto.price
126
+ * }));
127
+ */
128
+ setState(name, updater) {
129
+ if (!this.contexts.has(name)) {
130
+ slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
131
+ return;
132
+ }
133
+
134
+ const context = this.contexts.get(name);
135
+ const prevState = context.state;
136
+
137
+ // Calcular nuevo estado
138
+ let newState;
139
+ if (typeof updater === 'function') {
140
+ newState = updater(prevState);
141
+ } else {
142
+ newState = updater;
143
+ }
144
+
145
+ // Guardar nuevo estado
146
+ context.state = newState;
147
+
148
+ // Persistir si está habilitado
149
+ if (context.options.persist) {
150
+ this._saveToStorage(name, newState, context.options.storageKey);
151
+ }
152
+
153
+ // Emitir evento para notificar a los watchers
154
+ slice.events.emit(`context:${name}`, newState, prevState);
155
+
156
+ slice.logger.logInfo('ContextManager', `Contexto "${name}" actualizado`);
157
+ }
158
+
159
+ /**
160
+ * Merge a partial object into the context's state (first-level merge).
161
+ * `setState` REPLACES the whole state; `patch` keeps the existing fields and
162
+ * overrides only the keys you pass — the common "update one field" case.
163
+ * @param {string} name - Nombre del contexto
164
+ * @param {Object} partial - Campos a fusionar sobre el estado actual
165
+ * @returns {void}
166
+ *
167
+ * @example
168
+ * slice.context.patch('cart', { discount: 0.1 }); // items/total se conservan
169
+ */
170
+ patch(name, partial) {
171
+ if (!this.contexts.has(name)) {
172
+ slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
173
+ return;
174
+ }
175
+ if (!partial || typeof partial !== 'object' || Array.isArray(partial)) {
176
+ slice.logger.logError('ContextManager', `patch("${name}") requiere un objeto parcial`);
177
+ return;
178
+ }
179
+ return this.setState(name, (prev) => ({ ...prev, ...partial }));
180
+ }
181
+
182
+ /**
183
+ * Return a handle bound to a single context so you call its methods without
184
+ * repeating the name on every call.
185
+ * @param {string} name - Nombre del contexto
186
+ * @returns {{ get: Function, set: Function, patch: Function, watch: Function, bind: Function, has: Function, destroy: Function }}
187
+ *
188
+ * @example
189
+ * const cart = slice.context.use('cart');
190
+ * cart.get(); // estado actual
191
+ * cart.patch({ discount: 0.1 }); // merge (conserva el resto)
192
+ * cart.watch(this, (s) => this.render(s));
193
+ * // watch + render inicial en una línea:
194
+ * cart.bind(this, (count) => { this.$badge.textContent = count; }, (s) => s.items.length);
195
+ */
196
+ use(name) {
197
+ return {
198
+ get: () => this.getState(name),
199
+ set: (updater) => this.setState(name, updater),
200
+ patch: (partial) => this.patch(name, partial),
201
+ watch: (component, callback, selector = null) => this.watch(name, component, callback, selector),
202
+ // watch + an immediate call with the current value (initial render in one line)
203
+ bind: (component, callback, selector = null) => {
204
+ const state = this.getState(name);
205
+ callback(selector ? selector(state) : state);
206
+ return this.watch(name, component, callback, selector);
207
+ },
208
+ has: () => this.has(name),
209
+ destroy: () => this.destroy(name),
210
+ };
211
+ }
212
+
213
+ // ============================================
214
+ // WATCH (OBSERVAR CAMBIOS)
215
+ // ============================================
216
+
217
+ /**
218
+ * Observar cambios en un contexto
219
+ * @param {string} name - Nombre del contexto
220
+ * @param {HTMLElement} component - Componente Slice para auto-cleanup
221
+ * @param {(value: any) => void} callback - Funcion a ejecutar cuando cambia
222
+ * @param {(state: any) => any} [selector] - Opcional. Funcion para seleccionar campos
223
+ * @returns {string|null} subscriptionId
224
+ *
225
+ * @example
226
+ * // Observar todo el estado
227
+ * slice.context.watch('auth', this, (state) => {
228
+ * this.render(state);
229
+ * });
230
+ *
231
+ * // Observar un campo especifico
232
+ * slice.context.watch('auth', this, (name) => {
233
+ * this.$name.textContent = name;
234
+ * }, state => state.user.name);
235
+ *
236
+ * // Observar multiples campos
237
+ * slice.context.watch('auth', this, (data) => {
238
+ * this.$name.textContent = data.name;
239
+ * this.$avatar.src = data.avatar;
240
+ * }, state => ({
241
+ * name: state.user.name,
242
+ * avatar: state.user.avatar
243
+ * }));
244
+ *
245
+ * // Valores computados
246
+ * slice.context.watch('cart', this, (total) => {
247
+ * this.$total.textContent = `${total}`;
248
+ * }, state => state.items.reduce((sum, i) => sum + i.price, 0));
249
+ */
250
+ watch(name, component, callback, selector = null) {
251
+ if (!this.contexts.has(name)) {
252
+ slice.logger.logError('ContextManager', `El contexto "${name}" no existe`);
253
+ return null;
254
+ }
255
+
256
+ if (!component?.sliceId) {
257
+ slice.logger.logError('ContextManager', 'watch() requiere un componente Slice válido');
258
+ return null;
259
+ }
260
+
261
+ if (typeof callback !== 'function') {
262
+ slice.logger.logError('ContextManager', 'El callback debe ser una función');
263
+ return null;
264
+ }
265
+
266
+ // Guardar el valor anterior del selector para comparar
267
+ let lastSelectedValue = selector ? this._getSelectedValue(name, selector) : this.getState(name);
268
+
269
+ // Crear wrapper que aplica el selector y compara
270
+ const wrappedCallback = (newState, prevState) => {
271
+ if (selector) {
272
+ const newSelectedValue = this._applySelector(newState, selector);
273
+ const prevSelectedValue = this._applySelector(prevState, selector);
274
+
275
+ // Solo ejecutar si el valor seleccionado cambió
276
+ if (!this._isEqual(newSelectedValue, prevSelectedValue)) {
277
+ lastSelectedValue = newSelectedValue;
278
+ callback(newSelectedValue);
279
+ }
280
+ } else {
281
+ // Sin selector, siempre ejecutar
282
+ callback(newState);
283
+ }
284
+ };
285
+
286
+ // Suscribirse al evento del contexto
287
+ const subscriptionId = slice.events.subscribe(`context:${name}`, wrappedCallback, { component });
288
+
289
+ slice.logger.logInfo('ContextManager', `Watch registrado en "${name}" para ${component.sliceId}`);
290
+
291
+ return subscriptionId;
292
+ }
293
+
294
+ // ============================================
295
+ // UTILIDADES
296
+ // ============================================
297
+
298
+ /**
299
+ * Verificar si un contexto existe
300
+ * @param {string} name - Nombre del contexto
301
+ * @returns {boolean}
302
+ */
303
+ has(name) {
304
+ return this.contexts.has(name);
305
+ }
306
+
307
+ /**
308
+ * Eliminar un contexto
309
+ * @param {string} name - Nombre del contexto
310
+ * @returns {boolean}
311
+ */
312
+ destroy(name) {
313
+ if (!this.contexts.has(name)) {
314
+ return false;
315
+ }
316
+
317
+ const context = this.contexts.get(name);
318
+
319
+ // Limpiar storage si existe
320
+ if (context.options.persist) {
321
+ this._removeFromStorage(context.options.storageKey);
322
+ }
323
+
324
+ this.contexts.delete(name);
325
+
326
+ slice.logger.logInfo('ContextManager', `Contexto "${name}" eliminado`);
327
+
328
+ return true;
329
+ }
330
+
331
+ /**
332
+ * Obtener lista de todos los contextos
333
+ * @returns {string[]}
334
+ */
335
+ list() {
336
+ return Array.from(this.contexts.keys());
337
+ }
338
+
339
+ // ============================================
340
+ // MÉTODOS PRIVADOS
341
+ // ============================================
342
+
343
+ /**
344
+ * Aplicar selector al estado
345
+ */
346
+ _applySelector(state, selector) {
347
+ try {
348
+ return selector(state);
349
+ } catch (error) {
350
+ slice.logger.error('ContextManager', 'Error applying selector', error);
351
+ throw error;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Obtener valor seleccionado del estado actual
357
+ */
358
+ _getSelectedValue(name, selector) {
359
+ const state = this.getState(name);
360
+ return this._applySelector(state, selector);
361
+ }
362
+
363
+ /**
364
+ * Comparar dos valores (shallow)
365
+ */
366
+ _isEqual(a, b) {
367
+ // Mismo valor primitivo o referencia
368
+ if (a === b) return true;
369
+
370
+ // Si alguno es null/undefined
371
+ if (a == null || b == null) return false;
372
+
373
+ // Si no son objetos, ya sabemos que son diferentes
374
+ if (typeof a !== 'object' || typeof b !== 'object') return false;
375
+
376
+ // Comparación shallow de objetos
377
+ const keysA = Object.keys(a);
378
+ const keysB = Object.keys(b);
379
+
380
+ if (keysA.length !== keysB.length) return false;
381
+
382
+ for (const key of keysA) {
383
+ if (a[key] !== b[key]) return false;
384
+ }
385
+
386
+ return true;
387
+ }
388
+
389
+ /**
390
+ * Cargar estado desde localStorage
391
+ */
392
+ _loadFromStorage(storageKey) {
393
+ try {
394
+ const data = localStorage.getItem(storageKey);
395
+ if (data) {
396
+ return JSON.parse(data);
397
+ }
398
+ } catch (error) {
399
+ slice.logger.error('ContextManager', `Error loading data from localStorage "${storageKey}"`, error);
400
+ }
401
+ return null;
402
+ }
403
+
404
+ _saveToStorage(name, state, storageKey) {
405
+ try {
406
+ localStorage.setItem(storageKey, JSON.stringify(state));
407
+ } catch (error) {
408
+ slice.logger.error('ContextManager', `Error saving "${name}" to localStorage`, error);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Eliminar estado de localStorage
414
+ */
415
+ _removeFromStorage(storageKey) {
416
+ try {
417
+ localStorage.removeItem(storageKey);
418
+ } catch (error) {
419
+ slice.logger.warn('ContextManager', `Error removing "${storageKey}" from localStorage`, error);
420
+ }
421
+ }
422
+ }