@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,1616 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* BlocksArtifactView (origin mos-0ninw; jatui-vsuzi) — a whole blocks page:
|
|
4
|
+
* header chrome, the live-data poller, the BlockRenderer and the review
|
|
5
|
+
* moment (Finish review / Done reviewing).
|
|
6
|
+
*
|
|
7
|
+
* Everything host-specific arrives through ONE object, `adapter`
|
|
8
|
+
* (types.ts HostAdapter): the crumb and desk/next slots, comments, the
|
|
9
|
+
* composer and its upload leg, the review send + delivery poll, history,
|
|
10
|
+
* live data, Listen and the block services. A missing piece degrades to
|
|
11
|
+
* read-only, loudly — never to a control that looks live and is not.
|
|
12
|
+
*
|
|
13
|
+
* Key this component by artifact id in the host ({#key artifactId}): the
|
|
14
|
+
* poller's lifecycle is per ARTIFACT, and a SvelteKit host reuses
|
|
15
|
+
* same-route components across an in-app navigation.
|
|
16
|
+
*
|
|
17
|
+
* Server-rendered data is the initial blob (no poll flash); the poller
|
|
18
|
+
* replaces it from the first tick onward.
|
|
19
|
+
*/
|
|
20
|
+
import ContextMenu from "../components/ContextMenu.svelte"
|
|
21
|
+
import ReviewProgress from "../components/review/ReviewProgress.svelte"
|
|
22
|
+
import { motionFade } from "../motion/transitions"
|
|
23
|
+
import { showConfirm } from "../stores/confirmDialog"
|
|
24
|
+
import { flushSync, tick } from "svelte"
|
|
25
|
+
import BlockRenderer from "./BlockRenderer.svelte"
|
|
26
|
+
import FoldedRounds from "./FoldedRounds.svelte"
|
|
27
|
+
import Glyph, { type GlyphName } from "./Glyph.svelte"
|
|
28
|
+
import HistoryScrubber from "./HistoryScrubber.svelte"
|
|
29
|
+
import NoteComposer from "./NoteComposer.svelte"
|
|
30
|
+
import { provideHostAdapter } from "./adapter.js"
|
|
31
|
+
import { blockTasksOf } from "./contractBlocks.js"
|
|
32
|
+
import {
|
|
33
|
+
createLiveDataPoller,
|
|
34
|
+
dataAgeLine,
|
|
35
|
+
dataAgeTitle,
|
|
36
|
+
} from "./liveDataPoller.js"
|
|
37
|
+
import { createPageReviewKeys, isPageChromeTarget } from "./pageReviewKeys.js"
|
|
38
|
+
import { blockDef } from "./registry.js"
|
|
39
|
+
import { createOpenNav, revealUnit, type JumpUnit } from "./openNav.js"
|
|
40
|
+
import { AUTO_SEND_DELAY_MS, createAutoSend } from "./autoSend.js"
|
|
41
|
+
import {
|
|
42
|
+
changedSinceSend,
|
|
43
|
+
decisionCardsOf,
|
|
44
|
+
itemVerdictsOf,
|
|
45
|
+
countReviewUnits,
|
|
46
|
+
pageReviewProgress,
|
|
47
|
+
sentAnswerLine,
|
|
48
|
+
takesBlockVerdict,
|
|
49
|
+
verdictsFromBlocks,
|
|
50
|
+
isItemReviewBlock,
|
|
51
|
+
openReviewUnits,
|
|
52
|
+
type OpenReviewUnit,
|
|
53
|
+
} from "./review.js"
|
|
54
|
+
import type {
|
|
55
|
+
BlocksSpec,
|
|
56
|
+
FoldedRound,
|
|
57
|
+
HostAdapter,
|
|
58
|
+
NoteAttachment,
|
|
59
|
+
SentAnswer,
|
|
60
|
+
SentItemsMap,
|
|
61
|
+
VerdictEntry,
|
|
62
|
+
} from "./types.js"
|
|
63
|
+
|
|
64
|
+
interface Props {
|
|
65
|
+
artifactId: string
|
|
66
|
+
version: number
|
|
67
|
+
title: string
|
|
68
|
+
summary: string | null
|
|
69
|
+
byline: string
|
|
70
|
+
spec: BlocksSpec | null
|
|
71
|
+
/** The host's services (types.ts HostAdapter). {} = read-only. */
|
|
72
|
+
adapter?: HostAdapter
|
|
73
|
+
/** The server-rendered live-data blob ({} until a data publish). */
|
|
74
|
+
data?: Record<string, unknown>
|
|
75
|
+
/** ISO string or null — when the data last changed. */
|
|
76
|
+
dataUpdatedAt?: string | null
|
|
77
|
+
/**
|
|
78
|
+
* Write <title>, description and robots into <svelte:head>. Default
|
|
79
|
+
* true (a page of its own); false when the view is embedded in a host
|
|
80
|
+
* page that owns its head.
|
|
81
|
+
*/
|
|
82
|
+
head?: boolean
|
|
83
|
+
/** Robots meta for the head (a share surface adds noarchive). */
|
|
84
|
+
robotsContent?: string
|
|
85
|
+
/**
|
|
86
|
+
* Inline chrome on the title line (mos-3q1o3): the host's Details
|
|
87
|
+
* control renders right after the page title. Omitted on the share
|
|
88
|
+
* surface.
|
|
89
|
+
*/
|
|
90
|
+
titleActions?: import("svelte").Snippet
|
|
91
|
+
/**
|
|
92
|
+
* The linked task's chip (mos-t9wr6), leading the byline row. Omitted on
|
|
93
|
+
* the share surface — a share link never shows the task.
|
|
94
|
+
*/
|
|
95
|
+
taskLine?: import("svelte").Snippet
|
|
96
|
+
/**
|
|
97
|
+
* The host's Listen player (mos-3q1o3, mos-8fvt9): a pill at the right
|
|
98
|
+
* end of the meta line; opened, its bar takes a full-width row.
|
|
99
|
+
*/
|
|
100
|
+
player?: import("svelte").Snippet
|
|
101
|
+
/**
|
|
102
|
+
* Controls for the frame this page sits in (mos-8fvt9): inside the desk
|
|
103
|
+
* drawer, "Open in new tab" + Close join the right end of the top row
|
|
104
|
+
* instead of a drawer bar of their own. Omitted everywhere else.
|
|
105
|
+
*/
|
|
106
|
+
frameActions?: import("svelte").Snippet
|
|
107
|
+
/**
|
|
108
|
+
* Where this page's answers really go (mos-guqgs): /a/<slug>. When the
|
|
109
|
+
* surface has NO save plumbing but the spec carries answer-taking
|
|
110
|
+
* content, a read-only notice renders with this link — the "Open to
|
|
111
|
+
* answer" affordance. Null hides the link.
|
|
112
|
+
*/
|
|
113
|
+
answerUrl?: string | null
|
|
114
|
+
/**
|
|
115
|
+
* The host's own wording for the read-only notice. `kind` is
|
|
116
|
+
* "read-only" (no save plumbing — a share link) or "unsent" (picks save
|
|
117
|
+
* but no review leg sends them). Default: jatui's neutral copy.
|
|
118
|
+
*/
|
|
119
|
+
readOnlyNotice?: import("svelte").Snippet<
|
|
120
|
+
[{ kind: "read-only" | "unsent"; answerUrl: string | null }]
|
|
121
|
+
>
|
|
122
|
+
/**
|
|
123
|
+
* This page's folded earlier rounds: each with its items and every
|
|
124
|
+
* viewer's verdicts/notes, locked. Empty when the page superseded
|
|
125
|
+
* nothing (or the host's read failed).
|
|
126
|
+
*/
|
|
127
|
+
foldedRounds?: FoldedRound[]
|
|
128
|
+
/** Start the folded rounds expanded (a host redirected an old slug here). */
|
|
129
|
+
foldedRoundsOpen?: boolean
|
|
130
|
+
/** Per-block state plumbing (omitted on the read-only share surface). */
|
|
131
|
+
savedBlocks?: Record<string, unknown> | null
|
|
132
|
+
onSaveBlock?: (payload: {
|
|
133
|
+
kind: "blocks"
|
|
134
|
+
blocks: Record<string, unknown>
|
|
135
|
+
}) => Promise<void>
|
|
136
|
+
/**
|
|
137
|
+
* Comments + the review moment (mos-kib2f). Omitted on the read-only
|
|
138
|
+
* share surface: no session → no drawer, no Done-reviewing button.
|
|
139
|
+
*/
|
|
140
|
+
comments?: {
|
|
141
|
+
enabled: boolean
|
|
142
|
+
counts: Record<string, number>
|
|
143
|
+
/** This viewer already recorded review_completed for this version. */
|
|
144
|
+
reviewed: boolean
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The jat task this page serves (mos-wywa3). When set, decision cards
|
|
148
|
+
* and reviewable blocks (takesBlockVerdict) collect answers and the
|
|
149
|
+
* "Send answers" section at the foot of the page sends them as ONE
|
|
150
|
+
* answer to the task. Null → the plain "Done reviewing" moment.
|
|
151
|
+
*/
|
|
152
|
+
taskId?: string | null
|
|
153
|
+
/** Who the answer reaches (the publishing agent), for the send label. */
|
|
154
|
+
answerTo?: string | null
|
|
155
|
+
/**
|
|
156
|
+
* The server-seeded done state (mos-7j6r5): THIS viewer's last Send on
|
|
157
|
+
* THIS version, read by the page load from the review_completed event.
|
|
158
|
+
* Null when they never sent — a first visit. This is what makes a
|
|
159
|
+
* reload remember: without it the page offered "Finish review" again
|
|
160
|
+
* after every refresh, reading as still open when the task was done.
|
|
161
|
+
*/
|
|
162
|
+
reviewAnswer?: SentAnswer | null
|
|
163
|
+
/**
|
|
164
|
+
* The linked task is closed/done (jat state, read server-side). With
|
|
165
|
+
* every item decided, the page shows the completed state — "Decided ·
|
|
166
|
+
* task closed" — even for a viewer who did not send.
|
|
167
|
+
*/
|
|
168
|
+
taskClosed?: boolean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let {
|
|
172
|
+
artifactId,
|
|
173
|
+
version,
|
|
174
|
+
title,
|
|
175
|
+
summary,
|
|
176
|
+
byline,
|
|
177
|
+
spec,
|
|
178
|
+
adapter = {},
|
|
179
|
+
data = {},
|
|
180
|
+
dataUpdatedAt = null,
|
|
181
|
+
head = true,
|
|
182
|
+
robotsContent = "noindex, nofollow",
|
|
183
|
+
titleActions = undefined,
|
|
184
|
+
taskLine = undefined,
|
|
185
|
+
player = undefined,
|
|
186
|
+
frameActions = undefined,
|
|
187
|
+
answerUrl = null,
|
|
188
|
+
readOnlyNotice = undefined,
|
|
189
|
+
foldedRounds = [],
|
|
190
|
+
foldedRoundsOpen = false,
|
|
191
|
+
savedBlocks = null,
|
|
192
|
+
onSaveBlock = undefined,
|
|
193
|
+
comments = undefined,
|
|
194
|
+
taskId = null,
|
|
195
|
+
answerTo = null,
|
|
196
|
+
reviewAnswer = null,
|
|
197
|
+
taskClosed = false,
|
|
198
|
+
}: Props = $props()
|
|
199
|
+
|
|
200
|
+
// The adapter is read ONCE, at mount (key the view by artifact to swap
|
|
201
|
+
// hosts): every component below reads it from the context.
|
|
202
|
+
// svelte-ignore state_referenced_locally
|
|
203
|
+
const host: HostAdapter = adapter
|
|
204
|
+
provideHostAdapter(host)
|
|
205
|
+
const reviewService = host.review
|
|
206
|
+
|
|
207
|
+
// The title's right-click menu (MOS: "Copy reference for an agent", copy
|
|
208
|
+
// link / slug). Keyboard users need the same actions elsewhere in the host.
|
|
209
|
+
let titleMenu = $state({ open: false, x: 0, y: 0 })
|
|
210
|
+
const titleMenuItems = $derived(host.titleMenu?.items ?? [])
|
|
211
|
+
function openTitleMenu(e: MouseEvent) {
|
|
212
|
+
if (titleMenuItems.length === 0) return
|
|
213
|
+
e.preventDefault()
|
|
214
|
+
titleMenu = { open: true, x: e.clientX, y: e.clientY }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Finished in THIS tab: the next actions take focus once they render. */
|
|
218
|
+
let deskFocus = $state(false)
|
|
219
|
+
|
|
220
|
+
// svelte-ignore state_referenced_locally
|
|
221
|
+
const live = createLiveDataPoller({
|
|
222
|
+
fetch: host.liveData
|
|
223
|
+
? () => host.liveData!.fetch({ artifactId })
|
|
224
|
+
: undefined,
|
|
225
|
+
intervalMs: host.liveData?.intervalMs,
|
|
226
|
+
initialData: data,
|
|
227
|
+
initialUpdatedAt: dataUpdatedAt,
|
|
228
|
+
})
|
|
229
|
+
$effect(() => {
|
|
230
|
+
live.start()
|
|
231
|
+
return () => live.stop()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
/** Save-status line for the blocks header. */
|
|
235
|
+
let blocksStatus: "idle" | "saving" | "saved" | "failed" = $state("idle")
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Does this spec carry anything a viewer ANSWERS (mos-guqgs)? Decision
|
|
239
|
+
* cards, item-review blocks (media-picker / diff-review / checklist),
|
|
240
|
+
* actions blocks, and the verdict-taking work-product blocks. A page with
|
|
241
|
+
* none of these is a pure read everywhere — no read-only notice needed.
|
|
242
|
+
*/
|
|
243
|
+
const answerContent = $derived.by(() => {
|
|
244
|
+
if (!spec) return false
|
|
245
|
+
if (decisionCardsOf(spec).size > 0) return true
|
|
246
|
+
return spec.blocks.some(
|
|
247
|
+
(b) =>
|
|
248
|
+
isItemReviewBlock(b.type) ||
|
|
249
|
+
b.type === "actions" ||
|
|
250
|
+
takesBlockVerdict(b.type, b.params),
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
/**
|
|
254
|
+
* The read-only contract (mos-guqgs): a mount with no save plumbing that
|
|
255
|
+
* still carries answer-taking content must SAY SO — a visible notice with
|
|
256
|
+
* an "Open to answer" affordance, never interactive-looking controls over
|
|
257
|
+
* a save that will not happen.
|
|
258
|
+
*/
|
|
259
|
+
const readOnly = $derived(!onSaveBlock && answerContent)
|
|
260
|
+
|
|
261
|
+
/** The host can record "Done reviewing" (HostAdapter.review.markReviewed). */
|
|
262
|
+
const canMarkReviewed = Boolean(reviewService?.markReviewed)
|
|
263
|
+
|
|
264
|
+
/** The "Done reviewing" moment: recorded once per (viewer, version). */
|
|
265
|
+
let reviewed = $state(false)
|
|
266
|
+
$effect(() => {
|
|
267
|
+
reviewed = comments?.reviewed ?? false
|
|
268
|
+
})
|
|
269
|
+
let reviewing = $state(false)
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* POST review_completed (session-only server-side; keys are refused).
|
|
273
|
+
*
|
|
274
|
+
* mos-3fl0h: with cards still open this is never silent. The press read
|
|
275
|
+
* as "this section is done" and the page answered "Review complete ✓" for
|
|
276
|
+
* the whole page. Now it asks first, and "Go to next open" walks to the
|
|
277
|
+
* first open card instead (the same jump the n key does).
|
|
278
|
+
*/
|
|
279
|
+
/** The last "Done reviewing" failure, shown beside the control. */
|
|
280
|
+
let reviewError = $state<string | null>(null)
|
|
281
|
+
|
|
282
|
+
async function doneReviewing() {
|
|
283
|
+
// No host leg → nothing to record. Never a silent no-op behind a
|
|
284
|
+
// control: the button and the auto-send are both gated on this too.
|
|
285
|
+
if (reviewing || reviewed || !reviewService?.markReviewed) return
|
|
286
|
+
if (commentsReview && undecided > 0) {
|
|
287
|
+
const ok = await showConfirm({
|
|
288
|
+
title: "Finish with items still open?",
|
|
289
|
+
body:
|
|
290
|
+
undecided === 1
|
|
291
|
+
? "1 card still has no pick. Finish anyway?"
|
|
292
|
+
: `${undecided} cards still have no pick. Finish anyway?`,
|
|
293
|
+
confirmLabel: "Finish anyway",
|
|
294
|
+
cancelLabel: "Go to next open",
|
|
295
|
+
danger: false,
|
|
296
|
+
timeoutMs: 0,
|
|
297
|
+
})
|
|
298
|
+
if (!ok) {
|
|
299
|
+
// Let the dialog close and hand focus back before the jump moves it.
|
|
300
|
+
await tick()
|
|
301
|
+
openNav.jump(1)
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
reviewing = true
|
|
306
|
+
reviewError = null
|
|
307
|
+
try {
|
|
308
|
+
// The host reads the viewer's saved state: land pending saves first.
|
|
309
|
+
await renderer?.flush()
|
|
310
|
+
await reviewService.markReviewed({ artifactId, version, taskId })
|
|
311
|
+
reviewed = true
|
|
312
|
+
deskFocus = true
|
|
313
|
+
} catch (err) {
|
|
314
|
+
reviewError =
|
|
315
|
+
err instanceof Error && err.message
|
|
316
|
+
? err.message
|
|
317
|
+
: "Could not record that you finished. Try again."
|
|
318
|
+
} finally {
|
|
319
|
+
reviewing = false
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---- Finish review (mos-wywa3): one answer to the linked jat task.
|
|
324
|
+
// Needs the host's send: without it the page never offers a Send it
|
|
325
|
+
// cannot make.
|
|
326
|
+
const reviewMode = $derived(
|
|
327
|
+
Boolean(taskId && onSaveBlock && comments?.enabled && reviewService?.send),
|
|
328
|
+
)
|
|
329
|
+
let renderer:
|
|
330
|
+
| {
|
|
331
|
+
flush: () => Promise<void>
|
|
332
|
+
failedSaveBlocks?: () => string[]
|
|
333
|
+
revealBlock?: (blockId: string) => boolean
|
|
334
|
+
}
|
|
335
|
+
| undefined = $state()
|
|
336
|
+
/**
|
|
337
|
+
* The renderer's live state, reported through onLocalChange. Null until
|
|
338
|
+
* the renderer mounts and reports; derivations read `effectiveBlocks`,
|
|
339
|
+
* which falls back to the server's saved map — the SAME map the renderer
|
|
340
|
+
* seeds itself from — so every count (progress, changed-after-send, the
|
|
341
|
+
* done state) is correct from the first paint, not only after hydration
|
|
342
|
+
* (mos-7j6r5: a reload must render its seeded state on the server too).
|
|
343
|
+
*/
|
|
344
|
+
let localBlocks = $state<Record<string, unknown> | null>(null)
|
|
345
|
+
const effectiveBlocks = $derived(localBlocks ?? savedBlocks ?? {})
|
|
346
|
+
let finishText = $state("")
|
|
347
|
+
/** The Finish note's attachments — descriptors the host's upload leg
|
|
348
|
+
* already stored. */
|
|
349
|
+
let finishClips = $state<NoteAttachment[]>([])
|
|
350
|
+
let finishing = $state(false)
|
|
351
|
+
let finishError = $state<string | null>(null)
|
|
352
|
+
/**
|
|
353
|
+
* This viewer's answer was sent (mos-7j6r5): seeded from the server's
|
|
354
|
+
* `reviewAnswer` on load, so a reload remembers; updated in place by a
|
|
355
|
+
* live send so the tab never flips back to the pre-send state.
|
|
356
|
+
*/
|
|
357
|
+
let answered = $state(reviewAnswer != null)
|
|
358
|
+
/** Who the server says got the answer (the page owner), once sent. */
|
|
359
|
+
let sentTo = $state<string | null>(reviewAnswer?.sentTo ?? null)
|
|
360
|
+
/** When it was sent (ISO), seeded or set by the live send. */
|
|
361
|
+
let sentAt = $state<string | null>(reviewAnswer?.sentAt ?? null)
|
|
362
|
+
/**
|
|
363
|
+
* Delivery (mos-bi8t0): the Send is QUEUED for the owner until the drain
|
|
364
|
+
* puts it in the owner's mailbox and stamps the event. "Sent to … ✓" waits
|
|
365
|
+
* for that stamp (or for jat recording the answer on the task); until then
|
|
366
|
+
* the page says "Sending to …" and polls the event. An answer saved before
|
|
367
|
+
* this field existed (no eventId) reads as sent, the way it always did.
|
|
368
|
+
*/
|
|
369
|
+
let sentEventId = $state<string | null>(reviewAnswer?.eventId ?? null)
|
|
370
|
+
let deliveredAt = $state<string | null>(reviewAnswer?.deliveredAt ?? null)
|
|
371
|
+
let deliverable = $state<boolean>(reviewAnswer?.deliverable ?? true)
|
|
372
|
+
let deliveredNow = $state(false)
|
|
373
|
+
// A host with no deliveryStatus leg cannot report a delivery, so a Send
|
|
374
|
+
// reads as delivered — never a "Sending…" spinner nothing will ever stop.
|
|
375
|
+
const delivered = $derived(
|
|
376
|
+
deliveredNow ||
|
|
377
|
+
deliveredAt !== null ||
|
|
378
|
+
sentEventId === null ||
|
|
379
|
+
!reviewService?.deliveryStatus,
|
|
380
|
+
)
|
|
381
|
+
/** After this long without delivery, the page says so plainly. */
|
|
382
|
+
const DELIVERY_STALL_MS = 90_000
|
|
383
|
+
const DELIVERY_POLL_MS = 5_000
|
|
384
|
+
let nowMs = $state(Date.now())
|
|
385
|
+
const deliveryStalled = $derived(
|
|
386
|
+
answered &&
|
|
387
|
+
!delivered &&
|
|
388
|
+
(!deliverable ||
|
|
389
|
+
(sentAt !== null &&
|
|
390
|
+
nowMs - new Date(sentAt).getTime() > DELIVERY_STALL_MS)),
|
|
391
|
+
)
|
|
392
|
+
/** The verdicts as sent — the changed-after-send diff reads them. */
|
|
393
|
+
let sentVerdicts = $state<Record<string, VerdictEntry> | null>(
|
|
394
|
+
reviewAnswer?.verdicts ?? null,
|
|
395
|
+
)
|
|
396
|
+
let sentItems = $state<SentItemsMap | null>(reviewAnswer?.items ?? null)
|
|
397
|
+
/** The quiet done state's "Send again" reopens the composer. */
|
|
398
|
+
let composerOpen = $state(false)
|
|
399
|
+
/** The sent-summary block, focused after a send so keyboard focus lands
|
|
400
|
+
* on the state that replaced the composer (never dropped to <body>). */
|
|
401
|
+
let sentSummaryEl = $state<HTMLElement | null>(null)
|
|
402
|
+
// Re-seed whenever the server's answer changes (invalidateAll): the
|
|
403
|
+
// server read reflects every send this tab made, so the seed and a live
|
|
404
|
+
// send can never disagree for long.
|
|
405
|
+
$effect(() => {
|
|
406
|
+
const seed = reviewAnswer ?? null
|
|
407
|
+
answered = seed !== null
|
|
408
|
+
sentTo = seed?.sentTo ?? null
|
|
409
|
+
sentAt = seed?.sentAt ?? null
|
|
410
|
+
sentEventId = seed?.eventId ?? null
|
|
411
|
+
deliveredAt = seed?.deliveredAt ?? null
|
|
412
|
+
deliverable = seed?.deliverable ?? true
|
|
413
|
+
deliveredNow = false
|
|
414
|
+
sentVerdicts = seed?.verdicts ?? null
|
|
415
|
+
sentItems = seed?.items ?? null
|
|
416
|
+
composerOpen = false
|
|
417
|
+
})
|
|
418
|
+
const recipient = $derived(sentTo ?? answerTo ?? "the page owner")
|
|
419
|
+
|
|
420
|
+
// Poll the Send's delivery stamp while it is queued (mos-bi8t0). Stops
|
|
421
|
+
// the moment it lands, when the Send has no owner to reach, when the
|
|
422
|
+
// linked task is closed (mos-m3nfp — the answers were acted on; nobody
|
|
423
|
+
// is coming to stamp anything), or on unmount.
|
|
424
|
+
$effect(() => {
|
|
425
|
+
if (!answered || delivered || !sentEventId || !deliverable) return
|
|
426
|
+
if (taskClosed) return
|
|
427
|
+
const poll = reviewService?.deliveryStatus
|
|
428
|
+
if (!poll) return
|
|
429
|
+
const eventId = sentEventId
|
|
430
|
+
let stopped = false
|
|
431
|
+
const check = async () => {
|
|
432
|
+
nowMs = Date.now()
|
|
433
|
+
try {
|
|
434
|
+
const body = await poll({ artifactId, eventId })
|
|
435
|
+
if (stopped) return
|
|
436
|
+
if (body?.delivered && !stopped) {
|
|
437
|
+
deliveredAt = body.deliveredAt ?? new Date().toISOString()
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
// Offline for a beat: the next tick retries.
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const timer = window.setInterval(check, DELIVERY_POLL_MS)
|
|
444
|
+
void check()
|
|
445
|
+
return () => {
|
|
446
|
+
stopped = true
|
|
447
|
+
window.clearInterval(timer)
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* A drafted note the host keeps in the viewer's state (MOS: a cast from
|
|
453
|
+
* the desk under the reserved `desk:note` key — HostAdapter.review
|
|
454
|
+
* .draftNote). The send panel pre-fills from it and says where it came
|
|
455
|
+
* from; editing is free (drafts are editable).
|
|
456
|
+
*/
|
|
457
|
+
const deskDraft = $derived(
|
|
458
|
+
reviewService?.draftNote?.(effectiveBlocks as Record<string, unknown>) ??
|
|
459
|
+
null,
|
|
460
|
+
)
|
|
461
|
+
/** The desk note this tab already sent (its text rode the answer): the
|
|
462
|
+
* same draft must not show again as pending in a re-send composer — a
|
|
463
|
+
* NEW cast after the send reads as different text and shows. */
|
|
464
|
+
let consumedDeskNote = $state<string | null>(null)
|
|
465
|
+
const pendingDeskDraft = $derived(
|
|
466
|
+
deskDraft && deskDraft.note !== consumedDeskNote ? deskDraft : null,
|
|
467
|
+
)
|
|
468
|
+
let deskDraftShown = $state(false)
|
|
469
|
+
$effect(() => {
|
|
470
|
+
// Pre-fill once, when the draft first arrives and the box is empty —
|
|
471
|
+
// never fight the reviewer's own typing.
|
|
472
|
+
if (!deskDraftShown && pendingDeskDraft && !finishText) {
|
|
473
|
+
finishText = pendingDeskDraft.note
|
|
474
|
+
deskDraftShown = true
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
/** The verdicts to send, built from the viewer's local state map — the
|
|
479
|
+
* same reading the server runs for a desk Send (mos-yra79). */
|
|
480
|
+
const verdicts = $derived(verdictsFromBlocks(spec, effectiveBlocks))
|
|
481
|
+
/** Reviewable units: items inside item-review blocks, not
|
|
482
|
+
* the blocks that hold them — the same units the census counts — plus
|
|
483
|
+
* every other block's verdict and decision pick. */
|
|
484
|
+
const units = $derived(countReviewUnits(spec, effectiveBlocks))
|
|
485
|
+
const progress = $derived(pageReviewProgress(units))
|
|
486
|
+
const decidedCount = $derived(
|
|
487
|
+
units.approved + units.needsWork + units.rejected + units.chose,
|
|
488
|
+
)
|
|
489
|
+
const undecided = $derived(Math.max(0, units.total - decidedCount))
|
|
490
|
+
/**
|
|
491
|
+
* The plain "Done reviewing" moment over a page with things to decide
|
|
492
|
+
* (mos-3fl0h): no linked task, but decision cards or verdict blocks the
|
|
493
|
+
* viewer can answer. The header carries the same live progress, the
|
|
494
|
+
* still-open chip and the n / shift+N walk as Finish review.
|
|
495
|
+
*/
|
|
496
|
+
const commentsReview = $derived(
|
|
497
|
+
!reviewMode && Boolean(comments?.enabled && onSaveBlock) && units.total > 0,
|
|
498
|
+
)
|
|
499
|
+
/** Either review moment: the open-item walk is live in both. */
|
|
500
|
+
const walkOpen = $derived(reviewMode || commentsReview)
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Picks save, but nothing sends or records them: the page carries
|
|
504
|
+
* answerable content and save plumbing, yet the host gave no review leg
|
|
505
|
+
* this page can use (no `review.send` for a linked task, no
|
|
506
|
+
* `review.markReviewed` for Done reviewing). The viewer is told, instead
|
|
507
|
+
* of answering into a page nobody reads.
|
|
508
|
+
*/
|
|
509
|
+
const answersGoNowhere = $derived(
|
|
510
|
+
Boolean(onSaveBlock) &&
|
|
511
|
+
answerContent &&
|
|
512
|
+
!reviewMode &&
|
|
513
|
+
!(canMarkReviewed && comments?.enabled),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
// ---- The seeded done state (mos-7j6r5) --------------------------------
|
|
517
|
+
// The item verdicts the viewer would send NOW, read by the SAME function
|
|
518
|
+
// the server ran at send time — so a difference below is a real changed
|
|
519
|
+
// verdict, never a derivation mismatch. And the diff itself: what changed
|
|
520
|
+
// since this viewer last sent (the "Send changes (n)" count).
|
|
521
|
+
const currentItems = $derived.by(() => {
|
|
522
|
+
if (!spec) return {} as SentItemsMap
|
|
523
|
+
return itemVerdictsOf(effectiveBlocks, spec)
|
|
524
|
+
})
|
|
525
|
+
const changed = $derived(
|
|
526
|
+
changedSinceSend(sentVerdicts, verdicts, sentItems, currentItems),
|
|
527
|
+
)
|
|
528
|
+
/** The quiet done state's one-line "what was sent". */
|
|
529
|
+
const sentSummaryLine = $derived(sentAnswerLine(sentVerdicts, sentItems))
|
|
530
|
+
|
|
531
|
+
/** The chip's date: "Oct 13" (the year joins when it is not this one). */
|
|
532
|
+
function shortDay(iso: string): string {
|
|
533
|
+
const d = new Date(iso)
|
|
534
|
+
if (!Number.isFinite(d.getTime())) return ""
|
|
535
|
+
const opts: Intl.DateTimeFormatOptions = { month: "short", day: "numeric" }
|
|
536
|
+
if (d.getFullYear() !== new Date().getFullYear()) opts.year = "numeric"
|
|
537
|
+
return d.toLocaleDateString(undefined, opts)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ---- Still-open units (mos-cs31v) -------------------------------------
|
|
541
|
+
// The census counts units; openReviewUnits names them, in page order. The
|
|
542
|
+
// header chip, the n / shift+N walk and the Finish panel's jump cards all
|
|
543
|
+
// read this one list, so they can never disagree with the count.
|
|
544
|
+
const OPEN_CARDS_MAX = 10
|
|
545
|
+
const openUnits = $derived(openReviewUnits(spec, effectiveBlocks))
|
|
546
|
+
const openCount = $derived(openUnits.length)
|
|
547
|
+
|
|
548
|
+
// ---- Header rows (mos-8fvt9) ------------------------------------------
|
|
549
|
+
/** Row 1's right side: save status + the review moment's one control. */
|
|
550
|
+
const showReviewControl = $derived(
|
|
551
|
+
(onSaveBlock !== undefined && blocksStatus !== "idle") ||
|
|
552
|
+
reviewMode ||
|
|
553
|
+
Boolean(comments?.enabled),
|
|
554
|
+
)
|
|
555
|
+
/** The still-open chip (Finish review mode) — the meter carries its own. */
|
|
556
|
+
const showOpenChip = $derived(
|
|
557
|
+
walkOpen && !answered && openCount > 0 && !commentsReview,
|
|
558
|
+
)
|
|
559
|
+
/** Row 2's right side: the progress meter and/or the still-open walk. */
|
|
560
|
+
const showProgress = $derived(showOpenChip || commentsReview)
|
|
561
|
+
const firstOpenCards = $derived(openUnits.slice(0, OPEN_CARDS_MAX))
|
|
562
|
+
|
|
563
|
+
/** What a card calls a unit: its own label, else the block registry's
|
|
564
|
+
* name for the type, else the block id (a plain verdict block may carry
|
|
565
|
+
* no params title). */
|
|
566
|
+
function unitLabel(u: Pick<OpenReviewUnit, "label" | "blockId"> & { type?: string }): string {
|
|
567
|
+
return u.label || (u.type ? blockDef(u.type)?.title : undefined) || u.blockId
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** The placeholder tile's glyph, per block kind. */
|
|
571
|
+
function openIcon(type: string): GlyphName {
|
|
572
|
+
switch (type) {
|
|
573
|
+
case "checklist":
|
|
574
|
+
return "list-checks"
|
|
575
|
+
case "media-picker":
|
|
576
|
+
return "images"
|
|
577
|
+
case "diff-review":
|
|
578
|
+
return "file-diff"
|
|
579
|
+
case "callout":
|
|
580
|
+
return "circle-help"
|
|
581
|
+
default:
|
|
582
|
+
return "badge-check"
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** The screen-reader line announcing a jump ("focus moves to the item,
|
|
587
|
+
* announced"): cleared, then set a beat later, so the SAME destination
|
|
588
|
+
* twice still announces. */
|
|
589
|
+
let jumpAnnouncement = $state("")
|
|
590
|
+
function announceJump(unit: JumpUnit) {
|
|
591
|
+
const label = unitLabel(unit)
|
|
592
|
+
const rest =
|
|
593
|
+
openCount === 1 ? "the last open item" : `${openCount} items still open`
|
|
594
|
+
jumpAnnouncement = ""
|
|
595
|
+
window.setTimeout(() => {
|
|
596
|
+
jumpAnnouncement = label ? `Jumped to ${label}. ${rest}.` : rest + "."
|
|
597
|
+
}, 60)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** The jump cursor (openNav.ts): next/previous over the open list, and a
|
|
601
|
+
* direct jump for a Finish card click. One shared instance — its cursor
|
|
602
|
+
* outlives re-renders and inherits forward when the item under it is
|
|
603
|
+
* decided. */
|
|
604
|
+
const openNav = createOpenNav({
|
|
605
|
+
getUnits: () => openUnits,
|
|
606
|
+
onJump: (unit) => {
|
|
607
|
+
// A card in a collapsed group is hidden, not gone: open its group
|
|
608
|
+
// and let the DOM catch up before the jump looks for it.
|
|
609
|
+
if (renderer?.revealBlock?.(unit.blockId)) flushSync()
|
|
610
|
+
revealUnit(unit)
|
|
611
|
+
announceJump(unit)
|
|
612
|
+
},
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
/** n / shift+N walk the still-open items (mos-cs31v) — page-level keys
|
|
616
|
+
* under the same "not inside a block" rule as j / f / ? : inside a block
|
|
617
|
+
* the block's own n (open a note) wins. Dead outside a review moment. */
|
|
618
|
+
function openNavKeys(e: KeyboardEvent): boolean {
|
|
619
|
+
if (e.defaultPrevented || e.isComposing) return false
|
|
620
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return false
|
|
621
|
+
if (!isPageChromeTarget(e.target)) return false
|
|
622
|
+
const dir =
|
|
623
|
+
e.key === "n" && !e.shiftKey ? 1 : e.key === "N" && e.shiftKey ? -1 : 0
|
|
624
|
+
if (dir === 0 || !walkOpen || answered) return false
|
|
625
|
+
e.preventDefault()
|
|
626
|
+
openNav.jump(dir)
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* One final action (mos-lfkys): the "Send answers" section sits at the
|
|
632
|
+
* foot of the page, right under the last card, so answering the last
|
|
633
|
+
* decision puts the send button in reach. The header button only brings
|
|
634
|
+
* you there — it is never a second step of its own.
|
|
635
|
+
*/
|
|
636
|
+
let sendButton: HTMLButtonElement | undefined = $state()
|
|
637
|
+
function goToSend() {
|
|
638
|
+
const target = document.getElementById("send-answers")
|
|
639
|
+
if (!target) return
|
|
640
|
+
const reduce = window.matchMedia?.(
|
|
641
|
+
"(prefers-reduced-motion: reduce)",
|
|
642
|
+
).matches
|
|
643
|
+
target.scrollIntoView({
|
|
644
|
+
behavior: reduce ? "auto" : "smooth",
|
|
645
|
+
block: "center",
|
|
646
|
+
})
|
|
647
|
+
sendButton?.focus({ preventScroll: true })
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function finishReview() {
|
|
651
|
+
if (finishing) return
|
|
652
|
+
finishing = true
|
|
653
|
+
finishError = null
|
|
654
|
+
try {
|
|
655
|
+
await renderer?.flush()
|
|
656
|
+
// mos-lwbbt (pe-dusk-sweep): a save that failed (burst cap, offline)
|
|
657
|
+
// left the answers ONLY in this tab — the server has none of them.
|
|
658
|
+
// Sending now would answer the task with nothing and overwrite the
|
|
659
|
+
// viewer's saved state with an empty fold. Refuse until every block's
|
|
660
|
+
// save lands; the Retry lives on the failed block itself.
|
|
661
|
+
const failed = renderer?.failedSaveBlocks?.() ?? []
|
|
662
|
+
if (failed.length > 0) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
"Some answers are not saved yet — use Retry on the highlighted block(s), then send again.",
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
if (!reviewService) throw new Error("Answers can't be sent from here.")
|
|
668
|
+
const body = await reviewService.send({
|
|
669
|
+
artifactId,
|
|
670
|
+
version,
|
|
671
|
+
taskId: taskId ?? null,
|
|
672
|
+
blockTasks: Object.fromEntries(blockTasksOf(spec)),
|
|
673
|
+
verdicts: $state.snapshot(verdicts) as Record<string, VerdictEntry>,
|
|
674
|
+
text: finishText.trim() || undefined,
|
|
675
|
+
...(finishClips.length > 0
|
|
676
|
+
? { attachments: $state.snapshot(finishClips) as NoteAttachment[] }
|
|
677
|
+
: {}),
|
|
678
|
+
})
|
|
679
|
+
if (!body?.answered) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
body?.error ?? "Your answers could not be sent. Send again.",
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
sentTo = body.sentTo ?? null
|
|
685
|
+
sentAt = new Date().toISOString()
|
|
686
|
+
nowMs = Date.now()
|
|
687
|
+
// A host that tracks no delivery sends no delivery block: delivered.
|
|
688
|
+
sentEventId = body.delivery?.eventId ?? null
|
|
689
|
+
deliveredNow = body.delivery ? body.delivery.delivered === true : true
|
|
690
|
+
deliveredAt = null
|
|
691
|
+
deliverable = body.delivery?.deliverable ?? true
|
|
692
|
+
// The tab knows what it just sent — seed the diff from it (deep-copied
|
|
693
|
+
// out of the renderer's $state map) so the quiet state reads
|
|
694
|
+
// "unchanged" immediately, without waiting for a reload.
|
|
695
|
+
sentVerdicts = $state.snapshot(verdicts) as Record<string, VerdictEntry>
|
|
696
|
+
sentItems = $state.snapshot(currentItems) as SentItemsMap
|
|
697
|
+
answered = true
|
|
698
|
+
reviewed = true
|
|
699
|
+
deskFocus = true
|
|
700
|
+
composerOpen = false
|
|
701
|
+
// The answer went out with its note; a "Send again" starts a NEW
|
|
702
|
+
// answer, not a re-edit of the old one (the context line above the
|
|
703
|
+
// composer says what went before).
|
|
704
|
+
finishText = ""
|
|
705
|
+
finishClips = []
|
|
706
|
+
consumedDeskNote = deskDraft?.note ?? null
|
|
707
|
+
// Keyboard focus follows the state change: the composer just
|
|
708
|
+
// unmounted, so land focus ON the sent summary (announced to screen
|
|
709
|
+
// readers by its aria-live status line).
|
|
710
|
+
await tick()
|
|
711
|
+
sentSummaryEl?.focus()
|
|
712
|
+
} catch (err) {
|
|
713
|
+
finishError = err instanceof Error ? err.message : String(err)
|
|
714
|
+
} finally {
|
|
715
|
+
finishing = false
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ---- Auto-send when complete (mos-yra79) ------------------------------
|
|
720
|
+
// Deciding the LAST open item sends the page by itself after a short
|
|
721
|
+
// grace, through the page's own Send (Finish review) or Done reviewing.
|
|
722
|
+
// A page that loads already complete, or one partly decided, stays a
|
|
723
|
+
// draft. Undo (or Esc) keeps it a draft; typing restarts the grace.
|
|
724
|
+
const autoSend = createAutoSend({
|
|
725
|
+
onSend: () => (reviewMode ? finishReview() : doneReviewing()),
|
|
726
|
+
})
|
|
727
|
+
$effect(() => {
|
|
728
|
+
const complete = units.total > 0 && undecided === 0
|
|
729
|
+
const eligible = reviewMode
|
|
730
|
+
? !answered && !finishing
|
|
731
|
+
: commentsReview && canMarkReviewed && !reviewed && !reviewing
|
|
732
|
+
autoSend.update(complete, eligible)
|
|
733
|
+
})
|
|
734
|
+
$effect(() => () => autoSend.stop())
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Review keys from page load (mos-lsmve.13): with focus on <body>, j / f /
|
|
738
|
+
* ? / gg go to the first review block. Verdict keys are never routed.
|
|
739
|
+
*/
|
|
740
|
+
const pageKeys = createPageReviewKeys({
|
|
741
|
+
getBlocks: () =>
|
|
742
|
+
(spec?.blocks ?? []).filter((b) => isItemReviewBlock(b.type)),
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
/** Measured view width, exposed as --artifact-page-w for wide blocks. */
|
|
746
|
+
let pageWidth = $state(0)
|
|
747
|
+
</script>
|
|
748
|
+
|
|
749
|
+
<svelte:head>
|
|
750
|
+
{#if head}
|
|
751
|
+
<title>{title}</title>
|
|
752
|
+
{#if summary}
|
|
753
|
+
<meta name="description" content={summary} />
|
|
754
|
+
{/if}
|
|
755
|
+
<!-- Never let an internal page be indexed or previewed by a link unfurler. -->
|
|
756
|
+
<meta name="robots" content={robotsContent} />
|
|
757
|
+
{/if}
|
|
758
|
+
</svelte:head>
|
|
759
|
+
|
|
760
|
+
<svelte:window
|
|
761
|
+
oninput={() => autoSend.bump()}
|
|
762
|
+
onkeydown={(e) => {
|
|
763
|
+
if (autoSend.pending && e.key === "Escape" && !e.defaultPrevented) {
|
|
764
|
+
e.preventDefault()
|
|
765
|
+
autoSend.undo()
|
|
766
|
+
return
|
|
767
|
+
}
|
|
768
|
+
if (pageKeys.handleKeydown(e)) return
|
|
769
|
+
openNavKeys(e)
|
|
770
|
+
}}
|
|
771
|
+
/>
|
|
772
|
+
|
|
773
|
+
<!-- data-artifact-page + --artifact-page-w: wide blocks (a table with many
|
|
774
|
+
columns) may break out of the max-w-3xl reading column up to this view's
|
|
775
|
+
own measured width, never the viewport's (mos-8cbma). Measured, not a
|
|
776
|
+
CSS container: container-type would trap the drawers' position:fixed. -->
|
|
777
|
+
<div
|
|
778
|
+
class="bg-base-100 flex min-h-screen flex-col"
|
|
779
|
+
data-artifact-page
|
|
780
|
+
bind:clientWidth={pageWidth}
|
|
781
|
+
style:--artifact-page-w={pageWidth ? `${pageWidth}px` : undefined}
|
|
782
|
+
>
|
|
783
|
+
{#snippet openArrows()}
|
|
784
|
+
<button
|
|
785
|
+
type="button"
|
|
786
|
+
class="btn btn-ghost btn-sm rounded-field px-1.5"
|
|
787
|
+
aria-label="Previous open item (shift+N)"
|
|
788
|
+
title="Previous open item (shift+N)"
|
|
789
|
+
onclick={() => openNav.jump(-1)}
|
|
790
|
+
>
|
|
791
|
+
<Glyph name="chevron-up" size={16} />
|
|
792
|
+
</button>
|
|
793
|
+
<button
|
|
794
|
+
type="button"
|
|
795
|
+
class="btn btn-ghost btn-sm rounded-field px-1.5"
|
|
796
|
+
aria-label="Next open item (N)"
|
|
797
|
+
title="Next open item (N)"
|
|
798
|
+
onclick={() => openNav.jump(1)}
|
|
799
|
+
>
|
|
800
|
+
<Glyph name="chevron-down" size={16} />
|
|
801
|
+
</button>
|
|
802
|
+
{/snippet}
|
|
803
|
+
|
|
804
|
+
<header
|
|
805
|
+
class="border-base-content/10 bg-base-100 sticky top-[var(--rail-shell-top,0px)] z-10 border-b px-4 py-2.5 sm:px-6"
|
|
806
|
+
>
|
|
807
|
+
<!-- mos-8fvt9 (Joe 2026-10-20, "20% of the screen is chrome"): two rows,
|
|
808
|
+
three with the meta line. Each row is crumb/title/meta on the left
|
|
809
|
+
and its controls on the right:
|
|
810
|
+
1. crumb ............ save status · review control · frame actions
|
|
811
|
+
2. title + Details .................. progress meter / open chip
|
|
812
|
+
3. task chip · byline · data age .................... Listen pill
|
|
813
|
+
An expanded Listen player takes a new full-width row under row 3;
|
|
814
|
+
collapsed it takes none. Every row is flex-wrap with a min left
|
|
815
|
+
width, so on a phone the right side drops under its left side,
|
|
816
|
+
still right-aligned. DOM order is the reading order: crumb → review
|
|
817
|
+
→ frame actions → title → details → progress → meta → player. -->
|
|
818
|
+
<div class="mx-auto max-w-3xl">
|
|
819
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
820
|
+
<div class="min-w-[min(100%,8rem)] flex-1">
|
|
821
|
+
{#if host.crumb}
|
|
822
|
+
{@render host.crumb()}
|
|
823
|
+
{/if}
|
|
824
|
+
</div>
|
|
825
|
+
{#if showReviewControl || frameActions}
|
|
826
|
+
<div
|
|
827
|
+
class="ml-auto flex max-w-full min-w-0 items-center justify-end gap-2"
|
|
828
|
+
data-header-end
|
|
829
|
+
>
|
|
830
|
+
{#if showReviewControl}
|
|
831
|
+
<div
|
|
832
|
+
class="flex max-w-full min-w-0 flex-wrap items-center justify-end gap-2"
|
|
833
|
+
data-review-control
|
|
834
|
+
>
|
|
835
|
+
{#if onSaveBlock && blocksStatus !== "idle"}
|
|
836
|
+
<span
|
|
837
|
+
class="shrink-0 text-xs tabular-nums {blocksStatus ===
|
|
838
|
+
'failed'
|
|
839
|
+
? 'font-medium text-warning'
|
|
840
|
+
: 'text-base-content/40'}"
|
|
841
|
+
aria-live="polite"
|
|
842
|
+
>
|
|
843
|
+
{blocksStatus === "saving"
|
|
844
|
+
? "Saving…"
|
|
845
|
+
: blocksStatus === "saved"
|
|
846
|
+
? "Saved ✓"
|
|
847
|
+
: "Not saved. Retry on the block"}
|
|
848
|
+
</span>
|
|
849
|
+
{/if}
|
|
850
|
+
{#if reviewMode}
|
|
851
|
+
{#if answered && changed.total > 0}
|
|
852
|
+
<!-- Changed after sending (mos-7j6r5): the ONLY state where the
|
|
853
|
+
header carries a primary action again — the count is what
|
|
854
|
+
changed, and the panel below holds the composer. -->
|
|
855
|
+
<button
|
|
856
|
+
type="button"
|
|
857
|
+
class="btn btn-primary btn-sm rounded-field shrink-0"
|
|
858
|
+
aria-controls="send-answers"
|
|
859
|
+
onclick={goToSend}
|
|
860
|
+
>
|
|
861
|
+
Send changes
|
|
862
|
+
<span class="tabular-nums opacity-80"
|
|
863
|
+
>({changed.total})</span
|
|
864
|
+
>
|
|
865
|
+
</button>
|
|
866
|
+
{:else if answered && taskClosed && !delivered}
|
|
867
|
+
<!-- mos-m3nfp: the linked task closed with this viewer's answers
|
|
868
|
+
in — they were acted on. Never the delivery warning for a
|
|
869
|
+
review that is already over. -->
|
|
870
|
+
<span
|
|
871
|
+
class="border-success/30 bg-success/10 text-success inline-flex min-w-0 shrink-0 items-center gap-1.5 rounded-field border px-2.5 py-1 text-xs"
|
|
872
|
+
role="status"
|
|
873
|
+
title="The linked task ({taskId}) is closed. Your answers were received"
|
|
874
|
+
>
|
|
875
|
+
<span class="max-w-[9rem] truncate sm:max-w-none">
|
|
876
|
+
Answers received · task closed
|
|
877
|
+
</span>
|
|
878
|
+
</span>
|
|
879
|
+
{:else if answered && !delivered}
|
|
880
|
+
<!-- Queued, not yet with the owner (mos-bi8t0). A page no agent
|
|
881
|
+
owns (mos-m3nfp) says so plainly instead of warning about a
|
|
882
|
+
delivery that will never happen. -->
|
|
883
|
+
<span
|
|
884
|
+
class="inline-flex min-w-0 shrink-0 items-center gap-1.5 rounded-field border px-2.5 py-1 text-xs {!deliverable ||
|
|
885
|
+
!deliveryStalled
|
|
886
|
+
? 'border-base-content/15 bg-base-200 text-base-content/70'
|
|
887
|
+
: 'border-warning/40 bg-warning/10 text-warning-content'}"
|
|
888
|
+
role="status"
|
|
889
|
+
title={!deliverable
|
|
890
|
+
? "No agent owns this page. Nobody was notified"
|
|
891
|
+
: deliveryStalled
|
|
892
|
+
? `Not delivered to ${recipient} yet. Your answers are saved.`
|
|
893
|
+
: `Sending to ${recipient}`}
|
|
894
|
+
>
|
|
895
|
+
{#if deliverable && !deliveryStalled}
|
|
896
|
+
<span class="loading loading-spinner loading-xs"></span>
|
|
897
|
+
{/if}
|
|
898
|
+
<span class="max-w-[9rem] truncate sm:max-w-none">
|
|
899
|
+
{!deliverable
|
|
900
|
+
? "No agent owns this page"
|
|
901
|
+
: deliveryStalled
|
|
902
|
+
? "Saved, not delivered yet"
|
|
903
|
+
: `Sending to ${recipient}…`}
|
|
904
|
+
</span>
|
|
905
|
+
</span>
|
|
906
|
+
{:else if answered}
|
|
907
|
+
<!-- Done (mos-7j6r5): a quiet status chip, never a primary
|
|
908
|
+
button — the review is over until the viewer changes a
|
|
909
|
+
verdict. Seeded from the server, so a reload keeps it. -->
|
|
910
|
+
<span
|
|
911
|
+
class="border-success/30 bg-success/10 text-success inline-flex min-w-0 shrink-0 items-center gap-1.5 rounded-field border px-2.5 py-1 text-xs"
|
|
912
|
+
role="status"
|
|
913
|
+
title="Sent to {recipient} ({taskId})"
|
|
914
|
+
>
|
|
915
|
+
<span class="max-w-[9rem] truncate sm:max-w-none">
|
|
916
|
+
Sent to {recipient} ✓
|
|
917
|
+
</span>
|
|
918
|
+
{#if sentAt}
|
|
919
|
+
<span class="text-success-content/70 tabular-nums">
|
|
920
|
+
· {shortDay(sentAt)}
|
|
921
|
+
</span>
|
|
922
|
+
{/if}
|
|
923
|
+
</span>
|
|
924
|
+
{:else if taskClosed && units.total > 0 && undecided === 0}
|
|
925
|
+
<!-- The task closed with every item decided (mos-7j6r5): the
|
|
926
|
+
completed state, even for a viewer who never sent. -->
|
|
927
|
+
<span
|
|
928
|
+
class="border-success/30 bg-success/10 text-success shrink-0 rounded-field border px-2.5 py-1 text-xs"
|
|
929
|
+
title="The linked task ({taskId}) is closed — your decisions stay saved on this page"
|
|
930
|
+
>
|
|
931
|
+
Decided · task closed
|
|
932
|
+
</span>
|
|
933
|
+
{:else}
|
|
934
|
+
<button
|
|
935
|
+
type="button"
|
|
936
|
+
class="btn btn-primary btn-sm rounded-field shrink-0"
|
|
937
|
+
aria-controls="send-answers"
|
|
938
|
+
onclick={goToSend}
|
|
939
|
+
>
|
|
940
|
+
Finish review
|
|
941
|
+
{#if units.total > 0}
|
|
942
|
+
<span class="tabular-nums opacity-80"
|
|
943
|
+
>{decidedCount}/{units.total}</span
|
|
944
|
+
>
|
|
945
|
+
{/if}
|
|
946
|
+
</button>
|
|
947
|
+
{/if}
|
|
948
|
+
{:else if comments?.enabled}
|
|
949
|
+
{#if reviewed && commentsReview && undecided > 0}
|
|
950
|
+
<!-- Recorded with items still open (an earlier press, or Finish
|
|
951
|
+
anyway): say so. "Review complete" is for a page with nothing
|
|
952
|
+
left to decide. -->
|
|
953
|
+
<span
|
|
954
|
+
class="border-base-content/15 bg-base-200 text-base-content/70 shrink-0 rounded-field border px-2.5 py-1 text-xs"
|
|
955
|
+
title="You marked this version done with items still open. Your picks still save"
|
|
956
|
+
data-reviewed-open
|
|
957
|
+
>
|
|
958
|
+
Marked done · {undecided} open
|
|
959
|
+
</span>
|
|
960
|
+
{:else if reviewed}
|
|
961
|
+
<span
|
|
962
|
+
class="border-success/30 bg-success/10 text-success shrink-0 rounded-field border px-2.5 py-1 text-xs"
|
|
963
|
+
title="Recorded for this version — republishing starts a fresh page"
|
|
964
|
+
>
|
|
965
|
+
Review complete ✓
|
|
966
|
+
</span>
|
|
967
|
+
{:else if reviewService?.markReviewed}
|
|
968
|
+
<button
|
|
969
|
+
type="button"
|
|
970
|
+
class="btn btn-outline btn-sm rounded-field shrink-0"
|
|
971
|
+
disabled={reviewing}
|
|
972
|
+
title={commentsReview && undecided > 0
|
|
973
|
+
? `${undecided} still open. Tell the publisher you are done reviewing this version`
|
|
974
|
+
: "Tell the publisher you are done reviewing this version"}
|
|
975
|
+
onclick={doneReviewing}
|
|
976
|
+
>
|
|
977
|
+
{reviewing ? "Recording…" : "Done reviewing"}
|
|
978
|
+
</button>
|
|
979
|
+
{/if}
|
|
980
|
+
{#if reviewError && !reviewed}
|
|
981
|
+
<span
|
|
982
|
+
role="alert"
|
|
983
|
+
class="text-error max-w-[16rem] text-xs"
|
|
984
|
+
data-review-error>{reviewError}</span
|
|
985
|
+
>
|
|
986
|
+
{/if}
|
|
987
|
+
{#if reviewed && host.nextActions}
|
|
988
|
+
<!-- The cursor is still on the Done reviewing press, so
|
|
989
|
+
the next actions land right beside it. -->
|
|
990
|
+
{@render host.nextActions({
|
|
991
|
+
compact: true,
|
|
992
|
+
autofocus: deskFocus,
|
|
993
|
+
nextKey: false,
|
|
994
|
+
})}
|
|
995
|
+
{/if}
|
|
996
|
+
{/if}
|
|
997
|
+
</div>
|
|
998
|
+
{/if}
|
|
999
|
+
{#if frameActions}
|
|
1000
|
+
<div class="-mr-2 flex shrink-0 items-center" data-frame-actions>
|
|
1001
|
+
{@render frameActions()}
|
|
1002
|
+
</div>
|
|
1003
|
+
{/if}
|
|
1004
|
+
</div>
|
|
1005
|
+
{/if}
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
1008
|
+
<div class="flex min-w-[min(100%,16rem)] flex-1 items-start gap-2">
|
|
1009
|
+
<h1
|
|
1010
|
+
class="min-w-0 line-clamp-2 text-lg font-semibold break-words sm:text-xl"
|
|
1011
|
+
title={titleMenuItems.length > 0
|
|
1012
|
+
? "Right-click for more actions"
|
|
1013
|
+
: undefined}
|
|
1014
|
+
oncontextmenu={openTitleMenu}
|
|
1015
|
+
>
|
|
1016
|
+
{title}
|
|
1017
|
+
</h1>
|
|
1018
|
+
{#if titleActions}
|
|
1019
|
+
<div class="flex shrink-0 items-center gap-2" data-title-actions>
|
|
1020
|
+
{@render titleActions()}
|
|
1021
|
+
</div>
|
|
1022
|
+
{/if}
|
|
1023
|
+
</div>
|
|
1024
|
+
{#if showProgress}
|
|
1025
|
+
<!-- mos-8fvt9: the progress meter and the still-open arrows sit
|
|
1026
|
+
on the title row, under the review control (Joe's mockup). -->
|
|
1027
|
+
<div
|
|
1028
|
+
class="ml-auto flex shrink-0 items-center justify-end gap-2"
|
|
1029
|
+
data-review-progress
|
|
1030
|
+
>
|
|
1031
|
+
{#if showOpenChip}
|
|
1032
|
+
<!-- Still-open chip (mos-cs31v): the count is live, and the arrows
|
|
1033
|
+
walk the open items without leaving the header. -->
|
|
1034
|
+
<div
|
|
1035
|
+
class="flex shrink-0 items-center gap-0.5"
|
|
1036
|
+
role="group"
|
|
1037
|
+
aria-label="Still-open items"
|
|
1038
|
+
>
|
|
1039
|
+
<span
|
|
1040
|
+
class="border-warning/30 bg-warning/10 text-warning rounded-field border px-2 py-1 text-xs font-medium tabular-nums"
|
|
1041
|
+
aria-live="polite"
|
|
1042
|
+
aria-atomic="true"
|
|
1043
|
+
>
|
|
1044
|
+
{openCount} open
|
|
1045
|
+
</span>
|
|
1046
|
+
{@render openArrows()}
|
|
1047
|
+
</div>
|
|
1048
|
+
{/if}
|
|
1049
|
+
{#if commentsReview}
|
|
1050
|
+
<!-- Live progress (mos-3fl0h): one card's pick is one step, and the
|
|
1051
|
+
header says how many are left — never only a page-wide
|
|
1052
|
+
button that reads as done after the first section. -->
|
|
1053
|
+
<div class="flex shrink-0 items-center gap-2" data-review-meter>
|
|
1054
|
+
<progress
|
|
1055
|
+
class="progress {undecided === 0
|
|
1056
|
+
? 'progress-success'
|
|
1057
|
+
: 'progress-primary'} h-1.5 w-14 sm:w-20"
|
|
1058
|
+
value={decidedCount}
|
|
1059
|
+
max={units.total}
|
|
1060
|
+
aria-label="Review progress"
|
|
1061
|
+
></progress>
|
|
1062
|
+
{#if !answered && openCount > 0}
|
|
1063
|
+
<!-- mos-7h9kt: the count itself goes to the next open item —
|
|
1064
|
+
"2 of 3 decided" over a page whose open item sits far
|
|
1065
|
+
below the fold read as "nothing left to press". -->
|
|
1066
|
+
<button
|
|
1067
|
+
type="button"
|
|
1068
|
+
class="text-base-content/70 hover:text-base-content rounded-field text-xs tabular-nums underline-offset-2 hover:underline"
|
|
1069
|
+
title="Go to the next open item (N)"
|
|
1070
|
+
data-review-count
|
|
1071
|
+
onclick={() => openNav.jump(1)}
|
|
1072
|
+
>
|
|
1073
|
+
<span aria-live="polite" aria-atomic="true"
|
|
1074
|
+
>{decidedCount} of {units.total} decided</span
|
|
1075
|
+
>
|
|
1076
|
+
</button>
|
|
1077
|
+
{:else}
|
|
1078
|
+
<span
|
|
1079
|
+
class="text-base-content/70 text-xs tabular-nums"
|
|
1080
|
+
aria-live="polite"
|
|
1081
|
+
aria-atomic="true"
|
|
1082
|
+
data-review-count
|
|
1083
|
+
>
|
|
1084
|
+
{decidedCount} of {units.total} decided
|
|
1085
|
+
</span>
|
|
1086
|
+
{/if}
|
|
1087
|
+
{#if !answered && openCount > 0}
|
|
1088
|
+
<div
|
|
1089
|
+
class="flex items-center gap-0.5"
|
|
1090
|
+
role="group"
|
|
1091
|
+
aria-label="Still-open items"
|
|
1092
|
+
>
|
|
1093
|
+
{@render openArrows()}
|
|
1094
|
+
</div>
|
|
1095
|
+
{/if}
|
|
1096
|
+
</div>
|
|
1097
|
+
{/if}
|
|
1098
|
+
</div>
|
|
1099
|
+
{/if}
|
|
1100
|
+
</div>
|
|
1101
|
+
{#if summary}
|
|
1102
|
+
<p class="text-base-content/70 mt-0.5 line-clamp-2 text-sm">
|
|
1103
|
+
{summary}
|
|
1104
|
+
</p>
|
|
1105
|
+
{/if}
|
|
1106
|
+
<div
|
|
1107
|
+
class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-1"
|
|
1108
|
+
data-meta-row
|
|
1109
|
+
>
|
|
1110
|
+
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
|
|
1111
|
+
{#if taskLine}
|
|
1112
|
+
{@render taskLine()}
|
|
1113
|
+
{/if}
|
|
1114
|
+
<p class="text-base-content/50 text-xs">{byline}</p>
|
|
1115
|
+
{#if dataAgeLine(live)}
|
|
1116
|
+
<p
|
|
1117
|
+
class="text-base-content/40 text-xs tabular-nums"
|
|
1118
|
+
title={dataAgeTitle(live)}
|
|
1119
|
+
>
|
|
1120
|
+
{dataAgeLine(live)}
|
|
1121
|
+
</p>
|
|
1122
|
+
{/if}
|
|
1123
|
+
</div>
|
|
1124
|
+
{#if player}
|
|
1125
|
+
<!-- Collapsed: a pill at the end of the meta line. Expanded: the
|
|
1126
|
+
bar takes a full-width row of its own (see <style>). -->
|
|
1127
|
+
<div class="meta-player ml-auto min-w-0" data-artifact-player>
|
|
1128
|
+
{@render player()}
|
|
1129
|
+
</div>
|
|
1130
|
+
{/if}
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</header>
|
|
1134
|
+
{#if titleMenuItems.length > 0}
|
|
1135
|
+
<ContextMenu
|
|
1136
|
+
bind:open={titleMenu.open}
|
|
1137
|
+
x={titleMenu.x}
|
|
1138
|
+
y={titleMenu.y}
|
|
1139
|
+
header={host.titleMenu?.header}
|
|
1140
|
+
items={titleMenuItems}
|
|
1141
|
+
/>
|
|
1142
|
+
{/if}
|
|
1143
|
+
|
|
1144
|
+
<main class="mx-auto w-full max-w-3xl flex-1 px-4 py-6 sm:px-6">
|
|
1145
|
+
{#if readOnly || answersGoNowhere}
|
|
1146
|
+
<!-- Never look answerable over an answer that goes nowhere: a mount
|
|
1147
|
+
with no save plumbing (a share link), or one that saves picks but
|
|
1148
|
+
has no host leg to send or record them, says so here. The host
|
|
1149
|
+
may word it itself (readOnlyNotice). -->
|
|
1150
|
+
<aside
|
|
1151
|
+
role="note"
|
|
1152
|
+
class="border-base-content/15 bg-base-200 rounded-box mb-4 border p-3 text-sm"
|
|
1153
|
+
data-read-only-notice={readOnly ? "read-only" : "unsent"}
|
|
1154
|
+
>
|
|
1155
|
+
{#if readOnlyNotice}
|
|
1156
|
+
{@render readOnlyNotice({
|
|
1157
|
+
kind: readOnly ? "read-only" : "unsent",
|
|
1158
|
+
answerUrl,
|
|
1159
|
+
})}
|
|
1160
|
+
{:else if readOnly}
|
|
1161
|
+
<p class="font-medium">Read-only view</p>
|
|
1162
|
+
<p class="text-base-content/70 mt-0.5">
|
|
1163
|
+
Answers aren't saved on this view.
|
|
1164
|
+
{#if answerUrl}
|
|
1165
|
+
<a href={answerUrl} class="link link-primary">Open to answer</a>.
|
|
1166
|
+
{:else}
|
|
1167
|
+
Open the page to answer.
|
|
1168
|
+
{/if}
|
|
1169
|
+
</p>
|
|
1170
|
+
{:else}
|
|
1171
|
+
<p class="font-medium">Your picks save, but go nowhere yet</p>
|
|
1172
|
+
<p class="text-base-content/70 mt-0.5">
|
|
1173
|
+
They stay on this page for you. Nobody is notified from here.
|
|
1174
|
+
{#if answerUrl}
|
|
1175
|
+
<a href={answerUrl} class="link link-primary">Open to answer</a>.
|
|
1176
|
+
{/if}
|
|
1177
|
+
</p>
|
|
1178
|
+
{/if}
|
|
1179
|
+
</aside>
|
|
1180
|
+
{/if}
|
|
1181
|
+
{#if foldedRounds.length > 0}
|
|
1182
|
+
<!-- mos-guqgs scope 3: the rounds this page superseded, locked. -->
|
|
1183
|
+
<FoldedRounds rounds={foldedRounds} initiallyOpen={foldedRoundsOpen} />
|
|
1184
|
+
{/if}
|
|
1185
|
+
{#if spec && comments && host.history}
|
|
1186
|
+
<!-- History scrubber: session surfaces only (the host's service);
|
|
1187
|
+
renders nothing without a trail. -->
|
|
1188
|
+
<HistoryScrubber
|
|
1189
|
+
{artifactId}
|
|
1190
|
+
service={host.history}
|
|
1191
|
+
currentData={live.data}
|
|
1192
|
+
currentVersion={version}
|
|
1193
|
+
/>
|
|
1194
|
+
{/if}
|
|
1195
|
+
{#if spec}
|
|
1196
|
+
<BlockRenderer
|
|
1197
|
+
bind:this={renderer}
|
|
1198
|
+
review={reviewMode || commentsReview}
|
|
1199
|
+
onLocalChange={(b) => (localBlocks = b)}
|
|
1200
|
+
{spec}
|
|
1201
|
+
{artifactId}
|
|
1202
|
+
{version}
|
|
1203
|
+
{savedBlocks}
|
|
1204
|
+
data={live.data}
|
|
1205
|
+
{onSaveBlock}
|
|
1206
|
+
readonly={!onSaveBlock}
|
|
1207
|
+
comments={comments?.enabled
|
|
1208
|
+
? { enabled: true, counts: comments.counts }
|
|
1209
|
+
: undefined}
|
|
1210
|
+
onStatus={(s) => (blocksStatus = s)}
|
|
1211
|
+
/>
|
|
1212
|
+
{:else}
|
|
1213
|
+
<!-- Malformed stored spec: a visible card, never a crash. -->
|
|
1214
|
+
<div
|
|
1215
|
+
role="alert"
|
|
1216
|
+
class="border-error/30 bg-error/10 text-error-content rounded-box border p-4 text-sm"
|
|
1217
|
+
>
|
|
1218
|
+
<p class="font-medium">This page's blocks spec could not be read.</p>
|
|
1219
|
+
<p class="text-error-content/80 mt-1">
|
|
1220
|
+
The stored version is malformed. Republishing the page fixes it.
|
|
1221
|
+
</p>
|
|
1222
|
+
</div>
|
|
1223
|
+
{/if}
|
|
1224
|
+
{#if spec && reviewMode}
|
|
1225
|
+
<section
|
|
1226
|
+
id="send-answers"
|
|
1227
|
+
class="border-base-content/10 bg-base-200 rounded-box mt-8 border p-4"
|
|
1228
|
+
aria-label="Send answers"
|
|
1229
|
+
>
|
|
1230
|
+
{#if answered && changed.total === 0 && !composerOpen}
|
|
1231
|
+
<!-- Done (mos-7j6r5): the sent summary — what was sent, when, to
|
|
1232
|
+
whom — never an empty composer waiting under an answer that
|
|
1233
|
+
already went. "Send again" stays a secondary here. -->
|
|
1234
|
+
<div
|
|
1235
|
+
class="flex flex-col gap-1.5"
|
|
1236
|
+
data-sent-summary
|
|
1237
|
+
bind:this={sentSummaryEl}
|
|
1238
|
+
tabindex="-1"
|
|
1239
|
+
>
|
|
1240
|
+
{#if delivered}
|
|
1241
|
+
<p
|
|
1242
|
+
class="text-success font-medium"
|
|
1243
|
+
role="status"
|
|
1244
|
+
aria-live="polite"
|
|
1245
|
+
>
|
|
1246
|
+
Sent to {recipient} ✓
|
|
1247
|
+
</p>
|
|
1248
|
+
{:else if taskClosed}
|
|
1249
|
+
<!-- mos-m3nfp: the linked task closed — the answers were acted
|
|
1250
|
+
on. Says received, not "not delivered"; no Send again (the
|
|
1251
|
+
review is over; the changed-verdict path still offers one
|
|
1252
|
+
when this viewer edits after the fact). -->
|
|
1253
|
+
<p
|
|
1254
|
+
class="text-success font-medium"
|
|
1255
|
+
role="status"
|
|
1256
|
+
aria-live="polite"
|
|
1257
|
+
>
|
|
1258
|
+
Answers received. The linked task is closed.
|
|
1259
|
+
</p>
|
|
1260
|
+
<p class="text-base-content/60 text-xs">
|
|
1261
|
+
Your decisions stay saved on this page.
|
|
1262
|
+
</p>
|
|
1263
|
+
{:else if deliveryStalled}
|
|
1264
|
+
<!-- The fallback (mos-bi8t0): the owner has not picked the
|
|
1265
|
+
answer up. Say so — never a "Sent ✓" it did not earn. -->
|
|
1266
|
+
<div
|
|
1267
|
+
class="alert alert-warning rounded-box text-sm"
|
|
1268
|
+
role="status"
|
|
1269
|
+
aria-live="polite"
|
|
1270
|
+
data-delivery-stalled
|
|
1271
|
+
>
|
|
1272
|
+
<div class="flex flex-col gap-1">
|
|
1273
|
+
<p class="font-medium">
|
|
1274
|
+
{deliverable
|
|
1275
|
+
? `Not delivered to ${recipient} yet.`
|
|
1276
|
+
: "No agent owns this page, so no one was notified."}
|
|
1277
|
+
</p>
|
|
1278
|
+
<p>
|
|
1279
|
+
Your answers are saved on this page.
|
|
1280
|
+
{deliverable
|
|
1281
|
+
? `They go to ${recipient} as soon as delivery runs. This page updates when they arrive.`
|
|
1282
|
+
: "Tell the person who sent you this page that you finished."}
|
|
1283
|
+
</p>
|
|
1284
|
+
</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
{:else}
|
|
1287
|
+
<p
|
|
1288
|
+
class="text-base-content/70 inline-flex items-center gap-2 font-medium"
|
|
1289
|
+
role="status"
|
|
1290
|
+
aria-live="polite"
|
|
1291
|
+
>
|
|
1292
|
+
<span class="loading loading-spinner loading-xs"></span>
|
|
1293
|
+
Sending to {recipient}…
|
|
1294
|
+
</p>
|
|
1295
|
+
{/if}
|
|
1296
|
+
{#if sentSummaryLine}
|
|
1297
|
+
<p class="text-base-content/70 text-xs">{sentSummaryLine}</p>
|
|
1298
|
+
{/if}
|
|
1299
|
+
<p class="text-base-content/50 text-xs">
|
|
1300
|
+
{#if sentAt}{shortDay(sentAt)} ·
|
|
1301
|
+
{/if}<span class="font-mono">{taskId}</span>
|
|
1302
|
+
</p>
|
|
1303
|
+
{#if !taskClosed || host.nextActions}
|
|
1304
|
+
<div class="mt-2 flex flex-wrap items-end gap-2">
|
|
1305
|
+
{#if !taskClosed}
|
|
1306
|
+
<!-- Send again lives on every live review (a real stall
|
|
1307
|
+
keeps it, mos-bi8t0); a CLOSED task's review is over,
|
|
1308
|
+
so it steps aside here (mos-m3nfp). -->
|
|
1309
|
+
<button
|
|
1310
|
+
type="button"
|
|
1311
|
+
class="btn btn-outline btn-sm rounded-field"
|
|
1312
|
+
onclick={() => (composerOpen = true)}
|
|
1313
|
+
>
|
|
1314
|
+
Send again
|
|
1315
|
+
</button>
|
|
1316
|
+
{/if}
|
|
1317
|
+
{#if host.nextActions}
|
|
1318
|
+
<!-- On the right, where the Send press left the cursor. -->
|
|
1319
|
+
{@render host.nextActions({
|
|
1320
|
+
compact: false,
|
|
1321
|
+
autofocus: deskFocus,
|
|
1322
|
+
nextKey: true,
|
|
1323
|
+
})}
|
|
1324
|
+
{/if}
|
|
1325
|
+
</div>
|
|
1326
|
+
{/if}
|
|
1327
|
+
</div>
|
|
1328
|
+
{:else if !answered && taskClosed && units.total > 0 && undecided === 0}
|
|
1329
|
+
<!-- The task closed with every item decided (mos-7j6r5): the
|
|
1330
|
+
review moment is over even though this viewer never sent. -->
|
|
1331
|
+
<div class="flex flex-col gap-1.5" data-task-closed>
|
|
1332
|
+
<p class="text-success font-medium" role="status">
|
|
1333
|
+
Decided · task closed
|
|
1334
|
+
</p>
|
|
1335
|
+
<p class="text-base-content/60 text-xs">
|
|
1336
|
+
The linked task is closed. Your decisions stay saved on this page.
|
|
1337
|
+
</p>
|
|
1338
|
+
<p class="text-base-content/50 font-mono text-xs">{taskId}</p>
|
|
1339
|
+
{#if host.nextActions}
|
|
1340
|
+
<div class="mt-2 flex">
|
|
1341
|
+
{@render host.nextActions({
|
|
1342
|
+
compact: false,
|
|
1343
|
+
autofocus: false,
|
|
1344
|
+
nextKey: true,
|
|
1345
|
+
})}
|
|
1346
|
+
</div>
|
|
1347
|
+
{/if}
|
|
1348
|
+
</div>
|
|
1349
|
+
{:else}
|
|
1350
|
+
{#if answered}
|
|
1351
|
+
<!-- Re-send context (mos-7j6r5): what went before, quietly,
|
|
1352
|
+
above the composer that will send the changes as a NEW
|
|
1353
|
+
answer. -->
|
|
1354
|
+
<p class="text-base-content/60 text-xs" data-sent-context>
|
|
1355
|
+
{delivered ? "Sent to" : "Not yet delivered to"}
|
|
1356
|
+
{recipient}{sentAt
|
|
1357
|
+
? ` on ${shortDay(sentAt)}`
|
|
1358
|
+
: ""}{sentSummaryLine ? ` (${sentSummaryLine})` : ""}. Your
|
|
1359
|
+
changes send as a new answer.
|
|
1360
|
+
</p>
|
|
1361
|
+
{/if}
|
|
1362
|
+
{#if units.total > 0}
|
|
1363
|
+
{#if progress.lines.length === 0}
|
|
1364
|
+
<ReviewProgress
|
|
1365
|
+
{...progress}
|
|
1366
|
+
size="sm"
|
|
1367
|
+
label="Review progress across the page"
|
|
1368
|
+
/>
|
|
1369
|
+
{:else}
|
|
1370
|
+
<!-- Mixed vocabularies (mos-lsmve.14): no one set of words is
|
|
1371
|
+
on the page, so the bar sums by tone and each block keeps
|
|
1372
|
+
its own sentence — never "approved" for a Pass or an Accept. -->
|
|
1373
|
+
<ReviewProgress
|
|
1374
|
+
{...progress}
|
|
1375
|
+
size="sm"
|
|
1376
|
+
hideSentence
|
|
1377
|
+
label="Review progress across the page: {progress.headline}"
|
|
1378
|
+
/>
|
|
1379
|
+
<p
|
|
1380
|
+
class="text-base-content/70 mt-1.5 text-xs tabular-nums"
|
|
1381
|
+
aria-live="polite"
|
|
1382
|
+
>
|
|
1383
|
+
{progress.headline}
|
|
1384
|
+
</p>
|
|
1385
|
+
<ul
|
|
1386
|
+
class="mt-2 flex flex-col gap-2"
|
|
1387
|
+
aria-label="Progress by block"
|
|
1388
|
+
>
|
|
1389
|
+
{#each progress.lines as line (line.key)}
|
|
1390
|
+
<li class="flex flex-col gap-0.5">
|
|
1391
|
+
<span
|
|
1392
|
+
class="text-base-content/80 truncate text-xs font-medium"
|
|
1393
|
+
>{line.title}</span
|
|
1394
|
+
>
|
|
1395
|
+
<ReviewProgress
|
|
1396
|
+
verdicts={line.verdicts}
|
|
1397
|
+
counts={line.counts}
|
|
1398
|
+
total={line.total}
|
|
1399
|
+
retest={line.retest}
|
|
1400
|
+
decidedWord={line.decidedWord}
|
|
1401
|
+
size="xs"
|
|
1402
|
+
live={false}
|
|
1403
|
+
label="{line.title} review progress"
|
|
1404
|
+
/>
|
|
1405
|
+
</li>
|
|
1406
|
+
{/each}
|
|
1407
|
+
</ul>
|
|
1408
|
+
{/if}
|
|
1409
|
+
{/if}
|
|
1410
|
+
{#if openUnits.length > 0}
|
|
1411
|
+
<!-- Still-open jump cards (mos-cs31v): nobody scrolls back up
|
|
1412
|
+
hunting "1 still open" — the open items are HERE, one click
|
|
1413
|
+
from being in front of you. -->
|
|
1414
|
+
<div class="@container mt-3">
|
|
1415
|
+
<ul
|
|
1416
|
+
class="grid grid-cols-2 gap-2 @xl:grid-cols-3"
|
|
1417
|
+
aria-label="Still-open items"
|
|
1418
|
+
>
|
|
1419
|
+
{#each firstOpenCards as u (u.key)}
|
|
1420
|
+
<li>
|
|
1421
|
+
<button
|
|
1422
|
+
type="button"
|
|
1423
|
+
class="border-base-content/10 bg-base-100 hover:border-primary focus-visible:border-primary flex w-full items-center gap-2 rounded-box border p-1.5 text-left transition-colors"
|
|
1424
|
+
onclick={() => openNav.jumpTo(u)}
|
|
1425
|
+
aria-label="Jump to {unitLabel(u)}"
|
|
1426
|
+
>
|
|
1427
|
+
{#if u.thumb}
|
|
1428
|
+
<img
|
|
1429
|
+
src={u.thumb}
|
|
1430
|
+
alt=""
|
|
1431
|
+
loading="lazy"
|
|
1432
|
+
class="bg-base-content/5 h-10 w-10 shrink-0 rounded-field object-cover"
|
|
1433
|
+
/>
|
|
1434
|
+
{:else}
|
|
1435
|
+
<span
|
|
1436
|
+
class="bg-base-content/5 text-base-content/40 flex h-10 w-10 shrink-0 items-center justify-center rounded-field"
|
|
1437
|
+
aria-hidden="true"
|
|
1438
|
+
>
|
|
1439
|
+
<Glyph name={openIcon(u.type)} size={16} />
|
|
1440
|
+
</span>
|
|
1441
|
+
{/if}
|
|
1442
|
+
<span class="min-w-0 flex-1">
|
|
1443
|
+
<span class="block truncate text-xs font-medium">
|
|
1444
|
+
{unitLabel(u)}
|
|
1445
|
+
</span>
|
|
1446
|
+
{#if u.context && u.label}
|
|
1447
|
+
<span
|
|
1448
|
+
class="text-base-content/50 text-2xs block truncate"
|
|
1449
|
+
>
|
|
1450
|
+
{u.context}
|
|
1451
|
+
</span>
|
|
1452
|
+
{/if}
|
|
1453
|
+
{#if u.retest}
|
|
1454
|
+
<span class="text-warning text-2xs block font-medium">
|
|
1455
|
+
to retest
|
|
1456
|
+
</span>
|
|
1457
|
+
{/if}
|
|
1458
|
+
</span>
|
|
1459
|
+
</button>
|
|
1460
|
+
</li>
|
|
1461
|
+
{/each}
|
|
1462
|
+
{#if openUnits.length > OPEN_CARDS_MAX}
|
|
1463
|
+
<li class="col-span-2 flex items-center gap-2 @xl:col-span-3">
|
|
1464
|
+
<p class="text-base-content/60 text-xs">
|
|
1465
|
+
and {openUnits.length - OPEN_CARDS_MAX} more…
|
|
1466
|
+
</p>
|
|
1467
|
+
<button
|
|
1468
|
+
type="button"
|
|
1469
|
+
class="btn btn-outline btn-xs rounded-field ml-auto"
|
|
1470
|
+
onclick={() => openNav.jump(1)}
|
|
1471
|
+
>
|
|
1472
|
+
Next open
|
|
1473
|
+
</button>
|
|
1474
|
+
</li>
|
|
1475
|
+
{/if}
|
|
1476
|
+
</ul>
|
|
1477
|
+
</div>
|
|
1478
|
+
{/if}
|
|
1479
|
+
{#if pendingDeskDraft}
|
|
1480
|
+
<!-- The desk's draft (mos-chsj7 D2): what will be sent, said up
|
|
1481
|
+
front — the note pre-fills below and edits freely. -->
|
|
1482
|
+
<p
|
|
1483
|
+
class="border-info/30 bg-info/10 text-info-content mt-3 rounded-field border px-3 py-2 text-xs"
|
|
1484
|
+
data-desk-draft
|
|
1485
|
+
>
|
|
1486
|
+
{pendingDeskDraft.label}
|
|
1487
|
+
</p>
|
|
1488
|
+
{/if}
|
|
1489
|
+
<!-- The ONE composer (mos-rccb5): the Finish note carries text,
|
|
1490
|
+
voice/video/screen recordings and files (pasted, dropped or
|
|
1491
|
+
picked), stored in the artifact's private asset store and sent
|
|
1492
|
+
with the ONE answer. No submit row of its own — Send answers
|
|
1493
|
+
below is the page's one final action and drives the send. The
|
|
1494
|
+
desk-draft prefill (above) writes `finishText`, which the
|
|
1495
|
+
composer binds as its draft text — D2's semantics unchanged. -->
|
|
1496
|
+
<NoteComposer
|
|
1497
|
+
{artifactId}
|
|
1498
|
+
class="mt-3"
|
|
1499
|
+
bind:text={finishText}
|
|
1500
|
+
bind:clips={finishClips}
|
|
1501
|
+
rows={2}
|
|
1502
|
+
maxLength={4000}
|
|
1503
|
+
placeholder="Anything else to say? (optional)"
|
|
1504
|
+
label="Note with your answers (optional)"
|
|
1505
|
+
autofocus={composerOpen}
|
|
1506
|
+
/>
|
|
1507
|
+
{#if finishError}
|
|
1508
|
+
<div
|
|
1509
|
+
role="alert"
|
|
1510
|
+
class="border-error/30 bg-error/10 text-error-content rounded-box mt-2 border p-2 text-sm"
|
|
1511
|
+
>
|
|
1512
|
+
{finishError}
|
|
1513
|
+
</div>
|
|
1514
|
+
{/if}
|
|
1515
|
+
<div
|
|
1516
|
+
class="mt-3 flex flex-wrap items-center justify-end gap-x-3 gap-y-2"
|
|
1517
|
+
>
|
|
1518
|
+
<p class="text-base-content/60 mr-auto min-w-0 text-xs">
|
|
1519
|
+
{#if undecided > 0}
|
|
1520
|
+
{undecided} still open. Send now or keep going.
|
|
1521
|
+
{:else if deliverable}
|
|
1522
|
+
Goes to {recipient}
|
|
1523
|
+
{:else}
|
|
1524
|
+
<!-- mos-m3nfp: never promise a recipient nobody can be. -->
|
|
1525
|
+
No agent owns this page. Answers save here
|
|
1526
|
+
{/if}
|
|
1527
|
+
<span class="text-base-content/40 font-mono">· {taskId}</span>
|
|
1528
|
+
</p>
|
|
1529
|
+
<button
|
|
1530
|
+
type="button"
|
|
1531
|
+
bind:this={sendButton}
|
|
1532
|
+
class="btn btn-sm rounded-field {undecided > 0
|
|
1533
|
+
? 'btn-outline'
|
|
1534
|
+
: 'btn-primary'}"
|
|
1535
|
+
disabled={finishing}
|
|
1536
|
+
onclick={finishReview}
|
|
1537
|
+
>
|
|
1538
|
+
{finishing
|
|
1539
|
+
? "Sending…"
|
|
1540
|
+
: changed.total > 0
|
|
1541
|
+
? `Send changes (${changed.total})`
|
|
1542
|
+
: answered
|
|
1543
|
+
? "Send again"
|
|
1544
|
+
: finishError
|
|
1545
|
+
? "Send again"
|
|
1546
|
+
: "Send answers"}
|
|
1547
|
+
</button>
|
|
1548
|
+
</div>
|
|
1549
|
+
{/if}
|
|
1550
|
+
</section>
|
|
1551
|
+
{/if}
|
|
1552
|
+
</main>
|
|
1553
|
+
|
|
1554
|
+
{#if autoSend.pending}
|
|
1555
|
+
<!-- Auto-send (mos-yra79): polite, never steals focus; Undo and Send
|
|
1556
|
+
now are both reachable by Tab, and Esc undoes. The live text is
|
|
1557
|
+
announced once; the ticking seconds are visual only. -->
|
|
1558
|
+
<div
|
|
1559
|
+
class="fixed inset-x-0 bottom-4 z-40 flex justify-center px-4"
|
|
1560
|
+
transition:motionFade={{ duration: "fast" }}
|
|
1561
|
+
data-auto-send
|
|
1562
|
+
>
|
|
1563
|
+
<div
|
|
1564
|
+
class="bg-base-content text-base-100 rounded-box flex max-w-full flex-wrap items-center gap-x-3 gap-y-2 px-4 py-2.5 text-sm shadow-lg"
|
|
1565
|
+
>
|
|
1566
|
+
<p role="status" aria-live="polite">
|
|
1567
|
+
<span class="font-semibold">All decided.</span>
|
|
1568
|
+
<span class="sr-only"
|
|
1569
|
+
>Your answers send in {AUTO_SEND_DELAY_MS / 1000} seconds. Press Undo
|
|
1570
|
+
to keep them as a draft.</span
|
|
1571
|
+
>
|
|
1572
|
+
<span aria-hidden="true"
|
|
1573
|
+
>Sending in <span class="tabular-nums">{autoSend.remaining}s</span
|
|
1574
|
+
></span
|
|
1575
|
+
>
|
|
1576
|
+
</p>
|
|
1577
|
+
<button
|
|
1578
|
+
type="button"
|
|
1579
|
+
class="btn btn-sm btn-ghost text-base-100 rounded-field"
|
|
1580
|
+
onclick={() => autoSend.undo()}
|
|
1581
|
+
data-auto-send-undo
|
|
1582
|
+
>
|
|
1583
|
+
Undo
|
|
1584
|
+
</button>
|
|
1585
|
+
<button
|
|
1586
|
+
type="button"
|
|
1587
|
+
class="btn btn-sm btn-primary rounded-field"
|
|
1588
|
+
onclick={() => autoSend.sendNow()}
|
|
1589
|
+
>
|
|
1590
|
+
Send now
|
|
1591
|
+
</button>
|
|
1592
|
+
</div>
|
|
1593
|
+
</div>
|
|
1594
|
+
{/if}
|
|
1595
|
+
|
|
1596
|
+
<!-- Jump announcements (mos-cs31v): the header chip's count is live, and
|
|
1597
|
+
focus lands ON the item, but the jump itself says where it landed —
|
|
1598
|
+
a card whose own label announces nothing still reports the jump. -->
|
|
1599
|
+
<p class="sr-only" role="status" aria-live="polite">
|
|
1600
|
+
{jumpAnnouncement}
|
|
1601
|
+
</p>
|
|
1602
|
+
</div>
|
|
1603
|
+
|
|
1604
|
+
<style>
|
|
1605
|
+
/* mos-8fvt9: the page's Listen pill rides the meta line; once opened,
|
|
1606
|
+
jatui's bar (.lc-bar) replaces the pill and the cell takes a new
|
|
1607
|
+
full-width row of the header (flex-basis 100% forces the wrap). */
|
|
1608
|
+
.meta-player {
|
|
1609
|
+
margin-block: -0.25rem;
|
|
1610
|
+
}
|
|
1611
|
+
.meta-player:has(:global(.lc-bar)) {
|
|
1612
|
+
flex-basis: 100%;
|
|
1613
|
+
margin-block: 0.25rem 0;
|
|
1614
|
+
margin-left: 0;
|
|
1615
|
+
}
|
|
1616
|
+
</style>
|