@doxbrix/doxloop 0.1.5 → 0.2.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 (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,1248 @@
1
+ /**
2
+ * The review center: four screens joined by real URLs.
3
+ *
4
+ * / the proposals waiting for a decision
5
+ * /runs/:id what one proposal changes, and why
6
+ * /runs/:id/pages/:n one page at a time, current beside proposed
7
+ * /runs/:id/done what was applied
8
+ *
9
+ * The reading screen carries one bar above the comparison and one below it;
10
+ * everything else lives on the screen where it belongs.
11
+ */
12
+ const ICONS = {
13
+ page: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="14" y2="17"/>',
14
+ navigation: '<rect x="3" y="3" width="7" height="18" rx="1.5"/><rect x="14" y="3" width="7" height="8" rx="1.5"/><rect x="14" y="15" width="7" height="6" rx="1.5"/>',
15
+ configuration: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.72l.15-.1a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
16
+ evidence: '<path d="M20 13c0 5-3.5 7.5-8 9-4.5-1.5-8-4-8-9V5l8-3 8 3z"/><path d="m9 12 2 2 4-4"/>',
17
+ asset: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-4.5-4.5L9 18"/>',
18
+ check: '<polyline points="20 6 9 17 4 12"/>',
19
+ 'check-circle': '<circle cx="12" cy="12" r="10"/><polyline points="16 9.5 10.8 15 8 12.3"/>',
20
+ x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
21
+ refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.4"/><polyline points="21 3 21 9 15 9"/>',
22
+ clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6.5 12 12 15.5 14"/>',
23
+ columns: '<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="12" y1="4" x2="12" y2="20"/>',
24
+ code: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
25
+ alert: '<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
26
+ shield: '<path d="M20 13c0 5-3.5 7.5-8 9-4.5-1.5-8-4-8-9V5l8-3 8 3z"/>',
27
+ keyboard: '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 13h.01M18 13h.01M10 13h4"/>',
28
+ inbox: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5h13a2 2 0 0 1 1.9 1.4L22 12v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6l1.6-5.6A2 2 0 0 1 5.5 5z"/>',
29
+ arrowLeft: '<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>',
30
+ arrowRight: '<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>',
31
+ chevronRight: '<polyline points="9 18 15 12 9 6"/>',
32
+ chevronDown: '<polyline points="6 9 12 15 18 9"/>',
33
+ sliders: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>',
34
+ sparkle: '<path d="M12 3l1.9 5.8L20 11l-6.1 2.2L12 19l-1.9-5.8L4 11l6.1-2.2z"/>',
35
+ search: '<circle cx="11" cy="11" r="7"/><line x1="20" y1="20" x2="16.65" y2="16.65"/>',
36
+ activity: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
37
+ };
38
+ export function icon(name, size = 16) {
39
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name] ?? ICONS.page}</svg>`;
40
+ }
41
+ export function reviewDocument(token) {
42
+ return `<!doctype html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="utf-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1">
47
+ <title>Doxloop · Review</title>
48
+ <link rel="icon" href="/brand/favicon.png">
49
+ <link rel="preconnect" href="https://fonts.googleapis.com">
50
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
51
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
52
+ <style>${shellCss()}</style>
53
+ </head>
54
+ <body>
55
+ <div id="app" class="rv-app"></div>
56
+ <div id="toast" class="rv-toast" role="status" aria-live="polite"></div>
57
+ <div id="overlay" class="rv-overlay" hidden>
58
+ <div class="rv-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
59
+ <div class="rv-modal-head"><h2 id="modal-title">Keyboard shortcuts</h2><button class="rv-icon-btn" data-close aria-label="Close">${icon('x', 15)}</button></div>
60
+ <dl class="rv-keys">
61
+ <div><dt><kbd>Enter</kbd> <kbd>A</kbd></dt><dd>Accept this page and continue</dd></div>
62
+ <div><dt><kbd>S</kbd></dt><dd>Skip this page</dd></div>
63
+ <div><dt><kbd>←</kbd> <kbd>→</kbd></dt><dd>Previous / next page</dd></div>
64
+ <div><dt><kbd>D</kbd></dt><dd>Switch between rendered and source</dd></div>
65
+ <div><dt><kbd>Esc</kbd></dt><dd>Back to the proposal</dd></div>
66
+ <div><dt><kbd>?</kbd></dt><dd>This help</dd></div>
67
+ </dl>
68
+ <p class="rv-modal-note">Inside the comparison, <kbd>N</kbd> and <kbd>P</kbd> jump between changed blocks.</p>
69
+ </div>
70
+ </div>
71
+ <script>${shellScript(token)}</script>
72
+ </body>
73
+ </html>`;
74
+ }
75
+ /* ── Styles ───────────────────────────────────────────────────────────── */
76
+ function shellCss() {
77
+ return `
78
+ :root{
79
+ /* Brand, sampled from the Doxloop wordmark. */
80
+ --navy:#022D78;--navy-hover:#01235E;--navy-active:#011A48;
81
+ --navy-light:#E7ECF7;--navy-lighter:#F4F7FC;--navy-rgb:2 45 120;
82
+ --teal:#17B1AC;--teal-hover:#129490;--teal-strong:#0C6F6B;
83
+ --teal-soft:#E5F7F6;--teal-border:#A9E5E2;--teal-rgb:23 177 172;
84
+
85
+ --text-primary:#0E1A31;--text-secondary:#4C5875;--text-muted:#6B7695;--text-subtle:#96A0B8;
86
+ --surface-page:#F5F7FB;--surface-card:#FFFFFF;--bg-section:#EEF1F7;--bg-rail:#FAFBFD;
87
+ --border:#E1E6F0;--border-input:#D5DCEA;
88
+
89
+ --success:#16A34A;--success-strong:#136B36;--success-soft:#E8F7EE;--success-border:#B7E4C7;
90
+ --danger:#DC2626;--danger-strong:#A81C1C;--danger-soft:#FDECEC;--danger-border:#F5C4C4;
91
+ --warning:#C2760A;--warning-strong:#8F5606;--warning-soft:#FDF3E3;--warning-border:#F0DBAF;
92
+
93
+ --shadow-xs:0 1px 2px rgba(2,45,120,.05);
94
+ --shadow-sm:0 1px 4px rgba(2,45,120,.07),0 0 1px rgba(2,45,120,.05);
95
+ --shadow-md:0 6px 18px rgba(2,45,120,.10),0 1px 3px rgba(2,45,120,.06);
96
+ --shadow-lg:0 16px 40px rgba(2,45,120,.16),0 2px 8px rgba(2,45,120,.08);
97
+ --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
98
+ color-scheme:light;
99
+ }
100
+ *{box-sizing:border-box}
101
+ html,body{height:100%}
102
+ body{margin:0;font-family:"Instrument Sans",system-ui,-apple-system,"Segoe UI",sans-serif;font-size:13.5px;line-height:1.5;color:var(--text-primary);background:var(--surface-page);-webkit-font-smoothing:antialiased}
103
+ button,input{font-family:inherit}
104
+ h1,h2,h3,p,dl,dd,ul{margin:0}
105
+ ul{list-style:none;padding:0}
106
+ a{color:inherit}
107
+ kbd{font-family:var(--mono);font-size:10.5px;background:#fff;border:1px solid var(--border);border-bottom-width:2px;border-radius:4px;padding:1px 5px;color:var(--text-secondary)}
108
+ [hidden]{display:none !important}
109
+ .rv-app{display:flex;flex-direction:column;height:100%}
110
+
111
+ /* ── App bar ── */
112
+ .rv-appbar{display:flex;align-items:center;gap:14px;height:56px;min-height:56px;padding:0 22px;background:#fff;border-bottom:1px solid var(--border)}
113
+ .rv-logo{display:inline-flex;align-items:center;border:0;background:transparent;padding:0;cursor:pointer}
114
+ .rv-logo img{height:21px;width:auto;display:block}
115
+ .rv-appbar-spacer{flex:1}
116
+ .rv-back{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 11px 0 8px;border:1px solid var(--border);border-radius:8px;background:#fff;color:var(--text-secondary);font-size:12.5px;font-weight:600;cursor:pointer;transition:background .14s,color .14s,border-color .14s}
117
+ .rv-back:hover{background:var(--navy-lighter);color:var(--navy);border-color:var(--border-input)}
118
+ .rv-appbar-divider{width:1px;height:22px;background:var(--border)}
119
+ .rv-appbar-title{min-width:0;display:flex;flex-direction:column;line-height:1.25}
120
+ .rv-appbar-title strong{font-size:13.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
121
+ .rv-appbar-title span{font-family:var(--mono);font-size:10.5px;color:var(--text-subtle);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
122
+
123
+ /* ── Page canvas ── */
124
+ .rv-scroll{flex:1;overflow-y:auto}
125
+ .rv-page{width:100%;max-width:860px;margin:0 auto;padding:36px 24px 64px}
126
+ .rv-page--wide{max-width:1020px}
127
+ .rv-page-head{margin-bottom:26px}
128
+ .rv-eyebrow{display:block;font-size:10.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-subtle);margin-bottom:8px}
129
+ .rv-page h1{font-size:26px;font-weight:700;letter-spacing:-.02em;line-height:1.2}
130
+ .rv-page-sub{margin-top:7px;font-size:14px;color:var(--text-secondary);max-width:60ch}
131
+ .rv-section{margin-top:28px}
132
+ .rv-section-title{display:flex;align-items:baseline;gap:8px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-subtle);margin-bottom:10px}
133
+
134
+ /* ── Buttons ── */
135
+ .rv-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:38px;padding:0 16px;border-radius:9px;border:1px solid transparent;font-size:13.5px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background .14s,border-color .14s,color .14s,transform .08s,box-shadow .14s}
136
+ .rv-btn:active:not(:disabled){transform:scale(.985)}
137
+ .rv-btn:disabled{opacity:.45;cursor:not-allowed}
138
+ .rv-btn--primary{background:var(--navy);color:#fff;box-shadow:0 1px 2px rgba(var(--navy-rgb),.35)}
139
+ .rv-btn--primary:hover:not(:disabled){background:var(--navy-hover)}
140
+ .rv-btn--ghost{background:#fff;border-color:var(--border-input);color:var(--text-secondary)}
141
+ .rv-btn--ghost:hover:not(:disabled){background:var(--navy-lighter);color:var(--navy);border-color:var(--navy-light)}
142
+ .rv-btn--quiet{background:transparent;color:var(--text-muted);padding:0 10px}
143
+ .rv-btn--quiet:hover:not(:disabled){color:var(--danger-strong);background:var(--danger-soft)}
144
+ .rv-btn--sm{height:32px;padding:0 12px;font-size:12.5px;border-radius:8px}
145
+ .rv-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;border:1px solid var(--border);background:#fff;color:var(--text-muted);cursor:pointer;transition:background .14s,color .14s}
146
+ .rv-icon-btn:hover{background:var(--navy-lighter);color:var(--navy)}
147
+ .rv-icon-btn.is-busy svg{animation:rv-spin .7s linear infinite}
148
+ @keyframes rv-spin{to{transform:rotate(360deg)}}
149
+ .rv-link{border:0;background:none;padding:0;font:inherit;font-weight:600;color:var(--navy);cursor:pointer;text-decoration:underline;text-underline-offset:2px;text-decoration-color:rgb(var(--navy-rgb)/.3)}
150
+ .rv-link:hover{text-decoration-color:var(--navy)}
151
+
152
+ /* ── Segmented ── */
153
+ .rv-segmented{display:inline-flex;padding:2px;background:var(--bg-section);border-radius:9px}
154
+ .rv-segmented button{border:0;background:transparent;font-size:12.5px;font-weight:600;color:var(--text-muted);padding:6px 13px;border-radius:7px;cursor:pointer;transition:background .14s,color .14s}
155
+ .rv-segmented button.is-active{background:#fff;color:var(--navy);box-shadow:var(--shadow-xs)}
156
+
157
+ /* ── Status ── */
158
+ .rv-pill{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 11px;border-radius:100px;font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:capitalize;background:var(--bg-section);color:var(--text-secondary)}
159
+ .rv-pill--awaiting-review,.rv-pill--partially-applied{background:var(--warning-soft);color:var(--warning-strong)}
160
+ .rv-pill--applied{background:var(--success-soft);color:var(--success-strong)}
161
+ .rv-pill--failed,.rv-pill--conflicted,.rv-pill--rejected{background:var(--danger-soft);color:var(--danger-strong)}
162
+ .rv-pill--generating{background:var(--teal-soft);color:var(--teal-strong)}
163
+ .rv-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--text-subtle)}
164
+ .rv-dot--awaiting-review,.rv-dot--partially-applied{background:var(--warning)}
165
+ .rv-dot--applied{background:var(--success)}
166
+ .rv-dot--failed,.rv-dot--conflicted,.rv-dot--rejected{background:var(--danger)}
167
+ .rv-dot--generating{background:var(--teal);animation:rv-pulse 1.2s ease-in-out infinite}
168
+ @keyframes rv-pulse{50%{opacity:.35}}
169
+
170
+ /* ── Proposal cards ── */
171
+ .rv-cards{display:flex;flex-direction:column;gap:12px}
172
+ .rv-card{display:block;width:100%;text-align:left;background:#fff;border:1px solid var(--border);border-radius:14px;padding:18px 20px;box-shadow:var(--shadow-xs);transition:border-color .15s,box-shadow .15s,transform .15s}
173
+ button.rv-card{cursor:pointer;font:inherit;color:inherit}
174
+ button.rv-card:hover{border-color:var(--navy-light);box-shadow:var(--shadow-md);transform:translateY(-1px)}
175
+ .rv-card-top{display:flex;align-items:center;gap:9px;margin-bottom:9px}
176
+ .rv-card-meta{margin-left:auto;font-size:11.5px;color:var(--text-muted);white-space:nowrap}
177
+ .rv-card h2{font-size:17px;font-weight:700;letter-spacing:-.01em}
178
+ .rv-card-why{margin-top:5px;font-size:13px;color:var(--text-muted)}
179
+ .rv-card-foot{display:flex;align-items:center;gap:12px;margin-top:14px}
180
+ .rv-card-cta{margin-left:auto;display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:var(--navy)}
181
+ .rv-bar{flex:1;max-width:180px;height:5px;border-radius:3px;background:var(--bg-section);overflow:hidden}
182
+ .rv-bar span{display:block;height:100%;border-radius:3px;background:var(--teal);transition:width .3s ease}
183
+ .rv-bar--done span{background:var(--success)}
184
+
185
+ /* ── Overview ── */
186
+ .rv-why{display:flex;gap:12px;padding:14px 16px;background:#fff;border:1px solid var(--border);border-radius:12px;margin-top:18px}
187
+ .rv-why-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:9px;background:var(--teal-soft);color:var(--teal-strong);flex:none}
188
+ .rv-why-body{min-width:0;font-size:13px;color:var(--text-secondary)}
189
+ .rv-why-body strong{display:block;color:var(--text-primary);font-size:12.5px;margin-bottom:2px}
190
+ .rv-why-body code{font-family:var(--mono);font-size:11.5px;color:var(--text-muted);word-break:break-word}
191
+ .rv-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;font-size:12.5px;color:var(--text-muted)}
192
+ .rv-meta-row .rv-sep{color:var(--border-input)}
193
+ .rv-meta-ok{display:inline-flex;align-items:center;gap:5px;color:var(--success-strong);font-weight:600}
194
+
195
+ .rv-rows{border:1px solid var(--border);border-radius:12px;overflow:hidden;background:#fff}
196
+ .rv-row{display:flex;align-items:center;gap:13px;width:100%;text-align:left;padding:14px 16px;border:0;border-bottom:1px solid var(--border);background:#fff;cursor:pointer;font:inherit;color:inherit;transition:background .14s}
197
+ .rv-row:last-child{border-bottom:0}
198
+ .rv-row:hover{background:var(--navy-lighter)}
199
+ .rv-row-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:10px;background:var(--navy-light);color:var(--navy);flex:none}
200
+ .rv-row-body{min-width:0;flex:1;display:flex;flex-direction:column}
201
+ .rv-row-title{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
202
+ .rv-row-path{font-family:var(--mono);font-size:11px;color:var(--text-subtle);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
203
+ .rv-row-right{display:flex;align-items:center;gap:12px;flex:none;color:var(--text-muted);font-size:12.5px}
204
+ .rv-chip{display:inline-flex;align-items:center;gap:5px;height:22px;padding:0 9px;border-radius:7px;font-size:11px;font-weight:700;letter-spacing:.02em;background:var(--bg-section);color:var(--text-muted)}
205
+ .rv-chip--accepted{background:var(--success-soft);color:var(--success-strong)}
206
+ .rv-chip--pending{background:var(--warning-soft);color:var(--warning-strong)}
207
+
208
+ /* Still-authoring state */
209
+ .rv-working{display:flex;gap:14px;align-items:flex-start;margin-top:20px;padding:18px 20px;border:1px solid var(--teal-border);border-radius:12px;background:var(--teal-soft)}
210
+ .rv-working-body{display:flex;flex-direction:column;gap:4px;min-width:0}
211
+ .rv-working-body strong{font-size:14px;color:var(--teal-strong)}
212
+ .rv-working-body span{font-size:13px;color:var(--text-secondary);line-height:1.55}
213
+ .rv-spinner{flex:none;width:20px;height:20px;margin-top:1px;border-radius:50%;border:2px solid rgb(var(--teal-rgb)/.25);border-top-color:var(--teal);animation:rv-spin .8s linear infinite}
214
+ .rv-spinner--sm{width:13px;height:13px;border-width:2px;margin:0}
215
+
216
+ .rv-support{margin-top:18px;border:1px solid var(--border);border-radius:12px;background:#fff;overflow:hidden}
217
+ .rv-support summary{display:flex;align-items:center;gap:10px;padding:13px 16px;cursor:pointer;list-style:none;font-size:13px}
218
+ .rv-support summary::-webkit-details-marker{display:none}
219
+ .rv-support-chevron{color:var(--text-subtle);transition:transform .15s}
220
+ .rv-support[open] .rv-support-chevron{transform:rotate(90deg)}
221
+ .rv-support-note{margin-left:auto;font-size:12px;color:var(--text-muted);text-align:right}
222
+ .rv-support-body{border-top:1px solid var(--border);background:var(--bg-rail)}
223
+
224
+ .rv-actionbar{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;margin-top:28px;padding:14px 0 0;border-top:1px solid var(--border);background:linear-gradient(180deg,rgba(245,247,251,0),var(--surface-page) 40%)}
225
+ .rv-actionbar .rv-spacer{flex:1}
226
+
227
+ /* ── Focus screen ── */
228
+ .rv-focus{display:flex;flex-direction:column;flex:1;min-height:0}
229
+ .rv-focus-bar{display:flex;align-items:center;gap:12px;height:52px;min-height:52px;padding:0 18px;background:#fff;border-bottom:1px solid var(--border)}
230
+ .rv-title-btn{display:inline-flex;align-items:center;gap:8px;min-width:0;border:1px solid transparent;background:transparent;border-radius:9px;padding:5px 9px;cursor:pointer;font:inherit;color:inherit;transition:background .14s,border-color .14s}
231
+ .rv-title-btn:hover{background:var(--navy-lighter);border-color:var(--navy-light)}
232
+ .rv-title-btn strong{font-size:14.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
233
+ .rv-title-btn svg{flex:none;color:var(--text-subtle)}
234
+ .rv-counter{font-size:12.5px;font-weight:600;color:var(--text-muted);white-space:nowrap}
235
+ .rv-menu-wrap{position:relative}
236
+ .rv-menu{position:absolute;z-index:60;top:calc(100% + 6px);min-width:284px;max-width:340px;background:#fff;border:1px solid var(--border);border-radius:12px;box-shadow:var(--shadow-lg);padding:6px;display:flex;flex-direction:column;gap:2px}
237
+ .rv-menu--right{right:0}
238
+ .rv-menu-label{padding:8px 10px 4px;font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text-subtle)}
239
+ .rv-menu-item{display:flex;align-items:flex-start;gap:9px;width:100%;text-align:left;border:0;background:transparent;border-radius:8px;padding:8px 10px;cursor:pointer;font:inherit;font-size:13px;color:var(--text-primary);transition:background .12s}
240
+ .rv-menu-item>svg{flex:none;margin-top:2px;color:var(--text-muted)}
241
+ .rv-menu-item.is-active>svg{color:var(--navy)}
242
+ .rv-menu-item:hover{background:var(--navy-lighter)}
243
+ .rv-menu-item.is-active{color:var(--navy);font-weight:600}
244
+ .rv-menu-item .rv-menu-check{margin-left:auto;color:var(--navy)}
245
+ .rv-menu-item small{display:block;font-size:11px;color:var(--text-subtle);font-weight:400}
246
+ .rv-menu-sep{height:1px;background:var(--border);margin:4px 2px}
247
+
248
+ .rv-stage{position:relative;flex:1;min-height:0;background:var(--surface-page)}
249
+ .rv-stage iframe{width:100%;height:100%;border:0;display:block;background:#fff}
250
+ .rv-source{position:absolute;inset:0;overflow:auto;padding:18px 20px 40px}
251
+
252
+ .rv-focus-foot{display:flex;align-items:center;gap:14px;height:60px;min-height:60px;padding:0 18px;background:#fff;border-top:1px solid var(--border)}
253
+ .rv-progress-dots{display:flex;align-items:center;gap:6px}
254
+ .rv-pdot{width:9px;height:9px;border-radius:50%;background:var(--border-input);transition:background .2s,transform .2s}
255
+ .rv-pdot--done{background:var(--success)}
256
+ .rv-pdot--current{background:var(--navy);transform:scale(1.25)}
257
+ .rv-foot-count{font-size:13px;color:var(--text-muted)}
258
+ .rv-foot-actions{margin-left:auto;display:flex;align-items:center;gap:10px}
259
+ .rv-applied-note{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:var(--success-strong)}
260
+
261
+ /* ── Source diff ── */
262
+ .rv-hunk{border:1px solid var(--border);border-radius:11px;background:#fff;overflow:hidden;margin-bottom:12px;box-shadow:var(--shadow-xs)}
263
+ .rv-hunk--accepted{border-color:var(--success-border)}
264
+ .rv-hunk--rejected{opacity:.6}
265
+ .rv-hunk-head{display:flex;align-items:center;gap:10px;padding:10px 13px;border-bottom:1px solid var(--border);background:var(--bg-rail)}
266
+ .rv-hunk-title{font-size:12.5px;font-weight:700}
267
+ .rv-hunk-lines{font-family:var(--mono);font-size:10.5px;color:var(--text-subtle)}
268
+ .rv-hunk-actions{margin-left:auto;display:flex;align-items:center;gap:8px}
269
+ .rv-hunk-state{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700}
270
+ .rv-hunk-state--accepted{color:var(--success-strong)}
271
+ .rv-hunk-state--rejected{color:var(--danger-strong)}
272
+ .rv-btn--accept{background:var(--success-soft);border-color:var(--success-border);color:var(--success-strong)}
273
+ .rv-btn--accept:hover:not(:disabled){background:var(--success);border-color:var(--success);color:#fff}
274
+ .rv-lines{font-family:var(--mono);font-size:12px;line-height:1.7}
275
+ .rv-line{display:grid;grid-template-columns:46px 46px 18px minmax(0,1fr);align-items:baseline}
276
+ .rv-line-num{padding-right:9px;text-align:right;font-size:10.5px;color:#AAB3C7;user-select:none}
277
+ .rv-line-sign{text-align:center;font-weight:700;color:#AAB3C7;user-select:none}
278
+ .rv-line-code{padding:0 12px 0 4px;white-space:pre-wrap;overflow-wrap:anywhere}
279
+ .rv-line--insert{background:#EDFAF2}
280
+ .rv-line--insert .rv-line-sign{color:var(--success)}
281
+ .rv-line--delete{background:#FDF1F1}
282
+ .rv-line--delete .rv-line-sign{color:var(--danger)}
283
+ .rv-line--gap{grid-template-columns:110px minmax(0,1fr);background:var(--bg-rail);border-block:1px solid var(--border);color:var(--text-subtle);font-size:11px;padding:3px 0}
284
+ .rv-line--gap span:first-child{text-align:center}
285
+ .rv-word{border-radius:3px;padding:0 2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}
286
+ .rv-word--ins{background:#C4EFD5;color:#0A5330}
287
+ .rv-word--del{background:#FACFCF;color:#851B1B}
288
+ .rv-hint{display:flex;align-items:center;gap:9px;margin-bottom:14px;padding:10px 14px;border-radius:10px;background:var(--teal-soft);border:1px solid var(--teal-border);color:var(--teal-strong);font-size:12.5px;font-weight:500}
289
+ .rv-hint svg{flex:none}
290
+
291
+ /* ── Feedback ── */
292
+ .rv-banner{display:flex;align-items:flex-start;gap:10px;padding:12px 15px;border-radius:11px;font-size:13px;line-height:1.5;margin-top:16px}
293
+ .rv-banner--error{background:var(--danger-soft);border:1px solid var(--danger-border);color:var(--danger-strong)}
294
+ .rv-banner--warn{background:var(--warning-soft);border:1px solid var(--warning-border);color:var(--warning-strong)}
295
+ .rv-banner svg{flex:none;margin-top:1px}
296
+ .rv-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;text-align:center;padding:70px 24px;color:var(--text-muted)}
297
+ .rv-empty-icon{display:inline-flex;align-items:center;justify-content:center;width:58px;height:58px;border-radius:17px;background:#fff;border:1px solid var(--border);color:var(--navy);margin-bottom:4px;box-shadow:var(--shadow-xs)}
298
+ .rv-empty h2{font-size:17px;font-weight:700;color:var(--text-primary)}
299
+ .rv-empty p{max-width:44ch;font-size:13.5px}
300
+ .rv-empty code,.rv-code{font-family:var(--mono);font-size:12px;background:var(--bg-section);border-radius:5px;padding:2px 7px;color:var(--text-secondary)}
301
+ .rv-skeleton{height:74px;border-radius:12px;margin-bottom:12px;background:linear-gradient(90deg,var(--bg-section) 25%,#F8FAFD 50%,var(--bg-section) 75%);background-size:200% 100%;animation:rv-shimmer 1.2s infinite}
302
+ @keyframes rv-shimmer{to{background-position:-200% 0}}
303
+
304
+ .rv-done{display:flex;flex-direction:column;align-items:center;text-align:center;padding:70px 24px}
305
+ .rv-done-mark{display:inline-flex;align-items:center;justify-content:center;width:66px;height:66px;border-radius:50%;background:var(--success-soft);color:var(--success);border:1px solid var(--success-border);margin-bottom:18px}
306
+ .rv-done-mark--neutral{background:#fff;color:var(--text-subtle);border-color:var(--border)}
307
+ .rv-done h1{font-size:26px;font-weight:700;letter-spacing:-.02em}
308
+ .rv-done p{margin-top:8px;font-size:14.5px;color:var(--text-secondary);max-width:46ch}
309
+ .rv-done-actions{display:flex;gap:10px;margin-top:26px;flex-wrap:wrap;justify-content:center}
310
+ .rv-done-list{margin-top:26px;width:100%;max-width:520px;text-align:left}
311
+
312
+ /* ── Overlay & toast ── */
313
+ .rv-overlay{position:fixed;inset:0;z-index:120;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(6,18,45,.45);backdrop-filter:blur(2px)}
314
+ .rv-modal{width:100%;max-width:430px;background:#fff;border-radius:16px;padding:22px;box-shadow:var(--shadow-lg)}
315
+ .rv-modal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
316
+ .rv-modal-head h2{font-size:16.5px;font-weight:700}
317
+ .rv-keys{display:flex;flex-direction:column;gap:10px}
318
+ .rv-keys>div{display:flex;align-items:center;justify-content:space-between;gap:16px}
319
+ .rv-keys dt{display:flex;gap:4px}
320
+ .rv-keys dd{font-size:13px;color:var(--text-secondary);text-align:right}
321
+ .rv-modal-note{margin-top:16px;padding-top:13px;border-top:1px solid var(--border);font-size:12px;color:var(--text-muted)}
322
+ .rv-toast{position:fixed;left:50%;bottom:26px;z-index:200;display:flex;align-items:center;gap:10px;max-width:min(560px,calc(100vw - 40px));padding:12px 16px;border-radius:11px;background:#0E1A31;color:#fff;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);opacity:0;transform:translate(-50%,14px);pointer-events:none;transition:opacity .18s,transform .18s}
323
+ .rv-toast.is-visible{opacity:1;transform:translate(-50%,0)}
324
+ .rv-toast--success{background:#12603C}
325
+ .rv-toast--error{background:#8E1D1D}
326
+ .rv-toast svg{flex:none}
327
+
328
+ /* ── Review center shell ── */
329
+ .rv-shell{display:grid;grid-template-columns:260px minmax(0,1fr);height:100%;min-height:0;background:#fff}
330
+ .rv-sidebar{display:flex;flex-direction:column;min-height:0;padding:27px 12px 22px;border-right:1px solid var(--border);background:#fff}
331
+ .rv-sidebar-logo{display:flex;align-items:center;height:30px;padding:0 5px;margin-bottom:40px;border:0;background:transparent;cursor:pointer}
332
+ .rv-sidebar-logo img{display:block;width:98px;height:auto}
333
+ .rv-sidebar-label{padding:0 5px 10px;font-size:11px;font-weight:600;letter-spacing:.035em;text-transform:uppercase;color:var(--text-muted)}
334
+ .rv-nav{display:flex;flex-direction:column;gap:4px}
335
+ .rv-nav-item{display:flex;align-items:center;gap:13px;width:100%;height:42px;padding:0 14px;border:0;border-radius:8px;background:transparent;color:var(--text-primary);font:inherit;font-size:14px;text-align:left;cursor:pointer}
336
+ .rv-nav-item svg{color:var(--text-secondary);flex:none}
337
+ .rv-nav-item:hover{background:var(--navy-lighter)}
338
+ .rv-nav-item.is-active{background:#EEF3FD;color:#062D91;font-weight:600}
339
+ .rv-nav-item.is-active svg{color:#2876E8}
340
+ .rv-sidebar-safe{display:flex;align-items:center;gap:13px;margin-top:auto;padding:21px 7px 0;border-top:1px solid var(--border);color:var(--text-secondary)}
341
+ .rv-sidebar-safe>svg{width:28px;height:28px;color:var(--text-primary);flex:none}
342
+ .rv-sidebar-safe strong,.rv-sidebar-safe span{display:block;font-size:12.5px;line-height:1.45}
343
+ .rv-sidebar-safe strong{font-weight:500}
344
+ .rv-sidebar-safe span{color:var(--text-muted)}
345
+ .rv-main{display:flex;flex-direction:column;min-width:0;min-height:0;background:#fff}
346
+
347
+ .rv-appbar{height:64px;min-height:64px;padding:0 24px;background:#fff}
348
+ .rv-appbar-search{position:relative;width:min(520px,46vw);margin-left:auto}
349
+ .rv-appbar-search svg{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-secondary);pointer-events:none}
350
+ .rv-appbar-search input{width:100%;height:42px;padding:0 14px 0 43px;border:1px solid var(--border-input);border-radius:9px;background:#fff;color:var(--text-primary);font-size:14px;outline:0;transition:border-color .14s,box-shadow .14s}
351
+ .rv-appbar-search input::placeholder{color:var(--text-muted)}
352
+ .rv-appbar-search input:focus{border-color:#7AA7F8;box-shadow:0 0 0 3px rgba(40,118,232,.1)}
353
+ .rv-appbar .rv-icon-btn{width:42px;height:42px;margin-left:10px}
354
+ .rv-back{height:auto;padding:6px 0;border:0;background:transparent;border-radius:0;font-size:14px;font-weight:500}
355
+ .rv-back:hover{border-color:transparent;background:transparent;color:var(--navy)}
356
+
357
+ .rv-scroll{background:linear-gradient(135deg,#fff 0%,#fbfcff 55%,#f8faff 100%)}
358
+ .rv-page{max-width:1200px;padding:30px 32px 58px}
359
+ .rv-page--wide{max-width:1200px}
360
+ .rv-page-head{display:flex;align-items:flex-start;gap:24px;margin-bottom:25px}
361
+ .rv-page-head-copy{min-width:0}
362
+ .rv-page-head .rv-btn{margin-left:auto}
363
+ .rv-page h1{font-size:25px;line-height:1.22;letter-spacing:-.025em}
364
+ .rv-page-sub{margin-top:5px;max-width:none;font-size:13.5px}
365
+ .rv-eyebrow{font-size:13px;font-weight:400;letter-spacing:0;text-transform:capitalize;margin-bottom:7px;color:var(--text-muted)}
366
+ .rv-section{margin-top:24px}
367
+ .rv-section-title{font-size:15px;letter-spacing:0;text-transform:none;color:var(--text-primary);margin-bottom:10px}
368
+ .rv-count{display:inline-flex;align-items:center;justify-content:center;min-width:31px;height:24px;padding:0 9px;border-radius:999px;background:var(--bg-section);color:var(--text-secondary);font-size:12px;font-weight:600}
369
+
370
+ .rv-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;margin-bottom:25px}
371
+ .rv-metric{display:flex;align-items:center;gap:20px;min-height:116px;padding:22px;border:1px solid var(--border-input);border-radius:9px;background:rgba(255,255,255,.86)}
372
+ .rv-metric-icon{display:flex;align-items:center;justify-content:center;width:60px;height:60px;border-radius:9px;background:#EAF2FF;color:#2876E8;flex:none}
373
+ .rv-metric-icon svg{width:28px;height:28px}
374
+ .rv-metric:nth-child(2) .rv-metric-icon{background:#E7F8F7;color:#0AA59F}
375
+ .rv-metric:nth-child(3) .rv-metric-icon{background:#FFF4E6;color:#C77900}
376
+ .rv-metric-copy{display:flex;flex-direction:column;min-width:0}
377
+ .rv-metric-label{font-size:12.5px;font-weight:600;color:var(--text-secondary)}
378
+ .rv-metric-value{font-size:25px;font-weight:600;line-height:1.2;color:var(--text-primary);letter-spacing:-.02em}
379
+ .rv-metric-note{font-size:12px;color:var(--text-muted)}
380
+
381
+ .rv-queue-head{display:flex;align-items:center;gap:10px;margin-bottom:7px}
382
+ .rv-queue-head h2{font-size:15px;line-height:1.3}
383
+ .rv-segmented{padding:0;background:transparent;border-radius:0;margin-bottom:2px}
384
+ .rv-segmented button{padding:7px 13px;border:1px solid transparent;border-radius:7px;font-size:12.5px}
385
+ .rv-segmented button.is-active{border-color:var(--border-input);background:#fff;box-shadow:var(--shadow-xs)}
386
+ .rv-cards{gap:10px}
387
+ .rv-card{border-radius:8px;padding:17px 20px 14px;box-shadow:none}
388
+ button.rv-card:hover{transform:none;border-color:#AFC5EB;box-shadow:0 4px 14px rgba(2,45,120,.07)}
389
+ .rv-card-top{margin-bottom:11px}
390
+ .rv-pill{height:23px;font-size:10.5px;text-transform:uppercase}
391
+ .rv-card-meta{font-size:12px}
392
+ .rv-card h2{font-size:17px}
393
+ .rv-card-why{margin-top:3px;font-size:13px}
394
+ .rv-card-foot{min-height:44px;margin-top:12px;padding-top:11px;border-top:1px solid var(--border);flex-wrap:wrap}
395
+ .rv-card-fact{display:inline-flex;align-items:center;gap:7px;color:var(--text-secondary);font-size:12.5px}
396
+ .rv-card-fact svg{color:var(--text-secondary)}
397
+ .rv-card-fact--ok{margin-left:8px;padding-left:22px;border-left:1px solid var(--border);color:var(--success-strong)}
398
+ .rv-card-fact--ok svg{color:var(--success-strong)}
399
+ .rv-card-cta{height:40px;padding:0 18px;border-radius:8px;background:var(--navy);color:#fff}
400
+ .rv-card-cta:hover{background:var(--navy-hover)}
401
+
402
+ .rv-activity{margin-top:18px;border:1px solid var(--border-input);border-radius:8px;background:#fff;overflow:hidden}
403
+ .rv-activity h2{padding:14px 18px 5px;font-size:15px}
404
+ .rv-activity-row{display:grid;grid-template-columns:minmax(0,1fr) 190px 120px;gap:20px;align-items:center;min-height:50px;padding:0 18px;border-top:1px solid var(--border);font-size:13px}
405
+ .rv-activity-row--head{min-height:34px;border-top:0;color:var(--text-secondary);font-size:12px;font-weight:600}
406
+ .rv-activity .rv-pill{justify-self:start}
407
+
408
+ .rv-overview-head{margin-bottom:24px}
409
+ .rv-overview-head .rv-meta-row{margin-top:14px}
410
+ .rv-overview-grid{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:24px;align-items:start}
411
+ .rv-overview-grid .rv-section{margin-top:0}
412
+ .rv-rows{border-radius:9px;border-color:var(--border-input)}
413
+ .rv-row{min-height:69px;padding:12px 20px;gap:16px}
414
+ .rv-row-icon{width:46px;height:46px;border-radius:9px;background:#EEF3FD}
415
+ .rv-row-title{font-size:15px}
416
+ .rv-row-path{font-family:inherit;font-size:12.5px;color:var(--text-muted);margin-top:0}
417
+ .rv-chip{height:25px;border-radius:999px;background:#FFF3DE;color:#A55E00}
418
+ .rv-summary{position:sticky;top:24px;border:1px solid var(--border-input);border-radius:9px;background:#fff;padding:18px}
419
+ .rv-summary h2{font-size:16px;margin-bottom:20px}
420
+ .rv-summary-why{display:flex;gap:14px;margin-bottom:18px}
421
+ .rv-summary-why .rv-why-icon{width:40px;height:40px;margin:0}
422
+ .rv-summary-label{display:block;font-size:12.5px;color:var(--text-secondary);margin-bottom:5px}
423
+ .rv-summary-why strong{display:block;font-size:13.5px;font-weight:600;line-height:1.45}
424
+ .rv-summary-stats{display:flex;flex-direction:column;gap:11px;padding:4px 0 17px}
425
+ .rv-summary-stat{display:flex;justify-content:space-between;gap:16px;color:var(--text-secondary);font-size:13px}
426
+ .rv-summary-stat strong{color:var(--text-primary);font-weight:500}
427
+ .rv-summary-stat .is-success{color:var(--success-strong);font-weight:600}
428
+ .rv-summary-support{border-block:1px solid var(--border);margin-bottom:15px}
429
+ .rv-summary-support summary{display:flex;align-items:center;gap:8px;min-height:50px;cursor:pointer;list-style:none;font-size:13px}
430
+ .rv-summary-support summary::-webkit-details-marker{display:none}
431
+ .rv-summary-support summary svg{margin-left:auto}
432
+ .rv-summary-support[open] summary svg{transform:rotate(90deg)}
433
+ .rv-summary-support .rv-rows{margin:0 -8px 10px;border:0}
434
+ .rv-summary-support .rv-row{min-height:54px;padding:8px}
435
+ .rv-summary-support .rv-row-icon{width:34px;height:34px}
436
+ .rv-summary-note{margin-bottom:16px;text-align:center;font-size:11.5px;color:var(--text-muted)}
437
+ .rv-summary-actions{display:flex;flex-direction:column;gap:9px}
438
+ .rv-summary-actions .rv-btn{width:100%}
439
+ .rv-summary-actions .rv-btn--quiet{color:var(--danger)}
440
+ .rv-safe-note{display:flex;align-items:center;gap:10px;margin-top:13px;padding:0 5px;color:var(--text-muted);font-size:12.5px}
441
+ .rv-safe-note svg{flex:none}
442
+ .rv-why{margin-top:0}
443
+
444
+ @media(max-width:960px){
445
+ .rv-shell{grid-template-columns:200px minmax(0,1fr)}
446
+ .rv-overview-grid{grid-template-columns:minmax(0,1fr) 300px}
447
+ .rv-page{padding-inline:24px}
448
+ .rv-metric{gap:13px;padding:17px}.rv-metric-icon{width:48px;height:48px}
449
+ }
450
+ @media(max-width:760px){
451
+ .rv-shell{display:block;overflow:hidden}.rv-sidebar{display:none}.rv-main{height:100%}
452
+ .rv-page{padding:24px 16px 56px}.rv-appbar,.rv-focus-bar,.rv-focus-foot{padding:0 14px}
453
+ .rv-appbar-search{width:100%}.rv-metrics{grid-template-columns:1fr;gap:10px}.rv-metric{min-height:88px}
454
+ .rv-overview-grid{grid-template-columns:1fr}.rv-summary{position:static}.rv-counter,.rv-foot-count{display:none}
455
+ .rv-activity-row{grid-template-columns:minmax(0,1fr) 100px}.rv-activity-row>*:last-child{display:none}
456
+ }
457
+ `;
458
+ }
459
+ /* ── Client behaviour ─────────────────────────────────────────────────── */
460
+ function shellScript(token) {
461
+ const iconLiterals = JSON.stringify({
462
+ page: icon('page', 16),
463
+ navigation: icon('navigation', 16),
464
+ configuration: icon('configuration', 16),
465
+ evidence: icon('evidence', 16),
466
+ asset: icon('asset', 16),
467
+ check: icon('check', 15),
468
+ checkCircle: icon('check-circle', 15),
469
+ checkBig: icon('check', 30),
470
+ x: icon('x', 15),
471
+ refresh: icon('refresh', 15),
472
+ clock: icon('clock', 14),
473
+ columns: icon('columns', 15),
474
+ code: icon('code', 15),
475
+ alert: icon('alert', 16),
476
+ shield: icon('shield', 16),
477
+ keyboard: icon('keyboard', 14),
478
+ inbox: icon('inbox', 26),
479
+ arrowLeft: icon('arrowLeft', 15),
480
+ arrowRight: icon('arrowRight', 15),
481
+ chevronRight: icon('chevronRight', 14),
482
+ chevronDown: icon('chevronDown', 14),
483
+ sliders: icon('sliders', 15),
484
+ sparkle: icon('sparkle', 15),
485
+ search: icon('search', 18),
486
+ activity: icon('activity', 18),
487
+ });
488
+ return `
489
+ const TOKEN = ${JSON.stringify(token)};
490
+ const ICON = ${iconLiterals};
491
+ const ACTIONABLE = ['awaiting-review', 'partially-applied', 'conflicted'];
492
+
493
+ const app = document.getElementById('app');
494
+ let runs = [];
495
+ let run = null;
496
+ let filter = 'pending';
497
+ let query = '';
498
+ let prefs = loadPrefs();
499
+ let busy = false;
500
+
501
+ function loadPrefs() {
502
+ try {
503
+ const saved = JSON.parse(localStorage.getItem('doxloop-review-prefs') || '{}');
504
+ return { layout: saved.layout === 'unified' ? 'unified' : 'split', onlyChanges: !!saved.onlyChanges, source: !!saved.source };
505
+ } catch (error) { return { layout: 'split', onlyChanges: false, source: false }; }
506
+ }
507
+ function savePrefs() { localStorage.setItem('doxloop-review-prefs', JSON.stringify(prefs)); }
508
+
509
+ function esc(value) {
510
+ return String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[c]));
511
+ }
512
+
513
+ async function request(path, options) {
514
+ const config = options || {};
515
+ const response = await fetch(path, Object.assign({}, config, {
516
+ headers: Object.assign({ 'content-type': 'application/json', 'x-doxloop-review-token': TOKEN }, config.headers || {}),
517
+ }));
518
+ const value = await response.json();
519
+ if (!response.ok) throw new Error(value.error || 'The review action failed.');
520
+ return value;
521
+ }
522
+
523
+ /* ── Model helpers ── */
524
+
525
+ // Data files carry site configuration, never prose. Older proposals classified
526
+ // anything under the content directory as a page, so the guard stays here to
527
+ // keep those runs out of the page-by-page reading flow.
528
+ const DATA_FILE = /\\.(json|ya?ml|toml|ini|csv|lock)$/i;
529
+ function isPage(change) {
530
+ return change.category === 'page' && !change.binary && !DATA_FILE.test(change.path);
531
+ }
532
+ function pagesOf(item) { return item.changes.filter(isPage); }
533
+ function supportingOf(item) { return item.changes.filter((change) => !isPage(change)); }
534
+ function pending(item) { return ACTIONABLE.indexOf(item.status) !== -1; }
535
+ function hunksOf(list) { return list.reduce((all, change) => all.concat(change.hunks), []); }
536
+ function settled(change) { return change.hunks.every((hunk) => hunk.acceptedAt || hunk.rejectedAt); }
537
+ function accepted(change) { return change.hunks.length > 0 && change.hunks.every((hunk) => hunk.acceptedAt); }
538
+ function openCount(change) { return change.hunks.filter((hunk) => !hunk.acceptedAt && !hunk.rejectedAt).length; }
539
+
540
+ function relativeTime(value) {
541
+ const seconds = Math.max(0, (Date.now() - Date.parse(value)) / 1000);
542
+ if (seconds < 60) return 'just now';
543
+ if (seconds < 3600) return Math.floor(seconds / 60) + ' min ago';
544
+ if (seconds < 86400) return Math.floor(seconds / 3600) + ' h ago';
545
+ return Math.floor(seconds / 86400) + ' d ago';
546
+ }
547
+ function label(value) { return String(value).replace(/-/g, ' '); }
548
+ function plural(count, one, many) { return count + ' ' + (count === 1 ? one : many); }
549
+
550
+ function generating(item) { return item.status === 'generating'; }
551
+
552
+ /** Machine wording like "3 proposed file changes" reads badly to a writer. */
553
+ function headline(item) {
554
+ const pages = pagesOf(item).length;
555
+ if (generating(item)) return 'Drafting a documentation update';
556
+ if (item.status === 'failed') return 'This proposal could not be generated';
557
+ if (pages === 0) return 'Supporting files need an update';
558
+ return plural(pages, 'page needs', 'pages need') + ' an update';
559
+ }
560
+
561
+ /** "3 changed source files: M\\ta.js, M\\tb.js" -> "Because a.js and 2 other files changed". */
562
+ function reason(item) {
563
+ const match = /^(\\d+) changed source files?: (.*)$/.exec(item.sourceSummary || '');
564
+ if (!match) return item.sourceSummary || '';
565
+ // The listed paths are truncated after twelve entries, so the count in the
566
+ // summary is the total; counting the visible entries would under-report it.
567
+ const total = Number(match[1]);
568
+ // Git status letters are separated from the path by a tab, not a space.
569
+ const first = match[2].split(', ')[0].replace(/^[A-Z]\\s+/, '').trim();
570
+ if (total <= 1) return first + ' changed';
571
+ return first + ' and ' + plural(total - 1, 'other file', 'other files') + ' changed';
572
+ }
573
+
574
+ /* ── Routing ── */
575
+
576
+ function go(path, replace) {
577
+ if (replace) history.replaceState({}, '', path); else history.pushState({}, '', path);
578
+ void render();
579
+ }
580
+ window.addEventListener('popstate', () => void render());
581
+
582
+ let pollTimer;
583
+ /** Re-read a run that is still being authored until it settles. */
584
+ function pollRun() {
585
+ clearTimeout(pollTimer);
586
+ pollTimer = setTimeout(async () => {
587
+ if (!run || !generating(run)) return;
588
+ try {
589
+ const next = await request('/api/runs/' + run.id);
590
+ if (!run || next.id !== run.id) return;
591
+ run = next;
592
+ if (generating(run)) { pollRun(); return; }
593
+ runs = await request('/api/runs').catch(() => runs);
594
+ await render();
595
+ } catch (error) { pollRun(); }
596
+ }, 4000);
597
+ }
598
+
599
+ async function render() {
600
+ const path = location.pathname;
601
+ clearTimeout(pollTimer);
602
+ closeMenus();
603
+ if (path === '/' || path === '') { await screenList(); return; }
604
+ const match = /^\\/runs\\/([a-z0-9-]+)(?:\\/pages\\/(\\d+)|\\/files\\/(change-\\d+)|\\/(done))?\\/?$/.exec(path);
605
+ if (!match) { go('/', true); return; }
606
+ try {
607
+ if (!run || run.id !== match[1]) run = await request('/api/runs/' + match[1]);
608
+ } catch (error) { showToast(error.message, 'error'); go('/', true); return; }
609
+ if (match[4]) { screenDone(); return; }
610
+ if (match[3]) { screenFile(match[3]); return; }
611
+ if (match[2]) { screenFocus(Number(match[2])); return; }
612
+ screenOverview();
613
+ }
614
+
615
+ /* ── Screen 1: proposals ── */
616
+
617
+ function sidebar() {
618
+ return '<aside class="rv-sidebar">' +
619
+ '<button class="rv-sidebar-logo" data-home aria-label="Doxloop review home"><img src="/brand/logo.png" alt="Doxloop"></button>' +
620
+ '<div class="rv-sidebar-label">Review center</div><nav class="rv-nav" aria-label="Review center">' +
621
+ '<button class="rv-nav-item is-active" data-home>' + ICON.page + '<span>Proposals</span></button>' +
622
+ '<button class="rv-nav-item" data-activity>' + ICON.activity + '<span>Activity</span></button>' +
623
+ '<button class="rv-nav-item" data-shortcuts>' + ICON.keyboard + '<span>Keyboard shortcuts</span></button>' +
624
+ '</nav><div class="rv-sidebar-safe">' + ICON.shield + '<span><strong>Safe review mode</strong><span>Files stay unchanged</span></span></div></aside>';
625
+ }
626
+
627
+ function shell(inner) {
628
+ return '<div class="rv-shell">' + sidebar() + '<main class="rv-main">' + inner + '</main></div>';
629
+ }
630
+
631
+ function appbar(inner) {
632
+ return '<header class="rv-appbar">' + (inner || '<span class="rv-appbar-spacer"></span>') + '</header>';
633
+ }
634
+
635
+ async function screenList() {
636
+ run = null;
637
+ app.dataset.focusKey = '';
638
+ app.innerHTML = shell(appbar('<label class="rv-appbar-search">' + ICON.search +
639
+ '<input id="proposal-search" type="search" placeholder="Search proposals..." aria-label="Search proposals"></label>' +
640
+ '<button class="rv-icon-btn" id="refresh" title="Refresh" aria-label="Refresh">' + ICON.refresh + '</button>') +
641
+ '<div class="rv-scroll"><div class="rv-page"><div class="rv-page-head">' +
642
+ '<div class="rv-page-head-copy"><h1>Proposals</h1><p class="rv-page-sub">Review documentation updates drafted from product changes. Nothing is written until you approve.</p></div>' +
643
+ '<button class="rv-btn rv-btn--ghost rv-btn--sm" id="refresh-page">' + ICON.refresh + 'Refresh</button>' +
644
+ '</div><div id="list-controls"><div class="rv-skeleton"></div></div><div id="list"><div class="rv-skeleton"></div></div><div id="activity"></div></div></div>');
645
+ document.getElementById('refresh').addEventListener('click', reload);
646
+ document.getElementById('refresh-page').addEventListener('click', reload);
647
+ document.getElementById('proposal-search').addEventListener('input', (event) => { query = event.target.value.trim().toLowerCase(); paintList(); });
648
+ try { runs = await request('/api/runs'); } catch (error) { showToast(error.message, 'error'); return; }
649
+ if (runs.length > 0 && runs.filter(pending).length === 0) filter = 'all';
650
+ paintList();
651
+ if (location.hash === '#activity') requestAnimationFrame(() => document.getElementById('activity')?.scrollIntoView({ behavior: 'smooth' }));
652
+ }
653
+
654
+ function paintList() {
655
+ const controls = document.getElementById('list-controls');
656
+ if (!controls) return;
657
+ const actionable = runs.filter(pending);
658
+ const pages = actionable.reduce((count, item) => count + pagesOf(item).length, 0);
659
+ const latest = runs[0];
660
+ controls.innerHTML = '<div class="rv-metrics">' +
661
+ metricHtml(ICON.page, 'Needs review', actionable.length, 'Awaiting your decision') +
662
+ metricHtml(ICON.navigation, 'Pages impacted', pages, 'Across open proposals') +
663
+ metricHtml(ICON.clock, 'Last generated', latest ? relativeTime(latest.createdAt).replace(/ ago$/, '') : '—', latest ? label(latest.trigger) + ' sync' : 'No proposals yet') +
664
+ '</div>' +
665
+ '<div class="rv-queue-head"><h2>Review queue</h2><span class="rv-count">' + actionable.length + '</span></div>' +
666
+ (runs.length === 0 ? '' : '<div class="rv-segmented" role="group" aria-label="Filter proposals">' +
667
+ '<button type="button" data-filter="pending"' + (filter === 'pending' ? ' class="is-active"' : '') + '>Needs review</button>' +
668
+ '<button type="button" data-filter="all"' + (filter === 'all' ? ' class="is-active"' : '') + '>All</button></div>');
669
+ controls.querySelectorAll('[data-filter]').forEach((button) => {
670
+ button.addEventListener('click', () => { filter = button.dataset.filter; paintList(); });
671
+ });
672
+
673
+ const list = document.getElementById('list');
674
+ const visible = runs.filter((item) => (filter === 'all' || pending(item)) && (!query || (headline(item) + ' ' + reason(item) + ' ' + label(item.status)).toLowerCase().includes(query)));
675
+ if (runs.length === 0) {
676
+ list.innerHTML = '<div class="rv-empty"><span class="rv-empty-icon">' + ICON.inbox + '</span>' +
677
+ '<h2>No proposals yet</h2><p>Run <code>doxloop sync now</code> to check your documentation against the product and draft an update.</p></div>';
678
+ paintActivity(); return;
679
+ }
680
+ if (visible.length === 0) {
681
+ list.innerHTML = '<div class="rv-empty"><span class="rv-empty-icon">' + ICON.checkCircle + '</span>' +
682
+ (query ? '<h2>No matching proposals</h2><p>Try a different search term or clear the search.</p>' : '<h2>Nothing is waiting for you</h2><p>Every proposal has been reviewed. Switch to <strong>All</strong> to look back over the history.</p>') + '</div>';
683
+ paintActivity(); return;
684
+ }
685
+ list.innerHTML = '<div class="rv-cards">' + visible.map(cardHtml).join('') + '</div>';
686
+ list.querySelectorAll('[data-run]').forEach((card) => {
687
+ card.addEventListener('click', () => go('/runs/' + card.dataset.run));
688
+ });
689
+ paintActivity();
690
+ }
691
+
692
+ function metricHtml(iconSvg, title, value, note) {
693
+ return '<div class="rv-metric"><span class="rv-metric-icon">' + iconSvg + '</span><span class="rv-metric-copy">' +
694
+ '<span class="rv-metric-label">' + esc(title) + '</span><strong class="rv-metric-value">' + esc(value) + '</strong><span class="rv-metric-note">' + esc(note) + '</span></span></div>';
695
+ }
696
+
697
+ function paintActivity() {
698
+ const target = document.getElementById('activity');
699
+ if (!target) return;
700
+ const history = runs.filter((item) => !pending(item) && !generating(item)).slice(0, 4);
701
+ if (history.length === 0) { target.innerHTML = ''; return; }
702
+ target.innerHTML = '<section class="rv-activity"><h2>Recent activity</h2>' +
703
+ '<div class="rv-activity-row rv-activity-row--head"><span>Proposal</span><span>Status</span><span>Created</span></div>' +
704
+ history.map((item) => '<button class="rv-activity-row" data-run="' + esc(item.id) + '" style="width:100%;border-inline:0;border-bottom:0;background:#fff;text-align:left;font:inherit;cursor:pointer">' +
705
+ '<span>' + esc(headline(item).replace(' need an update', ' updated').replace(' needs an update', ' updated')) + '</span>' +
706
+ '<span class="rv-pill rv-pill--' + esc(item.status) + '">' + esc(label(item.status)) + '</span><span>' + esc(relativeTime(item.createdAt)) + '</span></button>').join('') + '</section>';
707
+ target.querySelectorAll('[data-run]').forEach((row) => row.addEventListener('click', () => go('/runs/' + row.dataset.run)));
708
+ }
709
+
710
+ function cardHtml(item) {
711
+ const done = hunksOf(item.changes).filter((hunk) => hunk.acceptedAt).length;
712
+ const open = pending(item);
713
+ const pages = pagesOf(item).length;
714
+ const navigation = item.changes.filter((change) => change.category === 'navigation').length;
715
+ const configuration = item.changes.filter((change) => change.category === 'configuration').length;
716
+ const detail = generating(item)
717
+ ? item.summary + ' · ' + reason(item)
718
+ : open
719
+ ? reason(item)
720
+ : item.status === 'applied' ? plural(done, 'change was', 'changes were') + ' applied to your documentation'
721
+ : item.status === 'rejected' ? 'Rejected — nothing was written'
722
+ : item.error ? item.error : reason(item);
723
+ return '<button class="rv-card" data-run="' + esc(item.id) + '">' +
724
+ '<span class="rv-card-top"><span class="rv-dot rv-dot--' + esc(item.status) + '"></span>' +
725
+ '<span class="rv-pill rv-pill--' + esc(item.status) + '">' + esc(label(item.status)) + '</span>' +
726
+ '<span class="rv-card-meta">' + esc(label(item.trigger)) + ' · ' + esc(relativeTime(item.createdAt)) + '</span></span>' +
727
+ '<h2>' + esc(headline(item)) + '</h2>' +
728
+ '<p class="rv-card-why">' + esc(detail) + '</p>' +
729
+ '<span class="rv-card-foot">' +
730
+ '<span class="rv-card-fact">' + ICON.page + plural(pages, 'page', 'pages') + '</span>' +
731
+ (navigation ? '<span class="rv-card-fact">' + ICON.navigation + plural(navigation, 'navigation', 'navigation') + '</span>' : '') +
732
+ (configuration ? '<span class="rv-card-fact">' + ICON.configuration + plural(configuration, 'configuration', 'configurations') + '</span>' : '') +
733
+ (item.validation && item.validation.errors === 0 ? '<span class="rv-card-fact rv-card-fact--ok">' + ICON.checkCircle + 'Validation passed</span>' : '') +
734
+ '<span class="rv-card-cta">' +
735
+ (generating(item) ? '<span class="rv-spinner rv-spinner--sm"></span>In progress' : (open ? 'Review proposal' : 'View proposal') + ICON.arrowRight) +
736
+ '</span></span></button>';
737
+ }
738
+
739
+ /* ── Screen 2: proposal overview ── */
740
+
741
+ function screenOverview() {
742
+ app.dataset.focusKey = '';
743
+ const pages = pagesOf(run);
744
+ const support = supportingOf(run);
745
+ const open = pending(run);
746
+ const nextIndex = firstOpenPage();
747
+ const validation = run.validation;
748
+
749
+ // A run that is still being authored has no changes yet; saying "nothing
750
+ // changed" would be wrong, so the screen reports that it is still working.
751
+ if (generating(run)) {
752
+ app.innerHTML = shell(appbar('<button class="rv-back" data-home>' + ICON.arrowLeft + 'Back to proposals</button><span class="rv-appbar-spacer"></span>' +
753
+ '<button class="rv-icon-btn" data-shortcuts title="Keyboard shortcuts">' + ICON.keyboard + '</button>') +
754
+ '<div class="rv-scroll"><div class="rv-page">' +
755
+ '<div class="rv-page-head"><span class="rv-eyebrow">' + esc(label(run.trigger)) + ' · ' + esc(relativeTime(run.createdAt)) + '</span>' +
756
+ '<h1>' + esc(headline(run)) + '</h1></div>' +
757
+ '<div class="rv-why"><span class="rv-why-icon">' + ICON.sparkle + '</span><span class="rv-why-body">' +
758
+ '<strong>' + esc(reason(run)) + '</strong><code>' + esc(run.sourceSummary || '') + '</code></span></div>' +
759
+ '<div class="rv-working"><span class="rv-spinner"></span><span class="rv-working-body">' +
760
+ '<strong>Doxloop is writing the update</strong>' +
761
+ '<span>' + esc(run.summary) + '. Your documentation stays untouched while this runs, and the pages appear here as soon as the draft is ready.</span>' +
762
+ '</span></div>' +
763
+ '</div></div>');
764
+ pollRun();
765
+ return;
766
+ }
767
+
768
+ const rows = pages.length === 0
769
+ ? '<div class="rv-empty" style="padding:34px 20px"><p>No page content changed in this proposal.</p></div>'
770
+ : '<div class="rv-rows">' + pages.map((change, index) => {
771
+ const state = accepted(change) ? '<span class="rv-chip rv-chip--accepted">' + ICON.check + 'Accepted</span>'
772
+ : settled(change) ? '<span class="rv-chip">Resolved</span>'
773
+ : '<span class="rv-chip rv-chip--pending">' + plural(openCount(change), 'change', 'changes') + '</span>';
774
+ return '<button class="rv-row" data-page="' + (index + 1) + '">' +
775
+ '<span class="rv-row-icon">' + ICON.page + '</span>' +
776
+ '<span class="rv-row-body"><span class="rv-row-title">' + esc(change.title) + '</span>' +
777
+ '<span class="rv-row-path">' + esc(change.path) + '</span></span>' +
778
+ '<span class="rv-row-right">' + state + ICON.chevronRight + '</span></button>';
779
+ }).join('') + '</div>';
780
+
781
+ const supportBlock = support.length === 0 ? '' :
782
+ '<details class="rv-summary-support"><summary><strong>' + plural(support.length, 'supporting file', 'supporting files') + '</strong>' + ICON.chevronRight + '</summary>' +
783
+ '<div class="rv-rows">' +
784
+ support.map((change) => '<button class="rv-row" data-support="' + esc(change.id) + '">' +
785
+ '<span class="rv-row-icon">' + (ICON[change.category] || ICON.configuration) + '</span>' +
786
+ '<span class="rv-row-body"><span class="rv-row-title">' + esc(change.title) + '</span>' +
787
+ '<span class="rv-row-path">' + esc(change.path) + '</span></span>' +
788
+ '<span class="rv-row-right">' + (accepted(change) ? '<span class="rv-chip rv-chip--accepted">' + ICON.check + 'Applied</span>' : ICON.chevronRight) + '</span></button>').join('') +
789
+ '</div></details>';
790
+
791
+ app.innerHTML = shell(appbar('<button class="rv-back" data-home>' + ICON.arrowLeft + 'Back to proposals</button><span class="rv-appbar-spacer"></span>' +
792
+ '<button class="rv-icon-btn" data-shortcuts title="Keyboard shortcuts" aria-label="Keyboard shortcuts">' + ICON.keyboard + '</button>' +
793
+ '<button class="rv-icon-btn" id="refresh" title="Refresh" aria-label="Refresh">' + ICON.refresh + '</button>') +
794
+ '<div class="rv-scroll"><div class="rv-page">' +
795
+ '<div class="rv-overview-head"><span class="rv-eyebrow">' + esc(label(run.trigger)) + ' &nbsp;·&nbsp; ' + esc(relativeTime(run.createdAt)) + '</span>' +
796
+ '<h1>' + esc(headline(run)) + '</h1>' +
797
+ '<div class="rv-meta-row"><span class="rv-pill rv-pill--' + esc(run.status) + '">' + esc(label(run.status)) + '</span>' +
798
+ (validation ? '<span class="rv-sep">·</span>' + (validation.errors > 0
799
+ ? '<span style="color:var(--danger-strong);font-weight:600">' + plural(validation.errors, 'validation error', 'validation errors') + '</span>'
800
+ : '<span class="rv-meta-ok">' + ICON.check + 'Validates cleanly</span>') : '') +
801
+ '</div></div>' +
802
+ (run.error ? '<div class="rv-banner rv-banner--error">' + ICON.alert + '<span>' + esc(run.error) + '</span></div>' : '') +
803
+ '<div class="rv-overview-grid"><div class="rv-section"><div class="rv-section-title">Pages to review <span class="rv-count">' + pages.length + '</span></div>' + rows + '</div>' +
804
+ '<aside><div class="rv-summary"><h2>Proposal summary</h2>' +
805
+ '<div class="rv-summary-why"><span class="rv-why-icon">' + ICON.sparkle + '</span><span><span class="rv-summary-label">Why this proposal</span><strong>' + esc(reason(run)) + '</strong></span></div>' +
806
+ '<div class="rv-summary-stats"><div class="rv-summary-stat"><span>Pages</span><strong>' + pages.length + '</strong></div>' +
807
+ '<div class="rv-summary-stat"><span>Source files</span><strong>' + (String(run.sourceSummary || '').match(/^\d+/) || [support.length])[0] + '</strong></div>' +
808
+ '<div class="rv-summary-stat"><span>Validation</span><strong class="' + (validation && validation.errors === 0 ? 'is-success' : '') + '">' + (validation ? validation.errors === 0 ? 'Passed' : plural(validation.errors, 'error', 'errors') : 'Not run') + '</strong></div></div>' +
809
+ supportBlock +
810
+ (support.length ? '<p class="rv-summary-note">Updated automatically after every page is accepted.</p>' : '') +
811
+ (open ? '<div class="rv-summary-actions">' +
812
+ (nextIndex > 0 ? '<button class="rv-btn rv-btn--primary" id="start">' + (firstOpenPage() === 1 && pages.every((change) => !settled(change)) ? 'Start review' : 'Continue review') + ICON.arrowRight + '</button>' : '<button class="rv-btn rv-btn--primary" id="accept-all-only">Apply supporting updates</button>') +
813
+ (pages.length > 0 ? '<button class="rv-btn rv-btn--ghost" id="accept-all">Accept all</button>' : '') +
814
+ '<button class="rv-btn rv-btn--quiet" id="reject">Reject proposal</button></div>' : '') +
815
+ '</div><div class="rv-safe-note">' + ICON.shield + '<span>Nothing is written until you approve.</span></div></aside></div>' +
816
+ '</div></div>');
817
+
818
+ app.querySelectorAll('[data-page]').forEach((row) => {
819
+ row.addEventListener('click', () => go('/runs/' + run.id + '/pages/' + row.dataset.page));
820
+ });
821
+ app.querySelectorAll('[data-support]').forEach((row) => {
822
+ row.addEventListener('click', () => go('/runs/' + run.id + '/files/' + row.dataset.support));
823
+ });
824
+ const start = document.getElementById('start');
825
+ if (start) start.addEventListener('click', () => go('/runs/' + run.id + '/pages/' + firstOpenPage()));
826
+ const acceptAll = document.getElementById('accept-all') || document.getElementById('accept-all-only');
827
+ if (acceptAll) acceptAll.addEventListener('click', () => void acceptEverything());
828
+ const reject = document.getElementById('reject');
829
+ if (reject) reject.addEventListener('click', () => void rejectRun());
830
+ document.getElementById('refresh').addEventListener('click', reload);
831
+ }
832
+
833
+ function firstOpenPage() {
834
+ const pages = pagesOf(run);
835
+ const index = pages.findIndex((change) => !settled(change));
836
+ return index === -1 ? (pages.length > 0 ? pages.length : 0) : index + 1;
837
+ }
838
+
839
+ /* ── Screen 3: one page at a time ── */
840
+
841
+ function screenFocus(index) {
842
+ const pages = pagesOf(run);
843
+ if (pages.length === 0) { go('/runs/' + run.id, true); return; }
844
+ const position = Math.min(Math.max(1, index), pages.length);
845
+ if (position !== index) { go('/runs/' + run.id + '/pages/' + position, true); return; }
846
+ const change = pages[position - 1];
847
+ const open = pending(run) && !settled(change);
848
+ const key = [run.id, change.id, prefs.source ? 'src' : 'doc', change.hunks.filter((hunk) => hunk.acceptedAt).length].join('|');
849
+
850
+ if (app.dataset.focusKey !== key) {
851
+ app.dataset.focusKey = key;
852
+ // One bar above the comparison: where you are, which page, how to view it.
853
+ app.innerHTML = shell(appbar(
854
+ '<button class="rv-back" data-overview>' + ICON.arrowLeft + 'Proposal</button>' +
855
+ '<span class="rv-appbar-divider"></span>' +
856
+ '<span class="rv-menu-wrap"><button class="rv-title-btn" data-menu="pages" aria-haspopup="true">' +
857
+ '<strong>' + esc(change.title) + '</strong>' + ICON.chevronDown + '</button></span>' +
858
+ '<span class="rv-counter">Page ' + position + ' of ' + pages.length + '</span>' +
859
+ '<span class="rv-appbar-spacer"></span>' +
860
+ '<span class="rv-menu-wrap"><button class="rv-btn rv-btn--ghost rv-btn--sm" data-menu="view" aria-haspopup="true">' +
861
+ ICON.sliders + 'View' + ICON.chevronDown + '</button></span>' +
862
+ '<button class="rv-icon-btn" data-shortcuts title="Keyboard shortcuts" aria-label="Keyboard shortcuts">' + ICON.keyboard + '</button>') +
863
+ '<div class="rv-focus">' +
864
+ '<div class="rv-stage">' +
865
+ (prefs.source
866
+ ? '<div class="rv-source" id="source"><div class="rv-skeleton"></div></div>'
867
+ : '<iframe id="preview" title="The current page beside the proposed update" src="' + previewSrc(change) + '"></iframe>') +
868
+ '</div>' +
869
+ '<div class="rv-focus-foot" id="focus-foot"></div></div>');
870
+ if (prefs.source) void paintSource(change);
871
+ }
872
+ paintFocusFoot(change, position, pages, open);
873
+ wireFocus(change, position, pages);
874
+ }
875
+
876
+ function previewSrc(change) {
877
+ return '/preview/' + run.id + '/' + change.id +
878
+ '?layout=' + prefs.layout + (prefs.onlyChanges ? '&only=1' : '') + '&v=' + Date.now();
879
+ }
880
+
881
+ function paintFocusFoot(change, position, pages, open) {
882
+ const foot = document.getElementById('focus-foot');
883
+ if (!foot) return;
884
+ const dots = pages.map((entry, index) => '<span class="rv-pdot' +
885
+ (index + 1 === position ? ' rv-pdot--current' : accepted(entry) ? ' rv-pdot--done' : '') + '"></span>').join('');
886
+ const count = openCount(change);
887
+ foot.innerHTML = '<span class="rv-progress-dots">' + dots + '</span>' +
888
+ '<span class="rv-foot-count">' +
889
+ (count > 0
890
+ ? '<button class="rv-link" data-source>' + plural(count, 'change', 'changes') + ' on this page</button>'
891
+ : '<button class="rv-link" data-source>See the source diff</button>') +
892
+ '</span>' +
893
+ '<span class="rv-foot-actions">' +
894
+ (open
895
+ ? '<button class="rv-btn rv-btn--ghost rv-btn--sm" data-skip>Skip for now</button>' +
896
+ '<button class="rv-btn rv-btn--primary" data-accept>Accept page' + ICON.arrowRight + '</button>'
897
+ : '<span class="rv-applied-note">' + (accepted(change) ? ICON.check + 'Applied' : 'No pending changes') + '</span>' +
898
+ (position < pages.length ? '<button class="rv-btn rv-btn--ghost rv-btn--sm" data-next>Next page' + ICON.arrowRight + '</button>'
899
+ : '<button class="rv-btn rv-btn--ghost rv-btn--sm" data-overview>Back to proposal</button>')) +
900
+ '</span>';
901
+ }
902
+
903
+ function wireFocus(change, position, pages) {
904
+ app.querySelectorAll('[data-overview]').forEach((button) => {
905
+ button.addEventListener('click', () => go('/runs/' + run.id));
906
+ });
907
+ const shortcuts = app.querySelector('[data-shortcuts]');
908
+ if (shortcuts) shortcuts.addEventListener('click', () => { document.getElementById('overlay').hidden = false; });
909
+ const skip = app.querySelector('[data-skip]');
910
+ if (skip) skip.addEventListener('click', () => advance(position, pages));
911
+ const next = app.querySelector('[data-next]');
912
+ if (next) next.addEventListener('click', () => advance(position, pages));
913
+ const accept = app.querySelector('[data-accept]');
914
+ if (accept) accept.addEventListener('click', () => void acceptPage(change, position, pages));
915
+ const source = app.querySelector('[data-source]');
916
+ if (source) source.addEventListener('click', () => toggleSource());
917
+
918
+ app.querySelectorAll('[data-menu]').forEach((button) => {
919
+ button.addEventListener('click', (event) => {
920
+ event.stopPropagation();
921
+ const wrap = button.parentElement;
922
+ const isOpen = wrap.querySelector('.rv-menu');
923
+ closeMenus();
924
+ if (isOpen) return;
925
+ wrap.insertAdjacentHTML('beforeend', button.dataset.menu === 'pages' ? pagesMenu(pages, position) : viewMenu());
926
+ wrap.querySelectorAll('[data-goto]').forEach((item) => {
927
+ item.addEventListener('click', () => go('/runs/' + run.id + '/pages/' + item.dataset.goto));
928
+ });
929
+ wrap.querySelectorAll('[data-pref]').forEach((item) => {
930
+ item.addEventListener('click', () => {
931
+ const [name, value] = item.dataset.pref.split(':');
932
+ if (name === 'layout') prefs.layout = value;
933
+ if (name === 'only') prefs.onlyChanges = !prefs.onlyChanges;
934
+ if (name === 'source') prefs.source = !prefs.source;
935
+ savePrefs();
936
+ closeMenus();
937
+ app.dataset.focusKey = '';
938
+ void render();
939
+ });
940
+ });
941
+ });
942
+ });
943
+ }
944
+
945
+ function pagesMenu(pages, position) {
946
+ return '<div class="rv-menu"><div class="rv-menu-label">Pages in this proposal</div>' +
947
+ pages.map((change, index) => '<button class="rv-menu-item' + (index + 1 === position ? ' is-active' : '') +
948
+ '" data-goto="' + (index + 1) + '">' + ICON.page + '<span>' + esc(change.title) +
949
+ '<small>' + (accepted(change) ? 'Accepted' : plural(openCount(change), 'change', 'changes')) + '</small></span>' +
950
+ (index + 1 === position ? '<span class="rv-menu-check">' + ICON.check + '</span>' : '') + '</button>').join('') +
951
+ '</div>';
952
+ }
953
+
954
+ function viewMenu() {
955
+ const tick = '<span class="rv-menu-check">' + ICON.check + '</span>';
956
+ return '<div class="rv-menu rv-menu--right"><div class="rv-menu-label">Layout</div>' +
957
+ '<button class="rv-menu-item' + (prefs.layout === 'split' ? ' is-active' : '') + '" data-pref="layout:split">' + ICON.columns + '<span>Side by side</span>' + (prefs.layout === 'split' ? tick : '') + '</button>' +
958
+ '<button class="rv-menu-item' + (prefs.layout === 'unified' ? ' is-active' : '') + '" data-pref="layout:unified">' + ICON.page + '<span>Stacked</span>' + (prefs.layout === 'unified' ? tick : '') + '</button>' +
959
+ '<div class="rv-menu-sep"></div>' +
960
+ '<button class="rv-menu-item' + (prefs.onlyChanges ? ' is-active' : '') + '" data-pref="only:toggle">' + ICON.sparkle + '<span>Only the changes</span>' + (prefs.onlyChanges ? tick : '') + '</button>' +
961
+ '<div class="rv-menu-sep"></div>' +
962
+ '<button class="rv-menu-item' + (prefs.source ? ' is-active' : '') + '" data-pref="source:toggle">' + ICON.code + '<span>' + (prefs.source ? 'Rendered comparison' : 'Source diff') + '<small>' + (prefs.source ? 'Read it as a finished page' : 'Line by line, accept one change at a time') + '</small></span></button>' +
963
+ '</div>';
964
+ }
965
+
966
+ function toggleSource() {
967
+ prefs.source = !prefs.source;
968
+ savePrefs();
969
+ app.dataset.focusKey = '';
970
+ void render();
971
+ }
972
+
973
+ function closeMenus() { document.querySelectorAll('.rv-menu').forEach((menu) => menu.remove()); }
974
+ document.addEventListener('click', closeMenus);
975
+
976
+ async function paintSource(change) {
977
+ const target = document.getElementById('source');
978
+ if (!target) return;
979
+ let diff;
980
+ try { diff = await request('/api/runs/' + run.id + '/changes/' + change.id + '/diff'); }
981
+ catch (error) { target.innerHTML = '<div class="rv-banner rv-banner--error">' + ICON.alert + '<span>' + esc(error.message) + '</span></div>'; return; }
982
+ if (!document.getElementById('source')) return;
983
+
984
+ if (diff.binary) {
985
+ target.innerHTML = '<div class="rv-hint">' + ICON.sparkle + '<span>This is a binary asset, so there is no line-by-line diff.</span></div>';
986
+ return;
987
+ }
988
+ const groups = [];
989
+ diff.rows.forEach((row) => {
990
+ const last = groups[groups.length - 1];
991
+ if (row.hunkId && last && last.hunkId === row.hunkId) { last.rows.push(row); return; }
992
+ if (!row.hunkId && last && !last.hunkId) { last.rows.push(row); return; }
993
+ groups.push({ hunkId: row.hunkId, state: row.hunkState, rows: [row] });
994
+ });
995
+ let number = 0;
996
+ const body = groups.map((group) => {
997
+ const lines = group.rows.map(lineHtml).join('');
998
+ if (!group.hunkId) return '<div class="rv-hunk"><div class="rv-lines">' + lines + '</div></div>';
999
+ number += 1;
1000
+ const first = group.rows[0];
1001
+ const actionable = pending(run) && group.state === 'pending';
1002
+ return '<div class="rv-hunk rv-hunk--' + esc(group.state) + '"><div class="rv-hunk-head">' +
1003
+ '<span class="rv-hunk-title">Change ' + number + '</span>' +
1004
+ '<span class="rv-hunk-lines">line ' + (first.oldNumber || first.newNumber || 1) + '</span>' +
1005
+ '<span class="rv-hunk-actions">' +
1006
+ (actionable ? '<button class="rv-btn rv-btn--accept rv-btn--sm" data-hunk="' + esc(group.hunkId) + '">' + ICON.check + 'Accept</button>'
1007
+ : '<span class="rv-hunk-state rv-hunk-state--' + esc(group.state) + '">' +
1008
+ (group.state === 'accepted' ? ICON.check + 'Accepted' : group.state === 'rejected' ? ICON.x + 'Rejected' : '') + '</span>') +
1009
+ '</span></div><div class="rv-lines">' + lines + '</div></div>';
1010
+ }).join('');
1011
+
1012
+ target.innerHTML = '<div class="rv-hint">' + ICON.sparkle + '<span>' +
1013
+ plural(diff.added, 'line', 'lines') + ' added and ' + plural(diff.removed, 'line', 'lines') +
1014
+ ' removed. Accept changes one at a time here' +
1015
+ (isPage(change) ? ', or switch back to the rendered comparison from the View menu.' : '.') +
1016
+ '</span></div>' + body;
1017
+ target.querySelectorAll('[data-hunk]').forEach((button) => {
1018
+ button.addEventListener('click', () => void acceptHunk(change, button.dataset.hunk));
1019
+ });
1020
+ }
1021
+
1022
+ function lineHtml(row) {
1023
+ if (row.type === 'gap') {
1024
+ return '<div class="rv-line rv-line--gap"><span>⋯</span><span>' + plural(row.hidden, 'unchanged line', 'unchanged lines') + '</span></div>';
1025
+ }
1026
+ const sign = row.type === 'insert' ? '+' : row.type === 'delete' ? '−' : '';
1027
+ return '<div class="rv-line rv-line--' + row.type + '">' +
1028
+ '<span class="rv-line-num">' + (row.oldNumber || '') + '</span>' +
1029
+ '<span class="rv-line-num">' + (row.newNumber || '') + '</span>' +
1030
+ '<span class="rv-line-sign">' + sign + '</span>' +
1031
+ '<span class="rv-line-code">' + (row.html || ' ') + '</span></div>';
1032
+ }
1033
+
1034
+ /* ── Supporting file: source diff only, reached from the overview ── */
1035
+
1036
+ function screenFile(changeId) {
1037
+ const change = run.changes.find((entry) => entry.id === changeId);
1038
+ if (!change) { go('/runs/' + run.id, true); return; }
1039
+ const key = ['file', run.id, change.id, change.hunks.filter((hunk) => hunk.acceptedAt).length].join('|');
1040
+ if (app.dataset.focusKey === key) return;
1041
+ app.dataset.focusKey = key;
1042
+
1043
+ app.innerHTML = shell(appbar(
1044
+ '<button class="rv-back" data-overview>' + ICON.arrowLeft + 'Proposal</button>' +
1045
+ '<span class="rv-appbar-divider"></span>' +
1046
+ '<span class="rv-appbar-title"><strong>' + esc(change.title) + '</strong><span>' + esc(change.path) + '</span></span>' +
1047
+ '<span class="rv-appbar-spacer"></span>') +
1048
+ '<div class="rv-focus"><div class="rv-focus-bar">' +
1049
+ '<span class="rv-chip">' + esc(label(change.category)) + '</span>' +
1050
+ '<span class="rv-foot-count">Supporting file — Doxloop keeps this in step with your pages.</span>' +
1051
+ '<span class="rv-appbar-spacer"></span>' +
1052
+ (pending(run) && openCount(change) > 0
1053
+ ? '<button class="rv-btn rv-btn--ghost rv-btn--sm" data-accept-file>' + ICON.check + 'Apply now</button>'
1054
+ : '<span class="rv-applied-note">' + (accepted(change) ? ICON.check + 'Applied' : 'No pending changes') + '</span>') +
1055
+ '</div><div class="rv-stage"><div class="rv-source" id="source"><div class="rv-skeleton"></div></div></div></div>');
1056
+
1057
+ app.querySelectorAll('[data-overview]').forEach((button) => {
1058
+ button.addEventListener('click', () => go('/runs/' + run.id));
1059
+ });
1060
+ const apply = app.querySelector('[data-accept-file]');
1061
+ if (apply) apply.addEventListener('click', () => void acceptSupporting(change));
1062
+ void paintSource(change);
1063
+ }
1064
+
1065
+ async function acceptSupporting(change) {
1066
+ if (busy) return;
1067
+ busy = true;
1068
+ try {
1069
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'page', changeId: change.id }) });
1070
+ showToast(change.title + ' was applied and validated.', 'success');
1071
+ } catch (error) { showToast(error.message, 'error'); }
1072
+ busy = false;
1073
+ app.dataset.focusKey = '';
1074
+ await render();
1075
+ }
1076
+
1077
+ /* ── Screen 4: done ── */
1078
+
1079
+ function screenDone() {
1080
+ app.dataset.focusKey = '';
1081
+ const pages = pagesOf(run);
1082
+ const done = pages.filter(accepted);
1083
+ const waiting = pages.filter((change) => !settled(change));
1084
+ const support = supportingOf(run);
1085
+ const supportDone = support.filter(accepted).length;
1086
+
1087
+ // Nothing was written, so nothing is celebrated.
1088
+ const nothingDone = done.length === 0;
1089
+ app.innerHTML = shell(appbar('<button class="rv-back" data-home>' + ICON.arrowLeft + 'Proposals</button><span class="rv-appbar-spacer"></span>') +
1090
+ '<div class="rv-scroll"><div class="rv-page"><div class="rv-done">' +
1091
+ '<span class="rv-done-mark' + (nothingDone ? ' rv-done-mark--neutral' : '') + '">' + (nothingDone ? ICON.inbox : ICON.checkBig) + '</span>' +
1092
+ '<h1>' + (waiting.length === 0 ? 'All set' : nothingDone ? 'Nothing applied yet' : 'Progress saved') + '</h1>' +
1093
+ '<p>' + plural(done.length, 'page was', 'pages were') + ' updated and validated' +
1094
+ (supportDone > 0 ? ', along with ' + plural(supportDone, 'supporting file', 'supporting files') : '') + '.' +
1095
+ (waiting.length > 0 ? ' ' + plural(waiting.length, 'page is', 'pages are') + ' still waiting for a decision.' : '') + '</p>' +
1096
+ '<div class="rv-done-actions">' +
1097
+ (waiting.length > 0 ? '<button class="rv-btn rv-btn--primary" id="resume">Review the remaining ' + (waiting.length === 1 ? 'page' : 'pages') + ICON.arrowRight + '</button>' : '') +
1098
+ '<button class="rv-btn ' + (waiting.length > 0 ? 'rv-btn--ghost' : 'rv-btn--primary') + '" data-home>Back to proposals</button>' +
1099
+ '</div>' +
1100
+ (waiting.length > 0 && support.length > 0 && supportDone === 0
1101
+ ? '<div class="rv-banner rv-banner--warn" style="max-width:520px">' + ICON.alert +
1102
+ '<span>' + plural(support.length, 'supporting file', 'supporting files') + ' will be updated once every page is accepted.</span></div>' : '') +
1103
+ '</div></div></div>');
1104
+ const resume = document.getElementById('resume');
1105
+ if (resume) resume.addEventListener('click', () => go('/runs/' + run.id + '/pages/' + firstOpenPage()));
1106
+ }
1107
+
1108
+ /* ── Actions ── */
1109
+
1110
+ function advance(position, pages) {
1111
+ if (position < pages.length) { go('/runs/' + run.id + '/pages/' + (position + 1)); return; }
1112
+ go('/runs/' + run.id + '/done');
1113
+ }
1114
+
1115
+ async function acceptPage(change, position, pages) {
1116
+ if (busy) return;
1117
+ busy = true;
1118
+ try {
1119
+ showToast('Applying ' + change.title + '…');
1120
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'page', changeId: change.id }) });
1121
+ // Supporting files carry the bookkeeping for the pages; they are written
1122
+ // once every page in the proposal has been accepted.
1123
+ if (pagesOf(run).every(accepted) && supportingOf(run).some((entry) => openCount(entry) > 0)) {
1124
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'all' }) });
1125
+ }
1126
+ showToast(change.title + ' was applied and validated.', 'success');
1127
+ busy = false;
1128
+ app.dataset.focusKey = '';
1129
+ advance(position, pages);
1130
+ } catch (error) {
1131
+ busy = false;
1132
+ showToast(error.message, 'error');
1133
+ await reload();
1134
+ }
1135
+ }
1136
+
1137
+ async function acceptHunk(change, hunkId) {
1138
+ if (busy) return;
1139
+ busy = true;
1140
+ try {
1141
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'hunk', changeId: change.id, hunkId: hunkId }) });
1142
+ if (pagesOf(run).every(accepted) && supportingOf(run).some((entry) => openCount(entry) > 0)) {
1143
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'all' }) });
1144
+ }
1145
+ showToast('The change was applied and validated.', 'success');
1146
+ } catch (error) { showToast(error.message, 'error'); }
1147
+ busy = false;
1148
+ app.dataset.focusKey = '';
1149
+ await render();
1150
+ }
1151
+
1152
+ async function acceptEverything() {
1153
+ if (busy) return;
1154
+ if (!confirm('Apply every remaining change in this proposal to your documentation?')) return;
1155
+ busy = true;
1156
+ try {
1157
+ showToast('Applying every remaining change…');
1158
+ run = await request('/api/runs/' + run.id + '/accept', { method: 'POST', body: JSON.stringify({ scope: 'all' }) });
1159
+ showToast('The proposal was applied and validated.', 'success');
1160
+ busy = false;
1161
+ go('/runs/' + run.id + '/done');
1162
+ } catch (error) {
1163
+ busy = false;
1164
+ showToast(error.message, 'error');
1165
+ await reload();
1166
+ }
1167
+ }
1168
+
1169
+ async function rejectRun() {
1170
+ if (!confirm('Reject this proposal? Nothing will be written to your documentation.')) return;
1171
+ try {
1172
+ run = await request('/api/runs/' + run.id + '/reject', { method: 'POST', body: '{}' });
1173
+ showToast('The proposal was rejected. No documentation was changed.', 'success');
1174
+ go('/');
1175
+ } catch (error) { showToast(error.message, 'error'); }
1176
+ }
1177
+
1178
+ async function reload() {
1179
+ const button = document.getElementById('refresh');
1180
+ if (button) button.classList.add('is-busy');
1181
+ try {
1182
+ runs = await request('/api/runs');
1183
+ if (run) run = await request('/api/runs/' + run.id);
1184
+ app.dataset.focusKey = '';
1185
+ await render();
1186
+ } catch (error) { showToast(error.message, 'error'); }
1187
+ if (button) button.classList.remove('is-busy');
1188
+ }
1189
+
1190
+ let toastTimer;
1191
+ function showToast(message, tone) {
1192
+ const toast = document.getElementById('toast');
1193
+ toast.innerHTML = (tone === 'success' ? ICON.checkCircle : tone === 'error' ? ICON.alert : '') + '<span>' + esc(message) + '</span>';
1194
+ toast.className = 'rv-toast is-visible' + (tone ? ' rv-toast--' + tone : '');
1195
+ clearTimeout(toastTimer);
1196
+ toastTimer = setTimeout(() => { toast.className = 'rv-toast'; }, tone === 'error' ? 8000 : 3400);
1197
+ }
1198
+
1199
+ /* ── Global wiring ── */
1200
+
1201
+ document.addEventListener('click', (event) => {
1202
+ const home = event.target.closest('[data-home]');
1203
+ if (home) { event.preventDefault(); go('/'); }
1204
+ const activity = event.target.closest('[data-activity]');
1205
+ if (activity) {
1206
+ event.preventDefault(); filter = 'all';
1207
+ if (location.pathname === '/') { paintList(); document.getElementById('activity')?.scrollIntoView({ behavior: 'smooth' }); }
1208
+ else go('/#activity');
1209
+ }
1210
+ const shortcuts = event.target.closest('[data-shortcuts]');
1211
+ if (shortcuts) document.getElementById('overlay').hidden = false;
1212
+ const close = event.target.closest('[data-close]');
1213
+ if (close || event.target === document.getElementById('overlay')) document.getElementById('overlay').hidden = true;
1214
+ });
1215
+
1216
+ document.addEventListener('keydown', (event) => {
1217
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
1218
+ const tag = document.activeElement ? document.activeElement.tagName : '';
1219
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
1220
+ const overlay = document.getElementById('overlay');
1221
+ if (event.key === '?') { overlay.hidden = !overlay.hidden; return; }
1222
+ if (event.key === 'Escape') {
1223
+ // Escape dismisses whatever is open before it navigates anywhere.
1224
+ if (!overlay.hidden) { overlay.hidden = true; return; }
1225
+ if (document.querySelector('.rv-menu')) { closeMenus(); return; }
1226
+ if (run && /\\/(pages|files)\\//.test(location.pathname)) go('/runs/' + run.id);
1227
+ return;
1228
+ }
1229
+ const match = /^\\/runs\\/([a-z0-9-]+)\\/pages\\/(\\d+)$/.exec(location.pathname);
1230
+ if (!match || !run) return;
1231
+ const pages = pagesOf(run);
1232
+ const position = Number(match[2]);
1233
+ const change = pages[position - 1];
1234
+ if (!change) return;
1235
+ if (event.key === 'ArrowRight') { if (position < pages.length) go('/runs/' + run.id + '/pages/' + (position + 1)); return; }
1236
+ if (event.key === 'ArrowLeft') { if (position > 1) go('/runs/' + run.id + '/pages/' + (position - 1)); return; }
1237
+ if (event.key === 'd') { toggleSource(); return; }
1238
+ if (event.key === 's') { advance(position, pages); return; }
1239
+ if ((event.key === 'a' || event.key === 'Enter') && pending(run) && !settled(change)) {
1240
+ event.preventDefault();
1241
+ void acceptPage(change, position, pages);
1242
+ }
1243
+ });
1244
+
1245
+ void render();
1246
+ `;
1247
+ }
1248
+ //# sourceMappingURL=review-ui.js.map