Give a table rails to reshape it by #5
- KC
Tables were schema and text-in-cells only: no way to add a row, remove a column, reorder anything, or make one in the first place. `packages/editor` had no table code at all — not even the plugins `@lexical/table` assumes are registered.
## What this adds
Hovering a table, or putting the caret in one, raises a rail on each axis: a grip per column above it and per row beside it. Drag a grip to reorder, press the cross to remove, press the plus between two grips to insert there. A column's grip also carries an alignment button, cycling default → left → centre → right. Everything has a keyboard equivalent — `Ctrl/Cmd+Arrow` on a focused grip moves it — because a reorder that is only a drag is a reorder some people cannot perform at all.
`/table` joins the slash menu, and column alignment is finally drawn: it has round-tripped through MDX all along while the stylesheet hard-coded `text-align: start` over the top of it.
Also registered, because the editor was running without them: `registerTablePlugin` (cell navigation, the integrity transforms), `registerTableSelectionObserver` (cell-range selection), and `registerTableCellUnmergeTransform`, which keeps a table pasted from HTML to a shape GFM can actually write — the export visitor ignores spans, so a merged cell silently redistributed itself at the next save.
## Three things worth reviewing
**The header row does not take part.** It cannot be dragged, dropped onto, removed, or inserted above. Nothing in the document marks a row as the header: being first is the whole of what makes it one, and the column alignment hangs off its cells. A drag that moved it would not reorder the table so much as change what it claims — two rows would swap meaning at the next save. The last *body* row can go, though; `| a |` over `| - |` is well formed and round-trips.
**Limits are checked where the button is, not where the document is.** A table past `MAX_TABLE_ROWS` or `MAX_TABLE_COLUMNS` applies locally, syncs cleanly and is then refused by the server — which cannot undo a Yjs transaction, so it rebuilds the room under a fresh epoch and everyone in it loses their undo and their cursors. `table/shape.ts` is asked before anything is created. Same argument as `toolbar/url.ts`, one order of magnitude worse.
**The rails are a measured overlay, not chrome in the node.** A `<table>` cannot contain a `<div>` — the browser hoists a stray one straight back out — so the `data-plan-chrome` slot that callouts and tabs portal into is not available at any price. The choice was between subclassing `TableNode` and reading cell rectangles; reading rectangles is what the selection bubble already does.
## Two silent failures found on the way
`@lexical/table` paints a selected cell by adding `theme.tableCellSelected` and does nothing else — no inline style, no fallback — and MDXEditor's `lexicalTheme` names no table class of any kind. Cell selection was therefore live and completely invisible, and the next keystroke replaced everything it covered. The theme entries are in `plan-editor.tsx` beside the `collaboration` block, which exists for the identical reason.
`$moveTableRow` and `$moveTableColumn` return silently on a table with merged cells, which is why `shape.ts` carries `simple` and the grips go inert rather than becoming drags that do nothing.
Both are written down in `agents.md`, along with two mistakes made and fixed here: a remove button placed in the middle of a grip swallowed the drag it was drawn beside — intermittently, depending on whether a re-render landed between the pointer arriving and the button going down — and a rail that passed pointer events through its gaps blinked out from under a hand moving across it.
## Layout
``` table/shape.ts what a table permits; the limits live here table/geometry.ts seam ↔ index arithmetic and the rail's boxes table/ops.ts the Lexical mutations, each guarded by shape.ts table/rails.tsx the overlay: measuring, pointers, registration ```
The first three are pure and tested; `rails.tsx` is rectangles and pointers and the test runtime has no DOM, so it is untested on purpose — the `trail.ts` and `changes.ts` split.
## Verification
`bun test` (500), `bun run types` and `bun run ci` are green on each of the three commits independently, and `bun run build` is clean with one copy of `@lexical/table` in the bundle — two would make every `$isTableNode` return false in silence, which `table/ops.test.ts` would catch the way `interop.test.ts` does for the dialect.
Driven in Chrome against a real room as well: insert, remove, drag, keyboard move and alignment all confirmed against the canonical MDX written to disk. A column dragged to the end produced `| B | C | A |` with every row following; a right-align wrote `---:`; a row dragged above the header clamped to just below it rather than going nowhere.
`agents.md` also had its line counts and test count corrected — they were already stale before this branch.
Krzysztof-Cieslakmerged 3a9582a9f2893 months ago on GitHub
Krzysztof-Cieslakhead-ref-deleted3 months ago on GitHub