@jsenv/navi 0.30.26 → 0.30.27

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,7 @@
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, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
5
+ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal, getSafeAreaInsets } from "./jsenv_navi_side_effects.js";
6
6
  import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight, subscribeVisualViewportResizeSettled, subscribeVirtualKeyboardGeometryChange, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
@@ -783,6 +783,91 @@ const stringifyForDisplay = (
783
783
  return String(value);
784
784
  };
785
785
 
786
+ /*
787
+ * Who asks for an action instance right now, and what a run called off for
788
+ * want of askers owes to whoever comes next.
789
+ *
790
+ * Instances are shared by params: two route actions reading the same thing
791
+ * with the same params, or a route action and a component reading it with
792
+ * `run: true`, ask ONE instance. So a binding whose params turn falsy (its
793
+ * route left) does not own the run in flight: it calls it off only when
794
+ * nothing else asks for that instance at that moment. What asks is an asker:
795
+ * a binding of actionRunEffect while its params are truthy, a mounted
796
+ * `useAsyncData` reader with `run: true`. Each says which instance it asks for
797
+ * when the question is put, rather than registering one: a reader of a
798
+ * binding follows the binding's target the moment it moves, not at its next
799
+ * render.
800
+ *
801
+ * Calling a run off is a guess about who is coming: a reader mounting after
802
+ * it (the page arriving), or suspended before its first commit, finds an
803
+ * instance nobody will run again. So the instance is marked, and a `run: true`
804
+ * reader finding it marked asks again, as an arriving binding does when its
805
+ * run() finds the instance aborted. An abort decided by anyone else (abort(),
806
+ * a give-up on a busy popup) carries no mark: aborting does not undo, and the
807
+ * reader leaves it be.
808
+ */
809
+
810
+ const askerSet = createIterableWeakSet("action_asker");
811
+
812
+ /**
813
+ * `readAskedInstance()` returns what is asked for now: an instance, a binding
814
+ * (its current target is what counts), or null. The asker is held weakly:
815
+ * whoever adds it keeps it, and removes it once it asks for nothing for good.
816
+ */
817
+ const addActionAsker = (readAskedInstance) => {
818
+ const asker = { readAskedInstance };
819
+ askerSet.add(asker);
820
+ return asker;
821
+ };
822
+ const removeActionAsker = (asker) => {
823
+ askerSet.delete(asker);
824
+ };
825
+
826
+ const calledOffWeakSet = new WeakSet();
827
+
828
+ /**
829
+ * Aborts the instance's run unless an asker asks for it, and marks a run
830
+ * called off that way (see isCalledOffForWantOfAskers).
831
+ */
832
+ const callOffUnlessAsked = (action, reason) => {
833
+ const instance = resolveInstance(action);
834
+ for (const asker of askerSet) {
835
+ if (resolveInstance(asker.readAskedInstance()) === instance) {
836
+ return;
837
+ }
838
+ }
839
+ if (!instance.abort(reason)) {
840
+ return;
841
+ }
842
+ // A callback that ignores its abort signal completes all the same.
843
+ if (instance.runningStateSignal.peek() !== ABORTED) {
844
+ return;
845
+ }
846
+ calledOffWeakSet.add(instance);
847
+ // The mark is for this abort only: once the instance moves, an abort that
848
+ // comes later is somebody else's.
849
+ const unsubscribe = instance.runningStateSignal.subscribe((state) => {
850
+ if (state !== ABORTED) {
851
+ calledOffWeakSet.delete(instance);
852
+ unsubscribe();
853
+ }
854
+ });
855
+ };
856
+
857
+ const isCalledOffForWantOfAskers = (action) => {
858
+ return calledOffWeakSet.has(resolveInstance(action));
859
+ };
860
+
861
+ // A binding counts as the instance it targets now. Read as it stands, never
862
+ // through getCurrentAction(), which would flush a debounced binding.
863
+ const resolveInstance = (action) => {
864
+ let instance = action;
865
+ while (instance && instance.isProxy) {
866
+ instance = getActionPrivateProperties(instance).currentAction;
867
+ }
868
+ return instance;
869
+ };
870
+
786
871
  const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
787
872
 
