@jsenv/navi 0.29.157 → 0.29.159

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, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, 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, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
@@ -3184,8 +3184,11 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
3184
3184
  * - prerun actions may have no other reference yet, so
3185
3185
  * prerunProtectionRegistry pins them for a few minutes.
3186
3186
  *
3187
- * An action run never throws: failures land in errorSignal and are reported
3188
- * once, by one rule, in action_error_report.js (see the comment in onRunError).
3187
+ * A failing run rejects, and writes its error into errorSignal as well. The
3188
+ * callers that cannot take a rejection — a run started from a signal effect, a
3189
+ * control that already draws the failure — swallow it where they start it, and
3190
+ * that is where an error nobody displayed is reported once, by one rule (see
3191
+ * run_unwatched.js and action_error_report.js).
3189
3192
  *
3190
3193
  * Blast radius: this file is the substrate of routing (route_action.js,
3191
3194
  * action_run_effect.js), resources (state/rest) and every control's `action`
@@ -3568,16 +3571,27 @@ ${lines.join("\n")}`);
3568
3571
 
3569
3572
  const actionToRunPrivateProperties =
3570
3573
  getActionPrivateProperties(actionToPrerunOrRun);
3571
- const performRunResult = actionToRunPrivateProperties.performRun({
3572
- globalAbortSignal,
3573
- abortSignal: effectiveSignal,
3574
- reason,
3575
- event,
3576
- isPrerun,
3577
- onComplete,
3578
- onAbort,
3579
- onError,
3580
- });
3574
+ let performRunResult;
3575
+ try {
3576
+ performRunResult = actionToRunPrivateProperties.performRun({
3577
+ globalAbortSignal,
3578
+ abortSignal: effectiveSignal,
3579
+ reason,
3580
+ event,
3581
+ isPrerun,
3582
+ onComplete,
3583
+ onAbort,
3584
+ onError,
3585
+ });
3586
+ } catch (error) {
3587
+ // A synchronous callback that threw. Caught here so the update carries
3588
+ // on — the other actions of the same update have nothing to do with
3589
+ // this one — and given back further down to whoever asked for THIS
3590
+ // action, still as a throw.
3591
+ activationWeakSet.add(actionToPrerunOrRun);
3592
+ resultArray.push({ type: "sync_error", error });
3593
+ return;
3594
+ }
3581
3595
  activationWeakSet.add(actionToPrerunOrRun);
3582
3596
 
3583
3597
  if (performRunResult && typeof performRunResult.then === "function") {
@@ -3623,14 +3637,30 @@ ${lines.join("\n")}`);
3623
3637
  requestedResult = null;
3624
3638
  } else if (hasAsync) {
3625
3639
  requestedResult = Promise.all(
3626
- resultArray.map((item) =>
3627
- item.type === "sync" ? item.result : item.promise,
3628
- ),
3640
+ resultArray.map((item) => {
3641
+ if (item.type === "sync") {
3642
+ return item.result;
3643
+ }
3644
+ if (item.type === "sync_error") {
3645
+ return Promise.reject(item.error);
3646
+ }
3647
+ return item.promise;
3648
+ }),
3629
3649
  );
3630
3650
  } else {
3651
+ const itemFailed = resultArray.find((item) => item.type === "sync_error");
3652
+ if (itemFailed) {
3653
+ // Nothing of this update is left to do, so the failure can leave the way
3654
+ // it arrived: thrown, to the caller that asked for that action.
3655
+ throw itemFailed.error;
3656
+ }
3631
3657
  requestedResult = resultArray.map((item) => item.result);
3632
3658
  }
3633
3659
 
3660
+ // Settled rather than raced: a failure here is one outcome among several —
3661
+ // the other actions of the same update ran too, and "the update is over" must
3662
+ // not become "one of them failed". Waiting on this never rejects, which is
3663
+ // what lets a navigation be waited on (see via_navigation.js).
3634
3664
  const allResult = allThenableArray.length
3635
3665
  ? Promise.allSettled(allThenableArray)
3636
3666
  : null;
@@ -4239,20 +4269,31 @@ const createAction = (callback, rootOptions = {}) => {
4239
4269
  onError?.(error, { event, action, args });
4240
4270
  });
4241
4271
 
4242
- // The error is in errorSignal; from here it is the UI's, and running
4243
- // the action is not the place to decide whether the UI wants it —
4244
- // that answer does not exist yet at this instant (see
4245
- // action_error_report.js). So the run never throws: it settles with
4246
- // the error as its value, and being displayed or not is constated
4247
- // afterwards, in one place, by one rule.
4248
4272
  if (onError) {
4249
4273
  // Asking for the error IS taking it.
4250
4274
  markErrorAsDisplayedBy(error, "onError");
4251
4275
  }
4252
- reportErrorIfNobodyDisplaysIt(error, { action });
4276
+ // Handed back, not delivered: failRun decides whether this leaves as
4277
+ // a throw or as a rejection, because only the caller below knows
4278
+ // which of the two this run was. The error is in errorSignal either
4279
+ // way — a screen reads it there, whether or not anything was waiting.
4253
4280
  return error;
4254
4281
  };
4255
4282
 
4283
+ // A failure leaves the run the way it came: thrown when the callback
4284
+ // threw synchronously — code that knows this action is synchronous
4285
+ // writes a plain `try/catch` around the call and has to find it there —
4286
+ // and rejected when the run was asynchronous, since the throw below
4287
+ // then happens inside a `then`. An abort is neither: it settles with
4288
+ // its reason as its value, the way it always has.
4289
+ const failRun = (error) => {
4290
+ const errorSettled = onRunError(error);
4291
+ if (runningStateSignal.peek() === ABORTED) {
4292
+ return errorSettled;
4293
+ }
4294
+ throw errorSettled;
4295
+ };
4296
+
4256
4297
  try {
4257
4298
  const thenableArray = [];
4258
4299
  const callbackResult = callback(...args);
@@ -4290,12 +4331,12 @@ const createAction = (callback, rootOptions = {}) => {
4290
4331
  }
4291
4332
  return Promise.all(thenableArray).then(() => {
4292
4333
  if (rejected) {
4293
- return onRunError(rejectedValue);
4334
+ return failRun(rejectedValue);
4294
4335
  }
4295
4336
  return onRunEnd();
4296
4337
  });
4297
4338
  } catch (e) {
4298
- return onRunError(e);
4339
+ return failRun(e);
4299
4340
  }
4300
4341
  };
4301
4342
 
@@ -4705,6 +4746,46 @@ const isPlainObject = (obj) => {
4705
4746
  );
4706
4747
  };
4707
4748
 
