@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,137 @@
1
+ /**
2
+ * Largest serialized artifacts.data accepted after a write. Generous for a
3
+ * real dashboard (thousands of table rows), small enough that a runaway
4
+ * generator cannot balloon a page's poll payload. The service enforces it
5
+ * inside the UPDATE itself — a write that would exceed the cap changes
6
+ * NOTHING (no partial merge, no data_updated_at bump).
7
+ */
8
+ export const MAX_DATA_BYTES = 1024 * 1024;
9
+ /**
10
+ * Data keys are lowercase-ish identifiers, 1-80 chars, NO dots: the contract
11
+ * is exact-key match (`data[dataKey]`), and a dot would read as path
12
+ * semantics that do not exist. dataKey params in the block schemas carry the
13
+ * same 80-char cap, so every key a block can name is a key a publish can
14
+ * store.
15
+ */
16
+ const DATA_KEY_RE = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,79}$/;
17
+ function isPlainObject(v) {
18
+ return typeof v === "object" && v !== null && !Array.isArray(v);
19
+ }
20
+ /** Own-property read only — a data key must never resolve off the prototype
21
+ * chain (a hostile `dataKey: "__proto__"` would otherwise read
22
+ * Object.prototype as params). JSON.parse creates own properties only, so
23
+ * every legit key passes this. */
24
+ function hasOwn(obj, key) {
25
+ return Object.prototype.hasOwnProperty.call(obj, key);
26
+ }
27
+ /**
28
+ * A value is storable iff it is PLAIN JSON: finite numbers, strings, booleans,
29
+ * nulls, arrays, and objects whose prototype is Object.prototype (a Date, a
30
+ * Map, or a class instance would serialize with loss or throw — refuse them
31
+ * rather than store something the reader did not publish). NaN/Infinity are
32
+ * numbers but serialize to "null" — the classic silent corruption, so they
33
+ * are refused. JSON.parse output always passes: the route's body is already
34
+ * pure JSON, and this guards the service's direct server callers.
35
+ */
36
+ function isSerializableJson(v) {
37
+ if (v === null)
38
+ return true;
39
+ const t = typeof v;
40
+ if (t === "number")
41
+ return Number.isFinite(v);
42
+ if (t === "string" || t === "boolean")
43
+ return true;
44
+ if (t === "object") {
45
+ if (Array.isArray(v)) {
46
+ return v.every((el) => isSerializableJson(el));
47
+ }
48
+ const proto = Object.getPrototypeOf(v);
49
+ if (proto !== null && proto !== Object.prototype)
50
+ return false;
51
+ return Object.values(v).every((el) => isSerializableJson(el));
52
+ }
53
+ return false;
54
+ }
55
+ /** Serialized UTF-8 byte length of a JSON value. TextEncoder, not Buffer —
56
+ * this module is imported by BlockRenderer in the browser too. */
57
+ const textEncoder = new TextEncoder();
58
+ export function serializedBytes(v) {
59
+ return textEncoder.encode(JSON.stringify(v) ?? "").length;
60
+ }
61
+ /**
62
+ * Validate + normalize a data publish. Throws with a message an agent can
63
+ * act on; the route turns that into a 400. `undefined` incoming data is a
64
+ * programmer error (the body always parses to something) and is refused like
65
+ * any other invalid input.
66
+ */
67
+ export function normalizeDataPublish(input) {
68
+ if (input.data === undefined)
69
+ throw new Error("data is required (the JSON value to publish)");
70
+ const hasKey = input.key !== undefined && input.key !== null && input.key !== false;
71
+ if (hasKey && input.replace === true)
72
+ throw new Error("pass either --key (upsert one key) or replace (replace every key), not both");
73
+ if (hasKey) {
74
+ if (typeof input.key !== "string" || !DATA_KEY_RE.test(input.key))
75
+ throw new Error(`key must match ${DATA_KEY_RE.source} (1-80 chars, no dots)`);
76
+ if (!isSerializableJson(input.data))
77
+ throw new Error("data must be JSON-serializable (no NaN, Infinity or functions)");
78
+ return { mode: "key", key: input.key, value: input.data };
79
+ }
80
+ if (!isPlainObject(input.data))
81
+ throw new Error("data must be a JSON object of { dataKey: value } — or name a single key with --key");
82
+ for (const k of Object.keys(input.data)) {
83
+ if (!DATA_KEY_RE.test(k))
84
+ throw new Error(`data key "${k}" must match ${DATA_KEY_RE.source} (1-80 chars, no dots)`);
85
+ if (!isSerializableJson(input.data[k]))
86
+ throw new Error(`data key "${k}" must be JSON-serializable`);
87
+ }
88
+ return {
89
+ mode: input.replace === true ? "replace" : "merge",
90
+ key: null,
91
+ value: input.data,
92
+ };
93
+ }
94
+ /**
95
+ * The dataParam binding. `def.dataParam` names the params field holding the
96
+ * data key; when that field names a key WITH a published value, the live
97
+ * value replaces the params wholesale. Everything else renders inline:
98
+ * no opt-in, no published value, or a key that exists only on the prototype
99
+ * chain (hasOwn guard).
100
+ */
101
+ export function resolveBlockParams(def, rawParams, data) {
102
+ const dataKey = def?.dataParam && isPlainObject(rawParams)
103
+ ? rawParams[def.dataParam]
104
+ : undefined;
105
+ if (typeof dataKey !== "string" || !dataKey)
106
+ return { params: rawParams, dataKey: null, fromData: false };
107
+ if (!hasOwn(data, dataKey))
108
+ return { params: rawParams, dataKey, fromData: false };
109
+ return { params: data[dataKey], dataKey, fromData: true };
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Reader-facing "updated Nm ago"
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * The subtle freshness line: "just now" → "42s ago" → "2m ago" → "3h ago" →
116
+ * "5d ago". Empty string when there is no data yet (the line does not render
117
+ * at all).
118
+ */
119
+ export function formatDataAge(updatedAt, now) {
120
+ if (!updatedAt)
121
+ return "";
122
+ const t = new Date(updatedAt).getTime();
123
+ if (!Number.isFinite(t))
124
+ return "";
125
+ const s = Math.max(0, Math.floor((new Date(now).getTime() - t) / 1000));
126
+ if (s < 15)
127
+ return "just now";
128
+ if (s < 90)
129
+ return `${s}s ago`;
130
+ const m = Math.floor(s / 60);
131
+ if (m < 60)
132
+ return `${m}m ago`;
133
+ const h = Math.floor(m / 60);
134
+ if (h < 36)
135
+ return `${h}h ago`;
136
+ return `${Math.floor(h / 24)}d ago`;
137
+ }
@@ -0,0 +1,41 @@
1
+ export interface LiveDataPoller {
2
+ /** The latest data blob (starts as the server-rendered copy). */
3
+ readonly data: Record<string, unknown>;
4
+ /** When the blob last CHANGED (server's data_updated_at, ISO string). */
5
+ readonly updatedAt: string | null;
6
+ /** A ticking clock for age strings — advances every interval, browser-only. */
7
+ readonly now: number;
8
+ /** Last poll outcome, for a subtle failure hint (never a blocking UI). */
9
+ readonly lastError: "fetch" | "rate_limited" | null;
10
+ /** True while a fetch is in flight. */
11
+ readonly polling: boolean;
12
+ /** The host gave a refresh leg: the page really is live. */
13
+ readonly live: boolean;
14
+ /** The poll cadence, ms. */
15
+ readonly intervalMs: number;
16
+ start: () => void;
17
+ stop: () => void;
18
+ }
19
+ interface PollerOptions {
20
+ /** The host's read of the current data. Undefined → never polls. */
21
+ fetch?: () => Promise<{
22
+ data: Record<string, unknown>;
23
+ dataUpdatedAt: string | null;
24
+ }>;
25
+ /** The server-rendered data (SSR renders it; the poller takes over). */
26
+ initialData: Record<string, unknown>;
27
+ initialUpdatedAt: string | null;
28
+ /** Poll cadence. Default 30s (the phase-3 contract). */
29
+ intervalMs?: number;
30
+ }
31
+ export declare function createLiveDataPoller(options: PollerOptions): LiveDataPoller;
32
+ /**
33
+ * The header's freshness line: "live · updated 2m ago", or an empty string
34
+ * when this page has no data yet (the line does not render at all). The dot
35
+ * is text — a failed poll shows "refresh pending" instead of a lie, and a
36
+ * page with no refresh leg never claims to be live.
37
+ */
38
+ export declare function dataAgeLine(poller: LiveDataPoller): string;
39
+ /** The freshness line's hover text. */
40
+ export declare function dataAgeTitle(poller: LiveDataPoller): string;
41
+ export {};
@@ -0,0 +1,140 @@
1
+ /* liveDataPoller.svelte.js generated by Svelte v5.53.11 */
2
+ import * as $ from 'svelte/internal/client';
3
+ import { formatDataAge } from "./liveData.js";
4
+
5
+ const DEFAULT_INTERVAL_MS = 30_000;
6
+
7
+ export function createLiveDataPoller(options) {
8
+ let data = $.state($.proxy(options.initialData ?? {}));
9
+ let updatedAt = $.state($.proxy(options.initialUpdatedAt ?? null));
10
+ let now = $.state(0);
11
+ let lastError = $.state(null);
12
+ let polling = $.state(false);
13
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
14
+ let timer = null;
15
+ let started = false;
16
+
17
+ async function poll() {
18
+ if ($.get(polling)) return; // a slow response never stacks requests
19
+
20
+ $.set(polling, true);
21
+ $.set(now, Date.now(), true);
22
+
23
+ try {
24
+ if (!options.fetch) return;
25
+
26
+ const body = await options.fetch();
27
+
28
+ $.set(lastError, null);
29
+
30
+ // REPLACE, never merge: the endpoint answers with the whole blob, so
31
+ // the page always renders exactly what is stored.
32
+ $.set(data, body.data ?? {}, true);
33
+
34
+ $.set(updatedAt, body.dataUpdatedAt ?? null, true);
35
+ } catch(err) {
36
+ // A host may signal a rate limit by throwing { status: 429 }.
37
+ $.set(lastError, err?.status === 429 ? "rate_limited" : "fetch", true);
38
+
39
+ // Keep the last good data on the page — a failed poll must never
40
+ // blank a dashboard.
41
+ } finally {
42
+ $.set(polling, false);
43
+ }
44
+ }
45
+
46
+ function tick() {
47
+ // document is undefined during SSR module init — guarded because start()
48
+ // is only ever called from a browser $effect, but the listener setup is
49
+ // cheap enough to keep defensive.
50
+ if (typeof document !== "undefined" && !document.hidden) void poll(); else $.set(now, Date.now(), true);
51
+ }
52
+
53
+ function onVisibility() {
54
+ // Regaining visibility refreshes immediately instead of waiting out the
55
+ // interval — the common gesture is "switch back to the dashboard tab".
56
+ if (typeof document !== "undefined" && !document.hidden) void poll();
57
+ }
58
+
59
+ function start() {
60
+ if (started || !options.fetch) return;
61
+
62
+ started = true;
63
+ $.set(now, Date.now(), true);
64
+ timer = setInterval(tick, intervalMs);
65
+
66
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
67
+ }
68
+
69
+ function stop() {
70
+ if (!started) return;
71
+
72
+ started = false;
73
+
74
+ if (timer) clearInterval(timer);
75
+
76
+ timer = null;
77
+
78
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
79
+ }
80
+
81
+ return {
82
+ get data() {
83
+ return $.get(data);
84
+ },
85
+
86
+ get updatedAt() {
87
+ return $.get(updatedAt);
88
+ },
89
+
90
+ get now() {
91
+ return $.get(now);
92
+ },
93
+
94
+ get lastError() {
95
+ return $.get(lastError);
96
+ },
97
+
98
+ get polling() {
99
+ return $.get(polling);
100
+ },
101
+
102
+ get live() {
103
+ return Boolean(options.fetch);
104
+ },
105
+ intervalMs,
106
+ start,
107
+ stop
108
+ };
109
+ }
110
+
111
+ /**
112
+ * The header's freshness line: "live · updated 2m ago", or an empty string
113
+ * when this page has no data yet (the line does not render at all). The dot
114
+ * is text — a failed poll shows "refresh pending" instead of a lie, and a
115
+ * page with no refresh leg never claims to be live.
116
+ */
117
+ export function dataAgeLine(poller) {
118
+ if (!poller.updatedAt) return "";
119
+
120
+ const age = formatDataAge(poller.updatedAt, poller.now || Date.now());
121
+
122
+ if (!age) return "";
123
+ if (!poller.live) return `updated ${age}`;
124
+ if (poller.lastError) return `updated ${age} · refresh pending`;
125
+
126
+ return `live · updated ${age}`;
127
+ }
128
+
129
+ /** The freshness line's hover text. */
130
+ export function dataAgeTitle(poller) {
131
+ if (!poller.live) return "Data as of when this page loaded";
132
+
133
+ const s = Math.round(poller.intervalMs / 1000);
134
+
135
+ const every = s % 60 === 0
136
+ ? `${s / 60} minute${s === 60 ? "" : "s"}`
137
+ : `${s} seconds`;
138
+
139
+ return `Live data refreshes this page every ${every} while it is open`;
140
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Jump navigation for a review page's still-open units (mos-cs31v).
3
+ *
4
+ * The Finish panel's thumbnail cards and the sticky header's "N open" chip
5
+ * both land the reviewer ON an undecided item: scroll it into view (instant
6
+ * under prefers-reduced-motion), move focus there — the screen reader
7
+ * announces the card, the sighted eye gets a ring flash — and a CURSOR over
8
+ * the open list so N / shift+N walk next/previous without ever losing the
9
+ * thread. The cursor survives decisions: when the unit it names gets decided
10
+ * it leaves the list, and the unit that took its POSITION inherits the
11
+ * cursor — so "decide, N, decide, N" walks down the page instead of
12
+ * restarting at the top.
13
+ *
14
+ * DOM knowledge is kept to two small functions; the cursor is pure and
15
+ * unit-tested. Blocks render `id="block-<id>"` (BlockRenderer) and their
16
+ * reviewable items carry `data-item-id` (checklist rows, media cards, diff
17
+ * sections) — the same handles the blocks' own keyboard nav use.
18
+ */
19
+ import type { OpenReviewUnit } from "./review.js";
20
+ /** The part of a unit the jump machinery needs. */
21
+ export type JumpUnit = Pick<OpenReviewUnit, "key" | "blockId" | "itemId" | "label">;
22
+ /**
23
+ * Scroll the unit into view, focus it, flash it. `reduceMotion` overrides
24
+ * the media query (tests); under it the jump is instant and no flash runs.
25
+ * Returns the element that took the focus, or null when the unit renders
26
+ * nowhere (the page is not mounted, or the block is gone from this version).
27
+ */
28
+ export declare function revealUnit(unit: JumpUnit, opts?: {
29
+ reduceMotion?: boolean;
30
+ }): HTMLElement | null;
31
+ export interface OpenNav {
32
+ /** Walk to the next (1) / previous (-1) open unit and reveal it. Null
33
+ * when the page has no open units — the callers hide the controls then. */
34
+ jump: (dir: 1 | -1) => JumpUnit | null;
35
+ /** Jump to ONE unit (a Finish card click), cursor and all. */
36
+ jumpTo: (unit: JumpUnit) => void;
37
+ }
38
+ /**
39
+ * The cursor over the still-open list. `getUnits` is read at every jump —
40
+ * the list shrinks live as items are decided — and `onJump` does the DOM
41
+ * work (revealUnit + the announcement) exactly once per jump.
42
+ */
43
+ export declare function createOpenNav(options: {
44
+ getUnits: () => JumpUnit[];
45
+ onJump: (unit: JumpUnit) => void;
46
+ }): OpenNav;
@@ -0,0 +1,106 @@
1
+ /** BlockRenderer's section anchor for a block id. */
2
+ const sectionFor = (blockId) => typeof document === "undefined"
3
+ ? null
4
+ : document.getElementById(`block-${blockId}`);
5
+ /** Escape an id for an attribute selector. CSS.escape everywhere real;
6
+ * jsdom test environments ship no CSS global, and the ids that reach here
7
+ * are schema-restricted (never a quote or a backslash), so the fallback
8
+ * only has to be honest about those two. */
9
+ const attrEscape = (v) => typeof CSS !== "undefined" && typeof CSS.escape === "function"
10
+ ? CSS.escape(v)
11
+ : v.replace(/["\\]/g, "\\$&");
12
+ /** The element a unit renders at: its item card, else the block section.
13
+ * A card hidden behind the block's own filter (diff-review's Undecided
14
+ * view) falls back to the section — still the right neighborhood. */
15
+ function reviewUnitElement(unit) {
16
+ const root = sectionFor(unit.blockId);
17
+ if (!root || !unit.itemId)
18
+ return root;
19
+ const card = root.querySelector(`[data-item-id="${attrEscape(unit.itemId)}"]`);
20
+ if (!card || card.checkVisibility?.() === false)
21
+ return root;
22
+ return card;
23
+ }
24
+ const FOCUSABLE = "a[href], button, input, textarea, select, [tabindex]";
25
+ /** Put focus on the unit's element, adding a temporary tabindex when it has
26
+ * none of its own (a section, a plain card). preventScroll: the scroll
27
+ * below already centers the item; a focusing scroll would fight it. */
28
+ function focusUnit(el) {
29
+ if (!el.matches(FOCUSABLE))
30
+ el.setAttribute("tabindex", "-1");
31
+ el.focus({ preventScroll: true });
32
+ }
33
+ /** A brief ring around the jumped item — WAAPI, so it needs no shared CSS
34
+ * and releases the element the moment it ends. Skipped entirely under
35
+ * prefers-reduced-motion (the jump itself is the feedback there). */
36
+ function flashBorder(el) {
37
+ el.animate?.([
38
+ { boxShadow: "0 0 0 0px transparent" },
39
+ { boxShadow: "0 0 0 3px var(--color-primary)", offset: 0.15 },
40
+ { boxShadow: "0 0 0 3px transparent" },
41
+ ], { duration: 1400, easing: "ease-out" });
42
+ }
43
+ /**
44
+ * Scroll the unit into view, focus it, flash it. `reduceMotion` overrides
45
+ * the media query (tests); under it the jump is instant and no flash runs.
46
+ * Returns the element that took the focus, or null when the unit renders
47
+ * nowhere (the page is not mounted, or the block is gone from this version).
48
+ */
49
+ export function revealUnit(unit, opts = {}) {
50
+ const el = reviewUnitElement(unit);
51
+ if (!el)
52
+ return null;
53
+ const reduce = opts.reduceMotion ??
54
+ window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
55
+ el.scrollIntoView?.({
56
+ behavior: reduce ? "auto" : "smooth",
57
+ block: "center",
58
+ });
59
+ focusUnit(el);
60
+ if (!reduce)
61
+ flashBorder(el);
62
+ return el;
63
+ }
64
+ /**
65
+ * The cursor over the still-open list. `getUnits` is read at every jump —
66
+ * the list shrinks live as items are decided — and `onJump` does the DOM
67
+ * work (revealUnit + the announcement) exactly once per jump.
68
+ */
69
+ export function createOpenNav(options) {
70
+ /** The key of the unit the cursor names; null until the first jump. */
71
+ let cursorKey = null;
72
+ /** The POSITION the cursor held — the fallback once its unit is decided
73
+ * away, because the successor inherits the position, not the identity. */
74
+ let cursorIndex = 0;
75
+ function jump(dir) {
76
+ const units = options.getUnits();
77
+ const len = units.length;
78
+ if (len === 0)
79
+ return null;
80
+ const at = cursorKey ? units.findIndex((u) => u.key === cursorKey) : -1;
81
+ const t = at === -1
82
+ ? // The cursor's unit was decided away, or this is the first press.
83
+ // Forwards lands ON the stored position — the first press starts
84
+ // at the top of the page, and a decided unit hands its place to
85
+ // its successor. Backwards lands BEFORE it (wrapping), so the
86
+ // first shift+N reaches the last open item.
87
+ dir === 1
88
+ ? Math.min(cursorIndex, len - 1)
89
+ : cursorIndex - 1
90
+ : at + dir;
91
+ const i = ((t % len) + len) % len;
92
+ const unit = units[i];
93
+ cursorKey = unit.key;
94
+ cursorIndex = i;
95
+ options.onJump(unit);
96
+ return unit;
97
+ }
98
+ function jumpTo(unit) {
99
+ const at = options.getUnits().findIndex((u) => u.key === unit.key);
100
+ cursorKey = unit.key;
101
+ if (at !== -1)
102
+ cursorIndex = at;
103
+ options.onJump(unit);
104
+ }
105
+ return { jump, jumpTo };
106
+ }
@@ -0,0 +1,17 @@
1
+ export interface PageReviewBlock {
2
+ id: string;
3
+ type: string;
4
+ }
5
+ export interface PageReviewKeysOptions {
6
+ /** The review blocks on the page, in page order. */
7
+ getBlocks: () => PageReviewBlock[];
8
+ /** The section a block renders in (BlockRenderer: `#block-<id>`). */
9
+ sectionFor?: (id: string) => HTMLElement | null;
10
+ }
11
+ /** True when focus sits on the page or its chrome, not in a block or modal.
12
+ * Exported for the page's OTHER chrome-level key handlers (mos-cs31v: the
13
+ * still-open n / shift+N walk shares the same "not inside a block" rule). */
14
+ export declare function isPageChromeTarget(t: EventTarget | null): boolean;
15
+ export declare function createPageReviewKeys(options: PageReviewKeysOptions): {
16
+ handleKeydown: (e: KeyboardEvent) => boolean;
17
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Page-level review keys (mos-lsmve.13).
3
+ *
4
+ * Each review block's jatui `reviewKeys` listens on the block only (a page
5
+ * can carry several), so on page load — focus on <body> — j, f and ? did
6
+ * nothing until the reviewer clicked or tabbed into a card, and nothing on
7
+ * the page said so. This router listens on the window and, ONLY when focus
8
+ * is on the page itself or its chrome (never inside a block, a text field or
9
+ * an open dialog), hands the navigation keys to the FIRST review block:
10
+ *
11
+ * j, gg focus that block's first visible card
12
+ * f ask the block for its next item that needs you (its own
13
+ * needs-you rule); if nothing takes focus, its first card
14
+ * ? that block's own shortcuts
15
+ *
16
+ * Verdict keys (a / x / r / b, u, n, …) are deliberately NOT routed:
17
+ * deciding needs a visible, focused item. Once a card has focus, the block's
18
+ * own handler owns every key and this router steps aside.
19
+ */
20
+ import { isTextTarget, visualItems } from "./review/visualOrder.js";
21
+ /** Block types whose `f` is needs-you. The checklist still honours its old
22
+ * `f` = Fail letter, which would act on a stale cursor — so a checklist
23
+ * gets a plain focus instead of a routed `f`. */
24
+ const F_ROUTES_TO_BLOCK = new Set(["media-picker", "diff-review"]);
25
+ /** How long the first `g` of `gg` waits for the second (ms). */
26
+ const GG_WINDOW_MS = 800;
27
+ const defaultSection = (id) => typeof document === "undefined"
28
+ ? null
29
+ : document.getElementById(`block-${id}`);
30
+ /** True when focus sits on the page or its chrome, not in a block or modal.
31
+ * Exported for the page's OTHER chrome-level key handlers (mos-cs31v: the
32
+ * still-open n / shift+N walk shares the same "not inside a block" rule). */
33
+ export function isPageChromeTarget(t) {
34
+ if (typeof document === "undefined")
35
+ return false;
36
+ if (t === null || t === window || t === document)
37
+ return true;
38
+ if (!(t instanceof HTMLElement))
39
+ return false;
40
+ if (isTextTarget(t))
41
+ return false;
42
+ if (t.closest('[id^="block-"]'))
43
+ return false;
44
+ if (t.closest('dialog, [role="dialog"], [aria-modal="true"]'))
45
+ return false;
46
+ // A modal that holds no focus (focus fell back to <body>) still owns keys.
47
+ if (document.querySelector('dialog[open], [aria-modal="true"]'))
48
+ return false;
49
+ return true;
50
+ }
51
+ export function createPageReviewKeys(options) {
52
+ const sectionFor = options.sectionFor ?? defaultSection;
53
+ let lastG = 0;
54
+ /** The first review block that has a card to land on. */
55
+ function firstBlock() {
56
+ for (const b of options.getBlocks()) {
57
+ const section = sectionFor(b.id);
58
+ if (!section)
59
+ continue;
60
+ const cards = visualItems(section);
61
+ if (cards.length > 0)
62
+ return { block: b, section, cards };
63
+ }
64
+ return null;
65
+ }
66
+ function focusCard(el) {
67
+ el.focus({ preventScroll: true });
68
+ el.scrollIntoView?.({ block: "center" });
69
+ }
70
+ /** Re-dispatch a key from inside the block so its own listener answers. */
71
+ function forward(target, key) {
72
+ target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true }));
73
+ }
74
+ /** Returns true when the key was routed (the caller's event is consumed). */
75
+ function handleKeydown(e) {
76
+ if (e.defaultPrevented || e.isComposing)
77
+ return false;
78
+ if (e.ctrlKey || e.metaKey || e.altKey)
79
+ return false;
80
+ if (!isPageChromeTarget(e.target))
81
+ return false;
82
+ const key = e.key;
83
+ if (key !== "g")
84
+ lastG = 0;
85
+ if (key !== "j" && key !== "f" && key !== "?" && key !== "g")
86
+ return false;
87
+ if (key === "g") {
88
+ const now = Date.now();
89
+ if (!lastG || now - lastG > GG_WINDOW_MS) {
90
+ lastG = now;
91
+ return false;
92
+ }
93
+ lastG = 0;
94
+ }
95
+ const hit = firstBlock();
96
+ if (!hit)
97
+ return false;
98
+ e.preventDefault();
99
+ const first = hit.cards[0];
100
+ if (key === "?") {
101
+ forward(first, "?");
102
+ return true;
103
+ }
104
+ if (key === "f" && F_ROUTES_TO_BLOCK.has(hit.block.type)) {
105
+ forward(first, "f");
106
+ // The block focused its next needs-you card; if nothing needs you,
107
+ // land on the first card so the reviewer is somewhere.
108
+ if (hit.section.contains(document.activeElement))
109
+ return true;
110
+ }
111
+ focusCard(first);
112
+ return true;
113
+ }
114
+ return { handleKeydown };
115
+ }