@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
@@ -5,15 +5,35 @@ import { createStore, safeStorage } from "./visitor-drafts/store.js";
5
5
  import { createUiMemory } from "./visitor-drafts/ui-memory.js";
6
6
  import { rememberSource, downloadUrl } from "./visitor-drafts/source.js";
7
7
  import { mountPrompt } from "./visitor-drafts/prompt.js";
8
- import { mountIntro } from "./visitor-drafts/intro.js";
9
- import { mountOwnerChip } from "./visitor-drafts/owner-chip.js";
10
-
11
- const DOWNLOAD_URL = "/_/download?as=htmlclay";
8
+ import { DOWNLOAD_URL, backToDraft, editOriginal, inertVisitorDrafts } from "./visitor-drafts/inert.js";
12
9
 
13
10
  let store = null;
14
11
  let restoredDetail = null;
15
12
  let downloadLink = DOWNLOAD_URL;
16
13
  let source = null;
14
+ let ready = Promise.resolve();
15
+
16
+ // The chip is a convenience: a failed import leaves the draft working without it.
17
+ function mountChip() {
18
+ return import("./visitor-drafts/owner-chip.js")
19
+ .then((m) => m.mountOwnerChip())
20
+ .catch((error) => {
21
+ console.warn("visitor-drafts: the owner chip did not load", error);
22
+ return null;
23
+ });
24
+ }
25
+
26
+ // Most visitors never see the intro (no template, a draft already kept, or dismissed),
27
+ // so it is fetched only for a page that has one; a failed import costs only the intro.
28
+ function mountIntroLate(options) {
29
+ if (!options.template) return Promise.resolve(null);
30
+ return import("./visitor-drafts/intro.js")
31
+ .then((m) => m.mountIntro(options))
32
+ .catch((error) => {
33
+ console.warn("visitor-drafts: the intro did not load", error);
34
+ return null;
35
+ });
36
+ }
17
37
 
18
38
  const slot = draftSlot();
19
39
  const rulesTag = document.querySelector('script[data-rules-name~="api"]');
