@jsenv/navi 0.29.9 → 0.29.11

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
@@ -9665,7 +9742,7 @@ const setupNetworkMonitoring = () => {
9665
9742
  };
9666
9743
  setupNetworkMonitoring();
9667
9744
 
9668
- installImportMetaCssBuild(import.meta);const css$X = /* css */`
9745
+ installImportMetaCssBuild(import.meta);const css$Y = /* css */`
9669
9746
  .navi_loading_indicator_fluid_container {
9670
9747
  position: relative;
9671
9748
  display: flex;
@@ -9697,7 +9774,7 @@ const LoadingIndicatorFluid = ({
9697
9774
  visuallyHidden,
9698
9775
  ...rest
9699
9776
  }) => {
9700
- import.meta.css = [css$X, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
9777
+ import.meta.css = [css$Y, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
9701
9778
  const ref = useRef(null);
9702
9779
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
9703
9780
  const [containerWidth, setContainerWidth] = useState(0);
@@ -9902,7 +9979,7 @@ const LoadingRectangleSvg = ({
9902
9979
  });
9903
9980
  };
9904
9981
 
9905
- installImportMetaCssBuild(import.meta);const css$W = /* css */`
9982
+ installImportMetaCssBuild(import.meta);const css$X = /* css */`
9906
9983
  .navi_loading_outline_wrapper {
9907
9984
  position: absolute;
9908
9985
  /* Controls place the outline slightly outside their box, right on top of
@@ -9939,7 +10016,7 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
9939
10016
  }
9940
10017
  `;
9941
10018
  const LoadingOutline = props => {
9942
- import.meta.css = [css$W, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
10019
+ import.meta.css = [css$X, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
9943
10020
  if (props.containerRef) {
9944
10021
  const container = props.containerRef.current;
9945
10022
  if (!container) {
@@ -10264,7 +10341,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
10264
10341
  };
10265
10342
 
10266
10343
  installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
10267
- const css$V = /* css */`
10344
+ const css$W = /* css */`
10268
10345
  @layer navi {
10269
10346
  .navi_text {
10270
10347
  &[data-skeleton] {
@@ -10770,7 +10847,7 @@ const TextShrinkWrap = props => {
10770
10847
  });
10771
10848
  };
10772
10849
  const TextUI = props => {
10773
- import.meta.css = [css$V, "@jsenv/navi/src/text/text.jsx"];
10850
+ import.meta.css = [css$W, "@jsenv/navi/src/text/text.jsx"];
10774
10851
  let {
10775
10852
  ref,
10776
10853
  spacing,
@@ -13484,7 +13561,7 @@ installImportMetaCssBuild(import.meta);/**
13484
13561
  * - Arrow automatically shows when pointing at a valid anchor element
13485
13562
  * - Centers in viewport when no anchor element provided or anchor is too big
13486
13563
  */
13487
- const css$U = /* css */`
13564
+ const css$V = /* css */`
13488
13565
  @layer navi {
13489
13566
  .navi_callout {
13490
13567
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -13704,7 +13781,7 @@ const openCallout = (message, {
13704
13781
  skipFocus = false,
13705
13782
  debug = () => {}
13706
13783
  } = {}) => {
13707
- import.meta.css = [css$U, "@jsenv/navi/src/control/rules/callout/callout.js"];
13784
+ import.meta.css = [css$V, "@jsenv/navi/src/control/rules/callout/callout.js"];
13708
13785
  if (debug === true) {
13709
13786
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
13710
13787
  }
@@ -22782,7 +22859,7 @@ const getAssociatedLabels = element => {
22782
22859
  return [];
22783
22860
  };
22784
22861
 
22785
- installImportMetaCssBuild(import.meta);const css$T = /* css */`
22862
+ installImportMetaCssBuild(import.meta);const css$U = /* css */`
22786
22863
  @layer navi {
22787
22864
  .navi_button {
22788
22865
  --button-border-radius: var(--navi-control-border-radius);
@@ -23175,7 +23252,7 @@ installImportMetaCssBuild(import.meta);const css$T = /* css */`
23175
23252
  }
23176
23253
  `;
23177
23254
  const ButtonUI = props => {
23178
- import.meta.css = [css$T, "@jsenv/navi/src/control/input/button_ui.jsx"];
23255
+ import.meta.css = [css$U, "@jsenv/navi/src/control/input/button_ui.jsx"];
23179
23256
  const {
23180
23257
  ref,
23181
23258
  // href/link
@@ -25303,7 +25380,7 @@ installImportMetaCssBuild(import.meta);/**
25303
25380
  * reaches the real container.
25304
25381
  */
25305
25382
  let openLocalDialogCount = 0;
25306
- const css$S = /* css */`
25383
+ const css$T = /* css */`
25307
25384
  @layer navi {
25308
25385
  .navi_dialog {
25309
25386
  /* Min gap between the dialog and the edges of its container. Written
@@ -25713,7 +25790,7 @@ const css$S = /* css */`
25713
25790
  * @param {import("ignore:preact").ComponentChildren} props.children
25714
25791
  */
25715
25792
  const Dialog = props => {
25716
- import.meta.css = [css$S, "@jsenv/navi/src/layout/dialog.jsx"];
25793
+ import.meta.css = [css$T, "@jsenv/navi/src/layout/dialog.jsx"];
25717
25794
  if (props.openController) {
25718
25795
  return jsx(ControlledDialog, {
25719
25796
  ...props
@@ -26531,7 +26608,7 @@ installImportMetaCssBuild(import.meta);/**
26531
26608
  * and applied.
26532
26609
  */
26533
26610
  let openLocalPopoverCount = 0;
26534
- const css$R = /* css */`
26611
+ const css$S = /* css */`
26535
26612
  @layer navi {
26536
26613
  .navi_popover {
26537
26614
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -26901,7 +26978,7 @@ const css$R = /* css */`
26901
26978
  * @param {import("ignore:preact").ComponentChildren} props.children
26902
26979
  */
26903
26980
  const Popover = props => {
26904
- import.meta.css = [css$R, "@jsenv/navi/src/layout/popover.jsx"];
26981
+ import.meta.css = [css$S, "@jsenv/navi/src/layout/popover.jsx"];
26905
26982
  if (props.openController) {
26906
26983
  return jsx(ControlledPopover, {
26907
26984
  ...props
@@ -27865,7 +27942,7 @@ installImportMetaCssBuild(import.meta);/**
27865
27942
  * event, and a caller replacing the body entirely then has one protocol to
27866
27943
  * follow — `--navi-confirm` for yes, anything that closes for no.
27867
27944
  */
27868
- const css$Q = /* css */`
27945
+ const css$R = /* css */`
27869
27946
  /* The width lives on the body rather than on the popup, so that custom
27870
27947
  content (which replaces this body entirely) sizes itself instead of
27871
27948
  inheriting a ceiling meant for a sentence-long question. */
@@ -28002,7 +28079,7 @@ const ConfirmPopup = ({
28002
28079
  onAnswer,
28003
28080
  onClosed
28004
28081
  }) => {
28005
- import.meta.css = [css$Q, "@jsenv/navi/src/action/confirm_popup.jsx"];
28082
+ import.meta.css = [css$R, "@jsenv/navi/src/action/confirm_popup.jsx"];
28006
28083
  const {
28007
28084
  mode,
28008
28085
  confirmLabel,
@@ -28086,7 +28163,7 @@ const defaultBody = (message, {
28086
28163
  });
28087
28164
  };
28088
28165
 
28089
- installImportMetaCssBuild(import.meta);const css$P = /* css */`
28166
+ installImportMetaCssBuild(import.meta);const css$Q = /* css */`
28090
28167
  .action_error {
28091
28168
  margin-top: 0;
28092
28169
  margin-bottom: 20px;
@@ -28111,7 +28188,7 @@ const ActionRenderer = ({
28111
28188
  children,
28112
28189
  disabled
28113
28190
  }) => {
28114
- import.meta.css = [css$P, "@jsenv/navi/src/action/action_renderer.jsx"];
28191
+ import.meta.css = [css$Q, "@jsenv/navi/src/action/action_renderer.jsx"];
28115
28192
  if (action === undefined) {
28116
28193
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
28117
28194
  }
@@ -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
  }
@@ -35142,7 +35234,7 @@ const PhoneSvg = () => {
35142
35234
  };
35143
35235
 
35144
35236
  installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
35145
- const css$O = /* css */`
35237
+ const css$P = /* css */`
35146
35238
  .navi_text_anchor {
35147
35239
  vertical-align: baseline;
35148
35240
  user-select: none;
@@ -35177,7 +35269,7 @@ const TextAnchor = ({
35177
35269
  textSize,
35178
35270
  lineLayout
35179
35271
  }) => {
35180
- import.meta.css = [css$O, "@jsenv/navi/src/text/text_anchor.jsx"];
35272
+ import.meta.css = [css$P, "@jsenv/navi/src/text/text_anchor.jsx"];
35181
35273
  const anchorRef = useRef();
35182
35274
 
35183
35275
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
@@ -35292,7 +35384,7 @@ const computeTopOffset = ({
35292
35384
  };
35293
35385
  const charTopCanvas = document.createElement("canvas");
35294
35386
 
35295
- installImportMetaCssBuild(import.meta);const css$N = /* css */`
35387
+ installImportMetaCssBuild(import.meta);const css$O = /* css */`
35296
35388
  @layer navi {
35297
35389
  /* Ensure data attributes from box.jsx can win to update display */
35298
35390
  .navi_icon {
@@ -35450,7 +35542,7 @@ const Icon = ({
35450
35542
  fillLine,
35451
35543
  ...props
35452
35544
  }) => {
35453
- import.meta.css = [css$N, "@jsenv/navi/src/text/icon.jsx"];
35545
+ import.meta.css = [css$O, "@jsenv/navi/src/text/icon.jsx"];
35454
35546
  const innerChildren = href ? jsx("svg", {
35455
35547
  width: "100%",
35456
35548
  height: "100%",
@@ -35603,7 +35695,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
35603
35695
  });
35604
35696
  };
35605
35697
 
35606
- installImportMetaCssBuild(import.meta);const css$M = /* css */`
35698
+ installImportMetaCssBuild(import.meta);const css$N = /* css */`
35607
35699
  @layer navi {
35608
35700
  .navi_link {
35609
35701
  --link-border-radius: unset;
@@ -36042,7 +36134,7 @@ Object.assign(PSEUDO_CLASSES, {
36042
36134
  * @param {boolean} [props.readOnly]
36043
36135
  */
36044
36136
  const Link = props => {
36045
- import.meta.css = [css$M, "@jsenv/navi/src/nav/link/link.jsx"];
36137
+ import.meta.css = [css$N, "@jsenv/navi/src/nav/link/link.jsx"];
36046
36138
  if (props.route) {
36047
36139
  return jsx(LinkWithRoute, {
36048
36140
  ...props
@@ -36277,7 +36369,7 @@ installImportMetaCssBuild(import.meta);/**
36277
36369
  * TabList component with support for horizontal and vertical layouts
36278
36370
  * https://dribbble.com/search/tabs
36279
36371
  */
36280
- const css$L = /* css */`
36372
+ const css$M = /* css */`
36281
36373
  @layer navi {
36282
36374
  .navi_nav {
36283
36375
  --nav-border: none;
@@ -36452,7 +36544,7 @@ const Nav = ({
36452
36544
  // "before" or "after": which side the panel sits on, turning the nav into folder tabs
36453
36545
  ...props
36454
36546
  }) => {
36455
- import.meta.css = [css$L, "@jsenv/navi/src/nav/link/nav.jsx"];
36547
+ import.meta.css = [css$M, "@jsenv/navi/src/nav/link/nav.jsx"];
36456
36548
  children = toChildArray(children);
36457
36549
  return jsx(Box, {
36458
36550
  as: "nav",
@@ -36844,7 +36936,7 @@ installImportMetaCssBuild(import.meta);/**
36844
36936
  * Border width participates in layout (it is added to the tab and page
36845
36937
  * padding): a thick border grows the binder rather than eating into the text.
36846
36938
  */
36847
- const css$K = /* css */`
36939
+ const css$L = /* css */`
36848
36940
  @layer navi {
36849
36941
  .navi_binder {
36850
36942
  --binder-border-width: var(--navi-control-border-width);
@@ -37157,7 +37249,7 @@ const Binder = ({
37157
37249
  pagePadding,
37158
37250
  ...props
37159
37251
  }) => {
37160
- import.meta.css = [css$K, "@jsenv/navi/src/nav/binder/binder.jsx"];
37252
+ import.meta.css = [css$L, "@jsenv/navi/src/nav/binder/binder.jsx"];
37161
37253
  const items = toChildArray(children).map((child, index) => {
37162
37254
  const {
37163
37255
  value: itemValue,
@@ -37579,7 +37671,7 @@ installImportMetaCssBuild(import.meta);/**
37579
37671
  * into the size; a box-shadow draws the identical line and stays out of
37580
37672
  * layout.
37581
37673
  */
37582
- const css$J = /* css */`
37674
+ const css$K = /* css */`
37583
37675
  @layer navi {
37584
37676
  :root {
37585
37677
  --navi-fixed-bar-width: 56px;
@@ -37717,7 +37809,7 @@ const FixedBar = ({
37717
37809
  border = true,
37718
37810
  ...props
37719
37811
  }) => {
37720
- import.meta.css = [css$J, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
37812
+ import.meta.css = [css$K, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
37721
37813
  const defaultRef = useRef();
37722
37814
  props.ref = props.ref || defaultRef;
37723
37815
  // Whichever of width/height crosses the edge the bar sits on is what the
@@ -37754,6 +37846,178 @@ const FixedBar = ({
37754
37846
  });
37755
37847
  };
37756
37848
 
37849
+ /**
37850
+ * Finds the element that is too wide for the page and says which one it is.
37851
+ *
37852
+ * A page that overflows horizontally is a bug wherever it happens, and on
37853
+ * Chrome Android it is a catastrophic one: the layout viewport inflates to the
37854
+ * content and `position: fixed` centering goes with it (see
37855
+ * docs/MOBILE_LAYOUT_PITFALLS.md). The remedy there is a wrapper in
37856
+ * `overflow-x: clip`, and it works — at the price of making the cause
37857
+ * invisible: nothing sticks out anymore, so nothing says a fixed width, a
37858
+ * `min-width` or an unbreakable string is still oversized. This puts the
37859
+ * signal back, without giving up the net.
37860
+ *
37861
+ * What counts as "too wide" is measured against the box that clips, not
37862
+ * against the document: with `clip` there is no scrollable overflow to read
37863
+ * (`scrollWidth` reports none), so the rectangles of the descendants are what
37864
+ * tells.
37865
+ *
37866
+ * Two things are deliberately not reported, because they cannot reach the
37867
+ * document:
37868
+ * - anything inside a box that clips or scrolls on its own — a wide table in
37869
+ * its `overflow-x: auto` container is doing exactly what it should;
37870
+ * - anything out of flow in the viewport's own coordinates (`position: fixed`,
37871
+ * the top layer), which contributes nothing to the document's overflow.
37872
+ */
37873
+
37874
+
37875
+ // Subpixel layout rounds rectangles up on boxes that fit exactly.
37876
+ const OVERFLOW_TOLERANCE = 1;
37877
+
37878
+ const css$J = /* css */ `
37879
+ [data-navi-overflow-x] {
37880
+ outline: 2px dashed #e74c3c;
37881
+ outline-offset: -2px;
37882
+ }
37883
+ `;
37884
+
37885
+ /**
37886
+ * @param {object} [options]
37887
+ * @param {Element} [options.root=document.body] The box the content must fit
37888
+ * in — the wrapper carrying `overflow-x: clip`, when there is one.
37889
+ * @param {boolean} [options.highlight=true] Outline the culprits on screen.
37890
+ * @param {(overflows: Array<{element: Element, overflow: number, side: "left"|"right"}>) => void} [options.onDetect]
37891
+ * Replaces the default console warning.
37892
+ * @returns {() => void} Stops watching.
37893
+ */
37894
+ const detectHorizontalOverflow = ({
37895
+ root = document.body,
37896
+ highlight = true,
37897
+ onDetect = warnOverflows,
37898
+ } = {}) => {
37899
+ let styleEl = null;
37900
+ if (highlight) {
37901
+ styleEl = document.createElement("style");
37902
+ styleEl.textContent = css$J;
37903
+ document.head.appendChild(styleEl);
37904
+ }
37905
+
37906
+ let highlightedSet = new Set();
37907
+ const detect = () => {
37908
+ const overflows = findOverflows(root);
37909
+ if (highlight) {
37910
+ const nextHighlightedSet = new Set();
37911
+ for (const { element } of overflows) {
37912
+ element.setAttribute("data-navi-overflow-x", "");
37913
+ nextHighlightedSet.add(element);
37914
+ }
37915
+ for (const element of highlightedSet) {
37916
+ if (!nextHighlightedSet.has(element)) {
37917
+ element.removeAttribute("data-navi-overflow-x");
37918
+ }
37919
+ }
37920
+ highlightedSet = nextHighlightedSet;
37921
+ }
37922
+ if (overflows.length) {
37923
+ onDetect(overflows);
37924
+ }
37925
+ };
37926
+
37927
+ // Measuring inside a resize callback is what makes the loop; wait for the
37928
+ // frame that resize produced.
37929
+ let frame = null;
37930
+ const requestDetect = () => {
37931
+ if (frame !== null) {
37932
+ return;
37933
+ }
37934
+ frame = requestAnimationFrame(() => {
37935
+ frame = null;
37936
+ detect();
37937
+ });
37938
+ };
37939
+
37940
+ // Resize catches the window narrowing and the content growing (the root gets
37941
+ // taller); mutations catch a wide element arriving without changing the
37942
+ // root's size.
37943
+ const resizeObserver = new ResizeObserver(requestDetect);
37944
+ resizeObserver.observe(root);
37945
+ const mutationObserver = new MutationObserver(requestDetect);
37946
+ mutationObserver.observe(root, { subtree: true, childList: true });
37947
+ requestDetect();
37948
+
37949
+ return () => {
37950
+ resizeObserver.disconnect();
37951
+ mutationObserver.disconnect();
37952
+ if (frame !== null) {
37953
+ cancelAnimationFrame(frame);
37954
+ frame = null;
37955
+ }
37956
+ for (const element of highlightedSet) {
37957
+ element.removeAttribute("data-navi-overflow-x");
37958
+ }
37959
+ highlightedSet.clear();
37960
+ if (styleEl) {
37961
+ styleEl.remove();
37962
+ }
37963
+ };
37964
+ };
37965
+
37966
+ const warnOverflows = (overflows) => {
37967
+ for (const { element, overflow, side } of overflows) {
37968
+ console.warn(
37969
+ `${getElementSignature(element)} overflows the page by ${Math.round(
37970
+ overflow,
37971
+ )}px on the ${side}. Look for a width in px, a min-width, or an unbreakable string; if it is meant to be wider than the screen, give it its own "overflow-x: auto".`,
37972
+ element,
37973
+ );
37974
+ }
37975
+ };
37976
+
37977
+ const findOverflows = (root) => {
37978
+ const rootRect = root.getBoundingClientRect();
37979
+ const rootStyle = getComputedStyle(root);
37980
+ // Only the end side is watched: what sticks out there is what the document
37981
+ // grows to hold, and what inflates the layout viewport. Past the start edge
37982
+ // the content is simply unreachable, and that is where the offscreen
37983
+ // patterns (a label parked at `left: -9999px`) live.
37984
+ const side = rootStyle.direction === "rtl" ? "left" : "right";
37985
+ const overflows = [];
37986
+ const collect = (parentEl) => {
37987
+ for (const el of parentEl.children) {
37988
+ const style = getComputedStyle(el);
37989
+ if (style.display === "none" || style.position === "fixed") {
37990
+ continue;
37991
+ }
37992
+ if (el.hasAttribute("popover") || el.tagName === "DIALOG") {
37993
+ continue;
37994
+ }
37995
+ const rect = el.getBoundingClientRect();
37996
+ if (rect.width === 0 && rect.height === 0) {
37997
+ continue;
37998
+ }
37999
+ const overflow =
38000
+ side === "right"
38001
+ ? rect.right - rootRect.right
38002
+ : rootRect.left - rect.left;
38003
+ if (overflow > OVERFLOW_TOLERANCE) {
38004
+ // The outermost box that sticks out is the one to fix; its children
38005
+ // stick out because it does.
38006
+ overflows.push({ element: el, overflow, side });
38007
+ continue;
38008
+ }
38009
+ // A box holding its own horizontal overflow cannot leak into the page,
38010
+ // whatever it holds — a wide table in its own scroll box is right.
38011
+ if (style.overflowX !== "visible") {
38012
+ continue;
38013
+ }
38014
+ collect(el);
38015
+ }
38016
+ };
38017
+ collect(root);
38018
+ return overflows;
38019
+ };
38020
+
37757
38021
  const useFocusGroup = (
37758
38022
  elementRef,
37759
38023
  {
@@ -46956,7 +47220,7 @@ const css$u = /* css */`
46956
47220
  around it does. Its own scroll box must then be transparent to
46957
47221
  layout — otherwise it would cap the list at a height of its own and
46958
47222
  start a second, nested scroll inside the page's. */
46959
- &[data-scroller="parent"] {
47223
+ &[data-scroller] {
46960
47224
  max-height: none;
46961
47225
  overflow: visible;
46962
47226
 
@@ -46966,6 +47230,14 @@ const css$u = /* css */`
46966
47230
  }
46967
47231
  }
46968
47232
 
47233
+ /* Scrolling with the page means sticking to the viewport, and a FixedBar
47234
+ is in front of that viewport: without the offset a sticky label lands
47235
+ behind the bar. The bar publishes the room it takes (see
47236
+ fixed_bar_space.js) and it is 0px when there is no bar. */
47237
+ &[data-scroller="document"] {
47238
+ --x-list-group-label-top: var(--navi-fixed-bar-space-top, 0px);
47239
+ }
47240
+
46969
47241
  &[data-expand-x] {
46970
47242
  width: 100%;
46971
47243
  }
@@ -47324,7 +47596,7 @@ const css$u = /* css */`
47324
47596
 
47325
47597
  .navi_list_item_group_label {
47326
47598
  position: sticky;
47327
- top: 0;
47599
+ top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
47328
47600
  z-index: 1;
47329
47601
  display: block;
47330
47602
  background-color: var(--list-group-label-background-color);
@@ -47567,7 +47839,7 @@ const ListUI = props => {
47567
47839
  baseClassName: "navi_list_container",
47568
47840
  popover: popover,
47569
47841
  "data-horizontal": horizontal ? "" : undefined,
47570
- "data-scroller": scroller === "self" ? undefined : "parent",
47842
+ "data-scroller": getScrollerAttribute(scroller),
47571
47843
  "data-expand-x": expandX || expand ? "" : undefined,
47572
47844
  "data-expand-y": expandY || expand ? "" : undefined,
47573
47845
  expandX: expandX,
@@ -47826,6 +48098,7 @@ const useListScrollSync = ({
47826
48098
  setScrollerElResolved(current => current === scrollerElNow ? current : scrollerElNow);
47827
48099
  };
47828
48100
  useLayoutEffect(resolveScroller);
48101
+ useStickyScrollportWarning();
47829
48102
 
47830
48103
  // The row the scroll holds onto across a change of geometry, and where it
47831
48104
  // sat when that change was decided. Captured at the two moments the list
@@ -48603,6 +48876,28 @@ const getScrollerViewportRect = scrollerEl => {
48603
48876
  }
48604
48877
  return scrollerEl.getBoundingClientRect();
48605
48878
  };
48879
+ const useStickyScrollportWarning = (ref, scroller) => {
48880
+ useRef(false);
48881
+ useLayoutEffect(() => {
48882
+ {
48883
+ return;
48884
+ }
48885
+ });
48886
+ };
48887
+
48888
+ // The CSS needs to tell "the page scrolls me" from "some box around me
48889
+ // scrolls me": only the first one sticks to the viewport, where the fixed bars
48890
+ // are.
48891
+ const getScrollerAttribute = scroller => {
48892
+ if (scroller === "self") {
48893
+ return undefined;
48894
+ }
48895
+ if (scroller === "document") {
48896
+ return "document";
48897
+ }
48898
+ return "parent";
48899
+ };
48900
+
48606
48901
  // scroller="parent": the list virtualizes against the scroll box it lives in
48607
48902
  // instead of one of its own. Which box that is can only be measured, and a
48608
48903
  // measurement holds for the geometry it was taken on — see resolveScroller in
@@ -52047,6 +52342,10 @@ const css$p = /* css */`
52047
52342
  keyboard here (see navi-focus-delegate below). */
52048
52343
  outline-color: var(--navi-focus-outline-color);
52049
52344
  outline-offset: 0px;
52345
+ /* No grey flash under a finger: what a press does is said by the chevron's
52346
+ own background, and the browser's rectangle is drawn square over corners
52347
+ that are round. Inherited, so the three pieces inside get it too. */
52348
+ -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
52050
52349
  }
52051
52350
  /* The middle holds the keyboard, and this box wears its ring: whatever is in
52052
52351
  there fills it, so a ring of its own would be drawn a pixel inside this
@@ -52173,7 +52472,12 @@ const css$p = /* css */`
52173
52472
  border-radius: 0;
52174
52473
  cursor: pointer;
52175
52474
  }
52176
- .navi_picker_spin > .navi_picker_spin_way_out:hover {
52475
+ /* Said as data-hover rather than :hover: a touch browser synthesizes the
52476
+ enter and never the leave, so a CSS :hover would stay grey under the last
52477
+ chevron pressed until something else was touched. What tracks it (see
52478
+ pseudo_styles.js) knows there is no hover on such a device and simply does
52479
+ not set the attribute. */
52480
+ .navi_picker_spin > .navi_picker_spin_way_out[data-hover] {
52177
52481
  background: color-mix(in srgb, currentColor 8%, transparent);
52178
52482
  }
52179
52483
  /* Nothing that way: still there, still pressable — pressing it is how one
@@ -52199,22 +52503,28 @@ const css$p = /* css */`
52199
52503
  of a rounded spin is rounded there too, and nowhere else — the two corners
52200
52504
  it does not own stay at the 0 above. Said with inherit rather than clipped
52201
52505
  away with overflow, which would cut the focus ring of the very button it
52202
- rounds. */
52506
+ rounds.
52507
+ Which chevron is which is asked of the chevron itself (data-way-out) rather
52508
+ than of its place among its siblings: the loading outline is a <span> too
52509
+ and it is written first, so :first-of-type named IT and the chevron at the
52510
+ start went unrounded. */
52203
52511
  .navi_picker_spin:not([data-vertical])
52204
- > .navi_picker_spin_way_out:first-of-type {
52512
+ > .navi_picker_spin_way_out[data-way-out="start"] {
52205
52513
  border-start-start-radius: inherit;
52206
52514
  border-end-start-radius: inherit;
52207
52515
  }
52208
52516
  .navi_picker_spin:not([data-vertical])
52209
- > .navi_picker_spin_way_out:last-of-type {
52517
+ > .navi_picker_spin_way_out[data-way-out="end"] {
52210
52518
  border-start-end-radius: inherit;
52211
52519
  border-end-end-radius: inherit;
52212
52520
  }
52213
- .navi_picker_spin[data-vertical] > .navi_picker_spin_way_out:first-of-type {
52521
+ .navi_picker_spin[data-vertical]
52522
+ > .navi_picker_spin_way_out[data-way-out="start"] {
52214
52523
  border-start-start-radius: inherit;
52215
52524
  border-start-end-radius: inherit;
52216
52525
  }
52217
- .navi_picker_spin[data-vertical] > .navi_picker_spin_way_out:last-of-type {
52526
+ .navi_picker_spin[data-vertical]
52527
+ > .navi_picker_spin_way_out[data-way-out="end"] {
52218
52528
  border-end-end-radius: inherit;
52219
52529
  border-end-start-radius: inherit;
52220
52530
  }
@@ -52455,6 +52765,7 @@ const Spin = ({
52455
52765
  const wayOut = atStart => {
52456
52766
  const isNext = atStart ? startIsNext : !startIsNext;
52457
52767
  return jsx(WayOut, {
52768
+ atStart: atStart,
52458
52769
  unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
52459
52770
  label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
52460
52771
  onPress: e => {
@@ -52632,6 +52943,7 @@ const Spin = ({
52632
52943
  // the container's own tabIndex, or the field's), where the arrows already mean
52633
52944
  // this.
52634
52945
  const WayOut = ({
52946
+ atStart,
52635
52947
  commandFor,
52636
52948
  unavailableMessage,
52637
52949
  label,
@@ -52640,6 +52952,17 @@ const WayOut = ({
52640
52952
  }) => jsx(Box, {
52641
52953
  as: "span",
52642
52954
  baseClassName: "navi_picker_spin_way_out"
52955
+ // Which end of the box it sits in, said by the chevron rather than read
52956
+ // from its place among its siblings: that is what the corners it is
52957
+ // rounded by are keyed on (see the CSS above).
52958
+ ,
52959
+
52960
+ "data-way-out": atStart ? "start" : "end"
52961
+ // Tracked rather than left to CSS :hover, which stays on after a tap on a
52962
+ // touch device (see the CSS above).
52963
+ ,
52964
+
52965
+ pseudoClasses: WAY_OUT_PSEUDO_CLASSES
52643
52966
  // Announced as a button because that is what it is to whoever cannot see
52644
52967
  // the chevron — and marked unavailable rather than removed when there is
52645
52968
  // nothing that way, so it keeps its place.
@@ -52691,6 +53014,7 @@ const WayOut = ({
52691
53014
  })
52692
53015
  });
52693
53016
  const PICKER_SPIN_PSEUDO_CLASSES = [":hover", ":focus-visible"];
53017
+ const WAY_OUT_PSEUDO_CLASSES = [":hover"];
52694
53018
 
52695
53019
  // A padding written on this box would sit between its border and the chevrons,
52696
53020
  // which are meant to reach the corners they are rounded by — so each padding
@@ -62662,5 +62986,5 @@ const UserSvg = () => jsx("svg", {
62662
62986
  })
62663
62987
  });
62664
62988
 
62665
- 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 };
62989
+ 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, detectHorizontalOverflow, 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 };
62666
62990
  //# sourceMappingURL=jsenv_navi.js.map