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