@jsenv/navi 0.29.305 → 0.29.307

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.
@@ -12,7 +12,7 @@ import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, use
12
12
  import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
13
13
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
14
14
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
15
- import { durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, createValidity, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, CHAR_CLASS_PRESETS, parseDuration, durationToISOString } from "@jsenv/validity";
15
+ import { TYPE_RULE, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, createValidity, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, CHAR_CLASS_PRESETS, parseDuration, durationToISOString } from "@jsenv/validity";
16
16
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
17
17
  import { Suspense, createPortal, forwardRef } from "preact/compat";
18
18
 
@@ -4947,6 +4947,11 @@ const radioControllersByName = new Map();
4947
4947
  // The controller id is used as key — if the controller has no id, the value is not registered.
4948
4948
  const naviJsRegistry = new Map();
4949
4949
 
4950
+ // Controllers already reported for sharing their id with another one. An id is
4951
+ // stated on every render of its control, so without this the warning below
4952
+ // would repeat for as long as both controls are on screen.
4953
+ const controllersWarned = new WeakSet();
4954
+
4950
4955
  const getUIStateControllerById = (id) => controllersById.get(id);
4951
4956
  const getRadioSiblings = (radioUIStateController) => {
4952
4957
  const siblings = radioControllersByName.get(radioUIStateController.name);
@@ -4984,10 +4989,15 @@ const onUIStateControllerCreated = (uiStateController) => {
4984
4989
  if (id) {
4985
4990
  {
4986
4991
  const existing = controllersById.get(id);
4987
- if (existing && existing !== uiStateController) {
4992
+ if (
4993
+ existing &&
4994
+ existing !== uiStateController &&
4995
+ !controllersWarned.has(uiStateController)
4996
+ ) {
4997
+ controllersWarned.add(uiStateController);
4988
4998
  console.warn(
4989
4999
  `[navi] Two controls share the same id "${id}" ("${existing.controlType}" and "${controlType}"). ` +
4990
- `Lookups by id resolve to an arbitrary one of them, and when either unmounts it deletes the registry entry, leaving the surviving control unreachable (undefined uiState, dead picker buttons). ` +
5000
+ `The id resolves to whichever of them rendered last, so the other one is unreachable through it (undefined uiState, dead picker buttons). ` +
4991
5001
  `Common causes: several controls inside a single <Field> (the Field provides one id via context and every control consumes it — put each control in its own <Field>, or give one an explicit id), ` +
4992
5002
  `or the same content mounted twice (e.g. two popups sharing an id prefix while both hold their content).`,
4993
5003
  );
@@ -5020,8 +5030,15 @@ const onUIStateControllerCreated = (uiStateController) => {
5020
5030
  const onUIStateControllerDestroyed = (uiStateController) => {
5021
5031
  const { id, name, controlType } = uiStateController;
5022
5032
  if (id) {
5023
- controllersById.delete(id);
5024
- naviJsRegistry.delete(id);
5033
+ // Only the controller the id currently points at may take the entry away:
5034
+ // when two controls share an id, the one leaving would otherwise unregister
5035
+ // the one staying (see the warning in onUIStateControllerCreated), and the
5036
+ // same holds while a control is being replaced by its successor, which
5037
+ // registers during its render, before this cleanup runs.
5038
+ if (controllersById.get(id) === uiStateController) {
5039
+ controllersById.delete(id);
5040
+ naviJsRegistry.delete(id);
5041
+ }
5025
5042
  }
5026
5043
  const proxyFor = uiStateController.props["navi-control-proxy-for"];
5027
5044
  if (proxyFor) {
@@ -8427,6 +8444,75 @@ const languagesSignal = computed(() => {
8427
8444
  // (nor the "languagechange" listener above).
8428
8445
  setRuntimeLangSource(() => languagesSignal.value);
8429
8446
 
8447
+ /**
8448
+ * Where navi meets @jsenv/validity.
8449
+ *
8450
+ * validity names a refusal with a key and its parameters rather than a
8451
+ * sentence, so that a field and a server can refuse in the same words in the
8452
+ * language of the person reading. navi is one of those two callers: it says the
8453
+ * sentence, in the browser, through `naviI18n`. The keys line up on purpose —
8454
+ * validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
8455
+ * — so overriding a message is looking up one key, not going through a
8456
+ * translation table.
8457
+ *
8458
+ * The other direction is `constraintFromValidityRule`: a rule an app wrote for
8459
+ * its server, worn by a control as a constraint.
8460
+ */
8461
+
8462
+
8463
+ /**
8464
+ * Turns a @jsenv/validity rule into a constraint a control can wear, so an app
8465
+ * rule written once — in the package its server reads too — is checked on both
8466
+ * sides instead of being written twice.
8467
+ *
8468
+ * @param {object} rule
8469
+ * `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
8470
+ * to `createValidity({ rules })`.
8471
+ * @param {object} [ruleConfig]
8472
+ * What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
8473
+ * rule named `maxWords`. Pass `formatMessage` here to say the refusal through
8474
+ * the app's own i18n; without it the key is looked up in `naviI18n` under
8475
+ * `constraint.<key>`, and a rule answering with a finished sentence is shown
8476
+ * as-is.
8477
+ *
8478
+ * Call it once, at module level: a constraint rebuilt on every render is a new
8479
+ * object on every check.
8480
+ */
8481
+ const constraintFromValidityRule = (rule, ruleConfig = {}) => {
8482
+ const { formatMessage, ...ruleParams } = ruleConfig;
8483
+ return {
8484
+ name: rule.name,
8485
+ check: (field) => {
8486
+ const result = rule.applyOn(
8487
+ ruleParams[rule.name],
8488
+ field.uiState,
8489
+ ruleParams,
8490
+ );
8491
+ if (!result) {
8492
+ return null;
8493
+ }
8494
+ if (typeof result === "string") {
8495
+ return result;
8496
+ }
8497
+ if (formatMessage) {
8498
+ return formatMessage(result.key, result.params);
8499
+ }
8500
+ return naviI18nFromValidityMessage(result);
8501
+ },
8502
+ };
8503
+ };
8504
+
8505
+ const naviI18nFromValidityMessage = ({ key, params }) => {
8506
+ if (params && typeof params.max === "number") {
8507
+ // Lets a template pluralize on the bound it names: "[max] retour[s]".
8508
+ return naviI18n(`constraint.${key}`, {
8509
+ ...params,
8510
+ s: params.max > 1 ? "s" : "",
8511
+ });
8512
+ }
8513
+ return naviI18n(`constraint.${key}`, params);
8514
+ };
8515
+
8430
8516
  /**
8431
8517
  * https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
8432
8518
  */
@@ -8576,9 +8662,8 @@ const PATTERN_CONSTRAINT = {
8576
8662
  };
8577
8663
  CONSTRAINT_ATTRIBUTE_SET.add("pattern");
8578
8664
 
8579
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/email#validation
8580
- const emailregex =
8581
- /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
8665
+ // What an address is, is @jsenv/validity's `type: "email"`: a server checking
8666
+ // the value again refuses it for the same reason and in the same words.
8582
8667
  const TYPE_EMAIL_CONSTRAINT = {
8583
8668
  name: "type_email",
8584
8669
  messageAttribute: "data-type-message",
@@ -8592,14 +8677,11 @@ const TYPE_EMAIL_CONSTRAINT = {
8592
8677
  if (!valueAsString) {
8593
8678
  return null;
8594
8679
  }
8595
- if (emailregex.test(valueAsString)) {
8680
+ const result = TYPE_RULE.applyOn("email", valueAsString);
8681
+ if (!result) {
8596
8682
  return null;
8597
8683
  }
8598
-
8599
- if (!valueAsString.includes("@")) {
8600
- return naviI18n("constraint.type.email.at", { value: valueAsString });
8601
- }
8602
- return naviI18n("constraint.type.email.invalid");
8684
+ return naviI18nFromValidityMessage(result);
8603
8685
  },
8604
8686
  };
8605
8687
 
@@ -11900,75 +11982,6 @@ const isFunctionButNotAnActionFunction = (action) => {
11900
11982
  return typeof action === "function" && !action.isAction;
11901
11983
  };
11902
11984
 
11903
- /**
11904
- * Where navi meets @jsenv/validity.
11905
- *
11906
- * validity names a refusal with a key and its parameters rather than a
11907
- * sentence, so that a field and a server can refuse in the same words in the
11908
- * language of the person reading. navi is one of those two callers: it says the
11909
- * sentence, in the browser, through `naviI18n`. The keys line up on purpose —
11910
- * validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
11911
- * — so overriding a message is looking up one key, not going through a
11912
- * translation table.
11913
- *
11914
- * The other direction is `constraintFromValidityRule`: a rule an app wrote for
11915
- * its server, worn by a control as a constraint.
11916
- */
11917
-
11918
-
11919
- /**
11920
- * Turns a @jsenv/validity rule into a constraint a control can wear, so an app
11921
- * rule written once — in the package its server reads too — is checked on both
11922
- * sides instead of being written twice.
11923
- *
11924
- * @param {object} rule
11925
- * `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
11926
- * to `createValidity({ rules })`.
11927
- * @param {object} [ruleConfig]
11928
- * What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
11929
- * rule named `maxWords`. Pass `formatMessage` here to say the refusal through
11930
- * the app's own i18n; without it the key is looked up in `naviI18n` under
11931
- * `constraint.<key>`, and a rule answering with a finished sentence is shown
11932
- * as-is.
11933
- *
11934
- * Call it once, at module level: a constraint rebuilt on every render is a new
11935
- * object on every check.
11936
- */
11937
- const constraintFromValidityRule = (rule, ruleConfig = {}) => {
11938
- const { formatMessage, ...ruleParams } = ruleConfig;
11939
- return {
11940
- name: rule.name,
11941
- check: (field) => {
11942
- const result = rule.applyOn(
11943
- ruleParams[rule.name],
11944
- field.uiState,
11945
- ruleParams,
11946
- );
11947
- if (!result) {
11948
- return null;
11949
- }
11950
- if (typeof result === "string") {
11951
- return result;
11952
- }
11953
- if (formatMessage) {
11954
- return formatMessage(result.key, result.params);
11955
- }
11956
- return naviI18nFromValidityMessage(result);
11957
- },
11958
- };
11959
- };
11960
-
11961
- const naviI18nFromValidityMessage = ({ key, params }) => {
11962
- if (params && typeof params.max === "number") {
11963
- // Lets a template pluralize on the bound it names: "[max] retour[s]".
11964
- return naviI18n(`constraint.${key}`, {
11965
- ...params,
11966
- s: params.max > 1 ? "s" : "",
11967
- });
11968
- }
11969
- return naviI18n(`constraint.${key}`, params);
11970
- };
11971
-
11972
11985
  /**
11973
11986
  * `data-displayable` — the value must be something the layout can actually
11974
11987
  * draw. What it refuses and why lives in @jsenv/validity's DISPLAYABLE_RULE:
@@ -35277,17 +35290,21 @@ const useUIStateController = (
35277
35290
  if (el) {
35278
35291
  el.__uiStateController__ = controller;
35279
35292
  }
35280
- // Re-register so the radio registry stays in sync when props.ref changes
35281
- // identity (e.g. across a Suspense boundary). The render-phase call in
35282
- // control_hooks.jsx handles the initial mount; this call handles re-runs.
35283
- onUIStateControllerCreated(controller);
35284
35293
  return () => {
35285
35294
  if (el && el.__uiStateController__ === controller) {
35286
35295
  delete el.__uiStateController__;
35287
35296
  }
35288
- onUIStateControllerDestroyed(controller);
35289
35297
  };
35290
35298
  }, [controllerRef]);
35299
+ // The registry entry lives as long as the component does. Registration is
35300
+ // done by the render (see useInteractiveProps); this cleanup is the only
35301
+ // thing that takes the entry away, so it must not follow the ref: a ref
35302
+ // identity change would run it right after the render that registered.
35303
+ useLayoutEffect(() => {
35304
+ return () => {
35305
+ onUIStateControllerDestroyed(controller);
35306
+ };
35307
+ }, []);
35291
35308
 
35292
35309
  const { parentUIStateController: parentController } = scope;
35293
35310
  useLayoutEffect(() => {
@@ -38916,11 +38933,13 @@ const useInteractiveProps = (props, {
38916
38933
  // an action's own completion side effect — has to read the signal to get an
38917
38934
  // answer that is not one frame late (see BUSY_CONSTRAINT).
38918
38935
  uiStateController.boundAction = boundAction;
38936
+ // "This id is that controller", stated on every render: a Suspense boundary
38937
+ // parking a subtree runs every hook cleanup below it — the unregistration
38938
+ // included — while the controllers themselves survive, and readers look a
38939
+ // controller up by id while rendering, before any effect can run again.
38940
+ // Placed after controlHostProps is set, which the constraints read.
38941
+ onUIStateControllerCreated(uiStateController);
38919
38942
  if (firstRender) {
38920
- // Deferred from the factory so these run after controlHostProps is set.
38921
- // Constraints like READONLY_CONSTRAINT and findControlProxyTargetController
38922
- // read controlHostProps — calling these earlier would throw or produce wrong results.
38923
- onUIStateControllerCreated(uiStateController);
38924
38943
  uiStateController.rules.validation.checkValidity();
38925
38944
  }
38926
38945
  return [controlRootProps, controlHostProps];
@@ -48839,10 +48858,11 @@ const ButtonUI = props => {
48839
48858
  cta,
48840
48859
  spacing,
48841
48860
  contentDisplay,
48842
- // Whether the button draws the loading outline itself. A button that is
48843
- // one half of a bigger control says no: what is busy is the control, and
48844
- // the outline belongs around the whole of it (see split_button.jsx).
48845
- loadingOutline = true
48861
+ // Who draws the loading outline. Left unsaid, the button does; "custom"
48862
+ // says someone else does — a button that is one half of a bigger control:
48863
+ // what is busy is the control, and the outline belongs around the whole
48864
+ // of it (see split_button.jsx).
48865
+ loadingOutline
48846
48866
  } = props;
48847
48867
  const [buttonControlRootProps, buttonControlHostProps, controlChildrenWrapperProps] = useControlProps(props, {
48848
48868
  controlType: "button"
@@ -48968,7 +48988,7 @@ const ButtonUI = props => {
48968
48988
  visualSelector: visualSelector,
48969
48989
  hasChildUsingForwardedProps: true,
48970
48990
  children: [jsx(LoadingOutline, {
48971
- loading: loadingOutline && loading,
48991
+ loading: loadingOutline === "custom" ? false : loading,
48972
48992
  inset: -1,
48973
48993
  color: "var(--button-loader-color)"
48974
48994
  }), jsx(ControlChildrenWrapper, {
@@ -59548,6 +59568,15 @@ const css$E = /* css */`
59548
59568
  to filter). An entrance animation moves the dialog through scale and
59549
59569
  transform instead, which compose under it: see popup_css.js. */
59550
59570
 
59571
+ /* animation="growing" brings its own wall: the popup is the anchor
59572
+ continued, not a surface shown over a page that goes on being read (see
59573
+ navi_css_vars.js for the paint). Before the rules below, so everything
59574
+ said out loud — an outside press that captures, a backdropVariant, the
59575
+ two paint props — still wins over what the animation assumes. */
59576
+ &[data-growing] {
59577
+ --backdrop-background: var(--navi-backdrop-grow-background);
59578
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59579
+ }
59551
59580
  &[data-pointer-interaction-outside="capture"] {
59552
59581
  --backdrop-background: var(--navi-backdrop-capture-background);
59553
59582
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59718,6 +59747,15 @@ const css$E = /* css */`
59718
59747
  --backdrop-background: var(--navi-backdrop-close-background);
59719
59748
  --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
59720
59749
  }
59750
+ /* animation="growing" brings its own wall: the popup is the anchor
59751
+ continued, not a surface shown over a page that goes on being read (see
59752
+ navi_css_vars.js for the paint). Before the rules below, so everything
59753
+ said out loud — an outside press that captures, a backdropVariant, the
59754
+ two paint props — still wins over what the animation assumes. */
59755
+ &[data-growing] {
59756
+ --backdrop-background: var(--navi-backdrop-grow-background);
59757
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
59758
+ }
59721
59759
  &[data-pointer-interaction-outside="capture"] {
59722
59760
  --backdrop-background: var(--navi-backdrop-capture-background);
59723
59761
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59935,7 +59973,8 @@ const css$E = /* css */`
59935
59973
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
59936
59974
  * visible the backdrop is, independently of what it does. `"auto"`: the
59937
59975
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
59938
- * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
59976
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`), or the opaque wall
59977
+ * `animation="growing"` asks for. `"discrete"`: a
59939
59978
  * barely-there dim. `"invisible"`: fully transparent — a wall that is not
59940
59979
  * seen, still catching every press. This only changes how much the dialog
59941
59980
  * insists visually, never what an outside click does; whether there is a
@@ -59968,7 +60007,11 @@ const css$E = /* css */`
59968
60007
  * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
59969
60008
  * prop) and grows into whatever inside the dialog carries `data-grow`, the
59970
60009
  * dialog itself when nothing does. `"auto"` never picks it: only the caller
59971
- * knows the two boxes are one object. See `popup_grow.js`.
60010
+ * knows the two boxes are one object. It also brings its own backdrop —
60011
+ * opaque and blurred (`--navi-backdrop-grow-*`), the page it came out of
60012
+ * being what the movement leaves rather than a context to keep readable;
60013
+ * `backdropVariant="discrete"` asks for the light wash back. See
60014
+ * `popup_grow.js`.
59972
60015
  * @param {"box"|"scene"} [props.grow="box"] - Under `animation="growing"`,
59973
60016
  * what the anchor and what it grows into are to each other, which decides
59974
60017
  * how their pictures sit in the box moving between them. `"box"`: one
@@ -60998,6 +61041,7 @@ const useDialogProps = props => {
60998
61041
  "animationDuration": rest.animationDuration,
60999
61042
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
61000
61043
  "data-backdrop-variant": backdropVariant,
61044
+ "data-growing": growing ? "" : undefined,
61001
61045
  backdropColor,
61002
61046
  backdropFilter
61003
61047
  });
@@ -61033,6 +61077,11 @@ const useDialogProps = props => {
61033
61077
  // (and harmless for a local dialog, whose real backdrop element gets it
61034
61078
  // via backdropProps).
61035
61079
  "data-backdrop-variant": backdropVariant,
61080
+ // That this popup is the anchor continued rather than a surface shown
61081
+ // over the page, which is what its backdrop is painted from (see this
61082
+ // file's CSS). It cannot be read off navi-animation: growing arms no CSS
61083
+ // animation of its own, so that attribute is deliberately absent here.
61084
+ "data-growing": growing ? "" : undefined,
61036
61085
  // That this dialog has no wall, said in the DOM: the browser generates a
61037
61086
  // ::backdrop for the popover a wall-less top-layer dialog is shown as,
61038
61087
  // and the CSS above keys off this to leave it unpainted. Also the only
@@ -61186,6 +61235,9 @@ const DIALOG_STYLE_CSS_VARS = {
61186
61235
  animationDuration: "--popup-animation-duration",
61187
61236
  backdropColor: "--backdrop-background",
61188
61237
  backdropFilter: "--backdrop-filter",
61238
+ backgroundColor: "--dialog-background-color",
61239
+ boxShadow: "--dialog-box-shadow",
61240
+ borderWidth: "--dialog-border-width",
61189
61241
  borderRadius: "--dialog-border-radius",
61190
61242
  minWidth: "--dialog-min-width",
61191
61243
  maxWidth: "--dialog-max-width",
@@ -62779,10 +62831,17 @@ const POPOVER_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
62779
62831
  // Lets consumers pass animationDuration="0.5s"/borderRadius="8px" as regular
62780
62832
  // props; Box maps them to the CSS vars for us (see box.jsx's styleCSSVars
62781
62833
  // handling).
62834
+ // The paint props go through the popover's own vars rather than the properties
62835
+ // themselves, so the stylesheet keeps the last word on them (the same reason
62836
+ // dialog.jsx gives for borderRadius).
62782
62837
  const POPUP_STYLE_CSS_VARS = {
62783
62838
  animationDuration: "--popup-animation-duration",
62784
62839
  backdropColor: "--backdrop-background",
62785
62840
  backdropFilter: "--backdrop-filter",
62841
+ backgroundColor: "--popover-background-color",
62842
+ boxShadow: "--popover-box-shadow",
62843
+ borderWidth: "--popover-border-width",
62844
+ borderRadius: "--popover-border-radius",
62786
62845
  minWidth: "--popover-min-width",
62787
62846
  maxWidth: "--popover-max-width",
62788
62847
  minHeight: "--popover-min-height",
@@ -63045,83 +63104,56 @@ const Popup = props => {
63045
63104
  Popup.Close = PopupClose;
63046
63105
 
63047
63106
  installImportMetaCssBuild(import.meta);
63048
- const css$B = /* css */`.navi_picker {
63049
- &[aria-haspopup="listbox"] {
63050
- & .navi_popover {
63051
- --popover-border-radius: var(--picker-popup-border-radius, var(--picker-border-radius));
63052
- --popover-border-width: var(--picker-border-width);
63053
- --popover-border-color: var(--x-picker-border-color);
63054
- --popover-background-color: var(--picker-popup-background-color, var(--navi-popup-background-color));
63055
- --popover-outline-width: var(--picker-outline-width);
63056
- --popover-outline-color: var(--picker-outline-color);
63057
- --popover-box-shadow: var(--picker-popup-box-shadow, var(--navi-popup-box-shadow));
63058
- --popover-max-height: var(--picker-popover-max-height);
63059
- min-width: var(--picker-popover-min-width, var(--anchor-width, 0px));
63060
- cursor: default;
63061
-
63062
- & .navi_list_container {
63063
- --list-border-radius: max(0px,
63064
- var(--popover-border-radius) - var(--popover-border-width));
63065
- overscroll-behavior: none;
63066
- width: 100%;
63107
+ const css$B = /* css */`.navi_popover[data-picker-popup] {
63108
+ --popover-border-radius: var(--picker-border-radius);
63109
+ --popover-border-width: var(--picker-border-width);
63110
+ --popover-border-color: var(--x-picker-border-color);
63111
+ --popover-outline-width: var(--picker-outline-width);
63112
+ --popover-outline-color: var(--picker-outline-color);
63113
+ cursor: default;
63067
63114
 
63068
- &:not([data-overflow-visible]) {
63069
- overflow: auto;
63070
- }
63071
- }
63072
- }
63115
+ &:not([navi-hidden]) {
63116
+ flex-direction: column;
63117
+ display: flex;
63118
+ }
63073
63119
 
63074
- &[aria-expanded="true"] {
63075
- &[navi-popover-mode="overlay"] {
63076
- border-color: #0000;
63077
- }
63120
+ & .navi_list_container {
63121
+ --list-border-radius: max(0px,
63122
+ var(--popover-border-radius) - var(--popover-border-width));
63123
+ overscroll-behavior: none;
63124
+ width: 100%;
63078
63125
 
63079
- & .navi_popover {
63080
- flex-direction: column;
63081
- display: flex;
63082
- }
63126
+ &:not([data-overflow-visible]) {
63127
+ overflow: auto;
63083
63128
  }
63084
63129
  }
63130
+ }
63085
63131
 
63086
- &[aria-haspopup="dialog"] {
63087
- & .navi_dialog {
63088
- --dialog-border-radius: var(--picker-popup-border-radius, var(--picker-border-radius));
63089
- --dialog-border-width: var(--picker-dialog-border-width, 0px);
63090
- --dialog-border-color: var(--x-picker-border-color);
63091
- --dialog-background-color: var(--picker-popup-background-color, var(--navi-popup-background-color));
63092
- --dialog-outline-width: var(--picker-outline-width);
63093
- --dialog-outline-color: var(--picker-outline-color);
63094
- --dialog-box-shadow: var(--picker-popup-box-shadow, var(--navi-popup-box-shadow));
63095
- --dialog-min-width: var(--picker-dialog-min-width);
63096
- --dialog-min-height: var(--picker-dialog-min-height);
63097
- --dialog-max-width: var(--picker-dialog-max-width);
63098
- --dialog-max-height: var(--picker-dialog-max-height);
63099
- cursor: default;
63132
+ .navi_picker[aria-haspopup="listbox"][aria-expanded="true"][navi-popover-mode="overlay"] {
63133
+ border-color: #0000;
63134
+ }
63100
63135
 
63101
- &[open] {
63102
- flex-direction: column;
63103
- }
63136
+ .navi_dialog[data-picker-popup] {
63137
+ --dialog-border-radius: var(--picker-border-radius);
63138
+ --dialog-border-color: var(--x-picker-border-color);
63139
+ --dialog-outline-width: var(--picker-outline-width);
63140
+ --dialog-outline-color: var(--picker-outline-color);
63141
+ cursor: default;
63104
63142
 
63105
- & .navi_list_container {
63106
- --list-border-radius: max(0px,
63107
- var(--dialog-border-radius) - var(--dialog-border-width));
63108
- overscroll-behavior: none;
63109
- width: 100%;
63143
+ &[open] {
63144
+ flex-direction: column;
63145
+ }
63110
63146
 
63111
- &:not([data-overflow-visible]) {
63112
- overflow: auto;
63113
- }
63114
- }
63115
- }
63147
+ & .navi_list_container {
63148
+ --list-border-radius: max(0px,
63149
+ var(--dialog-border-radius) - var(--dialog-border-width));
63150
+ overscroll-behavior: none;
63151
+ width: 100%;
63116
63152
 
63117
- &[data-dialog-size-from-anchor] .navi_dialog {
63118
- --dialog-max-width: var(--picker-dialog-max-width, var(--anchor-width));
63153
+ &:not([data-overflow-visible]) {
63154
+ overflow: auto;
63119
63155
  }
63120
63156
  }
63121
-
63122
- &[data-popup-width-fit-content] {
63123
- --picker-popover-min-width: 0px;
63124
- }
63125
63157
  }
63126
63158
  `;
63127
63159
  const PickerCustomResolver = props => {
@@ -63885,6 +63917,22 @@ const PickerContentInsidePopup = props => {
63885
63917
  // dialogMaxWidth="var(--anchor-width)", as a ceiling) — what keeps a card
63886
63918
  // its own width once lifted. Dialog's own `sizeFromAnchor`.
63887
63919
  dialogSizeFromAnchor,
63920
+ // The caller's word on the popup's own box. Written on the popup element,
63921
+ // where Popover/Dialog map them to their own vars, rather than as vars on
63922
+ // the picker: a var on the picker inherits into everything the popup
63923
+ // holds, and the pickers in there would read it as their own.
63924
+ popupBackgroundColor,
63925
+ popupBorderRadius,
63926
+ popupBoxShadow,
63927
+ popoverMaxHeight,
63928
+ dialogBorderWidth,
63929
+ dialogMinWidth,
63930
+ dialogMinHeight,
63931
+ dialogMaxWidth,
63932
+ dialogMaxHeight,
63933
+ // The popover is at least as wide as the trigger. True when the CONTENT
63934
+ // should size it (a Wheel) instead of being stretched to the trigger.
63935
+ popupWidthFitContent,
63888
63936
  animation,
63889
63937
  grow,
63890
63938
  animationDuration,
@@ -63903,7 +63951,6 @@ const PickerContentInsidePopup = props => {
63903
63951
  return jsx(Next, {
63904
63952
  "aria-haspopup": isPopover ? "listbox" : "dialog",
63905
63953
  "navi-popover-mode": isPopover ? popoverMode : undefined,
63906
- "data-dialog-size-from-anchor": !isPopover && dialogSizeFromAnchor ? "" : undefined,
63907
63954
  ...rest,
63908
63955
  // On popupProps already (see the picker's popup assembly); they mean
63909
63956
  // nothing to the picker element.
@@ -63945,7 +63992,23 @@ const PickerContentInsidePopup = props => {
63945
63992
  })
63946
63993
  }) : jsx(Popup, {
63947
63994
  ...popupProps,
63948
- "data-testid": popupTestId,
63995
+ "data-testid": popupTestId
63996
+ // What the CSS above keys on: this picker's own popup, and no other
63997
+ // popup below the picker.
63998
+ ,
63999
+ "data-picker-popup": "",
64000
+ backgroundColor: popupBackgroundColor,
64001
+ borderRadius: popupBorderRadius,
64002
+ boxShadow: popupBoxShadow,
64003
+ borderWidth: isPopover ? undefined : dialogBorderWidth,
64004
+ minWidth: isPopover ? popupWidthFitContent ? undefined : "var(--anchor-width, 0px)" : dialogMinWidth,
64005
+ minHeight: isPopover ? undefined : dialogMinHeight
64006
+ // As wide as the trigger under dialogSizeFromAnchor: Dialog's own
64007
+ // sizeFromAnchor is a floor, this is the ceiling. A dialogMaxWidth of
64008
+ // the caller's still wins.
64009
+ ,
64010
+ maxWidth: isPopover ? undefined : dialogMaxWidth === undefined && dialogSizeFromAnchor ? "var(--anchor-width)" : dialogMaxWidth,
64011
+ maxHeight: isPopover ? popoverMaxHeight : dialogMaxHeight,
63949
64012
  mode: mode,
63950
64013
  layer: popupLayer,
63951
64014
  animation: animation,
@@ -72043,6 +72106,26 @@ const css$t = /* css */`@layer navi {
72043
72106
  }
72044
72107
  }
72045
72108
 
72109
+ & [data-picker-content] {
72110
+ --picker-padding: initial;
72111
+ --picker-padding-x: initial;
72112
+ --picker-padding-y: initial;
72113
+ --picker-padding-top: initial;
72114
+ --picker-padding-right: initial;
72115
+ --picker-padding-bottom: initial;
72116
+ --picker-padding-left: initial;
72117
+ --picker-press-padding: initial;
72118
+ --picker-press-padding-x: initial;
72119
+ --picker-press-padding-y: initial;
72120
+ --picker-press-padding-top: initial;
72121
+ --picker-press-padding-right: initial;
72122
+ --picker-press-padding-bottom: initial;
72123
+ --picker-press-padding-left: initial;
72124
+ --picker-slot-spacing: initial;
72125
+ --picker-align-x: initial;
72126
+ --picker-align-y: initial;
72127
+ }
72128
+
72046
72129
  & .navi_picker_content {
72047
72130
  --x-corner-top-left-radius: initial;
72048
72131
  --x-corner-top-right-radius: initial;
@@ -72246,11 +72329,6 @@ const PickerButton = props => {
72246
72329
  placeholder,
72247
72330
  ui,
72248
72331
  maxLines: maxLinesProp = 1,
72249
- // By default the popover is at least as wide as the trigger (min-width:
72250
- // --anchor-width). Set true when the CONTENT should dictate the popover width
72251
- // (e.g. a Wheel) instead of being stretched to the trigger — see
72252
- // picker_custom.jsx.
72253
- popupWidthFitContent,
72254
72332
  // Adds a clear button to the right slot, the same one type="search" puts at
72255
72333
  // the end of an input: a picker holds a value the user chose, and unsetting
72256
72334
  // it should not require reopening the popup to hunt for a "none" entry.
@@ -72275,7 +72353,13 @@ const PickerButton = props => {
72275
72353
  // (see PickerInput below).
72276
72354
  role,
72277
72355
  readOnly,
72278
- error
72356
+ error,
72357
+ // Who draws the loading outline. Left unsaid, the picker does, around its
72358
+ // own box; "custom" says someone else does — a bigger surface this box is
72359
+ // a piece of, waiting as a whole (see the `loadingOutline` doc).
72360
+ loadingOutline,
72361
+ // Where the outline runs, in px from the picker's box: negative is outside.
72362
+ loadingOutlineInset = -2
72279
72363
  } = props;
72280
72364
  // A word in a sentence is never truncated — and the clamp's overflow: hidden
72281
72365
  // would cut its dotted underline, which sits on the edge of the line box
@@ -72395,7 +72479,6 @@ const PickerButton = props => {
72395
72479
  "navi-single-line": isSingleLine ? "" : undefined,
72396
72480
  "navi-ui-custom": ui === "default" ? undefined : "",
72397
72481
  "data-readonly-opens": readOnlyOpens ? "" : undefined,
72398
- "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
72399
72482
  ...pickerRemainingProps,
72400
72483
  basePseudoState: basePseudoState,
72401
72484
  styleCSSVars: PickerStyleCSSVars,
@@ -72410,9 +72493,10 @@ const PickerButton = props => {
72410
72493
  openWhileReadOnly: undefined,
72411
72494
  ui: undefined,
72412
72495
  maxLines: undefined,
72413
- popupWidthFitContent: undefined,
72414
72496
  error: undefined,
72415
- dayLabel: undefined
72497
+ dayLabel: undefined,
72498
+ loadingOutline: undefined,
72499
+ loadingOutlineInset: undefined
72416
72500
  // This wrapper will receive keyboard event bubbling from the picker popup content
72417
72501
  // we re-dispatch on the input (to get escape to close for instance)
72418
72502
  ,
@@ -72443,9 +72527,9 @@ const PickerButton = props => {
72443
72527
  children: jsxs(PickerContext.Provider, {
72444
72528
  value: pickerContext,
72445
72529
  children: [variant === "headless" ? null : jsx(LoadingOutline, {
72446
- loading: loading,
72530
+ loading: loadingOutline === "custom" ? false : loading,
72447
72531
  color: "var(--picker-loader-color)",
72448
- inset: -2
72532
+ inset: loadingOutlineInset
72449
72533
  }), jsx(PickerInput, {
72450
72534
  role: hostRole,
72451
72535
  "aria-labelledby": hostLabelId,
@@ -72680,7 +72764,7 @@ const PickerClear = ({
72680
72764
  // removed is the whole field's, and the picker already draws the wait
72681
72765
  // around all of it. Two outlines for one wait is one too many.
72682
72766
  ,
72683
- loadingOutline: false
72767
+ loadingOutline: "custom"
72684
72768
  // preventDefault, not just tabIndex="-1": a mousedown focuses its target
72685
72769
  // before any click happens, and this button should never hold focus at
72686
72770
  // all — the field keeps it.
@@ -72854,21 +72938,16 @@ const PICKER_BUTTON_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":fo
72854
72938
  const PickerInputPseudoClasses = [":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
72855
72939
 
72856
72940
  // A pair is [the css variable, the style whose values the prop is written in]:
72857
- // popupBorderRadius takes what borderRadius takes, dialogMinWidth what minWidth
72858
- // takes — see box.jsx's readCSSVarEntry.
72941
+ // slotSpacing takes what margin takes, pressPadding what padding takes — see
72942
+ // box.jsx's readCSSVarEntry.
72943
+ // What the props say about the picker's own box only. The popup props
72944
+ // (popupBackgroundColor, dialogMaxWidth…) are handed to the popup element by
72945
+ // picker_custom.jsx: a variable on the root inherits into everything the popup
72946
+ // holds, and the pickers in there would read it as their own.
72859
72947
  const PickerStyleCSSVars = {
72860
72948
  "outlineWidth": "--picker-outline-width",
72861
72949
  "borderWidth": "--picker-border-width",
72862
72950
  "borderRadius": "--picker-border-radius",
72863
- "popoverMaxHeight": ["--picker-popover-max-height", "maxHeight"],
72864
- "dialogMinWidth": ["--picker-dialog-min-width", "minWidth"],
72865
- "dialogMinHeight": ["--picker-dialog-min-height", "minHeight"],
72866
- "dialogMaxWidth": ["--picker-dialog-max-width", "maxWidth"],
72867
- "dialogMaxHeight": ["--picker-dialog-max-height", "maxHeight"],
72868
- "popupBackgroundColor": "--picker-popup-background-color",
72869
- "popupBorderRadius": ["--picker-popup-border-radius", "borderRadius"],
72870
- "popupBoxShadow": ["--picker-popup-box-shadow", "boxShadow"],
72871
- "dialogBorderWidth": ["--picker-dialog-border-width", "borderWidth"],
72872
72951
  "slotSpacing": ["--picker-slot-spacing", "margin"],
72873
72952
  "pressPadding": ["--picker-press-padding", "padding"],
72874
72953
  "pressPaddingX": ["--picker-press-padding-x", "padding"],
@@ -72975,6 +73054,8 @@ const PickerFirstResolver = props => {
72975
73054
  * popoverMode?: "nearby" | "overlay",
72976
73055
  * positionArea?: string,
72977
73056
  * popupWidthFitContent?: boolean,
73057
+ * loadingOutline?: "custom",
73058
+ * loadingOutlineInset?: number,
72978
73059
  * variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "text" | "bare" | "picker",
72979
73060
  * icon?: boolean,
72980
73061
  * alignX?: "start" | "center" | "end",
@@ -73092,9 +73173,12 @@ const PickerFirstResolver = props => {
73092
73173
  * its own as well. Either way a card shows the score the moment the sheet
73093
73174
  * leaves and takes it back on a refusal, with nothing held by the caller.
73094
73175
  * `loading` is the wait it wears meanwhile: navi draws its loading outline
73095
- * on every trigger, but on a card-sized `variant="bare"` a two-pixel run
73096
- * around the box says nothing — a big surface draws its own waiting state
73097
- * from that flag, or from `[data-loading]` on the picker root in css.
73176
+ * on every trigger, two pixels out of the box. On a card-sized
73177
+ * `variant="bare"` that run sits on the card's own border and reads as a
73178
+ * border blinking; `loadingOutlineInset` moves it clear of it. And when the
73179
+ * picker's box is not the shape of what waits, `loadingOutline="custom"`
73180
+ * lets the drawing draw the wait itself, from that flag or from
73181
+ * `[data-loading]` on the picker root in css.
73098
73182
  * @param {boolean} [readOnly] Nothing in this picker can be changed — and it
73099
73183
  * still opens, so what is in the popup can be read: everything in there is
73100
73184
  * held read-only in turn, each control greying out and saying why on its own.
@@ -73221,6 +73305,21 @@ const PickerFirstResolver = props => {
73221
73305
  * nearest positioned ancestor it happens to find. `"picker"` (or an explicit
73222
73306
  * `variant={undefined}`) asks a confirm or callout picker for the field-like
73223
73307
  * drawing every other picker has.
73308
+ * @param {"custom"} [loadingOutline] Who draws the loading outline while the
73309
+ * action runs. Left unsaid, the picker does, around its own box. `"custom"`
73310
+ * says someone else does — what it means on a `Button` too — for a picker
73311
+ * that carries a wait drawn elsewhere: two nested pickers sharing one run
73312
+ * (a score picker drawn inside the card an edit picker wraps, see
73313
+ * `12_picker_card_demo.html`) are one wait, and one outline; and a
73314
+ * `variant="bare"` whose box is not
73315
+ * the shape of what waits (a card with a note pinned under it) leaves the
73316
+ * outline to the card, which knows its own box. The wait itself is still
73317
+ * worn: `aria-busy`, the busy refusal, the error callout, `[data-loading]`.
73318
+ * @param {number} [loadingOutlineInset=-2] Where the loading outline runs, in
73319
+ * px from the picker's box — negative is outside. A field wants it right on
73320
+ * its border; a card-sized trigger wants it a few pixels out, clear of the
73321
+ * card's own frame, so it reads as a run around the card rather than as its
73322
+ * border blinking.
73224
73323
  * @param {boolean} [icon] The trigger is one icon — the `ui`, or the chevron
73225
73324
  * when there is none. Its box is that icon's box plus the padding asked for
73226
73325
  * and nothing else: no right slot, and no control line to be as tall as, so
@@ -73306,7 +73405,9 @@ const PickerFirstResolver = props => {
73306
73405
  * (the popup itself when nothing does), and back on close. The trigger IS
73307
73406
  * the anchor, so nothing has to be named for it — `dialogSizeFromAnchor`
73308
73407
  * for a card that must keep its width on the way, left out for a drawing
73309
- * that opens precisely to get bigger.
73408
+ * that opens precisely to get bigger. It brings an opaque, blurred backdrop
73409
+ * with it (`--navi-backdrop-grow-*`); `backdropVariant="discrete"` asks for
73410
+ * the light wash back.
73310
73411
  * @param {"box"|"scene"} [grow="box"] Dialog's own, under `animation="growing"`:
73311
73412
  * `"box"` for a card that extends (its top stays, the box uncovers the rest),
73312
73413
  * `"scene"` for a thumbnail and the bigger view it is cut from (each picture
@@ -73317,9 +73418,9 @@ const PickerFirstResolver = props => {
73317
73418
  * as a ceiling, unless a `dialogMaxWidth` says otherwise. What keeps a card
73318
73419
  * its own width once lifted out of the page (`animation="growing"`, with
73319
73420
  * `data-grow` on the card inside the popup): it moves, and nothing else
73320
- * about it changes. The ceiling is set here rather than through
73321
- * `dialogMaxWidth="var(--anchor-width)"` because that variable lives on the
73322
- * dialog element, and a prop on the picker is resolved on the picker.
73421
+ * about it changes. `dialogMaxWidth="var(--anchor-width)"` says the same
73422
+ * ceiling by hand: the popup props are written on the dialog element, where
73423
+ * that variable lives.
73323
73424
  * @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
73324
73425
  * picker. "cancel" puts back the value the picker had at open, and a dialog
73325
73426
  * picker also goes back in history — so anything written to the url while it
@@ -75965,7 +76066,7 @@ const SplitButton = props => {
75965
76066
  readOnly,
75966
76067
  disabled,
75967
76068
  // The outline belongs around the pair, drawn below.
75968
- loadingOutline: false
76069
+ loadingOutline: "custom"
75969
76070
  };
75970
76071
  return jsxs(Box, {
75971
76072
  ref: rootRef,