@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.
Files changed (271) hide show
  1. package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
  2. package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
  3. package/dist/artifact-blocks/BlockComments.svelte +309 -0
  4. package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
  5. package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
  6. package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
  7. package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
  8. package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
  9. package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
  10. package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
  11. package/dist/artifact-blocks/CONTRACT.md +225 -0
  12. package/dist/artifact-blocks/CommentPill.svelte +49 -0
  13. package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
  14. package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
  15. package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
  16. package/dist/artifact-blocks/Glyph.svelte +54 -0
  17. package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
  18. package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
  19. package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
  20. package/dist/artifact-blocks/Markdown.svelte +186 -0
  21. package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
  22. package/dist/artifact-blocks/NoteComposer.svelte +31 -0
  23. package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
  24. package/dist/artifact-blocks/PlainComposer.svelte +200 -0
  25. package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
  26. package/dist/artifact-blocks/SectionTools.svelte +102 -0
  27. package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
  28. package/dist/artifact-blocks/adapter.d.ts +13 -0
  29. package/dist/artifact-blocks/adapter.js +31 -0
  30. package/dist/artifact-blocks/autoSend.d.ts +49 -0
  31. package/dist/artifact-blocks/autoSend.js +103 -0
  32. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
  33. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
  34. package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
  35. package/dist/artifact-blocks/blocks/actions/block.js +74 -0
  36. package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
  37. package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
  38. package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
  39. package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
  40. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
  41. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
  42. package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
  43. package/dist/artifact-blocks/blocks/callout/block.js +39 -0
  44. package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
  45. package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
  46. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
  47. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
  48. package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
  49. package/dist/artifact-blocks/blocks/chart/block.js +70 -0
  50. package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
  51. package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
  52. package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
  53. package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
  54. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
  55. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
  56. package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
  57. package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
  58. package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
  59. package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
  60. package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
  61. package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
  62. package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
  63. package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
  64. package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
  65. package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
  66. package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
  67. package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
  68. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
  69. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
  70. package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
  71. package/dist/artifact-blocks/blocks/code/block.js +31 -0
  72. package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
  73. package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
  74. package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
  75. package/dist/artifact-blocks/blocks/code/schema.js +39 -0
  76. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
  77. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
  78. package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
  79. package/dist/artifact-blocks/blocks/collage/block.js +29 -0
  80. package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
  81. package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
  82. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
  83. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
  84. package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
  85. package/dist/artifact-blocks/blocks/compare/block.js +32 -0
  86. package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
  87. package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
  88. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
  89. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
  90. package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
  91. package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
  92. package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
  93. package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
  94. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
  95. package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
  96. package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
  97. package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
  98. package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
  99. package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
  100. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
  101. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
  102. package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
  103. package/dist/artifact-blocks/blocks/diff/block.js +36 -0
  104. package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
  105. package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
  106. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
  107. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
  108. package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
  109. package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
  110. package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
  111. package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
  112. package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
  113. package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
  114. package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
  115. package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
  116. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
  117. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
  118. package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
  119. package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
  120. package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
  121. package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
  122. package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
  123. package/dist/artifact-blocks/blocks/group-content.js +16 -0
  124. package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
  125. package/dist/artifact-blocks/blocks/group-data.js +7 -0
  126. package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
  127. package/dist/artifact-blocks/blocks/group-review.js +12 -0
  128. package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
  129. package/dist/artifact-blocks/blocks/group-visual.js +14 -0
  130. package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
  131. package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
  132. package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
  133. package/dist/artifact-blocks/blocks/map/block.js +62 -0
  134. package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
  135. package/dist/artifact-blocks/blocks/map/schema.js +113 -0
  136. package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
  137. package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
  138. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
  139. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
  140. package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
  141. package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
  142. package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
  143. package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
  144. package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
  145. package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
  146. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
  147. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
  148. package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
  149. package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
  150. package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
  151. package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
  152. package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
  153. package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
  154. package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
  155. package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
  156. package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
  157. package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
  158. package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
  159. package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
  160. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
  161. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
  162. package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
  163. package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
  164. package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
  165. package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
  166. package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
  167. package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
  168. package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
  169. package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
  170. package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
  171. package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
  172. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
  173. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
  174. package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
  175. package/dist/artifact-blocks/blocks/program/block.js +68 -0
  176. package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
  177. package/dist/artifact-blocks/blocks/program/schema.js +268 -0
  178. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
  179. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
  180. package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
  181. package/dist/artifact-blocks/blocks/stats/block.js +49 -0
  182. package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
  183. package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
  184. package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
  185. package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
  186. package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
  187. package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
  188. package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
  189. package/dist/artifact-blocks/blocks/table/block.js +100 -0
  190. package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
  191. package/dist/artifact-blocks/blocks/table/schema.js +278 -0
  192. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
  193. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
  194. package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
  195. package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
  196. package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
  197. package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
  198. package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
  199. package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
  200. package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
  201. package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
  202. package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
  203. package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
  204. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
  205. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
  206. package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
  207. package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
  208. package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
  209. package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
  210. package/dist/artifact-blocks/builtins.d.ts +40 -0
  211. package/dist/artifact-blocks/builtins.js +22 -0
  212. package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
  213. package/dist/artifact-blocks/contractBlocks.js +279 -0
  214. package/dist/artifact-blocks/history.d.ts +140 -0
  215. package/dist/artifact-blocks/history.js +363 -0
  216. package/dist/artifact-blocks/index.d.ts +40 -0
  217. package/dist/artifact-blocks/index.js +46 -0
  218. package/dist/artifact-blocks/listen/context.d.ts +30 -0
  219. package/dist/artifact-blocks/listen/context.js +30 -0
  220. package/dist/artifact-blocks/liveData.d.ts +78 -0
  221. package/dist/artifact-blocks/liveData.js +137 -0
  222. package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
  223. package/dist/artifact-blocks/liveDataPoller.js +140 -0
  224. package/dist/artifact-blocks/openNav.d.ts +46 -0
  225. package/dist/artifact-blocks/openNav.js +106 -0
  226. package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
  227. package/dist/artifact-blocks/pageReviewKeys.js +115 -0
  228. package/dist/artifact-blocks/params.d.ts +98 -0
  229. package/dist/artifact-blocks/params.js +159 -0
  230. package/dist/artifact-blocks/registry.d.ts +27 -0
  231. package/dist/artifact-blocks/registry.js +83 -0
  232. package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
  233. package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
  234. package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
  235. package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
  236. package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
  237. package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
  238. package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
  239. package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
  240. package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
  241. package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
  242. package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
  243. package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
  244. package/dist/artifact-blocks/review/attachments.d.ts +10 -0
  245. package/dist/artifact-blocks/review/attachments.js +37 -0
  246. package/dist/artifact-blocks/review/bulk.d.ts +52 -0
  247. package/dist/artifact-blocks/review/bulk.js +61 -0
  248. package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
  249. package/dist/artifact-blocks/review/itemReview.js +112 -0
  250. package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
  251. package/dist/artifact-blocks/review/roundLines.js +20 -0
  252. package/dist/artifact-blocks/review/rounds.d.ts +86 -0
  253. package/dist/artifact-blocks/review/rounds.js +302 -0
  254. package/dist/artifact-blocks/review/saver.d.ts +23 -0
  255. package/dist/artifact-blocks/review/saver.js +48 -0
  256. package/dist/artifact-blocks/review/team.d.ts +31 -0
  257. package/dist/artifact-blocks/review/team.js +42 -0
  258. package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
  259. package/dist/artifact-blocks/review/visualOrder.js +66 -0
  260. package/dist/artifact-blocks/review.d.ts +180 -0
  261. package/dist/artifact-blocks/review.js +716 -0
  262. package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
  263. package/dist/artifact-blocks/thumb-kit.js +149 -0
  264. package/dist/artifact-blocks/types.d.ts +513 -0
  265. package/dist/artifact-blocks/types.js +1 -0
  266. package/dist/components/Button.svelte +8 -0
  267. package/dist/components/TextInput.svelte +39 -6
  268. package/dist/components/TextInput.svelte.d.ts +15 -7
  269. package/dist/components/basic/Checkbox.svelte +52 -14
  270. package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
  271. 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
+ }