@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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locked round lines (./rounds.ts `RoundView.history`, newest first) →
|
|
3
|
+
* jatui RoundHistory `rounds` (media-picker, diff-review).
|
|
4
|
+
*
|
|
5
|
+
* RoundHistory keys its list on `round`, so keys must be unique non-empty
|
|
6
|
+
* strings: a legacy line (round null) becomes "legacy", and a repeated round
|
|
7
|
+
* (two lines for one round — e.g. a viewer line and an older copy) gets a
|
|
8
|
+
* `#n` suffix on the LATER line, so the newest keeps the plain key a version
|
|
9
|
+
* switcher pairs with. The list is handed over oldest first, which is how
|
|
10
|
+
* RoundHistory reads input order when not every line carries a date.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReviewRound } from "../../components/review/reviewSummary.js";
|
|
13
|
+
import type { RoundLine } from "./rounds.js";
|
|
14
|
+
export declare function historyRounds(lines: readonly RoundLine[], label: (round: string | null) => string): ReviewRound[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const LEGACY_ROUND_KEY = "legacy";
|
|
2
|
+
export function historyRounds(lines, label) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const l of lines) {
|
|
6
|
+
const base = l.round ?? LEGACY_ROUND_KEY;
|
|
7
|
+
let key = base;
|
|
8
|
+
for (let n = 2; seen.has(key); n++)
|
|
9
|
+
key = `${base}#${n}`;
|
|
10
|
+
seen.add(key);
|
|
11
|
+
out.push({
|
|
12
|
+
round: key,
|
|
13
|
+
verdict: l.verdict ?? null,
|
|
14
|
+
note: l.note ?? null,
|
|
15
|
+
at: l.at ?? null,
|
|
16
|
+
label: label(l.round),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return out.reverse();
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review rounds: the shared, block-agnostic round model (mos-lsmve.5, M1).
|
|
3
|
+
* Design: docs/review-ux/proposal.md → "The round model".
|
|
4
|
+
*
|
|
5
|
+
* A ROUND is one thing put in front of the reviewer: a version of the art
|
|
6
|
+
* (media-picker `currentVersion`) or one "ready to retest" of a checklist row
|
|
7
|
+
* (its `retestSince`). Each round has its own verdict and its own note.
|
|
8
|
+
*
|
|
9
|
+
* Saved shape, backward compatible — the flat fields stay the CURRENT round,
|
|
10
|
+
* so every existing reader keeps working:
|
|
11
|
+
*
|
|
12
|
+
* { verdict|status, note, at, …block fields,
|
|
13
|
+
* round?: "2026-10-11T15:00:00Z" | "v8", // the round this answers
|
|
14
|
+
* history?: [{ round, verdict, note, at }] } // locked, newest first
|
|
15
|
+
*
|
|
16
|
+
* An entry with no `round` is "round 1 (legacy)". Joe D1 = A: when the round
|
|
17
|
+
* under review differs from the entry's round, the FIRST save in the new
|
|
18
|
+
* round moves the old {verdict, note, at} into history and the new round
|
|
19
|
+
* starts blank. That happens here, on the client, on the owner's own save —
|
|
20
|
+
* never as a data migration, so stored state is untouched until its owner
|
|
21
|
+
* acts. Until then `viewEntry()` shows the same split without writing it.
|
|
22
|
+
*
|
|
23
|
+
* Pure: no DOM, no Svelte. Every stored value is untrusted JSON.
|
|
24
|
+
*/
|
|
25
|
+
/** One locked line of an item's history. `verdict` holds the block's own
|
|
26
|
+
* vocabulary (checklist status or media/diff verdict). */
|
|
27
|
+
export type RoundLine = {
|
|
28
|
+
/** The round this line answered; null = round 1 (legacy). */
|
|
29
|
+
round: string | null;
|
|
30
|
+
verdict?: string;
|
|
31
|
+
note?: string;
|
|
32
|
+
at?: string;
|
|
33
|
+
/** Where the line came from: the viewer's own saves, or the publisher's
|
|
34
|
+
* spec (media-picker `history[]` param). */
|
|
35
|
+
source?: "viewer" | "publisher";
|
|
36
|
+
};
|
|
37
|
+
/** How one block names its round-scoped fields. */
|
|
38
|
+
export type RoundsConfig = {
|
|
39
|
+
/** The saved field holding the verdict: "verdict" (media, diff) or
|
|
40
|
+
* "status" (checklist). */
|
|
41
|
+
verdictKey: string;
|
|
42
|
+
/** The block's verdict vocabulary; anything else is dropped as junk. */
|
|
43
|
+
verdicts: readonly string[];
|
|
44
|
+
/** A verdict that means "no verdict" (checklist "untested"): it is never
|
|
45
|
+
* written into history on its own. */
|
|
46
|
+
emptyVerdict?: string;
|
|
47
|
+
/** Further fields that belong to a round and start blank in a new one
|
|
48
|
+
* (checklist `statusAt`, diff `edited`). note/at always do. */
|
|
49
|
+
roundFields?: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
/** The display label of a round id. */
|
|
52
|
+
declare function roundLabel(round: string | null | undefined): string;
|
|
53
|
+
/**
|
|
54
|
+
* Do two round ids name the same round? Identity, not ordering — so client
|
|
55
|
+
* clock skew can never move a verdict. Two ISO strings for the same instant
|
|
56
|
+
* ("…T15:00Z" vs "…T15:00:00.000Z") are the same round.
|
|
57
|
+
*/
|
|
58
|
+
declare function sameRound(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
59
|
+
/** The round id an entry answers; null = round 1 (legacy) or junk. */
|
|
60
|
+
declare function roundOf(entry: unknown): string | null;
|
|
61
|
+
/** What a block renders for one item: the current round and the locked
|
|
62
|
+
* history, newest first. Nothing is written. */
|
|
63
|
+
export type RoundView = {
|
|
64
|
+
/** The round under review (null = the item has no rounds). */
|
|
65
|
+
round: string | null;
|
|
66
|
+
/** The current round's fields — blank when the stored entry is stale.
|
|
67
|
+
* Non-round block fields (media `variant`) are kept either way. */
|
|
68
|
+
current: Record<string, unknown>;
|
|
69
|
+
history: RoundLine[];
|
|
70
|
+
/** True when the stored entry belongs to an earlier round; the first save
|
|
71
|
+
* will move it into history. */
|
|
72
|
+
stale: boolean;
|
|
73
|
+
};
|
|
74
|
+
/** Bind a block's config once: `const rounds = useRounds({ verdictKey: "status", … })`. */
|
|
75
|
+
export declare function useRounds(cfg: RoundsConfig, defaults?: {
|
|
76
|
+
stampKeys?: readonly string[];
|
|
77
|
+
}): {
|
|
78
|
+
roundOf: typeof roundOf;
|
|
79
|
+
roundLabel: typeof roundLabel;
|
|
80
|
+
sameRound: typeof sameRound;
|
|
81
|
+
isStale: (entry: unknown, current: string | null | undefined) => boolean;
|
|
82
|
+
parseHistory: (raw: unknown) => RoundLine[];
|
|
83
|
+
view: (raw: unknown, current: string | null | undefined, publisherHistory?: unknown) => RoundView;
|
|
84
|
+
save: (stored: unknown, patch: Record<string, unknown>, current: string | null | undefined) => Record<string, unknown>;
|
|
85
|
+
};
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review rounds: the shared, block-agnostic round model (mos-lsmve.5, M1).
|
|
3
|
+
* Design: docs/review-ux/proposal.md → "The round model".
|
|
4
|
+
*
|
|
5
|
+
* A ROUND is one thing put in front of the reviewer: a version of the art
|
|
6
|
+
* (media-picker `currentVersion`) or one "ready to retest" of a checklist row
|
|
7
|
+
* (its `retestSince`). Each round has its own verdict and its own note.
|
|
8
|
+
*
|
|
9
|
+
* Saved shape, backward compatible — the flat fields stay the CURRENT round,
|
|
10
|
+
* so every existing reader keeps working:
|
|
11
|
+
*
|
|
12
|
+
* { verdict|status, note, at, …block fields,
|
|
13
|
+
* round?: "2026-10-11T15:00:00Z" | "v8", // the round this answers
|
|
14
|
+
* history?: [{ round, verdict, note, at }] } // locked, newest first
|
|
15
|
+
*
|
|
16
|
+
* An entry with no `round` is "round 1 (legacy)". Joe D1 = A: when the round
|
|
17
|
+
* under review differs from the entry's round, the FIRST save in the new
|
|
18
|
+
* round moves the old {verdict, note, at} into history and the new round
|
|
19
|
+
* starts blank. That happens here, on the client, on the owner's own save —
|
|
20
|
+
* never as a data migration, so stored state is untouched until its owner
|
|
21
|
+
* acts. Until then `viewEntry()` shows the same split without writing it.
|
|
22
|
+
*
|
|
23
|
+
* Pure: no DOM, no Svelte. Every stored value is untrusted JSON.
|
|
24
|
+
*/
|
|
25
|
+
/** Cap on stored history lines per item — matches media-picker's param cap. */
|
|
26
|
+
const HISTORY_MAX = 12;
|
|
27
|
+
/** Cap on a stored round id / note in a history line. */
|
|
28
|
+
const ROUND_MAX_CHARS = 200;
|
|
29
|
+
const NOTE_MAX_CHARS = 10_000;
|
|
30
|
+
/** The display label of a round id. */
|
|
31
|
+
function roundLabel(round) {
|
|
32
|
+
if (round == null || round === "")
|
|
33
|
+
return "Round 1 (legacy)";
|
|
34
|
+
const t = isoMs(round);
|
|
35
|
+
if (t != null) {
|
|
36
|
+
const d = new Date(t);
|
|
37
|
+
return `Retest of ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
|
|
38
|
+
}
|
|
39
|
+
return round;
|
|
40
|
+
}
|
|
41
|
+
function isoMs(s) {
|
|
42
|
+
// Only ISO-shaped dates count as times; "v8" must never parse as a date.
|
|
43
|
+
if (!/^\d{4}-\d{2}-\d{2}(T|$)/.test(s))
|
|
44
|
+
return null;
|
|
45
|
+
const t = Date.parse(s);
|
|
46
|
+
return Number.isNaN(t) ? null : t;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Do two round ids name the same round? Identity, not ordering — so client
|
|
50
|
+
* clock skew can never move a verdict. Two ISO strings for the same instant
|
|
51
|
+
* ("…T15:00Z" vs "…T15:00:00.000Z") are the same round.
|
|
52
|
+
*/
|
|
53
|
+
function sameRound(a, b) {
|
|
54
|
+
const x = a || null;
|
|
55
|
+
const y = b || null;
|
|
56
|
+
if (x === y)
|
|
57
|
+
return true;
|
|
58
|
+
if (x == null || y == null)
|
|
59
|
+
return false;
|
|
60
|
+
const tx = isoMs(x);
|
|
61
|
+
const ty = isoMs(y);
|
|
62
|
+
return tx != null && tx === ty;
|
|
63
|
+
}
|
|
64
|
+
function str(v, max) {
|
|
65
|
+
return typeof v === "string" && v !== "" ? v.slice(0, max) : undefined;
|
|
66
|
+
}
|
|
67
|
+
function isRecord(v) {
|
|
68
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
69
|
+
}
|
|
70
|
+
/** The round id an entry answers; null = round 1 (legacy) or junk. */
|
|
71
|
+
function roundOf(entry) {
|
|
72
|
+
if (!isRecord(entry))
|
|
73
|
+
return null;
|
|
74
|
+
return str(entry.round, ROUND_MAX_CHARS) ?? null;
|
|
75
|
+
}
|
|
76
|
+
/** Defensively parse one history line; null when it carries nothing. */
|
|
77
|
+
function parseRoundLine(raw, cfg) {
|
|
78
|
+
if (!isRecord(raw))
|
|
79
|
+
return null;
|
|
80
|
+
const verdictRaw = raw.verdict ?? raw[cfg.verdictKey] ?? raw.status;
|
|
81
|
+
const verdict = typeof verdictRaw === "string" &&
|
|
82
|
+
cfg.verdicts.includes(verdictRaw) &&
|
|
83
|
+
verdictRaw !== cfg.emptyVerdict
|
|
84
|
+
? verdictRaw
|
|
85
|
+
: undefined;
|
|
86
|
+
const note = str(raw.note, NOTE_MAX_CHARS);
|
|
87
|
+
if (!verdict && !note)
|
|
88
|
+
return null;
|
|
89
|
+
const line = {
|
|
90
|
+
// media-picker's publisher param names it `version`
|
|
91
|
+
round: str(raw.round, ROUND_MAX_CHARS) ??
|
|
92
|
+
str(raw.version, ROUND_MAX_CHARS) ??
|
|
93
|
+
null,
|
|
94
|
+
};
|
|
95
|
+
if (verdict)
|
|
96
|
+
line.verdict = verdict;
|
|
97
|
+
if (note)
|
|
98
|
+
line.note = note;
|
|
99
|
+
const at = str(raw.at, 64);
|
|
100
|
+
if (at)
|
|
101
|
+
line.at = at;
|
|
102
|
+
if (raw.source === "viewer" || raw.source === "publisher")
|
|
103
|
+
line.source = raw.source;
|
|
104
|
+
return line;
|
|
105
|
+
}
|
|
106
|
+
function lineKey(l) {
|
|
107
|
+
const r = l.round == null ? "" : (isoMs(l.round) ?? l.round);
|
|
108
|
+
return `${r}\u0000${l.verdict ?? ""}\u0000${l.note ?? ""}\u0000${l.at ?? ""}`;
|
|
109
|
+
}
|
|
110
|
+
/** Defensively parse a stored history array: junk dropped, exact repeats
|
|
111
|
+
* (a double-open re-move) collapsed, capped at HISTORY_MAX. */
|
|
112
|
+
function parseHistory(raw, cfg) {
|
|
113
|
+
if (!Array.isArray(raw))
|
|
114
|
+
return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const item of raw.slice(0, HISTORY_MAX * 4)) {
|
|
118
|
+
const line = parseRoundLine(item, cfg);
|
|
119
|
+
if (!line)
|
|
120
|
+
continue;
|
|
121
|
+
const k = lineKey(line);
|
|
122
|
+
if (seen.has(k))
|
|
123
|
+
continue;
|
|
124
|
+
seen.add(k);
|
|
125
|
+
out.push(line);
|
|
126
|
+
if (out.length >= HISTORY_MAX)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** The current round's {verdict, note, at} of an entry, as a history line. */
|
|
132
|
+
function currentAsLine(entry, cfg) {
|
|
133
|
+
return parseRoundLine({
|
|
134
|
+
verdict: entry[cfg.verdictKey],
|
|
135
|
+
note: entry.note,
|
|
136
|
+
at: entry.at,
|
|
137
|
+
round: entry.round,
|
|
138
|
+
}, cfg);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Is the entry's current verdict/note from a round OTHER than `current`?
|
|
142
|
+
*
|
|
143
|
+
* - `current` null (the item has no rounds): never stale.
|
|
144
|
+
* - Entry with a `round`: identity comparison (`sameRound`).
|
|
145
|
+
* - Legacy entry (no `round`) against an ISO `current` (a checklist
|
|
146
|
+
* retestSince): a save stamped at or after the round opened already
|
|
147
|
+
* answered it (the same reading `rowAwaitsViewer` makes); an unstamped or
|
|
148
|
+
* earlier save is from before. `stampKeys` names the timestamps to trust,
|
|
149
|
+
* most specific first (checklist: ["statusAt", "at"]).
|
|
150
|
+
* - Legacy entry against a version id ("v8"): NOT stale — it is adopted as
|
|
151
|
+
* the current round. Every media-picker save before this module is legacy
|
|
152
|
+
* and was made against the version then under review; blanking them all on
|
|
153
|
+
* deploy would be a silent data loss. The next save stamps the round.
|
|
154
|
+
* - An entry that carries no verdict and no note is never stale (there is
|
|
155
|
+
* nothing to lock away).
|
|
156
|
+
*/
|
|
157
|
+
function isStale(entry, current, cfg, stampKeys = ["at"]) {
|
|
158
|
+
if (!current)
|
|
159
|
+
return false;
|
|
160
|
+
if (!isRecord(entry))
|
|
161
|
+
return false;
|
|
162
|
+
if (!currentAsLine(entry, cfg))
|
|
163
|
+
return false;
|
|
164
|
+
const r = roundOf(entry);
|
|
165
|
+
if (r != null)
|
|
166
|
+
return !sameRound(r, current);
|
|
167
|
+
const since = isoMs(current);
|
|
168
|
+
if (since == null)
|
|
169
|
+
return false;
|
|
170
|
+
for (const k of stampKeys) {
|
|
171
|
+
const s = entry[k];
|
|
172
|
+
if (typeof s !== "string")
|
|
173
|
+
continue;
|
|
174
|
+
const t = Date.parse(s);
|
|
175
|
+
if (!Number.isNaN(t))
|
|
176
|
+
return t < since;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
function blankRound(entry, cfg) {
|
|
181
|
+
const out = {};
|
|
182
|
+
const drop = new Set([
|
|
183
|
+
cfg.verdictKey,
|
|
184
|
+
"note",
|
|
185
|
+
"at",
|
|
186
|
+
"round",
|
|
187
|
+
"history",
|
|
188
|
+
...(cfg.roundFields ?? []),
|
|
189
|
+
]);
|
|
190
|
+
for (const [k, v] of Object.entries(entry))
|
|
191
|
+
if (!drop.has(k))
|
|
192
|
+
out[k] = v;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
/** Merge viewer history with publisher-side lines (media-picker `history[]`
|
|
196
|
+
* param). The viewer's own line for a round wins; a publisher line fills a
|
|
197
|
+
* round the viewer has no record of. Viewer lines first (newest first as
|
|
198
|
+
* stored), then the publisher's in the publisher's order. */
|
|
199
|
+
function mergeHistory(viewer, publisher) {
|
|
200
|
+
const out = viewer.map((l) => ({
|
|
201
|
+
...l,
|
|
202
|
+
source: l.source ?? "viewer",
|
|
203
|
+
}));
|
|
204
|
+
for (const p of publisher) {
|
|
205
|
+
if (out.some((l) => sameRound(l.round, p.round)))
|
|
206
|
+
continue;
|
|
207
|
+
out.push({ ...p, source: "publisher" });
|
|
208
|
+
}
|
|
209
|
+
return out.slice(0, HISTORY_MAX * 2);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Read one stored entry for display under round `current`.
|
|
213
|
+
* `publisherHistory` is the spec's own history (untrusted JSON too).
|
|
214
|
+
*/
|
|
215
|
+
function viewEntry(raw, current, cfg, opts = {}) {
|
|
216
|
+
const round = current || null;
|
|
217
|
+
const entry = isRecord(raw) ? raw : {};
|
|
218
|
+
let history = parseHistory(entry.history, cfg);
|
|
219
|
+
const stale = isStale(entry, round, cfg, opts.stampKeys);
|
|
220
|
+
let currentFields;
|
|
221
|
+
if (stale) {
|
|
222
|
+
const moved = currentAsLine(entry, cfg);
|
|
223
|
+
if (moved)
|
|
224
|
+
history = prependLine(history, moved);
|
|
225
|
+
currentFields = blankRound(entry, cfg);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
currentFields = { ...entry };
|
|
229
|
+
delete currentFields.history;
|
|
230
|
+
}
|
|
231
|
+
const publisher = parseHistory(opts.publisherHistory, cfg).filter(
|
|
232
|
+
// the publisher's line for the round under review is not history
|
|
233
|
+
(l) => !(round != null && sameRound(l.round, round)));
|
|
234
|
+
return {
|
|
235
|
+
round,
|
|
236
|
+
current: currentFields,
|
|
237
|
+
history: mergeHistory(history, publisher),
|
|
238
|
+
stale,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function prependLine(history, line) {
|
|
242
|
+
const k = lineKey(line);
|
|
243
|
+
const rest = history.filter((l) => lineKey(l) !== k);
|
|
244
|
+
return [line, ...rest].slice(0, HISTORY_MAX);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Apply one save by the viewer to the stored entry, under round `current`.
|
|
248
|
+
*
|
|
249
|
+
* `patch` is the block's change (e.g. `{ status: "fail", at }`); a key set to
|
|
250
|
+
* `undefined` deletes that field (Undo). When the stored entry is stale, its
|
|
251
|
+
* {verdict, note, at} move into `history` first and the round's fields start
|
|
252
|
+
* blank — the patch then lands on the blank round. The result is stamped with
|
|
253
|
+
* `round` (omitted when the item has no rounds, so a round-less block's saved
|
|
254
|
+
* shape is exactly what it was before this module).
|
|
255
|
+
*
|
|
256
|
+
* Idempotent across a double-open: applying the same patch twice, or a stale
|
|
257
|
+
* tab re-applying after another tab already moved the round, never duplicates
|
|
258
|
+
* a history line.
|
|
259
|
+
*/
|
|
260
|
+
function applySave(stored, patch, current, cfg, opts = {}) {
|
|
261
|
+
const round = current || null;
|
|
262
|
+
const entry = isRecord(stored) ? stored : {};
|
|
263
|
+
let history = parseHistory(entry.history, cfg);
|
|
264
|
+
let next;
|
|
265
|
+
if (isStale(entry, round, cfg, opts.stampKeys)) {
|
|
266
|
+
const moved = currentAsLine(entry, cfg);
|
|
267
|
+
if (moved)
|
|
268
|
+
history = prependLine(history, moved);
|
|
269
|
+
next = blankRound(entry, cfg);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
next = { ...entry };
|
|
273
|
+
delete next.history;
|
|
274
|
+
}
|
|
275
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
276
|
+
if (k === "history" || k === "round")
|
|
277
|
+
continue;
|
|
278
|
+
if (v === undefined)
|
|
279
|
+
delete next[k];
|
|
280
|
+
else
|
|
281
|
+
next[k] = v;
|
|
282
|
+
}
|
|
283
|
+
if (round != null)
|
|
284
|
+
next.round = round;
|
|
285
|
+
else
|
|
286
|
+
delete next.round;
|
|
287
|
+
if (history.length > 0)
|
|
288
|
+
next.history = history;
|
|
289
|
+
return next;
|
|
290
|
+
}
|
|
291
|
+
/** Bind a block's config once: `const rounds = useRounds({ verdictKey: "status", … })`. */
|
|
292
|
+
export function useRounds(cfg, defaults = {}) {
|
|
293
|
+
return {
|
|
294
|
+
roundOf,
|
|
295
|
+
roundLabel,
|
|
296
|
+
sameRound,
|
|
297
|
+
isStale: (entry, current) => isStale(entry, current, cfg, defaults.stampKeys),
|
|
298
|
+
parseHistory: (raw) => parseHistory(raw, cfg),
|
|
299
|
+
view: (raw, current, publisherHistory) => viewEntry(raw, current, cfg, { ...defaults, publisherHistory }),
|
|
300
|
+
save: (stored, patch, current) => applySave(stored, patch, current, cfg, defaults),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-state saver for the review blocks (media-picker, diff-review).
|
|
3
|
+
*
|
|
4
|
+
* The blocks used to fire-and-forget `ctx.saveState(...).catch(() => {})`,
|
|
5
|
+
* so a failed save showed only in the page header (proposal critique #9).
|
|
6
|
+
* This keeps the block's own "did my last save land?" state: only the LATEST
|
|
7
|
+
* save's outcome counts (an older failure resolving after a newer success
|
|
8
|
+
* must not raise a stale alarm), and `retry()` re-sends the last state.
|
|
9
|
+
*
|
|
10
|
+
* `save()` rejects on failure so a ReviewNote `onSave` can show its own
|
|
11
|
+
* "Not saved / Retry"; verdict saves swallow the rejection and let the
|
|
12
|
+
* block render `failed`.
|
|
13
|
+
*/
|
|
14
|
+
import type { BlockState } from "../types.js";
|
|
15
|
+
type SaveFn = ((next: BlockState) => Promise<void>) | undefined;
|
|
16
|
+
export declare function createStateSaver(getSave: () => SaveFn): {
|
|
17
|
+
/** True while the most recent save failed. */
|
|
18
|
+
readonly failed: boolean;
|
|
19
|
+
save: (next: BlockState) => Promise<void>;
|
|
20
|
+
/** Re-send the last state (the alert's Retry). */
|
|
21
|
+
retry(): void;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* saver.svelte.js generated by Svelte v5.53.11 */
|
|
2
|
+
import * as $ from 'svelte/internal/client';
|
|
3
|
+
|
|
4
|
+
export function createStateSaver(getSave) {
|
|
5
|
+
let failed = $.state(false);
|
|
6
|
+
let last = null;
|
|
7
|
+
let seq = 0;
|
|
8
|
+
|
|
9
|
+
function save(next) {
|
|
10
|
+
const fn = getSave();
|
|
11
|
+
|
|
12
|
+
if (!fn) return Promise.resolve();
|
|
13
|
+
|
|
14
|
+
last = next;
|
|
15
|
+
|
|
16
|
+
const mine = ++seq;
|
|
17
|
+
let pending;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
pending = Promise.resolve(fn(next));
|
|
21
|
+
} catch(err) {
|
|
22
|
+
pending = Promise.reject(err);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return pending.then(
|
|
26
|
+
() => {
|
|
27
|
+
if (mine === seq) $.set(failed, false);
|
|
28
|
+
},
|
|
29
|
+
(err) => {
|
|
30
|
+
if (mine === seq) $.set(failed, true);
|
|
31
|
+
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
/** True while the most recent save failed. */
|
|
39
|
+
get failed() {
|
|
40
|
+
return $.get(failed);
|
|
41
|
+
},
|
|
42
|
+
save,
|
|
43
|
+
/** Re-send the last state (the alert's Retry). */
|
|
44
|
+
retry() {
|
|
45
|
+
if (last) save(last).catch(() => {});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item TEAM status for the Gallery (media-picker) and Redline (diff-review)
|
|
3
|
+
* blocks — decision D7 = A in docs/review-ux/proposal.md: "any item can be
|
|
4
|
+
* In progress / Ready, and Ready opens a round".
|
|
5
|
+
*
|
|
6
|
+
* It is a PUBLISHER param (`items[].team`), never viewer state: the team
|
|
7
|
+
* updates it by republishing, exactly like the checklist's Team column.
|
|
8
|
+
*
|
|
9
|
+
* team: { status: "in_progress" } working on it
|
|
10
|
+
* team: { status: "ready", readySince: "2026-10-11T15:00:00Z" }
|
|
11
|
+
* fixed — look again
|
|
12
|
+
*
|
|
13
|
+
* `ready` REQUIRES `readySince`: the timestamp is the round's identity, so a
|
|
14
|
+
* second "ready" (a new readySince) opens a new round and a republish that
|
|
15
|
+
* keeps the same readySince does not. Rounds compare by identity, never by
|
|
16
|
+
* clock order (see ./rounds.ts).
|
|
17
|
+
*/
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
declare const ITEM_TEAM_STATUSES: readonly ["in_progress", "ready"];
|
|
20
|
+
export type ItemTeamStatus = (typeof ITEM_TEAM_STATUSES)[number];
|
|
21
|
+
export type ItemTeam = {
|
|
22
|
+
status: ItemTeamStatus;
|
|
23
|
+
/** When the team marked the item ready (ISO). Required for `ready`. */
|
|
24
|
+
readySince?: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
export declare const itemTeamSchema: z.ZodType<ItemTeam>;
|
|
27
|
+
/** The Team chip's words — the same ones the checklist Team column uses. */
|
|
28
|
+
export declare const ITEM_TEAM_LABEL: Record<ItemTeamStatus, string>;
|
|
29
|
+
/** The round a `ready` opens (its readySince), else null. */
|
|
30
|
+
export declare function teamRound(team: ItemTeam | undefined | null): string | null;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item TEAM status for the Gallery (media-picker) and Redline (diff-review)
|
|
3
|
+
* blocks — decision D7 = A in docs/review-ux/proposal.md: "any item can be
|
|
4
|
+
* In progress / Ready, and Ready opens a round".
|
|
5
|
+
*
|
|
6
|
+
* It is a PUBLISHER param (`items[].team`), never viewer state: the team
|
|
7
|
+
* updates it by republishing, exactly like the checklist's Team column.
|
|
8
|
+
*
|
|
9
|
+
* team: { status: "in_progress" } working on it
|
|
10
|
+
* team: { status: "ready", readySince: "2026-10-11T15:00:00Z" }
|
|
11
|
+
* fixed — look again
|
|
12
|
+
*
|
|
13
|
+
* `ready` REQUIRES `readySince`: the timestamp is the round's identity, so a
|
|
14
|
+
* second "ready" (a new readySince) opens a new round and a republish that
|
|
15
|
+
* keeps the same readySince does not. Rounds compare by identity, never by
|
|
16
|
+
* clock order (see ./rounds.ts).
|
|
17
|
+
*/
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
const ITEM_TEAM_STATUSES = ["in_progress", "ready"];
|
|
20
|
+
export const itemTeamSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
status: z.enum(ITEM_TEAM_STATUSES),
|
|
23
|
+
readySince: z.iso.datetime({ offset: true }).optional(),
|
|
24
|
+
})
|
|
25
|
+
.check((ctx) => {
|
|
26
|
+
if (ctx.value.status === "ready" && !ctx.value.readySince)
|
|
27
|
+
ctx.issues.push({
|
|
28
|
+
code: "custom",
|
|
29
|
+
input: ctx.value,
|
|
30
|
+
path: ["readySince"],
|
|
31
|
+
message: 'team status "ready" needs readySince (ISO timestamp) — it opens the new review round',
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
/** The Team chip's words — the same ones the checklist Team column uses. */
|
|
35
|
+
export const ITEM_TEAM_LABEL = {
|
|
36
|
+
ready: "Ready to retest",
|
|
37
|
+
in_progress: "In progress",
|
|
38
|
+
};
|
|
39
|
+
/** The round a `ready` opens (its readySince), else null. */
|
|
40
|
+
export function teamRound(team) {
|
|
41
|
+
return team?.status === "ready" && team.readySince ? team.readySince : null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The items jatui `reviewKeys()` walks, in VISUAL order (media-picker,
|
|
3
|
+
* diff-review).
|
|
4
|
+
*
|
|
5
|
+
* Decided cards sink with CSS `order`, so DOM order is not what the reviewer
|
|
6
|
+
* sees: j must step to the card that is visually next, not jump over one that
|
|
7
|
+
* moved to the end. Hidden cards (filtered out) are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export declare function visualItems(root: HTMLElement | null): HTMLElement[];
|
|
10
|
+
/** True when a keydown target is a text field (keys are text there). */
|
|
11
|
+
export declare function isTextTarget(t: EventTarget | null): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The sort is a SNAPSHOT (mos-lsmve.12). If `order` tracked live verdicts, a
|
|
14
|
+
* card would jump to the end the moment it was decided and j from it would
|
|
15
|
+
* walk the NEW order onto an already-decided card (UAT: an Accept on §4.2
|
|
16
|
+
* was overwritten by a Reject + note meant for §7.4). Blocks take ranks once
|
|
17
|
+
* at load and again only on a filter change; while the reviewer works,
|
|
18
|
+
* every card stays where it was.
|
|
19
|
+
*
|
|
20
|
+
* CSS `order` per item id from a verdict snapshot. Undecided (or a verdict
|
|
21
|
+
* with no rank) is 0.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sortRanks(ids: readonly string[], verdictOf: (id: string) => string | null | undefined, rank: Readonly<Partial<Record<string, number>>>): Record<string, number>;
|
|
24
|
+
/**
|
|
25
|
+
* The first undecided id after `fromId` in `orderedIds`, wrapping, never
|
|
26
|
+
* `fromId` itself. Null when nothing else is undecided (or `fromId` is not
|
|
27
|
+
* in the list), so the caller can fall back to the plain next card.
|
|
28
|
+
*/
|
|
29
|
+
export declare function nextUndecided(orderedIds: readonly string[], fromId: string, isDecided: (id: string) => boolean): string | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The items jatui `reviewKeys()` walks, in VISUAL order (media-picker,
|
|
3
|
+
* diff-review).
|
|
4
|
+
*
|
|
5
|
+
* Decided cards sink with CSS `order`, so DOM order is not what the reviewer
|
|
6
|
+
* sees: j must step to the card that is visually next, not jump over one that
|
|
7
|
+
* moved to the end. Hidden cards (filtered out) are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export function visualItems(root) {
|
|
10
|
+
if (!root)
|
|
11
|
+
return [];
|
|
12
|
+
return [...root.querySelectorAll("[data-item-id]")]
|
|
13
|
+
.filter((el) => !el.hidden && !el.closest("[hidden]"))
|
|
14
|
+
.map((el, i) => ({ el, i, order: Number(el.style.order) || 0 }))
|
|
15
|
+
.sort((a, b) => a.order - b.order || a.i - b.i)
|
|
16
|
+
.map((x) => x.el);
|
|
17
|
+
}
|
|
18
|
+
/** True when a keydown target is a text field (keys are text there). */
|
|
19
|
+
export function isTextTarget(t) {
|
|
20
|
+
if (!(t instanceof HTMLElement))
|
|
21
|
+
return false;
|
|
22
|
+
if (t.isContentEditable)
|
|
23
|
+
return true;
|
|
24
|
+
if (t.matches("textarea, select"))
|
|
25
|
+
return true;
|
|
26
|
+
if (!t.matches("input"))
|
|
27
|
+
return false;
|
|
28
|
+
const type = t.type;
|
|
29
|
+
return !["button", "checkbox", "radio", "range", "submit", "reset"].includes(type);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The sort is a SNAPSHOT (mos-lsmve.12). If `order` tracked live verdicts, a
|
|
33
|
+
* card would jump to the end the moment it was decided and j from it would
|
|
34
|
+
* walk the NEW order onto an already-decided card (UAT: an Accept on §4.2
|
|
35
|
+
* was overwritten by a Reject + note meant for §7.4). Blocks take ranks once
|
|
36
|
+
* at load and again only on a filter change; while the reviewer works,
|
|
37
|
+
* every card stays where it was.
|
|
38
|
+
*
|
|
39
|
+
* CSS `order` per item id from a verdict snapshot. Undecided (or a verdict
|
|
40
|
+
* with no rank) is 0.
|
|
41
|
+
*/
|
|
42
|
+
export function sortRanks(ids, verdictOf, rank) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const id of ids) {
|
|
45
|
+
const v = verdictOf(id);
|
|
46
|
+
out[id] = (v && rank[v]) || 0;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The first undecided id after `fromId` in `orderedIds`, wrapping, never
|
|
52
|
+
* `fromId` itself. Null when nothing else is undecided (or `fromId` is not
|
|
53
|
+
* in the list), so the caller can fall back to the plain next card.
|
|
54
|
+
*/
|
|
55
|
+
export function nextUndecided(orderedIds, fromId, isDecided) {
|
|
56
|
+
const n = orderedIds.length;
|
|
57
|
+
const from = orderedIds.indexOf(fromId);
|
|
58
|
+
if (from === -1)
|
|
59
|
+
return null;
|
|
60
|
+
for (let step = 1; step < n; step++) {
|
|
61
|
+
const id = orderedIds[(from + step) % n];
|
|
62
|
+
if (!isDecided(id))
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|