@jsenv/navi 0.29.45 → 0.29.46

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.
@@ -2,7 +2,8 @@
2
2
  * AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
- import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal } from "./jsenv_navi_side_effects.js";
5
+ import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
+ export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
6
7
  import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
7
8
  export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
8
9
  import { signal, computed, effect, batch, useSignal } from "@preact/signals";
@@ -49,17 +50,28 @@ const css$10 = /* css */`
49
50
  --navi-z-index-control-focused: 2;
50
51
 
51
52
  /* Kept stuck while something scrolls under it: a list header, the head
52
- and foot of a side panel, a table's sticky cells. Above raised
53
- controls a control scrolling past must go under the header that
54
- pins the column it belongs to, never over it.
55
-
56
- "While stuck" is the whole condition, and a sticky element cannot read
57
- its own stuck state in CSS: List marks its parts with a navi-stuck
58
- attribute and applies this band only there (see --list-*-z-index in
59
- list.jsx). A
60
- sticky part at rest is a block in the flow with nothing passing under
61
- it; giving it this band anyway is what slices whatever a neighbouring
62
- row lets out of its box. */
53
+ and foot of a side panel, a table's sticky cells, the header and
54
+ footer of any scrolling Box. Above raised controls a control
55
+ scrolling past must go under the header that pins the column it
56
+ belongs to, never over it.
57
+
58
+ A sticky element is a positioned one, so it already wins against
59
+ everything in the flow — the band is what it takes to also win against
60
+ what the page positioned itself, which loses to DOM order otherwise
61
+ (a sticky part is written before what scrolls under it). Box applies
62
+ it by default, isolated, and lets a call site write auto back:
63
+ --box-header-z-index / --box-footer-z-index.
64
+
65
+ "While stuck" is the condition the name states, and it costs something
66
+ to ignore: a sticky part at rest is a block in the flow with nothing
67
+ passing under it, and the band there is what slices whatever a
68
+ neighbouring row lets out of its box. CSS cannot express the
69
+ condition — an element cannot read its own stuck state — so it takes
70
+ measuring, which List does (it marks its parts with a navi-stuck
71
+ attribute against its own scroller and applies the band only there,
72
+ see --list-*-z-index in list.jsx) and Box does not: a generic
73
+ scrolling area does not know what it was given to scroll, and dropping
74
+ to auto there loses to a single position: relative. */
63
75
  --navi-z-index-sticky: 10;
64
76
 
65
77
  /* Pinned to the viewport, over the whole page: FixedBar. A decade of its
@@ -19030,14 +19042,24 @@ import.meta.css = [/* css */`
19030
19042
 
