Skip to content

ui: decision toggle on any question, and a separate opener for the host's decision card - #1753

Merged
backnotprop merged 2 commits into
mainfrom
feat/ui-decision-toggle-any-question
Oct 7, 2026
Merged

backnotprop merged 2 commits into
mainfrom
feat/ui-decision-toggle-any-question

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Follow-up to the 0.52.0 decision toggle for the Workspaces Inbox (approved design: "the tag is the switch" on every question, off by default; "clicking the tag's words opens the decision card"). Targets ui 0.52.1, additive. No version bump in this PR.

What changes

  1. Any question can record a decision. questionDecisionRecording (Viewer) / decisionRecording (QuestionBlock) now also reaches a question with no Decision: line. A boolean adds the "◇ Records a decision" tag in that state, and with the toggle handler makes it the toggle. undefined leaves the question tag-less, as before. On shows the "Answering this records a decision" row. Off hides it. A recorded decision is never affected.

  2. A separate opener. New onOpenQuestionDecision(key, anchor) (Viewer, BlockRenderer) / onOpenDecision(key, anchor) (QuestionBlock). When it is set, the tag is two sibling buttons inside one visual tag ([data-question-decision-tag]):

    • The diamond is the switch: aria-pressed, labelled "Record as a decision", [data-question-decision-toggle].
    • The words are a button with aria-haspopup="dialog" and [data-question-decision-open]. It calls the handler with its own element, which the host uses as the popover anchor.

    The opener works whether recording is on or off; the host decides what the card does for an off question. Without the opener, the whole tag is the toggle, exactly as in 0.52.0.

  3. Read-only cards are never interactive. Viewer drops the opener under readOnly.

Behavior change for 0.52.0 hosts: a host that returned a boolean for every question now gets the tag on plain questions. To keep a plain question without a tag, return undefined for it. This is in HANDOFF.

Verification

  • Byte-identical without props: I rendered main and this branch through BlockRenderer and Viewer, both interactive and readOnly, with no new props. The outputs are identical (cmp).
  • Tests: new DOM test QuestionBlock.decisionAnyQuestion.test.tsx, added to the DOM_TESTS step. I updated one 0.52.0 test whose premise changed: a plain question given a state now gets the toggle. I mutation-checked the new tests and both mutations fail them.
  • Suites: bun test packages/ui and DOM_TESTS=1 bun test --isolate packages/ui both pass, and so do bun run typecheck and smoke:package.
  • guides.show: build:viewer plus check:manifest report the manifest in sync. Viewer CSS and JS are unchanged.
  • Headless Chromium (default theme, --use-mock-keychain --password-store=basic):
    • Tab order is switch, then words. Space toggles and Enter opens with the anchor.
    • Contrast when off: 7.37:1 dark, 8.98:1 light. When on: 5.84:1 dark, 5.15:1 light.
    • The focus ring measures 7.53:1 dark and 6.53:1 light.
    • The tag has the same box as in 0.52.0.
    • Known: both targets are 17.75 px tall, which is below the 24 px target size. The approved design keeps the tag compact.

…st's decision card

- questionDecisionRecording / decisionRecording now also reach a question
  without a decision line: a boolean adds the Records a decision tag in that
  state (on shows the row), undefined leaves the question tag-less.
- New onOpenQuestionDecision (Viewer, BlockRenderer) / onOpenDecision
  (QuestionBlock): the tag splits into a diamond switch (aria-pressed) and the
  words as an aria-haspopup=dialog button that hands the host its own element
  as the popover anchor. Without it the whole tag toggles, as in 0.52.0.
- Read-only cards stay static; Plannotator passes none of this and renders
  byte-identically. New DOM test added to the DOM_TESTS step.
…h target

Review follow-up to the 0.52.1 decision toggle:

- questionDecisionScope (Viewer/BlockRenderer) / decisionScope (QuestionBlock),
  'when-answered' (default, 0.52.0) | 'any'. A boolean from
  questionDecisionRecording reaches a question with no decision line only
  under 'any', so a 0.52.0 host that returns a boolean for every question
  (documented as ignored in 0.52.0) renders byte-identically after the patch.
- The split tag now draws exactly the 0.52.0 tag (4 px between diamond and
  words, 127.66 x 17.75 px in Chromium), and the diamond switch gets an
  invisible before: hit box (the annotation panel's existing
  before:absolute before:-inset-1.5 pattern) so it clicks as 24 x 29.75 px;
  the positioned words paint over its right side. No new utilities, so the
  guides.show viewer manifest stays in sync.
- Tests: a 0.52.0-shaped host without the scope leaves plain questions alone;
  existing any-question cases opt in.
@backnotprop
backnotprop merged commit 9f18658 into main Oct 7, 2026
28 checks passed
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