feat(ui): the field row both redesigns compose from #211

Merged
nalum merged 2 commits from feat/field-row into main 2026-08-26 18:33:57 +00:00
Owner

Closes #171.

Both surfaces ship a job sheet you read, then press Edit, then fill in a
form, then Save. Every field waits on every other field, and a
half-finished form is a decision about all of them. The redesigns replace
that with rows that are their own controls — so the row has to exist
before any screen can be rebuilt on it.

The affordance rule is enforced by the type, not by convention. The
edit prop is a union: the picker variant can only open something and
always draws the chevron; the in-place variant can only edit here and
never draws one. Neither surface can express the other arrangement, so
"a chevron means a picker opens" cannot drift.

Four states at one height — resting, editing, just committed, will not
commit. Each is a fill plus an inset ring, never a border, so nothing
below the row moves when typing starts; the goldens pin identical heights
across all four. The commit flashes once and raises a snackbar with Undo,
because a persistent tick on a screen where every row saves is noise.
Undo restores the value and reopens the row: a wrong edit is usually a
mistyped one. An invalid value traps the field and never the screen —
close and Escape still work and discard that row alone.

The offline queue is part of the component's contract, not each
caller's problem:

  • the flash fires on enqueue rather than acknowledgement, so the row never
    lies about latency and SyncChip stays the honest in-flight signal;
  • Undo genuinely dequeues — the row claims the queued id, cancels it while
    it is still pending, and tells the caller dequeued=false when it has
    already flushed so they compensate instead of the component pretending;
  • a conflict discovered at flush lands as a persistent notice, because the
    snackbar it belongs to left minutes ago.

An open row ignores live updates while everything around it keeps
refreshing, a commit that overwrote someone says whose change it replaced,
and a record that vanishes while open collapses to a note rather than
disappearing.

Reduced motion drops the movement and keeps the moment on both surfaces:
the flash becomes an instant state held for the same span, paced
explicitly rather than by an animation event that a zeroed duration would
fire after one frame.

Nothing consumes it yet — Jobs.tsx and ItemSheets.kt are untouched on
purpose. #172 is the first screen to compose from it.

conformance/render-field-row.json holds both surfaces to the same string
inventory; goldens are the next commit.

🤖 Generated with Claude Code

Closes #171. Both surfaces ship a job sheet you read, then press Edit, then fill in a form, then Save. Every field waits on every other field, and a half-finished form is a decision about all of them. The redesigns replace that with rows that are their own controls — so the row has to exist before any screen can be rebuilt on it. **The affordance rule is enforced by the type, not by convention.** The edit prop is a union: the picker variant can only open something and always draws the chevron; the in-place variant can only edit here and never draws one. Neither surface can express the other arrangement, so "a chevron means a picker opens" cannot drift. Four states at one height — resting, editing, just committed, will not commit. Each is a fill plus an *inset* ring, never a border, so nothing below the row moves when typing starts; the goldens pin identical heights across all four. The commit flashes once and raises a snackbar with Undo, because a persistent tick on a screen where every row saves is noise. Undo restores the value **and** reopens the row: a wrong edit is usually a mistyped one. An invalid value traps the field and never the screen — close and Escape still work and discard that row alone. **The offline queue is part of the component's contract**, not each caller's problem: - the flash fires on enqueue rather than acknowledgement, so the row never lies about latency and SyncChip stays the honest in-flight signal; - Undo genuinely dequeues — the row claims the queued id, cancels it while it is still pending, and tells the caller `dequeued=false` when it has already flushed so they compensate instead of the component pretending; - a conflict discovered at flush lands as a persistent notice, because the snackbar it belongs to left minutes ago. An open row ignores live updates while everything around it keeps refreshing, a commit that overwrote someone says whose change it replaced, and a record that vanishes while open collapses to a note rather than disappearing. Reduced motion drops the movement and keeps the moment on both surfaces: the flash becomes an instant state held for the same span, paced explicitly rather than by an animation event that a zeroed duration would fire after one frame. Nothing consumes it yet — `Jobs.tsx` and `ItemSheets.kt` are untouched on purpose. #172 is the first screen to compose from it. `conformance/render-field-row.json` holds both surfaces to the same string inventory; goldens are the next commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ui): the field row both redesigns compose from
Some checks failed
check / commits (pull_request) Successful in 9s
check / web (pull_request) Failing after 2m32s
check / go (pull_request) Successful in 2m51s
android / build (pull_request) Failing after 4m43s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 5s
50e6ceb006
The jobs and calendar redesigns replace a view-then-edit model whose real
cost was not the extra screen but the extra decision: every sheet had to
re-answer where a field is edited, when it saves, and what happens when
it will not. Thirteen answers drifted apart before anyone noticed. This
row is the one answer, on both surfaces, so the screens that follow
compose rather than re-decide.

