@vitavision/ui 0.12.0 → 0.13.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 +171 -7
- package/dist/index.js +490 -61
- 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/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 +155 -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 +10 -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";
|
|
@@ -767,6 +767,9 @@ function SkeletonRows({ rows = 3, className }) {
|
|
|
767
767
|
* A `role="progressbar"` with `aria-valuenow` in percent. Its accessible name is
|
|
768
768
|
* `aria-label` if given, else `label`, else "Progress" — pass one that names the job when
|
|
769
769
|
* a screen shows more than one bar.
|
|
770
|
+
*
|
|
771
|
+
* @deprecated Use `Progress`: a value out of a `max` with a "412 of 2,556" value text, an
|
|
772
|
+
* indeterminate state, and a track that follows the density.
|
|
770
773
|
*/
|
|
771
774
|
function ProgressBar({ fraction, label, "aria-label": ariaLabel, className }) {
|
|
772
775
|
const percent = Math.round(Math.min(Math.max(fraction, 0), 1) * 100);
|
|
@@ -885,6 +888,131 @@ function Field({ label, description, error, required = false, annotation, as = "
|
|
|
885
888
|
});
|
|
886
889
|
}
|
|
887
890
|
//#endregion
|
|
891
|
+
//#region src/components/ui/highlightSpans.ts
|
|
892
|
+
/** The kinds in the order their styles stack: a later one wins where two would clash. */
|
|
893
|
+
const HIGHLIGHT_KINDS = [
|
|
894
|
+
"tag",
|
|
895
|
+
"protected-term",
|
|
896
|
+
"placeholder",
|
|
897
|
+
"mismatch"
|
|
898
|
+
];
|
|
899
|
+
/**
|
|
900
|
+
* Cuts `text` into runs that are each covered by one fixed set of spans. Spans are clamped
|
|
901
|
+
* to the text; empty, inverted and non-finite ones are dropped. Overlapping spans are kept:
|
|
902
|
+
* the overlap is a run covered by both.
|
|
903
|
+
*
|
|
904
|
+
* @param text - The string, shown as text.
|
|
905
|
+
* @param spans - The highlights, in any order.
|
|
906
|
+
* @returns The runs, in order, together covering the whole text.
|
|
907
|
+
*/
|
|
908
|
+
function segmentText(text, spans) {
|
|
909
|
+
const length = text.length;
|
|
910
|
+
const clamp = (offset) => Math.floor(Math.max(0, Math.min(length, offset)));
|
|
911
|
+
const valid = spans.map((span) => ({
|
|
912
|
+
span,
|
|
913
|
+
start: clamp(span.start),
|
|
914
|
+
end: clamp(span.end)
|
|
915
|
+
})).filter(({ start, end }) => Number.isFinite(start) && Number.isFinite(end) && end > start);
|
|
916
|
+
const cuts = /* @__PURE__ */ new Set([0, length]);
|
|
917
|
+
for (const { start, end } of valid) {
|
|
918
|
+
cuts.add(start);
|
|
919
|
+
cuts.add(end);
|
|
920
|
+
}
|
|
921
|
+
const points = [...cuts].sort((a, b) => a - b);
|
|
922
|
+
const segments = [];
|
|
923
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
924
|
+
const start = points[i];
|
|
925
|
+
const end = points[i + 1];
|
|
926
|
+
const covering = valid.filter((v) => v.start <= start && v.end >= end).map((v) => v.span);
|
|
927
|
+
segments.push({
|
|
928
|
+
text: text.slice(start, end),
|
|
929
|
+
start,
|
|
930
|
+
end,
|
|
931
|
+
spans: covering
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return segments;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Spans for every match of every rule: `%1`, `<b>`, a glossary term. Empty matches are
|
|
938
|
+
* skipped. The result is sorted by start, then longest first.
|
|
939
|
+
*
|
|
940
|
+
* @param text - The string to search.
|
|
941
|
+
* @param rules - Each pattern and the kind its matches get.
|
|
942
|
+
* @returns The spans, ready for `HighlightedText`.
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```ts
|
|
946
|
+
* findSpans("Open %1 in <b>ParamsManager</b>", [
|
|
947
|
+
* { pattern: /%\d+/, kind: "placeholder" },
|
|
948
|
+
* { pattern: /<\/?[a-z][^>]*>/i, kind: "tag" },
|
|
949
|
+
* { pattern: /\bParamsManager\b/, kind: "protected-term" },
|
|
950
|
+
* ]);
|
|
951
|
+
* ```
|
|
952
|
+
*/
|
|
953
|
+
function findSpans(text, rules) {
|
|
954
|
+
const spans = [];
|
|
955
|
+
for (const rule of rules) {
|
|
956
|
+
const flags = rule.pattern.flags.includes("g") ? rule.pattern.flags : `${rule.pattern.flags}g`;
|
|
957
|
+
for (const match of text.matchAll(new RegExp(rule.pattern.source, flags))) {
|
|
958
|
+
if (match[0] === "") continue;
|
|
959
|
+
const start = match.index;
|
|
960
|
+
spans.push({
|
|
961
|
+
start,
|
|
962
|
+
end: start + match[0].length,
|
|
963
|
+
kind: rule.kind,
|
|
964
|
+
...rule.label === void 0 ? {} : { label: rule.label }
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return spans.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
971
|
+
//#region src/components/ui/HighlightedText.tsx
|
|
972
|
+
/** What a screen reader hears after a span of each kind, by default. */
|
|
973
|
+
const LABELS = {
|
|
974
|
+
placeholder: "placeholder",
|
|
975
|
+
tag: "markup",
|
|
976
|
+
"protected-term": "protected term",
|
|
977
|
+
mismatch: "mismatch"
|
|
978
|
+
};
|
|
979
|
+
/** The look of each kind; listed in `HIGHLIGHT_KINDS` order, so a later kind wins a clash. */
|
|
980
|
+
const STYLES = {
|
|
981
|
+
tag: "rounded-sm bg-raised px-px font-mono text-fg-muted",
|
|
982
|
+
"protected-term": "underline decoration-fg-subtle decoration-dotted decoration-1 underline-offset-[3px]",
|
|
983
|
+
placeholder: "rounded-sm bg-raised px-px font-mono text-fg ring-1 ring-line-strong ring-inset",
|
|
984
|
+
mismatch: "bg-defect/12 text-fg underline decoration-defect decoration-wavy decoration-1 underline-offset-[3px]"
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* A string with highlighted spans: placeholders (`%1`, `{count}`), markup tags (`<b>`),
|
|
988
|
+
* protected terms (`ParamsManager`) and mismatches, each drawn in its own shape. The caller
|
|
989
|
+
* supplies the spans, or finds them with `findSpans`.
|
|
990
|
+
*
|
|
991
|
+
* The text is rendered as text, never as HTML. Each span is a `<mark>` with `data-kind` (the
|
|
992
|
+
* kinds covering it, space-separated), followed by its label for screen readers ("%1,
|
|
993
|
+
* placeholder"), which a copy of the text leaves out.
|
|
994
|
+
*/
|
|
995
|
+
function HighlightedText({ text, spans = [], labels, className, ...rest }) {
|
|
996
|
+
const segments = segmentText(text, spans);
|
|
997
|
+
return /* @__PURE__ */ jsx("span", {
|
|
998
|
+
...rest,
|
|
999
|
+
className: cn("break-words whitespace-pre-wrap", className),
|
|
1000
|
+
children: segments.map((segment) => {
|
|
1001
|
+
if (segment.spans.length === 0) return segment.text;
|
|
1002
|
+
const kinds = HIGHLIGHT_KINDS.filter((kind) => segment.spans.some((span) => span.kind === kind));
|
|
1003
|
+
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 !== "");
|
|
1004
|
+
return /* @__PURE__ */ jsxs("mark", {
|
|
1005
|
+
"data-kind": kinds.join(" "),
|
|
1006
|
+
className: cn("bg-transparent text-inherit", ...kinds.map((kind) => STYLES[kind])),
|
|
1007
|
+
children: [segment.text, said.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
1008
|
+
className: "sr-only select-none",
|
|
1009
|
+
children: `, ${[...new Set(said)].join(", ")}`
|
|
1010
|
+
})]
|
|
1011
|
+
}, segment.start);
|
|
1012
|
+
})
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
//#endregion
|
|
888
1016
|
//#region src/components/ui/numberText.ts
|
|
889
1017
|
/**
|
|
890
1018
|
* A number at a fixed number of decimals, without a negative zero (`-0.000` reads as a sign
|
|
@@ -1074,17 +1202,6 @@ function UnitNumberInput({ unit, style, "aria-describedby": describedBy, ...rest
|
|
|
1074
1202
|
})]
|
|
1075
1203
|
});
|
|
1076
1204
|
}
|
|
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
1205
|
//#endregion
|
|
1089
1206
|
//#region src/components/ui/listboxModel.ts
|
|
1090
1207
|
/**
|
|
@@ -1361,6 +1478,52 @@ function ReadoutStrip({ items, className }) {
|
|
|
1361
1478
|
});
|
|
1362
1479
|
}
|
|
1363
1480
|
//#endregion
|
|
1481
|
+
//#region src/components/ui/Progress.tsx
|
|
1482
|
+
/** Counts written the same on the server and in every browser: `2,556`, `0.5`. */
|
|
1483
|
+
const count = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
1484
|
+
/**
|
|
1485
|
+
* The accessible value text of a progress: `"412 of 2,556"`, or `null` while indeterminate.
|
|
1486
|
+
*
|
|
1487
|
+
* @param value - Done so far; `null` or `undefined` for an unknown amount.
|
|
1488
|
+
* @param max - The whole. Defaults to 100.
|
|
1489
|
+
* @returns The text, with the value clamped to `[0, max]`.
|
|
1490
|
+
*/
|
|
1491
|
+
function progressText(value, max = 100) {
|
|
1492
|
+
if (value === null || value === void 0 || !Number.isFinite(value)) return null;
|
|
1493
|
+
const whole = max > 0 ? max : 100;
|
|
1494
|
+
return `${count.format(Math.min(Math.max(value, 0), whole))} of ${count.format(whole)}`;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* A progress bar: determinate when it has a `value` (a `max`, 100 by default), indeterminate
|
|
1498
|
+
* without one. Name it with `aria-label` or `aria-labelledby`.
|
|
1499
|
+
*
|
|
1500
|
+
* A `progressbar` with `aria-valuenow`, `aria-valuemax` and `aria-valuetext` ("412 of 2,556",
|
|
1501
|
+
* or your `valueText`); indeterminate, it has none of them, as the ARIA pattern asks.
|
|
1502
|
+
* The track follows the density in force. State is exposed as `data-state`
|
|
1503
|
+
* (`"indeterminate"`, `"loading"` or `"complete"`). With reduced motion the indeterminate bar
|
|
1504
|
+
* holds still.
|
|
1505
|
+
*/
|
|
1506
|
+
function Progress({ value, max = 100, valueText, className, ...rest }) {
|
|
1507
|
+
const density = useDensity();
|
|
1508
|
+
const whole = max > 0 ? max : 100;
|
|
1509
|
+
const text = progressText(value, whole);
|
|
1510
|
+
const done = text === null ? null : Math.min(Math.max(value, 0), whole);
|
|
1511
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1512
|
+
role: "progressbar",
|
|
1513
|
+
"aria-valuemin": done === null ? void 0 : 0,
|
|
1514
|
+
"aria-valuemax": done === null ? void 0 : whole,
|
|
1515
|
+
"aria-valuenow": done ?? void 0,
|
|
1516
|
+
"aria-valuetext": done === null ? void 0 : valueText ?? text ?? void 0,
|
|
1517
|
+
"data-state": done === null ? "indeterminate" : done >= whole ? "complete" : "loading",
|
|
1518
|
+
...rest,
|
|
1519
|
+
className: cn("relative w-full overflow-hidden rounded-full bg-line", byDensity(density, "h-1.5", "h-1"), className),
|
|
1520
|
+
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", {
|
|
1521
|
+
className: "h-full rounded-full bg-signal transition-[width] duration-300",
|
|
1522
|
+
style: { width: `${done / whole * 100}%` }
|
|
1523
|
+
})
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1364
1527
|
//#region src/components/ui/Popover.tsx
|
|
1365
1528
|
/**
|
|
1366
1529
|
* Floating content anchored to `trigger`, on the `overlay` surface with a shadow (floating
|
|
@@ -1854,7 +2017,68 @@ function sortRows(rows, columns, sort) {
|
|
|
1854
2017
|
return sortOrder(rows, columns, sort).map((index) => rows[index]);
|
|
1855
2018
|
}
|
|
1856
2019
|
//#endregion
|
|
2020
|
+
//#region src/components/ui/tableWindow.ts
|
|
2021
|
+
/**
|
|
2022
|
+
* The rows that intersect the visible part of the body, plus `overscan` on each side.
|
|
2023
|
+
* Degenerate input (no rows, a row height ≤ 0, NaN) gives an empty window.
|
|
2024
|
+
*/
|
|
2025
|
+
function rowWindow({ rowCount, rowHeight, scrollTop, viewportHeight, headerHeight, overscan }) {
|
|
2026
|
+
const count = Number.isFinite(rowCount) ? Math.max(0, Math.floor(rowCount)) : 0;
|
|
2027
|
+
if (count === 0 || !(rowHeight > 0)) return {
|
|
2028
|
+
start: 0,
|
|
2029
|
+
end: 0,
|
|
2030
|
+
before: 0,
|
|
2031
|
+
after: 0
|
|
2032
|
+
};
|
|
2033
|
+
const extra = Math.max(0, Math.floor(overscan) || 0);
|
|
2034
|
+
const top = Math.max(0, scrollTop || 0);
|
|
2035
|
+
const bottom = Math.max(top, top + Math.max(0, viewportHeight || 0) - Math.max(0, headerHeight || 0));
|
|
2036
|
+
const first = Math.min(count, Math.floor(top / rowHeight));
|
|
2037
|
+
const last = Math.min(count, Math.max(first + 1, Math.ceil(bottom / rowHeight)));
|
|
2038
|
+
const start = Math.max(0, first - extra);
|
|
2039
|
+
const end = Math.min(count, last + extra);
|
|
2040
|
+
return {
|
|
2041
|
+
start,
|
|
2042
|
+
end,
|
|
2043
|
+
before: start * rowHeight,
|
|
2044
|
+
after: (count - end) * rowHeight
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* The `scrollTop` that shows the row at `position` in full below the sticky header, moving
|
|
2049
|
+
* as little as possible: unchanged when it is already in view, else aligned to the nearer
|
|
2050
|
+
* edge. A viewport shorter than one row aligns the row's top.
|
|
2051
|
+
*/
|
|
2052
|
+
function scrollTopFor(position, { rowHeight, scrollTop, viewportHeight, headerHeight }) {
|
|
2053
|
+
const rowTop = position * rowHeight;
|
|
2054
|
+
const visible = viewportHeight - headerHeight;
|
|
2055
|
+
if (rowTop < scrollTop || visible < rowHeight) return rowTop;
|
|
2056
|
+
if (rowTop + rowHeight > scrollTop + visible) return rowTop + rowHeight - visible;
|
|
2057
|
+
return scrollTop;
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* The position a navigation key moves to from `position`, among `rowCount` rows, `page` rows
|
|
2061
|
+
* to a screen; `null` for a key that does not navigate.
|
|
2062
|
+
*/
|
|
2063
|
+
function navigate(key, position, rowCount, page) {
|
|
2064
|
+
const last = rowCount - 1;
|
|
2065
|
+
if (last < 0) return null;
|
|
2066
|
+
const step = Math.max(1, Math.floor(page));
|
|
2067
|
+
const clamp = (p) => Math.min(last, Math.max(0, p));
|
|
2068
|
+
switch (key) {
|
|
2069
|
+
case "ArrowDown": return clamp(position + 1);
|
|
2070
|
+
case "ArrowUp": return clamp(position - 1);
|
|
2071
|
+
case "PageDown": return clamp(position + step);
|
|
2072
|
+
case "PageUp": return clamp(position - step);
|
|
2073
|
+
case "Home": return 0;
|
|
2074
|
+
case "End": return last;
|
|
2075
|
+
default: return null;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
//#endregion
|
|
1857
2079
|
//#region src/components/ui/Table.tsx
|
|
2080
|
+
/** Rows per screen before a windowed table has measured itself (a server render, the first paint). */
|
|
2081
|
+
const INITIAL_ROWS = 20;
|
|
1858
2082
|
/** The header icon for a sort state: the direction when sorted, both arrows when only sortable. */
|
|
1859
2083
|
const SORT_ICONS = {
|
|
1860
2084
|
ascending: ArrowUp,
|
|
@@ -1875,8 +2099,12 @@ const SORT_ICONS = {
|
|
|
1875
2099
|
* `defaultSort`) or controlled (`sort` with `onSortChange`); with `manualSort` the headers
|
|
1876
2100
|
* show it and the rows render as given. Row callbacks always receive a row's index in `rows`,
|
|
1877
2101
|
* whatever the order on screen.
|
|
2102
|
+
*
|
|
2103
|
+
* Activatable rows also move focus with the arrow keys, Page Up / Page Down and Home / End.
|
|
2104
|
+
* With `windowed` the table scrolls itself (vertically, under a sticky header), renders only the
|
|
2105
|
+
* rows in view, and uses a fixed layout: give columns a `width` where the content needs it.
|
|
1878
2106
|
*/
|
|
1879
|
-
function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort = null, onSortChange, manualSort = false }) {
|
|
2107
|
+
function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort = null, onSortChange, manualSort = false, windowed }) {
|
|
1880
2108
|
const density = useDensity();
|
|
1881
2109
|
const [ownSort, setOwnSort] = useState(defaultSort);
|
|
1882
2110
|
const controlled = controlledSort !== void 0;
|
|
@@ -1892,71 +2120,272 @@ function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, classN
|
|
|
1892
2120
|
if (!controlled) setOwnSort(next);
|
|
1893
2121
|
onSortChange?.(next);
|
|
1894
2122
|
};
|
|
2123
|
+
const rowHeight = windowed?.rowHeight ?? 0;
|
|
2124
|
+
const scrollRef = useRef(null);
|
|
2125
|
+
const headRef = useRef(null);
|
|
2126
|
+
const bodyRef = useRef(null);
|
|
2127
|
+
const pendingFocus = useRef(null);
|
|
2128
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
2129
|
+
const [measured, setMeasured] = useState(null);
|
|
2130
|
+
const viewportHeight = measured?.viewport ?? windowed?.height ?? INITIAL_ROWS * rowHeight;
|
|
2131
|
+
const headerHeight = measured?.header ?? 0;
|
|
2132
|
+
const range = windowed ? rowWindow({
|
|
2133
|
+
rowCount: order.length,
|
|
2134
|
+
rowHeight,
|
|
2135
|
+
scrollTop,
|
|
2136
|
+
viewportHeight,
|
|
2137
|
+
headerHeight,
|
|
2138
|
+
overscan: windowed.overscan ?? 8
|
|
2139
|
+
}) : {
|
|
2140
|
+
start: 0,
|
|
2141
|
+
end: order.length,
|
|
2142
|
+
before: 0,
|
|
2143
|
+
after: 0
|
|
2144
|
+
};
|
|
2145
|
+
const isWindowed = windowed !== void 0;
|
|
2146
|
+
useLayoutEffect(() => {
|
|
2147
|
+
const scroller = scrollRef.current;
|
|
2148
|
+
if (!isWindowed || !scroller || typeof ResizeObserver === "undefined") return;
|
|
2149
|
+
const measure = () => {
|
|
2150
|
+
const next = {
|
|
2151
|
+
viewport: scroller.clientHeight,
|
|
2152
|
+
header: headRef.current?.offsetHeight ?? 0
|
|
2153
|
+
};
|
|
2154
|
+
setMeasured((last) => last?.viewport === next.viewport && last.header === next.header ? last : next);
|
|
2155
|
+
};
|
|
2156
|
+
measure();
|
|
2157
|
+
const observer = new ResizeObserver(measure);
|
|
2158
|
+
observer.observe(scroller);
|
|
2159
|
+
return () => observer.disconnect();
|
|
2160
|
+
}, [isWindowed]);
|
|
2161
|
+
useLayoutEffect(() => {
|
|
2162
|
+
const target = pendingFocus.current;
|
|
2163
|
+
if (target === null) return;
|
|
2164
|
+
const row = bodyRef.current?.querySelector(`tr[data-position="${target}"]`);
|
|
2165
|
+
if (!row) return;
|
|
2166
|
+
pendingFocus.current = null;
|
|
2167
|
+
row.focus({ preventScroll: isWindowed });
|
|
2168
|
+
});
|
|
2169
|
+
/** Moves focus from the row at `position` for a navigation key; `true` when the key was one. */
|
|
2170
|
+
const moveFocus = (key, position) => {
|
|
2171
|
+
const page = windowed ? Math.floor((viewportHeight - headerHeight) / rowHeight) : 10;
|
|
2172
|
+
const target = navigate(key, position, order.length, page);
|
|
2173
|
+
if (target === null) return false;
|
|
2174
|
+
pendingFocus.current = target;
|
|
2175
|
+
const scroller = scrollRef.current;
|
|
2176
|
+
if (windowed && scroller) {
|
|
2177
|
+
scroller.scrollTop = scrollTopFor(target, {
|
|
2178
|
+
rowHeight,
|
|
2179
|
+
scrollTop: scroller.scrollTop,
|
|
2180
|
+
viewportHeight,
|
|
2181
|
+
headerHeight
|
|
2182
|
+
});
|
|
2183
|
+
setScrollTop(scroller.scrollTop);
|
|
2184
|
+
}
|
|
2185
|
+
const row = bodyRef.current?.querySelector(`tr[data-position="${target}"]`);
|
|
2186
|
+
if (row && !windowed) {
|
|
2187
|
+
pendingFocus.current = null;
|
|
2188
|
+
row.focus();
|
|
2189
|
+
}
|
|
2190
|
+
return true;
|
|
2191
|
+
};
|
|
1895
2192
|
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
1896
2193
|
className: cn("text-center text-fg-muted", byDensity(density, "py-6 text-sm", "py-3 text-xs"), className),
|
|
1897
2194
|
children: empty
|
|
1898
2195
|
});
|
|
2196
|
+
const spacer = (height) => /* @__PURE__ */ jsx("tr", {
|
|
2197
|
+
"aria-hidden": "true",
|
|
2198
|
+
style: { height },
|
|
2199
|
+
children: /* @__PURE__ */ jsx("td", {
|
|
2200
|
+
colSpan: columns.length,
|
|
2201
|
+
className: "p-0"
|
|
2202
|
+
})
|
|
2203
|
+
});
|
|
2204
|
+
const scrollStop = windowed !== void 0 && !onRowClick && !columns.some(isSortable);
|
|
1899
2205
|
return /* @__PURE__ */ jsx("div", {
|
|
1900
|
-
|
|
2206
|
+
ref: scrollRef,
|
|
2207
|
+
className: cn("w-full", windowed ? scrollStop && "focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-signal" : "overflow-x-auto", className),
|
|
2208
|
+
style: windowed ? {
|
|
2209
|
+
overflow: "auto",
|
|
2210
|
+
height: windowed.height ?? "100%"
|
|
2211
|
+
} : void 0,
|
|
2212
|
+
onScroll: windowed ? (event) => setScrollTop(event.currentTarget.scrollTop) : void 0,
|
|
2213
|
+
...scrollStop ? {
|
|
2214
|
+
tabIndex: 0,
|
|
2215
|
+
role: "region",
|
|
2216
|
+
"aria-label": caption ?? "Table"
|
|
2217
|
+
} : {},
|
|
1901
2218
|
children: /* @__PURE__ */ jsxs("table", {
|
|
1902
2219
|
className: cn("w-full text-left", byDensity(density, "text-sm", "text-[11px]")),
|
|
2220
|
+
style: windowed ? { tableLayout: "fixed" } : void 0,
|
|
2221
|
+
"aria-rowcount": windowed ? order.length + 1 : void 0,
|
|
1903
2222
|
children: [
|
|
1904
2223
|
caption && /* @__PURE__ */ jsx("caption", {
|
|
1905
2224
|
className: "sr-only",
|
|
1906
2225
|
children: caption
|
|
1907
2226
|
}),
|
|
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
|
-
|
|
2227
|
+
/* @__PURE__ */ jsx("thead", {
|
|
2228
|
+
ref: headRef,
|
|
2229
|
+
children: /* @__PURE__ */ jsx("tr", {
|
|
2230
|
+
className: "border-b border-line",
|
|
2231
|
+
"aria-rowindex": windowed ? 1 : void 0,
|
|
2232
|
+
children: columns.map((column) => {
|
|
2233
|
+
const sortable = isSortable(column);
|
|
2234
|
+
const direction = sortable && sort?.key === column.key ? sort.direction : void 0;
|
|
2235
|
+
const SortIcon = SORT_ICONS[direction ?? "none"];
|
|
2236
|
+
return /* @__PURE__ */ jsx("th", {
|
|
2237
|
+
scope: "col",
|
|
2238
|
+
style: windowed ? {
|
|
2239
|
+
position: "sticky",
|
|
2240
|
+
top: 0,
|
|
2241
|
+
zIndex: 1,
|
|
2242
|
+
...column.width ? { width: column.width } : {}
|
|
2243
|
+
} : column.width ? { width: column.width } : void 0,
|
|
2244
|
+
"aria-sort": direction,
|
|
2245
|
+
"data-sort": sortable ? direction ?? "none" : void 0,
|
|
2246
|
+
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)]"),
|
|
2247
|
+
children: sortable ? /* @__PURE__ */ jsxs("button", {
|
|
2248
|
+
type: "button",
|
|
2249
|
+
onClick: () => changeSort(column),
|
|
2250
|
+
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"),
|
|
2251
|
+
children: [column.header, /* @__PURE__ */ jsx(SortIcon, {
|
|
2252
|
+
"aria-hidden": true,
|
|
2253
|
+
className: cn("shrink-0", byDensity(density, "size-3", "size-2.5"), !direction && "text-fg-subtle")
|
|
2254
|
+
})]
|
|
2255
|
+
}) : column.header
|
|
2256
|
+
}, column.key);
|
|
2257
|
+
})
|
|
1930
2258
|
})
|
|
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
|
-
|
|
2259
|
+
}),
|
|
2260
|
+
/* @__PURE__ */ jsxs("tbody", {
|
|
2261
|
+
ref: bodyRef,
|
|
2262
|
+
children: [
|
|
2263
|
+
range.before > 0 && spacer(range.before),
|
|
2264
|
+
order.slice(range.start, range.end).map((index, offset) => {
|
|
2265
|
+
const row = rows[index];
|
|
2266
|
+
const active = isRowActive?.(row, index) ?? false;
|
|
2267
|
+
const position = range.start + offset;
|
|
2268
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
2269
|
+
"data-position": position,
|
|
2270
|
+
"aria-rowindex": windowed ? position + 2 : void 0,
|
|
2271
|
+
style: windowed ? { height: rowHeight } : void 0,
|
|
2272
|
+
"data-state": active ? "active" : void 0,
|
|
2273
|
+
className: cn(!windowed && "border-b border-line/60 last:border-0", onRowClick && "cursor-pointer hover:bg-raised", active && "bg-signal/10"),
|
|
2274
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
2275
|
+
"aria-current": active ? true : void 0,
|
|
2276
|
+
onClick: onRowClick ? (event) => onRowClick(row, index, event) : void 0,
|
|
2277
|
+
onKeyDown: onRowClick ? (event) => {
|
|
2278
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2279
|
+
event.preventDefault();
|
|
2280
|
+
onRowClick(row, index, event);
|
|
2281
|
+
} else if (event.target === event.currentTarget && moveFocus(event.key, position)) event.preventDefault();
|
|
2282
|
+
} : void 0,
|
|
2283
|
+
onPointerEnter: onRowHover ? () => onRowHover(row, index) : void 0,
|
|
2284
|
+
onPointerLeave: onRowHover ? () => onRowHover(null, null) : void 0,
|
|
2285
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("td", {
|
|
2286
|
+
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)]"),
|
|
2287
|
+
style: windowed ? {
|
|
2288
|
+
whiteSpace: "nowrap",
|
|
2289
|
+
overflow: "hidden",
|
|
2290
|
+
textOverflow: "ellipsis"
|
|
2291
|
+
} : void 0,
|
|
2292
|
+
children: column.cell(row)
|
|
2293
|
+
}, column.key))
|
|
2294
|
+
}, rowKey(row, index));
|
|
2295
|
+
}),
|
|
2296
|
+
range.after > 0 && spacer(range.after)
|
|
2297
|
+
]
|
|
2298
|
+
})
|
|
1955
2299
|
]
|
|
1956
2300
|
})
|
|
1957
2301
|
});
|
|
1958
2302
|
}
|
|
1959
2303
|
//#endregion
|
|
2304
|
+
//#region src/components/ui/Textarea.tsx
|
|
2305
|
+
/** Sets a ref of either kind. */
|
|
2306
|
+
function assign(ref, value) {
|
|
2307
|
+
if (typeof ref === "function") ref(value);
|
|
2308
|
+
else if (ref) ref.current = value;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Fits a textarea's height to its content between `minRows` and `maxRows` lines, and scrolls
|
|
2312
|
+
* only past the cap.
|
|
2313
|
+
*/
|
|
2314
|
+
function fit(node, minRows, maxRows) {
|
|
2315
|
+
const style = getComputedStyle(node);
|
|
2316
|
+
const line = Number.parseFloat(style.lineHeight) || Number.parseFloat(style.fontSize) * 1.2 || 20;
|
|
2317
|
+
const border = Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth);
|
|
2318
|
+
const chrome = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom) + border;
|
|
2319
|
+
const min = minRows * line + chrome;
|
|
2320
|
+
const max = maxRows === void 0 ? Number.POSITIVE_INFINITY : Math.max(min, maxRows * line + chrome);
|
|
2321
|
+
node.style.height = `${min}px`;
|
|
2322
|
+
const content = node.scrollHeight + border;
|
|
2323
|
+
node.style.height = `${Math.min(Math.max(content, min), max)}px`;
|
|
2324
|
+
node.style.overflowY = content > max + .5 ? "auto" : "hidden";
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Multi-line text, on the shared control palette. Takes every `<textarea>` prop, `ref`
|
|
2328
|
+
* included; inside a `Field` it is described by the field's description and error, and
|
|
2329
|
+
* shown invalid while the field has an error.
|
|
2330
|
+
*
|
|
2331
|
+
* With `minRows` or `maxRows` it grows with its content between the two (and scrolls past
|
|
2332
|
+
* `maxRows`); without them it is `rows` tall and the reader can resize it. State is exposed
|
|
2333
|
+
* as `data-invalid`.
|
|
2334
|
+
*/
|
|
2335
|
+
function Textarea({ className, "aria-describedby": describedBy, invalid, mono = false, minRows, maxRows, rows, value, onChange, onValueChange, ref, ...rest }) {
|
|
2336
|
+
const field = useFieldDescription(describedBy);
|
|
2337
|
+
const isInvalid = invalid === true || field["aria-invalid"] === true;
|
|
2338
|
+
const grows = minRows !== void 0 || maxRows !== void 0;
|
|
2339
|
+
const floor = Math.max(1, Math.floor(minRows ?? 1));
|
|
2340
|
+
const cap = maxRows === void 0 ? void 0 : Math.max(floor, Math.floor(maxRows));
|
|
2341
|
+
const node = useRef(null);
|
|
2342
|
+
const setNode = useCallback((element) => {
|
|
2343
|
+
node.current = element;
|
|
2344
|
+
assign(ref, element);
|
|
2345
|
+
}, [ref]);
|
|
2346
|
+
useLayoutEffect(() => {
|
|
2347
|
+
if (grows && node.current) fit(node.current, floor, cap);
|
|
2348
|
+
}, [
|
|
2349
|
+
grows,
|
|
2350
|
+
floor,
|
|
2351
|
+
cap,
|
|
2352
|
+
mono,
|
|
2353
|
+
value
|
|
2354
|
+
]);
|
|
2355
|
+
useLayoutEffect(() => {
|
|
2356
|
+
const element = node.current;
|
|
2357
|
+
if (!grows || !element || typeof ResizeObserver === "undefined") return void 0;
|
|
2358
|
+
let width = element.clientWidth;
|
|
2359
|
+
const observer = new ResizeObserver(() => {
|
|
2360
|
+
if (element.clientWidth === width) return;
|
|
2361
|
+
width = element.clientWidth;
|
|
2362
|
+
fit(element, floor, cap);
|
|
2363
|
+
});
|
|
2364
|
+
observer.observe(element);
|
|
2365
|
+
return () => observer.disconnect();
|
|
2366
|
+
}, [
|
|
2367
|
+
grows,
|
|
2368
|
+
floor,
|
|
2369
|
+
cap
|
|
2370
|
+
]);
|
|
2371
|
+
const change = (event) => {
|
|
2372
|
+
onChange?.(event);
|
|
2373
|
+
onValueChange?.(event.currentTarget.value);
|
|
2374
|
+
if (grows) fit(event.currentTarget, floor, cap);
|
|
2375
|
+
};
|
|
2376
|
+
return /* @__PURE__ */ jsx("textarea", {
|
|
2377
|
+
...field,
|
|
2378
|
+
"aria-invalid": isInvalid || void 0,
|
|
2379
|
+
"data-invalid": isInvalid ? "" : void 0,
|
|
2380
|
+
...rest,
|
|
2381
|
+
...value === void 0 ? {} : { value },
|
|
2382
|
+
ref: setNode,
|
|
2383
|
+
rows: grows ? floor : rows,
|
|
2384
|
+
onChange: change,
|
|
2385
|
+
className: cn(controlClasses, "py-1.5", grows && "resize-none", mono && "font-mono", "data-invalid:border-defect data-invalid:hover:border-defect", className)
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
//#endregion
|
|
1960
2389
|
//#region src/components/ui/Toggle.tsx
|
|
1961
2390
|
/**
|
|
1962
2391
|
* A setting: changes how the thing behaves from now on, and reads as one ("show the mask").
|
|
@@ -2325,6 +2754,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
|
|
|
2325
2754
|
});
|
|
2326
2755
|
}
|
|
2327
2756
|
//#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 };
|
|
2757
|
+
export { Badge, 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, buttonClasses, byDensity, cn, controlClasses, createToastStore, defaultToastStore, findSpans, focusRing, focusRingInset, formatNumber, initTheme, inputClasses, parseNumber, progressText, readThemeChoice, resolveTheme, setThemeChoice, sortRows, toast, toneColor, useControlHeight, useDensity };
|
|
2329
2758
|
|
|
2330
2759
|
//# sourceMappingURL=index.js.map
|