@jsenv/navi 0.29.488 → 0.29.489

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.
@@ -993,6 +993,10 @@ const buttonI18n = /*#__PURE__*/ defineNaviMessages({
993
993
  en: "Copied!",
994
994
  fr: "Copié !",
995
995
  },
996
+ "button.copy_refused": {
997
+ en: "Could not copy.",
998
+ fr: "Impossible de copier.",
999
+ },
996
1000
  });
997
1001
 
998
1002
  // Spin messages — the ends of what one steps through when the control holding
@@ -2755,18 +2759,18 @@ const debounceSignal = (
2755
2759
  signalToDebounce,
2756
2760
  { delay = 300, deepCompare = true } = {},
2757
2761
  ) => {
2762
+ const isSameValue = (a, b) =>
2763
+ deepCompare ? compareTwoJsValues(a, b) : a === b;
2758
2764
  let timeoutId;
2759
2765
  let latestValue = signalToDebounce.peek();
2760
- const debouncedSignal = signal(latestValue);
2761
-
2762
- effect(() => {
2766
+ // Following the source subscribes to it, and through it to whatever it reads:
2767
+ // an app-level signal it reads would keep it — and its timer — alive for
2768
+ // good. So it is followed only while something reads the debounced value.
2769
+ let stopFollowing = null;
2770
+ const follow = () => {
2763
2771
  const value = signalToDebounce.value;
2764
2772
  const debouncedValue = debouncedSignal.peek();
2765
- if (
2766
- deepCompare
2767
- ? compareTwoJsValues(value, debouncedValue)
2768
- : value === debouncedValue
2769
- ) {
2773
+ if (isSameValue(value, debouncedValue)) {
2770
2774
  // The signal came back to where the debounced one already is: whatever it
2771
2775
  // passed through on the way is not on its way any more. Leaving the timer
2772
2776
  // armed would publish that in-between value once the delay elapses, and
@@ -2781,11 +2785,37 @@ const debounceSignal = (
2781
2785
  timeoutId = setTimeout(() => {
2782
2786
  debouncedSignal.value = latestValue;
2783
2787
  }, delay);
2788
+ };
2789
+ const catchUp = () => {
2790
+ const value = signalToDebounce.peek();
2791
+ if (!isSameValue(value, debouncedSignal.peek())) {
2792
+ debouncedSignal.value = value;
2793
+ }
2794
+ latestValue = debouncedSignal.peek();
2795
+ };
2796
+ const debouncedSignal = signal(latestValue, {
2797
+ watched: () => {
2798
+ // Nobody was waiting on what the source said while nothing read it:
2799
+ // that is taken at once, not after a delay.
2800
+ catchUp();
2801
+ stopFollowing = effect(follow);
2802
+ },
2803
+ unwatched: () => {
2804
+ stopFollowing();
2805
+ stopFollowing = null;
2806
+ clearTimeout(timeoutId);
2807
+ timeoutId = undefined;
2808
+ },
2784
2809
  });
2785
2810
 
2786
2811
  debouncedSignal.flush = () => {
2787
2812
  clearTimeout(timeoutId);
2788
- debouncedSignal.value = latestValue;
2813
+ timeoutId = undefined;
2814
+ if (stopFollowing) {
2815
+ debouncedSignal.value = latestValue;
2816
+ return;
2817
+ }
2818
+ catchUp();
2789
2819
  };
2790
2820
  // Whoever reads the debounced value gets the previous one during the delay,
2791
2821
  // with nothing saying a newer one is on its way. This says it.
@@ -4936,10 +4966,46 @@ const createActionProxyFromSignal = (
4936
4966
  });
4937
4967
  actionWeakMap.set(actionProxy, actionProxy);
4938
4968
 
4969
+ // Following the params subscribes to them, and through them to whatever
4970
+ // they read: a component's computed params reading an app-level signal stay
4971
+ // reachable from that signal for as long as anything listens to them. So a
4972
+ // binding follows its params only while something reads what it says (a
4973
+ // mounted component, a run's subscriber), and catches up when read again;
4974
+ // unread, every method retargets first (see proxyMethod). A binding asked to
4975
+ // act on their changes (runOnce, rerunOnChange, onChange: a route action,
4976
+ // actionRunEffect) follows them for as long as it exists.
4977
+ const followsParamsAlways = runOnce || rerunOnChange || Boolean(onChange);
4978
+ let readerCount = 0;
4979
+ let stopFollowingParams = null;
4980
+ const readerWatchOptions = followsParamsAlways
4981
+ ? undefined
4982
+ : {
4983
+ watched: () => {
4984
+ readerCount++;
4985
+ if (readerCount === 1) {
4986
+ const stopMirroringParams = effect(() => {
4987
+ proxyParamsSignal.value = paramsSignal.value;
4988
+ });
4989
+ const stopRetargeting = effect(retargetOnParamsChange);
4990
+ stopFollowingParams = () => {
4991
+ stopMirroringParams();
4992
+ stopRetargeting();
4993
+ };
4994
+ }
4995
+ },
4996
+ unwatched: () => {
4997
+ readerCount--;
4998
+ if (readerCount === 0) {
4999
+ stopFollowingParams();
5000
+ stopFollowingParams = null;
5001
+ }
5002
+ },
5003
+ };
5004
+
4939
5005
  // Create our own signal for params that we control completely
4940
5006
  const proxyParamsSignal = signal(paramsSignal.value);
4941
5007
  const proxySignal = (signalPropertyName, propertyName) => {
4942
- const signalProxy = signal();
5008
+ const signalProxy = signal(undefined, readerWatchOptions);
4943
5009
  let dispose;
4944
5010
  onActionTargetChange(() => {
4945
5011
  if (dispose) {
@@ -5066,20 +5132,34 @@ const createActionProxyFromSignal = (
5066
5132
  }
5067
5133
  // Watch for changes in the original paramsSignal and update ours
5068
5134
  // (original signal wins over any replaceParams calls)
5069
- weakEffect(
5070
- [paramsSignal, proxyParamsSignal],
5071
- (paramsSignalRef, proxyParamsSignalRef) => {
5072
- const newParams = paramsSignalRef.value;
5073
- proxyParamsSignalRef.value = newParams;
5074
- },
5075
- );
5076
- weakEffect([action], () => {
5135
+ const retargetOnParamsChange = () => {
5077
5136
  // eslint-disable-next-line no-unused-expressions
5078
5137
  proxyParamsSignal.value;
5079
5138
  _updateTarget({
5080
5139
  changeCause: "params_signal_change",
5081
5140
  });
5082
- });
5141
+ };
5142
+ if (followsParamsAlways) {
5143
+ weakEffect(
5144
+ [paramsSignal, proxyParamsSignal],
5145
+ (paramsSignalRef, proxyParamsSignalRef) => {
5146
+ const newParams = paramsSignalRef.value;
5147
+ proxyParamsSignalRef.value = newParams;
5148
+ },
5149
+ );
5150
+ weakEffect([action], retargetOnParamsChange);
5151
+ } else {
5152
+ // The first target is resolved now, as following would resolve it: the
5153
+ // subscribers wired above read what it says from the start (see
5154
+ // onActionTargetChange above). Untracked, as inside the effect that
5155
+ // resolves it otherwise: a binding is made during a render, which must
5156
+ // not subscribe to what retargeting reads.
5157
+ untracked(() => {
5158
+ _updateTarget({
5159
+ changeCause: "params_signal_change",
5160
+ });
5161
+ });
5162
+ }
5083
5163
  actionProxy.replaceParams = (newParams) => {
5084
5164
  if (currentAction === action) {
5085
5165
  const currentParams = proxyParamsSignal.value;
@@ -6068,6 +6148,10 @@ const isPickerBoundaryFor = (candidate, el) => {
6068
6148
  */
6069
6149
  const getParentControl = (el) => {
6070
6150
  const ownControlRoot = el.closest("[navi-control]");
6151
+ if (!ownControlRoot) {
6152
+ // In no control at all (a list's root, a plain element): none around it.
6153
+ return null;
6154
+ }
6071
6155
  const parentControlRoot = ownControlRoot.parentNode.closest("[navi-control]");
6072
6156
  return parentControlRoot;
6073
6157
  };
@@ -6543,10 +6627,14 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
6543
6627
  // signal wants a date field — but a boolean one maps to a checkbox, and
6544
6628
  // a picker made into a checkbox is not a picker with a different look:
6545
6629
  // it is another control, with no popup to open. A picker asked to hold
6546
- // a yes/no keeps its two rows and stays itself.
6630
+ // a yes/no keeps its two rows and stays itself. Nor does a picker with
6631
+ // a popup of its own become the field for that type: it is what its
6632
+ // popup is — a frise one slides in is no number one types.
6547
6633
  const wouldChangeWhatTheControlIs =
6548
6634
  controlType === "picker" &&
6549
- (typeFromSignal === "checkbox" || typeFromSignal === "radio");
6635
+ (props.children !== undefined ||
6636
+ typeFromSignal === "checkbox" ||
6637
+ typeFromSignal === "radio");
6550
6638
  if (typeFromSignal !== undefined && !wouldChangeWhatTheControlIs) {
6551
6639
  props.type = typeFromSignal;
6552
6640
  }
@@ -7818,7 +7906,8 @@ const STATE_SIGNAL_OPTIONS = new Set([
7818
7906
  * deploys holds one copy, not ten. Only the storage key changes, the id the url reads stays `id`.
7819
7907
  * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
7820
7908
  * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
7821
- * `"time"`…). Without it the value is not coerced.
7909
+ * `"time"`…). Without it the value is not coerced. On a type that is not text (`"string"`, `"email"`,
7910
+ * `"url"`), `""` — what a cleared control writes — is absence.
7822
7911
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
7823
7912
  * Used when reading the value back from a url search param, where everything is a string:
7824
7913
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -7913,6 +8002,8 @@ const STATE_SIGNAL_OPTIONS = new Set([
7913
8002
  * const childTab = stateSignal(parentTab);
7914
8003
  * // childTab follows parentTab changes unless explicitly set
7915
8004
  */
8005
+ // The types "" is a value of: an empty text is still a text.
8006
+ const TEXT_TYPE_SET = new Set(["string", "email", "url"]);
7916
8007
  const stateSignal = (defaultValue, options = {}) => {
7917
8008
  const {
7918
8009
  id,
@@ -8104,8 +8195,13 @@ const stateSignal = (defaultValue, options = {}) => {
8104
8195
  };
8105
8196
 
8106
8197
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
8198
+ // Whether "" says nothing at all: on a type that is not text, it is what a
8199
+ // cleared control writes (an empty field, a picker's cross) — no number, no
8200
+ // date — and kept, it would be an invalid value the address carries.
8201
+ const emptyStringIsAbsence =
8202
+ type !== undefined && !TEXT_TYPE_SET.has(type);
8107
8203
  const processValue = (value) => {
8108
- if (value === undefined) {
8204
+ if (value === undefined || (value === "" && emptyStringIsAbsence)) {
8109
8205
  // Absence is never validated, and the verdict on the previous value
8110
8206
  // (validSignal, validity.valid) must not outlive it.
8111
8207
  clearValidity();
@@ -15651,6 +15747,14 @@ PSEUDO_CLASSES[":active"] = {
15651
15747
  // focus_classes block below. At module scope so isMatchingFocusVisible (used
15652
15748
  // here and in control_hooks.jsx) can read it.
15653
15749
  let keyboardNavigationUsed = false;
15750
+ // The element a caller focused saying the ring must show (`focusVisible: true`
15751
+ // to moveFocusTo or autoFocus): it knows better than the modality — a screen
15752
+ // showing where Enter goes before any key was pressed. Until a pointer goes
15753
+ // down, which says the hand is elsewhere.
15754
+ let focusVisibleAskedElement = null;
15755
+ const askFocusVisible = (element) => {
15756
+ focusVisibleAskedElement = element;
15757
+ };
15654
15758
 
15655
15759
  // HOW FOCUS-VISIBLE IS DECIDED (the details behind isMatchingFocusVisible)
15656
15760
  //
@@ -15680,7 +15784,10 @@ let keyboardNavigationUsed = false;
15680
15784
  //
15681
15785
  // `keyboardNavigationUsed` has no such blind spot — it is set by navigation
15682
15786
  // keydowns and cleared by pointerdown, whatever anyone prevents afterwards
15683
- // — so it is the authority for everything rule 1 does not cover.
15787
+ // — so it is the authority for everything rule 1 does not cover. Save for
15788
+ // the element a caller explicitly focused with a ring (askFocusVisible):
15789
+ // that is somebody knowing better than the last interaction, not a state
15790
+ // left behind.
15684
15791
  //
15685
15792
  // Ring INHERITANCE (a controlled element ringing because its aria-controls
15686
15793
  // controller is focused) stays gated on the keyboard modality even for an
@@ -15703,7 +15810,7 @@ const isMatchingFocusVisible = (el) => {
15703
15810
  return false;
15704
15811
  }
15705
15812
  if (!keyboardNavigationUsed) {
15706
- return false;
15813
+ return el === focusVisibleAskedElement;
15707
15814
  }
15708
15815
  return true;
15709
15816
  };
@@ -15824,6 +15931,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
15824
15931
  "pointerdown",
15825
15932
  () => {
15826
15933
  keyboardNavigationUsed = false;
15934
+ focusVisibleAskedElement = null;
15827
15935
  },
15828
15936
  { capture: true },
15829
15937
  );
@@ -17000,7 +17108,8 @@ const retryWhenPlaceable = (
17000
17108
  * @param {object} [options]
17001
17109
  * @param {boolean} [options.focusVisible] Decides the ring instead of the
17002
17110
  * modality — for a caller that knows better than the last interaction does
17003
- * (a transfer speaking for the gesture that opened a container, say).
17111
+ * (a transfer speaking for the gesture that opened a container, say). `true`
17112
+ * draws the ring even before any key was pressed, until a pointer goes down.
17004
17113
  * @param {boolean} [options.preventScroll=true] Pass false to let the browser
17005
17114
  * scroll the element into view.
17006
17115
  */
@@ -17008,6 +17117,9 @@ const moveFocusTo = (
17008
17117
  element,
17009
17118
  { focusVisible, preventScroll = true } = {},
17010
17119
  ) => {
17120
+ if (focusVisible === true) {
17121
+ askFocusVisible(element);
17122
+ }
17011
17123
  element.focus({
17012
17124
  preventScroll,
17013
17125
  focusVisible:
@@ -17185,6 +17297,14 @@ document.addEventListener(
17185
17297
  const openedDuringThisPress = (openController) =>
17186
17298
  openController.pressCountAtOpen === pressCount;
17187
17299
 
17300
+ /**
17301
+ * Which press the events landing now belong to: the pointerdown, mousedown and
17302
+ * click of one press read the same count, a tap's compatibility events
17303
+ * included (see above). A click the keyboard makes brings no pointerdown, so it
17304
+ * reads the count of the last press: tell it apart by `event.detail === 0`.
17305
+ */
17306
+ const readPressCount = () => pressCount;
17307
+
17188
17308
  // How long a popup waits before handing the focus to a field, when giving it
17189
17309
  // is what raises the on-screen keyboard.
17190
17310
  //
@@ -17577,6 +17697,14 @@ const createOpenController = (
17577
17697
  // whole point: learning it afterwards means the content mounted on the
17578
17698
  // previous subject first.
17579
17699
  onOpen: null,
17700
+ // What the popup is open ON, as its content reads it (usePopupValue in
17701
+ // popup_value.js): written at the opening, right after `onOpen`, and left
17702
+ // alone by the close, so the content keeps drawing it while it leaves.
17703
+ popupValueSignal: signal(undefined),
17704
+ // How the opening reads that value, set by whoever holds the open state
17705
+ // (see useOpenPropsEffectOnOpenController); left unset, the value the
17706
+ // opening carries (a --navi-open source's).
17707
+ readPopupValue: null,
17580
17708
  // What an Escape means for this popup, whoever heard the key — its own
17581
17709
  // shortcut, its trigger's, the group it stands in, a bubble's: a cancel
17582
17710
  // (what it holds reverts), unless its owner says otherwise (a Picker's
@@ -17711,6 +17839,9 @@ const createOpenController = (
17711
17839
  // openEffect, which shows it: what the popup opens ON has to be
17712
17840
  // known before either (see `onOpen` above).
17713
17841
  controller.onOpen?.(requestOpenEvent);
17842
+ controller.popupValueSignal.value = controller.readPopupValue
17843
+ ? controller.readPopupValue(requestOpenEvent)
17844
+ : requestOpenEvent.detail.value;
17714
17845
  // After prepareFocusTransfer, which has to record what held the
17715
17846
  // focus before anything inside the popup can claim it, and before
17716
17847
  // openEffect, which measures the popup to place it.
@@ -18090,6 +18221,39 @@ const useOpenPropsEffectOnOpenController = (
18090
18221
  if (signal && open) {
18091
18222
  openSignalValueRef.current = signal.value;
18092
18223
  }
18224
+ // What the popup is open ON (see usePopupValue): the id the signal names
18225
+ // when it holds more than "open", else the popup's own `value`, else what the
18226
+ // opening carries. Read once `onOpen` has run, which may write the signal.
18227
+ openController.readPopupValue = (requestOpenEvent) => {
18228
+ if (signal) {
18229
+ const signalValue = signal.peek();
18230
+ if (signalValue !== true && readOpened(signalValue, value)) {
18231
+ return signalValue;
18232
+ }
18233
+ }
18234
+ if (value !== undefined) {
18235
+ return value;
18236
+ }
18237
+ return requestOpenEvent.detail.value;
18238
+ };
18239
+ // The signal walked to another id while open (a sheet's "next"): the
18240
+ // content follows it. Written closed, it is left alone — that is the close
18241
+ // starting, and the content still has its exit to draw.
18242
+ useLayoutEffect(() => {
18243
+ if (!signal) {
18244
+ return undefined;
18245
+ }
18246
+ return effect(() => {
18247
+ const signalValue = signal.value;
18248
+ if (
18249
+ openController.opened &&
18250
+ signalValue !== true &&
18251
+ readOpened(signalValue, value)
18252
+ ) {
18253
+ openController.popupValueSignal.value = signalValue;
18254
+ }
18255
+ });
18256
+ }, [signal, value]);
18093
18257
  // Assigned on every render, like openEffect, so it always closes over the
18094
18258
  // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
18095
18259
  // --navi-close command) writes what happened where the caller keeps it, so
@@ -26049,6 +26213,14 @@ const arraySignalStore = (
26049
26213
  if (props === null || typeof props !== "object") {
26050
26214
  return props;
26051
26215
  }
26216
+ if (props[idKey] === undefined) {
26217
+ // Kept under `undefined`, every such item would be one and the same: a
26218
+ // list drawing its last row as many times as it has rows, a creation
26219
+ // rewriting all of them — and nothing saying why.
26220
+ throw new Error(
26221
+ `${name || "store"}: an item arrived without "${idKey}" (it has ${Object.keys(props).join(", ")}) — declare its key with idKey, or give a single item an id of its own ({ id: "me", … })`,
26222
+ );
26223
+ }
26052
26224
  const item = createItem(props);
26053
26225
  return item;
26054
26226
  };
@@ -27234,13 +27406,12 @@ const createResourceLifecycleManager = () => {
27234
27406
  if (!isSameResource && !isDependent) {
27235
27407
  continue;
27236
27408
  }
27409
+ // A write of a resource this one depends on reaches its lists as it
27410
+ // reruns its GET and GET_MANY: being heard is the whole condition (see
27411
+ // findEffectOnActions). rerunOn is about this resource's own writes.
27237
27412
  if (
27238
- !shouldReadRerunAfter(
27239
- "GET_RANGE",
27240
- config,
27241
- triggeringAction,
27242
- isSameResource,
27243
- )
27413
+ isSameResource &&
27414
+ !shouldReadRerunAfter("GET_RANGE", config, triggeringAction, true)
27244
27415
  ) {
27245
27416
  continue;
27246
27417
  }
@@ -30437,6 +30608,17 @@ const useActionAsyncData = (action, {
30437
30608
  useOnLoad(action, onLoad);
30438
30609
  usePoll(action, poll);
30439
30610
 
30611
+ // The instance the binding points at, remembered from one render to the
30612
+ // next: a binding that comes back to params whose instance failed — a search
30613
+ // typed again after a network blip — asks again, as typing them the first
30614
+ // time did. Staying on the instance that just failed asks nothing (see
30615
+ // runIsOwed), and neither does a mount, which never switched to anything.
30616
+ // Read as it stands: getCurrentAction() would flush a debounced binding,
30617
+ // publishing every keystroke the moment it renders.
30618
+ const instance = action.isProxy ? getActionPrivateProperties(action).currentAction : action;
30619
+ const instanceRef = useRef(instance);
30620
+ const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
30621
+ instanceRef.current = instance;
30440
30622
  // `run: true` — this component owns the request, so it is what starts it, and
30441
30623
  // it starts it from the render rather than from an effect: a component that
30442
30624
  // suspends has no effects, so a run written in one would be waiting for
@@ -30444,7 +30626,7 @@ const useActionAsyncData = (action, {
30444
30626
  // no-op — which is what makes starting it while rendering sound. From there
30445
30627
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
30446
30628
  // component under `loading: true`, exactly as for data someone else ran.
30447
- if (run && action.paramsSignal.peek() !== undefined && runIsOwed(action)) {
30629
+ if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
30448
30630
  // Nothing waits on this run: the failure is held by the action, and this
30449
30631
  // hook is what reads it back.
30450
30632
  runUnwatched(() => action.run({
@@ -32082,6 +32264,38 @@ const resolveContainerLength = (size, element) => {
32082
32264
  return (parseFloat(amount) / 100) * containerSize;
32083
32265
  };
32084
32266
 
32267
+ // "s", "-m" — the spacing scale, whose tokens are ems of the font where they
32268
+ // apply: a length only CSS knows, handed back as the var() it maps to, which
32269
+ // nothing can place with. Read off the given element (the root when none is),
32270
+ // for a caller asking for a number.
32271
+ const SCALE_WORD_REGEX = /^(-?)(xxs|xs|xl|xxl|[lms])$/;
32272
+ const TOKEN_LENGTH_REGEX = /^(-?[\d.]+)(px|r?em)$/;
32273
+ const resolveScaleLength = (size, element = document.documentElement) => {
32274
+ if (typeof size !== "string") {
32275
+ return null;
32276
+ }
32277
+ const match = SCALE_WORD_REGEX.exec(size);
32278
+ if (!match) {
32279
+ return null;
32280
+ }
32281
+ const [, sign, word] = match;
32282
+ const computedStyle = getComputedStyle(element);
32283
+ const token = computedStyle.getPropertyValue(`--navi-${word}`).trim();
32284
+ const tokenMatch = TOKEN_LENGTH_REGEX.exec(token);
32285
+ if (!tokenMatch) {
32286
+ return null;
32287
+ }
32288
+ const [, amount, unit] = tokenMatch;
32289
+ let unitInPx = 1;
32290
+ if (unit === "em") {
32291
+ unitInPx = parseFloat(computedStyle.fontSize);
32292
+ } else if (unit === "rem") {
32293
+ unitInPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
32294
+ }
32295
+ const length = parseFloat(amount) * unitInPx;
32296
+ return sign ? -length : length;
32297
+ };
32298
+
32085
32299
  const resolveSpacingSize = (size, element, property = "padding") => {
32086
32300
  const viewportLength = resolveViewportLength(size);
32087
32301
  if (viewportLength !== null) {
@@ -32091,6 +32305,10 @@ const resolveSpacingSize = (size, element, property = "padding") => {
32091
32305
  if (containerLength !== null) {
32092
32306
  return containerLength;
32093
32307
  }
32308
+ const scaleLength = resolveScaleLength(size, element);
32309
+ if (scaleLength !== null) {
32310
+ return scaleLength;
32311
+ }
32094
32312
  return normalizeStyle(SIZE_MAP[size] || size, property, "js", element);
32095
32313
  };
32096
32314
  // Whether a spacing is nothing at all, answered without resolving it. A
@@ -34725,20 +34943,32 @@ const useUrlSearchParam = (paramName, defaultValue) => {
34725
34943
  * the one it would have picked, painted over.
34726
34944
  */
34727
34945
 
34728
- const headEntries = [];
34946
+ const headEntries$1 = [];
34729
34947
  const popupLayers = [];
34730
34948
  const ownMetas = [];
34731
34949
 
34732
34950
  /**
34733
- * @param {Array<{ content: string, media: string|null }>} themeColors
34734
- * @returns {() => void} Takes them away.
34951
+ * A Head's place in the stack, taken as it arrives and kept while what it asks
34952
+ * for changes (see head.jsx).
34953
+ *
34954
+ * @returns {{
34955
+ * set: (themeColors: Array<{ content: string, media: string|null }>) => void,
34956
+ * remove: () => void,
34957
+ * }}
34735
34958
  */
34736
- const pushHeadThemeColors = (themeColors) => {
34737
- headEntries.push(themeColors);
34738
- queueRender();
34739
- return () => {
34740
- headEntries.splice(headEntries.indexOf(themeColors), 1);
34741
- queueRender();
34959
+ const pushHeadThemeColors = () => {
34960
+ const themeColors = [];
34961
+ headEntries$1.push(themeColors);
34962
+ return {
34963
+ set: (themeColorsNext) => {
34964
+ themeColors.length = 0;
34965
+ themeColors.push(...themeColorsNext);
34966
+ queueRender();
34967
+ },
34968
+ remove: () => {
34969
+ headEntries$1.splice(headEntries$1.indexOf(themeColors), 1);
34970
+ queueRender();
34971
+ },
34742
34972
  };
34743
34973
  };
34744
34974
 
@@ -34836,8 +35066,8 @@ const readBackdropColor = (element) => {
34836
35066
  };
34837
35067
 
34838
35068
  let renderQueued = false;
34839
- // Once per commit: a Head re-rendering leaves and arrives again in the same
34840
- // commit, and the browser must not be handed the colour from in between.
35069
+ // Once per commit: a page leaving as another arrives, or several Heads
35070
+ // changing together, hand the browser one colour, never the one in between.
34841
35071
  const queueRender = () => {
34842
35072
  if (renderQueued) {
34843
35073
  return;
@@ -34856,8 +35086,8 @@ const render = () => {
34856
35086
  media: meta.getAttribute("media"),
34857
35087
  }));
34858
35088
  let colors = [];
34859
- for (let i = headEntries.length - 1; i >= 0; i--) {
34860
- colors.push(...headEntries[i]);
35089
+ for (let i = headEntries$1.length - 1; i >= 0; i--) {
35090
+ colors.push(...headEntries$1[i]);
34861
35091
  }
34862
35092
  colors.push(...htmlColors);
34863
35093
  for (const layer of popupLayers) {
@@ -34983,67 +35213,119 @@ const drawColor = (color) => {
34983
35213
  // title it found would put its own page's title over the new one. A
34984
35214
  // theme-color meta follows the same rule, kept by theme_color.js, where the
34985
35215
  // popups of the top layer paint over it.
34986
- const titleEntries = [];
35216
+ // Arriving is mounting: a Head that renders again — a layout's head whose
35217
+ // count moved, children built anew at every render — is updated where it
35218
+ // stands, under the Heads that arrived after it. Taken off and pushed back, a
35219
+ // layout's head would take the title from every page below it.
35220
+ const headEntries = [];
34987
35221
  let titleWithoutHead = null;
34988
35222
  const Head = ({
34989
35223
  children
34990
35224
  }) => {
35225
+ const entryRef = useRef(null);
34991
35226
  useLayoutEffect(() => {
34992
- if (!children) {
34993
- return undefined;
35227
+ const entry = {
35228
+ title: null,
35229
+ themeColors: pushHeadThemeColors()
35230
+ };
35231
+ headEntries.push(entry);
35232
+ entryRef.current = entry;
35233
+ return () => {
35234
+ entry.themeColors.remove();
35235
+ headEntries.splice(headEntries.indexOf(entry), 1);
35236
+ writeDocumentTitle();
35237
+ };
35238
+ }, []);
35239
+ const {
35240
+ title,
35241
+ themeColors,
35242
+ elements
35243
+ } = readHeadChildren(children);
35244
+ // What the children say, rather than the children: the elements are built
35245
+ // anew at every render, and saying the same thing again changes nothing.
35246
+ const contentKey = JSON.stringify([title, themeColors, elements]);
35247
+ useLayoutEffect(() => {
35248
+ const entry = entryRef.current;
35249
+ if (title !== null && !headEntries.some(other => other.title !== null)) {
35250
+ titleWithoutHead = document.title;
34994
35251
  }
34995
- const childArray = Array.isArray(children) ? children : [children];
35252
+ entry.title = title;
35253
+ writeDocumentTitle();
35254
+ entry.themeColors.set(themeColors);
34996
35255
  const appendedElements = [];
34997
- const themeColors = [];
34998
- let titleEntry = null;
34999
- for (const child of childArray) {
35000
- if (!child) {
35001
- continue;
35002
- }
35003
- if (child.type === "title") {
35004
- const titleChildren = child.props.children;
35005
- titleEntry = {
35006
- title: Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? ""
35007
- };
35008
- continue;
35009
- }
35010
- if (child.type === "meta" && child.props.name === "theme-color") {
35011
- themeColors.push({
35012
- content: child.props.content,
35013
- media: child.props.media ?? null
35014
- });
35015
- continue;
35016
- }
35017
- const el = document.createElement(child.type);
35018
- const props = child.props || {};
35256
+ for (const {
35257
+ type,
35258
+ props
35259
+ } of elements) {
35260
+ const el = document.createElement(type);
35019
35261
  for (const [key, value] of Object.entries(props)) {
35020
35262
  el.setAttribute(key, value);
35021
35263
  }
35022
35264
  document.head.appendChild(el);
35023
35265
  appendedElements.push(el);
35024
35266
  }
35025
- if (titleEntry) {
35026
- if (titleEntries.length === 0) {
35027
- titleWithoutHead = document.title;
35028
- }
35029
- titleEntries.push(titleEntry);
35030
- document.title = titleEntry.title;
35031
- }
35032
- const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
35033
35267
  return () => {
35034
- removeThemeColors?.();
35035
- if (titleEntry) {
35036
- titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
35037
- const lastEntry = titleEntries[titleEntries.length - 1];
35038
- document.title = lastEntry ? lastEntry.title : titleWithoutHead;
35039
- }
35040
35268
  for (const el of appendedElements) {
35041
35269
  el.remove();
35042
35270
  }
35043
35271
  };
35044
- }, [children]);
35272
+ }, [contentKey]);
35045
35273
  return null;
35046
35274
  };
35275
+ const writeDocumentTitle = () => {
35276
+ for (let i = headEntries.length - 1; i >= 0; i--) {
35277
+ const {
35278
+ title
35279
+ } = headEntries[i];
35280
+ if (title !== null) {
35281
+ document.title = title;
35282
+ return;
35283
+ }
35284
+ }
35285
+ if (titleWithoutHead !== null) {
35286
+ document.title = titleWithoutHead;
35287
+ titleWithoutHead = null;
35288
+ }
35289
+ };
35290
+ const readHeadChildren = children => {
35291
+ let title = null;
35292
+ const themeColors = [];
35293
+ const elements = [];
35294
+ if (!children) {
35295
+ return {
35296
+ title,
35297
+ themeColors,
35298
+ elements
35299
+ };
35300
+ }
35301
+ const childArray = Array.isArray(children) ? children : [children];
35302
+ for (const child of childArray) {
35303
+ if (!child) {
35304
+ continue;
35305
+ }
35306
+ if (child.type === "title") {
35307
+ const titleChildren = child.props.children;
35308
+ title = Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? "";
35309
+ continue;
35310
+ }
35311
+ if (child.type === "meta" && child.props.name === "theme-color") {
35312
+ themeColors.push({
35313
+ content: child.props.content,
35314
+ media: child.props.media ?? null
35315
+ });
35316
+ continue;
35317
+ }
35318
+ elements.push({
35319
+ type: child.type,
35320
+ props: child.props || {}
35321
+ });
35322
+ }
35323
+ return {
35324
+ title,
35325
+ themeColors,
35326
+ elements
35327
+ };
35328
+ };
35047
35329
 
35048
35330
  /**
35049
35331
  * A page is `{ route, params }`, never the route alone: a section of a page is
@@ -41789,7 +42071,7 @@ const resolveCommandValue = (source, event) => {
41789
42071
  return event.detail.value;
41790
42072
  }
41791
42073
  if (source.hasAttribute("command-value")) {
41792
- return source.getAttribute("command-value");
42074
+ return readCommandValue(source);
41793
42075
  }
41794
42076
  if (source.type === "radio" || source.type === "checkbox") {
41795
42077
  // Use readControlValue so that radio/checkbox sources return their `value`
@@ -41805,6 +42087,20 @@ const resolveCommandValue = (source, event) => {
41805
42087
  // selected, and the screen it opens would prefill a new entry with it.
41806
42088
  return getUIStateFromElement(source, { own: true });
41807
42089
  };
42090
+ // The value as it was given: a <Button command-value={12}> holds the number, an
42091
+ // object, whatever the prop said, where its attribute can only say a string. A
42092
+ // plain element carrying the attribute says that string.
42093
+ const readCommandValue = (source) => {
42094
+ let responded = false;
42095
+ let value;
42096
+ dispatchCustomEvent(source, "navi_get_command_value", {
42097
+ respondWith: (jsValue) => {
42098
+ responded = true;
42099
+ value = jsValue;
42100
+ },
42101
+ });
42102
+ return responded ? value : source.getAttribute("command-value");
42103
+ };
41808
42104
 
41809
42105
  const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
41810
42106
  const { command, event, source, implementation } = e.detail;
@@ -42572,7 +42868,11 @@ const executeNaviDefine = (source, event, target) => {
42572
42868
  // reads it (see List). "--navi-scroll:smooth" asks for the glide.
42573
42869
  const scrollCommand = (source, event, { argument, value }) => {
42574
42870
  const target =
42575
- resolveExplicitTarget(source) || resolveFirstParentControl(source);
42871
+ resolveExplicitTarget(source) ||
42872
+ // The list sending it about itself, as List's JSDoc writes a scroll asked
42873
+ // by code: triggerNaviCommand(listEl, "--navi-scroll", event, { value }).
42874
+ (source.matches(".navi_list_container") ? source : null) ||
42875
+ resolveFirstParentControl(source);
42576
42876
  if (!target) {
42577
42877
  return undefined;
42578
42878
  }
@@ -44106,6 +44406,9 @@ const useAutoFocus = (
44106
44406
  `[autofocus] "${ancestorType}" opened ${getElementSignature(ancestor)} -> ${focusDebugCall}`,
44107
44407
  );
44108
44408
  }
44409
+ if (focusVisible === true) {
44410
+ askFocusVisible(focusableElement);
44411
+ }
44109
44412
  focusableElement.focus({ preventScroll, focusVisible });
44110
44413
  if (autoSelect) {
44111
44414
  focusableElement.select();
@@ -49147,12 +49450,17 @@ const useControlProps = (props, {
49147
49450
  }
49148
49451
  };
49149
49452
  }
49453
+
49454
+ // A field with an action of its own has Enter answered already: the
49455
+ // input effect (input_effect.js) commits it — through the change Enter
49456
+ // fires, or on the keydown itself under actionDebounce — and an action
49457
+ // run on every keystroke has nothing left to commit. --navi-send would
49458
+ // run that same action a second time. Nothing answers it under
49459
+ // actionEvent="custom", where the field's caller does.
49460
+ const enterAnsweredByInputEffect = Boolean(actionDebounce) || props.action !== undefined && actionEvent !== "custom";
49150
49461
  const keyDownDefaultOnInput = e => {
49151
49462
  if (e.key === "Enter") {
49152
- if (actionDebounce) {
49153
- // The input has its own debounced action; Enter fires it directly
49154
- // (input_effect.js cancels the debounce and triggers the action via the change event).
49155
- // Don't propagate to --navi-send, which would cause a double action call.
49463
+ if (enterAnsweredByInputEffect) {
49156
49464
  return null;
49157
49465
  }
49158
49466
  return enterToSend(e);
@@ -53837,12 +54145,15 @@ const TextOverflow = ({
53837
54145
  }) => {
53838
54146
  return jsx(Text, {
53839
54147
  block: true,
53840
- as: "div",
53841
- pre: noWrap === undefined ? true : undefined
54148
+ as: "div"
54149
+ // One line does not wrap, at a typed newline either: white-space: pre
54150
+ // would keep every newline, and the text would be one line only while it
54151
+ // has none. A white-space the caller says wins.
54152
+ ,
54153
+ noWrap: noWrap ?? (rest.as === "p" || saysWhiteSpace(rest) ? undefined : true)
53842
54154
  // For paragraph we prefer to keep lines and only hide unbreakable long sections
53843
54155
  ,
53844
54156
  preLine: rest.as === "p" ? true : undefined,
53845
- noWrap: noWrap,
53846
54157
  ...rest,
53847
54158
  maxLines: undefined,
53848
54159
  "data-text-overflow": "",
@@ -59432,7 +59743,7 @@ const css$S = /* css */`@layer navi {
59432
59743
  transparent);
59433
59744
  }
59434
59745
 
59435
- .navi_button[data-content-display="contents"] {
59746
+ .navi_button {
59436
59747
  background: none;
59437
59748
  border: none;
59438
59749
  padding: 0;
@@ -59464,10 +59775,8 @@ a.navi_button {
59464
59775
  outline: none;
59465
59776
  position: relative;
59466
59777
 
59467
- &:where(:not([data-content-display="contents"])) {
59468
- background: none;
59469
- border: none;
59470
- padding: 0;
59778
+ & .navi_button_content > [data-navi-space] {
59779
+ display: contents;
59471
59780
  }
59472
59781
 
59473
59782
  & .navi_button_content {
@@ -59742,6 +60051,12 @@ const ButtonUI = props => {
59742
60051
  ,
59743
60052
  onnavi_get_value: e => {
59744
60053
  e.detail.respondWith(props.value);
60054
+ }
60055
+ // Same for what a command is about, when it is not the button's value
60056
+ // (see resolveCommandValue).
60057
+ ,
60058
+ onnavi_get_command_value: e => {
60059
+ e.detail.respondWith(props["command-value"]);
59745
60060
  },
59746
60061
  onContextMenu: e => {
59747
60062
  if (as === "a") {
@@ -59782,7 +60097,12 @@ const ButtonUI = props => {
59782
60097
  ...controlChildrenWrapperProps,
59783
60098
  children: jsx(ButtonContent, {
59784
60099
  spacing: spacing,
59785
- contentDisplay: contentDisplay,
60100
+ contentDisplay: contentDisplay
60101
+ // The frame lays the children out in the button's flow: told so, a
60102
+ // `spacing` is its gap rather than a space one line tall. Not without
60103
+ // a frame: the button's box lays them out then.
60104
+ ,
60105
+ flex: contentDisplay === "contents" ? undefined : props.flex ?? (icon ? "x" : undefined),
59786
60106
  children: children
59787
60107
  })
59788
60108
  })]
@@ -59795,12 +60115,14 @@ const ButtonUI = props => {
59795
60115
  const ButtonContent = ({
59796
60116
  spacing,
59797
60117
  contentDisplay = "inherit",
60118
+ flex,
59798
60119
  children
59799
60120
  }) => {
59800
60121
  const boxForwardedProps = useContext(BoxForwardedPropsContext);
59801
60122
  return jsxs(Text, {
59802
60123
  ...boxForwardedProps,
59803
60124
  display: contentDisplay,
60125
+ flex: flex,
59804
60126
  spacing: spacing,
59805
60127
  className: "navi_button_content",
59806
60128
  children: [children, jsx(ButtonShadow, {})]
@@ -70091,7 +70413,10 @@ const engageGroup = (groupEl, group, {
70091
70413
  const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
70092
70414
  // What the focus goes back to, when nothing holds it any more at the close.
70093
70415
  const focusOrigin = !focusWasInGroup && focusedBefore && focusedBefore !== document.body && focusedBefore !== groupEl ? focusedBefore : null;
70094
- const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
70416
+ // The ring navi drew, not the browser's flag: the flag outlives a press
70417
+ // whose mousedown was prevented, and given back as `focusVisible: true` it
70418
+ // would draw a ring nobody saw before (see isMatchingFocusVisible).
70419
+ const ringBefore = Boolean(focusedBefore?.matches && isMatchingFocusVisible(focusedBefore));
70095
70420
  const {
70096
70421
  scrollTop,
70097
70422
  scrollLeft
@@ -70267,7 +70592,7 @@ const engageGroup = (groupEl, group, {
70267
70592
  // or the user, put it since. Unless nothing moved it, that element is
70268
70593
  // made no focus target for the call: inert — or, holding the focus
70269
70594
  // inside it, without its tabindex, inert taking the focus with it.
70270
- const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
70595
+ const ringBeforeClose = Boolean(focusedBeforeClose?.matches && isMatchingFocusVisible(focusedBeforeClose));
70271
70596
  const focusMovedInGroup = focusWasInGroup && focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusedBefore && focusedBeforeClose.isConnected && focusedBefore.isConnected;
70272
70597
  const holdsFocusInside = focusMovedInGroup && focusedBefore.contains(focusedBeforeClose);
70273
70598
  const madeInert = focusMovedInGroup && !holdsFocusInside && !focusedBefore.inert;
@@ -72250,6 +72575,29 @@ const usePopupName = ({
72250
72575
  };
72251
72576
  const NO_TITLE = [];
72252
72577
 
72578
+ // The open controller's popupValueSignal (see open_controller.js), handed to
72579
+ // the content by whatever renders it: Dialog, Popover, a picker's callout.
72580
+ const PopupValueContext = createContext(null);
72581
+
72582
+ /**
72583
+ * What the popup this is rendered inside is open ON: the value its `signal`
72584
+ * held while it read open (the id a single sheet bound to `?open=<id>` shows),
72585
+ * else its `value`, else the value of whatever opened it
72586
+ * (`<Button value={id} command="--navi-open">`).
72587
+ *
72588
+ * Held from the opening to the end of the exit, whoever asks the close: the
72589
+ * signal and the address read closed as soon as the close starts, while an
72590
+ * animated exit — and a lift flying back to the card it came from — still
72591
+ * draws what the content renders. A refused close keeps it; a reopening on
72592
+ * another value switches it at the reopening.
72593
+ *
72594
+ * @returns {any} `undefined` outside a popup.
72595
+ */
72596
+ const usePopupValue = () => {
72597
+ const popupValueSignal = useContext(PopupValueContext);
72598
+ return popupValueSignal ? popupValueSignal.value : undefined;
72599
+ };
72600
+
72253
72601
  installImportMetaCssBuild(import.meta);
72254
72602
  // Same need popover.jsx has for its own local renderer (see its
72255
72603
  // openLocalPopoverCount): a layer="local" dialog is a plain positioned element
@@ -73194,7 +73542,7 @@ const css$C = /* css */`
73194
73542
  * whatever triggered the open (`e.detail.source`). A string is resolved via
73195
73543
  * `document.getElementById` when the dialog opens — see popover.jsx's own
73196
73544
  * `anchor` doc for why (mainly `defaultOpen`).
73197
- * @param {Element|{current: Element}|string} [props.liftAnchor] - Under
73545
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [props.liftAnchor] - Under
73198
73546
  * `animation="lifting"` or `animation={{ open, close: "lifting" }}`, where the closing
73199
73547
  * brings the box back to, when that is not where it came from: a popup one
73200
73548
  * walks through (a row of cards shown one at a time) has something else in
@@ -73202,8 +73550,10 @@ const css$C = /* css */`
73202
73550
  * card the press opened on. Same grammar as `anchor` (element, ref or id),
73203
73551
  * resolved once the close is made — after `onClose` — so whatever names the
73204
73552
  * card currently in front, or the box the close itself renders, is read
73205
- * then and not at the opening. Left out, the box comes back to the anchor
73206
- * it came out of.
73553
+ * then and not at the opening. A function is called then with what the
73554
+ * dialog was open on (see `usePopupValue`): a close the holder asks for
73555
+ * (Back taking the id out of the address) has emptied its signal by then.
73556
+ * Left out, the box comes back to the anchor it came out of.
73207
73557
  * @param {boolean} [props.sizeFromAnchor=false] - Whether the dialog takes the
73208
73558
  * anchor's width/height as a min-width/min-height floor
73209
73559
  * (`--anchor-width`/`--anchor-height`). Off by default: unlike a popover,
@@ -73736,9 +74086,12 @@ const useDialogProps = props => {
73736
74086
  openerAriaLabel,
73737
74087
  openerAriaLabelledBy
73738
74088
  });
73739
- const children = contentMounted ? jsx(PopupTitleContext.Provider, {
73740
- value: registerTitle,
73741
- children: childrenProp
74089
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
74090
+ value: openController.popupValueSignal,
74091
+ children: jsx(PopupTitleContext.Provider, {
74092
+ value: registerTitle,
74093
+ children: childrenProp
74094
+ })
73742
74095
  }) : null;
73743
74096
  // Where it is placed, and whether there is a wall — the two questions that
73744
74097
  // pick the show call (see this file's top comment).
@@ -73939,12 +74292,18 @@ const useDialogProps = props => {
73939
74292
  // Silent when it names nothing: the close may be what renders it, and
73940
74293
  // popup_lift.js asks again until it is there (and warns past that).
73941
74294
  const resolveLiftAnchorElement = () => {
73942
- if (typeof liftAnchor === "string") {
73943
- return document.getElementById(liftAnchor);
74295
+ // Asked with what the dialog was open on, which the signal holding it no
74296
+ // longer says once the close has started (see usePopupValue).
74297
+ const named = typeof liftAnchor === "function" ? liftAnchor(openController.popupValueSignal.peek()) : liftAnchor;
74298
+ if (!named) {
74299
+ return null;
74300
+ }
74301
+ if (typeof named === "string") {
74302
+ return document.getElementById(named);
73944
74303
  }
73945
74304
  // A ref is unwrapped even when it holds nothing, the same way `anchor` is:
73946
74305
  // the ref object itself has no box to come back to.
73947
- return "current" in liftAnchor ? liftAnchor.current : liftAnchor;
74306
+ return "current" in named ? named.current : named;
73948
74307
  };
73949
74308
 
73950
74309
  // The dialog and the anchor are the same box at two sizes, so the opening
@@ -74186,10 +74545,10 @@ const useDialogProps = props => {
74186
74545
  // resolveSpacingSize walks up from there to find that container.
74187
74546
  let marginWithContainerInPixels = resolveSpacingSize(marginWithContainer, dialogEl.parentElement);
74188
74547
  if (typeof marginWithContainerInPixels !== "number") {
74189
- // A value only CSS could evaluate (a spacing token resolving to a var(),
74190
- // a percentage…) — the placement below needs a real number, and letting
74191
- // it through would put the dialog at NaN.
74192
- console.warn(`Dialog: marginWithContainer="${marginWithContainer}" cannot be resolved to pixels. Use a number, a viewport length ("3appw", "3vvw", "2vvh") or a container length ("3cqw", "2cqh").`);
74548
+ // A value only CSS could evaluate (a percentage, a calc()…) — the
74549
+ // placement below needs a real number, and letting it through would
74550
+ // put the dialog at NaN.
74551
+ console.warn(`Dialog: marginWithContainer="${marginWithContainer}" cannot be resolved to pixels. Use a number, a spacing word ("s", "m"…), a viewport length ("3appw", "3vvw", "2vvh") or a container length ("3cqw", "2cqh").`);
74193
74552
  marginWithContainerInPixels = 0;
74194
74553
  }
74195
74554
  // The size caps read the same gap in CSS as the placement below applies
@@ -75226,9 +75585,11 @@ const css$B = /* css */`
75226
75585
  * big to bother anchoring to (`isAnchorTooBig`, always checked — see
75227
75586
  * `pickPositionRelativeTo`'s own doc in visible_rect.js).
75228
75587
  * @param {string|number} [props.marginWithContainer=0] - Extra spacing kept
75229
- * between the popover and the edges of its container.
75588
+ * between the popover and the edges of its container: a number of px, a
75589
+ * spacing word (`"s"`, read at the page's font size), a viewport length
75590
+ * (`"3appw"`) or a container length (`"3cqw"`).
75230
75591
  * @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
75231
- * between the popover and the edges of its anchor.
75592
+ * between the popover and the edges of its anchor, written the same way.
75232
75593
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
75233
75594
  * the popover and the page at all — asked before what an outside press does
75234
75595
  * and before how the backdrop is painted. `false` leaves the page
@@ -75692,9 +76053,12 @@ const usePopoverProps = props => {
75692
76053
  openerAriaLabel,
75693
76054
  openerAriaLabelledBy
75694
76055
  });
75695
- const children = contentMounted ? jsx(PopupTitleContext.Provider, {
75696
- value: registerTitle,
75697
- children: childrenProp
76056
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
76057
+ value: openController.popupValueSignal,
76058
+ children: jsx(PopupTitleContext.Provider, {
76059
+ value: registerTitle,
76060
+ children: childrenProp
76061
+ })
75698
76062
  }) : null;
75699
76063
  const isTopLayer = layer === "top";
75700
76064
  const ref = props.ref;
@@ -76009,8 +76373,8 @@ const usePopoverProps = props => {
76009
76373
  positionArea,
76010
76374
  positionAreaFixed,
76011
76375
  positionAreaWhenAnchorIsInvalid,
76012
- marginWithAnchor: resolveSpacingSize(marginWithAnchor),
76013
- marginWithContainer: resolveSpacingSize(marginWithContainer),
76376
+ marginWithAnchor: resolveMarginInPixels(marginWithAnchor, "marginWithAnchor"),
76377
+ marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
76014
76378
  // Only meaningful for the custom renderer: popoverEl is always
76015
76379
  // position: absolute relative to its own positioned ancestor,
76016
76380
  // real anchor or not — this tells pickPositionRelativeTo to
@@ -76067,7 +76431,7 @@ const usePopoverProps = props => {
76067
76431
  const pickOptions = {
76068
76432
  positionArea,
76069
76433
  container: positionedAncestor,
76070
- marginWithContainer: resolveSpacingSize(marginWithContainer),
76434
+ marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
76071
76435
  event: positionEvent
76072
76436
  };
76073
76437
  position = pickPositionRelativeTo(popoverEl, null, pickOptions);
@@ -76643,6 +77007,17 @@ const resolvePositionAreaAndAnimationKind = ({
76643
77007
  Popover.Close = PopupClose;
76644
77008
  Popover.Title = PopupTitle;
76645
77009
 
77010
+ // The placement needs pixels: a value only CSS could evaluate (a percentage, a
77011
+ // calc()…) would put the popover at NaN, which is the top of the page.
77012
+ const resolveMarginInPixels = (margin, name) => {
77013
+ const pixels = resolveSpacingSize(margin);
77014
+ if (typeof pixels === "number") {
77015
+ return pixels;
77016
+ }
77017
+ console.warn(`Popover: ${name}="${margin}" cannot be resolved to pixels. Use a number, a spacing word ("xs", "s", "m"…), a viewport length ("3appw", "3vvw") or a container length ("3cqw", "2cqh").`);
77018
+ return 0;
77019
+ };
77020
+
76646
77021
  installImportMetaCssBuild(import.meta);
76647
77022
  const css$A = /* css */`@layer navi {
76648
77023
  .navi_popup {
@@ -76702,7 +77077,7 @@ const css$A = /* css */`@layer navi {
76702
77077
  * `maxWidth` below.
76703
77078
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
76704
77079
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
76705
- * @param {Element|{current: Element}|string} [props.liftAnchor] -
77080
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [props.liftAnchor] -
76706
77081
  * **Dialog-only**, same guard.
76707
77082
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
76708
77083
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
@@ -76991,9 +77366,12 @@ const getPickerHostLabelId = (id, props) => {
76991
77366
  * right now: in the popup, what is being picked, before anything is sent; on
76992
77367
  * the façade, the answer the popup just closed on, before the server has said
76993
77368
  * anything, and put back if the action fails. `loading` is the run in flight;
76994
- * `interactive` is false while the picker is disabled, read-only or busy.
77369
+ * `interactive` is false while the picker is disabled, read-only or busy;
77370
+ * `readOnly` says the picker is read-only or held by the network policy — a
77371
+ * hold the controls a caller draws on the façade are not held by, so a face
77372
+ * that wants to look held reads it here.
76995
77373
  *
76996
- * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
77374
+ * @returns {{ value: any, loading: boolean, interactive: boolean, readOnly: boolean } | null}
76997
77375
  * null outside a picker.
76998
77376
  */
76999
77377
  const usePickerState = () => {
@@ -77004,12 +77382,14 @@ const usePickerState = () => {
77004
77382
  const {
77005
77383
  value,
77006
77384
  loading,
77007
- interactive
77385
+ interactive,
77386
+ readOnly
77008
77387
  } = context;
77009
77388
  return {
77010
77389
  value,
77011
77390
  loading,
77012
- interactive
77391
+ interactive,
77392
+ readOnly
77013
77393
  };
77014
77394
  };
77015
77395
 
@@ -77347,6 +77727,9 @@ const PickerCustom = props => {
77347
77727
  onConfirm,
77348
77728
  onClose
77349
77729
  };
77730
+ // The press the trigger's mousedown answered (see its reactions below): the
77731
+ // click it ends with is that press, not a second question.
77732
+ const pressAnsweredAtMouseDownRef = useRef(null);
77350
77733
  // aria-controls + id
77351
77734
  const popupId = `${props.id}_picker_popup`;
77352
77735
  {
@@ -77851,6 +78234,7 @@ const PickerCustom = props => {
77851
78234
  // Closing stays on the press even then: a gesture starting on an
77852
78235
  // open picker wants the popup out of the way, and there is no
77853
78236
  // choice being taken from anyone.
78237
+ pressAnsweredAtMouseDownRef.current = readPressCount();
77854
78238
  return {
77855
78239
  name: "mousedown to close picker",
77856
78240
  intent: "read",
@@ -77869,6 +78253,7 @@ const PickerCustom = props => {
77869
78253
  if (!opensOnPress || interactionsDispute || isPressDisputedByDrag(e.target) || isPressDisputedByHold(ref.current)) {
77870
78254
  return null;
77871
78255
  }
78256
+ pressAnsweredAtMouseDownRef.current = readPressCount();
77872
78257
  return {
77873
78258
  name: "mousedown to open picker",
77874
78259
  intent: "read",
@@ -77883,6 +78268,15 @@ const PickerCustom = props => {
77883
78268
  if (isWithinPickerContent(e.target)) {
77884
78269
  return null;
77885
78270
  }
78271
+ // One press, one answer: the press its mousedown answered is only
78272
+ // ending. Asked again, a refusal still on screen would read this
78273
+ // click as "got it" and take its explanation down before a frame
78274
+ // is drawn — under a finger the mousedown and the click arrive
78275
+ // together, after touchend (see reportInteractivity).
78276
+ if (e.detail !== 0 && pressAnsweredAtMouseDownRef.current === readPressCount()) {
78277
+ e.preventDefault();
78278
+ return null;
78279
+ }
77886
78280
  // Still open at the click of a later press: that press brought no
77887
78281
  // mousedown to close on (a drag source cancelled its pointerdown),
77888
78282
  // so it closes here, as it opens here in that case. A mousedown
@@ -77988,17 +78382,35 @@ const getTriggerLabelIds = pickerEl => {
77988
78382
  return ids.length === 0 ? undefined : ids.join(" ");
77989
78383
  };
77990
78384
  // What the picker holds without the person having changed it: its `value` (or
77991
- // signal) moving, a default moving under it — also when carried up from a
77992
- // control in its popup — and what its popup's controls are placed on as they
77993
- // mount.
78385
+ // signal) moving, a default moving under it, and what its popup's controls are
78386
+ // placed on as they mount. Read along the chain, never off the event alone: a
78387
+ // control in the popup carries its change up to the picker as an event of its
78388
+ // own (see the facade in ui_state_controller.js), chained to what caused it —
78389
+ // an app writing that control's bound signal included.
78390
+ const OUTSIDE_WRITE_TYPE_SET = new Set(["state_prop_change", "initial_state_push", "facade_child_mount_sync", "outside_value_follow"]);
77994
78391
  const isWrittenByTheOutside = e => {
77995
78392
  if (!e) {
77996
78393
  return false;
77997
78394
  }
77998
- if (e.type === "state_prop_change" || e.type === "initial_state_push" || e.type === "facade_child_mount_sync") {
78395
+ return Boolean(findEvent(e, event => OUTSIDE_WRITE_TYPE_SET.has(event.type)));
78396
+ };
78397
+ // The popup echoes the trigger's corners (see the CSS above), right for a
78398
+ // length and wrong for a shape: a round trigger's 50% read on a sheet of
78399
+ // another size is an ellipse, and a pill's 999px a stadium. A shape leaves the
78400
+ // popup on the corners every popup has.
78401
+ const PILL_RADIUS_MIN_PX = 100;
78402
+ const isBorderRadiusAShape = borderRadius => {
78403
+ if (typeof borderRadius === "number") {
78404
+ return borderRadius >= PILL_RADIUS_MIN_PX;
78405
+ }
78406
+ if (typeof borderRadius !== "string") {
78407
+ return false;
78408
+ }
78409
+ const radius = borderRadius.trim();
78410
+ if (radius.endsWith("%")) {
77999
78411
  return true;
78000
78412
  }
78001
- return Boolean(findEvent(e, "outside_value_follow"));
78413
+ return radius.endsWith("px") && parseFloat(radius) >= PILL_RADIUS_MIN_PX;
78002
78414
  };
78003
78415
  const getPickerInputUIState = pickerEl => {
78004
78416
  const pickerInput = getPickerInput(pickerEl);
@@ -78173,7 +78585,7 @@ const PickerContentInsidePopup = props => {
78173
78585
  ,
78174
78586
  "data-picker-popup": "",
78175
78587
  backgroundColor: popupBackgroundColor,
78176
- borderRadius: popupBorderRadius,
78588
+ borderRadius: popupBorderRadius === undefined && isBorderRadiusAShape(props.borderRadius) ? "var(--navi-popup-border-radius)" : popupBorderRadius,
78177
78589
  boxShadow: popupBoxShadow,
78178
78590
  borderWidth: isPopover ? undefined : dialogBorderWidth,
78179
78591
  minWidth: isPopover ? popupWidthFitContent ? undefined : "var(--anchor-width, 0px)" : dialogMinWidth,
@@ -78273,7 +78685,10 @@ const PickerCalloutPopup = ({
78273
78685
  mount,
78274
78686
  anchor
78275
78687
  });
78276
- const children = contentMounted ? childrenProp : null;
78688
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
78689
+ value: openController.popupValueSignal,
78690
+ children: childrenProp
78691
+ }) : null;
78277
78692
  // Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): it
78278
78693
  // decides what a press outside the callout does, and the callout spends and
78279
78694
  // closes on none of its own.
@@ -78432,6 +78847,13 @@ const findFileChooser = pickerEl => {
78432
78847
  return pickerEl.querySelector(".navi_picker_file_chooser");
78433
78848
  };
78434
78849
  const showNativePicker = (pickerInput, event) => {
78850
+ if (pickerInput.type === "file") {
78851
+ // WebKit presents nothing for a file input's showPicker() and throws
78852
+ // nothing, so the catch below never learns of it; a click, within the
78853
+ // press, opens the chooser in every engine.
78854
+ pickerInput.click();
78855
+ return;
78856
+ }
78435
78857
  if (findEvent(event, isTouchDrivenEvent)) {
78436
78858
  moveFocusTo(pickerInput, {
78437
78859
  focusVisible: false
@@ -79063,7 +79485,8 @@ const css$x = /* css */`@layer navi {
79063
79485
  * vertical one matches the line of text it sits on.
79064
79486
  * @param {string} [props.color] - The line's own colour — `--color`.
79065
79487
  * @param {string|number} [props.margin="0.5em"] - Room kept on both sides of
79066
- * the line — `--margin`.
79488
+ * the line — `--margin`. A spacing word (`"xs"`, `"s"`, `"m"`…), a length or
79489
+ * a number of px, like any margin prop.
79067
79490
  * @param {string|number} [props.marginStart] - Room on one side only —
79068
79491
  * `--margin-start`, above a horizontal rule and left of a vertical one.
79069
79492
  * @param {string|number} [props.marginEnd] - …and the other, `--margin-end`.
@@ -79094,9 +79517,9 @@ const Separator = ({
79094
79517
  ...cssVars({
79095
79518
  "--size": lengthValue(size),
79096
79519
  "--color": color,
79097
- "--margin": lengthValue(margin),
79098
- "--margin-start": lengthValue(marginStart),
79099
- "--margin-end": lengthValue(marginEnd)
79520
+ "--margin": spacingValue(margin),
79521
+ "--margin-start": spacingValue(marginStart),
79522
+ "--margin-end": spacingValue(marginEnd)
79100
79523
  }),
79101
79524
  ...style
79102
79525
  }
@@ -79117,6 +79540,10 @@ const cssVars = vars => {
79117
79540
  return declared;
79118
79541
  };
79119
79542
  const lengthValue = value => typeof value === "number" ? `${value}px` : value;
79543
+ // A margin reads the spacing scale like every other margin prop ("s" →
79544
+ // var(--navi-s)): written raw, a word is a valid custom property and an invalid
79545
+ // margin, which computes to 0 without falling back to anything.
79546
+ const spacingValue = value => value === undefined ? undefined : stringifySpacingStyle(value, "margin");
79120
79547
 
79121
79548
  const ListItemHeaderOrFooterResolver = props => {
79122
79549
  const Next = useNextResolver();
@@ -79617,7 +80044,9 @@ const createListItems = () => {
79617
80044
  dropItemLocator: (ownerId) => {
79618
80045
  locatorByOwner.delete(ownerId);
79619
80046
  },
79620
- // Where the item named by that id sits, asked of whoever holds it.
80047
+ // Where the item named by that id sits, asked of whoever holds it: a run
80048
+ // knows its items, drawn or not; an item declared one by one is its own
80049
+ // holder, and says its place when it draws.
79621
80050
  locateItem: (id) => {
79622
80051
  for (const locator of locatorByOwner.values()) {
79623
80052
  const index = locator.indexOf(id);
@@ -79625,6 +80054,16 @@ const createListItems = () => {
79625
80054
  return index;
79626
80055
  }
79627
80056
  }
80057
+ for (const [entryId, entry] of entryById) {
80058
+ if (
80059
+ entry.ownerId === entryId &&
80060
+ entry.item &&
80061
+ entry.place !== undefined &&
80062
+ entry.data.itemId === id
80063
+ ) {
80064
+ return entry.place;
80065
+ }
80066
+ }
79628
80067
  return null;
79629
80068
  },
79630
80069
  // The room the item at that index takes: the room it took when it was
@@ -81015,6 +81454,11 @@ const css$v = /* css */`@property --list-group-label-height {
81015
81454
  overflow: clip;
81016
81455
  }
81017
81456
 
81457
+ .navi_list_container[data-horizontal]:not([data-scroller], [data-overflow-visible]) > .navi_list_scroll_container > .navi_list:not([navi-box-flow="grid"]) {
81458
+ width: min-content;
81459
+ min-width: 100%;
81460
+ }
81461
+
81018
81462
  .navi_list_container[data-horizontal] {
81019
81463
  --list-max-height: none;
81020
81464
 
@@ -81162,6 +81606,7 @@ const css$v = /* css */`@property --list-group-label-height {
81162
81606
 
81163
81607
  & .navi_list_item_group_list {
81164
81608
  --list-group-label-height: inherit;
81609
+ gap: var(--x-list-gap, 0px);
81165
81610
  flex-direction: column;
81166
81611
  width: 100%;
81167
81612
  margin: 0;
@@ -88777,6 +89222,16 @@ const PickerChip = ({
88777
89222
  children,
88778
89223
  ...rest
88779
89224
  }) => {
89225
+ // Its picker's own state, not the zone's: on the façade the network policy's
89226
+ // hold is not handed down (see faceReadOnly in picker.jsx), and the cross is
89227
+ // what changes the picker.
89228
+ const pickerContext = usePickerContext();
89229
+ if (commandFor === undefined && pickerContext?.readOnly) {
89230
+ return jsx(Badge, {
89231
+ ...rest,
89232
+ children: children
89233
+ });
89234
+ }
88780
89235
  return jsxs(Badge, {
88781
89236
  ...rest,
88782
89237
  children: [children, jsx(Badge.Button, {
@@ -89381,7 +89836,7 @@ const css$r = /* css */`@property --navi-picker-container-font-size {
89381
89836
  flex-grow: 1;
89382
89837
  display: inline-block;
89383
89838
 
89384
- &[data-picker-facade] {
89839
+ &[data-picker-facade="drawing"] {
89385
89840
  line-height: var(--navi-line-height);
89386
89841
  }
89387
89842
 
@@ -89548,6 +90003,10 @@ const css$r = /* css */`@property --navi-picker-container-font-size {
89548
90003
  --x-picker-cursor: pointer;
89549
90004
  }
89550
90005
 
90006
+ &[data-readonly]:has( > .navi_picker_box > .navi_picker_input[data-readonly-reason="network-policy"]) > .navi_picker_box > .navi_picker_value[data-picker-facade="drawing"] {
90007
+ color: var(--picker-color);
90008
+ }
90009
+
89551
90010
  &[data-focus-within]:has( > .navi_picker_box > .navi_picker_input[data-focus-visible]), &[data-focus-visible] {
89552
90011
  --x-picker-border-color: transparent;
89553
90012
 
@@ -89773,6 +90232,12 @@ const PickerButton = props => {
89773
90232
  // with it: what the popup changes is what the picker would send.
89774
90233
  const readOnlyFromAbove = useContext(ReadOnlyContext);
89775
90234
  const [readOnlyResolved, readOnlyMessageResolved] = readOnlyHandedDown(uiStateController, readOnly || readOnlyFromAbove);
90235
+ // What a caller draws on the face (its `ui`, its right slot) is read-only
90236
+ // with the picker, and not held with it: the network policy holds what the
90237
+ // picker would send, which its popup changes. A card used as the trigger
90238
+ // keeps its links and buttons, each held for what it sends itself.
90239
+ const faceReadOnly = readOnly || readOnlyFromAbove;
90240
+ const faceReadOnlyMessage = faceReadOnly ? props.readOnlyMessage ?? uiStateController.readOnlyMessageFromAbove : undefined;
89776
90241
  // Read off the controller rather than worked out again: what makes a
89777
90242
  // read-only picker open is settled once, where the gate reads it (see
89778
90243
  // createControlInfo's readOnlyOpens). Needed here for the cursor.
@@ -89794,6 +90259,9 @@ const PickerButton = props => {
89794
90259
  maxLines,
89795
90260
  id: inputProps.id,
89796
90261
  interactive,
90262
+ // Read-only, or held by the network policy — never busy: an action in
90263
+ // flight is not a value nobody may change (see the ReadOnlyProvider below).
90264
+ readOnly: Boolean(readOnlyResolved),
89797
90265
  loading,
89798
90266
  clearConfirm,
89799
90267
  requestValue
@@ -90004,7 +90472,7 @@ const PickerButton = props => {
90004
90472
  // the picker draws itself, so each is written on its own line
90005
90473
  // (see .navi_picker_value in the CSS above).
90006
90474
  ,
90007
- "data-picker-facade": ui === undefined ? undefined : ""
90475
+ "data-picker-facade": ui === undefined ? undefined : typeof ui === "string" || typeof ui === "number" ? "text" : "drawing"
90008
90476
  // A placeholder is the picker saying "nothing here yet" about
90009
90477
  // the value navi draws — the default rendering, or the one a
90010
90478
  // typed picker installs for itself. A button's label is not
@@ -90016,6 +90484,8 @@ const PickerButton = props => {
90016
90484
  "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && !drawnAsButton && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
90017
90485
  maxLines: maxLines,
90018
90486
  children: jsx(PickerOwnContent, {
90487
+ readOnly: faceReadOnly,
90488
+ readOnlyMessage: faceReadOnlyMessage,
90019
90489
  children: jsx(MaxLinesContext.Provider, {
90020
90490
  value: maxLines,
90021
90491
  children: ui === undefined ? isIcon ?
@@ -90043,6 +90513,8 @@ const PickerButton = props => {
90043
90513
  }), hasRightSlot ? jsx("span", {
90044
90514
  className: "navi_picker_right_slot",
90045
90515
  children: jsx(PickerOwnContent, {
90516
+ readOnly: faceReadOnly,
90517
+ readOnlyMessage: faceReadOnlyMessage,
90046
90518
  children: clearable && pickerHasSomethingToClear(pickerContext) ? jsx(PickerClear, {
90047
90519
  size: rightSlotIconSize
90048
90520
  }) : rightSlot === undefined ?
@@ -90203,12 +90675,18 @@ const warnOnClearOutsidePicker = () => {
90203
90675
  // is empty, a chip the moment its value is taken out — takes the picker's own
90204
90676
  // entry with it, leaving the picker looking for a controller that is gone.
90205
90677
  const PickerOwnContent = ({
90678
+ readOnly,
90679
+ readOnlyMessage,
90206
90680
  children
90207
90681
  }) => jsx(ControlIdContext.Provider, {
90208
90682
  value: undefined,
90209
90683
  children: jsx(ControlNameContext.Provider, {
90210
90684
  value: undefined,
90211
- children: children
90685
+ children: jsx(ReadOnlyProvider, {
90686
+ readOnly: readOnly,
90687
+ readOnlyMessage: readOnlyMessage,
90688
+ children: children
90689
+ })
90212
90690
  })
90213
90691
  });
90214
90692
 
@@ -90580,7 +91058,7 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
90580
91058
  * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
90581
91059
  * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
90582
91060
  * lift?: "box" | "scene",
90583
- * liftAnchor?: Element | { current: Element } | string,
91061
+ * liftAnchor?: Element | { current: Element } | string | ((popupValue: any) => Element | { current: Element } | string),
90584
91062
  * animationDuration?: string,
90585
91063
  * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
90586
91064
  * calloutIcon?: boolean,
@@ -90655,7 +91133,9 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
90655
91133
  * follows the theme) — the trigger's own `backgroundColor` is not it, so a
90656
91134
  * variant that paints the trigger transparent leaves the popup a real sheet.
90657
91135
  * @param {number|string} [popupBorderRadius] The popup's corners. Left out they
90658
- * echo the trigger's `borderRadius`.
91136
+ * echo the trigger's `borderRadius` when it is a length of corners; a shape
91137
+ * (a percentage, a pill's 999px) leaves the popup on the corners every popup
91138
+ * has.
90659
91139
  * @param {string} [popupBoxShadow] The popup's shadow, popover or dialog. Left
90660
91140
  * out it is the one every navi popup casts. `"none"` for a popup that is
90661
91141
  * not a sheet — a card lifted out of a page draws its own edges, and the
@@ -91027,11 +91507,12 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
91027
91507
  * that opens precisely to get bigger. It brings an opaque, blurred backdrop
91028
91508
  * with it (`--navi-backdrop-lift-*`); `backdropVariant="discrete"` asks for
91029
91509
  * the light wash back.
91030
- * @param {Element|{current: Element}|string} [liftAnchor] Dialog mode,
91510
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [liftAnchor] Dialog mode,
91031
91511
  * Dialog's own: where a lifting close lands when that is not the trigger
91032
91512
  * (`animation="lifting"`, or `animation={{ open, close: "lifting" }}` for a
91033
91513
  * popup that opens the plain way and only lands on close),
91034
- * read once the close is made (after `onClose`).
91514
+ * read once the close is made (after `onClose`). A function is called with
91515
+ * what the popup was open on (see `usePopupValue`).
91035
91516
  * @param {"box"|"scene"} [lift="box"] Dialog's own, under `animation="lifting"`:
91036
91517
  * `"box"` for a card that extends (its top stays, the box uncovers the rest),
91037
91518
  * `"scene"` for a thumbnail that is a band cut from the middle of the bigger
@@ -101071,9 +101552,11 @@ const KeyboardShortcutAriaElement = ({
101071
101552
  * <Button action={copy}>{copied ? i18n("Copied") : i18n("Copy link")}</Button>
101072
101553
  * ```
101073
101554
  *
101074
- * `copy` rejects when the browser refuses the write, so an `action` running it
101075
- * says why on the control. A press while `copied` holds starts the moment
101076
- * again.
101555
+ * `copy` rejects when the browser refuses the write (permission blocked, page
101556
+ * not in front, no clipboard outside a secure context), with navi's
101557
+ * `"button.copy_refused"` sentence — the browser's own error as its `cause` —
101558
+ * so an `action` running it says why on the control in the page's language. A
101559
+ * press while `copied` holds starts the moment again.
101077
101560
  *
101078
101561
  * @param {string | (() => string | Promise<string>)} text What is copied. A
101079
101562
  * function is called at the press: for a text only built then (a report, a
@@ -101113,6 +101596,14 @@ const useCopyToClipboard = (text, { duration = 1500 } = {}) => {
101113
101596
 
101114
101597
  const writeToClipboard = async (text) => {
101115
101598
  const value = typeof text === "function" ? text() : text;
101599
+ try {
101600
+ await writeValueToClipboard(value);
101601
+ } catch (error) {
101602
+ // The browser's words are in its own language, and name its API.
101603
+ throw new Error(buttonI18n("button.copy_refused"), { cause: error });
101604
+ }
101605
+ };
101606
+ const writeValueToClipboard = async (value) => {
101116
101607
  if (typeof value === "string") {
101117
101608
  await window.navigator.clipboard.writeText(value);
101118
101609
  return;
@@ -104117,5 +104608,5 @@ const UserSvg = () => jsx("svg", {
104117
104608
  })
104118
104609
  });
104119
104610
 
104120
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, PopupGroup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, activeRouteReadsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isPressOnControl, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, todaySignal, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useCopyToClipboard, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useHoldScrollRestoration, useInputGroup, useKeyboardShortcuts, useListItemState, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
104611
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NetworkPolicyError, NumberSpin, Paragraph, Picker, Popover, Popup, PopupGroup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, activeRouteActionsSignal, activeRouteReadsSignal, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, dispatchRequestSetUIState, enableDebugActions, enableDebugOnDocumentLoading, enableVirtualKeyboardOverlay, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, getNowHours, getNowHoursRoundedToStep, isCellSelected, isColumnSelected, isNetworkPolicyError, isPressOnControl, isRowSelected, isScrolling, isToday, keepPageOnScreen, languagesSignal, localStorageSignal, markAsOutsideTextFlow, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, preloadUrl, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, routeFallback, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, todaySignal, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useCopyToClipboard, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useHoldScrollRestoration, useInputGroup, useKeyboardShortcuts, useListItemState, useNavState, useNetworkPolicyReason, useOrderedColumns, usePickerState, usePopupMode, usePopupValue, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
104121
104612
  //# sourceMappingURL=jsenv_navi.js.map