slicejs-web-framework 3.2.2 → 3.3.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.
Files changed (36) hide show
  1. package/.opencode/opencode.json +14 -0
  2. package/README.md +57 -134
  3. package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +233 -110
  4. package/Slice/Components/Structural/Controller/Controller.js +9 -0
  5. package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -0
  6. package/Slice/Components/Structural/Debugger/Debugger.js +392 -442
  7. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +264 -149
  8. package/Slice/Components/Structural/Router/Router.js +45 -14
  9. package/Slice/tests/props-allowed-values-validation.test.js +119 -0
  10. package/package.json +11 -9
  11. package/src/App/index.html +2 -8
  12. package/src/App/index.js +18 -21
  13. package/src/App/style.css +8 -37
  14. package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -0
  15. package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -0
  16. package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -0
  17. package/src/Components/AppComponents/AppShell/AppShell.css +10 -0
  18. package/src/Components/AppComponents/AppShell/AppShell.html +4 -0
  19. package/src/Components/AppComponents/AppShell/AppShell.js +36 -0
  20. package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -0
  21. package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -0
  22. package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -0
  23. package/src/Components/Visual/MultiRoute/MultiRoute.js +13 -6
  24. package/src/Components/components.js +4 -16
  25. package/src/routes.js +6 -12
  26. package/src/sliceConfig.json +2 -1
  27. package/Slice/Components/Structural/Debugger/Debugger.css +0 -620
  28. package/src/Components/AppComponents/HomePage/HomePage.css +0 -201
  29. package/src/Components/AppComponents/HomePage/HomePage.html +0 -37
  30. package/src/Components/AppComponents/HomePage/HomePage.js +0 -210
  31. package/src/Components/AppComponents/Playground/Playground.css +0 -12
  32. package/src/Components/AppComponents/Playground/Playground.html +0 -0
  33. package/src/Components/AppComponents/Playground/Playground.js +0 -111
  34. package/src/images/Slice.js-logo.png +0 -0
  35. package/src/images/im2/Slice.js-logo.png +0 -0
  36. package/src/testing.js +0 -888
@@ -173,15 +173,19 @@ export default class EventManagerDebugger extends HTMLElement {
173
173
  return `
174
174
  <div id="events-debugger">
175
175
  <div class="events-header">
176
- <div class="title">Events</div>
176
+ <div class="brand">
177
+ <span class="status-dot"></span>
178
+ <span class="glyph">◇</span>
179
+ <span class="title">EVENTS</span>
180
+ </div>
177
181
  <div class="actions">
178
- <button id="events-refresh" class="btn">Refresh</button>
179
- <button id="events-close" class="btn">Close</button>
182
+ <button id="events-refresh" class="btn" title="Refresh" aria-label="Refresh">⟳</button>
183
+ <button id="events-close" class="btn" title="Close" aria-label="Close">✕</button>
180
184
  </div>
181
185
  </div>
182
186
  <div class="events-toolbar">
183
- <input id="events-filter" type="text" placeholder="Filter events" />
184
- <div class="count">Total: <span id="events-count">0</span></div>
187
+ <input id="events-filter" type="text" placeholder="filter events" autocomplete="off" spellcheck="false" />
188
+ <div class="count"><span id="events-count">0</span></div>
185
189
  </div>
186
190
  <div class="events-list" id="events-list"></div>
187
191
  </div>
@@ -190,170 +194,281 @@ export default class EventManagerDebugger extends HTMLElement {
190
194
 
191
195
  renderStyles() {
192
196
  return `
193
- #events-debugger {
194
- position: fixed;
195
- bottom: 20px;
196
- right: 20px;
197
- width: min(360px, calc(100vw - 40px));
198
- max-height: 60vh;
199
- background: var(--primary-background-color);
200
- border: 1px solid var(--medium-color);
201
- border-radius: 12px;
202
- box-shadow: 0 16px 32px rgba(0, 0, 0, 0.15);
203
- display: none;
204
- flex-direction: column;
205
- z-index: 10001;
206
- overflow: hidden;
207
- }
197
+ /* Slice Instruments — events console. All selectors scoped to the
198
+ <slice-eventmanager-debugger> tag so nothing clashes with app styles. */
199
+ slice-eventmanager-debugger {
200
+ --si-accent: var(--primary-color, #6ee7ff);
201
+ --si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
202
+ --si-surface: rgba(17, 19, 28, 0.86);
203
+ --si-raised: rgba(255, 255, 255, 0.035);
204
+ --si-raised-2: rgba(255, 255, 255, 0.06);
205
+ --si-border: rgba(255, 255, 255, 0.09);
206
+ --si-text: #e8eaf2;
207
+ --si-dim: #888fa6;
208
+ --si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
209
+ }
208
210
 
209
- #events-debugger.active {
210
- display: flex;
211
- }
211
+ slice-eventmanager-debugger #events-debugger {
212
+ position: fixed;
213
+ bottom: 20px;
214
+ right: 20px;
215
+ width: min(380px, calc(100vw - 40px));
216
+ max-height: 64vh;
217
+ background: var(--si-surface);
218
+ border: 1px solid var(--si-border);
219
+ border-radius: 14px;
220
+ box-shadow:
221
+ 0 24px 60px -12px rgba(0, 0, 0, 0.55),
222
+ 0 0 0 1px rgba(0, 0, 0, 0.2),
223
+ 0 0 38px -18px rgba(var(--si-accent-rgb), 0.55);
224
+ -webkit-backdrop-filter: blur(22px) saturate(1.3);
225
+ backdrop-filter: blur(22px) saturate(1.3);
226
+ display: none;
227
+ flex-direction: column;
228
+ z-index: 10001;
229
+ overflow: hidden;
230
+ color: var(--si-text);
231
+ font-family: var(--si-mono);
232
+ }
212
233
 
