@jsenv/navi 0.29.316 → 0.29.318

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.
@@ -63528,25 +63300,44 @@ const PickerCalloutPopup = ({
63528
63300
  onnavi_request_confirm?.(e);
63529
63301
  };
63530
63302
  calloutElement.addEventListener("navi_request_confirm", forwardConfirm);
63303
+ // Said once the content is in the shown callout: what the content defers
63304
+ // until it is displayed (useDisplayedLayoutEffect, watching this attribute)
63305
+ // then measures it where it is drawn. Same call stack as the open, like
63306
+ // Popover's own — see observeAncestorOpenState in @jsenv/dom.
63307
+ dock.setAttribute("aria-expanded", "true");
63531
63308
  return closeEvent => {
63532
63309
  calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
63533
63310
  calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
63311
+ dock.appendChild(host);
63312
+ dock.setAttribute("aria-expanded", "false");
63534
63313
  };
63535
63314
  };
63536
63315
  return (
63537
63316
  // What the picker addresses (aria-controls, the request events it
63538
63317
  // forwards); the callout itself lives where the callout manager puts it.
63318
+ // aria-expanded here, on the dock rather than on the content element: the
63319
+ // openable ancestor the content finds at mount (the only time it looks),
63320
+ // and one that is no ancestor of the content once the callout holds it —
63321
+ // so a --navi-close said in there still resolves to the callout element,
63322
+ // the closest [aria-expanded] from the button. The open effect keeps it
63323
+ // current; preact leaves it alone, the prop never changes.
63539
63324
  jsx(Box, {
63540
63325
  as: "span",
63541
63326
  ref: ref,
63542
63327
  id: id,
63328
+ className: "navi_picker_callout_dock",
63329
+ "aria-expanded": "false",
63543
63330
  style: {
63544
63331
  display: "contents"
63545
63332
  },
63546
63333
  onnavi_request_open: onnavi_request_open,
63547
63334
  onnavi_request_close: onnavi_request_close,
63548
63335
  onnavi_request_confirm: onnavi_request_confirm,
63549
- children: createPortal(children, hostRef.current)
63336
+ children: jsx("div", {
63337
+ ref: hostRef,
63338
+ "data-picker-content": "",
63339
+ children: children
63340
+ })
63550
63341
  })
63551
63342
  );
63552
63343
  };
@@ -63855,6 +63646,43 @@ document.addEventListener(
63855
63646
  { capture: true, passive: true },
63856
63647
  );
63857
63648
 
