Calendar: the event card is the editor #193

Closed
opened 2026-08-23 22:54:06 +00:00 by nalum · 2 comments
Owner

Part of the calendar redesign. The design handoff (design_handoff_calendar, held outside the repo) is the source; the facts it depends on are repeated here so this issue stands alone.

No Edit button and no separate edit screen (the jobs law: a record has one surface). Rows commit on blur, and a chevron predicts picker-versus-in-place. This is what removes the shipped scope prompt's cause: that flow must ask up front because its form edits everything at once. Per-field commits ask per change, and only where scope is a real question.

  • The subtitle states the series and the position — Tuesdays and Thursdays · 4 of 12 — or, once split, Was part of Tuesdays and Thursdays, in the past tense, because a split never rejoins.
  • One primary action, and it is not Save: Leave this one. On a repeating event it is not scope-free — see the scope issue.
  • ... holds everything destructive: cancel this occurrence, stop it repeating, delete the series.
  • No Never mind — a third exit beside the close control and click-away.
  • ... position follows the container, not the record type: in the title row on a sheet (44x44, 2px border, 13px radius), in the footer beside the filled action on a card (48px).

The pickers land here too: Kind (defaults to GENERAL, each kind carrying its one-line consequence), When, Who's going (multi-select, since an event has no owner), and Remind. The Remind picker must say that a Deadline with a lead also rings a day ahead (reminder.go:134) and that a Birthday reminder broadcasts to every user with an email rather than the attendees (notify.go:407-419).

No event anywhere shows an owner or a Done action. Events are not completable, and that asymmetry with the job card is the clearest statement of the type difference in the product.


Two requirements added after #213 shipped

The job sheet went out to the family's cluster three times before it was right.
Seven visual defects, none caught by a suite: pickers that unfolded forms
instead of opening, three Save buttons on a commit-on-blur screen, (optional)
labels, a reward's name used as a field label, an unstyled add row, checkboxes
with a ghosted tick, and a create form with a browser spinner. Every one had to
be found by a person looking at a screen.

1. Screenshot goldens are part of this ticket

Record gallery (Playwright) and Roborazzi cases for every surface this ticket
builds or visibly changes, and treat them as a deliverable rather than a
follow-up. Three of #213's defects — the unstyled add row, the ghosted
checkboxes, the spinner in the create form — were pixel faults that a golden
would have caught before a deploy did.

#172 deferred its goldens on the reasoning that later tickets would re-record
them. That reasoning was sound then and is not now: the row grammar and the
pickers are settled, so an image recorded here stays valid.

Mechanics, from AGENTS.md: a new case rides in the same commit as the code
that adds it — a commit that adds a gallery or Roborazzi case without its image
is red on its own, because check / web runs npm run test:screens and
android / build runs verifyRoborazziDebug. A re-recorded existing case
goes in its own commit, so the reviewable diff stays code.

2. Check for leaked form styling before you finish

Every visual defect on #213 came from one place: the shipped app's form
fragments reused inside the new row grammar. They pass every test, because the
tests assert the pieces exist rather than that the screen reads right.

Before you call this done, grep the surfaces you touched and report what you
found and what you did about each:

  • a raw <input> / OutlinedTextField / BasicTextField outside the row and
    picker components — a form field dropped into a list of rows
  • type="number", which renders browser spinner arrows
  • btn, link-btn, btn-quiet or TextButton where a row or a quiet row
    verb belongs — underlined links reading as broken text
  • (optional) in any label — §8 names that pattern as the form admitting it
    should not have asked
  • any Save button on a surface that commits on blur, including Save changes and Save the points
  • a disabled-and-grey control where law 5 wants it absent
  • a record's title used as a field row's label — that column is for a
    field's name (Who, When, Worth), and a long title wraps in it

If your ticket is the first to land after this was written, add the check to
AGENTS.md beside the goldens rule, so it stops being a thing I remember to say.


Update — the demo locks the sheet, and two rulings landed (2026-08-25)

calendar-demo.dc.html wires the whole sheet; build from it.

Decided in the updated handoff:

  • An event with no attendees is household-owned: any member may edit and delete it. Attendance is not a permission — a Birthday and a Holiday have no attendees by definition and must never become uneditable, and Save nobody going must never lock an event. The server does not do this yet — #257.
  • Kinds where attendance is meaningless (Birthday, Holiday) show no I'm going.
  • The non-member sheet: every field visible, no chevrons, no ⋯, one I'm going — which asks scope on a repeating event, and should ride #209's JoinOccurrence, not Split+AddUser.

