Skip to content

slides: table row/column/range cell selection + multi-cell formatting#9

Merged
b32n merged 8 commits into
masterfrom
feat/slides-table-selection
Jul 10, 2026
Merged

slides: table row/column/range cell selection + multi-cell formatting#9
b32n merged 8 commits into
masterfrom
feat/slides-table-selection

Conversation

@b32n

@b32n b32n commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What & why

A table wasn't really finished: styling a header row meant formatting each cell one at a time. This adds a spreadsheet-style cell selection model so formatting applies to a whole row, column, or block at once.

Interaction model (PowerPoint / Google Slides)

  • Click a table → selects the object; drag its frame border to move it.
  • Click / drag inside a selected table → selects a cell or a range.
  • Row / column header gutters (top & left edges) → click to grab a whole row/column; the top-left corner selects everything. Drag along the headers to extend.
  • Double-click a cell → edit its text (unchanged).
  • Selected cells are highlighted; deselecting the table clears the cell selection.

Formatting the selection

With cells selected, the toolbar collapses to a single-row cell-formatting bar:

  • Fill every selected cell in one click (header row → select row → fill).
  • Text formatting — bold, color, size, alignment, etc. — applies to every selected cell's text.

Both go through new single-undo deck ops: updateTableCells and applyTableCellsFormat.

Implementation

  • TableSelectionStore — local (anchor, focus) cell rectangle (never in the CRDT).
  • TableGuttersOverlay — row/column/all header bars.
  • SelectionOverlay — frame-border move bands for a selected table.
  • InteractiveSlide — cell-range drag gesture, gutter selection, table move, and clearing the selection on deselect.
  • TextFormatBar / TableFormatBar route formatting + fill through the cell range.

Testing

  • Unit: multi-cell fill/format (single undo, out-of-grid ignored) + selection-geometry helpers. First unit tests for the react package (adds its ts-jest config).
  • Real-browser (playwright-core + system Chrome): row/column/range selection highlight, header-row fill, contextual text bar, and frame-border move all verified end-to-end.
  • Full local gate green: build · type-check · 384 tests · lint · website typecheck + build.

Deferred: cell merge/span (a larger model change).

b32n added 8 commits July 10, 2026 09:50
TableSelectionStore holds an (anchor, focus) cell rectangle as local view
state; deck gains updateTableCells / applyTableCellsFormat to fill or format
a whole row/column/range in a single undo. Adds a ts-jest config for the
react package (its first unit tests).
Selecting a table now works like a spreadsheet: click to select the object
(drag its frame border to move), then click/drag inside to select cells, or
click the row/column header gutters to grab a whole row/column (corner selects
all). The cell-range is highlighted; the toolbar collapses to a cell-formatting
bar where fill and text formatting (bold, color, size, align…) apply to every
selected cell at once — so a header row is one action, not cell-by-cell.

- TableGuttersOverlay: clickable/draggable row+column+all header bars.
- SelectionOverlay: frame-border move bands for a selected table.
- InteractiveSlide: cell-range drag gesture, gutter selection, table move,
  and clearing the cell selection when the table is deselected.
- TextFormatBar/TableFormatBar route formatting + fill through the cell range.
StaticRichText inherits font-size from the <td>, which set none, so idle cells
rendered at the inherited size while the live editor forced 18px — double-
clicking a cell visibly resized/shifted the text. Give the cell explicit,
shared metrics (font-size + padding) so view and edit match.
Wrapped cell text can make a table render taller than its frame height, so the
selection box, row/column gutters, and resize handles (all positioned from the
frame) only covered the top of a long table. A ResizeObserver on the rendered
table now grows the element's height to match its content via a new silent,
history-free deck method (autoSizeElementHeight) — grow-only, so a manually
enlarged table is left alone. Frame and overlays now cover the whole table.
…ntrols

- Move: the gutters and resize grips now follow the table's live frame during
  a drag instead of snapping into place only on drop (they read the transient
  frame like the selection box does).
- A visible move grip (⊕) sits at the table's top-left corner — a clear,
  discoverable drag handle (the body selects cells); a plain click on it still
  selects all cells.
- Insert/delete: the table toolbar gains insert row above/below and column
  left/right plus delete row(s)/column(s), all relative to the current cell
  selection (the same actions remain on the cell right-click menu). Multi-row/
  column deletes are one undo via new removeTableRows/removeTableColumns.
- Toolbar: the arrange group hides for a selected table (like text) so the
  richer table bar stays on one row.
Right-clicking a row or column header (or the move grip) now opens the table
menu instead of the generic element menu — it selects that row/column first,
so insert/delete and fill act on it. The menu is now selection-aware: delete
and fill span the whole selection ("Delete 2 columns", "Fill selection"), and
a delete that would empty the table is disabled.
…ed gutters)

Row heights were driven by rowFractions x the frame height, but the frame
auto-grows to fit content — so an inserted empty row claimed a big slice of the
tall frame (a giant empty row) and the gutters, computed from the same
fractions, no longer lined up with the real rows.

Rows now size to their content like a real table:
- TableView renders content-driven rows and publishes each row's measured top
  (as a fraction of the table height) to a new TableMetricsStore; the element
  height syncs exactly to the rendered table (syncElementHeight, no undo entry).
- The gutters read those measured positions, so row headers always match the
  actual rows; column edges still come from colFractions (fixed layout).
- Row-height resize grips are gone (rows are content-driven); column resize
  stays.

Verified: inserted empty row is a normal single-line height, and row headers
align with rows to the pixel before and after inserts.
Restores dragging a row boundary to size a row, in a way that coexists with
content-driven rows: a row's height is max(content, a manual minimum). New
optional rowHeights[] holds the per-row minimum in px (0 = auto); dragging a
row grip sets it and content can still push the row taller. rowHeights stays
aligned through row insert/delete. The row grips sit at the measured row
boundaries, so they track the real rows. Column resize unchanged.

Verified: dragging a row grew that row to the dragged minimum (others
unchanged), gutters stayed pixel-aligned, and undo restored it in one step.
@b32n
b32n merged commit 2a1012e into master Jul 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant