@plannotator/ui 0.52.0 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HANDOFF.md CHANGED
@@ -1880,7 +1880,7 @@ Pinned by `components/DiagramBlock.askAI.test.tsx` and
1880
1880
 
1881
1881
  ---
1882
1882
 
1883
- ## Ask AI names a text selection's source lines (unreleased; additive, no core change)
1883
+ ## Ask AI names a text selection's source lines (ui 0.52.0; additive, no core change)
1884
1884
 
1885
1885
  An Ask AI question asked from a markdown text selection now says which
1886
1886
  source lines the selection is on, so a phrase that occurs more than once in
@@ -1920,7 +1920,7 @@ heading for single-block selections) and `utils/aiPrompt.test.ts`.
1920
1920
 
1921
1921
  ---
1922
1922
 
1923
- ## `CompletionOverlay` `done` state (unreleased; additive)
1923
+ ## `CompletionOverlay` `done` state (ui 0.52.0; additive)
1924
1924
 
1925
1925
  `submitted` accepts a new value, `'done'`: the session finished with nothing
1926
1926
  sent (Plannotator uses it for an annotate Done with nothing to send). It
@@ -1937,7 +1937,7 @@ so this is a visible change for `'exited'` only there. Every other value
1937
1937
  renders exactly as before. The overlay root now carries
1938
1938
  `data-completion-state={submitted}`.
1939
1939
 
1940
- ## Quick Labels: reorder and editable emoji (unreleased; additive, no core change)
1940
+ ## Quick Labels: reorder and editable emoji (ui 0.52.0; additive, no core change)
1941
1941
 
1942
1942
  Settings → Labels (#1736) lets a person move a label up or down (per-row
1943
1943
  buttons, `aria-label="Move <text> up|down"`, a polite live region announces
@@ -1970,7 +1970,7 @@ What a host should know:
1970
1970
  emoji grapheme, including ZWJ sequences, flags, keycaps and skin tones, or
1971
1971
  null) and `emojiFromFieldInput`. Nothing was removed or renamed.
1972
1972
 
1973
- ## Question decision toggle and status tag (ui 0.52.0, unreleased; additive, no core change)
1973
+ ## Question decision toggle and status tag (ui 0.52.0; additive, no core change)
1974
1974
 
1975
1975
  What Workspaces asked for: the reviewer can switch a `Decision: when answered`
1976
1976
  question's recording off and on from the card itself, and the host can draw
@@ -2029,8 +2029,92 @@ row hidden when off, recorded and plain questions untouched, read-only cards
2029
2029
  static, `statusTag: 'none'`, the `Viewer` threading, and a card with none of
2030
2030
  the props rendering the same markup.
2031
2031
 
