@jsenv/navi 0.29.418 → 0.29.420

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +584 -196
  3. package/dist/dev/jsenv_navi.js.map +35 -35
  4. package/dist/jsenv_navi.js +563 -187
  5. package/dist/jsenv_navi.js.map +35 -35
  6. package/docs/AI_INSTRUCTIONS.md +177 -261
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +169 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +39 -43
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +115 -174
  16. package/docs/drag_to_travel.md +10 -9
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +66 -81
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +6 -4
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -482
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -335
  37. package/docs/state_binding.md +2 -1
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -426,6 +426,22 @@ const whenRenderingResumes = (callback) => {
426
426
  renderingHold.waiting.push(callback);
427
427
  };
428
428
 
429
+ // After the render the change being applied has queued, for what depends on
430
+ // the page that change puts on screen — an offset given back to a page is
431
+ // clamped to whatever page the document holds when it is written. Under a hold
432
+ // that render runs when the hold is released, and this right after it.
433
+ // Otherwise the render is Preact's own, queued in a microtask, and every
434
+ // microtask has run by the next frame — before it paints.
435
+ const whenPageRendered = (callback) => {
436
+ if (renderingHold) {
437
+ renderingHold.waiting.push(callback);
438
+ return;
439
+ }
440
+ requestAnimationFrame(() => {
441
+ callback();
442
+ });
443
+ };
444
+
429
445
  // The hold a navigation takes on its way in — from before its first write,
430
446
  // because by the time a route announces that it matches, Preact has already
431
447
  // been told and the render is queued; a hold taken then is a hold taken too
@@ -800,7 +816,10 @@ const writeTarget = (element) => {
800
816
  silencedElement.id = addressTargetId;
801
817
  }
802
818
  window.history.replaceState(historyState, null, address);
803
- if (entry && navigation.currentEntry) {
819
+ // Only a state the entry had is put back: an entry nobody wrote one on
820
+ // (via_history.js never does) has nothing to lose, and updateCurrentEntry
821
+ // cannot say "none" — `state` is a required member, and undefined throws.
822
+ if (entry && navigation.currentEntry && entryState !== undefined) {
804
823
  navigation.updateCurrentEntry({ state: entryState });
805
824
  }
806
825
  targetWriteInProgress = false;
@@ -3675,6 +3694,8 @@ const actionWeakMap = new WeakMap();
3675
3694
  * than a first load (data_states.md). Called at the start of every run whose
3676
3695
  * value is undefined; the answer replaces it as usual, and `undefined` means
3677
3696
  * "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).
3678
3699
  * @param {{ ms?: number, max?: number }} [rootOptions.keep] - how long an
3679
3700
  * answer stays good once it has landed. Without it an answer lives exactly as
3680
3701
  * long as something references it: the screen that asked goes away, and
@@ -4342,7 +4363,14 @@ const createAction = (callback, rootOptions = {}) => {
4342
4363
  if (rejected) {
4343
4364
  return failRun(rejectedValue);
4344
4365
  }
4345
- return onRunEnd();
4366
+ // Turning the answer into a value can throw as well — a resource
4367
+ // write answering nothing to upsert. That fails the run, as the
4368
+ // catch below does when the callback answered synchronously.
4369
+ try {
4370
+ return onRunEnd();
4371
+ } catch (e) {
4372
+ return failRun(e);
4373
+ }
4346
4374
  });
