@panphora/clayjs 1.15.2 → 1.17.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 (551) hide show
  1. package/README.md +4 -5
  2. package/THIRD-PARTY-NOTICES.md +16 -0
  3. package/compat/highlight-11.9.0.min.js +1213 -0
  4. package/compat/highlight-LICENSE +29 -0
  5. package/compat/journal-assets.lock.json +22 -0
  6. package/compat/overtype-2.6.1.min.js +116 -0
  7. package/compat/overtype-LICENSE +21 -0
  8. package/dist/all.js +90 -0
  9. package/dist/clay-dom.js +84 -0
  10. package/dist/clay-events.js +103 -0
  11. package/dist/clay-internals.js +85 -0
  12. package/dist/clay-options.js +93 -0
  13. package/dist/clay-router.js +98 -0
  14. package/dist/clay-ui.js +95 -0
  15. package/dist/clay-utils.js +84 -0
  16. package/dist/clay.js +146 -0
  17. package/dist/clay.standalone.js +84291 -57611
  18. package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
  19. package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
  20. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  21. package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2SPVQSHC.js +1 -0
  24. package/dist/m/c-2YYDMLNZ.js +1 -0
  25. package/dist/m/c-336SGU5W.js +1 -0
  26. package/dist/m/c-33JJ6CBX.js +2 -0
  27. package/dist/m/c-3GJ4SDE2.js +1 -0
  28. package/dist/m/c-3H6BZDNJ.js +6 -0
  29. package/dist/m/c-3OWDMNJR.js +2 -0
  30. package/dist/m/c-3RUU4TPX.js +91 -0
  31. package/dist/m/c-3XIRD4NU.js +1 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4DFEJ2PO.js +3 -0
  34. package/dist/m/c-4JZDZFF6.js +1 -0
  35. package/dist/m/c-4L3BTFRK.js +1 -0
  36. package/dist/m/c-4Q2XTQ2N.js +2 -0
  37. package/dist/m/c-4WKFKEKF.js +1 -0
  38. package/dist/m/c-56QRZTZH.js +1 -0
  39. package/dist/m/c-5J4E7O4Y.js +4 -0
  40. package/dist/m/c-5LKDWQTL.js +19 -0
  41. package/dist/m/c-5QWQ7A5L.js +1 -0
  42. package/dist/m/c-5RJFTLSF.js +1 -0
  43. package/dist/m/c-5V3MOVJT.js +1 -0
  44. package/dist/m/c-5VYOWDTI.js +1 -0
  45. package/dist/m/c-5YIHJ3TJ.js +1 -0
  46. package/dist/m/c-6AZOGAPE.js +1 -0
  47. package/dist/m/c-6CRUUXOX.js +5 -0
  48. package/dist/m/c-6HMB24EV.js +1 -0
  49. package/dist/m/c-6KEVEQ7L.js +1 -0
  50. package/dist/m/c-6NC5I2TV.js +1 -0
  51. package/dist/m/c-74MGYWZF.js +1 -0
  52. package/dist/m/c-7DJDKPGL.js +1 -0
  53. package/dist/m/c-7OLLJPWP.js +1 -0
  54. package/dist/m/c-7SWMYKAI.js +1 -0
  55. package/dist/m/c-A3E3NIMC.js +1 -0
  56. package/dist/m/c-A7LS5UZW.js +1 -0
  57. package/dist/m/c-AMSNPOCX.js +1 -0
  58. package/dist/m/c-AQLFEATB.js +1 -0
  59. package/dist/m/c-B3JM5ZXC.js +1 -0
  60. package/dist/m/c-BBVGTQG7.js +1 -0
  61. package/dist/m/c-BPF2NSPV.js +1 -0
  62. package/dist/m/c-BQ6LOBEE.js +1 -0
  63. package/dist/m/c-BTDI4Y23.js +1 -0
  64. package/dist/m/c-BWFVSBDF.js +1 -0
  65. package/dist/m/c-BYTKL6UX.js +1 -0
  66. package/dist/m/c-CFHW5AMY.js +6 -0
  67. package/dist/m/c-CJ3A7ZKE.js +1 -0
  68. package/dist/m/c-CQHPGKX2.js +1 -0
  69. package/dist/m/c-CYWLPPUS.js +1 -0
  70. package/dist/m/c-CZUSOB7V.js +1 -0
  71. package/dist/m/c-DCFSEJZG.js +1 -0
  72. package/dist/m/c-DCXOXBF3.js +1 -0
  73. package/dist/m/c-DFS4XZJV.js +2 -0
  74. package/dist/m/c-DNENWY4O.js +1 -0
  75. package/dist/m/c-DNZ5IFM6.js +1 -0
  76. package/dist/m/c-E4LIR5X5.js +1 -0
  77. package/dist/m/c-EJTGYSLN.js +1 -0
  78. package/dist/m/c-EVCQ3BKA.js +1 -0
  79. package/dist/m/c-EZU56NQV.js +34 -0
  80. package/dist/m/c-FDNHHZY5.js +1 -0
  81. package/dist/m/c-FILLTJA7.js +1 -0
  82. package/dist/m/c-FRLHSY7E.js +1 -0
  83. package/dist/m/c-FROJUZMV.js +1 -0
  84. package/dist/m/c-G2JPKCTP.js +1 -0
  85. package/dist/m/c-GB6TU7WV.js +1 -0
  86. package/dist/m/c-GFJJEBTU.js +1 -0
  87. package/dist/m/c-GJAEGGXY.js +1 -0
  88. package/dist/m/c-GOWBDM6J.js +1 -0
  89. package/dist/m/c-GQ6F6IDK.js +1 -0
  90. package/dist/m/c-GSFIGKOP.js +1 -0
  91. package/dist/m/c-GY422N3Q.js +1 -0
  92. package/dist/m/c-H4LG442Q.js +1 -0
  93. package/dist/m/c-HA6INT6J.js +1 -0
  94. package/dist/m/c-HAWFNBYG.js +1 -0
  95. package/dist/m/c-HDDEEL77.js +1 -0
  96. package/dist/m/c-HFKUVACH.js +1 -0
  97. package/dist/m/c-HHDFCNHV.js +1 -0
  98. package/dist/m/c-HPURYVZS.js +2 -0
  99. package/dist/m/c-I2ZATQCD.js +2 -0
  100. package/dist/m/c-IEM4NTCS.js +1 -0
  101. package/dist/m/c-IJUK2GPW.js +1 -0
  102. package/dist/m/c-IW6GGOU2.js +1 -0
  103. package/dist/m/c-JAX2667S.js +6 -0
  104. package/dist/m/c-JDCX26UP.js +25 -0
  105. package/dist/m/c-JRKY44DK.js +1 -0
  106. package/dist/m/c-JU3NK25H.js +1 -0
  107. package/dist/m/c-KAU6E43D.js +1 -0
  108. package/dist/m/c-KBS3UV6B.js +1 -0
  109. package/dist/m/c-KGVGROZT.js +1 -0
  110. package/dist/m/c-KI7M4AY3.js +1 -0
  111. package/dist/m/c-KIPQJX4B.js +1 -0
  112. package/dist/m/c-KQ6TYJAP.js +135 -0
  113. package/dist/m/c-KU3ZFADW.js +1 -0
  114. package/dist/m/c-L4WQSC27.js +1 -0
  115. package/dist/m/c-LFPHCXY3.js +1 -0
  116. package/dist/m/c-LRPUO2MX.js +1 -0
  117. package/dist/m/c-LWLBHLVC.js +3 -0
  118. package/dist/m/c-LZJPNR2W.js +1 -0
  119. package/dist/m/c-M2L47S2X.js +7 -0
  120. package/dist/m/c-MFP2ZWD4.js +1 -0
  121. package/dist/m/c-MKSQ4Y57.js +1 -0
  122. package/dist/m/c-MTQHWQC3.js +1 -0
  123. package/dist/m/c-N326R2JD.js +1 -0
  124. package/dist/m/c-N7JPUPGC.js +1 -0
  125. package/dist/m/c-NRPJEVJS.js +1 -0
  126. package/dist/m/c-NUFFJVVJ.js +1 -0
  127. package/dist/m/c-NUHP2XDB.js +1 -0
  128. package/dist/m/c-NUHSPT5P.js +1 -0
  129. package/dist/m/c-O72HVYSA.js +1 -0
  130. package/dist/m/c-OCSZ4QGN.js +1 -0
  131. package/dist/m/c-OIBUTDXI.js +1 -0
  132. package/dist/m/c-OIDRZKAF.js +1 -0
  133. package/dist/m/c-OW5QIGG3.js +1 -0
  134. package/dist/m/c-P7KNDG34.js +1 -0
  135. package/dist/m/c-PGRCK73F.js +1 -0
  136. package/dist/m/c-PQ5LK6QE.js +993 -0
  137. package/dist/m/c-PS5ARVWR.js +9 -0
  138. package/dist/m/c-PZ4XEPUP.js +2 -0
  139. package/dist/m/c-Q622NS2Y.js +1 -0
  140. package/dist/m/c-Q7NQD65V.js +1 -0
  141. package/dist/m/c-QH63LQOS.js +1 -0
  142. package/dist/m/c-QHIWXHRO.js +1 -0
  143. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  144. package/dist/m/c-QICP2YR5.js +1 -0
  145. package/dist/m/c-QJ4OM3G6.js +1 -0
  146. package/dist/m/c-QNKREOL6.js +1 -0
  147. package/dist/m/c-R3L7ZJGB.js +2 -0
  148. package/dist/m/c-R7IWVINQ.js +1 -0
  149. package/dist/m/c-RM7J7BV2.js +1 -0
  150. package/dist/m/c-RSMOODDU.js +1 -0
  151. package/dist/m/c-RVMWGXEJ.js +1 -0
  152. package/dist/m/c-RYUXUUBX.js +1 -0
  153. package/dist/m/c-S4ZEH46B.js +9 -0
  154. package/dist/m/c-S7H2DRCB.js +1 -0
  155. package/dist/m/c-S7LK4KUE.js +1 -0
  156. package/dist/m/c-SDAXDM2D.js +1 -0
  157. package/dist/m/c-SGIA4IEQ.js +188 -0
  158. package/dist/m/c-SMBZG5X4.js +1 -0
  159. package/dist/m/c-SMGU7RT3.js +235 -0
  160. package/dist/m/c-SPIORRYS.js +1 -0
  161. package/dist/m/c-SWLRT66R.js +1 -0
  162. package/dist/m/c-SZGYW5HT.js +1 -0
  163. package/dist/m/c-TCCA5BZN.js +2 -0
  164. package/dist/m/c-TCJTVAFN.js +1 -0
  165. package/dist/m/c-TJ6WOJL3.js +67 -0
  166. package/dist/m/c-TQEMVJCS.js +1 -0
  167. package/dist/m/c-TRS6TM6O.js +1 -0
  168. package/dist/m/c-TYLXX4NF.js +21 -0
  169. package/dist/m/c-U62OB4BT.js +1 -0
  170. package/dist/m/c-U6YWUAR3.js +8 -0
  171. package/dist/m/c-UDCSU5G5.js +2 -0
  172. package/dist/m/c-UDUUJPFZ.js +1 -0
  173. package/dist/m/c-UGHH4MB3.js +1 -0
  174. package/dist/m/c-UL2YUY52.js +1 -0
  175. package/dist/m/c-VAK7SG37.js +1 -0
  176. package/dist/m/c-VDSS4K3Z.js +1 -0
  177. package/dist/m/c-VE6ZMYMC.js +1 -0
  178. package/dist/m/c-VG5XIN4T.js +1 -0
  179. package/dist/m/c-VHM5XQHK.js +1 -0
  180. package/dist/m/c-VHNSBD2M.js +1 -0
  181. package/dist/m/c-VI5RMSVY.js +1 -0
  182. package/dist/m/c-VKEDWYDF.js +1 -0
  183. package/dist/m/c-VRZN56XY.js +1 -0
  184. package/dist/m/c-W5MCHEGA.js +1 -0
  185. package/dist/m/c-WDC5GVKU.js +1 -0
  186. package/dist/m/c-WN5AY3OZ.js +1 -0
  187. package/dist/m/c-WNYBLIOZ.js +1 -0
  188. package/dist/m/c-WRBKEJD4.js +1 -0
  189. package/dist/m/c-WWXIDH4O.js +28 -0
  190. package/dist/m/c-WYFDLITL.js +1 -0
  191. package/dist/m/c-XCNTK76U.js +1 -0
  192. package/dist/m/c-XMS6DPBR.js +1 -0
  193. package/dist/m/c-XN6MBKI6.js +67 -0
  194. package/dist/m/c-XPELB6E5.js +1 -0
  195. package/dist/m/c-XQXZAO7N.js +1 -0
  196. package/dist/m/c-Y5HBBIQ4.js +1 -0
  197. package/dist/m/c-ZE46QDIH.js +1 -0
  198. package/dist/m/c-ZUCOKOHT.js +1 -0
  199. package/dist/m/c-ZVPVXTJF.js +4 -0
  200. package/dist/m/c-ZVRKXKM7.js +1 -0
  201. package/dist/m/c-ZWIOJGMT.js +1 -0
  202. package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
  203. package/dist/m/core/autosave-HRYGUWV6.js +1 -0
  204. package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
  205. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  206. package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
  207. package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
  208. package/dist/m/core/persist-JFYRHMRJ.js +3 -0
  209. package/dist/m/core/recovery-FLTCC2OC.js +1 -0
  210. package/dist/m/core/save-HQZ4S667.js +1 -0
  211. package/dist/m/core/save-core-KGQWFUNH.js +1 -0
  212. package/dist/m/core/snapshot-EGQZNT45.js +1 -0
  213. package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
  214. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  215. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  216. package/dist/m/events/index-RN2JNGBV.js +1 -0
  217. package/dist/m/internals/index-PVXGPUDA.js +1 -0
  218. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  219. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  220. package/dist/m/loader-S4I7GKXR.js +1 -0
  221. package/dist/m/manifest-4C419D67.js +1 -0
  222. package/dist/m/manifest-edit-B077F288.js +1 -0
  223. package/dist/m/manifest.json +3988 -0
  224. package/dist/m/options/options-EJMS3K24.js +1 -0
  225. package/dist/m/page/editor-NNJA4OKD.js +202 -0
  226. package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
  227. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  228. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  229. package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
  230. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  231. package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
  232. package/dist/m/plugins/live-HFWD4OIH.js +1 -0
  233. package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
  234. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  235. package/dist/m/plugins/page-JOKB6XPR.js +2 -0
  236. package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
  237. package/dist/m/plugins/people-SGPJKANK.js +1 -0
  238. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  239. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  240. package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
  241. package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
  242. package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
  243. package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
  244. package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
  245. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  246. package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
  247. package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
  248. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
  249. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
  250. package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
  251. package/dist/m/reader-core-DXYCURFF.js +68 -0
  252. package/dist/m/route/route-KOCZSEGN.js +1 -0
  253. package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
  254. package/dist/m/ui/index-3JLVI27O.js +1 -0
  255. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  256. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  257. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  258. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  259. package/dist/offline-manifest.json +4034 -0
  260. package/dist/page/themes/light-1.css +430 -0
  261. package/dist/server/core.cjs +23748 -22855
  262. package/dist/server/offline-runtime.cjs +4954 -0
  263. package/dist/server/offline-runtime.mjs +4923 -0
  264. package/dist/server/rooms.mjs +9 -0
  265. package/docs/guide.md +1078 -669
  266. package/entries/all.js +2 -0
  267. package/entries/clay-data.js +1 -1
  268. package/entries/clay-dom.js +2 -0
  269. package/entries/clay-events.js +2 -0
  270. package/entries/clay-internals.js +2 -0
  271. package/entries/clay-options.js +2 -0
  272. package/entries/clay-router.js +2 -0
  273. package/entries/clay-ui.js +2 -0
  274. package/entries/clay-utils.js +2 -0
  275. package/entries/clay.js +2 -0
  276. package/entries/sap.js +6 -6
  277. package/fonts/ibm-plex-mono-LICENSE +93 -0
  278. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  279. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  280. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  281. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  282. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  283. package/fonts/ibm-plex-sans-LICENSE +93 -0
  284. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  285. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  286. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  287. package/fonts/ibm-plex.css +71 -0
  288. package/fonts/inter-LICENSE +93 -0
  289. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  290. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  291. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  292. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  293. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  294. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  295. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  296. package/fonts/inter.css +62 -0
  297. package/fonts/newsreader-LICENSE +93 -0
  298. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  299. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  300. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  301. package/fonts/newsreader.css +26 -0
  302. package/fonts/work-sans-LICENSE +93 -0
  303. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  304. package/fonts/work-sans.css +8 -0
  305. package/package.json +20 -3
  306. package/packed-contract.json +1 -0
  307. package/src/attrs/add-from.js +27 -4
  308. package/src/attrs/save-freeze.js +7 -52
  309. package/src/core/admin-attrs.js +119 -12
  310. package/src/core/admin-contenteditable.js +12 -10
  311. package/src/core/admin-inputs.js +14 -15
  312. package/src/core/admin-onclick.js +2 -0
  313. package/src/core/autosave.js +74 -21
  314. package/src/core/boot-phase.js +2 -1
  315. package/src/core/conflict-download.js +19 -23
  316. package/src/core/conflict-notice-ui.js +1173 -0
  317. package/src/core/conflict-notice.js +140 -697
  318. package/src/core/download.js +32 -0
  319. package/src/core/edit-mode.js +147 -14
  320. package/src/core/etag.js +30 -12
  321. package/src/core/host-meta.js +90 -8
  322. package/src/core/host-permission.js +29 -0
  323. package/src/core/is-edit-mode.js +145 -12
  324. package/src/core/mode-key.js +125 -0
  325. package/src/core/mode-lifecycle.js +544 -0
  326. package/src/core/mode-receipt.js +30 -0
  327. package/src/core/mode-refusal.js +13 -0
  328. package/src/core/mode-resources.js +52 -0
  329. package/src/core/mode-support.js +82 -0
  330. package/src/core/mode-switch.js +259 -0
  331. package/src/core/mode-transition.js +112 -0
  332. package/src/core/page-data-write.js +305 -0
  333. package/src/core/page-data.js +26 -227
  334. package/src/core/page-format-wire.js +4 -0
  335. package/src/core/page-format.js +58 -0
  336. package/src/core/persist.js +37 -9
  337. package/src/core/preserve-authored.js +114 -0
  338. package/src/core/recovery.js +772 -0
  339. package/src/core/runtime-url.js +105 -0
  340. package/src/core/save-core.js +113 -12
  341. package/src/core/save-replace.js +72 -0
  342. package/src/core/save-size-notice.js +134 -0
  343. package/src/core/save-size.js +140 -0
  344. package/src/core/save.js +806 -119
  345. package/src/core/snapshot.js +159 -11
  346. package/src/core/source-map.js +157 -9
  347. package/src/core/unsaved-warning.js +14 -4
  348. package/src/core/upgrade-run.js +408 -0
  349. package/src/core/version.js +18 -0
  350. package/src/lib/assets.js +30 -0
  351. package/src/lib/autosave-debug-gate.js +50 -0
  352. package/src/lib/autosave-scheduler.js +140 -0
  353. package/src/lib/boot-imports.js +8 -0
  354. package/src/lib/derived.js +3 -1
  355. package/src/lib/dirty-gate.js +15 -0
  356. package/src/lib/doc-storage.js +33 -0
  357. package/src/lib/effect.js +9 -4
  358. package/src/lib/mutation.js +4 -2
  359. package/src/lib/offline-runtime.js +226 -0
  360. package/src/lib/preload.js +48 -0
  361. package/src/lib/root-attrs.js +1 -1
  362. package/src/lib/trash.js +18 -0
  363. package/src/lib/undo/capture-move.js +152 -0
  364. package/src/live/local-channel.js +61 -0
  365. package/src/live/local-lane.js +60 -9
  366. package/src/live/server-lane.js +6 -3
  367. package/src/loader-logic.js +76 -15
  368. package/src/loader.js +218 -30
  369. package/src/page/adopt-boot.js +76 -22
  370. package/src/page/adopt.js +25 -4
  371. package/src/page/awareness.js +230 -0
  372. package/src/page/blocks.js +113 -6
  373. package/src/page/blocksel.js +20 -4
  374. package/src/page/board-face.js +13 -0
  375. package/src/page/board-panel.js +5 -17
  376. package/src/page/board-style.js +2 -13
  377. package/src/page/board-view.js +1 -11
  378. package/src/page/board-views.js +3 -0
  379. package/src/page/board.js +2 -2
  380. package/src/page/boot-pack.js +26 -0
  381. package/src/page/boot.js +237 -0
  382. package/src/page/chart-tables.js +11 -0
  383. package/src/page/chart.js +6 -10
  384. package/src/page/checklist.js +1 -1
  385. package/src/page/checkpoint-proof.js +289 -0
  386. package/src/page/chrome.js +431 -63
  387. package/src/page/csv-import.js +17 -2
  388. package/src/page/document-title-dom.js +52 -0
  389. package/src/page/document-title-runtime.js +90 -0
  390. package/src/page/document-title.js +12 -0
  391. package/src/page/drag.js +48 -13
  392. package/src/page/drop.js +2 -1
  393. package/src/page/editor.js +399 -90
  394. package/src/page/emoji-data.js +162 -0
  395. package/src/page/feature-loader.js +72 -0
  396. package/src/page/features/board.js +21 -0
  397. package/src/page/features/chart.js +11 -0
  398. package/src/page/features/code.js +13 -0
  399. package/src/page/features/dates.js +180 -0
  400. package/src/page/features/emoji.js +187 -0
  401. package/src/page/features/media.js +290 -0
  402. package/src/page/features/mentions.js +188 -0
  403. package/src/page/features/registry.js +28 -0
  404. package/src/page/features/table.js +383 -0
  405. package/src/page/{tool-view.js → features/tool.js} +33 -2
  406. package/src/page/format-codec.js +215 -0
  407. package/src/page/format-command.js +9 -0
  408. package/src/page/format-gate.js +179 -0
  409. package/src/page/format-host.js +54 -0
  410. package/src/page/format-level.js +157 -0
  411. package/src/page/format-native-commands.js +142 -0
  412. package/src/page/format-native.js +42 -0
  413. package/src/page/highlight-slot.js +23 -0
  414. package/src/page/highlight.js +30 -16
  415. package/src/page/history-intent.js +78 -0
  416. package/src/page/inline.js +45 -28
  417. package/src/page/items.js +67 -14
  418. package/src/page/link.js +1 -0
  419. package/src/page/markdown.js +112 -3
  420. package/src/page/media-views.js +3 -0
  421. package/src/page/media.js +33 -245
  422. package/src/page/mention-picker.js +99 -0
  423. package/src/page/merge.js +1 -1
  424. package/src/page/outline-cache.js +11 -0
  425. package/src/page/placeholder.js +27 -7
  426. package/src/page/projection.js +240 -51
  427. package/src/page/reader-patch.js +306 -0
  428. package/src/page/recover.js +1 -1
  429. package/src/page/rich/page-tool.js +1 -4
  430. package/src/page/runtime-owner.js +27 -0
  431. package/src/page/save-projection.js +46 -4
  432. package/src/page/save-states.js +1 -1
  433. package/src/page/schema.js +47 -0
  434. package/src/page/table-rect-key.js +3 -0
  435. package/src/page/table-select.js +4 -3
  436. package/src/page/table-style.js +1 -1
  437. package/src/page/table-views.js +3 -0
  438. package/src/page/table.js +8 -293
  439. package/src/page/themes/content.css +25 -1
  440. package/src/page/themes/light-1.css +163 -2
  441. package/src/page/themes/light-1.source.css +269 -0
  442. package/src/page/tool-capability.js +3 -0
  443. package/src/page/tool-views.js +3 -0
  444. package/src/page/view-hooks.js +32 -0
  445. package/src/page/view.js +390 -107
  446. package/src/page/ybind/awareness.js +172 -0
  447. package/src/page/ybind/binding.js +40 -5
  448. package/src/page/ybind/bridge.js +50 -47
  449. package/src/page/ybind/decode.js +52 -19
  450. package/src/page/ybind/doc.js +27 -2
  451. package/src/page/ybind/group.js +12 -3
  452. package/src/page/ybind/index.js +2 -0
  453. package/src/page/ybind/lineage.js +71 -0
  454. package/src/page/ybind/marks.js +26 -1
  455. package/src/page/ybind/migrate.js +48 -13
  456. package/src/page/ybind/moves.js +32 -21
  457. package/src/page/ybind/positions.js +50 -0
  458. package/src/page/ybind/provider.js +97 -24
  459. package/src/page/ybind/recovery-proof.js +191 -0
  460. package/src/page/ybind/recovery-replay.js +488 -0
  461. package/src/page/ybind/tokens.js +15 -6
  462. package/src/page/ybind/walk.js +10 -3
  463. package/src/page/ybind/writer.js +48 -4
  464. package/src/plugins/ai-edit.js +167 -32
  465. package/src/plugins/bind-text.js +218 -0
  466. package/src/plugins/cms.js +93 -0
  467. package/src/plugins/indicator.js +57 -5
  468. package/src/plugins/live.js +319 -35
  469. package/src/plugins/menu/claygoo.svg.js +1 -0
  470. package/src/plugins/menu/seo-head.js +323 -0
  471. package/src/plugins/menu/seo.js +849 -0
  472. package/src/plugins/menu.js +425 -0
  473. package/src/plugins/page-view.js +131 -0
  474. package/src/plugins/page.js +1131 -270
  475. package/src/plugins/people.js +158 -31
  476. package/src/plugins/richclay.js +119 -9
  477. package/src/plugins/sortable.js +370 -60
  478. package/src/plugins/source.js +189 -366
  479. package/src/plugins/undo.js +289 -19
  480. package/src/plugins/unsent.js +132 -0
  481. package/src/plugins/upgrade-run.js +1 -0
  482. package/src/plugins/upgrade.js +210 -0
  483. package/src/plugins/upload-task.js +571 -0
  484. package/src/plugins/upload-ui.js +265 -0
  485. package/src/plugins/upload.js +34 -153
  486. package/src/plugins/visitor-drafts/inert.js +32 -0
  487. package/src/plugins/visitor-drafts/intro.js +2 -1
  488. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  489. package/src/plugins/visitor-drafts/prompt.js +2 -2
  490. package/src/plugins/visitor-drafts-owner.js +85 -0
  491. package/src/plugins/visitor-drafts.js +31 -42
  492. package/src/plugins/wire.js +82 -1
  493. package/src/reader-core.js +26 -0
  494. package/src/recovery/conflicts.js +287 -0
  495. package/src/recovery/identity.js +154 -0
  496. package/src/recovery/store.js +792 -0
  497. package/src/recovery/undo.js +257 -0
  498. package/src/server/checkpoint/index.js +68 -4
  499. package/src/server/offline-runtime/index.js +214 -0
  500. package/src/server/rooms/index.js +2 -1
  501. package/src/server/rooms/live-data-rooms.js +2 -2
  502. package/src/server/rooms/page-admission.js +33 -0
  503. package/src/server/rooms/page-lineage.js +56 -0
  504. package/src/server/rooms/page-rebase.js +11 -5
  505. package/src/server/rooms/page-state.js +2 -0
  506. package/src/server/rooms/page-write-admission.js +9 -3
  507. package/src/source/session.js +705 -0
  508. package/src/sync/conflict-revert.js +5 -4
  509. package/src/sync/conflicts-edit.js +119 -0
  510. package/src/sync/conflicts-facade.js +49 -0
  511. package/src/sync/conflicts.js +99 -1
  512. package/src/sync/editing-marker.js +37 -6
  513. package/src/sync/editing-sender.js +8 -1
  514. package/src/sync/live-sync-edit.js +63 -0
  515. package/src/sync/live-sync.js +364 -119
  516. package/src/sync/local-presence.js +187 -0
  517. package/src/sync/merge-declarations.js +10 -0
  518. package/src/sync/merge-engine.js +12 -0
  519. package/src/sync/presence-store.js +412 -0
  520. package/src/sync/presence-target.js +243 -0
  521. package/src/sync/presence.js +72 -41
  522. package/src/sync/saved-handoff.js +200 -0
  523. package/src/sync/saved-stream.js +544 -0
  524. package/src/sync/version-guard.js +92 -0
  525. package/src/sync/wire-profile.js +81 -0
  526. package/src/ui/bevel-controls.js +2 -8
  527. package/src/ui/page-scheme.js +7 -0
  528. package/src/ui/skins/cms.js +1 -1
  529. package/src/ui/theme-config.js +150 -0
  530. package/src/ui/theme.js +6 -138
  531. package/src/ui/toast-lazy.js +11 -0
  532. package/src/vendor/LICENSES-optional.md +253 -0
  533. package/src/vendor/highlight.vendor.js +3 -0
  534. package/src/vendor/hljs-bash.vendor.js +2 -0
  535. package/src/vendor/hljs-css.vendor.js +2 -0
  536. package/src/vendor/hljs-javascript.vendor.js +2 -0
  537. package/src/vendor/hljs-json.vendor.js +2 -0
  538. package/src/vendor/hljs-python.vendor.js +2 -0
  539. package/src/vendor/hljs-xml.vendor.js +2 -0
  540. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  541. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  542. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  543. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  544. package/src/vendor/hyper-morph.vendor.js +6 -6
  545. package/src/vendor/hyper-undo.vendor.js +1 -1
  546. package/src/vendor/hypercms.vendor.js +169 -84
  547. package/src/vendor/marked.vendor.js +68 -0
  548. package/src/vendor/overtype.vendor.js +994 -0
  549. package/src/vendor/richclay.vendor.js +17 -17
  550. package/src/vendor/sap-core.vendor.js +5 -5
  551. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,1173 @@
