@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.
@@ -966,6 +966,10 @@ const buttonI18n = /*#__PURE__*/ defineNaviMessages({
966
966
  en: "Copied!",
967
967
  fr: "Copié !",
968
968
  },
969
+ "button.copy_refused": {
970
+ en: "Could not copy.",
971
+ fr: "Impossible de copier.",
972
+ },
969
973
  });
970
974
 
971
975
  // Spin messages — the ends of what one steps through when the control holding
@@ -2723,18 +2727,18 @@ const debounceSignal = (
2723
2727
  signalToDebounce,
2724
2728
  { delay = 300, deepCompare = true } = {},
2725
2729
  ) => {
2730
+ const isSameValue = (a, b) =>
2731
+ deepCompare ? compareTwoJsValues(a, b) : a === b;
2726
2732
  let timeoutId;
2727
2733
  let latestValue = signalToDebounce.peek();
2728
- const debouncedSignal = signal(latestValue);
2729
-
2730
- effect(() => {
2734
+ // Following the source subscribes to it, and through it to whatever it reads:
2735
+ // an app-level signal it reads would keep it — and its timer — alive for
2736
+ // good. So it is followed only while something reads the debounced value.
2737
+ let stopFollowing = null;
2738
+ const follow = () => {
2731
2739
  const value = signalToDebounce.value;
2732
2740
  const debouncedValue = debouncedSignal.peek();
2733
- if (
2734
- deepCompare
2735
- ? compareTwoJsValues(value, debouncedValue)
2736
- : value === debouncedValue
2737
- ) {
2741
+ if (isSameValue(value, debouncedValue)) {
2738
2742
  // The signal came back to where the debounced one already is: whatever it
2739
2743
  // passed through on the way is not on its way any more. Leaving the timer
2740
2744
  // armed would publish that in-between value once the delay elapses, and
@@ -2749,11 +2753,37 @@ const debounceSignal = (
2749
2753
  timeoutId = setTimeout(() => {
2750
2754
  debouncedSignal.value = latestValue;
2751
2755
  }, delay);
2756
+ };
2757
+ const catchUp = () => {
2758
+ const value = signalToDebounce.peek();
2759
+ if (!isSameValue(value, debouncedSignal.peek())) {
2760
+ debouncedSignal.value = value;
2761
+ }
2762
+ latestValue = debouncedSignal.peek();
2763
+ };
2764
+ const debouncedSignal = signal(latestValue, {
2765
+ watched: () => {
2766
+ // Nobody was waiting on what the source said while nothing read it:
2767
+ // that is taken at once, not after a delay.
2768
+ catchUp();
2769
+ stopFollowing = effect(follow);
2770
+ },
2771
+ unwatched: () => {
2772
+ stopFollowing();
2773
+ stopFollowing = null;
2774
+ clearTimeout(timeoutId);
2775
+ timeoutId = undefined;
2776
+ },
2752
2777
  });
2753
2778
 
2754
2779
  debouncedSignal.flush = () => {
2755
2780
  clearTimeout(timeoutId);
2756
- debouncedSignal.value = latestValue;
2781
+ timeoutId = undefined;
2782
+ if (stopFollowing) {
2783
+ debouncedSignal.value = latestValue;
2784
+ return;
2785
+ }
2786
+ catchUp();
2757
2787
  };
2758
2788
  // Whoever reads the debounced value gets the previous one during the delay,
2759
2789
  // with nothing saying a newer one is on its way. This says it.
@@ -4667,10 +4697,46 @@ const createActionProxyFromSignal = (
4667
4697
  });
4668
4698
  actionWeakMap.set(actionProxy, actionProxy);
4669
4699
 
4700
+ // Following the params subscribes to them, and through them to whatever
4701
+ // they read: a component's computed params reading an app-level signal stay
4702
+ // reachable from that signal for as long as anything listens to them. So a
4703
+ // binding follows its params only while something reads what it says (a
4704
+ // mounted component, a run's subscriber), and catches up when read again;
4705
+ // unread, every method retargets first (see proxyMethod). A binding asked to
4706
+ // act on their changes (runOnce, rerunOnChange, onChange: a route action,
4707
+ // actionRunEffect) follows them for as long as it exists.
4708
+ const followsParamsAlways = runOnce || rerunOnChange || Boolean(onChange);
4709
+ let readerCount = 0;
4710
+ let stopFollowingParams = null;
4711
+ const readerWatchOptions = followsParamsAlways
4712
+ ? undefined
4713
+ : {
4714
+ watched: () => {
4715
+ readerCount++;
4716
+ if (readerCount === 1) {
4717
+ const stopMirroringParams = effect(() => {
4718
+ proxyParamsSignal.value = paramsSignal.value;
4719
+ });
4720
+ const stopRetargeting = effect(retargetOnParamsChange);
4721
+ stopFollowingParams = () => {
4722
+ stopMirroringParams();
4723
+ stopRetargeting();
4724
+ };
4725
+ }
4726
+ },
4727
+ unwatched: () => {
4728
+ readerCount--;
4729
+ if (readerCount === 0) {
4730
+ stopFollowingParams();
4731
+ stopFollowingParams = null;
4732
+ }
4733
+ },
4734
+ };
4735
+
4670
4736
  // Create our own signal for params that we control completely
4671
4737
  const proxyParamsSignal = signal(paramsSignal.value);
4672
4738
  const proxySignal = (signalPropertyName, propertyName) => {
4673
- const signalProxy = signal();
4739
+ const signalProxy = signal(undefined, readerWatchOptions);
4674
4740
  let dispose;
4675
4741
  onActionTargetChange(() => {
4676
4742
  if (dispose) {
@@ -4797,20 +4863,34 @@ const createActionProxyFromSignal = (
4797
4863
  }
4798
4864
  // Watch for changes in the original paramsSignal and update ours
4799
4865
  // (original signal wins over any replaceParams calls)
4800
- weakEffect(
4801
- [paramsSignal, proxyParamsSignal],
4802
- (paramsSignalRef, proxyParamsSignalRef) => {
4803
- const newParams = paramsSignalRef.value;
4804
- proxyParamsSignalRef.value = newParams;
4805
- },
4806
- );
4807
- weakEffect([action], () => {
4866
+ const retargetOnParamsChange = () => {
4808
4867
  // eslint-disable-next-line no-unused-expressions
4809
4868
  proxyParamsSignal.value;
4810
4869
  _updateTarget({
4811
4870
  changeCause: "params_signal_change",
4812
4871
  });
4813
- });
4872
+ };
4873
+ if (followsParamsAlways) {
4874
+ weakEffect(
4875
+ [paramsSignal, proxyParamsSignal],
4876
+ (paramsSignalRef, proxyParamsSignalRef) => {
4877
+ const newParams = paramsSignalRef.value;
4878
+ proxyParamsSignalRef.value = newParams;
4879
+ },
4880
+ );
4881
+ weakEffect([action], retargetOnParamsChange);
4882
+ } else {
4883
+ // The first target is resolved now, as following would resolve it: the
4884
+ // subscribers wired above read what it says from the start (see
4885
+ // onActionTargetChange above). Untracked, as inside the effect that
4886
+ // resolves it otherwise: a binding is made during a render, which must
4887
+ // not subscribe to what retargeting reads.
4888
+ untracked(() => {
4889
+ _updateTarget({
4890
+ changeCause: "params_signal_change",
4891
+ });
4892
+ });
4893
+ }
4814
4894
  actionProxy.replaceParams = (newParams) => {
4815
4895
  if (currentAction === action) {
4816
4896
  const currentParams = proxyParamsSignal.value;
@@ -5775,6 +5855,10 @@ const isPickerBoundaryFor = (candidate, el) => {
5775
5855
  */
5776
5856
  const getParentControl = (el) => {
5777
5857
  const ownControlRoot = el.closest("[navi-control]");
5858
+ if (!ownControlRoot) {
5859
+ // In no control at all (a list's root, a plain element): none around it.
5860
+ return null;
5861
+ }
5778
5862
  const parentControlRoot = ownControlRoot.parentNode.closest("[navi-control]");
5779
5863
  return parentControlRoot;
5780
5864
  };
@@ -6250,10 +6334,14 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
6250
6334
  // signal wants a date field — but a boolean one maps to a checkbox, and
6251
6335
  // a picker made into a checkbox is not a picker with a different look:
6252
6336
  // it is another control, with no popup to open. A picker asked to hold
6253
- // a yes/no keeps its two rows and stays itself.
6337
+ // a yes/no keeps its two rows and stays itself. Nor does a picker with
6338
+ // a popup of its own become the field for that type: it is what its
6339
+ // popup is — a frise one slides in is no number one types.
6254
6340
  const wouldChangeWhatTheControlIs =
6255
6341
  controlType === "picker" &&
6256
- (typeFromSignal === "checkbox" || typeFromSignal === "radio");
6342
+ (props.children !== undefined ||
6343
+ typeFromSignal === "checkbox" ||
6344
+ typeFromSignal === "radio");
6257
6345
  if (typeFromSignal !== undefined && !wouldChangeWhatTheControlIs) {
6258
6346
  props.type = typeFromSignal;
6259
6347
  }
@@ -7331,7 +7419,8 @@ const generateSignalId = () => {
7331
7419
  * deploys holds one copy, not ten. Only the storage key changes, the id the url reads stays `id`.
7332
7420
  * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
7333
7421
  * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
7334
- * `"time"`…). Without it the value is not coerced.
7422
+ * `"time"`…). Without it the value is not coerced. On a type that is not text (`"string"`, `"email"`,
7423
+ * `"url"`), `""` — what a cleared control writes — is absence.
7335
7424
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
7336
7425
  * Used when reading the value back from a url search param, where everything is a string:
7337
7426
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -7426,6 +7515,8 @@ const generateSignalId = () => {
7426
7515
  * const childTab = stateSignal(parentTab);
7427
7516
  * // childTab follows parentTab changes unless explicitly set
7428
7517
  */
7518
+ // The types "" is a value of: an empty text is still a text.
7519
+ const TEXT_TYPE_SET = new Set(["string", "email", "url"]);
7429
7520
  const stateSignal = (defaultValue, options = {}) => {
7430
7521
  const {
7431
7522
  id,
@@ -7595,8 +7686,13 @@ const stateSignal = (defaultValue, options = {}) => {
7595
7686
  };
7596
7687
 
7597
7688
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
7689
+ // Whether "" says nothing at all: on a type that is not text, it is what a
7690
+ // cleared control writes (an empty field, a picker's cross) — no number, no
7691
+ // date — and kept, it would be an invalid value the address carries.
7692
+ const emptyStringIsAbsence =
7693
+ type !== undefined && !TEXT_TYPE_SET.has(type);
7598
7694
  const processValue = (value) => {
7599
- if (value === undefined) {
7695
+ if (value === undefined || (value === "" && emptyStringIsAbsence)) {
7600
7696
  // Absence is never validated, and the verdict on the previous value
7601
7697
  // (validSignal, validity.valid) must not outlive it.
7602
7698
  clearValidity();
@@ -14911,6 +15007,14 @@ PSEUDO_CLASSES[":active"] = {
14911
15007
  // focus_classes block below. At module scope so isMatchingFocusVisible (used
14912
15008
  // here and in control_hooks.jsx) can read it.
14913
15009
  let keyboardNavigationUsed = false;
15010
+ // The element a caller focused saying the ring must show (`focusVisible: true`
15011
+ // to moveFocusTo or autoFocus): it knows better than the modality — a screen
15012
+ // showing where Enter goes before any key was pressed. Until a pointer goes
15013
+ // down, which says the hand is elsewhere.
15014
+ let focusVisibleAskedElement = null;
15015
+ const askFocusVisible = (element) => {
15016
+ focusVisibleAskedElement = element;
15017
+ };
14914
15018
 
14915
15019
  // HOW FOCUS-VISIBLE IS DECIDED (the details behind isMatchingFocusVisible)
14916
15020
  //
@@ -14940,7 +15044,10 @@ let keyboardNavigationUsed = false;
14940
15044
  //
14941
15045
  // `keyboardNavigationUsed` has no such blind spot — it is set by navigation
14942
15046
  // keydowns and cleared by pointerdown, whatever anyone prevents afterwards
14943
- // — so it is the authority for everything rule 1 does not cover.
15047
+ // — so it is the authority for everything rule 1 does not cover. Save for
15048
+ // the element a caller explicitly focused with a ring (askFocusVisible):
15049
+ // that is somebody knowing better than the last interaction, not a state
15050
+ // left behind.
14944
15051
  //
14945
15052
  // Ring INHERITANCE (a controlled element ringing because its aria-controls
14946
15053
  // controller is focused) stays gated on the keyboard modality even for an
@@ -14963,7 +15070,7 @@ const isMatchingFocusVisible = (el) => {
14963
15070
  return false;
14964
15071
  }
14965
15072
  if (!keyboardNavigationUsed) {
14966
- return false;
15073
+ return el === focusVisibleAskedElement;
14967
15074
  }
14968
15075
  return true;
14969
15076
  };
@@ -15084,6 +15191,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
15084
15191
  "pointerdown",
15085
15192
  () => {
15086
15193
  keyboardNavigationUsed = false;
15194
+ focusVisibleAskedElement = null;
15087
15195
  },
15088
15196
  { capture: true },
15089
15197
  );
@@ -16232,7 +16340,8 @@ const retryWhenPlaceable = (
16232
16340
  * @param {object} [options]
16233
16341
  * @param {boolean} [options.focusVisible] Decides the ring instead of the
16234
16342
  * modality — for a caller that knows better than the last interaction does
16235
- * (a transfer speaking for the gesture that opened a container, say).
16343
+ * (a transfer speaking for the gesture that opened a container, say). `true`
16344
+ * draws the ring even before any key was pressed, until a pointer goes down.
16236
16345
  * @param {boolean} [options.preventScroll=true] Pass false to let the browser
16237
16346
  * scroll the element into view.
16238
16347
  */
@@ -16240,6 +16349,9 @@ const moveFocusTo = (
16240
16349
  element,
16241
16350
  { focusVisible, preventScroll = true } = {},
16242
16351
  ) => {
16352
+ if (focusVisible === true) {
16353
+ askFocusVisible(element);
16354
+ }
16243
16355
  element.focus({
16244
16356
  preventScroll,
16245
16357
  focusVisible:
@@ -16417,6 +16529,14 @@ document.addEventListener(
16417
16529
  const openedDuringThisPress = (openController) =>
16418
16530
  openController.pressCountAtOpen === pressCount;
16419
16531
 
16532
+ /**
16533
+ * Which press the events landing now belong to: the pointerdown, mousedown and
16534
+ * click of one press read the same count, a tap's compatibility events
16535
+ * included (see above). A click the keyboard makes brings no pointerdown, so it
16536
+ * reads the count of the last press: tell it apart by `event.detail === 0`.
16537
+ */
16538
+ const readPressCount = () => pressCount;
16539
+
16420
16540
  // How long a popup waits before handing the focus to a field, when giving it
16421
16541
  // is what raises the on-screen keyboard.
16422
16542
  //
@@ -16809,6 +16929,14 @@ const createOpenController = (
16809
16929
  // whole point: learning it afterwards means the content mounted on the
16810
16930
  // previous subject first.
16811
16931
  onOpen: null,
16932
+ // What the popup is open ON, as its content reads it (usePopupValue in
16933
+ // popup_value.js): written at the opening, right after `onOpen`, and left
16934
+ // alone by the close, so the content keeps drawing it while it leaves.
16935
+ popupValueSignal: signal(undefined),
16936
+ // How the opening reads that value, set by whoever holds the open state
16937
+ // (see useOpenPropsEffectOnOpenController); left unset, the value the
16938
+ // opening carries (a --navi-open source's).
16939
+ readPopupValue: null,
16812
16940
  // What an Escape means for this popup, whoever heard the key — its own
16813
16941
  // shortcut, its trigger's, the group it stands in, a bubble's: a cancel
16814
16942
  // (what it holds reverts), unless its owner says otherwise (a Picker's
@@ -16943,6 +17071,9 @@ const createOpenController = (
16943
17071
  // openEffect, which shows it: what the popup opens ON has to be
16944
17072
  // known before either (see `onOpen` above).
16945
17073
  controller.onOpen?.(requestOpenEvent);
17074
+ controller.popupValueSignal.value = controller.readPopupValue
17075
+ ? controller.readPopupValue(requestOpenEvent)
17076
+ : requestOpenEvent.detail.value;
16946
17077
  // After prepareFocusTransfer, which has to record what held the
16947
17078
  // focus before anything inside the popup can claim it, and before
16948
17079
  // openEffect, which measures the popup to place it.
@@ -17304,6 +17435,39 @@ const useOpenPropsEffectOnOpenController = (
17304
17435
  if (signal && open) {
17305
17436
  openSignalValueRef.current = signal.value;
17306
17437
  }
17438
+ // What the popup is open ON (see usePopupValue): the id the signal names
17439
+ // when it holds more than "open", else the popup's own `value`, else what the
17440
+ // opening carries. Read once `onOpen` has run, which may write the signal.
17441
+ openController.readPopupValue = (requestOpenEvent) => {
17442
+ if (signal) {
17443
+ const signalValue = signal.peek();
17444
+ if (signalValue !== true && readOpened(signalValue, value)) {
17445
+ return signalValue;
17446
+ }
17447
+ }
17448
+ if (value !== undefined) {
17449
+ return value;
17450
+ }
17451
+ return requestOpenEvent.detail.value;
17452
+ };
17453
+ // The signal walked to another id while open (a sheet's "next"): the
17454
+ // content follows it. Written closed, it is left alone — that is the close
17455
+ // starting, and the content still has its exit to draw.
17456
+ useLayoutEffect(() => {
17457
+ if (!signal) {
17458
+ return undefined;
17459
+ }
17460
+ return effect(() => {
17461
+ const signalValue = signal.value;
17462
+ if (
17463
+ openController.opened &&
17464
+ signalValue !== true &&
17465
+ readOpened(signalValue, value)
17466
+ ) {
17467
+ openController.popupValueSignal.value = signalValue;
17468
+ }
17469
+ });
17470
+ }, [signal, value]);
17307
17471
  // Assigned on every render, like openEffect, so it always closes over the
17308
17472
  // latest prop: a popup that opens or closes on its own (Escape, backdrop, a
17309
17473
  // --navi-close command) writes what happened where the caller keeps it, so
@@ -25013,6 +25177,14 @@ const arraySignalStore = (
25013
25177
  if (props === null || typeof props !== "object") {
25014
25178
  return props;
25015
25179
  }
25180
+ if (props[idKey] === undefined) {
25181
+ // Kept under `undefined`, every such item would be one and the same: a
25182
+ // list drawing its last row as many times as it has rows, a creation
25183
+ // rewriting all of them — and nothing saying why.
25184
+ throw new Error(
25185
+ `${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", … })`,
25186
+ );
25187
+ }
25016
25188
  const item = createItem(props);
25017
25189
  return item;
25018
25190
  };
@@ -26198,13 +26370,12 @@ const createResourceLifecycleManager = () => {
26198
26370
  if (!isSameResource && !isDependent) {
26199
26371
  continue;
26200
26372
  }
26373
+ // A write of a resource this one depends on reaches its lists as it
26374
+ // reruns its GET and GET_MANY: being heard is the whole condition (see
26375
+ // findEffectOnActions). rerunOn is about this resource's own writes.
26201
26376
  if (
26202
- !shouldReadRerunAfter(
26203
- "GET_RANGE",
26204
- config,
26205
- triggeringAction,
26206
- isSameResource,
26207
- )
26377
+ isSameResource &&
26378
+ !shouldReadRerunAfter("GET_RANGE", config, triggeringAction, true)
26208
26379
  ) {
26209
26380
  continue;
26210
26381
  }
@@ -29378,6 +29549,17 @@ const useActionAsyncData = (action, {
29378
29549
  useOnLoad(action, onLoad);
29379
29550
  usePoll(action, poll);
29380
29551
 
29552
+ // The instance the binding points at, remembered from one render to the
29553
+ // next: a binding that comes back to params whose instance failed — a search
29554
+ // typed again after a network blip — asks again, as typing them the first
29555
+ // time did. Staying on the instance that just failed asks nothing (see
29556
+ // runIsOwed), and neither does a mount, which never switched to anything.
29557
+ // Read as it stands: getCurrentAction() would flush a debounced binding,
29558
+ // publishing every keystroke the moment it renders.
29559
+ const instance = action.isProxy ? getActionPrivateProperties(action).currentAction : action;
29560
+ const instanceRef = useRef(instance);
29561
+ const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
29562
+ instanceRef.current = instance;
29381
29563
  // `run: true` — this component owns the request, so it is what starts it, and
29382
29564
  // it starts it from the render rather than from an effect: a component that
29383
29565
  // suspends has no effects, so a run written in one would be waiting for
@@ -29385,7 +29567,7 @@ const useActionAsyncData = (action, {
29385
29567
  // no-op — which is what makes starting it while rendering sound. From there
29386
29568
  // the wait is the ordinary one: suspended into <Loading>, or drawn by the
29387
29569
  // component under `loading: true`, exactly as for data someone else ran.
29388
- if (run && action.paramsSignal.peek() !== undefined && runIsOwed(action)) {
29570
+ if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
29389
29571
  // Nothing waits on this run: the failure is held by the action, and this
29390
29572
  // hook is what reads it back.
29391
29573
  runUnwatched(() => action.run({
@@ -31006,6 +31188,38 @@ const resolveContainerLength = (size, element) => {
31006
31188
  return (parseFloat(amount) / 100) * containerSize;
31007
31189
  };
31008
31190
 
31191
+ // "s", "-m" — the spacing scale, whose tokens are ems of the font where they
31192
+ // apply: a length only CSS knows, handed back as the var() it maps to, which
31193
+ // nothing can place with. Read off the given element (the root when none is),
31194
+ // for a caller asking for a number.
31195
+ const SCALE_WORD_REGEX = /^(-?)(xxs|xs|xl|xxl|[lms])$/;
31196
+ const TOKEN_LENGTH_REGEX = /^(-?[\d.]+)(px|r?em)$/;
31197
+ const resolveScaleLength = (size, element = document.documentElement) => {
31198
+ if (typeof size !== "string") {
31199
+ return null;
31200
+ }
31201
+ const match = SCALE_WORD_REGEX.exec(size);
31202
+ if (!match) {
31203
+ return null;
31204
+ }
31205
+ const [, sign, word] = match;
31206
+ const computedStyle = getComputedStyle(element);
31207
+ const token = computedStyle.getPropertyValue(`--navi-${word}`).trim();
31208
+ const tokenMatch = TOKEN_LENGTH_REGEX.exec(token);
31209
+ if (!tokenMatch) {
31210
+ return null;
31211
+ }
31212
+ const [, amount, unit] = tokenMatch;
31213
+ let unitInPx = 1;
31214
+ if (unit === "em") {
31215
+ unitInPx = parseFloat(computedStyle.fontSize);
31216
+ } else if (unit === "rem") {
31217
+ unitInPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
31218
+ }
31219
+ const length = parseFloat(amount) * unitInPx;
31220
+ return sign ? -length : length;
31221
+ };
31222
+
31009
31223
  const resolveSpacingSize = (size, element, property = "padding") => {
31010
31224
  const viewportLength = resolveViewportLength(size);
31011
31225
  if (viewportLength !== null) {
@@ -31015,6 +31229,10 @@ const resolveSpacingSize = (size, element, property = "padding") => {
31015
31229
  if (containerLength !== null) {
31016
31230
  return containerLength;
31017
31231
  }
31232
+ const scaleLength = resolveScaleLength(size, element);
31233
+ if (scaleLength !== null) {
31234
+ return scaleLength;
31235
+ }
31018
31236
  return normalizeStyle(SIZE_MAP[size] || size, property, "js", element);
31019
31237
  };
31020
31238
  // Whether a spacing is nothing at all, answered without resolving it. A
@@ -33536,20 +33754,32 @@ const useUrlSearchParam = (paramName, defaultValue) => {
33536
33754
  * the one it would have picked, painted over.
33537
33755
  */
33538
33756
 
33539
- const headEntries = [];
33757
+ const headEntries$1 = [];
33540
33758
  const popupLayers = [];
33541
33759
  const ownMetas = [];
33542
33760
 
33543
33761
  /**
33544
- * @param {Array<{ content: string, media: string|null }>} themeColors
33545
- * @returns {() => void} Takes them away.
33762
+ * A Head's place in the stack, taken as it arrives and kept while what it asks
33763
+ * for changes (see head.jsx).
33764
+ *
33765
+ * @returns {{
33766
+ * set: (themeColors: Array<{ content: string, media: string|null }>) => void,
33767
+ * remove: () => void,
33768
+ * }}
33546
33769
  */
33547
- const pushHeadThemeColors = (themeColors) => {
33548
- headEntries.push(themeColors);
33549
- queueRender();
33550
- return () => {
33551
- headEntries.splice(headEntries.indexOf(themeColors), 1);
33552
- queueRender();
33770
+ const pushHeadThemeColors = () => {
33771
+ const themeColors = [];
33772
+ headEntries$1.push(themeColors);
33773
+ return {
33774
+ set: (themeColorsNext) => {
33775
+ themeColors.length = 0;
33776
+ themeColors.push(...themeColorsNext);
33777
+ queueRender();
33778
+ },
33779
+ remove: () => {
33780
+ headEntries$1.splice(headEntries$1.indexOf(themeColors), 1);
33781
+ queueRender();
33782
+ },
33553
33783
  };
33554
33784
  };
33555
33785
 
@@ -33647,8 +33877,8 @@ const readBackdropColor = (element) => {
33647
33877
  };
33648
33878
 
33649
33879
  let renderQueued = false;
33650
- // Once per commit: a Head re-rendering leaves and arrives again in the same
33651
- // commit, and the browser must not be handed the colour from in between.
33880
+ // Once per commit: a page leaving as another arrives, or several Heads
33881
+ // changing together, hand the browser one colour, never the one in between.
33652
33882
  const queueRender = () => {
33653
33883
  if (renderQueued) {
33654
33884
  return;
@@ -33667,8 +33897,8 @@ const render = () => {
33667
33897
  media: meta.getAttribute("media"),
33668
33898
  }));
33669
33899
  let colors = [];
33670
- for (let i = headEntries.length - 1; i >= 0; i--) {
33671
- colors.push(...headEntries[i]);
33900
+ for (let i = headEntries$1.length - 1; i >= 0; i--) {
33901
+ colors.push(...headEntries$1[i]);
33672
33902
  }
33673
33903
  colors.push(...htmlColors);
33674
33904
  for (const layer of popupLayers) {
@@ -33794,67 +34024,119 @@ const drawColor = (color) => {
33794
34024
  // title it found would put its own page's title over the new one. A
33795
34025
  // theme-color meta follows the same rule, kept by theme_color.js, where the
33796
34026
  // popups of the top layer paint over it.
33797
- const titleEntries = [];
34027
+ // Arriving is mounting: a Head that renders again — a layout's head whose
34028
+ // count moved, children built anew at every render — is updated where it
34029
+ // stands, under the Heads that arrived after it. Taken off and pushed back, a
34030
+ // layout's head would take the title from every page below it.
34031
+ const headEntries = [];
33798
34032
  let titleWithoutHead = null;
33799
34033
  const Head = ({
33800
34034
  children
33801
34035
  }) => {
34036
+ const entryRef = useRef(null);
33802
34037
  useLayoutEffect(() => {
33803
- if (!children) {
33804
- return undefined;
34038
+ const entry = {
34039
+ title: null,
34040
+ themeColors: pushHeadThemeColors()
34041
+ };
34042
+ headEntries.push(entry);
34043
+ entryRef.current = entry;
34044
+ return () => {
34045
+ entry.themeColors.remove();
34046
+ headEntries.splice(headEntries.indexOf(entry), 1);
34047
+ writeDocumentTitle();
34048
+ };
34049
+ }, []);
34050
+ const {
34051
+ title,
34052
+ themeColors,
34053
+ elements
34054
+ } = readHeadChildren(children);
34055
+ // What the children say, rather than the children: the elements are built
34056
+ // anew at every render, and saying the same thing again changes nothing.
34057
+ const contentKey = JSON.stringify([title, themeColors, elements]);
34058
+ useLayoutEffect(() => {
34059
+ const entry = entryRef.current;
34060
+ if (title !== null && !headEntries.some(other => other.title !== null)) {
34061
+ titleWithoutHead = document.title;
33805
34062
  }
33806
- const childArray = Array.isArray(children) ? children : [children];
34063
+ entry.title = title;
34064
+ writeDocumentTitle();
34065
+ entry.themeColors.set(themeColors);
33807
34066
  const appendedElements = [];
33808
- const themeColors = [];
33809
- let titleEntry = null;
33810
- for (const child of childArray) {
33811
- if (!child) {
33812
- continue;
33813
- }
33814
- if (child.type === "title") {
33815
- const titleChildren = child.props.children;
33816
- titleEntry = {
33817
- title: Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? ""
33818
- };
33819
- continue;
33820
- }
33821
- if (child.type === "meta" && child.props.name === "theme-color") {
33822
- themeColors.push({
33823
- content: child.props.content,
33824
- media: child.props.media ?? null
33825
- });
33826
- continue;
33827
- }
33828
- const el = document.createElement(child.type);
33829
- const props = child.props || {};
34067
+ for (const {
34068
+ type,
34069
+ props
34070
+ } of elements) {
34071
+ const el = document.createElement(type);
33830
34072
  for (const [key, value] of Object.entries(props)) {
33831
34073
  el.setAttribute(key, value);
33832
34074
  }
33833
34075
  document.head.appendChild(el);
33834
34076
  appendedElements.push(el);
33835
34077
  }
33836
- if (titleEntry) {
33837
- if (titleEntries.length === 0) {
33838
- titleWithoutHead = document.title;
33839
- }
33840
- titleEntries.push(titleEntry);
33841
- document.title = titleEntry.title;
33842
- }
33843
- const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
33844
34078
  return () => {
33845
- removeThemeColors?.();
33846
- if (titleEntry) {
33847
- titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
33848
- const lastEntry = titleEntries[titleEntries.length - 1];
33849
- document.title = lastEntry ? lastEntry.title : titleWithoutHead;
33850
- }
33851
34079
  for (const el of appendedElements) {
33852
34080
  el.remove();
33853
34081
  }
33854
34082
  };
33855
- }, [children]);
34083
+ }, [contentKey]);
33856
34084
  return null;
33857
34085
  };
34086
+ const writeDocumentTitle = () => {
34087
+ for (let i = headEntries.length - 1; i >= 0; i--) {
34088
+ const {
34089
+ title
34090
+ } = headEntries[i];
34091
+ if (title !== null) {
34092
+ document.title = title;
34093
+ return;
34094
+ }
34095
+ }
34096
+ if (titleWithoutHead !== null) {
34097
+ document.title = titleWithoutHead;
34098
+ titleWithoutHead = null;
34099
+ }
34100
+ };
34101
+ const readHeadChildren = children => {
34102
+ let title = null;
34103
+ const themeColors = [];
34104
+ const elements = [];
34105
+ if (!children) {
34106
+ return {
34107
+ title,
34108
+ themeColors,
34109
+ elements
34110
+ };
34111
+ }
34112
+ const childArray = Array.isArray(children) ? children : [children];
34113
+ for (const child of childArray) {
34114
+ if (!child) {
34115
+ continue;
34116
+ }
34117
+ if (child.type === "title") {
34118
+ const titleChildren = child.props.children;
34119
+ title = Array.isArray(titleChildren) ? titleChildren.join("") : titleChildren ?? "";
34120
+ continue;
34121
+ }
34122
+ if (child.type === "meta" && child.props.name === "theme-color") {
34123
+ themeColors.push({
34124
+ content: child.props.content,
34125
+ media: child.props.media ?? null
34126
+ });
34127
+ continue;
34128
+ }
34129
+ elements.push({
34130
+ type: child.type,
34131
+ props: child.props || {}
34132
+ });
34133
+ }
34134
+ return {
34135
+ title,
34136
+ themeColors,
34137
+ elements
34138
+ };
34139
+ };
33858
34140
 
33859
34141
  /**
33860
34142
  * A page is `{ route, params }`, never the route alone: a section of a page is
@@ -40510,7 +40792,7 @@ const resolveCommandValue = (source, event) => {
40510
40792
  return event.detail.value;
40511
40793
  }
40512
40794
  if (source.hasAttribute("command-value")) {
40513
- return source.getAttribute("command-value");
40795
+ return readCommandValue(source);
40514
40796
  }
40515
40797
  if (source.type === "radio" || source.type === "checkbox") {
40516
40798
  // Use readControlValue so that radio/checkbox sources return their `value`
@@ -40526,6 +40808,20 @@ const resolveCommandValue = (source, event) => {
40526
40808
  // selected, and the screen it opens would prefill a new entry with it.
40527
40809
  return getUIStateFromElement(source, { own: true });
40528
40810
  };
40811
+ // The value as it was given: a <Button command-value={12}> holds the number, an
40812
+ // object, whatever the prop said, where its attribute can only say a string. A
40813
+ // plain element carrying the attribute says that string.
40814
+ const readCommandValue = (source) => {
40815
+ let responded = false;
40816
+ let value;
40817
+ dispatchCustomEvent(source, "navi_get_command_value", {
40818
+ respondWith: (jsValue) => {
40819
+ responded = true;
40820
+ value = jsValue;
40821
+ },
40822
+ });
40823
+ return responded ? value : source.getAttribute("command-value");
40824
+ };
40529
40825
 
40530
40826
  const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
40531
40827
  const { command, event, source, implementation } = e.detail;
@@ -41292,7 +41588,11 @@ const executeNaviDefine = (source, event, target) => {
41292
41588
  // reads it (see List). "--navi-scroll:smooth" asks for the glide.
41293
41589
  const scrollCommand = (source, event, { argument, value }) => {
41294
41590
  const target =
41295
- resolveExplicitTarget(source) || resolveFirstParentControl(source);
41591
+ resolveExplicitTarget(source) ||
41592
+ // The list sending it about itself, as List's JSDoc writes a scroll asked
41593
+ // by code: triggerNaviCommand(listEl, "--navi-scroll", event, { value }).
41594
+ (source.matches(".navi_list_container") ? source : null) ||
41595
+ resolveFirstParentControl(source);
41296
41596
  if (!target) {
41297
41597
  return undefined;
41298
41598
  }
@@ -42786,6 +43086,9 @@ const useAutoFocus = (
42786
43086
  `[autofocus] "${ancestorType}" opened ${getElementSignature(ancestor)} -> ${focusDebugCall}`,
42787
43087
  );
42788
43088
  }
43089
+ if (focusVisible === true) {
43090
+ askFocusVisible(focusableElement);
43091
+ }
42789
43092
  focusableElement.focus({ preventScroll, focusVisible });
42790
43093
  if (autoSelect) {
42791
43094
  focusableElement.select();
@@ -47711,12 +48014,17 @@ const useControlProps = (props, {
47711
48014
  }
47712
48015
  };
47713
48016
  }
48017
+
48018
+ // A field with an action of its own has Enter answered already: the
48019
+ // input effect (input_effect.js) commits it — through the change Enter
48020
+ // fires, or on the keydown itself under actionDebounce — and an action
48021
+ // run on every keystroke has nothing left to commit. --navi-send would
48022
+ // run that same action a second time. Nothing answers it under
48023
+ // actionEvent="custom", where the field's caller does.
48024
+ const enterAnsweredByInputEffect = Boolean(actionDebounce) || props.action !== undefined && actionEvent !== "custom";
47714
48025
  const keyDownDefaultOnInput = e => {
47715
48026
  if (e.key === "Enter") {
47716
- if (actionDebounce) {
47717
- // The input has its own debounced action; Enter fires it directly
47718
- // (input_effect.js cancels the debounce and triggers the action via the change event).
47719
- // Don't propagate to --navi-send, which would cause a double action call.
48027
+ if (enterAnsweredByInputEffect) {
47720
48028
  return null;
47721
48029
  }
47722
48030
  return enterToSend(e);
@@ -52376,12 +52684,15 @@ const TextOverflow = ({
52376
52684
  }) => {
52377
52685
  return jsx(Text, {
52378
52686
  block: true,
52379
- as: "div",
52380
- pre: noWrap === undefined ? true : undefined
52687
+ as: "div"
52688
+ // One line does not wrap, at a typed newline either: white-space: pre
52689
+ // would keep every newline, and the text would be one line only while it
52690
+ // has none. A white-space the caller says wins.
52691
+ ,
52692
+ noWrap: noWrap ?? (rest.as === "p" || saysWhiteSpace(rest) ? undefined : true)
52381
52693
  // For paragraph we prefer to keep lines and only hide unbreakable long sections
52382
52694
  ,
52383
52695
  preLine: rest.as === "p" ? true : undefined,
52384
- noWrap: noWrap,
52385
52696
  ...rest,
52386
52697
  maxLines: undefined,
52387
52698
  "data-text-overflow": "",
@@ -57955,7 +58266,7 @@ const css$S = /* css */`@layer navi {
57955
58266
  transparent);
57956
58267
  }
57957
58268
 
57958
- .navi_button[data-content-display="contents"] {
58269
+ .navi_button {
57959
58270
  background: none;
57960
58271
  border: none;
57961
58272
  padding: 0;
@@ -57987,10 +58298,8 @@ a.navi_button {
57987
58298
  outline: none;
57988
58299
  position: relative;
57989
58300
 
57990
- &:where(:not([data-content-display="contents"])) {
57991
- background: none;
57992
- border: none;
57993
- padding: 0;
58301
+ & .navi_button_content > [data-navi-space] {
58302
+ display: contents;
57994
58303
  }
57995
58304
 
57996
58305
  & .navi_button_content {
@@ -58265,6 +58574,12 @@ const ButtonUI = props => {
58265
58574
  ,
58266
58575
  onnavi_get_value: e => {
58267
58576
  e.detail.respondWith(props.value);
58577
+ }
58578
+ // Same for what a command is about, when it is not the button's value
58579
+ // (see resolveCommandValue).
58580
+ ,
58581
+ onnavi_get_command_value: e => {
58582
+ e.detail.respondWith(props["command-value"]);
58268
58583
  },
58269
58584
  onContextMenu: e => {
58270
58585
  if (as === "a") {
@@ -58305,7 +58620,12 @@ const ButtonUI = props => {
58305
58620
  ...controlChildrenWrapperProps,
58306
58621
  children: jsx(ButtonContent, {
58307
58622
  spacing: spacing,
58308
- contentDisplay: contentDisplay,
58623
+ contentDisplay: contentDisplay
58624
+ // The frame lays the children out in the button's flow: told so, a
58625
+ // `spacing` is its gap rather than a space one line tall. Not without
58626
+ // a frame: the button's box lays them out then.
58627
+ ,
58628
+ flex: contentDisplay === "contents" ? undefined : props.flex ?? (icon ? "x" : undefined),
58309
58629
  children: children
58310
58630
  })
58311
58631
  })]
@@ -58318,12 +58638,14 @@ const ButtonUI = props => {
58318
58638
  const ButtonContent = ({
58319
58639
  spacing,
58320
58640
  contentDisplay = "inherit",
58641
+ flex,
58321
58642
  children
58322
58643
  }) => {
58323
58644
  const boxForwardedProps = useContext(BoxForwardedPropsContext);
58324
58645
  return jsxs(Text, {
58325
58646
  ...boxForwardedProps,
58326
58647
  display: contentDisplay,
58648
+ flex: flex,
58327
58649
  spacing: spacing,
58328
58650
  className: "navi_button_content",
58329
58651
  children: [children, jsx(ButtonShadow, {})]
@@ -68496,7 +68818,10 @@ const engageGroup = (groupEl, group, {
68496
68818
  const focusWasInGroup = Boolean(focusedBefore) && focusedBefore !== groupEl && groupEl.contains(focusedBefore);
68497
68819
  // What the focus goes back to, when nothing holds it any more at the close.
68498
68820
  const focusOrigin = !focusWasInGroup && focusedBefore && focusedBefore !== document.body && focusedBefore !== groupEl ? focusedBefore : null;
68499
- const ringBefore = Boolean(focusedBefore?.matches?.(":focus-visible"));
68821
+ // The ring navi drew, not the browser's flag: the flag outlives a press
68822
+ // whose mousedown was prevented, and given back as `focusVisible: true` it
68823
+ // would draw a ring nobody saw before (see isMatchingFocusVisible).
68824
+ const ringBefore = Boolean(focusedBefore?.matches && isMatchingFocusVisible(focusedBefore));
68500
68825
  const {
68501
68826
  scrollTop,
68502
68827
  scrollLeft
@@ -68672,7 +68997,7 @@ const engageGroup = (groupEl, group, {
68672
68997
  // or the user, put it since. Unless nothing moved it, that element is
68673
68998
  // made no focus target for the call: inert — or, holding the focus
68674
68999
  // inside it, without its tabindex, inert taking the focus with it.
68675
- const ringBeforeClose = Boolean(focusedBeforeClose?.matches?.(":focus-visible"));
69000
+ const ringBeforeClose = Boolean(focusedBeforeClose?.matches && isMatchingFocusVisible(focusedBeforeClose));
68676
69001
  const focusMovedInGroup = focusWasInGroup && focusedBeforeClose && focusedBeforeClose !== document.body && focusedBeforeClose !== groupEl && focusedBeforeClose !== focusedBefore && focusedBeforeClose.isConnected && focusedBefore.isConnected;
68677
69002
  const holdsFocusInside = focusMovedInGroup && focusedBefore.contains(focusedBeforeClose);
68678
69003
  const madeInert = focusMovedInGroup && !holdsFocusInside && !focusedBefore.inert;
@@ -70655,6 +70980,29 @@ const usePopupName = ({
70655
70980
  };
70656
70981
  const NO_TITLE = [];
70657
70982
 
70983
+ // The open controller's popupValueSignal (see open_controller.js), handed to
70984
+ // the content by whatever renders it: Dialog, Popover, a picker's callout.
70985
+ const PopupValueContext = createContext(null);
70986
+
70987
+ /**
70988
+ * What the popup this is rendered inside is open ON: the value its `signal`
70989
+ * held while it read open (the id a single sheet bound to `?open=<id>` shows),
70990
+ * else its `value`, else the value of whatever opened it
70991
+ * (`<Button value={id} command="--navi-open">`).
70992
+ *
70993
+ * Held from the opening to the end of the exit, whoever asks the close: the
70994
+ * signal and the address read closed as soon as the close starts, while an
70995
+ * animated exit — and a lift flying back to the card it came from — still
70996
+ * draws what the content renders. A refused close keeps it; a reopening on
70997
+ * another value switches it at the reopening.
70998
+ *
70999
+ * @returns {any} `undefined` outside a popup.
71000
+ */
71001
+ const usePopupValue = () => {
71002
+ const popupValueSignal = useContext(PopupValueContext);
71003
+ return popupValueSignal ? popupValueSignal.value : undefined;
71004
+ };
71005
+
70658
71006
  installImportMetaCssBuild(import.meta);
70659
71007
  // Same need popover.jsx has for its own local renderer (see its
70660
71008
  // openLocalPopoverCount): a layer="local" dialog is a plain positioned element
@@ -71599,7 +71947,7 @@ const css$C = /* css */`
71599
71947
  * whatever triggered the open (`e.detail.source`). A string is resolved via
71600
71948
  * `document.getElementById` when the dialog opens — see popover.jsx's own
71601
71949
  * `anchor` doc for why (mainly `defaultOpen`).
71602
- * @param {Element|{current: Element}|string} [props.liftAnchor] - Under
71950
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [props.liftAnchor] - Under
71603
71951
  * `animation="lifting"` or `animation={{ open, close: "lifting" }}`, where the closing
71604
71952
  * brings the box back to, when that is not where it came from: a popup one
71605
71953
  * walks through (a row of cards shown one at a time) has something else in
@@ -71607,8 +71955,10 @@ const css$C = /* css */`
71607
71955
  * card the press opened on. Same grammar as `anchor` (element, ref or id),
71608
71956
  * resolved once the close is made — after `onClose` — so whatever names the
71609
71957
  * card currently in front, or the box the close itself renders, is read
71610
- * then and not at the opening. Left out, the box comes back to the anchor
71611
- * it came out of.
71958
+ * then and not at the opening. A function is called then with what the
71959
+ * dialog was open on (see `usePopupValue`): a close the holder asks for
71960
+ * (Back taking the id out of the address) has emptied its signal by then.
71961
+ * Left out, the box comes back to the anchor it came out of.
71612
71962
  * @param {boolean} [props.sizeFromAnchor=false] - Whether the dialog takes the
71613
71963
  * anchor's width/height as a min-width/min-height floor
71614
71964
  * (`--anchor-width`/`--anchor-height`). Off by default: unlike a popover,
@@ -72139,9 +72489,12 @@ const useDialogProps = props => {
72139
72489
  openerAriaLabel,
72140
72490
  openerAriaLabelledBy
72141
72491
  });
72142
- const children = contentMounted ? jsx(PopupTitleContext.Provider, {
72143
- value: registerTitle,
72144
- children: childrenProp
72492
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
72493
+ value: openController.popupValueSignal,
72494
+ children: jsx(PopupTitleContext.Provider, {
72495
+ value: registerTitle,
72496
+ children: childrenProp
72497
+ })
72145
72498
  }) : null;
72146
72499
  // Where it is placed, and whether there is a wall — the two questions that
72147
72500
  // pick the show call (see this file's top comment).
@@ -72336,12 +72689,18 @@ const useDialogProps = props => {
72336
72689
  // Silent when it names nothing: the close may be what renders it, and
72337
72690
  // popup_lift.js asks again until it is there (and warns past that).
72338
72691
  const resolveLiftAnchorElement = () => {
72339
- if (typeof liftAnchor === "string") {
72340
- return document.getElementById(liftAnchor);
72692
+ // Asked with what the dialog was open on, which the signal holding it no
72693
+ // longer says once the close has started (see usePopupValue).
72694
+ const named = typeof liftAnchor === "function" ? liftAnchor(openController.popupValueSignal.peek()) : liftAnchor;
72695
+ if (!named) {
72696
+ return null;
72697
+ }
72698
+ if (typeof named === "string") {
72699
+ return document.getElementById(named);
72341
72700
  }
72342
72701
  // A ref is unwrapped even when it holds nothing, the same way `anchor` is:
72343
72702
  // the ref object itself has no box to come back to.
72344
- return "current" in liftAnchor ? liftAnchor.current : liftAnchor;
72703
+ return "current" in named ? named.current : named;
72345
72704
  };
72346
72705
 
72347
72706
  // The dialog and the anchor are the same box at two sizes, so the opening
@@ -72564,10 +72923,10 @@ const useDialogProps = props => {
72564
72923
  // resolveSpacingSize walks up from there to find that container.
72565
72924
  let marginWithContainerInPixels = resolveSpacingSize(marginWithContainer, dialogEl.parentElement);
72566
72925
  if (typeof marginWithContainerInPixels !== "number") {
72567
- // A value only CSS could evaluate (a spacing token resolving to a var(),
72568
- // a percentage…) — the placement below needs a real number, and letting
72569
- // it through would put the dialog at NaN.
72570
- 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").`);
72926
+ // A value only CSS could evaluate (a percentage, a calc()…) — the
72927
+ // placement below needs a real number, and letting it through would
72928
+ // put the dialog at NaN.
72929
+ 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").`);
72571
72930
  marginWithContainerInPixels = 0;
72572
72931
  }
72573
72932
  // The size caps read the same gap in CSS as the placement below applies
@@ -73542,9 +73901,11 @@ const css$B = /* css */`
73542
73901
  * big to bother anchoring to (`isAnchorTooBig`, always checked — see
73543
73902
  * `pickPositionRelativeTo`'s own doc in visible_rect.js).
73544
73903
  * @param {string|number} [props.marginWithContainer=0] - Extra spacing kept
73545
- * between the popover and the edges of its container.
73904
+ * between the popover and the edges of its container: a number of px, a
73905
+ * spacing word (`"s"`, read at the page's font size), a viewport length
73906
+ * (`"3appw"`) or a container length (`"3cqw"`).
73546
73907
  * @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
73547
- * between the popover and the edges of its anchor.
73908
+ * between the popover and the edges of its anchor, written the same way.
73548
73909
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
73549
73910
  * the popover and the page at all — asked before what an outside press does
73550
73911
  * and before how the backdrop is painted. `false` leaves the page
@@ -74006,9 +74367,12 @@ const usePopoverProps = props => {
74006
74367
  openerAriaLabel,
74007
74368
  openerAriaLabelledBy
74008
74369
  });
74009
- const children = contentMounted ? jsx(PopupTitleContext.Provider, {
74010
- value: registerTitle,
74011
- children: childrenProp
74370
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
74371
+ value: openController.popupValueSignal,
74372
+ children: jsx(PopupTitleContext.Provider, {
74373
+ value: registerTitle,
74374
+ children: childrenProp
74375
+ })
74012
74376
  }) : null;
74013
74377
  const isTopLayer = layer === "top";
74014
74378
  const ref = props.ref;
@@ -74314,8 +74678,8 @@ const usePopoverProps = props => {
74314
74678
  positionArea,
74315
74679
  positionAreaFixed,
74316
74680
  positionAreaWhenAnchorIsInvalid,
74317
- marginWithAnchor: resolveSpacingSize(marginWithAnchor),
74318
- marginWithContainer: resolveSpacingSize(marginWithContainer),
74681
+ marginWithAnchor: resolveMarginInPixels(marginWithAnchor, "marginWithAnchor"),
74682
+ marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
74319
74683
  // Only meaningful for the custom renderer: popoverEl is always
74320
74684
  // position: absolute relative to its own positioned ancestor,
74321
74685
  // real anchor or not — this tells pickPositionRelativeTo to
@@ -74372,7 +74736,7 @@ const usePopoverProps = props => {
74372
74736
  const pickOptions = {
74373
74737
  positionArea,
74374
74738
  container: positionedAncestor,
74375
- marginWithContainer: resolveSpacingSize(marginWithContainer),
74739
+ marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
74376
74740
  event: positionEvent
74377
74741
  };
74378
74742
  position = pickPositionRelativeTo(popoverEl, null, pickOptions);
@@ -74947,6 +75311,17 @@ const resolvePositionAreaAndAnimationKind = ({
74947
75311
  Popover.Close = PopupClose;
74948
75312
  Popover.Title = PopupTitle;
74949
75313
 
75314
+ // The placement needs pixels: a value only CSS could evaluate (a percentage, a
75315
+ // calc()…) would put the popover at NaN, which is the top of the page.
75316
+ const resolveMarginInPixels = (margin, name) => {
75317
+ const pixels = resolveSpacingSize(margin);
75318
+ if (typeof pixels === "number") {
75319
+ return pixels;
75320
+ }
75321
+ 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").`);
75322
+ return 0;
75323
+ };
75324
+
74950
75325
  installImportMetaCssBuild(import.meta);
74951
75326
  const css$A = /* css */`@layer navi {
74952
75327
  .navi_popup {
@@ -75006,7 +75381,7 @@ const css$A = /* css */`@layer navi {
75006
75381
  * `maxWidth` below.
75007
75382
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
75008
75383
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
75009
- * @param {Element|{current: Element}|string} [props.liftAnchor] -
75384
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [props.liftAnchor] -
75010
75385
  * **Dialog-only**, same guard.
75011
75386
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
75012
75387
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
@@ -75295,9 +75670,12 @@ const getPickerHostLabelId = (id, props) => {
75295
75670
  * right now: in the popup, what is being picked, before anything is sent; on
75296
75671
  * the façade, the answer the popup just closed on, before the server has said
75297
75672
  * anything, and put back if the action fails. `loading` is the run in flight;
75298
- * `interactive` is false while the picker is disabled, read-only or busy.
75673
+ * `interactive` is false while the picker is disabled, read-only or busy;
75674
+ * `readOnly` says the picker is read-only or held by the network policy — a
75675
+ * hold the controls a caller draws on the façade are not held by, so a face
75676
+ * that wants to look held reads it here.
75299
75677
  *
75300
- * @returns {{ value: any, loading: boolean, interactive: boolean } | null}
75678
+ * @returns {{ value: any, loading: boolean, interactive: boolean, readOnly: boolean } | null}
75301
75679
  * null outside a picker.
75302
75680
  */
75303
75681
  const usePickerState = () => {
@@ -75308,12 +75686,14 @@ const usePickerState = () => {
75308
75686
  const {
75309
75687
  value,
75310
75688
  loading,
75311
- interactive
75689
+ interactive,
75690
+ readOnly
75312
75691
  } = context;
75313
75692
  return {
75314
75693
  value,
75315
75694
  loading,
75316
- interactive
75695
+ interactive,
75696
+ readOnly
75317
75697
  };
75318
75698
  };
75319
75699
 
@@ -75651,6 +76031,9 @@ const PickerCustom = props => {
75651
76031
  onConfirm,
75652
76032
  onClose
75653
76033
  };
76034
+ // The press the trigger's mousedown answered (see its reactions below): the
76035
+ // click it ends with is that press, not a second question.
76036
+ const pressAnsweredAtMouseDownRef = useRef(null);
75654
76037
  // aria-controls + id
75655
76038
  const popupId = `${props.id}_picker_popup`;
75656
76039
  {
@@ -76142,6 +76525,7 @@ const PickerCustom = props => {
76142
76525
  // Closing stays on the press even then: a gesture starting on an
76143
76526
  // open picker wants the popup out of the way, and there is no
76144
76527
  // choice being taken from anyone.
76528
+ pressAnsweredAtMouseDownRef.current = readPressCount();
76145
76529
  return {
76146
76530
  name: "mousedown to close picker",
76147
76531
  intent: "read",
@@ -76160,6 +76544,7 @@ const PickerCustom = props => {
76160
76544
  if (!opensOnPress || interactionsDispute || isPressDisputedByDrag(e.target) || isPressDisputedByHold(ref.current)) {
76161
76545
  return null;
76162
76546
  }
76547
+ pressAnsweredAtMouseDownRef.current = readPressCount();
76163
76548
  return {
76164
76549
  name: "mousedown to open picker",
76165
76550
  intent: "read",
@@ -76173,6 +76558,15 @@ const PickerCustom = props => {
76173
76558
  if (isWithinPickerContent(e.target)) {
76174
76559
  return null;
76175
76560
  }
76561
+ // One press, one answer: the press its mousedown answered is only
76562
+ // ending. Asked again, a refusal still on screen would read this
76563
+ // click as "got it" and take its explanation down before a frame
76564
+ // is drawn — under a finger the mousedown and the click arrive
76565
+ // together, after touchend (see reportInteractivity).
76566
+ if (e.detail !== 0 && pressAnsweredAtMouseDownRef.current === readPressCount()) {
76567
+ e.preventDefault();
76568
+ return null;
76569
+ }
76176
76570
  // Still open at the click of a later press: that press brought no
76177
76571
  // mousedown to close on (a drag source cancelled its pointerdown),
76178
76572
  // so it closes here, as it opens here in that case. A mousedown
@@ -76278,17 +76672,35 @@ const getTriggerLabelIds = pickerEl => {
76278
76672
  return ids.length === 0 ? undefined : ids.join(" ");
76279
76673
  };
76280
76674
  // What the picker holds without the person having changed it: its `value` (or
76281
- // signal) moving, a default moving under it — also when carried up from a
76282
- // control in its popup — and what its popup's controls are placed on as they
76283
- // mount.
76675
+ // signal) moving, a default moving under it, and what its popup's controls are
76676
+ // placed on as they mount. Read along the chain, never off the event alone: a
76677
+ // control in the popup carries its change up to the picker as an event of its
76678
+ // own (see the facade in ui_state_controller.js), chained to what caused it —
76679
+ // an app writing that control's bound signal included.
76680
+ const OUTSIDE_WRITE_TYPE_SET = new Set(["state_prop_change", "initial_state_push", "facade_child_mount_sync", "outside_value_follow"]);
76284
76681
  const isWrittenByTheOutside = e => {
76285
76682
  if (!e) {
76286
76683
  return false;
76287
76684
  }
76288
- if (e.type === "state_prop_change" || e.type === "initial_state_push" || e.type === "facade_child_mount_sync") {
76685
+ return Boolean(findEvent(e, event => OUTSIDE_WRITE_TYPE_SET.has(event.type)));
76686
+ };
76687
+ // The popup echoes the trigger's corners (see the CSS above), right for a
76688
+ // length and wrong for a shape: a round trigger's 50% read on a sheet of
76689
+ // another size is an ellipse, and a pill's 999px a stadium. A shape leaves the
76690
+ // popup on the corners every popup has.
76691
+ const PILL_RADIUS_MIN_PX = 100;
76692
+ const isBorderRadiusAShape = borderRadius => {
76693
+ if (typeof borderRadius === "number") {
76694
+ return borderRadius >= PILL_RADIUS_MIN_PX;
76695
+ }
76696
+ if (typeof borderRadius !== "string") {
76697
+ return false;
76698
+ }
76699
+ const radius = borderRadius.trim();
76700
+ if (radius.endsWith("%")) {
76289
76701
  return true;
76290
76702
  }
76291
- return Boolean(findEvent(e, "outside_value_follow"));
76703
+ return radius.endsWith("px") && parseFloat(radius) >= PILL_RADIUS_MIN_PX;
76292
76704
  };
76293
76705
  const getPickerInputUIState = pickerEl => {
76294
76706
  const pickerInput = getPickerInput(pickerEl);
@@ -76463,7 +76875,7 @@ const PickerContentInsidePopup = props => {
76463
76875
  ,
76464
76876
  "data-picker-popup": "",
76465
76877
  backgroundColor: popupBackgroundColor,
76466
- borderRadius: popupBorderRadius,
76878
+ borderRadius: popupBorderRadius === undefined && isBorderRadiusAShape(props.borderRadius) ? "var(--navi-popup-border-radius)" : popupBorderRadius,
76467
76879
  boxShadow: popupBoxShadow,
76468
76880
  borderWidth: isPopover ? undefined : dialogBorderWidth,
76469
76881
  minWidth: isPopover ? popupWidthFitContent ? undefined : "var(--anchor-width, 0px)" : dialogMinWidth,
@@ -76563,7 +76975,10 @@ const PickerCalloutPopup = ({
76563
76975
  mount,
76564
76976
  anchor
76565
76977
  });
76566
- const children = contentMounted ? childrenProp : null;
76978
+ const children = contentMounted ? jsx(PopupValueContext.Provider, {
76979
+ value: openController.popupValueSignal,
76980
+ children: childrenProp
76981
+ }) : null;
76567
76982
  // Inside a <PopupGroup> the wall is the group's (see popup_group.jsx): it
76568
76983
  // decides what a press outside the callout does, and the callout spends and
76569
76984
  // closes on none of its own.
@@ -76722,6 +77137,13 @@ const findFileChooser = pickerEl => {
76722
77137
  return pickerEl.querySelector(".navi_picker_file_chooser");
76723
77138
  };
76724
77139
  const showNativePicker = (pickerInput, event) => {
77140
+ if (pickerInput.type === "file") {
77141
+ // WebKit presents nothing for a file input's showPicker() and throws
77142
+ // nothing, so the catch below never learns of it; a click, within the
77143
+ // press, opens the chooser in every engine.
77144
+ pickerInput.click();
77145
+ return;
77146
+ }
76725
77147
  if (findEvent(event, isTouchDrivenEvent)) {
76726
77148
  moveFocusTo(pickerInput, {
76727
77149
  focusVisible: false
@@ -77353,7 +77775,8 @@ const css$x = /* css */`@layer navi {
77353
77775
  * vertical one matches the line of text it sits on.
77354
77776
  * @param {string} [props.color] - The line's own colour — `--color`.
77355
77777
  * @param {string|number} [props.margin="0.5em"] - Room kept on both sides of
77356
- * the line — `--margin`.
77778
+ * the line — `--margin`. A spacing word (`"xs"`, `"s"`, `"m"`…), a length or
77779
+ * a number of px, like any margin prop.
77357
77780
  * @param {string|number} [props.marginStart] - Room on one side only —
77358
77781
  * `--margin-start`, above a horizontal rule and left of a vertical one.
77359
77782
  * @param {string|number} [props.marginEnd] - …and the other, `--margin-end`.
@@ -77384,9 +77807,9 @@ const Separator = ({
77384
77807
  ...cssVars({
77385
77808
  "--size": lengthValue(size),
77386
77809
  "--color": color,
77387
- "--margin": lengthValue(margin),
77388
- "--margin-start": lengthValue(marginStart),
77389
- "--margin-end": lengthValue(marginEnd)
77810
+ "--margin": spacingValue(margin),
77811
+ "--margin-start": spacingValue(marginStart),
77812
+ "--margin-end": spacingValue(marginEnd)
77390
77813
  }),
77391
77814
  ...style
77392
77815
  }
@@ -77407,6 +77830,10 @@ const cssVars = vars => {
77407
77830
  return declared;
77408
77831
  };
77409
77832
  const lengthValue = value => typeof value === "number" ? `${value}px` : value;
77833
+ // A margin reads the spacing scale like every other margin prop ("s" →
77834
+ // var(--navi-s)): written raw, a word is a valid custom property and an invalid
77835
+ // margin, which computes to 0 without falling back to anything.
77836
+ const spacingValue = value => value === undefined ? undefined : stringifySpacingStyle(value, "margin");
77410
77837
 
77411
77838
  const ListItemHeaderOrFooterResolver = props => {
77412
77839
  const Next = useNextResolver();
@@ -77907,7 +78334,9 @@ const createListItems = () => {
77907
78334
  dropItemLocator: (ownerId) => {
77908
78335
  locatorByOwner.delete(ownerId);
77909
78336
  },
77910
- // Where the item named by that id sits, asked of whoever holds it.
78337
+ // Where the item named by that id sits, asked of whoever holds it: a run
78338
+ // knows its items, drawn or not; an item declared one by one is its own
78339
+ // holder, and says its place when it draws.
77911
78340
  locateItem: (id) => {
77912
78341
  for (const locator of locatorByOwner.values()) {
77913
78342
  const index = locator.indexOf(id);
@@ -77915,6 +78344,16 @@ const createListItems = () => {
77915
78344
  return index;
77916
78345
  }
77917
78346
  }
78347
+ for (const [entryId, entry] of entryById) {
78348
+ if (
78349
+ entry.ownerId === entryId &&
78350
+ entry.item &&
78351
+ entry.place !== undefined &&
78352
+ entry.data.itemId === id
78353
+ ) {
78354
+ return entry.place;
78355
+ }
78356
+ }
77918
78357
  return null;
77919
78358
  },
77920
78359
  // The room the item at that index takes: the room it took when it was
@@ -79305,6 +79744,11 @@ const css$v = /* css */`@property --list-group-label-height {
79305
79744
  overflow: clip;
79306
79745
  }
79307
79746
 
79747
+ .navi_list_container[data-horizontal]:not([data-scroller], [data-overflow-visible]) > .navi_list_scroll_container > .navi_list:not([navi-box-flow="grid"]) {
79748
+ width: min-content;
79749
+ min-width: 100%;
79750
+ }
79751
+
79308
79752
  .navi_list_container[data-horizontal] {
79309
79753
  --list-max-height: none;
79310
79754
 
@@ -79452,6 +79896,7 @@ const css$v = /* css */`@property --list-group-label-height {
79452
79896
 
79453
79897
  & .navi_list_item_group_list {
79454
79898
  --list-group-label-height: inherit;
79899
+ gap: var(--x-list-gap, 0px);
79455
79900
  flex-direction: column;
79456
79901
  width: 100%;
79457
79902
  margin: 0;
@@ -86876,6 +87321,16 @@ const PickerChip = ({
86876
87321
  children,
86877
87322
  ...rest
86878
87323
  }) => {
87324
+ // Its picker's own state, not the zone's: on the façade the network policy's
87325
+ // hold is not handed down (see faceReadOnly in picker.jsx), and the cross is
87326
+ // what changes the picker.
87327
+ const pickerContext = usePickerContext();
87328
+ if (commandFor === undefined && pickerContext?.readOnly) {
87329
+ return jsx(Badge, {
87330
+ ...rest,
87331
+ children: children
87332
+ });
87333
+ }
86879
87334
  return jsxs(Badge, {
86880
87335
  ...rest,
86881
87336
  children: [children, jsx(Badge.Button, {
@@ -87480,7 +87935,7 @@ const css$r = /* css */`@property --navi-picker-container-font-size {
87480
87935
  flex-grow: 1;
87481
87936
  display: inline-block;
87482
87937
 
87483
- &[data-picker-facade] {
87938
+ &[data-picker-facade="drawing"] {
87484
87939
  line-height: var(--navi-line-height);
87485
87940
  }
87486
87941
 
@@ -87647,6 +88102,10 @@ const css$r = /* css */`@property --navi-picker-container-font-size {
87647
88102
  --x-picker-cursor: pointer;
87648
88103
  }
87649
88104
 
88105
+ &[data-readonly]:has( > .navi_picker_box > .navi_picker_input[data-readonly-reason="network-policy"]) > .navi_picker_box > .navi_picker_value[data-picker-facade="drawing"] {
88106
+ color: var(--picker-color);
88107
+ }
88108
+
87650
88109
  &[data-focus-within]:has( > .navi_picker_box > .navi_picker_input[data-focus-visible]), &[data-focus-visible] {
87651
88110
  --x-picker-border-color: transparent;
87652
88111
 
@@ -87867,6 +88326,12 @@ const PickerButton = props => {
87867
88326
  // with it: what the popup changes is what the picker would send.
87868
88327
  const readOnlyFromAbove = useContext(ReadOnlyContext);
87869
88328
  const [readOnlyResolved, readOnlyMessageResolved] = readOnlyHandedDown(uiStateController, readOnly || readOnlyFromAbove);
88329
+ // What a caller draws on the face (its `ui`, its right slot) is read-only
88330
+ // with the picker, and not held with it: the network policy holds what the
88331
+ // picker would send, which its popup changes. A card used as the trigger
88332
+ // keeps its links and buttons, each held for what it sends itself.
88333
+ const faceReadOnly = readOnly || readOnlyFromAbove;
88334
+ const faceReadOnlyMessage = faceReadOnly ? props.readOnlyMessage ?? uiStateController.readOnlyMessageFromAbove : undefined;
87870
88335
  // Read off the controller rather than worked out again: what makes a
87871
88336
  // read-only picker open is settled once, where the gate reads it (see
87872
88337
  // createControlInfo's readOnlyOpens). Needed here for the cursor.
@@ -87888,6 +88353,9 @@ const PickerButton = props => {
87888
88353
  maxLines,
87889
88354
  id: inputProps.id,
87890
88355
  interactive,
88356
+ // Read-only, or held by the network policy — never busy: an action in
88357
+ // flight is not a value nobody may change (see the ReadOnlyProvider below).
88358
+ readOnly: Boolean(readOnlyResolved),
87891
88359
  loading,
87892
88360
  clearConfirm,
87893
88361
  requestValue
@@ -88098,7 +88566,7 @@ const PickerButton = props => {
88098
88566
  // the picker draws itself, so each is written on its own line
88099
88567
  // (see .navi_picker_value in the CSS above).
88100
88568
  ,
88101
- "data-picker-facade": ui === undefined ? undefined : ""
88569
+ "data-picker-facade": ui === undefined ? undefined : typeof ui === "string" || typeof ui === "number" ? "text" : "drawing"
88102
88570
  // A placeholder is the picker saying "nothing here yet" about
88103
88571
  // the value navi draws — the default rendering, or the one a
88104
88572
  // typed picker installs for itself. A button's label is not
@@ -88110,6 +88578,8 @@ const PickerButton = props => {
88110
88578
  "navi-placeholder": (ui === undefined || pickerUIIsNaviOwn(ui)) && !drawnAsButton && variant !== "text" && uiStateHoldsNothing(value) ? "" : undefined,
88111
88579
  maxLines: maxLines,
88112
88580
  children: jsx(PickerOwnContent, {
88581
+ readOnly: faceReadOnly,
88582
+ readOnlyMessage: faceReadOnlyMessage,
88113
88583
  children: jsx(MaxLinesContext.Provider, {
88114
88584
  value: maxLines,
88115
88585
  children: ui === undefined ? isIcon ?
@@ -88137,6 +88607,8 @@ const PickerButton = props => {
88137
88607
  }), hasRightSlot ? jsx("span", {
88138
88608
  className: "navi_picker_right_slot",
88139
88609
  children: jsx(PickerOwnContent, {
88610
+ readOnly: faceReadOnly,
88611
+ readOnlyMessage: faceReadOnlyMessage,
88140
88612
  children: clearable && pickerHasSomethingToClear(pickerContext) ? jsx(PickerClear, {
88141
88613
  size: rightSlotIconSize
88142
88614
  }) : rightSlot === undefined ?
@@ -88288,12 +88760,18 @@ const pickerHasSomethingToClear = ({
88288
88760
  // is empty, a chip the moment its value is taken out — takes the picker's own
88289
88761
  // entry with it, leaving the picker looking for a controller that is gone.
88290
88762
  const PickerOwnContent = ({
88763
+ readOnly,
88764
+ readOnlyMessage,
88291
88765
  children
88292
88766
  }) => jsx(ControlIdContext.Provider, {
88293
88767
  value: undefined,
88294
88768
  children: jsx(ControlNameContext.Provider, {
88295
88769
  value: undefined,
88296
- children: children
88770
+ children: jsx(ReadOnlyProvider, {
88771
+ readOnly: readOnly,
88772
+ readOnlyMessage: readOnlyMessage,
88773
+ children: children
88774
+ })
88297
88775
  })
88298
88776
  });
88299
88777
 
@@ -88558,7 +89036,7 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
88558
89036
  * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
88559
89037
  * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
88560
89038
  * lift?: "box" | "scene",
88561
- * liftAnchor?: Element | { current: Element } | string,
89039
+ * liftAnchor?: Element | { current: Element } | string | ((popupValue: any) => Element | { current: Element } | string),
88562
89040
  * animationDuration?: string,
88563
89041
  * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
88564
89042
  * calloutIcon?: boolean,
@@ -88633,7 +89111,9 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
88633
89111
  * follows the theme) — the trigger's own `backgroundColor` is not it, so a
88634
89112
  * variant that paints the trigger transparent leaves the popup a real sheet.
88635
89113
  * @param {number|string} [popupBorderRadius] The popup's corners. Left out they
88636
- * echo the trigger's `borderRadius`.
89114
+ * echo the trigger's `borderRadius` when it is a length of corners; a shape
89115
+ * (a percentage, a pill's 999px) leaves the popup on the corners every popup
89116
+ * has.
88637
89117
  * @param {string} [popupBoxShadow] The popup's shadow, popover or dialog. Left
88638
89118
  * out it is the one every navi popup casts. `"none"` for a popup that is
88639
89119
  * not a sheet — a card lifted out of a page draws its own edges, and the
@@ -89005,11 +89485,12 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
89005
89485
  * that opens precisely to get bigger. It brings an opaque, blurred backdrop
89006
89486
  * with it (`--navi-backdrop-lift-*`); `backdropVariant="discrete"` asks for
89007
89487
  * the light wash back.
89008
- * @param {Element|{current: Element}|string} [liftAnchor] Dialog mode,
89488
+ * @param {Element|{current: Element}|string|((popupValue: any) => Element|{current: Element}|string)} [liftAnchor] Dialog mode,
89009
89489
  * Dialog's own: where a lifting close lands when that is not the trigger
89010
89490
  * (`animation="lifting"`, or `animation={{ open, close: "lifting" }}` for a
89011
89491
  * popup that opens the plain way and only lands on close),
89012
- * read once the close is made (after `onClose`).
89492
+ * read once the close is made (after `onClose`). A function is called with
89493
+ * what the popup was open on (see `usePopupValue`).
89013
89494
  * @param {"box"|"scene"} [lift="box"] Dialog's own, under `animation="lifting"`:
89014
89495
  * `"box"` for a card that extends (its top stays, the box uncovers the rest),
89015
89496
  * `"scene"` for a thumbnail that is a band cut from the middle of the bigger
@@ -99038,9 +99519,11 @@ const KeyboardShortcutAriaElement = ({
99038
99519
  * <Button action={copy}>{copied ? i18n("Copied") : i18n("Copy link")}</Button>
99039
99520
  * ```
99040
99521
  *
99041
- * `copy` rejects when the browser refuses the write, so an `action` running it
99042
- * says why on the control. A press while `copied` holds starts the moment
99043
- * again.
99522
+ * `copy` rejects when the browser refuses the write (permission blocked, page
99523
+ * not in front, no clipboard outside a secure context), with navi's
99524
+ * `"button.copy_refused"` sentence — the browser's own error as its `cause` —
99525
+ * so an `action` running it says why on the control in the page's language. A
99526
+ * press while `copied` holds starts the moment again.
99044
99527
  *
99045
99528
  * @param {string | (() => string | Promise<string>)} text What is copied. A
99046
99529
  * function is called at the press: for a text only built then (a report, a
@@ -99080,6 +99563,14 @@ const useCopyToClipboard = (text, { duration = 1500 } = {}) => {
99080
99563
 
99081
99564
  const writeToClipboard = async (text) => {
99082
99565
  const value = typeof text === "function" ? text() : text;
99566
+ try {
99567
+ await writeValueToClipboard(value);
99568
+ } catch (error) {
99569
+ // The browser's words are in its own language, and name its API.
99570
+ throw new Error(buttonI18n("button.copy_refused"), { cause: error });
99571
+ }
99572
+ };
99573
+ const writeValueToClipboard = async (value) => {
99083
99574
  if (typeof value === "string") {
99084
99575
  await window.navigator.clipboard.writeText(value);
99085
99576
  return;
@@ -102084,5 +102575,5 @@ const UserSvg = () => jsx("svg", {
102084
102575
  })
102085
102576
  });
102086
102577
 
102087
- 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 };
102578
+ 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 };
102088
102579
  //# sourceMappingURL=jsenv_navi.js.map