213
- #events-debugger * {
214
- box-sizing: border-box;
215
- }
234
+ slice-eventmanager-debugger #events-debugger.active {
235
+ display: flex;
236
+ animation: si-events-in 0.26s cubic-bezier(0.16, 1, 0.3, 1);
237
+ }
216
238
 
217
- .events-header {
218
- display: flex;
219
- justify-content: space-between;
220
- align-items: center;
221
- padding: 12px 14px;
222
- background: var(--tertiary-background-color);
223
- border-bottom: 1px solid var(--medium-color);
224
- user-select: none;
225
- }
239
+ @keyframes si-events-in {
240
+ from { opacity: 0; transform: translateY(10px) scale(0.985); }
241
+ to { opacity: 1; transform: translateY(0) scale(1); }
242
+ }
226
243
 
227
- .events-header .title {
228
- font-weight: 600;
229
- color: var(--font-primary-color);
230
- }
244
+ slice-eventmanager-debugger #events-debugger * { box-sizing: border-box; }
231
245
 
232
- .events-header .actions {
233
- display: flex;
234
- gap: 8px;
235
- }
246
+ slice-eventmanager-debugger #events-debugger::before {
247
+ content: '';
248
+ position: absolute;
249
+ left: 0; top: 0; bottom: 0;
250
+ width: 2px;
251
+ background: linear-gradient(180deg, var(--si-accent), transparent 70%);
252
+ opacity: 0.85;
253
+ pointer-events: none;
254
+ }
236
255
 
237
- .events-header .btn {
238
- padding: 6px 10px;
239
- border-radius: 6px;
240
- border: 1px solid var(--medium-color);
241
- background: var(--primary-background-color);
242
- color: var(--font-primary-color);
243
- cursor: pointer;
244
- font-size: 12px;
245
- }
256
+ slice-eventmanager-debugger .events-header {
257
+ display: flex;
258
+ justify-content: space-between;
259
+ align-items: center;
260
+ padding: 12px 14px;
261
+ background:
262
+ radial-gradient(120% 140% at 0% 0%, rgba(var(--si-accent-rgb), 0.10), transparent 60%),
263
+ var(--si-raised);
264
+ border-bottom: 1px solid var(--si-border);
265
+ user-select: none;
266
+ }
246
267
 
