@panphora/clayjs 1.15.1 → 1.16.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 (506) 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 +83090 -58706
  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-NWPPXOCZ.js +1 -0
  21. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2JL6E75N.js +1 -0
  24. package/dist/m/c-2M73252O.js +1 -0
  25. package/dist/m/c-2SPVQSHC.js +1 -0
  26. package/dist/m/c-33HUYEDI.js +1 -0
  27. package/dist/m/c-33JJ6CBX.js +2 -0
  28. package/dist/m/c-35NBGMZB.js +1 -0
  29. package/dist/m/c-3GJ4SDE2.js +1 -0
  30. package/dist/m/c-3IQRECYS.js +1 -0
  31. package/dist/m/c-3OWDMNJR.js +2 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4B6H4G5V.js +1 -0
  34. package/dist/m/c-4DRMS4QG.js +9 -0
  35. package/dist/m/c-4FFHZM32.js +1 -0
  36. package/dist/m/c-4JZDZFF6.js +1 -0
  37. package/dist/m/c-4L3BTFRK.js +1 -0
  38. package/dist/m/c-4Q2XTQ2N.js +2 -0
  39. package/dist/m/c-4WKFKEKF.js +1 -0
  40. package/dist/m/c-4YSCRICO.js +1 -0
  41. package/dist/m/c-54RCDCBU.js +1 -0
  42. package/dist/m/c-5M4YEKCX.js +4 -0
  43. package/dist/m/c-5R4PTRG2.js +1 -0
  44. package/dist/m/c-5V3MOVJT.js +1 -0
  45. package/dist/m/c-5VYOWDTI.js +1 -0
  46. package/dist/m/c-5Y6TNX5W.js +34 -0
  47. package/dist/m/c-5YIHJ3TJ.js +1 -0
  48. package/dist/m/c-66O32MV3.js +28 -0
  49. package/dist/m/c-6HMB24EV.js +1 -0
  50. package/dist/m/c-7DJDKPGL.js +1 -0
  51. package/dist/m/c-7O6AFR65.js +1 -0
  52. package/dist/m/c-7SWMYKAI.js +1 -0
  53. package/dist/m/c-7VGZ7RU5.js +1 -0
  54. package/dist/m/c-A7LS5UZW.js +1 -0
  55. package/dist/m/c-ANQTJ3B5.js +2 -0
  56. package/dist/m/c-AQLFEATB.js +1 -0
  57. package/dist/m/c-AYVOREBE.js +1 -0
  58. package/dist/m/c-B2ZXPOS3.js +3 -0
  59. package/dist/m/c-BBVGTQG7.js +1 -0
  60. package/dist/m/c-BMIZKMPV.js +1 -0
  61. package/dist/m/c-BTDI4Y23.js +1 -0
  62. package/dist/m/c-BWFVSBDF.js +1 -0
  63. package/dist/m/c-BYTKL6UX.js +1 -0
  64. package/dist/m/c-CFHW5AMY.js +6 -0
  65. package/dist/m/c-CJZSLMVQ.js +1 -0
  66. package/dist/m/c-CYWLPPUS.js +1 -0
  67. package/dist/m/c-CZUSOB7V.js +1 -0
  68. package/dist/m/c-DBE6ORSI.js +1 -0
  69. package/dist/m/c-DCFSEJZG.js +1 -0
  70. package/dist/m/c-DNZ5IFM6.js +1 -0
  71. package/dist/m/c-EDHUR4MX.js +1 -0
  72. package/dist/m/c-EJTGYSLN.js +1 -0
  73. package/dist/m/c-EY6UOM54.js +1 -0
  74. package/dist/m/c-F3CKJTJS.js +1 -0
  75. package/dist/m/c-F7Y6FLPY.js +1 -0
  76. package/dist/m/c-FA4DZVTC.js +1 -0
  77. package/dist/m/c-FILLTJA7.js +1 -0
  78. package/dist/m/c-G2BUZITR.js +1 -0
  79. package/dist/m/c-GB3JTV6E.js +1 -0
  80. package/dist/m/c-GBGWXTX4.js +1 -0
  81. package/dist/m/c-GHBCCVG4.js +1 -0
  82. package/dist/m/c-GJAEGGXY.js +1 -0
  83. package/dist/m/c-GOWBDM6J.js +1 -0
  84. package/dist/m/c-GQ6F6IDK.js +1 -0
  85. package/dist/m/c-H4LG442Q.js +1 -0
  86. package/dist/m/c-HA6INT6J.js +1 -0
  87. package/dist/m/c-HPURYVZS.js +2 -0
  88. package/dist/m/c-HRDCD3WN.js +67 -0
  89. package/dist/m/c-HT33EQRN.js +1 -0
  90. package/dist/m/c-HVYXIS72.js +1 -0
  91. package/dist/m/c-IB52R237.js +1 -0
  92. package/dist/m/c-IJUK2GPW.js +1 -0
  93. package/dist/m/c-IKWNE4R4.js +1 -0
  94. package/dist/m/c-IU3IYEU6.js +1 -0
  95. package/dist/m/c-IZ2EBRAJ.js +1 -0
  96. package/dist/m/c-J7CA54IP.js +1 -0
  97. package/dist/m/c-JAX2667S.js +6 -0
  98. package/dist/m/c-KI7M4AY3.js +1 -0
  99. package/dist/m/c-KIPQJX4B.js +1 -0
  100. package/dist/m/c-KKWLDOPM.js +1 -0
  101. package/dist/m/c-KR4UW4KW.js +1 -0
  102. package/dist/m/c-KY3VJGUA.js +8 -0
  103. package/dist/m/c-L4WQSC27.js +1 -0
  104. package/dist/m/c-LFPHCXY3.js +1 -0
  105. package/dist/m/c-LR4CGEPT.js +91 -0
  106. package/dist/m/c-LRPUO2MX.js +1 -0
  107. package/dist/m/c-LTGMXXKO.js +1 -0
  108. package/dist/m/c-LXENEBV7.js +1 -0
  109. package/dist/m/c-LZJPNR2W.js +1 -0
  110. package/dist/m/c-MMUIUUZG.js +2 -0
  111. package/dist/m/c-MTQHWQC3.js +1 -0
  112. package/dist/m/c-N326R2JD.js +1 -0
  113. package/dist/m/c-N7JPUPGC.js +1 -0
  114. package/dist/m/c-NMJRXKPC.js +8 -0
  115. package/dist/m/c-NRPJEVJS.js +1 -0
  116. package/dist/m/c-NUHP2XDB.js +1 -0
  117. package/dist/m/c-NZMNXVF6.js +1 -0
  118. package/dist/m/c-OCSZ4QGN.js +1 -0
  119. package/dist/m/c-OIBUTDXI.js +1 -0
  120. package/dist/m/c-OIDRZKAF.js +1 -0
  121. package/dist/m/c-OYTFP5QN.js +1 -0
  122. package/dist/m/c-P7KNDG34.js +1 -0
  123. package/dist/m/c-PPOSCAQD.js +1 -0
  124. package/dist/m/c-PQ5LK6QE.js +993 -0
  125. package/dist/m/c-PZ4XEPUP.js +2 -0
  126. package/dist/m/c-QHIWXHRO.js +1 -0
  127. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  128. package/dist/m/c-QIX5BJYN.js +1 -0
  129. package/dist/m/c-QNKREOL6.js +1 -0
  130. package/dist/m/c-QQHEX6HA.js +1 -0
  131. package/dist/m/c-R7RMANJI.js +9 -0
  132. package/dist/m/c-RI4NT75Y.js +1 -0
  133. package/dist/m/c-RM7J7BV2.js +1 -0
  134. package/dist/m/c-RUJKSUQW.js +1 -0
  135. package/dist/m/c-RVMWGXEJ.js +1 -0
  136. package/dist/m/c-S7H2DRCB.js +1 -0
  137. package/dist/m/c-S7LK4KUE.js +1 -0
  138. package/dist/m/c-SGCCC4FW.js +25 -0
  139. package/dist/m/c-SGIA4IEQ.js +188 -0
  140. package/dist/m/c-SWRJFCNN.js +1 -0
  141. package/dist/m/c-T4VO4OCB.js +1 -0
  142. package/dist/m/c-TCCA5BZN.js +2 -0
  143. package/dist/m/c-TQEMVJCS.js +1 -0
  144. package/dist/m/c-TQZ2JYHD.js +1 -0
  145. package/dist/m/c-TR6RMDQ7.js +3 -0
  146. package/dist/m/c-TRS6TM6O.js +1 -0
  147. package/dist/m/c-UGHH4MB3.js +1 -0
  148. package/dist/m/c-UKHTLWUW.js +1 -0
  149. package/dist/m/c-UL2YUY52.js +1 -0
  150. package/dist/m/c-UNN7LWCT.js +1 -0
  151. package/dist/m/c-UXBMRDZB.js +235 -0
  152. package/dist/m/c-V7PZZRG7.js +1 -0
  153. package/dist/m/c-VAK7SG37.js +1 -0
  154. package/dist/m/c-VDSS4K3Z.js +1 -0
  155. package/dist/m/c-VFX45IZQ.js +1 -0
  156. package/dist/m/c-VHNSBD2M.js +1 -0
  157. package/dist/m/c-VKEDWYDF.js +1 -0
  158. package/dist/m/c-VL52ADDR.js +1 -0
  159. package/dist/m/c-WN5AY3OZ.js +1 -0
  160. package/dist/m/c-WNYBLIOZ.js +1 -0
  161. package/dist/m/c-WRBKEJD4.js +1 -0
  162. package/dist/m/c-WWDBYXLK.js +1 -0
  163. package/dist/m/c-WYFDLITL.js +1 -0
  164. package/dist/m/c-XN6MBKI6.js +67 -0
  165. package/dist/m/c-XNNW2CWK.js +1 -0
  166. package/dist/m/c-XPELB6E5.js +1 -0
  167. package/dist/m/c-XQXZAO7N.js +1 -0
  168. package/dist/m/c-Y6USGTS5.js +5 -0
  169. package/dist/m/c-YDH7Q3NY.js +1 -0
  170. package/dist/m/c-YG2NJYS6.js +135 -0
  171. package/dist/m/c-Z36J63PP.js +1 -0
  172. package/dist/m/c-Z4VRC7GU.js +7 -0
  173. package/dist/m/c-Z6K52OLP.js +19 -0
  174. package/dist/m/c-ZDY2MDAA.js +21 -0
  175. package/dist/m/c-ZE46QDIH.js +1 -0
  176. package/dist/m/c-ZUCOKOHT.js +1 -0
  177. package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
  178. package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
  179. package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
  180. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  181. package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
  182. package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
  183. package/dist/m/core/persist-HKHA66IY.js +3 -0
  184. package/dist/m/core/recovery-TBBSICE2.js +1 -0
  185. package/dist/m/core/save-WUCHTQBE.js +1 -0
  186. package/dist/m/core/save-core-FJVHT5M3.js +1 -0
  187. package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
  188. package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
  189. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  190. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  191. package/dist/m/events/index-23DPTUT4.js +1 -0
  192. package/dist/m/internals/index-WOVAP3RU.js +1 -0
  193. package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
  194. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  195. package/dist/m/loader-HXX6YNH5.js +1 -0
  196. package/dist/m/manifest-F22DEDAC.js +1 -0
  197. package/dist/m/manifest-edit-C3C7376B.js +1 -0
  198. package/dist/m/manifest.json +3491 -0
  199. package/dist/m/options/options-2LH76JQ3.js +1 -0
  200. package/dist/m/page/editor-XUTB2HB2.js +189 -0
  201. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
  202. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  203. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  204. package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
  205. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  206. package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
  207. package/dist/m/plugins/live-NUTTXSBS.js +1 -0
  208. package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
  209. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  210. package/dist/m/plugins/page-KSUH7G6J.js +2 -0
  211. package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
  212. package/dist/m/plugins/people-AJJ32YCG.js +1 -0
  213. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  214. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  215. package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
  216. package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
  217. package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
  218. package/dist/m/plugins/source-KHK5UUSK.js +1 -0
  219. package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
  220. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  221. package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
  222. package/dist/m/plugins/upload-XSGNILUY.js +1 -0
  223. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
  224. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
  225. package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
  226. package/dist/m/reader-core-ILWLXVMP.js +68 -0
  227. package/dist/m/route/route-KOCZSEGN.js +1 -0
  228. package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
  229. package/dist/m/ui/index-EVHKMKBL.js +1 -0
  230. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  231. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  232. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  233. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  234. package/dist/offline-manifest.json +3800 -0
  235. package/dist/page/themes/light-1.css +406 -0
  236. package/dist/server/core.cjs +23662 -22860
  237. package/dist/server/offline-runtime.cjs +4954 -0
  238. package/dist/server/offline-runtime.mjs +4923 -0
  239. package/dist/server/rooms.mjs +9 -0
  240. package/docs/guide.md +1078 -669
  241. package/entries/all.js +2 -0
  242. package/entries/clay-data.js +1 -1
  243. package/entries/clay-dom.js +2 -0
  244. package/entries/clay-events.js +2 -0
  245. package/entries/clay-internals.js +2 -0
  246. package/entries/clay-options.js +2 -0
  247. package/entries/clay-router.js +2 -0
  248. package/entries/clay-ui.js +2 -0
  249. package/entries/clay-utils.js +2 -0
  250. package/entries/clay.js +2 -0
  251. package/entries/sap.js +6 -6
  252. package/fonts/ibm-plex-mono-LICENSE +93 -0
  253. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  254. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  255. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  256. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  257. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  258. package/fonts/ibm-plex-sans-LICENSE +93 -0
  259. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  260. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  261. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  262. package/fonts/ibm-plex.css +71 -0
  263. package/fonts/inter-LICENSE +93 -0
  264. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  265. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  266. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  267. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  268. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  269. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  270. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  271. package/fonts/inter.css +62 -0
  272. package/fonts/newsreader-LICENSE +93 -0
  273. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  274. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  275. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  276. package/fonts/newsreader.css +26 -0
  277. package/fonts/work-sans-LICENSE +93 -0
  278. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  279. package/fonts/work-sans.css +8 -0
  280. package/package.json +20 -3
  281. package/packed-contract.json +1 -0
  282. package/src/attrs/add-from.js +27 -4
  283. package/src/attrs/save-freeze.js +7 -52
  284. package/src/core/admin-attrs.js +119 -12
  285. package/src/core/admin-contenteditable.js +12 -10
  286. package/src/core/admin-inputs.js +14 -15
  287. package/src/core/admin-onclick.js +2 -0
  288. package/src/core/autosave.js +74 -21
  289. package/src/core/conflict-download.js +22 -0
  290. package/src/core/conflict-notice-ui.js +1173 -0
  291. package/src/core/conflict-notice.js +140 -697
  292. package/src/core/edit-mode.js +147 -14
  293. package/src/core/etag.js +30 -12
  294. package/src/core/host-meta.js +72 -8
  295. package/src/core/host-permission.js +29 -0
  296. package/src/core/is-edit-mode.js +145 -12
  297. package/src/core/mode-key.js +125 -0
  298. package/src/core/mode-lifecycle.js +544 -0
  299. package/src/core/mode-receipt.js +30 -0
  300. package/src/core/mode-refusal.js +13 -0
  301. package/src/core/mode-resources.js +52 -0
  302. package/src/core/mode-support.js +82 -0
  303. package/src/core/mode-switch.js +259 -0
  304. package/src/core/mode-transition.js +112 -0
  305. package/src/core/page-data-write.js +305 -0
  306. package/src/core/page-data.js +26 -227
  307. package/src/core/page-format-wire.js +4 -0
  308. package/src/core/page-format.js +58 -0
  309. package/src/core/persist.js +37 -9
  310. package/src/core/preserve-authored.js +114 -0
  311. package/src/core/recovery.js +772 -0
  312. package/src/core/runtime-url.js +105 -0
  313. package/src/core/save-core.js +109 -12
  314. package/src/core/save-replace.js +72 -0
  315. package/src/core/save-size-notice.js +134 -0
  316. package/src/core/save-size.js +140 -0
  317. package/src/core/save.js +805 -118
  318. package/src/core/snapshot.js +121 -11
  319. package/src/core/source-map.js +157 -9
  320. package/src/core/unsaved-warning.js +14 -4
  321. package/src/core/upgrade-run.js +408 -0
  322. package/src/core/version.js +18 -0
  323. package/src/lib/assets.js +30 -0
  324. package/src/lib/autosave-debug-gate.js +50 -0
  325. package/src/lib/autosave-scheduler.js +141 -0
  326. package/src/lib/boot-imports.js +8 -0
  327. package/src/lib/derived.js +3 -1
  328. package/src/lib/dirty-gate.js +15 -0
  329. package/src/lib/doc-storage.js +33 -0
  330. package/src/lib/effect.js +9 -4
  331. package/src/lib/mutation.js +4 -2
  332. package/src/lib/offline-runtime.js +226 -0
  333. package/src/lib/preload.js +48 -0
  334. package/src/lib/root-attrs.js +1 -1
  335. package/src/lib/trash.js +18 -0
  336. package/src/lib/undo/capture-move.js +152 -0
  337. package/src/live/local-channel.js +61 -0
  338. package/src/live/local-lane.js +60 -9
  339. package/src/live/server-lane.js +6 -3
  340. package/src/loader-logic.js +76 -15
  341. package/src/loader.js +218 -30
  342. package/src/page/adopt-boot.js +76 -22
  343. package/src/page/adopt.js +25 -4
  344. package/src/page/awareness.js +230 -0
  345. package/src/page/blocks.js +79 -5
  346. package/src/page/blocksel.js +20 -4
  347. package/src/page/board-face.js +13 -0
  348. package/src/page/board-panel.js +5 -17
  349. package/src/page/board-style.js +2 -13
  350. package/src/page/board-view.js +1 -11
  351. package/src/page/board-views.js +17 -0
  352. package/src/page/board.js +2 -2
  353. package/src/page/boot-pack.js +26 -0
  354. package/src/page/boot.js +237 -0
  355. package/src/page/checklist.js +1 -1
  356. package/src/page/chrome.js +411 -61
  357. package/src/page/csv-import.js +17 -2
  358. package/src/page/document-title-dom.js +40 -0
  359. package/src/page/document-title-runtime.js +76 -0
  360. package/src/page/document-title.js +9 -0
  361. package/src/page/drag.js +48 -13
  362. package/src/page/drop.js +2 -1
  363. package/src/page/editor.js +330 -86
  364. package/src/page/feature-loader.js +72 -0
  365. package/src/page/features/board.js +12 -0
  366. package/src/page/features/code.js +13 -0
  367. package/src/page/features/media.js +290 -0
  368. package/src/page/features/registry.js +23 -0
  369. package/src/page/features/table.js +383 -0
  370. package/src/page/{tool-view.js → features/tool.js} +33 -2
  371. package/src/page/format-codec.js +215 -0
  372. package/src/page/format-gate.js +179 -0
  373. package/src/page/format-level.js +157 -0
  374. package/src/page/highlight-slot.js +23 -0
  375. package/src/page/highlight.js +30 -16
  376. package/src/page/history-intent.js +78 -0
  377. package/src/page/inline.js +18 -25
  378. package/src/page/items.js +52 -11
  379. package/src/page/link.js +1 -0
  380. package/src/page/markdown.js +86 -1
  381. package/src/page/media-views.js +3 -0
  382. package/src/page/media.js +33 -245
  383. package/src/page/outline-cache.js +11 -0
  384. package/src/page/placeholder.js +27 -7
  385. package/src/page/projection.js +196 -50
  386. package/src/page/reader-patch.js +306 -0
  387. package/src/page/recover.js +1 -1
  388. package/src/page/rich/page-tool.js +1 -4
  389. package/src/page/runtime-owner.js +27 -0
  390. package/src/page/save-projection.js +7 -4
  391. package/src/page/schema.js +14 -0
  392. package/src/page/table-rect-key.js +3 -0
  393. package/src/page/table-select.js +4 -3
  394. package/src/page/table-style.js +1 -1
  395. package/src/page/table-views.js +3 -0
  396. package/src/page/table.js +8 -293
  397. package/src/page/themes/light-1.css +139 -2
  398. package/src/page/themes/light-1.source.css +269 -0
  399. package/src/page/tool-capability.js +3 -0
  400. package/src/page/tool-views.js +3 -0
  401. package/src/page/view.js +390 -107
  402. package/src/page/ybind/awareness.js +172 -0
  403. package/src/page/ybind/binding.js +9 -1
  404. package/src/page/ybind/bridge.js +50 -47
  405. package/src/page/ybind/decode.js +52 -19
  406. package/src/page/ybind/doc.js +27 -2
  407. package/src/page/ybind/group.js +12 -3
  408. package/src/page/ybind/index.js +2 -0
  409. package/src/page/ybind/lineage.js +71 -0
  410. package/src/page/ybind/marks.js +25 -1
  411. package/src/page/ybind/migrate.js +48 -13
  412. package/src/page/ybind/moves.js +32 -21
  413. package/src/page/ybind/positions.js +50 -0
  414. package/src/page/ybind/provider.js +84 -22
  415. package/src/page/ybind/recovery-proof.js +191 -0
  416. package/src/page/ybind/recovery-replay.js +130 -0
  417. package/src/page/ybind/tokens.js +15 -6
  418. package/src/page/ybind/walk.js +10 -3
  419. package/src/page/ybind/writer.js +48 -4
  420. package/src/plugins/ai-edit.js +167 -32
  421. package/src/plugins/bind-text.js +218 -0
  422. package/src/plugins/cms.js +93 -0
  423. package/src/plugins/indicator.js +57 -5
  424. package/src/plugins/live.js +282 -34
  425. package/src/plugins/menu/claygoo.svg.js +1 -0
  426. package/src/plugins/menu/seo-head.js +323 -0
  427. package/src/plugins/menu/seo.js +849 -0
  428. package/src/plugins/menu.js +425 -0
  429. package/src/plugins/page-view.js +131 -0
  430. package/src/plugins/page.js +1022 -275
  431. package/src/plugins/people.js +158 -31
  432. package/src/plugins/richclay.js +119 -9
  433. package/src/plugins/sortable.js +370 -60
  434. package/src/plugins/source.js +189 -366
  435. package/src/plugins/undo.js +289 -19
  436. package/src/plugins/unsent.js +132 -0
  437. package/src/plugins/upgrade-run.js +1 -0
  438. package/src/plugins/upgrade.js +210 -0
  439. package/src/plugins/upload-task.js +571 -0
  440. package/src/plugins/upload-ui.js +265 -0
  441. package/src/plugins/upload.js +34 -153
  442. package/src/plugins/visitor-drafts/inert.js +32 -0
  443. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  444. package/src/plugins/visitor-drafts/prompt.js +2 -2
  445. package/src/plugins/visitor-drafts-owner.js +85 -0
  446. package/src/plugins/visitor-drafts.js +31 -42
  447. package/src/plugins/wire.js +82 -1
  448. package/src/reader-core.js +26 -0
  449. package/src/recovery/conflicts.js +287 -0
  450. package/src/recovery/identity.js +154 -0
  451. package/src/recovery/store.js +792 -0
  452. package/src/recovery/undo.js +257 -0
  453. package/src/server/checkpoint/index.js +68 -4
  454. package/src/server/offline-runtime/index.js +214 -0
  455. package/src/server/rooms/index.js +2 -1
  456. package/src/server/rooms/live-data-rooms.js +2 -2
  457. package/src/server/rooms/page-admission.js +33 -0
  458. package/src/server/rooms/page-lineage.js +56 -0
  459. package/src/server/rooms/page-rebase.js +11 -5
  460. package/src/server/rooms/page-state.js +2 -0
  461. package/src/server/rooms/page-write-admission.js +9 -3
  462. package/src/source/session.js +705 -0
  463. package/src/sync/conflict-revert.js +5 -4
  464. package/src/sync/conflicts-edit.js +119 -0
  465. package/src/sync/conflicts-facade.js +49 -0
  466. package/src/sync/conflicts.js +99 -1
  467. package/src/sync/editing-marker.js +37 -6
  468. package/src/sync/editing-sender.js +8 -1
  469. package/src/sync/live-sync-edit.js +63 -0
  470. package/src/sync/live-sync.js +364 -119
  471. package/src/sync/local-presence.js +187 -0
  472. package/src/sync/merge-declarations.js +10 -0
  473. package/src/sync/merge-engine.js +12 -0
  474. package/src/sync/presence-store.js +412 -0
  475. package/src/sync/presence-target.js +243 -0
  476. package/src/sync/presence.js +72 -41
  477. package/src/sync/saved-handoff.js +200 -0
  478. package/src/sync/saved-stream.js +535 -0
  479. package/src/sync/version-guard.js +92 -0
  480. package/src/sync/wire-profile.js +81 -0
  481. package/src/ui/bevel-controls.js +2 -8
  482. package/src/ui/page-scheme.js +7 -0
  483. package/src/ui/skins/cms.js +1 -1
  484. package/src/ui/theme-config.js +150 -0
  485. package/src/ui/theme.js +6 -138
  486. package/src/ui/toast-lazy.js +11 -0
  487. package/src/vendor/LICENSES-optional.md +253 -0
  488. package/src/vendor/highlight.vendor.js +3 -0
  489. package/src/vendor/hljs-bash.vendor.js +2 -0
  490. package/src/vendor/hljs-css.vendor.js +2 -0
  491. package/src/vendor/hljs-javascript.vendor.js +2 -0
  492. package/src/vendor/hljs-json.vendor.js +2 -0
  493. package/src/vendor/hljs-python.vendor.js +2 -0
  494. package/src/vendor/hljs-xml.vendor.js +2 -0
  495. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  496. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  497. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  498. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  499. package/src/vendor/hyper-morph.vendor.js +6 -6
  500. package/src/vendor/hyper-undo.vendor.js +1 -1
  501. package/src/vendor/hypercms.vendor.js +169 -84
  502. package/src/vendor/marked.vendor.js +68 -0
  503. package/src/vendor/overtype.vendor.js +994 -0
  504. package/src/vendor/richclay.vendor.js +17 -17
  505. package/src/vendor/sap-core.vendor.js +5 -5
  506. package/src/vendor/sap-ownership.vendor.js +11 -7
