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 ============ */}