@jsenv/navi 0.29.322 → 0.29.324

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.
@@ -7,7 +7,7 @@ export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
7
  import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
- import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, Fragment as Fragment$1 } from "preact";
10
+ import { isValidElement, createContext, render, h, toChildArray, options, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
11
11
  import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
12
12
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
13
13
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
@@ -32476,8 +32476,26 @@ const resolveExplicitTarget = (element) => {
32476
32476
  }
32477
32477
  return undefined;
32478
32478
  };
32479
+ // The control around the source that a command can be about: one holding a
32480
+ // value of its own. A popup is a control root too (it answers interactions,
32481
+ // carries a callout), but it has no host of its own — asked for one, it
32482
+ // answers with the first control INSIDE it, which for a field sending
32483
+ // --navi-update from a picker's popup is that very field: the command then
32484
+ // sets the field to what it already holds, which fires the command again,
32485
+ // without end. Such a surface is walked past, up to the picker or the form.
32479
32486
  const resolveFirstParentControl = (el) => {
32480
- return getParentControl(el);
32487
+ let parentControl = getParentControl(el);
32488
+ while (parentControl && !ownsControlHost(parentControl)) {
32489
+ parentControl = getParentControl(parentControl);
32490
+ }
32491
+ return parentControl;
32492
+ };
32493
+ const ownsControlHost = (controlRoot) => {
32494
+ const controlHost = findControlHost(controlRoot);
32495
+ return (
32496
+ Boolean(controlHost) &&
32497
+ controlHost.closest("[navi-control]") === controlRoot
32498
+ );
32481
32499
  };
