@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
 
@@ -4910,8 +4910,15 @@ const onUIStateControllerCreated = (uiStateController) => {
4910
4910
  const onUIStateControllerDestroyed = (uiStateController) => {
4911
4911
  const { id, name, controlType } = uiStateController;
4912
4912
  if (id) {
4913
- controllersById.delete(id);
4914
- naviJsRegistry.delete(id);
4913
+ // Only the controller the id currently points at may take the entry away:
4914
+ // when two controls share an id, the one leaving would otherwise unregister
4915
+ // the one staying (see the warning in onUIStateControllerCreated), and the
4916
+ // same holds while a control is being replaced by its successor, which
4917
+ // registers during its render, before this cleanup runs.
4918
+ if (controllersById.get(id) === uiStateController) {
4919
+ controllersById.delete(id);
4920
+ naviJsRegistry.delete(id);
4921
+ }
4915
4922
  }
4916
4923
  const proxyFor = uiStateController.props["navi-control-proxy-for"];
4917
4924
  if (proxyFor) {
@@ -8204,6 +8211,75 @@ const languagesSignal = computed(() => {
8204
8211
  // (nor the "languagechange" listener above).
8205
8212
  setRuntimeLangSource(() => languagesSignal.value);
8206
8213
 
8214
+ /**
8215
+ * Where navi meets @jsenv/validity.
8216
+ *
8217
+ * validity names a refusal with a key and its parameters rather than a
8218
+ * sentence, so that a field and a server can refuse in the same words in the
8219
+ * language of the person reading. navi is one of those two callers: it says the
8220
+ * sentence, in the browser, through `naviI18n`. The keys line up on purpose —
8221
+ * validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
8222
+ * — so overriding a message is looking up one key, not going through a
8223
+ * translation table.
8224
+ *
8225
+ * The other direction is `constraintFromValidityRule`: a rule an app wrote for
8226
+ * its server, worn by a control as a constraint.
8227
+ */
8228
+
8229
+
8230
+ /**
8231
+ * Turns a @jsenv/validity rule into a constraint a control can wear, so an app
8232
+ * rule written once — in the package its server reads too — is checked on both
8233
+ * sides instead of being written twice.
8234
+ *
8235
+ * @param {object} rule
8236
+ * `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
8237
+ * to `createValidity({ rules })`.
8238
+ * @param {object} [ruleConfig]
8239
+ * What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
8240
+ * rule named `maxWords`. Pass `formatMessage` here to say the refusal through
8241
+ * the app's own i18n; without it the key is looked up in `naviI18n` under
8242
+ * `constraint.<key>`, and a rule answering with a finished sentence is shown
8243
+ * as-is.
8244
+ *
8245
+ * Call it once, at module level: a constraint rebuilt on every render is a new
8246
+ * object on every check.
8247
+ */
8248
+ const constraintFromValidityRule = (rule, ruleConfig = {}) => {
8249
+ const { formatMessage, ...ruleParams } = ruleConfig;
8250
+ return {
8251
+ name: rule.name,
8252
+ check: (field) => {
8253
+ const result = rule.applyOn(
8254
+ ruleParams[rule.name],
8255
+ field.uiState,
8256
+ ruleParams,
8257
+ );
8258
+ if (!result) {
8259
+ return null;
8260
+ }
8261
+ if (typeof result === "string") {
8262
+ return result;
8263
+ }
8264
+ if (formatMessage) {
8265
+ return formatMessage(result.key, result.params);
8266
+ }
8267
+ return naviI18nFromValidityMessage(result);
8268
+ },
8269
+ };
8270
+ };
8271
+
8272
+ const naviI18nFromValidityMessage = ({ key, params }) => {
8273
+ if (params && typeof params.max === "number") {
8274
+ // Lets a template pluralize on the bound it names: "[max] retour[s]".
8275
+ return naviI18n(`constraint.${key}`, {
8276
+ ...params,
8277
+ s: params.max > 1 ? "s" : "",
8278
+ });
8279
+ }
8280
+ return naviI18n(`constraint.${key}`, params);
8281
+ };
8282
+
8207
8283
  /**
8208
8284
  * https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
8209
8285
  */
@@ -8353,9 +8429,8 @@ const PATTERN_CONSTRAINT = {
8353
8429
  };
8354
8430
  CONSTRAINT_ATTRIBUTE_SET.add("pattern");
8355
8431
 
8356
- // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/email#validation
8357
- const emailregex =
8358
- /^[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])?)*$/;
8432
+ // What an address is, is @jsenv/validity's `type: "email"`: a server checking
8433
+ // the value again refuses it for the same reason and in the same words.
8359
8434
  const TYPE_EMAIL_CONSTRAINT = {
8360
8435
  name: "type_email",
8361
8436
  messageAttribute: "data-type-message",
@@ -8369,14 +8444,11 @@ const TYPE_EMAIL_CONSTRAINT = {
8369
8444
  if (!valueAsString) {
8370
8445
  return null;
8371
8446
  }
8372
- if (emailregex.test(valueAsString)) {
8447
+ const result = TYPE_RULE.applyOn("email", valueAsString);
8448
+ if (!result) {
8373
8449
  return null;
8374
8450
  }
8375
-
8376
- if (!valueAsString.includes("@")) {
8377
- return naviI18n("constraint.type.email.at", { value: valueAsString });
8378
- }
8379
- return naviI18n("constraint.type.email.invalid");
8451
+ return naviI18nFromValidityMessage(result);
8380
8452
  },
8381
8453
  };
8382
8454
 
@@ -11535,75 +11607,6 @@ const isFunctionButNotAnActionFunction = (action) => {
11535
11607
  return typeof action === "function" && !action.isAction;
11536
11608
  };
11537
11609
 
11538
- /**
11539
- * Where navi meets @jsenv/validity.
11540
- *
11541
- * validity names a refusal with a key and its parameters rather than a
11542
- * sentence, so that a field and a server can refuse in the same words in the
11543
- * language of the person reading. navi is one of those two callers: it says the
11544
- * sentence, in the browser, through `naviI18n`. The keys line up on purpose —
11545
- * validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
11546
- * — so overriding a message is looking up one key, not going through a
11547
- * translation table.
11548
- *
11549
- * The other direction is `constraintFromValidityRule`: a rule an app wrote for
11550
- * its server, worn by a control as a constraint.
11551
- */
11552
-
11553
-
11554
- /**
11555
- * Turns a @jsenv/validity rule into a constraint a control can wear, so an app
11556
- * rule written once — in the package its server reads too — is checked on both
11557
- * sides instead of being written twice.
11558
- *
11559
- * @param {object} rule
11560
- * `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
11561
- * to `createValidity({ rules })`.
11562
- * @param {object} [ruleConfig]
11563
- * What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
11564
- * rule named `maxWords`. Pass `formatMessage` here to say the refusal through
11565
- * the app's own i18n; without it the key is looked up in `naviI18n` under
11566
- * `constraint.<key>`, and a rule answering with a finished sentence is shown
11567
- * as-is.
11568
- *
11569
- * Call it once, at module level: a constraint rebuilt on every render is a new
11570
- * object on every check.
11571
- */
11572
- const constraintFromValidityRule = (rule, ruleConfig = {}) => {
11573
- const { formatMessage, ...ruleParams } = ruleConfig;
11574
- return {
11575
- name: rule.name,
11576
- check: (field) => {
11577
- const result = rule.applyOn(
11578
- ruleParams[rule.name],
11579
- field.uiState,
11580
- ruleParams,
11581
- );
11582
- if (!result) {
11583
- return null;
11584
- }
11585
- if (typeof result === "string") {
11586
- return result;
11587
- }
11588
- if (formatMessage) {
11589
- return formatMessage(result.key, result.params);
11590
- }
11591
- return naviI18nFromValidityMessage(result);
11592
- },
11593
- };
11594
- };
11595
-
11596
- const naviI18nFromValidityMessage = ({ key, params }) => {
11597
- if (params && typeof params.max === "number") {
11598
- // Lets a template pluralize on the bound it names: "[max] retour[s]".
11599
- return naviI18n(`constraint.${key}`, {
11600
- ...params,
11601
- s: params.max > 1 ? "s" : "",
11602
- });
11603
- }
11604
- return naviI18n(`constraint.${key}`, params);
11605
- };
11606
-
11607
11610
  /**
11608
11611
  * `data-displayable` — the value must be something the layout can actually
11609
11612
  * draw. What it refuses and why lives in @jsenv/validity's DISPLAYABLE_RULE:
@@ -34723,17 +34726,21 @@ const useUIStateController = (
34723
34726
  if (el) {
34724
34727
  el.__uiStateController__ = controller;
34725
34728
  }
34726
- // Re-register so the radio registry stays in sync when props.ref changes
34727
- // identity (e.g. across a Suspense boundary). The render-phase call in
34728
- // control_hooks.jsx handles the initial mount; this call handles re-runs.
34729
- onUIStateControllerCreated(controller);
34730
34729
  return () => {
34731
34730
  if (el && el.__uiStateController__ === controller) {
34732
34731
  delete el.__uiStateController__;
34733
34732
  }
34734
- onUIStateControllerDestroyed(controller);
34735
34733
  };
34736
34734
  }, [controllerRef]);
34735
+ // The registry entry lives as long as the component does. Registration is
34736
+ // done by the render (see useInteractiveProps); this cleanup is the only
34737
+ // thing that takes the entry away, so it must not follow the ref: a ref
34738
+ // identity change would run it right after the render that registered.
34739
+ useLayoutEffect(() => {
34740
+ return () => {
34741
+ onUIStateControllerDestroyed(controller);
34742
+ };
34743
+ }, []);
34737
34744
 
34738
34745
  const { parentUIStateController: parentController } = scope;
34739
34746
  useLayoutEffect(() => {
@@ -38254,11 +38261,13 @@ const useInteractiveProps = (props, {
38254
38261
  // an action's own completion side effect — has to read the signal to get an
38255
38262
  // answer that is not one frame late (see BUSY_CONSTRAINT).
38256
38263
  uiStateController.boundAction = boundAction;
38264
+ // "This id is that controller", stated on every render: a Suspense boundary
38265
+ // parking a subtree runs every hook cleanup below it — the unregistration
38266
+ // included — while the controllers themselves survive, and readers look a
38267
+ // controller up by id while rendering, before any effect can run again.
38268
+ // Placed after controlHostProps is set, which the constraints read.
38269
+ onUIStateControllerCreated(uiStateController);
38257
38270
  if (firstRender) {
38258
- // Deferred from the factory so these run after controlHostProps is set.
38259
- // Constraints like READONLY_CONSTRAINT and findControlProxyTargetController
38260
- // read controlHostProps — calling these earlier would throw or produce wrong results.
38261
- onUIStateControllerCreated(uiStateController);
38262
38271
  uiStateController.rules.validation.checkValidity();
38263
38272
  }
38264
38273
  return [controlRootProps, controlHostProps];
@@ -48112,10 +48121,11 @@ const ButtonUI = props => {
48112
48121
  cta,
48113
48122
  spacing,
48114
48123
  contentDisplay,
48115
- // Whether the button draws the loading outline itself. A button that is
48116
- // one half of a bigger control says no: what is busy is the control, and
48117
- // the outline belongs around the whole of it (see split_button.jsx).
48118
- loadingOutline = true
48124
+ // Who draws the loading outline. Left unsaid, the button does; "custom"
48125
+ // says someone else does — a button that is one half of a bigger control:
48126
+ // what is busy is the control, and the outline belongs around the whole
48127
+ // of it (see split_button.jsx).
48128
+ loadingOutline
48119
48129
  } = props;
48120
48130
  const [buttonControlRootProps, buttonControlHostProps, controlChildrenWrapperProps] = useControlProps(props, {
48121
48131
  controlType: "button"
@@ -48241,7 +48251,7 @@ const ButtonUI = props => {
48241
48251
  visualSelector: visualSelector,
48242
48252
  hasChildUsingForwardedProps: true,
48243
48253
  children: [jsx(LoadingOutline, {
48244
- loading: loadingOutline && loading,
48254
+ loading: loadingOutline === "custom" ? false : loading,
48245
48255
  inset: -1,
48246
48256
  color: "var(--button-loader-color)"
48247
48257
  }), jsx(ControlChildrenWrapper, {
@@ -58765,6 +58775,15 @@ const css$E = /* css */`
58765
58775
  to filter). An entrance animation moves the dialog through scale and
58766
58776
  transform instead, which compose under it: see popup_css.js. */
58767
58777
 
58778
+ /* animation="growing" brings its own wall: the popup is the anchor
58779
+ continued, not a surface shown over a page that goes on being read (see
58780
+ navi_css_vars.js for the paint). Before the rules below, so everything
58781
+ said out loud — an outside press that captures, a backdropVariant, the
58782
+ two paint props — still wins over what the animation assumes. */
58783
+ &[data-growing] {
58784
+ --backdrop-background: var(--navi-backdrop-grow-background);
58785
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
58786
+ }
58768
58787
  &[data-pointer-interaction-outside="capture"] {
58769
58788
  --backdrop-background: var(--navi-backdrop-capture-background);
58770
58789
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -58935,6 +58954,15 @@ const css$E = /* css */`
58935
58954
  --backdrop-background: var(--navi-backdrop-close-background);
58936
58955
  --backdrop-filter: var(--navi-backdrop-close-backdrop-filter);
58937
58956
  }
58957
+ /* animation="growing" brings its own wall: the popup is the anchor
58958
+ continued, not a surface shown over a page that goes on being read (see
58959
+ navi_css_vars.js for the paint). Before the rules below, so everything
58960
+ said out loud — an outside press that captures, a backdropVariant, the
58961
+ two paint props — still wins over what the animation assumes. */
58962
+ &[data-growing] {
58963
+ --backdrop-background: var(--navi-backdrop-grow-background);
58964
+ --backdrop-filter: var(--navi-backdrop-grow-backdrop-filter);
58965
+ }
58938
58966
  &[data-pointer-interaction-outside="capture"] {
58939
58967
  --backdrop-background: var(--navi-backdrop-capture-background);
58940
58968
  --backdrop-filter: var(--navi-backdrop-capture-backdrop-filter);
@@ -59152,7 +59180,8 @@ const css$E = /* css */`
59152
59180
  * @param {"auto"|"discrete"|"invisible"} [props.backdropVariant="auto"] - How
59153
59181
  * visible the backdrop is, independently of what it does. `"auto"`: the
59154
59182
  * paint `pointerInteractionOutsideEffect` implies (dimmed for
59155
- * `"close"`/`"cancel"`, blurred glass for `"capture"`). `"discrete"`: a
59183
+ * `"close"`/`"cancel"`, blurred glass for `"capture"`), or the opaque wall
59184
+ * `animation="growing"` asks for. `"discrete"`: a
59156
59185
  * barely-there dim. `"invisible"`: fully transparent — a wall that is not
59157
59186
  * seen, still catching every press. This only changes how much the dialog
59158
59187
  * insists visually, never what an outside click does; whether there is a
@@ -59185,7 +59214,11 @@ const css$E = /* css */`
59185
59214
  * over it. It needs an anchor (whatever opened the dialog, or the `anchor`
59186
59215
  * prop) and grows into whatever inside the dialog carries `data-grow`, the
59187
59216
  * dialog itself when nothing does. `"auto"` never picks it: only the caller
59188
- * knows the two boxes are one object. See `popup_grow.js`.
59217
+ * knows the two boxes are one object. It also brings its own backdrop —
59218
+ * opaque and blurred (`--navi-backdrop-grow-*`), the page it came out of
59219
+ * being what the movement leaves rather than a context to keep readable;
59220
+ * `backdropVariant="discrete"` asks for the light wash back. See
59221
+ * `popup_grow.js`.
59189
59222
  * @param {"box"|"scene"} [props.grow="box"] - Under `animation="growing"`,
59190
59223
  * what the anchor and what it grows into are to each other, which decides
59191
59224
  * how their pictures sit in the box moving between them. `"box"`: one
@@ -60206,6 +60239,7 @@ const useDialogProps = props => {
60206
60239
  "animationDuration": rest.animationDuration,
60207
60240
  "data-pointer-interaction-outside": pointerInteractionOutsideEffect,
60208
60241
  "data-backdrop-variant": backdropVariant,
60242
+ "data-growing": growing ? "" : undefined,
60209
60243
  backdropColor,
60210
60244
  backdropFilter
60211
60245
  });
@@ -60241,6 +60275,11 @@ const useDialogProps = props => {
60241
60275
  // (and harmless for a local dialog, whose real backdrop element gets it
60242
60276
  // via backdropProps).
60243
60277
  "data-backdrop-variant": backdropVariant,
60278
+ // That this popup is the anchor continued rather than a surface shown
60279
+ // over the page, which is what its backdrop is painted from (see this
60280
+ // file's CSS). It cannot be read off navi-animation: growing arms no CSS
60281
+ // animation of its own, so that attribute is deliberately absent here.
60282
+ "data-growing": growing ? "" : undefined,
60244
60283
  // That this dialog has no wall, said in the DOM: the browser generates a
60245
60284
  // ::backdrop for the popover a wall-less top-layer dialog is shown as,
60246
60285
  // and the CSS above keys off this to leave it unpainted. Also the only
@@ -60394,6 +60433,9 @@ const DIALOG_STYLE_CSS_VARS = {
60394
60433
  animationDuration: "--popup-animation-duration",
60395
60434
  backdropColor: "--backdrop-background",
60396
60435
  backdropFilter: "--backdrop-filter",
60436
+ backgroundColor: "--dialog-background-color",
60437
+ boxShadow: "--dialog-box-shadow",
60438
+ borderWidth: "--dialog-border-width",
60397
60439
  borderRadius: "--dialog-border-radius",
60398
60440
  minWidth: "--dialog-min-width",
60399
60441
  maxWidth: "--dialog-max-width",
@@ -61981,10 +62023,17 @@ const POPOVER_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible",
61981
62023
  // Lets consumers pass animationDuration="0.5s"/borderRadius="8px" as regular
61982
62024
  // props; Box maps them to the CSS vars for us (see box.jsx's styleCSSVars
61983
62025
  // handling).
62026
+ // The paint props go through the popover's own vars rather than the properties
62027
+ // themselves, so the stylesheet keeps the last word on them (the same reason
62028
+ // dialog.jsx gives for borderRadius).
61984
62029
  const POPUP_STYLE_CSS_VARS = {
61985
62030
  animationDuration: "--popup-animation-duration",
61986
62031
  backdropColor: "--backdrop-background",
61987
62032
  backdropFilter: "--backdrop-filter",
62033
+ backgroundColor: "--popover-background-color",
62034
+ boxShadow: "--popover-box-shadow",
62035
+ borderWidth: "--popover-border-width",
62036
+ borderRadius: "--popover-border-radius",
61988
62037
  minWidth: "--popover-min-width",
61989
62038
  maxWidth: "--popover-max-width",
61990
62039
  minHeight: "--popover-min-height",
@@ -62247,83 +62296,56 @@ const Popup = props => {
62247
62296
  Popup.Close = PopupClose;
62248
62297
 
62249
62298
  installImportMetaCssBuild(import.meta);
62250
- const css$B = /* css */`.navi_picker {
62251
- &[aria-haspopup="listbox"] {
62252
- & .navi_popover {
62253
- --popover-border-radius: var(--picker-popup-border-radius, var(--picker-border-radius));
62254
- --popover-border-width: var(--picker-border-width);
62255
- --popover-border-color: var(--x-picker-border-color);
62256
- --popover-background-color: var(--picker-popup-background-color, var(--navi-popup-background-color));
62257
- --popover-outline-width: var(--picker-outline-width);
62258
- --popover-outline-color: var(--picker-outline-color);
62259
- --popover-box-shadow: var(--picker-popup-box-shadow, var(--navi-popup-box-shadow));
62260
- --popover-max-height: var(--picker-popover-max-height);
62261
- min-width: var(--picker-popover-min-width, var(--anchor-width, 0px));
62262
- cursor: default;
62263
-
62264
- & .navi_list_container {
62265
- --list-border-radius: max(0px,
62266
- var(--popover-border-radius) - var(--popover-border-width));
62267
- overscroll-behavior: none;
62268
- width: 100%;
62299
+ const css$B = /* css */`.navi_popover[data-picker-popup] {
62300
+ --popover-border-radius: var(--picker-border-radius);
62301
+ --popover-border-width: var(--picker-border-width);
62302
+ --popover-border-color: var(--x-picker-border-color);
62303
+ --popover-outline-width: var(--picker-outline-width);
62304
+ --popover-outline-color: var(--picker-outline-color);
62305
+ cursor: default;
62269
62306
 
62270
- &:not([data-overflow-visible]) {
62271
- overflow: auto;
62272
- }
62273
- }
62274
- }
62307
+ &:not([navi-hidden]) {
62308
+ flex-direction: column;
62309
+ display: flex;
62310
+ }
62275
62311
 
62276
- &[aria-expanded="true"] {
62277
- &[navi-popover-mode="overlay"] {
62278
- border-color: #0000;
62279
- }
62312
+ & .navi_list_container {
62313
+ --list-border-radius: max(0px,
62314
+ var(--popover-border-radius) - var(--popover-border-width));
62315
+ overscroll-behavior: none;
62316
+ width: 100%;
62280
62317
 
62281
- & .navi_popover {
62282
- flex-direction: column;
62283
- display: flex;
62284
- }
62318
+ &:not([data-overflow-visible]) {
62319
+ overflow: auto;
62285
62320
  }
62286
62321
  }
62322
+ }
62287
62323
 
62288
- &[aria-haspopup="dialog"] {
62289
- & .navi_dialog {
62290
- --dialog-border-radius: var(--picker-popup-border-radius, var(--picker-border-radius));
62291
- --dialog-border-width: var(--picker-dialog-border-width, 0px);
62292
- --dialog-border-color: var(--x-picker-border-color);
62293
- --dialog-background-color: var(--picker-popup-background-color, var(--navi-popup-background-color));
62294
- --dialog-outline-width: var(--picker-outline-width);
62295
- --dialog-outline-color: var(--picker-outline-color);
62296
- --dialog-box-shadow: var(--picker-popup-box-shadow, var(--navi-popup-box-shadow));
62297
- --dialog-min-width: var(--picker-dialog-min-width);
62298
- --dialog-min-height: var(--picker-dialog-min-height);
62299
- --dialog-max-width: var(--picker-dialog-max-width);
62300
- --dialog-max-height: var(--picker-dialog-max-height);
62301
- cursor: default;
62324
+ .navi_picker[aria-haspopup="listbox"][aria-expanded="true"][navi-popover-mode="overlay"] {
62325
+ border-color: #0000;
62326
+ }
62302
62327
 
62303
- &[open] {
62304
- flex-direction: column;
62305
- }
62328
+ .navi_dialog[data-picker-popup] {
62329
+ --dialog-border-radius: var(--picker-border-radius);
62330
+ --dialog-border-color: var(--x-picker-border-color);
62331
+ --dialog-outline-width: var(--picker-outline-width);
62332
+ --dialog-outline-color: var(--picker-outline-color);
62333
+ cursor: default;
62306
62334
 
62307
- & .navi_list_container {
62308
- --list-border-radius: max(0px,
62309
- var(--dialog-border-radius) - var(--dialog-border-width));
62310
- overscroll-behavior: none;
62311
- width: 100%;
62335
+ &[open] {
62336
+ flex-direction: column;
62337
+ }
62312
62338
 
62313
- &:not([data-overflow-visible]) {
62314
- overflow: auto;
62315
- }
62316
- }
62317
- }
62339
+ & .navi_list_container {
62340
+ --list-border-radius: max(0px,
62341
+ var(--dialog-border-radius) - var(--dialog-border-width));
62342
+ overscroll-behavior: none;
62343
+ width: 100%;
62318
62344
 
62319
- &[data-dialog-size-from-anchor] .navi_dialog {
62320
- --dialog-max-width: var(--picker-dialog-max-width, var(--anchor-width));
62345
+ &:not([data-overflow-visible]) {
62346
+ overflow: auto;
62321
62347
  }
62322
62348
  }
62323
-
62324
- &[data-popup-width-fit-content] {
62325
- --picker-popover-min-width: 0px;
62326
- }
62327
62349
  }
62328
62350
  `;
62329
62351
  const PickerCustomResolver = props => {
@@ -63087,6 +63109,22 @@ const PickerContentInsidePopup = props => {
63087
63109
  // dialogMaxWidth="var(--anchor-width)", as a ceiling) — what keeps a card
63088
63110
  // its own width once lifted. Dialog's own `sizeFromAnchor`.
63089
63111
  dialogSizeFromAnchor,
63112
+ // The caller's word on the popup's own box. Written on the popup element,
63113
+ // where Popover/Dialog map them to their own vars, rather than as vars on
63114
+ // the picker: a var on the picker inherits into everything the popup
63115
+ // holds, and the pickers in there would read it as their own.
63116
+ popupBackgroundColor,
63117
+ popupBorderRadius,
63118
+ popupBoxShadow,
63119
+ popoverMaxHeight,
63120
+ dialogBorderWidth,
63121
+ dialogMinWidth,
63122
+ dialogMinHeight,
63123
+ dialogMaxWidth,
63124
+ dialogMaxHeight,
63125
+ // The popover is at least as wide as the trigger. True when the CONTENT
63126
+ // should size it (a Wheel) instead of being stretched to the trigger.
63127
+ popupWidthFitContent,
63090
63128
  animation,
63091
63129
  grow,
63092
63130
  animationDuration,
@@ -63105,7 +63143,6 @@ const PickerContentInsidePopup = props => {
63105
63143
  return jsx(Next, {
63106
63144
  "aria-haspopup": isPopover ? "listbox" : "dialog",
63107
63145
  "navi-popover-mode": isPopover ? popoverMode : undefined,
63108
- "data-dialog-size-from-anchor": !isPopover && dialogSizeFromAnchor ? "" : undefined,
63109
63146
  ...rest,
63110
63147
  // On popupProps already (see the picker's popup assembly); they mean
63111
63148
  // nothing to the picker element.
@@ -63147,7 +63184,23 @@ const PickerContentInsidePopup = props => {
63147
63184
  })
63148
63185
  }) : jsx(Popup, {
63149
63186
  ...popupProps,
63150
- "data-testid": popupTestId,
63187
+ "data-testid": popupTestId
63188
+ // What the CSS above keys on: this picker's own popup, and no other
63189
+ // popup below the picker.
63190
+ ,
63191
+ "data-picker-popup": "",
63192
+ backgroundColor: popupBackgroundColor,
63193
+ borderRadius: popupBorderRadius,
63194
+ boxShadow: popupBoxShadow,
63195
+ borderWidth: isPopover ? undefined : dialogBorderWidth,
63196
+ minWidth: isPopover ? popupWidthFitContent ? undefined : "var(--anchor-width, 0px)" : dialogMinWidth,
63197
+ minHeight: isPopover ? undefined : dialogMinHeight
63198
+ // As wide as the trigger under dialogSizeFromAnchor: Dialog's own
63199
+ // sizeFromAnchor is a floor, this is the ceiling. A dialogMaxWidth of
63200
+ // the caller's still wins.
63201
+ ,
63202
+ maxWidth: isPopover ? undefined : dialogMaxWidth === undefined && dialogSizeFromAnchor ? "var(--anchor-width)" : dialogMaxWidth,
63203
+ maxHeight: isPopover ? popoverMaxHeight : dialogMaxHeight,
63151
63204
  mode: mode,
63152
63205
  layer: popupLayer,
63153
63206
  animation: animation,
@@ -71187,6 +71240,26 @@ const css$t = /* css */`@layer navi {
71187
71240
  }
71188
71241
  }
71189
71242
 
71243
+ & [data-picker-content] {
71244
+ --picker-padding: initial;
71245
+ --picker-padding-x: initial;
71246
+ --picker-padding-y: initial;
71247
+ --picker-padding-top: initial;
71248
+ --picker-padding-right: initial;
71249
+ --picker-padding-bottom: initial;
71250
+ --picker-padding-left: initial;
71251
+ --picker-press-padding: initial;
71252
+ --picker-press-padding-x: initial;
71253
+ --picker-press-padding-y: initial;
71254
+ --picker-press-padding-top: initial;
71255
+ --picker-press-padding-right: initial;
71256
+ --picker-press-padding-bottom: initial;
71257
+ --picker-press-padding-left: initial;
71258
+ --picker-slot-spacing: initial;
71259
+ --picker-align-x: initial;
71260
+ --picker-align-y: initial;
71261
+ }
71262
+
71190
71263
  & .navi_picker_content {
71191
71264
  --x-corner-top-left-radius: initial;
71192
71265
  --x-corner-top-right-radius: initial;
@@ -71388,11 +71461,6 @@ const PickerButton = props => {
71388
71461
  placeholder,
71389
71462
  ui,
71390
71463
  maxLines: maxLinesProp = 1,
71391
- // By default the popover is at least as wide as the trigger (min-width:
71392
- // --anchor-width). Set true when the CONTENT should dictate the popover width
71393
- // (e.g. a Wheel) instead of being stretched to the trigger — see
71394
- // picker_custom.jsx.
71395
- popupWidthFitContent,
71396
71464
  // Adds a clear button to the right slot, the same one type="search" puts at
71397
71465
  // the end of an input: a picker holds a value the user chose, and unsetting
71398
71466
  // it should not require reopening the popup to hunt for a "none" entry.
@@ -71417,7 +71485,13 @@ const PickerButton = props => {
71417
71485
  // (see PickerInput below).
71418
71486
  role,
71419
71487
  readOnly,
71420
- error
71488
+ error,
71489
+ // Who draws the loading outline. Left unsaid, the picker does, around its
71490
+ // own box; "custom" says someone else does — a bigger surface this box is
71491
+ // a piece of, waiting as a whole (see the `loadingOutline` doc).
71492
+ loadingOutline,
71493
+ // Where the outline runs, in px from the picker's box: negative is outside.
71494
+ loadingOutlineInset = -2
71421
71495
  } = props;
71422
71496
  // A word in a sentence is never truncated — and the clamp's overflow: hidden
71423
71497
  // would cut its dotted underline, which sits on the edge of the line box
@@ -71536,7 +71610,6 @@ const PickerButton = props => {
71536
71610
  "navi-single-line": isSingleLine ? "" : undefined,
71537
71611
  "navi-ui-custom": ui === "default" ? undefined : "",
71538
71612
  "data-readonly-opens": readOnlyOpens ? "" : undefined,
71539
- "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
71540
71613
  ...pickerRemainingProps,
71541
71614
  basePseudoState: basePseudoState,
71542
71615
  styleCSSVars: PickerStyleCSSVars,
@@ -71551,9 +71624,10 @@ const PickerButton = props => {
71551
71624
  openWhileReadOnly: undefined,
71552
71625
  ui: undefined,
71553
71626
  maxLines: undefined,
71554
- popupWidthFitContent: undefined,
71555
71627
  error: undefined,
71556
- dayLabel: undefined
71628
+ dayLabel: undefined,
71629
+ loadingOutline: undefined,
71630
+ loadingOutlineInset: undefined
71557
71631
  // This wrapper will receive keyboard event bubbling from the picker popup content
71558
71632
  // we re-dispatch on the input (to get escape to close for instance)
71559
71633
  ,
@@ -71584,9 +71658,9 @@ const PickerButton = props => {
71584
71658
  children: jsxs(PickerContext.Provider, {
71585
71659
  value: pickerContext,
71586
71660
  children: [variant === "headless" ? null : jsx(LoadingOutline, {
71587
- loading: loading,
71661
+ loading: loadingOutline === "custom" ? false : loading,
71588
71662
  color: "var(--picker-loader-color)",
71589
- inset: -2
71663
+ inset: loadingOutlineInset
71590
71664
  }), jsx(PickerInput, {
71591
71665
  role: hostRole,
71592
71666
  "aria-labelledby": hostLabelId,
@@ -71820,7 +71894,7 @@ const PickerClear = ({
71820
71894
  // removed is the whole field's, and the picker already draws the wait
71821
71895
  // around all of it. Two outlines for one wait is one too many.
71822
71896
  ,
71823
- loadingOutline: false
71897
+ loadingOutline: "custom"
71824
71898
  // preventDefault, not just tabIndex="-1": a mousedown focuses its target
71825
71899
  // before any click happens, and this button should never hold focus at
71826
71900
  // all — the field keeps it.
@@ -71983,21 +72057,16 @@ const PICKER_BUTTON_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":fo
71983
72057
  const PickerInputPseudoClasses = [":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value", ":-navi-expanded"];
71984
72058
 
71985
72059
  // A pair is [the css variable, the style whose values the prop is written in]:
71986
- // popupBorderRadius takes what borderRadius takes, dialogMinWidth what minWidth
71987
- // takes — see box.jsx's readCSSVarEntry.
72060
+ // slotSpacing takes what margin takes, pressPadding what padding takes — see
72061
+ // box.jsx's readCSSVarEntry.
72062
+ // What the props say about the picker's own box only. The popup props
72063
+ // (popupBackgroundColor, dialogMaxWidth…) are handed to the popup element by
72064
+ // picker_custom.jsx: a variable on the root inherits into everything the popup
72065
+ // holds, and the pickers in there would read it as their own.
71988
72066
  const PickerStyleCSSVars = {
71989
72067
  "outlineWidth": "--picker-outline-width",
71990
72068
  "borderWidth": "--picker-border-width",
71991
72069
  "borderRadius": "--picker-border-radius",
71992
- "popoverMaxHeight": ["--picker-popover-max-height", "maxHeight"],
71993
- "dialogMinWidth": ["--picker-dialog-min-width", "minWidth"],
71994
- "dialogMinHeight": ["--picker-dialog-min-height", "minHeight"],
71995
- "dialogMaxWidth": ["--picker-dialog-max-width", "maxWidth"],
71996
- "dialogMaxHeight": ["--picker-dialog-max-height", "maxHeight"],
71997
- "popupBackgroundColor": "--picker-popup-background-color",
71998
- "popupBorderRadius": ["--picker-popup-border-radius", "borderRadius"],
71999
- "popupBoxShadow": ["--picker-popup-box-shadow", "boxShadow"],
72000
- "dialogBorderWidth": ["--picker-dialog-border-width", "borderWidth"],
72001
72070
  "slotSpacing": ["--picker-slot-spacing", "margin"],
72002
72071
  "pressPadding": ["--picker-press-padding", "padding"],
72003
72072
  "pressPaddingX": ["--picker-press-padding-x", "padding"],
@@ -74562,7 +74631,7 @@ const SplitButton = props => {
74562
74631
  readOnly,
74563
74632
  disabled,
74564
74633
  // The outline belongs around the pair, drawn below.
74565
- loadingOutline: false
74634
+ loadingOutline: "custom"
74566
74635
  };
74567
74636
  return jsxs(Box, {
74568
74637
  ref: rootRef,