
# Working in the Grid

> What a table with editable columns does without being configured — the cell cursor, ranges and the fill handle, the trailing row that creates records, and live refresh.

A grid with editable columns behaves like a spreadsheet. Nothing on this page is configured: it follows from a column being `editable`, and from the bound table's `canCreate` permission for the trailing row.

The whole grid is a single tab stop. Tab moves into it once, and from there the arrow keys move a cell cursor rather than walking every cell in the tab order. Arrow keys move one cell and cross read-only columns rather than skipping them; `Home` and `End` jump to the ends of the row; `PageUp` and `PageDown` move a viewport, landing on a real row; `Enter` commits an edit and drops the cursor one row, ready to retype a column; `Escape` cancels and keeps the cursor; `Tab` moves one cell along, wrapping to the next row at the row end. The cursor stops at every edge and never wraps around the grid. The grid announces itself with the `grid`, `row` and `gridcell` roles, and the cursor carries `aria-selected`.

Shift-clicking from the cursor extends the selection into a rectangle of cells, not a range of whole rows. The cursor cell carries a small square at its bottom-right corner — the fill handle — on an editable column only. Dragging it previews the span without writing anything; releasing writes one record per row in the span, and dragging sideways fills across columns, each destination coercing the value it receives. A value the destination column cannot hold is refused and named, and the rest of the fill still lands: dragging a word into a number column tells you which column refused it and what that column takes. Double-clicking the handle fills down to the end of the neighbouring column.

The last row of the body is a blank trailing row, below the data and above the summary. Typing into it and committing creates a record and grows a fresh trailing row underneath. `Escape` discards an unsaved trailing row and writes nothing; a commit missing a required field is refused, the column is named, and what was typed stays to be corrected. Grouping gives each group its own trailing row, prefilled with that group's value. The trailing row follows the same permission rule as the toolbar's create button: a role that may not create records gets no trailing row at all, rather than a disabled one.

**A grid does not refresh itself by default.** With no `refreshMode` declared on its `dataSource`, a change made elsewhere does not reach an open grid. `refreshMode: realtime` opens a live connection and applies changes as they arrive, and a connection stuck reconnecting says so in the grid rather than leaving a stale table looking current.

## Which cells are editable

A cell is editable when its column declares `editable: true`. A double-click opens the editor; a click on the selection checkbox selects the row instead, and a click on a group header folds that group — the **target** decides what a click means, never the timing.

An action column's `editSelect` is the inline dropdown a row editor offers for a choice column. It takes `options` or an `optionsSource` — the same table- or endpoint-backed binding a `select` component takes — and the two are mutually exclusive, with neither declared refused outright: a dropdown with nothing in it is not a useful control.

## Behaviour

### Inline add row

- A trailing row sits last in `<tbody>`, above the summary footer
- Typing then committing creates a record and grows a fresh trailing row
- Escape discards the unsaved row without writing anything
- A missing required field refuses the commit, names the column, and keeps the draft
- A `relationship` column opens its record picker in the trailing row
- `canCreate: false` removes the trailing row entirely — absent, not disabled
- A grouped grid gets one trailing row per group, prefilled with the group's value
- A `code` column in the trailing row accepts a multi-line snippet
- An operator adds records inline, grouped and linked, without leaving the trailing row

### Field-type cell editors

- A multi-select cell picks from its declared options and stores an array, not a joined string
- A relationship cell searches the related table on `displayField` and stores the foreign key
- A user cell picks a directory entry by name and never exposes an email
- An attachment cell uploads through the bucket and writes the shape its column type accepts
- A checkbox cell toggles on a single click and stores a boolean, without selecting its row
- A rating cell sets a score on a single click and clears to null rather than to zero
- A datetime cell edits in its declared `timeZone` and round-trips an instant, not free text
- A rich-text cell opens the existing editor with its declared toolbar and budgets against HTML
- Tab commits and advances out of a select-like editor instead of being swallowed by it
- Tab commits and advances out of the attachment editor's file control
- Tab commits and advances out of the rich-text surface rather than indenting inside it
- Every new editor opens and persists inside a grouped grid, not only a flat one
- Escape dismisses every new editor without writing
- The grid resolves its editor through the total field-type dispatch, so no type falls through
- A code cell opens the CodeMirror editor its field `language` declares
- A code cell accepts multi-line source and stores the newlines
- A code cell's editor is drawn from every property its field declares, and an undeclared one keeps the default
- An operator edits every field type inline, on both dialects, flat and grouped
- An operator rewrites a stored query from the grid, over two lines (regression)

### Field-type cell renderers

- A rating cell renders one glyph per declared `max`, filled to the stored value, with a text equivalent
- Rating glyphs stay monochrome, since the field declares no colour
- A progress cell renders a bar whose fill width tracks the value, with an accessible readout
- A progress bar paints a declared `color`, and stays neutral when none is declared
- A color cell paints the stored hex as a bordered swatch and keeps the code readable
- A barcode cell renders its digits in a fixed-advance face and exposes its symbology
- A duration cell honours its `displayFormat`, reading the stored value as seconds
- A checkbox cell renders a boolean affordance from its field type alone
- A single-attachment cell links the file by name rather than printing its object
- A multiple-attachments cell lists one named link per file
- A to-one relationship cell shows the related record via its declared `displayField`, not the FK
- A to-many relationship cell renders one labelled pill per linked record, and none when unlinked
- A relationship declaring no `displayField` keeps showing the identifier rather than guessing a label
- A datetime / created-at cell renders a human-readable instant, not a raw ISO string
- A rich-text cell previews the prose with markup stripped — never escaped, never executed
- Every registry-less field type renders a purpose-built cell, colour admitted only where declared