32482
32500
  const resolveFirstChildControl = (el) => {
32483
32501
  let startEl;
@@ -34689,19 +34707,27 @@ const writeInSignal = (signal, value, { history }) => {
34689
34707
  // is open (`?seat=<gameId>` over a list of cards), and closed is the signal
34690
34708
  // holding none of their values — `undefined`, which a state signal reads as
34691
34709
  // its default.
34710
+ //
34711
+ // A popup without a `value` reads anything the signal holds — save `false`,
34712
+ // `null` and `undefined` — as open: a single sheet bound to `?open=<id>` is
34713
+ // open on whichever id the address names, and the id is the popup's content
34714
+ // to read, not its to write. Opening writes `true` only into a signal that
34715
+ // reads closed; closing writes `false` where the signal held `true`, so a
34716
+ // boolean signal keeps its boolean, and `undefined` for anything else.
34692
34717
  const writeOpenedInSignal = (signal, opened, event, popupValue) => {
34693
- if (readOpened(signal.peek(), popupValue) === opened) {
34718
+ const signalValue = signal.peek();
34719
+ if (readOpened(signalValue, popupValue) === opened) {
34694
34720
  // The signal already says so, meaning this open/close IS what it asked
34695
34721
  // for: a back press that took the popup out of the url, the application
34696
34722
  // writing it. Nothing to write back — and nothing to go back to either,
34697
34723
  // since the navigation navBack would undo is the one that asked for this.
34698
34724
  return;
34699
34725
  }
34700
- const closedValue = popupValue === undefined ? false : undefined;
34701
34726
  if (opened) {
34702
34727
  signal.value = popupValue === undefined ? true : popupValue;
34703
34728
  return;
34704
34729
  }
34730
+ const closedValue = readClosedValue(signalValue);
34705
34731
  if (
34706
34732
  signal.options?.getHistory?.() === "push" &&
34707
34733
  // Nothing of this document behind: the popup was opened by the url itself
@@ -34722,10 +34748,15 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
34722
34748
  };
34723
34749
  const readOpened = (signalValue, popupValue) => {
34724
34750
  if (popupValue === undefined) {
34725
- return signalValue;
34751
+ return (
34752
+ signalValue !== undefined && signalValue !== null && signalValue !== false
34753
+ );
34726
34754
  }
34727
34755
  return signalValue === popupValue;
34728
34756
  };
34757
+ const readClosedValue = (openSignalValue) => {
34758
+ return openSignalValue === true ? false : undefined;
34759
+ };
34729
34760
 
34730
34761
  /**
34731
34762
  * Keeps an open controller in sync with where the caller says the popup should
@@ -34780,6 +34811,13 @@ const useOpenPropsEffectOnOpenController = (
34780
34811
  : signal
34781
34812
  ? readOpened(signal.value, value)
34782
34813
  : props.open;
34814
+ // What the signal holds while it reads open — `true`, the popup's `value`,
34815
+ // or the id a single sheet is open on. Put back when a close is refused,
34816
+ // since by then the signal has been written closed.
34817
+ const openSignalValueRef = useRef(undefined);
34818
+ if (signal && open) {
34819
+ openSignalValueRef.current = signal.value;
34820
+ }
34783
34821
  // Assigned on every render, like openEffect, so it always closes over the
34784
34822
  // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
34785
34823
  // --navi-close command) writes what happened where the caller keeps it, so
@@ -34860,10 +34898,11 @@ const useOpenPropsEffectOnOpenController = (
34860
34898
  // request already reads in the signal, and with a `value` what the
34861
34899
  // signal holds may be another popup's, which the write would erase.
34862
34900
  if (signal && openController.opened !== open) {
34863
- const opened = openController.opened;
34864
34901
  writeInSignal(
34865
34902
  signal,
34866
- value === undefined ? opened : opened ? value : undefined,
34903
+ openController.opened
34904
+ ? openSignalValueRef.current
34905
+ : readClosedValue(signal.peek()),
34867
34906
  { history: "replace" },
34868
34907
  );
34869
34908
  }
@@ -35257,6 +35296,21 @@ const warnPopupHasNoElementToOpen = (popupKind) => {
35257
35296
  */
35258
35297
  const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
35259
35298
 
35299
+ /**
35300
+ * `data-navi-popup-inside` is the other direction: a box of the PAGE the
35301
+ * caller says belongs to a popup, so a press on it is not outside that popup
35302
+ * even though it lies beyond the border box — a card whose press is what
35303
+ * fills the one panel of a board. Its value names the popup by `id`, the way
35304
+ * `commandfor` does (several ids, space separated), because the exemption is
35305
+ * one popup's: a menu open beside the board still closes on the same press.
35306
+ *
35307
+ * Unlike the outside marker it answers for its whole subtree: the outside
35308
+ * marker carves free space out of a surface whose controls stay surface, this
35309
+ * one adds a whole thing — the card, its count, its buttons — to the popup's
35310
+ * own ground, and nothing in it is a dismissal.
35311
+ */
35312
+ const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
35313
+
35260
35314
  /**
35261
35315
  * What a press landing on a region the caller declared as not-its-surface
35262
35316
  * does: exactly what the same press on the backdrop would do.
@@ -35403,6 +35457,14 @@ const armOutsidePressClose = (
35403
35457
  ) {
35404
35458
  return;
35405
35459
  }
35460
+ if (
35461
+ popupEl.id &&
35462
+ pointerDownEvent.target.closest?.(
35463
+ `[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
35464
+ )
35465
+ ) {
35466
+ return;
35467
+ }
35406
35468
  // Real DOM containment wins over the rectangle below — an element
35407
35469
  // genuinely inside the popup (`overflow: visible`, a negative margin, an
35408
35470
  // absolutely-positioned child) can be painted outside its border box, and
@@ -37410,6 +37472,46 @@ const acknowledgeOwnAction = (controller) => {
37410
37472
  controller.acknowledgeUIState();
37411
37473
  };
37412
37474
 
37475
+ // What a group of checkboxes or radios is worth is a claim about values, and
37476
+ // a value whose box is not there cannot be contradicted by that box: a row
37477
+ // scrolled out of a virtualized list, a filtered-out option, a whole page being
37478
+ // parked while a route action reruns — its children leave one at a time, and
37479
+ // each partial reading would otherwise be taken for the answer and written
37480
+ // into whatever the group is bound to (an array signal emptied down to `[]`
37481
+ // by its own unmount). `kept` is what the group holds as it is asked; a value
37482
+ // in it stays until a DRAWN child carrying that value says otherwise.
37483
+ //
37484
+ // Read off the group rather than remembered here on purpose: a value put ON
37485
+ // the group (a `value` prop, a signal, a reopened popup) replaces the whole
37486
+ // selection, and a private memory would go on holding the rows it could not
37487
+ // see being unselected.
37488
+ const keptAndCheckedChildUIStates = (children, fallbackState, kept) => {
37489
+ const drawnValues = new Set(children.map((child) => child.props.value));
37490
+ const values = Array.isArray(kept)
37491
+ ? kept.filter((value) => !drawnValues.has(value))
37492
+ : [];
37493
+ for (const child of children) {
37494
+ if (child.uiState !== undefined) {
37495
+ values.push(child.uiState);
37496
+ }
37497
+ }
37498
+ return values.length === 0 ? undefined : values;
37499
+ };
37500
+ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
37501
+ for (const child of children) {
37502
+ if (child.uiState !== undefined) {
37503
+ return child.uiState;
37504
+ }
37505
+ }
37506
+ // No drawn child claims it. If the child that held it IS drawn, it was really
37507
+ // deselected; if it is not, the group keeps what it holds.
37508
+ const keptIsDrawn = children.some((child) => child.props.value === kept);
37509
+ if (keptIsDrawn) {
37510
+ return undefined;
37511
+ }
37512
+ return kept;
37513
+ };
37514
+
37413
37515
  // Default aggregate/distribute implementations keyed by controlType or stateType.
37414
37516
  // Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
37415
37517
  // distributeChildUIState. If neither a default nor an explicit impl is found for a
@@ -37418,7 +37520,7 @@ const GROUP_DEFAULTS = {
37418
37520
  radio_group: {
37419
37521
  childControlFilter: (child) =>
37420
37522
  child.controlType === "input" && child.controlHostProps?.type === "radio",
37421
- aggregateChildStates: firstDefinedChildUIState,
37523
+ aggregateChildStates: keptOrFirstDefinedChildUIState,
37422
37524
  distributeChildUIState: (newUIState, childUIStateController) => {
37423
37525
  const childSelected = childUIStateController.props.value === newUIState;
37424
37526
  if (childSelected) {
@@ -37431,16 +37533,7 @@ const GROUP_DEFAULTS = {
37431
37533
  childControlFilter: (child) =>
37432
37534
  child.controlType === "input" &&
37433
37535
  child.controlHostProps?.type === "checkbox",
37434
- aggregateChildStates: (children) => {
37435
- const values = [];
37436
- for (const child of children) {
37437
- const childUIState = child.uiState;
37438
- if (childUIState !== undefined) {
37439
- values.push(childUIState);
37440
- }
37441
- }
37442
- return values.length === 0 ? undefined : values;
37443
- },
37536
+ aggregateChildStates: keptAndCheckedChildUIStates,
37444
37537
  distributeChildUIState: (newUIState, childUIStateController) => {
37445
37538
  const childSelected =
37446
37539
  Array.isArray(newUIState) &&
@@ -48792,33 +48885,6 @@ const createToggleEvent = open => {
48792
48885
  return toggleEvent;
48793
48886
  };
48794
48887
 
48795
- const NextResolverContext = createContext(null);
48796
- const useNextResolver = () => useContext(NextResolverContext);
48797
-
48798
- /**
48799
- * Creates a renderComponent function that passes props through a chain of resolvers.
48800
- * Each resolver is a Preact component rendered in sequence (hooks are allowed).
48801
- * To pass through to the next resolver, call useNextResolver() and render the
48802
- * returned Next component with the desired props.
48803
- * To terminate the chain early (e.g. render a specialized component), render
48804
- * directly without calling Next.
48805
- *
48806
- * The last entry in the array is the final/target component — it receives null
48807
- * from useNextResolver() indicating it is terminal.
48808
- *
48809
- * Usage:
48810
- * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
48811
- * // Then inside a component render:
48812
- * renderButton(props)
48813
- *
48814
- * Each position of the chain has a runner of its own, defined once: it renders
48815
- * its resolver under a NextResolverContext holding the runner after it. A
48816
- * resolver that re-renders on its own and renders <Next> therefore resumes at
48817
- * its own position without any bookkeeping — the Next it was given is the one
48818
- * made for it. A chain rendered by the hundred (a list's rows) pays two
48819
- * components per position, the runner and the resolver, so nothing here is a
48820
- * component that merely forwards.
48821
- */
48822
48888
  const createComponentResolver = (resolvers, {
48823
48889
  pure
48824
48890
  } = {}) => {
@@ -48827,13 +48893,10 @@ const createComponentResolver = (resolvers, {
48827
48893
  for (let index = 0; index < resolvers.length; index++) {
48828
48894
  const Resolver = resolvers[index];
48829
48895
  const isLast = index === lastIndex;
48830
- const Runner = props => jsx(NextResolverContext.Provider, {
48831
- value: isLast ? null : runners[index + 1],
48832
- children: jsx(Resolver, {
48833
- ...props
48834
- })
48835
- });
48836
- Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
48896
+ function Runner(props) {
48897
+ return runResolverBody(this, Resolver, props, isLast ? null : runners[index + 1]);
48898
+ }
48899
+ Runner.displayName = Resolver.displayName || Resolver.name;
48837
48900
  runners.push(Runner);
48838
48901
  }
48839
48902
  const FirstRunner = runners[0];
@@ -48864,6 +48927,54 @@ const createComponentResolver = (resolvers, {
48864
48927
  }
48865
48928
  return PureRenderComponent;
48866
48929
  };
48930
+ const useNextResolver = () => {
48931
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
48932
+ throw new Error("useNextResolver() called outside a resolver body: a resolver rendered by another resolver is rendered with renderResolver(Resolver, props), not as a JSX child.");
48933
+ }
48934
+ return currentNext;
48935
+ };
48936
+ const renderResolver = (Resolver, props) => {
48937
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
48938
+ throw new Error(`renderResolver(${Resolver.displayName || Resolver.name}) called outside a resolver body: only a resolver can render another one.`);
48939
+ }
48940
+ let SubRunner = subRunnerByResolver.get(Resolver);
48941
+ if (!SubRunner) {
48942
+ SubRunner = function (props) {
48943
+ const {
48944
+ [NEXT_PROP]: next,
48945
+ ...ownProps
48946
+ } = props;
48947
+ return runResolverBody(this, Resolver, ownProps, next);
48948
+ };
48949
+ SubRunner.displayName = Resolver.displayName || Resolver.name;
48950
+ subRunnerByResolver.set(Resolver, SubRunner);
48951
+ }
48952
+ return createElement(SubRunner, {
48953
+ ...props,
48954
+ [NEXT_PROP]: currentNext
48955
+ });
48956
+ };
48957
+
48958
+ // What useNextResolver() answers: the runner after the one whose body is
48959
+ // running, null for the last — and, outside any body, a value that is neither.
48960
+ const OUTSIDE_RESOLVER_BODY = Symbol("outside_resolver_body");
48961
+ let currentNext = OUTSIDE_RESOLVER_BODY;
48962
+ const runResolverBody = (instance, Resolver, props, next) => {
48963
+ const nextBefore = currentNext;
48964
+ currentNext = next;
48965
+ try {
48966
+ return Resolver.call(instance, props);
48967
+ } finally {
48968
+ currentNext = nextBefore;
48969
+ }
48970
+ };
48971
+
48972
+ // One runner per resolver rendered through renderResolver, made on first use:
48973
+ // the same component type on every render, so a resolver picked again is
48974
+ // updated in place and another one picked is a remount, like any child.
48975
+ const subRunnerByResolver = new WeakMap();
48976
+ // Rides on the props of a sub-runner, stripped before the body sees them.
48977
+ const NEXT_PROP = "navi-resolver-next";
48867
48978
  function pureShouldComponentUpdate(nextProps) {
48868
48979
  return shallowDiffers(this.props, nextProps);
48869
48980
  }
@@ -49689,9 +49800,7 @@ const ButtonConfirm = ({
49689
49800
  const ButtonRouteResolver = props => {
49690
49801
  const Next = useNextResolver();
49691
49802
  if (props.route) {
49692
- return jsx(ButtonWithRoute, {
49693
- ...props
49694
- });
49803
+ return renderResolver(ButtonWithRoute, props);
49695
49804
  }
49696
49805
  return jsx(Next, {
49697
49806
  ...props
@@ -52295,9 +52404,7 @@ const installInputCss = () => {
52295
52404
  const InputModeResolver = props => {
52296
52405
  const Next = useNextResolver();
52297
52406
  if (props.inputMode === "numeric" || props.inputMode === "decimal") {
52298
- return jsx(InputModeNumericOrDecimal, {
52299
- ...props
52300
- });
52407
+ return renderResolver(InputModeNumericOrDecimal, props);
52301
52408
  }
52302
52409
  return jsx(Next, {
52303
52410
  ...props
@@ -52525,39 +52632,25 @@ const InputTypeResolver = props => {
52525
52632
  // very same "icon while empty, clear button once filled" affordance without
52526
52633
  // pretending to be a search box.
52527
52634
  if (props.clearable && props.type !== "search") {
52528
- return jsx(InputClearable, {
52529
- ...props
52530
- });
52635
+ return renderResolver(InputClearable, props);
52531
52636
  }
52532
52637
  if (props.type === "search") {
52533
- return jsx(InputSearch, {
52534
- ...props
52535
- });
52638
+ return renderResolver(InputSearch, props);
52536
52639
  }
52537
52640
  if (props.type === "email") {
52538
- return jsx(InputEmail, {
52539
- ...props
52540
- });
52641
+ return renderResolver(InputEmail, props);
52541
52642
  }
52542
52643
  if (props.type === "tel") {
52543
- return jsx(InputTel, {
52544
- ...props
52545
- });
52644
+ return renderResolver(InputTel, props);
52546
52645
  }
52547
52646
  if (props.type === "number") {
52548
- return jsx(InputNumber, {
52549
- ...props
52550
- });
52647
+ return renderResolver(InputNumber, props);
52551
52648
  }
52552
52649
  if (props.type === "color") {
52553
- return jsx(InputColor, {
52554
- ...props
52555
- });
52650
+ return renderResolver(InputColor, props);
52556
52651
  }
52557
52652
  if (props.type === "datetime-local") {
52558
- return jsx(InputDatetimeLocal, {
52559
- ...props
52560
- });
52653
+ return renderResolver(InputDatetimeLocal, props);
52561
52654
  }
52562
52655
  return jsx(Next, {
52563
52656
  ...props
@@ -52670,9 +52763,7 @@ const InputDatetimeLocal = props => {
52670
52763
  const InputWithListResolver = props => {
52671
52764
  const Next = useNextResolver();
52672
52765
  if (props["navi-list"]) {
52673
- return jsx(InputWithList, {
52674
- ...props
52675
- });
52766
+ return renderResolver(InputWithList, props);
52676
52767
  }
52677
52768
  return jsx(Next, {
52678
52769
  ...props
@@ -52779,9 +52870,7 @@ const ChevronDownSvg$1 = () => {
52779
52870
  const InputWithSuggestionsResolver = props => {
52780
52871
  const Next = useNextResolver();
52781
52872
  if (props["navi-suggestions"]) {
52782
- return jsx(InputTextualWithSuggestions, {
52783
- ...props
52784
- });
52873
+ return renderResolver(InputTextualWithSuggestions, props);
52785
52874
  }
52786
52875
  return jsx(Next, {
52787
52876
  ...props
@@ -60553,14 +60642,15 @@ const css$E = /* css */`
60553
60642
  * the focus only leaves it for something that asked by name (`autoFocus` on
60554
60643
  * that element, which outranks whatever the dialog says).
60555
60644
  * @param {boolean} [props.open] - Controlled open state.
60556
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
60645
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
60557
60646
  * state said the way every navi control says it: the dialog opens and closes
60558
60647
  * to match the signal, and writes into it whenever it opens or closes on its
60559
60648
  * own (Escape, backdrop, a --navi-close command) — one binding to both drive
60560
60649
  * the dialog and know where it is, and the state stays where the app put it.
60561
60650
  * Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
60562
60651
  * mount behaves like `defaultOpen`: the dialog was already open, no entrance
60563
- * plays.
60652
+ * plays. A signal holding an id (one dialog showing whichever card
60653
+ * the address names) reads as open, and the dialog leaves the id alone.
60564
60654
  * @param {any} [props.value] - What `signal` holds while THIS dialog is
60565
60655
  * open, for several of them sharing one signal that says which is open
60566
60656
  * (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
@@ -62214,14 +62304,15 @@ const css$D = /* css */`
62214
62304
  * the focus only leaves it for something that asked by name (`autoFocus` on
62215
62305
  * that element, which outranks whatever the popover says).
62216
62306
  * @param {boolean} [props.open] - Controlled open state.
62217
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
62307
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
62218
62308
  * state said the way every navi control says it: the popover opens and closes
62219
62309
  * to match the signal, and writes into it whenever it opens or closes on its
62220
62310
  * own (Escape, light dismiss, a --navi-close command) — one binding to both
62221
62311
  * drive the popover and know where it is, and the state stays where the app
62222
62312
  * put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
62223
62313
  * `true` at mount behaves like `defaultOpen`: the popover was already open,
62224
- * no entrance plays.
62314
+ * no entrance plays. A signal holding an id (one popover showing whichever card
62315
+ * the address names) reads as open, and the popover leaves the id alone.
62225
62316
  * @param {any} [props.value] - What `signal` holds while THIS popover is
62226
62317
  * open, for several of them sharing one signal that says which is open
62227
62318
  * (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
@@ -63663,9 +63754,7 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
63663
63754
  const PickerCustomResolver = props => {
63664
63755
  import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
63665
63756
  if (props.children === undefined) {
63666
- return jsx(PickerNative, {
63667
- ...props
63668
- });
63757
+ return renderResolver(PickerNative, props);
63669
63758
  }
63670
63759
  if (props.mode === "callout") {
63671
63760
  // A tooltip is an icon one presses, unless told otherwise. Own-property
@@ -63727,14 +63816,12 @@ const PickerCustomResolver = props => {
63727
63816
  // how a field says its value is a JS one, kept beside the DOM (see
63728
63817
  // controller_registry.js) — the same thing type="array"/"object" already
63729
63818
  // say for their shapes.
63730
- return jsx(PickerCustom, {
63819
+ return renderResolver(PickerCustom, {
63731
63820
  ...props,
63732
63821
  type: "navi_js"
63733
63822
  });
63734
63823
  }
63735
- return jsx(PickerCustom, {
63736
- ...props
63737
- });
63824
+ return renderResolver(PickerCustom, props);
63738
63825
  };
63739
63826
  const PickerNative = props => {
63740
63827
  const Next = useNextResolver();
@@ -64360,9 +64447,9 @@ const PickerCustom = props => {
64360
64447
  });
64361
64448
  }
64362
64449
  }
64363
- return jsx(PickerContentInsidePopup, {
64450
+ return renderResolver(PickerContentInsidePopup, {
64364
64451
  ...pickerProps,
64365
- mode: mode
64452
+ mode
64366
64453
  });
64367
64454
  };
64368
64455
 
@@ -65629,14 +65716,10 @@ const createItemTracker = (onChange) => {
65629
65716
  const ListItemHeaderOrFooterResolver = props => {
65630
65717
  const Next = useNextResolver();
65631
65718
  if (props.header) {
65632
- return jsx(ListItemHeader, {
65633
- ...props
65634
- });
65719
+ return renderResolver(ListItemHeader, props);
65635
65720
  }
65636
65721
  if (props.footer) {
65637
- return jsx(ListItemFooter, {
65638
- ...props
65639
- });
65722
+ return renderResolver(ListItemFooter, props);
65640
65723
  }
65641
65724
  return jsx(Next, {
65642
65725
  ...props
@@ -65868,9 +65951,7 @@ const ListSelectableContext = createContext(false);
65868
65951
  const ListSelectableResolver = props => {
65869
65952
  const Next = useNextResolver();
65870
65953
  if (props.selectable) {
65871
- return jsx(ListSelectable, {
65872
- ...props
65873
- });
65954
+ return renderResolver(ListSelectable, props);
65874
65955
  }
65875
65956
  return jsx(ListSelectableContext.Provider, {
65876
65957
  value: false,
@@ -65892,45 +65973,6 @@ const ListSelectable = props => {
65892
65973
  focusGroupDirection,
65893
65974
  focusGroupWrap
65894
65975
  } = props;
65895
- // `kept` is what the list holds as it is asked, which is not the same as what
65896
- // its rows say: a list draws the rows it needs and no more, so the selected
65897
- // one may be scrolled out of the window or filtered out of the view, and a
65898
- // row that is not there cannot say it is not selected. Reading it off the
65899
- // group rather than remembering it here is what makes a value put ON the list
65900
- // (a `value` prop, a signal, a reopened popup) replace the whole selection —
65901
- // a private memory of its own would go on holding the rows it could not see
65902
- // being unselected.
65903
- //
65904
- // `fallbackState` is the empty of the shape the list declared below
65905
- // (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
65906
- // it is what lets an emptied list say "empty" — `undefined` is the word for
65907
- // "unset", and a bound stateSignal reads that as "nothing decided here, go
65908
- // back to the default" (see docs/control_value.md), which is how a list
65909
- // emptied down to its last row puts that row back on reload.
65910
- const aggregateChildStates = (children, fallbackState, kept) => {
65911
- if (multiple) {
65912
- const drawnValues = new Set(children.map(child => child.props.value));
65913
- const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
65914
- for (const child of children) {
65915
- if (child.uiState !== undefined) {
65916
- stillSelected.push(child.uiState);
65917
- }
65918
- }
65919
- return stillSelected.length === 0 ? fallbackState : stillSelected;
65920
- }
65921
- for (const child of children) {
65922
- if (child.uiState !== undefined) {
65923
- return child.uiState;
65924
- }
65925
- }
65926
- // No drawn row claims it. If the row that held it IS drawn, it was really
65927
- // deselected; if it is not, the list keeps what it holds.
65928
- const keptIsDrawn = children.some(child => child.props.value === kept);
65929
- if (keptIsDrawn) {
65930
- return undefined;
65931
- }
65932
- return kept;
65933
- };
65934
65976
  const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
65935
65977
  // The defaults of the group this IS (see CheckboxGroup / RadioGroup):
65936
65978
  // what a selectable list shows is a claim about what was accepted, so
@@ -65942,9 +65984,15 @@ const ListSelectable = props => {
65942
65984
  resetOnError: true,
65943
65985
  ...props
65944
65986
  }, {
65987
+ // A list draws the rows it needs and no more, so a selected row may be
65988
+ // scrolled out of the window or filtered out of the view: the default
65989
+ // aggregate of these two groups keeps a value whose row is not drawn
65990
+ // (see GROUP_DEFAULTS in ui_state_controller.js). `stateType` is what
65991
+ // an emptied multiple list says — `[]`, not `undefined`, which a bound
65992
+ // stateSignal reads as "nothing decided here, go back to the default"
65993
+ // (see docs/control_value.md).
65945
65994
  stateType: multiple ? "array" : "",
65946
- controlType: multiple ? "checkbox_group" : "radio_group",
65947
- aggregateChildStates
65995
+ controlType: multiple ? "checkbox_group" : "radio_group"
65948
65996
  });
65949
65997
  const uiGroupStateController = getUIStateControllerById(listControlProps.id);
65950
65998
  useFocusGroup(ref, {
@@ -66203,7 +66251,7 @@ const ListItemSelectableResolver = props => {
66203
66251
  const isHeaderOrFooter = Boolean(props.header || props.footer);
66204
66252
  const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
66205
66253
  if (selectable) {
66206
- return jsx(ListItemSelectable, {
66254
+ return renderResolver(ListItemSelectable, {
66207
66255
  ...props,
66208
66256
  selectable: true
66209
66257
  });
@@ -67146,6 +67194,7 @@ const ListUI = props => {
67146
67194
  ref,
67147
67195
  tracker,
67148
67196
  renderBudget,
67197
+ renderBudgetSteady: renderBudgetAfterPaint,
67149
67198
  virtualItemSize,
67150
67199
  virtual,
67151
67200
  scrolled,
@@ -67473,6 +67522,7 @@ const useListScrollSync = ({
67473
67522
  ref,
67474
67523
  tracker,
67475
67524
  renderBudget,
67525
+ renderBudgetSteady,
67476
67526
  virtualItemSize,
67477
67527
  virtual,
67478
67528
  scrolled,
@@ -68226,31 +68276,36 @@ const useListScrollSync = ({
68226
68276
  }
68227
68277
  });
68228
68278
 
68229
- // Scroll listener — slides the window as the user scrolls.
68230
- useLayoutEffect(() => {
68231
- const listContainerEl = ref.current;
68232
- if (!listContainerEl) {
68233
- return undefined;
68279
+ // The window the visible band asks for, decided from geometry: the band's
68280
+ // two edges in row units, from where the scroller's viewport cuts the list
68281
+ // and the row size. What has to be drawn is everything between the edges,
68282
+ // plus what the scroll is about to bring — so the window is judged on the
68283
+ // rows it keeps AHEAD of the band, in the direction the user goes, and moves
68284
+ // once those fall under half a screen. Re-framed, it puts three quarters of
68285
+ // its spare rows ahead and one quarter behind: a window centred on the band
68286
+ // is due to move again as soon as the band has crossed the few rows it kept
68287
+ // ahead, and each move is rows drawn while the user waits for them.
68288
+ // Without a row size there is no geometry to reason on; the row under a
68289
+ // probe stands in for the band, framed the old way.
68290
+ const scrollDirectionRef = useRef(1);
68291
+ const windowSlidRef = useRef(false);
68292
+ const budgetWarnedRef = useRef(false);
68293
+ const evaluateWindow = reason => {
68294
+ const total = virtual.totalSignal.peek();
68295
+ if (total <= renderBudget) {
68296
+ return;
68234
68297
  }
68235
68298
  const scrollerEl = getScroller();
68236
68299
  const listEl = getListEl();
68237
- const onScroll = () => {
68238
- updateCurrentScroll();
68239
- // Where the user is now is where things must be held from now on.
68240
- anchorRef.current = null;
68241
- if (scrolledByListRef.current) {
68242
- // The window stays where it is — the position it would be re-derived
68243
- // from was chosen to keep the rows still — but where the list is has
68244
- // genuinely changed, and whoever keeps that position must hear it.
68245
- scrolledByListRef.current = false;
68246
- reportPosition();
68247
- return;
68248
- }
68249
- reportPosition();
68250
- const total = virtual.totalSignal.peek();
68251
- if (total <= renderBudget) {
68252
- return;
68253
- }
68300
+ if (!scrollerEl || !listEl) {
68301
+ return;
68302
+ }
68303
+ const {
68304
+ start,
68305
+ end
68306
+ } = renderWindowRef.current;
68307
+ const virtualItemSize = virtualItemSizeSignal.peek();
68308
+ if (virtualItemSize === 0) {
68254
68309
  const scrollInfo = getScrollInfo({
68255
68310
  scrollValues: {
68256
68311
  left: scrollerEl.scrollLeft,
@@ -68267,17 +68322,8 @@ const useListScrollSync = ({
68267
68322
  return;
68268
68323
  }
68269
68324
  const {
68270
- index,
68271
- reason
68325
+ index
68272
68326
  } = scrollInfo;
68273
- // Recentering on every row crossed would rebuild the whole window a few
68274
- // times a second, and a window rebuilt is every row of it rendered
68275
- // again. It only moves once what is on screen comes near one of its
68276
- // edges — until then, it already holds what has to be drawn.
68277
- const {
68278
- start,
68279
- end
68280
- } = renderWindowRef.current;
68281
68327
  const margin = Math.floor(renderBudget / 4);
68282
68328
  const farFromStart = index - start >= margin || start === 0;
68283
68329
  const farFromEnd = end - index > margin || end === total;
@@ -68285,12 +68331,117 @@ const useListScrollSync = ({
68285
68331
  return;
68286
68332
  }
68287
68333
  const half = Math.floor(renderBudget / 2);
68288
- let newStart = Math.max(0, index - half);
68289
- let newEnd = Math.min(total, newStart + renderBudget);
68290
- if (newEnd === total) {
68291
- newStart = Math.max(0, total - renderBudget);
68334
+ let newStart = index - half < 0 ? 0 : index - half;
68335
+ let newEnd = newStart + renderBudget;
68336
+ if (newEnd > total) {
68337
+ newEnd = total;
68338
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
68339
+ }
68340
+ windowSlidRef.current = true;
68341
+ updateRenderWindow(newStart, newEnd, `${reason}: ${scrollInfo.reason}`);
68342
+ return;
68343
+ }
68344
+ const viewportRect = getScrollerViewportRect(scrollerEl);
68345
+ const listRect = listEl.getBoundingClientRect();
68346
+ const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
68347
+ const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
68348
+ const listFrom = horizontal ? listRect.left : listRect.top;
68349
+ let bandStart = Math.floor((viewportFrom - listFrom) / virtualItemSize);
68350
+ // Exclusive, like the window's end.
68351
+ let bandEnd = Math.ceil((viewportTo - listFrom) / virtualItemSize);
68352
+ if (bandStart < 0) {
68353
+ bandStart = 0;
68354
+ }
68355
+ if (bandEnd > total) {
68356
+ bandEnd = total;
68357
+ }
68358
+ if (bandEnd <= bandStart) {
68359
+ return;
68360
+ }
68361
+ const visibleCount = bandEnd - bandStart;
68362
+ const spare = renderBudget - visibleCount;
68363
+ if (!budgetWarnedRef.current && renderBudget === renderBudgetSteady && spare < 2) {
68364
+ budgetWarnedRef.current = true;
68365
+ console.warn(`List: renderBudget=${renderBudget} draws ${renderBudget} rows and this scroller shows ${visibleCount} at once: rows will go blank as it scrolls. Give it room for a screen ahead — ${visibleCount * 2} or more.`);
68366
+ }
68367
+ const behind = spare > 0 ? Math.floor(spare / 4) : 0;
68368
+ const ahead = spare > 0 ? spare - behind : 0;
68369
+ const halfScreen = Math.ceil(visibleCount / 2);
68370
+ const aheadNeeded = halfScreen < ahead / 2 ? halfScreen : Math.floor(ahead / 2);
68371
+ const behindNeeded = halfScreen < behind / 2 ? halfScreen : Math.floor(behind / 2);
68372
+ const forward = scrollDirectionRef.current > 0;
68373
+ const rowsAheadOfBand = forward ? end - bandEnd : bandStart - start;
68374
+ const rowsBehindBand = forward ? bandStart - start : end - bandEnd;
68375
+ const atEdgeAhead = forward ? end === total : start === 0;
68376
+ const atEdgeBehind = forward ? start === 0 : end === total;
68377
+ const coversBand = start <= bandStart && end >= bandEnd;
68378
+ const coversAhead = atEdgeAhead || rowsAheadOfBand >= aheadNeeded;
68379
+ const coversBehind = atEdgeBehind || rowsBehindBand >= behindNeeded;
68380
+ if (coversBand && coversAhead && coversBehind) {
68381
+ return;
68382
+ }
68383
+ let newStart = forward ? bandStart - behind : bandEnd + behind - renderBudget;
68384
+ if (newStart < 0) {
68385
+ newStart = 0;
68386
+ }
68387
+ let newEnd = newStart + renderBudget;
68388
+ if (newEnd > total) {
68389
+ newEnd = total;
68390
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
68391
+ }
68392
+ windowSlidRef.current = true;
68393
+ updateRenderWindow(newStart, newEnd, `${reason}: rows ${bandStart}-${bandEnd} on screen, going ${forward ? "forward" : "backward"}`);
68394
+ };
68395
+ // A slide is judged again once its rows are laid out: the scroll had moved
68396
+ // on while the rows were being drawn, or the anchoring of this very commit
68397
+ // moved it, and a window that stops short of the screen's edge with no
68398
+ // scroll event to come would stay short for good. On the next frame, after
68399
+ // the layout this commit's anchoring settles on.
68400
+ useLayoutEffect(() => {
68401
+ if (!windowSlidRef.current) {
68402
+ return undefined;
68403
+ }
68404
+ windowSlidRef.current = false;
68405
+ const frameId = requestAnimationFrame(() => {
68406
+ evaluateWindow("after slide");
68407
+ });
68408
+ return () => {
68409
+ cancelAnimationFrame(frameId);
68410
+ };
68411
+ }, [renderWindow]);
68412
+
68413
+ // Scroll listener — slides the window as the user scrolls.
68414
+ useLayoutEffect(() => {
68415
+ const listContainerEl = ref.current;
68416
+ if (!listContainerEl) {
68417
+ return undefined;
68418
+ }
68419
+ const scrollerEl = getScroller();
68420
+ const onScroll = () => {
68421
+ // Which way the user is going, read before the position is stored: what
68422
+ // the window keeps ahead is ahead of this.
68423
+ const previousScroll = currentScrollRef.current;
68424
+ if (previousScroll) {
68425
+ const delta = horizontal ? scrollerEl.scrollLeft - previousScroll.left : scrollerEl.scrollTop - previousScroll.top;
68426
+ if (delta > 0) {
68427
+ scrollDirectionRef.current = 1;
68428
+ } else if (delta < 0) {
68429
+ scrollDirectionRef.current = -1;
68430
+ }
68431
+ }
68432
+ updateCurrentScroll();
68433
+ // Where the user is now is where things must be held from now on.
68434
+ anchorRef.current = null;
68435
+ if (scrolledByListRef.current) {
68436
+ // The window stays where it is — the position it would be re-derived
68437
+ // from was chosen to keep the rows still — but where the list is has
68438
+ // genuinely changed, and whoever keeps that position must hear it.
68439
+ scrolledByListRef.current = false;
68440
+ reportPosition();
68441
+ return;
68292
68442
  }
68293
- updateRenderWindow(newStart, newEnd, reason);
68443
+ reportPosition();
68444
+ evaluateWindow("scroll");
68294
68445
  };
68295
68446
  // A page-level scroller does not emit "scroll" on the element itself
68296
68447
  // (document.scrollingElement); the document does.
@@ -71282,8 +71433,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
71282
71433
  * window, which slides as the user scrolls while fillers hold the room of
71283
71434
  * the rows outside it. Rows declared one by one as `<List.Item>` children
71284
71435
  * are all drawn, whatever this says — a list with more than a few dozen rows
71285
- * gives them to a run (see docs/scroll.md, "Many rows"). Below 30 the list
71286
- * warns: a window shorter than a tall screen shows blank fillers.
71436
+ * gives them to a run (see docs/scroll.md, "Many rows"). The window keeps
71437
+ * most of its spare rows ahead of the scroll, and moves once those fall
71438
+ * under half a screen; it has to be larger than what the scroller shows at
71439
+ * once, with room for that lookahead — the list warns below 30, and when a
71440
+ * budget leaves fewer than two rows beyond the screen.
71287
71441
  *
71288
71442
  * `{ initial, after }` for a list drawn in the click that opens it (a popup):
71289
71443
  * `initial` rows in the commit the browser paints first — about what a phone
@@ -71405,14 +71559,10 @@ const generateTimeSlots = (min, max, stepSeconds) => {
71405
71559
  const PickerPresetResolver = props => {
71406
71560
  const Next = useNextResolver();
71407
71561
  if (props.type === "navi_time") {
71408
- return jsx(PickerNaviTime, {
71409
- ...props
71410
- });
71562
+ return renderResolver(PickerNaviTime, props);
71411
71563
  }
71412
71564
  if (props.type === "navi_minute") {
71413
- return jsx(PickerNaviMinute, {
71414
- ...props
71415
- });
71565
+ return renderResolver(PickerNaviMinute, props);
71416
71566
  }
71417
71567
  return jsx(Next, {
71418
71568
  ...props
@@ -72210,59 +72360,37 @@ const PencilSvg = () => {
72210
72360
  const PickerTypeResolver = props => {
72211
72361
  const Next = useNextResolver();
72212
72362
  if (props.type === "color") {
72213
- return jsx(PickerColor, {
72214
- ...props
72215
- });
72363
+ return renderResolver(PickerColor, props);
72216
72364
  }
72217
72365
  if (props.type === "datetime") {
72218
- return jsx(PickerDatetime, {
72219
- ...props
72220
- });
72366
+ return renderResolver(PickerDatetime, props);
72221
72367
  }
72222
72368
  if (props.type === "date") {
72223
- return jsx(PickerDate, {
72224
- ...props
72225
- });
72369
+ return renderResolver(PickerDate, props);
72226
72370
  }
72227
72371
  if (props.type === "month") {
72228
- return jsx(PickerMonth, {
72229
- ...props
72230
- });
72372
+ return renderResolver(PickerMonth, props);
72231
72373
  }
72232
72374
  if (props.type === "week") {
72233
- return jsx(PickerWeek, {
72234
- ...props
72235
- });
72375
+ return renderResolver(PickerWeek, props);
72236
72376
  }
72237
72377
  if (props.type === "time") {
72238
- return jsx(PickerTime, {
72239
- ...props
72240
- });
72378
+ return renderResolver(PickerTime, props);
72241
72379
  }
72242
72380
  if (props.type === "duration") {
72243
- return jsx(PickerDuration, {
72244
- ...props
72245
- });
72381
+ return renderResolver(PickerDuration, props);
72246
72382
  }
72247
72383
  if (props.type === "file") {
72248
- return jsx(PickerFile, {
72249
- ...props
72250
- });
72384
+ return renderResolver(PickerFile, props);
72251
72385
  }
72252
72386
  if (props.type === "text") {
72253
- return jsx(PickerText, {
72254
- ...props
72255
- });
72387
+ return renderResolver(PickerText, props);
72256
72388
  }
72257
72389
  if (props.type === "array") {
72258
- return jsx(PickerArray, {
72259
- ...props
72260
- });
72390
+ return renderResolver(PickerArray, props);
72261
72391
  }
72262
72392
  if (props.type === "object") {
72263
- return jsx(PickerObject, {
72264
- ...props
72265
- });
72393
+ return renderResolver(PickerObject, props);
72266
72394
  }
72267
72395
  return jsx(Next, {
72268
72396
  ...props
@@ -85896,7 +86024,7 @@ const css = /* css */`.navi_side_panel {
85896
86024
  * @param {object} props
85897
86025
  * @param {boolean} [props.open] - Controlled open state, forwarded as-is to
85898
86026
  * `Popup`'s own `open`.
85899
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
86027
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
85900
86028
  * state said the way every navi control says it: the panel opens and closes
85901
86029
  * to match the signal, and writes into it whenever it opens or closes on its
85902
86030
  * own (Escape, swipe, a --navi-close command) — one binding to both drive
@@ -85970,7 +86098,9 @@ const css = /* css */`.navi_side_panel {
85970
86098
  * closes the panel on an outside click instead, and also enables trapping
85971
86099
  * Tab navigation inside the panel (`focusCapture`) — closing on outside
85972
86100
  * interaction only makes sense paired with not letting focus silently
85973
- * leave the panel first.
86101
+ * leave the panel first. A box of the page whose press must not close the
86102
+ * panel (a card that fills it) names the panel:
86103
+ * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
85974
86104
  * @param {boolean} [props.swipeToClose=true] - Pushing the panel back
85975
86105
  * towards the edge it is docked to closes it: the panel follows the
85976
86106
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -86105,6 +86235,7 @@ SidePanel.Foot = SidePanelFoot;
86105
86235
 
86106
86236
  const createSlot = (SlotRenderer = Box) => {
86107
86237
  const slotPropsSignal = signal();
86238
+ let filler = null;
86108
86239
  const Slot = () => {
86109
86240
  const props = slotPropsSignal.value;
86110
86241
  return jsx(SlotRenderer, {
@@ -86113,9 +86244,15 @@ const createSlot = (SlotRenderer = Box) => {
86113
86244
  });
86114
86245
  };
86115
86246
  const SlotFill = props => {
86247
+ const fillerRef = useRef();
86248
+ filler = fillerRef;
86116
86249
  slotPropsSignal.value = props;
86117
86250
  useLayoutEffect(() => {
86118
86251
  return () => {
86252
+ if (filler !== fillerRef) {
86253
+ return;
86254
+ }
86255
+ filler = null;
86119
86256
  slotPropsSignal.value = null;
86120
86257
  };
86121
86258
  }, []);