@jsenv/navi 0.29.89 → 0.29.90

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.
@@ -24747,9 +24747,6 @@ const useUIGroupStateController = (
24747
24747
  if (!shouldPropagateStateToChild(childUIStateController)) {
24748
24748
  return;
24749
24749
  }
24750
- if (childUIStateController.hasStateProp) {
24751
- return;
24752
- }
24753
24750
  const childNewState = resolvedDistributeChildUIState(
24754
24751
  groupUIState,
24755
24752
  childUIStateController,
@@ -24757,6 +24754,22 @@ const useUIGroupStateController = (
24757
24754
  if (childNewState === CANNOT_DERIVE) {
24758
24755
  return;
24759
24756
  }
24757
+ if (
24758
+ childUIStateController.hasStateProp &&
24759
+ !childUIStateController.props.signal
24760
+ ) {
24761
+ // A child bound to a signal is placed like any other: bound is not
24762
+ // frozen, and the placement writes the signal, so both ends keep
24763
+ // saying the same thing. Only a child controlled by a `value` /
24764
+ // `checked` prop cannot be moved — its owner decides. Worth saying
24765
+ // out loud only when the two disagree: a child already showing what
24766
+ // the group would put there has lost nothing, and both being fed
24767
+ // from the same value is a legitimate way to write a group.
24768
+ if (
24769
+ !compareTwoJsValues(childNewState, childUIStateController.uiState)
24770
+ ) ;
24771
+ return;
24772
+ }
24760
24773
  childUIStateController.setUIState(childNewState, e);
24761
24774
  },
24762
24775
  setUIState: (newUIState, e) => {
@@ -25447,6 +25460,11 @@ const PROPAGATE_DOWN_EVENT_SET = new Set([
25447
25460
  "propagate_down_set_ui_state",
25448
25461
  "propagate_down_reset_ui_state",
25449
25462
  "propagate_down_clear_ui_state",
25463
+ // The FIRST value handed down is one too: a control placed as it registers
25464
+ // (a group filling a child that just arrived, a picker filling its popup)
25465
+ // holds it from that moment, and a signal that kept saying nothing would have
25466
+ // the app and the screen disagree from the very first paint.
25467
+ "initial_state_push",
25450
25468
  ]);
25451
25469
  const isPropagateDownEvent = (e) => {
25452
25470
  return PROPAGATE_DOWN_EVENT_SET.has(e.type);
@@ -28092,6 +28110,7 @@ const createOpenController = (
28092
28110
  // Last: the close effects above are what starts the exit transition the
28093
28111
  // content must outlive (see popup_content_mount.js).
28094
28112
  controller.unmountContent?.();
28113
+ controller.onOpenedChange?.(false);
28095
28114
  };
28096
28115
  const controller = {
28097
28116
  opened: false,
@@ -28110,6 +28129,11 @@ const createOpenController = (
28110
28129
  // The counterpart, set only when the popup was told to throw its content
28111
28130
  // away on close (`unmountWhenClosed`). Called from performClose above.
28112
28131
  unmountContent: null,
28132
+ // Told whenever `opened` actually changes, whatever asked for it — an
28133
+ // interaction, a command, a prop. What lets a `signal` prop reflect the
28134
+ // popup's real state (see useOpenPropsEffectOnOpenController), called once
28135
+ // the open/close has fully happened rather than mid-sequence.
28136
+ onOpenedChange: null,
28113
28137
  open: (e, detail) => {
28114
28138
  if (controller.opened || !controller.openEffect) {
28115
28139
  return;
@@ -28212,6 +28236,7 @@ const createOpenController = (
28212
28236
  openEffectReturnValue?.(closeEvent);
28213
28237
  };
28214
28238
  closeHandlers = openHandler(requestOpenEvent) || null;
28239
+ controller.onOpenedChange?.(true);
28215
28240
  },
28216
28241
  requestClose: (
28217
28242
  e = new CustomEvent("programmatic", { detail: {} }),
@@ -28340,10 +28365,23 @@ const scheduleMountOpen = (run) => {
28340
28365
  * `requestOpen`/`requestClose` wrappers).
28341
28366
  *
28342
28367
  * @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
28343
- * @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction" }} props
28368
+ * @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean> }} props
28344
28369
  */
28345
28370
  const useOpenPropsEffectOnOpenController = (openController, props) => {
28346
- const { open, defaultOpen } = props;
28371
+ const { signal, defaultOpen } = props;
28372
+ // What the caller holds, however they hold it: an `open` they re-render
28373
+ // themselves, or a `signal` this hook also writes (see onOpenedChange below).
28374
+ // Reading .value during render is what subscribes the popup to it.
28375
+ const open = signal ? signal.value : props.open;
28376
+ // Assigned on every render, like openEffect, so it always closes over the
28377
+ // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
28378
+ // --navi-close command) writes what happened into the signal, so whoever
28379
+ // holds it always reads where the popup is.
28380
+ openController.onOpenedChange = signal
28381
+ ? (opened) => {
28382
+ signal.value = opened;
28383
+ }
28384
+ : null;
28347
28385
  // Tracks whether the effect below has ever run before — only the very
28348
28386
  // first run gets the "mount already open" treatment (`open` truthy from
28349
28387
  // the start, or the uncontrolled, mount-only `defaultOpen`); every
@@ -28396,6 +28434,12 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
28396
28434
  { isCancel: true },
28397
28435
  );
28398
28436
  }
28437
+ if (signal) {
28438
+ // The request can be refused (a busy form denying the close): the popup
28439
+ // then stays where it was, and the signal is told so — otherwise it
28440
+ // would keep saying "closed" about a popup still open.
28441
+ signal.value = openController.opened;
28442
+ }
28399
28443
  }, [open]);
28400
28444
  };
28401
28445
 
@@ -29820,6 +29864,14 @@ const css$X = /* css */`
29820
29864
  * the focus only leaves it for something that asked by name (`autoFocus` on
29821
29865
  * that element, which outranks whatever the dialog says).
29822
29866
  * @param {boolean} [props.open] - Controlled open state.
29867
+ * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
29868
+ * state said the way every navi control says it: the dialog opens and closes
29869
+ * to match the signal, and writes into it whenever it opens or closes on its
29870
+ * own (Escape, backdrop, a --navi-close command) — one binding to both drive
29871
+ * the dialog and know where it is, and the state stays where the app put it.
29872
+ * Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
29873
+ * mount behaves like `defaultOpen`: the dialog was already open, no entrance
29874
+ * plays.
29823
29875
  * @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
29824
29876
  * initial open state. `true` plays no entrance animation: the dialog was
29825
29877
  * already open when the page appeared, and nothing was ever shown as "closed"
@@ -29902,6 +29954,7 @@ const UncontrolledDialog = props => {
29902
29954
  return jsx(ControlledDialog, {
29903
29955
  ...props,
29904
29956
  open: undefined,
29957
+ signal: undefined,
29905
29958
  defaultOpen: undefined,
29906
29959
  onClose: undefined,
29907
29960
  openController: openController,
@@ -31259,6 +31312,14 @@ const css$W = /* css */`
31259
31312
  * the focus only leaves it for something that asked by name (`autoFocus` on
31260
31313
  * that element, which outranks whatever the popover says).
31261
31314
  * @param {boolean} [props.open] - Controlled open state.
31315
+ * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
31316
+ * state said the way every navi control says it: the popover opens and closes
31317
+ * to match the signal, and writes into it whenever it opens or closes on its
31318
+ * own (Escape, light dismiss, a --navi-close command) — one binding to both
31319
+ * drive the popover and know where it is, and the state stays where the app
31320
+ * put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
31321
+ * `true` at mount behaves like `defaultOpen`: the popover was already open,
31322
+ * no entrance plays.
31262
31323
  * @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
31263
31324
  * initial open state. `true` plays no entrance animation: the popover was
31264
31325
  * already open when the page appeared, and nothing was ever shown as "closed"
@@ -31339,6 +31400,7 @@ const UncontrolledPopover = props => {
31339
31400
  return jsx(ControlledPopover, {
31340
31401
  ...props,
31341
31402
  open: undefined,
31403
+ signal: undefined,
31342
31404
  defaultOpen: undefined,
31343
31405
  onClose: undefined,
31344
31406
  openController: openController,
@@ -54163,6 +54225,9 @@ const css$A = /* css */`
54163
54225
  * to `--dialog-maxmax-height` (`data-expand-y`).
54164
54226
  * @param {boolean} [props.scrollCapture] - Forwarded as-is.
54165
54227
  * @param {boolean} [props.open] - Forwarded as-is (controlled).
54228
+ * @param {import("@preact/signals").Signal<boolean>} [props.signal] -
54229
+ * Forwarded as-is: one binding to both drive the popup's open state and
54230
+ * know where it is (see `Dialog`/`Popover`'s own `signal`).
54166
54231
  * @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
54167
54232
  * mount-only).
54168
54233
  * @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
@@ -74247,6 +74312,12 @@ const css = /* css */`
74247
74312
  * @param {object} props
74248
74313
  * @param {boolean} [props.open] - Controlled open state, forwarded as-is to
74249
74314
  * `Popup`'s own `open`.
74315
+ * @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
74316
+ * state said the way every navi control says it: the panel opens and closes
74317
+ * to match the signal, and writes into it whenever it opens or closes on its
74318
+ * own (Escape, swipe, a --navi-close command) — one binding to both drive
74319
+ * the panel and know where it is. Forwarded as-is to `Popup`; excludes
74320
+ * `open` (see `Dialog`/`Popover`'s own `signal`).
74250
74321
  * @param {boolean} [props.defaultOpen] - Uncontrolled, mount-only initial
74251
74322
  * open state, forwarded as-is to `Popup`. Neither this nor `open` is
74252
74323
  * required at all for a purely command-driven panel (an `id` plus a
@@ -74313,6 +74384,7 @@ const css = /* css */`
74313
74384
  */
74314
74385
  const SidePanel = ({
74315
74386
  open,
74387
+ signal,
74316
74388
  defaultOpen,
74317
74389
  onClose,
74318
74390
  children,
@@ -74334,6 +74406,7 @@ const SidePanel = ({
74334
74406
  return jsx(Popup, {
74335
74407
  mode: mode,
74336
74408
  open: open,
74409
+ signal: signal,
74337
74410
  defaultOpen: defaultOpen,
74338
74411
  onClose: onClose,
74339
74412
  layer: layer,