Agent docs

InlineEdit

Text that edits in place: hover reveals a pencil, click swaps in an input.

Text that edits in place: hover or focus reveals a pencil, a click (or Enter) swaps the text for an input, Enter or blur commits, Escape reverts. Both states render inside the same box metrics, so nothing around it shifts when editing starts (inline-edit.tsx:8-13). The caller supplies whatever typography the spot needs, a heading, a table cell, and wraps InlineEdit inside it.

The canon behind it. docs/design-language/00-overview.md model 4 (00-overview.md:71, feedback is local and layered, belonging to the thing that is loading) and docs/design-language/02-buttons-and-actions.md §A5 (02-buttons-and-actions.md:31, pending state lives on the control performing the action, never a status badge).

When to reach for it#

Renaming something in place, without a separate edit modal or route: a job or run's title, a name inside a table cell. Speedway-only as of this pass; no walmart usage found.

Contract#

  • value: the text currently shown. The caller owns it and re-renders with the committed value; InlineEdit holds only a local edit-mode draft.
  • onCommit(next): value-first, receives the trimmed new text on Enter or blur. May return a promise: the control shows a pending state (a spinner replaces the save icon) until it settles, and stays in edit mode with the draft intact if it rejects. The component itself renders no error text; the caller surfaces the failure. Never called for an empty-after-trim or unchanged draft, the control just drops back to read mode instead.
  • disabled: renders the plain text with no pencil affordance and no edit swap.
  • ariaLabel: accessible name for the edit control. Defaults to `Edit "<value>"`.
  • Slot classnames per InlineEditClassNames: "" the read/edit container, input the edit-mode input.

Mechanics#

Escape sets a ref flag (cancelledRef) before dropping out of edit mode, because Escape also triggers the input's blur event, and without the flag that blur would immediately re-commit the value Escape just cancelled (inline-edit.tsx:27-29,64-68,86-91). The save button's own onClick is mostly redundant with blur-commit for a mouse click (clicking it blurs the input first), but it exists as the keyboard/assistive-tech path; commit()'s own pending guard keeps the pair from double-firing (inline-edit.tsx:99-101). The input's size attribute is Math.max(draft.length, 4), so the box grows with the text instead of clipping or wrapping mid-edit (inline-edit.tsx:93).

Real usage#

All three shipped call sites are speedway rename flows, each committing through the same rename.submit({ intent: "rename-job" | "rename-run", ... }) fetcher pattern:

  • speedway/app/routes/workspaces/jobs/job.tsx:256-272: the job page's <h1> title. PageTitle only auto-styles a string title, so a JSX title like this one has to dress itself.
  • speedway/app/routes/workspaces/jobs/run.tsx:345-359: the same pattern for a run's title, falling back to a generated name (`${MODULE_TITLES[job.module]} run`) when job.name is empty.
  • speedway/app/components/JobsTable.tsx:617-639: inline rename directly inside a table cell, wrapped in a <span onClick={(e) => e.stopPropagation()}> so entering edit mode never also opens the row, with className="w-full min-w-[30ch]" so the name can never crunch below 30 characters in that column (owner ruling, JobsTable.tsx:629-630).

Sanctioned combinations#

CombinationProducesWhere usedWhy
Wrapped in a JSX <h1> inside PageTitle's title slotAn inline-renamable page headingjob.tsx:256-272, run.tsx:340-359PageTitle cannot auto-style JSX titles, so the heading classes move onto the wrapping <h1>
Wrapped in a stopPropagation span with a min-w-[Nch] floor, inside a table cell's renderAn inline-renamable table cell that never triggers the row's own clickJobsTable.tsx:617-639The row is itself clickable (opens the job); the rename control must not also fire that

Banned combinations#

None found. No shipped call site sets disabled, and no shipped call site exists in walmart; both are implemented and correct, not proven in production use outside speedway's rename flows.

Before you adopt this#

Five questions to answer before reaching for InlineEdit.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, InlineEdit is inline content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? Not applicable, no local InlineEdit clone is documented in either app.
  3. Does this app's kit pin reach the version this component or prop landed in? Not applicable, no shipped call site exists outside speedway's rename flows yet.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? ariaLabel defaults to Edit "<value>"; Escape and blur are handled, not the caller's job.
  5. Which canon §A rules bind this surface, and which does the composition break? §A5 keeps the pending state on the control, never a status badge.

Travels with#

  • PageTitle, for the JSX-title wrapping pattern above.
  • RenderIcon, internally, for the pencil/save glyphs.

Snippet#

// speedway/app/routes/workspaces/jobs/job.tsx:256-272 (trimmed)
<PageTitle
className="shrink-0"
title={
// PageTitle styles STRING titles only; a JSX title must dress itself.
<h1 className="truncate text-2xl font-bold">
<InlineEdit
value={job.name}
ariaLabel="Rename this job"
onCommit={(next) =>
rename.submit(
{ intent: "rename-job", jobId: job.id, name: next },
{ method: "post", action: `/workspaces/${wid}/jobs` },
)
}
/>
</h1>
}
subtitle={subtitle}
/>
@versable-git/ui · reference, canon, and method, read in place