@jsenv/navi 0.29.301 → 0.29.303

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";
@@ -10376,6 +10376,11 @@ const readNumberFromDom = (element, attribute, defaultValue) => {
10376
10376
  * happened. Registered through the same door an application uses (see
10377
10377
  * interaction_registry.js) — navi has no private one.
10378
10378
  *
10379
+ * `dblclick` is not among them, and deliberately: the browser only fires it for a
10380
+ * mouse, so an element declaring it would answer half the hands that reach it.
10381
+ * What a hand does twice is `double_click`, counted from the pointer in
10382
+ * interaction_press.js.
10383
+ *
10379
10384
  * `contextmenu` is the only one that takes something away: the browser's own menu
10380
10385
  * would cover the answer to the request it is. It takes it away AFTER, though —
10381
10386
  * a native interaction IS its own event, the very one the gate reads, and a gate
@@ -10389,7 +10394,6 @@ const NATIVE_TYPE_SET = new Set([
10389
10394
  "mousedown",
10390
10395
  "mouseup",
10391
10396
  "click",
10392
- "dblclick",
10393
10397
  "contextmenu",
10394
10398
  ]);
10395
10399
 
@@ -10447,6 +10451,15 @@ const SWIPE_THRESHOLD_DEFAULT = 0.33;
10447
10451
  const LONGPRESS_DELAY_DEFAULT = 450;
10448
10452
  // Past this the finger is going somewhere: it swipes, it does not hold.
10449
10453
  const LONGPRESS_SLOP_DEFAULT = 8;
10454
+ // The window a double click is counted in, opened by the FIRST press. Under the
10455
+ // hold's delay on purpose: a pause longer than the wait navi calls "held" is
10456
+ // longer than one gesture.
10457
+ const DOUBLE_CLICK_DELAY_DEFAULT = 400;
10458
+ // How far apart the two presses may be, and how far either of them may travel
10459
+ // before it is a gesture rather than a tap. A fingertip and not a pixel: between
10460
+ // the two the finger leaves the glass and lands again where it means to, which is
10461
+ // a wider question than the hold's slop above (has this finger stood still?).
10462
+ const DOUBLE_CLICK_SLOP_DEFAULT = 30;
10450
10463
  // How long the element takes to reach where the gesture leaves it, or to come
10451
10464
  // back. Written into the CSS below from here: the state is cleaned up when the
10452
10465
  // movement is over, so a duration living only in the stylesheet would be a timing
@@ -10458,11 +10471,19 @@ const SETTLE_DURATION_MS = 200;
10458
10471
  const SWIPE_THRESHOLD_ATTRIBUTE = "data-swipe-threshold";
10459
10472
  const LONGPRESS_DELAY_ATTRIBUTE = "data-longpress-delay";
10460
10473
  const LONGPRESS_SLOP_ATTRIBUTE = "data-longpress-slop";
10474
+ const DOUBLE_CLICK_DELAY_ATTRIBUTE = "data-double-click-delay";
10475
+ const DOUBLE_CLICK_SLOP_ATTRIBUTE = "data-double-click-slop";
10461
10476
 
10462
- // Which axes this element takes a swipe on, and that it takes a hold: said in the
10463
- // DOM at render time, for the CSS below and for the boxes above to read.
10477
+ // Which axes this element takes a swipe on, and that it takes a hold or counts
10478
+ // taps: said in the DOM at render time, for the CSS below and for the boxes above
10479
+ // to read.
10464
10480
  const SWIPE_AXES_ATTRIBUTE = "data-swipe";
10465
10481
  const LONGPRESS_ATTRIBUTE = "data-longpress";
10482
+ const DOUBLE_CLICK_ATTRIBUTE = "data-double-click";
10483
+
10484
+ const LONGPRESS = "longpress";
10485
+ const DOUBLE_CLICK = "double_click";
10486
+ const SINGLE_CLICK = "single_click";
10466
10487
 
10467
10488
  import.meta.css = /* css */ [`@property --swipe-progress {
10468
10489
  syntax: "<number>";
@@ -10488,15 +10509,19 @@ import.meta.css = /* css */ [`@property --swipe-progress {
10488
10509
  touch-action: none;
10489
10510
  }
10490
10511
 
10512
+ :where([data-double-click]) {
10513
+ touch-action: manipulation;
10514
+ }
10515
+
10491
10516
  [data-longpress] {
10492
10517
  -webkit-touch-callout: none;
10493
10518
  }
10494
10519
 
10495
- [data-longpress], [data-swipe] {
10520
+ [data-longpress], [data-swipe], [data-double-click] {
10496
10521
  user-select: none;
10497
10522
  }
10498
10523
 
10499
- :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"]) {
10524
+ :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"]) {
10500
10525
  user-select: text;
10501
10526
  }
10502
10527
 
@@ -10515,9 +10540,13 @@ import.meta.css = /* css */ [`@property --swipe-progress {
10515
10540
 
10516
10541
  defineInteractionDetector({
10517
10542
  name: "press",
10518
- claims: (type) => type in AXIS_BY_SWIPE_TYPE || type === "longpress",
10519
- // A swipe and a hold are both "what this press turns out to be": until it
10520
- // turns out, the press is theirs.
10543
+ claims: (type) =>
10544
+ type in AXIS_BY_SWIPE_TYPE ||
10545
+ type === LONGPRESS ||
10546
+ type === DOUBLE_CLICK ||
10547
+ type === SINGLE_CLICK,
10548
+ // Every one of them is "what this press turns out to be": until it turns out,
10549
+ // the press is theirs.
10521
10550
  disputesPress: true,
10522
10551
  setup: (element, trigger, { types, readConfig }) => {
10523
10552
  let axes = "";
@@ -10527,7 +10556,15 @@ defineInteractionDetector({
10527
10556
  axes += axis;
10528
10557
  }
10529
10558
  }
10530
- const hasLongPress = types.includes("longpress");
10559
+ const hasLongPress = types.includes(LONGPRESS);
10560
+ const hasDoubleClick = types.includes(DOUBLE_CLICK);
10561
+ const hasSingleClick = types.includes(SINGLE_CLICK);
10562
+ const countsTaps = hasDoubleClick || hasSingleClick;
10563
+ if (hasSingleClick && !hasDoubleClick) {
10564
+ console.warn(
10565
+ `interactions: "${SINGLE_CLICK}" waits for a "${DOUBLE_CLICK}" that is not declared here, so all it does is answer a click late. Declare "${DOUBLE_CLICK}" beside it, or use "click".`,
10566
+ );
10567
+ }
10531
10568
 
10532
10569
  const undo = [];
10533
10570
  const mark = (attribute, value) => {
@@ -10580,6 +10617,79 @@ defineInteractionDetector({
10580
10617
  if (hasLongPress) {
10581
10618
  mark(LONGPRESS_ATTRIBUTE, "");
10582
10619
  }
10620
+ if (countsTaps) {
10621
+ mark(DOUBLE_CLICK_ATTRIBUTE, "");
10622
+ }
10623
+
10624
+ // The tap a next press may pair with, and the window it is waited for in. It
10625
+ // outlives the press that made it — which is what a double click IS — so it
10626
+ // lives here rather than in the pointerdown below.
10627
+ let firstTap = null;
10628
+ let tapWindowTimeout = null;
10629
+ let tapWait = null;
10630
+ const forgetTaps = () => {
10631
+ clearTimeout(tapWindowTimeout);
10632
+ tapWindowTimeout = null;
10633
+ firstTap = null;
10634
+ tapWait?.cancel();
10635
+ tapWait = null;
10636
+ };
10637
+ undo.push(forgetTaps);
10638
+ // The window closes on a tap nothing came back for. Counted from the press
10639
+ // that opened it rather than from the tap that ends it, so what the caller
10640
+ // tunes is one rhythm and not a press plus a pause — a press slow enough to be
10641
+ // a hold therefore closes its own window on the spot, and cannot start a
10642
+ // double click.
10643
+ const openTapWindow = (pressEvent, tapEvent, delay) => {
10644
+ firstTap = {
10645
+ at: pressEvent.timeStamp,
10646
+ x: tapEvent.clientX,
10647
+ y: tapEvent.clientY,
10648
+ pointerType: tapEvent.pointerType,
10649
+ };
10650
+ if (hasSingleClick) {
10651
+ // The element's click is navi's from here: it is held for the length of
10652
+ // the window and `single_click` is what says it happened (see the top of
10653
+ // this file).
10654
+ const clickSuppressionIsOver = suppressClickAfterGesture();
10655
+ clickSuppressionIsOver();
10656
+ }
10657
+ clearTimeout(tapWindowTimeout);
10658
+ tapWindowTimeout = setTimeout(
10659
+ () => {
10660
+ tapWindowTimeout = null;
10661
+ firstTap = null;
10662
+ if (hasSingleClick) {
10663
+ trigger(SINGLE_CLICK, tapEvent, {
10664
+ pointerType: tapEvent.pointerType,
10665
+ });
10666
+ }
10667
+ },
10668
+ delay - (tapEvent.timeStamp - pressEvent.timeStamp),
10669
+ );
10670
+ };
10671
+
10672
+ if (hasSingleClick) {
10673
+ // A click no press made — a keyboard activation, an `element.click()`. The
10674
+ // window exists to find out whether a second press is coming, and there is
10675
+ // no press here, so it is not held: said at once, the way the browser's own
10676
+ // click would have been.
10677
+ const onClick = (clickEvent) => {
10678
+ if (isPressDrivenClick(clickEvent)) {
10679
+ // A press's own click, swallowed by the suppression armed at its tap:
10680
+ // it never reaches here, and if it does (the suppression having already
10681
+ // been spent) the tap has said it or is about to.
10682
+ return;
10683
+ }
10684
+ trigger(SINGLE_CLICK, clickEvent, {
10685
+ pointerType: clickEvent.pointerType,
10686
+ });
10687
+ };
10688
+ element.addEventListener("click", onClick);
10689
+ undo.push(() => {
10690
+ element.removeEventListener("click", onClick);
10691
+ });
10692
+ }
10583
10693
 
10584
10694
  const onPointerDown = (pointerDownEvent) => {
10585
10695
  if (pointerDownEvent.button !== 0) {
@@ -10590,6 +10700,50 @@ defineInteractionDetector({
10590
10700
  // Set below, with the hold; a no-op until then so the swipe can call it.
10591
10701
  let forgetInnerLongPress = () => {};
10592
10702
 
10703
+ if (countsTaps) {
10704
+ const delay = readConfig(
10705
+ DOUBLE_CLICK_DELAY_ATTRIBUTE,
10706
+ DOUBLE_CLICK_DELAY_DEFAULT,
10707
+ );
10708
+ const slop = readConfig(
10709
+ DOUBLE_CLICK_SLOP_ATTRIBUTE,
10710
+ DOUBLE_CLICK_SLOP_DEFAULT,
10711
+ );
10712
+ // Whether this press is the second of a pair is settled HERE, when it
10713
+ // lands, and not when it is let go of: the window is about where the
10714
+ // hand went and how soon, and a second press held a little longer than
10715
+ // the window would otherwise be answered as a lone click while it is
10716
+ // still down.
10717
+ const completesTheFirstTap =
10718
+ firstTap && continuesTap(firstTap, pointerDownEvent, delay, slop);
10719
+ if (completesTheFirstTap) {
10720
+ clearTimeout(tapWindowTimeout);
10721
+ tapWindowTimeout = null;
10722
+ } else {
10723
+ forgetTaps();
10724
+ }
10725
+ tapWait = waitForTap(pointerDownEvent, {
10726
+ slop,
10727
+ onTap: (tapEvent) => {
10728
+ tapWait = null;
10729
+ if (completesTheFirstTap) {
10730
+ firstTap = null;
10731
+ // The click this second press leaves behind was already answered,
10732
+ // by the gesture the two of them made. Armed and released at once:
10733
+ // the release does not lift the suppression, it says the gesture is
10734
+ // over (see suppressClickAfterGesture).
10735
+ const clickSuppressionIsOver = suppressClickAfterGesture();
10736
+ clickSuppressionIsOver();
10737
+ trigger(DOUBLE_CLICK, tapEvent, {
10738
+ pointerType: tapEvent.pointerType,
10739
+ });
10740
+ return;
10741
+ }
10742
+ openTapWindow(pointerDownEvent, tapEvent, delay);
10743
+ },
10744
+ });
10745
+ }
10746
+
10593
10747
  if (axes) {
10594
10748
  swipe = startSwipe(pointerDownEvent, {
10595
10749
  element,
@@ -10609,6 +10763,9 @@ defineInteractionDetector({
10609
10763
  press?.cancel();
10610
10764
  press = null;
10611
10765
  forgetInnerLongPress();
10766
+ // …and it is not a tap either, nor the second of a pair: the press
10767
+ // has left, and what it left is a swipe.
10768
+ forgetTaps();
10612
10769
  },
10613
10770
  });
10614
10771
  }
@@ -10626,6 +10783,7 @@ defineInteractionDetector({
10626
10783
  press.cancel();
10627
10784
  press = null;
10628
10785
  forgetInnerLongPress();
10786
+ warnOnceAboutHoldTakenByInner(element, longPressEvent.target);
10629
10787
  };
10630
10788
  forgetInnerLongPress = () => {
10631
10789
  element.removeEventListener("longpress", onInnerLongPress);
@@ -10638,9 +10796,12 @@ defineInteractionDetector({
10638
10796
  onPressHeld: (pressEvent, { endPress }) => {
10639
10797
  forgetInnerLongPress();
10640
10798
  // The hold won the arbitration: the swipe never got the distance it
10641
- // needed, and must not get it from whatever the finger does next.
10799
+ // needed, and must not get it from whatever the finger does next. The
10800
+ // double click loses the same way — a press held this long is the
10801
+ // hold's whether it is the first of a pair or the second.
10642
10802
  swipe?.stop();
10643
10803
  swipe = null;
10804
+ forgetTaps();
10644
10805
  const clickSuppressionIsOver = suppressClickAfterGesture();
10645
10806
  const onPointerEnd = () => {
10646
10807
  window.removeEventListener("pointerup", onPointerEnd, true);
@@ -10670,6 +10831,24 @@ defineInteractionDetector({
10670
10831
  },
10671
10832
  });
10672
10833
 
10834
+ // Said once per element, in dev: the hold declared here can never fire while
10835
+ // the one inside it is declared, and nothing else would tell — the outer one
10836
+ // simply never happens. Two holds on the same pixels are usually one hold too
10837
+ // many (see interactions.md); the fix is on the caller's side, so it has to be
10838
+ // named there.
10839
+ const holdTakenWarnedSet = new WeakSet();
10840
+ const warnOnceAboutHoldTakenByInner = (element, innerElement) => {
10841
+ if (holdTakenWarnedSet.has(element)) {
10842
+ return;
10843
+ }
10844
+ holdTakenWarnedSet.add(element);
10845
+ console.warn(
10846
+ `interactions: the "longpress" declared on this element was taken by the "longpress" declared inside it — the nearer hold wins, and this one never fires while the inner one is declared. One hold, one meaning: give the two things two gestures (a hold and a click, say), or declare the hold on one element only.`,
10847
+ element,
10848
+ innerElement,
10849
+ );
10850
+ };
10851
+
10673
10852
  const startSwipe = (
10674
10853
  pointerDownEvent,
10675
10854
  { element, axes, types, trigger, threshold, onSwipeStart },
@@ -10770,6 +10949,21 @@ const startSwipe = (
10770
10949
  });
10771
10950
  };
10772
10951
 
10952
+ // Whether a press landing now completes the tap before it: the same hand, soon
10953
+ // enough, and near enough. Axis by axis rather than as a distance, the way every
10954
+ // other slop in this family is read.
10955
+ const continuesTap = (tap, pointerDownEvent, delay, slop) => {
10956
+ if (pointerDownEvent.pointerType !== tap.pointerType) {
10957
+ return false;
10958
+ }
10959
+ if (pointerDownEvent.timeStamp - tap.at >= delay) {
10960
+ return false;
10961
+ }
10962
+ const xApart = Math.abs(pointerDownEvent.clientX - tap.x);
10963
+ const yApart = Math.abs(pointerDownEvent.clientY - tap.y);
10964
+ return xApart < slop && yApart < slop;
10965
+ };
10966
+
10773
10967
  const swipeTypeOf = (axis, pulled) => {
10774
10968
  const { positive, negative } = SWIPE_TYPE_BY_AXIS[axis];
10775
10969
  return pulled > 0 ? positive : negative;
@@ -26657,6 +26851,31 @@ const getHrefTargetInfo = (href) => {
26657
26851
  };
26658
26852
  };
26659
26853
 
26854
+ /*
26855
+ * An app can ship more than one document — an admin panel, a status page, an
26856
+ * entry point of its own — and a link to one of them is not a route, it is a
26857
+ * page load. The address does not say which of the two it is, and guessing
26858
+ * would be worse than not knowing: "no route matches, so it must be another
26859
+ * document" turns a typo'd in-app url into a silent reload of the app instead
26860
+ * of its own not-found screen. So the link says it.
26861
+ *
26862
+ * `<Link document>` / `<Button document>` is that, and the point of saying it
26863
+ * on a link rather than navigating from an action is everything a link is: an
26864
+ * address in the status bar, a middle click, "open in new tab", something for
26865
+ * assistive technology to read. Only the interception steps aside; the element
26866
+ * stays a real link and the browser loads the page.
26867
+ *
26868
+ * It travels as an attribute for the same reason `replace` does (see
26869
+ * link_replace.js): whoever answers the press sees the element, not the
26870
+ * component that rendered it.
26871
+ */
26872
+
26873
+ const LINK_DOCUMENT_ATTRIBUTE = "data-navi-document";
26874
+
26875
+ const linkAsksForDocument = (linkElement) => {
26876
+ return linkElement.hasAttribute(LINK_DOCUMENT_ATTRIBUTE);
26877
+ };
26878
+
26660
26879
  /*
26661
26880
  * A press aims at a place; it does not always go one step deeper. A row of tabs
26662
26881
  * is a lateral move — the neighbour is one finger away — so the whole row should
@@ -26990,6 +27209,12 @@ const setupBrowserIntegrationViaHistory = ({
26990
27209
  }
26991
27210
  return;
26992
27211
  }
27212
+ if (linkAsksForDocument(linkElement)) {
27213
+ // The link says its address is another document of this origin (see
27214
+ // link_document.js). Routing to it would land on the fallback route
27215
+ // instead of loading the page.
27216
+ return;
27217
+ }
26993
27218
  // Nothing here declared a route, so there is nothing to route to: the
26994
27219
  // page is a plain document and a link in it is a plain link. Taking it
26995
27220
  // over anyway would push the url and then have nothing to show for it —
@@ -27577,6 +27802,160 @@ const Head = ({
27577
27802
  return null;
27578
27803
  };
27579
27804
 
27805
+ /**
27806
+ * A page is `{ route, params }`, never the route alone: a section of a page is
27807
+ * as often a PARAM as it is a route of its own, and three branches of the same
27808
+ * route told apart by their params are three pages one walks between.
27809
+ * `params` is undefined for a page that is a route on its own.
27810
+ *
27811
+ * Whether such a page is the one on screen is asked from three places — the
27812
+ * row a <RouteTravel> walks, the relations a movement is written between, and
27813
+ * the fallback naming the pages it is the absence of — and they must all get
27814
+ * the same answer, which is why the reading lives here rather than next to any
27815
+ * one of them.
27816
+ */
27817
+
27818
+ // `matchesParams` reads paramsSignal, so a caller reading this during a render
27819
+ // is subscribed to the param changes that walk from one tab to the next —
27820
+ // matchingSignal alone never moves there, and a row whose tabs are params of
27821
+ // one route would never re-render.
27822
+ //
27823
+ // The params are read only for a route that matches, and that is not a signal
27824
+ // left unread: a reader wakes on anything it read last time, so what matters is
27825
+ // that everything able to make this answer change is among them.
27826
+ // matchingSignal is read whatever happens, and it is a NECESSARY condition —
27827
+ // while it is false no param of that route can put this page on screen, and the
27828
+ // day one could, matchingSignal itself has to turn true to say so, which is the
27829
+ // read that brings the params back in. (Asking anyway would be worse than
27830
+ // useless: the params of a route that does not match are not params.)
27831
+ const pageIsCurrent = ({ route, params }) => {
27832
+ if (!route.matchingSignal.value) {
27833
+ return false;
27834
+ }
27835
+ return params ? route.matchesParams(params) : true;
27836
+ };
27837
+
27838
+ /**
27839
+ * The page a <Route> container shows when none of its branches matched, given
27840
+ * a name.
27841
+ *
27842
+ * Every other branch of a container is a route, and a route is a thing one can
27843
+ * talk ABOUT from outside the tree: write a movement between two of them (see
27844
+ * route_transition.jsx), put one in a row (see route_travel.jsx), read whether
27845
+ * it is on screen. The fallback branch is the one page defined by an ABSENCE —
27846
+ * "an address none of these claim" — so it has no url pattern to be declared
27847
+ * from, and until it is named, nothing can be said about it.
27848
+ *
27849
+ * const NOT_FOUND_PAGE = routeFallback();
27850
+ * <Route fallback={NOT_FOUND_PAGE} element={NotFoundPage} />
27851
+ * defineRouteTransition(null, NOT_FOUND_PAGE, "slide-x");
27852
+ *
27853
+ * The absence is of a PARTICULAR set of claims — the branches of the container
27854
+ * holding it — and that set is not knowable from the routes of the application:
27855
+ * a route matching every address is a normal thing to declare (a carrier for
27856
+ * search params and route actions the whole app shares), and it would answer
27857
+ * "something matched" everywhere. So the container fills it in from the
27858
+ * children it was written with (see collectBranches in route.jsx), and a
27859
+ * fallback no container holds claims nothing.
27860
+ *
27861
+ * That leaves what a container cannot see: the address its own subtree lives
27862
+ * under. A section owning its sub-router renders it several components below
27863
+ * the leaf that matched the section's prefix, and "none of these five matched"
27864
+ * is then true on every screen of the application. Such a fallback names the
27865
+ * route it is under, and answers only there:
27866
+ *
27867
+ * const DASHBOARD_NOT_FOUND_PAGE = routeFallback(DASHBOARD_SECTION_ROUTE);
27868
+ */
27869
+
27870
+
27871
+ const routeFallbackPrivatePropertiesMap = new WeakMap();
27872
+
27873
+ /**
27874
+ * Names the fallback branch of a <Route> container, so relations can be
27875
+ * written about the page it shows and its `matchingSignal` read from anywhere.
27876
+ *
27877
+ * @param {object} [scopeRoute] - the route this container's subtree lives
27878
+ * under, when the container is not the application's own router. Without it
27879
+ * the fallback answers wherever its container is on screen.
27880
+ * @returns {object} pass it as `<Route fallback={...}>`.
27881
+ */
27882
+ const routeFallback = (scopeRoute) => {
27883
+ const pagesSignal = signal(null);
27884
+ const nothingMatches = () => {
27885
+ const pages = pagesSignal.value;
27886
+ if (!pages) {
27887
+ return false;
27888
+ }
27889
+ for (const page of pages) {
27890
+ if (pageIsCurrent(page)) {
27891
+ return false;
27892
+ }
27893
+ }
27894
+ return true;
27895
+ };
27896
+ const routeFallback = {
27897
+ isRouteFallback: true,
27898
+ scopeRoute,
27899
+ matchingSignal: scopeRoute
27900
+ ? computed(() => {
27901
+ if (!scopeRoute.matchingSignal.value) {
27902
+ return false;
27903
+ }
27904
+ return nothingMatches();
27905
+ })
27906
+ : computed(nothingMatches),
27907
+ toString: () => {
27908
+ if (scopeRoute) {
27909
+ return `route fallback under ${scopeRoute}`;
27910
+ }
27911
+ return `route fallback`;
27912
+ },
27913
+ };
27914
+ Object.preventExtensions(routeFallback);
27915
+ routeFallbackPrivatePropertiesMap.set(routeFallback, { pagesSignal });
27916
+ return routeFallback;
27917
+ };
27918
+
27919
+ /**
27920
+ * The pages the fallback is the absence of, handed over by the container
27921
+ * holding it. Read as a signal rather than at render: a movement is decided
27922
+ * while rendering is held, before the container has had a chance to choose a
27923
+ * branch (see rendering_hold.js), so "we are on the fallback" has to be
27924
+ * answerable from the route signals alone.
27925
+ */
27926
+ const wireRouteFallback = (routeFallback, pages) => {
27927
+ const { pagesSignal } = routeFallbackPrivatePropertiesMap.get(routeFallback);
27928
+ const pagesCurrent = pagesSignal.peek();
27929
+ if (pagesCurrent && samePages(pagesCurrent, pages)) {
27930
+ return;
27931
+ }
27932
+ pagesSignal.value = pages;
27933
+ };
27934
+
27935
+ const unwireRouteFallback = (routeFallback) => {
27936
+ const { pagesSignal } = routeFallbackPrivatePropertiesMap.get(routeFallback);
27937
+ pagesSignal.value = null;
27938
+ };
27939
+
27940
+ const samePages = (pages, otherPages) => {
27941
+ if (pages.length !== otherPages.length) {
27942
+ return false;
27943
+ }
27944
+ let i = 0;
27945
+ while (i < pages.length) {
27946
+ const page = pages[i];
27947
+ const otherPage = otherPages[i];
27948
+ if (page.route !== otherPage.route) {
27949
+ return false;
27950
+ }
27951
+ if (!compareTwoJsValues(page.params, otherPage.params)) {
27952
+ return false;
27953
+ }
27954
+ i++;
27955
+ }
27956
+ return true;
27957
+ };
27958
+
27580
27959
  /**
27581
27960
  * Route is the single primitive for URL-based rendering.
27582
27961
  *
@@ -27595,10 +27974,15 @@ const Head = ({
27595
27974
  * <Route element={AuthLayout}>
27596
27975
  * <Route route={PROFILE_ROUTE} element={ProfilePage} />
27597
27976
  * <Route route={SETTINGS_ROUTE} element={SettingsPage} />
27598
- * <Route fallback element={AuthNotFoundPage} />
27599
27977
  * </Route>
27600
27978
  * ```
27601
27979
  *
27980
+ * Such a container holds no fallback, and cannot: what makes this one the right
27981
+ * layout is a child matching, and having no prefix is the very reason it was
27982
+ * reached for — so there is no address that belongs to it and to none of its
27983
+ * children. An address nothing claims is the router's to answer; write the
27984
+ * fallback there.
27985
+ *
27602
27986
  * ## Self-contained section pattern
27603
27987
  * Use this when routes share a common URL prefix (e.g. `/dashboard/`).
27604
27988
  * A single leaf Route in the top-level router matches the prefix; the component
@@ -27679,7 +28063,10 @@ const debug$1 = (...args) => {
27679
28063
  * @param {object} props
27680
28064
  * @param {object} [props.route] - the route this branch is for, from `route()`
27681
28065
  * @param {object} [props.routeParams] - selects a branch on a param of that route
27682
- * @param {boolean} [props.fallback] - the branch taken when no sibling matches
28066
+ * @param {boolean|object} [props.fallback] - the branch taken when no sibling
28067
+ * matches. `routeFallback()` gives that page a name, which is what lets a
28068
+ * movement be written about it and its `matchingSignal` be read from outside
28069
+ * the tree (see route_fallback.js).
27683
28070
  * @param {Function|import("ignore:preact").VNode} [props.element] - what the branch renders
27684
28071
  * @param {object} [props.elementProps] - props given to `element`
27685
28072
  *
@@ -27757,7 +28144,7 @@ const collectRoutePages = children => {
27757
28144
  // RouteContainer: traverses children statically per render, finds the active branch,
27758
28145
  // and renders only that branch — or the fallback if nothing matches.
27759
28146
  // No contexts, no state of its own: it reads the route signals directly, and
27760
- // its one effect only tells the outside what it has just done.
28147
+ // its effects only tell the outside what it has just done.
27761
28148
  const RouteContainer = ({
27762
28149
  id,
27763
28150
  element,
@@ -27765,8 +28152,11 @@ const RouteContainer = ({
27765
28152
  children
27766
28153
  }) => {
27767
28154
  const {
27768
- activeBranch
28155
+ activeBranch,
28156
+ fallbackBranch,
28157
+ pages
27769
28158
  } = collectBranches(children);
28159
+ const fallbackPage = fallbackBranch ? fallbackBranch.page : null;
27770
28160
 
27771
28161
  // Told to hold still: what is on screen stays on screen. Kept as the very
27772
28162
  // vnode that was rendered last time, which is how Preact is told there is
@@ -27778,11 +28168,25 @@ const RouteContainer = ({
27778
28168
  }
27779
28169
  const branch = shownBranchRef.current || activeBranch;
27780
28170
 
27781
- // The one effect here, and it says the only thing this component knows that
27782
- // nobody outside can find out: the branch it chose is now in the DOM.
28171
+ // The two things this component knows that nobody outside can find out: the
28172
+ // pages a named fallback is the absence of — the children it was written
28173
+ // with — and that the branch it chose is in the DOM.
27783
28174
  useLayoutEffect(() => {
28175
+ if (fallbackPage) {
28176
+ wireRouteFallback(fallbackPage, pages);
28177
+ }
27784
28178
  publishRouteRender();
27785
28179
  });
28180
+ // Being wired means the container holding it is on screen: a router taken out
28181
+ // of the document leaves no fallback behind still claiming an address.
28182
+ useLayoutEffect(() => {
28183
+ if (!fallbackPage) {
28184
+ return undefined;
28185
+ }
28186
+ return () => {
28187
+ unwireRouteFallback(fallbackPage);
28188
+ };
28189
+ }, [fallbackPage]);
27786
28190
  debug$1(`[container "${id}"] RENDER, active=${branch ? branch.type : "none"}`);
27787
28191
  const content = branch ? branch.node : null;
27788
28192
  if (!content) {
@@ -27796,10 +28200,16 @@ const RouteContainer = ({
27796
28200
  // Walk JSX children vnodes (without rendering) to build a branch list and
27797
28201
  // find the active one in the same pass. Anything that is not a <Route> is read
27798
28202
  // through and kept around the branch it holds (see below).
27799
- // Returns { matchingBranch, fallbackBranch, activeBranch }.
28203
+ // Returns { matchingBranch, fallbackBranch, activeBranch, pages }.
27800
28204
  const collectBranches = children => {
27801
28205
  let matchingBranch = null;
27802
28206
  let fallbackBranch = null;
28207
+ // Every page a branch can be on, at any depth: what a named fallback is the
28208
+ // ABSENCE of (see route_fallback.js). A container with a guard route counts
28209
+ // as a page of its own AND contributes the pages inside it — either its guard
28210
+ // matching or a leaf matching under it is enough to select it, and the
28211
+ // fallback then stands down.
28212
+ const pages = [];
27803
28213
  const visit = child => {
27804
28214
  if (!child || child === true || child === false) {
27805
28215
  return;
@@ -27827,8 +28237,12 @@ const collectBranches = children => {
27827
28237
  }
27828
28238
  const {
27829
28239
  matchingBranch: matchingInside,
27830
- fallbackBranch: fallbackInside
28240
+ fallbackBranch: fallbackInside,
28241
+ pages: pagesInside
27831
28242
  } = collectBranches(wrapperChildren);
28243
+ for (const pageInside of pagesInside) {
28244
+ pages.push(pageInside);
28245
+ }
27832
28246
  if (matchingInside && !matchingBranch) {
27833
28247
  matchingBranch = wrapBranch(matchingInside, child);
27834
28248
  }
@@ -27845,8 +28259,31 @@ const collectBranches = children => {
27845
28259
  } = child.props;
27846
28260
  if (nodeChildren) {
27847
28261
  const {
27848
- matchingBranch: matchingChild
28262
+ matchingBranch: matchingChild,
28263
+ fallbackBranch: fallbackChild,
28264
+ pages: pagesChild
27849
28265
  } = collectBranches(nodeChildren);
28266
+ if (fallbackChild && !route) {
28267
+ // A fallback is the branch taken for an address none of its siblings
28268
+ // claim — so something has to make that address BELONG to this
28269
+ // container, and only a route of its own does. Without one the
28270
+ // container is selected by a child matching, which is the one case
28271
+ // where its fallback is not wanted: the branch could never render, and
28272
+ // an unmatched address is the router's to answer, not a layout's.
28273
+ 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.`);
28274
+ }
28275
+ if (route) {
28276
+ // Params are left out on purpose: a guard selects its container on
28277
+ // matching alone (see guardMatching below), so the fallback stands
28278
+ // down for the whole route, not for one of its cases.
28279
+ pages.push({
28280
+ route,
28281
+ params: undefined
28282
+ });
28283
+ }
28284
+ for (const pageChild of pagesChild) {
28285
+ pages.push(pageChild);
28286
+ }
27850
28287
  const branch = {
27851
28288
  type: "container",
27852
28289
  node: child
@@ -27866,7 +28303,8 @@ const collectBranches = children => {
27866
28303
  if (!fallbackBranch) {
27867
28304
  fallbackBranch = {
27868
28305
  type: "fallback",
27869
- node: child
28306
+ node: child,
28307
+ page: fallback.isRouteFallback ? fallback : null
27870
28308
  };
27871
28309
  }
27872
28310
  } else {
@@ -27874,6 +28312,10 @@ const collectBranches = children => {
27874
28312
  type: "leaf",
27875
28313
  node: child
27876
28314
  };
28315
+ pages.push({
28316
+ route,
28317
+ params: routeParams
28318
+ });
27877
28319
  // every signal is read even once a match is found: reading is what
27878
28320
  // subscribes the container to it, and a branch that is skipped today is
27879
28321
  // the one that must wake the container up tomorrow
@@ -27889,9 +28331,19 @@ const collectBranches = children => {
27889
28331
  return {
27890
28332
  matchingBranch,
27891
28333
  fallbackBranch,
27892
- activeBranch
28334
+ activeBranch,
28335
+ pages
27893
28336
  };
27894
28337
  };
28338
+ const describeContainer = ({
28339
+ props
28340
+ }) => {
28341
+ const {
28342
+ element
28343
+ } = props;
28344
+ const name = typeof element === "function" ? element.name : null;
28345
+ return name ? `<Route element={${name}}>` : `<Route>`;
28346
+ };
27895
28347
  const wrapBranch = (branch, wrapper) => {
27896
28348
  return {
27897
28349
  ...branch,
@@ -29731,7 +30183,7 @@ const beginTransition = ({
29731
30183
  // on, those would freeze the page until the browser gives up. The wait
29732
30184
  // is raced with a short timer instead.
29733
30185
  await Promise.race([renderWait.rendered, waitMs(50)]);
29734
- } else if (pageIsCurrent$1(page)) {
30186
+ } else if (pageIsCurrent(page)) {
29735
30187
  await renderWait.rendered;
29736
30188
  }
29737
30189
  } finally {
@@ -29946,16 +30398,21 @@ const armRouteRenderWait$1 = () => {
29946
30398
  };
29947
30399
  const waitMs = ms => new Promise(resolve => setTimeout(resolve, ms));
29948
30400
 
29949
- // A page written as the route itself, and the page written nowhere: `from`
29950
- // may be left out, which is a relation about arriving at `to` from anywhere.
30401
+ // A page written as the route itself, the page named by the absence of every
30402
+ // route around it (see route_fallback.js), and the page written nowhere:
30403
+ // `from` may be left out, which is a relation about arriving at `to` from
30404
+ // anywhere.
29951
30405
  const normalizePage$1 = page => {
29952
30406
  if (!page) {
29953
30407
  return null;
29954
30408
  }
29955
- return page.isRoute ? {
29956
- route: page,
29957
- params: undefined
29958
- } : page;
30409
+ if (page.isRoute || page.isRouteFallback) {
30410
+ return {
30411
+ route: page,
30412
+ params: undefined
30413
+ };
30414
+ }
30415
+ return page;
29959
30416
  };
29960
30417
 
29961
30418
  // Two pages are the same page when they select the same thing, not when they
@@ -29971,19 +30428,6 @@ const samePage$1 = (a, b) => {
29971
30428
  };
29972
30429
  const pageIndexOf$1 = (pages, page) => pages.findIndex(candidate => samePage$1(candidate, page));
29973
30430
 
29974
- // Whether this page is the one on screen — same reading as route_travel.jsx's
29975
- // own: matchingSignal is the necessary condition and is read whatever happens,
29976
- // params only for a route that matches (the params of a route that does not
29977
- // match are not params).
29978
- const pageIsCurrent$1 = ({
29979
- route,
29980
- params
29981
- }) => {
29982
- if (!route.matchingSignal.value) {
29983
- return false;
29984
- }
29985
- return params ? route.matchesParams(params) : true;
29986
- };
29987
30431
  // The FIRST page that answers, and every page read all the same: a page that
29988
30432
  // is not the current one today is the one that must wake the reader tomorrow.
29989
30433
  //
@@ -29996,7 +30440,7 @@ const pageIsCurrent$1 = ({
29996
30440
  const currentPageIndex$1 = pages => {
29997
30441
  let currentIndex = -1;
29998
30442
  for (let i = 0; i < pages.length; i++) {
29999
- const isCurrent = pageIsCurrent$1(pages[i]);
30443
+ const isCurrent = pageIsCurrent(pages[i]);
30000
30444
  if (!isCurrent) {
30001
30445
  continue;
30002
30446
  }
@@ -31349,28 +31793,6 @@ const samePage = (a, b) => {
31349
31793
  };
31350
31794
  const pageIndexOf = (pages, page) => pages.findIndex(candidate => samePage(candidate, page));
31351
31795
 
31352
- // Whether this page is the one on screen. `matchesParams` reads paramsSignal,
31353
- // so a caller reading this during a render is subscribed to the param changes
31354
- // that walk from one tab to the next — matchingSignal alone never moves there,
31355
- // and a row whose tabs are params of one route would never re-render.
31356
- //
31357
- // The params are read only for a route that matches, and that is not a signal
31358
- // left unread: a reader wakes on anything it read last time, so what matters is
31359
- // that everything able to make this answer change is among them.
31360
- // matchingSignal is read whatever happens, and it is a NECESSARY condition —
31361
- // while it is false no param of that route can put this page on screen, and the
31362
- // day one could, matchingSignal itself has to turn true to say so, which is the
31363
- // read that brings the params back in. (Asking anyway would be worse than
31364
- // useless: the params of a route that does not match are not params.)
31365
- const pageIsCurrent = ({
31366
- route,
31367
- params
31368
- }) => {
31369
- if (!route.matchingSignal.value) {
31370
- return false;
31371
- }
31372
- return params ? route.matchesParams(params) : true;
31373
- };
31374
31796
  // The FIRST page that answers, as with the branches of a <Route>: several
31375
31797
  // routes match at once — a literal one and the parameterized one it is a case of
31376
31798
  // ("/games/new" is also a "/games/:gameId"), a section and the page inside it —
@@ -34286,6 +34708,17 @@ const useUIStateController = (
34286
34708
  `merging button state into parent control group:`,
34287
34709
  mergedState,
34288
34710
  );
34711
+ // The sender's value is part of the answer, and whoever mirrors
34712
+ // this group — a picker, through its façade — reads the group's
34713
+ // state, not its children's: told the way a mount sync is
34714
+ // (silently, nobody has answered yet), so the send that follows
34715
+ // finds the value there. Not through the group's own onChange:
34716
+ // that re-aggregates from the children, and a button is not one.
34717
+ parentController.parentUIStateController?.onChildUIAction?.(
34718
+ parentController,
34719
+ e,
34720
+ { stateChanged: true, silent: true },
34721
+ );
34289
34722
  }
34290
34723
  }
34291
34724
  // Trigger uiAction/command side effects without changing UI state.
@@ -35950,6 +36383,10 @@ const useUIGroupStateController = (
35950
36383
  const { controller } = s;
35951
36384
  const prevDefaultValue = controller.defaultValue;
35952
36385
  controller.props = props;
36386
+ // Published like a leaf's (see the leaf update above): a child's named
36387
+ // button reaches the group's owner through it (see the button branch of
36388
+ // onUIAction).
36389
+ controller.parentUIStateController = parentUIStateController;
35953
36390
  controller.ref = ref;
35954
36391
  controller.id = id;
35955
36392
  controller.name = name;
@@ -42168,6 +42605,17 @@ Object.assign(PSEUDO_CLASSES, {
42168
42605
  * way there changes. What a row of tabs wants — the neighbour is a lateral
42169
42606
  * move, not a step deeper, so the whole row weighs one entry and the back
42170
42607
  * button leaves by where the reader came in.
42608
+ * @param {boolean} [props.document] - The address is ANOTHER DOCUMENT of this
42609
+ * origin — an admin panel, a status page, anything built as its own entry
42610
+ * point — so the press is left to the browser and loads that page. Without
42611
+ * it navi routes every same-origin address it can intercept, and one no
42612
+ * route matches lands on the app's not-found screen. Said rather than
42613
+ * guessed: "no route matches" is also what a typo in an in-app url looks
42614
+ * like, and that one must show the not-found screen instead of silently
42615
+ * reloading the app. The element stays a real link — an address in the
42616
+ * status bar, a middle click, "open in new tab" — which is what navigating
42617
+ * from an `action` costs; `target="_blank"` is a different thing, a new tab.
42618
+ * Nothing is prefetched on the way there: the routes do not lead there.
42171
42619
  * @param {boolean} [props.prefetch=true] Fetch the code of where this leads when the
42172
42620
  * pointer or the focus arrives, ahead of the press (see
42173
42621
  * docs/dynamic_import.md): the route actions that ask nothing of the
@@ -42279,13 +42727,17 @@ const LinkPlain = props => {
42279
42727
  routeTransition,
42280
42728
  pressableDuringRouteTransition,
42281
42729
  replace,
42730
+ document: isDocument,
42282
42731
  prefetch = true,
42283
42732
  children
42284
42733
  } = props;
42285
42734
  if (anchor && !props.id) {
42286
42735
  props.id = href.slice(1);
42287
42736
  }
42288
- usePreloadOnIntent(props.ref, href, prefetch);
42737
+ // A document link leads out of the routes, so there is nothing there to
42738
+ // prefetch — and the address would otherwise be preloaded against the very
42739
+ // routes it is escaping.
42740
+ usePreloadOnIntent(props.ref, href, isDocument ? false : prefetch);
42289
42741
  const selectionContext = useContext(SelectionContext);
42290
42742
  const nav = useContext(NavContext);
42291
42743
  const visited = useIsVisited(href);
@@ -42400,6 +42852,12 @@ const LinkPlain = props => {
42400
42852
  const replaceRequest = replace ? {
42401
42853
  [LINK_REPLACE_ATTRIBUTE]: ""
42402
42854
  } : null;
42855
+
42856
+ // That this address is another document is the link's to say, worn where the
42857
+ // interception looks for it (see link_document.js).
42858
+ const documentRequest = isDocument ? {
42859
+ [LINK_DOCUMENT_ATTRIBUTE]: ""
42860
+ } : null;
42403
42861
  const innerChildren = children || (hrefFallback ? href : children);
42404
42862
  const startIconEl = startIcon;
42405
42863
  const endIconEl = innerEndIcon;
@@ -42453,12 +42911,14 @@ const LinkPlain = props => {
42453
42911
  routeTransition: undefined,
42454
42912
  pressableDuringRouteTransition: undefined,
42455
42913
  replace: undefined,
42914
+ document: undefined,
42456
42915
  prefetch: undefined,
42457
42916
  "data-navi-route-transition-request": routeTransitionRequest,
42458
42917
  ...(pressableDuringRouteTransition ? {
42459
42918
  [PRESSABLE_ATTRIBUTE]: ""
42460
42919
  } : null),
42461
42920
  ...replaceRequest,
42921
+ ...documentRequest,
42462
42922
  // The control's own handlers first — the interaction gate, the caller's
42463
42923
  // onClick/onKeyDown, the command and the action — then what only a link
42464
42924
  // does. Written over the spread above, so they have to be called here.
@@ -48352,6 +48812,7 @@ const ButtonUI = props => {
48352
48812
  target,
48353
48813
  rel,
48354
48814
  replace,
48815
+ document: isDocument,
48355
48816
  pressableDuringRouteTransition,
48356
48817
  prefetch = true,
48357
48818
  // visual
@@ -48406,13 +48867,21 @@ const ButtonUI = props => {
48406
48867
  [LINK_REPLACE_ATTRIBUTE]: ""
48407
48868
  } : null;
48408
48869
 
48870
+ // And this one says the address is another document of this origin, so the
48871
+ // press is the browser's (see link_document.js).
48872
+ const documentRequest = isDocument ? {
48873
+ [LINK_DOCUMENT_ATTRIBUTE]: ""
48874
+ } : null;
48875
+
48409
48876
  // Worn as an attribute too, and read at the document by whoever catches the
48410
48877
  // press a movement would have swallowed (see transition_press.js).
48411
48878
  const pressableRequest = pressableDuringRouteTransition ? {
48412
48879
  [PRESSABLE_ATTRIBUTE]: ""
48413
48880
  } : null;
48414
48881
  const visualSelector = ".navi_button_content";
48415
- usePreloadOnIntent(ref, href, prefetch);
48882
+ // Nothing to prefetch on the way to another document: the routes do not
48883
+ // lead there.
48884
+ usePreloadOnIntent(ref, href, isDocument ? false : prefetch);
48416
48885
  useAccentColorAttributes(ref, null, {
48417
48886
  elementSelector: visualSelector
48418
48887
  });
@@ -48440,6 +48909,8 @@ const ButtonUI = props => {
48440
48909
  rel: innerRel,
48441
48910
  replace: undefined,
48442
48911
  ...replaceRequest,
48912
+ document: undefined,
48913
+ ...documentRequest,
48443
48914
  pressableDuringRouteTransition: undefined,
48444
48915
  ...pressableRequest,
48445
48916
  prefetch: undefined
@@ -48886,6 +49357,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48886
49357
  * selfInteractions?: string,
48887
49358
  * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
48888
49359
  * replace?: boolean,
49360
+ * document?: boolean,
48889
49361
  * actionStandalone?: boolean,
48890
49362
  * actionAbortable?: boolean,
48891
49363
  * optimistic?: boolean,
@@ -48895,6 +49367,13 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48895
49367
  * `--navi-nav-to` command — by TAKING THE PLACE of the current history entry
48896
49368
  * rather than stacking on it: what `<Link replace>` says, for a press drawn
48897
49369
  * as a button.
49370
+ * @param {boolean} [document] The `href` is ANOTHER DOCUMENT of this origin —
49371
+ * an admin panel, a status page, anything built as its own entry point — so
49372
+ * the press is left to the browser and loads that page, instead of being
49373
+ * routed to the app's not-found screen. What `<Link document>` says, for a
49374
+ * press drawn as a button; and the reason to say it rather than navigate
49375
+ * from an `action` is that the button stays a real link (an address, a
49376
+ * middle click, "open in new tab"). Nothing is prefetched on the way there.
48898
49377
  * @param {boolean} [prefetch=true] Fetch the code of where this leads when the
48899
49378
  * pointer or the focus arrives, ahead of the press (see
48900
49379
  * docs/dynamic_import.md): the route actions that ask nothing of the
@@ -58886,10 +59365,10 @@ const css$E = /* css */`
58886
59365
  var(--dialog-maxmax-height)
58887
59366
  );
58888
59367
 
58889
- /* Base default: also the custom renderer's own permanent value — its
59368
+ /* Base default: also a local dialog's own permanent value — its
58890
59369
  containing block is genuinely its nearest positioned ancestor,
58891
59370
  regardless of positionArea. See the [data-layer="top"] rule below for
58892
- why the via-attribute renderer overrides this. Position is always
59371
+ why a top-layer one overrides this. Position is always
58893
59372
  JS-driven (pickPositionRelativeTo, see useDialogProps below) — no CSS
58894
59373
  alignment/inset math here at all. */
58895
59374
  position: absolute;
@@ -58972,13 +59451,8 @@ const css$E = /* css */`
58972
59451
  var(--container-position-remaining-width, var(--dialog-maxmax-width)),
58973
59452
  var(--dialog-maxmax-width)
58974
59453
  );
58975
- /* The sheet rests on the screen's bottom edge, which on a phone is the
58976
- home indicator and, since iOS 26, Safari's own floating bar — a band
58977
- the browser does not paint fixed content into. The surface still
58978
- reaches the edge (the sheet comes out from behind the bar); what it
58979
- holds stops above it. */
58980
- padding-bottom: env(safe-area-inset-bottom, 0px);
58981
59454
  }
59455
+
58982
59456
  /* The clamped max, not --dialog-maxmax-*: that one is the viewport minus
58983
59457
  the spacing, which is only the real ceiling for layer="top". A local
58984
59458
  dialog is confined to its positioned ancestor, whose size reaches here
@@ -59006,6 +59480,35 @@ const css$E = /* css */`
59006
59480
  border-bottom-left-radius: 0;
59007
59481
  }
59008
59482
 
59483
+ /* An edge the dialog sits flush against is an edge of the screen, and the
59484
+ device keeps a band there it paints nothing into: the notch, the home
59485
+ indicator, Safari's own floating bar since iOS 26. The surface still
59486
+ reaches the edge — a sheet comes out from behind the bar rather than
59487
+ stopping short of it — and what it holds stops at the band.
59488
+
59489
+ env(), not --navi-safe-area-inset-*: that one is the app's own free
59490
+ region and counts its fixed bars, which this dialog is in front of. The
59491
+ device's inset is the only thing left to keep.
59492
+
59493
+ layer="top" only. A local dialog is flush with the box it was declared
59494
+ in, and whether that box reaches the screen's edge is something only the
59495
+ app knows — it gives that band back on its own container (see
59496
+ safe_area.js's [data-navi-safe-area]). */
59497
+ &[data-layer="top"] {
59498
+ &[data-flush-top] {
59499
+ padding-top: env(safe-area-inset-top, 0px);
59500
+ }
59501
+ &[data-flush-right] {
59502
+ padding-right: env(safe-area-inset-right, 0px);
59503
+ }
59504
+ &[data-flush-bottom] {
59505
+ padding-bottom: env(safe-area-inset-bottom, 0px);
59506
+ }
59507
+ &[data-flush-left] {
59508
+ padding-left: env(safe-area-inset-left, 0px);
59509
+ }
59510
+ }
59511
+
59009
59512
  /* The placement is a translate, so the translate property is spoken for
59010
59513
  here (see applyNewPosition in visible_rect.js, which owns it and animates
59011
59514
  it itself through the Web Animations API rather than through this file's
@@ -59018,8 +59521,8 @@ const css$E = /* css */`
59018
59521
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
59019
59522
  }
59020
59523
  /* backdropVariant, after the rules it overrides: same specificity, so
59021
- order is what decides. showModal() still makes the page inert either
59022
- way — only the paint goes away. */
59524
+ order is what decides. The wall is still there either way — only the
59525
+ paint goes away. */
59023
59526
  &[data-backdrop-variant="discrete"] {
59024
59527
  --backdrop-background: var(--navi-backdrop-discrete-background);
59025
59528
  --backdrop-filter: var(--navi-backdrop-discrete-backdrop-filter);
@@ -59083,7 +59586,10 @@ const css$E = /* css */`
59083
59586
  --navi-focus-outline-style: none;
59084
59587
  }
59085
59588
 
59086
- &[open] {
59589
+ /* :popover-open too — a top-layer dialog with no wall is shown with
59590
+ showPopover(), which never sets the [open] attribute. */
59591
+ &[open],
59592
+ &:popover-open {
59087
59593
  display: flex;
59088
59594
  }
59089
59595
 
@@ -59120,14 +59626,12 @@ const css$E = /* css */`
59120
59626
  }
59121
59627
  }
59122
59628
 
59123
- /* Custom renderer only — .show()'d dialogs get no ::backdrop, so this is
59629
+ /* layer="local" only — a .show()'d dialog gets no ::backdrop, so its wall is
59124
59630
  a real sibling element instead, same idea/CSS shape as Popover's own
59125
- .navi_popover_backdrop (see popover.jsx's top comment for the design
59126
- this mirrors). Always rendered (never skipped like Popover's own
59127
- "none" case): a dialog is always modal, so there's always at least a
59128
- click-absorbing backdrop, matching what showModal() already gives the
59129
- via-attribute renderer for free regardless of
59130
- pointerInteractionOutsideEffect. */
59631
+ .navi_popover_backdrop (see popover.jsx's top comment for the design this
59632
+ mirrors). Rendered whenever backdrop={true}, whatever
59633
+ pointerInteractionOutsideEffect says: a wall that closes nothing still
59634
+ absorbs the press, matching what showModal() gives a modal for free. */
59131
59635
  .navi_dialog_backdrop {
59132
59636
  --popup-animation-duration: 0.18s;
59133
59637
 
@@ -59142,7 +59646,7 @@ const css$E = /* css */`
59142
59646
  in openEffect) gets pointer-events: none mid-transition. */
59143
59647
  pointer-events: auto;
59144
59648
 
59145
- /* Painted through the same two variables as the via-attribute renderer's
59649
+ /* Painted through the same two variables as a modal's own
59146
59650
  ::backdrop (see them for what each rule is for) — here they resolve on
59147
59651
  the element that paints, which is also the element the
59148
59652
  backdropColor/backdropFilter props are set on. Declared unconditionally
@@ -59234,12 +59738,15 @@ const css$E = /* css */`
59234
59738
  * grammar, anchor's sizing-only role, backdrop mechanics).
59235
59739
  *
59236
59740
  * @param {object} props
59237
- * @param {"top"|"local"} [props.layer="top"] - `"top"`: `showModal()`'d
59238
- * into the browser's own top layer (native focus trap, `Escape`-to-cancel,
59239
- * hardware back-button dismissal, rest-of-document made inert). `"local"`:
59240
- * shown via the non-modal `.show()` instead, staying in normal document
59741
+ * @param {"top"|"local"} [props.layer="top"] - What the dialog is placed
59742
+ * against. `"top"`: the browser's own top layer, placed against the screen,
59743
+ * never clipped by anything the app declared. `"local"`: normal document
59241
59744
  * flow inside its own positioned ancestor — confined to (and clipped by)
59242
- * that container instead of the whole viewport.
59745
+ * that container instead of the whole viewport. Whether the top-layer one
59746
+ * is modal is `backdrop`'s answer, not this one's: with a backdrop it is
59747
+ * `showModal()`'d (native focus trap, `Escape`-to-cancel, hardware
59748
+ * back-button dismissal, rest-of-document made inert), without one it is
59749
+ * shown through the Popover API and the page behind stays live.
59243
59750
  * @param {boolean} [props.dockedOnSmallTouchScreen] - Turns the dialog into a
59244
59751
  * bottom sheet (docked flush to the bottom edge, full width) on a small touch
59245
59752
  * screen, and leaves it alone otherwise. For a dialog meant to be interacted
@@ -59302,9 +59809,12 @@ const css$E = /* css */`
59302
59809
  * outside press does or how the backdrop is painted. `false` leaves the page
59303
59810
  * reachable: a press outside closes the dialog (per
59304
59811
  * `pointerInteractionOutsideEffect`) *and* is answered by whatever it landed
59305
- * on, in the same gesture. **`layer="local"` only** — `showModal()` makes
59306
- * everything behind genuinely inert, so a top-layer dialog has no way to let
59307
- * a press through and warns instead. See docs/popup_backdrop.md.
59812
+ * on, in the same gesture; focus is free to leave too, the page behind being
59813
+ * meant to be reached. Works in either layer — a top-layer dialog with no
59814
+ * wall is shown through the Popover API rather than `showModal()`, which is
59815
+ * what a sheet docked to the screen's bottom edge over a still-readable map
59816
+ * needs. What it gives up is what only a modal gets natively: the hardware
59817
+ * back button no longer dismisses it. See docs/popup_backdrop.md.
59308
59818
  * @param {"close"|"cancel"|"capture"|"none"} [props.pointerInteractionOutsideEffect="close"]
59309
59819
  * - `"close"` closes the dialog on an outside click. `"capture"`/`"none"`
59310
59820
  * both just absorb the click without closing (visually dimmed backdrop vs.
@@ -59564,7 +60074,7 @@ const UncontrolledDialog = props => {
59564
60074
  // Picks which rendering strategy actually mounts, from `layer` alone — see
59565
60075
  // this file's top comment. Done after the controlled/uncontrolled split
59566
60076
  // above, so an openController is always already resolved by the time
59567
- // DialogAsModal/DialogLocal (and the useDialogProps hook they share) ever
60077
+ // DialogInTopLayer/DialogLocal (and the useDialogProps hook they share) ever
59568
60078
  // run.
59569
60079
  const ControlledDialog = props => {
59570
60080
  if (props.layer === "local") {
@@ -59572,11 +60082,11 @@ const ControlledDialog = props => {
59572
60082
  ...props
59573
60083
  });
59574
60084
  }
59575
- return jsx(DialogAsModal, {
60085
+ return jsx(DialogInTopLayer, {
59576
60086
  ...props
59577
60087
  });
59578
60088
  };
59579
- const DialogAsModal = props => {
60089
+ const DialogInTopLayer = props => {
59580
60090
  const [backdropProps, contentProps] = useDialogProps(props);
59581
60091
  return jsxs(Fragment, {
59582
60092
  children: [backdropProps && jsx(Box, {
@@ -59608,12 +60118,14 @@ const DialogLocal = props => {
59608
60118
  /**
59609
60119
  * Everything both rendering strategies share once an `openController` is
59610
60120
  * already resolved: focus/debug/id plumbing, the open-commit sequence, the
59611
- * close handler — inlined in `openEffect`, branching on `isModal` at each
60121
+ * close handler — inlined in `openEffect`, branching on how the dialog is
60122
+ * shown at each
59612
60123
  * point the two renderers genuinely differ (same pattern as popover.jsx's
59613
60124
  * own usePopoverProps — see its top comment for why this stays inline
59614
60125
  * rather than split into two functions). Returns `[backdropProps,
59615
- * contentProps]` — `backdropProps` is `null` for the via-attribute renderer
59616
- * (its own backdrop is native, not a real element).
60126
+ * contentProps]` — `backdropProps` is `null` unless there is a wall of our
60127
+ * own to render: a modal's is the native `::backdrop`, and `backdrop={false}`
60128
+ * has none at all.
59617
60129
  */
59618
60130
  // What a dialog turns into on a small touch screen. "bottom" is not a taste:
59619
60131
  // it puts the dialog in the zone a phone is actually operated from — where the
@@ -59645,10 +60157,10 @@ const useDialogProps = props => {
59645
60157
  props.ref = props.ref || defaultRef;
59646
60158
  const {
59647
60159
  openController,
59648
- // "top" (default) → real <dialog>, showModal(), the browser's own top
59649
- // layer. "local" → also a real <dialog>, but shown via the non-modal
59650
- // .show() instead, staying in normal document flow, position: absolute
59651
- // relative to its own positioned ancestor. See this file's top comment.
60160
+ // "top" (default) → the browser's own top layer, placed against the
60161
+ // screen. "local" → normal document flow, position: absolute relative to
60162
+ // its own positioned ancestor. Which show call each takes also depends on
60163
+ // `backdrop` — see this file's top comment.
59652
60164
  layer = "top",
59653
60165
  dockedOnSmallTouchScreen,
59654
60166
  // Same grammar as Popover's own positionArea — see this file's top
@@ -59662,17 +60174,16 @@ const useDialogProps = props => {
59662
60174
  expandX: expandXProp,
59663
60175
  expandY: expandYProp,
59664
60176
  // "close" (default) closes on an outside click. "capture"/"none" both
59665
- // just absorb it without closing — for the via-attribute renderer,
59666
- // showModal() already makes the rest of the page inert, so there's
59667
- // nothing for a click to reach either way; for the custom renderer,
59668
- // there's no native inert-ing, so the real backdrop below is what
59669
- // actually makes "capture"/"none" behave the same way here too.
60177
+ // just absorb it without closing — for a modal, showModal() already makes
60178
+ // the rest of the page inert, so there's nothing for a click to reach
60179
+ // either way; otherwise there's no native inert-ing, so the real backdrop
60180
+ // below is what makes "capture"/"none" behave the same way here too.
59670
60181
  pointerInteractionOutsideEffect = "close",
59671
60182
  // Whether there is a wall between the dialog and the page at all, asked
59672
60183
  // before what a press on it does (pointerInteractionOutsideEffect) and
59673
- // before how it is painted (backdropVariant below). layer="top" cannot
59674
- // honour false: showModal() makes the page inert before anything here
59675
- // runs.
60184
+ // before how it is painted (backdropVariant below). It also picks the show
60185
+ // call for layer="top": a wall is what showModal() is for, and without one
60186
+ // the dialog goes to the same top layer through the Popover API instead.
59676
60187
  backdrop = true,
59677
60188
  // How loudly the backdrop says it is there — independent of what it
59678
60189
  // *does* (that's pointerInteractionOutsideEffect above). "invisible" is a
@@ -59729,10 +60240,11 @@ const useDialogProps = props => {
59729
60240
  anchor
59730
60241
  });
59731
60242
  const children = contentMounted ? childrenProp : null;
59732
- const isModal = layer === "top";
59733
- if (isModal && !backdrop) {
59734
- 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.`);
59735
- }
60243
+ // Where it is placed, and whether there is a wall — the two questions that
60244
+ // pick the show call (see this file's top comment).
60245
+ const isTopLayer = layer === "top";
60246
+ const isModal = isTopLayer && backdrop;
60247
+ const isTopLayerPopover = isTopLayer && !backdrop;
59736
60248
  if (!backdrop && pointerInteractionOutsideEffect === "capture") {
59737
60249
  console.warn(`Dialog: pointerInteractionOutsideEffect="capture" needs a backdrop. Absorbing a press is what a wall does, and backdrop={false} takes it away.`);
59738
60250
  }
@@ -59753,7 +60265,7 @@ const useDialogProps = props => {
59753
60265
  // container being that screen (the viewport, unless the app declared a
59754
60266
  // narrower one) — and the positioned ancestor for a local one, where
59755
60267
  // reading 3% of the screen gives an absurd gap inside a small box.
59756
- isModal ? "3appw" : "3cqw");
60268
+ isTopLayer ? "3appw" : "3cqw");
59757
60269
  // "expand || expandX", the shorthand semantics Popup used to apply before
59758
60270
  // handing them over — the docked default only applies when neither was said
59759
60271
  const expandXUnset = expand === undefined && expandXProp === undefined;
@@ -59951,14 +60463,14 @@ const useDialogProps = props => {
59951
60463
  // from dialogEl.parentElement, which for DialogLocal is the
59952
60464
  // .navi_dialog_clip_wrapper (itself position: absolute) rather than the
59953
60465
  // real, meaningful ancestor beyond it.
59954
- const positionedAncestor = isModal ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
60466
+ const positionedAncestor = isTopLayer ? document.documentElement : getPositionedParent(dialogEl.parentElement /* dialogEl is inside the clip_wrapper */);
59955
60467
  const [cleanup, addCleanup] = createPubSub(true);
59956
60468
  const anchorElement = resolveAnchorElement(e);
59957
60469
  // Kept for the closing, which has no opening event to read it back from:
59958
60470
  // the box the dialog shrinks back into is the one it came out of.
59959
60471
  anchorElementRef.current = anchorElement;
59960
60472
  debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
59961
- if (!isModal) {
60473
+ if (!isTopLayer) {
59962
60474
  // see openLocalDialogCount's own comment
59963
60475
  dialogEl.style.setProperty("--dialog-stack-order", openLocalDialogCount++);
59964
60476
  }
@@ -60001,10 +60513,12 @@ const useDialogProps = props => {
60001
60513
  }
60002
60514
  if (isModal) {
60003
60515
  dialogEl.showModal();
60516
+ } else if (isTopLayerPopover) {
60517
+ dialogEl.showPopover();
60004
60518
  } else {
60005
60519
  dialogEl.show();
60006
60520
  }
60007
- // Regardless of isModal — see the backdrop's own [navi-hidden] CSS rule
60521
+ // Whichever show call ran — see the backdrop's own [navi-hidden] CSS rule
60008
60522
  // and popover.jsx's identical reasoning: showModal()/show() alone only
60009
60523
  // wins over a stray, still-present [navi-hidden] { display: none }
60010
60524
  // default when nothing else authored also sets display on dialogEl —
@@ -60012,24 +60526,23 @@ const useDialogProps = props => {
60012
60526
  // property (e.g. Popup's own flex prop) defeats the UA stylesheet's own
60013
60527
  // dialog:not([open]) default the same way it can for Popover.
60014
60528
  dialogEl.removeAttribute("navi-hidden");
60015
- if (isModal) ; else {
60529
+ if (isModal) ; else if (backdrop) {
60016
60530
  addCleanup(trapFocusInside(dialogEl, {
60017
60531
  debug: debugFocus,
60018
60532
  boundaryElement: positionedAncestor,
60019
- // A dialog is always modal (see this file's top comment) — a
60020
- // mousedown on some other focusable element inside the same
60021
- // container (but outside the dialog) must not steal focus away
60022
- // from it either, not just a Tab press.
60533
+ // The wall stops the keyboard too — a mousedown on some other
60534
+ // focusable element behind it must not steal the focus away either,
60535
+ // not just a Tab press.
60023
60536
  pointerTrap: true
60024
60537
  }));
60025
60538
  }
60026
60539
  if (scrollCapture) {
60027
- // A modal dialog always has its own ::backdrop; the custom renderer has
60028
- // the backdrop element when it renders one.
60540
+ // A modal dialog always has its own ::backdrop; a local one has the
60541
+ // backdrop element when it renders one.
60029
60542
  addCleanup(trapScrollInside(dialogEl, {
60030
60543
  backdrop: isModal || backdropEl
60031
60544
  }));
60032
- } else if (!isModal) {
60545
+ } else if (!isTopLayer) {
60033
60546
  // A local dialog is confined to its positioned ancestor, and so is its
60034
60547
  // backdrop (inset: 0 covers the scrollport, not the scrolled content):
60035
60548
  // letting that ancestor scroll would slide the dialog away and reveal
@@ -60046,7 +60559,7 @@ const useDialogProps = props => {
60046
60559
  // Positioning: dialogEl is already shown (display: flex, per this
60047
60560
  // file's own [open] CSS) by this point, so its own dimensions are real
60048
60561
  // — pickPositionRelativeTo's own no-anchor/docked mode (no `anchor`
60049
- // argument at all) docks it against the viewport (layer="top"/isModal)
60562
+ // argument at all) docks it against the viewport (layer="top")
60050
60563
  // or its own positioned ancestor (layer="local", the same
60051
60564
  // positionedAncestor computed above), same mechanism as Popover's own
60052
60565
  // custom renderer. applyDialogPosition sets --container-position-remaining-height/-width
@@ -60078,7 +60591,7 @@ const useDialogProps = props => {
60078
60591
  // box is what the caps must read, and nothing in CSS tracks it.
60079
60592
  const applyDialogPosition = position => {
60080
60593
  applyNewPosition(dialogEl, position);
60081
- if (isModal) {
60594
+ if (isTopLayer) {
60082
60595
  dialogEl.style.removeProperty("--container-position-remaining-height");
60083
60596
  dialogEl.style.removeProperty("--container-position-remaining-width");
60084
60597
  }
@@ -60177,7 +60690,7 @@ const useDialogProps = props => {
60177
60690
 
60178
60691
  // Reposition on the same triggers Popover's own visibleRectEffect
60179
60692
  // already reacts to generically — window resize/scroll/visual-viewport
60180
- // changes for layer="top"/isModal (positionedAncestor is already
60693
+ // changes for layer="top" (positionedAncestor is already
60181
60694
  // document.documentElement there, see its own computation above;
60182
60695
  // visibleRectEffect already debounces visualViewport resize by 100ms
60183
60696
  // to avoid the mobile tap-to-tap-input keyboard flicker, so no
@@ -60275,15 +60788,19 @@ const useDialogProps = props => {
60275
60788
  debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
60276
60789
  clearTextSelectionInside(dialogEl);
60277
60790
  dialogEl.setAttribute("aria-expanded", "false");
60278
- if (!isModal) {
60791
+ if (!isTopLayer) {
60279
60792
  openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
60280
60793
  dialogEl.style.removeProperty("--dialog-stack-order");
60281
60794
  }
60282
60795
  // See openEffect's own identical comment for why this is needed
60283
- // regardless of isModal, not just when a stray authored display
60796
+ // whichever show call ran, not just when a stray authored display
60284
60797
  // property is actually present — harmless the rest of the time.
60285
60798
  dialogEl.setAttribute("navi-hidden", "");
60286
- dialogEl.close();
60799
+ if (isTopLayerPopover) {
60800
+ dialogEl.hidePopover();
60801
+ } else {
60802
+ dialogEl.close();
60803
+ }
60287
60804
  // Held at the size it has right now, for the whole way out. cleanup()
60288
60805
  // below already stops the JS repositioning, but the size is CSS-driven
60289
60806
  // (--x-dialog-max-height, and `height` outright under expandY) and
@@ -60310,9 +60827,8 @@ const useDialogProps = props => {
60310
60827
  };
60311
60828
  const onKeyDownShortcuts = createOnKeyDownForShortcuts({
60312
60829
  escape: e => {
60313
- // Only the custom renderer needs this — a modal <dialog> already
60314
- // fires "cancel" (handled via onCancel below) on Escape natively; a
60315
- // non-modal .show()'d one doesn't.
60830
+ // Only a modal <dialog> fires "cancel" on Escape natively (handled via
60831
+ // onCancel below); neither a .show()n nor a showPopover()n one does.
60316
60832
  if (isModal || !openController.opened) {
60317
60833
  return null;
60318
60834
  }
@@ -60331,10 +60847,10 @@ const useDialogProps = props => {
60331
60847
  // most fields are shared: renderer-specific bits (the outside-click
60332
60848
  // handler below, in particular) are just assigned onto whichever of the
60333
60849
  // two actually owns that concern for a given renderer, instead of one
60334
- // object's own field branching internally on isModal. backdropProps only
60335
- // gets returned (see the bottom of this function) when !isModal — the
60336
- // via-attribute renderer's own backdrop is native (::backdrop), not a
60337
- // real element we render ourselves.
60850
+ // object's own field branching internally. backdropProps only gets returned
60851
+ // (see the bottom of this function) for the one dialog whose wall is a real
60852
+ // element of ours: a modal's is native (::backdrop), and backdrop={false}
60853
+ // has none at all.
60338
60854
  Object.assign(backdropProps, {
60339
60855
  "ref": backdropRef,
60340
60856
  // Out of flow like the popup it belongs to — see the content element's own
@@ -60384,21 +60900,21 @@ const useDialogProps = props => {
60384
60900
  // after measuring (see this file's top comment) — so there's no reason
60385
60901
  // to withhold the attribute for the auto case the way Popover has to.
60386
60902
  "navi-animation": resolvedAnimation,
60387
- // Only meaningful for the via-attribute renderer's own native
60388
- // ::backdrop (see this file's CSS for the "capture" glass effect) — a
60903
+ // Only meaningful for a modal's own native ::backdrop (see this file's
60904
+ // CSS for the "capture" glass effect) — a
60389
60905
  // pseudo-element can't carry its own attributes, so this has to live on
60390
60906
  // the originating .navi_dialog element instead, same reasoning as
60391
- // navi-animation above. Harmless for the custom renderer too (its own
60392
- // real backdrop element already gets the same attribute via
60393
- // backdropProps above, which is what its own CSS actually keys off).
60907
+ // navi-animation above. Harmless for a local dialog too (its own real
60908
+ // backdrop element already gets the same attribute via backdropProps
60909
+ // above, which is what its own CSS actually keys off).
60394
60910
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
60395
- // Only load-bearing for the via-attribute renderer's own native
60396
- // ::backdrop, same "a pseudo-element can't carry attributes" reasoning
60397
- // as the prop just above (and harmless for the custom renderer, whose
60398
- // real backdrop element gets it via backdropProps).
60911
+ // Only load-bearing for a modal's own native ::backdrop, same "a
60912
+ // pseudo-element can't carry attributes" reasoning as the prop just above
60913
+ // (and harmless for a local dialog, whose real backdrop element gets it
60914
+ // via backdropProps).
60399
60915
  "data-backdrop-variant": backdropVariant,
60400
60916
  // Read by the native ::backdrop, which inherits them from here (see this
60401
- // file's CSS) — the custom renderer's own backdrop element gets them via
60917
+ // file's CSS) — a local dialog's own backdrop element gets them via
60402
60918
  // backdropProps above.
60403
60919
  backdropColor,
60404
60920
  backdropFilter,
@@ -60427,6 +60943,12 @@ const useDialogProps = props => {
60427
60943
  // absolutely placed by its own code — so it says so once, here (group.jsx
60428
60944
  // reads this attribute to tell a member from anything else on its line).
60429
60945
  "navi-out-of-flow": "",
60946
+ // The top layer without a wall: showPopover() is what puts it there, and
60947
+ // it only accepts an element that declares itself a popover. "manual" so
60948
+ // the browser's own light dismiss stays out of it — what an outside press
60949
+ // does is pointerInteractionOutsideEffect's answer, given by the
60950
+ // document-level listener in openEffect above.
60951
+ "popover": isTopLayerPopover ? "manual" : undefined,
60430
60952
  "baseClassName": "navi_dialog",
60431
60953
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
60432
60954
  // Distinguishes the two renderers for the CSS above (position: fixed
@@ -60476,8 +60998,8 @@ const useDialogProps = props => {
60476
60998
  },
60477
60999
  "onCancel": e => {
60478
61000
  // The dialog's own "cancel" (Escape on a modal showModal() dialog —
60479
- // the custom renderer's Escape handling lives in onKeyDownShortcuts
60480
- // above) fires on the dialog element itself. But a child
61001
+ // the Escape handling for the other two shows lives in
61002
+ // onKeyDownShortcuts above) fires on the dialog element itself. But a child
60481
61003
  // <input type="file"> also fires a BUBBLING "cancel" when the user
60482
61004
  // dismisses the file chooser (per the HTML spec), and it lands here
60483
61005
  // too; only the dialog's own cancel means "close".
@@ -60491,12 +61013,12 @@ const useDialogProps = props => {
60491
61013
  children
60492
61014
  });
60493
61015
 
60494
- // Outside-click handling for layer="local" only — the via-attribute
60495
- // renderer's own is a plain document-level listener instead, set up in
60496
- // openEffect above (see this file's top comment for why: neither a real
60497
- // backdrop element nor dialogEl's own mousedown reliably fires for a
60498
- // native ::backdrop click).
60499
- if (!isModal && backdrop) {
61016
+ // Outside-click handling for the wall we render ourselves. A modal's is a
61017
+ // plain document-level listener instead, set up in openEffect above (see
61018
+ // this file's top comment for why: neither a real backdrop element nor
61019
+ // dialogEl's own mousedown reliably fires for a native ::backdrop click),
61020
+ // and so is a wall-less dialog's.
61021
+ if (!isTopLayer && backdrop) {
60500
61022
  backdropProps.onMouseDown = mouseDownEvent => {
60501
61023
  if (mouseDownEvent.button !== 0) {
60502
61024
  return;
@@ -60506,7 +61028,7 @@ const useDialogProps = props => {
60506
61028
  if (openedDuringThisPress(openController)) {
60507
61029
  return;
60508
61030
  }
60509
- // See the custom renderer's own onDocumentMouseDown: a click inside
61031
+ // See the document-level listener in openEffect above: a click inside
60510
61032
  // another popup is a click on what is in front, not an outside click.
60511
61033
  const dialogEl = ref.current;
60512
61034
  const popupUnderPointer = mouseDownEvent.target.closest?.(`[navi-control="dialog"], [navi-control="popover"]`);
@@ -60523,7 +61045,7 @@ const useDialogProps = props => {
60523
61045
  // the same behavior for Dialog.
60524
61046
  };
60525
61047
  }
60526
- return [isModal || !backdrop ? null : backdropProps, contentProps];
61048
+ return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
60527
61049
  };
60528
61050
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
60529
61051
 
@@ -62248,22 +62770,18 @@ const css$C = /* css */`@layer navi {
62248
62770
  * — without this, the exact same `<Popup>` usage would behave
62249
62771
  * differently (close-on-outside-click or not) purely based on which mode
62250
62772
  * the screen-size check happens to pick, which defeats the point of
62251
- * having one shared API in the first place. Note this can only ever go so
62252
- * far: in dialog mode, `"none"`/`"capture"` still absorb every outside
62253
- * click (no visual effect vs. dimmed) rather than truly letting it
62254
- * through, since a `<dialog>` is always modal one way or another (see
62255
- * `dialog.jsx`'s own doc) — a popover's fully passive, click-through
62256
- * backdrop has no dialog-mode equivalent. Whatever content `Popup` opens
62257
- * is unavoidably *more* intrusive once it switches to dialog mode than
62258
- * the exact same usage would be as a popover — worth keeping in mind for
62259
- * anything that relies on `Popup` and can end up on a small screen.
62773
+ * having one shared API in the first place. Note this only says what a
62774
+ * press outside *does*; whether it reaches the page at all is `backdrop`
62775
+ * below, and `"none"`/`"capture"` describe a wall either way — the popup
62776
+ * absorbs the press without closing, dimmed or not.
62260
62777
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
62261
62778
  * popup and the page at all: `false` lets a press outside both dismiss the
62262
- * popup and reach whatever it landed on, in one gesture. Reaches the popover
62263
- * only — a `Dialog` is `showModal()`'d in the `layer="top"` it defaults to,
62264
- * and the page behind is then genuinely inert — so it is dropped rather than
62265
- * forwarded when the mode resolution picks a top-layer dialog, the same
62266
- * "only so far" as `pointerInteractionOutsideEffect` above.
62779
+ * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
62780
+ * honoured in either mode — a wall-less `Dialog` is shown through the
62781
+ * Popover API rather than `showModal()` — so which mode the screen-size
62782
+ * resolution picks says nothing about whether the page behind stays live.
62783
+ * It is also what makes a sheet docked to the bottom of a phone's screen
62784
+ * (`dockedOnSmallTouchScreen`) non-modal.
62267
62785
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant] - Forwarded
62268
62786
  * as-is to whichever component renders (both understand it identically):
62269
62787
  * how visible the backdrop is, independently of what an outside click
@@ -62334,9 +62852,6 @@ const Popup = props => {
62334
62852
  // exact same <Popup> usage behaves identically regardless of which
62335
62853
  // mode the automatic screen-size resolution happens to pick.
62336
62854
  pointerInteractionOutsideEffect = "close",
62337
- // Read here rather than left in ...rest so the dialog branch can drop it:
62338
- // a top-layer dialog is modal and has no press left to let through, and
62339
- // warns when asked (see Dialog's own backdrop prop).
62340
62855
  backdrop,
62341
62856
  // Popover-only (see this component's own doc) — destructured out so
62342
62857
  // they're never part of ...rest, and therefore never forwarded to
@@ -62371,7 +62886,7 @@ const Popup = props => {
62371
62886
  sizeFromAnchor: sizeFromAnchor,
62372
62887
  maxWidth: maxWidth,
62373
62888
  pointerInteractionOutsideEffect: pointerInteractionOutsideEffect,
62374
- backdrop: rest.layer === "local" ? backdrop : undefined,
62889
+ backdrop: backdrop,
62375
62890
  className: withPropsClassName("navi_popup", className),
62376
62891
  expand: expand,
62377
62892
  expandX: expandX,
@@ -72382,7 +72897,7 @@ const PickerFirstResolver = props => {
72382
72897
  * @param {"press"|"longpress"|"contextmenu"|string|string[]} [openOn="press"]
72383
72898
  * What opens the popup. The press, by default — the way a native select
72384
72899
  * opens. Any other name is an interaction navi detects (see
72385
- * docs/interactions.md: `"longpress"`, `"contextmenu"`, `"dblclick"`…), or
72900
+ * docs/interactions.md: `"longpress"`, `"contextmenu"`, `"double_click"`…), or
72386
72901
  * a list of them — `["longpress", "contextmenu"]` is the hold on a phone and
72387
72902
  * the right click at a desk. The picker declares it on itself, so it is
72388
72903
  * arbitrated like any other gesture: a swipe on the same card takes the
@@ -72686,8 +73201,10 @@ const PickerFirstResolver = props => {
72686
73201
  * and the page at all. `false` lets a press outside both close the popup and
72687
73202
  * reach whatever it landed on, in one gesture — for a picker opened over a
72688
73203
  * page that stays as pressable as it looks (a plan, a map, a canvas), where a
72689
- * wall would spend the first press on dismissing. Only the popover mode can
72690
- * honour it: a dialog is modal and the page behind it is inert.
73204
+ * wall would spend the first press on dismissing. Honoured in both modes: a
73205
+ * dialog with no wall is shown through the Popover API rather than
73206
+ * `showModal()`, so a picker docked as a bottom sheet on a phone still
73207
+ * leaves the plan behind it live.
72691
73208
  * @param {"auto"|"discrete"|"invisible"} [backdropVariant="auto"] How visible the
72692
73209
  * popup's backdrop is, independently of what a click outside does: `"auto"`
72693
73210
  * is the paint `pointerInteractionOutsideEffect` implies, `"discrete"` a
@@ -84434,11 +84951,11 @@ const css = /* css */`.navi_side_panel {
84434
84951
  * @param {boolean} [props.closeOnClickOutside=false] - `false` (default):
84435
84952
  * maps to `pointerInteractionOutsideEffect="none"` — in popover mode, no
84436
84953
  * backdrop at all, outside clicks pass straight through; in dialog mode,
84437
- * the outside click is still absorbed (a `<dialog>` always blocks
84438
- * interaction with the rest of the page one way or another — see
84439
- * `dialog.jsx`'s own doc) but with no dimming effect. `true`: closes the
84440
- * panel on an outside click instead, and also enables trapping Tab
84441
- * navigation inside the panel (`focusCapture`) — closing on outside
84954
+ * the outside click is absorbed by the panel's own wall but changes
84955
+ * nothing. Pass `backdrop={false}` (forwarded to `Popup`) for a panel with
84956
+ * no wall in either mode, whose outside presses reach the page. `true`:
84957
+ * closes the panel on an outside click instead, and also enables trapping
84958
+ * Tab navigation inside the panel (`focusCapture`) — closing on outside
84442
84959
  * interaction only makes sense paired with not letting focus silently
84443
84960
  * leave the panel first.
84444
84961
  * @param {boolean} [props.swipeToClose=true] - Pushing the panel back
@@ -84448,12 +84965,11 @@ const css = /* css */`.navi_side_panel {
84448
84965
  * deliberately. See `swipe_to_close.js` for when the gesture is claimed.
84449
84966
  * @param {"dialog"|"popover"} [props.mode] - Forwarded to `Popup` — forces
84450
84967
  * one underlying renderer instead of its automatic screen-size
84451
- * resolution. Note that if `Popup` ends up in dialog mode (small screen,
84452
- * or forced here), the panel becomes modal regardless of
84453
- * `closeOnClickOutside`/`pointerInteractionOutsideEffect`: a `<dialog>`
84454
- * always blocks interaction with the rest of the page one way or another
84455
- * (see `dialog.jsx`'s own doc) — there is no dialog-mode equivalent of a
84456
- * popover's fully passive, click-through backdrop.
84968
+ * resolution. Note that if `Popup` ends up in dialog mode (small screen, or
84969
+ * forced here), the panel is modal unless it says `backdrop={false}`:
84970
+ * `closeOnClickOutside`/`pointerInteractionOutsideEffect` only say what a
84971
+ * press on the wall does, not whether there is one (see `dialog.jsx`'s own
84972
+ * doc).
84457
84973
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
84458
84974
  * close button — add one wherever it makes sense for the layout (e.g. a
84459
84975
  * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
@@ -84758,5 +85274,5 @@ const UserSvg = () => jsx("svg", {
84758
85274
  })
84759
85275
  });
84760
85276
 
84761
- 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, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
85277
+ 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 };
84762
85278
  //# sourceMappingURL=jsenv_navi.js.map