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.
- package/.opencode/opencode.json +14 -0
- package/README.md +57 -134
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +233 -110
- package/Slice/Components/Structural/Controller/Controller.js +9 -0
- package/Slice/Components/Structural/Controller/allowedValuesValidation.js +52 -0
- package/Slice/Components/Structural/Debugger/Debugger.js +392 -442
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +264 -149
- package/Slice/Components/Structural/Router/Router.js +45 -14
- package/Slice/tests/props-allowed-values-validation.test.js +119 -0
- package/package.json +11 -9
- package/src/App/index.html +2 -8
- package/src/App/index.js +18 -21
- package/src/App/style.css +8 -37
- package/src/Components/AppComponents/AboutSection/AboutSection.css +9 -0
- package/src/Components/AppComponents/AboutSection/AboutSection.html +8 -0
- package/src/Components/AppComponents/AboutSection/AboutSection.js +12 -0
- package/src/Components/AppComponents/AppShell/AppShell.css +10 -0
- package/src/Components/AppComponents/AppShell/AppShell.html +4 -0
- package/src/Components/AppComponents/AppShell/AppShell.js +36 -0
- package/src/Components/AppComponents/HomeSection/HomeSection.css +20 -0
- package/src/Components/AppComponents/HomeSection/HomeSection.html +10 -0
- package/src/Components/AppComponents/HomeSection/HomeSection.js +19 -0
- package/src/Components/Visual/MultiRoute/MultiRoute.js +13 -6
- package/src/Components/components.js +4 -16
- package/src/routes.js +6 -12
- package/src/sliceConfig.json +2 -1
- package/Slice/Components/Structural/Debugger/Debugger.css +0 -620
- package/src/Components/AppComponents/HomePage/HomePage.css +0 -201
- package/src/Components/AppComponents/HomePage/HomePage.html +0 -37
- package/src/Components/AppComponents/HomePage/HomePage.js +0 -210
- package/src/Components/AppComponents/Playground/Playground.css +0 -12
- package/src/Components/AppComponents/Playground/Playground.html +0 -0
- package/src/Components/AppComponents/Playground/Playground.js +0 -111
- package/src/images/Slice.js-logo.png +0 -0
- package/src/images/im2/Slice.js-logo.png +0 -0
- 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="
|
|
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"
|
|
179
|
-
<button id="events-close" class="btn"
|
|
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="
|
|
184
|
-
<div class="count"
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
228
|
-
font-weight: 600;
|
|
229
|
-
color: var(--font-primary-color);
|
|
230
|
-
}
|
|
244
|
+
slice-eventmanager-debugger #events-debugger * { box-sizing: border-box; }
|
|
231
245
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
301
|
+
* Navigate to a route path (guards run automatically).
|
|
302
302
|
* @param {string} path
|
|
303
|
-
* @param {
|
|
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,
|
|
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 (
|
|
312
|
-
slice.logger.logError('Router', `Guard redirection loop detected: ${
|
|
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 (
|
|
318
|
-
slice.logger.logError('Router', `Too many redirections: ${
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|