@usetheo/ui 0.20.0 → 0.22.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/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/file-dropzone/file-dropzone.d.ts +26 -0
- package/dist/components/primitives/file-dropzone/index.d.ts +1 -0
- package/dist/components/primitives/file-dropzone/validate.d.ts +40 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +495 -82
- 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
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileDropzone — drag-drop + file-picker primitive, dependency-free (HTML5
|
|
3
|
+
* DnD). The drag mechanics, the typed-rejection shape and the browser quirks
|
|
4
|
+
* are ported from the react-dropzone reference study (blueprint M5): nested
|
|
5
|
+
* dragenter/dragleave target counting, empty MIME type during drag (Chrome),
|
|
6
|
+
* and the all-or-nothing maxFiles rule.
|
|
7
|
+
*
|
|
8
|
+
* Boundary: this component SELECTS and VALIDATES files. Uploading (network,
|
|
9
|
+
* progress) belongs to the consumer — compose with `Progress` for that.
|
|
10
|
+
* Directory drops are out of scope (entries without a real File are skipped).
|
|
11
|
+
*/
|
|
12
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
13
|
+
import type { FileRejection, ValidateFilesOptions } from "./validate.js";
|
|
14
|
+
export { matchesAccept, validateFiles, type FileDropzoneError, type FileDropzoneErrorCode, type FileRejection, type ValidateFilesOptions, } from "./validate.js";
|
|
15
|
+
export type FileDropzoneState = "idle" | "drag-over" | "drag-reject" | "disabled";
|
|
16
|
+
export interface FileDropzoneProps extends Omit<HTMLAttributes<HTMLDivElement>, "onDrop" | "children">, ValidateFilesOptions {
|
|
17
|
+
/** Accessible name for the dropzone (aria-label). */
|
|
18
|
+
label: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
onFilesAccepted?: (files: File[]) => void;
|
|
21
|
+
onFilesRejected?: (rejections: FileRejection[]) => void;
|
|
22
|
+
/** Custom instructions content; defaults to a drag-or-click hint. */
|
|
23
|
+
instructions?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
declare const FileDropzone: import("react").ForwardRefExoticComponent<FileDropzoneProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
26
|
+
export { FileDropzone };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FileDropzone, matchesAccept, validateFiles, type FileDropzoneError, type FileDropzoneErrorCode, type FileDropzoneProps, type FileDropzoneState, type FileRejection, type ValidateFilesOptions, } from "./file-dropzone.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validation core of FileDropzone — typed rejections, accept matching
|
|
3
|
+
* (MIME exact / wildcard / extension), size and collective count rules.
|
|
4
|
+
* Ported behaviors from the react-dropzone reference study (blueprint M5).
|
|
5
|
+
*/
|
|
6
|
+
export type FileDropzoneErrorCode = "file-invalid-type" | "file-too-large" | "file-too-small" | "too-many-files";
|
|
7
|
+
export interface FileDropzoneError {
|
|
8
|
+
/** One of the four built-in codes, or a custom code from `validator`. */
|
|
9
|
+
code: FileDropzoneErrorCode | string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FileRejection {
|
|
13
|
+
file: File;
|
|
14
|
+
errors: FileDropzoneError[];
|
|
15
|
+
}
|
|
16
|
+
export interface ValidateFilesOptions {
|
|
17
|
+
accept?: Record<string, string[]>;
|
|
18
|
+
maxSize?: number;
|
|
19
|
+
minSize?: number;
|
|
20
|
+
maxFiles?: number;
|
|
21
|
+
multiple?: boolean;
|
|
22
|
+
validator?: (file: File) => FileDropzoneError | FileDropzoneError[] | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Matches a file against an `accept` map ({mime: [ext...]}): exact MIME,
|
|
26
|
+
* `type/*` wildcard, or extension (case-insensitive). No accept → accepts
|
|
27
|
+
* everything. An empty `file.type` is accepted (Chrome reports "" for some
|
|
28
|
+
* types during drag) — real validation happens again on drop.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchesAccept(file: File, accept?: Record<string, string[]>): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Validates files against accept/size/count rules plus an optional custom
|
|
33
|
+
* validator. Per-file errors accumulate; the maxFiles/multiple rule is
|
|
34
|
+
* COLLECTIVE and all-or-nothing: exceeding it rejects EVERY file with
|
|
35
|
+
* `too-many-files` (ported from the reference behavior).
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateFiles(files: File[], options?: ValidateFilesOptions): {
|
|
38
|
+
accepted: File[];
|
|
39
|
+
rejections: FileRejection[];
|
|
40
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -45,10 +45,11 @@ 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";
|
|
52
|
+
export { FileDropzone, matchesAccept, validateFiles, type FileDropzoneError, type FileDropzoneErrorCode, type FileDropzoneProps, type FileDropzoneState, type FileRejection, type ValidateFilesOptions, } from "./components/primitives/file-dropzone/index.js";
|
|
52
53
|
export { MetricsPanel, type Metric } from "./components/primitives/metrics-panel/index.js";
|
|
53
54
|
export { SocialAuthRow, type SocialProvider, } from "./components/primitives/social-auth-row/index.js";
|
|
54
55
|
export { LoginSplit } from "./components/primitives/login-split/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({
|
|
@@ -4094,6 +4095,74 @@ var PinInput = forwardRef(
|
|
|
4094
4095
|
}
|
|
4095
4096
|
);
|
|
4096
4097
|
PinInput.displayName = "PinInput";
|
|
4098
|
+
function DataTableHeaderRow(props) {
|
|
4099
|
+
const { columns, sort, onSortClick, hasExpandColumn = false, hasActionsColumn = false } = props;
|
|
4100
|
+
return /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
4101
|
+
hasExpandColumn ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Expand" }) }) : null,
|
|
4102
|
+
columns.map((col) => {
|
|
4103
|
+
const isSortable = col.sortable === true;
|
|
4104
|
+
const isActive = sort?.key === col.key;
|
|
4105
|
+
return /* @__PURE__ */ jsx(
|
|
4106
|
+
Table.HeaderCell,
|
|
4107
|
+
{
|
|
4108
|
+
align: col.align,
|
|
4109
|
+
onSort: isSortable ? () => onSortClick(col.key) : void 0,
|
|
4110
|
+
sortDirection: isSortable ? isActive ? sort?.direction : "none" : void 0,
|
|
4111
|
+
style: col.width ? { width: col.width } : void 0,
|
|
4112
|
+
children: col.label
|
|
4113
|
+
},
|
|
4114
|
+
col.key
|
|
4115
|
+
);
|
|
4116
|
+
}),
|
|
4117
|
+
hasActionsColumn ? /* @__PURE__ */ jsx(Table.HeaderCell, { children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Actions" }) }) : null
|
|
4118
|
+
] });
|
|
4119
|
+
}
|
|
4120
|
+
function DataTableRowActionsCell(props) {
|
|
4121
|
+
return /* @__PURE__ */ jsx(Table.Cell, { align: "right", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
4122
|
+
/* @__PURE__ */ jsx(
|
|
4123
|
+
DropdownMenu.Trigger,
|
|
4124
|
+
{
|
|
4125
|
+
"aria-label": "Row actions",
|
|
4126
|
+
className: cn(
|
|
4127
|
+
"inline-flex size-7 items-center justify-center rounded-md",
|
|
4128
|
+
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
4129
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
4130
|
+
),
|
|
4131
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true", className: "size-4" })
|
|
4132
|
+
}
|
|
4133
|
+
),
|
|
4134
|
+
/* @__PURE__ */ jsx(DropdownMenu.Content, { align: "end", children: props.children })
|
|
4135
|
+
] }) });
|
|
4136
|
+
}
|
|
4137
|
+
function renderCellContent(row, col) {
|
|
4138
|
+
return col.render ? col.render(row) : String(row[col.key] ?? "");
|
|
4139
|
+
}
|
|
4140
|
+
function DataTableLoading(props) {
|
|
4141
|
+
const { columns, hasExpandColumn, hasActionsColumn, stickyHeader, className } = props;
|
|
4142
|
+
const totalCols = columns.length + (hasExpandColumn ? 1 : 0) + (hasActionsColumn ? 1 : 0);
|
|
4143
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "data-table", className: cn("w-full", className), children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
4144
|
+
/* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 bg-card" : void 0, children: /* @__PURE__ */ jsx(
|
|
4145
|
+
DataTableHeaderRow,
|
|
4146
|
+
{
|
|
4147
|
+
columns: columns.map((col) => ({ ...col, sortable: false })),
|
|
4148
|
+
sort: null,
|
|
4149
|
+
onSortClick: () => {
|
|
4150
|
+
},
|
|
4151
|
+
hasExpandColumn,
|
|
4152
|
+
hasActionsColumn
|
|
4153
|
+
}
|
|
4154
|
+
) }),
|
|
4155
|
+
/* @__PURE__ */ jsx(Table.Body, { children: Array.from({ length: 5 }, (_, i) => (
|
|
4156
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders
|
|
4157
|
+
/* @__PURE__ */ jsx(Table.Row, { children: Array.from({ length: totalCols }, (_2, j) => (
|
|
4158
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders
|
|
4159
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, `s-${i}-${j}`)
|
|
4160
|
+
)) }, `skeleton-${i}`)
|
|
4161
|
+
)) })
|
|
4162
|
+
] }) });
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
// src/components/composites/data-table/compare-values.ts
|
|
4097
4166
|
function compareValues(a, b) {
|
|
4098
4167
|
if (a === b) return 0;
|
|
4099
4168
|
if (a === null || a === void 0) return -1;
|
|
@@ -4101,7 +4170,184 @@ function compareValues(a, b) {
|
|
|
4101
4170
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
4102
4171
|
return String(a).localeCompare(String(b));
|
|
4103
4172
|
}
|
|
4173
|
+
|
|
4174
|
+
// src/components/composites/data-table/use-data-table-sort.ts
|
|
4175
|
+
function useDataTableSort(opts) {
|
|
4176
|
+
const { data, columns, defaultSort, sort: controlledSort, onSortChange } = opts;
|
|
4177
|
+
const isControlledSort = onSortChange !== void 0;
|
|
4178
|
+
const [uncontrolledSort, setUncontrolledSort] = useState(
|
|
4179
|
+
defaultSort ?? null
|
|
4180
|
+
);
|
|
4181
|
+
const sort = isControlledSort ? controlledSort ?? null : uncontrolledSort;
|
|
4182
|
+
function handleSort(columnKey) {
|
|
4183
|
+
let nextSort;
|
|
4184
|
+
if (sort?.key !== columnKey) {
|
|
4185
|
+
nextSort = { key: columnKey, direction: "asc" };
|
|
4186
|
+
} else if (sort.direction === "asc") {
|
|
4187
|
+
nextSort = { key: columnKey, direction: "desc" };
|
|
4188
|
+
} else {
|
|
4189
|
+
nextSort = null;
|
|
4190
|
+
}
|
|
4191
|
+
if (isControlledSort) {
|
|
4192
|
+
onSortChange?.(nextSort);
|
|
4193
|
+
} else {
|
|
4194
|
+
setUncontrolledSort(nextSort);
|
|
4195
|
+
opts.onUncontrolledSortChange?.();
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
const sortedData = useMemo(() => {
|
|
4199
|
+
if (isControlledSort || sort === null) return data;
|
|
4200
|
+
const col = columns.find((c) => c.key === sort.key);
|
|
4201
|
+
if (!col) return data;
|
|
4202
|
+
const sorted = [...data].sort((a, b) => {
|
|
4203
|
+
const aVal = col.render ? null : a[sort.key];
|
|
4204
|
+
const bVal = col.render ? null : b[sort.key];
|
|
4205
|
+
const cmp = compareValues(aVal, bVal);
|
|
4206
|
+
return sort.direction === "asc" ? cmp : -cmp;
|
|
4207
|
+
});
|
|
4208
|
+
return sorted;
|
|
4209
|
+
}, [data, sort, isControlledSort, columns]);
|
|
4210
|
+
return { sort, handleSort, sortedData };
|
|
4211
|
+
}
|
|
4212
|
+
var EXCLUSIVE_PROPS = ["pagination", "expandable"];
|
|
4213
|
+
function SpacerRow(props) {
|
|
4214
|
+
if (props.height <= 0) return null;
|
|
4215
|
+
return (
|
|
4216
|
+
// 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
|
|
4217
|
+
/* @__PURE__ */ jsx(
|
|
4218
|
+
"tr",
|
|
4219
|
+
{
|
|
4220
|
+
"data-slot": `data-table-virtual-spacer-${props.slot}`,
|
|
4221
|
+
role: "presentation",
|
|
4222
|
+
style: { height: `${props.height}px` },
|
|
4223
|
+
children: /* @__PURE__ */ jsx("td", { colSpan: props.colSpan, style: { padding: 0 } })
|
|
4224
|
+
}
|
|
4225
|
+
)
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
function DataTableVirtualizedBody(props) {
|
|
4229
|
+
const {
|
|
4230
|
+
data,
|
|
4231
|
+
columns,
|
|
4232
|
+
rowKey,
|
|
4233
|
+
virtualized,
|
|
4234
|
+
rowActions,
|
|
4235
|
+
defaultSort,
|
|
4236
|
+
sort: controlledSort,
|
|
4237
|
+
onSortChange,
|
|
4238
|
+
loading = false,
|
|
4239
|
+
emptyState,
|
|
4240
|
+
className
|
|
4241
|
+
} = props;
|
|
4242
|
+
if (isDev()) {
|
|
4243
|
+
for (const key of EXCLUSIVE_PROPS) {
|
|
4244
|
+
if (props[key]) {
|
|
4245
|
+
console.warn(
|
|
4246
|
+
`DataTable: \`${key}\` is ignored in virtualized mode \u2014 the modes are mutually exclusive.`
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
if (virtualized.rowHeight < 1) {
|
|
4251
|
+
console.warn("DataTable: virtualized.rowHeight must be >= 1px \u2014 falling back to 1.");
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
const rowHeight = Math.max(1, virtualized.rowHeight);
|
|
4255
|
+
const parentRef = useRef(null);
|
|
4256
|
+
const { sort, handleSort, sortedData } = useDataTableSort({
|
|
4257
|
+
data,
|
|
4258
|
+
columns,
|
|
4259
|
+
defaultSort,
|
|
4260
|
+
sort: controlledSort,
|
|
4261
|
+
onSortChange
|
|
4262
|
+
});
|
|
4263
|
+
const virtualizer = useVirtualizer({
|
|
4264
|
+
// Injection first (review F-dom-6): the @internal test hook can add
|
|
4265
|
+
// observeElementRect but can never override the core window contract.
|
|
4266
|
+
...virtualized.virtualizerOptions,
|
|
4267
|
+
count: sortedData.length,
|
|
4268
|
+
getScrollElement: () => parentRef.current,
|
|
4269
|
+
estimateSize: () => rowHeight,
|
|
4270
|
+
overscan: virtualized.overscan ?? 5
|
|
4271
|
+
});
|
|
4272
|
+
if (loading) {
|
|
4273
|
+
return /* @__PURE__ */ jsx(
|
|
4274
|
+
DataTableLoading,
|
|
4275
|
+
{
|
|
4276
|
+
columns,
|
|
4277
|
+
hasExpandColumn: false,
|
|
4278
|
+
hasActionsColumn: rowActions !== void 0,
|
|
4279
|
+
stickyHeader: true,
|
|
4280
|
+
className
|
|
4281
|
+
}
|
|
4282
|
+
);
|
|
4283
|
+
}
|
|
4284
|
+
if (sortedData.length === 0) {
|
|
4285
|
+
return /* @__PURE__ */ jsx("div", { className: cn("w-full", className), children: emptyState ?? /* @__PURE__ */ jsx(EmptyState, { title: "No data", description: "There's nothing here yet." }) });
|
|
4286
|
+
}
|
|
4287
|
+
const items = virtualizer.getVirtualItems();
|
|
4288
|
+
const totalSize = virtualizer.getTotalSize();
|
|
4289
|
+
const firstItem = items[0];
|
|
4290
|
+
const lastItem = items[items.length - 1];
|
|
4291
|
+
const paddingTop = firstItem ? firstItem.start : 0;
|
|
4292
|
+
const paddingBottom = lastItem ? totalSize - lastItem.end : totalSize;
|
|
4293
|
+
const colSpan = columns.length + (rowActions ? 1 : 0);
|
|
4294
|
+
return /* @__PURE__ */ jsx("div", { "data-slot": "data-table-virtual-body", className: cn("w-full", className), children: /* @__PURE__ */ jsx(
|
|
4295
|
+
"section",
|
|
4296
|
+
{
|
|
4297
|
+
ref: parentRef,
|
|
4298
|
+
"aria-label": "Table scroll area",
|
|
4299
|
+
tabIndex: 0,
|
|
4300
|
+
className: "overflow-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
4301
|
+
style: { height: virtualized.height },
|
|
4302
|
+
"data-slot": "data-table-virtual-scroll",
|
|
4303
|
+
children: /* @__PURE__ */ jsxs(Table, { "aria-rowcount": sortedData.length + 1, children: [
|
|
4304
|
+
/* @__PURE__ */ jsx(Table.Header, { className: "sticky top-0 z-10 bg-card", children: /* @__PURE__ */ jsx(
|
|
4305
|
+
DataTableHeaderRow,
|
|
4306
|
+
{
|
|
4307
|
+
columns,
|
|
4308
|
+
sort,
|
|
4309
|
+
onSortClick: handleSort,
|
|
4310
|
+
hasActionsColumn: rowActions !== void 0
|
|
4311
|
+
}
|
|
4312
|
+
) }),
|
|
4313
|
+
/* @__PURE__ */ jsxs(Table.Body, { children: [
|
|
4314
|
+
/* @__PURE__ */ jsx(SpacerRow, { slot: "top", height: paddingTop, colSpan }),
|
|
4315
|
+
items.map((vRow) => {
|
|
4316
|
+
const row = sortedData[vRow.index];
|
|
4317
|
+
return /* @__PURE__ */ jsxs(
|
|
4318
|
+
Table.Row,
|
|
4319
|
+
{
|
|
4320
|
+
"aria-rowindex": vRow.index + 2,
|
|
4321
|
+
style: { height: `${rowHeight}px` },
|
|
4322
|
+
children: [
|
|
4323
|
+
columns.map((col) => /* @__PURE__ */ jsx(
|
|
4324
|
+
Table.Cell,
|
|
4325
|
+
{
|
|
4326
|
+
align: col.align,
|
|
4327
|
+
className: cn("overflow-hidden py-1.5", col.className),
|
|
4328
|
+
children: renderCellContent(row, col)
|
|
4329
|
+
},
|
|
4330
|
+
col.key
|
|
4331
|
+
)),
|
|
4332
|
+
rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
|
|
4333
|
+
]
|
|
4334
|
+
},
|
|
4335
|
+
rowKey(row)
|
|
4336
|
+
);
|
|
4337
|
+
}),
|
|
4338
|
+
/* @__PURE__ */ jsx(SpacerRow, { slot: "bottom", height: paddingBottom, colSpan })
|
|
4339
|
+
] })
|
|
4340
|
+
] })
|
|
4341
|
+
}
|
|
4342
|
+
) });
|
|
4343
|
+
}
|
|
4104
4344
|
function DataTable(props) {
|
|
4345
|
+
if (props.virtualized) {
|
|
4346
|
+
return /* @__PURE__ */ jsx(DataTableVirtualizedBody, { ...props, virtualized: props.virtualized });
|
|
4347
|
+
}
|
|
4348
|
+
return /* @__PURE__ */ jsx(DataTableDefaultBody, { ...props });
|
|
4349
|
+
}
|
|
4350
|
+
function DataTableDefaultBody(props) {
|
|
4105
4351
|
const {
|
|
4106
4352
|
data,
|
|
4107
4353
|
columns,
|
|
@@ -4118,32 +4364,22 @@ function DataTable(props) {
|
|
|
4118
4364
|
emptyState,
|
|
4119
4365
|
className
|
|
4120
4366
|
} = props;
|
|
4121
|
-
const isControlledSort = onSortChange !== void 0;
|
|
4122
|
-
const [uncontrolledSort, setUncontrolledSort] = useState(
|
|
4123
|
-
defaultSort ?? null
|
|
4124
|
-
);
|
|
4125
|
-
const sort = isControlledSort ? controlledSort ?? null : uncontrolledSort;
|
|
4126
4367
|
const isControlledPage = pagination?.controlledPage !== void 0;
|
|
4127
4368
|
const [uncontrolledPage, setUncontrolledPage] = useState(0);
|
|
4128
4369
|
const currentPage = isControlledPage ? pagination?.controlledPage ?? 0 : uncontrolledPage;
|
|
4129
4370
|
const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);
|
|
4130
4371
|
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);
|
|
4372
|
+
const { sort, handleSort, sortedData } = useDataTableSort({
|
|
4373
|
+
data,
|
|
4374
|
+
columns,
|
|
4375
|
+
defaultSort,
|
|
4376
|
+
sort: controlledSort,
|
|
4377
|
+
onSortChange,
|
|
4378
|
+
// EC-8: sort change resets pagination to page 0
|
|
4379
|
+
onUncontrolledSortChange: () => {
|
|
4144
4380
|
if (!isControlledPage) setUncontrolledPage(0);
|
|
4145
4381
|
}
|
|
4146
|
-
}
|
|
4382
|
+
});
|
|
4147
4383
|
function handlePageChange(page) {
|
|
4148
4384
|
const zeroIdx = page - 1;
|
|
4149
4385
|
if (isControlledPage) {
|
|
@@ -4167,18 +4403,6 @@ function DataTable(props) {
|
|
|
4167
4403
|
});
|
|
4168
4404
|
}
|
|
4169
4405
|
}
|
|
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
4406
|
const visibleData = useMemo(() => {
|
|
4183
4407
|
if (!pagination) return sortedData;
|
|
4184
4408
|
if (isControlledPage) return sortedData;
|
|
@@ -4187,22 +4411,17 @@ function DataTable(props) {
|
|
|
4187
4411
|
}, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);
|
|
4188
4412
|
const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);
|
|
4189
4413
|
const expandedColSpan = columns.length + extraCols;
|
|
4190
|
-
const totalCols = columns.length + extraCols;
|
|
4191
4414
|
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
|
-
] }) });
|
|
4415
|
+
return /* @__PURE__ */ jsx(
|
|
4416
|
+
DataTableLoading,
|
|
4417
|
+
{
|
|
4418
|
+
columns,
|
|
4419
|
+
hasExpandColumn: expandable !== void 0,
|
|
4420
|
+
hasActionsColumn: rowActions !== void 0,
|
|
4421
|
+
stickyHeader,
|
|
4422
|
+
className
|
|
4423
|
+
}
|
|
4424
|
+
);
|
|
4206
4425
|
}
|
|
4207
4426
|
if (sortedData.length === 0) {
|
|
4208
4427
|
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 +4429,16 @@ function DataTable(props) {
|
|
|
4210
4429
|
const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;
|
|
4211
4430
|
return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), children: [
|
|
4212
4431
|
/* @__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
|
-
] }) }),
|
|
4432
|
+
/* @__PURE__ */ jsx(Table.Header, { className: stickyHeader ? "sticky top-0 z-10 bg-card" : void 0, children: /* @__PURE__ */ jsx(
|
|
4433
|
+
DataTableHeaderRow,
|
|
4434
|
+
{
|
|
4435
|
+
columns,
|
|
4436
|
+
sort,
|
|
4437
|
+
onSortClick: handleSort,
|
|
4438
|
+
hasExpandColumn: expandable !== void 0,
|
|
4439
|
+
hasActionsColumn: rowActions !== void 0
|
|
4440
|
+
}
|
|
4441
|
+
) }),
|
|
4232
4442
|
/* @__PURE__ */ jsx(Table.Body, { children: visibleData.map((row) => {
|
|
4233
4443
|
const key = rowKey(row);
|
|
4234
4444
|
const expandedContent = expandable ? expandable(row) : null;
|
|
@@ -4248,22 +4458,8 @@ function DataTable(props) {
|
|
|
4248
4458
|
children: isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-4" })
|
|
4249
4459
|
}
|
|
4250
4460
|
) : 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
|
|
4461
|
+
columns.map((col) => /* @__PURE__ */ jsx(Table.Cell, { align: col.align, className: col.className, children: renderCellContent(row, col) }, col.key)),
|
|
4462
|
+
rowActions ? /* @__PURE__ */ jsx(DataTableRowActionsCell, { children: rowActions(row) }) : null
|
|
4267
4463
|
] }),
|
|
4268
4464
|
isExpanded && isExpandable ? /* @__PURE__ */ jsx("tr", { id: `expanded-${key}`, children: /* @__PURE__ */ jsx("td", { colSpan: expandedColSpan, className: "bg-muted/30 p-4", children: expandedContent }) }) : null
|
|
4269
4465
|
] }, key);
|
|
@@ -4628,6 +4824,223 @@ var Stepper = forwardRef(
|
|
|
4628
4824
|
)
|
|
4629
4825
|
);
|
|
4630
4826
|
Stepper.displayName = "Stepper";
|
|
4827
|
+
|
|
4828
|
+
// src/components/primitives/file-dropzone/validate.ts
|
|
4829
|
+
function matchesAccept(file, accept) {
|
|
4830
|
+
const entries = Object.entries(accept ?? {});
|
|
4831
|
+
if (entries.length === 0) return true;
|
|
4832
|
+
if (file.type === "") {
|
|
4833
|
+
const exts = entries.flatMap(([, e]) => e);
|
|
4834
|
+
if (exts.length === 0) return true;
|
|
4835
|
+
return exts.some((ext) => file.name.toLowerCase().endsWith(ext.toLowerCase()));
|
|
4836
|
+
}
|
|
4837
|
+
return entries.some(([mime, exts]) => {
|
|
4838
|
+
if (mime === file.type) return true;
|
|
4839
|
+
if (mime.endsWith("/*") && file.type.startsWith(mime.slice(0, -1))) return true;
|
|
4840
|
+
return exts.some((ext) => file.name.toLowerCase().endsWith(ext.toLowerCase()));
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
function acceptedExtensions(accept) {
|
|
4844
|
+
const exts = Object.values(accept).flat();
|
|
4845
|
+
return exts.length > 0 ? exts.join(", ") : Object.keys(accept).join(", ");
|
|
4846
|
+
}
|
|
4847
|
+
function validateFiles(files, options = {}) {
|
|
4848
|
+
const { accept, maxSize, minSize, maxFiles, multiple = true, validator } = options;
|
|
4849
|
+
const accepted = [];
|
|
4850
|
+
const rejections = [];
|
|
4851
|
+
for (const file of files) {
|
|
4852
|
+
const errors = [];
|
|
4853
|
+
if (accept && !matchesAccept(file, accept)) {
|
|
4854
|
+
errors.push({
|
|
4855
|
+
code: "file-invalid-type",
|
|
4856
|
+
message: `File type must be one of ${acceptedExtensions(accept)}`
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
if (maxSize !== void 0 && file.size > maxSize) {
|
|
4860
|
+
errors.push({ code: "file-too-large", message: `File is larger than ${maxSize} bytes` });
|
|
4861
|
+
}
|
|
4862
|
+
if (minSize !== void 0 && file.size < minSize) {
|
|
4863
|
+
errors.push({ code: "file-too-small", message: `File is smaller than ${minSize} bytes` });
|
|
4864
|
+
}
|
|
4865
|
+
const custom = validator?.(file);
|
|
4866
|
+
if (custom) errors.push(...Array.isArray(custom) ? custom : [custom]);
|
|
4867
|
+
if (errors.length > 0) {
|
|
4868
|
+
rejections.push({ file, errors });
|
|
4869
|
+
} else {
|
|
4870
|
+
accepted.push(file);
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
const tooMany = !multiple && accepted.length > 1 || maxFiles !== void 0 && maxFiles >= 1 && accepted.length > maxFiles;
|
|
4874
|
+
if (tooMany) {
|
|
4875
|
+
const limit = !multiple ? 1 : maxFiles;
|
|
4876
|
+
for (const file of accepted) {
|
|
4877
|
+
rejections.push({
|
|
4878
|
+
file,
|
|
4879
|
+
errors: [{ code: "too-many-files", message: `Too many files \u2014 at most ${limit} allowed` }]
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
accepted.length = 0;
|
|
4883
|
+
}
|
|
4884
|
+
return { accepted, rejections };
|
|
4885
|
+
}
|
|
4886
|
+
function hasFiles(event) {
|
|
4887
|
+
return Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
4888
|
+
}
|
|
4889
|
+
var FileDropzone = forwardRef(
|
|
4890
|
+
({
|
|
4891
|
+
label,
|
|
4892
|
+
disabled = false,
|
|
4893
|
+
accept,
|
|
4894
|
+
maxSize,
|
|
4895
|
+
minSize,
|
|
4896
|
+
maxFiles,
|
|
4897
|
+
multiple = true,
|
|
4898
|
+
validator,
|
|
4899
|
+
onFilesAccepted,
|
|
4900
|
+
onFilesRejected,
|
|
4901
|
+
instructions,
|
|
4902
|
+
className,
|
|
4903
|
+
...props
|
|
4904
|
+
}, ref) => {
|
|
4905
|
+
const dragTargetsRef = useRef([]);
|
|
4906
|
+
const inputRef = useRef(null);
|
|
4907
|
+
const [dragState, setDragState] = useState("idle");
|
|
4908
|
+
const [rejections, setRejections] = useState([]);
|
|
4909
|
+
const validateOptions = {
|
|
4910
|
+
accept,
|
|
4911
|
+
maxSize,
|
|
4912
|
+
minSize,
|
|
4913
|
+
maxFiles,
|
|
4914
|
+
multiple,
|
|
4915
|
+
validator
|
|
4916
|
+
};
|
|
4917
|
+
const processFiles = (files) => {
|
|
4918
|
+
if (files.length === 0) return;
|
|
4919
|
+
const result = validateFiles(files, validateOptions);
|
|
4920
|
+
setRejections(result.rejections);
|
|
4921
|
+
if (result.accepted.length > 0) onFilesAccepted?.(result.accepted);
|
|
4922
|
+
if (result.rejections.length > 0) onFilesRejected?.(result.rejections);
|
|
4923
|
+
};
|
|
4924
|
+
const openPicker = () => {
|
|
4925
|
+
const input = inputRef.current;
|
|
4926
|
+
if (!input || disabled) return;
|
|
4927
|
+
input.value = "";
|
|
4928
|
+
input.click();
|
|
4929
|
+
};
|
|
4930
|
+
const onKeyDown = (event) => {
|
|
4931
|
+
if (event.target !== event.currentTarget) return;
|
|
4932
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
4933
|
+
event.preventDefault();
|
|
4934
|
+
openPicker();
|
|
4935
|
+
}
|
|
4936
|
+
};
|
|
4937
|
+
const onDragEnter = (event) => {
|
|
4938
|
+
event.preventDefault();
|
|
4939
|
+
if (disabled || !hasFiles(event)) return;
|
|
4940
|
+
dragTargetsRef.current = [...dragTargetsRef.current, event.target];
|
|
4941
|
+
const items = Array.from(event.dataTransfer?.items ?? []).filter(
|
|
4942
|
+
(item) => item.kind === "file"
|
|
4943
|
+
);
|
|
4944
|
+
const reject = accept !== void 0 && items.length > 0 && items.some(
|
|
4945
|
+
(item) => item.type !== "" && !matchesAccept(
|
|
4946
|
+
new File([], item.getAsFile?.()?.name ?? "", { type: item.type }),
|
|
4947
|
+
accept
|
|
4948
|
+
)
|
|
4949
|
+
);
|
|
4950
|
+
setDragState(reject ? "drag-reject" : "drag-over");
|
|
4951
|
+
};
|
|
4952
|
+
const onDragOver = (event) => {
|
|
4953
|
+
event.preventDefault();
|
|
4954
|
+
if (event.dataTransfer) {
|
|
4955
|
+
try {
|
|
4956
|
+
event.dataTransfer.dropEffect = "copy";
|
|
4957
|
+
} catch {
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
};
|
|
4961
|
+
const onDragLeave = (event) => {
|
|
4962
|
+
event.preventDefault();
|
|
4963
|
+
const root = event.currentTarget;
|
|
4964
|
+
const targets = dragTargetsRef.current.filter((t) => root.contains(t));
|
|
4965
|
+
const idx = targets.indexOf(event.target);
|
|
4966
|
+
if (idx !== -1) targets.splice(idx, 1);
|
|
4967
|
+
dragTargetsRef.current = targets;
|
|
4968
|
+
if (targets.length === 0) setDragState("idle");
|
|
4969
|
+
};
|
|
4970
|
+
const onDrop = (event) => {
|
|
4971
|
+
event.preventDefault();
|
|
4972
|
+
dragTargetsRef.current = [];
|
|
4973
|
+
setDragState("idle");
|
|
4974
|
+
if (disabled || !hasFiles(event)) return;
|
|
4975
|
+
processFiles(Array.from(event.dataTransfer?.files ?? []));
|
|
4976
|
+
};
|
|
4977
|
+
const state = disabled ? "disabled" : dragState;
|
|
4978
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
4979
|
+
/* @__PURE__ */ jsx(
|
|
4980
|
+
"div",
|
|
4981
|
+
{
|
|
4982
|
+
"data-slot": "file-dropzone",
|
|
4983
|
+
"data-state": state,
|
|
4984
|
+
ref,
|
|
4985
|
+
role: "button",
|
|
4986
|
+
"aria-label": label,
|
|
4987
|
+
"aria-disabled": disabled || void 0,
|
|
4988
|
+
tabIndex: disabled ? void 0 : 0,
|
|
4989
|
+
onClick: () => openPicker(),
|
|
4990
|
+
onKeyDown,
|
|
4991
|
+
onDragEnter,
|
|
4992
|
+
onDragOver,
|
|
4993
|
+
onDragLeave,
|
|
4994
|
+
onDrop,
|
|
4995
|
+
className: cn(
|
|
4996
|
+
"flex min-h-24 cursor-pointer flex-col items-center justify-center gap-1 rounded-md border border-border border-dashed p-4 text-center",
|
|
4997
|
+
state === "drag-over" && "border-primary bg-primary/5",
|
|
4998
|
+
state === "drag-reject" && "border-destructive bg-destructive/5",
|
|
4999
|
+
state === "disabled" && "cursor-not-allowed opacity-50",
|
|
5000
|
+
className
|
|
5001
|
+
),
|
|
5002
|
+
...props,
|
|
5003
|
+
children: /* @__PURE__ */ jsx(
|
|
5004
|
+
"div",
|
|
5005
|
+
{
|
|
5006
|
+
"data-slot": "file-dropzone-instructions",
|
|
5007
|
+
className: "text-body-sm text-muted-foreground",
|
|
5008
|
+
children: instructions ?? "Drag files here or click to browse."
|
|
5009
|
+
}
|
|
5010
|
+
)
|
|
5011
|
+
}
|
|
5012
|
+
),
|
|
5013
|
+
/* @__PURE__ */ jsx(
|
|
5014
|
+
"input",
|
|
5015
|
+
{
|
|
5016
|
+
"data-slot": "file-dropzone-input",
|
|
5017
|
+
ref: inputRef,
|
|
5018
|
+
type: "file",
|
|
5019
|
+
"aria-hidden": "true",
|
|
5020
|
+
tabIndex: -1,
|
|
5021
|
+
multiple,
|
|
5022
|
+
accept: accept ? Object.entries(accept).flatMap(([m, e]) => [m, ...e]).join(",") : void 0,
|
|
5023
|
+
disabled,
|
|
5024
|
+
className: "sr-only",
|
|
5025
|
+
onChange: (event) => processFiles(Array.from(event.target.files ?? []))
|
|
5026
|
+
}
|
|
5027
|
+
),
|
|
5028
|
+
rejections.length > 0 && /* @__PURE__ */ jsx(
|
|
5029
|
+
"ul",
|
|
5030
|
+
{
|
|
5031
|
+
"data-slot": "file-dropzone-rejections",
|
|
5032
|
+
className: "space-y-0.5 text-destructive text-label",
|
|
5033
|
+
children: rejections.map((r) => /* @__PURE__ */ jsxs("li", { children: [
|
|
5034
|
+
r.file.name,
|
|
5035
|
+
" \u2014 ",
|
|
5036
|
+
r.errors.map((e) => e.message).join("; ")
|
|
5037
|
+
] }, r.file.name))
|
|
5038
|
+
}
|
|
5039
|
+
)
|
|
5040
|
+
] });
|
|
5041
|
+
}
|
|
5042
|
+
);
|
|
5043
|
+
FileDropzone.displayName = "FileDropzone";
|
|
4631
5044
|
var MetricsPanel = forwardRef(
|
|
4632
5045
|
({ className, title, description, metrics, columns, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
4633
5046
|
"div",
|
|
@@ -5558,4 +5971,4 @@ function CommandPalette({
|
|
|
5558
5971
|
] }) });
|
|
5559
5972
|
}
|
|
5560
5973
|
|
|
5561
|
-
export { AccountMenu, ActionBar, Alert, Avatar, BadgeWithDot as Badge, Breadcrumb, Button, Card, Checkbox, CodeBlock, Combobox, CommandPalette, ConfirmDialog, CopyButton, DangerZone, DataTable, DeploymentRow, DescriptionList, Dialog, DomainConfig, DropdownMenu, EmptyState, EnvVarEditor, FormField, Input, JsonViewer, Label, LoginSplit, MetricCard, MetricsPanel, PageShell, Pagination, PinInput, PlanBadge, PreviewEnvCard, Progress, ProjectCard, RadioGroup, RollbackUI, ScrollArea, Select, Sheet, Sidebar, Skeleton, Slider, SocialAuthRow, StatTile, StatusDot, StatusIndicator, Stepper, Switch, Table, Tabs, TaskHeader, Textarea, Timestamp, Toast, Toaster, TooltipWithStatics as Tooltip, TopNav, TrendChart, UpdateBanner, avatarVariants, badgeVariants, buttonVariants, cn, computePageRange, deriveSteps, isNearBottom, linScale, niceMax, seriesPath, sheetVariants, useStickToBottom, useToast };
|
|
5974
|
+
export { AccountMenu, ActionBar, Alert, Avatar, BadgeWithDot as Badge, Breadcrumb, Button, Card, Checkbox, CodeBlock, Combobox, CommandPalette, ConfirmDialog, CopyButton, DangerZone, DataTable, DeploymentRow, DescriptionList, Dialog, DomainConfig, DropdownMenu, EmptyState, EnvVarEditor, FileDropzone, FormField, Input, JsonViewer, Label, LoginSplit, MetricCard, MetricsPanel, PageShell, Pagination, PinInput, PlanBadge, PreviewEnvCard, Progress, ProjectCard, RadioGroup, RollbackUI, ScrollArea, Select, Sheet, Sidebar, Skeleton, Slider, SocialAuthRow, StatTile, StatusDot, StatusIndicator, Stepper, Switch, Table, Tabs, TaskHeader, Textarea, Timestamp, Toast, Toaster, TooltipWithStatics as Tooltip, TopNav, TrendChart, UpdateBanner, avatarVariants, badgeVariants, buttonVariants, cn, computePageRange, deriveSteps, isNearBottom, linScale, matchesAccept, niceMax, seriesPath, sheetVariants, useStickToBottom, useToast, validateFiles };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usetheo/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
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",
|