From 5b8a400c78aa19df41450cbb43e051661c7069ad Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 6 Feb 2026 23:44:00 +0000 Subject: [PATCH] feat: polish table editor insert zones, sticky header, and layout Insert column/row zones: - Expanded hover area to 32px wide (columns) and 20px tall (rows) so the insert trigger is much easier to reach - Column + button is centered right above the vertical border between two columns, inside the header cells with absolute positioning - Row + button is centered on the horizontal border between two rows, rendered via a zero-height spacer with an absolutely positioned hover zone - z-index set to z-30 (above the z-20 sticky header) so column insert buttons render on top of everything - On hover, a 3px blue highlight line appears at the exact border where the new column/row will be inserted, giving clear visual feedback - The entire zone is clickable (not just the button) for easier use Sticky header improvements: - Added a solid bg-background mask row at the top of the sticky thead that hides table cells scrolling underneath the header area - All header cells including the row-number spacer and add-column button now have explicit bg-background so nothing bleeds through - Header cell background (bg-accent/50) now wraps the full cell content including sort and delete buttons (moved bg from input to the containing div), giving the header a cohesive look Row insert zones use dedicated spacer elements between data rows (instead of absolutely positioned elements inside cells), which is more reliable across different table widths and avoids clipping issues. Co-authored-by: milvasic --- web/src/components/TableEditorDialog.tsx | 252 ++++++++++++----------- 1 file changed, 130 insertions(+), 122 deletions(-) diff --git a/web/src/components/TableEditorDialog.tsx b/web/src/components/TableEditorDialog.tsx index d12394bd6..4e05f2e23 100644 --- a/web/src/components/TableEditorDialog.tsx +++ b/web/src/components/TableEditorDialog.tsx @@ -1,5 +1,5 @@ import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, PlusIcon, TrashIcon } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import type { ColumnAlignment, TableData } from "@/utils/markdown-table"; import { createEmptyTable, serializeMarkdownTable } from "@/utils/markdown-table"; @@ -40,11 +40,8 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table const inputRefs = useRef>(new Map()); const setInputRef = useCallback((key: string, el: HTMLInputElement | null) => { - if (el) { - inputRefs.current.set(key, el); - } else { - inputRefs.current.delete(key); - } + if (el) inputRefs.current.set(key, el); + else inputRefs.current.delete(key); }, []); useEffect(() => { @@ -129,16 +126,13 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table newDir = "desc"; } setSortState({ col, dir: newDir }); - setRows((prev) => { const sorted = [...prev].sort((a, b) => { const va = (a[col] || "").toLowerCase(); const vb = (b[col] || "").toLowerCase(); const na = Number(va); const nb = Number(vb); - if (!Number.isNaN(na) && !Number.isNaN(nb)) { - return newDir === "asc" ? na - nb : nb - na; - } + if (!Number.isNaN(na) && !Number.isNaN(nb)) return newDir === "asc" ? na - nb : nb - na; const cmp = va.localeCompare(vb); return newDir === "asc" ? cmp : -cmp; }); @@ -149,43 +143,38 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table // ---- Tab / keyboard navigation ---- const handleKeyDown = (e: React.KeyboardEvent, row: number, col: number) => { - if (e.key === "Tab") { - e.preventDefault(); - const nextCol = e.shiftKey ? col - 1 : col + 1; - let nextRow = row; - - if (nextCol >= colCount) { - if (row < rowCount - 1) { - nextRow = row + 1; - focusCell(nextRow, 0); - } else { - addRow(); - setTimeout(() => focusCell(rowCount, 0), 0); - } - } else if (nextCol < 0) { - if (row > 0) { - nextRow = row - 1; - focusCell(nextRow, colCount - 1); - } else { - focusCell(-1, colCount - 1); - } + if (e.key !== "Tab") return; + e.preventDefault(); + const nextCol = e.shiftKey ? col - 1 : col + 1; + let nextRow = row; + if (nextCol >= colCount) { + if (row < rowCount - 1) { + nextRow = row + 1; + focusCell(nextRow, 0); } else { - focusCell(nextRow, nextCol); + addRow(); + setTimeout(() => focusCell(rowCount, 0), 0); } + } else if (nextCol < 0) { + if (row > 0) { + nextRow = row - 1; + focusCell(nextRow, colCount - 1); + } else { + focusCell(-1, colCount - 1); + } + } else { + focusCell(nextRow, nextCol); } }; const focusCell = (row: number, col: number) => { - const key = `${row}:${col}`; - const el = inputRefs.current.get(key); - el?.focus(); + inputRefs.current.get(`${row}:${col}`)?.focus(); }; // ---- Confirm ---- const handleConfirm = () => { - const data: TableData = { headers, rows, alignments }; - const md = serializeMarkdownTable(data); + const md = serializeMarkdownTable({ headers, rows, alignments }); onConfirm(md); onOpenChange(false); }; @@ -199,6 +188,9 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table return ; }; + // Total colSpan: row-number col + data cols + action col + const totalColSpan = colCount + 2; + return ( @@ -211,52 +203,54 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table Edit table headers, rows, columns and sort data +
{/* Scrollable table area */} -
- {/* Insert-column buttons row (above the table) */} -
- {/* We position "+" buttons at each column border using the same grid layout */} -
- {/* Offset for row-number column */} -
- {headers.map((_, col) => ( -
- {/* "+" button on the left edge of each column (= between col-1 and col) */} - {col > 0 && ( -
- - - - - Insert column - -
- )} -
- ))} -
-
- +
- {/* Sticky header */} - + {/* ============ STICKY HEADER ============ */} + + {/* Mask row: solid background strip that hides content scrolling behind the header */} - {/* Row number column */} - + + {/* Actual header row */} + + {/* Row-number spacer */} + ))} + {/* Add column at end */} - - {/* Data rows */} + {/* ============ DATA ROWS ============ */} {rows.map((row, rowIdx) => ( - - {/* Row number */} - - - {/* Data cells */} - {row.map((cell, col) => ( - + + + )} + + {/* ---- Actual data row ---- */} + + {/* Row number */} + + + {/* Data cells */} + {row.map((cell, col) => ( + + ))} + + {/* Row delete button (end of row) */} + - ))} - - {/* Row delete button (end of row) */} - - + + ))}
+ +
+ {headers.map((header, col) => ( - -
+
+ {/* ---- Insert-column zone (between col-1 and col) ---- */} + {col > 0 && ( +
insertColumnAt(col)} + > + {/* Blue vertical highlight line */} +
+ {/* + button */} + + + + + Insert column + +
+ )} + + {/* Header cell content — bg extends across input + action buttons */} +
setInputRef(`-1:${col}`, el)} style={{ fontFamily: MONO_FONT }} - className="flex-1 min-w-0 px-2 py-1.5 font-semibold text-xs uppercase tracking-wide bg-accent/50 border border-border rounded-tl-md focus:outline-none focus:ring-1 focus:ring-primary/40" + className="flex-1 min-w-0 px-2 py-1.5 font-semibold text-xs uppercase tracking-wide bg-transparent focus:outline-none focus:ring-1 focus:ring-primary/40" value={header} onChange={(e) => updateHeader(col, e.target.value)} onKeyDown={(e) => handleKeyDown(e, -1, col)} @@ -291,8 +285,9 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table
+
- {rowIdx + 1} - - setInputRef(`${rowIdx}:${col}`, el)} - style={{ fontFamily: MONO_FONT }} - className={cn( - "w-full px-2 py-1.5 text-sm bg-transparent border border-border focus:outline-none focus:ring-1 focus:ring-primary/40", - rowIdx === rowCount - 1 && "rounded-bl-md", - )} - value={cell} - onChange={(e) => updateCell(rowIdx, col, e.target.value)} - onKeyDown={(e) => handleKeyDown(e, rowIdx, col)} - /> - {/* Insert-row button: shown on the top border between rows */} - {rowIdx > 0 && col === 0 && ( -
+ + {/* ---- Insert-row zone (between row rowIdx-1 and rowIdx) ---- */} + {rowIdx > 0 && ( +
+
insertRowAt(rowIdx)} + > + {/* Blue horizontal highlight line */} +
+ {/* + button */} @@ -348,34 +331,59 @@ const TableEditorDialog = ({ open, onOpenChange, initialData, onConfirm }: Table Insert row
+
+ {rowIdx + 1} + + setInputRef(`${rowIdx}:${col}`, el)} + style={{ fontFamily: MONO_FONT }} + className={cn( + "w-full px-2 py-1.5 text-sm bg-transparent border border-border focus:outline-none focus:ring-1 focus:ring-primary/40", + rowIdx === rowCount - 1 && "rounded-bl-md", + )} + value={cell} + onChange={(e) => updateCell(rowIdx, col, e.target.value)} + onKeyDown={(e) => handleKeyDown(e, rowIdx, col)} + /> + + {rowCount > 1 && ( + + + + + Remove row + )} - {rowCount > 1 && ( - - - - - Remove row - - )} -
- {/* Footer */} + {/* ============ FOOTER ============ */}