Skip to content

Add a decision option with one confirm, wrapped and auto-selected - #541

Merged
MaggieAppleton merged 8 commits into
mainfrom
design-add-option-flow
Oct 13, 2026
Merged

MaggieAppleton merged 8 commits into
mainfrom
design-add-option-flow

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the two "Add an option" problems on decision cards (inline deck and Decisions tab share QuestionView).

What changed

1. Long options wrap instead of scrolling the card sideways. The field was a single-line <input> with field-sizing: content, so long text widened the row and the card (overflow: hidden, which still allows programmatic scroll) scrolled left to follow the caret, clipping the question and options. Now:

  • The field is a <textarea rows=1> that grows downwards (field-sizing: content, with a small height fallback for browsers without it). Pasted line breaks become spaces, because option labels are one line in the document.
  • SidecarCard uses overflow: clip, so focus or caret reveal can never scroll a card horizontally.
  • Option labels get overflow-wrap: anywhere so a long URL can't push the row wide either.

2. One confirm adds the option and chooses it. The leading + tile becomes a brand-coloured check button as soon as there is text, with a cross-fade (opacity/scale/blur; opacity only under reduced motion). It stays a quiet + while empty or when the text duplicates an option. Enter does the same; Escape cancels. Shift+Enter does not add a newline, because labels are single-line.

After the server acknowledges question:option (the reply already returned option; the controller now passes its id through), the view:

  • selects the new option through the same path as clicking a row (onLocalChange + onChange), so the save bar counts it and Save is enabled immediately;
  • collapses the field back to the quiet + Add an option row;
  • moves focus to the new option's radio. Its focus ring shows only after a keyboard confirm. A mouse confirm gives the radio focus without :focus-visible;
  • announces "Added and selected " through a polite status region.

Server contract is unchanged. Selection happens only after the acknowledgement (persistence before publication). There is no optimistic option, and the idempotency key still comes from the controller. Other writers' drafts: in a single-choice question, a collaborator's existing answer in the shared draft is not replaced. The option is still added for everyone, and the announcement says "Added ". Your own earlier answer is replaced only while the shared draft still matches it. For a multiple-choice question, the new option is added to the set and nothing is unchecked. Escape or removing the question card while the request is pending abandons the selection. Reaching the 10-option limit (which unmounts the field) still selects.

Screenshots

Before (inline deck, 1440px): the long text scrolls the whole card left; after Enter the option is added unselected, the focused trigger looks like an empty field, and Save stays disabled.

After: the field wraps, the check confirms, the option is selected, the add row is quiet and Save is ready. Also shown: the Decisions tab with a mouse confirm (no ring), a collaborator's view, and phone at 390px with no horizontal page overflow.

