@panphora/clayjs 1.15.2 → 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
@@ -41,10 +41,9 @@
41
41
  * invisible to the mutation system (its status text changes constantly), absent from
42
42
  * every snapshot and from what peers receive.
43
43
  */
44
- import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
45
- import { mergeTagRecognizers } from "../sync/merge-tags.js";
44
+ import { loadedEngine, mergeEngine } from "../sync/merge-engine.js";
46
45
  import Mutation from "../lib/mutation.js";
47
- import { isEditMode } from "../core/is-edit-mode.js";
46
+ import { startsAtImport } from "../core/is-edit-mode.js";
48
47
  import { holdAllSaves, releaseAllSaves } from "../core/save.js";
49
48
  import { STRIP_FROM_SAVE, SNAPSHOT_REMOVE_SELECTOR } from "../lib/region-policy.js";
50
49
  import { enableContentEditable } from "../core/admin-contenteditable.js";
@@ -81,7 +80,10 @@ let barMode = null; // "working", "ready", "saving" or "error"
81
80
  let barLifecycle = null;
82
81
  let barCloseTimer = null;
83
82
  let panelPointerDown = false; // the last pointerdown began in the panel: its click is not a page click
84
- const drafts = new WeakMap(); // compose text a target keeps while the panel is shut
83
+ let drafts = new WeakMap(); // compose text a target keeps while the panel is shut
84
+ // A WeakMap cannot be read back, and an exit has to know whether anything typed here is
85
+ // still unsent, so the targets that hold a draft are counted beside it.
86
+ const drafted = new Set();
85
87
  let anchorEl = null; // element the panel is currently anchored to
86
88
  let chipTarget = null; // unit the hover chip currently points at
87
89
  let chipHideTimer = null;
@@ -92,6 +94,24 @@ let selectionTimer = null;
92
94
  let anchorRange = null; // the selection the open panel was raised for
93
95
  let returnFocus = null; // where focus goes back to when the panel closes
94
96
  let helperState = null; // "ready" or "unavailable"
97
+ let listeners = null; // the AbortController every handler one session added listens on
98
+ let observers = []; // the page observers that session opened
99
+ let styleEl = null; // the one stylesheet the chrome needed
100
+ let chromeToken = 0; // the latest init(); a teardown retires the one before it
101
+
102
+ function dropDraft(el) {
103
+ drafts.delete(el);
104
+ drafted.delete(el);
105
+ }
106
+
107
+ function keepDraft(el, text) {
108
+ if (!text) {
109
+ dropDraft(el);
110
+ return;
111
+ }
112
+ drafts.set(el, text);
113
+ drafted.add(el);
114
+ }
95
115
 
96
116
  const HIGHLIGHT = 'clay-ai-edit';
97
117
  const OFF_MESSAGE = 'AI editing is turned off. Turn it on from the app’s menu.';