4347
4375
  } catch (e) {
4348
4376
  return failRun(e);
@@ -4385,6 +4413,7 @@ const createAction = (callback, rootOptions = {}) => {
4385
4413
 
4386
4414
  const privateProperties = {
4387
4415
  valueInitial,
4416
+ provisionalValue,
4388
4417
 
4389
4418
  performRun,
4390
4419
  performReset,
@@ -4416,7 +4445,8 @@ const createAction = (callback, rootOptions = {}) => {
4416
4445
  * This enables live updates - for example, performing an HTTP GET request every time
4417
4446
  * a list of filters changes, providing real-time results without user interaction.
4418
4447
  * @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
4419
- * By default (true), the proxy carries over the previous target's value and error into the new action.
4448
+ * 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.
4420
4450
  * This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
4421
4451
  * new action completes, not when it starts loading. Code that needs to distinguish loading state can still
4422
4452
  * check `action.runningState`, while code that just reads `action.data` always sees the most recent
@@ -4463,8 +4493,14 @@ const createActionProxyFromSignal = (
4463
4493
  let currentActionPrivateProperties = getActionPrivateProperties(action);
4464
4494
  let actionTargetPreviousWeakRef = null;
4465
4495
 
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.
4500
+ const inheritsAnswer =
4501
+ inheritData && !getActionPrivateProperties(action).provisionalValue;
4466
4502
  const createTarget = (params) => {
4467
- if (inheritData) {
4503
+ if (inheritsAnswer) {
4468
4504
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
4469
4505
  const previousTarget = previousActionTarget || action;
4470
4506
  return action.bindParams(params, {
@@ -4577,6 +4613,12 @@ const createActionProxyFromSignal = (
4577
4613
  return signalProxy;
4578
4614
  };
4579
4615
 
4616
+ // Params bound onto this binding make the action bound to both — these over
4617
+ // the ones it follows, as an instance's go over its action's — following the
4618
+ // signals of both: what a control does with the action it is given, and
4619
+ // actionRunEffect. Kept per params like the action's own bindings, since a
4620
+ // control binds its action again at every render.
4621
+ const bindingOnProxyWeakMap = createJsValueWeakMap();
4580
4622
  Object.assign(actionProxy, {
4581
4623
  isAction: true,
4582
4624
  isProxy: true,
@@ -4601,14 +4643,27 @@ const createActionProxyFromSignal = (
4601
4643
  });
4602
4644
  return currentAction;
4603
4645
  },
4604
- bindParams: () => {
4605
- throw new Error(
4606
- `bindParams() is not supported on action proxies, use the underlying action instead`,
4646
+ bindParams: (newParamsOrSignal, options) => {
4647
+ const existingBinding = bindingOnProxyWeakMap.get(newParamsOrSignal);
4648
+ if (existingBinding) {
4649
+ return existingBinding;
4650
+ }
4651
+ const binding = action.bindParams(
4652
+ computed(() =>
4653
+ mergeActionParams(
4654
+ paramsSignal.value,
4655
+ readParamsHandedToBind(newParamsOrSignal),
4656
+ ),
4657
+ ),
4658
+ options,
4607
4659
  );
4660
+ bindingOnProxyWeakMap.set(newParamsOrSignal, binding);
4661
+ return binding;
4608
4662
  },
4609
4663
  replaceParams: null, // Will be set below
4610
4664
  toString: () => callSourceSignal.peek(), // peek: see the action's toString
4611
4665
  meta: action.meta,
4666
+ debug: action.debug,
4612
4667
 
4613
4668
  paramsSignal: proxyParamsSignal,
4614
4669
  paramsSettlingSignal,
@@ -4751,6 +4806,29 @@ const generateActionCallSource = (name, params) => {
4751
4806
  return `${name}${argsString}`;
4752
4807
  };
4753
4808
 
4809
+ // What params handed to bindParams say at this moment, read as a binding reads
4810
+ // them (see _bindParams): a signal is its value, and so is each signal a plain
4811
+ // object holds at its first level. Read inside a computed, it follows them.
4812
+ const readParamsHandedToBind = (newParamsOrSignal) => {
4813
+ if (isSignal(newParamsOrSignal)) {
4814
+ return newParamsOrSignal.value;
4815
+ }
4816
+ if (!isPlainObject(newParamsOrSignal)) {
4817
+ return newParamsOrSignal;
4818
+ }
4819
+ const params = {};
4820
+ for (const key of Object.keys(newParamsOrSignal)) {
4821
+ const value = newParamsOrSignal[key];
4822
+ const valueSignal = isSignal(value)
4823
+ ? value
4824
+ : value
4825
+ ? value[SYMBOL_OBJECT_SIGNAL]
4826
+ : undefined;
4827
+ params[key] = valueSignal ? valueSignal.value : value;
4828
+ }
4829
+ return params;
4830
+ };
4831
+
4754
4832
  const isPlainObject = (obj) => {
4755
4833
  if (typeof obj !== "object" || obj === null) {
4756
4834
  return false;
@@ -4863,9 +4941,16 @@ const actionRunEffect = (
4863
4941
  }
4864
4942
  return params;
4865
4943
  });
4944
+ // Whether the getter asks for a run is read off what it returned, never off
4945
+ // the params of the instance it leads to: an action handed here holding
4946
+ // params of its own (an instance, a binding) keeps them when the getter
4947
+ // returns nothing — and nothing must still run nothing.
4948
+ let asking = Boolean(actionParamsSignal.peek());
4866
4949
  const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
4867
4950
  debounce,
4868
4951
  onChange: (actionTarget, actionTargetPrevious, { explicitRunIntent }) => {
4952
+ const askingBefore = asking;
4953
+ asking = Boolean(actionParamsSignal.peek());
4869
4954
  if (explicitRunIntent) {
4870
4955
  // The caller already issued an explicit run/rerun/prerun/reset/abort —
4871
4956
  // don't attempt to also auto-run from the params change to avoid double-runs.
@@ -4876,7 +4961,7 @@ const actionRunEffect = (
4876
4961
  }
4877
4962
  if (!actionTargetPrevious && actionTarget) {
4878
4963
  // first run
4879
- if (!actionTarget.params) {
4964
+ if (!asking) {
4880
4965
  // falsy params, don't run
4881
4966
  return;
4882
4967
  }
@@ -4892,12 +4977,12 @@ const actionRunEffect = (
4892
4977
  actionTarget
4893
4978
  ) {
4894
4979
  // params changed
4895
- if (!actionTarget.params) {
4980
+ if (!asking) {
4896
4981
  // falsy params, don't run
4897
4982
  actionTargetPrevious.abort("abortOnFalsyParams");
4898
4983
  return;
4899
4984
  }
4900
- if (!actionTargetPrevious.params) {
4985
+ if (!askingBefore) {
4901
4986
  // coming from falsy-params state: action may already be cached, avoid unnecessary rerun
4902
4987
  runUnwatched(() =>
4903
4988
  actionTarget.run({ reason: "params restored from falsy state" }),
@@ -4909,7 +4994,7 @@ const actionRunEffect = (
4909
4994
  },
4910
4995
  ...options,
4911
4996
  });
4912
- if (actionParamsSignal.peek()) {
4997
+ if (asking) {
4913
4998
  runUnwatched(() =>
4914
4999
  actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
4915
5000
  );
@@ -6981,6 +7066,28 @@ let navDepthMax = 0;
6981
7066
 
6982
7067
  const getNavDepth = () => navDepth;
6983
7068
 
7069
+ // Where an entry's state says it stands, undefined when it says nothing.
7070
+ const readNavDepthInState = (state) => {
7071
+ if (state && typeof state[NAV_DEPTH_STATE_KEY] === "number") {
7072
+ return state[NAV_DEPTH_STATE_KEY];
7073
+ }
7074
+ return undefined;
7075
+ };
7076
+
7077
+ // The state of the entry a document is loaded into, carrying its depth. One
7078
+ // that says a depth was written by this document (a reload in the middle of
7079
+ // the stack) and keeps it. One that says none was not: a url opened cold, the
7080
+ // first of the stack — or, rarely, an entry the browser made for a fragment,
7081
+ // reloaded, which 0 under-reports as above. Written into the entry by the
7082
+ // integrations, because a back that returns there reads where it stands from
7083
+ // the entry alone.
7084
+ const stateWithNavDepthOnLoad = (state) => {
7085
+ if (readNavDepthInState(state) !== undefined) {
7086
+ return state;
7087
+ }
7088
+ return { ...state, [NAV_DEPTH_STATE_KEY]: 0 };
7089
+ };
7090
+
6984
7091
  const applyNavigationToNavDepth = (navigationType, state) => {
6985
7092
  if (navigationType === "push") {
6986
7093
  navDepth++;
@@ -6988,10 +7095,7 @@ const applyNavigationToNavDepth = (navigationType, state) => {
6988
7095
  navDepthMax = navDepth;
6989
7096
  } else if (navigationType === "replace") ; else {
6990
7097
  // load, reload, traverse: the entry itself says where it stands.
6991
- const depthInState =
6992
- state && typeof state[NAV_DEPTH_STATE_KEY] === "number"
6993
- ? state[NAV_DEPTH_STATE_KEY]
6994
- : undefined;
7098
+ const depthInState = readNavDepthInState(state);
6995
7099
  if (depthInState !== undefined) {
6996
7100
  navDepth = depthInState;
6997
7101
  if (navDepth > navDepthMax) {
@@ -10524,6 +10628,18 @@ const preloadUrl = (url) => {
10524
10628
  }
10525
10629
  };
10526
10630
 
10631
+ // What a reload asks of each action bound to a route (see routeAction): the
10632
+ // action to run again, or nothing when its params ask nothing.
10633
+ const routeReloadMap = new WeakMap();
10634
+ const registerRouteReload = (route, reload) => {
10635
+ let reloadSet = routeReloadMap.get(route);
10636
+ if (!reloadSet) {
10637
+ reloadSet = new Set();
10638
+ routeReloadMap.set(route, reloadSet);
10639
+ }
10640
+ reloadSet.add(reload);
10641
+ };
10642
+
10527
10643
  const routeStatePreloadMap = new WeakMap();
10528
10644
  const registerRouteStatePreload = (route, preload) => {
10529
10645
  let preloadSet = routeStatePreloadMap.get(route);
@@ -10613,6 +10729,7 @@ This prevents cross-test pollution and ensures clean state.`,
10613
10729
  const updateRoutes = (
10614
10730
  url,
10615
10731
  {
10732
+ navigationType,
10616
10733
  isVisited = () => false,
10617
10734
  // state
10618
10735
  } = {},
@@ -10912,6 +11029,27 @@ This prevents cross-test pollution and ensures clean state.`,
10912
11029
  }
10913
11030
  }
10914
11031
 
11032
+ // A route's actions run again when their params change (see routeAction),
11033
+ // and a reload changes none: it asks again for the page one is on. So the
11034
+ // actions of every matching route are handed back, to be rerun — except
11035
+ // those whose params ask nothing.
11036
+ if (navigationType === "reload") {
11037
+ const reloadSet = new Set();
11038
+ for (const route of returnValue.matchingRouteSet) {
11039
+ const routeReloadSet = routeReloadMap.get(route);
11040
+ if (!routeReloadSet) {
11041
+ continue;
11042
+ }
11043
+ for (const reload of routeReloadSet) {
11044
+ const actionToRerun = reload();
11045
+ if (actionToRerun) {
11046
+ reloadSet.add(actionToRerun);
11047
+ }
11048
+ }
11049
+ }
11050
+ returnValue.reloadSet = reloadSet;
11051
+ }
11052
+
10915
11053
  return returnValue;
10916
11054
  };
10917
11055
 
@@ -11318,11 +11456,18 @@ const installScrollRestoration = () => {
11318
11456
  }
11319
11457
  };
11320
11458
 
11459
+ // Where the document lands is the last arrival's to say. A return waits for its
11460
+ // page (see restoreScrollPositionOnReturn), and an arrival decided meanwhile — a
11461
+ // page that sends the reader elsewhere as it mounts — must not be scrolled to
11462
+ // where the return was going.
11463
+ let arrivalCount = 0;
11464
+
11321
11465
  // Nothing to put back is not the same as putting back the top: a page arrived
11322
11466
  // at for the first time is startAtTop's business, and this must not step on it.
11323
11467
  // Whether there was anything, for a caller who has an answer of its own for the
11324
11468
  // page that has never been read.
11325
11469
  const restoreScrollPosition = (url) => {
11470
+ arrivalCount++;
11326
11471
  const position = positionByUrl.get(new URL(url, window.location.href).href);
11327
11472
  if (!position) {
11328
11473
  return false;
@@ -11336,6 +11481,28 @@ const restoreScrollPosition = (url) => {
11336
11481
  return true;
11337
11482
  };
11338
11483
 
11484
+ // A traversal back to `url`. The offset is written once the page returned to is
11485
+ // rendered: written sooner, the document still holds the page being left and
11486
+ // clamps it to that page's height. It is read at once all the same — a page
11487
+ // left taller than the one arriving is clamped by the render itself, and that
11488
+ // clamp is a scroll recorded under the url already returned to.
11489
+ const restoreScrollPositionOnReturn = (url) => {
11490
+ const arrival = ++arrivalCount;
11491
+ const position = positionByUrl.get(new URL(url, window.location.href).href);
11492
+ if (!position) {
11493
+ return;
11494
+ }
11495
+ whenPageRendered(() => {
11496
+ if (arrival !== arrivalCount) {
11497
+ return;
11498
+ }
11499
+ if (documentHoldCount) {
11500
+ return;
11501
+ }
11502
+ scrollTo(position);
11503
+ });
11504
+ };
11505
+
11339
11506
  // A page one arrives at for the first time starts at its top. Only a document
11340
11507
  // navigation does that on its own: a pushState creates its entry with whatever
11341
11508
  // scroll happened to be there, so without this the page opens at the offset of
@@ -11361,6 +11528,7 @@ const startAtTop = (url, { from } = {}) => {
11361
11528
  if (!isArrival(url, { from })) {
11362
11529
  return;
11363
11530
  }
11531
+ arrivalCount++;
11364
11532
  window.scrollTo({ top: 0, left: 0, behavior: "instant" });
11365
11533
  };
11366
11534
  const isArrival = (url, { from }) => {
@@ -11831,12 +11999,12 @@ const setupBrowserIntegrationViaHistory = ({
11831
11999
  isVisited,
11832
12000
  state,
11833
12001
  });
11834
- // Where the document lands, said by what kind of arrival this is. Both are
11835
- // waited for, and for the same two reasons: the page has to be there to be
11836
- // scrolled, and a picture taken before it would be of a page at its top
11837
- // (see rendering_hold.js, which is where the waiting happens). After the
11838
- // history has been written too, so the entry being left keeps the offset
11839
- // it is at.
12002
+ // Where the document lands, said by what kind of arrival this is. Never
12003
+ // before the picture of a transition is taken, which would be of a page
12004
+ // at its top (see rendering_hold.js), and after the history has been
12005
+ // written, so the entry being left keeps the offset it is at. A return
12006
+ // also waits for the page it returns to: an offset is clamped to whatever
12007
+ // page the document holds when it is written.
11840
12008
  //
11841
12009
  // A replace gets neither: it is the same place said differently — a param
11842
12010
  // settling, a state written — and moving the reader for it would throw
@@ -11847,7 +12015,7 @@ const setupBrowserIntegrationViaHistory = ({
11847
12015
  if (navigationType === "push") {
11848
12016
  whenRenderingResumes(() => startAtTop(url, { from: urlLeft }));
11849
12017
  } else if (navigationType === "traverse") {
11850
- whenRenderingResumes(() => restoreScrollPosition(url));
12018
+ restoreScrollPositionOnReturn(url);
11851
12019
  }
11852
12020
  executeWithCleanup(
11853
12021
  () => allResult,
@@ -12082,11 +12250,12 @@ const setupBrowserIntegrationViaHistory = ({
12082
12250
  const init = () => {
12083
12251
  const url = window.location.href;
12084
12252
  const stateOnEntry = window.history.state;
12085
- const state = dropGeneratedIdKeys(stateOnEntry);
12253
+ const state = stateWithNavDepthOnLoad(dropGeneratedIdKeys(stateOnEntry));
12086
12254
  if (state !== stateOnEntry) {
12087
- // The entry itself has to lose them too, not just the document state:
12088
- // getDocumentState() reads the entry back, and every state write copies
12089
- // what it finds there onto the next one.
12255
+ // The entry itself has to carry it, not just the document state:
12256
+ // getDocumentState() reads the entry back, every state write copies what
12257
+ // it finds there onto the next one, and a back returning here reads its
12258
+ // depth from it.
12090
12259
  writeHistoryEntry("replace", state, url);
12091
12260
  }
12092
12261
  handleRoutingTask(url, {
@@ -12256,7 +12425,6 @@ const applyRouting = (
12256
12425
  url,
12257
12426
  {
12258
12427
  globalAbortSignal,
12259
- abortSignal,
12260
12428
  // state
12261
12429
  navigationType,
12262
12430
  isVisited,
@@ -12268,45 +12436,36 @@ const applyRouting = (
12268
12436
  // likely because code does not uses routing at all
12269
12437
  return {};
12270
12438
  }
12271
- const {
12272
- loadSet,
12273
- reloadSet,
12274
- abortSignalMap,
12275
- routeLoadRequestedMap,
12276
- activeRouteSet,
12277
- } = updateRoutes(url, {
12439
+ // The routes' actions follow the address by themselves (see routeAction):
12440
+ // what a navigation leaves to the routing is a reload, the one that asks
12441
+ // them again with the address unchanged.
12442
+ const { reloadSet } = updateRoutes(url, {
12278
12443
  navigationType,
12279
12444
  isVisited,
12280
12445
  // state,
12281
12446
  });
12282
- if (
12283
- (!loadSet || loadSet.size === 0) &&
12284
- (!reloadSet || reloadSet.size === 0)
12285
- ) {
12447
+ if (!reloadSet || reloadSet.size === 0) {
12286
12448
  return {
12287
12449
  allResult: undefined,
12288
12450
  requestedResult: undefined,
12289
- activeRouteSet: new Set(),
12290
12451
  };
12291
12452
  }
12292
12453
  const updateActionsResult = updateActions({
12293
12454
  globalAbortSignal,
12294
- abortSignal,
12295
- runSet: loadSet,
12455
+ // Not the navigation's own signal: the next navigation aborts that one, and
12456
+ // an action whose route still matches there would be left aborted — its
12457
+ // params do not change, so nothing would run it again.
12458
+ abortSignal: new AbortController().signal,
12296
12459
  rerunSet: reloadSet,
12297
- abortSignalMap,
12298
12460
  reason,
12299
- isReplace: navigationType === "replace",
12300
12461
  });
12301
12462
  const { allResult, runningActionSet } = updateActionsResult;
12302
12463
  const pendingTaskNameArray = [];
12303
- for (const [route, routeAction] of routeLoadRequestedMap) {
12304
- if (runningActionSet.has(routeAction)) {
12305
- pendingTaskNameArray.push(`${route.relativeUrl} -> ${routeAction.name}`);
12306
- }
12464
+ for (const runningAction of runningActionSet) {
12465
+ pendingTaskNameArray.push(runningAction.name);
12307
12466
  }
12308
12467
  routingWhile(() => allResult, pendingTaskNameArray);
12309
- return { ...updateActionsResult, activeRouteSet };
12468
+ return updateActionsResult;
12310
12469
  };
12311
12470
 
12312
12471
  const setupBrowserIntegration =
@@ -12388,6 +12547,12 @@ const stopLoad = (reason = "stopLoad() called") => {
12388
12547
  browserIntegration.stop(reason);
12389
12548
  }
12390
12549
  };
12550
+ /**
12551
+ * Asks again for the page one is on, without reloading the document: the
12552
+ * actions of the routes matching the url run again (see routeAction), one that
12553
+ * failed included — what a "Retry" under a page that could not load calls.
12554
+ * The address, the history and the scroll stay as they are.
12555
+ */
12391
12556
  const reload = browserIntegration.reload;
12392
12557
  /**
12393
12558
  * Go back to the screen this document came from.
@@ -12410,11 +12575,13 @@ const reload = browserIntegration.reload;
12410
12575
  * way a screen closed over a url keeps what was written to the url while it
12411
12576
  * was open (see useNavState's leave()).
12412
12577
  * @returns {Promise<boolean>|undefined}
12413
- * When there is something to go back to: a promise resolved once the back
12414
- * has landed and been applied (`true` — the document url and state say
12415
- * where it landed), or `false` when the traversal was preempted and landed
12416
- * nowhere. `undefined` otherwise — nothing happened, or the fallback took
12417
- * the current entry's place.
12578
+ * When there is something to go back to: a promise resolved with `true`
12579
+ * once the back has landed and been applied — the document url and state
12580
+ * then say where it landed. A back preempted by another navigation, which
12581
+ * lands nowhere, resolves `false` only under the Navigation API integration
12582
+ * (see USE_NAVIGATION_API, off today): the History API does not tell it
12583
+ * apart. `undefined` otherwise — nothing happened, or the fallback took the
12584
+ * current entry's place.
12418
12585
  */
12419
12586
  const navBack = browserIntegration.navBack;
12420
12587
  const navForward = browserIntegration.navForward;
@@ -13429,10 +13596,11 @@ const isKeyboardModality = () => keyboardNavigationUsed;
13429
13596
  {
13430
13597
  // We implement :focus and :focus-visible with enriched semantics:
13431
13598
  // an element is considered focused not only when it natively has focus, but also
13432
- // when a "focus proxy" element has focus (e.g. a read-only range input delegates
13433
- // focus to a sibling span) or when a controlling element has focus (e.g. a combobox
13434
- // input with aria-controls pointing to a listbox — the listbox should appear focused
13435
- // while the input is focused).
13599
+ // when it is a control proxy whose real control has focus (the proxy is what is
13600
+ // seen, the focus is dealt to the real control — see control_proxy.js) or when
13601
+ // a controlling element has focus (e.g. a combobox input with aria-controls
13602
+ // pointing to a listbox — the listbox should appear focused while the input is
13603
+ // focused).
13436
13604
  //
13437
13605
  // We intentionally reuse the native :focus / :focus-visible names rather than
13438
13606
  // introducing new navi-specific pseudo-classes (e.g. :-navi-focus). This is a
@@ -13524,7 +13692,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
13524
13692
 
13525
13693
  // Returns true when el holds focus indirectly — either because a controlling
13526
13694
  // element (aria-controls) has focus, or because el is a proxy whose target
13527
- // is itself controlled by a focused element.
13695
+ // has focus or is itself controlled by a focused element.
13528
13696
  const hasIndirectFocus = (el, { requireFocusVisible = false } = {}) => {
13529
13697
  // No ring inheritance without a keyboard: an editable target draws its own
13530
13698
  // ring on any focus (see isMatchingFocusVisible), but propagating that ring
@@ -15063,7 +15231,11 @@ const createOpenController = (
15063
15231
  const applyChange = () => {
15064
15232
  // Recorded with the change itself: what the DOM shows is what a render
15065
15233
  // landing between the ask and the picture must draw (see `openedInDom`).
15066
- controller.openedInDom = opened;
15234
+ // An opening records it inside the change, once its content is built
15235
+ // (see open() below).
15236
+ if (!opened) {
15237
+ controller.openedInDom = false;
15238
+ }
15067
15239
  change();
15068
15240
  };
15069
15241
  const { transitionChange } = controller;
@@ -15343,15 +15515,17 @@ const createOpenController = (
15343
15515
  controller.mountContent?.();
15344
15516
  // Only now — after the content has been built, before openEffect
15345
15517
  // shows it. Dialog/Popover recompute aria-expanded and navi-hidden
15346
- // from this flag on every render, and mountContent above renders
15518
+ // from `openedInDom` on every render, and mountContent above renders
15347
15519
  // synchronously: flipping it any earlier commits an already-open DOM
15348
15520
  // (aria-expanded "true", navi-hidden gone) before openEffect has run
15349
15521
  // a single statement, so the "closed" frame it pins to transition
15350
15522
  // from is in fact the open one and the entrance animation has
15351
- // nothing to play. It also gives the content it just built the
15352
- // opening it is documented to observe — mounted while the popup
15353
- // reads as closed, told it opened right after (see
15354
- // popup_content_mount.js and use_displayed_layout_effect.js).
15523
+ // nothing to play — on every first opening that builds its content.
15524
+ // It also gives the content it just built the opening it is
15525
+ // documented to observe — mounted while the popup reads as closed,
15526
+ // told it opened right after (see popup_content_mount.js and
15527
+ // use_displayed_layout_effect.js).
15528
+ controller.openedInDom = true;
15355
15529
  controller.opened = true;
15356
15530
  // Which press it opened during, so the release of that press is not
15357
15531
  // read as somebody dismissing it (see openedDuringThisPress).
@@ -15760,7 +15934,14 @@ const useOpenPropsEffectOnOpenController = (
15760
15934
  lastRunOpenRef.current = open;
15761
15935
 
15762
15936
  if (isFirstRun) {
15763
- const mountOpenReason = open || defaultOpen;
15937
+ // "interaction" is said even over what the caller holds reading open: a
15938
+ // popup built by the request that opens it — a dialog standing in until
15939
+ // its signal said open, see DialogStandIn in dialog.jsx — mounts BECAUSE
15940
+ // of that opening.
15941
+ const mountOpenReason =
15942
+ open && defaultOpen === "interaction"
15943
+ ? "interaction"
15944
+ : open || defaultOpen;
15764
15945
  if (mountOpenReason) {
15765
15946
  // Whether this popup being open is something that just happened, or
15766
15947
  // something that was already true when the page appeared. "interaction"
@@ -22258,10 +22439,11 @@ const swipeTypeOf = (axis, pulled) => {
22258
22439
  * insists.
22259
22440
  *
22260
22441
  * So the interaction stays declared and says "refuse" in place of what it does.
22261
- * The threshold is the same one — a mouse travelling, a finger holding still, the
22262
- * first pixel inside a `data-drag-on-contact` — and at the instant the grab would
22263
- * have been acquired there is none: nothing translates, no copy is made, no
22264
- * release is answered. `refuse` is that instant.
22442
+ * The threshold is the same one — a mouse travelling a few pixels, a finger
22443
+ * holding still, or travelling those same few pixels inside a
22444
+ * `data-drag-on-contact` — and at the instant the grab would have been
22445
+ * acquired there is none: nothing translates, no copy is made, no release is
22446
+ * answered. `refuse` is that instant.
22265
22447
  *
22266
22448
  * It walks no axis, so it takes none: a box that travels and a surface that pans
22267
22449
  * both step over an element that is refusing, and a gesture starting on a locked
@@ -25978,8 +26160,9 @@ const createResourceLifecycleManager = () => {
25978
26160
  }
25979
26161
 
25980
26162
  // scopedMany auto-dependency: only rerun parent singular GET on child POST,
25981
- // and only when the parent GET previously returned the sub-resource embedded
25982
- // inside its response (detected via action._resultProperties).
26163
+ // only the GET of the owner the child was posted to, and only when that
26164
+ // GET previously returned the sub-resource embedded inside its response
26165
+ // (detected via action._resultProperties).
25983
26166
  // GET_MANY is excluded — a list of parents is not stale just because one
25984
26167
  // child item was added to one of them.
25985
26168
  scoped_many_effect: {
@@ -26000,6 +26183,15 @@ const createResourceLifecycleManager = () => {
26000
26183
  if (parentResource !== resourceScope) {
26001
26184
  continue;
26002
26185
  }
26186
+ if (
26187
+ !isOwnerReadBy(
26188
+ triggerResourceScope,
26189
+ triggeringAction,
26190
+ actionCandidate,
26191
+ )
26192
+ ) {
26193
+ break scoped_many_effect;
26194
+ }
26003
26195
  // Only rerun if the last GET response included the embedded sub-resource.
26004
26196
  if (
26005
26197
  !getActionResultProperties(actionCandidate)?.has(propertyName)
@@ -26116,6 +26308,27 @@ const isParamSubset = (parentParams, childParams) => {
26116
26308
  return true;
26117
26309
  };
26118
26310
 
26311
+ // Whether a GET of the owner read the item a scoped child's action was answered
26312
+ // for. The child's value names its owner first ([ownerKey, …], see scopedMany);
26313
+ // the GET's value is the id of the item it read. The key may be a unique key
26314
+ // rather than that id: both lead to the owner's one store of children. Values
26315
+ // are read off the signals: the child's action is still completing, and its
26316
+ // `value` property is only written once that batch ends.
26317
+ const isOwnerReadBy = (childResource, childAction, ownerGetAction) => {
26318
+ const childValue = childAction.valueSignal.peek();
26319
+ if (!Array.isArray(childValue)) {
26320
+ return false;
26321
+ }
26322
+ const [ownerKey] = childValue;
26323
+ const ownerId = ownerGetAction.valueSignal.peek();
26324
+ const { getChildStore } = childResource;
26325
+ if (!getChildStore) {
26326
+ return ownerKey === ownerId;
26327
+ }
26328
+ const childStore = getChildStore(ownerKey);
26329
+ return childStore !== undefined && childStore === getChildStore(ownerId);
26330
+ };
26331
+
26119
26332
  const paramScopeWeakSet = createIterableWeakSet();
26120
26333
  let paramScopeIdCounter = 0;
26121
26334
  const getParamScope = (params) => {
@@ -28693,6 +28906,18 @@ const useAsyncData = (promiseOrAction, {
28693
28906
 
28694
28907
  // ─── useAction ────────────────────────────────────────────────────────────────
28695
28908
 
28909
+ // subscribe() calls back at once with the value the signal holds; what these
28910
+ // subscriptions are for is what changes after.
28911
+ const subscribeToChanges = (signal, onChange) => {
28912
+ let initial = true;
28913
+ return signal.subscribe(value => {
28914
+ if (initial) {
28915
+ initial = false;
28916
+ return;
28917
+ }
28918
+ onChange(value);
28919
+ });
28920
+ };
28696
28921
  const LoadingContext = createContext(null);
28697
28922
  const actionPendingPromiseWeakMap = new WeakMap();
28698
28923
  const dismissedActionWeakSet = new WeakSet();
@@ -28737,6 +28962,15 @@ const useActionAsyncData = (action, {
28737
28962
  const runningState = action.runningStateSignal.peek();
28738
28963
  const [, setTick] = useState(0);
28739
28964
  const routePage = useContext(RoutePageContext);
28965
+ // What this render read of the action, for the subscriptions below to tell
28966
+ // a change that came between the render and them from none at all.
28967
+ const readRef = useRef(null);
28968
+ readRef.current = {
28969
+ runningState,
28970
+ data: action.dataSignal.peek(),
28971
+ params: action.paramsSignal.peek(),
28972
+ settling: action.paramsSettlingSignal.peek()
28973
+ };
28740
28974
  useEffect(() => {
28741
28975
  let stopOwingRender = null;
28742
28976
  const rerender = () => {
@@ -28764,7 +28998,12 @@ const useActionAsyncData = (action, {
28764
28998
  }
28765
28999
  setTick(n => n + 1);
28766
29000
  };
28767
- const unsubscribeFromRunningState = action.runningStateSignal.subscribe(state => {
29001
+ // A run under way is a new try: an error dismissed before it is not the
29002
+ // one it may end on.
29003
+ if (action.runningStateSignal.peek() === RUNNING) {
29004
+ dismissedActionWeakSet.delete(action);
29005
+ }
29006
+ const unsubscribeFromRunningState = subscribeToChanges(action.runningStateSignal, state => {
28768
29007
  if (state === RUNNING) {
28769
29008
  dismissedActionWeakSet.delete(action);
28770
29009
  }
@@ -28775,38 +29014,24 @@ const useActionAsyncData = (action, {
28775
29014
  // (a PUT upserting an item that a GET_MANY list already holds) changes the
28776
29015
  // data while this action stays COMPLETED. Subscribing here re-renders
28777
29016
  // through the same controlled path as the run state, instead of `.value`.
28778
- let dataNotificationIsInitial = true;
28779
- const unsubscribeFromData = action.dataSignal.subscribe(() => {
28780
- if (dataNotificationIsInitial) {
28781
- // subscribe() calls back synchronously with the current value
28782
- dataNotificationIsInitial = false;
28783
- return;
28784
- }
28785
- rerender();
28786
- });
29017
+ const unsubscribeFromData = subscribeToChanges(action.dataSignal, rerender);
28787
29018
  // The params say WHICH question this is, and this hook reads them: to know
28788
29019
  // there is nothing to ask for, and to start the run it owns. A binding
28789
29020
  // retargeting from no question to a question — a filter chosen, a first
28790
29021
  // character typed — is announced by nothing above: a fresh target is IDLE
28791
29022
  // holding no data, the very state the hook already sees.
28792
- let paramsNotificationIsInitial = true;
28793
- const unsubscribeFromParams = action.paramsSignal.subscribe(() => {
28794
- if (paramsNotificationIsInitial) {
28795
- paramsNotificationIsInitial = false;
28796
- return;
28797
- }
28798
- rerender();
28799
- });
29023
+ const unsubscribeFromParams = subscribeToChanges(action.paramsSignal, rerender);
28800
29024
  // A debounced binding waits before it retargets, so nothing above changes
28801
29025
  // while the delay runs — but what is on screen is already out of date.
28802
- let settlingNotificationIsInitial = true;
28803
- const unsubscribeFromParamsSettling = action.paramsSettlingSignal.subscribe(() => {
28804
- if (settlingNotificationIsInitial) {
28805
- settlingNotificationIsInitial = false;
28806
- return;
28807
- }
29026
+ const unsubscribeFromParamsSettling = subscribeToChanges(action.paramsSettlingSignal, rerender);
29027
+ // Subscribed once the browser has painted, so the action may have moved
29028
+ // since the render: a run that very render started may be over already,
29029
+ // and nothing would say so again. What the render read, against what is
29030
+ // there now — and nothing moved, nothing to draw again.
29031
+ const read = readRef.current;
29032
+ if (action.runningStateSignal.peek() !== read.runningState || action.dataSignal.peek() !== read.data || action.paramsSignal.peek() !== read.params || action.paramsSettlingSignal.peek() !== read.settling) {
28808
29033
  rerender();
28809
- });
29034
+ }
28810
29035
  return () => {
28811
29036
  unsubscribeFromRunningState();
28812
29037
  unsubscribeFromData();
@@ -34704,11 +34929,13 @@ const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
34704
34929
  * where they are going, at the movement's own pace, each animation at the rate
34705
34930
  * that gets it there in that time.
34706
34931
  *
34707
- * Where they visibly stand is computed from the clock THROUGH the easing curve,
34708
- * never read off the pseudo-elements: getComputedStyle on them answers with the
34709
- * un-animated value — the animated one lives on the compositor, where no
34710
- * reading from here reaches. It is the same trap as the playbackRate setter,
34711
- * which is why every rate below is handed over with updatePlaybackRate.
34932
+ * Where they visibly stand is computed from the clock THROUGH the easing curve:
34933
+ * a fraction of the way, whatever property carries it — getComputedStyle on
34934
+ * the pseudo-elements gives the animated value of each property, with nothing
34935
+ * saying how far along its keyframes that value is. The rates are handed over
34936
+ * with updatePlaybackRate: in Chromium, the playbackRate setter given a
34937
+ * negative rate puts the pictures at their start at once, and leaves them
34938
+ * there while the clock counts down.
34712
34939
  */
34713
34940
 
34714
34941
  const viewTransitionAnimations = () => {
@@ -35293,11 +35520,11 @@ const css$12 = /* css */`@property --navi-route-transition-leave {
35293
35520
  *
35294
35521
  * It is a real box, and it must be: what is photographed and clipped IS its
35295
35522
  * rectangle. So `display: contents` cannot be used on it — an element with no
35296
- * box is never captured, the movement plays on nothing and the browser aborts
35297
- * the transition. Give it the layout the pages need instead — it is a `Box`,
35298
- * so `flex`, `className`, `style` and the rest are there for that. An
35299
- * application that already has an element holding its pages can mark that one
35300
- * with `data-navi-route-transition-area` rather than nesting another.
35523
+ * box is never captured, and the movement then plays on nothing (navi warns
35524
+ * once the pictures are taken). Give it the layout the pages need instead —
35525
+ * it is a `Box`, so `flex`, `className`, `style` and the rest are there for
35526
+ * that. An application that already has an element holding its pages can mark
35527
+ * that one with `data-navi-route-transition-area` rather than nesting another.
35301
35528
  *
35302
35529
  * @type {import("ignore:preact").FunctionComponent<{ children?: any, [key: string]: any }>}
35303
35530
  */
@@ -35358,10 +35585,14 @@ const RouteTransitionArea = ({
35358
35585
  * length of the transition the root carries
35359
35586
  * `data-navi-route-transition-type="<type>"` next to
35360
35587
  * `data-navi-route-transition="forward"|"back"`, and the application's CSS
35361
- * defines the movement against the view transition pseudo-elements:
35588
+ * defines the movement against the view transition pseudo-elements, for
35589
+ * both groups: `root` when the document travels, `navi-route-transition`
35590
+ * when an area is marked — the root is then unnamed, and a rule written for
35591
+ * `(root)` alone never applies:
35362
35592
  *
35363
35593
  * :root[data-navi-route-transition-type="spin"][data-navi-route-transition="forward"] {
35364
- * &::view-transition-new(root) {
35594
+ * &::view-transition-new(root),
35595
+ * &::view-transition-new(navi-route-transition) {
35365
35596
  * animation-name: my-spin-in;
35366
35597
  * }
35367
35598
  * }
@@ -36019,7 +36250,7 @@ const beginTransition = ({
36019
36250
  // and the movement then plays on the document itself.
36020
36251
  const areaElements = document.querySelectorAll(`[${TRANSITION_AREA_ATTRIBUTE}]`);
36021
36252
  if (areaElements.length > 1) {
36022
- warnOnce("several-areas", `${areaElements.length} elements carry ${TRANSITION_AREA_ATTRIBUTE}. They all take the same view-transition-name, and a name belongs to one element at a time: the browser refuses EVERY view transition of the document while this holds. Mark the one element the pages live in.`);
36253
+ warnOnce("several-areas", `${areaElements.length} elements carry ${TRANSITION_AREA_ATTRIBUTE}. They all take the same view-transition-name, and a name belongs to one element at a time: the browser refuses EVERY route transition while this holds. Mark the one element the pages live in.`);
36023
36254
  }
36024
36255
  const areaElement = areaElements.length > 0 ? areaElements[0] : null;
36025
36256
  // Everything about to be photographed goes deaf to the pointer; the controls
@@ -36763,8 +36994,9 @@ const css$11 = /* css */`.navi_route_travel {
36763
36994
  * - how to go to a tab. The default REPLACES the current history entry
36764
36995
  * rather than pushing one: a swipe is how one browses a page, not a place one
36765
36996
  * aimed at, and three swipes back and forth must not bury the way out of the
36766
- * page under six entries. A tab pressed is the other case and pushes, which
36767
- * is what its <Link> already does.
36997
+ * page under six entries. A tab pressed goes the way its <Link> says —
36998
+ * `<Link replace>` for a row of tabs, so a press and a swipe towards the
36999
+ * same neighbour write the same history.
36768
37000
  *
36769
37001
  * Every other prop lands on the box itself (`id`, `data-testid`, `className`,
36770
37002
  * …), which is how one of these is named: a page can hold several — a row of
@@ -36779,10 +37011,11 @@ const css$11 = /* css */`.navi_route_travel {
36779
37011
  * gesture applies its change on release instead of dragging a picture that does
36780
37012
  * not exist.
36781
37013
  *
36782
- * While a travel plays, the rest of the page is taken as a picture too — this
36783
- * box asks for `view-transition-name: root` back for that time, so an
36784
- * application that opts the document out for its own transitions gets its rule
36785
- * back the moment the travel is over.
37014
+ * While a travel plays, the rest of the page is NOT taken as a picture: the
37015
+ * root's name is dropped for that time (`view-transition-name: none`), so what
37016
+ * stands around the box — a tab row beside it — stays live and answers a press
37017
+ * mid-travel. The application's own rule for the root applies again the moment
37018
+ * the travel is over.
36786
37019
  */
36787
37020
  const RouteTravel = ({
36788
37021
  routes: routesProp,
@@ -37971,9 +38204,9 @@ const writeCover = (edge, element, size) => {
37971
38204
 
37972
38205
  /**
37973
38206
  * Binds an action to a route: it runs when the route matches, with the params
37974
- * the effect reads off the address, and is aborted when the route is left.
37975
- * Its data, its wait and its failure are read by the page through
37976
- * `useAsyncData`.
38207
+ * the effect reads off the address, runs again on a `reload()`, and is aborted
38208
+ * when the route is left. Its data, its wait and its failure are read by the
38209
+ * page through `useAsyncData`.
37977
38210
  *
37978
38211
  * A page's CODE is declared the same way — the import is one more thing the
37979
38212
  * address asks for, started with the data rather than after it:
@@ -38017,18 +38250,21 @@ const routeAction = (
38017
38250
  ? routes[0].matchingSignal
38018
38251
  : anyMatchingRouteSignal(routes);
38019
38252
  const readParams = paramsEffect || (() => true);
38020
- const actionBoundToRoute = actionRunEffect(
38021
- action,
38022
- () => {
38023
- const matching = routeMatchingSignal.value;
38024
- const params = readParams();
38025
- if (!matching) {
38026
- return null;
38027
- }
38028
- return params;
38029
- },
38030
- options,
38031
- );
38253
+ const readParamsAsked = () => {
38254
+ const matching = routeMatchingSignal.value;
38255
+ const params = readParams();
38256
+ if (!matching) {
38257
+ return null;
38258
+ }
38259
+ return params;
38260
+ };
38261
+ const actionBoundToRoute = actionRunEffect(action, readParamsAsked, options);
38262
+ for (const route of routes) {
38263
+ // Asked of the route's params as they are, like the effect asks them.
38264
+ registerRouteReload(route, () =>
38265
+ untracked(readParamsAsked) ? actionBoundToRoute : null,
38266
+ );
38267
+ }
38032
38268
  if (prefetch) {
38033
38269
  for (const route of routes) {
38034
38270
  if (paramsEffect) {
@@ -40013,10 +40249,11 @@ const flushSyncRendering = (fn) => {
40013
40249
  * without the content, and the browser builds it in an idle moment after
40014
40250
  * load — so by the time anyone clicks, it is usually already there.
40015
40251
  *
40016
- * "while-opened" is the opposite end: content that must be rebuilt from
40017
- * scratch every time, because what it shows is read once at build time and can
40018
- * change while the popup is closed — an uncontrolled field seeded from a
40019
- * `defaultValue`, a form whose fresh state is its initial state.
40252
+ * "while-opened" is the opposite end: content rebuilt from scratch every
40253
+ * time, a form whose fresh state is its initial state. What kept content
40254
+ * carries into the next opening is an unsent edit — an untouched field
40255
+ * follows a new `defaultValue` on its own (see followDefaultValue in
40256
+ * ui_state_controller.js) — and this is what throws that edit away.
40020
40257
  *
40021
40258
  * On top of whichever value is picked, intent on the anchor warms the content:
40022
40259
  * a pointer entering the popup's anchor, or focus landing in it, builds the
@@ -40050,9 +40287,11 @@ const flushSyncRendering = (fn) => {
40050
40287
  * click coming after the release, the browser taking the gesture
40051
40288
  * (`pointercancel`), or the next press starting, before any of its own
40052
40289
  * handlers run: one press at a time holds a content. A keyboard opening builds
40053
- * at open time. A popup with an `onOpen` is never warmed: that callback runs
40054
- * before the content is built (see open_controller.js), and a content seeding
40055
- * itself from what it writes must be built after it.
40290
+ * at open time. A "while-opened" popup with an `onOpen` is not warmed by the
40291
+ * press: that callback runs before the content is built (see
40292
+ * open_controller.js), and a content seeding itself from what it writes must
40293
+ * be built after it. Hover and focus warming, for the other values, builds
40294
+ * ahead of `onOpen` all the same.
40056
40295
  */
40057
40296
 
40058
40297
 
@@ -40384,6 +40623,17 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
40384
40623
  decisionDeferredToRef.current = popupMounting;
40385
40624
  return;
40386
40625
  }
40626
+ if (
40627
+ (el.tagName === "DIALOG" || el.hasAttribute("popover")) &&
40628
+ !isAncestorOpen(el)
40629
+ ) {
40630
+ // Itself a closed <dialog> or [popover]: display:none by the UA's own
40631
+ // rules, which the markup says without asking the layout — asked, every
40632
+ // closed popup mounting would force a style recalculation, and a list
40633
+ // whose rows each hold a few mounts hundreds of them. The observer
40634
+ // below fires as for any element the ancestor hides.
40635
+ return;
40636
+ }
40387
40637
  if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
40388
40638
  // Ancestor is closed and took this element off screen with it — skip
40389
40639
  // now; the observeAncestorOpenState call below will fire once it
@@ -50805,12 +51055,13 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
50805
51055
  * does so by being NAMED, which is all the browser needs: any change played as
50806
51056
  * a view transition animates it on the same clock as everything else in that
50807
51057
  * transition. Inside a `RouteTravel` that means it follows the pages, and the
50808
- * thumb dragging them, without either of them being told about the other. It
50809
- * is named for a movement between two tabs of the row and for nothing else:
50810
- * when the pages move and the row is on one side only, the bar leaves or
50811
- * arrives with its row. For a nav made of slides (`slideContainer`) the bar
50812
- * is one element for the whole row, and it reads the travel the container
50813
- * publishes.
51058
+ * thumb dragging them, without either of them being told about the other.
51059
+ * During a route transition or a route travel it keeps the name only for a
51060
+ * movement between two tabs of the row: when the row is on one side only,
51061
+ * the bar leaves or arrives with its row. Any other view transition finds it
51062
+ * named, a picture of its own. For a nav made of slides (`slideContainer`)
51063
+ * the bar is one element for the whole row, and it reads the travel the
51064
+ * container publishes.
50814
51065
  * @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
50815
51066
  * tabs are about: each one says which slide it is (`<Link slide="…">`), the
50816
51067
  * container says which one is on screen, and pressing a tab travels there.
@@ -52642,9 +52893,9 @@ const FixedBar = ({
52642
52893
  * A page that overflows horizontally is a bug wherever it happens, and on
52643
52894
  * Chrome Android it is a catastrophic one: the layout viewport inflates to the
52644
52895
  * content and `position: fixed` centering goes with it (see
52645
- * docs/mobile_layout_pitfalls.md). The remedy there is a wrapper in
52646
- * `overflow-x: clip`, and it works — at the price of making the cause
52647
- * invisible: nothing sticks out anymore, so nothing says a fixed width, a
52896
+ * docs/safe_area.md#a-document-wider-than-the-screen). The remedy there is a
52897
+ * wrapper in `overflow-x: clip`, and it works — at the price of making the
52898
+ * cause invisible: nothing sticks out anymore, so nothing says a fixed width, a
52648
52899
  * `min-width` or an unbreakable string is still oversized. This puts the
52649
52900
  * signal back, without giving up the net.
52650
52901
  *
@@ -55322,7 +55573,8 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
55322
55573
  * `command` runs at once — the popup it sits in closes over the run, which
55323
55574
  * goes on behind. A failure then has no popup to be read in: `resetOnError`
55324
55575
  * puts the button back and the error callout is drawn on what surrounds the
55325
- * closed popup (see docs/popup_open.md#the-popup-owns-its-open-state).
55576
+ * closed popup (see
55577
+ * docs/popup_open.md#closing-when-a-button-also-runs-an-action).
55326
55578
  * @param {boolean} [actionAbortable] The person waiting may give up on this
55327
55579
  * button's action: closing the popup the run holds calls it off and goes
55328
55580
  * through, instead of being refused. For a run whose answer may never come —
@@ -65250,7 +65502,7 @@ const css$E = /* css */`
65250
65502
  the length of the movement, and the picture of the lifted node comes out
65251
65503
  empty (Chrome 153, reproduced in a bare page). The price is a page frozen
65252
65504
  and unpressable while the movement plays: under a modal wall at the
65253
- opening, and for a quarter of a second at the closing. */
65505
+ opening, and for the popup's --popup-animation-duration at the closing. */
65254
65506
  :root[data-navi-popup-lift] {
65255
65507
  /* The popup's own pace, published on the root by popup_lift.js because the
65256
65508
  ::view-transition tree hangs off it and inherits from nowhere else. */
@@ -65594,16 +65846,21 @@ const css$E = /* css */`
65594
65846
  * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
65595
65847
  * Where the keyboard goes when this dialog opens — one rung of the ladder in
65596
65848
  * `docs/autofocus.md`, which is what to read for the whole of it.
65597
- * - `true` — the dialog element itself takes the keyboard, whatever it holds.
65598
- * For a dialog whose content is READ before it is filled: the focus starts
65599
- * at the top of the reading order and no virtual keyboard rises over it.
65849
+ * - `true` — the dialog element itself takes the keyboard rather than the
65850
+ * first focusable it holds; something inside that asks by name
65851
+ * (`autoFocus`) still comes first. For a dialog whose content is READ
65852
+ * before it is filled: the focus starts at the top of the reading
65853
+ * order and no virtual keyboard rises over it.
65600
65854
  * - `"last-resort"` — the dialog takes the keyboard only if it holds nothing
65601
65855
  * focusable of its own.
65602
65856
  * - `"restore"` — the dialog stays out of the opening focus chain unless it
65603
65857
  * held focus when it closed.
65604
- * - `false` — the dialog element itself is never where the focus lands; the
65605
- * opening still hands the focus to what the dialog holds. Unlike
65606
- * `Popover`'s `false`, it does not leave the keyboard where it was.
65858
+ * - `false` — the dialog says nothing, and the opening still hands out the
65859
+ * focus: a `<dialog>` is focusable and the first focusable of its own
65860
+ * subtree, so it takes the keyboard as with `true`. Where the first
65861
+ * focusable is skipped (a touch device, below), a last resort inside is
65862
+ * tried instead, and nothing there leaves the keyboard where it was.
65863
+ * Unlike `Popover`'s `false`, the transfer runs.
65607
65864
  * Wherever the keyboard is a virtual one (a touch device), the surface is
65608
65865
  * already what one arrives on: a popup is read before it is reached there, so
65609
65866
  * the focus only leaves it for something that asked by name (`autoFocus` on
@@ -65637,9 +65894,9 @@ const css$E = /* css */`
65637
65894
  * is "new" or "edit X" depending on the press must know which one it is
65638
65895
  * before what it holds is rendered.
65639
65896
  * @param {(event: Event) => void} [props.onClose] - Called when the dialog
65640
- * actually closes — not preventable (see `open_controller.js`'s own
65641
- * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
65642
- * veto a close instead).
65897
+ * actually closes — not preventable: it is the close happening, not a
65898
+ * request. What refuses a close is navi's: a control inside still
65899
+ * running its action (see popup_busy.js for the runs that let it through).
65643
65900
  * @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
65644
65901
  * Keeps the open state in the history entry, so a screen left and come back
65645
65902
  * to finds this popup as it was — open, and without an entrance playing: it
@@ -65657,14 +65914,17 @@ const css$E = /* css */`
65657
65914
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
65658
65915
  * `children` are built and thrown away (see popup_content_mount.js).
65659
65916
  * `"from-first-open"` (the default) builds them on the first open and keeps
65660
- * them afterwards. `"always"` builds them right away, for content something
65917
+ * them afterwards — and the dialog itself is built then too: until its first
65918
+ * request (an open, a press on what opens it, its signal saying open) it is
65919
+ * a bare `<dialog>` holding its id (see docs/popup_open.md). `"always"` builds them right away, for content something
65661
65920
  * depends on while the popup is still closed: a value read off it, fields a
65662
65921
  * surrounding form collects on submit, a size measured from outside.
65663
65922
  * `"idle"` builds them in a browser idle moment after load — "always" minus
65664
65923
  * the cost on the critical render. `"while-opened"` throws them away once
65665
65924
  * the popup has finished closing, for content whose fresh state is its
65666
- * initial state: an uncontrolled field seeded from a `defaultValue` that
65667
- * changed while the popup was closed. Intent on the anchor (pointer
65925
+ * initial state: an edit left unsent at the close does not come back at
65926
+ * the next opening (an untouched field needs none of this to show a new
65927
+ * `defaultValue` — it follows one). Intent on the anchor (pointer
65668
65928
  * entering it, focus landing in it) builds the content ahead of the click;
65669
65929
  * under `"while-opened"`, only a press on what opens the popup does (a
65670
65930
  * `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
@@ -65678,11 +65938,122 @@ const Dialog = props => {
65678
65938
  ...props
65679
65939
  });
65680
65940
  }
65681
- return jsx(UncontrolledDialog, {
65941
+ return jsx(DialogStandIn, {
65682
65942
  ...props
65683
65943
  });
65684
65944
  };
65685
65945
 
65946
+ // A dialog nobody has asked for yet stands in as its bare <dialog>. That
65947
+ // element is all that reaches a closed dialog before its first opening: a
65948
+ // command finds it by id (`commandfor`) and asks it to open, a press on what
65949
+ // opens it announces itself on it (navi_open_press), --navi-toggle reads its
65950
+ // aria-expanded. The dialog proper — its open controller, its box, its focus
65951
+ // and pseudo-class wiring — is built at the first request: an open, a press
65952
+ // about to open it, its signal or its `open` saying open. A list whose rows
65953
+ // each hold a few dialogs mounts hundreds of them, and would pay for every one
65954
+ // at every mount, for openings that mostly never come. Built once, the dialog
65955
+ // stays: the next opening finds it there.
65956
+ const DialogStandIn = props => {
65957
+ const {
65958
+ ref,
65959
+ id,
65960
+ signal,
65961
+ value,
65962
+ open
65963
+ } = props;
65964
+ const ownRef = useRef();
65965
+ const dialogRef = ref || ownRef;
65966
+ // Why the dialog proper exists, once it does; never undone.
65967
+ const builtRef = useRef(null);
65968
+ const mountedRef = useRef(false);
65969
+ const isMount = !mountedRef.current;
65970
+ mountedRef.current = true;
65971
+ // Its own reading of the signal: the dialog of one card among many, told
65972
+ // apart by `value`, follows whether IT is the one open rather than every
65973
+ // write the others cause.
65974
+ const openedSignal = useMemo(() => signal ? computed(() => readOpened(signal.value, value)) : null, [signal, value]);
65975
+ const openedBySignal = openedSignal ? openedSignal.value : false;
65976
+ if (builtRef.current === null) {
65977
+ if (isMount && (needsDialogFromMount(props) || open || openedBySignal)) {
65978
+ builtRef.current = "mount";
65979
+ } else if (open || openedBySignal) {
65980
+ // Asked open while this stood in: the opening is happening now, and it
65981
+ // plays like any other (see defaultOpen "interaction").
65982
+ builtRef.current = "interaction";
65983
+ }
65984
+ }
65985
+ const [, setBuildCount] = useState(0);
65986
+ const warmRef = useRef(null);
65987
+ const build = reason => {
65988
+ if (builtRef.current !== null) {
65989
+ return;
65990
+ }
65991
+ builtRef.current = reason;
65992
+ flushSyncRendering(() => {
65993
+ setBuildCount(count => count + 1);
65994
+ });
65995
+ };
65996
+ const builtReason = builtRef.current;
65997
+ if (builtReason !== null) {
65998
+ return jsx(UncontrolledDialog, {
65999
+ ...props,
66000
+ ref: dialogRef,
66001
+ defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
66002
+ });
66003
+ }
66004
+ return jsx("dialog", {
66005
+ ref: dialogRef,
66006
+ id: id,
66007
+ className: "navi_dialog"
66008
+ // eslint-disable-next-line react/no-unknown-property
66009
+ ,
66010
+ "navi-hidden": ""
66011
+ // eslint-disable-next-line react/no-unknown-property
66012
+ ,
66013
+ "navi-out-of-flow": "",
66014
+ "aria-expanded": "false",
66015
+ "data-testid": props["data-testid"]
66016
+ // eslint-disable-next-line react/no-unknown-property
66017
+ ,
66018
+ onnavi_request_open: e => {
66019
+ // Built in this same event, and asked again: the opening is the one
66020
+ // the dialog proper would have had — its source, its anchor, its value.
66021
+ build("request");
66022
+ const opened = dispatchCustomEvent(dialogRef.current, "navi_request_open", e.detail);
66023
+ if (!opened) {
66024
+ e.preventDefault();
66025
+ }
66026
+ }
66027
+ // eslint-disable-next-line react/no-unknown-property
66028
+ ,
66029
+ onnavi_open_press: () => {
66030
+ // What the opening will read is asked for at once, as the dialog
66031
+ // proper does (see open_controller.js); the dialog itself two frames
66032
+ // later, so the pressed trigger is painted first — the click that
66033
+ // follows opens it built, or builds it itself when it comes sooner.
66034
+ if (signal) {
66035
+ preloadState(signal, readOpenValue(value));
66036
+ }
66037
+ if (warmRef.current) {
66038
+ return;
66039
+ }
66040
+ warmRef.current = requestFrameAfterNext(() => {
66041
+ warmRef.current = null;
66042
+ build("press");
66043
+ });
66044
+ }
66045
+ });
66046
+ };
66047
+ // What needs the dialog proper from the start: an opening at mount, content
66048
+ // built ahead of any opening, a history entry to read, an anchor whose hover
66049
+ // or focus builds the content ahead of the click.
66050
+ const needsDialogFromMount = props => {
66051
+ if (props.defaultOpen || props.navState || props.anchor !== undefined) {
66052
+ return true;
66053
+ }
66054
+ return props.mount !== undefined && props.mount !== "from-first-open";
66055
+ };
66056
+
65686
66057
  // No openController passed: this Dialog is used declaratively (e.g. driven
65687
66058
  // by --navi-toggle/--navi-open/--navi-close commands, the `open` prop, or
65688
66059
  // `defaultOpen`) rather than owned by a parent component.
@@ -66242,10 +66613,10 @@ const useDialogProps = props => {
66242
66613
  }
66243
66614
 
66244
66615
  // Suppressed until committed below — same @starting-style-avoidance
66245
- // reasoning as popover.jsx's own openEffect (see its top comment), even
66246
- // though Dialog never needs to measure/flip anything: it still needs a
66247
- // genuinely rendered "closed" frame to transition from, not a jump
66248
- // straight from not-shown to aria-expanded="true".
66616
+ // reasoning as popover.jsx's own openEffect (see popup_css.js's top
66617
+ // comment), even though Dialog never needs to measure/flip anything: it
66618
+ // still needs a genuinely rendered "closed" frame to transition from, not a
66619
+ // jump straight from not-shown to aria-expanded="true".
66249
66620
  dialogEl.style.transitionProperty = "none";
66250
66621
  if (backdropEl) {
66251
66622
  disarmBackdropHideRef.current?.();
@@ -67384,10 +67755,11 @@ const css$D = /* css */`
67384
67755
  * @param {boolean|"last-resort"|"restore"} [props.autoFocus="last-resort"] -
67385
67756
  * Where the keyboard goes when this popover opens — one rung of the ladder in
67386
67757
  * `docs/autofocus.md`, which is what to read for the whole of it.
67387
- * - `true` — the popover element itself takes the keyboard, whatever it
67388
- * holds. For a popover whose content is READ before it is filled: the focus
67389
- * starts at the top of the reading order and no virtual keyboard rises over
67390
- * it.
67758
+ * - `true` — the popover element itself takes the keyboard rather than the
67759
+ * first focusable it holds; something inside that asks by name
67760
+ * (`autoFocus`) still comes first. For a popover whose content is READ
67761
+ * before it is filled: the focus starts at the top of the reading
67762
+ * order and no virtual keyboard rises over it.
67391
67763
  * - `"last-resort"` — the popover takes the keyboard only if it holds nothing
67392
67764
  * focusable of its own.
67393
67765
  * - `"restore"` — the popover stays out of the opening focus chain unless it
@@ -67428,9 +67800,9 @@ const css$D = /* css */`
67428
67800
  * is "new" or "edit X" depending on the press must know which one it is
67429
67801
  * before what it holds is rendered.
67430
67802
  * @param {(event: Event) => void} [props.onClose] - Called when the popover
67431
- * actually closes — not preventable (see `open_controller.js`'s own
67432
- * `onRequestClose`/`onClose` distinction; `onRequestClose` is where you'd
67433
- * veto a close instead).
67803
+ * actually closes — not preventable: it is the close happening, not a
67804
+ * request. What refuses a close is navi's: a control inside still
67805
+ * running its action (see popup_busy.js for the runs that let it through).
67434
67806
  * @param {boolean|string|{id?: string, type?: "push"|"replace"}} [props.navState] -
67435
67807
  * Keeps the open state in the history entry, so a screen left and come back
67436
67808
  * to finds this popup as it was — open, and without an entrance playing: it
@@ -67454,8 +67826,9 @@ const css$D = /* css */`
67454
67826
  * `"idle"` builds them in a browser idle moment after load — "always" minus
67455
67827
  * the cost on the critical render. `"while-opened"` throws them away once
67456
67828
  * the popup has finished closing, for content whose fresh state is its
67457
- * initial state: an uncontrolled field seeded from a `defaultValue` that
67458
- * changed while the popup was closed. Intent on the anchor (pointer
67829
+ * initial state: an edit left unsent at the close does not come back at
67830
+ * the next opening (an untouched field needs none of this to show a new
67831
+ * `defaultValue` — it follows one). Intent on the anchor (pointer
67459
67832
  * entering it, focus landing in it) builds the content ahead of the click;
67460
67833
  * under `"while-opened"`, only a press on what opens the popup does (a
67461
67834
  * `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
@@ -67888,7 +68261,7 @@ const usePopoverProps = props => {
67888
68261
  });
67889
68262
 
67890
68263
  // Suppressed until the popover is actually measured/positioned below —
67891
- // see this file's top comment for why @starting-style can't drive the
68264
+ // see popup_css.js's top comment for why @starting-style can't drive the
67892
68265
  // opening transition (it needs the popover's actual resting position
67893
68266
  // already in place, which requires a layout box that only exists once
67894
68267
  // shown).
@@ -68236,7 +68609,7 @@ const usePopoverProps = props => {
68236
68609
  // frame set up above as a real rendered state (the reflow), re-enables
68237
68610
  // transitions, flips aria-expanded="true" (only then does the CSS
68238
68611
  // transition play, from that just-committed frame to the open one,
68239
- // with no @starting-style involved at all — see this file's top
68612
+ // with no @starting-style involved at all — see popup_css.js's top
68240
68613
  // comment), suppresses pointer events until it settles, and transfers
68241
68614
  // focus in. Inlined rather than a standalone function since it only
68242
68615
  // has this one call site.
@@ -68724,8 +69097,9 @@ const css$C = /* css */`@layer navi {
68724
69097
  * `"idle"` builds them in a browser idle moment after load — `"always"`
68725
69098
  * minus the cost on the critical render.
68726
69099
  * `"while-opened"` throws them away once the popup has finished closing, for
68727
- * content whose fresh state is its initial state: an uncontrolled field
68728
- * seeded from a `defaultValue` that changed while the popup was closed.
69100
+ * content whose fresh state is its initial state: an edit left unsent at
69101
+ * the close does not come back at the next opening (an untouched field
69102
+ * needs none of this to show a new `defaultValue` — it follows one).
68729
69103
  * @param {import("ignore:preact").ComponentChildren} props.children
68730
69104
  */
68731
69105
  const Popup = props => {
@@ -69024,8 +69398,9 @@ const PickerCustom = props => {
69024
69398
  open,
69025
69399
  defaultOpen,
69026
69400
  // What Escape means for this picker. "cancel" (the default) puts back the
69027
- // value the picker had at open and, for a dialog, goes back in history —
69028
- // so everything written to the url while it was open goes back too.
69401
+ // value the picker had at open and, for a dialog with an explicit id, goes
69402
+ // back in history — so everything written to the url while it was open
69403
+ // goes back too (see pickerNavType below).
69029
69404
  // "close" makes Escape say the same thing as clicking outside: keep what
69030
69405
  // was chosen, close the popup.
69031
69406
  escapeEffect = "cancel",
@@ -69693,9 +70068,10 @@ const PickerContentInsidePopup = props => {
69693
70068
  // itself, and this one does not. Popover ignores it, same as Dialog ignores
69694
70069
  // marginWithAnchor.
69695
70070
  dockedOnSmallTouchScreen,
69696
- // Same again: the dialog takes the trigger's box as a floor (and, with
69697
- // dialogMaxWidth="var(--anchor-width)", as a ceiling) — what keeps a card
69698
- // its own width once lifted. Dialog's own `sizeFromAnchor`.
70071
+ // Same again: the dialog takes the trigger's box as a floor (Dialog's own
70072
+ // `sizeFromAnchor`) and, unless a dialogMaxWidth says otherwise, as a
70073
+ // ceiling (see maxWidth below) — what keeps a card its own width once
70074
+ // lifted.
69699
70075
  dialogSizeFromAnchor,
69700
70076
  // The caller's word on the popup's own box. Written on the popup element,
69701
70077
  // where Popover/Dialog map them to their own vars, rather than as vars on
@@ -77527,10 +77903,10 @@ const useBadgeRegistry = (children, enabled) => {
77527
77903
  * list renders nothing at all. In a <Picker> this is the only placeholder
77528
77904
  * the user gets — a picker given a `ui` does not draw its own — so pass the
77529
77905
  * placeholder text: `fallback="Select skills…"`. Plain text is the right
77530
- * choice there: it reads at the picker's size, in the placeholder color the
77531
- * picker gives its empty value slot, and the box stays the same height. A
77532
- * transparent <Badge> matches the slot to the pixel instead, at the price of
77533
- * badge-sized text. See docs/badge_list.md.
77906
+ * choice there: it reads at the picker's size, in the normal text color (a
77907
+ * caller's `ui` is never greyed as a placeholder), and the box stays the same
77908
+ * height. A transparent <Badge> matches the slot to the pixel instead, at the
77909
+ * price of badge-sized text. See docs/typography.md#badgelist.
77534
77910
  * @param {boolean} [shrinkWrap]
77535
77911
  * Narrows the list down to its widest row so the last row isn't ragged.
77536
77912
  * Defaults to true inside a <Picker> — the trigger draws a border around the
@@ -79675,6 +80051,7 @@ const PickerFirstResolver = props => {
79675
80051
  * dialogExpandX?: boolean,
79676
80052
  * dialogExpandY?: boolean,
79677
80053
  * dialogSizeFromAnchor?: boolean,
80054
+ * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
79678
80055
  * marginWithContainer?: number | string,
79679
80056
  * anchor?: import("ignore:preact").RefObject<HTMLElement> | HTMLElement,
79680
80057
  * escapeEffect?: "cancel" | "close",
@@ -79731,8 +80108,8 @@ const PickerFirstResolver = props => {
79731
80108
  * filter", "anywhere"), so its empty look is the caller's. A `ui` must
79732
80109
  * therefore say on its own that the picker is empty, and take the same room
79733
80110
  * empty as it does filled. A <BadgeList> gets both from its `fallback` — the
79734
- * placeholder text, as plain text, see docs/badge_list.md. `"default"` draws
79735
- * nothing in the slot at all.
80111
+ * placeholder text, as plain text, see docs/typography.md#badgelist.
80112
+ * `"default"` draws nothing in the slot at all.
79736
80113
  *
79737
80114
  * Under `variant="icon"` there is no value to draw, so this IS the trigger:
79738
80115
  * left out it is the icon the right slot would have shown (`rightSlotIcon`,
@@ -80053,14 +80430,23 @@ const PickerFirstResolver = props => {
80053
80430
  * about it changes. `dialogMaxWidth="var(--anchor-width)"` says the same
80054
80431
  * ceiling by hand: the popup props are written on the dialog element, where
80055
80432
  * that variable lives.
80433
+ * @param {boolean|"top"|"bottom"} [dockedOnSmallTouchScreen] Dialog mode,
80434
+ * Dialog's own: on a small touch screen the dialog becomes a sheet docked
80435
+ * full width to one edge — `true` or `"top"` the top edge, out of the
80436
+ * virtual keyboard's way; `"bottom"` the bottom one, for a sheet that is
80437
+ * read and tapped. Anywhere else it stays the centered box. Ignored in
80438
+ * popover mode.
80056
80439
  * @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
80057
80440
  * picker. "cancel" puts back the value the picker had at open, and a dialog
80058
- * picker also goes back in history — so anything written to the url while it
80059
- * was open (a route `stateSignal`, a search param) goes back with it. "close"
80060
- * makes Escape say what clicking outside says: keep what was chosen, close —
80061
- * a last resort, see docs/popup_open.md ("Escape cancels, the other gestures
80062
- * keep") for why Escape should go on meaning cancel, and for what the value
80063
- * at open is on the picker's very first open.
80441
+ * picker with an explicit `id` (its opening being an entry of its own)
80442
+ * also goes back in history — so anything written to the url while it was
80443
+ * open (a route `stateSignal`, a search param) goes back with it. "close"
80444
+ * makes Escape say what clicking outside says: keep what was chosen,
80445
+ * close — a last resort, see
80446
+ * docs/popup_open.md#escape-cancels-the-other-gestures-keep for why Escape
80447
+ * should go on meaning cancel, and
80448
+ * docs/popup_open.md#what-cancel-actually-undoes for what a cancel puts
80449
+ * back.
80064
80450
  * @param {import("ignore:preact").RefObject<HTMLElement>|HTMLElement} [anchor] What
80065
80451
  * the popup hangs off, when that is not the picker itself: a picker whose
80066
80452
  * trigger is one piece of a bigger control (the chevron half of a split
@@ -80115,7 +80501,9 @@ const PickerFirstResolver = props => {
80115
80501
  * positioned ancestor for `popupLayer="local"`). Caps the popup's size as
80116
80502
  * well as its placement, so what an expanded dialog leaves visible around
80117
80503
  * itself is set here. Defaults to `popoverSpacing` in popover mode, and to
80118
- * Dialog's own 3vvw in dialog mode.
80504
+ * Dialog's own in dialog mode: `3appw` in the top layer, `3cqw` under
80505
+ * `popupLayer="local"`, 0 for a dialog docked by
80506
+ * `dockedOnSmallTouchScreen`.
80119
80507
  * @param {string} [popupTestId] The `data-testid` of the popup element — the
80120
80508
  * popover or dialog the children open in. A `data-testid` on the picker
80121
80509
  * itself names the trigger (see docs/testid.md); this one is for a test that