### Fill handle and range selection

- An editable cell carries a fill handle; a non-editable one does not
- Dragging the handle previews the span without writing anything
- Releasing the handle writes one record per row in the dragged span, and no further
- Dragging across columns coerces the value per destination column
- Double-clicking the handle fills to the end of the neighbouring column
- A refused cell is named in the error and the rest of the fill still lands
- A non-editable column carries no fill handle
- Shift-click / Shift-Arrow builds a cell RECTANGLE, not a row range
- An operator normalises a column of deals with the fill handle

### Cell editing and row action coexistence

- A grouped grid still edits inline, persists the edit, and leaves sibling fields untouched (R5)
- Double-clicking an editable cell edits rather than firing `onRowClick` (R1)
- A grouped grid wired to a drawer both edits a cell and expands a record (R1 + R5)
- A non-editable cell still fires the row action, on grids with and without editable columns (R2)
- Editing and the row action are each reachable by keyboard, and neither triggers the other (R3 + R4)
- A row ACTION runs its own action rather than the row navigation, and the action actually fires (R6)
- An operator edits cells and expands records in a grouped grid (regression)

### Inline select-edit action

- The `editSelect` trigger reveals an inline `<select>` (named `editSelect.label`) preset to the clicked row's `field` value
- Committing a picked option POSTs the action's arbitrary endpoint with the picked value overriding `$record.<field>` in the body
- `onSuccess.refetch` re-reads the `dataSource.system` grid so the edited cell shows the new value without a reload
- User can reveal the preset `<select>`, POST the picked value to a custom endpoint, and refetch the system grid (dual-dialect)

### Keyboard cell navigation

- ArrowDown/ArrowUp move the cursor one row without scrolling the page
- ArrowRight/ArrowLeft cross read-only columns rather than skipping them
- Home and End jump to the first and last cell of the SAME row
- PageDown/PageUp move a viewport of rows and clamp at the first/last row
- The cursor stops at the grid edges and never wraps
- Enter commits the edit, writes it, and drops the cursor one row
- Escape cancels the edit, writes nothing, and keeps the cell as the cursor
- Shift-Tab is the exact inverse of Tab
- Tab at the row end wraps to the first editable cell of the next row
- The grid is a single tab stop; only the cursor carries `tabindex="0"`
- `role="grid"` / `role="gridcell"` / `aria-selected` do not wait on `aria-label`
- A slow write closes only the cell it was saving, never the next cell's editor
- An operator retypes a column of deals using only the keyboard

### Live refresh and realtime

- Realtime mode opens a connection and announces it in the DOM (already shipped — control)
- A change made right after the connection opens is not guaranteed delivery — ENGINE GAP, confirmed live
- With no `refreshMode` declared, the same change does not arrive (documents the shipped default)
- A connection stuck reconnecting is visible to the reader, not only to the DOM attribute
- Realtime connects, should deliver a change fast, and a broken connection says so

### Data Table Auto-Save

- Data table with `autoSave.saveMode: 'auto'` persists cell edit after debounce
- Only the changed cell value is sent in the PATCH update payload
- Multiple cells edited rapidly batch into per-row save operations
- Editing a different row flushes pending save for the previous row
- Auto-save respects column-level `editable: false` (no save triggered)
- Auto-save respects table-level permissions (404 shows error indicator)
- Data table auto-save regression test

### Kanban & Calendar Auto-Save

- Kanban with `autoSave` persists drag-drop without explicit `persistAction`
- Kanban auto-save updates the groupBy field value after card drop
- Calendar with `autoSave` persists event date changes after drag
- Failed save on kanban reverts card to original column (optimistic rollback)
- Failed save on calendar reverts event to original time slot
- Kanban and calendar auto-save regression test

### Paste Rows from Spreadsheet

- GIVEN a focused table, WHEN I press Ctrl+V with TSV data in the clipboard, THEN a paste preview dialog opens showing parsed rows with column mapping dropdowns
- GIVEN the paste preview dialog, WHEN column headers in the TSV data match table field names, THEN the column mappings are auto-populated with the matching fields
- GIVEN a paste preview with valid column mapping, WHEN I click "Paste", THEN records are created via the batch records API and the dialog closes
- GIVEN pasted data with type mismatches (e.g. text in a number field), WHEN the preview renders, THEN the mismatched cells are highlighted in red with a tooltip explaining the error
- GIVEN a paste of 100+ rows, WHEN record creation completes, THEN a toast shows "N records created" with an Undo option that deletes the batch
- User can complete full paste-from-spreadsheet workflow (regression)

### Copy Rows to Clipboard

- GIVEN selected rows in a table, WHEN I press Ctrl+C, THEN visible column values are copied as TSV to the system clipboard including a header row
- GIVEN no rows selected but a cell focused, WHEN I press Ctrl+C, THEN just the focused cell value is copied as plain text
- GIVEN TSV data copied from a table, WHEN I paste it into Excel or Google Sheets, THEN each field maps to the correct spreadsheet column with proper headers
- User can complete full copy-to-clipboard workflow (regression)