@@ -30,11 +50,11 @@ if (isDraftMode && slot && rulesTag) {
30
50
  const appName = document.querySelector('meta[name="application-name"]')?.content?.trim() || document.title.split(/:|\s\|\s/)[0].trim() || "This app";
31
51
  const template = document.querySelector("template[visitor-intro]");
32
52
  const start = () => {
33
- const prompt = mountPrompt({ memory, appName, link: downloadLink });
34
- mountIntro({ memory, template, appName, hadDraft: result.hadDraft });
35
- if (isOwner) mountOwnerChip();
36
- document.addEventListener("clay:drafts-store-failed", () => prompt.showError());
37
- document.addEventListener("clay:drafts-saved", () => prompt.clearError());
53
+ ready = mountPrompt({ memory, appName, link: downloadLink }).then((prompt) => mountIntroLate({ memory, template, appName, hadDraft: result.hadDraft }).then(() => prompt)).then((prompt) => {
54
+ document.addEventListener("clay:drafts-store-failed", () => prompt.showError());
55
+ document.addEventListener("clay:drafts-saved", () => prompt.clearError());
56
+ return (isOwner ? mountChip() : Promise.resolve(null)).then(() => prompt);
57
+ });
38
58
  };
39
59
  document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", start, { once: true }) : start();
40
60
  setDraftFlush(() => store.flush());
@@ -48,41 +68,10 @@ if (isDraftMode && slot && rulesTag) {
48
68
  if (!rulesTag) missing.push('a <script data-rules-name~="api"> rules tag');
49
69
  console.warn(`visitor-drafts: this page keeps no draft (missing ${missing.join(" and ")})`);
50
70
  } else if (isOwner && isEditMode && !hasSaveToken()) {
51
- mountOwnerChip();
52
- }
53
-
54
- export const ready = Promise.resolve();
55
-
56
- function editOriginal() {
57
- const url = new URL(window.location.href);
58
- url.searchParams.set("editmode", "true");
59
- window.location.href = url.toString();
60
- return true;
61
- }
62
-
63
- function backToDraft() {
64
- const url = new URL(window.location.href);
65
- url.searchParams.delete("editmode");
66
- window.location.href = url.toString();
67
- return true;
71
+ ready = mountChip();
68
72
  }
69
73
 
70
- function inertVisitorDrafts() {
71
- return {
72
- active: false,
73
- slot: null,
74
- key: null,
75
- hasDraft: () => false,
76
- restored: () => null,
77
- saveNow: () => false,
78
- reset: () => false,
79
- onRestore: () => false,
80
- editOriginal,
81
- backToDraft,
82
- downloadUrl: () => DOWNLOAD_URL,
83
- source: null,
84
- };
85
- }
74
+ export { ready };
86
75
 
87
76
  export const visitorDrafts = store ? {
88
77
  active: true,
@@ -1,4 +1,4 @@
1
- import { isEditMode } from "../core/is-edit-mode.js";
1
+ import { isEditMode, modeState } from "../core/is-edit-mode.js";
2
2
  import { saveToken } from "../core/host-attrs.js";
3
3
  import { hostMeta, hostSupports } from "../core/host-meta.js";
4
4
 
@@ -475,6 +475,80 @@ function releaseSaving(rec) {
475
475
  saveLane?.resumeAutosave();
476
476
  }
477
477
 
478
+ // --- write admission -------------------------------------------------------
479
+
480
+ // A request that asks for the page's own bytes is an edit-session operation: it flushes
481
+ // the page and holds the save lane around the POST, so it may only start while the
482
+ // session it was sent in still admits writes. The lease below closes that admission
483
+ // while an exit drains and leaves an active save hold — this request's own operation —
484
+ // pending until it settles, so an exit cannot report a clean view over an agent that is
485
+ // still writing the file. A read-only request (`document: "none"`, which is every
486
+ // helper and message workflow) is not gated: it writes nothing here.
487
+ let session = null;
488
+
489
+ const admits = () => !session || session.admitted;
490
+
491
+ const fault = (message, code, pending) => Object.assign(new Error(`clay.wire: ${message}`), { code, pending });
492
+
493
+ // The generation is what makes an await safe to come back from. A helper's identity
494
+ // round trip, the save lane's import and the flush all outlive a switch that started
495
+ // while they were in flight; the record is sent for the session it began in, and a
496
+ // stale one ends as an error rather than handing an agent bytes from a session that
497
+ // has gone.
498
+ function admitsRecord(rec) {
499
+ if (rec.document !== "edit") return true;
500
+ if (rec.generation === modeState().generation && admits()) return true;
501
+ finish(rec, "error", "the page changed mode before this request went out", {
502
+ errorCode: "stale-document",
503
+ errorSource: "client",
504
+ });
505
+ return false;
506
+ }
507
+
508
+ /**
509
+ * The wire as a mode lease.
510
+ *
511
+ * Nothing to construct: the channel is lazy and belongs to the page. `quiesce` closes
512
+ * admission for edit requests and refuses the exit while one is still open, because
513
+ * that request's save hold is work the barrier cannot flush around — the hold stands
514
+ * until the request itself settles (a terminal frame, a cancel, or its deadline).
515
+ * `resume` reopens admission over the same records. `dispose` (after the barrier) only
516
+ * forgets the session: with none open the page is back to its unnegotiated rule.
517
+ *
518
+ * `initial: true` adopts the boot's rule instead of starting one: an explicit edit boot
519
+ * negotiated no session, so the adopted lease is admitted from the start and enable has
520
+ * nothing left to open.
521
+ */
522
+ export function wireParticipant() {
523
+ return {
524
+ id: "clay:wire",
525
+ order: 900,
526
+ prepare(context = {}) {
527
+ const lease = { admitted: context.initial === true };
528
+ session = lease;
529
+ return {
530
+ enable() {
531
+ if (session === lease) lease.admitted = true;
532
+ },
533
+ quiesce() {
534
+ if (session !== lease) return;
535
+ lease.admitted = false;
536
+ const pending = [...records.values()]
537
+ .filter((rec) => rec.document === "edit" && (rec.holdsSave || OPEN_STATES.has(rec.state)))
538
+ .map((rec) => rec.id);
539
+ if (pending.length) throw fault("a request on this page has not settled", "pending-input", pending);
540
+ },
541
+ resume() {
542
+ if (session === lease) lease.admitted = true;
543
+ },
544
+ dispose() {
545
+ if (session === lease) session = null;
546
+ },
547
+ };
548
+ },
549
+ };
550
+ }
551
+
478
552
  function once(eventName, timeout) {
479
553
  return new Promise((resolve) => {
480
554
  let done = false;
@@ -616,6 +690,7 @@ function send(payload, opts = {}) {
616
690
  landingTimer: null,
617
691
  abort: typeof AbortController === "function" ? new AbortController() : null,
618
692
  holdsSave: false,
693
+ generation: modeState().generation,
619
694
  settle: null,
620
695
  signal: opts.signal && typeof opts.signal.addEventListener === "function" ? opts.signal : null,
621
696
  signalHandler: null,
@@ -649,12 +724,16 @@ function send(payload, opts = {}) {
649
724
  }
650
725
  }
651
726
 
727
+ if (!admitsRecord(rec)) return;
728
+
652
729
  if (rec.document === "edit") {
653
730
  await holdSaving(rec);
654
731
  if (rec.state !== "sent") return;
732
+ if (!admitsRecord(rec)) return;
655
733
 
656
734
  await flushSave();
657
735
  if (rec.state !== "sent") return;
736
+ if (!admitsRecord(rec)) return;
658
737
  }
659
738
 
660
739
  // Subscribe before posting. A handler can acknowledge in single-digit
@@ -804,6 +883,8 @@ async function helpers() {
804
883
  .map(({ name, state }) => ({ name, state }));
805
884
  }
806
885
 
886
+ export { wireParticipant as modeParticipant };
887
+
807
888
  export const wire = { send, cancel, get, list, isBusy, on, helpers };
808
889
 
809
890
  export default wire;
@@ -0,0 +1,26 @@
1
+ // The always-wave core as one module graph for the built release. The chunk build
2
+ // makes this an entry and rewrites the loader's always-wave thunks to read the
3
+ // namespace each path names here, so a page fetches one file instead of six while
4
+ // the loader still activates the core before any plugin, in this order.
5
+ //
6
+ // The import list is CORE_WAVES.always (src/loader-logic.js), written out because
7
+ // import statements cannot be computed. tests/unit/build/reader-core-order.test.js
8
+ // runs the built graph and the source graph and compares their evaluation order, so
9
+ // a module added to that wave without a line here fails a test instead of quietly
10
+ // moving to the end. Source mode never imports this file.
11
+ import * as mutation from "./lib/mutation.js";
12
+ import * as preserveAuthored from "./core/preserve-authored.js";
13
+ import * as editMode from "./core/edit-mode.js";
14
+ import * as modeKey from "./core/mode-key.js";
15
+ import * as staleHostNotice from "./core/stale-host-notice.js";
16
+ import * as pageData from "./core/page-data.js";
17
+
18
+ // Keyed by the logical path the loader and the manifest use.
19
+ export const core = {
20
+ "lib/mutation.js": mutation,
21
+ "core/preserve-authored.js": preserveAuthored,
22
+ "core/edit-mode.js": editMode,
23
+ "core/mode-key.js": modeKey,
24
+ "core/stale-host-notice.js": staleHostNotice,
25
+ "core/page-data.js": pageData,
26
+ };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * conflicts.js — the durable half of the conflict ledger.
3
+ *
4
+ * The ledger (sync/conflicts.js) keeps what an incoming frame won over this tab's
5
+ * editing, and it keeps it in memory: the root the merge read as "mine", the nodes
6
+ * the report pointed at, the conflict records themselves. A reload takes every one
7
+ * of those away, and replaying the paths afterwards would point at a DOM that is
8
+ * not the one they were recorded against. So the ledger hands this module the
9
+ * material it cannot rebuild — the pre-apply root as bytes, and the conflicts as
10
+ * plain fields — and this module stores them in the shared recovery store.
11
+ *
12
+ * One record per apply: the root is stored once, and the conflicts reference it.
13
+ * The word "durable" means one completed IndexedDB transaction, and nothing more:
14
+ * a stored record is not a save, and the page never renders from it.
15
+ *
16
+ * What reaches IndexedDB is strings, numbers and booleans. A DOM node, a function,
17
+ * a Map or anything else the structured clone algorithm would refuse is dropped
18
+ * from the record and named in `dropped`, because one uncloneable field must not
19
+ * cost somebody the whole copy. The store's own refusal (quota, unknown schema,
20
+ * an unreachable database) is reported as a code and never removes the live
21
+ * conflict, which is still the person's copy in this tab.
22
+ */
23
+
24
+ import { storage } from "../lib/doc-storage.js";
25
+
26
+ export const CONFLICTS_CHANNEL = "clay.conflicts";
27
+ export const CONFLICTS_VERSION = 1;
28
+ export const CONFLICTS_KIND = "conflict";
29
+
30
+ // A cheap read for the notice's shell, which must not open the database on every
31
+ // edit boot. It says only that this document has stored conflicts to review.
32
+ const HINT_KEY = "conflicts";
33
+
34
+ const CONTEXT_FIELDS = ["kind", "id", "label", "field"];
35
+
36
+ function recoveryService() {
37
+ const clay = globalThis.window?.clay ?? globalThis.clay;
38
+ return clay?.recovery ?? null;
39
+ }
40
+
41
+ let registered = null;
42
+
43
+ function channel() {
44
+ const service = recoveryService();
45
+ if (!service || typeof service.register !== "function") return null;
46
+ if (registered?.service === service) return registered.channel;
47
+ try {
48
+ registered = {
49
+ service,
50
+ channel: service.register({ channel: CONFLICTS_CHANNEL, version: CONFLICTS_VERSION }),
51
+ };
52
+ } catch (_) {
53
+ return null;
54
+ }
55
+ return registered.channel;
56
+ }
57
+
58
+ /**
59
+ * How many conflicts this document's store is holding for review, without opening
60
+ * the database. Zero means the notice's shell stays asleep.
61
+ *
62
+ * @returns {number}
63
+ */
64
+ export function pendingConflicts() {
65
+ const stored = storage.local.get(HINT_KEY);
66
+ return Number.isFinite(stored?.count) && stored.count > 0 ? stored.count : 0;
67
+ }
68
+
69
+ function noteHint(count) {
70
+ if (count > 0) storage.local.set(HINT_KEY, { count, at: Date.now() });
71
+ else storage.local.remove(HINT_KEY);
72
+ }
73
+
74
+ const DROPPED = Symbol("dropped");
75
+
76
+ function isPlainObject(value) {
77
+ if (!value || typeof value !== "object") return false;
78
+ const proto = Object.getPrototypeOf(value);
79
+ return proto === Object.prototype || proto === null;
80
+ }
81
+
82
+ function copyField(value, dropped, field) {
83
+ const type = typeof value;
84
+ if (value === null || type === "string" || type === "number" || type === "boolean") return value;
85
+ if (type === "undefined") return DROPPED;
86
+ if (type !== "object") {
87
+ dropped.push(field);
88
+ return DROPPED;
89
+ }
90
+ if (Array.isArray(value)) {
91
+ return value.map((item, at) => {
92
+ const kept = copyField(item, dropped, `${field}[${at}]`);
93
+ return kept === DROPPED ? null : kept;
94
+ });
95
+ }
96
+ if (!isPlainObject(value)) {
97
+ dropped.push(field);
98
+ return DROPPED;
99
+ }
100
+ const out = {};
101
+ for (const [key, item] of Object.entries(value)) {
102
+ const kept = copyField(item, dropped, field ? `${field}.${key}` : key);
103
+ if (kept !== DROPPED) out[key] = kept;
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * A copy that survives the trip into IndexedDB: plain objects, arrays, strings,
110
+ * numbers and booleans, with everything else dropped and named.
111
+ *
112
+ * @param {*} value
113
+ * @param {string[]} [dropped] - collects the fields that could not be stored
114
+ * @param {string} [field]
115
+ * @returns {*}
116
+ */
117
+ export function plainCopy(value, dropped = [], field = "") {
118
+ const kept = copyField(value, dropped, field);
119
+ return kept === DROPPED ? null : kept;
120
+ }
121
+
122
+ /**
123
+ * The part of a conflict's context that can be stored: its identity, its label and
124
+ * its field. The resolver's `open` is a function bound to this page's tree, so it
125
+ * lives on the live record and never here.
126
+ *
127
+ * @param {*} context
128
+ * @returns {?Object}
129
+ */
130
+ export function storedContext(context) {
131
+ if (!context || typeof context !== "object") return null;
132
+ const out = {};
133
+ for (const field of CONTEXT_FIELDS) {
134
+ const value = context[field];
135
+ if (typeof value === "string") {
136
+ if (value) out[field] = value;
137
+ } else if (typeof value === "number" && Number.isFinite(value)) {
138
+ out[field] = String(value);
139
+ }
140
+ }
141
+ return Object.keys(out).length ? out : null;
142
+ }
143
+
144
+ /**
145
+ * Copy one apply and its conflicts into the recovery store, before the ledger
146
+ * releases any of it.
147
+ *
148
+ * Returns synchronously with the staged ids: the copy is already in memory and the
149
+ * write is under way. It is a recovery copy, not a durable receipt.
150
+ *
151
+ * @param {Object} spec
152
+ * @param {Object} spec.apply - the ledger's apply, without its DOM roots
153
+ * @param {?string} spec.root - the pre-apply root as saved bytes
154
+ * @param {Object[]} spec.conflicts - plain conflict entries
155
+ * @returns {{id: *, revision: number}[]}
156
+ */
157
+ export function recordConflicts({ apply = {}, root = null, conflicts = [] } = {}) {
158
+ const entries = [];
159
+ for (const conflict of conflicts) {
160
+ const dropped = [];
161
+ const entry = plainCopy(conflict, dropped, "");
162
+ if (!entry || typeof entry !== "object" || !entry.id) continue;
163
+ if (dropped.length) entry.dropped = dropped;
164
+ entries.push(entry);
165
+ }
166
+ if (!entries.length) return [];
167
+ const payload = {
168
+ apply: plainCopy(apply, [], "") ?? {},
169
+ root: typeof root === "string" ? root : null,
170
+ savedAt: Date.now(),
171
+ conflicts: entries,
172
+ };
173
+ try {
174
+ const ch = channel();
175
+ if (!ch) return [];
176
+ const staged = ch.stage({ key: String(apply.id ?? ""), kind: CONFLICTS_KIND, payload });
177
+ ch.flush().catch(() => {});
178
+ noteHint(pendingConflicts() + entries.length);
179
+ return [{ id: staged.id, revision: staged.revision }];
180
+ } catch (err) {
181
+ // The live conflict is the copy that matters right now: nothing is removed here.
182
+ console.error("clayjs: the conflict could not be stored for a later reload; it is still here", err);
183
+ return [];
184
+ }
185
+ }
186
+
187
+ const textOf = (value) => (typeof value === "string" ? value : value == null ? "" : String(value));
188
+
189
+ function usableConflict(conflict) {
190
+ if (!conflict || typeof conflict !== "object") return null;
191
+ const id = textOf(conflict.id);
192
+ if (!id) return null;
193
+ const context = storedContext(conflict.context);
194
+ return {
195
+ id,
196
+ kind: textOf(conflict.kind),
197
+ key: textOf(conflict.key),
198
+ name: textOf(conflict.name),
199
+ sentence: textOf(conflict.sentence),
200
+ yours: textOf(conflict.yours),
201
+ now: textOf(conflict.now),
202
+ context,
203
+ dropped: Array.isArray(conflict.dropped) ? conflict.dropped.map(textOf) : [],
204
+ };
205
+ }
206
+
207
+ function usableRecord(record) {
208
+ const payload = record?.payload;
209
+ if (!payload || typeof payload !== "object") return null;
210
+ const conflicts = Array.isArray(payload.conflicts) ? payload.conflicts.map(usableConflict).filter(Boolean) : [];
211
+ if (!conflicts.length) return null;
212
+ const apply = payload.apply && typeof payload.apply === "object" ? payload.apply : {};
213
+ return {
214
+ id: record.id,
215
+ revision: record.revision,
216
+ supported: record.supported !== false,
217
+ savedAt: Number.isFinite(payload.savedAt) ? payload.savedAt : null,
218
+ apply,
219
+ root: typeof payload.root === "string" ? payload.root : null,
220
+ conflicts,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Every stored conflict for this document, from every writer: a record does not
226
+ * belong to the tab that made it, because the tab that made it may be gone.
227
+ *
228
+ * @returns {Promise<{ok: boolean, code?: string, records: Object[], status?: Object}>}
229
+ */
230
+ export async function listConflicts() {
231
+ const ch = channel();
232
+ if (!ch) return { ok: true, records: [] };
233
+ let listed;
234
+ try {
235
+ listed = await ch.list();
236
+ } catch (_) {
237
+ return { ok: false, code: "error", records: [] };
238
+ }
239
+ if (!listed.ok) return { ok: false, code: listed.code, records: [] };
240
+ const records = listed.records.map(usableRecord).filter(Boolean);
241
+ noteHint(records.reduce((sum, record) => sum + record.conflicts.length, 0));
242
+ return { ok: true, records, status: listed.status };
243
+ }
244
+
245
+ /**
246
+ * Acknowledge conflicts, by the id the ledger gave them. This is the only way a
247
+ * stored conflict leaves: a person said so, or a live conflict was accepted,
248
+ * reverted or reconciled and its own record has already gone.
249
+ *
250
+ * Only the named conflicts go. A record that still holds another conflict keeps
251
+ * its root, so the surviving text is never lost because a neighbour was dismissed;
252
+ * the remainder is written under a new revision before the old one is deleted, so
253
+ * a failed write leaves the original exactly as it was.
254
+ *
255
+ * @param {string[]} ids
256
+ * @returns {Promise<{ok: boolean, code?: string, removed?: number}>}
257
+ */
258
+ export async function dismissConflicts(ids) {
259
+ const wanted = new Set((Array.isArray(ids) ? ids : [ids]).filter((id) => id != null).map(String));
260
+ if (!wanted.size) return { ok: true, removed: 0 };
261
+ const ch = channel();
262
+ if (!ch) return { ok: true, removed: 0 };
263
+ const listed = await listConflicts();
264
+ if (!listed.ok) return { ok: false, code: listed.code };
265
+ let removed = 0;
266
+ for (const record of listed.records) {
267
+ const remaining = record.conflicts.filter((entry) => !wanted.has(entry.id));
268
+ if (remaining.length === record.conflicts.length) continue;
269
+ removed += record.conflicts.length - remaining.length;
270
+ if (!remaining.length) {
271
+ await ch.dismiss(record.id, { revision: record.revision });
272
+ continue;
273
+ }
274
+ ch.stage({
275
+ key: String(record.id),
276
+ kind: CONFLICTS_KIND,
277
+ payload: { apply: record.apply, root: record.root, savedAt: record.savedAt, conflicts: remaining },
278
+ });
279
+ const written = await ch.flush();
280
+ if (!written.ok) continue;
281
+ await ch.dismiss(record.id, { revision: record.revision });
282
+ }
283
+ await listConflicts();
284
+ // The notice may be holding a record this just changed.
285
+ globalThis.document?.dispatchEvent(new CustomEvent("clay:conflict-store-changed"));
286
+ return { ok: true, removed };
287
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * identity.js — which recovery records belong to this document, and who is writing.
3
+ *
4
+ * Two tabs on one file must not overwrite each other's unsaved writing, and two
5
+ * documents on one origin (every file on a hyperclay.com subdomain) must not share
6
+ * a record at all. So a record is filed under a four-part scope plus a writer, and
7
+ * this module is the one place that decides both.
8
+ *
9
+ * The document half is the host's own file identity: `documentid`, or the pre-rename
10
+ * `htmlclayid` a document saved before that rename still carries on disk. It is read
11
+ * from HOST_IDENTITY_ATTRS, never from the save-token list, because a name in that
12
+ * list becomes a credential in the save URL. The root is read ONCE, here: a morph or
13
+ * a stream restart can rewrite the root afterwards, and a re-read would file this
14
+ * tab's writing under a document it is not looking at. `check()` reads the root
15
+ * again only to report that mismatch, and never to change the scope.
16
+ *
17
+ * Without a host identity the exact pathname is the identity: no query, no hash, and
18
+ * no decoding of distinct paths into one. A draft is always path-scoped, because a
19
+ * visitor's draft never reaches the host's file and this browser is the only place
20
+ * it lives. Nothing here makes a request; a draft must not ask the host anything.
21
+ */
22
+
23
+ import { HOST_IDENTITY_ATTRS } from "../lib/root-attrs.js";
24
+
25
+ export const PATH_KIND = "path";
26
+ export const EDIT_MODE = "edit";
27
+ export const DRAFT_MODE = "draft";
28
+
29
+ /**
30
+ * A fresh writer id, once per page load.
31
+ *
32
+ * Never from sessionStorage: a duplicated tab inherits that storage, and two tabs
33
+ * holding one writer id would overwrite each other's recovery for the same key.
34
+ *
35
+ * @returns {string}
36
+ */
37
+ export function newWriterId() {
38
+ const minted = globalThis.crypto?.randomUUID?.();
39
+ if (minted) return minted;
40
+ return `wr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
41
+ }
42
+
43
+ /**
44
+ * The durable file identity this response carries, or null.
45
+ *
46
+ * Host-injected and stripped before saving, so it names a file and not a revision.
47
+ * A response carrying neither spelling is a downloaded copy, a local file, or a
48
+ * host that mints no identity, and all three fall back to the pathname.
49
+ *
50
+ * @param {?Element} [root] - the document's root element
51
+ * @returns {?{kind: string, identity: string}}
52
+ */
53
+ export function hostIdentityOf(root = globalThis.document?.documentElement) {
54
+ if (!root || typeof root.getAttribute !== "function") return null;
55
+ for (const attr of HOST_IDENTITY_ATTRS) {
56
+ const value = root.getAttribute(attr);
57
+ if (value) return { kind: attr, identity: value };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * The pathname as an identity: no query, no hash, nothing decoded.
64
+ *
65
+ * Two URLs that differ only in how they were opened are one document. A caller
66
+ * that hands over a whole URL string still gets the path, because filing one
67
+ * document's writing under two identities is how recovery goes missing.
68
+ *
69
+ * @param {string} pathname
70
+ * @returns {{kind: string, identity: string}}
71
+ */
72
+ export function pathIdentityOf(pathname) {
73
+ const identity = String(pathname || "/").split(/[?#]/)[0];
74
+ return { kind: PATH_KIND, identity: identity || "/" };
75
+ }
76
+
77
+ /**
78
+ * This page's recovery identity: its scope, its writer, and a check on the root.
79
+ *
80
+ * @param {Object} [options]
81
+ * @param {string} [options.mode] - "edit" or "draft"
82
+ * @param {?Element} [options.root]
83
+ * @param {?Location} [options.location]
84
+ * @param {string} [options.pathname]
85
+ * @param {string} [options.origin]
86
+ * @param {string} [options.writerId]
87
+ * @returns {Object}
88
+ */
89
+ /**
90
+ * Whether two authorities are the same document, seen by the same account.
91
+ *
92
+ * `principalId` is the account, never a token, and a changed login is another principal:
93
+ * a journal is replayed into the authority and principal it was written under or not at
94
+ * all. A missing field is never equal to a missing field.
95
+ *
96
+ * @param {?Object} a
97
+ * @param {?Object} b
98
+ * @returns {boolean}
99
+ */
100
+ export function sameAuthority(a, b) {
101
+ if (!a || !b) return false;
102
+ if (!a.host || !a.documentId || !a.principalId) return false;
103
+ return a.host === b.host && a.documentId === b.documentId && a.principalId === b.principalId;
104
+ }
105
+
106
+ export function createIdentity({
107
+ mode = EDIT_MODE,
108
+ root = globalThis.document?.documentElement,
109
+ location: where = globalThis.window?.location,
110
+ pathname,
111
+ origin,
112
+ writerId,
113
+ } = {}) {
114
+ const scopeMode = mode === DRAFT_MODE ? DRAFT_MODE : EDIT_MODE;
115
+ const path = pathname ?? where?.pathname ?? "/";
116
+ const from = origin ?? where?.origin ?? "";
117
+ const pathIdentity = pathIdentityOf(path);
118
+ const hostIdentity = scopeMode === DRAFT_MODE ? null : hostIdentityOf(root);
119
+ const captured = hostIdentity ?? pathIdentity;
120
+ const scope = [from, captured.kind, captured.identity, scopeMode];
121
+
122
+ return {
123
+ origin: from,
124
+ pathname: path,
125
+ mode: scopeMode,
126
+ scope,
127
+ writerId: writerId || newWriterId(),
128
+ identityKind: captured.kind,
129
+ identity: captured.identity,
130
+
131
+ /**
132
+ * Is the root still naming the document this page filed its records under?
133
+ *
134
+ * A mismatch is reported, never applied: records written under one identity are
135
+ * not moved into another document's scope by a later root rewrite. A host
136
+ * identity that appears after this load is a later binding, and the records stay
137
+ * where they are until a confirmed move offers both scopes for review.
138
+ *
139
+ * @param {?Element} [next]
140
+ * @returns {{ok: boolean, code: ?string}}
141
+ */
142
+ check(next = globalThis.document?.documentElement) {
143
+ const found = hostIdentityOf(next);
144
+ if (!hostIdentity) {
145
+ return found ? { ok: true, code: null, bound: found, scope } : { ok: true, code: null };
146
+ }
147
+ if (!found) return { ok: false, code: "identity-missing", captured: hostIdentity, scope };
148
+ if (found.kind !== hostIdentity.kind || found.identity !== hostIdentity.identity) {
149
+ return { ok: false, code: "identity-mismatch", captured: hostIdentity, found, scope };
150
+ }
151
+ return { ok: true, code: null };
152
+ },
153
+ };
154
+ }