63649
+ /**
63650
+ * Calls back once the browser has painted what is committed now.
63651
+ *
63652
+ * The callbacks of the next frame run before that frame paints, so the call is
63653
+ * made from a task queued from inside one — the same way preact schedules
63654
+ * useEffect. Unlike an effect, nothing runs this early: preact flushes a
63655
+ * component's pending effects as soon as that component renders again, and a
63656
+ * re-render before the paint is exactly what a caller waiting for the paint
63657
+ * has to survive. A frame not coming at all (a background tab) still answers,
63658
+ * late, through the timeout.
63659
+ *
63660
+ * @returns {() => void} cancel
63661
+ */
63662
+ const FRAME_TIMEOUT_MS = 100;
63663
+
63664
+ const afterPaint = (callback) => {
63665
+ let called = false;
63666
+ let timeoutId;
63667
+ let frameId;
63668
+ const onFrame = () => {
63669
+ if (called) {
63670
+ return;
63671
+ }
63672
+ called = true;
63673
+ clearTimeout(timeoutId);
63674
+ cancelAnimationFrame(frameId);
63675
+ timeoutId = setTimeout(callback, 0);
63676
+ };
63677
+ timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
63678
+ frameId = requestAnimationFrame(onFrame);
63679
+ return () => {
63680
+ called = true;
63681
+ clearTimeout(timeoutId);
63682
+ cancelAnimationFrame(frameId);
63683
+ };
63684
+ };
63685
+
63858
63686
  const LoadingDotsSvg = () => {
63859
63687
  return jsxs("svg", {
63860
63688
  viewBox: "0 0 200 200",
@@ -65814,6 +65642,32 @@ const css$x = /* css */`@layer navi {
65814
65642
  middle of the padding. On the scroll box the scrollbar stays against the
65815
65643
  border and the padding is what separates the rows from it. */
65816
65644
  const LIST_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
65645
+
65646
+ // Accepts a string too (renderBudget="50" from an HTML attribute): the
65647
+ // arithmetic on the budget (renderBudget / 2, start + renderBudget) would
65648
+ // silently misbehave on a raw string ("+" concatenates).
65649
+ const toRenderBudgetNumber = value => {
65650
+ if (typeof value === "string") {
65651
+ const parsed = Number(value);
65652
+ return Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
65653
+ }
65654
+ if (typeof value === "number") {
65655
+ return value;
65656
+ }
65657
+ return RENDER_BUDGET_DEFAULT;
65658
+ };
65659
+ const resolveRenderBudget = renderBudget => {
65660
+ if (renderBudget && typeof renderBudget === "object") {
65661
+ return {
65662
+ initial: renderBudget.initial === undefined ? undefined : toRenderBudgetNumber(renderBudget.initial),
65663
+ after: toRenderBudgetNumber(renderBudget.after)
65664
+ };
65665
+ }
65666
+ return {
65667
+ initial: undefined,
65668
+ after: toRenderBudgetNumber(renderBudget)
65669
+ };
65670
+ };
65817
65671
  const ListUI = props => {
65818
65672
  import.meta.css = [css$x, "@jsenv/navi/src/control/list/list.jsx"];
65819
65673
  const {
@@ -65862,18 +65716,34 @@ const ListUI = props => {
65862
65716
  delete rest[name];
65863
65717
  }
65864
65718
  }
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
- }
65719
+ // `renderBudget` is a number, or `{ initial, after }`: the window of the
65720
+ // first commit, until the browser has painted it, and the window from then
65721
+ // on. A list opening inside a popup draws in the click that opens it, and
65722
+ // the browser paints nothing before that render ends: rows below the fold
65723
+ // cost the same as rows on screen there, and are drawn to be seen one frame
65724
+ // later just as well. `after` takes over after the paint (see afterPaint for
65725
+ // why not an effect), and the floor of 30 is its: it protects the scrolling
65726
+ // window, not a picture nobody has seen yet.
65727
+ const {
65728
+ initial: initialRenderBudget,
65729
+ after: renderBudgetAfterPaint
65730
+ } = resolveRenderBudget(renderBudgetProp);
65731
+ let renderBudget = renderBudgetAfterPaint;
65874
65732
  if (renderBudget < 30 && !renderBudgetSkipCheck) {
65875
65733
  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
65734
  }
65735
+ const [firstPaintPending, setFirstPaintPending] = useState(initialRenderBudget !== undefined);
65736
+ useLayoutEffect(() => {
65737
+ if (!firstPaintPending) {
65738
+ return undefined;
65739
+ }
65740
+ return afterPaint(() => {
65741
+ setFirstPaintPending(false);
65742
+ });
65743
+ }, []);
65744
+ if (firstPaintPending) {
65745
+ renderBudget = initialRenderBudget;
65746
+ }
65877
65747
 
65878
65748
  // lockSize: capture the container's dimensions on first render so filtering
65879
65749
  // cannot collapse the layout. Measurement happens on the initial (unfiltered)
@@ -65910,7 +65780,10 @@ const ListUI = props => {
65910
65780
  onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
65911
65781
  }
65912
65782
  });
65913
- virtual.renderBudget = renderBudget;
65783
+ // What the runs ask for and stand for: the steady budget, whatever the
65784
+ // window of the first paint draws — a run asking for the rows of the first
65785
+ // picture and then for the rest is two round trips for one opening.
65786
+ virtual.renderBudget = renderBudgetAfterPaint;
65914
65787
  virtual.scrolled = scrolled ?? defaultScrolled;
65915
65788
  const {
65916
65789
  virtualItemSizeSignal,
@@ -66259,7 +66132,10 @@ const useListScrollSync = ({
66259
66132
  horizontal
66260
66133
  }) => {
66261
66134
  const debugScroll = useDebugScroll();
66262
- const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal);
66135
+ const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
66136
+ virtual,
66137
+ renderBudget
66138
+ });
66263
66139
  const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
66264
66140
  const getListEl = () => ref.current.querySelector(".navi_list");
66265
66141
  // Which box scrolls is measured (see getScrollerEl), so the answer holds
@@ -66315,6 +66191,40 @@ const useListScrollSync = ({
66315
66191
  });
66316
66192
  const renderWindowRef = useRef(null);
66317
66193
  renderWindowRef.current = renderWindow;
66194
+ // The window is as wide as the budget says, whatever the state holds: a
66195
+ // budget that changes (the first paint's giving way to the scrolling one)
66196
+ // re-frames the window where it stands, in the render itself, the way
66197
+ // holdWindow below moves it — nothing else would, the scroll listener only
66198
+ // moves a window the user is about to leave. Derived on every render and
66199
+ // not written once: the state keeps the width it had, and the next render
66200
+ // copies it back into the ref above. The same object is handed out for as
66201
+ // long as the numbers hold, so the rows are not told about a window that
66202
+ // did not move.
66203
+ const framedWindowRef = useRef(null);
66204
+ {
66205
+ const {
66206
+ start,
66207
+ end
66208
+ } = renderWindowRef.current;
66209
+ const total = virtual.totalSignal.peek();
66210
+ let framedStart = start;
66211
+ let framedEnd = start + renderBudget;
66212
+ if (total > 0 && framedEnd > total) {
66213
+ framedEnd = total;
66214
+ framedStart = total - renderBudget < 0 ? 0 : total - renderBudget;
66215
+ }
66216
+ if (framedStart !== start || framedEnd !== end) {
66217
+ const framed = framedWindowRef.current;
66218
+ if (framed && framed.start === framedStart && framed.end === framedEnd) {
66219
+ renderWindowRef.current = framed;
66220
+ } else {
66221
+ framedWindowRef.current = renderWindowRef.current = {
66222
+ start: framedStart,
66223
+ end: framedEnd
66224
+ };
66225
+ }
66226
+ }
66227
+ }
66318
66228
  const updateRenderWindow = (newStart, newEnd, reason) => {
66319
66229
  const {
66320
66230
  start,
@@ -66324,7 +66234,6 @@ const useListScrollSync = ({
66324
66234
  return;
66325
66235
  }
66326
66236
  captureAnchor();
66327
- debugScroll(`updateRenderWindow(${newStart}, ${newEnd}, "${reason}")`);
66328
66237
  const renderWindow = {
66329
66238
  start: newStart,
66330
66239
  end: newEnd
@@ -66417,12 +66326,11 @@ const useListScrollSync = ({
66417
66326
  return;
66418
66327
  }
66419
66328
  const scrollItemIntoView = itemEl => {
66420
- const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
66329
+ `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
66421
66330
  // When we display the list we prefer to have selected item at the center
66422
66331
  // otherwise, usually when focused by arrow nav, we want to keep it into view close to the nearest edge
66423
66332
  const block = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
66424
- const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
66425
- debugScroll(`${trigger} -> ${scrollToItemCall}`);
66333
+ `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
66426
66334
  // The list is going somewhere on purpose, so there is no view to hold
66427
66335
  // still any more: an anchor captured before this drop it, or it would
66428
66336
  // put the list back where it was the moment the rows move under it.
@@ -66458,7 +66366,7 @@ const useListScrollSync = ({
66458
66366
  const half = Math.floor(renderBudget / 2);
66459
66367
  const newStart = Math.max(0, index - half);
66460
66368
  const newEnd = newStart + renderBudget;
66461
- updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
66369
+ updateRenderWindow(newStart, newEnd);
66462
66370
  };
66463
66371
 
66464
66372
  // Where the list must be: what the caller holds it at (`scrolled`), or where
@@ -66590,8 +66498,7 @@ const useListScrollSync = ({
66590
66498
  return undefined;
66591
66499
  }
66592
66500
  savedScrollRef.current = null;
66593
- debugScroll("Restoring scroll to", savedScroll);
66594
- updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
66501
+ updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end);
66595
66502
  // Tracked in a ref rather than cancelled via this effect's own cleanup:
66596
66503
  // updateRenderWindow above triggers a re-render, which re-runs this
66597
66504
  // effect (it has no dependency array — it needs to reactively poll
@@ -66933,7 +66840,7 @@ const useListScrollSync = ({
66933
66840
  }
66934
66841
  const endWanted = startWanted + windowSize;
66935
66842
  if (startWanted !== start || endWanted !== end) {
66936
- updateRenderWindow(startWanted, endWanted, `${indexShift} row(s) inserted before the anchored row`);
66843
+ updateRenderWindow(startWanted, endWanted);
66937
66844
  return;
66938
66845
  }
66939
66846
  }
@@ -67028,7 +66935,7 @@ const useListScrollSync = ({
67028
66935
  if (newEnd === total) {
67029
66936
  newStart = Math.max(0, total - renderBudget);
67030
66937
  }
67031
- updateRenderWindow(newStart, newEnd, reason);
66938
+ updateRenderWindow(newStart, newEnd);
67032
66939
  };
67033
66940
  // A page-level scroller does not emit "scroll" on the element itself
67034
66941
  // (document.scrollingElement); the document does.
@@ -67579,7 +67486,10 @@ const measureItemSize = (listEl, horizontal) => {
67579
67486
  fromSkeletons
67580
67487
  };
67581
67488
  };
67582
- const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
67489
+ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
67490
+ virtual,
67491
+ renderBudget
67492
+ }) => {
67583
67493
  const virtualSizeSignalRef = useRef(null);
67584
67494
  if (!virtualSizeSignalRef.current) {
67585
67495
  virtualSizeSignalRef.current = signal(virtualItemSizeProp);
@@ -67632,8 +67542,12 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
67632
67542
  // as the rows it was measured on. Written from a layout effect it would
67633
67543
  // resize them one commit later — after the scroll anchoring of that commit
67634
67544
  // had already run, which is exactly the jump anchoring exists to prevent.
67545
+ // And only while some rows are held off screen: the fillers are what the
67546
+ // size is for, and a list drawing every row it has would pay a layout on
67547
+ // each of its renders for a number nothing reads.
67635
67548
  const sizeAlreadyKnown = virtualSizeSignal.peek() !== 0;
67636
- if (!virtualItemSizeProp && sizeAlreadyKnown && ref.current) {
67549
+ const rowsHeldOffScreen = virtual.totalSignal.peek() > renderBudget;
67550
+ if (!virtualItemSizeProp && sizeAlreadyKnown && rowsHeldOffScreen && ref.current) {
67637
67551
  const listEl = ref.current.querySelector(".navi_list");
67638
67552
  const measure = listEl ? measureItemSize(listEl, horizontal) : null;
67639
67553
  if (measure) {
@@ -67798,12 +67712,15 @@ const Fallback = ({
67798
67712
  children: fallback
67799
67713
  });
67800
67714
  };
67715
+ // Reads the row size itself: it is what the size is for, and a run holding
67716
+ // every row it draws must not be redrawn — every row of it — because the size
67717
+ // settled after the first commit.
67801
67718
  const VirtualFiller = ({
67802
67719
  edge,
67803
- itemCount,
67804
- virtualItemSize
67720
+ itemCount
67805
67721
  }) => {
67806
- const sizeToFill = itemCount * virtualItemSize;
67722
+ const virtual = useContext(ListVirtualContext);
67723
+ const sizeToFill = itemCount * virtual.virtualItemSizeSignal.value;
67807
67724
  if (!sizeToFill) {
67808
67725
  return null;
67809
67726
  }
@@ -68857,6 +68774,21 @@ const ListItems = ({
68857
68774
  const virtual = useContext(ListVirtualContext);
68858
68775
  const slotId = useContext(ListSlotContext);
68859
68776
  const renderWindow = useContext(RenderWindowContext);
68777
+ // The vnode drawn for a row, kept by item: a run rendering again (its window
68778
+ // moving, its first paint's budget giving way to the full one) hands preact
68779
+ // the same vnode for a row that has not changed, and preact leaves that
68780
+ // row's whole subtree alone. Only for a `renderItem` that is the same
68781
+ // function as last time — a new one may close over new state — and for a
68782
+ // row at the same index, in the same refreshing state: everything the
68783
+ // function is given.
68784
+ const rowVnodesRef = useRef(null);
68785
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
68786
+ rowVnodesRef.current = {
68787
+ renderItem,
68788
+ byItem: new Map()
68789
+ };
68790
+ }
68791
+ const rowVnodesByItem = rowVnodesRef.current.byItem;
68860
68792
  const separator = useContext(SeparatorContext);
68861
68793
  const store = useItemStore({
68862
68794
  items,
@@ -68867,16 +68799,26 @@ const ListItems = ({
68867
68799
  });
68868
68800
  const renderRowSkeleton = renderSkeleton === undefined ? virtual.renderSkeleton : renderSkeleton;
68869
68801
  // 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`;
68802
+ // and the rows drawn stop short of where the scroll says they are. Read
68803
+ // where a row is actually missing, and not before: the size settles after
68804
+ // the first commit, and a run holding every row it draws would otherwise be
68805
+ // redrawn whole by a number it has no use for.
68806
+ let skeletonRow = null;
68807
+ const getSkeletonRow = () => {
68808
+ if (skeletonRow) {
68809
+ return skeletonRow;
68810
+ }
68811
+ skeletonRow = {};
68812
+ const virtualItemSize = virtual.virtualItemSizeSignal.value;
68813
+ if (virtualItemSize) {
68814
+ if (virtual.horizontal) {
68815
+ skeletonRow.rowMinWidth = `${virtualItemSize}px`;
68816
+ } else {
68817
+ skeletonRow.rowMinHeight = `${virtualItemSize}px`;
68818
+ }
68878
68819
  }
68879
- }
68820
+ return skeletonRow;
68821
+ };
68880
68822
  const runStart = virtual.take(ownerId, store.rowCount, slotId);
68881
68823
  const runEnd = runStart + store.rowCount;
68882
68824
  // The two ways to count the same row. The list numbers its rows from its own
@@ -69045,8 +68987,7 @@ const ListItems = ({
69045
68987
  if (windowFrom > runStart) {
69046
68988
  rows.push(jsx(VirtualFiller, {
69047
68989
  edge: "before",
69048
- itemCount: windowFrom - runStart,
69049
- virtualItemSize: virtualItemSize
68990
+ itemCount: windowFrom - runStart
69050
68991
  }, "navi-list-filler-before"));
69051
68992
  }
69052
68993
  const renderItemState = {
@@ -69060,7 +69001,7 @@ const ListItems = ({
69060
69001
  rows.push(jsx("li", {
69061
69002
  className: "navi_list_failed_rows",
69062
69003
  style: {
69063
- "--size-to-fill": `${failedRowCount * virtualItemSize}px`
69004
+ "--size-to-fill": `${failedRowCount * virtual.virtualItemSizeSignal.value}px`
69064
69005
  },
69065
69006
  children: renderError ? renderError({
69066
69007
  error: store.failure.error,
@@ -69079,7 +69020,17 @@ const ListItems = ({
69079
69020
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
69080
69021
  let rowVnode;
69081
69022
  if (item !== undefined) {
69082
- rowVnode = renderItem(item, rowIndex, renderItemState);
69023
+ const rowVnodeKept = rowVnodesByItem.get(item);
69024
+ if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
69025
+ rowVnode = rowVnodeKept.vnode;
69026
+ } else {
69027
+ rowVnode = renderItem(item, rowIndex, renderItemState);
69028
+ rowVnodesByItem.set(item, {
69029
+ vnode: rowVnode,
69030
+ rowIndex,
69031
+ refreshing: renderItemState.refreshing
69032
+ });
69033
+ }
69083
69034
  } else if (renderRowSkeleton === false) {
69084
69035
  // The row must still take its room: without it the rows below would
69085
69036
  // climb up and slide back down as the answer arrives.
@@ -69110,7 +69061,7 @@ const ListItems = ({
69110
69061
  value: item === undefined ? {
69111
69062
  id: key,
69112
69063
  index: rowIndex,
69113
- ...skeletonRow
69064
+ ...getSkeletonRow()
69114
69065
  } : {
69115
69066
  id: key,
69116
69067
  index: rowIndex,
@@ -69125,8 +69076,7 @@ const ListItems = ({
69125
69076
  if (runEnd > windowTo) {
69126
69077
  rows.push(jsx(VirtualFiller, {
69127
69078
  edge: "after",
69128
- itemCount: runEnd - windowTo,
69129
- virtualItemSize: virtualItemSize
69079
+ itemCount: runEnd - windowTo
69130
69080
  }, "navi-list-filler-after"));
69131
69081
  }
69132
69082
  return rows;
@@ -69883,6 +69833,25 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
69883
69833
  * starts. Keep it to come back to it
69884
69834
  * later through `scrolled`/`defaultScrolled` — an index would not do, since
69885
69835
  * rows get inserted while a list is being read.
69836
+ * @param {number|{initial: number, after: number}} [props.renderBudget=100]
69837
+ * How many rows of a `<List.Items>` run are in the DOM at once: the render
69838
+ * window, which slides as the user scrolls while fillers hold the room of
69839
+ * the rows outside it. Rows declared one by one as `<List.Item>` children
69840
+ * are all drawn, whatever this says — a list with more than a few dozen rows
69841
+ * gives them to a run (see docs/scroll.md, "Many rows"). Below 30 the list
69842
+ * warns: a window shorter than a tall screen shows blank fillers.
69843
+ *
69844
+ * `{ initial, after }` for a list drawn in the click that opens it (a popup):
69845
+ * `initial` rows in the commit the browser paints first — about what a phone
69846
+ * screen shows — and `after` from the paint on, where the floor of 30
69847
+ * applies. The runs ask their source for `after` rows from the start, so the
69848
+ * first picture costs no second request.
69849
+ * @param {number} [props.virtualItemSize]
69850
+ * The size of one row along the scroll axis, in px, when every row has the
69851
+ * same: what the fillers are sized with and what a scroll position is
69852
+ * estimated from. Left out, the list measures its rows — once when it
69853
+ * mounts, again when a popup around it opens, and after each commit while
69854
+ * rows are held off screen. Given, it never measures.
69886
69855
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
69887
69856
  * Which box scrolls. `"self"` gives the list a scroll box of its own;
69888
69857
  * `"parent"` makes it virtualize against the scrollable ancestor it lives in
@@ -71261,6 +71230,7 @@ const css$t = /* css */`@layer navi {
71261
71230
  --picker-icon-color-disabled: var(--picker-icon-color-readonly);
71262
71231
  --picker-align-x-default: flex-start;
71263
71232
  --picker-align-y-default: center;
71233
+ --picker-text-align-default: initial;
71264
71234
  }
71265
71235
  }
71266
71236
 
@@ -71399,7 +71369,7 @@ const css$t = /* css */`@layer navi {
71399
71369
  }
71400
71370
 
71401
71371
  &[navi-single-line] {
71402
- & .navi_picker_right_slot {
71372
+ & > .navi_picker_box > .navi_picker_right_slot {
71403
71373
  align-self: var(--x-picker-align-y);
71404
71374
  }
71405
71375
  }
@@ -71436,7 +71406,7 @@ const css$t = /* css */`@layer navi {
71436
71406
  }
71437
71407
 
71438
71408
  &[navi-ui-custom] {
71439
- & .navi_picker_input {
71409
+ & > .navi_picker_box > .navi_picker_input {
71440
71410
  top: calc(-1 * (var(--picker-border-width) + var(--x-picker-press-padding-top)));
71441
71411
  right: calc(-1 *
71442
71412
  (var(--picker-border-width) + var(--x-picker-press-padding-right)));
@@ -71499,10 +71469,10 @@ const css$t = /* css */`@layer navi {
71499
71469
  --x-picker-cursor: pointer;
71500
71470
  }
71501
71471
 
71502
- &[data-focus-within]:has(.navi_picker_input[data-focus-visible]), &[data-focus-visible] {
71472
+ &[data-focus-within]:has( > .navi_picker_box > .navi_picker_input[data-focus-visible]), &[data-focus-visible] {
71503
71473
  --x-picker-border-color: transparent;
71504
71474
 
71505
- & .navi_picker_box {
71475
+ & > .navi_picker_box {
71506
71476
  outline-style: solid;
71507
71477
  }
71508
71478
  }
@@ -71560,7 +71530,7 @@ const css$t = /* css */`@layer navi {
71560
71530
  --picker-background-color-disabled: var(--picker-background-color);
71561
71531
  --picker-icon-color: currentColor;
71562
71532
 
71563
- & .navi_picker_box {
71533
+ & > .navi_picker_box {
71564
71534
  z-index: -1;
71565
71535
  position: absolute;
71566
71536
  inset: 0;
@@ -71585,11 +71555,11 @@ const css$t = /* css */`@layer navi {
71585
71555
  font-family: inherit;
71586
71556
  line-height: inherit;
71587
71557
 
71588
- & .navi_picker_box {
71558
+ & > .navi_picker_box {
71589
71559
  min-height: 0;
71590
71560
  }
71591
71561
 
71592
- & .navi_picker_value[data-picker-facade] {
71562
+ & > .navi_picker_box > .navi_picker_value[data-picker-facade] {
71593
71563
  align-items: var(--x-picker-align-y);
71594
71564
  display: flex;
71595
71565
  }
@@ -71640,11 +71610,11 @@ const css$t = /* css */`@layer navi {
71640
71610
  --picker-padding-y-default: 0;
71641
71611
  --picker-align-x-default: center;
71642
71612
 
71643
- & .navi_picker_box {
71613
+ & > .navi_picker_box {
71644
71614
  min-height: 0;
71645
71615
  }
71646
71616
 
71647
- & .navi_picker_value {
71617
+ & > .navi_picker_box > .navi_picker_value {
71648
71618
  flex-grow: 0;
71649
71619
  align-items: center;
71650
71620
  display: inline-flex;
@@ -76103,8 +76073,7 @@ const useWheelInteractions = ({
76103
76073
  // A real drag begins: stop the glide and re-anchor to here so the wheel
76104
76074
  // doesn't jump (startPos = the just-frozen position, startClient = now).
76105
76075
  drag.moved = true;
76106
- const caught = cancelAnim();
76107
- debug(e, caught ? "catch: grabbed a moving wheel" : "drag: start");
76076
+ cancelAnim();
76108
76077
  drag.startPos = posRef.current;
76109
76078
  drag.startClient = client;
76110
76079
  }
@@ -76749,11 +76718,6 @@ function WheelUI(props) {
76749
76718
  // on the press settles the DOM up front, before the tap completes.
76750
76719
  const commitIfAnimating = () => {
76751
76720
  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
76721
  if (!animating) {
76758
76722
  return;
76759
76723
  }
@@ -76815,7 +76779,6 @@ function WheelUI(props) {
76815
76779
  glideFromOutsideRef.current = fromOutside;
76816
76780
  }
76817
76781
  if (glideRef.current === null) {
76818
- debugScroll("glide: start");
76819
76782
  glideRef.current = requestAnimationFrame(() => glideStep(vp, performance.now()));
76820
76783
  }
76821
76784
  // else: the loop is already running and will chase the updated target — no
@@ -76830,7 +76793,6 @@ function WheelUI(props) {
76830
76793
  const settle = (vp, velocity) => {
76831
76794
  cancelAnim();
76832
76795
  glideFromOutsideRef.current = false;
76833
- debugScroll(`settle: momentum start (v=${velocity}px/ms)`);
76834
76796
  // A drag fling: allow the full swipe velocity (see WHEEL_FLING_MAX_VELOCITY)
76835
76797
  // so a hard swipe carries across the list instead of being clipped to a few
76836
76798
  // rows. The mouse wheel never reaches here (it uses wheelSettle/glide).