@jsenv/navi 0.29.324 → 0.29.325

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.
package/README.md CHANGED
@@ -29,7 +29,7 @@ between related actions. Parent/child relations are first-class — `.one`,
29
29
 
30
30
  **`Box`** is the main layout primitive. It wraps CSS Flexbox with a friendlier API: `flex` for horizontal layout, `flex="y"` for vertical (no more guessing what `flex-direction: column` does visually). Supports `grid`, `inline`, alignment via `alignX`/`alignY`, and spacing props.
31
31
 
32
- **`createSlot`** renders content declared in one place at another: a toolbar, a status, or one `SidePanel` for a whole board that stays open as long as something fills it (`open={isFilled}`, see [docs/popup_open.md](./docs/popup_open.md)).
32
+ **`createSlot`** renders content declared in one place at another: a toolbar, a status, or one `SidePanel` for a whole board bound to the state that names what it shows (see [docs/popup_open.md](./docs/popup_open.md)).
33
33
 
34
34
  **`Text`** and related components (`Title`, `Paragraph`, `Code`, `Caption`) handle typography consistently across the app — including the parts of a line that are not text: icons, counts, truncation (`maxLines`), skeletons, and where a line may break. See [docs/typography.md](./docs/typography.md).
35
35
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
@@ -13207,6 +13207,12 @@ const useExecuteAction = (
13207
13207
  // whose command sent — is stored as the callout display target, so the
13208
13208
  // message appears on whoever asked rather than on the whole control.
13209
13209
  const element = elementRef.current;
13210
+ if (!element) {
13211
+ // The control left the page while its run was out (its own answer can
13212
+ // unmount it, see the outcome events below): there is no box to hang
13213
+ // the refusal on. onActionError is still told, by the run's side effect.
13214
+ return;
13215
+ }
13210
13216
  let target = requester;
13211
13217
  // A requester that is no longer on the page is not a place to show
13212
13218
  // anything: the clear cross leaves with the value it optimistically
@@ -13310,12 +13316,20 @@ const useExecuteAction = (
13310
13316
  // Either three callbacks, one per outcome, or a single one for "however
13311
13317
  // this ends" — it receives `{ aborted, reason }`, `{ error }` or
13312
13318
  // `{ data }`, so a caller that only needs to know the action settled
13313
- // (and whether it worked) does not have to register three.
13319
+ // (and whether it worked) does not have to register three. Each also
13320
+ // receives the outcome event (navi_action_abort/error/end) as its
13321
+ // second argument.
13314
13322
  const addSideEffect = (sideEffect) => {
13315
13323
  if (typeof sideEffect === "function") {
13316
- addAbortCallback((reason) => sideEffect({ aborted: true, reason }));
13317
- addErrorCallback((error) => sideEffect({ error }));
13318
- addCompleteCallback((data) => sideEffect({ data }));
13324
+ addAbortCallback((reason, outcomeEvent) =>
13325
+ sideEffect({ aborted: true, reason }, outcomeEvent),
13326
+ );
13327
+ addErrorCallback((error, outcomeEvent) =>
13328
+ sideEffect({ error }, outcomeEvent),
13329
+ );
13330
+ addCompleteCallback((data, outcomeEvent) =>
13331
+ sideEffect({ data }, outcomeEvent),
13332
+ );
13319
13333
  return;
13320
13334
  }
13321
13335
  const { abort, error, complete } = sideEffect;
@@ -13323,56 +13337,6 @@ const useExecuteAction = (
13323
13337
  addErrorCallback(error);
13324
13338
  addCompleteCallback(complete);
13325
13339
  };
13326
- addSideEffect({
13327
- abort: (reason) => {
13328
- const element = elementRef.current;
13329
- if (
13330
- // at this stage the action side effect might have removed the <element> from the DOM
13331
- // (in theory no because action side effect are batched to happen after)
13332
- // but other side effects might do this
13333
- element
13334
- ) {
13335
- dispatchInternalCustomEvent(element, "navi_action_abort", {
13336
- ...sharedActionEventDetail,
13337
- reason,
13338
- });
13339
- }
13340
- },
13341
- error: (error) => {
13342
- if (errorEffect === "show_validation_message") {
13343
- addErrorMessage(error, { requester });
13344
- } else if (errorEffect === "throw") {
13345
- setError(error);
13346
- }
13347
-
13348
- const element = elementRef.current;
13349
- if (
13350
- // at this stage the action side effect might have removed the <element> from the DOM
13351
- // (in theory no because action side effect are batched to happen after)
13352
- // but other side effects might do this
13353
- element
13354
- ) {
13355
- dispatchInternalCustomEvent(element, "navi_action_error", {
13356
- ...sharedActionEventDetail,
13357
- error,
13358
- });
13359
- }
13360
- },
13361
- complete: (data) => {
13362
- const element = elementRef.current;
13363
- if (
13364
- // at this stage the action side effect might have removed the <element> from the DOM
13365
- // (in theory no because action side effect are batched to happen after)
13366
- // but other side effects might do this
13367
- element
13368
- ) {
13369
- dispatchInternalCustomEvent(element, "navi_action_end", {
13370
- ...sharedActionEventDetail,
13371
- data,
13372
- });
13373
- }
13374
- },
13375
- });
13376
13340
 
13377
13341
  const actionStartEventDetail = {
13378
13342
  ...sharedActionEventDetail,
@@ -13384,13 +13348,50 @@ const useExecuteAction = (
13384
13348
  actionStartEventDetail,
13385
13349
  );
13386
13350
 
13351
+ // The outcome is told twice: to the element, when there still is one,
13352
+ // and to the side effects registered at navi_action_start, always. A
13353
+ // run can outlive its control — the run's own answer, written to a
13354
+ // store the parent reads to decide what it draws, unmounts the control
13355
+ // one microtask before the run settles — and the element is then no
13356
+ // place to dispatch to: Preact drops a detached element's listeners.
13357
+ // The side effects are the run's, not the element's, so the event
13358
+ // reaches them either way.
13359
+ const tellOutcome = (outcomeEventName, outcomeEventDetail, trigger) => {
13360
+ const outcomeEvent = createInternalCustomEvent(outcomeEventName, {
13361
+ ...sharedActionEventDetail,
13362
+ ...outcomeEventDetail,
13363
+ });
13364
+ const element = elementRef.current;
13365
+ if (element) {
13366
+ element.dispatchEvent(outcomeEvent);
13367
+ }
13368
+ trigger(outcomeEvent);
13369
+ };
13370
+
13387
13371
  const runAction = () => {
13388
13372
  return action[method]({
13389
13373
  event: actionEvent,
13390
13374
  reason: `"${event.type}" event on ${getElementSignature(event.target)}`,
13391
- onAbort: triggerAbort,
13392
- onError: triggerError,
13393
- onComplete: triggerComplete,
13375
+ onAbort: (reason) => {
13376
+ tellOutcome("navi_action_abort", { reason }, (outcomeEvent) => {
13377
+ triggerAbort(reason, outcomeEvent);
13378
+ });
13379
+ },
13380
+ onError: (error) => {
13381
+ if (errorEffect === "show_validation_message") {
13382
+ addErrorMessage(error, { requester });
13383
+ } else if (errorEffect === "throw") {
13384
+ setError(error);
13385
+ }
13386
+ tellOutcome("navi_action_error", { error }, (outcomeEvent) => {
13387
+ triggerError(error, outcomeEvent);
13388
+ });
13389
+ },
13390
+ onComplete: (data) => {
13391
+ tellOutcome("navi_action_end", { data }, (outcomeEvent) => {
13392
+ triggerComplete(data, outcomeEvent);
13393
+ });
13394
+ },
13394
13395
  });
13395
13396
  };
13396
13397
 
@@ -40910,10 +40911,24 @@ const useInteractiveProps = (props, {
40910
40911
  // aborted run, whose promise is awaited to completion (see
40911
40912
  // performRun in actions.js) — which is exactly what "the server is
40912
40913
  // done with it" means, and therefore when the queued request may go.
40913
- e.detail.addSideEffect(outcome => {
40914
+ e.detail.addSideEffect((outcome, outcomeEvent) => {
40914
40915
  uiStateController.actionInFlight = false;
40915
40916
  uiStateController.runningActionSignal.value = null;
40916
40917
  uiStateController.parallelGuard?.release(uiStateController);
40918
+ // The outcome callbacks are told from here, the run's side effect,
40919
+ // and not from the onnavi_action_* handlers below: the run can
40920
+ // outlive this control (its own answer, written to a store the
40921
+ // parent reads, unmounts it one microtask before it settles), and
40922
+ // whoever wears the run — a parent's `loading` following these,
40923
+ // see 12_picker_card_demo.html — must hear it end all the same.
40924
+ // The handlers below keep what needs the element.
40925
+ if (outcome.aborted) {
40926
+ onActionAborted?.(outcomeEvent);
40927
+ } else if ("error" in outcome) {
40928
+ onActionError?.(outcome.error, outcomeEvent);
40929
+ } else {
40930
+ onActionEnd?.(outcome.data, outcomeEvent);
40931
+ }
40917
40932
  const queuedEvent = uiStateController.queuedActionAllowedEvent;
40918
40933
  if (!queuedEvent) {
40919
40934
  syncOptimisticAttribute(false);
@@ -40953,7 +40968,6 @@ const useInteractiveProps = (props, {
40953
40968
  if (resetOnAbort && !superseded) {
40954
40969
  dispatchRequestResetUIState(e.currentTarget, e);
40955
40970
  }
40956
- onActionAborted?.(e);
40957
40971
  },
40958
40972
  onnavi_action_error: e => {
40959
40973
  const {
@@ -40963,7 +40977,6 @@ const useInteractiveProps = (props, {
40963
40977
  if (resetOnError) {
40964
40978
  dispatchRequestResetUIState(e.currentTarget, e);
40965
40979
  }
40966
- onActionError?.(error, e);
40967
40980
  uiStateController.onActionError(e);
40968
40981
  },
40969
40982
  onnavi_action_end: e => {
@@ -40971,7 +40984,6 @@ const useInteractiveProps = (props, {
40971
40984
  data
40972
40985
  } = e.detail;
40973
40986
  debugAction(e, `action end with data: ${JSON.stringify(data)}`);
40974
- onActionEnd?.(data, e);
40975
40987
  controlRootProps.onnavi_action_end?.(e);
40976
40988
  uiStateController.onActionEnd(e);
40977
40989
 
@@ -60451,34 +60463,44 @@ const css$E = /* css */`
60451
60463
  * `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
60452
60464
  * back-button dismissal, rest-of-document made inert), without one it is
60453
60465
  * shown through the Popover API and the page behind stays live.
60454
- * @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
60455
- * bottom sheet (docked flush to the bottom edge, full width) on a small touch
60456
- * screen, and leaves it alone otherwise. For a dialog meant to be interacted
60457
- * with rather than merely read: on a phone the keyboard owns the bottom of
60458
- * the screen and a centered box ends up both cramped and out of thumb reach,
60459
- * while under a mouse the centered box is already the right shape. Both
60460
- * halves of the name matter (`smallTouchScreenSignal`): touch alone would
60461
- * dock a big touch screen — a tablet, a kiosk panel — a whole screen away
60462
- * from where the finger just tapped, and size alone would dock a narrow
60463
- * desktop window, which is still a mouse. It supplies defaults for
60464
- * `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
60465
- * any of them can still be pinned explicitly — including `expandX={false}`,
60466
+ * @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] - Turns
60467
+ * the dialog into a sheet (docked flush to one edge, full width) on a small
60468
+ * touch screen, and leaves it alone otherwise: under a mouse the centered box
60469
+ * is already the right shape, while on a phone a centered box ends up cramped
60470
+ * between the keyboard and the top of the screen. The value says which edge
60471
+ * the sheet rests on. `true` (or `"top"`) rests it on the top edge, out of
60472
+ * the virtual keyboard's way: a field tapped in the sheet raises the keyboard
60473
+ * below it, and the question and the field stay where the finger found them.
60474
+ * `"bottom"` rests it on the bottom edge, where the thumbs are — for a sheet
60475
+ * that is read and tapped (a list, a couple of buttons, a confirmation) and
60476
+ * holds nothing that raises a keyboard; one that does is reflowed into the
60477
+ * strip left above the keyboard at every keystroke. The edge is said here
60478
+ * rather than through `positionArea`, which places both shapes:
60479
+ * `positionArea="bottom"` would also pin the centered box to the bottom of a
60480
+ * desktop window. Both halves of the name matter (`smallTouchScreenSignal`):
60481
+ * touch alone would dock a big touch screen — a tablet, a kiosk panel — a
60482
+ * whole screen away from where the finger just tapped, and size alone would
60483
+ * dock a narrow desktop window, which is still a mouse. It supplies defaults
60484
+ * for `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`,
60485
+ * so any of them can still be pinned explicitly — including `expandX={false}`,
60466
60486
  * which opts the docked dialog out of the full-width stretch and leaves it a
60467
- * floating box at the bottom. It also withdraws `maxWidth` while docked: a
60487
+ * floating box against its edge. It also withdraws `maxWidth` while docked: a
60468
60488
  * sheet is container-wide by definition, and a `maxWidth` is an answer about
60469
60489
  * the *centered* shape, so the two can be stated together (`maxWidth="16rem"
60470
60490
  * dockedOnSmallTouchScreen`) and each applies where it means something.
60471
60491
  * `minWidth` needs no such rule — its floor is below the full width — and
60472
60492
  * `maxHeight`/`minHeight` keep applying, a sheet being content-tall.
60473
- * Ignored entirely when `expandY`
60474
- * (or `expand`) is set: a dialog already filling the height is on the bottom
60475
- * edge docking would bring it to, so docking could only take away the shape
60476
- * the caller asked for. Re-resolves live as the pointer
60477
- * type or the window size changes. A sheet resting on the bottom edge is also
60478
- * pushed back down to close it, held by its header (a `Box` with the `header`
60479
- * prop) and by anything else carrying `data-swipe-grip`. The rest of the sheet
60480
- * is left to what it holds, so a board something is dragged across keeps its
60481
- * own gestures. See `swipe_to_close.js`.
60493
+ * Ignored entirely when `expandY` (or `expand`) is set: a dialog already
60494
+ * filling the height touches both edges, so docking could only take away the
60495
+ * shape the caller asked for. Re-resolves live as the pointer type or the
60496
+ * window size changes. A sheet is also pushed back through the edge it rests
60497
+ * on to close it (down for a bottom sheet, up for a top one), held by its
60498
+ * header (a `Box` with the `header` prop) and by anything else carrying
60499
+ * `data-swipe-grip`. The rest of the sheet is left to what it holds, so a
60500
+ * board something is dragged across keeps its own gestures. See
60501
+ * `swipe_to_close.js`. In dev, a `"bottom"` sheet found holding a field that
60502
+ * raises the keyboard is warned about at open, on any screen — the mistake
60503
+ * itself only shows on a phone.
60482
60504
  * @param {string} [props.positionArea="center"] - Where to dock the dialog
60483
60505
  * within its container (the viewport for `layer="top"`, the positioned
60484
60506
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
@@ -60846,28 +60868,28 @@ const DialogLocal = props => {
60846
60868
  * own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
60847
60869
  * has none at all.
60848
60870
  */
60849
- // What a dialog turns into on a small touch screen. "bottom" is not a taste:
60850
- // it puts the dialog in the zone a phone is actually operated from — where the
60851
- // thumbs rest and where the virtual keyboard comes up — instead of the middle
60852
- // of the screen, which is the farthest point from both.
60871
+ // What a dialog turns into on a small touch screen: a sheet flush against one
60872
+ // edge of the screen. Which edge is the caller's word (the prop's value, see
60873
+ // its JSDoc), the top one unless told "bottom": the cheap mistake must be the
60874
+ // default one — a sheet docked top by mistake is a little farther from the
60875
+ // thumb, a sheet docked bottom by mistake fights the virtual keyboard, which
60876
+ // rises from that very edge, on every keystroke.
60853
60877
  // Only defaults: an explicitly passed prop still wins, so the docked shape can
60854
60878
  // be adjusted one axis at a time instead of being all-or-nothing.
60855
60879
  const DOCKED = {
60856
- positionArea: "bottom",
60857
60880
  marginWithContainer: 0,
60858
60881
  expandX: true,
60859
- // A sheet resting on the bottom edge is dragged with a thumb, and a drag that
60860
- // runs past its own edge must not land on the page behind it: the same
60861
- // reasoning as "bottom" above, applied to the gesture instead of the shape.
60882
+ // A sheet is dragged with a thumb, and a drag that runs past its own edge
60883
+ // must not land on the page behind it.
60862
60884
  scrollCapture: true
60863
60885
  };
60864
60886
 
60865
- // Where a bottom sheet is held to push it back down: the strip a Box declares
60866
- // with `header`, plus anything the application marked as one more. Everything
60867
- // else in the sheet is content the finger came to operate — a board a piece is
60868
- // dragged across, a list, a map — and a press there belongs to it. A sheet with
60869
- // no header and nothing marked is not pushed down at all; it is closed by its
60870
- // own controls, by the backdrop and by Escape.
60887
+ // Where a sheet is held to push it back through its edge: the strip a Box
60888
+ // declares with `header`, plus anything the application marked as one more.
60889
+ // Everything else in the sheet is content the finger came to operate — a board
60890
+ // a piece is dragged across, a list, a map — and a press there belongs to it. A
60891
+ // sheet with no header and nothing marked is not pushed back at all; it is
60892
+ // closed by its own controls, by the backdrop and by Escape.
60871
60893
  const DOCKED_SWIPE_GRIP = "[data-header],[data-swipe-grip]";
60872
60894
  const useDialogProps = props => {
60873
60895
  const backdropProps = {};
@@ -60973,12 +60995,15 @@ const useDialogProps = props => {
60973
60995
  // Only a small touch screen changes anything: on a mouse — and on a touch
60974
60996
  // screen too big to reach the bottom edge of — a dialog already wants to be
60975
60997
  // the centered box it is by default, so there is nothing to resolve.
60976
- // expandY cancels the docking outright: docking exists to bring the dialog
60977
- // down to the edge the thumb is on, and a dialog already filling the height
60978
- // is on that edge — all docking could still do is take away the shape the
60979
- // caller asked for (and arm a swipe-down on something that never rose).
60998
+ // expandY cancels the docking outright: a dialog already filling the height
60999
+ // touches both edges — all docking could still do is take away the shape
61000
+ // the caller asked for (and arm a swipe on something that never rose).
60980
61001
  const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY;
60981
- const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
61002
+ const dockedEdge = dockedOnSmallTouchScreen === "bottom" ? "bottom" : "top";
61003
+ const positionArea = positionAreaProp ?? (isDocked ? dockedEdge : "center");
61004
+ // Read off the shape a phone would give, not the one this screen gives: the
61005
+ // author of a bottom sheet tests on a desktop, where the mistake is silent.
61006
+ const sheetRestsOnBottom = Boolean(dockedOnSmallTouchScreen) && !expandY && restsOnBottom(positionAreaProp ?? dockedEdge);
60982
61007
  const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
60983
61008
  // A share of whatever holds the dialog: the app's own screen for a
60984
61009
  // top-layer one — where appw is exactly "3% of the container", the
@@ -61040,12 +61065,12 @@ const useDialogProps = props => {
61040
61065
  y: "center",
61041
61066
  x: "center"
61042
61067
  };
61043
- // Pushing the sheet back down closes it — a bottom sheet is reached with a
61044
- // thumb, and the thumb is already on the edge it would push. Only a sheet
61045
- // actually resting on the bottom edge: anywhere else the gesture would send
61046
- // the dialog somewhere it never came from.
61047
- const swipeToCloseDown = isDocked && parsedPositionArea.y === "bottom";
61048
- const onSwipePointerDown = swipeToCloseDown ? createSwipeToClose("bottom", {
61068
+ // Pushing the sheet back through the edge it rests on closes it — the way
61069
+ // it came in, and the way the thumb on that edge already pushes. Only a sheet
61070
+ // actually resting on an edge: anywhere else the gesture would send the
61071
+ // dialog somewhere it never came from.
61072
+ const swipeToCloseSide = isDocked && (parsedPositionArea.y === "top" || parsedPositionArea.y === "bottom") ? parsedPositionArea.y : null;
61073
+ const onSwipePointerDown = swipeToCloseSide ? createSwipeToClose(swipeToCloseSide, {
61049
61074
  grip: DOCKED_SWIPE_GRIP
61050
61075
  }) : null;
61051
61076
  // A corner sitting exactly on the container's own corner must not be
@@ -61173,6 +61198,9 @@ const useDialogProps = props => {
61173
61198
  }
61174
61199
  {
61175
61200
  warnAboutUnreachableOutsideRegions(dialogEl);
61201
+ if (sheetRestsOnBottom) {
61202
+ warnBottomSheetHoldingKeyboardControl(dialogEl);
61203
+ }
61176
61204
  }
61177
61205
 
61178
61206
  // Set by useOpenControllerByProps for the very first open triggered by
@@ -61721,12 +61749,12 @@ const useDialogProps = props => {
61721
61749
  "data-flush-right": flushEdges.right ? "" : undefined,
61722
61750
  "data-flush-bottom": flushEdges.bottom ? "" : undefined,
61723
61751
  "data-flush-left": flushEdges.left ? "" : undefined,
61724
- "data-swipe-to-close": swipeToCloseDown ? "" : undefined,
61752
+ "data-swipe-to-close": swipeToCloseSide ?? undefined,
61725
61753
  // The axis the sheet travels on when it is pushed back, said to the shared
61726
61754
  // gesture layer: it is what a box travelling inside the sheet reads to know
61727
61755
  // this axis is already walked (see @jsenv/dom's drag_to_travel).
61728
- "data-drag-travel": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
61729
- "data-travel-by-drag": swipeToCloseDown ? SWIPE_AXIS_BY_SIDE.bottom : undefined,
61756
+ "data-drag-travel": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
61757
+ "data-travel-by-drag": swipeToCloseSide ? SWIPE_AXIS_BY_SIDE[swipeToCloseSide] : undefined,
61730
61758
  "onPointerDown": e => {
61731
61759
  rest.onPointerDown?.(e);
61732
61760
  onSwipePointerDown?.(e);
@@ -61798,6 +61826,33 @@ const useDialogProps = props => {
61798
61826
  }
61799
61827
  return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
61800
61828
  };
61829
+ const restsOnBottom = positionArea => {
61830
+ const parsed = parsePositionArea(positionArea);
61831
+ return Boolean(parsed) && parsed.y === "bottom";
61832
+ };
61833
+
61834
+ // What raises a virtual keyboard when tapped: a text-like input, a textarea,
61835
+ // anything editable. Not a `select` — it opens a menu, which takes no screen
61836
+ // from the sheet — and not a read-only field.
61837
+ const KEYBOARD_RAISING_CONTROL_SELECTOR = `
61838
+ input:not([type]):not([readonly]),
61839
+ input[type="text"]:not([readonly]),
61840
+ input[type="search"]:not([readonly]),
61841
+ input[type="email"]:not([readonly]),
61842
+ input[type="url"]:not([readonly]),
61843
+ input[type="tel"]:not([readonly]),
61844
+ input[type="password"]:not([readonly]),
61845
+ input[type="number"]:not([readonly]),
61846
+ textarea:not([readonly]),
61847
+ [contenteditable]:not([contenteditable="false"])
61848
+ `;
61849
+ const warnBottomSheetHoldingKeyboardControl = dialogEl => {
61850
+ const controlEl = dialogEl.querySelector(KEYBOARD_RAISING_CONTROL_SELECTOR);
61851
+ if (!controlEl) {
61852
+ return;
61853
+ }
61854
+ console.warn(`[navi] a Dialog docked to the bottom edge (dockedOnSmallTouchScreen="bottom") holds a field that raises the virtual keyboard. On a phone the keyboard rises from the edge the sheet rests on and reflows the sheet into the strip left above it, at every keystroke. A sheet one types into rests on the top edge: dockedOnSmallTouchScreen without a value. "bottom" is for a sheet one reads and taps.`, controlEl);
61855
+ };
61801
61856
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
61802
61857
 
61803
61858
  // Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
@@ -63529,8 +63584,8 @@ const css$C = /* css */`@layer navi {
63529
63584
  * @param {string} [props.positionAreaFixed] - **Popover-only**, same guard.
63530
63585
  * @param {string} [props.positionAreaWhenAnchorIsInvalid] - **Popover-only**,
63531
63586
  * same guard.
63532
- * @param {boolean} [props.dockedOnSmallTouchScreen] - **Dialog-only** (a
63533
- * popover is never a bottom sheet), destructured out for the same reason
63587
+ * @param {boolean|"top"|"bottom"} [props.dockedOnSmallTouchScreen] -
63588
+ * **Dialog-only** (a popover is never a sheet), destructured out for the same reason
63534
63589
  * turned around: it must not land on the popover element as a stray DOM
63535
63590
  * attribute when the screen-size resolution picks `mode="popover"`. Where
63536
63591
  * that resolution lands is what decides whether it applies at all, so a
@@ -63558,7 +63613,7 @@ const css$C = /* css */`@layer navi {
63558
63613
  * honoured in either mode — a wall-less `Dialog` is shown through the
63559
63614
  * Popover API rather than `showModal()` — so which mode the screen-size
63560
63615
  * resolution picks says nothing about whether the page behind stays live.
63561
- * It is also what makes a sheet docked to the bottom of a phone's screen
63616
+ * It is also what makes a sheet docked to an edge of a phone's screen
63562
63617
  * (`dockedOnSmallTouchScreen`) non-modal.
63563
63618
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
63564
63619
  * as-is to whichever component renders (both understand it identically):
@@ -76813,7 +76868,7 @@ const css$n = /* css */`.navi_split_button {
76813
76868
  * dialogExpand?: boolean,
76814
76869
  * dialogExpandX?: boolean,
76815
76870
  * dialogExpandY?: boolean,
76816
- * dockedOnSmallTouchScreen?: boolean,
76871
+ * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
76817
76872
  * marginWithContainer?: number | string,
76818
76873
  * backdrop?: boolean,
76819
76874
  * backdropVariant?: "auto" | "discrete" | "invisible",
@@ -86249,6 +86304,9 @@ const createSlot = (SlotRenderer = Box) => {
86249
86304
  slotPropsSignal.value = props;
86250
86305
  useLayoutEffect(() => {
86251
86306
  return () => {
86307
+ // Within one diff preact mounts the newcomer before unmounting the
86308
+ // leaver, so the leaver may no longer be the filler by the time it
86309
+ // is cleaned up.
86252
86310
  if (filler !== fillerRef) {
86253
86311
  return;
86254
86312
  }