@jsenv/navi 0.29.493 → 0.29.495

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.
@@ -6,7 +6,7 @@ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, win
6
6
  import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, overflowGoesToViewport, parsePositionArea, snapToPixel, trapFocusInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
- import { useState, useLayoutEffect, useErrorBoundary, useContext, useMemo, useRef, useEffect, useReducer, useCallback, useId } from "preact/hooks";
9
+ import { useState, useLayoutEffect, useErrorBoundary, useContext, useMemo, useRef, useEffect, useReducer, useCallback } from "preact/hooks";
10
10
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
11
11
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatMinuteDuration, formatTime, formatSecondDuration, formatHourDuration, formatTimeRelative, pluralCategory, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatByteSize, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatList, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText, pluralCategory } from "@jsenv/humanize";
@@ -14058,13 +14058,13 @@ const writePressWritesHere = (pressWrites) => {
14058
14058
  }
14059
14059
  };
14060
14060
 
14061
- // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
14062
- // mask is derived from render order within the nearest root/async boundary:
14063
- // stable across re-renders of the same mount, but not across two mounts on the
14064
- // same page, and not across two documents.
14065
- const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
14066
- const isLikelyPreactGeneratedId = (id) => {
14067
- return PREACT_GENERATED_ID_REGEX.test(id);
14061
+ // An id made up at render names one mount: navi's own ("navi_<n>", see
14062
+ // useGeneratedId) counts mounts in the document, and preact's useId(), which
14063
+ // an app may hand a component, gives "P<mask0>-<mask1>" from the render order.
14064
+ // Neither names the same component in two mounts or two documents.
14065
+ const GENERATED_ID_REGEX = /^(?:navi_\d+|P\d+-\d+)/;
14066
+ const isGeneratedId = (id) => {
14067
+ return GENERATED_ID_REGEX.test(id);
14068
14068
  };
14069
14069
 
14070
14070
  /**
@@ -14088,7 +14088,7 @@ const dropGeneratedIdKeys = (state) => {
14088
14088
  }
14089
14089
  let stateWithoutGeneratedIdKeys = state;
14090
14090
  for (const key of Object.keys(state)) {
14091
- if (!isLikelyPreactGeneratedId(key)) {
14091
+ if (!isGeneratedId(key)) {
14092
14092
  continue;
14093
14093
  }
14094
14094
  if (stateWithoutGeneratedIdKeys === state) {
@@ -15393,7 +15393,7 @@ Consider using unique IDs for each component instance.`,
15393
15393
  return useNavStateBasic(id, options);
15394
15394
  };
15395
15395
 
15396
- // A generated id (preact's useId()) names one mount. State written under one
15396
+ // A generated id (see isGeneratedId) names one mount. State written under one
15397
15397
  // outlives that mount in the history entry — a page left with a popup open —
15398
15398
  // and the mount coming back to the entry generates another id: the state is
15399
15399
  // there, and nothing reads it. Reported when a component mounting on the entry
@@ -15419,7 +15419,7 @@ const warnAboutOrphanGeneratedKeys = () => {
15419
15419
  }
15420
15420
  for (const key of Object.keys(state)) {
15421
15421
  if (
15422
- !isLikelyPreactGeneratedId(key) ||
15422
+ !isGeneratedId(key) ||
15423
15423
  idUsageMap.has(key) ||
15424
15424
  orphanKeysReported.has(key)
15425
15425
  ) {
@@ -15427,7 +15427,7 @@ const warnAboutOrphanGeneratedKeys = () => {
15427
15427
  }
15428
15428
  orphanKeysReported.add(key);
15429
15429
  console.warn(
15430
- `useNavState: this history entry holds "${key}", written by a component whose id was generated (preact's useId()) and that is not mounted anymore — a Picker without an id, a popup with navState and no id. A generated id names one mount, so nothing will read that state again: a popup open when this screen was left comes back closed. Give the component a stable id if it was meant to be found as it was.`,
15430
+ `useNavState: this history entry holds "${key}", written by a component whose id was generated and that is not mounted anymore — a Picker without an id, a popup with navState and no id. A generated id names one mount, so nothing will read that state again: a popup open when this screen was left comes back closed. Give the component a stable id if it was meant to be found as it was.`,
15431
15431
  );
15432
15432
  }
15433
15433
  };
@@ -15515,13 +15515,13 @@ const useNavStateBasic = (
15515
15515
 
15516
15516
  let effectiveType = type;
15517
15517
  // A push bakes the key into a history entry of its own, and an id naming a
15518
- // render position (see isLikelyPreactGeneratedId) can be claimed by two
15518
+ // render position (see isGeneratedId) can be claimed by two
15519
15519
  // mounts of this same page. A replace keeps the key on the entry the document
15520
15520
  // is already on, where the id at least names one render.
15521
- if (type === "push" && isLikelyPreactGeneratedId(id)) {
15521
+ if (type === "push" && isGeneratedId(id)) {
15522
15522
  {
15523
15523
  console.warn(
15524
- `useNavState(${id}): this id looks auto-generated (e.g. preact's useId()) and type is "push" — falling back to "replace". A push history entry keyed by an unstable id won't survive a reload correctly, and could even collide with a different component's own auto-generated id. Pass a stable, explicit id to use type: "push".`,
15524
+ `useNavState(${id}): this id looks auto-generated and type is "push" — falling back to "replace". A push history entry keyed by an unstable id won't survive a reload correctly, and could even collide with a different component's own auto-generated id. Pass a stable, explicit id to use type: "push".`,
15525
15525
  );
15526
15526
  }
15527
15527
  effectiveType = "replace";
@@ -15639,7 +15639,7 @@ const useNavStateBasic = (
15639
15639
  * - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
15640
15640
  * - "replace": updates the current history entry — no extra history entry is created.
15641
15641
  * Silently downgraded to "replace" (with a dev-only console.warn) when `id`
15642
- * looks auto-generated (e.g. preact's own useId()) — an unstable id baked
15642
+ * looks auto-generated (navi's own, or preact's useId()) — an unstable id baked
15643
15643
  * into a pushed history entry won't survive a reload correctly, and could
15644
15644
  * even collide with a different component's own auto-generated id. Pass a
15645
15645
  * stable, explicit id to actually get "push" behavior.
@@ -30792,7 +30792,8 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30792
30792
  * - `DELETE: async ({ id }) => [id, null]`
30793
30793
  *
30794
30794
  * `ownerId` may also be `{ [uniqueKey]: value }` when the owner is known by an alternate key.
30795
- * The property is `null` until a callback provides data; setting it to `null` clears it.
30795
+ * The property is `undefined` until an answer speaks for the owner (its row, or a
30796
+ * callback), `null` when that answer holds none; setting it to `null` clears it.
30796
30797
  * Mutations apply directly to the owner's signal, so the parent GET is never rerun; an
30797
30798
  * owner the store does not hold yet is left alone, its child comes with its next read.
30798
30799
  *
@@ -30851,7 +30852,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30851
30852
  for (const childSetup of childSetupCallbackSet) {
30852
30853
  childSetup(childItem);
30853
30854
  }
30854
- const childSignal = signal(null);
30855
+ // `undefined` until something answers for this owner, `null` once an
30856
+ // answer says it holds none: a screen tells « not read yet » from
30857
+ // « nothing » as it does with an action's data.
30858
+ const childSignal = signal(undefined);
30855
30859
  // The child keeps its identity while its props change, so the owner's
30856
30860
  // getter also reads this version, bumped by each change in place:
30857
30861
  // whichever way the props came in — the owner's row, a write of this
@@ -30860,7 +30864,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30860
30864
  // Returns whether the child changed — what the store asks a relation
30861
30865
  // (see assign in array_signal_store.js).
30862
30866
  const applyProps = (props) => {
30863
- if (!props) {
30867
+ if (props === undefined) {
30868
+ return false;
30869
+ }
30870
+ if (props === null) {
30864
30871
  if (childSignal.peek() === null) {
30865
30872
  return false;
30866
30873
  }
@@ -30882,7 +30889,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
30882
30889
  }
30883
30890
  }
30884
30891
  if (childSignal.peek() !== childItem) {
30885
- childSignal.value = childItem; // first activation: null → childItem
30892
+ childSignal.value = childItem; // first activation
30886
30893
  return true;
30887
30894
  }
30888
30895
  if (changed) {
@@ -34777,7 +34784,12 @@ const Box = props => {
34777
34784
  mount,
34778
34785
  ...computeProps
34779
34786
  } = props;
34780
- const parentBoxFlow = useContext(BoxFlowContext);
34787
+ // The flow of the nearest Box above in the component tree, which lays this
34788
+ // box out unless a plain element sits between the two in the DOM: the box
34789
+ // then reads that element's flow once mounted (see readDomParentFlow).
34790
+ const contextBoxFlow = useContext(BoxFlowContext);
34791
+ const [domParentFlow, setDomParentFlow] = useState(null);
34792
+ const parentBoxFlow = domParentFlow && domParentFlow.contextBoxFlow === contextBoxFlow ? domParentFlow.flow : contextBoxFlow;
34781
34793
  // Which interactions this box answers, and with what. Read here rather than
34782
34794
  // on the control, so a swipe or a hold can be declared on anything — a row, a
34783
34795
  // card, a block of text — and reach the control it belongs to (which is what
@@ -34824,7 +34836,17 @@ const Box = props => {
34824
34836
  childForwardedProps,
34825
34837
  styleDeps
34826
34838
  } = computed;
34827
- const syncBox = useCallback(computed.syncBox, styleDeps);
34839
+ const verifyParentFlow = parentBoxFlow === contextBoxFlow && FLOW_STYLING_ITS_CHILDREN_SET.has(contextBoxFlow) && readsParentFlow(computeProps);
34840
+ const syncBox = useCallback(verifyParentFlow ? boxEl => {
34841
+ const flow = readDomParentFlow(boxEl);
34842
+ if (flow !== contextBoxFlow) {
34843
+ setDomParentFlow({
34844
+ contextBoxFlow,
34845
+ flow
34846
+ });
34847
+ }
34848
+ return computed.syncBox(boxEl);
34849
+ } : computed.syncBox, styleDeps);
34828
34850
  const finalRef = useComposeElementRef(syncBox, ref);
34829
34851
  useInteractionsEffect(finalRef, interactionsRef);
34830
34852
  let innerChildren = children;
@@ -35137,6 +35159,13 @@ const computeBox = (props, parentBoxFlow) => {
35137
35159
  const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext);
35138
35160
  const cssVar = readCSSVarName(styleContext.styleCSSVars[name]);
35139
35161
  if (cssVar) {
35162
+ if (name === "color" && value === "inherit") {
35163
+ // A token set to `inherit` takes the token of the parent element
35164
+ // (a list's white for its selected items), never the parent's color.
35165
+ // `color: currentColor` IS `color: inherit`, and a token carries it.
35166
+ addCSSVar("currentColor", cssVar, stylesTarget);
35167
+ return true;
35168
+ }
35140
35169
  addCSSVar(mergedValue, cssVar, stylesTarget);
35141
35170
  if (name === "borderRadius" && value === "inherit") {
35142
35171
  // "inherit" cannot be expressed via a CSS variable — a var() reference
@@ -35453,6 +35482,47 @@ const declaresSomething = value => {
35453
35482
  }
35454
35483
  return true;
35455
35484
  };
35485
+ // The flows the styles of a child branch on (see expandX, selfAlignX… in
35486
+ // box_style_util.js): under any other the child writes the same styles
35487
+ // whatever its parent is.
35488
+ const FLOW_STYLING_ITS_CHILDREN_SET = new Set(["flex-x", "flex-y", "inline-flex-x", "inline-flex-y", "grid", "inline-grid"]);
35489
+ const PARENT_FLOW_PROP_NAMES = ["expand", "expandX", "expandY", "selfAlignX", "selfAlignY"];
35490
+ const readsParentFlow = props => {
35491
+ for (const name of PARENT_FLOW_PROP_NAMES) {
35492
+ if (props[name]) {
35493
+ return true;
35494
+ }
35495
+ }
35496
+ return false;
35497
+ };
35498
+ // A Box parent says its flow on its element; a plain element is read from its
35499
+ // computed style, which only a box mounted under one pays for.
35500
+ const readDomParentFlow = boxEl => {
35501
+ let parent = boxEl.parentElement;
35502
+ while (parent) {
35503
+ const flowAttribute = parent.getAttribute("navi-box-flow");
35504
+ if (flowAttribute) {
35505
+ return flowAttribute;
35506
+ }
35507
+ const {
35508
+ display,
35509
+ flexDirection
35510
+ } = window.getComputedStyle(parent);
35511
+ if (display === "contents") {
35512
+ parent = parent.parentElement;
35513
+ continue;
35514
+ }
35515
+ if (display === "flex" || display === "inline-flex") {
35516
+ const axis = flexDirection.startsWith("column") ? "y" : "x";
35517
+ return `${display}-${axis}`;
35518
+ }
35519
+ if (display === "grid" || display === "inline-grid") {
35520
+ return display;
35521
+ }
35522
+ return "block";
35523
+ }
35524
+ return undefined;
35525
+ };
35456
35526
  const isScrollingOverflow = value => value === "auto" || value === "scroll";
35457
35527
  const mayHoldComponent = children => {
35458
35528
  if (children === null || children === undefined) {
@@ -46703,17 +46773,22 @@ document.body.addEventListener(
46703
46773
  * on screen. A registry keyed by id then holds the newcomer, and loses the
46704
46774
  * first control when the newcomer leaves. navi renders on the client only, so
46705
46775
  * a counter is all the uniqueness needs.
46776
+ *
46777
+ * Held in a ref, not in useState: @preact/signals' shouldComponentUpdate
46778
+ * skips the render of a component holding hook state whose props did not
46779
+ * change, so an id kept as state would change when its caller renders.
46706
46780
  */
46707
46781
 
46708
46782
 
46709
46783
  let generatedIdCount = 0;
46710
46784
 
46711
46785
  const useGeneratedId = () => {
46712
- const [id] = useState(() => {
46786
+ const idRef = useRef(null);
46787
+ if (idRef.current === null) {
46713
46788
  generatedIdCount++;
46714
- return `navi_${generatedIdCount}`;
46715
- });
46716
- return id;
46789
+ idRef.current = `navi_${generatedIdCount}`;
46790
+ }
46791
+ return idRef.current;
46717
46792
  };
46718
46793
 
46719
46794
  /**
@@ -61334,7 +61409,7 @@ const Expandable = props => {
61334
61409
  const rootRef = ref || defaultRef;
61335
61410
  const uiRef = useRef();
61336
61411
  const contentContainerRef = useRef();
61337
- const contentId = useId();
61412
+ const contentId = useGeneratedId();
61338
61413
  const isColumn = layout === "column";
61339
61414
  const closedContentSized = isColumn && mount === "always";
61340
61415
  // Only the two directions the layout has room for: a column layout reveals
@@ -62974,7 +63049,7 @@ const ButtonConfirm = ({
62974
63049
  // it is elsewhere by then, and pulling it back would take it from whatever
62975
63050
  // the person moved to.
62976
63051
  const restoreFocusRef = useRef(false);
62977
- const questionId = `${useId()}_question`;
63052
+ const questionId = `${useGeneratedId()}_question`;
62978
63053
  const cancel = restoreFocus => {
62979
63054
  restoreFocusRef.current = restoreFocus;
62980
63055
  setAsking(false);
@@ -63776,7 +63851,7 @@ const ControlSwap = props => {
63776
63851
  const activeSide = sides[activeIndex];
63777
63852
  const capRefs = [useRef(), useRef()];
63778
63853
  const slotRefs = [useRef(), useRef()];
63779
- const slotIdPrefix = useId();
63854
+ const slotIdPrefix = useGeneratedId();
63780
63855
  const activeNameRef = useRef(floor.name);
63781
63856
  activeNameRef.current = activeSide.name;
63782
63857
  const swapTo = (name, event, pressedCapIndex = null) => {
@@ -64637,7 +64712,7 @@ const InputCheckboxFieldInterface = props => {
64637
64712
  // setting's name — which are its label and its accessible name: a mode one
64638
64713
  // toggles is a checkbox, not a button writing a signal on each press.
64639
64714
  const hasLabel = variant === "button" && children !== undefined && children !== null && children !== false;
64640
- const labelId = useId();
64715
+ const labelId = useGeneratedId();
64641
64716
  const boxRef = useRef();
64642
64717
  useAccentColorAttributes(boxRef, accentColor, {
64643
64718
  elementSelector: ".navi_checkbox_accent_probe"
@@ -75878,7 +75953,7 @@ const PopupTitleContext = createContext(null);
75878
75953
  const PopupTitle = props => {
75879
75954
  import.meta.css = [css$E, "@jsenv/navi/src/layout/popup_title.jsx"];
75880
75955
  const registerTitle = useContext(PopupTitleContext);
75881
- const idDefault = useId();
75956
+ const idDefault = useGeneratedId();
75882
75957
  const id = props.id || `popup_title_${idDefault}`;
75883
75958
  useLayoutEffect(() => {
75884
75959
  if (!registerTitle) {
@@ -79274,7 +79349,7 @@ const usePopoverProps = props => {
79274
79349
  // open, two separate invocations of openEffect that don't otherwise share
79275
79350
  // any scope, hence the ref.
79276
79351
  const disarmBackdropHideRef = useRef(null);
79277
- const defaultId = useId();
79352
+ const defaultId = useGeneratedId();
79278
79353
  const id = rest.id || defaultId;
79279
79354
  const backdropId = `${id}-backdrop`;
79280
79355
  const debugPopup = useDebugPopup();
@@ -80877,10 +80952,10 @@ const PickerCustom = props => {
80877
80952
  // before computing popupId below, so two Pickers without an explicit id never collide.
80878
80953
  // Captured before the fallback chain below overwrites props.id — needed to
80879
80954
  // know whether the id actually came from the caller (stable) or from
80880
- // useId()/ControlIdContext (a generated id names one mount: a reload, or a
80955
+ // useGeneratedId()/ControlIdContext (a generated id names one mount: a reload, or a
80881
80956
  // return to this page, generates another), see pickerNavType below.
80882
80957
  const hasExplicitId = Boolean(props.id);
80883
- const idDefault = useId();
80958
+ const idDefault = useGeneratedId();
80884
80959
  const controlId = useContext(ControlIdContext);
80885
80960
  props.id = props.id || controlId || idDefault;
80886
80961
  // Given no value, the picker holds what the control in its popup holds (see
@@ -80982,7 +81057,7 @@ const PickerCustom = props => {
80982
81057
  const debugPopup = useDebugPopup();
80983
81058
  // In "dialog" mode with a stable, caller-provided id, enterExpanded()
80984
81059
  // pushes a history entry so the back button closes it. Every other case
80985
- // (popover mode, or a dialog whose id was auto-generated via useId()/
81060
+ // (popover mode, or a dialog whose id was auto-generated via useGeneratedId()/
80986
81061
  // ControlIdContext) replaces the current history state instead — a
80987
81062
  // generated id names one mount, so pushing it would either leave an entry
80988
81063
  // nothing reads or, worse, collide with a different component's own
@@ -83875,7 +83950,7 @@ const ListSelectable = props => {
83875
83950
  const Next = useNextResolver();
83876
83951
  import.meta.css = [css$x, "@jsenv/navi/src/control/list/list_selectable.jsx"];
83877
83952
  // we allow ourselves to auto-generate a name
83878
- const defaultName = useId();
83953
+ const defaultName = useGeneratedId();
83879
83954
  props.name = props.name || `listbox_${defaultName}`;
83880
83955
  const {
83881
83956
  ref,
@@ -84224,7 +84299,7 @@ const ListItemSelectableResolver = props => {
84224
84299
  };
84225
84300
  const ListItemSelectable = props => {
84226
84301
  const Next = useNextResolver();
84227
- const defaultId = useId();
84302
+ const defaultId = useGeneratedId();
84228
84303
  // Whether the caller SAYS anything about this row's selection. Passing
84229
84304
  // `checked: undefined` is not the same as not passing it: a control with the
84230
84305
  // key present calls itself controlled, and a controlled row is one the list
@@ -85214,7 +85289,7 @@ const ListUI = props => {
85214
85289
  } = props;
85215
85290
  // Remembered by name, and a name made up at render (see ListFirstResolver)
85216
85291
  // names no list a later mount would recognize.
85217
- const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
85292
+ const rememberScroll = !scrollResetOnNavigation && !isGeneratedId(rest.id);
85218
85293
  // Where the list was when its screen was left, when this is the way back
85219
85294
  // (see scroll_restoration.js). Read once: `defaultScrolled` is held by
85220
85295
  // reference, and a place read again each render would be a list moved each
@@ -85524,7 +85599,7 @@ const ListFirstResolver = props => {
85524
85599
  const Next = useNextResolver();
85525
85600
  const refDefault = useRef(null);
85526
85601
  props.ref = props.ref || refDefault;
85527
- const idDefault = useId();
85602
+ const idDefault = useGeneratedId();
85528
85603
  props.id = props.id || idDefault;
85529
85604
  const listItemsRef = useRef(null);
85530
85605
  if (!listItemsRef.current) {
@@ -89117,7 +89192,7 @@ const ListItemUI = props => {
89117
89192
  if (identityMatters && props.id === undefined) {
89118
89193
  console.warn("ListItem is missing an explicit id prop. Provide a stable id so pointed/selected state survives search reordering.");
89119
89194
  }
89120
- const idDefault = useId();
89195
+ const idDefault = useGeneratedId();
89121
89196
  props.id = props.id || idDefault;
89122
89197
  const listItems = useContext(ListItemsContext);
89123
89198
  const groupId = useContext(ListGroupContext);
@@ -89933,7 +90008,7 @@ const useRunItems = (store, {
89933
90008
  renderSkeleton,
89934
90009
  renderError
89935
90010
  }) => {
89936
- const ownerId = useId();
90011
+ const ownerId = useGeneratedId();
89937
90012
  // Dev only: the group `groupOfMissing` put an item not held in, by rank,
89938
90013
  // checked against `groupBy` once the item lands.
89939
90014
  const missingGroupCheckRef = useRef(null);
@@ -90502,7 +90577,7 @@ const ListRunSkeletonItem = ({
90502
90577
  }) => {
90503
90578
  const listItems = useContext(ListItemsContext);
90504
90579
  const groupId = useContext(ListGroupContext);
90505
- const entryId = useId();
90580
+ const entryId = useGeneratedId();
90506
90581
  listItems.draw(entryId, {
90507
90582
  ownerId: runItem.ownerId,
90508
90583
  place: runItem.index,
@@ -91243,7 +91318,7 @@ const ListItemGroup = ({
91243
91318
  children,
91244
91319
  ...rest
91245
91320
  }) => {
91246
- const groupId = useId();
91321
+ const groupId = useGeneratedId();
91247
91322
  const listItems = useContext(ListItemsContext);
91248
91323
  const group = listItems.group(groupId);
91249
91324
  useLayoutEffect(() => {
@@ -95587,7 +95662,7 @@ const Spin = ({
95587
95662
  } else {
95588
95663
  compareValues ??= compareValuesDefault;
95589
95664
  }
95590
- const id = useId();
95665
+ const id = useGeneratedId();
95591
95666
  // What the group around it says, when there is one: how big the whole thing
95592
95667
  // is written is said once, on the group, and every spin in it follows.
95593
95668
  const group = useContext(SpinGroupContext);
@@ -96805,7 +96880,7 @@ const TimeRangeSpin = ({
96805
96880
  endTimeProps,
96806
96881
  ...rest
96807
96882
  }) => {
96808
- const startId = useId();
96883
+ const startId = useGeneratedId();
96809
96884
  return jsxs(ControlGroup, {
96810
96885
  flex: true,
96811
96886
  alignY: "center",
@@ -96898,7 +96973,7 @@ const css$q = /* css */`.navi_checkbox_group {
96898
96973
  const CheckboxGroup = props => {
96899
96974
  const refDefault = useRef(null);
96900
96975
  props.ref = props.ref || refDefault;
96901
- const defaultName = useId();
96976
+ const defaultName = useGeneratedId();
96902
96977
  props.name = props.name || `checkbox_group_${defaultName}`;
96903
96978
  const checkboxGroup = jsx(CheckboxGroupInterface, {
96904
96979
  ...props
@@ -97663,7 +97738,7 @@ const css$o = /* css */`.navi_radio_group {
97663
97738
  const RadioGroup = props => {
97664
97739
  const refDefault = useRef(null);
97665
97740
  props.ref = props.ref || refDefault;
97666
- const defaultName = useId();
97741
+ const defaultName = useGeneratedId();
97667
97742
  props.name = props.name || `radio_group_${defaultName}`;
97668
97743
  const radioGroup = jsx(RadioGroupInterface, {
97669
97744
  ...props
@@ -98004,7 +98079,7 @@ const SplitButton = props => {
98004
98079
  if (popupProps.positionArea === undefined && mode === "popover") {
98005
98080
  popupProps.positionArea = "bottom-end";
98006
98081
  }
98007
- const idDefault = useId();
98082
+ const idDefault = useGeneratedId();
98008
98083
  const idResolved = id || idDefault;
98009
98084
  const menuId = `${idResolved}_menu`;
98010
98085
  const rootRef = useRef(null);
@@ -98759,12 +98834,18 @@ const buildMatchInfo = (searchText, items, matchFn) => {
98759
98834
  * needed. `data.role === "presentation"` keeps it out entirely.
98760
98835
  * Returns the visible rank (position among visible items), or -1 when hidden
98761
98836
  * or filtered.
98837
+ * An item rendered without an index keeps the position it has, and a new one
98838
+ * goes last: for a host that cannot know the order at render and gives it
98839
+ * afterwards with setOrder.
98762
98840
  * Signals and onChange are deferred to a microtask so multiple items updating
98763
98841
  * in one commit cause only one notification.
98764
98842
  *
98765
98843
  * getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
98766
98844
  * its visible rank. Returns undefined when index is out of range.
98767
98845
  *
98846
+ * setOrder(ids): gives each tracked item its position from its rank in
98847
+ * `ids`.
98848
+ *
98768
98849
  * peekItems(): the items as they stand right now, without waiting for the
98769
98850
  * deferred notification — what a sibling rendering after the items must read
98770
98851
  * to paint them in the same commit.
@@ -99045,8 +99126,14 @@ const createItemTracker = (onChange) => {
99045
99126
  // that determines this item's position among siblings.
99046
99127
  // Returns the item's visible rank among non-hidden items, or -1 when hidden.
99047
99128
  const useTrackItem = (data) => {
99048
- const { id, index } = data;
99129
+ const { id } = data;
99049
99130
  const key = keyForId(id);
99131
+ let { index } = data;
99132
+ if (index === undefined) {
99133
+ index = keyToExplicitOrder.has(key)
99134
+ ? keyToExplicitOrder.get(key)
99135
+ : Infinity;
99136
+ }
99050
99137
 
99051
99138
  syncItem(key, index, data);
99052
99139
  notify();
@@ -99064,6 +99151,24 @@ const createItemTracker = (onChange) => {
99064
99151
  return keyToOrderedIndex.get(key) ?? -1;
99065
99152
  };
99066
99153
 
99154
+ const setOrder = (ids) => {
99155
+ let changed = false;
99156
+ for (let i = 0; i < ids.length; i++) {
99157
+ const key = idToKey.get(ids[i]);
99158
+ if (key === undefined || !allRegistrations.has(key)) {
99159
+ continue;
99160
+ }
99161
+ if (keyToExplicitOrder.get(key) === i) {
99162
+ continue;
99163
+ }
99164
+ syncItem(key, i, allRegistrations.get(key));
99165
+ changed = true;
99166
+ }
99167
+ if (changed) {
99168
+ notify();
99169
+ }
99170
+ };
99171
+
99067
99172
  const getTrackedItemByIndex = (index) => {
99068
99173
  const key = orderedKeys[index];
99069
99174
  if (key === undefined) {
@@ -99097,6 +99202,7 @@ const createItemTracker = (onChange) => {
99097
99202
  return {
99098
99203
  useTrackItem,
99099
99204
  getTrackedItemByIndex,
99205
+ setOrder,
99100
99206
  peekItems,
99101
99207
  itemsSignal,
99102
99208
  visibleItemsSignal,
@@ -99494,8 +99600,8 @@ const getNumericRange = items => {
99494
99600
 
99495
99601
  // Wheel.Item registers its {value, label} here so WheelUI knows the full ordered
99496
99602
  // list of items regardless of how children are wrapped (providers, fragments…).
99497
- // indexRef gives each item its position: WheelUI resets it to 0 every render and
99498
- // each item reads-and-increments it as it renders, in document order.
99603
+ // The position comes after the commit, from the marker each item leaves where it
99604
+ // stands (see WheelUI).
99499
99605
  const WheelItemTrackerContext = createContext(null);
99500
99606
 
99501
99607
  // Lets a WheelGroup push shared presentation (glass, orientation) down to every
@@ -100118,15 +100224,21 @@ function WheelUI(props) {
100118
100224
  // (children may be wrapped). This ordered list is the source of truth for the
100119
100225
  // whole wheel — the rendered rows are a small recycled window over it.
100120
100226
  const tracker = useItemTracker();
100121
- const indexRef = useRef(0);
100122
- indexRef.current = 0;
100123
- const trackerContextRef = useRef(null);
100124
- if (!trackerContextRef.current) {
100125
- trackerContextRef.current = {
100126
- tracker,
100127
- indexRef
100128
- };
100129
- }
100227
+ // The order is read from the markers the items leave in the DOM, not from the
100228
+ // order they render in: an item whose props did not change may skip a render
100229
+ // (signals' shouldComponentUpdate), and a render count would then rank the
100230
+ // ones that did render first.
100231
+ useLayoutEffect(() => {
100232
+ const el = ref.current;
100233
+ if (!el) {
100234
+ return;
100235
+ }
100236
+ const ids = [];
100237
+ for (const marker of el.querySelectorAll("[data-navi-wheel-item]")) {
100238
+ ids.push(marker.getAttribute("data-navi-wheel-item"));
100239
+ }
100240
+ tracker.setOrder(ids);
100241
+ });
100130
100242
  const trackedItems = tracker.itemsSignal.value;
100131
100243
  const trackedItemsRef = useRef(trackedItems);
100132
100244
  trackedItemsRef.current = trackedItems;
@@ -100933,7 +101045,7 @@ function WheelUI(props) {
100933
101045
  className: "navi_wheel_pane",
100934
101046
  "data-side": "start"
100935
101047
  }), jsx(WheelItemTrackerContext.Provider, {
100936
- value: trackerContextRef.current,
101048
+ value: tracker,
100937
101049
  children: jsx(ControlFacadeChildrenWrapper, {
100938
101050
  facadeController: facadeController,
100939
101051
  children: children
@@ -101046,9 +101158,10 @@ const WheelWindow = ({
101046
101158
  * Wheel.Item — a value in a Wheel. Must be used inside <Wheel>.
101047
101159
  *
101048
101160
  * Registration only: it records its {value, label, itemProps} with the wheel and
101049
- * renders NOTHING. The wheel is virtualized — it renders a small recycled window
101050
- * of rows and fills each from this tracked data — so the value list is the source
101051
- * of truth, not a per-item DOM node. Selection lives on the wheel's value.
101161
+ * renders no row, only a hidden marker the wheel reads its position from. The
101162
+ * wheel is virtualized — it renders a small recycled window of rows and fills
101163
+ * each from this tracked data — so the value list is the source of truth, not a
101164
+ * per-item DOM node. Selection lives on the wheel's value.
101052
101165
  *
101053
101166
  * @type {import("preact").FunctionComponent<{
101054
101167
  * value: any,
@@ -101062,19 +101175,20 @@ const WheelItem = ({
101062
101175
  children,
101063
101176
  ...rest
101064
101177
  }) => {
101065
- const idDefault = useId();
101178
+ const idDefault = useGeneratedId();
101066
101179
  const resolvedId = id || idDefault;
101067
101180
  // Wheel.Item must live inside a Wheel, so the context is always present.
101068
- const trackerContext = useContext(WheelItemTrackerContext);
101069
- const index = trackerContext.indexRef.current++;
101070
- trackerContext.tracker.useTrackItem({
101181
+ const tracker = useContext(WheelItemTrackerContext);
101182
+ tracker.useTrackItem({
101071
101183
  id: resolvedId,
101072
- index,
101073
101184
  value,
101074
101185
  label: children,
101075
101186
  itemProps: rest
101076
101187
  });
101077
- return null;
101188
+ return jsx("span", {
101189
+ hidden: true,
101190
+ "data-navi-wheel-item": resolvedId
101191
+ });
101078
101192
  };
101079
101193
  Wheel.Item = WheelItem;
101080
101194
 
@@ -101660,7 +101774,7 @@ const TimeRangeWheel = ({
101660
101774
  ...rest
101661
101775
  }) => {
101662
101776
  import.meta.css = [css$j, "@jsenv/navi/src/control/wheel/wheel_time.jsx"];
101663
- const startId = useId();
101777
+ const startId = useGeneratedId();
101664
101778
  const startRef = useRef(null);
101665
101779
  const endRef = useRef(null);
101666
101780
  const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
@@ -104648,7 +104762,7 @@ const useIsInTableHead = () => useContext(TableSectionContext) === "head";
104648
104762
  const Table = props => {
104649
104763
  installTableCss();
104650
104764
  const tableDefaultRef = useRef();
104651
- const tableDefaultId = `table-${useId()}`;
104765
+ const tableDefaultId = `table-${useGeneratedId()}`;
104652
104766
  const {
104653
104767
  id = tableDefaultId,
104654
104768
  selection = [],