@@ -190,6 +210,7 @@ function strippedBodyHTML() {
190
210
  // ---------------------------------------------------------------- morphing
191
211
 
192
212
  function morphTo(el, content, scripts) {
213
+ const { HyperMorph } = loadedEngine();
193
214
  return HyperMorph.morph(el, content, { morphStyle: 'outerHTML', restoreFocus: true, scripts });
194
215
  }
195
216
 
@@ -201,6 +222,7 @@ function morphTo(el, content, scripts) {
201
222
  // against the snapshot the model saw, so data the user changed mid-flight survives
202
223
  // the reply. Rewinds must restore the snapshot exactly, so they disable merging.
203
224
  function sessionMorph(s, content, { rewind = false } = {}) {
225
+ const { HyperMorph, mergeTagRecognizers } = loadedEngine();
204
226
  // handle:false: a reply's scripts never run, so the preview cannot execute code
205
227
  // before Keep, and neither can a rewind.
206
228
  const scripts = rewind
@@ -300,7 +322,7 @@ async function sendRequest(el, comment, quote) {
300
322
  }
301
323
  // The sent comment is this target's draft until the reply is ready: Error, Stop, a
302
324
  // refused reply, a morph failure and a host cancellation all leave it in place.
303
- drafts.set(el, comment);
325
+ keepDraft(el, comment);
304
326
  closePanelForSend();
305
327
  session.paused = true;
306
328
  pauseObservers();
@@ -321,6 +343,9 @@ async function sendRequest(el, comment, quote) {
321
343
  session.handle = handle;
322
344
  const outcome = await handle.done;
323
345
  if (!session || session.handle !== handle) return;
346
+ // The engine is fetched here, where a reply is applied, not at boot.
347
+ await mergeEngine();
348
+ if (!session || session.handle !== handle) return;
324
349
  if (outcome.state === 'done') onDone(outcome.result || {});
325
350
  else if (outcome.state === 'cancelled') onError('HTML Clay stopped this edit.');
326
351
  else onError(outcome.error || 'the helper reported an error');
@@ -417,7 +442,7 @@ function onDone(payload) {
417
442
  return;
418
443
  }
419
444
  positionChrome();
420
- drafts.delete(session.el); // delivered: the comment is no longer this target's draft
445
+ dropDraft(session.el); // delivered: the comment is no longer this target's draft
421
446
  showBar('ready');
422
447
  setBarStatus(readyText(warnings, payload.model), warnings.length ? 'warn' : '');
423
448
  }
@@ -444,7 +469,7 @@ function revertSession() {
444
469
  session = null;
445
470
  if (!s) return;
446
471
  try {
447
- if (s.state === 'requesting' || s.state === 'deciding') {
472
+ if ((s.state === 'requesting' || s.state === 'deciding') && loadedEngine()) {
448
473
  sessionMorph(s, s.snapshot, { rewind: true });
449
474
  }
450
475
  } finally {
@@ -507,15 +532,16 @@ function marked(el, name, value) {
507
532
  }
508
533
 
509
534
  function buildChrome() {
510
- const style = document.createElement('style');
511
- style.setAttribute('clay', RUNTIME_ONLY);
512
- style.textContent = `
535
+ const { signal } = listeners;
536
+ styleEl = document.createElement('style');
537
+ styleEl.setAttribute('clay', RUNTIME_ONLY);
538
+ styleEl.textContent = `
513
539
  [editmode\\:contenteditable][contenteditable]:focus {
514
540
  outline: 1px solid #4a4a6a; outline-offset: 4px; border-radius: 2px;
515
541
  }
516
542
  ::highlight(clay-ai-edit) { background-color: ${TOKENS['brass-soft']} !important; }
517
543
  `;
518
- document.head.appendChild(style);
544
+ document.head.appendChild(styleEl);
519
545
 
520
546
  const scheme = `color-scheme:${pageScheme()}`;
521
547
 
@@ -641,24 +667,28 @@ function buildChrome() {
641
667
  openPanel(document.body, quoteFromSelection(document.body));
642
668
  });
643
669
 
644
- window.addEventListener('scroll', () => { positionChrome(); hideChip(); }, { passive: true });
645
- window.addEventListener('resize', () => { placeBubble(); if (panel && !panel.hidden) fitTextarea(); positionChrome(); }, { passive: true });
670
+ window.addEventListener('scroll', () => { positionChrome(); hideChip(); }, { passive: true, signal });
671
+ window.addEventListener('resize', () => { placeBubble(); if (panel && !panel.hidden) fitTextarea(); positionChrome(); }, { passive: true, signal });
646
672
  // The CMS toggle can arrive after this chrome, and a docked sidebar slides it left.
647
673
  const replace = () => { placeBubble(); positionChrome(); };
648
- document.addEventListener('hcms:open', () => { replace(); setTimeout(replace, 350); });
649
- document.addEventListener('hcms:close', () => { replace(); setTimeout(replace, 350); });
650
- new MutationObserver((records) => {
674
+ document.addEventListener('hcms:open', () => { replace(); setTimeout(replace, 350); }, { signal });
675
+ document.addEventListener('hcms:close', () => { replace(); setTimeout(replace, 350); }, { signal });
676
+ const cmsToggle = new MutationObserver((records) => {
651
677
  syncBubble();
652
678
  const touched = (nodes) => [...nodes].some((n) => n.nodeType === 1 && n.hasAttribute('data-hcms-toggle-host'));
653
679
  if (records.some((r) => touched(r.addedNodes) || touched(r.removedNodes))) replace();
654
- }).observe(document.body, { childList: true });
680
+ });
681
+ cmsToggle.observe(document.body, { childList: true });
682
+ observers.push(cmsToggle);
655
683
 
656
684
  // A target removed from the page ends its edit, so the save hold and the paused
657
685
  // observers never outlive it. A running bar is no exception: it is torn down with
658
686
  // the target it was describing.
659
- new MutationObserver(() => {
687
+ const targetGone = new MutationObserver(() => {
660
688
  if (anchorEl && !anchorEl.isConnected) abandonTarget();
661
- }).observe(document.body, { childList: true, subtree: true });
689
+ });
690
+ targetGone.observe(document.body, { childList: true, subtree: true });
691
+ observers.push(targetGone);
662
692
  }
663
693
 
664
694
  // The panel's height changes as the box grows and the status line fills, so it is
@@ -709,6 +739,7 @@ function applyHelperState() {
709
739
  // The host's toggle can change while the page is open, so each opening asks again.
710
740
  function refreshHelperState() {
711
741
  helperStateNow().then((state) => {
742
+ if (!panel) return; // the session this asked for has ended
712
743
  helperState = state || 'unavailable';
713
744
  if (!panel.hidden && !session) applyHelperState();
714
745
  else syncBubble();
@@ -939,8 +970,7 @@ function openPanel(el, quote) {
939
970
  // panel is opened on that target again.
940
971
  function storeDraft() {
941
972
  if (!anchorEl) return;
942
- if (textarea.value) drafts.set(anchorEl, textarea.value);
943
- else drafts.delete(anchorEl);
973
+ keepDraft(anchorEl, textarea.value);
944
974
  }
945
975
 
946
976
  function closePanel() {
@@ -1023,6 +1053,7 @@ function escapeIsMine(event) {
1023
1053
  }
1024
1054
 
1025
1055
  function wireInteractions() {
1056
+ const { signal } = listeners;
1026
1057
  // Clicks: text units are contenteditable (caret) and figures keep their own
1027
1058
  // interactivity, so neither is intercepted, and nothing here prevents the click from
1028
1059
  // landing. Any click on the page away from ClayJS chrome closes the box, a click
@@ -1033,7 +1064,7 @@ function wireInteractions() {
1033
1064
  // box and ends on the page is still the box's, not a click away from it.
1034
1065
  document.addEventListener('pointerdown', (event) => {
1035
1066
  panelPointerDown = panel.contains(event.target);
1036
- }, { capture: true });
1067
+ }, { capture: true, signal });
1037
1068
 
1038
1069
  document.addEventListener('click', (event) => {
1039
1070
  const startedInPanel = panelPointerDown;
@@ -1050,7 +1081,7 @@ function wireInteractions() {
1050
1081
  } else if (!panel.hidden) {
1051
1082
  closePanel();
1052
1083
  }
1053
- });
1084
+ }, { signal });
1054
1085
 
1055
1086
  // Hover chip: follows the unit under the cursor while no panel is open.
1056
1087
  let lastMove = 0;
@@ -1064,7 +1095,7 @@ function wireInteractions() {
1064
1095
  const unit = unitFrom(event.target);
1065
1096
  if (unit) showChipFor(unit);
1066
1097
  else scheduleChipHide();
1067
- });
1098
+ }, { signal });
1068
1099
 
1069
1100
  document.addEventListener('keydown', (event) => {
1070
1101
  if (isShortcut(event)) {
@@ -1094,26 +1125,35 @@ function wireInteractions() {
1094
1125
  if (barMode === 'error') { closeBar(); return; }
1095
1126
  if (barMode) return;
1096
1127
  if (!panel.hidden) closePanel();
1097
- });
1128
+ }, { signal });
1098
1129
 
1099
1130
  document.addEventListener('selectionchange', () => {
1100
1131
  clearTimeout(selectionTimer);
1101
1132
  selectionTimer = setTimeout(showChipForSelection, 150);
1102
- });
1133
+ }, { signal });
1103
1134
  }
1104
1135
 
1105
1136
  // ---------------------------------------------------------------- boot
1106
1137
 
1107
- async function init() {
1108
- if (!isEditMode || panel) return; // an owner/editor feature, built once
1138
+ async function loadHelper() {
1139
+ const token = ++chromeToken;
1109
1140
  helperState = await helperStateNow();
1110
- // No ai-edit helper on this host: stay dormant. A concurrent open() that got here
1111
- // first may also have built the chrome already.
1141
+ return token === chromeToken;
1142
+ }
1143
+
1144
+ function build() {
1112
1145
  if (!helperState || panel) return;
1146
+ listeners = new AbortController();
1113
1147
  buildChrome();
1114
1148
  wireInteractions();
1115
1149
  }
1116
1150
 
1151
+ async function init() {
1152
+ if (!startsAtImport() || panel) return; // an owner/editor feature, built once
1153
+ if (!(await loadHelper())) return;
1154
+ build();
1155
+ }
1156
+
1117
1157
  // A target the composer can be opened on: a connected content element of this
1118
1158
  // document. ClayJS chrome, transient UI, everything the save strips and everything
1119
1159
  // that holds no text are not targets.
@@ -1141,11 +1181,106 @@ async function open(el, { prompt } = {}) {
1141
1181
  textarea.focus();
1142
1182
  return true;
1143
1183
  }
1144
- if (typeof prompt === 'string' && !drafts.has(el)) drafts.set(el, prompt);
1184
+ if (typeof prompt === 'string' && !drafts.has(el)) keepDraft(el, prompt);
1145
1185
  openPanel(el, null);
1146
1186
  return true;
1147
1187
  }
1148
1188
 
1149
- onDomReady(() => { init().catch(() => {}); });
1189
+ /**
1190
+ * A committed exit: the chrome, every handler this session added and every page
1191
+ * observer go. What the person typed is a draft, and an exit that could not represent it
1192
+ * never gets here, so nothing unsent is thrown away by this.
1193
+ */
1194
+ function teardown() {
1195
+ chromeToken++;
1196
+ const s = session;
1197
+ session = null;
1198
+ try {
1199
+ s?.handle?.cancel();
1200
+ if (s) {
1201
+ if (s.paused) resumeObservers();
1202
+ if (s.held) releaseAllSaves();
1203
+ }
1204
+ } catch {}
1205
+ clearTimeout(selectionTimer);
1206
+ selectionTimer = null;
1207
+ clearTimeout(chipHideTimer);
1208
+ chipHideTimer = null;
1209
+ clearTimeout(barCloseTimer);
1210
+ barCloseTimer = null;
1211
+ clearHighlight();
1212
+ listeners?.abort();
1213
+ listeners = null;
1214
+ for (const observer of observers) observer.disconnect();
1215
+ observers = [];
1216
+ for (const node of [styleEl, ring, panel, bar, chip, docBubble]) node?.remove();
1217
+ styleEl = ring = panel = textarea = statusEl = pointer = bar = barStatus = null;
1218
+ barStop = barKeep = barRevert = barClose = chip = docBubble = null;
1219
+ buttons = {};
1220
+ barMode = null;
1221
+ barLifecycle = null;
1222
+ anchorEl = null;
1223
+ anchorRange = null;
1224
+ returnFocus = null;
1225
+ pendingSelection = undefined;
1226
+ chipTarget = null;
1227
+ chipSelection = undefined;
1228
+ panelPointerDown = false;
1229
+ helperState = null;
1230
+ drafts = new WeakMap();
1231
+ drafted.clear();
1232
+ }
1233
+
1234
+ /**
1235
+ * ai-edit as a mode lease.
1236
+ *
1237
+ * Preparing this lease asks the host whether it lists a ready helper, which is safe while
1238
+ * the committed mode is still view; the commit that enters edit builds the chrome, inert
1239
+ * until somebody opens it, exactly as `clay.aiEdit.init()` does. What a session adds is
1240
+ * everything still unresolved — a request on the wire, a preview waiting for Keep or
1241
+ * Revert, a comment typed and not sent — so `quiesce` refuses the exit and `resume`
1242
+ * leaves that same preview, comment and selection timer exactly as they were.
1243
+ * `dispose` (after the barrier) stops every editor-only handler and takes the chrome
1244
+ * down, so a save or selection event in view draws nothing. `initial: true` adopts the
1245
+ * chrome the boot's own import built, and asks the host nothing a second time.
1246
+ */
1247
+ export function aiEditParticipant() {
1248
+ return {
1249
+ id: "clay:ai-edit",
1250
+ order: 950, // after the wire (900): this request is sent through it
1251
+ prepare(context = {}) {
1252
+ const initial = context.initial === true;
1253
+ return {
1254
+ ready: initial ? boot : loadHelper(),
1255
+ enable() {
1256
+ if (!initial) build();
1257
+ },
1258
+ quiesce() {
1259
+ const unsent = (panel && !panel.hidden && textarea.value.trim() !== '') ||
1260
+ [...drafted].some((el) => el.isConnected);
1261
+ const pending = [];
1262
+ if (session) pending.push(session.id);
1263
+ if (barMode === 'saving') pending.push('save');
1264
+ if (unsent) pending.push('prompt');
1265
+ if (!pending.length) return;
1266
+ throw Object.assign(new Error('clay.aiEdit: an edit here is still unresolved'), {
1267
+ code: 'pending-input',
1268
+ pending,
1269
+ });
1270
+ },
1271
+ resume() {},
1272
+ dispose() {
1273
+ teardown();
1274
+ },
1275
+ };
1276
+ },
1277
+ };
1278
+ }
1279
+
1280
+ // The boot's own answer, which an explicit edit boot's exit adopts rather than re-asking.
1281
+ let boot = null;
1282
+ onDomReady(() => { boot = init().then(() => {}, () => {}); });
1283
+
1284
+ export { aiEditParticipant as modeParticipant };
1150
1285
 
1151
1286
  export const aiEdit = { init, open };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * clay.bindText — an editor textarea that lives outside the saved records.
3
+ *
4
+ * The textarea sits in a `clay="freeze"` region and edits one record's text
5
+ * element (for example `<pre><code class="entry-body">`). Every input is
6
+ * written into the record at once, so a save captures it as the record's text
7
+ * and a peer's merge of another paragraph keeps both edits. A change to the
8
+ * record from elsewhere (a peer's merge, a restored draft, a JSON API write) is
9
+ * shown in the textarea with the caret kept. During an IME composition nothing
10
+ * is written or shown, and when it ends both edits apply unless they touch the
11
+ * same stretch of text. Nothing here is Journal-specific: OverType, the
12
+ * timestamps and the note arrive as callbacks.
13
+ *
14
+ * The binding answers for its own lifecycle too: `flush()` reports whether the
15
+ * record now holds the text and refuses while a composition is running, `open()`
16
+ * and `close()` honor that refusal, and `dispose()` only detaches once the exit
17
+ * barrier has landed.
18
+ */
19
+
20
+ // Applies two edits to one text when each is one stretch of it (an IME composition always is) and the
21
+ // stretches do not overlap (two inserts at one point keep both); null when they do.
22
+ export function spliceMerge(base, local, remote) {
23
+ const span = (text) => {
24
+ let pre = 0;
25
+ const max = Math.min(base.length, text.length);
26
+ while (pre < max && base[pre] === text[pre]) pre++;
27
+ let suf = 0;
28
+ while (suf < max - pre && base[base.length - 1 - suf] === text[text.length - 1 - suf]) suf++;
29
+ return { start: pre, end: base.length - suf, text: text.slice(pre, text.length - suf) };
30
+ };
31
+ const l = span(local);
32
+ const r = span(remote);
33
+ if (l.end <= r.start) return base.slice(0, l.start) + l.text + base.slice(l.end, r.start) + r.text + base.slice(r.end);
34
+ if (r.end <= l.start) return base.slice(0, r.start) + r.text + base.slice(r.end, l.start) + l.text + base.slice(l.end);
35
+ return null;
36
+ }
37
+
38
+ // Where an offset in `before` lands in `after`, when the two differ in one stretch.
39
+ export function mapCaret(before, after, offset) {
40
+ let pre = 0;
41
+ const max = Math.min(before.length, after.length);
42
+ while (pre < max && before[pre] === after[pre]) pre++;
43
+ if (offset <= pre) return offset;
44
+ let suf = 0;
45
+ while (suf < max - pre && before[before.length - 1 - suf] === after[after.length - 1 - suf]) suf++;
46
+ return offset >= before.length - suf ? after.length - (before.length - offset) : after.length - suf;
47
+ }
48
+
49
+ export function bindText(textarea, { onWrite, onShow, onConflict, onGone } = {}) {
50
+ const OBSERVE = { characterData: true, childList: true, subtree: true };
51
+ let field = textarea;
52
+ const ta = () => field;
53
+ let s = null; // { key, code, seen, composing, resolve }
54
+ let disposed = false;
55
+ const observer = new MutationObserver(() => {
56
+ observer.takeRecords();
57
+ if (s && !s.composing && s.code.textContent !== s.seen) reconcile(s.code.textContent);
58
+ });
59
+
60
+ // New durable input is the lifecycle's to admit, not the binding's: while a transition
61
+ // runs, a keystroke is not new input and is refused. A standalone page has no lifecycle
62
+ // and always admits.
63
+ const admitted = () => window.clay?.modeLifecycle?.canWrite?.() !== false;
64
+
65
+ // The record holds what the textarea holds. That is the only proof a flush landed: a
66
+ // callback that staged a conflict or a gone record has not made the text represented.
67
+ const represented = () => !!(s && s.code.isConnected && s.code.textContent === ta().value);
68
+
69
+ function write(value) {
70
+ const t = s.code.firstChild;
71
+ if (t && t.nodeType === 3 && !t.nextSibling) t.data = value;
72
+ else s.code.textContent = value;
73
+ observer.takeRecords();
74
+ s.seen = value;
75
+ onWrite?.(s.code);
76
+ }
77
+ function show(value) {
78
+ const el = ta();
79
+ const before = el.value;
80
+ const [a, b] = [mapCaret(before, value, el.selectionStart), mapCaret(before, value, el.selectionEnd)];
81
+ el.value = value;
82
+ if (document.activeElement === el) el.setSelectionRange(a, b);
83
+ s.seen = value;
84
+ onShow?.();
85
+ }
86
+ // The record changed while the textarea held text the record had not taken yet.
87
+ // `base` is the text both sides were editing, read before show() moves `seen`: a
88
+ // caller staging the losing side needs what the clash was measured against, not
89
+ // what the textarea holds afterwards. The second argument is additive, so a
90
+ // caller that only wants the typed string ignores it.
91
+ function reconcile(now) {
92
+ const typed = ta().value;
93
+ const base = s.seen;
94
+ const merged = typed === base ? now : spliceMerge(base, typed, now);
95
+ if (merged == null) {
96
+ onConflict?.(typed, { key: s.key, base, remote: now });
97
+ show(now);
98
+ return { ok: false, code: "conflict" };
99
+ }
100
+ if (merged !== now) write(merged);
101
+ show(merged);
102
+ return { ok: true };
103
+ }
104
+ // A record that cannot be resolved hands its text to `onGone` and is released: there is
105
+ // nothing left here that a later write could land in, and holding the detached element
106
+ // would refuse every record switch after it.
107
+ function rebind() {
108
+ const code = s.resolve ? s.resolve() : null;
109
+ if (!code) {
110
+ onGone?.(ta().value, { key: s.key, base: s.seen });
111
+ observer.disconnect();
112
+ s = null;
113
+ return false;
114
+ }
115
+ s.code = code;
116
+ observer.disconnect();
117
+ observer.observe(s.code, OBSERVE);
118
+ return true;
119
+ }
120
+ // `captured` is the composition this binding already holds: it finishes through its own
121
+ // operation, so a closed admission does not throw away what the person was typing.
122
+ function input(captured = false) {
123
+ if (!s) return { ok: true };
124
+ if (s.composing) return { ok: false, code: "composition" };
125
+ if (!captured && !admitted()) return { ok: false, code: "busy" };
126
+ if (!s.code.isConnected && !rebind()) return { ok: false, code: "gone" };
127
+ const now = s.code.textContent;
128
+ if (now !== s.seen) return reconcile(now);
129
+ const value = ta().value;
130
+ if (value !== s.seen) write(value);
131
+ return { ok: true };
132
+ }
133
+ // A composition in progress is not something a flush may end on the person's behalf: it
134
+ // is refused, and `s.composing` stays exactly as it was. Everything else runs the input
135
+ // path above, and its answer says whether the record now holds the text.
136
+ function flush() {
137
+ if (!s) return { ok: true };
138
+ if (s.composing) return { ok: false, code: "composition" };
139
+ const result = input();
140
+ if (!result.ok) return result;
141
+ return represented() ? { ok: true } : { ok: false, code: "conflict" };
142
+ }
143
+ function open(textEl, options = {}) {
144
+ if (disposed) return { ok: false, code: "disposed" };
145
+ const key = options.key;
146
+ if (s && s.key === key) {
147
+ s.resolve = options.resolve;
148
+ if (s.code !== textEl || !s.code.isConnected) {
149
+ if (!rebind()) return { ok: false, code: "gone" };
150
+ return input();
151
+ }
152
+ return { ok: true };
153
+ }
154
+ // A refused flush keeps the open record and its observer: nothing was written, so
155
+ // switching records here would drop the text the refusal was protecting. A record that
156
+ // was gone has already handed its text over and released itself, so there is nothing
157
+ // left to protect and the switch goes ahead.
158
+ const flushed = flush();
159
+ if (!flushed.ok && s) return flushed;
160
+ observer.disconnect();
161
+ s = { key, code: textEl, seen: textEl.textContent, composing: false, resolve: options.resolve };
162
+ observer.observe(textEl, OBSERVE);
163
+ const el = ta();
164
+ el.value = s.seen;
165
+ el.setSelectionRange(0, 0);
166
+ onShow?.();
167
+ return { ok: true };
168
+ }
169
+ function close() {
170
+ const flushed = flush();
171
+ if (!flushed.ok && s) return flushed;
172
+ observer.disconnect();
173
+ s = null;
174
+ return { ok: true };
175
+ }
176
+ // Detaching only. The barrier has already succeeded when a lease disposes the binding, so
177
+ // this never flushes: a composition still in progress or text the record never took keeps
178
+ // the listeners where they are, and the caller is told why.
179
+ function dispose() {
180
+ if (disposed) return { ok: true };
181
+ if (s?.composing) return { ok: false, code: "composition" };
182
+ if (s && !represented()) return { ok: false, code: "pending-input" };
183
+ disposed = true;
184
+ observer.disconnect();
185
+ detach();
186
+ s = null;
187
+ return { ok: true };
188
+ }
189
+ function attach(next) {
190
+ if (disposed || !next || next === field) return;
191
+ detach();
192
+ field = next;
193
+ listen();
194
+ }
195
+
196
+ function onInput() { input(); }
197
+ function onCompositionStart() { if (s) s.composing = true; }
198
+ function onCompositionEnd() { if (s) { s.composing = false; input(true); } }
199
+ function listen() {
200
+ field.addEventListener("input", onInput);
201
+ field.addEventListener("compositionstart", onCompositionStart);
202
+ field.addEventListener("compositionend", onCompositionEnd);
203
+ }
204
+ function detach() {
205
+ field.removeEventListener("input", onInput);
206
+ field.removeEventListener("compositionstart", onCompositionStart);
207
+ field.removeEventListener("compositionend", onCompositionEnd);
208
+ }
209
+
210
+ listen();
211
+
212
+ return {
213
+ open, close, flush, dispose,
214
+ current: () => s && s.code,
215
+ attach,
216
+ composing: () => !!(s && s.composing),
217
+ };
218
+ }
@@ -4,9 +4,102 @@
4
4
  import { installSkin } from "../ui/vendor-skin.js";
5
5
  import { CSS } from "../ui/skins/cms.js";
6
6
  import { CSS as TOGGLE_CSS, ROOTS as TOGGLE_ROOTS } from "../ui/skins/cms-toggle.js";
7
+ import { cms as hypercms } from "../vendor/hypercms.vendor.js";
7
8
 
8
9
  export { cms, default } from "../vendor/hypercms.vendor.js";
9
10
 
10
11
  installSkin("cms", CSS);
11
12
  // The toggle takes the page's scheme, so Bevel's face and ink resolve the way the page reads.
12
13
  installSkin("cms-toggle", TOGGLE_CSS, { roots: TOGGLE_ROOTS });
14
+
15
+ const fault = (message, code) => Object.assign(new Error(`clay:cms: ${message}`), { code });
16
+
17
+ const isStale = (context) => {
18
+ if (!context) return false;
19
+ if (context.signal?.aborted) return true;
20
+ return typeof context.isCurrent === "function" && !context.isCurrent();
21
+ };
22
+
23
+ // One edit session over the page the CMS edits, as a ClayJS lease. The source owns the
24
+ // session itself, so every method here is the source's own lifecycle call: `enable`
25
+ // opens and mounts, `resume` is the refused exit over the same session, and `dispose`
26
+ // goes through the CMS's teardown after the barrier.
27
+ //
28
+ // `quiesce` is the one call the lease shapes. The source answers for the work it still
29
+ // holds, an upload in flight or a painted preview, with `{ok:false}` rather than by
30
+ // tearing that work down: an exit refused there keeps the session, its buffers and the
31
+ // upload, and the controller resumes the same lease once the person finishes or
32
+ // discards it. The lease contract carries a refusal as the `code` a controller reports,
33
+ // so the non-ok answer is thrown as `pending-input`.
34
+ function cmsLease(source) {
35
+ let disposed = false;
36
+ return {
37
+ ready: source.ready,
38
+ enable() {
39
+ if (disposed) return;
40
+ source.enable();
41
+ },
42
+ quiesce() {
43
+ if (disposed) return;
44
+ const result = source.quiesce();
45
+ if (result && result.ok === false) {
46
+ throw Object.assign(fault("the CMS still has work to finish", result.code || "pending-input"), {
47
+ pending: result.pending,
48
+ });
49
+ }
50
+ return result;
51
+ },
52
+ resume() {
53
+ if (disposed) return;
54
+ source.resume();
55
+ },
56
+ dispose() {
57
+ if (disposed) return;
58
+ disposed = true;
59
+ source.dispose();
60
+ },
61
+ };
62
+ }
63
+
64
+ // The boot this tab made, adopted instead of prepared. A page that opened the CMS itself
65
+ // (`?cms=true`) holds a session the source hands no lease out for, so it cannot be
66
+ // quiesced or torn down through the adapter and the caller keeps its reload instead;
67
+ // a boot that opened nothing has nothing to hand back, and this lease starts nothing.
68
+ function adopt() {
69
+ if (hypercms.isOpen) throw fault("the CMS this boot opened cannot be taken back", "unsupported-plugin");
70
+ const lease = cmsLease(hypercms.prepareModeLease({}));
71
+ return {
72
+ enable() {},
73
+ quiesce: () => lease.quiesce(),
74
+ resume: () => lease.resume(),
75
+ dispose: () => lease.dispose(),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The CMS as a mode participant: the session the commit that enters edit opens, and the
81
+ * one the exit destroys.
82
+ *
83
+ * `prepare` is inert by construction, because the source's lease mounts nothing until
84
+ * `enable`; a vendor build without that lifecycle is refused here rather than opened
85
+ * beside the controller, where an exit would have no session to quiesce and no teardown
86
+ * to run. `order` is the source's: the CMS edits the page's own attributes and content,
87
+ * so it enters after the modules that prepare that page for editing, and it leaves
88
+ * first, while its session can still refuse an exit over an upload or a preview.
89
+ */
90
+ export function cmsParticipant(options = {}) {
91
+ return {
92
+ id: "clay:cms",
93
+ order: 1000,
94
+ async prepare(context = {}) {
95
+ if (typeof hypercms.prepareModeLease !== "function") {
96
+ throw fault("this vendor build has no quiescable session", "unsupported-plugin");
97
+ }
98
+ if (isStale(context)) throw fault("prepared for a stale generation", "stale-document");
99
+ if (context.initial === true) return adopt();
100
+ return cmsLease(hypercms.prepareModeLease(options));
101
+ },
102
+ };
103
+ }
104
+
105
+ export { cmsParticipant as modeParticipant };