@jsenv/navi 0.29.316 → 0.29.317

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,12 +4,12 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, refuseDragTo, startDragTo, installPanZoom, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
11
11
  import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
12
- import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
+ import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
13
13
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
14
14
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
15
15
  import { TYPE_RULE, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, createValidity, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, CHAR_CLASS_PRESETS, parseDuration, durationToISOString } from "@jsenv/validity";
@@ -191,9 +191,7 @@ const executeWithCleanup = (fn, cleanup) => {
191
191
  }
192
192
  };
193
193
 
194
- let DEBUG$3 = false;
195
194
  const enableDebugOnDocumentLoading = () => {
196
- DEBUG$3 = true;
197
195
  };
198
196
 
199
197
  const windowIsLoadingSignal = signal(true);
@@ -213,20 +211,9 @@ const [
213
211
  removeFromDocumentLoadingRouteArraySignal,
214
212
  ] = arraySignal([]);
215
213
  const routingWhile = (fn, routeNames = []) => {
216
- if (DEBUG$3 && routeNames.length > 0) {
217
- console.debug(`routingWhile: Adding routes to loading state:`, routeNames);
218
- }
219
214
  addToDocumentLoadingRouteArraySignal(...routeNames);
220
215
  return executeWithCleanup(fn, () => {
221
216
  removeFromDocumentLoadingRouteArraySignal(...routeNames);
222
- if (DEBUG$3 && routeNames.length > 0) {
223
- console.debug(
224
- `routingWhile: Removed routes from loading state:`,
225
- routeNames,
226
- "state after removing:",
227
- documentLoadingRouteArraySignal.peek(),
228
- );
229
- }
230
217
  });
231
218
  };
232
219
 
@@ -236,23 +223,9 @@ const [
236
223
  removeFromDocumentLoadingActionArraySignal,
237
224
  ] = arraySignal([]);
238
225
  const workingWhile = (fn, actionNames = []) => {
239
- if (DEBUG$3 && actionNames.length > 0) {
240
- console.debug(
241
- `workingWhile: Adding actions to loading state:`,
242
- actionNames,
243
- );
244
- }
245
226
  addToDocumentLoadingActionArraySignal(...actionNames);
246
227
  return executeWithCleanup(fn, () => {
247
228
  removeFromDocumentLoadingActionArraySignal(...actionNames);
248
- if (DEBUG$3 && actionNames.length > 0) {
249
- console.debug(
250
- `routingWhile: Removed action from loading state:`,
251
- actionNames,
252
- "start after removing:",
253
- documentLoadingActionArraySignal.peek(),
254
- );
255
- }
256
229
  });
257
230
  };
258
231
 
@@ -2612,10 +2585,7 @@ const setActionPrivateProperties = (action, properties) => {
2612
2585
  // Params waiting to settle exist only under a debounced binding; every other
2613
2586
  // action answers "no" so nobody has to check whether it can be asked.
2614
2587
  const NOT_SETTLING_SIGNAL = signal(false);
2615
-
2616
- let DEBUG$2 = false;
2617
2588
  const enableDebugActions = () => {
2618
- DEBUG$2 = true;
2619
2589
  };
2620
2590
 
2621
2591
  let dispatchActions = (params) => {
@@ -2759,17 +2729,6 @@ const createKeptAnswerRegistry = ({ ms, max }) => {
2759
2729
  };
2760
2730
  };
2761
2731
 
2762
- const formatActionSet = (actionSet, prefix = "") => {
2763
- let message = prefix;
2764
- for (const action of actionSet) {
2765
- message += "\n";
2766
- message += prefixFirstAndIndentRemainingLines(String(action), {
2767
- prefix: " -",
2768
- });
2769
- }
2770
- return message;
2771
- };
2772
-
2773
2732
  const actionAbortMap = new Map();
2774
2733
  const actionPromiseMap = new Map();
2775
2734
  const activationWeakSet = createIterableWeakSet("activation");
@@ -2851,24 +2810,6 @@ const updateActions = ({
2851
2810
 
2852
2811
  const { runningSet, settledSet } = getActivationInfo();
2853
2812
 
2854
- if (DEBUG$2) {
2855
- let argSource = `reason: ${JSON.stringify(reason)}`;
2856
- if (isReplace) {
2857
- argSource += `, isReplace: true`;
2858
- }
2859
- console.group(`updateActions({ ${argSource} })`);
2860
- const lines = [
2861
- ...(prerunSet.size ? [formatActionSet(prerunSet, "- prerun:")] : []),
2862
- ...(runSet.size ? [formatActionSet(runSet, "- run:")] : []),
2863
- ...(rerunSet.size ? [formatActionSet(rerunSet, "- rerun:")] : []),
2864
- ...(resetSet.size ? [formatActionSet(resetSet, "- reset:")] : []),
2865
- ];
2866
- console.debug(
2867
- `requested operations:
2868
- ${lines.join("\n")}`,
2869
- );
2870
- }
2871
-
2872
2813
  // Internal sets that track what operations will actually be performed
2873
2814
  const willResetSet = new Set();
2874
2815
  const willPrerunSet = new Set();
@@ -2995,34 +2936,6 @@ ${lines.join("\n")}`,
2995
2936
  }
2996
2937
  }
2997
2938
  }
2998
- if (DEBUG$2) {
2999
- const lines = [
3000
- ...(willResetSet.size
3001
- ? [formatActionSet(willResetSet, "- will reset:")]
3002
- : []),
3003
- ...(willPrerunSet.size
3004
- ? [formatActionSet(willPrerunSet, "- will prerun:")]
3005
- : []),
3006
- ...(willPromoteSet.size
3007
- ? [formatActionSet(willPromoteSet, "- will promote:")]
3008
- : []),
3009
- ...(willRunSet.size ? [formatActionSet(willRunSet, "- will run:")] : []),
3010
- ...(staysRunningSet.size
3011
- ? [formatActionSet(staysRunningSet, "- stays running:")]
3012
- : []),
3013
- ...(staysAbortedSet.size
3014
- ? [formatActionSet(staysAbortedSet, "- stays aborted:")]
3015
- : []),
3016
- ...(staysFailedSet.size
3017
- ? [formatActionSet(staysFailedSet, "- stays failed:")]
3018
- : []),
3019
- ...(staysCompletedSet.size
3020
- ? [formatActionSet(staysCompletedSet, "- stays completed:")]
3021
- : []),
3022
- ];
3023
- console.debug(`operations that will be performed:
3024
- ${lines.join("\n")}`);
3025
- }
3026
2939
 
3027
2940
  // Step 4: Execute resets
3028
2941
  {
@@ -3127,9 +3040,6 @@ ${lines.join("\n")}`);
3127
3040
  }
3128
3041
  }
3129
3042
  }
3130
- if (DEBUG$2) {
3131
- console.groupEnd();
3132
- }
3133
3043
 
3134
3044
  // Calculate requestedResult based on the execution results
3135
3045
  let requestedResult;