788
873
  /*
@@ -5055,6 +5140,8 @@ const writeActionState = (write) => {
5055
5140
  }
5056
5141
  };
5057
5142
 
5143
+ const askerByBindingWeakMap = new WeakMap();
5144
+
5058
5145
  /**
5059
5146
  * Reactively runs an action whenever the params derived from signals change.
5060
5147
  *
@@ -5151,8 +5238,9 @@ const actionRunEffect = (
5151
5238
  if (actionTargetPrevious && actionTarget) {
5152
5239
  // params changed
5153
5240
  if (!asking) {
5154
- // falsy params, don't run
5155
- actionTargetPrevious.abort("abortOnFalsyParams");
5241
+ // falsy params, don't run; the instance is shared by params, and
5242
+ // something else may still ask for it (see action_askers.js)
5243
+ callOffUnlessAsked(actionTargetPrevious, "abortOnFalsyParams");
5156
5244
  return;
5157
5245
  }
5158
5246
  if (!askingBefore) {
@@ -5178,6 +5266,11 @@ const actionRunEffect = (
5178
5266
  },
5179
5267
  ...options,
5180
5268
  });
5269
+ // Held by the binding, as long as it lives: askers are held weakly.
5270
+ askerByBindingWeakMap.set(
5271
+ actionRunnedByThisEffect,
5272
+ addActionAsker(() => (asking ? actionRunnedByThisEffect : null)),
5273
+ );
5181
5274
  if (asking) {
5182
5275
  runUnwatched(() =>
5183
5276
  actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
@@ -9909,8 +10002,10 @@ const activeRouteReadsSignal = computed(() => {
9909
10002
  /**
9910
10003
  * Binds an action to a route: it runs when the route matches, with the params
9911
10004
  * the effect reads off the address, runs again on a `reload()`, and is aborted
9912
- * when the route is left. Its data, its wait and its failure are read by the
9913
- * page through `useAsyncData`.
10005
+ * when the route is left, unless something else still asks for the same read
10006
+ * with the same params: another route action, a mounted `useAsyncData` with
10007
+ * `run: true`. Its data, its wait and its failure are read by the page through
10008
+ * `useAsyncData`.
9914
10009
  *
9915
10010
  * A page's CODE is declared the same way — the import is one more thing the
9916
10011
  * address asks for, started with the data rather than after it:
@@ -31977,6 +32072,11 @@ const useForceRender = () => {
31977
32072
  * failure a prerun left (a `prerun: true` head start, a press that preran
31978
32073
  * it): nothing on screen asked for that run, so this asks again, once.
31979
32074
  *
32075
+ * While mounted, it asks for that instance as a route action does: a route
32076
+ * action reading the same thing with the same params does not call the run
32077
+ * off under it when its route is left. A run called off that way before
32078
+ * this component was there to ask is asked again.
32079
+ *
31980
32080
  * Prefer a `routeAction` wherever the parameter is one the address holds (a
31981
32081
  * path param, a search param bound to a `stateSignal`), popup included: it is
31982
32082
  * asked for when the address changes, with everything else that address
@@ -32126,6 +32226,20 @@ const useActionAsyncData = (action, {
32126
32226
  }
32127
32227
  useOnLoad(action, onLoad);
32128
32228
  usePoll(action, poll);
32229
+ const routePage = useContext(RoutePageContext);
32230
+
32231
+ // `run: true` asks for the instance as long as this reader is mounted: a
32232
+ // binding sharing it whose route is left does not call the run off under it
32233
+ // (see action_askers.js). Asked through the binding itself, whose target
32234
+ // moves before this renders again.
32235
+ const askedRef = useRef(null);
32236
+ askedRef.current = run ? action : null;
32237
+ useLayoutEffect(() => {
32238
+ const asker = addActionAsker(() => askedRef.current);
32239
+ return () => {
32240
+ removeActionAsker(asker);
32241
+ };
32242
+ }, []);
32129
32243
 
32130
32244
  // The instance the binding points at, remembered from one render to the
32131
32245
  // next: a binding that comes back to params whose instance failed — a search
@@ -32138,6 +32252,11 @@ const useActionAsyncData = (action, {
32138
32252
  const instanceRef = useRef(instance);
32139
32253
  const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
32140
32254
  instanceRef.current = instance;
32255
+ // A run called off because nothing asked for it any more, before this reader
32256
+ // was there to ask (mounting after, or suspended before its first commit):
32257
+ // nobody else will run it again. Not from a page the address has left, whose
32258
+ // own route action is what called it off.
32259
+ const calledOffBeforeThisReader = isCalledOffForWantOfAskers(action) && !(routePage && !routePage.isShown());
32141
32260
  // `run: true` — this component owns the request, so it is what starts it, and
32142
32261
  // it starts it from the render rather than from an effect: a component that
32143
32262
  // suspends has no effects, so a run written in one would be waiting for
@@ -32145,7 +32264,7 @@ const useActionAsyncData = (action, {
32145
32264
  // no-op — which is what makes starting it while rendering sound. From there
32146
32265
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
32147
32266
  // component under `loading: true`, exactly as for data someone else ran.
32148
- if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
32267
+ if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure || calledOffBeforeThisReader)) {
32149
32268
  // Nothing waits on this run: the failure is held by the action, and this
32150
32269
  // hook is what reads it back.
32151
32270
  runUnwatched(() => action.run({
@@ -32168,7 +32287,6 @@ const useActionAsyncData = (action, {
32168
32287
  // has already processed the settlement and the detached DOM is discarded.
32169
32288
  const runningState = action.runningStateSignal.peek();
32170
32289
  const [, setTick] = useState(0);
32171
- const routePage = useContext(RoutePageContext);
32172
32290
  // What this render read of the action, for the subscriptions below to tell
32173
32291
  // a change that came between the render and them from none at all.
32174
32292
  const readRef = useRef(null);
@@ -32376,6 +32494,13 @@ const useActionAsyncData = (action, {
32376
32494
  actionPendingPromiseWeakMap.delete(action);
32377
32495
  unsubscribe();
32378
32496
  resolve();
32497
+ } else if (reason === "loading" && state === ABORTED) {
32498
+ // The run waited on was called off: drawn again, this reader asks
32499
+ // for it again when nothing else will (calledOffBeforeThisReader),
32500
+ // or waits for the next run as idle.
32501
+ actionPendingPromiseWeakMap.delete(action);
32502
+ unsubscribe();
32503
+ resolve();
32379
32504
  }
32380
32505
  });
32381
32506
  });
@@ -59565,6 +59690,7 @@ const measureDrawing = ({
59565
59690
  * never seen under a bar, and after every read of the commit that queued them.
59566
59691
  */
