@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.
@@ -14034,12 +14034,18 @@ const setupBrowserIntegrationViaHistory = ({
14034
14034
  url !== readAddress()
14035
14035
  ) {
14036
14036
  const delta = adjacentEntryDelta(url);
14037
- if (delta === -1) {
14038
- window.history.back();
14039
- return undefined;
14040
- }
14041
- if (delta === 1) {
14042
- window.history.forward();
14037
+ if (delta !== 0) {
14038
+ // The press is still the press: what it asks of a route transition is
14039
+ // about this crossing, and the traversal is announced with it.
14040
+ elidedPressPending = {
14041
+ element: options.element,
14042
+ routeTransition: options.routeTransition,
14043
+ };
14044
+ if (delta === -1) {
14045
+ window.history.back();
14046
+ } else {
14047
+ window.history.forward();
14048
+ }
14043
14049
  return undefined;
14044
14050
  }
14045
14051
  }
@@ -14427,6 +14433,8 @@ const setupBrowserIntegrationViaHistory = ({
14427
14433
  const state = popstateEvent.state;
14428
14434
  const landOn = landOnPending;
14429
14435
  landOnPending = null;
14436
+ const elidedPress = elidedPressPending;
14437
+ elidedPressPending = null;
14430
14438
  if (landOn) {
14431
14439
  handleRoutingTask(landOn.url, {
14432
14440
  reason: `"popstate" event for ${url}, landing on ${landOn.url}`,
@@ -14440,6 +14448,7 @@ const setupBrowserIntegrationViaHistory = ({
14440
14448
  reason: `"popstate" event for ${url}`,
14441
14449
  navigationType: "traverse",
14442
14450
  state,
14451
+ ...elidedPress,
14443
14452
  });
14444
14453
  });
14445
14454
 
@@ -14507,6 +14516,10 @@ const setupBrowserIntegrationViaHistory = ({
14507
14516
  // asked for it (see navBack's `landOn`). Read by the popstate listener,
14508
14517
  // which is the one place the landing is applied.
14509
14518
  let landOnPending = null;
14519
+ // The press a push to the entry next door was made by, when it was taken as
14520
+ // a traversal (see handleRoutingTask): announced with the "popstate" that
14521
+ // answers it, which has no press of its own.
14522
+ let elidedPressPending = null;
14510
14523
  // The back navBack asked for, from history.back() to the "popstate" that
14511
14524
  // answers it. Meanwhile the document still reads the entry being left (see
14512
14525
  // whenBackLanded).
@@ -16342,11 +16355,12 @@ PSEUDO_CLASSES[":-navi-has-value"] = {
16342
16355
  };
16343
16356
  {
16344
16357
  // How long a press stays drawn, at least, from the frame that first painted
16345
- // it (Android's PRESSED_STATE_DURATION). An ordinary tap lasts 30 to 80ms,
16346
- // and its pointerup can be handled right after its pointerdown when the main
16347
- // thread was busy: released at once, a press shows on 0 to 3 frames, which
16348
- // is no press at all to the eye.
16349
- const PRESS_SHOWN_AT_LEAST = 64;
16358
+ // it. An ordinary tap lasts 30 to 80ms, and its pointerup can be handled
16359
+ // right after its pointerdown when the main thread was busy: released at
16360
+ // once, a press shows on 0 to 3 frames, which is no press at all to the eye.
16361
+ // 140ms is the Button's shrink (button_ui.jsx): 100ms down, then 40ms at
16362
+ // the bottom before it goes back up, whatever the length of the tap.
16363
+ const PRESS_SHOWN_AT_LEAST = 140;
16350
16364
  // element → the press going on there, until the element stops being drawn
16351
16365
  // pressed. Keyed by the element, not held by its tracker: Box re-runs its
16352
16366
  // tracking whenever the element's state changes, which is what pressing it
@@ -26879,7 +26893,9 @@ const arraySignalStore = (
26879
26893
  name,
26880
26894
  };
26881
26895
 
26882
- const createItemFromProps = (props) => {
26896
+ // `answer`: the props come from an answer, see upsertAnswer. createItem
26897
+ // hands it to the relations, which write the children inside the props.
26898
+ const createItemFromProps = (props, answer) => {
26883
26899
  if (props === null || typeof props !== "object") {
26884
26900
  return props;
26885
26901
  }
@@ -26891,7 +26907,7 @@ const arraySignalStore = (
26891
26907
  `${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", … })`,
26892
26908
  );
26893
26909
  }
26894
- const item = createItem(props);
26910
+ const item = createItem(props, answer);
26895
26911
  return item;
26896
26912
  };
26897
26913
 
@@ -27117,7 +27133,12 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27117
27133
  const valueChangedByWrite = (newValue, oldValue) => newValue !== oldValue;
27118
27134
  const valueChangedByAnswer = (newValue, oldValue) =>
27119
27135
  newValue !== oldValue && !compareTwoJsValues(newValue, oldValue);
27120
- const upsertWith = (args, valueChanged) => {
27136
+ // One write, however many stores its relations reach: an effect reading the
27137
+ // rows (a render, the page kept for the next document) runs once the whole
27138
+ // write is in, rather than once per child written, half of it still to come.
27139
+ const upsertWith = (args, answer) => batch(() => upsertWithNow(args, answer));
27140
+ const upsertWithNow = (args, answer) => {
27141
+ const valueChanged = answer ? valueChangedByAnswer : valueChangedByWrite;
27121
27142
  const mutationsMap = new Map(); // Map<itemId, propertyMutations>
27122
27143
  const triggerPropertyMutations = () => {
27123
27144
  for (const itemPropertiesObserver of itemPropertiesObserverSet) {
@@ -27186,11 +27207,12 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27186
27207
  // A relation (.one(), .many() and their scoped forms in
27187
27208
  // resource_graph.js): the row holds which children it points to,
27188
27209
  // the children live in their own store, and whoever reads them
27189
- // follows them there. Its setter writes them and says whether the
27190
- // row points elsewhere now. Compared as a value instead, the
27191
- // children received are never the object the row hands out, and
27192
- // every write naming the relation would replace the row.
27193
- if (set.call(itemWithProps, newValue)) {
27210
+ // follows them there. Its setter writes them, compared as this
27211
+ // write compares (`answer`), and says whether the row points
27212
+ // elsewhere now. Compared as a value instead, the children
27213
+ // received are never the object the row hands out, and every
27214
+ // write naming the relation would replace the row.
27215
+ if (set.call(itemWithProps, newValue, answer)) {
27194
27216
  hasChanges = true;
27195
27217
  propertyMutations[key] = {
27196
27218
  oldValue,
@@ -27238,7 +27260,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27238
27260
  if (array.length === 0) {
27239
27261
  const arrayAllCreated = [];
27240
27262
  for (const props of propsArray) {
27241
- const item = createItemFromProps(props);
27263
+ const item = createItemFromProps(props, answer);
27242
27264
  arrayAllCreated.push(item);
27243
27265
  }
27244
27266
  arraySignal.value = arrayAllCreated;
@@ -27275,7 +27297,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27275
27297
  arrayWithOnlyAffectedItems.push(itemWithPropsOrItem);
27276
27298
  } else {
27277
27299
  hasNew = true;
27278
- const item = createItemFromProps(props);
27300
+ const item = createItemFromProps(props, answer);
27279
27301
  arraySomeUpdated.push(item);
27280
27302
  arrayWithOnlyAffectedItems.push(item);
27281
27303
  }
@@ -27347,23 +27369,24 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27347
27369
  triggerPropertyMutations();
27348
27370
  return updatedItem;
27349
27371
  }
27350
- const item = createItemFromProps(props);
27372
+ const item = createItemFromProps(props, answer);
27351
27373
  arraySomeUpdated.push(item);
27352
27374
  arraySignal.value = arraySomeUpdated;
27353
27375
  triggerPropertyMutations();
27354
27376
  return item;
27355
27377
  };
27356
- const upsert = (...args) => upsertWith(args, valueChangedByWrite);
27378
+ const upsert = (...args) => upsertWith(args, false);
27357
27379
  /**
27358
27380
  * Writes what the server sent — a callback's answer, a pushed message (a
27359
27381
  * websocket's, a server-sent event's) — into the store. Takes what `upsert`
27360
27382
  * takes and returns the same: a row equal in depth to the one held stays
27361
27383
  * the one held, and its readers stay still, where `upsert` compares by
27362
- * reference and JSON makes every nested object a new one. A new row joins no
27384
+ * reference and JSON makes every nested object a new one. The children its
27385
+ * relations carry into their own stores are compared the same way. A new row joins no
27363
27386
  * `GET_MANY` and no `GET_RANGE` composition: read again what it belongs to
27364
27387
  * (docs/resource.md).
27365
27388
  */
27366
- const upsertAnswer = (...args) => upsertWith(args, valueChangedByAnswer);
27389
+ const upsertAnswer = (...args) => upsertWith(args, true);
27367
27390
  const drop = (...args) => {
27368
27391
  const removedItemArray = [];
27369
27392
  const triggerRemovedMutations = () => {
@@ -29159,7 +29182,7 @@ const resource = (name, restCallbacks = {}) => {
29159
29182
  const store = arraySignalStore([], idKey, {
29160
29183
  uniqueKeys,
29161
29184
  name: `${name} store`,
29162
- createItem: (props) => {
29185
+ createItem: (props, answer) => {
29163
29186
  const item = Object.create(itemPrototype);
29164
29187
  Object.assign(item, props);
29165
29188
  Object.defineProperty(item, SYMBOL_IDENTITY, {
@@ -29169,7 +29192,7 @@ const resource = (name, restCallbacks = {}) => {
29169
29192
  configurable: false,
29170
29193
  });
29171
29194
  for (const setupCallback of setupCallbackSet) {
29172
- setupCallback(item);
29195
+ setupCallback(item, answer);
29173
29196
  }
29174
29197
  return item;
29175
29198
  },
@@ -29499,9 +29522,9 @@ const createResource = (
29499
29522
  const childName = `${name}.${propertyName}`;
29500
29523
  const childIdKey = childResource.idKey;
29501
29524
  const childStore = childResource.store;
29502
- addItemSetup((item) => {
29525
+ addItemSetup((item, answer) => {
29503
29526
  const childItemIdSignal = signal();
29504
- const updateChildItemId = (value) => {
29527
+ const updateChildItemId = (value, answer) => {
29505
29528
  const currentChildItemId = childItemIdSignal.peek();
29506
29529
  let childItemProps;
29507
29530
  if (isProps(value)) {
@@ -29515,7 +29538,9 @@ const createResource = (
29515
29538
  childItemIdSignal.value = undefined;
29516
29539
  return true;
29517
29540
  }
29518
- const childItem = childStore.upsert(childItemProps);
29541
+ const childItem = answer
29542
+ ? childStore.upsertAnswer(childItemProps)
29543
+ : childStore.upsert(childItemProps);
29519
29544
  const childItemId = childItem[childIdKey];
29520
29545
  if (currentChildItemId === childItemId) {
29521
29546
  return false;
@@ -29523,7 +29548,7 @@ const createResource = (
29523
29548
  childItemIdSignal.value = childItemId;
29524
29549
  return true;
29525
29550
  };
29526
- updateChildItemId(item[propertyName]);
29551
+ updateChildItemId(item[propertyName], answer);
29527
29552
  const childItemSignal = computed(() =>
29528
29553
  childStore.select(childItemIdSignal.value),
29529
29554
  );
@@ -29708,14 +29733,14 @@ Return the child alone, and "${propertyName}" embedded in what the "${name}" cal
29708
29733
  ${name}.many("${propertyName}") source location: ${declarationSite}`,
29709
29734
  );
29710
29735
  };
29711
- addItemSetup((item) => {
29736
+ addItemSetup((item, answer) => {
29712
29737
  const childItemIdArraySignal = signal([]);
29713
29738
  const updateChildItemIdArray = createChildIdArrayUpdater(
29714
29739
  childStore,
29715
29740
  childIdKey,
29716
29741
  childItemIdArraySignal,
29717
29742
  );
29718
- updateChildItemIdArray(item[propertyName]);
29743
+ updateChildItemIdArray(item[propertyName], answer);
29719
29744
  const childItemArraySignal = computed(() => {
29720
29745
  const idArray = childItemIdArraySignal.value;
29721
29746
  const arr = childStore.selectAll(idArray);
@@ -30051,7 +30076,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30051
30076
  const childSetupCallbackSet = new Set();
30052
30077
  const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
30053
30078
  const applyPropsMap = new Map(); // ownerId → applyProps(props | null)
30054
- addItemSetup((ownerItem) => {
30079
+ addItemSetup((ownerItem, answer) => {
30055
30080
  const ownerId = ownerItem[idKey];
30056
30081
  // A stable child object, mutated in place: reactive getters/setters from
30057
30082
  // chained .one() etc. are defined on it once and survive prop updates.
@@ -30069,8 +30094,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30069
30094
  // relation, `owner[propertyName] = …` — whoever reads the child sees it.
30070
30095
  const childVersionSignal = signal(0);
30071
30096
  // Returns whether the child changed — what the store asks a relation
30072
- // (see assign in array_signal_store.js).
30073
- const applyProps = (props) => {
30097
+ // (see assign in array_signal_store.js). `answer`: the props come from
30098
+ // an answer, and a value equal in depth to the one held is no change
30099
+ // (see upsertAnswer).
30100
+ const applyProps = (props, answer) => {
30074
30101
  if (props === undefined) {
30075
30102
  return false;
30076
30103
  }
@@ -30087,13 +30114,20 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30087
30114
  for (const [key, value] of Object.entries(props)) {
30088
30115
  const descriptor = Object.getOwnPropertyDescriptor(childItem, key);
30089
30116
  if (descriptor && descriptor.set) {
30090
- if (descriptor.set.call(childItem, value)) {
30117
+ if (descriptor.set.call(childItem, value, answer)) {
30091
30118
  changed = true;
30092
30119
  }
30093
- } else if (childItem[key] !== value) {
30094
- childItem[key] = value;
30095
- changed = true;
30120
+ continue;
30096
30121
  }
30122
+ const held = childItem[key];
30123
+ if (held === value) {
30124
+ continue;
30125
+ }
30126
+ if (answer && compareTwoJsValues(held, value)) {
30127
+ continue;
30128
+ }
30129
+ childItem[key] = value;
30130
+ changed = true;
30097
30131
  }
30098
30132
  if (childSignal.peek() !== childItem) {
30099
30133
  childSignal.value = childItem; // first activation
@@ -30111,7 +30145,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30111
30145
  applyPropsMap.set(ownerId, applyProps);
30112
30146
  }
30113
30147
 
30114
- applyProps(ownerItem[propertyName]);
30148
+ applyProps(ownerItem[propertyName], answer);
30115
30149
 
30116
30150
  Object.defineProperty(ownerItem, propertyName, {
30117
30151
  get: () => {
@@ -30154,7 +30188,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30154
30188
  // brings its child.
30155
30189
  const applyProps = applyPropsMap.get(ownerId);
30156
30190
  if (applyProps) {
30157
- applyProps(props);
30191
+ applyProps(props, true);
30158
30192
  }
30159
30193
  return [ownerId, props];
30160
30194
  },
@@ -30284,18 +30318,18 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30284
30318
  const createScope = (ownerKey) => {
30285
30319
  const childStore = arraySignalStore([], childIdKey, {
30286
30320
  name: `${childName}#${ownerKey} store`,
30287
- createItem: (props) => {
30321
+ createItem: (props, answer) => {
30288
30322
  const childItem = {};
30289
30323
  Object.assign(childItem, props);
30290
30324
  for (const childSetup of childSetupCallbackSet) {
30291
- childSetup(childItem);
30325
+ childSetup(childItem, answer);
30292
30326
  }
30293
30327
  return childItem;
30294
30328
  },
30295
30329
  });
30296
30330
  return { childStore, idArraySignal: signal([]) };
30297
30331
  };
30298
- addItemSetup((item) => {
30332
+ addItemSetup((item, answer) => {
30299
30333
  const ownerId = item[idKey];
30300
30334
  let scope;
30301
30335
  if (ownerName) {
@@ -30342,7 +30376,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30342
30376
  // untouched — children may have been fetched by uniqueKey before the
30343
30377
  // parent was loaded. Only an explicit value replaces the collection.
30344
30378
  if (item[propertyName] !== undefined) {
30345
- updateChildItemIdArray(item[propertyName]);
30379
+ updateChildItemIdArray(item[propertyName], answer);
30346
30380
  }
30347
30381
 
30348
30382
  // When an id is renamed (PUT/PATCH changes the idKey), patch the id array.
@@ -30966,7 +31000,8 @@ ${relationLabel} source location: ${declarationSite}`,
30966
31000
  // child ids, upserting each entry into the child store. The id array signal is
30967
31001
  // only touched when the resulting ids actually differ.
30968
31002
  // Returns whether the children the row points to changed — what the store asks
30969
- // a relation (see assign in array_signal_store.js).
31003
+ // a relation (see assign in array_signal_store.js). `answer`: the children come
31004
+ // from an answer, and are written as one (see upsertAnswer).
30970
31005
  // `wholeChildren` (.scopedMany()): each child received is the whole child, see
30971
31006
  // toWholeScopedChild.
30972
31007
  const createChildIdArrayUpdater = (
@@ -30975,7 +31010,7 @@ const createChildIdArrayUpdater = (
30975
31010
  idArraySignal,
30976
31011
  { wholeChildren = false } = {},
30977
31012
  ) => {
30978
- return (valueArray) => {
31013
+ return (valueArray, answer) => {
30979
31014
  const currentIdArray = idArraySignal.peek();
30980
31015
  if (!Array.isArray(valueArray)) {
30981
31016
  if (currentIdArray.length > 0) {
@@ -30997,7 +31032,9 @@ const createChildIdArrayUpdater = (
30997
31032
  } else {
30998
31033
  continue;
30999
31034
  }
31000
- const childItem = childStore.upsert(childItemProps);
31035
+ const childItem = answer
31036
+ ? childStore.upsertAnswer(childItemProps)
31037
+ : childStore.upsert(childItemProps);
31001
31038
  const childItemId = childItem[childIdKey];
31002
31039
  if (currentIdArray[idArray.length] !== childItemId) {
31003
31040
  modified = true;
@@ -38785,7 +38822,8 @@ const TRANSITION_COVERED_ATTRIBUTE = "data-navi-route-transition-covered";
38785
38822
  const TRANSITION_AREA_ATTRIBUTE = "data-navi-route-transition-area";
38786
38823
  const TRANSITION_TARGET_ATTRIBUTE = "data-navi-route-transition-target";
38787
38824
  // What a history entry remembers of the crossing that created it, in the
38788
- // entry's own state: `{ from, type, direction, duration }` (see recordCrossing).
38825
+ // entry's own state: `{ fromDepth, type, direction, duration }` (see
38826
+ // recordCrossing).
38789
38827
  const CROSSING_STATE_KEY = "jsenv_route_transition";
38790
38828
  const AREA_NAME = "navi-route-transition";
38791
38829
  // The pictures carrying the movement, among everything else it takes along —
@@ -39442,51 +39480,35 @@ const normalizeRequest = transition => {
39442
39480
  * What a TRAVERSAL asks for: the crossing it retraces.
39443
39481
  *
39444
39482
  * The entry a push creates remembers the crossing that created it (see
39445
- * recordCrossing). A back onto the page that crossing came from undoes it —
39483
+ * recordCrossing). A back onto the entry that crossing came from undoes it —
39446
39484
  * the same movement, the other way — and a forward onto an entry whose
39447
- * crossing came from the page being left plays it again as it was. Both
39485
+ * crossing came from the entry being left plays it again as it was. Both
39448
39486
  * entries are read because each side is the only one that knows its case: the
39449
39487
  * entry being LEFT says how it was reached (a back), the entry being REACHED
39450
39488
  * says how it was reached (a forward).
39451
39489
  *
39452
- * Both can be true at once — A, B, A again: leaving the second A for B is the
39453
- * way back of A → B and the way in of B → A. The entries' depths in the stack
39454
- * tell them apart; without a depth on both, a back is assumed, the traversal
39455
- * by far the most often made.
39490
+ * An entry is named by its depth in the stack, never by its url: a replace
39491
+ * keeps the entry and its state while it rewrites the address — a param of
39492
+ * the page settling, a list writing where it stands — and the crossing is
39493
+ * still between the same two entries. A crossing always comes from below the
39494
+ * entry it is recorded on, so at most one of the two cases matches.
39456
39495
  *
39457
39496
  * Answers in the shape of readNavigationRequest, with every field said, so
39458
- * that the relations have nothing left to answer for — and marked as a replay,
39459
- * because one relation still outranks it: the one written for the exact way
39460
- * travelled (see the watcher's onMove).
39497
+ * that the relations have nothing left to answer for — except the one written
39498
+ * for the exact way travelled, which outranks it (see the watcher's onMove).
39461
39499
  */
39462
- const readTraversalReplay = ({
39463
- url,
39464
- state
39465
- }, {
39466
- fromUrl,
39467
- fromState
39468
- }) => {
39469
- const to = absoluteUrl(url);
39470
- const from = absoluteUrl(fromUrl);
39471
- if (!to || !from) {
39500
+ const readTraversalReplay = (state, fromState) => {
39501
+ const depthIn = navDepthOf(state);
39502
+ const depthOut = navDepthOf(fromState);
39503
+ if (depthIn === undefined || depthOut === undefined) {
39472
39504
  return null;
39473
39505
  }
39474
39506
  const crossingIn = crossingRecordedOn(state);
39475
- const crossingOut = crossingRecordedOn(fromState);
39476
- const isForward = crossingIn !== null && crossingIn.from === from;
39477
- const isBack = crossingOut !== null && crossingOut.from === to;
39478
- if (isForward && isBack) {
39479
- const depthIn = navDepthOf(state);
39480
- const depthOut = navDepthOf(fromState);
39481
- if (depthIn !== undefined && depthOut !== undefined && depthIn > depthOut) {
39482
- return replayOf(crossingIn);
39483
- }
39484
- return replayOf(reverseCrossing(crossingOut));
39485
- }
39486
- if (isForward) {
39507
+ if (crossingIn !== null && crossingIn.fromDepth === depthOut) {
39487
39508
  return replayOf(crossingIn);
39488
39509
  }
39489
- if (isBack) {
39510
+ const crossingOut = crossingRecordedOn(fromState);
39511
+ if (crossingOut !== null && crossingOut.fromDepth === depthIn) {
39490
39512
  return replayOf(reverseCrossing(crossingOut));
39491
39513
  }
39492
39514
  return null;
@@ -39496,7 +39518,7 @@ const crossingRecordedOn = state => {
39496
39518
  return null;
39497
39519
  }
39498
39520
  const crossing = state[CROSSING_STATE_KEY];
39499
- if (!crossing || typeof crossing.from !== "string") {
39521
+ if (!crossing || typeof crossing.fromDepth !== "number") {
39500
39522
  return null;
39501
39523
  }
39502
39524
  return crossing;
@@ -39506,8 +39528,7 @@ const crossingRecordedOn = state => {
39506
39528
  * Which way the history says a navigation goes, for a default that reads it
39507
39529
  * (see defineRouteDefaultTransition): a push is one level deeper, a traversal
39508
39530
  * goes where the depth of the entry it lands on puts it — back when that is
39509
- * not known, as in readTraversalReplay —, and anything else (a replace) stays
39510
- * on its level: null.
39531
+ * not known —, and anything else (a replace) stays on its level: null.
39511
39532
  */
39512
39533
  const readHistoryDirection = (navigationType, {
39513
39534
  state,
@@ -39558,8 +39579,25 @@ const replayOf = ({
39558
39579
  type,
39559
39580
  typeSaid: true,
39560
39581
  duration,
39561
- direction,
39562
- replay: true
39582
+ direction
39583
+ };
39584
+ };
39585
+
39586
+ // What a press asked, field by field, over the crossing its traversal
39587
+ // retraces: a press naming only a direction keeps the movement retraced and
39588
+ // turns it round, as it would a relation's (see readNavigationRequest).
39589
+ const layerRequest = (request, replay) => {
39590
+ if (!replay) {
39591
+ return request;
39592
+ }
39593
+ if (!request) {
39594
+ return replay;
39595
+ }
39596
+ return {
39597
+ type: request.typeSaid ? request.type : replay.type,
39598
+ typeSaid: true,
39599
+ duration: request.duration === undefined ? replay.duration : request.duration,
39600
+ direction: request.direction === undefined ? replay.direction : request.direction
39563
39601
  };
39564
39602
  };
39565
39603
 
@@ -39645,9 +39683,10 @@ const rebuildWatcher = () => {
39645
39683
  // reverse of a pair, a page from anywhere — and not over a relation
39646
39684
  // written for this exact way: that line is the author's one tool for
39647
39685
  // breaking reciprocity, and the back button is the way back it has to
39648
- // reach. A request made by a link or a navTo() is never dropped: it is
39649
- // about this one crossing, and a written relation is about every one.
39650
- const request = navigationRequest && navigationRequest.replay && found && found.written ? null : navigationRequest;
39686
+ // reach. A request made by a link or a navTo() is never dropped, not even
39687
+ // when its push was taken as a traversal: it is about this one crossing,
39688
+ // and a written relation is about every one.
39689
+ const request = layerRequest(navigationRequest, found && found.written ? null : navigationReplay);
39651
39690
  if (!found && !request) {
39652
39691
  // No relation says anything about these two and this navigation asked
39653
39692
  // for nothing: they are side by side, and silence is the fact — not a
@@ -39698,6 +39737,10 @@ const rebuildWatcher = () => {
39698
39737
  // start of every navigation, so they are always about the latest one.
39699
39738
  let defaultTransition = null;
39700
39739
  let navigationRequest = null;
39740
+ // The crossing a traversal retraces (see readTraversalReplay), kept apart from
39741
+ // what was asked: a relation written for the exact way drops the one and never
39742
+ // the other.
39743
+ let navigationReplay = null;
39701
39744
  let navigationUrl = null;
39702
39745
  // Where the document stands as the navigation begins — the other end of the
39703
39746
  // crossing, and what lets the NEXT navigation be recognised as this one's way
@@ -39705,6 +39748,9 @@ let navigationUrl = null;
39705
39748
  // on a browser without the Navigation API is announced from "popstate", where
39706
39749
  // the address has already moved and location would answer with the destination.
39707
39750
  let navigationFromUrl = null;
39751
+ // Where the entry being left stands in the stack: what a push's crossing names
39752
+ // as the entry it came from (see recordCrossing).
39753
+ let navigationFromDepth;
39708
39754
  let navigationAnimated = false;
39709
39755
  // "push", "replace", "traverse", … — a push is the one navigation that creates
39710
39756
  // the entry a crossing is recorded on.
@@ -39733,17 +39779,18 @@ observeBeforeRouting(details => {
39733
39779
  // Read before the document state moves, so the state peeked is the entry
39734
39780
  // being left.
39735
39781
  const fromState = documentStateSignal.peek();
39736
- // A traversal has no element and no call to ask anything: what it asks is
39737
- // the crossing it retraces — reversed for a back, as it was for a forward.
39738
- navigationRequest = navigationType === "traverse" ? readTraversalReplay(details, {
39739
- fromUrl: navigationFromUrl,
39740
- fromState
39741
- }) : readNavigationRequest(details);
39782
+ navigationFromDepth = navDepthOf(fromState);
39783
+ // A traversal is asked something only when a push to the entry next door
39784
+ // was taken as one, and then carries its press (see the integrations). What
39785
+ // else it asks is the crossing it retraces — reversed for a back, as it was
39786
+ // for a forward.
39787
+ navigationRequest = readNavigationRequest(details);
39788
+ navigationReplay = navigationType === "traverse" ? readTraversalReplay(details.state, fromState) : null;
39742
39789
  navigationHistoryDirection = defaultTransition && defaultTransition.readsHistory ? readHistoryDirection(navigationType, {
39743
39790
  state: details.state,
39744
39791
  fromState
39745
39792
  }) : undefined;
39746
- if (relations.length === 0 && !defaultTransition && !navigationRequest) {
39793
+ if (relations.length === 0 && !defaultTransition && !navigationRequest && !navigationReplay) {
39747
39794
  return;
39748
39795
  }
39749
39796
  holdRenderingForRouting();
@@ -39756,16 +39803,19 @@ observeBeforeRouting(details => {
39756
39803
  }
39757
39804
  });
39758
39805
  observeAfterRouting(() => {
39759
- const request = navigationRequest;
39806
+ const request = layerRequest(navigationRequest, navigationReplay);
39760
39807
  const url = navigationUrl;
39761
39808
  const fromUrl = navigationFromUrl;
39809
+ const fromDepth = navigationFromDepth;
39762
39810
  const type = navigationType;
39763
39811
  const historyDirection = navigationHistoryDirection;
39764
39812
  // Read here and dropped here: a request answers for the navigation it was
39765
39813
  // made on, and the next one is back to the relations.
39766
39814
  navigationRequest = null;
39815
+ navigationReplay = null;
39767
39816
  navigationUrl = null;
39768
39817
  navigationFromUrl = null;
39818
+ navigationFromDepth = undefined;
39769
39819
  navigationType = null;
39770
39820
  navigationHistoryDirection = undefined;
39771
39821
  // A default reading the history plays between two pages a level apart: not
@@ -39804,7 +39854,7 @@ observeAfterRouting(() => {
39804
39854
  if (type === "push") {
39805
39855
  recordCrossing({
39806
39856
  url,
39807
- fromUrl,
39857
+ fromDepth,
39808
39858
  decision: navigationDecision
39809
39859
  });
39810
39860
  }
@@ -39822,15 +39872,14 @@ observeAfterRouting(() => {
39822
39872
  // reached by one keeps the crossing that led to where it stands.
39823
39873
  const recordCrossing = ({
39824
39874
  url,
39825
- fromUrl,
39875
+ fromDepth,
39826
39876
  decision
39827
39877
  }) => {
39828
- if (!decision) {
39878
+ if (!decision || fromDepth === undefined) {
39829
39879
  return;
39830
39880
  }
39831
39881
  const to = absoluteUrl(url);
39832
- const from = absoluteUrl(fromUrl);
39833
- if (!to || !from) {
39882
+ if (!to) {
39834
39883
  return;
39835
39884
  }
39836
39885
  if (documentUrlSignal.peek() !== to) {
@@ -39838,7 +39887,7 @@ const recordCrossing = ({
39838
39887
  return;
39839
39888
  }
39840
39889
  const crossing = {
39841
- from
39890
+ fromDepth
39842
39891
  };
39843
39892
  if (decision.type !== undefined) {
39844
39893
  crossing.type = decision.type;
@@ -44871,15 +44920,16 @@ const armOutsidePressClose = (
44871
44920
  * follows a new `defaultValue` on its own (see followDefaultValue in
44872
44921
  * ui_state_controller.js) — and this is what throws that edit away.
44873
44922
  *
44874
- * On top of whichever value is picked, intent on the anchor warms the content:
44875
- * a pointer entering the popup's anchor, or focus landing in it, builds the
44876
- * content ahead of the click that will open it. Deferring the build to the
44877
- * opening puts its whole cost in the frame right after the click — the frame
44878
- * where a delay is felt hardest — while the ~100-300ms between hovering a
44879
- * trigger and pressing it are free. The warming render starts at the frame
44880
- * after the one that follows the intent: nothing here needs the content in the
44881
- * DOM before the click, only before the open that follows it, and a build
44882
- * started at once would hold back the frame that shows the hover or the focus.
44923
+ * On top of whichever value is picked, intent warms the content: a press on
44924
+ * what opens the popup, and but for "while-opened" a pointer entering the
44925
+ * popup's anchor or focus landing in it, builds the content ahead of the click
44926
+ * that will open it. Deferring the build to the opening puts its whole cost in
44927
+ * the frame right after the click — the frame where a delay is felt hardest —
44928
+ * while the ~100-300ms between hovering a trigger and pressing it are free.
44929
+ * The warming render starts at the frame after the one that follows the
44930
+ * intent: nothing here needs the content in the DOM before the click, only
44931
+ * before the open that follows it, and a build started at once would hold
44932
+ * back the frame that shows the hover or the focus.
44883
44933
  *
44884
44934
  * Under a finger, `pointerover` is not an intent yet: it arrives with the
44885
44935
  * `pointerdown` of every touch, the ones starting a scroll included, and at
@@ -44899,7 +44949,12 @@ const armOutsidePressClose = (
44899
44949
  * press does not open (`data-open-on`: a picker opened by a hold, or only by a
44900
44950
  * command) says nothing; the keyboard still opens it, so focus there warms.
44901
44951
  *
44902
- * "while-opened" content is warmed by an opening press, never by a hover or a
44952
+ * An opening press warms whatever the value. A popup with no anchor — a sheet,
44953
+ * a side panel, a layer opened by a button — has nothing else saying it is
44954
+ * about to open, and under a finger the ~100ms a tap is held are as free as a
44955
+ * hover.
44956
+ *
44957
+ * "while-opened" content is warmed by that press only, never by a hover or a
44903
44958
  * focus. That mode promises content built fresh for the gesture that opens
44904
44959
  * it, and mounted only while that gesture and its opening last: callers lean
44905
44960
  * on it (several pickers sharing one set of content ids, because only one
@@ -44916,15 +44971,16 @@ const armOutsidePressClose = (
44916
44971
  * The press builds the content when it starts, a touch once it has been held
44917
44972
  * as above: the announcement carries the pointerdown that made it. A press
44918
44973
  * released before its build leaves the building to the open its click asks
44919
- * for. The press ending without an open throws the content away — its click
44920
- * reaching the document with the popup still closed, no
44974
+ * for. A press ending without an open keeps what it built, as a hover does:
44975
+ * the next press finds it there. Under "while-opened" it throws the content
44976
+ * away — its click reaching the document with the popup still closed, no
44921
44977
  * click coming after the release, the browser taking the gesture
44922
44978
  * (`pointercancel`), or the next press starting, before any of its own
44923
44979
  * handlers run: one press at a time holds a content. A keyboard opening builds
44924
44980
  * at open time. A "while-opened" popup with an `onOpen` is not warmed by the
44925
44981
  * press: that callback runs before the content is built (see
44926
44982
  * open_controller.js), and a content seeding itself from what it writes must
44927
- * be built after it. Hover and focus warming, for the other values, builds
44983
+ * be built after it. For the other values, a press, a hover and a focus build
44928
44984
  * ahead of `onOpen` all the same.
44929
44985
  */
44930
44986
 
@@ -44961,11 +45017,14 @@ const cancelIdle = (id) =>
44961
45017
  ? cancelIdleCallback(id)
44962
45018
  : clearTimeout(id);
44963
45019
 
44964
- // How long a finger rests on an anchor before it warms it: past the ~35ms at
44965
- // which the browser takes a touch moving at reading speed for a scroll, short
44966
- // of the ~100ms a tap is held, so a deliberate tap is warmed before its
44967
- // release.
44968
- const TOUCH_WARM_HOLD_MS = 80;
45020
+ // How long a finger rests on an anchor or an opener before it warms it: just
45021
+ // past the ~35ms at which the browser takes a touch moving at reading speed
45022
+ // for a scroll. A tap is held ~100ms, and the hold ends at a frame, which a
45023
+ // phone's busy main thread runs late: at 80ms a 100ms tap on 2v2's settings
45024
+ // wheel (CPU 6x) lifted before that frame 5 times out of 7 and was warmed by
45025
+ // nothing. The price is a scroll that starts slowly enough to be taken past
45026
+ // 40ms: it builds what the open would have built.
45027
+ const TOUCH_WARM_HOLD_MS = 40;
44969
45028
 
44970
45029
  const usePopupContentMount = (
44971
45030
  openController,
@@ -45128,9 +45187,9 @@ const usePopupContentMount = (
45128
45187
  anchorElement.removeEventListener("focusin", onFocusIn);
45129
45188
  };
45130
45189
  }, [contentMounted, anchor, mount]);
45131
- // Warm on an opening press, for "while-opened" (see the top comment). Told
45132
- // by the popup's own element, which hears the announcement where it hears
45133
- // the opening request: `onnavi_open_press` beside each `onnavi_request_open`.
45190
+ // Warm on an opening press (see the top comment). Told by the popup's own
45191
+ // element, which hears the announcement where it hears the opening request:
45192
+ // `onnavi_open_press` beside each `onnavi_request_open`.
45134
45193
  const stopPressWarmRef = useRef(null);
45135
45194
  openController.onOpeningPress =
45136
45195
  mount === "while-opened"
@@ -45156,7 +45215,21 @@ const usePopupContentMount = (
45156
45215
  stopWatching();
45157
45216
  };
45158
45217
  }
45159
- : null;
45218
+ : (openPressEvent) => {
45219
+ // Two fingers on two of its openers are one build.
45220
+ if (contentMountedRef.current || stopPressWarmRef.current) {
45221
+ return;
45222
+ }
45223
+ stopPressWarmRef.current = requestBuildOnPress(openPressEvent, {
45224
+ onBuild: () => {
45225
+ stopPressWarmRef.current = null;
45226
+ setContentMounted(true);
45227
+ },
45228
+ onGiveUp: () => {
45229
+ stopPressWarmRef.current = null;
45230
+ },
45231
+ });
45232
+ };
45160
45233
  useEffect(() => {
45161
45234
  return () => {
45162
45235
  stopPressWarmRef.current?.();
@@ -61648,8 +61721,8 @@ a.navi_button {
61648
61721
  outline-color: var(--button-outline-color);
61649
61722
  outline-offset: var(--button-outline-offset);
61650
61723
  transition-property: transform;
61651
- transition-duration: .15s;
61652
- transition-timing-function: cubic-bezier(.4, 0, .2, 1);
61724
+ transition-duration: .25s;
61725
+ transition-timing-function: cubic-bezier(.2, 0, 0, 1);
61653
61726
  position: relative;
61654
61727
 
61655
61728
  & .navi_button_shadow {
@@ -61692,7 +61765,7 @@ a.navi_button {
61692
61765
 
61693
61766
  &[data-pressed] {
61694
61767
  & .navi_button_content {
61695
- transition-duration: 0s;
61768
+ transition-duration: .1s;
61696
61769
  transform: scale(.9);
61697
61770
  }
61698
61771
  }
@@ -73769,10 +73842,12 @@ const giveUpOnControlsHoldingPopup = (controlsHolding, reason) => {
73769
73842
  * itself on it (navi_open_press), --navi-toggle reads its aria-expanded. The
73770
73843
  * popup proper — its open controller, its box, its backdrop, its focus and
73771
73844
  * pseudo-class wiring — is built at the first request: an open, a press about
73772
- * to open it, its signal or its `open` saying open. A list whose rows each hold
73773
- * a few popups mounts hundreds of them, and would pay for every one at every
73774
- * mount, for openings that mostly never come. Built once, the popup stays: the
73775
- * next opening finds it there.
73845
+ * to open it, its signal or its `open` saying open. A press builds what the
73846
+ * popup holds along with it, as it warms the content of a popup already built
73847
+ * (see popup_content_mount.js): the click that follows only opens. A list
73848
+ * whose rows each hold a few popups mounts hundreds of them, and would pay for
73849
+ * every one at every mount, for openings that mostly never come. Built once,
73850
+ * the popup stays: the next opening finds it there.
73776
73851
  *
73777
73852
  * Nothing about where the popup goes is needed at rest: a popover reads its
73778
73853
  * anchor at the opening, from the request.
@@ -76168,6 +76243,11 @@ const DialogStandIn = props => {
76168
76243
  ...props,
76169
76244
  ref: dialogRef,
76170
76245
  defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
76246
+ // Built by a press on what opens it: the content comes with it, and
76247
+ // stays, as a "from-first-open" content does once built (see
76248
+ // popup_stand_in.js).
76249
+ ,
76250
+ mount: builtReason === "press" ? "always" : props.mount
76171
76251
  });
76172
76252
  }
76173
76253
  return jsx("dialog", {
@@ -78133,6 +78213,11 @@ const PopoverStandIn = props => {
78133
78213
  ...props,
78134
78214
  ref: popoverRef,
78135
78215
  defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
78216
+ // Built by a press on what opens it: the content comes with it, and
78217
+ // stays, as a "from-first-open" content does once built (see
78218
+ // popup_stand_in.js).
78219
+ ,
78220
+ mount: builtReason === "press" ? "always" : props.mount
78136
78221
  });
78137
78222
  }
78138
78223
  return jsx("div", {
@@ -89179,6 +89264,13 @@ const useRunItems = (store, {
89179
89264
  if (askEnd > runEnd - 1) {
89180
89265
  askEnd = runEnd - 1;
89181
89266
  }
89267
+ // Only the item before the window's first one says whether that one opens
89268
+ // its group (see whereGroupContinues). Asked for with it, the first item
89269
+ // lands with its group known, not guessed: a wrong guess changes the room
89270
+ // above it, label and scroll inset, once the item before comes.
89271
+ if (groupBy && askStart === windowFrom && windowFrom > runStart && !store.holds(rankOf(windowFrom - 1)) && (!groupOfMissing || groupOfMissing(windowFrom - 1) === undefined)) {
89272
+ askStart = windowFrom - 1;
89273
+ }
89182
89274
  }
89183
89275
  // The item the missing ones hang from, when there is one: a source paginating
89184
89276
  // by cursor ("the 50 before this one") needs an item to count from, and an