@jsenv/navi 0.29.304 → 0.29.306

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.
@@ -4947,6 +4947,11 @@ const radioControllersByName = new Map();
4947
4947
  // The controller id is used as key — if the controller has no id, the value is not registered.
4948
4948
  const naviJsRegistry = new Map();
4949
4949
 
4950
+ // Controllers already reported for sharing their id with another one. An id is
4951
+ // stated on every render of its control, so without this the warning below
4952
+ // would repeat for as long as both controls are on screen.
4953
+ const controllersWarned = new WeakSet();
4954
+
4950
4955
  const getUIStateControllerById = (id) => controllersById.get(id);
4951
4956
  const getRadioSiblings = (radioUIStateController) => {
4952
4957
  const siblings = radioControllersByName.get(radioUIStateController.name);
@@ -4984,10 +4989,15 @@ const onUIStateControllerCreated = (uiStateController) => {
4984
4989
  if (id) {
4985
4990
  {
4986
4991
  const existing = controllersById.get(id);
4987
- if (existing && existing !== uiStateController) {
4992
+ if (
4993
+ existing &&
4994
+ existing !== uiStateController &&
4995
+ !controllersWarned.has(uiStateController)
4996
+ ) {
4997
+ controllersWarned.add(uiStateController);
4988
4998
  console.warn(
4989
4999
  `[navi] Two controls share the same id "${id}" ("${existing.controlType}" and "${controlType}"). ` +
4990
- `Lookups by id resolve to an arbitrary one of them, and when either unmounts it deletes the registry entry, leaving the surviving control unreachable (undefined uiState, dead picker buttons). ` +
5000
+ `The id resolves to whichever of them rendered last, so the other one is unreachable through it (undefined uiState, dead picker buttons). ` +
4991
5001
  `Common causes: several controls inside a single <Field> (the Field provides one id via context and every control consumes it — put each control in its own <Field>, or give one an explicit id), ` +
4992
5002
  `or the same content mounted twice (e.g. two popups sharing an id prefix while both hold their content).`,
4993
5003
  );
@@ -5020,8 +5030,15 @@ const onUIStateControllerCreated = (uiStateController) => {
5020
5030
  const onUIStateControllerDestroyed = (uiStateController) => {
5021
5031
  const { id, name, controlType } = uiStateController;
5022
5032
  if (id) {
5023
- controllersById.delete(id);
5024
- naviJsRegistry.delete(id);
5033
+ // Only the controller the id currently points at may take the entry away:
5034
+ // when two controls share an id, the one leaving would otherwise unregister
5035
+ // the one staying (see the warning in onUIStateControllerCreated), and the
5036
+ // same holds while a control is being replaced by its successor, which
5037
+ // registers during its render, before this cleanup runs.
5038
+ if (controllersById.get(id) === uiStateController) {
5039
+ controllersById.delete(id);
5040
+ naviJsRegistry.delete(id);
5041
+ }
5025
5042
  }
5026
5043
  const proxyFor = uiStateController.props["navi-control-proxy-for"];
5027
5044
  if (proxyFor) {
@@ -35277,17 +35294,21 @@ const useUIStateController = (
35277
35294
  if (el) {
35278
35295
  el.__uiStateController__ = controller;
35279
35296
  }
35280
- // Re-register so the radio registry stays in sync when props.ref changes
35281
- // identity (e.g. across a Suspense boundary). The render-phase call in
35282
- // control_hooks.jsx handles the initial mount; this call handles re-runs.
35283
- onUIStateControllerCreated(controller);
35284
35297
  return () => {
35285
35298
  if (el && el.__uiStateController__ === controller) {
35286
35299
  delete el.__uiStateController__;
35287
35300
  }
35288
- onUIStateControllerDestroyed(controller);
35289
35301
  };
35290
35302
  }, [controllerRef]);
35303
+ // The registry entry lives as long as the component does. Registration is
35304
+ // done by the render (see useInteractiveProps); this cleanup is the only
35305
+ // thing that takes the entry away, so it must not follow the ref: a ref
35306
+ // identity change would run it right after the render that registered.
35307
+ useLayoutEffect(() => {
35308
+ return () => {
35309
+ onUIStateControllerDestroyed(controller);
35310
+ };
35311
+ }, []);
35291
35312
 
35292
35313
  const { parentUIStateController: parentController } = scope;
35293
35314
  useLayoutEffect(() => {
@@ -38366,6 +38387,15 @@ const useInteractiveProps = (props, {
38366
38387
  const debugAction = useDebugAction();
38367
38388
  const debugInteraction = useDebugInteraction();
38368
38389
  const debugFocus = useDebugFocus();
38390
+
38391
+ // The very instance a run of this control is using, held in a SIGNAL because
38392
+ // the busy state below follows it: it is captured at navi_action_start and
38393
+ // released when the run settles (see onnavi_action_start).
38394
+ const runningActionSignalRef = useRef(null);
38395
+ if (runningActionSignalRef.current === null) {
38396
+ runningActionSignalRef.current = signal(null);
38397
+ }
38398
+ uiStateController.runningActionSignal = runningActionSignalRef.current;
38369
38399
  {
38370
38400
  const {
38371
38401
  autoFocus,
@@ -38404,7 +38434,15 @@ const useInteractiveProps = (props, {
38404
38434
  const isActionRequesterComputed = useMemo(() => parentActionRequesterSignal ? computed(() => parentActionRequesterSignal.value === ref.current) : null, [parentActionRequesterSignal]);
38405
38435
  const parallelGuard = useContext(ParallelGuardContext);
38406
38436
  const parentAction = useContext(ActionContext);
38407
- const actionStatus = useActionStatus(boundAction);
38437
+ // The instance the run in flight is using when there is one, and only
38438
+ // then boundAction: boundAction can be a proxy following the UI state
38439
+ // signal, and that state moves while a run is out — a popup content
38440
+ // unmounting takes the group's value with it. The proxy would resolve to
38441
+ // the instance for the new value, which is idle, and the control would
38442
+ // stop waiting on a run that is still going. What it waits on is the run
38443
+ // it started, from navi_action_start to its settlement.
38444
+ const runningAction = uiStateController.runningActionSignal.value;
38445
+ const actionStatus = useActionStatus(runningAction || boundAction);
38408
38446
  const networkPolicyReason = useNetworkPolicyReason();
38409
38447
  const {
38410
38448
  disabled,
@@ -38765,7 +38803,7 @@ const useInteractiveProps = (props, {
38765
38803
  // state has already moved to the new value by now — the proxy would
38766
38804
  // resolve to the instance for that new value, which is not the one
38767
38805
  // running.
38768
- uiStateController.runningAction?.abort(`superseded by a newer request on this control`);
38806
+ uiStateController.runningActionSignal.peek()?.abort(`superseded by a newer request on this control`);
38769
38807
  return;
38770
38808
  }
38771
38809
  debugAction(e, `executing action ${e.detail.action.callSource}`);
@@ -38787,14 +38825,14 @@ const useInteractiveProps = (props, {
38787
38825
  // this run (see the optimistic queue above), the state — and the
38788
38826
  // proxy's resolution — will have moved on.
38789
38827
  const runAction = e.detail.action;
38790
- uiStateController.runningAction = runAction.getCurrentAction?.() ?? runAction;
38828
+ uiStateController.runningActionSignal.value = runAction.getCurrentAction?.() ?? runAction;
38791
38829
  // Fires when the run's underlying work has settled — even for an
38792
38830
  // aborted run, whose promise is awaited to completion (see
38793
38831
  // performRun in actions.js) — which is exactly what "the server is
38794
38832
  // done with it" means, and therefore when the queued request may go.
38795
38833
  e.detail.addSideEffect(outcome => {
38796
38834
  uiStateController.actionInFlight = false;
38797
- uiStateController.runningAction = null;
38835
+ uiStateController.runningActionSignal.value = null;
38798
38836
  uiStateController.parallelGuard?.release(uiStateController);
38799
38837
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
38800
38838
  if (!queuedEvent) {
@@ -38899,11 +38937,13 @@ const useInteractiveProps = (props, {
38899
38937
  // an action's own completion side effect — has to read the signal to get an
38900
38938
  // answer that is not one frame late (see BUSY_CONSTRAINT).
38901
38939
  uiStateController.boundAction = boundAction;
38940
+ // "This id is that controller", stated on every render: a Suspense boundary
38941
+ // parking a subtree runs every hook cleanup below it — the unregistration
38942
+ // included — while the controllers themselves survive, and readers look a
38943
+ // controller up by id while rendering, before any effect can run again.
38944
+ // Placed after controlHostProps is set, which the constraints read.
38945
+ onUIStateControllerCreated(uiStateController);
38902
38946
  if (firstRender) {
38903
- // Deferred from the factory so these run after controlHostProps is set.
38904
- // Constraints like READONLY_CONSTRAINT and findControlProxyTargetController
38905
- // read controlHostProps — calling these earlier would throw or produce wrong results.
38906
- onUIStateControllerCreated(uiStateController);
38907
38947
  uiStateController.rules.validation.checkValidity();
38908
38948
  }
38909
38949
  return [controlRootProps, controlHostProps];
@@ -59115,14 +59155,22 @@ const clipOf = (side, distance) => {
59115
59155
  // before the next one takes it (see releaseGrowInProgress).
59116
59156
  const NAME = "navi-popup-grow";
59117
59157
  const NAME_PROPERTY = "view-transition-name";
59118
- // Worn by the root for the length of the movement — what the CSS keys the
59119
- // page's own opt-out on, and the movement's only trace in the document.
59158
+ // Worn by the root for the length of the movement, saying which way it goes
59159
+ // ("opening" | "closing") — what the CSS keys the page's own opt-out and the
59160
+ // clip near the anchor's end on, and the movement's only trace in the document.
59120
59161
  const ROOT_ATTRIBUTE = "data-navi-popup-grow";
59162
+ // What the two boxes are to each other (Dialog's `grow`: "box" | "scene"),
59163
+ // worn by the root too — it decides how each picture sits in the moving box.
59164
+ const KIND_ATTRIBUTE = "data-navi-popup-grow-kind";
59121
59165
  // Inside the popup, the one node that IS the anchor once it has grown.
59122
59166
  const TARGET_SELECTOR = "[data-grow]";
59123
59167
  // The popup's own animation duration, published on the root because the
59124
59168
  // ::view-transition tree hangs off it and inherits from nowhere else.
59125
59169
  const DURATION_PROPERTY = "--navi-popup-grow-duration";
59170
+ // The corners of the box being left, published the same way: the pictures are
59171
+ // clipped to the moving box (dialog.jsx), and a card with rounded corners must
59172
+ // not travel with square ones.
59173
+ const BORDER_RADIUS_PROPERTY = "--navi-popup-grow-border-radius";
59126
59174
 
59127
59175
  let releaseGrowInProgress = null;
59128
59176
 
@@ -59133,13 +59181,13 @@ let releaseGrowInProgress = null;
59133
59181
  * `opened` says which way: the box being left is the anchor when the popup is
59134
59182
  * opening and the popup when it is closing, and the arriving one is only known
59135
59183
  * once the change has been made (the content a popup grows into is built by
59136
- * that very change).
59184
+ * that very change). `grow` is Dialog's own prop of that name.
59137
59185
  */
59138
59186
  const growPopupFromAnchor = (
59139
59187
  popupEl,
59140
59188
  anchorElement,
59141
59189
  applyChange,
59142
- { opened },
59190
+ { opened, grow },
59143
59191
  ) => {
59144
59192
  const startViewTransition = ensureDocumentStartViewTransition();
59145
59193
  // A movement still wearing the name would make the name two elements wide,
@@ -59150,7 +59198,8 @@ const growPopupFromAnchor = (
59150
59198
  const elementLeaving = opened ? anchorElement : resolveGrowTarget(popupEl);
59151
59199
  const giveBackNameLeaving = wearGrowName(elementLeaving);
59152
59200
  const root = document.documentElement;
59153
- root.setAttribute(ROOT_ATTRIBUTE, "");
59201
+ root.setAttribute(ROOT_ATTRIBUTE, opened ? "opening" : "closing");
59202
+ root.setAttribute(KIND_ATTRIBUTE, grow);
59154
59203
  const duration = getComputedStyle(popupEl)
59155
59204
  .getPropertyValue("--popup-animation-duration")
59156
59205
  .trim();
@@ -59160,6 +59209,10 @@ const growPopupFromAnchor = (
59160
59209
  // browser's own pace.
59161
59210
  root.style.setProperty(DURATION_PROPERTY, duration);
59162
59211
  }
59212
+ const borderRadius = getComputedStyle(elementLeaving).borderRadius;
59213
+ if (borderRadius) {
59214
+ root.style.setProperty(BORDER_RADIUS_PROPERTY, borderRadius);
59215
+ }
59163
59216
 
59164
59217
  let giveBackNameArriving = null;
59165
59218
  const release = () => {
@@ -59170,7 +59223,9 @@ const growPopupFromAnchor = (
59170
59223
  giveBackNameLeaving();
59171
59224
  giveBackNameArriving?.();
59172
59225
  root.removeAttribute(ROOT_ATTRIBUTE);
59226
+ root.removeAttribute(KIND_ATTRIBUTE);
59173
59227
  root.style.removeProperty(DURATION_PROPERTY);
59228
+ root.style.removeProperty(BORDER_RADIUS_PROPERTY);
59174
59229
  };
59175
59230
  releaseGrowInProgress = release;
59176
59231
 
@@ -59516,6 +59571,15 @@ const css$E = /* css */`
59516
59571
  to filter). An entrance animation moves the dialog through scale and
59517
59572
  transform instead, which compose under it: see popup_css.js. */
59518
59573
 
59574
+ /* animation="growing" brings its own wall: the popup is the anchor
59575
+ continued, not a surface shown over a page that goes on being read (see
59576
+ navi_css_vars.js for the paint). Before the rules below, so everything
59577
+ said out loud — an outside press that captures, a backdropVariant, the
59578
+ two paint props — still wins over what the animation assumes. */
59579
+ &[data-growing] {
59580
+ --backdrop-background: var(--navi-backdrop-grow-background);
59581
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59582
+ }
59519
59583
  &[data-pointer-interaction-outside="capture"] {
59520
59584
  --backdrop-background: var(--navi-backdrop-capture-background);
59521
59585
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59686,6 +59750,15 @@ const css$E = /* css */`
59686
59750
  --backdrop-background: var(--navi-backdrop-close-background);
59687
59751
  --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
59688
59752
  }
59753
+ /* animation="growing" brings its own wall: the popup is the anchor
59754
+ continued, not a surface shown over a page that goes on being read (see
59755
+ navi_css_vars.js for the paint). Before the rules below, so everything
59756
+ said out loud — an outside press that captures, a backdropVariant, the
59757
+ two paint props — still wins over what the animation assumes. */
59758
+ &[data-growing] {
59759
+ --backdrop-background: var(--navi-backdrop-grow-background);
59760
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59761
+ }
59689
59762
  &[data-pointer-interaction-outside="capture"] {
59690
59763
  --backdrop-background: var(--navi-backdrop-capture-background);
59691
59764
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59734,6 +59807,73 @@ const css$E = /* css */`
59734
59807
  &::view-transition-new(navi-popup-grow) {
59735
59808
  animation-duration: var(--navi-popup-grow-duration, 0.25s);
59736
59809
  }
59810
+
59811
+ /* Each picture is drawn as wide as the box and as tall as it is, so a box
59812
+ that grows in height alone (a card keeping its width) would show the
59813
+ whole taller picture from the first frame, and the movement would read
59814
+ as a fade. Clipped to the box, the picture is uncovered as the box
59815
+ grows and covered back as it shrinks — behind the corners the box
59816
+ has in the page, published by popup_grow.js. */
59817
+ &::view-transition-image-pair(navi-popup-grow) {
59818
+ border-radius: var(--navi-popup-grow-border-radius, 0);
59819
+ overflow: clip;
59820
+ }
59821
+
59822
+ /* One scene through two frames (Dialog's grow="scene"): each picture
59823
+ covers the box, cropped around its centre, so what both frames show
59824
+ lands on itself. */
59825
+ &[data-navi-popup-grow-kind="scene"] {
59826
+ &::view-transition-old(navi-popup-grow),
59827
+ &::view-transition-new(navi-popup-grow) {
59828
+ height: 100%;
59829
+ object-fit: cover;
59830
+ }
59831
+ }
59832
+
59833
+ /* The anchor lives in the page, under the fixed bars; the popup lives in
59834
+ the top layer, over them. The pictures are painted above everything,
59835
+ bars included, so near the anchor's end of the movement the whole tree
59836
+ is clipped to the room between the bars — a thumbnail half under the
59837
+ top bar leaves from under it and comes back under it — and freed near
59838
+ the popup's end, where a tall dialog may stand over them. Clipped for
59839
+ the 65% of the time nearest the anchor: with the group's own ease that
59840
+ leaves the last 35% of the time to cover more than half of the trip,
59841
+ so the picture is out of the bars before the clip closes on it. */
59842
+ &::view-transition {
59843
+ animation: navi-popup-grow-clip var(--navi-popup-grow-duration, 0.25s)
59844
+ ease both;
59845
+ }
59846
+ &[data-navi-popup-grow="closing"]::view-transition {
59847
+ animation-direction: reverse;
59848
+ }
59849
+ }
59850
+ /* The bars sit at the app's own inset (fixed_bar.jsx), so the room they
59851
+ take starts there; both tokens are 0px where nothing takes any. */
59852
+ @keyframes navi-popup-grow-clip {
59853
+ 0%,
59854
+ 65% {
59855
+ clip-path: inset(
59856
+ calc(
59857
+ var(--navi-app-inset-top, 0px) +
59858
+ var(--navi-fixed-bar-space-top, 0px)
59859
+ )
59860
+ calc(
59861
+ var(--navi-app-inset-right, 0px) +
59862
+ var(--navi-fixed-bar-space-right, 0px)
59863
+ )
59864
+ calc(
59865
+ var(--navi-app-inset-bottom, 0px) +
59866
+ var(--navi-fixed-bar-space-bottom, 0px)
59867
+ )
59868
+ calc(
59869
+ var(--navi-app-inset-left, 0px) +
59870
+ var(--navi-fixed-bar-space-left, 0px)
59871
+ )
59872
+ );
59873
+ }
59874
+ 100% {
59875
+ clip-path: inset(0);
59876
+ }
59737
59877
  }
59738
59878
 
59739
59879
  ${surfaceTextCss}
@@ -59836,7 +59976,8 @@ const css$E = /* css */`
59836
59976
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
59837
59977
  * visible the backdrop is, independently of what it does. `"auto"`: the
59838
59978
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
59839
- * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
59979
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`), or the opaque wall
59980
+ * `animation="growing"` asks for. `"discrete"`: a
59840
59981
  * barely-there dim. `"invisible"`: fully transparent — a wall that is not
59841
59982
  * seen, still catching every press. This only changes how much the dialog
59842
59983
  * insists visually, never what an outside click does; whether there is a
@@ -59869,7 +60010,20 @@ const css$E = /* css */`
59869
60010
  * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
59870
60011
  * prop) and grows into whatever inside the dialog carries `data-grow`, the
59871
60012
  * dialog itself when nothing does. `"auto"` never picks it: only the caller
59872
- * knows the two boxes are one object. See `popup_grow.js`.
60013
+ * knows the two boxes are one object. It also brings its own backdrop —
60014
+ * opaque and blurred (`--navi-backdrop-grow-*`), the page it came out of
60015
+ * being what the movement leaves rather than a context to keep readable;
60016
+ * `backdropVariant="discrete"` asks for the light wash back. See
60017
+ * `popup_grow.js`.
60018
+ * @param {"box"|"scene"} [props.grow="box"] - Under `animation="growing"`,
60019
+ * what the anchor and what it grows into are to each other, which decides
60020
+ * how their pictures sit in the box moving between them. `"box"`: one
60021
+ * object at two sizes — a card gaining fields. Each picture is drawn at the
60022
+ * box's width from its top edge, and a box growing in height uncovers more
60023
+ * of it: the header stays where it is, the rest extends. `"scene"`: one
60024
+ * scene through two frames — a thumbnail and the map it is cut from. Each
60025
+ * picture covers the box, cropped around its centre and never distorted, so
60026
+ * what both frames show lands on itself.
59873
60027
  * @param {string} [props.animationDuration] - Maps to
59874
60028
  * `--popup-animation-duration`.
59875
60029
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
@@ -60212,6 +60366,7 @@ const useDialogProps = props => {
60212
60366
  // once, held at that size while open. See this prop's own JSDoc above.
60213
60367
  sizing = "auto",
60214
60368
  animation,
60369
+ grow = "box",
60215
60370
  // Inert unless sizeFromAnchor below (see this file's top comment) —
60216
60371
  // Dialog's own positioning is never relative to it.
60217
60372
  anchor,
@@ -60438,7 +60593,8 @@ const useDialogProps = props => {
60438
60593
  return;
60439
60594
  }
60440
60595
  growPopupFromAnchor(dialogEl, anchorElement, applyChange, {
60441
- opened
60596
+ opened,
60597
+ grow
60442
60598
  });
60443
60599
  } : null;
60444
60600
 
@@ -60888,6 +61044,7 @@ const useDialogProps = props => {
60888
61044
  "animationDuration": rest.animationDuration,
60889
61045
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
60890
61046
  "data-backdrop-variant": backdropVariant,
61047
+ "data-growing": growing ? "" : undefined,
60891
61048
  backdropColor,
60892
61049
  backdropFilter
60893
61050
  });
@@ -60923,6 +61080,11 @@ const useDialogProps = props => {
60923
61080
  // (and harmless for a local dialog, whose real backdrop element gets it
60924
61081
  // via backdropProps).
60925
61082
  "data-backdrop-variant": backdropVariant,
61083
+ // That this popup is the anchor continued rather than a surface shown
61084
+ // over the page, which is what its backdrop is painted from (see this
61085
+ // file's CSS). It cannot be read off navi-animation: growing arms no CSS
61086
+ // animation of its own, so that attribute is deliberately absent here.
61087
+ "data-growing": growing ? "" : undefined,
60926
61088
  // That this dialog has no wall, said in the DOM: the browser generates a
60927
61089
  // ::backdrop for the popover a wall-less top-layer dialog is shown as,
60928
61090
  // and the CSS above keys off this to leave it unpainted. Also the only
@@ -62777,6 +62939,7 @@ const css$C = /* css */`@layer navi {
62777
62939
  * popup meant to dock on a phone must not declare itself compact — see
62778
62940
  * `maxWidth` below.
62779
62941
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
62942
+ * @param {"box"|"scene"} [props.grow] - **Dialog-only**, same guard.
62780
62943
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
62781
62944
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
62782
62945
  * deliberately not homogenized here (each reads best for its own typical
@@ -62883,6 +63046,7 @@ const Popup = props => {
62883
63046
  // in the dialog branch, so they never reach the popover element.
62884
63047
  dockedOnSmallTouchScreen,
62885
63048
  sizeFromAnchor,
63049
+ grow,
62886
63050
  ...rest
62887
63051
  } = props;
62888
63052
  const [mode] = useResolvedPopupMode(modeProp, maxWidth, {
@@ -62901,6 +63065,7 @@ const Popup = props => {
62901
63065
  ...rest,
62902
63066
  dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
62903
63067
  sizeFromAnchor: sizeFromAnchor,
63068
+ grow: grow,
62904
63069
  maxWidth: maxWidth,
62905
63070
  pointerInteractionOutsideEffect: pointerInteractionOutsideEffect,
62906
63071
  backdrop: backdrop,
@@ -63360,19 +63525,26 @@ const PickerCustom = props => {
63360
63525
  commitUIStateAsAnswer(controller, closeEvent);
63361
63526
  }
63362
63527
  }
63363
- leaveExpanded({
63364
- isBack: closeEvent.detail.isCancel
63365
- });
63366
- // Reset so the next opening re-evaluates screen size
63367
- resetMode();
63368
63528
  const confirmEvent = confirmEventRef.current;
63369
63529
  confirmEventRef.current = null;
63370
63530
  if (confirmEvent && !closeEvent.detail.isCancel) {
63371
63531
  onConfirm?.(confirmEvent);
63372
63532
  }
63373
- // Last, after the value bookkeeping above: whoever listens reads the
63374
- // picker's value as it ends up — committed, or restored on a cancel.
63533
+ // After the value bookkeeping above — whoever listens reads the
63534
+ // picker's value as it ends up, committed or restored on a cancel —
63535
+ // and before leaveExpanded below, which is where the caller's
63536
+ // reaction runs for a Dialog too. A close that keeps goes back onto
63537
+ // the entry the popup was opened from while KEEPING the url as it
63538
+ // stands at that moment (see useNavState's leave()), so what onClose
63539
+ // spells into the address — a route param naming what was being
63540
+ // edited, cleared now that nothing is — has to be written before,
63541
+ // or the landing puts it back.
63375
63542
  onClose?.(closeEvent);
63543
+ leaveExpanded({
63544
+ isBack: closeEvent.detail.isCancel
63545
+ });
63546
+ // Reset so the next opening re-evaluates screen size
63547
+ resetMode();
63376
63548
  }
63377
63549
  };
63378
63550
  });
@@ -63766,6 +63938,7 @@ const PickerContentInsidePopup = props => {
63766
63938
  // its own width once lifted. Dialog's own `sizeFromAnchor`.
63767
63939
  dialogSizeFromAnchor,
63768
63940
  animation,
63941
+ grow,
63769
63942
  animationDuration,
63770
63943
  // mode="callout": what the callout says about what it holds, and paints
63771
63944
  // in its border and icon — "none" for a plain tooltip (see the callout
@@ -63844,6 +64017,7 @@ const PickerContentInsidePopup = props => {
63844
64017
  expandY: isPopover ? undefined : dialogExpandY,
63845
64018
  dockedOnSmallTouchScreen: isPopover ? undefined : dockedOnSmallTouchScreen,
63846
64019
  sizeFromAnchor: isPopover ? undefined : dialogSizeFromAnchor,
64020
+ grow: isPopover ? undefined : grow,
63847
64021
  children: jsx(PopupModeContext.Provider, {
63848
64022
  value: mode,
63849
64023
  children: children
@@ -72845,6 +73019,7 @@ const PickerFirstResolver = props => {
72845
73019
  * mode?: "popover" | "dialog" | "callout",
72846
73020
  * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
72847
73021
  * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "growing" | `slide-from-${string}`,
73022
+ * grow?: "box" | "scene",
72848
73023
  * animationDuration?: string,
72849
73024
  * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
72850
73025
  * calloutIcon?: boolean,
@@ -73135,7 +73310,9 @@ const PickerFirstResolver = props => {
73135
73310
  * the user validated: that is `action`. `closeEvent.detail.isCancel` tells a
73136
73311
  * cancel (Escape, the back gesture) from a close that keeps. Fires after the
73137
73312
  * picker's value has settled — committed, or restored on a cancel — so the
73138
- * value read in here is the one the picker ends up holding.
73313
+ * value read in here is the one the picker ends up holding, and before the
73314
+ * popup leaves the history entry it pushed, so a route param cleared in here
73315
+ * is part of the url a close that keeps carries back.
73139
73316
  * @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
73140
73317
  * right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
73141
73318
  * cross. "inherit" takes the picker's own font size.
@@ -73181,7 +73358,13 @@ const PickerFirstResolver = props => {
73181
73358
  * (the popup itself when nothing does), and back on close. The trigger IS
73182
73359
  * the anchor, so nothing has to be named for it — `dialogSizeFromAnchor`
73183
73360
  * for a card that must keep its width on the way, left out for a drawing
73184
- * that opens precisely to get bigger.
73361
+ * that opens precisely to get bigger. It brings an opaque, blurred backdrop
73362
+ * with it (`--navi-backdrop-grow-*`); `backdropVariant="discrete"` asks for
73363
+ * the light wash back.
73364
+ * @param {"box"|"scene"} [grow="box"] Dialog's own, under `animation="growing"`:
73365
+ * `"box"` for a card that extends (its top stays, the box uncovers the rest),
73366
+ * `"scene"` for a thumbnail and the bigger view it is cut from (each picture
73367
+ * covers the box, cropped around its centre).
73185
73368
  * @param {string} [animationDuration] The popup's own (`--popup-animation-duration`).
73186
73369
  * @param {boolean} [dialogSizeFromAnchor] Dialog mode: the dialog is as wide
73187
73370
  * as the trigger — its box as a floor (Dialog's own `sizeFromAnchor`) and