247
- .events-toolbar {
248
- display: flex;
249
- gap: 10px;
250
- align-items: center;
251
- padding: 10px 12px;
252
- border-bottom: 1px solid var(--medium-color);
253
- }
268
+ slice-eventmanager-debugger .brand { display: flex; align-items: center; gap: 9px; }
254
269
 
255
- .events-toolbar input {
256
- flex: 1;
257
- min-width: 0;
258
- padding: 6px 8px;
259
- border-radius: 6px;
260
- border: 1px solid var(--medium-color);
261
- background: var(--primary-background-color);
262
- color: var(--font-primary-color);
263
- }
270
+ slice-eventmanager-debugger .status-dot {
271
+ width: 7px; height: 7px;
272
+ border-radius: 50%;
273
+ background: var(--si-accent);
274
+ animation: si-pulse-ev 2.4s ease-out infinite;
275
+ }
264
276
 
265
- .events-list {
266
- padding: 10px 12px;
267
- overflow: auto;
268
- display: flex;
269
- flex-direction: column;
270
- gap: 8px;
271
- }
277
+ @keyframes si-pulse-ev {
278
+ 0% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0.55); }
279
+ 70% { box-shadow: 0 0 0 7px rgba(var(--si-accent-rgb), 0); }
280
+ 100% { box-shadow: 0 0 0 0 rgba(var(--si-accent-rgb), 0); }
281
+ }
272
282
 
273
- .event-row {
274
- display: block;
275
- padding: 8px 10px;
276
- background: var(--tertiary-background-color);
277
- border-radius: 6px;
278
- border: 1px solid var(--medium-color);
279
- }
283
+ slice-eventmanager-debugger .glyph { color: var(--si-accent); font-size: 12px; opacity: 0.9; }
280
284
 
281
- .event-row summary {
282
- display: flex;
283
- align-items: center;
284
- justify-content: space-between;
285
- gap: 8px;
286
- cursor: pointer;
287
- list-style: none;
288
- }
285
+ slice-eventmanager-debugger .title {
286
+ font-weight: 600;
287
+ font-size: 11px;
288
+ letter-spacing: 0.18em;
289
+ color: var(--si-text);
290
+ }
289
291
 
290
- .event-row summary::-webkit-details-marker {
291
- display: none;
292
- }
292
+ slice-eventmanager-debugger .actions { display: flex; gap: 6px; }
293
+
294
+ slice-eventmanager-debugger .btn {
295
+ width: 26px; height: 26px;
296
+ display: flex; align-items: center; justify-content: center;
297
+ border-radius: 7px;
298
+ border: 1px solid var(--si-border);
299
+ background: var(--si-raised);
300
+ color: var(--si-dim);
301
+ cursor: pointer;
302
+ font-size: 13px;
303
+ line-height: 1;
304
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
305
+ }
306
+ slice-eventmanager-debugger .btn:hover {
307
+ color: var(--si-text);
308
+ background: var(--si-raised-2);
309
+ border-color: rgba(var(--si-accent-rgb), 0.5);
310
+ }
311
+ slice-eventmanager-debugger .btn:active { transform: scale(0.92); }
312
+ slice-eventmanager-debugger #events-refresh:hover { color: var(--si-accent); }
313
+
314
+ slice-eventmanager-debugger .events-toolbar {
315
+ display: flex;
316
+ gap: 10px;
317
+ align-items: center;
318
+ padding: 10px 12px;
319
+ border-bottom: 1px solid var(--si-border);
320
+ }
293
321
 