4749
+ /**
4750
+ * Starts a run navi has nobody to await.
4751
+ *
4752
+ * A failing run does not stay quiet: it throws when the callback threw
4753
+ * synchronously, and rejects when it was asynchronous (see failRun in
4754
+ * actions.js). That is what makes `ACTION(params)` behave like any other call
4755
+ * that can fail — and code that lets such a failure go gets the runtime's own
4756
+ * unhandled-error report, which is the right answer for it.
4757
+ *
4758
+ * Some runs have no such caller by construction — one started from a signal
4759
+ * effect because its params changed, one whose failure the control that started
4760
+ * it already draws, a routing whose result every caller drops. Their failure
4761
+ * would be an anonymous unhandled one, naming the machinery instead of what
4762
+ * failed. They start here, which is why the run itself is passed rather than
4763
+ * its result: both deliveries have to be taken.
4764
+ *
4765
+ * And taking them is what obliges this place to ask the other question: did
4766
+ * anything ever display this error? The answer does not exist yet at this
4767
+ * instant — a route action fails before the page that shows it exists — so it is
4768
+ * asked with a deadline, once, in action_error_report.js.
4769
+ *
4770
+ * The error is never hidden: it stays in the action's `errorSignal`, which is
4771
+ * where a screen reads it.
4772
+ */
4773
+ const runUnwatched = (startRun) => {
4774
+ let result;
4775
+ try {
4776
+ result = startRun();
4777
+ } catch (error) {
4778
+ reportErrorIfNobodyDisplaysIt(error);
4779
+ return undefined;
4780
+ }
4781
+ if (result && typeof result.catch === "function") {
4782
+ result.catch((error) => {
4783
+ reportErrorIfNobodyDisplaysIt(error);
4784
+ });
4785
+ }
4786
+ return result;
4787
+ };
4788
+
4708
4789
  /**
4709
4790
  * Reactively runs an action whenever the params derived from signals change.
4710
4791
  *
@@ -4734,12 +4815,6 @@ const isPlainObject = (obj) => {
4734
4815
  // one — in dev, an error overlay thrown over a page that is already saying what
4735
4816
  // went wrong. Nothing is lost by dropping it: the failure is held by the action
4736
4817
  // itself, and whoever reads it (useAsyncData, <Button action>) is what shows it.
4737
- const runUnwatched = (result) => {
4738
- if (result && typeof result.catch === "function") {
4739
- result.catch(() => {});
4740
- }
4741
- };
4742
-
4743
4818
  const actionRunEffect = (
4744
4819
  action,
4745
4820
  deriveActionParamsFromSignals,
@@ -4783,7 +4858,9 @@ const actionRunEffect = (
4783
4858
  // falsy params, don't run
4784
4859
  return;
4785
4860
  }
4786
- runUnwatched(actionTarget.run({ reason: "truthy params first run" }));
4861
+ runUnwatched(() =>
4862
+ actionTarget.run({ reason: "truthy params first run" }),
4863
+ );
4787
4864
  return;
4788
4865
  }
4789
4866
 
@@ -4800,18 +4877,18 @@ const actionRunEffect = (
4800
4877
  }
4801
4878
  if (!actionTargetPrevious.params) {
4802
4879
  // coming from falsy-params state: action may already be cached, avoid unnecessary rerun
4803
- runUnwatched(
4880
+ runUnwatched(() =>
4804
4881
  actionTarget.run({ reason: "params restored from falsy state" }),
4805
4882
  );
4806
4883
  } else {
4807
- runUnwatched(actionTarget.rerun({ reason: "params modified" }));
4884
+ runUnwatched(() => actionTarget.rerun({ reason: "params modified" }));
4808
4885
  }
4809
4886
  }
4810
4887
  },
4811
4888
  ...options,
4812
4889
  });
4813
4890
  if (actionParamsSignal.peek()) {
4814
- runUnwatched(
4891
+ runUnwatched(() =>
4815
4892
  actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
4816
4893
  );
4817
4894
  }
@@ -5693,6 +5770,16 @@ const ActionRequesterContext = createContext();
5693
5770
  * Whichever mode, the affordance's own handler runs from inside its own gate
5694
5771
  * rather than from the DOM: a caller's `onClick` fires before any of this, and
5695
5772
  * would go off from a button drawn greyed.
5773
+ *
5774
+ * These two answer about the GESTURE. Whose VALUE an element carries is a third
5775
+ * question, and `standalone` is where it is answered (see useUIStateController).
5776
+ * They are said separately because they genuinely come apart: a door that holds
5777
+ * no value of its own can still write into the control around it — it is
5778
+ * `standalone` and must still shut when that control is read-only — and an
5779
+ * affordance can claim a press without ever being a field. Something that is
5780
+ * its own on all three counts says all three (a diskette filing a name into the
5781
+ * reader's own address book: `selfInteractions="click"`,
5782
+ * `whenSelfInteractionsBlocked="ignore"`, `standalone`).
5696
5783
  */
5697
5784
 
5698
5785
 
@@ -10245,6 +10332,35 @@ const watchActionCompletion = (element, dispatchAction) => {
10245
10332
  };
10246
10333
  };
10247
10334
 
10335
+ /**
10336
+ * What follows the control's OWN action: it runs once that action has
10337
+ * succeeded, and not at all otherwise.
10338
+ *
10339
+ * The outcome arrives in one of three shapes and each decides differently:
10340
+ * - the gate turned the request down (`result === false`): nothing happened,
10341
+ * so nothing follows;
10342
+ * - the action is running: what follows waits for it, and an error or an abort
10343
+ * drops it — whatever the action left in front of the user stays;
10344
+ * - it is already settled, or never started at all. A control with nothing to
10345
+ * run leaves no outcome behind, and no outcome means nothing went wrong.
10346
+ */
10347
+ const runWhenActionSucceeded = (completion, callback) => {
10348
+ if (completion.result === false) {
10349
+ return;
10350
+ }
10351
+ if (completion.isRunning) {
10352
+ completion.whenSucceeded(callback);
10353
+ return;
10354
+ }
10355
+ let succeeded = true;
10356
+ completion.whenSettled(({ error, aborted }) => {
10357
+ succeeded = !error && !aborted;
10358
+ });
10359
+ if (succeeded) {
10360
+ callback();
10361
+ }
10362
+ };
10363
+
10248
10364
  const tryActionAfterInteractionAllowed = (
10249
10365
  element,
10250
10366
  {
@@ -12905,7 +13021,10 @@ const useExecuteAction = (
12905
13021
  });
12906
13022
  };
12907
13023
 
12908
- return runAction();
13024
+ // The control is already holding the failure — the error side effect
13025
+ // above drew the callout, or threw it at the boundary — so nothing here
13026
+ // is waiting on the rejection.
13027
+ return runUnwatched(runAction);
12909
13028
  },
12910
13029
  [elementRef, errorEffect],
12911
13030
  );