59567
59692
 
59693
+
59568
59694
  // Several bars can share an edge — during a page transition the outgoing and
59569
59695
  // the incoming one are both mounted. They are all pinned to that same edge, so
59570
59696
  // they overlap: the room to give back is the largest of them, not their sum,
@@ -59582,6 +59708,12 @@ const pendingSizeMapByArea = new Map();
59582
59708
  let flushMicrotaskQueued = false;
59583
59709
  let flushFrame = null;
59584
59710
 
59711
+ // The safe area moves with these writes and no resize comes with them: what is
59712
+ // placed in it (a Popover with container="safe-area") is told here.
59713
+ const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
59714
+ createPubSub();
59715
+ const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
59716
+
59585
59717
  // A size a render produced: a prop, the children or a theme variable changed
59586
59718
  // the bar, and the room it takes is given back before that commit is painted.
59587
59719
  /**
@@ -59632,13 +59764,19 @@ const queueSize = (area, barElement, size) => {
59632
59764
  };
59633
59765
 
59634
59766
  const flushPendingSizes = () => {
59767
+ let written = false;
59635
59768
  for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
59636
59769
  for (const [barElement, size] of pendingSizeMap) {
59637
59770
  storeSize(area, barElement, size);
59638
59771
  }
59639
- writeSpace(area);
59772
+ if (writeSpace(area)) {
59773
+ written = true;
59774
+ }
59640
59775
  }
59641
59776
  pendingSizeMapByArea.clear();
59777
+ if (written) {
59778
+ publishFixedBarSpaceChange();
59779
+ }
59642
59780
  };
59643
59781
 
59644
59782
  const storeSize = (area, barElement, size) => {
@@ -59665,18 +59803,20 @@ const writeSpace = (area) => {
59665
59803
  const property = `--navi-fixed-bar-space-${area}`;
59666
59804
  const { style } = document.documentElement;
59667
59805
  if (sizeMap.size === 0) {
59668
- if (writtenValueByArea.has(area)) {
59669
- writtenValueByArea.delete(area);
59670
- style.removeProperty(property);
59806
+ if (!writtenValueByArea.has(area)) {
59807
+ return false;
59671
59808
  }
59672
- return;
59809
+ writtenValueByArea.delete(area);
59810
+ style.removeProperty(property);
59811
+ return true;
59673
59812
  }
59674
59813
  const value = `${largestSize}px`;
59675
59814
  if (writtenValueByArea.get(area) === value) {
59676
- return;
59815
+ return false;
59677
59816
  }
59678
59817
  writtenValueByArea.set(area, value);
59679
59818
  style.setProperty(property, value);
59819
+ return true;
59680
59820
  };
59681
59821
 
59682
59822
  installImportMetaCssBuild(import.meta);
@@ -74467,8 +74607,9 @@ const needsPopupFromMount = (props) => {
74467
74607
  * (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
74468
74608
  * with its head missing, then the head arriving from below). So the cut is
74469
74609
  * taken only where it hides something the screen would otherwise show — the
74470
- * band of glass beside an app narrowed with --navi-app-max-width — and is a
74471
- * constant everywhere else: at layer="top" the viewport's own edge cuts the
74610
+ * band of glass beside an app narrowed with --navi-app-max-width, the fixed
74611
+ * bars beside a popover placed in the safe area — and is a constant
74612
+ * everywhere else: at layer="top" the viewport's own edge cuts the
74472
74613
  * travel on the compositor for free, and at layer="local" the clip wrapper
74473
74614
  * (a static ancestor, overflow: hidden) does. Where a band exists the cut
74474
74615
  * still animates and still can drift under load; the full answer there is a
@@ -74600,22 +74741,34 @@ const popupCss = /* css */ `
74600
74741
  0 for a 0 band and 100vmax for any other, a step written without a
74601
74742
  branch. See this file's top comment for why an animated cut has to be
74602
74743
  that rare. */
74744
+ --x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
74745
+ --x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
74746
+ --x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
74747
+ --x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
74603
74748
  --x-popup-cut-top-at-area: calc(
74604
74749
  -1 * var(--container-position-room-top, 100vmax) - 100vmax +
74605
- min(100vmax, var(--navi-app-inset-top, 0px) * 1000000)
74750
+ min(100vmax, var(--x-popup-area-inset-top) * 1000000)
74606
74751
  );
74607
74752
  --x-popup-cut-right-at-area: calc(
74608
74753
  -1 * var(--container-position-room-right, 100vmax) - 100vmax +
74609
- min(100vmax, var(--navi-app-inset-right, 0px) * 1000000)
74754
+ min(100vmax, var(--x-popup-area-inset-right) * 1000000)
74610
74755
  );
74611
74756
  --x-popup-cut-bottom-at-area: calc(
74612
74757
  -1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
74613
- min(100vmax, var(--navi-app-inset-bottom, 0px) * 1000000)
74758
+ min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
74614
74759
  );
74615
74760
  --x-popup-cut-left-at-area: calc(
74616
74761
  -1 * var(--container-position-room-left, 100vmax) - 100vmax +
74617
- min(100vmax, var(--navi-app-inset-left, 0px) * 1000000)
74762
+ min(100vmax, var(--x-popup-area-inset-left) * 1000000)
74618
74763
  );
74764
+ /* Placed in the safe area (Popover's container="safe-area"): past its edge
74765
+ lie the fixed bars, and a popup sliding in comes out from behind them. */
74766
+ &[data-container="safe-area"] {
74767
+ --x-popup-area-inset-top: var(--navi-safe-area-inset-top);
74768
+ --x-popup-area-inset-right: var(--navi-safe-area-inset-right);
74769
+ --x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
74770
+ --x-popup-area-inset-left: var(--navi-safe-area-inset-left);
74771
+ }
74619
74772
  /* The translate property is deliberately absent from this list — it
74620
74773
  carries where the popup stands, and applyNewPosition (visible_rect.js)
74621
74774
  owns it and drives its own transition through the Web Animations API
@@ -78412,6 +78565,14 @@ const css$C = /* css */`
78412
78565
  * (`"3appw"`) or a container length (`"3cqw"`).
