@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, 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, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } 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, useReducer, useEffect, useCallback, useId } from "preact/hooks";
9
+ import { useState, useLayoutEffect, useErrorBoundary, useContext, useMemo, useRef, useReducer, useEffect, 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, 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";
@@ -13457,13 +13457,13 @@ const writePressWritesHere = (pressWrites) => {
13457
13457
  }
13458
13458
  };
13459
13459
 
13460
- // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
13461
- // mask is derived from render order within the nearest root/async boundary:
13462
- // stable across re-renders of the same mount, but not across two mounts on the
13463
- // same page, and not across two documents.
13464
- const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
13465
- const isLikelyPreactGeneratedId = (id) => {
13466
- return PREACT_GENERATED_ID_REGEX.test(id);
13460
+ // An id made up at render names one mount: navi's own ("navi_<n>", see
13461
+ // useGeneratedId) counts mounts in the document, and preact's useId(), which
13462
+ // an app may hand a component, gives "P<mask0>-<mask1>" from the render order.
13463
+ // Neither names the same component in two mounts or two documents.
13464
+ const GENERATED_ID_REGEX = /^(?:navi_\d+|P\d+-\d+)/;
13465
+ const isGeneratedId = (id) => {
13466
+ return GENERATED_ID_REGEX.test(id);
13467
13467
  };
13468
13468
 
13469
13469
  /**
@@ -13487,7 +13487,7 @@ const dropGeneratedIdKeys = (state) => {
13487
13487
  }
13488
13488
  let stateWithoutGeneratedIdKeys = state;
13489
13489
  for (const key of Object.keys(state)) {
13490
- if (!isLikelyPreactGeneratedId(key)) {
13490
+ if (!isGeneratedId(key)) {
13491
13491
  continue;
13492
13492
  }
13493
13493
  if (stateWithoutGeneratedIdKeys === state) {
@@ -14776,10 +14776,10 @@ const useNavStateBasic = (
14776
14776
 
14777
14777
  let effectiveType = type;
14778
14778
  // A push bakes the key into a history entry of its own, and an id naming a
14779
- // render position (see isLikelyPreactGeneratedId) can be claimed by two
14779
+ // render position (see isGeneratedId) can be claimed by two
14780
14780
  // mounts of this same page. A replace keeps the key on the entry the document
14781
14781
  // is already on, where the id at least names one render.
14782
- if (type === "push" && isLikelyPreactGeneratedId(id)) {
14782
+ if (type === "push" && isGeneratedId(id)) {
14783
14783
  effectiveType = "replace";
14784
14784
  }
14785
14785
 
@@ -14895,7 +14895,7 @@ const useNavStateBasic = (
14895
14895
  * - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
14896
14896
  * - "replace": updates the current history entry — no extra history entry is created.
14897
14897
  * Silently downgraded to "replace" (with a dev-only console.warn) when `id`
14898
- * looks auto-generated (e.g. preact's own useId()) — an unstable id baked
14898
+ * looks auto-generated (navi's own, or preact's useId()) — an unstable id baked
14899
14899
  * into a pushed history entry won't survive a reload correctly, and could
14900
14900
  * even collide with a different component's own auto-generated id. Pass a
14901
14901
  * stable, explicit id to actually get "push" behavior.
@@ -29677,7 +29677,8 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
29677
29677
  * - `DELETE: async ({ id }) => [id, null]`
29678
29678
  *
29679
29679
  * `ownerId` may also be `{ [uniqueKey]: value }` when the owner is known by an alternate key.
29680
- * The property is `null` until a callback provides data; setting it to `null` clears it.
29680
+ * The property is `undefined` until an answer speaks for the owner (its row, or a
29681
+ * callback), `null` when that answer holds none; setting it to `null` clears it.
29681
29682
  * Mutations apply directly to the owner's signal, so the parent GET is never rerun; an
29682
29683
  * owner the store does not hold yet is left alone, its child comes with its next read.
29683
29684
  *
@@ -29733,7 +29734,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
29733
29734
  for (const childSetup of childSetupCallbackSet) {
29734
29735
  childSetup(childItem);
29735
29736
  }
29736
- const childSignal = signal(null);
29737
+ // `undefined` until something answers for this owner, `null` once an
29738
+ // answer says it holds none: a screen tells « not read yet » from
29739
+ // « nothing » as it does with an action's data.
29740
+ const childSignal = signal(undefined);
29737
29741
  // The child keeps its identity while its props change, so the owner's
29738
29742
  // getter also reads this version, bumped by each change in place:
29739
29743
  // whichever way the props came in — the owner's row, a write of this
@@ -29742,7 +29746,10 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
29742
29746
  // Returns whether the child changed — what the store asks a relation
29743
29747
  // (see assign in array_signal_store.js).
29744
29748
  const applyProps = (props) => {
29745
- if (!props) {
29749
+ if (props === undefined) {
29750
+ return false;
29751
+ }
29752
+ if (props === null) {
29746
29753
  if (childSignal.peek() === null) {
29747
29754
  return false;
29748
29755
  }
@@ -29764,7 +29771,7 @@ ${name}.many("${propertyName}") source location: ${declarationSite}`,
29764
29771
  }
29765
29772
  }
29766
29773
  if (childSignal.peek() !== childItem) {
29767
- childSignal.value = childItem; // first activation: null → childItem
29774
+ childSignal.value = childItem; // first activation
29768
29775
  return true;
29769
29776
  }
29770
29777
  if (changed) {
@@ -33639,7 +33646,12 @@ const Box = props => {
33639
33646
  mount,
33640
33647
  ...computeProps
33641
33648
  } = props;
33642
- const parentBoxFlow = useContext(BoxFlowContext);
33649
+ // The flow of the nearest Box above in the component tree, which lays this
33650
+ // box out unless a plain element sits between the two in the DOM: the box
33651
+ // then reads that element's flow once mounted (see readDomParentFlow).
33652
+ const contextBoxFlow = useContext(BoxFlowContext);
33653
+ const [domParentFlow, setDomParentFlow] = useState(null);
33654
+ const parentBoxFlow = domParentFlow && domParentFlow.contextBoxFlow === contextBoxFlow ? domParentFlow.flow : contextBoxFlow;
33643
33655
  // Which interactions this box answers, and with what. Read here rather than
33644
33656
  // on the control, so a swipe or a hold can be declared on anything — a row, a
33645
33657
  // card, a block of text — and reach the control it belongs to (which is what
@@ -33686,7 +33698,17 @@ const Box = props => {
33686
33698
  childForwardedProps,
33687
33699
  styleDeps
33688
33700
  } = computed;
33689
- const syncBox = useCallback(computed.syncBox, styleDeps);
33701
+ const verifyParentFlow = parentBoxFlow === contextBoxFlow && FLOW_STYLING_ITS_CHILDREN_SET.has(contextBoxFlow) && readsParentFlow(computeProps);
33702
+ const syncBox = useCallback(verifyParentFlow ? boxEl => {
33703
+ const flow = readDomParentFlow(boxEl);
33704
+ if (flow !== contextBoxFlow) {
33705
+ setDomParentFlow({
33706
+ contextBoxFlow,
33707
+ flow
33708
+ });
33709
+ }
33710
+ return computed.syncBox(boxEl);
33711
+ } : computed.syncBox, styleDeps);
33690
33712
  const finalRef = useComposeElementRef(syncBox, ref);
33691
33713
  useInteractionsEffect(finalRef, interactionsRef);
33692
33714
  let innerChildren = children;
@@ -33991,6 +34013,13 @@ const computeBox = (props, parentBoxFlow) => {
33991
34013
  const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext);
33992
34014
  const cssVar = readCSSVarName(styleContext.styleCSSVars[name]);
33993
34015
  if (cssVar) {
34016
+ if (name === "color" && value === "inherit") {
34017
+ // A token set to `inherit` takes the token of the parent element
34018
+ // (a list's white for its selected items), never the parent's color.
34019
+ // `color: currentColor` IS `color: inherit`, and a token carries it.
34020
+ addCSSVar("currentColor", cssVar, stylesTarget);
34021
+ return true;
34022
+ }
33994
34023
  addCSSVar(mergedValue, cssVar, stylesTarget);
33995
34024
  if (name === "borderRadius" && value === "inherit") {
33996
34025
  // "inherit" cannot be expressed via a CSS variable — a var() reference
@@ -34302,6 +34331,47 @@ const declaresSomething = value => {
34302
34331
  }
34303
34332
  return true;
34304
34333
  };
34334
+ // The flows the styles of a child branch on (see expandX, selfAlignX… in
34335
+ // box_style_util.js): under any other the child writes the same styles
34336
+ // whatever its parent is.
34337
+ const FLOW_STYLING_ITS_CHILDREN_SET = new Set(["flex-x", "flex-y", "inline-flex-x", "inline-flex-y", "grid", "inline-grid"]);
34338
+ const PARENT_FLOW_PROP_NAMES = ["expand", "expandX", "expandY", "selfAlignX", "selfAlignY"];
34339
+ const readsParentFlow = props => {
34340
+ for (const name of PARENT_FLOW_PROP_NAMES) {
34341
+ if (props[name]) {
34342
+ return true;
34343
+ }
34344
+ }
34345
+ return false;
34346
+ };
34347
+ // A Box parent says its flow on its element; a plain element is read from its
34348
+ // computed style, which only a box mounted under one pays for.
34349
+ const readDomParentFlow = boxEl => {
34350
+ let parent = boxEl.parentElement;
34351
+ while (parent) {
34352
+ const flowAttribute = parent.getAttribute("navi-box-flow");
34353
+ if (flowAttribute) {
34354
+ return flowAttribute;
34355
+ }
34356
+ const {
34357
+ display,
34358
+ flexDirection
34359
+ } = window.getComputedStyle(parent);
34360
+ if (display === "contents") {
34361
+ parent = parent.parentElement;
34362
+ continue;
34363
+ }
34364
+ if (display === "flex" || display === "inline-flex") {
34365
+ const axis = flexDirection.startsWith("column") ? "y" : "x";
34366
+ return `${display}-${axis}`;
34367
+ }
34368
+ if (display === "grid" || display === "inline-grid") {
34369
+ return display;
34370
+ }
34371
+ return "block";
34372
+ }
34373
+ return undefined;
34374
+ };
34305
34375
  const isScrollingOverflow = value => value === "auto" || value === "scroll";
34306
34376
  const mayHoldComponent = children => {
34307
34377
  if (children === null || children === undefined) {
@@ -45314,17 +45384,22 @@ document.body.addEventListener(
45314
45384
  * on screen. A registry keyed by id then holds the newcomer, and loses the
45315
45385
  * first control when the newcomer leaves. navi renders on the client only, so
45316
45386
  * a counter is all the uniqueness needs.
45387
+ *
45388
+ * Held in a ref, not in useState: @preact/signals' shouldComponentUpdate
45389
+ * skips the render of a component holding hook state whose props did not
45390
+ * change, so an id kept as state would change when its caller renders.
45317
45391
  */
45318
45392
 
45319
45393
 
45320
45394
  let generatedIdCount = 0;
45321
45395
 
45322
45396
  const useGeneratedId = () => {
45323
- const [id] = useState(() => {
45397
+ const idRef = useRef(null);
45398
+ if (idRef.current === null) {
45324
45399
  generatedIdCount++;
45325
- return `navi_${generatedIdCount}`;
45326
- });
45327
- return id;
45400
+ idRef.current = `navi_${generatedIdCount}`;
45401
+ }
45402
+ return idRef.current;
45328
45403
  };
45329
45404
 
45330
45405
  /**
@@ -59788,7 +59863,7 @@ const Expandable = props => {
59788
59863
  const rootRef = ref || defaultRef;
59789
59864
  const uiRef = useRef();
59790
59865
  const contentContainerRef = useRef();
59791
- const contentId = useId();
59866
+ const contentId = useGeneratedId();
59792
59867
  const isColumn = layout === "column";
59793
59868
  const closedContentSized = isColumn && mount === "always";
59794
59869
  // Only the two directions the layout has room for: a column layout reveals
@@ -61428,7 +61503,7 @@ const ButtonConfirm = ({
61428
61503
  // it is elsewhere by then, and pulling it back would take it from whatever
61429
61504
  // the person moved to.
61430
61505
  const restoreFocusRef = useRef(false);
61431
- const questionId = `${useId()}_question`;
61506
+ const questionId = `${useGeneratedId()}_question`;
61432
61507
  const cancel = restoreFocus => {
61433
61508
  restoreFocusRef.current = restoreFocus;
61434
61509
  setAsking(false);
@@ -62213,7 +62288,7 @@ const ControlSwap = props => {
62213
62288
  const activeSide = sides[activeIndex];
62214
62289
  const capRefs = [useRef(), useRef()];
62215
62290
  const slotRefs = [useRef(), useRef()];
62216
- const slotIdPrefix = useId();
62291
+ const slotIdPrefix = useGeneratedId();
62217
62292
  const activeNameRef = useRef(floor.name);
62218
62293
  activeNameRef.current = activeSide.name;
62219
62294
  const swapTo = (name, event, pressedCapIndex = null) => {
@@ -63063,7 +63138,7 @@ const InputCheckboxFieldInterface = props => {
63063
63138
  // setting's name — which are its label and its accessible name: a mode one
63064
63139
  // toggles is a checkbox, not a button writing a signal on each press.
63065
63140
  const hasLabel = variant === "button" && children !== undefined && children !== null && children !== false;
63066
- const labelId = useId();
63141
+ const labelId = useGeneratedId();
63067
63142
  const boxRef = useRef();
63068
63143
  useAccentColorAttributes(boxRef, accentColor, {
63069
63144
  elementSelector: ".navi_checkbox_accent_probe"
@@ -74208,7 +74283,7 @@ const PopupTitleContext = createContext(null);
74208
74283
  const PopupTitle = props => {
74209
74284
  import.meta.css = [css$E, "@jsenv/navi/src/layout/popup_title.jsx"];
74210
74285
  const registerTitle = useContext(PopupTitleContext);
74211
- const idDefault = useId();
74286
+ const idDefault = useGeneratedId();
74212
74287
  const id = props.id || `popup_title_${idDefault}`;
74213
74288
  useLayoutEffect(() => {
74214
74289
  if (!registerTitle) {
@@ -77513,7 +77588,7 @@ const usePopoverProps = props => {
77513
77588
  // open, two separate invocations of openEffect that don't otherwise share
77514
77589
  // any scope, hence the ref.
77515
77590
  const disarmBackdropHideRef = useRef(null);
77516
- const defaultId = useId();
77591
+ const defaultId = useGeneratedId();
77517
77592
  const id = rest.id || defaultId;
77518
77593
  const backdropId = `${id}-backdrop`;
77519
77594
  const debugPopup = useDebugPopup();
@@ -79103,10 +79178,10 @@ const PickerCustom = props => {
79103
79178
  // before computing popupId below, so two Pickers without an explicit id never collide.
79104
79179
  // Captured before the fallback chain below overwrites props.id — needed to
79105
79180
  // know whether the id actually came from the caller (stable) or from
79106
- // useId()/ControlIdContext (a generated id names one mount: a reload, or a
79181
+ // useGeneratedId()/ControlIdContext (a generated id names one mount: a reload, or a
79107
79182
  // return to this page, generates another), see pickerNavType below.
79108
79183
  const hasExplicitId = Boolean(props.id);
79109
- const idDefault = useId();
79184
+ const idDefault = useGeneratedId();
79110
79185
  const controlId = useContext(ControlIdContext);
79111
79186
  props.id = props.id || controlId || idDefault;
79112
79187
  // Given no value, the picker holds what the control in its popup holds (see
@@ -79207,7 +79282,7 @@ const PickerCustom = props => {
79207
79282
  const debugPopup = useDebugPopup();
79208
79283
  // In "dialog" mode with a stable, caller-provided id, enterExpanded()
79209
79284
  // pushes a history entry so the back button closes it. Every other case
79210
- // (popover mode, or a dialog whose id was auto-generated via useId()/
79285
+ // (popover mode, or a dialog whose id was auto-generated via useGeneratedId()/
79211
79286
  // ControlIdContext) replaces the current history state instead — a
79212
79287
  // generated id names one mount, so pushing it would either leave an entry
79213
79288
  // nothing reads or, worse, collide with a different component's own
@@ -82084,7 +82159,7 @@ const ListSelectable = props => {
82084
82159
  const Next = useNextResolver();
82085
82160
  import.meta.css = [css$x, "@jsenv/navi/src/control/list/list_selectable.jsx"];
82086
82161
  // we allow ourselves to auto-generate a name
82087
- const defaultName = useId();
82162
+ const defaultName = useGeneratedId();
82088
82163
  props.name = props.name || `listbox_${defaultName}`;
82089
82164
  const {
82090
82165
  ref,
@@ -82433,7 +82508,7 @@ const ListItemSelectableResolver = props => {
82433
82508
  };
82434
82509
  const ListItemSelectable = props => {
82435
82510
  const Next = useNextResolver();
82436
- const defaultId = useId();
82511
+ const defaultId = useGeneratedId();
82437
82512
  // Whether the caller SAYS anything about this row's selection. Passing
82438
82513
  // `checked: undefined` is not the same as not passing it: a control with the
82439
82514
  // key present calls itself controlled, and a controlled row is one the list
@@ -83423,7 +83498,7 @@ const ListUI = props => {
83423
83498
  } = props;
83424
83499
  // Remembered by name, and a name made up at render (see ListFirstResolver)
83425
83500
  // names no list a later mount would recognize.
83426
- const rememberScroll = !scrollResetOnNavigation && !isLikelyPreactGeneratedId(rest.id);
83501
+ const rememberScroll = !scrollResetOnNavigation && !isGeneratedId(rest.id);
83427
83502
  // Where the list was when its screen was left, when this is the way back
83428
83503
  // (see scroll_restoration.js). Read once: `defaultScrolled` is held by
83429
83504
  // reference, and a place read again each render would be a list moved each
@@ -83733,7 +83808,7 @@ const ListFirstResolver = props => {
83733
83808
  const Next = useNextResolver();
83734
83809
  const refDefault = useRef(null);
83735
83810
  props.ref = props.ref || refDefault;
83736
- const idDefault = useId();
83811
+ const idDefault = useGeneratedId();
83737
83812
  props.id = props.id || idDefault;
83738
83813
  const listItemsRef = useRef(null);
83739
83814
  if (!listItemsRef.current) {
@@ -87147,7 +87222,7 @@ const ListItemUI = props => {
87147
87222
  if (identityMatters && props.id === undefined) {
87148
87223
  console.warn("ListItem is missing an explicit id prop. Provide a stable id so pointed/selected state survives search reordering.");
87149
87224
  }
87150
- const idDefault = useId();
87225
+ const idDefault = useGeneratedId();
87151
87226
  props.id = props.id || idDefault;
87152
87227
  const listItems = useContext(ListItemsContext);
87153
87228
  const groupId = useContext(ListGroupContext);
@@ -87960,7 +88035,7 @@ const useRunItems = (store, {
87960
88035
  renderSkeleton,
87961
88036
  renderError
87962
88037
  }) => {
87963
- const ownerId = useId();
88038
+ const ownerId = useGeneratedId();
87964
88039
  // Dev only: the group `groupOfMissing` put an item not held in, by rank,
87965
88040
  // checked against `groupBy` once the item lands.
87966
88041
  const missingGroupCheckRef = useRef(null);
@@ -88515,7 +88590,7 @@ const ListRunSkeletonItem = ({
88515
88590
  }) => {
88516
88591
  const listItems = useContext(ListItemsContext);
88517
88592
  const groupId = useContext(ListGroupContext);
88518
- const entryId = useId();
88593
+ const entryId = useGeneratedId();
88519
88594
  listItems.draw(entryId, {
88520
88595
  ownerId: runItem.ownerId,
88521
88596
  place: runItem.index,
@@ -89247,7 +89322,7 @@ const ListItemGroup = ({
89247
89322
  children,
89248
89323
  ...rest
89249
89324
  }) => {
89250
- const groupId = useId();
89325
+ const groupId = useGeneratedId();
89251
89326
  const listItems = useContext(ListItemsContext);
89252
89327
  const group = listItems.group(groupId);
89253
89328
  useLayoutEffect(() => {
@@ -93470,7 +93545,7 @@ const Spin = ({
93470
93545
  } else {
93471
93546
  compareValues ??= compareValuesDefault;
93472
93547
  }
93473
- const id = useId();
93548
+ const id = useGeneratedId();
93474
93549
  // What the group around it says, when there is one: how big the whole thing
93475
93550
  // is written is said once, on the group, and every spin in it follows.
93476
93551
  const group = useContext(SpinGroupContext);
@@ -94688,7 +94763,7 @@ const TimeRangeSpin = ({
94688
94763
  endTimeProps,
94689
94764
  ...rest
94690
94765
  }) => {
94691
- const startId = useId();
94766
+ const startId = useGeneratedId();
94692
94767
  return jsxs(ControlGroup, {
94693
94768
  flex: true,
94694
94769
  alignY: "center",
@@ -94781,7 +94856,7 @@ const css$q = /* css */`.navi_checkbox_group {
94781
94856
  const CheckboxGroup = props => {
94782
94857
  const refDefault = useRef(null);
94783
94858
  props.ref = props.ref || refDefault;
94784
- const defaultName = useId();
94859
+ const defaultName = useGeneratedId();
94785
94860
  props.name = props.name || `checkbox_group_${defaultName}`;
94786
94861
  const checkboxGroup = jsx(CheckboxGroupInterface, {
94787
94862
  ...props
@@ -95546,7 +95621,7 @@ const css$o = /* css */`.navi_radio_group {
95546
95621
  const RadioGroup = props => {
95547
95622
  const refDefault = useRef(null);
95548
95623
  props.ref = props.ref || refDefault;
95549
- const defaultName = useId();
95624
+ const defaultName = useGeneratedId();
95550
95625
  props.name = props.name || `radio_group_${defaultName}`;
95551
95626
  const radioGroup = jsx(RadioGroupInterface, {
95552
95627
  ...props
@@ -95884,7 +95959,7 @@ const SplitButton = props => {
95884
95959
  if (popupProps.positionArea === undefined && mode === "popover") {
95885
95960
  popupProps.positionArea = "bottom-end";
95886
95961
  }
95887
- const idDefault = useId();
95962
+ const idDefault = useGeneratedId();
95888
95963
  const idResolved = id || idDefault;
95889
95964
  const menuId = `${idResolved}_menu`;
95890
95965
  const rootRef = useRef(null);
@@ -96639,12 +96714,18 @@ const buildMatchInfo = (searchText, items, matchFn) => {
96639
96714
  * needed. `data.role === "presentation"` keeps it out entirely.
96640
96715
  * Returns the visible rank (position among visible items), or -1 when hidden
96641
96716
  * or filtered.
96717
+ * An item rendered without an index keeps the position it has, and a new one
96718
+ * goes last: for a host that cannot know the order at render and gives it
96719
+ * afterwards with setOrder.
96642
96720
  * Signals and onChange are deferred to a microtask so multiple items updating
96643
96721
  * in one commit cause only one notification.
96644
96722
  *
96645
96723
  * getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
96646
96724
  * its visible rank. Returns undefined when index is out of range.
96647
96725
  *
96726
+ * setOrder(ids): gives each tracked item its position from its rank in
96727
+ * `ids`.
96728
+ *
96648
96729
  * peekItems(): the items as they stand right now, without waiting for the
96649
96730
  * deferred notification — what a sibling rendering after the items must read
96650
96731
  * to paint them in the same commit.
@@ -96925,8 +97006,14 @@ const createItemTracker = (onChange) => {
96925
97006
  // that determines this item's position among siblings.
96926
97007
  // Returns the item's visible rank among non-hidden items, or -1 when hidden.
96927
97008
  const useTrackItem = (data) => {
96928
- const { id, index } = data;
97009
+ const { id } = data;
96929
97010
  const key = keyForId(id);
97011
+ let { index } = data;
97012
+ if (index === undefined) {
97013
+ index = keyToExplicitOrder.has(key)
97014
+ ? keyToExplicitOrder.get(key)
97015
+ : Infinity;
97016
+ }
96930
97017
 
96931
97018
  syncItem(key, index, data);
96932
97019
  notify();
@@ -96944,6 +97031,24 @@ const createItemTracker = (onChange) => {
96944
97031
  return keyToOrderedIndex.get(key) ?? -1;
96945
97032
  };
96946
97033
 
97034
+ const setOrder = (ids) => {
97035
+ let changed = false;
97036
+ for (let i = 0; i < ids.length; i++) {
97037
+ const key = idToKey.get(ids[i]);
97038
+ if (key === undefined || !allRegistrations.has(key)) {
97039
+ continue;
97040
+ }
97041
+ if (keyToExplicitOrder.get(key) === i) {
97042
+ continue;
97043
+ }
97044
+ syncItem(key, i, allRegistrations.get(key));
97045
+ changed = true;
97046
+ }
97047
+ if (changed) {
97048
+ notify();
97049
+ }
97050
+ };
97051
+
96947
97052
  const getTrackedItemByIndex = (index) => {
96948
97053
  const key = orderedKeys[index];
96949
97054
  if (key === undefined) {
@@ -96977,6 +97082,7 @@ const createItemTracker = (onChange) => {
96977
97082
  return {
96978
97083
  useTrackItem,
96979
97084
  getTrackedItemByIndex,
97085
+ setOrder,
96980
97086
  peekItems,
96981
97087
  itemsSignal,
96982
97088
  visibleItemsSignal,
@@ -97374,8 +97480,8 @@ const getNumericRange = items => {
97374
97480
 
97375
97481
  // Wheel.Item registers its {value, label} here so WheelUI knows the full ordered
97376
97482
  // list of items regardless of how children are wrapped (providers, fragments…).
97377
- // indexRef gives each item its position: WheelUI resets it to 0 every render and
97378
- // each item reads-and-increments it as it renders, in document order.
97483
+ // The position comes after the commit, from the marker each item leaves where it
97484
+ // stands (see WheelUI).
97379
97485
  const WheelItemTrackerContext = createContext(null);
97380
97486
 
97381
97487
  // Lets a WheelGroup push shared presentation (glass, orientation) down to every
@@ -97997,15 +98103,21 @@ function WheelUI(props) {
97997
98103
  // (children may be wrapped). This ordered list is the source of truth for the
97998
98104
  // whole wheel — the rendered rows are a small recycled window over it.
97999
98105
  const tracker = useItemTracker();
98000
- const indexRef = useRef(0);
98001
- indexRef.current = 0;
98002
- const trackerContextRef = useRef(null);
98003
- if (!trackerContextRef.current) {
98004
- trackerContextRef.current = {
98005
- tracker,
98006
- indexRef
98007
- };
98008
- }
98106
+ // The order is read from the markers the items leave in the DOM, not from the
98107
+ // order they render in: an item whose props did not change may skip a render
98108
+ // (signals' shouldComponentUpdate), and a render count would then rank the
98109
+ // ones that did render first.
98110
+ useLayoutEffect(() => {
98111
+ const el = ref.current;
98112
+ if (!el) {
98113
+ return;
98114
+ }
98115
+ const ids = [];
98116
+ for (const marker of el.querySelectorAll("[data-navi-wheel-item]")) {
98117
+ ids.push(marker.getAttribute("data-navi-wheel-item"));
98118
+ }
98119
+ tracker.setOrder(ids);
98120
+ });
98009
98121
  const trackedItems = tracker.itemsSignal.value;
98010
98122
  const trackedItemsRef = useRef(trackedItems);
98011
98123
  trackedItemsRef.current = trackedItems;
@@ -98805,7 +98917,7 @@ function WheelUI(props) {
98805
98917
  className: "navi_wheel_pane",
98806
98918
  "data-side": "start"
98807
98919
  }), jsx(WheelItemTrackerContext.Provider, {
98808
- value: trackerContextRef.current,
98920
+ value: tracker,
98809
98921
  children: jsx(ControlFacadeChildrenWrapper, {
98810
98922
  facadeController: facadeController,
98811
98923
  children: children
@@ -98918,9 +99030,10 @@ const WheelWindow = ({
98918
99030
  * Wheel.Item — a value in a Wheel. Must be used inside <Wheel>.
98919
99031
  *
98920
99032
  * Registration only: it records its {value, label, itemProps} with the wheel and
98921
- * renders NOTHING. The wheel is virtualized — it renders a small recycled window
98922
- * of rows and fills each from this tracked data — so the value list is the source
98923
- * of truth, not a per-item DOM node. Selection lives on the wheel's value.
99033
+ * renders no row, only a hidden marker the wheel reads its position from. The
99034
+ * wheel is virtualized — it renders a small recycled window of rows and fills
99035
+ * each from this tracked data — so the value list is the source of truth, not a
99036
+ * per-item DOM node. Selection lives on the wheel's value.
98924
99037
  *
98925
99038
  * @type {import("preact").FunctionComponent<{
98926
99039
  * value: any,
@@ -98934,19 +99047,20 @@ const WheelItem = ({
98934
99047
  children,
98935
99048
  ...rest
98936
99049
  }) => {
98937
- const idDefault = useId();
99050
+ const idDefault = useGeneratedId();
98938
99051
  const resolvedId = id || idDefault;
98939
99052
  // Wheel.Item must live inside a Wheel, so the context is always present.
98940
- const trackerContext = useContext(WheelItemTrackerContext);
98941
- const index = trackerContext.indexRef.current++;
98942
- trackerContext.tracker.useTrackItem({
99053
+ const tracker = useContext(WheelItemTrackerContext);
99054
+ tracker.useTrackItem({
98943
99055
  id: resolvedId,
98944
- index,
98945
99056
  value,
98946
99057
  label: children,
98947
99058
  itemProps: rest
98948
99059
  });
98949
- return null;
99060
+ return jsx("span", {
99061
+ hidden: true,
99062
+ "data-navi-wheel-item": resolvedId
99063
+ });
98950
99064
  };
98951
99065
  Wheel.Item = WheelItem;
98952
99066
 
@@ -99532,7 +99646,7 @@ const TimeRangeWheel = ({
99532
99646
  ...rest
99533
99647
  }) => {
99534
99648
  import.meta.css = [css$j, "@jsenv/navi/src/control/wheel/wheel_time.jsx"];
99535
- const startId = useId();
99649
+ const startId = useGeneratedId();
99536
99650
  const startRef = useRef(null);
99537
99651
  const endRef = useRef(null);
99538
99652
  const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
@@ -102520,7 +102634,7 @@ const useIsInTableHead = () => useContext(TableSectionContext) === "head";
102520
102634
  const Table = props => {
102521
102635
  installTableCss();
102522
102636
  const tableDefaultRef = useRef();
102523
- const tableDefaultId = `table-${useId()}`;
102637
+ const tableDefaultId = `table-${useGeneratedId()}`;
102524
102638
  const {
102525
102639
  id = tableDefaultId,
102526
102640
  selection = [],