@jsenv/navi 0.29.300 → 0.29.302

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.
@@ -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, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, isPressDrivenClick, 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, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, 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, Fragment as Fragment$1 } from "preact";
@@ -10127,6 +10127,11 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
10127
10127
  * happened. Registered through the same door an application uses (see
10128
10128
  * interaction_registry.js) — navi has no private one.
10129
10129
  *
10130
+ * `dblclick` is not among them, and deliberately: the browser only fires it for a
10131
+ * mouse, so an element declaring it would answer half the hands that reach it.
10132
+ * What a hand does twice is `double_click`, counted from the pointer in
10133
+ * interaction_press.js.
10134
+ *
10130
10135
  * `contextmenu` is the only one that takes something away: the browser's own menu
10131
10136
  * would cover the answer to the request it is. It takes it away AFTER, though —
10132
10137
  * a native interaction IS its own event, the very one the gate reads, and a gate
@@ -10140,7 +10145,6 @@ const NATIVE_TYPE_SET = new Set([
10140
10145
  "mousedown",
10141
10146
  "mouseup",
10142
10147
  "click",
10143
- "dblclick",
10144
10148
  "contextmenu",
10145
10149
  ]);
10146
10150
 
@@ -10198,6 +10202,15 @@ const SWIPE_THRESHOLD_DEFAULT = 0.33;
10198
10202
  const LONGPRESS_DELAY_DEFAULT = 450;
10199
10203
  // Past this the finger is going somewhere: it swipes, it does not hold.
10200
10204
  const LONGPRESS_SLOP_DEFAULT = 8;
10205
+ // The window a double click is counted in, opened by the FIRST press. Under the
10206
+ // hold's delay on purpose: a pause longer than the wait navi calls "held" is
10207
+ // longer than one gesture.
10208
+ const DOUBLE_CLICK_DELAY_DEFAULT = 400;
10209
+ // How far apart the two presses may be, and how far either of them may travel
10210
+ // before it is a gesture rather than a tap. A fingertip and not a pixel: between
10211
+ // the two the finger leaves the glass and lands again where it means to, which is
10212
+ // a wider question than the hold's slop above (has this finger stood still?).
10213
+ const DOUBLE_CLICK_SLOP_DEFAULT = 30;
10201
10214
  // How long the element takes to reach where the gesture leaves it, or to come
10202
10215
  // back. Written into the CSS below from here: the state is cleaned up when the
10203
10216
  // movement is over, so a duration living only in the stylesheet would be a timing
@@ -10209,11 +10222,19 @@ const SETTLE_DURATION_MS = 200;
10209
10222
  const SWIPE_THRESHOLD_ATTRIBUTE = "data-swipe-threshold";
10210
10223
  const LONGPRESS_DELAY_ATTRIBUTE = "data-longpress-delay";
10211
10224
  const LONGPRESS_SLOP_ATTRIBUTE = "data-longpress-slop";
10225
+ const DOUBLE_CLICK_DELAY_ATTRIBUTE = "data-double-click-delay";
10226
+ const DOUBLE_CLICK_SLOP_ATTRIBUTE = "data-double-click-slop";
10212
10227
 
10213
- // Which axes this element takes a swipe on, and that it takes a hold: said in the
10214
- // DOM at render time, for the CSS below and for the boxes above to read.
10228
+ // Which axes this element takes a swipe on, and that it takes a hold or counts
10229
+ // taps: said in the DOM at render time, for the CSS below and for the boxes above
10230
+ // to read.
10215
10231
  const SWIPE_AXES_ATTRIBUTE = "data-swipe";
10216
10232
  const LONGPRESS_ATTRIBUTE = "data-longpress";
10233
+ const DOUBLE_CLICK_ATTRIBUTE = "data-double-click";
10234
+
10235
+ const LONGPRESS = "longpress";
10236
+ const DOUBLE_CLICK = "double_click";
10237
+ const SINGLE_CLICK = "single_click";
10217
10238
 
