@jsenv/navi 0.29.42 → 0.29.44

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.
@@ -51,7 +51,15 @@ const css$10 = /* css */`
51
51
  /* Kept stuck while something scrolls under it: a list header, the head
52
52
  and foot of a side panel, a table's sticky cells. Above raised
53
53
  controls — a control scrolling past must go under the header that
54
- pins the column it belongs to, never over it. */
54
+ pins the column it belongs to, never over it.
55
+
56
+ "While stuck" is the whole condition, and a sticky element cannot read
57
+ its own stuck state in CSS: List marks its parts with a navi-stuck
58
+ attribute and applies this band only there (see --list-*-z-index in
59
+ list.jsx). A
60
+ sticky part at rest is a block in the flow with nothing passing under
61
+ it; giving it this band anyway is what slices whatever a neighbouring
62
+ row lets out of its box. */
55
63
  --navi-z-index-sticky: 10;
56
64
 
57
65
  /* Pinned to the viewport, over the whole page: FixedBar. A decade of its
@@ -22983,6 +22991,9 @@ const useUIStateController = (
22983
22991
  // resolveCommandValue in commands.js.
22984
22992
  ownUIStateSignal,
22985
22993
  value: controlInfo.value,
22994
+ // The suggestion this control started on — what tells a field showing
22995
+ // its default from one carrying an answer (see isUIStateHeld).
22996
+ defaultValue: controlInfo.defaultValue,
22986
22997
 
22987
22998
  facadeChild: null,
22988
22999
  getManagedControls: () => {
@@ -23401,9 +23412,16 @@ const useUIStateController = (
23401
23412
  controller.id = props.id; // never supposed to change, not supported for now
23402
23413
  controller.name = props.name;
23403
23414
  controller.parentUIStateController = parentUIStateController;
23404
- const { value, hasStateProp, state, stateInitial, stateFromSignal } =
23405
- controlInfo;
23415
+ const {
23416
+ value,
23417
+ defaultValue,
23418
+ hasStateProp,
23419
+ state,
23420
+ stateInitial,
23421
+ stateFromSignal,
23422
+ } = controlInfo;
23406
23423
  controller.value = value;
23424
+ controller.defaultValue = defaultValue;
23407
23425
  if (hasStateProp) {
23408
23426
  controller.hasStateProp = true;
23409
23427
  const currentState = controller.state;
@@ -23740,9 +23758,9 @@ const useUIGroupStateController = (
23740
23758
  const delegatedChildrenRef = useRef(new Map());
23741
23759
 
23742
23760
  const groupIsRenderingRef = useRef(false);
23743
- const pendingChangeRef = useRef(false);
23761
+ const pendingChangeRef = useRef(null);
23744
23762
  groupIsRenderingRef.current = true;
23745
- pendingChangeRef.current = false;
23763
+ pendingChangeRef.current = null;
23746
23764
 
23747
23765
  const isMonitoringChild = (childUIStateController) => {
23748
23766
  if (childUIStateController.isProxy) return false;
@@ -23774,7 +23792,18 @@ const useUIGroupStateController = (
23774
23792
  // signals/pubsub without needing external refs.
23775
23793
  const onChange = (e, { notifyExternal }) => {
23776
23794
  if (groupIsRenderingRef.current) {
23777
- pendingChangeRef.current = true;
23795
+ // Held until the layout effect below, WITH what it asked for: a child
23796
+ // whose bound signal was written from the outside changes during the
23797
+ // render that follows, and replaying that as a mount sync is what
23798
+ // makes a group silently drift — its own state comes up to date while
23799
+ // the form around it is never told anything moved. A real change
23800
+ // deferred alongside a mount sync stays a real change.
23801
+ const pendingChange = pendingChangeRef.current;
23802
+ pendingChangeRef.current = {
23803
+ e,
23804
+ notifyExternal:
23805
+ pendingChange?.notifyExternal === true ? true : notifyExternal,
23806
+ };
23778
23807
  return;
23779
23808
  }
23780
23809
  const aggChildState = resolvedAggregateChildStates(
@@ -24238,12 +24267,16 @@ const useUIGroupStateController = (
24238
24267
 
24239
24268
  useLayoutEffect(() => {
24240
24269
  groupIsRenderingRef.current = false;
24241
- if (pendingChangeRef.current) {
24242
- pendingChangeRef.current = false;
24243
- scope._onChange(
24244
- new CustomEvent(`${controlType}_batched_ui_state_update`),
24245
- { notifyExternal: "silent" },
24270
+ const pendingChange = pendingChangeRef.current;
24271
+ if (pendingChange) {
24272
+ pendingChangeRef.current = null;
24273
+ const batchedEvent = new CustomEvent(
24274
+ `${controlType}_batched_ui_state_update`,
24246
24275
  );
24276
+ chainEvent(batchedEvent, pendingChange.e);
24277
+ scope._onChange(batchedEvent, {
24278
+ notifyExternal: pendingChange.notifyExternal,
24279
+ });
24247
24280
  }
24248
24281
  });
24249
24282
 
@@ -24304,20 +24337,6 @@ const useUIFacadeStateController = (props, realUIStateController) => {
24304
24337
  // not be treated as the picker's synced child.
24305
24338
  return false;
24306
24339
  }
24307
- if (props.type === "form" && childController.controlType !== "form") {
24308
- // Only a form: what a type="form" picker syncs with is the form in
24309
- // its popup, not any control that happens to be in there (an input
24310
- // sitting outside the form, a ControlGroup — which is a way of
24311
- // grouping controls INSIDE a form, not a thing a picker talks to).
24312
- return false;
24313
- }
24314
- if (
24315
- props.type === "array" &&
24316
- childController.controlType !== "checkbox_group"
24317
- ) {
24318
- // only selectable list expose array, ignore others
24319
- return false;
24320
- }
24321
24340
  return true;
24322
24341
  };
24323
24342
 
@@ -24334,8 +24353,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
24334
24353
  const childType = child.controlType;
24335
24354
  if (firstChildControllerRef.current) {
24336
24355
  console.warn(
24337
- `[useUIFacadeStateController] A second child ("${childType}"${child.name ? ` name="${child.name}"` : ""}) tried to register in the picker facade. ` +
24338
- `The facade only syncs with the first child wrap multiple controls in a single ControlGroup.`,
24356
+ `[navi] a second control ("${childType}"${child.name ? ` name="${child.name}"` : ""}) registered in the ${describePicker(props)} popup. ` +
24357
+ `A picker talks to ONE control: the first one receives the picker's whole value and is the only one read back, ` +
24358
+ `so this one is neither filled nor collected. ` +
24359
+ `A popup holding several values needs one group around them — wrap them in a <ControlGroup>, name each control inside it, and give the picker type="object".`,
24339
24360
  child,
24340
24361
  );
24341
24362
  } else {
@@ -24463,6 +24484,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
24463
24484
  return scope.controller;
24464
24485
  };
24465
24486
 
24487
+ const describePicker = (props) =>
24488
+ `<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
24489
+
24466
24490
  /**
24467
24491
  * Returns true when `e` should trigger parent notification (child → parent bubbling).
24468
24492
  *
@@ -24612,12 +24636,19 @@ const ControlgroupChildrenWrapper = ({
24612
24636
  */
24613
24637
  const useControlProps = (props, {
24614
24638
  controlType,
24615
- allowNameless,
24639
+ allowNameless: allowNamelessByDefault,
24616
24640
  persists,
24617
24641
  uiActionInternal
24618
24642
  }) => {
24619
24643
  const debugUIState = useDebugUIState();
24620
24644
  const debugAction = useDebugAction();
24645
+
24646
+ // A control that is not a field: it opens something, it goes somewhere, and
24647
+ // the group around it must expect no value from it — no name, and no warning
24648
+ // about the missing name. Buttons and links say so from inside navi; the prop
24649
+ // is how a control used as a door says the same thing from the outside.
24650
+ const allowNameless = props.allowNameless ?? allowNamelessByDefault;
24651
+ delete props.allowNameless;
24621
24652
  const idDefault = useId();
24622
24653
  const controlId = useContext(ControlIdContext);
24623
24654
  props.id = props.id || controlId || idDefault;
@@ -25426,6 +25457,18 @@ const createControlInfo = (props, {
25426
25457
  // getDefaultEventReactionDefinitions.
25427
25458
  readOnlySupported = controlType === "picker" && INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
25428
25459
  }
25460
+
25461
+ // The suggestion the control starts on, as opposed to what it holds — what a
25462
+ // reset goes back to, and what tells a field left on its default from one
25463
+ // carrying an answer (see isUIStateHeld in held_ui_state.js).
25464
+ let defaultValue;
25465
+ if (!hasStateProp) {
25466
+ if (signalHoldsChecked) {
25467
+ defaultValue = props.defaultChecked ? value : undefined;
25468
+ } else if (Object.hasOwn(props, "defaultValue")) {
25469
+ defaultValue = props.defaultValue;
25470
+ }
25471
+ }
25429
25472
  return {
25430
25473
  controlType,
25431
25474
  statePropName,
@@ -25434,6 +25477,7 @@ const createControlInfo = (props, {
25434
25477
  stateInitial,
25435
25478
  state: stateInitial,
25436
25479
  value,
25480
+ defaultValue,
25437
25481
  signal,
25438
25482
  signalHoldsChecked,
25439
25483
  stateFromSignal,
@@ -28339,6 +28383,20 @@ const css$V = /* css */`
28339
28383
  backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