@@ -17419,15 +17538,11 @@ const useActionAsyncData = (action, {
17419
17538
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
17420
17539
  // component under `loading: true`, exactly as for data someone else ran.
17421
17540
  if (run && action.runningStateSignal.peek() === IDLE && action.paramsSignal.peek() !== undefined) {
17422
- const runResult = action.run({
17541
+ // Nothing waits on this run: the failure is held by the action, and this
17542
+ // hook is what reads it back.
17543
+ runUnwatched(() => action.run({
17423
17544
  reason: "useAsyncData({ run: true })"
17424
- });
17425
- // Nobody awaits this run, and a rejection nobody awaits is an unhandled
17426
- // one — in dev, an overlay over a component already saying what failed.
17427
- // The failure is held by the action, and this hook is what reads it.
17428
- if (runResult && typeof runResult.catch === "function") {
17429
- runResult.catch(() => {});
17430
- }
17545
+ }));
17431
17546
  }
17432
17547
 
17433
17548
  // Use peek() instead of .value to avoid subscribing this component to the signal.
@@ -17538,12 +17653,6 @@ const useActionAsyncData = (action, {
17538
17653
  throw actionError;
17539
17654
  }
17540
17655
 
17541
- // RUNNING with loadingEffect: "use" — return stale data + loading flag, no suspend
17542
- if (loadingEffect === "use" && runningState === RUNNING) {
17543
- const staleData = action.dataSignal.peek();
17544
- return [staleData, true, undefined];
17545
- }
17546
-
17547
17656
  // An action without params has nothing to run and no one to start it: this is
17548
17657
  // where a route action lands when its params getter returns false. It is not a
17549
17658
  // load in progress, so there is nothing to wait for — suspending here would
@@ -17553,6 +17662,17 @@ const useActionAsyncData = (action, {
17553
17662
  return [action.dataSignal.peek(), false, undefined];
17554
17663
  }
17555
17664
 
17665
+ // `loading: true` says the component draws its own wait, so this hook never
17666
+ // suspends: running, or holding params nobody has started yet — a debounced
17667
+ // binding that just retargeted, an aborted run, a run this very render is
17668
+ // about to issue. Either way what is on screen is not the current answer,
17669
+ // which is what `loading` says. Suspending would not draw a spinner: it swaps
17670
+ // the subtree for the boundary, and everything under it remounts — a dialog
17671
+ // open inside it is then closed, by a hook the caller told to stay out of it.
17672
+ if (loadingEffect === "use") {
17673
+ return [action.dataSignal.peek(), true, undefined];
17674
+ }
17675
+
17556
17676
  // IDLE or RUNNING with loadingEffect: "delegate" — suspend
17557
17677
  const reason = runningState === RUNNING ? "loading" : "idle";
17558
17678
  loadingRef.current = {
@@ -26329,18 +26449,12 @@ const setupBrowserIntegrationViaHistory = ({
26329
26449
  // something at the first announcement has a definite place to give it back.
26330
26450
  publishBeforeRouting({ url, ...options });
26331
26451
  try {
26332
- const routingResult = applyRoutingTask(url, options);
26333
- if (routingResult && typeof routingResult.then === "function") {
26334
- // Every caller below drops this value — a click handler has nothing to
26335
- // do with what the routing returns — so a rejection here would become
26336
- // an anonymous unhandled one, pointing at the navigation rather than at
26337
- // what failed. It goes to the single place that knows what to do with
26338
- // an error nobody displays (see action_error_report.js).
26339
- routingResult.catch((e) => {
26340
- reportErrorIfNobodyDisplaysIt(e);
26341
- });
26342
- }
26343
- return routingResult;
26452
+ // Every caller below drops this value — a click handler has nothing to do
26453
+ // with what the routing returns — so a rejection here would become an
26454
+ // anonymous unhandled one, pointing at the navigation rather than at what
26455
+ // failed. Taken, and passed to the one place that knows what to do with
26456
+ // an error nobody displays (see run_unwatched.js).
26457
+ return runUnwatched(() => applyRoutingTask(url, options));
26344
26458
  } finally {
26345
26459
  publishAfterRouting({ url, ...options });
26346
26460
  }
@@ -32755,20 +32869,21 @@ const ParentUIStateControllerContext = createContext();
32755
32869
  */
32756
32870
  const useUIStateController = (
32757
32871
  props,
32758
- {
32759
- controlInfo,
32760
- syncDomState,
32761
- uiActionInternal,
32762
- persists,
32763
- allowNameless = false,
32764
- } = {},
32872
+ { controlInfo, syncDomState, uiActionInternal, persists, standalone } = {},
32765
32873
  ) => {
32766
32874
  const debugPopup = useDebugPopup();
32767
32875
  const debugInteraction = useDebugInteraction();
32768
32876
  const debugUIState = useDebugUIState();
32769
32877
  const debugFocus = useDebugFocus();
32770
32878
 
32771
- const parentUIStateController = useContext(ParentUIStateControllerContext);
32879
+ // The whole of `standalone`: with no parent to register into, the effect
32880
+ // below has nobody to call, and every question a group asks about its
32881
+ // children is asked about someone else. Read here rather than filtered by
32882
+ // each group shape in turn, so a control that answers for itself is out of
32883
+ // the value on the way up AND out of the reset, the validation cascade and
32884
+ // the distribution on the way down.
32885
+ const parentFromContext = useContext(ParentUIStateControllerContext);
32886
+ const parentUIStateController = standalone ? undefined : parentFromContext;
32772
32887
  const formContext = useContext(FormContext);
32773
32888
  if (persists === undefined && formContext) {
32774
32889
  persists = true;
@@ -32842,7 +32957,6 @@ const useUIStateController = (
32842
32957
  parentUIStateController,
32843
32958
  parentUiStateSignalHolder,
32844
32959
  isProxy,
32845
- allowNameless,
32846
32960
  emptyUIState,
32847
32961
  // Set here too, not only in `update` below: a control rendered once and
32848
32962
  // never re-rendered would otherwise never say whether it was GIVEN a
@@ -33508,18 +33622,14 @@ const GROUP_DEFAULTS = {
33508
33622
  single: {
33509
33623
  // The same exclusions canRegisterAsFacadeChild already makes below (the
33510
33624
  // picker façade asked the very same question: which child IS the value).
33511
- // Buttons and links never hold one, and neither does a control that
33512
- // declared itself nameless. A control *carrying* navi-list is the search
33513
- // box driving some other list — not the list itself, which stays a
33625
+ // Buttons and links never hold one. A control *carrying* navi-list is the
33626
+ // search box driving some other list — not the list itself, which stays a
33514
33627
  // perfectly good single value here (one item, or the array a multiple list
33515
33628
  // exposes). Excluding the searcher is what leaves the list alone.
33516
33629
  childControlFilter: (child) => {
33517
33630
  if (child.controlType === "button" || child.controlType === "link") {
33518
33631
  return false;
33519
33632
  }
33520
- if (child.allowNameless) {
33521
- return false;
33522
- }
33523
33633
  if (child.props?.["navi-list"]) {
33524
33634
  return false;
33525
33635
  }
@@ -33544,7 +33654,7 @@ const GROUP_DEFAULTS = {
33544
33654
  aggregateChildStates: (children) => {
33545
33655
  const groupValues = {};
33546
33656
  for (const child of children) {
33547
- const { name, allowNameless, emptyUIState } = child;
33657
+ const { name, emptyUIState } = child;
33548
33658
  // A control holding nothing writes its own empty, not a hole: the key
33549
33659
  // is in the object either way, and what is read from it keeps the shape
33550
33660
  // the reader was promised (see resolveEmptyUIState).
@@ -33553,13 +33663,6 @@ const GROUP_DEFAULTS = {
33553
33663
  ? emptyUIState
33554
33664
  : child.uiState;
33555
33665
  if (!name) {
33556
- if (allowNameless) {
33557
- // A control that says it is not a field is not one, whatever it
33558
- // holds: a picker used as a door holds the shape its popup draws,
33559
- // and merging that in would put the popup's keys in the object as
33560
- // if the door had been a group.
33561
- continue;
33562
- }
33563
33666
  // A nameless GROUP is a grouping, not a value: it exists to hold its
33564
33667
  // children together (a WheelGroup sharing navigation, a fieldset-ish
33565
33668
  // cluster) without claiming a key of its own, so what it holds is
@@ -33672,6 +33775,7 @@ const useUIGroupStateController = (
33672
33775
  uiActionInternal,
33673
33776
  allowCapture = false,
33674
33777
  cascadeValidationToChildren = false,
33778
+ standalone,
33675
33779
  },
33676
33780
  ) => {
33677
33781
  const debugPopup = useDebugPopup();
@@ -33707,7 +33811,11 @@ const useUIGroupStateController = (
33707
33811
  `Either use a known controlType/stateType or provide aggregateChildStates and distributeChildUIState explicitly.`,
33708
33812
  );
33709
33813
  }
33710
- const parentUIStateController = useContext(ParentUIStateControllerContext);
33814
+ // See the leaf controller's own `standalone` above: the same opt-out, and a
33815
+ // group taking it also takes its whole subtree out — the controls inside it
33816
+ // register into IT, and it registers into nobody.
33817
+ const parentFromContext = useContext(ParentUIStateControllerContext);
33818
+ const parentUIStateController = standalone ? undefined : parentFromContext;
33711
33819
  // A bound signal seeds the group the way `defaultValue` does — uncontrolled,
33712
33820
  // with the signal's current value as what it starts on. Write-back is handled
33713
33821
  // by applyState's own boundSignal; the read half is below: children are
@@ -34478,7 +34586,6 @@ const EMPTY_OBJECT = {};
34478
34586
  */
34479
34587
  const useUIFacadeStateController = (props, realUIStateController) => {
34480
34588
  const firstChildControllerRef = useRef(null);
34481
- const namelessChildSetRef = useRef(new Set());
34482
34589
  const updatingRef = useRef(false);
34483
34590
  const debugPopup = useDebugPopup();
34484
34591
  const debugInteraction = useDebugInteraction();
@@ -34506,16 +34613,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
34506
34613
  if (childController.isProxy) {
34507
34614
  return false;
34508
34615
  }
34509
- if (childController.allowNameless) {
34510
- // A control saying it is not a field is not the one the picker talks
34511
- // to: the search box above the list, the "select all" switch beside
34512
- // it. It is there to help find the answer, not to be it.
34513
- namelessChildSetRef.current.add(childController);
34514
- return false;
34515
- }
34516
34616
  if (childController.props["navi-list"]) {
34517
- // Controls with navi-list act as standalone list navigators and should
34518
- // not be treated as the picker's synced child.
34617
+ // Controls with navi-list act as list navigators driving some other
34618
+ // list, not as the picker's synced child.
34519
34619
  return false;
34520
34620
  }
34521
34621
  return true;
@@ -34533,10 +34633,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
34533
34633
  }
34534
34634
  const child = firstChildControllerRef.current;
34535
34635
  if (!child) {
34536
- warnPopupHasNothingButNamelessControls(
34537
- props,
34538
- namelessChildSetRef.current,
34539
- );
34540
34636
  return;
34541
34637
  }
34542
34638
  updatingRef.current = true;
@@ -34571,7 +34667,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
34571
34667
  `A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
34572
34668
  `so this one is neither filled nor collected. ` +
34573
34669
  `A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object". ` +
34574
- `A control that is there to FIND the answer rather than be it (a search box, a "select all") says so with allowNameless and steps out of the way.`,
34670
+ `A control that answers for itself rather than for the picker — a search box, a list acting on every touch — says so with standalone and steps out of the way.`,
34575
34671
  child,
34576
34672
  );
34577
34673
  } else {
@@ -34698,11 +34794,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
34698
34794
 
34699
34795
  const describePicker = (props) =>
34700
34796
  `<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
34701
- const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
34702
- {
34703
- return;
34704
- }
34705
- };
34706
34797
 
34707
34798
  /**
34708
34799
  * Returns true when `e` should trigger parent notification (child → parent bubbling).
@@ -34975,19 +35066,19 @@ const ControlgroupChildrenWrapper = ({
34975
35066
  */
34976
35067
  const useControlProps = (props, {
34977
35068
  controlType,
34978
- allowNameless: allowNamelessByDefault,
34979
35069
  persists,
34980
35070
  uiActionInternal
34981
35071
  }) => {
34982
35072
  const debugUIState = useDebugUIState();
34983
35073
  const debugAction = useDebugAction();
34984
35074
 
34985
- // A control that is not a field: it opens something, it goes somewhere, and
34986
- // the group around it must expect no value from it — no name, and no warning
34987
- // about the missing name. Buttons and links say so from inside navi; the prop
34988
- // is how a control used as a door says the same thing from the outside.
34989
- const allowNameless = props.allowNameless ?? allowNamelessByDefault;
34990
- delete props.allowNameless;
35075
+ // This control answers for itself: whatever group it sits in expects nothing
35076
+ // from it and gives it nothing back. See useUIStateController, which is where
35077
+ // the whole of it happens.
35078
+ const {
35079
+ standalone
35080
+ } = props;
35081
+ delete props.standalone;
34991
35082
  const idDefault = useId();
34992
35083
  const controlId = useContext(ControlIdContext);
34993
35084
  props.id = props.id || controlId || idDefault;
@@ -35043,7 +35134,7 @@ const useControlProps = (props, {
35043
35134
  const uiStateController = useUIStateController(props, {
35044
35135
  controlInfo,
35045
35136
  syncDomState,
35046
- allowNameless,
35137
+ standalone,
35047
35138
  persists,
35048
35139
  uiActionInternal
35049
35140
  });
@@ -35242,28 +35333,7 @@ const useControlProps = (props, {
35242
35333
  if (!deferredCommand) {
35243
35334
  return;
35244
35335
  }
35245
- if (completion.result === false) {
35246
- // The action was turned down (a failing constraint, a gate saying
35247
- // no) — nothing happened, so nothing follows.
35248
- return;
35249
- }
35250
- if (completion.isRunning) {
35251
- completion.whenSucceeded(deferredCommand);
35252
- return;
35253
- }
35254
- // Synchronous: already settled, and how it ended still decides. An
35255
- // action that never started (nothing to run) leaves no outcome, and
35256
- // the command runs as it always did.
35257
- let succeeded = true;
35258
- completion.whenSettled(({
35259
- error,
35260
- aborted
35261
- }) => {
35262
- succeeded = !error && !aborted;
35263
- });
35264
- if (succeeded) {
35265
- deferredCommand();
35266
- }
35336
+ runWhenActionSucceeded(completion, deferredCommand);
35267
35337
  };
35268
35338
  return {
35269
35339
  keyDown: keyDownDefault,
@@ -36024,8 +36094,10 @@ const useControlgroupProps = (props, {
36024
36094
  cascadeValidationToChildren = false
36025
36095
  }) => {
36026
36096
  const {
36027
- action
36097
+ action,
36098
+ standalone
36028
36099
  } = props;
36100
+ delete props.standalone;
36029
36101
  const uiGroupStateController = useUIGroupStateController(props, controlType, {
36030
36102
  stateType,
36031
36103
  childControlFilter,
@@ -36035,7 +36107,8 @@ const useControlgroupProps = (props, {
36035
36107
  wantRequesterButtonState,
36036
36108
  uiActionInternal,
36037
36109
  allowCapture,
36038
- cascadeValidationToChildren
36110
+ cascadeValidationToChildren,
36111
+ standalone
36039
36112
  });
36040
36113
  const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
36041
36114
  // Mirror single-input behaviour: a controlled value with nobody listening
@@ -40403,8 +40476,7 @@ const LinkPlain = props => {
40403
40476
  selectionController
40404
40477
  });
40405
40478
  const [controlRootProps, controlHostProps] = useControlProps(props, {
40406
- controlType: "link",
40407
- allowNameless: true
40479
+ controlType: "link"
40408
40480
  });
40409
40481
  const {
40410
40482
  basePseudoState
@@ -40836,6 +40908,10 @@ let navCount = 0;
40836
40908
  // Worn by a nav of routes while a route movement between two of its tabs is
40837
40909
  // pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
40838
40910
  const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
40911
+ // What one notch of a wheel that counts in LINES is worth on a row that counts
40912
+ // in pixels. A rough line, deliberately: nothing here needs to agree with a
40913
+ // paragraph, only to move the row about as far as a notch moves a page.
40914
+ const WHEEL_LINE_SIZE = 16;
40839
40915
  const css$W = /* css */`@layer navi {
40840
40916
  .navi_nav {
40841
40917
  --nav-border: none;
@@ -40919,6 +40995,14 @@ const css$W = /* css */`@layer navi {
40919
40995
  justify-content: stretch;
40920
40996
  display: flex;
40921
40997
 
40998
+ &[data-scrollable] {
40999
+ max-width: 100%;
41000
+ }
41001
+
41002
+ &[data-scrollable][data-vertical] {
41003
+ max-height: 100%;
41004
+ }
41005
+
40922
41006
  & .navi_link {
40923
41007
  user-select: none;
40924
41008
  --x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
@@ -41087,6 +41171,39 @@ const Nav = ({
41087
41171
  const slideContainerElementRef = useRef(null);
41088
41172
  const indicatorPosition = slideContainer ? positionOfCurrentIndicator(currentIndicator, vertical) : null;
41089
41173
 
41174
+ // Which tab of this row is the one being shown: the slide the container says
41175
+ // it is on, or, for a row of routes, the link the browser is on.
41176
+ const getCurrentTabElement = () => {
41177
+ const navElement = navRef.current;
41178
+ if (!navElement) {
41179
+ return null;
41180
+ }
41181
+ const containerElement = slideContainerElementRef.current;
41182
+ if (!containerElement) {
41183
+ return navElement.querySelector("[data-href-current]");
41184
+ }
41185
+ const currentArea = containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE);
41186
+ if (currentArea === null) {
41187
+ return null;
41188
+ }
41189
+ return navElement.querySelector(`[data-slide-target="${CSS.escape(currentArea)}"]`);
41190
+ };
41191
+ // …and which one the picture leans on while it is not on the current one:
41192
+ // the slide a finger is pulling in, the slide an animation is leaving. There
41193
+ // is one only while a travel is playing.
41194
+ const getTowardTabElement = () => {
41195
+ const navElement = navRef.current;
41196
+ const containerElement = slideContainerElementRef.current;
41197
+ if (!navElement || !containerElement) {
41198
+ return null;
41199
+ }
41200
+ const towardArea = containerElement.getAttribute(SLIDE_TOWARD_ATTRIBUTE);
41201
+ if (towardArea === null) {
41202
+ return null;
41203
+ }
41204
+ return navElement.querySelector(`[data-slide-target="${CSS.escape(towardArea)}"]`);
41205
+ };
41206
+
41090
41207
  // Where the trait is and where it is headed, as four numbers of pixels the
41091
41208
  // CSS above interpolates between (see the .navi_nav_indicator rules). Written
41092
41209
  // by hand rather than rendered: it is read off the row as it stands, and the
@@ -41100,8 +41217,7 @@ const Nav = ({
41100
41217
  }
41101
41218
  const tabElements = Array.from(navElement.querySelectorAll("[data-slide-target]"));
41102
41219
  const areaOf = tabElement => tabElement.getAttribute("data-slide-target");
41103
- const currentArea = containerElement.getAttribute("data-slide-current");
41104
- const currentIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === currentArea);
41220
+ const currentIndex = tabElements.indexOf(getCurrentTabElement());
41105
41221
  if (currentIndex === -1) {
41106
41222
  // On a slide no tab in this row names: there is no tab to sit under.
41107
41223
  navElement.removeAttribute("data-nav-indicator-measured");
@@ -41143,6 +41259,226 @@ const Nav = ({
41143
41259
  // about the row as it is now.
41144
41260
  const paintIndicatorGeometryRef = useRef(null);
41145
41261
  paintIndicatorGeometryRef.current = paintIndicatorGeometry;
41262
+
41263
+ // A row of tabs wider than the box holding it scrolls (the caller asks for
41264
+ // that with overflowX) — and then saying which tab one is on is not enough,
41265
+ // that tab must be somewhere one can see. The trait needs nothing here: it
41266
+ // is placed with offsetLeft, inside the row, so it rides along.
41267
+ // The ROW is what gets scrolled, never the tab: element.scrollIntoView()
41268
+ // walks every scrolling ancestor it finds, and a tab row lives in sheets
41269
+ // that sit on pages that scroll — bringing a tab into sight must move the
41270
+ // row and nothing else.
41271
+ const tabInSightRef = useRef(null);
41272
+ const keepCurrentTabInSight = () => {
41273
+ const navElement = navRef.current;
41274
+ const currentTabElement = getCurrentTabElement();
41275
+ // Brought into sight once, and then left where it is: the row scrolls, so
41276
+ // the user may have pushed it elsewhere since to read the other tabs, and
41277
+ // a render that changed none of this has no business taking that back.
41278
+ if (currentTabElement === tabInSightRef.current) {
41279
+ return;
41280
+ }
41281
+ tabInSightRef.current = currentTabElement;
41282
+ if (!currentTabElement) {
41283
+ return;
41284
+ }
41285
+ scrollIntoViewScoped(currentTabElement, {
41286
+ container: navElement
41287
+ });
41288
+ };
41289
+
41290
+ // …and while a travel is playing the row gets there AT THE PACE OF THE
41291
+ // TRAVEL, off the very number the trait is drawn from: the container
41292
+ // publishes how far between the two slides the picture stands
41293
+ // (--slide-travel-progress, written on this row — see followerElements), and
41294
+ // the row is scrolled the same fraction of the way between the two tabs. The
41295
+ // slides gliding while the tabs jump at the end is one movement told twice.
41296
+ //
41297
+ // Read frame by frame, because that number is nobody's news: under a finger
41298
+ // the container writes it per frame with no announcement (a write is not a
41299
+ // change), and for a travel that was asked for it is the browser that
41300
+ // animates it. So the row samples it, exactly as the trait does — except that
41301
+ // a scroll offset is not a thing CSS can be handed.
41302
+ const travelScrollRef = useRef(null);
41303
+ const syncTabScroll = ({
41304
+ afterAFrame
41305
+ } = {}) => {
41306
+ const navElement = navRef.current;
41307
+ if (!navElement) {
41308
+ return;
41309
+ }
41310
+ const currentTabElement = getCurrentTabElement();
41311
+ const towardTabElement = getTowardTabElement();
41312
+ if (!currentTabElement || !towardTabElement) {
41313
+ travelScrollRef.current = null;
41314
+ // Nothing is travelling — but a travel that was ASKED for is announced a
41315
+ // breath after the slide it lands on, so a row that lands the moment the
41316
+ // current tab changes lands ahead of the movement it should have ridden,
41317
+ // and the slides then glide towards a row already there. Giving it a
41318
+ // frame costs nothing: the slides have not moved either.
41319
+ // Only for a row of slides, and only once — a row of routes has no travel
41320
+ // to ride, and the second look is the one that lands.
41321
+ if (slideContainer && !afterAFrame && tabInSightRef.current && tabInSightRef.current !== currentTabElement) {
41322
+ scheduleTabScrollSync({
41323
+ nextFrame: true
41324
+ });
41325
+ return;
41326
+ }
41327
+ // Nowhere between: the row lands where it belongs.
41328
+ keepCurrentTabInSight();
41329
+ return;
41330
+ }
41331
+ let travel = travelScrollRef.current;
41332
+ if (!travel || travel.from !== currentTabElement || travel.toward !== towardTabElement) {
41333
+ // The two ends, measured once for this travel: measuring them again on a
41334
+ // row that has since moved would be measuring from the answer.
41335
+ const fromOffsets = getScrollIntoViewScopedOffsets(currentTabElement, {
41336
+ container: navElement
41337
+ });
41338
+ const towardOffsets = getScrollIntoViewScopedOffsets(towardTabElement, {
41339
+ container: navElement
41340
+ });
41341
+ // Signed the way the container counts: +1 when the picture leans on a
41342
+ // slide sitting BEFORE the current one, -1 when it sits after — the same
41343
+ // reading paintIndicatorGeometry does of the same number.
41344
+ const sign = currentTabElement.compareDocumentPosition(towardTabElement) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
41345
+ travel = {
41346
+ from: currentTabElement,
41347
+ toward: towardTabElement,
41348
+ left: fromOffsets.left,
41349
+ top: fromOffsets.top,
41350
+ leftDelta: (towardOffsets.left - fromOffsets.left) * sign,
41351
+ topDelta: (towardOffsets.top - fromOffsets.top) * sign
41352
+ };
41353
+ travelScrollRef.current = travel;
41354
+ }
41355
+ const progress = parseFloat(getComputedStyle(navElement).getPropertyValue("--slide-travel-progress")) || 0;
41356
+ navElement.scrollTo({
41357
+ left: travel.left + progress * travel.leftDelta,
41358
+ top: travel.top + progress * travel.topDelta,
41359
+ // The row is being driven frame by frame: a caller who asked for smooth
41360
+ // scrolling asked it of a scroll that lands, not of one that is held.
41361
+ behavior: "instant"
41362
+ });
41363
+ scheduleTabScrollSync({
41364
+ nextFrame: true
41365
+ });
41366
+ };
41367
+ const syncTabScrollRef = useRef(null);
41368
+ syncTabScrollRef.current = syncTabScroll;
41369
+
41370
+ // When to look: on the next frame while a travel is being followed, and
41371
+ // otherwise at the end of the current task. A MICROTASK and not a frame,
41372
+ // because the two are told apart in the same breath — a travel that was
41373
+ // asked for announces the slide it lands on before it announces the one it
41374
+ // is leaving, so a row settling straight away would jump to the destination
41375
+ // and only then be asked to travel there. And it is still before the paint,
41376
+ // which a frame is not: a row that opens on a far tab must be scrolled
41377
+ // already the first time it is seen.
41378
+ const syncScheduledRef = useRef(false);
41379
+ const syncFrameRef = useRef(0);
41380
+ const scheduleTabScrollSync = ({
41381
+ nextFrame
41382
+ } = {}) => {
41383
+ if (syncScheduledRef.current) {
41384
+ return;
41385
+ }
41386
+ syncScheduledRef.current = true;
41387
+ const run = () => {
41388
+ syncScheduledRef.current = false;
41389
+ syncFrameRef.current = 0;
41390
+ syncTabScrollRef.current({
41391
+ afterAFrame: nextFrame
41392
+ });
41393
+ };
41394
+ if (nextFrame) {
41395
+ syncFrameRef.current = requestAnimationFrame(run);
41396
+ } else {
41397
+ queueMicrotask(run);
41398
+ }
41399
+ };
41400
+ const scheduleTabScrollSyncRef = useRef(null);
41401
+ scheduleTabScrollSyncRef.current = scheduleTabScrollSync;
41402
+ useLayoutEffect(() => {
41403
+ return () => {
41404
+ cancelAnimationFrame(syncFrameRef.current);
41405
+ };
41406
+ }, []);
41407
+
41408
+ // The row watches its own size and reads itself again when it changes — a
41409
+ // badge count, a font that just arrived, a window resized: what was written
41410
+ // is pixels, where the trait sits and how far the row is scrolled, and
41411
+ // pixels measured on a row of another width are wrong. A row that had no
41412
+ // width at all is the same story with a sharper edge: inside a closed
41413
+ // <dialog> everything measures 0, and the current tab does not change when
41414
+ // the dialog opens, so nothing else would ever look again.
41415
+ // A scroll takes no size from anything, so this stays quiet while the row is
41416
+ // being pushed around by hand.
41417
+ useLayoutEffect(() => {
41418
+ const sizeObserver = new ResizeObserver(() => {
41419
+ paintIndicatorGeometryRef.current();
41420
+ // What was brought into sight was brought there for another row.
41421
+ tabInSightRef.current = null;
41422
+ scheduleTabScrollSyncRef.current();
41423
+ });
41424
+ sizeObserver.observe(navRef.current);
41425
+ return () => {
41426
+ sizeObserver.disconnect();
41427
+ };
41428
+ }, []);
41429
+
41430
+ // A row that scrolls sideways, and a mouse that only turns one way. A
41431
+ // vertical wheel is left to the vertical axis, which a row of tabs does not
41432
+ // have: the push goes past the row to the page, and the tabs stay where they
41433
+ // were — a row a trackpad can push around and a mouse cannot reach at all.
41434
+ // Up and further along are the same wish here, so the push moves the row.
41435
+ // Only for a row that scrolls on ONE axis: one that scrolls the way the
41436
+ // wheel turns already has the answer it wants, and taking the push would be
41437
+ // answering it twice.
41438
+ useLayoutEffect(() => {
41439
+ const navElement = navRef.current;
41440
+ const onWheel = wheelEvent => {
41441
+ // The burst is already somebody else's — a box travelling under the
41442
+ // pointer, a wheel picker — and stays theirs until it dies out, wherever
41443
+ // the pointer has drifted since (see wheel_gesture.js).
41444
+ if (wheelGestureIsTakenFrom(navElement)) {
41445
+ return;
41446
+ }
41447
+ const {
41448
+ deltaX,
41449
+ deltaY
41450
+ } = wheelEvent;
41451
+ if (Math.abs(deltaY) <= Math.abs(deltaX)) {
41452
+ // Sideways already: the browser knows where to put it.
41453
+ return;
41454
+ }
41455
+ const scrollableWidth = navElement.scrollWidth - navElement.clientWidth;
41456
+ const scrollableHeight = navElement.scrollHeight - navElement.clientHeight;
41457
+ if (scrollableWidth <= 0 || scrollableHeight > 0) {
41458
+ return;
41459
+ }
41460
+ // Not every wheel speaks in pixels: some count lines, some count pages.
41461
+ const notch = wheelEvent.deltaMode === 1 ? WHEEL_LINE_SIZE : wheelEvent.deltaMode === 2 ? navElement.clientWidth : 1;
41462
+ const scrollLeftBefore = navElement.scrollLeft;
41463
+ const scrollLeftWanted = Math.max(0, Math.min(scrollableWidth, scrollLeftBefore + deltaY * notch));
41464
+ if (scrollLeftWanted === scrollLeftBefore) {
41465
+ // The row is at its end and the hand is still pushing: what is left
41466
+ // over is the page's, exactly as it would have been without any of
41467
+ // this.
41468
+ return;
41469
+ }
41470
+ wheelEvent.preventDefault();
41471
+ claimWheelGesture(navElement);
41472
+ navElement.scrollLeft = scrollLeftWanted;
41473
+ };
41474
+ navElement.addEventListener("wheel", onWheel, {
41475
+ passive: false
41476
+ });
41477
+ return () => {
41478
+ navElement.removeEventListener("wheel", onWheel);
41479
+ releaseWheelGesture(navElement);
41480
+ };
41481
+ }, []);
41146
41482
  useLayoutEffect(() => {
41147
41483
  if (!slideContainer) {
41148
41484
  return undefined;
@@ -41156,6 +41492,7 @@ const Nav = ({
41156
41492
  const readContainer = () => {
41157
41493
  setCurrentSlideArea(containerElement.getAttribute(SLIDE_CURRENT_ATTRIBUTE) ?? undefined);
41158
41494
  paintIndicatorGeometryRef.current();
41495
+ scheduleTabScrollSyncRef.current();
41159
41496
  };
41160
41497
  readContainer();
41161
41498
  // Where one is and what the picture leans on are written on the container,
@@ -41165,16 +41502,8 @@ const Nav = ({
41165
41502
  // because a write is not a change — the attribute the trait follows is
41166
41503
  // written on every frame of a gesture with the same value in it.
41167
41504
  containerElement.addEventListener(SLIDE_STATE_EVENT, readContainer);
41168
- // A row whose tabs changed width — a badge count, a font that just
41169
- // arrived, a window resized — is measured again: what was written is
41170
- // pixels, and pixels go stale.
41171
- const sizeObserver = new ResizeObserver(() => {
41172
- paintIndicatorGeometryRef.current();
41173
- });
41174
- sizeObserver.observe(navRef.current);
41175
41505
  return () => {
41176
41506
  containerElement.removeEventListener(SLIDE_STATE_EVENT, readContainer);
41177
- sizeObserver.disconnect();
41178
41507
  slideContainerElementRef.current = null;
41179
41508
  };
41180
41509
  }, [slideContainer]);
@@ -41183,6 +41512,7 @@ const Nav = ({
41183
41512
  // and no observer above watches this row's own children.
41184
41513
  useLayoutEffect(() => {
41185
41514
  paintIndicatorGeometry();
41515
+ scheduleTabScrollSync();
41186
41516
  });
41187
41517
 
41188
41518
  // Whether the bar keeps its name for the route movement about to be pictured
@@ -43850,7 +44180,7 @@ const Expandable = props => {
43850
44180
  }
43851
44181
  if (hasAction) {
43852
44182
  if (nextOpen) {
43853
- effectiveAction.run();
44183
+ runUnwatched(() => effectiveAction.run());
43854
44184
  } else {
43855
44185
  effectiveAction.abort();
43856
44186
  }
@@ -44001,7 +44331,7 @@ const Expandable = props => {
44001
44331
  // Mounted already open: the content is visible, its data is due.
44002
44332
  useEffect(() => {
44003
44333
  if (openedRef.current && hasAction) {
44004
- effectiveAction.run();
44334
+ runUnwatched(() => effectiveAction.run());
44005
44335
  }
44006
44336
  }, []);
44007
44337
  const onRootKeyDown = keyboardEvent => {
@@ -44817,8 +45147,7 @@ const ButtonUI = props => {
44817
45147
  loadingOutline = true
44818
45148
  } = props;
44819
45149
  const [buttonControlRootProps, buttonControlHostProps, controlChildrenWrapperProps] = useControlProps(props, {
44820
- controlType: "button",
44821
- allowNameless: true
45150
+ controlType: "button"
44822
45151
  });
44823
45152
  const {
44824
45153
  basePseudoState,
@@ -45213,6 +45542,7 @@ const css$O = /* css */`.navi_control_swap {
45213
45542
 
45214
45543
  & > .navi_control_swap_slot {
45215
45544
  flex: 0 0 100cqw;
45545
+ min-width: 0;
45216
45546
  display: flex;
45217
45547
 
45218
45548
  & > * {
@@ -49882,30 +50212,18 @@ const Form = props => {
49882
50212
  // is a different component: same group, no <form> element and none of the
49883
50213
  // browser machinery that comes with it.
49884
50214
  const isNested = Boolean(useContext(FormContext));
49885
- const form = isNested ? jsx(FormNested, {
50215
+ return isNested ? jsx(FormNested, {
49886
50216
  ...props
49887
50217
  }) : jsx(FormControl, {
49888
50218
  ...props
49889
50219
  });
49890
- if (props.standalone) {
49891
- // Nothing above to register with: the group hooks read the parent from
49892
- // this context, so emptying it here is the whole opt-out.
49893
- return jsx(ParentUIStateControllerContext.Provider, {
49894
- value: undefined,
49895
- children: form
49896
- });
49897
- }
49898
- return form;
49899
50220
  };
49900
50221
 
49901
50222
  // What both forms are made of: one group, one context for what is inside it.
49902
- // standalone is read by Form above and never goes further — least of all to the
49903
- // DOM.
49904
50223
  const useFormGroup = props => {
49905
50224
  const propsForGroup = {
49906
50225
  ...props
49907
50226
  };
49908
- delete propsForGroup.standalone;
49909
50227
  delete propsForGroup.canSendWhileUnchanged;
49910
50228
  delete propsForGroup.pristineKey;
49911
50229
  // Not the generic control `command`, which a control triggers on its own ui
@@ -58795,13 +59113,13 @@ const PickerCustomResolver = props => {
58795
59113
  if (circle) {
58796
59114
  props.variant = "icon";
58797
59115
  }
58798
- // A door, never a field (see `allowNameless`): the form around it expects
58799
- // no value from it, and no name. So the trigger answers to "button" — it is
58800
- // pressed to read something, never typed into — the same thing a confirm
58801
- // picker says through `picksNothing` (see picker.jsx). Said as the role
58802
- // here rather than through that prop: `picksNothing` also takes the right
58803
- // slot away, and the status icon a callout trigger draws sits in it.
58804
- props.allowNameless = true;
59116
+ // A door, never a field (see `standalone`): the form around it expects no
59117
+ // value from it. So the trigger answers to "button" — it is pressed to read
59118
+ // something, never typed into — the same thing a confirm picker says
59119
+ // through `picksNothing` (see picker.jsx). Said as the role here rather
59120
+ // than through that prop: `picksNothing` also takes the right slot away,
59121
+ // and the status icon a callout trigger draws sits in it.
59122
+ props.standalone = true;
58805
59123
  if (props.role === undefined) {
58806
59124
  props.role = "button";
58807
59125
  }
@@ -59643,7 +59961,8 @@ installImportMetaCssBuild(import.meta);/**
59643
59961
  *
59644
59962
  * Yes is said with `--navi-confirm` (see commands.js). The popup closes on it,
59645
59963
  * and only then does the picker do what it was standing in for — run its
59646
- * `action`, or trigger its `command` — so the work runs on the trigger, where
59964
+ * `action`, trigger its `command`, or both, the command following the action
59965
+ * and only if it succeeded — so the work runs on the trigger, where
59647
59966
  * the loading state and the error callout are drawn, and where the user is
59648
59967
  * looking again. Anything else that closes the popup (the cancel button,
59649
59968
  * Escape, a click outside) is no.
@@ -59705,21 +60024,31 @@ const PickerConfirmResolver = props => {
59705
60024
  // trigger.
59706
60025
  const onConfirm = confirmEvent => {
59707
60026
  const inputEl = getPickerInput(ref.current);
59708
- if (action !== undefined) {
59709
- dispatchRequestAction(inputEl, {
59710
- event: confirmEvent,
59711
- name: "confirm"
59712
- });
60027
+ const runCommand = () => {
60028
+ triggerNaviCommand(inputEl, command, confirmEvent);
60029
+ };
60030
+ if (action === undefined) {
60031
+ if (command) {
60032
+ runCommand();
60033
+ }
59713
60034
  return;
59714
60035
  }
60036
+ // Both props on one trigger means "do this, then go there": the action runs
60037
+ // and the command follows it, only if it worked — a delete that fails must
60038
+ // not send the user to the list it did not leave. Same rule a `<Button
60039
+ // action command>` has (see onButtonInteractionAllowed in control_hooks).
60040
+ const completion = watchActionCompletion(findControlHost(inputEl) || inputEl, () => dispatchRequestAction(inputEl, {
60041
+ event: confirmEvent,
60042
+ name: "confirm"
60043
+ }));
59715
60044
  if (command) {
59716
- triggerNaviCommand(inputEl, command, confirmEvent);
60045
+ runWhenActionSucceeded(completion, runCommand);
59717
60046
  }
59718
60047
  };
59719
60048
  return jsx(Next, {
59720
60049
  ...props,
59721
60050
  type: "navi_js",
59722
- allowNameless: true
60051
+ standalone: true
59723
60052
  // No chevron, no clear cross, whatever variant the trigger is drawn in:
59724
60053
  // both announce a value, and a question holds none.
59725
60054
  ,
@@ -62119,6 +62448,7 @@ const ListFirstResolver = props => {
62119
62448
  * deselectable?: boolean,
62120
62449
  * maxLength?: number,
62121
62450
  * maxLengthGuard?: number,
62451
+ * standalone?: boolean,
62122
62452
  * action?: (value: any) => void,
62123
62453
  * uiAction?: (value: any) => void,
62124
62454
  * popover?: boolean,
@@ -62294,6 +62624,12 @@ const ListFirstResolver = props => {
62294
62624
  * taking — and `uiAction` is not called. The selected ones stay takeable
62295
62625
  * back, so a selection that arrived too long can always be brought back
62296
62626
  * under the limit. Implies `maxLength` for validity.
62627
+ * @param {boolean} [props.standalone]
62628
+ * This list answers for itself: it does not register with the control group
62629
+ * or picker around it, so its selection stays out of that value and nothing
62630
+ * coming down — a distributed value, a reset — reaches it. What a popup whose
62631
+ * one answer is spread over several lists says, so that none of them is taken
62632
+ * for the answer itself.
62297
62633
  */
62298
62634
  const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
62299
62635
  const ListContent = ({
@@ -66060,8 +66396,7 @@ const BadgeButtonUI = props => {
66060
66396
  const defaultRef = useRef();
66061
66397
  props.ref = props.ref || defaultRef;
66062
66398
  const [buttonRootProps, buttonHostProps] = useControlProps(props, {
66063
- controlType: "button",
66064
- allowNameless: true
66399
+ controlType: "button"
66065
66400
  });
66066
66401
  return jsx(Text, {
66067
66402
  className: "navi_badge_button",
@@ -70725,7 +71060,7 @@ const SplitButton = props => {
70725
71060
  }), jsx(Picker, {
70726
71061
  id: menuId,
70727
71062
  variant: "headless",
70728
- allowNameless: true,
71063
+ standalone: true,
70729
71064
  anchor: rootRef,
70730
71065
  ...popupProps,
70731
71066
  readOnly: readOnly,