Demo specifics now normative: the subtitle's position reads · 4 of 12 on an until-bounded rule and · the 9th since 3 August on an endless one; a field commit's undo puts the value back and reopens the row; an empty name traps its own field (⚠ Needs a name), never the sheet; the Who picker commits with Save 2 going / Save nobody going; the When picker is chips → month grid → an All day toggle that hides the time rows → Starts/Ends as real 15-minute lists (the end list only offers times after the start and names each duration), a tap on a date never closes the sheet, and the commit reads Move it here; the Remind picker's title is Remind who's going and it explains itself only where the audience is not that row (Deadline's second ring, Birthday's broadcast).

Open conflict, ruled in #257 before this builds: the demo gates editability on presence (member), and §0.1 says joining turns the sheet into the editor — but ADR-0036 (PRs #205/#209) deliberately splits attendance from authority: a self-add writes an ATTENDEE ref that cannot edit. One of the two must move.


Correction — #257 is ruled, the other way (2026-08-25)

The "household-owned" bullet above is superseded. #257's ruling: ownerless events stay admin-only editable, and the state is prevented instead — the last attendee cannot leave (they delete instead), so Save nobody going cannot stand as written. And joining does not grant edit rights: the sheet gates its chevrons and ⋯ on holding an OWNER ref, not on presence — an attendee who joined sees the read-only shape with Leave this one as its primary. The design project is updating the handoff and demo to match; revalidate the bundle against #257 before building this ticket.

