@jsenv/navi 0.29.485 → 0.29.487

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.
@@ -5420,7 +5420,13 @@ const MessagePropsRefContext = createContext();
5420
5420
  const ControlIdContext = createContext();
5421
5421
  const ControlNameContext = createContext();
5422
5422
  const DisabledContext = createContext();
5423
+ // Truthy when what is above is read-only, and "network-policy" when that is a
5424
+ // write held by the network policy: what it holds is held in turn, and says
5425
+ // so in the policy's words (see readOnlyHandedDown in control_hooks.jsx).
5423
5426
  const ReadOnlyContext = createContext();
5427
+ // The words that go with it, when what handed read-only down gave some (its
5428
+ // readOnlyMessage, or the ones it was handed in turn).
5429
+ const ReadOnlyMessageContext = createContext();
5424
5430
  const RequiredContext = createContext();
5425
5431
  const LoadingContext$1 = createContext();
5426
5432
  createContext();
@@ -11489,6 +11495,31 @@ const resolveEffectiveDocumentState = (
11489
11495
  return { ...state, ...sharedState };
11490
11496
  };
11491
11497
 
11498
+ /**
11499
+ * What a press on a link handed to the navigation it starts (see the
11500
+ * integrations' `carryByLinkNavigation`): each write is an `editState` applied
11501
+ * to the entry that navigation writes, and an `otherwise` that makes the write
11502
+ * where it would have been made, for a press that starts no navigation here.
11503
+ */
11504
+ const applyPressWrites = (state, pressWrites) => {
11505
+ if (!pressWrites) {
11506
+ return state;
11507
+ }
11508
+ let stateWritten = state;
11509
+ for (const { editState } of pressWrites) {
11510
+ stateWritten = editState(stateWritten);
11511
+ }
11512
+ return stateWritten;
11513
+ };
11514
+ const writePressWritesHere = (pressWrites) => {
11515
+ if (!pressWrites) {
11516
+ return;
11517
+ }
11518
+ for (const { otherwise } of pressWrites) {
11519
+ otherwise();
11520
+ }
11521
+ };
11522
+
11492
11523
  // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
11493
11524
  // mask is derived from render order within the nearest root/async boundary:
11494
11525
  // stable across re-renders of the same mount, but not across two mounts on the
