@jsenv/navi 0.29.116 → 0.29.118

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.
@@ -26618,6 +26618,49 @@ const RouteUI = ({
26618
26618
  return element;
26619
26619
  };
26620
26620
 
26621
+ /**
26622
+ * Where a route movement is heading, said before its first picture is taken.
26623
+ *
26624
+ * A view transition pairs, across its two pictures, whatever wears the same
26625
+ * name on both — and a name is read off the DOM as it stands when the
26626
+ * transition starts. So whoever names an element for a movement between two
26627
+ * pages (the bar under the current tab of a <Nav>, which glides from one tab
26628
+ * to the next) has to decide whether this movement is one it takes part in
26629
+ * before that reading: on a page the row is not on, its bar must leave with the
26630
+ * page, unnamed.
26631
+ *
26632
+ * Everything that decision needs is on the page itself, except one thing:
26633
+ * where the movement goes. That is what is published here, by whoever starts
26634
+ * the movement — route_transition.jsx for a relation between two pages,
26635
+ * route_travel.jsx for a row a finger pushes — before their transition starts.
26636
+ * The navigation's own announcement (before_routing.js) cannot stand in for
26637
+ * it: a travel a finger sets off starts its transition first and navigates
26638
+ * from inside it.
26639
+ *
26640
+ * Only one movement plays at a time (one view transition per document), which
26641
+ * is what lets both say it in one place. Taken down by the one that put it up,
26642
+ * and by it alone: a movement ending after another has replaced it must not
26643
+ * wipe what the new one stands on.
26644
+ */
26645
+
26646
+
26647
+ const [publishDestination, observeTransitionDestination] = createPubSub();
26648
+
26649
+ let destinationOwner = null;
26650
+
26651
+ const holdTransitionDestination = (owner, url) => {
26652
+ destinationOwner = owner;
26653
+ publishDestination(new URL(url, window.location.href).href);
26654
+ };
26655
+
26656
+ const releaseTransitionDestination = (owner) => {
26657
+ if (owner !== destinationOwner) {
26658
+ return;
26659
+ }
26660
+ destinationOwner = null;
26661
+ publishDestination(null);
26662
+ };
26663
+
26621
26664
  /**
26622
26665
  * The window two pages are seen through while one replaces the other, measured
26623
26666
  * once and published for the length of the movement.
@@ -27498,6 +27541,7 @@ const rebuildWatcher = () => {
27498
27541
  }
27499
27542
  beginTransition({
27500
27543
  page: pages[index],
27544
+ url: navigationUrl,
27501
27545
  // Which way it plays: what the navigation itself said first — the link
27502
27546
  // being pressed is where the way the app is being walked is known — then
27503
27547
  // the relation, and forward for a navigation that asked for a movement
@@ -27518,11 +27562,12 @@ const rebuildWatcher = () => {
27518
27562
 
27519
27563
  // What plays when no relation matched (see defineRouteDefaultTransition), what
27520
27564
  // the navigation now landing asked for on its own (see readNavigationRequest),
27521
- // and whether it found an answer already — a relation's transition, a "none",
27522
- // a RouteTravel travel. The last two are read at the start of every
27523
- // navigation, so they are always about the latest one.
27565
+ // where it goes, and whether it found an answer already — a relation's
27566
+ // transition, a "none", a RouteTravel travel. The last three are read at the
27567
+ // start of every navigation, so they are always about the latest one.
27524
27568
  let defaultTransition = null;
27525
27569
  let navigationRequest = null;
27570
+ let navigationUrl = null;
27526
27571
  let navigationAnimated = false;
27527
27572
 
27528
27573
  // The two ends of every navigation, watched from here on. The picture of the
@@ -27534,6 +27579,7 @@ let navigationAnimated = false;
27534
27579
  observeBeforeRouting(details => {
27535
27580
  navigationAnimated = false;
27536
27581
  navigationRequest = readNavigationRequest(details);
27582
+ navigationUrl = details.url;
27537
27583
  if (relations.length === 0 && !defaultTransition && !navigationRequest) {
27538
27584
  return;
27539
27585
  }
@@ -27541,9 +27587,11 @@ observeBeforeRouting(details => {
27541
27587
  });
27542
27588
  observeAfterRouting(() => {
27543
27589
  const request = navigationRequest;
27590
+ const url = navigationUrl;
27544
27591
  // Read here and dropped here: a request answers for the navigation it was
27545
27592
  // made on, and the next one is back to the relations.
27546
27593
  navigationRequest = null;
27594
+ navigationUrl = null;
27547
27595
  if (!navigationAnimated && (request || defaultTransition)) {
27548
27596
  const {
27549
27597
  type,
@@ -27552,6 +27600,7 @@ observeAfterRouting(() => {
27552
27600
  if (type !== "none") {
27553
27601
  beginTransition({
27554
27602
  page: null,
27603
+ url,
27555
27604
  // A default has no direction: nothing says which of two arbitrary
27556
27605
  // pages is before the other, and the attribute is then worn empty —
27557
27606
  // present for whoever keys on "one of ours is playing", silent on the
@@ -27599,6 +27648,7 @@ const findRelation = (fromPage, toPage) => {
27599
27648
  let currentTransition = null;
27600
27649
  const beginTransition = ({
27601
27650
  page,
27651
+ url,
27602
27652
  direction,
27603
27653
  type,
27604
27654
  duration
@@ -27615,6 +27665,9 @@ const beginTransition = ({
27615
27665
  }
27616
27666
  const transition = {};
27617
27667
  currentTransition = transition;
27668
+ // Said before the picture is taken: whoever names something for a movement
27669
+ // between two pages decides on it now (see transition_destination.js).
27670
+ holdTransitionDestination(transition, url);
27618
27671
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
27619
27672
  if (type) {
27620
27673
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
@@ -27719,6 +27772,7 @@ const beginTransition = ({
27719
27772
  documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
27720
27773
  documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
27721
27774
  releaseTransitionWindow(transition);
27775
+ releaseTransitionDestination(transition);
27722
27776
  if (restoreDuration) {
27723
27777
  restoreDuration();
27724
27778
  }
@@ -28380,8 +28434,12 @@ const RouteTravel = ({
28380
28434
  travelRef.current = travel;
28381
28435
  // Taken before the picture is: the browser reads the name off the DOM as it
28382
28436
  // stands when the transition starts, and this box is only a picture of its
28383
- // own for as long as it is the one travelling.
28437
+ // own for as long as it is the one travelling. Where it travels to is said
28438
+ // at the same moment and for the same reason: whoever names something for
28439
+ // a movement between two pages decides on it now (see
28440
+ // transition_destination.js).
28384
28441
  nameForTravel(elementRef.current);
28442
+ holdTransitionDestination(travel, page.route.buildUrl(page.params));
28385
28443
  document.documentElement.setAttribute(TRAVEL_ATTRIBUTE, direction);
28386
28444
  document.documentElement.setAttribute(TRAVEL_AXIS_ATTRIBUTE, axis);
28387
28445
  if (scrub) {
@@ -28737,6 +28795,7 @@ const RouteTravel = ({
28737
28795
  document.documentElement.removeAttribute(DRAGGED_ATTRIBUTE);
28738
28796
  document.documentElement.removeAttribute(TURNED_ATTRIBUTE);
28739
28797
  releaseTransitionWindow(travel);
28798
+ releaseTransitionDestination(travel);
28740
28799
  }
28741
28800
  };
28742
28801
 
@@ -29628,7 +29687,11 @@ registerNaviCommand("--navi-void", (source) => {
29628
29687
  };
29629
29688
  });
29630
29689
 
29631
- registerNaviCommand("--navi-update", (source, event) => {
29690
+ // "--navi-update:smooth" asks the control to be SEEN moving to the value — a
29691
+ // wheel scrolls to it rather than swapping its digits — so a shortcut under
29692
+ // two wheels shows which one it changed, and by how much. The value itself is
29693
+ // set at once either way; only the drawing is concerned.
29694
+ registerNaviCommand("--navi-update", (source, event, { argument }) => {
29632
29695
  const target =
29633
29696
  resolveExplicitTarget(source) || resolveFirstParentControl(source);
29634
29697
  if (!target) {
@@ -29645,6 +29708,7 @@ registerNaviCommand("--navi-update", (source, event) => {
29645
29708
  const commandValue = resolveCommandValue(source, event);
29646
29709
  dispatchRequestSetUIState(target, commandValue, {
29647
29710
  event,
29711
+ behavior: argument,
29648
29712
  });
29649
29713
  },
29650
29714
  });
@@ -38719,8 +38783,7 @@ const BinderItemContext = createContext(null);
38719
38783
 
38720
38784
  /**
38721
38785
  * What a <Link> learns from the <Nav> around it: where to draw the bar that
38722
- * says "you are here", the name under which the browser is to recognise that
38723
- * bar from one page to the next, and — for a row of tabs that are slides — which
38786
+ * says "you are here", and for a row of tabs that are slides — which
38724
38787
  * <SlideContainer> they are about and which of its slides is on screen (see
38725
38788
  * nav.jsx).
38726
38789
  */
@@ -39416,14 +39479,7 @@ const LinkPlain = props => {
39416
39479
  // around this link.
39417
39480
  const currentIndicatorAsked = currentIndicator ?? nav?.currentIndicator;
39418
39481
  const currentIndicatorPosition = currentIndicatorAsked === true ? "bottom" : currentIndicatorAsked;
39419
- const currentIndicatorEl = currentIndicatorPosition === "left" || currentIndicatorPosition === "right" || currentIndicatorPosition === "top" || currentIndicatorPosition === "bottom" ? jsx(LinkCurrentIndicator
39420
- // Only the bar one can actually see carries the row's name, because a
39421
- // name belongs to one element at a time and every tab holds a bar. The
39422
- // browser then has the same thing in two places from one page to the
39423
- // next, and moves it — which is the whole of "the bar slides".
39424
- , {
39425
- viewTransitionName: innerCurrent ? nav?.indicatorName : null
39426
- }) : null;
39482
+ const currentIndicatorEl = currentIndicatorPosition === "left" || currentIndicatorPosition === "right" || currentIndicatorPosition === "top" || currentIndicatorPosition === "bottom" ? jsx(LinkCurrentIndicator, {}) : null;
39427
39483
  const {
39428
39484
  onClick,
39429
39485
  preventDefault
@@ -39542,14 +39598,12 @@ const LinkPlain = props => {
39542
39598
  children: [startIconEl, innerChildren, endIconEl]
39543
39599
  });
39544
39600
  };
39545
- const LinkCurrentIndicator = ({
39546
- viewTransitionName
39547
- }) => {
39601
+
39602
+ // Named by the <Nav> around the link when the link is current, from its CSS:
39603
+ // that is what makes the bar glide from one tab to the next (see nav.jsx).
39604
+ const LinkCurrentIndicator = () => {
39548
39605
  return jsx("span", {
39549
- className: "navi_current_indicator",
39550
- style: viewTransitionName ? {
39551
- viewTransitionName
39552
- } : undefined
39606
+ className: "navi_current_indicator"
39553
39607
  });
39554
39608
  };
39555
39609
  markAsOutsideTextFlow(LinkCurrentIndicator);
@@ -39559,6 +39613,9 @@ installImportMetaCssBuild(import.meta);/**
39559
39613
  * https://dribbble.com/search/tabs
39560
39614
  */
39561
39615
  let navCount = 0;
39616
+ // Worn by a nav of routes while a route movement between two of its tabs is
39617
+ // pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
39618
+ const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
39562
39619
  const css$V = /* css */`
39563
39620
  @layer navi {
39564
39621
  .navi_nav {
@@ -39638,6 +39695,30 @@ const css$V = /* css */`
39638
39695
  }
39639
39696
  }
