@jsenv/navi 0.29.316 → 0.29.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3678,7 +3678,7 @@ const createAction = (callback, rootOptions = {}) => {
3678
3678
  toString: () => actionCallSourceSignal.peek(),
3679
3679
  meta,
3680
3680
  debug: (...args) => {
3681
- if (!meta.debug && !DEBUG$2) {
3681
+ if ((!meta.debug && !DEBUG$2)) {
3682
3682
  return;
3683
3683
  }
3684
3684
  console.debug(...args);
@@ -12922,68 +12922,153 @@ const useResetErrorBoundary = () => {
12922
12922
  return resetErrorBoundary;
12923
12923
  };
12924
12924
 
12925
- const DebugCommandContext = createContext(false);
12926
- const DebugInteractionContext = createContext(false);
12927
- const DebugFocusContext = createContext(false);
12928
- const DebugScrollContext = createContext(false);
12929
- const DebugPopupContext = createContext(false);
12930
- const DebugActionContext = createContext(false);
12931
- const DebugUIStateContext = createContext(false);
12925
+ /*
12926
+ * Everything dev-only in this module hangs off one `import.meta.dev` test at
12927
+ * the binding level (`import.meta.dev ? naviDebug.x : noop`), not inside the
12928
+ * function bodies. Rollup follows a binding to the function it names, but
12929
+ * not an early `return noop` inside a body: the first shape lets it drop every
12930
+ * `debugX("…")` call site of the production build, the second keeps them all.
12931
+ */
12932
+
12932
12933
  const debugNoop = () => {};
12933
- const eventGroupLogger = createEventGroupLogger();
12934
- const debugCommandDefault = eventGroupLogger.createCategory("[command]", "#8e44ad");
12935
- const debugInteractionDefault = eventGroupLogger.createCategory("[interaction]", "#2980b9");
12936
- const debugActionDefault = eventGroupLogger.createCategory("[action]", "#e67e22");
12937
- const debugPopupDefault = eventGroupLogger.createCategory("[popup]", "#27ae60");
12938
- const debugUIStateDefault = eventGroupLogger.createCategory("[uistate]", "#7f8c8d");
12939
- const debugFocusDefault = eventGroupLogger.createCategory("[focus]", "#2980b9");
12940
- const debugScrollDefault = eventGroupLogger.createCategory("[scroll]", "#2980b9");
12934
+ const createNaviDebug = () => {
12935
+ const DebugCommandContext = createContext(false);
12936
+ const DebugInteractionContext = createContext(false);
12937
+ const DebugFocusContext = createContext(false);
12938
+ const DebugScrollContext = createContext(false);
12939
+ const DebugPopupContext = createContext(false);
12940
+ const DebugActionContext = createContext(false);
12941
+ const DebugUIStateContext = createContext(false);
12942
+ const eventGroupLogger = createEventGroupLogger();
12943
+ const debugCommandDefault = eventGroupLogger.createCategory("[command]", "#8e44ad");
12944
+ const debugInteractionDefault = eventGroupLogger.createCategory("[interaction]", "#2980b9");
12945
+ const debugActionDefault = eventGroupLogger.createCategory("[action]", "#e67e22");
12946
+ const debugPopupDefault = eventGroupLogger.createCategory("[popup]", "#27ae60");
12947
+ const debugUIStateDefault = eventGroupLogger.createCategory("[uistate]", "#7f8c8d");
12948
+ const debugFocusDefault = eventGroupLogger.createCategory("[focus]", "#2980b9");
12949
+ const debugScrollDefault = eventGroupLogger.createCategory("[scroll]", "#2980b9");
12950
+ const useDebugCommand = () => {
12951
+ const debug = useContext(DebugCommandContext);
12952
+ return debug || debugNoop;
12953
+ };
12954
+ const useDebugInteraction = () => {
12955
+ const debug = useContext(DebugInteractionContext);
12956
+ return debug || debugNoop;
12957
+ };
12958
+ const useDebugFocus = () => {
12959
+ const debug = useContext(DebugFocusContext);
12960
+ return debug || debugNoop;
12961
+ };
12962
+ const useDebugScroll = () => {
12963
+ const debug = useContext(DebugScrollContext);
12964
+ return debug || debugNoop;
12965
+ };
12966
+ const useDebugPopup = () => {
12967
+ const debug = useContext(DebugPopupContext);
12968
+ return debug || debugNoop;
12969
+ };
12970
+ const useDebugAction = () => {
12971
+ const debug = useContext(DebugActionContext);
12972
+ return debug || debugNoop;
12973
+ };
12974
+ const useDebugUIState = () => {
12975
+ const debug = useContext(DebugUIStateContext);
12976
+ return debug || debugNoop;
12977
+ };
12978
+ const NaviDebug = ({
12979
+ debugAll,
12980
+ debugCommand = debugAll,
12981
+ debugInteraction = debugAll,
12982
+ debugFocus = debugAll,
12983
+ debugScroll = debugAll,
12984
+ debugPopup = debugAll,
12985
+ debugAction = debugAll,
12986
+ debugUIState = debugAll,
12987
+ children
12988
+ }) => {
12989
+ if (debugCommand === true) {
12990
+ debugCommand = debugCommandDefault;
12991
+ }
12992
+ if (debugInteraction === true) {
12993
+ debugInteraction = debugInteractionDefault;
12994
+ }
12995
+ if (debugFocus === true || debugInteraction && debugFocus === undefined) {
12996
+ debugFocus = debugFocusDefault;
12997
+ }
12998
+ if (debugScroll === true || debugInteraction && debugScroll === undefined) {
12999
+ debugScroll = debugScrollDefault;
13000
+ }
13001
+ if (debugPopup === true || debugInteraction && debugPopup === undefined) {
13002
+ debugPopup = debugPopupDefault;
13003
+ }
13004
+ if (debugAction === true) {
13005
+ debugAction = debugActionDefault;
13006
+ }
13007
+ if (debugUIState === true) {
13008
+ debugUIState = debugUIStateDefault;
13009
+ }
13010
+ return jsx(DebugCommandContext.Provider, {
13011
+ value: debugCommand,
13012
+ children: jsx(DebugInteractionContext.Provider, {
13013
+ value: debugInteraction,
13014
+ children: jsx(DebugFocusContext.Provider, {
13015
+ value: debugFocus,
13016
+ children: jsx(DebugScrollContext.Provider, {
13017
+ value: debugScroll,
13018
+ children: jsx(DebugPopupContext.Provider, {
13019
+ value: debugPopup,
13020
+ children: jsx(DebugActionContext.Provider, {
13021
+ value: debugAction,
13022
+ children: jsx(DebugUIStateContext.Provider, {
13023
+ value: debugUIState,
13024
+ children: children
13025
+ })
13026
+ })
13027
+ })
13028
+ })
13029
+ })
13030
+ })
13031
+ });
13032
+ };
13033
+ return {
13034
+ useDebugCommand,
13035
+ useDebugInteraction,
13036
+ useDebugFocus,
13037
+ useDebugScroll,
13038
+ useDebugPopup,
13039
+ useDebugAction,
13040
+ useDebugUIState,
13041
+ NaviDebug
13042
+ };
13043
+ };
13044
+ const naviDebug = createNaviDebug() ;
12941
13045
 
12942
13046
  // The hooks below expose one concern's logger to components inside <NaviDebug>.
12943
13047
  // Each returns the logger function enabled for that concern, or a no-op when the
12944
13048
  // concern is off — so call sites can `const debug = useDebugX()` unconditionally.
12945
13049
  // The logger is called as `debug(message, …)` or, to group a side effect under
12946
13050
  // the native event that caused it, `debug(event, message, …)`.
13051
+ // In production every hook is the no-op returner and the bundler drops the
13052
+ // call sites along with their messages.
12947
13053
 
12948
13054
  /** Logger for navi command dispatch (`--navi-*`), or a no-op when disabled. */
12949
- const useDebugCommand = () => {
12950
- const debug = useContext(DebugCommandContext);
12951
- return debug || debugNoop;
12952
- };
13055
+ const useDebugCommand = naviDebug.useDebugCommand ;
12953
13056
  /** Logger for gated interactions (click/scroll/select/…), or a no-op. */
12954
- const useDebugInteraction = () => {
12955
- const debug = useContext(DebugInteractionContext);
12956
- return debug || debugNoop;
12957
- };
13057
+ const useDebugInteraction = naviDebug.useDebugInteraction ;
12958
13058
  /** Logger for focus moves and focus-visible decisions, or a no-op. */
12959
- const useDebugFocus = () => {
12960
- const debug = useContext(DebugFocusContext);
12961
- return debug || debugNoop;
12962
- };
13059
+ const useDebugFocus = naviDebug.useDebugFocus ;
12963
13060
  /**
12964
13061
  * Logger for virtual scroll / wheel motion (drag, momentum, glide) and for what
12965
13062
  * a virtualized list does about it — the render window moving, and the rows the
12966
13063
  * run asks for or decides not to ask for. Or a no-op.
12967
13064
  */
12968
- const useDebugScroll = () => {
12969
- const debug = useContext(DebugScrollContext);
12970
- return debug || debugNoop;
12971
- };
13065
+ const useDebugScroll = naviDebug.useDebugScroll ;
12972
13066
  /** Logger for popover/dialog open/close/positioning, or a no-op. */
12973
- const useDebugPopup = () => {
12974
- const debug = useContext(DebugPopupContext);
12975
- return debug || debugNoop;
12976
- };
13067
+ const useDebugPopup = naviDebug.useDebugPopup ;
12977
13068
  /** Logger for the action lifecycle (request → run → end), or a no-op. */
12978
- const useDebugAction = () => {
12979
- const debug = useContext(DebugActionContext);
12980
- return debug || debugNoop;
12981
- };
13069
+ const useDebugAction = naviDebug.useDebugAction ;
12982
13070
  /** Logger for UI-state transitions, validation and synthetic events, or a no-op. */
12983
- const useDebugUIState = () => {
12984
- const debug = useContext(DebugUIStateContext);
12985
- return debug || debugNoop;
12986
- };
13071
+ const useDebugUIState = naviDebug.useDebugUIState ;
12987
13072
 
12988
13073
  /**
12989
13074
  * Turns on navi's color-coded console logging for everything rendered inside it.
@@ -13002,6 +13087,8 @@ const useDebugUIState = () => {
13002
13087
  * `debugScroll` and `debugPopup` unless those are set explicitly, since they
13003
13088
  * describe the same interaction.
13004
13089
  *
13090
+ * Dev-only: the production build of navi renders the children and logs nothing.
13091
+ *
13005
13092
  * @param {object} props
13006
13093
  * @param {boolean|Function} [props.debugAll] - Default for every concern below.
13007
13094
  * @param {boolean|Function} [props.debugCommand] - navi command dispatch (`--navi-*`).
@@ -13027,61 +13114,12 @@ const useDebugUIState = () => {
13027
13114
  * <Wheel>…</Wheel>
13028
13115
  * </NaviDebug>
13029
13116
  */
13030
- const NaviDebug = ({
13031
- debugAll,
13032
- debugCommand = debugAll,
13033
- debugInteraction = debugAll,
13034
- debugFocus = debugAll,
13035
- debugScroll = debugAll,
13036
- debugPopup = debugAll,
13037
- debugAction = debugAll,
13038
- debugUIState = debugAll,
13039
- children
13040
- }) => {
13041
- if (debugCommand === true) {
13042
- debugCommand = debugCommandDefault;
13043
- }
13044
- if (debugInteraction === true) {
13045
- debugInteraction = debugInteractionDefault;
13046
- }
13047
- if (debugFocus === true || debugInteraction && debugFocus === undefined) {
13048
- debugFocus = debugFocusDefault;
13049
- }
13050
- if (debugScroll === true || debugInteraction && debugScroll === undefined) {
13051
- debugScroll = debugScrollDefault;
13052
- }
13053
- if (debugPopup === true || debugInteraction && debugPopup === undefined) {
13054
- debugPopup = debugPopupDefault;
13055
- }
13056
- if (debugAction === true) {
13057
- debugAction = debugActionDefault;
13058
- }
13059
- if (debugUIState === true) {
13060
- debugUIState = debugUIStateDefault;
13061
- }
13062
- return jsx(DebugCommandContext.Provider, {
13063
- value: debugCommand,
13064
- children: jsx(DebugInteractionContext.Provider, {
13065
- value: debugInteraction,
13066
- children: jsx(DebugFocusContext.Provider, {
13067
- value: debugFocus,
13068
- children: jsx(DebugScrollContext.Provider, {
13069
- value: debugScroll,
13070
- children: jsx(DebugPopupContext.Provider, {
13071
- value: debugPopup,
13072
- children: jsx(DebugActionContext.Provider, {
13073
- value: debugAction,
13074
- children: jsx(DebugUIStateContext.Provider, {
13075
- value: debugUIState,
13076
- children: children
13077
- })
13078
- })
13079
- })
13080
- })
13081
- })
13082
- })
13083
- });
13084
- };
13117
+ const NaviDebug = naviDebug.NaviDebug ;
13118
+
13119
+ // For a caller whose message costs something to build (a line of numbers
13120
+ // formatted on every placement): nothing to build for a logger that logs nothing.
13121
+ // Always true in production, so the bundler drops the building too.
13122
+ const isDebugNoop = debug => debug === debugNoop ;
13085
13123
 
13086
13124
  const actionErrorWeakMap = new WeakMap();
13087
13125
  const NAVI_ACTION_ERROR_CONSTRAINT = {
@@ -22328,17 +22366,19 @@ const createUITransitionController = (
22328
22366
  pauseBreakpoints = [],
22329
22367
  } = {},
22330
22368
  ) => {
22331
- const debugConfig = {
22332
- detection: root.hasAttribute("data-debug-detection"),
22333
- size: root.hasAttribute("data-debug-size"),
22334
- };
22335
- const hasDebugLogs = debugConfig.size;
22369
+ const hasDebugDetection =
22370
+ root.hasAttribute("data-debug-detection");
22371
+ const hasDebugLogs = root.hasAttribute("data-debug-size");
22336
22372
  const debugDetection = (message) => {
22337
- if (!debugConfig.detection) return;
22373
+ if (!hasDebugDetection) {
22374
+ return;
22375
+ }
22338
22376
  console.debug(`[detection]`, message);
22339
22377
  };
22340
22378
  const debugSize = (message) => {
22341
- if (!debugConfig.size) return;
22379
+ if (!hasDebugLogs) {
22380
+ return;
22381
+ }
22342
22382
  console.debug(`[size]`, message);
22343
22383
  };
22344
22384
 
@@ -23188,7 +23228,8 @@ const useUITransitionContentId = value => {
23188
23228
 
23189
23229
 
23190
23230
  const DEBUG$1 =
23191
- typeof process === "object" ? process.env.DEBUG === "true" : false;
23231
+ typeof process === "object" &&
23232
+ process.env.DEBUG === "true";
23192
23233
  const debug$2 = (...args) => {
23193
23234
  if (DEBUG$1) {
23194
23235
  console.debug(...args);
@@ -25615,9 +25656,30 @@ const route = (
25615
25656
  });
25616
25657
  // methods
25617
25658
  registerSetup(({ routeSet, getUrl }) => {
25659
+ // One computed per set of params. Building a url reads the route family's
25660
+ // signals (see readSignalForUrlBuild) and verifies the candidate against
25661
+ // every pattern of the family; a list of a hundred links to one route
25662
+ // would do that a hundred times on each of its renders. A computed does it
25663
+ // once, again only when a signal it read changes — and a component reading
25664
+ // it subscribes to exactly those, which is what reading the signals during
25665
+ // render gave it.
25666
+ const relativeUrlByParamsKey = new Map();
25618
25667
  route.buildRelativeUrl = (params) => {
25619
- // buildMostPreciseUrl now handles parameter resolution internally
25620
- return routePattern.buildMostPreciseUrl(params);
25668
+ const paramsKey = getParamsCacheKey(params);
25669
+ if (paramsKey === null) {
25670
+ return routePattern.buildMostPreciseUrl(params);
25671
+ }
25672
+ let relativeUrlComputed = relativeUrlByParamsKey.get(paramsKey);
25673
+ if (!relativeUrlComputed) {
25674
+ if (relativeUrlByParamsKey.size >= PARAMS_CACHE_MAX) {
25675
+ relativeUrlByParamsKey.clear();
25676
+ }
25677
+ relativeUrlComputed = computed(() =>
25678
+ routePattern.buildMostPreciseUrl(params),
25679
+ );
25680
+ relativeUrlByParamsKey.set(paramsKey, relativeUrlComputed);
25681
+ }
25682
+ return relativeUrlComputed.value;
25621
25683
  };
25622
25684
  route.buildUrl = (params) => {
25623
25685
  const routeRelativeUrl = route.buildRelativeUrl(params);
@@ -26413,6 +26475,36 @@ const setOnAllRouteReady = (callback) => {
26413
26475
  onAllRouteReady = callback;
26414
26476
  };
26415
26477
 
26478
+ // Params are cached by content, and only params whose content a string can
26479
+ // stand for: anything else (an object, a function) builds without the cache.
26480
+ // A key set to undefined is content too — it says "without this one", which
26481
+ // a params object not naming it does not say (see buildIntendedState).
26482
+ const PARAMS_CACHE_MAX = 1000;
26483
+ const getParamsCacheKey = (params) => {
26484
+ if (params === undefined || params === null) {
26485
+ return "";
26486
+ }
26487
+ if (typeof params !== "object") {
26488
+ return null;
26489
+ }
26490
+ const keys = Object.keys(params).sort();
26491
+ let key = "";
26492
+ for (const name of keys) {
26493
+ const value = params[name];
26494
+ const type = typeof value;
26495
+ if (
26496
+ value !== null &&
26497
+ type !== "string" &&
26498
+ type !== "number" &&
26499
+ type !== "boolean"
26500
+ ) {
26501
+ return null;
26502
+ }
26503
+ key += `${name}=${type}:${value}\n`;
26504
+ }
26505
+ return key;
26506
+ };
26507
+
26416
26508
  /**
26417
26509
  * Is there an entry of THIS document behind the current one? And ahead of it?
26418
26510
  *
@@ -42736,6 +42828,73 @@ const LoadingOutlineWithPortal = props => {
42736
42828
  });
42737
42829
  };
42738
42830
 
42831
+ /**
42832
+ * Reads of the layout gathered across effects, so that the browser brings its
42833
+ * styles and layout up to date once for all of them rather than once per
42834
+ * element.
42835
+ *
42836
+ * A layout effect that reads (a computed style, a rect) and then writes (an
42837
+ * attribute, a style) is fine on its own. A hundred of them in one commit are
42838
+ * not: every write dirties the tree, and the next element's read forces the
42839
+ * browser to recompute it — a style recalculation per element, each one over
42840
+ * everything dirtied since the last. Most of a long mount's time goes there,
42841
+ * not in the effects themselves.
42842
+ *
42843
+ * So an effect hands its read over instead of running it. Reads run together
42844
+ * in a microtask — still before the paint, so nothing shows uncorrected — and
42845
+ * each returns the write that depends on it, run after every read of the
42846
+ * round. A write that needs to read again returns that read: it joins the next
42847
+ * round, reads first, writes after, and so on until nothing is left.
42848
+ *
42849
+ * The cancel returned is for an element unmounted before its round: a write
42850
+ * on a node that is gone is at best wasted.
42851
+ *
42852
+ * @param {() => (undefined | (() => undefined | Function))} read
42853
+ * @returns {() => void} cancel
42854
+ */
42855
+ const scheduleLayoutRead = (read) => {
42856
+ const entry = { read, cancelled: false };
42857
+ round.push(entry);
42858
+ if (!flushScheduled) {
42859
+ flushScheduled = true;
42860
+ queueMicrotask(flush);
42861
+ }
42862
+ return () => {
42863
+ entry.cancelled = true;
42864
+ };
42865
+ };
42866
+
42867
+ let round = [];
42868
+ let flushScheduled = false;
42869
+
42870
+ const flush = () => {
42871
+ flushScheduled = false;
42872
+ while (round.length > 0) {
42873
+ const entries = round;
42874
+ round = [];
42875
+ const writes = [];
42876
+ for (const entry of entries) {
42877
+ if (entry.cancelled) {
42878
+ continue;
42879
+ }
42880
+ const write = entry.read();
42881
+ if (typeof write === "function") {
42882
+ writes.push({ entry, write });
42883
+ }
42884
+ }
42885
+ for (const { entry, write } of writes) {
42886
+ if (entry.cancelled) {
42887
+ continue;
42888
+ }
42889
+ const readAgain = write();
42890
+ if (typeof readAgain === "function") {
42891
+ entry.read = readAgain;
42892
+ round.push(entry);
42893
+ }
42894
+ }
42895
+ }
42896
+ };
42897
+
42739
42898
  installImportMetaCssBuild(import.meta);
42740
42899
  const css$_ = /* css */`.navi_text_anchor {
42741
42900
  vertical-align: baseline;
@@ -42777,6 +42936,16 @@ const TextAnchor = ({
42777
42936
  }) => {
42778
42937
  import.meta.css = [css$_, "@jsenv/navi/src/text/text_anchor.jsx"];
42779
42938
  const anchorRef = useRef();
42939
+ // The correction reads the layout, with the other reads of the commit (see
42940
+ // layout_batch.js) — an anchor per icon, each measured between two writes,
42941
+ // is a style recalculation per icon. Cancelled by a newer correction or by
42942
+ // the unmount: a write on a node that is gone is wasted.
42943
+ const cancelReadRef = useRef(null);
42944
+ useLayoutEffect(() => {
42945
+ return () => {
42946
+ cancelReadRef.current?.();
42947
+ };
42948
+ }, []);
42780
42949
 
42781
42950
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
42782
42951
  // (e.g. a closed SidePanel) is still display:none — every rect involved
@@ -42805,23 +42974,36 @@ const TextAnchor = ({
42805
42974
  if (!anchorEl || !childEl) {
42806
42975
  return;
42807
42976
  }
42808
- // Only correct when the anchor lives in an inline formatting context.
42809
- // If the parent is a flex/grid container, inline layout rules don't apply
42810
- // and our font-metrics model is invalid.
42811
- const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
42812
- if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
42813
- // we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
42814
- anchorEl.setAttribute("hidden", "");
42815
- setTopOffset(childEl, 0);
42816
- return;
42817
- }
42818
- anchorEl.removeAttribute("hidden");
42819
- const topOffset = computeTopOffset({
42820
- anchorEl,
42821
- childEl,
42822
- textAnchor
42977
+ cancelReadRef.current?.();
42978
+ cancelReadRef.current = scheduleLayoutRead(() => {
42979
+ // Only correct when the anchor lives in an inline formatting context.
42980
+ // If the parent is a flex/grid container, inline layout rules don't
42981
+ // apply and our font-metrics model is invalid.
42982
+ const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
42983
+ if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
42984
+ return () => {
42985
+ // we must hide the anchor otherwise it would affect layout without
42986
+ // providing any benefit (would trigger flex gap for instance)
42987
+ anchorEl.setAttribute("hidden", "");
42988
+ setTopOffset(childEl, 0);
42989
+ };
42990
+ }
42991
+ return () => {
42992
+ // The anchor has to be in the flow to be measured: shown here, read
42993
+ // in the next round, with the other anchors shown by this one.
42994
+ anchorEl.removeAttribute("hidden");
42995
+ return () => {
42996
+ const topOffset = computeTopOffset({
42997
+ anchorEl,
42998
+ childEl,
42999
+ textAnchor
43000
+ });
43001
+ return () => {
43002
+ setTopOffset(childEl, topOffset);
43003
+ };
43004
+ };
43005
+ };
42823
43006
  });
42824
- setTopOffset(childEl, topOffset);
42825
43007
  }, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
42826
43008
  return jsxs(Fragment, {
42827
43009
  children: [children, jsx("span", {
@@ -48501,53 +48683,35 @@ const useNextResolver = () => useContext(NextResolverContext);
48501
48683
  * // Then inside a component render:
48502
48684
  * renderButton(props)
48503
48685
  *
48504
- * NextResolverContext exposes a stable Next component so resolvers can continue
48505
- * the chain via useNextResolver().
48506
- * ResolverIndexContext tracks which resolver is next so that when a resolver
48507
- * re-renders and calls Next, the chain resumes from the correct position.
48686
+ * Each position of the chain has a runner of its own, defined once: it renders
48687
+ * its resolver under a NextResolverContext holding the runner after it. A
48688
+ * resolver that re-renders on its own and renders <Next> therefore resumes at
48689
+ * its own position without any bookkeeping — the Next it was given is the one
48690
+ * made for it. A chain rendered by the hundred (a list's rows) pays two
48691
+ * components per position, the runner and the resolver, so nothing here is a
48692
+ * component that merely forwards.
48508
48693
  */
48509
48694
  const createComponentResolver = (resolvers, {
48510
48695
  pure
48511
48696
  } = {}) => {
48512
- const ResolverIndexContext = createContext(0);
48513
- const ChainRunner = props => {
48514
- const index = useContext(ResolverIndexContext);
48515
- if (index >= resolvers.length) {
48516
- return null;
48517
- }
48697
+ const runners = [];
48698
+ const lastIndex = resolvers.length - 1;
48699
+ for (let index = 0; index < resolvers.length; index++) {
48518
48700
  const Resolver = resolvers[index];
48519
- const isLast = index === resolvers.length - 1;
48520
- return jsx(ResolverIndexContext.Provider, {
48521
- value: index + 1,
48522
- children: isLast ? jsx(NextResolverContext.Provider, {
48523
- value: null,
48524
- children: jsx(Resolver, {
48525
- ...props
48526
- })
48527
- }) : jsx(Resolver, {
48701
+ const isLast = index === lastIndex;
48702
+ const Runner = props => jsx(NextResolverContext.Provider, {
48703
+ value: isLast ? null : runners[index + 1],
48704
+ children: jsx(Resolver, {
48528
48705
  ...props
48529
48706
  })
48530
48707
  });
48531
- };
48532
-
48533
- // Stable component defined once per createComponentResolver call.
48534
- // Renders ChainRunner directly — no new providers — so ResolverIndexContext
48535
- // is inherited from the parent tree. When a resolver calls <Next>, the chain
48536
- // resumes from index+1 (already set by the Provider wrapping that resolver).
48537
- const NextComponent = props => jsx(ChainRunner, {
48708
+ Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
48709
+ runners.push(Runner);
48710
+ }
48711
+ const FirstRunner = runners[0];
48712
+ const renderComponent = props => jsx(FirstRunner, {
48538
48713
  ...props
48539
48714
  });
48540
- const renderComponent = props => {
48541
- return jsx(NextResolverContext.Provider, {
48542
- value: NextComponent,
48543
- children: jsx(ResolverIndexContext.Provider, {
48544
- value: 0,
48545
- children: jsx(ChainRunner, {
48546
- ...props
48547
- })
48548
- })
48549
- });
48550
- };
48551
48715
  if (!pure) {
48552
48716
  return renderComponent;
48553
48717
  }
@@ -48643,36 +48807,36 @@ const useAccentColorAttributes = (
48643
48807
  return undefined;
48644
48808
  }
48645
48809
  }
48810
+ // Read with the other layout reads of the commit (see layout_batch.js): a
48811
+ // computed color per control, each read between two writes, is a style
48812
+ // recalculation per control.
48813
+ let cancelRead = null;
48646
48814
  const updateAttributes = () => {
48647
- const computedStyle = getComputedStyle(elementToCheck);
48648
- const color = computedStyle[colorProperty];
48649
- if (!color) {
48650
- el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
48651
- el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
48652
- el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
48653
- return;
48654
- }
48655
- const luminance = resolveOklchLightness(color, el);
48656
- if (luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD) {
48657
- el.setAttribute(LIGHT_ACCENT_ATTRIBUTE, "");
48658
- } else {
48659
- el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
48660
- }
48661
- if (luminance !== null && luminance > VERY_LIGHT_LUMINANCE_THRESHOLD) {
48662
- el.setAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE, "");
48663
- } else {
48664
- el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
48665
- }
48666
- const bestContrast = contrastColor(
48667
- color,
48668
- el,
48669
- DARK_CONTRAST_LIGHTNESS_THRESHOLD,
48670
- );
48671
- if (bestContrast === "black") {
48672
- el.setAttribute(DARK_CONTRAST_ATTRIBUTE, "");
48673
- } else {
48674
- el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
48675
- }
48815
+ cancelRead?.();
48816
+ cancelRead = scheduleLayoutRead(() => {
48817
+ const computedStyle = getComputedStyle(elementToCheck);
48818
+ const color = computedStyle[colorProperty];
48819
+ if (!color) {
48820
+ return () => {
48821
+ el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
48822
+ el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
48823
+ el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
48824
+ };
48825
+ }
48826
+ const luminance = resolveOklchLightness(color, el);
48827
+ const light =
48828
+ luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD;
48829
+ const veryLight =
48830
+ luminance !== null && luminance > VERY_LIGHT_LUMINANCE_THRESHOLD;
48831
+ const needsDarkForeground =
48832
+ contrastColor(color, el, DARK_CONTRAST_LIGHTNESS_THRESHOLD) ===
48833
+ "black";
48834
+ return () => {
48835
+ el.toggleAttribute(LIGHT_ACCENT_ATTRIBUTE, light);
48836
+ el.toggleAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE, veryLight);
48837
+ el.toggleAttribute(DARK_CONTRAST_ATTRIBUTE, needsDarkForeground);
48838
+ };
48839
+ });
48676
48840
  };
48677
48841
  updateAttributes();
48678
48842
  const unsubscribeFromPseudoState = subscribeToPseudoState(
@@ -48680,6 +48844,7 @@ const useAccentColorAttributes = (
48680
48844
  updateAttributes,
48681
48845
  );
48682
48846
  return () => {
48847
+ cancelRead?.();
48683
48848
  unsubscribeFromPseudoState();
48684
48849
  el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
48685
48850
  el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
@@ -61006,6 +61171,9 @@ const useDialogProps = props => {
61006
61171
  // strip), and the absence of a line after the keyboard arrives means the
61007
61172
  // placement was never asked to run again.
61008
61173
  const logPlacement = (triggerEvent, position) => {
61174
+ if (isDebugNoop(debugPopup)) {
61175
+ return;
61176
+ }
61009
61177
  // The decided target, never the current rect: the placement animates, so
61010
61178
  // a box read right after applying one is somewhere between the two and
61011
61179
  // reads like a decision nobody took.
@@ -63307,7 +63475,11 @@ const Popup = props => {
63307
63475
  Popup.Close = PopupClose;
63308
63476
 
63309
63477
  installImportMetaCssBuild(import.meta);
63310
- const css$B = /* css */`.navi_popover[data-picker-popup] {
63478
+ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
63479
+ display: none;
63480
+ }
63481
+
63482
+ .navi_popover[data-picker-popup] {
63311
63483
  --popover-border-radius: var(--picker-border-radius);
63312
63484
  --popover-border-width: var(--picker-border-width);
63313
63485
  --popover-border-color: var(--x-picker-border-color);
@@ -64259,10 +64431,14 @@ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
64259
64431
  * closes the controller for real — the popup is already gone, there is no
64260
64432
  * choice left to offer `requestClose`.
64261
64433
  *
64262
- * The content is rendered through a portal into an element this component
64263
- * owns, handed to the callout as its message (a Node, appended as-is). It is
64264
- * rendered whether the callout is open or not, so what the content holds
64265
- * survives a close, the way a popup's `mount="always"` keeps it. The element
64434
+ * The content is rendered into an element this component owns, handed to the
64435
+ * callout as its message (a Node, appended as-is) and taken back when the
64436
+ * callout closes. Between two opens it is docked, hidden, in the span below:
64437
+ * in the document the whole time, the way a popup's `mount="always"` keeps its
64438
+ * content — so what it holds survives a close, and what it measures of itself
64439
+ * at mount (a computed color, a light-dark() pair) resolves against a real
64440
+ * ancestry. An element with no document has no computed style, and a badge
64441
+ * reading its own background there would see nothing at all. The element
64266
64442
  * carries data-picker-content: the callout is appended inside the picker root,
64267
64443
  * and a press in there must read as inside the popup, not on the trigger.
64268
64444
  */
@@ -64282,23 +64458,22 @@ const PickerCalloutPopup = ({
64282
64458
  children
64283
64459
  }) => {
64284
64460
  const hostRef = useRef(null);
64285
- if (!hostRef.current) {
64286
- const host = document.createElement("div");
64287
- host.setAttribute("data-picker-content", "");
64288
- hostRef.current = host;
64289
- }
64290
64461
  // Reassigned on every render, like Popover's own, so it closes over the
64291
64462
  // latest props.
64292
64463
  openController.getElement = () => pickerRef.current;
64293
64464
  openController.openEffect = openEvent => {
64294
64465
  const pickerEl = pickerRef.current;
64466
+ const host = hostRef.current;
64467
+ // Where the content sits while the callout is closed (the span below);
64468
+ // the callout takes the element out of it and the cleanup puts it back.
64469
+ const dock = host.parentNode;
64295
64470
  const calloutManager = getPickerInput(pickerEl).__uiStateController__.rules.callout;
64296
64471
  // Only an anchor the caller named: left unsaid, the manager anchors on the
64297
64472
  // picker's own input — which is where the data-callout-* attributes a
64298
64473
  // caller puts on the picker land, and where the callout reads them.
64299
64474
  const anchorElement = anchor === pickerRef ? undefined : anchor && "current" in anchor ? anchor.current : anchor;
64300
64475
  calloutManager.addOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, {
64301
- message: hostRef.current,
64476
+ message: host,
64302
64477
  // The popup a `popupTestId` names is this callout: it is the surface the
64303
64478
  // picker opens, drawn by navi, so it is the one thing the caller cannot
64304
64479
  // name from its own children.
@@ -64332,6 +64507,7 @@ const PickerCalloutPopup = ({
64332
64507
  return closeEvent => {
64333
64508
  calloutElement.removeEventListener("navi_request_confirm", forwardConfirm);
64334
64509
  calloutManager.removeOpenToken(PICKER_CALLOUT_CONTENT_TOKEN, closeEvent);
64510
+ dock.appendChild(host);
64335
64511
  };
64336
64512
  };
64337
64513
  return (
@@ -64341,13 +64517,18 @@ const PickerCalloutPopup = ({
64341
64517
  as: "span",
64342
64518
  ref: ref,
64343
64519
  id: id,
64520
+ className: "navi_picker_callout_dock",
64344
64521
  style: {
64345
64522
  display: "contents"
64346
64523
  },
64347
64524
  onnavi_request_open: onnavi_request_open,
64348
64525
  onnavi_request_close: onnavi_request_close,
64349
64526
  onnavi_request_confirm: onnavi_request_confirm,
64350
- children: createPortal(children, hostRef.current)
64527
+ children: jsx("div", {
64528
+ ref: hostRef,
64529
+ "data-picker-content": "",
64530
+ children: children
64531
+ })
64351
64532
  })
64352
64533
  );
64353
64534
  };
@@ -64656,6 +64837,43 @@ document.addEventListener(
64656
64837
  { capture: true, passive: true },
64657
64838
  );
64658
64839
 
64840
+ /**
64841
+ * Calls back once the browser has painted what is committed now.
64842
+ *
64843
+ * The callbacks of the next frame run before that frame paints, so the call is
64844
+ * made from a task queued from inside one — the same way preact schedules
64845
+ * useEffect. Unlike an effect, nothing runs this early: preact flushes a
64846
+ * component's pending effects as soon as that component renders again, and a
64847
+ * re-render before the paint is exactly what a caller waiting for the paint
64848
+ * has to survive. A frame not coming at all (a background tab) still answers,
64849
+ * late, through the timeout.
64850
+ *
64851
+ * @returns {() => void} cancel
64852
+ */
64853
+ const FRAME_TIMEOUT_MS = 100;
64854
+
64855
+ const afterPaint = (callback) => {
64856
+ let called = false;
64857
+ let timeoutId;
64858
+ let frameId;
64859
+ const onFrame = () => {
64860
+ if (called) {
64861
+ return;
64862
+ }
64863
+ called = true;
64864
+ clearTimeout(timeoutId);
64865
+ cancelAnimationFrame(frameId);
64866
+ timeoutId = setTimeout(callback, 0);
64867
+ };
64868
+ timeoutId = setTimeout(onFrame, FRAME_TIMEOUT_MS);
64869
+ frameId = requestAnimationFrame(onFrame);
64870
+ return () => {
64871
+ called = true;
64872
+ clearTimeout(timeoutId);
64873
+ cancelAnimationFrame(frameId);
64874
+ };
64875
+ };
64876
+
64659
64877
  const LoadingDotsSvg = () => {
64660
64878
  return jsxs("svg", {
64661
64879
  viewBox: "0 0 200 200",
@@ -66615,6 +66833,32 @@ const css$x = /* css */`@layer navi {
66615
66833
  middle of the padding. On the scroll box the scrollbar stays against the
66616
66834
  border and the padding is what separates the rows from it. */
66617
66835
  const LIST_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
66836
+
66837
+ // Accepts a string too (renderBudget="50" from an HTML attribute): the
66838
+ // arithmetic on the budget (renderBudget / 2, start + renderBudget) would
66839
+ // silently misbehave on a raw string ("+" concatenates).
66840
+ const toRenderBudgetNumber = value => {
66841
+ if (typeof value === "string") {
66842
+ const parsed = Number(value);
66843
+ return Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
66844
+ }
66845
+ if (typeof value === "number") {
66846
+ return value;
66847
+ }
66848
+ return RENDER_BUDGET_DEFAULT;
66849
+ };
66850
+ const resolveRenderBudget = renderBudget => {
66851
+ if (renderBudget && typeof renderBudget === "object") {
66852
+ return {
66853
+ initial: renderBudget.initial === undefined ? undefined : toRenderBudgetNumber(renderBudget.initial),
66854
+ after: toRenderBudgetNumber(renderBudget.after)
66855
+ };
66856
+ }
66857
+ return {
66858
+ initial: undefined,
66859
+ after: toRenderBudgetNumber(renderBudget)
66860
+ };
66861
+ };
66618
66862
  const ListUI = props => {
66619
66863
  import.meta.css = [css$x, "@jsenv/navi/src/control/list/list.jsx"];
66620
66864
  const {
@@ -66663,18 +66907,34 @@ const ListUI = props => {
66663
66907
  delete rest[name];
66664
66908
  }
66665
66909
  }
66666
- // Accept a string (e.g. from an HTML attribute: renderBudget="50") the
66667
- // same way a bare number would work — arithmetic below (renderBudget / 2,
66668
- // start + renderBudget, etc.) would silently misbehave on a raw string
66669
- // ("+" concatenates instead of adding).
66670
- let renderBudget = renderBudgetProp;
66671
- if (typeof renderBudget === "string") {
66672
- const parsed = Number(renderBudget);
66673
- renderBudget = Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
66674
- }
66910
+ // `renderBudget` is a number, or `{ initial, after }`: the window of the
66911
+ // first commit, until the browser has painted it, and the window from then
66912
+ // on. A list opening inside a popup draws in the click that opens it, and
66913
+ // the browser paints nothing before that render ends: rows below the fold
66914
+ // cost the same as rows on screen there, and are drawn to be seen one frame
66915
+ // later just as well. `after` takes over after the paint (see afterPaint for
66916
+ // why not an effect), and the floor of 30 is its: it protects the scrolling
66917
+ // window, not a picture nobody has seen yet.
66918
+ const {
66919
+ initial: initialRenderBudget,
66920
+ after: renderBudgetAfterPaint
66921
+ } = resolveRenderBudget(renderBudgetProp);
66922
+ let renderBudget = renderBudgetAfterPaint;
66675
66923
  if (renderBudget < 30 && !renderBudgetSkipCheck) {
66676
66924
  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}).`);
66677
66925
  }
66926
+ const [firstPaintPending, setFirstPaintPending] = useState(initialRenderBudget !== undefined);
66927
+ useLayoutEffect(() => {
66928
+ if (!firstPaintPending) {
66929
+ return undefined;
66930
+ }
66931
+ return afterPaint(() => {
66932
+ setFirstPaintPending(false);
66933
+ });
66934
+ }, []);
66935
+ if (firstPaintPending) {
66936
+ renderBudget = initialRenderBudget;
66937
+ }
66678
66938
 
66679
66939
  // lockSize: capture the container's dimensions on first render so filtering
66680
66940
  // cannot collapse the layout. Measurement happens on the initial (unfiltered)
@@ -66711,7 +66971,10 @@ const ListUI = props => {
66711
66971
  onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
66712
66972
  }
66713
66973
  });
66714
- virtual.renderBudget = renderBudget;
66974
+ // What the runs ask for and stand for: the steady budget, whatever the
66975
+ // window of the first paint draws — a run asking for the rows of the first
66976
+ // picture and then for the rest is two round trips for one opening.
66977
+ virtual.renderBudget = renderBudgetAfterPaint;
66715
66978
  virtual.scrolled = scrolled ?? defaultScrolled;
66716
66979
  const {
66717
66980
  virtualItemSizeSignal,
@@ -67060,7 +67323,10 @@ const useListScrollSync = ({
67060
67323
  horizontal
67061
67324
  }) => {
67062
67325
  const debugScroll = useDebugScroll();
67063
- const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal);
67326
+ const virtualItemSizeSignal = useVirtualItemSizeSignal(ref, virtualItemSize, horizontal, {
67327
+ virtual,
67328
+ renderBudget
67329
+ });
67064
67330
  const getScroller = () => getScrollerEl(ref.current, scroller, horizontal);
67065
67331
  const getListEl = () => ref.current.querySelector(".navi_list");
67066
67332
  // Which box scrolls is measured (see getScrollerEl), so the answer holds
@@ -67116,6 +67382,28 @@ const useListScrollSync = ({
67116
67382
  });
67117
67383
  const renderWindowRef = useRef(null);
67118
67384
  renderWindowRef.current = renderWindow;
67385
+ // A budget that changes (the first paint's giving way to the scrolling one)
67386
+ // re-frames the window where it stands, in this very render — the way
67387
+ // holdWindow below moves it: nothing else would, the scroll listener only
67388
+ // moves a window the user is about to leave.
67389
+ const renderBudgetRef = useRef(renderBudget);
67390
+ if (renderBudgetRef.current !== renderBudget) {
67391
+ renderBudgetRef.current = renderBudget;
67392
+ const {
67393
+ start
67394
+ } = renderWindowRef.current;
67395
+ const total = virtual.totalSignal.peek();
67396
+ let newStart = start;
67397
+ let newEnd = start + renderBudget;
67398
+ if (total > 0 && newEnd > total) {
67399
+ newEnd = total;
67400
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
67401
+ }
67402
+ renderWindowRef.current = {
67403
+ start: newStart,
67404
+ end: newEnd
67405
+ };
67406
+ }
67119
67407
  const updateRenderWindow = (newStart, newEnd, reason) => {
67120
67408
  const {
67121
67409
  start,
@@ -68429,7 +68717,10 @@ const measureItemSize = (listEl, horizontal) => {
68429
68717
  fromSkeletons
68430
68718
  };
68431
68719
  };
68432
- const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
68720
+ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal, {
68721
+ virtual,
68722
+ renderBudget
68723
+ }) => {
68433
68724
  const virtualSizeSignalRef = useRef(null);
68434
68725
  if (!virtualSizeSignalRef.current) {
68435
68726
  virtualSizeSignalRef.current = signal(virtualItemSizeProp);
@@ -68482,8 +68773,12 @@ const useVirtualItemSizeSignal = (ref, virtualItemSizeProp = 0, horizontal) => {
68482
68773
  // as the rows it was measured on. Written from a layout effect it would
68483
68774
  // resize them one commit later — after the scroll anchoring of that commit
68484
68775
  // had already run, which is exactly the jump anchoring exists to prevent.
68776
+ // And only while some rows are held off screen: the fillers are what the
68777
+ // size is for, and a list drawing every row it has would pay a layout on
68778
+ // each of its renders for a number nothing reads.
68485
68779
  const sizeAlreadyKnown = virtualSizeSignal.peek() !== 0;
68486
- if (!virtualItemSizeProp && sizeAlreadyKnown && ref.current) {
68780
+ const rowsHeldOffScreen = virtual.totalSignal.peek() > renderBudget;
68781
+ if (!virtualItemSizeProp && sizeAlreadyKnown && rowsHeldOffScreen && ref.current) {
68487
68782
  const listEl = ref.current.querySelector(".navi_list");
68488
68783
  const measure = listEl ? measureItemSize(listEl, horizontal) : null;
68489
68784
  if (measure) {
@@ -68648,12 +68943,15 @@ const Fallback = ({
68648
68943
  children: fallback
68649
68944
  });
68650
68945
  };
68946
+ // Reads the row size itself: it is what the size is for, and a run holding
68947
+ // every row it draws must not be redrawn — every row of it — because the size
68948
+ // settled after the first commit.
68651
68949
  const VirtualFiller = ({
68652
68950
  edge,
68653
- itemCount,
68654
- virtualItemSize
68951
+ itemCount
68655
68952
  }) => {
68656
- const sizeToFill = itemCount * virtualItemSize;
68953
+ const virtual = useContext(ListVirtualContext);
68954
+ const sizeToFill = itemCount * virtual.virtualItemSizeSignal.value;
68657
68955
  if (!sizeToFill) {
68658
68956
  return null;
68659
68957
  }
@@ -69707,6 +70005,21 @@ const ListItems = ({
69707
70005
  const virtual = useContext(ListVirtualContext);
69708
70006
  const slotId = useContext(ListSlotContext);
69709
70007
  const renderWindow = useContext(RenderWindowContext);
70008
+ // The vnode drawn for a row, kept by item: a run rendering again (its window
70009
+ // moving, its first paint's budget giving way to the full one) hands preact
70010
+ // the same vnode for a row that has not changed, and preact leaves that
70011
+ // row's whole subtree alone. Only for a `renderItem` that is the same
70012
+ // function as last time — a new one may close over new state — and for a
70013
+ // row at the same index, in the same refreshing state: everything the
70014
+ // function is given.
70015
+ const rowVnodesRef = useRef(null);
70016
+ if (!rowVnodesRef.current || rowVnodesRef.current.renderItem !== renderItem) {
70017
+ rowVnodesRef.current = {
70018
+ renderItem,
70019
+ byItem: new Map()
70020
+ };
70021
+ }
70022
+ const rowVnodesByItem = rowVnodesRef.current.byItem;
69710
70023
  const separator = useContext(SeparatorContext);
69711
70024
  const store = useItemStore({
69712
70025
  items,
@@ -69717,16 +70030,26 @@ const ListItems = ({
69717
70030
  });
69718
70031
  const renderRowSkeleton = renderSkeleton === undefined ? virtual.renderSkeleton : renderSkeleton;
69719
70032
  // A row on its way takes the room the list reserves for it: anything else
69720
- // and the rows drawn stop short of where the scroll says they are.
69721
- const virtualItemSize = virtual.virtualItemSizeSignal.value;
69722
- const skeletonRow = {};
69723
- if (virtualItemSize) {
69724
- if (virtual.horizontal) {
69725
- skeletonRow.rowMinWidth = `${virtualItemSize}px`;
69726
- } else {
69727
- skeletonRow.rowMinHeight = `${virtualItemSize}px`;
70033
+ // and the rows drawn stop short of where the scroll says they are. Read
70034
+ // where a row is actually missing, and not before: the size settles after
70035
+ // the first commit, and a run holding every row it draws would otherwise be
70036
+ // redrawn whole by a number it has no use for.
70037
+ let skeletonRow = null;
70038
+ const getSkeletonRow = () => {
70039
+ if (skeletonRow) {
70040
+ return skeletonRow;
70041
+ }
70042
+ skeletonRow = {};
70043
+ const virtualItemSize = virtual.virtualItemSizeSignal.value;
70044
+ if (virtualItemSize) {
70045
+ if (virtual.horizontal) {
70046
+ skeletonRow.rowMinWidth = `${virtualItemSize}px`;
70047
+ } else {
70048
+ skeletonRow.rowMinHeight = `${virtualItemSize}px`;
70049
+ }
69728
70050
  }
69729
- }
70051
+ return skeletonRow;
70052
+ };
69730
70053
  const runStart = virtual.take(ownerId, store.rowCount, slotId);
69731
70054
  const runEnd = runStart + store.rowCount;
69732
70055
  // The two ways to count the same row. The list numbers its rows from its own
@@ -69895,8 +70218,7 @@ const ListItems = ({
69895
70218
  if (windowFrom > runStart) {
69896
70219
  rows.push(jsx(VirtualFiller, {
69897
70220
  edge: "before",
69898
- itemCount: windowFrom - runStart,
69899
- virtualItemSize: virtualItemSize
70221
+ itemCount: windowFrom - runStart
69900
70222
  }, "navi-list-filler-before"));
69901
70223
  }
69902
70224
  const renderItemState = {
@@ -69910,7 +70232,7 @@ const ListItems = ({
69910
70232
  rows.push(jsx("li", {
69911
70233
  className: "navi_list_failed_rows",
69912
70234
  style: {
69913
- "--size-to-fill": `${failedRowCount * virtualItemSize}px`
70235
+ "--size-to-fill": `${failedRowCount * virtual.virtualItemSizeSignal.value}px`
69914
70236
  },
69915
70237
  children: renderError ? renderError({
69916
70238
  error: store.failure.error,
@@ -69929,7 +70251,17 @@ const ListItems = ({
69929
70251
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
69930
70252
  let rowVnode;
69931
70253
  if (item !== undefined) {
69932
- rowVnode = renderItem(item, rowIndex, renderItemState);
70254
+ const rowVnodeKept = rowVnodesByItem.get(item);
70255
+ if (rowVnodeKept && rowVnodeKept.rowIndex === rowIndex && rowVnodeKept.refreshing === renderItemState.refreshing) {
70256
+ rowVnode = rowVnodeKept.vnode;
70257
+ } else {
70258
+ rowVnode = renderItem(item, rowIndex, renderItemState);
70259
+ rowVnodesByItem.set(item, {
70260
+ vnode: rowVnode,
70261
+ rowIndex,
70262
+ refreshing: renderItemState.refreshing
70263
+ });
70264
+ }
69933
70265
  } else if (renderRowSkeleton === false) {
69934
70266
  // The row must still take its room: without it the rows below would
69935
70267
  // climb up and slide back down as the answer arrives.
@@ -69960,7 +70292,7 @@ const ListItems = ({
69960
70292
  value: item === undefined ? {
69961
70293
  id: key,
69962
70294
  index: rowIndex,
69963
- ...skeletonRow
70295
+ ...getSkeletonRow()
69964
70296
  } : {
69965
70297
  id: key,
69966
70298
  index: rowIndex,
@@ -69975,8 +70307,7 @@ const ListItems = ({
69975
70307
  if (runEnd > windowTo) {
69976
70308
  rows.push(jsx(VirtualFiller, {
69977
70309
  edge: "after",
69978
- itemCount: runEnd - windowTo,
69979
- virtualItemSize: virtualItemSize
70310
+ itemCount: runEnd - windowTo
69980
70311
  }, "navi-list-filler-after"));
69981
70312
  }
69982
70313
  return rows;
@@ -70742,6 +71073,25 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
70742
71073
  * starts. Keep it to come back to it
70743
71074
  * later through `scrolled`/`defaultScrolled` — an index would not do, since
70744
71075
  * rows get inserted while a list is being read.
71076
+ * @param {number|{initial: number, after: number}} [props.renderBudget=100]
71077
+ * How many rows of a `<List.Items>` run are in the DOM at once: the render
71078
+ * window, which slides as the user scrolls while fillers hold the room of
71079
+ * the rows outside it. Rows declared one by one as `<List.Item>` children
71080
+ * are all drawn, whatever this says — a list with more than a few dozen rows
71081
+ * gives them to a run (see docs/scroll.md, "Many rows"). Below 30 the list
71082
+ * warns: a window shorter than a tall screen shows blank fillers.
71083
+ *
71084
+ * `{ initial, after }` for a list drawn in the click that opens it (a popup):
71085
+ * `initial` rows in the commit the browser paints first — about what a phone
71086
+ * screen shows — and `after` from the paint on, where the floor of 30
71087
+ * applies. The runs ask their source for `after` rows from the start, so the
71088
+ * first picture costs no second request.
71089
+ * @param {number} [props.virtualItemSize]
71090
+ * The size of one row along the scroll axis, in px, when every row has the
71091
+ * same: what the fillers are sized with and what a scroll position is
71092
+ * estimated from. Left out, the list measures its rows — once when it
71093
+ * mounts, again when a popup around it opens, and after each commit while
71094
+ * rows are held off screen. Given, it never measures.
70745
71095
  * @param {"self"|"parent"|"document"|Element|{current: Element}} [props.scroller="self"]
70746
71096
  * Which box scrolls. `"self"` gives the list a scroll box of its own;
70747
71097
  * `"parent"` makes it virtualize against the scrollable ancestor it lives in
@@ -72120,6 +72470,7 @@ const css$t = /* css */`@layer navi {
72120
72470
  --picker-icon-color-disabled: var(--picker-icon-color-readonly);
72121
72471
  --picker-align-x-default: flex-start;
72122
72472
  --picker-align-y-default: center;
72473
+ --picker-text-align-default: initial;
72123
72474
  }
72124
72475
  }
72125
72476
 
@@ -72258,7 +72609,7 @@ const css$t = /* css */`@layer navi {
72258
72609
  }
72259
72610
 
72260
72611
  &[navi-single-line] {
72261
- & .navi_picker_right_slot {
72612
+ & > .navi_picker_box > .navi_picker_right_slot {
72262
72613
  align-self: var(--x-picker-align-y);
72263
72614
  }
72264
72615
  }
@@ -72295,7 +72646,7 @@ const css$t = /* css */`@layer navi {
72295
72646
  }
72296
72647
 
72297
72648
  &[navi-ui-custom] {
72298
- & .navi_picker_input {
72649
+ & > .navi_picker_box > .navi_picker_input {
72299
72650
  top: calc(-1 * (var(--picker-border-width) + var(--x-picker-press-padding-top)));
72300
72651
  right: calc(-1 *
72301
72652
  (var(--picker-border-width) + var(--x-picker-press-padding-right)));
@@ -72358,10 +72709,10 @@ const css$t = /* css */`@layer navi {
72358
72709
  --x-picker-cursor: pointer;
72359
72710
  }
72360
72711
 
72361
- &[data-focus-within]:has(.navi_picker_input[data-focus-visible]), &[data-focus-visible] {
72712
+ &[data-focus-within]:has( > .navi_picker_box > .navi_picker_input[data-focus-visible]), &[data-focus-visible] {
72362
72713
  --x-picker-border-color: transparent;
72363
72714
 
72364
- & .navi_picker_box {
72715
+ & > .navi_picker_box {
72365
72716
  outline-style: solid;
72366
72717
  }
72367
72718
  }
@@ -72419,7 +72770,7 @@ const css$t = /* css */`@layer navi {
72419
72770
  --picker-background-color-disabled: var(--picker-background-color);
72420
72771
  --picker-icon-color: currentColor;
72421
72772
 
72422
- & .navi_picker_box {
72773
+ & > .navi_picker_box {
72423
72774
  z-index: -1;
72424
72775
  position: absolute;
72425
72776
  inset: 0;
@@ -72444,11 +72795,11 @@ const css$t = /* css */`@layer navi {
72444
72795
  font-family: inherit;
72445
72796
  line-height: inherit;
72446
72797
 
72447
- & .navi_picker_box {
72798
+ & > .navi_picker_box {
72448
72799
  min-height: 0;
72449
72800
  }
72450
72801
 
72451
- & .navi_picker_value[data-picker-facade] {
72802
+ & > .navi_picker_box > .navi_picker_value[data-picker-facade] {
72452
72803
  align-items: var(--x-picker-align-y);
72453
72804
  display: flex;
72454
72805
  }
@@ -72499,11 +72850,11 @@ const css$t = /* css */`@layer navi {
72499
72850
  --picker-padding-y-default: 0;
72500
72851
  --picker-align-x-default: center;
72501
72852
 
72502
- & .navi_picker_box {
72853
+ & > .navi_picker_box {
72503
72854
  min-height: 0;
72504
72855
  }
72505
72856
 
72506
- & .navi_picker_value {
72857
+ & > .navi_picker_box > .navi_picker_value {
72507
72858
  flex-grow: 0;
72508
72859
  align-items: center;
72509
72860
  display: inline-flex;