@joewinke/jatui 0.36.3 → 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 (271) 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/dist/components/Button.svelte +8 -0
  267. package/dist/components/TextInput.svelte +39 -6
  268. package/dist/components/TextInput.svelte.d.ts +15 -7
  269. package/dist/components/basic/Checkbox.svelte +52 -14
  270. package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
  271. package/package.json +11 -2
@@ -0,0 +1,674 @@
1
+ <script lang="ts">
2
+ /**
3
+ * BlockRenderer (origin mos-bqeoa) — the host half of the blocks contract.
4
+ *
5
+ * Renders a BlocksSpec IN the host page (no iframe): a blocks artifact is
6
+ * data, every block is schema-validated at publish, so this component's
7
+ * only job is lookup + delegation + the two failure modes the contract
8
+ * names explicitly:
9
+ *
10
+ * - an UNKNOWN block type (the registry moved on since this version was
11
+ * published) and an INVALID params object render a visible error card
12
+ * — never a thrown error, never a silent skip. History is truth; a
13
+ * restored old version may reference a block the present no longer
14
+ * knows, and the page must still stand.
15
+ * - per-viewer state flows through ctx: the renderer owns the saved
16
+ * blocks map and a debounced per-block save, so a block widget calls
17
+ * `ctx.saveState(next)` and stays dumb about the network.
18
+ *
19
+ * Read-only surfaces (public share without comment rights, print) get
20
+ * `onSaveBlock === undefined` → ctx.saveState is undefined, and every
21
+ * block must render correctly in that mode (the contract block authors
22
+ * signed up to).
23
+ *
24
+ * Groups (PARAMS-SPEC §5): a block whose authored params carry
25
+ * `group: { key, label, quiet? }` sits under a header the renderer draws
26
+ * BETWEEN cards whenever the key changes — anchored `#group-<key>` for
27
+ * an intro's jump links, with the run's card count; a `quiet` group
28
+ * starts collapsed to that one row.
29
+ *
30
+ * Host services: pass `adapter` when mounting the renderer alone (the
31
+ * view passes its own down). It reaches every block as `ctx.services`
32
+ * and every chrome piece through the context (adapter.ts).
33
+ */
34
+ import { blockDef } from "./registry.js"
35
+ import { PENDING_BLOCK_TYPES } from "./builtins.js"
36
+ import { resolveBlockParams } from "./liveData.js"
37
+ import BlockComments from "./BlockComments.svelte"
38
+ import CommentPill from "./CommentPill.svelte"
39
+ import BlockVerdict from "./BlockVerdict.svelte"
40
+ import { hostAdapter, hostServices, provideHostAdapter } from "./adapter.js"
41
+ import { reviewKey, takesBlockVerdict } from "./review.js"
42
+ import type {
43
+ BlockGroup,
44
+ BlockReviewState,
45
+ BlockState,
46
+ BlocksSpec,
47
+ HostAdapter,
48
+ } from "./types.js"
49
+ import { flushSync, onMount, type Snippet } from "svelte"
50
+
51
+ /** SSR vs browser, without a SvelteKit import (jatui is host-agnostic). */
52
+ const browser = typeof window !== "undefined"
53
+
54
+ interface Props {
55
+ spec: BlocksSpec
56
+ artifactId: string
57
+ version: number
58
+ /** This viewer's saved per-block state: blockId → its saved blob. */
59
+ savedBlocks?: Record<string, unknown> | null
60
+ /**
61
+ * Persist the WHOLE per-block state map (debounced here). Omit on a
62
+ * read-only surface — blocks then get ctx.saveState === undefined. The
63
+ * renderer owns the merge: it posts its current map (the touched block's
64
+ * new state plus every other block's latest), so a host page can POST it
65
+ * verbatim and never hold a stale copy.
66
+ */
67
+ onSaveBlock?: (payload: {
68
+ kind: "blocks"
69
+ blocks: Record<string, unknown>
70
+ }) => Promise<void>
71
+ /** True on the public share route — blocks must not show viewer chrome. */
72
+ readonly?: boolean
73
+ /** Live data published for the artifact; {} when none. */
74
+ data?: Record<string, unknown>
75
+ /** Save-status changes, for the host page's header indicator. */
76
+ onStatus?: (status: "idle" | "saving" | "saved" | "failed") => void
77
+ /**
78
+ * Per-block comment threads (mos-kib2f). Undefined on the read-only
79
+ * share surface — blocks render with no viewer chrome, per the contract.
80
+ */
81
+ comments?: {
82
+ enabled: boolean
83
+ /** Unresolved counts per block (server-rendered). */
84
+ counts: Record<string, number>
85
+ }
86
+ /**
87
+ * Per-block approve / needs-a-pass / note (mos-wywa3): on for a page
88
+ * linked to a jat task, where "Finish review" answers that task. Each
89
+ * verdict saves under the block's `review:<blockId>` key.
90
+ */
91
+ review?: boolean
92
+ /** Every change to the local state map, for the host's Finish review. */
93
+ onLocalChange?: (blocks: Record<string, unknown>) => void
94
+ /**
95
+ * The host's services (comments, composer, listen, block services…).
96
+ * Omit inside BlocksArtifactView (it provides its own); set it when
97
+ * mounting the renderer alone.
98
+ */
99
+ adapter?: HostAdapter
100
+ }
101
+
102
+ let {
103
+ spec,
104
+ artifactId,
105
+ version,
106
+ savedBlocks = null,
107
+ onSaveBlock = undefined,
108
+ readonly = false,
109
+ data = {},
110
+ onStatus = undefined,
111
+ comments = undefined,
112
+ review = false,
113
+ onLocalChange = undefined,
114
+ adapter = undefined,
115
+ }: Props = $props()
116
+
117
+ // An explicit adapter wins; otherwise inherit the view's (or none).
118
+ // svelte-ignore state_referenced_locally
119
+ if (adapter) provideHostAdapter(adapter)
120
+ const host = hostAdapter()
121
+ const services = hostServices(host)
122
+ /** Comments need the host's service too: no service → no pills. */
123
+ const commentsOn = $derived(
124
+ comments?.enabled === true && host.comments !== undefined,
125
+ )
126
+
127
+ // ---- Groups between cards (PARAMS-SPEC §5) ----------------------------
128
+ const GROUP_KEY_RE = /^[a-z0-9-]{1,32}$/
129
+ function groupOf(params: unknown): BlockGroup | null {
130
+ if (typeof params !== "object" || params === null) return null
131
+ const g = (params as Record<string, unknown>).group
132
+ if (typeof g !== "object" || g === null) return null
133
+ const { key, label, quiet } = g as Record<string, unknown>
134
+ if (typeof key !== "string" || !GROUP_KEY_RE.test(key)) return null
135
+ if (typeof label !== "string" || !label.trim()) return null
136
+ return { key, label: label.trim().slice(0, 60), quiet: quiet === true }
137
+ }
138
+ interface GroupRun extends BlockGroup {
139
+ /** Index of the run's first block in spec.blocks. */
140
+ start: number
141
+ count: number
142
+ /** The first run of this key carries the `#group-<key>` anchor. */
143
+ anchor: boolean
144
+ }
145
+ /** Runs of consecutive blocks sharing a group key, keyed by start index;
146
+ * `runOf[i]` = the run block i sits in (null = ungrouped). */
147
+ const groups = $derived.by(() => {
148
+ const headers = new Map<number, GroupRun>()
149
+ const runOf: (GroupRun | null)[] = []
150
+ const anchored = new Set<string>()
151
+ let current: GroupRun | null = null
152
+ spec.blocks.forEach((b, i) => {
153
+ const g = groupOf(b.params)
154
+ if (!g) {
155
+ current = null
156
+ runOf.push(null)
157
+ return
158
+ }
159
+ if (!current || current.key !== g.key) {
160
+ current = { ...g, start: i, count: 0, anchor: !anchored.has(g.key) }
161
+ anchored.add(g.key)
162
+ headers.set(i, current)
163
+ }
164
+ current.count++
165
+ runOf.push(current)
166
+ })
167
+ return { headers, runOf }
168
+ })
169
+ /**
170
+ * Collapsed groups, by group KEY (a republish that reorders cards keeps
171
+ * the viewer's choice); a quiet group starts collapsed. Collapsing HIDES
172
+ * the cards (`hidden`), never unmounts them: a card keeps its state and
173
+ * stays a jump target — revealBlock() expands its group first.
174
+ */
175
+ let collapsed = $state<Record<string, boolean>>({})
176
+ function isCollapsed(run: GroupRun): boolean {
177
+ return collapsed[run.key] ?? run.quiet === true
178
+ }
179
+
180
+ /**
181
+ * Make a block visible before a jump lands on it (the view's open-item
182
+ * walk, a Finish card, a #block-<id> link): expands the group it sits in.
183
+ * Returns true when it had to expand one.
184
+ */
185
+ export function revealBlock(blockId: string): boolean {
186
+ const i = spec.blocks.findIndex((b) => b.id === blockId)
187
+ const run = i >= 0 ? groups.runOf[i] : null
188
+ if (!run || !isCollapsed(run)) return false
189
+ collapsed = { ...collapsed, [run.key]: false }
190
+ return true
191
+ }
192
+
193
+ function revealHash() {
194
+ const m = /^#block-(.+)$/.exec(location.hash)
195
+ if (m && revealBlock(decodeURIComponent(m[1]!))) {
196
+ flushSync()
197
+ document.getElementById(`block-${m[1]}`)?.scrollIntoView()
198
+ }
199
+ }
200
+ // Once at mount (an arrival on a deep link); never re-run on a toggle,
201
+ // or collapsing the group under the hash would re-open it.
202
+ onMount(revealHash)
203
+
204
+ /**
205
+ * Local copy of every block's saved state. Reactive: a block that reads
206
+ * ctx.state re-renders when its own key changes (its own save round-trip).
207
+ * Seeded from the server's saved map and updated optimistically on save.
208
+ */
209
+ let blocks = $state<Record<string, unknown>>(
210
+ savedBlocks ? ($state.snapshot(savedBlocks) as typeof savedBlocks) : {},
211
+ )
212
+
213
+ $effect(() => {
214
+ onLocalChange?.(blocks)
215
+ })
216
+
217
+ function reviewStateOf(blockId: string): BlockReviewState | null {
218
+ const s = blocks[reviewKey(blockId)]
219
+ return typeof s === "object" && s !== null ? (s as BlockReviewState) : null
220
+ }
221
+
222
+ /**
223
+ * Fire every save still in its debounce window and wait for all flights:
224
+ * the host calls this before "Finish review" so the server reads the
225
+ * viewer's latest state.
226
+ */
227
+ export async function flush(): Promise<void> {
228
+ for (const [blockId, t] of pending) {
229
+ clearTimeout(t)
230
+ pending.delete(blockId)
231
+ pendingResolve.get(blockId)?.()
232
+ pendingResolve.delete(blockId)
233
+ void fireSave(blockId)
234
+ }
235
+ await Promise.allSettled([...inFlight.values()])
236
+ }
237
+
238
+ /** Block ids whose latest save FAILED — the Finish-review gate reads this
239
+ * before sending an answer: a send over a failed save would answer the
240
+ * task with whatever the server happens to hold (possibly nothing) and
241
+ * overwrite the viewer's state with that (mos-lwbbt, pe-dusk-sweep). */
242
+ export function failedSaveBlocks(): string[] {
243
+ return Object.keys(failedBlocks)
244
+ }
245
+
246
+ type SaveStatus = "idle" | "saving" | "saved" | "failed"
247
+ let status: SaveStatus = $state("idle")
248
+
249
+ /**
250
+ * A hosted comment trigger's state (mos-8fvt9): the pill renders on the
251
+ * block's heading row (SectionTools), the thread drawer stays here.
252
+ */
253
+ let commentOpen = $state<Record<string, boolean>>({})
254
+ let commentUnresolved = $state<Record<string, number>>({})
255
+
256
+ /** Per-block debounce timers — one save in flight per block, not per key. */
257
+ const pending = new Map<string, ReturnType<typeof setTimeout>>()
258
+ const inFlight = new Map<string, Promise<void>>()
259
+ /**
260
+ * The promise each debounced save returned to its block (mos-4dkv9). A
261
+ * save-on-press block (the decision callout) races its own debounce: a
262
+ * second save within the window CLEARS the first save's timer, and a
263
+ * promise whose timer was cleared used to never settle — a widget that
264
+ * awaits ctx.saveState stuck on "Saving…" forever. The superseded state
265
+ * rides the newer save's POST (the map at fire time carries it), so
266
+ * settling the displaced promise without its own POST is honest.
267
+ */
268
+ const pendingResolve = new Map<string, () => void>()
269
+
270
+ /**
271
+ * The per-block context handed to every block component. Getters (not
272
+ * snapshot values) so a block reading ctx.state sees the current local
273
+ * copy, and saveState is present iff this surface can actually save.
274
+ */
275
+ function ctxFor(blockId: string, tools?: Snippet) {
276
+ const canSave = !readonly && onSaveBlock !== undefined
277
+ return {
278
+ artifactId,
279
+ version,
280
+ blockId,
281
+ tools,
282
+ get state() {
283
+ const s = blocks[blockId]
284
+ return s === undefined ? null : (s as BlockState)
285
+ },
286
+ get saveState() {
287
+ return canSave
288
+ ? (next: BlockState) => saveBlock(blockId, next)
289
+ : undefined
290
+ },
291
+ data,
292
+ readonly,
293
+ services,
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Validate one block's params: the error-card message (null when valid)
299
+ * and — when valid — the PARSED params the component should render.
300
+ *
301
+ * The component receives the schema-PARSED form, never the raw input
302
+ * (mos-659rz): publish stores the spec verbatim (history is truth), and
303
+ * zod defaults + preprocessing apply only at parse — so a raw params
304
+ * object can PASS validation yet still be missing fields the component
305
+ * requires. A mockup variant without `device` did exactly that: publish
306
+ * OK, then every render 500'd at MOCKUP_DIMS[undefined].maxFrameW. The
307
+ * component sees the normalized form the schema always meant it to see.
308
+ *
309
+ * `fromData` says these params ARE a live-data value (mos-0ninw) — the
310
+ * error then names the dataKey, because the thing to fix is the published
311
+ * DATA (the host's data publish), not the block's inline params. */
312
+ function blockRender(
313
+ blockId: string,
314
+ type: string,
315
+ params: unknown,
316
+ fromData: string | null,
317
+ ): { err: string | null; params: unknown } {
318
+ const def = blockDef(type)
319
+ if (!def) {
320
+ return {
321
+ err: PENDING_BLOCK_TYPES.includes(type)
322
+ ? `Block type "${type}" is not in this renderer yet — it is still being ported to jatui.`
323
+ : `Unknown block type "${type}" — this version predates a block rename, or was published for a block this host does not register.`,
324
+ params: null,
325
+ }
326
+ }
327
+ const parsed = def.schema.safeParse(params)
328
+ if (!parsed.success) {
329
+ const first = parsed.error.issues
330
+ .map((i) => `${i.path.join(".") || "params"}: ${i.message}`)
331
+ .slice(0, 3)
332
+ .join("; ")
333
+ const source = fromData
334
+ ? `live data "${fromData}"`
335
+ : `params for "${type}"`
336
+ return {
337
+ err: `Invalid ${source} — ${first}${fromData ? " (published as live data; the inline params are NOT shown while a data value for this key exists)" : ""}`,
338
+ params: null,
339
+ }
340
+ }
341
+ return { err: null, params: parsed.data }
342
+ }
343
+
344
+ function setStatus(next: SaveStatus) {
345
+ status = next
346
+ onStatus?.(next)
347
+ }
348
+ /** Per-block failed flag — the visible "Not saved — Retry" strip is keyed
349
+ * by blockId, because a failure on one block must not fail the page.
350
+ * (mos-yntqm: the DataCloneError era proved a save can die without ANY
351
+ * visible trace; network failures were only a tiny header line.) */
352
+ let failedBlocks = $state<Record<string, true>>({})
353
+
354
+ function markFailed(blockId: string) {
355
+ failedBlocks = { ...failedBlocks, [blockId]: true }
356
+ setStatus("failed")
357
+ }
358
+
359
+ function markSaving(blockId: string) {
360
+ if (failedBlocks[blockId]) {
361
+ const next = { ...failedBlocks }
362
+ delete next[blockId]
363
+ failedBlocks = next
364
+ }
365
+ setStatus("saving")
366
+ }
367
+
368
+ /**
369
+ * POST the current map NOW for one block — the debounce timer's body, and
370
+ * the Retry button's action. Returns the flight so callers that want its
371
+ * outcome can await it; the widget-facing saveBlock promise is unaffected.
372
+ */
373
+ function fireSave(blockId: string): Promise<void> {
374
+ if (!onSaveBlock) return Promise.resolve()
375
+ markSaving(blockId)
376
+ // The map at FIRE time, not click time — a second block's save within
377
+ // the debounce window rides along instead of clobbering it.
378
+ const flight = onSaveBlock({ kind: "blocks", blocks: { ...blocks } })
379
+ .then(() => {
380
+ inFlight.delete(blockId)
381
+ setStatus("saved")
382
+ })
383
+ .catch((err: unknown) => {
384
+ inFlight.delete(blockId)
385
+ // The next save retries; the widget stays usable — a failed save
386
+ // must not brick the block or the page. The failure is VISIBLE:
387
+ // a per-block "Not saved — Retry" strip (mos-yntqm) — and a 429
388
+ // retries itself when the window rolls (mos-lwbbt).
389
+ markFailed(blockId)
390
+ scheduleBackoff(blockId, err)
391
+ })
392
+ inFlight.set(blockId, flight)
393
+ return flight
394
+ }
395
+
396
+ /** Retry a failed save immediately with the block's latest local state. */
397
+ function retrySave(blockId: string) {
398
+ const t = pending.get(blockId)
399
+ if (t) {
400
+ clearTimeout(t)
401
+ pending.delete(blockId)
402
+ pendingResolve.get(blockId)?.()
403
+ pendingResolve.delete(blockId)
404
+ }
405
+ void fireSave(blockId)
406
+ }
407
+
408
+ /** One scheduled retry per block after a 429 (mos-lwbbt, pe-dusk-sweep):
409
+ * Joe approved 90 items through the lightbox faster than the save burst
410
+ * cap, every save got refused, the data lived only in the tab, and Send
411
+ * answered the task with nothing. A refused save now retries itself when
412
+ * the server says the window rolls — the queue drains, nothing is lost. */
413
+ const backoffs = new Map<string, ReturnType<typeof setTimeout>>()
414
+ const MAX_BACKOFF_MS = 60_000
415
+ function scheduleBackoff(blockId: string, err: unknown) {
416
+ const retryAfter = (err as { retryAfterSeconds?: unknown })
417
+ ?.retryAfterSeconds
418
+ const secs =
419
+ typeof retryAfter === "number" &&
420
+ Number.isFinite(retryAfter) &&
421
+ retryAfter > 0
422
+ ? Math.min(retryAfter, MAX_BACKOFF_MS / 1000)
423
+ : 5
424
+ if (backoffs.has(blockId)) return // one flight per block; it carries the whole map
425
+ const t = setTimeout(() => {
426
+ backoffs.delete(blockId)
427
+ if (inFlight.has(blockId) || pending.has(blockId)) return
428
+ void fireSave(blockId)
429
+ }, secs * 1000)
430
+ backoffs.set(blockId, t)
431
+ }
432
+
433
+ /** Optimistic local update + debounced POST of the WHOLE blocks map. The
434
+ * payload is keyed by blockId (feedback.ts validates it); the server
435
+ * resolves the viewer, the artifact and the version from the session. */
436
+ function saveBlock(blockId: string, next: BlockState): Promise<void> {
437
+ if (!onSaveBlock) return Promise.resolve()
438
+ // EVERY failure lands in the visible failed state (mos-yntqm): a sync
439
+ // throw (the DataCloneError class) used to escape into the block's click
440
+ // handler and vanish — no status change, no POST, no trace. Caught here,
441
+ // the block keeps its optimistic local state and the strip shows.
442
+ try {
443
+ blocks = { ...blocks, [blockId]: $state.snapshot(next) as BlockState }
444
+ } catch {
445
+ markFailed(blockId)
446
+ return Promise.resolve()
447
+ }
448
+ markSaving(blockId)
449
+
450
+ const existing = pending.get(blockId)
451
+ if (existing) {
452
+ clearTimeout(existing)
453
+ // The save this timer belonged to is superseded — settle its promise
454
+ // so the block's own awaiter unblocks (mos-4dkv9).
455
+ pendingResolve.get(blockId)?.()
456
+ pendingResolve.delete(blockId)
457
+ }
458
+ return new Promise<void>((resolve) => {
459
+ pendingResolve.set(blockId, resolve)
460
+ const timer = setTimeout(() => {
461
+ pending.delete(blockId)
462
+ pendingResolve.delete(blockId)
463
+ resolve(fireSave(blockId))
464
+ }, 600)
465
+ pending.set(blockId, timer)
466
+ })
467
+ }
468
+
469
+ $effect(() => () => {
470
+ // A save still in its debounce window when the page goes away is a real
471
+ // viewer decision — FIRE it instead of dropping it (mos-yntqm): the
472
+ // fetch keeps running through an SPA navigation, and the host page's
473
+ // keepalive fetch survives a full unload. A fired save into a detached
474
+ // component only sets state nobody reads.
475
+ for (const [blockId, t] of pending) {
476
+ clearTimeout(t)
477
+ void fireSave(blockId)
478
+ pendingResolve.get(blockId)?.()
479
+ }
480
+ pending.clear()
481
+ pendingResolve.clear()
482
+ for (const t of backoffs.values()) clearTimeout(t)
483
+ backoffs.clear()
484
+ })
485
+ </script>
486
+
487
+ <!-- A #block-<id> link into a collapsed group opens the group first. -->
488
+ <svelte:window onhashchange={revealHash} />
489
+
490
+ <div class="flex max-w-full min-w-0 flex-col gap-4">
491
+ {#each spec.blocks as b, i (b.id)}
492
+ {@const header = groups.headers.get(i)}
493
+ {@const run = groups.runOf[i]}
494
+ {#if header}
495
+ <!-- A group header between cards: sticky label + count; a quiet group
496
+ collapses to this one row. -->
497
+ <div
498
+ id={header.anchor ? `group-${header.key}` : undefined}
499
+ class="ab-group-header border-base-content/10 bg-base-100/95 sticky top-[var(--artifact-sticky-top,0px)] z-[1] -mb-1 flex scroll-mt-20 items-center gap-2 border-b pt-3 pb-1.5"
500
+ data-group-header={header.key}
501
+ >
502
+ <h2 class="text-base-content/70 min-w-0 truncate text-xs font-semibold tracking-wide uppercase">
503
+ {header.label}
504
+ </h2>
505
+ <span
506
+ class="bg-base-content/10 text-base-content/70 rounded-field px-1.5 text-xs tabular-nums"
507
+ aria-label="{header.count} {header.count === 1 ? 'card' : 'cards'}"
508
+ >{header.count}</span
509
+ >
510
+ <button
511
+ type="button"
512
+ class="btn btn-ghost btn-xs rounded-field ml-auto"
513
+ aria-expanded={!isCollapsed(header)}
514
+ onclick={() =>
515
+ (collapsed = { ...collapsed, [header.key]: !isCollapsed(header) })}
516
+ >
517
+ {isCollapsed(header) ? "Show" : "Hide"}
518
+ </button>
519
+ </div>
520
+ {/if}
521
+ {@const def = blockDef(b.type)}
522
+ {@const resolved = resolveBlockParams(def, b.params, data)}
523
+ {@const render = blockRender(
524
+ b.id,
525
+ b.type,
526
+ resolved.params,
527
+ resolved.fromData ? resolved.dataKey : null,
528
+ )}
529
+ {@const wideBlock = def?.wide?.(render.params) === true}
530
+ <section
531
+ id={`block-${b.id}`}
532
+ class="min-w-0 scroll-mt-20"
533
+ aria-label={def?.title ?? `Block ${b.id}`}
534
+ data-group={run ? run.key : undefined}
535
+ hidden={run !== null && isCollapsed(run)}
536
+ >
537
+ {#if render.err}
538
+ <!-- Visible, honest, and non-fatal: the rest of the page stands. -->
539
+ <div
540
+ role="alert"
541
+ class="border-error/30 bg-error/10 text-error-content rounded-box border p-4 text-sm"
542
+ >
543
+ <p class="font-medium">Block "{b.id}" ({b.type}) could not render</p>
544
+ <p class="text-error-content/80 mt-1 break-words">{render.err}</p>
545
+ </div>
546
+ {:else if def}
547
+ {@const Block = def.component}
548
+ {@const label =
549
+ (typeof render.params === "object" &&
550
+ render.params !== null &&
551
+ !Array.isArray(render.params) &&
552
+ typeof (render.params as Record<string, unknown>).title === "string"
553
+ ? ((render.params as Record<string, unknown>).title as string)
554
+ : null) ??
555
+ def?.title ??
556
+ b.id}
557
+ {@const hosted =
558
+ commentsOn &&
559
+ !readonly &&
560
+ def.hostsTools?.(render.params as never) === true}
561
+ <!-- mos-8fvt9: a block that hosts its tools gets the comment pill on
562
+ its heading row (ctx.tools → SectionTools); every other block
563
+ keeps the pill on a row of its own above it. The thread drawer
564
+ is always rendered here, outside any block's @container. -->
565
+ {#snippet commentTool()}
566
+ <CommentPill
567
+ {label}
568
+ unresolved={commentUnresolved[b.id] ?? comments?.counts[b.id] ?? 0}
569
+ open={commentOpen[b.id] ?? false}
570
+ onclick={() => (commentOpen[b.id] = !(commentOpen[b.id] ?? false))}
571
+ />
572
+ {/snippet}
573
+ {#if commentsOn && comments}
574
+ <BlockComments
575
+ {artifactId}
576
+ {version}
577
+ blockId={b.id}
578
+ blockLabel={label}
579
+ count={comments.counts[b.id] ?? 0}
580
+ enabled={!readonly}
581
+ wide={wideBlock}
582
+ {hosted}
583
+ bind:open={() => commentOpen[b.id] ?? false,
584
+ (v) => (commentOpen[b.id] = v)}
585
+ onunresolved={(n) => (commentUnresolved[b.id] = n)}
586
+ />
587
+ {/if}
588
+ {#if failedBlocks[b.id]}
589
+ <!-- mos-yntqm: a failed save is a VISIBLE, actionable card on the
590
+ block itself — never a silent loss, never a throwaway line. -->
591
+ <div
592
+ role="alert"
593
+ class="border-warning/40 bg-warning/10 text-warning-content rounded-box mb-2 flex flex-wrap items-center gap-2 border p-3 text-sm"
594
+ >
595
+ <span class="font-medium">Not saved</span>
596
+ <span class="text-warning-content/80">
597
+ Your last change here could not be saved.</span
598
+ >
599
+ <button
600
+ type="button"
601
+ class="btn btn-xs rounded-field ml-auto"
602
+ onclick={() => retrySave(b.id)}
603
+ >
604
+ Retry
605
+ </button>
606
+ </div>
607
+ {/if}
608
+ <!-- Per-block error boundary (mos-659rz): a block component that
609
+ THROWS — a bug the schema cannot see — degrades to the same
610
+ quiet notice; one bad block never takes the page down.
611
+
612
+ The boundary is ACTIVE ONLY AT SSR and INTENTIONALLY
613
+ TRANSPARENT on the client (failed = undefined → Svelte
614
+ rethrows past it). Svelte boundaries cannot distinguish a
615
+ render error from an effect error — both destroy the block and
616
+ render the failed snippet — and a harmless effect hiccup
617
+ (canvas/audio/observer) inside e.g. the program block's player
618
+ must NOT blank the whole block. At SSR effects never run, so
619
+ the boundary there is render-only by construction: exactly the
620
+ 500 class this fix targets (a block throwing during SSR used
621
+ to 500 the whole page). A client-side render throw behaves as
622
+ it always has (no boundary): it propagates to the app-level
623
+ handler. -->
624
+ <svelte:boundary failed={browser ? undefined : blockCrashed}>
625
+ <Block
626
+ params={render.params}
627
+ ctx={ctxFor(b.id, hosted ? commentTool : undefined)}
628
+ />
629
+ </svelte:boundary>
630
+ {#snippet blockCrashed(error: unknown, reset: () => void)}
631
+ <!-- Svelte adapts the failed snippet's signature per side: at SSR
632
+ it is called (renderer, error, reset), on the client
633
+ (error, reset) — declaring (error, reset) is correct on both. -->
634
+ <div
635
+ role="alert"
636
+ class="border-error/30 bg-error/10 text-error-content rounded-box border p-4 text-sm"
637
+ >
638
+ <p class="font-medium">
639
+ Block "{b.id}" ({b.type}) crashed while rendering
640
+ </p>
641
+ <p class="text-error-content/80 mt-1">
642
+ {error instanceof Error ? error.message : String(error)}
643
+ </p>
644
+ </div>
645
+ {/snippet}
646
+ <!-- Only blocks that take a verdict (mos-lfkys): information blocks
647
+ (stats, table, markdown, a plain callout) show no strip. Read
648
+ from the AUTHORED params — `reviewable` is a spec opt-in. -->
649
+ {#if review && !readonly && takesBlockVerdict(b.type, b.params)}
650
+ <BlockVerdict
651
+ {artifactId}
652
+ blockId={b.id}
653
+ blockLabel={def?.title ?? b.id}
654
+ saved={reviewStateOf(b.id)}
655
+ onChange={onSaveBlock
656
+ ? (next) => void saveBlock(reviewKey(b.id), { ...next })
657
+ : undefined}
658
+ />
659
+ {/if}
660
+ {/if}
661
+ </section>
662
+ {:else}
663
+ <p class="text-base-content/50 text-sm">This page has no blocks yet.</p>
664
+ {/each}
665
+ </div>
666
+ {#if status !== "idle"}
667
+ <span class="sr-only" aria-live="polite">
668
+ {status === "saving"
669
+ ? "Saving…"
670
+ : status === "saved"
671
+ ? "Saved"
672
+ : "Not saved"}
673
+ </span>
674
+ {/if}