@jsenv/navi 0.29.435 → 0.29.437

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.
@@ -7,11 +7,11 @@ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchIntern
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
- import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
10
+ import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useReducer, useEffect, useCallback, useId } from "preact/hooks";
11
11
  import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
14
- import { CHAR_CLASS_PRESETS, createValidity, TYPE_RULE, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, parseDuration, durationToISOString } from "@jsenv/validity";
14
+ import { CHAR_CLASS_PRESETS, createValidity, durationContainsNaN, compareTwoDurations, TYPE_RULE, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, parseDuration, durationToISOString } from "@jsenv/validity";
15
15
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
16
16
  import { Suspense, createPortal, forwardRef } from "preact/compat";
17
17
 
@@ -944,8 +944,10 @@ naviI18n.addAll({
944
944
  },
945
945
  });
946
946
 
947
- // Spin messages — the ends of what one steps through, said without naming
948
- // what it is made of: the same words fit days, months, pages or sizes.
947
+ // Spin messages — the ends of what one steps through when the control holding
948
+ // it has no sentence of its own for a bound (a day or a number says its
949
+ // constraint.min/max one, see picker_spin.jsx): said without naming what it is
950
+ // made of, so they fit pages, sizes or any list of items.
949
951
  naviI18n.addAll({
950
952
  "spin.previous": {
951
953
  en: "Previous",
@@ -3546,8 +3548,11 @@ const actionWeakMap = new WeakMap();
3546
3548
  * than a first load (data_states.md). Called at the start of every run whose
3547
3549
  * value is undefined; the answer replaces it as usual, and `undefined` means
3548
3550
  * "nothing known". A resource `GET` uses it to draw the row its store holds.
3549
- * An action declaring it is asked for it by a binding moving to new params,
3550
- * rather than handed the previous params' answer (see inheritData).
3551
+ * @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
3552
+ * to new params starts the new instance without the previous params' answer
3553
+ * (see createActionProxyFromSignal). For an answer that would say something
3554
+ * false about the new params (a `GET` drawing another row), not for one that
3555
+ * stands in while the next one comes (a list of search results).
3551
3556
  * @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
3552
3557
  * answer stays good once it has landed. Without it an answer lives exactly as
3553
3558
  * long as something references it: the screen that asked goes away, and
@@ -3590,6 +3595,7 @@ const createAction = (callback, rootOptions = {}) => {
3590
3595
  resultToValue,
3591
3596
  valueToData,
3592
3597
  provisionalValue,
3598
+ inheritData = true,
3593
3599
  dataDefault,
3594
3600
  data = dataDefault,
3595
3601
 
@@ -4253,6 +4259,7 @@ const createAction = (callback, rootOptions = {}) => {
4253
4259
  const privateProperties = {
4254
4260
  valueInitial,
4255
4261
  provisionalValue,
4262
+ inheritData,
4256
4263
 
4257
4264
  performRun,
4258
4265
  performReset,
@@ -4285,7 +4292,7 @@ const createAction = (callback, rootOptions = {}) => {
4285
4292
  * a list of filters changes, providing real-time results without user interaction.
4286
4293
  * @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
4287
4294
  * By default (true), the proxy carries over the previous target's value and error into the new action —
4288
- * unless the action declares a `provisionalValue`, which then says what the new one holds.
4295
+ * unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
4289
4296
  * This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
4290
4297
  * new action completes, not when it starts loading. Code that needs to distinguish loading state can still
4291
4298
  * check `action.runningState`, while code that just reads `action.data` always sees the most recent
@@ -4332,12 +4339,12 @@ const createActionProxyFromSignal = (
4332
4339
  let currentActionPrivateProperties = getActionPrivateProperties(action);
4333
4340
  let actionTargetPreviousWeakRef = null;
4334
4341
 
4335
- // An action saying for itself what it holds for params it has not answered
4336
- // (provisionalValue: a resource GET draws the row its store holds) is not
4337
- // handed the previous params' answer — an answer about another row. What it
4338
- // does not know stays unknown: a skeleton, never a different row.
4342
+ // An action whose answer for other params would be false for these (a
4343
+ // resource GET: another row) says so itself, and is not handed it. What it
4344
+ // does not know stays unknown: a skeleton, or its provisionalValue, never a
4345
+ // different row.
4339
4346
  const inheritsAnswer =
4340
- inheritData && !getActionPrivateProperties(action).provisionalValue;
4347
+ inheritData && getActionPrivateProperties(action).inheritData;
4341
4348
  const createTarget = (params) => {
4342
4349
  if (inheritsAnswer) {
4343
4350
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
@@ -10240,6 +10247,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
10240
10247
 
10241
10248
  let setupRoutesCalled = false;
10242
10249
  let activeRouteSet = null;
10250
+ // True once the routes have been matched against an address. Before that, no
10251
+ // route matching says nothing about the page: nothing has been asked yet.
10252
+ const routedSignal = signal(false);
10243
10253
 
10244
10254
  const routePreloadMap = new WeakMap();
10245
10255
  const registerRoutePreload = (route, preload) => {
@@ -10664,6 +10674,7 @@ This prevents cross-test pollution and ensures clean state.`,
10664
10674
  }
10665
10675
  // Reset flag after URL -> Signal synchronization is complete
10666
10676
  isUpdatingRoutesFromUrl = false;
10677
+ routedSignal.value = true;
10667
10678
  Object.assign(returnValue, { matchingRouteSet });
10668
10679
  for (const [route, repairedParams] of repairedParamsByRoute) {
10669
10680
  route.replaceParams(repairedParams, {
@@ -10713,6 +10724,7 @@ This prevents cross-test pollution and ensures clean state.`,
10713
10724
  droppingRouteSet = null;
10714
10725
  setupRoutesCalled = false;
10715
10726
  activeRouteSet = null;
10727
+ routedSignal.value = false;
10716
10728
  };
10717
10729
  return {
10718
10730
  updateRoutes,
@@ -10779,6 +10791,42 @@ const urlWithoutSearchAndHash = (url) => {
10779
10791
  return `${urlObject.origin}${urlObject.pathname}`;
10780
10792
  };
10781
10793
 
10794
+ /**
10795
+ * What a component can learn about the page it is rendered in, from the route
10796
+ * container rendering that page (see useContentKeepingLeavingPages in
10797
+ * route.jsx). `null` outside of any route container.
10798
+ *
10799
+ * - `isShown()` — whether the container would still show the page. False as
10800
+ * soon as the address leads elsewhere: the page is about to be taken down, or
10801
+ * hidden until the route transition leaving it is over, and nothing it reads
10802
+ * is worth a render any more. False for good once it is kept while leaving: a
10803
+ * return to its route mounts the page anew. Asked outside any render — by a
10804
+ * subscription deciding whether to re-render — and it reads the routes
10805
+ * untracked, so asking subscribes nobody to them.
10806
+ * - `leavingSignal` — true once the page is hidden and kept while its movement
10807
+ * plays: still mounted, so what it fills outside its own nodes (a slot in a
10808
+ * bar) steps out rather than showing with the page arriving.
10809
+ */
10810
+ const RoutePageContext = createContext(null);
10811
+
10812
+ /**
10813
+ * A container has put its page on screen — or as much of it as it can.
10814
+ *
10815
+ * A route matching is a signal changing, and the page it selects reaches the
10816
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
10817
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
10818
+ * as it has been decided (a travel about to have its picture taken by the
10819
+ * browser, see route_travel.jsx) waits for this instead of counting.
10820
+ *
10821
+ * A page waiting on data is announced too, by the boundary showing its loading
10822
+ * state (see Loading in use_async_data.jsx): what the container could put on
10823
+ * screen is what the browser is about to take a picture of, and a page that
10824
+ * cannot render yet would otherwise be waited on until the transition dies of
10825
+ * it. It lives in a module of its own for that: the async layer says it as much
10826
+ * as the router does, and neither can import the other.
10827
+ */
10828
+ const [publishRouteRender, observeRouteRender] = createPubSub();
10829
+
10782
10830
  const documentStateSignal = signal(null);
10783
10831
  const useDocumentState = () => {
10784
10832
  return documentStateSignal.value;
@@ -11038,24 +11086,6 @@ const readTopInWindow = element => {
11038
11086
  return element.getBoundingClientRect().top;
11039
11087
  };
11040
11088
 
11041
- /**
11042
- * A container has put its page on screen — or as much of it as it can.
11043
- *
11044
- * A route matching is a signal changing, and the page it selects reaches the
11045
- * DOM only once Preact has rendered — an unknown number of passes later, in an
11046
- * unknown number of microtasks. Anyone who needs the page as it IS rather than
11047
- * as it has been decided (a travel about to have its picture taken by the
11048
- * browser, see route_travel.jsx) waits for this instead of counting.
11049
- *
11050
- * A page waiting on data is announced too, by the boundary showing its loading
11051
- * state (see Loading in use_async_data.jsx): what the container could put on
11052
- * screen is what the browser is about to take a picture of, and a page that
11053
- * cannot render yet would otherwise be waited on until the transition dies of
11054
- * it. It lives in a module of its own for that: the async layer says it as much
11055
- * as the router does, and neither can import the other.
11056
- */
11057
- const [publishRouteRender, observeRouteRender] = createPubSub();
11058
-
11059
11089
  /**
11060
11090
  * Where a page was left, given back when one comes back to it.
11061
11091
  *
@@ -12450,6 +12480,7 @@ browserIntegration.handleActionTask;
12450
12480
 
12451
12481
  const NO_OP = () => {};
12452
12482
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
12483
+ const countRenders$1 = (count) => count + 1;
12453
12484
  // What the computed below answers for a key the document state does not hold:
12454
12485
  // key presence is the flag, and the value under a present key may be anything,
12455
12486
  // undefined included.
@@ -12478,7 +12509,36 @@ const useNavStateBasic = (
12478
12509
  [id],
12479
12510
  );
12480
12511
  const keyValue = keyValueComputed.value;
12481
- const keyInState = keyValue !== KEY_NOT_IN_STATE;
12512
+ // A page the address has left keeps reading the entry it was shown on until
12513
+ // it is taken down (kept hidden while a route transition plays): the new
12514
+ // entry would close what was open there — a Picker as a cancel — where the
12515
+ // same page taken down at once is only unmounted. Its render still happens:
12516
+ // the state is written before the routes say the page is left.
12517
+ const routePage = useContext(RoutePageContext);
12518
+ const keyValueReadRef = useRef(keyValue);
12519
+ const keyValueWithheld =
12520
+ keyValue !== keyValueReadRef.current &&
12521
+ routePage !== null &&
12522
+ !routePage.isShown();
12523
+ if (!keyValueWithheld) {
12524
+ keyValueReadRef.current = keyValue;
12525
+ }
12526
+ const keyValueRead = keyValueReadRef.current;
12527
+ // Owed rather than dropped, as in useAsyncData: the address can come back
12528
+ // before the container renders (a redirect straight back), and the page then
12529
+ // stays on screen.
12530
+ const [, rerender] = useReducer(countRenders$1, 0);
12531
+ useEffect(() => {
12532
+ if (!keyValueWithheld) {
12533
+ return undefined;
12534
+ }
12535
+ return observeRouteRender(() => {
12536
+ if (routePage.isShown()) {
12537
+ rerender();
12538
+ }
12539
+ });
12540
+ }, [keyValueWithheld]);
12541
+ const keyInState = keyValueRead !== KEY_NOT_IN_STATE;
12482
12542
  const onLeaveRef = useRef(onLeave);
12483
12543
  onLeaveRef.current = onLeave;
12484
12544
  const prevKeyInStateRef = useRef(keyInState);
@@ -12508,7 +12568,7 @@ const useNavStateBasic = (
12508
12568
  effectiveType = "replace";
12509
12569
  }
12510
12570
 
12511
- const currentValue = keyInState ? keyValue : defaultValue;
12571
+ const currentValue = keyInState ? keyValueRead : defaultValue;
12512
12572
 
12513
12573
  if (debug) {
12514
12574
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -12576,6 +12636,8 @@ const useNavStateBasic = (
12576
12636
  /**
12577
12637
  * Stores a named value in the browser's document state and returns it reactively.
12578
12638
  * The component re-renders whenever the value changes (navigation, back/forward button).
12639
+ * In a page the address has left — kept hidden while a route transition plays —
12640
+ * the value stays the one of the entry that page was shown on.
12579
12641
  *
12580
12642
  * @param {string} id
12581
12643
  * Unique key used to store the value in document state. Must be stable across renders.
@@ -14981,6 +15043,13 @@ const createOpenController = (
14981
15043
  change();
14982
15044
  };
14983
15045
  const runChange = (change, { opened, event }) => {
15046
+ // What the controller answers about itself is the decision, written before
15047
+ // the change runs, whatever runs it. Whoever asks meanwhile reads it: the
15048
+ // render an opening forces (onOpen, mountContent) reads the open state
15049
+ // the caller holds, and an onOpen writing the popup's own `signal` has
15050
+ // useOpenPropsEffectOnOpenController asking for this very opening — a
15051
+ // second one, nested in the first, if `opened` still said closed.
15052
+ controller.opened = opened;
14984
15053
  const applyChange = () => {
14985
15054
  // Recorded with the change itself: what the DOM shows is what a render
14986
15055
  // landing between the ask and the picture must draw (see `openedInDom`).
@@ -14996,12 +15065,8 @@ const createOpenController = (
14996
15065
  applyChange();
14997
15066
  return;
14998
15067
  }
14999
- // What the controller answers about itself does not wait for the picture:
15000
- // whoever just asked reads `opened` on the spot (see
15001
- // useOpenPropsEffectOnOpenController, which writes it back into the
15002
- // caller's own signal), and a request arriving before the change lands
15003
- // runs it first rather than reading a DOM that disagrees.
15004
- controller.opened = opened;
15068
+ // A request arriving before the change lands runs it first rather than
15069
+ // reading a DOM that disagrees.
15005
15070
  changeAwaitingTransition = applyChange;
15006
15071
  transitionChange(
15007
15072
  () => {
@@ -15014,7 +15079,6 @@ const createOpenController = (
15014
15079
  };
15015
15080
 
15016
15081
  const performClose = (closeEvent) => {
15017
- controller.opened = false;
15018
15082
  // Read before any close effect touches the DOM: closing a native <dialog>
15019
15083
  // hands the focus back to whatever held it at showModal() time, so by the
15020
15084
  // time the close cleanup runs, the popup's content has already lost the
@@ -15282,7 +15346,6 @@ const createOpenController = (
15282
15346
  // told it opened right after (see popup_content_mount.js and
15283
15347
  // use_displayed_layout_effect.js).
15284
15348
  controller.openedInDom = true;
15285
- controller.opened = true;
15286
15349
  // Which press it opened during, so the release of that press is not
15287
15350
  // read as somebody dismissing it (see openedDuringThisPress).
15288
15351
  controller.pressCountAtOpen = pressCount;
@@ -15709,8 +15772,10 @@ const useOpenPropsEffectOnOpenController = (
15709
15772
  return undefined;
15710
15773
  }
15711
15774
  // Skip when the controller is already in the desired state.
15712
- // openController.opened tracks actual open/close (updated by onopen/onclose,
15713
- // not by renders) so it is the authoritative check against feedback loops.
15775
+ // openController.opened is what the controller decided, written before an
15776
+ // opening renders anything (see runChange), so it is the authoritative
15777
+ // check against feedback loops — the popup's own onOpen writing its
15778
+ // `signal` included.
15714
15779
  if (open === openController.opened) {
15715
15780
  return undefined;
15716
15781
  }
@@ -19553,18 +19618,17 @@ const MIN_CONSTRAINT = {
19553
19618
  if (cmp === null || cmp >= 0) {
19554
19619
  return null;
19555
19620
  }
19556
-
19557
- return naviI18n("constraint.min.duration.default", {
19558
- min: formatDuration(min),
19559
- });
19621
+ return boundMessage(field, "min");
19560
19622
  }
19561
19623
  if (field.controlType !== "input" && field.controlType !== "picker") {
19562
19624
  return null;
19563
19625
  }
19564
- const minString = field.controlHostProps.min;
19565
- if (!minString) {
19626
+ const min = field.controlHostProps.min;
19627
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
19628
+ if (min === undefined || min === null || min === "") {
19566
19629
  return null;
19567
19630
  }
19631
+ const minString = String(min);
19568
19632
  const type = field.controlHostProps.type;
19569
19633
  const naviInputType = field.controlHostProps["navi-input-type"];
19570
19634
  const valueAsString = uiStateAsText(field.uiState);
@@ -19580,40 +19644,16 @@ const MIN_CONSTRAINT = {
19580
19644
  if (isNaN(numericValue)) {
19581
19645
  return null;
19582
19646
  }
19583
- if (numericValue < minNumber) {
19584
- if (naviInputType === "hour") {
19585
- return naviI18n(`constraint.min.hour.default`, {
19586
- min: minString,
19587
- });
19588
- }
19589
- if (naviInputType === "minute") {
19590
- return naviI18n(`constraint.min.minute.default`, {
19591
- min: minString,
19592
- });
19593
- }
19594
- if (naviInputType === "second") {
19595
- return naviI18n(`constraint.min.second.default`, {
19596
- min: minString,
19597
- });
19598
- }
19599
- if (naviInputType === "percentage") {
19600
- return naviI18n(`constraint.min.percentage.default`, {
19601
- min: minString,
19602
- });
19603
- }
19604
- return naviI18n(`constraint.min.number.default`, {
19605
- min: minString,
19606
- });
19647
+ if (numericValue >= minNumber) {
19648
+ return null;
19607
19649
  }
19608
- return null;
19650
+ return boundMessage(field, "min");
19609
19651
  }
19610
19652
  if (type === "time") {
19611
19653
  const [minHours, minMinutes] = minString.split(":").map(Number);
19612
19654
  const [hours, minutes] = valueAsString.split(":").map(Number);
19613
19655
  if (hours < minHours || (hours === minHours && minutes < minMinutes)) {
19614
- return naviI18n("constraint.min.time.default", {
19615
- min: minString,
19616
- });
19656
+ return boundMessage(field, "min");
19617
19657
  }
19618
19658
  return null;
19619
19659
  }
@@ -19621,18 +19661,13 @@ const MIN_CONSTRAINT = {
19621
19661
  // so they never need a min/max validation message.
19622
19662
  if (DATE_INPUT_TYPE_SET.has(type)) {
19623
19663
  if (valueAsString < minString) {
19624
- const todayIso = getTodayIso(type);
19625
- if (minString === todayIso) {
19626
- return naviI18n("constraint.min.date.today.default");
19627
- }
19628
- return naviI18n("constraint.min.date.default", {
19629
- min: formatDateIso(minString, type),
19630
- });
19664
+ return boundMessage(field, "min");
19631
19665
  }
19632
19666
  return null;
19633
19667
  }
19634
19668
  return null;
19635
19669
  },
19670
+ messageAtBound: (field) => boundMessage(field, "min"),
19636
19671
  };
19637
19672
  CONSTRAINT_ATTRIBUTE_SET.add("min");
19638
19673
 
@@ -19652,18 +19687,17 @@ const MAX_CONSTRAINT = {
19652
19687
  if (cmp === null || cmp <= 0) {
19653
19688
  return null;
19654
19689
  }
19655
-
19656
- return naviI18n("constraint.max.duration.default", {
19657
- max: formatDuration(max),
19658
- });
19690
+ return boundMessage(field, "max");
19659
19691
  }
19660
19692
  if (field.controlType !== "input" && field.controlType !== "picker") {
19661
19693
  return null;
19662
19694
  }
19663
- const maxString = field.controlHostProps.max;
19664
- if (!maxString) {
19695
+ const max = field.controlHostProps.max;
19696
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
19697
+ if (max === undefined || max === null || max === "") {
19665
19698
  return null;
19666
19699
  }
19700
+ const maxString = String(max);
19667
19701
  const type = field.controlHostProps.type;
19668
19702
  const naviInputType = field.controlHostProps["navi-input-type"];
19669
19703
  const valueAsString = uiStateAsText(field.uiState);
@@ -19682,38 +19716,13 @@ const MAX_CONSTRAINT = {
19682
19716
  if (numericValue <= maxNumber) {
19683
19717
  return null;
19684
19718
  }
19685
-
19686
- if (naviInputType === "hour") {
19687
- return naviI18n(`constraint.max.hour.default`, {
19688
- max: maxString,
19689
- });
19690
- }
19691
- if (naviInputType === "minute") {
19692
- return naviI18n(`constraint.max.minute.default`, {
19693
- max: maxString,
19694
- });
19695
- }
19696
- if (naviInputType === "second") {
19697
- return naviI18n(`constraint.max.second.default`, {
19698
- max: maxString,
19699
- });
19700
- }
19701
- if (naviInputType === "percentage") {
19702
- return naviI18n(`constraint.max.percentage.default`, {
19703
- max: maxString,
19704
- });
19705
- }
19706
- return naviI18n(`constraint.max.number.default`, {
19707
- max: maxString,
19708
- });
19719
+ return boundMessage(field, "max");
19709
19720
  }
19710
19721
  if (type === "time") {
19711
19722
  const [maxHours, maxMinutes] = maxString.split(":").map(Number);
19712
19723
  const [hours, minutes] = valueAsString.split(":").map(Number);
19713
19724
  if (hours > maxHours || (hours === maxHours && minutes > maxMinutes)) {
19714
- return naviI18n("constraint.max.time.default", {
19715
- max: maxString,
19716
- });
19725
+ return boundMessage(field, "max");
19717
19726
  }
19718
19727
  return null;
19719
19728
  }
@@ -19721,17 +19730,11 @@ const MAX_CONSTRAINT = {
19721
19730
  if (valueAsString <= maxString) {
19722
19731
  return null;
19723
19732
  }
19724
-
19725
- const todayIso = getTodayIso(type);
19726
- if (maxString === todayIso) {
19727
- return naviI18n("constraint.max.date.today.default");
19728
- }
19729
- return naviI18n("constraint.max.date.default", {
19730
- max: formatDateIso(maxString, type),
19731
- });
19733
+ return boundMessage(field, "max");
19732
19734
  }
19733
19735
  return null;
19734
19736
  },
19737
+ messageAtBound: (field) => boundMessage(field, "max"),
19735
19738
  };
19736
19739
  CONSTRAINT_ATTRIBUTE_SET.add("max");
19737
19740
 
@@ -19926,6 +19929,51 @@ const formatMsToTime = (ms, showSeconds) => {
19926
19929
  return `${hh}:${mm}:${ss}`;
19927
19930
  };
19928
19931
 
19932
+ // What a bound says, whatever the value: the sentence `check` gives a value
19933
+ // past it, and the one a spin gives when its chevron is refused there
19934
+ // (picker_spin.jsx) — one sentence, so the two refuse in the same words. null
19935
+ // for a type that has nothing to say about a bound.
19936
+ const boundMessage = (field, bound) => {
19937
+ const boundValue = field.controlHostProps[bound];
19938
+ if (field.controlType === "duration_group") {
19939
+ return naviI18n(`constraint.${bound}.duration.default`, {
19940
+ [bound]: formatDuration(boundValue),
19941
+ });
19942
+ }
19943
+ const type = field.controlHostProps.type;
19944
+ const naviInputType = field.controlHostProps["navi-input-type"];
19945
+ if (isNumberInput(type, naviInputType)) {
19946
+ const numberKind = NUMBER_BOUND_KIND_SET.has(naviInputType)
19947
+ ? naviInputType
19948
+ : "number";
19949
+ return naviI18n(`constraint.${bound}.${numberKind}.default`, {
19950
+ [bound]: String(boundValue),
19951
+ });
19952
+ }
19953
+ if (type === "time") {
19954
+ return naviI18n(`constraint.${bound}.time.default`, {
19955
+ [bound]: boundValue,
19956
+ });
19957
+ }
19958
+ if (DATE_INPUT_TYPE_SET.has(type)) {
19959
+ if (boundValue === getTodayIso(type)) {
19960
+ return naviI18n(`constraint.${bound}.date.today.default`);
19961
+ }
19962
+ return naviI18n(`constraint.${bound}.date.default`, {
19963
+ [bound]: formatDateIso(boundValue, type),
19964
+ });
19965
+ }
19966
+ return null;
19967
+ };
19968
+ // The numbers whose bound is said with what they count ("Max 23 heures");
19969
+ // any other is "Ce nombre".
19970
+ const NUMBER_BOUND_KIND_SET = new Set([
19971
+ "hour",
19972
+ "minute",
19973
+ "second",
19974
+ "percentage",
19975
+ ]);
19976
+
19929
19977
  const getTodayIso = (inputType) => {
19930
19978
  const now = new Date();
19931
19979
  const yyyy = now.getFullYear();
@@ -25793,6 +25841,171 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
25793
25841
  return childStore !== undefined && childStore === getChildStore(ownerId);
25794
25842
  };
25795
25843
 
25844
+ /*
25845
+ * What a previous document kept of the page it was on (see keepPageOnScreen in
25846
+ * nav/page_kept.js), waiting for the reads that draw it again, and what those
25847
+ * reads are made of on the way out: rows with their relations inline.
25848
+ *
25849
+ * A kept read is found by the read's name and the params it was asked with
25850
+ * (getKeptReadKey). It is handed out once: the first run of the read draws it
25851
+ * while its request is out, and from then on the store holds the rows. Handed
25852
+ * out again, after a reset say, it would write the previous document's rows
25853
+ * over a fresher answer.
25854
+ *
25855
+ * This module is the half the resources see: `resource()` and the range reader
25856
+ * take from it, and say which of their reads can be kept at all. Which reads
25857
+ * are on the page on screen, and what the slot is written into, is
25858
+ * page_kept.js.
25859
+ */
25860
+
25861
+ // key → what the read answered in the previous document, not handed out yet
25862
+ const offeredReadMap = new Map();
25863
+ const offerKeptReads = (entryMap) => {
25864
+ offeredReadMap.clear();
25865
+ for (const [key, entry] of entryMap) {
25866
+ offeredReadMap.set(key, entry);
25867
+ }
25868
+ };
25869
+ const withdrawKeptReads = () => {
25870
+ offeredReadMap.clear();
25871
+ };
25872
+ const takeKeptRead = (key) => {
25873
+ const entry = offeredReadMap.get(key);
25874
+ if (entry === undefined) {
25875
+ return undefined;
25876
+ }
25877
+ offeredReadMap.delete(key);
25878
+ return entry;
25879
+ };
25880
+
25881
+ // The root actions whose answer is rows of a store, written and read back the
25882
+ // same way: a resource's GET and GET_MANY. Anything else a route runs (a page's
25883
+ // code, a computation) has nothing a later document could draw.
25884
+ const keepableReadWeakSet = new WeakSet();
25885
+ const markKeepableRead = (rootAction) => {
25886
+ keepableReadWeakSet.add(rootAction);
25887
+ };
25888
+ const isKeepableRead = (action) => {
25889
+ const rootAction = action.rootAction || action;
25890
+ return keepableReadWeakSet.has(rootAction);
25891
+ };
25892
+
25893
+ // The range readers a <List.Items> on screen reads through, each with the number
25894
+ // of lists reading it: the reads of the page a route action does not cover.
25895
+ const shownRangeReadersSignal = signal(new Map());
25896
+ const showRangeReader = (reader) => {
25897
+ const shownMap = new Map(shownRangeReadersSignal.peek());
25898
+ shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
25899
+ shownRangeReadersSignal.value = shownMap;
25900
+ return () => {
25901
+ const shownMapNow = new Map(shownRangeReadersSignal.peek());
25902
+ const count = shownMapNow.get(reader);
25903
+ if (count === 1) {
25904
+ shownMapNow.delete(reader);
25905
+ } else {
25906
+ shownMapNow.set(reader, count - 1);
25907
+ }
25908
+ shownRangeReadersSignal.value = shownMapNow;
25909
+ };
25910
+ };
25911
+
25912
+ // A row already in the store is the fresher one: the kept copy only stands in
25913
+ // for rows nothing has answered yet. Its relations are written as they were
25914
+ // kept, which a real answer corrects like any other.
25915
+ const upsertKeptItem = (store, keptItem) => {
25916
+ const id = keptItem[store.idKey];
25917
+ if (id !== undefined) {
25918
+ const existing = store.select(id);
25919
+ if (existing) {
25920
+ return existing;
25921
+ }
25922
+ }
25923
+ return store.upsert(keptItem);
25924
+ };
25925
+
25926
+ const getKeptReadKey = (name, params) => {
25927
+ return `${name}|${getParamsKey(params)}`;
25928
+ };
25929
+ // `true` is what a route action without params is bound to (see routeAction),
25930
+ // and a key holding undefined says nothing a missing one does not.
25931
+ const getParamsKey = (params) => {
25932
+ if (params === undefined || params === NO_PARAMS || params === true) {
25933
+ return "";
25934
+ }
25935
+ if (isPlainParams(params)) {
25936
+ const keys = Object.keys(params).filter((key) => params[key] !== undefined);
25937
+ if (keys.length === 0) {
25938
+ return "";
25939
+ }
25940
+ }
25941
+ return stableStringify(params);
25942
+ };
25943
+ const isPlainParams = (value) => {
25944
+ return value !== null && typeof value === "object" && !Array.isArray(value);
25945
+ };
25946
+ const stableStringify = (value) => {
25947
+ if (value === null || typeof value !== "object") {
25948
+ return JSON.stringify(value);
25949
+ }
25950
+ if (Array.isArray(value)) {
25951
+ return `[${value.map(stableStringify).join(",")}]`;
25952
+ }
25953
+ const keys = Object.keys(value)
25954
+ .filter((key) => value[key] !== undefined)
25955
+ .sort();
25956
+ const parts = [];
25957
+ for (const key of keys) {
25958
+ parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
25959
+ }
25960
+ return `{${parts.join(",")}}`;
25961
+ };
25962
+
25963
+ // What a read's data is made of, ready to be written: a row, or the rows of a
25964
+ // list, each with its relations inline.
25965
+ const serializeData = (data) => {
25966
+ if (Array.isArray(data)) {
25967
+ return data.map((item) => serializeItem(item, new Set()));
25968
+ }
25969
+ return serializeItem(data, new Set());
25970
+ };
25971
+ // The item with its relations inline: a relation value stands for what its
25972
+ // signal holds (the child row, the child rows, or nothing), a row reached
25973
+ // twice on the same path is written as its id — the setters accept both.
25974
+ const serializeItem = (item, ancestorSet) => {
25975
+ ancestorSet.add(item);
25976
+ const serialized = {};
25977
+ for (const key of Object.keys(item)) {
25978
+ serialized[key] = serializeValue(item[key], ancestorSet);
25979
+ }
25980
+ ancestorSet.delete(item);
25981
+ return serialized;
25982
+ };
25983
+ const serializeValue = (value, ancestorSet) => {
25984
+ if (value === null || typeof value !== "object") {
25985
+ return value;
25986
+ }
25987
+ if (Array.isArray(value)) {
25988
+ return value.map((entry) => serializeValue(entry, ancestorSet));
25989
+ }
25990
+ const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
25991
+ if (objectSignal) {
25992
+ const held = objectSignal.value;
25993
+ if (held === undefined || held === null) {
25994
+ return null;
25995
+ }
25996
+ return serializeValue(held, ancestorSet);
25997
+ }
25998
+ if (typeof value.toJSON === "function") {
25999
+ return value;
26000
+ }
26001
+ if (ancestorSet.has(value)) {
26002
+ return Object.hasOwn(value, SYMBOL_IDENTITY)
26003
+ ? value[SYMBOL_IDENTITY]
26004
+ : null;
26005
+ }
26006
+ return serializeItem(value, ancestorSet);
26007
+ };
26008
+
25796
26009
  const paramScopeWeakSet = createIterableWeakSet();
25797
26010
  let paramScopeIdCounter = 0;
25798
26011
  const getParamScope = (params) => {
@@ -25963,66 +26176,6 @@ const createResourcePersistence = (
25963
26176
  };
25964
26177
  };
25965
26178
 
25966
- const getParamsKey = (params) => {
25967
- if (params === undefined || params === NO_PARAMS) {
25968
- return "";
25969
- }
25970
- return stableStringify(params);
25971
- };
25972
-
25973
- const stableStringify = (value) => {
25974
- if (value === null || typeof value !== "object") {
25975
- return JSON.stringify(value);
25976
- }
25977
- if (Array.isArray(value)) {
25978
- return `[${value.map(stableStringify).join(",")}]`;
25979
- }
25980
- const keys = Object.keys(value).sort();
25981
- const parts = [];
25982
- for (const key of keys) {
25983
- parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
25984
- }
25985
- return `{${parts.join(",")}}`;
25986
- };
25987
-
25988
- // The item with its relations inline: a relation value stands for what its
25989
- // signal holds (the child row, the child rows, or nothing), a row reached
25990
- // twice on the same path is written as its id — the setters accept both.
25991
- const serializeItem = (item, ancestorSet) => {
25992
- ancestorSet.add(item);
25993
- const serialized = {};
25994
- for (const key of Object.keys(item)) {
25995
- serialized[key] = serializeValue(item[key], ancestorSet);
25996
- }
25997
- ancestorSet.delete(item);
25998
- return serialized;
25999
- };
26000
- const serializeValue = (value, ancestorSet) => {
26001
- if (value === null || typeof value !== "object") {
26002
- return value;
26003
- }
26004
- if (Array.isArray(value)) {
26005
- return value.map((entry) => serializeValue(entry, ancestorSet));
26006
- }
26007
- const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
26008
- if (objectSignal) {
26009
- const held = objectSignal.value;
26010
- if (held === undefined || held === null) {
26011
- return null;
26012
- }
26013
- return serializeValue(held, ancestorSet);
26014
- }
26015
- if (typeof value.toJSON === "function") {
26016
- return value;
26017
- }
26018
- if (ancestorSet.has(value)) {
26019
- return Object.hasOwn(value, SYMBOL_IDENTITY)
26020
- ? value[SYMBOL_IDENTITY]
26021
- : null;
26022
- }
26023
- return serializeItem(value, ancestorSet);
26024
- };
26025
-
26026
26179
  /*
26027
26180
  * GET_RANGE: reading a resource one slice at a time, for a list that draws its
26028
26181
  * rows as it goes (`<List.Items itemsAction>`).
@@ -26059,9 +26212,23 @@ const serializeValue = (value, ancestorSet) => {
26059
26212
  * of what a rerun is for an action. Every reader made by `bindParams` shares
26060
26213
  * the signal and the compositions of the one it comes from: the params say
26061
26214
  * which slices are read, not which collection.
26215
+ *
26216
+ * A composition also outlives the document when a list on the page on screen
26217
+ * reads it (see keepPageOnScreen): the ranks around the window it draws are
26218
+ * written with their rows, and the first read of the next document finds them
26219
+ * as a composition left by a list that went away — the same return, after a
26220
+ * longer absence.
26062
26221
  */
26063
26222
 
26064
26223
 
26224
+ // The ranks kept of a composition for the next document: the window a list
26225
+ // draws, widened by at least one step on each side, and snapped to steps so a
26226
+ // scroll within one draws the same ranks and writes nothing.
26227
+ const KEPT_RANK_STEP = 20;
26228
+ // Counts the answers every composition took, so that one landing can be told
26229
+ // from one seen before (see readKept).
26230
+ let answerSequence = 0;
26231
+
26065
26232
  const createRangeReader = (
26066
26233
  actionName,
26067
26234
  callback,
@@ -26070,6 +26237,9 @@ const createRangeReader = (
26070
26237
  params: boundParams,
26071
26238
  invalidationSignal = signal(0),
26072
26239
  compositionSet = new Set(),
26240
+ // Bumped whenever what is kept of a composition for the next document
26241
+ // changes (see readKept); shared like the compositions themselves.
26242
+ keptVersionSignal = signal(0),
26073
26243
  },
26074
26244
  ) => {
26075
26245
  // Which composition this reader is about: the values its params hold, not its
@@ -26086,6 +26256,40 @@ const createRangeReader = (
26086
26256
  }
26087
26257
  return null;
26088
26258
  };
26259
+ const keptKeyOf = (params) => getKeptReadKey(actionName, params);
26260
+ const bumpKeptVersion = () => {
26261
+ keptVersionSignal.value = keptVersionSignal.peek() + 1;
26262
+ };
26263
+ // The composition the previous document kept for these params, entering the
26264
+ // store and the compositions as if a list had just left it.
26265
+ const restoreKeptComposition = (params) => {
26266
+ const kept = takeKeptRead(keptKeyOf(params));
26267
+ if (!kept || typeof kept.count !== "number" || !kept.items) {
26268
+ return null;
26269
+ }
26270
+ const composition = {
26271
+ params,
26272
+ idByIndex: new Map(),
26273
+ count: kept.count,
26274
+ keptFrom: undefined,
26275
+ keptTo: undefined,
26276
+ answeredAt: 0,
26277
+ };
26278
+ for (const rank of Object.keys(kept.items)) {
26279
+ const index = Number(rank);
26280
+ const item = upsertKeptItem(store, kept.items[rank]);
26281
+ composition.idByIndex.set(index, item[store.idKey]);
26282
+ if (composition.keptFrom === undefined || index < composition.keptFrom) {
26283
+ composition.keptFrom = index;
26284
+ }
26285
+ if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
26286
+ composition.keptTo = index + 1;
26287
+ }
26288
+ }
26289
+ compositionSet.add(composition);
26290
+ bumpKeptVersion();
26291
+ return composition;
26292
+ };
26089
26293
  const readRange = async (range = {}) => {
26090
26294
  const { signal, ...rangeParams } = range;
26091
26295
  const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
@@ -26119,11 +26323,15 @@ const createRangeReader = (
26119
26323
  readRange.invalidate = () => {
26120
26324
  compositionSet.clear();
26121
26325
  invalidationSignal.value = invalidationSignal.peek() + 1;
26326
+ bumpKeptVersion();
26122
26327
  };
26123
26328
  // The rows of a composition, drawn from the store: a rank whose row is gone
26124
26329
  // from the store resolves to nothing and is asked for again.
26125
26330
  readRange.readComposition = () => {
26126
- const composition = findComposition(currentParams());
26331
+ const params = currentParams();
26332
+ const composition = untracked(
26333
+ () => findComposition(params) || restoreKeptComposition(params),
26334
+ );
26127
26335
  if (!composition) {
26128
26336
  return null;
26129
26337
  }
@@ -26147,18 +26355,85 @@ const createRangeReader = (
26147
26355
  const params = currentParams();
26148
26356
  let composition = findComposition(params);
26149
26357
  if (!composition) {
26150
- composition = { params, idByIndex: new Map(), count };
26358
+ composition = {
26359
+ params,
26360
+ idByIndex: new Map(),
26361
+ count,
26362
+ keptFrom: undefined,
26363
+ keptTo: undefined,
26364
+ answeredAt: 0,
26365
+ };
26151
26366
  compositionSet.add(composition);
26152
26367
  } else if (replace) {
26153
26368
  composition.idByIndex = new Map();
26154
26369
  }
26155
26370
  composition.count = count;
26371
+ answerSequence++;
26372
+ composition.answeredAt = answerSequence;
26156
26373
  for (const [index, item] of byIndex) {
26157
26374
  const id = item ? item[store.idKey] : undefined;
26158
26375
  if (id !== undefined) {
26159
26376
  composition.idByIndex.set(index, id);
26160
26377
  }
26161
26378
  }
26379
+ bumpKeptVersion();
26380
+ };
26381
+ // The ranks [windowFrom, windowTo) a list draws of this composition: what is
26382
+ // kept of it for the next document is around them (see readKept).
26383
+ readRange.frameComposition = (windowFrom, windowTo) => {
26384
+ const composition = findComposition(currentParams());
26385
+ if (!composition) {
26386
+ return;
26387
+ }
26388
+ let keptFrom =
26389
+ Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
26390
+ KEPT_RANK_STEP;
26391
+ if (keptFrom < 0) {
26392
+ keptFrom = 0;
26393
+ }
26394
+ const keptTo =
26395
+ Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
26396
+ if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
26397
+ return;
26398
+ }
26399
+ composition.keptFrom = keptFrom;
26400
+ composition.keptTo = keptTo;
26401
+ bumpKeptVersion();
26402
+ };
26403
+ // What a document keeps of the composition these params read, for the
26404
+ // next one (see keepPageOnScreen): the count, and the ranks around the
26405
+ // window last drawn with their rows inline. Read inside an effect, it
26406
+ // follows the composition and the rows. `answeredAt` grows with every answer
26407
+ // the composition takes (0: none in this document, restored from a kept one).
26408
+ readRange.readKept = () => {
26409
+ // eslint-disable-next-line no-unused-expressions
26410
+ keptVersionSignal.value;
26411
+ const params = resolveParams(boundParams);
26412
+ const key = keptKeyOf(params);
26413
+ const composition = findComposition(params);
26414
+ if (!composition || composition.count === undefined) {
26415
+ return { key, answeredAt: 0, readEntry: () => undefined };
26416
+ }
26417
+ const readEntry = () => {
26418
+ const items = {};
26419
+ for (const [index, id] of composition.idByIndex) {
26420
+ if (
26421
+ composition.keptFrom !== undefined &&
26422
+ index < composition.keptFrom
26423
+ ) {
26424
+ continue;
26425
+ }
26426
+ if (composition.keptTo !== undefined && index >= composition.keptTo) {
26427
+ continue;
26428
+ }
26429
+ const item = store.select(id);
26430
+ if (item) {
26431
+ items[index] = serializeItem(item, new Set());
26432
+ }
26433
+ }
26434
+ return { count: composition.count, items };
26435
+ };
26436
+ return { key, answeredAt: composition.answeredAt, readEntry };
26162
26437
  };
26163
26438
  // The same trade a list makes with the rows it holds, applied to what is kept
26164
26439
  // for the next mount. Two lists on one collection each trim by their own
@@ -26169,11 +26444,16 @@ const createRangeReader = (
26169
26444
  if (!composition || composition.idByIndex.size <= budget) {
26170
26445
  return;
26171
26446
  }
26447
+ let trimmed = false;
26172
26448
  for (const index of composition.idByIndex.keys()) {
26173
26449
  if (index < keepFrom || index > keepTo) {
26174
26450
  composition.idByIndex.delete(index);
26451
+ trimmed = true;
26175
26452
  }
26176
26453
  }
26454
+ if (trimmed) {
26455
+ bumpKeptVersion();
26456
+ }
26177
26457
  };
26178
26458
  // Memoized for the reasons an action's bindParams is (see actions.js): params
26179
26459
  // and the reader they make have synchronized lifetimes, and params equal in
@@ -26189,6 +26469,7 @@ const createRangeReader = (
26189
26469
  params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
26190
26470
  invalidationSignal,
26191
26471
  compositionSet,
26472
+ keptVersionSignal,
26192
26473
  });
26193
26474
  readerByParams.set(paramsToBind, reader);
26194
26475
  return reader;
@@ -27623,7 +27904,7 @@ const createRestActionFactoryForRoot = (
27623
27904
  `${name}.${verb}`,
27624
27905
  declarationSite,
27625
27906
  );
27626
- return createAction(callback, {
27907
+ const action = createAction(callback, {
27627
27908
  name: `${name}.${verb}`,
27628
27909
  meta: { verb, isMany: false, paramScope },
27629
27910
  resultToValue: (result, action) => {
@@ -27659,17 +27940,33 @@ const createRestActionFactoryForRoot = (
27659
27940
  },
27660
27941
  valueToData: (itemId) => store.select(itemId),
27661
27942
  // While the request is out, the row the store already holds for these
27662
- // params is drawn: the action starts on the "refresh over a known
27943
+ // params is drawn, or the row the page kept from the previous document
27944
+ // (kept_reads.js): the action starts on the "refresh over a known
27663
27945
  // answer" line of data_states.md rather than the first-load one.
27664
27946
  provisionalValue:
27665
27947
  verb === "GET"
27666
- ? (params) => {
27667
- const item = untracked(() => findItemForGet(params));
27948
+ ? (params, action) => {
27949
+ const item = untracked(() => {
27950
+ const itemForGet = findItemForGet(params);
27951
+ if (itemForGet) {
27952
+ return itemForGet;
27953
+ }
27954
+ const keptItem = takeKeptRead(
27955
+ getKeptReadKey(action.name, params),
27956
+ );
27957
+ return keptItem ? upsertKeptItem(store, keptItem) : null;
27958
+ });
27668
27959
  return item ? item[idKey] : undefined;
27669
27960
  }
27670
27961
  : undefined,
27962
+ // Another params' answer is another row.
27963
+ inheritData: verb !== "GET",
27671
27964
  completeSideEffect: onActionComplete,
27672
27965
  });
27966
+ if (verb === "GET") {
27967
+ markKeepableRead(action);
27968
+ }
27969
+ return action;
27673
27970
  };
27674
27971
  const createActionAffectingManyItems = (
27675
27972
  verb,
@@ -27684,7 +27981,7 @@ const createRestActionFactoryForRoot = (
27684
27981
  return itemArray.map((item) => item[idKey]);
27685
27982
  };
27686
27983
 
27687
- return createAction(callback, {
27984
+ const action = createAction(callback, {
27688
27985
  meta: { verb, isMany: true, paramScope },
27689
27986
  name: `${name}.${verb}_MANY`,
27690
27987
  resultToValue: applyResultToValue,
@@ -27692,6 +27989,25 @@ const createRestActionFactoryForRoot = (
27692
27989
  // undefined so a screen can tell "not asked yet" from "answered: none".
27693
27990
  valueToData: (idArray) =>
27694
27991
  idArray === undefined ? undefined : store.selectAll(idArray),
27992
+ // The rows the page kept from the previous document (kept_reads.js),
27993
+ // drawn while the request is out. A binding moving to new params still
27994
+ // hands over the previous answer when there are none: a list standing
27995
+ // in for the next one beats a skeleton.
27996
+ provisionalValue:
27997
+ verb === "GET"
27998
+ ? (params, action) =>
27999
+ untracked(() => {
28000
+ const keptItems = takeKeptRead(
28001
+ getKeptReadKey(action.name, params),
28002
+ );
28003
+ if (!Array.isArray(keptItems)) {
28004
+ return undefined;
28005
+ }
28006
+ return keptItems.map(
28007
+ (keptItem) => upsertKeptItem(store, keptItem)[idKey],
28008
+ );
28009
+ })
28010
+ : undefined,
27695
28011
  completeSideEffect: (actionCompleted) => {
27696
28012
  onActionComplete(actionCompleted);
27697
28013
  if (
@@ -27707,6 +28023,10 @@ const createRestActionFactoryForRoot = (
27707
28023
  return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
27708
28024
  },
27709
28025
  });
28026
+ if (verb === "GET") {
28027
+ markKeepableRead(action);
28028
+ }
28029
+ return action;
27710
28030
  };
27711
28031
 
27712
28032
  return createActionForRoot;
@@ -27717,10 +28037,12 @@ const createRestActionFactoryForRoot = (
27717
28037
  // NetworkPolicyError carrying the policy's reason. A read is called or not depending
27718
28038
  // on where the policy answers reads from. Answered from the store, a GET of a
27719
28039
  // root resource completes with the row the store holds for it (see
27720
- // findItemInStore) — handing the item back is an upsert without effect, so the
27721
- // action completes with what it had and nothing is asked; a relationship GET
27722
- // has no row of its own to answer with and settles with the same error. A
27723
- // completed GET asked to rerun never gets here (actions.js holds it).
28040
+ // findItemInStore), and a root GET or GET_MANY holding rows for these params
28041
+ // (the kept page, drawn while the request is out) with those — handing them
28042
+ // back is an upsert without effect, so the action completes with what it had
28043
+ // and nothing is asked. A relationship GET has no row of its own to answer
28044
+ // with and settles with the same error. A completed GET asked to rerun never
28045
+ // gets here (actions.js holds it).
27724
28046
  const applyNetworkPolicy = (
27725
28047
  restCallback,
27726
28048
  { verb, isMany, findItemInStore },
@@ -27734,6 +28056,12 @@ const applyNetworkPolicy = (
27734
28056
  if (!policy.readsFromStore) {
27735
28057
  return restCallback(params, context);
27736
28058
  }
28059
+ if (isKeepableRead(context.action)) {
28060
+ const dataHeld = context.action.dataSignal.peek();
28061
+ if (dataHeld !== undefined) {
28062
+ return dataHeld;
28063
+ }
28064
+ }
27737
28065
  if (!isMany && findItemInStore) {
27738
28066
  const item = findItemInStore(params, context.action);
27739
28067
  if (item) {
@@ -28261,24 +28589,6 @@ const TYPE_CONVERTERS = {
28261
28589
  },
28262
28590
  };
28263
28591
 
28264
- /**
28265
- * What a component can learn about the page it is rendered in, from the route
28266
- * container rendering that page (see useContentKeepingLeavingPages in
28267
- * route.jsx). `null` outside of any route container.
28268
- *
28269
- * - `isShown()` — whether the container would still show the page. False as
28270
- * soon as the address leads elsewhere: the page is about to be taken down, or
28271
- * hidden until the route transition leaving it is over, and nothing it reads
28272
- * is worth a render any more. False for good once it is kept while leaving: a
28273
- * return to its route mounts the page anew. Asked outside any render — by a
28274
- * subscription deciding whether to re-render — and it reads the routes
28275
- * untracked, so asking subscribes nobody to them.
28276
- * - `leavingSignal` — true once the page is hidden and kept while its movement
28277
- * plays: still mounted, so what it fills outside its own nodes (a slot in a
28278
- * bar) steps out rather than showing with the page arriving.
28279
- */
28280
- const RoutePageContext = createContext(null);
28281
-
28282
28592
  const promiseStateWeakMap = new WeakMap();
28283
28593
  const usePromiseAsyncData = (
28284
28594
  promise,
@@ -33120,11 +33430,17 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
33120
33430
  }
33121
33431
  }
33122
33432
  });
33433
+ // Also run when a Suspense boundary above parks this container — a page
33434
+ // arriving that suspends to a `<Loading>` around both pages: every hook of
33435
+ // the pages kept while leaving has then been cleaned up, which is all that
33436
+ // keeping them saved. Dropped, so the render resuming the container takes
33437
+ // them down, rather than re-running every effect of a hidden page.
33123
33438
  useLayoutEffect(() => {
33124
33439
  return () => {
33125
33440
  for (const entry of leavingRef.current) {
33126
33441
  entry.hold.containers.delete(rerender);
33127
33442
  }
33443
+ leavingRef.current = [];
33128
33444
  };
33129
33445
  }, []);
33130
33446
  if (leaving.length === 0) {
@@ -37896,6 +38212,37 @@ const writeCover = (edge, element, size) => {
37896
38212
  style.setProperty(property, value);
37897
38213
  };
37898
38214
 
38215
+ // Every route action, with whether it asks something of the address at the
38216
+ // moment: its route matches and its params getter returns something.
38217
+ const routeActionEntriesSignal = signal([]);
38218
+ let activeRouteActionsPrevious = new Set();
38219
+ /**
38220
+ * The route actions asking something for the page on screen: their route
38221
+ * matches and their params getter returns something — the running ones, not
38222
+ * every matching one, so an action bound to every page that asks only when a
38223
+ * popup is open over it counts while it is open. Read to rerun what the page
38224
+ * reads (the app back from the background), and by `keepPageOnScreen`.
38225
+ * The set is handed out again only when its members change.
38226
+ *
38227
+ * A page's code is declared as a route action too; `action.meta.verb` says
38228
+ * which ones read data.
38229
+ *
38230
+ * @type {import("@preact/signals").ReadonlySignal<Set<object>>}
38231
+ */
38232
+ const activeRouteActionsSignal = computed(() => {
38233
+ const active = new Set();
38234
+ for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
38235
+ if (askingSignal.value) {
38236
+ active.add(routeAction);
38237
+ }
38238
+ }
38239
+ if (haveSameMembers(active, activeRouteActionsPrevious)) {
38240
+ return activeRouteActionsPrevious;
38241
+ }
38242
+ activeRouteActionsPrevious = active;
38243
+ return active;
38244
+ });
38245
+
37899
38246
  /**
37900
38247
  * Binds an action to a route: it runs when the route matches, with the params
37901
38248
  * the effect reads off the address, runs again on a `reload()`, and is aborted
@@ -37953,6 +38300,13 @@ const routeAction = (
37953
38300
  return params;
37954
38301
  };
37955
38302
  const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
38303
+ routeActionEntriesSignal.value = [
38304
+ ...routeActionEntriesSignal.peek(),
38305
+ {
38306
+ routeAction: actionBoundToRoute,
38307
+ askingSignal: computed(() => Boolean(readParamsAsked())),
38308
+ },
38309
+ ];
37956
38310
  for (const route of routes) {
37957
38311
  // Asked of the route's params as they are, like the effect asks them.
37958
38312
  registerRouteReload(route, () =>
@@ -38036,6 +38390,343 @@ const anyMatchingRouteSignal = (routes) => {
38036
38390
  return anyMatchingSignal;
38037
38391
  };
38038
38392
 
38393
+ const haveSameMembers = (setA, setB) => {
38394
+ if (setA.size !== setB.size) {
38395
+ return false;
38396
+ }
38397
+ for (const value of setA) {
38398
+ if (!setB.has(value)) {
38399
+ return false;
38400
+ }
38401
+ }
38402
+ return true;
38403
+ };
38404
+
38405
+ /*
38406
+ * The page on screen, kept for the next document: what its reads answered,
38407
+ * written into a signal the app hands over, and drawn again by the same reads
38408
+ * in a document opening on the same page (a reload, a tab the system
38409
+ * discarded). The read side is state/rest/kept_reads.js; this is what decides
38410
+ * which reads are the page, and when the slot is written.
38411
+ *
38412
+ * The unit is the page, not the resource. One resource is read by several
38413
+ * screens — a search, someone's games, a game opened from anywhere — and kept
38414
+ * per resource it would keep every row ever opened. The page's reads are what
38415
+ * navi already runs for it: the route actions asking something for the page
38416
+ * (activeRouteActionsSignal) and the compositions the lists on screen read
38417
+ * (shownRangeReadersSignal). A search typed on the page is neither, and is not
38418
+ * kept.
38419
+ *
38420
+ * A read is written once it has answered in this document. Before that it
38421
+ * keeps what the previous document kept for it, never what it shows meanwhile:
38422
+ * an answer handed over from other params (a list standing in for the next
38423
+ * one) says nothing about these.
38424
+ *
38425
+ * The write waits for the page to settle and is flushed when the document is
38426
+ * hidden. A navigation goes through pages half there — the route matched, its
38427
+ * list not mounted yet, the page left still drawn under a transition — and
38428
+ * each would otherwise be written out in full. Hidden is the moment the system
38429
+ * may discard the document, so nothing is lost by waiting until then.
38430
+ *
38431
+ * The signal turning `undefined` from outside (sign-out) forgets the page, and
38432
+ * nothing is written until a read lands again: written at once, the page still
38433
+ * on screen would put back the account just left.
38434
+ */
38435
+
38436
+ const WRITE_DELAY = 1000;
38437
+ const NOTHING_PENDING = {};
38438
+ let stopKeepingCurrent = null;
38439
+
38440
+ /**
38441
+ * Keeps what the page on screen reads, so that a new document opening on it —
38442
+ * a reload, a pull-to-refresh, a tab the system discarded in the background —
38443
+ * draws it again while its reads go out, rather than skeletons.
38444
+ *
38445
+ * What the page reads is what navi runs for it: the route actions asking
38446
+ * something for it (see `activeRouteActionsSignal`) whose action is a resource
38447
+ * `GET` or `GET_MANY`, and the `GET_RANGE` compositions the `<List.Items>` on
38448
+ * screen read, around the window they draw. A navigation replaces the slot with
38449
+ * the next page's reads, and the page left is dropped. In the next document,
38450
+ * the first run of the same read with the same params draws the kept answer as
38451
+ * its provisional value: `data` set while `loading` is `true`, the answer
38452
+ * replacing it. Under a network policy answering reads from the store, the
38453
+ * kept answer answers the read. The copy follows the store: a `PUT` on a row
38454
+ * rewrites it.
38455
+ *
38456
+ * Call it before the routes start: the first runs are the ones that look.
38457
+ *
38458
+ * @param {object} options
38459
+ * @param {import("@preact/signals").Signal} options.signal - where the slot is
38460
+ * written: a `stateSignal` with `persists: true` and `type: "object"`, its
38461
+ * `id` holding whatever makes a kept page unusable (the deployed version).
38462
+ * Writing `undefined` into it (sign-out) forgets the page, and nothing is
38463
+ * written again until a read lands.
38464
+ * @param {() => boolean} [options.when] - read whenever the slot is about to be
38465
+ * read or written: `false` reads nothing, writes nothing, and empties the
38466
+ * signal (a tab viewing as someone else). A function, so it can read a signal.
38467
+ * @param {object[]} [options.always] - routes whose reads stay kept once the
38468
+ * page is left, until it is visited again: the page an app opens on when it
38469
+ * is launched rather than reloaded.
38470
+ * @returns {() => void} stops keeping the page.
38471
+ * @see docs/resource.md — what is kept and what is not, what stays the app's
38472
+ */
38473
+ const keepPageOnScreen = ({
38474
+ signal: keptSignal,
38475
+ when,
38476
+ always = [],
38477
+ } = {}) => {
38478
+ if (!isSignal(keptSignal)) {
38479
+ throw new TypeError(
38480
+ `keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
38481
+ );
38482
+ }
38483
+ if (stopKeepingCurrent) {
38484
+ stopKeepingCurrent();
38485
+ }
38486
+ const isAllowed = () => {
38487
+ return when ? Boolean(when()) : true;
38488
+ };
38489
+ const alwaysMatchingSignal = anyMatchingRouteSignal(always);
38490
+
38491
+ // What the signal held when it was read: the previous document's page.
38492
+ let loadedEntryMap = new Map();
38493
+ let loadedAlwaysKeys = [];
38494
+ // Which reads answered in this document (since the last forget): only
38495
+ // those write what they show.
38496
+ let answeredActionWeakSet = new WeakSet();
38497
+ let answeredRangeKeySet = new Set();
38498
+ // The state each action was last seen in, and the last answer seen of each
38499
+ // range reader: a read lands when they move, and not because the slot looks
38500
+ // again at something that had answered before a forget.
38501
+ const stateSeenWeakMap = new WeakMap();
38502
+ const answeredAtSeenWeakMap = new WeakMap();
38503
+ // Forgotten: nothing is written until a read lands.
38504
+ let paused = false;
38505
+ // The reads of the always-kept page, as they were the last time it was on
38506
+ // screen in this document; null until then.
38507
+ let alwaysReads = null;
38508
+
38509
+ const forget = () => {
38510
+ loadedEntryMap = new Map();
38511
+ loadedAlwaysKeys = [];
38512
+ answeredActionWeakSet = new WeakSet();
38513
+ answeredRangeKeySet = new Set();
38514
+ alwaysReads = null;
38515
+ paused = true;
38516
+ withdrawKeptReads();
38517
+ };
38518
+
38519
+ let lastWritten;
38520
+ let pendingWrite = NOTHING_PENDING;
38521
+ let writeTimeout;
38522
+ const flush = () => {
38523
+ clearTimeout(writeTimeout);
38524
+ if (pendingWrite === NOTHING_PENDING) {
38525
+ return;
38526
+ }
38527
+ const value = pendingWrite;
38528
+ pendingWrite = NOTHING_PENDING;
38529
+ if (compareTwoJsValues(value, lastWritten)) {
38530
+ return;
38531
+ }
38532
+ lastWritten = value;
38533
+ keptSignal.value = value;
38534
+ };
38535
+ const scheduleWrite = (value) => {
38536
+ pendingWrite = value;
38537
+ clearTimeout(writeTimeout);
38538
+ writeTimeout = setTimeout(flush, WRITE_DELAY);
38539
+ };
38540
+ const onVisibilityChange = () => {
38541
+ if (document.visibilityState === "hidden") {
38542
+ flush();
38543
+ }
38544
+ };
38545
+ document.addEventListener("visibilitychange", onVisibilityChange);
38546
+ window.addEventListener("pagehide", flush);
38547
+
38548
+ // Runs at once with what the signal already holds (the slot read from
38549
+ // storage), then on every write — the slot's own ones are recognized and
38550
+ // left alone.
38551
+ const unsubscribe = keptSignal.subscribe((value) => {
38552
+ if (value === lastWritten) {
38553
+ return;
38554
+ }
38555
+ lastWritten = value;
38556
+ pendingWrite = NOTHING_PENDING;
38557
+ clearTimeout(writeTimeout);
38558
+ if (
38559
+ value === undefined ||
38560
+ value === null ||
38561
+ typeof value !== "object" ||
38562
+ !isAllowed()
38563
+ ) {
38564
+ forget();
38565
+ return;
38566
+ }
38567
+ const entries =
38568
+ value.entries && typeof value.entries === "object" ? value.entries : {};
38569
+ loadedEntryMap = new Map(Object.entries(entries));
38570
+ loadedAlwaysKeys = Array.isArray(value.always)
38571
+ ? value.always.filter((key) => loadedEntryMap.has(key))
38572
+ : [];
38573
+ alwaysReads = null;
38574
+ paused = false;
38575
+ offerKeptReads(loadedEntryMap);
38576
+ });
38577
+
38578
+ const actionReadWeakMap = new WeakMap();
38579
+ const actionReadOf = (action) => {
38580
+ let read = actionReadWeakMap.get(action);
38581
+ if (read) {
38582
+ return read;
38583
+ }
38584
+ read = () => {
38585
+ const state = action.runningStateSignal.value;
38586
+ const stateSeen = stateSeenWeakMap.get(action);
38587
+ stateSeenWeakMap.set(action, state);
38588
+ const landed = state === COMPLETED && stateSeen !== COMPLETED;
38589
+ if (landed) {
38590
+ answeredActionWeakSet.add(action);
38591
+ }
38592
+ return {
38593
+ key: getKeptReadKey(action.name, action.params),
38594
+ answered: answeredActionWeakSet.has(action),
38595
+ landed,
38596
+ readEntry: () => {
38597
+ const data = action.dataSignal.value;
38598
+ if (data === undefined || data === null) {
38599
+ return undefined;
38600
+ }
38601
+ return serializeData(data);
38602
+ },
38603
+ };
38604
+ };
38605
+ actionReadWeakMap.set(action, read);
38606
+ return read;
38607
+ };
38608
+ const rangeReadWeakMap = new WeakMap();
38609
+ const rangeReadOf = (reader) => {
38610
+ let read = rangeReadWeakMap.get(reader);
38611
+ if (read) {
38612
+ return read;
38613
+ }
38614
+ read = () => {
38615
+ const { key, answeredAt, readEntry } = reader.readKept();
38616
+ const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
38617
+ answeredAtSeenWeakMap.set(reader, answeredAt);
38618
+ const landed = answeredAt > answeredAtSeen;
38619
+ if (landed) {
38620
+ answeredRangeKeySet.add(key);
38621
+ }
38622
+ return {
38623
+ key,
38624
+ answered: answeredRangeKeySet.has(key),
38625
+ landed,
38626
+ readEntry,
38627
+ };
38628
+ };
38629
+ rangeReadWeakMap.set(reader, read);
38630
+ return read;
38631
+ };
38632
+ const readsOnScreen = () => {
38633
+ const reads = [];
38634
+ for (const routeAction of activeRouteActionsSignal.value) {
38635
+ // The instance is what is kept, so that a page left keeps what it read.
38636
+ // The binding's callSource names that instance and is rewritten once the
38637
+ // binding has moved to another one: read, it brings the slot along.
38638
+ // eslint-disable-next-line no-unused-expressions
38639
+ routeAction.callSource;
38640
+ const action = getActionPrivateProperties(routeAction).currentAction;
38641
+ if (action.params === NO_PARAMS || !isKeepableRead(action)) {
38642
+ continue;
38643
+ }
38644
+ reads.push(actionReadOf(action));
38645
+ }
38646
+ for (const reader of shownRangeReadersSignal.value.keys()) {
38647
+ reads.push(rangeReadOf(reader));
38648
+ }
38649
+ return reads;
38650
+ };
38651
+
38652
+ const stopEffect = effect(() => {
38653
+ const allowed = isAllowed();
38654
+ const routed = routedSignal.value;
38655
+ const alwaysMatching = alwaysMatchingSignal.value;
38656
+ if (!allowed) {
38657
+ forget();
38658
+ pendingWrite = undefined;
38659
+ flush();
38660
+ return;
38661
+ }
38662
+ if (!routed) {
38663
+ // No page yet: the slot is what the first reads will draw.
38664
+ return;
38665
+ }
38666
+ const screenReads = readsOnScreen();
38667
+ if (alwaysMatching) {
38668
+ alwaysReads = screenReads;
38669
+ }
38670
+ const entries = {};
38671
+ let landed = false;
38672
+ const collect = (reads) => {
38673
+ const keys = [];
38674
+ for (const read of reads) {
38675
+ const result = read();
38676
+ if (result.landed) {
38677
+ landed = true;
38678
+ }
38679
+ const entry = result.answered
38680
+ ? result.readEntry()
38681
+ : loadedEntryMap.get(result.key);
38682
+ if (entry === undefined) {
38683
+ continue;
38684
+ }
38685
+ entries[result.key] = entry;
38686
+ keys.push(result.key);
38687
+ }
38688
+ return keys;
38689
+ };
38690
+ const screenKeys = collect(screenReads);
38691
+ let alwaysKeys;
38692
+ if (alwaysReads === screenReads) {
38693
+ alwaysKeys = screenKeys;
38694
+ } else if (alwaysReads) {
38695
+ alwaysKeys = collect(alwaysReads);
38696
+ } else {
38697
+ alwaysKeys = loadedAlwaysKeys;
38698
+ for (const key of alwaysKeys) {
38699
+ entries[key] = loadedEntryMap.get(key);
38700
+ }
38701
+ }
38702
+ if (paused) {
38703
+ if (!landed) {
38704
+ return;
38705
+ }
38706
+ paused = false;
38707
+ }
38708
+ if (screenKeys.length === 0 && alwaysKeys.length === 0) {
38709
+ scheduleWrite(undefined);
38710
+ return;
38711
+ }
38712
+ scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
38713
+ });
38714
+
38715
+ const stop = () => {
38716
+ flush();
38717
+ stopEffect();
38718
+ unsubscribe();
38719
+ document.removeEventListener("visibilitychange", onVisibilityChange);
38720
+ window.removeEventListener("pagehide", flush);
38721
+ withdrawKeptReads();
38722
+ if (stopKeepingCurrent === stop) {
38723
+ stopKeepingCurrent = null;
38724
+ }
38725
+ };
38726
+ stopKeepingCurrent = stop;
38727
+ return stop;
38728
+ };
38729
+
38039
38730
  /**
38040
38731
  * A popup that is the anchor, brought to the front.
38041
38732
  *
@@ -65108,7 +65799,7 @@ const css$E = /* css */`
65108
65799
  /* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
65109
65800
  by default until .show()/.showModal() adds [open]) — needs its own
65110
65801
  starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
65111
- own backdropProps (recomputed from openController.opened on every
65802
+ own backdropProps (recomputed from openController.openedInDom on every
65112
65803
  render, present from the very first one), then toggled by plain
65113
65804
  removeAttribute/setAttribute in openEffect/close, never an explicit
65114
65805
  display override — removing the attribute just lets this rule stop
@@ -66709,7 +67400,7 @@ const useDialogProps = props => {
66709
67400
  "navi-out-of-flow": "",
66710
67401
  "baseClassName": "navi_dialog_backdrop",
66711
67402
  "aria-hidden": "true",
66712
- // Recomputed fresh on every render from openController.opened (not
67403
+ // Recomputed fresh on every render from openController.openedInDom (not
66713
67404
  // driven through a mount-time layout effect, unlike this file's own
66714
67405
  // imperative open/close toggling below) — present in the DOM
66715
67406
  // synchronously from the very first commit, matching this file's own
@@ -66719,7 +67410,7 @@ const useDialogProps = props => {
66719
67410
  // needs — see popover.jsx's own identical prop for the full reasoning.
66720
67411
  "aria-expanded": openController.openedInDom ? "true" : "false",
66721
67412
  // Present from this very first render (recomputed fresh on every one
66722
- // from openController.opened, not a frozen mount-time constant) so
67413
+ // from openController.openedInDom, not a frozen mount-time constant) so
66723
67414
  // there's no gap for the browser to ever paint this plain-div backdrop
66724
67415
  // visible before anything has actually opened it — see popover.jsx's
66725
67416
  // own identical prop for the full reasoning, and this file's own CSS
@@ -66739,7 +67430,7 @@ const useDialogProps = props => {
66739
67430
  // (kept once, not repeated here).
66740
67431
  "aria-expanded": openController.openedInDom ? "true" : "false",
66741
67432
  // Present from the very first render (recomputed fresh from
66742
- // openController.opened every time, not a frozen mount-time constant —
67433
+ // openController.openedInDom every time, not a frozen mount-time constant —
66743
67434
  // see popover.jsx's own identical prop for the full reasoning) so a
66744
67435
  // consumer whose own CSS also sets display (e.g. Popup's flex prop)
66745
67436
  // can't silently defeat showModal()/close()'s native open/close — see
@@ -67115,7 +67806,7 @@ const css$D = /* css */`
67115
67806
  without this it would flash visible for one frame on mount, before
67116
67807
  openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
67117
67808
  set from usePopoverProps' own contentProps (recomputed from
67118
- openController.opened on every render, present from the very first
67809
+ openController.openedInDom on every render, present from the very first
67119
67810
  one — see there for why), then toggled by plain
67120
67811
  removeAttribute/setAttribute in openEffect/close, never an explicit
67121
67812
  display override: removing the attribute just lets this rule stop
@@ -68471,7 +69162,7 @@ const usePopoverProps = props => {
68471
69162
  "navi-out-of-flow": "",
68472
69163
  "baseClassName": "navi_popover_backdrop",
68473
69164
  "aria-hidden": "true",
68474
- // Recomputed fresh on every render from openController.opened (not a
69165
+ // Recomputed fresh on every render from openController.openedInDom (not a
68475
69166
  // frozen mount-time constant, same pattern as navi-hidden just below)
68476
69167
  // rather than driven through a mount-time layout effect — its own
68477
69168
  // actual open/close toggling is still done entirely imperatively
@@ -68546,7 +69237,7 @@ const usePopoverProps = props => {
68546
69237
  // starting-hidden default) — present from this very first render so
68547
69238
  // there's no gap for the browser to ever paint it visible before
68548
69239
  // anything has actually opened it. Recomputed fresh on every render from
68549
- // openController.opened (not a frozen mount-time constant) — Preact
69240
+ // openController.openedInDom (not a frozen mount-time constant) — Preact
68550
69241
  // only touches the DOM for a prop whose value actually changed since
68551
69242
  // the last render, so as long as this always reflects the *current*
68552
69243
  // truth, it never fights the imperative
@@ -72972,7 +73663,6 @@ const ListUI = props => {
72972
73663
  // What the runs ask for and stand for, in items: a source paginates in items,
72973
73664
  // and asks for the first picture's items and the rest in one round trip.
72974
73665
  listItems.pageSize = PAGE_SIZE_DEFAULT;
72975
- listItems.scrolled = scrolled ?? defaultScrolled;
72976
73666
  const {
72977
73667
  virtualItemSizeSignal,
72978
73668
  renderWindow,
@@ -73312,6 +74002,45 @@ const useListScrollSync = ({
73312
74002
  columns
73313
74003
  }) => {
73314
74004
  const debugScroll = useDebugScroll();
74005
+ // Where the list must be: what the caller holds it at (`scrolled`), or where
74006
+ // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
74007
+ // the caller moving the list, so the hold is armed again — that is what makes
74008
+ // it controlled. A `defaultScrolled` that changes is where the list opens,
74009
+ // read again from something that answered again (a count fresher than the
74010
+ // one kept from the previous visit): followed for as long as nobody has
74011
+ // moved the list, and compared by value, since it is said at every render.
74012
+ // Nothing to hold it at (a position not saved yet) is not a position: the
74013
+ // list opens where it opens.
74014
+ const startPlaceRef = useRef(null);
74015
+ if (startPlaceRef.current === null) {
74016
+ startPlaceRef.current = {
74017
+ userTookOver: false,
74018
+ wanted: scrolled ?? defaultScrolled,
74019
+ scrolled,
74020
+ defaultScrolled,
74021
+ way: null,
74022
+ arrived: false
74023
+ };
74024
+ } else {
74025
+ const place = startPlaceRef.current;
74026
+ if (place.scrolled !== scrolled) {
74027
+ place.scrolled = scrolled;
74028
+ place.defaultScrolled = defaultScrolled;
74029
+ place.wanted = scrolled ?? defaultScrolled;
74030
+ place.userTookOver = false;
74031
+ } else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
74032
+ place.defaultScrolled = defaultScrolled;
74033
+ if (!place.userTookOver) {
74034
+ place.wanted = defaultScrolled;
74035
+ // Placed again from where it stands: the way to the previous place
74036
+ // says nothing about whose the next scrolls are.
74037
+ place.way = null;
74038
+ place.arrived = false;
74039
+ }
74040
+ }
74041
+ }
74042
+ const scrolledWanted = startPlaceRef.current.wanted;
74043
+ listItems.scrolled = scrolledWanted;
73315
74044
  // The items drawn, [start, end) among the list's own. A ref as well as a
73316
74045
  // state: the render moves it where the list is held (holdWindow) without a
73317
74046
  // commit of its own.
@@ -73326,7 +74055,7 @@ const useListScrollSync = ({
73326
74055
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
73327
74056
  inLines: Boolean(columns),
73328
74057
  windowLeavesItemsOut,
73329
- scrolledWanted: scrolled ?? defaultScrolled,
74058
+ scrolledWanted,
73330
74059
  // The fillers hold the room of items above the screen at this size: a
73331
74060
  // size that changes moves what is on screen, like items landing above it.
73332
74061
  beforeSizeChange: () => listItems.captureAnchor()
@@ -73657,25 +74386,7 @@ const useListScrollSync = ({
73657
74386
  const newEnd = newStart + windowSize;
73658
74387
  updateRenderWindow(newStart, newEnd);
73659
74388
  };
73660
-
73661
- // Where the list must be: what the caller holds it at (`scrolled`), or where
73662
- // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
73663
- // the caller moving the list, so the hold is armed again — that is what makes
73664
- // it controlled.
73665
- // Nothing to hold it at (a position not saved yet) is not a position: the
73666
- // list opens where it opens.
73667
- const scrolledWanted = scrolled ?? defaultScrolled;
73668
74389
  const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
73669
- const startPlaceRef = useRef({
73670
- userTookOver: false,
73671
- wanted: scrolledWanted,
73672
- way: null,
73673
- arrived: false
73674
- });
73675
- if (startPlaceRef.current.wanted !== scrolledWanted) {
73676
- startPlaceRef.current.wanted = scrolledWanted;
73677
- startPlaceRef.current.userTookOver = false;
73678
- }
73679
74390
  // Only one thing owns the scroll at a time: while the list is holding itself
73680
74391
  // somewhere, the anchoring stays out of it (holding an item still is precisely
73681
74392
  // not being at the end anymore once what is above it shrinks).
@@ -77395,6 +78106,17 @@ const useItemStore = ({
77395
78106
  }
77396
78107
  const pages = pagesRef.current;
77397
78108
  const [, setPageVersion] = useState(0);
78109
+ // A list on screen is part of what the page reads: the composition it reads
78110
+ // is kept with the page for the next document (see keepPageOnScreen).
78111
+ useLayoutEffect(() => {
78112
+ if (inMemory || typeof itemsAction !== "function") {
78113
+ return undefined;
78114
+ }
78115
+ if (!itemsAction.isRangeReader) {
78116
+ return undefined;
78117
+ }
78118
+ return showRangeReader(itemsAction);
78119
+ }, [inMemory, itemsAction]);
77398
78120
  // The ranks whose item is from before: the run came back to the screen, or
77399
78121
  // the source said the collection moved. They stay drawn until a page confirms
77400
78122
  // or replaces them, rank by rank. A page covers what the window framed when it
@@ -77528,6 +78250,7 @@ const useItemStore = ({
77528
78250
  }
77529
78251
  }
77530
78252
  if (typeof itemsAction === "function" && itemsAction.trimComposition) {
78253
+ itemsAction.frameComposition(windowFrom, windowTo);
77531
78254
  itemsAction.trimComposition(keepFrom, keepTo, budget);
77532
78255
  }
77533
78256
  },
@@ -78097,6 +78820,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
78097
78820
  * room has to be restated as a number by whoever asks. The `index` and
78098
78821
  * `visibleCount` it also hands out say where to aim before the item is found,
78099
78822
  * and how many items to draw before the first paint (see `renderBudget`).
78823
+ * A value that changes before anyone has moved the list is followed (a place
78824
+ * read from a count that answered again): it is compared by value, so it can
78825
+ * be computed at every render.
78100
78826
  * @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
78101
78827
  * The same, but held: the list goes back there every time this changes, even
78102
78828
  * after the user has scrolled — the caller owns where the list is (see
@@ -80965,6 +81691,8 @@ const TRAVELS_NONE = [];
80965
81691
  * name?: string,
80966
81692
  * min?: any,
80967
81693
  * max?: any,
81694
+ * minMessage?: any,
81695
+ * maxMessage?: any,
80968
81696
  * step?: number,
80969
81697
  * type?: string,
80970
81698
  * editable?: boolean,
@@ -81003,7 +81731,15 @@ const TRAVELS_NONE = [];
81003
81731
  * @param {number} [step=1] How many steps a press covers.
81004
81732
  * @param {number} [duration=250] How long a travel takes, in milliseconds.
81005
81733
  * @param {any} [min] The first value one can reach; `max` is the last. Beyond
81006
- * them the travel simply does not happen and the chevron that way says so.
81734
+ * them the travel simply does not happen and the chevron that way says so,
81735
+ * in the words the control in the middle uses for a value past that end ("La
81736
+ * date doit être au plus tard le 2 avril 2027", "Max 5"). A value the control
81737
+ * has no such sentence for — what a plain `Spin` steps through — is "nothing
81738
+ * after this one".
81739
+ * @param {any} [maxMessage] What is said at `max` instead: by the chevron
81740
+ * refused there, by a travel asked past it, and by the control in the middle
81741
+ * for a value past it (picked in the calendar, typed). `minMessage` is its
81742
+ * twin at `min`.
81007
81743
  * @param {string} [padding] The room around the value, `paddingX`/`paddingY`
81008
81744
  * and the four sides included. It goes on what is inside the box rather than
81009
81745
  * on the box: above and below it is taken by the value AND by the two
@@ -81033,6 +81769,8 @@ const Spin = ({
81033
81769
  name,
81034
81770
  min,
81035
81771
  max,
81772
+ minMessage,
81773
+ maxMessage,
81036
81774
  step = 1,
81037
81775
  duration = 250,
81038
81776
  type = "text",
@@ -81158,15 +81896,31 @@ const Spin = ({
81158
81896
  const readOnlyResolved = Boolean(readOnly || readOnlyFromAbove || loadingResolved);
81159
81897
  const disabledResolved = Boolean(disabled || disabledFromAbove);
81160
81898
 
81899
+ // The end of what one may reach, said the way the control in the middle
81900
+ // says it about a value past that end — its `minMessage`/`maxMessage`, or the
81901
+ // sentence its type has for the bound — so a chevron, the calendar and a
81902
+ // typed value refuse in the same words. Asked of the control at the press:
81903
+ // it is not there yet on the first render. `goingNext` is towards the bigger
81904
+ // values, whichever end of the box that is.
81905
+ const boundMessage = goingNext => {
81906
+ const constraint = goingNext ? MAX_CONSTRAINT : MIN_CONSTRAINT;
81907
+ const controlEl = controlRef.current;
81908
+ const controller = (findControlHost(controlEl) || controlEl).__uiStateController__;
81909
+ const {
81910
+ message
81911
+ } = getConstraintMessage(controller, constraint, constraint.messageAtBound(controller), {});
81912
+ return message ?? naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before");
81913
+ };
81914
+
81161
81915
  // Why a chevron refuses, in its own words — and only when the reason is
81162
81916
  // ours: the end of what one may reach is something this control knows and
81163
81917
  // navi cannot guess. For everything else the reason belongs to the state the
81164
81918
  // whole control is in, and it is said about the control ("read-only",
81165
81919
  // "busy") rather than about the button, which is not what the user was
81166
81920
  // pressing.
81167
- const wayOutMessage = (allowed, endKey) => {
81921
+ const wayOutMessage = (allowed, isNext) => {
81168
81922
  if (!allowed) {
81169
- return naviI18n(endKey);
81923
+ return boundMessage(isNext);
81170
81924
  }
81171
81925
  if (loadingResolved) {
81172
81926
  return naviI18n("constraint.busy.default");
@@ -81278,7 +82032,7 @@ const Spin = ({
81278
82032
  // answer belongs.
81279
82033
  e.preventDefault();
81280
82034
  e.stopPropagation();
81281
- openCallout(naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before"), {
82035
+ openCallout(boundMessage(goingNext), {
81282
82036
  anchorElement: middleRef.current,
81283
82037
  status: "info",
81284
82038
  openingEvent: e.detail.event ?? e
@@ -81337,12 +82091,14 @@ const Spin = ({
81337
82091
  const valueDrawnAtEnd = travelPending?.area === "end" ? travelPending.value : valueAtEnd;
81338
82092
  const wayOut = atStart => {
81339
82093
  const isNext = atStart ? startIsNext : !startIsNext;
82094
+ const allowed = atStart ? startAllowed : endAllowed;
81340
82095
  return jsx(WayOut, {
81341
82096
  atStart: atStart,
81342
82097
  onPointerDown: e => {
81343
82098
  wayOutPointerTypeRef.current = e.pointerType;
81344
82099
  },
81345
- unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
82100
+ unavailable: !allowed || loadingResolved || readOnlyResolved,
82101
+ unavailableMessage: () => wayOutMessage(allowed, isNext),
81346
82102
  label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
81347
82103
  onPress: e => {
81348
82104
  focusMiddle();
@@ -81389,6 +82145,8 @@ const Spin = ({
81389
82145
  ...valueProps,
81390
82146
  min: min,
81391
82147
  max: max,
82148
+ minMessage: minMessage,
82149
+ maxMessage: maxMessage,
81392
82150
  step: step,
81393
82151
  readOnly: readOnly,
81394
82152
  disabled: disabled,
@@ -81422,6 +82180,8 @@ const Spin = ({
81422
82180
  ...valueProps,
81423
82181
  min: min,
81424
82182
  max: max,
82183
+ minMessage: minMessage,
82184
+ maxMessage: maxMessage,
81425
82185
  readOnly: readOnly,
81426
82186
  disabled: disabled,
81427
82187
  loading: loading,
@@ -81532,6 +82292,7 @@ const Spin = ({
81532
82292
  const WayOut = ({
81533
82293
  atStart,
81534
82294
  commandFor,
82295
+ unavailable,
81535
82296
  unavailableMessage,
81536
82297
  label,
81537
82298
  onPress,
@@ -81542,10 +82303,10 @@ const WayOut = ({
81542
82303
  // onPointerUp below).
81543
82304
  const pointerDownRef = useRef(null);
81544
82305
  const press = e => {
81545
- if (unavailableMessage) {
82306
+ if (unavailable) {
81546
82307
  // Why it does nothing, said where one pressed: a control would have
81547
82308
  // done this through its own interaction gate, and this one has none.
81548
- openCallout(unavailableMessage, {
82309
+ openCallout(unavailableMessage(), {
81549
82310
  anchorElement: e.currentTarget,
81550
82311
  status: "info",
81551
82312
  openingEvent: e
@@ -81572,8 +82333,8 @@ const WayOut = ({
81572
82333
  ,
81573
82334
  role: "button",
81574
82335
  "aria-label": label,
81575
- "aria-disabled": unavailableMessage ? "true" : undefined,
81576
- "data-unavailable": unavailableMessage ? "" : undefined
82336
+ "aria-disabled": unavailable ? "true" : undefined,
82337
+ "data-unavailable": unavailable ? "" : undefined
81577
82338
  // At the chevron, not beside it: a callout aims its arrow at where the
81578
82339
  // anchor's text starts, and there is no text here — only a glyph in the
81579
82340
  // middle of the box, which is what one pressed and what the answer is
@@ -81806,6 +82567,8 @@ SpinGroup.Separator = SpinGroupSeparator;
81806
82567
  * defaultValue?: number|string,
81807
82568
  * min?: number,
81808
82569
  * max?: number,
82570
+ * minMessage?: any,
82571
+ * maxMessage?: any,
81809
82572
  * step?: number,
81810
82573
  * pad?: number,
81811
82574
  * loop?: boolean,
@@ -81911,6 +82674,8 @@ const numberAtStep = (value, count, {
81911
82674
  * name?: string,
81912
82675
  * min?: string,
81913
82676
  * max?: string,
82677
+ * minMessage?: any,
82678
+ * maxMessage?: any,
81914
82679
  * step?: number,
81915
82680
  * lang?: string,
81916
82681
  * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
@@ -93478,5 +94243,5 @@ const UserSvg = () => jsx("svg", {
93478
94243
  })
93479
94244
  });
93480
94245
 
93481
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
94246
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
93482
94247
  //# sourceMappingURL=jsenv_navi.js.map