78413
78566
  * @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
78414
78567
  * between the popover and the edges of its anchor, written the same way.
78568
+ * @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
78569
+ * `layer="top"` popover is placed in: docked against with no anchor,
78570
+ * flipped and clamped within with one. `"app"`: the app's own screen, its
78571
+ * fixed bars included — the popover covers them. `"safe-area"`: that screen
78572
+ * less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
78573
+ * something docked over the page that must leave the bars uncovered, a
78574
+ * toast. Followed live as a bar comes or goes. Still in the top layer, so
78575
+ * above what the page holds. See docs/safe_area.md.
78415
78576
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
78416
78577
  * the popover and the page at all — asked before what an outside press does
78417
78578
  * and before how the backdrop is painted. `false` leaves the page
@@ -78841,6 +79002,7 @@ const usePopoverProps = props => {
78841
79002
  // "center" default.
78842
79003
  positionAreaWhenAnchorIsInvalid,
78843
79004
  marginWithContainer = 0,
79005
+ container = "app",
78844
79006
  // "close" → pointer press outside closes (a plain close = commit)
78845
79007
  // "cancel" → pointer press outside closes AS A CANCEL (revert)
78846
79008
  // "capture"→ absorb the press, stay open
@@ -78932,6 +79094,7 @@ const usePopoverProps = props => {
78932
79094
  })
