@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, useEffect, useReducer, useCallback, useId } from "preact/hooks";
11
11
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, 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
 
@@ -971,8 +971,10 @@ naviI18n.addAll({
971
971
  },
972
972
  });
973
973
 
974
- // Spin messages — the ends of what one steps through, said without naming
975
- // what it is made of: the same words fit days, months, pages or sizes.
974
+ // Spin messages — the ends of what one steps through when the control holding
975
+ // it has no sentence of its own for a bound (a day or a number says its
976
+ // constraint.min/max one, see picker_spin.jsx): said without naming what it is
977
+ // made of, so they fit pages, sizes or any list of items.
976
978
  naviI18n.addAll({
977
979
  "spin.previous": {
978
980
  en: "Previous",
@@ -3694,8 +3696,11 @@ const actionWeakMap = new WeakMap();
3694
3696
  * than a first load (data_states.md). Called at the start of every run whose
3695
3697
  * value is undefined; the answer replaces it as usual, and `undefined` means
3696
3698
  * "nothing known". A resource `GET` uses it to draw the row its store holds.
3697
- * An action declaring it is asked for it by a binding moving to new params,
3698
- * rather than handed the previous params' answer (see inheritData).
3699
+ * @param {boolean} [rootOptions.inheritData=true] - `false`: a binding moving
3700
+ * to new params starts the new instance without the previous params' answer
3701
+ * (see createActionProxyFromSignal). For an answer that would say something
3702
+ * false about the new params (a `GET` drawing another row), not for one that
3703
+ * stands in while the next one comes (a list of search results).
3699
3704
  * @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
3700
3705
  * answer stays good once it has landed. Without it an answer lives exactly as
3701
3706
  * long as something references it: the screen that asked goes away, and
@@ -3738,6 +3743,7 @@ const createAction = (callback, rootOptions = {}) => {
3738
3743
  resultToValue,
3739
3744
  valueToData,
3740
3745
  provisionalValue,
3746
+ inheritData = true,
3741
3747
  dataDefault,
3742
3748
  data = dataDefault,
3743
3749
 
@@ -4414,6 +4420,7 @@ const createAction = (callback, rootOptions = {}) => {
4414
4420
  const privateProperties = {
4415
4421
  valueInitial,
4416
4422
  provisionalValue,
4423
+ inheritData,
4417
4424
 
4418
4425
  performRun,
4419
4426
  performReset,
@@ -4446,7 +4453,7 @@ const createAction = (callback, rootOptions = {}) => {
4446
4453
  * a list of filters changes, providing real-time results without user interaction.
4447
4454
  * @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
4448
4455
  * By default (true), the proxy carries over the previous target's value and error into the new action —
4449
- * unless the action declares a `provisionalValue`, which then says what the new one holds.
4456
+ * unless the action itself declares `inheritData: false` (createAction), which a resource `GET` does.
4450
4457
  * This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
4451
4458
  * new action completes, not when it starts loading. Code that needs to distinguish loading state can still
4452
4459
  * check `action.runningState`, while code that just reads `action.data` always sees the most recent
@@ -4493,12 +4500,12 @@ const createActionProxyFromSignal = (
4493
4500
  let currentActionPrivateProperties = getActionPrivateProperties(action);
4494
4501
  let actionTargetPreviousWeakRef = null;
4495
4502
 
4496
- // An action saying for itself what it holds for params it has not answered
4497
- // (provisionalValue: a resource GET draws the row its store holds) is not
4498
- // handed the previous params' answer — an answer about another row. What it
4499
- // does not know stays unknown: a skeleton, never a different row.
4503
+ // An action whose answer for other params would be false for these (a
4504
+ // resource GET: another row) says so itself, and is not handed it. What it
4505
+ // does not know stays unknown: a skeleton, or its provisionalValue, never a
4506
+ // different row.
4500
4507
  const inheritsAnswer =
4501
- inheritData && !getActionPrivateProperties(action).provisionalValue;
4508
+ inheritData && getActionPrivateProperties(action).inheritData;
4502
4509
  const createTarget = (params) => {
4503
4510
  if (inheritsAnswer) {
4504
4511
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
@@ -10649,6 +10656,9 @@ const paramsTargetCanPlace = (redirectRoute, urlParams) => {
10649
10656
 
10650
10657
  let setupRoutesCalled = false;
10651
10658
  let activeRouteSet = null;
10659
+ // True once the routes have been matched against an address. Before that, no
10660
+ // route matching says nothing about the page: nothing has been asked yet.
10661
+ const routedSignal = signal(false);
10652
10662
 
10653
10663
  const routePreloadMap = new WeakMap();
10654
10664
  const registerRoutePreload = (route, preload) => {
@@ -11073,6 +11083,7 @@ This prevents cross-test pollution and ensures clean state.`,
11073
11083
  }
11074
11084
  // Reset flag after URL -> Signal synchronization is complete
11075
11085
  isUpdatingRoutesFromUrl = false;
11086
+ routedSignal.value = true;
11076
11087
  Object.assign(returnValue, { matchingRouteSet });
11077
11088
  for (const [route, repairedParams] of repairedParamsByRoute) {
11078
11089
  route.replaceParams(repairedParams, {
@@ -11122,6 +11133,7 @@ This prevents cross-test pollution and ensures clean state.`,
11122
11133
  droppingRouteSet = null;
11123
11134
  setupRoutesCalled = false;
11124
11135
  activeRouteSet = null;
11136
+ routedSignal.value = false;
11125
11137
  };
11126
11138
  return {
11127
11139
  updateRoutes,
@@ -11207,6 +11219,42 @@ const urlWithoutSearchAndHash = (url) => {
11207
11219
  return `${urlObject.origin}${urlObject.pathname}`;
11208
11220
  };
11209
11221
 
11222
+ /**
11223
+ * What a component can learn about the page it is rendered in, from the route
11224
+ * container rendering that page (see useContentKeepingLeavingPages in
11225
+ * route.jsx). `null` outside of any route container.
11226
+ *
11227
+ * - `isShown()` — whether the container would still show the page. False as
11228
+ * soon as the address leads elsewhere: the page is about to be taken down, or
11229
+ * hidden until the route transition leaving it is over, and nothing it reads
11230
+ * is worth a render any more. False for good once it is kept while leaving: a
11231
+ * return to its route mounts the page anew. Asked outside any render — by a
11232
+ * subscription deciding whether to re-render — and it reads the routes
11233
+ * untracked, so asking subscribes nobody to them.
11234
+ * - `leavingSignal` — true once the page is hidden and kept while its movement
11235
+ * plays: still mounted, so what it fills outside its own nodes (a slot in a
11236
+ * bar) steps out rather than showing with the page arriving.
11237
+ */
11238
+ const RoutePageContext = createContext(null);
11239
+
11240
+ /**
11241
+ * A container has put its page on screen — or as much of it as it can.
11242
+ *
11243
+ * A route matching is a signal changing, and the page it selects reaches the
11244
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
11245
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
11246
+ * as it has been decided (a travel about to have its picture taken by the
11247
+ * browser, see route_travel.jsx) waits for this instead of counting.
11248
+ *
11249
+ * A page waiting on data is announced too, by the boundary showing its loading
11250
+ * state (see Loading in use_async_data.jsx): what the container could put on
11251
+ * screen is what the browser is about to take a picture of, and a page that
11252
+ * cannot render yet would otherwise be waited on until the transition dies of
11253
+ * it. It lives in a module of its own for that: the async layer says it as much
11254
+ * as the router does, and neither can import the other.
11255
+ */
11256
+ const [publishRouteRender, observeRouteRender] = createPubSub();
11257
+
11210
11258
  const documentStateSignal = signal(null);
11211
11259
  const useDocumentState = () => {
11212
11260
  return documentStateSignal.value;
@@ -11466,24 +11514,6 @@ const readTopInWindow = element => {
11466
11514
  return element.getBoundingClientRect().top;
11467
11515
  };
11468
11516
 
11469
- /**
11470
- * A container has put its page on screen — or as much of it as it can.
11471
- *
11472
- * A route matching is a signal changing, and the page it selects reaches the
11473
- * DOM only once Preact has rendered — an unknown number of passes later, in an
11474
- * unknown number of microtasks. Anyone who needs the page as it IS rather than
11475
- * as it has been decided (a travel about to have its picture taken by the
11476
- * browser, see route_travel.jsx) waits for this instead of counting.
11477
- *
11478
- * A page waiting on data is announced too, by the boundary showing its loading
11479
- * state (see Loading in use_async_data.jsx): what the container could put on
11480
- * screen is what the browser is about to take a picture of, and a page that
11481
- * cannot render yet would otherwise be waited on until the transition dies of
11482
- * it. It lives in a module of its own for that: the async layer says it as much
11483
- * as the router does, and neither can import the other.
11484
- */
11485
- const [publishRouteRender, observeRouteRender] = createPubSub();
11486
-
11487
11517
  /**
11488
11518
  * Where a page was left, given back when one comes back to it.
11489
11519
  *
@@ -13011,6 +13041,7 @@ const warnAboutOrphanGeneratedKeys = () => {
13011
13041
 
13012
13042
  const NO_OP = () => {};
13013
13043
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
13044
+ const countRenders$1 = (count) => count + 1;
13014
13045
  // What the computed below answers for a key the document state does not hold:
13015
13046
  // key presence is the flag, and the value under a present key may be anything,
13016
13047
  // undefined included.
@@ -13039,7 +13070,36 @@ const useNavStateBasic = (
13039
13070
  [id],
13040
13071
  );
13041
13072
  const keyValue = keyValueComputed.value;
13042
- const keyInState = keyValue !== KEY_NOT_IN_STATE;
13073
+ // A page the address has left keeps reading the entry it was shown on until
13074
+ // it is taken down (kept hidden while a route transition plays): the new
13075
+ // entry would close what was open there — a Picker as a cancel — where the
13076
+ // same page taken down at once is only unmounted. Its render still happens:
13077
+ // the state is written before the routes say the page is left.
13078
+ const routePage = useContext(RoutePageContext);
13079
+ const keyValueReadRef = useRef(keyValue);
13080
+ const keyValueWithheld =
13081
+ keyValue !== keyValueReadRef.current &&
13082
+ routePage !== null &&
13083
+ !routePage.isShown();
13084
+ if (!keyValueWithheld) {
13085
+ keyValueReadRef.current = keyValue;
13086
+ }
13087
+ const keyValueRead = keyValueReadRef.current;
13088
+ // Owed rather than dropped, as in useAsyncData: the address can come back
13089
+ // before the container renders (a redirect straight back), and the page then
13090
+ // stays on screen.
13091
+ const [, rerender] = useReducer(countRenders$1, 0);
13092
+ useEffect(() => {
13093
+ if (!keyValueWithheld) {
13094
+ return undefined;
13095
+ }
13096
+ return observeRouteRender(() => {
13097
+ if (routePage.isShown()) {
13098
+ rerender();
13099
+ }
13100
+ });
13101
+ }, [keyValueWithheld]);
13102
+ const keyInState = keyValueRead !== KEY_NOT_IN_STATE;
13043
13103
  const onLeaveRef = useRef(onLeave);
13044
13104
  onLeaveRef.current = onLeave;
13045
13105
  const prevKeyInStateRef = useRef(keyInState);
@@ -13074,7 +13134,7 @@ const useNavStateBasic = (
13074
13134
  effectiveType = "replace";
13075
13135
  }
13076
13136
 
13077
- const currentValue = keyInState ? keyValue : defaultValue;
13137
+ const currentValue = keyInState ? keyValueRead : defaultValue;
13078
13138
 
13079
13139
  if (debug) {
13080
13140
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -13142,6 +13202,8 @@ const useNavStateBasic = (
13142
13202
  /**
13143
13203
  * Stores a named value in the browser's document state and returns it reactively.
13144
13204
  * The component re-renders whenever the value changes (navigation, back/forward button).
13205
+ * In a page the address has left — kept hidden while a route transition plays —
13206
+ * the value stays the one of the entry that page was shown on.
13145
13207
  *
13146
13208
  * @param {string} id
13147
13209
  * Unique key used to store the value in document state. Must be stable across renders.
@@ -15576,6 +15638,13 @@ const createOpenController = (
15576
15638
  change();
15577
15639
  };
15578
15640
  const runChange = (change, { opened, event }) => {
15641
+ // What the controller answers about itself is the decision, written before
15642
+ // the change runs, whatever runs it. Whoever asks meanwhile reads it: the
15643
+ // render an opening forces (onOpen, mountContent) reads the open state
15644
+ // the caller holds, and an onOpen writing the popup's own `signal` has
15645
+ // useOpenPropsEffectOnOpenController asking for this very opening — a
15646
+ // second one, nested in the first, if `opened` still said closed.
15647
+ controller.opened = opened;
15579
15648
  const applyChange = () => {
15580
15649
  // Recorded with the change itself: what the DOM shows is what a render
15581
15650
  // landing between the ask and the picture must draw (see `openedInDom`).
@@ -15591,12 +15660,8 @@ const createOpenController = (
15591
15660
  applyChange();
15592
15661
  return;
15593
15662
  }
15594
- // What the controller answers about itself does not wait for the picture:
15595
- // whoever just asked reads `opened` on the spot (see
15596
- // useOpenPropsEffectOnOpenController, which writes it back into the
15597
- // caller's own signal), and a request arriving before the change lands
15598
- // runs it first rather than reading a DOM that disagrees.
15599
- controller.opened = opened;
15663
+ // A request arriving before the change lands runs it first rather than
15664
+ // reading a DOM that disagrees.
15600
15665
  changeAwaitingTransition = applyChange;
15601
15666
  transitionChange(
15602
15667
  () => {
@@ -15609,7 +15674,6 @@ const createOpenController = (
15609
15674
  };
15610
15675
 
15611
15676
  const performClose = (closeEvent) => {
15612
- controller.opened = false;
15613
15677
  // Read before any close effect touches the DOM: closing a native <dialog>
15614
15678
  // hands the focus back to whatever held it at showModal() time, so by the
15615
15679
  // time the close cleanup runs, the popup's content has already lost the
@@ -15877,7 +15941,6 @@ const createOpenController = (
15877
15941
  // told it opened right after (see popup_content_mount.js and
15878
15942
  // use_displayed_layout_effect.js).
15879
15943
  controller.openedInDom = true;
15880
- controller.opened = true;
15881
15944
  // Which press it opened during, so the release of that press is not
15882
15945
  // read as somebody dismissing it (see openedDuringThisPress).
15883
15946
  controller.pressCountAtOpen = pressCount;
@@ -16322,8 +16385,10 @@ const useOpenPropsEffectOnOpenController = (
16322
16385
  return undefined;
16323
16386
  }
16324
16387
  // Skip when the controller is already in the desired state.
16325
- // openController.opened tracks actual open/close (updated by onopen/onclose,
16326
- // not by renders) so it is the authoritative check against feedback loops.
16388
+ // openController.opened is what the controller decided, written before an
16389
+ // opening renders anything (see runChange), so it is the authoritative
16390
+ // check against feedback loops — the popup's own onOpen writing its
16391
+ // `signal` included.
16327
16392
  if (open === openController.opened) {
16328
16393
  return undefined;
16329
16394
  }
@@ -20293,18 +20358,17 @@ const MIN_CONSTRAINT = {
20293
20358
  if (cmp === null || cmp >= 0) {
20294
20359
  return null;
20295
20360
  }
20296
-
20297
- return naviI18n("constraint.min.duration.default", {
20298
- min: formatDuration(min),
20299
- });
20361
+ return boundMessage(field, "min");
20300
20362
  }
20301
20363
  if (field.controlType !== "input" && field.controlType !== "picker") {
20302
20364
  return null;
20303
20365
  }
20304
- const minString = field.controlHostProps.min;
20305
- if (!minString) {
20366
+ const min = field.controlHostProps.min;
20367
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
20368
+ if (min === undefined || min === null || min === "") {
20306
20369
  return null;
20307
20370
  }
20371
+ const minString = String(min);
20308
20372
  const type = field.controlHostProps.type;
20309
20373
  const naviInputType = field.controlHostProps["navi-input-type"];
20310
20374
  const valueAsString = uiStateAsText(field.uiState);
@@ -20320,40 +20384,16 @@ const MIN_CONSTRAINT = {
20320
20384
  if (isNaN(numericValue)) {
20321
20385
  return null;
20322
20386
  }
20323
- if (numericValue < minNumber) {
20324
- if (naviInputType === "hour") {
20325
- return naviI18n(`constraint.min.hour.default`, {
20326
- min: minString,
20327
- });
20328
- }
20329
- if (naviInputType === "minute") {
20330
- return naviI18n(`constraint.min.minute.default`, {
20331
- min: minString,
20332
- });
20333
- }
20334
- if (naviInputType === "second") {
20335
- return naviI18n(`constraint.min.second.default`, {
20336
- min: minString,
20337
- });
20338
- }
20339
- if (naviInputType === "percentage") {
20340
- return naviI18n(`constraint.min.percentage.default`, {
20341
- min: minString,
20342
- });
20343
- }
20344
- return naviI18n(`constraint.min.number.default`, {
20345
- min: minString,
20346
- });
20387
+ if (numericValue >= minNumber) {
20388
+ return null;
20347
20389
  }
20348
- return null;
20390
+ return boundMessage(field, "min");
20349
20391
  }
20350
20392
  if (type === "time") {
20351
20393
  const [minHours, minMinutes] = minString.split(":").map(Number);
20352
20394
  const [hours, minutes] = valueAsString.split(":").map(Number);
20353
20395
  if (hours < minHours || (hours === minHours && minutes < minMinutes)) {
20354
- return naviI18n("constraint.min.time.default", {
20355
- min: minString,
20356
- });
20396
+ return boundMessage(field, "min");
20357
20397
  }
20358
20398
  return null;
20359
20399
  }
@@ -20361,18 +20401,13 @@ const MIN_CONSTRAINT = {
20361
20401
  // so they never need a min/max validation message.
20362
20402
  if (DATE_INPUT_TYPE_SET.has(type)) {
20363
20403
  if (valueAsString < minString) {
20364
- const todayIso = getTodayIso(type);
20365
- if (minString === todayIso) {
20366
- return naviI18n("constraint.min.date.today.default");
20367
- }
20368
- return naviI18n("constraint.min.date.default", {
20369
- min: formatDateIso(minString, type),
20370
- });
20404
+ return boundMessage(field, "min");
20371
20405
  }
20372
20406
  return null;
20373
20407
  }
20374
20408
  return null;
20375
20409
  },
20410
+ messageAtBound: (field) => boundMessage(field, "min"),
20376
20411
  };
20377
20412
  CONSTRAINT_ATTRIBUTE_SET.add("min");
20378
20413
 
@@ -20392,18 +20427,17 @@ const MAX_CONSTRAINT = {
20392
20427
  if (cmp === null || cmp <= 0) {
20393
20428
  return null;
20394
20429
  }
20395
-
20396
- return naviI18n("constraint.max.duration.default", {
20397
- max: formatDuration(max),
20398
- });
20430
+ return boundMessage(field, "max");
20399
20431
  }
20400
20432
  if (field.controlType !== "input" && field.controlType !== "picker") {
20401
20433
  return null;
20402
20434
  }
20403
- const maxString = field.controlHostProps.max;
20404
- if (!maxString) {
20435
+ const max = field.controlHostProps.max;
20436
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
20437
+ if (max === undefined || max === null || max === "") {
20405
20438
  return null;
20406
20439
  }
20440
+ const maxString = String(max);
20407
20441
  const type = field.controlHostProps.type;
20408
20442
  const naviInputType = field.controlHostProps["navi-input-type"];
20409
20443
  const valueAsString = uiStateAsText(field.uiState);
@@ -20422,38 +20456,13 @@ const MAX_CONSTRAINT = {
20422
20456
  if (numericValue <= maxNumber) {
20423
20457
  return null;
20424
20458
  }
20425
-
20426
- if (naviInputType === "hour") {
20427
- return naviI18n(`constraint.max.hour.default`, {
20428
- max: maxString,
20429
- });
20430
- }
20431
- if (naviInputType === "minute") {
20432
- return naviI18n(`constraint.max.minute.default`, {
20433
- max: maxString,
20434
- });
20435
- }
20436
- if (naviInputType === "second") {
20437
- return naviI18n(`constraint.max.second.default`, {
20438
- max: maxString,
20439
- });
20440
- }
20441
- if (naviInputType === "percentage") {
20442
- return naviI18n(`constraint.max.percentage.default`, {
20443
- max: maxString,
20444
- });
20445
- }
20446
- return naviI18n(`constraint.max.number.default`, {
20447
- max: maxString,
20448
- });
20459
+ return boundMessage(field, "max");
20449
20460
  }
20450
20461
  if (type === "time") {
20451
20462
  const [maxHours, maxMinutes] = maxString.split(":").map(Number);
20452
20463
  const [hours, minutes] = valueAsString.split(":").map(Number);
20453
20464
  if (hours > maxHours || (hours === maxHours && minutes > maxMinutes)) {
20454
- return naviI18n("constraint.max.time.default", {
20455
- max: maxString,
20456
- });
20465
+ return boundMessage(field, "max");
20457
20466
  }
20458
20467
  return null;
20459
20468
  }
@@ -20461,17 +20470,11 @@ const MAX_CONSTRAINT = {
20461
20470
  if (valueAsString <= maxString) {
20462
20471
  return null;
20463
20472
  }
20464
-
20465
- const todayIso = getTodayIso(type);
20466
- if (maxString === todayIso) {
20467
- return naviI18n("constraint.max.date.today.default");
20468
- }
20469
- return naviI18n("constraint.max.date.default", {
20470
- max: formatDateIso(maxString, type),
20471
- });
20473
+ return boundMessage(field, "max");
20472
20474
  }
20473
20475
  return null;
20474
20476
  },
20477
+ messageAtBound: (field) => boundMessage(field, "max"),
20475
20478
  };
20476
20479
  CONSTRAINT_ATTRIBUTE_SET.add("max");
20477
20480
 
@@ -20666,6 +20669,51 @@ const formatMsToTime = (ms, showSeconds) => {
20666
20669
  return `${hh}:${mm}:${ss}`;
20667
20670
  };
20668
20671
 
20672
+ // What a bound says, whatever the value: the sentence `check` gives a value
20673
+ // past it, and the one a spin gives when its chevron is refused there
20674
+ // (picker_spin.jsx) — one sentence, so the two refuse in the same words. null
20675
+ // for a type that has nothing to say about a bound.
20676
+ const boundMessage = (field, bound) => {
20677
+ const boundValue = field.controlHostProps[bound];
20678
+ if (field.controlType === "duration_group") {
20679
+ return naviI18n(`constraint.${bound}.duration.default`, {
20680
+ [bound]: formatDuration(boundValue),
20681
+ });
20682
+ }
20683
+ const type = field.controlHostProps.type;
20684
+ const naviInputType = field.controlHostProps["navi-input-type"];
20685
+ if (isNumberInput(type, naviInputType)) {
20686
+ const numberKind = NUMBER_BOUND_KIND_SET.has(naviInputType)
20687
+ ? naviInputType
20688
+ : "number";
20689
+ return naviI18n(`constraint.${bound}.${numberKind}.default`, {
20690
+ [bound]: String(boundValue),
20691
+ });
20692
+ }
20693
+ if (type === "time") {
20694
+ return naviI18n(`constraint.${bound}.time.default`, {
20695
+ [bound]: boundValue,
20696
+ });
20697
+ }
20698
+ if (DATE_INPUT_TYPE_SET.has(type)) {
20699
+ if (boundValue === getTodayIso(type)) {
20700
+ return naviI18n(`constraint.${bound}.date.today.default`);
20701
+ }
20702
+ return naviI18n(`constraint.${bound}.date.default`, {
20703
+ [bound]: formatDateIso(boundValue, type),
20704
+ });
20705
+ }
20706
+ return null;
20707
+ };
20708
+ // The numbers whose bound is said with what they count ("Max 23 heures");
20709
+ // any other is "Ce nombre".
20710
+ const NUMBER_BOUND_KIND_SET = new Set([
20711
+ "hour",
20712
+ "minute",
20713
+ "second",
20714
+ "percentage",
20715
+ ]);
20716
+
20669
20717
  const getTodayIso = (inputType) => {
20670
20718
  const now = new Date();
20671
20719
  const yyyy = now.getFullYear();
@@ -26690,6 +26738,171 @@ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
26690
26738
  return childStore !== undefined && childStore === getChildStore(ownerId);
26691
26739
  };
26692
26740
 
26741
+ /*
26742
+ * What a previous document kept of the page it was on (see keepPageOnScreen in
26743
+ * nav/page_kept.js), waiting for the reads that draw it again, and what those
26744
+ * reads are made of on the way out: rows with their relations inline.
26745
+ *
26746
+ * A kept read is found by the read's name and the params it was asked with
26747
+ * (getKeptReadKey). It is handed out once: the first run of the read draws it
26748
+ * while its request is out, and from then on the store holds the rows. Handed
26749
+ * out again, after a reset say, it would write the previous document's rows
26750
+ * over a fresher answer.
26751
+ *
26752
+ * This module is the half the resources see: `resource()` and the range reader
26753
+ * take from it, and say which of their reads can be kept at all. Which reads
26754
+ * are on the page on screen, and what the slot is written into, is
26755
+ * page_kept.js.
26756
+ */
26757
+
26758
+ // key → what the read answered in the previous document, not handed out yet
26759
+ const offeredReadMap = new Map();
26760
+ const offerKeptReads = (entryMap) => {
26761
+ offeredReadMap.clear();
26762
+ for (const [key, entry] of entryMap) {
26763
+ offeredReadMap.set(key, entry);
26764
+ }
26765
+ };
26766
+ const withdrawKeptReads = () => {
26767
+ offeredReadMap.clear();
26768
+ };
26769
+ const takeKeptRead = (key) => {
26770
+ const entry = offeredReadMap.get(key);
26771
+ if (entry === undefined) {
26772
+ return undefined;
26773
+ }
26774
+ offeredReadMap.delete(key);
26775
+ return entry;
26776
+ };
26777
+
26778
+ // The root actions whose answer is rows of a store, written and read back the
26779
+ // same way: a resource's GET and GET_MANY. Anything else a route runs (a page's
26780
+ // code, a computation) has nothing a later document could draw.
26781
+ const keepableReadWeakSet = new WeakSet();
26782
+ const markKeepableRead = (rootAction) => {
26783
+ keepableReadWeakSet.add(rootAction);
26784
+ };
26785
+ const isKeepableRead = (action) => {
26786
+ const rootAction = action.rootAction || action;
26787
+ return keepableReadWeakSet.has(rootAction);
26788
+ };
26789
+
26790
+ // The range readers a <List.Items> on screen reads through, each with the number
26791
+ // of lists reading it: the reads of the page a route action does not cover.
26792
+ const shownRangeReadersSignal = signal(new Map());
26793
+ const showRangeReader = (reader) => {
26794
+ const shownMap = new Map(shownRangeReadersSignal.peek());
26795
+ shownMap.set(reader, (shownMap.get(reader) || 0) + 1);
26796
+ shownRangeReadersSignal.value = shownMap;
26797
+ return () => {
26798
+ const shownMapNow = new Map(shownRangeReadersSignal.peek());
26799
+ const count = shownMapNow.get(reader);
26800
+ if (count === 1) {
26801
+ shownMapNow.delete(reader);
26802
+ } else {
26803
+ shownMapNow.set(reader, count - 1);
26804
+ }
26805
+ shownRangeReadersSignal.value = shownMapNow;
26806
+ };
26807
+ };
26808
+
26809
+ // A row already in the store is the fresher one: the kept copy only stands in
26810
+ // for rows nothing has answered yet. Its relations are written as they were
26811
+ // kept, which a real answer corrects like any other.
26812
+ const upsertKeptItem = (store, keptItem) => {
26813
+ const id = keptItem[store.idKey];
26814
+ if (id !== undefined) {
26815
+ const existing = store.select(id);
26816
+ if (existing) {
26817
+ return existing;
26818
+ }
26819
+ }
26820
+ return store.upsert(keptItem);
26821
+ };
26822
+
26823
+ const getKeptReadKey = (name, params) => {
26824
+ return `${name}|${getParamsKey(params)}`;
26825
+ };
26826
+ // `true` is what a route action without params is bound to (see routeAction),
26827
+ // and a key holding undefined says nothing a missing one does not.
26828
+ const getParamsKey = (params) => {
26829
+ if (params === undefined || params === NO_PARAMS || params === true) {
26830
+ return "";
26831
+ }
26832
+ if (isPlainParams(params)) {
26833
+ const keys = Object.keys(params).filter((key) => params[key] !== undefined);
26834
+ if (keys.length === 0) {
26835
+ return "";
26836
+ }
26837
+ }
26838
+ return stableStringify(params);
26839
+ };
26840
+ const isPlainParams = (value) => {
26841
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26842
+ };
26843
+ const stableStringify = (value) => {
26844
+ if (value === null || typeof value !== "object") {
26845
+ return JSON.stringify(value);
26846
+ }
26847
+ if (Array.isArray(value)) {
26848
+ return `[${value.map(stableStringify).join(",")}]`;
26849
+ }
26850
+ const keys = Object.keys(value)
26851
+ .filter((key) => value[key] !== undefined)
26852
+ .sort();
26853
+ const parts = [];
26854
+ for (const key of keys) {
26855
+ parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
26856
+ }
26857
+ return `{${parts.join(",")}}`;
26858
+ };
26859
+
26860
+ // What a read's data is made of, ready to be written: a row, or the rows of a
26861
+ // list, each with its relations inline.
26862
+ const serializeData = (data) => {
26863
+ if (Array.isArray(data)) {
26864
+ return data.map((item) => serializeItem(item, new Set()));
26865
+ }
26866
+ return serializeItem(data, new Set());
26867
+ };
26868
+ // The item with its relations inline: a relation value stands for what its
26869
+ // signal holds (the child row, the child rows, or nothing), a row reached
26870
+ // twice on the same path is written as its id — the setters accept both.
26871
+ const serializeItem = (item, ancestorSet) => {
26872
+ ancestorSet.add(item);
26873
+ const serialized = {};
26874
+ for (const key of Object.keys(item)) {
26875
+ serialized[key] = serializeValue(item[key], ancestorSet);
26876
+ }
26877
+ ancestorSet.delete(item);
26878
+ return serialized;
26879
+ };
26880
+ const serializeValue = (value, ancestorSet) => {
26881
+ if (value === null || typeof value !== "object") {
26882
+ return value;
26883
+ }
26884
+ if (Array.isArray(value)) {
26885
+ return value.map((entry) => serializeValue(entry, ancestorSet));
26886
+ }
26887
+ const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
26888
+ if (objectSignal) {
26889
+ const held = objectSignal.value;
26890
+ if (held === undefined || held === null) {
26891
+ return null;
26892
+ }
26893
+ return serializeValue(held, ancestorSet);
26894
+ }
26895
+ if (typeof value.toJSON === "function") {
26896
+ return value;
26897
+ }
26898
+ if (ancestorSet.has(value)) {
26899
+ return Object.hasOwn(value, SYMBOL_IDENTITY)
26900
+ ? value[SYMBOL_IDENTITY]
26901
+ : null;
26902
+ }
26903
+ return serializeItem(value, ancestorSet);
26904
+ };
26905
+
26693
26906
  const paramScopeWeakSet = createIterableWeakSet();
26694
26907
  let paramScopeIdCounter = 0;
26695
26908
  const getParamScope = (params) => {
@@ -26860,66 +27073,6 @@ const createResourcePersistence = (
26860
27073
  };
26861
27074
  };
26862
27075
 
26863
- const getParamsKey = (params) => {
26864
- if (params === undefined || params === NO_PARAMS) {
26865
- return "";
26866
- }
26867
- return stableStringify(params);
26868
- };
26869
-
26870
- const stableStringify = (value) => {
26871
- if (value === null || typeof value !== "object") {
26872
- return JSON.stringify(value);
26873
- }
26874
- if (Array.isArray(value)) {
26875
- return `[${value.map(stableStringify).join(",")}]`;
26876
- }
26877
- const keys = Object.keys(value).sort();
26878
- const parts = [];
26879
- for (const key of keys) {
26880
- parts.push(`${JSON.stringify(key)}:${stableStringify(value[key])}`);
26881
- }
26882
- return `{${parts.join(",")}}`;
26883
- };
26884
-
26885
- // The item with its relations inline: a relation value stands for what its
26886
- // signal holds (the child row, the child rows, or nothing), a row reached
26887
- // twice on the same path is written as its id — the setters accept both.
26888
- const serializeItem = (item, ancestorSet) => {
26889
- ancestorSet.add(item);
26890
- const serialized = {};
26891
- for (const key of Object.keys(item)) {
26892
- serialized[key] = serializeValue(item[key], ancestorSet);
26893
- }
26894
- ancestorSet.delete(item);
26895
- return serialized;
26896
- };
26897
- const serializeValue = (value, ancestorSet) => {
26898
- if (value === null || typeof value !== "object") {
26899
- return value;
26900
- }
26901
- if (Array.isArray(value)) {
26902
- return value.map((entry) => serializeValue(entry, ancestorSet));
26903
- }
26904
- const objectSignal = value[SYMBOL_OBJECT_SIGNAL];
26905
- if (objectSignal) {
26906
- const held = objectSignal.value;
26907
- if (held === undefined || held === null) {
26908
- return null;
26909
- }
26910
- return serializeValue(held, ancestorSet);
26911
- }
26912
- if (typeof value.toJSON === "function") {
26913
- return value;
26914
- }
26915
- if (ancestorSet.has(value)) {
26916
- return Object.hasOwn(value, SYMBOL_IDENTITY)
26917
- ? value[SYMBOL_IDENTITY]
26918
- : null;
26919
- }
26920
- return serializeItem(value, ancestorSet);
26921
- };
26922
-
26923
27076
  /*
26924
27077
  * GET_RANGE: reading a resource one slice at a time, for a list that draws its
26925
27078
  * rows as it goes (`<List.Items itemsAction>`).
@@ -26956,9 +27109,23 @@ const serializeValue = (value, ancestorSet) => {
26956
27109
  * of what a rerun is for an action. Every reader made by `bindParams` shares
26957
27110
  * the signal and the compositions of the one it comes from: the params say
26958
27111
  * which slices are read, not which collection.
27112
+ *
27113
+ * A composition also outlives the document when a list on the page on screen
27114
+ * reads it (see keepPageOnScreen): the ranks around the window it draws are
27115
+ * written with their rows, and the first read of the next document finds them
27116
+ * as a composition left by a list that went away — the same return, after a
27117
+ * longer absence.
26959
27118
  */
26960
27119
 
26961
27120
 
27121
+ // The ranks kept of a composition for the next document: the window a list
27122
+ // draws, widened by at least one step on each side, and snapped to steps so a
27123
+ // scroll within one draws the same ranks and writes nothing.
27124
+ const KEPT_RANK_STEP = 20;
27125
+ // Counts the answers every composition took, so that one landing can be told
27126
+ // from one seen before (see readKept).
27127
+ let answerSequence = 0;
27128
+
26962
27129
  const createRangeReader = (
26963
27130
  actionName,
26964
27131
  callback,
@@ -26967,6 +27134,9 @@ const createRangeReader = (
26967
27134
  params: boundParams,
26968
27135
  invalidationSignal = signal(0),
26969
27136
  compositionSet = new Set(),
27137
+ // Bumped whenever what is kept of a composition for the next document
27138
+ // changes (see readKept); shared like the compositions themselves.
27139
+ keptVersionSignal = signal(0),
26970
27140
  },
26971
27141
  ) => {
26972
27142
  // Which composition this reader is about: the values its params hold, not its
@@ -26983,6 +27153,40 @@ const createRangeReader = (
26983
27153
  }
26984
27154
  return null;
26985
27155
  };
27156
+ const keptKeyOf = (params) => getKeptReadKey(actionName, params);
27157
+ const bumpKeptVersion = () => {
27158
+ keptVersionSignal.value = keptVersionSignal.peek() + 1;
27159
+ };
27160
+ // The composition the previous document kept for these params, entering the
27161
+ // store and the compositions as if a list had just left it.
27162
+ const restoreKeptComposition = (params) => {
27163
+ const kept = takeKeptRead(keptKeyOf(params));
27164
+ if (!kept || typeof kept.count !== "number" || !kept.items) {
27165
+ return null;
27166
+ }
27167
+ const composition = {
27168
+ params,
27169
+ idByIndex: new Map(),
27170
+ count: kept.count,
27171
+ keptFrom: undefined,
27172
+ keptTo: undefined,
27173
+ answeredAt: 0,
27174
+ };
27175
+ for (const rank of Object.keys(kept.items)) {
27176
+ const index = Number(rank);
27177
+ const item = upsertKeptItem(store, kept.items[rank]);
27178
+ composition.idByIndex.set(index, item[store.idKey]);
27179
+ if (composition.keptFrom === undefined || index < composition.keptFrom) {
27180
+ composition.keptFrom = index;
27181
+ }
27182
+ if (composition.keptTo === undefined || index + 1 > composition.keptTo) {
27183
+ composition.keptTo = index + 1;
27184
+ }
27185
+ }
27186
+ compositionSet.add(composition);
27187
+ bumpKeptVersion();
27188
+ return composition;
27189
+ };
26986
27190
  const readRange = async (range = {}) => {
26987
27191
  const { signal, ...rangeParams } = range;
26988
27192
  const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
@@ -27016,11 +27220,15 @@ const createRangeReader = (
27016
27220
  readRange.invalidate = () => {
27017
27221
  compositionSet.clear();
27018
27222
  invalidationSignal.value = invalidationSignal.peek() + 1;
27223
+ bumpKeptVersion();
27019
27224
  };
27020
27225
  // The rows of a composition, drawn from the store: a rank whose row is gone
27021
27226
  // from the store resolves to nothing and is asked for again.
27022
27227
  readRange.readComposition = () => {
27023
- const composition = findComposition(currentParams());
27228
+ const params = currentParams();
27229
+ const composition = untracked(
27230
+ () => findComposition(params) || restoreKeptComposition(params),
27231
+ );
27024
27232
  if (!composition) {
27025
27233
  return null;
27026
27234
  }
@@ -27044,18 +27252,85 @@ const createRangeReader = (
27044
27252
  const params = currentParams();
27045
27253
  let composition = findComposition(params);
27046
27254
  if (!composition) {
27047
- composition = { params, idByIndex: new Map(), count };
27255
+ composition = {
27256
+ params,
27257
+ idByIndex: new Map(),
27258
+ count,
27259
+ keptFrom: undefined,
27260
+ keptTo: undefined,
27261
+ answeredAt: 0,
27262
+ };
27048
27263
  compositionSet.add(composition);
27049
27264
  } else if (replace) {
27050
27265
  composition.idByIndex = new Map();
27051
27266
  }
27052
27267
  composition.count = count;
27268
+ answerSequence++;
27269
+ composition.answeredAt = answerSequence;
27053
27270
  for (const [index, item] of byIndex) {
27054
27271
  const id = item ? item[store.idKey] : undefined;
27055
27272
  if (id !== undefined) {
27056
27273
  composition.idByIndex.set(index, id);
27057
27274
  }
27058
27275
  }
27276
+ bumpKeptVersion();
27277
+ };
27278
+ // The ranks [windowFrom, windowTo) a list draws of this composition: what is
27279
+ // kept of it for the next document is around them (see readKept).
27280
+ readRange.frameComposition = (windowFrom, windowTo) => {
27281
+ const composition = findComposition(currentParams());
27282
+ if (!composition) {
27283
+ return;
27284
+ }
27285
+ let keptFrom =
27286
+ Math.floor((windowFrom - KEPT_RANK_STEP) / KEPT_RANK_STEP) *
27287
+ KEPT_RANK_STEP;
27288
+ if (keptFrom < 0) {
27289
+ keptFrom = 0;
27290
+ }
27291
+ const keptTo =
27292
+ Math.ceil((windowTo + KEPT_RANK_STEP) / KEPT_RANK_STEP) * KEPT_RANK_STEP;
27293
+ if (composition.keptFrom === keptFrom && composition.keptTo === keptTo) {
27294
+ return;
27295
+ }
27296
+ composition.keptFrom = keptFrom;
27297
+ composition.keptTo = keptTo;
27298
+ bumpKeptVersion();
27299
+ };
27300
+ // What a document keeps of the composition these params read, for the
27301
+ // next one (see keepPageOnScreen): the count, and the ranks around the
27302
+ // window last drawn with their rows inline. Read inside an effect, it
27303
+ // follows the composition and the rows. `answeredAt` grows with every answer
27304
+ // the composition takes (0: none in this document, restored from a kept one).
27305
+ readRange.readKept = () => {
27306
+ // eslint-disable-next-line no-unused-expressions
27307
+ keptVersionSignal.value;
27308
+ const params = resolveParams(boundParams);
27309
+ const key = keptKeyOf(params);
27310
+ const composition = findComposition(params);
27311
+ if (!composition || composition.count === undefined) {
27312
+ return { key, answeredAt: 0, readEntry: () => undefined };
27313
+ }
27314
+ const readEntry = () => {
27315
+ const items = {};
27316
+ for (const [index, id] of composition.idByIndex) {
27317
+ if (
27318
+ composition.keptFrom !== undefined &&
27319
+ index < composition.keptFrom
27320
+ ) {
27321
+ continue;
27322
+ }
27323
+ if (composition.keptTo !== undefined && index >= composition.keptTo) {
27324
+ continue;
27325
+ }
27326
+ const item = store.select(id);
27327
+ if (item) {
27328
+ items[index] = serializeItem(item, new Set());
27329
+ }
27330
+ }
27331
+ return { count: composition.count, items };
27332
+ };
27333
+ return { key, answeredAt: composition.answeredAt, readEntry };
27059
27334
  };
27060
27335
  // The same trade a list makes with the rows it holds, applied to what is kept
27061
27336
  // for the next mount. Two lists on one collection each trim by their own
@@ -27066,11 +27341,16 @@ const createRangeReader = (
27066
27341
  if (!composition || composition.idByIndex.size <= budget) {
27067
27342
  return;
27068
27343
  }
27344
+ let trimmed = false;
27069
27345
  for (const index of composition.idByIndex.keys()) {
27070
27346
  if (index < keepFrom || index > keepTo) {
27071
27347
  composition.idByIndex.delete(index);
27348
+ trimmed = true;
27072
27349
  }
27073
27350
  }
27351
+ if (trimmed) {
27352
+ bumpKeptVersion();
27353
+ }
27074
27354
  };
27075
27355
  // Memoized for the reasons an action's bindParams is (see actions.js): params
27076
27356
  // and the reader they make have synchronized lifetimes, and params equal in
@@ -27086,6 +27366,7 @@ const createRangeReader = (
27086
27366
  params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
27087
27367
  invalidationSignal,
27088
27368
  compositionSet,
27369
+ keptVersionSignal,
27089
27370
  });
27090
27371
  readerByParams.set(paramsToBind, reader);
27091
27372
  return reader;
@@ -28520,7 +28801,7 @@ const createRestActionFactoryForRoot = (
28520
28801
  `${name}.${verb}`,
28521
28802
  declarationSite,
28522
28803
  );
28523
- return createAction(callback, {
28804
+ const action = createAction(callback, {
28524
28805
  name: `${name}.${verb}`,
28525
28806
  meta: { verb, isMany: false, paramScope },
28526
28807
  resultToValue: (result, action) => {
@@ -28556,17 +28837,33 @@ const createRestActionFactoryForRoot = (
28556
28837
  },
28557
28838
  valueToData: (itemId) => store.select(itemId),
28558
28839
  // While the request is out, the row the store already holds for these
28559
- // params is drawn: the action starts on the "refresh over a known
28840
+ // params is drawn, or the row the page kept from the previous document
28841
+ // (kept_reads.js): the action starts on the "refresh over a known
28560
28842
  // answer" line of data_states.md rather than the first-load one.
28561
28843
  provisionalValue:
28562
28844
  verb === "GET"
28563
- ? (params) => {
28564
- const item = untracked(() => findItemForGet(params));
28845
+ ? (params, action) => {
28846
+ const item = untracked(() => {
28847
+ const itemForGet = findItemForGet(params);
28848
+ if (itemForGet) {
28849
+ return itemForGet;
28850
+ }
28851
+ const keptItem = takeKeptRead(
28852
+ getKeptReadKey(action.name, params),
28853
+ );
28854
+ return keptItem ? upsertKeptItem(store, keptItem) : null;
28855
+ });
28565
28856
  return item ? item[idKey] : undefined;
28566
28857
  }
28567
28858
  : undefined,
28859
+ // Another params' answer is another row.
28860
+ inheritData: verb !== "GET",
28568
28861
  completeSideEffect: onActionComplete,
28569
28862
  });
28863
+ if (verb === "GET") {
28864
+ markKeepableRead(action);
28865
+ }
28866
+ return action;
28570
28867
  };
28571
28868
  const createActionAffectingManyItems = (
28572
28869
  verb,
@@ -28581,7 +28878,7 @@ const createRestActionFactoryForRoot = (
28581
28878
  return itemArray.map((item) => item[idKey]);
28582
28879
  };
28583
28880
 
28584
- return createAction(callback, {
28881
+ const action = createAction(callback, {
28585
28882
  meta: { verb, isMany: true, paramScope },
28586
28883
  name: `${name}.${verb}_MANY`,
28587
28884
  resultToValue: applyResultToValue,
@@ -28589,6 +28886,25 @@ const createRestActionFactoryForRoot = (
28589
28886
  // undefined so a screen can tell "not asked yet" from "answered: none".
28590
28887
  valueToData: (idArray) =>
28591
28888
  idArray === undefined ? undefined : store.selectAll(idArray),
28889
+ // The rows the page kept from the previous document (kept_reads.js),
28890
+ // drawn while the request is out. A binding moving to new params still
28891
+ // hands over the previous answer when there are none: a list standing
28892
+ // in for the next one beats a skeleton.
28893
+ provisionalValue:
28894
+ verb === "GET"
28895
+ ? (params, action) =>
28896
+ untracked(() => {
28897
+ const keptItems = takeKeptRead(
28898
+ getKeptReadKey(action.name, params),
28899
+ );
28900
+ if (!Array.isArray(keptItems)) {
28901
+ return undefined;
28902
+ }
28903
+ return keptItems.map(
28904
+ (keptItem) => upsertKeptItem(store, keptItem)[idKey],
28905
+ );
28906
+ })
28907
+ : undefined,
28592
28908
  completeSideEffect: (actionCompleted) => {
28593
28909
  onActionComplete(actionCompleted);
28594
28910
  if (
@@ -28604,6 +28920,10 @@ const createRestActionFactoryForRoot = (
28604
28920
  return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
28605
28921
  },
28606
28922
  });
28923
+ if (verb === "GET") {
28924
+ markKeepableRead(action);
28925
+ }
28926
+ return action;
28607
28927
  };
28608
28928
 
28609
28929
  return createActionForRoot;
@@ -28614,10 +28934,12 @@ const createRestActionFactoryForRoot = (
28614
28934
  // NetworkPolicyError carrying the policy's reason. A read is called or not depending
28615
28935
  // on where the policy answers reads from. Answered from the store, a GET of a
28616
28936
  // root resource completes with the row the store holds for it (see
28617
- // findItemInStore) — handing the item back is an upsert without effect, so the
28618
- // action completes with what it had and nothing is asked; a relationship GET
28619
- // has no row of its own to answer with and settles with the same error. A
28620
- // completed GET asked to rerun never gets here (actions.js holds it).
28937
+ // findItemInStore), and a root GET or GET_MANY holding rows for these params
28938
+ // (the kept page, drawn while the request is out) with those — handing them
28939
+ // back is an upsert without effect, so the action completes with what it had
28940
+ // and nothing is asked. A relationship GET has no row of its own to answer
28941
+ // with and settles with the same error. A completed GET asked to rerun never
28942
+ // gets here (actions.js holds it).
28621
28943
  const applyNetworkPolicy = (
28622
28944
  restCallback,
28623
28945
  { verb, isMany, findItemInStore },
@@ -28631,6 +28953,12 @@ const applyNetworkPolicy = (
28631
28953
  if (!policy.readsFromStore) {
28632
28954
  return restCallback(params, context);
28633
28955
  }
28956
+ if (isKeepableRead(context.action)) {
28957
+ const dataHeld = context.action.dataSignal.peek();
28958
+ if (dataHeld !== undefined) {
28959
+ return dataHeld;
28960
+ }
28961
+ }
28634
28962
  if (!isMany && findItemInStore) {
28635
28963
  const item = findItemInStore(params, context.action);
28636
28964
  if (item) {
@@ -29158,24 +29486,6 @@ const TYPE_CONVERTERS = {
29158
29486
  },
29159
29487
  };
29160
29488
 
29161
- /**
29162
- * What a component can learn about the page it is rendered in, from the route
29163
- * container rendering that page (see useContentKeepingLeavingPages in
29164
- * route.jsx). `null` outside of any route container.
29165
- *
29166
- * - `isShown()` — whether the container would still show the page. False as
29167
- * soon as the address leads elsewhere: the page is about to be taken down, or
29168
- * hidden until the route transition leaving it is over, and nothing it reads
29169
- * is worth a render any more. False for good once it is kept while leaving: a
29170
- * return to its route mounts the page anew. Asked outside any render — by a
29171
- * subscription deciding whether to re-render — and it reads the routes
29172
- * untracked, so asking subscribes nobody to them.
29173
- * - `leavingSignal` — true once the page is hidden and kept while its movement
29174
- * plays: still mounted, so what it fills outside its own nodes (a slot in a
29175
- * bar) steps out rather than showing with the page arriving.
29176
- */
29177
- const RoutePageContext = createContext(null);
29178
-
29179
29489
  const promiseStateWeakMap = new WeakMap();
29180
29490
  const usePromiseAsyncData = (
29181
29491
  promise,
@@ -34076,11 +34386,17 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
34076
34386
  }
34077
34387
  }
34078
34388
  });
34389
+ // Also run when a Suspense boundary above parks this container — a page
34390
+ // arriving that suspends to a `<Loading>` around both pages: every hook of
34391
+ // the pages kept while leaving has then been cleaned up, which is all that
34392
+ // keeping them saved. Dropped, so the render resuming the container takes
34393
+ // them down, rather than re-running every effect of a hidden page.
34079
34394
  useLayoutEffect(() => {
34080
34395
  return () => {
34081
34396
  for (const entry of leavingRef.current) {
34082
34397
  entry.hold.containers.delete(rerender);
34083
34398
  }
34399
+ leavingRef.current = [];
34084
34400
  };
34085
34401
  }, []);
34086
34402
  if (leaving.length === 0) {
@@ -38892,6 +39208,37 @@ const writeCover = (edge, element, size) => {
38892
39208
  style.setProperty(property, value);
38893
39209
  };
38894
39210
 
39211
+ // Every route action, with whether it asks something of the address at the
39212
+ // moment: its route matches and its params getter returns something.
39213
+ const routeActionEntriesSignal = signal([]);
39214
+ let activeRouteActionsPrevious = new Set();
39215
+ /**
39216
+ * The route actions asking something for the page on screen: their route
39217
+ * matches and their params getter returns something — the running ones, not
39218
+ * every matching one, so an action bound to every page that asks only when a
39219
+ * popup is open over it counts while it is open. Read to rerun what the page
39220
+ * reads (the app back from the background), and by `keepPageOnScreen`.
39221
+ * The set is handed out again only when its members change.
39222
+ *
39223
+ * A page's code is declared as a route action too; `action.meta.verb` says
39224
+ * which ones read data.
39225
+ *
39226
+ * @type {import("@preact/signals").ReadonlySignal<Set<object>>}
39227
+ */
39228
+ const activeRouteActionsSignal = computed(() => {
39229
+ const active = new Set();
39230
+ for (const { routeAction, askingSignal } of routeActionEntriesSignal.value) {
39231
+ if (askingSignal.value) {
39232
+ active.add(routeAction);
39233
+ }
39234
+ }
39235
+ if (haveSameMembers(active, activeRouteActionsPrevious)) {
39236
+ return activeRouteActionsPrevious;
39237
+ }
39238
+ activeRouteActionsPrevious = active;
39239
+ return active;
39240
+ });
39241
+
38895
39242
  /**
38896
39243
  * Binds an action to a route: it runs when the route matches, with the params
38897
39244
  * the effect reads off the address, runs again on a `reload()`, and is aborted
@@ -38949,6 +39296,13 @@ const routeAction = (
38949
39296
  return params;
38950
39297
  };
38951
39298
  const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
39299
+ routeActionEntriesSignal.value = [
39300
+ ...routeActionEntriesSignal.peek(),
39301
+ {
39302
+ routeAction: actionBoundToRoute,
39303
+ askingSignal: computed(() => Boolean(readParamsAsked())),
39304
+ },
39305
+ ];
38952
39306
  for (const route of routes) {
38953
39307
  // Asked of the route's params as they are, like the effect asks them.
38954
39308
  registerRouteReload(route, () =>
@@ -39032,6 +39386,343 @@ const anyMatchingRouteSignal = (routes) => {
39032
39386
  return anyMatchingSignal;
39033
39387
  };
39034
39388
 
39389
+ const haveSameMembers = (setA, setB) => {
39390
+ if (setA.size !== setB.size) {
39391
+ return false;
39392
+ }
39393
+ for (const value of setA) {
39394
+ if (!setB.has(value)) {
39395
+ return false;
39396
+ }
39397
+ }
39398
+ return true;
39399
+ };
39400
+
39401
+ /*
39402
+ * The page on screen, kept for the next document: what its reads answered,
39403
+ * written into a signal the app hands over, and drawn again by the same reads
39404
+ * in a document opening on the same page (a reload, a tab the system
39405
+ * discarded). The read side is state/rest/kept_reads.js; this is what decides
39406
+ * which reads are the page, and when the slot is written.
39407
+ *
39408
+ * The unit is the page, not the resource. One resource is read by several
39409
+ * screens — a search, someone's games, a game opened from anywhere — and kept
39410
+ * per resource it would keep every row ever opened. The page's reads are what
39411
+ * navi already runs for it: the route actions asking something for the page
39412
+ * (activeRouteActionsSignal) and the compositions the lists on screen read
39413
+ * (shownRangeReadersSignal). A search typed on the page is neither, and is not
39414
+ * kept.
39415
+ *
39416
+ * A read is written once it has answered in this document. Before that it
39417
+ * keeps what the previous document kept for it, never what it shows meanwhile:
39418
+ * an answer handed over from other params (a list standing in for the next
39419
+ * one) says nothing about these.
39420
+ *
39421
+ * The write waits for the page to settle and is flushed when the document is
39422
+ * hidden. A navigation goes through pages half there — the route matched, its
39423
+ * list not mounted yet, the page left still drawn under a transition — and
39424
+ * each would otherwise be written out in full. Hidden is the moment the system
39425
+ * may discard the document, so nothing is lost by waiting until then.
39426
+ *
39427
+ * The signal turning `undefined` from outside (sign-out) forgets the page, and
39428
+ * nothing is written until a read lands again: written at once, the page still
39429
+ * on screen would put back the account just left.
39430
+ */
39431
+
39432
+ const WRITE_DELAY = 1000;
39433
+ const NOTHING_PENDING = {};
39434
+ let stopKeepingCurrent = null;
39435
+
39436
+ /**
39437
+ * Keeps what the page on screen reads, so that a new document opening on it —
39438
+ * a reload, a pull-to-refresh, a tab the system discarded in the background —
39439
+ * draws it again while its reads go out, rather than skeletons.
39440
+ *
39441
+ * What the page reads is what navi runs for it: the route actions asking
39442
+ * something for it (see `activeRouteActionsSignal`) whose action is a resource
39443
+ * `GET` or `GET_MANY`, and the `GET_RANGE` compositions the `<List.Items>` on
39444
+ * screen read, around the window they draw. A navigation replaces the slot with
39445
+ * the next page's reads, and the page left is dropped. In the next document,
39446
+ * the first run of the same read with the same params draws the kept answer as
39447
+ * its provisional value: `data` set while `loading` is `true`, the answer
39448
+ * replacing it. Under a network policy answering reads from the store, the
39449
+ * kept answer answers the read. The copy follows the store: a `PUT` on a row
39450
+ * rewrites it.
39451
+ *
39452
+ * Call it before the routes start: the first runs are the ones that look.
39453
+ *
39454
+ * @param {object} options
39455
+ * @param {import("@preact/signals").Signal} options.signal - where the slot is
39456
+ * written: a `stateSignal` with `persists: true` and `type: "object"`, its
39457
+ * `id` holding whatever makes a kept page unusable (the deployed version).
39458
+ * Writing `undefined` into it (sign-out) forgets the page, and nothing is
39459
+ * written again until a read lands.
39460
+ * @param {() => boolean} [options.when] - read whenever the slot is about to be
39461
+ * read or written: `false` reads nothing, writes nothing, and empties the
39462
+ * signal (a tab viewing as someone else). A function, so it can read a signal.
39463
+ * @param {object[]} [options.always] - routes whose reads stay kept once the
39464
+ * page is left, until it is visited again: the page an app opens on when it
39465
+ * is launched rather than reloaded.
39466
+ * @returns {() => void} stops keeping the page.
39467
+ * @see docs/resource.md — what is kept and what is not, what stays the app's
39468
+ */
39469
+ const keepPageOnScreen = ({
39470
+ signal: keptSignal,
39471
+ when,
39472
+ always = [],
39473
+ } = {}) => {
39474
+ if (!isSignal(keptSignal)) {
39475
+ throw new TypeError(
39476
+ `keepPageOnScreen() needs a signal to write the page into, received ${keptSignal}`,
39477
+ );
39478
+ }
39479
+ if (stopKeepingCurrent) {
39480
+ stopKeepingCurrent();
39481
+ }
39482
+ const isAllowed = () => {
39483
+ return when ? Boolean(when()) : true;
39484
+ };
39485
+ const alwaysMatchingSignal = anyMatchingRouteSignal(always);
39486
+
39487
+ // What the signal held when it was read: the previous document's page.
39488
+ let loadedEntryMap = new Map();
39489
+ let loadedAlwaysKeys = [];
39490
+ // Which reads answered in this document (since the last forget): only
39491
+ // those write what they show.
39492
+ let answeredActionWeakSet = new WeakSet();
39493
+ let answeredRangeKeySet = new Set();
39494
+ // The state each action was last seen in, and the last answer seen of each
39495
+ // range reader: a read lands when they move, and not because the slot looks
39496
+ // again at something that had answered before a forget.
39497
+ const stateSeenWeakMap = new WeakMap();
39498
+ const answeredAtSeenWeakMap = new WeakMap();
39499
+ // Forgotten: nothing is written until a read lands.
39500
+ let paused = false;
39501
+ // The reads of the always-kept page, as they were the last time it was on
39502
+ // screen in this document; null until then.
39503
+ let alwaysReads = null;
39504
+
39505
+ const forget = () => {
39506
+ loadedEntryMap = new Map();
39507
+ loadedAlwaysKeys = [];
39508
+ answeredActionWeakSet = new WeakSet();
39509
+ answeredRangeKeySet = new Set();
39510
+ alwaysReads = null;
39511
+ paused = true;
39512
+ withdrawKeptReads();
39513
+ };
39514
+
39515
+ let lastWritten;
39516
+ let pendingWrite = NOTHING_PENDING;
39517
+ let writeTimeout;
39518
+ const flush = () => {
39519
+ clearTimeout(writeTimeout);
39520
+ if (pendingWrite === NOTHING_PENDING) {
39521
+ return;
39522
+ }
39523
+ const value = pendingWrite;
39524
+ pendingWrite = NOTHING_PENDING;
39525
+ if (compareTwoJsValues(value, lastWritten)) {
39526
+ return;
39527
+ }
39528
+ lastWritten = value;
39529
+ keptSignal.value = value;
39530
+ };
39531
+ const scheduleWrite = (value) => {
39532
+ pendingWrite = value;
39533
+ clearTimeout(writeTimeout);
39534
+ writeTimeout = setTimeout(flush, WRITE_DELAY);
39535
+ };
39536
+ const onVisibilityChange = () => {
39537
+ if (document.visibilityState === "hidden") {
39538
+ flush();
39539
+ }
39540
+ };
39541
+ document.addEventListener("visibilitychange", onVisibilityChange);
39542
+ window.addEventListener("pagehide", flush);
39543
+
39544
+ // Runs at once with what the signal already holds (the slot read from
39545
+ // storage), then on every write — the slot's own ones are recognized and
39546
+ // left alone.
39547
+ const unsubscribe = keptSignal.subscribe((value) => {
39548
+ if (value === lastWritten) {
39549
+ return;
39550
+ }
39551
+ lastWritten = value;
39552
+ pendingWrite = NOTHING_PENDING;
39553
+ clearTimeout(writeTimeout);
39554
+ if (
39555
+ value === undefined ||
39556
+ value === null ||
39557
+ typeof value !== "object" ||
39558
+ !isAllowed()
39559
+ ) {
39560
+ forget();
39561
+ return;
39562
+ }
39563
+ const entries =
39564
+ value.entries && typeof value.entries === "object" ? value.entries : {};
39565
+ loadedEntryMap = new Map(Object.entries(entries));
39566
+ loadedAlwaysKeys = Array.isArray(value.always)
39567
+ ? value.always.filter((key) => loadedEntryMap.has(key))
39568
+ : [];
39569
+ alwaysReads = null;
39570
+ paused = false;
39571
+ offerKeptReads(loadedEntryMap);
39572
+ });
39573
+
39574
+ const actionReadWeakMap = new WeakMap();
39575
+ const actionReadOf = (action) => {
39576
+ let read = actionReadWeakMap.get(action);
39577
+ if (read) {
39578
+ return read;
39579
+ }
39580
+ read = () => {
39581
+ const state = action.runningStateSignal.value;
39582
+ const stateSeen = stateSeenWeakMap.get(action);
39583
+ stateSeenWeakMap.set(action, state);
39584
+ const landed = state === COMPLETED && stateSeen !== COMPLETED;
39585
+ if (landed) {
39586
+ answeredActionWeakSet.add(action);
39587
+ }
39588
+ return {
39589
+ key: getKeptReadKey(action.name, action.params),
39590
+ answered: answeredActionWeakSet.has(action),
39591
+ landed,
39592
+ readEntry: () => {
39593
+ const data = action.dataSignal.value;
39594
+ if (data === undefined || data === null) {
39595
+ return undefined;
39596
+ }
39597
+ return serializeData(data);
39598
+ },
39599
+ };
39600
+ };
39601
+ actionReadWeakMap.set(action, read);
39602
+ return read;
39603
+ };
39604
+ const rangeReadWeakMap = new WeakMap();
39605
+ const rangeReadOf = (reader) => {
39606
+ let read = rangeReadWeakMap.get(reader);
39607
+ if (read) {
39608
+ return read;
39609
+ }
39610
+ read = () => {
39611
+ const { key, answeredAt, readEntry } = reader.readKept();
39612
+ const answeredAtSeen = answeredAtSeenWeakMap.get(reader) || 0;
39613
+ answeredAtSeenWeakMap.set(reader, answeredAt);
39614
+ const landed = answeredAt > answeredAtSeen;
39615
+ if (landed) {
39616
+ answeredRangeKeySet.add(key);
39617
+ }
39618
+ return {
39619
+ key,
39620
+ answered: answeredRangeKeySet.has(key),
39621
+ landed,
39622
+ readEntry,
39623
+ };
39624
+ };
39625
+ rangeReadWeakMap.set(reader, read);
39626
+ return read;
39627
+ };
39628
+ const readsOnScreen = () => {
39629
+ const reads = [];
39630
+ for (const routeAction of activeRouteActionsSignal.value) {
39631
+ // The instance is what is kept, so that a page left keeps what it read.
39632
+ // The binding's callSource names that instance and is rewritten once the
39633
+ // binding has moved to another one: read, it brings the slot along.
39634
+ // eslint-disable-next-line no-unused-expressions
39635
+ routeAction.callSource;
39636
+ const action = getActionPrivateProperties(routeAction).currentAction;
39637
+ if (action.params === NO_PARAMS || !isKeepableRead(action)) {
39638
+ continue;
39639
+ }
39640
+ reads.push(actionReadOf(action));
39641
+ }
39642
+ for (const reader of shownRangeReadersSignal.value.keys()) {
39643
+ reads.push(rangeReadOf(reader));
39644
+ }
39645
+ return reads;
39646
+ };
39647
+
39648
+ const stopEffect = effect(() => {
39649
+ const allowed = isAllowed();
39650
+ const routed = routedSignal.value;
39651
+ const alwaysMatching = alwaysMatchingSignal.value;
39652
+ if (!allowed) {
39653
+ forget();
39654
+ pendingWrite = undefined;
39655
+ flush();
39656
+ return;
39657
+ }
39658
+ if (!routed) {
39659
+ // No page yet: the slot is what the first reads will draw.
39660
+ return;
39661
+ }
39662
+ const screenReads = readsOnScreen();
39663
+ if (alwaysMatching) {
39664
+ alwaysReads = screenReads;
39665
+ }
39666
+ const entries = {};
39667
+ let landed = false;
39668
+ const collect = (reads) => {
39669
+ const keys = [];
39670
+ for (const read of reads) {
39671
+ const result = read();
39672
+ if (result.landed) {
39673
+ landed = true;
39674
+ }
39675
+ const entry = result.answered
39676
+ ? result.readEntry()
39677
+ : loadedEntryMap.get(result.key);
39678
+ if (entry === undefined) {
39679
+ continue;
39680
+ }
39681
+ entries[result.key] = entry;
39682
+ keys.push(result.key);
39683
+ }
39684
+ return keys;
39685
+ };
39686
+ const screenKeys = collect(screenReads);
39687
+ let alwaysKeys;
39688
+ if (alwaysReads === screenReads) {
39689
+ alwaysKeys = screenKeys;
39690
+ } else if (alwaysReads) {
39691
+ alwaysKeys = collect(alwaysReads);
39692
+ } else {
39693
+ alwaysKeys = loadedAlwaysKeys;
39694
+ for (const key of alwaysKeys) {
39695
+ entries[key] = loadedEntryMap.get(key);
39696
+ }
39697
+ }
39698
+ if (paused) {
39699
+ if (!landed) {
39700
+ return;
39701
+ }
39702
+ paused = false;
39703
+ }
39704
+ if (screenKeys.length === 0 && alwaysKeys.length === 0) {
39705
+ scheduleWrite(undefined);
39706
+ return;
39707
+ }
39708
+ scheduleWrite({ entries, screen: screenKeys, always: alwaysKeys });
39709
+ });
39710
+
39711
+ const stop = () => {
39712
+ flush();
39713
+ stopEffect();
39714
+ unsubscribe();
39715
+ document.removeEventListener("visibilitychange", onVisibilityChange);
39716
+ window.removeEventListener("pagehide", flush);
39717
+ withdrawKeptReads();
39718
+ if (stopKeepingCurrent === stop) {
39719
+ stopKeepingCurrent = null;
39720
+ }
39721
+ };
39722
+ stopKeepingCurrent = stop;
39723
+ return stop;
39724
+ };
39725
+
39035
39726
  /**
39036
39727
  * A popup that is the anchor, brought to the front.
39037
39728
  *
@@ -66394,7 +67085,7 @@ const css$E = /* css */`
66394
67085
  /* A plain div, unlike dialogEl itself (a real <dialog>, natively hidden
66395
67086
  by default until .show()/.showModal() adds [open]) — needs its own
66396
67087
  starting-hidden mechanism. [navi-hidden] is set from useDialogProps'
66397
- own backdropProps (recomputed from openController.opened on every
67088
+ own backdropProps (recomputed from openController.openedInDom on every
66398
67089
  render, present from the very first one), then toggled by plain
66399
67090
  removeAttribute/setAttribute in openEffect/close, never an explicit
66400
67091
  display override — removing the attribute just lets this rule stop
@@ -68054,7 +68745,7 @@ const useDialogProps = props => {
68054
68745
  "navi-out-of-flow": "",
68055
68746
  "baseClassName": "navi_dialog_backdrop",
68056
68747
  "aria-hidden": "true",
68057
- // Recomputed fresh on every render from openController.opened (not
68748
+ // Recomputed fresh on every render from openController.openedInDom (not
68058
68749
  // driven through a mount-time layout effect, unlike this file's own
68059
68750
  // imperative open/close toggling below) — present in the DOM
68060
68751
  // synchronously from the very first commit, matching this file's own
@@ -68064,7 +68755,7 @@ const useDialogProps = props => {
68064
68755
  // needs — see popover.jsx's own identical prop for the full reasoning.
68065
68756
  "aria-expanded": openController.openedInDom ? "true" : "false",
68066
68757
  // Present from this very first render (recomputed fresh on every one
68067
- // from openController.opened, not a frozen mount-time constant) so
68758
+ // from openController.openedInDom, not a frozen mount-time constant) so
68068
68759
  // there's no gap for the browser to ever paint this plain-div backdrop
68069
68760
  // visible before anything has actually opened it — see popover.jsx's
68070
68761
  // own identical prop for the full reasoning, and this file's own CSS
@@ -68084,7 +68775,7 @@ const useDialogProps = props => {
68084
68775
  // (kept once, not repeated here).
68085
68776
  "aria-expanded": openController.openedInDom ? "true" : "false",
68086
68777
  // Present from the very first render (recomputed fresh from
68087
- // openController.opened every time, not a frozen mount-time constant —
68778
+ // openController.openedInDom every time, not a frozen mount-time constant —
68088
68779
  // see popover.jsx's own identical prop for the full reasoning) so a
68089
68780
  // consumer whose own CSS also sets display (e.g. Popup's flex prop)
68090
68781
  // can't silently defeat showModal()/close()'s native open/close — see
@@ -68487,7 +69178,7 @@ const css$D = /* css */`
68487
69178
  without this it would flash visible for one frame on mount, before
68488
69179
  openEffect's own JS ever gets a chance to hide it. [navi-hidden] is
68489
69180
  set from usePopoverProps' own contentProps (recomputed from
68490
- openController.opened on every render, present from the very first
69181
+ openController.openedInDom on every render, present from the very first
68491
69182
  one — see there for why), then toggled by plain
68492
69183
  removeAttribute/setAttribute in openEffect/close, never an explicit
68493
69184
  display override: removing the attribute just lets this rule stop
@@ -69852,7 +70543,7 @@ const usePopoverProps = props => {
69852
70543
  "navi-out-of-flow": "",
69853
70544
  "baseClassName": "navi_popover_backdrop",
69854
70545
  "aria-hidden": "true",
69855
- // Recomputed fresh on every render from openController.opened (not a
70546
+ // Recomputed fresh on every render from openController.openedInDom (not a
69856
70547
  // frozen mount-time constant, same pattern as navi-hidden just below)
69857
70548
  // rather than driven through a mount-time layout effect — its own
69858
70549
  // actual open/close toggling is still done entirely imperatively
@@ -69927,7 +70618,7 @@ const usePopoverProps = props => {
69927
70618
  // starting-hidden default) — present from this very first render so
69928
70619
  // there's no gap for the browser to ever paint it visible before
69929
70620
  // anything has actually opened it. Recomputed fresh on every render from
69930
- // openController.opened (not a frozen mount-time constant) — Preact
70621
+ // openController.openedInDom (not a frozen mount-time constant) — Preact
69931
70622
  // only touches the DOM for a prop whose value actually changed since
69932
70623
  // the last render, so as long as this always reflects the *current*
69933
70624
  // truth, it never fights the imperative
@@ -74367,7 +75058,6 @@ const ListUI = props => {
74367
75058
  // What the runs ask for and stand for, in items: a source paginates in items,
74368
75059
  // and asks for the first picture's items and the rest in one round trip.
74369
75060
  listItems.pageSize = PAGE_SIZE_DEFAULT;
74370
- listItems.scrolled = scrolled ?? defaultScrolled;
74371
75061
  const {
74372
75062
  virtualItemSizeSignal,
74373
75063
  renderWindow,
@@ -74746,6 +75436,45 @@ const useListScrollSync = ({
74746
75436
  columns
74747
75437
  }) => {
74748
75438
  const debugScroll = useDebugScroll();
75439
+ // Where the list must be: what the caller holds it at (`scrolled`), or where
75440
+ // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
75441
+ // the caller moving the list, so the hold is armed again — that is what makes
75442
+ // it controlled. A `defaultScrolled` that changes is where the list opens,
75443
+ // read again from something that answered again (a count fresher than the
75444
+ // one kept from the previous visit): followed for as long as nobody has
75445
+ // moved the list, and compared by value, since it is said at every render.
75446
+ // Nothing to hold it at (a position not saved yet) is not a position: the
75447
+ // list opens where it opens.
75448
+ const startPlaceRef = useRef(null);
75449
+ if (startPlaceRef.current === null) {
75450
+ startPlaceRef.current = {
75451
+ userTookOver: false,
75452
+ wanted: scrolled ?? defaultScrolled,
75453
+ scrolled,
75454
+ defaultScrolled,
75455
+ way: null,
75456
+ arrived: false
75457
+ };
75458
+ } else {
75459
+ const place = startPlaceRef.current;
75460
+ if (place.scrolled !== scrolled) {
75461
+ place.scrolled = scrolled;
75462
+ place.defaultScrolled = defaultScrolled;
75463
+ place.wanted = scrolled ?? defaultScrolled;
75464
+ place.userTookOver = false;
75465
+ } else if ((scrolled === undefined || scrolled === null) && !compareTwoJsValues(place.defaultScrolled, defaultScrolled)) {
75466
+ place.defaultScrolled = defaultScrolled;
75467
+ if (!place.userTookOver) {
75468
+ place.wanted = defaultScrolled;
75469
+ // Placed again from where it stands: the way to the previous place
75470
+ // says nothing about whose the next scrolls are.
75471
+ place.way = null;
75472
+ place.arrived = false;
75473
+ }
75474
+ }
75475
+ }
75476
+ const scrolledWanted = startPlaceRef.current.wanted;
75477
+ listItems.scrolled = scrolledWanted;
74749
75478
  // The items drawn, [start, end) among the list's own. A ref as well as a
74750
75479
  // state: the render moves it where the list is held (holdWindow) without a
74751
75480
  // commit of its own.
@@ -74760,7 +75489,7 @@ const useListScrollSync = ({
74760
75489
  const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
74761
75490
  inLines: Boolean(columns),
74762
75491
  windowLeavesItemsOut,
74763
- scrolledWanted: scrolled ?? defaultScrolled,
75492
+ scrolledWanted,
74764
75493
  // The fillers hold the room of items above the screen at this size: a
74765
75494
  // size that changes moves what is on screen, like items landing above it.
74766
75495
  beforeSizeChange: () => listItems.captureAnchor()
@@ -75093,25 +75822,7 @@ const useListScrollSync = ({
75093
75822
  const newEnd = newStart + windowSize;
75094
75823
  updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
75095
75824
  };
75096
-
75097
- // Where the list must be: what the caller holds it at (`scrolled`), or where
75098
- // it opens and then lets go (`defaultScrolled`). A `scrolled` that changes is
75099
- // the caller moving the list, so the hold is armed again — that is what makes
75100
- // it controlled.
75101
- // Nothing to hold it at (a position not saved yet) is not a position: the
75102
- // list opens where it opens.
75103
- const scrolledWanted = scrolled ?? defaultScrolled;
75104
75825
  const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
75105
- const startPlaceRef = useRef({
75106
- userTookOver: false,
75107
- wanted: scrolledWanted,
75108
- way: null,
75109
- arrived: false
75110
- });
75111
- if (startPlaceRef.current.wanted !== scrolledWanted) {
75112
- startPlaceRef.current.wanted = scrolledWanted;
75113
- startPlaceRef.current.userTookOver = false;
75114
- }
75115
75826
  // Only one thing owns the scroll at a time: while the list is holding itself
75116
75827
  // somewhere, the anchoring stays out of it (holding an item still is precisely
75117
75828
  // not being at the end anymore once what is above it shrinks).
@@ -78939,6 +79650,17 @@ const useItemStore = ({
78939
79650
  }
78940
79651
  const pages = pagesRef.current;
78941
79652
  const [, setPageVersion] = useState(0);
79653
+ // A list on screen is part of what the page reads: the composition it reads
79654
+ // is kept with the page for the next document (see keepPageOnScreen).
79655
+ useLayoutEffect(() => {
79656
+ if (inMemory || typeof itemsAction !== "function") {
79657
+ return undefined;
79658
+ }
79659
+ if (!itemsAction.isRangeReader) {
79660
+ return undefined;
79661
+ }
79662
+ return showRangeReader(itemsAction);
79663
+ }, [inMemory, itemsAction]);
78942
79664
  // The ranks whose item is from before: the run came back to the screen, or
78943
79665
  // the source said the collection moved. They stay drawn until a page confirms
78944
79666
  // or replaces them, rank by rank. A page covers what the window framed when it
@@ -79072,6 +79794,7 @@ const useItemStore = ({
79072
79794
  }
79073
79795
  }
79074
79796
  if (typeof itemsAction === "function" && itemsAction.trimComposition) {
79797
+ itemsAction.frameComposition(windowFrom, windowTo);
79075
79798
  itemsAction.trimComposition(keepFrom, keepTo, budget);
79076
79799
  }
79077
79800
  },
@@ -79641,6 +80364,9 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
79641
80364
  * room has to be restated as a number by whoever asks. The `index` and
79642
80365
  * `visibleCount` it also hands out say where to aim before the item is found,
79643
80366
  * and how many items to draw before the first paint (see `renderBudget`).
80367
+ * A value that changes before anyone has moved the list is followed (a place
80368
+ * read from a count that answered again): it is compared by value, so it can
80369
+ * be computed at every render.
79644
80370
  * @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
79645
80371
  * The same, but held: the list goes back there every time this changes, even
79646
80372
  * after the user has scrolled — the caller owns where the list is (see
@@ -83172,6 +83898,8 @@ const TRAVELS_NONE = [];
83172
83898
  * name?: string,
83173
83899
  * min?: any,
83174
83900
  * max?: any,
83901
+ * minMessage?: any,
83902
+ * maxMessage?: any,
83175
83903
  * step?: number,
83176
83904
  * type?: string,
83177
83905
  * editable?: boolean,
@@ -83210,7 +83938,15 @@ const TRAVELS_NONE = [];
83210
83938
  * @param {number} [step=1] How many steps a press covers.
83211
83939
  * @param {number} [duration=250] How long a travel takes, in milliseconds.
83212
83940
  * @param {any} [min] The first value one can reach; `max` is the last. Beyond
83213
- * them the travel simply does not happen and the chevron that way says so.
83941
+ * them the travel simply does not happen and the chevron that way says so,
83942
+ * in the words the control in the middle uses for a value past that end ("La
83943
+ * date doit être au plus tard le 2 avril 2027", "Max 5"). A value the control
83944
+ * has no such sentence for — what a plain `Spin` steps through — is "nothing
83945
+ * after this one".
83946
+ * @param {any} [maxMessage] What is said at `max` instead: by the chevron
83947
+ * refused there, by a travel asked past it, and by the control in the middle
83948
+ * for a value past it (picked in the calendar, typed). `minMessage` is its
83949
+ * twin at `min`.
83214
83950
  * @param {string} [padding] The room around the value, `paddingX`/`paddingY`
83215
83951
  * and the four sides included. It goes on what is inside the box rather than
83216
83952
  * on the box: above and below it is taken by the value AND by the two
@@ -83240,6 +83976,8 @@ const Spin = ({
83240
83976
  name,
83241
83977
  min,
83242
83978
  max,
83979
+ minMessage,
83980
+ maxMessage,
83243
83981
  step = 1,
83244
83982
  duration = 250,
83245
83983
  type = "text",
@@ -83365,15 +84103,31 @@ const Spin = ({
83365
84103
  const readOnlyResolved = Boolean(readOnly || readOnlyFromAbove || loadingResolved);
83366
84104
  const disabledResolved = Boolean(disabled || disabledFromAbove);
83367
84105
 
84106
+ // The end of what one may reach, said the way the control in the middle
84107
+ // says it about a value past that end — its `minMessage`/`maxMessage`, or the
84108
+ // sentence its type has for the bound — so a chevron, the calendar and a
84109
+ // typed value refuse in the same words. Asked of the control at the press:
84110
+ // it is not there yet on the first render. `goingNext` is towards the bigger
84111
+ // values, whichever end of the box that is.
84112
+ const boundMessage = goingNext => {
84113
+ const constraint = goingNext ? MAX_CONSTRAINT : MIN_CONSTRAINT;
84114
+ const controlEl = controlRef.current;
84115
+ const controller = (findControlHost(controlEl) || controlEl).__uiStateController__;
84116
+ const {
84117
+ message
84118
+ } = getConstraintMessage(controller, constraint, constraint.messageAtBound(controller), {});
84119
+ return message ?? naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before");
84120
+ };
84121
+
83368
84122
  // Why a chevron refuses, in its own words — and only when the reason is
83369
84123
  // ours: the end of what one may reach is something this control knows and
83370
84124
  // navi cannot guess. For everything else the reason belongs to the state the
83371
84125
  // whole control is in, and it is said about the control ("read-only",
83372
84126
  // "busy") rather than about the button, which is not what the user was
83373
84127
  // pressing.
83374
- const wayOutMessage = (allowed, endKey) => {
84128
+ const wayOutMessage = (allowed, isNext) => {
83375
84129
  if (!allowed) {
83376
- return naviI18n(endKey);
84130
+ return boundMessage(isNext);
83377
84131
  }
83378
84132
  if (loadingResolved) {
83379
84133
  return naviI18n("constraint.busy.default");
@@ -83485,7 +84239,7 @@ const Spin = ({
83485
84239
  // answer belongs.
83486
84240
  e.preventDefault();
83487
84241
  e.stopPropagation();
83488
- openCallout(naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before"), {
84242
+ openCallout(boundMessage(goingNext), {
83489
84243
  anchorElement: middleRef.current,
83490
84244
  status: "info",
83491
84245
  openingEvent: e.detail.event ?? e
@@ -83544,12 +84298,14 @@ const Spin = ({
83544
84298
  const valueDrawnAtEnd = travelPending?.area === "end" ? travelPending.value : valueAtEnd;
83545
84299
  const wayOut = atStart => {
83546
84300
  const isNext = atStart ? startIsNext : !startIsNext;
84301
+ const allowed = atStart ? startAllowed : endAllowed;
83547
84302
  return jsx(WayOut, {
83548
84303
  atStart: atStart,
83549
84304
  onPointerDown: e => {
83550
84305
  wayOutPointerTypeRef.current = e.pointerType;
83551
84306
  },
83552
- unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
84307
+ unavailable: !allowed || loadingResolved || readOnlyResolved,
84308
+ unavailableMessage: () => wayOutMessage(allowed, isNext),
83553
84309
  label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
83554
84310
  onPress: e => {
83555
84311
  focusMiddle();
@@ -83596,6 +84352,8 @@ const Spin = ({
83596
84352
  ...valueProps,
83597
84353
  min: min,
83598
84354
  max: max,
84355
+ minMessage: minMessage,
84356
+ maxMessage: maxMessage,
83599
84357
  step: step,
83600
84358
  readOnly: readOnly,
83601
84359
  disabled: disabled,
@@ -83629,6 +84387,8 @@ const Spin = ({
83629
84387
  ...valueProps,
83630
84388
  min: min,
83631
84389
  max: max,
84390
+ minMessage: minMessage,
84391
+ maxMessage: maxMessage,
83632
84392
  readOnly: readOnly,
83633
84393
  disabled: disabled,
83634
84394
  loading: loading,
@@ -83739,6 +84499,7 @@ const Spin = ({
83739
84499
  const WayOut = ({
83740
84500
  atStart,
83741
84501
  commandFor,
84502
+ unavailable,
83742
84503
  unavailableMessage,
83743
84504
  label,
83744
84505
  onPress,
@@ -83749,10 +84510,10 @@ const WayOut = ({
83749
84510
  // onPointerUp below).
83750
84511
  const pointerDownRef = useRef(null);
83751
84512
  const press = e => {
83752
- if (unavailableMessage) {
84513
+ if (unavailable) {
83753
84514
  // Why it does nothing, said where one pressed: a control would have
83754
84515
  // done this through its own interaction gate, and this one has none.
83755
- openCallout(unavailableMessage, {
84516
+ openCallout(unavailableMessage(), {
83756
84517
  anchorElement: e.currentTarget,
83757
84518
  status: "info",
83758
84519
  openingEvent: e
@@ -83779,8 +84540,8 @@ const WayOut = ({
83779
84540
  ,
83780
84541
  role: "button",
83781
84542
  "aria-label": label,
83782
- "aria-disabled": unavailableMessage ? "true" : undefined,
83783
- "data-unavailable": unavailableMessage ? "" : undefined
84543
+ "aria-disabled": unavailable ? "true" : undefined,
84544
+ "data-unavailable": unavailable ? "" : undefined
83784
84545
  // At the chevron, not beside it: a callout aims its arrow at where the
83785
84546
  // anchor's text starts, and there is no text here — only a glyph in the
83786
84547
  // middle of the box, which is what one pressed and what the answer is
@@ -84013,6 +84774,8 @@ SpinGroup.Separator = SpinGroupSeparator;
84013
84774
  * defaultValue?: number|string,
84014
84775
  * min?: number,
84015
84776
  * max?: number,
84777
+ * minMessage?: any,
84778
+ * maxMessage?: any,
84016
84779
  * step?: number,
84017
84780
  * pad?: number,
84018
84781
  * loop?: boolean,
@@ -84118,6 +84881,8 @@ const numberAtStep = (value, count, {
84118
84881
  * name?: string,
84119
84882
  * min?: string,
84120
84883
  * max?: string,
84884
+ * minMessage?: any,
84885
+ * maxMessage?: any,
84121
84886
  * step?: number,
84122
84887
  * lang?: string,
84123
84888
  * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
@@ -95696,5 +96461,5 @@ const UserSvg = () => jsx("svg", {
95696
96461
  })
95697
96462
  });
95698
96463
 
95699
- 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 };
96464
+ 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 };
95700
96465
  //# sourceMappingURL=jsenv_navi.js.map