@jsenv/navi 0.16.27 → 0.16.28

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.
@@ -2401,15 +2401,18 @@ const generateSignalId = () => {
2401
2401
  * 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
2402
2402
  * 3. When default signal changes, it only updates if no explicit value was ever set
2403
2403
  * 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
2404
+ * 5. If dynamic default is undefined and options.default is provided, uses the static fallback
2404
2405
  *
2405
2406
  * This is useful for:
2406
2407
  * - Backend data that can change but shouldn't override user preferences
2407
2408
  * - Route parameters with dynamic defaults based on other state
2408
2409
  * - Cascading configuration where defaults can be updated without losing user customizations
2410
+ * - Having a static fallback when dynamic defaults might be undefined
2409
2411
  *
2410
2412
  * @param {any|import("@preact/signals").Signal} defaultValue - Static default value OR signal for dynamic default behavior
2411
2413
  * @param {Object} [options={}] - Configuration options
2412
2414
  * @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.
2415
+ * @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
2413
2416
  * @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
2414
2417
  * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
2415
2418
  * @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
@@ -2450,6 +2453,21 @@ const generateSignalId = () => {
2450
2453
  * // Reset: userTheme.value = undefined; // Now follows dynamic default again
2451
2454
  *
2452
2455
  * @example
2456
+ * // Dynamic default with static fallback
2457
+ * const backendValue = signal(undefined); // might be undefined initially
2458
+ * const userValue = stateSignal(backendValue, {
2459
+ * default: "fallback",
2460
+ * persists: true
2461
+ * });
2462
+ *
2463
+ * // Initially: userValue.value = "fallback" (static fallback since dynamic is undefined)
2464
+ * // Backend loads: backendValue.value = "loaded"; userValue.value = "loaded" (follows dynamic)
2465
+ * // User sets: userValue.value = "custom" (explicit choice, persisted)
2466
+ * // Backend changes: backendValue.value = "updated"
2467
+ * // Result: userValue.value = "custom" (user choice preserved)
2468
+ * // Reset: userValue.value = undefined; userValue.value = "updated" (follows dynamic again)
2469
+ *
2470
+ * @example
2453
2471
  * // Route parameter with dynamic default from parent route
2454
2472
  * const parentTab = signal("overview");
2455
2473
  * const childTab = stateSignal(parentTab);
@@ -2464,6 +2482,7 @@ const stateSignal = (defaultValue, options = {}) => {
2464
2482
  autoFix,
2465
2483
  persists = false,
2466
2484
  debug,
2485
+ default: staticFallback,
2467
2486
  } = options;
2468
2487
 
2469
2488
  // Check if defaultValue is a signal (dynamic default) or static value
@@ -2473,7 +2492,7 @@ const stateSignal = (defaultValue, options = {}) => {
2473
2492
  "value" in defaultValue &&
2474
2493
  "peek" in defaultValue;
2475
2494
  const dynamicDefaultSignal = isDynamicDefault ? defaultValue : null;
2476
- const staticDefaultValue = isDynamicDefault ? undefined : defaultValue;
2495
+ const staticDefaultValue = isDynamicDefault ? staticFallback : defaultValue;
2477
2496
  const signalId = id || generateSignalId();
2478
2497
  // Convert numeric IDs to strings for consistency
2479
2498
  const signalIdString = String(signalId);
@@ -2505,7 +2524,15 @@ const stateSignal = (defaultValue, options = {}) => {
2505
2524
  if (dynamicDefaultSignal) {
2506
2525
  const dynamicValue = dynamicDefaultSignal.peek();
2507
2526
  if (dynamicValue === undefined) {
2508
- return undefined;
2527
+ if (staticDefaultValue === undefined) {
2528
+ return undefined;
2529
+ }
2530
+ if (debug) {
2531
+ console.debug(
2532
+ `[stateSignal:${signalIdString}] dynamic default is undefined, using static default=${staticDefaultValue}`,
2533
+ );
2534
+ }
2535
+ return staticDefaultValue;
2509
2536
  }
2510
2537
  if (debug) {
2511
2538
  console.debug(
@@ -2526,7 +2553,11 @@ const stateSignal = (defaultValue, options = {}) => {
2526
2553
  return false;
2527
2554
  }
2528
2555
  if (dynamicDefaultSignal) {
2529
- return value !== dynamicDefaultSignal.peek();
2556
+ const dynamicValue = dynamicDefaultSignal.peek();
2557
+ if (dynamicValue === undefined) {
2558
+ return value !== staticDefaultValue;
2559
+ }
2560
+ return value !== dynamicValue;
2530
2561
  }
2531
2562
  return value !== staticDefaultValue;
2532
2563
  };
@@ -2577,21 +2608,40 @@ const stateSignal = (defaultValue, options = {}) => {
2577
2608
  dynamicDefaultPreviousValue = dynamicDefaultValue;
2578
2609
  return;
2579
2610
  }
2580
- if (value !== dynamicDefaultPreviousValue) {
2611
+ // Check if current signal value matches the PREVIOUS dynamic default
2612
+ // If so, it was following the dynamic default and should update
2613
+ // Special case: if previous was undefined and we were using static fallback
2614
+ let wasFollowingDefault = false;
2615
+ if (
2616
+ dynamicDefaultPreviousValue === undefined &&
2617
+ staticDefaultValue !== undefined
2618
+ ) {
2619
+ // Signal might have been using static fallback
2620
+ wasFollowingDefault = value === staticDefaultValue;
2621
+ } else {
2622
+ // Signal was following the previous dynamic default
2623
+ wasFollowingDefault = value === dynamicDefaultPreviousValue;
2624
+ }
2625
+
2626
+ if (!wasFollowingDefault) {
2627
+ // Signal has a custom value, don't update even if dynamic default changes
2581
2628
  dynamicDefaultPreviousValue = dynamicDefaultValue;
2582
2629
  return;
2583
2630
  }
2584
- dynamicDefaultPreviousValue = dynamicDefaultValue;
2585
- const defaultValue = getDefaultValue();
2586
- if (defaultValue === value) {
2631
+
2632
+ // Signal was using default value, update to new default
2633
+ const newDefaultValue = getDefaultValue();
2634
+ if (newDefaultValue === value) {
2635
+ dynamicDefaultPreviousValue = dynamicDefaultValue;
2587
2636
  return;
2588
2637
  }
2589
2638
  if (debug) {
2590
2639
  console.debug(
2591
- `[stateSignal:${signalIdString}] dynamic default updated, update to ${defaultValue}`,
2640
+ `[stateSignal:${signalIdString}] dynamic default updated, update to ${newDefaultValue}`,
2592
2641
  );
2593
2642
  }
2594
- advancedSignal.value = defaultValue;
2643
+ dynamicDefaultPreviousValue = dynamicDefaultValue;
2644
+ advancedSignal.value = newDefaultValue;
2595
2645
  });
2596
2646
  }
2597
2647
  persist_in_local_storage: {