@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
@@ -1,4 +1,4 @@
1
- import { isEditMode } from "../core/is-edit-mode.js";
1
+ import { startsAtImport } from "../core/is-edit-mode.js";
2
2
  import onDomReady from "../lib/dom-ready.js";
3
3
  import { set, style } from "../lib/hostile-css.js";
4
4
  import { bevelSurface, pageScheme } from "../ui/bevel-controls.js";
@@ -79,13 +79,65 @@ function show(state) {
79
79
  if (!STICKY.has(state)) hideTimer = setTimeout(() => { set(node, "opacity", "0"); }, 2200);
80
80
  }
81
81
 
82
- function init() {
83
- if (!isEditMode) return;
82
+ let listeners = null;
83
+
84
+ // Arming is idempotent: the plugin's own boot and a mode lease both ask for the same
85
+ // listeners, and one exit must be able to take all of them back.
86
+ function arm() {
84
87
  saveFeedback.chip = true;
88
+ if (listeners) return;
89
+ listeners = new AbortController();
90
+ const { signal } = listeners;
85
91
  for (const state of Object.keys(LABELS)) {
86
- document.addEventListener("clay:save-" + state, () => show(state));
92
+ document.addEventListener("clay:save-" + state, () => show(state), { signal });
87
93
  }
88
- document.addEventListener("clay:page-save-state", (event) => show(event.detail?.state));
94
+ document.addEventListener("clay:page-save-state", (event) => show(event.detail?.state), { signal });
95
+ }
96
+
97
+ function disarm() {
98
+ listeners?.abort();
99
+ listeners = null;
100
+ clearTimeout(hideTimer);
101
+ hideTimer = null;
102
+ el?.remove();
103
+ el = null;
89
104
  }
90
105
 
106
+ function init() {
107
+ if (!startsAtImport()) return;
108
+ arm();
109
+ }
110
+
111
+ /**
112
+ * The save chip as a mode lease.
113
+ *
114
+ * The chip owns no input: it reports a save somebody else is making, so an exit never
115
+ * waits on it. `quiesce` leaves the chip and its fade timer exactly as they were, which
116
+ * is what a refusal has to keep showing; `resume` has nothing to reopen. `initial: true`
117
+ * adopts the chip the boot already armed and arms nothing again. `dispose` (after the
118
+ * barrier) takes the chip, its listeners and its timer down, so a save event in view
119
+ * draws nothing.
120
+ */
121
+ export function indicatorParticipant() {
122
+ return {
123
+ id: "clay:indicator",
124
+ order: 700,
125
+ prepare(context = {}) {
126
+ return {
127
+ enable() {
128
+ if (context.initial === true) return;
129
+ arm();
130
+ },
131
+ quiesce() {},
132
+ resume() {},
133
+ dispose() {
134
+ disarm();
135
+ },
136
+ };
137
+ },
138
+ };
139
+ }
140
+
141
+ export { indicatorParticipant as modeParticipant };
142
+
91
143
  onDomReady(init);
@@ -15,13 +15,14 @@ import { withKeepalive } from "../core/save-core.js";
15
15
  import { isEditMode } from "../core/is-edit-mode.js";
16
16
  import { registerUnsavedState } from "../lib/unsaved-state.js";
17
17
  import { createCheckpoints } from "../live/checkpoint.js";
18
- import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
18
+ import { parseJsonRelaxed } from "../vendor/hyper-morph-json.vendor.js";
19
19
  import * as hostMeta from "../core/host-meta.js";
20
20
  import { createFastLane } from "../live/fast-lane.js";
21
21
  import { createSaveHistoryRecovery, RECOVERY_WINDOW_MS } from "../live/save-history.js";
22
22
  import { serverTransport } from "../live/server-lane.js";
23
23
  import { localTransport, localLaneSupported } from "../live/local-lane.js";
24
24
  import { captureYValue } from "../page/ybind/parts.js";
25
+ import { lastSeenEtag, recordEtag } from "../core/etag.js";
25
26
 
26
27
  const NAME = /^[A-Za-z0-9_.:-]{1,64}$/;
27
28
  const entries = new Map();
@@ -406,17 +407,25 @@ function sortedKeys(value) {
406
407
 
407
408
  function canonical(text) {
408
409
  try {
409
- return JSON.stringify(sortedKeys(HyperMorph.parseJsonRelaxed(text)));
410
+ return JSON.stringify(sortedKeys(parseJsonRelaxed(text)));
410
411
  } catch {
411
412
  return null;
412
413
  }
413
414
  }
414
415
 
416
+ // The writer session a mode lease owns (see liveWriterParticipant). Null until one is
417
+ // prepared, and null again once it is disposed: a page whose loader has not registered
418
+ // a writer keeps the old rule, one lane per edit-mode tab. While a session has closed
419
+ // admission (entry preparing, or an exit draining what this tab still holds) no local
420
+ // op is framed and no checkpoint saves.
421
+ let writerSession = null;
422
+ const writerAdmitted = () => !writerSession || writerSession.admitted;
423
+
415
424
  // Saved stores with changes no accepted save holds yet. Only an edit-mode tab
416
425
  // saves. In file mode that is this tab's unconfirmed ops; in fast mode the room
417
426
  // says the file does not reach its latest change, and only the writer saves.
418
427
  function unsavedLiveChanges() {
419
- if (!isEditMode) return false;
428
+ if (!isEditMode || !writerAdmitted()) return false;
420
429
  for (const entry of entries.values()) {
421
430
  if (entry.store.ephemeral) continue;
422
431
  if (entry.store.handle.status.mode === "fast") {
@@ -437,7 +446,7 @@ function fileMoved() {
437
446
 
438
447
  const checkpoints = createCheckpoints({
439
448
  needsSave: unsavedLiveChanges,
440
- blocked: () => !baselineSettled() || isSaveConflicted() || autosaveIsSuspended() || historyPending(),
449
+ blocked: () => !writerAdmitted() || !baselineSettled() || isSaveConflicted() || autosaveIsSuspended() || historyPending(),
441
450
  save: () => savePageForce().finally(fileMoved),
442
451
  saveNow: () => withKeepalive(true, () => savePageForce()),
443
452
  });
@@ -446,6 +455,9 @@ const checkpoints = createCheckpoints({
446
455
  // clay.live() call in an edit-mode tab.
447
456
  let fast = null;
448
457
  let laneRequested = false;
458
+ // Bumped by every request and every release, so an answer that arrives after its
459
+ // session was released starts nothing.
460
+ let laneRequest = 0;
449
461
  let serverLane = null;
450
462
  // The host merges exact save provenance (live-by v2); without it a save that
451
463
  // needs provenance is refused rather than risk applying an edit twice.
@@ -455,6 +467,144 @@ let local = null;
455
467
  // forward): only a closing page's save is reported to the local room at capture.
456
468
  let closing = false;
457
469
 
470
+ // A listener for the store behind a session: a reader's tag, and the values other
471
+ // tabs saved to it, keep following the file.
472
+ function sessionStores() {
473
+ return [...entries.values()].filter((entry) => !entry.store.ephemeral);
474
+ }
475
+
476
+ // What this tab still holds that the page's own bytes cannot carry: ops a store in a
477
+ // room has not had acknowledged. A reader's own change is not one of them.
478
+ function liveWriterPending() {
479
+ if (!writerSession || writerSession.admitted || !fast || fast.status.stopped) return false;
480
+ for (const entry of sessionStores()) {
481
+ if (entry.store.handle.status.mode === "fast" && entry.store.pending.length > 0) return true;
482
+ }
483
+ return false;
484
+ }
485
+
486
+ // Frame what the lane has not sent and wait for the room to take it. This is the one
487
+ // transfer that lets the writer be released: a checkpoint writes the room's value, so
488
+ // an unacknowledged op of this tab has to travel through the room, not the file.
489
+ export async function drainLiveWriter({ timeoutMs = 5000 } = {}) {
490
+ const deadline = Date.now() + timeoutMs;
491
+ const frame = () => {
492
+ for (const entry of sessionStores()) {
493
+ if (entry.store.handle.status.mode !== "fast") continue;
494
+ if (entry.store.pending.length > 0) fast?.release(entry.store.name);
495
+ }
496
+ };
497
+ frame();
498
+ while (liveWriterPending()) {
499
+ if (Date.now() >= deadline) return { ok: false, code: "timeout" };
500
+ await new Promise((resolve) => setTimeout(resolve, 5));
501
+ frame();
502
+ }
503
+ return { ok: true };
504
+ }
505
+
506
+ registerUnsavedState({ id: "live:writer", isPending: liveWriterPending, flush: drainLiveWriter });
507
+
508
+ // The lane a session starts from: the reader's own ops are not this session's edits,
509
+ // and no position a previous session's lane reported is carried into this one.
510
+ function resetWriterSession() {
511
+ for (const entry of entries.values()) {
512
+ entry.framesSince = false;
513
+ const ids = entry.store.pendingEntries().map((item) => item.id);
514
+ if (ids.length) entry.store.rebase(entry.store.authoritative, { source: "state", dropIds: ids });
515
+ }
516
+ }
517
+
518
+ // Give back everything the lane holds: the transport's subscription (its stream
519
+ // handlers and its local channel), the room position, and the request for a lane, so
520
+ // the next session starts a fresh one.
521
+ function releaseWriter() {
522
+ const lane = fast;
523
+ fast = null;
524
+ laneRequested = false;
525
+ laneRequest++;
526
+ serverLane = null;
527
+ local = null;
528
+ provenanceHost = false;
529
+ // The gates went with the lane: a second entry must not read this released one as ready.
530
+ laneReady = null;
531
+ writerReady = null;
532
+ settleLane = null;
533
+ settleWriter = null;
534
+ lane?.stop();
535
+ }
536
+
537
+ /**
538
+ * clay.live as a mode participant: the store's writer, started by the commit that
539
+ * enters edit and released by the one that leaves it.
540
+ *
541
+ * `prepare` closes admission and puts the stores back on the reader's value, so
542
+ * nothing a reader changed can be uploaded as this person's edit. `enable` starts
543
+ * the tab's lane. `quiesce` closes admission again and drains the ops the room has
544
+ * not acknowledged, so a refused exit resumes the very same lane. `dispose` (after the
545
+ * barrier) stops the lane and forgets its subscription, its room and its send queue.
546
+ */
547
+ export function liveWriterParticipant() {
548
+ return {
549
+ id: "clay:live",
550
+ order: 200,
551
+ prepare(context = {}) {
552
+ // An adoption is the session this tab's edit boot started: the exit takes that one
553
+ // back, and the entry asks for no second lane.
554
+ if (context.initial === true) {
555
+ if (!writerSession) writerSession = { admitted: true };
556
+ return writerLease(writerSession, { adopted: true });
557
+ }
558
+ freshLaneGates();
559
+ // The lane's transport opens with the preparation, not with the commit: the page
560
+ // (order 400) waits on the room while it prepares, and it prepares before any
561
+ // enable runs. Admission is still the commit's, so what a person does is what
562
+ // reaches the room.
563
+ const session = { admitted: false, wantsLane: true };
564
+ writerSession = session;
565
+ resetWriterSession();
566
+ ensureLane();
567
+ return writerLease(session);
568
+ },
569
+ };
570
+ }
571
+
572
+ function releaseHeld() {
573
+ for (const entry of sessionStores()) {
574
+ if (entry.store.pending.length > 0) fast?.release(entry.store.name);
575
+ }
576
+ }
577
+
578
+ function writerLease(session, { adopted = false } = {}) {
579
+ return {
580
+ enable() {
581
+ if (adopted || writerSession !== session) return;
582
+ session.admitted = true;
583
+ ensureLane();
584
+ // A lane opened at preparation may already hold ops the page made before the
585
+ // commit (an adoption's), which onLocal kept back while admission was closed.
586
+ releaseHeld();
587
+ },
588
+ quiesce() {
589
+ if (writerSession !== session) return;
590
+ session.admitted = false;
591
+ return drainLiveWriter();
592
+ },
593
+ resume() {
594
+ if (writerSession !== session) return;
595
+ session.admitted = true;
596
+ releaseHeld();
597
+ },
598
+ dispose() {
599
+ if (writerSession !== session) return;
600
+ writerSession = null;
601
+ releaseWriter();
602
+ },
603
+ };
604
+ }
605
+
606
+ export { liveWriterParticipant as modeParticipant };
607
+
458
608
  // The page plugin's one-shot gates: the fast lane's setup, and the room's
459
609
  // writer election. Both resolve null when this tab has no lane at all.
460
610
  let laneReady = null;
@@ -462,11 +612,15 @@ let writerReady = null;
462
612
  let settleLane = null;
463
613
  let settleWriter = null;
464
614
 
615
+ // A fresh pair for a new session: the page plugin awaits these, and the ones a released
616
+ // session settled answer for a lane that no longer exists.
617
+ function freshLaneGates() {
618
+ laneReady = new Promise((resolve) => { settleLane = resolve; });
619
+ writerReady = new Promise((resolve) => { settleWriter = resolve; });
620
+ }
621
+
465
622
  function laneGates() {
466
- if (!laneReady) {
467
- laneReady = new Promise((resolve) => { settleLane = resolve; });
468
- writerReady = new Promise((resolve) => { settleWriter = resolve; });
469
- }
623
+ if (!laneReady) freshLaneGates();
470
624
  return { laneReady, writerReady };
471
625
  }
472
626
 
@@ -597,7 +751,7 @@ function seedLocalRoom() {
597
751
  continue;
598
752
  }
599
753
  try {
600
- const value = HyperMorph.parseJsonRelaxed(tag.element.textContent);
754
+ const value = parseJsonRelaxed(tag.element.textContent);
601
755
  if (value !== null && typeof value === "object") values.set(name, { value, ephemeral: tag.ephemeral });
602
756
  } catch {}
603
757
  }
@@ -606,17 +760,21 @@ function seedLocalRoom() {
606
760
 
607
761
  function ensureLane() {
608
762
  laneGates();
609
- if (laneRequested || !isEditMode) return;
763
+ if (laneRequested || !(writerSession ? writerSession.admitted || writerSession.wantsLane : isEditMode)) return;
610
764
  laneRequested = true;
765
+ const request = ++laneRequest;
611
766
  hostMeta.hostSupports("live-data").then(
612
767
  async (supported) => {
613
768
  const provenance = supported && (await hostMeta.hostSupports("live-provenance"));
769
+ if (request !== laneRequest) return;
614
770
  serverLane = supported;
615
771
  provenanceHost = provenance;
616
772
  if (supported) {
617
773
  fast = createFastLane(laneOptions(serverTransport()));
618
774
  } else if (localLaneSupported()) {
619
- local = localTransport({ seed: seedLocalRoom });
775
+ // The document's own floor picks the channel scope: a floor-1 document
776
+ // never shares the lane a tab that cannot read it is on.
777
+ local = localTransport({ seed: seedLocalRoom, floor: document.querySelector('meta[name="clay-page-format"]')?.getAttribute("content") });
620
778
  fast = createFastLane(laneOptions(local));
621
779
  } else {
622
780
  settleLane?.(null);
@@ -707,7 +865,7 @@ export function serializeLines(value) {
707
865
  // already holds (its frames that landed before the value's stamp).
708
866
  function hashOfText(text) {
709
867
  try {
710
- return textHash(serializeLive(HyperMorph.parseJsonRelaxed(text)));
868
+ return textHash(serializeLive(parseJsonRelaxed(text)));
711
869
  } catch {
712
870
  return null;
713
871
  }
@@ -716,28 +874,16 @@ function hashOfText(text) {
716
874
  function receive(entry, text, source, dropIds) {
717
875
  let value;
718
876
  try {
719
- value = HyperMorph.parseJsonRelaxed(text);
877
+ value = parseJsonRelaxed(text);
720
878
  } catch {
721
879
  return;
722
880
  }
723
881
  if (entry.store.rebase(value, { source, ...(dropIds?.length && { dropIds }) })) entry.framesSince = true;
724
882
  }
725
883
 
726
- export function live(name) {
727
- const existing = entries.get(name);
728
- if (existing) return existing.store.handle;
729
- const tag = liveTags().get(name);
730
- if (!tag) throw new Error(`clay.live: this page has no <script type="application/json" merge="${name}" clay="live"> tag`);
731
- const text = tag.element.textContent;
732
- let value;
733
- try {
734
- value = HyperMorph.parseJsonRelaxed(text);
735
- } catch (err) {
736
- throw new SyntaxError(`clay.live("${name}"): the tag's JSON does not parse: ${err.message}`);
737
- }
738
- const store = createStore({ name, value, ephemeral: tag.ephemeral });
739
- const layout = tag.element.getAttribute("live-format") === "lines" ? "lines" : text.includes("\n") ? "pretty" : "line";
740
- const entry = { store, layout, framesSince: false, last: 0, landed: 0, leftAt: lineageOf(tag.element), captureOps: new Map(), withheld: false, reading: false, timer: null, recovery: null, historicalRefreshPending: false, refreshTimer: null };
884
+ // One open store and what the plugin follows about it. A staged store has no lane.
885
+ function openEntry(name, store, { layout = "line", leftAt = null, staged = null } = {}) {
886
+ const entry = { store, layout, framesSince: false, last: 0, landed: 0, leftAt, captureOps: new Map(), withheld: false, reading: false, timer: null, recovery: null, historicalRefreshPending: false, refreshTimer: null, staged };
741
887
  entries.set(name, entry);
742
888
  store.onDelivery((event) => {
743
889
  if (event.code === "receipt-expired") document.dispatchEvent(new CustomEvent("clay:live-notice", {
@@ -756,14 +902,79 @@ export function live(name) {
756
902
  settle(entry);
757
903
  if (store.handle.status.mode === "file" && ["undone", "refused", "decided"].includes(event.cause)) entry.leftAt = null;
758
904
  });
759
- if (!tag.ephemeral) store.onLocal(() => checkpoints.changed());
760
- store.onLocal((ops) => fast?.local(name, ops, store.mark));
905
+ if (!store.ephemeral) store.onLocal(() => checkpoints.changed());
906
+ store.onLocal((ops) => { if (!entry.staged && writerAdmitted()) fast?.local(name, ops, store.mark); });
761
907
  store.handle.on("change", () => {
762
908
  if (!store.ephemeral && store.handle.status.mode === "fast") checkpoints.changed();
763
909
  });
764
- fast?.storeOpened(name);
910
+ if (!staged) {
911
+ fast?.storeOpened(name);
912
+ ensureLane();
913
+ }
914
+ return entry;
915
+ }
916
+
917
+ export function live(name) {
918
+ const existing = entries.get(name);
919
+ if (existing) return existing.store.handle;
920
+ const tag = liveTags().get(name);
921
+ if (!tag) throw new Error(`clay.live: this page has no <script type="application/json" merge="${name}" clay="live"> tag`);
922
+ const text = tag.element.textContent;
923
+ let value;
924
+ try {
925
+ value = parseJsonRelaxed(text);
926
+ } catch (err) {
927
+ throw new SyntaxError(`clay.live("${name}"): the tag's JSON does not parse: ${err.message}`);
928
+ }
929
+ const store = createStore({ name, value, ephemeral: tag.ephemeral });
930
+ const layout = tag.element.getAttribute("live-format") === "lines" ? "lines" : text.includes("\n") ? "pretty" : "line";
931
+ const entry = openEntry(name, store, { layout, leftAt: lineageOf(tag.element) });
932
+ return entry.store.handle;
933
+ }
934
+
935
+ // A page whose file carries no state of its own yet: the seed in memory, no tag in
936
+ // the document, no request, until its first genuine op is written by the ordinary
937
+ // save. Only an accepted write opens its room.
938
+ export function stageStore(name, { value, represented = null, signal = null } = {}) {
939
+ const existing = entries.get(name);
940
+ if (existing && !existing.staged) throw new Error(`clay.live("${name}"): the store is open in a room`);
941
+ const entry = existing ?? openEntry(name, createStore({ name, value, ephemeral: false }), { staged: { represented, signal } });
942
+ return {
943
+ handle: entry.store.handle,
944
+ isPending: () => !!entry.staged && entry.store.pending.length > 0,
945
+ flush: (options = {}) => flushStaged(entry, options),
946
+ dispose() {
947
+ if (entry.staged && entries.get(name) === entry) entries.delete(name);
948
+ },
949
+ // The session that staged this store is over, whether or not its first write was
950
+ // accepted: the room keeps the value, and the next session stages its own from the
951
+ // bytes it represents.
952
+ close() {
953
+ if (entries.get(name) === entry) entries.delete(name);
954
+ },
955
+ };
956
+ }
957
+
958
+ // The first write, through the save this tab would make anyway.
959
+ async function flushStaged(entry, { timeoutMs = 10_000 } = {}) {
960
+ const staged = entry.staged;
961
+ if (!staged || !entry.store.pending.length) return { ok: true };
962
+ if (staged.conflict) return { ok: false, code: staged.conflict };
963
+ const at = staged.represented?.etag || null;
964
+ const held = lastSeenEtag();
965
+ if (at && held && at !== held) return { ok: false, code: "stale-document" };
966
+ if (at && !held) recordEtag(at);
967
+ const result = await requestCheckpoint({ timeoutMs });
968
+ return result.ok ? { ok: true } : { ok: false, code: staged.conflict ?? result.code ?? null };
969
+ }
970
+
971
+ function activateStaged(entry) {
972
+ if (!entry.staged) return;
973
+ entry.staged = null;
974
+ // Its file had no tag and the live page never gets one: every later capture builds it.
975
+ entry.tagless = true;
976
+ fast?.storeOpened(entry.store.name);
765
977
  ensureLane();
766
- return store.handle;
767
978
  }
768
979
 
769
980
  // Every capture carries each saved store's current value. A never-saved store
@@ -773,7 +984,19 @@ onSnapshot((clone, { forSave, requestRefresh }) => {
773
984
  const tags = liveTags(clone, { warn: false });
774
985
  for (const entry of entries.values()) {
775
986
  if (entry.store.ephemeral) continue;
776
- const tag = tags.get(entry.store.name);
987
+ let tag = tags.get(entry.store.name);
988
+ // A staged store reaches the file here, where the boot would have put its tag, and
989
+ // so does one that started that way, on every save after its first.
990
+ if (!tag && (entry.staged || entry.tagless)) {
991
+ const article = clone.querySelector("article[data-page]");
992
+ if (!article) continue;
993
+ const element = article.ownerDocument.createElement("script");
994
+ element.setAttribute("type", "application/json");
995
+ element.setAttribute("merge", entry.store.name);
996
+ element.setAttribute("clay", "live");
997
+ article.parentNode.insertBefore(element, article);
998
+ tag = { name: entry.store.name, ephemeral: false, element };
999
+ }
777
1000
  if (!tag) continue;
778
1001
  // A recovering store's own operations are not written again while the answer
779
1002
  // about them is still open, and not before the value the file holds now has
@@ -936,6 +1159,7 @@ function confirmPin(html, pin) {
936
1159
  } else {
937
1160
  entry.store.confirm(saved.mark);
938
1161
  entry.leftAt = saved.lineage && tag.element.getAttribute("live-by") === saved.by ? saved.lineage : { disk: true };
1162
+ activateStaged(entry);
939
1163
  }
940
1164
  settle(entry);
941
1165
  }
@@ -992,6 +1216,17 @@ onSaveOutcome(({ captureId, result, attempts, sent }) => {
992
1216
  if (result?.ok) return;
993
1217
  const pin = capturePins.get(captureId);
994
1218
  if (!pin) return;
1219
+ const stagedEntry = [...pin.keys()].map((name) => entries.get(name)).find((entry) => entry?.staged);
1220
+ // Nothing was written: the staged store is clean and nothing follows.
1221
+ if (stagedEntry && result.code === "page-nothing-to-write") {
1222
+ stagedEntry.store.confirm(stagedEntry.store.mark);
1223
+ dropCapture(captureId);
1224
+ releaseBody();
1225
+ settle(stagedEntry);
1226
+ return;
1227
+ }
1228
+ // This tab's value does not descend from the stored document: never send it again.
1229
+ if (stagedEntry && result.code === "page-lineage-stale") stagedEntry.staged.conflict = "conflict";
995
1230
  dropCapture(captureId);
996
1231
  const landed = mayHaveLanded(result, sent);
997
1232
  const plan = landed ? historyPlan(captureId) : null;
@@ -1088,6 +1323,8 @@ export function receiveFrame(html, seq = null) {
1088
1323
  else if (pageTag.getAttribute("live-by") !== by) pageTag.setAttribute("live-by", by);
1089
1324
  }
1090
1325
  if (entry && !entry.store.ephemeral) {
1326
+ // An old seed is never merged into a newer article before its first write.
1327
+ if (entry.staged) continue;
1091
1328
  if (entry.store.handle.status.mode === "file") receive(entry, text, "remote", dropIds);
1092
1329
  continue;
1093
1330
  }
@@ -1100,10 +1337,13 @@ document.addEventListener("clay:sync-applied", (event) => receiveFrame(event.det
1100
1337
  // The last chance to save live changes made since the previous checkpoint. A
1101
1338
  // keepalive body is capped near 60 KB; a larger page's save may not finish.
1102
1339
  document.addEventListener("visibilitychange", () => {
1103
- if (document.visibilityState === "hidden") checkpoints.flush();
1340
+ if (document.visibilityState === "hidden" && isEditMode && writerAdmitted()) checkpoints.flush();
1104
1341
  });
1105
1342
  window.addEventListener("pagehide", () => {
1106
1343
  closing = true;
1344
+ // A view, and an edit whose session has closed admission or been released, saves
1345
+ // nothing here: its bytes are the reader's, and the exit's own barrier writes them.
1346
+ if (!isEditMode || !writerAdmitted()) return;
1107
1347
  // A hide save still on the wire is answered after the page is gone too.
1108
1348
  const latest = unsentCaptureId ?? wireCaptureId;
1109
1349
  const pin = checkpoints.flushing && latest !== null ? capturePins.get(latest) : null;
@@ -1121,6 +1361,14 @@ export function storeOf(name) {
1121
1361
  return entries.get(name)?.store || null;
1122
1362
  }
1123
1363
 
1364
+ // A caller that opened a store itself is done with it: the next entry reads the tag the
1365
+ // reader now states. Only the store that caller opened is closed; one the app opened
1366
+ // first, or a staged store, stays.
1367
+ export function closeStore(name, store) {
1368
+ const entry = entries.get(name);
1369
+ if (entry && entry.store === store && !entry.staged) entries.delete(name);
1370
+ }
1371
+
1124
1372
  export function openStores() {
1125
1373
  return [...entries.values()].map((entry) => entry.store);
1126
1374
  }
@@ -0,0 +1 @@
1
+ export default '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-45.87 -57.96 100.26 60.98" fill="currentColor" aria-hidden="true" focusable="false"><path fill-rule="evenodd" d="M44.35 -4.56Q40.03 -0.52 34.69 -0.39Q29.34 -0.25 24.81 -0.14Q20.27 -0.02 15.6 0Q10.93 0.02 6.41 0Q1.89 -0.02 -2.79 -0.02Q-7.48 -0.02 -12.11 -0.14Q-16.73 -0.25 -21.64 -0.27Q-26.55 -0.3 -31.72 -2.25Q-36.88 -4.2 -39.87 -9.12Q-42.87 -14.03 -42.8 -19.61Q-42.74 -25.18 -40.43 -29.94Q-38.12 -34.69 -34.82 -38.78Q-31.52 -42.87 -27.25 -45.6Q-22.98 -48.33 -18.35 -50.59Q-13.73 -52.85 -8.7 -53.88Q-3.68 -54.91 1.32 -54.93Q6.32 -54.96 11.44 -54.56Q16.55 -54.16 21.35 -52.2Q26.16 -50.24 30.69 -48.01Q35.22 -45.78 38.99 -42.18Q42.76 -38.59 45.67 -34.37Q48.59 -30.15 49.99 -24.87Q51.39 -19.59 50.04 -14.09Q48.68 -8.6 44.35 -4.56ZM0.66 -30.49C0.5 -28.73 -0.88 -27.42 -2.42 -27.57C-3.96 -27.71 -5.08 -29.25 -4.91 -31.01C-4.75 -32.77 -3.36 -34.08 -1.83 -33.94C-0.29 -33.79 0.83 -32.25 0.66 -30.49ZM13.61 -29.28C13.44 -27.52 12.06 -26.21 10.52 -26.35C8.98 -26.5 7.87 -28.04 8.03 -29.8C8.2 -31.56 9.58 -32.87 11.12 -32.73C12.66 -32.58 13.77 -31.04 13.61 -29.28ZM-1.23 -17.71Q-0.62 -17.35 -0.03 -17.08Q0.56 -16.81 1.15 -16.54Q1.73 -16.28 2.4 -16Q3.07 -15.72 3.87 -15.6Q4.66 -15.48 5.4 -15.55Q6.13 -15.62 6.77 -15.79Q7.41 -15.96 7.97 -16.23Q8.53 -16.5 9 -16.91Q9.46 -17.32 9.73 -17.9Q9.99 -18.47 10.08 -19.12Q10.17 -19.77 10.08 -20.42Q9.99 -21.06 9.45 -21.32Q8.91 -21.58 8.27 -21.38Q7.63 -21.17 6.99 -20.91Q6.36 -20.65 5.7 -20.61Q5.04 -20.56 4.33 -20.73Q3.62 -20.9 2.92 -21.14Q2.23 -21.39 1.54 -21.68Q0.85 -21.97 0.16 -22.27Q-0.52 -22.56 -1.22 -22.79Q-1.92 -23.03 -2.49 -22.61Q-3.05 -22.19 -3.23 -21.5Q-3.4 -20.81 -3.2 -20.09Q-3.01 -19.38 -3.02 -19.15Q-3.02 -18.93 -2.42 -18.5Q-1.83 -18.06 -1.23 -17.71Z"/></svg>';