10218
10239
  import.meta.css = /* css */ [`@property --swipe-progress {
10219
10240
  syntax: "<number>";
@@ -10239,15 +10260,19 @@ import.meta.css = /* css */ [`@property --swipe-progress {
10239
10260
  touch-action: none;
10240
10261
  }
10241
10262
 
10263
+ :where([data-double-click]) {
10264
+ touch-action: manipulation;
10265
+ }
10266
+
10242
10267
  [data-longpress] {
10243
10268
  -webkit-touch-callout: none;
10244
10269
  }
10245
10270
 
10246
- [data-longpress], [data-swipe] {
10271
+ [data-longpress], [data-swipe], [data-double-click] {
10247
10272
  user-select: none;
10248
10273
  }
10249
10274
 
10250
- :is([data-longpress], [data-swipe]) :is([data-drag-ignore], [popover], dialog), :is([data-longpress], [data-swipe]) :is(input:not([data-press-only]), textarea), :is([data-longpress], [data-swipe]) :is([contenteditable=""], [contenteditable="true"]) {
10275
+ :is([data-longpress], [data-swipe], [data-double-click]) :is([data-drag-ignore], [popover], dialog), :is([data-longpress], [data-swipe], [data-double-click]) :is(input:not([data-press-only]), textarea), :is([data-longpress], [data-swipe], [data-double-click]) :is([contenteditable=""], [contenteditable="true"]) {
10251
10276
  user-select: text;
10252
10277
  }
10253
10278
 
@@ -10266,9 +10291,13 @@ import.meta.css = /* css */ [`@property --swipe-progress {
10266
10291
 
10267
10292
  defineInteractionDetector({
10268
10293
  name: "press",
10269
- claims: (type) => type in AXIS_BY_SWIPE_TYPE || type === "longpress",
10270
- // A swipe and a hold are both "what this press turns out to be": until it
10271
- // turns out, the press is theirs.
10294
+ claims: (type) =>
10295
+ type in AXIS_BY_SWIPE_TYPE ||
10296
+ type === LONGPRESS ||
10297
+ type === DOUBLE_CLICK ||
10298
+ type === SINGLE_CLICK,
10299
+ // Every one of them is "what this press turns out to be": until it turns out,
10300
+ // the press is theirs.
10272
10301
  disputesPress: true,
10273
10302
  setup: (element, trigger, { types, readConfig }) => {
10274
10303
  let axes = "";
@@ -10278,7 +10307,10 @@ defineInteractionDetector({
10278
10307
  axes += axis;
10279
10308
  }
10280
10309
  }
10281
- const hasLongPress = types.includes("longpress");
10310
+ const hasLongPress = types.includes(LONGPRESS);
10311
+ const hasDoubleClick = types.includes(DOUBLE_CLICK);
10312
+ const hasSingleClick = types.includes(SINGLE_CLICK);
10313
+ const countsTaps = hasDoubleClick || hasSingleClick;
10282
10314
 
10283
10315
  const undo = [];
10284
10316
  const mark = (attribute, value) => {
@@ -10331,6 +10363,79 @@ defineInteractionDetector({
10331
10363
  if (hasLongPress) {
10332
10364
  mark(LONGPRESS_ATTRIBUTE, "");
10333
10365
  }
10366
+ if (countsTaps) {
10367
+ mark(DOUBLE_CLICK_ATTRIBUTE, "");
10368
+ }
10369
+
10370
+ // The tap a next press may pair with, and the window it is waited for in. It
10371
+ // outlives the press that made it — which is what a double click IS — so it
10372
+ // lives here rather than in the pointerdown below.
10373
+ let firstTap = null;
10374
+ let tapWindowTimeout = null;
10375
+ let tapWait = null;
10376
+ const forgetTaps = () => {
10377
+ clearTimeout(tapWindowTimeout);
10378
+ tapWindowTimeout = null;
10379
+ firstTap = null;
10380
+ tapWait?.cancel();
10381
+ tapWait = null;
10382
+ };
10383
+ undo.push(forgetTaps);
10384
+ // The window closes on a tap nothing came back for. Counted from the press
10385
+ // that opened it rather than from the tap that ends it, so what the caller
10386
+ // tunes is one rhythm and not a press plus a pause — a press slow enough to be
10387
+ // a hold therefore closes its own window on the spot, and cannot start a
10388
+ // double click.
10389
+ const openTapWindow = (pressEvent, tapEvent, delay) => {
10390
+ firstTap = {
10391
+ at: pressEvent.timeStamp,
10392
+ x: tapEvent.clientX,
10393
+ y: tapEvent.clientY,
10394
+ pointerType: tapEvent.pointerType,
10395
+ };
10396
+ if (hasSingleClick) {
10397
+ // The element's click is navi's from here: it is held for the length of
10398
+ // the window and `single_click` is what says it happened (see the top of
10399
+ // this file).
10400
+ const clickSuppressionIsOver = suppressClickAfterGesture();
10401
+ clickSuppressionIsOver();
10402
+ }
10403
+ clearTimeout(tapWindowTimeout);
10404
+ tapWindowTimeout = setTimeout(
10405
+ () => {
10406
+ tapWindowTimeout = null;
10407
+ firstTap = null;
10408
+ if (hasSingleClick) {
10409
+ trigger(SINGLE_CLICK, tapEvent, {
10410
+ pointerType: tapEvent.pointerType,
10411
+ });
10412
+ }
10413
+ },
10414
+ delay - (tapEvent.timeStamp - pressEvent.timeStamp),
10415
+ );
10416
+ };
10417
+
10418
+ if (hasSingleClick) {
10419
+ // A click no press made — a keyboard activation, an `element.click()`. The
10420
+ // window exists to find out whether a second press is coming, and there is
10421
+ // no press here, so it is not held: said at once, the way the browser's own
10422
+ // click would have been.
10423
+ const onClick = (clickEvent) => {
10424
+ if (isPressDrivenClick(clickEvent)) {
10425
+ // A press's own click, swallowed by the suppression armed at its tap:
10426
+ // it never reaches here, and if it does (the suppression having already
10427
+ // been spent) the tap has said it or is about to.
10428
+ return;
10429
+ }
10430
+ trigger(SINGLE_CLICK, clickEvent, {
10431
+ pointerType: clickEvent.pointerType,
10432
+ });
10433
+ };
10434
+ element.addEventListener("click", onClick);
10435
+ undo.push(() => {
10436
+ element.removeEventListener("click", onClick);
10437
+ });
10438
+ }
10334
10439
 
10335
10440
  const onPointerDown = (pointerDownEvent) => {
10336
10441
  if (pointerDownEvent.button !== 0) {
@@ -10338,6 +10443,52 @@ defineInteractionDetector({
10338
10443
  }
10339
10444
  let swipe = null;
10340
10445
  let press = null;
10446
+ // Set below, with the hold; a no-op until then so the swipe can call it.
10447
+ let forgetInnerLongPress = () => {};
10448
+
10449
+ if (countsTaps) {
10450
+ const delay = readConfig(
10451
+ DOUBLE_CLICK_DELAY_ATTRIBUTE,
10452
+ DOUBLE_CLICK_DELAY_DEFAULT,
10453
+ );
10454
+ const slop = readConfig(
10455
+ DOUBLE_CLICK_SLOP_ATTRIBUTE,
10456
+ DOUBLE_CLICK_SLOP_DEFAULT,
10457
+ );
10458
+ // Whether this press is the second of a pair is settled HERE, when it
10459
+ // lands, and not when it is let go of: the window is about where the
10460
+ // hand went and how soon, and a second press held a little longer than
10461
+ // the window would otherwise be answered as a lone click while it is
10462
+ // still down.
10463
+ const completesTheFirstTap =
10464
+ firstTap && continuesTap(firstTap, pointerDownEvent, delay, slop);
10465
+ if (completesTheFirstTap) {
10466
+ clearTimeout(tapWindowTimeout);
10467
+ tapWindowTimeout = null;
10468
+ } else {
10469
+ forgetTaps();
10470
+ }
10471
+ tapWait = waitForTap(pointerDownEvent, {
10472
+ slop,
10473
+ onTap: (tapEvent) => {
10474
+ tapWait = null;
10475
+ if (completesTheFirstTap) {
10476
+ firstTap = null;
10477
+ // The click this second press leaves behind was already answered,
10478
+ // by the gesture the two of them made. Armed and released at once:
10479
+ // the release does not lift the suppression, it says the gesture is
10480
+ // over (see suppressClickAfterGesture).
10481
+ const clickSuppressionIsOver = suppressClickAfterGesture();
10482
+ clickSuppressionIsOver();
10483
+ trigger(DOUBLE_CLICK, tapEvent, {
10484
+ pointerType: tapEvent.pointerType,
10485
+ });
10486
+ return;
10487
+ }
10488
+ openTapWindow(pointerDownEvent, tapEvent, delay);
10489
+ },
10490
+ });
10491
+ }
10341
10492
 
10342
10493
  if (axes) {
10343
10494
  swipe = startSwipe(pointerDownEvent, {
@@ -10357,18 +10508,46 @@ defineInteractionDetector({
10357
10508
  onSwipeStart: () => {
10358
10509
  press?.cancel();
10359
10510
  press = null;
10511
+ forgetInnerLongPress();
10512
+ // …and it is not a tap either, nor the second of a pair: the press
10513
+ // has left, and what it left is a swipe.
10514
+ forgetTaps();
10360
10515
  },
10361
10516
  });
10362
10517
  }
10363
10518
  if (hasLongPress) {
10519
+ // A hold declared INSIDE this element, on this same press: the nearer
10520
+ // one answers, the way a click is the innermost target's, and this
10521
+ // wait is given up — two sheets opening from one hold is nobody's
10522
+ // intention. Delays being equal, the inner timer was set first (the
10523
+ // press reached it first) and fires first; the inner hold arrives here
10524
+ // as its own event, bubbling, before this timer runs.
10525
+ const onInnerLongPress = (longPressEvent) => {
10526
+ if (longPressEvent.target === element || !press) {
10527
+ return;
10528
+ }
10529
+ press.cancel();
10530
+ press = null;
10531
+ forgetInnerLongPress();
10532
+ warnOnceAboutHoldTakenByInner(element, longPressEvent.target);
10533
+ };
10534
+ forgetInnerLongPress = () => {
10535
+ element.removeEventListener("longpress", onInnerLongPress);
10536
+ };
10537
+ element.addEventListener("longpress", onInnerLongPress);
10364
10538
  press = waitForPressHeld(pointerDownEvent, {
10365
10539
  delay: readConfig(LONGPRESS_DELAY_ATTRIBUTE, LONGPRESS_DELAY_DEFAULT),
10366
10540
  slop: readConfig(LONGPRESS_SLOP_ATTRIBUTE, LONGPRESS_SLOP_DEFAULT),
10541
+ onPressCancel: forgetInnerLongPress,
10367
10542
  onPressHeld: (pressEvent, { endPress }) => {
10543
+ forgetInnerLongPress();
10368
10544
  // The hold won the arbitration: the swipe never got the distance it
10369
- // needed, and must not get it from whatever the finger does next.
10545
+ // needed, and must not get it from whatever the finger does next. The
10546
+ // double click loses the same way — a press held this long is the
10547
+ // hold's whether it is the first of a pair or the second.
10370
10548
  swipe?.stop();
10371
10549
  swipe = null;
10550
+ forgetTaps();
10372
10551
  const clickSuppressionIsOver = suppressClickAfterGesture();
10373
10552
  const onPointerEnd = () => {
10374
10553
  window.removeEventListener("pointerup", onPointerEnd, true);
@@ -10397,6 +10576,11 @@ defineInteractionDetector({
10397
10576
  };
10398
10577
  },
10399
10578
  });
10579
+ const warnOnceAboutHoldTakenByInner = (element, innerElement) => {
10580
+ {
10581
+ return;
10582
+ }
10583
+ };
10400
10584
 
10401
10585
  const startSwipe = (
10402
10586
  pointerDownEvent,
@@ -10498,6 +10682,21 @@ const startSwipe = (
10498
10682
  });
10499
10683
  };
10500
10684
 
10685
+ // Whether a press landing now completes the tap before it: the same hand, soon
10686
+ // enough, and near enough. Axis by axis rather than as a distance, the way every
10687
+ // other slop in this family is read.
10688
+ const continuesTap = (tap, pointerDownEvent, delay, slop) => {
10689
+ if (pointerDownEvent.pointerType !== tap.pointerType) {
10690
+ return false;
10691
+ }
10692
+ if (pointerDownEvent.timeStamp - tap.at >= delay) {
10693
+ return false;
10694
+ }
10695
+ const xApart = Math.abs(pointerDownEvent.clientX - tap.x);
10696
+ const yApart = Math.abs(pointerDownEvent.clientY - tap.y);
10697
+ return xApart < slop && yApart < slop;
10698
+ };
10699
+
10501
10700
  const swipeTypeOf = (axis, pulled) => {
10502
10701
  const { positive, negative } = SWIPE_TYPE_BY_AXIS[axis];
10503
10702
  return pulled > 0 ? positive : negative;
@@ -26192,6 +26391,31 @@ const getHrefTargetInfo = (href) => {
26192
26391
  };
26193
26392
  };
26194
26393
 
26394
+ /*
26395
+ * An app can ship more than one document — an admin panel, a status page, an
26396
+ * entry point of its own — and a link to one of them is not a route, it is a
26397
+ * page load. The address does not say which of the two it is, and guessing
26398
+ * would be worse than not knowing: "no route matches, so it must be another
26399
+ * document" turns a typo'd in-app url into a silent reload of the app instead
26400
+ * of its own not-found screen. So the link says it.
26401
+ *
26402
+ * `<Link document>` / `<Button document>` is that, and the point of saying it
26403
+ * on a link rather than navigating from an action is everything a link is: an
26404
+ * address in the status bar, a middle click, "open in new tab", something for
26405
+ * assistive technology to read. Only the interception steps aside; the element
26406
+ * stays a real link and the browser loads the page.
26407
+ *
26408
+ * It travels as an attribute for the same reason `replace` does (see
26409
+ * link_replace.js): whoever answers the press sees the element, not the
26410
+ * component that rendered it.
26411
+ */
26412
+
26413
+ const LINK_DOCUMENT_ATTRIBUTE = "data-navi-document";
26414
+
26415
+ const linkAsksForDocument = (linkElement) => {
26416
+ return linkElement.hasAttribute(LINK_DOCUMENT_ATTRIBUTE);
26417
+ };
26418
+
26195
26419
  /*
26196
26420
  * A press aims at a place; it does not always go one step deeper. A row of tabs
26197
26421
  * is a lateral move — the neighbour is one finger away — so the whole row should
@@ -26525,6 +26749,12 @@ const setupBrowserIntegrationViaHistory = ({
26525
26749
  }
26526
26750
  return;
26527
26751
  }
26752
+ if (linkAsksForDocument(linkElement)) {
26753
+ // The link says its address is another document of this origin (see
26754
+ // link_document.js). Routing to it would land on the fallback route
26755
+ // instead of loading the page.
26756
+ return;
26757
+ }
26528
26758
  // Nothing here declared a route, so there is nothing to route to: the
26529
26759
  // page is a plain document and a link in it is a plain link. Taking it
26530
26760
  // over anyway would push the url and then have nothing to show for it —
@@ -27074,6 +27304,160 @@ const Head = ({
27074
27304
  return null;
27075
27305
  };
27076
27306
 
27307
+ /**
27308
+ * A page is `{ route, params }`, never the route alone: a section of a page is
27309
+ * as often a PARAM as it is a route of its own, and three branches of the same
27310
+ * route told apart by their params are three pages one walks between.
27311
+ * `params` is undefined for a page that is a route on its own.
27312
+ *
27313
+ * Whether such a page is the one on screen is asked from three places — the
27314
+ * row a <RouteTravel> walks, the relations a movement is written between, and
27315
+ * the fallback naming the pages it is the absence of — and they must all get
27316
+ * the same answer, which is why the reading lives here rather than next to any
27317
+ * one of them.
27318
+ */
27319
+
27320
+ // `matchesParams` reads paramsSignal, so a caller reading this during a render
27321
+ // is subscribed to the param changes that walk from one tab to the next —
27322
+ // matchingSignal alone never moves there, and a row whose tabs are params of
27323
+ // one route would never re-render.
27324
+ //
27325
+ // The params are read only for a route that matches, and that is not a signal
27326
+ // left unread: a reader wakes on anything it read last time, so what matters is
27327
+ // that everything able to make this answer change is among them.
27328
+ // matchingSignal is read whatever happens, and it is a NECESSARY condition —
27329
+ // while it is false no param of that route can put this page on screen, and the
27330
+ // day one could, matchingSignal itself has to turn true to say so, which is the
27331
+ // read that brings the params back in. (Asking anyway would be worse than
27332
+ // useless: the params of a route that does not match are not params.)
27333
+ const pageIsCurrent = ({ route, params }) => {
27334
+ if (!route.matchingSignal.value) {
27335
+ return false;
27336
+ }
27337
+ return params ? route.matchesParams(params) : true;
27338
+ };
27339
+
27340
+ /**
27341
+ * The page a <Route> container shows when none of its branches matched, given
27342
+ * a name.
27343
+ *
27344
+ * Every other branch of a container is a route, and a route is a thing one can
27345
+ * talk ABOUT from outside the tree: write a movement between two of them (see
27346
+ * route_transition.jsx), put one in a row (see route_travel.jsx), read whether
27347
+ * it is on screen. The fallback branch is the one page defined by an ABSENCE —
27348
+ * "an address none of these claim" — so it has no url pattern to be declared
27349
+ * from, and until it is named, nothing can be said about it.
27350
+ *
27351
+ * const NOT_FOUND_PAGE = routeFallback();
27352
+ * <Route fallback={NOT_FOUND_PAGE} element={NotFoundPage} />
27353
+ * defineRouteTransition(null, NOT_FOUND_PAGE, "slide-x");
27354
+ *
27355
+ * The absence is of a PARTICULAR set of claims — the branches of the container
27356
+ * holding it — and that set is not knowable from the routes of the application:
27357
+ * a route matching every address is a normal thing to declare (a carrier for
27358
+ * search params and route actions the whole app shares), and it would answer
27359
+ * "something matched" everywhere. So the container fills it in from the
27360
+ * children it was written with (see collectBranches in route.jsx), and a
27361
+ * fallback no container holds claims nothing.
27362
+ *
27363
+ * That leaves what a container cannot see: the address its own subtree lives
27364
+ * under. A section owning its sub-router renders it several components below
27365
+ * the leaf that matched the section's prefix, and "none of these five matched"
27366
+ * is then true on every screen of the application. Such a fallback names the
27367
+ * route it is under, and answers only there:
27368
+ *
27369
+ * const DASHBOARD_NOT_FOUND_PAGE = routeFallback(DASHBOARD_SECTION_ROUTE);
27370
+ */
27371
+
27372
+
27373
+ const routeFallbackPrivatePropertiesMap = new WeakMap();
27374
+
27375
+ /**
27376
+ * Names the fallback branch of a <Route> container, so relations can be
27377
+ * written about the page it shows and its `matchingSignal` read from anywhere.
27378
+ *
27379
+ * @param {object} [scopeRoute] - the route this container's subtree lives
27380
+ * under, when the container is not the application's own router. Without it
27381
+ * the fallback answers wherever its container is on screen.
27382
+ * @returns {object} pass it as `<Route fallback={...}>`.
27383
+ */
27384
+ const routeFallback = (scopeRoute) => {
27385
+ const pagesSignal = signal(null);
27386
+ const nothingMatches = () => {
27387
+ const pages = pagesSignal.value;
27388
+ if (!pages) {
27389
+ return false;
27390
+ }
27391
+ for (const page of pages) {
27392
+ if (pageIsCurrent(page)) {
27393
+ return false;
27394
+ }
27395
+ }
27396
+ return true;
27397
+ };
27398
+ const routeFallback = {
27399
+ isRouteFallback: true,
27400
+ scopeRoute,
27401
+ matchingSignal: scopeRoute
27402
+ ? computed(() => {
27403
+ if (!scopeRoute.matchingSignal.value) {
27404
+ return false;
27405
+ }
27406
+ return nothingMatches();
27407
+ })
27408
+ : computed(nothingMatches),
27409
+ toString: () => {
27410
+ if (scopeRoute) {
27411
+ return `route fallback under ${scopeRoute}`;
27412
+ }
27413
+ return `route fallback`;
27414
+ },
27415
+ };
27416
+ Object.preventExtensions(routeFallback);
27417
+ routeFallbackPrivatePropertiesMap.set(routeFallback, { pagesSignal });
27418
+ return routeFallback;
27419
+ };
27420
+
27421
+ /**
27422
+ * The pages the fallback is the absence of, handed over by the container
27423
+ * holding it. Read as a signal rather than at render: a movement is decided
27424
+ * while rendering is held, before the container has had a chance to choose a
27425
+ * branch (see rendering_hold.js), so "we are on the fallback" has to be
27426
+ * answerable from the route signals alone.
27427
+ */
27428
+ const wireRouteFallback = (routeFallback, pages) => {
27429
+ const { pagesSignal } = routeFallbackPrivatePropertiesMap.get(routeFallback);
27430
+ const pagesCurrent = pagesSignal.peek();
27431
+ if (pagesCurrent && samePages(pagesCurrent, pages)) {
27432
+ return;
27433
+ }
27434
+ pagesSignal.value = pages;
27435
+ };
27436
+
27437
+ const unwireRouteFallback = (routeFallback) => {
27438
+ const { pagesSignal } = routeFallbackPrivatePropertiesMap.get(routeFallback);
27439
+ pagesSignal.value = null;
27440
+ };
27441
+
27442
+ const samePages = (pages, otherPages) => {
27443
+ if (pages.length !== otherPages.length) {
27444
+ return false;
27445
+ }
27446
+ let i = 0;
27447
+ while (i < pages.length) {
27448
+ const page = pages[i];
27449
+ const otherPage = otherPages[i];
27450
+ if (page.route !== otherPage.route) {
27451
+ return false;
27452
+ }
27453
+ if (!compareTwoJsValues(page.params, otherPage.params)) {
27454
+ return false;
27455
+ }
27456
+ i++;
27457
+ }
27458
+ return true;
27459
+ };
27460
+
27077
27461
  /**
27078
27462
  * Route is the single primitive for URL-based rendering.
27079
27463
  *
@@ -27092,10 +27476,15 @@ const Head = ({
27092
27476
  * <Route element={AuthLayout}>
27093
27477
  * <Route route={PROFILE_ROUTE} element={ProfilePage} />
27094
27478
  * <Route route={SETTINGS_ROUTE} element={SettingsPage} />
27095
- * <Route fallback element={AuthNotFoundPage} />
27096
27479
  * </Route>
27097
27480
  * ```
27098
27481
  *
27482
+ * Such a container holds no fallback, and cannot: what makes this one the right
27483
+ * layout is a child matching, and having no prefix is the very reason it was
27484
+ * reached for — so there is no address that belongs to it and to none of its
27485
+ * children. An address nothing claims is the router's to answer; write the
27486
+ * fallback there.
27487
+ *
27099
27488
  * ## Self-contained section pattern
27100
27489
  * Use this when routes share a common URL prefix (e.g. `/dashboard/`).
27101
27490
  * A single leaf Route in the top-level router matches the prefix; the component
@@ -27176,7 +27565,10 @@ const debug$1 = (...args) => {
27176
27565
  * @param {object} props
27177
27566
  * @param {object} [props.route] - the route this branch is for, from `route()`
27178
27567
  * @param {object} [props.routeParams] - selects a branch on a param of that route
27179
- * @param {boolean} [props.fallback] - the branch taken when no sibling matches
27568
+ * @param {boolean|object} [props.fallback] - the branch taken when no sibling
27569
+ * matches. `routeFallback()` gives that page a name, which is what lets a
27570
+ * movement be written about it and its `matchingSignal` be read from outside
27571
+ * the tree (see route_fallback.js).
27180
27572
  * @param {Function|import("ignore:preact").VNode} [props.element] - what the branch renders
27181
27573
  * @param {object} [props.elementProps] - props given to `element`
27182
27574
  *
@@ -27254,7 +27646,7 @@ const collectRoutePages = children => {
27254
27646
  // RouteContainer: traverses children statically per render, finds the active branch,
27255
27647
  // and renders only that branch — or the fallback if nothing matches.
27256
27648
  // No contexts, no state of its own: it reads the route signals directly, and
27257
- // its one effect only tells the outside what it has just done.
27649
+ // its effects only tell the outside what it has just done.
27258
27650
  const RouteContainer = ({
27259
27651
  id,
27260
27652
  element,
@@ -27262,8 +27654,11 @@ const RouteContainer = ({
27262
27654
  children
27263
27655
  }) => {
27264
27656
  const {
27265
- activeBranch
27657
+ activeBranch,
27658
+ fallbackBranch,
27659
+ pages
27266
27660
  } = collectBranches(children);
27661
+ const fallbackPage = fallbackBranch ? fallbackBranch.page : null;
27267
27662
 
27268
27663
  // Told to hold still: what is on screen stays on screen. Kept as the very
27269
27664
  // vnode that was rendered last time, which is how Preact is told there is
@@ -27275,11 +27670,25 @@ const RouteContainer = ({
27275
27670
  }
27276
27671
  const branch = shownBranchRef.current || activeBranch;
27277
27672
 
27278
- // The one effect here, and it says the only thing this component knows that
27279
- // nobody outside can find out: the branch it chose is now in the DOM.
27673
+ // The two things this component knows that nobody outside can find out: the
27674
+ // pages a named fallback is the absence of — the children it was written
27675
+ // with — and that the branch it chose is in the DOM.
27280
27676
  useLayoutEffect(() => {
27677
+ if (fallbackPage) {
27678
+ wireRouteFallback(fallbackPage, pages);
27679
+ }
27281
27680
  publishRouteRender();
27282
27681
  });
27682
+ // Being wired means the container holding it is on screen: a router taken out
27683
+ // of the document leaves no fallback behind still claiming an address.
27684
+ useLayoutEffect(() => {
27685
+ if (!fallbackPage) {
27686
+ return undefined;
27687
+ }
27688
+ return () => {
27689
+ unwireRouteFallback(fallbackPage);
27690
+ };
27691
+ }, [fallbackPage]);
27283
27692
  debug$1(`[container "${id}"] RENDER, active=${branch ? branch.type : "none"}`);
27284
27693
  const content = branch ? branch.node : null;
27285
27694
  if (!content) {
@@ -27293,10 +27702,16 @@ const RouteContainer = ({
27293
27702
  // Walk JSX children vnodes (without rendering) to build a branch list and
27294
27703
  // find the active one in the same pass. Anything that is not a <Route> is read
27295
27704
  // through and kept around the branch it holds (see below).
27296
- // Returns { matchingBranch, fallbackBranch, activeBranch }.
27705
+ // Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
27297
27706
  const collectBranches = children => {
27298
27707
  let matchingBranch = null;
27299
27708
  let fallbackBranch = null;
27709
+ // Every page a branch can be on, at any depth: what a named fallback is the
27710
+ // ABSENCE of (see route_fallback.js). A container with a guard route counts
27711
+ // as a page of its own AND contributes the pages inside it — either its guard
27712
+ // matching or a leaf matching under it is enough to select it, and the
27713
+ // fallback then stands down.
27714
+ const pages = [];
27300
27715
  const visit = child => {
27301
27716
  if (!child || child === true || child === false) {
27302
27717
  return;
@@ -27324,8 +27739,12 @@ const collectBranches = children => {
27324
27739
  }
27325
27740
  const {
27326
27741
  matchingBranch: matchingInside,
27327
- fallbackBranch: fallbackInside
27742
+ fallbackBranch: fallbackInside,
27743
+ pages: pagesInside
27328
27744
  } = collectBranches(wrapperChildren);
27745
+ for (const pageInside of pagesInside) {
27746
+ pages.push(pageInside);
27747
+ }
27329
27748
  if (matchingInside && !matchingBranch) {
27330
27749
  matchingBranch = wrapBranch(matchingInside, child);
27331
27750
  }
@@ -27342,8 +27761,31 @@ const collectBranches = children => {
27342
27761
  } = child.props;
27343
27762
  if (nodeChildren) {
27344
27763
  const {
27345
- matchingBranch: matchingChild
27764
+ matchingBranch: matchingChild,
27765
+ fallbackBranch: fallbackChild,
27766
+ pages: pagesChild
27346
27767
  } = collectBranches(nodeChildren);
27768
+ if (fallbackChild && !route) {
27769
+ // A fallback is the branch taken for an address none of its siblings
27770
+ // claim — so something has to make that address BELONG to this
27771
+ // container, and only a route of its own does. Without one the
27772
+ // container is selected by a child matching, which is the one case
27773
+ // where its fallback is not wanted: the branch could never render, and
27774
+ // an unmatched address is the router's to answer, not a layout's.
27775
+ throw new Error(`${describeContainer(child)} has no route of its own, so its fallback could never render: it is selected by one of its children matching, and an address none of them claim belongs to the router around it. Give it a route to be the fallback of (<Route route={SECTION}>), or write the fallback in that router.`);
27776
+ }
27777
+ if (route) {
27778
+ // Params are left out on purpose: a guard selects its container on
27779
+ // matching alone (see guardMatching below), so the fallback stands
27780
+ // down for the whole route, not for one of its cases.
27781
+ pages.push({
27782
+ route,
27783
+ params: undefined
27784
+ });
27785
+ }
27786
+ for (const pageChild of pagesChild) {
27787
+ pages.push(pageChild);
27788
+ }
27347
27789
  const branch = {
27348
27790
  type: "container",
27349
27791
  node: child
@@ -27363,7 +27805,8 @@ const collectBranches = children => {
27363
27805
  if (!fallbackBranch) {
27364
27806
  fallbackBranch = {
27365
27807
  type: "fallback",
27366
- node: child
27808
+ node: child,
27809
+ page: fallback.isRouteFallback ? fallback : null
27367
27810
  };
27368
27811
  }
27369
27812
  } else {
@@ -27371,6 +27814,10 @@ const collectBranches = children => {
27371
27814
  type: "leaf",
27372
27815
  node: child
27373
27816
  };
27817
+ pages.push({
27818
+ route,
27819
+ params: routeParams
27820
+ });
27374
27821
  // every signal is read even once a match is found: reading is what
27375
27822
  // subscribes the container to it, and a branch that is skipped today is
27376
27823
  // the one that must wake the container up tomorrow
@@ -27386,9 +27833,19 @@ const collectBranches = children => {
27386
27833
  return {
27387
27834
  matchingBranch,
27388
27835
  fallbackBranch,
27389
- activeBranch
27836
+ activeBranch,
27837
+ pages
27390
27838
  };
27391
27839
  };
27840
+ const describeContainer = ({
27841
+ props
27842
+ }) => {
27843
+ const {
27844
+ element
27845
+ } = props;
27846
+ const name = typeof element === "function" ? element.name : null;
27847
+ return name ? `<Route element={${name}}>` : `<Route>`;
27848
+ };
27392
27849
  const wrapBranch = (branch, wrapper) => {
27393
27850
  return {
27394
27851
  ...branch,
@@ -29188,7 +29645,7 @@ const beginTransition = ({
29188
29645
  // on, those would freeze the page until the browser gives up. The wait
29189
29646
  // is raced with a short timer instead.
29190
29647
  await Promise.race([renderWait.rendered, waitMs(50)]);
29191
- } else if (pageIsCurrent$1(page)) {
29648
+ } else if (pageIsCurrent(page)) {
29192
29649
  await renderWait.rendered;
29193
29650
  }
29194
29651
  } finally {
@@ -29403,16 +29860,21 @@ const armRouteRenderWait$1 = () => {
29403
29860
  };
29404
29861
  const waitMs = ms => new Promise(resolve => setTimeout(resolve, ms));
29405
29862
 
29406
- // A page written as the route itself, and the page written nowhere: `from`
29407
- // may be left out, which is a relation about arriving at `to` from anywhere.
29863
+ // A page written as the route itself, the page named by the absence of every
29864
+ // route around it (see route_fallback.js), and the page written nowhere:
29865
+ // `from` may be left out, which is a relation about arriving at `to` from
29866
+ // anywhere.
29408
29867
  const normalizePage$1 = page => {
29409
29868
  if (!page) {
29410
29869
  return null;
29411
29870
  }
29412
- return page.isRoute ? {
29413
- route: page,
29414
- params: undefined
29415
- } : page;
29871
+ if (page.isRoute || page.isRouteFallback) {
29872
+ return {
29873
+ route: page,
29874
+ params: undefined
29875
+ };
29876
+ }
29877
+ return page;
29416
29878
  };
29417
29879
 
29418
29880
  // Two pages are the same page when they select the same thing, not when they
@@ -29428,19 +29890,6 @@ const samePage$1 = (a, b) => {
29428
29890
  };
29429
29891
  const pageIndexOf$1 = (pages, page) => pages.findIndex(candidate => samePage$1(candidate, page));
29430
29892
 
29431
- // Whether this page is the one on screen — same reading as route_travel.jsx's
29432
- // own: matchingSignal is the necessary condition and is read whatever happens,
29433
- // params only for a route that matches (the params of a route that does not
29434
- // match are not params).
29435
- const pageIsCurrent$1 = ({
29436
- route,
29437
- params
29438
- }) => {
29439
- if (!route.matchingSignal.value) {
29440
- return false;
29441
- }
29442
- return params ? route.matchesParams(params) : true;
29443
- };
29444
29893
  // The FIRST page that answers, and every page read all the same: a page that
29445
29894
  // is not the current one today is the one that must wake the reader tomorrow.
29446
29895
  //
@@ -29453,7 +29902,7 @@ const pageIsCurrent$1 = ({
29453
29902
  const currentPageIndex$1 = pages => {
29454
29903
  let currentIndex = -1;
29455
29904
  for (let i = 0; i < pages.length; i++) {
29456
- const isCurrent = pageIsCurrent$1(pages[i]);
29905
+ const isCurrent = pageIsCurrent(pages[i]);
29457
29906
  if (!isCurrent) {
29458
29907
  continue;
29459
29908
  }
@@ -30806,28 +31255,6 @@ const samePage = (a, b) => {
30806
31255
  };
30807
31256
  const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
30808
31257
 
30809
- // Whether this page is the one on screen. `matchesParams` reads paramsSignal,
30810
- // so a caller reading this during a render is subscribed to the param changes
30811
- // that walk from one tab to the next — matchingSignal alone never moves there,
30812
- // and a row whose tabs are params of one route would never re-render.
30813
- //
30814
- // The params are read only for a route that matches, and that is not a signal
30815
- // left unread: a reader wakes on anything it read last time, so what matters is
30816
- // that everything able to make this answer change is among them.
30817
- // matchingSignal is read whatever happens, and it is a NECESSARY condition —
30818
- // while it is false no param of that route can put this page on screen, and the
30819
- // day one could, matchingSignal itself has to turn true to say so, which is the
30820
- // read that brings the params back in. (Asking anyway would be worse than
30821
- // useless: the params of a route that does not match are not params.)
30822
- const pageIsCurrent = ({
30823
- route,
30824
- params
30825
- }) => {
30826
- if (!route.matchingSignal.value) {
30827
- return false;
30828
- }
30829
- return params ? route.matchesParams(params) : true;
30830
- };
30831
31258
  // The FIRST page that answers, as with the branches of a <Route>: several
30832
31259
  // routes match at once — a literal one and the parameterized one it is a case of
30833
31260
  // ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
@@ -33727,6 +34154,17 @@ const useUIStateController = (
33727
34154
  `merging button state into parent control group:`,
33728
34155
  mergedState,
33729
34156
  );
34157
+ // The sender's value is part of the answer, and whoever mirrors
34158
+ // this group — a picker, through its façade — reads the group's
34159
+ // state, not its children's: told the way a mount sync is
34160
+ // (silently, nobody has answered yet), so the send that follows
34161
+ // finds the value there. Not through the group's own onChange:
34162
+ // that re-aggregates from the children, and a button is not one.
34163
+ parentController.parentUIStateController?.onChildUIAction?.(
34164
+ parentController,
34165
+ e,
34166
+ { stateChanged: true, silent: true },
34167
+ );
33730
34168
  }
33731
34169
  }
33732
34170
  // Trigger uiAction/command side effects without changing UI state.
@@ -34095,6 +34533,12 @@ const useUIStateController = (
34095
34533
  },
34096
34534
  resetUIState: (e) => {
34097
34535
  controller.setUIState(controller.state, e);
34536
+ // What the control holds moved back, and a bound signal mirrors what
34537
+ // it holds (see PROPAGATE_DOWN_EVENT_SET). Left on the value the
34538
+ // reset just undid — written there by the control itself at the
34539
+ // pick — the signal would hand it straight back on the next render,
34540
+ // and the refused value would win over the rollback.
34541
+ writeBoundSignal(controller.state);
34098
34542
  },
34099
34543
  // Read by the callout manager when it has nowhere else to point.
34100
34544
  getCalloutAnchorElement: (event) =>
@@ -34198,9 +34642,27 @@ const useUIStateController = (
34198
34642
  controller.hasStateProp = true;
34199
34643
  const currentState = controller.state;
34200
34644
  if (!compareTwoJsValues(state, currentState)) {
34201
- controller.state = state;
34202
- if (!optimisticWorkInFlight) {
34203
- controller.setUIState(state, new CustomEvent("state_prop_change"));
34645
+ // A bound signal controls the state the way a `value` prop does —
34646
+ // and, unlike a `value`, it is written by the control itself at
34647
+ // every ui action. Read back here on a control that runs an action,
34648
+ // a signal matching the ui state is that echo (see the signal branch
34649
+ // below for the same rule on an uncontrolled control): `state` is
34650
+ // the rollback target, and taking the echo would make resetOnError
34651
+ // put back the very value the action is about to send, or has just
34652
+ // been refused. The action settles it — success acknowledges,
34653
+ // failure resets to what is kept here.
34654
+ const signalEchoesOwnWrite =
34655
+ Boolean(controlInfo.signal) &&
34656
+ Boolean(props.action) &&
34657
+ compareTwoJsValues(state, controller.uiState);
34658
+ if (!signalEchoesOwnWrite) {
34659
+ controller.state = state;
34660
+ if (!optimisticWorkInFlight) {
34661
+ controller.setUIState(
34662
+ state,
34663
+ new CustomEvent("state_prop_change"),
34664
+ );
34665
+ }
34204
34666
  }
34205
34667
  }
34206
34668
  } else {
@@ -35360,6 +35822,10 @@ const useUIGroupStateController = (
35360
35822
  const { controller } = s;
35361
35823
  const prevDefaultValue = controller.defaultValue;
35362
35824
  controller.props = props;
35825
+ // Published like a leaf's (see the leaf update above): a child's named
35826
+ // button reaches the group's owner through it (see the button branch of
35827
+ // onUIAction).
35828
+ controller.parentUIStateController = parentUIStateController;
35363
35829
  controller.ref = ref;
35364
35830
  controller.id = id;
35365
35831
  controller.name = name;
@@ -41477,6 +41943,17 @@ Object.assign(PSEUDO_CLASSES, {
41477
41943
  * way there changes. What a row of tabs wants — the neighbour is a lateral
41478
41944
  * move, not a step deeper, so the whole row weighs one entry and the back
41479
41945
  * button leaves by where the reader came in.
41946
+ * @param {boolean} [props.document] - The address is ANOTHER DOCUMENT of this
41947
+ * origin — an admin panel, a status page, anything built as its own entry
41948
+ * point — so the press is left to the browser and loads that page. Without
41949
+ * it navi routes every same-origin address it can intercept, and one no
41950
+ * route matches lands on the app's not-found screen. Said rather than
41951
+ * guessed: "no route matches" is also what a typo in an in-app url looks
41952
+ * like, and that one must show the not-found screen instead of silently
41953
+ * reloading the app. The element stays a real link — an address in the
41954
+ * status bar, a middle click, "open in new tab" — which is what navigating
41955
+ * from an `action` costs; `target="_blank"` is a different thing, a new tab.
41956
+ * Nothing is prefetched on the way there: the routes do not lead there.
41480
41957
  * @param {boolean} [props.prefetch=true] Fetch the code of where this leads when the
41481
41958
  * pointer or the focus arrives, ahead of the press (see
41482
41959
  * docs/dynamic_import.md): the route actions that ask nothing of the
@@ -41579,13 +42056,17 @@ const LinkPlain = props => {
41579
42056
  routeTransition,
41580
42057
  pressableDuringRouteTransition,
41581
42058
  replace,
42059
+ document: isDocument,
41582
42060
  prefetch = true,
41583
42061
  children
41584
42062
  } = props;
41585
42063
  if (anchor && !props.id) {
41586
42064
  props.id = href.slice(1);
41587
42065
  }
41588
- usePreloadOnIntent(props.ref, href, prefetch);
42066
+ // A document link leads out of the routes, so there is nothing there to
42067
+ // prefetch — and the address would otherwise be preloaded against the very
42068
+ // routes it is escaping.
42069
+ usePreloadOnIntent(props.ref, href, isDocument ? false : prefetch);
41589
42070
  const selectionContext = useContext(SelectionContext);
41590
42071
  const nav = useContext(NavContext);
41591
42072
  const visited = useIsVisited(href);
@@ -41700,6 +42181,12 @@ const LinkPlain = props => {
41700
42181
  const replaceRequest = replace ? {
41701
42182
  [LINK_REPLACE_ATTRIBUTE]: ""
41702
42183
  } : null;
42184
+
42185
+ // That this address is another document is the link's to say, worn where the
42186
+ // interception looks for it (see link_document.js).
42187
+ const documentRequest = isDocument ? {
42188
+ [LINK_DOCUMENT_ATTRIBUTE]: ""
42189
+ } : null;
41703
42190
  const innerChildren = children || (hrefFallback ? href : children);
41704
42191
  const startIconEl = startIcon;
41705
42192
  const endIconEl = innerEndIcon;
@@ -41753,12 +42240,14 @@ const LinkPlain = props => {
41753
42240
  routeTransition: undefined,
41754
42241
  pressableDuringRouteTransition: undefined,
41755
42242
  replace: undefined,
42243
+ document: undefined,
41756
42244
  prefetch: undefined,
41757
42245
  "data-navi-route-transition-request": routeTransitionRequest,
41758
42246
  ...(pressableDuringRouteTransition ? {
41759
42247
  [PRESSABLE_ATTRIBUTE]: ""
41760
42248
  } : null),
41761
42249
  ...replaceRequest,
42250
+ ...documentRequest,
41762
42251
  // The control's own handlers first — the interaction gate, the caller's
41763
42252
  // onClick/onKeyDown, the command and the action — then what only a link
41764
42253
  // does. Written over the spread above, so they have to be called here.
@@ -44883,9 +45372,15 @@ const createOpenController = (
44883
45372
  change();
44884
45373
  };
44885
45374
  const runChange = (change, { opened, event }) => {
45375
+ const applyChange = () => {
45376
+ // Recorded with the change itself: what the DOM shows is what a render
45377
+ // landing between the ask and the picture must draw (see `openedInDom`).
45378
+ controller.openedInDom = opened;
45379
+ change();
45380
+ };
44886
45381
  const { transitionChange } = controller;
44887
45382
  if (!transitionChange) {
44888
- change();
45383
+ applyChange();
44889
45384
  return;
44890
45385
  }
44891
45386
  // What the controller answers about itself does not wait for the picture:
@@ -44894,10 +45389,10 @@ const createOpenController = (
44894
45389
  // caller's own signal), and a request arriving before the change lands
44895
45390
  // runs it first rather than reading a DOM that disagrees.
44896
45391
  controller.opened = opened;
44897
- changeAwaitingTransition = change;
45392
+ changeAwaitingTransition = applyChange;
44898
45393
  transitionChange(
44899
45394
  () => {
44900
- if (changeAwaitingTransition === change) {
45395
+ if (changeAwaitingTransition === applyChange) {
44901
45396
  flushChangeAwaitingTransition();
44902
45397
  }
44903
45398
  },
@@ -45015,6 +45510,13 @@ const createOpenController = (
45015
45510
  };
45016
45511
  const controller = {
45017
45512
  opened: false,
45513
+ // What the DOM currently shows, as opposed to what the controller has
45514
+ // decided: the two differ for the frame a change spends waiting for the
45515
+ // browser to photograph the state being left (see runChange). A render
45516
+ // landing in that frame — a picker re-rendering because its action just
45517
+ // started — must draw THIS, or it paints the closed state before the
45518
+ // picture is taken and the movement has nothing to leave from.
45519
+ openedInDom: false,
45018
45520
  // Which press the popup opened during, written at every open (see
45019
45521
  // openedDuringThisPress). Never any press before there has been one.
45020
45522
  pressCountAtOpen: null,
@@ -47583,6 +48085,7 @@ const ButtonUI = props => {
47583
48085
  target,
47584
48086
  rel,
47585
48087
  replace,
48088
+ document: isDocument,
47586
48089
  pressableDuringRouteTransition,
47587
48090
  prefetch = true,
47588
48091
  // visual
@@ -47637,13 +48140,21 @@ const ButtonUI = props => {
47637
48140
  [LINK_REPLACE_ATTRIBUTE]: ""
47638
48141
  } : null;
47639
48142
 
48143
+ // And this one says the address is another document of this origin, so the
48144
+ // press is the browser's (see link_document.js).
48145
+ const documentRequest = isDocument ? {
48146
+ [LINK_DOCUMENT_ATTRIBUTE]: ""
48147
+ } : null;
48148
+
47640
48149
  // Worn as an attribute too, and read at the document by whoever catches the
47641
48150
  // press a movement would have swallowed (see transition_press.js).
47642
48151
  const pressableRequest = pressableDuringRouteTransition ? {
47643
48152
  [PRESSABLE_ATTRIBUTE]: ""
47644
48153
  } : null;
47645
48154
  const visualSelector = ".navi_button_content";
47646
- usePreloadOnIntent(ref, href, prefetch);
48155
+ // Nothing to prefetch on the way to another document: the routes do not
48156
+ // lead there.
48157
+ usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
47647
48158
  useAccentColorAttributes(ref, null, {
47648
48159
  elementSelector: visualSelector
47649
48160
  });
@@ -47671,6 +48182,8 @@ const ButtonUI = props => {
47671
48182
  rel: innerRel,
47672
48183
  replace: undefined,
47673
48184
  ...replaceRequest,
48185
+ document: undefined,
48186
+ ...documentRequest,
47674
48187
  pressableDuringRouteTransition: undefined,
47675
48188
  ...pressableRequest,
47676
48189
  prefetch: undefined
@@ -48114,6 +48627,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48114
48627
  * selfInteractions?: string,
48115
48628
  * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
48116
48629
  * replace?: boolean,
48630
+ * document?: boolean,
48117
48631
  * actionStandalone?: boolean,
48118
48632
  * actionAbortable?: boolean,
48119
48633
  * optimistic?: boolean,
@@ -48123,6 +48637,13 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48123
48637
  * `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
48124
48638
  * rather than stacking on it: what `<Link replace>` says, for a press drawn
48125
48639
  * as a button.
48640
+ * @param {boolean} [document] The `href` is ANOTHER DOCUMENT of this origin —
48641
+ * an admin panel, a status page, anything built as its own entry point — so
48642
+ * the press is left to the browser and loads that page, instead of being
48643
+ * routed to the app's not-found screen. What `<Link document>` says, for a
48644
+ * press drawn as a button; and the reason to say it rather than navigate
48645
+ * from an `action` is that the button stays a real link (an address, a
48646
+ * middle click, "open in new tab"). Nothing is prefetched on the way there.
48126
48647
  * @param {boolean} [prefetch=true] Fetch the code of where this leads when the
48127
48648
  * pointer or the focus arrives, ahead of the press (see
48128
48649
  * docs/dynamic_import.md): the route actions that ask nothing of the
@@ -58061,10 +58582,10 @@ const css$E = /* css */`
58061
58582
  var(--dialog-maxmax-height)
58062
58583
  );
58063
58584
 
58064
- /* Base default: also the custom renderer's own permanent value — its
58585
+ /* Base default: also a local dialog's own permanent value — its
58065
58586
  containing block is genuinely its nearest positioned ancestor,
58066
58587
  regardless of positionArea. See the [data-layer="top"] rule below for
58067
- why the via-attribute renderer overrides this. Position is always
58588
+ why a top-layer one overrides this. Position is always
58068
58589
  JS-driven (pickPositionRelativeTo, see useDialogProps below) — no CSS
58069
58590
  alignment/inset math here at all. */
58070
58591
  position: absolute;
@@ -58147,13 +58668,8 @@ const css$E = /* css */`
58147
58668
  var(--container-position-remaining-width, var(--dialog-maxmax-width)),
58148
58669
  var(--dialog-maxmax-width)
58149
58670
  );
58150
- /* The sheet rests on the screen's bottom edge, which on a phone is the
58151
- home indicator and, since iOS 26, Safari's own floating bar — a band
58152
- the browser does not paint fixed content into. The surface still
58153
- reaches the edge (the sheet comes out from behind the bar); what it
58154
- holds stops above it. */
58155
- padding-bottom: env(safe-area-inset-bottom, 0px);
58156
58671
  }
58672
+
58157
58673
  /* The clamped max, not --dialog-maxmax-*: that one is the viewport minus
58158
58674
  the spacing, which is only the real ceiling for layer="top". A local
58159
58675
  dialog is confined to its positioned ancestor, whose size reaches here
@@ -58181,6 +58697,35 @@ const css$E = /* css */`
58181
58697
  border-bottom-left-radius: 0;
58182
58698
  }
58183
58699
 
58700
+ /* An edge the dialog sits flush against is an edge of the screen, and the
58701
+ device keeps a band there it paints nothing into: the notch, the home
58702
+ indicator, Safari's own floating bar since iOS 26. The surface still
58703
+ reaches the edge — a sheet comes out from behind the bar rather than
58704
+ stopping short of it — and what it holds stops at the band.
58705
+
58706
+ env(), not --navi-safe-area-inset-*: that one is the app's own free
58707
+ region and counts its fixed bars, which this dialog is in front of. The
58708
+ device's inset is the only thing left to keep.
58709
+
58710
+ layer="top" only. A local dialog is flush with the box it was declared
58711
+ in, and whether that box reaches the screen's edge is something only the
58712
+ app knows — it gives that band back on its own container (see
58713
+ safe_area.js's [data-navi-safe-area]). */
58714
+ &[data-layer="top"] {
58715
+ &[data-flush-top] {
58716
+ padding-top: env(safe-area-inset-top, 0px);
58717
+ }
58718
+ &[data-flush-right] {
58719
+ padding-right: env(safe-area-inset-right, 0px);
58720
+ }
58721
+ &[data-flush-bottom] {
58722
+ padding-bottom: env(safe-area-inset-bottom, 0px);
58723
+ }
58724
+ &[data-flush-left] {
58725
+ padding-left: env(safe-area-inset-left, 0px);
58726
+ }
58727
+ }
58728
+
58184
58729
  /* The placement is a translate, so the translate property is spoken for
58185
58730
  here (see applyNewPosition in visible_rect.js, which owns it and animates
58186
58731
  it itself through the Web Animations API rather than through this file's
@@ -58193,8 +58738,8 @@ const css$E = /* css */`
58193
58738
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
58194
58739
  }
58195
58740
  /* backdropVariant, after the rules it overrides: same specificity, so
58196
- order is what decides. showModal() still makes the page inert either
58197
- way — only the paint goes away. */
58741
+ order is what decides. The wall is still there either way — only the
58742
+ paint goes away. */
58198
58743
  &[data-backdrop-variant="discrete"] {
58199
58744
  --backdrop-background: var(--navi-backdrop-discrete-background);
58200
58745
  --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
@@ -58258,7 +58803,10 @@ const css$E = /* css */`
58258
58803
  --navi-focus-outline-style: none;
58259
58804
  }
58260
58805
 
58261
- &[open] {
58806
+ /* :popover-open too — a top-layer dialog with no wall is shown with
58807
+ showPopover(), which never sets the [open] attribute. */
58808
+ &[open],
58809
+ &:popover-open {
58262
58810
  display: flex;
58263
58811
  }
58264
58812
 
@@ -58295,14 +58843,12 @@ const css$E = /* css */`
58295
58843
  }
58296
58844
  }
58297
58845
 
58298
- /* Custom renderer only — .show()'d dialogs get no ::backdrop, so this is
58846
+ /* layer="local" only — a .show()'d dialog gets no ::backdrop, so its wall is
58299
58847
  a real sibling element instead, same idea/CSS shape as Popover's own
58300
- .navi_popover_backdrop (see popover.jsx's top comment for the design
58301
- this mirrors). Always rendered (never skipped like Popover's own
58302
- "none" case): a dialog is always modal, so there's always at least a
58303
- click-absorbing backdrop, matching what showModal() already gives the
58304
- via-attribute renderer for free regardless of
58305
- pointerInteractionOutsideEffect. */
58848
+ .navi_popover_backdrop (see popover.jsx's top comment for the design this
58849
+ mirrors). Rendered whenever backdrop={true}, whatever
58850
+ pointerInteractionOutsideEffect says: a wall that closes nothing still
58851
+ absorbs the press, matching what showModal() gives a modal for free. */
58306
58852
  .navi_dialog_backdrop {
58307
58853
  --popup-animation-duration: 0.18s;
58308
58854
 
@@ -58317,7 +58863,7 @@ const css$E = /* css */`
58317
58863
  in openEffect) gets pointer-events: none mid-transition. */
58318
58864
  pointer-events: auto;
58319
58865
 
58320
- /* Painted through the same two variables as the via-attribute renderer's
58866
+ /* Painted through the same two variables as a modal's own
58321
58867
  ::backdrop (see them for what each rule is for) — here they resolve on
58322
58868
  the element that paints, which is also the element the
58323
58869
  backdropColor/backdropFilter props are set on. Declared unconditionally
@@ -58409,12 +58955,15 @@ const css$E = /* css */`
58409
58955
  * grammar, anchor's sizing-only role, backdrop mechanics).
58410
58956
  *
58411
58957
  * @param {object} props
58412
- * @param {"top"|"local"} [props.layer="top"] - `"top"`: `showModal()`'d
58413
- * into the browser's own top layer (native focus trap, `Escape`-to-cancel,
58414
- * hardware back-button dismissal, rest-of-document made inert). `"local"`:
58415
- * shown via the non-modal `.show()` instead, staying in normal document
58958
+ * @param {"top"|"local"} [props.layer="top"] - What the dialog is placed
58959
+ * against. `"top"`: the browser's own top layer, placed against the screen,
58960
+ * never clipped by anything the app declared. `"local"`: normal document
58416
58961
  * flow inside its own positioned ancestor — confined to (and clipped by)
58417
- * that container instead of the whole viewport.
58962
+ * that container instead of the whole viewport. Whether the top-layer one
58963
+ * is modal is `backdrop`'s answer, not this one's: with a backdrop it is
58964
+ * `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
58965
+ * back-button dismissal, rest-of-document made inert), without one it is
58966
+ * shown through the Popover API and the page behind stays live.
58418
58967
  * @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
58419
58968
  * bottom sheet (docked flush to the bottom edge, full width) on a small touch
58420
58969
  * screen, and leaves it alone otherwise. For a dialog meant to be interacted
@@ -58477,9 +59026,12 @@ const css$E = /* css */`
58477
59026
  * outside press does or how the backdrop is painted. `false` leaves the page
58478
59027
  * reachable: a press outside closes the dialog (per
58479
59028
  * `pointerInteractionOutsideEffect`) *and* is answered by whatever it landed
58480
- * on, in the same gesture. **`layer="local"` only** — `showModal()` makes
58481
- * everything behind genuinely inert, so a top-layer dialog has no way to let
58482
- * a press through and warns instead. See docs/popup_backdrop.md.
59029
+ * on, in the same gesture; focus is free to leave too, the page behind being
59030
+ * meant to be reached. Works in either layer — a top-layer dialog with no
59031
+ * wall is shown through the Popover API rather than `showModal()`, which is
59032
+ * what a sheet docked to the screen's bottom edge over a still-readable map
59033
+ * needs. What it gives up is what only a modal gets natively: the hardware
59034
+ * back button no longer dismisses it. See docs/popup_backdrop.md.
58483
59035
  * @param {"close"|"cancel"|"capture"|"none"} [props.pointerInteractionOutsideEffect="close"]
58484
59036
  * - `"close"` closes the dialog on an outside click. `"capture"`/`"none"`
58485
59037
  * both just absorb the click without closing (visually dimmed backdrop vs.
@@ -58739,7 +59291,7 @@ const UncontrolledDialog = props => {
58739
59291
  // Picks which rendering strategy actually mounts, from `layer` alone — see
58740
59292
  // this file's top comment. Done after the controlled/uncontrolled split
58741
59293
  // above, so an openController is always already resolved by the time
58742
- // DialogAsModal/DialogLocal (and the useDialogProps hook they share) ever
59294
+ // DialogInTopLayer/DialogLocal (and the useDialogProps hook they share) ever
58743
59295
  // run.
58744
59296
  const ControlledDialog = props => {
58745
59297
  if (props.layer === "local") {
@@ -58747,11 +59299,11 @@ const ControlledDialog = props => {
58747
59299
  ...props
58748
59300
  });
58749
59301
  }
58750
- return jsx(DialogAsModal, {
59302
+ return jsx(DialogInTopLayer, {
58751
59303
  ...props
58752
59304
  });
58753
59305
  };
58754
- const DialogAsModal = props => {
59306
+ const DialogInTopLayer = props => {
58755
59307
  const [backdropProps, contentProps] = useDialogProps(props);
58756
59308
  return jsxs(Fragment, {
58757
59309
  children: [backdropProps && jsx(Box, {
@@ -58783,12 +59335,14 @@ const DialogLocal = props => {
58783
59335
  /**
58784
59336
  * Everything both rendering strategies share once an `openController` is
58785
59337
  * already resolved: focus/debug/id plumbing, the open-commit sequence, the
58786
- * close handler — inlined in `openEffect`, branching on `isModal` at each
59338
+ * close handler — inlined in `openEffect`, branching on how the dialog is
59339
+ * shown at each
58787
59340
  * point the two renderers genuinely differ (same pattern as popover.jsx's
58788
59341
  * own usePopoverProps — see its top comment for why this stays inline
58789
59342
  * rather than split into two functions). Returns `[backdropProps,
58790
- * contentProps]` — `backdropProps` is `null` for the via-attribute renderer
58791
- * (its own backdrop is native, not a real element).
59343
+ * contentProps]` — `backdropProps` is `null` unless there is a wall of our
59344
+ * own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
59345
+ * has none at all.
58792
59346
  */
58793
59347
  // What a dialog turns into on a small touch screen. "bottom" is not a taste:
58794
59348
  // it puts the dialog in the zone a phone is actually operated from — where the
@@ -58820,10 +59374,10 @@ const useDialogProps = props => {
58820
59374
  props.ref = props.ref || defaultRef;
58821
59375
  const {
58822
59376
  openController,
58823
- // "top" (default) → real <dialog>, showModal(), the browser's own top
58824
- // layer. "local" → also a real <dialog>, but shown via the non-modal
58825
- // .show() instead, staying in normal document flow, position: absolute
58826
- // relative to its own positioned ancestor. See this file's top comment.
59377
+ // "top" (default) → the browser's own top layer, placed against the
59378
+ // screen. "local" → normal document flow, position: absolute relative to
59379
+ // its own positioned ancestor. Which show call each takes also depends on
59380
+ // `backdrop` — see this file's top comment.
58827
59381
  layer = "top",
58828
59382
  dockedOnSmallTouchScreen,
58829
59383
  // Same grammar as Popover's own positionArea — see this file's top
@@ -58837,17 +59391,16 @@ const useDialogProps = props => {
58837
59391
  expandX: expandXProp,
58838
59392
  expandY: expandYProp,
58839
59393
  // "close" (default) closes on an outside click. "capture"/"none" both
58840
- // just absorb it without closing — for the via-attribute renderer,
58841
- // showModal() already makes the rest of the page inert, so there's
58842
- // nothing for a click to reach either way; for the custom renderer,
58843
- // there's no native inert-ing, so the real backdrop below is what
58844
- // actually makes "capture"/"none" behave the same way here too.
59394
+ // just absorb it without closing — for a modal, showModal() already makes
59395
+ // the rest of the page inert, so there's nothing for a click to reach
59396
+ // either way; otherwise there's no native inert-ing, so the real backdrop
59397
+ // below is what makes "capture"/"none" behave the same way here too.
58845
59398
  pointerInteractionOutsideEffect = "close",
58846
59399
  // Whether there is a wall between the dialog and the page at all, asked
58847
59400
  // before what a press on it does (pointerInteractionOutsideEffect) and
58848
- // before how it is painted (backdropVariant below). layer="top" cannot
58849
- // honour false: showModal() makes the page inert before anything here
58850
- // runs.
59401
+ // before how it is painted (backdropVariant below). It also picks the show
59402
+ // call for layer="top": a wall is what showModal() is for, and without one
59403
+ // the dialog goes to the same top layer through the Popover API instead.
58851
59404
  backdrop = true,
58852
59405
  // How loudly the backdrop says it is there — independent of what it
58853
59406
  // *does* (that's pointerInteractionOutsideEffect above). "invisible" is a
@@ -58904,10 +59457,11 @@ const useDialogProps = props => {
58904
59457
  anchor
58905
59458
  });
58906
59459
  const children = contentMounted ? childrenProp : null;
58907
- const isModal = layer === "top";
58908
- if (isModal && !backdrop) {
58909
- console.warn(`Dialog: backdrop={false} needs layer="local". A layer="top" dialog is shown with showModal(), which makes everything behind it inert before any of this runs — there is no press left to let through.`);
58910
- }
59460
+ // Where it is placed, and whether there is a wall — the two questions that
59461
+ // pick the show call (see this file's top comment).
59462
+ const isTopLayer = layer === "top";
59463
+ const isModal = isTopLayer && backdrop;
59464
+ const isTopLayerPopover = isTopLayer && !backdrop;
58911
59465
  if (!backdrop && pointerInteractionOutsideEffect === "capture") {
58912
59466
  console.warn(`Dialog: pointerInteractionOutsideEffect="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away.`);
58913
59467
  }
@@ -58928,7 +59482,7 @@ const useDialogProps = props => {
58928
59482
  // container being that screen (the viewport, unless the app declared a
58929
59483
  // narrower one) — and the positioned ancestor for a local one, where
58930
59484
  // reading 3% of the screen gives an absurd gap inside a small box.
58931
- isModal ? "3appw" : "3cqw");
59485
+ isTopLayer ? "3appw" : "3cqw");
58932
59486
  // "expand || expandX", the shorthand semantics Popup used to apply before
58933
59487
  // handing them over — the docked default only applies when neither was said
58934
59488
  const expandXUnset = expand === undefined && expandXProp === undefined;
@@ -59117,14 +59671,14 @@ const useDialogProps = props => {
59117
59671
  // from dialogEl.parentElement, which for DialogLocal is the
59118
59672
  // .navi_dialog_clip_wrapper (itself position: absolute) rather than the
59119
59673
  // real, meaningful ancestor beyond it.
59120
- const positionedAncestor = isModal ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
59674
+ const positionedAncestor = isTopLayer ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
59121
59675
  const [cleanup, addCleanup] = createPubSub(true);
59122
59676
  const anchorElement = resolveAnchorElement(e);
59123
59677
  // Kept for the closing, which has no opening event to read it back from:
59124
59678
  // the box the dialog shrinks back into is the one it came out of.
59125
59679
  anchorElementRef.current = anchorElement;
59126
59680
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
59127
- if (!isModal) {
59681
+ if (!isTopLayer) {
59128
59682
  // see openLocalDialogCount's own comment
59129
59683
  dialogEl.style.setProperty("--dialog-stack-order", openLocalDialogCount++);
59130
59684
  }
@@ -59167,10 +59721,12 @@ const useDialogProps = props => {
59167
59721
  }
59168
59722
  if (isModal) {
59169
59723
  dialogEl.showModal();
59724
+ } else if (isTopLayerPopover) {
59725
+ dialogEl.showPopover();
59170
59726
  } else {
59171
59727
  dialogEl.show();
59172
59728
  }
59173
- // Regardless of isModal — see the backdrop's own [navi-hidden] CSS rule
59729
+ // Whichever show call ran — see the backdrop's own [navi-hidden] CSS rule
59174
59730
  // and popover.jsx's identical reasoning: showModal()/show() alone only
59175
59731
  // wins over a stray, still-present [navi-hidden] { display: none }
59176
59732
  // default when nothing else authored also sets display on dialogEl —
@@ -59178,24 +59734,23 @@ const useDialogProps = props => {
59178
59734
  // property (e.g. Popup's own flex prop) defeats the UA stylesheet's own
59179
59735
  // dialog:not([open]) default the same way it can for Popover.
59180
59736
  dialogEl.removeAttribute("navi-hidden");
59181
- if (isModal) ; else {
59737
+ if (isModal) ; else if (backdrop) {
59182
59738
  addCleanup(trapFocusInside(dialogEl, {
59183
59739
  debug: debugFocus,
59184
59740
  boundaryElement: positionedAncestor,
59185
- // A dialog is always modal (see this file's top comment) — a
59186
- // mousedown on some other focusable element inside the same
59187
- // container (but outside the dialog) must not steal focus away
59188
- // from it either, not just a Tab press.
59741
+ // The wall stops the keyboard too — a mousedown on some other
59742
+ // focusable element behind it must not steal the focus away either,
59743
+ // not just a Tab press.
59189
59744
  pointerTrap: true
59190
59745
  }));
59191
59746
  }
59192
59747
  if (scrollCapture) {
59193
- // A modal dialog always has its own ::backdrop; the custom renderer has
59194
- // the backdrop element when it renders one.
59748
+ // A modal dialog always has its own ::backdrop; a local one has the
59749
+ // backdrop element when it renders one.
59195
59750
  addCleanup(trapScrollInside(dialogEl, {
59196
59751
  backdrop: isModal || backdropEl
59197
59752
  }));
59198
- } else if (!isModal) {
59753
+ } else if (!isTopLayer) {
59199
59754
  // A local dialog is confined to its positioned ancestor, and so is its
59200
59755
  // backdrop (inset: 0 covers the scrollport, not the scrolled content):
59201
59756
  // letting that ancestor scroll would slide the dialog away and reveal
@@ -59212,7 +59767,7 @@ const useDialogProps = props => {
59212
59767
  // Positioning: dialogEl is already shown (display: flex, per this
59213
59768
  // file's own [open] CSS) by this point, so its own dimensions are real
59214
59769
  // — pickPositionRelativeTo's own no-anchor/docked mode (no `anchor`
59215
- // argument at all) docks it against the viewport (layer="top"/isModal)
59770
+ // argument at all) docks it against the viewport (layer="top")
59216
59771
  // or its own positioned ancestor (layer="local", the same
59217
59772
  // positionedAncestor computed above), same mechanism as Popover's own
59218
59773
  // custom renderer. applyDialogPosition sets --container-position-remaining-height/-width
@@ -59244,7 +59799,7 @@ const useDialogProps = props => {
59244
59799
  // box is what the caps must read, and nothing in CSS tracks it.
59245
59800
  const applyDialogPosition = position => {
59246
59801
  applyNewPosition(dialogEl, position);
59247
- if (isModal) {
59802
+ if (isTopLayer) {
59248
59803
  dialogEl.style.removeProperty("--container-position-remaining-height");
59249
59804
  dialogEl.style.removeProperty("--container-position-remaining-width");
59250
59805
  }
@@ -59343,7 +59898,7 @@ const useDialogProps = props => {
59343
59898
 
59344
59899
  // Reposition on the same triggers Popover's own visibleRectEffect
59345
59900
  // already reacts to generically — window resize/scroll/visual-viewport
59346
- // changes for layer="top"/isModal (positionedAncestor is already
59901
+ // changes for layer="top" (positionedAncestor is already
59347
59902
  // document.documentElement there, see its own computation above;
59348
59903
  // visibleRectEffect already debounces visualViewport resize by 100ms
59349
59904
  // to avoid the mobile tap-to-tap-input keyboard flicker, so no
@@ -59441,15 +59996,19 @@ const useDialogProps = props => {
59441
59996
  debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
59442
59997
  clearTextSelectionInside(dialogEl);
59443
59998
  dialogEl.setAttribute("aria-expanded", "false");
59444
- if (!isModal) {
59999
+ if (!isTopLayer) {
59445
60000
  openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
59446
60001
  dialogEl.style.removeProperty("--dialog-stack-order");
59447
60002
  }
59448
60003
  // See openEffect's own identical comment for why this is needed
59449
- // regardless of isModal, not just when a stray authored display
60004
+ // whichever show call ran, not just when a stray authored display
59450
60005
  // property is actually present — harmless the rest of the time.
59451
60006
  dialogEl.setAttribute("navi-hidden", "");
59452
- dialogEl.close();
60007
+ if (isTopLayerPopover) {
60008
+ dialogEl.hidePopover();
60009
+ } else {
60010
+ dialogEl.close();
60011
+ }
59453
60012
  // Held at the size it has right now, for the whole way out. cleanup()
59454
60013
  // below already stops the JS repositioning, but the size is CSS-driven
59455
60014
  // (--x-dialog-max-height, and `height` outright under expandY) and
@@ -59476,9 +60035,8 @@ const useDialogProps = props => {
59476
60035
  };
59477
60036
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
59478
60037
  escape: e => {
59479
- // Only the custom renderer needs this — a modal <dialog> already
59480
- // fires "cancel" (handled via onCancel below) on Escape natively; a
59481
- // non-modal .show()'d one doesn't.
60038
+ // Only a modal <dialog> fires "cancel" on Escape natively (handled via
60039
+ // onCancel below); neither a .show()n nor a showPopover()n one does.
59482
60040
  if (isModal || !openController.opened) {
59483
60041
  return null;
59484
60042
  }
@@ -59497,10 +60055,10 @@ const useDialogProps = props => {
59497
60055
  // most fields are shared: renderer-specific bits (the outside-click
59498
60056
  // handler below, in particular) are just assigned onto whichever of the
59499
60057
  // two actually owns that concern for a given renderer, instead of one
59500
- // object's own field branching internally on isModal. backdropProps only
59501
- // gets returned (see the bottom of this function) when !isModal — the
59502
- // via-attribute renderer's own backdrop is native (::backdrop), not a
59503
- // real element we render ourselves.
60058
+ // object's own field branching internally. backdropProps only gets returned
60059
+ // (see the bottom of this function) for the one dialog whose wall is a real
60060
+ // element of ours: a modal's is native (::backdrop), and backdrop={false}
60061
+ // has none at all.
59504
60062
  Object.assign(backdropProps, {
59505
60063
  "ref": backdropRef,
59506
60064
  // Out of flow like the popup it belongs to — see the content element's own
@@ -59516,14 +60074,14 @@ const useDialogProps = props => {
59516
60074
  // and matching what a descendant relying on
59517
60075
  // use_displayed_layout_effect.js's own aria-expanded-presence check
59518
60076
  // needs — see popover.jsx's own identical prop for the full reasoning.
59519
- "aria-expanded": openController.opened ? "true" : "false",
60077
+ "aria-expanded": openController.openedInDom ? "true" : "false",
59520
60078
  // Present from this very first render (recomputed fresh on every one
59521
60079
  // from openController.opened, not a frozen mount-time constant) so
59522
60080
  // there's no gap for the browser to ever paint this plain-div backdrop
59523
60081
  // visible before anything has actually opened it — see popover.jsx's
59524
60082
  // own identical prop for the full reasoning, and this file's own CSS
59525
60083
  // for the rule it drives.
59526
- "navi-hidden": openController.opened ? undefined : "",
60084
+ "navi-hidden": openController.openedInDom ? undefined : "",
59527
60085
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
59528
60086
  "animationDuration": rest.animationDuration,
59529
60087
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
@@ -59535,7 +60093,7 @@ const useDialogProps = props => {
59535
60093
  tabIndex,
59536
60094
  // See backdropProps' own identical prop above for the full reasoning
59537
60095
  // (kept once, not repeated here).
59538
- "aria-expanded": openController.opened ? "true" : "false",
60096
+ "aria-expanded": openController.openedInDom ? "true" : "false",
59539
60097
  // Present from the very first render (recomputed fresh from
59540
60098
  // openController.opened every time, not a frozen mount-time constant —
59541
60099
  // see popover.jsx's own identical prop for the full reasoning) so a
@@ -59543,28 +60101,28 @@ const useDialogProps = props => {
59543
60101
  // can't silently defeat showModal()/close()'s native open/close — see
59544
60102
  // this file's own CSS rule for dialogEl and the open/close steps below
59545
60103
  // for how it's toggled.
59546
- "navi-hidden": openController.opened ? undefined : "",
60104
+ "navi-hidden": openController.openedInDom ? undefined : "",
59547
60105
  // Unlike Popover (which genuinely can't resolve "auto" until it
59548
60106
  // measures against a real anchor), resolvedAnimation is already fully
59549
60107
  // known synchronously here — a dialog never needs to flip anything
59550
60108
  // after measuring (see this file's top comment) — so there's no reason
59551
60109
  // to withhold the attribute for the auto case the way Popover has to.
59552
60110
  "navi-animation": resolvedAnimation,
59553
- // Only meaningful for the via-attribute renderer's own native
59554
- // ::backdrop (see this file's CSS for the "capture" glass effect) — a
60111
+ // Only meaningful for a modal's own native ::backdrop (see this file's
60112
+ // CSS for the "capture" glass effect) — a
59555
60113
  // pseudo-element can't carry its own attributes, so this has to live on
59556
60114
  // the originating .navi_dialog element instead, same reasoning as
59557
- // navi-animation above. Harmless for the custom renderer too (its own
59558
- // real backdrop element already gets the same attribute via
59559
- // backdropProps above, which is what its own CSS actually keys off).
60115
+ // navi-animation above. Harmless for a local dialog too (its own real
60116
+ // backdrop element already gets the same attribute via backdropProps
60117
+ // above, which is what its own CSS actually keys off).
59560
60118
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
59561
- // Only load-bearing for the via-attribute renderer's own native
59562
- // ::backdrop, same "a pseudo-element can't carry attributes" reasoning
59563
- // as the prop just above (and harmless for the custom renderer, whose
59564
- // real backdrop element gets it via backdropProps).
60119
+ // Only load-bearing for a modal's own native ::backdrop, same "a
60120
+ // pseudo-element can't carry attributes" reasoning as the prop just above
60121
+ // (and harmless for a local dialog, whose real backdrop element gets it
60122
+ // via backdropProps).
59565
60123
  "data-backdrop-variant": backdropVariant,
59566
60124
  // Read by the native ::backdrop, which inherits them from here (see this
59567
- // file's CSS) — the custom renderer's own backdrop element gets them via
60125
+ // file's CSS) — a local dialog's own backdrop element gets them via
59568
60126
  // backdropProps above.
59569
60127
  backdropColor,
59570
60128
  backdropFilter,
@@ -59593,6 +60151,12 @@ const useDialogProps = props => {
59593
60151
  // absolutely placed by its own code — so it says so once, here (group.jsx
59594
60152
  // reads this attribute to tell a member from anything else on its line).
59595
60153
  "navi-out-of-flow": "",
60154
+ // The top layer without a wall: showPopover() is what puts it there, and
60155
+ // it only accepts an element that declares itself a popover. "manual" so
60156
+ // the browser's own light dismiss stays out of it — what an outside press
60157
+ // does is pointerInteractionOutsideEffect's answer, given by the
60158
+ // document-level listener in openEffect above.
60159
+ "popover": isTopLayerPopover ? "manual" : undefined,
59596
60160
  "baseClassName": "navi_dialog",
59597
60161
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
59598
60162
  // Distinguishes the two renderers for the CSS above (position: fixed
@@ -59642,8 +60206,8 @@ const useDialogProps = props => {
59642
60206
  },
59643
60207
  "onCancel": e => {
59644
60208
  // The dialog's own "cancel" (Escape on a modal showModal() dialog —
59645
- // the custom renderer's Escape handling lives in onKeyDownShortcuts
59646
- // above) fires on the dialog element itself. But a child
60209
+ // the Escape handling for the other two shows lives in
60210
+ // onKeyDownShortcuts above) fires on the dialog element itself. But a child
59647
60211
  // <input type="file"> also fires a BUBBLING "cancel" when the user
59648
60212
  // dismisses the file chooser (per the HTML spec), and it lands here
59649
60213
  // too; only the dialog's own cancel means "close".
@@ -59657,12 +60221,12 @@ const useDialogProps = props => {
59657
60221
  children
59658
60222
  });
59659
60223
 
59660
- // Outside-click handling for layer="local" only — the via-attribute
59661
- // renderer's own is a plain document-level listener instead, set up in
59662
- // openEffect above (see this file's top comment for why: neither a real
59663
- // backdrop element nor dialogEl's own mousedown reliably fires for a
59664
- // native ::backdrop click).
59665
- if (!isModal && backdrop) {
60224
+ // Outside-click handling for the wall we render ourselves. A modal's is a
60225
+ // plain document-level listener instead, set up in openEffect above (see
60226
+ // this file's top comment for why: neither a real backdrop element nor
60227
+ // dialogEl's own mousedown reliably fires for a native ::backdrop click),
60228
+ // and so is a wall-less dialog's.
60229
+ if (!isTopLayer && backdrop) {
59666
60230
  backdropProps.onMouseDown = mouseDownEvent => {
59667
60231
  if (mouseDownEvent.button !== 0) {
59668
60232
  return;
@@ -59672,7 +60236,7 @@ const useDialogProps = props => {
59672
60236
  if (openedDuringThisPress(openController)) {
59673
60237
  return;
59674
60238
  }
59675
- // See the custom renderer's own onDocumentMouseDown: a click inside
60239
+ // See the document-level listener in openEffect above: a click inside
59676
60240
  // another popup is a click on what is in front, not an outside click.
59677
60241
  const dialogEl = ref.current;
59678
60242
  const popupUnderPointer = mouseDownEvent.target.closest?.(`[navi-control="dialog"], [navi-control="popover"]`);
@@ -59689,7 +60253,7 @@ const useDialogProps = props => {
59689
60253
  // the same behavior for Dialog.
59690
60254
  };
59691
60255
  }
59692
- return [isModal || !backdrop ? null : backdropProps, contentProps];
60256
+ return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
59693
60257
  };
59694
60258
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
59695
60259
 
@@ -61160,12 +61724,12 @@ const usePopoverProps = props => {
61160
61724
  // commit (a plain prop is, no effect needed) — see
61161
61725
  // use_displayed_layout_effect.js's own comments for why a descendant
61162
61726
  // relying on aria-expanded's mere presence needs that.
61163
- "aria-expanded": openController.opened ? "true" : "false",
61727
+ "aria-expanded": openController.openedInDom ? "true" : "false",
61164
61728
  // Read fresh on every render (not frozen at mount), so it stays
61165
61729
  // correct even across a re-render that happens to occur while open —
61166
61730
  // see contentProps' own identical prop just below for the full
61167
61731
  // reasoning (kept once, not repeated here).
61168
- "navi-hidden": openController.opened ? undefined : "",
61732
+ "navi-hidden": openController.openedInDom ? undefined : "",
61169
61733
  "styleCSSVars": POPUP_STYLE_CSS_VARS,
61170
61734
  "animationDuration": rest.animationDuration,
61171
61735
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
@@ -61214,7 +61778,7 @@ const usePopoverProps = props => {
61214
61778
  "navi-animation": isAutoAnimation ? undefined : animation,
61215
61779
  // See backdropProps' own identical prop above for the full reasoning
61216
61780
  // (kept once, not repeated here).
61217
- "aria-expanded": openController.opened ? "true" : "false",
61781
+ "aria-expanded": openController.openedInDom ? "true" : "false",
61218
61782
  // Only load-bearing for the custom renderer (a plain div has no native
61219
61783
  // starting-hidden default) — present from this very first render so
61220
61784
  // there's no gap for the browser to ever paint it visible before
@@ -61224,7 +61788,7 @@ const usePopoverProps = props => {
61224
61788
  // the last render, so as long as this always reflects the *current*
61225
61789
  // truth, it never fights the imperative
61226
61790
  // removeAttribute/setAttribute openEffect/close do directly.
61227
- "navi-hidden": openController.opened ? undefined : "",
61791
+ "navi-hidden": openController.openedInDom ? undefined : "",
61228
61792
  "styleCSSVars": POPUP_STYLE_CSS_VARS,
61229
61793
  ...rest,
61230
61794
  ...autoFocusProps,
@@ -61408,22 +61972,18 @@ const css$C = /* css */`@layer navi {
61408
61972
  * — without this, the exact same `<Popup>` usage would behave
61409
61973
  * differently (close-on-outside-click or not) purely based on which mode
61410
61974
  * the screen-size check happens to pick, which defeats the point of
61411
- * having one shared API in the first place. Note this can only ever go so
61412
- * far: in dialog mode, `"none"`/`"capture"` still absorb every outside
61413
- * click (no visual effect vs. dimmed) rather than truly letting it
61414
- * through, since a `<dialog>` is always modal one way or another (see
61415
- * `dialog.jsx`'s own doc) — a popover's fully passive, click-through
61416
- * backdrop has no dialog-mode equivalent. Whatever content `Popup` opens
61417
- * is unavoidably *more* intrusive once it switches to dialog mode than
61418
- * the exact same usage would be as a popover — worth keeping in mind for
61419
- * anything that relies on `Popup` and can end up on a small screen.
61975
+ * having one shared API in the first place. Note this only says what a
61976
+ * press outside *does*; whether it reaches the page at all is `backdrop`
61977
+ * below, and `"none"`/`"capture"` describe a wall either way — the popup
61978
+ * absorbs the press without closing, dimmed or not.
61420
61979
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
61421
61980
  * popup and the page at all: `false` lets a press outside both dismiss the
61422
- * popup and reach whatever it landed on, in one gesture. Reaches the popover
61423
- * only — a `Dialog` is `showModal()`'d in the `layer="top"` it defaults to,
61424
- * and the page behind is then genuinely inert — so it is dropped rather than
61425
- * forwarded when the mode resolution picks a top-layer dialog, the same
61426
- * "only so far" as `pointerInteractionOutsideEffect` above.
61981
+ * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
61982
+ * honoured in either mode — a wall-less `Dialog` is shown through the
61983
+ * Popover API rather than `showModal()` — so which mode the screen-size
61984
+ * resolution picks says nothing about whether the page behind stays live.
61985
+ * It is also what makes a sheet docked to the bottom of a phone's screen
61986
+ * (`dockedOnSmallTouchScreen`) non-modal.
61427
61987
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
61428
61988
  * as-is to whichever component renders (both understand it identically):
61429
61989
  * how visible the backdrop is, independently of what an outside click
@@ -61494,9 +62054,6 @@ const Popup = props => {
61494
62054
  // exact same <Popup> usage behaves identically regardless of which
61495
62055
  // mode the automatic screen-size resolution happens to pick.
61496
62056
  pointerInteractionOutsideEffect = "close",
61497
- // Read here rather than left in ...rest so the dialog branch can drop it:
61498
- // a top-layer dialog is modal and has no press left to let through, and
61499
- // warns when asked (see Dialog's own backdrop prop).
61500
62057
  backdrop,
61501
62058
  // Popover-only (see this component's own doc) — destructured out so
61502
62059
  // they're never part of ...rest, and therefore never forwarded to
@@ -61531,7 +62088,7 @@ const Popup = props => {
61531
62088
  sizeFromAnchor: sizeFromAnchor,
61532
62089
  maxWidth: maxWidth,
61533
62090
  pointerInteractionOutsideEffect: pointerInteractionOutsideEffect,
61534
- backdrop: rest.layer === "local" ? backdrop : undefined,
62091
+ backdrop: backdrop,
61535
62092
  className: withPropsClassName("navi_popup", className),
61536
62093
  expand: expand,
61537
62094
  expandX: expandX,
@@ -61628,6 +62185,10 @@ const css$B = /* css */`.navi_picker {
61628
62185
  }
61629
62186
  }
61630
62187
  }
62188
+
62189
+ &[data-dialog-size-from-anchor] .navi_dialog {
62190
+ --dialog-max-width: var(--picker-dialog-max-width, var(--anchor-width));
62191
+ }
61631
62192
  }
61632
62193
 
61633
62194
  &[data-popup-width-fit-content] {
@@ -62276,7 +62837,7 @@ const PickerCustom = props => {
62276
62837
  // because that is where such a box says what it travels by — and
62277
62838
  // asked at every press, since what the picker sits in is not the
62278
62839
  // picker's to know at mount.
62279
- if (!opensOnPress || interactionsDispute || isPressDisputedByDrag(e.target)) {
62840
+ if (!opensOnPress || interactionsDispute || isPressDisputedByDrag(e.target) || isPressDisputedByHold(ref.current)) {
62280
62841
  return null;
62281
62842
  }
62282
62843
  return {
@@ -62326,6 +62887,13 @@ const PickerCustom = props => {
62326
62887
  mode: mode
62327
62888
  });
62328
62889
  };
62890
+
62891
+ // A hold declared on something AROUND the picker (a card opened by
62892
+ // `openOn="longpress"`, holding this one in its drawing): the finger going
62893
+ // down may be the start of that hold, so this picker opens on the click — which
62894
+ // the hold, if it completes, swallows — rather than on the press. The picker's
62895
+ // own hold (its `openOn`) is not "around" it, and has already stepped back.
62896
+ const isPressDisputedByHold = pickerEl => Boolean(pickerEl?.parentElement?.closest(`[${LONGPRESS_ATTRIBUTE}]`));
62329
62897
  const getPickerInput = pickerEl => {
62330
62898
  return pickerEl.querySelector(".navi_picker_input");
62331
62899
  };
@@ -62399,6 +62967,7 @@ const PickerContentInsidePopup = props => {
62399
62967
  return jsx(Next, {
62400
62968
  "aria-haspopup": isPopover ? "listbox" : "dialog",
62401
62969
  "navi-popover-mode": isPopover ? popoverMode : undefined,
62970
+ "data-dialog-size-from-anchor": !isPopover && dialogSizeFromAnchor ? "" : undefined,
62402
62971
  ...rest,
62403
62972
  // On popupProps already (see the picker's popup assembly); they mean
62404
62973
  // nothing to the picker element.
@@ -62740,6 +63309,34 @@ const pickerUIIsNaviOwn = ui => {
62740
63309
  return Boolean(ui) && typeof ui === "object" && Boolean(ui.type?.isPickerOwnUI);
62741
63310
  };
62742
63311
 
63312
+ /**
63313
+ * What the picker holds, read from inside its `ui` — for a drawing given as
63314
+ * an element (`ui={<MyCard game={game} />}`), which the picker cannot hand
63315
+ * props to. `value` is the state the picker holds right now: the answer the
63316
+ * popup just closed on, before the server has said anything, and put back if
63317
+ * the action fails. `loading` is the run in flight; `interactive` is false
63318
+ * while the picker is disabled, read-only or busy.
63319
+ *
63320
+ * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
63321
+ * null outside a picker.
63322
+ */
63323
+ const usePickerState = () => {
63324
+ const context = useContext(PickerContext);
63325
+ if (!context) {
63326
+ return null;
63327
+ }
63328
+ const {
63329
+ value,
63330
+ loading,
63331
+ interactive
63332
+ } = context;
63333
+ return {
63334
+ value,
63335
+ loading,
63336
+ interactive
63337
+ };
63338
+ };
63339
+
62743
63340
  const PickerNaviMinute = props => {
62744
63341
  const Next = useNextResolver();
62745
63342
  const {
@@ -70424,6 +71021,16 @@ const css$t = /* css */`@layer navi {
70424
71021
  user-select: none;
70425
71022
  }
70426
71023
 
71024
+ &[data-open-on] {
71025
+ & > .navi_picker_box > .navi_picker_input {
71026
+ pointer-events: none;
71027
+ }
71028
+
71029
+ & > .navi_picker_box > .navi_picker_value {
71030
+ pointer-events: auto;
71031
+ }
71032
+ }
71033
+
70427
71034
  &[navi-ui-custom] {
70428
71035
  & .navi_picker_input {
70429
71036
  top: calc(-1 * (var(--picker-border-width) + var(--x-picker-press-padding-top)));
@@ -70749,12 +71356,15 @@ const PickerButton = props => {
70749
71356
  // What the picker knows about itself, for the pieces it does not place: the
70750
71357
  // drawings of its value (Picker.UI.*), and the affordances a caller may put
70751
71358
  // in their own `ui` (Picker.Clear) as much as the ones it puts in its slot.
71359
+ // `ui={MyCard}`: a component the picker renders itself, told what it holds.
71360
+ const UIComponent = typeof ui === "function" ? ui : null;
70752
71361
  const pickerContext = {
70753
71362
  value,
70754
71363
  placeholder,
70755
71364
  maxLines,
70756
71365
  id: inputProps.id,
70757
71366
  interactive,
71367
+ loading,
70758
71368
  clearConfirm
70759
71369
  };
70760
71370
  return (
@@ -70945,7 +71555,17 @@ const PickerButton = props => {
70945
71555
  size: rightSlotIconSize,
70946
71556
  lineOverflow: "allow",
70947
71557
  children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
70948
- }) : jsx(PickerDefaultUI, {}) : ui
71558
+ }) : jsx(PickerDefaultUI, {}) : UIComponent ?
71559
+ // Told what the picker HOLDS — the value it just closed
71560
+ // on, before the server has said anything — so a façade
71561
+ // shows the answer at once, and the wait with it. The
71562
+ // same state usePickerState() hands an element (see the
71563
+ // `ui` doc).
71564
+ jsx(UIComponent, {
71565
+ value: value,
71566
+ loading: loading,
71567
+ interactive: interactive
71568
+ }) : ui
70949
71569
  })
70950
71570
  })
70951
71571
  }), hasRightSlot ? jsx("span", {
@@ -82935,11 +83555,11 @@ const css = /* css */`.navi_side_panel {
82935
83555
  * @param {boolean} [props.closeOnClickOutside=false] - `false` (default):
82936
83556
  * maps to `pointerInteractionOutsideEffect="none"` — in popover mode, no
82937
83557
  * backdrop at all, outside clicks pass straight through; in dialog mode,
82938
- * the outside click is still absorbed (a `<dialog>` always blocks
82939
- * interaction with the rest of the page one way or another — see
82940
- * `dialog.jsx`'s own doc) but with no dimming effect. `true`: closes the
82941
- * panel on an outside click instead, and also enables trapping Tab
82942
- * navigation inside the panel (`focusCapture`) — closing on outside
83558
+ * the outside click is absorbed by the panel's own wall but changes
83559
+ * nothing. Pass `backdrop={false}` (forwarded to `Popup`) for a panel with
83560
+ * no wall in either mode, whose outside presses reach the page. `true`:
83561
+ * closes the panel on an outside click instead, and also enables trapping
83562
+ * Tab navigation inside the panel (`focusCapture`) — closing on outside
82943
83563
  * interaction only makes sense paired with not letting focus silently
82944
83564
  * leave the panel first.
82945
83565
  * @param {boolean} [props.swipeToClose=true] - Pushing the panel back
@@ -82949,12 +83569,11 @@ const css = /* css */`.navi_side_panel {
82949
83569
  * deliberately. See `swipe_to_close.js` for when the gesture is claimed.
82950
83570
  * @param {"dialog"|"popover"} [props.mode] - Forwarded to `Popup` — forces
82951
83571
  * one underlying renderer instead of its automatic screen-size
82952
- * resolution. Note that if `Popup` ends up in dialog mode (small screen,
82953
- * or forced here), the panel becomes modal regardless of
82954
- * `closeOnClickOutside`/`pointerInteractionOutsideEffect`: a `<dialog>`
82955
- * always blocks interaction with the rest of the page one way or another
82956
- * (see `dialog.jsx`'s own doc) — there is no dialog-mode equivalent of a
82957
- * popover's fully passive, click-through backdrop.
83572
+ * resolution. Note that if `Popup` ends up in dialog mode (small screen, or
83573
+ * forced here), the panel is modal unless it says `backdrop={false}`:
83574
+ * `closeOnClickOutside`/`pointerInteractionOutsideEffect` only say what a
83575
+ * press on the wall does, not whether there is one (see `dialog.jsx`'s own
83576
+ * doc).
82958
83577
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
82959
83578
  * close button — add one wherever it makes sense for the layout (e.g. a
82960
83579
  * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
@@ -83259,5 +83878,5 @@ const UserSvg = () => jsx("svg", {
83259
83878
  })
83260
83879
  });
83261
83880
 
83262
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
83881
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
83263
83882
  //# sourceMappingURL=jsenv_navi.js.map