@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.
@@ -5689,7 +5689,13 @@ const MessagePropsRefContext = createContext();
5689
5689
  const ControlIdContext = createContext();
5690
5690
  const ControlNameContext = createContext();
5691
5691
  const DisabledContext = createContext();
5692
+ // Truthy when what is above is read-only, and "network-policy" when that is a
5693
+ // write held by the network policy: what it holds is held in turn, and says
5694
+ // so in the policy's words (see readOnlyHandedDown in control_hooks.jsx).
5692
5695
  const ReadOnlyContext = createContext();
5696
+ // The words that go with it, when what handed read-only down gave some (its
5697
+ // readOnlyMessage, or the ones it was handed in turn).
5698
+ const ReadOnlyMessageContext = createContext();
5693
5699
  const RequiredContext = createContext();
5694
5700
  const LoadingContext$1 = createContext();
5695
5701
  createContext();
@@ -12079,6 +12085,31 @@ const resolveEffectiveDocumentState = (
12079
12085
  return { ...state, ...sharedState };
12080
12086
  };
12081
12087
 
12088
+ /**
12089
+ * What a press on a link handed to the navigation it starts (see the
12090
+ * integrations' `carryByLinkNavigation`): each write is an `editState` applied
12091
+ * to the entry that navigation writes, and an `otherwise` that makes the write
12092
+ * where it would have been made, for a press that starts no navigation here.
12093
+ */
12094
+ const applyPressWrites = (state, pressWrites) => {
12095
+ if (!pressWrites) {
12096
+ return state;
12097
+ }
12098
+ let stateWritten = state;
12099
+ for (const { editState } of pressWrites) {
12100
+ stateWritten = editState(stateWritten);
12101
+ }
12102
+ return stateWritten;
12103
+ };
12104
+ const writePressWritesHere = (pressWrites) => {
12105
+ if (!pressWrites) {
12106
+ return;
12107
+ }
12108
+ for (const { otherwise } of pressWrites) {
12109
+ otherwise();
12110
+ }
12111
+ };
12112
+
12082
12113
  // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
12083
12114
  // mask is derived from render order within the nearest root/async boundary:
12084
12115
  // stable across re-renders of the same mount, but not across two mounts on the
