feat(rte): table creation and editing UI (GFM envelope) #106

Merged
dominik.polakovics merged 1 commit from afk/95 into main 2026-07-18 12:00:45 +02:00

Editor affordances for rte tables, available only in bodies whose allow-set includes the opt-in table token (#94): a contextual Table ▾ toolbar menu following the existing Insert ▾ chip-menu pattern.

What it does

  • Outside a table: Insert table — 2 columns, header + 2 body rows.
  • Inside a table: exactly the GFM-safe operations — Add row below, Add column left/right, Remove row, Remove column, Delete table. No merge/split items, no header toggles, and deliberately no "add row above" (from the header row prosemirror-tables would insert a body row above the header, breaking the first-row-is-header invariant).
  • Header protection: Remove row is disabled while the selection is in the header row and body rows exist — enforced both in the menu's disabled state and at the command level (removeRow in table.ts). On a header-only table it maps to delete-table, since prosemirror-tables' deleteRow never removes the last row.
  • Alignment inheritance: addRowBelow copies each header cell's textAlign onto the new row in the same transaction, so GFM column alignment renders immediately instead of after a save/reload round trip.
  • Keyboard: Tab/Shift-Tab cell navigation is the stock Table extension behavior (Tab at the last cell appends a body row), now covered by tests; all operations are reachable via the focusable toolbar button and menu items.

Acceptance criteria → proof

  • Insert control only with the token: component tests (present with table, absent under the 8-token default) + browser check against the scaffolded Quote element (no rte-table).
  • Ops work, header row protected: 27 new jsdom tests assert every operation on the serialized pipe-table emission; header-guard tested at both button and command level.
  • UI-created table → valid GFM → real Hugo build: the fresh-insert emission (| | |\n| --- | --- |\n| | |\n| | |) is pinned in the markdown identity table (an unfilled saved table can never degrade to literal pipe text on reload); Go-side TestBodySaveUITableAccepted proves byte-identical body round-trip through the server gate, and TestPublishUITableRealHugo publishes a page carrying the emission through a real Hugo pre-flight build.
  • Keyboard: Tab/Shift-Tab tests incl. the Tab-appends-tableCell-row case.
  • Nothing outside the envelope: envelope sweep test (no HTML, no colspan/rowspan after scripted op sequences); #94's inert merge/split/header commands untouched.

Additionally verified end-to-end in real Chromium against harness serve on a freshly scaffolded site: insert → type across cells with Tab → row/col ops → save; stored body is the exact canonical pipe table and the Hugo preview renders it in the theme's ce-table scroll wrapper.

scaffold/site/AGENTS.md is deliberately untouched — no engine-contract surface changed (editor affordances are out of its scope; the token itself was documented in #94).

Full suite green: 867 web tests / 42 files, tsc --noEmit clean, go test ./... all packages incl. the real-Hugo integration tests.

Closes #95

🤖 Generated with Claude Code

Editor affordances for rte tables, available only in bodies whose allow-set includes the opt-in `table` token (#94): a contextual **Table ▾** toolbar menu following the existing Insert ▾ chip-menu pattern. ## What it does - **Outside a table**: Insert table — 2 columns, header + 2 body rows. - **Inside a table**: exactly the GFM-safe operations — Add row below, Add column left/right, Remove row, Remove column, Delete table. No merge/split items, no header toggles, and deliberately no "add row above" (from the header row prosemirror-tables would insert a body row *above* the header, breaking the first-row-is-header invariant). - **Header protection**: Remove row is disabled while the selection is in the header row and body rows exist — enforced both in the menu's disabled state and at the command level (`removeRow` in `table.ts`). On a header-only table it maps to delete-table, since prosemirror-tables' `deleteRow` never removes the last row. - **Alignment inheritance**: `addRowBelow` copies each header cell's `textAlign` onto the new row in the same transaction, so GFM column alignment renders immediately instead of after a save/reload round trip. - **Keyboard**: Tab/Shift-Tab cell navigation is the stock Table extension behavior (Tab at the last cell appends a body row), now covered by tests; all operations are reachable via the focusable toolbar button and menu items. ## Acceptance criteria → proof - *Insert control only with the token*: component tests (present with `table`, absent under the 8-token default) + browser check against the scaffolded Quote element (no `rte-table`). - *Ops work, header row protected*: 27 new jsdom tests assert every operation on the serialized pipe-table emission; header-guard tested at both button and command level. - *UI-created table → valid GFM → real Hugo build*: the fresh-insert emission (`| | |\n| --- | --- |\n| | |\n| | |`) is pinned in the markdown identity table (an unfilled saved table can never degrade to literal pipe text on reload); Go-side `TestBodySaveUITableAccepted` proves byte-identical body round-trip through the server gate, and `TestPublishUITableRealHugo` publishes a page carrying the emission through a real Hugo pre-flight build. - *Keyboard*: Tab/Shift-Tab tests incl. the Tab-appends-tableCell-row case. - *Nothing outside the envelope*: envelope sweep test (no HTML, no colspan/rowspan after scripted op sequences); #94's inert merge/split/header commands untouched. Additionally verified end-to-end in real Chromium against `harness serve` on a freshly scaffolded site: insert → type across cells with Tab → row/col ops → save; stored body is the exact canonical pipe table and the Hugo preview renders it in the theme's `ce-table` scroll wrapper. `scaffold/site/AGENTS.md` is deliberately untouched — no engine-contract surface changed (editor affordances are out of its scope; the token itself was documented in #94). Full suite green: 867 web tests / 42 files, `tsc --noEmit` clean, `go test ./...` all packages incl. the real-Hugo integration tests. Closes #95 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(rte): table creation and editing UI inside the GFM envelope
All checks were successful
ci / build-and-test (pull_request) Successful in 8m25s
faa4cf8f5b
Add the creation/editing affordances for the opt-in table token (#94):
a contextual "Table ▾" toolbar menu in RichTextField, rendered only when
the body's allow-set grants table, following the Insert ▾ chip-menu
pattern (aria-haspopup, mutually exclusive openers).

Outside a table the menu offers Insert table (2 columns, header + 2 body
rows); inside it offers exactly the GFM-safe operations: add row below,
add column left/right, remove row, remove column, delete table. No
merge/split, no header toggles, and deliberately no "add row above" —
from the header row prosemirror-tables would insert a body row above the
header, breaking the first-row-is-header invariant.

Selection gating lives in table.ts helpers: tableSelectionInfo (wrapped
selectedRect), removeRow (refuses the header row while body rows exist —
command-level, not just the disabled button — and maps a header-only
table to deleteTable, since prosemirror-tables' deleteRow never removes
the last row), and addRowBelow (single-transaction row insert that
copies each header cell's textAlign onto the new row so GFM column
alignment renders immediately instead of after a save/reload).

Tab/Shift-Tab cell navigation is the stock Table extension behavior,
now covered by tests; a fresh insert's all-empty emission
("|  |  |\n| --- | --- |\n|  |  |\n|  |  |") is pinned in the markdown
identity table so a saved empty table can never degrade to literal pipe
text on reload. Disabled menu items get the rte-tool disabled styling.

Server side, two tests prove the acceptance criterion end to end: the
body gate accepts the editor's canonical emission (empty-cell row
included) byte-identically, and a page carrying it survives the real
Hugo pre-flight build and publishes verbatim.

Closes #95

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

This was generated by AI while landing a PR.

Landing validation — PASS

  • CI: ci / build-and-test (pull_request) success (full suite, 8m25s) — relied on, not re-run.
  • Title: Conventional Commits (feat(rte): …). ✓
  • AFK contract: head afk/95, body carries Closes #95. ✓
  • Mergeable: main is an ancestor of afk/95 — no conflict. ✓
  • Intent vs #95: matches — insert (3×2 with header), row/col add/remove, delete-table, header-row protection, all inside the GFM envelope from #94.
  • AGENTS.md: correctly untouched — no new token or schema (uses the table token from #94); the agentsdoc_test.go drift test passed in CI.

Diff review confirmed sound: header-is-first-row invariant enforced at both the menu (disabled state) and command level (removeRow); addRowBelow inherits header-cell textAlign in one transaction so GFM alignment renders without a reload; insert withheld inside a table (no nested tables); no "add row above". Ready to merge on maintainer go-ahead.

> *This was generated by AI while landing a PR.* **Landing validation — PASS** - **CI**: `ci / build-and-test (pull_request)` **success** (full suite, 8m25s) — relied on, not re-run. - **Title**: Conventional Commits (`feat(rte): …`). ✓ - **AFK contract**: head `afk/95`, body carries `Closes #95`. ✓ - **Mergeable**: `main` is an ancestor of `afk/95` — no conflict. ✓ - **Intent vs #95**: matches — insert (3×2 with header), row/col add/remove, delete-table, header-row protection, all inside the GFM envelope from #94. - **AGENTS.md**: correctly untouched — no new token or schema (uses the `table` token from #94); the `agentsdoc_test.go` drift test passed in CI. Diff review confirmed sound: header-is-first-row invariant enforced at both the menu (disabled state) and command level (`removeRow`); `addRowBelow` inherits header-cell `textAlign` in one transaction so GFM alignment renders without a reload; insert withheld inside a table (no nested tables); no "add row above". Ready to merge on maintainer go-ahead.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!106
No description provided.