1
+ import { isEditMode } from "./is-edit-mode.js";
2
+ import { rowOf, plain } from "./conflict-presentation.js";
3
+ import { downloadRecovery, downloadStored } from "./conflict-download.js";
4
+ import { pendingConflicts, listConflicts, dismissConflicts } from "../recovery/conflicts.js";
5
+ import { set, make } from "../lib/hostile-css.js";
6
+ import { isSaveConflicted } from "./save.js";
7
+ import { gateCaptureToken } from "../lib/dirty-gate.js";
8
+ import { reloadAfterDiscard } from "./unsaved-warning.js";
9
+ import { bevelButton, bevelIconButton, bevelSurface, bevelWell, bevelText, protectIcon, RUNTIME_ONLY } from "../ui/bevel-controls.js";
10
+ import { TOKENS, FONT_SANS, FONT_MONO, GLYPHS } from "../ui/bevel.js";
11
+
12
+ // One notice for everything this tab holds that the page no longer shows: edits
13
+ // another save replaced (the ledger, clay.conflicts) and a save the host refused.
14
+ // It never decides for the person. Minimize, Escape, a save, a reconnect or a later
15
+ // frame leave every loss where it is; only Accept theirs (or Writer's own merge)
16
+ // acknowledges one, and only the ids the panel actually drew.
17
+ //
18
+ // Ledger changes draw on the next frame, not at once: a listener that claims its
19
+ // own records inside clay:sync-applied (Writer) must get there before the first
20
+ // draw, or the bar flashes a count that is not the person's to deal with.
21
+
22
+ export const WARN = "light-dark(#D08A1E, #E3A33F)";
23
+ const ARM_MS = 5000;
24
+ const TOAST_MS = 4000;
25
+ const DOUBLE_PRESS_MS = 500;
26
+
27
+ // The host names what moved the file when it knows. Anything unrecognised falls
28
+ // back to the phrase that is true in every case.
29
+ const SOURCES = {
30
+ "another-tab": "in another tab",
31
+ "another-person": "by someone else",
32
+ "an-agent": "by an agent",
33
+ };
34
+
35
+ const MINIMIZE_SVG = '<svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true"><path d="M2.5 9.5h7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square"/></svg>';
36
+
37
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
38
+
39
+ const BLOCKED_ROW = "This spot changed again. Review the latest edit or download your copy.";
40
+ const DATA_BLOCKED_ROW = "A data value can't be put back from here. Download your copy to keep yours.";
41
+
42
+ let view = "hidden";
43
+ let listOpen = false;
44
+ let renderedIds = [];
45
+ const seenIds = new Set();
46
+ const freshIds = new Set();
47
+ let refused = null;
48
+ let refusalGeneration = 0;
49
+ let upgraded = null;
50
+ let armed = null;
51
+ let armTimer = null;
52
+ let busy = null;
53
+ let keepFailed = false;
54
+ let downloaded = false;
55
+ let downloadFailed = false;
56
+ const blockedRows = new Map();
57
+ let ledgerRevision = 0;
58
+ let frameQueued = false;
59
+ let checkRefusal = false;
60
+ let toastText = null;
61
+ let toastTimer = null;
62
+ let pendingFocus = null;
63
+ let armedAt = 0;
64
+ let armLost = false;
65
+ let keepAttempt = 0;
66
+ let lastAnnounced = null;
67
+ let panelEl = null;
68
+ let recovered = [];
69
+ let recoveredUndo = [];
70
+ let actionFailed = false;
71
+ let copiedId = null;
72
+ let copiedTimer = null;
73
+
74
+ let root = null;
75
+ let wellEl = null;
76
+ let keyed = new Map();
77
+
78
+ const ledger = () => window.clay?.conflicts || null;
79
+ // Conflicts a reload took out of memory. Only the store has them now: a root as the
80
+ // bytes a save would have written, and the losing text as plain fields. There is no
81
+ // live node and no path worth replaying, so nothing here points at the page.
82
+ const storedCount = () => recovered.reduce((sum, record) => sum + record.conflicts.length, 0) + recoveredUndo.length;
83
+ // Keep what a refused undo or redo would have written, and what the page kept itself,
84
+ // are on the undo plugin's own channel. The panel asks the plugin, never the recovery
85
+ // module, so a boot with nothing kept downloads no recovery code beyond this notice.
86
+ const undoApi = () => {
87
+ const api = window.clay?.undo;
88
+ return api && typeof api.kept === "function" ? api : null;
89
+ };
90
+ const losses = () => ledger()?.list().filter((r) => !r.claimedBy && r.kind !== "apply-incomplete") ?? [];
91
+ const failures = () => ledger()?.list().filter((r) => !r.claimedBy && r.kind === "apply-incomplete") ?? [];
92
+
93
+ // A refused save goes first: saving is blocked until it is answered.
94
+ function mode() {
95
+ // Nothing else is asked for once the document itself was upgraded: this tab holds
96
+ // a version the host no longer stores, and the only way out is a reload.
97
+ if (upgraded) return "upgraded";
98
+ if (refused) return "refused";
99
+ if (losses().length) return "replaced";
100
+ if (failures().length) return "failed";
101
+ if (recovered.length || recoveredUndo.length) return "recovered";
102
+ return null;
103
+ }
104
+
105
+ function el(tag, rules, text) {
106
+ const node = make(tag, rules, text);
107
+ if (rules[0] === "all:initial") set(node, "color-scheme", "inherit");
108
+ node.setAttribute("clay", RUNTIME_ONLY);
109
+ return node;
110
+ }
111
+
112
+ function key(node, name) {
113
+ keyed.set(name, node);
114
+ return node;
115
+ }
116
+
117
+ function schemeOf() {
118
+ const s = getComputedStyle(document.documentElement).colorScheme;
119
+ return s === "light" || s === "dark" ? s : "light dark";
120
+ }
121
+
122
+ // A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to
123
+ // the layout viewport, which parks a bottom-anchored bar behind the keyboard at the
124
+ // exact moment somebody is typing. Lift it by the difference instead.
125
+ function place() {
126
+ if (!root) return;
127
+ const vv = window.visualViewport;
128
+ const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
129
+ set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
130
+ if (panelEl) set(panelEl, "max-height", `${Math.max(160, (vv?.height || window.innerHeight) - 32)}px`);
131
+ }
132
+
133
+ function build() {
134
+ root = el("div", [
135
+ "all:initial", "position:fixed", "left:50%", "transform:translateX(-50%)",
136
+ "z-index:2147483001", "display:none", "flex-direction:column", "align-items:center",
137
+ "max-width:calc(100vw - 24px)", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`,
138
+ "text-align:left",
139
+ ]);
140
+ root.setAttribute("data-clay-conflict", "");
141
+ document.body.appendChild(root);
142
+ window.visualViewport?.addEventListener("resize", place);
143
+ window.visualViewport?.addEventListener("scroll", place);
144
+ }
145
+
146
+ export function schedule() {
147
+ if (frameQueued) return;
148
+ frameQueued = true;
149
+ const run = () => { frameQueued = false; render(); };
150
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
151
+ else setTimeout(run, 16);
152
+ }
153
+
154
+ function render() {
155
+ if (checkRefusal) {
156
+ checkRefusal = false;
157
+ if (refused && !isSaveConflicted()) refused = null;
158
+ }
159
+ const m = mode();
160
+ if (!m) {
161
+ view = "hidden";
162
+ listOpen = false;
163
+ seenIds.clear();
164
+ freshIds.clear();
165
+ renderedIds = [];
166
+ disarm();
167
+ lastAnnounced = null;
168
+ armLost = false;
169
+ } else if (view === "hidden") {
170
+ view = "bar";
171
+ }
172
+ if (!root) {
173
+ if (view === "hidden" && !toastText) return;
174
+ if (!document.body) return;
175
+ build();
176
+ }
177
+ // A body replaced wholesale takes the root with it.
178
+ if (!root.isConnected && document.body) document.body.appendChild(root);
179
+
180
+ const active = document.activeElement;
181
+ let focusKey = null;
182
+ for (const [name, node] of keyed) if (node === active) focusKey = name;
183
+ const scroll = wellEl ? wellEl.scrollTop : 0;
184
+ keyed = new Map();
185
+ wellEl = null;
186
+ panelEl = null;
187
+ root.replaceChildren();
188
+ set(root, "color-scheme", schemeOf());
189
+
190
+ if (toastText) {
191
+ root.append(drawToast(toastText));
192
+ } else if (view === "bar") {
193
+ root.append(drawBar(m));
194
+ } else if (view === "panel") {
195
+ root.append(drawPanel(m));
196
+ }
197
+ set(root, "display", toastText || view !== "hidden" ? "flex" : "none");
198
+ if (wellEl) wellEl.scrollTop = scroll;
199
+
200
+ const want = pendingFocus || focusKey;
201
+ if (want && keyed.has(want)) keyed.get(want).focus({ preventScroll: true });
202
+ // A request for a control the toast is standing in for waits until the toast goes.
203
+ if (!toastText) pendingFocus = null;
204
+ place();
205
+ }
206
+
207
+ function dot() {
208
+ return el("span", ["all:initial", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${WARN}`]);
209
+ }
210
+
211
+ function refusedSource() {
212
+ return SOURCES[refused?.changedBy] || null;
213
+ }
214
+
215
+ function barMessage(m, n) {
216
+ if (m === "upgraded") return upgraded;
217
+ if (m === "refused") {
218
+ const named = refusedSource();
219
+ if (!named && refused.afterTimeout) {
220
+ return "This page changed elsewhere, possibly by your own save that timed out. Your edits here are safe.";
221
+ }
222
+ return `This page changed ${named || "elsewhere"}. Your edits here are safe.`;
223
+ }
224
+ if (m === "failed") return "A sync update did not finish. Your edits here are safe.";
225
+ if (m === "recovered") return `${plural(n, "change")} replaced before this page loaded ${n === 1 ? "is" : "are"} still here.`;
226
+ return `Another edit replaced ${plural(n, "change")}.`;
227
+ }
228
+
229
+ function titleOf(m, n) {
230
+ if (m === "upgraded") return upgraded;
231
+ if (m === "refused") return `This page changed ${refusedSource() || "elsewhere"}`;
232
+ if (m === "failed") return "A sync update did not finish";
233
+ if (m === "recovered") return `${plural(n, "change")} kept from before this page loaded`;
234
+ return `Another edit replaced ${plural(n, "change")}`;
235
+ }
236
+
237
+ function drawToast(text) {
238
+ const t = bevelSurface("div", ["display:block", `border-left:3px solid ${TOKENS.teal}`, "padding:10px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
239
+ t.setAttribute("role", "status");
240
+ t.textContent = text;
241
+ return t;
242
+ }
243
+
244
+ function drawBar(m) {
245
+ const n = m === "failed" ? failures().length : m === "recovered" ? storedCount() : losses().length;
246
+ const bar = bevelSurface("div", ["display:flex", "flex-wrap:wrap", "align-items:center", "gap:10px 12px", "padding:8px 8px 8px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
247
+ // A redraw rebuilds the bar; only a new message is announced again.
248
+ const message = barMessage(m, n);
249
+ if (message !== lastAnnounced) {
250
+ bar.setAttribute("role", m === "refused" ? "alert" : "status");
251
+ lastAnnounced = message;
252
+ }
253
+ bar.append(dot(), bevelText("span", [], message));
254
+ if (m === "upgraded") {
255
+ bar.append(key(bevelButton("Reload", { variant: "primary", onClick: () => window.location.reload() }), "reload"));
256
+ return bar;
257
+ }
258
+ bar.append(key(bevelButton("Review", { onClick: openPanel }), "review"));
259
+ return bar;
260
+ }
261
+
262
+ function action(label, name, { variant = "default", onClick, push = false } = {}) {
263
+ const phone = window.innerWidth < 480;
264
+ const extra = phone
265
+ ? ["flex:1 1 100%", ...(variant === "primary" ? ["order:-1"] : [])]
266
+ : (push ? ["margin-left:auto"] : []);
267
+ return key(bevelButton(label, { variant, onClick, extra }), name);
268
+ }
269
+
270
+ function lede(text) {
271
+ return bevelText("div", ["display:block", "padding:0 16px 12px", `color:${TOKENS["ink-2"]}`, "font-size:13.5px"], text);
272
+ }
273
+
274
+ function drawPanel(m) {
275
+ const rows = m === "replaced" ? losses() : m === "failed" ? failures() : [];
276
+ const held = m === "refused" ? losses() : rows;
277
+ const count = m === "recovered" ? storedCount() : rows.length;
278
+ if (seenIds.size) for (const r of rows) if (!seenIds.has(r.id)) freshIds.add(r.id);
279
+ for (const id of [...freshIds]) if (!rows.some((r) => r.id === id)) freshIds.delete(id);
280
+
281
+ const title = titleOf(m, count);
282
+ const maxHeight = Math.max(160, (window.visualViewport?.height || window.innerHeight) - 32);
283
+ const panel = bevelSurface("div", ["display:flex", "flex-direction:column", "width:520px", "max-width:calc(100vw - 24px)", `max-height:${maxHeight}px`]);
284
+ panelEl = panel;
285
+ panel.setAttribute("role", "dialog");
286
+ panel.setAttribute("aria-label", title);
287
+
288
+ const head = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", "padding:12px 12px 8px 16px", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`]);
289
+ const t = key(bevelText("b", ["font-weight:600", "font-size:14.5px", "outline:none"], title), "title");
290
+ t.setAttribute("tabindex", "-1");
291
+ head.append(dot(), t);
292
+ if (m === "replaced" && freshIds.size) {
293
+ head.append(bevelText("span", [
294
+ `font:600 10.5px/1 ${FONT_MONO}`, "letter-spacing:.12em", "text-transform:uppercase", "padding:3px 6px",
295
+ `color:color-mix(in srgb, ${TOKENS.brass} 80%, ${TOKENS.ink})`, `background:${TOKENS["brass-soft"]}`,
296
+ ], `${freshIds.size} new`));
297
+ }
298
+ const minimizeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
299
+ const minimize = key(bevelIconButton(MINIMIZE_SVG, { label: "Minimize", onClick: minimizePanel }), "minimize");
300
+ minimize.title = "Minimize to the bar. Nothing is accepted until you choose.";
301
+ minimizeWrap.append(minimize);
302
+ head.append(minimizeWrap);
303
+ panel.append(head);
304
+
305
+ if (m === "recovered") {
306
+ drawRecovered(panel);
307
+ return panel;
308
+ }
309
+
310
+ if (m === "refused") {
311
+ panel.append(lede("Your edits here are safe and not saved yet; nothing will be overwritten until you choose. Their version is not on this page, so there is no list of edits to compare. Keep yours, which saves over theirs, or accept theirs, which reloads the page and drops your unsaved edits."));
312
+ if (held.length) {
313
+ panel.append(lede(`${plural(held.length, "earlier replaced edit")} ${held.length === 1 ? "is" : "are"} also held in this tab. Download my copy includes them.`));
314
+ }
315
+ if (keepFailed) panel.append(lede("That did not save either. Your edits here are still here."));
316
+ if (armLost) panel.append(lede("Something changed on the page after your first press, so nothing was dropped. Press Accept theirs again if you still want their version."));
317
+ if (armed) {
318
+ const warn = el("div", [
319
+ "all:initial", "display:block", "margin:0 16px 12px", "padding:8px 10px",
320
+ `border-left:3px solid ${WARN}`, `background:color-mix(in srgb, ${WARN} 14%, transparent)`,
321
+ `color:${TOKENS.ink}`, `font:13px/1.5 ${FONT_SANS}`,
322
+ ]);
323
+ warn.append(el("b", ["all:initial", "font:inherit", "font-weight:700", "color:inherit"], "This drops your unsaved edits."), document.createTextNode(" Download your copy first if you might want them. Press again to confirm."));
324
+ panel.append(warn);
325
+ }
326
+ } else if (m === "upgraded") {
327
+ panel.append(lede("Its layout and code changed with it, so this page stops here. Reload to see the new version."));
328
+ } else if (m === "failed") {
329
+ panel.append(lede("Part of an update from elsewhere could not be applied, so this page may be missing some of it. Your edits here are safe. Download a copy of the page as you had it, or accept to keep the page as it is."));
330
+ } else {
331
+ panel.append(lede("The page is showing the other edit. This stays until you choose. You can also save a copy of the page as you had it."));
332
+ }
333
+
334
+ const actions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]);
335
+ if (m === "upgraded") {
336
+ actions.append(action("Reload", "reload", { variant: "primary", onClick: () => window.location.reload(), push: true }));
337
+ panel.append(actions);
338
+ renderedIds = [];
339
+ return panel;
340
+ }
341
+ const dl = action(downloadFailed ? "Try again" : downloaded ? "Downloaded" : "Download my copy", "download", { onClick: onDownload });
342
+ actions.append(dl);
343
+ if (m === "refused") {
344
+ const keep = action(busy === "overwrite" ? "Saving…" : keepFailed ? "Try again" : "Keep mine", "keep", { onClick: onKeep });
345
+ keep.setDisabled(busy === "overwrite");
346
+ actions.append(keep, action(armed ? "Yes, drop my edits" : "Accept theirs", "accept", { variant: "primary", onClick: onRefusedAccept, push: true }));
347
+ } else {
348
+ if (m === "replaced") {
349
+ const pending = ledger().hasPendingApply();
350
+ const put = action(pending ? "Finishing the incoming edit…" : busy === "revert" ? "Putting back…" : "Revert to mine", "revert", { onClick: onRevert });
351
+ put.setDisabled(pending || busy === "revert");
352
+ actions.append(put);
353
+ }
354
+ actions.append(action("Accept theirs", "accept", { variant: "primary", onClick: onAccept, push: true }));
355
+ }
356
+ panel.append(actions);
357
+
358
+ if (m === "replaced") {
359
+ if (!listOpen) {
360
+ const more = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", `border-top:1px solid ${TOKENS.line}`, "padding:8px 10px 8px 16px", `color:${TOKENS.muted}`, `font:13px/1.5 ${FONT_SANS}`]);
361
+ const seeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
362
+ seeWrap.append(key(bevelButton("See the edits", { small: true, onClick: toggleList }), "list"));
363
+ more.append(el("span", ["all:initial", "font:inherit", "color:inherit"], plural(rows.length, "edit")), seeWrap);
364
+ panel.append(more);
365
+ } else {
366
+ panel.append(drawWell(rows));
367
+ }
368
+ }
369
+
370
+ renderedIds = held.map((r) => r.id);
371
+ for (const r of rows) seenIds.add(r.id);
372
+ return panel;
373
+ }
374
+
375
+ function drawWell(rows) {
376
+ // The sunk ground, not the recess: the recess's lighter blue glares against the panel in dark mode.
377
+ const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`]);
378
+ const head = el("div", ["all:initial", "position:sticky", "top:0", "z-index:1", "display:flex", "align-items:center", "gap:8px", "padding:8px 12px", `background:${TOKENS.sunk}`, `border-bottom:1px solid ${TOKENS["line-2"]}`, `color:${TOKENS.muted}`, `font:600 10.5px/1 ${FONT_MONO}`, "letter-spacing:.12em", "text-transform:uppercase"]);
379
+ const hideWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
380
+ hideWrap.append(key(bevelButton("Hide", { small: true, variant: "quiet", onClick: toggleList }), "list"));
381
+ head.append(el("span", ["all:initial", "font:inherit", "color:inherit", "letter-spacing:inherit", "text-transform:inherit"], "Replaced edits"), hideWrap);
382
+ well.append(head);
383
+ rows.forEach((r, i) => well.append(buildRow(r, i)));
384
+ wellEl = well;
385
+ return well;
386
+ }
387
+
388
+ // A stored conflict's name: the app's own label when it registered one, and the
389
+ // generic name the ledger recorded otherwise. Never guessed from today's route.
390
+ function storedName(entry) {
391
+ const base = entry.context?.label || entry.name || (entry.context?.kind === "entry" ? "Entry" : "Edit");
392
+ return entry.context?.field ? `${base} \u00b7 ${entry.context.field}` : base;
393
+ }
394
+
395
+ // Why a refused step was not applied, in one phrase per code the engine reports.
396
+ const UNDO_REASONS = {
397
+ "content-changed": "the page changed there",
398
+ "structure-changed": "the structure changed there",
399
+ "target-detached": "that part of the page is gone",
400
+ "duplicate-id": "another part of the page now has the same id",
401
+ "protected": "that region keeps its own history",
402
+ "unsupported": "this kind of step cannot be put back",
403
+ };
404
+
405
+ const undoReason = (code) => UNDO_REASONS[code] || "the page changed there";
406
+
407
+ // The text a refused step would have written, and the spot it would have written it
408
+ // into. A text or value step holds both endpoints, so the one for this direction is
409
+ // the one a replay would write; an add or a remove holds the markup of its nodes.
410
+ function writtenBy(steps, direction) {
411
+ const list = Array.isArray(steps) ? steps : [];
412
+ for (const step of list) {
413
+ if (!step || (step.kind !== "text" && step.kind !== "value")) continue;
414
+ const wrote = direction === "redo" ? step.after : step.before;
415
+ if (typeof wrote === "string" && wrote) return { text: wrote, id: step.target?.id ?? null };
416
+ }
417
+ for (const step of list) {
418
+ if (!step || (step.kind !== "add" && step.kind !== "remove")) continue;
419
+ const node = Array.isArray(step.nodes) ? step.nodes.find((item) => typeof item?.html === "string" && item.html) : null;
420
+ if (node) return { text: plain(node.html).replace(/\s+/g, " ").trim(), id: step.parent?.id ?? null };
421
+ }
422
+ for (const step of list) {
423
+ if (!step) continue;
424
+ const wrote = direction === "redo" ? step.after : step.before;
425
+ if (typeof wrote === "string" && wrote) return { text: wrote, id: step.target?.id ?? null };
426
+ }
427
+ return { text: "", id: null };
428
+ }
429
+
430
+ // The page as it reads there today, when the spot an old step names is still on it.
431
+ function liveText(id) {
432
+ if (id == null || typeof document === "undefined") return "";
433
+ const escaped = window.CSS?.escape ? window.CSS.escape(String(id)) : String(id).replace(/["\\]/g, "\\$&");
434
+ let node = null;
435
+ try {
436
+ node = document.querySelector(`[data-id="${escaped}"]`);
437
+ } catch (_) {
438
+ return "";
439
+ }
440
+ if (!node) return "";
441
+ const field = node.matches?.("input, textarea, select") ? node.value : null;
442
+ const text = typeof field === "string" ? field : node.textContent ?? "";
443
+ return text.replace(/\s+/g, " ").trim();
444
+ }
445
+
446
+ // One kept undo record as a row. A refused step says what was not applied and quotes
447
+ // both sides; a copy the app kept shows its own name and text. Neither points at a
448
+ // live node: an old step's target may be gone, and only its own id survived.
449
+ function undoRow(record) {
450
+ const payload = record?.payload && typeof record.payload === "object" ? record.payload : {};
451
+ const key = String(record?.key ?? "");
452
+ const base = { id: record?.id, revision: record?.revision, record, kind: record?.kind ?? "conflict" };
453
+ if (key.startsWith("app:")) {
454
+ return {
455
+ ...base,
456
+ name: typeof payload.label === "string" && payload.label ? payload.label : key.slice(4),
457
+ sentence: "",
458
+ yours: typeof payload.text === "string" ? payload.text : "",
459
+ now: "",
460
+ };
461
+ }
462
+ const direction = payload.direction === "redo" ? "redo" : "undo";
463
+ const contexts = Array.isArray(payload.context) ? payload.context : [];
464
+ const named = contexts.find((item) => typeof item?.label === "string" && item.label) ?? null;
465
+ const name = named?.label || (typeof payload.label === "string" && payload.label) || "Edit";
466
+ const wrote = writtenBy(payload.steps, direction);
467
+ return {
468
+ ...base,
469
+ name,
470
+ sentence: `The ${direction} of "${name}" was not applied because ${undoReason(payload.code)}.`,
471
+ yours: wrote.text,
472
+ now: wrote.id == null ? "" : liveText(wrote.id),
473
+ };
474
+ }
475
+
476
+ // The page's own buttons for one kept record, asked of the undo plugin on every draw.
477
+ function undoActionsOf(entry) {
478
+ const api = undoApi();
479
+ if (!entry.record || typeof api?.reviewActionsFor !== "function") return [];
480
+ try {
481
+ return (api.reviewActionsFor(entry.record) || []).filter((item) => item && typeof item.run === "function" && item.label);
482
+ } catch (_) {
483
+ return [];
484
+ }
485
+ }
486
+
487
+ // Stored text is already plain: no markup, no live node, nothing to parse.
488
+ const storedSide = (text) => ({ before: "", hit: text, after: "", cutBefore: false, cutAfter: false });
489
+
490
+ function storedRow(entry) {
491
+ const box = bevelText("div", ["display:block", "padding:8px 12px 10px"]);
492
+ const where = bevelText("div", ["display:flex", "align-items:center", "gap:8px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"]);
493
+ // No eye: there is no live element to ring, and an old recovery path may name a
494
+ // place today's page no longer has.
495
+ where.append(bevelText("span", [], storedName(entry)));
496
+ box.append(where);
497
+ if (entry.sentence) box.append(bevelText("div", ["display:block", "font-size:13px", `color:${TOKENS["ink-2"]}`], entry.sentence));
498
+ if (entry.yours) {
499
+ const grid = bevelText("div", ["display:grid", "grid-template-columns:44px 1fr", "gap:3px 10px", "align-items:start", `font:12.5px/1.5 ${FONT_MONO}`, ...(entry.sentence ? ["margin-top:4px"] : [])]);
500
+ grid.append(...valueLine("Yours", storedSide(entry.yours), { mine: true }));
501
+ if (entry.now) grid.append(...valueLine("Now", storedSide(entry.now), { mine: false }));
502
+ box.append(grid);
503
+ }
504
+ const rowActions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "margin-top:8px"]);
505
+ undoActionsOf(entry).forEach((item, at) => {
506
+ const rowAction = action(item.label, `run:${entry.id}:${at}`, { onClick: () => onUndoAction(entry, item, rowAction) });
507
+ rowActions.append(rowAction);
508
+ });
509
+ rowActions.append(action(copiedId === entry.id ? "Copied" : "Copy", `copy:${entry.id}`, { onClick: () => onCopyStored(entry) }));
510
+ rowActions.append(action("Dismiss", `dismiss:${entry.id}`, { onClick: () => onDismissStored(entry) }));
511
+ box.append(rowActions);
512
+ return box;
513
+ }
514
+
515
+ // Review for copies a reload left behind. Every action here reads the stored record;
516
+ // none of them touches the tree, and dismissal is the only thing that removes one.
517
+ function drawRecovered(panel) {
518
+ panel.append(lede(recovered.length
519
+ ? "These copies were replaced before this page loaded. The page never puts them back on its own. Copy or download the ones you want; dismiss one when you no longer need it."
520
+ : "These copies were kept because an undo could not be put back or the page set them aside. The page never puts them back on its own. Copy or download the ones you want; dismiss one when you no longer need it."));
521
+ if (actionFailed) panel.append(lede("That action did not run. The copy is still here."));
522
+ const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`]);
523
+ for (const record of recovered) for (const entry of record.conflicts) well.append(storedRow(entry));
524
+ for (const entry of recoveredUndo) well.append(storedRow(entry));
525
+ wellEl = well;
526
+ panel.append(well);
527
+ const actions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]);
528
+ if (recovered.some((record) => record.root) || recoveredUndo.length) actions.append(action(downloaded ? "Downloaded" : "Download my copy", "download", { onClick: onDownloadRecovered }));
529
+ panel.append(actions);
530
+ renderedIds = [];
531
+ }
532
+
533
+ function valueLine(label, value, { mine }) {
534
+ const cells = [bevelText("span", [`color:${TOKENS.faint}`, `font-family:${FONT_SANS}`], label)];
535
+ const box = bevelText("span", [
536
+ "display:-webkit-box", "-webkit-line-clamp:2", "-webkit-box-orient:vertical", "overflow:hidden",
537
+ "overflow-wrap:anywhere",
538
+ ...(mine
539
+ ? [`color:${TOKENS.ink}`]
540
+ : [`color:${TOKENS.muted}`]),
541
+ ]);
542
+ const full = (value.cutBefore ? "…" : "") + value.before + value.hit + value.after + (value.cutAfter ? "…" : "");
543
+ box.title = full;
544
+ if (value.before) box.append(bevelText("span", [`color:${TOKENS.faint}`], (value.cutBefore ? "…" : "") + value.before));
545
+ const hit = bevelText("span", mine
546
+ ? [`background:color-mix(in srgb, ${WARN} 18%, transparent)`, "padding:0 4px"]
547
+ : (value.before || value.after
548
+ ? ["text-decoration:underline", `text-decoration-color:color-mix(in srgb, ${WARN} 70%, transparent)`, "text-underline-offset:3px"]
549
+ : []), value.hit || "(empty)");
550
+ box.append(hit);
551
+ if (value.after) box.append(bevelText("span", [`color:${TOKENS.faint}`], value.after + (value.cutAfter ? "…" : "")));
552
+ cells.push(box);
553
+ return cells;
554
+ }
555
+
556
+ function buildRow(record, index) {
557
+ const row = rowOf(record);
558
+ const box = bevelText("div", ["display:block", "padding:8px 12px 10px", ...(index ? [`border-top:1px solid ${TOKENS.line}`] : [])]);
559
+ const where = bevelText("div", ["display:flex", "align-items:center", "gap:4px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"]);
560
+ where.append(bevelText("span", [], row.name));
561
+ if (row.target && row.target.isConnected) where.append(key(eyeButton(row), `eye:${record.id}`));
562
+ box.append(where);
563
+ if (row.sentence) box.append(bevelText("div", ["display:block", "font-size:13px", `color:${TOKENS["ink-2"]}`], row.sentence));
564
+ if (blockedRows.has(record.id)) box.append(bevelText("div", ["display:block", "font-size:13px", "margin-top:4px", `color:${WARN}`], blockedRows.get(record.id)));
565
+ if (row.yours) {
566
+ const grid = bevelText("div", ["display:grid", "grid-template-columns:44px 1fr", "gap:3px 10px", "align-items:start", `font:12.5px/1.5 ${FONT_MONO}`, ...(row.sentence ? ["margin-top:4px"] : [])]);
567
+ grid.append(...valueLine("Yours", row.yours, { mine: true }));
568
+ if (row.now) grid.append(...valueLine("Now", row.now, { mine: false }));
569
+ box.append(grid);
570
+ }
571
+ return box;
572
+ }
573
+
574
+ function eyeButton(row) {
575
+ const base = ["all:initial", "color-scheme:inherit", "box-sizing:border-box", "cursor:pointer", "display:inline-grid", "place-items:center", "width:22px", "height:20px", `color:${TOKENS.ink}`];
576
+ const b = el("button", base);
577
+ b.type = "button";
578
+ b.innerHTML = GLYPHS.conflictEye;
579
+ protectIcon(b.firstElementChild, 16);
580
+ b.setAttribute("aria-label", `Show ${row.name} on page`);
581
+ b.title = "Show on page";
582
+ const paint = (extra) => { b.style.cssText = ""; for (const r of [...base, ...extra]) { const i = r.indexOf(":"); b.style.setProperty(r.slice(0, i), r.slice(i + 1), "important"); } };
583
+ b.addEventListener("pointerenter", () => paint([`background:color-mix(in srgb, ${WARN} 16%, transparent)`]));
584
+ b.addEventListener("pointerleave", () => paint([]));
585
+ b.addEventListener("focus", () => paint([`outline:2px solid ${TOKENS.brass}`, "outline-offset:1px"]));
586
+ b.addEventListener("blur", () => paint([]));
587
+ b.addEventListener("click", () => ring(row.target));
588
+ return b;
589
+ }
590
+
591
+ // The eye: scroll the element into view and draw a ring over it. The ring is ours
592
+ // and runtime-only; the page's element gets no attribute, class or style, so the
593
+ // dirty gate and the saved bytes see nothing.
594
+ let ringEl = null;
595
+ let ringTimer = null;
596
+ let ringTarget = null;
597
+
598
+ function dropRing() {
599
+ clearTimeout(ringTimer);
600
+ window.removeEventListener("scroll", placeRing, true);
601
+ window.removeEventListener("resize", placeRing);
602
+ ringEl?.remove();
603
+ ringEl = null;
604
+ ringTarget = null;
605
+ }
606
+
607
+ function placeRing() {
608
+ if (!ringEl || !ringTarget) return;
609
+ const r = ringTarget.getBoundingClientRect();
610
+ set(ringEl, "left", `${r.left}px`);
611
+ set(ringEl, "top", `${r.top}px`);
612
+ set(ringEl, "width", `${r.width}px`);
613
+ set(ringEl, "height", `${r.height}px`);
614
+ }
615
+
616
+ function ring(target) {
617
+ dropRing();
618
+ if (!target || !target.isConnected) return;
619
+ const still = !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
620
+ const behavior = still ? "instant" : "smooth";
621
+ // The notice sits at the bottom of the viewport: centre the target in what is left
622
+ // above it, or the outline lands on the panel instead of the text.
623
+ const top = root?.getBoundingClientRect().top ?? 0;
624
+ const cover = root && top > 0 ? Math.max(0, window.innerHeight - top) : 0;
625
+ if (!cover) {
626
+ target.scrollIntoView?.({ block: "center", behavior });
627
+ } else {
628
+ // Centre first, so a page whose own scroller holds the target still brings it
629
+ // up; then move the window the rest of the way. A target taller than the free
630
+ // space shows its top instead.
631
+ target.scrollIntoView?.({ block: "center", behavior: "instant" });
632
+ const r = target.getBoundingClientRect();
633
+ const free = window.innerHeight - cover;
634
+ const shift = r.height > free - 24 ? r.top - 12 : r.top + r.height / 2 - free / 2;
635
+ window.scrollBy?.({ top: shift, behavior });
636
+ }
637
+ ringTarget = target;
638
+ ringEl = el("div", ["all:initial", "color-scheme:inherit", "position:fixed", "pointer-events:none", "z-index:2147483001", `outline:2px solid ${WARN}`, "outline-offset:3px", `background:color-mix(in srgb, ${WARN} 14%, transparent)`]);
639
+ document.body.appendChild(ringEl);
640
+ placeRing();
641
+ window.addEventListener("scroll", placeRing, true);
642
+ window.addEventListener("resize", placeRing);
643
+ ringTimer = setTimeout(dropRing, 2500);
644
+ }
645
+
646
+ function openPanel() {
647
+ if (upgraded) return;
648
+ view = "panel";
649
+ pendingFocus = "title";
650
+ render();
651
+ }
652
+
653
+ function minimizePanel() {
654
+ dropRing();
655
+ view = "bar";
656
+ disarm();
657
+ pendingFocus = "review";
658
+ render();
659
+ }
660
+
661
+ function toggleList() {
662
+ listOpen = !listOpen;
663
+ if (listOpen) freshIds.clear();
664
+ render();
665
+ }
666
+
667
+ // The ids copied before anything else runs: a record that landed after the draw was never shown, so this choice does not cover it, and a record something claimed since then is its owner's to settle.
668
+ function onAccept() {
669
+ const l = ledger();
670
+ const ids = renderedIds.filter((id) => { const r = l?.get(id); return r && !r.claimedBy; });
671
+ if (l && ids.length) l.acknowledge(ids, { reason: "accepted" });
672
+ view = mode() ? "bar" : "hidden";
673
+ pendingFocus = "review";
674
+ schedule();
675
+ }
676
+
677
+ // Revert to mine. The ids are the rendered ones, copied before anything runs, as
678
+ // Accept reads them. The module that does the work loads on first use, and only
679
+ // here, where the sync plugin has published the ledger. What it could not put back
680
+ // keeps its row, with the reason under it, until the ledger moves on.
681
+ async function onRevert() {
682
+ const l = ledger();
683
+ if (busy || !l || l.hasPendingApply()) return;
684
+ const ids = renderedIds.filter((id) => { const r = l.get(id); return r && !r.claimedBy; });
685
+ if (!ids.length) return;
686
+ busy = "revert";
687
+ render();
688
+ // Focus that lands outside the notice while the revert and its save run is the
689
+ // person moving on; Review takes focus afterwards only if it stayed here.
690
+ let stayed = true;
691
+ const track = (e) => { stayed = !!root?.contains(e.target); };
692
+ document.addEventListener("focus", track, true);
693
+ let result;
694
+ try {
695
+ const { revertConflicts } = await import("../sync/conflict-revert.js");
696
+ result = await revertConflicts(ids);
697
+ } catch (err) {
698
+ console.error("[clay] Revert to mine did not finish", err);
699
+ result = { revertedIds: [], blockedIds: ids, saveResult: null };
700
+ } finally {
701
+ document.removeEventListener("focus", track, true);
702
+ }
703
+ busy = null;
704
+ for (const id of result.blockedIds) blockedRows.set(id, ledger()?.get(id)?.kind === "json" ? DATA_BLOCKED_ROW : BLOCKED_ROW);
705
+ view = mode() ? "bar" : "hidden";
706
+ pendingFocus = stayed ? "review" : null;
707
+ const saving = result.saveResult && (result.saveResult.ok || result.saveResult.msg === "Save already in progress");
708
+ if (result.revertedIds.length && saving) toast(`Put back ${plural(result.revertedIds.length, "change")}. Saving.`);
709
+ else render();
710
+ }
711
+
712
+ let downloadedTimer = null;
713
+
714
+ // Download my copy. Nothing here acknowledges or releases a record.
715
+ function onDownload() {
716
+ const l = ledger();
717
+ // A refusal ends in a reload that takes every record with it, so its copy carries
718
+ // all of them; a replaced notice covers the rows it drew.
719
+ const records = refused ? (l?.list() ?? []) : renderedIds.map((id) => l?.get(id)).filter(Boolean);
720
+ const ok = downloadRecovery(records, { refused: !!refused });
721
+ downloadFailed = !ok;
722
+ downloaded = ok;
723
+ clearTimeout(downloadedTimer);
724
+ if (ok) downloadedTimer = setTimeout(() => { downloaded = false; schedule(); }, 1200);
725
+ render();
726
+ }
727
+
728
+ // Read what the store holds. Nothing is drawn until a transaction has answered, and
729
+ // a failed read leaves whatever was last shown alone.
730
+ async function refreshRecovered() {
731
+ let listed = null;
732
+ if (pendingConflicts()) {
733
+ try {
734
+ listed = await listConflicts();
735
+ } catch (_) {
736
+ listed = null;
737
+ }
738
+ }
739
+ const api = undoApi();
740
+ let keptUndo = null;
741
+ if (api) {
742
+ try {
743
+ keptUndo = await api.kept();
744
+ } catch (_) {
745
+ keptUndo = null;
746
+ }
747
+ }
748
+ let moved = false;
749
+ if (listed?.ok) {
750
+ recovered = listed.records;
751
+ moved = true;
752
+ } else if (!pendingConflicts() && recovered.length) {
753
+ recovered = [];
754
+ moved = true;
755
+ }
756
+ if (keptUndo?.ok) {
757
+ const rows = (Array.isArray(keptUndo.records) ? keptUndo.records : [])
758
+ .filter((record) => record && record.supported !== false)
759
+ .map(undoRow);
760
+ const same = rows.length === recoveredUndo.length
761
+ && rows.every((row, at) => row.id === recoveredUndo[at].id && row.revision === recoveredUndo[at].revision);
762
+ if (!same) moved = true;
763
+ recoveredUndo = rows;
764
+ } else if (keptUndo && recoveredUndo.length) {
765
+ // The store answered and holds nothing for us now.
766
+ recoveredUndo = [];
767
+ moved = true;
768
+ }
769
+ if (moved) schedule();
770
+ }
771
+
772
+ async function copyText(text) {
773
+ try {
774
+ await navigator.clipboard.writeText(text);
775
+ return true;
776
+ } catch (_) {}
777
+ const field = document.createElement("textarea");
778
+ field.value = text;
779
+ field.setAttribute("clay", RUNTIME_ONLY);
780
+ field.style.setProperty("position", "fixed", "important");
781
+ field.style.setProperty("opacity", "0", "important");
782
+ document.body?.append(field);
783
+ field.select();
784
+ let ok = false;
785
+ try {
786
+ ok = document.execCommand?.("copy") === true;
787
+ } catch (_) {}
788
+ field.remove();
789
+ return ok;
790
+ }
791
+
792
+ // Copying is not acknowledging: the copy stays until the person dismisses it, and the
793
+ // notice stays on screen rather than standing a toast in for itself.
794
+ async function onCopyStored(entry) {
795
+ const text = entry.record ? entry.yours : `${storedName(entry)}\n\nYours:\n${entry.yours}\n\nNow:\n${entry.now}`;
796
+ const ok = await copyText(text);
797
+ if (!ok) {
798
+ toast("Could not copy. Select the text instead.");
799
+ return;
800
+ }
801
+ copiedId = entry.id;
802
+ clearTimeout(copiedTimer);
803
+ copiedTimer = setTimeout(() => { copiedId = null; schedule(); }, 1200);
804
+ render();
805
+ }
806
+
807
+ // Download my copy, from the stored root: the page as this tab had it, with the
808
+ // losing text in it. Nothing here acknowledges or releases a record.
809
+ function onDownloadRecovered() {
810
+ const record = recovered.find((item) => item.root);
811
+ const edits = [];
812
+ for (const item of recovered) {
813
+ for (const entry of item.conflicts) edits.push({ id: entry.id, kind: entry.kind, name: storedName(entry), yours: entry.yours, now: entry.now, sentence: entry.sentence });
814
+ }
815
+ for (const entry of recoveredUndo) edits.push({ id: entry.id, kind: entry.kind, name: entry.name, yours: entry.yours, now: entry.now, sentence: entry.sentence });
816
+ const ok = downloadStored({ html: record?.root ?? null, edits });
817
+ downloadFailed = !ok;
818
+ downloaded = ok;
819
+ clearTimeout(downloadedTimer);
820
+ if (ok) downloadedTimer = setTimeout(() => { downloaded = false; schedule(); }, 1200);
821
+ render();
822
+ }
823
+
824
+ async function onDismissStored(entry) {
825
+ let receipt;
826
+ try {
827
+ receipt = entry.record
828
+ ? await undoApi()?.dismissKept(entry.id, { revision: entry.revision })
829
+ : await dismissConflicts([entry.id]);
830
+ receipt ||= { ok: false };
831
+ } catch (_) {
832
+ receipt = { ok: false };
833
+ }
834
+ if (!receipt.ok) toast("That copy could not be dismissed.");
835
+ await refreshRecovered();
836
+ schedule();
837
+ }
838
+
839
+ // One activation per row action: while its run is pending the button is disabled and
840
+ // busy, and further activations are ignored, so the page's own action cannot apply
841
+ // twice at once. The button is the one that was drawn with this row; a redraw during
842
+ // the run leaves it detached and there is nothing to hand back.
843
+ const runningActions = new Set();
844
+
845
+ // An action the page offers for one kept record: Keep as new, on a later step. It may
846
+ // be async, and it answers whether the copy was dealt with. Only a run that says both
847
+ // ok and dismiss takes the row away, and only at the revision the row was drawn at: a
848
+ // throw or a refusal leaves the copy where it is and says so above the list.
849
+ async function onUndoAction(entry, item, button = null) {
850
+ if (runningActions.has(item)) return;
851
+ runningActions.add(item);
852
+ if (button) {
853
+ button.disabled = true;
854
+ button.setAttribute("aria-busy", "true");
855
+ }
856
+ actionFailed = false;
857
+ try {
858
+ let result = null;
859
+ try {
860
+ result = await item.run(entry.record);
861
+ } catch (_) {
862
+ result = null;
863
+ }
864
+ if (result?.ok === true && result?.dismiss === true) {
865
+ await onDismissStored(entry);
866
+ return;
867
+ }
868
+ actionFailed = result?.ok !== true;
869
+ await refreshRecovered();
870
+ schedule();
871
+ } finally {
872
+ runningActions.delete(item);
873
+ if (button && button.isConnected) {
874
+ button.disabled = false;
875
+ button.removeAttribute("aria-busy");
876
+ }
877
+ }
878
+ }
879
+
880
+ // A refusal this tab just kept is reviewable the moment the store has it: the same
881
+ // bar and Review a stored conflict gets, so no new chrome and no new entry point.
882
+ function onUndoKept(e) {
883
+ if (e?.detail?.ok !== true) return;
884
+ actionFailed = false;
885
+ refreshRecovered();
886
+ schedule();
887
+ }
888
+
889
+ function disarm() {
890
+ clearTimeout(armTimer);
891
+ armTimer = null;
892
+ armed = null;
893
+ }
894
+
895
+ async function onKeep() {
896
+ if (busy) return;
897
+ const attempt = ++keepAttempt;
898
+ busy = "overwrite";
899
+ keepFailed = false;
900
+ disarm();
901
+ render();
902
+ let result;
903
+ try {
904
+ result = await window.clay?.save?.overwrite?.();
905
+ } catch {
906
+ result = { ok: false };
907
+ }
908
+ // A newer attempt, a new refusal or a landed save already settled this one.
909
+ if (attempt !== keepAttempt || busy !== "overwrite") return;
910
+ if (!result?.ok && isSaveConflicted()) {
911
+ // Left showing: the save still has not happened.
912
+ busy = null;
913
+ keepFailed = true;
914
+ }
915
+ schedule();
916
+ }
917
+
918
+ // Two presses. The first is a choice, not a confirmation, and this is the one
919
+ // control here that throws away work nothing can bring back. The second only
920
+ // counts if nothing changed in between: a new refusal, a new loss or new typing
921
+ // each disarm it.
922
+ function onRefusedAccept() {
923
+ if (!armed) {
924
+ armed = { refusalGeneration, ledgerRevision, dirtyGeneration: gateCaptureToken().gen };
925
+ armedAt = Date.now();
926
+ armLost = false;
927
+ clearTimeout(armTimer);
928
+ armTimer = setTimeout(() => { disarm(); schedule(); }, ARM_MS);
929
+ render();
930
+ return;
931
+ }
932
+ // The second half of a double click is not a second decision.
933
+ if (Date.now() - armedAt < DOUBLE_PRESS_MS) return;
934
+ const token = armed;
935
+ const current = () =>
936
+ armed === token &&
937
+ token.refusalGeneration === refusalGeneration &&
938
+ token.ledgerRevision === ledgerRevision &&
939
+ token.dirtyGeneration === gateCaptureToken().gen;
940
+ if (!reloadAfterDiscard({ isCurrent: current, reload: () => window.location.reload() })) {
941
+ disarm();
942
+ armLost = true;
943
+ render();
944
+ }
945
+ }
946
+
947
+ export function toast(text) {
948
+ toastText = text;
949
+ clearTimeout(toastTimer);
950
+ toastTimer = setTimeout(() => { toastText = null; schedule(); }, TOAST_MS);
951
+ render();
952
+ }
953
+
954
+ function onStoreChanged() {
955
+ refreshRecovered();
956
+ schedule();
957
+ }
958
+
959
+ function onLedgerChanged() {
960
+ ledgerRevision++;
961
+ if (armed) disarm();
962
+ for (const id of [...blockedRows.keys()]) if (!ledger()?.get(id)) blockedRows.delete(id);
963
+ refreshRecovered();
964
+ schedule();
965
+ }
966
+
967
+ function onUpgraded(e) {
968
+ upgraded = (e?.detail && e.detail.message) || "This document was upgraded.";
969
+ toastText = null;
970
+ clearTimeout(toastTimer);
971
+ disarm();
972
+ busy = null;
973
+ keepFailed = false;
974
+ armLost = false;
975
+ view = "bar";
976
+ schedule();
977
+ }
978
+
979
+ function onSaveConflict(e) {
980
+ const d = e?.detail || {};
981
+ refused = { generation: ++refusalGeneration, changedBy: d.changedBy, afterTimeout: !!d.afterTimeout, etag: d.etag ?? null };
982
+ // The refusal answers the save a toast may still be announcing; it replaces the
983
+ // toast at once.
984
+ toastText = null;
985
+ clearTimeout(toastTimer);
986
+ disarm();
987
+ busy = null;
988
+ keepFailed = false;
989
+ keepAttempt++;
990
+ armLost = false;
991
+ schedule();
992
+ }
993
+
994
+ // An old save event cannot erase a newer refusal: only a tab that is no longer
995
+ // holding one clears it. Losses are untouched; a save never acknowledges.
996
+ function onSaveSettled(e) {
997
+ if (isSaveConflicted()) {
998
+ // save.js fires clay:save-saved just before it releases the hold, in the same
999
+ // task; look again once it has.
1000
+ if (e.type === "clay:save-saved") queueMicrotask(() => { if (!isSaveConflicted()) onSaveSettled(e); });
1001
+ return;
1002
+ }
1003
+ const keptMine = busy === "overwrite" && e.type === "clay:save-saved";
1004
+ if (refused || busy) {
1005
+ refused = null;
1006
+ busy = null;
1007
+ keepFailed = false;
1008
+ armLost = false;
1009
+ disarm();
1010
+ }
1011
+ if (keptMine) toast("Saved your version over theirs.");
1012
+ else schedule();
1013
+ }
1014
+
1015
+ // On a manual page, conflictResolvedBySync can release the hold without firing
1016
+ // clay:save-conflict-resolved. Checked a frame later, after the apply finished.
1017
+ function onSyncApplied() {
1018
+ if (!refused) return;
1019
+ checkRefusal = true;
1020
+ schedule();
1021
+ }
1022
+
1023
+ // conflictResolvedBySync released the hold on a manual page with unsaved work:
1024
+ // no save happened and no clay:save-conflict-resolved fired, but their version
1025
+ // is on this page now, so the refusal is over.
1026
+ function onConflictReleased() {
1027
+ if (!refused || isSaveConflicted()) return;
1028
+ refused = null;
1029
+ busy = null;
1030
+ keepFailed = false;
1031
+ armLost = false;
1032
+ disarm();
1033
+ schedule();
1034
+ }
1035
+
1036
+ function onKeydown(e) {
1037
+ if (e.key !== "Escape") return;
1038
+ if (armed) {
1039
+ e.preventDefault();
1040
+ disarm();
1041
+ render();
1042
+ return;
1043
+ }
1044
+ if (view !== "panel") return;
1045
+ const a = document.activeElement;
1046
+ if (a && a !== document.body && !root?.contains(a)) return;
1047
+ e.preventDefault();
1048
+ minimizePanel();
1049
+ }
1050
+
1051
+ function onInput(e) {
1052
+ if (!armed || root?.contains(e.target)) return;
1053
+ disarm();
1054
+ schedule();
1055
+ }
1056
+
1057
+ // Focus that leaves the notice while a toast stands in for it is the person moving
1058
+ // on: the control the toast replaced does not take focus back afterwards.
1059
+ function onFocus(e) {
1060
+ if (toastText && pendingFocus && !root?.contains(e.target)) pendingFocus = null;
1061
+ }
1062
+
1063
+ function init() {
1064
+ listeners = new AbortController();
1065
+ const { signal } = listeners;
1066
+ // Registered before the edit-mode gate: an upgraded reader has nothing to reconcile
1067
+ // and this is still the one notice it can be shown.
1068
+ document.addEventListener("clay:document-upgraded", onUpgraded, { signal });
1069
+ if (!isEditMode) return;
1070
+ document.addEventListener("clay:sync-conflicts-changed", onLedgerChanged, { signal });
1071
+ // An apply beginning or ending changes only whether Revert may run.
1072
+ document.addEventListener("clay:unsaved-state-changed", schedule, { signal });
1073
+ document.addEventListener("clay:save-conflict", onSaveConflict, { signal });
1074
+ document.addEventListener("clay:save-saved", onSaveSettled, { signal });
1075
+ document.addEventListener("clay:save-conflict-resolved", onSaveSettled, { signal });
1076
+ document.addEventListener("clay:sync-applied", onSyncApplied, { signal });
1077
+ document.addEventListener("clay:save-conflict-released", onConflictReleased, { signal });
1078
+ document.addEventListener("keydown", onKeydown, { signal });
1079
+ document.addEventListener("input", onInput, { capture: true, signal });
1080
+ document.addEventListener("focus", onFocus, { capture: true, signal });
1081
+ document.addEventListener("clay:conflict-store-changed", onStoreChanged, { signal });
1082
+ document.addEventListener("clay:undo-kept", onUndoKept, { signal });
1083
+ if (ledger()?.size) schedule();
1084
+ refreshRecovered();
1085
+ }
1086
+
1087
+ // The shell (conflict-notice.js) asks for this file only when there is something to
1088
+ // show, and hands it the event that asked: it is replayed here, after init() has
1089
+ // registered the listeners, exactly as if it had arrived on a page that already held
1090
+ // the notice.
1091
+ const HANDLERS = {
1092
+ "clay:sync-conflicts-changed": onLedgerChanged,
1093
+ "clay:save-conflict": onSaveConflict,
1094
+ "clay:document-upgraded": onUpgraded,
1095
+ "clay:undo-kept": onUndoKept,
1096
+ };
1097
+
1098
+ let started = false;
1099
+ let listeners = null;
1100
+
1101
+ export function start(trigger) {
1102
+ if (!started) {
1103
+ started = true;
1104
+ init();
1105
+ }
1106
+ const handler = trigger && HANDLERS[trigger.type];
1107
+ if (handler) handler(trigger);
1108
+ // This file is fetched, so the events between the one that asked for it and its
1109
+ // evaluation are gone: a refusal answered in that window must not be drawn.
1110
+ if (refused) {
1111
+ checkRefusal = true;
1112
+ schedule();
1113
+ }
1114
+ }
1115
+
1116
+ // A committed exit takes the notice with it: every handler above, every timer it owns
1117
+ // and the node it drew. Nothing here acknowledges a record or releases a refusal — an
1118
+ // exit that still held one never got this far — and a later `start()` builds the notice
1119
+ // again from whatever the ledger and the store hold then.
1120
+ export function stop() {
1121
+ if (!started) return;
1122
+ started = false;
1123
+ listeners?.abort();
1124
+ listeners = null;
1125
+ disarm();
1126
+ clearTimeout(toastTimer);
1127
+ toastTimer = null;
1128
+ clearTimeout(downloadedTimer);
1129
+ downloadedTimer = null;
1130
+ clearTimeout(copiedTimer);
1131
+ copiedTimer = null;
1132
+ clearTimeout(ringTimer);
1133
+ ringTimer = null;
1134
+ dropRing();
1135
+ window.visualViewport?.removeEventListener("resize", place);
1136
+ window.visualViewport?.removeEventListener("scroll", place);
1137
+ root?.remove();
1138
+ root = null;
1139
+ keyed = new Map();
1140
+ wellEl = null;
1141
+ panelEl = null;
1142
+ view = "hidden";
1143
+ listOpen = false;
1144
+ renderedIds = [];
1145
+ seenIds.clear();
1146
+ freshIds.clear();
1147
+ blockedRows.clear();
1148
+ toastText = null;
1149
+ refused = null;
1150
+ upgraded = null;
1151
+ busy = null;
1152
+ keepFailed = false;
1153
+ keepAttempt++;
1154
+ armed = null;
1155
+ armedAt = 0;
1156
+ armLost = false;
1157
+ recovered = [];
1158
+ recoveredUndo = [];
1159
+ actionFailed = false;
1160
+ frameQueued = false;
1161
+ checkRefusal = false;
1162
+ pendingFocus = null;
1163
+ lastAnnounced = null;
1164
+ downloaded = false;
1165
+ downloadFailed = false;
1166
+ copiedId = null;
1167
+ }
1168
+
1169
+ export const _test = {
1170
+ state: () => ({ view, listOpen, renderedIds: renderedIds.slice(), refused, armed, busy, ledgerRevision, upgraded }),
1171
+ render,
1172
+ resetUpgrade: () => { upgraded = null; },
1173
+ };