@@ -13734,6 +13765,7 @@ const setupBrowserIntegrationViaHistory = ({
13734
13765
  // Asked to go where we already are: an address that redirects is never
13735
13766
  // the one being displayed, so there is nothing to go to and nothing to
13736
13767
  // stack on the history.
13768
+ writePressWritesHere(options.pressWrites);
13737
13769
  return undefined;
13738
13770
  }
13739
13771
  return handleRoutingTask(redirectionUrl, {
@@ -13745,10 +13777,13 @@ const setupBrowserIntegrationViaHistory = ({
13745
13777
  // Decided before anything is announced: an elided push IS the traversal it
13746
13778
  // becomes, and the traversal will make its own announcements when the
13747
13779
  // browser answers — a before/after cycle here would be about a navigation
13748
- // that never happens.
13780
+ // that never happens. A push with something of its own for its entry — a
13781
+ // state, what the press handed over (see carryByLinkNavigation) — is a
13782
+ // new entry, never one standing there already.
13749
13783
  if (
13750
13784
  options.navigationType === "push" &&
13751
13785
  options.state === undefined &&
13786
+ !options.pressWrites &&
13752
13787
  url !== readAddress()
13753
13788
  ) {
13754
13789
  const delta = adjacentEntryDelta(url);
@@ -13798,6 +13833,7 @@ const setupBrowserIntegrationViaHistory = ({
13798
13833
  redirected,
13799
13834
  landOn,
13800
13835
  debounce,
13836
+ pressWrites,
13801
13837
  } = options;
13802
13838
  let { state } = options;
13803
13839
 
@@ -13808,15 +13844,18 @@ const setupBrowserIntegrationViaHistory = ({
13808
13844
 
13809
13845
  if (navigationType === "push" || navigationType === "replace") {
13810
13846
  markUrlAsVisited(url);
13811
- const effectiveState = resolveEffectiveDocumentState(state, {
13812
- navigationType,
13813
- isArrival: isArrival(url, { from: urlLeft }),
13814
- currentState: getDocumentState(),
13815
- sharedState: {
13816
- jsenv_visited_urls: Array.from(visitedUrlSet),
13817
- [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13818
- },
13819
- });
13847
+ const effectiveState = applyPressWrites(
13848
+ resolveEffectiveDocumentState(state, {
13849
+ navigationType,
13850
+ isArrival: isArrival(url, { from: urlLeft }),
13851
+ currentState: getDocumentState(),
13852
+ sharedState: {
13853
+ jsenv_visited_urls: Array.from(visitedUrlSet),
13854
+ [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13855
+ },
13856
+ }),
13857
+ pressWrites,
13858
+ );
13820
13859
  if (navigationType === "replace" && debounce && state === undefined) {
13821
13860
  // Nothing of its own for the entry: the address moves, the state is
13822
13861
  // carried over. The one kind of write allowed to lag, and only when
@@ -13916,6 +13955,36 @@ const setupBrowserIntegrationViaHistory = ({
13916
13955
  return requestedResult;
13917
13956
  };
13918
13957
 
13958
+ // A press on a link is one navigation, and what the press asks of the
13959
+ // history entry is part of it: a popup the link's own command closes is
13960
+ // closed on the entry the link leads to, while the one it leaves keeps what
13961
+ // the reader was looking at — a back finds the popup open. The link's
13962
+ // listeners run before the routing is asked (below), so a write they make
13963
+ // is handed over, keyed by the click, rather than written onto the entry
13964
+ // being left. Once the link has been answered there is nothing to hand
13965
+ // over to: the navigation, when there was one, has already written its
13966
+ // entry, and a write lands there by itself.
13967
+ const linkPressWritesMap = new WeakMap();
13968
+ const answeredLinkClickSet = new WeakSet();
13969
+ const carryByLinkNavigation = (event, pressWrite) => {
13970
+ const clickEvent = findEvent(event, "click");
13971
+ if (
13972
+ !clickEvent ||
13973
+ clickEvent.eventPhase === Event.NONE ||
13974
+ answeredLinkClickSet.has(clickEvent) ||
13975
+ !clickEvent.target.closest("a")
13976
+ ) {
13977
+ return false;
13978
+ }
13979
+ const pressWrites = linkPressWritesMap.get(clickEvent);
13980
+ if (pressWrites) {
13981
+ pressWrites.push(pressWrite);
13982
+ } else {
13983
+ linkPressWritesMap.set(clickEvent, [pressWrite]);
13984
+ }
13985
+ return true;
13986
+ };
13987
+
13919
13988
  // A click on a link is answered on the link itself, after the link's own
13920
13989
  // listeners: a navi control decides there whether the press is allowed — it
13921
13990
  // refuses with preventDefault from its click reaction — and runs its command
@@ -13923,34 +13992,42 @@ const setupBrowserIntegrationViaHistory = ({
13923
13992
  // is the order the Navigation API integration gets for free, where the
13924
13993
  // navigation starts once the click has been dispatched.
13925
13994
  const onLinkClick = (e) => {
13995
+ answeredLinkClickSet.add(e);
13996
+ const pressWrites = linkPressWritesMap.get(e);
13997
+ linkPressWritesMap.delete(e);
13998
+ if (!routeLinkClick(e, pressWrites)) {
13999
+ writePressWritesHere(pressWrites);
14000
+ }
14001
+ };
14002
+ const routeLinkClick = (e, pressWrites) => {
13926
14003
  if (e.button !== 0) {
13927
14004
  // Ignore non-left clicks
13928
- return;
14005
+ return false;
13929
14006
  }
13930
14007
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13931
14008
  // A modifier asks the browser for something other than this tab: a new
13932
14009
  // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13933
14010
  // (alt).
13934
- return;
14011
+ return false;
13935
14012
  }
13936
14013
  if (e.defaultPrevented) {
13937
14014
  // Refused — by the link itself, or by whoever came before it.
13938
- return;
14015
+ return false;
13939
14016
  }
13940
14017
  const linkElement = e.currentTarget;
13941
14018
  if (linkElement.hasAttribute("data-readonly")) {
13942
- return;
14019
+ return false;
13943
14020
  }
13944
14021
  if (linkOpensElsewhere(linkElement)) {
13945
14022
  // Another tab or window, or a download (see link_elsewhere.js).
13946
- return;
14023
+ return false;
13947
14024
  }
13948
14025
  const href = linkElement.href;
13949
14026
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
13950
14027
  getHrefTargetInfo(href);
13951
14028
  if (isEmpty || !isSameOrigin) {
13952
14029
  // Let link to other origins be handled by the browser
13953
- return;
14030
+ return false;
13954
14031
  }
13955
14032
  if (isAnchor) {
13956
14033
  // Fragment navigation belongs to the browser: it owns the indicated
@@ -13962,13 +14039,13 @@ const setupBrowserIntegrationViaHistory = ({
13962
14039
  // change reaches whoever is waiting on the designated element.
13963
14040
  rearmUrlTarget();
13964
14041
  }
13965
- return;
14042
+ return false;
13966
14043
  }
13967
14044
  if (linkAsksForDocument(linkElement)) {
13968
14045
  // The link says its address is another document of this origin (see
13969
14046
  // link_document.js). Routing to it would land on the fallback route
13970
14047
  // instead of loading the page.
13971
- return;
14048
+ return false;
13972
14049
  }
13973
14050
  // Nothing here declared a route, so there is nothing to route to: the
13974
14051
  // page is a plain document and a link in it is a plain link. Taking it
@@ -13976,7 +14053,7 @@ const setupBrowserIntegrationViaHistory = ({
13976
14053
  // the address bar moves and the page does not (see applyRouting's own
13977
14054
  // "not called yet" branch, which is where that used to end up).
13978
14055
  if (!isRouting()) {
13979
- return;
14056
+ return false;
13980
14057
  }
13981
14058
  e.preventDefault();
13982
14059
  handleRoutingTask(href, {
@@ -13990,7 +14067,9 @@ const setupBrowserIntegrationViaHistory = ({
13990
14067
  // whoever knows what to do with it, and it is the anchor itself —
13991
14068
  // resolved here, where it already is.
13992
14069
  element: linkElement,
14070
+ pressWrites,
13993
14071
  });
14072
+ return true;
13994
14073
  };
13995
14074
  // Wired from the capture phase on window, the first place a click is seen,
13996
14075
  // so that a link is answered whatever handler stops the click on its way up
@@ -14170,6 +14249,7 @@ const setupBrowserIntegrationViaHistory = ({
14170
14249
  navBack,
14171
14250
  navForward,
14172
14251
  getDocumentState,
14252
+ carryByLinkNavigation,
14173
14253
  isVisited,
14174
14254
  visitedUrlsSignal,
14175
14255
  };
@@ -14707,8 +14787,32 @@ const useNavStateBasic = (
14707
14787
  // that param for gone. Only with nothing of this document behind (the state
14708
14788
  // was entered on a cold-loaded url, navBack has nowhere to go) does it
14709
14789
  // rewrite in place.
14710
- const leave = ({ isBack } = {}) => {
14790
+ //
14791
+ // event: what asked for it. A leave the press of a link asks for (its
14792
+ // --navi-close) is made by the navigation that link starts: the entry the
14793
+ // link leads to does not hold the key, and every entry behind is left as
14794
+ // the reader saw it — a back finds the state still entered.
14795
+ const leave = ({ isBack, event } = {}) => {
14711
14796
  enteredRef.current = false;
14797
+ if (!Object.hasOwn(browserIntegration.getDocumentState() || {}, id)) {
14798
+ return;
14799
+ }
14800
+ if (
14801
+ event &&
14802
+ browserIntegration.carryByLinkNavigation(event, {
14803
+ editState: (state) => {
14804
+ const stateWithoutKey = { ...state };
14805
+ delete stateWithoutKey[id];
14806
+ return stateWithoutKey;
14807
+ },
14808
+ otherwise: () => leaveEntry({ isBack }),
14809
+ })
14810
+ ) {
14811
+ return;
14812
+ }
14813
+ leaveEntry({ isBack });
14814
+ };
14815
+ const leaveEntry = ({ isBack }) => {
14712
14816
  const currentStateCopy = browserIntegration.getDocumentState() || {};
14713
14817
  if (!Object.hasOwn(currentStateCopy, id)) {
14714
14818
  return;
@@ -14766,6 +14870,9 @@ const useNavStateBasic = (
14766
14870
  * the value allows associating extra data when needed.
14767
14871
  * - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
14768
14872
  * or goes back in history when `type` is "push").
14873
+ * `leave({ event })` with the event that asked for it: asked by the press
14874
+ * of a link that navigates, the key is left out of the entry the link
14875
+ * leads to, and the entry it leaves keeps it.
14769
14876
  */
14770
14877
  const useNavState = useNavStateWithWarnings
14771
14878
  ;
@@ -17996,8 +18103,13 @@ const useOpenPropsEffectOnOpenController = (
17996
18103
  } else {
17997
18104
  // Under type "push" a cancel discards everything written while
17998
18105
  // the popup was open — it goes back with the entry; a confirmed
17999
- // close keeps those writes (see useNavState's own leave()).
18000
- leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
18106
+ // close keeps those writes (see useNavState's own leave()). A
18107
+ // close a link's press asked for is written by the navigation
18108
+ // the link starts.
18109
+ leaveNavState({
18110
+ isBack: Boolean(event?.detail?.isCancel),
18111
+ event,
18112
+ });
18001
18113
  }
18002
18114
  }
18003
18115
  if (signal) {
@@ -18218,8 +18330,9 @@ const useOpenControllerByProps = (props, name) => {
18218
18330
  * opened it, so a section written in a display font gets its tooltips in that
18219
18331
  * font. `color`, `font-size` and `font-weight` are absent too — each surface
18220
18332
  * answers those its own way (a popup and a callout write in
18221
- * --navi-popup-color; a popup follows the size of what opened it, a callout
18222
- * resets to the document's size).
18333
+ * --navi-popup-color; a popup follows the size of what opened it — for a
18334
+ * picker's popup, what is around the picker, see .navi_picker_content — a
18335
+ * callout resets to the document's size).
18223
18336
  *
18224
18337
  * In `@layer navi` so an app that wants one of these back says so on the
18225
18338
  * surface and wins, without having to out-specify anything.
@@ -21909,6 +22022,13 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-readonly");
21909
22022
  CONSTRAINT_ATTRIBUTE_SET.add("data-readonly-reason");
21910
22023
 
21911
22024
  const readOnlyMessage = (field) => {
22025
+ // Read-only from above, in the words of what made it so (a picker's or a
22026
+ // group's readOnlyMessage): what it holds says what it says — over the
22027
+ // network policy's words too, as that control's own words win over them on
22028
+ // itself.
22029
+ if (field.readOnlyMessageFromAbove) {
22030
+ return field.readOnlyMessageFromAbove;
22031
+ }
21912
22032
  // Read-only for a reason the control named itself, read off the reason
21913
22033
  // rather than off the surrounding state: a button read-only for its own
21914
22034
  // reasons, inside a form that happens to be unchanged, is not waiting for
@@ -48049,6 +48169,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
48049
48169
  s.controller.controlHostProps = realUIStateController.controlHostProps;
48050
48170
  s.controller.readOnlyWithoutRun =
48051
48171
  realUIStateController.readOnlyWithoutRun;
48172
+ s.controller.readOnlyMessageFromAbove =
48173
+ realUIStateController.readOnlyMessageFromAbove;
48052
48174
  realUIStateController.pushStateDownToFacadeChild =
48053
48175
  s.controller.pushStateDownToChild;
48054
48176
 
@@ -48472,6 +48594,7 @@ const ControlgroupChildrenWrapper = ({
48472
48594
  required,
48473
48595
  disabled,
48474
48596
  readOnly,
48597
+ readOnlyMessage,
48475
48598
  loading,
48476
48599
  boundAction,
48477
48600
  actionRequesterSignal
@@ -48485,8 +48608,9 @@ const ControlgroupChildrenWrapper = ({
48485
48608
  value: name,
48486
48609
  children: jsx(DisabledContext.Provider, {
48487
48610
  value: disabled,
48488
- children: jsx(ReadOnlyContext.Provider, {
48489
- value: readOnly,
48611
+ children: jsx(ReadOnlyProvider, {
48612
+ readOnly: readOnly,
48613
+ readOnlyMessage: readOnlyMessage,
48490
48614
  children: jsx(RequiredContext.Provider, {
48491
48615
  value: required,
48492
48616
  children: jsx(LoadingContext$1.Provider, {
@@ -48506,6 +48630,34 @@ const ControlgroupChildrenWrapper = ({
48506
48630
  })
48507
48631
  })
48508
48632
  });
48633
+ // What a control hands to what it holds about being read-only: the state, and
48634
+ // the words it is said in. What it holds writes into it, so it is held for the
48635
+ // same reason and says what the control says. Held by the network policy, it
48636
+ // hands down the hold itself — read-only alone, what it holds would refuse in
48637
+ // navi's generic words. Read-only, it hands down its readOnlyMessage, or the
48638
+ // one it was handed.
48639
+ const readOnlyHandedDown = (uiStateController, readOnly) => {
48640
+ const {
48641
+ controlHostProps,
48642
+ props
48643
+ } = uiStateController;
48644
+ const readOnlyResolved = controlHostProps["data-readonly-reason"] === "network-policy" ? "network-policy" : readOnly;
48645
+ if (!readOnlyResolved) {
48646
+ return [readOnlyResolved, undefined];
48647
+ }
48648
+ return [readOnlyResolved, props.readOnlyMessage ?? uiStateController.readOnlyMessageFromAbove];
48649
+ };
48650
+ const ReadOnlyProvider = ({
48651
+ readOnly,
48652
+ readOnlyMessage,
48653
+ children
48654
+ }) => jsx(ReadOnlyContext.Provider, {
48655
+ value: readOnly,
48656
+ children: jsx(ReadOnlyMessageContext.Provider, {
48657
+ value: readOnlyMessage,
48658
+ children: children
48659
+ })
48660
+ });
48509
48661
 
48510
48662
  // What the caller writes for these is called from the control's own reactions
48511
48663
  // (see `reactions` below), so the split leaves them out of both sides.
@@ -49756,7 +49908,7 @@ const useControlgroupProps = (props, {
49756
49908
  basePseudoState
49757
49909
  } = controlgroupProps;
49758
49910
  const disabled = basePseudoState[":disabled"];
49759
- const readOnly = basePseudoState[":read-only"];
49911
+ const [readOnly, readOnlyMessage] = readOnlyHandedDown(uiGroupStateController, basePseudoState[":read-only"]);
49760
49912
  const loading = basePseudoState[":-navi-loading"];
49761
49913
  const controlgroupChildrenWrapperProps = useMemo(() => ({
49762
49914
  uiGroupStateController,
@@ -49764,10 +49916,11 @@ const useControlgroupProps = (props, {
49764
49916
  required: controlgroupProps.required,
49765
49917
  disabled,
49766
49918
  readOnly,
49919
+ readOnlyMessage,
49767
49920
  loading,
49768
49921
  boundAction,
49769
49922
  actionRequesterSignal
49770
- }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequesterSignal]);
49923
+ }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, readOnlyMessage, loading, boundAction, actionRequesterSignal]);
49771
49924
  return [controlRootProps, {
49772
49925
  ...controlgroupProps,
49773
49926
  "name": undefined,
@@ -49963,6 +50116,7 @@ const useInteractiveProps = (props, {
49963
50116
  {
49964
50117
  const controlDisabledFromAbove = useContext(DisabledContext);
49965
50118
  const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
50119
+ const controlReadOnlyMessageFromAbove = useContext(ReadOnlyMessageContext);
49966
50120
  const controlRequired = useContext(RequiredContext);
49967
50121
  const controlLoadingFromAbove = useContext(LoadingContext$1);
49968
50122
  // A SIGNAL holding the element whose press the group's action is wearing
@@ -50002,8 +50156,15 @@ const useInteractiveProps = (props, {
50002
50156
  // pressable on a game nobody may edit. Its own props still hold; only what
50003
50157
  // came from above is dropped (see self_interactions.js for the three modes).
50004
50158
  const zoneStateApplies = !selfInteractionsIgnoreBlock(props);
50159
+ // A press that only leaves the popup it sits in writes nothing to the zone
50160
+ // around it either, so that zone being read-only — or holding a write the
50161
+ // network policy keeps — does not hold it: the popup answers the close
50162
+ // itself. A read-only picker's popup then closes from its own button as
50163
+ // it does from a press outside, and refuses both alike when it holds a
50164
+ // change it cannot send.
50165
+ const zoneReadOnlyApplies = zoneStateApplies && !pressOnlyLeaves(props);
50005
50166
  const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
50006
- const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
50167
+ const controlReadOnly = zoneReadOnlyApplies && controlReadOnlyFromAbove;
50007
50168
  const controlLoading = zoneStateApplies && controlLoadingFromAbove;
50008
50169
  const disabledResolved = disabled || controlDisabled;
50009
50170
  const requiredResolved = required || controlRequired;
@@ -50018,10 +50179,11 @@ const useInteractiveProps = (props, {
50018
50179
  // but cannot be taken.
50019
50180
  const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
50020
50181
  // A write cannot leave under a network policy, so the control asking for
50021
- // one — or sitting in a form that does — refuses before the press and says
50022
- // why (see network_policy.js): a press that looks accepted and fails after
50023
- // is what the policy exists to avoid.
50024
- const heldByNetworkPolicy = networkPolicyReason !== null && (isWriteAction(boundAction) || Boolean(zoneStateApplies && parentAction && isWriteAction(parentAction)));
50182
+ // one — sitting in a form that does, or in what a held control holds (a
50183
+ // picker's popup) — refuses before the press and says why (see
50184
+ // network_policy.js): a press that looks accepted and fails after is what
50185
+ // the policy exists to avoid.
50186
+ const heldByNetworkPolicy = networkPolicyReason !== null && (isWriteAction(boundAction) || Boolean(zoneReadOnlyApplies && parentAction && isWriteAction(parentAction)) || controlReadOnly === "network-policy");
50025
50187
  // Held back because the surface above already has as many runs in flight as
50026
50188
  // it allows (see parallel_guard.js). Only a control that would START one:
50027
50189
  // anything without an action of its own runs nothing and has nothing to
@@ -50040,6 +50202,10 @@ const useInteractiveProps = (props, {
50040
50202
  // still read-only in the tick the run settles — the tick a form's
50041
50203
  // after-send `--navi-reset` asks the form to empty itself.
50042
50204
  uiStateController.readOnlyWithoutRun = Boolean(readOnlyBase);
50205
+ // The words of what made this control read-only from above, when it gave
50206
+ // some: read by READONLY_CONSTRAINT, and handed further down with the
50207
+ // read-only state (see readOnlyHandedDown).
50208
+ uiStateController.readOnlyMessageFromAbove = controlReadOnly ? controlReadOnlyMessageFromAbove : undefined;
50043
50209
  // Read-only, and what this control opens still opens: reading what is in
50044
50210
  // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
50045
50211
  // interaction that only reads through on it.
@@ -50574,6 +50740,12 @@ const splitControlProps = (props, {
50574
50740
  }
50575
50741
  return [controlRootProps, controlHostProps];
50576
50742
  };
50743
+ // What a press leaving a popup asks for: closing it, or cancelling it. Only on
50744
+ // a control running nothing of its own — one with an action writes, wherever
50745
+ // it then leads.
50746
+ const LEAVING_COMMAND_SET = new Set(["--navi-close", "--navi-cancel"]);
50747
+ const pressOnlyLeaves = props => LEAVING_COMMAND_SET.has(props.command) && !props.action;
50748
+
50577
50749
  // `onPointerDown`, `onFocusOut`… but not navi's own events (`onnavi_command`),
50578
50750
  // which keep the element they are dispatched on.
50579
50751
  const isDomListenerProp = key => /^on[A-Z]/.test(key);
@@ -54584,7 +54756,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
54584
54756
  * fetching on a hover.
54585
54757
  * @param {string} [props.command] - What the press asks of a control around
54586
54758
  * the link — `"--navi-close"` on a link that leaves the sheet it is in.
54587
- * Triggered on the press, before the navigation.
54759
+ * Triggered on the press, before the navigation. A popup it closes that
54760
+ * keeps its open state in the history entry (`navState`) is closed on the
54761
+ * entry the navigation leads to: Back finds it open.
54588
54762
  * @param {Function} [props.action] - Work the press runs, before the
54589
54763
  * navigation. Nothing waits for it — not the navigation, not `command`: what
54590
54764
  * the next page must find has to be written synchronously (a draft in a
@@ -59148,7 +59322,7 @@ const css$S = /* css */`@layer navi {
59148
59322
  --button-color: currentColor;
59149
59323
  --button-cursor: pointer;
59150
59324
  --button-font-size: var(--navi-control-font-size);
59151
- --button-font-family: var(--navi-control-font-family, inherit);
59325
+ --button-font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
59152
59326
  --button-border-color-hover: color-mix(in srgb,
59153
59327
  var(--button-border-color) 70%,
59154
59328
  black);
@@ -60893,14 +61067,16 @@ const css$O = /* css */`@layer navi {
60893
61067
  --checkmark-color-disabled: #eee;
60894
61068
  --border-color-disabled-checked: #d3d3d3;
60895
61069
  --background-color-disabled-checked: #d3d3d3;
60896
- --button-border-color: light-dark(#767676, #8e8e93);
60897
- --button-background-color: light-dark(#f3f4f6, #2d3748);
61070
+ --button-border-color: var(--navi-control-border-color);
61071
+ --button-background-color: var(--navi-button-background-color);
60898
61072
  --button-border-color-hover: color-mix(in srgb,
60899
61073
  var(--button-border-color) 70%,
60900
61074
  black);
60901
61075
  --button-background-color-hover: color-mix(in srgb,
60902
61076
  var(--button-background-color) 95%,
60903
61077
  black);
61078
+ --button-background-color-readonly: var(--button-background-color);
61079
+ --button-background-color-disabled: var(--button-background-color-readonly);
60904
61080
  }
60905
61081
 
60906
61082
  .navi_checkbox[data-variant="switch"] {
@@ -60928,19 +61104,40 @@ const css$O = /* css */`@layer navi {
60928
61104
  --margin: 0;
60929
61105
  --width: auto;
60930
61106
  --height: auto;
61107
+ --font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
60931
61108
  --border-color: var(--button-border-color);
60932
61109
  --border-color-hover: var(--button-border-color-hover);
60933
61110
  --background-color: var(--button-background-color);
60934
61111
  --background-color-hover: var(--button-background-color-hover);
60935
61112
  --background-color-readonly: var(--button-background-color-readonly);
60936
61113
  --background-color-disabled: var(--button-background-color-disabled);
61114
+ }
61115
+
61116
+ .navi_checkbox[data-variant="button"]:not([data-label]) {
61117
+ --button-padding-x-default: 0;
61118
+ --button-padding-y-default: 0;
60937
61119
  --border-color-checked: var(--button-border-color);
61120
+ --border-color-hover-checked: var(--button-border-color-hover);
60938
61121
  --background-color-checked: var(--button-background-color);
61122
+ --background-color-hover-checked: var(--button-background-color-hover);
61123
+ --checkmark-color: currentColor;
60939
61124
  }
60940
61125
 
60941
61126
  .navi_checkbox[data-variant="button"][data-label] {
60942
- --border-color-checked: var(--accent-color);
60943
- --background-color-checked: var(--accent-color);
61127
+ --border-radius: var(--navi-control-border-radius);
61128
+ --button-padding-x-default: var(--navi-button-padding-x-default);
61129
+ --button-padding-y-default: var(--navi-button-padding-y-default);
61130
+ line-height: var(--navi-control-line-height);
61131
+ }
61132
+
61133
+ .navi_checkbox[data-variant="button"][data-button-variant="discrete"] {
61134
+ --border-width: 0;
61135
+ --button-border-color: transparent;
61136
+ --button-border-color-hover: var(--button-border-color);
61137
+ --button-background-color: transparent;
61138
+ --button-background-color-hover: color-mix(in srgb,
61139
+ currentColor 8%,
61140
+ var(--button-background-color));
60944
61141
  }
60945
61142
  }
60946
61143
 
@@ -61048,23 +61245,24 @@ const css$O = /* css */`@layer navi {
61048
61245
  --color-mix: var(--color-mix-light);
61049
61246
  }
61050
61247
 
61248
+ & .navi_checkbox_marker {
61249
+ opacity: 0;
61250
+ stroke: var(--x-checkmark-color);
61251
+ transform: scale(.5);
61252
+ }
61253
+
61254
+ &[data-checked] .navi_checkbox_marker {
61255
+ opacity: 1;
61256
+ transition-property: opacity, transform;
61257
+ transition-duration: .15s;
61258
+ transition-timing-function: ease;
61259
+ transform: scale(1);
61260
+ }
61261
+
61051
61262
  &[data-variant="checkbox"] {
61052
61263
  & .navi_checkbox_marker {
61053
- opacity: 0;
61054
61264
  width: 100%;
61055
61265
  height: 100%;
61056
- stroke: var(--x-checkmark-color);
61057
- transform: scale(.5);
61058
- }
61059
-
61060
- &[data-checked] {
61061
- & .navi_checkbox_marker {
61062
- opacity: 1;
61063
- transition-property: opacity, transform;
61064
- transition-duration: .15s;
61065
- transition-timing-function: ease;
61066
- transform: scale(1);
61067
- }
61068
61266
  }
61069
61267
 
61070
61268
  &[data-accent-very-light] {
@@ -61103,6 +61301,11 @@ const css$O = /* css */`@layer navi {
61103
61301
  justify-content: center;
61104
61302
  align-items: center;
61105
61303
 
61304
+ & .navi_checkbox_marker {
61305
+ width: 1em;
61306
+ height: 1em;
61307
+ }
61308
+
61106
61309
  &[data-label] {
61107
61310
  &[data-checked] .navi_checkbox_label {
61108
61311
  color: var(--x-checkmark-color);
@@ -61162,7 +61365,8 @@ const InputCheckboxFieldInterface = props => {
61162
61365
  const {
61163
61366
  icon,
61164
61367
  switch: switchProp,
61165
- variant = icon ? "icon" : switchProp ? "switch" : "checkbox",
61368
+ buttonVariant,
61369
+ variant = buttonVariant ? "button" : icon ? "icon" : switchProp ? "switch" : "checkbox",
61166
61370
  // "checkbox", "switch", "icon", "button"
61167
61371
  accentColor
61168
61372
  } = props;
@@ -61217,7 +61421,9 @@ const InputCheckboxFieldInterface = props => {
61217
61421
  variant: undefined,
61218
61422
  switch: undefined,
61219
61423
  icon: undefined,
61424
+ buttonVariant: undefined,
61220
61425
  "data-variant": variant,
61426
+ "data-button-variant": variant === "button" ? buttonVariant : undefined,
61221
61427
  "data-label": hasLabel ? "" : undefined,
61222
61428
  baseClassName: "navi_checkbox",
61223
61429
  pseudoStateSelector: ".navi_control_input",
@@ -61691,6 +61897,9 @@ const css$M = /* css */`@layer navi {
61691
61897
  --radiomark-color-disabled: #d3d3d3;
61692
61898
  --border-color-checked-disabled: #d3d3d3;
61693
61899
  --background-color-disabled-checked: var(--background-color);
61900
+ --button-padding-x-default: 0;
61901
+ --button-padding-y-default: 0;
61902
+ --button-border-radius: var(--navi-control-border-radius);
61694
61903
  --button-border-width: 1px;
61695
61904
  --button-border-color: transparent;
61696
61905
  --button-background-color: transparent;
@@ -63915,6 +64124,7 @@ const InputPseudoElements = ["::-navi-loader"];
63915
64124
  * boolean?: boolean,
63916
64125
  * selectOnPress?: boolean,
63917
64126
  * variant?: "checkbox" | "switch" | "icon" | "button",
64127
+ * buttonVariant?: "discrete",
63918
64128
  * actionVerb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
63919
64129
  * [key: string]: any,
63920
64130
  * }>}
@@ -63930,6 +64140,10 @@ const InputPseudoElements = ["::-navi-loader"];
63930
64140
  * is a square around its checkmark (or its `icon`, which takes an
63931
64141
  * `[off, on]` pair). Bound with `signal`, it follows whatever else writes
63932
64142
  * that signal.
64143
+ * @param {"discrete"} [buttonVariant] A checkbox drawn as a button, drawn as
64144
+ * that variant of `Button`: `"discrete"` has no box at rest and a wash on
64145
+ * hover (checked, its words still fill with the accent). Implies
64146
+ * `variant="button"`.
63933
64147
  * @param {"GET"|"POST"|"PUT"|"PATCH"|"DELETE"} [actionVerb] What a plain
63934
64148
  * function `action` does to the server. A write verb puts the field under
63935
64149
  * the network policy: held read-only up front, with the policy's message,
@@ -73258,6 +73472,10 @@ const UncontrolledDialog = props => {
73258
73472
  // needs the dialog element to look at what it contains.
73259
73473
  const defaultRef = useRef();
73260
73474
  props.ref = props.ref || defaultRef;
73475
+ // The close is built once per opening (see useOpenController below): read
73476
+ // through a ref, an onClose given by a render while open is the one called.
73477
+ const onCloseRef = useRef(null);
73478
+ onCloseRef.current = props.onClose;
73261
73479
  const openController = useOpenController(openEvent => {
73262
73480
  const dialogEl = props.ref.current;
73263
73481
  debugPopup(openEvent, `dialog opened`);
@@ -73285,7 +73503,7 @@ const UncontrolledDialog = props => {
73285
73503
  requestCloseEvent.preventDefault();
73286
73504
  },
73287
73505
  onClose: closeEvent => {
73288
- props.onClose?.(closeEvent);
73506
+ onCloseRef.current?.(closeEvent);
73289
73507
  }
73290
73508
  };
73291
73509
  });
@@ -75232,6 +75450,9 @@ const UncontrolledPopover = props => {
75232
75450
  // the popover element to read what it holds.
75233
75451
  const defaultRef = useRef();
75234
75452
  props.ref = props.ref || defaultRef;
75453
+ // Read at the close, not captured at the opening: see UncontrolledDialog.
75454
+ const onCloseRef = useRef(null);
75455
+ onCloseRef.current = props.onClose;
75235
75456
  // Same as Dialog's own UncontrolledDialog, for the same reasons — see it for
75236
75457
  // the full story: a popover is a surface, so the only thing it has to say
75237
75458
  // about closing is that what it contains must not be interrupted mid-action.
@@ -75257,7 +75478,7 @@ const UncontrolledPopover = props => {
75257
75478
  requestCloseEvent.preventDefault();
75258
75479
  },
75259
75480
  onClose: closeEvent => {
75260
- props.onClose?.(closeEvent);
75481
+ onCloseRef.current?.(closeEvent);
75261
75482
  }
75262
75483
  };
75263
75484
  });
@@ -77117,6 +77338,15 @@ const PickerCustom = props => {
77117
77338
  // closed (see onClose below) — a press that ran before the close would make
77118
77339
  // the picker busy, and a busy picker refuses the very close that follows.
77119
77340
  const confirmEventRef = useRef(null);
77341
+ // The close is built once per opening (see useOpenController below), but
77342
+ // these props answer how it ends: read through a ref, a render while open
77343
+ // (a sheet turned into the next question) is heard by the close.
77344
+ const closePropsRef = useRef(null);
77345
+ closePropsRef.current = {
77346
+ canSendWhileUnchanged,
77347
+ onConfirm,
77348
+ onClose
77349
+ };
77120
77350
  // aria-controls + id
77121
77351
  const popupId = `${props.id}_picker_popup`;
77122
77352
  {
@@ -77217,7 +77447,7 @@ const PickerCustom = props => {
77217
77447
  // and for that picker the same value said again means something.
77218
77448
  // A press outside, Escape, the cross say nothing and keep skipping.
77219
77449
  const said = requestCloseEvent.detail.isSend || confirmEventRef.current;
77220
- if (!(canSendWhileUnchanged && said)) {
77450
+ if (!(closePropsRef.current.canSendWhileUnchanged && said)) {
77221
77451
  // No action to run, but still allow the close.
77222
77452
  return;
77223
77453
  }
@@ -77289,7 +77519,7 @@ const PickerCustom = props => {
77289
77519
  const confirmEvent = confirmEventRef.current;
77290
77520
  confirmEventRef.current = null;
77291
77521
  if (confirmEvent && !closeEvent.detail.isCancel) {
77292
- onConfirm?.(confirmEvent);
77522
+ closePropsRef.current.onConfirm?.(confirmEvent);
77293
77523
  }
77294
77524
  // After the value bookkeeping above — whoever listens reads the
77295
77525
  // picker's value as it ends up, committed or restored on a cancel —
@@ -77300,7 +77530,7 @@ const PickerCustom = props => {
77300
77530
  // spells into the address — a route param naming what was being
77301
77531
  // edited, cleared now that nothing is — has to be written before,
77302
77532
  // or the landing puts it back.
77303
- onClose?.(closeEvent);
77533
+ closePropsRef.current.onClose?.(closeEvent);
77304
77534
  leaveExpanded({
77305
77535
  isBack: closeEvent.detail.isCancel
77306
77536
  });
@@ -77390,6 +77620,11 @@ const PickerCustom = props => {
77390
77620
  // button) hangs its popup off the whole control instead.
77391
77621
  anchor: props.anchor || props.ref,
77392
77622
  openController,
77623
+ // Before the content is built and shown, so it is laid out and placed
77624
+ // at the size it is read at (see .navi_picker_content in picker.jsx).
77625
+ onOpen: () => {
77626
+ writeContainerFont(ref.current, popupRef.current);
77627
+ },
77393
77628
  // A picker whose value was never given to it reads it off the control in
77394
77629
  // its popup (see useUIFacadeStateController), so that control is built as
77395
77630
  // soon as something reads the value: at mount when the picker sees the
@@ -78214,6 +78449,30 @@ const showNativePicker = (pickerInput, event) => {
78214
78449
  }
78215
78450
  };
78216
78451
 
78452
+ // The popup of a picker is opened where the picker stands, so it writes at the
78453
+ // size and weight of what is around it — the ones a Dialog rendered there
78454
+ // inherits — and never at the ones the trigger is drawn at. CSS cannot reach
78455
+ // past the picker's own: its root holds the popup and sets both. Written on the
78456
+ // holder only when they differ, so an opening that changes nothing invalidates
78457
+ // no style.
78458
+ const writeContainerFont = (pickerEl, popupEl) => {
78459
+ const container = pickerEl?.parentElement;
78460
+ const holder = popupEl?.closest("[data-picker-content]");
78461
+ if (!container || !holder) {
78462
+ return;
78463
+ }
78464
+ const {
78465
+ fontSize,
78466
+ fontWeight
78467
+ } = getComputedStyle(container);
78468
+ if (holder.style.fontSize !== fontSize) {
78469
+ holder.style.fontSize = fontSize;
78470
+ }
78471
+ if (holder.style.fontWeight !== fontWeight) {
78472
+ holder.style.fontWeight = fontWeight;
78473
+ }
78474
+ };
78475
+
78217
78476
  installImportMetaCssBuild(import.meta);
78218
78477
  const css$y = /* css */`.navi_picker_confirm_body {
78219
78478
  min-width: 180px;
@@ -80181,11 +80440,16 @@ const ListItemSelectable = props => {
80181
80440
  // A list waiting on something is not unavailable, it is not answering yet —
80182
80441
  // busy rather than read-only (see BUSY_CONSTRAINT, which is asked first),
80183
80442
  // and the wait is about the same thing the read-only would be about.
80443
+ //
80444
+ // Unless what made the list read-only says it better: in words of its own (a
80445
+ // picker's or a list's readOnlyMessage), or because it is the network
80446
+ // policy holding the write the selection would make.
80184
80447
  const readOnlyFromAbove = useContext(ReadOnlyContext);
80448
+ const readOnlyMessageFromAbove = useContext(ReadOnlyMessageContext);
80185
80449
  const readOnlyMessageKey = readOnlyFromAbove ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
80186
80450
  // Handed to the row as well as to the control: whichever of the two catches
80187
80451
  // the press answers it, and they must not answer it differently.
80188
- const readOnlyMessageResolved = props.readOnlyMessage ?? constraintI18n(readOnlyMessageKey, props);
80452
+ const readOnlyMessageResolved = props.readOnlyMessage ?? (readOnlyFromAbove ? readOnlyMessageFromAbove : undefined) ?? (readOnlyFromAbove === "network-policy" ? getNetworkPolicyReadOnlyMessage() : constraintI18n(readOnlyMessageKey, props));
80189
80453
  const busyMessageResolved = props.busyMessage ?? (loading ? listItemBusyMessage(loading, props) : constraintI18n(multiple ? `constraint.busy.selection` : `constraint.busy.choice`, props));
80190
80454
  const inputRef = useRef();
80191
80455
  const inputType = multiple ? "checkbox" : "radio";
@@ -88978,6 +89242,7 @@ const css$r = /* css */`@layer navi {
88978
89242
  .navi_picker:is([data-variant="button"], [data-variant="border"], [data-variant="discrete-border"]) {
88979
89243
  --picker-padding-x-default: var(--navi-button-padding-x-default);
88980
89244
  --picker-padding-y-default: var(--navi-button-padding-y-default);
89245
+ --picker-font-family: var(--navi-button-font-family, var(--navi-control-font-family, inherit));
88981
89246
  --picker-align-x-default: center;
88982
89247
  --picker-background-color: var(--navi-button-background-color);
88983
89248
  --picker-border-color-readonly: color-mix(in srgb,
@@ -89277,8 +89542,9 @@ const css$r = /* css */`@layer navi {
89277
89542
  --x-corner-top-right-radius: initial;
89278
89543
  --x-corner-bottom-right-radius: initial;
89279
89544
  --x-corner-bottom-left-radius: initial;
89280
- font-size: var(--navi-control-font-size);
89545
+ font-weight: initial;
89281
89546
  text-align: initial;
89547
+ font-size: 1rem;
89282
89548
  display: contents;
89283
89549
  }
89284
89550
 
@@ -89521,9 +89787,10 @@ const PickerButton = props => {
89521
89787
  // The same chain useControlProps resolves (own prop first, then what the
89522
89788
  // group above says), for the two things it does not carry: the popup content,
89523
89789
  // which is read-only along with the picker, and the cursor, which stays a
89524
- // pointer on a picker that still opens.
89790
+ // pointer on a picker that still opens. A hold of the network policy goes
89791
+ // with it: what the popup changes is what the picker would send.
89525
89792
  const readOnlyFromAbove = useContext(ReadOnlyContext);
89526
- const readOnlyResolved = readOnly || readOnlyFromAbove;
89793
+ const [readOnlyResolved, readOnlyMessageResolved] = readOnlyHandedDown(uiStateController, readOnly || readOnlyFromAbove);
89527
89794
  // Read off the controller rather than worked out again: what makes a
89528
89795
  // read-only picker open is settled once, where the gate reads it (see
89529
89796
  // createControlInfo's readOnlyOpens). Needed here for the cursor.
@@ -89571,11 +89838,13 @@ const PickerButton = props => {
89571
89838
  /* Read-only crosses into everything the picker is made of: what it really
89572
89839
  holds is drawn by controls of their own — in the popup, and on the façade
89573
89840
  where an application puts its own affordances — and each of them refuses
89574
- in its own words once told. Said from the read-only state alone, never
89841
+ once told, in the picker's words when it gave some (readOnlyMessage),
89842
+ in its own otherwise. Said from the read-only state alone, never
89575
89843
  from the busy one — an action running for a moment is not the same thing
89576
89844
  as a value nobody may change. */
89577
- jsx(ReadOnlyContext.Provider, {
89578
- value: readOnlyResolved,
89845
+ jsx(ReadOnlyProvider, {
89846
+ readOnly: readOnlyResolved,
89847
+ readOnlyMessage: readOnlyMessageResolved,
89579
89848
  children: jsxs(Box
89580
89849
  // A word in a sentence (variant="text") sits in a <p>, where a <div> is
89581
89850
  // not allowed: phrasing content there, block content elsewhere.