@usetheo/ui 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/composites/data-table/compare-values.d.ts +2 -0
- package/dist/components/composites/data-table/data-table-loading.d.ts +10 -0
- package/dist/components/composites/data-table/data-table-parts.d.ts +16 -0
- package/dist/components/composites/data-table/data-table-virtualized.d.ts +32 -0
- package/dist/components/composites/data-table/data-table.d.ts +27 -8
- package/dist/components/composites/data-table/index.d.ts +1 -0
- package/dist/components/composites/data-table/use-data-table-sort.d.ts +19 -0
- package/dist/components/primitives/action-bar/action-bar.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +278 -81
- package/package.json +2 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DataTableColumn } from "./data-table.js";
|
|
3
|
+
/** Skeleton state shared by both DataTable modes (extracted — 300-line hook floor). */
|
|
4
|
+
export declare function DataTableLoading<T>(props: {
|
|
5
|
+
columns: DataTableColumn<T>[];
|
|
6
|
+
hasExpandColumn: boolean;
|
|
7
|
+
hasActionsColumn: boolean;
|
|
8
|
+
stickyHeader: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}): ReactNode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DataTableColumn, DataTableSort } from "./data-table.js";
|
|
3
|
+
/** Header row shared by the default and virtualized DataTable bodies. */
|
|
4
|
+
export declare function DataTableHeaderRow<T>(props: {
|
|
5
|
+
columns: DataTableColumn<T>[];
|
|
6
|
+
sort: DataTableSort | null;
|
|
7
|
+
onSortClick: (columnKey: string) => void;
|
|
8
|
+
hasExpandColumn?: boolean;
|
|
9
|
+
hasActionsColumn?: boolean;
|
|
10
|
+
}): ReactNode;
|
|
11
|
+
/** Row-actions dropdown cell shared by both DataTable bodies. */
|
|
12
|
+
export declare function DataTableRowActionsCell(props: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): ReactNode;
|
|
15
|
+
/** Renders a body cell's content per column definition (shared by both bodies). */
|
|
16
|
+
export declare function renderCellContent<T>(row: T, col: DataTableColumn<T>): ReactNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { VirtualizerOptions } from "@tanstack/react-virtual";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { DataTableBaseProps } from "./data-table.js";
|
|
4
|
+
/**
|
|
5
|
+
* Virtualized body of DataTable — renders 10K+ rows over @tanstack/react-virtual
|
|
6
|
+
* (blueprint M6, ADR D1/D2 + review fix F-dom-1). Semantic `<table>` preserved
|
|
7
|
+
* with the SPACER-ROW technique: two fixed-height spacer `<tr>`s grow the
|
|
8
|
+
* table's real layout box to the full dataset height, so the sticky `<thead>`
|
|
9
|
+
* keeps its containing block for the whole scroll and the last row stays
|
|
10
|
+
* reachable — per-row transforms (the official example's pattern) do NOT grow
|
|
11
|
+
* the layout box and un-stick the header after ~one window.
|
|
12
|
+
*
|
|
13
|
+
* Limitations (documented contract — mutually exclusive at the type level):
|
|
14
|
+
* no pagination, no expandable rows (fixed row heights are required by
|
|
15
|
+
* estimateSize). Cell content must fit `rowHeight` (cells are clipped with
|
|
16
|
+
* reduced padding; keep rowHeight >= ~32px). A row-action dropdown unmounts
|
|
17
|
+
* when its row leaves the overscan window.
|
|
18
|
+
*/
|
|
19
|
+
export interface DataTableVirtualizedOptions {
|
|
20
|
+
/** Scroll container height (px or CSS length). */
|
|
21
|
+
height: number | string;
|
|
22
|
+
/** Fixed row height in px — required by the virtualizer's estimateSize. */
|
|
23
|
+
rowHeight: number;
|
|
24
|
+
/** Rows rendered beyond the viewport on each side. @default 5 */
|
|
25
|
+
overscan?: number;
|
|
26
|
+
/** @internal test-only — inject virtualizer options (e.g. observeElementRect). */
|
|
27
|
+
virtualizerOptions?: Partial<VirtualizerOptions<HTMLElement, Element>>;
|
|
28
|
+
}
|
|
29
|
+
export type DataTableVirtualizedBodyProps<T> = Omit<DataTableBaseProps<T>, "stickyHeader"> & {
|
|
30
|
+
virtualized: DataTableVirtualizedOptions;
|
|
31
|
+
};
|
|
32
|
+
export declare function DataTableVirtualizedBody<T>(props: DataTableVirtualizedBodyProps<T>): ReactNode;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { DataTableVirtualizedOptions } from "./data-table-virtualized.js";
|
|
2
3
|
/**
|
|
3
4
|
* DataTable — generic, sortable, expandable composite over `<Table>`.
|
|
4
5
|
*
|
|
@@ -9,6 +10,10 @@ import type { ReactNode } from "react";
|
|
|
9
10
|
* pagination support controlled OR uncontrolled mode (consumer
|
|
10
11
|
* passes onSortChange / onPageChange to take over state).
|
|
11
12
|
*
|
|
13
|
+
* `virtualized` mode (M6) renders 10K+ rows over @tanstack/react-virtual.
|
|
14
|
+
* It is mutually exclusive with `pagination` and `expandable` at the type
|
|
15
|
+
* level — a virtualized list is one continuous scroll of fixed-height rows.
|
|
16
|
+
*
|
|
12
17
|
* @example
|
|
13
18
|
* <DataTable
|
|
14
19
|
* columns={[
|
|
@@ -39,19 +44,12 @@ export interface DataTableSort {
|
|
|
39
44
|
key: string;
|
|
40
45
|
direction: "asc" | "desc";
|
|
41
46
|
}
|
|
42
|
-
export interface
|
|
47
|
+
export interface DataTableBaseProps<T> {
|
|
43
48
|
data: T[];
|
|
44
49
|
columns: DataTableColumn<T>[];
|
|
45
50
|
rowKey: (row: T) => string;
|
|
46
51
|
stickyHeader?: boolean;
|
|
47
|
-
expandable?: (row: T) => ReactNode | null;
|
|
48
|
-
expandMode?: "single" | "multiple";
|
|
49
52
|
rowActions?: (row: T) => ReactNode;
|
|
50
|
-
pagination?: {
|
|
51
|
-
pageSize: number;
|
|
52
|
-
controlledPage?: number;
|
|
53
|
-
onPageChange?: (page: number) => void;
|
|
54
|
-
} | null;
|
|
55
53
|
defaultSort?: DataTableSort;
|
|
56
54
|
sort?: DataTableSort | null;
|
|
57
55
|
onSortChange?: (sort: DataTableSort | null) => void;
|
|
@@ -59,5 +57,26 @@ export interface DataTableProps<T> {
|
|
|
59
57
|
emptyState?: ReactNode;
|
|
60
58
|
className?: string;
|
|
61
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Discriminated union: the default mode keeps pagination/expandable; the
|
|
62
|
+
* virtualized mode excludes them at the type level (fixed-height single
|
|
63
|
+
* scroll — see DataTableVirtualizedOptions for the documented limitations).
|
|
64
|
+
*/
|
|
65
|
+
export type DataTableProps<T> = DataTableBaseProps<T> & ({
|
|
66
|
+
virtualized?: never;
|
|
67
|
+
expandable?: (row: T) => ReactNode | null;
|
|
68
|
+
expandMode?: "single" | "multiple";
|
|
69
|
+
pagination?: {
|
|
70
|
+
pageSize: number;
|
|
71
|
+
controlledPage?: number;
|
|
72
|
+
onPageChange?: (page: number) => void;
|
|
73
|
+
} | null;
|
|
74
|
+
} | {
|
|
75
|
+
virtualized: DataTableVirtualizedOptions;
|
|
76
|
+
expandable?: never;
|
|
77
|
+
expandMode?: never;
|
|
78
|
+
pagination?: never;
|
|
79
|
+
stickyHeader?: never;
|
|
80
|
+
});
|
|
62
81
|
declare function DataTable<T>(props: DataTableProps<T>): ReactNode;
|
|
63
82
|
export { DataTable };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DataTableColumn, DataTableSort } from "./data-table.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared controlled/uncontrolled sort state for both DataTable modes.
|
|
4
|
+
* Cycle: none → asc → desc → none. Client-side sorting applies only in
|
|
5
|
+
* uncontrolled mode (controlled consumers pre-sort their data).
|
|
6
|
+
*/
|
|
7
|
+
export declare function useDataTableSort<T>(opts: {
|
|
8
|
+
data: T[];
|
|
9
|
+
columns: DataTableColumn<T>[];
|
|
10
|
+
defaultSort?: DataTableSort;
|
|
11
|
+
sort?: DataTableSort | null;
|
|
12
|
+
onSortChange?: (sort: DataTableSort | null) => void;
|
|
13
|
+
/** Fired on uncontrolled sort change (EC-8: page reset in paginated mode). */
|
|
14
|
+
onUncontrolledSortChange?: () => void;
|
|
15
|
+
}): {
|
|
16
|
+
sort: DataTableSort | null;
|
|
17
|
+
handleSort: (columnKey: string) => void;
|
|
18
|
+
sortedData: T[];
|
|
19
|
+
};
|
|
@@ -19,6 +19,8 @@ import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
|
19
19
|
export interface ActionBarProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
20
20
|
search?: {
|
|
21
21
|
placeholder: string;
|
|
22
|
+
/** Accessible name; defaults to the placeholder with trailing ellipsis stripped (issue #8). */
|
|
23
|
+
"aria-label"?: string;
|
|
22
24
|
value: string;
|
|
23
25
|
onChange: (value: string) => void;
|
|
24
26
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export { Pagination, computePageRange, type PaginationProps, } from "./component
|
|
|
45
45
|
export { DropdownMenu } from "./components/primitives/dropdown-menu/index.js";
|
|
46
46
|
export { ActionBar, type ActionBarProps } from "./components/primitives/action-bar/index.js";
|
|
47
47
|
export { PinInput, type PinInputProps } from "./components/primitives/pin-input/index.js";
|
|
48
|
-
export { DataTable, type DataTableColumn, type DataTableProps, type DataTableSort, } from "./components/composites/data-table/index.js";
|
|
48
|
+
export { DataTable, type DataTableColumn, type DataTableProps, type DataTableSort, type DataTableVirtualizedOptions, } from "./components/composites/data-table/index.js";
|
|
49
49
|
export { PageShell, type PageShellProps } from "./components/composites/page-shell/index.js";
|
|
50
50
|
export { linScale, niceMax, seriesPath, TrendChart, type TrendChartProps, type TrendPoint, type TrendSeries, } from "./components/composites/trend-chart/index.js";
|
|
51
51
|
export { Stepper, deriveSteps, type StepStatus, type StepperProps, type StepperStepData, } from "./components/composites/stepper/index.js";
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
|
19
19
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
20
20
|
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
21
21
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
22
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
22
23
|
|
|
23
24
|
// src/lib/cn.ts
|
|
24
25
|
var twMerge = extendTailwindMerge({
|
|
@@ -3898,6 +3899,7 @@ var ActionBar = forwardRef(
|
|
|
3898
3899
|
"input",
|
|
3899
3900
|
{
|
|
3900
3901
|
type: "search",
|
|
3902
|
+
"aria-label": search["aria-label"] ?? search.placeholder.replace(/(\.{3}|…)\s*$/, ""),
|
|
3901
3903
|
placeholder: search.placeholder,
|
|
3902
3904
|
value: search.value,
|
|
3903
3905
|
onChange: (e) => search.onChange(e.target.value),
|
|
@@ -4094,6 +4096,74 @@ var PinInput = forwardRef(
|
|
|
4094
4096
|
}
|
|
4095
4097
|
);
|
|
4096
4098
|
PinInput.displayName = "PinInput";
|
|
4099
|
+
function DataTableHeaderRow(props) {
|
|
4100
|
+
const { columns, sort, onSortClick, hasExpandColumn = false, hasActionsColumn = false } = props;
|
|
4101
|
+
return /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
4102
|
+
hasExpandColumn ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Expand" }) }) : null,
|
|
4103
|
+
columns.map((col) => {
|
|
4104
|
+
const isSortable = col.sortable === true;
|
|
4105
|
+
const isActive = sort?.key === col.key;
|
|
4106
|
+
return /* @__PURE__ */ jsx(
|
|
4107
|
+
Table.HeaderCell,
|
|
4108
|
+
{
|
|
4109
|
+
align: col.align,
|
|
4110
|
+
onSort: isSortable ? () => onSortClick(col.key) : void 0,
|
|
4111
|
+
sortDirection: isSortable ? isActive ? sort?.direction : "none" : void 0,
|
|
4112
|
+
style: col.width ? { width: col.width } : void 0,
|
|
4113
|
+
children: col.label
|
|
4114
|
+
},
|
|
4115
|
+
col.key
|
|
4116
|
+
);
|
|
4117
|
+
}),
|
|
4118
|
+
hasActionsColumn ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) }) : null
|
|
4119
|
+
] });
|
|
4120
|
+
}
|
|
4121
|
+
function DataTableRowActionsCell(props) {
|
|
4122
|
+
return /* @__PURE__ */ jsx(Table.Cell, { align: "right", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
4123
|
+
/* @__PURE__ */ jsx(
|
|
4124
|
+
DropdownMenu.Trigger,
|
|
4125
|
+
{
|
|
4126
|
+
"aria-label": "Row actions",
|
|
4127
|
+
className: cn(
|
|
4128
|
+
"inline-flex size-7 items-center justify-center rounded-md",
|
|
4129
|
+
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
4130
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
4131
|
+
),
|
|
4132
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true", className: "size-4" })
|
|
4133
|
+
}
|
|
4134
|
+
),
|
|
4135
|
+
/* @__PURE__ */ jsx(DropdownMenu.Content, { align: "end", children: props.children })
|
|
4136
|
+
] }) });
|
|
4137
|
+
}
|
|
4138
|
+
function renderCellContent(row, col) {
|
|
4139
|
+
return col.render ? col.render(row) : String(row[col.key] ?? "");
|
|
4140
|
+
}
|
|
4141
|
+
function DataTableLoading(props) {
|
|
4142
|
+
const { columns, hasExpandColumn, hasActionsColumn, stickyHeader, className } = props;
|
|
4143
|
+
const totalCols = columns.length + (hasExpandColumn ? 1 : 0) + (hasActionsColumn ? 1 : 0);
|
|
4144
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "data-table", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
4145
|
+
/* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 bg-card" : void 0, children: /* @__PURE__ */ jsx(
|
|
4146
|
+
DataTableHeaderRow,
|
|
4147
|
+
{
|
|
4148
|
+
columns: columns.map((col) => ({ ...col, sortable: false })),
|
|
4149
|
+
sort: null,
|
|
4150
|
+
onSortClick: () => {
|
|
4151
|
+
},
|
|
4152
|
+
hasExpandColumn,
|
|
4153
|
+
hasActionsColumn
|
|
4154
|
+
}
|
|
4155
|
+
) }),
|
|
4156
|
+
/* @__PURE__ */ jsx(Table.Body, { children: Array.from({ length: 5 }, (_, i) => (
|
|
4157
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders
|
|
4158
|
+
/* @__PURE__ */ jsx(Table.Row, { children: Array.from({ length: totalCols }, (_2, j) => (
|
|
4159
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders
|
|
4160
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, `s-${i}-${j}`)
|
|
4161
|
+
)) }, `skeleton-${i}`)
|
|
4162
|
+
)) })
|
|
4163
|
+
] }) });
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// src/components/composites/data-table/compare-values.ts
|
|
4097
4167
|
function compareValues(a, b) {
|
|
4098
4168
|
if (a === b) return 0;
|
|
4099
4169
|
if (a === null || a === void 0) return -1;
|
|
@@ -4101,7 +4171,184 @@ function compareValues(a, b) {
|
|
|
4101
4171
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
4102
4172
|
return String(a).localeCompare(String(b));
|
|
4103
4173
|
}
|
|
4174
|
+
|
|
4175
|
+
// src/components/composites/data-table/use-data-table-sort.ts
|
|
4176
|
+
function useDataTableSort(opts) {
|
|
4177
|
+
const { data, columns, defaultSort, sort: controlledSort, onSortChange } = opts;
|
|
4178
|
+
const isControlledSort = onSortChange !== void 0;
|
|
4179
|
+
const [uncontrolledSort, setUncontrolledSort] = useState(
|
|
4180
|
+
defaultSort ?? null
|
|
4181
|
+
);
|
|
4182
|
+
const sort = isControlledSort ? controlledSort ?? null : uncontrolledSort;
|
|
4183
|
+
function handleSort(columnKey) {
|
|
4184
|
+
let nextSort;
|
|
4185
|
+
if (sort?.key !== columnKey) {
|
|
4186
|
+
nextSort = { key: columnKey, direction: "asc" };
|
|
4187
|
+
} else if (sort.direction === "asc") {
|
|
4188
|
+
nextSort = { key: columnKey, direction: "desc" };
|
|
4189
|
+
} else {
|
|
4190
|
+
nextSort = null;
|
|
4191
|
+
}
|
|
4192
|
+
if (isControlledSort) {
|
|
4193
|
+
onSortChange?.(nextSort);
|
|
4194
|
+
} else {
|
|
4195
|
+
setUncontrolledSort(nextSort);
|
|
4196
|
+
opts.onUncontrolledSortChange?.();
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
const sortedData = useMemo(() => {
|
|
4200
|
+
if (isControlledSort || sort === null) return data;
|
|
4201
|
+
const col = columns.find((c) => c.key === sort.key);
|
|
4202
|
+
if (!col) return data;
|
|
4203
|
+
const sorted = [...data].sort((a, b) => {
|
|
4204
|
+
const aVal = col.render ? null : a[sort.key];
|
|
4205
|
+
const bVal = col.render ? null : b[sort.key];
|
|
4206
|
+
const cmp = compareValues(aVal, bVal);
|
|
4207
|
+
return sort.direction === "asc" ? cmp : -cmp;
|
|
4208
|
+
});
|
|
4209
|
+
return sorted;
|
|
4210
|
+
}, [data, sort, isControlledSort, columns]);
|
|
4211
|
+
return { sort, handleSort, sortedData };
|
|
4212
|
+
}
|
|
4213
|
+
var EXCLUSIVE_PROPS = ["pagination", "expandable"];
|
|
4214
|
+
function SpacerRow(props) {
|
|
4215
|
+
if (props.height <= 0) return null;
|
|
4216
|
+
return (
|
|
4217
|
+
// biome-ignore lint/a11y/noInteractiveElementToNoninteractiveRole: spacer row is pure layout filler (no content, no interaction) inside a semantic table; presentation keeps SRs on the aria-rowcount contract
|
|
4218
|
+
/* @__PURE__ */ jsx(
|
|
4219
|
+
"tr",
|
|
4220
|
+
{
|
|
4221
|
+
"data-slot": `data-table-virtual-spacer-${props.slot}`,
|
|
4222
|
+
role: "presentation",
|
|
4223
|
+
style: { height: `${props.height}px` },
|
|
4224
|
+
children: /* @__PURE__ */ jsx("td", { colSpan: props.colSpan, style: { padding: 0 } })
|
|
4225
|
+
}
|
|
4226
|
+
)
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
4229
|
+
function DataTableVirtualizedBody(props) {
|
|
4230
|
+
const {
|
|
4231
|
+
data,
|
|
4232
|
+
columns,
|
|
4233
|
+
rowKey,
|
|
4234
|
+
virtualized,
|
|
4235
|
+
rowActions,
|
|
4236
|
+
defaultSort,
|
|
4237
|
+
sort: controlledSort,
|
|
4238
|
+
onSortChange,
|
|
4239
|
+
loading = false,
|
|
4240
|
+
emptyState,
|
|
4241
|
+
className
|
|
4242
|
+
} = props;
|
|
4243
|
+
if (isDev()) {
|
|
4244
|
+
for (const key of EXCLUSIVE_PROPS) {
|
|
4245
|
+
if (props[key]) {
|
|
4246
|
+
console.warn(
|
|
4247
|
+
`DataTable: \`${key}\` is ignored in virtualized mode \u2014 the modes are mutually exclusive.`
|
|
4248
|
+
);
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
if (virtualized.rowHeight < 1) {
|
|
4252
|
+
console.warn("DataTable: virtualized.rowHeight must be >= 1px \u2014 falling back to 1.");
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
const rowHeight = Math.max(1, virtualized.rowHeight);
|
|
4256
|
+
const parentRef = useRef(null);
|
|
4257
|
+
const { sort, handleSort, sortedData } = useDataTableSort({
|
|
4258
|
+
data,
|
|
4259
|
+
columns,
|
|
4260
|
+
defaultSort,
|
|
4261
|
+
sort: controlledSort,
|
|
4262
|
+
onSortChange
|
|
4263
|
+
});
|
|
4264
|
+
const virtualizer = useVirtualizer({
|
|
4265
|
+
// Injection first (review F-dom-6): the @internal test hook can add
|
|
4266
|
+
// observeElementRect but can never override the core window contract.
|
|
4267
|
+
...virtualized.virtualizerOptions,
|
|
4268
|
+
count: sortedData.length,
|
|
4269
|
+
getScrollElement: () => parentRef.current,
|
|
4270
|
+
estimateSize: () => rowHeight,
|
|
4271
|
+
overscan: virtualized.overscan ?? 5
|
|
4272
|
+
});
|
|
4273
|
+
if (loading) {
|
|
4274
|
+
return /* @__PURE__ */ jsx(
|
|
4275
|
+
DataTableLoading,
|
|
4276
|
+
{
|
|
4277
|
+
columns,
|
|
4278
|
+
hasExpandColumn: false,
|
|
4279
|
+
hasActionsColumn: rowActions !== void 0,
|
|
4280
|
+
stickyHeader: true,
|
|
4281
|
+
className
|
|
4282
|
+
}
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
if (sortedData.length === 0) {
|
|
4286
|
+
return /* @__PURE__ */ jsx("div", { className: cn("w-full", className), children: emptyState ?? /* @__PURE__ */ jsx(EmptyState, { title: "No data", description: "There's nothing here yet." }) });
|
|
4287
|
+
}
|
|
4288
|
+
const items = virtualizer.getVirtualItems();
|
|
4289
|
+
const totalSize = virtualizer.getTotalSize();
|
|
4290
|
+
const firstItem = items[0];
|
|
4291
|
+
const lastItem = items[items.length - 1];
|
|
4292
|
+
const paddingTop = firstItem ? firstItem.start : 0;
|
|
4293
|
+
const paddingBottom = lastItem ? totalSize - lastItem.end : totalSize;
|
|
4294
|
+
const colSpan = columns.length + (rowActions ? 1 : 0);
|
|
4295
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "data-table-virtual-body", className: cn("w-full", className), children: /* @__PURE__ */ jsx(
|
|
4296
|
+
"section",
|
|
4297
|
+
{
|
|
4298
|
+
ref: parentRef,
|
|
4299
|
+
"aria-label": "Table scroll area",
|
|
4300
|
+
tabIndex: 0,
|
|
4301
|
+
className: "overflow-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
4302
|
+
style: { height: virtualized.height },
|
|
4303
|
+
"data-slot": "data-table-virtual-scroll",
|
|
4304
|
+
children: /* @__PURE__ */ jsxs(Table, { "aria-rowcount": sortedData.length + 1, children: [
|
|
4305
|
+
/* @__PURE__ */ jsx(Table.Header, { className: "sticky top-0 z-10 bg-card", children: /* @__PURE__ */ jsx(
|
|
4306
|
+
DataTableHeaderRow,
|
|
4307
|
+
{
|
|
4308
|
+
columns,
|
|
4309
|
+
sort,
|
|
4310
|
+
onSortClick: handleSort,
|
|
4311
|
+
hasActionsColumn: rowActions !== void 0
|
|
4312
|
+
}
|
|
4313
|
+
) }),
|
|
4314
|
+
/* @__PURE__ */ jsxs(Table.Body, { children: [
|
|
4315
|
+
/* @__PURE__ */ jsx(SpacerRow, { slot: "top", height: paddingTop, colSpan }),
|
|
4316
|
+
items.map((vRow) => {
|
|
4317
|
+
const row = sortedData[vRow.index];
|
|
4318
|
+
return /* @__PURE__ */ jsxs(
|
|
4319
|
+
Table.Row,
|
|
4320
|
+
{
|
|
4321
|
+
"aria-rowindex": vRow.index + 2,
|
|
4322
|
+
style: { height: `${rowHeight}px` },
|
|
4323
|
+
children: [
|
|
4324
|
+
columns.map((col) => /* @__PURE__ */ jsx(
|
|
4325
|
+
Table.Cell,
|
|
4326
|
+
{
|
|
4327
|
+
align: col.align,
|
|
4328
|
+
className: cn("overflow-hidden py-1.5", col.className),
|
|
4329
|
+
children: renderCellContent(row, col)
|
|
4330
|
+
},
|
|
4331
|
+
col.key
|
|
4332
|
+
)),
|
|
4333
|
+
rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
|
|
4334
|
+
]
|
|
4335
|
+
},
|
|
4336
|
+
rowKey(row)
|
|
4337
|
+
);
|
|
4338
|
+
}),
|
|
4339
|
+
/* @__PURE__ */ jsx(SpacerRow, { slot: "bottom", height: paddingBottom, colSpan })
|
|
4340
|
+
] })
|
|
4341
|
+
] })
|
|
4342
|
+
}
|
|
4343
|
+
) });
|
|
4344
|
+
}
|
|
4104
4345
|
function DataTable(props) {
|
|
4346
|
+
if (props.virtualized) {
|
|
4347
|
+
return /* @__PURE__ */ jsx(DataTableVirtualizedBody, { ...props, virtualized: props.virtualized });
|
|
4348
|
+
}
|
|
4349
|
+
return /* @__PURE__ */ jsx(DataTableDefaultBody, { ...props });
|
|
4350
|
+
}
|
|
4351
|
+
function DataTableDefaultBody(props) {
|
|
4105
4352
|
const {
|
|
4106
4353
|
data,
|
|
4107
4354
|
columns,
|
|
@@ -4118,32 +4365,22 @@ function DataTable(props) {
|
|
|
4118
4365
|
emptyState,
|
|
4119
4366
|
className
|
|
4120
4367
|
} = props;
|
|
4121
|
-
const isControlledSort = onSortChange !== void 0;
|
|
4122
|
-
const [uncontrolledSort, setUncontrolledSort] = useState(
|
|
4123
|
-
defaultSort ?? null
|
|
4124
|
-
);
|
|
4125
|
-
const sort = isControlledSort ? controlledSort ?? null : uncontrolledSort;
|
|
4126
4368
|
const isControlledPage = pagination?.controlledPage !== void 0;
|
|
4127
4369
|
const [uncontrolledPage, setUncontrolledPage] = useState(0);
|
|
4128
4370
|
const currentPage = isControlledPage ? pagination?.controlledPage ?? 0 : uncontrolledPage;
|
|
4129
4371
|
const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);
|
|
4130
4372
|
const [expanded, setExpanded] = useState(/* @__PURE__ */ new Set());
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
}
|
|
4140
|
-
if (isControlledSort) {
|
|
4141
|
-
onSortChange?.(nextSort);
|
|
4142
|
-
} else {
|
|
4143
|
-
setUncontrolledSort(nextSort);
|
|
4373
|
+
const { sort, handleSort, sortedData } = useDataTableSort({
|
|
4374
|
+
data,
|
|
4375
|
+
columns,
|
|
4376
|
+
defaultSort,
|
|
4377
|
+
sort: controlledSort,
|
|
4378
|
+
onSortChange,
|
|
4379
|
+
// EC-8: sort change resets pagination to page 0
|
|
4380
|
+
onUncontrolledSortChange: () => {
|
|
4144
4381
|
if (!isControlledPage) setUncontrolledPage(0);
|
|
4145
4382
|
}
|
|
4146
|
-
}
|
|
4383
|
+
});
|
|
4147
4384
|
function handlePageChange(page) {
|
|
4148
4385
|
const zeroIdx = page - 1;
|
|
4149
4386
|
if (isControlledPage) {
|
|
@@ -4167,18 +4404,6 @@ function DataTable(props) {
|
|
|
4167
4404
|
});
|
|
4168
4405
|
}
|
|
4169
4406
|
}
|
|
4170
|
-
const sortedData = useMemo(() => {
|
|
4171
|
-
if (isControlledSort || sort === null) return data;
|
|
4172
|
-
const col = columns.find((c) => c.key === sort.key);
|
|
4173
|
-
if (!col) return data;
|
|
4174
|
-
const sorted = [...data].sort((a, b) => {
|
|
4175
|
-
const aVal = col.render ? null : a[sort.key];
|
|
4176
|
-
const bVal = col.render ? null : b[sort.key];
|
|
4177
|
-
const cmp = compareValues(aVal, bVal);
|
|
4178
|
-
return sort.direction === "asc" ? cmp : -cmp;
|
|
4179
|
-
});
|
|
4180
|
-
return sorted;
|
|
4181
|
-
}, [data, sort, isControlledSort, columns]);
|
|
4182
4407
|
const visibleData = useMemo(() => {
|
|
4183
4408
|
if (!pagination) return sortedData;
|
|
4184
4409
|
if (isControlledPage) return sortedData;
|
|
@@ -4187,22 +4412,17 @@ function DataTable(props) {
|
|
|
4187
4412
|
}, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);
|
|
4188
4413
|
const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);
|
|
4189
4414
|
const expandedColSpan = columns.length + extraCols;
|
|
4190
|
-
const totalCols = columns.length + extraCols;
|
|
4191
4415
|
if (loading) {
|
|
4192
|
-
return /* @__PURE__ */ jsx(
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
columns
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, `s-${i}-${j}`)
|
|
4203
|
-
)) }, `skeleton-${i}`)
|
|
4204
|
-
)) })
|
|
4205
|
-
] }) });
|
|
4416
|
+
return /* @__PURE__ */ jsx(
|
|
4417
|
+
DataTableLoading,
|
|
4418
|
+
{
|
|
4419
|
+
columns,
|
|
4420
|
+
hasExpandColumn: expandable !== void 0,
|
|
4421
|
+
hasActionsColumn: rowActions !== void 0,
|
|
4422
|
+
stickyHeader,
|
|
4423
|
+
className
|
|
4424
|
+
}
|
|
4425
|
+
);
|
|
4206
4426
|
}
|
|
4207
4427
|
if (sortedData.length === 0) {
|
|
4208
4428
|
return /* @__PURE__ */ jsx("div", { className: cn("w-full", className), children: emptyState ?? /* @__PURE__ */ jsx(EmptyState, { title: "No data", description: "There's nothing here yet." }) });
|
|
@@ -4210,25 +4430,16 @@ function DataTable(props) {
|
|
|
4210
4430
|
const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;
|
|
4211
4431
|
return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [
|
|
4212
4432
|
/* @__PURE__ */ jsxs(Table, { children: [
|
|
4213
|
-
/* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
sortDirection: isSortable ? isActive ? sort?.direction : "none" : void 0,
|
|
4224
|
-
style: col.width ? { width: col.width } : void 0,
|
|
4225
|
-
children: col.label
|
|
4226
|
-
},
|
|
4227
|
-
col.key
|
|
4228
|
-
);
|
|
4229
|
-
}),
|
|
4230
|
-
rowActions ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) }) : null
|
|
4231
|
-
] }) }),
|
|
4433
|
+
/* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */ jsx(
|
|
4434
|
+
DataTableHeaderRow,
|
|
4435
|
+
{
|
|
4436
|
+
columns,
|
|
4437
|
+
sort,
|
|
4438
|
+
onSortClick: handleSort,
|
|
4439
|
+
hasExpandColumn: expandable !== void 0,
|
|
4440
|
+
hasActionsColumn: rowActions !== void 0
|
|
4441
|
+
}
|
|
4442
|
+
) }),
|
|
4232
4443
|
/* @__PURE__ */ jsx(Table.Body, { children: visibleData.map((row) => {
|
|
4233
4444
|
const key = rowKey(row);
|
|
4234
4445
|
const expandedContent = expandable ? expandable(row) : null;
|
|
@@ -4248,22 +4459,8 @@ function DataTable(props) {
|
|
|
4248
4459
|
children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" })
|
|
4249
4460
|
}
|
|
4250
4461
|
) : null }) : null,
|
|
4251
|
-
columns.map((col) => /* @__PURE__ */ jsx(Table.Cell, { align: col.align, className: col.className, children:
|
|
4252
|
-
rowActions ? /* @__PURE__ */ jsx(
|
|
4253
|
-
/* @__PURE__ */ jsx(
|
|
4254
|
-
DropdownMenu.Trigger,
|
|
4255
|
-
{
|
|
4256
|
-
"aria-label": "Row actions",
|
|
4257
|
-
className: cn(
|
|
4258
|
-
"inline-flex size-7 items-center justify-center rounded-md",
|
|
4259
|
-
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
4260
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
4261
|
-
),
|
|
4262
|
-
children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true", className: "size-4" })
|
|
4263
|
-
}
|
|
4264
|
-
),
|
|
4265
|
-
/* @__PURE__ */ jsx(DropdownMenu.Content, { align: "end", children: rowActions(row) })
|
|
4266
|
-
] }) }) : null
|
|
4462
|
+
columns.map((col) => /* @__PURE__ */ jsx(Table.Cell, { align: col.align, className: col.className, children: renderCellContent(row, col) }, col.key)),
|
|
4463
|
+
rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
|
|
4267
4464
|
] }),
|
|
4268
4465
|
isExpanded && isExpandable ? /* @__PURE__ */ jsx("tr", { id: `expanded-${key}`, children: /* @__PURE__ */ jsx("td", { colSpan: expandedColSpan, className: "bg-muted/30 p-4", children: expandedContent }) }) : null
|
|
4269
4466
|
] }, key);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usetheo/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Violet Forge design system — generic React components + primitives (the non-AI foundation of @theokit/ui).",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@radix-ui/react-tabs": "^1.1.3",
|
|
41
41
|
"@radix-ui/react-toast": "^1.2.15",
|
|
42
42
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
43
|
+
"@tanstack/react-virtual": "^3.14.6",
|
|
43
44
|
"class-variance-authority": "^0.7.1",
|
|
44
45
|
"clsx": "^2.1.1",
|
|
45
46
|
"cmdk": "^1.1.1",
|