@jsenv/navi 0.29.112 → 0.29.114

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.
@@ -4256,14 +4256,6 @@ const findControlProxy = (el) => {
4256
4256
  return firstProxy;
4257
4257
  };
4258
4258
 
4259
- let renderMessageText = text => text;
4260
- const setCalloutMessageTextRenderer = renderer => {
4261
- renderMessageText = renderer;
4262
- };
4263
-
4264
- // What a callout's own JSX may know about the callout around it: how to close
4265
- // it, and the element — whose id is what `commandFor` needs when the button
4266
- // closing it is not inside it (inside, `--navi-close` finds it on its own).
4267
4259
  const CalloutContext = createContext();
4268
4260
  const useCalloutRequestClose = () => {
4269
4261
  return useContext(CalloutContext)?.requestClose;
@@ -4285,9 +4277,10 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
4285
4277
  render(calloutJsx, calloutMessageElement);
4286
4278
  };
4287
4279
 
4288
- // An HTML message is rendered through preact rather than innerHTML so that its
4289
- // text can go through renderMessageText: an emoji in a validation message must
4290
- // not make the first line taller than the icon and close button beside it.
4280
+ // An HTML message is rendered through preact rather than innerHTML: what a
4281
+ // message holds is then a preact tree like any other, so a callout's own JSX
4282
+ // (its context, its close button) works the same whether the message arrived
4283
+ // as markup or as elements.
4291
4284
  const renderHtmlIntoCallout = (html, calloutMessageElement, {
4292
4285
  requestClose,
4293
4286
  element
@@ -4311,7 +4304,7 @@ const domToVNodes = node => {
4311
4304
  const vnodes = [];
4312
4305
  for (const child of node.childNodes) {
4313
4306
  if (child.nodeType === Node.TEXT_NODE) {
4314
- vnodes.push(renderMessageText(child.data));
4307
+ vnodes.push(child.data);
4315
4308
  continue;
4316
4309
  }
4317
4310
  if (child.nodeType !== Node.ELEMENT_NODE) {
@@ -4452,11 +4445,12 @@ const css$11 = /* css */`
4452
4445
  --callout-icon-color: black;
4453
4446
  --callout-padding: 8px;
4454
4447
  --callout-z-index: var(--navi-z-index-callout);
4455
- /* The callout's own, like its font: the icon and the cross are columns
4456
- one line tall (1lh), and that line has to be the message's first
4457
- line — which it is only if both read the same line-height, rather
4458
- than whatever the element the callout sits in happens to use. */
4459
- --callout-line-height: 1.5;
4448
+ /* The page's line, said here rather than inherited: the icon and the
4449
+ cross are columns one line tall (1lh), and that line has to be the
4450
+ message's first line — which it is only if both read the same
4451
+ line-height, rather than whatever the element the callout sits in
4452
+ happens to use. */
4453
+ --callout-line-height: var(--navi-line-height);
4460
4454
  /* The cross is furniture, not content: quieter than the message beside
4461
4455
  it, and the size of a glyph on the message's first line. */
4462
4456
  --callout-close-button-color: color-mix(
@@ -17681,13 +17675,17 @@ const singleLineEllipsisStyles = () => {
17681
17675
  };
17682
17676
  };
17683
17677
  // The lines beyond the clamp are still laid out, so the clip edge decides
17684
- // whether the top of the first hidden one is visible: "overflow: hidden" clips
17685
- // at the padding box and lets it show inside the block-end padding. Clipping at
17686
- // the content box instead ends the element right after its last visible line.
17678
+ // whether the top of the first hidden one is visible: the padding box lets it
17679
+ // show inside the block-end padding, the content box ends the element right
17680
+ // after its last visible line. That box also ends the inline axis at the last
17681
+ // glyph's advance width, and a script or italic face draws past it, so the
17682
+ // clip is pushed back out by the half-leading: room for ink that leaves the
17683
+ // advance width on every side, still above the hidden line's own em box.
17684
+ // `overflow-clip-margin` has no per-axis form, hence one length for all four.
17687
17685
  const lineClampStyles = (value) => {
17688
17686
  return {
17689
17687
  "overflow": "clip",
17690
- "overflowClipMargin": "content-box",
17688
+ "overflowClipMargin": "content-box calc((1lh - 1em) / 2)",
17691
17689
  "display": "-webkit-box",
17692
17690
  "-webkit-box-orient": "vertical",
17693
17691
  "-webkit-line-clamp": value,
@@ -37537,10 +37535,14 @@ const css$Y = /* css */`
37537
37535
  * the child so that its visual position matches the requested `textAnchor` value — regardless of
37538
37536
  * font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
37539
37537
  *
37540
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
37538
+ * @param {"line-top"|"char-top"|"center"|"char-center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
37541
37539
  * - `"line-top"` — child top aligns with the top of the surrounding line box
37542
37540
  * - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
37543
37541
  * - `"center"` — child is vertically centered on the surrounding line box
37542
+ * - `"char-center"` — child is vertically centered on the capitals: midway between the
37543
+ * ink ascent and the baseline. What "centered on the text" means to
37544
+ * the eye — the line box center sits above it, by half the leading
37545
+ * plus whatever the font keeps above its capitals.
37544
37546
  * - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
37545
37547
  * - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
37546
37548
  * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
@@ -37650,13 +37652,14 @@ const computeTopOffset = ({
37650
37652
  let desiredChildTopY = 0;
37651
37653
  if (textAnchor === "line-top") {
37652
37654
  desiredChildTopY = anchorRect.top;
37653
- } else if (textAnchor === "char-top") {
37655
+ } else if (textAnchor === "char-top" || textAnchor === "char-center") {
37654
37656
  const anchorStyle = getComputedStyle(anchorEl);
37655
37657
  const ctx = charTopCanvas.getContext("2d");
37656
37658
  ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
37657
37659
  const m = ctx.measureText("M");
37658
37660
  const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
37659
- desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
37661
+ const capTopY = baselineY - m.actualBoundingBoxAscent;
37662
+ desiredChildTopY = textAnchor === "char-top" ? capTopY : (capTopY + baselineY) / 2 - childH / 2;
37660
37663
  } else if (textAnchor === "center") {
37661
37664
  const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
37662
37665
  desiredChildTopY = anchorCenterY - childH / 2;
@@ -38062,12 +38065,15 @@ const CustomWidthSpace = ({
38062
38065
  // copy-pasting the text produces an actual space, but we also want
38063
38066
  // full control over the visual width of that gap.
38064
38067
  // - First span: contains the real space but rendered at font-size:0 so it
38065
- // takes up zero visual space.
38068
+ // takes up zero visual space. line-height:0 with it: a line-height
38069
+ // inherited as a length (a control's, snapped to the pixel) would give
38070
+ // this empty box that height, centered on the baseline — and its lower
38071
+ // half would push the line box down under the text.
38066
38072
  // - Second span: a zero-width joiner (​) with padding-left set to
38067
38073
  // the desired gap size. This is the only visible part.
38068
38074
  return jsxs("span", {
38069
38075
  children: [jsx("span", {
38070
- style: "font-size: 0",
38076
+ style: "font-size: 0; line-height: 0",
38071
38077
  children: " "
38072
38078
  }), jsx("span", {
38073
38079
  style: `padding-left: ${value}`,
@@ -38223,7 +38229,6 @@ const shouldInjectSpacingBetween = (left, right) => {
38223
38229
  * loading?: boolean,
38224
38230
  * skeleton?: boolean,
38225
38231
  * attachLastChild?: boolean,
38226
- * emojiAsIcon?: boolean,
38227
38232
  * preventSpaceUnderlines?: boolean,
38228
38233
  * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
38229
38234
  * boldStable?: boolean,
@@ -38263,14 +38268,6 @@ const shouldInjectSpacingBetween = (left, right) => {
38263
38268
  * truncation belongs outside the `Text` instead (see `docs/typography.md`).
38264
38269
  * `Link` sets it on its own whenever it renders an end icon.
38265
38270
  *
38266
- * @param {boolean} [emojiAsIcon]
38267
- * Renders every emoji found in the string children as an `Icon`, so it sits
38268
- * in the line like a character and never makes the line taller than the
38269
- * text. For free text a user typed (a message, a description); see
38270
- * `docs/typography.md`. Only the strings this `Text` receives are rewritten —
38271
- * a string a child component renders is out of reach, and that component
38272
- * calls `renderEmojiAsIcon()` itself instead.
38273
- *
38274
38271
  * @param {boolean} [preventSpaceUnderlines]
38275
38272
  * Replaces real space characters between children with padding-based spaces.
38276
38273
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
@@ -38385,7 +38382,6 @@ const TextUI = props => {
38385
38382
  spacing,
38386
38383
  preventSpaceUnderlines = false,
38387
38384
  attachLastChild = false,
38388
- emojiAsIcon = false,
38389
38385
  boldStable,
38390
38386
  holdSpaceForStyle,
38391
38387
  capitalize,
@@ -38416,11 +38412,6 @@ const TextUI = props => {
38416
38412
  } else {
38417
38413
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
38418
38414
  }
38419
- if (emojiAsIcon) {
38420
- // After the spacing pass: an emoji glued to a word ("hello👋") must not
38421
- // get a separator injected as if it were a child element.
38422
- children = renderEmojiAsIcon(children);
38423
- }
38424
38415
  if (boldStable) {
38425
38416
  const {
38426
38417
  bold
@@ -38568,12 +38559,12 @@ const TextWithSelectRange = ({
38568
38559
  * meaning-bearing icon that needs to be exposed to assistive tech.
38569
38560
  * @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
38570
38561
  * interactive (`data-interactive`, pointer cursor) and non-decorative.
38571
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
38562
+ * @param {"line-top"|"char-top"|"center"|"char-center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
38572
38563
  * - Vertical alignment within the surrounding text line for the inline
38573
38564
  * char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
38574
38565
  * align to the line box edges, `"char-top"` to the ink ascent, `"center"`
38575
- * centers on the line box, `"char-bottom"` sits on the baseline. See
38576
- * `text_anchor.jsx`.
38566
+ * centers on the line box, `"char-center"` on the capitals, `"char-bottom"`
38567
+ * sits on the baseline. See `text_anchor.jsx`.
38577
38568
  * @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
38578
38569
  * Describes the surrounding line context (font size / vertical-align),
38579
38570
  * forwarded to `TextAnchor` so it recomputes the vertical correction when
@@ -38697,84 +38688,6 @@ const Icon = ({
38697
38688
  });
38698
38689
  };
38699
38690
 
38700
- // An emoji-presentation character (🌸), or a pictogram forced into emoji
38701
- // presentation by VS16 (❤️), followed by its skin-tone modifiers and ZWJ
38702
- // sequence. A flag is two regional indicators that must stay together.
38703
- const EMOJI_REGEX = /\p{Regional_Indicator}{2}|(?:\p{Emoji_Presentation}|[\p{Extended_Pictographic}--\p{Emoji_Presentation}]\uFE0F)(?:[\p{Emoji_Modifier}\uFE0F]|\u200D\p{Extended_Pictographic}\uFE0F?)*/gv;
38704
-
38705
- /**
38706
- * What `emojiAsIcon` does, for something that renders text without going
38707
- * through `Text` (a callout's message): every emoji in the string children
38708
- * comes back wrapped in an `Icon`. The system emoji fonts have a taller
38709
- * ascent/descent than text fonts, so a raw emoji glyph makes its line taller
38710
- * than the lines around it; inside an Icon it is capped at 1em and centered on
38711
- * the line like any glyph icon.
38712
- *
38713
- * Children come back in the shape they arrived in: a string stays a string
38714
- * when it holds no emoji, and the array is only built once a child actually
38715
- * needs rewriting.
38716
- */
38717
- const renderEmojiAsIcon = children => {
38718
- if (typeof children === "string") {
38719
- return renderEmojiInString(children);
38720
- }
38721
- const childArray = toChildArray(children);
38722
- let result = null;
38723
- let index = 0;
38724
- for (const child of childArray) {
38725
- if (typeof child === "string") {
38726
- const rendered = renderEmojiInString(child);
38727
- if (rendered !== child) {
38728
- if (result === null) {
38729
- result = childArray.slice(0, index);
38730
- }
38731
- for (const part of rendered) {
38732
- result.push(part);
38733
- }
38734
- index++;
38735
- continue;
38736
- }
38737
- }
38738
- if (result !== null) {
38739
- result.push(child);
38740
- }
38741
- index++;
38742
- }
38743
- if (result === null) {
38744
- return children;
38745
- }
38746
- return result;
38747
- };
38748
- const renderEmojiInString = string => {
38749
- let parts = null;
38750
- let lastIndex = 0;
38751
- for (const match of string.matchAll(EMOJI_REGEX)) {
38752
- if (parts === null) {
38753
- parts = [];
38754
- }
38755
- if (match.index > lastIndex) {
38756
- parts.push(string.slice(lastIndex, match.index));
38757
- }
38758
- parts.push(jsx(Icon, {
38759
- decorative: false,
38760
- children: jsx("span", {
38761
- children: match[0]
38762
- })
38763
- }));
38764
- lastIndex = match.index + match[0].length;
38765
- }
38766
- if (parts === null) {
38767
- return string;
38768
- }
38769
- if (lastIndex < string.length) {
38770
- parts.push(string.slice(lastIndex));
38771
- }
38772
- return parts;
38773
- };
38774
- // A callout message is free text like any other; see callout.jsx for why it
38775
- // cannot import this itself.
38776
- setCalloutMessageTextRenderer(renderEmojiAsIcon);
38777
-
38778
38691
  /**
38779
38692
  * Hook that reactively checks if a URL is visited.
38780
38693
  * Re-renders when the visited URL set changes.
@@ -39782,12 +39695,23 @@ const css$V = /* css */`
39782
39695
  }
39783
39696
  }
39784
39697
 
39785
- &[data-expand] {
39698
+ /* A nav asked to expand is a nav whose tabs share it: one equal slice each,
39699
+ the label in the middle of the target that slice makes — a row that fills
39700
+ its container while its tabs sit at their text width stops in the middle,
39701
+ and gives every tab a different size to aim at.
39702
+ The main axis only: a vertical nav expanding horizontally fills the width
39703
+ (align-items below) rather than sharing its height between its tabs. */
39704
+ &[data-expand-x] {
39786
39705
  flex-grow: 1;
39787
39706
 
39788
- .navi_tab {
39707
+ &:not([data-vertical]) .navi_link {
39708
+ flex: 1;
39709
+ justify-content: center;
39710
+ }
39711
+ }
39712
+ &[data-expand-y][data-vertical] {
39713
+ .navi_link {
39789
39714
  flex: 1;
39790
- justify-content: start;
39791
39715
  }
39792
39716
  }
39793
39717
  /* Vertical layout */
@@ -39795,17 +39719,6 @@ const css$V = /* css */`
39795
39719
  /* overflow-x: hidden; */
39796
39720
  /* overflow-y: auto; */
39797
39721
  align-items: stretch;
39798
-
39799
- &[data-expand] {
39800
- .navi_tab {
39801
- align-items: stretch;
39802
- }
39803
- }
39804
- .navi_tab {
39805
- width: 100%;
39806
- flex-direction: row;
39807
- text-align: left;
39808
- }
39809
39722
  }
39810
39723
 
39811
39724
  /* Folder tabs: the current tab and the panel share one surface. Every tab
@@ -39921,6 +39834,7 @@ const Nav = ({
39921
39834
  vertical,
39922
39835
  expand,
39923
39836
  expandX,
39837
+ expandY,
39924
39838
  linkBorderRadiusInherit,
39925
39839
  currentIndicator,
39926
39840
  currentIndicatorSlides = true,
@@ -40057,7 +39971,8 @@ const Nav = ({
40057
39971
  column: !vertical,
40058
39972
  baseClassName: "navi_nav",
40059
39973
  "data-link-border-radius-inherit": linkBorderRadiusInherit ? "" : undefined,
40060
- "data-expand": expand || expandX ? "" : undefined,
39974
+ "data-expand-x": expand || expandX ? "" : undefined,
39975
+ "data-expand-y": expand || expandY ? "" : undefined,
40061
39976
  "data-vertical": vertical ? "" : undefined,
40062
39977
  "data-panel-position": panelPosition,
40063
39978
  "data-nav-indicator": indicatorPosition ?? undefined
@@ -40075,6 +39990,7 @@ const Nav = ({
40075
39990
  "aria-orientation": slideContainer && vertical ? "vertical" : undefined,
40076
39991
  expand: expand,
40077
39992
  expandX: expandX,
39993
+ expandY: expandY,
40078
39994
  spacing: spacing,
40079
39995
  ...props,
40080
39996
  styleCSSVars: NavStyleCSSVars,
@@ -45533,6 +45449,10 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
45533
45449
  display: inline-block; /* So box css can override when wanting to put button inline flex */
45534
45450
  font-size: var(--button-font-size);
45535
45451
  font-family: var(--button-font-family);
45452
+ /* A form control comes with a line of its own from the browser, and that
45453
+ line is "normal": a label holding an emoji would then be taller than
45454
+ the same label without one. The page's line, like the page's font. */
45455
+ line-height: var(--navi-control-line-height);
45536
45456
  }
45537
45457
  }
45538
45458
 
@@ -45905,7 +45825,6 @@ const ButtonUI = props => {
45905
45825
  icon,
45906
45826
  cta,
45907
45827
  spacing,
45908
- emojiAsIcon = true,
45909
45828
  // Whether the button draws the loading outline itself. A button that is
45910
45829
  // one half of a bigger control says no: what is busy is the control, and
45911
45830
  // the outline belongs around the whole of it (see split_button.jsx).
@@ -45969,7 +45888,6 @@ const ButtonUI = props => {
45969
45888
 
45970
45889
  type: "button",
45971
45890
  spacing: undefined,
45972
- emojiAsIcon: undefined,
45973
45891
  cta: undefined,
45974
45892
  pressEffect: undefined,
45975
45893
  loadingOutline: undefined,
@@ -46023,7 +45941,6 @@ const ButtonUI = props => {
46023
45941
  ...controlChildrenWrapperProps,
46024
45942
  children: jsx(ButtonContent, {
46025
45943
  spacing: spacing,
46026
- emojiAsIcon: emojiAsIcon,
46027
45944
  children: children
46028
45945
  })
46029
45946
  })]
@@ -46031,7 +45948,6 @@ const ButtonUI = props => {
46031
45948
  };
46032
45949
  const ButtonContent = ({
46033
45950
  spacing,
46034
- emojiAsIcon,
46035
45951
  children
46036
45952
  }) => {
46037
45953
  const boxForwardedProps = useContext(BoxForwardedPropsContext);
@@ -46039,7 +45955,6 @@ const ButtonContent = ({
46039
45955
  ...boxForwardedProps,
46040
45956
  display: "inherit",
46041
45957
  spacing: spacing,
46042
- emojiAsIcon: emojiAsIcon,
46043
45958
  className: "navi_button_content",
46044
45959
  children: [children, jsx(ButtonShadow, {})]
46045
45960
  });
@@ -46198,7 +46113,6 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
46198
46113
  /**
46199
46114
  * @type {import("ignore:preact").FunctionComponent<{
46200
46115
  * ownTarget?: boolean | "refuse" | "always",
46201
- * emojiAsIcon?: boolean,
46202
46116
  * replace?: boolean,
46203
46117
  * [key: string]: any,
46204
46118
  * }>}
@@ -46209,9 +46123,6 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
46209
46123
  * @param {Function} [action] On a button with an `href` or a `route`, the
46210
46124
  * same order as a Link's: it runs on the press, before the navigation, and
46211
46125
  * the navigation does not wait for it (see Link's `action`).
46212
- * @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
46213
- * so the button keeps the height of its text — `Text`'s prop, on by default
46214
- * here. Pass `false` to let an emoji draw at its natural size.
46215
46126
  * @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
46216
46127
  * that belongs to another control — a chip's cross on a picker's façade, an
46217
46128
  * eye on a pressable row, a diskette inside a slide that travels. The press is
@@ -46814,7 +46725,8 @@ const inputCss = /* css */`
46814
46725
  --background-color: var(--navi-surface-color);
46815
46726
  --color: currentColor;
46816
46727
  --color-dimmed: color-mix(in srgb, currentColor 60%, transparent);
46817
- --placeholder-color: var(--color-dimmed);
46728
+ --placeholder-color: var(--navi-placeholder-color);
46729
+ --placeholder-font-style: var(--navi-placeholder-font-style);
46818
46730
  /* Hover */
46819
46731
  --border-color-hover: color-mix(in srgb, var(--border-color) 70%, black);
46820
46732
  --background-color-hover: color-mix(
@@ -46852,6 +46764,7 @@ const inputCss = /* css */`
46852
46764
  --x-background-color: var(--background-color);
46853
46765
  --x-color: var(--color);
46854
46766
  --x-placeholder-color: var(--placeholder-color);
46767
+ --x-placeholder-font-style: var(--placeholder-font-style);
46855
46768
  --x-padding-top: var(
46856
46769
  --padding-top,
46857
46770
  var(--padding-y, var(--padding, var(--navi-control-padding-y-default)))
@@ -46881,6 +46794,13 @@ const inputCss = /* css */`
46881
46794
  font-size: var(--font-size);
46882
46795
  font-family: var(--font-family);
46883
46796
  text-align: initial;
46797
+ /* On the root, not only on the field: what sits beside the field — a unit
46798
+ slot, a prefix, an icon — is text of the same box and has to land on
46799
+ the same row as what is typed. A slot left on the page's line would sit
46800
+ a fraction of a pixel away from the field's own, and read as misaligned
46801
+ ("+33" next to a number). The field repeats it below because a form
46802
+ control does not inherit it. */
46803
+ line-height: var(--navi-control-line-height);
46884
46804
  background-color: var(--x-background-color);
46885
46805
  border-width: var(--border-width);
46886
46806
  border-style: solid;
@@ -46931,9 +46851,12 @@ const inputCss = /* css */`
46931
46851
  font-size: inherit;
46932
46852
  /* A form control does not inherit the font on its own — the browser has
46933
46853
  one of its own for it, monospace for a <textarea> — so the box's font
46934
- (--navi-control-font-family) is handed down by hand. */
46854
+ (--navi-control-font-family) is handed down by hand. Its line comes
46855
+ from the page's token for the same reason: what is typed must sit on
46856
+ the same line as what displays it afterwards, emoji included. */
46935
46857
  font-family: inherit;
46936
46858
  text-align: inherit;
46859
+ line-height: var(--navi-control-line-height);
46937
46860
  background: none;
46938
46861
  border: none;
46939
46862
  border-radius: inherit;
@@ -46942,6 +46865,7 @@ const inputCss = /* css */`
46942
46865
 
46943
46866
  &::placeholder {
46944
46867
  color: var(--x-placeholder-color);
46868
+ font-style: var(--x-placeholder-font-style);
46945
46869
  }
46946
46870
  /* Webkit is putting a slight blue bckground on autofilled input */
46947
46871
  /* For now we override with out custom background color */
@@ -46969,10 +46893,6 @@ const inputCss = /* css */`
46969
46893
  /* Nothing to read yet is still a line: the box may not collapse just
46970
46894
  because the value is empty. */
46971
46895
  min-height: 1lh;
46972
- /* A form control keeps a line of its own whatever line-height the page
46973
- is written in; the text that stands in for one has to say the same
46974
- number, or the box it is meant to match changes height. */
46975
- line-height: normal;
46976
46896
  }
46977
46897
  /* The value is cut by a box of its own, and that is the whole reason it
46978
46898
  exists: a field ends its text at the content edge, while an overflow set
@@ -48014,15 +47934,321 @@ Input.UI = {
48014
47934
  };
48015
47935
 
48016
47936
  installImportMetaCssBuild(import.meta);/**
48017
- * - We must keep the edited element in the DOM so that
48018
- * the layout remains the same (especially important for table cells)
48019
- * And the editable part is in absolute so that it takes the original content dimensions
48020
- * AND for table cells it can actually take the table cell dimensions
47937
+ * Multiline text control that grows with what is typed.
47938
+ *
47939
+ * Autosize is native: `field-sizing: content` lets the browser size the
47940
+ * textarea from its value no hidden mirror textarea to measure against (the
47941
+ * technique libraries used before the property existed). `minRows`/`maxRows`
47942
+ * become min/max heights in `lh` units on top of it; past `maxRows` the
47943
+ * content scrolls.
48021
47944
  *
48022
- * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
47945
+ * TextareaCharCount is the counter that goes with it, and the caller places
47946
+ * it: under the box, in a form footer, next to a label — fed with the same
47947
+ * value/signal as the textarea. The textarea draws no counter of its own.
48023
47948
  *
47949
+ * Styled as a `.navi_input` box (border, background, focus ring, readonly and
47950
+ * disabled fades, variants): one look for everything one types into. The
47951
+ * shared sheet is registered here too — a page may render a Textarea without
47952
+ * any Input.
48024
47953
  */
48025
47954
  const css$I = /* css */`
47955
+ .navi_textarea {
47956
+ textarea {
47957
+ min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
47958
+ /* Above maxRows the box stops growing and the content scrolls. The
47959
+ 99999 fallback means "no cap" without needing a conditional rule. */
47960
+ max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
47961
+ field-sizing: content;
47962
+ /* Explicit, never normal, for two independent reasons.
47963
+ minRows/maxRows are lengths in lh, and with line-height normal the lh
47964
+ unit resolves to a theoretical value that does not match the real
47965
+ rendered line — the box then jumps by a few pixels the moment the first
47966
+ character replaces the theory with a real line.
47967
+ And a line box under "normal" takes the height of the tallest font it
47968
+ holds, so the one line carrying an emoji stands taller than the ones
47969
+ around it — and a tighter line would cut the top off the glyph.
47970
+ The number is the page's own (--navi-line-height, 1.25), snapped to
47971
+ the pixel like every control's: a message typed here and the same
47972
+ message displayed afterwards sit on the same line, emoji included.
47973
+ Bound to the token rather than inherited so it can never come back as
47974
+ "normal" from a container that sets one. See docs/typography.md. */
47975
+ line-height: var(--navi-control-line-height);
47976
+ /* The control grows itself; resizable below hands the handle back. */
47977
+ resize: none;
47978
+ overflow: auto;
47979
+ /* A placeholder must be readable in full before anything is typed: a
47980
+ field that opens already scrolled reads as a field that already has
47981
+ text in it. The lines it wraps to are counted (see
47982
+ usePlaceholderHeight) because they only exist once laid out, and they
47983
+ only raise the floor while the placeholder is what is being shown —
47984
+ what is typed sizes the box on its own. A count of lines rather than
47985
+ a height, so the floor is on the same grid as minRows. */
47986
+ &:placeholder-shown {
47987
+ min-height: max(
47988
+ calc(var(--textarea-min-rows, 1.5) * 1lh),
47989
+ calc(var(--x-textarea-placeholder-rows, 0) * 1lh)
47990
+ );
47991
+ }
47992
+ }
47993
+ &[data-resizable] {
47994
+ .navi_control_input {
47995
+ height: calc(var(--textarea-min-rows, 1.5) * 1lh);
47996
+ /* The two are exclusive: with field-sizing content the browser removes
47997
+ the resize handle (the size follows the content, there is nothing to
47998
+ drag). resizable means the hand takes over — fixed sizing, starting
47999
+ at minRows, and the drag writes its own inline height from there. */
48000
+ field-sizing: fixed;
48001
+ resize: vertical;
48002
+ }
48003
+ }
48004
+ }
48005
+ .navi_textarea_char_count {
48006
+ color: color-mix(in srgb, currentColor 60%, transparent);
48007
+ font-size: 0.75em;
48008
+ user-select: none;
48009
+ }
48010
+ `;
48011
+
48012
+ // minRows/maxRows are read by the CSS above, so they travel the way every
48013
+ // other style of this box travels: a prop mapped to the custom property it
48014
+ // stands for, written by Box itself (see its styleCSSVars).
48015
+ const TextareaStyleCSSVars = {
48016
+ ...InputStyleCSSVars,
48017
+ minRows: "--textarea-min-rows",
48018
+ maxRows: "--textarea-max-rows"
48019
+ };
48020
+
48021
+ /**
48022
+ * @type {import("ignore:preact").FunctionComponent<{
48023
+ * value?: string,
48024
+ * defaultValue?: string,
48025
+ * signal?: import("@preact/signals").Signal<string>,
48026
+ * name?: string,
48027
+ * minRows?: number,
48028
+ * maxRows?: number,
48029
+ * resizable?: boolean,
48030
+ * maxLength?: number,
48031
+ * width?: string,
48032
+ * [key: string]: any,
48033
+ * }>}
48034
+ * @param {number} [minRows=1.5] Height the empty control starts at, in lines.
48035
+ * The default shows half of a second line: enough to read "multiline" at a
48036
+ * glance without the height of a full extra row.
48037
+ * @param {number} [maxRows] Lines after which the control stops growing and
48038
+ * scrolls instead. Without it the control grows with its content.
48039
+ * @param {boolean} [resizable] Give the browser's vertical resize handle back.
48040
+ * An exchange, not an addition: the hand takes over from the automatic
48041
+ * growth, so the control stops following what is typed and stays at the
48042
+ * height it was last dragged to (starting at `minRows`).
48043
+ * @param {number} [maxLength] The character limit, validated at submit. Pair
48044
+ * with `maxLengthGuard` to block typing past it, and render a
48045
+ * TextareaCharCount to show it.
48046
+ * @param {string} [width="35ch"] The control's width. Fixed on purpose: with
48047
+ * field-sizing the width would otherwise follow the longest line, and a box
48048
+ * that widens while one types is a box one chases.
48049
+ */
48050
+ const Textarea = ({
48051
+ // Destructured, never deleted off the props object: Preact reuses the same
48052
+ // props object when an internal state update re-renders the component, so a
48053
+ // delete would make these props vanish from the second render on (the box
48054
+ // then jumps back to its default size at the first keystroke).
48055
+ resizable,
48056
+ width = "35ch",
48057
+ ...props
48058
+ }) => {
48059
+ import.meta.css = [inputCss + css$I, "@jsenv/navi/src/control/input/textarea.jsx"];
48060
+ const defaultRef = useRef(null);
48061
+ props.ref = props.ref || defaultRef;
48062
+ usePlaceholderHeight(props.ref, props.placeholder);
48063
+ const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
48064
+ controlType: "input"
48065
+ });
48066
+ const {
48067
+ basePseudoState,
48068
+ children
48069
+ } = hostProps;
48070
+ // Children go through ControlChildrenWrapper below; inside the <textarea>
48071
+ // element they would become its text content.
48072
+ delete hostProps.children;
48073
+ const loading = basePseudoState[":-navi-loading"];
48074
+ delete rootProps.width;
48075
+ hostProps.width = width;
48076
+ return jsxs(Box, {
48077
+ as: "span",
48078
+ inline: true,
48079
+ flex: true,
48080
+ baseClassName: "navi_input navi_textarea",
48081
+ ...rootProps,
48082
+ basePseudoState: basePseudoState,
48083
+ "data-resizable": resizable ? "" : undefined,
48084
+ styleCSSVars: TextareaStyleCSSVars,
48085
+ pseudoStateSelector: ".navi_control_input",
48086
+ pseudoClasses: InputPseudoClasses,
48087
+ pseudoElements: InputPseudoElements,
48088
+ "data-callout-anchor": ".navi_control_input",
48089
+ children: [jsx(LoadingOutline, {
48090
+ loading: loading,
48091
+ color: "var(--loader-color)",
48092
+ inset: -1
48093
+ }), jsx(RealTextarea, {
48094
+ ...hostProps
48095
+ }), jsx(ControlChildrenWrapper, {
48096
+ ...childrenWrapperProps,
48097
+ children: children
48098
+ })]
48099
+ });
48100
+ };
48101
+
48102
+ /**
48103
+ * The counter that goes with a Textarea: "50/200" — how many characters are
48104
+ * typed over how many the limit allows, the way Material writes it (just the
48105
+ * count when there is no `maxLength`). Where it goes is the caller's call,
48106
+ * which is why it is a separate component rather than something the textarea
48107
+ * draws: put it under the box, in a form footer, next to a label, and feed it
48108
+ * the same value or signal as the textarea.
48109
+ *
48110
+ * @type {import("ignore:preact").FunctionComponent<{
48111
+ * value?: string,
48112
+ * signal?: import("@preact/signals").Signal<string>,
48113
+ * maxLength?: number,
48114
+ * [key: string]: any,
48115
+ * }>}
48116
+ * @param {string} [value] The text being counted. Say `signal` instead for a
48117
+ * two-way bound textarea: reading it here subscribes the count to it.
48118
+ * @param {number} [maxLength] The limit, shown after the count ("50/200").
48119
+ * Without it the count stands alone.
48120
+ */
48121
+ const TextareaCharCount = ({
48122
+ value,
48123
+ signal,
48124
+ maxLength,
48125
+ ...rest
48126
+ }) => {
48127
+ import.meta.css = [css$I, "@jsenv/navi/src/control/input/textarea.jsx"];
48128
+ const resolvedValue = signal ? signal.value : value;
48129
+ const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
48130
+ return jsx(Box, {
48131
+ as: "span",
48132
+ baseClassName: "navi_textarea_char_count",
48133
+ ...rest,
48134
+ children: maxLength === undefined ? length : `${length}/${maxLength}`
48135
+ });
48136
+ };
48137
+
48138
+ // `field-sizing: content` sizes the box from the value, and an empty field has
48139
+ // none — the placeholder is text the browser refuses to make room for. So the
48140
+ // lines it wraps to are counted and published as --x-textarea-placeholder-rows
48141
+ // for the CSS above to use as a floor.
48142
+ const usePlaceholderHeight = (ref, placeholder) => {
48143
+ useLayoutEffect(() => {
48144
+ const textareaEl = ref.current;
48145
+ if (!placeholder) {
48146
+ textareaEl.style.removeProperty("--x-textarea-placeholder-rows");
48147
+ return null;
48148
+ }
48149
+ let widthMeasured;
48150
+ const measure = () => {
48151
+ // What is typed sizes the box itself; the placeholder is not displayed
48152
+ // then, and scrollHeight would report the value's height instead.
48153
+ if (textareaEl.value !== "") {
48154
+ return;
48155
+ }
48156
+ const {
48157
+ paddingTop,
48158
+ paddingBottom,
48159
+ lineHeight
48160
+ } = getComputedStyle(textareaEl);
48161
+ // Read with no floor under the box: scrollHeight can never report less
48162
+ // than the height the element already has, so a min-height still in
48163
+ // place — minRows, or the previous measure — would be read back as
48164
+ // lines of placeholder (a one-line placeholder over a 1.5-row floor
48165
+ // would count as two), and the box could only ever grow, never shrink
48166
+ // back on a wider viewport.
48167
+ textareaEl.style.setProperty("min-height", "0px");
48168
+ const contentHeight = textareaEl.scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
48169
+ textareaEl.style.removeProperty("min-height");
48170
+ // scrollHeight is a whole number of pixels where a line need not be: a
48171
+ // height handed back as is would put the floor a fraction above the
48172
+ // line grid, and the empty box and the one showing its placeholder
48173
+ // would not be the same height. A count of lines has no fraction.
48174
+ const rows = Math.round(contentHeight / parseFloat(lineHeight));
48175
+ widthMeasured = textareaEl.clientWidth;
48176
+ textareaEl.style.setProperty("--x-textarea-placeholder-rows", String(rows));
48177
+ };
48178
+ measure();
48179
+ // Measuring writes the variable that sets this element's own height, and
48180
+ // mutating layout from inside a resize callback is what makes the browser
48181
+ // report "ResizeObserver loop completed with undelivered notifications".
48182
+ // So the write waits for the frame that resize produced.
48183
+ let measureFrame = null;
48184
+ const requestMeasure = () => {
48185
+ if (measureFrame !== null) {
48186
+ return;
48187
+ }
48188
+ measureFrame = requestAnimationFrame(() => {
48189
+ measureFrame = null;
48190
+ measure();
48191
+ });
48192
+ };
48193
+ // The placeholder wraps against the available width, so a new width is a
48194
+ // new number of lines. Height changes are ignored: this measure is what
48195
+ // causes them, and reacting to them would be reacting to ourselves.
48196
+ const resizeObserver = new ResizeObserver(() => {
48197
+ if (textareaEl.clientWidth !== widthMeasured) {
48198
+ requestMeasure();
48199
+ }
48200
+ });
48201
+ resizeObserver.observe(textareaEl);
48202
+ // The width may have changed while the field held a value, when measuring
48203
+ // was impossible — emptying it is when the placeholder comes back.
48204
+ const onInput = () => {
48205
+ if (textareaEl.value === "") {
48206
+ measure();
48207
+ }
48208
+ };
48209
+ textareaEl.addEventListener("input", onInput);
48210
+ return () => {
48211
+ if (measureFrame !== null) {
48212
+ cancelAnimationFrame(measureFrame);
48213
+ }
48214
+ resizeObserver.disconnect();
48215
+ textareaEl.removeEventListener("input", onInput);
48216
+ };
48217
+ }, [placeholder]);
48218
+ };
48219
+ const RealTextarea = ({
48220
+ maxLength,
48221
+ ...domProps
48222
+ }) => {
48223
+ const autoSelectReadOnlyProps = useAutoSelectReadOnly(domProps);
48224
+ return jsx(Box, {
48225
+ ...domProps,
48226
+ as: "textarea",
48227
+ baseClassName: "navi_control_input",
48228
+ ...autoSelectReadOnlyProps,
48229
+ // Native maxLength stays off, like RealInput in input_textual.jsx: the
48230
+ // maxLengthGuard handles live blocking, the constraint validates at
48231
+ // submit, and navi-max-length keeps the value readable from the DOM.
48232
+ "navi-max-length": maxLength
48233
+ });
48234
+ };
48235
+
48236
+ installImportMetaCssBuild(import.meta);/**
48237
+ * Edition in place: a value is read where it is written, and the field editing
48238
+ * it is drawn exactly on that spot.
48239
+ *
48240
+ * The read text stays in the DOM the whole time and the field is laid over it,
48241
+ * in absolute. That is what keeps the layout still: the text is what gives the
48242
+ * box its size (a table cell especially, whose width belongs to the whole
48243
+ * column), so taking it away would resize the box the moment one starts
48244
+ * editing. The field is there at all times too — inert and transparent until
48245
+ * edition starts — so appearing costs no mount.
48246
+ *
48247
+ * Being absolute, the field lands on the closest positioned ancestor. What it
48248
+ * takes for it to land ON the text and not merely near it is the contract
48249
+ * described on `Editable` below.
48250
+ */
48251
+ const css$H = /* css */`
48026
48252
  .navi_editable_wrapper {
48027
48253
  --inset-top: 0px;
48028
48254
  --inset-right: 0px;
@@ -48034,12 +48260,30 @@ const css$I = /* css */`
48034
48260
  right: var(--inset-right);
48035
48261
  bottom: var(--inset-bottom);
48036
48262
  left: var(--inset-left);
48263
+ font-size: inherit;
48264
+ font-family: inherit;
48265
+ line-height: inherit;
48266
+ border-radius: inherit;
48037
48267
  opacity: 0;
48038
48268
  pointer-events: none;
48039
48269
 
48040
- input {
48270
+ /* The field takes the place of the text, so the text is what decides how
48271
+ it is drawn — a control's own font (its size and family come from
48272
+ --navi-control-*) would make the value change size the moment one edits
48273
+ it. Unlayered, so it wins over the control's own sheet. */
48274
+ .navi_input {
48275
+ font-size: inherit;
48276
+ font-family: inherit;
48277
+ border-radius: inherit;
48278
+ }
48279
+ input,
48280
+ textarea {
48041
48281
  font-weight: inherit;
48282
+ font-size: inherit;
48283
+ font-family: inherit;
48042
48284
  text-align: inherit;
48285
+ line-height: inherit;
48286
+ border-radius: inherit;
48043
48287
  }
48044
48288
 
48045
48289
  &[data-editing] {
@@ -48048,6 +48292,15 @@ const css$I = /* css */`
48048
48292
  }
48049
48293
  }
48050
48294
  `;
48295
+
48296
+ /**
48297
+ * The state of "this thing is being edited", to hand to `Editable`.
48298
+ *
48299
+ * `startEditing` takes the event that asked for edition; `Editable` reads
48300
+ * `event.detail.initialValue` from it and types it into the field as a first
48301
+ * keystroke, which is how a table starts editing on a letter key instead of
48302
+ * losing that letter.
48303
+ */
48051
48304
  const useEditionController = () => {
48052
48305
  const [editing, editingSetter] = useState(null);
48053
48306
  const startEditing = useCallback(event => {
@@ -48070,8 +48323,108 @@ const useEditionController = () => {
48070
48323
  editionJustEnded
48071
48324
  };
48072
48325
  };
48326
+
48327
+ /**
48328
+ * The value written in place, with the field that edits it drawn exactly over
48329
+ * it: same first character, same font, same box.
48330
+ *
48331
+ * `Editable` renders the text (`children`, or `value` in a `<span>`) and, next
48332
+ * to it, an absolutely positioned field. Landing that field on the text is a
48333
+ * contract with the element around it:
48334
+ *
48335
+ * 1. **That element must be positioned** (`position: relative`) and must be the
48336
+ * one drawing the text. The field is absolute: it goes to the closest
48337
+ * positioned ancestor, so `Editable` belongs where the text is, never lifted
48338
+ * out of it.
48339
+ * 2. **The text must start at the same place in both.** The field is pulled out
48340
+ * over the parent's border (its insets are the parent's border sizes,
48341
+ * negated) so it covers the parent's border box; from there, the field's own
48342
+ * border + padding must add up to the parent's border + padding, or the
48343
+ * first character moves by the difference. The way to say it is to repeat
48344
+ * both on `Editable` — `paddingLeft`, `paddingTop`, `borderWidth`, plus
48345
+ * `borderRadius` so the corners fall on the parent's. A parent that paints
48346
+ * no border of its own (a table cell drawing its grid with a pseudo-element)
48347
+ * needs `borderWidth="0"` here, otherwise the control's default border eats
48348
+ * into the text.
48349
+ * 3. **The field must fill that box**: `width="100%"` (or `expandX`), and
48350
+ * `height="100%"` when the text sits in a box taller than a line. Horizontal
48351
+ * placement comes from the inherited `text-align`, and an inherited
48352
+ * alignment only means something in a box spanning the whole width.
48353
+ *
48354
+ * The font is not part of the contract: family, size, weight, line height and
48355
+ * alignment are inherited from the text, so the value keeps its own appearance
48356
+ * while being edited. Declaring one here is how a value changes size the moment
48357
+ * one edits it.
48358
+ *
48359
+ * For reference, both ways of holding up that end: `Table`'s editable cells do
48360
+ * it in CSS (the `[data-editing] input` rules in `control/table/table_css.js`),
48361
+ * the demo `control/demos/action/8_editable_demo.html` does it with props.
48362
+ *
48363
+ * @type {import("ignore:preact").FunctionComponent<{
48364
+ * children?: import("ignore:preact").ComponentChildren,
48365
+ * action: Function | object,
48366
+ * editing?: { event?: Event } | null,
48367
+ * onEditEnd?: (detail: { success?: boolean, cancelled?: boolean, event: Event }) => void,
48368
+ * name?: string,
48369
+ * value?: any,
48370
+ * valueSignal?: import("@preact/signals").Signal,
48371
+ * constraints?: any,
48372
+ * type?: string,
48373
+ * multiline?: boolean,
48374
+ * minRows?: number,
48375
+ * maxRows?: number,
48376
+ * required?: boolean,
48377
+ * readOnly?: boolean,
48378
+ * min?: number | string,
48379
+ * max?: number | string,
48380
+ * step?: number | string,
48381
+ * minLength?: number,
48382
+ * maxLength?: number,
48383
+ * pattern?: string,
48384
+ * wrapperProps?: object,
48385
+ * autoFocusSelect?: boolean,
48386
+ * width?: string | number,
48387
+ * height?: string | number,
48388
+ * [key: string]: any,
48389
+ * }>}
48390
+ *
48391
+ * @param {Function|object} action
48392
+ * What saves the value; it receives what was typed. Edition ends on its
48393
+ * success, and a failure keeps the field open with what the user wrote.
48394
+ *
48395
+ * @param {{ event?: Event }|null} [editing]
48396
+ * Whether the field is open, and what asked for it — the object
48397
+ * `useEditionController` holds. Anything falsy leaves the text alone.
48398
+ *
48399
+ * @param {Function} [onEditEnd]
48400
+ * Called once edition is over, whichever way it went: `success` after the
48401
+ * action, `cancelled` on Escape, on a blur that changed nothing, or on a blur
48402
+ * leaving an invalid value.
48403
+ *
48404
+ * @param {import("@preact/signals").Signal} [valueSignal]
48405
+ * The value as a signal, when what is typed must be readable live by the rest
48406
+ * of the page. It is written as the user types and restored to what it was on
48407
+ * cancel; `value` alone is enough for the ordinary case.
48408
+ *
48409
+ * @param {boolean} [multiline]
48410
+ * Swaps the field for a `Textarea` — the same edition in place, for a value
48411
+ * written on several lines (a message, a description). Enter then makes a
48412
+ * line instead of validating: what is typed is saved when the field is left,
48413
+ * and Escape gives up on it. `minRows` / `maxRows` bound its height; the props
48414
+ * only an `<input>` understands (`type`, `min`, `max`, `step`, `pattern`) are
48415
+ * dropped.
48416
+ *
48417
+ * @param {boolean} [autoFocusSelect=true]
48418
+ * Selects the whole value when the field opens, so typing replaces it. Turn
48419
+ * it off to have the caret land in a value one comes to amend.
48420
+ *
48421
+ * @param {object} [wrapperProps]
48422
+ * Props for the absolutely positioned wrapper around the field, for the rare
48423
+ * case where its own box needs a say (a stacking context, an inset to nudge).
48424
+ * Every other unknown prop goes to the field itself, styling props included.
48425
+ */
48073
48426
  const Editable = props => {
48074
- import.meta.css = [css$I, "@jsenv/navi/src/control/edition/editable.jsx"];
48427
+ import.meta.css = [css$H, "@jsenv/navi/src/control/edition/editable.jsx"];
48075
48428
  let {
48076
48429
  children,
48077
48430
  action,
@@ -48082,6 +48435,9 @@ const Editable = props => {
48082
48435
  onEditEnd,
48083
48436
  constraints,
48084
48437
  type,
48438
+ multiline,
48439
+ minRows,
48440
+ maxRows,
48085
48441
  required,
48086
48442
  readOnly,
48087
48443
  min,
@@ -48127,29 +48483,24 @@ const Editable = props => {
48127
48483
  }
48128
48484
  }
48129
48485
  }, [editing]);
48130
- const input = jsx(Input, {
48131
- ref: ref,
48486
+ const controlProps = {
48487
+ ref,
48132
48488
  ...rest,
48133
- type: type,
48134
- name: name,
48135
- value: value,
48136
- valueSignal: valueSignal,
48489
+ name,
48490
+ value,
48491
+ valueSignal,
48137
48492
  autoFocus: editing,
48138
48493
  autoFocusVisible: true,
48139
- autoFocusSelect: autoFocusSelect,
48494
+ autoFocusSelect,
48140
48495
  cancelOnEscape: true,
48141
48496
  cancelOnBlurInvalid: true,
48142
- constraints: constraints,
48143
- required: required,
48144
- readOnly: readOnly,
48145
- min: min,
48146
- max: max,
48147
- step: step,
48148
- minLength: minLength,
48149
- maxLength: maxLength,
48150
- pattern: pattern,
48151
- width: width,
48152
- height: height,
48497
+ constraints,
48498
+ required,
48499
+ readOnly,
48500
+ minLength,
48501
+ maxLength,
48502
+ width,
48503
+ height,
48153
48504
  onCancel: e => {
48154
48505
  if (valueSignal) {
48155
48506
  valueSignal.value = valueWhenEditStartRef.current;
@@ -48186,6 +48537,21 @@ const Editable = props => {
48186
48537
  event: e
48187
48538
  });
48188
48539
  }
48540
+ };
48541
+ // What is typed on several lines goes into a textarea; the props that only
48542
+ // mean something to an <input> (its type and the bounds that come with it)
48543
+ // stay on that side.
48544
+ const control = multiline ? jsx(Textarea, {
48545
+ ...controlProps,
48546
+ minRows: minRows,
48547
+ maxRows: maxRows
48548
+ }) : jsx(Input, {
48549
+ ...controlProps,
48550
+ type: type,
48551
+ min: min,
48552
+ max: max,
48553
+ step: step,
48554
+ pattern: pattern
48189
48555
  });
48190
48556
  const wrapperRef = useRef();
48191
48557
  useLayoutEffect(() => {
@@ -48213,7 +48579,7 @@ const Editable = props => {
48213
48579
  // - is ignored by screen readers
48214
48580
  inert: editing ? undefined : "",
48215
48581
  "data-editing": editing ? "" : undefined,
48216
- children: input
48582
+ children: control
48217
48583
  })]
48218
48584
  });
48219
48585
  };
@@ -48620,7 +48986,7 @@ installImportMetaCssBuild(import.meta);/**
48620
48986
  * meet are drawn once instead of twice, and only the outer corners stay
48621
48987
  * rounded. See docs/control_group.md.
48622
48988
  */
48623
- const css$H = /* css */`
48989
+ const css$G = /* css */`
48624
48990
  .navi_group {
48625
48991
  --group-border-width: var(--navi-control-border-width);
48626
48992
 
@@ -48755,7 +49121,7 @@ const Group = ({
48755
49121
  vertical = row,
48756
49122
  ...props
48757
49123
  }) => {
48758
- import.meta.css = [css$H, "@jsenv/navi/src/control/group.jsx"];
49124
+ import.meta.css = [css$G, "@jsenv/navi/src/control/group.jsx"];
48759
49125
  return jsx(Box, {
48760
49126
  baseClassName: "navi_group",
48761
49127
  "data-vertical": vertical ? "" : undefined
@@ -48954,7 +49320,7 @@ installImportMetaCssBuild(import.meta);/**
48954
49320
  * So: nothing scrollable between the cap and the slides (a shared [data-body]
48955
49321
  * around them IS a scroller, see box.jsx), and `overflow="auto"` on each Slide.
48956
49322
  */
48957
- const css$G = /* css */`
49323
+ const css$F = /* css */`
48958
49324
  /* Where the picture stands relative to the slide that is current, in boxes
48959
49325
  (see paintTravelProgress). Declared, so that it is a NUMBER the browser can
48960
49326
  interpolate: the trait an indicator draws has to travel with the slides,
@@ -49448,7 +49814,7 @@ const SlideContainer = ({
49448
49814
  children,
49449
49815
  ...rest
49450
49816
  }) => {
49451
- import.meta.css = [css$G, "@jsenv/navi/src/layout/slide_container.jsx"];
49817
+ import.meta.css = [css$F, "@jsenv/navi/src/layout/slide_container.jsx"];
49452
49818
  const debugFocus = useDebugFocus();
49453
49819
  const trackRef = useRef();
49454
49820
  // The box itself: it is what takes the keyboard when what is on screen holds
@@ -53452,7 +53818,7 @@ installImportMetaCssBuild(import.meta);/**
53452
53818
  * reaches the real container.
53453
53819
  */
53454
53820
  let openLocalDialogCount = 0;
53455
- const css$F = /* css */`
53821
+ const css$E = /* css */`
53456
53822
  @layer navi {
53457
53823
  .navi_dialog {
53458
53824
  /* Min gap between the dialog and the edges of its container. Written
@@ -54040,7 +54406,7 @@ const css$F = /* css */`
54040
54406
  * @param {import("ignore:preact").ComponentChildren} props.children
54041
54407
  */
54042
54408
  const Dialog = props => {
54043
- import.meta.css = [css$F, "@jsenv/navi/src/layout/dialog.jsx"];
54409
+ import.meta.css = [css$E, "@jsenv/navi/src/layout/dialog.jsx"];
54044
54410
  if (props.openController) {
54045
54411
  return jsx(ControlledDialog, {
54046
54412
  ...props
@@ -55036,7 +55402,7 @@ installImportMetaCssBuild(import.meta);/**
55036
55402
  * and applied.
55037
55403
  */
55038
55404
  let openLocalPopoverCount = 0;
55039
- const css$E = /* css */`
55405
+ const css$D = /* css */`
55040
55406
  @layer navi {
55041
55407
  .navi_popover {
55042
55408
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -55496,7 +55862,7 @@ const css$E = /* css */`
55496
55862
  * @param {import("ignore:preact").ComponentChildren} props.children
55497
55863
  */
55498
55864
  const Popover = props => {
55499
- import.meta.css = [css$E, "@jsenv/navi/src/layout/popover.jsx"];
55865
+ import.meta.css = [css$D, "@jsenv/navi/src/layout/popover.jsx"];
55500
55866
  if (props.openController) {
55501
55867
  return jsx(ControlledPopover, {
55502
55868
  ...props
@@ -56535,7 +56901,7 @@ installImportMetaCssBuild(import.meta);/**
56535
56901
  * and only under its own `sizeFromAnchor`) pass through untouched via
56536
56902
  * `...rest` to whichever of Popover/Dialog actually renders.
56537
56903
  */
56538
- const css$D = /* css */`
56904
+ const css$C = /* css */`
56539
56905
  @layer navi {
56540
56906
  .navi_popup {
56541
56907
  --popup-border-radius: var(--navi-popup-border-radius);
@@ -56652,7 +57018,7 @@ const css$D = /* css */`
56652
57018
  * @param {import("ignore:preact").ComponentChildren} props.children
56653
57019
  */
56654
57020
  const Popup = props => {
56655
- import.meta.css = [css$D, "@jsenv/navi/src/layout/popup.jsx"];
57021
+ import.meta.css = [css$C, "@jsenv/navi/src/layout/popup.jsx"];
56656
57022
  const {
56657
57023
  mode: modeProp,
56658
57024
  maxWidth,
@@ -56713,7 +57079,7 @@ const Popup = props => {
56713
57079
  });
56714
57080
  };
56715
57081
 
56716
- installImportMetaCssBuild(import.meta);const css$C = /* css */`
57082
+ installImportMetaCssBuild(import.meta);const css$B = /* css */`
56717
57083
  .navi_picker {
56718
57084
  /* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
56719
57085
  overflow... are already handled correctly by Popup/Popover/Dialog
@@ -56867,7 +57233,7 @@ installImportMetaCssBuild(import.meta);const css$C = /* css */`
56867
57233
  }
56868
57234
  `;
56869
57235
  const PickerCustomResolver = props => {
56870
- import.meta.css = [css$C, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
57236
+ import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
56871
57237
  if (props.children === undefined) {
56872
57238
  return jsx(PickerNative, {
56873
57239
  ...props
@@ -57727,7 +58093,7 @@ installImportMetaCssBuild(import.meta);/**
57727
58093
  * looking again. Anything else that closes the popup (the cancel button,
57728
58094
  * Escape, a click outside) is no.
57729
58095
  */
57730
- const css$B = /* css */`
58096
+ const css$A = /* css */`
57731
58097
  .navi_picker_confirm_body {
57732
58098
  display: flex;
57733
58099
  min-width: 180px;
@@ -57744,7 +58110,7 @@ const css$B = /* css */`
57744
58110
  }
57745
58111
  `;
57746
58112
  const PickerConfirmResolver = props => {
57747
- import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_confirm.jsx"];
58113
+ import.meta.css = [css$A, "@jsenv/navi/src/control/picker/picker_confirm.jsx"];
57748
58114
  const Next = useNextResolver();
57749
58115
  if (props.type !== "confirm") {
57750
58116
  return jsx(Next, {
@@ -58052,7 +58418,7 @@ const LoadingIndicator = ({
58052
58418
  });
58053
58419
  };
58054
58420
 
58055
- installImportMetaCssBuild(import.meta);const css$A = /* css */`
58421
+ installImportMetaCssBuild(import.meta);const css$z = /* css */`
58056
58422
  @layer navi {
58057
58423
  .navi_separator {
58058
58424
  --size: 1px;
@@ -58130,7 +58496,7 @@ const Separator = ({
58130
58496
  style,
58131
58497
  ...props
58132
58498
  }) => {
58133
- import.meta.css = [css$A, "@jsenv/navi/src/layout/separator.jsx"];
58499
+ import.meta.css = [css$z, "@jsenv/navi/src/layout/separator.jsx"];
58134
58500
  return jsx(Box, {
58135
58501
  as: vertical ? "span" : "hr",
58136
58502
  ...props,
@@ -58623,7 +58989,7 @@ const ListItemFooter = props => {
58623
58989
  });
58624
58990
  };
58625
58991
 
58626
- installImportMetaCssBuild(import.meta);const css$z = /* css */`
58992
+ installImportMetaCssBuild(import.meta);const css$y = /* css */`
58627
58993
  @layer navi {
58628
58994
  .navi_list_container[navi-selectable] {
58629
58995
  /* Focus outline */
@@ -58676,6 +59042,10 @@ installImportMetaCssBuild(import.meta);const css$z = /* css */`
58676
59042
  position: relative;
58677
59043
  font-size: var(--navi-control-font-size);
58678
59044
  font-family: var(--navi-control-font-family);
59045
+ /* A selectable item is a control: it stands in a row with pickers and
59046
+ inputs (a horizontal list of choices), so it takes the control line
59047
+ with the control font — the same number of pixels tall as them. */
59048
+ line-height: var(--navi-control-line-height);
58679
59049
  -webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
58680
59050
  }
58681
59051
  }
@@ -58838,7 +59208,7 @@ const ListSelectableResolver = props => {
58838
59208
  };
58839
59209
  const ListSelectable = props => {
58840
59210
  const Next = useNextResolver();
58841
- import.meta.css = [css$z, "@jsenv/navi/src/control/list/list_selectable.jsx"];
59211
+ import.meta.css = [css$y, "@jsenv/navi/src/control/list/list_selectable.jsx"];
58842
59212
  // we allow ourselves to auto-generate a name
58843
59213
  const defaultName = useId();
58844
59214
  props.name = props.name || `listbox_${defaultName}`;
@@ -59481,7 +59851,7 @@ const ListVirtualContext = createContext(null);
59481
59851
  // that returning a component of one's own — instead of a bare <List.Item> —
59482
59852
  // works the same way.
59483
59853
  const ListRowContext = createContext(null);
59484
- const css$y = /* css */`
59854
+ const css$x = /* css */`
59485
59855
  @layer navi {
59486
59856
  .navi_list_container {
59487
59857
  --list-outline-width: 1px;
@@ -60030,7 +60400,7 @@ const css$y = /* css */`
60030
60400
  gap: 8px;
60031
60401
  color: light-dark(#b91c1c, #fca5a5);
60032
60402
  font-size: 0.9em;
60033
- line-height: 1.4;
60403
+ line-height: var(--navi-line-height);
60034
60404
  background: light-dark(#fef2f2, rgba(127, 29, 29, 0.25));
60035
60405
  border: 1px solid light-dark(#fecaca, rgba(248, 113, 113, 0.4));
60036
60406
  border-radius: 6px;
@@ -60038,7 +60408,7 @@ const css$y = /* css */`
60038
60408
  .navi_list_error_icon {
60039
60409
  flex: none;
60040
60410
  font-size: 1em;
60041
- line-height: 1.4;
60411
+ line-height: var(--navi-line-height);
60042
60412
  }
60043
60413
  /* Same rule as [data-scrollable] in box.jsx, said again for this scroller:
60044
60414
  what an item holds IS against the edge of the scroll container — the list
@@ -60180,7 +60550,7 @@ const css$y = /* css */`
60180
60550
  }
60181
60551
  `;
60182
60552
  const ListUI = props => {
60183
- import.meta.css = [css$y, "@jsenv/navi/src/control/list/list.jsx"];
60553
+ import.meta.css = [css$x, "@jsenv/navi/src/control/list/list.jsx"];
60184
60554
  const {
60185
60555
  ref,
60186
60556
  renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
@@ -64021,7 +64391,7 @@ const createBadgeRegistry = () => {
64021
64391
  };
64022
64392
  };
64023
64393
 
64024
- installImportMetaCssBuild(import.meta);const css$x = /* css */`
64394
+ installImportMetaCssBuild(import.meta);const css$w = /* css */`
64025
64395
  @layer navi {
64026
64396
  }
64027
64397
  .navi_badge {
@@ -64153,7 +64523,7 @@ const BadgeUI = ({
64153
64523
  className,
64154
64524
  ...props
64155
64525
  }) => {
64156
- import.meta.css = [css$x, "@jsenv/navi/src/text/badge.jsx"];
64526
+ import.meta.css = [css$w, "@jsenv/navi/src/text/badge.jsx"];
64157
64527
  const defaultRef = useRef();
64158
64528
  props.ref = props.ref || defaultRef;
64159
64529
  const {
@@ -64221,7 +64591,7 @@ const BadgeButtonUI = props => {
64221
64591
  };
64222
64592
  Badge.Button = BadgeButton;
64223
64593
 
64224
- installImportMetaCssBuild(import.meta);const css$w = /* css */`
64594
+ installImportMetaCssBuild(import.meta);const css$v = /* css */`
64225
64595
  @layer navi {
64226
64596
  }
64227
64597
  .navi_badge_list {
@@ -64362,7 +64732,7 @@ const useBadgeRegistry = (children, enabled) => {
64362
64732
  * component grants (a <Picker>, see max_lines_context.js).
64363
64733
  */
64364
64734
  const BadgeList = props => {
64365
- import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
64735
+ import.meta.css = [css$v, "@jsenv/navi/src/text/badge_list.jsx"];
64366
64736
  const {
64367
64737
  maxLines,
64368
64738
  max,
@@ -64705,7 +65075,7 @@ const BadgeListContent = ({
64705
65075
  });
64706
65076
  };
64707
65077
 
64708
- installImportMetaCssBuild(import.meta);const css$v = /* css */`
65078
+ installImportMetaCssBuild(import.meta);const css$u = /* css */`
64709
65079
  .navi_color {
64710
65080
  display: block;
64711
65081
  aspect-ratio: 1/1;
@@ -64736,7 +65106,7 @@ const Color = ({
64736
65106
  children,
64737
65107
  ...rest
64738
65108
  }) => {
64739
- import.meta.css = [css$v, "@jsenv/navi/src/text/color.jsx"];
65109
+ import.meta.css = [css$u, "@jsenv/navi/src/text/color.jsx"];
64740
65110
  const color = children || undefined;
64741
65111
  return jsx(Box, {
64742
65112
  as: "span",
@@ -65053,7 +65423,7 @@ const PickerDateUI = props => {
65053
65423
  if (!placeholder) {
65054
65424
  return jsx(Time, {
65055
65425
  type: "date",
65056
- color: "var(--picker-placeholder-color",
65426
+ color: "var(--picker-placeholder-color)",
65057
65427
  capitalize: true,
65058
65428
  ...props
65059
65429
  });
@@ -65085,7 +65455,7 @@ const PickerMonthUI = props => {
65085
65455
  if (!placeholder) {
65086
65456
  return jsx(Time, {
65087
65457
  type: "month",
65088
- color: "var(--picker-placeholder-color",
65458
+ color: "var(--picker-placeholder-color)",
65089
65459
  ...props
65090
65460
  });
65091
65461
  }
@@ -65116,7 +65486,7 @@ const PickerWeekUI = props => {
65116
65486
  if (!placeholder) {
65117
65487
  return jsx(Time, {
65118
65488
  type: "week",
65119
- color: "var(--picker-placeholder-color",
65489
+ color: "var(--picker-placeholder-color)",
65120
65490
  ...props
65121
65491
  });
65122
65492
  }
@@ -65147,7 +65517,7 @@ const PickerTimeUI = props => {
65147
65517
  if (!placeholder) {
65148
65518
  return jsx(Time, {
65149
65519
  type: "time",
65150
- color: "var(--picker-placeholder-color",
65520
+ color: "var(--picker-placeholder-color)",
65151
65521
  ...props
65152
65522
  });
65153
65523
  }
@@ -65178,7 +65548,7 @@ const PickerDurationUI = props => {
65178
65548
  if (!placeholder) {
65179
65549
  return jsx(Time, {
65180
65550
  type: "time",
65181
- color: "var(--picker-placeholder-color",
65551
+ color: "var(--picker-placeholder-color)",
65182
65552
  ...props
65183
65553
  });
65184
65554
  }
@@ -65208,7 +65578,7 @@ const PickerDatetimeUI = props => {
65208
65578
  if (!placeholder) {
65209
65579
  return jsx(Time, {
65210
65580
  type: "datetime",
65211
- color: "var(--picker-placeholder-color",
65581
+ color: "var(--picker-placeholder-color)",
65212
65582
  ...props
65213
65583
  });
65214
65584
  }
@@ -65243,7 +65613,7 @@ const PickerFileUI = () => {
65243
65613
  return String(value);
65244
65614
  };
65245
65615
 
65246
- installImportMetaCssBuild(import.meta);const css$u = /* css */`
65616
+ installImportMetaCssBuild(import.meta);const css$t = /* css */`
65247
65617
  @layer navi {
65248
65618
  .navi_picker {
65249
65619
  --picker-border-radius: var(--navi-control-border-radius);
@@ -65261,11 +65631,8 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
65261
65631
  --picker-border-color: var(--navi-control-border-color);
65262
65632
  --picker-background-color: white;
65263
65633
  --picker-color: currentColor;
65264
- --picker-placeholder-color: color-mix(
65265
- in srgb,
65266
- currentColor 60%,
65267
- transparent
65268
- );
65634
+ --picker-placeholder-color: var(--navi-placeholder-color);
65635
+ --picker-placeholder-font-style: var(--navi-placeholder-font-style);
65269
65636
  --picker-color-dimmed: color-mix(in srgb, currentColor 60%, transparent);
65270
65637
  /* Hover */
65271
65638
  --picker-border-color-hover: color-mix(
@@ -65364,6 +65731,11 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
65364
65731
  font-size: var(--picker-font-size);
65365
65732
  font-family: var(--picker-font-family);
65366
65733
  text-align: inherit;
65734
+ /* The control line, like the control font: a picker is drawn by hand next
65735
+ to inputs that sit on it, and its box is sized in lh — the same number
65736
+ of pixels, or a picker and an input in one row are not the same height
65737
+ and their text is not on the same row. */
65738
+ line-height: var(--navi-control-line-height);
65367
65739
  /* The frame is drawn by the box, but its radius is declared here, on the
65368
65740
  control root, like every other navi control does — so anything styling
65369
65741
  the picker from the outside (a Group squaring the corners it joins) has
@@ -65448,6 +65820,7 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
65448
65820
 
65449
65821
  &[navi-placeholder] {
65450
65822
  color: var(--picker-placeholder-color);
65823
+ font-style: var(--picker-placeholder-font-style);
65451
65824
  }
65452
65825
 
65453
65826
  /* A <BadgeList> caps its own rows: it renders the badges that fit and a
@@ -65758,7 +66131,7 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
65758
66131
  }
65759
66132
  `;
65760
66133
  const PickerButton = props => {
65761
- import.meta.css = [css$u, "@jsenv/navi/src/control/picker/picker.jsx"];
66134
+ import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker.jsx"];
65762
66135
  if (typeof props.maxLines === "string") {
65763
66136
  props.maxLines = parseInt(props.maxLines);
65764
66137
  }
@@ -66328,7 +66701,7 @@ installImportMetaCssBuild(import.meta);/**
66328
66701
  * refuse it on purpose, which is what keeps the focus where the travel happens
66329
66702
  * instead of moving it into a slide that is about to leave.
66330
66703
  */
66331
- const css$t = /* css */`
66704
+ const css$s = /* css */`
66332
66705
  @layer navi {
66333
66706
  .navi_picker_spin {
66334
66707
  /* A picker one steps through is still a picker: what themes every picker
@@ -66843,7 +67216,7 @@ const Spin = ({
66843
67216
  nextLabel,
66844
67217
  ...rest
66845
67218
  }) => {
66846
- import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
67219
+ import.meta.css = [css$s, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
66847
67220
  const id = useId();
66848
67221
  // What the group around it says, when there is one: how big the whole thing
66849
67222
  // is written is said once, on the group, and every spin in it follows.
@@ -67379,7 +67752,7 @@ const renderValueDefault = value => String(value ?? "");
67379
67752
  * are passed on to the spins sitting in them.
67380
67753
  */
67381
67754
  const SpinGroup = props => {
67382
- import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
67755
+ import.meta.css = [css$s, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
67383
67756
  const {
67384
67757
  size
67385
67758
  } = props;
@@ -67892,7 +68265,7 @@ const TimeRangeSpin = ({
67892
68265
  };
67893
68266
 
67894
68267
  installImportMetaCssBuild(import.meta);// TOFIX: select in data then reset, it reset to red/blue instead of red/blue/green
67895
- const css$s = /* css */`
68268
+ const css$r = /* css */`
67896
68269
  .navi_checkbox_group {
67897
68270
  border-style: solid;
67898
68271
 
@@ -67933,7 +68306,7 @@ const CheckboxGroup = props => {
67933
68306
  return checkboxGroup;
67934
68307
  };
67935
68308
  const CheckboxGroupInterface = props => {
67936
- import.meta.css = [css$s, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
68309
+ import.meta.css = [css$r, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
67937
68310
  const {
67938
68311
  ref
67939
68312
  } = props;
@@ -67964,291 +68337,6 @@ const CheckboxGroupInterface = props => {
67964
68337
  });
67965
68338
  };
67966
68339
 
67967
- installImportMetaCssBuild(import.meta);/**
67968
- * Multiline text control that grows with what is typed.
67969
- *
67970
- * Autosize is native: `field-sizing: content` lets the browser size the
67971
- * textarea from its value — no hidden mirror textarea to measure against (the
67972
- * technique libraries used before the property existed). `minRows`/`maxRows`
67973
- * become min/max heights in `lh` units on top of it; past `maxRows` the
67974
- * content scrolls.
67975
- *
67976
- * TextareaCharCount is the counter that goes with it, and the caller places
67977
- * it: under the box, in a form footer, next to a label — fed with the same
67978
- * value/signal as the textarea. The textarea draws no counter of its own.
67979
- *
67980
- * Styled as a `.navi_input` box (border, background, focus ring, readonly and
67981
- * disabled fades, variants): one look for everything one types into. The
67982
- * shared sheet is registered here too — a page may render a Textarea without
67983
- * any Input.
67984
- */
67985
- const css$r = /* css */`
67986
- .navi_input.navi_textarea {
67987
- .navi_control_input {
67988
- min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
67989
- /* Above maxRows the box stops growing and the content scrolls. The
67990
- 99999 fallback means "no cap" without needing a conditional rule. */
67991
- max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
67992
- field-sizing: content;
67993
- /* Explicit, never normal, for two independent reasons.
67994
- minRows/maxRows are lengths in lh, and with line-height normal the lh
67995
- unit resolves to a theoretical value that does not match the real
67996
- rendered line — the box then jumps by a few pixels the moment the first
67997
- character replaces the theory with a real line.
67998
- And a line box under "normal" takes the height of the tallest font it
67999
- holds, so the one line carrying an emoji stands taller than the ones
68000
- around it — here, where the text is typed and no glyph can be wrapped
68001
- the way emojiAsIcon wraps one, the line height is the only lever.
68002
- 1.5 is also tall enough to contain an emoji's own box, so it is not
68003
- clipped either; a tighter value would keep the rows even and cut the
68004
- glyph. See docs/typography.md. */
68005
- line-height: 1.5;
68006
- /* The control grows itself; resizable below hands the handle back. */
68007
- resize: none;
68008
- overflow: auto;
68009
- /* A placeholder must be readable in full before anything is typed: a
68010
- field that opens already scrolled reads as a field that already has
68011
- text in it. Its wrapped height is measured (see usePlaceholderHeight)
68012
- because it only exists once laid out, and it only raises the floor
68013
- while the placeholder is what is being shown — what is typed sizes the
68014
- box on its own. */
68015
- &:placeholder-shown {
68016
- min-height: max(
68017
- calc(var(--textarea-min-rows, 1.5) * 1lh),
68018
- var(--x-textarea-placeholder-height, 0px)
68019
- );
68020
- }
68021
- }
68022
- &[data-resizable] .navi_control_input {
68023
- height: calc(var(--textarea-min-rows, 1.5) * 1lh);
68024
- /* The two are exclusive: with field-sizing content the browser removes
68025
- the resize handle (the size follows the content, there is nothing to
68026
- drag). resizable means the hand takes over — fixed sizing, starting
68027
- at minRows, and the drag writes its own inline height from there. */
68028
- field-sizing: fixed;
68029
- resize: vertical;
68030
- }
68031
- }
68032
- .navi_textarea_char_count {
68033
- color: color-mix(in srgb, currentColor 60%, transparent);
68034
- font-size: 0.75em;
68035
- user-select: none;
68036
- }
68037
- `;
68038
-
68039
- /**
68040
- * @type {import("ignore:preact").FunctionComponent<{
68041
- * value?: string,
68042
- * defaultValue?: string,
68043
- * signal?: import("@preact/signals").Signal<string>,
68044
- * name?: string,
68045
- * minRows?: number,
68046
- * maxRows?: number,
68047
- * resizable?: boolean,
68048
- * maxLength?: number,
68049
- * width?: string,
68050
- * [key: string]: any,
68051
- * }>}
68052
- * @param {number} [minRows=1.5] Height the empty control starts at, in lines.
68053
- * The default shows half of a second line: enough to read "multiline" at a
68054
- * glance without the height of a full extra row.
68055
- * @param {number} [maxRows] Lines after which the control stops growing and
68056
- * scrolls instead. Without it the control grows with its content.
68057
- * @param {boolean} [resizable] Give the browser's vertical resize handle back.
68058
- * An exchange, not an addition: the hand takes over from the automatic
68059
- * growth, so the control stops following what is typed and stays at the
68060
- * height it was last dragged to (starting at `minRows`).
68061
- * @param {number} [maxLength] The character limit, validated at submit. Pair
68062
- * with `maxLengthGuard` to block typing past it, and render a
68063
- * TextareaCharCount to show it.
68064
- * @param {string} [width="35ch"] The control's width. Fixed on purpose: with
68065
- * field-sizing the width would otherwise follow the longest line, and a box
68066
- * that widens while one types is a box one chases.
68067
- */
68068
- const Textarea = ({
68069
- // Destructured, never deleted off the props object: Preact reuses the same
68070
- // props object when an internal state update re-renders the component, so a
68071
- // delete would make these props vanish from the second render on (the box
68072
- // then jumps back to the default minRows at the first keystroke).
68073
- minRows = 1.5,
68074
- maxRows,
68075
- resizable,
68076
- width = "35ch",
68077
- ...props
68078
- }) => {
68079
- import.meta.css = [inputCss + css$r, "@jsenv/navi/src/control/input/textarea.jsx"];
68080
- const defaultRef = useRef(null);
68081
- props.ref = props.ref || defaultRef;
68082
- usePlaceholderHeight(props.ref, props.placeholder);
68083
- const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
68084
- controlType: "input"
68085
- });
68086
- const {
68087
- basePseudoState,
68088
- children
68089
- } = hostProps;
68090
- // Children go through ControlChildrenWrapper below; inside the <textarea>
68091
- // element they would become its text content.
68092
- delete hostProps.children;
68093
- const loading = basePseudoState[":-navi-loading"];
68094
- delete rootProps.width;
68095
- hostProps.width = width;
68096
- return jsxs(Box, {
68097
- as: "span",
68098
- inline: true,
68099
- flex: true,
68100
- baseClassName: "navi_input",
68101
- className: "navi_textarea",
68102
- ...rootProps,
68103
- basePseudoState: basePseudoState,
68104
- "data-resizable": resizable ? "" : undefined,
68105
- styleCSSVars: InputStyleCSSVars,
68106
- pseudoStateSelector: ".navi_control_input",
68107
- pseudoClasses: InputPseudoClasses,
68108
- pseudoElements: InputPseudoElements,
68109
- "data-callout-anchor": ".navi_control_input",
68110
- style: {
68111
- "--textarea-min-rows": minRows,
68112
- "--textarea-max-rows": maxRows,
68113
- ...rootProps.style
68114
- },
68115
- children: [jsx(LoadingOutline, {
68116
- loading: loading,
68117
- color: "var(--loader-color)",
68118
- inset: -1
68119
- }), jsx(RealTextarea, {
68120
- ...hostProps
68121
- }), jsx(ControlChildrenWrapper, {
68122
- ...childrenWrapperProps,
68123
- children: children
68124
- })]
68125
- });
68126
- };
68127
-
68128
- /**
68129
- * The counter that goes with a Textarea: "50/200" — how many characters are
68130
- * typed over how many the limit allows, the way Material writes it (just the
68131
- * count when there is no `maxLength`). Where it goes is the caller's call,
68132
- * which is why it is a separate component rather than something the textarea
68133
- * draws: put it under the box, in a form footer, next to a label, and feed it
68134
- * the same value or signal as the textarea.
68135
- *
68136
- * @type {import("ignore:preact").FunctionComponent<{
68137
- * value?: string,
68138
- * signal?: import("@preact/signals").Signal<string>,
68139
- * maxLength?: number,
68140
- * [key: string]: any,
68141
- * }>}
68142
- * @param {string} [value] The text being counted. Say `signal` instead for a
68143
- * two-way bound textarea: reading it here subscribes the count to it.
68144
- * @param {number} [maxLength] The limit, shown after the count ("50/200").
68145
- * Without it the count stands alone.
68146
- */
68147
- const TextareaCharCount = ({
68148
- value,
68149
- signal,
68150
- maxLength,
68151
- ...rest
68152
- }) => {
68153
- import.meta.css = [css$r, "@jsenv/navi/src/control/input/textarea.jsx"];
68154
- const resolvedValue = signal ? signal.value : value;
68155
- const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
68156
- return jsx(Box, {
68157
- as: "span",
68158
- baseClassName: "navi_textarea_char_count",
68159
- ...rest,
68160
- children: maxLength === undefined ? length : `${length}/${maxLength}`
68161
- });
68162
- };
68163
-
68164
- // `field-sizing: content` sizes the box from the value, and an empty field has
68165
- // none — the placeholder is text the browser refuses to make room for. So the
68166
- // height it wraps to is measured and published as --x-textarea-placeholder-height
68167
- // for the CSS above to use as a floor.
68168
- const usePlaceholderHeight = (ref, placeholder) => {
68169
- useLayoutEffect(() => {
68170
- const textareaEl = ref.current;
68171
- if (!placeholder) {
68172
- textareaEl.style.removeProperty("--x-textarea-placeholder-height");
68173
- return null;
68174
- }
68175
- let widthMeasured;
68176
- const measure = () => {
68177
- // What is typed sizes the box itself; the placeholder is not displayed
68178
- // then, and scrollHeight would report the value's height instead.
68179
- if (textareaEl.value !== "") {
68180
- return;
68181
- }
68182
- const {
68183
- paddingTop,
68184
- paddingBottom
68185
- } = getComputedStyle(textareaEl);
68186
- // Cleared before reading: scrollHeight can never report less than the
68187
- // height already applied, so measuring on top of a previous measure could
68188
- // only ever grow the box, never let it shrink back on a wider viewport.
68189
- textareaEl.style.setProperty("--x-textarea-placeholder-height", "0px");
68190
- const contentHeight = textareaEl.scrollHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
68191
- widthMeasured = textareaEl.clientWidth;
68192
- textareaEl.style.setProperty("--x-textarea-placeholder-height", `${contentHeight}px`);
68193
- };
68194
- measure();
68195
- // Measuring writes the variable that sets this element's own height, and
68196
- // mutating layout from inside a resize callback is what makes the browser
68197
- // report "ResizeObserver loop completed with undelivered notifications".
68198
- // So the write waits for the frame that resize produced.
68199
- let measureFrame = null;
68200
- const requestMeasure = () => {
68201
- if (measureFrame !== null) {
68202
- return;
68203
- }
68204
- measureFrame = requestAnimationFrame(() => {
68205
- measureFrame = null;
68206
- measure();
68207
- });
68208
- };
68209
- // The placeholder wraps against the available width, so a new width is a
68210
- // new number of lines. Height changes are ignored: this measure is what
68211
- // causes them, and reacting to them would be reacting to ourselves.
68212
- const resizeObserver = new ResizeObserver(() => {
68213
- if (textareaEl.clientWidth !== widthMeasured) {
68214
- requestMeasure();
68215
- }
68216
- });
68217
- resizeObserver.observe(textareaEl);
68218
- // The width may have changed while the field held a value, when measuring
68219
- // was impossible — emptying it is when the placeholder comes back.
68220
- const onInput = () => {
68221
- if (textareaEl.value === "") {
68222
- measure();
68223
- }
68224
- };
68225
- textareaEl.addEventListener("input", onInput);
68226
- return () => {
68227
- if (measureFrame !== null) {
68228
- cancelAnimationFrame(measureFrame);
68229
- }
68230
- resizeObserver.disconnect();
68231
- textareaEl.removeEventListener("input", onInput);
68232
- };
68233
- }, [placeholder]);
68234
- };
68235
- const RealTextarea = ({
68236
- maxLength,
68237
- ...domProps
68238
- }) => {
68239
- const autoSelectReadOnlyProps = useAutoSelectReadOnly(domProps);
68240
- return jsx(Box, {
68241
- ...domProps,
68242
- as: "textarea",
68243
- baseClassName: "navi_control_input",
68244
- ...autoSelectReadOnlyProps,
68245
- // Native maxLength stays off, like RealInput in input_textual.jsx: the
68246
- // maxLengthGuard handles live blocking, the constraint validates at
68247
- // submit, and navi-max-length keeps the value readable from the DOM.
68248
- "navi-max-length": maxLength
68249
- });
68250
- };
68251
-
68252
68340
  const Unit = ({
68253
68341
  unit,
68254
68342
  plural,
@@ -68988,9 +69076,10 @@ const css$o = /* css */`
68988
69076
  /* Room for the chevron, which sits over the padding rather than beside
68989
69077
  the control — anything beside it would be a click that misses. */
68990
69078
  padding-right: calc(var(--x-padding-right) + 1em);
68991
- /* A form control keeps a line of its own whatever the page is written in,
68992
- and lh units elsewhere in the box are resolved against a real number. */
68993
- line-height: normal;
69079
+ /* Same line as every other field (see --navi-line-height): a select and
69080
+ an input side by side must be the same height, and lh units elsewhere
69081
+ in the box need a real number to resolve against. */
69082
+ line-height: var(--navi-control-line-height);
68994
69083
  /* The closed control is drawn by us so it matches the other fields; the
68995
69084
  list it opens is untouched and stays the system's. */
68996
69085
  appearance: none;
@@ -76302,7 +76391,10 @@ installImportMetaCssBuild(import.meta);const css$c = /* css */`
76302
76391
  --x-color-contrasting: var(--navi-color-white);
76303
76392
  --x-color: var(--color, var(--x-color-contrasting));
76304
76393
  --badge-count-padding-x-default: 0.5em;
76305
- --badge-count-padding-y-default: 0.2em;
76394
+ /* Ink to edge: ~6px above and below the digits at the default size, for
76395
+ ~7px on the sides — a little less than the sides, which is what reads
76396
+ as balanced on a pill; 0.2em read as squashed. */
76397
+ --badge-count-padding-y-default: 0.3em;
76306
76398
 
76307
76399
  /* Each side resolves the most specific value it was given, from the side
76308
76400
  itself down to the axis, the shorthand, then the default. */
@@ -76339,6 +76431,11 @@ installImportMetaCssBuild(import.meta);const css$c = /* css */`
76339
76431
  color: var(--x-color);
76340
76432
  font-size: var(--font-size);
76341
76433
  font-variant-numeric: tabular-nums;
76434
+ /* Its own line, relative to its own font: inherited from a control it
76435
+ would arrive as that control's pixels (a button's 17px), and a badge
76436
+ drawn bigger or smaller than the button's text would get a line box
76437
+ that does not match its glyph — a digit off its circle's center. */
76438
+ line-height: var(--navi-line-height);
76342
76439
  vertical-align: inherit;
76343
76440
 
76344
76441
  &[data-accent-needs-dark-fg] {
@@ -76362,7 +76459,6 @@ installImportMetaCssBuild(import.meta);const css$c = /* css */`
76362
76459
  padding-right: var(--x-badge-count-padding-right);
76363
76460
  padding-bottom: var(--x-badge-count-padding-bottom);
76364
76461
  padding-left: var(--x-badge-count-padding-left);
76365
- line-height: normal;
76366
76462
  background: var(--x-background);
76367
76463
  background-color: var(--x-background-color);
76368
76464
  border-radius: 1em;
@@ -76417,7 +76513,10 @@ installImportMetaCssBuild(import.meta);const css$c = /* css */`
76417
76513
  --x-number-font-size: unset;
76418
76514
  }
76419
76515
  &[data-two-chars] {
76420
- --x-radius: 2em;
76516
+ /* 1.8em of the badge font is 1.26em of the text's: inside the line
76517
+ (1.25), so a circle beside a button's label does not make that
76518
+ button taller than its neighbours. 2em did, by a pixel and a half. */
76519
+ --x-radius: 1.8em;
76421
76520
  --x-number-font-size: unset;
76422
76521
  }
76423
76522
  &[data-three-chars] {
@@ -76451,7 +76550,11 @@ const BadgeCount = ({
76451
76550
  integer,
76452
76551
  lang,
76453
76552
  loading,
76454
- textAnchor = "center",
76553
+ // On the capitals rather than on the line box: a count sits beside a word,
76554
+ // and the eye centers it on the letters, not on the leading around them —
76555
+ // the line box center is above that, by half the leading plus what the
76556
+ // font keeps above its capitals (see TextAnchor).
76557
+ textAnchor = "char-center",
76455
76558
  lineLayout,
76456
76559
  ...props
76457
76560
  }) => {
@@ -77084,9 +77187,6 @@ const MessageBox = ({
77084
77187
  padding = "sm",
77085
77188
  icon,
77086
77189
  leftStripe,
77087
- // A message is free text: an emoji is expected in it, and must not push
77088
- // the first line down next to the icon and the close button.
77089
- emojiAsIcon = true,
77090
77190
  children,
77091
77191
  onClose,
77092
77192
  ...rest
@@ -77130,7 +77230,6 @@ const MessageBox = ({
77130
77230
  aspectRatio: "auto",
77131
77231
  children: icon
77132
77232
  }), jsx(Text, {
77133
- emojiAsIcon: emojiAsIcon,
77134
77233
  children: children
77135
77234
  }), onClose &&
77136
77235
  // A column as tall as the first line of the message, pinned to the
@@ -79267,5 +79366,5 @@ const UserSvg = () => jsx("svg", {
79267
79366
  })
79268
79367
  });
79269
79368
 
79270
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, 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, NumberSpin, Paragraph, Picker, Popover, Popup, 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, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
79369
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, 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, NumberSpin, Paragraph, Picker, Popover, Popup, 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, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
79271
79370
  //# sourceMappingURL=jsenv_navi.js.map