@@ -3625,10 +3535,9 @@ const createAction = (callback, rootOptions = {}) => {
3625
3535
  toString: () => actionCallSourceSignal.peek(),
3626
3536
  meta,
3627
3537
  debug: (...args) => {
3628
- if (!meta.debug && !DEBUG$2) {
3538
+ {
3629
3539
  return;
3630
3540
  }
3631
- console.debug(...args);
3632
3541
  },
3633
3542
 
3634
3543
  paramsSignal,
@@ -3697,9 +3606,6 @@ const createAction = (callback, rootOptions = {}) => {
3697
3606
  if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
3698
3607
  prerunProtectionRegistry.unprotect(action);
3699
3608
  }
3700
- if (DEBUG$2) {
3701
- console.log(`"${action}" aborted (reason: ${abortReason})`);
3702
- }
3703
3609
  };
3704
3610
 
3705
3611
  const onAbortFromSpecific = () => {
@@ -3791,9 +3697,6 @@ const createAction = (callback, rootOptions = {}) => {
3791
3697
  if (keptAnswerRegistry) {
3792
3698
  keptAnswerRegistry.keep(action);
3793
3699
  }
3794
- if (DEBUG$2) {
3795
- console.log(`"${action}": completed`);
3796
- }
3797
3700
  const data = dataSignal.peek();
3798
3701
  return data;
3799
3702
  };
@@ -3818,9 +3721,6 @@ const createAction = (callback, rootOptions = {}) => {
3818
3721
  onAbort?.(error, { event, action, args });
3819
3722
  return error;
3820
3723
  }
3821
- if (DEBUG$2) {
3822
- console.log(`"${action}": failed (error: ${error})`);
3823
- }
3824
3724
  error.action = action;
3825
3725
  batch(() => {
3826
3726
  errorSignal.value = error;
@@ -3887,9 +3787,6 @@ const createAction = (callback, rootOptions = {}) => {
3887
3787
 
3888
3788
  const performReset = ({ reason, willRunOrPrerun }) => {
3889
3789
  abort(reason);
3890
- if (DEBUG$2) {
3891
- console.log(`"${action}": resetting (reason: ${reason})`);
3892
- }
3893
3790
 
3894
3791
  prerunProtectionRegistry.unprotect(action);
3895
3792
  if (keptAnswerRegistry) {
@@ -9457,14 +9354,13 @@ const onRequestInteraction = (
9457
9354
  } = requestInteractionCustomEvent.detail;
9458
9355
 
9459
9356
  const onPrevented = (reason) => {
9460
- debugInteraction(event, `"${name}" prevented (${reason})`);
9461
9357
  requestInteractionCustomEvent.preventDefault();
9462
9358
  prevented?.();
9463
9359
  always?.();
9464
9360
  };
9465
9361
 
9466
9362
  if (event.defaultPrevented) {
9467
- onPrevented("event.defaultPrevented");
9363
+ onPrevented();
9468
9364
  return false;
9469
9365
  }
9470
9366
 
@@ -9487,10 +9383,6 @@ const onRequestInteraction = (
9487
9383
  // Unless that element is the one asking: what it took the press to decide is
9488
9384
  // arriving here, and it is this control's to answer.
9489
9385
  if (isAimedAtSelfInteractionsBelow(event, controlHost, requester)) {
9490
- debugInteraction(
9491
- event,
9492
- `"${name}" is for a self-interactions element below`,
9493
- );
9494
9386
  requestInteractionCustomEvent.preventDefault();
9495
9387
  return false;
9496
9388
  }
@@ -9505,7 +9397,7 @@ const onRequestInteraction = (
9505
9397
  const failedInfo =
9506
9398
  ci.interactionFailedConstraintInfo ??
9507
9399
  ci.failingManagedInteraction?.interactionFailedConstraintInfo;
9508
- const reason = failedInfo
9400
+ failedInfo
9509
9401
  ? `failing interaction constraint "${failedInfo.name}"`
9510
9402
  : "not interactable";
9511
9403
  // A refusal is explained to whoever asked, and an automatic follow-up
@@ -9518,13 +9410,11 @@ const onRequestInteraction = (
9518
9410
  if (!automatic) {
9519
9411
  ci.reportInteractivity({ event });
9520
9412
  }
9521
- onPrevented(reason);
9413
+ onPrevented();
9522
9414
  return false;
9523
9415
  }
9524
9416
  }
9525
9417
  }
9526
-
9527
- debugInteraction(event, `"${name}" allowed`);
9528
9418
  allowed?.();
9529
9419
  always?.();
9530
9420
  // The click served this control; it must not serve a second time whatever
@@ -12554,68 +12444,46 @@ const useResetErrorBoundary = () => {
12554
12444
  return resetErrorBoundary;
12555
12445
  };
12556
12446
 
12557
- const DebugCommandContext = createContext(false);
12558
- const DebugInteractionContext = createContext(false);
12559
- const DebugFocusContext = createContext(false);
12560
- const DebugScrollContext = createContext(false);
12561
- const DebugPopupContext = createContext(false);
12562
- const DebugActionContext = createContext(false);
12563
- const DebugUIStateContext = createContext(false);
12447
+ /*
12448
+ * Everything dev-only in this module hangs off one `import.meta.dev` test at
12449
+ * the binding level (`import.meta.dev ? naviDebug.x : noop`), not inside the
12450
+ * function bodies. Rollup follows a binding to the function it names, but
12451
+ * not an early `return noop` inside a body: the first shape lets it drop every
12452
+ * `debugX("…")` call site of the production build, the second keeps them all.
12453
+ */
12454
+
12564
12455
  const debugNoop = () => {};
12565
- const eventGroupLogger = createEventGroupLogger();
12566
- const debugCommandDefault = eventGroupLogger.createCategory("[command]", "#8e44ad");
12567
- const debugInteractionDefault = eventGroupLogger.createCategory("[interaction]", "#2980b9");
12568
- const debugActionDefault = eventGroupLogger.createCategory("[action]", "#e67e22");
12569
- const debugPopupDefault = eventGroupLogger.createCategory("[popup]", "#27ae60");
12570
- const debugUIStateDefault = eventGroupLogger.createCategory("[uistate]", "#7f8c8d");
12571
- const debugFocusDefault = eventGroupLogger.createCategory("[focus]", "#2980b9");
12572
- const debugScrollDefault = eventGroupLogger.createCategory("[scroll]", "#2980b9");
12456
+ const useDebugNoop = () => debugNoop;
12457
+ const NaviDebugNoop = ({
12458
+ children
12459
+ }) => children;
12573
12460
 
12574
12461
  // The hooks below expose one concern's logger to components inside <NaviDebug>.
12575
12462
  // Each returns the logger function enabled for that concern, or a no-op when the
12576
12463
  // concern is off — so call sites can `const debug = useDebugX()` unconditionally.
12577
12464
  // The logger is called as `debug(message, …)` or, to group a side effect under
12578
12465
  // the native event that caused it, `debug(event, message, …)`.
12466
+ // In production every hook is the no-op returner and the bundler drops the
12467
+ // call sites along with their messages.
12579
12468
 
12580
12469
  /** Logger for navi command dispatch (`--navi-*`), or a no-op when disabled. */
12581
- const useDebugCommand = () => {
12582
- const debug = useContext(DebugCommandContext);
12583
- return debug || debugNoop;
12584
- };
12470
+ const useDebugCommand = useDebugNoop;
12585
12471
  /** Logger for gated interactions (click/scroll/select/…), or a no-op. */
12586
- const useDebugInteraction = () => {
12587
- const debug = useContext(DebugInteractionContext);
12588
- return debug || debugNoop;
12589
- };
12472
+ const useDebugInteraction = useDebugNoop;
12590
12473
  /** Logger for focus moves and focus-visible decisions, or a no-op. */
12591
- const useDebugFocus = () => {
12592
- const debug = useContext(DebugFocusContext);
12593
- return debug || debugNoop;
12594
- };
12474
+ const useDebugFocus = useDebugNoop;
12595
12475
  /**
12596
12476
  * Logger for virtual scroll / wheel motion (drag, momentum, glide) and for what
12597
12477
  * a virtualized list does about it — the render window moving, and the rows the
12598
12478
  * run asks for or decides not to ask for. Or a no-op.
12599
12479
  */
12600
- const useDebugScroll = () => {
12601
- const debug = useContext(DebugScrollContext);
12602
- return debug || debugNoop;
12603
- };
12480
+ const useDebugScroll = useDebugNoop;
12604
12481
  /** Logger for popover/dialog open/close/positioning, or a no-op. */
12605
- const useDebugPopup = () => {
12606
- const debug = useContext(DebugPopupContext);
12607
- return debug || debugNoop;
12608
- };
12482
+ const useDebugPopup = useDebugNoop;
12609
12483
  /** Logger for the action lifecycle (request → run → end), or a no-op. */
12610
- const useDebugAction = () => {
12611
- const debug = useContext(DebugActionContext);
12612
- return debug || debugNoop;
12613
- };
12484
+ const useDebugAction = useDebugNoop;
12614
12485
  /** Logger for UI-state transitions, validation and synthetic events, or a no-op. */
12615
- const useDebugUIState = () => {
12616
- const debug = useContext(DebugUIStateContext);
12617
- return debug || debugNoop;
12618
- };
12486
+ const useDebugUIState = useDebugNoop;
12619
12487
 
12620
12488
  /**
12621
12489
  * Turns on navi's color-coded console logging for everything rendered inside it.
@@ -12634,6 +12502,8 @@ const useDebugUIState = () => {
12634
12502
  * `debugScroll` and `debugPopup` unless those are set explicitly, since they
12635
12503
  * describe the same interaction.
12636
12504
  *
12505
+ * Dev-only: the production build of navi renders the children and logs nothing.
12506
+ *
12637
12507
  * @param {object} props
12638
12508
  * @param {boolean|Function} [props.debugAll] - Default for every concern below.
12639
12509
  * @param {boolean|Function} [props.debugCommand] - navi command dispatch (`--navi-*`).
@@ -12659,61 +12529,7 @@ const useDebugUIState = () => {
12659
12529
  * <Wheel>…</Wheel>
12660
12530
  * </NaviDebug>
12661
12531
  */
12662
- const NaviDebug = ({
12663
- debugAll,
12664
- debugCommand = debugAll,
12665
- debugInteraction = debugAll,
12666
- debugFocus = debugAll,
12667
- debugScroll = debugAll,
12668
- debugPopup = debugAll,
12669
- debugAction = debugAll,
12670
- debugUIState = debugAll,
12671
- children
12672
- }) => {
12673
- if (debugCommand === true) {
12674
- debugCommand = debugCommandDefault;
12675
- }
12676
- if (debugInteraction === true) {
12677
- debugInteraction = debugInteractionDefault;
12678
- }
12679
- if (debugFocus === true || debugInteraction && debugFocus === undefined) {
12680
- debugFocus = debugFocusDefault;
12681
- }
12682
- if (debugScroll === true || debugInteraction && debugScroll === undefined) {
12683
- debugScroll = debugScrollDefault;
12684
- }
12685
- if (debugPopup === true || debugInteraction && debugPopup === undefined) {
12686
- debugPopup = debugPopupDefault;
12687
- }
12688
- if (debugAction === true) {
12689
- debugAction = debugActionDefault;
12690
- }
12691
- if (debugUIState === true) {
12692
- debugUIState = debugUIStateDefault;
12693
- }
12694
- return jsx(DebugCommandContext.Provider, {
12695
- value: debugCommand,
12696
- children: jsx(DebugInteractionContext.Provider, {
12697
- value: debugInteraction,
12698
- children: jsx(DebugFocusContext.Provider, {
12699
- value: debugFocus,
12700
- children: jsx(DebugScrollContext.Provider, {
12701
- value: debugScroll,
12702
- children: jsx(DebugPopupContext.Provider, {
12703
- value: debugPopup,
12704
- children: jsx(DebugActionContext.Provider, {
12705
- value: debugAction,
12706
- children: jsx(DebugUIStateContext.Provider, {
12707
- value: debugUIState,
12708
- children: children
12709
- })
12710
- })
12711
- })
12712
- })
12713
- })
12714
- })
12715
- });
12716
- };
12532
+ const NaviDebug = NaviDebugNoop;
12717
12533
 
12718
12534
  const actionErrorWeakMap = new WeakMap();
12719
12535
  const NAVI_ACTION_ERROR_CONSTRAINT = {
@@ -12755,7 +12571,6 @@ const useExecuteAction = (
12755
12571
  errorMapping,
12756
12572
  } = {},
12757
12573
  ) => {
12758
- const debugAction = useDebugAction();
12759
12574
 
12760
12575
  // see https://medium.com/trabe/catching-asynchronous-errors-in-react-using-error-boundaries-5e8a5fd7b971
12761
12576
  // and https://codepen.io/dmail/pen/XJJqeGp?editors=0010
@@ -12854,7 +12669,6 @@ const useExecuteAction = (
12854
12669
  event: actionEvent,
12855
12670
  method,
12856
12671
  };
12857
- debugAction(event, "executing action, requested by", requester);
12858
12672
 
12859
12673
  if (resetErrorBoundary) {
12860
12674
  resetErrorBoundary();
@@ -21917,18 +21731,10 @@ const createUITransitionController = (
21917
21731
  pauseBreakpoints = [],
21918
21732
  } = {},
21919
21733
  ) => {
21920
- const debugConfig = {
21921
- detection: root.hasAttribute("data-debug-detection"),
21922
- size: root.hasAttribute("data-debug-size"),
21923
- };
21924
- const hasDebugLogs = debugConfig.size;
21925
- const debugDetection = (message) => {
21926
- if (!debugConfig.detection) return;
21927
- console.debug(`[detection]`, message);
21928
- };
21929
21734
  const debugSize = (message) => {
21930
- if (!debugConfig.size) return;
21931
- console.debug(`[size]`, message);
21735
+ {
21736
+ return;
21737
+ }
21932
21738
  };
21933
21739
 
21934
21740
  const activeGroup = root.querySelector(".ui_transition_active_group");
@@ -22134,8 +21940,6 @@ const createUITransitionController = (
22134
21940
  };
22135
21941
 
22136
21942
  updateAlignment();
22137
-
22138
- let transitionType = "none";
22139
21943
  const groupTransitionOptions = {
22140
21944
  // debugBreakpoints: [0.25],
22141
21945
  pauseBreakpoints,
@@ -22171,9 +21975,6 @@ const createUITransitionController = (
22171
21975
  const fromHeight = previousTargetSlotConfiguration.height || 0;
22172
21976
  const toWidth = targetSlotConfiguration.width || 0;
22173
21977
  const toHeight = targetSlotConfiguration.height || 0;
22174
- debugSize(
22175
- `transition from [${fromWidth}x${fromHeight}] to [${toWidth}x${toHeight}]`,
22176
- );
22177
21978
  const restoreOverflow = preventIntermediateScrollbar(root, {
22178
21979
  fromWidth,
22179
21980
  fromHeight,
@@ -22192,7 +21993,6 @@ const createUITransitionController = (
22192
21993
  }
22193
21994
  },
22194
21995
  onRestore: () => {
22195
- debugSize(`Restored overflow after transition`);
22196
21996
  },
22197
21997
  });
22198
21998
 
@@ -22239,9 +22039,6 @@ const createUITransitionController = (
22239
22039
  toHeight,
22240
22040
  alignY,
22241
22041
  );
22242
- debugSize(
22243
- `Positions in container: from [${fromLeft},${fromTop}] ${fromWidth}x${fromHeight} to [${targetLeft},${targetTop}] ${toWidth}x${toHeight}`,
22244
- );
22245
22042
  // Get border-radius values
22246
22043
  const fromBorderRadius =
22247
22044
  previousTargetSlotConfiguration.borderRadius || 0;
@@ -22350,9 +22147,6 @@ const createUITransitionController = (
22350
22147
  onFinish: () => {
22351
22148
  moveConfigurationIntoSlot(UNSET, previousTargetSlot);
22352
22149
  moveConfigurationIntoSlot(UNSET, previousOutgoingSlot);
22353
- if (hasDebugLogs) {
22354
- console.groupEnd();
22355
- }
22356
22150
  },
22357
22151
  });
22358
22152
  transition.play();
@@ -22371,10 +22165,6 @@ const createUITransitionController = (
22371
22165
  const transition = transitionController.update(transitions, {
22372
22166
  onFinish: () => {
22373
22167
  moveConfigurationIntoSlot(UNSET, outgoingSlot);
22374
-
22375
- if (hasDebugLogs) {
22376
- console.groupEnd();
22377
- }
22378
22168
  },
22379
22169
  });
22380
22170
  transition.play();
@@ -22393,9 +22183,6 @@ const createUITransitionController = (
22393
22183
  onFinish: () => {
22394
22184
  moveConfigurationIntoSlot(UNSET, previousTargetSlot);
22395
22185
  moveConfigurationIntoSlot(UNSET, previousOutgoingSlot);
22396
- if (hasDebugLogs) {
22397
- console.groupEnd();
22398
- }
22399
22186
  },
22400
22187
  });
22401
22188
  transition.play();
@@ -22437,26 +22224,11 @@ const createUITransitionController = (
22437
22224
  }
22438
22225
  const fromConfiguration = targetSlotConfiguration;
22439
22226
  const toConfiguration = detectConfiguration(targetSlot);
22440
- if (hasDebugLogs) {
22441
- console.group(`targetSlotEffect()`);
22442
- console.debug(`reasons:`);
22443
- console.debug(`- ${reasons.join("\n- ")}`);
22444
- }
22445
22227
  if (isSameConfiguration(fromConfiguration, toConfiguration)) {
22446
- debugDetection(
22447
- `already in desired state (${toConfiguration}) -> early return`,
22448
- );
22449
- if (hasDebugLogs) {
22450
- console.groupEnd();
22451
- }
22452
22228
  return;
22453
22229
  }
22454
- const fromConfigType = fromConfiguration.type;
22455
- const toConfigType = toConfiguration.type;
22456
- transitionType = `${fromConfigType}_to_${toConfigType}`;
22457
- debugDetection(
22458
- `Prepare "${transitionType}" transition (${fromConfiguration} -> ${toConfiguration})`,
22459
- );
22230
+ fromConfiguration.type;
22231
+ toConfiguration.type;
22460
22232
  // content_to_empty / content_phase_to_empty
22461
22233
  if (toConfiguration.isEmpty) {
22462
22234
  applyToEmptyTransition();
@@ -22529,7 +22301,7 @@ const createUITransitionController = (
22529
22301
  if (reasonParts.length === 0) {
22530
22302
  return;
22531
22303
  }
22532
- targetSlotEffect(reasonParts);
22304
+ targetSlotEffect();
22533
22305
  });
22534
22306
  mutationObserver.observe(targetSlot, {
22535
22307
  childList: true,
@@ -22775,13 +22547,7 @@ const useUITransitionContentId = value => {
22775
22547
  * Replaces URLPattern with a simpler, more predictable approach
22776
22548
  */
22777
22549
 
22778
-
22779
- const DEBUG$1 =
22780
- typeof process === "object" ? process.env.DEBUG === "true" : false;
22781
22550
  const debug$2 = (...args) => {
22782
- if (DEBUG$1) {
22783
- console.debug(...args);
22784
- }
22785
22551
  };
22786
22552
 
22787
22553
  // Base URL management
@@ -22891,9 +22657,6 @@ const createRoutePattern = (
22891
22657
  queryConnectionMap,
22892
22658
  paramConstraintMap,
22893
22659
  });
22894
-
22895
- debug$2(`[CustomPattern] Created pattern:`, parsedPattern);
22896
- debug$2(`[CustomPattern] Signal connections:`, connections);
22897
22660
  debug$2(`[CustomPattern] Path connections:`, pathConnectionMap.size);
22898
22661
  debug$2(`[CustomPattern] Query connections:`, queryConnectionMap.size);
22899
22662
 
@@ -22915,11 +22678,6 @@ const createRoutePattern = (
22915
22678
  patternObj: patternObject,
22916
22679
  });
22917
22680
 
22918
- debug$2(
22919
- `[CustomPattern] Matching "${url}" against "${cleanPattern}":`,
22920
- result,
22921
- );
22922
-
22923
22681
  return result;
22924
22682
  };
22925
22683
 
@@ -24507,10 +24265,7 @@ const buildHierarchicalQueryParams = (
24507
24265
  currentParent = currentParent.parent;
24508
24266
  }
24509
24267
  }
24510
-
24511
- debug$2(`Building params for ${originalPattern}`);
24512
24268
  debug$2(`parsedPattern:`, parsedPattern.original);
24513
- debug$2(`params:`, params);
24514
24269
  debug$2(
24515
24270
  `ancestorPatterns:`,
24516
24271
  ancestorPatterns.map((p) => p.urlPatternRaw),
@@ -24947,7 +24702,6 @@ const setupRoutePatterns = (routePatterns) => {
24947
24702
  for (const routePattern of routePatternSet) {
24948
24703
  calculatePatternDepth(routePattern);
24949
24704
  }
24950
- debug$2("Pattern registry updated");
24951
24705
  };
24952
24706
  // Store the most specific parent (closest parent in hierarchy)
24953
24707
  const getPathSegmentCount = (pattern) => {
@@ -25184,9 +24938,30 @@ const route = (
25184
24938
  });
25185
24939
  // methods
25186
24940
  registerSetup(({ routeSet, getUrl }) => {
24941
+ // One computed per set of params. Building a url reads the route family's
24942
+ // signals (see readSignalForUrlBuild) and verifies the candidate against
24943
+ // every pattern of the family; a list of a hundred links to one route
24944
+ // would do that a hundred times on each of its renders. A computed does it
24945
+ // once, again only when a signal it read changes — and a component reading
24946
+ // it subscribes to exactly those, which is what reading the signals during
24947
+ // render gave it.
24948
+ const relativeUrlByParamsKey = new Map();
25187
24949
  route.buildRelativeUrl = (params) => {
25188
- // buildMostPreciseUrl now handles parameter resolution internally
25189
- return routePattern.buildMostPreciseUrl(params);
24950
+ const paramsKey = getParamsCacheKey(params);
24951
+ if (paramsKey === null) {
24952
+ return routePattern.buildMostPreciseUrl(params);
24953
+ }
24954
+ let relativeUrlComputed = relativeUrlByParamsKey.get(paramsKey);
24955
+ if (!relativeUrlComputed) {
24956
+ if (relativeUrlByParamsKey.size >= PARAMS_CACHE_MAX) {
24957
+ relativeUrlByParamsKey.clear();
24958
+ }
24959
+ relativeUrlComputed = computed(() =>
24960
+ routePattern.buildMostPreciseUrl(params),
24961
+ );
24962
+ relativeUrlByParamsKey.set(paramsKey, relativeUrlComputed);
24963
+ }
24964
+ return relativeUrlComputed.value;
25190
24965
  };
25191
24966
  route.buildUrl = (params) => {
25192
24967
  const routeRelativeUrl = route.buildRelativeUrl(params);
@@ -25950,6 +25725,36 @@ const setOnAllRouteReady = (callback) => {
25950
25725
  onAllRouteReady = callback;
25951
25726
  };
25952
25727
 
25728
+ // Params are cached by content, and only params whose content a string can
25729
+ // stand for: anything else (an object, a function) builds without the cache.
25730
+ // A key set to undefined is content too — it says "without this one", which
25731
+ // a params object not naming it does not say (see buildIntendedState).
25732
+ const PARAMS_CACHE_MAX = 1000;
25733
+ const getParamsCacheKey = (params) => {
25734
+ if (params === undefined || params === null) {
25735
+ return "";
25736
+ }
25737
+ if (typeof params !== "object") {
25738
+ return null;
25739
+ }
25740
+ const keys = Object.keys(params).sort();
25741
+ let key = "";
25742
+ for (const name of keys) {
25743
+ const value = params[name];
25744
+ const type = typeof value;
25745
+ if (
25746
+ value !== null &&
25747
+ type !== "string" &&
25748
+ type !== "number" &&
25749
+ type !== "boolean"
25750
+ ) {
25751
+ return null;
25752
+ }
25753
+ key += `${name}=${type}:${value}\n`;
25754
+ }
25755
+ return key;
25756
+ };
25757
+
25953
25758
  /**
25954
25759
  * Is there an entry of THIS document behind the current one? And ahead of it?
25955
25760
  *
@@ -35130,9 +34935,7 @@ const useAutoFocus = (
35130
34935
  return () => {};
35131
34936
  }
35132
34937
  const focusDebugCall = `${getElementSignature(focusableElement)}.focus({ preventScroll: ${preventScroll} })`;
35133
- if (ancestorType === "document") {
35134
- debugFocus(e, `[autofocus] document -> ${focusDebugCall}`);
35135
- } else {
34938
+ if (ancestorType === "document") ; else {
35136
34939
  debugFocus(
35137
34940
  e,
35138
34941
  `[autofocus] "${ancestorType}" opened ${getElementSignature(ancestor)} -> ${focusDebugCall}`,
@@ -35868,10 +35671,6 @@ const useUIStateController = (
35868
35671
  [buttonName]: controller.uiState,
35869
35672
  };
35870
35673
  parentController.syncInternalState(mergedState);
35871
- debugUIState(
35872
- `merging button state into parent control group:`,
35873
- mergedState,
35874
- );
35875
35674
  // The sender's value is part of the answer, and whoever mirrors
35876
35675
  // this group — a picker, through its façade — reads the group's
35877
35676
  // state, not its children's: told the way a mount sync is
@@ -35890,7 +35689,6 @@ const useUIStateController = (
35890
35689
  writeBoundSignal(currentUIState);
35891
35690
  s.uiActionInternal?.(currentUIState, e);
35892
35691
  if (s.uiAction) {
35893
- debugUIState(`calling uiAction for ${controlType}`, currentUIState);
35894
35692
  s.uiAction(currentUIState, e);
35895
35693
  }
35896
35694
  if (skipCommand) ; else {
@@ -35898,9 +35696,6 @@ const useUIStateController = (
35898
35696
  if (command) {
35899
35697
  const element = controller.ref.current;
35900
35698
  if (element) {
35901
- debugUIState(
35902
- `triggering command "${command}" for "${controlType}"`,
35903
- );
35904
35699
  // What the press means may not be due yet: a button with an
35905
35700
  // action of its own runs the work first and lets its command
35906
35701
  // follow only once that work succeeded (see control_hooks).
@@ -36185,10 +35980,6 @@ const useUIStateController = (
36185
35980
  if (!existingInputEvent) {
36186
35981
  if (el.tagName === "INPUT") {
36187
35982
  if (el.type === "radio" || el.type === "checkbox") {
36188
- debugUIState(
36189
- e,
36190
- "dispatching synthetic input event without data for checkbox/radio",
36191
- );
36192
35983
  dispatchSyntheticInput(
36193
35984
  el,
36194
35985
  new Event("input", { bubbles: true }),
@@ -36196,10 +35987,6 @@ const useUIStateController = (
36196
35987
  );
36197
35988
  syntheticInputFired = true;
36198
35989
  } else {
36199
- debugUIState(
36200
- e,
36201
- `dispatching synthetic input event with data "${newUIState}" for input`,
36202
- );
36203
35990
  dispatchSyntheticInput(
36204
35991
  el,
36205
35992
  new InputEvent("input", {
@@ -36213,10 +36000,6 @@ const useUIStateController = (
36213
36000
  syntheticInputFired = true;
36214
36001
  }
36215
36002
  } else if (el.tagName === "SELECT") {
36216
- debugUIState(
36217
- e,
36218
- `dispatching synthetic input event for select "${newUIState}"`,
36219
- );
36220
36003
  // A plain Event, not an InputEvent: that is what the browser
36221
36004
  // itself fires on a select, and input_effect reads the value off
36222
36005
  // the element anyway.
@@ -36273,12 +36056,10 @@ const useUIStateController = (
36273
36056
  controller.state = controller.uiState;
36274
36057
  },
36275
36058
  onActionEnd: (e) => {
36276
- debugUIState(`"${controlType}" actionEnd called`);
36277
36059
  acknowledgeOwnAction(controller);
36278
36060
  controller.rules.validation.syncValidity(e);
36279
36061
  },
36280
36062
  onActionError: (e) => {
36281
- debugUIState(`"${controlType}" actionError called`);
36282
36063
  controller.rules.validation.syncValidity(e, { report: true });
36283
36064
  },
36284
36065
  subscribe: subscribeUIState,
@@ -36917,9 +36698,6 @@ const useUIGroupStateController = (
36917
36698
  const scope = useRenderScope(
36918
36699
  // ── init: runs once on mount ───────────────────────────────────────────
36919
36700
  (s) => {
36920
- debugUIGroup(
36921
- `Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
36922
- );
36923
36701
  const [publishUIState, subscribeUIState] = createPubSub();
36924
36702
  const uiStateSignal = signal(stateInitial);
36925
36703
 
@@ -37424,9 +37202,6 @@ const useUIGroupStateController = (
37424
37202
  childUIStateController,
37425
37203
  );
37426
37204
  if (index === -1) {
37427
- debugUIGroup(
37428
- `${controlType}.unregisterChild("${childControlType}") -> not found`,
37429
- );
37430
37205
  return;
37431
37206
  }
37432
37207
  childUIStateControllerArray.splice(index, 1);
@@ -38182,7 +37957,6 @@ const useControlProps = (props, {
38182
37957
  uiActionInternal
38183
37958
  }) => {
38184
37959
  const debugUIState = useDebugUIState();
38185
- const debugAction = useDebugAction();
38186
37960
 
38187
37961
  // This control answers for itself: whatever group it sits in expects nothing
38188
37962
  // from it and gives it nothing back. See useUIStateController, which is where
@@ -38805,7 +38579,6 @@ const useControlProps = (props, {
38805
38579
  const lastActionValue = lastActionValueRef.current;
38806
38580
  const valueSameAsLastAction = lastActionValue !== NO_ACTION_YET && compareTwoJsValues(currentValue, lastActionValue);
38807
38581
  if (valueSameAsLastAction) {
38808
- debugAction(e, `skipping action: value same as last action`);
38809
38582
  return false;
38810
38583
  }
38811
38584
  }
@@ -38860,7 +38633,6 @@ const useControlProps = (props, {
38860
38633
  intent,
38861
38634
  bypassInteractivity,
38862
38635
  prevented: () => {
38863
- debugInteraction(e, `interaction not allowed`);
38864
38636
  if (e.type === "keydown") {
38865
38637
  e.preventDefault();
38866
38638
  }
@@ -39842,7 +39614,6 @@ const useInteractiveProps = (props, {
39842
39614
  // awaited before the queued request runs. See the abort section in
39843
39615
  // docs/actions.md.
39844
39616
  if (optimistic && uiStateController.actionInFlight) {
39845
- debugAction(e, `queueing action (one already in flight)`);
39846
39617
  uiStateController.queuedActionAllowedEvent = e;
39847
39618
  // The instance captured at navi_action_start, NOT boundAction:
39848
39619
  // boundAction is a proxy following the UI state signal, and the UI
@@ -39925,7 +39696,6 @@ const useInteractiveProps = (props, {
39925
39696
  const {
39926
39697
  error
39927
39698
  } = e.detail;
39928
- debugAction(e, `action error`, error);
39929
39699
  if (resetOnError) {
39930
39700
  dispatchRequestResetUIState(e.currentTarget, e);
39931
39701
  }
@@ -42015,6 +41785,73 @@ const LoadingOutlineWithPortal = props => {
42015
41785
  });
42016
41786
  };
42017
41787
 
41788
+ /**
41789
+ * Reads of the layout gathered across effects, so that the browser brings its
41790
+ * styles and layout up to date once for all of them rather than once per
41791
+ * element.
41792
+ *
41793
+ * A layout effect that reads (a computed style, a rect) and then writes (an
41794
+ * attribute, a style) is fine on its own. A hundred of them in one commit are
41795
+ * not: every write dirties the tree, and the next element's read forces the
41796
+ * browser to recompute it — a style recalculation per element, each one over
41797
+ * everything dirtied since the last. Most of a long mount's time goes there,
41798
+ * not in the effects themselves.
41799
+ *
41800
+ * So an effect hands its read over instead of running it. Reads run together
41801
+ * in a microtask — still before the paint, so nothing shows uncorrected — and
41802
+ * each returns the write that depends on it, run after every read of the
41803
+ * round. A write that needs to read again returns that read: it joins the next
41804
+ * round, reads first, writes after, and so on until nothing is left.
41805
+ *
41806
+ * The cancel returned is for an element unmounted before its round: a write
41807
+ * on a node that is gone is at best wasted.
41808
+ *
41809
+ * @param {() => (undefined | (() => undefined | Function))} read
41810
+ * @returns {() => void} cancel
41811
+ */
41812
+ const scheduleLayoutRead = (read) => {
41813
+ const entry = { read, cancelled: false };
41814
+ round.push(entry);
41815
+ if (!flushScheduled) {
41816
+ flushScheduled = true;
41817
+ queueMicrotask(flush);
41818
+ }
41819
+ return () => {
41820
+ entry.cancelled = true;
41821
+ };
41822
+ };
41823
+
41824
+ let round = [];
41825
+ let flushScheduled = false;
41826
+
41827
+ const flush = () => {
41828
+ flushScheduled = false;
41829
+ while (round.length > 0) {
41830
+ const entries = round;
41831
+ round = [];
41832
+ const writes = [];
41833
+ for (const entry of entries) {
41834
+ if (entry.cancelled) {
41835
+ continue;
41836
+ }
41837
+ const write = entry.read();
41838
+ if (typeof write === "function") {
41839
+ writes.push({ entry, write });
41840
+ }
41841
+ }
41842
+ for (const { entry, write } of writes) {
41843
+ if (entry.cancelled) {
41844
+ continue;
41845
+ }
41846
+ const readAgain = write();
41847
+ if (typeof readAgain === "function") {
41848
+ entry.read = readAgain;
41849
+ round.push(entry);
41850
+ }
41851
+ }
41852
+ }
41853
+ };
41854
+
42018
41855
  installImportMetaCssBuild(import.meta);
42019
41856
  const css$_ = /* css */`.navi_text_anchor {
42020
41857
  vertical-align: baseline;
@@ -42056,6 +41893,16 @@ const TextAnchor = ({
42056
41893
  }) => {
42057
41894
  import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
42058
41895
  const anchorRef = useRef();
41896
+ // The correction reads the layout, with the other reads of the commit (see
41897
+ // layout_batch.js) — an anchor per icon, each measured between two writes,
41898
+ // is a style recalculation per icon. Cancelled by a newer correction or by
41899
+ // the unmount: a write on a node that is gone is wasted.
41900
+ const cancelReadRef = useRef(null);
41901
+ useLayoutEffect(() => {
41902
+ return () => {
41903
+ cancelReadRef.current?.();
41904
+ };
41905
+ }, []);
42059
41906
 
42060
41907
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
42061
41908
  // (e.g. a closed SidePanel) is still display:none — every rect involved
@@ -42084,23 +41931,36 @@ const TextAnchor = ({
42084
41931
  if (!anchorEl || !childEl) {
42085
41932
  return;
42086
41933
  }
42087
- // Only correct when the anchor lives in an inline formatting context.
42088
- // If the parent is a flex/grid container, inline layout rules don't apply
42089
- // and our font-metrics model is invalid.
42090
- const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
42091
- if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
42092
- // we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
42093
- anchorEl.setAttribute("hidden", "");
42094
- setTopOffset(childEl, 0);
42095
- return;
42096
- }
42097
- anchorEl.removeAttribute("hidden");
42098
- const topOffset = computeTopOffset({
42099
- anchorEl,
42100
- childEl,
42101
- textAnchor
41934
+ cancelReadRef.current?.();
41935
+ cancelReadRef.current = scheduleLayoutRead(() => {
41936
+ // Only correct when the anchor lives in an inline formatting context.
41937
+ // If the parent is a flex/grid container, inline layout rules don't
41938
+ // apply and our font-metrics model is invalid.
41939
+ const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
41940
+ if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
41941
+ return () => {
41942
+ // we must hide the anchor otherwise it would affect layout without
41943
+ // providing any benefit (would trigger flex gap for instance)
41944
+ anchorEl.setAttribute("hidden", "");
41945
+ setTopOffset(childEl, 0);
41946
+ };
41947
+ }
41948
+ return () => {
41949
+ // The anchor has to be in the flow to be measured: shown here, read
41950
+ // in the next round, with the other anchors shown by this one.
41951
+ anchorEl.removeAttribute("hidden");
41952
+ return () => {
41953
+ const topOffset = computeTopOffset({
41954
+ anchorEl,
41955
+ childEl,
41956
+ textAnchor
41957
+ });
41958
+ return () => {
41959
+ setTopOffset(childEl, topOffset);
41960
+ };
41961
+ };
41962
+ };
42102
41963
  });
42103
- setTopOffset(childEl, topOffset);
42104
41964
  }, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
42105
41965
  return jsxs(Fragment, {
42106
41966
  children: [children, jsx("span", {
@@ -47771,53 +47631,35 @@ const useNextResolver = () => useContext(NextResolverContext);
47771
47631
  * // Then inside a component render:
47772
47632
  * renderButton(props)
47773
47633
  *
47774
- * NextResolverContext exposes a stable Next component so resolvers can continue
47775
- * the chain via useNextResolver().
47776
- * ResolverIndexContext tracks which resolver is next so that when a resolver
47777
- * re-renders and calls Next, the chain resumes from the correct position.
47634
+ * Each position of the chain has a runner of its own, defined once: it renders
47635
+ * its resolver under a NextResolverContext holding the runner after it. A
47636
+ * resolver that re-renders on its own and renders <Next> therefore resumes at
47637
+ * its own position without any bookkeeping — the Next it was given is the one
47638
+ * made for it. A chain rendered by the hundred (a list's rows) pays two
47639
+ * components per position, the runner and the resolver, so nothing here is a
47640
+ * component that merely forwards.
47778
47641
  */
47779
47642
  const createComponentResolver = (resolvers, {
47780
47643
  pure
47781
47644
  } = {}) => {
47782
- const ResolverIndexContext = createContext(0);
47783
- const ChainRunner = props => {
47784
- const index = useContext(ResolverIndexContext);
47785
- if (index >= resolvers.length) {
47786
- return null;
47787
- }
47645
+ const runners = [];
47646
+ const lastIndex = resolvers.length - 1;
47647
+ for (let index = 0; index < resolvers.length; index++) {
47788
47648
  const Resolver = resolvers[index];
47789
- const isLast = index === resolvers.length - 1;
47790
- return jsx(ResolverIndexContext.Provider, {
47791
- value: index + 1,
47792
- children: isLast ? jsx(NextResolverContext.Provider, {
47793
- value: null,
47794
- children: jsx(Resolver, {
47795
- ...props
47796
- })
47797
- }) : jsx(Resolver, {
47649
+ const isLast = index === lastIndex;
47650
+ const Runner = props => jsx(NextResolverContext.Provider, {
47651
+ value: isLast ? null : runners[index + 1],
47652
+ children: jsx(Resolver, {
47798
47653
  ...props
47799
47654
  })
47800
47655
  });
47801
- };
47802
-
47803
- // Stable component defined once per createComponentResolver call.
47804
- // Renders ChainRunner directly — no new providers — so ResolverIndexContext
47805
- // is inherited from the parent tree. When a resolver calls <Next>, the chain
47806
- // resumes from index+1 (already set by the Provider wrapping that resolver).
47807
- const NextComponent = props => jsx(ChainRunner, {
47656
+ Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
47657
+ runners.push(Runner);
47658
+ }
47659
+ const FirstRunner = runners[0];
47660
+ const renderComponent = props => jsx(FirstRunner, {
47808
47661
  ...props
47809
47662
  });
47810
- const renderComponent = props => {
47811
- return jsx(NextResolverContext.Provider, {
47812
- value: NextComponent,
47813
- children: jsx(ResolverIndexContext.Provider, {
47814
- value: 0,
47815
- children: jsx(ChainRunner, {
47816
- ...props
47817
- })
47818
- })
47819
- });
47820
- };
47821
47663
  if (!pure) {
47822
47664
  return renderComponent;
47823
47665
  }
@@ -47913,36 +47755,36 @@ const useAccentColorAttributes = (
47913
47755
  return undefined;
47914
47756
  }
47915
47757
  }
47758
+ // Read with the other layout reads of the commit (see layout_batch.js): a
47759
+ // computed color per control, each read between two writes, is a style
47760
+ // recalculation per control.
47761
+ let cancelRead = null;
47916
47762
  const updateAttributes = () => {
47917
- const computedStyle = getComputedStyle(elementToCheck);
47918
- const color = computedStyle[colorProperty];
47919
- if (!color) {
47920
- el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
47921
- el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
47922
- el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
47923
- return;
47924
- }
47925
- const luminance = resolveOklchLightness(color, el);
47926
- if (luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD) {
47927
- el.setAttribute(LIGHT_ACCENT_ATTRIBUTE, "");
47928
- } else {
47929
- el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
47930
- }
47931
- if (luminance !== null && luminance > VERY_LIGHT_LUMINANCE_THRESHOLD) {
47932
- el.setAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE, "");
47933
- } else {
47934
- el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
47935
- }
47936
- const bestContrast = contrastColor(
47937
- color,
47938
- el,
47939
- DARK_CONTRAST_LIGHTNESS_THRESHOLD,
47940
- );
47941
- if (bestContrast === "black") {
47942
- el.setAttribute(DARK_CONTRAST_ATTRIBUTE, "");
47943
- } else {
47944
- el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
47945
- }
47763
+ cancelRead?.();
47764
+ cancelRead = scheduleLayoutRead(() => {
47765
+ const computedStyle = getComputedStyle(elementToCheck);
47766
+ const color = computedStyle[colorProperty];
47767
+ if (!color) {
47768
+ return () => {
47769
+ el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
47770
+ el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
47771
+ el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
47772
+ };
47773
+ }
47774
+ const luminance = resolveOklchLightness(color, el);
47775
+ const light =
47776
+ luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD;
47777
+ const veryLight =
47778
+ luminance !== null && luminance > VERY_LIGHT_LUMINANCE_THRESHOLD;
47779
+ const needsDarkForeground =
47780
+ contrastColor(color, el, DARK_CONTRAST_LIGHTNESS_THRESHOLD) ===
47781
+ "black";
47782
+ return () => {
47783
+ el.toggleAttribute(LIGHT_ACCENT_ATTRIBUTE, light);
47784
+ el.toggleAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE, veryLight);
47785
+ el.toggleAttribute(DARK_CONTRAST_ATTRIBUTE, needsDarkForeground);
47786
+ };
47787
+ });
47946
47788
  };
47947
47789
  updateAttributes();
47948
47790
  const unsubscribeFromPseudoState = subscribeToPseudoState(
@@ -47950,6 +47792,7 @@ const useAccentColorAttributes = (
47950
47792
  updateAttributes,
47951
47793
  );
47952
47794
  return () => {
47795
+ cancelRead?.();
47953
47796
  unsubscribeFromPseudoState();
47954
47797
  el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
47955
47798
  el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
@@ -57468,26 +57311,11 @@ const useInputGroup = (ref) => {
57468
57311
  const inputs = getInputs();
57469
57312
  const idx = inputs.indexOf(active);
57470
57313
  if (idx === -1) {
57471
- debugFocus(
57472
- e,
57473
- "InputGroup ArrowRight on non group input → do nothing",
57474
- );
57475
57314
  return;
57476
57315
  }
57477
57316
  if (idx === inputs.length - 1) {
57478
- debugFocus(
57479
- e,
57480
- "InputGroup ArrowRight at end of last input → do nothing",
57481
- );
57482
57317
  return;
57483
57318
  }
57484
-
57485
- debugFocus(
57486
- e,
57487
- "InputGroup ArrowRight at end of input[%d] → focus input[%d]",
57488
- idx,
57489
- idx + 1,
57490
- );
57491
57319
  e.preventDefault();
57492
57320
  focusInput(inputs[idx + 1]);
57493
57321
  return;
@@ -57506,12 +57334,6 @@ const useInputGroup = (ref) => {
57506
57334
  if (idx === 0) {
57507
57335
  return;
57508
57336
  }
57509
- debugFocus(
57510
- e,
57511
- "InputGroup ArrowLeft at start of input[%d] → focus input[%d]",
57512
- idx,
57513
- idx - 1,
57514
- );
57515
57337
  e.preventDefault();
57516
57338
  focusInput(inputs[idx - 1]);
57517
57339
  };
@@ -57534,12 +57356,6 @@ const useInputGroup = (ref) => {
57534
57356
  return;
57535
57357
  }
57536
57358
  const nextInput = inputs[idx + 1];
57537
- debugFocus(
57538
- e,
57539
- "InputGroup navi_input_full on input -> move to next input",
57540
- input,
57541
- nextInput,
57542
- );
57543
57359
  e.preventDefault();
57544
57360
  focusInput(nextInput);
57545
57361
  };
@@ -59549,14 +59365,12 @@ const Dialog = props => {
59549
59365
  // by --navi-toggle/--navi-open/--navi-close commands, the `open` prop, or
59550
59366
  // `defaultOpen`) rather than owned by a parent component.
59551
59367
  const UncontrolledDialog = props => {
59552
- const debugPopup = useDebugPopup();
59553
59368
  // Resolved here rather than left to useDialogProps: the open handler below
59554
59369
  // needs the dialog element to look at what it contains.
59555
59370
  const defaultRef = useRef();
59556
59371
  props.ref = props.ref || defaultRef;
59557
59372
  const openController = useOpenController(openEvent => {
59558
59373
  const dialogEl = props.ref.current;
59559
- debugPopup(openEvent, `dialog opened`);
59560
59374
  return {
59561
59375
  onRequestClose: requestCloseEvent => {
59562
59376
  // Whatever is inside must not be interrupted mid-action: a form that is
@@ -60197,38 +60011,6 @@ const useDialogProps = props => {
60197
60011
  position = pickPositionRelativeTo(dialogEl, null, pickOptions);
60198
60012
  applyDialogPosition(position);
60199
60013
  }
60200
- // A descendant's own visibleRectEffect (visible_rect.js — e.g. a
60201
- // Callout anchored to something inside this Dialog) knowing to
60202
- // recheck its own position whenever this dialog itself moves is
60203
- // handled generically by applyNewPosition itself (dispatches
60204
- // navi_position_change on every call) — nothing to do here.
60205
- logPlacement(triggerEvent, position);
60206
- };
60207
- // The whole picture in one line: where the dialog ended up, and what it
60208
- // believed about the room when it decided. Both are needed together — a box
60209
- // that looks wrong on screen while these numbers are right means something
60210
- // covers the screen that no API describes (a phone's autofill/suggestion
60211
- // strip), and the absence of a line after the keyboard arrives means the
60212
- // placement was never asked to run again.
60213
- const logPlacement = (triggerEvent, position) => {
60214
- // The decided target, never the current rect: the placement animates, so
60215
- // a box read right after applying one is somewhere between the two and
60216
- // reads like a decision nobody took.
60217
- const {
60218
- visualViewport
60219
- } = window;
60220
- // Reads nothing that brings the layout up to date — no getComputedStyle,
60221
- // no window.innerWidth/innerHeight. That is the very thing this line is
60222
- // here to observe (see getVisibleViewportRect in @jsenv/dom): a log that
60223
- // forced it would hand the NEXT placement a fresh viewport and hide the
60224
- // bug it was printed to show. Everything below is either already computed
60225
- // (the pick's own numbers, "room" being the room it had) or free to read.
60226
- const message = `placed on "${triggerEvent ? triggerEvent.type : "open"}": ${Math.round(position.width)}x${Math.round(position.height)} at ${Math.round(position.top)}->${Math.round(position.top + position.height)} | room ${Math.round(position.containerHeightAvailable)} | viewport ${Math.round(visualViewport.width)}x${Math.round(visualViewport.height)} +${Math.round(visualViewport.offsetLeft)},${Math.round(visualViewport.offsetTop)} | keyboard ${getVirtualKeyboardOverlayHeight()}`;
60227
- if (triggerEvent) {
60228
- debugPopup(triggerEvent, message);
60229
- } else {
60230
- debugPopup(message);
60231
- }
60232
60014
  };
60233
60015
  // Cleared here rather than on close, where the box is deliberately left
60234
60016
  // frozen at the size it was closing at (see the closing function's own
@@ -61197,7 +60979,6 @@ const Popover = props => {
61197
60979
  // by --navi-toggle/--navi-open/--navi-close commands, or by the `open` prop)
61198
60980
  // rather than owned by a parent component.
61199
60981
  const UncontrolledPopover = props => {
61200
- const debugPopup = useDebugPopup();
61201
60982
  // Resolved here rather than in usePopoverProps: the open handler below needs
61202
60983
  // the popover element to read what it holds.
61203
60984
  const defaultRef = useRef();
@@ -61207,7 +60988,6 @@ const UncontrolledPopover = props => {
61207
60988
  // about closing is that what it contains must not be interrupted mid-action.
61208
60989
  const openController = useOpenController(openEvent => {
61209
60990
  const popoverEl = props.ref.current;
61210
- debugPopup(openEvent, `popover opened`);
61211
60991
  return {
61212
60992
  onRequestClose: requestCloseEvent => {
61213
60993
  const controlsHolding = findControlsHoldingPopup(popoverEl);
@@ -61996,7 +61776,6 @@ const usePopoverProps = props => {
61996
61776
  // The close callback openEffect returns — also inlined for the same
61997
61777
  // reason: only ever built here.
61998
61778
  return closeEvent => {
61999
- debugPopup(closeEvent, `closePopover()`);
62000
61779
  clearTextSelectionInside(popoverEl);
62001
61780
  popoverEl.setAttribute("aria-expanded", "false");
62002
61781
  // Set regardless of isTopLayer — see the open side's own identical
@@ -62506,7 +62285,11 @@ const Popup = props => {
62506
62285
  Popup.Close = PopupClose;
62507
62286
 
62508
62287
  installImportMetaCssBuild(import.meta);
62509
- const css$B = /* css */`.navi_popover[data-picker-popup] {
62288
+ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
62289
+ display: none;
62290
+ }
62291
+
62292
+ .navi_popover[data-picker-popup] {
62510
62293
  --popover-border-radius: var(--picker-border-radius);
62511
62294
  --popover-border-width: var(--picker-border-width);
62512
62295
  --popover-border-color: var(--x-picker-border-color);
@@ -62789,7 +62572,6 @@ const PickerCustom = props => {
62789
62572
  }
62790
62573
  // aria-expanded + open close + interactions to open close
62791
62574
  {
62792
- const debugFocus = useDebugFocus();
62793
62575
  const debugPopup = useDebugPopup();
62794
62576
  // In "dialog" mode with a stable, caller-provided id, enterExpanded()
62795
62577
  // pushes a history entry so the back button closes it. Every other case
@@ -62824,7 +62606,6 @@ const PickerCustom = props => {
62824
62606
  // ("yes, 2h15") — the same rule Form applies to an untouched field (see
62825
62607
  // isUIStateHeld). Read at open, before anything inside can change it.
62826
62608
  const heldAtOpen = isUIStateHeld(getPickerInput(ref.current)?.__uiStateController__);
62827
- debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen, heldAtOpen ? `(held)` : `(a suggestion, not an answer yet)`);
62828
62609
  onOpen?.(openEvent);
62829
62610
  return {
62830
62611
  onRequestClose: requestCloseEvent => {
@@ -62892,18 +62673,7 @@ const PickerCustom = props => {
62892
62673
  const inputEl = getPickerInput(ref.current);
62893
62674
  const valueAtClose = getUIStateFromElement(inputEl);
62894
62675
  const controller = inputEl?.__uiStateController__;
62895
- if (controller?.controlHostProps.readOnly) {
62896
- // Opened only to be read: what it shows stays the suggestion it
62897
- // was. A look is not an answer, and the signal behind it is not
62898
- // written by one.
62899
- debugPopup(closeEvent, `picker is read-only -> nothing to commit`);
62900
- } else if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) {
62901
- // Same third case onRequestClose steps around: nothing held,
62902
- // nothing shown, nothing picked. There is no suggestion here to
62903
- // turn into an answer.
62904
- debugPopup(closeEvent, `picker showed nothing -> nothing to commit`);
62905
- } else {
62906
- debugPopup(closeEvent, `picker defined a suggestion -> commit it`);
62676
+ if (controller?.controlHostProps.readOnly) ; else if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) ; else {
62907
62677
  commitUIStateAsAnswer(controller, closeEvent);
62908
62678
  }
62909
62679
  }
@@ -63213,7 +62983,6 @@ const PickerCustom = props => {
63213
62983
  name: "mousedown to open picker",
63214
62984
  intent: "read",
63215
62985
  allowed: () => {
63216
- debugFocus(e, `prevent browser giving focus to button (mousedown.preventDefault())`);
63217
62986
  requestOpen(e);
63218
62987
  e.preventDefault(); // prevent browser trying to give focus to the select (popover will take focus)
63219
62988
  }
@@ -63458,10 +63227,14 @@ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
63458
63227
  * closes the controller for real — the popup is already gone, there is no
63459
63228
  * choice left to offer `requestClose`.
63460
63229
  *
63461
- * The content is rendered through a portal into an element this component
63462
- * owns, handed to the callout as its message (a Node, appended as-is). It is
63463
- * rendered whether the callout is open or not, so what the content holds
63464
- * survives a close, the way a popup's `mount="always"` keeps it. The element
63230
+ * The content is rendered into an element this component owns, handed to the
63231
+ * callout as its message (a Node, appended as-is) and taken back when the
63232
+ * callout closes. Between two opens it is docked, hidden, in the span below:
63233
+ * in the document the whole time, the way a popup's `mount="always"` keeps its
63234
+ * content — so what it holds survives a close, and what it measures of itself
63235
+ * at mount (a computed color, a light-dark() pair) resolves against a real
63236
+ * ancestry. An element with no document has no computed style, and a badge
63237
+ * reading its own background there would see nothing at all. The element
63465
63238
  * carries data-picker-content: the callout is appended inside the picker root,
63466
63239
  * and a press in there must read as inside the popup, not on the trigger.
63467
63240
  */
@@ -63481,23 +63254,22 @@ const PickerCalloutPopup = ({
63481
63254
  children
63482
63255
  }) => {
63483
63256
  const hostRef = useRef(null);
63484
- if (!hostRef.current) {
63485
- const host = document.createElement("div");
63486
- host.setAttribute("data-picker-content", "");
63487
- hostRef.current = host;
63488
- }
63489
63257
  // Reassigned on every render, like Popover's own, so it closes over the
63490
63258
  // latest props.
63491
63259
  openController.getElement = () => pickerRef.current;
63492
63260
  openController.openEffect = openEvent => {
63493
63261
  const pickerEl = pickerRef.current;
63262
+ const host = hostRef.current;
63263
+ // Where the content sits while the callout is closed (the span below);
63264
+ // the callout takes the element out of it and the cleanup puts it back.
63265
+ const dock = host.parentNode;
63494
63266
  const calloutManager = getPickerInput(pickerEl).__uiStateController__.rules.callout;
63495
63267
  // Only an anchor the caller named: left unsaid, the manager anchors on the
63496
63268
  // picker's own input — which is where the data-callout-* attributes a
63497
63269
  // caller puts on the picker land, and where the callout reads them.
63498
63270
  const anchorElement = anchor === pickerRef ? undefined : anchor && "current" in anchor ? anchor.current : anchor;
63499
63271
  calloutManager.addOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, {
63500
- message: hostRef.current,
63272
+ message: host,
63501
63273
  // The popup a `popupTestId` names is this callout: it is the surface the
63502
63274
  // picker opens, drawn by navi, so it is the one thing the caller cannot
63503
63275
  // name from its own children.
@@ -63531,6 +63303,7 @@ const PickerCalloutPopup = ({
63531
63303
  return closeEvent => {
63532
63304
  calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
63533
63305
  calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
63306
+ dock.appendChild(host);
63534
63307
  };
63535
63308
  };
63536
63309
  return (
@@ -63540,13 +63313,18 @@ const PickerCalloutPopup = ({
63540
63313
  as: "span",
63541
63314
  ref: ref,
63542
63315
  id: id,
63316
+ className: "navi_picker_callout_dock",
63543
63317
  style: {
63544
63318
  display: "contents"
63545
63319
  },
63546
63320
  onnavi_request_open: onnavi_request_open,
63547
63321
  onnavi_request_close: onnavi_request_close,
63548
63322
  onnavi_request_confirm: onnavi_request_confirm,
63549
- children: createPortal(children, hostRef.current)
63323
+ children: jsx("div", {
63324
+ ref: hostRef,
63325
+ "data-picker-content": "",
63326
+ children: children
63327
+ })
63550
63328
  })
63551
63329
  );
63552
63330
  };
@@ -63855,6 +63633,43 @@ document.addEventListener(
63855
63633
  { capture: true, passive: true },
63856
63634
  );
63857
63635
 
63636
+ /**
63637
+ * Calls back once the browser has painted what is committed now.
63638
+ *
63639
+ * The callbacks of the next frame run before that frame paints, so the call is
63640
+ * made from a task queued from inside one — the same way preact schedules
63641
+ * useEffect. Unlike an effect, nothing runs this early: preact flushes a
63642
+ * component's pending effects as soon as that component renders again, and a
63643
+ * re-render before the paint is exactly what a caller waiting for the paint
63644
+ * has to survive. A frame not coming at all (a background tab) still answers,
63645
+ * late, through the timeout.
63646
+ *
63647
+ * @returns {() => void} cancel
63648
+ */
63649
+ const FRAME_TIMEOUT_MS = 100;
63650
+
63651
+ const afterPaint = (callback) => {
63652
+ let called = false;
63653
+ let timeoutId;
63654
+ let frameId;
63655
+ const onFrame = () => {
63656
+ if (called) {
63657
+ return;
63658
+ }
63659
+ called = true;
63660
+ clearTimeout(timeoutId);
63661
+ cancelAnimationFrame(frameId);
63662
+ timeoutId = setTimeout(callback, 0);
63663
+ };
63664
+ timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
63665
+ frameId = requestAnimationFrame(onFrame);
63666
+ return () => {
63667
+ called = true;
63668
+ clearTimeout(timeoutId);
63669
+ cancelAnimationFrame(frameId);
63670
+ };
63671
+ };
63672
+
63858
63673
  const LoadingDotsSvg = () => {
63859
63674
  return jsxs("svg", {
63860
63675
  viewBox: "0 0 200 200",
@@ -65814,6 +65629,32 @@ const css$x = /* css */`@layer navi {
65814
65629
  middle of the padding. On the scroll box the scrollbar stays against the
65815
65630
  border and the padding is what separates the rows from it. */
65816
65631
  const LIST_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
65632
+
65633
+ // Accepts a string too (renderBudget="50" from an HTML attribute): the
65634
+ // arithmetic on the budget (renderBudget / 2, start + renderBudget) would
65635
+ // silently misbehave on a raw string ("+" concatenates).
65636
+ const toRenderBudgetNumber = value => {
65637
+ if (typeof value === "string") {
65638
+ const parsed = Number(value);
65639
+ return Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
65640
+ }
65641
+ if (typeof value === "number") {
65642
+ return value;
65643
+ }
65644
+ return RENDER_BUDGET_DEFAULT;
65645
+ };
65646
+ const resolveRenderBudget = renderBudget => {
65647
+ if (renderBudget && typeof renderBudget === "object") {
65648
+ return {
65649
+ initial: renderBudget.initial === undefined ? undefined : toRenderBudgetNumber(renderBudget.initial),
65650
+ after: toRenderBudgetNumber(renderBudget.after)
65651
+ };
65652
+ }
65653
+ return {
65654
+ initial: undefined,
65655
+ after: toRenderBudgetNumber(renderBudget)
65656
+ };
65657
+ };
65817
65658
  const ListUI = props => {
65818
65659
  import.meta.css = [css$x, "@jsenv/navi/src/control/list/list.jsx"];
65819
65660
  const {
@@ -65862,18 +65703,34 @@ const ListUI = props => {
65862
65703
  delete rest[name];
65863
65704
  }
65864
65705
  }
65865
- // Accept a string (e.g. from an HTML attribute: renderBudget="50") the
65866
- // same way a bare number would work — arithmetic below (renderBudget / 2,
65867
- // start + renderBudget, etc.) would silently misbehave on a raw string
65868
- // ("+" concatenates instead of adding).
65869
- let renderBudget = renderBudgetProp;
65870
- if (typeof renderBudget === "string") {
65871
- const parsed = Number(renderBudget);
65872
- renderBudget = Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
65873
- }
65706
+ // `renderBudget` is a number, or `{ initial, after }`: the window of the
65707
+ // first commit, until the browser has painted it, and the window from then
65708
+ // on. A list opening inside a popup draws in the click that opens it, and
65709
+ // the browser paints nothing before that render ends: rows below the fold
65710
+ // cost the same as rows on screen there, and are drawn to be seen one frame
65711
+ // later just as well. `after` takes over after the paint (see afterPaint for
65712
+ // why not an effect), and the floor of 30 is its: it protects the scrolling
65713
+ // window, not a picture nobody has seen yet.
65714
+ const {
65715
+ initial: initialRenderBudget,
65716
+ after: renderBudgetAfterPaint
65717
+ } = resolveRenderBudget(renderBudgetProp);
65718
+ let renderBudget = renderBudgetAfterPaint;
65874
65719
  if (renderBudget < 30 && !renderBudgetSkipCheck) {
65875
65720
  console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
65876
65721
  }
65722
+ const [firstPaintPending, setFirstPaintPending] = useState(initialRenderBudget !== undefined);
65723
+ useLayoutEffect(() => {
65724
+ if (!firstPaintPending) {
65725
+ return undefined;
65726
+ }
65727
+ return afterPaint(() => {
65728
+ setFirstPaintPending(false);
65729
+ });
65730
+ }, []);
65731
+ if (firstPaintPending) {
65732
+ renderBudget = initialRenderBudget;
65733
+ }
65877
65734
 
65878
65735
  // lockSize: capture the container's dimensions on first render so filtering
65879
65736
  // cannot collapse the layout. Measurement happens on the initial (unfiltered)
@@ -65910,7 +65767,10 @@ const ListUI = props => {
65910
65767
  onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
65911
65768
  }
65912
65769
  });
65913
- virtual.renderBudget = renderBudget;
65770
+ // What the runs ask for and stand for: the steady budget, whatever the
65771
+ // window of the first paint draws — a run asking for the rows of the first
65772
+ // picture and then for the rest is two round trips for one opening.
65773
+ virtual.renderBudget = renderBudgetAfterPaint;
65914
65774
  virtual.scrolled = scrolled ?? defaultScrolled;
65915
65775
  const {
65916
65776
  virtualItemSizeSignal,
@@ -66259,7 +66119,10 @@ const useListScrollSync = ({
66259
66119
  horizontal
66260
66120
  }) => {
66261
66121
  const debugScroll = useDebugScroll();
66262
- const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal);
66122
+ const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
66123
+ virtual,
66124
+ renderBudget
66125
+ });
66263
66126
  const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
66264
66127
  const getListEl = () => ref.current.querySelector(".navi_list");
66265
66128
  // Which box scrolls is measured (see getScrollerEl), so the answer holds
@@ -66315,6 +66178,28 @@ const useListScrollSync = ({
66315
66178
  });
66316
66179
  const renderWindowRef = useRef(null);
66317
66180
  renderWindowRef.current = renderWindow;
66181
+ // A budget that changes (the first paint's giving way to the scrolling one)
66182
+ // re-frames the window where it stands, in this very render — the way
66183
+ // holdWindow below moves it: nothing else would, the scroll listener only
66184
+ // moves a window the user is about to leave.
66185
+ const renderBudgetRef = useRef(renderBudget);
66186
+ if (renderBudgetRef.current !== renderBudget) {
66187
+ renderBudgetRef.current = renderBudget;
66188
+ const {
66189
+ start
66190
+ } = renderWindowRef.current;
66191
+ const total = virtual.totalSignal.peek();
66192
+ let newStart = start;
66193
+ let newEnd = start + renderBudget;
66194
+ if (total > 0 && newEnd > total) {
66195
+ newEnd = total;
66196
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
66197
+ }
66198
+ renderWindowRef.current = {
66199
+ start: newStart,
66200
+ end: newEnd
66201
+ };
66202
+ }
66318
66203
  const updateRenderWindow = (newStart, newEnd, reason) => {
66319
66204
  const {
66320
66205
  start,
@@ -66324,7 +66209,6 @@ const useListScrollSync = ({
66324
66209
  return;
66325
66210
  }
66326
66211
  captureAnchor();
66327
- debugScroll(`updateRenderWindow(${newStart}, ${newEnd}, "${reason}")`);
66328
66212
  const renderWindow = {
66329
66213
  start: newStart,
66330
66214
  end: newEnd
@@ -66417,12 +66301,11 @@ const useListScrollSync = ({
66417
66301
  return;
66418
66302
  }
66419
66303
  const scrollItemIntoView = itemEl => {
66420
- const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
66304
+ `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
66421
66305
  // When we display the list we prefer to have selected item at the center
66422
66306
  // otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
66423
66307
  const block = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
66424
- const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
66425
- debugScroll(`${trigger} -> ${scrollToItemCall}`);
66308
+ `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
66426
66309
  // The list is going somewhere on purpose, so there is no view to hold
66427
66310
  // still any more: an anchor captured before this drop it, or it would
66428
66311
  // put the list back where it was the moment the rows move under it.
@@ -66458,7 +66341,7 @@ const useListScrollSync = ({
66458
66341
  const half = Math.floor(renderBudget / 2);
66459
66342
  const newStart = Math.max(0, index - half);
66460
66343
  const newEnd = newStart + renderBudget;
66461
- updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
66344
+ updateRenderWindow(newStart, newEnd);
66462
66345
  };
66463
66346
 
66464
66347
  // Where the list must be: what the caller holds it at (`scrolled`), or where
@@ -66590,8 +66473,7 @@ const useListScrollSync = ({
66590
66473
  return undefined;
66591
66474
  }
66592
66475
  savedScrollRef.current = null;
66593
- debugScroll("Restoring scroll to", savedScroll);
66594
- updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
66476
+ updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end);
66595
66477
  // Tracked in a ref rather than cancelled via this effect's own cleanup:
66596
66478
  // updateRenderWindow above triggers a re-render, which re-runs this
66597
66479
  // effect (it has no dependency array — it needs to reactively poll
@@ -66933,7 +66815,7 @@ const useListScrollSync = ({
66933
66815
  }
66934
66816
  const endWanted = startWanted + windowSize;
66935
66817
  if (startWanted !== start || endWanted !== end) {
66936
- updateRenderWindow(startWanted, endWanted, `${indexShift} row(s) inserted before the anchored row`);
66818
+ updateRenderWindow(startWanted, endWanted);
66937
66819
  return;
66938
66820
  }
66939
66821
  }
@@ -67028,7 +66910,7 @@ const useListScrollSync = ({
67028
66910
  if (newEnd === total) {
67029
66911
  newStart = Math.max(0, total - renderBudget);
67030
66912
  }
67031
- updateRenderWindow(newStart, newEnd, reason);
66913
+ updateRenderWindow(newStart, newEnd);
67032
66914
  };
67033
66915
  // A page-level scroller does not emit "scroll" on the element itself
67034
66916
  // (document.scrollingElement); the document does.
@@ -67579,7 +67461,10 @@ const measureItemSize = (listEl, horizontal) => {
67579
67461
  fromSkeletons
67580
67462
  };
67581
67463
  };
67582
- const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
67464
+ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
67465
+ virtual,
67466
+ renderBudget
67467
+ }) => {
67583
67468
  const virtualSizeSignalRef = useRef(null);
67584
67469
  if (!virtualSizeSignalRef.current) {
67585
67470
  virtualSizeSignalRef.current = signal(virtualItemSizeProp);
@@ -67632,8 +67517,12 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
67632
67517
  // as the rows it was measured on. Written from a layout effect it would
67633
67518
  // resize them one commit later — after the scroll anchoring of that commit
67634
67519
  // had already run, which is exactly the jump anchoring exists to prevent.
67520
+ // And only while some rows are held off screen: the fillers are what the
67521
+ // size is for, and a list drawing every row it has would pay a layout on
67522
+ // each of its renders for a number nothing reads.
67635
67523
  const sizeAlreadyKnown = virtualSizeSignal.peek() !== 0;
67636
- if (!virtualItemSizeProp && sizeAlreadyKnown && ref.current) {
67524
+ const rowsHeldOffScreen = virtual.totalSignal.peek() > renderBudget;
67525
+ if (!virtualItemSizeProp && sizeAlreadyKnown && rowsHeldOffScreen && ref.current) {
67637
67526
  const listEl = ref.current.querySelector(".navi_list");
67638
67527
  const measure = listEl ? measureItemSize(listEl, horizontal) : null;
67639
67528
  if (measure) {
@@ -67798,12 +67687,15 @@ const Fallback = ({
67798
67687
  children: fallback
67799
67688
  });
67800
67689
  };
67690
+ // Reads the row size itself: it is what the size is for, and a run holding
67691
+ // every row it draws must not be redrawn — every row of it — because the size
67692
+ // settled after the first commit.
67801
67693
  const VirtualFiller = ({
67802
67694
  edge,
67803
- itemCount,
67804
- virtualItemSize
67695
+ itemCount
67805
67696
  }) => {
67806
- const sizeToFill = itemCount * virtualItemSize;
67697
+ const virtual = useContext(ListVirtualContext);
67698
+ const sizeToFill = itemCount * virtual.virtualItemSizeSignal.value;
67807
67699
  if (!sizeToFill) {
67808
67700
  return null;
67809
67701
  }
@@ -68857,6 +68749,21 @@ const ListItems = ({
68857
68749
  const virtual = useContext(ListVirtualContext);
68858
68750
  const slotId = useContext(ListSlotContext);
68859
68751
  const renderWindow = useContext(RenderWindowContext);
68752
+ // The vnode drawn for a row, kept by item: a run rendering again (its window
68753
+ // moving, its first paint's budget giving way to the full one) hands preact
68754
+ // the same vnode for a row that has not changed, and preact leaves that
68755
+ // row's whole subtree alone. Only for a `renderItem` that is the same
68756
+ // function as last time — a new one may close over new state — and for a
68757
+ // row at the same index, in the same refreshing state: everything the
68758
+ // function is given.
68759
+ const rowVnodesRef = useRef(null);
68760
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
68761
+ rowVnodesRef.current = {
68762
+ renderItem,
68763
+ byItem: new Map()
68764
+ };
68765
+ }
68766
+ const rowVnodesByItem = rowVnodesRef.current.byItem;
68860
68767
  const separator = useContext(SeparatorContext);
68861
68768
  const store = useItemStore({
68862
68769
  items,
@@ -68867,16 +68774,26 @@ const ListItems = ({
68867
68774
  });
68868
68775
  const renderRowSkeleton = renderSkeleton === undefined ? virtual.renderSkeleton : renderSkeleton;
68869
68776
  // A row on its way takes the room the list reserves for it: anything else
68870
- // and the rows drawn stop short of where the scroll says they are.
68871
- const virtualItemSize = virtual.virtualItemSizeSignal.value;
68872
- const skeletonRow = {};
68873
- if (virtualItemSize) {
68874
- if (virtual.horizontal) {
68875
- skeletonRow.rowMinWidth = `${virtualItemSize}px`;
68876
- } else {
68877
- skeletonRow.rowMinHeight = `${virtualItemSize}px`;
68777
+ // and the rows drawn stop short of where the scroll says they are. Read
68778
+ // where a row is actually missing, and not before: the size settles after
68779
+ // the first commit, and a run holding every row it draws would otherwise be
68780
+ // redrawn whole by a number it has no use for.
68781
+ let skeletonRow = null;
68782
+ const getSkeletonRow = () => {
68783
+ if (skeletonRow) {
68784
+ return skeletonRow;
68785
+ }
68786
+ skeletonRow = {};
68787
+ const virtualItemSize = virtual.virtualItemSizeSignal.value;
68788
+ if (virtualItemSize) {
68789
+ if (virtual.horizontal) {
68790
+ skeletonRow.rowMinWidth = `${virtualItemSize}px`;
68791
+ } else {
68792
+ skeletonRow.rowMinHeight = `${virtualItemSize}px`;
68793
+ }
68878
68794
  }
68879
- }
68795
+ return skeletonRow;
68796
+ };
68880
68797
  const runStart = virtual.take(ownerId, store.rowCount, slotId);
68881
68798
  const runEnd = runStart + store.rowCount;
68882
68799
  // The two ways to count the same row. The list numbers its rows from its own
@@ -69045,8 +68962,7 @@ const ListItems = ({
69045
68962
  if (windowFrom > runStart) {
69046
68963
  rows.push(jsx(VirtualFiller, {
69047
68964
  edge: "before",
69048
- itemCount: windowFrom - runStart,
69049
- virtualItemSize: virtualItemSize
68965
+ itemCount: windowFrom - runStart
69050
68966
  }, "navi-list-filler-before"));
69051
68967
  }
69052
68968
  const renderItemState = {
@@ -69060,7 +68976,7 @@ const ListItems = ({
69060
68976
  rows.push(jsx("li", {
69061
68977
  className: "navi_list_failed_rows",
69062
68978
  style: {
69063
- "--size-to-fill": `${failedRowCount * virtualItemSize}px`
68979
+ "--size-to-fill": `${failedRowCount * virtual.virtualItemSizeSignal.value}px`
69064
68980
  },
69065
68981
  children: renderError ? renderError({
69066
68982
  error: store.failure.error,
@@ -69079,7 +68995,17 @@ const ListItems = ({
69079
68995
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
69080
68996
  let rowVnode;
69081
68997
  if (item !== undefined) {
69082
- rowVnode = renderItem(item, rowIndex, renderItemState);
68998
+ const rowVnodeKept = rowVnodesByItem.get(item);
68999
+ if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
69000
+ rowVnode = rowVnodeKept.vnode;
69001
+ } else {
69002
+ rowVnode = renderItem(item, rowIndex, renderItemState);
69003
+ rowVnodesByItem.set(item, {
69004
+ vnode: rowVnode,
69005
+ rowIndex,
69006
+ refreshing: renderItemState.refreshing
69007
+ });
69008
+ }
69083
69009
  } else if (renderRowSkeleton === false) {
69084
69010
  // The row must still take its room: without it the rows below would
69085
69011
  // climb up and slide back down as the answer arrives.
@@ -69110,7 +69036,7 @@ const ListItems = ({
69110
69036
  value: item === undefined ? {
69111
69037
  id: key,
69112
69038
  index: rowIndex,
69113
- ...skeletonRow
69039
+ ...getSkeletonRow()
69114
69040
  } : {
69115
69041
  id: key,
69116
69042
  index: rowIndex,
@@ -69125,8 +69051,7 @@ const ListItems = ({
69125
69051
  if (runEnd > windowTo) {
69126
69052
  rows.push(jsx(VirtualFiller, {
69127
69053
  edge: "after",
69128
- itemCount: runEnd - windowTo,
69129
- virtualItemSize: virtualItemSize
69054
+ itemCount: runEnd - windowTo
69130
69055
  }, "navi-list-filler-after"));
69131
69056
  }
69132
69057
  return rows;
@@ -69883,6 +69808,25 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
69883
69808
  * starts. Keep it to come back to it
69884
69809
  * later through `scrolled`/`defaultScrolled` — an index would not do, since
69885
69810
  * rows get inserted while a list is being read.
69811
+ * @param {number|{initial: number, after: number}} [props.renderBudget=100]
69812
+ * How many rows of a `<List.Items>` run are in the DOM at once: the render
69813
+ * window, which slides as the user scrolls while fillers hold the room of
69814
+ * the rows outside it. Rows declared one by one as `<List.Item>` children
69815
+ * are all drawn, whatever this says — a list with more than a few dozen rows
69816
+ * gives them to a run (see docs/scroll.md, "Many rows"). Below 30 the list
69817
+ * warns: a window shorter than a tall screen shows blank fillers.
69818
+ *
69819
+ * `{ initial, after }` for a list drawn in the click that opens it (a popup):
69820
+ * `initial` rows in the commit the browser paints first — about what a phone
69821
+ * screen shows — and `after` from the paint on, where the floor of 30
69822
+ * applies. The runs ask their source for `after` rows from the start, so the
69823
+ * first picture costs no second request.
69824
+ * @param {number} [props.virtualItemSize]
69825
+ * The size of one row along the scroll axis, in px, when every row has the
69826
+ * same: what the fillers are sized with and what a scroll position is
69827
+ * estimated from. Left out, the list measures its rows — once when it
69828
+ * mounts, again when a popup around it opens, and after each commit while
69829
+ * rows are held off screen. Given, it never measures.
69886
69830
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
69887
69831
  * Which box scrolls. `"self"` gives the list a scroll box of its own;
69888
69832
  * `"parent"` makes it virtualize against the scrollable ancestor it lives in
@@ -71261,6 +71205,7 @@ const css$t = /* css */`@layer navi {
71261
71205
  --picker-icon-color-disabled: var(--picker-icon-color-readonly);
71262
71206
  --picker-align-x-default: flex-start;
71263
71207
  --picker-align-y-default: center;
71208
+ --picker-text-align-default: initial;
71264
71209
  }
71265
71210
  }
71266
71211
 
@@ -71399,7 +71344,7 @@ const css$t = /* css */`@layer navi {
71399
71344
  }
71400
71345
 
71401
71346
  &[navi-single-line] {
71402
- & .navi_picker_right_slot {
71347
+ & > .navi_picker_box > .navi_picker_right_slot {
71403
71348
  align-self: var(--x-picker-align-y);
71404
71349
  }
71405
71350
  }
@@ -71436,7 +71381,7 @@ const css$t = /* css */`@layer navi {
71436
71381
  }
71437
71382
 
71438
71383
  &[navi-ui-custom] {
71439
- & .navi_picker_input {
71384
+ & > .navi_picker_box > .navi_picker_input {
71440
71385
  top: calc(-1 * (var(--picker-border-width) + var(--x-picker-press-padding-top)));
71441
71386
  right: calc(-1 *
71442
71387
  (var(--picker-border-width) + var(--x-picker-press-padding-right)));
@@ -71499,10 +71444,10 @@ const css$t = /* css */`@layer navi {
71499
71444
  --x-picker-cursor: pointer;
71500
71445
  }
71501
71446
 
71502
- &[data-focus-within]:has(.navi_picker_input[data-focus-visible]), &[data-focus-visible] {
71447
+ &[data-focus-within]:has( > .navi_picker_box > .navi_picker_input[data-focus-visible]), &[data-focus-visible] {
71503
71448
  --x-picker-border-color: transparent;
71504
71449
 
71505
- & .navi_picker_box {
71450
+ & > .navi_picker_box {
71506
71451
  outline-style: solid;
71507
71452
  }
71508
71453
  }
@@ -71560,7 +71505,7 @@ const css$t = /* css */`@layer navi {
71560
71505
  --picker-background-color-disabled: var(--picker-background-color);
71561
71506
  --picker-icon-color: currentColor;
71562
71507
 
71563
- & .navi_picker_box {
71508
+ & > .navi_picker_box {
71564
71509
  z-index: -1;
71565
71510
  position: absolute;
71566
71511
  inset: 0;
@@ -71585,11 +71530,11 @@ const css$t = /* css */`@layer navi {
71585
71530
  font-family: inherit;
71586
71531
  line-height: inherit;
71587
71532
 
71588
- & .navi_picker_box {
71533
+ & > .navi_picker_box {
71589
71534
  min-height: 0;
71590
71535
  }
71591
71536
 
71592
- & .navi_picker_value[data-picker-facade] {
71537
+ & > .navi_picker_box > .navi_picker_value[data-picker-facade] {
71593
71538
  align-items: var(--x-picker-align-y);
71594
71539
  display: flex;
71595
71540
  }
@@ -71640,11 +71585,11 @@ const css$t = /* css */`@layer navi {
71640
71585
  --picker-padding-y-default: 0;
71641
71586
  --picker-align-x-default: center;
71642
71587
 
71643
- & .navi_picker_box {
71588
+ & > .navi_picker_box {
71644
71589
  min-height: 0;
71645
71590
  }
71646
71591
 
71647
- & .navi_picker_value {
71592
+ & > .navi_picker_box > .navi_picker_value {
71648
71593
  flex-grow: 0;
71649
71594
  align-items: center;
71650
71595
  display: inline-flex;
@@ -76103,8 +76048,7 @@ const useWheelInteractions = ({
76103
76048
  // A real drag begins: stop the glide and re-anchor to here so the wheel
76104
76049
  // doesn't jump (startPos = the just-frozen position, startClient = now).
76105
76050
  drag.moved = true;
76106
- const caught = cancelAnim();
76107
- debug(e, caught ? "catch: grabbed a moving wheel" : "drag: start");
76051
+ cancelAnim();
76108
76052
  drag.startPos = posRef.current;
76109
76053
  drag.startClient = client;
76110
76054
  }
@@ -76749,11 +76693,6 @@ function WheelUI(props) {
76749
76693
  // on the press settles the DOM up front, before the tap completes.
76750
76694
  const commitIfAnimating = () => {
76751
76695
  const animating = momentumRef.current !== null || glideRef.current !== null;
76752
- // Logs on every press outside the wheel — tells us whether a tap on a dialog
76753
- // button lands while the wheel is still coasting (animating=true) or after it
76754
- // already settled (animating=false), which decides where the dropped click
76755
- // comes from.
76756
- debugScroll(`external press while wheel animating=${animating}`);
76757
76696
  if (!animating) {
76758
76697
  return;
76759
76698
  }
@@ -76815,7 +76754,6 @@ function WheelUI(props) {
76815
76754
  glideFromOutsideRef.current = fromOutside;
76816
76755
  }
76817
76756
  if (glideRef.current === null) {
76818
- debugScroll("glide: start");
76819
76757
  glideRef.current = requestAnimationFrame(() => glideStep(vp, performance.now()));
76820
76758
  }
76821
76759
  // else: the loop is already running and will chase the updated target — no
@@ -76830,7 +76768,6 @@ function WheelUI(props) {
76830
76768
  const settle = (vp, velocity) => {
76831
76769
  cancelAnim();
76832
76770
  glideFromOutsideRef.current = false;
76833
- debugScroll(`settle: momentum start (v=${velocity}px/ms)`);
76834
76771
  // A drag fling: allow the full swipe velocity (see WHEEL_FLING_MAX_VELOCITY)
76835
76772
  // so a hard swipe carries across the list instead of being clipped to a few
76836
76773
  // rows. The mouse wheel never reaches here (it uses wheelSettle/glide).