Part of the calendar redesign. The design handoff (`design_handoff_calendar`, held outside the repo) is the source; the facts it depends on are repeated here so this issue stands alone. No Edit button and no separate edit screen (the jobs law: a record has one surface). Rows commit on blur, and a chevron predicts picker-versus-in-place. This is what removes the shipped scope prompt's cause: that flow must ask up front because its form edits everything at once. Per-field commits ask per change, and only where scope is a real question. - **The subtitle states the series and the position** — *Tuesdays and Thursdays · 4 of 12* — or, once split, *Was part of Tuesdays and Thursdays*, in the past tense, because a split never rejoins. - **One primary action, and it is not Save:** `Leave this one`. On a repeating event it is not scope-free — see the scope issue. - **`...` holds everything destructive**: cancel this occurrence, stop it repeating, delete the series. - No `Never mind` — a third exit beside the close control and click-away. - `...` position follows the container, not the record type: in the title row on a sheet (44x44, 2px border, 13px radius), in the footer beside the filled action on a card (48px). The pickers land here too: **Kind** (defaults to `GENERAL`, each kind carrying its one-line consequence), **When**, **Who's going** (multi-select, since an event has no owner), and **Remind**. The Remind picker must say that a Deadline with a lead also rings a day ahead (`reminder.go:134`) and that a Birthday reminder broadcasts to every user with an email rather than the attendees (`notify.go:407-419`). No event anywhere shows an owner or a `Done` action. Events are not completable, and that asymmetry with the job card is the clearest statement of the type difference in the product. --- ## Two requirements added after #213 shipped The job sheet went out to the family's cluster three times before it was right. Seven visual defects, none caught by a suite: pickers that unfolded forms instead of opening, three Save buttons on a commit-on-blur screen, `(optional)` labels, a reward's name used as a field label, an unstyled add row, checkboxes with a ghosted tick, and a create form with a browser spinner. Every one had to be found by a person looking at a screen. ### 1. Screenshot goldens are part of this ticket Record gallery (Playwright) and Roborazzi cases for every surface this ticket builds or visibly changes, and treat them as a deliverable rather than a follow-up. Three of #213's defects — the unstyled add row, the ghosted checkboxes, the spinner in the create form — were pixel faults that a golden would have caught before a deploy did. `#172` deferred its goldens on the reasoning that later tickets would re-record them. That reasoning was sound then and is not now: the row grammar and the pickers are settled, so an image recorded here stays valid. Mechanics, from AGENTS.md: a **new** case rides in the same commit as the code that adds it — a commit that adds a gallery or Roborazzi case without its image is red on its own, because `check / web` runs `npm run test:screens` and `android / build` runs `verifyRoborazziDebug`. A **re-recorded existing** case goes in its own commit, so the reviewable diff stays code. ### 2. Check for leaked form styling before you finish Every visual defect on #213 came from one place: the shipped app's form fragments reused inside the new row grammar. They pass every test, because the tests assert the pieces exist rather than that the screen reads right. Before you call this done, grep the surfaces you touched and report what you found and what you did about each: - a raw `<input>` / `OutlinedTextField` / `BasicTextField` outside the row and picker components — a form field dropped into a list of rows - `type="number"`, which renders browser spinner arrows - `btn`, `link-btn`, `btn-quiet` or `TextButton` where a row or a quiet row verb belongs — underlined links reading as broken text - `(optional)` in any label — §8 names that pattern as the form admitting it should not have asked - **any Save button on a surface that commits on blur**, including `Save changes` and `Save the points` - a disabled-and-grey control where law 5 wants it absent - a record's title used as a field row's `label` — that column is for a field's name (`Who`, `When`, `Worth`), and a long title wraps in it If your ticket is the first to land after this was written, add the check to AGENTS.md beside the goldens rule, so it stops being a thing I remember to say. --- ## Update — the demo locks the sheet, and two rulings landed (2026-08-25) `calendar-demo.dc.html` wires the whole sheet; build from it. Decided in the updated handoff: - **An event with no attendees is household-owned:** any member may edit and delete it. Attendance is not a permission — a Birthday and a Holiday have no attendees by definition and must never become uneditable, and *Save nobody going* must never lock an event. The server does not do this yet — #257. - **Kinds where attendance is meaningless (Birthday, Holiday) show no `I'm going`.** - The non-member sheet: every field visible, no chevrons, no `⋯`, one `I'm going` — which asks scope on a repeating event, and should ride #209's `JoinOccurrence`, not Split+AddUser. Demo specifics now normative: the subtitle's position reads `· 4 of 12` on an until-bounded rule and `· the 9th since 3 August` on an endless one; a field commit's undo puts the value back **and reopens the row**; an empty name traps its own field (`⚠ Needs a name`), never the sheet; the Who picker commits with `Save 2 going` / `Save nobody going`; the When picker is chips → month grid → an *All day* toggle that hides the time rows → `Starts`/`Ends` as real 15-minute lists (the end list only offers times after the start and names each duration), a tap on a date never closes the sheet, and the commit reads `Move it here`; the Remind picker's title is `Remind who's going` and it explains itself only where the audience is not that row (Deadline's second ring, Birthday's broadcast). **Open conflict, ruled in #257 before this builds:** the demo gates editability on presence (`member`), and §0.1 says joining turns the sheet into the editor — but ADR-0036 (PRs #205/#209) deliberately splits attendance from authority: a self-add writes an ATTENDEE ref that cannot edit. One of the two must move. --- ## Correction — #257 is ruled, the other way (2026-08-25) The "household-owned" bullet above is superseded. #257's ruling: **ownerless events stay admin-only editable**, and the state is prevented instead — the last attendee cannot leave (they delete instead), so *Save nobody going* cannot stand as written. And **joining does not grant edit rights**: the sheet gates its chevrons and `⋯` on holding an OWNER ref, not on presence — an attendee who joined sees the read-only shape with `Leave this one` as its primary. The design project is updating the handoff and demo to match; revalidate the bundle against #257 before building this ticket.
Author
Owner

Delivered in PR #263: EventSheet.tsx / EventSheet.kt — the card is the editor, FieldRow grammar, OWNER-ref gating per the #257 ruling (attendance is not authority), JoinOccurrence/LeaveOccurrence replacing the split-then-add pair, sole-person footnotes instead of a Leave row.

Delivered in PR #263: EventSheet.tsx / EventSheet.kt — the card is the editor, FieldRow grammar, OWNER-ref gating per the #257 ruling (attendance is not authority), JoinOccurrence/LeaveOccurrence replacing the split-then-add pair, sole-person footnotes instead of a Leave row.
Author
Owner

Delivered by PR #263 (EventSheet — the card is the editor, owner-gated per the #257 rulings, Join/LeaveOccurrence), merged to main 2026-08-26.

Delivered by PR #263 (EventSheet — the card is the editor, owner-gated per the #257 rulings, Join/LeaveOccurrence), merged to main 2026-08-26.
nalum closed this issue 2026-08-26 18:48:00 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#193
No description provided.