@jsenv/navi 0.30.14 → 0.30.16

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.
@@ -14640,12 +14640,18 @@ const setupBrowserIntegrationViaHistory = ({
14640
14640
  url !== readAddress()
14641
14641
  ) {
14642
14642
  const delta = adjacentEntryDelta(url);
14643
- if (delta === -1) {
14644
- window.history.back();
14645
- return undefined;
14646
- }
14647
- if (delta === 1) {
14648
- window.history.forward();
14643
+ if (delta !== 0) {
14644
+ // The press is still the press: what it asks of a route transition is
14645
+ // about this crossing, and the traversal is announced with it.
14646
+ elidedPressPending = {
14647
+ element: options.element,
14648
+ routeTransition: options.routeTransition,
14649
+ };
14650
+ if (delta === -1) {
14651
+ window.history.back();
14652
+ } else {
14653
+ window.history.forward();
14654
+ }
14649
14655
  return undefined;
14650
14656
  }
14651
14657
  }
@@ -15033,6 +15039,8 @@ const setupBrowserIntegrationViaHistory = ({
15033
15039
  const state = popstateEvent.state;
15034
15040
  const landOn = landOnPending;
15035
15041
  landOnPending = null;
15042
+ const elidedPress = elidedPressPending;
15043
+ elidedPressPending = null;
15036
15044
  if (landOn) {
15037
15045
  handleRoutingTask(landOn.url, {
15038
15046
  reason: `"popstate" event for ${url}, landing on ${landOn.url}`,
@@ -15046,6 +15054,7 @@ const setupBrowserIntegrationViaHistory = ({
15046
15054
  reason: `"popstate" event for ${url}`,
15047
15055
  navigationType: "traverse",
15048
15056
  state,
15057
+ ...elidedPress,
15049
15058
  });
15050
15059
  });
15051
15060
 
@@ -15113,6 +15122,10 @@ const setupBrowserIntegrationViaHistory = ({
15113
15122
  // asked for it (see navBack's `landOn`). Read by the popstate listener,
15114
15123
  // which is the one place the landing is applied.
15115
15124
  let landOnPending = null;
15125
+ // The press a push to the entry next door was made by, when it was taken as
15126
+ // a traversal (see handleRoutingTask): announced with the "popstate" that
15127
+ // answers it, which has no press of its own.
15128
+ let elidedPressPending = null;
15116
15129
  // The back navBack asked for, from history.back() to the "popstate" that
15117
15130
  // answers it. Meanwhile the document still reads the entry being left (see
15118
15131
  // whenBackLanded).
@@ -17087,11 +17100,12 @@ PSEUDO_CLASSES[":-navi-has-value"] = {
17087
17100
  };
17088
17101
  {
17089
17102
  // How long a press stays drawn, at least, from the frame that first painted
17090
- // it (Android's PRESSED_STATE_DURATION). An ordinary tap lasts 30 to 80ms,
17091
- // and its pointerup can be handled right after its pointerdown when the main
17092
- // thread was busy: released at once, a press shows on 0 to 3 frames, which
17093
- // is no press at all to the eye.
17094
- const PRESS_SHOWN_AT_LEAST = 64;
17103
+ // it. An ordinary tap lasts 30 to 80ms, and its pointerup can be handled
17104
+ // right after its pointerdown when the main thread was busy: released at
17105
+ // once, a press shows on 0 to 3 frames, which is no press at all to the eye.
17106
+ // 140ms is the Button's shrink (button_ui.jsx): 100ms down, then 40ms at
17107
+ // the bottom before it goes back up, whatever the length of the tap.
17108
+ const PRESS_SHOWN_AT_LEAST = 140;
17095
17109
  // element → the press going on there, until the element stops being drawn
17096
17110
  // pressed. Keyed by the element, not held by its tracker: Box re-runs its
17097
17111
  // tracking whenever the element's state changes, which is what pressing it
@@ -27944,7 +27958,9 @@ const arraySignalStore = (
27944
27958
  name,
27945
27959
  };
27946
27960
 
27947
- const createItemFromProps = (props) => {
27961
+ // `answer`: the props come from an answer, see upsertAnswer. createItem
27962
+ // hands it to the relations, which write the children inside the props.
27963
+ const createItemFromProps = (props, answer) => {
27948
27964
  if (props === null || typeof props !== "object") {
27949
27965
  return props;
27950
27966
  }
@@ -27956,7 +27972,7 @@ const arraySignalStore = (
27956
27972
  `${name || "store"}: an item arrived without "${idKey}" (it has ${Object.keys(props).join(", ")}) — declare its key with idKey, or give a single item an id of its own ({ id: "me", … })`,
27957
27973
  );
27958
27974
  }
27959
- const item = createItem(props);
27975
+ const item = createItem(props, answer);
27960
27976
  return item;
27961
27977
  };
27962
27978
 
@@ -28182,7 +28198,12 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28182
28198
  const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
28183
28199
  const valueChangedByAnswer = (newValue, oldValue) =>
28184
28200
  newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
28185
- const upsertWith = (args, valueChanged) => {
28201
+ // One write, however many stores its relations reach: an effect reading the
28202
+ // rows (a render, the page kept for the next document) runs once the whole
28203
+ // write is in, rather than once per child written, half of it still to come.
28204
+ const upsertWith = (args, answer) => batch(() => upsertWithNow(args, answer));
28205
+ const upsertWithNow = (args, answer) => {
28206
+ const valueChanged = answer ? valueChangedByAnswer : valueChangedByWrite;
28186
28207
  const mutationsMap = new Map(); // Map<itemId, propertyMutations>
28187
28208
  const triggerPropertyMutations = () => {
28188
28209
  for (const itemPropertiesObserver of itemPropertiesObserverSet) {
@@ -28251,11 +28272,12 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28251
28272
  // A relation (.one(), .many() and their scoped forms in
28252
28273
  // resource_graph.js): the row holds which children it points to,
28253
28274
  // the children live in their own store, and whoever reads them
28254
- // follows them there. Its setter writes them and says whether the
28255
- // row points elsewhere now. Compared as a value instead, the
28256
- // children received are never the object the row hands out, and
28257
- // every write naming the relation would replace the row.
28258
- if (set.call(itemWithProps, newValue)) {
28275
+ // follows them there. Its setter writes them, compared as this
28276
+ // write compares (`answer`), and says whether the row points
28277
+ // elsewhere now. Compared as a value instead, the children
28278
+ // received are never the object the row hands out, and every
28279
+ // write naming the relation would replace the row.
28280
+ if (set.call(itemWithProps, newValue, answer)) {
28259
28281
  hasChanges = true;
28260
28282
  propertyMutations[key] = {
28261
28283
  oldValue,
@@ -28303,7 +28325,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28303
28325
  if (array.length === 0) {
28304
28326
  const arrayAllCreated = [];
28305
28327
  for (const props of propsArray) {
28306
- const item = createItemFromProps(props);
28328
+ const item = createItemFromProps(props, answer);
28307
28329
  arrayAllCreated.push(item);
28308
28330
  }
28309
28331
  arraySignal.value = arrayAllCreated;
@@ -28340,7 +28362,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28340
28362
  arrayWithOnlyAffectedItems.push(itemWithPropsOrItem);
28341
28363
  } else {
28342
28364
  hasNew = true;
28343
- const item = createItemFromProps(props);
28365
+ const item = createItemFromProps(props, answer);
28344
28366
  arraySomeUpdated.push(item);
28345
28367
  arrayWithOnlyAffectedItems.push(item);
28346
28368
  }
@@ -28412,23 +28434,24 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
28412
28434
  triggerPropertyMutations();
28413
28435
  return updatedItem;
28414
28436
  }
28415
- const item = createItemFromProps(props);
28437
+ const item = createItemFromProps(props, answer);
28416
28438
  arraySomeUpdated.push(item);
28417
28439
  arraySignal.value = arraySomeUpdated;
28418
28440
  triggerPropertyMutations();
28419
28441
  return item;
28420
28442
  };
28421
- const upsert = (...args) => upsertWith(args, valueChangedByWrite);
28443
+ const upsert = (...args) => upsertWith(args, false);
28422
28444
  /**
28423
28445
  * Writes what the server sent — a callback's answer, a pushed message (a
28424
28446
  * websocket's, a server-sent event's) — into the store. Takes what `upsert`
28425
28447
  * takes and returns the same: a row equal in depth to the one held stays
28426
28448
  * the one held, and its readers stay still, where `upsert` compares by
28427
- * reference and JSON makes every nested object a new one. A new row joins no
28449
+ * reference and JSON makes every nested object a new one. The children its
28450
+ * relations carry into their own stores are compared the same way. A new row joins no
28428
28451
  * `GET_MANY` and no `GET_RANGE` composition: read again what it belongs to
28429
28452
  * (docs/resource.md).
28430
28453
  */
28431
- const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
28454
+ const upsertAnswer = (...args) => upsertWith(args, true);
28432
28455
  const drop = (...args) => {
28433
28456
  const removedItemArray = [];
28434
28457
  const triggerRemovedMutations = () => {
@@ -30275,7 +30298,7 @@ const resource = (name, restCallbacks = {}) => {
30275
30298
  const store = arraySignalStore([], idKey, {
30276
30299
  uniqueKeys,
30277
30300
  name: `${name} store`,
30278
- createItem: (props) => {
30301
+ createItem: (props, answer) => {
30279
30302
  const item = Object.create(itemPrototype);
30280
30303
  Object.assign(item, props);
30281
30304
  Object.defineProperty(item, SYMBOL_IDENTITY, {
@@ -30285,7 +30308,7 @@ const resource = (name, restCallbacks = {}) => {
30285
30308
  configurable: false,
30286
30309
  });
30287
30310
  for (const setupCallback of setupCallbackSet) {
30288
- setupCallback(item);
30311
+ setupCallback(item, answer);
30289
30312
  }
30290
30313
  return item;
30291
30314
  },
@@ -30615,9 +30638,9 @@ const createResource = (
30615
30638
  const childName = `${name}.${propertyName}`;
30616
30639
  const childIdKey = childResource.idKey;
30617
30640
  const childStore = childResource.store;
30618
- addItemSetup((item) => {
30641
+ addItemSetup((item, answer) => {
30619
30642
  const childItemIdSignal = signal();
30620
- const updateChildItemId = (value) => {
30643
+ const updateChildItemId = (value, answer) => {
30621
30644
  const currentChildItemId = childItemIdSignal.peek();
30622
30645
  let childItemProps;
30623
30646
  if (isProps(value)) {
@@ -30631,7 +30654,9 @@ const createResource = (
30631
30654
  childItemIdSignal.value = undefined;
30632
30655
  return true;
30633
30656
  }
30634
- const childItem = childStore.upsert(childItemProps);
30657
+ const childItem = answer
30658
+ ? childStore.upsertAnswer(childItemProps)
30659
+ : childStore.upsert(childItemProps);
30635
30660
  const childItemId = childItem[childIdKey];
30636
30661
  if (currentChildItemId === childItemId) {
30637
30662
  return false;
@@ -30639,7 +30664,7 @@ const createResource = (
30639
30664
  childItemIdSignal.value = childItemId;
30640
30665
  return true;
30641
30666
  };
30642
- updateChildItemId(item[propertyName]);
30667
+ updateChildItemId(item[propertyName], answer);
30643
30668
  const childItemSignal = computed(() =>
30644
30669
  childStore.select(childItemIdSignal.value),
30645
30670
  );
@@ -30824,14 +30849,14 @@ Return the child alone, and "${propertyName}" embedded in what the "${name}" cal
30824
30849
  ${name}.many("${propertyName}") source location: ${declarationSite}`,
30825
30850
  );
30826
30851
  };
30827
- addItemSetup((item) => {
30852
+ addItemSetup((item, answer) => {
30828
30853
  const childItemIdArraySignal = signal([]);
30829
30854
  const updateChildItemIdArray = createChildIdArrayUpdater(
30830
30855
  childStore,
30831
30856
  childIdKey,
30832
30857
  childItemIdArraySignal,
30833
30858
  );
30834
- updateChildItemIdArray(item[propertyName]);
30859
+ updateChildItemIdArray(item[propertyName], answer);
30835
30860
  const childItemArraySignal = computed(() => {
30836
30861
  const idArray = childItemIdArraySignal.value;
30837
30862
  const arr = childStore.selectAll(idArray);
@@ -31170,7 +31195,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31170
31195
  const childSetupCallbackSet = new Set();
31171
31196
  const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
31172
31197
  const applyPropsMap = new Map(); // ownerId → applyProps(props | null)
31173
- addItemSetup((ownerItem) => {
31198
+ addItemSetup((ownerItem, answer) => {
31174
31199
  const ownerId = ownerItem[idKey];
31175
31200
  // A stable child object, mutated in place: reactive getters/setters from
31176
31201
  // chained .one() etc. are defined on it once and survive prop updates.
@@ -31188,8 +31213,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31188
31213
  // relation, `owner[propertyName] = …` — whoever reads the child sees it.
31189
31214
  const childVersionSignal = signal(0);
31190
31215
  // Returns whether the child changed — what the store asks a relation
31191
- // (see assign in array_signal_store.js).
31192
- const applyProps = (props) => {
31216
+ // (see assign in array_signal_store.js). `answer`: the props come from
31217
+ // an answer, and a value equal in depth to the one held is no change
31218
+ // (see upsertAnswer).
31219
+ const applyProps = (props, answer) => {
31193
31220
  if (props === undefined) {
31194
31221
  return false;
31195
31222
  }
@@ -31206,13 +31233,20 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31206
31233
  for (const [key, value] of Object.entries(props)) {
31207
31234
  const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
31208
31235
  if (descriptor && descriptor.set) {
31209
- if (descriptor.set.call(childItem, value)) {
31236
+ if (descriptor.set.call(childItem, value, answer)) {
31210
31237
  changed = true;
31211
31238
  }
31212
- } else if (childItem[key] !== value) {
31213
- childItem[key] = value;
31214
- changed = true;
31239
+ continue;
31215
31240
  }
31241
+ const held = childItem[key];
31242
+ if (held === value) {
31243
+ continue;
31244
+ }
31245
+ if (answer && compareTwoJsValues(held, value)) {
31246
+ continue;
31247
+ }
31248
+ childItem[key] = value;
31249
+ changed = true;
31216
31250
  }
31217
31251
  if (childSignal.peek() !== childItem) {
31218
31252
  childSignal.value = childItem; // first activation
@@ -31230,7 +31264,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31230
31264
  applyPropsMap.set(ownerId, applyProps);
31231
31265
  }
31232
31266
 
31233
- applyProps(ownerItem[propertyName]);
31267
+ applyProps(ownerItem[propertyName], answer);
31234
31268
 
31235
31269
  Object.defineProperty(ownerItem, propertyName, {
31236
31270
  get: () => {
@@ -31273,7 +31307,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31273
31307
  // brings its child.
31274
31308
  const applyProps = applyPropsMap.get(ownerId);
31275
31309
  if (applyProps) {
31276
- applyProps(props);
31310
+ applyProps(props, true);
31277
31311
  }
31278
31312
  return [ownerId, props];
31279
31313
  },
@@ -31406,18 +31440,18 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31406
31440
  const createScope = (ownerKey) => {
31407
31441
  const childStore = arraySignalStore([], childIdKey, {
31408
31442
  name: `${childName}#${ownerKey} store`,
31409
- createItem: (props) => {
31443
+ createItem: (props, answer) => {
31410
31444
  const childItem = {};
31411
31445
  Object.assign(childItem, props);
31412
31446
  for (const childSetup of childSetupCallbackSet) {
31413
- childSetup(childItem);
31447
+ childSetup(childItem, answer);
31414
31448
  }
31415
31449
  return childItem;
31416
31450
  },
31417
31451
  });
31418
31452
  return { childStore, idArraySignal: signal([]) };
31419
31453
  };
31420
- addItemSetup((item) => {
31454
+ addItemSetup((item, answer) => {
31421
31455
  const ownerId = item[idKey];
31422
31456
  let scope;
31423
31457
  if (ownerName) {
@@ -31464,7 +31498,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
31464
31498
  // untouched — children may have been fetched by uniqueKey before the
31465
31499
  // parent was loaded. Only an explicit value replaces the collection.
31466
31500
  if (item[propertyName] !== undefined) {
31467
- updateChildItemIdArray(item[propertyName]);
31501
+ updateChildItemIdArray(item[propertyName], answer);
31468
31502
  }
31469
31503
 
31470
31504
  // When an id is renamed (PUT/PATCH changes the idKey), patch the id array.
@@ -32088,7 +32122,8 @@ ${relationLabel} source location: ${declarationSite}`,
32088
32122
  // child ids, upserting each entry into the child store. The id array signal is
32089
32123
  // only touched when the resulting ids actually differ.
32090
32124
  // Returns whether the children the row points to changed — what the store asks
32091
- // a relation (see assign in array_signal_store.js).
32125
+ // a relation (see assign in array_signal_store.js). `answer`: the children come
32126
+ // from an answer, and are written as one (see upsertAnswer).
32092
32127
  // `wholeChildren` (.scopedMany()): each child received is the whole child, see
32093
32128
  // toWholeScopedChild.
32094
32129
  const createChildIdArrayUpdater = (
@@ -32097,7 +32132,7 @@ const createChildIdArrayUpdater = (
32097
32132
  idArraySignal,
32098
32133
  { wholeChildren = false } = {},
32099
32134
  ) => {
32100
- return (valueArray) => {
32135
+ return (valueArray, answer) => {
32101
32136
  const currentIdArray = idArraySignal.peek();
32102
32137
  if (!Array.isArray(valueArray)) {
32103
32138
  if (currentIdArray.length > 0) {
@@ -32119,7 +32154,9 @@ const createChildIdArrayUpdater = (
32119
32154
  } else {
32120
32155
  continue;
32121
32156
  }
32122
- const childItem = childStore.upsert(childItemProps);
32157
+ const childItem = answer
32158
+ ? childStore.upsertAnswer(childItemProps)
32159
+ : childStore.upsert(childItemProps);
32123
32160
  const childItemId = childItem[childIdKey];
32124
32161
  if (currentIdArray[idArray.length] !== childItemId) {
32125
32162
  modified = true;
@@ -40077,7 +40114,8 @@ const TRANSITION_COVERED_ATTRIBUTE = "data-navi-route-transition-covered";
40077
40114
  const TRANSITION_AREA_ATTRIBUTE = "data-navi-route-transition-area";
40078
40115
  const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
40079
40116
  // What a history entry remembers of the crossing that created it, in the
40080
- // entry's own state: `{ from, type, direction, duration }` (see recordCrossing).
40117
+ // entry's own state: `{ fromDepth, type, direction, duration }` (see
40118
+ // recordCrossing).
40081
40119
  const CROSSING_STATE_KEY = "jsenv_route_transition";
40082
40120
  const AREA_NAME = "navi-route-transition";
40083
40121
  // The pictures carrying the movement, among everything else it takes along —
@@ -40734,51 +40772,35 @@ const normalizeRequest = transition => {
40734
40772
  * What a TRAVERSAL asks for: the crossing it retraces.
40735
40773
  *
40736
40774
  * The entry a push creates remembers the crossing that created it (see
40737
- * recordCrossing). A back onto the page that crossing came from undoes it —
40775
+ * recordCrossing). A back onto the entry that crossing came from undoes it —
40738
40776
  * the same movement, the other way — and a forward onto an entry whose
40739
- * crossing came from the page being left plays it again as it was. Both
40777
+ * crossing came from the entry being left plays it again as it was. Both
40740
40778
  * entries are read because each side is the only one that knows its case: the
40741
40779
  * entry being LEFT says how it was reached (a back), the entry being REACHED
40742
40780
  * says how it was reached (a forward).
40743
40781
  *
40744
- * Both can be true at once — A, B, A again: leaving the second A for B is the
40745
- * way back of A → B and the way in of B → A. The entries' depths in the stack
40746
- * tell them apart; without a depth on both, a back is assumed, the traversal
40747
- * by far the most often made.
40782
+ * An entry is named by its depth in the stack, never by its url: a replace
40783
+ * keeps the entry and its state while it rewrites the address — a param of
40784
+ * the page settling, a list writing where it stands — and the crossing is
40785
+ * still between the same two entries. A crossing always comes from below the
40786
+ * entry it is recorded on, so at most one of the two cases matches.
40748
40787
  *
40749
40788
  * Answers in the shape of readNavigationRequest, with every field said, so
40750
- * that the relations have nothing left to answer for — and marked as a replay,
40751
- * because one relation still outranks it: the one written for the exact way
40752
- * travelled (see the watcher's onMove).
40789
+ * that the relations have nothing left to answer for — except the one written
40790
+ * for the exact way travelled, which outranks it (see the watcher's onMove).
40753
40791
  */
40754
- const readTraversalReplay = ({
40755
- url,
40756
- state
40757
- }, {
40758
- fromUrl,
40759
- fromState
40760
- }) => {
40761
- const to = absoluteUrl(url);
40762
- const from = absoluteUrl(fromUrl);
40763
- if (!to || !from) {
40792
+ const readTraversalReplay = (state, fromState) => {
40793
+ const depthIn = navDepthOf(state);
40794
+ const depthOut = navDepthOf(fromState);
40795
+ if (depthIn === undefined || depthOut === undefined) {
40764
40796
  return null;
40765
40797
  }
40766
40798
  const crossingIn = crossingRecordedOn(state);
40767
- const crossingOut = crossingRecordedOn(fromState);
40768
- const isForward = crossingIn !== null && crossingIn.from === from;
40769
- const isBack = crossingOut !== null && crossingOut.from === to;
40770
- if (isForward && isBack) {
40771
- const depthIn = navDepthOf(state);
40772
- const depthOut = navDepthOf(fromState);
40773
- if (depthIn !== undefined && depthOut !== undefined && depthIn > depthOut) {
40774
- return replayOf(crossingIn);
40775
- }
40776
- return replayOf(reverseCrossing(crossingOut));
40777
- }
40778
- if (isForward) {
40799
+ if (crossingIn !== null && crossingIn.fromDepth === depthOut) {
40779
40800
  return replayOf(crossingIn);
40780
40801
  }
40781
- if (isBack) {
40802
+ const crossingOut = crossingRecordedOn(fromState);
40803
+ if (crossingOut !== null && crossingOut.fromDepth === depthIn) {
40782
40804
  return replayOf(reverseCrossing(crossingOut));
40783
40805
  }
40784
40806
  return null;
@@ -40788,7 +40810,7 @@ const crossingRecordedOn = state => {
40788
40810
  return null;
40789
40811
  }
40790
40812
  const crossing = state[CROSSING_STATE_KEY];
40791
- if (!crossing || typeof crossing.from !== "string") {
40813
+ if (!crossing || typeof crossing.fromDepth !== "number") {
40792
40814
  return null;
40793
40815
  }
40794
40816
  return crossing;
@@ -40798,8 +40820,7 @@ const crossingRecordedOn = state => {
40798
40820
  * Which way the history says a navigation goes, for a default that reads it
40799
40821
  * (see defineRouteDefaultTransition): a push is one level deeper, a traversal
40800
40822
  * goes where the depth of the entry it lands on puts it — back when that is
40801
- * not known, as in readTraversalReplay —, and anything else (a replace) stays
40802
- * on its level: null.
40823
+ * not known —, and anything else (a replace) stays on its level: null.
40803
40824
  */
40804
40825
  const readHistoryDirection = (navigationType, {
40805
40826
  state,
@@ -40850,8 +40871,25 @@ const replayOf = ({
40850
40871
  type,
40851
40872
  typeSaid: true,
40852
40873
  duration,
40853
- direction,
40854
- replay: true
40874
+ direction
40875
+ };
40876
+ };
40877
+
40878
+ // What a press asked, field by field, over the crossing its traversal
40879
+ // retraces: a press naming only a direction keeps the movement retraced and
40880
+ // turns it round, as it would a relation's (see readNavigationRequest).
40881
+ const layerRequest = (request, replay) => {
40882
+ if (!replay) {
40883
+ return request;
40884
+ }
40885
+ if (!request) {
40886
+ return replay;
40887
+ }
40888
+ return {
40889
+ type: request.typeSaid ? request.type : replay.type,
40890
+ typeSaid: true,
40891
+ duration: request.duration === undefined ? replay.duration : request.duration,
40892
+ direction: request.direction === undefined ? replay.direction : request.direction
40855
40893
  };
40856
40894
  };
40857
40895
 
@@ -40937,9 +40975,10 @@ const rebuildWatcher = () => {
40937
40975
  // reverse of a pair, a page from anywhere — and not over a relation
40938
40976
  // written for this exact way: that line is the author's one tool for
40939
40977
  // breaking reciprocity, and the back button is the way back it has to
40940
- // reach. A request made by a link or a navTo() is never dropped: it is
40941
- // about this one crossing, and a written relation is about every one.
40942
- const request = navigationRequest && navigationRequest.replay && found && found.written ? null : navigationRequest;
40978
+ // reach. A request made by a link or a navTo() is never dropped, not even
40979
+ // when its push was taken as a traversal: it is about this one crossing,
40980
+ // and a written relation is about every one.
40981
+ const request = layerRequest(navigationRequest, found && found.written ? null : navigationReplay);
40943
40982
  if (!found && !request) {
40944
40983
  // No relation says anything about these two and this navigation asked
40945
40984
  // for nothing: they are side by side, and silence is the fact — not a
@@ -40990,6 +41029,10 @@ const rebuildWatcher = () => {
40990
41029
  // start of every navigation, so they are always about the latest one.
40991
41030
  let defaultTransition = null;
40992
41031
  let navigationRequest = null;
41032
+ // The crossing a traversal retraces (see readTraversalReplay), kept apart from
41033
+ // what was asked: a relation written for the exact way drops the one and never
41034
+ // the other.
41035
+ let navigationReplay = null;
40993
41036
  let navigationUrl = null;
40994
41037
  // Where the document stands as the navigation begins — the other end of the
40995
41038
  // crossing, and what lets the NEXT navigation be recognised as this one's way
@@ -40997,6 +41040,9 @@ let navigationUrl = null;
40997
41040
  // on a browser without the Navigation API is announced from "popstate", where
40998
41041
  // the address has already moved and location would answer with the destination.
40999
41042
  let navigationFromUrl = null;
41043
+ // Where the entry being left stands in the stack: what a push's crossing names
41044
+ // as the entry it came from (see recordCrossing).
41045
+ let navigationFromDepth;
41000
41046
  let navigationAnimated = false;
41001
41047
  // "push", "replace", "traverse", … — a push is the one navigation that creates
41002
41048
  // the entry a crossing is recorded on.
@@ -41025,17 +41071,18 @@ observeBeforeRouting(details => {
41025
41071
  // Read before the document state moves, so the state peeked is the entry
41026
41072
  // being left.
41027
41073
  const fromState = documentStateSignal.peek();
41028
- // A traversal has no element and no call to ask anything: what it asks is
41029
- // the crossing it retraces — reversed for a back, as it was for a forward.
41030
- navigationRequest = navigationType === "traverse" ? readTraversalReplay(details, {
41031
- fromUrl: navigationFromUrl,
41032
- fromState
41033
- }) : readNavigationRequest(details);
41074
+ navigationFromDepth = navDepthOf(fromState);
41075
+ // A traversal is asked something only when a push to the entry next door
41076
+ // was taken as one, and then carries its press (see the integrations). What
41077
+ // else it asks is the crossing it retraces — reversed for a back, as it was
41078
+ // for a forward.
41079
+ navigationRequest = readNavigationRequest(details);
41080
+ navigationReplay = navigationType === "traverse" ? readTraversalReplay(details.state, fromState) : null;
41034
41081
  navigationHistoryDirection = defaultTransition && defaultTransition.readsHistory ? readHistoryDirection(navigationType, {
41035
41082
  state: details.state,
41036
41083
  fromState
41037
41084
  }) : undefined;
41038
- if (relations.length === 0 && !defaultTransition && !navigationRequest) {
41085
+ if (relations.length === 0 && !defaultTransition && !navigationRequest && !navigationReplay) {
41039
41086
  return;
41040
41087
  }
41041
41088
  holdRenderingForRouting();
@@ -41048,16 +41095,19 @@ observeBeforeRouting(details => {
41048
41095
  }
41049
41096
  });
41050
41097
  observeAfterRouting(() => {
41051
- const request = navigationRequest;
41098
+ const request = layerRequest(navigationRequest, navigationReplay);
41052
41099
  const url = navigationUrl;
41053
41100
  const fromUrl = navigationFromUrl;
41101
+ const fromDepth = navigationFromDepth;
41054
41102
  const type = navigationType;
41055
41103
  const historyDirection = navigationHistoryDirection;
41056
41104
  // Read here and dropped here: a request answers for the navigation it was
41057
41105
  // made on, and the next one is back to the relations.
41058
41106
  navigationRequest = null;
41107
+ navigationReplay = null;
41059
41108
  navigationUrl = null;
41060
41109
  navigationFromUrl = null;
41110
+ navigationFromDepth = undefined;
41061
41111
  navigationType = null;
41062
41112
  navigationHistoryDirection = undefined;
41063
41113
  // A default reading the history plays between two pages a level apart: not
@@ -41096,7 +41146,7 @@ observeAfterRouting(() => {
41096
41146
  if (type === "push") {
41097
41147
  recordCrossing({
41098
41148
  url,
41099
- fromUrl,
41149
+ fromDepth,
41100
41150
  decision: navigationDecision
41101
41151
  });
41102
41152
  }
@@ -41114,15 +41164,14 @@ observeAfterRouting(() => {
41114
41164
  // reached by one keeps the crossing that led to where it stands.
41115
41165
  const recordCrossing = ({
41116
41166
  url,
41117
- fromUrl,
41167
+ fromDepth,
41118
41168
  decision
41119
41169
  }) => {
41120
- if (!decision) {
41170
+ if (!decision || fromDepth === undefined) {
41121
41171
  return;
41122
41172
  }
41123
41173
  const to = absoluteUrl(url);
41124
- const from = absoluteUrl(fromUrl);
41125
- if (!to || !from) {
41174
+ if (!to) {
41126
41175
  return;
41127
41176
  }
41128
41177
  if (documentUrlSignal.peek() !== to) {
@@ -41130,7 +41179,7 @@ const recordCrossing = ({
41130
41179
  return;
41131
41180
  }
41132
41181
  const crossing = {
41133
- from
41182
+ fromDepth
41134
41183
  };
41135
41184
  if (decision.type !== undefined) {
41136
41185
  crossing.type = decision.type;
@@ -46259,15 +46308,16 @@ const armOutsidePressClose = (
46259
46308
  * follows a new `defaultValue` on its own (see followDefaultValue in
46260
46309
  * ui_state_controller.js) — and this is what throws that edit away.
46261
46310
  *
46262
- * On top of whichever value is picked, intent on the anchor warms the content:
46263
- * a pointer entering the popup's anchor, or focus landing in it, builds the
46264
- * content ahead of the click that will open it. Deferring the build to the
46265
- * opening puts its whole cost in the frame right after the click — the frame
46266
- * where a delay is felt hardest — while the ~100-300ms between hovering a
46267
- * trigger and pressing it are free. The warming render starts at the frame
46268
- * after the one that follows the intent: nothing here needs the content in the
46269
- * DOM before the click, only before the open that follows it, and a build
46270
- * started at once would hold back the frame that shows the hover or the focus.
46311
+ * On top of whichever value is picked, intent warms the content: a press on
46312
+ * what opens the popup, and but for "while-opened" a pointer entering the
46313
+ * popup's anchor or focus landing in it, builds the content ahead of the click
46314
+ * that will open it. Deferring the build to the opening puts its whole cost in
46315
+ * the frame right after the click — the frame where a delay is felt hardest —
46316
+ * while the ~100-300ms between hovering a trigger and pressing it are free.
46317
+ * The warming render starts at the frame after the one that follows the
46318
+ * intent: nothing here needs the content in the DOM before the click, only
46319
+ * before the open that follows it, and a build started at once would hold
46320
+ * back the frame that shows the hover or the focus.
46271
46321
  *
46272
46322
  * Under a finger, `pointerover` is not an intent yet: it arrives with the
46273
46323
  * `pointerdown` of every touch, the ones starting a scroll included, and at
@@ -46287,7 +46337,12 @@ const armOutsidePressClose = (
46287
46337
  * press does not open (`data-open-on`: a picker opened by a hold, or only by a
46288
46338
  * command) says nothing; the keyboard still opens it, so focus there warms.
46289
46339
  *
46290
- * "while-opened" content is warmed by an opening press, never by a hover or a
46340
+ * An opening press warms whatever the value. A popup with no anchor — a sheet,
46341
+ * a side panel, a layer opened by a button — has nothing else saying it is
46342
+ * about to open, and under a finger the ~100ms a tap is held are as free as a
46343
+ * hover.
46344
+ *
46345
+ * "while-opened" content is warmed by that press only, never by a hover or a
46291
46346
  * focus. That mode promises content built fresh for the gesture that opens
46292
46347
  * it, and mounted only while that gesture and its opening last: callers lean
46293
46348
  * on it (several pickers sharing one set of content ids, because only one
@@ -46304,15 +46359,16 @@ const armOutsidePressClose = (
46304
46359
  * The press builds the content when it starts, a touch once it has been held
46305
46360
  * as above: the announcement carries the pointerdown that made it. A press
46306
46361
  * released before its build leaves the building to the open its click asks
46307
- * for. The press ending without an open throws the content away — its click
46308
- * reaching the document with the popup still closed, no
46362
+ * for. A press ending without an open keeps what it built, as a hover does:
46363
+ * the next press finds it there. Under "while-opened" it throws the content
46364
+ * away — its click reaching the document with the popup still closed, no
46309
46365
  * click coming after the release, the browser taking the gesture
46310
46366
  * (`pointercancel`), or the next press starting, before any of its own
46311
46367
  * handlers run: one press at a time holds a content. A keyboard opening builds
46312
46368
  * at open time. A "while-opened" popup with an `onOpen` is not warmed by the
46313
46369
  * press: that callback runs before the content is built (see
46314
46370
  * open_controller.js), and a content seeding itself from what it writes must
46315
- * be built after it. Hover and focus warming, for the other values, builds
46371
+ * be built after it. For the other values, a press, a hover and a focus build
46316
46372
  * ahead of `onOpen` all the same.
46317
46373
  */
46318
46374
 
@@ -46349,11 +46405,14 @@ const cancelIdle = (id) =>
46349
46405
  ? cancelIdleCallback(id)
46350
46406
  : clearTimeout(id);
46351
46407
 
46352
- // How long a finger rests on an anchor before it warms it: past the ~35ms at
46353
- // which the browser takes a touch moving at reading speed for a scroll, short
46354
- // of the ~100ms a tap is held, so a deliberate tap is warmed before its
46355
- // release.
46356
- const TOUCH_WARM_HOLD_MS = 80;
46408
+ // How long a finger rests on an anchor or an opener before it warms it: just
46409
+ // past the ~35ms at which the browser takes a touch moving at reading speed
46410
+ // for a scroll. A tap is held ~100ms, and the hold ends at a frame, which a
46411
+ // phone's busy main thread runs late: at 80ms a 100ms tap on 2v2's settings
46412
+ // wheel (CPU 6x) lifted before that frame 5 times out of 7 and was warmed by
46413
+ // nothing. The price is a scroll that starts slowly enough to be taken past
46414
+ // 40ms: it builds what the open would have built.
46415
+ const TOUCH_WARM_HOLD_MS = 40;
46357
46416
 
46358
46417
  const usePopupContentMount = (
46359
46418
  openController,
@@ -46516,9 +46575,9 @@ const usePopupContentMount = (
46516
46575
  anchorElement.removeEventListener("focusin", onFocusIn);
46517
46576
  };
46518
46577
  }, [contentMounted, anchor, mount]);
46519
- // Warm on an opening press, for "while-opened" (see the top comment). Told
46520
- // by the popup's own element, which hears the announcement where it hears
46521
- // the opening request: `onnavi_open_press` beside each `onnavi_request_open`.
46578
+ // Warm on an opening press (see the top comment). Told by the popup's own
46579
+ // element, which hears the announcement where it hears the opening request:
46580
+ // `onnavi_open_press` beside each `onnavi_request_open`.
46522
46581
  const stopPressWarmRef = useRef(null);
46523
46582
  openController.onOpeningPress =
46524
46583
  mount === "while-opened"
@@ -46544,7 +46603,21 @@ const usePopupContentMount = (
46544
46603
  stopWatching();
46545
46604
  };
46546
46605
  }
46547
- : null;
46606
+ : (openPressEvent) => {
46607
+ // Two fingers on two of its openers are one build.
46608
+ if (contentMountedRef.current || stopPressWarmRef.current) {
46609
+ return;
46610
+ }
46611
+ stopPressWarmRef.current = requestBuildOnPress(openPressEvent, {
46612
+ onBuild: () => {
46613
+ stopPressWarmRef.current = null;
46614
+ setContentMounted(true);
46615
+ },
46616
+ onGiveUp: () => {
46617
+ stopPressWarmRef.current = null;
46618
+ },
46619
+ });
46620
+ };
46548
46621
  useEffect(() => {
46549
46622
  return () => {
46550
46623
  stopPressWarmRef.current?.();
@@ -63195,8 +63268,8 @@ a.navi_button {
63195
63268
  outline-color: var(--button-outline-color);
63196
63269
  outline-offset: var(--button-outline-offset);
63197
63270
  transition-property: transform;
63198
- transition-duration: .15s;
63199
- transition-timing-function: cubic-bezier(.4, 0, .2, 1);
63271
+ transition-duration: .25s;
63272
+ transition-timing-function: cubic-bezier(.2, 0, 0, 1);
63200
63273
  position: relative;
63201
63274
 
63202
63275
  & .navi_button_shadow {
@@ -63239,7 +63312,7 @@ a.navi_button {
63239
63312
 
63240
63313
  &[data-pressed] {
63241
63314
  & .navi_button_content {
63242
- transition-duration: 0s;
63315
+ transition-duration: .1s;
63243
63316
  transform: scale(.9);
63244
63317
  }
63245
63318
  }
@@ -75437,10 +75510,12 @@ const giveUpOnControlsHoldingPopup = (controlsHolding, reason) => {
75437
75510
  * itself on it (navi_open_press), --navi-toggle reads its aria-expanded. The
75438
75511
  * popup proper — its open controller, its box, its backdrop, its focus and
75439
75512
  * pseudo-class wiring — is built at the first request: an open, a press about
75440
- * to open it, its signal or its `open` saying open. A list whose rows each hold
75441
- * a few popups mounts hundreds of them, and would pay for every one at every
75442
- * mount, for openings that mostly never come. Built once, the popup stays: the
75443
- * next opening finds it there.
75513
+ * to open it, its signal or its `open` saying open. A press builds what the
75514
+ * popup holds along with it, as it warms the content of a popup already built
75515
+ * (see popup_content_mount.js): the click that follows only opens. A list
75516
+ * whose rows each hold a few popups mounts hundreds of them, and would pay for
75517
+ * every one at every mount, for openings that mostly never come. Built once,
75518
+ * the popup stays: the next opening finds it there.
75444
75519
  *
75445
75520
  * Nothing about where the popup goes is needed at rest: a popover reads its
75446
75521
  * anchor at the opening, from the request.
@@ -77839,6 +77914,11 @@ const DialogStandIn = props => {
77839
77914
  ...props,
77840
77915
  ref: dialogRef,
77841
77916
  defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
77917
+ // Built by a press on what opens it: the content comes with it, and
77918
+ // stays, as a "from-first-open" content does once built (see
77919
+ // popup_stand_in.js).
77920
+ ,
77921
+ mount: builtReason === "press" ? "always" : props.mount
77842
77922
  });
77843
77923
  }
77844
77924
  return jsx("dialog", {
@@ -79896,6 +79976,11 @@ const PopoverStandIn = props => {
79896
79976
  ...props,
79897
79977
  ref: popoverRef,
79898
79978
  defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
79979
+ // Built by a press on what opens it: the content comes with it, and
79980
+ // stays, as a "from-first-open" content does once built (see
79981
+ // popup_stand_in.js).
79982
+ ,
79983
+ mount: builtReason === "press" ? "always" : props.mount
79899
79984
  });
79900
79985
  }
79901
79986
  return jsx("div", {
@@ -91156,6 +91241,13 @@ const useRunItems = (store, {
91156
91241
  if (askEnd > runEnd - 1) {
91157
91242
  askEnd = runEnd - 1;
91158
91243
  }
91244
+ // Only the item before the window's first one says whether that one opens
91245
+ // its group (see whereGroupContinues). Asked for with it, the first item
91246
+ // lands with its group known, not guessed: a wrong guess changes the room
91247
+ // above it, label and scroll inset, once the item before comes.
91248
+ if (groupBy && askStart === windowFrom && windowFrom > runStart && !store.holds(rankOf(windowFrom - 1)) && (!groupOfMissing || groupOfMissing(windowFrom - 1) === undefined)) {
91249
+ askStart = windowFrom - 1;
91250
+ }
91159
91251
  }
91160
91252
  // The item the missing ones hang from, when there is one: a source paginating
91161
91253
  // by cursor ("the 50 before this one") needs an item to count from, and an