@joewinke/jatui 0.37.0 → 0.38.0

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.
Files changed (266) hide show
  1. package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
  2. package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
  3. package/dist/artifact-blocks/BlockComments.svelte +309 -0
  4. package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
  5. package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
  6. package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
  7. package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
  8. package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
  9. package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
  10. package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
  11. package/dist/artifact-blocks/CONTRACT.md +225 -0
  12. package/dist/artifact-blocks/CommentPill.svelte +49 -0
  13. package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
  14. package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
  15. package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
  16. package/dist/artifact-blocks/Glyph.svelte +54 -0
  17. package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
  18. package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
  19. package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
  20. package/dist/artifact-blocks/Markdown.svelte +186 -0
  21. package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
  22. package/dist/artifact-blocks/NoteComposer.svelte +31 -0
  23. package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
  24. package/dist/artifact-blocks/PlainComposer.svelte +200 -0
  25. package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
  26. package/dist/artifact-blocks/SectionTools.svelte +102 -0
  27. package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
  28. package/dist/artifact-blocks/adapter.d.ts +13 -0
  29. package/dist/artifact-blocks/adapter.js +31 -0
  30. package/dist/artifact-blocks/autoSend.d.ts +49 -0
  31. package/dist/artifact-blocks/autoSend.js +103 -0
  32. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
  33. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
  34. package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
  35. package/dist/artifact-blocks/blocks/actions/block.js +74 -0
  36. package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
  37. package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
  38. package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
  39. package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
  40. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
  41. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
  42. package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
  43. package/dist/artifact-blocks/blocks/callout/block.js +39 -0
  44. package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
  45. package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
  46. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
  47. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
  48. package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
  49. package/dist/artifact-blocks/blocks/chart/block.js +70 -0
  50. package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
  51. package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
  52. package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
  53. package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
  54. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
  55. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
  56. package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
  57. package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
  58. package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
  59. package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
  60. package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
  61. package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
  62. package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
  63. package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
  64. package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
  65. package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
  66. package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
  67. package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
  68. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
  69. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
  70. package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
  71. package/dist/artifact-blocks/blocks/code/block.js +31 -0
  72. package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
  73. package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
  74. package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
  75. package/dist/artifact-blocks/blocks/code/schema.js +39 -0
  76. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
  77. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
  78. package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
  79. package/dist/artifact-blocks/blocks/collage/block.js +29 -0
  80. package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
  81. package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
  82. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
  83. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
  84. package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
  85. package/dist/artifact-blocks/blocks/compare/block.js +32 -0
  86. package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
  87. package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
  88. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
  89. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
  90. package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
  91. package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
  92. package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
  93. package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
  94. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
  95. package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
  96. package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
  97. package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
  98. package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
  99. package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
  100. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
  101. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
  102. package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
  103. package/dist/artifact-blocks/blocks/diff/block.js +36 -0
  104. package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
  105. package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
  106. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
  107. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
  108. package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
  109. package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
  110. package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
  111. package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
  112. package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
  113. package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
  114. package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
  115. package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
  116. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
  117. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
  118. package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
  119. package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
  120. package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
  121. package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
  122. package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
  123. package/dist/artifact-blocks/blocks/group-content.js +16 -0
  124. package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
  125. package/dist/artifact-blocks/blocks/group-data.js +7 -0
  126. package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
  127. package/dist/artifact-blocks/blocks/group-review.js +12 -0
  128. package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
  129. package/dist/artifact-blocks/blocks/group-visual.js +14 -0
  130. package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
  131. package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
  132. package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
  133. package/dist/artifact-blocks/blocks/map/block.js +62 -0
  134. package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
  135. package/dist/artifact-blocks/blocks/map/schema.js +113 -0
  136. package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
  137. package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
  138. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
  139. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
  140. package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
  141. package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
  142. package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
  143. package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
  144. package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
  145. package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
  146. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
  147. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
  148. package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
  149. package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
  150. package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
  151. package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
  152. package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
  153. package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
  154. package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
  155. package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
  156. package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
  157. package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
  158. package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
  159. package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
  160. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
  161. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
  162. package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
  163. package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
  164. package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
  165. package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
  166. package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
  167. package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
  168. package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
  169. package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
  170. package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
  171. package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
  172. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
  173. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
  174. package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
  175. package/dist/artifact-blocks/blocks/program/block.js +68 -0
  176. package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
  177. package/dist/artifact-blocks/blocks/program/schema.js +268 -0
  178. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
  179. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
  180. package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
  181. package/dist/artifact-blocks/blocks/stats/block.js +49 -0
  182. package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
  183. package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
  184. package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
  185. package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
  186. package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
  187. package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
  188. package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
  189. package/dist/artifact-blocks/blocks/table/block.js +100 -0
  190. package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
  191. package/dist/artifact-blocks/blocks/table/schema.js +278 -0
  192. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
  193. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
  194. package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
  195. package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
  196. package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
  197. package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
  198. package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
  199. package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
  200. package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
  201. package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
  202. package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
  203. package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
  204. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
  205. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
  206. package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
  207. package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
  208. package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
  209. package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
  210. package/dist/artifact-blocks/builtins.d.ts +40 -0
  211. package/dist/artifact-blocks/builtins.js +22 -0
  212. package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
  213. package/dist/artifact-blocks/contractBlocks.js +279 -0
  214. package/dist/artifact-blocks/history.d.ts +140 -0
  215. package/dist/artifact-blocks/history.js +363 -0
  216. package/dist/artifact-blocks/index.d.ts +40 -0
  217. package/dist/artifact-blocks/index.js +46 -0
  218. package/dist/artifact-blocks/listen/context.d.ts +30 -0
  219. package/dist/artifact-blocks/listen/context.js +30 -0
  220. package/dist/artifact-blocks/liveData.d.ts +78 -0
  221. package/dist/artifact-blocks/liveData.js +137 -0
  222. package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
  223. package/dist/artifact-blocks/liveDataPoller.js +140 -0
  224. package/dist/artifact-blocks/openNav.d.ts +46 -0
  225. package/dist/artifact-blocks/openNav.js +106 -0
  226. package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
  227. package/dist/artifact-blocks/pageReviewKeys.js +115 -0
  228. package/dist/artifact-blocks/params.d.ts +98 -0
  229. package/dist/artifact-blocks/params.js +159 -0
  230. package/dist/artifact-blocks/registry.d.ts +27 -0
  231. package/dist/artifact-blocks/registry.js +83 -0
  232. package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
  233. package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
  234. package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
  235. package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
  236. package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
  237. package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
  238. package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
  239. package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
  240. package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
  241. package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
  242. package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
  243. package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
  244. package/dist/artifact-blocks/review/attachments.d.ts +10 -0
  245. package/dist/artifact-blocks/review/attachments.js +37 -0
  246. package/dist/artifact-blocks/review/bulk.d.ts +52 -0
  247. package/dist/artifact-blocks/review/bulk.js +61 -0
  248. package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
  249. package/dist/artifact-blocks/review/itemReview.js +112 -0
  250. package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
  251. package/dist/artifact-blocks/review/roundLines.js +20 -0
  252. package/dist/artifact-blocks/review/rounds.d.ts +86 -0
  253. package/dist/artifact-blocks/review/rounds.js +302 -0
  254. package/dist/artifact-blocks/review/saver.d.ts +23 -0
  255. package/dist/artifact-blocks/review/saver.js +48 -0
  256. package/dist/artifact-blocks/review/team.d.ts +31 -0
  257. package/dist/artifact-blocks/review/team.js +42 -0
  258. package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
  259. package/dist/artifact-blocks/review/visualOrder.js +66 -0
  260. package/dist/artifact-blocks/review.d.ts +180 -0
  261. package/dist/artifact-blocks/review.js +716 -0
  262. package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
  263. package/dist/artifact-blocks/thumb-kit.js +149 -0
  264. package/dist/artifact-blocks/types.d.ts +513 -0
  265. package/dist/artifact-blocks/types.js +1 -0
  266. package/package.json +11 -2