294
- .event-name {
295
- font-family: monospace;
296
- font-size: 12px;
297
- color: var(--font-primary-color);
298
- overflow: hidden;
299
- text-overflow: ellipsis;
300
- white-space: nowrap;
301
- }
322
+ slice-eventmanager-debugger .events-toolbar input {
323
+ flex: 1;
324
+ min-width: 0;
325
+ padding: 7px 10px 7px 30px;
326
+ border-radius: 8px;
327
+ border: 1px solid var(--si-border);
328
+ background:
329
+ var(--si-raised) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%23888fa6' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat 10px center;
330
+ color: var(--si-text);
331
+ font-family: var(--si-mono);
332
+ font-size: 12px;
333
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
334
+ }
335
+ slice-eventmanager-debugger .events-toolbar input::placeholder { color: var(--si-dim); }
336
+ slice-eventmanager-debugger .events-toolbar input:focus {
337
+ outline: none;
338
+ border-color: rgba(var(--si-accent-rgb), 0.6);
339
+ box-shadow: 0 0 0 3px rgba(var(--si-accent-rgb), 0.12);
340
+ }
302
341
 
303
- .event-count {
304
- font-weight: 600;
305
- color: var(--primary-color);
306
- }
342
+ slice-eventmanager-debugger .events-toolbar .count { font-size: 11px; color: var(--si-dim); min-width: 22px; text-align: center; }
343
+ slice-eventmanager-debugger .events-toolbar .count span { color: var(--si-accent); font-weight: 600; }
307
344
 
308
- .subscriber-list {
309
- margin-top: 10px;
310
- display: flex;
311
- flex-direction: column;
312
- gap: 8px;
313
- }
345
+ slice-eventmanager-debugger .events-list {
346
+ padding: 10px 12px 12px;
347
+ overflow: auto;
348
+ display: flex;
349
+ flex-direction: column;
350
+ gap: 7px;
351
+ }
352
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar { width: 8px; }
353
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb {
354
+ background: var(--si-raised-2);
355
+ border-radius: 8px;
356
+ border: 2px solid transparent;
357
+ background-clip: padding-box;
358
+ }
359
+ slice-eventmanager-debugger .events-list::-webkit-scrollbar-thumb:hover { background: rgba(var(--si-accent-rgb), 0.4); background-clip: padding-box; }
360
+
361
+ slice-eventmanager-debugger .event-row {
362
+ display: block;
363
+ padding: 9px 11px;
364
+ background: var(--si-raised);
365
+ border-radius: 9px;
366
+ border: 1px solid var(--si-border);
367
+ border-left: 2px solid transparent;
368
+ transition: border-color 0.18s ease, background 0.18s ease;
369
+ }
370
+ slice-eventmanager-debugger .event-row:hover { background: var(--si-raised-2); border-left-color: var(--si-accent); }
371
+
372
+ slice-eventmanager-debugger .event-row summary {
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: space-between;
376
+ gap: 10px;
377
+ cursor: pointer;
378
+ list-style: none;
379
+ }
380
+ slice-eventmanager-debugger .event-row summary::-webkit-details-marker { display: none; }
381
+ slice-eventmanager-debugger .event-row summary::after {
382
+ content: '›';
383
+ margin-left: auto;
384
+ color: var(--si-dim);
385
+ font-size: 15px;
386
+ line-height: 1;
387
+ transition: transform 0.2s ease, color 0.2s ease;
388
+ }
389
+ slice-eventmanager-debugger .event-row[open] summary::after { transform: rotate(90deg); color: var(--si-accent); }
390
+
391
+ slice-eventmanager-debugger .event-name {
392
+ font-family: var(--si-mono);
393
+ font-size: 12px;
394
+ color: var(--si-text);
395
+ overflow: hidden;
396
+ text-overflow: ellipsis;
397
+ white-space: nowrap;
398
+ }
314
399
 
315
- .subscriber-row {
316
- display: flex;
317
- justify-content: space-between;
318
- gap: 8px;
319
- padding: 6px 8px;
320
- border-radius: 6px;
321
- background: var(--primary-background-color);
322
- border: 1px solid var(--medium-color);
323
- }
400
+ slice-eventmanager-debugger .event-count {
401
+ font-weight: 600;
402
+ font-size: 11px;
403
+ color: var(--si-accent);
404
+ background: rgba(var(--si-accent-rgb), 0.12);
405
+ border: 1px solid rgba(var(--si-accent-rgb), 0.25);
406
+ padding: 1px 8px;
407
+ border-radius: 999px;
408
+ min-width: 22px;
409
+ text-align: center;
410
+ }
324
411
 
