@jsenv/navi 0.27.18 → 0.27.19

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.
@@ -427,6 +427,21 @@ const compareTwoJsValues = (
427
427
  ) {
428
428
  return true;
429
429
  }
430
+ // Date objects must be compared by time value, not by enumerable keys (which are empty)
431
+ {
432
+ const aIsDate = a instanceof Date;
433
+ const bIsDate = b instanceof Date;
434
+ if (aIsDate !== bIsDate) {
435
+ return false;
436
+ }
437
+ if (aIsDate && bIsDate) {
438
+ const aTime = a.getTime();
439
+ const bTime = b.getTime();
440
+ if (aTime !== bTime) {
441
+ return false;
442
+ }
443
+ }
444
+ }
430
445
  const aKeys = Object.keys(a);
431
446
  const bKeys = Object.keys(b);
432
447
  if (aKeys.length !== bKeys.length) {
@@ -5263,17 +5278,19 @@ const generateSignalId = () => {
5263
5278
  const stateSignal = (defaultValue, options = {}) => {
5264
5279
  const {
5265
5280
  id,
5281
+ // NOTE: when adding support for a new type here, also update route_pattern.js
5282
+ // (buildQueryString for encoding, extractSearchParams for decoding)
5266
5283
  type,
5267
5284
  min,
5268
5285
  max,
5269
5286
  step,
5270
5287
  oneOf,
5271
5288
  localStorageRepresentation,
5272
- urlRepresentation,
5273
5289
  persists = false,
5274
5290
  debug,
5275
5291
  default: staticFallback,
5276
5292
  ignoreArrayOrder,
5293
+ autoFix,
5277
5294
  } = options;
5278
5295
 
5279
5296
  // Check if defaultValue is a signal (dynamic default) or static value
@@ -5303,7 +5320,7 @@ const stateSignal = (defaultValue, options = {}) => {
5303
5320
  step,
5304
5321
  oneOf,
5305
5322
  localStorageRepresentation,
5306
- urlRepresentation,
5323
+ autoFix,
5307
5324
  });
5308
5325
  const readFromLocalStorage = persists
5309
5326
  ? () => {
@@ -5428,7 +5445,12 @@ const stateSignal = (defaultValue, options = {}) => {
5428
5445
  return valueDescriptor.get.call(preactSignal);
5429
5446
  },
5430
5447
  set(newValue) {
5431
- valueDescriptor.set.call(preactSignal, processValue(newValue));
5448
+ const processedValue = processValue(newValue);
5449
+ // const currentValue = valueDescriptor.get.call(preactSignal);
5450
+ // if (compareTwoJsValues(processedValue, currentValue)) {
5451
+ // return;
5452
+ // }
5453
+ valueDescriptor.set.call(preactSignal, processedValue);
5432
5454
  },
5433
5455
  enumerable: true,
5434
5456
  configurable: true,
@@ -5436,6 +5458,12 @@ const stateSignal = (defaultValue, options = {}) => {
5436
5458
 
5437
5459
  const facadeSignal = preactSignal;
5438
5460
  facadeSignal.validity = validity;
5461
+ facadeSignal.validSignal = computed(() => {
5462
+ // Reading facadeSignal.value establishes the reactive dependency.
5463
+ // eslint-disable-next-line no-unused-expressions
5464
+ facadeSignal.value;
5465
+ return validity.representations.valid?.value;
5466
+ });
5439
5467
  facadeSignal.__signalId = signalIdString;
5440
5468
  facadeSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
5441
5469
  // 1. when signal value changes to undefined, it needs to fallback to default value
@@ -5552,14 +5580,6 @@ const stateSignal = (defaultValue, options = {}) => {
5552
5580
  }
5553
5581
  });
5554
5582
  }
5555
- // update validity object according to the signal value
5556
- {
5557
- effect(() => {
5558
- const value = preactSignal.value;
5559
- facadeSignal.value = processValue(value);
5560
- });
5561
- }
5562
-
5563
5583
  // Create isDefaultValue function for this signal
5564
5584
  const isDefaultValue = (value) => {
5565
5585
  const currentDefault = getDefaultValue(false);
@@ -12340,38 +12360,6 @@ const encodeParamValue = (value, isWildcard = false) => {
12340
12360
  return encodeURIComponent(value);
12341
12361
  };
12342
12362
 
12343
- /**
12344
- * Build query string from parameters, respecting rawUrlPart values
12345
- */
12346
- const buildQueryString = (params) => {
12347
- const searchParamPairs = [];
12348
-
12349
- for (const [key, value] of Object.entries(params)) {
12350
- if (value !== undefined && value !== null) {
12351
- const encodedKey = encodeURIComponent(key);
12352
-
12353
- // Handle array values - join with commas
12354
- if (Array.isArray(value)) {
12355
- if (value.length === 0) ; else {
12356
- const encodedValue = value
12357
- .map((item) => encodeURIComponent(String(item)))
12358
- .join(",");
12359
- searchParamPairs.push(`${encodedKey}=${encodedValue}`);
12360
- }
12361
- }
12362
- // Handle boolean values - if true, just add the key without value
12363
- else if (value === true || value === "") {
12364
- searchParamPairs.push(encodedKey);
12365
- } else {
12366
- const encodedValue = encodeParamValue(value, false); // Search params encode slashes
12367
- searchParamPairs.push(`${encodedKey}=${encodedValue}`);
12368
- }
12369
- }
12370
- }
12371
-
12372
- return searchParamPairs.join("&");
12373
- };
12374
-
12375
12363
  // Function to detect signals in route patterns and connect them
12376
12364
  const detectSignals = (routePattern) => {
12377
12365
  const signalConnections = [];
@@ -12838,6 +12826,45 @@ const matchUrl = (
12838
12826
  return params;
12839
12827
  };
12840
12828
 
12829
+ /**
12830
+ * Build query string from parameters, respecting rawUrlPart values
12831
+ */
12832
+ const buildQueryString = (params) => {
12833
+ const searchParamPairs = [];
12834
+
12835
+ for (const [key, value] of Object.entries(params)) {
12836
+ if (value !== undefined && value !== null) {
12837
+ const encodedKey = encodeURIComponent(key);
12838
+
12839
+ // Handle array values - join with commas
12840
+ if (Array.isArray(value)) {
12841
+ if (value.length === 0) ; else {
12842
+ const encodedValue = value
12843
+ .map((item) => encodeURIComponent(String(item)))
12844
+ .join(",");
12845
+ searchParamPairs.push(`${encodedKey}=${encodedValue}`);
12846
+ }
12847
+ }
12848
+ // Handle boolean values - if true, just add the key without value
12849
+ else if (value === true || value === "") {
12850
+ searchParamPairs.push(encodedKey);
12851
+ }
12852
+ // Handle Date objects - format as YYYY-MM-DD using UTC to match new Date('YYYY-MM-DD') semantics
12853
+ else if (value instanceof Date) {
12854
+ const yyyy = value.getUTCFullYear();
12855
+ const mm = String(value.getUTCMonth() + 1).padStart(2, "0");
12856
+ const dd = String(value.getUTCDate()).padStart(2, "0");
12857
+ searchParamPairs.push(`${encodedKey}=${yyyy}-${mm}-${dd}`);
12858
+ } else {
12859
+ const encodedValue = encodeParamValue(value, false); // Search params encode slashes
12860
+ searchParamPairs.push(`${encodedKey}=${encodedValue}`);
12861
+ }
12862
+ }
12863
+ }
12864
+
12865
+ return searchParamPairs.join("&");
12866
+ };
12867
+
12841
12868
  /**
12842
12869
  * Extract search parameters from URL
12843
12870
  */
@@ -12905,6 +12932,17 @@ const extractSearchParams = (urlObj, queryConnectionMap) => {
12905
12932
  // ?walk=0 → false
12906
12933
  params[key] =
12907
12934
  decodedValue === "true" || decodedValue === "1" || decodedValue === "";
12935
+ } else if (signalType === "date") {
12936
+ const decodedValue = decodeURIComponent(rawValue);
12937
+ // Accept both "YYYY-MM-DD" and full ISO string, always parse as UTC date
12938
+ const datePart = decodedValue.slice(0, 10);
12939
+ const [year, month, day] = datePart.split("-").map(Number);
12940
+ const d = new Date(Date.UTC(year, month - 1, day));
12941
+ params[key] = isNaN(d.getTime()) ? decodedValue : d;
12942
+ } else if (signalType === "datetime") {
12943
+ const decodedValue = decodeURIComponent(rawValue);
12944
+ const d = new Date(decodedValue);
12945
+ params[key] = isNaN(d.getTime()) ? decodedValue : d;
12908
12946
  } else {
12909
12947
  params[key] = decodeURIComponent(rawValue);
12910
12948
  }
@@ -13756,8 +13794,6 @@ const route = (pattern, { searchParams } = {}) => {
13756
13794
  // eslint-disable-next-line no-loop-func
13757
13795
  const cleanupSignalUrlEffect = effect(() => {
13758
13796
  const value = paramSignal.value;
13759
- const urlValue =
13760
- paramSignal.validity?.representations.url.value ?? value;
13761
13797
  // Use peek() to avoid subscribing to URL-derived signals.
13762
13798
  // This effect should only re-run when the param signal changes,
13763
13799
  // not when the URL changes (which would create a cycle: signal→URL→signal).
@@ -13783,11 +13819,11 @@ const route = (pattern, { searchParams } = {}) => {
13783
13819
  }
13784
13820
  if (debug) {
13785
13821
  console.debug(
13786
- `[route] Signal->URL: ${paramName} adding custom value ${urlValue} to URL (default: ${connection.getDefaultValue()})`,
13822
+ `[route] Signal->URL: ${paramName} adding custom value ${value} to URL (default: ${connection.getDefaultValue()})`,
13787
13823
  );
13788
13824
  }
13789
13825
  route.replaceParams(
13790
- { [paramName]: urlValue },
13826
+ { [paramName]: value },
13791
13827
  {
13792
13828
  callReason: `${paramName} signal change on ${route}`,
13793
13829
  isSignalChange: true,
@@ -13813,17 +13849,17 @@ const route = (pattern, { searchParams } = {}) => {
13813
13849
  return;
13814
13850
  }
13815
13851
 
13816
- if (urlValue === urlParamValue) {
13852
+ if (compareTwoJsValues(value, urlParamValue)) {
13817
13853
  // Values already match, no sync needed
13818
13854
  return;
13819
13855
  }
13820
13856
  if (debug) {
13821
13857
  console.debug(
13822
- `[route] Signal->URL: ${paramName} updating URL ${urlParamValue} -> ${urlValue}`,
13858
+ `[route] Signal->URL: ${paramName} updating URL ${urlParamValue} -> ${value}`,
13823
13859
  );
13824
13860
  }
13825
13861
  route.replaceParams(
13826
- { [paramName]: urlValue },
13862
+ { [paramName]: value },
13827
13863
  {
13828
13864
  callReason: `${paramName} signal change on ${route}`,
13829
13865
  isSignalChange: true,