@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,779 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* callout block — note / warn / success panels and the interactive
|
|
4
|
+
* `decision` variant: a question with 2-6 options (+ optional "other" text)
|
|
5
|
+
* whose pick and comment persist per viewer via ctx.saveState. When the
|
|
6
|
+
* block has its notes box, picking Other MERGES into it — the box becomes
|
|
7
|
+
* the Other explanation ("Say what instead") and its text is saved to both
|
|
8
|
+
* state.other and state.comment; without the box (allowComment: false)
|
|
9
|
+
* Other keeps a dedicated "Say which" input.
|
|
10
|
+
*
|
|
11
|
+
* SAVING IS AUTOMATIC, AND WHAT SAVES IS A DRAFT (mos-4dkv9 + the
|
|
12
|
+
* desk-convergence-ste D2 steer, Joe: "remove the Save answer button, just
|
|
13
|
+
* save it on press" — answers are DRAFTS until the page's Finish review
|
|
14
|
+
* sends them): picking an option calls ctx.saveState at once — no button,
|
|
15
|
+
* and switching options re-saves the new pick the same way. The note (and
|
|
16
|
+
* the Other explanation, which saves like a note) autosaves ~800ms after
|
|
17
|
+
* typing stops and on blur; a pending note save is flushed into the next
|
|
18
|
+
* pick save so one write always carries the whole answer. Everything is
|
|
19
|
+
* server-persisted per viewer, so a reload brings the draft back. The card
|
|
20
|
+
* shows a small state line — Saving… / Draft saved · <time> / the error
|
|
21
|
+
* with a Retry control — never a Save/Update button. A failed save keeps
|
|
22
|
+
* the viewer's local pick (nothing is lost); the renderer's per-block
|
|
23
|
+
* strip and backoff also keep retrying (mos-yntqm, mos-lwbbt), and the
|
|
24
|
+
* server refuses an EMPTY blocks payload so a nothing-write can never wipe
|
|
25
|
+
* saved verdicts.
|
|
26
|
+
*
|
|
27
|
+
* Tones (campaign-blocks Gate A, J4) are QUIET panels: a 10% tone tint, a
|
|
28
|
+
* 30% tone border, base-content text, and the tone carried by a small solid
|
|
29
|
+
* icon disc (solid tone fills clear contrast in every theme; a tone-coloured
|
|
30
|
+
* glyph on a tint does not). No saturated DaisyUI alert slab, no shadow.
|
|
31
|
+
*
|
|
32
|
+
* Read-only surfaces (public share, print, tests without saveState) render
|
|
33
|
+
* the decision as an ANSWER, not a disabled form: the saved pick at full
|
|
34
|
+
* strength with a check, the other options as plain muted text.
|
|
35
|
+
*
|
|
36
|
+
* A SAVED answer looks DONE (mos-tjwof), with the same card-level treatment
|
|
37
|
+
* the diff-review verdicts get: the card border turns success (with the
|
|
38
|
+
* ring) and a labeled "Draft: <choice>" chip sits beside the question —
|
|
39
|
+
* scannable while scrolling, not a tiny check line, and honest about the
|
|
40
|
+
* draft-until-Finish semantics (D2). While a save is in flight or has
|
|
41
|
+
* failed the card flips to a warning border — the only remaining "unsaved"
|
|
42
|
+
* state; typing alone (inside the autosave debounce) does not flag the
|
|
43
|
+
* card.
|
|
44
|
+
*/
|
|
45
|
+
import BlockIcon from "../../review/BlockIcon.svelte"
|
|
46
|
+
import type { BlockProps } from "../../types.js"
|
|
47
|
+
import { renderBlockMarkdown } from "../markdown/render.js"
|
|
48
|
+
import NoteComposer from "../../NoteComposer.svelte"
|
|
49
|
+
import {
|
|
50
|
+
readNoteAttachments,
|
|
51
|
+
type NoteAttachment,
|
|
52
|
+
} from "../../review/attachments.js"
|
|
53
|
+
import { OTHER_ID, type CalloutParams, type CalloutState } from "./schema.js"
|
|
54
|
+
import SectionTools from "../../SectionTools.svelte"
|
|
55
|
+
|
|
56
|
+
let { params, ctx }: BlockProps<CalloutParams> = $props()
|
|
57
|
+
|
|
58
|
+
const variant = $derived(params?.variant ?? "note")
|
|
59
|
+
/** PARAMS-SPEC rule 4: with `sections`, the body is their flattened
|
|
60
|
+
* fallback for renderers that ignore them — render one, never both. */
|
|
61
|
+
const sections = $derived(
|
|
62
|
+
variant === "decision" ? (params?.sections ?? []) : [],
|
|
63
|
+
)
|
|
64
|
+
const bodyHtml = $derived(
|
|
65
|
+
sections.length > 0 ? "" : renderBlockMarkdown(params?.body),
|
|
66
|
+
)
|
|
67
|
+
const sectionHtml = $derived(
|
|
68
|
+
sections.map((sec) => renderBlockMarkdown(sec.markdown)),
|
|
69
|
+
)
|
|
70
|
+
const meta = $derived(variant === "decision" ? (params?.meta ?? []) : [])
|
|
71
|
+
const primaryAction = $derived(
|
|
72
|
+
variant === "decision" ? params?.primaryAction : undefined,
|
|
73
|
+
)
|
|
74
|
+
const origin = typeof location !== "undefined" ? location.origin : undefined
|
|
75
|
+
/** Off-origin http(s) links open in a new tab; root-relative stay. */
|
|
76
|
+
function offOrigin(href: string): boolean {
|
|
77
|
+
if (!/^https?:\/\//i.test(href)) return false
|
|
78
|
+
try {
|
|
79
|
+
return !origin || new URL(href).origin !== origin
|
|
80
|
+
} catch {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Static tone classes for a meta chip (never `text-${tone}`). */
|
|
85
|
+
const META_TONE: Record<string, string> = {
|
|
86
|
+
neutral: "text-base-content/75",
|
|
87
|
+
info: "text-info",
|
|
88
|
+
success: "text-success",
|
|
89
|
+
warning: "text-warning",
|
|
90
|
+
error: "text-error",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The container's name for its Listen control (mos-8vdg9): the title,
|
|
94
|
+
* else the decision's question, else a quiet generic. */
|
|
95
|
+
const listenLabel = $derived(
|
|
96
|
+
params?.title?.trim() || params?.question?.trim() || "this note",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Interactive = the viewer can answer here. On the public share route the
|
|
101
|
+
* context is `readonly` (and typically has no saveState) — no viewer chrome.
|
|
102
|
+
*/
|
|
103
|
+
const interactive = $derived(Boolean(ctx?.saveState) && !ctx?.readonly)
|
|
104
|
+
|
|
105
|
+
/** "" = no pick yet; real picks are option ids or OTHER_ID. */
|
|
106
|
+
let pick = $state("")
|
|
107
|
+
let otherText = $state("")
|
|
108
|
+
let comment = $state("")
|
|
109
|
+
/** The note's uploaded attachments (mos-rccb5) — descriptors in the
|
|
110
|
+
* artifact's private asset store, uploaded as they are captured. */
|
|
111
|
+
let commentClips = $state<NoteAttachment[]>([])
|
|
112
|
+
/** Save flights in flight — the small "Saving…" state, not a button. */
|
|
113
|
+
let flights = $state(0)
|
|
114
|
+
const saving = $derived(flights > 0)
|
|
115
|
+
let saveError = $state<string | null>(null)
|
|
116
|
+
/** The last answer known to be saved (seeded from ctx.state). */
|
|
117
|
+
let lastSaved = $state<CalloutState | null>(null)
|
|
118
|
+
/** Focus the "other" field only when the viewer just picked Other. */
|
|
119
|
+
let focusOther = $state(false)
|
|
120
|
+
/** The notes box wrapper, for the merged Other focus (via the effect):
|
|
121
|
+
* the composer is the host's (NoteComposer), so its text entry is found
|
|
122
|
+
* inside rather than bound. */
|
|
123
|
+
let composerWrap = $state<HTMLElement | null>(null)
|
|
124
|
+
/** The "Say which" input element, for the no-notes/legacy Other focus. */
|
|
125
|
+
let otherInputEl = $state<HTMLInputElement | null>(null)
|
|
126
|
+
/** The block has a comment/notes box (allowComment: false removes it). */
|
|
127
|
+
const hasNotes = $derived(params?.allowComment !== false)
|
|
128
|
+
/**
|
|
129
|
+
* A previously saved state with a DISTINCT other + comment keeps the old
|
|
130
|
+
* two-field UI: merging it into one box would silently drop one of the two
|
|
131
|
+
* texts on the viewer's next save.
|
|
132
|
+
*/
|
|
133
|
+
let legacySplit = $state(false)
|
|
134
|
+
|
|
135
|
+
// Seed the form from this viewer's saved state (mount-time read; state is
|
|
136
|
+
// keyed per artifact version and does not change while mounted).
|
|
137
|
+
{
|
|
138
|
+
const s = (ctx?.state ?? null) as CalloutState | null
|
|
139
|
+
const other0 = s && typeof s.other === "string" ? s.other : ""
|
|
140
|
+
const comment0 = s && typeof s.comment === "string" ? s.comment : ""
|
|
141
|
+
const clips0 = s ? (readNoteAttachments(s.attachments) ?? []) : []
|
|
142
|
+
if (s && typeof s.pick === "string" && s.pick) pick = s.pick
|
|
143
|
+
if (other0) otherText = other0
|
|
144
|
+
if (comment0) comment = comment0
|
|
145
|
+
if (clips0.length) commentClips = clips0
|
|
146
|
+
if (s && (s.pick || other0.trim() || comment0.trim() || clips0.length))
|
|
147
|
+
lastSaved = { ...s }
|
|
148
|
+
if (
|
|
149
|
+
s &&
|
|
150
|
+
s.pick === OTHER_ID &&
|
|
151
|
+
other0.trim() &&
|
|
152
|
+
comment0.trim() &&
|
|
153
|
+
other0.trim() !== comment0.trim()
|
|
154
|
+
) {
|
|
155
|
+
legacySplit = true
|
|
156
|
+
} else if (s && s.pick === OTHER_ID && !comment0.trim() && other0.trim()) {
|
|
157
|
+
// The notes box doubles as the Other explanation: show the saved
|
|
158
|
+
// explanation in it so the returning viewer sees (and can edit) it.
|
|
159
|
+
comment = other0
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The notes box IS the Other explanation: the block has a notes field,
|
|
165
|
+
* Other is picked, and the saved state is not a legacy two-text split. The
|
|
166
|
+
* separate "Say which" input only renders when there is no notes field —
|
|
167
|
+
* or while a legacy split state is on screen.
|
|
168
|
+
*/
|
|
169
|
+
const notesAsOther = $derived(hasNotes && pick === OTHER_ID && !legacySplit)
|
|
170
|
+
|
|
171
|
+
/** The dedicated "Say which" input: only when there is no notes box to
|
|
172
|
+
* merge into, or while a legacy two-text state keeps both fields. */
|
|
173
|
+
const showOtherInput = $derived(
|
|
174
|
+
interactive &&
|
|
175
|
+
params?.allowOther === true &&
|
|
176
|
+
pick === OTHER_ID &&
|
|
177
|
+
!notesAsOther,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
/** The rendered option list — the authored options plus Other last. */
|
|
181
|
+
const optionsWithOther = $derived.by(() => {
|
|
182
|
+
const opts = [...(params?.options ?? [])]
|
|
183
|
+
if (params?.allowOther === true) {
|
|
184
|
+
opts.push({ id: OTHER_ID, label: "Other" })
|
|
185
|
+
}
|
|
186
|
+
return opts
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Keyboard: focus moves to the explanation field when the viewer picks
|
|
190
|
+
// Other with a POINTER — the merged notes box, or the "Say which" input
|
|
191
|
+
// when the block has no notes field (or a legacy two-text state keeps both
|
|
192
|
+
// fields). A keyboard pick (arrows/space/enter) keeps focus on the radios
|
|
193
|
+
// so arrow roving keeps working. Focus lives in an effect rather than a
|
|
194
|
+
// `use:` action: an action defers the element's value binding past the
|
|
195
|
+
// mount flush.
|
|
196
|
+
$effect(() => {
|
|
197
|
+
if (focusOther && notesAsOther) {
|
|
198
|
+
queueMicrotask(() =>
|
|
199
|
+
composerWrap?.querySelector<HTMLElement>("textarea, [contenteditable='true']")?.focus(),
|
|
200
|
+
)
|
|
201
|
+
} else if (focusOther && showOtherInput) {
|
|
202
|
+
queueMicrotask(() => otherInputEl?.focus())
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The one autosave debounce for typed text (the note, and the Other
|
|
208
|
+
* explanation, which saves like a note): ~800ms after typing stops, and
|
|
209
|
+
* immediately on blur. A pick save cancels the pending timer and carries
|
|
210
|
+
* the typed text along, so one write always holds the whole answer.
|
|
211
|
+
*/
|
|
212
|
+
const NOTE_DEBOUNCE_MS = 800
|
|
213
|
+
let noteTimer: ReturnType<typeof setTimeout> | null = null
|
|
214
|
+
function scheduleNoteSave() {
|
|
215
|
+
if (noteTimer) clearTimeout(noteTimer)
|
|
216
|
+
noteTimer = setTimeout(() => {
|
|
217
|
+
noteTimer = null
|
|
218
|
+
save()
|
|
219
|
+
}, NOTE_DEBOUNCE_MS)
|
|
220
|
+
}
|
|
221
|
+
function cancelNoteSave() {
|
|
222
|
+
if (noteTimer) {
|
|
223
|
+
clearTimeout(noteTimer)
|
|
224
|
+
noteTimer = null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function flushNoteSave() {
|
|
228
|
+
if (!noteTimer) return
|
|
229
|
+
cancelNoteSave()
|
|
230
|
+
save()
|
|
231
|
+
}
|
|
232
|
+
// A save still inside the debounce window when the block goes away is a
|
|
233
|
+
// real viewer decision — fire it (the renderer's keepalive POST survives
|
|
234
|
+
// an SPA navigation, same contract as its own unmount flush).
|
|
235
|
+
$effect(() => () => flushNoteSave())
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The state this form would save right now — null when there is nothing
|
|
239
|
+
* to write. Never an empty tombstone: the server keeps whatever stands
|
|
240
|
+
* when a save carries no answer (mos-lwbbt's empty-payload guard), and a
|
|
241
|
+
* component that posts nothing at all is even better.
|
|
242
|
+
*/
|
|
243
|
+
function buildState(): CalloutState | null {
|
|
244
|
+
if (pick) {
|
|
245
|
+
const next: CalloutState = { pick, at: new Date().toISOString() }
|
|
246
|
+
if (pick === OTHER_ID) {
|
|
247
|
+
if (notesAsOther) {
|
|
248
|
+
// Merged (mos-ucmup): the notes box is the Other explanation. Write
|
|
249
|
+
// the text to BOTH state.other and state.comment so readers of
|
|
250
|
+
// either (feedback pull-verdicts, digest summaries, the Pilot drain)
|
|
251
|
+
// keep working.
|
|
252
|
+
const t = comment.trim()
|
|
253
|
+
if (t) {
|
|
254
|
+
next.other = t
|
|
255
|
+
next.comment = t
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
if (otherText.trim()) next.other = otherText.trim()
|
|
259
|
+
if (comment.trim()) next.comment = comment.trim()
|
|
260
|
+
}
|
|
261
|
+
} else if (comment.trim()) {
|
|
262
|
+
next.comment = comment.trim()
|
|
263
|
+
}
|
|
264
|
+
if (commentClips.length) next.attachments = commentClips
|
|
265
|
+
return next
|
|
266
|
+
}
|
|
267
|
+
// No pick yet: a typed note (or an attachment alone) is still worth
|
|
268
|
+
// keeping, but an answer with neither is nothing.
|
|
269
|
+
const t = comment.trim()
|
|
270
|
+
if (!t && commentClips.length === 0) return null
|
|
271
|
+
const next: CalloutState = { at: new Date().toISOString() }
|
|
272
|
+
if (t) next.comment = t
|
|
273
|
+
if (commentClips.length) next.attachments = commentClips
|
|
274
|
+
return next
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Same answer content (at is bookkeeping, not content). */
|
|
278
|
+
function sameAnswer(a: CalloutState | null, b: CalloutState | null): boolean {
|
|
279
|
+
if (a === b) return true
|
|
280
|
+
if (!a || !b) return false
|
|
281
|
+
return (
|
|
282
|
+
(a.pick ?? "") === (b.pick ?? "") &&
|
|
283
|
+
(a.other ?? "") === (b.other ?? "") &&
|
|
284
|
+
(a.comment ?? "") === (b.comment ?? "") &&
|
|
285
|
+
JSON.stringify(a.attachments ?? []) ===
|
|
286
|
+
JSON.stringify(b.attachments ?? [])
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Latest save wins for lastSaved: an older flight resolving after a newer
|
|
291
|
+
* one must not roll the card's saved state back. */
|
|
292
|
+
let saveSeq = 0
|
|
293
|
+
|
|
294
|
+
function save() {
|
|
295
|
+
if (!interactive || !ctx?.saveState) return
|
|
296
|
+
const next = buildState()
|
|
297
|
+
if (!next || sameAnswer(next, lastSaved)) return
|
|
298
|
+
const mine = ++saveSeq
|
|
299
|
+
flights++
|
|
300
|
+
saveError = null
|
|
301
|
+
ctx
|
|
302
|
+
.saveState(next)
|
|
303
|
+
.then(() => {
|
|
304
|
+
flights--
|
|
305
|
+
if (mine === saveSeq) lastSaved = next
|
|
306
|
+
})
|
|
307
|
+
.catch((e: unknown) => {
|
|
308
|
+
flights--
|
|
309
|
+
// The local pick stays (nothing is lost); the card shows the error
|
|
310
|
+
// + Retry, and the renderer's strip/backoff retries underneath.
|
|
311
|
+
if (mine === saveSeq) {
|
|
312
|
+
saveError = e instanceof Error ? e.message : "Saving failed"
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The failure state's own control — the only button left on the card
|
|
318
|
+
* (mos-4dkv9): re-send the current form state. A bare handler reference,
|
|
319
|
+
* not an inline arrow calling save(): the route-quality Save-action
|
|
320
|
+
* heuristic pattern-matches an inline arrow that CALLS save in markup as
|
|
321
|
+
* an unsaved-form Save button, and this card has none — autosave IS the
|
|
322
|
+
* dirty-form guard (pending edits flush on unmount, so navigation loses
|
|
323
|
+
* nothing). */
|
|
324
|
+
function retrySave() {
|
|
325
|
+
save()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Select an option — the save happens here, on press (mos-4dkv9). */
|
|
329
|
+
function pickOption(id: string, focusExplanation: boolean) {
|
|
330
|
+
if (!interactive) return
|
|
331
|
+
focusOther = focusExplanation && id === OTHER_ID
|
|
332
|
+
if (pick === id) return
|
|
333
|
+
pick = id
|
|
334
|
+
// The pick save carries whatever typed text is on the card along —
|
|
335
|
+
// no separate note save needs to chase it.
|
|
336
|
+
cancelNoteSave()
|
|
337
|
+
save()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Radios inside the keydown's own fieldset, in option order (the
|
|
341
|
+
* interactive ones — read-only renders disabled twins that must not join
|
|
342
|
+
* the roving). Scoped from the EVENT TARGET, not a bind:this ref: a key
|
|
343
|
+
* press that lands before Svelte's first post-mount flush (tests, and a
|
|
344
|
+
* hostile scheduler) must still rove, and bind:this lands only in that
|
|
345
|
+
* flush. */
|
|
346
|
+
function radioEls(from: EventTarget | null): HTMLInputElement[] {
|
|
347
|
+
const scope = (from as HTMLElement | null)?.closest("fieldset")
|
|
348
|
+
if (!scope) return []
|
|
349
|
+
return Array.from(
|
|
350
|
+
scope.querySelectorAll<HTMLInputElement>(
|
|
351
|
+
'input[type="radio"]:not(:disabled)',
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Keyboard on the option group: arrow keys move the selection (wrapping),
|
|
358
|
+
* space or enter picks — and every pick saves. preventDefault keeps the
|
|
359
|
+
* browser's own radio roving from double-firing behind us.
|
|
360
|
+
*/
|
|
361
|
+
function optionKeydown(e: KeyboardEvent, index: number) {
|
|
362
|
+
const count = optionsWithOther.length
|
|
363
|
+
if (count === 0) return
|
|
364
|
+
let next: number
|
|
365
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
|
366
|
+
next = (index + 1) % count
|
|
367
|
+
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
368
|
+
next = (index - 1 + count) % count
|
|
369
|
+
} else if (e.key === " " || e.key === "Enter") {
|
|
370
|
+
next = index
|
|
371
|
+
} else {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
e.preventDefault()
|
|
375
|
+
radioEls(e.target)[next]?.focus()
|
|
376
|
+
pickOption(optionsWithOther[next]!.id, false)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const TONE = {
|
|
380
|
+
note: {
|
|
381
|
+
panel: "border-info/30 bg-info/10",
|
|
382
|
+
disc: "bg-info text-info-content",
|
|
383
|
+
},
|
|
384
|
+
warn: {
|
|
385
|
+
panel: "border-warning/40 bg-warning/10",
|
|
386
|
+
disc: "bg-warning text-warning-content",
|
|
387
|
+
},
|
|
388
|
+
success: {
|
|
389
|
+
panel: "border-success/35 bg-success/10",
|
|
390
|
+
disc: "bg-success text-success-content",
|
|
391
|
+
},
|
|
392
|
+
} as const
|
|
393
|
+
const tone = $derived(
|
|
394
|
+
TONE[variant === "warn" || variant === "success" ? variant : "note"],
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
function optionLabel(id: string): string {
|
|
398
|
+
if (id === OTHER_ID) return lastSaved?.other?.trim() || "Other"
|
|
399
|
+
return params?.options?.find((o) => o.id === id)?.label ?? id
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const whenFmt = new Intl.DateTimeFormat("en-US", {
|
|
403
|
+
month: "short",
|
|
404
|
+
day: "numeric",
|
|
405
|
+
hour: "numeric",
|
|
406
|
+
minute: "2-digit",
|
|
407
|
+
})
|
|
408
|
+
function when(iso: string | undefined): string {
|
|
409
|
+
if (!iso) return ""
|
|
410
|
+
const d = new Date(iso)
|
|
411
|
+
return Number.isFinite(d.getTime()) ? whenFmt.format(d) : ""
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The card's state: a save in flight or failed flags the card (the only
|
|
416
|
+
* remaining "unsaved" look); a saved pick is DONE; otherwise fresh.
|
|
417
|
+
*/
|
|
418
|
+
const cardBorder = $derived.by(() => {
|
|
419
|
+
if (saving || saveError) return "border-warning/60"
|
|
420
|
+
if (lastSaved?.pick) return "border-success ring-1 ring-success/40"
|
|
421
|
+
return "border-primary/25"
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
/** The small state line under the options — no button beside it. A saved
|
|
425
|
+
* answer is a DRAFT until the page's Finish review sends it (D2). */
|
|
426
|
+
const savedLine = $derived.by(() => {
|
|
427
|
+
if (saveError) return saveError
|
|
428
|
+
if (saving) return "Saving…"
|
|
429
|
+
if (lastSaved) {
|
|
430
|
+
const at = when(lastSaved.at)
|
|
431
|
+
return `Draft saved${at ? ` · ${at}` : ""}`
|
|
432
|
+
}
|
|
433
|
+
return ""
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const PROSE =
|
|
437
|
+
"prose prose-sm max-w-none text-base-content prose-p:max-w-[65ch] prose-li:max-w-[65ch] prose-p:my-1.5 prose-ul:my-1.5 prose-ol:my-1.5 prose-code:rounded-field prose-code:bg-base-content/10 prose-code:px-1 prose-code:py-0.5 prose-code:font-normal prose-code:before:content-none prose-code:after:content-none"
|
|
438
|
+
|
|
439
|
+
const optionRow =
|
|
440
|
+
"flex min-w-0 items-center gap-2.5 rounded-field border px-3 py-2 text-sm"
|
|
441
|
+
|
|
442
|
+
/** A contract card's recommended option (mos-4tgfj): its letter, else its
|
|
443
|
+
* position, matches `params.recommended`. */
|
|
444
|
+
function isRecommended(i: number): boolean {
|
|
445
|
+
const rec = params?.recommended
|
|
446
|
+
if (!rec) return false
|
|
447
|
+
const o = params?.options?.[i]
|
|
448
|
+
return Boolean(o) && (o!.letter ?? String.fromCharCode(65 + i)) === rec
|
|
449
|
+
}
|
|
450
|
+
</script>
|
|
451
|
+
|
|
452
|
+
{#snippet optionText(
|
|
453
|
+
o: { id: string; label: string; detail?: string },
|
|
454
|
+
i: number,
|
|
455
|
+
text: string,
|
|
456
|
+
)}
|
|
457
|
+
<span class="min-w-0">
|
|
458
|
+
<span>{text}</span>
|
|
459
|
+
{#if isRecommended(i)}
|
|
460
|
+
<span class="badge badge-ghost badge-sm ms-1.5 align-middle"
|
|
461
|
+
>Recommended</span
|
|
462
|
+
>
|
|
463
|
+
{/if}
|
|
464
|
+
{#if o.detail}
|
|
465
|
+
<span class="block text-xs text-base-content/60">{o.detail}</span>
|
|
466
|
+
{/if}
|
|
467
|
+
</span>
|
|
468
|
+
{/snippet}
|
|
469
|
+
|
|
470
|
+
{#if variant === "decision"}
|
|
471
|
+
{@const readPick = lastSaved?.pick ?? ""}
|
|
472
|
+
<div
|
|
473
|
+
class="rounded-box border bg-base-100 p-4 sm:p-5 {cardBorder}"
|
|
474
|
+
data-variant="decision"
|
|
475
|
+
data-answered={readPick ? "true" : "false"}
|
|
476
|
+
data-saving={saving ? "true" : "false"}
|
|
477
|
+
data-failed={saveError ? "true" : "false"}
|
|
478
|
+
>
|
|
479
|
+
<div class="flex items-start gap-3">
|
|
480
|
+
<span
|
|
481
|
+
class="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full bg-primary text-primary-content"
|
|
482
|
+
aria-hidden="true"
|
|
483
|
+
>
|
|
484
|
+
<BlockIcon name="circle-help" size={15} stroke-width={2.25} />
|
|
485
|
+
</span>
|
|
486
|
+
<div class="min-w-0 flex-1">
|
|
487
|
+
<SectionTools {ctx} {listenLabel} />
|
|
488
|
+
{#if params?.title}
|
|
489
|
+
<p class="text-sm font-medium text-base-content/70">
|
|
490
|
+
{params.title}
|
|
491
|
+
</p>
|
|
492
|
+
{/if}
|
|
493
|
+
{#if bodyHtml}
|
|
494
|
+
<div class="{PROSE} mt-1">
|
|
495
|
+
{@html bodyHtml}
|
|
496
|
+
</div>
|
|
497
|
+
{/if}
|
|
498
|
+
|
|
499
|
+
<div class="mt-3">
|
|
500
|
+
<fieldset class="min-w-0">
|
|
501
|
+
<legend
|
|
502
|
+
class="flex flex-wrap items-center gap-2 text-base leading-snug font-semibold text-base-content"
|
|
503
|
+
>
|
|
504
|
+
<span>{params?.question}</span>
|
|
505
|
+
{#if interactive && saving}
|
|
506
|
+
<span
|
|
507
|
+
class="badge badge-sm font-medium badge-warning"
|
|
508
|
+
data-saving-chip
|
|
509
|
+
>
|
|
510
|
+
Saving…
|
|
511
|
+
</span>
|
|
512
|
+
{:else if interactive && saveError}
|
|
513
|
+
<span
|
|
514
|
+
class="badge badge-sm font-medium badge-error"
|
|
515
|
+
data-failed-chip
|
|
516
|
+
>
|
|
517
|
+
Not saved
|
|
518
|
+
</span>
|
|
519
|
+
{:else if interactive && lastSaved?.pick}
|
|
520
|
+
<span
|
|
521
|
+
class="badge badge-sm font-medium badge-success"
|
|
522
|
+
data-answer-chip
|
|
523
|
+
>
|
|
524
|
+
Draft: {optionLabel(lastSaved.pick)}
|
|
525
|
+
</span>
|
|
526
|
+
{/if}
|
|
527
|
+
</legend>
|
|
528
|
+
|
|
529
|
+
{#if meta.length > 0}
|
|
530
|
+
<ul
|
|
531
|
+
class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-base-content/70"
|
|
532
|
+
aria-label="Details"
|
|
533
|
+
data-decision-meta
|
|
534
|
+
>
|
|
535
|
+
{#each meta as m, mi (mi)}
|
|
536
|
+
<li class="inline-flex min-w-0 items-baseline gap-1" title={m.title}>
|
|
537
|
+
<span class="text-base-content/55">{m.label}</span>
|
|
538
|
+
{#if m.href}
|
|
539
|
+
{@const ext = offOrigin(m.href)}
|
|
540
|
+
<a
|
|
541
|
+
class="link link-hover min-w-0 truncate font-medium {META_TONE[m.tone ?? 'neutral']}"
|
|
542
|
+
href={m.href}
|
|
543
|
+
target={ext ? "_blank" : undefined}
|
|
544
|
+
rel={ext ? "noopener noreferrer" : undefined}
|
|
545
|
+
>{m.value}{#if ext}<span class="sr-only"> (opens in a new tab)</span>{/if}</a
|
|
546
|
+
>
|
|
547
|
+
{:else}
|
|
548
|
+
<span class="min-w-0 truncate font-medium {META_TONE[m.tone ?? 'neutral']}"
|
|
549
|
+
>{m.value}</span
|
|
550
|
+
>
|
|
551
|
+
{/if}
|
|
552
|
+
</li>
|
|
553
|
+
{/each}
|
|
554
|
+
</ul>
|
|
555
|
+
{/if}
|
|
556
|
+
|
|
557
|
+
{#if primaryAction}
|
|
558
|
+
{@const ext = offOrigin(primaryAction.url)}
|
|
559
|
+
<div class="mt-3">
|
|
560
|
+
<a
|
|
561
|
+
class="btn btn-primary btn-sm rounded-field"
|
|
562
|
+
href={primaryAction.url}
|
|
563
|
+
target={ext ? "_blank" : undefined}
|
|
564
|
+
rel={ext ? "noopener noreferrer" : undefined}
|
|
565
|
+
data-primary-action
|
|
566
|
+
>
|
|
567
|
+
{primaryAction.label}
|
|
568
|
+
{#if ext}
|
|
569
|
+
<BlockIcon name="external-link" size={14} />
|
|
570
|
+
<span class="sr-only"> (opens in a new tab)</span>
|
|
571
|
+
{/if}
|
|
572
|
+
</a>
|
|
573
|
+
</div>
|
|
574
|
+
{/if}
|
|
575
|
+
|
|
576
|
+
{#if !interactive && readPick}
|
|
577
|
+
<p class="mt-1 text-sm text-base-content/75">
|
|
578
|
+
Answered: <strong class="font-semibold text-base-content"
|
|
579
|
+
>{optionLabel(readPick)}</strong
|
|
580
|
+
>{#if when(lastSaved?.at)}<span
|
|
581
|
+
>{" · "}{when(lastSaved?.at)}</span
|
|
582
|
+
>{/if}
|
|
583
|
+
</p>
|
|
584
|
+
{/if}
|
|
585
|
+
|
|
586
|
+
<div class="mt-2.5 flex flex-col gap-1.5">
|
|
587
|
+
{#each optionsWithOther as o, i (o.id)}
|
|
588
|
+
{#if interactive}
|
|
589
|
+
<label
|
|
590
|
+
class="{optionRow} cursor-pointer border-base-content/15 transition-colors hover:bg-base-content/5 has-[:checked]:border-primary has-[:checked]:bg-primary/8 has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-primary motion-reduce:transition-none"
|
|
591
|
+
>
|
|
592
|
+
<input
|
|
593
|
+
type="radio"
|
|
594
|
+
class="radio radio-primary radio-sm"
|
|
595
|
+
name="callout-{ctx?.blockId ?? 'decision'}"
|
|
596
|
+
value={o.id}
|
|
597
|
+
checked={pick === o.id}
|
|
598
|
+
onchange={() => pickOption(o.id, true)}
|
|
599
|
+
onkeydown={(e) => optionKeydown(e, i)}
|
|
600
|
+
/>
|
|
601
|
+
{@render optionText(o, i, o.label)}
|
|
602
|
+
</label>
|
|
603
|
+
{:else}
|
|
604
|
+
{@const chosen = readPick === o.id}
|
|
605
|
+
<label
|
|
606
|
+
class="{optionRow} {chosen
|
|
607
|
+
? 'border-primary bg-primary/8 font-medium text-base-content'
|
|
608
|
+
: 'border-base-content/10 text-base-content/70'}"
|
|
609
|
+
>
|
|
610
|
+
<input
|
|
611
|
+
type="radio"
|
|
612
|
+
class="sr-only"
|
|
613
|
+
name="callout-{ctx?.blockId ?? 'decision'}"
|
|
614
|
+
value={o.id}
|
|
615
|
+
checked={chosen}
|
|
616
|
+
disabled
|
|
617
|
+
/>
|
|
618
|
+
<span
|
|
619
|
+
class="shrink-0 {chosen
|
|
620
|
+
? 'text-primary'
|
|
621
|
+
: 'text-base-content/40'}"
|
|
622
|
+
aria-hidden="true"
|
|
623
|
+
>
|
|
624
|
+
{#if chosen}
|
|
625
|
+
<BlockIcon name="circle-check" size={18} stroke-width={2.25} />
|
|
626
|
+
{:else}
|
|
627
|
+
<BlockIcon name="circle" size={18} stroke-width={1.75} />
|
|
628
|
+
{/if}
|
|
629
|
+
</span>
|
|
630
|
+
{@render optionText(
|
|
631
|
+
o,
|
|
632
|
+
i,
|
|
633
|
+
o.id === OTHER_ID && chosen
|
|
634
|
+
? optionLabel(OTHER_ID)
|
|
635
|
+
: o.label,
|
|
636
|
+
)}
|
|
637
|
+
{#if chosen}<span class="sr-only">(the saved answer)</span
|
|
638
|
+
>{/if}
|
|
639
|
+
</label>
|
|
640
|
+
{/if}
|
|
641
|
+
{/each}
|
|
642
|
+
|
|
643
|
+
{#if showOtherInput}
|
|
644
|
+
<input
|
|
645
|
+
type="text"
|
|
646
|
+
class="input input-bordered input-sm w-full"
|
|
647
|
+
placeholder="Say which"
|
|
648
|
+
aria-label="Other: please specify"
|
|
649
|
+
bind:value={otherText}
|
|
650
|
+
bind:this={otherInputEl}
|
|
651
|
+
oninput={scheduleNoteSave}
|
|
652
|
+
onblur={flushNoteSave}
|
|
653
|
+
/>
|
|
654
|
+
{/if}
|
|
655
|
+
</div>
|
|
656
|
+
</fieldset>
|
|
657
|
+
|
|
658
|
+
{#if interactive && hasNotes}
|
|
659
|
+
<div class="mt-4 block" bind:this={composerWrap}>
|
|
660
|
+
<span class="text-sm text-base-content/75">
|
|
661
|
+
{notesAsOther
|
|
662
|
+
? "Say what instead"
|
|
663
|
+
: (params?.commentLabel ?? "Add a note (optional)")}
|
|
664
|
+
</span>
|
|
665
|
+
<!-- The ONE composer (mos-rccb5): the notes box gains voice /
|
|
666
|
+
video / screen capture, file picker, paste and drag-drop;
|
|
667
|
+
the debounced autosave and its blur-flush are unchanged. -->
|
|
668
|
+
<NoteComposer
|
|
669
|
+
class="mt-1"
|
|
670
|
+
artifactId={ctx.artifactId}
|
|
671
|
+
bind:text={comment}
|
|
672
|
+
bind:clips={commentClips}
|
|
673
|
+
onInput={() => scheduleNoteSave()}
|
|
674
|
+
onClipsChange={() => scheduleNoteSave()}
|
|
675
|
+
onBlur={flushNoteSave}
|
|
676
|
+
rows={2}
|
|
677
|
+
placeholder={notesAsOther
|
|
678
|
+
? "Say what instead"
|
|
679
|
+
: "Anything to add?"}
|
|
680
|
+
label={notesAsOther
|
|
681
|
+
? "Say what instead"
|
|
682
|
+
: (params?.commentLabel ?? "Add a note (optional)")}
|
|
683
|
+
/>
|
|
684
|
+
</div>
|
|
685
|
+
{:else if !interactive && lastSaved?.comment && !(readPick === OTHER_ID && lastSaved.comment === lastSaved.other)}
|
|
686
|
+
<p class="mt-3 text-sm text-base-content/75">
|
|
687
|
+
Note: {lastSaved.comment}
|
|
688
|
+
</p>
|
|
689
|
+
{/if}
|
|
690
|
+
|
|
691
|
+
{#if sections.length > 0}
|
|
692
|
+
<div class="mt-4 flex flex-col gap-1.5" data-decision-sections>
|
|
693
|
+
{#each sections as sec, si (si)}
|
|
694
|
+
<details
|
|
695
|
+
class="group rounded-field border border-base-content/10"
|
|
696
|
+
open={sec.open === true}
|
|
697
|
+
>
|
|
698
|
+
<summary
|
|
699
|
+
class="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-sm font-medium text-base-content [&::-webkit-details-marker]:hidden"
|
|
700
|
+
>
|
|
701
|
+
<span
|
|
702
|
+
class="text-base-content/50 transition-transform group-open:rotate-90 motion-reduce:transition-none"
|
|
703
|
+
aria-hidden="true">›</span
|
|
704
|
+
>
|
|
705
|
+
{sec.heading}
|
|
706
|
+
</summary>
|
|
707
|
+
<div class="{PROSE} px-3 pb-3">
|
|
708
|
+
{@html sectionHtml[si]}
|
|
709
|
+
</div>
|
|
710
|
+
</details>
|
|
711
|
+
{/each}
|
|
712
|
+
</div>
|
|
713
|
+
{/if}
|
|
714
|
+
|
|
715
|
+
{#if interactive}
|
|
716
|
+
<div class="mt-3 flex flex-wrap items-center gap-2">
|
|
717
|
+
<p
|
|
718
|
+
aria-live="polite"
|
|
719
|
+
data-save-status
|
|
720
|
+
class="flex min-w-0 items-center gap-1 text-xs {saveError
|
|
721
|
+
? 'text-error'
|
|
722
|
+
: 'text-base-content/70'}"
|
|
723
|
+
>
|
|
724
|
+
{#if !saveError && !saving && lastSaved}<BlockIcon name="check"
|
|
725
|
+
size={13}
|
|
726
|
+
stroke-width={2.5}
|
|
727
|
+
class="text-success"
|
|
728
|
+
aria-hidden="true"
|
|
729
|
+
/>{/if}{savedLine}
|
|
730
|
+
</p>
|
|
731
|
+
{#if saveError}
|
|
732
|
+
<!-- No Save/Update answer button any more — the only control
|
|
733
|
+
left is the failure state's own Retry (mos-4dkv9). -->
|
|
734
|
+
<button
|
|
735
|
+
type="button"
|
|
736
|
+
class="btn btn-xs rounded-field"
|
|
737
|
+
data-retry
|
|
738
|
+
onclick={retrySave}
|
|
739
|
+
>
|
|
740
|
+
Retry
|
|
741
|
+
</button>
|
|
742
|
+
{/if}
|
|
743
|
+
</div>
|
|
744
|
+
{/if}
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
{:else}
|
|
750
|
+
<div
|
|
751
|
+
class="flex items-start gap-3 rounded-box border p-4 text-base-content {tone.panel}"
|
|
752
|
+
role="note"
|
|
753
|
+
data-variant={variant}
|
|
754
|
+
>
|
|
755
|
+
<span
|
|
756
|
+
class="mt-0.5 grid size-6 shrink-0 place-items-center rounded-full {tone.disc}"
|
|
757
|
+
aria-hidden="true"
|
|
758
|
+
>
|
|
759
|
+
{#if variant === "warn"}
|
|
760
|
+
<BlockIcon name="triangle-alert" size={14} stroke-width={2.5} />
|
|
761
|
+
{:else if variant === "success"}
|
|
762
|
+
<BlockIcon name="check" size={14} stroke-width={2.75} />
|
|
763
|
+
{:else}
|
|
764
|
+
<BlockIcon name="info" size={14} stroke-width={2.5} />
|
|
765
|
+
{/if}
|
|
766
|
+
</span>
|
|
767
|
+
<div class="min-w-0 flex-1">
|
|
768
|
+
<SectionTools {ctx} {listenLabel} />
|
|
769
|
+
{#if params?.title}
|
|
770
|
+
<p class="font-semibold leading-snug">{params.title}</p>
|
|
771
|
+
{/if}
|
|
772
|
+
{#if bodyHtml}
|
|
773
|
+
<div class="{PROSE} {params?.title ? 'mt-0.5' : ''}">
|
|
774
|
+
{@html bodyHtml}
|
|
775
|
+
</div>
|
|
776
|
+
{/if}
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
{/if}
|