package/src/loader.js CHANGED
@@ -1,22 +1,67 @@
1
- import { resolveModules, MODULES } from "./loader-logic.js";
1
+ import { resolveModules, MODULES, PLUGIN_PATHS } from "./loader-logic.js";
2
2
  import onDomReady from "./lib/dom-ready.js";
3
- import { initTheme } from "./ui/theme.js";
3
+ import { initTheme } from "./ui/theme-config.js";
4
4
  import { ephemeral } from "./lib/ephemeral.js";
5
+ import { storage } from "./lib/doc-storage.js";
6
+ import { trash } from "./lib/trash.js";
5
7
  import { effect } from "./lib/effect.js";
6
8
  import { style } from "./lib/style.js";
7
9
  import { derived } from "./lib/derived.js";
10
+ import { assets } from "./lib/assets.js";
8
11
  import { bootPhase } from "./core/boot-phase.js";
12
+ import { mergeEngine } from "./sync/merge-engine.js";
13
+ import { mergeManifests, preloadEntries } from "./lib/preload.js";
14
+
15
+ // The plugin name a late path announces: the loader's own name-to-path table,
16
+ // read the other way.
17
+ const PLUGIN_NAMES = Object.fromEntries(
18
+ Object.entries(PLUGIN_PATHS).flatMap(([name, spec]) =>
19
+ spec.ownerPath ? [[spec.path, name], [spec.ownerPath, name]] : [[spec.path, name]]
20
+ )
21
+ );
22
+
23
+ // Every namespace this page imported, boot's plan and a live entry's alike: one record,
24
+ // so an entry skips what boot has and the edit members find their modules.
25
+ const loaded = {};
26
+
27
+ // clay:plugin-ready fires at most once per plugin per page: boot's late tail and a live
28
+ // entry can both reach the same module.
29
+ const announced = new Set();
30
+
31
+ function announce(path) {
32
+ if (announced.has(path)) return;
33
+ announced.add(path);
34
+ document.dispatchEvent(new CustomEvent("clay:plugin-ready", { detail: { plugin: PLUGIN_NAMES[path] } }));
35
+ }
36
+
37
+ // A live entry's hint fetch: the bootstrap's cached one, preloading the plan the entry will
38
+ // import. It lives here, beside the helper, because one more static edge into the reader's
39
+ // graph is one more chunk of every reader's closure.
40
+ const editHints = (loadEditHints, manifest, plan, hasSapJs) => () =>
41
+ loadEditHints().then((hints) => {
42
+ preloadEntries(mergeManifests(manifest, hints), [...plan.core, ...plan.plugins, ...plan.late], { isEditMode: true, hasSapJs });
43
+ return hints;
44
+ });
9
45
 
