From 4b4937b391d6135212d78bd601fe70bacc9e2320 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 4 Aug 2026 09:14:42 -0700 Subject: [PATCH] [eric] tool-ui: data-table columns resize by edge drag and reorder by header drag --- .../components/data-table/data-table.tsx | 103 ++++++++++++++++-- .../src/toolui/components/data-table/types.ts | 3 + 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/frontend/src/toolui/components/data-table/data-table.tsx b/frontend/src/toolui/components/data-table/data-table.tsx index 287850a3..a6ffd0bc 100644 --- a/frontend/src/toolui/components/data-table/data-table.tsx +++ b/frontend/src/toolui/components/data-table/data-table.tsx @@ -150,8 +150,33 @@ function DataTableProvider({ [sortBy, sortDirection, controlledSort, onSortChange], ); + // User-adjustable column order + widths (drag header to reorder, drag the edge strip to resize). + const [colOrder, setColOrder] = React.useState(null); + const [colWidths, setColWidths] = React.useState>({}); + const orderedColumns = React.useMemo(() => { + if (!colOrder) return columns; + const byKey = new Map(columns.map((c) => [String(c.key), c])); + const out = colOrder.map((k) => byKey.get(k)).filter(Boolean) as Column[]; + for (const c of columns) if (!colOrder.includes(String(c.key))) out.push(c); + return out; + }, [columns, colOrder]); + const setColWidth = React.useCallback((key: string, px: number) => { + setColWidths((w) => ({ ...w, [key]: Math.max(64, Math.round(px)) })); + }, []); + const moveColumn = React.useCallback((fromKey: string, toKey: string) => { + if (fromKey === toKey) return; + setColOrder((prev) => { + const keys = prev ?? columns.map((c) => String(c.key)); + const next = keys.filter((k) => k !== fromKey); + const at = next.indexOf(toKey); + if (at < 0) return keys; + next.splice(at, 0, fromKey); + return next; + }); + }, [columns]); + const contextValue: DataTableContextValue = { - columns, + columns: orderedColumns, data, rowIdKey, sortBy, @@ -159,6 +184,9 @@ function DataTableProvider({ toggleSort: handleSort, id, locale: resolvedLocale, + colWidths, + setColWidth, + moveColumn, }; return ( @@ -181,7 +209,7 @@ function DataTableLayout({ maxHeight, className, }: DataTableLayoutProps) { - const { columns, data, rowIdKey, sortBy, sortDirection, id } = useDataTable(); + const { columns, data, rowIdKey, sortBy, sortDirection, id, colWidths } = useDataTable(); const rowKeys = React.useMemo( () => createDataTableRowKeys( @@ -232,15 +260,18 @@ function DataTableLayout({ : undefined } > - +
{columns.length > 0 && ( - {columns.map((col) => ( - - ))} + {columns.map((col) => { + const w = colWidths?.[String(col.key)] ?? col.width; + return ( + + ); + })} )} {data.length === 0 ? ( @@ -437,15 +468,35 @@ interface DataTableHeadProps { totalColumns?: number; } +const COL_DRAG_MIME = "application/x-osw-col"; + function DataTableHead({ column, columnIndex = 0, totalColumns = 1, }: DataTableHeadProps) { - const { sortBy, sortDirection, toggleSort } = useDataTable(); + const { sortBy, sortDirection, toggleSort, colWidths, setColWidth, moveColumn } = useDataTable(); const isFirstColumn = columnIndex === 0; const isLastColumn = columnIndex === totalColumns - 1; + const startResize = (e: React.PointerEvent) => { + if (!setColWidth) return; + e.preventDefault(); + e.stopPropagation(); + const th = (e.currentTarget as HTMLElement).closest("th"); + if (!th) return; + const startX = e.clientX; + const startW = th.getBoundingClientRect().width; + const key = String(column.key); + const onMove = (ev: PointerEvent) => setColWidth(key, startW + (ev.clientX - startX)); + const onUp = () => { + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + }; + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + }; + const isSortable = column.sortable !== false; const isSorted = sortBy === column.key; @@ -478,15 +529,35 @@ function DataTableHead({ ? "text-center" : "text-left"; + const effectiveWidth = colWidths?.[String(column.key)] ?? column.width; return ( { + e.dataTransfer.setData(COL_DRAG_MIME, String(column.key)); + e.dataTransfer.effectAllowed = "move"; + }} + onDragOver={(e: React.DragEvent) => { + if (e.dataTransfer.types.includes(COL_DRAG_MIME)) { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + } + }} + onDrop={(e: React.DragEvent) => { + const from = e.dataTransfer.getData(COL_DRAG_MIME); + if (from && moveColumn) { + e.preventDefault(); + moveColumn(from, String(column.key)); + } + }} aria-sort={ isSorted ? direction === "asc" @@ -552,6 +623,16 @@ function DataTableHead({ )} {isSortable && } + {setColWidth && ( + e.preventDefault()} + onPointerDown={startResize} + onClick={(e: React.MouseEvent) => e.stopPropagation()} + className="hover:bg-border absolute inset-y-1 right-0 w-1 cursor-col-resize touch-none rounded select-none" + /> + )} ); } diff --git a/frontend/src/toolui/components/data-table/types.ts b/frontend/src/toolui/components/data-table/types.ts index e8902803..d083f367 100644 --- a/frontend/src/toolui/components/data-table/types.ts +++ b/frontend/src/toolui/components/data-table/types.ts @@ -259,4 +259,7 @@ export interface DataTableContextValue { toggleSort?: (key: ColumnKey) => void; id?: string; locale?: string; + colWidths?: Record; + setColWidth?: (key: string, px: number) => void; + moveColumn?: (fromKey: string, toKey: string) => void; }