19031
19043
  [data-scrollable] {
19032
19044
  overflow: var(--x-scrollable-overflow, auto);
19045
+ --box-header-z-index: var(--navi-z-index-sticky);
19046
+ --box-footer-z-index: var(--navi-z-index-sticky);
19047
+ /* The band stays inside this box: without a stacking context here, "in
19048
+ front of my body" would be read as "in front of everything on the page"
19049
+ and a header would reach past a bar or a popup — which is exactly what
19050
+ the decades in navi_z_indexes.js are there to prevent. See
19051
+ docs/z_index.md. */
19052
+ isolation: isolate;
19033
19053
 
19034
19054
  &[data-scrollable-overflow="scroll"] {
19035
19055
  --x-scrollable-overflow: scroll;
19036
19056
  }
19037
19057
 
19038
- /* box-shadow rather than a border: it draws the separation without taking
19039
- part in the layout, so a header keeps the exact height its content asks
19040
- for and nothing shifts by a pixel when the line appears. */
19058
+ /* A real border and not a box-shadow: a shadow is drawn outside the box, so
19059
+ it lands on top of whatever comes next in the painting order and loses to
19060
+ it a body painting its own background over the line that was meant to
19061
+ separate them. The border belongs to the part itself and is always
19062
+ visible; the pixel it adds shifts nothing, these parts never shrink. */
19041
19063
  /* The corners are the container's, not the part's: a header sitting at the
19042
19064
  top of a rounded box has to follow that curve or it paints square over
19043
19065
  it (a dark header in a rounded popup is where this shows). inherit and
@@ -19045,18 +19067,18 @@ import.meta.css = [/* css */`
19045
19067
  > [data-header] {
19046
19068
  position: sticky;
19047
19069
  top: 0;
19048
- z-index: 1;
19070
+ z-index: var(--box-header-z-index);
19071
+ border-bottom: 1px solid var(--navi-separator-color-default);
19049
19072
  border-top-left-radius: inherit;
19050
19073
  border-top-right-radius: inherit;
19051
- box-shadow: 0 1px 0 var(--navi-separator-color-default);
19052
19074
  }
19053
19075
  > [data-footer] {
19054
19076
  position: sticky;
19055
19077
  bottom: 0;
19056
- z-index: 1;
19078
+ z-index: var(--box-footer-z-index);
19079
+ border-top: 1px solid var(--navi-separator-color-default);
19057
19080
  border-bottom-right-radius: inherit;
19058
19081
  border-bottom-left-radius: inherit;
19059
- box-shadow: 0 -1px 0 var(--navi-separator-color-default);
19060
19082
  }
19061
19083
 
19062
19084
  &:has(> [data-body]) {
@@ -19070,8 +19092,11 @@ import.meta.css = [/* css */`
19070
19092
 
19071
19093
  > [data-header],
19072
19094
  > [data-footer] {
19095
+ /* Nothing scrolls under them here — the body does that, next to them —
19096
+ so they are back to being blocks in the flow, and stacking is not
19097
+ their business anymore. */
19073
19098
  position: static;
19074
- z-index: unset;
19099
+ z-index: auto;
19075
19100
  flex-shrink: 0;
19076
19101
  }
19077
19102
 
@@ -25441,6 +25466,13 @@ const useControlProps = (props, {
25441
25466
  const onButtonInteractionAllowed = e => {
25442
25467
  triggerUIAction(e);
25443
25468
  const control = ref.current;
25469
+ if (!control) {
25470
+ // What the button just did took the button away: a command that
25471
+ // navigates, a popup closing over it. There is no control left to
25472
+ // ask for an action, and nothing is lost by not asking — what the
25473
+ // press was for has already happened.
25474
+ return;
25475
+ }
25444
25476
  tryActionAfterInteractionAllowed(control, {
25445
25477
  event: e,
25446
25478
  action: boundAction,
@@ -29169,17 +29201,19 @@ const css$V = /* css */`
29169
29201
  * shown via the non-modal `.show()` instead, staying in normal document
29170
29202
  * flow inside its own positioned ancestor — confined to (and clipped by)
29171
29203
  * that container instead of the whole viewport.
29172
- * @param {boolean} [props.dockedOnTouch] - Turns the dialog into a bottom sheet
29173
- * (docked flush to the bottom edge, full width) when the pointer is coarse,
29174
- * and leaves it alone otherwise. For a dialog meant to be interacted with
29175
- * rather than merely read: under a finger the keyboard owns the bottom of
29176
- * the screen and a centered box ends up both cramped and out of thumb
29177
- * reach, while under a mouse the centered box is already the right shape
29178
- * hence a prop that only ever does something on touch. It supplies defaults
29179
- * for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
29180
- * so any of them can still be pinned explicitly. Keyed off `(pointer: coarse)` (the
29181
- * input device, not a width breakpoint — a narrow desktop window is still a
29182
- * mouse) via `coarsePointerSignal`, so it re-resolves live.
29204
+ * @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
29205
+ * bottom sheet (docked flush to the bottom edge, full width) on a small touch
29206
+ * screen, and leaves it alone otherwise. For a dialog meant to be interacted
29207
+ * with rather than merely read: on a phone the keyboard owns the bottom of
29208
+ * the screen and a centered box ends up both cramped and out of thumb reach,
29209
+ * while under a mouse the centered box is already the right shape. Both
29210
+ * halves of the name matter (`smallTouchScreenSignal`): touch alone would
29211
+ * dock a big touch screen — a tablet, a kiosk panel — a whole screen away
29212
+ * from where the finger just tapped, and size alone would dock a narrow
29213
+ * desktop window, which is still a mouse. It supplies defaults for
29214
+ * `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
29215
+ * any of them can still be pinned explicitly. Re-resolves live as the pointer
29216
+ * type or the window size changes.
29183
29217
  * @param {string} [props.positionArea="center"] - Where to dock the dialog
29184
29218
  * within its container (the viewport for `layer="top"`, the positioned
29185
29219
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
@@ -29192,8 +29226,8 @@ const css$V = /* css */`
29192
29226
  * `inset(top)`) for the overlapping variant.
29193
29227
  * @param {boolean} [props.expand] - Shorthand for both `expandX` and `expandY`.
29194
29228
  * @param {boolean} [props.expandX] - Stretches the dialog to the full width its
29195
- * container allows (`--dialog-maxmax-width`). Set by `dockedOnTouch` on a
29196
- * touch device.
29229
+ * container allows (`--dialog-maxmax-width`). Set by
29230
+ * `dockedOnSmallTouchScreen` on a small touch screen.
29197
29231
  * @param {boolean} [props.expandY] - Same, vertically
29198
29232
  * (`--dialog-maxmax-height`).
29199
29233
  * @param {string|number} [props.marginWithContainer="3appw"] - Minimum gap kept
@@ -29223,7 +29257,7 @@ const css$V = /* css */`
29223
29257
  * A `layer="local"` dialog always locks its own positioned ancestor's
29224
29258
  * scroll while open (its backdrop only covers the scrollport, so scrolling
29225
29259
  * there would reveal uncovered content); this prop extends the lock to the
29226
- * whole page. Defaults to `true` for a dialog docked by `dockedOnTouch`.
29260
+ * whole page. Defaults to `true` for a dialog docked by `dockedOnSmallTouchScreen`.
29227
29261
  * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
29228
29262
  * - `true`/`"auto"` resolves to `"scaling"` for a centered `positionArea`,
29229
29263
  * or a concrete `"slide-from-*"` direction otherwise. Any other explicit
@@ -29406,10 +29440,10 @@ const DialogLocal = props => {
29406
29440
  * contentProps]` — `backdropProps` is `null` for the via-attribute renderer
29407
29441
  * (its own backdrop is native, not a real element).
29408
29442
  */
29409
- // What a dialog turns into under a finger. "bottom" is not a taste: it puts
29410
- // the dialog in the zone a handheld device is actually operated from — where
29411
- // the thumbs rest and where the virtual keyboard comes up — instead of the
29412
- // middle of the screen, which is the farthest point from both.
29443
+ // What a dialog turns into on a small touch screen. "bottom" is not a taste:
29444
+ // it puts the dialog in the zone a phone is actually operated from — where the
29445
+ // thumbs rest and where the virtual keyboard comes up — instead of the middle
29446
+ // of the screen, which is the farthest point from both.
29413
29447
  // Only defaults: an explicitly passed prop still wins, so the docked shape can
29414
29448
  // be adjusted one axis at a time instead of being all-or-nothing.
29415
29449
  const DOCKED = {
@@ -29447,7 +29481,7 @@ const useDialogProps = props => {
29447
29481
  // .show() instead, staying in normal document flow, position: absolute
29448
29482
  // relative to its own positioned ancestor. See this file's top comment.
29449
29483
  layer = "top",
29450
- dockedOnTouch,
29484
+ dockedOnSmallTouchScreen,
29451
29485
  // Same grammar as Popover's own positionArea — see this file's top
29452
29486
  // comment and popup_shared.js's parsePositionArea.
29453
29487
  positionArea: positionAreaProp,
@@ -29500,9 +29534,10 @@ const useDialogProps = props => {
29500
29534
  });
29501
29535
  const isModal = layer === "top";
29502
29536
  const ref = props.ref;
29503
- // Only touch changes anything: with a mouse a dialog already wants to be the
29504
- // centered box it is by default, so there is nothing to resolve there.
29505
- const isDocked = dockedOnTouch && coarsePointerSignal.value;
29537
+ // Only a small touch screen changes anything: on a mouse and on a touch
29538
+ // screen too big to reach the bottom edge of a dialog already wants to be
29539
+ // the centered box it is by default, so there is nothing to resolve.
29540
+ const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value;
29506
29541
  const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
29507
29542
  const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
29508
29543
  // A share of whatever holds the dialog: the app's own screen for a
@@ -31645,8 +31680,8 @@ const css$T = /* css */`
31645
31680
  * @property {"close"|"cancel"|"capture"|"none"} [pointerInteractionOutsideEffect]
31646
31681
  * - What a click outside does. `"capture"`/`"none"` force an explicit answer
31647
31682
  * by refusing to treat a click elsewhere as one.
31648
- * @property {boolean} [dockedOnTouch] - `"dialog"` mode only: turn the popup
31649
- * into a bottom sheet under a finger.
31683
+ * @property {boolean} [dockedOnSmallTouchScreen] - `"dialog"` mode only: turn
31684
+ * the popup into a bottom sheet on a small touch screen.
31650
31685
  * @property {(params: { message: import("ignore:preact").ComponentChildren }) => import("ignore:preact").ComponentChildren} [renderContent]
31651
31686
  * - Replaces the popup body — the question and the two buttons — for every
31652
31687
  * confirmation at once. The per-button `confirmPopupContent` prop is the same
@@ -31664,7 +31699,7 @@ const confirmPopupOptions = {
31664
31699
  animationDuration: undefined,
31665
31700
  positionArea: undefined,
31666
31701
  pointerInteractionOutsideEffect: "close",
31667
- dockedOnTouch: false,
31702
+ dockedOnSmallTouchScreen: false,
31668
31703
  renderContent: undefined
31669
31704
  };
31670
31705
 
@@ -31746,7 +31781,7 @@ const ConfirmPopup = ({
31746
31781
  animationDuration,
31747
31782
  positionArea,
31748
31783
  pointerInteractionOutsideEffect,
31749
- dockedOnTouch,
31784
+ dockedOnSmallTouchScreen,
31750
31785
  renderContent
31751
31786
  } = confirmPopupOptions;
31752
31787
 
@@ -31782,7 +31817,7 @@ const ConfirmPopup = ({
31782
31817
  if (mode === "dialog") {
31783
31818
  return jsx(Dialog, {
31784
31819
  className: "navi_confirm_popup",
31785
- dockedOnTouch: dockedOnTouch,
31820
+ dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
31786
31821
  ...popupProps,
31787
31822
  children: body
31788
31823
  });
@@ -32037,6 +32072,16 @@ const debounceSignal = (
32037
32072
  * The action will not fire while the user is actively changing filters; it fires once
32038
32073
  * they pause for half a second.
32039
32074
  */
32075
+ // The run is not awaited here, and a rejection nobody waits for is an unhandled
32076
+ // one — in dev, an error overlay thrown over a page that is already saying what
32077
+ // went wrong. Nothing is lost by dropping it: the failure is held by the action
32078
+ // itself, and whoever reads it (useAsyncData, <Button action>) is what shows it.
32079
+ const runUnwatched = (result) => {
32080
+ if (result && typeof result.catch === "function") {
32081
+ result.catch(() => {});
32082
+ }
32083
+ };
32084
+
32040
32085
  const actionRunEffect = (
32041
32086
  action,
32042
32087
  deriveActionParamsFromSignals,
@@ -32086,7 +32131,7 @@ const actionRunEffect = (
32086
32131
  // falsy params, don't run
32087
32132
  return;
32088
32133
  }
32089
- actionTarget.run({ reason: "truthy params first run" });
32134
+ runUnwatched(actionTarget.run({ reason: "truthy params first run" }));
32090
32135
  return;
32091
32136
  }
32092
32137
 
@@ -32103,16 +32148,20 @@ const actionRunEffect = (
32103
32148
  }
32104
32149
  if (!actionTargetPrevious.params) {
32105
32150
  // coming from falsy-params state: action may already be cached, avoid unnecessary rerun
32106
- actionTarget.run({ reason: "params restored from falsy state" });
32151
+ runUnwatched(
32152
+ actionTarget.run({ reason: "params restored from falsy state" }),
32153
+ );
32107
32154
  } else {
32108
- actionTarget.rerun({ reason: "params modified" });
32155
+ runUnwatched(actionTarget.rerun({ reason: "params modified" }));
32109
32156
  }
32110
32157
  }
32111
32158
  },
32112
32159
  ...options,
32113
32160
  });
32114
32161
  if (actionParamsSignal.peek()) {
32115
- actionRunnedByThisEffect.run({ reason: "initial truthy params" });
32162
+ runUnwatched(
32163
+ actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
32164
+ );
32116
32165
  }
32117
32166
  return actionRunnedByThisEffect;
32118
32167
  };
@@ -35236,6 +35285,24 @@ const TYPE_CONVERTERS = {
35236
35285
  },
35237
35286
  };
35238
35287
 
35288
+ /**
35289
+ * A container has put its page on screen — or as much of it as it can.
35290
+ *
35291
+ * A route matching is a signal changing, and the page it selects reaches the
35292
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
35293
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
35294
+ * as it has been decided (a travel about to have its picture taken by the
35295
+ * browser, see route_travel.jsx) waits for this instead of counting.
35296
+ *
35297
+ * A page waiting on data is announced too, by the boundary showing its loading
35298
+ * state (see Loading in use_async_data.jsx): what the container could put on
35299
+ * screen is what the browser is about to take a picture of, and a page that
35300
+ * cannot render yet would otherwise be waited on until the transition dies of
35301
+ * it. It lives in a module of its own for that: the async layer says it as much
35302
+ * as the router does, and neither can import the other.
35303
+ */
35304
+ const [publishRouteRender, observeRouteRender] = createPubSub();
35305
+
35239
35306
  const promiseStateWeakMap = new WeakMap();
35240
35307
  const usePromiseAsyncData = (
35241
35308
  promise,
@@ -35290,7 +35357,8 @@ const useForceRender = () => {
35290
35357
 
35291
35358
  const useAsyncData = (promiseOrAction, {
35292
35359
  loading = "delegate",
35293
- error = "delegate"
35360
+ error = "delegate",
35361
+ onLoad
35294
35362
  } = {}) => {
35295
35363
  const isAction = Boolean(promiseOrAction && promiseOrAction.isAction);
35296
35364
  if (loading === true) {
@@ -35302,7 +35370,8 @@ const useAsyncData = (promiseOrAction, {
35302
35370
  if (isAction) {
35303
35371
  return useActionAsyncData(promiseOrAction, {
35304
35372
  loadingEffect: loading,
35305
- errorEffect: error
35373
+ errorEffect: error,
35374
+ onLoad
35306
35375
  });
35307
35376
  }
35308
35377
  return usePromiseAsyncData(promiseOrAction, {
@@ -35319,12 +35388,14 @@ const dismissedActionWeakSet = new WeakSet();
35319
35388
  const dismissedActionPendingPromiseWeakMap = new WeakMap();
35320
35389
  const useActionAsyncData = (action, {
35321
35390
  loadingEffect,
35322
- errorEffect
35391
+ errorEffect,
35392
+ onLoad
35323
35393
  }) => {
35324
35394
  const loadingRef = useContext(LoadingContext);
35325
35395
  if (!loadingRef) {
35326
35396
  throw new Error("Missing <Loading>");
35327
35397
  }
35398
+ useOnLoad(action, onLoad);
35328
35399
 
35329
35400
  // Use peek() instead of .value to avoid subscribing this component to the signal.
35330
35401
  // Reading .value would make Preact re-render the component reactively when the state
@@ -35445,6 +35516,41 @@ const useActionAsyncData = (action, {
35445
35516
  throw pendingPromise;
35446
35517
  };
35447
35518
 
35519
+ // What a screen does with the data once, when it becomes known (see onLoad in
35520
+ // the JSDoc above). Kept apart because the two questions it answers are not the
35521
+ // ones the hook around it answers: WHEN — a layout effect, so a form taking its
35522
+ // reference in the same tick sees what was written; and HOW OFTEN — once per set
35523
+ // of params, which is the action's own answer to "is this another thing or the
35524
+ // same one again".
35525
+ const NOTHING_SEEDED = Symbol("nothing_seeded");
35526
+ const useOnLoad = (action, onLoad) => {
35527
+ const onLoadRef = useRef(onLoad);
35528
+ onLoadRef.current = onLoad;
35529
+ const paramsSeededRef = useRef(NOTHING_SEEDED);
35530
+ useLayoutEffect(() => {
35531
+ const callback = onLoadRef.current;
35532
+ if (!callback) {
35533
+ return;
35534
+ }
35535
+ if (action.runningStateSignal.peek() !== COMPLETED) {
35536
+ return;
35537
+ }
35538
+ const data = action.dataSignal.peek();
35539
+ if (data === undefined) {
35540
+ return;
35541
+ }
35542
+ const params = action.paramsSignal.peek();
35543
+ const paramsSeeded = paramsSeededRef.current;
35544
+ if (paramsSeeded !== NOTHING_SEEDED && compareTwoJsValues(params, paramsSeeded)) {
35545
+ return;
35546
+ }
35547
+ paramsSeededRef.current = params;
35548
+ callback(data, {
35549
+ params
35550
+ });
35551
+ });
35552
+ };
35553
+
35448
35554
  // ─── Loading ──────────────────────────────────────────────────────────────────
35449
35555
  // Wraps Suspense. Provides LoadingContext so useAction can write the suspension
35450
35556
  // reason. LoadingFallback reads that reason and subscribes to the action so it
@@ -35484,6 +35590,14 @@ const LoadingFallback = ({
35484
35590
  setTick(n => n + 1);
35485
35591
  });
35486
35592
  }, [action]);
35593
+ // A page that suspends never gets to say it is on screen — its own effects
35594
+ // are held with it — so this says it for it: what the document shows of the
35595
+ // page arriving is this. Anyone waiting for the page to be there before
35596
+ // moving (a travel about to have its picture taken, see route_travel.jsx)
35597
+ // would otherwise wait for a render that cannot happen until the data does.
35598
+ useLayoutEffect(() => {
35599
+ publishRouteRender();
35600
+ });
35487
35601
  if (loadingRef.current.reason !== "loading") {
35488
35602
  return null;
35489
35603
  }
@@ -36916,7 +37030,6 @@ const Head = ({
36916
37030
  * ```
36917
37031
  */
36918
37032
 
36919
- const [publishRouteRender, observeRouteRender] = createPubSub();
36920
37033
 
36921
37034
  /**
36922
37035
  * Keep every container showing the page it is showing, whatever the routes say.
@@ -37204,6 +37317,10 @@ installImportMetaCssBuild(import.meta);/**
37204
37317
  const CAN_KEEP_PICTURE = Boolean(document.startViewTransition && !document.startViewTransition.isPolyfill);
37205
37318
  const startViewTransition = ensureDocumentStartViewTransition();
37206
37319
  const TRAVEL_ATTRIBUTE = "data-navi-route-travel";
37320
+ // Which way the pages move, said on the document: the pictures of a transition
37321
+ // hang off the root, not off the box that travels, so the box's own `axis` has
37322
+ // to be lent to the document for the length of the travel.
37323
+ const TRAVEL_AXIS_ATTRIBUTE = "data-navi-route-travel-axis";
37207
37324
  // While a finger holds the travel: the pictures stand still and go exactly
37208
37325
  // where it says (see the CSS, and scrubTravel).
37209
37326
  const HOLD_ATTRIBUTE = "data-navi-route-travel-held";
@@ -37397,6 +37514,28 @@ const css$R = /* css */`
37397
37514
  }
37398
37515
  }
37399
37516
 
37517
+ /* The same four movements, along the axis the pages are laid out on: the
37518
+ start of a column is its top, so going forward there is the page rising and
37519
+ the next one coming up from below. */
37520
+ :root[${TRAVEL_AXIS_ATTRIBUTE}="y"] {
37521
+ &[${TRAVEL_ATTRIBUTE}="forward"] {
37522
+ &::view-transition-old(navi-route-travel) {
37523
+ animation-name: navi-route-travel-leave-towards-top;
37524
+ }
37525
+ &::view-transition-new(navi-route-travel) {
37526
+ animation-name: navi-route-travel-enter-from-bottom;
37527
+ }
37528
+ }
37529
+ &[${TRAVEL_ATTRIBUTE}="back"] {
37530
+ &::view-transition-old(navi-route-travel) {
37531
+ animation-name: navi-route-travel-leave-towards-bottom;
37532
+ }
37533
+ &::view-transition-new(navi-route-travel) {
37534
+ animation-name: navi-route-travel-enter-from-top;
37535
+ }
37536
+ }
37537
+ }
37538
+
37400
37539
  @keyframes navi-route-travel-leave-towards-start {
37401
37540
  from {
37402
37541
  translate: 0 0;
@@ -37429,6 +37568,38 @@ const css$R = /* css */`
37429
37568
  translate: 0 0;
37430
37569
  }
37431
37570
  }
37571
+ @keyframes navi-route-travel-leave-towards-top {
37572
+ from {
37573
+ translate: 0 0;
37574
+ }
37575
+ to {
37576
+ translate: 0 -100%;
37577
+ }
37578
+ }
37579
+ @keyframes navi-route-travel-enter-from-bottom {
37580
+ from {
37581
+ translate: 0 100%;
37582
+ }
37583
+ to {
37584
+ translate: 0 0;
37585
+ }
37586
+ }
37587
+ @keyframes navi-route-travel-leave-towards-bottom {
37588
+ from {
37589
+ translate: 0 0;
37590
+ }
37591
+ to {
37592
+ translate: 0 100%;
37593
+ }
37594
+ }
37595
+ @keyframes navi-route-travel-enter-from-top {
37596
+ from {
37597
+ translate: 0 -100%;
37598
+ }
37599
+ to {
37600
+ translate: 0 0;
37601
+ }
37602
+ }
37432
37603
  `;
37433
37604
 
37434
37605
  /**
@@ -37565,6 +37736,7 @@ const RouteTravel = ({
37565
37736
  // own for as long as it is the one travelling.
37566
37737
  nameForTravel(elementRef.current);
37567
37738
  document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
37739
+ document.documentElement.setAttribute(TRAVEL_AXIS_ATTRIBUTE, axis);
37568
37740
  if (scrub) {
37569
37741
  holdPictures(travel);
37570
37742
  document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
@@ -37578,14 +37750,21 @@ const RouteTravel = ({
37578
37750
  const releaseRendering = renderingHeldForRouting || holdRendering();
37579
37751
  renderingHeldForRouting = null;
37580
37752
  // The picture the browser is about to take must be of the page that was
37581
- // asked for, and a route matching is not yet a page rendered.
37753
+ // asked for, and a route matching is not yet a page rendered. Watched from
37754
+ // here rather than from inside the callback below: the browser calls that
37755
+ // callback a frame later, and a navigation that has already been decided
37756
+ // (what follows a send, a command) renders its page in between. A wait
37757
+ // armed then waits for something that has already happened — until the
37758
+ // browser gives up on the transition, leaving the page it was leaving on
37759
+ // screen and an error nobody asked for.
37760
+ const renderWait = armRouteRenderWait();
37582
37761
  const viewTransition = startViewTransition(async () => {
37583
37762
  await whilePageRenders(page, async () => {
37584
37763
  releaseRendering();
37585
37764
  if (change) {
37586
37765
  await change();
37587
37766
  }
37588
- });
37767
+ }, renderWait);
37589
37768
  // The page arriving is in the DOM and the transition has not started
37590
37769
  // playing: the one moment both boxes can be known.
37591
37770
  holdTravelHeight(elementRef.current, heightBefore);
@@ -37610,6 +37789,7 @@ const RouteTravel = ({
37610
37789
  viewTransition.finished.catch(() => {
37611
37790
  // A transition that fails before it ever calls back leaves the page held:
37612
37791
  // whoever asked for the hold gives it back, here as everywhere else.
37792
+ renderWait.stop();
37613
37793
  releaseRendering();
37614
37794
  endTravel(travel);
37615
37795
  });
@@ -37913,6 +38093,7 @@ const RouteTravel = ({
37913
38093
  // rather than pick.
37914
38094
  unnameAfterTravel(elementRef.current);
37915
38095
  document.documentElement.removeAttribute(TRAVEL_ATTRIBUTE);
38096
+ document.documentElement.removeAttribute(TRAVEL_AXIS_ATTRIBUTE);
37916
38097
  document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
37917
38098
  document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
37918
38099
  releaseTravelHeight();
@@ -38475,20 +38656,30 @@ const scrubTravel = (travel, ratio) => {
38475
38656
  // inside the callback of a view transition: the browser has stopped rendering
38476
38657
  // and is waiting on this very promise to take its picture, so a wait that never
38477
38658
  // ends is a page frozen under a transition that never became ready.
38478
- const whilePageRenders = async (page, change) => {
38659
+ // Listening starts before the change, or a render landing while the change is
38660
+ // being awaited is a render nobody heard. Armed apart from the wait itself
38661
+ // because the two do not always happen at the same moment: a view transition
38662
+ // calls its update callback a frame after it is started, and the render can
38663
+ // land in that gap — see beginTravel, which arms this the moment the travel is
38664
+ // decided and hands it over.
38665
+ const armRouteRenderWait = () => {
38479
38666
  let stopListening;
38480
38667
  const rendered = new Promise(resolve => {
38481
- // Listened for before the change, or a render landing while the change is
38482
- // being awaited is a render nobody heard.
38483
38668
  stopListening = observeRouteRender(resolve);
38484
38669
  });
38670
+ return {
38671
+ rendered,
38672
+ stop: () => stopListening()
38673
+ };
38674
+ };
38675
+ const whilePageRenders = async (page, change, wait = armRouteRenderWait()) => {
38485
38676
  try {
38486
38677
  await change();
38487
38678
  if (pageIsCurrent(page)) {
38488
- await rendered;
38679
+ await wait.rendered;
38489
38680
  }
38490
38681
  } finally {
38491
- stopListening();
38682
+ wait.stop();
38492
38683
  }
38493
38684
  };
38494
38685
 
@@ -38536,12 +38727,20 @@ const pageIsCurrent = ({
38536
38727
  }
38537
38728
  return params ? route.matchesParams(params) : true;
38538
38729
  };
38539
- // Every page is read, never only up to the one that answers yes: a page that is
38540
- // not the current one today is the one that must wake the reader tomorrow.
38730
+ // The FIRST page that answers, as with the branches of a <Route>: several
38731
+ // routes match at once — a literal one and the parameterized one it is a case of
38732
+ // ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
38733
+ // and the row has to be on the page the router is showing, which is the first
38734
+ // one written that matches.
38735
+ //
38736
+ // Every page is read all the same, never only up to the one that answers yes: a
38737
+ // page that is not the current one today is the one that must wake the reader
38738
+ // tomorrow.
38541
38739
  const currentPageIndex = pages => {
38542
38740
  let currentIndex = -1;
38543
38741
  for (let i = 0; i < pages.length; i++) {
38544
- if (pageIsCurrent(pages[i])) {
38742
+ const isCurrent = pageIsCurrent(pages[i]);
38743
+ if (isCurrent && currentIndex === -1) {
38545
38744
  currentIndex = i;
38546
38745
  }
38547
38746
  }
@@ -52361,7 +52560,7 @@ const PickerContentInsidePopup = props => {
52361
52560
  // above: those exist because "expand" already means something on the picker
52362
52561
  // itself, and this one does not. Popover ignores it, same as Dialog ignores
52363
52562
  // marginWithAnchor.
52364
- dockedOnTouch,
52563
+ dockedOnSmallTouchScreen,
52365
52564
  animation,
52366
52565
  ...rest
52367
52566
  } = props;
@@ -52409,7 +52608,7 @@ const PickerContentInsidePopup = props => {
52409
52608
  expand: isPopover ? undefined : dialogExpand,
52410
52609
  expandX: isPopover ? undefined : dialogExpandX,
52411
52610
  expandY: isPopover ? undefined : dialogExpandY,
52412
- dockedOnTouch: isPopover ? undefined : dockedOnTouch,
52611
+ dockedOnSmallTouchScreen: isPopover ? undefined : dockedOnSmallTouchScreen,
52413
52612
  children: jsx(PopupModeContext.Provider, {
52414
52613
  value: mode,
52415
52614
  children: children
@@ -54087,7 +54286,7 @@ const css$v = /* css */`
54087
54286
 
54088
54287
  /* Same reasoning, for the corners: a dialog squares off whatever corner
54089
54288
  lands on its container's own (see the data-flush-* rules in dialog.jsx —
54090
- a bottom sheet from dockedOnTouch squares its two bottom ones). A list
54289
+ a bottom sheet from dockedOnSmallTouchScreen squares its two bottom ones). A list
54091
54290
  drawn right against that corner has to square the same one, otherwise its
54092
54291
  own radius carves a notch out of the popup's square corner. Direct child
54093
54292
  only: any deeper and the list is presumably inset from the popup's edge,
@@ -71079,5 +71278,5 @@ const UserSvg = () => jsx("svg", {
71079
71278
  })
71080
71279
  });
71081
71280
 
71082
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
71281
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeSpin, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
71083
71282
  //# sourceMappingURL=jsenv_navi.js.map