@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, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
10
- 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, 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";
@@ -31648,8 +31648,26 @@ const resolveExplicitTarget = (element) => {
31648
31648
  }
31649
31649
  return undefined;
31650
31650
  };
31651
+ // The control around the source that a command can be about: one holding a
31652
+ // value of its own. A popup is a control root too (it answers interactions,
31653
+ // carries a callout), but it has no host of its own — asked for one, it
31654
+ // answers with the first control INSIDE it, which for a field sending
31655
+ // --navi-update from a picker's popup is that very field: the command then
31656
+ // sets the field to what it already holds, which fires the command again,
31657
+ // without end. Such a surface is walked past, up to the picker or the form.
31651
31658
  const resolveFirstParentControl = (el) => {
31652
- return getParentControl(el);
31659
+ let parentControl = getParentControl(el);
31660
+ while (parentControl && !ownsControlHost(parentControl)) {
31661
+ parentControl = getParentControl(parentControl);
31662
+ }
31663
+ return parentControl;
31664
+ };
31665
+ const ownsControlHost = (controlRoot) => {
31666
+ const controlHost = findControlHost(controlRoot);
31667
+ return (
31668
+ Boolean(controlHost) &&
31669
+ controlHost.closest("[navi-control]") === controlRoot
31670
+ );
31653
31671
  };
