@jsenv/navi 0.16.37 → 0.16.39

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.
@@ -4,6 +4,7 @@ import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
4
4
  import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, resolveCSSSize, findBefore, findAfter, createValueEffect, createStyleController, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, activeElementSignal, canInterceptKeys, pickLightOrDark, resolveColorLuminance, initFocusGroup, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, elementIsFocusable } from "@jsenv/dom";
5
5
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
6
6
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
7
+ import { createValidity } from "@jsenv/validity";
7
8
  import { createContext, render, isValidElement, toChildArray, createRef, cloneElement } from "preact";
8
9
  import { createPortal, forwardRef } from "preact/compat";
9
10
 
@@ -2113,100 +2114,44 @@ const localStorageSignal = (key) => {
2113
2114
  return valueSignal;
2114
2115
  };
2115
2116
 
2116
- const valueInLocalStorage = (
2117
- key,
2118
- { type = "string", fallback } = {},
2119
- ) => {
2120
- const converter = typeConverters[type];
2121
- if (converter === undefined) {
2122
- console.warn(
2123
- `Invalid type "${type}" for "${key}" in local storage, expected one of ${Object.keys(
2124
- typeConverters,
2125
- ).join(", ")}`,
2126
- );
2127
- }
2128
- const getValidityMessage = (
2129
- valueToCheck,
2130
- valueInLocalStorage = valueToCheck,
2131
- ) => {
2132
- if (!converter) {
2133
- return "";
2134
- }
2135
- if (!converter.checkValidity) {
2136
- return "";
2137
- }
2138
- const checkValidityResult = converter.checkValidity(valueToCheck);
2139
- if (checkValidityResult === false) {
2140
- return `${valueInLocalStorage}`;
2141
- }
2142
- if (!checkValidityResult) {
2143
- return "";
2144
- }
2145
- return `${checkValidityResult}, got "${valueInLocalStorage}"`;
2146
- };
2117
+ const valueInLocalStorage = (key, { type = "any" } = {}) => {
2118
+ const converter = TYPE_CONVERTERS[type];
2147
2119
 
2148
2120
  const get = () => {
2149
2121
  let valueInLocalStorage = window.localStorage.getItem(key);
2150
2122
  if (valueInLocalStorage === null) {
2151
- return fallback;
2123
+ return undefined;
2152
2124
  }
2125
+ let valueToReturn = valueInLocalStorage;
2153
2126
  if (converter && converter.decode) {
2154
- const valueDecoded = converter.decode(valueInLocalStorage);
2155
- const validityMessage = getValidityMessage(
2156
- valueDecoded,
2157
- valueInLocalStorage,
2158
- );
2159
- if (validityMessage) {
2160
- console.warn(
2161
- `The value found in localStorage "${key}" is invalid: ${validityMessage}`,
2162
- );
2127
+ try {
2128
+ const valueDecoded = converter.decode(valueInLocalStorage);
2129
+ valueToReturn = valueDecoded;
2130
+ } catch (e) {
2131
+ console.error(`Error decoding localStorage "${key}" value:`, e);
2163
2132
  return undefined;
2164
2133
  }
2165
- return valueDecoded;
2166
2134
  }
2167
- const validityMessage = getValidityMessage(valueInLocalStorage);
2168
- if (validityMessage) {
2135
+ if (type !== "any" && typeof valueToReturn !== type) {
2169
2136
  console.warn(
2170
- `The value found in localStorage "${key}" is invalid: ${validityMessage}`,
2137
+ `localStorage "${key}" value is invalid: should be a "${type}", got ${valueInLocalStorage}`,
2171
2138
  );
2172
2139
  return undefined;
2173
2140
  }
2174
- return valueInLocalStorage;
2141
+ return valueToReturn;
2175
2142
  };
2143
+
2176
2144
  const set = (value) => {
2177
2145
  if (value === undefined) {
2178
2146
  window.localStorage.removeItem(key);
2179
2147
  return;
2180
2148
  }
2181
-
2182
- let valueToSet = value;
2183
- let validityMessage = getValidityMessage(valueToSet);
2184
-
2185
- // If validation fails, try to convert the value
2186
- if (validityMessage && converter) {
2187
- const convertedValue = tryConvertValue(valueToSet, type);
2188
- if (convertedValue !== valueToSet) {
2189
- const convertedValidityMessage = getValidityMessage(convertedValue);
2190
- if (!convertedValidityMessage) {
2191
- // Conversion successful and valid
2192
- valueToSet = convertedValue;
2193
- validityMessage = "";
2194
- }
2195
- }
2196
- }
2197
-
2198
- if (validityMessage) {
2199
- console.warn(
2200
- `The value to set in localStorage "${key}" is invalid: ${validityMessage}`,
2201
- );
2202
- }
2203
-
2149
+ let valueToStore = value;
2204
2150
  if (converter && converter.encode) {
2205
- const valueEncoded = converter.encode(valueToSet);
2206
- window.localStorage.setItem(key, valueEncoded);
2207
- return;
2151
+ const valueEncoded = converter.encode(valueToStore);
2152
+ valueToStore = valueEncoded;
2208
2153
  }
2209
- window.localStorage.setItem(key, valueToSet);
2154
+ window.localStorage.setItem(key, valueToStore);
2210
2155
  };
2211
2156
  const remove = () => {
2212
2157
  window.localStorage.removeItem(key);
@@ -2215,169 +2160,41 @@ const valueInLocalStorage = (
2215
2160
  return [get, set, remove];
2216
2161
  };
2217
2162
 
2218
- const tryConvertValue = (value, type) => {
2219
- const validator = typeConverters[type];
2220
- if (!validator) {
2221
- return value;
2222
- }
2223
- if (!validator.cast) {
2224
- return value;
2225
- }
2226
- const fromType = typeof value;
2227
- const castFunction = validator.cast[fromType];
2228
- if (!castFunction) {
2229
- return value;
2230
- }
2231
- const convertedValue = castFunction(value);
2232
- return convertedValue;
2233
- };
2234
-
2235
- const createNumberValidator = ({ min, max, step } = {}) => {
2236
- return {
2237
- cast: {
2238
- string: (value) => {
2239
- const parsed = parseFloat(value);
2240
- if (!isNaN(parsed) && isFinite(parsed)) {
2241
- return parsed;
2242
- }
2243
- return value;
2244
- },
2245
- },
2246
- decode: (value) => {
2247
- const valueParsed = parseFloat(value);
2248
- return valueParsed;
2249
- },
2250
- checkValidity: (value) => {
2251
- if (typeof value !== "number") {
2252
- return `must be a number`;
2253
- }
2254
- if (!Number.isFinite(value)) {
2255
- return `must be finite`;
2256
- }
2257
- if (min !== undefined && value < min) {
2258
- return min === 0 ? `must be positive` : `must be >= ${min}`;
2259
- }
2260
- if (max !== undefined && value > max) {
2261
- return max === 0 ? `must be negative` : `must be <= ${max}`;
2262
- }
2263
- if (step !== undefined) {
2264
- const remainder = (value - (min || 0)) % step;
2265
- const epsilon = 0.0000001;
2266
- if (remainder > epsilon && step - remainder > epsilon) {
2267
- if (step === 1) {
2268
- return `must be an integer`;
2269
- }
2270
- return `must be a multiple of ${step}`;
2271
- }
2272
- }
2273
- return "";
2274
- },
2275
- };
2276
- };
2277
- const typeConverters = {
2163
+ const TYPE_CONVERTERS = {
2164
+ any: {
2165
+ decode: (valueFromLocalStorage) => JSON.parse(valueFromLocalStorage),
2166
+ encode: (value) => JSON.stringify(value),
2167
+ },
2278
2168
  boolean: {
2279
- cast: {
2280
- string: (value) => {
2281
- if (value === "true") return true;
2282
- if (value === "false") return false;
2283
- return value;
2284
- },
2285
- number: (value) => {
2286
- return Boolean(value);
2287
- },
2288
- },
2289
- checkValidity: (value) => {
2290
- if (typeof value !== "boolean") {
2291
- return `must be a boolean`;
2169
+ decode: (valueFromLocalStorage) => {
2170
+ if (
2171
+ valueFromLocalStorage === "true" ||
2172
+ valueFromLocalStorage === "on" ||
2173
+ valueFromLocalStorage === "1"
2174
+ ) {
2175
+ return true;
2292
2176
  }
2293
- return "";
2294
- },
2295
- decode: (value) => {
2296
- return value === "true";
2177
+ return false;
2297
2178
  },
2298
2179
  encode: (value) => {
2299
2180
  return value ? "true" : "false";
2300
2181
  },
2301
2182
  },
2302
- string: {
2303
- cast: {
2304
- number: String,
2305
- boolean: String,
2306
- },
2307
- checkValidity: (value) => {
2308
- if (typeof value !== "string") {
2309
- return `must be a string`;
2310
- }
2311
- return "";
2312
- },
2313
- },
2314
- number: createNumberValidator(),
2315
- float: createNumberValidator(),
2316
- positive_number: createNumberValidator({ min: 0 }),
2317
- integer: createNumberValidator({ step: 1 }),
2318
- positive_integer: createNumberValidator({ min: 0, step: 1 }),
2319
- percentage: {
2320
- cast: {
2321
- number: (value) => {
2322
- if (value >= 0 && value <= 100) {
2323
- return `${value}%`;
2324
- }
2325
- return value;
2326
- },
2327
- string: (value) => {
2328
- if (value.endsWith("%")) {
2329
- return value;
2330
- }
2331
- const parsed = parseFloat(value);
2332
- if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) {
2333
- return `${parsed}%`;
2334
- }
2335
- return value;
2336
- },
2337
- },
2338
- checkValidity: (value) => {
2339
- if (typeof value !== "string") {
2340
- return `must be a percentage`;
2341
- }
2342
- if (!value.endsWith("%")) {
2343
- return `must end with %`;
2344
- }
2345
- const percentageString = value.slice(0, -1);
2346
- const percentageFloat = parseFloat(percentageString);
2347
- if (typeof percentageFloat !== "number") {
2348
- return `must be a percentage`;
2349
- }
2350
- if (percentageFloat < 0 || percentageFloat > 100) {
2351
- return `must be between 0 and 100`;
2352
- }
2353
- return "";
2183
+ number: {
2184
+ decode: (valueFromLocalStorage) => {
2185
+ const valueParsed = parseFloat(valueFromLocalStorage);
2186
+ return valueParsed;
2354
2187
  },
2355
2188
  },
2356
2189
  object: {
2357
- cast: {
2358
- string: (value) => {
2359
- try {
2360
- return JSON.parse(value);
2361
- } catch {
2362
- // Invalid JSON, can't convert
2363
- return value;
2364
- }
2365
- },
2366
- },
2367
- decode: (value) => {
2368
- const valueParsed = JSON.parse(value);
2190
+ decode: (valueFromLocalStorage) => {
2191
+ const valueParsed = JSON.parse(valueFromLocalStorage);
2369
2192
  return valueParsed;
2370
2193
  },
2371
2194
  encode: (value) => {
2372
2195
  const valueStringified = JSON.stringify(value);
2373
2196
  return valueStringified;
2374
2197
  },
2375
- checkValidity: (value) => {
2376
- if (value === null || typeof value !== "object") {
2377
- return `must be an object`;
2378
- }
2379
- return "";
2380
- },
2381
2198
  },
2382
2199
  };
2383
2200
 
@@ -2417,7 +2234,6 @@ const generateSignalId = () => {
2417
2234
  * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
2418
2235
  * @param {number} [options.step] - For number type: step size for precision. Values will be rounded to nearest multiple of step.
2419
2236
  * @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
2420
- * @param {Function} [options.autoFix] - Function to call when validation fails to automatically fix the value
2421
2237
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
2422
2238
  * @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
2423
2239
  *
@@ -2478,10 +2294,11 @@ const NO_LOCAL_STORAGE = [() => undefined, () => {}, () => {}];
2478
2294
  const stateSignal = (defaultValue, options = {}) => {
2479
2295
  const {
2480
2296
  id,
2481
- type = "string",
2297
+ type,
2298
+ min,
2299
+ max,
2482
2300
  step,
2483
2301
  oneOf,
2484
- autoFix,
2485
2302
  persists = false,
2486
2303
  debug,
2487
2304
  default: staticFallback,
@@ -2512,15 +2329,6 @@ const stateSignal = (defaultValue, options = {}) => {
2512
2329
  ? valueInLocalStorage(localStorageKey, { type })
2513
2330
  : NO_LOCAL_STORAGE;
2514
2331
 
2515
- /**
2516
- * Value processor - applies step rounding for numbers, passthrough for others
2517
- */
2518
- const processValue =
2519
- type === "number" && step !== undefined
2520
- ? (value) =>
2521
- typeof value === "number" ? Math.round(value / step) * step : value
2522
- : (value) => value;
2523
-
2524
2332
  /**
2525
2333
  * Returns the current default value from code logic only (static or dynamic).
2526
2334
  * NEVER considers localStorage - used for URL building and route matching.
@@ -2591,6 +2399,49 @@ const stateSignal = (defaultValue, options = {}) => {
2591
2399
  };
2592
2400
 
2593
2401
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
2402
+ const [validity, updateValidity] = createValidity({
2403
+ type,
2404
+ min,
2405
+ max,
2406
+ step,
2407
+ oneOf,
2408
+ });
2409
+ const processValue = (value) => {
2410
+ const wasValid = validity.valid;
2411
+ updateValidity(value);
2412
+ if (validity.valid) {
2413
+ if (!wasValid) {
2414
+ if (debug) {
2415
+ console.debug(
2416
+ `[stateSignal:${signalIdString}] validation now passes`,
2417
+ { value },
2418
+ );
2419
+ }
2420
+ }
2421
+ return value;
2422
+ }
2423
+ if (debug) {
2424
+ console.debug(`[stateSignal:${signalIdString}] validation failed`, {
2425
+ value,
2426
+ oneOf,
2427
+ hasAutoFix: Boolean(validity.validSuggestion),
2428
+ });
2429
+ }
2430
+ if (validity.validSuggestion) {
2431
+ const validValue = validity.validSuggestion.value;
2432
+ if (debug) {
2433
+ console.debug(
2434
+ `[stateSignal:${signalIdString}] autoFix applied: ${value} → ${validValue}`,
2435
+ {
2436
+ value,
2437
+ validValue,
2438
+ },
2439
+ );
2440
+ }
2441
+ return validValue;
2442
+ }
2443
+ return value;
2444
+ };
2594
2445
  const preactSignal = signal(processValue(getFallbackValue()));
2595
2446
 
2596
2447
  // Create wrapper signal that applies step rounding on setValue
@@ -2612,7 +2463,6 @@ const stateSignal = (defaultValue, options = {}) => {
2612
2463
  },
2613
2464
  };
2614
2465
 
2615
- const validity = { valid: true };
2616
2466
  facadeSignal.validity = validity;
2617
2467
  facadeSignal.__signalId = signalIdString;
2618
2468
  facadeSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
@@ -2718,41 +2568,8 @@ const stateSignal = (defaultValue, options = {}) => {
2718
2568
  // update validity object according to the signal value
2719
2569
  {
2720
2570
  effect(() => {
2721
- const wasValid = validity.valid;
2722
2571
  const value = preactSignal.value;
2723
- updateValidity({ oneOf }, validity, value);
2724
- if (validity.valid) {
2725
- if (!wasValid) {
2726
- if (debug) {
2727
- console.debug(
2728
- `[stateSignal:${signalIdString}] validation now passes`,
2729
- { value },
2730
- );
2731
- }
2732
- }
2733
- return;
2734
- }
2735
- if (debug) {
2736
- console.debug(`[stateSignal:${signalIdString}] validation failed`, {
2737
- value,
2738
- oneOf,
2739
- hasAutoFix: Boolean(autoFix),
2740
- });
2741
- }
2742
- if (autoFix) {
2743
- const fixedValue = autoFix(value);
2744
- if (debug) {
2745
- console.debug(
2746
- `[stateSignal:${signalIdString}] autoFix applied: ${value} → ${fixedValue}`,
2747
- {
2748
- value,
2749
- fixedValue,
2750
- },
2751
- );
2752
- }
2753
- facadeSignal.value = fixedValue;
2754
- return;
2755
- }
2572
+ facadeSignal.value = processValue(value);
2756
2573
  });
2757
2574
  }
2758
2575
 
@@ -2773,6 +2590,8 @@ const stateSignal = (defaultValue, options = {}) => {
2773
2590
  isDefaultValue,
2774
2591
  type,
2775
2592
  step,
2593
+ min,
2594
+ max,
2776
2595
  persists,
2777
2596
  localStorageKey,
2778
2597
  debug,
@@ -2795,15 +2614,6 @@ const stateSignal = (defaultValue, options = {}) => {
2795
2614
  return facadeSignal;
2796
2615
  };
2797
2616
 
2798
- const updateValidity = (rules, validity, value) => {
2799
- const { oneOf } = rules;
2800
- if (oneOf && !oneOf.includes(value)) {
2801
- validity.valid = false;
2802
- return;
2803
- }
2804
- validity.valid = true;
2805
- };
2806
-
2807
2617
  const getCallerInfo = (targetFunction = null, additionalOffset = 0) => {
2808
2618
  const originalPrepareStackTrace = Error.prepareStackTrace;
2809
2619
  try {