Show decision evidence as a summary row with objections #361
- MA
## Why
On a decision card that came from Chat, the reasons, constraints and objections were hidden behind a `<>` icon in the header. Objections never showed up at all, even though they had been applied. The popover had no title, used a text "Close" button, and showed a lone avatar with no label. The Decisions tab showed no evidence, even though it's the view built for working through decisions.
## What changed
- **Summary row on the card.** Under the options, at the start of the action row: "1 reason 路 1 constraint 路 1 objection" (`text-xs`, tertiary; the objection count uses `text-warning-ink`). It's the trigger for the popover. The header `<>` icon is gone. - **Popover.** It now has an "Evidence" header and an icon close (`btn-icon btn-ghost`, `CloseIcon`). Rows are grouped by option. Options with no evidence are left out. Decision-wide items sit under "Applies to all options" instead of a repeated question title. Avatars are labelled ("Supported by", "Opposed by"). It opens and closes with the shared `motion-popover` recipe (150 ms close, honours reduced motion). - **Objections.** `evidenceRows` now turns quoted oppose stances into `objection` items, filed under their option or as decision-wide. Each shows its quote, `@handle` and a source jump. **No server change:** objections were already in client thread state as `stances` with `position: "oppose"`. They were dropped because the client only drew an avatar ring, and skipped stances that had no option. If an objection already names a person, that person isn't repeated in "Opposed by". - **Decisions tab.** Open cards in the list now get the same summary and popover. `showEvidence` no longer requires the document view. - API: the editor's `evidence` option now returns `{ summary, content }` (`DecisionEvidence`) instead of a bare node. `QuestionView` gains an optional `summary` slot in its footer.
I kept these edits off the lines touched by #289, #302 and #347, using separate insertion points in `questionnaire.tsx`, `question-view.tsx`, `decisions.tsx` and `room-workspace.tsx`.
## Screenshots
Card, before (evidence behind the header `<>`): 
Card, after (summary row): 
Popover, before (no title, text Close, empty option rows, unlabelled avatar, no objection): 
Popover, after: 
Decisions tab, before (no evidence): 
Decisions tab, after: 
Phone (390 px), after: 
The Jev fixture sends options to the first open card, so in these shots the evidence lands on a sample card. That's the fixture's routing, not the product.
## Follow-up polish (review)
- Phone (under 640px): evidence opens as a bottom sheet and scrolls the card so its actions sit above it. Wider viewports fall back to below or above the card before ever covering it. - The dialog takes focus itself, so the close icon doesn't open with a tooltip. `title` was dropped. - "Opposed by" rings use the warning tone. - The summary trigger has a chevron and a pointer cursor. - Motion comes from `motionContract("popover")`, with a sheet variant that rises using the drawer tokens. - A leading "I object:" / "Objection:" marker is dropped from objection quotes. Nothing else is rewritten.
Phone sheet, after: 
## Testing
- `bun run types`: pass - Native suite `bunx playwright test -c e2e/source/playwright.config.ts evidence-hover.native.ts`: 10 passed. Its fixture had a stale `useState` import (it failed on main too); fixed. It now asserts the sheet leaves the card's actions clear. `decision-evidence.e2e.ts` adds the same assertion at 390 px. - `bun test apps/web packages/editor packages/question`: 1127 pass. Four tests (`focus.test.ts` 脳3 and the icon-family test) timed out under load and pass when run alone. - New unit coverage: objections in `evidenceRows` and `evidenceCounts`; popover grouping and objection styling; summary text; the summary showing on both inline and listed cards. - E2E specs updated for the new trigger (`/^Evidence:/`). `decision-evidence.e2e.ts` now also asserts the summary text, the popover header, the objection, and opening evidence from the Decisions tab. Local E2E is suspended for this run, so CI is the source of truth. - `bun run ci`: dprint, oxlint and tokens pass. The design-contract check fails only on pinned hashes, listed below.
## Needs a design-contract review
The design-contract check reports "reviewed dynamic owner changed" for files pinned in `scripts/design-contract/exceptions/`. Their reviewed dynamic class expressions didn't change. The lines only moved because of new props or slot wiring:
- `packages/question/src/react/question-view.tsx` (lines 449, 450, 466, 562): `dynamic-packages.json` - `packages/editor/src/plan-editor.tsx` (341, 352): `dynamic-editor.json` - `packages/editor/src/widgets/questionnaire.tsx` (196, 343, 498): `dynamic-editor.json`
New dynamic exception needed: `packages/editor/src/widgets/evidence-hover.tsx` (`motion?.className` and `presence.className` on the panel). These are the host's `motionContract("popover")` class and the finite `is-open`/`is-closing` presence state. The pattern matches `MotionDisclosure`'s existing exception in `dynamic-editor.json`.
馃 Generated with [Claude Code](https://claude.com/claude-code)
- MA
**Review:** Looks good (non-blocking polish below)
Checked in a real browser (desktop 1440x900, phone 390x844, read-only viewer, Decisions tab), plus code and `gh pr checks`.
**What works** - Summary row reads calmly ("1 reason 路 1 constraint 路 1 objection", tertiary, amber only on the objection count). Accessible name is "Evidence: 1 reason 1 constraint 1 objection" (sr-only prefix, dots aria-hidden). Hover gives a soft fill, focus shows the ring, `aria-expanded` keeps the fill while open. - Popover hierarchy is clear: Evidence header + icon close, option groups, "Applies to all options", labelled avatars. Objection label is amber text only: warning without alarm. Cards with no evidence (the fake "Should we ship a small pilot?" card) show nothing. Read-only viewer still gets the summary and can read the popover. Decisions tab shows the same summary and popover. - API change: `evidence` option now returns `{summary, content}`. grep shows the only consumers are `room-workspace.tsx` (PlanEditor + Decisions), both updated; no other callers. - CI: only the design-contract "reviewed dynamic owner changed" hashes fail (listed in the PR, safe, not touched by me). e2e and container pass.
**Findings** 1. Phone (should fix): the popover (320px, left-aligned at 58px) opens over the lower half of its own card and covers the summary trigger and Discard/Save. Closing is only via the X or outside tap. Suggest below 640px render it as a bottom sheet (or place it below the card) with the same content. e2e only asserts the summary is in the viewport, not the popover/card overlap. 2. Close button autofocus on open shows the "Close evidence" tooltip every time on pointer open (`evidence-hover.tsx`, close button `title`). Drop the `title` (aria-label is enough) or focus the panel instead. 3. Opposers' avatars still use `ring-destructive` (red) while the objection label is amber. Pick one tone (amber ring) so "Opposed by" matches the objection text. 4. Discoverability: trigger has `cursor: default` and no affordance until hover. Consider a trailing chevron or `cursor-pointer` per the app's button convention; on touch there is no hover at all. 5. #353 (open) drives its analysis popover with `motionContract("popover")`; this PR hardcodes `motion-popover is-open/is-closing` class strings. Same CSS recipe, but worth converging when both land. 6. The objection quote is the whole chat message ("I object: ..."), so the leading "I object:" repeats the "Objection" label. Not this PR's fault, but looks clumsy.
Motion: CSS recipe `motion-popover` (opacity + scale, 150ms close, reduced-motion rule present). Frame capture under heavy machine load was too coarse to judge easing, but open/close both complete with no jump and focus returns to the trigger on close.
- MA
Thanks for the review. I addressed all six points in 37eb4e7d:
1. **Phone:** under 640px, evidence opens as a bottom sheet (`data-side="sheet"`, full width, max 70dvh). The comment sheet is tied to comment wording and is being changed in #301, so I didn't reuse it. On open, the sheet scrolls the plan or Decisions list so the card's actions sit above it. At 640px and up, if neither side fits, the panel now goes below or above the card before it would clamp over it. The native suite and `decision-evidence.e2e.ts` (390px) both assert that the summary's bottom is at or above the sheet's top. 2. **Tooltip:** focus goes to the dialog itself (`tabIndex={-1}`), and the close icon no longer has a `title`. 3. **Tone:** "Opposed by" rings now use `ring-warning`. 4. **Trigger:** the summary trigger has a 12px chevron and `cursor: pointer`. 5. **Motion:** the host passes `motionContract("popover")` through `DecisionEvidence.motion`. A sheet variant (`[data-motion-presentation="sheet"]`) rises using the drawer tokens. This adds one dynamic-class exception for `evidence-hover.tsx`, which is noted in the PR body. 6. **Markers:** only a leading `I object:` or `Objection:` (case-insensitive) is removed from objection quotes. The rest of the quote is unchanged.
I also fixed a stale `useState` import in the evidence-hover native fixture; the suite was failing on main because of it. It now passes: 10 tests.
MaggieAppletonhead-ref-force-pushed 1c01ee3fd738last month on GitHub