kumarabhirup dee323b7ad
fix lint/build errors and bump to 2026.2.15-1.4
- Fix all oxlint errors (curly, no-unused-vars, no-base-to-string,
  no-floating-promises, approx-constant, restrict-template-expressions)
- Fix TS build errors: rewrite update-cli.ts as thin wrapper over
  submodules, restore missing chat abort helpers in chat.ts
- Fix web build: wrap handleNewSession in async for ChatPanelHandle,
  add missing safeString helper to entry-detail-modal
- Bump version to 2026.2.15-1.4 and publish

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 00:30:13 -08:00

807 lines
25 KiB
TypeScript

"use client";
import React, { useState, useCallback, useMemo, useRef, useEffect } from "react";
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
type ColumnDef,
type SortingState,
type ColumnFiltersState,
type VisibilityState,
type Row,
type OnChangeFn,
type PaginationState,
} from "@tanstack/react-table";
import {
DndContext,
closestCenter,
type DragEndEvent,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
horizontalListSortingStrategy,
useSortable,
arrayMove,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { rankItem } from "@tanstack/match-sorter-utils";
/* ─── Types ─── */
export type RowAction<TData> = {
label: string;
onClick?: (row: TData) => void;
icon?: React.ReactNode;
variant?: "default" | "destructive";
};
export type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
loading?: boolean;
// search
searchPlaceholder?: string;
enableGlobalFilter?: boolean;
// sorting
enableSorting?: boolean;
// row selection
enableRowSelection?: boolean;
rowSelection?: Record<string, boolean>;
onRowSelectionChange?: OnChangeFn<Record<string, boolean>>;
bulkActions?: React.ReactNode;
// column features
enableColumnReordering?: boolean;
onColumnReorder?: (newOrder: string[]) => void;
initialColumnVisibility?: VisibilityState;
// pagination
pageSize?: number;
// actions
onRefresh?: () => void;
onAdd?: () => void;
addButtonLabel?: string;
onRowClick?: (row: TData, index: number) => void;
rowActions?: (row: TData) => RowAction<TData>[];
// toolbar
toolbarExtra?: React.ReactNode;
title?: string;
titleIcon?: React.ReactNode;
// sticky
stickyFirstColumn?: boolean;
};
/* ─── Fuzzy filter ─── */
function fuzzyFilter(
row: Row<unknown>,
columnId: string,
filterValue: string,
) {
const result = rankItem(row.getValue(columnId), filterValue);
return result.passed;
}
/* ─── Sortable header cell (DnD) ─── */
function SortableHeader({
id,
children,
style,
className,
}: {
id: string;
children: React.ReactNode;
style?: React.CSSProperties;
className?: string;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const dragStyle: React.CSSProperties = {
...style,
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
cursor: "grab",
};
return (
<th
ref={setNodeRef}
style={dragStyle}
className={className}
{...attributes}
{...listeners}
>
{children}
</th>
);
}
/* ─── Sort icon ─── */
function SortIcon({ direction }: { direction: "asc" | "desc" | false }) {
if (!direction) {
return (
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.25 }}>
<path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" />
</svg>
);
}
return (
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{direction === "asc" ? <path d="m5 12 7-7 7 7" /> : <path d="m19 12-7 7-7-7" />}
</svg>
);
}
/* ─── Main component ─── */
export function DataTable<TData, TValue>({
columns,
data,
loading = false,
searchPlaceholder = "Search...",
enableGlobalFilter = true,
enableSorting = true,
enableRowSelection = false,
rowSelection: externalRowSelection,
onRowSelectionChange,
bulkActions,
enableColumnReordering = false,
onColumnReorder,
initialColumnVisibility,
pageSize: defaultPageSize = 100,
onRefresh,
onAdd,
addButtonLabel = "+ Add",
onRowClick,
rowActions,
toolbarExtra,
title,
titleIcon,
stickyFirstColumn: stickyFirstProp = true,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState("");
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(initialColumnVisibility ?? {});
const [internalRowSelection, setInternalRowSelection] = useState<Record<string, boolean>>({});
const [showColumnsMenu, setShowColumnsMenu] = useState(false);
const [stickyFirstColumn, setStickyFirstColumn] = useState(stickyFirstProp);
const [isScrolled, setIsScrolled] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: defaultPageSize });
const columnsMenuRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const rowSelectionState = externalRowSelection !== undefined ? externalRowSelection : internalRowSelection;
// Extract column ID from ColumnDef
const getColumnId = useCallback((c: ColumnDef<TData, TValue>): string => {
if ("id" in c && typeof c.id === "string") {return c.id;}
if ("accessorKey" in c && typeof c.accessorKey === "string") {return c.accessorKey;}
return "";
}, []);
// Column order for DnD — include "select" at start and "actions" at end
// so TanStack doesn't push them to the end of the table
const buildColumnOrder = useCallback(
(dataCols: ColumnDef<TData, TValue>[]) => {
const dataOrder = dataCols.map(getColumnId);
const order: string[] = [];
if (enableRowSelection) {order.push("select");}
order.push(...dataOrder);
if (rowActions) {order.push("actions");}
return order;
},
[getColumnId, enableRowSelection, rowActions],
);
const [columnOrder, setColumnOrder] = useState<string[]>(() =>
buildColumnOrder(columns),
);
// Update column order when columns change
useEffect(() => {
setColumnOrder(buildColumnOrder(columns));
}, [columns, buildColumnOrder]);
// DnD sensors
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
setColumnOrder((old) => {
const oldIndex = old.indexOf(active.id as string);
const newIndex = old.indexOf(over.id as string);
const newOrder = arrayMove(old, oldIndex, newIndex);
onColumnReorder?.(newOrder);
return newOrder;
});
}
},
[onColumnReorder],
);
// Scroll tracking for sticky column shadow
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
setIsScrolled(e.currentTarget.scrollLeft > 0);
}, []);
// Close columns menu on click outside
useEffect(() => {
function handleClick(e: MouseEvent) {
if (columnsMenuRef.current && !columnsMenuRef.current.contains(e.target as Node)) {
setShowColumnsMenu(false);
}
}
if (showColumnsMenu) {
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}
}, [showColumnsMenu]);
// Build selection column
const selectionColumn: ColumnDef<TData> | null = enableRowSelection
? {
id: "select",
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllPageRowsSelected()}
onChange={table.getToggleAllPageRowsSelectedHandler()}
className="w-3.5 h-3.5 rounded accent-[var(--color-accent)] cursor-pointer"
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
onClick={(e) => e.stopPropagation()}
className="w-3.5 h-3.5 rounded accent-[var(--color-accent)] cursor-pointer"
/>
),
size: 40,
enableSorting: false,
enableHiding: false,
}
: null;
// Build actions column
const actionsColumn: ColumnDef<TData> | null = rowActions
? {
id: "actions",
header: () => null,
cell: ({ row }) => (
<RowActionsMenu
row={row.original}
actions={rowActions(row.original)}
/>
),
size: 48,
enableSorting: false,
enableHiding: false,
}
: null;
const allColumns = useMemo(() => {
const cols: ColumnDef<TData, TValue>[] = [];
if (selectionColumn) {cols.push(selectionColumn as ColumnDef<TData, TValue>);}
cols.push(...columns);
if (actionsColumn) {cols.push(actionsColumn as ColumnDef<TData, TValue>);}
return cols;
}, [columns, selectionColumn, actionsColumn]);
const table = useReactTable({
data,
columns: allColumns,
state: {
sorting,
globalFilter,
columnFilters,
columnVisibility,
rowSelection: rowSelectionState,
columnOrder: enableColumnReordering ? columnOrder : undefined,
pagination,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: (updater) => {
if (onRowSelectionChange) {
onRowSelectionChange(updater);
} else {
setInternalRowSelection(updater);
}
},
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
enableRowSelection,
enableSorting,
globalFilterFn: fuzzyFilter,
columnResizeMode: "onChange",
});
const selectedCount = Object.keys(rowSelectionState).filter((k) => rowSelectionState[k]).length;
const visibleColumns = table.getVisibleFlatColumns().filter((c) => c.id !== "select" && c.id !== "actions");
// ─── Render ───
return (
<div className="flex flex-col h-full">
{/* Toolbar */}
<div
className="flex items-center gap-2 px-4 py-2.5 flex-shrink-0 flex-wrap"
style={{ borderBottom: "1px solid var(--color-border)" }}
>
{title && (
<div className="flex items-center gap-2 mr-2">
{titleIcon}
<span className="text-sm font-semibold" style={{ color: "var(--color-text)" }}>
{title}
</span>
</div>
)}
{/* Search */}
{enableGlobalFilter && (
<div className="relative flex-1 min-w-[180px] max-w-[320px]">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: "var(--color-text-muted)" }}>
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
</svg>
<input
type="text"
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder={searchPlaceholder}
className="w-full pl-9 pr-3 py-1.5 text-xs rounded-full outline-none"
style={{
background: "var(--color-surface-hover)",
color: "var(--color-text)",
border: "1px solid var(--color-border)",
}}
/>
{globalFilter && (
<button
type="button"
onClick={() => setGlobalFilter("")}
className="absolute right-2.5 top-1/2 -translate-y-1/2"
style={{ color: "var(--color-text-muted)" }}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
</button>
)}
</div>
)}
{/* Bulk actions */}
{selectedCount > 0 && bulkActions && (
<div className="flex items-center gap-2">
<span className="text-xs font-medium" style={{ color: "var(--color-text-muted)" }}>
{selectedCount} selected
</span>
{bulkActions}
</div>
)}
<div className="flex-1" />
{toolbarExtra}
{/* Refresh */}
{onRefresh && (
<button
type="button"
onClick={onRefresh}
className="p-1.5 rounded-lg"
style={{ color: "var(--color-text-muted)" }}
title="Refresh"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M3 21v-5h5" />
</svg>
</button>
)}
{/* Columns menu */}
<div className="relative" ref={columnsMenuRef}>
<button
type="button"
onClick={() => setShowColumnsMenu((v) => !v)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium"
style={{
color: "var(--color-text-muted)",
border: "1px solid var(--color-border)",
background: showColumnsMenu ? "var(--color-surface-hover)" : "transparent",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /><path d="M15 3v18" />
</svg>
Columns
</button>
{showColumnsMenu && (
<div
className="absolute right-0 top-full mt-1 z-50 min-w-[200px] rounded-xl overflow-hidden py-1"
style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
boxShadow: "var(--shadow-lg)",
}}
>
{/* Sticky first col toggle */}
<label
className="flex items-center gap-2 px-3 py-2 text-xs cursor-pointer"
style={{ color: "var(--color-text-muted)", borderBottom: "1px solid var(--color-border)" }}
>
<input
type="checkbox"
checked={stickyFirstColumn}
onChange={() => setStickyFirstColumn((v) => !v)}
className="w-3.5 h-3.5 rounded accent-[var(--color-accent)]"
/>
Freeze first column
</label>
{visibleColumns.length === 0 ? (
<div className="px-3 py-2 text-xs" style={{ color: "var(--color-text-muted)" }}>
No toggleable columns
</div>
) : (
table.getAllLeafColumns()
.filter((c) => c.id !== "select" && c.id !== "actions" && c.getCanHide())
.map((column) => (
<label
key={column.id}
className="flex items-center gap-2 px-3 py-1.5 text-xs cursor-pointer"
style={{ color: "var(--color-text)" }}
>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={column.getToggleVisibilityHandler()}
className="w-3.5 h-3.5 rounded accent-[var(--color-accent)]"
/>
{typeof column.columnDef.header === "string"
? column.columnDef.header
: column.id}
</label>
))
)}
</div>
)}
</div>
{/* Add button */}
{onAdd && (
<button
type="button"
onClick={onAdd}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
style={{
background: "var(--color-accent)",
color: "white",
}}
>
{addButtonLabel}
</button>
)}
</div>
{/* Table */}
<div
ref={scrollContainerRef}
className="flex-1 overflow-auto min-w-0"
onScroll={handleScroll}
>
{loading ? (
<LoadingSkeleton columnCount={allColumns.length} />
) : data.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 gap-3">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ color: "var(--color-text-muted)", opacity: 0.4 }}>
<rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 9h18" /><path d="M3 15h18" /><path d="M9 3v18" />
</svg>
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>No data</p>
</div>
) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<table className="w-full text-sm" style={{ borderCollapse: "separate", borderSpacing: 0 }}>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
<SortableContext items={columnOrder} strategy={horizontalListSortingStrategy}>
{headerGroup.headers.map((header, colIdx) => {
const isFirstData = colIdx === (enableRowSelection ? 1 : 0);
const isSticky = stickyFirstColumn && isFirstData;
const isSelectCol = header.id === "select";
const isActionsCol = header.id === "actions";
const canSort = header.column.getCanSort();
const headerStyle: React.CSSProperties = {
borderColor: "var(--color-border)",
background: "var(--color-surface)",
position: "sticky",
top: 0,
zIndex: isSticky || isSelectCol ? 31 : 30,
...(isSticky ? { left: enableRowSelection ? 40 : 0, boxShadow: isScrolled ? "4px 0 8px -2px rgba(0,0,0,0.08)" : "none" } : {}),
...(isSelectCol ? { left: 0, position: "sticky", zIndex: 31, width: 40 } : {}),
width: header.getSize(),
};
const content = header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext());
if (enableColumnReordering && !isSelectCol && !isActionsCol) {
return (
<SortableHeader
key={header.id}
id={header.id}
style={headerStyle}
className="text-left px-3 py-2.5 font-medium text-xs uppercase tracking-wider whitespace-nowrap border-b select-none"
>
<span
className={`flex items-center gap-1 ${canSort ? "cursor-pointer" : ""}`}
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
>
{content}
{canSort && <SortIcon direction={header.column.getIsSorted()} />}
</span>
</SortableHeader>
);
}
return (
<th
key={header.id}
style={headerStyle}
className="text-left px-3 py-2.5 font-medium text-xs uppercase tracking-wider whitespace-nowrap border-b select-none"
>
<span
className={`flex items-center gap-1 ${canSort ? "cursor-pointer" : ""}`}
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
style={{ color: "var(--color-text-muted)" }}
>
{content}
{canSort && <SortIcon direction={header.column.getIsSorted()} />}
</span>
</th>
);
})}
</SortableContext>
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row, rowIdx) => {
const isSelected = row.getIsSelected();
return (
<tr
key={row.id}
className={`transition-colors duration-75 ${onRowClick ? "cursor-pointer" : ""}`}
style={{
background: isSelected
? "var(--color-accent-light)"
: rowIdx % 2 === 0
? "transparent"
: "var(--color-surface)",
}}
onClick={() => onRowClick?.(row.original, rowIdx)}
onMouseEnter={(e) => {
if (!isSelected)
{(e.currentTarget as HTMLElement).style.background = "var(--color-surface-hover)";}
}}
onMouseLeave={(e) => {
if (!isSelected)
{(e.currentTarget as HTMLElement).style.background =
rowIdx % 2 === 0 ? "transparent" : "var(--color-surface)";}
}}
>
{row.getVisibleCells().map((cell, colIdx) => {
const isFirstData = colIdx === (enableRowSelection ? 1 : 0);
const isSticky = stickyFirstColumn && isFirstData;
const isSelectCol = cell.column.id === "select";
const cellStyle: React.CSSProperties = {
borderColor: "var(--color-border)",
...(isSticky
? {
position: "sticky" as const,
left: enableRowSelection ? 40 : 0,
zIndex: 20,
background: isSelected
? "var(--color-accent-light)"
: "var(--color-bg)",
boxShadow: isScrolled ? "4px 0 8px -2px rgba(0,0,0,0.08)" : "none",
}
: {}),
...(isSelectCol
? {
position: "sticky" as const,
left: 0,
zIndex: 20,
background: isSelected
? "var(--color-accent-light)"
: "var(--color-bg)",
width: 40,
}
: {}),
};
return (
<td
key={cell.id}
className="px-3 py-2 border-b whitespace-nowrap"
style={cellStyle}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</DndContext>
)}
</div>
{/* Pagination footer */}
{!loading && data.length > 0 && (
<div
className="flex items-center justify-between px-4 py-2 text-xs flex-shrink-0"
style={{
borderTop: "1px solid var(--color-border)",
color: "var(--color-text-muted)",
}}
>
<span>
Showing {table.getRowModel().rows.length} of {data.length} results
{selectedCount > 0 && ` (${selectedCount} selected)`}
</span>
<div className="flex items-center gap-2">
<span>Rows per page</span>
<select
value={pagination.pageSize}
onChange={(e) => setPagination((p) => ({ ...p, pageSize: Number(e.target.value), pageIndex: 0 }))}
className="px-1.5 py-0.5 rounded-md text-xs outline-none"
style={{
background: "var(--color-surface-hover)",
color: "var(--color-text)",
border: "1px solid var(--color-border)",
}}
>
{[20, 50, 100, 250, 500].map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
<span>
Page {pagination.pageIndex + 1} of {table.getPageCount()}
</span>
<div className="flex gap-0.5">
<PaginationButton onClick={() => setPagination((p) => ({ ...p, pageIndex: 0 }))} disabled={!table.getCanPreviousPage()} label="&laquo;" />
<PaginationButton onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} label="&lsaquo;" />
<PaginationButton onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} label="&rsaquo;" />
<PaginationButton onClick={() => setPagination((p) => ({ ...p, pageIndex: table.getPageCount() - 1 }))} disabled={!table.getCanNextPage()} label="&raquo;" />
</div>
</div>
</div>
)}
</div>
);
}
/* ─── Sub-components ─── */
function PaginationButton({ onClick, disabled, label }: { onClick: () => void; disabled: boolean; label: string }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className="w-6 h-6 rounded flex items-center justify-center text-xs disabled:opacity-30"
style={{ color: "var(--color-text-muted)", border: "1px solid var(--color-border)" }}
// biome-ignore lint: using html entity label
dangerouslySetInnerHTML={{ __html: label }}
/>
);
}
function RowActionsMenu<TData>({ row, actions }: { row: TData; actions: RowAction<TData>[] }) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) {
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}
}, [open]);
return (
<div className="relative" ref={ref}>
<button
type="button"
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); }}
className="p-1 rounded-md"
style={{ color: "var(--color-text-muted)" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5" /><circle cx="12" cy="12" r="1.5" /><circle cx="12" cy="19" r="1.5" /></svg>
</button>
{open && (
<div
className="absolute right-0 top-full mt-1 z-50 min-w-[140px] rounded-xl overflow-hidden py-1"
style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
boxShadow: "var(--shadow-lg)",
}}
>
{actions.map((action, i) => (
<button
key={i}
type="button"
onClick={(e) => {
e.stopPropagation();
action.onClick?.(row);
setOpen(false);
}}
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-left"
style={{
color: action.variant === "destructive" ? "var(--color-error)" : "var(--color-text)",
}}
>
{action.icon}
{action.label}
</button>
))}
</div>
)}
</div>
);
}
function LoadingSkeleton({ columnCount }: { columnCount: number }) {
return (
<div className="p-4 space-y-2">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="flex gap-3">
{Array.from({ length: Math.min(columnCount, 6) }).map((_, j) => (
<div
key={j}
className="h-8 rounded-lg animate-pulse flex-1"
style={{ background: "var(--color-surface-hover)", animationDelay: `${j * 50}ms` }}
/>
))}
</div>
))}
</div>
);
}