The affordance rule is absolute because a rule with exceptions predicts
nothing: after the first row where a chevron edits in place, the reader
must tap to find out, and every chevron on every screen has stopped
meaning anything. The types enforce it rather than a convention — the
edit parameter is either a picker (which draws the chevron and can only
open) or in-place (which draws no chevron and can only edit here), so the
other arrangement is unsayable in both TypeScript and Kotlin.

The four states are drawn as fills and inset rings and never as an extra
line, so the row is 52px in all of them. Anything else moves the rows
below when typing starts, which on a sheet of six fields is the whole
layout jumping under a thumb. The refusal reason therefore rides in the
placeholder when the field is empty, and only takes a visible slot when
there is typing in the way.

The offline queue lives here rather than in each caller because
per-field commit-on-blur multiplies queued writes, and a sheet whose rows
each invented their own answer would multiply the ways of getting it
wrong. Three rules: the flash fires on ENQUEUE, not acknowledgement —
otherwise the row lies about latency and SyncChip stops being the honest
signal for what is still in flight; Undo against a still-pending write
DEQUEUES it, because a change that never left the device should leave no
trail and a compensating write queued behind the original would replay
as two edits rather than as nothing; and a conflict discovered at flush
lands as a notice, since its snackbar went hours ago.

An open row refuses live updates: everything else keeps refreshing, but
the field being typed in belongs to its editor until they commit or
leave. Undo reopens the row as well as restoring the value, because a
wrong edit is usually a mistyped one.

No screen consumes it yet — deliberately. Jobs.tsx and ItemSheets.kt are
later tickets and in flight.

Test report

Suite Tests Result Skipped
Unit 1434 ✅ pass 1
Integration 130 ✅ pass —

Coverage: 27.0%

Updated by the check workflow · commit c797007dea

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1434 | ✅ pass | 1 | | Integration | 130 | ✅ pass | — | **Coverage:** 27.0% <sub>Updated by the check workflow · commit c797007deaec21cf4af5d83e994b49a2819f6820</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) ❌ 1 failed

Coverage:

Updated by the android workflow · commit c797007dea

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | | ❌ 1 failed | | **Coverage:** <sub>Updated by the android workflow · commit c797007deaec21cf4af5d83e994b49a2819f6820</sub>
nalum force-pushed feat/field-row from 50e6ceb006
Some checks failed
check / commits (pull_request) Successful in 9s
check / web (pull_request) Failing after 2m32s
check / go (pull_request) Successful in 2m51s
android / build (pull_request) Failing after 4m43s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 5s
to ada0df8701
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 3m5s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m13s
android / build (pull_request) Successful in 6m16s
android / report (pull_request) Successful in 3s
2026-08-24 11:24:42 +00:00
Compare
Author
Owner

Force-pushed: the goldens are back in this commit.

The earlier split into #212 made this branch red on check / web and
android / build. Both jobs verify goldens — npm run test:screens and
verifyRoborazziDebug — and this commit adds 54 new gallery cases per surface,
so without their images it could not pass. #212 was green precisely because it
carried them.

The repo rule about re-recording goldens in a separate commit covers changing an
existing component, where the code commit still has the old images to compare
against. A new component has nothing to fall back on. One commit it is.

Force-pushed: the goldens are back in this commit. The earlier split into #212 made this branch red on `check / web` and `android / build`. Both jobs verify goldens — `npm run test:screens` and `verifyRoborazziDebug` — and this commit adds 54 new gallery cases per surface, so without their images it could not pass. #212 was green precisely because it carried them. The repo rule about re-recording goldens in a separate commit covers changing an existing component, where the code commit still has the old images to compare against. A new component has nothing to fall back on. One commit it is.
nalum force-pushed feat/field-row from ada0df8701
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 3m5s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m13s
android / build (pull_request) Successful in 6m16s
android / report (pull_request) Successful in 3s
to 6f1d4ec3d4
All checks were successful
check / commits (pull_request) Successful in 12s
check / go (pull_request) Successful in 2m52s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m17s
android / build (pull_request) Successful in 6m24s
android / report (pull_request) Successful in 3s
2026-08-24 11:34:29 +00:00
Compare
nalum force-pushed feat/field-row from 6f1d4ec3d4
All checks were successful
check / commits (pull_request) Successful in 12s
check / go (pull_request) Successful in 2m52s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m17s
android / build (pull_request) Successful in 6m24s
android / report (pull_request) Successful in 3s
to c797007dea
Some checks failed
check / commits (pull_request) Successful in 15s
android / build (pull_request) Failing after 5m1s
check / go (pull_request) Successful in 2m49s
check / web (pull_request) Successful in 4m13s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
android / build (push) Has been cancelled
android / report (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
check / commits (push) Has been cancelled
tag / tag (push) Has been cancelled
2026-08-26 07:03:31 +00:00
Compare
nalum changed target branch from fix/custom-theme-roles to main 2026-08-26 18:33:54 +00:00
nalum merged commit c797007dea into main 2026-08-26 18:33:57 +00:00
nalum deleted branch feat/field-row 2026-08-26 18:33:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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
eagraiclainne/app!211
No description provided.