@vitavision/ui 0.10.0 → 0.12.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 +2 -2
- package/dist/index.d.ts +114 -11
- package/dist/index.js +192 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ThemeToggle.tsx +2 -3
- package/src/components/ui/Button.tsx +4 -1
- package/src/components/ui/SegmentedControl.tsx +27 -1
- package/src/components/ui/Table.tsx +188 -65
- package/src/components/ui/ToggleChip.tsx +33 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/tableSort.ts +121 -0
- package/src/fonts/fonts.css +1 -1
- package/src/styles.css +3 -7
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The vitavision *instrument* design system: true-neutral greys, one accent, and verdict colours
|
|
4
4
|
reserved for verdicts. Tailwind v4 design tokens, a theme controller, and the React primitives
|
|
5
|
-
every
|
|
5
|
+
every vitavision app shares. React 19, no CSS-in-JS, no runtime theme engine.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -31,7 +31,7 @@ Each stylesheet ships **unprocessed** — Tailwind v4 *source*, resolved by your
|
|
|
31
31
|
declares its own `@source "./"`, which Tailwind resolves against that stylesheet: the package's
|
|
32
32
|
class names are found without an `@source` line of yours.
|
|
33
33
|
|
|
34
|
-
`fonts.css` serves the type pair, IBM Plex Sans and IBM Plex Mono
|
|
34
|
+
`fonts.css` serves the type pair, IBM Plex Sans and IBM Plex Mono, from files in this
|
|
35
35
|
package (SIL Open Font License). Its Plex Mono is IBM's own build, which keeps the `zero` feature,
|
|
36
36
|
so values get a slashed zero; `unicode-range` means a page fetches only the subsets its text
|
|
37
37
|
uses (about 80 KB for Latin). Drop any fontsource or Google Fonts Plex you loaded before — their
|
package/dist/index.d.ts
CHANGED
|
@@ -161,7 +161,10 @@ export declare function CountRun({ counts, className }: {
|
|
|
161
161
|
* destroys something.
|
|
162
162
|
*/
|
|
163
163
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
164
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* A control's height and padding, shared by `Button`, `SegmentedControl` and `ToggleChip`. For a
|
|
166
|
+
* button, `md` is the comfortable density's size and `sm` the compact one's.
|
|
167
|
+
*/
|
|
165
168
|
type ButtonSize = "sm" | "md";
|
|
166
169
|
/**
|
|
167
170
|
* The classes that make a control look like a button, for the one element that has to be
|
|
@@ -890,8 +893,11 @@ export declare function PoseInput({ value, onValueChange, rotationView, translat
|
|
|
890
893
|
* or `defaultValue` while `value` is `""` — and choosing the segment that matches
|
|
891
894
|
* `defaultValue` reports `""`, so the default stays defined in one place. Each segment
|
|
892
895
|
* carries `data-state="checked"` or `"unchecked"`; the group carries `data-disabled`.
|
|
896
|
+
*
|
|
897
|
+
* Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
|
|
898
|
+
* `Button`, pass `size="md"` and the strip's outer height matches theirs.
|
|
893
899
|
*/
|
|
894
|
-
export declare function SegmentedControl({ value, defaultValue, options, onValueChange, disabled, "aria-label": ariaLabel, className }: {
|
|
900
|
+
export declare function SegmentedControl({ value, defaultValue, options, onValueChange, disabled, "aria-label": ariaLabel, size, className }: {
|
|
895
901
|
/** `""` when the field is untouched. */
|
|
896
902
|
value: string;
|
|
897
903
|
/** What applies when nothing is sent. Highlighted while `value` is `""`. */
|
|
@@ -907,6 +913,13 @@ export declare function SegmentedControl({ value, defaultValue, options, onValue
|
|
|
907
913
|
disabled?: boolean | undefined;
|
|
908
914
|
/** Names the radio group. */
|
|
909
915
|
"aria-label"?: string | undefined;
|
|
916
|
+
/**
|
|
917
|
+
* The strip's height: `sm` (the default) is the toolbar size, about 30px with `text-xs`
|
|
918
|
+
* labels; `md` is exactly 32px (`h-8`) outside, border included, with `text-sm` labels, the
|
|
919
|
+
* height of a comfortable-density `Input` or `Button`. Not derived from the density in
|
|
920
|
+
* force, so a consumer that never passes it renders as it always did.
|
|
921
|
+
*/
|
|
922
|
+
size?: ButtonSize | undefined;
|
|
910
923
|
/** Merged with the strip's own classes through `cn`. */
|
|
911
924
|
className?: string | undefined;
|
|
912
925
|
}): import("react").JSX.Element;
|
|
@@ -985,6 +998,34 @@ export declare function Slider({ value, onValueChange, onValueCommit, min, max,
|
|
|
985
998
|
className?: string | undefined;
|
|
986
999
|
}): import("react").JSX.Element;
|
|
987
1000
|
//#endregion
|
|
1001
|
+
//#region src/components/ui/tableSort.d.ts
|
|
1002
|
+
/** Which way a column is sorted. Named as `aria-sort` names them: `"ascending"` puts the smallest first. */
|
|
1003
|
+
type SortDirection = "ascending" | "descending";
|
|
1004
|
+
/** The column a table is sorted by, and which way. Where a sort is expected, `null` means the rows' own order. */
|
|
1005
|
+
interface TableSort {
|
|
1006
|
+
/** The sorted column's `key`. */
|
|
1007
|
+
key: string;
|
|
1008
|
+
/** Which way. */
|
|
1009
|
+
direction: SortDirection;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* The rows in the order a `Table` with these columns shows them under `sort`.
|
|
1013
|
+
*
|
|
1014
|
+
* The sort is stable in both directions: rows that compare equal keep their order in `rows`.
|
|
1015
|
+
* With the column's `compare`, that function decides (reversed for `"descending"`). With its
|
|
1016
|
+
* `sortValue`, numbers compare by value and strings by a fixed English collation that reads
|
|
1017
|
+
* digit runs as numbers ("frame 9" before "frame 10"), the same on a server as in any
|
|
1018
|
+
* browser; numbers come before strings; and `null`, `undefined` and `NaN` go last in both
|
|
1019
|
+
* directions. A `null` sort, a `key` that names no column, and a column that is not sortable or
|
|
1020
|
+
* has neither function leave the order as given.
|
|
1021
|
+
*
|
|
1022
|
+
* @param rows - The rows, in their own order. Not modified.
|
|
1023
|
+
* @param columns - The table's columns; only `key`, `sortable`, `sortValue` and `compare` are read.
|
|
1024
|
+
* @param sort - The column and direction, or `null` for the rows' own order.
|
|
1025
|
+
* @returns A new array of the same rows, sorted.
|
|
1026
|
+
*/
|
|
1027
|
+
export declare function sortRows<Row>(rows: readonly Row[], columns: readonly Pick<Column<Row>, "key" | "sortable" | "sortValue" | "compare">[], sort: TableSort | null | undefined): Row[];
|
|
1028
|
+
//#endregion
|
|
988
1029
|
//#region src/components/ui/Table.d.ts
|
|
989
1030
|
/** One column of a `Table`: its header and how to render a row's cell. */
|
|
990
1031
|
type Column<Row> = {
|
|
@@ -998,6 +1039,30 @@ type Column<Row> = {
|
|
|
998
1039
|
width?: string;
|
|
999
1040
|
/** The cell's content for a row. */
|
|
1000
1041
|
cell: (row: Row) => ReactNode;
|
|
1042
|
+
/**
|
|
1043
|
+
* Whether the header sorts the table. Defaults to `true` when the column has a `sortValue`
|
|
1044
|
+
* or a `compare`. Set it `true` with neither for a column the app sorts itself (see the
|
|
1045
|
+
* table's `manualSort`), or `false` to keep a column with a `sortValue` unsortable.
|
|
1046
|
+
*/
|
|
1047
|
+
sortable?: boolean | undefined;
|
|
1048
|
+
/**
|
|
1049
|
+
* The value the column sorts by. Numbers compare by value; strings by a fixed English
|
|
1050
|
+
* collation that reads digit runs as numbers ("frame 9" before "frame 10"), so a server
|
|
1051
|
+
* render and the browser agree; numbers come before strings. `null`, `undefined` and `NaN`
|
|
1052
|
+
* go last in both directions.
|
|
1053
|
+
*/
|
|
1054
|
+
sortValue?: ((row: Row) => number | string | null | undefined) | undefined;
|
|
1055
|
+
/**
|
|
1056
|
+
* A full ascending comparison, for an order one value cannot express (two fields, a rank):
|
|
1057
|
+
* negative when `a` comes first. Takes precedence over `sortValue`. Reversed for
|
|
1058
|
+
* `"descending"`; rows that compare equal keep their order in `rows` either way.
|
|
1059
|
+
*/
|
|
1060
|
+
compare?: ((a: Row, b: Row) => number) | undefined;
|
|
1061
|
+
/**
|
|
1062
|
+
* The direction of the first click on the header. Defaults to `"ascending"`; `"descending"`
|
|
1063
|
+
* suits a score, where the best comes first.
|
|
1064
|
+
*/
|
|
1065
|
+
firstSort?: SortDirection | undefined;
|
|
1001
1066
|
};
|
|
1002
1067
|
/**
|
|
1003
1068
|
* A table of rows, with numeric columns set in mono and right-aligned.
|
|
@@ -1005,13 +1070,21 @@ type Column<Row> = {
|
|
|
1005
1070
|
* With `onRowClick` the rows are keyboard-reachable (Tab, then Enter or Space) while keeping
|
|
1006
1071
|
* their table semantics. The active row carries `aria-current` and `data-state="active"`;
|
|
1007
1072
|
* padding follows the density in force. With no rows it renders the `empty` message instead.
|
|
1008
|
-
|
|
1009
|
-
|
|
1073
|
+
*
|
|
1074
|
+
* A column with a `sortValue` or a `compare` gets a header button: each click moves its sort
|
|
1075
|
+
* from the column's `firstSort` direction to the other one, then back to the rows' own order.
|
|
1076
|
+
* The sorted header carries `aria-sort` and `data-sort` (`"ascending"` or `"descending"`);
|
|
1077
|
+
* other sortable headers carry `data-sort="none"`. The sort is kept by the table (from
|
|
1078
|
+
* `defaultSort`) or controlled (`sort` with `onSortChange`); with `manualSort` the headers
|
|
1079
|
+
* show it and the rows render as given. Row callbacks always receive a row's index in `rows`,
|
|
1080
|
+
* whatever the order on screen.
|
|
1081
|
+
*/
|
|
1082
|
+
export declare function Table<Row>({ columns, rows, rowKey, empty, caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort, onSortChange, manualSort }: {
|
|
1010
1083
|
/** The columns, in order. */
|
|
1011
1084
|
columns: Column<Row>[];
|
|
1012
|
-
/** The rows, in order. */
|
|
1085
|
+
/** The rows, in their own order: the order shown while the table is not sorted. */
|
|
1013
1086
|
rows: Row[];
|
|
1014
|
-
/** A stable key per row. */
|
|
1087
|
+
/** A stable key per row. `index` is the row's position in `rows`, whatever the sort. */
|
|
1015
1088
|
rowKey: (row: Row, index: number) => string | number;
|
|
1016
1089
|
/** Shown instead of the table when there are no rows. Defaults to "Nothing here.". */
|
|
1017
1090
|
empty?: ReactNode;
|
|
@@ -1022,14 +1095,33 @@ export declare function Table<Row>({ columns, rows, rowKey, empty, caption, clas
|
|
|
1022
1095
|
/** Makes rows activatable. Keyboard-reachable, so a table used as a list of
|
|
1023
1096
|
* destinations is not a mouse-only control. The event is passed so a caller can read
|
|
1024
1097
|
* modifiers — a list kept in step with a canvas needs shift-range and meta-toggle to mean
|
|
1025
|
-
* the same thing in both places.
|
|
1098
|
+
* the same thing in both places. `index` is the row's position in `rows`, whatever the
|
|
1099
|
+
* sort. */
|
|
1026
1100
|
onRowClick?: (row: Row, index: number, event: MouseEvent<HTMLTableRowElement> | KeyboardEvent<HTMLTableRowElement>) => void;
|
|
1027
1101
|
/** Marks the row that is current — the selected match, the open frame. Rendered as
|
|
1028
|
-
* `aria-current`, so it is announced rather than only tinted.
|
|
1102
|
+
* `aria-current`, so it is announced rather than only tinted. `index` is the row's
|
|
1103
|
+
* position in `rows`, whatever the sort. */
|
|
1029
1104
|
isRowActive?: (row: Row, index: number) => boolean;
|
|
1030
1105
|
/** Pointer enter/leave, for tables kept in step with a canvas overlay.
|
|
1031
|
-
* `null` on leave. */
|
|
1106
|
+
* `null` on leave. `index` is the row's position in `rows`, whatever the sort. */
|
|
1032
1107
|
onRowHover?: (row: Row | null, index: number | null) => void;
|
|
1108
|
+
/**
|
|
1109
|
+
* The sort, controlled: a column `key` and a direction, or `null` for the rows' own order.
|
|
1110
|
+
* Leave it undefined and the table keeps the sort itself, starting from `defaultSort`.
|
|
1111
|
+
*/
|
|
1112
|
+
sort?: TableSort | null | undefined;
|
|
1113
|
+
/** The sort the table starts with when `sort` is not given. Defaults to `null`, the rows' own order. */
|
|
1114
|
+
defaultSort?: TableSort | null | undefined;
|
|
1115
|
+
/**
|
|
1116
|
+
* Called on a header click with the new sort: the column's `firstSort` direction, then the
|
|
1117
|
+
* other, then `null`. A click on another column starts that column at its `firstSort`.
|
|
1118
|
+
*/
|
|
1119
|
+
onSortChange?: ((sort: TableSort | null) => void) | undefined;
|
|
1120
|
+
/**
|
|
1121
|
+
* The app orders the rows (on a server, say): the headers show the sort and report clicks,
|
|
1122
|
+
* and the rows render exactly as given. Mark the columns it can sort `sortable: true`.
|
|
1123
|
+
*/
|
|
1124
|
+
manualSort?: boolean | undefined;
|
|
1033
1125
|
}): import("react").JSX.Element;
|
|
1034
1126
|
//#endregion
|
|
1035
1127
|
//#region src/components/ui/Toggle.d.ts
|
|
@@ -1091,8 +1183,11 @@ export declare function Checkbox({ checked, onCheckedChange, onClick, label, des
|
|
|
1091
1183
|
* colour: a toolbar-sized `Switch` (`role="switch"`, `aria-checked`).
|
|
1092
1184
|
*
|
|
1093
1185
|
* Controlled. State is exposed as `data-state` (`checked`/`unchecked`).
|
|
1186
|
+
*
|
|
1187
|
+
* Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
|
|
1188
|
+
* `Button`, pass `size="md"` and the chip's outer height matches theirs.
|
|
1094
1189
|
*/
|
|
1095
|
-
export declare function ToggleChip({ checked, onCheckedChange, children, swatch, disabled, title, className }: {
|
|
1190
|
+
export declare function ToggleChip({ checked, onCheckedChange, children, swatch, disabled, title, size, className }: {
|
|
1096
1191
|
/** Whether the layer is shown. */
|
|
1097
1192
|
checked: boolean;
|
|
1098
1193
|
/** Called with the new state. */
|
|
@@ -1105,6 +1200,14 @@ export declare function ToggleChip({ checked, onCheckedChange, children, swatch,
|
|
|
1105
1200
|
disabled?: boolean | undefined;
|
|
1106
1201
|
/** Why the control is disabled, or what the layer is. Never the only explanation. */
|
|
1107
1202
|
title?: string | undefined;
|
|
1203
|
+
/**
|
|
1204
|
+
* The chip's height: `sm` (the default) is the toolbar size, about 26px with `text-xs`
|
|
1205
|
+
* text; `md` is exactly 32px (`h-8`) outside, border included, with `text-sm` text and a
|
|
1206
|
+
* slightly larger swatch, the height of a comfortable-density `Input` or `Button`. Not
|
|
1207
|
+
* derived from the density in force, so a consumer that never passes it renders as it
|
|
1208
|
+
* always did.
|
|
1209
|
+
*/
|
|
1210
|
+
size?: ButtonSize | undefined;
|
|
1108
1211
|
/** Merged with the chip's own classes through `cn`. */
|
|
1109
1212
|
className?: string | undefined;
|
|
1110
1213
|
}): import("react").JSX.Element;
|
|
@@ -1319,5 +1422,5 @@ export declare function Tabs<Id extends string>({ items, active, onSelect, label
|
|
|
1319
1422
|
className?: string | undefined;
|
|
1320
1423
|
}): import("react").JSX.Element;
|
|
1321
1424
|
//#endregion
|
|
1322
|
-
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, DropdownMenuProps, ListboxOption, ListboxOptionState, ListboxProps, MeasureTone, MenuCheckboxItemProps, MenuItemProps, NumberInputProps, PopoverProps, PoseInputProps, PoseValue, Quaternion, ReadoutItem, RotationView, SelectOption, TabItem, ThemeChoice, ToastOptions, ToastRecord, ToastStore, ToastTone, ToasterProps, Tone, Vec3, VectorInputProps };
|
|
1425
|
+
export type { BackLink, ButtonSize, ButtonVariant, CalloutTone, Column, Density, DropdownMenuProps, ListboxOption, ListboxOptionState, ListboxProps, MeasureTone, MenuCheckboxItemProps, MenuItemProps, NumberInputProps, PopoverProps, PoseInputProps, PoseValue, Quaternion, ReadoutItem, RotationView, SelectOption, SortDirection, TabItem, TableSort, ThemeChoice, ToastOptions, ToastRecord, ToastStore, ToastTone, ToasterProps, Tone, Vec3, VectorInputProps };
|
|
1323
1426
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertTriangle, Check, CheckCircle2, ChevronDown, ChevronRight, CircleCheck, CircleX, HelpCircle, Info, Loader2, Minus, Moon, Sun, SunMoon, TriangleAlert, X, XCircle } from "lucide-react";
|
|
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
2
|
import { createContext, isValidElement, use, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import * as RadixTooltip from "@radix-ui/react-tooltip";
|
|
4
4
|
import { clsx } from "clsx";
|
|
@@ -353,7 +353,7 @@ const VARIANT_CLASSES = {
|
|
|
353
353
|
ghost: "text-fg-muted hover:bg-raised hover:text-fg",
|
|
354
354
|
danger: "bg-defect/10 text-defect ring-1 ring-inset ring-defect/30 hover:bg-defect/15"
|
|
355
355
|
};
|
|
356
|
-
const SIZE_CLASSES = {
|
|
356
|
+
const SIZE_CLASSES$1 = {
|
|
357
357
|
sm: "h-7 gap-1.5 px-2.5 text-xs",
|
|
358
358
|
md: "h-8 gap-2 px-3 text-sm"
|
|
359
359
|
};
|
|
@@ -367,7 +367,7 @@ const SIZE_CLASSES = {
|
|
|
367
367
|
* @returns The class string.
|
|
368
368
|
*/
|
|
369
369
|
function buttonClasses({ variant = "secondary", size = "md", className } = {}) {
|
|
370
|
-
return cn("inline-flex shrink-0 items-center justify-center rounded-control font-medium transition-colors", "disabled:cursor-not-allowed disabled:opacity-45", SIZE_CLASSES[size], VARIANT_CLASSES[variant], focusRing, className);
|
|
370
|
+
return cn("inline-flex shrink-0 items-center justify-center rounded-control font-medium transition-colors", "disabled:cursor-not-allowed disabled:opacity-45", SIZE_CLASSES$1[size], VARIANT_CLASSES[variant], focusRing, className);
|
|
371
371
|
}
|
|
372
372
|
function ButtonIcon({ icon }) {
|
|
373
373
|
return /* @__PURE__ */ jsx("span", {
|
|
@@ -1621,6 +1621,16 @@ function PoseInput({ value, onValueChange, rotationView, translationUnit = "m",
|
|
|
1621
1621
|
//#endregion
|
|
1622
1622
|
//#region src/components/ui/SegmentedControl.tsx
|
|
1623
1623
|
/**
|
|
1624
|
+
* The segment's own box, by size. `sm` is the toolbar size and the default. `md` sets the
|
|
1625
|
+
* height explicitly rather than leaving it to padding plus line-height: the strip around it
|
|
1626
|
+
* is `h-8` with a 1px border and `p-0.5`, which leaves 26px of content box, and `h-full`
|
|
1627
|
+
* fills exactly that whatever the font's line height turns out to be.
|
|
1628
|
+
*/
|
|
1629
|
+
const SEGMENT_SIZE_CLASSES = {
|
|
1630
|
+
sm: "px-2.5 py-1 text-xs",
|
|
1631
|
+
md: "flex h-full items-center px-3.5 text-sm"
|
|
1632
|
+
};
|
|
1633
|
+
/**
|
|
1624
1634
|
* A choice among two or three, shown as a strip of segments (native radios in one group, so
|
|
1625
1635
|
* arrow keys move between them).
|
|
1626
1636
|
*
|
|
@@ -1628,8 +1638,11 @@ function PoseInput({ value, onValueChange, rotationView, translationUnit = "m",
|
|
|
1628
1638
|
* or `defaultValue` while `value` is `""` — and choosing the segment that matches
|
|
1629
1639
|
* `defaultValue` reports `""`, so the default stays defined in one place. Each segment
|
|
1630
1640
|
* carries `data-state="checked"` or `"unchecked"`; the group carries `data-disabled`.
|
|
1641
|
+
*
|
|
1642
|
+
* Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
|
|
1643
|
+
* `Button`, pass `size="md"` and the strip's outer height matches theirs.
|
|
1631
1644
|
*/
|
|
1632
|
-
function SegmentedControl({ value, defaultValue, options, onValueChange, disabled = false, "aria-label": ariaLabel, className }) {
|
|
1645
|
+
function SegmentedControl({ value, defaultValue, options, onValueChange, disabled = false, "aria-label": ariaLabel, size = "sm", className }) {
|
|
1633
1646
|
const nameId = useId();
|
|
1634
1647
|
const described = useFieldDescription();
|
|
1635
1648
|
const effective = value === "" ? defaultValue : value;
|
|
@@ -1638,12 +1651,12 @@ function SegmentedControl({ value, defaultValue, options, onValueChange, disable
|
|
|
1638
1651
|
"aria-label": ariaLabel,
|
|
1639
1652
|
"aria-describedby": described["aria-describedby"],
|
|
1640
1653
|
"data-disabled": disabled ? "" : void 0,
|
|
1641
|
-
className: cn("inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-control border border-line-strong bg-raised p-0.5", disabled && "opacity-50", className),
|
|
1654
|
+
className: cn("inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-control border border-line-strong bg-raised p-0.5", size === "md" && "h-8", disabled && "opacity-50", className),
|
|
1642
1655
|
children: options.map((option) => {
|
|
1643
1656
|
const checked = option.value === effective;
|
|
1644
1657
|
return /* @__PURE__ */ jsxs("label", {
|
|
1645
1658
|
"data-state": checked ? "checked" : "unchecked",
|
|
1646
|
-
className: cn("relative cursor-pointer whitespace-nowrap rounded-[0.3rem]
|
|
1659
|
+
className: cn("relative cursor-pointer whitespace-nowrap rounded-[0.3rem]", SEGMENT_SIZE_CLASSES[size], "font-medium transition-colors", "has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-signal", checked ? "bg-surface text-fg shadow-sm" : "text-fg-muted hover:text-fg", disabled && "cursor-not-allowed"),
|
|
1647
1660
|
children: [/* @__PURE__ */ jsx("input", {
|
|
1648
1661
|
type: "radio",
|
|
1649
1662
|
name: nameId,
|
|
@@ -1762,16 +1775,123 @@ function Slider({ value, onValueChange, onValueCommit, min = 0, max = 1, step =
|
|
|
1762
1775
|
});
|
|
1763
1776
|
}
|
|
1764
1777
|
//#endregion
|
|
1778
|
+
//#region src/components/ui/tableSort.ts
|
|
1779
|
+
/** Strings by a fixed English collation that reads digit runs as numbers: "frame 9" before "frame 10". */
|
|
1780
|
+
const collator = new Intl.Collator("en", { numeric: true });
|
|
1781
|
+
/**
|
|
1782
|
+
* Whether a column's header sorts the table: `sortable` if given, otherwise whether the column
|
|
1783
|
+
* says how to order (`sortValue` or `compare`).
|
|
1784
|
+
*/
|
|
1785
|
+
function isSortable(column) {
|
|
1786
|
+
return column.sortable ?? (column.sortValue !== void 0 || column.compare !== void 0);
|
|
1787
|
+
}
|
|
1788
|
+
/** The sort after a click on `column`'s header: its first direction, then the other, then none. */
|
|
1789
|
+
function nextSort(current, column) {
|
|
1790
|
+
const first = column.firstSort ?? "ascending";
|
|
1791
|
+
if (current?.key !== column.key) return {
|
|
1792
|
+
key: column.key,
|
|
1793
|
+
direction: first
|
|
1794
|
+
};
|
|
1795
|
+
if (current.direction !== first) return null;
|
|
1796
|
+
return {
|
|
1797
|
+
key: column.key,
|
|
1798
|
+
direction: first === "ascending" ? "descending" : "ascending"
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
/** A value with no place in the order: `null`, `undefined` or `NaN`. */
|
|
1802
|
+
function missing(value) {
|
|
1803
|
+
return value === null || value === void 0 || Number.isNaN(value);
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Two present values in ascending order: numbers by value (compared, not subtracted, so two
|
|
1807
|
+
* infinities tie), strings by the collator, and every number before every string.
|
|
1808
|
+
*/
|
|
1809
|
+
function compareValues(a, b) {
|
|
1810
|
+
if (typeof a === "number" && typeof b === "number") return a < b ? -1 : a > b ? 1 : 0;
|
|
1811
|
+
if (typeof a === "string" && typeof b === "string") return collator.compare(a, b);
|
|
1812
|
+
return typeof a === "number" ? -1 : 1;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* The display order of `rows` under `sort`, as indices into `rows`. `Table` renders through it
|
|
1816
|
+
* so each row keeps its original index for the row callbacks.
|
|
1817
|
+
*/
|
|
1818
|
+
function sortOrder(rows, columns, sort) {
|
|
1819
|
+
const order = rows.map((_, index) => index);
|
|
1820
|
+
if (!sort) return order;
|
|
1821
|
+
const column = columns.find((candidate) => candidate.key === sort.key);
|
|
1822
|
+
if (!column || !isSortable(column)) return order;
|
|
1823
|
+
const sign = sort.direction === "descending" ? -1 : 1;
|
|
1824
|
+
const { compare, sortValue } = column;
|
|
1825
|
+
if (compare) return order.sort((a, b) => sign * compare(rows[a], rows[b]) || a - b);
|
|
1826
|
+
if (!sortValue) return order;
|
|
1827
|
+
const values = rows.map((row) => sortValue(row));
|
|
1828
|
+
return order.sort((a, b) => {
|
|
1829
|
+
const left = values[a];
|
|
1830
|
+
const right = values[b];
|
|
1831
|
+
const leftMissing = missing(left);
|
|
1832
|
+
const rightMissing = missing(right);
|
|
1833
|
+
if (leftMissing || rightMissing) return leftMissing === rightMissing ? a - b : leftMissing ? 1 : -1;
|
|
1834
|
+
return sign * compareValues(left, right) || a - b;
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* The rows in the order a `Table` with these columns shows them under `sort`.
|
|
1839
|
+
*
|
|
1840
|
+
* The sort is stable in both directions: rows that compare equal keep their order in `rows`.
|
|
1841
|
+
* With the column's `compare`, that function decides (reversed for `"descending"`). With its
|
|
1842
|
+
* `sortValue`, numbers compare by value and strings by a fixed English collation that reads
|
|
1843
|
+
* digit runs as numbers ("frame 9" before "frame 10"), the same on a server as in any
|
|
1844
|
+
* browser; numbers come before strings; and `null`, `undefined` and `NaN` go last in both
|
|
1845
|
+
* directions. A `null` sort, a `key` that names no column, and a column that is not sortable or
|
|
1846
|
+
* has neither function leave the order as given.
|
|
1847
|
+
*
|
|
1848
|
+
* @param rows - The rows, in their own order. Not modified.
|
|
1849
|
+
* @param columns - The table's columns; only `key`, `sortable`, `sortValue` and `compare` are read.
|
|
1850
|
+
* @param sort - The column and direction, or `null` for the rows' own order.
|
|
1851
|
+
* @returns A new array of the same rows, sorted.
|
|
1852
|
+
*/
|
|
1853
|
+
function sortRows(rows, columns, sort) {
|
|
1854
|
+
return sortOrder(rows, columns, sort).map((index) => rows[index]);
|
|
1855
|
+
}
|
|
1856
|
+
//#endregion
|
|
1765
1857
|
//#region src/components/ui/Table.tsx
|
|
1858
|
+
/** The header icon for a sort state: the direction when sorted, both arrows when only sortable. */
|
|
1859
|
+
const SORT_ICONS = {
|
|
1860
|
+
ascending: ArrowUp,
|
|
1861
|
+
descending: ArrowDown,
|
|
1862
|
+
none: ArrowUpDown
|
|
1863
|
+
};
|
|
1766
1864
|
/**
|
|
1767
1865
|
* A table of rows, with numeric columns set in mono and right-aligned.
|
|
1768
1866
|
*
|
|
1769
1867
|
* With `onRowClick` the rows are keyboard-reachable (Tab, then Enter or Space) while keeping
|
|
1770
1868
|
* their table semantics. The active row carries `aria-current` and `data-state="active"`;
|
|
1771
1869
|
* padding follows the density in force. With no rows it renders the `empty` message instead.
|
|
1772
|
-
|
|
1773
|
-
|
|
1870
|
+
*
|
|
1871
|
+
* A column with a `sortValue` or a `compare` gets a header button: each click moves its sort
|
|
1872
|
+
* from the column's `firstSort` direction to the other one, then back to the rows' own order.
|
|
1873
|
+
* The sorted header carries `aria-sort` and `data-sort` (`"ascending"` or `"descending"`);
|
|
1874
|
+
* other sortable headers carry `data-sort="none"`. The sort is kept by the table (from
|
|
1875
|
+
* `defaultSort`) or controlled (`sort` with `onSortChange`); with `manualSort` the headers
|
|
1876
|
+
* show it and the rows render as given. Row callbacks always receive a row's index in `rows`,
|
|
1877
|
+
* whatever the order on screen.
|
|
1878
|
+
*/
|
|
1879
|
+
function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, className, onRowClick, isRowActive, onRowHover, sort: controlledSort, defaultSort = null, onSortChange, manualSort = false }) {
|
|
1774
1880
|
const density = useDensity();
|
|
1881
|
+
const [ownSort, setOwnSort] = useState(defaultSort);
|
|
1882
|
+
const controlled = controlledSort !== void 0;
|
|
1883
|
+
const sort = controlled ? controlledSort : ownSort;
|
|
1884
|
+
const order = useMemo(() => manualSort ? rows.map((_, index) => index) : sortOrder(rows, columns, sort), [
|
|
1885
|
+
rows,
|
|
1886
|
+
columns,
|
|
1887
|
+
sort,
|
|
1888
|
+
manualSort
|
|
1889
|
+
]);
|
|
1890
|
+
const changeSort = (column) => {
|
|
1891
|
+
const next = nextSort(sort, column);
|
|
1892
|
+
if (!controlled) setOwnSort(next);
|
|
1893
|
+
onSortChange?.(next);
|
|
1894
|
+
};
|
|
1775
1895
|
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
1776
1896
|
className: cn("text-center text-fg-muted", byDensity(density, "py-6 text-sm", "py-3 text-xs"), className),
|
|
1777
1897
|
children: empty
|
|
@@ -1787,32 +1907,51 @@ function Table({ columns, rows, rowKey, empty = "Nothing here.", caption, classN
|
|
|
1787
1907
|
}),
|
|
1788
1908
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
1789
1909
|
className: "border-b border-line",
|
|
1790
|
-
children: columns.map((column) =>
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1910
|
+
children: columns.map((column) => {
|
|
1911
|
+
const sortable = isSortable(column);
|
|
1912
|
+
const direction = sortable && sort?.key === column.key ? sort.direction : void 0;
|
|
1913
|
+
const SortIcon = SORT_ICONS[direction ?? "none"];
|
|
1914
|
+
return /* @__PURE__ */ jsx("th", {
|
|
1915
|
+
scope: "col",
|
|
1916
|
+
style: column.width ? { width: column.width } : void 0,
|
|
1917
|
+
"aria-sort": direction,
|
|
1918
|
+
"data-sort": sortable ? direction ?? "none" : void 0,
|
|
1919
|
+
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"),
|
|
1920
|
+
children: sortable ? /* @__PURE__ */ jsxs("button", {
|
|
1921
|
+
type: "button",
|
|
1922
|
+
onClick: () => changeSort(column),
|
|
1923
|
+
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"),
|
|
1924
|
+
children: [column.header, /* @__PURE__ */ jsx(SortIcon, {
|
|
1925
|
+
"aria-hidden": true,
|
|
1926
|
+
className: cn("shrink-0", byDensity(density, "size-3", "size-2.5"), !direction && "text-fg-subtle")
|
|
1927
|
+
})]
|
|
1928
|
+
}) : column.header
|
|
1929
|
+
}, column.key);
|
|
1930
|
+
})
|
|
1796
1931
|
}) }),
|
|
1797
|
-
/* @__PURE__ */ jsx("tbody", { children:
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1932
|
+
/* @__PURE__ */ jsx("tbody", { children: order.map((index) => {
|
|
1933
|
+
const row = rows[index];
|
|
1934
|
+
const active = isRowActive?.(row, index) ?? false;
|
|
1935
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
1936
|
+
"data-state": active ? "active" : void 0,
|
|
1937
|
+
className: cn("border-b border-line/60 last:border-0", onRowClick && "cursor-pointer hover:bg-raised", active && "bg-signal/10"),
|
|
1938
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
1939
|
+
"aria-current": active ? true : void 0,
|
|
1940
|
+
onClick: onRowClick ? (event) => onRowClick(row, index, event) : void 0,
|
|
1941
|
+
onKeyDown: onRowClick ? (event) => {
|
|
1942
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1943
|
+
event.preventDefault();
|
|
1944
|
+
onRowClick(row, index, event);
|
|
1945
|
+
}
|
|
1946
|
+
} : void 0,
|
|
1947
|
+
onPointerEnter: onRowHover ? () => onRowHover(row, index) : void 0,
|
|
1948
|
+
onPointerLeave: onRowHover ? () => onRowHover(null, null) : void 0,
|
|
1949
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("td", {
|
|
1950
|
+
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"),
|
|
1951
|
+
children: column.cell(row)
|
|
1952
|
+
}, column.key))
|
|
1953
|
+
}, rowKey(row, index));
|
|
1954
|
+
}) })
|
|
1816
1955
|
]
|
|
1817
1956
|
})
|
|
1818
1957
|
});
|
|
@@ -1901,12 +2040,28 @@ function Checkbox({ checked, onCheckedChange, onClick, label, description, disab
|
|
|
1901
2040
|
//#endregion
|
|
1902
2041
|
//#region src/components/ui/ToggleChip.tsx
|
|
1903
2042
|
/**
|
|
2043
|
+
* The chip's box, by size. `sm` is the toolbar size and the default. `md` sets the height
|
|
2044
|
+
* explicitly (`h-8`, border included) rather than leaving it to padding plus line-height.
|
|
2045
|
+
*/
|
|
2046
|
+
const SIZE_CLASSES = {
|
|
2047
|
+
sm: "px-2 py-1 text-xs",
|
|
2048
|
+
md: "h-8 px-3 text-sm"
|
|
2049
|
+
};
|
|
2050
|
+
/** The swatch dot, by size: a little larger beside `text-sm` so it keeps its weight against the label. */
|
|
2051
|
+
const SWATCH_SIZE_CLASSES = {
|
|
2052
|
+
sm: "size-2",
|
|
2053
|
+
md: "size-2.5"
|
|
2054
|
+
};
|
|
2055
|
+
/**
|
|
1904
2056
|
* A dense on/off chip for a layer drawn on screen, with an optional swatch in the layer's
|
|
1905
2057
|
* colour: a toolbar-sized `Switch` (`role="switch"`, `aria-checked`).
|
|
1906
2058
|
*
|
|
1907
2059
|
* Controlled. State is exposed as `data-state` (`checked`/`unchecked`).
|
|
2060
|
+
*
|
|
2061
|
+
* Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
|
|
2062
|
+
* `Button`, pass `size="md"` and the chip's outer height matches theirs.
|
|
1908
2063
|
*/
|
|
1909
|
-
function ToggleChip({ checked, onCheckedChange, children, swatch, disabled = false, title, className }) {
|
|
2064
|
+
function ToggleChip({ checked, onCheckedChange, children, swatch, disabled = false, title, size = "sm", className }) {
|
|
1910
2065
|
return /* @__PURE__ */ jsxs("button", {
|
|
1911
2066
|
type: "button",
|
|
1912
2067
|
role: "switch",
|
|
@@ -1915,10 +2070,10 @@ function ToggleChip({ checked, onCheckedChange, children, swatch, disabled = fal
|
|
|
1915
2070
|
disabled,
|
|
1916
2071
|
title,
|
|
1917
2072
|
onClick: () => onCheckedChange(!checked),
|
|
1918
|
-
className: cn("inline-flex items-center gap-1.5 rounded-control border
|
|
2073
|
+
className: cn("inline-flex items-center gap-1.5 rounded-control border", SIZE_CLASSES[size], "whitespace-nowrap transition-colors", checked ? "border-line-strong bg-raised text-fg" : "border-line bg-transparent text-fg-muted hover:text-fg", "disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-fg-muted", focusRing, className),
|
|
1919
2074
|
children: [swatch !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
1920
2075
|
"aria-hidden": true,
|
|
1921
|
-
className: cn(
|
|
2076
|
+
className: cn(SWATCH_SIZE_CLASSES[size], "shrink-0 rounded-full border transition-opacity", checked ? "opacity-100" : "opacity-30"),
|
|
1922
2077
|
style: {
|
|
1923
2078
|
backgroundColor: swatch,
|
|
1924
2079
|
borderColor: swatch
|
|
@@ -2170,6 +2325,6 @@ function Tabs({ items, active, onSelect, label, idPrefix, className }) {
|
|
|
2170
2325
|
});
|
|
2171
2326
|
}
|
|
2172
2327
|
//#endregion
|
|
2173
|
-
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, toast, toneColor, useControlHeight, useDensity };
|
|
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 };
|
|
2174
2329
|
|
|
2175
2330
|
//# sourceMappingURL=index.js.map
|