28340
28384
  }
28341
28385
 
28386
+ /* backdropAppearance, keyed off the originating element (a
28387
+ pseudo-element carries no attributes of its own — same reasoning as
28388
+ the capture rule just above). After the rules it overrides: same
28389
+ specificity, so order is what decides. showModal() still makes the
28390
+ page inert either way — only the paint goes away. */
28391
+ &[data-backdrop-appearance="discrete"]::backdrop {
28392
+ background: var(--navi-backdrop-discrete-background);
28393
+ backdrop-filter: none;
28394
+ }
28395
+ &[data-backdrop-appearance="none"]::backdrop {
28396
+ background: transparent;
28397
+ backdrop-filter: none;
28398
+ }
28399
+
28342
28400
  /* Nested under &[navi-animation] (not the other way around) so every
28343
28401
  attribute selector compiles *before* ::backdrop, not after — a
28344
28402
  pseudo-element can't be qualified by an attribute of its own
@@ -28458,6 +28516,18 @@ const css$V = /* css */`
28458
28516
  backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
28459
28517
  }
28460
28518
 
28519
+ /* Same override as the via-attribute renderer's own ::backdrop rules
28520
+ above, on the real element this renderer uses instead — see them for
28521
+ the specificity/ordering reasoning. */
28522
+ &[data-backdrop-appearance="discrete"] {
28523
+ background: var(--navi-backdrop-discrete-background);
28524
+ backdrop-filter: none;
28525
+ }
28526
+ &[data-backdrop-appearance="none"] {
28527
+ background: transparent;
28528
+ backdrop-filter: none;
28529
+ }
28530
+
28461
28531
  &[navi-animation] {
28462
28532
  opacity: 1;
28463
28533
  transition-property: display, opacity;
@@ -28528,6 +28598,13 @@ const css$V = /* css */`
28528
28598
  * both just absorb the click without closing (visually dimmed backdrop vs.
28529
28599
  * not) — a dialog is always modal one way or another, so there's always
28530
28600
  * at least a click-absorbing backdrop regardless of this prop.
