@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.
- package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/dist/components/Button.svelte +8 -0
- package/dist/components/TextInput.svelte +39 -6
- package/dist/components/TextInput.svelte.d.ts +15 -7
- package/dist/components/basic/Checkbox.svelte +52 -14
- package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
- 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 {};
|