@joewinke/jatui 0.36.3 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/dist/components/Button.svelte +8 -0
- package/dist/components/TextInput.svelte +39 -6
- package/dist/components/TextInput.svelte.d.ts +15 -7
- package/dist/components/basic/Checkbox.svelte +52 -14
- package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
- package/package.json +11 -2
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DiffReviewBlock — the Redline review archetype (docs/review-ux/
|
|
4
|
+
* proposal.md): per-section before/after diffs with accept / modify /
|
|
5
|
+
* reject (mos-a3ivi). Built for contract and copy reviews (a BAA/MSA pass):
|
|
6
|
+
* each section is a card showing the current text against the proposed
|
|
7
|
+
* text through jatui's HunkDiffView (word-level highlights; side by side
|
|
8
|
+
* when the block is wide, unified on a phone), and the reviewer decides it.
|
|
9
|
+
*
|
|
10
|
+
* The review chrome is the shared jatui review set (mos-lsmve.6, M2):
|
|
11
|
+
* VerdictControl + ReviewNote + RoundHistory per section, ReviewProgress +
|
|
12
|
+
* ReviewFilterBar on top, reviewKeys() for the one keyboard map and
|
|
13
|
+
* ReviewShortcuts rendered from the same key table. "Modify" opens an
|
|
14
|
+
* inline editor seeded with the proposed text; the reviewer's rewrite
|
|
15
|
+
* saves as `edited` and renders as a second diff (proposed → yours) so the
|
|
16
|
+
* agent reading feedback sees exactly what changed.
|
|
17
|
+
*
|
|
18
|
+
* State: `{ [itemId]: { verdict, note, edited, at, round?, history? } }`
|
|
19
|
+
* through ctx.saveState (see decisions.ts) — the flat fields are the
|
|
20
|
+
* current round; a section the team marks ready again (`team.readySince`,
|
|
21
|
+
* D7) opens a new round and the previous verdict + note lock into
|
|
22
|
+
* history. Without saveState (public link, print, tests) the block stays
|
|
23
|
+
* interactive and nothing persists; a failed save shows in the block with
|
|
24
|
+
* a Retry; on the read-only route the verdict chrome is hidden and the
|
|
25
|
+
* diffs stand alone.
|
|
26
|
+
*/
|
|
27
|
+
import HunkDiffView from "../../../components/HunkDiffView.svelte"
|
|
28
|
+
import ReviewFilterBar from "../../../components/review/ReviewFilterBar.svelte"
|
|
29
|
+
import ReviewProgress from "../../../components/review/ReviewProgress.svelte"
|
|
30
|
+
import ReviewShortcuts from "../../../components/review/ReviewShortcuts.svelte"
|
|
31
|
+
import RoundHistory from "../../../components/review/RoundHistory.svelte"
|
|
32
|
+
import VerdictControl from "../../../components/review/VerdictControl.svelte"
|
|
33
|
+
import { buildReviewFilters } from "../../../components/review/reviewSummary"
|
|
34
|
+
import { isTypingTarget } from "../../../utils/keyboardShortcutRegistry"
|
|
35
|
+
import {
|
|
36
|
+
reviewKeys,
|
|
37
|
+
type NeedsYou,
|
|
38
|
+
type ReviewKeyRow,
|
|
39
|
+
type ReviewKeysOptions,
|
|
40
|
+
type VerdictOption,
|
|
41
|
+
} from "../../../actions/reviewKeys"
|
|
42
|
+
import { showConfirm } from "../../../stores/confirmDialog"
|
|
43
|
+
import ItemNote from "../../review/ItemNote.svelte"
|
|
44
|
+
import type { NoteAttachment } from "../../review/attachments.js"
|
|
45
|
+
import { diffLines } from "diff"
|
|
46
|
+
import { tick, untrack } from "svelte"
|
|
47
|
+
import type { BlockProps } from "../../types.js"
|
|
48
|
+
import BulkRejectForm from "../../review/BulkRejectForm.svelte"
|
|
49
|
+
import BulkUndoBar from "../../review/BulkUndoBar.svelte"
|
|
50
|
+
import { decideMany, undoMany, type BulkUndo } from "../../review/bulk.js"
|
|
51
|
+
import NoteToggle from "../../review/NoteToggle.svelte"
|
|
52
|
+
import TeamChip from "../../review/TeamChip.svelte"
|
|
53
|
+
import { historyRounds } from "../../review/roundLines.js"
|
|
54
|
+
import type { RoundView } from "../../review/rounds.js"
|
|
55
|
+
import { createStateSaver } from "../../review/saver.js"
|
|
56
|
+
import { teamRound } from "../../review/team.js"
|
|
57
|
+
import {
|
|
58
|
+
nextUndecided,
|
|
59
|
+
sortRanks,
|
|
60
|
+
visualItems,
|
|
61
|
+
} from "../../review/visualOrder.js"
|
|
62
|
+
import {
|
|
63
|
+
EDITED_MAX_CHARS,
|
|
64
|
+
diffRounds,
|
|
65
|
+
isEmptyDecision,
|
|
66
|
+
parseDecisions,
|
|
67
|
+
type DiffDecision,
|
|
68
|
+
type DiffVerdict,
|
|
69
|
+
} from "./decisions.js"
|
|
70
|
+
import type { DiffReviewItem, DiffReviewParams } from "./types.js"
|
|
71
|
+
import SectionTools from "../../SectionTools.svelte"
|
|
72
|
+
|
|
73
|
+
let { params, ctx }: BlockProps<DiffReviewParams> = $props()
|
|
74
|
+
|
|
75
|
+
const VERDICTS: VerdictOption[] = [
|
|
76
|
+
{ id: "approve", label: "Accept", tone: "success", key: "a" },
|
|
77
|
+
{
|
|
78
|
+
id: "modify",
|
|
79
|
+
label: "Modify",
|
|
80
|
+
tone: "warning",
|
|
81
|
+
// m (mos-u8i35, Joe's flip keys); x stays as an alias, see onBlockKey.
|
|
82
|
+
key: "m",
|
|
83
|
+
opensNote: true,
|
|
84
|
+
},
|
|
85
|
+
{ id: "reject", label: "Reject", tone: "error", key: "r", opensNote: true },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/** Stored entries — seeded ONCE from the viewer's saved state (the host
|
|
89
|
+
* passes state at render, never streams it). */
|
|
90
|
+
let decisions = $state<Record<string, DiffDecision>>(
|
|
91
|
+
untrack(() => parseDecisions(ctx.state)),
|
|
92
|
+
)
|
|
93
|
+
let filter = $state("all")
|
|
94
|
+
let notesOpen = $state<Record<string, boolean>>({})
|
|
95
|
+
let editorsOpen = $state<Record<string, boolean>>({})
|
|
96
|
+
/** The verdict a section's ReviewNote reacts to, when it must NOT follow
|
|
97
|
+
* the saved one: typing a rewrite turns the section into a Modify, and
|
|
98
|
+
* that must not yank the caret out of the editor into the note. */
|
|
99
|
+
let noteVerdictPin = $state<Record<string, DiffVerdict | null>>({})
|
|
100
|
+
let helpOpen = $state(false)
|
|
101
|
+
let rootEl = $state<HTMLElement | null>(null)
|
|
102
|
+
let width = $state(0)
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* mos-guqgs: deciding requires a save channel, not merely a non-readonly
|
|
106
|
+
* ctx. `!ctx.readonly` left the share link and any saveless mount (where
|
|
107
|
+
* readonly was never passed) with live verdict controls over a save that
|
|
108
|
+
* could not happen. Same contract as checklist / compare / callout.
|
|
109
|
+
*/
|
|
110
|
+
const canDecide = $derived(Boolean(ctx.saveState) && !ctx.readonly)
|
|
111
|
+
const saver = createStateSaver(() => ctx.saveState)
|
|
112
|
+
|
|
113
|
+
const unified = $derived(
|
|
114
|
+
params.layout === "unified"
|
|
115
|
+
? true
|
|
116
|
+
: params.layout === "split"
|
|
117
|
+
? false
|
|
118
|
+
: width < 720,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
/** End every non-empty side with a newline so the last line diffs cleanly
|
|
122
|
+
* ("foo" vs "foo\n" would otherwise read as a change). */
|
|
123
|
+
function norm(text: string): string {
|
|
124
|
+
return text && !text.endsWith("\n") ? `${text}\n` : text
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function lineStats(a: string, b: string) {
|
|
128
|
+
let removed = 0
|
|
129
|
+
let added = 0
|
|
130
|
+
for (const c of diffLines(norm(a), norm(b))) {
|
|
131
|
+
if (c.removed) removed += c.count ?? 1
|
|
132
|
+
else if (c.added) added += c.count ?? 1
|
|
133
|
+
}
|
|
134
|
+
return { removed, added }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const itemsById = $derived(
|
|
138
|
+
new Map<string, DiffReviewItem>(params.items.map((i) => [i.id, i])),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const sections = $derived.by(() => {
|
|
142
|
+
const named = (params.groups ?? [])
|
|
143
|
+
.map((g) => ({
|
|
144
|
+
id: g.id,
|
|
145
|
+
title: g.title,
|
|
146
|
+
items: params.items.filter((it) => it.group === g.id),
|
|
147
|
+
}))
|
|
148
|
+
.filter((g) => g.items.length > 0)
|
|
149
|
+
const listed = new Set(named.flatMap((g) => g.items.map((i) => i.id)))
|
|
150
|
+
const rest = params.items.filter((it) => !listed.has(it.id))
|
|
151
|
+
// Nothing published is ever hidden: ungrouped items get a catch-all.
|
|
152
|
+
return rest.length > 0
|
|
153
|
+
? [
|
|
154
|
+
...named,
|
|
155
|
+
{
|
|
156
|
+
id: "__other__",
|
|
157
|
+
title: named.length > 0 ? "Other" : "",
|
|
158
|
+
items: rest,
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: named
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/** Each section's current round + locked history (pure read). The only
|
|
165
|
+
* round a section has is the team's readySince (D7). */
|
|
166
|
+
const views = $derived.by(() => {
|
|
167
|
+
const out: Record<string, RoundView> = {}
|
|
168
|
+
for (const item of params.items)
|
|
169
|
+
out[item.id] = diffRounds.view(decisions[item.id], teamRound(item.team))
|
|
170
|
+
return out
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
function currentOf(id: string): DiffDecision {
|
|
174
|
+
return (views[id]?.current ?? {}) as DiffDecision
|
|
175
|
+
}
|
|
176
|
+
function verdictOf(id: string): DiffVerdict | null {
|
|
177
|
+
return currentOf(id).verdict ?? null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A new round is open and this viewer has not decided it. */
|
|
181
|
+
function isRetest(item: DiffReviewItem): boolean {
|
|
182
|
+
if (verdictOf(item.id)) return false
|
|
183
|
+
return item.team?.status === "ready" || !!views[item.id]?.stale
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const counts = $derived.by(() => {
|
|
187
|
+
const byVerdict: Record<string, number> = {}
|
|
188
|
+
let retest = 0
|
|
189
|
+
for (const item of params.items) {
|
|
190
|
+
const v = verdictOf(item.id)
|
|
191
|
+
if (v) byVerdict[v] = (byVerdict[v] ?? 0) + 1
|
|
192
|
+
else if (isRetest(item)) retest++
|
|
193
|
+
}
|
|
194
|
+
return { byVerdict, retest, total: params.items.length }
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/** The chips read as states, "Accepted 40 / Rejected 12" (mos-u8i35): the
|
|
198
|
+
* reviewer scans one group and flips the outliers. */
|
|
199
|
+
const FILTER_VERDICTS: VerdictOption[] = VERDICTS.map((o) => ({
|
|
200
|
+
...o,
|
|
201
|
+
label:
|
|
202
|
+
o.id === "approve"
|
|
203
|
+
? "Accepted"
|
|
204
|
+
: o.id === "modify"
|
|
205
|
+
? "Modified"
|
|
206
|
+
: "Rejected",
|
|
207
|
+
}))
|
|
208
|
+
|
|
209
|
+
const filters = $derived(
|
|
210
|
+
buildReviewFilters({
|
|
211
|
+
verdicts: FILTER_VERDICTS,
|
|
212
|
+
counts: counts.byVerdict,
|
|
213
|
+
total: counts.total,
|
|
214
|
+
retest: counts.retest,
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
function visible(id: string): boolean {
|
|
219
|
+
if (filter === "all") return true
|
|
220
|
+
const v = verdictOf(id)
|
|
221
|
+
if (filter === "undecided") return !v
|
|
222
|
+
if (filter === "retest") {
|
|
223
|
+
const item = itemsById.get(id)
|
|
224
|
+
return !!item && isRetest(item)
|
|
225
|
+
}
|
|
226
|
+
return v === filter
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function needsYou(id: string): NeedsYou {
|
|
230
|
+
const item = itemsById.get(id)
|
|
231
|
+
if (!item || verdictOf(id)) return false
|
|
232
|
+
return isRetest(item) ? "retest" : "undecided"
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- saving -----------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Apply one change under the section's current round (the first save in
|
|
238
|
+
* a new round locks the old one into history). Local only. */
|
|
239
|
+
function update(item: DiffReviewItem, patch: Record<string, unknown>) {
|
|
240
|
+
const next = diffRounds.save(
|
|
241
|
+
decisions[item.id],
|
|
242
|
+
{ ...patch, at: new Date().toISOString() },
|
|
243
|
+
teamRound(item.team),
|
|
244
|
+
) as DiffDecision
|
|
245
|
+
const updated = { ...decisions }
|
|
246
|
+
if (isEmptyDecision(next)) delete updated[item.id]
|
|
247
|
+
else updated[item.id] = next
|
|
248
|
+
decisions = updated
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function flush(): Promise<void> {
|
|
252
|
+
return saver.save($state.snapshot(decisions) as Record<string, unknown>)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function persist(item: DiffReviewItem, patch: Record<string, unknown>) {
|
|
256
|
+
update(item, patch)
|
|
257
|
+
// A failure shows as the block's alert (saver.failed), not a throw.
|
|
258
|
+
flush().catch(() => {})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function unpinNote(id: string) {
|
|
262
|
+
if (!(id in noteVerdictPin)) return
|
|
263
|
+
const { [id]: _drop, ...rest } = noteVerdictPin
|
|
264
|
+
noteVerdictPin = rest
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** VerdictControl never re-sends the active verdict (mos-yntqm); Undo
|
|
268
|
+
* clears. A new Reject / Modify opens + focuses the note (ReviewNote,
|
|
269
|
+
* mos-calzz) — the note stays optional, the verdict is already saved.
|
|
270
|
+
* Modify also opens its rewrite editor; the note takes the caret (the
|
|
271
|
+
* ruling), the seeded editor waits one `e` away. A repeat KEY on a
|
|
272
|
+
* note-worthy verdict re-opens the note. */
|
|
273
|
+
function setVerdict(id: string, verdict: DiffVerdict) {
|
|
274
|
+
const item = itemsById.get(id)
|
|
275
|
+
if (!item || !canDecide) return
|
|
276
|
+
if (verdict === "modify") editorsOpen = { ...editorsOpen, [id]: true }
|
|
277
|
+
if (verdictOf(id) === verdict) {
|
|
278
|
+
unpinNote(id)
|
|
279
|
+
if (VERDICTS.find((o) => o.id === verdict)?.opensNote) openNote(id)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
unpinNote(id)
|
|
283
|
+
// A line flipped to Reject after a bulk reject borrows that bulk's
|
|
284
|
+
// reason, so Esc can keep the reject with no extra typing (mos-u8i35).
|
|
285
|
+
if (verdict === "reject" && sharedRejectNote && !currentOf(id).note)
|
|
286
|
+
persist(item, { verdict, note: sharedRejectNote })
|
|
287
|
+
else persist(item, { verdict })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Clear the verdict; the note and any rewrite stay (a remark or a draft
|
|
291
|
+
* is not a verdict, and typing is never thrown away). */
|
|
292
|
+
function undoVerdict(id: string) {
|
|
293
|
+
const item = itemsById.get(id)
|
|
294
|
+
if (!item || !canDecide || !verdictOf(id)) return
|
|
295
|
+
unpinNote(id)
|
|
296
|
+
persist(item, { verdict: undefined })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function onNoteInput(id: string, text: string) {
|
|
300
|
+
const item = itemsById.get(id)
|
|
301
|
+
if (item) update(item, { note: text.slice(0, 2000) || undefined })
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The note's uploaded clips (mos-rccb5): each upload adds its descriptor
|
|
305
|
+
* to the item's saved entry — the bytes are already in the artifact's
|
|
306
|
+
* private asset store by the time this fires. */
|
|
307
|
+
function onItemClips(id: string, clips: NoteAttachment[]) {
|
|
308
|
+
const item = itemsById.get(id)
|
|
309
|
+
if (item) persist(item, { attachments: clips })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** ReviewNote's debounced save: rejects so the note shows "Not saved". */
|
|
313
|
+
const noteSave = $derived(ctx.saveState ? () => flush() : undefined)
|
|
314
|
+
|
|
315
|
+
/** Typing a rewrite IS a modify: the verdict follows the edit. Text equal
|
|
316
|
+
* to the proposal is no rewrite at all, so `edited` drops. */
|
|
317
|
+
function setEdited(item: DiffReviewItem, text: string) {
|
|
318
|
+
const prev = verdictOf(item.id)
|
|
319
|
+
if (prev !== "modify" && !(item.id in noteVerdictPin))
|
|
320
|
+
noteVerdictPin = { ...noteVerdictPin, [item.id]: prev }
|
|
321
|
+
persist(item, {
|
|
322
|
+
verdict: "modify",
|
|
323
|
+
edited:
|
|
324
|
+
norm(text) === norm(item.after)
|
|
325
|
+
? undefined
|
|
326
|
+
: text.slice(0, EDITED_MAX_CHARS),
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function resetEdit(item: DiffReviewItem) {
|
|
331
|
+
if (currentOf(item.id).edited === undefined) return
|
|
332
|
+
persist(item, { edited: undefined })
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function noteVerdictOf(id: string): string | null {
|
|
336
|
+
return id in noteVerdictPin ? noteVerdictPin[id] : verdictOf(id)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function openNote(id: string) {
|
|
340
|
+
notesOpen = { ...notesOpen, [id]: true }
|
|
341
|
+
tick().then(() => document.getElementById(`noteinput-${id}`)?.focus())
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function toggleNote(id: string) {
|
|
345
|
+
if (!canDecide) return
|
|
346
|
+
if (notesOpen[id]) notesOpen = { ...notesOpen, [id]: false }
|
|
347
|
+
else openNote(id)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function openEditor(id: string) {
|
|
351
|
+
if (!canDecide) return
|
|
352
|
+
editorsOpen = { ...editorsOpen, [id]: true }
|
|
353
|
+
tick().then(() => {
|
|
354
|
+
rootEl
|
|
355
|
+
?.querySelector<HTMLTextAreaElement>(`#edit-${CSS.escape(id)}`)
|
|
356
|
+
?.focus()
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function closeEditor(id: string) {
|
|
361
|
+
editorsOpen = { ...editorsOpen, [id]: false }
|
|
362
|
+
rootEl
|
|
363
|
+
?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(id)}"]`)
|
|
364
|
+
?.focus()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** a from the keyboard: accept, then move on to the next UNDECIDED
|
|
368
|
+
* section (mos-lsmve.12), or the plain next one when none is left. "Next"
|
|
369
|
+
* is captured BEFORE deciding: under the Undecided filter this section is
|
|
370
|
+
* about to hide. Reject and Modify open the note instead of advancing
|
|
371
|
+
* (mos-calzz). */
|
|
372
|
+
function keyVerdict(id: string, v: DiffVerdict) {
|
|
373
|
+
if (v !== "approve") return setVerdict(id, v)
|
|
374
|
+
const items = visualItems(rootEl)
|
|
375
|
+
const at = items.findIndex((el) => el.dataset.itemId === id)
|
|
376
|
+
const nextId = nextUndecided(
|
|
377
|
+
items.map((el) => el.dataset.itemId ?? ""),
|
|
378
|
+
id,
|
|
379
|
+
(other) => !!verdictOf(other),
|
|
380
|
+
)
|
|
381
|
+
const next =
|
|
382
|
+
items.find((el) => el.dataset.itemId === nextId) ??
|
|
383
|
+
(at >= 0 ? items[(at + 1) % items.length] : null)
|
|
384
|
+
setVerdict(id, v)
|
|
385
|
+
if (next && next.dataset.itemId !== id) tick().then(() => next.focus())
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- bulk accept / reject (mos-nv4eb, mos-u8i35) --------------------------
|
|
389
|
+
|
|
390
|
+
/** Lines a bulk may decide: still undecided, not a retest (a line the
|
|
391
|
+
* team reopened deserves eyes — media-picker's Approve all rule), and
|
|
392
|
+
* shown under the active filter chip. A decided line is never touched. */
|
|
393
|
+
function bulkTargets(items: readonly DiffReviewItem[]): DiffReviewItem[] {
|
|
394
|
+
return items.filter(
|
|
395
|
+
(it) => !verdictOf(it.id) && !isRetest(it) && visible(it.id),
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const remaining = $derived(bulkTargets(params.items))
|
|
400
|
+
const hasRisk = $derived(params.items.some((it) => it.risk))
|
|
401
|
+
const lowRisk = $derived(remaining.filter((it) => it.risk === "low"))
|
|
402
|
+
|
|
403
|
+
/** The last bulk, for its Undo (cleared by Undo, dismiss or the next bulk). */
|
|
404
|
+
let lastBulk = $state<{
|
|
405
|
+
undo: BulkUndo<DiffDecision>
|
|
406
|
+
count: number
|
|
407
|
+
} | null>(null)
|
|
408
|
+
/** The open bulk-reject form: which lines it will reject, and the control
|
|
409
|
+
* that opened it (focus goes back there on Cancel). */
|
|
410
|
+
let rejectAsk = $state<{ ids: string[]; opener: HTMLElement | null } | null>(
|
|
411
|
+
null,
|
|
412
|
+
)
|
|
413
|
+
/** The reason the last bulk reject gave, reused by a later single flip. */
|
|
414
|
+
let sharedRejectNote = $state<string | null>(null)
|
|
415
|
+
|
|
416
|
+
/** Decide every target in ONE save — the same save path as a single
|
|
417
|
+
* click, batched. Decisions stay drafts until Finish review. */
|
|
418
|
+
function decideTargets(
|
|
419
|
+
targets: readonly DiffReviewItem[],
|
|
420
|
+
verdict: "approve" | "reject",
|
|
421
|
+
note?: string,
|
|
422
|
+
) {
|
|
423
|
+
if (!canDecide || targets.length === 0) return
|
|
424
|
+
for (const it of targets) unpinNote(it.id)
|
|
425
|
+
const { decisions: next, undo } = decideMany(
|
|
426
|
+
decisions,
|
|
427
|
+
targets.map((it) => ({ id: it.id, round: teamRound(it.team) })),
|
|
428
|
+
{ verdict, note, at: new Date().toISOString(), save: diffRounds.save },
|
|
429
|
+
)
|
|
430
|
+
decisions = next
|
|
431
|
+
lastBulk = { undo, count: targets.length }
|
|
432
|
+
flush().catch(() => {})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function lineCount(n: number): string {
|
|
436
|
+
return n === 1 ? "1 line" : `${n} lines`
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Page-level bulk accept (all remaining, or all low-risk): asks first. */
|
|
440
|
+
async function confirmAccept(targets: readonly DiffReviewItem[]) {
|
|
441
|
+
if (!canDecide || targets.length === 0) return
|
|
442
|
+
const ids = targets.map((it) => it.id)
|
|
443
|
+
const ok = await showConfirm({
|
|
444
|
+
title: `Accept ${lineCount(targets.length)}?`,
|
|
445
|
+
body: "You can still change any line after. Lines you already decided stay as they are.",
|
|
446
|
+
confirmLabel: "Accept",
|
|
447
|
+
cancelLabel: "Cancel",
|
|
448
|
+
danger: false,
|
|
449
|
+
timeoutMs: 0,
|
|
450
|
+
})
|
|
451
|
+
if (!ok) return
|
|
452
|
+
// Re-check after the dialog: a line decided meanwhile is not overwritten.
|
|
453
|
+
decideTargets(
|
|
454
|
+
bulkTargets(params.items).filter((it) => ids.includes(it.id)),
|
|
455
|
+
"approve",
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Bulk reject (page or section): the inline form asks ONE shared note. */
|
|
460
|
+
function askReject(targets: readonly DiffReviewItem[]) {
|
|
461
|
+
if (!canDecide || targets.length === 0) return
|
|
462
|
+
const active = document.activeElement
|
|
463
|
+
rejectAsk = {
|
|
464
|
+
ids: targets.map((it) => it.id),
|
|
465
|
+
opener: active instanceof HTMLElement ? active : null,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const rejectCount = $derived(
|
|
470
|
+
rejectAsk
|
|
471
|
+
? bulkTargets(params.items).filter((it) => rejectAsk!.ids.includes(it.id))
|
|
472
|
+
.length
|
|
473
|
+
: 0,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
function confirmReject(note: string) {
|
|
477
|
+
if (!rejectAsk) return
|
|
478
|
+
const ids = rejectAsk.ids
|
|
479
|
+
rejectAsk = null
|
|
480
|
+
sharedRejectNote = note
|
|
481
|
+
decideTargets(
|
|
482
|
+
bulkTargets(params.items).filter((it) => ids.includes(it.id)),
|
|
483
|
+
"reject",
|
|
484
|
+
note,
|
|
485
|
+
)
|
|
486
|
+
// The lines are decided now: start the flip pass at the first one.
|
|
487
|
+
tick().then(() =>
|
|
488
|
+
visualItems(rootEl)
|
|
489
|
+
.find((el) => !el.hidden)
|
|
490
|
+
?.focus(),
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function cancelReject() {
|
|
495
|
+
const opener = rejectAsk?.opener
|
|
496
|
+
rejectAsk = null
|
|
497
|
+
tick().then(() => opener?.isConnected && opener.focus())
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function undoBulk() {
|
|
501
|
+
if (!lastBulk || !canDecide) return
|
|
502
|
+
decisions = undoMany(decisions, lastBulk.undo)
|
|
503
|
+
lastBulk = null
|
|
504
|
+
flush().catch(() => {})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** The card a key acts on: the one holding focus, inside this block. */
|
|
508
|
+
function focusedItemId(target: EventTarget | null): string | null {
|
|
509
|
+
const el =
|
|
510
|
+
target instanceof Element
|
|
511
|
+
? target.closest<HTMLElement>("[data-item-id]")
|
|
512
|
+
: null
|
|
513
|
+
return el && rootEl?.contains(el) ? (el.dataset.itemId ?? null) : null
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Block keys beyond the shared review map (mos-nv4eb, mos-u8i35):
|
|
517
|
+
* - Shift+A / Shift+R: accept / reject all remaining (both ask first);
|
|
518
|
+
* - x: Modify, kept as an alias of m for the old shared slot;
|
|
519
|
+
* - Enter in a REJECT line's note: save it and go back to the line, so a
|
|
520
|
+
* flip pass is r, type, Enter, j.
|
|
521
|
+
* Never with Ctrl / Meta / Alt; the keys never fire while typing (the
|
|
522
|
+
* note's Enter is the one deliberate exception). */
|
|
523
|
+
function onBlockKey(e: KeyboardEvent) {
|
|
524
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return
|
|
525
|
+
if (e.defaultPrevented || e.isComposing || !canDecide) return
|
|
526
|
+
if (isTypingTarget(e.target)) {
|
|
527
|
+
const t = e.target as HTMLElement
|
|
528
|
+
if (e.key !== "Enter" || e.shiftKey || !t.id.startsWith("noteinput-"))
|
|
529
|
+
return
|
|
530
|
+
const id = t.id.slice("noteinput-".length)
|
|
531
|
+
if (verdictOf(id) !== "reject") return
|
|
532
|
+
e.preventDefault()
|
|
533
|
+
notesOpen = { ...notesOpen, [id]: false }
|
|
534
|
+
flush().catch(() => {})
|
|
535
|
+
rootEl
|
|
536
|
+
?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(id)}"]`)
|
|
537
|
+
?.focus()
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
if (e.shiftKey && (e.key === "A" || e.key === "R")) {
|
|
541
|
+
if (remaining.length === 0) return
|
|
542
|
+
e.preventDefault()
|
|
543
|
+
if (e.key === "A") void confirmAccept(remaining)
|
|
544
|
+
else askReject(remaining)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
if (e.key === "x" && !e.shiftKey) {
|
|
548
|
+
const id = focusedItemId(e.target)
|
|
549
|
+
if (!id) return
|
|
550
|
+
e.preventDefault()
|
|
551
|
+
keyVerdict(id, "modify")
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// On the block only, like the review map (`global: false`).
|
|
556
|
+
$effect(() => {
|
|
557
|
+
const el = rootEl
|
|
558
|
+
if (!el) return
|
|
559
|
+
el.addEventListener("keydown", onBlockKey)
|
|
560
|
+
return () => el.removeEventListener("keydown", onBlockKey)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const bulkKeyRows: ReviewKeyRow[] = $derived(
|
|
564
|
+
canDecide
|
|
565
|
+
? [
|
|
566
|
+
{
|
|
567
|
+
keys: ["Shift", "A"],
|
|
568
|
+
description: "Accept all remaining lines (asks first)",
|
|
569
|
+
group: "Decide",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
keys: ["Shift", "R"],
|
|
573
|
+
description: "Reject all remaining lines (asks for one note)",
|
|
574
|
+
group: "Decide",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
keys: ["Enter"],
|
|
578
|
+
description: "In a reject note: save it and go back to the line",
|
|
579
|
+
group: "Decide",
|
|
580
|
+
},
|
|
581
|
+
]
|
|
582
|
+
: [],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
const keysConfig: ReviewKeysOptions = $derived({
|
|
586
|
+
verdicts: VERDICTS,
|
|
587
|
+
onVerdict: canDecide
|
|
588
|
+
? (id: string, v: string) => keyVerdict(id, v as DiffVerdict)
|
|
589
|
+
: undefined,
|
|
590
|
+
onUndo: canDecide ? undoVerdict : undefined,
|
|
591
|
+
onNote: canDecide ? toggleNote : undefined,
|
|
592
|
+
onEdit: canDecide ? openEditor : undefined,
|
|
593
|
+
onOpen: canDecide ? (id: string) => openEditor(id) : undefined,
|
|
594
|
+
needsYou: canDecide ? needsYou : undefined,
|
|
595
|
+
onHelp: () => (helpOpen = true),
|
|
596
|
+
getItems: () => visualItems(rootEl),
|
|
597
|
+
global: false,
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
const VERDICT_WORD: Record<DiffVerdict, string> = {
|
|
601
|
+
approve: "accepted",
|
|
602
|
+
modify: "modified",
|
|
603
|
+
reject: "rejected",
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const RISK_CLASS = {
|
|
607
|
+
low: "badge-ghost",
|
|
608
|
+
medium: "badge-warning",
|
|
609
|
+
high: "badge-error",
|
|
610
|
+
} as const
|
|
611
|
+
|
|
612
|
+
/** Visual sort: undecided first, then modified, accepted, rejected —
|
|
613
|
+
* through the frozen `ranks` snapshot, never live verdicts. */
|
|
614
|
+
const ORDER: Record<DiffVerdict, number> = {
|
|
615
|
+
modify: 1,
|
|
616
|
+
approve: 2,
|
|
617
|
+
reject: 3,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Frozen sort (mos-lsmve.12, ../../review/visualOrder): re-taken only on
|
|
621
|
+
* a filter change, so a section decided mid-session stays put. */
|
|
622
|
+
function snapshotRanks() {
|
|
623
|
+
return sortRanks(
|
|
624
|
+
params.items.map((it) => it.id),
|
|
625
|
+
verdictOf,
|
|
626
|
+
ORDER,
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
let ranks = $state<Record<string, number>>(untrack(snapshotRanks))
|
|
630
|
+
$effect(() => {
|
|
631
|
+
void filter
|
|
632
|
+
untrack(() => (ranks = snapshotRanks()))
|
|
633
|
+
})
|
|
634
|
+
</script>
|
|
635
|
+
|
|
636
|
+
<!-- The section cards are the interactive elements; jatui reviewKeys
|
|
637
|
+
(listening on this block only) acts on the focused card. -->
|
|
638
|
+
<section
|
|
639
|
+
bind:this={rootEl}
|
|
640
|
+
bind:clientWidth={width}
|
|
641
|
+
class="@container rounded-box border border-base-content/10 bg-base-100"
|
|
642
|
+
aria-label={params.title ?? "Diff review"}
|
|
643
|
+
use:reviewKeys={keysConfig}
|
|
644
|
+
>
|
|
645
|
+
{#if params.title || params.intro}
|
|
646
|
+
<header class="px-4 pt-4 pb-1 sm:px-5">
|
|
647
|
+
{#if params.title}
|
|
648
|
+
<div class="flex items-center gap-2">
|
|
649
|
+
<h2 class="min-w-0 text-lg font-semibold tracking-tight">
|
|
650
|
+
{params.title}
|
|
651
|
+
</h2>
|
|
652
|
+
<SectionTools {ctx} inline />
|
|
653
|
+
</div>
|
|
654
|
+
{/if}
|
|
655
|
+
{#if params.intro}
|
|
656
|
+
<p
|
|
657
|
+
class="mt-0.5 max-w-[70ch] text-sm whitespace-pre-line text-base-content/75"
|
|
658
|
+
>
|
|
659
|
+
{params.intro}
|
|
660
|
+
</p>
|
|
661
|
+
{/if}
|
|
662
|
+
</header>
|
|
663
|
+
{/if}
|
|
664
|
+
|
|
665
|
+
<div
|
|
666
|
+
class="flex flex-col gap-2 border-b border-base-content/10 px-4 py-2.5 sm:px-5"
|
|
667
|
+
>
|
|
668
|
+
<ReviewProgress
|
|
669
|
+
verdicts={VERDICTS}
|
|
670
|
+
counts={counts.byVerdict}
|
|
671
|
+
total={counts.total}
|
|
672
|
+
retest={counts.retest}
|
|
673
|
+
size="xs"
|
|
674
|
+
label="Review progress"
|
|
675
|
+
/>
|
|
676
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
677
|
+
<ReviewFilterBar
|
|
678
|
+
{filters}
|
|
679
|
+
bind:active={filter}
|
|
680
|
+
onHelp={() => (helpOpen = true)}
|
|
681
|
+
label="Filter sections"
|
|
682
|
+
/>
|
|
683
|
+
{#if canDecide && remaining.length > 0}
|
|
684
|
+
<!-- Joe 2026-10-19: 52 lines clicked one by one. Low-risk first
|
|
685
|
+
(the safest bulk), then everything left. Both ask first. -->
|
|
686
|
+
<div class="ml-auto flex shrink-0 flex-wrap items-center gap-2">
|
|
687
|
+
{#if hasRisk && lowRisk.length > 0}
|
|
688
|
+
<button
|
|
689
|
+
type="button"
|
|
690
|
+
class="btn btn-ghost btn-xs rounded-field"
|
|
691
|
+
data-accept-low-risk
|
|
692
|
+
onclick={() => confirmAccept(lowRisk)}
|
|
693
|
+
>
|
|
694
|
+
Accept all low-risk
|
|
695
|
+
<span class="tabular-nums opacity-70">({lowRisk.length})</span>
|
|
696
|
+
</button>
|
|
697
|
+
{/if}
|
|
698
|
+
<button
|
|
699
|
+
type="button"
|
|
700
|
+
class="btn btn-outline btn-xs rounded-field"
|
|
701
|
+
aria-keyshortcuts="Shift+A"
|
|
702
|
+
data-accept-all
|
|
703
|
+
onclick={() => confirmAccept(remaining)}
|
|
704
|
+
>
|
|
705
|
+
Accept all remaining
|
|
706
|
+
<span class="tabular-nums opacity-70">({remaining.length})</span>
|
|
707
|
+
</button>
|
|
708
|
+
<!-- Joe 2026-10-19: "bulk accept OR reject, then flip the
|
|
709
|
+
outliers either way." Asks one shared note first. -->
|
|
710
|
+
<button
|
|
711
|
+
type="button"
|
|
712
|
+
class="btn btn-outline btn-error btn-xs rounded-field"
|
|
713
|
+
aria-keyshortcuts="Shift+R"
|
|
714
|
+
aria-expanded={!!rejectAsk}
|
|
715
|
+
data-reject-all
|
|
716
|
+
onclick={() => askReject(remaining)}
|
|
717
|
+
>
|
|
718
|
+
Reject all remaining
|
|
719
|
+
<span class="tabular-nums opacity-70">({remaining.length})</span>
|
|
720
|
+
</button>
|
|
721
|
+
</div>
|
|
722
|
+
{/if}
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
{#if rejectAsk && canDecide && rejectCount > 0}
|
|
727
|
+
<BulkRejectForm
|
|
728
|
+
count={rejectCount}
|
|
729
|
+
noun="line"
|
|
730
|
+
onConfirm={confirmReject}
|
|
731
|
+
onCancel={cancelReject}
|
|
732
|
+
/>
|
|
733
|
+
{/if}
|
|
734
|
+
|
|
735
|
+
{#if lastBulk && canDecide}
|
|
736
|
+
{@const rejected = lastBulk.undo.verdict === "reject"}
|
|
737
|
+
<BulkUndoBar
|
|
738
|
+
message="{rejected ? 'Rejected' : 'Accepted'} {lineCount(
|
|
739
|
+
lastBulk.count,
|
|
740
|
+
)}. Flip any line with a or r."
|
|
741
|
+
tone={rejected ? "error" : "success"}
|
|
742
|
+
onUndo={undoBulk}
|
|
743
|
+
onDismiss={() => (lastBulk = null)}
|
|
744
|
+
/>
|
|
745
|
+
{/if}
|
|
746
|
+
|
|
747
|
+
{#if saver.failed}
|
|
748
|
+
<div
|
|
749
|
+
class="mx-4 mt-3 flex items-center gap-2 rounded-field border border-error/40 bg-error/10 px-3 py-2 text-sm sm:mx-5"
|
|
750
|
+
role="alert"
|
|
751
|
+
data-save-error
|
|
752
|
+
>
|
|
753
|
+
<span>Your last change was not saved.</span>
|
|
754
|
+
<button
|
|
755
|
+
type="button"
|
|
756
|
+
class="btn btn-xs ml-auto rounded-field"
|
|
757
|
+
onclick={() => saver.retry()}>Retry</button
|
|
758
|
+
>
|
|
759
|
+
</div>
|
|
760
|
+
{/if}
|
|
761
|
+
|
|
762
|
+
<div class="px-4 pt-2 pb-5 sm:px-5">
|
|
763
|
+
{#if params.items.length === 0}
|
|
764
|
+
<p class="py-6 text-sm text-base-content/70">No changes to review.</p>
|
|
765
|
+
{/if}
|
|
766
|
+
{#each sections as section (section.id)}
|
|
767
|
+
{@const anyVisible = section.items.some((it) => visible(it.id))}
|
|
768
|
+
<section aria-label={section.title || "Sections"} hidden={!anyVisible}>
|
|
769
|
+
{#if section.title}
|
|
770
|
+
{@const sectionTargets = canDecide ? bulkTargets(section.items) : []}
|
|
771
|
+
<div class="mt-4 mb-2 flex flex-wrap items-center gap-2">
|
|
772
|
+
<h3 class="text-sm font-semibold text-base-content/80">
|
|
773
|
+
{section.title}
|
|
774
|
+
</h3>
|
|
775
|
+
{#if sectionTargets.length > 0}
|
|
776
|
+
<button
|
|
777
|
+
type="button"
|
|
778
|
+
class="btn btn-ghost btn-xs rounded-field text-base-content/70"
|
|
779
|
+
aria-label="Accept {lineCount(
|
|
780
|
+
sectionTargets.length,
|
|
781
|
+
)} left in {section.title}"
|
|
782
|
+
data-accept-section={section.id}
|
|
783
|
+
onclick={() => decideTargets(sectionTargets, "approve")}
|
|
784
|
+
>
|
|
785
|
+
Accept section
|
|
786
|
+
<span class="tabular-nums">({sectionTargets.length})</span>
|
|
787
|
+
</button>
|
|
788
|
+
<button
|
|
789
|
+
type="button"
|
|
790
|
+
class="btn btn-ghost btn-xs rounded-field text-error"
|
|
791
|
+
aria-label="Reject {lineCount(
|
|
792
|
+
sectionTargets.length,
|
|
793
|
+
)} left in {section.title}"
|
|
794
|
+
data-reject-section={section.id}
|
|
795
|
+
onclick={() => askReject(sectionTargets)}
|
|
796
|
+
>
|
|
797
|
+
Reject section
|
|
798
|
+
<span class="tabular-nums">({sectionTargets.length})</span>
|
|
799
|
+
</button>
|
|
800
|
+
{/if}
|
|
801
|
+
</div>
|
|
802
|
+
{/if}
|
|
803
|
+
<div class="flex flex-col gap-3">
|
|
804
|
+
{#each section.items as item (item.id)}
|
|
805
|
+
{@const d = currentOf(item.id)}
|
|
806
|
+
{@const v = d.verdict ?? null}
|
|
807
|
+
{@const stats = lineStats(item.before, item.after)}
|
|
808
|
+
{@const hasEdit = d.edited !== undefined}
|
|
809
|
+
{@const retest = isRetest(item)}
|
|
810
|
+
{@const history = historyRounds(
|
|
811
|
+
views[item.id]?.history ?? [],
|
|
812
|
+
diffRounds.roundLabel,
|
|
813
|
+
)}
|
|
814
|
+
<!-- The card is the j/k focus target (roving focus, as in
|
|
815
|
+
media-picker). -->
|
|
816
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
817
|
+
<article
|
|
818
|
+
data-item-id={item.id}
|
|
819
|
+
tabindex="0"
|
|
820
|
+
class="flex flex-col gap-2 rounded-box border bg-base-100 p-3 transition-opacity focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary motion-reduce:transition-none sm:p-4 {v ===
|
|
821
|
+
'approve'
|
|
822
|
+
? 'border-success ring-1 ring-success/40'
|
|
823
|
+
: v === 'modify'
|
|
824
|
+
? 'border-warning/60'
|
|
825
|
+
: v === 'reject'
|
|
826
|
+
? 'border-error/40 opacity-60 focus-within:opacity-100 hover:opacity-100'
|
|
827
|
+
: 'border-base-content/10'}"
|
|
828
|
+
style:order={ranks[item.id] ?? 0}
|
|
829
|
+
aria-label="{item.title}, {v
|
|
830
|
+
? VERDICT_WORD[v]
|
|
831
|
+
: retest
|
|
832
|
+
? 'to retest'
|
|
833
|
+
: 'undecided'}"
|
|
834
|
+
hidden={!visible(item.id)}
|
|
835
|
+
>
|
|
836
|
+
<div class="flex flex-wrap items-baseline gap-1.5">
|
|
837
|
+
<h4 class="text-sm leading-snug font-semibold">
|
|
838
|
+
{item.title}
|
|
839
|
+
</h4>
|
|
840
|
+
{#if item.risk}
|
|
841
|
+
<span class="badge badge-sm {RISK_CLASS[item.risk]}">
|
|
842
|
+
{item.risk} risk
|
|
843
|
+
</span>
|
|
844
|
+
{/if}
|
|
845
|
+
{#each item.tags ?? [] as t (t)}
|
|
846
|
+
<span class="badge badge-ghost badge-sm">{t}</span>
|
|
847
|
+
{/each}
|
|
848
|
+
{#if item.team || (retest && canDecide)}
|
|
849
|
+
<TeamChip team={item.team} retest={retest && canDecide} />
|
|
850
|
+
{/if}
|
|
851
|
+
{#if v}
|
|
852
|
+
<span
|
|
853
|
+
class="badge badge-sm font-medium {v === 'approve'
|
|
854
|
+
? 'badge-success'
|
|
855
|
+
: v === 'modify'
|
|
856
|
+
? 'badge-warning'
|
|
857
|
+
: 'badge-error'}"
|
|
858
|
+
>
|
|
859
|
+
{VERDICT_WORD[v]}
|
|
860
|
+
</span>
|
|
861
|
+
{/if}
|
|
862
|
+
</div>
|
|
863
|
+
{#if item.rationale}
|
|
864
|
+
<p
|
|
865
|
+
class="max-w-[80ch] text-xs leading-relaxed whitespace-pre-line text-base-content/75"
|
|
866
|
+
>
|
|
867
|
+
{item.rationale}
|
|
868
|
+
</p>
|
|
869
|
+
{/if}
|
|
870
|
+
|
|
871
|
+
<div
|
|
872
|
+
class="overflow-hidden rounded-box border border-base-content/10"
|
|
873
|
+
>
|
|
874
|
+
<div
|
|
875
|
+
class="flex items-center gap-2 border-b border-base-content/10 px-3 py-1.5 text-2xs text-base-content/70"
|
|
876
|
+
>
|
|
877
|
+
<span class="badge badge-error badge-sm font-mono"
|
|
878
|
+
>-{stats.removed}</span
|
|
879
|
+
>
|
|
880
|
+
<span class="badge badge-success badge-sm font-mono"
|
|
881
|
+
>+{stats.added}</span
|
|
882
|
+
>
|
|
883
|
+
<span class="ml-auto">
|
|
884
|
+
{item.before
|
|
885
|
+
? item.after
|
|
886
|
+
? "current → proposed"
|
|
887
|
+
: "proposed: delete this section"
|
|
888
|
+
: "proposed: new section"}
|
|
889
|
+
</span>
|
|
890
|
+
</div>
|
|
891
|
+
<HunkDiffView
|
|
892
|
+
oldText={norm(item.before)}
|
|
893
|
+
newText={norm(item.after)}
|
|
894
|
+
{unified}
|
|
895
|
+
/>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
{#if hasEdit}
|
|
899
|
+
<div
|
|
900
|
+
class="overflow-hidden rounded-box border border-warning/50"
|
|
901
|
+
>
|
|
902
|
+
<div
|
|
903
|
+
class="border-b border-base-content/10 px-3 py-1.5 text-2xs font-medium text-base-content/80"
|
|
904
|
+
>
|
|
905
|
+
Your rewrite (proposed → yours)
|
|
906
|
+
{#if v !== "modify"}
|
|
907
|
+
<span class="font-normal text-base-content/70"
|
|
908
|
+
>· draft, not applied unless Modify is chosen</span
|
|
909
|
+
>
|
|
910
|
+
{/if}
|
|
911
|
+
</div>
|
|
912
|
+
<HunkDiffView
|
|
913
|
+
oldText={norm(item.after)}
|
|
914
|
+
newText={norm(d.edited ?? "")}
|
|
915
|
+
{unified}
|
|
916
|
+
/>
|
|
917
|
+
</div>
|
|
918
|
+
{/if}
|
|
919
|
+
|
|
920
|
+
<!-- mos-guqgs: the verdict row renders on every surface, but
|
|
921
|
+
VISIBLY DISABLED where nothing can save — the old gate hid
|
|
922
|
+
the row on read-only mounts yet left it fully live on
|
|
923
|
+
saveless ones (readonly=false, saveState=undefined), the
|
|
924
|
+
exact interactive-but-unsaved bug. Same contract as
|
|
925
|
+
checklist / compare / callout: no saveState, dead controls. -->
|
|
926
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
927
|
+
<VerdictControl
|
|
928
|
+
options={VERDICTS}
|
|
929
|
+
value={v}
|
|
930
|
+
label="Verdict for {item.title}"
|
|
931
|
+
disabled={!canDecide}
|
|
932
|
+
onVerdict={(id) => setVerdict(item.id, id as DiffVerdict)}
|
|
933
|
+
onUndo={() => undoVerdict(item.id)}
|
|
934
|
+
/>
|
|
935
|
+
<NoteToggle
|
|
936
|
+
open={!!notesOpen[item.id]}
|
|
937
|
+
hasNote={!!d.note}
|
|
938
|
+
disabled={!canDecide}
|
|
939
|
+
controls="noteinput-{item.id}"
|
|
940
|
+
itemTitle={item.title}
|
|
941
|
+
onToggle={() => toggleNote(item.id)}
|
|
942
|
+
/>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
{#if canDecide}
|
|
946
|
+
{#if editorsOpen[item.id]}
|
|
947
|
+
<div class="flex flex-col gap-1.5">
|
|
948
|
+
<label
|
|
949
|
+
class="text-xs font-medium text-base-content/80"
|
|
950
|
+
for="edit-{item.id}"
|
|
951
|
+
>
|
|
952
|
+
Your version of “{item.title}”
|
|
953
|
+
</label>
|
|
954
|
+
<textarea
|
|
955
|
+
id="edit-{item.id}"
|
|
956
|
+
class="textarea textarea-sm min-h-32 w-full resize-y rounded-field border-base-content/15 bg-base-200 text-sm leading-relaxed"
|
|
957
|
+
rows={Math.min(
|
|
958
|
+
16,
|
|
959
|
+
Math.max(
|
|
960
|
+
4,
|
|
961
|
+
(d.edited ?? item.after).split("\n").length + 1,
|
|
962
|
+
),
|
|
963
|
+
)}
|
|
964
|
+
maxlength={EDITED_MAX_CHARS}
|
|
965
|
+
value={d.edited ?? item.after}
|
|
966
|
+
oninput={(e) => setEdited(item, e.currentTarget.value)}
|
|
967
|
+
onkeydown={(e) => {
|
|
968
|
+
if (e.key === "Escape") {
|
|
969
|
+
e.preventDefault()
|
|
970
|
+
closeEditor(item.id)
|
|
971
|
+
}
|
|
972
|
+
}}
|
|
973
|
+
></textarea>
|
|
974
|
+
<div class="flex items-center gap-2">
|
|
975
|
+
<button
|
|
976
|
+
type="button"
|
|
977
|
+
class="btn btn-ghost btn-sm rounded-field"
|
|
978
|
+
disabled={!hasEdit}
|
|
979
|
+
onclick={() => resetEdit(item)}
|
|
980
|
+
>
|
|
981
|
+
Reset to proposed
|
|
982
|
+
</button>
|
|
983
|
+
<button
|
|
984
|
+
type="button"
|
|
985
|
+
class="btn btn-sm ml-auto rounded-field"
|
|
986
|
+
onclick={() => closeEditor(item.id)}
|
|
987
|
+
>
|
|
988
|
+
Done
|
|
989
|
+
</button>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
{:else if v === "modify"}
|
|
993
|
+
<button
|
|
994
|
+
type="button"
|
|
995
|
+
class="btn btn-ghost btn-sm self-start rounded-field px-2"
|
|
996
|
+
onclick={() => openEditor(item.id)}
|
|
997
|
+
>
|
|
998
|
+
{hasEdit ? "Edit your rewrite" : "Edit the proposed text"}
|
|
999
|
+
</button>
|
|
1000
|
+
{/if}
|
|
1001
|
+
|
|
1002
|
+
<!-- The ONE composer in its per-item mode (mos-rccb5): the
|
|
1003
|
+
ReviewNote text leg is unchanged; the attach lane rides
|
|
1004
|
+
under it. -->
|
|
1005
|
+
<ItemNote
|
|
1006
|
+
artifactId={ctx.artifactId}
|
|
1007
|
+
note={{
|
|
1008
|
+
id: `noteinput-${item.id}`,
|
|
1009
|
+
value: d.note ?? "",
|
|
1010
|
+
verdict: noteVerdictOf(item.id),
|
|
1011
|
+
options: VERDICTS,
|
|
1012
|
+
round: views[item.id]?.round ?? null,
|
|
1013
|
+
label: `Note for ${item.title}`,
|
|
1014
|
+
placeholder: "Why, or what should change?",
|
|
1015
|
+
onInput: (t) => onNoteInput(item.id, t),
|
|
1016
|
+
onSave: noteSave,
|
|
1017
|
+
}}
|
|
1018
|
+
bind:open={() => !!notesOpen[item.id],
|
|
1019
|
+
(o) => (notesOpen = { ...notesOpen, [item.id]: o })}
|
|
1020
|
+
clips={currentOf(item.id).attachments ?? []}
|
|
1021
|
+
onClipsChange={(clips) => onItemClips(item.id, clips)}
|
|
1022
|
+
disabled={!canDecide}
|
|
1023
|
+
/>
|
|
1024
|
+
{/if}
|
|
1025
|
+
<RoundHistory
|
|
1026
|
+
rounds={history}
|
|
1027
|
+
verdicts={VERDICTS}
|
|
1028
|
+
toggleLabel={(n) =>
|
|
1029
|
+
n === 1 ? "1 earlier round" : `${n} earlier rounds`}
|
|
1030
|
+
/>
|
|
1031
|
+
</article>
|
|
1032
|
+
{/each}
|
|
1033
|
+
</div>
|
|
1034
|
+
</section>
|
|
1035
|
+
{/each}
|
|
1036
|
+
|
|
1037
|
+
{#if params.items.length > 0 && !params.items.some((it) => visible(it.id))}
|
|
1038
|
+
<p class="py-6 text-sm text-base-content/70">
|
|
1039
|
+
Nothing in this filter.
|
|
1040
|
+
<button
|
|
1041
|
+
type="button"
|
|
1042
|
+
class="link link-hover ml-1"
|
|
1043
|
+
onclick={() => (filter = "all")}>Show all</button
|
|
1044
|
+
>
|
|
1045
|
+
</p>
|
|
1046
|
+
{/if}
|
|
1047
|
+
</div>
|
|
1048
|
+
</section>
|
|
1049
|
+
|
|
1050
|
+
<ReviewShortcuts
|
|
1051
|
+
bind:open={helpOpen}
|
|
1052
|
+
config={keysConfig}
|
|
1053
|
+
extraRows={bulkKeyRows}
|
|
1054
|
+
footnote="Keys act on the focused section. Accept moves on to the next undecided one. Shift+A accepts and Shift+R rejects every undecided line in the current filter; lines you already decided stay. Then flip the outliers: j / k to move, a, r or m to change one line (x also means Modify). Decided sections sort to the end when you change the filter or reload. u (or Undo) clears the verdict; the note and your rewrite stay."
|
|
1055
|
+
/>
|