10
46
  function domReady() {
11
47
  return new Promise((resolve) => onDomReady(resolve));
12
48
  }
13
49
 
50
+ // Where this document came from, when it is a copy of a starter template.
51
+ function templateSource() {
52
+ const meta = document.querySelector('meta[name="hyper-source"]');
53
+ return meta ? meta.getAttribute("content") : null;
54
+ }
55
+
14
56
  // `base` has been unused since 1.1.0 (the loader imports through MODULES) and
15
57
  // stays anyway: /v1/clay.js and /v1/src/loader.js are cached independently, so
16
58
  // for a while after a deploy a browser can pair either file with the other's
17
59
  // previous release. A call shape that differs between them leaves the page with
18
- // no clayjs at all.
19
- export async function boot(base, params, readyResolve) {
60
+ // no clayjs at all. The optional arguments after the third come only from the chunked
61
+ // bootstrap: the manifest a reader downloads, the edit supplement an explicit
62
+ // ?editmode=true page fetched beside it, and the cached fetch of that supplement for
63
+ // every other resolved edit or draft. Their absence means no preloading.
64
+ export async function boot(base, params, readyResolve, manifest = null, supplement = null, loadEditHints = null) {
20
65
  initTheme();
21
66
  const draftMode = await MODULES["core/draft-mode.js"]();
22
67
  draftMode.noteBootParams(params);
@@ -26,7 +71,11 @@ export async function boot(base, params, readyResolve) {
26
71
  // would otherwise observe null
27
72
 
28
73
  const editMode = await MODULES["core/is-edit-mode.js"]();
29
- const { isEditMode, isOwner, siteVisibility } = editMode;
74
+ const { isEditMode, isDraftMode, isOwner, siteVisibility, canEdit } = editMode;
75
+
76
+ // A draft saved by its visitor never reaches the host, and hostMeta() answers
77
+ // bareHost() for it, so the request would fetch an answer nothing reads.
78
+ if (isEditMode && !isDraftMode) import("./core/host-permission.js").then((m) => m.checkHostPermission()).catch(() => {});
30
79
 
31
80
  // richclay's vendor build detects edit mode via this legacy global; set it
32
81
  // before any plugin import so its autoInit sees the right value. Overwrite
@@ -36,19 +85,67 @@ export async function boot(base, params, readyResolve) {
36
85
 
37
86
  const regionPolicy = await MODULES["lib/region-policy.js"]();
38
87
 
39
- const { isDraftMode } = editMode;
40
-
41
88
  const hasSap = Boolean(document.querySelector("[sap]"));
89
+ // derive.js imports the sapjs core only when no sap.js script took the page over.
90
+ const hasSapJs = Boolean(document.querySelector('script[src*="sap.js"]'));
42
91
  bootPhase.holdSettle = hasSap && isEditMode;
43
- const plan = resolveModules(params, isEditMode, { isDraftMode, hasSap });
44
- const loaded = {};
92
+ const plan = resolveModules(params, isEditMode, { isDraftMode, hasSap, canEdit, templateSource: templateSource() });
93
+ // Where edits land, resolved once: a visitor draft saves into its draft slot, the
94
+ // demo plugin into localStorage, and everything else to the host. setMode takes no
95
+ // destination, and V4 has no draft-to-host transition.
96
+ if (plan.plugins.includes("plugins/demo.js")) editMode.commitModeFacts({ destination: "demo" });
97
+ preloadEntries(mergeManifests(manifest, supplement), [...plan.core, ...plan.plugins], { isEditMode, isDraftMode, hasSapJs });
98
+ // A resolved initial edit or draft fetches the release's hint index, whether or not
99
+ // the URL announced it. It lands beside preparation and only ever adds modulepreloads:
100
+ // a failed fetch defers hints, never an import already mapped.
101
+ if ((isEditMode || isDraftMode) && !supplement && loadEditHints) {
102
+ loadEditHints()
103
+ .then((hints) => preloadEntries(mergeManifests(manifest, hints), [...plan.core, ...plan.plugins], { isEditMode, isDraftMode, hasSapJs }))
104
+ .catch(() => {});
105
+ }
106
+ // Not awaited by boot: an entry that overlaps the tail waits on this rather than
107
+ // import the same paths a second time.
108
+ let settleLate;
109
+ const lateDone = new Promise((resolve) => { settleLate = resolve; });
110
+ // The reader's transport choice comes from the plan just resolved, never from the
111
+ // URL: when generic sync loaded, it owns the saved lane and the reader follows
112
+ // the frames it applies rather than opening a second stream.
113
+ const readerOptions = { sync: plan.plugins.includes("sync/live-sync.js") };
45
114
 
46
115
  for (const path of plan.core) {
47
116
  loaded[path] = await MODULES[path](); // sequential: order is load-bearing
117
+ // The loader starts what it imports: a target that exports `install` gets it called
118
+ // here, once, in this order. A module another road reaches (a live switch, a second
119
+ // entry) then reuses that same installation instead of adding a second one.
120
+ loaded[path].install?.();
48
121
  }
49
122
 
50
- assembleCore(loaded, { isDraftMode, isEditMode, isOwner, siteVisibility }, regionPolicy); // window.clay MUST be assembled
51
- // before any plugin import
123
+ assembleCore(loaded, { isDraftMode, isOwner, siteVisibility }, regionPolicy);
124
+ attachEditCore(loaded); // window.clay MUST be assembled before any plugin import
125
+ // A document that signs the live-switch contract hands its boot to the mode dispatcher.
126
+ // Nothing is fetched for it here: the decision and the controller behind it wait for the
127
+ // first switch, so this costs a reader with a declaration no more than one attribute.
128
+ if (document.documentElement.getAttribute("clay-mode-switch") === "live") {
129
+ const editing = resolveModules(params, true, { isDraftMode, hasSap, canEdit, templateSource: templateSource() });
130
+ loaded["core/edit-mode.js"].noteLiveSwitch({
131
+ edit: editing,
132
+ // The plan a view boot would have resolved, not the edit plan this boot resolved: an
133
+ // exit imports it and starts its reader, and an edit-only module belongs to the
134
+ // session being left.
135
+ view: resolveModules(params, false, { isDraftMode, hasSap, canEdit, templateSource: templateSource() }),
136
+ loaded,
137
+ modules: MODULES,
138
+ attach: attachPluginMember,
139
+ attachEditCore,
140
+ announce,
141
+ loadEditHints: loadEditHints ? editHints(loadEditHints, manifest, editing, hasSapJs) : null,
142
+ late: lateDone,
143
+ });
144
+ }
145
+ // An explicit initial edit drains the preboot participant queue and enables its
146
+ // leases before a plugin can take input. A reader returns at the first line.
147
+ await loaded["core/edit-mode.js"].activateInitialMode();
148
+ window.clay.plugins = Object.freeze([...plan.plugins, ...plan.late].map((path) => PLUGIN_NAMES[path]));
52
149
 
53
150
  for (const path of plan.plugins) {
54
151
  // A plugin that throws on evaluation degrades to absent. Unguarded, its
@@ -64,6 +161,7 @@ export async function boot(base, params, readyResolve) {
64
161
  continue;
65
162
  }
66
163
  loaded[path] = mod;
164
+ if (path === "plugins/page-view.js") mod.start(readerOptions);
67
165
  attachPluginMember(path, mod, loaded); // immediately, not after the loop: hypercms's ?cms=true
68
166
  // auto-open runs as a microtask queued during ITS evaluation
69
167
  // (before boot resumes) and reads clay.RichClay and clay.undo,
@@ -76,9 +174,50 @@ export async function boot(base, params, readyResolve) {
76
174
 
77
175
  if (hasSap) mountDerivedEngine(loaded);
78
176
 
79
- installViewModeNotice(isEditMode);
177
+ installViewModeNotice(editMode);
80
178
  readyResolve(window.clay);
81
179
  document.dispatchEvent(new CustomEvent("clay:ready", { detail: { clay: window.clay } }));
180
+ if (plan.late.length) loadLate(plan.late, editMode).then(settleLate, settleLate);
181
+ else settleLate();
182
+ }
183
+
184
+ // An edit-only plugin belongs to the edit session that resolved it. Once the page has
185
+ // left edit, importing it would start an editor in a view, so the tail drops it instead:
186
+ // the module is not fetched, not attached and not announced. Dropping is not a reload —
187
+ // nothing is re-imported with a cache-busting URL, and a later entry asks the module
188
+ // cache for the same single instance.
189
+ //
190
+ // A transition in flight counts as left: until it commits, `isEditMode` still reads the
191
+ // mode being left, and a module that evaluated then would start an unadopted session. The
192
+ // modules ask the same question themselves (`startsAtImport`), so an import already in
193
+ // flight when the exit began starts nothing either.
194
+ const stale = (path, editMode) =>
195
+ PLUGIN_PATHS[PLUGIN_NAMES[path]]?.editOnly && !editMode.startsAtImport();
196
+
197
+ // Plugins no first paint or keystroke waits for. They load after the editor takes input
198
+ // (right away on a page without one), one at a time and in plan order, and a failure
199
+ // degrades to absent as at boot.
200
+ async function loadLate(paths, editMode) {
201
+ try {
202
+ await window.clay.page?.ready;
203
+ } catch {}
204
+ for (const path of paths) {
205
+ if (stale(path, editMode)) continue;
206
+ try {
207
+ const mod = await MODULES[path]();
208
+ // The session can end while the fetch is in flight, so ask again before the module
209
+ // is given a page to wire itself into.
210
+ if (stale(path, editMode)) continue;
211
+ loaded[path] = mod;
212
+ attachPluginMember(path, mod, loaded);
213
+ await mod.ready;
214
+ if (stale(path, editMode)) continue;
215
+ announce(path);
216
+ } catch (err) {
217
+ console.error(`clayjs: plugin "${path}" failed to load, continuing without it:`, err);
218
+ document.dispatchEvent(new CustomEvent("clay:plugin-failed", { detail: { plugin: PLUGIN_NAMES[path] } }));
219
+ }
220
+ }
82
221
  }
83
222
 
84
223
  // The explicit mount phase for a derive engine (sapjs or its core). It runs after
@@ -104,7 +243,7 @@ export function mountDerivedEngine(loaded) {
104
243
  if (save && fileSignature != null) save.schedulePaintHeal(fileSignature);
105
244
  }
106
245
 
107
- function assembleCore(loaded, { isDraftMode, isEditMode, isOwner, siteVisibility }, regionPolicy) {
246
+ function assembleCore(loaded, { isDraftMode, isOwner, siteVisibility }, regionPolicy) {
108
247
  const clay = window.clay;
109
248
 
110
249
  const mutation = loaded["lib/mutation.js"];
@@ -112,63 +251,106 @@ function assembleCore(loaded, { isDraftMode, isEditMode, isOwner, siteVisibility
112
251
 
113
252
  Object.assign(clay, {
114
253
  toggleEditMode: editModeMod.toggleEditMode,
254
+ setMode: editModeMod.setMode,
255
+ modeLifecycle: editModeMod.modeLifecycle,
115
256
  isDraftMode,
116
- isEditMode,
117
257
  isOwner,
118
258
  siteVisibility,
119
259
  Mutation: mutation.default,
120
260
  region: regionPolicy.windowRegionShape,
121
261
  });
122
262
 
123
- const snapshot = loaded["core/snapshot.js"];
124
- const save = loaded["core/save.js"];
125
- const cacheBustMod = loaded["lib/cache-bust.js"];
126
- const hyperMorph = loaded["vendor/hyper-morph.vendor.js"];
263
+ // Getters over the live mode facts, never copies: the lifecycle commits a new mode
264
+ // and these read it back, so `clay.mode` stays a string that cannot go stale.
265
+ for (const key of ["mode", "isEditMode", "canEdit"]) {
266
+ Object.defineProperty(clay, key, { get: () => editModeMod[key], enumerable: true, configurable: true });
267
+ }
268
+
269
+ clay.assets = assets;
270
+
127
271
  const pageData = loaded["core/page-data.js"];
128
272
 
129
273
  clay.readData = pageData.readData;
130
274
  clay.writeData = pageData.writeData;
131
275
  clay.ephemeral = ephemeral;
276
+ clay.storage = storage;
277
+ // About the document rather than the page, and asked for on use: a reader never
278
+ // downloads the discovery client for it.
279
+ clay.host = {
280
+ get versions() {
281
+ return import("./core/host-meta.js").then((m) => m.hostVersions());
282
+ },
283
+ };
284
+ clay.trash = trash;
132
285
  clay.derived = derived;
133
286
  clay.effect = effect;
134
287
  clay.style = style;
288
+ clay.morph = (oldEl, newEl, options) => mergeEngine().then(({ morph }) => morph(oldEl, newEl, options));
289
+ }
290
+
291
+ // The edit half of the assembly: boot runs it right after the always half, and a live entry
292
+ // runs it once its edit core has landed. It adds what those modules offer and replaces
293
+ // nothing already there, so a member a reader attached first survives.
294
+ function attachEditCore(loaded) {
295
+ const clay = window.clay;
135
296
 
297
+ const save = loaded["core/save.js"];
136
298
  if (save) {
137
299
  const saveFn = save.savePage || save.default;
138
300
  saveFn.force = save.savePageForce;
139
301
  saveFn.overwrite = save.saveOverwritingConflict;
140
302
  saveFn.flush = save.flushSave;
141
- clay.save = saveFn;
303
+ saveFn.soon = save.savePageSoon;
304
+ // What the host says it will store, and a measurement of candidate bytes. Both
305
+ // are asked for on use: a reader has neither, and a page that never asks pays
306
+ // nothing.
307
+ saveFn.limits = save.saveLimits;
308
+ saveFn.inspect = save.inspectSaveSize;
309
+ // An upgrade-capable reader's temporary save object hands its `replace` to the function.
310
+ if (clay.save !== saveFn && typeof clay.save?.replace === "function") saveFn.replace = clay.save.replace;
311
+ if (clay.save !== saveFn) clay.save = saveFn;
142
312
  }
143
313
  // The close warning's own predicate, reachable without a capture. It falls back to
144
314
  // false rather than to absent: the module is edit-only, and a document that cannot
145
315
  // save has nothing unsaved. Older clayjs has no such member, and that is the answer
146
316
  // a host feature-detects.
147
- clay.hasUnsavedChanges = loaded["core/unsaved-warning.js"]?.hasUnsavedChanges ?? (() => false);
317
+ clay.hasUnsavedChanges =
318
+ loaded["core/unsaved-warning.js"]?.hasUnsavedChanges ?? clay.hasUnsavedChanges ?? (() => false);
319
+ const snapshot = loaded["core/snapshot.js"];
148
320
  if (snapshot) {
149
- clay.getHTML = snapshot.getPageContents;
150
- clay.addDocumentTransform = snapshot.addDocumentTransform;
151
- clay.onSnapshot = snapshot.onSnapshot;
321
+ if (!clay.getHTML) clay.getHTML = snapshot.getPageContents;
322
+ if (!clay.addDocumentTransform) clay.addDocumentTransform = snapshot.addDocumentTransform;
323
+ if (!clay.onSnapshot) clay.onSnapshot = snapshot.onSnapshot;
152
324
  }
153
- if (cacheBustMod) {
325
+ const cacheBustMod = loaded["lib/cache-bust.js"];
326
+ if (cacheBustMod && !clay.cacheBust) {
154
327
  clay.cacheBust = cacheBustMod.default;
155
328
  }
156
- if (hyperMorph) clay.morph = hyperMorph.morph;
329
+ // Edit mode only, and absent entirely in view mode: a reader has nothing to
330
+ // recover and must never open the database. The facade is here; the store behind
331
+ // it arrives when the page's first adapter registers a channel.
332
+ const recoveryMod = loaded["core/recovery.js"];
333
+ if (recoveryMod && !clay.recovery) clay.recovery = recoveryMod.recovery;
157
334
  }
158
335
 
159
- function attachPluginMember(path, mod, loaded) {
336
+ function attachPluginMember(path, mod, namespaces = loaded) {
160
337
  const clay = window.clay;
161
338
 
162
339
  if (path === "plugins/undo.js") {
163
340
  clay.undo = mod.undo || mod.default;
341
+ } else if (path === "plugins/unsent.js") {
342
+ clay.unsent = mod.unsent;
164
343
  } else if (path === "sync/live-sync.js") {
165
344
  clay.morph = mod.morph;
166
345
  clay.conflicts = mod.conflicts;
167
- loaded["core/page-data.js"].setPageDataLiveSync(mod.liveSync);
346
+ namespaces["core/page-data.js"].setPageDataLiveSync(mod.liveSync);
168
347
  } else if (path === "plugins/live.js") {
169
348
  clay.live = mod.live;
170
349
  } else if (path === "plugins/page.js") {
171
350
  clay.page = mod.page;
351
+ } else if (path === "plugins/upgrade.js") {
352
+ clay.save = clay.save || {};
353
+ clay.save.replace = mod.replace;
172
354
  } else if (path === "plugins/cms.js") {
173
355
  clay.cms = mod.cms || mod.default;
174
356
  } else if (path === "plugins/upload.js") {
@@ -189,17 +371,23 @@ function attachPluginMember(path, mod, loaded) {
189
371
  clay.RichClay = mod.RichClay || mod.default;
190
372
  } else if (path === "plugins/quickcrop.js") {
191
373
  clay.quickcrop = mod.quickcrop || mod.default;
374
+ } else if (path === "plugins/sortable.js") {
375
+ // With the module, not with its vendor: a move answers before a drag can start.
376
+ clay.sortable = mod.sortable;
192
377
  } else if (path === "plugins/source.js") {
193
378
  clay.source = mod.source || mod.default;
194
- } else if (path === "plugins/visitor-drafts.js") {
379
+ } else if (path === "plugins/visitor-drafts.js" || path === "plugins/visitor-drafts-owner.js") {
195
380
  clay.visitorDrafts = mod.visitorDrafts;
381
+ } else if (path === "plugins/bind-text.js") {
382
+ clay.bindText = mod.bindText;
196
383
  }
197
384
  }
198
385
 
199
- function installViewModeNotice(isEditMode) {
200
- if (isEditMode) return;
386
+ function installViewModeNotice(editMode) {
387
+ if (editMode.isEditMode) return;
201
388
  document.addEventListener("click", (event) => {
202
389
  if (!event.target.closest?.("[trigger-save]")) return;
390
+ if (editMode.isEditMode) return;
203
391
  console.info("clayjs: you're not the owner of this page; changes stay local");
204
392
  document.dispatchEvent(new CustomEvent("clay:view-save-attempt"));
205
393
  });
@@ -10,9 +10,27 @@ import { mergePageModels } from "./merge.js";
10
10
  import { blockMap } from "./model.js";
11
11
  import { parseModel } from "./projection.js";
12
12
  import { ARTICLE, pageTags } from "./save-projection.js";
13
- import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
13
+ import { parseJsonRelaxed } from "../vendor/hyper-morph-json.vendor.js";
14
+ import { loadedEngine, mergeEngine } from "../sync/merge-engine.js";
14
15
 
15
- const parseTag = (text) => { try { return HyperMorph.parseJsonRelaxed(text); } catch { return null; } };
16
+ const parseTag = (text) => { try { return parseJsonRelaxed(text); } catch { return null; } };
17
+
18
+ // What one adoption attempt did, so a caller can tell a landed adoption from a
19
+ // stopped, refused or failed one. `ok` is true only when the store holds the
20
+ // accepted state afterwards, whether that meant splicing the file in or finding
21
+ // nothing to splice.
22
+ const UNCHANGED = { ok: true, outcome: "unchanged" };
23
+ const STOPPED = { ok: false, outcome: "disposed", reason: "adoption-disposed" };
24
+ const REFUSED = { ok: false, outcome: "refused", reason: "adoption-refused" };
25
+ const TIMED_OUT = { ok: false, outcome: "refused", reason: "adoption-timeout" };
26
+ const FAILED = { ok: false, outcome: "error", reason: "adoption-error" };
27
+ // Another tab holds the room's writer seat: the adoption is that tab's to land,
28
+ // and it reaches this one through the room. Not a refusal, and not a success yet.
29
+ const AWAITED = { ok: true, outcome: "writer-adopts" };
30
+ const landed = (counts) => ({ ok: true, outcome: "adopted", counts });
31
+
32
+ // The bound on adoption, and on waiting for the elected writer's adoption.
33
+ export const ADOPTION_TIMEOUT_MS = 10_000;
16
34
 
17
35
  export function readFile(root) {
18
36
  const article = root.querySelector(ARTICLE);
@@ -40,7 +58,7 @@ export function installAdoption({
40
58
  adopted = () => {},
41
59
  hold = suspendAutosave,
42
60
  release = resumeAutosave,
43
- holdMs = 10_000,
61
+ holdMs = ADOPTION_TIMEOUT_MS,
44
62
  doc = document,
45
63
  }) {
46
64
  let last = source ? source.article.innerHTML : null;
@@ -81,6 +99,7 @@ export function installAdoption({
81
99
 
82
100
  // The editor merged three-way with what the file changed since base.
83
101
  function land(blocks, current, baseItems, incoming) {
102
+ const { HyperMorph } = loadedEngine();
84
103
  const merged = mergePageModels(baseItems, current, incoming, { doc, merge3: HyperMorph.merge3, mergeJson: HyperMorph.mergeJson });
85
104
  const lost = merged.conflicts.filter((k) => k.kind === "text" || k.kind === "field");
86
105
  const copy = lost.length ? snapshot() : null;
@@ -101,16 +120,25 @@ export function installAdoption({
101
120
  if (mergeTagless && !file.tags.length) return adoptTagless(file);
102
121
  const found = classifyFile(file.article, file.tags);
103
122
  if (found.kind === "unbased") console.warn("page: the article and its data changed together; kept the data");
104
- if (found.kind !== "adopt" || !isAdopter()) return null;
123
+ if (found.kind !== "adopt") return UNCHANGED;
124
+ // Only the room's writer adopts, so another tab's write is this tab's adoption
125
+ // too: it arrives through the room, and the caller waits for it there.
126
+ if (!isAdopter()) return AWAITED;
105
127
  hold();
106
128
  try {
129
+ await mergeEngine();
130
+ if (disposed) return STOPPED;
131
+ if (!isAdopter()) return REFUSED;
107
132
  const waited = await waitForEditor();
108
133
  if (waited === "timeout") console.warn("page: gave up waiting for the editor; the hand edit was not adopted");
109
- if (waited !== "ready" || disposed || !isAdopter()) return null;
134
+ if (waited === "disposed" || disposed) return STOPPED;
135
+ if (waited !== "ready") return TIMED_OUT;
136
+ if (!isAdopter()) return REFUSED;
110
137
  const blocks = blockMap(view.state.doc);
111
138
  const counts = land(blocks, readBack(blocks, doc), found.baseItems, found.parsed);
112
- if (counts) adopted(found.A);
113
- return counts;
139
+ if (!counts) return UNCHANGED;
140
+ adopted(found.A);
141
+ return landed(counts);
114
142
  } finally {
115
143
  release();
116
144
  }
@@ -121,39 +149,60 @@ export function installAdoption({
121
149
  const list = (rev && seen.get(rev)) || [];
122
150
  const base = list.at(-1) ?? disk;
123
151
  // An untouched older copy leaves the page as it is.
124
- if (list.slice(0, -1).some((article) => article.innerHTML === file.article.innerHTML)) return null;
152
+ if (list.slice(0, -1).some((article) => article.innerHTML === file.article.innerHTML)) return UNCHANGED;
125
153
  record(file.article);
126
154
  disk = file.article;
127
- if (!base) return null;
155
+ if (!base) return UNCHANGED;
156
+ // The latest copy again, attributes and all: merging it into the page changes nothing.
157
+ if (base.outerHTML === file.article.outerHTML) return UNCHANGED;
128
158
  // Only the room's elected writer merges a frame, so it lands once however
129
159
  // many tabs see it; and it lands even when the editor never settles, since
130
160
  // the next save would otherwise write over the edit.
131
161
  hold();
132
162
  try {
163
+ await mergeEngine();
164
+ if (disposed) return STOPPED;
165
+ if (!isAdopter()) return REFUSED;
133
166
  await writerReady();
134
- if (disposed || !isAdopter()) return null;
167
+ if (disposed) return STOPPED;
168
+ if (!isAdopter()) return REFUSED;
135
169
  const waited = await waitForEditor();
136
- if (waited === "disposed" || disposed || !isAdopter()) return null;
170
+ if (waited === "disposed" || disposed) return STOPPED;
171
+ if (!isAdopter()) return REFUSED;
137
172
  const blocks = blockMap(view.state.doc);
138
173
  const current = readBack(blocks, doc);
139
174
  const known = new Map(current.map((item) => [item.id, item]));
140
175
  const parse = (article) => parseModel(article, { seed: article.getAttribute("data-rev") ?? "", known });
141
- return land(blocks, current, parse(base), parse(file.article));
176
+ const counts = land(blocks, current, parse(base), parse(file.article));
177
+ return counts ? landed(counts) : UNCHANGED;
142
178
  } finally {
143
179
  release();
144
180
  }
145
181
  }
146
182
 
147
- function run(file) {
183
+ // A failed attempt is an outcome, never a resolved success: the coordinator
184
+ // decides on `ok`, and the outside world keeps its events and quiet queue.
185
+ async function attempt(file) {
186
+ try {
187
+ return await adopt(file);
188
+ } catch (error) {
189
+ console.error("page: adoption failed", error);
190
+ return FAILED;
191
+ }
192
+ }
193
+
194
+ let bootOutcome = null;
195
+ function run(file, boot = false) {
148
196
  queue = queue
149
- .then(() => adopt(file))
150
- .then((counts) => {
151
- if (!counts) return;
197
+ .then(() => attempt(file))
198
+ .then((outcome) => {
199
+ if (boot) bootOutcome = outcome;
200
+ if (!outcome.ok || outcome.outcome !== "adopted") return outcome;
152
201
  requestCheckpoint();
153
- notice(counts);
154
- doc.dispatchEvent(new CustomEvent("clay:page-adopted", { detail: counts }));
155
- })
156
- .catch((error) => console.error("page: adoption failed", error));
202
+ notice(outcome.counts);
203
+ doc.dispatchEvent(new CustomEvent("clay:page-adopted", { detail: outcome.counts }));
204
+ return outcome;
205
+ });
157
206
  return queue;
158
207
  }
159
208
 
@@ -167,10 +216,15 @@ export function installAdoption({
167
216
  }
168
217
 
169
218
  doc.addEventListener("clay:sync-applied", onFrame);
170
- if (source) run(source);
219
+ if (source) run(source, true);
171
220
 
172
221
  return {
173
- idle: () => queue,
222
+ // Resolves once everything queued so far has been attempted, with the boot
223
+ // file's own outcome: landed, unchanged, stopped, refused or failed.
224
+ idle: async () => {
225
+ await queue;
226
+ return bootOutcome;
227
+ },
174
228
  // A save of this tab landed: the file holds what it sent, so that article is
175
229
  // the one an outside edit made on the saved file merges against.
176
230
  saved(html) {
package/src/page/adopt.js CHANGED
@@ -9,7 +9,8 @@
9
9
  // Cmd+Z takes the whole adoption back.
10
10
  import { model as pmModel, history } from "../vendor/prosemirror.vendor.js";
11
11
  import { blockMap, nodeFromBlock, repairSiblings } from "./model.js";
12
- import { project, canon, revOf, parseModel, renderModel, fragmentCleaner } from "./projection.js";
12
+ import { DEFAULTS, project, canon, revOf, parseModel, renderModel, fragmentCleaner, unitsOf } from "./projection.js";
13
+ import { unflatten } from "./inline.js";
13
14
  import { blocksFromPageValues } from "./ybind/decode.js";
14
15
 
15
16
  export const ADOPT = "page-adopt";
@@ -56,7 +57,7 @@ export function blockFromItem(item, cur) {
56
57
  const { id, t, ...fields } = item;
57
58
  const out = { t, pos: cur?.pos ?? null };
58
59
  Object.assign(out, fields);
59
- if (t === "table" && !fields.rows.length) fields.rows = [{ id: `r-${id}`, cells: {} }];
60
+ if (t === "table" && !fields.rows.length && fields.cols.length) fields.rows = [{ id: `r-${id}`, cells: {} }];
60
61
  if (t === "table") {
61
62
  out.cols = keyed(fields.cols, cur?.cols, "c", ({ id: _i, ...c }) => c);
62
63
  out.rows = keyed(fields.rows, cur?.rows, "r", ({ id: _i, cells, pos }) => ({ pos, cells }));
@@ -74,6 +75,23 @@ export function blockFromItem(item, cur) {
74
75
 
75
76
  // ---- the transaction ---------------------------------------------------------
76
77
 
78
+ // A block type this runtime has no schema for keeps its whole record in the
79
+ // inert node, and its text for mapping.
80
+ const opaqueNode = (schema, id, block) => {
81
+ const { t, pos, c, m, ...raw } = block;
82
+ const content = typeof c === "string" && c ? unflatten(schema, c, unitsOf(m, c.length), schema.nodes.opaque_block) : null;
83
+ return schema.nodes.opaque_block.create({ id, pos, raw: { t, ...raw } }, content);
84
+ };
85
+
86
+ // The editor’s blocks as the article and the record state them: an opaque node
87
+ // stands for the record it carries, never for a block type of its own.
88
+ const recordsOf = (blocks) => Object.fromEntries(Object.entries(blocks).map(([id, b]) => {
89
+ if (b.t !== "opaque_block") return [id, b];
90
+ const { t: _t, raw, ...rest } = b;
91
+ const { t, ...fields } = raw ?? {};
92
+ return [id, { t: t ?? "paragraph", pos: b.pos, ...fields, ...rest }];
93
+ }));
94
+
77
95
  function find(doc, id) {
78
96
  let found = null;
79
97
  doc.forEach((node, pos) => { if (!found && node.attrs.id === id) found = { node, pos }; });
@@ -119,7 +137,7 @@ export function adoptionTransaction(state, plan) {
119
137
  if (!plan) return null;
120
138
  const schema = state.schema;
121
139
  const doc = state.doc;
122
- const current = blockMap(doc);
140
+ const current = recordsOf(blockMap(doc));
123
141
  const tr = state.tr;
124
142
  const projected = new Map(project(current).map((item) => [item.id, item]));
125
143
  const displaced = {};
@@ -127,7 +145,10 @@ export function adoptionTransaction(state, plan) {
127
145
  const was = plan.base.get(id), now = projected.get(id);
128
146
  if (was && now && !same(body(was), body(now))) displaced[id] = current[id];
129
147
  }
130
- const nodeOf = (id, item) => nodeFromBlock(schema, id, blockFromItem(item, current[id]));
148
+ const nodeOf = (id, item) => {
149
+ const block = blockFromItem(item, current[id]);
150
+ return Object.hasOwn(DEFAULTS, item.t) ? nodeFromBlock(schema, id, block) : opaqueNode(schema, id, block);
151
+ };
131
152
 
132
153
  // Adopting into a page that had no blocks: the editor's empty placeholder line
133
154
  // goes, after the article's blocks are in (an empty doc would get a new one).