@jsenv/navi 0.16.25 → 0.16.26
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/dist/jsenv_navi.js +163 -144
- package/dist/jsenv_navi.js.map +4 -4
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2390,22 +2390,26 @@ const generateSignalId = () => {
|
|
|
2390
2390
|
};
|
|
2391
2391
|
|
|
2392
2392
|
/**
|
|
2393
|
-
* Creates an advanced signal with
|
|
2393
|
+
* Creates an advanced signal with dynamic default value, local storage persistence, and validation.
|
|
2394
2394
|
*
|
|
2395
|
-
* The
|
|
2396
|
-
*
|
|
2397
|
-
*
|
|
2398
|
-
* 3. When sourceSignal changes, it will override the current value again
|
|
2395
|
+
* The first parameter can be either a static value or a signal acting as a "dynamic default":
|
|
2396
|
+
* - If a static value: traditional default behavior
|
|
2397
|
+
* - If a signal: acts as a dynamic default that updates the signal ONLY when no explicit value has been set
|
|
2399
2398
|
*
|
|
2400
|
-
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2399
|
+
* Dynamic default behavior (when first param is a signal):
|
|
2400
|
+
* 1. Initially takes value from the default signal
|
|
2401
|
+
* 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
|
|
2402
|
+
* 3. When default signal changes, it only updates if no explicit value was ever set
|
|
2403
|
+
* 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
|
|
2404
2404
|
*
|
|
2405
|
-
*
|
|
2405
|
+
* This is useful for:
|
|
2406
|
+
* - Backend data that can change but shouldn't override user preferences
|
|
2407
|
+
* - Route parameters with dynamic defaults based on other state
|
|
2408
|
+
* - Cascading configuration where defaults can be updated without losing user customizations
|
|
2409
|
+
*
|
|
2410
|
+
* @param {any|import("@preact/signals").Signal} defaultValue - Static default value OR signal for dynamic default behavior
|
|
2406
2411
|
* @param {Object} [options={}] - Configuration options
|
|
2407
2412
|
* @param {string|number} [options.id] - Custom ID for the signal. If not provided, an auto-generated ID will be used. Used for localStorage key and route pattern detection.
|
|
2408
|
-
* @param {import("@preact/signals").Signal} [options.sourceSignal] - Source signal to synchronize with. When the source signal changes, this signal will be updated
|
|
2409
2413
|
* @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
|
|
2410
2414
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
2411
2415
|
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
@@ -2435,14 +2439,21 @@ const generateSignalId = () => {
|
|
|
2435
2439
|
* });
|
|
2436
2440
|
*
|
|
2437
2441
|
* @example
|
|
2438
|
-
* //
|
|
2439
|
-
* const
|
|
2440
|
-
* const
|
|
2442
|
+
* // Dynamic default that doesn't override user choices
|
|
2443
|
+
* const backendTheme = signal("light");
|
|
2444
|
+
* const userTheme = stateSignal(backendTheme, { persists: true });
|
|
2445
|
+
*
|
|
2446
|
+
* // Initially: userTheme.value = "light" (from dynamic default)
|
|
2447
|
+
* // User sets: userTheme.value = "dark" (explicit choice, persisted)
|
|
2448
|
+
* // Backend changes: backendTheme.value = "blue"
|
|
2449
|
+
* // Result: userTheme.value = "dark" (user choice preserved)
|
|
2450
|
+
* // Reset: userTheme.value = undefined; // Now follows dynamic default again
|
|
2441
2451
|
*
|
|
2442
|
-
*
|
|
2443
|
-
* //
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
2452
|
+
* @example
|
|
2453
|
+
* // Route parameter with dynamic default from parent route
|
|
2454
|
+
* const parentTab = signal("overview");
|
|
2455
|
+
* const childTab = stateSignal(parentTab);
|
|
2456
|
+
* // childTab follows parentTab changes unless explicitly set
|
|
2446
2457
|
*/
|
|
2447
2458
|
const NO_LOCAL_STORAGE = [() => undefined, () => {}, () => {}];
|
|
2448
2459
|
const stateSignal = (defaultValue, options = {}) => {
|
|
@@ -2451,10 +2462,18 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2451
2462
|
type = "string",
|
|
2452
2463
|
oneOf,
|
|
2453
2464
|
autoFix,
|
|
2454
|
-
sourceSignal,
|
|
2455
2465
|
persists = false,
|
|
2456
2466
|
debug,
|
|
2457
2467
|
} = options;
|
|
2468
|
+
|
|
2469
|
+
// Check if defaultValue is a signal (dynamic default) or static value
|
|
2470
|
+
const isDynamicDefault =
|
|
2471
|
+
defaultValue &&
|
|
2472
|
+
typeof defaultValue === "object" &&
|
|
2473
|
+
"value" in defaultValue &&
|
|
2474
|
+
"peek" in defaultValue;
|
|
2475
|
+
const dynamicDefaultSignal = isDynamicDefault ? defaultValue : null;
|
|
2476
|
+
const staticDefaultValue = isDynamicDefault ? undefined : defaultValue;
|
|
2458
2477
|
const signalId = id || generateSignalId();
|
|
2459
2478
|
// Convert numeric IDs to strings for consistency
|
|
2460
2479
|
const signalIdString = String(signalId);
|
|
@@ -2471,202 +2490,199 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2471
2490
|
persists
|
|
2472
2491
|
? valueInLocalStorage(localStorageKey, { type })
|
|
2473
2492
|
: NO_LOCAL_STORAGE;
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
if (
|
|
2478
|
-
console.debug(
|
|
2479
|
-
`[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2480
|
-
);
|
|
2481
|
-
}
|
|
2482
|
-
return valueFromLocalStorage;
|
|
2483
|
-
}
|
|
2484
|
-
if (sourceSignal) {
|
|
2485
|
-
const sourceValue = sourceSignal.peek();
|
|
2486
|
-
if (sourceValue !== undefined) {
|
|
2493
|
+
const getDefaultValue = () => {
|
|
2494
|
+
if (persists) {
|
|
2495
|
+
const valueFromLocalStorage = readFromLocalStorage();
|
|
2496
|
+
if (valueFromLocalStorage !== undefined) {
|
|
2487
2497
|
if (debug) {
|
|
2488
2498
|
console.debug(
|
|
2489
|
-
`[stateSignal:${signalIdString}] using value from
|
|
2499
|
+
`[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2490
2500
|
);
|
|
2491
2501
|
}
|
|
2492
|
-
return
|
|
2502
|
+
return valueFromLocalStorage;
|
|
2493
2503
|
}
|
|
2494
2504
|
}
|
|
2505
|
+
if (dynamicDefaultSignal) {
|
|
2506
|
+
const dynamicValue = dynamicDefaultSignal.peek();
|
|
2507
|
+
if (dynamicValue === undefined) {
|
|
2508
|
+
return undefined;
|
|
2509
|
+
}
|
|
2510
|
+
if (debug) {
|
|
2511
|
+
console.debug(
|
|
2512
|
+
`[stateSignal:${signalIdString}] using value from dynamic default signal=${dynamicValue}`,
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
return dynamicValue;
|
|
2516
|
+
}
|
|
2495
2517
|
if (debug) {
|
|
2496
2518
|
console.debug(
|
|
2497
|
-
`[stateSignal:${signalIdString}] using default value=${
|
|
2519
|
+
`[stateSignal:${signalIdString}] using static default value=${staticDefaultValue}`,
|
|
2498
2520
|
);
|
|
2499
2521
|
}
|
|
2500
|
-
return
|
|
2522
|
+
return staticDefaultValue;
|
|
2523
|
+
};
|
|
2524
|
+
const isCustomValue = (value) => {
|
|
2525
|
+
if (value === undefined) {
|
|
2526
|
+
return false;
|
|
2527
|
+
}
|
|
2528
|
+
if (dynamicDefaultSignal) {
|
|
2529
|
+
return value !== dynamicDefaultSignal.peek();
|
|
2530
|
+
}
|
|
2531
|
+
return value !== staticDefaultValue;
|
|
2501
2532
|
};
|
|
2502
2533
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
if (debug) {
|
|
2506
|
-
console.debug(
|
|
2507
|
-
`[stateSignal:${signalIdString}] created with initial value=${advancedSignal.peek()}`,
|
|
2508
|
-
{
|
|
2509
|
-
defaultValue,
|
|
2510
|
-
hasSourceSignal: Boolean(sourceSignal),
|
|
2511
|
-
persists,
|
|
2512
|
-
localStorageKey: persists ? localStorageKey : undefined,
|
|
2513
|
-
},
|
|
2514
|
-
);
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
// Set signal ID and create meaningful string representation
|
|
2518
|
-
advancedSignal.__signalId = signalIdString;
|
|
2519
|
-
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
2520
|
-
|
|
2521
|
-
// Store signal with its options for later route connection
|
|
2522
|
-
globalSignalRegistry.set(signalIdString, {
|
|
2523
|
-
signal: advancedSignal,
|
|
2524
|
-
options: {
|
|
2525
|
-
getFallbackValue,
|
|
2526
|
-
defaultValue,
|
|
2527
|
-
type,
|
|
2528
|
-
persists,
|
|
2529
|
-
localStorageKey,
|
|
2530
|
-
debug,
|
|
2531
|
-
...options,
|
|
2532
|
-
},
|
|
2533
|
-
});
|
|
2534
|
-
|
|
2534
|
+
// Create signal with initial value: use stored value, or undefined to indicate no explicit value
|
|
2535
|
+
const advancedSignal = signal(getDefaultValue());
|
|
2535
2536
|
const validity = { valid: true };
|
|
2536
2537
|
advancedSignal.validity = validity;
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
// 1.
|
|
2540
|
-
// 2.
|
|
2541
|
-
// 3. default value
|
|
2538
|
+
advancedSignal.__signalId = signalIdString;
|
|
2539
|
+
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
2540
|
+
// 1. when signal value changes to undefined, it needs to fallback to default value
|
|
2541
|
+
// 2. when dynamic default changes and signal value is not custom, it needs to update
|
|
2542
2542
|
{
|
|
2543
|
-
let
|
|
2543
|
+
let isFirstRun = true;
|
|
2544
2544
|
effect(() => {
|
|
2545
2545
|
const value = advancedSignal.value;
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2548
|
-
sourceSignal.value;
|
|
2549
|
-
}
|
|
2550
|
-
if (firstRun) {
|
|
2551
|
-
firstRun = true;
|
|
2546
|
+
if (isFirstRun) {
|
|
2547
|
+
isFirstRun = false;
|
|
2552
2548
|
return;
|
|
2553
2549
|
}
|
|
2554
2550
|
if (value !== undefined) {
|
|
2555
2551
|
return;
|
|
2556
2552
|
}
|
|
2557
|
-
|
|
2553
|
+
const defaultValue = getDefaultValue();
|
|
2554
|
+
if (defaultValue === value) {
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (debug) {
|
|
2558
|
+
console.debug(
|
|
2559
|
+
`[stateSignal:${signalIdString}] becomes undefined, reset to ${defaultValue}`,
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
advancedSignal.value = defaultValue;
|
|
2558
2563
|
});
|
|
2559
2564
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
break source_signal_override;
|
|
2565
|
+
dynamic_signal_effect: {
|
|
2566
|
+
if (!dynamicDefaultSignal) {
|
|
2567
|
+
break dynamic_signal_effect;
|
|
2564
2568
|
}
|
|
2565
|
-
|
|
2569
|
+
// here we listen only on the dynamic default signal
|
|
2566
2570
|
let isFirstRun = true;
|
|
2567
|
-
let
|
|
2571
|
+
let dynamicDefaultPreviousValue;
|
|
2568
2572
|
effect(() => {
|
|
2569
|
-
const
|
|
2573
|
+
const value = advancedSignal.peek();
|
|
2574
|
+
const dynamicDefaultValue = dynamicDefaultSignal.value;
|
|
2570
2575
|
if (isFirstRun) {
|
|
2571
|
-
// first run
|
|
2572
2576
|
isFirstRun = false;
|
|
2573
|
-
|
|
2577
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2574
2578
|
return;
|
|
2575
2579
|
}
|
|
2576
|
-
if (
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
sourceValue,
|
|
2584
|
-
currentValue: advancedSignal.peek(),
|
|
2585
|
-
},
|
|
2586
|
-
);
|
|
2587
|
-
}
|
|
2588
|
-
sourcePreviousValue = undefined;
|
|
2580
|
+
if (value !== dynamicDefaultPreviousValue) {
|
|
2581
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
dynamicDefaultPreviousValue = dynamicDefaultValue;
|
|
2585
|
+
const defaultValue = getDefaultValue();
|
|
2586
|
+
if (defaultValue === value) {
|
|
2589
2587
|
return;
|
|
2590
2588
|
}
|
|
2591
|
-
// the case we want to support: source signal value changes -> override current value
|
|
2592
2589
|
if (debug) {
|
|
2593
2590
|
console.debug(
|
|
2594
|
-
`[stateSignal:${signalIdString}]
|
|
2595
|
-
{
|
|
2596
|
-
sourcePreviousValue,
|
|
2597
|
-
sourceValue,
|
|
2598
|
-
previousValue: advancedSignal.peek(),
|
|
2599
|
-
},
|
|
2591
|
+
`[stateSignal:${signalIdString}] dynamic default updated, update to ${defaultValue}`,
|
|
2600
2592
|
);
|
|
2601
2593
|
}
|
|
2602
|
-
advancedSignal.value =
|
|
2603
|
-
sourcePreviousValue = sourceValue;
|
|
2594
|
+
advancedSignal.value = defaultValue;
|
|
2604
2595
|
});
|
|
2605
2596
|
}
|
|
2606
|
-
// Read/write into local storage when enabled
|
|
2607
2597
|
persist_in_local_storage: {
|
|
2608
2598
|
if (!localStorageKey) {
|
|
2609
2599
|
break persist_in_local_storage;
|
|
2610
2600
|
}
|
|
2611
2601
|
effect(() => {
|
|
2612
2602
|
const value = advancedSignal.value;
|
|
2613
|
-
if (value
|
|
2603
|
+
if (isCustomValue(value)) {
|
|
2614
2604
|
if (debug) {
|
|
2615
2605
|
console.debug(
|
|
2616
|
-
`[stateSignal:${signalIdString}]
|
|
2606
|
+
`[stateSignal:${signalIdString}] writing into localStorage "${localStorageKey}"=${value}`,
|
|
2617
2607
|
);
|
|
2618
2608
|
}
|
|
2619
|
-
|
|
2609
|
+
writeIntoLocalStorage(value);
|
|
2620
2610
|
} else {
|
|
2621
2611
|
if (debug) {
|
|
2622
2612
|
console.debug(
|
|
2623
|
-
`[stateSignal:${signalIdString}]
|
|
2613
|
+
`[stateSignal:${signalIdString}] removing "${localStorageKey}" from localStorage (value=${value})`,
|
|
2624
2614
|
);
|
|
2625
2615
|
}
|
|
2626
|
-
|
|
2616
|
+
removeFromLocalStorage();
|
|
2627
2617
|
}
|
|
2628
2618
|
});
|
|
2629
2619
|
}
|
|
2630
|
-
// update validity object according to the
|
|
2620
|
+
// update validity object according to the signal value
|
|
2631
2621
|
{
|
|
2632
2622
|
effect(() => {
|
|
2633
|
-
const value = advancedSignal.value;
|
|
2634
2623
|
const wasValid = validity.valid;
|
|
2624
|
+
const value = advancedSignal.value;
|
|
2635
2625
|
updateValidity({ oneOf }, validity, value);
|
|
2636
|
-
if (
|
|
2637
|
-
if (
|
|
2638
|
-
console.debug(`[stateSignal:${signalIdString}] validation failed`, {
|
|
2639
|
-
value,
|
|
2640
|
-
oneOf,
|
|
2641
|
-
hasAutoFix: Boolean(autoFix),
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
if (autoFix) {
|
|
2645
|
-
const fixedValue = autoFix();
|
|
2626
|
+
if (validity.valid) {
|
|
2627
|
+
if (!wasValid) {
|
|
2646
2628
|
if (debug) {
|
|
2647
2629
|
console.debug(
|
|
2648
|
-
`[stateSignal:${signalIdString}]
|
|
2649
|
-
{
|
|
2650
|
-
invalidValue: value,
|
|
2651
|
-
fixedValue,
|
|
2652
|
-
},
|
|
2630
|
+
`[stateSignal:${signalIdString}] validation now passes`,
|
|
2631
|
+
{ value },
|
|
2653
2632
|
);
|
|
2654
2633
|
}
|
|
2655
|
-
advancedSignal.value = fixedValue;
|
|
2656
|
-
return;
|
|
2657
2634
|
}
|
|
2658
|
-
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
if (debug) {
|
|
2638
|
+
console.debug(`[stateSignal:${signalIdString}] validation failed`, {
|
|
2639
|
+
value,
|
|
2640
|
+
oneOf,
|
|
2641
|
+
hasAutoFix: Boolean(autoFix),
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
if (autoFix) {
|
|
2645
|
+
const fixedValue = autoFix(value);
|
|
2659
2646
|
if (debug) {
|
|
2660
2647
|
console.debug(
|
|
2661
|
-
`[stateSignal:${signalIdString}]
|
|
2648
|
+
`[stateSignal:${signalIdString}] autoFix applied: ${value} → ${fixedValue}`,
|
|
2662
2649
|
{
|
|
2663
2650
|
value,
|
|
2651
|
+
fixedValue,
|
|
2664
2652
|
},
|
|
2665
2653
|
);
|
|
2666
2654
|
}
|
|
2655
|
+
advancedSignal.value = fixedValue;
|
|
2656
|
+
return;
|
|
2667
2657
|
}
|
|
2668
2658
|
});
|
|
2669
2659
|
}
|
|
2660
|
+
// Store signal with its options (used by route_pattern.js)
|
|
2661
|
+
globalSignalRegistry.set(signalIdString, {
|
|
2662
|
+
signal: advancedSignal,
|
|
2663
|
+
options: {
|
|
2664
|
+
getDefaultValue,
|
|
2665
|
+
defaultValue: staticDefaultValue,
|
|
2666
|
+
dynamicDefaultSignal,
|
|
2667
|
+
type,
|
|
2668
|
+
persists,
|
|
2669
|
+
localStorageKey,
|
|
2670
|
+
debug,
|
|
2671
|
+
...options,
|
|
2672
|
+
},
|
|
2673
|
+
});
|
|
2674
|
+
if (debug) {
|
|
2675
|
+
console.debug(
|
|
2676
|
+
`[stateSignal:${signalIdString}] created with initial value=${advancedSignal.value}`,
|
|
2677
|
+
{
|
|
2678
|
+
staticDefaultValue,
|
|
2679
|
+
hasDynamicDefault: Boolean(dynamicDefaultSignal),
|
|
2680
|
+
hasStoredValue: persists && readFromLocalStorage() !== undefined,
|
|
2681
|
+
persists,
|
|
2682
|
+
localStorageKey: persists ? localStorageKey : undefined,
|
|
2683
|
+
},
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2670
2686
|
|
|
2671
2687
|
return advancedSignal;
|
|
2672
2688
|
};
|
|
@@ -10287,12 +10303,15 @@ const updateRoutes = (
|
|
|
10287
10303
|
if (newMatching) {
|
|
10288
10304
|
// When route matches, sync signal with URL parameter value
|
|
10289
10305
|
// This ensures URL is the source of truth
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10306
|
+
const currentValue = stateSignal.peek();
|
|
10307
|
+
if (currentValue !== urlParamValue) {
|
|
10308
|
+
if (debug) {
|
|
10309
|
+
console.debug(
|
|
10310
|
+
`[route] Route matching: setting ${paramName} signal to URL value: ${urlParamValue}`,
|
|
10311
|
+
);
|
|
10312
|
+
}
|
|
10313
|
+
stateSignal.value = urlParamValue;
|
|
10294
10314
|
}
|
|
10295
|
-
stateSignal.value = urlParamValue;
|
|
10296
10315
|
} else {
|
|
10297
10316
|
// Route doesn't match - check if any matching route extracts this parameter
|
|
10298
10317
|
let parameterExtractedByMatchingRoute = false;
|