@vitavision/ui 0.12.0 → 0.14.0
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/README.md +6 -3
- package/dist/index.d.ts +244 -7
- package/dist/index.js +590 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Tabs.tsx +3 -10
- package/src/components/ThemeToggle.tsx +1 -5
- package/src/components/ui/Badge.tsx +1 -4
- package/src/components/ui/Breakdown.tsx +139 -0
- package/src/components/ui/Button.tsx +3 -15
- package/src/components/ui/Dialog.tsx +4 -19
- package/src/components/ui/Disclosure.tsx +2 -7
- package/src/components/ui/DropdownMenu.tsx +2 -16
- package/src/components/ui/Feedback.tsx +5 -9
- package/src/components/ui/Field.tsx +4 -15
- package/src/components/ui/HighlightedText.tsx +84 -0
- package/src/components/ui/Input.tsx +5 -43
- package/src/components/ui/Panel.tsx +4 -16
- package/src/components/ui/PoseInput.tsx +7 -5
- package/src/components/ui/Progress.tsx +80 -0
- package/src/components/ui/SegmentedControl.tsx +1 -3
- package/src/components/ui/Select.tsx +2 -10
- package/src/components/ui/Slider.tsx +2 -7
- package/src/components/ui/Table.tsx +182 -32
- package/src/components/ui/Textarea.tsx +149 -0
- package/src/components/ui/Toaster.tsx +3 -19
- package/src/components/ui/Toggle.tsx +1 -5
- package/src/components/ui/ToggleChip.tsx +1 -3
- package/src/components/ui/Tooltip.tsx +1 -5
- package/src/components/ui/VectorInput.tsx +1 -1
- package/src/components/ui/cn.ts +1 -2
- package/src/components/ui/highlightSpans.ts +110 -0
- package/src/components/ui/index.ts +18 -41
- package/src/components/ui/listboxModel.ts +1 -6
- package/src/components/ui/tableSort.ts +2 -9
- package/src/components/ui/tableWindow.ts +96 -0
- package/src/index.ts +1 -8
- package/src/styles.css +14 -0
- package/src/theme.ts +2 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AlertTriangle, ArrowDown, ArrowUp, ArrowUpDown, Check, CheckCircle2, ChevronDown, ChevronRight, CircleCheck, CircleX, HelpCircle, Info, Loader2, Minus, Moon, Sun, SunMoon, TriangleAlert, X, XCircle } from "lucide-react";
|
|
2
|
-
import { createContext, isValidElement, use, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { createContext, isValidElement, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import * as RadixTooltip from "@radix-ui/react-tooltip";
|
|
4
4
|
import { clsx } from "clsx";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
@@ -246,7 +246,7 @@ function ThemeToggle({ storageKey = DEFAULT_THEME_STORAGE_KEY, className }) {
|
|
|
246
246
|
}
|
|
247
247
|
//#endregion
|
|
248
248
|
//#region src/components/ui/Badge.tsx
|
|
249
|
-
const TONE_CLASSES = {
|
|
249
|
+
const TONE_CLASSES$1 = {
|
|
250
250
|
neutral: "bg-raised text-fg-muted ring-line",
|
|
251
251
|
normal: "bg-normal/12 text-normal ring-normal/25",
|
|
252
252
|
defect: "bg-defect/12 text-defect ring-defect/25",
|
|
@@ -271,7 +271,7 @@ const DOT_CLASSES = {
|
|
|
271
271
|
function Badge({ tone = "neutral", className, children }) {
|
|
272
272
|
return /* @__PURE__ */ jsx("span", {
|
|
273
273
|
"data-tone": tone,
|
|
274
|
-
className: cn("inline-flex items-center gap-1 rounded-control px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset", TONE_CLASSES[tone], className),
|
|
274
|
+
className: cn("inline-flex items-center gap-1 rounded-control px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset", TONE_CLASSES$1[tone], className),
|
|
275
275
|
children
|
|
276
276
|
});
|
|
277
277
|
}
|
|
@@ -346,6 +346,93 @@ function DensityProvider({ value, children }) {
|
|
|
346
346
|
});
|
|
347
347
|
}
|
|
348
348
|
//#endregion
|
|
349
|
+
//#region src/components/ui/Breakdown.tsx
|
|
350
|
+
/** Counts written the same on the server and in every browser: `2,556`, `0.5`. */
|
|
351
|
+
const count$1 = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
352
|
+
const TONE_CLASSES = {
|
|
353
|
+
normal: "bg-normal",
|
|
354
|
+
warn: "bg-warn",
|
|
355
|
+
defect: "bg-defect",
|
|
356
|
+
neutral: "bg-fg-subtle"
|
|
357
|
+
};
|
|
358
|
+
/** A count as drawn: 0 for anything that is not a finite, positive number. */
|
|
359
|
+
const amount = (value) => Number.isFinite(value) && value > 0 ? value : 0;
|
|
360
|
+
/** The segments that are drawn, the whole, and the rest of it. */
|
|
361
|
+
function measure(segments, total) {
|
|
362
|
+
const shown = segments.filter((segment) => amount(segment.value) > 0);
|
|
363
|
+
const sum = shown.reduce((acc, segment) => acc + amount(segment.value), 0);
|
|
364
|
+
const whole = total !== void 0 && Number.isFinite(total) && total > sum ? total : sum;
|
|
365
|
+
return {
|
|
366
|
+
shown,
|
|
367
|
+
sum,
|
|
368
|
+
whole,
|
|
369
|
+
rest: whole - sum
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The counts of a breakdown in words: `"412 done, 20 to check, 3 problems, 131 to do of 566"`.
|
|
374
|
+
*
|
|
375
|
+
* It is the default accessible name of `Breakdown`, and the text for a tooltip or a label printed
|
|
376
|
+
* beside the bar. Segments of 0 are left out; the rest of the total is named only with a
|
|
377
|
+
* `remainderLabel`. With nothing to count it reads `"0 of 566"`.
|
|
378
|
+
*
|
|
379
|
+
* @param segments - The categories, in order.
|
|
380
|
+
* @param options - The `total` and the `remainderLabel`.
|
|
381
|
+
* @returns The text.
|
|
382
|
+
*/
|
|
383
|
+
function breakdownText(segments, options = {}) {
|
|
384
|
+
const { shown, whole, rest } = measure(segments, options.total);
|
|
385
|
+
const parts = shown.map((segment) => `${count$1.format(amount(segment.value))} ${segment.label}`);
|
|
386
|
+
if (options.remainderLabel !== void 0 && rest > 0) parts.push(`${count$1.format(rest)} ${options.remainderLabel}`);
|
|
387
|
+
return `${parts.length > 0 ? parts.join(", ") : "0"} of ${count$1.format(whole)}`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* A thin bar of counts by category: 412 done, 20 to check and 3 problems out of 566, with the
|
|
391
|
+
* rest as empty track. Segments are drawn in the order given, each as wide as its share of the
|
|
392
|
+
* total (a non-zero count is never narrower than 2 px); segments of 0 are left out.
|
|
393
|
+
*
|
|
394
|
+
* Its colours are verdicts, so use it only for things the app has judged — never for a share of
|
|
395
|
+
* a continuous quantity or for series side by side (use `Progress` or a chart).
|
|
396
|
+
*
|
|
397
|
+
* A `role="img"` whose name lists every non-zero count, as `breakdownText` writes it: "412 done,
|
|
398
|
+
* 20 to check, 3 problems, 131 to do of 566". `aria-label` or `aria-labelledby` replaces it. The track
|
|
399
|
+
* matches `Progress` and follows the density in force. State is exposed as `data-state`
|
|
400
|
+
* (`"empty"` with nothing counted, `"partial"`, or `"complete"` when the segments fill the
|
|
401
|
+
* total), and each segment's tone as its `data-tone`. Width changes ease over 300 ms, and not at
|
|
402
|
+
* all with reduced motion.
|
|
403
|
+
*/
|
|
404
|
+
function Breakdown({ segments, total, remainderLabel, className, "aria-label": ariaLabel, ...rest }) {
|
|
405
|
+
const density = useDensity();
|
|
406
|
+
const { shown, sum, rest: remainder } = measure(segments, total);
|
|
407
|
+
const name = ariaLabel ?? (rest["aria-labelledby"] ? void 0 : breakdownText(segments, {
|
|
408
|
+
total,
|
|
409
|
+
remainderLabel
|
|
410
|
+
}));
|
|
411
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
412
|
+
role: "img",
|
|
413
|
+
"aria-label": name,
|
|
414
|
+
"data-state": sum === 0 ? "empty" : remainder > 0 ? "partial" : "complete",
|
|
415
|
+
...rest,
|
|
416
|
+
className: cn("w-full overflow-hidden rounded-full bg-line", byDensity(density, "h-1.5", "h-1"), className),
|
|
417
|
+
style: {
|
|
418
|
+
display: "flex",
|
|
419
|
+
gap: 1,
|
|
420
|
+
...rest.style
|
|
421
|
+
},
|
|
422
|
+
children: [shown.map((segment) => /* @__PURE__ */ jsx("div", {
|
|
423
|
+
"data-tone": segment.tone,
|
|
424
|
+
className: cn("h-full transition-[flex-grow] duration-300", TONE_CLASSES[segment.tone]),
|
|
425
|
+
style: {
|
|
426
|
+
flex: `${amount(segment.value)} 0 0`,
|
|
427
|
+
minWidth: 2
|
|
428
|
+
}
|
|
429
|
+
}, segment.label)), remainder > 0 && /* @__PURE__ */ jsx("div", {
|
|
430
|
+
className: "h-full transition-[flex-grow] duration-300",
|
|
431
|
+
style: { flex: `${remainder} 0 0` }
|
|
432
|
+
})]
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
349
436
|
//#region src/components/ui/Button.tsx
|
|
350
437
|
const VARIANT_CLASSES = {
|
|
351
438
|
primary: "bg-signal text-signal-fg hover:bg-signal-strong",
|
|
@@ -767,6 +854,9 @@ function SkeletonRows({ rows = 3, className }) {
|
|
|
767
854
|
* A `role="progressbar"` with `aria-valuenow` in percent. Its accessible name is
|
|
768
855
|
* `aria-label` if given, else `label`, else "Progress" — pass one that names the job when
|
|
769
856
|
* a screen shows more than one bar.
|
|
857
|
+
*
|
|
858
|
+
* @deprecated Use `Progress`: a value out of a `max` with a "412 of 2,556" value text, an
|
|
859
|
+
* indeterminate state, and a track that follows the density.
|
|
770
860
|
*/
|
|
771
861
|
function ProgressBar({ fraction, label, "aria-label": ariaLabel, className }) {
|
|
772
862
|
const percent = Math.round(Math.min(Math.max(fraction, 0), 1) * 100);
|
|
@@ -885,6 +975,131 @@ function Field({ label, description, error, required = false, annotation, as = "
|
|
|
885
975
|
});
|
|
886
976
|
}
|
|
887
977
|
//#endregion
|
|
978
|
+
//#region src/components/ui/highlightSpans.ts
|
|
979
|
+
/** The kinds in the order their styles stack: a later one wins where two would clash. */
|
|
980
|
+
const HIGHLIGHT_KINDS = [
|
|
981
|
+
"tag",
|
|
982
|
+
"protected-term",
|
|
983
|
+
"placeholder",
|
|
984
|
+
"mismatch"
|
|
985
|
+
];
|
|
986
|
+
/**
|
|
987
|
+
* Cuts `text` into runs that are each covered by one fixed set of spans. Spans are clamped
|
|
988
|
+
* to the text; empty, inverted and non-finite ones are dropped. Overlapping spans are kept:
|
|
989
|
+
* the overlap is a run covered by both.
|
|
990
|
+
*
|
|
991
|
+
* @param text - The string, shown as text.
|
|
992
|
+
* @param spans - The highlights, in any order.
|
|
993
|
+
* @returns The runs, in order, together covering the whole text.
|
|
994
|
+
*/
|
|
995
|
+
function segmentText(text, spans) {
|
|
996
|
+
const length = text.length;
|
|
997
|
+
const clamp = (offset) => Math.floor(Math.max(0, Math.min(length, offset)));
|
|
998
|
+
const valid = spans.map((span) => ({
|
|
999
|
+
span,
|
|
1000
|
+
start: clamp(span.start),
|
|
1001
|
+
end: clamp(span.end)
|
|
1002
|
+
})).filter(({ start, end }) => Number.isFinite(start) && Number.isFinite(end) && end > start);
|
|
1003
|
+
const cuts = /* @__PURE__ */ new Set([0, length]);
|
|
1004
|
+
for (const { start, end } of valid) {
|
|
1005
|
+
cuts.add(start);
|
|
1006
|
+
cuts.add(end);
|
|
1007
|
+
}
|
|
1008
|
+
const points = [...cuts].sort((a, b) => a - b);
|
|
1009
|
+
const segments = [];
|
|
1010
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
1011
|
+
const start = points[i];
|
|
1012
|
+
const end = points[i + 1];
|
|
1013
|
+
const covering = valid.filter((v) => v.start <= start && v.end >= end).map((v) => v.span);
|
|
1014
|
+
segments.push({
|
|
1015
|
+
text: text.slice(start, end),
|
|
1016
|
+
start,
|
|
1017
|
+
end,
|
|
1018
|
+
spans: covering
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return segments;
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Spans for every match of every rule: `%1`, `<b>`, a glossary term. Empty matches are
|
|
1025
|
+
* skipped. The result is sorted by start, then longest first.
|
|
1026
|
+
*
|
|
1027
|
+
* @param text - The string to search.
|
|
1028
|
+
* @param rules - Each pattern and the kind its matches get.
|
|
1029
|
+
* @returns The spans, ready for `HighlightedText`.
|
|
1030
|
+
*
|
|
1031
|
+
* @example
|
|
1032
|
+
* ```ts
|
|
1033
|
+
* findSpans("Open %1 in <b>ParamsManager</b>", [
|
|
1034
|
+
* { pattern: /%\d+/, kind: "placeholder" },
|
|
1035
|
+
* { pattern: /<\/?[a-z][^>]*>/i, kind: "tag" },
|
|
1036
|
+
* { pattern: /\bParamsManager\b/, kind: "protected-term" },
|
|
1037
|
+
* ]);
|
|
1038
|
+
* ```
|
|
1039
|
+
*/
|
|
1040
|
+
function findSpans(text, rules) {
|
|
1041
|
+
const spans = [];
|
|
1042
|
+
for (const rule of rules) {
|
|
1043
|
+
const flags = rule.pattern.flags.includes("g") ? rule.pattern.flags : `${rule.pattern.flags}g`;
|
|
1044
|
+
for (const match of text.matchAll(new RegExp(rule.pattern.source, flags))) {
|
|
1045
|
+
if (match[0] === "") continue;
|
|
1046
|
+
const start = match.index;
|
|
1047
|
+
spans.push({
|
|
1048
|
+
start,
|
|
1049
|
+
end: start + match[0].length,
|
|
1050
|
+
kind: rule.kind,
|
|
1051
|
+
...rule.label === void 0 ? {} : { label: rule.label }
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return spans.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1056
|
+
}
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/components/ui/HighlightedText.tsx
|
|
1059
|
+
/** What a screen reader hears after a span of each kind, by default. */
|
|
1060
|
+
const LABELS = {
|
|
1061
|
+
placeholder: "placeholder",
|
|
1062
|
+
tag: "markup",
|
|
1063
|
+
"protected-term": "protected term",
|
|
1064
|
+
mismatch: "mismatch"
|
|
1065
|
+
};
|
|
1066
|
+
/** The look of each kind; listed in `HIGHLIGHT_KINDS` order, so a later kind wins a clash. */
|
|
1067
|
+
const STYLES = {
|
|
1068
|
+
tag: "rounded-sm bg-raised px-px font-mono text-fg-muted",
|
|
1069
|
+
"protected-term": "underline decoration-fg-subtle decoration-dotted decoration-1 underline-offset-[3px]",
|
|
1070
|
+
placeholder: "rounded-sm bg-raised px-px font-mono text-fg ring-1 ring-line-strong ring-inset",
|
|
1071
|
+
mismatch: "bg-defect/12 text-fg underline decoration-defect decoration-wavy decoration-1 underline-offset-[3px]"
|
|
1072
|
+
};
|
|
1073
|
+
/**
|
|
1074
|
+
* A string with highlighted spans: placeholders (`%1`, `{count}`), markup tags (`<b>`),
|
|
1075
|
+
* protected terms (`ParamsManager`) and mismatches, each drawn in its own shape. The caller
|
|
1076
|
+
* supplies the spans, or finds them with `findSpans`.
|
|
1077
|
+
*
|
|
1078
|
+
* The text is rendered as text, never as HTML. Each span is a `<mark>` with `data-kind` (the
|
|
1079
|
+
* kinds covering it, space-separated), followed by its label for screen readers ("%1,
|
|
1080
|
+
* placeholder"), which a copy of the text leaves out.
|
|
1081
|
+
*/
|
|
1082
|
+
function HighlightedText({ text, spans = [], labels, className, ...rest }) {
|
|
1083
|
+
const segments = segmentText(text, spans);
|
|
1084
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1085
|
+
...rest,
|
|
1086
|
+
className: cn("break-words whitespace-pre-wrap", className),
|
|
1087
|
+
children: segments.map((segment) => {
|
|
1088
|
+
if (segment.spans.length === 0) return segment.text;
|
|
1089
|
+
const kinds = HIGHLIGHT_KINDS.filter((kind) => segment.spans.some((span) => span.kind === kind));
|
|
1090
|
+
const said = segment.spans.map((span) => span.label ?? (labels?.[span.kind] === void 0 ? LABELS[span.kind] : labels[span.kind])).filter((label) => typeof label === "string" && label !== "");
|
|
1091
|
+
return /* @__PURE__ */ jsxs("mark", {
|
|
1092
|
+
"data-kind": kinds.join(" "),
|
|
1093
|
+
className: cn("bg-transparent text-inherit", ...kinds.map((kind) => STYLES[kind])),
|
|
1094
|
+
children: [segment.text, said.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1095
|
+
className: "sr-only select-none",
|
|
1096
|
+
children: `, ${[...new Set(said)].join(", ")}`
|
|
1097
|
+
})]
|
|
1098
|
+
}, segment.start);
|
|
1099
|
+
})
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
888
1103
|
//#region src/components/ui/numberText.ts
|
|
889
1104
|
/**
|
|
890
1105
|
* A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign
|
|
@@ -1074,17 +1289,6 @@ function UnitNumberInput({ unit, style, "aria-describedby": describedBy, ...rest
|
|
|
1074
1289
|
})]
|
|
1075
1290
|
});
|
|
1076
1291
|
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Multi-line text — set in mono, for JSON and lists. Takes every `<textarea>` prop, `ref`
|
|
1079
|
-
* included.
|
|
1080
|
-
*/
|
|
1081
|
-
function Textarea({ className, "aria-describedby": describedBy, ...rest }) {
|
|
1082
|
-
return /* @__PURE__ */ jsx("textarea", {
|
|
1083
|
-
...useFieldDescription(describedBy),
|
|
1084
|
-
...rest,
|
|
1085
|
-
className: cn(controlClasses, "py-1.5 font-mono", className)
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
1292
|
//#endregion
|
|
1089
1293
|
//#region src/components/ui/listboxModel.ts
|
|
1090
1294
|
/**
|
|
@@ -1361,6 +1565,52 @@ function ReadoutStrip({ items, className }) {
|
|
|
1361
1565
|
});
|
|
1362
1566
|
}
|
|
1363
1567
|
//#endregion
|
|
1568
|
+
//#region src/components/ui/Progress.tsx
|
|
1569
|
+
/** Counts written the same on the server and in every browser: `2,556`, `0.5`. */
|
|
1570
|
+
const count = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
1571
|
+
/**
|
|
1572
|
+
* The accessible value text of a progress: `"412 of 2,556"`, or `null` while indeterminate.
|
|
1573
|
+
*
|
|
1574
|
+
* @param value - Done so far; `null` or `undefined` for an unknown amount.
|
|
1575
|
+
* @param max - The whole. Defaults to 100.
|
|
1576
|
+
* @returns The text, with the value clamped to `[0, max]`.
|
|
1577
|
+
*/
|
|
1578
|
+
function progressText(value, max = 100) {
|
|
1579
|
+
if (value === null || value === void 0 || !Number.isFinite(value)) return null;
|
|
1580
|
+
const whole = max > 0 ? max : 100;
|
|
1581
|
+
return `${count.format(Math.min(Math.max(value, 0), whole))} of ${count.format(whole)}`;
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* A progress bar: determinate when it has a `value` (a `max`, 100 by default), indeterminate
|
|
1585
|
+
* without one. Name it with `aria-label` or `aria-labelledby`.
|
|
1586
|
+
*
|
|
1587
|
+
* A `progressbar` with `aria-valuenow`, `aria-valuemax` and `aria-valuetext` ("412 of 2,556",
|
|
1588
|
+
* or your `valueText`); indeterminate, it has none of them, as the ARIA pattern asks.
|
|
1589
|
+
* The track follows the density in force. State is exposed as `data-state`
|
|
1590
|
+
* (`"indeterminate"`, `"loading"` or `"complete"`). With reduced motion the indeterminate bar
|
|
1591
|
+
* holds still.
|
|
1592
|
+
*/
|
|
1593
|
+
function Progress({ value, max = 100, valueText, className, ...rest }) {
|
|
1594
|
+
const density = useDensity();
|
|
1595
|
+
const whole = max > 0 ? max : 100;
|
|
1596
|
+
const text = progressText(value, whole);
|
|
1597
|
+
const done = text === null ? null : Math.min(Math.max(value, 0), whole);
|
|
1598
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1599
|
+
role: "progressbar",
|
|
1600
|
+
"aria-valuemin": done === null ? void 0 : 0,
|
|
1601
|
+
"aria-valuemax": done === null ? void 0 : whole,
|
|
1602
|
+
"aria-valuenow": done ?? void 0,
|
|
1603
|
+
"aria-valuetext": done === null ? void 0 : valueText ?? text ?? void 0,
|
|
1604
|
+
"data-state": done === null ? "indeterminate" : done >= whole ? "complete" : "loading",
|
|
1605
|
+
...rest,
|
|
1606
|
+
className: cn("relative w-full overflow-hidden rounded-full bg-line", byDensity(density, "h-1.5", "h-1"), className),
|
|
1607
|
+
children: done === null ? /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 left-0 w-1/3 animate-progress-sweep rounded-full bg-signal" }) : /* @__PURE__ */ jsx("div", {
|
|
1608
|
+
className: "h-full rounded-full bg-signal transition-[width] duration-300",
|
|
1609
|
+
style: { width: `${done / whole * 100}%` }
|
|
1610
|
+
})
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
//#endregion
|
|
1364
1614
|
//#region src/components/ui/Popover.tsx
|
|
1365
1615
|
/**
|
|
1366
1616
|
* Floating content anchored to `trigger`, on the `overlay` surface with a shadow (floating
|
|
@@ -1854,7 +2104,68 @@ function sortRows(rows, columns, sort) {
|
|
|
1854
2104
|
return sortOrder(rows, columns, sort).map((index) => rows[index]);
|
|
1855
2105
|
}
|
|
1856
2106
|
//#endregion
|
|
2107
|
+
//#region src/components/ui/tableWindow.ts
|
|
2108
|
+
/**
|
|
2109
|
+
* The rows that intersect the visible part of the body, plus `overscan` on each side.
|
|
2110
|
+
* Degenerate input (no rows, a row height ≤ 0, NaN) gives an empty window.
|
|
2111
|
+
*/
|
|
2112
|
+
function rowWindow({ rowCount, rowHeight, scrollTop, viewportHeight, headerHeight, overscan }) {
|
|
2113
|
+
const count = Number.isFinite(rowCount) ? Math.max(0, Math.floor(rowCount)) : 0;
|
|
2114
|
+
if (count === 0 || !(rowHeight > 0)) return {
|
|
2115
|
+
start: 0,
|
|
2116
|
+
end: 0,
|
|
2117
|
+
before: 0,
|
|
2118
|
+
after: 0
|
|
2119
|
+
};
|
|
2120
|
+
const extra = Math.max(0, Math.floor(overscan) || 0);
|
|
2121
|
+
const top = Math.max(0, scrollTop || 0);
|
|
2122
|
+
const bottom = Math.max(top, top + Math.max(0, viewportHeight || 0) - Math.max(0, headerHeight || 0));
|
|
2123
|
+
const first = Math.min(count, Math.floor(top / rowHeight));
|
|
2124
|
+
const last = Math.min(count, Math.max(first + 1, Math.ceil(bottom / rowHeight)));
|
|
2125
|
+
const start = Math.max(0, first - extra);
|
|
2126
|
+
const end = Math.min(count, last + extra);
|
|
2127
|
+
return {
|
|
2128
|
+
start,
|
|
2129
|
+
end,
|
|
2130
|
+
before: start * rowHeight,
|
|
2131
|
+
after: (count - end) * rowHeight
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* The `scrollTop` that shows the row at `position` in full below the sticky header, moving
|
|
2136
|
+
* as little as possible: unchanged when it is already in view, else aligned to the nearer
|
|
2137
|
+
* edge. A viewport shorter than one row aligns the row's top.
|
|
2138
|
+
*/
|
|
2139
|
+
function scrollTopFor(position, { rowHeight, scrollTop, viewportHeight, headerHeight }) {
|
|
2140
|
+
const rowTop = position * rowHeight;
|
|
2141
|
+
const visible = viewportHeight - headerHeight;
|
|
2142
|
+
if (rowTop < scrollTop || visible < rowHeight) return rowTop;
|
|
2143
|
+
if (rowTop + rowHeight > scrollTop + visible) return rowTop + rowHeight - visible;
|
|
2144
|
+
return scrollTop;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* The position a navigation key moves to from `position`, among `rowCount` rows, `page` rows
|
|
2148
|
+
* to a screen; `null` for a key that does not navigate.
|
|
2149
|
+
*/
|
|
2150
|
+
function navigate(key, position, rowCount, page) {
|
|
2151
|
+
const last = rowCount - 1;
|
|
2152
|
+
if (last < 0) return null;
|
|
2153
|
+
const step = Math.max(1, Math.floor(page));
|
|
2154
|
+
const clamp = (p) => Math.min(last, Math.max(0, p));
|
|
2155
|
+
switch (key) {
|
|
2156
|
+
case "ArrowDown": return clamp(position + 1);
|
|
2157
|
+
case "ArrowUp": return clamp(position - 1);
|
|
2158
|
+
case "PageDown": return clamp(position + step);
|
|
2159
|
+
case "PageUp": return clamp(position - step);
|
|
2160
|
+
case "Home": return 0;
|
|
2161
|
+
case "End": return last;
|
|
2162
|
+
default: return null;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
//#endregion
|
|
1857
2166
|
//#region src/components/ui/Table.tsx
|
|
2167
|
+
/** Rows per screen before a windowed table has measured itself (a server render, the first paint). */
|
|
2168
|
+
const INITIAL_ROWS = 20;
|
|
1858
2169
|
/** The header icon for a sort state: the direction when sorted, both arrows when only sortable. */
|
|
1859
2170
|
const SORT_ICONS = {
|
|
1860
2171
|
ascending: ArrowUp,
|
|
@@ -1875,8 +2186,13 @@ const SORT_ICONS = {
|
|
|
1875
2186
|
* `defaultSort`) or controlled (`sort` with `onSortChange`); with `manualSort` the headers
|
|
1876
2187
|
* show it and the rows render as given. Row callbacks always receive a row's index in `rows`,
|
|
1877
2188
|
* whatever the order on screen.
|
|
2189
|
+
*
|
|
2190
|
+
* Activatable rows also move focus with the arrow keys, Page Up / Page Down and Home / End;
|
|
2191
|
+
* with `selectionFollowsFocus` each of those moves also activates the row it lands on.
|
|
2192
|
+
* With `windowed` the table scrolls itself (vertically, under a sticky header), renders only the
|
|
2193
|
+
* rows in view, and uses a fixed layout: give columns a `width` where the content needs it.
|
|
1878
2194
|
*/
|
|
1879
|
-
function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort = null, onSortChange, manualSort = false }) {
|
|
2195
|
+
function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort = null, onSortChange, manualSort = false, windowed, selectionFollowsFocus = false }) {
|
|
1880
2196
|
const density = useDensity();
|
|
1881
2197
|
const [ownSort, setOwnSort] = useState(defaultSort);
|
|
1882
2198
|
const controlled = controlledSort !== void 0;
|
|
@@ -1892,71 +2208,282 @@ function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, classN
|
|
|
1892
2208
|
if (!controlled) setOwnSort(next);
|
|
1893
2209
|
onSortChange?.(next);
|
|
1894
2210
|
};
|
|
2211
|
+
const rowHeight = windowed?.rowHeight ?? 0;
|
|
2212
|
+
const scrollRef = useRef(null);
|
|
2213
|
+
const headRef = useRef(null);
|
|
2214
|
+
const bodyRef = useRef(null);
|
|
2215
|
+
const pendingFocus = useRef(null);
|
|
2216
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
2217
|
+
const [measured, setMeasured] = useState(null);
|
|
2218
|
+
const viewportHeight = measured?.viewport ?? windowed?.height ?? INITIAL_ROWS * rowHeight;
|
|
2219
|
+
const headerHeight = measured?.header ?? 0;
|
|
2220
|
+
const range = windowed ? rowWindow({
|
|
2221
|
+
rowCount: order.length,
|
|
2222
|
+
rowHeight,
|
|
2223
|
+
scrollTop,
|
|
2224
|
+
viewportHeight,
|
|
2225
|
+
headerHeight,
|
|
2226
|
+
overscan: windowed.overscan ?? 8
|
|
2227
|
+
}) : {
|
|
2228
|
+
start: 0,
|
|
2229
|
+
end: order.length,
|
|
2230
|
+
before: 0,
|
|
2231
|
+
after: 0
|
|
2232
|
+
};
|
|
2233
|
+
const isWindowed = windowed !== void 0;
|
|
2234
|
+
const hasRows = rows.length > 0;
|
|
2235
|
+
useLayoutEffect(() => {
|
|
2236
|
+
const scroller = scrollRef.current;
|
|
2237
|
+
if (!isWindowed || !hasRows || !scroller || typeof ResizeObserver === "undefined") return;
|
|
2238
|
+
const measure = () => {
|
|
2239
|
+
if (!scroller.isConnected) return;
|
|
2240
|
+
const next = {
|
|
2241
|
+
viewport: scroller.clientHeight,
|
|
2242
|
+
header: headRef.current?.offsetHeight ?? 0
|
|
2243
|
+
};
|
|
2244
|
+
setMeasured((last) => last?.viewport === next.viewport && last.header === next.header ? last : next);
|
|
2245
|
+
};
|
|
2246
|
+
measure();
|
|
2247
|
+
setScrollTop(scroller.scrollTop);
|
|
2248
|
+
const observer = new ResizeObserver(measure);
|
|
2249
|
+
observer.observe(scroller);
|
|
2250
|
+
return () => observer.disconnect();
|
|
2251
|
+
}, [isWindowed, hasRows]);
|
|
2252
|
+
useLayoutEffect(() => {
|
|
2253
|
+
const target = pendingFocus.current;
|
|
2254
|
+
if (target === null) return;
|
|
2255
|
+
const row = bodyRef.current?.querySelector(`tr[data-position="${target}"]`);
|
|
2256
|
+
if (!row) return;
|
|
2257
|
+
pendingFocus.current = null;
|
|
2258
|
+
row.focus({ preventScroll: isWindowed });
|
|
2259
|
+
});
|
|
2260
|
+
/**
|
|
2261
|
+
* Moves focus from the row at `position` for a navigation key, activating the new row when
|
|
2262
|
+
* selection follows focus; `true` when the key was one.
|
|
2263
|
+
*/
|
|
2264
|
+
const moveFocus = (event, position) => {
|
|
2265
|
+
const page = windowed ? Math.floor((viewportHeight - headerHeight) / rowHeight) : 10;
|
|
2266
|
+
const target = navigate(event.key, position, order.length, page);
|
|
2267
|
+
if (target === null) return false;
|
|
2268
|
+
if (selectionFollowsFocus && onRowClick && target !== position) {
|
|
2269
|
+
const index = order[target];
|
|
2270
|
+
onRowClick(rows[index], index, event);
|
|
2271
|
+
}
|
|
2272
|
+
pendingFocus.current = target;
|
|
2273
|
+
const scroller = scrollRef.current;
|
|
2274
|
+
if (windowed && scroller) {
|
|
2275
|
+
scroller.scrollTop = scrollTopFor(target, {
|
|
2276
|
+
rowHeight,
|
|
2277
|
+
scrollTop: scroller.scrollTop,
|
|
2278
|
+
viewportHeight,
|
|
2279
|
+
headerHeight
|
|
2280
|
+
});
|
|
2281
|
+
setScrollTop(scroller.scrollTop);
|
|
2282
|
+
}
|
|
2283
|
+
const row = bodyRef.current?.querySelector(`tr[data-position="${target}"]`);
|
|
2284
|
+
if (row) {
|
|
2285
|
+
pendingFocus.current = null;
|
|
2286
|
+
row.focus({ preventScroll: windowed !== void 0 });
|
|
2287
|
+
}
|
|
2288
|
+
return true;
|
|
2289
|
+
};
|
|
1895
2290
|
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
1896
2291
|
className: cn("text-center text-fg-muted", byDensity(density, "py-6 text-sm", "py-3 text-xs"), className),
|
|
1897
2292
|
children: empty
|
|
1898
2293
|
});
|
|
2294
|
+
const spacer = (height) => /* @__PURE__ */ jsx("tr", {
|
|
2295
|
+
"aria-hidden": "true",
|
|
2296
|
+
style: { height },
|
|
2297
|
+
children: /* @__PURE__ */ jsx("td", {
|
|
2298
|
+
colSpan: columns.length,
|
|
2299
|
+
className: "p-0"
|
|
2300
|
+
})
|
|
2301
|
+
});
|
|
2302
|
+
const scrollStop = windowed !== void 0 && !onRowClick && !columns.some(isSortable);
|
|
1899
2303
|
return /* @__PURE__ */ jsx("div", {
|
|
1900
|
-
|
|
2304
|
+
ref: scrollRef,
|
|
2305
|
+
className: cn("w-full", windowed ? scrollStop && "focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-signal" : "overflow-x-auto", className),
|
|
2306
|
+
style: windowed ? {
|
|
2307
|
+
overflow: "auto",
|
|
2308
|
+
height: windowed.height ?? "100%"
|
|
2309
|
+
} : void 0,
|
|
2310
|
+
onScroll: windowed ? (event) => setScrollTop(event.currentTarget.scrollTop) : void 0,
|
|
2311
|
+
...scrollStop ? {
|
|
2312
|
+
tabIndex: 0,
|
|
2313
|
+
role: "region",
|
|
2314
|
+
"aria-label": caption ?? "Table"
|
|
2315
|
+
} : {},
|
|
1901
2316
|
children: /* @__PURE__ */ jsxs("table", {
|
|
1902
2317
|
className: cn("w-full text-left", byDensity(density, "text-sm", "text-[11px]")),
|
|
2318
|
+
style: windowed ? { tableLayout: "fixed" } : void 0,
|
|
2319
|
+
"aria-rowcount": windowed ? order.length + 1 : void 0,
|
|
1903
2320
|
children: [
|
|
1904
2321
|
caption && /* @__PURE__ */ jsx("caption", {
|
|
1905
2322
|
className: "sr-only",
|
|
1906
2323
|
children: caption
|
|
1907
2324
|
}),
|
|
1908
|
-
/* @__PURE__ */ jsx("thead", {
|
|
1909
|
-
|
|
1910
|
-
children:
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
"
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2325
|
+
/* @__PURE__ */ jsx("thead", {
|
|
2326
|
+
ref: headRef,
|
|
2327
|
+
children: /* @__PURE__ */ jsx("tr", {
|
|
2328
|
+
className: "border-b border-line",
|
|
2329
|
+
"aria-rowindex": windowed ? 1 : void 0,
|
|
2330
|
+
children: columns.map((column) => {
|
|
2331
|
+
const sortable = isSortable(column);
|
|
2332
|
+
const direction = sortable && sort?.key === column.key ? sort.direction : void 0;
|
|
2333
|
+
const SortIcon = SORT_ICONS[direction ?? "none"];
|
|
2334
|
+
return /* @__PURE__ */ jsx("th", {
|
|
2335
|
+
scope: "col",
|
|
2336
|
+
style: windowed ? {
|
|
2337
|
+
position: "sticky",
|
|
2338
|
+
top: 0,
|
|
2339
|
+
zIndex: 1,
|
|
2340
|
+
...column.width ? { width: column.width } : {}
|
|
2341
|
+
} : column.width ? { width: column.width } : void 0,
|
|
2342
|
+
"aria-sort": direction,
|
|
2343
|
+
"data-sort": sortable ? direction ?? "none" : void 0,
|
|
2344
|
+
className: cn("font-medium text-fg-muted last:pr-0", byDensity(density, "pb-2 pr-3 text-xs", "pb-1 pr-2 text-[10px]"), column.numeric && "text-right", windowed && "bg-surface shadow-[inset_0_-1px_0_var(--line)]"),
|
|
2345
|
+
children: sortable ? /* @__PURE__ */ jsxs("button", {
|
|
2346
|
+
type: "button",
|
|
2347
|
+
onClick: () => changeSort(column),
|
|
2348
|
+
className: cn("-mx-1 flex w-fit max-w-full cursor-pointer items-center gap-1 rounded-control px-1", "font-medium transition-colors hover:text-fg", focusRing, column.numeric ? "ml-auto flex-row-reverse text-right" : "text-left", direction && "text-fg"),
|
|
2349
|
+
children: [column.header, /* @__PURE__ */ jsx(SortIcon, {
|
|
2350
|
+
"aria-hidden": true,
|
|
2351
|
+
className: cn("shrink-0", byDensity(density, "size-3", "size-2.5"), !direction && "text-fg-subtle")
|
|
2352
|
+
})]
|
|
2353
|
+
}) : column.header
|
|
2354
|
+
}, column.key);
|
|
2355
|
+
})
|
|
1930
2356
|
})
|
|
1931
|
-
})
|
|
1932
|
-
/* @__PURE__ */
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2357
|
+
}),
|
|
2358
|
+
/* @__PURE__ */ jsxs("tbody", {
|
|
2359
|
+
ref: bodyRef,
|
|
2360
|
+
children: [
|
|
2361
|
+
range.before > 0 && spacer(range.before),
|
|
2362
|
+
order.slice(range.start, range.end).map((index, offset) => {
|
|
2363
|
+
const row = rows[index];
|
|
2364
|
+
const active = isRowActive?.(row, index) ?? false;
|
|
2365
|
+
const position = range.start + offset;
|
|
2366
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
2367
|
+
"data-position": position,
|
|
2368
|
+
"aria-rowindex": windowed ? position + 2 : void 0,
|
|
2369
|
+
style: windowed ? { height: rowHeight } : void 0,
|
|
2370
|
+
"data-state": active ? "active" : void 0,
|
|
2371
|
+
className: cn(!windowed && "border-b border-line/60 last:border-0", onRowClick && "cursor-pointer hover:bg-raised", active && "bg-signal/10"),
|
|
2372
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
2373
|
+
"aria-current": active ? true : void 0,
|
|
2374
|
+
onClick: onRowClick ? (event) => onRowClick(row, index, event) : void 0,
|
|
2375
|
+
onKeyDown: onRowClick ? (event) => {
|
|
2376
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2377
|
+
event.preventDefault();
|
|
2378
|
+
onRowClick(row, index, event);
|
|
2379
|
+
} else if (event.target === event.currentTarget && moveFocus(event, position)) event.preventDefault();
|
|
2380
|
+
} : void 0,
|
|
2381
|
+
onPointerEnter: onRowHover ? () => onRowHover(row, index) : void 0,
|
|
2382
|
+
onPointerLeave: onRowHover ? () => onRowHover(null, null) : void 0,
|
|
2383
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("td", {
|
|
2384
|
+
className: cn("align-middle text-fg last:pr-0", byDensity(density, "py-2 pr-3", "py-0.5 pr-2"), column.numeric && "text-right font-mono tabular-nums", windowed && "shadow-[inset_0_-1px_0_color-mix(in_oklab,var(--line)_60%,transparent)]"),
|
|
2385
|
+
style: windowed ? {
|
|
2386
|
+
whiteSpace: "nowrap",
|
|
2387
|
+
overflow: "hidden",
|
|
2388
|
+
textOverflow: "ellipsis"
|
|
2389
|
+
} : void 0,
|
|
2390
|
+
children: column.cell(row)
|
|
2391
|
+
}, column.key))
|
|
2392
|
+
}, rowKey(row, index));
|
|
2393
|
+
}),
|
|
2394
|
+
range.after > 0 && spacer(range.after)
|
|
2395
|
+
]
|
|
2396
|
+
})
|
|
1955
2397
|
]
|
|
1956
2398
|
})
|
|
1957
2399
|
});
|
|
1958
2400
|
}
|
|
1959
2401
|
//#endregion
|
|
2402
|
+
//#region src/components/ui/Textarea.tsx
|
|
2403
|
+
/** Sets a ref of either kind. */
|
|
2404
|
+
function assign(ref, value) {
|
|
2405
|
+
if (typeof ref === "function") ref(value);
|
|
2406
|
+
else if (ref) ref.current = value;
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Fits a textarea's height to its content between `minRows` and `maxRows` lines, and scrolls
|
|
2410
|
+
* only past the cap.
|
|
2411
|
+
*/
|
|
2412
|
+
function fit(node, minRows, maxRows) {
|
|
2413
|
+
const style = getComputedStyle(node);
|
|
2414
|
+
const line = Number.parseFloat(style.lineHeight) || Number.parseFloat(style.fontSize) * 1.2 || 20;
|
|
2415
|
+
const border = Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth);
|
|
2416
|
+
const chrome = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom) + border;
|
|
2417
|
+
const min = minRows * line + chrome;
|
|
2418
|
+
const max = maxRows === void 0 ? Number.POSITIVE_INFINITY : Math.max(min, maxRows * line + chrome);
|
|
2419
|
+
node.style.height = `${min}px`;
|
|
2420
|
+
const content = node.scrollHeight + border;
|
|
2421
|
+
node.style.height = `${Math.min(Math.max(content, min), max)}px`;
|
|
2422
|
+
node.style.overflowY = content > max + .5 ? "auto" : "hidden";
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Multi-line text, on the shared control palette. Takes every `<textarea>` prop, `ref`
|
|
2426
|
+
* included; inside a `Field` it is described by the field's description and error, and
|
|
2427
|
+
* shown invalid while the field has an error.
|
|
2428
|
+
*
|
|
2429
|
+
* With `minRows` or `maxRows` it grows with its content between the two (and scrolls past
|
|
2430
|
+
* `maxRows`); without them it is `rows` tall and the reader can resize it. State is exposed
|
|
2431
|
+
* as `data-invalid`.
|
|
2432
|
+
*/
|
|
2433
|
+
function Textarea({ className, "aria-describedby": describedBy, invalid, mono = false, minRows, maxRows, rows, value, onChange, onValueChange, ref, ...rest }) {
|
|
2434
|
+
const field = useFieldDescription(describedBy);
|
|
2435
|
+
const isInvalid = invalid === true || field["aria-invalid"] === true;
|
|
2436
|
+
const grows = minRows !== void 0 || maxRows !== void 0;
|
|
2437
|
+
const floor = Math.max(1, Math.floor(minRows ?? 1));
|
|
2438
|
+
const cap = maxRows === void 0 ? void 0 : Math.max(floor, Math.floor(maxRows));
|
|
2439
|
+
const node = useRef(null);
|
|
2440
|
+
const setNode = useCallback((element) => {
|
|
2441
|
+
node.current = element;
|
|
2442
|
+
assign(ref, element);
|
|
2443
|
+
}, [ref]);
|
|
2444
|
+
useLayoutEffect(() => {
|
|
2445
|
+
if (grows && node.current) fit(node.current, floor, cap);
|
|
2446
|
+
}, [
|
|
2447
|
+
grows,
|
|
2448
|
+
floor,
|
|
2449
|
+
cap,
|
|
2450
|
+
mono,
|
|
2451
|
+
value
|
|
2452
|
+
]);
|
|
2453
|
+
useLayoutEffect(() => {
|
|
2454
|
+
const element = node.current;
|
|
2455
|
+
if (!grows || !element || typeof ResizeObserver === "undefined") return void 0;
|
|
2456
|
+
let width = element.clientWidth;
|
|
2457
|
+
const observer = new ResizeObserver(() => {
|
|
2458
|
+
if (element.clientWidth === width) return;
|
|
2459
|
+
width = element.clientWidth;
|
|
2460
|
+
fit(element, floor, cap);
|
|
2461
|
+
});
|
|
2462
|
+
observer.observe(element);
|
|
2463
|
+
return () => observer.disconnect();
|
|
2464
|
+
}, [
|
|
2465
|
+
grows,
|
|
2466
|
+
floor,
|
|
2467
|
+
cap
|
|
2468
|
+
]);
|
|
2469
|
+
const change = (event) => {
|
|
2470
|
+
onChange?.(event);
|
|
2471
|
+
onValueChange?.(event.currentTarget.value);
|
|
2472
|
+
if (grows) fit(event.currentTarget, floor, cap);
|
|
2473
|
+
};
|
|
2474
|
+
return /* @__PURE__ */ jsx("textarea", {
|
|
2475
|
+
...field,
|
|
2476
|
+
"aria-invalid": isInvalid || void 0,
|
|
2477
|
+
"data-invalid": isInvalid ? "" : void 0,
|
|
2478
|
+
...rest,
|
|
2479
|
+
...value === void 0 ? {} : { value },
|
|
2480
|
+
ref: setNode,
|
|
2481
|
+
rows: grows ? floor : rows,
|
|
2482
|
+
onChange: change,
|
|
2483
|
+
className: cn(controlClasses, "py-1.5", grows && "resize-none", mono && "font-mono", "data-invalid:border-defect data-invalid:hover:border-defect", className)
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
//#endregion
|
|
1960
2487
|
//#region src/components/ui/Toggle.tsx
|
|
1961
2488
|
/**
|
|
1962
2489
|
* A setting: changes how the thing behaves from now on, and reads as one ("show the mask").
|
|
@@ -2325,6 +2852,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
|
|
|
2325
2852
|
});
|
|
2326
2853
|
}
|
|
2327
2854
|
//#endregion
|
|
2328
|
-
export { Badge, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, DropdownMenu, Empty, ErrorBox, Field, InfoHint, Input, Kbd, Listbox, MenuCheckboxItem, MenuItem, MenuLabel, MenuSeparator, NumberInput, PageHeader, Panel, Popover, PopoverClose, PoseInput, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, Toaster, ToggleChip, Tooltip, TooltipProvider, VectorInput, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, focusRing, focusRingInset, formatNumber, initTheme, inputClasses, parseNumber, readThemeChoice, resolveTheme, setThemeChoice, sortRows, toast, toneColor, useControlHeight, useDensity };
|
|
2855
|
+
export { Badge, Breakdown, Button, ButtonLink, Callout, Checkbox, ConfirmDialog, CountRun, DEFAULT_THEME_STORAGE_KEY, DensityProvider, Dialog, DialogClose, Disclosure, DropdownMenu, Empty, ErrorBox, Field, HighlightedText, InfoHint, Input, Kbd, Listbox, MenuCheckboxItem, MenuItem, MenuLabel, MenuSeparator, NumberInput, PageHeader, Panel, Popover, PopoverClose, PoseInput, Progress, ProgressBar, ReadoutStrip, Section, SegmentedControl, Select, Skeleton, SkeletonRows, Slider, StatusDot, Switch, Table, Tabs, Textarea, ThemeToggle, Toaster, ToggleChip, Tooltip, TooltipProvider, VectorInput, breakdownText, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, findSpans, focusRing, focusRingInset, formatNumber, initTheme, inputClasses, parseNumber, progressText, readThemeChoice, resolveTheme, setThemeChoice, sortRows, toast, toneColor, useControlHeight, useDensity };
|
|
2329
2856
|
|
|
2330
2857
|
//# sourceMappingURL=index.js.map
|