39640
39697
 
39698
+ /* The bar under the current tab of a nav made of routes is NAMED: a change
39699
+ played as a view transition then finds it on both pictures and moves it
39700
+ from the tab it was under to the tab it is under, which is all "the bar
39701
+ slides" is. The name is the row's (--nav-indicator-name, absent when the
39702
+ row asked for no slide) and only the bar one can see wears it: every tab
39703
+ holds a bar, and a name belongs to one element at a time. */
39704
+ .navi_nav .navi_link[data-href-current] .navi_current_indicator {
39705
+ view-transition-name: var(--nav-indicator-name);
39706
+ }
39707
+ /* Named for a movement it takes part in, and for that alone. While the PAGES
39708
+ are the ones moving — a route transition, a route travel — a name lifts the
39709
+ bar out of its page's picture into a picture of its own, and a picture of
39710
+ its own is precisely what does not travel: it stands where it was captured,
39711
+ fading, while the row slides away under it. Right when the row is on both
39712
+ sides and the bar has a tab to glide to; wrong anywhere else, where the bar
39713
+ must leave or arrive with its row. So the name is dropped unless the
39714
+ movement goes from a tab of this row to another one of its tabs (see
39715
+ markIndicatorTakesPart). */
39716
+ :root:is([data-navi-route-transition], [data-navi-route-travel])
39717
+ .navi_nav:not([${BETWEEN_TABS_ATTRIBUTE}])
39718
+ .navi_current_indicator {
39719
+ view-transition-name: none;
39720
+ }
39721
+
39641
39722
  .navi_nav {
39642
39723
  display: flex;
39643
39724
  width: fit-content;
@@ -39819,9 +39900,12 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
39819
39900
  * does so by being NAMED, which is all the browser needs: any change played as
39820
39901
  * a view transition animates it on the same clock as everything else in that
39821
39902
  * transition. Inside a `RouteTravel` that means it follows the pages, and the
39822
- * thumb dragging them, without either of them being told about the other. For
39823
- * a nav made of slides (`slideContainer`) the bar is one element for the whole
39824
- * row, and it reads the travel the container publishes.
39903
+ * thumb dragging them, without either of them being told about the other. It
39904
+ * is named for a movement between two tabs of the row and for nothing else:
39905
+ * when the pages move and the row is on one side only, the bar leaves or
39906
+ * arrives with its row. For a nav made of slides (`slideContainer`) the bar
39907
+ * is one element for the whole row, and it reads the travel the container
39908
+ * publishes.
39825
39909
  * @param {string} [props.slideContainer] - the id of a `<SlideContainer>` these
39826
39910
  * tabs are about: each one says which slide it is (`<Link slide="…">`), the
39827
39911
  * container says which one is on screen, and pressing a tab travels there.
@@ -39954,16 +40038,47 @@ const Nav = ({
39954
40038
  useLayoutEffect(() => {
39955
40039
  paintIndicatorGeometry();
39956
40040
  });
40041
+
40042
+ // Whether the bar keeps its name for the route movement about to be pictured
40043
+ // (see the CSS above). Written on the DOM as the movement is announced rather
40044
+ // than rendered: rendering is held from a navigation's first word until the
40045
+ // movement's first picture is taken (see rendering_hold.js), so a render
40046
+ // would land on the second picture with the first already taken.
40047
+ useLayoutEffect(() => {
40048
+ const navElement = navRef.current;
40049
+ const markIndicatorTakesPart = destinationUrl => {
40050
+ if (destinationUrl === null) {
40051
+ navElement.removeAttribute(BETWEEN_TABS_ATTRIBUTE);
40052
+ return;
40053
+ }
40054
+ // From a tab of this row: the row still shows the page being left, and
40055
+ // the tab it is on. To a tab of this row: one of its links aims at the
40056
+ // page the movement goes to. A row mounted while the movement plays hears
40057
+ // nothing here and stays unnamed, rightly: its bar is on the second
40058
+ // picture alone, with nothing on the first to glide from.
40059
+ const fromTab = navElement.querySelector("[data-href-current]");
40060
+ let toTab = null;
40061
+ for (const linkElement of navElement.querySelectorAll("a[href]")) {
40062
+ if (linkElement.href === destinationUrl) {
40063
+ toTab = linkElement;
40064
+ break;
40065
+ }
40066
+ }
40067
+ if (fromTab && toTab) {
40068
+ navElement.setAttribute(BETWEEN_TABS_ATTRIBUTE, "");
40069
+ } else {
40070
+ navElement.removeAttribute(BETWEEN_TABS_ATTRIBUTE);
40071
+ }
40072
+ };
40073
+ return observeTransitionDestination(markIndicatorTakesPart);
40074
+ }, []);
39957
40075
  const navContextValue = useMemo(() => ({
39958
40076
  // The bar belongs to the row itself when the tabs are slides, so the
39959
40077
  // links draw none of their own.
39960
40078
  currentIndicator: slideContainer ? undefined : currentIndicator,
39961
- // Read by the link that is current, and by it alone: a name belongs to
39962
- // one element at a time, and the bar exists in every tab.
39963
- indicatorName: currentIndicatorSlides ? indicatorNameRef.current : null,
39964
40079
  slideContainer,
39965
40080
  currentSlideArea
39966
- }), [currentIndicator, currentIndicatorSlides, slideContainer, currentSlideArea]);
40081
+ }), [currentIndicator, slideContainer, currentSlideArea]);
39967
40082
  children = toChildArray(children);