2032
+ ## Decision toggle on any question, and an opener for the host's decision card (ui 0.52.1; additive, no core change)
2033
+
2034
+ Two gaps Workspaces found in 0.52.0 against its approved Inbox design
2035
+ ("the tag is the switch" on every question, off by default; "clicking the
2036
+ tag's words opens the decision card"). Plannotator passes none of this, and a
2037
+ card without the new props renders byte-identically to 0.52.0 (checked by
2038
+ rendering main and the branch through `BlockRenderer` and `Viewer`,
2039
+ interactive and read-only, with no props, with a 0.52.0-shaped host, and with
2040
+ a 0.52.0 host returning a boolean for every question).
2041
+
2042
+ | `Viewer` | `BlockRenderer` | `QuestionBlock` |
2043
+ | --- | --- | --- |
2044
+ | `questionDecisionScope?: 'when-answered' \| 'any'` | same | `decisionScope?: 'when-answered' \| 'any'` |
2045
+ | `onOpenQuestionDecision?: (key, anchor: HTMLElement) => void` | same | `onOpenDecision?: (key, anchor: HTMLElement) => void` |
2046
+
2047
+ 1. **Any question can record a decision, opt-in.** With
2048
+ `questionDecisionScope="any"` (`decisionScope: 'any'`),
2049
+ `decisionRecording` / `questionDecisionRecording` also reaches a question
2050
+ WITHOUT a decision line: a boolean adds the "◇ Records a decision" tag in
2051
+ that state (and, with `onToggleDecisionRecording`, makes it the toggle),
2052
+ `undefined` leaves the question as it was (no tag). The default,
2053
+ `'when-answered'`, is the 0.52.0 behaviour: only `Decision: when answered`
2054
+ questions carry the tag, and a boolean returned for a plain question is
2055
+ ignored, as 0.52.0 documented. It is an explicit opt-in rather than
2056
+ "a boolean means show it" so that a 0.52.0 host that returns a boolean for
2057
+ every question does not grow tags on its plain questions in a patch
2058
+ release. On shows the same "Answering this records
2059
+ a decision" row a `Decision: when answered` question shows; off hides it.
2060
+ The default for such a question is whatever you return: start it `false`
2061
+ for "off by default". A question with a recorded decision
2062
+ (`Decision: [statement](url)`) is still never affected, and
2063
+ `data-question-decision` still describes the question's own line (absent
2064
+ for a plain question); `data-question-decision-recording="on" | "off"`
2065
+ appears once you supply a state.
2066
+ 2. **A separate opener (`onOpenDecision` / `onOpenQuestionDecision`).** Given,
2067
+ the tag is drawn as two adjacent targets inside one visual tag
2068
+ (`[data-question-decision-tag]`), siblings, never nested:
2069
+ - the diamond is the switch: `<button aria-pressed aria-label="Record as a
2070
+ decision" data-question-decision-toggle>` calling
2071
+ `onToggleDecisionRecording(key, next)`. Without a toggle handler the
2072
+ diamond is decoration, not a control.
2073
+ - the words "Records a decision" are `<button aria-haspopup="dialog"
2074
+ data-question-decision-open>`, calling `onOpenDecision(key, element)`
2075
+ with the words' own button as the popover anchor. The card keeps no
2076
+ open state; your popover owns focus and returns it to the anchor.
2077
+ The opener is offered whether recording is on or off: what an off
2078
+ question's card does (switch it on as it opens, or open a card that says
2079
+ it is off) is the host's call. Tab order is switch, then words, both with
2080
+ the `ring-2 ring-primary` focus ring the 0.52.0 toggle uses (a ring, not
2081
+ an outline, because the dotted outline draws the off state, which is now
2082
+ on the wrapping tag). The off look is unchanged: muted-foreground text and
2083
+ a dotted outline (#1744's contrast fix). Without `onOpenDecision` the
2084
+ whole tag is the toggle exactly as in 0.52.0.
2085
+ 3. **Read-only cards are never interactive.** No answer handler (or
2086
+ `Viewer` `readOnly`, which also drops `onOpenQuestionDecision`) draws the
2087
+ tag as a plain tag in its current state.
2088
+
2089
+ Measured in headless Chromium with the default theme: switch and words are
2090
+ 7.37:1 (dark) / 8.98:1 (light) off, 5.84:1 / 5.15:1 on; the focus ring is
2091
+ 7.53:1 / 6.53:1 against the card. The split tag draws exactly the 0.52.0 tag
2092
+ (127.66 × 17.75 px, the same 4 px between diamond and words). Target size
2093
+ (WCAG 2.5.8) is met without changing that look: the diamond (18 × 17.75 px
2094
+ drawn) carries an invisible `before:` box 6 px past it on every side, so it
2095
+ clicks as 24 × 29.75 px; the words, positioned and later in the tree, paint
2096
+ over the box's right-hand part, so a click on the words is always the words,
2097
+ and the words (110 px wide) meet the spacing exception.
2098
+
2099
+ **No stylesheet change**: only utility classes the package already ships
2100
+ (the hit box is the `before:absolute before:-inset-1.5 before:content-['']`
2101
+ pattern of the annotation panel's icon buttons), so `styles.css` and the
2102
+ guides.show viewer manifest are unchanged (`check:manifest` in sync).
2103
+
2104
+ Pinned by `components/blocks/QuestionBlock.decisionAnyQuestion.test.tsx`
2105
+ (DOM-gated): the tag on a plain question only with the `'any'` scope and a
2106
+ supplied state (a boolean without the scope changes nothing), the
2107
+ toggle's arguments and the row on/off, the switch and the opener as sibling
2108
+ buttons with the anchor element, tab order, the opener without a toggle
2109
+ handler, 0.52.0 behavior without `onOpenDecision`, read-only cards, `Viewer`
2110
+ threading, and no-prop markup.
2111
+
2032
2112
  ---
2033
2113
 
2114
+ ## Stale-tab guard: `serverSession` (ui 0.52.0, core 0.25.12; additive)
2115
+
2116
+ Plannotator's servers issue one random `serverSession` nonce per process (new `@plannotator/core/server-session`) and refuse a decision that echoes a different one with `409 { code: "session_mismatch" }`, so a tab left open on a reused port cannot decide a new session. The client half is `utils/serverSession` (`adoptServerSession`, `withServerSession`, `withServerSessionQuery`, `noteServerSessionMismatch`, `useServerSessionReplaced`) plus `components/ServerSessionReplacedBanner`. Hosts that post decisions to their own backend need none of it: nothing in the shared components sends the field on its own, and a request without it is always accepted.
2117
+
2034
2118
  ## The law (guardrails for anyone editing `@plannotator/ui`)
2035
2119
 
2036
2120
  These are enforced socially and, where possible, by CI. They exist because a prior from-scratch reimplementation of this UI broke the app and was reverted.
package/README.md CHANGED
@@ -392,6 +392,13 @@ See HANDOFF.md § "Question host seams (ui 0.49.0, core 0.25.9)".
392
392
 
393
393
  See HANDOFF.md § "Question decision toggle and status tag (ui 0.52.0, unreleased; …)".
394
394
 
395
+ **Decision toggle on any question, and a decision-card opener (ui 0.52.1, unreleased).** Also optional on `Viewer`, also unused by Plannotator:
396
+
397
+ - `questionDecisionScope?: 'when-answered' | 'any'` (default `'when-answered'`, the 0.52.0 behaviour). With `'any'`, `questionDecisionRecording(question)` also applies to a question with no decision line: a boolean adds the "Records a decision" tag in that state (true also shows the "Answering this records a decision" row); `undefined` leaves it tag-less. Return `false` for "off by default".
398
+ - `onOpenQuestionDecision(key, anchor)`: splits the tag into a diamond switch (`aria-pressed`, "Record as a decision", driven by `onToggleQuestionDecisionRecording`) and the words as a button (`aria-haspopup="dialog"`) that calls this with its own element to anchor your decision card. Offered on and off; not offered under `readOnly`. Without it the whole tag is the toggle, as in 0.52.0.
399
+
400
+ See HANDOFF.md § "Decision toggle on any question, and an opener for the host's decision card (ui 0.52.1, …)".
401
+
395
402
  ### Diff file tree (`components/DiffFileTree`; next ui release)
396
403
 
397
404
  A read-only file tree for a list of changed files, to sit beside a host's own per-file diff list. It is Plannotator's code-review tree, not a lookalike: the builder (`utils/diffFileTree`), the folder open/closed hook (`hooks/useDiffFileTreeExpansion`) and the row atoms moved here from `packages/review-editor`, and the review now renders from them.
@@ -53,11 +53,15 @@ export const BlockRenderer: React.FC<{
53
53
  renderQuestionFooter?: QuestionBlockProps['renderFooter'];
54
54
  /** Whether answering this question records a decision (see `QuestionBlock`). */
55
55
  questionDecisionRecording?: QuestionBlockProps['decisionRecording'];
56
+ /** `'any'` lets a question without a decision line carry the tag (see `QuestionBlock`). */
57
+ questionDecisionScope?: QuestionBlockProps['decisionScope'];
56
58
  /** Makes the "Records a decision" tag a toggle (see `QuestionBlock`). */
57
59
  onToggleQuestionDecisionRecording?: QuestionBlockProps['onToggleDecisionRecording'];
60
+ /** Opens the host's decision card from the tag's words (see `QuestionBlock`). */
61
+ onOpenQuestionDecision?: QuestionBlockProps['onOpenDecision'];
58
62
  /** `'none'` hides the card's own status tag (see `QuestionBlock`). */
59
63
  questionStatusTag?: QuestionBlockProps['statusTag'];
60
- }> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, onToggleQuestionDecisionRecording, questionStatusTag }) => {
64
+ }> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, questionDecisionScope, onToggleQuestionDecisionRecording, onOpenQuestionDecision, questionStatusTag }) => {
61
65
  switch (block.type) {
62
66
  case 'heading': {
63
67
  const Tag = `h${block.level || 1}` as React.ElementType;
@@ -185,7 +189,9 @@ export const BlockRenderer: React.FC<{
185
189
  saveLabel={saveQuestionAnswerLabel}
186
190
  renderFooter={renderQuestionFooter}
187
191
  decisionRecording={questionDecisionRecording}
192
+ decisionScope={questionDecisionScope}
188
193
  onToggleDecisionRecording={onToggleQuestionDecisionRecording}
194
+ onOpenDecision={onOpenQuestionDecision}
189
195
  statusTag={questionStatusTag}
190
196
  onOpenLinkedDoc={onOpenLinkedDoc}
191
197
  onOpenCodeFile={onOpenCodeFile}
@@ -218,17 +218,31 @@ export interface ViewerProps {
218
218
  * decision"), given the indexed question and its saved answer. Rendered in
219
219
  * read-only cards too; return null for none. */
220
220
  renderQuestionFooter?: (question: IndexedQuestion, answer: QuestionAnswer | undefined) => React.ReactNode;
221
- /** Whether answering a `Decision: when answered` question records a
222
- * decision, per question: return false to draw its "Records a decision"
223
- * tag dimmed with a dotted outline and hide the "Answering this records a
224
- * decision" row; true or undefined keeps today's card. Not consulted for a
225
- * question whose decision is already recorded. */
221
+ /** Whether answering a question records a decision, per question. On a
222
+ * `Decision: when answered` question: false draws its "Records a
223
+ * decision" tag dimmed with a dotted outline and hides the "Answering this
224
+ * records a decision" row; true or undefined keeps today's card. On a
225
+ * question with no decision line it is ignored unless
226
+ * `questionDecisionScope` is `'any'`; then a boolean adds the tag in that
227
+ * state (true also shows the row) and undefined adds nothing. Has no
228
+ * effect on a question whose decision is already recorded. */
226
229
  questionDecisionRecording?: (question: IndexedQuestion) => boolean | undefined;
227
- /** Makes a `Decision: when answered` question's "Records a decision" tag a
228
- * toggle button (`aria-pressed`); a click calls this with the question's
229
- * key and the next state, which the host stores and returns from
230
+ /** Which questions can carry the "Records a decision" tag (ui 0.52.1).
231
+ * `'when-answered'` (default, the 0.52.0 behaviour): only questions with
232
+ * `Decision: when answered`. `'any'`: also a question with no decision
233
+ * line, when `questionDecisionRecording` returns a boolean for it. */
234
+ questionDecisionScope?: 'when-answered' | 'any';
235
+ /** Makes a question's "Records a decision" tag a toggle button
236
+ * (`aria-pressed`); a click calls this with the question's key and the
237
+ * next state, which the host stores and returns from
230
238
  * `questionDecisionRecording`. Ignored when `readOnly`. */
231
239
  onToggleQuestionDecisionRecording?: (key: string, next: boolean) => void;
240
+ /** Opens the host's decision card (ui 0.52.1): the tag splits into a
241
+ * diamond switch (the toggle above) and the words "Records a decision" as
242
+ * a button that calls this with the question's key and the words' element
243
+ * (anchor your popover to it). Offered on and off. Ignored when
244
+ * `readOnly`. */
245
+ onOpenQuestionDecision?: (key: string, anchor: HTMLElement) => void;
232
246
  /** `'none'` hides each question card's own status tag (Open / Answered /
233
247
  * Settled / Skipped) so the host can draw its own; the decision tags stay.
234
248
  * Default `'card'`. */
@@ -606,7 +620,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
606
620
  saveQuestionAnswerLabel,
607
621
  renderQuestionFooter,
608
622
  questionDecisionRecording,
623
+ questionDecisionScope,
609
624
  onToggleQuestionDecisionRecording,
625
+ onOpenQuestionDecision,
610
626
  questionStatusTag,
611
627
  onAskAI,
612
628
  allowImages = true,
@@ -1570,7 +1586,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
1570
1586
  saveQuestionAnswerLabel={saveQuestionAnswerLabel}
1571
1587
  renderQuestionFooter={renderQuestionFooter}
1572
1588
  questionDecisionRecording={question ? questionDecisionRecording?.(question) : undefined}
1589
+ questionDecisionScope={questionDecisionScope}
1573
1590
  onToggleQuestionDecisionRecording={readOnly ? undefined : onToggleQuestionDecisionRecording}
1591
+ onOpenQuestionDecision={readOnly ? undefined : onOpenQuestionDecision}
1574
1592
  questionStatusTag={questionStatusTag}
1575
1593
  />
1576
1594
  );
@@ -37,9 +37,12 @@ import { QuestionContext } from './QuestionContext';
37
37
  * A host can add its own actions at the right of the footer
38
38
  * (`renderFooter`), and a question carrying `Decision: when answered` or a
39
39
  * `Decision: [statement](url)` link shows that beside the prompt. A host can
40
- * also let the reviewer switch a `Decision: when answered` question's
41
- * recording off and on (`decisionRecording` + `onToggleDecisionRecording`),
42
- * and hide the card's own status tag to draw its own (`statusTag: 'none'`).
40
+ * also let the reviewer switch a question's decision recording off and on
41
+ * (`decisionRecording` + `onToggleDecisionRecording`; with
42
+ * `decisionScope: 'any'`, any question without a recorded decision once the
43
+ * host supplies a state), open its own decision
44
+ * card from the tag's words (`onOpenDecision`), and hide the card's own
45
+ * status tag to draw its own (`statusTag: 'none'`).
43
46
  */
44
47
 
45
48
  /** Pointer travel (px) above which a press on a choice row is a drag. */
@@ -127,17 +130,36 @@ export interface QuestionBlockProps {
127
130
  * saved answer (never the unsaved draft). Rendered in read-only cards too;
128
131
  * return null for nothing. */
129
132
  renderFooter?: (question: IndexedQuestion, answer: QuestionAnswer | undefined) => React.ReactNode;
130
- /** Whether answering a `Decision: when answered` question records a
131
- * decision (default true). False draws the "Records a decision" tag dimmed
132
- * with a dotted outline and hides the "Answering this records a decision"
133
- * row. Has no effect on a question whose decision is already recorded
134
- * (`Decision: [statement](url)`) or that carries no decision line. */
133
+ /** Whether answering this question records a decision. On a question
134
+ * carrying `Decision: when answered` it defaults to true; false draws the
135
+ * "Records a decision" tag dimmed with a dotted outline and hides the
136
+ * "Answering this records a decision" row. On a question WITHOUT a
137
+ * decision line it is ignored unless `decisionScope` is `'any'` (ui
138
+ * 0.52.1); then a defined value adds the tag in that state (false: off;
139
+ * true: on, with the row), so a host can let any answer become a
140
+ * decision, and undefined leaves the question with no tag. Has no effect
141
+ * on a question whose decision is already recorded
142
+ * (`Decision: [statement](url)`). */
135
143
  decisionRecording?: boolean;
144
+ /** Which questions can carry the "Records a decision" tag (ui 0.52.1).
145
+ * `'when-answered'` (default, the 0.52.0 behaviour): only a question that
146
+ * carries `Decision: when answered`. `'any'`: also a question with no
147
+ * decision line, once the host passes a `decisionRecording` for it. */
148
+ decisionScope?: 'when-answered' | 'any';
136
149
  /** Makes the "Records a decision" tag a toggle button (`aria-pressed`):
137
150
  * a click calls this with the question's key and the next state. The host
138
151
  * stores it and passes it back as `decisionRecording`. Not offered in a
139
152
  * read-only card (no answer handler), which draws the tag as it stands. */
140
153
  onToggleDecisionRecording?: (key: string, next: boolean) => void;
154
+ /** Opens the host's decision card (ui 0.52.1). Given, the tag splits into
155
+ * two targets inside one visual tag: the diamond is the switch
156
+ * (`onToggleDecisionRecording`, labelled "Record as a decision") and the
157
+ * words "Records a decision" are a button (`aria-haspopup="dialog"`) that
158
+ * calls this with the question's key and the words' own element, to
159
+ * anchor a popover. Offered whether recording is on or off (the host
160
+ * decides what an off question's card does). Not offered in a read-only
161
+ * card. */
162
+ onOpenDecision?: (key: string, anchor: HTMLElement) => void;
141
163
  /** `'none'` hides the card's own status tag (Open / Answered / Settled /
142
164
  * Skipped) so the host can draw its own; the decision tags stay. Default
143
165
  * `'card'`. */
@@ -161,7 +183,9 @@ export const QuestionBlock: React.FC<QuestionBlockProps> = ({
161
183
  saveLabel,
162
184
  renderFooter,
163
185
  decisionRecording,
186
+ decisionScope = 'when-answered',
164
187
  onToggleDecisionRecording,
188
+ onOpenDecision,
165
189
  statusTag = 'card',
166
190
  onOpenLinkedDoc,
167
191
  onOpenCodeFile,
@@ -355,11 +379,15 @@ export const QuestionBlock: React.FC<QuestionBlockProps> = ({
355
379
  const other = answer?.other ?? '';
356
380
  const otherOn = other.trim() !== '';
357
381
 
358
- // A `Decision: when answered` question the host may switch off. A recorded
359
- // decision (`question.decision`) is never affected.
360
- const decisionCapable = !question.decision && !!question.decisionOnAnswer;
382
+ // A `Decision: when answered` question the host may switch off, or (with
383
+ // `decisionScope: 'any'`) any other question once the host supplies a
384
+ // recording state for it. A recorded decision (`question.decision`) is
385
+ // never affected.
386
+ const decisionCapable = !question.decision
387
+ && (!!question.decisionOnAnswer || (decisionScope === 'any' && decisionRecording !== undefined));
361
388
  const recordingOn = decisionRecording ?? true;
362
389
  const decisionToggleable = decisionCapable && !readOnly && !!onToggleDecisionRecording;
390
+ const decisionOpenable = decisionCapable && !readOnly && !!onOpenDecision;
363
391
  // Present only when the host speaks to recording, so a card without these
364
392
  // props carries exactly the attributes it always did.
365
393
  const recordingAttr = decisionCapable && (decisionRecording !== undefined || !!onToggleDecisionRecording)
@@ -396,7 +424,41 @@ export const QuestionBlock: React.FC<QuestionBlockProps> = ({
396
424
  <span className="text-[10.5px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">{eyebrow}</span>
397
425
  <span className="ml-auto">
398
426
  {question.decision && <><Tag tone="rec"><DiamondGlyph />Decision</Tag>{' '}</>}
399
- {decisionCapable && (decisionToggleable ? (
427
+ {decisionCapable && (decisionOpenable ? (
428
+ // Two targets in one tag: the diamond switches recording, the
429
+ // words open the host's decision card. Siblings, never nested.
430
+ // Target size (WCAG 2.5.8) without changing the drawn tag: the
431
+ // diamond's invisible `before:` box reaches 6 px past it on every
432
+ // side (24 px wide, about 30 px tall), and the words, positioned
433
+ // and later in the tree, paint over its right-hand part, so a
434
+ // click on the words is always the words. The words (110 px wide)
435
+ // meet the spacing exception: nothing else to click within 12 px
436
+ // of their centre line.
437
+ <><span
438
+ className="annotation-exclude select-none inline-flex items-stretch whitespace-nowrap rounded align-[1px] text-[10.5px] font-semibold tracking-[0.03em] bg-primary/15 text-primary"
439
+ style={recordingOn ? undefined : DECISION_OFF_STYLE}
440
+ data-pinpoint-ignore=""
441
+ data-question-decision-tag=""
442
+ >{decisionToggleable ? (
443
+ <button
444
+ type="button"
445
+ aria-pressed={recordingOn}
446
+ aria-label="Record as a decision"
447
+ title="Record as a decision"
448
+ onClick={() => onToggleDecisionRecording!(question.key, !recordingOn)}
449
+ className="relative inline-flex cursor-pointer items-center rounded pl-1.5 pr-0.5 before:absolute before:-inset-1.5 before:content-[''] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
450
+ data-question-decision-toggle=""
451
+ ><DiamondGlyph /></button>
452
+ ) : (
453
+ <span className="inline-flex items-center pl-1.5 pr-0.5"><DiamondGlyph /></span>
454
+ )}<button
455
+ type="button"
456
+ aria-haspopup="dialog"
457
+ onClick={(event) => onOpenDecision!(question.key, event.currentTarget)}
458
+ className="relative cursor-pointer rounded py-px pl-0.5 pr-1.5 hover:underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
459
+ data-question-decision-open=""
460
+ >Records a decision</button></span>{' '}</>
461
+ ) : decisionToggleable ? (
400
462
  // Focus shows as a ring, not an outline: the dotted outline is
401
463
  // what draws the off state.
402
464
  <><button
@@ -427,7 +489,7 @@ export const QuestionBlock: React.FC<QuestionBlockProps> = ({
427
489
  <p id={promptId} className="m-0 text-[15px] font-semibold leading-[1.45] text-foreground" data-question-part="prompt">
428
490
  {inline(question.prompt)}
429
491
  </p>
430
- {(question.decision || (question.decisionOnAnswer && recordingOn)) && (
492
+ {(question.decision || (decisionCapable && recordingOn)) && (
431
493
  <div
432
494
  className="annotation-exclude mt-2 mb-0.5 flex items-center gap-2 rounded-lg bg-primary/8 px-[9px] py-1.5 text-[12.5px] leading-[18px] text-foreground [&>svg]:text-primary"
433
495
  data-pinpoint-ignore=""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.52.0",
3
+ "version": "0.52.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {