@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
package/src/page/view.js CHANGED
@@ -1,136 +1,419 @@
1
- // View mode: readers see the saved article, and each newer file the host sends
2
- // on the saved lane (every checkpoint is a save) is morphed into it. Live sync
3
- // never touches a frozen element's contents, so this is the only writer here.
4
- // No ProseMirror, no projection code: the writer's save already rendered it.
5
- // A toggle the reader opened or closed keeps the reader's state across updates.
6
- import { morph } from "../vendor/hyper-morph.vendor.js";
7
- import { createToolHost } from "./tools.js";
1
+ // View mode: readers see the saved article, and each newer file the host sends on
2
+ // the saved lane is projected onto it by key. One owner at a time: a reader claims
3
+ // the article before it attaches listeners and gives it back only on stop, so an
4
+ // editor never mounts underneath a live reader and a late frame never lands over a
5
+ // mounted tool. No ProseMirror, no projection code and no merge engine: the saved
6
+ // file is already the rendered form of the writer's model.
7
+ import { createSavedStream } from "../sync/saved-stream.js";
8
+ import { stopForUpgrade, upgradeReplaces, upgradeReplacesDocument } from "../sync/version-guard.js";
9
+ import { applyPreparedArticle, keepToolVersion, prepareSavedArticle, toolInfoOf, toolsOf, toolStateOf } from "./reader-patch.js";
10
+ import { claim } from "./runtime-owner.js";
8
11
 
9
12
  const ARTICLE = "article[data-page]";
10
13
  const TOOL = 'section[data-type="tool"][data-id]';
14
+ const PAGE_TAG = 'script[type="application/json"][merge="page"]';
15
+ const LINEAGE = ["live-at", "live-by"];
11
16
 