325
- .subscriber-name {
326
- font-size: 12px;
327
- color: var(--font-primary-color);
328
- overflow: hidden;
329
- text-overflow: ellipsis;
330
- white-space: nowrap;
331
- }
412
+ slice-eventmanager-debugger .subscriber-list {
413
+ margin-top: 9px;
414
+ padding-top: 9px;
415
+ border-top: 1px dashed var(--si-border);
416
+ display: flex;
417
+ flex-direction: column;
418
+ gap: 6px;
419
+ }
332
420
 
333
- .subscriber-meta {
334
- font-size: 11px;
335
- color: var(--font-secondary-color);
336
- display: flex;
337
- align-items: center;
338
- gap: 6px;
339
- white-space: nowrap;
340
- }
421
+ slice-eventmanager-debugger .subscriber-row {
422
+ display: flex;
423
+ justify-content: space-between;
424
+ align-items: center;
425
+ gap: 10px;
426
+ padding: 6px 9px;
427
+ border-radius: 7px;
428
+ background: rgba(0, 0, 0, 0.22);
429
+ border: 1px solid var(--si-border);
430
+ }
341
431
 
342
- .badge {
343
- padding: 2px 6px;
344
- border-radius: 999px;
345
- background: var(--secondary-color);
346
- color: var(--secondary-color-contrast);
347
- font-size: 10px;
348
- text-transform: uppercase;
349
- }
432
+ slice-eventmanager-debugger .subscriber-name {
433
+ font-size: 11.5px;
434
+ color: var(--si-text);
435
+ overflow: hidden;
436
+ text-overflow: ellipsis;
437
+ white-space: nowrap;
438
+ }
350
439
 