78933
79095
  }) : null;
78934
79096
  const isTopLayer = layer === "top";
79097
+ const placesInSafeArea = isTopLayer && container === "safe-area";
78935
79098
  const ref = props.ref;
78936
79099
  const backdropRef = useRef();
78937
79100
  // Disarms a still-pending backdrop hide from a previous close (see
@@ -78980,14 +79143,15 @@ const usePopoverProps = props => {
78980
79143
  positionAreaFixed,
78981
79144
  positionAreaWhenAnchorIsInvalid,
78982
79145
  marginWithAnchor,
78983
- marginWithContainer
79146
+ marginWithContainer,
79147
+ placesInSafeArea
78984
79148
  };
78985
79149
  const repositionRef = useRef(null);
78986
79150
  useEffect(() => {
78987
79151
  repositionRef.current?.(new CustomEvent("position_props_change", {
78988
79152
  detail: {}
78989
79153
  }));
78990
- }, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
79154
+ }, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
78991
79155
  // The freeze is taken where the value changes, not only at open time: a
78992
79156
  // popover showing skeletons first says sizing="auto" until its content is
78993
79157
  // there, and would otherwise be held at the size of the waiting state.
@@ -79237,8 +79401,11 @@ const usePopoverProps = props => {
79237
79401
  positionAreaFixed,
79238
79402
  positionAreaWhenAnchorIsInvalid,
79239
79403
  marginWithAnchor,
79240
- marginWithContainer
79404
+ marginWithContainer,
79405
+ placesInSafeArea
79241
79406
  } = positionPropsRef.current;
79407
+ // undefined keeps the app's own screen (setPlacementViewportInsets).
79408
+ const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
79242
79409
  let position;
79243
79410
  if (hasAnchorElement) {
79244
79411
  const {
@@ -79277,6 +79444,7 @@ const usePopoverProps = props => {
79277
79444
  // renderer, same as omitting `container` entirely would resolve
79278
79445
  // to on its own.
79279
79446
  container: positionedAncestor,
79447
+ viewportInsets,
79280
79448
  minLeft,
79281
79449
  event: positionEvent
79282
79450
  };
@@ -79323,6 +79491,7 @@ const usePopoverProps = props => {
79323
79491
  const pickOptions = {
79324
79492
  positionArea,
79325
79493
  container: positionedAncestor,
79494
+ viewportInsets,
79326
79495
  marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
79327
79496
  event: positionEvent
79328
79497
  };
@@ -79444,6 +79613,20 @@ const usePopoverProps = props => {
79444
79613
  // while open (e.g. an expand/collapse toggle inside it) — not just when
79445
79614
  // the anchor itself moves/resizes/re-anchors.
79446
79615
  rectEffect.observeSize(popoverEl);
79616
+ // A bar mounting or leaving moves the safe area and leaves the viewport as
79617
+ // it was, so rectEffect.check() would find nothing changed: placed
79618
+ // directly. "resize", so the popover glides to its new place.
79619
+ if (isTopLayer) {
79620
+ addCleanup(onFixedBarSpaceChange(() => {
79621
+ if (!positionPropsRef.current.placesInSafeArea) {
79622
+ return;
79623
+ }
79624
+ if (deferPlacementWhileCovered(popoverEl)) {
79625
+ return;
79626
+ }
79627
+ placeUnlessAnchorOutOfView(new CustomEvent("resize"));
79628
+ }));
79629
+ }
79447
79630
  // Whatever its own backdrop: a modal dialog above leaves all of the page
79448
79631
  // inert, a popover without a wall included. layer="local" is left out, its
79449
79632
  // ceiling answers to its container.
@@ -79784,6 +79967,7 @@ const usePopoverProps = props => {
79784
79967
  id,
79785
79968
  tabIndex,
79786
79969
  "data-layer": layer,
79970
+ "data-container": placesInSafeArea ? "safe-area" : undefined,
79787
79971
  "data-expand-x": expand || expandX ? "" : undefined,
79788
79972
  "data-expand-y": expand || expandY ? "" : undefined,
79789
79973
  "navi-animation": isAutoAnimation ? undefined : animation,