@@ -13133,6 +13164,7 @@ const setupBrowserIntegrationViaHistory = ({
13133
13164
  // Asked to go where we already are: an address that redirects is never
13134
13165
  // the one being displayed, so there is nothing to go to and nothing to
13135
13166
  // stack on the history.
13167
+ writePressWritesHere(options.pressWrites);
13136
13168
  return undefined;
13137
13169
  }
13138
13170
  return handleRoutingTask(redirectionUrl, {
@@ -13144,10 +13176,13 @@ const setupBrowserIntegrationViaHistory = ({
13144
13176
  // Decided before anything is announced: an elided push IS the traversal it
13145
13177
  // becomes, and the traversal will make its own announcements when the
13146
13178
  // browser answers — a before/after cycle here would be about a navigation
13147
- // that never happens.
13179
+ // that never happens. A push with something of its own for its entry — a
13180
+ // state, what the press handed over (see carryByLinkNavigation) — is a
13181
+ // new entry, never one standing there already.
13148
13182
  if (
13149
13183
  options.navigationType === "push" &&
13150
13184
  options.state === undefined &&
13185
+ !options.pressWrites &&
13151
13186
  url !== readAddress()
13152
13187
  ) {
13153
13188
  const delta = adjacentEntryDelta(url);
@@ -13197,6 +13232,7 @@ const setupBrowserIntegrationViaHistory = ({
13197
13232
  redirected,
13198
13233
  landOn,
13199
13234
  debounce,
13235
+ pressWrites,
13200
13236
  } = options;
13201
13237
  let { state } = options;
13202
13238
 
@@ -13207,15 +13243,18 @@ const setupBrowserIntegrationViaHistory = ({
13207
13243
 
13208
13244
  if (navigationType === "push" || navigationType === "replace") {
13209
13245
  markUrlAsVisited(url);
13210
- const effectiveState = resolveEffectiveDocumentState(state, {
13211
- navigationType,
13212
- isArrival: isArrival(url, { from: urlLeft }),
13213
- currentState: getDocumentState(),
13214
- sharedState: {
13215
- jsenv_visited_urls: Array.from(visitedUrlSet),
13216
- [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13217
- },
13218
- });
13246
+ const effectiveState = applyPressWrites(
13247
+ resolveEffectiveDocumentState(state, {
13248
+ navigationType,
13249
+ isArrival: isArrival(url, { from: urlLeft }),
13250
+ currentState: getDocumentState(),
13251
+ sharedState: {
13252
+ jsenv_visited_urls: Array.from(visitedUrlSet),
13253
+ [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13254
+ },
13255
+ }),
13256
+ pressWrites,
13257
+ );
13219
13258
  if (navigationType === "replace" && debounce && state === undefined) {
13220
13259
  // Nothing of its own for the entry: the address moves, the state is
13221
13260
  // carried over. The one kind of write allowed to lag, and only when
@@ -13315,6 +13354,36 @@ const setupBrowserIntegrationViaHistory = ({
13315
13354
  return requestedResult;
13316
13355
  };
13317
13356
 
13357
+ // A press on a link is one navigation, and what the press asks of the
13358
+ // history entry is part of it: a popup the link's own command closes is
13359
+ // closed on the entry the link leads to, while the one it leaves keeps what
13360
+ // the reader was looking at — a back finds the popup open. The link's
13361
+ // listeners run before the routing is asked (below), so a write they make
13362
+ // is handed over, keyed by the click, rather than written onto the entry
13363
+ // being left. Once the link has been answered there is nothing to hand
13364
+ // over to: the navigation, when there was one, has already written its
13365
+ // entry, and a write lands there by itself.
13366
+ const linkPressWritesMap = new WeakMap();
13367
+ const answeredLinkClickSet = new WeakSet();
13368
+ const carryByLinkNavigation = (event, pressWrite) => {
13369
+ const clickEvent = findEvent(event, "click");
13370
+ if (
13371
+ !clickEvent ||
13372
+ clickEvent.eventPhase === Event.NONE ||
13373
+ answeredLinkClickSet.has(clickEvent) ||
13374
+ !clickEvent.target.closest("a")
13375
+ ) {
13376
+ return false;
13377
+ }
13378
+ const pressWrites = linkPressWritesMap.get(clickEvent);
13379
+ if (pressWrites) {
13380
+ pressWrites.push(pressWrite);
13381
+ } else {
13382
+ linkPressWritesMap.set(clickEvent, [pressWrite]);
13383
+ }
13384
+ return true;
13385
+ };
13386
+
13318
13387
  // A click on a link is answered on the link itself, after the link's own
13319
13388
  // listeners: a navi control decides there whether the press is allowed — it
13320
13389
  // refuses with preventDefault from its click reaction — and runs its command
@@ -13322,34 +13391,42 @@ const setupBrowserIntegrationViaHistory = ({
13322
13391
  // is the order the Navigation API integration gets for free, where the
13323
13392
  // navigation starts once the click has been dispatched.
13324
13393
  const onLinkClick = (e) => {
13394
+ answeredLinkClickSet.add(e);
13395
+ const pressWrites = linkPressWritesMap.get(e);
13396
+ linkPressWritesMap.delete(e);
13397
+ if (!routeLinkClick(e, pressWrites)) {
13398
+ writePressWritesHere(pressWrites);
13399
+ }
13400
+ };
13401
+ const routeLinkClick = (e, pressWrites) => {
13325
13402
  if (e.button !== 0) {
13326
13403
  // Ignore non-left clicks
13327
- return;
13404
+ return false;
13328
13405
  }
13329
13406
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13330
13407
  // A modifier asks the browser for something other than this tab: a new
13331
13408
  // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13332
13409
  // (alt).
13333
- return;
13410
+ return false;
13334
13411
  }
13335
13412
  if (e.defaultPrevented) {
13336
13413
  // Refused — by the link itself, or by whoever came before it.
13337
- return;
13414
+ return false;
13338
13415
  }
13339
13416
  const linkElement = e.currentTarget;
13340
13417
  if (linkElement.hasAttribute("data-readonly")) {
13341
- return;
13418
+ return false;
13342
13419
  }
13343
13420
  if (linkOpensElsewhere(linkElement)) {
13344
13421
  // Another tab or window, or a download (see link_elsewhere.js).
13345
- return;
13422
+ return false;
13346
13423
  }
13347
13424
  const href = linkElement.href;
13348
13425
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
13349
13426
  getHrefTargetInfo(href);
13350
13427
  if (isEmpty || !isSameOrigin) {
13351
13428
  // Let link to other origins be handled by the browser
13352
- return;
13429
+ return false;
13353
13430
  }
13354
13431
  if (isAnchor) {
13355
13432
  // Fragment navigation belongs to the browser: it owns the indicated
@@ -13361,13 +13438,13 @@ const setupBrowserIntegrationViaHistory = ({
13361
13438
  // change reaches whoever is waiting on the designated element.
13362
13439
  rearmUrlTarget();
13363
13440
  }
13364
- return;
13441
+ return false;
13365
13442
  }
13366
13443
  if (linkAsksForDocument(linkElement)) {
13367
13444
  // The link says its address is another document of this origin (see
13368
13445
  // link_document.js). Routing to it would land on the fallback route
13369
13446
  // instead of loading the page.
13370
- return;
13447
+ return false;
13371
13448
  }
13372
13449
  // Nothing here declared a route, so there is nothing to route to: the
13373
13450
  // page is a plain document and a link in it is a plain link. Taking it
@@ -13375,7 +13452,7 @@ const setupBrowserIntegrationViaHistory = ({
13375
13452
  // the address bar moves and the page does not (see applyRouting's own
13376
13453
  // "not called yet" branch, which is where that used to end up).
13377
13454
  if (!isRouting()) {
13378
- return;
13455
+ return false;
13379
13456
  }
13380
13457
  e.preventDefault();
13381
13458
  handleRoutingTask(href, {
@@ -13389,7 +13466,9 @@ const setupBrowserIntegrationViaHistory = ({
13389
13466
  // whoever knows what to do with it, and it is the anchor itself —
13390
13467
  // resolved here, where it already is.
13391
13468
  element: linkElement,
13469
+ pressWrites,
13392
13470
  });
13471
+ return true;
13393
13472
  };
13394
13473
  // Wired from the capture phase on window, the first place a click is seen,
13395
13474
  // so that a link is answered whatever handler stops the click on its way up
@@ -13569,6 +13648,7 @@ const setupBrowserIntegrationViaHistory = ({
13569
13648
  navBack,
13570
13649
  navForward,
13571
13650
  getDocumentState,
13651
+ carryByLinkNavigation,
13572
13652
  isVisited,
13573
13653
  visitedUrlsSignal,
13574
13654
  };
@@ -13968,8 +14048,32 @@ const useNavStateBasic = (
13968
14048
  // that param for gone. Only with nothing of this document behind (the state
13969
14049
  // was entered on a cold-loaded url, navBack has nowhere to go) does it
13970
14050
  // rewrite in place.
13971
- const leave = ({ isBack } = {}) => {
14051
+ //
14052
+ // event: what asked for it. A leave the press of a link asks for (its
14053
+ // --navi-close) is made by the navigation that link starts: the entry the
14054
+ // link leads to does not hold the key, and every entry behind is left as
14055
+ // the reader saw it — a back finds the state still entered.
14056
+ const leave = ({ isBack, event } = {}) => {
13972
14057
  enteredRef.current = false;
14058
+ if (!Object.hasOwn(browserIntegration.getDocumentState() || {}, id)) {
14059
+ return;
14060
+ }
14061
+ if (
14062
+ event &&
14063
+ browserIntegration.carryByLinkNavigation(event, {
14064
+ editState: (state) => {
14065
+ const stateWithoutKey = { ...state };
14066
+ delete stateWithoutKey[id];
14067
+ return stateWithoutKey;
14068
+ },
14069
+ otherwise: () => leaveEntry({ isBack }),
14070
+ })
14071
+ ) {
14072
+ return;
14073
+ }
14074
+ leaveEntry({ isBack });
14075
+ };
14076
+ const leaveEntry = ({ isBack }) => {
13973
14077
  const currentStateCopy = browserIntegration.getDocumentState() || {};
13974
14078
  if (!Object.hasOwn(currentStateCopy, id)) {
13975
14079
  return;
@@ -14027,6 +14131,9 @@ const useNavStateBasic = (
14027
14131
  * the value allows associating extra data when needed.
14028
14132
  * - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
14029
14133
  * or goes back in history when `type` is "push").
14134
+ * `leave({ event })` with the event that asked for it: asked by the press
14135
+ * of a link that navigates, the key is left out of the entry the link
14136
+ * leads to, and the entry it leaves keeps it.
14030
14137
  */
14031
14138
  const useNavState = useNavStateBasic;
14032
14139
 
@@ -17210,8 +17317,13 @@ const useOpenPropsEffectOnOpenController = (
17210
17317
  } else {
17211
17318
  // Under type "push" a cancel discards everything written while
17212
17319
  // the popup was open — it goes back with the entry; a confirmed
17213
- // close keeps those writes (see useNavState's own leave()).
17214
- leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
17320
+ // close keeps those writes (see useNavState's own leave()). A
17321
+ // close a link's press asked for is written by the navigation
17322
+ // the link starts.
17323
+ leaveNavState({
17324
+ isBack: Boolean(event?.detail?.isCancel),
17325
+ event,
17326
+ });
17215
17327
  }
17216
17328
  }
17217
17329
  if (signal) {
@@ -17432,8 +17544,9 @@ const useOpenControllerByProps = (props, name) => {
17432
17544
  * opened it, so a section written in a display font gets its tooltips in that
17433
17545
  * font. `color`, `font-size` and `font-weight` are absent too — each surface
17434
17546
  * answers those its own way (a popup and a callout write in
17435
- * --navi-popup-color; a popup follows the size of what opened it, a callout
17436
- * resets to the document's size).
17547
+ * --navi-popup-color; a popup follows the size of what opened it — for a
17548
+ * picker's popup, what is around the picker, see .navi_picker_content — a
17549
+ * callout resets to the document's size).
17437
17550
  *
17438
17551
  * In `@layer navi` so an app that wants one of these back says so on the
17439
17552
  * surface and wins, without having to out-specify anything.
@@ -21034,6 +21147,13 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-readonly");
21034
21147
  CONSTRAINT_ATTRIBUTE_SET.add("data-readonly-reason");
21035
21148
 
21036
21149
  const readOnlyMessage = (field) => {
21150
+ // Read-only from above, in the words of what made it so (a picker's or a
21151
+ // group's readOnlyMessage): what it holds says what it says — over the
21152
+ // network policy's words too, as that control's own words win over them on
21153
+ // itself.
21154
+ if (field.readOnlyMessageFromAbove) {
21155
+ return field.readOnlyMessageFromAbove;
21156
+ }
21037
21157
  // Read-only for a reason the control named itself, read off the reason
21038
21158
  // rather than off the surrounding state: a button read-only for its own
21039
21159
  // reasons, inside a form that happens to be unchanged, is not waiting for
@@ -46692,6 +46812,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
46692
46812
  s.controller.controlHostProps = realUIStateController.controlHostProps;
46693
46813
  s.controller.readOnlyWithoutRun =
46694
46814
  realUIStateController.readOnlyWithoutRun;
46815
+ s.controller.readOnlyMessageFromAbove =
46816
+ realUIStateController.readOnlyMessageFromAbove;
46695
46817
  realUIStateController.pushStateDownToFacadeChild =
46696
46818
  s.controller.pushStateDownToChild;
46697
46819
 
@@ -47037,6 +47159,7 @@ const ControlgroupChildrenWrapper = ({
47037
47159
  required,
47038
47160
  disabled,
47039
47161
  readOnly,
47162
+ readOnlyMessage,
47040
47163
  loading,
47041
47164
  boundAction,
47042
47165
  actionRequesterSignal
@@ -47050,8 +47173,9 @@ const ControlgroupChildrenWrapper = ({
47050
47173
  value: name,
47051
47174
  children: jsx(DisabledContext.Provider, {
47052
47175
  value: disabled,
47053
- children: jsx(ReadOnlyContext.Provider, {
47054
- value: readOnly,
47176
+ children: jsx(ReadOnlyProvider, {
47177
+ readOnly: readOnly,
47178
+ readOnlyMessage: readOnlyMessage,
47055
47179
  children: jsx(RequiredContext.Provider, {
47056
47180
  value: required,
47057
47181
  children: jsx(LoadingContext$1.Provider, {
@@ -47071,6 +47195,34 @@ const ControlgroupChildrenWrapper = ({
47071
47195
  })
47072
47196
  })
47073
47197
  });
47198
+ // What a control hands to what it holds about being read-only: the state, and
47199
+ // the words it is said in. What it holds writes into it, so it is held for the
47200
+ // same reason and says what the control says. Held by the network policy, it
47201
+ // hands down the hold itself — read-only alone, what it holds would refuse in
47202
+ // navi's generic words. Read-only, it hands down its readOnlyMessage, or the
47203
+ // one it was handed.
47204
+ const readOnlyHandedDown = (uiStateController, readOnly) => {
47205
+ const {
47206
+ controlHostProps,
47207
+ props
47208
+ } = uiStateController;
47209
+ const readOnlyResolved = controlHostProps["data-readonly-reason"] === "network-policy" ? "network-policy" : readOnly;
47210
+ if (!readOnlyResolved) {
47211
+ return [readOnlyResolved, undefined];
47212
+ }
47213
+ return [readOnlyResolved, props.readOnlyMessage ?? uiStateController.readOnlyMessageFromAbove];
47214
+ };
47215
+ const ReadOnlyProvider = ({
47216
+ readOnly,
47217
+ readOnlyMessage,
47218
+ children
47219
+ }) => jsx(ReadOnlyContext.Provider, {
47220
+ value: readOnly,
47221
+ children: jsx(ReadOnlyMessageContext.Provider, {
47222
+ value: readOnlyMessage,
47223
+ children: children
47224
+ })
47225
+ });
47074
47226
 
47075
47227
  // What the caller writes for these is called from the control's own reactions
47076
47228
  // (see `reactions` below), so the split leaves them out of both sides.
@@ -48297,7 +48449,7 @@ const useControlgroupProps = (props, {
48297
48449
  basePseudoState
48298
48450
  } = controlgroupProps;
48299
48451
  const disabled = basePseudoState[":disabled"];
48300
- const readOnly = basePseudoState[":read-only"];
48452
+ const [readOnly, readOnlyMessage] = readOnlyHandedDown(uiGroupStateController, basePseudoState[":read-only"]);
48301
48453
  const loading = basePseudoState[":-navi-loading"];
48302
48454
  const controlgroupChildrenWrapperProps = useMemo(() => ({
48303
48455
  uiGroupStateController,
@@ -48305,10 +48457,11 @@ const useControlgroupProps = (props, {
48305
48457
  required: controlgroupProps.required,
48306
48458
  disabled,
48307
48459
  readOnly,
48460
+ readOnlyMessage,
48308
48461
  loading,
48309
48462
  boundAction,
48310
48463
  actionRequesterSignal
48311
- }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequesterSignal]);
48464
+ }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, readOnlyMessage, loading, boundAction, actionRequesterSignal]);
48312
48465
  return [controlRootProps, {
48313
48466
  ...controlgroupProps,
48314
48467
  "name": undefined,
@@ -48504,6 +48657,7 @@ const useInteractiveProps = (props, {
48504
48657
  {
48505
48658
  const controlDisabledFromAbove = useContext(DisabledContext);
48506
48659
  const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
48660
+ const controlReadOnlyMessageFromAbove = useContext(ReadOnlyMessageContext);
48507
48661
  const controlRequired = useContext(RequiredContext);
48508
48662
  const controlLoadingFromAbove = useContext(LoadingContext$1);
48509
48663
  // A SIGNAL holding the element whose press the group's action is wearing
@@ -48543,8 +48697,15 @@ const useInteractiveProps = (props, {
48543
48697
  // pressable on a game nobody may edit. Its own props still hold; only what
48544
48698
  // came from above is dropped (see self_interactions.js for the three modes).
48545
48699
  const zoneStateApplies = !selfInteractionsIgnoreBlock(props);
48700
+ // A press that only leaves the popup it sits in writes nothing to the zone
48701
+ // around it either, so that zone being read-only — or holding a write the
48702
+ // network policy keeps — does not hold it: the popup answers the close
48703
+ // itself. A read-only picker's popup then closes from its own button as
48704
+ // it does from a press outside, and refuses both alike when it holds a
48705
+ // change it cannot send.
48706
+ const zoneReadOnlyApplies = zoneStateApplies && !pressOnlyLeaves(props);
48546
48707
  const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
48547
- const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
48708
+ const controlReadOnly = zoneReadOnlyApplies && controlReadOnlyFromAbove;
48548
48709
  const controlLoading = zoneStateApplies && controlLoadingFromAbove;
48549
48710
  const disabledResolved = disabled || controlDisabled;
48550
48711
  const requiredResolved = required || controlRequired;
@@ -48559,10 +48720,11 @@ const useInteractiveProps = (props, {
48559
48720
  // but cannot be taken.
48560
48721
  const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
48561
48722
  // A write cannot leave under a network policy, so the control asking for
48562
- // one — or sitting in a form that does — refuses before the press and says
48563
- // why (see network_policy.js): a press that looks accepted and fails after
48564
- // is what the policy exists to avoid.
48565
- const heldByNetworkPolicy = networkPolicyReason !== null && (isWriteAction(boundAction) || Boolean(zoneStateApplies && parentAction && isWriteAction(parentAction)));
48723
+ // one — sitting in a form that does, or in what a held control holds (a
48724
+ // picker's popup) — refuses before the press and says why (see
48725
+ // network_policy.js): a press that looks accepted and fails after is what
48726
+ // the policy exists to avoid.
48727
+ const heldByNetworkPolicy = networkPolicyReason !== null && (isWriteAction(boundAction) || Boolean(zoneReadOnlyApplies && parentAction && isWriteAction(parentAction)) || controlReadOnly === "network-policy");
48566
48728
  // Held back because the surface above already has as many runs in flight as
48567
48729
  // it allows (see parallel_guard.js). Only a control that would START one:
48568
48730
  // anything without an action of its own runs nothing and has nothing to
@@ -48581,6 +48743,10 @@ const useInteractiveProps = (props, {
48581
48743
  // still read-only in the tick the run settles — the tick a form's
48582
48744
  // after-send `--navi-reset` asks the form to empty itself.
48583
48745
  uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
48746
+ // The words of what made this control read-only from above, when it gave
48747
+ // some: read by READONLY_CONSTRAINT, and handed further down with the
48748
+ // read-only state (see readOnlyHandedDown).
48749
+ uiStateController.readOnlyMessageFromAbove = controlReadOnly ? controlReadOnlyMessageFromAbove : undefined;
48584
48750
  // Read-only, and what this control opens still opens: reading what is in
48585
48751
  // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
48586
48752
  // interaction that only reads through on it.
@@ -49113,6 +49279,12 @@ const splitControlProps = (props, {
49113
49279
  }
49114
49280
  return [controlRootProps, controlHostProps];
49115
49281
  };
49282
+ // What a press leaving a popup asks for: closing it, or cancelling it. Only on
49283
+ // a control running nothing of its own — one with an action writes, wherever
49284
+ // it then leads.
49285
+ const LEAVING_COMMAND_SET = new Set(["--navi-close", "--navi-cancel"]);
49286
+ const pressOnlyLeaves = props => LEAVING_COMMAND_SET.has(props.command) && !props.action;
49287
+
49116
49288
  // `onPointerDown`, `onFocusOut`… but not navi's own events (`onnavi_command`),
49117
49289
  // which keep the element they are dispatched on.
49118
49290
  const isDomListenerProp = key => /^on[A-Z]/.test(key);
@@ -53123,7 +53295,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
53123
53295
  * fetching on a hover.
53124
53296
  * @param {string} [props.command] - What the press asks of a control around
53125
53297
  * the link — `"--navi-close"` on a link that leaves the sheet it is in.
53126
- * Triggered on the press, before the navigation.
53298
+ * Triggered on the press, before the navigation. A popup it closes that
53299
+ * keeps its open state in the history entry (`navState`) is closed on the
53300
+ * entry the navigation leads to: Back finds it open.
53127
53301
  * @param {Function} [props.action] - Work the press runs, before the
53128
53302
  * navigation. Nothing waits for it — not the navigation, not `command`: what
53129
53303
  * the next page must find has to be written synchronously (a draft in a
@@ -57671,7 +57845,7 @@ const css$S = /* css */`@layer navi {
57671
57845
  --button-color: currentColor;
57672
57846
  --button-cursor: pointer;
57673
57847
  --button-font-size: var(--navi-control-font-size);
57674
- --button-font-family: var(--navi-control-font-family, inherit);
57848
+ --button-font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
57675
57849
  --button-border-color-hover: color-mix(in srgb,
57676
57850
  var(--button-border-color) 70%,
57677
57851
  black);
@@ -59399,14 +59573,16 @@ const css$O = /* css */`@layer navi {
59399
59573
  --checkmark-color-disabled: #eee;
59400
59574
  --border-color-disabled-checked: #d3d3d3;
59401
59575
  --background-color-disabled-checked: #d3d3d3;
59402
- --button-border-color: light-dark(#767676, #8e8e93);
59403
- --button-background-color: light-dark(#f3f4f6, #2d3748);
59576
+ --button-border-color: var(--navi-control-border-color);
59577
+ --button-background-color: var(--navi-button-background-color);
59404
59578
  --button-border-color-hover: color-mix(in srgb,
59405
59579
  var(--button-border-color) 70%,
59406
59580
  black);
59407
59581
  --button-background-color-hover: color-mix(in srgb,
59408
59582
  var(--button-background-color) 95%,
59409
59583
  black);
59584
+ --button-background-color-readonly: var(--button-background-color);
59585
+ --button-background-color-disabled: var(--button-background-color-readonly);
59410
59586
  }
59411
59587
 
59412
59588
  .navi_checkbox[data-variant="switch"] {
@@ -59434,19 +59610,40 @@ const css$O = /* css */`@layer navi {
59434
59610
  --margin: 0;
59435
59611
  --width: auto;
59436
59612
  --height: auto;
59613
+ --font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
59437
59614
  --border-color: var(--button-border-color);
59438
59615
  --border-color-hover: var(--button-border-color-hover);
59439
59616
  --background-color: var(--button-background-color);
59440
59617
  --background-color-hover: var(--button-background-color-hover);
59441
59618
  --background-color-readonly: var(--button-background-color-readonly);
59442
59619
  --background-color-disabled: var(--button-background-color-disabled);
59620
+ }
59621
+
59622
+ .navi_checkbox[data-variant="button"]:not([data-label]) {
59623
+ --button-padding-x-default: 0;
59624
+ --button-padding-y-default: 0;
59443
59625
  --border-color-checked: var(--button-border-color);
59626
+ --border-color-hover-checked: var(--button-border-color-hover);
59444
59627
  --background-color-checked: var(--button-background-color);
59628
+ --background-color-hover-checked: var(--button-background-color-hover);
59629
+ --checkmark-color: currentColor;
59445
59630
  }
59446
59631
 
59447
59632
  .navi_checkbox[data-variant="button"][data-label] {
59448
- --border-color-checked: var(--accent-color);
59449
- --background-color-checked: var(--accent-color);
59633
+ --border-radius: var(--navi-control-border-radius);
59634
+ --button-padding-x-default: var(--navi-button-padding-x-default);
59635
+ --button-padding-y-default: var(--navi-button-padding-y-default);
59636
+ line-height: var(--navi-control-line-height);
59637
+ }
59638
+
59639
+ .navi_checkbox[data-variant="button"][data-button-variant="discrete"] {
59640
+ --border-width: 0;
59641
+ --button-border-color: transparent;
59642
+ --button-border-color-hover: var(--button-border-color);
59643
+ --button-background-color: transparent;
59644
+ --button-background-color-hover: color-mix(in srgb,
59645
+ currentColor 8%,
59646
+ var(--button-background-color));
59450
59647
  }
59451
59648
  }
59452
59649
 
@@ -59554,23 +59751,24 @@ const css$O = /* css */`@layer navi {
59554
59751
  --color-mix: var(--color-mix-light);
59555
59752
  }
59556
59753
 
59754
+ & .navi_checkbox_marker {
59755
+ opacity: 0;
59756
+ stroke: var(--x-checkmark-color);
59757
+ transform: scale(.5);
59758
+ }
59759
+
59760
+ &[data-checked] .navi_checkbox_marker {
59761
+ opacity: 1;
59762
+ transition-property: opacity, transform;
59763
+ transition-duration: .15s;
59764
+ transition-timing-function: ease;
59765
+ transform: scale(1);
59766
+ }
59767
+
59557
59768
  &[data-variant="checkbox"] {
59558
59769
  & .navi_checkbox_marker {
59559
- opacity: 0;
59560
59770
  width: 100%;
59561
59771
  height: 100%;
59562
- stroke: var(--x-checkmark-color);
59563
- transform: scale(.5);
59564
- }
59565
-
59566
- &[data-checked] {
59567
- & .navi_checkbox_marker {
59568
- opacity: 1;
59569
- transition-property: opacity, transform;
59570
- transition-duration: .15s;
59571
- transition-timing-function: ease;
59572
- transform: scale(1);
59573
- }
59574
59772
  }
59575
59773
 
59576
59774
  &[data-accent-very-light] {
@@ -59609,6 +59807,11 @@ const css$O = /* css */`@layer navi {
59609
59807
  justify-content: center;
59610
59808
  align-items: center;
59611
59809
 
59810
+ & .navi_checkbox_marker {
59811
+ width: 1em;
59812
+ height: 1em;
59813
+ }
59814
+
59612
59815
  &[data-label] {
59613
59816
  &[data-checked] .navi_checkbox_label {
59614
59817
  color: var(--x-checkmark-color);
@@ -59657,7 +59860,8 @@ const InputCheckboxFieldInterface = props => {
59657
59860
  const {
59658
59861
  icon,
59659
59862
  switch: switchProp,
59660
- variant = icon ? "icon" : switchProp ? "switch" : "checkbox",
59863
+ buttonVariant,
59864
+ variant = buttonVariant ? "button" : icon ? "icon" : switchProp ? "switch" : "checkbox",
59661
59865
  // "checkbox", "switch", "icon", "button"
59662
59866
  accentColor
59663
59867
  } = props;
@@ -59712,7 +59916,9 @@ const InputCheckboxFieldInterface = props => {
59712
59916
  variant: undefined,
59713
59917
  switch: undefined,
59714
59918
  icon: undefined,
59919
+ buttonVariant: undefined,
59715
59920
  "data-variant": variant,
59921
+ "data-button-variant": variant === "button" ? buttonVariant : undefined,
59716
59922
  "data-label": hasLabel ? "" : undefined,
59717
59923
  baseClassName: "navi_checkbox",
59718
59924
  pseudoStateSelector: ".navi_control_input",
@@ -60186,6 +60392,9 @@ const css$M = /* css */`@layer navi {
60186
60392
  --radiomark-color-disabled: #d3d3d3;
60187
60393
  --border-color-checked-disabled: #d3d3d3;
60188
60394
  --background-color-disabled-checked: var(--background-color);
60395
+ --button-padding-x-default: 0;
60396
+ --button-padding-y-default: 0;
60397
+ --button-border-radius: var(--navi-control-border-radius);
60189
60398
  --button-border-width: 1px;
60190
60399
  --button-border-color: transparent;
60191
60400
  --button-background-color: transparent;
@@ -62410,6 +62619,7 @@ const InputPseudoElements = ["::-navi-loader"];
62410
62619
  * boolean?: boolean,
62411
62620
  * selectOnPress?: boolean,
62412
62621
  * variant?: "checkbox" | "switch" | "icon" | "button",
62622
+ * buttonVariant?: "discrete",
62413
62623
  * actionVerb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
62414
62624
  * [key: string]: any,
62415
62625
  * }>}
@@ -62425,6 +62635,10 @@ const InputPseudoElements = ["::-navi-loader"];
62425
62635
  * is a square around its checkmark (or its `icon`, which takes an
62426
62636
  * `[off, on]` pair). Bound with `signal`, it follows whatever else writes
62427
62637
  * that signal.
62638
+ * @param {"discrete"} [buttonVariant] A checkbox drawn as a button, drawn as
62639
+ * that variant of `Button`: `"discrete"` has no box at rest and a wash on
62640
+ * hover (checked, its words still fill with the accent). Implies
62641
+ * `variant="button"`.
62428
62642
  * @param {"GET"|"POST"|"PUT"|"PATCH"|"DELETE"} [actionVerb] What a plain
62429
62643
  * function `action` does to the server. A write verb puts the field under
62430
62644
  * the network policy: held read-only up front, with the policy's message,
@@ -71662,6 +71876,10 @@ const UncontrolledDialog = props => {
71662
71876
  // needs the dialog element to look at what it contains.
71663
71877
  const defaultRef = useRef();
71664
71878
  props.ref = props.ref || defaultRef;
71879
+ // The close is built once per opening (see useOpenController below): read
71880
+ // through a ref, an onClose given by a render while open is the one called.
71881
+ const onCloseRef = useRef(null);
71882
+ onCloseRef.current = props.onClose;
71665
71883
  const openController = useOpenController(openEvent => {
71666
71884
  const dialogEl = props.ref.current;
71667
71885
  return {
@@ -71688,7 +71906,7 @@ const UncontrolledDialog = props => {
71688
71906
  requestCloseEvent.preventDefault();
71689
71907
  },
71690
71908
  onClose: closeEvent => {
71691
- props.onClose?.(closeEvent);
71909
+ onCloseRef.current?.(closeEvent);
71692
71910
  }
71693
71911
  };
71694
71912
  });
@@ -73547,6 +73765,9 @@ const UncontrolledPopover = props => {
73547
73765
  // the popover element to read what it holds.
73548
73766
  const defaultRef = useRef();
73549
73767
  props.ref = props.ref || defaultRef;
73768
+ // Read at the close, not captured at the opening: see UncontrolledDialog.
73769
+ const onCloseRef = useRef(null);
73770
+ onCloseRef.current = props.onClose;
73550
73771
  // Same as Dialog's own UncontrolledDialog, for the same reasons — see it for
73551
73772
  // the full story: a popover is a surface, so the only thing it has to say
73552
73773
  // about closing is that what it contains must not be interrupted mid-action.
@@ -73571,7 +73792,7 @@ const UncontrolledPopover = props => {
73571
73792
  requestCloseEvent.preventDefault();
73572
73793
  },
73573
73794
  onClose: closeEvent => {
73574
- props.onClose?.(closeEvent);
73795
+ onCloseRef.current?.(closeEvent);
73575
73796
  }
73576
73797
  };
73577
73798
  });
@@ -75421,6 +75642,15 @@ const PickerCustom = props => {
75421
75642
  // closed (see onClose below) — a press that ran before the close would make
75422
75643
  // the picker busy, and a busy picker refuses the very close that follows.
75423
75644
  const confirmEventRef = useRef(null);
75645
+ // The close is built once per opening (see useOpenController below), but
75646
+ // these props answer how it ends: read through a ref, a render while open
75647
+ // (a sheet turned into the next question) is heard by the close.
75648
+ const closePropsRef = useRef(null);
75649
+ closePropsRef.current = {
75650
+ canSendWhileUnchanged,
75651
+ onConfirm,
75652
+ onClose
75653
+ };
75424
75654
  // aria-controls + id
75425
75655
  const popupId = `${props.id}_picker_popup`;
75426
75656
  {
@@ -75519,7 +75749,7 @@ const PickerCustom = props => {
75519
75749
  // and for that picker the same value said again means something.
75520
75750
  // A press outside, Escape, the cross say nothing and keep skipping.
75521
75751
  const said = requestCloseEvent.detail.isSend || confirmEventRef.current;
75522
- if (!(canSendWhileUnchanged && said)) {
75752
+ if (!(closePropsRef.current.canSendWhileUnchanged && said)) {
75523
75753
  // No action to run, but still allow the close.
75524
75754
  return;
75525
75755
  }
@@ -75580,7 +75810,7 @@ const PickerCustom = props => {
75580
75810
  const confirmEvent = confirmEventRef.current;
75581
75811
  confirmEventRef.current = null;
75582
75812
  if (confirmEvent && !closeEvent.detail.isCancel) {
75583
- onConfirm?.(confirmEvent);
75813
+ closePropsRef.current.onConfirm?.(confirmEvent);
75584
75814
  }
75585
75815
  // After the value bookkeeping above — whoever listens reads the
75586
75816
  // picker's value as it ends up, committed or restored on a cancel —
@@ -75591,7 +75821,7 @@ const PickerCustom = props => {
75591
75821
  // spells into the address — a route param naming what was being
75592
75822
  // edited, cleared now that nothing is — has to be written before,
75593
75823
  // or the landing puts it back.
75594
- onClose?.(closeEvent);
75824
+ closePropsRef.current.onClose?.(closeEvent);
75595
75825
  leaveExpanded({
75596
75826
  isBack: closeEvent.detail.isCancel
75597
75827
  });
@@ -75681,6 +75911,11 @@ const PickerCustom = props => {
75681
75911
  // button) hangs its popup off the whole control instead.
75682
75912
  anchor: props.anchor || props.ref,
75683
75913
  openController,
75914
+ // Before the content is built and shown, so it is laid out and placed
75915
+ // at the size it is read at (see .navi_picker_content in picker.jsx).
75916
+ onOpen: () => {
75917
+ writeContainerFont(ref.current, popupRef.current);
75918
+ },
75684
75919
  // A picker whose value was never given to it reads it off the control in
75685
75920
  // its popup (see useUIFacadeStateController), so that control is built as
75686
75921
  // soon as something reads the value: at mount when the picker sees the
@@ -76504,6 +76739,30 @@ const showNativePicker = (pickerInput, event) => {
76504
76739
  }
76505
76740
  };
76506
76741
 
76742
+ // The popup of a picker is opened where the picker stands, so it writes at the
76743
+ // size and weight of what is around it — the ones a Dialog rendered there
76744
+ // inherits — and never at the ones the trigger is drawn at. CSS cannot reach
76745
+ // past the picker's own: its root holds the popup and sets both. Written on the
76746
+ // holder only when they differ, so an opening that changes nothing invalidates
76747
+ // no style.
76748
+ const writeContainerFont = (pickerEl, popupEl) => {
76749
+ const container = pickerEl?.parentElement;
76750
+ const holder = popupEl?.closest("[data-picker-content]");
76751
+ if (!container || !holder) {
76752
+ return;
76753
+ }
76754
+ const {
76755
+ fontSize,
76756
+ fontWeight
76757
+ } = getComputedStyle(container);
76758
+ if (holder.style.fontSize !== fontSize) {
76759
+ holder.style.fontSize = fontSize;
76760
+ }
76761
+ if (holder.style.fontWeight !== fontWeight) {
76762
+ holder.style.fontWeight = fontWeight;
76763
+ }
76764
+ };
76765
+
76507
76766
  installImportMetaCssBuild(import.meta);
76508
76767
  const css$y = /* css */`.navi_picker_confirm_body {
76509
76768
  min-width: 180px;
@@ -78471,11 +78730,16 @@ const ListItemSelectable = props => {
78471
78730
  // A list waiting on something is not unavailable, it is not answering yet —
78472
78731
  // busy rather than read-only (see BUSY_CONSTRAINT, which is asked first),
78473
78732
  // and the wait is about the same thing the read-only would be about.
78733
+ //
78734
+ // Unless what made the list read-only says it better: in words of its own (a
78735
+ // picker's or a list's readOnlyMessage), or because it is the network
78736
+ // policy holding the write the selection would make.
78474
78737
  const readOnlyFromAbove = useContext(ReadOnlyContext);
78738
+ const readOnlyMessageFromAbove = useContext(ReadOnlyMessageContext);
78475
78739
  const readOnlyMessageKey = readOnlyFromAbove ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
78476
78740
  // Handed to the row as well as to the control: whichever of the two catches
78477
78741
  // the press answers it, and they must not answer it differently.
78478
- const readOnlyMessageResolved = props.readOnlyMessage ?? constraintI18n(readOnlyMessageKey, props);
78742
+ const readOnlyMessageResolved = props.readOnlyMessage ?? (readOnlyFromAbove ? readOnlyMessageFromAbove : undefined) ?? (readOnlyFromAbove === "network-policy" ? getNetworkPolicyReadOnlyMessage() : constraintI18n(readOnlyMessageKey, props));
78479
78743
  const busyMessageResolved = props.busyMessage ?? (loading ? listItemBusyMessage(loading, props) : constraintI18n(multiple ? `constraint.busy.selection` : `constraint.busy.choice`, props));
78480
78744
  const inputRef = useRef();
78481
78745
  const inputType = multiple ? "checkbox" : "radio";
@@ -87077,6 +87341,7 @@ const css$r = /* css */`@layer navi {
87077
87341
  .navi_picker:is([data-variant="button"], [data-variant="border"], [data-variant="discrete-border"]) {
87078
87342
  --picker-padding-x-default: var(--navi-button-padding-x-default);
87079
87343
  --picker-padding-y-default: var(--navi-button-padding-y-default);
87344
+ --picker-font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
87080
87345
  --picker-align-x-default: center;
87081
87346
  --picker-background-color: var(--navi-button-background-color);
87082
87347
  --picker-border-color-readonly: color-mix(in srgb,
@@ -87376,8 +87641,9 @@ const css$r = /* css */`@layer navi {
87376
87641
  --x-corner-top-right-radius: initial;
87377
87642
  --x-corner-bottom-right-radius: initial;
87378
87643
  --x-corner-bottom-left-radius: initial;
87379
- font-size: var(--navi-control-font-size);
87644
+ font-weight: initial;
87380
87645
  text-align: initial;
87646
+ font-size: 1rem;
87381
87647
  display: contents;
87382
87648
  }
87383
87649
 
@@ -87615,9 +87881,10 @@ const PickerButton = props => {
87615
87881
  // The same chain useControlProps resolves (own prop first, then what the
87616
87882
  // group above says), for the two things it does not carry: the popup content,
87617
87883
  // which is read-only along with the picker, and the cursor, which stays a
87618
- // pointer on a picker that still opens.
87884
+ // pointer on a picker that still opens. A hold of the network policy goes
87885
+ // with it: what the popup changes is what the picker would send.
87619
87886
  const readOnlyFromAbove = useContext(ReadOnlyContext);
87620
- const readOnlyResolved = readOnly || readOnlyFromAbove;
87887
+ const [readOnlyResolved, readOnlyMessageResolved] = readOnlyHandedDown(uiStateController, readOnly || readOnlyFromAbove);
87621
87888
  // Read off the controller rather than worked out again: what makes a
87622
87889
  // read-only picker open is settled once, where the gate reads it (see
87623
87890
  // createControlInfo's readOnlyOpens). Needed here for the cursor.
@@ -87665,11 +87932,13 @@ const PickerButton = props => {
87665
87932
  /* Read-only crosses into everything the picker is made of: what it really
87666
87933
  holds is drawn by controls of their own — in the popup, and on the façade
87667
87934
  where an application puts its own affordances — and each of them refuses
87668
- in its own words once told. Said from the read-only state alone, never
87935
+ once told, in the picker's words when it gave some (readOnlyMessage),
87936
+ in its own otherwise. Said from the read-only state alone, never
87669
87937
  from the busy one — an action running for a moment is not the same thing
87670
87938
  as a value nobody may change. */
87671
- jsx(ReadOnlyContext.Provider, {
87672
- value: readOnlyResolved,
87939
+ jsx(ReadOnlyProvider, {
87940
+ readOnly: readOnlyResolved,
87941
+ readOnlyMessage: readOnlyMessageResolved,
87673
87942
  children: jsxs(Box
87674
87943
  // A word in a sentence (variant="text") sits in a <p>, where a <div> is
87675
87944
  // not allowed: phrasing content there, block content elsewhere.