39968
40083
  return jsxs(Box, {
39969
40084
  as: "nav",
@@ -39993,6 +40108,13 @@ const Nav = ({
39993
40108
  expandY: expandY,
39994
40109
  spacing: spacing,
39995
40110
  ...props,
40111
+ // The name the bar of the current tab wears, handed to the CSS above
40112
+ // rather than to the links: which bar wears it, and when, is decided
40113
+ // there.
40114
+ style: currentIndicatorSlides && !slideContainer ? {
40115
+ ...props.style,
40116
+ "--nav-indicator-name": indicatorNameRef.current
40117
+ } : props.style,
39996
40118
  styleCSSVars: NavStyleCSSVars,
39997
40119
  children: [indicatorPosition && jsx("span", {
39998
40120
  className: "navi_nav_indicator"
@@ -50058,8 +50180,12 @@ const SlideContainer = ({
50058
50180
  // after it — a step that has been answered is not a step to stay on.
50059
50181
  const done = (area, event) => {
50060
50182
  markAnswered(area);
50061
- if (!moveNext(event)) {
50062
- movePrevious(event);
50183
+ if (!moveNext(event, {
50184
+ released: true
50185
+ })) {
50186
+ movePrevious(event, {
50187
+ released: true
50188
+ });
50063
50189
  }
50064
50190
  };
50065
50191
 
@@ -50377,6 +50503,7 @@ const SlideContainer = ({
50377
50503
  forward,
50378
50504
  event,
50379
50505
  value,
50506
+ released,
50380
50507
  dx = 0,
50381
50508
  dy = 0
50382
50509
  } = {}) => {
@@ -50391,6 +50518,7 @@ const SlideContainer = ({
50391
50518
  forward,
50392
50519
  event,
50393
50520
  value,
50521
+ released,
50394
50522
  dx,
50395
50523
  dy
50396
50524
  });
@@ -50427,7 +50555,11 @@ const SlideContainer = ({
50427
50555
  // event dispatched by hand: a slide that holds on to the user holds them
50428
50556
  // whatever they press. Read off the slide being LEFT, because that is what
50429
50557
  // has a reason to keep them (an answer still missing, a step not taken).
50430
- if (currentElement?.hasAttribute(forward ? "data-prevent-nav-next" : "data-prevent-nav-previous")) {
50558
+ // `released` is the slide letting go itself (--navi-done, see Slide): the
50559
+ // one departure the forward hold does not apply to. Said here rather than
50560
+ // by dropping the attribute, which stays the render's to write — a hold
50561
+ // meant to stay (an explicit preventNavNext) is still there on the way back.
50562
+ if (forward ? !released && currentElement?.hasAttribute("data-prevent-nav-next") : currentElement?.hasAttribute("data-prevent-nav-previous")) {
50431
50563
  return false;
50432
50564
  }
50433
50565
  // The focus moves here, while the event that asked for it is still in hand:
@@ -50576,15 +50708,20 @@ const SlideContainer = ({
50576
50708
  dx,
50577
50709
  dy,
50578
50710
  event,
50579
- value
50711
+ value,
50712
+ released
50580
50713
  } = pendingRollsRef.current.shift();
50581
50714
  if (dx || dy) {
50582
- move(dx, dy, event, value);
50715
+ move(dx, dy, event, {
50716
+ value,
50717
+ released
50718
+ });
50583
50719
  return;
50584
50720
  }
50585
50721
  goToArea(area, {
50586
50722
  event,
50587
- value
50723
+ value,
50724
+ released
50588
50725
  });
50589
50726
  }, [noTravel]);
50590
50727
 
@@ -50703,10 +50840,14 @@ const SlideContainer = ({
50703
50840
  return area;
50704
50841
  }
50705
50842
  };
50706
- const move = (dx, dy, event, value) => goToArea(areaTowards(dx, dy), {
50843
+ const move = (dx, dy, event, {
50844
+ value,
50845
+ released
50846
+ } = {}) => goToArea(areaTowards(dx, dy), {
50707
50847
  forward: dx > 0 || dy > 0,
50708
50848
  event,
50709
50849
  value,
50850
+ released,
50710
50851
  dx,
50711
50852
  dy
50712
50853
  });
@@ -50716,8 +50857,8 @@ const SlideContainer = ({
50716
50857
  // slides are. On a map they mean the same thing, and fall back to the other
50717
50858
  // axis when there is nothing that way — a step onwards, however the screens
50718
50859
  // happen to be arranged.
50719
- const moveNext = event => vertical ? move(0, 1, event) || move(1, 0, event) : move(1, 0, event) || move(0, 1, event);
50720
- const movePrevious = event => vertical ? move(0, -1, event) || move(-1, 0, event) : move(-1, 0, event) || move(0, -1, event);
50860
+ const moveNext = (event, options) => vertical ? move(0, 1, event, options) || move(1, 0, event, options) : move(1, 0, event, options) || move(0, 1, event, options);
50861
+ const movePrevious = (event, options) => vertical ? move(0, -1, event, options) || move(-1, 0, event, options) : move(-1, 0, event, options) || move(0, -1, event, options);
50721
50862
 
50722
50863
  // Where the track is right now, as the gesture left it: the resting place of
50723
50864
  // the slide being dragged, plus what the pointer has pulled since.
@@ -51364,7 +51505,9 @@ const SlideContainer = ({
51364
51505
  // (a --navi-right command carries the click that ran it, that click its
51365
51506
  // own mousedown), and the focus transfer reads the whole chain to know
51366
51507
  // where the interaction started.
51367
- move(dx, dy, e, value);
51508
+ move(dx, dy, e, {
51509
+ value
51510
+ });
51368
51511
  }
51369
51512
  // By name rather than by direction (--navi-go-to-slide): the caller says
51370
51513
  // where, the map says nothing about it.
@@ -51524,11 +51667,6 @@ const Slide = ({
51524
51667
  ,
51525
51668
 
51526
51669
  onnavi_done: e => {
51527
- // Dropped imperatively rather than left to the re-render this
51528
- // schedules: moving on happens in this same handler, and the gate it
51529
- // goes through reads this attribute off the DOM (see goToArea) — a
51530
- // render is a microtask away, the move is not.
51531
- e.currentTarget.removeAttribute("data-prevent-nav-next");
51532
51670
  container?.done(slideArea);
51533
51671
  rest.onnavi_done?.(e);
51534
51672
  },
@@ -57182,7 +57320,12 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
57182
57320
  --picker-popup-border-radius,
57183
57321
  var(--picker-border-radius)
57184
57322
  );
57185
- --dialog-border-width: var(--picker-dialog-border-width);
57323
+ /* Explicit fallback, for the same reason as --popover-background-color
57324
+ above: the dialog paints border-width from this var with no fallback
57325
+ of its own, so an unset dialogBorderWidth would leave the var
57326
+ guaranteed-invalid and border-width at its initial "medium" (3px).
57327
+ 0px is the dialog's own default. */
57328
+ --dialog-border-width: var(--picker-dialog-border-width, 0px);
57186
57329
  --dialog-border-color: var(--x-picker-border-color);
57187
57330
  /* The picker's own surface is not this one — see the popover branch,
57188
57331
  including why the fallback is spelled out. */
@@ -70558,7 +70701,7 @@ const WheelGroupContext = createContext(null);
70558
70701
  * @param {boolean} [props.glass] - Frost the neighbouring rows so the center reads as a clear "window" (iOS-picker style). Inherited from a WheelGroup.
70559
70702
  * @param {boolean} [props.frameBorder] - Line the center-window edges with a faint frame (off by default; independent of glass). Tune via --wheel-frame-color.
70560
70703
  * @param {boolean|number} [props.zoom] - Make the centered value stand out by size: the neighbours are drawn smaller, so a value grows as it slides into the window and shrinks as it leaves (the centered one keeps its natural size). `true` uses the default ratio (1.3); a number is the ratio itself (1.8 = the center reads 80% bigger than its neighbours). Inherited from a WheelGroup.
70561
- * @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
70704
+ * @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps, a value set with `--navi-update:smooth` and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
70562
70705
  * @param {string} [props.type] - Informative value kind (e.g. "integer", "day"). Used only for rendering hints, like tabular figures for "integer".
70563
70706
  */
70564
70707
  const Wheel = props => {
@@ -71280,6 +71423,23 @@ function WheelUI(props) {
71280
71423
  // faster, so a second press mid-glide reads as accelerating, not restarting.
71281
71424
  const targetPosRef = useRef(null);
71282
71425
  const glideRef = useRef(null);
71426
+ // Who aimed the running glide. A value set from outside (`--navi-update:smooth`)
71427
+ // is already committed by whoever set it: the wheel only catches up with it,
71428
+ // and its arrival must not be reported as a settle — no navi_wheel_settle, no
71429
+ // action. A user input is the opposite: its arrival IS the settle. Written at
71430
+ // every motion start (settle, glideTo).
71431
+ const glideFromOutsideRef = useRef(false);
71432
+ // How the value that just arrived asked to be shown. A set request can say
71433
+ // `behavior: "smooth"` (what `--navi-update:smooth` puts on it); the wheel
71434
+ // then scrolls to the value instead of swapping the digits. Read off the
71435
+ // navi_ui_state_change the controller dispatches — the request may have
71436
+ // reached this wheel through a group distributing to its children, so it is
71437
+ // looked up along the event chain. Consumed by the sync effect below.
71438
+ const pendingBehaviorRef = useRef(null);
71439
+ const onUIStateChange = e => {
71440
+ const setRequest = findEvent(e, "navi_set_ui_state");
71441
+ pendingBehaviorRef.current = setRequest ? setRequest.detail.behavior : null;
71442
+ };
71283
71443
  // The glide loop is bound once (mount effect) but the speed can change live
71284
71444
  // (e.g. a demo control) — read it through a ref so it uses the latest.
71285
71445
  const glideSpeedRef = useRef(glideSpeed);
@@ -71470,11 +71630,11 @@ function WheelUI(props) {
71470
71630
  posRef.current = index * size;
71471
71631
  renderPos(vp);
71472
71632
  }
71473
- if (!interactive) {
71633
+ centeredIndexRef.current = index;
71634
+ if (!interactive || glideFromOutsideRef.current) {
71474
71635
  return;
71475
71636
  }
71476
71637
  const settleEvent = new CustomEvent("navi_wheel_settle");
71477
- centeredIndexRef.current = index;
71478
71638
  debugScroll(`settle: committed → ${trackedItemsRef.current[index].value}`);
71479
71639
  requestSelectValue(trackedItemsRef.current[index].value, settleEvent);
71480
71640
  // The value is now stable → commit the action. The wheel runs actionEvent
@@ -71557,13 +71717,21 @@ function WheelUI(props) {
71557
71717
  // Spring stiffness (fraction of remaining distance per ~frame). Scales with the
71558
71718
  // glide speed so slower = gentler chase; clamped so it never crawls or snaps.
71559
71719
  const glideSpringFactor = () => clampNumber(glideSpeedRef.current * 1.4, 0.06, 0.45);
71560
- const glideTo = (vp, target) => {
71720
+ const glideTo = (vp, target, {
71721
+ fromOutside = false
71722
+ } = {}) => {
71561
71723
  // A discrete glide overrides any fling momentum.
71562
71724
  if (momentumRef.current !== null) {
71563
71725
  cancelAnimationFrame(momentumRef.current);
71564
71726
  momentumRef.current = null;
71565
71727
  }
71566
71728
  targetPosRef.current = target;
71729
+ // A user input anywhere in the movement makes its arrival a user settle;
71730
+ // an outside value re-aiming a glide the user started never takes that
71731
+ // away from them (their tap still has to be answered with a settle).
71732
+ if (glideRef.current === null || !fromOutside) {
71733
+ glideFromOutsideRef.current = fromOutside;
71734
+ }
71567
71735
  if (glideRef.current === null) {
71568
71736
  debugScroll("glide: start");
71569
71737
  glideRef.current = requestAnimationFrame(() => glideStep(vp, performance.now()));
@@ -71579,6 +71747,7 @@ function WheelUI(props) {
71579
71747
  // overshoots only a handful of rows (a picker isn't a free-scrolling list).
71580
71748
  const settle = (vp, velocity) => {
71581
71749
  cancelAnim();
71750
+ glideFromOutsideRef.current = false;
71582
71751
  debugScroll(`settle: momentum start (v=${velocity}px/ms)`);
71583
71752
  // A drag fling: allow the full swipe velocity (see WHEEL_FLING_MAX_VELOCITY)
71584
71753
  // so a hard swipe carries across the list instead of being clipped to a few
@@ -71643,13 +71812,19 @@ function WheelUI(props) {
71643
71812
  glideTo(vp, target);
71644
71813
  };
71645
71814
 
71646
- // Center value `index` (external value / initial / keyboard / click). Smooth
71647
- // glides to the nearest copy (glideTargetFor) so a wrap goes the short way.
71815
+ // Center value `index` for a value the wheel did not choose itself (first
71816
+ // display, a value set from outside). "smooth" glides to the nearest copy
71817
+ // (glideTargetFor) so a wrap goes the short way, and arrives silently (see
71818
+ // glideFromOutsideRef); "auto" puts the row in place at once, stopping
71819
+ // whatever was moving so no glide lands on top of it a frame later.
71648
71820
  const centerOnIndex = (vp, index, behavior) => {
71649
71821
  const target = glideTargetFor(vp, index);
71650
71822
  if (behavior === "smooth") {
71651
- glideTo(vp, target);
71823
+ glideTo(vp, target, {
71824
+ fromOutside: true
71825
+ });
71652
71826
  } else {
71827
+ cancelAnim();
71653
71828
  setPos(vp, target);
71654
71829
  }
71655
71830
  };
@@ -71661,7 +71836,7 @@ function WheelUI(props) {
71661
71836
  const glideToIndex = (vp, index, event) => {
71662
71837
  centeredIndexRef.current = index;
71663
71838
  requestSelectValue(trackedItemsRef.current[index].value, event);
71664
- centerOnIndex(vp, index, "smooth");
71839
+ glideTo(vp, glideTargetFor(vp, index));
71665
71840
  };
71666
71841
  // Index we are heading to (the target, not the mid-glide visual center).
71667
71842
  const currentTargetIndex = vp => {
@@ -71690,14 +71865,12 @@ function WheelUI(props) {
71690
71865
  // Sync the center with the current value — used on first display and whenever
71691
71866
  // the controlled value changes from outside.
71692
71867
  const syncCenterToSelection = (viewportEl, behavior) => {
71693
- // A glide or momentum in flight (tap, arrow, fling) is already taking the
71694
- // wheel to the right row. This runs on every controlled-value re-render, which
71695
- // lags a frame behind our own centeredIndexRef so on rapid taps its guard
71696
- // below would miss and it would snap instantly mid-glide, leaving the wheel
71697
- // stuck off-center. Let the motion finish: commitSelection settles the value
71698
- // and the next render is a no-op. A genuine external change made during motion
71699
- // is honoured once it settles (centeredIndexRef then differs from selection).
71700
- if (glideRef.current !== null || momentumRef.current !== null) {
71868
+ // A fling in flight reports its own value at every row crossing and commits
71869
+ // on settle: the finger that threw it owns the wheel until then, and a value
71870
+ // arriving meanwhile is re-stated by the next crossing anyway. A glide is
71871
+ // different: its target is re-aimed below (glideTo never restarts the
71872
+ // loop), so a value set from outside mid-glide is where the wheel ends up.
71873
+ if (momentumRef.current !== null) {
71701
71874
  return;
71702
71875
  }
71703
71876
  if (trackedItemsRef.current.length === 0) {
@@ -71739,13 +71912,27 @@ function WheelUI(props) {
71739
71912
  };
71740
71913
  }, []);
71741
71914
 
71742
- // React to controlled value changes coming from outside.
71915
+ // React to controlled value changes coming from outside. The row is put in
71916
+ // place at once, unless the request asked for "smooth": the value is already
71917
+ // set for whoever reads it, and the wheel scrolls to it so the eye can follow
71918
+ // the change (a shortcut button under two wheels: which one moved, and by how
71919
+ // much). Never on the first centering (no target yet → nothing to glide from,
71920
+ // the displayed effect above handles it), nor under reduced motion.
71743
71921
  useLayoutEffect(() => {
71922
+ const requestedBehavior = pendingBehaviorRef.current;
71923
+ pendingBehaviorRef.current = null;
71744
71924
  const viewportEl = getViewport();
71745
71925
  if (!viewportEl || viewportEl.offsetParent === null) {
71746
71926
  return;
71747
71927
  }
71748
- syncCenterToSelection(viewportEl, "auto");
71928
+ let behavior = "auto";
71929
+ if (requestedBehavior === "smooth" && centeredIndexRef.current !== null) {
71930
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
71931
+ if (!prefersReducedMotion) {
71932
+ behavior = "smooth";
71933
+ }
71934
+ }
71935
+ syncCenterToSelection(viewportEl, behavior);
71749
71936
  });
71750
71937
  useWheelInteractions({
71751
71938
  ref,
@@ -71813,6 +72000,7 @@ function WheelUI(props) {
71813
72000
  children: [jsx(Box, {
71814
72001
  as: "input",
71815
72002
  ...controlHostProps,
72003
+ onnavi_ui_state_change: onUIStateChange,
71816
72004
  tabindex: -1,
71817
72005
  "aria-hidden": "true",
71818
72006
  className: "navi_wheel_input"