Tests

  • Unit (packages/question): chooseAdded (single choice, own vs collaborator, multiple), optionText, option rows carry ids, and the controller returns the acknowledged option id.
  • New e2e (inline-decision-deck.e2e.ts): a long option wraps (field taller than 1.5 lines, no ancestor scrollLeft, heading still in view), and one confirm results in a checked option, a collapsed field and an enabled Save.
  • I updated these e2e tests to the new behaviour: sidecar-option-contracts (a collaborator's choice is kept, the adder's own choice moves, focus goes to the new option, and the refresh test now expects the adder's latest choice), sidecar.e2e (a multiple-choice add is checked and Save is enabled), sidecar-option-async (a delayed ack selects and focuses), and conversation-plan-ui (adding now overrides a chat suggestion in one step). I also updated the source-native terminal-origin fixture.
  • Local validation: bun run ci, bun run types, bun run build, and all 153 question tests pass. All 16 option contract/delayed-reply browser cases passed; two context-teardown timeouts passed on a warm single-worker rerun with the original timeouts unchanged.

Design-contract review

The reviewed owner hashes are renewed. Related and Callout retain their class expressions and data flow; SidecarCard only changes overflow-hidden to overflow-clip. The pending-add guard follows the question view’s lifetime and current question, so removing the composer at the 10-option limit still selects the final option.

The deck pager is untouched.

🤖 Generated with Claude Code

Before: long text scrolls the whole card sideways

Before: after Enter, unselected and a focused trigger

After: field open, quiet plus

After: long text wraps, plus became a check

After: confirmed by Enter, selected and Save ready

After: Decisions tab, mouse confirm, no ring

After: a collaborator sees the new option

After: phone, wrapping field

After: phone, confirmed with save bar

Review follow-up

The check tile has a visible keyboard focus ring. Adding PostgreSQL preserves the collaborator’s newer SQLite choice.

Keyboard confirm focus and preserved collaborator choice

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Changes needed

Checked in Chromium against a fake-GitHub server (Decisions tab at 1440×900 and phone at 390×844, plus two users at once). Both of Maggie's asks work. Long URLs and long words wrap in the field and in the added row. There's no scrollLeft on any ancestor and no page overflow at either width. One Enter or check press adds the option, selects it, collapses the field, enables Save, focuses the new radio (focus ring after keyboard, none after mouse) and announces it. Multiple choice adds to the set. The 10th option still selects once the field unmounts. A duplicate keeps the + with the hint. Pasted line breaks become spaces. Escape returns focus to the trigger. Two issues need fixing first:

  1. A collaborator's newer choice is overwritten (desktop). question-view.tsx added() passes touched as own. Off-phone there's no PendingAnswersContext, so touched is ownTouched. That flag stays true once this person clicks anything on the card. It doesn't reset when someone else changes the draft afterwards. Repro: ana picks A, cat then picks B (ana now sees B), ana adds "Option E". The shared draft becomes E for both of them and cat's B is lost. That breaks the "never replace a collaborator's single choice" rule. Fix: treat it as own only when the draft still holds this person's last pick. Remember the last selection per question that choose() sent and compare it with selectionOf(draft), the same idea as stillMine on phone. touched is also card-wide, so on a multi-question card a click on Q2 counts as permission to replace Q1. Please add a contract e2e for "collaborator changed after my pick → add doesn't select".

  2. The ready check button has no visible focus ring. .question-add-confirm:focus-visible uses --focus-ring-color (brand) with --focus-ring-offset (-2px, inset) on a brand background, so the ring disappears (computed outline: brand 2px, offset -2px; Shift+Tab from the field shows nothing). Fix: use a positive offset such as var(--edge-width)/2px for this filled tile, or a contrasting ring colour.

Minor:

  • The unmount guard (useEffect(() => () => edit.current++)) is gone, so the limit case still selects. But stepping to another decision, or leaving the tab, while a request is pending now also writes a selection into the question you left. Probably fine, but consider guarding only the limit case, e.g. keep the guard and skip it when question.options.length + 1 >= MAX_SHARED_OPTIONS.
  • overflow-clip on SidecarCard: safe. Its only consumers are the questionnaire cards and the audit/experiment pages. There are no sticky descendants, and absolute popovers were already clipped by hidden. The card is flex, so losing the BFC changes no margins. Focus rings at the edges look the same.

Design contract: question-view.tsx Related (556/557/573) and Callout (733) only moved lines and their bodies are byte-identical. card.tsx 55/59 only changes the literal overflow-hidden→overflow-clip in the same template, with the same surface/focused/className inputs. The data flow is unchanged, so the hashes are safe to renew.

#540 overlap: a trial merge of design-deck-motion-decided into this branch is clean (both touch inline-decision-deck.e2e.ts). #540 edits dynamic-editor.json, so renewing the card.tsx hash there may conflict with it in text. Land one first, then renew on top.

MaggieAppleton and others added 7 commits October 13, 2026 11:00
The add field was a single-line input sized to its content, so a long
option widened the row and the card scrolled sideways to follow the
caret. It is now a wrapping textarea, and the sidecar card clips rather
than hides overflow so focus can never scroll it.

The leading plus tile becomes the confirm check once there is text.
Enter or the check adds the option; after the server acknowledges it
with its id, the view selects it for the adder, moves focus to the new
row and announces it, so Save is ready at once. A collaborator's single
choice in the shared draft is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deck's own status line is found by role; a live region without the
status role announces the added option without competing with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maggie approved renewing these hashes. card.tsx only swaps the overflow-hidden
literal for overflow-clip; question-view.tsx adds refs and static classes. The
reviewed dynamic class compositions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds on leaving

A just-added option replaces a single choice only while the shared draft
still holds this person's last pick for that question, not whenever they
touched the card. The confirm check's focus ring sits outside its filled
tile so it stays visible. Leaving the question while an add is pending
abandons its selection again, except when the add filled the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ator-choice fix

The commit only changes selection logic and refs; the reviewed Related and
Callout class compositions moved but are unchanged. Approved by Maggie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton merged commit bb78e9e into main Oct 13, 2026
3 of 4 checks passed
@MaggieAppleton
MaggieAppleton deleted the design-add-option-flow branch October 13, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant