From 0ebaf78095033d7c9e569c3799337cb1c6fd46d6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 20:59:26 -0700 Subject: [PATCH 01/10] =?UTF-8?q?docs(spec):=20cell=20editing=20v1=20polis?= =?UTF-8?q?h=20=E2=80=94=20blur-commit=20+=20all-themes=20error/pending=20?= =?UTF-8?q?skin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the two cell-editing follow-ups: blur-to-commit (guarded to the editing phase) and surfacing validate/commit errors + pending state with a11y. Ships the editor skin for every theme (material light+dark, excel light) via two new semantic tokens (--pretable-edit-bg, --pretable-text-error) + grid.css rules, per the @pretable/react=structure / @pretable/ui=skin split. --- ...026-06-09-cell-editing-v1-polish-design.md | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-09-cell-editing-v1-polish-design.md diff --git a/docs/superpowers/specs/2026-06-09-cell-editing-v1-polish-design.md b/docs/superpowers/specs/2026-06-09-cell-editing-v1-polish-design.md new file mode 100644 index 0000000..deb4e13 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-cell-editing-v1-polish-design.md @@ -0,0 +1,137 @@ +# Cell editing v1 polish — design + +**Date:** 2026-06-09 +**Status:** Approved (brainstorm) +**Branch:** `editing-v1-polish` (off latest `main`) + +## Goal + +Close the two known v1 follow-ups from the cell-editing feature (PR #174) and +ship a **productized, all-themes** editor skin: + +1. **Blur-to-commit** — fix the stuck-edit-on-outside-click wart. +2. **Surface validation/commit errors + pending state** in the default editor, + with a11y, and **ship the skin for every theme** (not just DOM hooks). + +Both touch the same files, so they ship together. + +## Architecture context (verified) + +- `@pretable/react` ships **layout-only inline styles** (`styles.ts`: "no colors, + no fonts, no skin"). The skin lives in `@pretable/ui`'s `grid.css`, in + `@layer pretable`, referencing `--pretable-*` **semantic** tokens. +- Token tiers: `tokens.css` defines primitive `--pt-*` palette (incl. + `--pt-sev-err: #b91c1c`); the semantic `--pretable-*` grid tokens are defined + **per theme**: `themes/material.css` (`:root` light + `[data-theme="dark"]` + + density variants) and `themes/excel.css` (`:root` light only). +- No error/invalid semantic token exists today. +- The editing feature already emits `data-pretable-edit-status` on the editing + cell and renders the default editor via `CellEditor` (`packages/react/src/ +cell-editor.tsx`); the surface constructs the `PretableEditorInput` where + `snapshot.editing` (with `status`/`error`) is in scope + (`packages/react/src/pretable-surface.tsx`). + +## Part A — `@pretable/react`: structure, behavior, a11y + +### A1. Extend `PretableEditorInput` + +Add `status: PretableEditStatus` and `error?: string` (from `snapshot.editing`). +Gives the default editor AND custom `renderEditor`s the lifecycle phase + message +(parity). The surface already has these values where it builds the input. Public +type change → regenerate `react.api.md`. + +### A2. Blur → commit in place + +Default editor: `onBlur={() => { if (input.status === "editing") input.commit(); }}` +— commit with **no direction** (no focus move), guarded to the `editing` phase so +it cannot double-submit during an in-flight `validating`/`saving`. A blur fired by +the editor unmounting after a successful commit is a safe no-op (the controller's +`commit` early-returns when `snapshot.editing` is null). Add an RTL test asserting +no double-submit when blurring during `saving`. + +### A3. Error + pending + a11y in the default editor + +- Render an error element when `input.error` is set: + `
{input.error}
` adjacent to the + input. (Shown for both `invalid` — status back to `editing` with a message — and + `error` — commit failure.) +- Input ARIA: `aria-label={column.header ?? columnId}`; `aria-invalid={!!input.error}`; + `aria-busy` true during `checking`/`validating`/`saving`. +- Input `readOnly` during `checking`/`validating`/`saving` (no draft edits + mid-flight); editable on `editing`/`error` (so `Enter` retries after a commit + failure — already supported by the controller). +- Custom `renderEditor`s receive `status`/`error` via the input and may render + their own treatment; the default editor is unaffected by their choice. + +## Part B — `@pretable/ui`: the skin, shipped for every theme + +### B1. New semantic tokens (minimal set) + +- `--pretable-edit-bg` — editor field surface. +- `--pretable-text-error` — invalid-input outline color + error-message text. +- Reuse the existing `--pretable-focus-ring` for the active-editor outline (no new + token). + +### B2. Define the tokens in all themes + +- `themes/material.css`: `:root` (light) **and** `[data-theme="dark"]` (a lighter + red for dark-mode contrast; `--pretable-edit-bg` = the dark editor surface). +- `themes/excel.css`: `:root` (light only). +- Light error value may reference the primitive `--pt-sev-err` (#b91c1c) or a + theme-appropriate red; dark uses a lighter red (e.g. #f2b8b5-ish per M3) for + contrast on the dark surface. + +### B3. `grid.css` rules (in `@layer pretable`) + +- `.pretable-cell-editor`: `background: var(--pretable-edit-bg)`, outline using + `--pretable-focus-ring`, inherit grid font/size, fill the cell box. +- `[aria-invalid="true"].pretable-cell-editor`: outline/border in + `var(--pretable-text-error)`. +- `[data-pretable-edit-error]`: small error text in `var(--pretable-text-error)`, + positioned within/under the cell, `font-size` from the cell scale. +- Pending: `[data-pretable-edit-status="saving"] , [..."validating"] , [..."checking"]` + (or `.pretable-cell-editor[aria-busy="true"]`) → reduced opacity + `cursor: wait`. + **CSS-only** — no spinner markup/JS. + +## Part C — Testing + +- **react** (RTL, in `packages/react/src/__tests__/`): blur commits the draft + + fires `onCellEdit` once; blur during `saving` does NOT double-submit; validate + reject shows the message + `aria-invalid`; commit reject shows `error` and + `Enter` retries; `aria-busy` + `readOnly` during a pending async save; + `aria-label` from `column.header`. +- **ui** (extend the existing `css-cascade`-style contract test in + `packages/ui/src/__tests__/`): assert `--pretable-edit-bg` and + `--pretable-text-error` are defined in `material.css` `:root`, material + `[data-theme="dark"]`, and `excel.css` `:root`; assert `grid.css` contains the + `.pretable-cell-editor` / `[data-pretable-edit-error]` / pending rules. + +## Part D — Docs + +- Update `apps/website/content/docs/grid/editing.mdx`: document blur-commits and + the error/pending/a11y behavior. +- Update the theming **token reference** (`apps/website/content/docs/theming/ +token-reference.mdx`) with `--pretable-edit-bg` and `--pretable-text-error`. + +## Scope + +**In:** both follow-ups + the all-themes skin (material light+dark, excel light) + +- a11y + docs. **Out (unchanged):** optimistic commit, drag-fill, + paste-into-range, multi-cell editing, undo, a spinner/animated busy indicator. + +## Coordination note + +This writes into `themes/material.css`, `themes/excel.css`, and the theming token +reference — files under active parallel theming work. The additions are small and +purely additive (two new semantic tokens + their values + grid rules), but the +theme files will need careful merge/sequence with that parallel work. + +## Risks / open items + +- Exact placement of `[data-pretable-edit-error]` relative to a variable-height + cell (overlay vs push-down) — settle in planning against the real cell DOM; + default to a compact inline element that doesn't disrupt row height during the + brief error window. +- Confirm `excel.css` has no dark variant to mirror (verified: light-only) so the + token set there is `:root` only. From 84148496256f5550cc11dd85b8d8de080eb91f3f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 21:09:25 -0700 Subject: [PATCH 02/10] docs(plan): cell editing v1 polish implementation plan 7-task TDD plan: extend PretableEditorInput (status/error) + surface plumbing (blur commits w/o moving focus); CellEditor blur-commit + error/pending + a11y; surface async error-path tests; edit-bg/text-error tokens across all themes; :where-wrapped grid.css skin; docs; full verification. --- .../2026-06-09-cell-editing-v1-polish.md | 543 ++++++++++++++++++ 1 file changed, 543 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-09-cell-editing-v1-polish.md diff --git a/docs/superpowers/plans/2026-06-09-cell-editing-v1-polish.md b/docs/superpowers/plans/2026-06-09-cell-editing-v1-polish.md new file mode 100644 index 0000000..b9df509 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-cell-editing-v1-polish.md @@ -0,0 +1,543 @@ +# Cell Editing v1 Polish Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close the two cell-editing follow-ups — blur-to-commit and surfacing validation/commit errors + pending state with a11y — and ship the editor skin for every theme. + +**Architecture:** `@pretable/react` adds behavior + a11y + DOM hooks (blur-commit, an error element, `aria-*`, `readOnly`, and `status`/`error` on `PretableEditorInput`). `@pretable/ui` ships the skin: two new semantic tokens defined in every theme + `:where()`-wrapped `grid.css` rules. Controlled/pessimistic flow unchanged. + +**Tech Stack:** TypeScript, React 19, vitest + @testing-library/react (jsdom), api-extractor, vanilla CSS (`@layer pretable`, `--pretable-*` tokens). + +**Spec:** `docs/superpowers/specs/2026-06-09-cell-editing-v1-polish-design.md` + +**Branch:** `editing-v1-polish` (off latest `main`). + +--- + +## File structure + +Modify: + +- `packages/react/src/types.ts` — `PretableEditorInput` gains `status` + `error`. +- `packages/react/src/pretable-surface.tsx` — pass `status`/`error` into the editor input; drop the `?? "down"` commit default (so blur commits with no move). +- `packages/react/src/cell-editor.tsx` — blur-commit, error element, ARIA, `readOnly`. +- `packages/react/react.api.md` — regenerated (public type change). +- `packages/ui/src/themes/excel.css`, `packages/ui/src/themes/material.css` — new tokens. +- `packages/ui/src/grid.css` — editor/error/pending rules. +- `packages/ui/src/__tests__/contract.test.ts` — add the two tokens to the contract list. +- `apps/website/content/docs/grid/editing.mdx`, `apps/website/content/docs/theming/token-reference.mdx` — docs. + +Test files touched/created: + +- `packages/react/src/__tests__/cell-editor.test.tsx` (extend) +- `packages/react/src/__tests__/pretable-surface-editing.test.tsx` (extend) +- `packages/ui/src/__tests__/css-cascade.test.ts` (extend) or `contract.test.ts` + +--- + +## Task 1: Extend `PretableEditorInput` + surface plumbing + +**Files:** + +- Modify: `packages/react/src/types.ts` +- Modify: `packages/react/src/pretable-surface.tsx` +- Regenerate: `packages/react/react.api.md` + +- [ ] **Step 1: Add `status` + `error` to `PretableEditorInput`** — in `packages/react/src/types.ts`, the interface currently is: + +```ts +export interface PretableEditorInput< + TRow extends PretableRow = PretableRow, +> extends Omit, "column"> { + column: PretableColumn; + draft: unknown; + setDraft: (value: unknown) => void; + commit: (direction?: PretableFocusDirection) => void; + cancel: () => void; +} +``` + +Add the two fields and import the status type: + +```ts +import type { PretableEditStatus } from "@pretable/core"; +``` + +```ts + status: PretableEditStatus; + error?: string; +``` + +(Place them after `column` / before `draft`, keeping the interface readable.) + +- [ ] **Step 2: Pass `status`/`error` from the surface + drop the commit default** — in `packages/react/src/pretable-surface.tsx`, find the `` object (~line 1794, where `setDraft`/`commit`/`cancel` are defined and `cellEdit` is the local for this cell's `snapshot.editing`). Add to the input object: + +```ts + status: cellEdit.status, + error: cellEdit.error, +``` + +and change the `commit` line from `(dir?: PretableFocusDirection) => void editController.commit(dir ?? "down")` to drop the default so a no-arg commit performs no focus move: + +```ts + commit: (dir?: PretableFocusDirection) => + void editController.commit(dir), +``` + +(Enter/Tab in the editor pass explicit `"down"`/`"right"`; blur will call `commit()` with no arg → no move.) + +- [ ] **Step 3: Typecheck** + +Run: `pnpm --filter @pretable/react typecheck` +Expected: PASS. (The editor doesn't yet read `status`/`error` — that's Task 2 — but the type + plumbing compile.) + +- [ ] **Step 4: Regenerate the API report** (required gate) + +Run: `pnpm --filter @pretable/react build && pnpm --filter @pretable/react api` +Expected: `react.api.md` updated (`PretableEditorInput` now has `status`/`error`); `pnpm --filter @pretable/react api:check` exits 0. + +- [ ] **Step 5: Run existing react tests (no regression)** + +Run: `pnpm --filter @pretable/react test` +Expected: PASS (existing editing tests still green; the input object now carries status/error but behavior is unchanged so far). + +- [ ] **Step 6: Commit** + +```bash +git add packages/react/src/types.ts packages/react/src/pretable-surface.tsx packages/react/react.api.md +git commit -m "feat(react): add status/error to PretableEditorInput; blur commits without moving focus" +``` + +--- + +## Task 2: CellEditor — blur-commit, error element, a11y (TDD) + +**Files:** + +- Modify: `packages/react/src/cell-editor.tsx` +- Test: `packages/react/src/__tests__/cell-editor.test.tsx` (extend) + +- [ ] **Step 1: Add the failing tests** — append to `cell-editor.test.tsx`. First update the existing `makeInput` helper to include the new required field `status` (default `"editing"`) so it stays type-correct: + +```tsx +// in makeInput's returned object, add: + status: "editing", +``` + +Then add these cases: + +```tsx +it("commits in place (no direction) on blur while editing", () => { + const commit = vi.fn(); + render(); + fireEvent.blur(screen.getByRole("textbox")); + expect(commit).toHaveBeenCalledTimes(1); + expect(commit).toHaveBeenCalledWith(); // no direction → no focus move +}); + +it("does NOT commit on blur while saving (no double-submit)", () => { + const commit = vi.fn(); + render(); + fireEvent.blur(screen.getByRole("textbox")); + expect(commit).not.toHaveBeenCalled(); +}); + +it("renders the error message with role=alert and marks the input invalid", () => { + render( + , + ); + expect(screen.getByRole("alert")).toHaveTextContent("too short"); + expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true"); +}); + +it("is readOnly and aria-busy while saving", () => { + render(); + const box = screen.getByRole("textbox"); + expect(box).toHaveAttribute("readonly"); + expect(box).toHaveAttribute("aria-busy", "true"); +}); + +it("labels the input from column.header", () => { + render( + , + ); + expect(screen.getByRole("textbox")).toHaveAttribute( + "aria-label", + "Full name", + ); +}); +``` + +- [ ] **Step 2: Run the tests, verify they fail** + +Run: `pnpm --filter @pretable/react test -- cell-editor` +Expected: FAIL — no blur handler, no error element, no aria/readOnly. + +- [ ] **Step 3: Rewrite `cell-editor.tsx`** + +```tsx +import { useEffect, useRef } from "react"; + +import type { PretableEditorInput } from "./types"; + +export interface CellEditorProps { + input: PretableEditorInput; +} + +const PENDING_STATUSES: ReadonlySet = new Set([ + "checking", + "validating", + "saving", +]); + +/** + * Renders a column's `renderEditor` if present, otherwise a default text input + * that drives the active edit's draft, commit/cancel, blur-to-commit, and + * surfaces validation/commit errors + pending state with ARIA. + */ +export function CellEditor({ input }: CellEditorProps) { + const ref = useRef(null); + + // Autofocus + select on mount so type-to-replace and immediate typing work. + useEffect(() => { + ref.current?.focus(); + ref.current?.select(); + }, []); + + if (input.column.renderEditor) { + return <>{input.column.renderEditor(input)}; + } + + const pending = PENDING_STATUSES.has(input.status); + + return ( + <> + input.setDraft(e.target.value)} + onBlur={() => { + // Commit in place (no direction → no focus move). Guarded to the + // editing phase so a blur during an in-flight validate/save can't + // double-submit; a blur from unmount-after-commit is a safe no-op. + if (input.status === "editing") input.commit(); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + input.commit("down"); + } else if (e.key === "Tab") { + e.preventDefault(); + e.stopPropagation(); + input.commit("right"); + } else if (e.key === "Escape" || e.key === "Esc") { + e.preventDefault(); + e.stopPropagation(); + input.cancel(); + } + }} + /> + {input.error ? ( +
+ {input.error} +
+ ) : null} + + ); +} +``` + +- [ ] **Step 4: Run the tests, verify they pass** + +Run: `pnpm --filter @pretable/react test -- cell-editor` +Expected: PASS (existing + 5 new). + +- [ ] **Step 5: Commit** + +```bash +git add packages/react/src/cell-editor.tsx packages/react/src/__tests__/cell-editor.test.tsx +git commit -m "feat(react): cell editor blur-commit + error/pending surfacing + a11y" +``` + +--- + +## Task 3: Surface-level async error-path tests + +End-to-end through the real controller + surface (the unit tests above use a mock input). + +**Files:** + +- Test: `packages/react/src/__tests__/pretable-surface-editing.test.tsx` (extend) + +- [ ] **Step 1: Add the failing tests** — append (reuse the file's existing `renderGrid`/cell helpers; if the helper signature differs, adapt to it): + +```tsx +const flush = () => new Promise((r) => setTimeout(r, 0)); + +it("shows a validation message and keeps the editor open on reject", async () => { + render( + + ariaLabel="people" + columns={[ + { + id: "name", + header: "Name", + editable: true, + validate: () => "too short", + }, + ]} + rows={ROWS} + getRowId={(r) => r.id} + viewportHeight={300} + onCellEdit={vi.fn()} + />, + ); + const cell = firstNameCell(); + fireEvent.click(cell); + fireEvent.keyDown(cell, { key: "Enter" }); + fireEvent.change(screen.getByRole("textbox"), { target: { value: "x" } }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(screen.getByRole("alert")).toHaveTextContent("too short"); + expect(screen.getByRole("textbox")).toBeInTheDocument(); +}); + +it("shows an error and allows Enter-retry when commit rejects then resolves", async () => { + const onCellEdit = vi + .fn() + .mockRejectedValueOnce(new Error("save failed")) + .mockResolvedValueOnce(undefined); + render( + + ariaLabel="people" + columns={[{ id: "name", header: "Name", editable: true }]} + rows={ROWS} + getRowId={(r) => r.id} + viewportHeight={300} + onCellEdit={onCellEdit} + />, + ); + const cell = firstNameCell(); + fireEvent.click(cell); + fireEvent.keyDown(cell, { key: "Enter" }); + fireEvent.change(screen.getByRole("textbox"), { + target: { value: "Ada L." }, + }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(screen.getByRole("alert")).toHaveTextContent("save failed"); + // retry + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(onCellEdit).toHaveBeenCalledTimes(2); +}); +``` + +- [ ] **Step 2: Run them** + +Run: `pnpm --filter @pretable/react test -- pretable-surface-editing` +Expected: PASS (these exercise code already implemented in Tasks 1–2 + the existing controller; if a test fails because of a helper-name mismatch, adapt the helper usage — do not change product code). If a genuine product gap surfaces, STOP and report. + +- [ ] **Step 3: Commit** + +```bash +git add packages/react/src/__tests__/pretable-surface-editing.test.tsx +git commit -m "test(react): surface-level validate-reject + commit-error retry paths" +``` + +--- + +## Task 4: Theme tokens — `--pretable-edit-bg` + `--pretable-text-error` (all themes) + +**Files:** + +- Modify: `packages/ui/src/themes/excel.css`, `packages/ui/src/themes/material.css` +- Modify: `packages/ui/src/__tests__/contract.test.ts` + +- [ ] **Step 1: Add the tokens to the contract list (failing test first)** — in `contract.test.ts`, add to the `TOKENS` array: + +```ts + "pretable-edit-bg", + "pretable-text-error", +``` + +- [ ] **Step 2: Run the contract test, verify it fails** + +Run: `pnpm --filter @pretable/ui test -- contract` +Expected: FAIL — `excel.css: --pretable-edit-bg is empty` (themes don't define them yet). + +- [ ] **Step 3: Define the tokens in `excel.css`** — in its `:root` block, add (matching the file's comment style): + +```css +--pretable-edit-bg: #ffffff; /* editor field — matches grid surface */ +--pretable-text-error: #b91c1c; /* invalid outline + error text */ +``` + +- [ ] **Step 4: Define the tokens in `material.css`** — in the `:root` (light) block: + +```css +--pretable-edit-bg: #fcfcfc; /* N99 — matches grid surface */ +--pretable-text-error: #b3261e; /* M3 error (light) */ +``` + +and in the `[data-theme="dark"]` block: + +```css +--pretable-edit-bg: #1c1c1c; /* surface-container-low (dark) */ +--pretable-text-error: #f2b8b5; /* M3 error (dark) */ +``` + +- [ ] **Step 5: Run the contract test, verify it passes** + +Run: `pnpm --filter @pretable/ui test -- contract` +Expected: PASS (both themes define the new tokens at `:root`). + +- [ ] **Step 6: Commit** + +```bash +git add packages/ui/src/themes/excel.css packages/ui/src/themes/material.css packages/ui/src/__tests__/contract.test.ts +git commit -m "feat(ui): edit-bg + text-error tokens across excel + material (light/dark)" +``` + +--- + +## Task 5: `grid.css` editor/error/pending skin (`:where()`-wrapped) + +Every selector MUST be `:where()`-wrapped (the cascade contract test enforces it). + +**Files:** + +- Modify: `packages/ui/src/grid.css` +- Test: `packages/ui/src/__tests__/css-cascade.test.ts` (extend) + +- [ ] **Step 1: Add a presence assertion (failing first)** — append to `css-cascade.test.ts`: + +```ts +test("grid.css styles the cell editor, error, and pending states", () => { + const css = fs.readFileSync(GRID_CSS, "utf8"); + expect(css).toMatch(/:where\(\.pretable-cell-editor\)/); + expect(css).toMatch(/:where\(\[data-pretable-edit-error\]\)/); + expect(css).toMatch(/var\(--pretable-edit-bg\)/); + expect(css).toMatch(/var\(--pretable-text-error\)/); +}); +``` + +- [ ] **Step 2: Run it, verify it fails** + +Run: `pnpm --filter @pretable/ui test -- css-cascade` +Expected: FAIL — those rules don't exist yet. + +- [ ] **Step 3: Add the rules to `grid.css`** — inside the `@layer pretable { ... }` block, append (all selectors `:where()`-wrapped): + +```css +/* Cell editor (inline editing) */ +:where(.pretable-cell-editor) { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: var(--pretable-cell-padding-y) var(--pretable-cell-padding-x); + border: none; + outline: 2px solid var(--pretable-focus-ring); + outline-offset: -2px; + background: var(--pretable-edit-bg); + color: var(--pretable-text-cell); + font-family: var(--pretable-font-sans); + font-size: var(--pretable-font-size-cell); +} + +:where(.pretable-cell-editor[aria-invalid="true"]) { + outline-color: var(--pretable-text-error); +} + +:where(.pretable-cell-editor[aria-busy="true"]) { + opacity: 0.7; + cursor: wait; +} + +:where([data-pretable-edit-error]) { + margin-top: 2px; + color: var(--pretable-text-error); + font-family: var(--pretable-font-sans); + font-size: var(--pretable-font-size-cell); +} +``` + +- [ ] **Step 4: Run the css tests, verify they pass** + +Run: `pnpm --filter @pretable/ui test` +Expected: PASS (new presence test + the existing `:where()`-wrapping contract still holds for the added selectors). + +- [ ] **Step 5: Commit** + +```bash +git add packages/ui/src/grid.css packages/ui/src/__tests__/css-cascade.test.ts +git commit -m "feat(ui): grid.css cell-editor + error + pending skin (layered, :where-wrapped)" +``` + +--- + +## Task 6: Docs — editing behavior + token reference + +**Files:** + +- Modify: `apps/website/content/docs/grid/editing.mdx` +- Modify: `apps/website/content/docs/theming/token-reference.mdx` + +- [ ] **Step 1: Update `editing.mdx`** — add a short subsection documenting: blur commits the current draft in place (no focus move); validation/commit failures show an inline message and keep the editor open (`Enter` retries a failed commit); the input is read-only and `aria-busy` during async validate/save; the editing cell exposes `data-pretable-edit-status` and the error element `data-pretable-edit-error` for custom styling. Keep house voice; every claim must match the shipped behavior. + +- [ ] **Step 2: Update `token-reference.mdx`** — add `--pretable-edit-bg` (editor field surface) and `--pretable-text-error` (invalid outline + error text) to the token table, matching the file's existing row format. + +- [ ] **Step 3: Format + build** + +Run: `pnpm exec prettier --write apps/website/content/docs/grid/editing.mdx apps/website/content/docs/theming/token-reference.mdx && pnpm --filter @pretable/app-website build` +Expected: build PASS. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/docs/grid/editing.mdx apps/website/content/docs/theming/token-reference.mdx +git commit -m "docs(website): editing blur/error/pending behavior + new edit tokens" +``` + +--- + +## Task 7: Full verification + +**Files:** none (verification only) + +- [ ] **Step 1: Package tests** — Run: `pnpm -r --filter './packages/*' test` + Expected: PASS (react + ui suites incl. the new tests). + +- [ ] **Step 2: Typecheck** — Run: `pnpm typecheck` → PASS. + +- [ ] **Step 3: Lint** — Run: `pnpm lint` → PASS. + +- [ ] **Step 4: Format** — Run: `pnpm format` → PASS (all files prettier-clean). + +- [ ] **Step 5: API freshness (required gate)** — Run: `pnpm api:check` → exit 0. If it fails, regenerate in a clean env (`rm -rf node_modules && pnpm install --frozen-lockfile && pnpm api`) and commit. + +- [ ] **Step 6: Website build** — Run: `pnpm --filter @pretable/app-website build` → PASS. + +- [ ] **Step 7: Final commit (any fixups)** + +```bash +git add -A && git commit -m "chore: cell editing v1 polish — verification fixups" +``` + +--- + +## Notes for the executor + +- **`grid.css` selectors must be `:where()`-wrapped** — the `css-cascade` contract test fails otherwise. +- **New tokens must be in `contract.test.ts`'s `TOKENS` and defined at `:root` in BOTH themes** (the contract test checks excel + material `:root`); material also gets the values in `[data-theme="dark"]` for correctness even though the test only checks `:root`. +- **Regenerate `react.api.md`** after the `PretableEditorInput` change (required `API Extractor — report freshness` gate). If a local run disagrees with CI, regenerate in a clean env (`rm -rf node_modules && pnpm install --frozen-lockfile`) — see `project_dependabot_api_extractor_gap` memory. Worktree gotcha: if a run fails with an esbuild error, relink `node_modules/esbuild` → `.pnpm/esbuild@*/node_modules/esbuild`. +- **Match real local names in `pretable-surface.tsx`** — the editor input object is built around line 1794 with the cell's `cellEdit` local (= `snapshot.editing` for that cell). Add `status`/`error`, change the `commit` wrapper to not default the direction. +- The theme files are under active parallel theming work — additions here are purely additive (two tokens), but expect to reconcile at merge. From cd39442a4969c90c90994235cdf4f27c4d2f289f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 21:21:04 -0700 Subject: [PATCH 03/10] feat(react): add status/error to PretableEditorInput; blur commits without moving focus --- packages/react/react.api.md | 6 +++++- packages/react/src/pretable-surface.tsx | 4 +++- packages/react/src/types.ts | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/react/react.api.md b/packages/react/react.api.md index 0915553..a303e6b 100644 --- a/packages/react/react.api.md +++ b/packages/react/react.api.md @@ -215,7 +215,11 @@ export interface PretableEditorInput ext // (undocumented) draft: unknown; // (undocumented) + error?: string; + // (undocumented) setDraft: (value: unknown) => void; + // (undocumented) + status: PretableEditStatus; } // @public @@ -689,7 +693,7 @@ export function ɵuseResolvedHeights(rowHeightProp?: number, headerHeightProp?: // Warnings were encountered during analysis: // -// dist/index.d.ts:464:9 - (ae-forgotten-export) The symbol "PretableSortDirection" needs to be exported by the entry point index.d.ts +// dist/index.d.ts:466:9 - (ae-forgotten-export) The symbol "PretableSortDirection" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/react/src/pretable-surface.tsx b/packages/react/src/pretable-surface.tsx index 724caae..300a583 100644 --- a/packages/react/src/pretable-surface.tsx +++ b/packages/react/src/pretable-surface.tsx @@ -1799,10 +1799,12 @@ export function PretableSurface({ row, column, value, + status: cellEdit.status, + error: cellEdit.error, draft: cellEdit.draft, setDraft: (v: unknown) => grid.setEditDraft(v), commit: (dir?: PretableFocusDirection) => - void editController.commit(dir ?? "down"), + void editController.commit(dir), cancel: () => editController.cancel(), } as unknown as PretableEditorInput } diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index feb8d23..aaa3310 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import type { PretableColumn as PretableBaseColumn, PretableEditInput, + PretableEditStatus, PretableFocusDirection, PretableFormatInput, PretableRow, @@ -31,6 +32,8 @@ export interface PretableEditorInput< TRow extends PretableRow = PretableRow, > extends Omit, "column"> { column: PretableColumn; + status: PretableEditStatus; + error?: string; draft: unknown; setDraft: (value: unknown) => void; commit: (direction?: PretableFocusDirection) => void; From 140b003accea12d79f3fcc4ae14b2ff4254533f8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 21:25:11 -0700 Subject: [PATCH 04/10] feat(react): cell editor blur-commit + error/pending surfacing + a11y --- .../react/src/__tests__/cell-editor.test.tsx | 43 ++++++++++++ packages/react/src/cell-editor.tsx | 70 +++++++++++++------ 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/react/src/__tests__/cell-editor.test.tsx b/packages/react/src/__tests__/cell-editor.test.tsx index 9e25461..52eca44 100644 --- a/packages/react/src/__tests__/cell-editor.test.tsx +++ b/packages/react/src/__tests__/cell-editor.test.tsx @@ -17,6 +17,7 @@ function makeInput( columnId: "name", row: { id: "r1", name: "Ada" }, column: { id: "name" }, + status: "editing", value: "Ada", draft: "Ada", setDraft: vi.fn(), @@ -61,4 +62,46 @@ describe("CellEditor (default)", () => { render(); expect(screen.getByText("custom")).toBeInTheDocument(); }); + + it("commits in place (no direction) on blur while editing", () => { + const commit = vi.fn(); + render(); + fireEvent.blur(screen.getByRole("textbox")); + expect(commit).toHaveBeenCalledTimes(1); + expect(commit).toHaveBeenCalledWith(); // no direction → no focus move + }); + + it("does NOT commit on blur while saving (no double-submit)", () => { + const commit = vi.fn(); + render(); + fireEvent.blur(screen.getByRole("textbox")); + expect(commit).not.toHaveBeenCalled(); + }); + + it("renders the error message with role=alert and marks the input invalid", () => { + render( + , + ); + expect(screen.getByRole("alert")).toHaveTextContent("too short"); + expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true"); + }); + + it("is readOnly and aria-busy while saving", () => { + render(); + const box = screen.getByRole("textbox"); + expect(box).toHaveAttribute("readonly"); + expect(box).toHaveAttribute("aria-busy", "true"); + }); + + it("labels the input from column.header", () => { + render( + , + ); + expect(screen.getByRole("textbox")).toHaveAttribute( + "aria-label", + "Full name", + ); + }); }); diff --git a/packages/react/src/cell-editor.tsx b/packages/react/src/cell-editor.tsx index 5191a7a..f5d77a5 100644 --- a/packages/react/src/cell-editor.tsx +++ b/packages/react/src/cell-editor.tsx @@ -6,9 +6,16 @@ export interface CellEditorProps { input: PretableEditorInput; } +const PENDING_STATUSES: ReadonlySet = new Set([ + "checking", + "validating", + "saving", +]); + /** * Renders a column's `renderEditor` if present, otherwise a default text input - * that drives the active edit's draft and commit/cancel. + * that drives the active edit's draft, commit/cancel, blur-to-commit, and + * surfaces validation/commit errors + pending state with ARIA. */ export function CellEditor({ input }: CellEditorProps) { const ref = useRef(null); @@ -23,27 +30,46 @@ export function CellEditor({ input }: CellEditorProps) { return <>{input.column.renderEditor(input)}; } + const pending = PENDING_STATUSES.has(input.status); + return ( - input.setDraft(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - input.commit("down"); - } else if (e.key === "Tab") { - e.preventDefault(); - e.stopPropagation(); - input.commit("right"); - } else if (e.key === "Escape" || e.key === "Esc") { - e.preventDefault(); - e.stopPropagation(); - input.cancel(); - } - }} - /> + <> + input.setDraft(e.target.value)} + onBlur={() => { + // Commit in place (no direction → no focus move). Guarded to the + // editing phase so a blur during an in-flight validate/save can't + // double-submit; a blur from unmount-after-commit is a safe no-op. + if (input.status === "editing") input.commit(); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + input.commit("down"); + } else if (e.key === "Tab") { + e.preventDefault(); + e.stopPropagation(); + input.commit("right"); + } else if (e.key === "Escape" || e.key === "Esc") { + e.preventDefault(); + e.stopPropagation(); + input.cancel(); + } + }} + /> + {input.error ? ( +
+ {input.error} +
+ ) : null} + ); } From 06223ef934fce37ad575e3c47e16c593a64d3e85 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 21:27:42 -0700 Subject: [PATCH 05/10] test(react): surface-level validate-reject + commit-error retry paths --- .../pretable-surface-editing.test.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/react/src/__tests__/pretable-surface-editing.test.tsx b/packages/react/src/__tests__/pretable-surface-editing.test.tsx index 184a198..8160d4a 100644 --- a/packages/react/src/__tests__/pretable-surface-editing.test.tsx +++ b/packages/react/src/__tests__/pretable-surface-editing.test.tsx @@ -132,4 +132,64 @@ describe("PretableSurface editing", () => { expect(onFocusChange).not.toHaveBeenCalled(); expect(screen.getByRole("textbox")).toBeInTheDocument(); }); + + const flush = () => new Promise((r) => setTimeout(r, 0)); + + it("shows a validation message and keeps the editor open on reject", async () => { + render( + + ariaLabel="people" + columns={[ + { + id: "name", + header: "Name", + editable: true, + validate: () => "too short", + }, + ]} + rows={ROWS} + getRowId={(r) => r.id} + viewportHeight={300} + onCellEdit={vi.fn()} + />, + ); + const cell = firstNameCell(); + fireEvent.click(cell); + fireEvent.keyDown(cell, { key: "Enter" }); + fireEvent.change(screen.getByRole("textbox"), { target: { value: "x" } }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(screen.getByRole("alert")).toHaveTextContent("too short"); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + it("shows an error and allows Enter-retry when commit rejects then resolves", async () => { + const onCellEdit = vi + .fn() + .mockRejectedValueOnce(new Error("save failed")) + .mockResolvedValueOnce(undefined); + render( + + ariaLabel="people" + columns={[{ id: "name", header: "Name", editable: true }]} + rows={ROWS} + getRowId={(r) => r.id} + viewportHeight={300} + onCellEdit={onCellEdit} + />, + ); + const cell = firstNameCell(); + fireEvent.click(cell); + fireEvent.keyDown(cell, { key: "Enter" }); + fireEvent.change(screen.getByRole("textbox"), { + target: { value: "Ada L." }, + }); + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(screen.getByRole("alert")).toHaveTextContent("save failed"); + // retry + fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" }); + await flush(); + expect(onCellEdit).toHaveBeenCalledTimes(2); + }); }); From c7fe246858e448511642da5c7a2059b56fb82218 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 22:07:40 -0700 Subject: [PATCH 06/10] feat(react): associate cell-editor error with input via aria-errormessage --- packages/react/src/__tests__/cell-editor.test.tsx | 11 +++++++++++ packages/react/src/cell-editor.tsx | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/react/src/__tests__/cell-editor.test.tsx b/packages/react/src/__tests__/cell-editor.test.tsx index 52eca44..506d5a5 100644 --- a/packages/react/src/__tests__/cell-editor.test.tsx +++ b/packages/react/src/__tests__/cell-editor.test.tsx @@ -86,6 +86,17 @@ describe("CellEditor (default)", () => { expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true"); }); + it("associates the error message with the input via aria-errormessage", () => { + render( + , + ); + const box = screen.getByRole("textbox"); + const alert = screen.getByRole("alert"); + const errorId = box.getAttribute("aria-errormessage"); + expect(errorId).toBeTruthy(); + expect(alert).toHaveAttribute("id", errorId); + }); + it("is readOnly and aria-busy while saving", () => { render(); const box = screen.getByRole("textbox"); diff --git a/packages/react/src/cell-editor.tsx b/packages/react/src/cell-editor.tsx index f5d77a5..b04fe68 100644 --- a/packages/react/src/cell-editor.tsx +++ b/packages/react/src/cell-editor.tsx @@ -31,6 +31,7 @@ export function CellEditor({ input }: CellEditorProps) { } const pending = PENDING_STATUSES.has(input.status); + const errorId = `pretable-edit-error-${input.rowId}-${input.columnId}`; return ( <> @@ -39,6 +40,7 @@ export function CellEditor({ input }: CellEditorProps) { className="pretable-cell-editor" aria-label={input.column.header ?? input.columnId} aria-invalid={input.error ? true : undefined} + aria-errormessage={input.error ? errorId : undefined} aria-busy={pending ? true : undefined} readOnly={pending} value={String(input.draft ?? "")} @@ -66,7 +68,7 @@ export function CellEditor({ input }: CellEditorProps) { }} /> {input.error ? ( -
+ ) : null} From 8700a65e569d2bab34cd316b8973bbd71961e0e5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 22:14:03 -0700 Subject: [PATCH 07/10] feat(ui): edit-bg + text-error tokens across excel + material (light/dark) --- packages/ui/src/__tests__/contract.test.ts | 2 ++ packages/ui/src/themes/excel.css | 2 ++ packages/ui/src/themes/material.css | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/packages/ui/src/__tests__/contract.test.ts b/packages/ui/src/__tests__/contract.test.ts index 711705a..58d244a 100644 --- a/packages/ui/src/__tests__/contract.test.ts +++ b/packages/ui/src/__tests__/contract.test.ts @@ -37,6 +37,8 @@ const TOKENS = [ "pretable-reorder-ghost-bg", "pretable-reorder-ghost-shadow", "pretable-reorder-drop-indicator", + "pretable-edit-bg", + "pretable-text-error", ]; const THEMES_DIR = path.resolve(__dirname, "../themes"); diff --git a/packages/ui/src/themes/excel.css b/packages/ui/src/themes/excel.css index 91eacec..e1880b9 100644 --- a/packages/ui/src/themes/excel.css +++ b/packages/ui/src/themes/excel.css @@ -30,6 +30,8 @@ --pretable-bg-selected: rgba(16, 124, 65, 0.1); /* Excel green range tint */ --pretable-text-selected: #1f1f1f; /* Don't invert selection text */ --pretable-focus-ring: #107c41; /* Excel app brand green active-cell border */ + --pretable-edit-bg: #ffffff; /* editor field — matches grid surface */ + --pretable-text-error: #b91c1c; /* invalid outline + error text */ /* Accent */ --pretable-accent: #107c41; /* Excel brand green */ diff --git a/packages/ui/src/themes/material.css b/packages/ui/src/themes/material.css index afa4390..bc68512 100644 --- a/packages/ui/src/themes/material.css +++ b/packages/ui/src/themes/material.css @@ -33,6 +33,8 @@ --pretable-bg-selected: #d1e4ff; /* secondary-container — M3 baseline blue */ --pretable-text-selected: #001d36; /* on-secondary-container */ --pretable-focus-ring: #0061a4; /* primary */ + --pretable-edit-bg: #fcfcfc; /* N99 — matches grid surface */ + --pretable-text-error: #b3261e; /* M3 error (light) */ /* Accent */ --pretable-accent: #0061a4; /* primary — M3 baseline blue */ @@ -108,6 +110,8 @@ --pretable-bg-selected: #00497d; /* secondary-container — M3 baseline blue (dark) */ --pretable-text-selected: #d1e4ff; /* on-secondary-container */ --pretable-focus-ring: #9ecaff; /* primary (dark) */ + --pretable-edit-bg: #1c1c1c; /* surface-container-low (dark) */ + --pretable-text-error: #f2b8b5; /* M3 error (dark) */ --pretable-accent: #9ecaff; } From d9316c9ada7d06d984437d97158849ec7429d999 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 22:20:05 -0700 Subject: [PATCH 08/10] feat(ui): grid.css cell-editor + error + pending skin (layered, :where-wrapped) --- packages/ui/src/__tests__/css-cascade.test.ts | 8 +++++ packages/ui/src/grid.css | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/ui/src/__tests__/css-cascade.test.ts b/packages/ui/src/__tests__/css-cascade.test.ts index 00ddd8b..bd8ec93 100644 --- a/packages/ui/src/__tests__/css-cascade.test.ts +++ b/packages/ui/src/__tests__/css-cascade.test.ts @@ -10,6 +10,14 @@ describe("grid.css cascade contract", () => { expect(css).toMatch(/@layer\s+pretable\s*\{/); }); + test("grid.css styles the cell editor, error, and pending states", () => { + const css = fs.readFileSync(GRID_CSS, "utf8"); + expect(css).toMatch(/:where\(\.pretable-cell-editor\)/); + expect(css).toMatch(/:where\(\[data-pretable-edit-error\]\)/); + expect(css).toMatch(/var\(--pretable-edit-bg\)/); + expect(css).toMatch(/var\(--pretable-text-error\)/); + }); + test("every grid.css rule selector is wrapped in :where()", () => { const css = fs.readFileSync(GRID_CSS, "utf8"); const noComments = css.replace(/\/\*[\s\S]*?\*\//g, ""); diff --git a/packages/ui/src/grid.css b/packages/ui/src/grid.css index 0a6144f..ca00ef1 100644 --- a/packages/ui/src/grid.css +++ b/packages/ui/src/grid.css @@ -225,4 +225,35 @@ border: 1px solid var(--pretable-rule); border-radius: var(--pretable-radius); } + + /* Cell editor (inline editing) */ + :where(.pretable-cell-editor) { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: var(--pretable-cell-padding-y) var(--pretable-cell-padding-x); + border: none; + outline: 2px solid var(--pretable-focus-ring); + outline-offset: -2px; + background: var(--pretable-edit-bg); + color: var(--pretable-text-cell); + font-family: var(--pretable-font-sans); + font-size: var(--pretable-font-size-cell); + } + + :where(.pretable-cell-editor[aria-invalid="true"]) { + outline-color: var(--pretable-text-error); + } + + :where(.pretable-cell-editor[aria-busy="true"]) { + opacity: 0.7; + cursor: wait; + } + + :where([data-pretable-edit-error]) { + margin-top: 2px; + color: var(--pretable-text-error); + font-family: var(--pretable-font-sans); + font-size: var(--pretable-font-size-cell); + } } From bdd95fb5df6184b34b732c5988ccc3979d37a245 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 22:38:54 -0700 Subject: [PATCH 09/10] docs(website): editing blur/error/pending behavior + new edit tokens Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/website/content/docs/grid/editing.mdx | 10 ++++++++++ .../content/docs/theming/token-reference.mdx | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/website/content/docs/grid/editing.mdx b/apps/website/content/docs/grid/editing.mdx index f7fb868..bdf4e5d 100644 --- a/apps/website/content/docs/grid/editing.mdx +++ b/apps/website/content/docs/grid/editing.mdx @@ -162,6 +162,16 @@ if (editing?.status === "saving") { the lifecycle for you. +## Default editor behavior + +The default text editor handles commit, errors, and pending state for you — no wiring required: + +- **Blur commits in place.** Clicking away from an open editor commits the current draft without moving focus. (`Enter` and `Tab` commit _and_ move; blur commits and stays put.) +- **Failures keep the editor open.** When `validate` rejects (returns a string) or `onCellEdit` throws, the editor stays open and renders the message inline below the field — so the user can fix the value and try again. Press `Enter` to retry a failed commit, or `Escape` to cancel. +- **Async work locks the field.** While an async `editable`, `validate`, or `onCellEdit` is in flight (the `checking`, `validating`, and `saving` phases), the input is read-only and marked `aria-busy="true"`, so the user can't edit a value mid-save. + +For custom styling, two DOM hooks are exposed: the editing cell carries `data-pretable-edit-status` (the current [lifecycle phase](#lifecycle)), and the inline error element carries `data-pretable-edit-error`. The `@pretable/ui` skin styles both — the field outline turns `--pretable-text-error` while invalid, and the message renders in the same color. + ## Keyboard Editing reuses the focused cell from the [selection](/docs/grid/selection) model. diff --git a/apps/website/content/docs/theming/token-reference.mdx b/apps/website/content/docs/theming/token-reference.mdx index a1de068..88551b1 100644 --- a/apps/website/content/docs/theming/token-reference.mdx +++ b/apps/website/content/docs/theming/token-reference.mdx @@ -1,13 +1,13 @@ --- title: Token reference -description: "The 34-token --pretable-* surface, with shape, default, and purpose for each." +description: "The 36-token --pretable-* surface, with shape, default, and purpose for each." nav: Theming order: 8 --- -Pretable's public theming surface is 34 CSS variables, all `--pretable-*` prefixed. Each theme defines all 34 at `:root`. Excel and Material 3 ship preset values; consumers override individual tokens at `:root` in their own stylesheet (see [Override tokens](/docs/theming/override-tokens)). +Pretable's public theming surface is 36 CSS variables, all `--pretable-*` prefixed. Each theme defines all 36 at `:root`. Excel and Material 3 ship preset values; consumers override individual tokens at `:root` in their own stylesheet (see [Override tokens](/docs/theming/override-tokens)). -> Two tokens — `--pretable-row-height` and `--pretable-header-height` — are read by the engine in JavaScript via the `useResolvedHeights` hook. The other 32 are CSS-only. +> Two tokens — `--pretable-row-height` and `--pretable-header-height` — are read by the engine in JavaScript via the `useResolvedHeights` hook. The other 34 are CSS-only. ## Surfaces (5) @@ -44,6 +44,15 @@ Pretable's public theming surface is 34 CSS variables, all `--pretable-*` prefix | `--pretable-text-selected` | Selected cell/row text color | color | `#1f1f1f` | `#1d192b` (`on-secondary-container`) | | `--pretable-focus-ring` | Focus outline color (cell focus, kbd nav) | color | `#107c41` (Excel green) | `#6750a4` (`primary`) | +## Editing (2) + +These tokens skin the inline cell editor (see [Editing](/docs/grid/editing)). + +| Token | Description | Type | Excel | Material 3 (light) | +| ----------------------- | -------------------------------------------------- | ----- | --------- | ------------------- | +| `--pretable-edit-bg` | Editor field surface (matches the grid background) | color | `#ffffff` | `#fcfcfc` (`N99`) | +| `--pretable-text-error` | Invalid-input outline + inline error-message text | color | `#b91c1c` | `#b3261e` (`error`) | + ## Accent (1) | Token | Description | Type | Excel | Material 3 (light) | From 0cc615e8241f4e2e45b617342f28caf175452108 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 8 Jun 2026 22:44:33 -0700 Subject: [PATCH 10/10] chore(react): prettier-format cell-editor test (verification fixup) --- packages/react/src/__tests__/cell-editor.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react/src/__tests__/cell-editor.test.tsx b/packages/react/src/__tests__/cell-editor.test.tsx index 506d5a5..37a4c4f 100644 --- a/packages/react/src/__tests__/cell-editor.test.tsx +++ b/packages/react/src/__tests__/cell-editor.test.tsx @@ -80,7 +80,9 @@ describe("CellEditor (default)", () => { it("renders the error message with role=alert and marks the input invalid", () => { render( - , + , ); expect(screen.getByRole("alert")).toHaveTextContent("too short"); expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true"); @@ -88,7 +90,9 @@ describe("CellEditor (default)", () => { it("associates the error message with the input via aria-errormessage", () => { render( - , + , ); const box = screen.getByRole("textbox"); const alert = screen.getByRole("alert");