31654
31672
  const resolveFirstChildControl = (el) => {
31655
31673
  let startEl;
@@ -33840,19 +33858,27 @@ const writeInSignal = (signal, value, { history }) => {
33840
33858
  // is open (`?seat=<gameId>` over a list of cards), and closed is the signal
33841
33859
  // holding none of their values — `undefined`, which a state signal reads as
33842
33860
  // its default.
33861
+ //
33862
+ // A popup without a `value` reads anything the signal holds — save `false`,
33863
+ // `null` and `undefined` — as open: a single sheet bound to `?open=<id>` is
33864
+ // open on whichever id the address names, and the id is the popup's content
33865
+ // to read, not its to write. Opening writes `true` only into a signal that
33866
+ // reads closed; closing writes `false` where the signal held `true`, so a
33867
+ // boolean signal keeps its boolean, and `undefined` for anything else.
33843
33868
  const writeOpenedInSignal = (signal, opened, event, popupValue) => {
33844
- if (readOpened(signal.peek(), popupValue) === opened) {
33869
+ const signalValue = signal.peek();
33870
+ if (readOpened(signalValue, popupValue) === opened) {
33845
33871
  // The signal already says so, meaning this open/close IS what it asked
33846
33872
  // for: a back press that took the popup out of the url, the application
33847
33873
  // writing it. Nothing to write back — and nothing to go back to either,
33848
33874
  // since the navigation navBack would undo is the one that asked for this.
33849
33875
  return;
33850
33876
  }
33851
- const closedValue = popupValue === undefined ? false : undefined;
33852
33877
  if (opened) {
33853
33878
  signal.value = popupValue === undefined ? true : popupValue;
33854
33879
  return;
33855
33880
  }
33881
+ const closedValue = readClosedValue(signalValue);
33856
33882
  if (
33857
33883
  signal.options?.getHistory?.() === "push" &&
33858
33884
  // Nothing of this document behind: the popup was opened by the url itself
@@ -33873,10 +33899,15 @@ const writeOpenedInSignal = (signal, opened, event, popupValue) => {
33873
33899
  };
33874
33900
  const readOpened = (signalValue, popupValue) => {
33875
33901
  if (popupValue === undefined) {
33876
- return signalValue;
33902
+ return (
33903
+ signalValue !== undefined && signalValue !== null && signalValue !== false
33904
+ );
33877
33905
  }
33878
33906
  return signalValue === popupValue;
33879
33907
  };
33908
+ const readClosedValue = (openSignalValue) => {
33909
+ return openSignalValue === true ? false : undefined;
33910
+ };
33880
33911
 
33881
33912
  /**
33882
33913
  * Keeps an open controller in sync with where the caller says the popup should
@@ -33918,6 +33949,13 @@ const useOpenPropsEffectOnOpenController = (
33918
33949
  : signal
33919
33950
  ? readOpened(signal.value, value)
33920
33951
  : props.open;
33952
+ // What the signal holds while it reads open — `true`, the popup's `value`,
33953
+ // or the id a single sheet is open on. Put back when a close is refused,
33954
+ // since by then the signal has been written closed.
33955
+ const openSignalValueRef = useRef(undefined);
33956
+ if (signal && open) {
33957
+ openSignalValueRef.current = signal.value;
33958
+ }
33921
33959
  // Assigned on every render, like openEffect, so it always closes over the
33922
33960
  // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
33923
33961
  // --navi-close command) writes what happened where the caller keeps it, so
@@ -33998,10 +34036,11 @@ const useOpenPropsEffectOnOpenController = (
33998
34036
  // request already reads in the signal, and with a `value` what the
33999
34037
  // signal holds may be another popup's, which the write would erase.
34000
34038
  if (signal && openController.opened !== open) {
34001
- const opened = openController.opened;
34002
34039
  writeInSignal(
34003
34040
  signal,
34004
- value === undefined ? opened : opened ? value : undefined,
34041
+ openController.opened
34042
+ ? openSignalValueRef.current
34043
+ : readClosedValue(signal.peek()),
34005
34044
  { history: "replace" },
34006
34045
  );
34007
34046
  }
@@ -34379,6 +34418,21 @@ const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
34379
34418
  */
34380
34419
  const OUTSIDE_REGION_ATTRIBUTE = "data-navi-popup-outside";
34381
34420
 
34421
+ /**
34422
+ * `data-navi-popup-inside` is the other direction: a box of the PAGE the
34423
+ * caller says belongs to a popup, so a press on it is not outside that popup
34424
+ * even though it lies beyond the border box — a card whose press is what
34425
+ * fills the one panel of a board. Its value names the popup by `id`, the way
34426
+ * `commandfor` does (several ids, space separated), because the exemption is
34427
+ * one popup's: a menu open beside the board still closes on the same press.
34428
+ *
34429
+ * Unlike the outside marker it answers for its whole subtree: the outside
34430
+ * marker carves free space out of a surface whose controls stay surface, this
34431
+ * one adds a whole thing — the card, its count, its buttons — to the popup's
34432
+ * own ground, and nothing in it is a dismissal.
34433
+ */
34434
+ const INSIDE_ATTRIBUTE = "data-navi-popup-inside";
34435
+
34382
34436
  /**
34383
34437
  * What a press landing on a region the caller declared as not-its-surface
34384
34438
  * does: exactly what the same press on the backdrop would do.
@@ -34503,6 +34557,14 @@ const armOutsidePressClose = (
34503
34557
  ) {
34504
34558
  return;
34505
34559
  }
34560
+ if (
34561
+ popupEl.id &&
34562
+ pointerDownEvent.target.closest?.(
34563
+ `[${INSIDE_ATTRIBUTE}~="${CSS.escape(popupEl.id)}"]`,
34564
+ )
34565
+ ) {
34566
+ return;
34567
+ }
34506
34568
  // Real DOM containment wins over the rectangle below — an element
34507
34569
  // genuinely inside the popup (`overflow: visible`, a negative margin, an
34508
34570
  // absolutely-positioned child) can be painted outside its border box, and
@@ -36486,6 +36548,46 @@ const acknowledgeOwnAction = (controller) => {
36486
36548
  controller.acknowledgeUIState();
36487
36549
  };
36488
36550
 
36551
+ // What a group of checkboxes or radios is worth is a claim about values, and
36552
+ // a value whose box is not there cannot be contradicted by that box: a row
36553
+ // scrolled out of a virtualized list, a filtered-out option, a whole page being
36554
+ // parked while a route action reruns — its children leave one at a time, and
36555
+ // each partial reading would otherwise be taken for the answer and written
36556
+ // into whatever the group is bound to (an array signal emptied down to `[]`
36557
+ // by its own unmount). `kept` is what the group holds as it is asked; a value
36558
+ // in it stays until a DRAWN child carrying that value says otherwise.
36559
+ //
36560
+ // Read off the group rather than remembered here on purpose: a value put ON
36561
+ // the group (a `value` prop, a signal, a reopened popup) replaces the whole
36562
+ // selection, and a private memory would go on holding the rows it could not
36563
+ // see being unselected.
36564
+ const keptAndCheckedChildUIStates = (children, fallbackState, kept) => {
36565
+ const drawnValues = new Set(children.map((child) => child.props.value));
36566
+ const values = Array.isArray(kept)
36567
+ ? kept.filter((value) => !drawnValues.has(value))
36568
+ : [];
36569
+ for (const child of children) {
36570
+ if (child.uiState !== undefined) {
36571
+ values.push(child.uiState);
36572
+ }
36573
+ }
36574
+ return values.length === 0 ? undefined : values;
36575
+ };
36576
+ const keptOrFirstDefinedChildUIState = (children, fallbackState, kept) => {
36577
+ for (const child of children) {
36578
+ if (child.uiState !== undefined) {
36579
+ return child.uiState;
36580
+ }
36581
+ }
36582
+ // No drawn child claims it. If the child that held it IS drawn, it was really
36583
+ // deselected; if it is not, the group keeps what it holds.
36584
+ const keptIsDrawn = children.some((child) => child.props.value === kept);
36585
+ if (keptIsDrawn) {
36586
+ return undefined;
36587
+ }
36588
+ return kept;
36589
+ };
36590
+
36489
36591
  // Default aggregate/distribute implementations keyed by controlType or stateType.
36490
36592
  // Looked up in useUIGroupStateController to fill in omitted aggregateChildStates /
36491
36593
  // distributeChildUIState. If neither a default nor an explicit impl is found for a
@@ -36494,7 +36596,7 @@ const GROUP_DEFAULTS = {
36494
36596
  radio_group: {
36495
36597
  childControlFilter: (child) =>
36496
36598
  child.controlType === "input" && child.controlHostProps?.type === "radio",
36497
- aggregateChildStates: firstDefinedChildUIState,
36599
+ aggregateChildStates: keptOrFirstDefinedChildUIState,
36498
36600
  distributeChildUIState: (newUIState, childUIStateController) => {
36499
36601
  const childSelected = childUIStateController.props.value === newUIState;
36500
36602
  if (childSelected) {
@@ -36507,16 +36609,7 @@ const GROUP_DEFAULTS = {
36507
36609
  childControlFilter: (child) =>
36508
36610
  child.controlType === "input" &&
36509
36611
  child.controlHostProps?.type === "checkbox",
36510
- aggregateChildStates: (children) => {
36511
- const values = [];
36512
- for (const child of children) {
36513
- const childUIState = child.uiState;
36514
- if (childUIState !== undefined) {
36515
- values.push(childUIState);
36516
- }
36517
- }
36518
- return values.length === 0 ? undefined : values;
36519
- },
36612
+ aggregateChildStates: keptAndCheckedChildUIStates,
36520
36613
  distributeChildUIState: (newUIState, childUIStateController) => {
36521
36614
  const childSelected =
36522
36615
  Array.isArray(newUIState) &&
@@ -47740,33 +47833,6 @@ const createToggleEvent = open => {
47740
47833
  return toggleEvent;
47741
47834
  };
47742
47835
 
47743
- const NextResolverContext = createContext(null);
47744
- const useNextResolver = () => useContext(NextResolverContext);
47745
-
47746
- /**
47747
- * Creates a renderComponent function that passes props through a chain of resolvers.
47748
- * Each resolver is a Preact component rendered in sequence (hooks are allowed).
47749
- * To pass through to the next resolver, call useNextResolver() and render the
47750
- * returned Next component with the desired props.
47751
- * To terminate the chain early (e.g. render a specialized component), render
47752
- * directly without calling Next.
47753
- *
47754
- * The last entry in the array is the final/target component — it receives null
47755
- * from useNextResolver() indicating it is terminal.
47756
- *
47757
- * Usage:
47758
- * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
47759
- * // Then inside a component render:
47760
- * renderButton(props)
47761
- *
47762
- * Each position of the chain has a runner of its own, defined once: it renders
47763
- * its resolver under a NextResolverContext holding the runner after it. A
47764
- * resolver that re-renders on its own and renders <Next> therefore resumes at
47765
- * its own position without any bookkeeping — the Next it was given is the one
47766
- * made for it. A chain rendered by the hundred (a list's rows) pays two
47767
- * components per position, the runner and the resolver, so nothing here is a
47768
- * component that merely forwards.
47769
- */
47770
47836
  const createComponentResolver = (resolvers, {
47771
47837
  pure
47772
47838
  } = {}) => {
@@ -47775,13 +47841,10 @@ const createComponentResolver = (resolvers, {
47775
47841
  for (let index = 0; index < resolvers.length; index++) {
47776
47842
  const Resolver = resolvers[index];
47777
47843
  const isLast = index === lastIndex;
47778
- const Runner = props => jsx(NextResolverContext.Provider, {
47779
- value: isLast ? null : runners[index + 1],
47780
- children: jsx(Resolver, {
47781
- ...props
47782
- })
47783
- });
47784
- Runner.displayName = `${Resolver.displayName || Resolver.name}Runner`;
47844
+ function Runner(props) {
47845
+ return runResolverBody(this, Resolver, props, isLast ? null : runners[index + 1]);
47846
+ }
47847
+ Runner.displayName = Resolver.displayName || Resolver.name;
47785
47848
  runners.push(Runner);
47786
47849
  }
47787
47850
  const FirstRunner = runners[0];
@@ -47812,6 +47875,54 @@ const createComponentResolver = (resolvers, {
47812
47875
  }
47813
47876
  return PureRenderComponent;
47814
47877
  };
47878
+ const useNextResolver = () => {
47879
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
47880
+ 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.");
47881
+ }
47882
+ return currentNext;
47883
+ };
47884
+ const renderResolver = (Resolver, props) => {
47885
+ if (currentNext === OUTSIDE_RESOLVER_BODY) {
47886
+ throw new Error(`renderResolver(${Resolver.displayName || Resolver.name}) called outside a resolver body: only a resolver can render another one.`);
47887
+ }
47888
+ let SubRunner = subRunnerByResolver.get(Resolver);
47889
+ if (!SubRunner) {
47890
+ SubRunner = function (props) {
47891
+ const {
47892
+ [NEXT_PROP]: next,
47893
+ ...ownProps
47894
+ } = props;
47895
+ return runResolverBody(this, Resolver, ownProps, next);
47896
+ };
47897
+ SubRunner.displayName = Resolver.displayName || Resolver.name;
47898
+ subRunnerByResolver.set(Resolver, SubRunner);
47899
+ }
47900
+ return createElement(SubRunner, {
47901
+ ...props,
47902
+ [NEXT_PROP]: currentNext
47903
+ });
47904
+ };
47905
+
47906
+ // What useNextResolver() answers: the runner after the one whose body is
47907
+ // running, null for the last — and, outside any body, a value that is neither.
47908
+ const OUTSIDE_RESOLVER_BODY = Symbol("outside_resolver_body");
47909
+ let currentNext = OUTSIDE_RESOLVER_BODY;
47910
+ const runResolverBody = (instance, Resolver, props, next) => {
47911
+ const nextBefore = currentNext;
47912
+ currentNext = next;
47913
+ try {
47914
+ return Resolver.call(instance, props);
47915
+ } finally {
47916
+ currentNext = nextBefore;
47917
+ }
47918
+ };
47919
+
47920
+ // One runner per resolver rendered through renderResolver, made on first use:
47921
+ // the same component type on every render, so a resolver picked again is
47922
+ // updated in place and another one picked is a remount, like any child.
47923
+ const subRunnerByResolver = new WeakMap();
47924
+ // Rides on the props of a sub-runner, stripped before the body sees them.
47925
+ const NEXT_PROP = "navi-resolver-next";
47815
47926
  function pureShouldComponentUpdate(nextProps) {
47816
47927
  return shallowDiffers(this.props, nextProps);
47817
47928
  }
@@ -48637,9 +48748,7 @@ const ButtonConfirm = ({
48637
48748
  const ButtonRouteResolver = props => {
48638
48749
  const Next = useNextResolver();
48639
48750
  if (props.route) {
48640
- return jsx(ButtonWithRoute, {
48641
- ...props
48642
- });
48751
+ return renderResolver(ButtonWithRoute, props);
48643
48752
  }
48644
48753
  return jsx(Next, {
48645
48754
  ...props
@@ -51237,9 +51346,7 @@ const installInputCss = () => {
51237
51346
  const InputModeResolver = props => {
51238
51347
  const Next = useNextResolver();
51239
51348
  if (props.inputMode === "numeric" || props.inputMode === "decimal") {
51240
- return jsx(InputModeNumericOrDecimal, {
51241
- ...props
51242
- });
51349
+ return renderResolver(InputModeNumericOrDecimal, props);
51243
51350
  }
51244
51351
  return jsx(Next, {
51245
51352
  ...props
@@ -51467,39 +51574,25 @@ const InputTypeResolver = props => {
51467
51574
  // very same "icon while empty, clear button once filled" affordance without
51468
51575
  // pretending to be a search box.
51469
51576
  if (props.clearable && props.type !== "search") {
51470
- return jsx(InputClearable, {
51471
- ...props
51472
- });
51577
+ return renderResolver(InputClearable, props);
51473
51578
  }
51474
51579
  if (props.type === "search") {
51475
- return jsx(InputSearch, {
51476
- ...props
51477
- });
51580
+ return renderResolver(InputSearch, props);
51478
51581
  }
51479
51582
  if (props.type === "email") {
51480
- return jsx(InputEmail, {
51481
- ...props
51482
- });
51583
+ return renderResolver(InputEmail, props);
51483
51584
  }
51484
51585
  if (props.type === "tel") {
51485
- return jsx(InputTel, {
51486
- ...props
51487
- });
51586
+ return renderResolver(InputTel, props);
51488
51587
  }
51489
51588
  if (props.type === "number") {
51490
- return jsx(InputNumber, {
51491
- ...props
51492
- });
51589
+ return renderResolver(InputNumber, props);
51493
51590
  }
51494
51591
  if (props.type === "color") {
51495
- return jsx(InputColor, {
51496
- ...props
51497
- });
51592
+ return renderResolver(InputColor, props);
51498
51593
  }
51499
51594
  if (props.type === "datetime-local") {
51500
- return jsx(InputDatetimeLocal, {
51501
- ...props
51502
- });
51595
+ return renderResolver(InputDatetimeLocal, props);
51503
51596
  }
51504
51597
  return jsx(Next, {
51505
51598
  ...props
@@ -51612,9 +51705,7 @@ const InputDatetimeLocal = props => {
51612
51705
  const InputWithListResolver = props => {
51613
51706
  const Next = useNextResolver();
51614
51707
  if (props["navi-list"]) {
51615
- return jsx(InputWithList, {
51616
- ...props
51617
- });
51708
+ return renderResolver(InputWithList, props);
51618
51709
  }
51619
51710
  return jsx(Next, {
51620
51711
  ...props
@@ -51721,9 +51812,7 @@ const ChevronDownSvg$1 = () => {
51721
51812
  const InputWithSuggestionsResolver = props => {
51722
51813
  const Next = useNextResolver();
51723
51814
  if (props["navi-suggestions"]) {
51724
- return jsx(InputTextualWithSuggestions, {
51725
- ...props
51726
- });
51815
+ return renderResolver(InputTextualWithSuggestions, props);
51727
51816
  }
51728
51817
  return jsx(Next, {
51729
51818
  ...props
@@ -59418,14 +59507,15 @@ const css$E = /* css */`
59418
59507
  * the focus only leaves it for something that asked by name (`autoFocus` on
59419
59508
  * that element, which outranks whatever the dialog says).
59420
59509
  * @param {boolean} [props.open] - Controlled open state.
59421
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
59510
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
59422
59511
  * state said the way every navi control says it: the dialog opens and closes
59423
59512
  * to match the signal, and writes into it whenever it opens or closes on its
59424
59513
  * own (Escape, backdrop, a --navi-close command) — one binding to both drive
59425
59514
  * the dialog and know where it is, and the state stays where the app put it.
59426
59515
  * Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
59427
59516
  * mount behaves like `defaultOpen`: the dialog was already open, no entrance
59428
- * plays.
59517
+ * plays. A signal holding an id (one dialog showing whichever card
59518
+ * the address names) reads as open, and the dialog leaves the id alone.
59429
59519
  * @param {any} [props.value] - What `signal` holds while THIS dialog is
59430
59520
  * open, for several of them sharing one signal that says which is open
59431
59521
  * (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
@@ -61033,14 +61123,15 @@ const css$D = /* css */`
61033
61123
  * the focus only leaves it for something that asked by name (`autoFocus` on
61034
61124
  * that element, which outranks whatever the popover says).
61035
61125
  * @param {boolean} [props.open] - Controlled open state.
61036
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
61126
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
61037
61127
  * state said the way every navi control says it: the popover opens and closes
61038
61128
  * to match the signal, and writes into it whenever it opens or closes on its
61039
61129
  * own (Escape, light dismiss, a --navi-close command) — one binding to both
61040
61130
  * drive the popover and know where it is, and the state stays where the app
61041
61131
  * put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
61042
61132
  * `true` at mount behaves like `defaultOpen`: the popover was already open,
61043
- * no entrance plays.
61133
+ * no entrance plays. A signal holding an id (one popover showing whichever card
61134
+ * the address names) reads as open, and the popover leaves the id alone.
61044
61135
  * @param {any} [props.value] - What `signal` holds while THIS popover is
61045
61136
  * open, for several of them sharing one signal that says which is open
61046
61137
  * (`?seat=<gameId>` over a list of cards): open while `signal.value` is this
@@ -62473,9 +62564,7 @@ const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
62473
62564
  const PickerCustomResolver = props => {
62474
62565
  import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
62475
62566
  if (props.children === undefined) {
62476
- return jsx(PickerNative, {
62477
- ...props
62478
- });
62567
+ return renderResolver(PickerNative, props);
62479
62568
  }
62480
62569
  if (props.mode === "callout") {
62481
62570
  // A tooltip is an icon one presses, unless told otherwise. Own-property
@@ -62537,14 +62626,12 @@ const PickerCustomResolver = props => {
62537
62626
  // how a field says its value is a JS one, kept beside the DOM (see
62538
62627
  // controller_registry.js) — the same thing type="array"/"object" already
62539
62628
  // say for their shapes.
62540
- return jsx(PickerCustom, {
62629
+ return renderResolver(PickerCustom, {
62541
62630
  ...props,
62542
62631
  type: "navi_js"
62543
62632
  });
62544
62633
  }
62545
- return jsx(PickerCustom, {
62546
- ...props
62547
- });
62634
+ return renderResolver(PickerCustom, props);
62548
62635
  };
62549
62636
  const PickerNative = props => {
62550
62637
  const Next = useNextResolver();
@@ -63156,9 +63243,9 @@ const PickerCustom = props => {
63156
63243
  });
63157
63244
  }
63158
63245
  }
63159
- return jsx(PickerContentInsidePopup, {
63246
+ return renderResolver(PickerContentInsidePopup, {
63160
63247
  ...pickerProps,
63161
- mode: mode
63248
+ mode
63162
63249
  });
63163
63250
  };
63164
63251
 
@@ -64425,14 +64512,10 @@ const createItemTracker = (onChange) => {
64425
64512
  const ListItemHeaderOrFooterResolver = props => {
64426
64513
  const Next = useNextResolver();
64427
64514
  if (props.header) {
64428
- return jsx(ListItemHeader, {
64429
- ...props
64430
- });
64515
+ return renderResolver(ListItemHeader, props);
64431
64516
  }
64432
64517
  if (props.footer) {
64433
- return jsx(ListItemFooter, {
64434
- ...props
64435
- });
64518
+ return renderResolver(ListItemFooter, props);
64436
64519
  }
64437
64520
  return jsx(Next, {
64438
64521
  ...props
@@ -64664,9 +64747,7 @@ const ListSelectableContext = createContext(false);
64664
64747
  const ListSelectableResolver = props => {
64665
64748
  const Next = useNextResolver();
64666
64749
  if (props.selectable) {
64667
- return jsx(ListSelectable, {
64668
- ...props
64669
- });
64750
+ return renderResolver(ListSelectable, props);
64670
64751
  }
64671
64752
  return jsx(ListSelectableContext.Provider, {
64672
64753
  value: false,
@@ -64688,45 +64769,6 @@ const ListSelectable = props => {
64688
64769
  focusGroupDirection,
64689
64770
  focusGroupWrap
64690
64771
  } = props;
64691
- // `kept` is what the list holds as it is asked, which is not the same as what
64692
- // its rows say: a list draws the rows it needs and no more, so the selected
64693
- // one may be scrolled out of the window or filtered out of the view, and a
64694
- // row that is not there cannot say it is not selected. Reading it off the
64695
- // group rather than remembering it here is what makes a value put ON the list
64696
- // (a `value` prop, a signal, a reopened popup) replace the whole selection —
64697
- // a private memory of its own would go on holding the rows it could not see
64698
- // being unselected.
64699
- //
64700
- // `fallbackState` is the empty of the shape the list declared below
64701
- // (`stateType`): `[]` for a multiple list, nothing for a single one. Taking
64702
- // it is what lets an emptied list say "empty" — `undefined` is the word for
64703
- // "unset", and a bound stateSignal reads that as "nothing decided here, go
64704
- // back to the default" (see docs/control_value.md), which is how a list
64705
- // emptied down to its last row puts that row back on reload.
64706
- const aggregateChildStates = (children, fallbackState, kept) => {
64707
- if (multiple) {
64708
- const drawnValues = new Set(children.map(child => child.props.value));
64709
- const stillSelected = Array.isArray(kept) ? kept.filter(value => !drawnValues.has(value)) : [];
64710
- for (const child of children) {
64711
- if (child.uiState !== undefined) {
64712
- stillSelected.push(child.uiState);
64713
- }
64714
- }
64715
- return stillSelected.length === 0 ? fallbackState : stillSelected;
64716
- }
64717
- for (const child of children) {
64718
- if (child.uiState !== undefined) {
64719
- return child.uiState;
64720
- }
64721
- }
64722
- // No drawn row claims it. If the row that held it IS drawn, it was really
64723
- // deselected; if it is not, the list keeps what it holds.
64724
- const keptIsDrawn = children.some(child => child.props.value === kept);
64725
- if (keptIsDrawn) {
64726
- return undefined;
64727
- }
64728
- return kept;
64729
- };
64730
64772
  const [listControlRootProps, listControlProps, childrenWrapperProps] = useControlgroupProps({
64731
64773
  // The defaults of the group this IS (see CheckboxGroup / RadioGroup):
64732
64774
  // what a selectable list shows is a claim about what was accepted, so
@@ -64738,9 +64780,15 @@ const ListSelectable = props => {
64738
64780
  resetOnError: true,
64739
64781
  ...props
64740
64782
  }, {
64783
+ // A list draws the rows it needs and no more, so a selected row may be
64784
+ // scrolled out of the window or filtered out of the view: the default
64785
+ // aggregate of these two groups keeps a value whose row is not drawn
64786
+ // (see GROUP_DEFAULTS in ui_state_controller.js). `stateType` is what
64787
+ // an emptied multiple list says — `[]`, not `undefined`, which a bound
64788
+ // stateSignal reads as "nothing decided here, go back to the default"
64789
+ // (see docs/control_value.md).
64741
64790
  stateType: multiple ? "array" : "",
64742
- controlType: multiple ? "checkbox_group" : "radio_group",
64743
- aggregateChildStates
64791
+ controlType: multiple ? "checkbox_group" : "radio_group"
64744
64792
  });
64745
64793
  const uiGroupStateController = getUIStateControllerById(listControlProps.id);
64746
64794
  useFocusGroup(ref, {
@@ -64999,7 +65047,7 @@ const ListItemSelectableResolver = props => {
64999
65047
  const isHeaderOrFooter = Boolean(props.header || props.footer);
65000
65048
  const selectable = props.selectable ?? (isHeaderOrFooter || props.role === "presentation" ? false : listSelectable);
65001
65049
  if (selectable) {
65002
- return jsx(ListItemSelectable, {
65050
+ return renderResolver(ListItemSelectable, {
65003
65051
  ...props,
65004
65052
  selectable: true
65005
65053
  });
@@ -65942,6 +65990,7 @@ const ListUI = props => {
65942
65990
  ref,
65943
65991
  tracker,
65944
65992
  renderBudget,
65993
+ renderBudgetSteady: renderBudgetAfterPaint,
65945
65994
  virtualItemSize,
65946
65995
  virtual,
65947
65996
  scrolled,
@@ -66269,6 +66318,7 @@ const useListScrollSync = ({
66269
66318
  ref,
66270
66319
  tracker,
66271
66320
  renderBudget,
66321
+ renderBudgetSteady,
66272
66322
  virtualItemSize,
66273
66323
  virtual,
66274
66324
  scrolled,
@@ -67019,31 +67069,36 @@ const useListScrollSync = ({
67019
67069
  }
67020
67070
  });
67021
67071
 
67022
- // Scroll listener — slides the window as the user scrolls.
67023
- useLayoutEffect(() => {
67024
- const listContainerEl = ref.current;
67025
- if (!listContainerEl) {
67026
- return undefined;
67072
+ // The window the visible band asks for, decided from geometry: the band's
67073
+ // two edges in row units, from where the scroller's viewport cuts the list
67074
+ // and the row size. What has to be drawn is everything between the edges,
67075
+ // plus what the scroll is about to bring — so the window is judged on the
67076
+ // rows it keeps AHEAD of the band, in the direction the user goes, and moves
67077
+ // once those fall under half a screen. Re-framed, it puts three quarters of
67078
+ // its spare rows ahead and one quarter behind: a window centred on the band
67079
+ // is due to move again as soon as the band has crossed the few rows it kept
67080
+ // ahead, and each move is rows drawn while the user waits for them.
67081
+ // Without a row size there is no geometry to reason on; the row under a
67082
+ // probe stands in for the band, framed the old way.
67083
+ const scrollDirectionRef = useRef(1);
67084
+ const windowSlidRef = useRef(false);
67085
+ useRef(false);
67086
+ const evaluateWindow = reason => {
67087
+ const total = virtual.totalSignal.peek();
67088
+ if (total <= renderBudget) {
67089
+ return;
67027
67090
  }
67028
67091
  const scrollerEl = getScroller();
67029
67092
  const listEl = getListEl();
67030
- const onScroll = () => {
67031
- updateCurrentScroll();
67032
- // Where the user is now is where things must be held from now on.
67033
- anchorRef.current = null;
67034
- if (scrolledByListRef.current) {
67035
- // The window stays where it is — the position it would be re-derived
67036
- // from was chosen to keep the rows still — but where the list is has
67037
- // genuinely changed, and whoever keeps that position must hear it.
67038
- scrolledByListRef.current = false;
67039
- reportPosition();
67040
- return;
67041
- }
67042
- reportPosition();
67043
- const total = virtual.totalSignal.peek();
67044
- if (total <= renderBudget) {
67045
- return;
67046
- }
67093
+ if (!scrollerEl || !listEl) {
67094
+ return;
67095
+ }
67096
+ const {
67097
+ start,
67098
+ end
67099
+ } = renderWindowRef.current;
67100
+ const virtualItemSize = virtualItemSizeSignal.peek();
67101
+ if (virtualItemSize === 0) {
67047
67102
  const scrollInfo = getScrollInfo({
67048
67103
  scrollValues: {
67049
67104
  left: scrollerEl.scrollLeft,
@@ -67060,17 +67115,8 @@ const useListScrollSync = ({
67060
67115
  return;
67061
67116
  }
67062
67117
  const {
67063
- index,
67064
- reason
67118
+ index
67065
67119
  } = scrollInfo;
67066
- // Recentering on every row crossed would rebuild the whole window a few
67067
- // times a second, and a window rebuilt is every row of it rendered
67068
- // again. It only moves once what is on screen comes near one of its
67069
- // edges — until then, it already holds what has to be drawn.
67070
- const {
67071
- start,
67072
- end
67073
- } = renderWindowRef.current;
67074
67120
  const margin = Math.floor(renderBudget / 4);
67075
67121
  const farFromStart = index - start >= margin || start === 0;
67076
67122
  const farFromEnd = end - index > margin || end === total;
@@ -67078,12 +67124,113 @@ const useListScrollSync = ({
67078
67124
  return;
67079
67125
  }
67080
67126
  const half = Math.floor(renderBudget / 2);
67081
- let newStart = Math.max(0, index - half);
67082
- let newEnd = Math.min(total, newStart + renderBudget);
67083
- if (newEnd === total) {
67084
- newStart = Math.max(0, total - renderBudget);
67127
+ let newStart = index - half < 0 ? 0 : index - half;
67128
+ let newEnd = newStart + renderBudget;
67129
+ if (newEnd > total) {
67130
+ newEnd = total;
67131
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
67132
+ }
67133
+ windowSlidRef.current = true;
67134
+ updateRenderWindow(newStart, newEnd, `${reason}: ${scrollInfo.reason}`);
67135
+ return;
67136
+ }
67137
+ const viewportRect = getScrollerViewportRect(scrollerEl);
67138
+ const listRect = listEl.getBoundingClientRect();
67139
+ const viewportFrom = horizontal ? viewportRect.left : viewportRect.top;
67140
+ const viewportTo = horizontal ? viewportRect.right : viewportRect.bottom;
67141
+ const listFrom = horizontal ? listRect.left : listRect.top;
67142
+ let bandStart = Math.floor((viewportFrom - listFrom) / virtualItemSize);
67143
+ // Exclusive, like the window's end.
67144
+ let bandEnd = Math.ceil((viewportTo - listFrom) / virtualItemSize);
67145
+ if (bandStart < 0) {
67146
+ bandStart = 0;
67147
+ }
67148
+ if (bandEnd > total) {
67149
+ bandEnd = total;
67150
+ }
67151
+ if (bandEnd <= bandStart) {
67152
+ return;
67153
+ }
67154
+ const visibleCount = bandEnd - bandStart;
67155
+ const spare = renderBudget - visibleCount;
67156
+ const behind = spare > 0 ? Math.floor(spare / 4) : 0;
67157
+ const ahead = spare > 0 ? spare - behind : 0;
67158
+ const halfScreen = Math.ceil(visibleCount / 2);
67159
+ const aheadNeeded = halfScreen < ahead / 2 ? halfScreen : Math.floor(ahead / 2);
67160
+ const behindNeeded = halfScreen < behind / 2 ? halfScreen : Math.floor(behind / 2);
67161
+ const forward = scrollDirectionRef.current > 0;
67162
+ const rowsAheadOfBand = forward ? end - bandEnd : bandStart - start;
67163
+ const rowsBehindBand = forward ? bandStart - start : end - bandEnd;
67164
+ const atEdgeAhead = forward ? end === total : start === 0;
67165
+ const atEdgeBehind = forward ? start === 0 : end === total;
67166
+ const coversBand = start <= bandStart && end >= bandEnd;
67167
+ const coversAhead = atEdgeAhead || rowsAheadOfBand >= aheadNeeded;
67168
+ const coversBehind = atEdgeBehind || rowsBehindBand >= behindNeeded;
67169
+ if (coversBand && coversAhead && coversBehind) {
67170
+ return;
67171
+ }
67172
+ let newStart = forward ? bandStart - behind : bandEnd + behind - renderBudget;
67173
+ if (newStart < 0) {
67174
+ newStart = 0;
67175
+ }
67176
+ let newEnd = newStart + renderBudget;
67177
+ if (newEnd > total) {
67178
+ newEnd = total;
67179
+ newStart = total - renderBudget < 0 ? 0 : total - renderBudget;
67180
+ }
67181
+ windowSlidRef.current = true;
67182
+ updateRenderWindow(newStart, newEnd);
67183
+ };
67184
+ // A slide is judged again once its rows are laid out: the scroll had moved
67185
+ // on while the rows were being drawn, or the anchoring of this very commit
67186
+ // moved it, and a window that stops short of the screen's edge with no
67187
+ // scroll event to come would stay short for good. On the next frame, after
67188
+ // the layout this commit's anchoring settles on.
67189
+ useLayoutEffect(() => {
67190
+ if (!windowSlidRef.current) {
67191
+ return undefined;
67192
+ }
67193
+ windowSlidRef.current = false;
67194
+ const frameId = requestAnimationFrame(() => {
67195
+ evaluateWindow("after slide");
67196
+ });
67197
+ return () => {
67198
+ cancelAnimationFrame(frameId);
67199
+ };
67200
+ }, [renderWindow]);
67201
+
67202
+ // Scroll listener — slides the window as the user scrolls.
67203
+ useLayoutEffect(() => {
67204
+ const listContainerEl = ref.current;
67205
+ if (!listContainerEl) {
67206
+ return undefined;
67207
+ }
67208
+ const scrollerEl = getScroller();
67209
+ const onScroll = () => {
67210
+ // Which way the user is going, read before the position is stored: what
67211
+ // the window keeps ahead is ahead of this.
67212
+ const previousScroll = currentScrollRef.current;
67213
+ if (previousScroll) {
67214
+ const delta = horizontal ? scrollerEl.scrollLeft - previousScroll.left : scrollerEl.scrollTop - previousScroll.top;
67215
+ if (delta > 0) {
67216
+ scrollDirectionRef.current = 1;
67217
+ } else if (delta < 0) {
67218
+ scrollDirectionRef.current = -1;
67219
+ }
67220
+ }
67221
+ updateCurrentScroll();
67222
+ // Where the user is now is where things must be held from now on.
67223
+ anchorRef.current = null;
67224
+ if (scrolledByListRef.current) {
67225
+ // The window stays where it is — the position it would be re-derived
67226
+ // from was chosen to keep the rows still — but where the list is has
67227
+ // genuinely changed, and whoever keeps that position must hear it.
67228
+ scrolledByListRef.current = false;
67229
+ reportPosition();
67230
+ return;
67085
67231
  }
67086
- updateRenderWindow(newStart, newEnd);
67232
+ reportPosition();
67233
+ evaluateWindow("scroll");
67087
67234
  };
67088
67235
  // A page-level scroller does not emit "scroll" on the element itself
67089
67236
  // (document.scrollingElement); the document does.
@@ -70017,8 +70164,11 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
70017
70164
  * window, which slides as the user scrolls while fillers hold the room of
70018
70165
  * the rows outside it. Rows declared one by one as `<List.Item>` children
70019
70166
  * are all drawn, whatever this says — a list with more than a few dozen rows
70020
- * gives them to a run (see docs/scroll.md, "Many rows"). Below 30 the list
70021
- * warns: a window shorter than a tall screen shows blank fillers.
70167
+ * gives them to a run (see docs/scroll.md, "Many rows"). The window keeps
70168
+ * most of its spare rows ahead of the scroll, and moves once those fall
70169
+ * under half a screen; it has to be larger than what the scroller shows at
70170
+ * once, with room for that lookahead — the list warns below 30, and when a
70171
+ * budget leaves fewer than two rows beyond the screen.
70022
70172
  *
70023
70173
  * `{ initial, after }` for a list drawn in the click that opens it (a popup):
70024
70174
  * `initial` rows in the commit the browser paints first — about what a phone
@@ -70140,14 +70290,10 @@ const generateTimeSlots = (min, max, stepSeconds) => {
70140
70290
  const PickerPresetResolver = props => {
70141
70291
  const Next = useNextResolver();
70142
70292
  if (props.type === "navi_time") {
70143
- return jsx(PickerNaviTime, {
70144
- ...props
70145
- });
70293
+ return renderResolver(PickerNaviTime, props);
70146
70294
  }
70147
70295
  if (props.type === "navi_minute") {
70148
- return jsx(PickerNaviMinute, {
70149
- ...props
70150
- });
70296
+ return renderResolver(PickerNaviMinute, props);
70151
70297
  }
70152
70298
  return jsx(Next, {
70153
70299
  ...props
@@ -70945,59 +71091,37 @@ const PencilSvg = () => {
70945
71091
  const PickerTypeResolver = props => {
70946
71092
  const Next = useNextResolver();
70947
71093
  if (props.type === "color") {
70948
- return jsx(PickerColor, {
70949
- ...props
70950
- });
71094
+ return renderResolver(PickerColor, props);
70951
71095
  }
70952
71096
  if (props.type === "datetime") {
70953
- return jsx(PickerDatetime, {
70954
- ...props
70955
- });
71097
+ return renderResolver(PickerDatetime, props);
70956
71098
  }
70957
71099
  if (props.type === "date") {
70958
- return jsx(PickerDate, {
70959
- ...props
70960
- });
71100
+ return renderResolver(PickerDate, props);
70961
71101
  }
70962
71102
  if (props.type === "month") {
70963
- return jsx(PickerMonth, {
70964
- ...props
70965
- });
71103
+ return renderResolver(PickerMonth, props);
70966
71104
  }
70967
71105
  if (props.type === "week") {
70968
- return jsx(PickerWeek, {
70969
- ...props
70970
- });
71106
+ return renderResolver(PickerWeek, props);
70971
71107
  }
70972
71108
  if (props.type === "time") {
70973
- return jsx(PickerTime, {
70974
- ...props
70975
- });
71109
+ return renderResolver(PickerTime, props);
70976
71110
  }
70977
71111
  if (props.type === "duration") {
70978
- return jsx(PickerDuration, {
70979
- ...props
70980
- });
71112
+ return renderResolver(PickerDuration, props);
70981
71113
  }
70982
71114
  if (props.type === "file") {
70983
- return jsx(PickerFile, {
70984
- ...props
70985
- });
71115
+ return renderResolver(PickerFile, props);
70986
71116
  }
70987
71117
  if (props.type === "text") {
70988
- return jsx(PickerText, {
70989
- ...props
70990
- });
71118
+ return renderResolver(PickerText, props);
70991
71119
  }
70992
71120
  if (props.type === "array") {
70993
- return jsx(PickerArray, {
70994
- ...props
70995
- });
71121
+ return renderResolver(PickerArray, props);
70996
71122
  }
70997
71123
  if (props.type === "object") {
70998
- return jsx(PickerObject, {
70999
- ...props
71000
- });
71124
+ return renderResolver(PickerObject, props);
71001
71125
  }
71002
71126
  return jsx(Next, {
71003
71127
  ...props
@@ -84052,7 +84176,7 @@ const css = /* css */`.navi_side_panel {
84052
84176
  * @param {object} props
84053
84177
  * @param {boolean} [props.open] - Controlled open state, forwarded as-is to
84054
84178
  * `Popup`'s own `open`.
84055
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
84179
+ * @param {import("@preact/signals").Signal<any>} [props.signal] - The open
84056
84180
  * state said the way every navi control says it: the panel opens and closes
84057
84181
  * to match the signal, and writes into it whenever it opens or closes on its
84058
84182
  * own (Escape, swipe, a --navi-close command) — one binding to both drive
@@ -84126,7 +84250,9 @@ const css = /* css */`.navi_side_panel {
84126
84250
  * closes the panel on an outside click instead, and also enables trapping
84127
84251
  * Tab navigation inside the panel (`focusCapture`) — closing on outside
84128
84252
  * interaction only makes sense paired with not letting focus silently
84129
- * leave the panel first.
84253
+ * leave the panel first. A box of the page whose press must not close the
84254
+ * panel (a card that fills it) names the panel:
84255
+ * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
84130
84256
  * @param {boolean} [props.swipeToClose=true] - Pushing the panel back
84131
84257
  * towards the edge it is docked to closes it: the panel follows the
84132
84258
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -84261,6 +84387,7 @@ SidePanel.Foot = SidePanelFoot;
84261
84387
 
84262
84388
  const createSlot = (SlotRenderer = Box) => {
84263
84389
  const slotPropsSignal = signal();
84390
+ let filler = null;
84264
84391
  const Slot = () => {
84265
84392
  const props = slotPropsSignal.value;
84266
84393
  return jsx(SlotRenderer, {
@@ -84269,9 +84396,15 @@ const createSlot = (SlotRenderer = Box) => {
84269
84396
  });
84270
84397
  };
84271
84398
  const SlotFill = props => {
84399
+ const fillerRef = useRef();
84400
+ filler = fillerRef;
84272
84401
  slotPropsSignal.value = props;
84273
84402
  useLayoutEffect(() => {
84274
84403
  return () => {
84404
+ if (filler !== fillerRef) {
84405
+ return;
84406
+ }
84407
+ filler = null;
84275
84408
  slotPropsSignal.value = null;
84276
84409
  };
84277
84410
  }, []);