28601
+ * @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
28602
+ * visible the backdrop is, independently of what it does. `"auto"`: the
28603
+ * paint `pointerInteractionOutsideEffect` implies (dimmed for
28604
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
28605
+ * barely-there dim. `"none"`: fully transparent. The dialog stays modal
28606
+ * either way — this only changes how much it insists visually, never what
28607
+ * an outside click does or whether the page behind stays reachable.
28531
28608
  * @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
28532
28609
  * dialog so the page/container behind it can't scroll while it's open.
28533
28610
  * A `layer="local"` dialog always locks its own positioned ancestor's
@@ -28775,6 +28852,11 @@ const useDialogProps = props => {
28775
28852
  // there's no native inert-ing, so the real backdrop below is what
28776
28853
  // actually makes "capture"/"none" behave the same way here too.
28777
28854
  pointerInteractionOutsideEffect = "close",
28855
+ // How loudly the backdrop says it is there — independent of what it
28856
+ // *does* (that's pointerInteractionOutsideEffect above). A dialog is
28857
+ // always modal, so "none" here never makes the page behind reachable:
28858
+ // it only stops the dim from being drawn.
28859
+ backdropAppearance = "auto",
28778
28860
  scrollCapture: scrollCaptureProp,
28779
28861
  // "auto" (default) → the dialog follows its content. "frozen" → measured
28780
28862
  // once, held at that size while open. See this prop's own JSDoc above.
@@ -29293,7 +29375,8 @@ const useDialogProps = props => {
29293
29375
  "navi-hidden": openController.opened ? undefined : "",
29294
29376
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
29295
29377
  "animationDuration": rest.animationDuration,
29296
- "data-pointer-interaction-outside": pointerInteractionOutsideEffect
29378
+ "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
29379
+ "data-backdrop-appearance": backdropAppearance
29297
29380
  });
29298
29381
  Object.assign(contentProps, {
29299
29382
  tabIndex,
@@ -29322,6 +29405,11 @@ const useDialogProps = props => {
29322
29405
  // real backdrop element already gets the same attribute via
29323
29406
  // backdropProps above, which is what its own CSS actually keys off).
29324
29407
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
29408
+ // Only load-bearing for the via-attribute renderer's own native
29409
+ // ::backdrop, same "a pseudo-element can't carry attributes" reasoning
29410
+ // as the prop just above (and harmless for the custom renderer, whose
29411
+ // real backdrop element gets it via backdropProps).
29412
+ "data-backdrop-appearance": backdropAppearance,
29325
29413
  "styleCSSVars": DIALOG_STYLE_CSS_VARS,
29326
29414
  ...rest,
29327
29415
  ...autoFocusProps,
@@ -29724,6 +29812,20 @@ const css$U = /* css */`
29724
29812
  backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
29725
29813
  }
29726
29814
 
29815
+ /* backdropAppearance overrides whatever the effect above picked — same
29816
+ specificity (class + one attribute), so these have to stay *after*
29817
+ them to win. Only the paint changes: the element is still rendered
29818
+ and still pointer-events: auto, so an outside click keeps doing
29819
+ exactly what pointerInteractionOutsideEffect says. */
29820
+ &[data-backdrop-appearance="discrete"] {
29821
+ background: var(--navi-backdrop-discrete-background);
29822
+ backdrop-filter: none;
29823
+ }
29824
+ &[data-backdrop-appearance="none"] {
29825
+ background: transparent;
29826
+ backdrop-filter: none;
29827
+ }
29828
+
29727
29829
  /* navi-animation mirrors the content popover's own resolved value (set
29728
29830
  imperatively in openEffect) — the backdrop only ever fades, regardless
29729
29831
  of which kind it is (translate/scale wouldn't mean anything on it).
@@ -29787,6 +29889,15 @@ const css$U = /* css */`
29787
29889
  * absorbs the click (dims the backdrop) without closing. Note this
29788
29890
  * default differs from `Dialog`'s own (`"close"`) — a popover is
29789
29891
  * typically a lightweight, non-modal affordance.
29892
+ * @param {"auto"|"discrete"|"none"} [props.backdropAppearance="auto"] - How
29893
+ * visible the backdrop is, independently of what it does. `"auto"`: the
29894
+ * paint `pointerInteractionOutsideEffect` implies (dimmed for
29895
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
29896
+ * barely-there dim. `"none"`: fully transparent. The backdrop is still
29897
+ * rendered and still catches outside clicks in every case — this only
29898
+ * changes how much the popover insists on being the thing you deal with.
29899
+ * Ignored when `pointerInteractionOutsideEffect="none"` (there is no
29900
+ * backdrop at all then, and outside clicks pass through).
29790
29901
  * @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
29791
29902
  * popover so the page/container behind it can't scroll while it's open.
29792
29903
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
@@ -30031,6 +30142,11 @@ const usePopoverProps = props => {
30031
30142
  // "capture"→ absorb the press, stay open
30032
30143
  // "none" → no backdrop
30033
30144
  pointerInteractionOutsideEffect = "none",
30145
+ // How loudly the backdrop says it is there — independent of what it
30146
+ // *does* (that's pointerInteractionOutsideEffect above). "auto" keeps
30147
+ // the paint the effect implies; "discrete"/"none" tone it down or
30148
+ // remove it entirely without giving up the outside click.
30149
+ backdropAppearance = "auto",
30034
30150
  scrollCapture,
30035
30151
  focusCapture,
30036
30152
  // "auto" (default) → the popover follows its content. "frozen" → measured
@@ -30704,6 +30820,7 @@ const usePopoverProps = props => {
30704
30820
  "styleCSSVars": POPUP_STYLE_CSS_VARS,
30705
30821
  "animationDuration": rest.animationDuration,
30706
30822
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
30823
+ "data-backdrop-appearance": backdropAppearance,
30707
30824
  "onMouseDown": mouseDownEvent => {
30708
30825
  if (mouseDownEvent.button !== 0) {
30709
30826
  return;
@@ -32230,6 +32347,12 @@ const defaultRerunOn = {
32230
32347
  GET: false,
32231
32348
  GET_MANY: ["POST"],
32232
32349
  };
32350
+ // What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
32351
+ // in there, unlike for GET_MANY: an action holds ids and the store drops the
32352
+ // deleted one out of every list holding it, while a list reading by slices
32353
+ // holds places — the row that left takes the ones after it one rank up, and
32354
+ // only the collection knows who fills the last one.
32355
+ const defaultInvalidateRangeOn = ["POST", "DELETE"];
32233
32356
 
32234
32357
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
32235
32358
  const createResourceLifecycleManager = () => {
@@ -32250,6 +32373,7 @@ const createResourceLifecycleManager = () => {
32250
32373
  paramScope,
32251
32374
  uniqueKeys,
32252
32375
  restActionSet: new Set(),
32376
+ rangeReaderSet: new Set(),
32253
32377
  });
32254
32378
 
32255
32379
  // Register dependencies
@@ -32262,6 +32386,14 @@ const createResourceLifecycleManager = () => {
32262
32386
  }
32263
32387
  }
32264
32388
  };
32389
+ // Only the reader the resource exposes is registered; the ones `bindParams`
32390
+ // makes share its signal (see resource_range_reader.js).
32391
+ const registerRangeReader = (resourceScope, rangeReader) => {
32392
+ const config = registeredResources.get(resourceScope);
32393
+ if (config) {
32394
+ config.rangeReaderSet.add(rangeReader);
32395
+ }
32396
+ };
32265
32397
  const registerAction = (resourceScope, restAction) => {
32266
32398
  const config = registeredResources.get(resourceScope);
32267
32399
  if (config) {
@@ -32492,7 +32624,39 @@ const createResourceLifecycleManager = () => {
32492
32624
  };
32493
32625
  };
32494
32626
 
32627
+ // Which readers hold slices of a collection the mutation just changed: the
32628
+ // one of the resource itself, and the ones of the resources that said they
32629
+ // depend on it.
32630
+ const invalidateRangeReaders = (
32631
+ triggeringAction,
32632
+ triggeringActionContext,
32633
+ ) => {
32634
+ const triggerVerb = triggeringAction.meta.verb;
32635
+ const triggerResourceScope = triggeringActionContext.resourceScope;
32636
+ for (const [resourceScope, config] of registeredResources) {
32637
+ if (config.rangeReaderSet.size === 0) {
32638
+ continue;
32639
+ }
32640
+ const isSameResource = resourceScope === triggerResourceScope;
32641
+ const isDependent = Boolean(
32642
+ triggerResourceScope &&
32643
+ resourceDependencies.get(triggerResourceScope)?.has(resourceScope),
32644
+ );
32645
+ if (!isSameResource && !isDependent) {
32646
+ continue;
32647
+ }
32648
+ const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
32649
+ if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
32650
+ continue;
32651
+ }
32652
+ for (const rangeReader of config.rangeReaderSet) {
32653
+ rangeReader.invalidate();
32654
+ }
32655
+ }
32656
+ };
32657
+
32495
32658
  const onActionComplete = (restActionWhoJustCompleted, restActionContext) => {
32659
+ invalidateRangeReaders(restActionWhoJustCompleted, restActionContext);
32496
32660
  const { actionsToRerun, actionsToReset, reasons } = findEffectOnActions(
32497
32661
  restActionWhoJustCompleted,
32498
32662
  restActionContext,
@@ -32511,6 +32675,7 @@ const createResourceLifecycleManager = () => {
32511
32675
  return {
32512
32676
  registerResource,
32513
32677
  registerAction,
32678
+ registerRangeReader,
32514
32679
  onActionComplete,
32515
32680
  // Registers: when `triggerResource` fires, rerun `dependentResource`'s actions.
32516
32681
  // Used by scopedMany to make the parent GET rerun when a child mutation completes.
@@ -32588,13 +32753,20 @@ const getParamScope = (params) => {
32588
32753
  *
32589
32754
  * The reader is a function, so a list feeds on it the way it feeds on any other
32590
32755
  * source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
32756
+ *
32757
+ * Keeping nothing does not mean hearing nothing: a mutation that decides who
32758
+ * belongs to the collection (a POST, a DELETE, whatever `rerunOn.GET_RANGE`
32759
+ * says) bumps `invalidationSignal`, and whoever reads slices through it goes
32760
+ * and asks again — the counterpart, for a reader, of what a rerun is for an
32761
+ * action. Every reader made by `bindParams` shares the signal of the one it
32762
+ * comes from: the params say which slices are read, not which collection.
32591
32763
  */
32592
32764
 
32593
32765
 
32594
32766
  const createRangeReader = (
32595
32767
  actionName,
32596
32768
  callback,
32597
- { store, params: boundParams },
32769
+ { store, params: boundParams, invalidationSignal = signal(0) },
32598
32770
  ) => {
32599
32771
  const readRange = async (range = {}) => {
32600
32772
  const { signal, ...rangeParams } = range;
@@ -32623,10 +32795,17 @@ const createRangeReader = (
32623
32795
  };
32624
32796
  Object.defineProperty(readRange, "name", { value: actionName });
32625
32797
  readRange.isRangeReader = true;
32798
+ // Bumped when the collection this reads has moved: the slices anyone holds
32799
+ // stand for a composition that is gone.
32800
+ readRange.invalidationSignal = invalidationSignal;
32801
+ readRange.invalidate = () => {
32802
+ invalidationSignal.value = invalidationSignal.peek() + 1;
32803
+ };
32626
32804
  readRange.bindParams = (paramsToBind) => {
32627
32805
  return createRangeReader(actionName, callback, {
32628
32806
  store,
32629
32807
  params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
32808
+ invalidationSignal,
32630
32809
  });
32631
32810
  };
32632
32811
  return readRange;
@@ -32693,9 +32872,11 @@ const debug$2 = (args) => {
32693
32872
  * - GET_MANY / POST_MANY / … → an array of item objects
32694
32873
  * - GET_RANGE → `{ items, start, count }`, one slice of the collection
32695
32874
  *
32696
- * `GET_RANGE` is a reader rather than an action: it keeps no value and takes no place in
32697
- * the rerun graph, so a `<List.Items>` can feed on it slice by slice
32698
- * (`itemsAction={USER.GET_RANGE.bindParams({ team })}`).
32875
+ * `GET_RANGE` is a reader rather than an action: it keeps no value and has nothing to
32876
+ * rerun, so a `<List.Items>` can feed on it slice by slice
32877
+ * (`itemsAction={USER.GET_RANGE.bindParams({ team })}`). A mutation listed in
32878
+ * `rerunOn.GET_RANGE` (`["POST", "DELETE"]` by default) tells it the collection moved,
32879
+ * and whoever holds slices reads them again.
32699
32880
  *
32700
32881
  * A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
32701
32882
  * relationship method, never as an `op`/`type` discriminator dispatched inside one
@@ -33875,13 +34056,18 @@ ${originalActionName} source location: ${locationInfo}`,
33875
34056
  continue;
33876
34057
  }
33877
34058
  if (restCallbackKey === "GET_RANGE") {
33878
- // A range is read, never kept: no action, no place in the rerun graph
34059
+ // A range is read, never kept: no action, nothing to rerun only a
34060
+ // signal saying the slices anyone holds are out of date
33879
34061
  // (see resource_range_reader.js).
33880
34062
  stateFacade.GET_RANGE = createRangeReader(
33881
34063
  `${name}.GET_RANGE`,
33882
34064
  restCallback,
33883
34065
  { store, params },
33884
34066
  );
34067
+ resourceLifecycleManager.registerRangeReader(
34068
+ stateFacade,
34069
+ stateFacade.GET_RANGE,
34070
+ );
33885
34071
  continue;
33886
34072
  }
33887
34073
  const isMany = restCallbackKey.endsWith("_MANY");
@@ -47374,6 +47560,86 @@ const Editable = props => {
47374
47560
  });
47375
47561
  };
47376
47562
 
47563
+ /**
47564
+ * What a control HOLDS, as opposed to what it is showing.
47565
+ *
47566
+ * A `value` is held: the control was given it, so handing it back says nothing
47567
+ * new. A `defaultValue` is only a suggestion — an age that is usually 18, a
47568
+ * duration that is usually 1h30 — so the control holds nothing, and confirming
47569
+ * the suggestion IS an answer ("yes, 18"). A control bound to a signal falls on
47570
+ * whichever side the signal put it: a signal with something in it is an answer
47571
+ * (restored from the url, set by whoever owns it), an empty one leaves the
47572
+ * control on its suggestion.
47573
+ *
47574
+ * The same distinction Form makes across its fields (see readHeldUIState in
47575
+ * form.jsx), asked of a single control — which is what lets a Picker tell "the
47576
+ * user re-confirmed what was already chosen" (nothing new) from "the user
47577
+ * accepted the proposal" (an answer).
47578
+ */
47579
+
47580
+ const isUIStateHeld = (controller) => {
47581
+ if (!controller) {
47582
+ return false;
47583
+ }
47584
+ // Given a value outright: held, whatever it is showing.
47585
+ if (controller.hasStateProp || controller.hasValueProp) {
47586
+ return true;
47587
+ }
47588
+ // A facade (a picker) shows what the control inside its popup holds, so that
47589
+ // is the one to ask — the facade itself was given nothing.
47590
+ const facadeChild = controller.facadeChild;
47591
+ if (facadeChild) {
47592
+ return isUIStateHeld(facadeChild);
47593
+ }
47594
+ const boundSignal = controller.props?.signal;
47595
+ if (boundSignal) {
47596
+ return boundSignal.value !== undefined;
47597
+ }
47598
+ // Uncontrolled with a suggestion: what it shows is that suggestion until it
47599
+ // differs from it.
47600
+ if (controller.defaultValue !== undefined) {
47601
+ return !compareTwoJsValues(controller.uiState, controller.defaultValue);
47602
+ }
47603
+ // A group holding nothing of its own is worth what its children are: two
47604
+ // wheels each on their own suggestion make a group still waiting for an
47605
+ // answer, one of them moved makes a group holding one.
47606
+ const childControllers = controller.getChildControllers?.() || [];
47607
+ if (childControllers.length > 0) {
47608
+ return childControllers.some((child) => isUIStateHeld(child));
47609
+ }
47610
+ return controller.uiState !== undefined;
47611
+ };
47612
+
47613
+ /**
47614
+ * Tell a control — and everything inside it — that what it is showing is now
47615
+ * the answer, without its state having to move.
47616
+ *
47617
+ * The state is already right; what has not happened is anyone saying so. A
47618
+ * control reports an answer through `onUIAction` (that is where a bound signal
47619
+ * is written, where `uiAction` fires), and a suggestion nobody touched never
47620
+ * got there. Confirming a picker is exactly that moment.
47621
+ *
47622
+ * Down the whole subtree because that is where the answer actually lives: a
47623
+ * picker holding a group of two wheels has one signal per wheel, and it is each
47624
+ * wheel that has to record what it is showing. Commands are skipped — a
47625
+ * `command` on a control is its reaction to being used, and this is a
47626
+ * confirmation happening elsewhere, whose own command (the picker's) is already
47627
+ * running.
47628
+ */
47629
+ const commitUIStateAsAnswer = (controller, e) => {
47630
+ if (!controller) {
47631
+ return;
47632
+ }
47633
+ const answering = controller.facadeChild || controller;
47634
+ commitSubtree(answering, e);
47635
+ };
47636
+ const commitSubtree = (controller, e) => {
47637
+ controller.onUIAction?.(e, { skipCommand: true });
47638
+ for (const child of controller.getChildControllers?.() || []) {
47639
+ commitSubtree(child, e);
47640
+ }
47641
+ };
47642
+
47377
47643
  /**
47378
47644
  *
47379
47645
  * Here we want the same behaviour as web standards:
@@ -47424,6 +47690,7 @@ const useFormGroup = props => {
47424
47690
  };
47425
47691
  delete propsForGroup.standalone;
47426
47692
  delete propsForGroup.canSendWhileUnchanged;
47693
+ delete propsForGroup.pristineKey;
47427
47694
  // Not the generic control `command`, which a control triggers on its own ui
47428
47695
  // actions — here it is what follows a SUCCESSFUL submission. So it is kept
47429
47696
  // out of the control machinery and left in the DOM for the send to read
@@ -47456,7 +47723,7 @@ const useFormGroup = props => {
47456
47723
  // microtask away, so typing and pressing Enter right after must not be read
47457
47724
  // against the state of the previous frame.
47458
47725
  uiStateController.shouldRequestAction = value => Boolean(props.canSendWhileUnchanged) || !compareTwoJsValues(withoutEmptyFields(value), uiStateController.sentUIState);
47459
- useFirstUIStateAsSent(uiStateController);
47726
+ useHeldUIStateAsSent(uiStateController, props.pristineKey);
47460
47727
  useUnregisteredControlWarning(props.ref);
47461
47728
  const {
47462
47729
  basePseudoState,
@@ -47554,13 +47821,10 @@ const FormNested = props => {
47554
47821
  });
47555
47822
  };
47556
47823
 
47557
- // What the form HOLDS, as opposed to what it is showing. A `value` is held: the
47558
- // form was given it, and sending it back says nothing new. A `defaultValue` is
47559
- // only a suggestion an age that is usually 18, a duration that is usually
47560
- // 1h30 so the form holds nothing for that field, and sending the suggestion
47561
- // back IS an answer ("yes, 18"). A field bound to a signal falls on whichever
47562
- // side the signal put it: one carrying a default seeds `defaultValue`, one
47563
- // without controls the field outright.
47824
+ // What the form HOLDS, as opposed to what it is showing field by field, the
47825
+ // question isUIStateHeld answers: a field it was given an answer for is held,
47826
+ // a field merely showing a suggestion is not, and confirming that suggestion IS
47827
+ // an answer ("yes, 18").
47564
47828
  const readHeldUIState = uiStateController => {
47565
47829
  const uiState = uiStateController.uiState;
47566
47830
  // A form given a value holds all of it, whatever its fields say.
@@ -47571,7 +47835,7 @@ const readHeldUIState = uiStateController => {
47571
47835
  ...uiState
47572
47836
  };
47573
47837
  for (const child of uiStateController.getChildControllers?.() || []) {
47574
- if (child.name && !child.hasStateProp) {
47838
+ if (child.name && !isUIStateHeld(child)) {
47575
47839
  delete held[child.name];
47576
47840
  }
47577
47841
  }
@@ -47601,10 +47865,22 @@ const withoutEmptyFields = uiState => {
47601
47865
  // register themselves in their own effects, which run first — this is the
47602
47866
  // earliest moment the form knows what it holds. Everything after this baseline
47603
47867
  // is a real send moving it forward (see useFormGroup's own onnavi_action_end).
47604
- const useFirstUIStateAsSent = uiStateController => {
47868
+ const useHeldUIStateAsSent = (uiStateController, pristineKey) => {
47869
+ // The render that brought a new pristineKey read `changed` against the
47870
+ // previous baseline, and nothing else is going to move: the button would stay
47871
+ // lit on a form that holds exactly what it was just given. So ask for the one
47872
+ // render that reads the new baseline — the first one has nobody to tell,
47873
+ // every field it is waiting for re-renders the form as it registers.
47874
+ const [, rereadBaseline] = useState(0);
47875
+ const isFirstRef = useRef(true);
47605
47876
  useLayoutEffect(() => {
47606
47877
  uiStateController.sentUIState = readHeldUIState(uiStateController);
47607
- }, [uiStateController]);
47878
+ if (isFirstRef.current) {
47879
+ isFirstRef.current = false;
47880
+ return;
47881
+ }
47882
+ rereadBaseline(count => count + 1);
47883
+ }, [uiStateController, pristineKey]);
47608
47884
  };
47609
47885
  const useUnregisteredControlWarning = ref => {
47610
47886
  // No dependency array: fields appear and disappear as the form re-renders,
@@ -47914,6 +48190,29 @@ installImportMetaCssBuild(import.meta);/**
47914
48190
  * container of ours could ever hold two of them side by side and translate the
47915
48191
  * pair. One popup holding slides of its own contents has no such problem — and
47916
48192
  * it is the same component in the document, in a dialog or in a popover.
48193
+ *
48194
+ * Padding belongs on the SLIDE, never on this box nor on anything above it.
48195
+ * Overflow clips at the PADDING edge, so a padding given to the container is a
48196
+ * band the clipping does not cover: a slide travelling through it is seen there
48197
+ * before it has reached the frame. And a padding above the slides does not
48198
+ * travel — the two contents crossing each other pass with nothing between them,
48199
+ * each flush against the other, instead of arriving already inset. Put on the
48200
+ * slide, the inset moves with what it insets, and a travel shows two paddings'
48201
+ * worth of gutter between the two.
48202
+ *
48203
+ * What SCROLLS is the slide too, and for the same reason read the other way
48204
+ * round: the box is as big as its largest slide, so a scroller placed above the
48205
+ * slides is always scrolling the tallest of them — stand on a short one and it
48206
+ * carries the scrollbar of a neighbour, scrolling through emptiness. The cap on
48207
+ * the height comes from above (a max-height on the popup, a column it has to
48208
+ * fit in) and must reach the slides as a CONSTRAINT rather than as a scroller:
48209
+ * this box shrinks into it (flex: 0 1 auto below), the grid hands that height
48210
+ * to every slide, and a slide with an overflow of its own scrolls only when ITS
48211
+ * content is taller than that. The tall slide scrolls; the short ones are tall
48212
+ * boxes with a short content in them, which is exactly what one wants — they
48213
+ * take the height the context imposes and ignore the height of their neighbour.
48214
+ * So: nothing scrollable between the cap and the slides (a shared [data-body]
48215
+ * around them IS a scroller, see box.jsx), and `overflow="auto"` on each Slide.
47917
48216
  */
47918
48217
  const css$A = /* css */`
47919
48218
  /* Where the picture stands relative to the slide that is current, in boxes
@@ -51238,6 +51537,11 @@ const css$z = /* css */`
51238
51537
  * is unavoidably *more* intrusive once it switches to dialog mode than
51239
51538
  * the exact same usage would be as a popover — worth keeping in mind for
51240
51539
  * anything that relies on `Popup` and can end up on a small screen.
51540
+ * @param {"auto"|"discrete"|"none"} [props.backdropAppearance] - Forwarded
51541
+ * as-is to whichever component renders (both understand it identically):
51542
+ * how visible the backdrop is, independently of what an outside click
51543
+ * does. Unlike `pointerInteractionOutsideEffect` above, this one needs no
51544
+ * default here — `"auto"` already means the same thing on both sides.
51241
51545
  * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
51242
51546
  * - Forwarded as-is.
51243
51547
  * @param {string} [props.animationDuration] - Forwarded as-is.
@@ -51344,86 +51648,6 @@ const Popup = props => {
51344
51648
  });
51345
51649
  };
51346
51650
 
51347
- /**
51348
- * What a control HOLDS, as opposed to what it is showing.
51349
- *
51350
- * A `value` is held: the control was given it, so handing it back says nothing
51351
- * new. A `defaultValue` is only a suggestion — an age that is usually 18, a
51352
- * duration that is usually 1h30 — so the control holds nothing, and confirming
51353
- * the suggestion IS an answer ("yes, 18"). A control bound to a signal falls on
51354
- * whichever side the signal put it: a signal with something in it is an answer
51355
- * (restored from the url, set by whoever owns it), an empty one leaves the
51356
- * control on its suggestion.
51357
- *
51358
- * The same distinction Form makes across its fields (see readHeldUIState in
51359
- * form.jsx), asked of a single control — which is what lets a Picker tell "the
51360
- * user re-confirmed what was already chosen" (nothing new) from "the user
51361
- * accepted the proposal" (an answer).
51362
- */
51363
-
51364
- const isUIStateHeld = (controller) => {
51365
- if (!controller) {
51366
- return false;
51367
- }
51368
- // Given a value outright: held, whatever it is showing.
51369
- if (controller.hasStateProp || controller.hasValueProp) {
51370
- return true;
51371
- }
51372
- // A facade (a picker) shows what the control inside its popup holds, so that
51373
- // is the one to ask — the facade itself was given nothing.
51374
- const facadeChild = controller.facadeChild;
51375
- if (facadeChild) {
51376
- return isUIStateHeld(facadeChild);
51377
- }
51378
- const boundSignal = controller.props?.signal;
51379
- if (boundSignal) {
51380
- return boundSignal.value !== undefined;
51381
- }
51382
- // Uncontrolled with a suggestion: what it shows is that suggestion until it
51383
- // differs from it.
51384
- if (controller.defaultValue !== undefined) {
51385
- return !compareTwoJsValues(controller.uiState, controller.defaultValue);
51386
- }
51387
- // A group holding nothing of its own is worth what its children are: two
51388
- // wheels each on their own suggestion make a group still waiting for an
51389
- // answer, one of them moved makes a group holding one.
51390
- const childControllers = controller.getChildControllers?.() || [];
51391
- if (childControllers.length > 0) {
51392
- return childControllers.some((child) => isUIStateHeld(child));
51393
- }
51394
- return controller.uiState !== undefined;
51395
- };
51396
-
51397
- /**
51398
- * Tell a control — and everything inside it — that what it is showing is now
51399
- * the answer, without its state having to move.
51400
- *
51401
- * The state is already right; what has not happened is anyone saying so. A
51402
- * control reports an answer through `onUIAction` (that is where a bound signal
51403
- * is written, where `uiAction` fires), and a suggestion nobody touched never
51404
- * got there. Confirming a picker is exactly that moment.
51405
- *
51406
- * Down the whole subtree because that is where the answer actually lives: a
51407
- * picker holding a group of two wheels has one signal per wheel, and it is each
51408
- * wheel that has to record what it is showing. Commands are skipped — a
51409
- * `command` on a control is its reaction to being used, and this is a
51410
- * confirmation happening elsewhere, whose own command (the picker's) is already
51411
- * running.
51412
- */
51413
- const commitUIStateAsAnswer = (controller, e) => {
51414
- if (!controller) {
51415
- return;
51416
- }
51417
- const answering = controller.facadeChild || controller;
51418
- commitSubtree(answering, e);
51419
- };
51420
- const commitSubtree = (controller, e) => {
51421
- controller.onUIAction?.(e, { skipCommand: true });
51422
- for (const child of controller.getChildControllers?.() || []) {
51423
- commitSubtree(child, e);
51424
- }
51425
- };
51426
-
51427
51651
  installImportMetaCssBuild(import.meta);const css$y = /* css */`
51428
51652
  .navi_picker {
51429
51653
  /* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
@@ -52039,6 +52263,9 @@ const PickerContentInsidePopup = props => {
52039
52263
  // action if the value changed) — Escape still cancels. Pass "cancel" to make
52040
52264
  // clicking outside revert instead, or "capture" to keep it open.
52041
52265
  pointerInteractionOutsideEffect = "close",
52266
+ // Named/forwarded rather than left in ...rest: rest goes to the picker
52267
+ // element itself, not the popup, and this belongs to the popup.
52268
+ backdropAppearance,
52042
52269
  dialogExpand,
52043
52270
  dialogExpandX,
52044
52271
  dialogExpandY,
@@ -52089,6 +52316,7 @@ const PickerContentInsidePopup = props => {
52089
52316
  marginWithContainer: marginWithContainer === undefined && isPopover ? popoverSpacing : marginWithContainer,
52090
52317
  scrollCapture: scrollCapture,
52091
52318
  pointerInteractionOutsideEffect: pointerLock ? "capture" : pointerInteractionOutsideEffect,
52319
+ backdropAppearance: backdropAppearance,
52092
52320
  focusCapture: isPopover ? focusCapture : undefined,
52093
52321
  expand: isPopover ? undefined : dialogExpand,
52094
52322
  expandX: isPopover ? undefined : dialogExpandX,
@@ -53711,6 +53939,28 @@ const css$v = /* css */`
53711
53939
  --list-border-width-default: 1px;
53712
53940
  --list-border-color: light-dark(#ccc, #555);
53713
53941
  --list-background-color: light-dark(#fff, #1e1e1e);
53942
+
53943
+ /* A sticky part paints over the rows only while it IS stuck — which is
53944
+ what --navi-z-index-sticky says it is for ("kept stuck while something
53945
+ scrolls under it"). At rest it is a block in the flow with nothing
53946
+ passing under it, and a 10 there is what slices whatever a neighbouring
53947
+ row lets out of its box: a focus ring, a badge, a stamp. See
53948
+ useStuckStickyParts for the navi-stuck attribute these read.
53949
+
53950
+ With "auto" at rest, a card whose badge overflows into the label
53951
+ below it gets past it by saying z-index: 1 on that badge — a literal,
53952
+ in the card, against its own neighbour, which is what docs/z_index.md
53953
+ asks for. These variables are the escape hatch for what that cannot
53954
+ reach, not the usual answer. Mind that a negative value here is
53955
+ compared against the page: it needs a stacking context between the
53956
+ label and the nearest opaque background, or the label goes behind that
53957
+ background and disappears. */
53958
+ --list-header-z-index: auto;
53959
+ --list-header-z-index-stuck: var(--navi-z-index-sticky);
53960
+ --list-footer-z-index: auto;
53961
+ --list-footer-z-index-stuck: var(--navi-z-index-sticky);
53962
+ --list-group-label-z-index: auto;
53963
+ --list-group-label-z-index-stuck: var(--navi-z-index-sticky);
53714
53964
  }
53715
53965
  .navi_list_item {
53716
53966
  --list-item-padding-x-default: 0px;
@@ -54092,8 +54342,12 @@ const css$v = /* css */`
54092
54342
  position: sticky;
54093
54343
  top: 0;
54094
54344
  left: 0;
54095
- z-index: var(--navi-z-index-sticky);
54345
+ z-index: var(--list-header-z-index);
54096
54346
  order: -2;
54347
+
54348
+ &[navi-stuck] {
54349
+ z-index: var(--list-header-z-index-stuck);
54350
+ }
54097
54351
  }
54098
54352
  .navi_list_fallback,
54099
54353
  .navi_list_search_fallback {
@@ -54199,8 +54453,12 @@ const css$v = /* css */`
54199
54453
  position: sticky;
54200
54454
  right: 0;
54201
54455
  bottom: 0;
54202
- z-index: var(--navi-z-index-sticky);
54456
+ z-index: var(--list-footer-z-index);
54203
54457
  order: 2;
54458
+
54459
+ &[navi-stuck] {
54460
+ z-index: var(--list-footer-z-index-stuck);
54461
+ }
54204
54462
  }
54205
54463
 
54206
54464
  ::highlight(navi-search-match) {
@@ -54215,11 +54473,15 @@ const css$v = /* css */`
54215
54473
  .navi_list_item_group_label {
54216
54474
  position: sticky;
54217
54475
  top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
54218
- z-index: var(--navi-z-index-sticky);
54476
+ z-index: var(--list-group-label-z-index);
54219
54477
  display: block;
54220
54478
  background-color: var(--list-group-label-background-color);
54221
54479
  user-select: none;
54222
54480
 
54481
+ &[navi-stuck] {
54482
+ z-index: var(--list-group-label-z-index-stuck);
54483
+ }
54484
+
54223
54485
  &[navi-default] {
54224
54486
  padding: 4px 12px 2px;
54225
54487
  color: light-dark(#888, #aaa);
@@ -54528,6 +54790,7 @@ const ListUI = props => {
54528
54790
  expand: expand,
54529
54791
  "navi-nothing-to-display": nothingToDisplay ? "" : undefined,
54530
54792
  "navi-loading": loading ? "" : undefined,
54793
+ "navi-refreshing": virtual.refreshingSignal.value ? "" : undefined,
54531
54794
  "navi-error": error ? "" : undefined,
54532
54795
  styleCSSVars: LIST_STYLE_CSS_VARS,
54533
54796
  pseudoClasses: LIST_PSEUDO_CLASSES,
@@ -54837,6 +55100,7 @@ const useListScrollSync = ({
54837
55100
  };
54838
55101
  useLayoutEffect(resolveScroller);
54839
55102
  useStickyScrollportWarning();
55103
+ useStuckStickyParts(ref, getScroller, scrollerElResolved, horizontal);
54840
55104
 
54841
55105
  // The row the scroll holds onto across a change of geometry, and where it
54842
55106
  // sat when that change was decided. Captured at the two moments the list
@@ -55614,6 +55878,14 @@ const getScrollerViewportRect = scrollerEl => {
55614
55878
  }
55615
55879
  return scrollerEl.getBoundingClientRect();
55616
55880
  };
55881
+ // What a sticky part of the list sticks to is the nearest scroll container in
55882
+ // the DOM; `scroller` has no say in it. A list told the page scrolls it can
55883
+ // therefore have its group labels and its header stuck to a wrapper that never
55884
+ // scrolls — and pushed down by that wrapper's scroll-padding on top of it. The
55885
+ // usual culprit is an app wrapper carrying `overflow-x: auto` to keep the
55886
+ // document from overflowing horizontally on mobile; `overflow-x: clip` keeps
55887
+ // that guarantee without making a scroll container.
55888
+ const STICKY_LIST_PART_SELECTOR = `.navi_list_item_header, .navi_list_item_footer, .navi_list_item_group_label`;
55617
55889
  const useStickyScrollportWarning = (ref, scroller) => {
55618
55890
  useRef(false);
55619
55891
  useLayoutEffect(() => {
@@ -55623,6 +55895,142 @@ const useStickyScrollportWarning = (ref, scroller) => {
55623
55895
  });
55624
55896
  };
55625
55897
 
55898
+ /**
55899
+ * "Am I stuck?" — the question a `position: sticky` element cannot ask about
55900
+ * itself. There is no selector for it, and `scroll-state(stuck: top)` does not
55901
+ * answer it either: that query styles a container's DESCENDANTS, so a part
55902
+ * cannot read its own stuck state, which is exactly the one a background, a
55903
+ * shadow or a stacking order has to depend on.
55904
+ *
55905
+ * So the list says it, on the three parts it makes sticky: `navi-stuck` while a
55906
+ * part sits at the edge it sticks to, gone while it rides along in the flow.
55907
+ * The list is the right place for it because it is the only one that knows
55908
+ * WHICH box its parts stick to — an app writing this outside would listen to
55909
+ * the window and be right only for `scroller="document"` (see getScrollerEl,
55910
+ * and useStickyScrollportWarning for the case where even the list is wrong
55911
+ * about it: a scroll container between the two, which dev mode reports).
55912
+ *
55913
+ * What reads it is navi's own z-index rule first (see --list-*-z-index above:
55914
+ * the sticky band is for a part with something scrolling under it, not for a
55915
+ * block at rest in the flow), and an app second, for anything it wants to say
55916
+ * about a part being stuck.
55917
+ */
55918
+ // Fractional layout is the rule, not the exception — zoom, screen density, a
55919
+ // scroller at a half-pixel offset. A part at its sticky offset can render a
55920
+ // fraction short of it, and without this slack it reads as being at rest: a
55921
+ // bug that shows up on one machine and not the next.
55922
+ const STUCK_SLACK = 1;
55923
+ // Which edge a part sticks to. The header and the footer stick along whichever
55924
+ // axis the list scrolls — their rules declare both insets (top/left, and
55925
+ // bottom/right) so the same markup works either way; a group label always caps
55926
+ // its group from the top.
55927
+ const getStickyEdge = (partEl, horizontal) => {
55928
+ if (partEl.classList.contains("navi_list_item_footer")) {
55929
+ return horizontal ? "right" : "bottom";
55930
+ }
55931
+ if (partEl.classList.contains("navi_list_item_header")) {
55932
+ return horizontal ? "left" : "top";
55933
+ }
55934
+ return "top";
55935
+ };
55936
+ // A sticky inset is measured from the scrollport — the padding box of the
55937
+ // scroller, or the viewport when the page scrolls. getScrollerViewportRect
55938
+ // gives the border box; the borders come off here, since a scroller with one
55939
+ // would otherwise read as a pixel of scrolling already done.
55940
+ const getScrollportRect = scrollerEl => {
55941
+ const rect = getScrollerViewportRect(scrollerEl);
55942
+ if (scrollerEl === document.scrollingElement) {
55943
+ return rect;
55944
+ }
55945
+ const top = rect.top + scrollerEl.clientTop;
55946
+ const left = rect.left + scrollerEl.clientLeft;
55947
+ return {
55948
+ top,
55949
+ left,
55950
+ bottom: top + scrollerEl.clientHeight,
55951
+ right: left + scrollerEl.clientWidth
55952
+ };
55953
+ };
55954
+ const isPartStuck = (partEl, edge, scrollportRect) => {
55955
+ // The inset is read computed, not from the rule: --list-group-label-top and
55956
+ // the FixedBar space behind it are what put the label where it sticks.
55957
+ const declared = parseFloat(getComputedStyle(partEl)[edge]);
55958
+ const inset = Number.isFinite(declared) ? declared : 0;
55959
+ const rect = partEl.getBoundingClientRect();
55960
+ if (edge === "top") {
55961
+ return rect.top - scrollportRect.top <= inset + STUCK_SLACK;
55962
+ }
55963
+ if (edge === "left") {
55964
+ return rect.left - scrollportRect.left <= inset + STUCK_SLACK;
55965
+ }
55966
+ if (edge === "bottom") {
55967
+ return scrollportRect.bottom - rect.bottom <= inset + STUCK_SLACK;
55968
+ }
55969
+ return scrollportRect.right - rect.right <= inset + STUCK_SLACK;
55970
+ };
55971
+ const useStuckStickyParts = (ref, getScroller, scrollerElResolved, horizontal) => {
55972
+ // Rewritten on every render so the listeners below, registered once per
55973
+ // scroller, always run against the current geometry.
55974
+ const updateRef = useRef(null);
55975
+ updateRef.current = () => {
55976
+ const listContainerEl = ref.current;
55977
+ if (!listContainerEl) {
55978
+ return;
55979
+ }
55980
+ const partEls = listContainerEl.querySelectorAll(STICKY_LIST_PART_SELECTOR);
55981
+ if (partEls.length === 0) {
55982
+ return;
55983
+ }
55984
+ const scrollerEl = getScroller();
55985
+ if (!scrollerEl) {
55986
+ return;
55987
+ }
55988
+ // One rect per RENDERED part: virtualization already bounds how many of
55989
+ // them exist, which is what keeps this affordable on every scroll event.
55990
+ const scrollportRect = getScrollportRect(scrollerEl);
55991
+ for (const partEl of partEls) {
55992
+ const edge = getStickyEdge(partEl, horizontal);
55993
+ partEl.toggleAttribute("navi-stuck", isPartStuck(partEl, edge, scrollportRect));
55994
+ }
55995
+ };
55996
+
55997
+ // Every commit, because a virtualized list changes which parts exist without
55998
+ // anything scrolling: group labels enter and leave the DOM as the window
55999
+ // moves, and one that arrives already at the edge has never been measured.
56000
+ useLayoutEffect(() => {
56001
+ updateRef.current();
56002
+ });
56003
+ useLayoutEffect(() => {
56004
+ const listContainerEl = ref.current;
56005
+ if (!listContainerEl) {
56006
+ return undefined;
56007
+ }
56008
+ const update = () => {
56009
+ // Synchronously, not on a rAF: scroll events are dispatched while the
56010
+ // frame is being put together, so the attribute lands in the same paint
56011
+ // as the scroll that caused it. A frame late is a frame of flicker.
56012
+ updateRef.current();
56013
+ };
56014
+ // A page-level scroller does not emit "scroll" on the element itself
56015
+ // (document.scrollingElement); the document does.
56016
+ const scrollerEl = getScroller();
56017
+ const scrollEventTarget = !scrollerEl || scrollerEl === document.scrollingElement ? document : scrollerEl;
56018
+ scrollEventTarget.addEventListener("scroll", update, {
56019
+ passive: true
56020
+ });
56021
+ window.addEventListener("resize", update);
56022
+ // The list growing under a scroller that has not moved — rows loaded by
56023
+ // scroll, a group unfolding — changes which parts sit at an edge.
56024
+ const observer = new ResizeObserver(update);
56025
+ observer.observe(listContainerEl);
56026
+ return () => {
56027
+ scrollEventTarget.removeEventListener("scroll", update);
56028
+ window.removeEventListener("resize", update);
56029
+ observer.disconnect();
56030
+ };
56031
+ }, [scrollerElResolved]);
56032
+ };
56033
+
55626
56034
  // The CSS needs to tell "the page scrolls me" from "some box around me
55627
56035
  // scrolls me": only the first one sticks to the viewport, where the fixed bars
55628
56036
  // are.
@@ -56707,6 +57115,10 @@ const createListVirtual = () => {
56707
57115
  // registers, nothing is drawn), and yet they are what it may have been
56708
57116
  // waiting for — the row it was told to open on, for one.
56709
57117
  const pagesSignal = signal(0);
57118
+ // How many runs are re-reading rows they already show. The list wears it as
57119
+ // an attribute: what is drawn is from before, and the app may want to say so
57120
+ // without taking anything away.
57121
+ const refreshingSignal = signal(0);
56710
57122
  const placeByOwner = new Map();
56711
57123
  const locatorByOwner = new Map();
56712
57124
  let passId = 0;
@@ -56714,6 +57126,7 @@ const createListVirtual = () => {
56714
57126
  const virtual = {
56715
57127
  totalSignal,
56716
57128
  pagesSignal,
57129
+ refreshingSignal,
56717
57130
  // What a run needs to know about the list it lives in: how many rows the
56718
57131
  // list is willing to draw at once, which end it opens on, and how much
56719
57132
  // room one row is given — a row whose content has not arrived must take
@@ -56816,7 +57229,7 @@ const VISIBILITY_HIDDEN_STYLE = {
56816
57229
  * inside `<List.Group>`s; each takes its place in declaration order.
56817
57230
  *
56818
57231
  * @type {import("ignore:preact").FunctionComponent<{
56819
- * renderItem: (item: any, index: number) => import("ignore:preact").ComponentChildren,
57232
+ * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
56820
57233
  * itemsAction: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
56821
57234
  * count?: number,
56822
57235
  * groupBy?: (item: any, index: number) => any,
@@ -56826,6 +57239,10 @@ const VISIBILITY_HIDDEN_STYLE = {
56826
57239
  * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
56827
57240
  * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
56828
57241
  * }>}
57242
+ * @param {(item: any, index: number, state: {refreshing: boolean}) => any} props.renderItem
57243
+ * What one row is, given the item and where it sits. `state.refreshing` says
57244
+ * the rows drawn are the ones from before while the run reads the collection
57245
+ * again — the list carries `navi-refreshing` for the same reason.
56829
57246
  * @param {(item: any, index: number) => any} [props.groupBy]
56830
57247
  * What tells rows that belong together apart from the others — the day of a
56831
57248
  * message, the month of a game. Consecutive rows sharing it are wrapped in a
@@ -57032,6 +57449,9 @@ const ListItems = ({
57032
57449
  virtualItemSize: virtualItemSize
57033
57450
  }, "navi-list-filler-before"));
57034
57451
  }
57452
+ const renderItemState = {
57453
+ refreshing: store.refreshing
57454
+ };
57035
57455
  let rowIndex = windowFrom;
57036
57456
  while (rowIndex < windowTo) {
57037
57457
  if (rowIndex >= failureFrom && rowIndex <= failureTo) {
@@ -57059,7 +57479,7 @@ const ListItems = ({
57059
57479
  const key = item === undefined ? `${ownerId}_skeleton_${rowIndex}` : idOf(item, rowIndex);
57060
57480
  let rowVnode;
57061
57481
  if (item !== undefined) {
57062
- rowVnode = renderItem(item, rowIndex);
57482
+ rowVnode = renderItem(item, rowIndex, renderItemState);
57063
57483
  } else if (renderRowSkeleton === false) {
57064
57484
  // The row must still take its room: without it the rows below would
57065
57485
  // climb up and slide back down as the answer arrives.
@@ -57160,6 +57580,22 @@ const useItemStore = ({
57160
57580
  }
57161
57581
  const pages = pagesRef.current;
57162
57582
  const [, setPageVersion] = useState(0);
57583
+ // The rows held are out of date and the run has not asked for the new ones
57584
+ // yet. They stay on screen until the answer comes: what is drawn is from
57585
+ // before, which is not the same thing as nothing to draw.
57586
+ const staleRef = useRef(false);
57587
+ const [refreshing, setRefreshing] = useState(false);
57588
+ // A source that says when what it reads has moved (a resource range reader
57589
+ // does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
57590
+ // to the collection is exactly what a run cannot deduce from the rows it
57591
+ // holds. A source that says nothing is read once and stays as it is.
57592
+ const invalidationSignal = typeof itemsAction === "function" ? itemsAction.invalidationSignal : null;
57593
+ const invalidation = invalidationSignal ? invalidationSignal.value : 0;
57594
+ const invalidationRef = useRef(invalidation);
57595
+ if (invalidationRef.current !== invalidation) {
57596
+ invalidationRef.current = invalidation;
57597
+ staleRef.current = true;
57598
+ }
57163
57599
  // The one request in flight, with the means to call it off: a page asked for
57164
57600
  // a window the list has left is work the server and the browser are doing for
57165
57601
  // nothing.
@@ -57169,7 +57605,8 @@ const useItemStore = ({
57169
57605
  end: -1,
57170
57606
  held: -1,
57171
57607
  controller: null,
57172
- generation: 0
57608
+ generation: 0,
57609
+ revalidating: false
57173
57610
  });
57174
57611
  // The rows asked for that never came. Kept as a range so the list can say
57175
57612
  // where the hole is, and cleared by a retry — which is what makes the same
@@ -57180,9 +57617,24 @@ const useItemStore = ({
57180
57617
  // It stands for a windowful of them: a list that is about to be filled looks
57181
57618
  // like rows on their way, not like an empty list.
57182
57619
  const rowCount = pages.count ?? count ?? virtual.renderBudget;
57620
+ // A run that never received anything has nothing to keep on screen: asking
57621
+ // again is its first ask, not a refresh.
57622
+ if (staleRef.current && pages.count === undefined) {
57623
+ staleRef.current = false;
57624
+ }
57625
+ useLayoutEffect(() => {
57626
+ if (!refreshing) {
57627
+ return null;
57628
+ }
57629
+ virtual.refreshingSignal.value = virtual.refreshingSignal.peek() + 1;
57630
+ return () => {
57631
+ virtual.refreshingSignal.value = virtual.refreshingSignal.peek() - 1;
57632
+ };
57633
+ }, [refreshing]);
57183
57634
  const store = {
57184
57635
  rowCount,
57185
57636
  failure,
57637
+ refreshing,
57186
57638
  // JS memory is cheap next to the DOM, but a long enough scroll accumulates
57187
57639
  // everything it ever went through. Rows far from what is on screen are
57188
57640
  // dropped and simply asked for again if the user goes back — the same
@@ -57223,7 +57675,25 @@ const useItemStore = ({
57223
57675
  let start = missingStart;
57224
57676
  let end = missingEnd;
57225
57677
  let around;
57226
- if (pages.count === undefined) {
57678
+ // Rows that are all there but out of date: the ask is the window itself,
57679
+ // anchored on the row at its top — a source paginating by cursor gets a
57680
+ // row to count from, and the reading position is what must survive.
57681
+ const revalidating = staleRef.current;
57682
+ if (revalidating) {
57683
+ start = windowFrom;
57684
+ end = windowTo - 1;
57685
+ if (end < start) {
57686
+ // Nothing of this run is on screen (the window frames another one, or
57687
+ // the list is scrolled past it): its own first rows are what it will
57688
+ // draw next.
57689
+ start = 0;
57690
+ end = budget - 1;
57691
+ }
57692
+ const firstHeld = pages.byIndex.get(windowFrom);
57693
+ if (firstHeld && firstHeld.id !== undefined) {
57694
+ around = firstHeld.id;
57695
+ }
57696
+ } else if (pages.count === undefined) {
57227
57697
  const scrolled = virtual.scrolled;
57228
57698
  if (scrolled === "end") {
57229
57699
  // Counting back from the end, the way an HTTP range does: a list
@@ -57254,6 +57724,14 @@ const useItemStore = ({
57254
57724
  return;
57255
57725
  }
57256
57726
  const request = requestRef.current;
57727
+ if (revalidating && request.busy) {
57728
+ if (request.revalidating) {
57729
+ return;
57730
+ }
57731
+ // A page for a window that is about to be replaced wholesale.
57732
+ request.controller?.abort();
57733
+ request.busy = false;
57734
+ }
57257
57735
  if (request.busy) {
57258
57736
  // Still worth waiting for as long as what it went to fetch is still
57259
57737
  // what the list would draw. Once it is not, it is called off — and
@@ -57272,8 +57750,9 @@ const useItemStore = ({
57272
57750
  }
57273
57751
  const held = pages.byIndex.size;
57274
57752
  // Asking again for a range that was already asked for, having received
57275
- // nothing since, can only produce the same answer.
57276
- if (request.start === start && request.end === end && request.held === held) {
57753
+ // nothing since, can only produce the same answer. A revalidation is
57754
+ // exactly the case where it produces another one.
57755
+ if (!revalidating && request.start === start && request.end === end && request.held === held) {
57277
57756
  return;
57278
57757
  }
57279
57758
  request.start = start;
@@ -57288,17 +57767,33 @@ const useItemStore = ({
57288
57767
  end,
57289
57768
  around,
57290
57769
  limit: end - start + 1,
57291
- before: cursor.before,
57292
- after: cursor.after,
57770
+ // A cursor names a row of the collection as it was; a revalidation
57771
+ // is asked precisely because that is what changed.
57772
+ before: revalidating ? undefined : cursor.before,
57773
+ after: revalidating ? undefined : cursor.after,
57293
57774
  count: pages.count,
57294
57775
  signal: controller.signal
57295
57776
  };
57296
57777
  request.busy = true;
57778
+ request.revalidating = revalidating;
57779
+ if (revalidating) {
57780
+ setRefreshing(true);
57781
+ }
57297
57782
  const done = page => {
57298
57783
  const current = generation === request.generation;
57299
57784
  if (current) {
57300
57785
  request.busy = false;
57786
+ request.revalidating = false;
57301
57787
  setFailure(null);
57788
+ if (revalidating) {
57789
+ staleRef.current = false;
57790
+ setRefreshing(false);
57791
+ }
57792
+ }
57793
+ if (revalidating && !current) {
57794
+ // Rows of a composition already superseded by a newer ask: keeping
57795
+ // them would mix two states of the collection.
57796
+ return;
57302
57797
  }
57303
57798
  if (!page) {
57304
57799
  return;
@@ -57309,6 +57804,12 @@ const useItemStore = ({
57309
57804
  // Before the rows land: what is on screen has to stay where it is,
57310
57805
  // and the DOM still shows the state to hold onto.
57311
57806
  virtual.captureAnchor();
57807
+ if (revalidating) {
57808
+ // The rows held stood for a composition that has moved on; the
57809
+ // ones outside the window are forgotten and asked for again if the
57810
+ // user goes back to them.
57811
+ pages.byIndex = new Map();
57812
+ }
57312
57813
  let i = 0;
57313
57814
  while (i < pageItems.length) {
57314
57815
  pages.byIndex.set(pageStart + i, pageItems[i]);
@@ -57324,6 +57825,14 @@ const useItemStore = ({
57324
57825
  return;
57325
57826
  }
57326
57827
  request.busy = false;
57828
+ request.revalidating = false;
57829
+ if (revalidating) {
57830
+ // The rows from before stay: a revalidation that failed has
57831
+ // nothing better to put in their place.
57832
+ staleRef.current = false;
57833
+ setRefreshing(false);
57834
+ return;
57835
+ }
57327
57836
  setFailure({
57328
57837
  start,
57329
57838
  end,
@@ -57928,8 +58437,8 @@ const PickerTypeResolver = props => {
57928
58437
  ...props
57929
58438
  });
57930
58439
  }
57931
- if (props.type === "form") {
57932
- return jsx(PickerForm, {
58440
+ if (props.type === "object") {
58441
+ return jsx(PickerObject, {
57933
58442
  ...props
57934
58443
  });
57935
58444
  }
@@ -57945,19 +58454,19 @@ const PickerText = props => {
57945
58454
  });
57946
58455
  };
57947
58456
 
57948
- // The popup holds a group of named controls — a `<Form>`, or a `<ControlGroup>`
57949
- // when the group is only a shape and has no submit — and this picker's value is
57950
- // whatever that group aggregates. The popup itself holds nothing: it is a
57951
- // surface (see dialog.jsx), so there is nothing to tell it about the shape.
57952
- const PickerForm = props => {
58457
+ // The popup holds a group of named controls — a `<ControlGroup>`, or a `<Form>`
58458
+ // when that group is a question with a send of its own — and this picker's
58459
+ // value is the object that group aggregates. The popup itself holds nothing: it
58460
+ // is a surface (see dialog.jsx), so there is nothing to tell it about the shape.
58461
+ const PickerObject = props => {
57953
58462
  const Next = useNextResolver();
57954
58463
  return jsx(Next, {
57955
- ui: jsx(PickerFormUI, {}),
58464
+ ui: jsx(PickerObjectUI, {}),
57956
58465
  ...props,
57957
58466
  type: "navi_js"
57958
58467
  });
57959
58468
  };
57960
- const PickerFormUI = () => {
58469
+ const PickerObjectUI = () => {
57961
58470
  const {
57962
58471
  value,
57963
58472
  placeholder
@@ -58958,110 +59467,6 @@ const PickerFirstResolver = props => {
58958
59467
  ...props
58959
59468
  });
58960
59469
  };
58961
-
58962
- /**
58963
- * Button-like trigger that opens a picker (native or custom popup) when clicked.
58964
- *
58965
- * Without `children`, opens the browser-native picker for the given `type`.
58966
- * With `children`, opens a popover (desktop) or dialog (mobile) containing the children.
58967
- * Pass `mode="popover"` or `mode="dialog"` to override the automatic choice.
58968
- *
58969
- * @type {import("ignore:preact").FunctionComponent<{
58970
- * type?: "date" | "month" | "week" | "time" | "datetime" | "color" | "hour" | "navi_time" | "navi_number" | "navi_percentage",
58971
- * value?: any,
58972
- * defaultValue?: any,
58973
- * name?: string,
58974
- * placeholder?: import("ignore:preact").ComponentChildren,
58975
- * required?: boolean,
58976
- * min?: Date | string | number,
58977
- * max?: Date | string | number,
58978
- * step?: string | number,
58979
- * disabled?: boolean,
58980
- * readOnly?: boolean,
58981
- * error?: boolean | string,
58982
- * uiAction?: (value: any, event: Event) => void,
58983
- * action?: (value: any, event: Event) => void,
58984
- * children?: import("ignore:preact").ComponentChildren,
58985
- * mode?: "popover" | "dialog",
58986
- * popoverMode?: "nearby" | "overlay",
58987
- * positionArea?: string,
58988
- * popupWidthFitContent?: boolean,
58989
- * variant?: "icon" | "headless" | "discrete",
58990
- * rightSlotIcon?: import("ignore:preact").ComponentChildren,
58991
- * rightSlotIconSize?: number | string,
58992
- * maxLines?: number,
58993
- * slotSpacing?: number | string,
58994
- * popoverMaxHeight?: number | string,
58995
- * dialogMaxWidth?: number | string,
58996
- * dialogMaxHeight?: number | string,
58997
- * popupBackgroundColor?: string,
58998
- * popupBorderRadius?: number | string,
58999
- * clearable?: boolean,
59000
- * popupLayer?: "top" | "local",
59001
- * dialogExpand?: boolean,
59002
- * dialogExpandX?: boolean,
59003
- * dialogExpandY?: boolean,
59004
- * marginWithContainer?: number | string,
59005
- * escapeEffect?: "cancel" | "close",
59006
- * pointerInteractionOutsideEffect?: "close" | "cancel" | "capture",
59007
- * ref?: import("ignore:preact").RefObject<HTMLElement>,
59008
- * [key: string]: any,
59009
- * }>}
59010
- * @param {boolean|string} [error] Something went wrong around this picker (its
59011
- * content failed to load, its value could not be resolved…). Shown as a
59012
- * callout on the trigger, open or closed — the caller has nothing to place.
59013
- * Dismissing it discards that error; a new `error` value raises another one.
59014
- * @param {"nearby"|"overlay"} [popoverMode="nearby"] "overlay" lays the popover
59015
- * over the trigger, "nearby" leaves a small gap below it.
59016
- * @param {string} [positionArea] Where the popup goes — relative to the trigger
59017
- * in popover mode, relative to the viewport in dialog mode. Same grammar as
59018
- * Popover/Dialog's own `positionArea` ("top", "right-end", "inset(top-left)",
59019
- * …). Defaults to "bottom-start" in popover mode ("inset(top-left)" when
59020
- * popoverMode is "overlay"), and to Dialog's own "center" in dialog mode. A
59021
- * popover still flips to the opposite side on its own when there isn't
59022
- * enough room.
59023
- * @param {boolean} [popupWidthFitContent] By default the popup is at least as
59024
- * wide as the trigger. Set this to let the content size it instead, so a
59025
- * popup narrower than the trigger stays narrow.
59026
- * @param {import("ignore:preact").ComponentChildren} [rightSlotIcon] What the right
59027
- * slot draws in place of the chevron. It is the whole slot, not an addition
59028
- * to it: the picker then no longer says on its own that it opens, so pass
59029
- * something that does.
59030
- * @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
59031
- * right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
59032
- * cross. "inherit" takes the picker's own font size.
59033
- * @param {number|string} [slotSpacing] Gap kept between what sits in the right
59034
- * slot (the chevron, or the clear button) and the picker's own edge — same
59035
- * prop name Input uses for its own slots. Accepts a spacing token ("s",
59036
- * "m"…) like any other spacing prop, or a length. Defaults to half the
59037
- * horizontal padding.
59038
- * @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
59039
- * (default 300px). The popover shrinks below it when space is tight.
59040
- * @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
59041
- * one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
59042
- * dialog a large sheet rather than a full screen. Capped in turn by the
59043
- * container minus `marginWithContainer`.
59044
- * @param {number|string} [dialogMaxHeight] Same, on the height.
59045
- * @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
59046
- * picker. "cancel" puts back the value the picker had at open, and a dialog
59047
- * picker also goes back in history — so anything written to the url while it
59048
- * was open (a route `stateSignal`, a search param) goes back with it. "close"
59049
- * makes Escape say what clicking outside says: keep what was chosen, close —
59050
- * a last resort, see docs/popup_open.md ("Escape cancels, the other gestures
59051
- * keep") for why Escape should go on meaning cancel, and for what the value
59052
- * at open is on the picker's very first open.
59053
- * @param {"close"|"cancel"|"capture"} [pointerInteractionOutsideEffect="close"]
59054
- * What a click outside the popup does: close and keep ("close"), close and
59055
- * put back the value at open ("cancel"), or nothing at all ("capture"). The
59056
- * default is what gives a popup with no confirm button its way out that
59057
- * keeps — see the same section.
59058
- * @param {number|string} [marginWithContainer] Minimum gap kept between the
59059
- * popup and the edges of what contains it (the viewport, or the picker's own
59060
- * positioned ancestor for `popupLayer="local"`). Caps the popup's size as
59061
- * well as its placement, so what an expanded dialog leaves visible around
59062
- * itself is set here. Defaults to `popoverSpacing` in popover mode, and to
59063
- * Dialog's own 3vvw in dialog mode.
59064
- */
59065
59470
  const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
59066
59471
  Picker.UI = PickerDefaultUI;
59067
59472
  Picker.UI.Date = PickerDateUI;
@@ -59071,7 +59476,7 @@ Picker.UI.Week = PickerWeekUI;
59071
59476
  Picker.UI.Datetime = PickerDatetimeUI;
59072
59477
  Picker.UI.File = PickerFileUI;
59073
59478
  Picker.UI.Color = PickerColorUI;
59074
- Picker.UI.Form = PickerFormUI;
59479
+ Picker.UI.Object = PickerObjectUI;
59075
59480
  Picker.UI.Multiple = PickerArrayUI;
59076
59481
  Picker.UI.PencilSvg = PencilSvg;
59077
59482
  Picker.UI.ChevronDownSvg = ChevronDownSvg$1;