advanced-react-datatable
Version:
A comprehensive React data grid component with advanced features like pinned columns, inline editing, grouping, filtering, and export capabilities
1,253 lines • 88 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import * as React from "react";
import { useState, useMemo, useEffect, useCallback } from "react";
import { X, ChevronDown, Pin, ArrowUpDown, ArrowUp, ArrowDown, Check, ChevronRight, Download, Filter, Settings, Maximize2, GripVertical, EyeOff, Eye, ChevronsLeft, ChevronLeft, ChevronsRight } from "lucide-react";
import { cva } from "class-variance-authority";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Transition, Dialog as Dialog$1 } from "@headlessui/react";
function cn(...inputs) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
const Button = React.forwardRef(
(_a, ref) => {
var _b = _a, { className, variant, size } = _b, props = __objRest(_b, ["className", "variant", "size"]);
return /* @__PURE__ */ jsx(
"button",
__spreadValues({
className: cn(buttonVariants({ variant, size, className })),
ref
}, props)
);
}
);
Button.displayName = "Button";
const Input = React.forwardRef(
(_c, ref) => {
var _d = _c, { className, type } = _d, props = __objRest(_d, ["className", "type"]);
return /* @__PURE__ */ jsx(
"input",
__spreadValues({
type,
className: cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
),
ref
}, props)
);
}
);
Input.displayName = "Input";
const PopoverContext = React.createContext(null);
const Popover = ({ children }) => {
const [isOpen, setIsOpen] = React.useState(false);
return /* @__PURE__ */ jsx(PopoverContext.Provider, { value: { isOpen, setIsOpen }, children: /* @__PURE__ */ jsx("div", { className: "relative inline-block w-full", children }) });
};
const PopoverTrigger = ({ children, asChild }) => {
const context = React.useContext(PopoverContext);
if (asChild) {
return React.cloneElement(children, {
onClick: () => context == null ? void 0 : context.setIsOpen(!context.isOpen)
});
}
return /* @__PURE__ */ jsx("div", { onClick: () => context == null ? void 0 : context.setIsOpen(!context.isOpen), children });
};
const PopoverContent = React.forwardRef((_e, ref) => {
var _f = _e, { className, children, align = "center", side = "bottom", sideOffset = 4 } = _f, props = __objRest(_f, ["className", "children", "align", "side", "sideOffset"]);
const context = React.useContext(PopoverContext);
const contentRef = React.useRef(null);
React.useEffect(() => {
const handleClickOutside = (event) => {
if (contentRef.current && !contentRef.current.contains(event.target)) {
context == null ? void 0 : context.setIsOpen(false);
}
};
if (context == null ? void 0 : context.isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [context == null ? void 0 : context.isOpen, context]);
if (!(context == null ? void 0 : context.isOpen)) return null;
const sideClass = side === "top" ? "bottom-full mb-1" : side === "left" ? "right-full mr-1" : side === "right" ? "left-full ml-1" : "top-full mt-1";
const alignClass = align === "start" ? "left-0" : align === "end" ? "right-0" : "left-1/2 -translate-x-1/2";
return /* @__PURE__ */ jsx(
"div",
__spreadProps(__spreadValues({
ref: contentRef,
className: cn(
"absolute z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
sideClass,
alignClass,
className
)
}, props), {
children
})
);
});
PopoverContent.displayName = "PopoverContent";
const badgeVariants = cva(
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
{
variants: {
variant: {
default: "bg-blue-50 text-blue-700 ring-blue-700/10",
secondary: "bg-gray-50 text-gray-600 ring-gray-500/10",
destructive: "bg-green-50 text-green-700 ring-green-600/20",
outline: "bg-gray-50 text-gray-600 ring-gray-500/10"
}
},
defaultVariants: {
variant: "default"
}
}
);
function Badge(_g) {
var _h = _g, { className, variant } = _h, props = __objRest(_h, ["className", "variant"]);
return /* @__PURE__ */ jsx("div", __spreadValues({ className: cn(badgeVariants({ variant }), className) }, props));
}
function getUniqueValues(data, field) {
const values = /* @__PURE__ */ new Set();
data.forEach((row) => {
const value = row[field];
if (value != null) {
values.add(String(value));
}
});
return Array.from(values).sort();
}
function filterData(data, filters) {
return data.filter((row) => {
return filters.every((filter) => {
const value = row[filter.field];
const filterValue = filter.value;
if (value == null) return false;
switch (filter.operator) {
case "contains":
return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
case "equals":
return value === filterValue;
case "startsWith":
return String(value).toLowerCase().startsWith(String(filterValue).toLowerCase());
case "endsWith":
return String(value).toLowerCase().endsWith(String(filterValue).toLowerCase());
case "gt":
return Number(value) > Number(filterValue);
case "lt":
return Number(value) < Number(filterValue);
case "gte":
if (typeof filterValue === "object" && filterValue.min !== void 0) {
const numValue = Number(value);
const minValue = Number(filterValue.min);
const maxValue = Number(filterValue.max);
return numValue >= minValue && (filterValue.max === "" || numValue <= maxValue);
}
return Number(value) >= Number(filterValue);
case "lte":
if (typeof filterValue === "object" && filterValue.max !== void 0) {
const numValue = Number(value);
const minValue = Number(filterValue.min);
const maxValue = Number(filterValue.max);
return numValue <= maxValue && (filterValue.min === "" || numValue >= minValue);
}
return Number(value) <= Number(filterValue);
case "in":
if (Array.isArray(filterValue)) {
return filterValue.includes(String(value));
}
return filterValue === String(value);
default:
return true;
}
});
});
}
function sortData(data, sorts) {
if (sorts.length === 0) return data;
return [...data].sort((a, b) => {
for (const sort of sorts) {
const aValue = a[sort.field];
const bValue = b[sort.field];
if (aValue == null && bValue == null) continue;
if (aValue == null) return 1;
if (bValue == null) return -1;
let comparison = 0;
const aNum = Number(aValue);
const bNum = Number(bValue);
if (!isNaN(aNum) && !isNaN(bNum)) {
comparison = aNum - bNum;
} else {
const aStr = String(aValue).toLowerCase();
const bStr = String(bValue).toLowerCase();
comparison = aStr < bStr ? -1 : aStr > bStr ? 1 : 0;
}
if (comparison !== 0) {
return sort.direction === "asc" ? comparison : -comparison;
}
}
return 0;
});
}
function groupData(data, groupFields, expandedGroups, columns) {
const fields = Array.isArray(groupFields) ? groupFields : [groupFields];
if (fields.length === 0 || fields[0] === "") return data;
function buildHierarchy(items, fieldIndex, parentPath = "") {
if (fieldIndex >= fields.length) {
return items;
}
const currentField = fields[fieldIndex];
const groups = /* @__PURE__ */ new Map();
items.forEach((item) => {
const value = String(item[currentField] || "Ungrouped");
if (!groups.has(value)) {
groups.set(value, []);
}
groups.get(value).push(item);
});
const result = [];
groups.forEach((groupItems, groupValue) => {
const groupPath = parentPath ? `${parentPath}|${groupValue}` : groupValue;
const isExpanded = expandedGroups.has(groupPath);
function getTotalCount(items2) {
return items2.length;
}
const summaries = /* @__PURE__ */ new Map();
columns.forEach((column) => {
if (column.type === "number") {
const sum = groupItems.reduce((acc, item) => {
const value = Number(item[column.field]);
return acc + (isNaN(value) ? 0 : value);
}, 0);
summaries.set(String(column.field), sum);
}
});
const groupHeader = {
__isGroupHeader: true,
__groupFields: [currentField],
__groupValue: groupValue,
__groupKey: groupPath,
__itemCount: getTotalCount(groupItems),
__expanded: isExpanded,
__summaries: summaries,
__level: fieldIndex,
__field: currentField
};
result.push(groupHeader);
if (isExpanded) {
if (fieldIndex === fields.length - 1) {
result.push(...groupItems.map((groupItem) => __spreadProps(__spreadValues({}, groupItem), {
__level: fields.length + 1
})));
} else {
const nestedGroups = buildHierarchy(groupItems, fieldIndex + 1, groupPath);
result.push(...nestedGroups);
}
}
});
return result;
}
return buildHierarchy(data, 0);
}
function DataTableColumnFilter({
column,
filter,
data,
onFilterChange
}) {
var _a, _b;
const [filterValue, setFilterValue] = useState((filter == null ? void 0 : filter.value) || "");
const [rangeMin, setRangeMin] = useState(
(filter == null ? void 0 : filter.operator) === "gte" ? filter.value : ((_a = filter == null ? void 0 : filter.value) == null ? void 0 : _a.min) || ""
);
const [rangeMax, setRangeMax] = useState(
(filter == null ? void 0 : filter.operator) === "lte" ? filter.value : ((_b = filter == null ? void 0 : filter.value) == null ? void 0 : _b.max) || ""
);
const [selectedValues, setSelectedValues] = useState(
(filter == null ? void 0 : filter.operator) === "in" && Array.isArray(filter.value) ? filter.value : []
);
const filterOptions = useMemo(() => {
if (column.type === "select" || column.type === "boolean") {
return getUniqueValues(data, column.field);
}
return column.options || [];
}, [data, column.field, column.type, column.options]);
useEffect(() => {
if (filter) {
setFilterValue(filter.value || "");
if (filter.operator === "gte") {
if (typeof filter.value === "object" && filter.value.min !== void 0) {
setRangeMin(filter.value.min);
setRangeMax(filter.value.max || "");
} else {
setRangeMin(filter.value);
}
} else if (filter.operator === "lte") {
if (typeof filter.value === "object" && filter.value.max !== void 0) {
setRangeMax(filter.value.max);
setRangeMin(filter.value.min || "");
} else {
setRangeMax(filter.value);
}
}
if (filter.operator === "in" && Array.isArray(filter.value)) {
setSelectedValues(filter.value);
}
} else {
setFilterValue("");
setRangeMin("");
setRangeMax("");
setSelectedValues([]);
}
}, [filter]);
const handleFilterChange = useCallback((value, operator = "contains") => {
if (value === "" || value === null || value === void 0 || value === "__all__") {
onFilterChange(null);
return;
}
const newFilter = {
field: String(column.field),
operator,
value,
type: column.type || "text"
};
onFilterChange(newFilter);
}, [column.field, column.type, onFilterChange]);
const handleMultiSelectChange = useCallback((value, checked) => {
setSelectedValues((prev) => {
const newValues = checked ? [...prev, value] : prev.filter((v) => v !== value);
if (newValues.length === 0) {
onFilterChange(null);
return [];
}
const newFilter = {
field: String(column.field),
operator: "in",
value: newValues,
type: column.type || "text"
};
onFilterChange(newFilter);
return newValues;
});
}, [column.field, column.type, onFilterChange]);
const clearMultiSelect = useCallback(() => {
setSelectedValues([]);
onFilterChange(null);
}, [onFilterChange]);
const handleRangeChange = useCallback(() => {
if (rangeMin !== "" && rangeMax !== "") {
const newFilter = {
field: String(column.field),
operator: "gte",
value: { min: rangeMin, max: rangeMax },
type: column.type || "number"
};
onFilterChange(newFilter);
} else if (rangeMin !== "") {
handleFilterChange(rangeMin, "gte");
} else if (rangeMax !== "") {
handleFilterChange(rangeMax, "lte");
} else {
onFilterChange(null);
}
}, [rangeMin, rangeMax, handleFilterChange]);
const clearFilter = useCallback(() => {
setFilterValue("");
setRangeMin("");
setRangeMax("");
onFilterChange(null);
}, [onFilterChange]);
if (!column.filterable) {
return /* @__PURE__ */ jsx("div", { className: "h-8" });
}
const renderFilter = () => {
switch (column.type) {
case "select":
return /* @__PURE__ */ jsxs(Popover, { children: [
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
Button,
{
variant: "outline",
size: "sm",
className: "h-8 w-full border-0 bg-gray-200 rounded-sm justify-between text-sm font-light min-w-0",
children: [
/* @__PURE__ */ jsx("span", { className: "truncate", children: selectedValues.length === 0 ? "All" : selectedValues.length === 1 ? selectedValues[0] : `${selectedValues.length} selected` }),
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 opacity-50 flex-shrink-0" })
]
}
) }),
/* @__PURE__ */ jsx(PopoverContent, { className: "w-56 p-2 bg-white ring-1 ring-gray-900/5", align: "start", children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: "Filter options" }),
selectedValues.length > 0 && /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
onClick: clearMultiSelect,
className: "h-6 px-2 text-xs",
children: "Clear"
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto", children: filterOptions.map((option) => /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2 py-1", children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
id: `${String(column.field)}-${option}`,
checked: selectedValues.includes(option),
onChange: (e) => handleMultiSelectChange(option, e.target.checked),
className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
}
),
/* @__PURE__ */ jsx(
"label",
{
htmlFor: `${String(column.field)}-${option}`,
className: "text-sm font-normal cursor-pointer flex-1",
children: option
}
)
] }, option)) }),
selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { className: "border-t pt-2", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: selectedValues.map((value) => /* @__PURE__ */ jsxs(
Badge,
{
variant: "secondary",
className: "text-xs",
children: [
value,
/* @__PURE__ */ jsx(
X,
{
className: "h-3 w-3 ml-1 cursor-pointer",
onClick: () => handleMultiSelectChange(value, false)
}
)
]
},
value
)) }) })
] }) })
] });
case "number":
return /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
/* @__PURE__ */ jsx(
Input,
{
type: "number",
placeholder: "Min",
value: rangeMin,
onChange: (e) => setRangeMin(e.target.value),
onBlur: handleRangeChange,
className: "h-8 text-sm font-light border-0 bg-gray-200 rounded-sm"
}
),
/* @__PURE__ */ jsx(
Input,
{
type: "number",
placeholder: "Max",
value: rangeMax,
onChange: (e) => setRangeMax(e.target.value),
onBlur: handleRangeChange,
className: "h-8 text-sm font-light border-0 bg-gray-200 rounded-sm"
}
)
] });
case "boolean":
return /* @__PURE__ */ jsxs(Popover, { children: [
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
Button,
{
variant: "outline",
size: "sm",
className: "h-8 w-full justify-between text-sm font-light border-0 bg-gray-200 rounded-sm min-w-0",
children: [
/* @__PURE__ */ jsx("span", { className: "truncate", children: selectedValues.length === 0 ? "All" : selectedValues.length === 1 ? selectedValues[0] : `${selectedValues.length} selected` }),
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 opacity-50 flex-shrink-0" })
]
}
) }),
/* @__PURE__ */ jsx(PopoverContent, { className: "w-40 p-2", align: "start", children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: "Status" }),
selectedValues.length > 0 && /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
onClick: clearMultiSelect,
className: "h-6 px-2 text-xs",
children: "Clear"
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: filterOptions.map((option) => /* @__PURE__ */ jsxs("div", { className: "flex items-center space-x-2", children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
id: `${String(column.field)}-${option}`,
checked: selectedValues.includes(option),
onChange: (e) => handleMultiSelectChange(option, e.target.checked),
className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
}
),
/* @__PURE__ */ jsx(
"label",
{
htmlFor: `${String(column.field)}-${option}`,
className: "text-xs font-normal cursor-pointer flex-1",
children: option
}
)
] }, option)) })
] }) })
] });
default:
return /* @__PURE__ */ jsx(
Input,
{
type: "text",
placeholder: "Filter...",
value: filterValue,
onChange: (e) => setFilterValue(e.target.value),
onBlur: () => handleFilterChange(filterValue),
onKeyDown: (e) => e.key === "Enter" && handleFilterChange(filterValue),
className: "h-8 text-sm font-light w-full min-w-0 border-0 bg-gray-200 rounded-sm"
}
);
}
};
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 w-full", children: [
/* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0", children: renderFilter() }),
filter && /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: "h-6 w-6 p-0 flex-shrink-0",
onClick: clearFilter,
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
}
)
] });
}
function DataTableHeader({
columns,
sorts,
filters,
selectedRows,
totalRows,
data,
onSort,
onSelectAll,
onFilterChange,
// showSelection,
showFilters = true
}) {
const getSortIcon = (field) => {
const sort = sorts == null ? void 0 : sorts.find((s) => s.field === field);
if (!sort) return /* @__PURE__ */ jsx(ArrowUpDown, { className: "h-4 w-4" });
return sort.direction === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "h-4 w-4 text-primary-500" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "h-4 w-4 text-primary-500" });
};
const visibleColumns = columns.filter((col) => !col.hidden);
const pinnedLeftColumns = visibleColumns.filter((col) => col.pinned === "left");
const unpinnedColumns = visibleColumns.filter((col) => !col.pinned);
const pinnedRightColumns = visibleColumns.filter((col) => col.pinned === "right");
const renderHeaderCell = (column, isPinned = false) => {
const currentFilter = filters == null ? void 0 : filters.find((f) => f.field === String(column.field));
if (column.useSelection) {
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
"px-4 py-3 text-left bg-gray-50",
isPinned && "sticky z-20 bg-gray-50",
column.pinned === "left" && "left-0 border-r border-gray-800/10",
column.pinned === "right" && "right-0 border-l border-gray-800/10"
),
style: {
minWidth: column.minWidth || (column.filterable ? "180px" : "120px"),
maxWidth: column.maxWidth,
width: column.width
},
children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
column.headerRenderer ? column.headerRenderer(column, { getSortIcon, onSort }, { selectedRows, onSelectAll, totalRows }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: selectedRows.length > 0 && selectedRows.length === totalRows,
onClick: (e) => {
e.stopPropagation();
},
onChange: () => {
onSelectAll();
},
className: cn(
"h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600",
"transition-all duration-150 ease-in-out",
"hover:scale-110 focus:scale-110"
)
}
),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsx("span", { className: "font-medium text-gray-900", children: column.header }),
column.pinned && /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-primary-500" }),
column.sortable && /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: cn(
"p-0 h-4 w-4",
"transition-all duration-200 ease-in-out",
"hover:scale-110 hover:bg-gray-100 hover:shadow-sm",
"active:scale-95",
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1"
),
onClick: () => onSort(String(column.field)),
children: /* @__PURE__ */ jsx("div", { className: "transition-transform duration-200 ease-in-out hover:rotate-[5deg]", children: getSortIcon(String(column.field)) })
}
)
] })
] }),
showFilters && column.filterable && /* @__PURE__ */ jsx("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx(
DataTableColumnFilter,
{
column,
filter: currentFilter,
data,
onFilterChange: (filter) => onFilterChange(String(column.field), filter)
}
) })
] })
},
String(column.field)
);
}
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
"px-4 py-3 text-left bg-gray-50",
isPinned && "sticky z-20 bg-gray-50",
column.pinned === "left" && "left-0 border-r border-gray-800/10",
column.pinned === "right" && "right-0 border-l border-gray-800/10"
),
style: {
minWidth: column.minWidth || (column.filterable ? "180px" : "120px"),
maxWidth: column.maxWidth,
width: column.width
},
children: /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: column.headerRenderer ? column.headerRenderer(column, { getSortIcon, onSort }) : /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("span", { className: "font-medium text-gray-900", children: column.header }),
column.pinned && /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-primary-500" }),
column.sortable && /* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: cn(
"p-0 h-4 w-4",
"transition-all duration-200 ease-in-out",
"hover:scale-110 hover:bg-gray-100 hover:shadow-sm",
"active:scale-95",
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1"
),
onClick: () => onSort(String(column.field)),
children: /* @__PURE__ */ jsx("div", { className: "transition-transform duration-200 ease-in-out hover:rotate-[5deg]", children: getSortIcon(String(column.field)) })
}
)
] }) }),
showFilters && column.filterable && /* @__PURE__ */ jsx("div", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx(
DataTableColumnFilter,
{
column,
filter: currentFilter,
data,
onFilterChange: (filter) => onFilterChange(String(column.field), filter)
}
) })
] })
},
String(column.field)
);
};
const generateGridColumns = () => {
const allColumns = [...pinnedLeftColumns, ...unpinnedColumns, ...pinnedRightColumns];
return allColumns.map((column, index) => {
if (index === 0) {
return "minmax(200px, 1fr)";
}
const width = column.width || column.minWidth || (column.filterable ? 180 : 120);
return `${width}px`;
}).join(" ");
};
return /* @__PURE__ */ jsxs(
"div",
{
className: "bg-gray-50 sticky top-0 z-30 grid border-b border-gray-200",
style: {
gridTemplateColumns: generateGridColumns(),
minWidth: "fit-content"
},
children: [
pinnedLeftColumns.map((column) => renderHeaderCell(column, true)),
unpinnedColumns.map((column) => renderHeaderCell(column, false)),
pinnedRightColumns.map((column) => renderHeaderCell(column, true))
]
}
);
}
const SelectContext = React.createContext(null);
const Select = ({ children, value, onValueChange, defaultValue = "" }) => {
const [isOpen, setIsOpen] = React.useState(false);
const [internalValue, setInternalValue] = React.useState(defaultValue);
const currentValue = value !== void 0 ? value : internalValue;
const handleValueChange = (newValue) => {
if (value === void 0) {
setInternalValue(newValue);
}
onValueChange == null ? void 0 : onValueChange(newValue);
setIsOpen(false);
};
return /* @__PURE__ */ jsx(SelectContext.Provider, { value: { isOpen, setIsOpen, value: currentValue, onValueChange: handleValueChange }, children: /* @__PURE__ */ jsx("div", { className: "relative w-full", children }) });
};
const SelectValue = ({ placeholder = "Select..." }) => {
const context = React.useContext(SelectContext);
return /* @__PURE__ */ jsx("span", { children: (context == null ? void 0 : context.value) || placeholder });
};
const SelectTrigger = React.forwardRef((_i, ref) => {
var _j = _i, { className, children } = _j, props = __objRest(_j, ["className", "children"]);
const context = React.useContext(SelectContext);
return /* @__PURE__ */ jsxs(
"button",
__spreadProps(__spreadValues({
ref,
type: "button",
className: cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
),
onClick: () => context == null ? void 0 : context.setIsOpen(!context.isOpen)
}, props), {
children: [
children,
/* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" })
]
})
);
});
SelectTrigger.displayName = "SelectTrigger";
const SelectContent = React.forwardRef((_k, ref) => {
var _l = _k, { className, children, position = "popper" } = _l, props = __objRest(_l, ["className", "children", "position"]);
const context = React.useContext(SelectContext);
const contentRef = React.useRef(null);
React.useEffect(() => {
const handleClickOutside = (event) => {
if (contentRef.current && !contentRef.current.contains(event.target)) {
context == null ? void 0 : context.setIsOpen(false);
}
};
if (context == null ? void 0 : context.isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [context == null ? void 0 : context.isOpen, context]);
if (!(context == null ? void 0 : context.isOpen)) return null;
return /* @__PURE__ */ jsx(
"div",
__spreadProps(__spreadValues({
ref: contentRef,
className: cn(
"absolute z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md top-full mt-1 animate-in fade-in-0 zoom-in-95",
className
)
}, props), {
children: /* @__PURE__ */ jsx("div", { className: "p-1", children })
})
);
});
SelectContent.displayName = "SelectContent";
const SelectLabel = React.forwardRef((_m, ref) => {
var _n = _m, { className, children } = _n, props = __objRest(_n, ["className", "children"]);
return /* @__PURE__ */ jsx(
"div",
__spreadProps(__spreadValues({
ref,
className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)
}, props), {
children
})
);
});
SelectLabel.displayName = "SelectLabel";
const SelectItem = React.forwardRef((_o, ref) => {
var _p = _o, { className, children, value } = _p, props = __objRest(_p, ["className", "children", "value"]);
const context = React.useContext(SelectContext);
const isSelected = (context == null ? void 0 : context.value) === value;
return /* @__PURE__ */ jsxs(
"div",
__spreadProps(__spreadValues({
ref,
className: cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
),
onClick: () => context == null ? void 0 : context.onValueChange(value)
}, props), {
children: [
/* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: isSelected && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }),
/* @__PURE__ */ jsx("span", { children })
]
})
);
});
SelectItem.displayName = "SelectItem";
const SelectSeparator = React.forwardRef((_q, ref) => {
var _r = _q, { className } = _r, props = __objRest(_r, ["className"]);
return /* @__PURE__ */ jsx(
"div",
__spreadValues({
ref,
className: cn("-mx-1 my-1 h-px bg-muted", className)
}, props)
);
});
SelectSeparator.displayName = "SelectSeparator";
function DataTableEditableCell({
value,
// row,
column,
onSave,
onCancel
}) {
const [editValue, setEditValue] = useState(value);
const handleSave = useCallback(() => {
onSave(editValue);
}, [editValue, onSave]);
const handleKeyDown = useCallback((e) => {
if (e.key === "Enter") {
handleSave();
} else if (e.key === "Escape") {
onCancel();
}
}, [handleSave, onCancel]);
const renderEditor = () => {
var _a;
switch (column.type) {
case "select":
return /* @__PURE__ */ jsxs(Select, { value: editValue, onValueChange: setEditValue, children: [
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
/* @__PURE__ */ jsx(SelectContent, { children: (_a = column.options) == null ? void 0 : _a.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: option }, option)) })
] });
case "number":
return /* @__PURE__ */ jsx(
Input,
{
type: "number",
value: editValue,
onChange: (e) => setEditValue(e.target.value),
onKeyDown: handleKeyDown,
className: "h-8 text-sm",
autoFocus: true
}
);
case "boolean":
return /* @__PURE__ */ jsxs(Select, { value: editValue ? "true" : "false", onValueChange: (val) => setEditValue(val === "true"), children: [
/* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 text-sm", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
/* @__PURE__ */ jsxs(SelectContent, { children: [
/* @__PURE__ */ jsx(SelectItem, { value: "true", children: "True" }),
/* @__PURE__ */ jsx(SelectItem, { value: "false", children: "False" })
] })
] });
default:
return /* @__PURE__ */ jsx(
Input,
{
type: "text",
value: editValue,
onChange: (e) => setEditValue(e.target.value),
onKeyDown: handleKeyDown,
className: "h-8 text-sm",
autoFocus: true
}
);
}
};
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
/* @__PURE__ */ jsx("div", { className: "flex-1", children: renderEditor() }),
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: "h-6 w-6 p-0",
onClick: handleSave,
children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
}
),
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
className: "h-6 w-6 p-0",
onClick: onCancel,
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
}
)
] });
}
const INDENT_SIZE = 15;
const DEFAULT_ROW_HEIGHT = 44;
function DataTableRow({
row,
level,
columns,
isSelected,
onRowSelect,
// onRowEdit,
// onRowDelete,
onCellEdit
}) {
const [editingCell, setEditingCell] = useState(null);
const visibleColumns = columns.filter((col) => !col.hidden);
const firstColumn = visibleColumns[0];
const pinnedLeftColumns = visibleColumns.filter((col) => col.pinned === "left");
const unpinnedColumns = visibleColumns.filter((col) => !col.pinned);
const pinnedRightColumns = visibleColumns.filter((col) => col.pinned === "right");
const handleCellDoubleClick = (column) => {
if (column.editable && onCellEdit) {
setEditingCell(column.field);
}
};
const handleCellSave = (field, value) => {
if (onCellEdit) {
onCellEdit(row, field, value);
}
setEditingCell(null);
};
const handleCellCancel = () => {
setEditingCell(null);
};
const renderCell = (column, isPinned = false) => {
const value = column.valueGetter ? column.valueGetter(row) : row[column.field];
const isEditing = editingCell === column.field;
const isFirstColumn = firstColumn.field === column.field;
if (column.useSelection) {
return /* @__PURE__ */ jsxs(
"div",
{
className: cn(
`min-h-[${DEFAULT_ROW_HEIGHT}px]`,
"px-4 py-2 flex items-center justify-start gap-3",
"transition-all duration-200 ease-in-out",
isPinned && "bg-white z-10 sticky border-gray-800/10 overflow-x-hidden",
column.pinned === "left" && "left-0 border-r",
column.pinned === "right" && "right-0 border-l"
),
style: {
paddingLeft: isFirstColumn && level ? `${level * INDENT_SIZE}px` : void 0,
minWidth: column.minWidth || 180,
maxWidth: column.maxWidth,
width: column.width
},
children: [
/* @__PURE__ */ jsx(
"input",
{
type: "checkbox",
checked: isSelected,
onClick: (e) => {
e.stopPropagation();
},
onChange: () => {
onRowSelect(row);
},
className: cn(
"h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600",
"transition-all duration-150 ease-in-out"
)
}
),
/* @__PURE__ */ jsx("span", { className: cn("flex-1", isPinned ? "truncate" : ""), children: column.cellRenderer ? column.cellRenderer(value, row) : value })
]
},
String(column.field)
);
}
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
`min-h-[${DEFAULT_ROW_HEIGHT}px]`,
"px-4 py-2 flex items-center justify-start",
"transition-all duration-200 ease-in-out",
isPinned && "bg-white sticky z-10 border-gray-800/10",
column.pinned === "left" && "left-0 border-r",
column.pinned === "right" && "right-0 border-l",
column.editable && "cursor-pointer hover:bg-gray-50 hover:scale-[1.02]",
editingCell === column.field && "bg-blue-50 ring-2 ring-indigo-500 ring-inset"
),
style: {
paddingLeft: isFirstColumn && level ? `${level * INDENT_SIZE}px` : void 0,
minWidth: column.minWidth,
maxWidth: column.maxWidth,
width: column.width
},
onDoubleClick: () => handleCellDoubleClick(column),
children: isEditing ? /* @__PURE__ */ jsx(
DataTableEditableCell,
{
value,
row,
column,
onSave: (newValue) => handleCellSave(column.field, newValue),
onCancel: handleCellCancel
}
) : column.cellRenderer ? column.cellRenderer(value, row) : value
},
String(column.field)
);
};
const generateGridColumns = () => {
const allColumns = [...pinnedLeftColumns, ...unpinnedColumns, ...pinnedRightColumns];
return allColumns.map((column, index) => {
if (index === 0) {
return "minmax(200px, 1fr)";
}
const width = column.width || column.minWidth || (column.filterable ? 180 : 120);
return `${width}px`;
}).join(" ");
};
return /* @__PURE__ */ jsxs(
"div",
{
className: cn(
"group relative grid",
"transition-all duration-200 ease-in-out",
isSelected && "*:bg-blue-50 transform"
),
style: {
gridTemplateColumns: generateGridColumns(),
minWidth: "fit-content"
},
onClick: () => onRowSelect(row),
children: [
pinnedLeftColumns.map((column) => renderCell(column, true)),
unpinnedColumns.map((column) => renderCell(column, false)),
pinnedRightColumns.map((column) => renderCell(column, true))
]
}
);
}
function DataTableGroupHeader({
groupValue,
itemCount,
expanded,
summaries,
columns,
onToggle,
level = 0,
field
}) {
const visibleColumns = columns.filter((col) => !col.hidden);
const generateGridColumns = () => {
return visibleColumns.map((column, index) => {
if (index === 0) {
return "minmax(200px, 1fr)";
}
const width = column.width || column.minWidth || (column.filterable ? 180 : 120);
return `${width}px`;
}).join(" ");
};
return /* @__PURE__ */ jsx(
"div",
{
className: "transition-colors grid",
style: {
gridTemplateColumns: generateGridColumns(),
minWidth: "fit-content"
},
children: visibleColumns.map((column, index) => {
const isCheckboxColumn = String(column.field) === "__select__";
const isFirstDataColumn = !isCheckboxColumn && index === (visibleColumns.some((col) => String(col.field) === "__select__") ? 1 : 0);
if (isCheckboxColumn || isFirstDataColumn) {
const indentSize = level * INDENT_SIZE;
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
`min-h-[${DEFAULT_ROW_HEIGHT}px]`,
"px-4 py-2 transition-colors bg-white flex items-center justify-start",
column.pinned === "left" ? "left-0 border-r z-10 sticky border-gray-800/10 shadow-md overflow-x-hidden" : ""
),
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", style: { paddingLeft: `${indentSize}px` }, children: [
/* @__PURE__ */ jsx(
Button,
{
variant: "ghost",
size: "sm",
onClick: onToggle,
className: cn(
"h-6 w-6 p-0 flex-shrink-0",
"transition-all duration-200 ease-in-out",
"hover:scale-110 active:scale-95",
"hover:bg-gray-200 focus:ring-2 focus:ring-indigo-500"
),
children: /* @__PURE__ */ jsx("div", { className: "transition-transform duration-200 ease-in-out", children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
}
),
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxs("span", { className: "font-medium text-gray-900 text-sm", children: [
groupValue,
" (",
itemCount,
")"
] }),
field && /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500 ml-1", children: [
"(",
field,
")"
] })
] })
] })
},
String(column.field)
);
} else {
const fieldName = String(column.field);
const summary = summaries == null ? void 0 : summaries.get(fieldName);
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
`min-w-[${column.minWidth || "120px"}]`,
`min-h-[${DEFAULT_ROW_HEIGHT}px]`,
"px-4 py-2 transition-colors flex items-center",
column.pinned === "left" ? "left-0 border-r sticky z-10 border-gray-800/10 shadow-md overflow-x-hidden" : "",
column.pinned === "right" ? "right-0 bg-white border-l sticky z-10 border-gray-800/10 shadow-md overflow-x-hidden" : ""
),
children: summary && /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs font-medium", children: [
"$",
summary.toLocaleString()
] })
},
String(column.field)
);
}
})
}
);
}
function DataTableActionBar({
filters,
onExport,
onClearFilters,
onOpenColumnConfig,
onToggleFullscreen,
isFullscreen,
enablePdfExport = false
}) {
const hasActiveFilters = filters.length > 0;
return /* @__PURE__ */ jsx("div", { className: "bg-gray-900 text-white p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxs(
Button,
{
variant: "outline",
size: "sm",
onClick: () => onExport == null ? void 0 : onExport("csv"),
className: "flex items-center gap-2 bg-gray-800/50 hover:bg-gray-800 text-white border-gray-400 hover:border-gray-400/10",
children: [
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 text-white" }),
"Export CSV"
]
}
),
enablePdfExport && /* @__PURE__ */ jsxs(
Button,
{
variant: "outline",
size: "sm",
onClick: () => onExport == null ? void 0 : onExport("pdf"),
className: "flex items-center gap-2 bg-gray-800/50 hover:bg-gray-800 text-white border-gray-400 hover:border-gray-400/10",
children: [
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 text-white" }),
"Export PDF"
]
}
)
] }),
hasActiveFilters && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
/* @__PURE__ */ jsxs(Badge, { variant: "secondary", className: "bg-indigo-500 text-white border-indigo-400", children: [
/* @__PURE__ */ jsx(Filter, { className: "h-3 w-3 mr-1 text-white" }),
filters.length,
" filter",
filters.length > 1 ? "s" : ""