@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,484 @@
1
+ <script lang="ts">
2
+ /**
3
+ * compare block — two media sides revealed through one draggable divider.
4
+ *
5
+ * a sits underneath (fully visible when the divider is at 0), b is clipped
6
+ * on top and revealed toward the right (vertical divider) or bottom
7
+ * (horizontal). The divider is a keyboard-accessible slider (role="slider":
8
+ * arrows, Home/End, Shift for a bigger step) and the whole stage is
9
+ * pointer-draggable with pointer capture.
10
+ *
11
+ * Under a 480px container the SAME DOM falls back to a side-by-side grid
12
+ * (CSS container query — no JS, SSR-safe): both sides fully visible with
13
+ * their labels, no divider, so a phone never hides half the comparison.
14
+ *
15
+ * Videos are muted + looping and start PAUSED — a shared play/pause runs
16
+ * both sides in sync while the divider moves. No autoplay: reduced-motion
17
+ * viewers get exactly what everyone else gets, just not moving.
18
+ *
19
+ * The optional pick verdict persists { pick: 'a' | 'b', at } through
20
+ * ctx.saveState; read-only surfaces render the saved pick as a badge with
21
+ * no buttons and never call saveState.
22
+ */
23
+ import Icon from "../checklist/Icon.svelte"
24
+ import type { BlockProps } from "../../types.js"
25
+ import {
26
+ altOf,
27
+ clipFor,
28
+ dividerValueText,
29
+ kindOf,
30
+ labelOf,
31
+ parsePick,
32
+ stepFor,
33
+ type CompareMedia,
34
+ type CompareParams,
35
+ type CompareSide,
36
+ } from "./schema.js"
37
+ import SectionTools from "../../SectionTools.svelte"
38
+
39
+ let { params, ctx }: BlockProps<CompareParams> = $props()
40
+
41
+ // ---- geometry -------------------------------------------------------------
42
+ const orientation = $derived(params?.orientation ?? "vertical")
43
+ const vertical = $derived(orientation === "vertical")
44
+ /** Divider position, % of the stage from left/top. Authored start is
45
+ * clamped to 5-95 so a spec can never publish a nearly-shut wipe. */
46
+ let percent = $state(Math.min(95, Math.max(5, params?.startPercent ?? 50)))
47
+
48
+ const clip = $derived(clipFor(percent, orientation))
49
+ const valueText = $derived(
50
+ dividerValueText(percent, {
51
+ a: labelOf(params?.a ?? { src: "" }, "a"),
52
+ b: labelOf(params?.b ?? { src: "" }, "b"),
53
+ }),
54
+ )
55
+
56
+ // ---- dragging (pointer) ---------------------------------------------------
57
+ let stageEl: HTMLDivElement | null = $state(null)
58
+ let dragging = $state(false)
59
+
60
+ function percentFromEvent(e: PointerEvent): number {
61
+ if (!stageEl) return percent
62
+ const rect = stageEl.getBoundingClientRect()
63
+ if (rect.width <= 0 || rect.height <= 0) return percent
64
+ const frac = vertical
65
+ ? (e.clientX - rect.left) / rect.width
66
+ : (e.clientY - rect.top) / rect.height
67
+ return Math.min(100, Math.max(0, frac * 100))
68
+ }
69
+
70
+ function onPointerDown(e: PointerEvent) {
71
+ dragging = true
72
+ try {
73
+ stageEl?.setPointerCapture?.(e.pointerId)
74
+ } catch {
75
+ /* jsdom / detached node — drag still works through move events */
76
+ }
77
+ percent = percentFromEvent(e)
78
+ }
79
+
80
+ function onPointerMove(e: PointerEvent) {
81
+ if (!dragging) return
82
+ percent = percentFromEvent(e)
83
+ }
84
+
85
+ function onPointerUp(e: PointerEvent) {
86
+ dragging = false
87
+ try {
88
+ stageEl?.releasePointerCapture?.(e.pointerId)
89
+ } catch {
90
+ /* capture was never taken or already released */
91
+ }
92
+ }
93
+
94
+ // ---- divider (keyboard) ---------------------------------------------------
95
+ function onDividerKey(e: KeyboardEvent) {
96
+ const next = stepFor(e.key, orientation, e.shiftKey, percent)
97
+ if (next === undefined) return
98
+ e.preventDefault()
99
+ percent = next
100
+ }
101
+
102
+ // ---- video transport ------------------------------------------------------
103
+ const kindA = $derived(kindOf(params?.a ?? { src: "" }))
104
+ const kindB = $derived(kindOf(params?.b ?? { src: "" }))
105
+ const anyVideo = $derived(kindA === "video" || kindB === "video")
106
+ let playing = $state(false)
107
+
108
+ /** The <video> elements inside the stage (both sides), SSR-safe. */
109
+ function videos(): HTMLVideoElement[] {
110
+ if (!stageEl) return []
111
+ return [...stageEl.querySelectorAll("video")]
112
+ }
113
+
114
+ function safePlay(v: HTMLVideoElement) {
115
+ try {
116
+ const p = v.play() as unknown
117
+ if (p && typeof (p as Promise<void>).catch === "function")
118
+ (p as Promise<void>).catch(() => {})
119
+ } catch {
120
+ /* jsdom or autoplay refusal — the paused start is the fallback */
121
+ }
122
+ }
123
+
124
+ function togglePlay() {
125
+ const vids = videos()
126
+ if (playing) {
127
+ for (const v of vids) v.pause()
128
+ playing = false
129
+ } else {
130
+ for (const v of vids) safePlay(v)
131
+ playing = true
132
+ }
133
+ }
134
+
135
+ // ---- pick A/B verdict -----------------------------------------------------
136
+ /** Interactive = the viewer can pick here (and it would persist). */
137
+ const interactive = $derived(Boolean(ctx?.saveState) && !ctx?.readonly)
138
+ const hasPick = $derived(Boolean(params?.pick))
139
+ /**
140
+ * This viewer's pick. ctx.state is keyed per (artifact, version, user,
141
+ * blockId) and does not change while mounted — one mount-time read, like
142
+ * the callout/checklist blocks; after that the local value is the truth.
143
+ */
144
+ let pick = $state<CompareSide | null>(parsePick(ctx?.state))
145
+ let saveError = $state<string | null>(null)
146
+
147
+ async function choose(side: CompareSide) {
148
+ if (!interactive || !ctx?.saveState) return
149
+ saveError = null
150
+ pick = side
151
+ try {
152
+ await ctx.saveState({ pick: side, at: new Date().toISOString() })
153
+ } catch (e) {
154
+ saveError = e instanceof Error ? e.message : "Saving failed"
155
+ }
156
+ }
157
+
158
+ /** Labels/alt for chips, fallback headers and the pick buttons. */
159
+ const labelA = $derived(labelOf(params?.a ?? { src: "" }, "a"))
160
+ const labelB = $derived(labelOf(params?.b ?? { src: "" }, "b"))
161
+ const altA = $derived(altOf(params?.a ?? { src: "" }, "a"))
162
+ const altB = $derived(altOf(params?.b ?? { src: "" }, "b"))
163
+ </script>
164
+
165
+ {#snippet media(m: CompareMedia, alt: string)}
166
+ {#if kindOf(m) === "video"}
167
+ <video src={m.src} muted loop playsinline preload="metadata"></video>
168
+ {:else}
169
+ <img src={m.src} {alt} loading="lazy" decoding="async" />
170
+ {/if}
171
+ {/snippet}
172
+
173
+ <section class="artifact-compare w-full">
174
+ {#if params?.title}
175
+ <div class="mb-2 flex items-center gap-2">
176
+ <div class="min-w-0 text-sm font-medium">{params.title}</div>
177
+ <SectionTools {ctx} inline />
178
+ </div>
179
+ {/if}
180
+
181
+ <div class="compare-frame rounded-box border border-base-300 bg-base-100">
182
+ <div
183
+ class="compare-stage touch-none"
184
+ class:dragging
185
+ style="height:{params?.height ?? 420}px; --stage-h:{params?.height ??
186
+ 420}px"
187
+ bind:this={stageEl}
188
+ role="group"
189
+ aria-label={params?.title ?? "Before and after comparison"}
190
+ onpointerdown={onPointerDown}
191
+ onpointermove={onPointerMove}
192
+ onpointerup={onPointerUp}
193
+ onpointercancel={onPointerUp}
194
+ >
195
+ <div class="layer layer-a">
196
+ <span class="chip">{labelA}</span>
197
+ {@render media(params.a, altA)}
198
+ </div>
199
+ <div class="layer layer-b" style="clip-path:{clip}">
200
+ <span class="chip">{labelB}</span>
201
+ {@render media(params.b, altB)}
202
+ </div>
203
+
204
+ <div
205
+ class="divider"
206
+ class:horizontal={!vertical}
207
+ style={vertical ? `left:${percent}%` : `top:${percent}%`}
208
+ >
209
+ <button
210
+ type="button"
211
+ role="slider"
212
+ tabindex="0"
213
+ aria-label="Comparison divider"
214
+ aria-valuemin={0}
215
+ aria-valuemax={100}
216
+ aria-valuenow={Math.round(percent)}
217
+ aria-valuetext={valueText}
218
+ aria-orientation={vertical ? "horizontal" : "vertical"}
219
+ onkeydown={onDividerKey}
220
+ >
221
+ <span class="grip" aria-hidden="true"></span>
222
+ </button>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ {#if anyVideo || hasPick}
228
+ <div class="mt-2 flex flex-wrap items-center justify-between gap-2">
229
+ {#if anyVideo}
230
+ <button
231
+ type="button"
232
+ class="btn btn-ghost btn-sm rounded-field"
233
+ aria-pressed={playing}
234
+ onclick={togglePlay}
235
+ >
236
+ {#if playing}
237
+ <Icon name="pause" size={14} aria-hidden="true" />
238
+ Pause both
239
+ {:else}
240
+ <Icon name="play" size={14} aria-hidden="true" />
241
+ Play both in sync
242
+ {/if}
243
+ </button>
244
+ {:else}
245
+ <span></span>
246
+ {/if}
247
+
248
+ {#if hasPick && interactive}
249
+ <div
250
+ class="flex items-center gap-2"
251
+ role="group"
252
+ aria-label={params.pick?.question}
253
+ >
254
+ <span class="text-xs text-base-content/70">
255
+ {params.pick?.question}
256
+ </span>
257
+ <button
258
+ type="button"
259
+ class="btn btn-sm rounded-field {pick === 'a'
260
+ ? 'btn-primary'
261
+ : 'btn-outline'}"
262
+ aria-pressed={pick === "a"}
263
+ onclick={() => choose("a")}
264
+ >
265
+ A · {labelA}
266
+ </button>
267
+ <button
268
+ type="button"
269
+ class="btn btn-sm rounded-field {pick === 'b'
270
+ ? 'btn-primary'
271
+ : 'btn-outline'}"
272
+ aria-pressed={pick === "b"}
273
+ onclick={() => choose("b")}
274
+ >
275
+ B · {labelB}
276
+ </button>
277
+ {#if saveError}
278
+ <span class="text-xs text-error" role="alert">{saveError}</span>
279
+ {/if}
280
+ </div>
281
+ {:else if hasPick && pick}
282
+ <span
283
+ class="badge badge-ghost gap-1 rounded-field text-xs"
284
+ aria-label="Saved pick"
285
+ >
286
+ <Icon name="check" size={12} aria-hidden="true" />
287
+ {params.pick?.question}: {pick === "a" ? labelA : labelB}
288
+ </span>
289
+ {/if}
290
+ </div>
291
+ {/if}
292
+ </section>
293
+
294
+ <style>
295
+ /* container-type turns .compare-frame into the query container the
296
+ side-by-side fallback reads — width of the BLOCK, not the viewport,
297
+ which is what matters for "is there room to wipe". */
298
+ .compare-frame {
299
+ container-type: inline-size;
300
+ }
301
+
302
+ .compare-stage {
303
+ position: relative;
304
+ width: 100%;
305
+ overflow: hidden;
306
+ border-radius: inherit;
307
+ }
308
+
309
+ .compare-stage.dragging {
310
+ cursor: grabbing;
311
+ }
312
+
313
+ .layer {
314
+ position: absolute;
315
+ inset: 0;
316
+ display: flex;
317
+ align-items: stretch;
318
+ }
319
+
320
+ .layer :is(img, video) {
321
+ display: block;
322
+ width: 100%;
323
+ height: 100%;
324
+ object-fit: cover;
325
+ user-select: none;
326
+ }
327
+
328
+ /* Floating label chips over each side. */
329
+ .chip {
330
+ position: absolute;
331
+ z-index: 2;
332
+ top: 8px;
333
+ left: 8px;
334
+ padding: 2px 8px;
335
+ border-radius: 9999px;
336
+ background: color-mix(in oklch, var(--color-base-100) 88%, transparent);
337
+ color: var(--color-base-content);
338
+ border: 1px solid
339
+ color-mix(in oklch, var(--color-base-content) 12%, transparent);
340
+ font-size: 0.6875rem;
341
+ line-height: 1.4;
342
+ font-weight: 500;
343
+ pointer-events: none;
344
+ }
345
+
346
+ .layer-b .chip {
347
+ left: auto;
348
+ right: 8px;
349
+ }
350
+
351
+ /* Horizontal divider: B is revealed at the BOTTOM, so its chip moves to
352
+ the bottom edge to sit over its own side. */
353
+ .compare-stage:has(.divider.horizontal) .layer-b .chip {
354
+ top: auto;
355
+ bottom: 8px;
356
+ }
357
+
358
+ /* The divider: a 1px line across the stage + a centered round handle. */
359
+ .divider {
360
+ position: absolute;
361
+ z-index: 3;
362
+ top: 0;
363
+ bottom: 0;
364
+ left: 50%;
365
+ width: 0;
366
+ }
367
+
368
+ .divider.horizontal {
369
+ top: 50%;
370
+ bottom: auto;
371
+ left: 0;
372
+ right: 0;
373
+ height: 0;
374
+ width: auto;
375
+ }
376
+
377
+ .divider::before {
378
+ content: "";
379
+ position: absolute;
380
+ top: 0;
381
+ bottom: 0;
382
+ left: -0.5px;
383
+ width: 1px;
384
+ background: color-mix(in oklch, var(--color-base-100) 85%, transparent);
385
+ }
386
+
387
+ .divider.horizontal::before {
388
+ top: -0.5px;
389
+ bottom: auto;
390
+ left: 0;
391
+ right: 0;
392
+ width: auto;
393
+ height: 1px;
394
+ }
395
+
396
+ .divider button {
397
+ position: absolute;
398
+ top: 50%;
399
+ left: 50%;
400
+ transform: translate(-50%, -50%);
401
+ width: 28px;
402
+ height: 28px;
403
+ border-radius: 9999px;
404
+ border: 1px solid
405
+ color-mix(in oklch, var(--color-base-content) 15%, transparent);
406
+ background: var(--color-base-100);
407
+ color: var(--color-base-content);
408
+ display: flex;
409
+ align-items: center;
410
+ justify-content: center;
411
+ cursor: ew-resize;
412
+ box-shadow: 0 1px 4px
413
+ color-mix(in oklch, var(--color-base-content) 25%, transparent);
414
+ }
415
+
416
+ .divider.horizontal button {
417
+ cursor: ns-resize;
418
+ }
419
+
420
+ .divider button:focus-visible {
421
+ outline: 2px solid var(--color-primary);
422
+ outline-offset: 2px;
423
+ }
424
+
425
+ /* The three-line grip. */
426
+ .grip {
427
+ display: block;
428
+ width: 10px;
429
+ height: 2px;
430
+ border-radius: 9999px;
431
+ background: color-mix(in oklch, var(--color-base-content) 55%, transparent);
432
+ box-shadow:
433
+ 0 -3px 0 color-mix(in oklch, var(--color-base-content) 55%, transparent),
434
+ 0 3px 0 color-mix(in oklch, var(--color-base-content) 55%, transparent);
435
+ }
436
+
437
+ /* ── Phone fallback: same DOM, side-by-side ─────────────────────────────
438
+ Under a 480px container the wipe halves get tiny, so BOTH sides are
439
+ shown fully, side by side, with their labels as headers. Pure CSS:
440
+ no JS mode switch, nothing to hydrate, SSR renders identically. */
441
+ @container (max-width: 479.9px) {
442
+ .compare-stage {
443
+ display: grid;
444
+ grid-template-columns: 1fr 1fr;
445
+ gap: 1px;
446
+ height: auto !important;
447
+ background: color-mix(
448
+ in oklch,
449
+ var(--color-base-content) 10%,
450
+ transparent
451
+ );
452
+ }
453
+
454
+ .layer {
455
+ position: static;
456
+ clip-path: none !important;
457
+ flex-direction: column;
458
+ }
459
+
460
+ .layer-a {
461
+ grid-column: 1;
462
+ }
463
+
464
+ .layer-b {
465
+ grid-column: 2;
466
+ }
467
+
468
+ .layer :is(img, video) {
469
+ height: var(--stage-h, 420px);
470
+ }
471
+
472
+ .chip {
473
+ position: static;
474
+ order: -1;
475
+ align-self: flex-start;
476
+ margin: 6px 0 6px 6px;
477
+ border-radius: 9999px;
478
+ }
479
+
480
+ .divider {
481
+ display: none;
482
+ }
483
+ }
484
+ </style>
@@ -0,0 +1,25 @@
1
+ import type { BlockProps } from "../../types.js";
2
+ declare const CompareBlock: import("svelte").Component<BlockProps<{
3
+ a: {
4
+ src: string;
5
+ kind?: "video" | "image" | undefined;
6
+ label?: string | undefined;
7
+ alt?: string | undefined;
8
+ };
9
+ b: {
10
+ src: string;
11
+ kind?: "video" | "image" | undefined;
12
+ label?: string | undefined;
13
+ alt?: string | undefined;
14
+ };
15
+ startPercent: number;
16
+ orientation: "horizontal" | "vertical";
17
+ height: number;
18
+ title?: string | undefined;
19
+ pick?: {
20
+ question: string;
21
+ } | undefined;
22
+ dataKey?: string | undefined;
23
+ }>, {}, "">;
24
+ type CompareBlock = ReturnType<typeof CompareBlock>;
25
+ export default CompareBlock;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compare block definition (block type "compare").
3
+ * Contract: src/lib/artifact-blocks/CONTRACT.md — registry.ts discovers this
4
+ * folder; nothing else registers a block.
5
+ */
6
+ import type { ArtifactBlockDef } from "../../types.js";
7
+ import { type CompareParams } from "./schema.js";
8
+ declare const def: ArtifactBlockDef<CompareParams>;
9
+ export default def;
@@ -0,0 +1,32 @@
1
+ import CompareBlock from "./CompareBlock.svelte";
2
+ import { compareSchema } from "./schema.js";
3
+ const EXAMPLE = {
4
+ title: "Landing hero — before / after",
5
+ a: {
6
+ src: "/media/hero-v1.jpg",
7
+ label: "Before",
8
+ alt: "First landing hero, centered text over a flat color band",
9
+ },
10
+ b: {
11
+ src: "/media/hero-v2.jpg",
12
+ label: "After",
13
+ alt: "Revised landing hero, left-aligned text over product photography",
14
+ },
15
+ startPercent: 50,
16
+ orientation: "vertical",
17
+ height: 420,
18
+ pick: { question: "Which hero should ship?" },
19
+ };
20
+ const def = {
21
+ type: "compare",
22
+ title: "Compare (before/after slider)",
23
+ description: "Two media items (images or videos) revealed through one draggable divider — the classic before/after wipe. a sits underneath (shown when the divider is fully left/top), b is clipped on top and revealed toward the right/bottom. Every string is text (labels default to Before/After); media src is an https:// URL or a root-relative MOS path — never base64. On narrow containers (< 480px) the slider falls back to showing BOTH sides side-by-side with their labels, so a phone never hides half the comparison. The divider is a keyboard-accessible slider (arrow keys, Home/End, Shift = bigger step). Videos render muted + looping and start PAUSED — a shared play/pause runs both sides in sync while dragging; reduced-motion viewers get the same paused start. Optional pick: { question } adds an interactive A/B verdict persisted per viewer via ctx.saveState as { pick: 'a' | 'b', at } — it renders read-only (saved pick as a badge, no buttons) where saving is impossible. Tip: keep both sides the same aspect ratio so the wipe aligns; use one compare per idea and put the why in a neighboring markdown block, not in the labels.",
24
+ schema: compareSchema,
25
+ component: CompareBlock,
26
+ // mos-8fvt9: the section's Listen + comment sit on the heading row.
27
+ hostsTools: (p) => Boolean(p.title),
28
+ example: EXAMPLE,
29
+ interactive: true,
30
+ dataParam: "dataKey",
31
+ };
32
+ export default def;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Compare block — params schema + pure helpers (before/after slider).
3
+ *
4
+ * Pure functions only (no DOM) so `block.test.ts` can exercise them and
5
+ * `CompareBlock.svelte` stays a thin renderer. Untrusted input rules
6
+ * (CONTRACT.md): URLs go through the shared `urlSchema` (https:// or
7
+ * root-relative — never data:/javascript:/http://), every string is
8
+ * length-capped, media is by URL only (never base64).
9
+ */
10
+ import { z } from "zod";
11
+ declare const COMPARE_SIDES: readonly ["a", "b"];
12
+ export type CompareSide = (typeof COMPARE_SIDES)[number];
13
+ export type MediaKind = "image" | "video";
14
+ export declare const compareSchema: z.ZodObject<{
15
+ title: z.ZodOptional<z.ZodString>;
16
+ a: z.ZodObject<{
17
+ src: z.ZodString;
18
+ kind: z.ZodOptional<z.ZodEnum<{
19
+ video: "video";
20
+ image: "image";
21
+ }>>;
22
+ label: z.ZodOptional<z.ZodString>;
23
+ alt: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strip>;
25
+ b: z.ZodObject<{
26
+ src: z.ZodString;
27
+ kind: z.ZodOptional<z.ZodEnum<{
28
+ video: "video";
29
+ image: "image";
30
+ }>>;
31
+ label: z.ZodOptional<z.ZodString>;
32
+ alt: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>;
34
+ startPercent: z.ZodDefault<z.ZodNumber>;
35
+ orientation: z.ZodDefault<z.ZodEnum<{
36
+ horizontal: "horizontal";
37
+ vertical: "vertical";
38
+ }>>;
39
+ height: z.ZodDefault<z.ZodNumber>;
40
+ pick: z.ZodOptional<z.ZodObject<{
41
+ question: z.ZodString;
42
+ }, z.core.$strip>>;
43
+ dataKey: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strip>;
45
+ export type CompareParams = z.infer<typeof compareSchema>;
46
+ export type CompareMedia = CompareParams["a"];
47
+ /** The renderer kind for a side: explicit param wins, else the extension. */
48
+ export declare function kindOf(media: CompareMedia): MediaKind;
49
+ /** Effective chip label for a side: authored, else Before/After. */
50
+ export declare function labelOf(media: CompareMedia, side: CompareSide): string;
51
+ /** Effective alt text: authored alt, else the chip label, else the side. */
52
+ export declare function altOf(media: CompareMedia, side: CompareSide): string;
53
+ /** Clamp a divider position into [DIVIDER_MIN, DIVIDER_MAX]. */
54
+ export declare function clampPercent(p: number): number;
55
+ /** Next divider position for a key press (undefined = key does nothing). */
56
+ export declare function stepFor(key: string, orientation: "vertical" | "horizontal", shift: boolean, current: number): number | undefined;
57
+ /** Accessible description of the divider position for aria-valuetext. */
58
+ export declare function dividerValueText(percent: number, labels: Record<CompareSide, string>): string;
59
+ /**
60
+ * clip-path insets that reveal side B from the right (vertical divider) or
61
+ * the bottom (horizontal) while side A sits fully underneath. Both sides
62
+ * always render; the clip does the reveal.
63
+ */
64
+ export declare function clipFor(percent: number, orientation: "vertical" | "horizontal"): string;
65
+ /** State saved through ctx.saveState for the pick verdict. */
66
+ export interface CompareState {
67
+ /** Chosen side, when the viewer has picked. */
68
+ pick?: CompareSide;
69
+ /** ISO timestamp of the last save. */
70
+ at?: string;
71
+ }
72
+ /** Read untrusted saved state into a pick — junk in, null out. */
73
+ export declare function parsePick(state: unknown): CompareSide | null;
74
+ export {};