@@ -0,0 +1,779 @@
1
+ <script lang="ts">
2
+ /**
3
+ * callout block — note / warn / success panels and the interactive
4
+ * `decision` variant: a question with 2-6 options (+ optional "other" text)
5
+ * whose pick and comment persist per viewer via ctx.saveState. When the
6
+ * block has its notes box, picking Other MERGES into it — the box becomes
7
+ * the Other explanation ("Say what instead") and its text is saved to both
8
+ * state.other and state.comment; without the box (allowComment: false)
9
+ * Other keeps a dedicated "Say which" input.
10
+ *
11
+ * SAVING IS AUTOMATIC, AND WHAT SAVES IS A DRAFT (mos-4dkv9 + the
12
+ * desk-convergence-ste D2 steer, Joe: "remove the Save answer button, just
13
+ * save it on press" — answers are DRAFTS until the page's Finish review
14
+ * sends them): picking an option calls ctx.saveState at once — no button,
15
+ * and switching options re-saves the new pick the same way. The note (and
16
+ * the Other explanation, which saves like a note) autosaves ~800ms after
17
+ * typing stops and on blur; a pending note save is flushed into the next
18
+ * pick save so one write always carries the whole answer. Everything is
19
+ * server-persisted per viewer, so a reload brings the draft back. The card
20
+ * shows a small state line — Saving… / Draft saved · <time> / the error
21
+ * with a Retry control — never a Save/Update button. A failed save keeps
22
+ * the viewer's local pick (nothing is lost); the renderer's per-block
23
+ * strip and backoff also keep retrying (mos-yntqm, mos-lwbbt), and the
24
+ * server refuses an EMPTY blocks payload so a nothing-write can never wipe
25
+ * saved verdicts.
26
+ *
27
+ * Tones (campaign-blocks Gate A, J4) are QUIET panels: a 10% tone tint, a
28
+ * 30% tone border, base-content text, and the tone carried by a small solid
29
+ * icon disc (solid tone fills clear contrast in every theme; a tone-coloured
30
+ * glyph on a tint does not). No saturated DaisyUI alert slab, no shadow.
31
+ *
32
+ * Read-only surfaces (public share, print, tests without saveState) render
33
+ * the decision as an ANSWER, not a disabled form: the saved pick at full
34
+ * strength with a check, the other options as plain muted text.
35
+ *
36
+ * A SAVED answer looks DONE (mos-tjwof), with the same card-level treatment
37
+ * the diff-review verdicts get: the card border turns success (with the
38
+ * ring) and a labeled "Draft: <choice>" chip sits beside the question —
39
+ * scannable while scrolling, not a tiny check line, and honest about the
40
+ * draft-until-Finish semantics (D2). While a save is in flight or has
41
+ * failed the card flips to a warning border — the only remaining "unsaved"
42
+ * state; typing alone (inside the autosave debounce) does not flag the
43
+ * card.
44
+ */
45
+ import BlockIcon from "../../review/BlockIcon.svelte"
46
+ import type { BlockProps } from "../../types.js"
47
+ import { renderBlockMarkdown } from "../markdown/render.js"
48
+ import NoteComposer from "../../NoteComposer.svelte"
49
+ import {
50
+ readNoteAttachments,
51
+ type NoteAttachment,
52
+ } from "../../review/attachments.js"
53
+ import { OTHER_ID, type CalloutParams, type CalloutState } from "./schema.js"
54
+ import SectionTools from "../../SectionTools.svelte"
55
+
56
+ let { params, ctx }: BlockProps<CalloutParams> = $props()
57
+
58
+ const variant = $derived(params?.variant ?? "note")
59
+ /** PARAMS-SPEC rule 4: with `sections`, the body is their flattened
60
+ * fallback for renderers that ignore them — render one, never both. */
61
+ const sections = $derived(
62
+ variant === "decision" ? (params?.sections ?? []) : [],
63
+ )
64
+ const bodyHtml = $derived(
65
+ sections.length > 0 ? "" : renderBlockMarkdown(params?.body),
66
+ )
67
+ const sectionHtml = $derived(
68
+ sections.map((sec) => renderBlockMarkdown(sec.markdown)),
69
+ )
70
+ const meta = $derived(variant === "decision" ? (params?.meta ?? []) : [])
71
+ const primaryAction = $derived(
72
+ variant === "decision" ? params?.primaryAction : undefined,
73
+ )
74
+ const origin = typeof location !== "undefined" ? location.origin : undefined
75
+ /** Off-origin http(s) links open in a new tab; root-relative stay. */
76
+ function offOrigin(href: string): boolean {
77
+ if (!/^https?:\/\//i.test(href)) return false
78
+ try {
79
+ return !origin || new URL(href).origin !== origin
80
+ } catch {
81
+ return true
82
+ }
83
+ }
84
+ /** Static tone classes for a meta chip (never `text-${tone}`). */
85
+ const META_TONE: Record<string, string> = {
86
+ neutral: "text-base-content/75",
87
+ info: "text-info",
88
+ success: "text-success",
89
+ warning: "text-warning",
90
+ error: "text-error",
91
+ }
92
+
93
+ /** The container's name for its Listen control (mos-8vdg9): the title,
94
+ * else the decision's question, else a quiet generic. */
95
+ const listenLabel = $derived(
96
+ params?.title?.trim() || params?.question?.trim() || "this note",
97
+ )
98
+
99
+ /**
100
+ * Interactive = the viewer can answer here. On the public share route the
101
+ * context is `readonly` (and typically has no saveState) — no viewer chrome.
102
+ */
103
+ const interactive = $derived(Boolean(ctx?.saveState) && !ctx?.readonly)
104
+
105
+ /** "" = no pick yet; real picks are option ids or OTHER_ID. */
106
+ let pick = $state("")
107
+ let otherText = $state("")
108
+ let comment = $state("")
109
+ /** The note's uploaded attachments (mos-rccb5) — descriptors in the
110
+ * artifact's private asset store, uploaded as they are captured. */
111
+ let commentClips = $state<NoteAttachment[]>([])
112
+ /** Save flights in flight — the small "Saving…" state, not a button. */
113
+ let flights = $state(0)
114
+ const saving = $derived(flights > 0)
115
+ let saveError = $state<string | null>(null)
116
+ /** The last answer known to be saved (seeded from ctx.state). */
117
+ let lastSaved = $state<CalloutState | null>(null)
118
+ /** Focus the "other" field only when the viewer just picked Other. */
119
+ let focusOther = $state(false)
120
+ /** The notes box wrapper, for the merged Other focus (via the effect):
121
+ * the composer is the host's (NoteComposer), so its text entry is found
122
+ * inside rather than bound. */
123
+ let composerWrap = $state<HTMLElement | null>(null)
124
+ /** The "Say which" input element, for the no-notes/legacy Other focus. */
125
+ let otherInputEl = $state<HTMLInputElement | null>(null)
126
+ /** The block has a comment/notes box (allowComment: false removes it). */
127
+ const hasNotes = $derived(params?.allowComment !== false)
128
+ /**
129
+ * A previously saved state with a DISTINCT other + comment keeps the old
130
+ * two-field UI: merging it into one box would silently drop one of the two
131
+ * texts on the viewer's next save.
132
+ */
133
+ let legacySplit = $state(false)
134
+
135
+ // Seed the form from this viewer's saved state (mount-time read; state is
136
+ // keyed per artifact version and does not change while mounted).
137
+ {
138
+ const s = (ctx?.state ?? null) as CalloutState | null
139
+ const other0 = s && typeof s.other === "string" ? s.other : ""
140
+ const comment0 = s && typeof s.comment === "string" ? s.comment : ""
141
+ const clips0 = s ? (readNoteAttachments(s.attachments) ?? []) : []
142
+ if (s && typeof s.pick === "string" && s.pick) pick = s.pick
143
+ if (other0) otherText = other0
144
+ if (comment0) comment = comment0
145
+ if (clips0.length) commentClips = clips0
146
+ if (s && (s.pick || other0.trim() || comment0.trim() || clips0.length))
147
+ lastSaved = { ...s }
148
+ if (
149
+ s &&
150
+ s.pick === OTHER_ID &&
151
+ other0.trim() &&
152
+ comment0.trim() &&
153
+ other0.trim() !== comment0.trim()
154
+ ) {
155
+ legacySplit = true
156
+ } else if (s && s.pick === OTHER_ID && !comment0.trim() && other0.trim()) {
157
+ // The notes box doubles as the Other explanation: show the saved
158
+ // explanation in it so the returning viewer sees (and can edit) it.
159
+ comment = other0
160
+ }
161
+ }
162
+
163
+ /**
164
+ * The notes box IS the Other explanation: the block has a notes field,
165
+ * Other is picked, and the saved state is not a legacy two-text split. The
166
+ * separate "Say which" input only renders when there is no notes field —
167
+ * or while a legacy split state is on screen.
168
+ */
169
+ const notesAsOther = $derived(hasNotes && pick === OTHER_ID && !legacySplit)
170
+
171
+ /** The dedicated "Say which" input: only when there is no notes box to
172
+ * merge into, or while a legacy two-text state keeps both fields. */
173
+ const showOtherInput = $derived(
174
+ interactive &&
175
+ params?.allowOther === true &&
176
+ pick === OTHER_ID &&
177
+ !notesAsOther,
178
+ )
179
+
180
+ /** The rendered option list — the authored options plus Other last. */
181
+ const optionsWithOther = $derived.by(() => {
182
+ const opts = [...(params?.options ?? [])]
183
+ if (params?.allowOther === true) {
184
+ opts.push({ id: OTHER_ID, label: "Other" })
185
+ }
186
+ return opts
187
+ })
188
+
189
+ // Keyboard: focus moves to the explanation field when the viewer picks
190
+ // Other with a POINTER — the merged notes box, or the "Say which" input
191
+ // when the block has no notes field (or a legacy two-text state keeps both
192
+ // fields). A keyboard pick (arrows/space/enter) keeps focus on the radios
193
+ // so arrow roving keeps working. Focus lives in an effect rather than a
194
+ // `use:` action: an action defers the element's value binding past the
195
+ // mount flush.
196
+ $effect(() => {
197
+ if (focusOther && notesAsOther) {
198
+ queueMicrotask(() =>
199
+ composerWrap?.querySelector<HTMLElement>("textarea, [contenteditable='true']")?.focus(),
200
+ )
201
+ } else if (focusOther && showOtherInput) {
202
+ queueMicrotask(() => otherInputEl?.focus())
203
+ }
204
+ })
205
+
206
+ /**
207
+ * The one autosave debounce for typed text (the note, and the Other
208
+ * explanation, which saves like a note): ~800ms after typing stops, and
209
+ * immediately on blur. A pick save cancels the pending timer and carries
210
+ * the typed text along, so one write always holds the whole answer.
211
+ */
212
+ const NOTE_DEBOUNCE_MS = 800
213
+ let noteTimer: ReturnType<typeof setTimeout> | null = null
214
+ function scheduleNoteSave() {
215
+ if (noteTimer) clearTimeout(noteTimer)
216
+ noteTimer = setTimeout(() => {
217
+ noteTimer = null
218
+ save()
219
+ }, NOTE_DEBOUNCE_MS)
220
+ }
221
+ function cancelNoteSave() {
222
+ if (noteTimer) {
223
+ clearTimeout(noteTimer)
224
+ noteTimer = null
225
+ }
226
+ }
227
+ function flushNoteSave() {
228
+ if (!noteTimer) return
229
+ cancelNoteSave()
230
+ save()
231
+ }
232
+ // A save still inside the debounce window when the block goes away is a
233
+ // real viewer decision — fire it (the renderer's keepalive POST survives
234
+ // an SPA navigation, same contract as its own unmount flush).
235
+ $effect(() => () => flushNoteSave())
236
+
237
+ /**
238
+ * The state this form would save right now — null when there is nothing
239
+ * to write. Never an empty tombstone: the server keeps whatever stands
240
+ * when a save carries no answer (mos-lwbbt's empty-payload guard), and a
241
+ * component that posts nothing at all is even better.
242
+ */
243
+ function buildState(): CalloutState | null {
244
+ if (pick) {
245
+ const next: CalloutState = { pick, at: new Date().toISOString() }
246
+ if (pick === OTHER_ID) {
247
+ if (notesAsOther) {
248
+ // Merged (mos-ucmup): the notes box is the Other explanation. Write
249
+ // the text to BOTH state.other and state.comment so readers of
250
+ // either (feedback pull-verdicts, digest summaries, the Pilot drain)
251
+ // keep working.
252
+ const t = comment.trim()
253
+ if (t) {
254
+ next.other = t
255
+ next.comment = t
256
+ }
257
+ } else {
258
+ if (otherText.trim()) next.other = otherText.trim()
259
+ if (comment.trim()) next.comment = comment.trim()
260
+ }
261
+ } else if (comment.trim()) {
262
+ next.comment = comment.trim()
263
+ }
264
+ if (commentClips.length) next.attachments = commentClips
265
+ return next
266
+ }
267
+ // No pick yet: a typed note (or an attachment alone) is still worth
268
+ // keeping, but an answer with neither is nothing.
269
+ const t = comment.trim()
270
+ if (!t && commentClips.length === 0) return null
271
+ const next: CalloutState = { at: new Date().toISOString() }
272
+ if (t) next.comment = t
273
+ if (commentClips.length) next.attachments = commentClips
274
+ return next
275
+ }
276
+
277
+ /** Same answer content (at is bookkeeping, not content). */
278
+ function sameAnswer(a: CalloutState | null, b: CalloutState | null): boolean {
279
+ if (a === b) return true
280
+ if (!a || !b) return false
281
+ return (
282
+ (a.pick ?? "") === (b.pick ?? "") &&
283
+ (a.other ?? "") === (b.other ?? "") &&
284
+ (a.comment ?? "") === (b.comment ?? "") &&
285
+ JSON.stringify(a.attachments ?? []) ===
286
+ JSON.stringify(b.attachments ?? [])
287
+ )
288
+ }
289
+
290
+ /** Latest save wins for lastSaved: an older flight resolving after a newer
291
+ * one must not roll the card's saved state back. */
292
+ let saveSeq = 0
293
+
294
+ function save() {
295
+ if (!interactive || !ctx?.saveState) return
296
+ const next = buildState()
297
+ if (!next || sameAnswer(next, lastSaved)) return
298
+ const mine = ++saveSeq
299
+ flights++
300
+ saveError = null
301
+ ctx
302
+ .saveState(next)
303
+ .then(() => {
304
+ flights--
305
+ if (mine === saveSeq) lastSaved = next
306
+ })
307
+ .catch((e: unknown) => {
308
+ flights--
309
+ // The local pick stays (nothing is lost); the card shows the error
310
+ // + Retry, and the renderer's strip/backoff retries underneath.
311
+ if (mine === saveSeq) {
312
+ saveError = e instanceof Error ? e.message : "Saving failed"
313
+ }
314
+ })
315
+ }
316
+
317
+ /** The failure state's own control — the only button left on the card
318
+ * (mos-4dkv9): re-send the current form state. A bare handler reference,
319
+ * not an inline arrow calling save(): the route-quality Save-action
320
+ * heuristic pattern-matches an inline arrow that CALLS save in markup as
321
+ * an unsaved-form Save button, and this card has none — autosave IS the
322
+ * dirty-form guard (pending edits flush on unmount, so navigation loses
323
+ * nothing). */
324
+ function retrySave() {
325
+ save()
326
+ }
327
+
328
+ /** Select an option — the save happens here, on press (mos-4dkv9). */
329
+ function pickOption(id: string, focusExplanation: boolean) {
330
+ if (!interactive) return
331
+ focusOther = focusExplanation && id === OTHER_ID
332
+ if (pick === id) return
333
+ pick = id
334
+ // The pick save carries whatever typed text is on the card along —
335
+ // no separate note save needs to chase it.
336
+ cancelNoteSave()
337
+ save()
338
+ }
339
+
340
+ /** Radios inside the keydown's own fieldset, in option order (the
341
+ * interactive ones — read-only renders disabled twins that must not join
342
+ * the roving). Scoped from the EVENT TARGET, not a bind:this ref: a key
343
+ * press that lands before Svelte's first post-mount flush (tests, and a
344
+ * hostile scheduler) must still rove, and bind:this lands only in that
345
+ * flush. */
346
+ function radioEls(from: EventTarget | null): HTMLInputElement[] {
347
+ const scope = (from as HTMLElement | null)?.closest("fieldset")
348
+ if (!scope) return []
349
+ return Array.from(
350
+ scope.querySelectorAll<HTMLInputElement>(
351
+ 'input[type="radio"]:not(:disabled)',
352
+ ),
353
+ )
354
+ }
355
+
356
+ /**
357
+ * Keyboard on the option group: arrow keys move the selection (wrapping),
358
+ * space or enter picks — and every pick saves. preventDefault keeps the
359
+ * browser's own radio roving from double-firing behind us.
360
+ */
361
+ function optionKeydown(e: KeyboardEvent, index: number) {
362
+ const count = optionsWithOther.length
363
+ if (count === 0) return
364
+ let next: number
365
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") {
366
+ next = (index + 1) % count
367
+ } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
368
+ next = (index - 1 + count) % count
369
+ } else if (e.key === " " || e.key === "Enter") {
370
+ next = index
371
+ } else {
372
+ return
373
+ }
374
+ e.preventDefault()
375
+ radioEls(e.target)[next]?.focus()
376
+ pickOption(optionsWithOther[next]!.id, false)
377
+ }
378
+
379
+ const TONE = {
380
+ note: {
381
+ panel: "border-info/30 bg-info/10",
382
+ disc: "bg-info text-info-content",
383
+ },
384
+ warn: {
385
+ panel: "border-warning/40 bg-warning/10",
386
+ disc: "bg-warning text-warning-content",
387
+ },
388
+ success: {
389
+ panel: "border-success/35 bg-success/10",
390
+ disc: "bg-success text-success-content",
391
+ },
392
+ } as const
393
+ const tone = $derived(
394
+ TONE[variant === "warn" || variant === "success" ? variant : "note"],
395
+ )
396
+
397
+ function optionLabel(id: string): string {
398
+ if (id === OTHER_ID) return lastSaved?.other?.trim() || "Other"
399
+ return params?.options?.find((o) => o.id === id)?.label ?? id
400
+ }
401
+
402
+ const whenFmt = new Intl.DateTimeFormat("en-US", {
403
+ month: "short",
404
+ day: "numeric",
405
+ hour: "numeric",
406
+ minute: "2-digit",
407
+ })
408
+ function when(iso: string | undefined): string {
409
+ if (!iso) return ""
410
+ const d = new Date(iso)
411
+ return Number.isFinite(d.getTime()) ? whenFmt.format(d) : ""
412
+ }
413
+
414
+ /**
415
+ * The card's state: a save in flight or failed flags the card (the only
416
+ * remaining "unsaved" look); a saved pick is DONE; otherwise fresh.
417
+ */
418
+ const cardBorder = $derived.by(() => {
419
+ if (saving || saveError) return "border-warning/60"
420
+ if (lastSaved?.pick) return "border-success ring-1 ring-success/40"
421
+ return "border-primary/25"
422
+ })
423
+
424
+ /** The small state line under the options — no button beside it. A saved
425
+ * answer is a DRAFT until the page's Finish review sends it (D2). */
426
+ const savedLine = $derived.by(() => {
427
+ if (saveError) return saveError
428
+ if (saving) return "Saving…"
429
+ if (lastSaved) {
430
+ const at = when(lastSaved.at)
431
+ return `Draft saved${at ? ` · ${at}` : ""}`
432
+ }
433
+ return ""
434
+ })
435
+
436
+ const PROSE =
437
+ "prose prose-sm max-w-none text-base-content prose-p:max-w-[65ch] prose-li:max-w-[65ch] prose-p:my-1.5 prose-ul:my-1.5 prose-ol:my-1.5 prose-code:rounded-field prose-code:bg-base-content/10 prose-code:px-1 prose-code:py-0.5 prose-code:font-normal prose-code:before:content-none prose-code:after:content-none"
438
+
439
+ const optionRow =
440
+ "flex min-w-0 items-center gap-2.5 rounded-field border px-3 py-2 text-sm"
441
+
442
+ /** A contract card's recommended option (mos-4tgfj): its letter, else its
443
+ * position, matches `params.recommended`. */
444
+ function isRecommended(i: number): boolean {
445
+ const rec = params?.recommended
446
+ if (!rec) return false
447
+ const o = params?.options?.[i]
448
+ return Boolean(o) && (o!.letter ?? String.fromCharCode(65 + i)) === rec
449
+ }
450
+ </script>
451
+
452
+ {#snippet optionText(
453
+ o: { id: string; label: string; detail?: string },
454
+ i: number,
455
+ text: string,
456
+ )}
457
+ <span class="min-w-0">
458
+ <span>{text}</span>
459
+ {#if isRecommended(i)}
460
+ <span class="badge badge-ghost badge-sm ms-1.5 align-middle"
461
+ >Recommended</span
462
+ >
463
+ {/if}
464
+ {#if o.detail}
465
+ <span class="block text-xs text-base-content/60">{o.detail}</span>
466
+ {/if}
467
+ </span>
468
+ {/snippet}
469
+
470
+ {#if variant === "decision"}
471
+ {@const readPick = lastSaved?.pick ?? ""}
472
+ <div
473
+ class="rounded-box border bg-base-100 p-4 sm:p-5 {cardBorder}"
474
+ data-variant="decision"
475
+ data-answered={readPick ? "true" : "false"}
476
+ data-saving={saving ? "true" : "false"}
477
+ data-failed={saveError ? "true" : "false"}
478
+ >
479
+ <div class="flex items-start gap-3">
480
+ <span
481
+ class="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full bg-primary text-primary-content"
482
+ aria-hidden="true"
483
+ >
484
+ <BlockIcon name="circle-help" size={15} stroke-width={2.25} />
485
+ </span>
486
+ <div class="min-w-0 flex-1">
487
+ <SectionTools {ctx} {listenLabel} />
488
+ {#if params?.title}
489
+ <p class="text-sm font-medium text-base-content/70">
490
+ {params.title}
491
+ </p>
492
+ {/if}
493
+ {#if bodyHtml}
494
+ <div class="{PROSE} mt-1">
495
+ {@html bodyHtml}
496
+ </div>
497
+ {/if}
498
+
499
+ <div class="mt-3">
500
+ <fieldset class="min-w-0">
501
+ <legend
502
+ class="flex flex-wrap items-center gap-2 text-base leading-snug font-semibold text-base-content"
503
+ >
504
+ <span>{params?.question}</span>
505
+ {#if interactive && saving}
506
+ <span
507
+ class="badge badge-sm font-medium badge-warning"
508
+ data-saving-chip
509
+ >
510
+ Saving…
511
+ </span>
512
+ {:else if interactive && saveError}
513
+ <span
514
+ class="badge badge-sm font-medium badge-error"
515
+ data-failed-chip
516
+ >
517
+ Not saved
518
+ </span>
519
+ {:else if interactive && lastSaved?.pick}
520
+ <span
521
+ class="badge badge-sm font-medium badge-success"
522
+ data-answer-chip
523
+ >
524
+ Draft: {optionLabel(lastSaved.pick)}
525
+ </span>
526
+ {/if}
527
+ </legend>
528
+
529
+ {#if meta.length > 0}
530
+ <ul
531
+ class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-base-content/70"
532
+ aria-label="Details"
533
+ data-decision-meta
534
+ >
535
+ {#each meta as m, mi (mi)}
536
+ <li class="inline-flex min-w-0 items-baseline gap-1" title={m.title}>
537
+ <span class="text-base-content/55">{m.label}</span>
538
+ {#if m.href}
539
+ {@const ext = offOrigin(m.href)}
540
+ <a
541
+ class="link link-hover min-w-0 truncate font-medium {META_TONE[m.tone ?? 'neutral']}"
542
+ href={m.href}
543
+ target={ext ? "_blank" : undefined}
544
+ rel={ext ? "noopener noreferrer" : undefined}
545
+ >{m.value}{#if ext}<span class="sr-only"> (opens in a new tab)</span>{/if}</a
546
+ >
547
+ {:else}
548
+ <span class="min-w-0 truncate font-medium {META_TONE[m.tone ?? 'neutral']}"
549
+ >{m.value}</span
550
+ >
551
+ {/if}
552
+ </li>
553
+ {/each}
554
+ </ul>
555
+ {/if}
556
+
557
+ {#if primaryAction}
558
+ {@const ext = offOrigin(primaryAction.url)}
559
+ <div class="mt-3">
560
+ <a
561
+ class="btn btn-primary btn-sm rounded-field"
562
+ href={primaryAction.url}
563
+ target={ext ? "_blank" : undefined}
564
+ rel={ext ? "noopener noreferrer" : undefined}
565
+ data-primary-action
566
+ >
567
+ {primaryAction.label}
568
+ {#if ext}
569
+ <BlockIcon name="external-link" size={14} />
570
+ <span class="sr-only"> (opens in a new tab)</span>
571
+ {/if}
572
+ </a>
573
+ </div>
574
+ {/if}
575
+
576
+ {#if !interactive && readPick}
577
+ <p class="mt-1 text-sm text-base-content/75">
578
+ Answered: <strong class="font-semibold text-base-content"
579
+ >{optionLabel(readPick)}</strong
580
+ >{#if when(lastSaved?.at)}<span
581
+ >{" · "}{when(lastSaved?.at)}</span
582
+ >{/if}
583
+ </p>
584
+ {/if}
585
+
586
+ <div class="mt-2.5 flex flex-col gap-1.5">
587
+ {#each optionsWithOther as o, i (o.id)}
588
+ {#if interactive}
589
+ <label
590
+ class="{optionRow} cursor-pointer border-base-content/15 transition-colors hover:bg-base-content/5 has-[:checked]:border-primary has-[:checked]:bg-primary/8 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-primary motion-reduce:transition-none"
591
+ >
592
+ <input
593
+ type="radio"
594
+ class="radio radio-primary radio-sm"
595
+ name="callout-{ctx?.blockId ?? 'decision'}"
596
+ value={o.id}
597
+ checked={pick === o.id}
598
+ onchange={() => pickOption(o.id, true)}
599
+ onkeydown={(e) => optionKeydown(e, i)}
600
+ />
601
+ {@render optionText(o, i, o.label)}
602
+ </label>
603
+ {:else}
604
+ {@const chosen = readPick === o.id}
605
+ <label
606
+ class="{optionRow} {chosen
607
+ ? 'border-primary bg-primary/8 font-medium text-base-content'
608
+ : 'border-base-content/10 text-base-content/70'}"
609
+ >
610
+ <input
611
+ type="radio"
612
+ class="sr-only"
613
+ name="callout-{ctx?.blockId ?? 'decision'}"
614
+ value={o.id}
615
+ checked={chosen}
616
+ disabled
617
+ />
618
+ <span
619
+ class="shrink-0 {chosen
620
+ ? 'text-primary'
621
+ : 'text-base-content/40'}"
622
+ aria-hidden="true"
623
+ >
624
+ {#if chosen}
625
+ <BlockIcon name="circle-check" size={18} stroke-width={2.25} />
626
+ {:else}
627
+ <BlockIcon name="circle" size={18} stroke-width={1.75} />
628
+ {/if}
629
+ </span>
630
+ {@render optionText(
631
+ o,
632
+ i,
633
+ o.id === OTHER_ID && chosen
634
+ ? optionLabel(OTHER_ID)
635
+ : o.label,
636
+ )}
637
+ {#if chosen}<span class="sr-only">(the saved answer)</span
638
+ >{/if}
639
+ </label>
640
+ {/if}
641
+ {/each}
642
+
643
+ {#if showOtherInput}
644
+ <input
645
+ type="text"
646
+ class="input input-bordered input-sm w-full"
647
+ placeholder="Say which"
648
+ aria-label="Other: please specify"
649
+ bind:value={otherText}
650
+ bind:this={otherInputEl}
651
+ oninput={scheduleNoteSave}
652
+ onblur={flushNoteSave}
653
+ />
654
+ {/if}
655
+ </div>
656
+ </fieldset>
657
+
658
+ {#if interactive && hasNotes}
659
+ <div class="mt-4 block" bind:this={composerWrap}>
660
+ <span class="text-sm text-base-content/75">
661
+ {notesAsOther
662
+ ? "Say what instead"
663
+ : (params?.commentLabel ?? "Add a note (optional)")}
664
+ </span>
665
+ <!-- The ONE composer (mos-rccb5): the notes box gains voice /
666
+ video / screen capture, file picker, paste and drag-drop;
667
+ the debounced autosave and its blur-flush are unchanged. -->
668
+ <NoteComposer
669
+ class="mt-1"
670
+ artifactId={ctx.artifactId}
671
+ bind:text={comment}
672
+ bind:clips={commentClips}
673
+ onInput={() => scheduleNoteSave()}
674
+ onClipsChange={() => scheduleNoteSave()}
675
+ onBlur={flushNoteSave}
676
+ rows={2}
677
+ placeholder={notesAsOther
678
+ ? "Say what instead"
679
+ : "Anything to add?"}
680
+ label={notesAsOther
681
+ ? "Say what instead"
682
+ : (params?.commentLabel ?? "Add a note (optional)")}
683
+ />
684
+ </div>
685
+ {:else if !interactive && lastSaved?.comment && !(readPick === OTHER_ID && lastSaved.comment === lastSaved.other)}
686
+ <p class="mt-3 text-sm text-base-content/75">
687
+ Note: {lastSaved.comment}
688
+ </p>
689
+ {/if}
690
+
691
+ {#if sections.length > 0}
692
+ <div class="mt-4 flex flex-col gap-1.5" data-decision-sections>
693
+ {#each sections as sec, si (si)}
694
+ <details
695
+ class="group rounded-field border border-base-content/10"
696
+ open={sec.open === true}
697
+ >
698
+ <summary
699
+ class="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium text-base-content [&::-webkit-details-marker]:hidden"
700
+ >
701
+ <span
702
+ class="text-base-content/50 transition-transform group-open:rotate-90 motion-reduce:transition-none"
703
+ aria-hidden="true">›</span
704
+ >
705
+ {sec.heading}
706
+ </summary>
707
+ <div class="{PROSE} px-3 pb-3">
708
+ {@html sectionHtml[si]}
709
+ </div>
710
+ </details>
711
+ {/each}
712
+ </div>
713
+ {/if}
714
+
715
+ {#if interactive}
716
+ <div class="mt-3 flex flex-wrap items-center gap-2">
717
+ <p
718
+ aria-live="polite"
719
+ data-save-status
720
+ class="flex min-w-0 items-center gap-1 text-xs {saveError
721
+ ? 'text-error'
722
+ : 'text-base-content/70'}"
723
+ >
724
+ {#if !saveError && !saving && lastSaved}<BlockIcon name="check"
725
+ size={13}
726
+ stroke-width={2.5}
727
+ class="text-success"
728
+ aria-hidden="true"
729
+ />{/if}{savedLine}
730
+ </p>
731
+ {#if saveError}
732
+ <!-- No Save/Update answer button any more — the only control
733
+ left is the failure state's own Retry (mos-4dkv9). -->
734
+ <button
735
+ type="button"
736
+ class="btn btn-xs rounded-field"
737
+ data-retry
738
+ onclick={retrySave}
739
+ >
740
+ Retry
741
+ </button>
742
+ {/if}
743
+ </div>
744
+ {/if}
745
+ </div>
746
+ </div>
747
+ </div>
748
+ </div>
749
+ {:else}
750
+ <div
751
+ class="flex items-start gap-3 rounded-box border p-4 text-base-content {tone.panel}"
752
+ role="note"
753
+ data-variant={variant}
754
+ >
755
+ <span
756
+ class="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full {tone.disc}"
757
+ aria-hidden="true"
758
+ >
759
+ {#if variant === "warn"}
760
+ <BlockIcon name="triangle-alert" size={14} stroke-width={2.5} />
761
+ {:else if variant === "success"}
762
+ <BlockIcon name="check" size={14} stroke-width={2.75} />
763
+ {:else}
764
+ <BlockIcon name="info" size={14} stroke-width={2.5} />
765
+ {/if}
766
+ </span>
767
+ <div class="min-w-0 flex-1">
768
+ <SectionTools {ctx} {listenLabel} />
769
+ {#if params?.title}
770
+ <p class="font-semibold leading-snug">{params.title}</p>
771
+ {/if}
772
+ {#if bodyHtml}
773
+ <div class="{PROSE} {params?.title ? 'mt-0.5' : ''}">
774
+ {@html bodyHtml}
775
+ </div>
776
+ {/if}
777
+ </div>
778
+ </div>
779
+ {/if}