17
+ export { applyPreparedArticle, prepareSavedArticle };
18
+
19
+ // The stamp of the file this reader accepted, recorded where a save reads it. The
20
+ // stamp module is the write path's own lazy chunk: a reader paints without it, and
21
+ // a save loads the one copy both paths then share.
22
+ const rememberStamp = (etag) => {
23
+ import("../core/etag.js").then(({ recordEtag }) => recordEtag(etag)).catch(() => {});
24
+ };
25
+
26
+ const freezeReceipt = (value) => Object.freeze({
27
+ bytes: typeof value?.bytes === "string" ? value.bytes : null,
28
+ etag: typeof value?.etag === "string" && value.etag ? value.etag : null,
29
+ seq: Number.isSafeInteger(value?.seq) ? value.seq : null,
30
+ });
31
+
32
+ // The tag a live entry opens its page store from is the one on screen, so a reader
33
+ // keeps it at the file it represents: a saved file that states a page tag replaces the
34
+ // live tag, and one that states none removes it.
35
+ const applySavedTag = (prepared, doc) => {
36
+ const saved = prepared.source.querySelector(PAGE_TAG);
37
+ const live = doc.querySelector(PAGE_TAG);
38
+ if (!saved) {
39
+ if (!live) return false;
40
+ live.remove();
41
+ return true;
42
+ }
43
+ if (live) {
44
+ // The stamp and the provenance the file carries travel with the text the file states:
45
+ // a tag whose lineage was left behind is a tag the next live entry reads as one it
46
+ // cannot place, and the room the save was made in goes with it.
47
+ const lineage = LINEAGE.map((name) => saved.getAttribute(name));
48
+ const same = live.textContent === saved.textContent &&
49
+ LINEAGE.every((name, i) => live.getAttribute(name) === lineage[i]);
50
+ if (same) return false;
51
+ if (live.textContent !== saved.textContent) live.textContent = saved.textContent;
52
+ LINEAGE.forEach((name, i) => {
53
+ if (lineage[i] === null) live.removeAttribute(name);
54
+ else live.setAttribute(name, lineage[i]);
55
+ });
56
+ return true;
57
+ }
58
+ const article = doc.querySelector(ARTICLE);
59
+ if (!article?.parentNode) return false;
60
+ article.parentNode.insertBefore(doc.importNode(saved, true), article);
61
+ return true;
62
+ };
63
+
64
+ // The compatibility entry: apply one saved file to the article already on screen.
65
+ // Newer callers start a reader, which owns the stream and reports its receipt.
12
66
  export function applySavedArticle(html, doc = document, readerOpen = new Map(), savedOpen = new Map(), preserveTools = new Set()) {
13
67
  const live = doc.querySelector(ARTICLE);
14
- if (!live || typeof html !== "string") return false;
15
- const incoming = new doc.defaultView.DOMParser().parseFromString(html, "text/html").querySelector(ARTICLE);
16
- if (!incoming) return false;
17
- for (const details of incoming.querySelectorAll("details[data-id]")) {
18
- const id = details.getAttribute("data-id");
19
- savedOpen.set(id, details.hasAttribute("open"));
20
- if (readerOpen.has(id)) details.toggleAttribute("open", readerOpen.get(id));
68
+ if (!live) return false;
69
+ const prepared = prepareSavedArticle(html, doc);
70
+ if (!prepared.ok) return false;
71
+ const keepTool = (root, incoming) => preserveTools.has(root.getAttribute("data-id")) || keepToolVersion(root, incoming);
72
+ const result = applyPreparedArticle(prepared, { article: live, readerOpen, savedOpen, keepTool });
73
+ let changed = result.changed;
74
+ if (prepared.rev !== live.getAttribute("data-rev")) {
75
+ if (prepared.rev === null) live.removeAttribute("data-rev");
76
+ else live.setAttribute("data-rev", prepared.rev);
77
+ changed = true;
21
78
  }
22
- const rev = incoming.getAttribute("data-rev");
23
- if (rev && rev === live.getAttribute("data-rev") && incoming.innerHTML === live.innerHTML) return false;
24
- const kept = new Map();
25
- for (const root of live.querySelectorAll(TOOL)) {
26
- const id = root.getAttribute("data-id");
27
- if (!preserveTools.has(id)) continue;
28
- kept.set(id, root);
29
- root.replaceWith(root.cloneNode(false));
79
+ if (prepared.title && prepared.title !== doc.title) {
80
+ doc.title = prepared.title;
81
+ changed = true;
30
82
  }
31
- // raw: the sync policy ignores everything under [clay~=freeze], which is all of it.
32
- // Scripts never run: a tool's fallback is markup, and tools mount from the store.
33
- morph(live, incoming.innerHTML, {
34
- policy: "raw", morphStyle: "innerHTML", scripts: { handle: false },
35
- key: (el) => el.getAttribute("data-id") || null,
36
- });
37
- for (const root of live.querySelectorAll(TOOL)) {
38
- const original = kept.get(root.getAttribute("data-id"));
39
- if (original) root.replaceWith(original);
40
- }
41
- if (rev) live.setAttribute("data-rev", rev);
42
- return true;
83
+ if (applySavedTag(prepared, doc)) changed = true;
84
+ return changed;
43
85
  }
44
86
 
45
- // A toggle counts as the reader's own when its state differs from what the
46
- // last saved article said: clicks, find-in-page and keyboard all fire `toggle`,
47
- // and a toggle the morph caused matches the saved state.
48
- export function startPageView(doc = document) {
87
+ function createReader({ doc, article, represented = null, feed = null, documentURL, servedEtag = null, stream: adopted = null, release: held = () => {} }) {
49
88
  const readerOpen = new Map();
50
89
  const savedOpen = new Map();
51
90
  const mounted = new Map();
52
91
  const toolStore = { data: {} };
53
- const host = createToolHost({ store: toolStore, mode: "view" });
92
+ const listeners = [];
93
+ let host = null;
94
+ let hostLoading = null;
95
+ let mountTurn = 0;
54
96
  let stopped = false;
55
- let sequence = 0;
56
- for (const details of doc.querySelectorAll(`${ARTICLE} details[data-id]`)) savedOpen.set(details.getAttribute("data-id"), details.hasAttribute("open"));
57
- const onToggle = (event) => {
58
- const details = event.target;
59
- if (details.tagName !== "DETAILS" || !details.closest(ARTICLE)) return;
60
- const id = details.getAttribute("data-id");
61
- if (!id) return;
62
- if (details.open === savedOpen.get(id)) readerOpen.delete(id);
63
- else readerOpen.set(id, details.open);
97
+ let stream = adopted;
98
+ let final = null;
99
+ let receipt = freezeReceipt(represented);
100
+ // A switch owns the saved lane from `prepareHandoff` until it commits or resumes.
101
+ let handoff = null;
102
+ let release = held;
103
+ // A switch that mounts an editor over this article takes the claim for the time it
104
+ // is up: the reader keeps the lane and the files still queued, and paints nothing.
105
+ let yielded = false;
106
+
107
+ const dispatch = (type, detail) => {
108
+ const win = doc.defaultView ?? globalThis;
109
+ doc.dispatchEvent(new win.CustomEvent(type, { detail }));
64
110
  };
65
- const toolsIn = async (source) => {
66
- if (!source.querySelector(`${ARTICLE} ${TOOL}`)) return null;
67
- const blocks = {};
68
- for (const root of source.querySelectorAll(`${ARTICLE} ${TOOL}`)) {
69
- const data = root.querySelector(':scope > script[type="application/json"][data-tool]');
70
- let tool = {};
71
- try { tool = data ? JSON.parse(data.textContent) : {}; } catch {}
72
- blocks[root.getAttribute("data-id")] = { t: "tool", artifact: tool.artifact ?? null, rev: tool.rev ?? null, state: tool.state ?? {} };
111
+
112
+ const disposeMounts = () => {
113
+ for (const item of mounted.values()) {
114
+ try { item.instance?.dispose?.(); } catch {}
73
115
  }
74
- const stateTag = source.querySelector('script[type="application/json"][merge="page.tools"]');
75
- const state = stateTag ? JSON.parse(stateTag.textContent) : {};
76
- return { blocks, state };
116
+ mounted.clear();
77
117
  };
78
- const syncTools = (decoded) => {
79
- toolStore.data = decoded?.state ?? {};
80
- const seen = new Set();
81
- for (const root of doc.querySelectorAll(`${ARTICLE} ${TOOL}`)) {
82
- const id = root.getAttribute("data-id");
83
- seen.add(id);
84
- const block = decoded?.blocks?.[id];
85
- const old = mounted.get(id);
86
- if (!block || block.t !== "tool" || !block.artifact || !block.rev) {
87
- old?.instance.dispose();
118
+
119
+ const hostReady = () => {
120
+ if (host) return Promise.resolve(host);
121
+ if (!hostLoading) {
122
+ hostLoading = import("./tools.js").then(({ createToolHost }) => {
123
+ if (stopped) return null;
124
+ host = createToolHost({ store: toolStore, mode: "view" });
125
+ return host;
126
+ }).catch((error) => {
127
+ hostLoading = null;
128
+ console.warn("clayjs: could not load the reader tool host", error);
129
+ return null;
130
+ });
131
+ }
132
+ return hostLoading;
133
+ };
134
+
135
+ const pruneTools = (tools) => {
136
+ for (const [id, item] of mounted) {
137
+ const next = tools.get(id);
138
+ if (!next || next.invalid || next.rev !== item.rev || next.signature !== item.signature) {
139
+ try { item.instance?.dispose?.(); } catch {}
88
140
  mounted.delete(id);
89
- continue;
90
141
  }
91
- if (old && old.root === root && old.rev === block.rev && JSON.stringify(old.artifact) === JSON.stringify(block.artifact)) continue;
92
- old?.instance.dispose();
93
- mounted.set(id, { root, rev: block.rev, artifact: block.artifact, instance: host.mount(root, {
94
- id, artifact: block.artifact, rev: block.rev, seed: block.state,
95
- }) });
96
142
  }
97
- for (const [id, old] of mounted) if (!seen.has(id)) { old.instance.dispose(); mounted.delete(id); }
98
143
  };
99
- const onFrame = (event) => {
100
- const html = event.detail?.html;
101
- if (typeof html !== "string") return;
102
- const source = new doc.defaultView.DOMParser().parseFromString(html, "text/html");
103
- const turn = ++sequence;
104
- const incomingIds = new Set([...source.querySelectorAll(`${ARTICLE} ${TOOL}`)].map((el) => el.getAttribute("data-id")));
105
- const preserve = new Set([...mounted.keys()].filter((id) => incomingIds.has(id)));
106
- for (const [id, old] of mounted) if (!incomingIds.has(id)) { old.instance.dispose(); mounted.delete(id); }
107
- applySavedArticle(html, doc, readerOpen, savedOpen, preserve);
108
- if (!incomingIds.size) {
109
- syncTools(null);
110
- return;
144
+
145
+ // Only a document with a tool pays for the tool host. A failed import leaves the
146
+ // saved fallback markup in place and the saved file still applies.
147
+ const mountTools = (tools, state) => {
148
+ const wanted = [];
149
+ for (const [id, block] of tools) {
150
+ if (block.invalid) console.warn("clayjs: could not decode saved tools", block.error);
151
+ else wanted.push([id, block]);
111
152
  }
112
- toolsIn(source).then((decoded) => {
113
- if (stopped || turn !== sequence) return;
114
- syncTools(decoded);
115
- }).catch((error) => {
116
- if (stopped || turn !== sequence) return;
117
- console.warn("clayjs: could not decode saved tools", error);
118
- applySavedArticle(html, doc, readerOpen, savedOpen);
119
- syncTools(null);
153
+ if (!wanted.length) return;
154
+ toolStore.data = state ?? {};
155
+ const turn = ++mountTurn;
156
+ hostReady().then((created) => {
157
+ if (stopped || !created || turn !== mountTurn) return;
158
+ for (const [id, block] of wanted) {
159
+ if (mounted.has(id)) continue;
160
+ const root = [...article.querySelectorAll(TOOL)].find((el) => el.getAttribute("data-id") === id);
161
+ if (!root) continue;
162
+ try {
163
+ const instance = created.mount(root, {
164
+ id, artifact: block.artifact, rev: block.rev, seed: block.state ?? {},
165
+ });
166
+ mounted.set(id, { rev: block.rev, signature: block.signature, instance });
167
+ } catch (error) {
168
+ console.warn(`clayjs: tool ${id} could not mount`, error);
169
+ }
170
+ }
120
171
  });
121
172
  };
173
+
174
+ // A mounted tool has no data-tool script left to read: the host owns that root
175
+ // now, so identity comes from the version the mount recorded.
176
+ const keepTool = (live, incoming) => {
177
+ const item = mounted.get(live.getAttribute("data-id"));
178
+ if (!item) return keepToolVersion(live, incoming);
179
+ const next = toolInfoOf(incoming);
180
+ return !next.invalid && next.rev === item.rev && next.signature === item.signature;
181
+ };
182
+
183
+ const onStreamStatus = (status) => {
184
+ // A transport that moved its receipt onto the bytes already on screen names the
185
+ // stamp this article now holds; the article is not repainted for it.
186
+ if (status?.state === "current") {
187
+ receipt = freezeReceipt({ bytes: receipt.bytes, etag: status.etag ?? null, seq: status.seq ?? null });
188
+ }
189
+ if (status?.state && status.state !== "applied") dispatch("clay:reader-status", status);
190
+ };
191
+
192
+ const paint = (prepared) => {
193
+ applyPreparedArticle(prepared, { article, readerOpen, savedOpen, keepTool });
194
+ if (prepared.rev === null) article.removeAttribute("data-rev");
195
+ else article.setAttribute("data-rev", prepared.rev);
196
+ if (prepared.title) doc.title = prepared.title;
197
+ applySavedTag(prepared, doc);
198
+ };
199
+
200
+ // A native apply can throw on markup the parser accepted and the DOM refuses.
201
+ // The receipt stays at the last accepted file, the article goes back to those
202
+ // bytes and the reader reports blocked instead of showing a half-applied tree.
203
+ const restore = (error) => {
204
+ console.warn("clayjs: the reader could not apply a saved file", error);
205
+ stream?.stop();
206
+ stream = null;
207
+ disposeMounts();
208
+ const previous = receipt.bytes ? prepareSavedArticle(receipt.bytes, doc) : null;
209
+ if (previous?.ok) {
210
+ try { paint(previous); } catch (again) { console.warn("clayjs: the reader could not restore the previous file", again); }
211
+ }
212
+ dispatch("clay:reader-status", { state: "blocked" });
213
+ };
214
+
215
+ function commit(candidate) {
216
+ if (stopped || yielded) return false;
217
+ const prepared = prepareSavedArticle(candidate.bytes, doc);
218
+ if (!prepared.ok) return false;
219
+ if (article.querySelector(".page-editor")) return false;
220
+ // A file published at another version of this document is not this reader's to
221
+ // show: the stream stops, this file and every later one are refused, and the
222
+ // notice offers a reload. Nothing of it reaches the article.
223
+ if (upgradeReplaces(candidate.bytes)) {
224
+ stream?.stop();
225
+ stream = null;
226
+ stopForUpgrade();
227
+ dispatch("clay:reader-status", { state: "blocked", reason: "upgraded" });
228
+ return false;
229
+ }
230
+ pruneTools(prepared.tools);
231
+ try {
232
+ paint(prepared);
233
+ } catch (error) {
234
+ restore(error);
235
+ return false;
236
+ }
237
+ receipt = freezeReceipt(candidate);
238
+ // The article now holds the version this file was, so a save this page makes
239
+ // afterwards has to carry that version's stamp instead of an older one.
240
+ if (receipt.etag) rememberStamp(receipt.etag);
241
+ mountTools(prepared.tools, prepared.toolState);
242
+ dispatch("clay:reader-status", { state: "applied", etag: receipt.etag, seq: receipt.seq });
243
+ return true;
244
+ }
245
+
246
+ // The successor's base: without a receipt there is nothing to hand over, so the
247
+ // reader keeps the article and its claim.
248
+ const retire = () => {
249
+ if (receipt.bytes === null) return false;
250
+ reader.stop();
251
+ return true;
252
+ };
253
+
254
+ const onToggle = (event) => {
255
+ const details = event.target;
256
+ if (details.tagName !== "DETAILS" || !article.contains(details)) return;
257
+ const id = details.getAttribute("data-id");
258
+ if (!id) return;
259
+ if (details.open === savedOpen.get(id)) readerOpen.delete(id);
260
+ else readerOpen.set(id, details.open);
261
+ };
122
262
  doc.addEventListener("toggle", onToggle, true);
123
- doc.addEventListener("clay:sync-applied", onFrame);
124
- if (doc.querySelector(`${ARTICLE} ${TOOL}`)) {
125
- const turn = ++sequence;
126
- toolsIn(doc).then((decoded) => { if (!stopped && turn === sequence) syncTools(decoded); })
127
- .catch((error) => { if (!stopped) console.warn("clayjs: could not decode saved tools", error); });
263
+ listeners.push(["toggle", onToggle, true]);
264
+
265
+ if (represented) {
266
+ const prepared = prepareSavedArticle(represented.bytes, doc);
267
+ if (!prepared.ok) throw new Error(`clayjs: the saved file is not a readable page (${prepared.reason})`);
268
+ paint(prepared);
269
+ mountTools(prepared.tools, prepared.toolState);
270
+ } else {
271
+ for (const details of article.querySelectorAll("details[data-id]")) savedOpen.set(details.getAttribute("data-id"), details.hasAttribute("open"));
272
+ mountTools(toolsOf(article), toolStateOf(doc));
128
273
  }
129
- return () => {
130
- stopped = true;
131
- sequence++;
132
- host.dispose();
133
- doc.removeEventListener("toggle", onToggle, true);
134
- doc.removeEventListener("clay:sync-applied", onFrame);
274
+
275
+ const reader = {
276
+ ready: null,
277
+ get represented() {
278
+ return receipt;
279
+ },
280
+ // Give the article up to a switch that mounts over it: the mounted tools go, the
281
+ // claim goes, and nothing is painted until `reclaimArticle` takes it back. The
282
+ // stream and the files it queued stay held, so no save falls between the two.
283
+ yieldArticle() {
284
+ if (stopped || yielded) return false;
285
+ mountTurn++;
286
+ disposeMounts();
287
+ yielded = true;
288
+ release();
289
+ return true;
290
+ },
291
+ // The article back, exactly as the receipt this reader holds says: a switch that
292
+ // fails gives it up unchanged, and a refused entry costs no frame.
293
+ reclaimArticle() {
294
+ if (stopped || !yielded) return false;
295
+ release = claim(article, "reader");
296
+ yielded = false;
297
+ if (receipt.bytes !== null) {
298
+ const prepared = prepareSavedArticle(receipt.bytes, doc);
299
+ if (prepared.ok) {
300
+ paint(prepared);
301
+ mountTools(prepared.tools, prepared.toolState);
302
+ }
303
+ }
304
+ return true;
305
+ },
306
+ // Hand this article over to a switch. The lane that owns the queue keeps the
307
+ // pending files and the repair it owes; nothing here stops a stream.
308
+ prepareHandoff(options = {}) {
309
+ if (handoff) return Promise.resolve(handoff);
310
+ if (stopped) return Promise.reject(new Error("clayjs: the reader is stopped"));
311
+ handoff = import("../sync/saved-handoff.js")
312
+ .then(({ readerHandoff }) => readerHandoff({
313
+ stream, options, apply: commit, retire, yieldArticle: reader.yieldArticle, reclaimArticle: reader.reclaimArticle,
314
+ }));
315
+ return handoff;
316
+ },
317
+ stop() {
318
+ if (stopped) return final;
319
+ stopped = true;
320
+ handoff = null;
321
+ mountTurn++;
322
+ stream?.stop();
323
+ stream = null;
324
+ for (const [type, handler, options] of listeners) doc.removeEventListener(type, handler, options);
325
+ listeners.length = 0;
326
+ disposeMounts();
327
+ try { host?.dispose(); } catch {}
328
+ host = null;
329
+ if (!yielded) release();
330
+ yielded = false;
331
+ release = () => false;
332
+ final = Object.freeze({ ...receipt });
333
+ return final;
334
+ },
135
335
  };
336
+ reader.ready = Promise.resolve(reader);
337
+
338
+ if (feed) {
339
+ const onFrame = (event) => {
340
+ const detail = event.detail;
341
+ if (typeof detail?.html !== "string") return;
342
+ // A live-lane frame is a preview of keystrokes nobody saved; a saved-lane
343
+ // frame is a file, whatever tag a sync implementation put on it. An event
344
+ // naming neither is taken as the saved one.
345
+ if (detail.source && detail.source !== "disk" && detail.lane !== "saved") return;
346
+ commit({ bytes: detail.html, etag: detail.etag ?? null, seq: detail.seq ?? null });
347
+ };
348
+ feed.addEventListener("clay:sync-applied", onFrame);
349
+ listeners.push(["clay:sync-applied", onFrame]);
350
+ } else if (stream) {
351
+ // Adopted from the caller's acquisition: this reader takes the stream over
352
+ // rather than opening a second one, so no save falls between the two.
353
+ stream.adopt({ onSnapshot: commit, onStatus: onStreamStatus });
354
+ } else {
355
+ // No file yet: the article on screen is what this reader holds, and the
356
+ // transport asks the host for the saved one as soon as it opens.
357
+ stream = createSavedStream({
358
+ documentURL,
359
+ servedEtag,
360
+ represented: receipt.bytes ? receipt : null,
361
+ onSnapshot: commit,
362
+ onStatus: onStreamStatus,
363
+ });
364
+ stream.start();
365
+ }
366
+ return reader;
367
+ }
368
+
369
+ /**
370
+ * Start a reader from the exact bytes one accepted saved file carried. Validation
371
+ * and the first apply happen before anything is replaced or received, so a caller
372
+ * that already holds a receipt needs no acquisition GET. `stop()` returns that
373
+ * receipt, or the last one a complete apply recorded, frozen and idempotent.
374
+ *
375
+ * `feed` starts over the page already on screen and takes its saved files from
376
+ * that event target's `clay:sync-applied` events, for a page whose saved lane
377
+ * somebody else already owns: its first receipt is the first frame it applies.
378
+ * `stream` adopts a saved transport the caller already opened, so acquisition and
379
+ * steady state share one connection.
380
+ */
381
+ export function start({ represented = null, documentURL, servedEtag = null, stream = null, feed = null, doc = document } = {}) {
382
+ if (represented && (typeof represented.bytes !== "string" || represented.bytes === "")) {
383
+ throw new TypeError("clayjs: the reader needs the full saved bytes to start");
384
+ }
385
+ if (!feed && !stream && !represented && !documentURL) {
386
+ throw new TypeError("clayjs: the reader needs a file, a stream or a page to follow");
387
+ }
388
+ const articles = doc.querySelectorAll(ARTICLE);
389
+ if (articles.length !== 1) throw new Error("clayjs: the reader needs exactly one article[data-page]");
390
+ const article = articles[0];
391
+ if (article.querySelector(".page-editor")) throw new Error("clayjs: the reader will not start over a mounted editor");
392
+ if (represented) {
393
+ const prepared = prepareSavedArticle(represented.bytes, doc);
394
+ if (!prepared.ok) throw new Error("clayjs: the saved file is not a readable page");
395
+ // The version is read from the file's own head before anything is claimed or
396
+ // painted: a file another version wrote replaces this page whole, so a reader
397
+ // showing the old one refuses it and asks for a reload instead.
398
+ if (upgradeReplacesDocument(prepared.source)) {
399
+ stopForUpgrade();
400
+ throw new Error("clayjs: the saved file was published at another version of this document");
401
+ }
402
+ }
403
+ const release = claim(article, "reader");
404
+ try {
405
+ return createReader({ doc, article, represented, feed, documentURL, servedEtag, stream, release });
406
+ } catch (error) {
407
+ release();
408
+ throw error;
409
+ }
136
410
  }
411
+
412
+ // The legacy entry: a reader fed by the generic sync lane's `clay:sync-applied`
413
+ // events. It owns no stream, so its first receipt is the first applied frame.
414
+ export function startPageView(doc = document) {
415
+ const articles = doc.querySelectorAll(ARTICLE);
416
+ if (articles.length !== 1) return () => null;
417
+ return createReader({ doc, article: articles[0], represented: null, feed: doc }).stop;
418
+ }
419
+