@jsenv/navi 0.29.8 → 0.29.10

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.
@@ -1663,6 +1663,11 @@ const generateSignalId = () => {
1663
1663
  * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
1664
1664
  * @param {number} [options.step] - For number type: step size for precision. Values will be rounded to nearest multiple of step.
1665
1665
  * @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
1666
+ * @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
1667
+ * url only when explicitly named (`routeParams={{ edit: id }}`), never inherited from the signal's current
1668
+ * value, and it goes back to the default value when the route stops matching. Use it for params like an
1669
+ * "edit this one" id, where every other link to the screen must lead to the plain screen.
1670
+ * Incompatible with `persists` (throws).
1666
1671
  * @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
1667
1672
  * @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.
1668
1673
  *
@@ -1735,8 +1740,15 @@ const stateSignal = (defaultValue, options = {}) => {
1735
1740
  default: staticFallback,
1736
1741
  ignoreArrayOrder,
1737
1742
  autoFix,
1743
+ weak = false,
1738
1744
  } = options;
1739
1745
 
1746
+ if (weak && persists) {
1747
+ throw new TypeError(
1748
+ `stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
1749
+ );
1750
+ }
1751
+
1740
1752
  // Check if defaultValue is a signal (dynamic default) or static value
1741
1753
  const isDynamicDefault =
1742
1754
  defaultValue &&
@@ -2099,6 +2111,23 @@ setBaseUrl(
2099
2111
  : window.location.origin,
2100
2112
  );
2101
2113
 
2114
+ /**
2115
+ * The single place where "this param was not provided, take the signal value"
2116
+ * happens while building a url. A weak param qualifies one visit, not the
2117
+ * screen: it enters a url only when the caller names it (or when it is already
2118
+ * in the url being preserved), so here it always reads as absent.
2119
+ * Not reading the signal also keeps the built url from depending on it.
2120
+ */
2121
+ const readSignalForUrlBuild = (connection) => {
2122
+ if (!connection || !connection.signal) {
2123
+ return undefined;
2124
+ }
2125
+ if (connection.weak) {
2126
+ return undefined;
2127
+ }
2128
+ return connection.signal.value;
2129
+ };
2130
+
2102
2131
  /**
2103
2132
  * Creates a custom route pattern matcher
2104
2133
  */
@@ -2173,7 +2202,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2173
2202
  // Parameter was explicitly provided - always respect explicit parameters
2174
2203
  continue;
2175
2204
  }
2176
- const signalValue = connection.signal.value;
2205
+ const signalValue = readSignalForUrlBuild(connection);
2177
2206
  if (signalValue !== undefined) {
2178
2207
  // Parameter was not provided, check signal value
2179
2208
  resolvedParams[paramName] = signalValue;
@@ -2186,7 +2215,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2186
2215
  // Parameter was explicitly provided - always respect explicit parameters
2187
2216
  continue;
2188
2217
  }
2189
- const signalValue = connection.signal.value;
2218
+ const signalValue = readSignalForUrlBuild(connection);
2190
2219
  if (signalValue !== undefined) {
2191
2220
  // Parameter was not provided, check signal value
2192
2221
  resolvedParams[paramName] = signalValue;
@@ -2229,7 +2258,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2229
2258
  continue;
2230
2259
  }
2231
2260
 
2232
- const ancestorSignalValue = ancestorConnection.signal.value;
2261
+ const ancestorSignalValue = readSignalForUrlBuild(ancestorConnection);
2233
2262
  if (
2234
2263
  ancestorSignalValue !== undefined &&
2235
2264
  ancestorSignalValue !== ancestorConnection.getDefaultValue()
@@ -2307,7 +2336,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2307
2336
  if (childParam in resolvedParams) {
2308
2337
  continue;
2309
2338
  }
2310
- const childSignalValue = childConnection.signal.value;
2339
+ const childSignalValue = readSignalForUrlBuild(childConnection);
2311
2340
  // Only include if not already resolved and is non-default
2312
2341
  if (
2313
2342
  childSignalValue !== undefined &&
@@ -2349,7 +2378,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2349
2378
  }
2350
2379
  } else {
2351
2380
  // Parameter not provided but signal has a value
2352
- const signalValue = connection.signal.value;
2381
+ const signalValue = readSignalForUrlBuild(connection);
2353
2382
  if (connection.isCustomValue(signalValue)) {
2354
2383
  // Only include custom values
2355
2384
  filtered[paramName] = signalValue;
@@ -2368,7 +2397,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2368
2397
  }
2369
2398
  } else {
2370
2399
  // Parameter not provided but signal has a value
2371
- const signalValue = connection.signal.value;
2400
+ const signalValue = readSignalForUrlBuild(connection);
2372
2401
  if (connection.isCustomValue(signalValue)) {
2373
2402
  // Only include custom values
2374
2403
  filtered[paramName] = signalValue;
@@ -2385,7 +2414,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2385
2414
  const canReachLiteralValue = (literalValue, params, literalPosition) => {
2386
2415
  // Check parent's own parameters (signals and user params)
2387
2416
  const parentCanProvide = connections.some((conn) => {
2388
- const signalValue = conn.signal.value;
2417
+ const signalValue = readSignalForUrlBuild(conn);
2389
2418
  const userValue = params[conn.paramName];
2390
2419
  const effectiveValue = userValue !== undefined ? userValue : signalValue;
2391
2420
  return (
@@ -2414,7 +2443,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2414
2443
  return false;
2415
2444
  }
2416
2445
  return connsAtPosition.some((conn) => {
2417
- const signalValue = conn.signal.value;
2446
+ const signalValue = readSignalForUrlBuild(conn);
2418
2447
  return signalValue === literalValue && conn.isCustomValue(signalValue);
2419
2448
  });
2420
2449
  };
@@ -2469,7 +2498,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2469
2498
  pathConnectionMap.get(paramName) ||
2470
2499
  queryConnectionMap.get(paramName);
2471
2500
  if (parentConnection) {
2472
- parentParamValue = parentConnection.signal.value;
2501
+ parentParamValue = readSignalForUrlBuild(parentConnection);
2473
2502
  }
2474
2503
  }
2475
2504
 
@@ -2554,7 +2583,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2554
2583
  paramValue = item.userValue;
2555
2584
  } else {
2556
2585
  paramName = item.paramName;
2557
- paramValue = item.signal.value;
2586
+ paramValue = readSignalForUrlBuild(item);
2558
2587
  // Only include custom parent signal values (not using defaults)
2559
2588
  if (paramValue === undefined || !item.isCustomValue(paramValue)) {
2560
2589
  return { isCompatible: true, shouldInclude: false };
@@ -2661,7 +2690,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2661
2690
  if (!siblingConnection) {
2662
2691
  continue;
2663
2692
  }
2664
- const siblingSignalValue = siblingConnection.signal.value;
2693
+ const siblingSignalValue = readSignalForUrlBuild(siblingConnection);
2665
2694
  if (siblingSignalValue === undefined) {
2666
2695
  continue;
2667
2696
  }
@@ -2705,7 +2734,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2705
2734
  const explicitValue = params[paramName];
2706
2735
  const connection =
2707
2736
  pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
2708
- const signalValue = connection ? connection.signal.value : undefined;
2737
+ const signalValue = readSignalForUrlBuild(connection);
2709
2738
 
2710
2739
  // Check if the parameter has the required value
2711
2740
  if (
@@ -2759,7 +2788,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2759
2788
  hasActiveParams = true;
2760
2789
  }
2761
2790
  } else {
2762
- const signalValue = connection.signal.value;
2791
+ const signalValue = readSignalForUrlBuild(connection);
2763
2792
  if (signalValue !== undefined) {
2764
2793
  // No explicit override - use signal value
2765
2794
  childParams[paramName] = signalValue;
@@ -2976,7 +3005,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2976
3005
  }
2977
3006
  // If explicitly undefined, don't include it (which means don't use child route)
2978
3007
  } else {
2979
- const signalValue = connection.signal.value;
3008
+ const signalValue = readSignalForUrlBuild(connection);
2980
3009
  if (
2981
3010
  signalValue !== undefined &&
2982
3011
  connection.isCustomValue(signalValue)
@@ -3016,7 +3045,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3016
3045
  continue; // Already have this parameter
3017
3046
  }
3018
3047
 
3019
- const signalValue = connection.signal.value;
3048
+ const signalValue = readSignalForUrlBuild(connection);
3020
3049
  // Only include custom signal values (not using defaults)
3021
3050
  if (
3022
3051
  signalValue !== undefined &&
@@ -3198,7 +3227,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3198
3227
  paramName,
3199
3228
  connection,
3200
3229
  ] of childPatternObj.pathConnectionMap) {
3201
- const signalValue = connection.signal.value;
3230
+ const signalValue = readSignalForUrlBuild(connection);
3202
3231
  if (
3203
3232
  signalValue !== undefined &&
3204
3233
  connection.isCustomValue(signalValue)
@@ -3220,7 +3249,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3220
3249
  paramName,
3221
3250
  connection,
3222
3251
  ] of childPatternObj.queryConnectionMap) {
3223
- const signalValue = connection.signal.value;
3252
+ const signalValue = readSignalForUrlBuild(connection);
3224
3253
  if (
3225
3254
  signalValue !== undefined &&
3226
3255
  connection.isCustomValue(signalValue)
@@ -3840,7 +3869,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3840
3869
  );
3841
3870
  for (const conn of targetAncestor.connections) {
3842
3871
  console.debug(
3843
- `[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${conn.signal.value}, isCustom=${conn.isCustomValue(conn.signal.value)}`,
3872
+ `[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${readSignalForUrlBuild(conn)}, isCustom=${conn.isCustomValue(readSignalForUrlBuild(conn))}`,
3844
3873
  );
3845
3874
  }
3846
3875
  }
@@ -3857,7 +3886,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3857
3886
  }
3858
3887
 
3859
3888
  // Only include if not already processed and has custom value (not default)
3860
- const signalValue = connection.signal.value;
3889
+ const signalValue = readSignalForUrlBuild(connection);
3861
3890
  if (signalValue !== undefined) {
3862
3891
  // Don't include path parameters that correspond to literal segments we're optimizing away
3863
3892
  const targetParam = targetParams.find((p) => p.name === paramName);
@@ -3903,7 +3932,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3903
3932
  }
3904
3933
 
3905
3934
  // Only inherit custom values (not defaults) that we don't already have
3906
- const signalValue = connection.signal.value;
3935
+ const signalValue = readSignalForUrlBuild(connection);
3907
3936
  if (
3908
3937
  signalValue !== undefined &&
3909
3938
  connection.isCustomValue(signalValue)
@@ -3989,7 +4018,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
3989
4018
  }
3990
4019
 
3991
4020
  // Only inherit if we don't have this param and parent has custom value (not default)
3992
- const parentSignalValue = parentConnection.signal.value;
4021
+ const parentSignalValue = readSignalForUrlBuild(parentConnection);
3993
4022
  if (
3994
4023
  parentSignalValue !== undefined &&
3995
4024
  parentConnection.isCustomValue(parentSignalValue)
@@ -4075,7 +4104,36 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
4075
4104
  // search params are updated. When buildMostPreciseUrl performs an ancestor
4076
4105
  // optimisation (e.g. "/map/isochrone/compare" → "/map/isochrone") it is trusted
4077
4106
  // as-is because the built pathname will differ from the route's own base pathname.
4107
+ // Weak params are never inherited from their signal, but a url that already
4108
+ // carries one keeps it: staying on the same screen while another param
4109
+ // changes must not end the visit this param qualifies.
4110
+ const carryOverWeakParams = (currentUrl, params) => {
4111
+ let paramsWithWeak = params;
4112
+ for (const [paramName, connection] of [
4113
+ ...pathConnectionMap,
4114
+ ...queryConnectionMap,
4115
+ ]) {
4116
+ if (!connection.weak || paramName in paramsWithWeak) {
4117
+ continue;
4118
+ }
4119
+ const currentValue = connection.signal.peek();
4120
+ if (currentValue === undefined) {
4121
+ continue;
4122
+ }
4123
+ const currentParams = applyOn(currentUrl);
4124
+ if (!currentParams || currentParams[paramName] === undefined) {
4125
+ continue;
4126
+ }
4127
+ if (paramsWithWeak === params) {
4128
+ paramsWithWeak = { ...params };
4129
+ }
4130
+ paramsWithWeak[paramName] = currentParams[paramName];
4131
+ }
4132
+ return paramsWithWeak;
4133
+ };
4134
+
4078
4135
  const buildUrlPreservingPath = (currentUrl, params = {}) => {
4136
+ params = carryOverWeakParams(currentUrl, params);
4079
4137
  const relativeBuiltUrl = buildMostPreciseUrl(params);
4080
4138
  if (!currentUrl) {
4081
4139
  return resolveRouteUrl(relativeBuiltUrl);
@@ -4332,7 +4390,7 @@ const parsePattern = (pattern, { pathConnectionMap, queryConnectionMap }) => {
4332
4390
  if (
4333
4391
  connection &&
4334
4392
  connection.signal &&
4335
- connection.signal.value === undefined &&
4393
+ readSignalForUrlBuild(connection) === undefined &&
4336
4394
  !hasDefault
4337
4395
  ) {
4338
4396
  isOptional = true;
@@ -5516,7 +5574,10 @@ const route = (pattern, { searchParams } = {}) => {
5516
5574
  });
5517
5575
  };
5518
5576
  route.matchesParams = (providedParams) => {
5519
- const currentParams = route.params;
5577
+ // paramsSignal (not route.params) so a component calling this during render
5578
+ // subscribes to param changes: a navigation from /games/me/a to /games/me/b
5579
+ // leaves matchingSignal untouched, only the params change.
5580
+ const currentParams = route.paramsSignal.value;
5520
5581
  const resolvedParams = routePattern.resolveParams({
5521
5582
  ...currentParams,
5522
5583
  ...providedParams,
@@ -5883,6 +5944,22 @@ This prevents cross-test pollution and ensures clean state.`,
5883
5944
  }
5884
5945
  }
5885
5946
 
5947
+ // A weak param qualifies one visit: leaving the route ends it,
5948
+ // whatever the family and whatever the default. Coming back by a
5949
+ // url that does not carry the param comes back to a blank screen.
5950
+ if (connection.weak) {
5951
+ if (!parameterExtractedByMatchingRoute) {
5952
+ const defaultValue = connection.getDefaultValue();
5953
+ if (debug) {
5954
+ console.debug(
5955
+ `[route] weak param ${paramName}: route no longer matching, back to ${defaultValue}`,
5956
+ );
5957
+ }
5958
+ paramSignal.value = defaultValue;
5959
+ }
5960
+ continue;
5961
+ }
5962
+
5886
5963
  // Only reset signal if:
5887
5964
  // 1. We're navigating within the same route family (not to completely unrelated routes)
5888
5965
  // 2. AND no matching route extracts this parameter from URL
@@ -31360,6 +31437,15 @@ const useActionAsyncData = (action, {
31360
31437
  return [staleData, true, undefined];
31361
31438
  }
31362
31439
 
31440
+ // An action without params has nothing to run and no one to start it: this is
31441
+ // where a route action lands when its params getter returns false. It is not a
31442
+ // load in progress, so there is nothing to wait for — suspending here would
31443
+ // throw a promise that never settles, and the whole <Loading> subtree would
31444
+ // stay hidden for good, silently.
31445
+ if (runningState !== RUNNING && action.paramsSignal.peek() === undefined) {
31446
+ return [action.dataSignal.peek(), false, undefined];
31447
+ }
31448
+
31363
31449
  // IDLE or RUNNING with loadingEffect: "delegate" — suspend
31364
31450
  const reason = runningState === RUNNING ? "loading" : "idle";
31365
31451
  loadingRef.current = {
@@ -33534,11 +33620,12 @@ const collectBranches = children => {
33534
33620
  type: "container",
33535
33621
  node: child
33536
33622
  };
33623
+ const guardMatching = route ? route.matchingSignal.value : false;
33537
33624
  if (!matchingBranch) {
33538
33625
  if (matchingChild) {
33539
33626
  // Real leaf match inside — always select this container
33540
33627
  matchingBranch = branch;
33541
- } else if (route && route.matchingSignal.value) {
33628
+ } else if (guardMatching) {
33542
33629
  // No leaf match but an explicit route guard matches — select this
33543
33630
  // container so it can render its own fallback inside its layout
33544
33631
  matchingBranch = branch;
@@ -33556,7 +33643,12 @@ const collectBranches = children => {
33556
33643
  type: "leaf",
33557
33644
  node: child
33558
33645
  };
33559
- if (!matchingBranch && route.matchingSignal.value && (!routeParams || route.matchesParams(routeParams))) {
33646
+ // every signal is read even once a match is found: reading is what
33647
+ // subscribes the container to it, and a branch that is skipped today is
33648
+ // the one that must wake the container up tomorrow
33649
+ const matching = route.matchingSignal.value;
33650
+ const paramsMatching = routeParams ? route.matchesParams(routeParams) : true;
33651
+ if (!matchingBranch && matching && paramsMatching) {
33560
33652
  matchingBranch = branch;
33561
33653
  }
33562
33654
  }
@@ -51959,25 +52051,30 @@ installImportMetaCssBuild(import.meta);/**
51959
52051
  * A value one steps through, one press at a time: what is chosen sits between
51960
52052
  * the way back and the way on, and the two of them are the whole control.
51961
52053
  *
51962
- * Days for now DaySpin below but nothing here is about days except how
51963
- * one is written and what "one step" adds. The name says where this is going:
51964
- * a picker whose value is stepped rather than typed (a month, a page, a size),
51965
- * shown as the picker it is.
52054
+ * `Spin` knows nothing about what it steps through. It is handed how to move
52055
+ * one step (`valueAtStep`), how to tell two values apart (`compareValues`) and
52056
+ * what to write for one (`renderValue`); everything else the frame, the two
52057
+ * ways out, the ends one cannot go past, the value a form carries — is the
52058
+ * same whatever the value is. `DaySpin` and `NumberSpin` at the bottom are two
52059
+ * of those answers, and a caller can write a third.
52060
+ *
52061
+ * The middle is one of two things, and that is the only real fork here:
52062
+ *
52063
+ * - a value one PICKS (`DaySpin`): a headless picker holds it, pressing the
52064
+ * middle opens it, and the three slides of a looping container show the one
52065
+ * before, this one and the one after — a window over a row with no end.
52066
+ * - a value one TYPES (`editable`, `NumberSpin`): the middle IS the field.
52067
+ * Nothing travels — one cannot slide what is being typed into — and the
52068
+ * arrows step the value where the caret already is.
51966
52069
  *
51967
52070
  * A picker, so it lives here: what one presses in the middle IS a picker, and
51968
52071
  * the stepping is a way of showing it. It is headless and behind the three
51969
52072
  * slides — there is one value being chosen, so there is one picker for it.
51970
52073
  *
51971
- * Three slides for a row with no end, kept by a looping slide container: a
51972
- * press travels by one, then the window comes back to the middle while the
51973
- * value moves one step under it (see its `loop`/`onLoop`). So the three are
51974
- * only ever "the one before, this one, the one after".
51975
- *
51976
- * Two things take the keyboard and no more: the container (arrows step,
51977
- * Enter/Space open the picker) and the two chevrons. What is in the middle is
51978
- * not a control — a click on the container opens the picker by command — which
51979
- * is what keeps the focus where the travel happens instead of moving it into a
51980
- * slide that is about to leave.
52074
+ * Two things take the keyboard and no more: the middle (the container in the
52075
+ * picking case, the field in the typing one) and nothing else the chevrons
52076
+ * refuse it on purpose, which is what keeps the focus where the travel happens
52077
+ * instead of moving it into a slide that is about to leave.
51981
52078
  */
51982
52079
  const css$p = /* css */`
51983
52080
  @layer navi {
@@ -51991,7 +52088,7 @@ const css$p = /* css */`
51991
52088
 
51992
52089
  .navi_picker_spin {
51993
52090
  /* The padding is written on what is inside the box rather than on the box
51994
- — the day takes all four sides, the two chevrons only the vertical ones
52091
+ — the value takes all four sides, the two chevrons only the vertical ones
51995
52092
  — so the four sides are resolved once here, in the side-then-axis-then
51996
52093
  -shorthand order a Box resolves them in. What writes them: the padding
51997
52094
  props, through PICKER_SPIN_STYLE_CSS_VARS below. */
@@ -52025,7 +52122,7 @@ const css$p = /* css */`
52025
52122
  );
52026
52123
  /* What the loading outline is drawn around. */
52027
52124
  position: relative;
52028
- /* Written in the control font, like the picker it wraps: the day and its
52125
+ /* Written in the control font, like the picker it wraps: the value and its
52029
52126
  two chevrons are a control, not running text. */
52030
52127
  font-size: var(--navi-control-font-size);
52031
52128
  font-family: var(--navi-control-font-family);
@@ -52042,17 +52139,24 @@ const css$p = /* css */`
52042
52139
  keyboard here (see navi-focus-delegate below). */
52043
52140
  outline-color: var(--navi-focus-outline-color);
52044
52141
  outline-offset: 0px;
52142
+ /* No grey flash under a finger: what a press does is said by the chevron's
52143
+ own background, and the browser's rectangle is drawn square over corners
52144
+ that are round. Inherited, so the three pieces inside get it too. */
52145
+ -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
52045
52146
  }
52046
- /* The days hold the keyboard, and this box wears their ring: the container
52047
- fills it, so its own ring would be drawn a pixel inside this border and
52048
- two rings that close together read as a mistake. Same offer a dialog and a
52049
- popover answer (data-focus-outline-delegate, see slide_container.jsx), and
52050
- the same reply — the delegate stands down.
52147
+ /* The middle holds the keyboard, and this box wears its ring: whatever is in
52148
+ there fills it, so a ring of its own would be drawn a pixel inside this
52149
+ border and two rings that close together read as a mistake. Same offer a
52150
+ dialog and a popover answer (data-focus-outline-delegate, see
52151
+ slide_container.jsx), and the same reply — the delegate stands down; a
52152
+ field says it with an outline of zero width instead (see Spin's own
52153
+ outlineWidth below).
52051
52154
  Said on this box too (the first selector): nothing focuses it for real —
52052
- it is the container that takes the keyboard — but it is where the ring is
52155
+ it is the middle that takes the keyboard — but it is where the ring is
52053
52156
  drawn, so a demo can hold it there and show what it looks like. */
52054
52157
  .navi_picker_spin[data-focus-visible],
52055
- .navi_picker_spin:has([data-focus-outline-delegate][data-focus-visible]) {
52158
+ .navi_picker_spin:has([data-focus-outline-delegate][data-focus-visible]),
52159
+ .navi_picker_spin:has(.navi_input[data-focus-visible]) {
52056
52160
  outline-style: solid;
52057
52161
  }
52058
52162
  .navi_picker_spin [data-focus-outline-delegate] {
@@ -52103,6 +52207,14 @@ const css$p = /* css */`
52103
52207
  width: min(100%, var(--picker-spin-picker-width, 12ch));
52104
52208
  translate: -50% 0;
52105
52209
  }
52210
+ /* A middle one types into is the field itself: it takes the whole room
52211
+ between the chevrons rather than sitting in the centre of it, so the
52212
+ caret is where the value is and a click anywhere in the middle lands in
52213
+ the field. */
52214
+ .navi_picker_spin_middle > .navi_input {
52215
+ min-width: 0;
52216
+ flex: 1 1 auto;
52217
+ }
52106
52218
  /* Where the padding lands: all four sides on the value, the two vertical
52107
52219
  ones on the chevrons below — the same number above and below is what makes
52108
52220
  the three one line rather than three boxes, while sideways it is the room
@@ -52128,9 +52240,9 @@ const css$p = /* css */`
52128
52240
  .navi_picker_spin[data-disabled] [data-slide-container] {
52129
52241
  cursor: default;
52130
52242
  }
52131
- /* Kept inside its own slide: the three days share one cell, so anything
52132
- sticking out would be written across the two beside it. A day too long for
52133
- the box simply wraps — the box grows, and the words are all there; say
52243
+ /* Kept inside its own slide: the three values share one cell, so anything
52244
+ sticking out would be written across the two beside it. A value too long
52245
+ for the box simply wraps — the box grows, and the words are all there; say
52134
52246
  maxLines to cut it instead, which the text itself knows how to do. */
52135
52247
  .navi_picker_spin [data-slide] > * {
52136
52248
  max-width: 100%;
@@ -52157,7 +52269,12 @@ const css$p = /* css */`
52157
52269
  border-radius: 0;
52158
52270
  cursor: pointer;
52159
52271
  }
52160
- .navi_picker_spin > .navi_picker_spin_way_out:hover {
52272
+ /* Said as data-hover rather than :hover: a touch browser synthesizes the
52273
+ enter and never the leave, so a CSS :hover would stay grey under the last
52274
+ chevron pressed until something else was touched. What tracks it (see
52275
+ pseudo_styles.js) knows there is no hover on such a device and simply does
52276
+ not set the attribute. */
52277
+ .navi_picker_spin > .navi_picker_spin_way_out[data-hover] {
52161
52278
  background: color-mix(in srgb, currentColor 8%, transparent);
52162
52279
  }
52163
52280
  /* Nothing that way: still there, still pressable — pressing it is how one
@@ -52183,22 +52300,28 @@ const css$p = /* css */`
52183
52300
  of a rounded spin is rounded there too, and nowhere else — the two corners
52184
52301
  it does not own stay at the 0 above. Said with inherit rather than clipped
52185
52302
  away with overflow, which would cut the focus ring of the very button it
52186
- rounds. */
52303
+ rounds.
52304
+ Which chevron is which is asked of the chevron itself (data-way-out) rather
52305
+ than of its place among its siblings: the loading outline is a <span> too
52306
+ and it is written first, so :first-of-type named IT and the chevron at the
52307
+ start went unrounded. */
52187
52308
  .navi_picker_spin:not([data-vertical])
52188
- > .navi_picker_spin_way_out:first-of-type {
52309
+ > .navi_picker_spin_way_out[data-way-out="start"] {
52189
52310
  border-start-start-radius: inherit;
52190
52311
  border-end-start-radius: inherit;
52191
52312
  }
52192
52313
  .navi_picker_spin:not([data-vertical])
52193
- > .navi_picker_spin_way_out:last-of-type {
52314
+ > .navi_picker_spin_way_out[data-way-out="end"] {
52194
52315
  border-start-end-radius: inherit;
52195
52316
  border-end-end-radius: inherit;
52196
52317
  }
52197
- .navi_picker_spin[data-vertical] > .navi_picker_spin_way_out:first-of-type {
52318
+ .navi_picker_spin[data-vertical]
52319
+ > .navi_picker_spin_way_out[data-way-out="start"] {
52198
52320
  border-start-start-radius: inherit;
52199
52321
  border-start-end-radius: inherit;
52200
52322
  }
52201
- .navi_picker_spin[data-vertical] > .navi_picker_spin_way_out:last-of-type {
52323
+ .navi_picker_spin[data-vertical]
52324
+ > .navi_picker_spin_way_out[data-way-out="end"] {
52202
52325
  border-end-end-radius: inherit;
52203
52326
  border-end-start-radius: inherit;
52204
52327
  }
@@ -52206,53 +52329,73 @@ const css$p = /* css */`
52206
52329
 
52207
52330
  /**
52208
52331
  * @type {import("ignore:preact").FunctionComponent<{
52209
- * value?: string,
52210
- * defaultValue?: string,
52211
- * signal?: import("@preact/signals").Signal<string>,
52332
+ * value?: any,
52333
+ * defaultValue?: any,
52334
+ * signal?: import("@preact/signals").Signal<any>,
52212
52335
  * name?: string,
52213
- * min?: string,
52214
- * max?: string,
52336
+ * min?: any,
52337
+ * max?: any,
52215
52338
  * step?: number,
52339
+ * type?: string,
52340
+ * editable?: boolean,
52341
+ * growsUpward?: boolean,
52342
+ * fallbackValue?: any,
52343
+ * valueAtStep: (value: any, count: number) => any,
52344
+ * compareValues?: (a: any, b: any) => number,
52345
+ * renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
52346
+ * controlProps?: object,
52216
52347
  * duration?: number,
52217
- * lang?: string,
52218
- * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
52348
+ * vertical?: boolean,
52219
52349
  * previousLabel?: string,
52220
52350
  * nextLabel?: string,
52221
52351
  * [key: string]: any,
52222
52352
  * }>}
52223
- * @param {string} [value] The day shown, as "YYYY-MM-DD". Held from above:
52224
- * `uiAction` says when it should move. Say `signal` for a two-way binding
52225
- * instead, or `defaultValue` to let the spin hold the day itself.
52226
- * @param {number} [step=1] How many days a press covers 7 for a week at a
52227
- * time, and the label then names the day one lands on, as it always does.
52353
+ * @param {any} [value] The value shown. Held from above: `uiAction` says when
52354
+ * it should move. Say `signal` for a two-way binding instead, or
52355
+ * `defaultValue` to let the spin hold the value itself.
52356
+ * @param {(value: any, count: number) => any} valueAtStep What is `count` steps
52357
+ * away from `value` — the whole of what a spin has to know about what it
52358
+ * steps through. Called with -1 and +1 (times `step`) for the two ways out.
52359
+ * @param {(a: any, b: any) => number} [compareValues] Which of two values comes
52360
+ * first, the way a sort comparator answers. Only `min`/`max` need it, and the
52361
+ * default compares them as they compare with `<` — which is what an ISO date
52362
+ * wants and what a number does not.
52363
+ * @param {any} [fallbackValue] What is shown when nobody said anything: a spin
52364
+ * always shows a value, so there is always one to fall back on.
52365
+ * @param {string} [type="text"] What kind of value it is, handed to the control
52366
+ * holding it — a picker's `type` ("date", "time"…) or an input's.
52367
+ * @param {boolean} [editable] The middle is typed into rather than pressed:
52368
+ * the field IS the middle, and nothing travels. Without it the value is
52369
+ * picked — a headless picker behind three slides that travel one step at a
52370
+ * press.
52371
+ * @param {object} [controlProps] Anything else the control in the middle takes
52372
+ * (`inputMode`, `maxLength`, `placeholder`…).
52373
+ * @param {number} [step=1] How many steps a press covers.
52228
52374
  * @param {number} [duration=250] How long a travel takes, in milliseconds.
52229
- * @param {string} [min] The first day one can reach, as "YYYY-MM-DD"; `max` is
52230
- * the last. Beyond them the travel simply does not happen and the chevron
52231
- * that way says so.
52232
- * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
52233
- * Long by default, since this is a control one reads rather than a column
52234
- * one scans; `short` where the room is not there.
52375
+ * @param {any} [min] The first value one can reach; `max` is the last. Beyond
52376
+ * them the travel simply does not happen and the chevron that way says so.
52235
52377
  * @param {string} [padding] The room around the value, `paddingX`/`paddingY`
52236
52378
  * and the four sides included. It goes on what is inside the box rather than
52237
- * on the box: above and below it is taken by the day AND by the two chevrons,
52238
- * which is what makes the three the same height; sideways it is the room
52239
- * between the day and the chevron beside it. Left unsaid it is the padding
52240
- * every picker takes (`--navi-picker-padding-x-default` and its `-y` twin),
52241
- * so a theme that spaces its fields spaces this one with them.
52242
- * @param {number} [maxLines] How many lines the day may take before it is cut
52243
- * with an ellipsis — `maxLines={1}` keeps it on one line. Without it a day
52379
+ * on the box: above and below it is taken by the value AND by the two
52380
+ * chevrons, which is what makes the three the same height; sideways it is the
52381
+ * room between the value and the chevron beside it. Left unsaid it is the
52382
+ * padding every picker takes (`--navi-picker-padding-x-default` and its `-y`
52383
+ * twin), so a theme that spaces its fields spaces this one with them.
52384
+ * @param {number} [maxLines] How many lines the value may take before it is cut
52385
+ * with an ellipsis — `maxLines={1}` keeps it on one line. Without it a value
52244
52386
  * too long for the box wraps, and the box grows.
52245
52387
  * @param {boolean} [vertical] The same control standing up: the ways out above
52246
- * and below rather than left and right, and the days travelling upwards.
52388
+ * and below rather than left and right.
52389
+ * @param {boolean} [growsUpward] Which end of a standing spin holds the value
52390
+ * one steps up to. Off by default — what one walks through comes from below,
52391
+ * the way the next line of a list does. On for a quantity: pressing ▲ has to
52392
+ * mean a bigger number. Nothing to say about a spin lying down, where the way
52393
+ * on is always to the right.
52247
52394
  * Everything a box takes is taken here too — `width`, `borderWidth`,
52248
52395
  * `borderRadius`, `backgroundColor`: this IS a box, and its corners are passed
52249
52396
  * on to the chevrons sitting in them.
52250
- * @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
52251
- * to write for a day. Defaults to the date plus what it is to today when
52252
- * there is a word for it ("samedi 8 août (demain)"), since a day near now is
52253
- * read as a distance from now before it is read as a date.
52254
52397
  */
52255
- const DaySpin = ({
52398
+ const Spin = ({
52256
52399
  value,
52257
52400
  defaultValue,
52258
52401
  uiAction,
@@ -52262,76 +52405,77 @@ const DaySpin = ({
52262
52405
  max,
52263
52406
  step = 1,
52264
52407
  duration = 250,
52265
- lang,
52266
- format = "long",
52408
+ type = "text",
52409
+ editable,
52410
+ growsUpward,
52411
+ fallbackValue,
52412
+ valueAtStep,
52413
+ compareValues = compareValuesDefault,
52414
+ renderValue = renderValueDefault,
52415
+ controlProps,
52267
52416
  vertical,
52268
52417
  readOnly,
52269
52418
  disabled,
52270
52419
  loading,
52271
52420
  maxLines,
52272
- renderDay = renderDayDefault,
52273
52421
  previousLabel,
52274
52422
  nextLabel,
52275
52423
  ...rest
52276
52424
  }) => {
52277
52425
  import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
52278
52426
  const id = useId();
52279
- const containerId = `${id}_days`;
52280
- const pickerId = `${id}_picker`;
52281
- // The picker holds the day, and it is asked rather than shadowed: `value`,
52282
- // `defaultValue` and `signal` are handed to it untouched (see below), it
52283
- // settles which of them wins — and what a form makes of each — and this reads
52284
- // the answer back. Nothing of that story is told twice.
52285
- const pickerRef = useRef();
52286
- const dayFallback = firstDayAllowed({
52287
- min,
52288
- max,
52289
- step
52290
- });
52291
- const day = useControlUIState(pickerRef, value ?? defaultValue ?? signalProp?.peek()) ?? dayFallback;
52427
+ const containerId = `${id}_values`;
52428
+ const controlId = `${id}_control`;
52429
+ // The control holds the value, and it is asked rather than shadowed:
52430
+ // `value`, `defaultValue` and `signal` are handed to it untouched (see
52431
+ // below), it settles which of them wins — and what a form makes of each —
52432
+ // and this reads the answer back. Nothing of that story is told twice.
52433
+ const controlRef = useRef();
52434
+ const middleRef = useRef();
52435
+ const valueShown = useControlUIState(controlRef, value ?? defaultValue ?? signalProp?.peek()) ?? fallbackValue;
52292
52436
 
52293
52437
  // …and the other way round when a signal was handed over: a bound signal is
52294
- // the day, wherever it is moved from — the url, a back/forward, a button
52295
- // elsewhere on the page — and the control follows it. A picker seeded from a
52438
+ // the value, wherever it is moved from — the url, a back/forward, a button
52439
+ // elsewhere on the page — and the control follows it. A control seeded from a
52296
52440
  // signal only writes back into it (see resolveInputProps), which is enough
52297
- // for a field one only ever types into and not for a day that is also moved
52298
- // from outside. Undefined is not a day: the signal has nothing to say, so the
52299
- // control goes back to what it started on.
52300
- const signalDay = signalProp ? signalProp.value : undefined;
52301
- // The day as of the render this effect belongs to, and not one the closure
52441
+ // for a field one only ever types into and not for a value that is also moved
52442
+ // from outside. Undefined is not a value: the signal has nothing to say, so
52443
+ // the control goes back to what it started on.
52444
+ const signalValue = signalProp ? signalProp.value : undefined;
52445
+ // The value as of the render this effect belongs to, and not one the closure
52302
52446
  // captured a while ago: a step writes the signal, the signal brings us back
52303
- // here, and comparing against a stale day would set it a second time — one
52447
+ // here, and comparing against a stale value would set it a second time — one
52304
52448
  // uiAction per step becoming two.
52305
- const dayRef = useRef(day);
52306
- dayRef.current = day;
52449
+ const valueRef = useRef(valueShown);
52450
+ valueRef.current = valueShown;
52307
52451
  useLayoutEffect(() => {
52308
- const pickerEl = pickerRef.current;
52309
- if (!signalProp || !pickerEl) {
52452
+ const controlEl = controlRef.current;
52453
+ if (!signalProp || !controlEl) {
52310
52454
  return;
52311
52455
  }
52312
- if (signalDay === undefined) {
52313
- dispatchRequestResetUIState(pickerEl);
52456
+ if (signalValue === undefined) {
52457
+ dispatchRequestResetUIState(controlEl);
52314
52458
  return;
52315
52459
  }
52316
- if (signalDay !== dayRef.current) {
52317
- dispatchRequestSetUIState(pickerEl, signalDay, {});
52460
+ if (signalValue !== valueRef.current) {
52461
+ dispatchRequestSetUIState(controlEl, signalValue, {});
52318
52462
  }
52319
- }, [signalDay]);
52463
+ }, [signalValue]);
52320
52464
 
52321
- // A step is a change made to the picker, not beside it: it goes in the way a
52465
+ // A step is a change made to the control, not beside it: it goes in the way a
52322
52466
  // paste or a pick from the calendar goes in, so the signal, the form and
52323
52467
  // `uiAction` all learn about it from the same place — and the event that
52324
52468
  // asked for it travels with it, which is how `uiAction` can tell a chevron
52325
52469
  // from the calendar.
52326
- // What asked for the day being set, while it is being set: the picker
52470
+ // What asked for the value being set, while it is being set: the control
52327
52471
  // announces the change as its own input event, which says nothing of what
52328
52472
  // started it — so it is held here for the length of the dispatch and handed
52329
- // to uiAction below. That is how a caller tells a chevron from the calendar.
52473
+ // to uiAction below.
52330
52474
  const stepEventRef = useRef(null);
52331
- const setDay = (dayNext, event) => {
52475
+ const setValue = (valueNext, event) => {
52332
52476
  stepEventRef.current = event;
52333
52477
  try {
52334
- dispatchRequestSetUIState(pickerRef.current, dayNext, {
52478
+ dispatchRequestSetUIState(controlRef.current, valueNext, {
52335
52479
  event
52336
52480
  });
52337
52481
  } finally {
@@ -52340,29 +52484,29 @@ const DaySpin = ({
52340
52484
  };
52341
52485
 
52342
52486
  // Passed through rather than defaulted here: a prop nobody wrote must not
52343
- // reach the picker at all (it reads the presence of `value`, not its
52487
+ // reach the control at all (it reads the presence of `value`, not its
52344
52488
  // content), so each is added only if it was given.
52345
- const dayProps = {};
52489
+ const valueProps = {};
52346
52490
  if (value !== undefined) {
52347
- dayProps.value = value;
52491
+ valueProps.value = value;
52348
52492
  } else {
52349
52493
  if (signalProp) {
52350
- dayProps.signal = signalProp;
52494
+ valueProps.signal = signalProp;
52351
52495
  }
52352
- // A day is always shown, so the picker always HOLDS one: what was named, or
52353
- // what the signal starts on, or today (the nearest day a min/max/step
52354
- // leaves reachable). Said as a default rather than as a value, so a form
52355
- // reads the day shown as an answer one can send rather than as something it
52356
- // already holds and said even when a signal is bound, because a signal
52357
- // with nothing in it would otherwise leave the picker empty while the
52358
- // spin shows a day, and a form has nothing to send about an empty field.
52359
- dayProps.defaultValue = defaultValue ?? signalProp?.options?.getDefaultValue?.(false) ?? dayFallback;
52496
+ // A value is always shown, so the control always HOLDS one: what was named,
52497
+ // or what the signal starts on, or the fallback. Said as a default rather
52498
+ // than as a value, so a form reads what is shown as an answer one can send
52499
+ // rather than as something it already holds and said even when a signal
52500
+ // is bound, because a signal with nothing in it would otherwise leave the
52501
+ // control empty while the spin shows a value, and a form has nothing to
52502
+ // send about an empty field.
52503
+ valueProps.defaultValue = defaultValue ?? signalProp?.options?.getDefaultValue?.(false) ?? fallbackValue;
52360
52504
  }
52361
52505
 
52362
52506
  // Told from above as often as said here: a form running its action puts
52363
52507
  // every control inside it out of service (that is how the chevrons grey out
52364
- // by themselves), and the day they sit around must fade with them — it is one
52365
- // control, not a box with three moods.
52508
+ // by themselves), and the value they sit around must fade with them — it is
52509
+ // one control, not a box with three moods.
52366
52510
  const readOnlyFromAbove = useContext(ReadOnlyContext);
52367
52511
  const loadingFromAbove = useContext(LoadingContext$1);
52368
52512
  const disabledFromAbove = useContext(DisabledContext);
@@ -52388,15 +52532,55 @@ const DaySpin = ({
52388
52532
  }
52389
52533
  return undefined;
52390
52534
  };
52391
- const dayTextProps = {
52392
- lang,
52393
- format,
52394
- maxLines
52535
+ const valuePrevious = valueAtStep(valueShown, -step);
52536
+ const valueNext = valueAtStep(valueShown, step);
52537
+ const hasMin = min !== undefined && min !== "";
52538
+ const hasMax = max !== undefined && max !== "";
52539
+ const previousAllowed = !hasMin || compareValues(valuePrevious, min) >= 0;
52540
+ const nextAllowed = !hasMax || compareValues(valueNext, max) <= 0;
52541
+
52542
+ // Where the keyboard is put back after a press on a chevron: whatever holds
52543
+ // it in this spin, so one can keep going with the keys where one was. Found
52544
+ // in the middle rather than by id — a field's id lands on the box around it,
52545
+ // and what takes the keyboard is the input inside.
52546
+ const focusMiddle = () => {
52547
+ const target = editable ? middleRef.current?.querySelector(".navi_control_input") : document.getElementById(containerId);
52548
+ target?.focus({
52549
+ preventScroll: true
52550
+ });
52551
+ };
52552
+
52553
+ // Which end of a standing spin holds the value one steps UP to. A quantity
52554
+ // grows upwards — pressing ▲ on a number means a bigger number, and anything
52555
+ // else is read as a bug. What one walks through does the opposite: the day
52556
+ // after today arrives from below, the way the next line of a list does.
52557
+ const startIsNext = vertical && growsUpward;
52558
+ const valueAtStart = startIsNext ? valueNext : valuePrevious;
52559
+ const valueAtEnd = startIsNext ? valuePrevious : valueNext;
52560
+ const startAllowed = startIsNext ? nextAllowed : previousAllowed;
52561
+ const endAllowed = startIsNext ? previousAllowed : nextAllowed;
52562
+ const wayOut = atStart => {
52563
+ const isNext = atStart ? startIsNext : !startIsNext;
52564
+ return jsx(WayOut, {
52565
+ atStart: atStart,
52566
+ unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
52567
+ label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
52568
+ onPress: e => {
52569
+ focusMiddle();
52570
+ if (editable) {
52571
+ setValue(atStart ? valueAtStart : valueAtEnd, e);
52572
+ return;
52573
+ }
52574
+ // A direction on the map, not a value: the slides are laid out from
52575
+ // start to end (see below), so the way out at the start walks
52576
+ // backwards through them whichever value sits there.
52577
+ const command = atStart ? vertical ? "--navi-up" : "--navi-left" : vertical ? "--navi-down" : "--navi-right";
52578
+ triggerNaviCommand(e.currentTarget, command, e);
52579
+ },
52580
+ commandFor: editable ? undefined : containerId,
52581
+ children: atStart ? vertical ? jsx(ChevronUpSvg, {}) : jsx(ChevronLeftSvg, {}) : vertical ? jsx(ChevronDownSvg, {}) : jsx(ChevronRightSvg, {})
52582
+ });
52395
52583
  };
52396
- const dayPrevious = addDays(day, -step);
52397
- const dayNext = addDays(day, step);
52398
- const previousAllowed = !min || dayPrevious >= min;
52399
- const nextAllowed = !max || dayNext <= max;
52400
52584
  return jsxs(Box, {
52401
52585
  ...rest,
52402
52586
  baseClassName: "navi_picker_spin",
@@ -52415,132 +52599,167 @@ const DaySpin = ({
52415
52599
  loading: loading,
52416
52600
  color: "var(--navi-loader-color)",
52417
52601
  inset: -2
52418
- }), jsx(WayOut, {
52419
- command: vertical ? "--navi-up" : "--navi-left",
52420
- containerId: containerId,
52421
- unavailableMessage: wayOutMessage(previousAllowed, "spin.nothing_before"),
52422
- label: previousLabel ?? naviI18n("spin.previous"),
52423
- children: vertical ? jsx(ChevronUpSvg, {}) : jsx(ChevronLeftSvg, {})
52424
- }), jsxs("div", {
52602
+ }), wayOut(true), jsx("div", {
52425
52603
  className: "navi_picker_spin_middle",
52426
- children: [jsx(Picker, {
52427
- ref: pickerRef,
52428
- id: pickerId,
52429
- type: "date",
52430
- variant: "headless",
52431
- name: name
52432
- // Whatever was said about the day, said to the picker: a `value` it
52433
- // holds, a `signal` it follows, a `defaultValue` it merely starts on —
52434
- // including what a form makes of the difference (it HOLDS a value and
52435
- // has nothing to send back, where a default is a suggestion and
52436
- // confirming it is an answer). A day is always shown, so there is
52437
- // always a default: today, when nobody named one.
52438
- ,
52439
-
52440
- ...dayProps,
52604
+ ref: middleRef,
52605
+ children: editable ? jsx(Input, {
52606
+ ref: controlRef,
52607
+ type: type,
52608
+ name: name,
52609
+ ...controlProps,
52610
+ ...valueProps,
52441
52611
  min: min,
52442
52612
  max: max,
52613
+ step: step,
52443
52614
  readOnly: readOnly,
52444
52615
  disabled: disabled,
52445
- loading: loading,
52446
- uiAction: (dayNext, event) => {
52447
- uiAction?.(dayNext, stepEventRef.current ?? event);
52448
- }
52449
- }), jsxs(SlideContainer, {
52450
- id: containerId,
52451
- layout: vertical ? "column" : "row"
52452
- // What is left beside the two chevrons, whatever the days it holds are
52453
- // long: a control that resizes as one steps through it is a control one
52454
- // has to aim at twice.
52616
+ loading: loading
52617
+ // No frame of its own inside a frame, and no ring of its own
52618
+ // either: the spin draws both (see the CSS above), and an outline
52619
+ // of zero width is how a field stands down without its focus
52620
+ // state being touched.
52455
52621
  ,
52456
52622
 
52623
+ variant: "discrete",
52624
+ outlineWidth: "0",
52625
+ textAlign: "center",
52457
52626
  expandX: true,
52458
- defaultCurrent: "current",
52459
- duration: `${duration}ms`
52460
- // The three days are a window over an endless row: the container plays
52461
- // the travel and comes back to the middle, and the day moves one step
52462
- // here, in onLoop, as it lands.
52463
- ,
52627
+ uiAction: (valueNext, event) => {
52628
+ uiAction?.(valueNext, stepEventRef.current ?? event);
52629
+ }
52630
+ }) : jsxs(Fragment$1, {
52631
+ children: [jsx(Picker, {
52632
+ ref: controlRef,
52633
+ id: controlId,
52634
+ type: type,
52635
+ variant: "headless",
52636
+ name: name,
52637
+ ...controlProps,
52638
+ ...valueProps,
52639
+ min: min,
52640
+ max: max,
52641
+ readOnly: readOnly,
52642
+ disabled: disabled,
52643
+ loading: loading,
52644
+ uiAction: (valueNext, event) => {
52645
+ uiAction?.(valueNext, stepEventRef.current ?? event);
52646
+ }
52647
+ }), jsxs(SlideContainer, {
52648
+ id: containerId,
52649
+ layout: vertical ? "column" : "row"
52650
+ // What is left beside the two chevrons, whatever the values it
52651
+ // holds are long: a control that resizes as one steps through it
52652
+ // is a control one has to aim at twice.
52653
+ ,
52464
52654
 
52465
- loop: true,
52466
- onLoop: ({
52467
- dx,
52468
- dy,
52469
- event
52470
- }) => {
52471
- // One step, whichever axis it came from: the map is a line, so only
52472
- // one of the two is ever anything but zero. The event goes with it —
52473
- // it is what says a chevron (or an arrow key) asked for this day.
52474
- setDay(addDays(day, (dx || dy) * step), event);
52475
- }
52476
- // The whole middle opens the calendar — a command, like the chevrons
52477
- // send one, and no button of its own: the day would then be one more
52478
- // Tab stop, and the focus would follow it out of the box as it travels.
52479
- ,
52655
+ expandX: true,
52656
+ defaultCurrent: "current",
52657
+ duration: `${duration}ms`
52658
+ // The three values are a window over an endless row: the
52659
+ // container plays the travel and comes back to the middle, and
52660
+ // the value moves one step here, in onLoop, as it lands.
52661
+ ,
52480
52662
 
52481
- commandFor: pickerId
52482
- // Sent whatever state the control is in: the picker is the one that
52483
- // knows it cannot be opened right now, and refusing there is what says
52484
- // so out loud (read-only, busy). Refusing here would be a press that
52485
- // does nothing and explains nothing.
52486
- // preventDefault, because a <Label> around the whole control forwards
52487
- // a click to what it labels the picker and that would open the
52488
- // calendar a second time, right after this command did.
52489
- ,
52663
+ loop: true,
52664
+ onLoop: ({
52665
+ dx,
52666
+ dy,
52667
+ event
52668
+ }) => {
52669
+ // One step, whichever axis it came from: the map is a line, so
52670
+ // only one of the two is ever anything but zero. Towards the
52671
+ // end of the line is a step forward, unless the line was laid
52672
+ // out the other way round (startIsNext). The event goes with
52673
+ // it — it is what says a chevron (or an arrow key) asked for
52674
+ // this value.
52675
+ const towardsEnd = dx || dy;
52676
+ setValue(valueAtStep(valueShown, (startIsNext ? -towardsEnd : towardsEnd) * step), event);
52677
+ }
52678
+ // The whole middle opens the picker — a command, like the
52679
+ // chevrons send one, and no button of its own: the value would
52680
+ // then be one more Tab stop, and the focus would follow it out of
52681
+ // the box as it travels.
52682
+ ,
52490
52683
 
52491
- onClick: e => {
52492
- e.preventDefault();
52493
- triggerNaviCommand(e.currentTarget, "--navi-open", e);
52494
- },
52495
- children: [jsx(Slide, {
52496
- area: "previous",
52497
- flex: true,
52498
- align: "center",
52499
- children: renderDay(dayPrevious, dayTextProps)
52500
- }), jsx(Slide, {
52501
- area: "current",
52502
- flex: true,
52503
- align: "center"
52504
- // The days a min/max leaves out are simply not reachable: the way out
52505
- // is closed on the slide being left, so a key, a chevron and a
52506
- // command are all stopped by the same thing.
52684
+ commandFor: controlId
52685
+ // Sent whatever state the control is in: the picker is the one
52686
+ // that knows it cannot be opened right now, and refusing there is
52687
+ // what says so out loud (read-only, busy). Refusing here would be
52688
+ // a press that does nothing and explains nothing.
52689
+ // preventDefault, because a <Label> around the whole control
52690
+ // forwards a click to what it labels — the picker — and that
52691
+ // would open the calendar a second time, right after this command
52692
+ // did.
52507
52693
  ,
52508
52694
 
52509
- preventNavPrevious: !previousAllowed,
52510
- preventNavNext: !nextAllowed,
52511
- children: renderDay(day, dayTextProps)
52512
- }), jsx(Slide, {
52513
- area: "next",
52514
- flex: true,
52515
- align: "center",
52516
- children: renderDay(dayNext, dayTextProps)
52695
+ onClick: e => {
52696
+ e.preventDefault();
52697
+ triggerNaviCommand(e.currentTarget, "--navi-open", e);
52698
+ },
52699
+ children: [jsx(Slide, {
52700
+ area: "start",
52701
+ flex: true,
52702
+ align: "center",
52703
+ children: renderValue(valueAtStart, {
52704
+ maxLines
52705
+ })
52706
+ }), jsx(Slide, {
52707
+ area: "current",
52708
+ flex: true,
52709
+ align: "center"
52710
+ // The values a min/max leaves out are simply not reachable: the
52711
+ // way out is closed on the slide being left, so a key, a
52712
+ // chevron and a command are all stopped by the same thing.
52713
+ ,
52714
+
52715
+ preventNavPrevious: !startAllowed,
52716
+ preventNavNext: !endAllowed,
52717
+ children: renderValue(valueShown, {
52718
+ maxLines
52719
+ })
52720
+ }), jsx(Slide, {
52721
+ area: "end",
52722
+ flex: true,
52723
+ align: "center",
52724
+ children: renderValue(valueAtEnd, {
52725
+ maxLines
52726
+ })
52727
+ })]
52517
52728
  })]
52518
- })]
52519
- }), jsx(WayOut, {
52520
- command: vertical ? "--navi-down" : "--navi-right",
52521
- containerId: containerId,
52522
- unavailableMessage: wayOutMessage(nextAllowed, "spin.nothing_after"),
52523
- label: nextLabel ?? naviI18n("spin.next"),
52524
- children: vertical ? jsx(ChevronDownSvg, {}) : jsx(ChevronRightSvg, {})
52525
- })]
52729
+ })
52730
+ }), wayOut(false)]
52526
52731
  });
52527
52732
  };
52528
52733
 
52529
52734
  // A way out is a place one presses, not a control: no <button>, on purpose.
52530
52735
  // A <button> is labelable, so a <Label> wrapping the whole spin would bind
52531
- // to the first chevron instead of to the picker — the thing that actually holds
52532
- // the value — and a form would carry two more controls that answer for nothing.
52533
- // It is not focusable either: the keyboard walks the days on the container (see
52534
- // its own tabIndex), where the arrows already mean this.
52736
+ // to the first chevron instead of to the control — the thing that actually
52737
+ // holds the value — and a form would carry two more controls that answer for
52738
+ // nothing.
52739
+ // It is not focusable either: the keyboard walks the values on the middle (see
52740
+ // the container's own tabIndex, or the field's), where the arrows already mean
52741
+ // this.
52535
52742
  const WayOut = ({
52536
- command,
52537
- containerId,
52743
+ atStart,
52744
+ commandFor,
52538
52745
  unavailableMessage,
52539
52746
  label,
52747
+ onPress,
52540
52748
  children
52541
52749
  }) => jsx(Box, {
52542
52750
  as: "span",
52543
52751
  baseClassName: "navi_picker_spin_way_out"
52752
+ // Which end of the box it sits in, said by the chevron rather than read
52753
+ // from its place among its siblings: that is what the corners it is
52754
+ // rounded by are keyed on (see the CSS above).
52755
+ ,
52756
+
52757
+ "data-way-out": atStart ? "start" : "end"
52758
+ // Tracked rather than left to CSS :hover, which stays on after a tap on a
52759
+ // touch device (see the CSS above).
52760
+ ,
52761
+
52762
+ pseudoClasses: WAY_OUT_PSEUDO_CLASSES
52544
52763
  // Announced as a button because that is what it is to whoever cannot see
52545
52764
  // the chevron — and marked unavailable rather than removed when there is
52546
52765
  // nothing that way, so it keeps its place.
@@ -52560,49 +52779,44 @@ const WayOut = ({
52560
52779
  // Read by triggerNaviCommand below the same way it reads a button's own.
52561
52780
  ,
52562
52781
 
52563
- commandfor: containerId,
52782
+ commandfor: commandFor,
52564
52783
  flex: true,
52565
52784
  align: "center"
52566
52785
  // A press, answered where it starts: mousedown rather than click, which is
52567
52786
  // what makes holding one feel immediate — and the click after it is stopped
52568
52787
  // below, so a <Label> wrapping the whole control does not forward it to the
52569
- // picker and open the calendar on the way past.
52788
+ // control and open the calendar on the way past.
52570
52789
  ,
52571
52790
 
52572
52791
  onClick: e => {
52573
52792
  e.preventDefault();
52574
52793
  },
52575
52794
  onMouseDown: e => {
52576
- // No focus, no text selection: the keyboard is put on the days below.
52795
+ // No focus, no text selection: the keyboard is put on the middle below.
52577
52796
  e.preventDefault();
52578
- const wayOutElement = e.currentTarget;
52579
52797
  if (unavailableMessage) {
52580
52798
  // Why it does nothing, said where one pressed: a control would have
52581
52799
  // done this through its own interaction gate, and this one has none.
52582
52800
  openCallout(unavailableMessage, {
52583
- anchorElement: wayOutElement,
52801
+ anchorElement: e.currentTarget,
52584
52802
  status: "info",
52585
52803
  openingEvent: e
52586
52804
  });
52587
52805
  return;
52588
52806
  }
52589
- // The keyboard follows the press: the days are where the arrows work, so
52590
- // pressing a chevron leaves one able to keep going with the keys.
52591
- document.getElementById(containerId)?.focus({
52592
- preventScroll: true
52593
- });
52594
- triggerNaviCommand(wayOutElement, command, e);
52807
+ onPress(e);
52595
52808
  },
52596
52809
  children: jsx(Icon, {
52597
52810
  children: children
52598
52811
  })
52599
52812
  });
52600
52813
  const PICKER_SPIN_PSEUDO_CLASSES = [":hover", ":focus-visible"];
52814
+ const WAY_OUT_PSEUDO_CLASSES = [":hover"];
52601
52815
 
52602
52816
  // A padding written on this box would sit between its border and the chevrons,
52603
52817
  // which are meant to reach the corners they are rounded by — so each padding
52604
- // prop becomes a variable instead, and the CSS above hands it to the day and to
52605
- // the chevrons themselves. Same trick, and same var chain, as Picker's.
52818
+ // prop becomes a variable instead, and the CSS above hands it to the value and
52819
+ // to the chevrons themselves. Same trick, and same var chain, as Picker's.
52606
52820
  const PICKER_SPIN_STYLE_CSS_VARS = {
52607
52821
  padding: "--picker-spin-padding",
52608
52822
  paddingX: "--picker-spin-padding-x",
@@ -52613,6 +52827,138 @@ const PICKER_SPIN_STYLE_CSS_VARS = {
52613
52827
  paddingLeft: "--picker-spin-padding-left"
52614
52828
  };
52615
52829
 
52830
+ // Two values compared the way `<` compares them: right for an ISO day, a time
52831
+ // or anything else written so that its order IS its alphabetical order. A
52832
+ // number is the exception, and NumberSpin below says so.
52833
+ const compareValuesDefault = (a, b) => {
52834
+ if (a < b) {
52835
+ return -1;
52836
+ }
52837
+ if (a > b) {
52838
+ return 1;
52839
+ }
52840
+ return 0;
52841
+ };
52842
+ const renderValueDefault = value => String(value ?? "");
52843
+
52844
+ /**
52845
+ * A whole number one steps through and types into: the field IS the middle, so
52846
+ * the value can be typed as readily as stepped, and the two chevrons stand
52847
+ * above and below it by default — sideways they would be where the caret
52848
+ * moves — with the bigger number up top.
52849
+ *
52850
+ * @type {import("ignore:preact").FunctionComponent<{
52851
+ * value?: number|string,
52852
+ * defaultValue?: number|string,
52853
+ * min?: number,
52854
+ * max?: number,
52855
+ * step?: number,
52856
+ * [key: string]: any,
52857
+ * }>}
52858
+ * @param {number} [min=0] The lowest number one can reach; `max` is the
52859
+ * highest. They also bound what typing can produce, and how wide the field
52860
+ * is asked to be (see `maxLength`).
52861
+ */
52862
+ const NumberSpin = ({
52863
+ min = 0,
52864
+ max,
52865
+ step = 1,
52866
+ vertical = true,
52867
+ growsUpward = true,
52868
+ controlProps,
52869
+ ...rest
52870
+ }) => jsx(Spin, {
52871
+ type: "navi_number",
52872
+ editable: true,
52873
+ growsUpward: growsUpward,
52874
+ min: min,
52875
+ max: max,
52876
+ step: step,
52877
+ vertical: vertical,
52878
+ fallbackValue: min,
52879
+ valueAtStep: (value, count) => numberAtStep(value, count, min),
52880
+ compareValues: (a, b) => Number(a) - Number(b),
52881
+ controlProps: {
52882
+ // The numeric keypad on a phone, and — through
52883
+ // input_resolver_mode — the "this field is full" event a group of
52884
+ // fields moves along on (see useInputGroup).
52885
+ inputMode: "numeric",
52886
+ maxLength: max === undefined ? undefined : String(max).length,
52887
+ ...controlProps
52888
+ },
52889
+ ...rest
52890
+ });
52891
+
52892
+ // One step away, bounds included: a step past `max` is a real number that
52893
+ // simply is not allowed, and Spin is the one that reads it as "nothing that
52894
+ // way" — clamping here would answer the chevron before it had a chance to say
52895
+ // so. A field mid-edit holding nothing (or nothing numeric) starts from `min`.
52896
+ const numberAtStep = (value, count, min) => {
52897
+ const number = Number(value);
52898
+ if (value === "" || value === undefined || Number.isNaN(number)) {
52899
+ return min;
52900
+ }
52901
+ return number + count;
52902
+ };
52903
+
52904
+ /**
52905
+ * A day one steps through: the date plus what it is to today when there is a
52906
+ * word for it, and a calendar behind it for the day that is far from here.
52907
+ *
52908
+ * @type {import("ignore:preact").FunctionComponent<{
52909
+ * value?: string,
52910
+ * defaultValue?: string,
52911
+ * signal?: import("@preact/signals").Signal<string>,
52912
+ * name?: string,
52913
+ * min?: string,
52914
+ * max?: string,
52915
+ * step?: number,
52916
+ * lang?: string,
52917
+ * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
52918
+ * [key: string]: any,
52919
+ * }>}
52920
+ * @param {string} [value] The day shown, as "YYYY-MM-DD".
52921
+ * @param {number} [step=1] How many days a press covers — 7 for a week at a
52922
+ * time, and the label then names the day one lands on, as it always does.
52923
+ * @param {string} [min] The first day one can reach, as "YYYY-MM-DD"; `max` is
52924
+ * the last.
52925
+ * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
52926
+ * Long by default, since this is a control one reads rather than a column
52927
+ * one scans; `short` where the room is not there.
52928
+ * @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
52929
+ * to write for a day. Defaults to the date plus what it is to today when
52930
+ * there is a word for it ("samedi 8 août (demain)"), since a day near now is
52931
+ * read as a distance from now before it is read as a date.
52932
+ */
52933
+ const DaySpin = ({
52934
+ min,
52935
+ max,
52936
+ step = 1,
52937
+ lang,
52938
+ format = "long",
52939
+ renderDay = renderDayDefault,
52940
+ ...rest
52941
+ }) => jsx(Spin, {
52942
+ type: "date",
52943
+ min: min,
52944
+ max: max,
52945
+ step: step,
52946
+ fallbackValue: firstDayAllowed({
52947
+ min,
52948
+ max,
52949
+ step
52950
+ }),
52951
+ valueAtStep: addDays,
52952
+ renderValue: (day, {
52953
+ maxLines
52954
+ }) => renderDay(day, {
52955
+ lang,
52956
+ format,
52957
+ maxLines
52958
+ }),
52959
+ ...rest
52960
+ });
52961
+
52616
52962
  // The date, and what it is to today when that is something one has a word for:
52617
52963
  // "samedi 8 août (demain)" says both where one is and how far that is, and only
52618
52964
  // the second is read at a glance.
@@ -54565,6 +54911,15 @@ const css$k = /* css */`
54565
54911
  --wheel-window-inset: calc((100% - var(--wheel-item-height)) / 2);
54566
54912
  --wheel-band-inset: calc((100% - var(--wheel-emphasis-size)) / 2);
54567
54913
 
54914
+ /* As wide as the rows ON SCREEN, which is not the same as "as wide as
54915
+ the widest value": the rows are a handful of recycled slots, so a wheel
54916
+ whose labels are not all the same width resizes as one scrolls — "9"
54917
+ becoming "10" widens it mid-glide, and whatever holds it (a popup)
54918
+ moves under the finger. Give such a wheel a width and the question is
54919
+ settled once; the per-row padding stays where it is, inside it, and
54920
+ stays scrollable. Digits are the case where this does not arise on its
54921
+ own: data-wheel-type="integer" makes them tabular above, so they all
54922
+ advance the same — until one of them takes a digit more. */
54568
54923
  width: fit-content;
54569
54924
 
54570
54925
  .navi_wheel_viewport {
@@ -54649,6 +55004,9 @@ const css$k = /* css */`
54649
55004
  --wheel-window-inset: calc((100% - var(--wheel-item-width)) / 2);
54650
55005
  --wheel-band-inset: calc((100% - var(--wheel-emphasis-size)) / 2);
54651
55006
 
55007
+ /* The cross axis follows the rows on screen — same story as the vertical
55008
+ branch above, and the same answer: say a height when the rows are not
55009
+ all as tall as each other. */
54652
55010
  height: fit-content;
54653
55011
 
54654
55012
  .navi_wheel_viewport {
@@ -62425,5 +62783,5 @@ const UserSvg = () => jsx("svg", {
62425
62783
  })
62426
62784
  });
62427
62785
 
62428
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
62786
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
62429
62787
  //# sourceMappingURL=jsenv_navi.js.map