351
- .empty {
352
- color: var(--font-secondary-color);
353
- font-size: 12px;
354
- text-align: center;
355
- padding: 12px 0;
356
- }
440
+ slice-eventmanager-debugger .subscriber-meta {
441
+ font-size: 10.5px;
442
+ color: var(--si-dim);
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 6px;
446
+ white-space: nowrap;
447
+ }
448
+
449
+ slice-eventmanager-debugger .badge {
450
+ padding: 1px 6px;
451
+ border-radius: 999px;
452
+ background: rgba(var(--si-accent-rgb), 0.16);
453
+ color: var(--si-accent);
454
+ border: 1px solid rgba(var(--si-accent-rgb), 0.3);
455
+ font-size: 9px;
456
+ letter-spacing: 0.06em;
457
+ text-transform: uppercase;
458
+ }
459
+
460
+ slice-eventmanager-debugger .empty {
461
+ color: var(--si-dim);
462
+ font-size: 11px;
463
+ letter-spacing: 0.04em;
464
+ text-align: center;
465
+ padding: 22px 0;
466
+ }
467
+
468
+ @media (prefers-reduced-motion: reduce) {
469
+ slice-eventmanager-debugger #events-debugger.active { animation: none; }
470
+ slice-eventmanager-debugger .status-dot { animation: none; }
471
+ }
357
472
  `;
358
473
  }
359
474
  }
@@ -298,24 +298,42 @@ export default class Router {
298
298
  // ============================================
299
299
 
300
300
  /**
301
- * Navigate to a route path with guards support. Add replace to do router.replace() instead of push.
301
+ * Navigate to a route path (guards run automatically).
302
302
  * @param {string} path
303
- * @param {string[]} [_redirectChain]
304
- * @param {{ replace?: boolean }} [_options]
303
+ * @param {{ replace?: boolean }} [options] - `{ replace: true }` replaces history instead of pushing.
305
304
  * @returns {Promise<void>}
306
305
  */
307
- async navigate(path, _redirectChain = [], _options = {}) {
306
+ async navigate(path, options = {}, legacyOptions) {
307
+ // Backward compatibility with the previous signature navigate(path, _redirectChain, _options):
308
+ // if the 2nd argument is the internal redirect chain (an array) or a 3rd argument is passed,
309
+ // use the 3rd argument as the options object.
310
+ if (Array.isArray(options)) {
311
+ options = legacyOptions || {};
312
+ } else if (legacyOptions !== undefined) {
313
+ options = legacyOptions || options;
314
+ }
315
+ return this._navigateWithGuards(path, options || {}, []);
316
+ }
317
+
318
+ /**
319
+ * Internal navigation that tracks the guard redirection chain (loop protection).
320
+ * @param {string} path
321
+ * @param {{ replace?: boolean }} options
322
+ * @param {string[]} redirectChain
323
+ * @returns {Promise<void>}
324
+ */
325
+ async _navigateWithGuards(path, options, redirectChain) {
308
326
  const currentPath = window.location.pathname;
309
327
 
310
328
  // Detectar loops infinitos: si ya visitamos esta ruta en la cadena de redirecciones
311
- if (_redirectChain.includes(path)) {
312
- slice.logger.logError('Router', `Guard redirection loop detected: ${_redirectChain.join(' → ')} → ${path}`);
329
+ if (redirectChain.includes(path)) {
330
+ slice.logger.logError('Router', `Guard redirection loop detected: ${redirectChain.join(' → ')} → ${path}`);
313
331
  return;
314
332
  }
315
333
 
316
334
  // Límite de seguridad: máximo 10 redirecciones
317
- if (_redirectChain.length >= 10) {
318
- slice.logger.logError('Router', `Too many redirections: ${_redirectChain.join(' → ')} → ${path}`);
335
+ if (redirectChain.length >= 10) {
336
+ slice.logger.logError('Router', `Too many redirections: ${redirectChain.join(' → ')} → ${path}`);
319
337
  return;
320
338
  }
321
339
 
@@ -332,8 +350,7 @@ export default class Router {
332
350
 
333
351
  // Si el guard redirige
334
352
  if (guardResult && guardResult.path) {
335
- const newChain = [..._redirectChain, path];
336
- return this.navigate(guardResult.path, newChain, guardResult.options);
353
+ return this._navigateWithGuards(guardResult.path, guardResult.options || {}, [...redirectChain, path]);
337
354
  }
338
355
 
339
356
  // Si el guard cancela la navegación (next(false))
@@ -344,7 +361,7 @@ export default class Router {
344
361
 
345
362
  // No hay redirección - continuar con la navegación normal
346
363
  // Usar replace o push según las opciones
347
- if (_options.replace) {
364
+ if (options.replace) {
348
365
  window.history.replaceState({}, path, window.location.origin + path);
349
366
  } else {
350
367
  window.history.pushState({}, path, window.location.origin + path);
@@ -466,7 +483,7 @@ export default class Router {
466
483
  const guardResult = await this._executeBeforeEachGuard(to, from);
467
484
 
468
485
  if (guardResult && guardResult.path) {
469
- return this.navigate(guardResult.path, [], guardResult.options);
486
+ return this.navigate(guardResult.path, guardResult.options || {});
470
487
  }
471
488
 
472
489
  // Si el guard cancela la navegación inicial (caso raro pero posible)
@@ -662,7 +679,20 @@ export default class Router {
662
679
  * @returns {RouteMatch}
663
680
  */
664
681
  matchRoute(path) {
665
- const exactMatch = this.pathToRouteMap.get(path);
682
+ // Normalize a trailing slash ('/about/' -> '/about'); keep root '/' as-is.
683
+ path = path.length > 1 ? path.replace(/\/+$/, '') : path;
684
+ // Exact match first (fast path), then a case-insensitive match on static paths
685
+ // so '/About' resolves to a route declared as '/about'.
686
+ let exactMatch = this.pathToRouteMap.get(path);
687
+ if (!exactMatch) {
688
+ const lowerPath = path.toLowerCase();
689
+ for (const [routePattern, route] of this.pathToRouteMap.entries()) {
690
+ if (!routePattern.includes('${') && routePattern.toLowerCase() === lowerPath) {
691
+ exactMatch = route;
692
+ break;
693
+ }
694
+ }
695
+ }
666
696
  if (exactMatch) {
667
697
  if (exactMatch.parentRoute) {
668
698
  return {
@@ -716,6 +746,7 @@ export default class Router {
716
746
  }) +
717
747
  '$';
718
748
 
719
- return { regex: new RegExp(regexPattern), paramNames };
749
+ // 'i' flag: paths match case-insensitively. Captured param values keep their original case.
750
+ return { regex: new RegExp(regexPattern, 'i'), paramNames };
720
751
  }
721
752
  }
@@ -0,0 +1,119 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { collectInvalidAllowedValueProps } from '../Components/Structural/Controller/allowedValuesValidation.js';
5
+
6
+ test('collectInvalidAllowedValueProps returns empty when value is allowed', () => {
7
+ const result = collectInvalidAllowedValueProps(
8
+ {
9
+ variant: {
10
+ type: 'string',
11
+ allowedValues: ['primary', 'secondary']
12
+ }
13
+ },
14
+ {
15
+ variant: 'primary'
16
+ }
17
+ );
18
+
19
+ assert.deepEqual(result, []);
20
+ });
21
+
22
+ test('collectInvalidAllowedValueProps reports invalid provided values', () => {
23
+ const result = collectInvalidAllowedValueProps(
24
+ {
25
+ variant: {
26
+ type: 'string',
27
+ allowedValues: ['primary', 'secondary']
28
+ },
29
+ size: {
30
+ type: 'number',
31
+ allowedValues: [12, 16]
32
+ }
33
+ },
34
+ {
35
+ variant: 'danger',
36
+ size: 14
37
+ }
38
+ );
39
+
40
+ assert.deepEqual(result, [
41
+ {
42
+ propName: 'variant',
43
+ value: 'danger',
44
+ allowedValues: ['primary', 'secondary']
45
+ },
46
+ {
47
+ propName: 'size',
48
+ value: 14,
49
+ allowedValues: [12, 16]
50
+ }
51
+ ]);
52
+ });
53
+
54
+ test('collectInvalidAllowedValueProps ignores props not provided', () => {
55
+ const result = collectInvalidAllowedValueProps(
56
+ {
57
+ variant: {
58
+ type: 'string',
59
+ allowedValues: ['primary', 'secondary']
60
+ },
61
+ disabled: {
62
+ type: 'boolean'
63
+ }
64
+ },
65
+ {
66
+ disabled: true
67
+ }
68
+ );
69
+
70
+ assert.deepEqual(result, []);
71
+ });
72
+
73
+ test('collectInvalidAllowedValueProps uses strict equality', () => {
74
+ const result = collectInvalidAllowedValueProps(
75
+ {
76
+ amount: {
77
+ type: 'number',
78
+ allowedValues: [1]
79
+ },
80
+ active: {
81
+ type: 'boolean',
82
+ allowedValues: [true]
83
+ }
84
+ },
85
+ {
86
+ amount: '1',
87
+ active: 1
88
+ }
89
+ );
90
+
91
+ assert.deepEqual(result, [
92
+ {
93
+ propName: 'amount',
94
+ value: '1',
95
+ allowedValues: [1]
96
+ },
97
+ {
98
+ propName: 'active',
99
+ value: 1,
100
+ allowedValues: [true]
101
+ }
102
+ ]);
103
+ });
104
+
105
+ test('collectInvalidAllowedValueProps ignores empty allowedValues arrays', () => {
106
+ const result = collectInvalidAllowedValueProps(
107
+ {
108
+ variant: {
109
+ type: 'string',
110
+ allowedValues: []
111
+ }
112
+ },
113
+ {
114
+ variant: 'unexpected'
115
+ }
116
+ );
117
+
118
+ assert.deepEqual(result, []);
119
+ });