@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.
- package/dist/jsenv_navi.js +620 -521
- package/dist/jsenv_navi.js.map +47 -52
- package/dist/jsenv_navi_side_effects.js +37 -0
- package/dist/jsenv_navi_side_effects.js.map +2 -2
- package/docs/AI_INSTRUCTIONS.md +4 -4
- package/docs/typography.md +52 -69
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
|
4289
|
-
//
|
|
4290
|
-
//
|
|
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(
|
|
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
|
|
4456
|
-
one line tall (1lh), and that line has to be the
|
|
4457
|
-
line — which it is only if both read the same
|
|
4458
|
-
than whatever the element the callout sits in
|
|
4459
|
-
|
|
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:
|
|
17685
|
-
//
|
|
17686
|
-
//
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
-
*
|
|
48018
|
-
*
|
|
48019
|
-
*
|
|
48020
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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$
|
|
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
|
|
48131
|
-
ref
|
|
48486
|
+
const controlProps = {
|
|
48487
|
+
ref,
|
|
48132
48488
|
...rest,
|
|
48133
|
-
|
|
48134
|
-
|
|
48135
|
-
|
|
48136
|
-
valueSignal: valueSignal,
|
|
48489
|
+
name,
|
|
48490
|
+
value,
|
|
48491
|
+
valueSignal,
|
|
48137
48492
|
autoFocus: editing,
|
|
48138
48493
|
autoFocusVisible: true,
|
|
48139
|
-
autoFocusSelect
|
|
48494
|
+
autoFocusSelect,
|
|
48140
48495
|
cancelOnEscape: true,
|
|
48141
48496
|
cancelOnBlurInvalid: true,
|
|
48142
|
-
constraints
|
|
48143
|
-
required
|
|
48144
|
-
readOnly
|
|
48145
|
-
|
|
48146
|
-
|
|
48147
|
-
|
|
48148
|
-
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
65265
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
/*
|
|
68992
|
-
|
|
68993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|