@panphora/clayjs 1.16.0 → 1.18.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 (511) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +72519 -59839
  11. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  12. package/dist/m/attrs/save-freeze-LWGF6XUN.js +1 -0
  13. package/dist/m/c-2GRUM77Q.js +30 -0
  14. package/dist/m/c-2KIEGNC4.js +1 -0
  15. package/dist/m/{c-5R4PTRG2.js → c-2KL3BD52.js} +1 -1
  16. package/dist/m/c-2YYDMLNZ.js +1 -0
  17. package/dist/m/c-2ZZBC45O.js +1 -0
  18. package/dist/m/c-372N6H3B.js +1 -0
  19. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  20. package/dist/m/c-3LDGIBMN.js +1 -0
  21. package/dist/m/c-3O7OBDCM.js +1 -0
  22. package/dist/m/c-4ASQPRQL.js +1 -0
  23. package/dist/m/c-4TXNLX4K.js +6 -0
  24. package/dist/m/c-4YU3BR4D.js +6 -0
  25. package/dist/m/c-5734MF2P.js +1 -0
  26. package/dist/m/c-57XDY6HC.js +1 -0
  27. package/dist/m/{c-VFX45IZQ.js → c-5B4YBZXC.js} +1 -1
  28. package/dist/m/c-5ONTYIUI.js +1 -0
  29. package/dist/m/c-5OOVFCJM.js +1 -0
  30. package/dist/m/c-5T6NZHNM.js +1 -0
  31. package/dist/m/{c-YDH7Q3NY.js → c-5ZQV7KC4.js} +1 -1
  32. package/dist/m/{c-Z36J63PP.js → c-6DB2OLRJ.js} +1 -1
  33. package/dist/m/c-6DNRCPNM.js +1 -0
  34. package/dist/m/c-6UGWFMQC.js +2 -0
  35. package/dist/m/c-7K4SCQB7.js +1 -0
  36. package/dist/m/c-7MG5RU63.js +1 -0
  37. package/dist/m/c-7PRJCA3E.js +1 -0
  38. package/dist/m/c-A6L3FMUS.js +1 -0
  39. package/dist/m/c-AMU3BPPR.js +4 -0
  40. package/dist/m/c-AMYEFA2E.js +1 -0
  41. package/dist/m/c-AQYCG7QW.js +1 -0
  42. package/dist/m/c-ATKAOH23.js +1 -0
  43. package/dist/m/c-B3JM5ZXC.js +1 -0
  44. package/dist/m/c-B5CS55WR.js +1 -0
  45. package/dist/m/c-B5UJQT7W.js +1 -0
  46. package/dist/m/c-B7J4K2AE.js +3 -0
  47. package/dist/m/c-BHZYSHW4.js +1 -0
  48. package/dist/m/c-BIJALSRD.js +1 -0
  49. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  50. package/dist/m/c-BPF2NSPV.js +1 -0
  51. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  52. package/dist/m/c-BUAYFEV5.js +1 -0
  53. package/dist/m/c-C6IPRG34.js +1 -0
  54. package/dist/m/{c-2JL6E75N.js → c-CAS3NND4.js} +1 -1
  55. package/dist/m/{c-4DRMS4QG.js → c-CCQ25GFY.js} +1 -1
  56. package/dist/m/c-CJFDLD43.js +1 -0
  57. package/dist/m/c-COPTL5WB.js +1 -0
  58. package/dist/m/c-D3E4WJ4M.js +166 -0
  59. package/dist/m/c-D5IKEKXW.js +7 -0
  60. package/dist/m/c-DHBRA6DL.js +1 -0
  61. package/dist/m/c-DKLS74XB.js +72 -0
  62. package/dist/m/c-DVYGFTSK.js +3 -0
  63. package/dist/m/c-E3ZXARWA.js +1 -0
  64. package/dist/m/c-EK63G3CG.js +1 -0
  65. package/dist/m/c-ERQSNR6K.js +13 -0
  66. package/dist/m/c-EVYAISP3.js +1 -0
  67. package/dist/m/c-EXKHPOVC.js +3 -0
  68. package/dist/m/{c-7VGZ7RU5.js → c-FWKHM4E7.js} +1 -1
  69. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  70. package/dist/m/c-GNDL3TKB.js +1 -0
  71. package/dist/m/c-GRWMD6O5.js +1 -0
  72. package/dist/m/c-GRXHHFPO.js +1 -0
  73. package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
  74. package/dist/m/c-HAWFNBYG.js +1 -0
  75. package/dist/m/c-HG2C77BM.js +4 -0
  76. package/dist/m/c-HHGJBU4D.js +1 -0
  77. package/dist/m/c-HMV6P4RX.js +1 -0
  78. package/dist/m/c-HMYWIHFG.js +1 -0
  79. package/dist/m/c-HT2DKBV2.js +1 -0
  80. package/dist/m/c-I2SCCUEW.js +1 -0
  81. package/dist/m/{c-HRDCD3WN.js → c-ICJYHABF.js} +2 -2
  82. package/dist/m/c-IMZCWYHR.js +7 -0
  83. package/dist/m/c-IVMD3FEM.js +1 -0
  84. package/dist/m/c-IVTRVENZ.js +1 -0
  85. package/dist/m/c-IW6GGOU2.js +1 -0
  86. package/dist/m/c-JKZJIUIP.js +1 -0
  87. package/dist/m/{c-NZMNXVF6.js → c-JNLX3RJB.js} +1 -1
  88. package/dist/m/c-JRKY44DK.js +1 -0
  89. package/dist/m/c-K3ADYNLU.js +1 -0
  90. package/dist/m/c-KBS3UV6B.js +1 -0
  91. package/dist/m/c-KGHVFRR3.js +1 -0
  92. package/dist/m/c-KJJPV77R.js +1 -0
  93. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  94. package/dist/m/{c-5Y6TNX5W.js → c-KP64LT2A.js} +2 -2
  95. package/dist/m/c-KQ7E3PYY.js +1 -0
  96. package/dist/m/c-L3F5ZLIV.js +1 -0
  97. package/dist/m/{c-LR4CGEPT.js → c-L42OLXWM.js} +3 -2
  98. package/dist/m/c-L5VFATT6.js +1 -0
  99. package/dist/m/c-L7JL5TL3.js +3 -0
  100. package/dist/m/c-LKSL4AUJ.js +1 -0
  101. package/dist/m/c-M6FNQCTQ.js +1 -0
  102. package/dist/m/c-MCOOHCSG.js +1 -0
  103. package/dist/m/c-MHI3GYKF.js +1 -0
  104. package/dist/m/c-MWXFLW4J.js +1 -0
  105. package/dist/m/c-NEL7HQW3.js +1 -0
  106. package/dist/m/c-NQJRVJ56.js +1 -0
  107. package/dist/m/c-NWX3MFT4.js +1 -0
  108. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  109. package/dist/m/c-OKXI52N7.js +1 -0
  110. package/dist/m/{c-MMUIUUZG.js → c-OPLUZR3A.js} +1 -1
  111. package/dist/m/{c-J7CA54IP.js → c-OTMKN2OO.js} +1 -1
  112. package/dist/m/{c-LTGMXXKO.js → c-OYRZ7XG2.js} +1 -1
  113. package/dist/m/c-P3OFYUMA.js +1 -0
  114. package/dist/m/c-PLN76GF3.js +6 -0
  115. package/dist/m/c-PLP4JKFC.js +1 -0
  116. package/dist/m/c-Q26MME6N.js +1 -0
  117. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  118. package/dist/m/{c-SWRJFCNN.js → c-QIKT6SKM.js} +1 -1
  119. package/dist/m/c-QIXT3Q7C.js +28 -0
  120. package/dist/m/c-QO4HNGCQ.js +1 -0
  121. package/dist/m/c-R27DWQKX.js +1 -0
  122. package/dist/m/{c-HVYXIS72.js → c-R2XUODVY.js} +1 -1
  123. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  124. package/dist/m/c-RND3G2S2.js +1 -0
  125. package/dist/m/c-S2U6NJU4.js +1 -0
  126. package/dist/m/c-SPZPGD3R.js +1 -0
  127. package/dist/m/c-SW4YEG2D.js +8 -0
  128. package/dist/m/c-SZGYW5HT.js +1 -0
  129. package/dist/m/c-T637T7SN.js +1 -0
  130. package/dist/m/c-TJAXGIRV.js +1 -0
  131. package/dist/m/{c-KKWLDOPM.js → c-TNEM3RNM.js} +1 -1
  132. package/dist/m/c-TTB5EK4S.js +32 -0
  133. package/dist/m/c-U7SHNWDH.js +1 -0
  134. package/dist/m/c-UDHIVLYH.js +20 -0
  135. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  136. package/dist/m/{c-TQZ2JYHD.js → c-UORXLSD3.js} +1 -1
  137. package/dist/m/c-UPD33ZPE.js +1 -0
  138. package/dist/m/c-URSMSFCB.js +1 -0
  139. package/dist/m/c-UY4QX42B.js +1 -0
  140. package/dist/m/c-V44KWI2L.js +4 -0
  141. package/dist/m/c-VG5XIN4T.js +1 -0
  142. package/dist/m/c-VILENZTD.js +1 -0
  143. package/dist/m/{c-33HUYEDI.js → c-VJYEVRYK.js} +1 -1
  144. package/dist/m/c-VR2BJ4AN.js +1 -0
  145. package/dist/m/c-W4JCVLAB.js +1 -0
  146. package/dist/m/c-W6DQSHXI.js +135 -0
  147. package/dist/m/c-WBGWGOM7.js +1 -0
  148. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  149. package/dist/m/c-WGW764WX.js +1 -0
  150. package/dist/m/c-WMRJKQ2V.js +5 -0
  151. package/dist/m/c-WNTYKCDG.js +1 -0
  152. package/dist/m/c-WQWIRWO3.js +72 -0
  153. package/dist/m/c-X5LNHET5.js +1 -0
  154. package/dist/m/c-X5TSXZYJ.js +2 -0
  155. package/dist/m/c-XGH4GS2T.js +1 -0
  156. package/dist/m/c-Y6F2FORL.js +1 -0
  157. package/dist/m/c-YBKHUAK7.js +10 -0
  158. package/dist/m/c-YBSS55KU.js +20 -0
  159. package/dist/m/c-YCLU4UNT.js +2 -0
  160. package/dist/m/c-YVZ2L7CE.js +1 -0
  161. package/dist/m/c-ZK4GIZH2.js +1 -0
  162. package/dist/m/c-ZRXJD4G6.js +1 -0
  163. package/dist/m/c-ZWBI4HYK.js +1 -0
  164. package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
  165. package/dist/m/core/admin-attrs-YY7T5FGU.js +1 -0
  166. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  167. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  168. package/dist/m/core/host-permission-6EVJGNPL.js +1 -0
  169. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  170. package/dist/m/core/{persist-HKHA66IY.js → persist-UKXV5KLA.js} +1 -1
  171. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  172. package/dist/m/core/save-LYY7K6U7.js +1 -0
  173. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  174. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  175. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  176. package/dist/m/events/index-RN2JNGBV.js +1 -0
  177. package/dist/m/internals/index-TGLQINN3.js +1 -0
  178. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  179. package/dist/m/loader-3BBNZZGM.js +1 -0
  180. package/dist/m/manifest-F449FF98.js +1 -0
  181. package/dist/m/manifest-edit-4396A009.js +1 -0
  182. package/dist/m/manifest.json +2509 -1474
  183. package/dist/m/options/options-EJMS3K24.js +1 -0
  184. package/dist/m/page/editor-6LRB3GE4.js +304 -0
  185. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  186. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  187. package/dist/m/plugins/demo-OYO5GCAO.js +1 -0
  188. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  189. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  190. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  191. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  192. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  193. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  194. package/dist/m/plugins/people-JCHVPF7P.js +1 -0
  195. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  196. package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-KLGETK77.js} +2 -2
  197. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  198. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  199. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  200. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  201. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  202. package/dist/m/plugins/upload-NHGJXZK2.js +1 -0
  203. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  204. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  205. package/dist/m/plugins/wire-VBWXEXU2.js +1 -0
  206. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  207. package/dist/m/route/route-62CHXL5B.js +1 -0
  208. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  209. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  210. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  211. package/dist/offline-manifest.json +1560 -889
  212. package/dist/page/themes/light-1.css +33 -8
  213. package/dist/server/core.cjs +1554 -311
  214. package/dist/server/offline-runtime.cjs +63 -63
  215. package/dist/server/offline-runtime.mjs +63 -63
  216. package/dist/server/page-compaction-worker.cjs +3710 -0
  217. package/dist/server/rooms.mjs +5 -0
  218. package/docs/guide.md +653 -101
  219. package/entries/sap.js +5 -5
  220. package/package.json +1 -1
  221. package/src/core/admin-contenteditable.js +5 -2
  222. package/src/core/admin-inputs.js +1 -0
  223. package/src/core/admin-onclick.js +2 -1
  224. package/src/core/boot-phase.js +2 -1
  225. package/src/core/conflict-download.js +3 -29
  226. package/src/core/conflict-notice-ui.js +4 -7
  227. package/src/core/download.js +32 -0
  228. package/src/core/host-meta.js +56 -0
  229. package/src/core/is-edit-mode.js +17 -1
  230. package/src/core/mode-key.js +125 -19
  231. package/src/core/mode-support.js +5 -0
  232. package/src/core/mode-transition.js +20 -6
  233. package/src/core/preserve-authored.js +4 -4
  234. package/src/core/recovery.js +121 -33
  235. package/src/core/save-accepted.js +33 -0
  236. package/src/core/save-core.js +111 -32
  237. package/src/core/save-job.js +108 -0
  238. package/src/core/save-prepare.js +53 -0
  239. package/src/core/save-size-notice.js +3 -1
  240. package/src/core/save-slices.js +15 -0
  241. package/src/core/save.js +557 -127
  242. package/src/core/serialize-steps.js +109 -0
  243. package/src/core/snapshot.js +209 -40
  244. package/src/core/source-map.js +83 -38
  245. package/src/core/stale-host-notice.js +1 -0
  246. package/src/core/upgrade-run.js +59 -8
  247. package/src/dom/active-element.js +5 -0
  248. package/src/lib/assets.js +1 -0
  249. package/src/lib/autosave-scheduler.js +3 -4
  250. package/src/lib/slice.js +65 -0
  251. package/src/lib/uimotion.js +774 -0
  252. package/src/lib/unsaved-state.js +2 -2
  253. package/src/live/fast-lane.js +10 -3
  254. package/src/live/local-lane.js +202 -11
  255. package/src/live/ops.js +16 -0
  256. package/src/live/page-compaction.js +162 -0
  257. package/src/live/server-lane.js +1 -0
  258. package/src/live/store.js +11 -3
  259. package/src/loader-logic.js +3 -1
  260. package/src/loader.js +14 -4
  261. package/src/page/adopt-boot.js +2 -1
  262. package/src/page/awareness.js +33 -2
  263. package/src/page/blocks.js +122 -27
  264. package/src/page/board-views.js +2 -16
  265. package/src/page/boot-pack.js +1 -1
  266. package/src/page/chart-tables.js +29 -0
  267. package/src/page/chart.js +411 -72
  268. package/src/page/checklist.js +10 -135
  269. package/src/page/checkpoint-proof.js +295 -0
  270. package/src/page/chrome.js +257 -40
  271. package/src/page/clipboard-shape.js +126 -0
  272. package/src/page/document-title-dom.js +12 -0
  273. package/src/page/document-title-runtime.js +15 -1
  274. package/src/page/document-title.js +4 -1
  275. package/src/page/drag.js +33 -3
  276. package/src/page/editor.js +207 -31
  277. package/src/page/emoji-data.js +162 -0
  278. package/src/page/export-assets.js +357 -0
  279. package/src/page/export-html.js +209 -0
  280. package/src/page/export-markdown.js +361 -0
  281. package/src/page/features/board.js +12 -3
  282. package/src/page/features/chart.js +11 -0
  283. package/src/page/features/checklist.js +428 -0
  284. package/src/page/features/dates.js +180 -0
  285. package/src/page/features/emoji.js +187 -0
  286. package/src/page/features/export-html.js +189 -0
  287. package/src/page/features/export-markdown.js +169 -0
  288. package/src/page/features/import-markdown.js +349 -0
  289. package/src/page/features/media.js +61 -5
  290. package/src/page/features/mentions.js +188 -0
  291. package/src/page/features/registry.js +24 -1
  292. package/src/page/features/table.js +375 -85
  293. package/src/page/features/title-cover.js +331 -0
  294. package/src/page/features/title-icon.js +213 -0
  295. package/src/page/features/toc.js +87 -0
  296. package/src/page/format-command.js +33 -0
  297. package/src/page/format-gate.js +68 -18
  298. package/src/page/format-host.js +71 -0
  299. package/src/page/format-native-commands.js +158 -0
  300. package/src/page/format-native.js +42 -0
  301. package/src/page/image-actions.js +64 -0
  302. package/src/page/import-markdown.js +262 -0
  303. package/src/page/inline.js +32 -8
  304. package/src/page/items.js +141 -11
  305. package/src/page/markdown.js +30 -2
  306. package/src/page/media.js +35 -119
  307. package/src/page/mention-picker.js +99 -0
  308. package/src/page/merge.js +1 -1
  309. package/src/page/placeholder.js +14 -1
  310. package/src/page/projection.js +156 -25
  311. package/src/page/recover.js +10 -12
  312. package/src/page/recovery-ui.js +464 -0
  313. package/src/page/rich/chart.js +63 -7
  314. package/src/page/save-projection.js +67 -0
  315. package/src/page/save-states.js +14 -4
  316. package/src/page/schema.js +52 -3
  317. package/src/page/table-paste.js +3 -1
  318. package/src/page/table-style.js +4 -4
  319. package/src/page/table.js +44 -2
  320. package/src/page/themes/content.css +25 -1
  321. package/src/page/themes/light-1.css +33 -8
  322. package/src/page/themes/light-1.source.css +8 -7
  323. package/src/page/toc-link.js +31 -0
  324. package/src/page/toc.js +5 -0
  325. package/src/page/view-hooks.js +58 -0
  326. package/src/page/view.js +13 -0
  327. package/src/page/ybind/binding.js +116 -6
  328. package/src/page/ybind/bridge.js +187 -20
  329. package/src/page/ybind/containment.js +162 -0
  330. package/src/page/ybind/doc.js +17 -9
  331. package/src/page/ybind/file-debt.js +63 -0
  332. package/src/page/ybind/layout.js +7 -2
  333. package/src/page/ybind/marks.js +2 -1
  334. package/src/page/ybind/moves.js +5 -4
  335. package/src/page/ybind/origins.js +106 -4
  336. package/src/page/ybind/parts.js +8 -1
  337. package/src/page/ybind/provider.js +125 -18
  338. package/src/page/ybind/recovery-proof.js +32 -9
  339. package/src/page/ybind/recovery-replay.js +905 -4
  340. package/src/page/ybind/recovery.js +174 -45
  341. package/src/page/ybind/table.js +67 -11
  342. package/src/page/ybind/walk-delta.js +258 -0
  343. package/src/page/ybind/walk.js +54 -29
  344. package/src/page/ybind/writer.js +1 -1
  345. package/src/plugins/ai-edit.js +13 -9
  346. package/src/plugins/indicator-chip.js +91 -0
  347. package/src/plugins/indicator.js +13 -80
  348. package/src/plugins/live.js +126 -15
  349. package/src/plugins/menu/actions.js +196 -0
  350. package/src/plugins/menu/people.js +279 -0
  351. package/src/plugins/menu/placement.js +121 -0
  352. package/src/plugins/menu/preferences.js +9 -0
  353. package/src/plugins/menu/seo.js +316 -397
  354. package/src/plugins/menu/style.js +171 -0
  355. package/src/plugins/menu/variants.js +296 -0
  356. package/src/plugins/menu.js +186 -183
  357. package/src/plugins/page-view.js +3 -2
  358. package/src/plugins/page.js +362 -43
  359. package/src/plugins/sortable.js +8 -1
  360. package/src/plugins/source.js +6 -0
  361. package/src/plugins/upgrade.js +51 -17
  362. package/src/plugins/upload-ui.js +12 -17
  363. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  364. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  365. package/src/plugins/visitor-drafts/intro.js +7 -4
  366. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  367. package/src/plugins/visitor-drafts/prompt.js +11 -9
  368. package/src/plugins/visitor-drafts/source.js +4 -3
  369. package/src/plugins/visitor-drafts/store.js +138 -9
  370. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  371. package/src/plugins/visitor-drafts-owner.js +4 -6
  372. package/src/plugins/visitor-drafts.js +5 -3
  373. package/src/recovery/store.js +1 -1
  374. package/src/route/route.js +4 -0
  375. package/src/server/checkpoint/index.js +2 -2
  376. package/src/server/rooms/index.js +5 -1
  377. package/src/server/rooms/live-data-rooms.js +302 -50
  378. package/src/server/rooms/page-compaction-worker.js +12 -0
  379. package/src/server/rooms/page-compactor.js +92 -0
  380. package/src/server/rooms/page-room-conformance.js +461 -0
  381. package/src/source/parse-steps.js +94 -0
  382. package/src/source/session.js +233 -57
  383. package/src/sync/editing-sender.js +12 -0
  384. package/src/sync/live-sync-edit.js +22 -2
  385. package/src/sync/live-sync.js +150 -23
  386. package/src/sync/page-relay.js +52 -0
  387. package/src/sync/presence-store.js +44 -4
  388. package/src/sync/presence.js +6 -3
  389. package/src/sync/saved-stream.js +28 -14
  390. package/src/ui/bevel-controls.js +1 -0
  391. package/src/ui/bevel-dialog.js +5 -2
  392. package/src/ui/editor-ui.js +77 -0
  393. package/src/ui/page-colour.js +281 -0
  394. package/src/ui/toast.js +16 -7
  395. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  396. package/src/vendor/hyper-undo.vendor.js +1 -1
  397. package/src/vendor/hypercms.vendor.js +9 -9
  398. package/src/vendor/parse5.vendor.js +2 -2
  399. package/src/vendor/sap-core.vendor.js +5 -5
  400. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
  401. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
  402. package/dist/m/c-2M73252O.js +0 -1
  403. package/dist/m/c-2SPVQSHC.js +0 -1
  404. package/dist/m/c-35NBGMZB.js +0 -1
  405. package/dist/m/c-3IQRECYS.js +0 -1
  406. package/dist/m/c-4B6H4G5V.js +0 -1
  407. package/dist/m/c-4FFHZM32.js +0 -1
  408. package/dist/m/c-4YSCRICO.js +0 -1
  409. package/dist/m/c-54RCDCBU.js +0 -1
  410. package/dist/m/c-5M4YEKCX.js +0 -4
  411. package/dist/m/c-5V3MOVJT.js +0 -1
  412. package/dist/m/c-66O32MV3.js +0 -28
  413. package/dist/m/c-6HMB24EV.js +0 -1
  414. package/dist/m/c-7O6AFR65.js +0 -1
  415. package/dist/m/c-ANQTJ3B5.js +0 -2
  416. package/dist/m/c-AYVOREBE.js +0 -1
  417. package/dist/m/c-B2ZXPOS3.js +0 -3
  418. package/dist/m/c-BMIZKMPV.js +0 -1
  419. package/dist/m/c-BTDI4Y23.js +0 -1
  420. package/dist/m/c-CJZSLMVQ.js +0 -1
  421. package/dist/m/c-DBE6ORSI.js +0 -1
  422. package/dist/m/c-EDHUR4MX.js +0 -1
  423. package/dist/m/c-EY6UOM54.js +0 -1
  424. package/dist/m/c-F3CKJTJS.js +0 -1
  425. package/dist/m/c-F7Y6FLPY.js +0 -1
  426. package/dist/m/c-FA4DZVTC.js +0 -1
  427. package/dist/m/c-G2BUZITR.js +0 -1
  428. package/dist/m/c-GB3JTV6E.js +0 -1
  429. package/dist/m/c-GBGWXTX4.js +0 -1
  430. package/dist/m/c-GHBCCVG4.js +0 -1
  431. package/dist/m/c-GJAEGGXY.js +0 -1
  432. package/dist/m/c-GOWBDM6J.js +0 -1
  433. package/dist/m/c-IB52R237.js +0 -1
  434. package/dist/m/c-IKWNE4R4.js +0 -1
  435. package/dist/m/c-IU3IYEU6.js +0 -1
  436. package/dist/m/c-IZ2EBRAJ.js +0 -1
  437. package/dist/m/c-KR4UW4KW.js +0 -1
  438. package/dist/m/c-KY3VJGUA.js +0 -8
  439. package/dist/m/c-LFPHCXY3.js +0 -1
  440. package/dist/m/c-LRPUO2MX.js +0 -1
  441. package/dist/m/c-LXENEBV7.js +0 -1
  442. package/dist/m/c-NMJRXKPC.js +0 -8
  443. package/dist/m/c-OIBUTDXI.js +0 -1
  444. package/dist/m/c-PPOSCAQD.js +0 -1
  445. package/dist/m/c-QIX5BJYN.js +0 -1
  446. package/dist/m/c-QQHEX6HA.js +0 -1
  447. package/dist/m/c-R7RMANJI.js +0 -9
  448. package/dist/m/c-RI4NT75Y.js +0 -1
  449. package/dist/m/c-RM7J7BV2.js +0 -1
  450. package/dist/m/c-RUJKSUQW.js +0 -1
  451. package/dist/m/c-SGCCC4FW.js +0 -25
  452. package/dist/m/c-T4VO4OCB.js +0 -1
  453. package/dist/m/c-TQEMVJCS.js +0 -1
  454. package/dist/m/c-TR6RMDQ7.js +0 -3
  455. package/dist/m/c-UKHTLWUW.js +0 -1
  456. package/dist/m/c-UNN7LWCT.js +0 -1
  457. package/dist/m/c-UXBMRDZB.js +0 -235
  458. package/dist/m/c-V7PZZRG7.js +0 -1
  459. package/dist/m/c-VL52ADDR.js +0 -1
  460. package/dist/m/c-WNYBLIOZ.js +0 -1
  461. package/dist/m/c-WWDBYXLK.js +0 -1
  462. package/dist/m/c-XNNW2CWK.js +0 -1
  463. package/dist/m/c-Y6USGTS5.js +0 -5
  464. package/dist/m/c-YG2NJYS6.js +0 -135
  465. package/dist/m/c-Z4VRC7GU.js +0 -7
  466. package/dist/m/c-Z6K52OLP.js +0 -19
  467. package/dist/m/c-ZDY2MDAA.js +0 -21
  468. package/dist/m/c-ZUCOKOHT.js +0 -1
  469. package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
  470. package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
  471. package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
  472. package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
  473. package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
  474. package/dist/m/core/recovery-TBBSICE2.js +0 -1
  475. package/dist/m/core/save-WUCHTQBE.js +0 -1
  476. package/dist/m/core/save-core-FJVHT5M3.js +0 -1
  477. package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
  478. package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
  479. package/dist/m/events/index-23DPTUT4.js +0 -1
  480. package/dist/m/internals/index-WOVAP3RU.js +0 -1
  481. package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
  482. package/dist/m/loader-HXX6YNH5.js +0 -1
  483. package/dist/m/manifest-F22DEDAC.js +0 -1
  484. package/dist/m/manifest-edit-C3C7376B.js +0 -1
  485. package/dist/m/options/options-2LH76JQ3.js +0 -1
  486. package/dist/m/page/editor-XUTB2HB2.js +0 -189
  487. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +0 -6
  488. package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
  489. package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
  490. package/dist/m/plugins/live-NUTTXSBS.js +0 -1
  491. package/dist/m/plugins/menu-HCKO3DSP.js +0 -138
  492. package/dist/m/plugins/page-KSUH7G6J.js +0 -2
  493. package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
  494. package/dist/m/plugins/people-AJJ32YCG.js +0 -1
  495. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  496. package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
  497. package/dist/m/plugins/source-KHK5UUSK.js +0 -1
  498. package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
  499. package/dist/m/plugins/upgrade-ZNYODP5Y.js +0 -1
  500. package/dist/m/plugins/upload-XSGNILUY.js +0 -1
  501. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
  502. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
  503. package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
  504. package/dist/m/reader-core-ILWLXVMP.js +0 -68
  505. package/dist/m/route/route-KOCZSEGN.js +0 -1
  506. package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
  507. package/dist/m/ui/index-EVHKMKBL.js +0 -1
  508. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  509. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  510. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  511. /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -1,15 +1,15 @@
1
- import { FEATURES, featureForType } from "../page/features/registry.js";
1
+ import { FEATURES, featureForNode, featureForType } from "../page/features/registry.js";
2
2
  import { mergeEngine } from "../sync/merge-engine.js";
3
3
  import { registerUnsavedState } from "../lib/unsaved-state.js";
4
4
  import { hostMeta } from "../core/host-meta.js";
5
- import { savePageForce, holdAllSaves, releaseAllSaves, suspendAutosave, resumeAutosave, getLastSavedBytes } from "../core/save.js";
6
- import { setSaveHeader } from "../core/save-core.js";
5
+ import { savePageForce, holdAllSaves, releaseAllSaves, suspendAutosave, resumeAutosave, getLastSavedBytes, savesAdmitted } from "../core/save.js";
6
+ import { setSaveHeader, onSaveAccepted } from "../core/save-core.js";
7
7
  import { lastSeenEtag, representedEtag, recordEtag } from "../core/etag.js";
8
8
  import { captureForSaveAndComparison } from "../core/snapshot.js";
9
- import { storeOf, stageStore, closeStore, checkpointWithin, requestCheckpoint, fastLane, pageLaneReady, pageWriterReady } from "./live.js";
9
+ import { storeOf, stageStore, closeStore, checkpointWithin, requestCheckpoint, requestExitCheckpoint, fastLane, localLane, lanePending, pageLaneReady, pageWriterReady, registerPageCheckpoints } from "./live.js";
10
10
  import { serializeLive } from "../live/ops.js";
11
11
  import { parseJsonRelaxed } from "../vendor/hyper-morph-json.vendor.js";
12
- import { saveStateFor, BLOCKED_TEXT, RETRY_TEXT, downloadPageCopy, downloadSnapshot, copyPageText } from "../page/save-states.js";
12
+ import { saveStateFor, pageStateFor, BLOCKED_TEXT, RETRY_TEXT, downloadPageCopy, downloadSnapshot, copyPageText } from "../page/save-states.js";
13
13
  import toast, { toastPersistent, dismissPersistent } from "../ui/toast-lazy.js";
14
14
  import { claim } from "../page/runtime-owner.js";
15
15
  import { authorIdentity } from "../page/ybind/author.js";
@@ -22,11 +22,14 @@ import { style } from "../lib/hostile-css.js";
22
22
  const ARTICLE_SEL = "article[data-page]";
23
23
  const TAG_SEL = 'script[type="application/json"][merge="page"][clay~="live"]';
24
24
  const BLOCKED = { state: "blocked", code: "unsupported-host", message: "This host does not support page editing" };
25
+ const STORE_TEXT = "Changes cannot be stored on this device. Download a copy to keep them.";
25
26
  const RETRY_BOUND_MS = 45000;
26
27
  const GENERIC_ERROR = "Couldn't save";
27
28
  const GENERIC_OFFLINE = "Offline, not saved";
28
29
  const REPLACED_TEXT = "This page was changed outside the editor. Reload to see it.";
29
30
  const TOO_LARGE_TEXT = "That change is too large to share, so it was not saved. Reload the page to keep editing.";
31
+ const CLIENT_KEY = /^[ur][0-9a-z]+(-[0-9a-z]+){2}$/;
32
+ export const PAGE_FULL_MESSAGE = "This document is full, though your changes are still here to download. To edit again, reload this tab after another editor, or the owner in another browser, deletes content.";
30
33
  const OVERWRITTEN_TEXT = "Some changes you made were replaced by a change made outside the editor.";
31
34
  const REPLACED_HOLD = "The page was changed outside the editor; reload to see it.";
32
35
  const LOADING_MS = 150;
@@ -72,7 +75,7 @@ function pageCodeReady() {
72
75
  function docFeatureIds(doc) {
73
76
  const ids = new Set();
74
77
  doc.forEach((node) => {
75
- const id = featureForType(node.type.name);
78
+ const id = featureForNode(node);
76
79
  if (id) ids.add(id);
77
80
  });
78
81
  return [...ids];
@@ -129,7 +132,7 @@ function scrollToHashBlock(view, loc = globalThis.location) {
129
132
  globalThis.requestAnimationFrame?.(() => view.nodeDOM(found)?.scrollIntoView?.({ block: "start" }));
130
133
  }
131
134
 
132
- function dispatchState(state, code, message) {
135
+ function dispatchState(state, code = null, message = "") {
133
136
  document.dispatchEvent(new CustomEvent("clay:page-save-state", { detail: { state, code, message } }));
134
137
  }
135
138
 
@@ -160,11 +163,24 @@ function armRetry(inst, code, message) {
160
163
  }, RETRY_BOUND_MS);
161
164
  }
162
165
 
163
- function showBlocked(inst, code, message) {
166
+ // The persistent save-state toasts a session raises, taken down together: at the page's
167
+ // own teardown the fade plays, and at a session's end, where nobody is left to read it,
168
+ // the nodes go now.
169
+ function dismissSaveStateToasts(inst, now = false) {
170
+ if (inst.retryTimer) {
171
+ clearTimeout(inst.retryTimer);
172
+ inst.retryTimer = null;
173
+ }
174
+ dismissPersistent(RETRY_TEXT, { now });
175
+ dismissPersistent(BLOCKED_TEXT, { now });
176
+ dismissPersistent(STORE_TEXT, { now });
177
+ }
178
+
179
+ function showBlocked(inst, code, message, state = "blocked") {
164
180
  clearRetry(inst);
165
181
  dismissGeneric();
166
- dispatchState("blocked", code, message);
167
- toastPersistent(BLOCKED_TEXT, "error", {
182
+ dispatchState(state, code, message);
183
+ toastPersistent(state === "unavailable" ? STORE_TEXT : BLOCKED_TEXT, "error", {
168
184
  detail: message,
169
185
  actions: [
170
186
  { label: "Download a copy", onClick: downloadPageCopy },
@@ -176,6 +192,7 @@ function showBlocked(inst, code, message) {
176
192
  function showSaved(inst) {
177
193
  clearRetry(inst);
178
194
  dismissPersistent(BLOCKED_TEXT);
195
+ dismissPersistent(STORE_TEXT);
179
196
  dispatchState("saved", null, "");
180
197
  }
181
198
 
@@ -218,7 +235,7 @@ function stopEditing(inst) {
218
235
  // room refused it): the saved document with the article rendered from the tab's document.
219
236
  function downloadTabCopy(inst) {
220
237
  if (!inst.tabValue) return downloadPageCopy();
221
- const { forSave } = captureForSaveAndComparison();
238
+ const { forSave } = captureForSaveAndComparison({ emitForSync: false });
222
239
  const doc = new DOMParser().parseFromString(forSave, "text/html");
223
240
  const articles = doc.querySelectorAll(ARTICLE_SEL);
224
241
  if (articles.length !== 1) return downloadPageCopy();
@@ -229,20 +246,23 @@ function downloadTabCopy(inst) {
229
246
  return downloadSnapshot(`<!DOCTYPE html>\n${doc.documentElement.outerHTML}`);
230
247
  }
231
248
 
232
- function stopWithNotice(inst, text) {
249
+ function stopWithNotice(inst, text, { reload = true } = {}) {
233
250
  if (inst.replaced) return;
234
251
  inst.replaced = true;
235
252
  stopEditing(inst);
253
+ const download = { label: "Download your copy", onClick: () => downloadTabCopy(inst) };
236
254
  toastPersistent(text, "warning", {
237
- actions: [
238
- { label: "Reload", onClick: () => window.location.reload() },
239
- { label: "Download your copy", onClick: () => downloadTabCopy(inst) },
240
- ],
255
+ actions: reload ? [{ label: "Reload", onClick: () => window.location.reload() }, download] : [download],
241
256
  });
242
257
  }
243
258
 
244
259
  const showReplaced = (inst) => stopWithNotice(inst, REPLACED_TEXT);
245
260
  const showTooLarge = (inst) => stopWithNotice(inst, TOO_LARGE_TEXT);
261
+ const showFull = (inst, code) => {
262
+ dispatchState("blocked", code, PAGE_FULL_MESSAGE);
263
+ // A reload replays the refused edit and is refused again, so the notice offers only the download.
264
+ stopWithNotice(inst, PAGE_FULL_MESSAGE, { reload: false });
265
+ };
246
266
 
247
267
  function applySaveState(inst, state, code, message) {
248
268
  if (state === "saved") showSaved(inst);
@@ -290,8 +310,7 @@ function listenForSaves(inst) {
290
310
  });
291
311
  }
292
312
 
293
- // The page the host stores now, read once: the article a first write is offered to
294
- // again, and the version it is offered against.
313
+ // The page the host stores now, including any merge state it keeps beside the file.
295
314
  async function readStoredArticle() {
296
315
  const response = await fetch(new URL(globalThis.location.href).href, { cache: "no-store" });
297
316
  if (!response?.ok) return null;
@@ -299,12 +318,17 @@ async function readStoredArticle() {
299
318
  const doc = new DOMParser().parseFromString(html, "text/html");
300
319
  const article = doc.querySelector(ARTICLE_SEL);
301
320
  if (!article) return null;
302
- return { article, etag: response.headers?.get?.("etag") || null };
321
+ const tag = doc.querySelector(TAG_SEL);
322
+ // The served root attribute names the stored document (spec §4). A host may add a weak
323
+ // HTTP ETag of its own, which names the response, never the document.
324
+ const header = response.headers?.get?.("etag") || null;
325
+ const etag = doc.documentElement.getAttribute("documentetag") || (header && !/^W\//.test(header) ? header : null);
326
+ return { article, value: tag ? parseJsonRelaxed(tag.textContent) : null, etag };
303
327
  }
304
328
 
305
329
  // A refused first write: the stored document has moved on, or it does not descend
306
- // from the seed this tab staged. The stored document is read once, the seed is rebuilt
307
- // from its article, this tab's own pending edit replays over it, and the write goes
330
+ // from the seed this tab staged. The stored document is read once, its merge state is
331
+ // kept (or seeded from the article), this tab's pending edit replays over it, and the write goes
308
332
  // out once more against the version just read. A second refusal keeps the text in the
309
333
  // tab and shows the conflict, exactly as a lone refusal does.
310
334
  async function retryFirstWrite(inst, event) {
@@ -319,11 +343,11 @@ async function retryFirstWrite(inst, event) {
319
343
  if (!found || !isCurrent(inst)) return;
320
344
  const page = await pageCodeReady();
321
345
  if (!isCurrent(inst)) return;
322
- const seed = page.seedPageFromArticle(found.article.cloneNode(true));
346
+ const seed = found.value ?? page.seedPageFromArticle(found.article.cloneNode(true));
323
347
  // This tab's next write is conditional on the version it has just read.
324
348
  const at = found.etag ?? event?.detail?.etag ?? null;
325
349
  if (at) recordEtag(at);
326
- // The tab's own edit replays over the seed the stored article states; an op the
350
+ // The tab's own edit replays over the stored page state; an op the
327
351
  // stored article cannot hold is dropped by the store, never written over it.
328
352
  inst.store.rebase(seed, { source: "state" });
329
353
  await savePageForce();
@@ -410,14 +434,10 @@ function dispose() {
410
434
  const inst = active;
411
435
  active = null;
412
436
  setSaveHeader("Page-Write", null);
437
+ registerPageCheckpoints(null);
413
438
  if (!inst) return;
414
439
  disposeInstall(inst);
415
- if (inst.retryTimer) {
416
- clearTimeout(inst.retryTimer);
417
- inst.retryTimer = null;
418
- }
419
- dismissPersistent(RETRY_TEXT);
420
- dismissPersistent(BLOCKED_TEXT);
440
+ dismissSaveStateToasts(inst);
421
441
  dismissPersistent(REPLACED_TEXT);
422
442
  for (const message of inst.notices?.values() ?? []) dismissPersistent(message);
423
443
  if (inst.savesHeld) {
@@ -438,6 +458,129 @@ function dispose() {
438
458
  restoreArticle(inst);
439
459
  }
440
460
 
461
+ // The page's journal, its replay and the proof that retires it, all imported the first
462
+ // time a page can actually journal: a reader, a page without a pinned identity and a host
463
+ // with no recovery service pay for none of it.
464
+ let recoveryModules = null;
465
+
466
+ function pageRecovery() {
467
+ if (!recoveryModules) {
468
+ recoveryModules = Promise.all([
469
+ import("../page/ybind/recovery-replay.js"),
470
+ import("../page/checkpoint-proof.js"),
471
+ import("../page/recovery-ui.js"),
472
+ ])
473
+ .then(([replay, proof, ui]) => ({ replay, proof, ui }))
474
+ .catch((error) => {
475
+ console.warn("clayjs: page journal could not load", error);
476
+ return null;
477
+ });
478
+ }
479
+ return recoveryModules;
480
+ }
481
+
482
+ // What this session journals under, or null when this page cannot pin an identity or this
483
+ // host has no recovery service: the document is then edited with no local copy, as before.
484
+ async function openJournal(meta, store) {
485
+ const service = globalThis.window?.clay?.recovery ?? null;
486
+ if (!service || !store) return null;
487
+ const modules = await pageRecovery();
488
+ if (!modules) return null;
489
+ try {
490
+ return modules.replay.openPageJournal({
491
+ meta,
492
+ service,
493
+ base: modules.replay.journalBase(store.value),
494
+ });
495
+ } catch (error) {
496
+ console.warn("clayjs: page journal could not open", error);
497
+ return null;
498
+ }
499
+ }
500
+
501
+ // Replay once, after the authority snapshot is adopted and before input starts. A capsule
502
+ // this page may not replay is kept and named for review; nothing of it is applied.
503
+ async function replayJournal(inst) {
504
+ const journal = inst.binding?.journal;
505
+ if (!journal || !inst.store) return { replayed: [], review: [] };
506
+ const modules = await pageRecovery();
507
+ if (!modules) return { replayed: [], review: [] };
508
+ const outcome = await modules.replay.replayInto({ journal, state: inst.store.value, into: inst.binding.ydoc });
509
+ // The recovered page is this tab's page now: it is offered to the room exactly as an
510
+ // edit would be, and it is not one more journal and not one giant undo step.
511
+ installJournalStatus(inst, modules);
512
+ if (outcome.replayed.length) inst.binding.provider.settle();
513
+ if (outcome.review.length) inst.recoveryReview = outcome.review;
514
+ installRecoveryReview(inst, modules.ui)?.update({ rows: modules.replay.describeReview(outcome.review) });
515
+ return outcome;
516
+ }
517
+
518
+ // The review of what this page may not replay, drawn in the editor's own chrome once per
519
+ // session, with the composition shadow that keeps typing which never became a
520
+ // transaction: one object, alive exactly as long as the session that mounted it.
521
+ function installRecoveryReview(inst, ui) {
522
+ if (inst.recoveryPanel) return inst.recoveryPanel;
523
+ const view = inst.view;
524
+ const mount = view?.dom.closest(".page-shell") ?? inst.article;
525
+ if (!mount) return null;
526
+ const review = ui.createRecoveryReview({
527
+ mount,
528
+ getView: () => (inst.view && !inst.view.isDestroyed ? inst.view : null),
529
+ getJournal: () => inst.binding?.journal ?? null,
530
+ isCurrent: () => isCurrent(inst) && inst.inputOpen && !!inst.view && !inst.view.isDestroyed,
531
+ });
532
+ const detach = review.watch(view?.dom ?? null);
533
+ addDisposer(inst, () => {
534
+ detach();
535
+ review.destroy();
536
+ });
537
+ inst.recoveryPanel = review;
538
+ return review;
539
+ }
540
+
541
+ // One accepted save retires the records a proof covers, and no others.
542
+ function installCheckpointProof(inst) {
543
+ registerPageCheckpoints(async ({ capture, state, article, etag }) => {
544
+ const journal = inst.binding?.journal;
545
+ if (!journal) return;
546
+ const modules = await pageRecovery();
547
+ const context = modules ? await journal.context() : null;
548
+ if (!context) return;
549
+ // Where this host serves the page state, if it serves one at all: asked here, because
550
+ // a page that journals nothing never needs the answer.
551
+ const { hostPageStateHref } = await import("../core/host-meta.js");
552
+ const answer = await modules.proof.checkpointAccepted({
553
+ channel: journal.channel,
554
+ records: journal.records(),
555
+ capture,
556
+ accepted: context,
557
+ article,
558
+ state,
559
+ etag,
560
+ href: await hostPageStateHref(),
561
+ });
562
+ if (!answer.ok) return;
563
+ journal.retire(answer.retired);
564
+ inst.pageStatus?.checkpoint();
565
+ await journal.rebase?.(modules.replay.journalBase(answer.proof.state));
566
+ });
567
+ }
568
+
569
+ // Both live in the journal's module, which a page without a journal never loads.
570
+ function installJournalStatus(inst, modules) {
571
+ const journal = inst.binding?.journal;
572
+ if (!journal || inst.pageStatus) return inst.pageStatus ?? null;
573
+ inst.pageStatus = modules.replay.watchJournal({
574
+ journal,
575
+ store: inst.store,
576
+ reduce: pageStateFor,
577
+ onState: (state) => dispatchState(state),
578
+ onUnavailable: (code) => showBlocked(inst, code, "", "unavailable"),
579
+ });
580
+ addDisposer(inst, () => inst.pageStatus?.dispose());
581
+ return inst.pageStatus;
582
+ }
583
+
441
584
  export function createSteps(inst, options) {
442
585
  let meta = null;
443
586
  let binding = null;
@@ -460,6 +603,42 @@ export function createSteps(inst, options) {
460
603
  addDisposer(inst, () => editorReadyResolve?.());
461
604
  addDisposer(inst, () => clearLoadingStatus(inst));
462
605
 
606
+ // The format gate, fetched when a newer-format command first asks for it: neither
607
+ // the gate nor its host adapter is on the editor's ready path. One gate per session,
608
+ // built against this editor installation, and disposed with the editor.
609
+ let formatGate = null;
610
+ let formatGateLoading = null;
611
+ inst.loadFormatGate = () => {
612
+ if (formatGate) return Promise.resolve(formatGate);
613
+ formatGateLoading ??= Promise.all([import("../page/format-gate.js"), import("../page/format-host.js")])
614
+ .then(([{ createFormatGate }, { createFormatHost }]) => {
615
+ const host = createFormatHost();
616
+ formatGate = createFormatGate({
617
+ host,
618
+ supported: host.supported,
619
+ installed: () => identity?.token ?? null,
620
+ revision: () => null,
621
+ // A floor rise moves this tab to the lane scope of the format it now
622
+ // writes, so a tab that cannot read that format never shares it. The
623
+ // transport is this tab's to create, on its own schedule, so the gate
624
+ // asks for it by the move rather than by the object.
625
+ lane: {
626
+ switchScope: (next, options) => {
627
+ const transport = localLane();
628
+ if (!transport) return { status: "refused", code: "lane-unavailable" };
629
+ return transport.switchScope(next, { ...options, covered: Boolean(options?.covered) || !lanePending() });
630
+ },
631
+ },
632
+ });
633
+ return formatGate;
634
+ })
635
+ .catch((error) => {
636
+ formatGateLoading = null;
637
+ throw error;
638
+ });
639
+ return formatGateLoading;
640
+ };
641
+
463
642
  // A document with a tool block, at boot or when one arrives, opens the store its
464
643
  // tools keep state in and the host that mounts them. A document with no tool
465
644
  // never waits for either.
@@ -511,6 +690,81 @@ export function createSteps(inst, options) {
511
690
  });
512
691
  return toolsOpening;
513
692
  };
693
+
694
+ // The document's own switches travel with it: the gear panel writes them into the
695
+ // page's live store, and a document that never touched one opens no store at all.
696
+ // Turning a switch off hides a tag; the owner and due values stay in the file.
697
+ const SETTINGS_SEL = 'script[type="application/json"][merge="page.settings"][clay~="live"]';
698
+ const SETTING_KEYS = ["assignees", "dates"];
699
+ let settingsOpening = null;
700
+ let settingsValue = { assignees: true, dates: true };
701
+
702
+ // Both switches default to on, and anything but an explicit false leaves them on.
703
+ const settingsFrom = (value) => ({
704
+ assignees: value?.assignees !== false,
705
+ dates: value?.dates !== false,
706
+ });
707
+
708
+ function readSettings() {
709
+ const tag = document.querySelector(SETTINGS_SEL);
710
+ if (!tag) return settingsFrom(null);
711
+ let value = null;
712
+ try { value = JSON.parse(tag.textContent); } catch (error) { value = null; }
713
+ return settingsFrom(value);
714
+ }
715
+
716
+ function applySettings(next) {
717
+ settingsValue = settingsFrom(next);
718
+ const dom = inst.view && !inst.view.isDestroyed ? inst.view.dom : null;
719
+ if (!dom) return;
720
+ for (const key of SETTING_KEYS) {
721
+ if (settingsValue[key]) dom.removeAttribute(`data-${key}`);
722
+ else dom.setAttribute(`data-${key}`, "false");
723
+ }
724
+ }
725
+
726
+ const openSettings = () => {
727
+ if (settingsOpening) return settingsOpening;
728
+ settingsOpening = (async () => {
729
+ if (!document.querySelector(SETTINGS_SEL)) {
730
+ const tag = document.createElement("script");
731
+ tag.setAttribute("type", "application/json");
732
+ tag.setAttribute("merge", "page.settings");
733
+ tag.setAttribute("clay", "live");
734
+ tag.textContent = "{}";
735
+ document.head.appendChild(tag);
736
+ }
737
+ let handle = null;
738
+ try {
739
+ handle = window.clay.live("page.settings");
740
+ await handle.ready;
741
+ } catch (error) {
742
+ console.warn("clayjs: page plugin could not open the settings store", error);
743
+ settingsOpening = null;
744
+ return null;
745
+ }
746
+ if (!isCurrent(inst)) return null;
747
+ applySettings(handle.data);
748
+ // A peer's change hides or shows the tags here too, without the view remounting.
749
+ handle.on("change", () => applySettings(handle.data));
750
+ return handle;
751
+ })();
752
+ return settingsOpening;
753
+ };
754
+
755
+ // The gear panel's API: what the document shows now, and a switch that writes the
756
+ // document's store and takes effect here at once, without waiting for the change.
757
+ inst.settings = {
758
+ read: () => ({ ...settingsValue }),
759
+ set: async (key, on) => {
760
+ if (!SETTING_KEYS.includes(key)) throw new TypeError(`clayjs: "${key}" is not a page setting`);
761
+ const handle = await openSettings();
762
+ if (!handle) throw new Error("clayjs: the page settings store could not be opened");
763
+ const next = { ...settingsFrom(handle.data), [key]: on !== false };
764
+ handle.set([key], next[key]);
765
+ applySettings(next);
766
+ },
767
+ };
514
768
  // One notice per feature a person needed and that failed, until Retry succeeds.
515
769
  const notices = new Map();
516
770
  const notifyFailed = (id) => {
@@ -588,9 +842,29 @@ export function createSteps(inst, options) {
588
842
  const code = await pageCodeReady();
589
843
  if (!isCurrent(inst)) return { ok: false, cancelled: true };
590
844
  if (seeded) {
591
- // The seed stays a value in memory: no tag and no write until someone edits.
592
845
  try {
593
- value = source.seedValue = code.seedPageFromArticle(source.article.cloneNode(true));
846
+ const meta = await hostMeta();
847
+ const href = meta?.extensions?.includes("page-recovery-v1") && meta.document?.pageState?.href;
848
+ const etag = inst.context?.represented ? inst.context.represented.etag : representedEtag() ?? lastSeenEtag();
849
+ if (href && etag) {
850
+ const { readPageState } = await import("../page/checkpoint-proof.js");
851
+ const stored = await readPageState({ href, etag });
852
+ if (!isCurrent(inst)) return { ok: false, cancelled: true };
853
+ if (stored.ok) {
854
+ if (stored.etag !== etag) return { ok: false, reason: "etag-mismatch" };
855
+ value = parseJsonRelaxed(stored.tag);
856
+ source.persisted = true;
857
+ } else if (stored.code !== "no-page-state") {
858
+ return { ok: false, reason: stored.code };
859
+ }
860
+ } else if (href) {
861
+ const stored = await readStoredArticle();
862
+ if (!isCurrent(inst)) return { ok: false, cancelled: true };
863
+ if (!stored || !stored.article.isEqualNode(source.article)) return { ok: false, reason: "stale-document" };
864
+ value = stored.value;
865
+ source.persisted = value !== null;
866
+ }
867
+ value = source.seedValue = value ?? code.seedPageFromArticle(source.article.cloneNode(true));
594
868
  } catch (error) {
595
869
  console.warn("clayjs: page plugin could not rebuild the page it found, leaving the page as saved HTML", error);
596
870
  return { ok: false, reason: "invalid-page-source" };
@@ -714,6 +988,7 @@ function mergeRepresentedPage(store, value) {
714
988
  const staged = stageStore("page", {
715
989
  value: source.seedValue,
716
990
  represented: options.represented ?? { bytes: source.article.outerHTML, etag: representedEtag() ?? lastSeenEtag() },
991
+ persisted: source.persisted,
717
992
  });
718
993
  inst.staged = staged;
719
994
  handle = staged.handle;
@@ -762,6 +1037,11 @@ function mergeRepresentedPage(store, value) {
762
1037
  return result;
763
1038
  };
764
1039
  identity = authorIdentity();
1040
+ // The journal is installed before the editor is enabled, and whether or not this tab
1041
+ // can reach the room: a refused offer, a lost room and an oversize update all leave
1042
+ // the work in a document that a crash must still be able to recover.
1043
+ const journal = await openJournal(meta, store);
1044
+ if (!isCurrent(inst)) return { ok: false, cancelled: true };
765
1045
  binding = code.createYPageBinding({
766
1046
  store,
767
1047
  onStopped: (reason) => {
@@ -769,6 +1049,8 @@ function mergeRepresentedPage(store, value) {
769
1049
  if (reason === "lineage") {
770
1050
  fastLane()?.abandon?.("page");
771
1051
  showReplaced(inst);
1052
+ } else if (reason === "page-full" || reason === "compaction-failed") {
1053
+ showFull(inst, reason);
772
1054
  } else {
773
1055
  showTooLarge(inst);
774
1056
  }
@@ -777,16 +1059,23 @@ function mergeRepresentedPage(store, value) {
777
1059
  ownedTokens: identity.owned,
778
1060
  checkpointWithin,
779
1061
  requestCheckpoint,
1062
+ requestExitCheckpoint: (options) => requestExitCheckpoint({ name: store.name, ...options }),
1063
+ journal,
1064
+ // An exit holds admission closed, which is the only time this tab's own input
1065
+ // still owing the file holds the leave: outside one, the binding reports what it
1066
+ // always reported.
1067
+ exitClosed: () => !savesAdmitted(),
780
1068
  registerUnsavedState: (def) => {
781
1069
  registered = registerUnsavedState({
782
1070
  id: def.id,
783
1071
  isPending: () => flushFailed || def.isPending(),
784
- flush: () => trackFlush(() => def.flush()),
1072
+ flush: (options) => trackFlush(() => def.flush(options)),
785
1073
  });
786
1074
  return registered;
787
1075
  },
788
1076
  });
789
1077
  inst.binding = binding;
1078
+ if (journal) installCheckpointProof(inst);
790
1079
  inst.flush = (options) => trackFlush(() => binding.flush(options));
791
1080
  listenForSaves(inst);
792
1081
  return { ok: true, store, handle };
@@ -851,6 +1140,7 @@ function mountAndAttach(inst, source, loaded) {
851
1140
  features,
852
1141
  onAiTool: aiRequested() ? (_view, remove) => { openAi({ remove }); } : null,
853
1142
  reviseTool: aiRequested() ? (toolId, rev) => { openAi({ target: { toolId, rev } }); } : null,
1143
+ formatGate: inst.loadFormatGate,
854
1144
  });
855
1145
  } catch (error) {
856
1146
  console.error("clayjs: page plugin could not mount the editor", error);
@@ -862,6 +1152,13 @@ function mountAndAttach(inst, source, loaded) {
862
1152
  }
863
1153
  editorView = view;
864
1154
  inst.view = view;
1155
+ applySettings(readSettings());
1156
+ // A document that states settings opens their store so a peer's change reaches this
1157
+ // tab; one that states none opens it when a switch is first written.
1158
+ if (document.querySelector(SETTINGS_SEL)) openSettings().catch(() => null);
1159
+ else listen(inst, "clay:sync-applied", () => {
1160
+ if (!settingsOpening && document.querySelector(SETTINGS_SEL)) openSettings().catch(() => null);
1161
+ });
865
1162
  const shell = view.dom.closest(".page-shell");
866
1163
  // A shell still waiting for its boot takes no input of any kind: a control inside
867
1164
  // a non-editable editor would otherwise write before the boot says ready. It is
@@ -952,7 +1249,8 @@ function mountAndAttach(inst, source, loaded) {
952
1249
  // lane but no writer yet, this tab claims the room (a redundant set of an
953
1250
  // existing Yjs update: a valid nonempty frame that changes nothing) and then
954
1251
  // waits for the election, so exactly one tab adopts.
955
- const found = code.classifyFile(source.article, source.tags);
1252
+ // A seeded page was built from this article: nothing to adopt.
1253
+ const found = source.seeded ? { kind: "untouched" } : code.classifyFile(source.article, source.tags);
956
1254
  const needsAdoption = found.kind === "adopt";
957
1255
  const awaitWriter = async () => {
958
1256
  const lane = await pageLaneReady();
@@ -961,8 +1259,13 @@ function mountAndAttach(inst, source, loaded) {
961
1259
  if (lane.status.writer === null) {
962
1260
  const y = inst.store.value.y;
963
1261
  const keys = Object.keys(y);
964
- const key = keys.length ? keys[0] : `claim-${binding.ydoc.clientID.toString(36)}`;
965
- const value = keys.length ? y[keys[0]] : code.yValueOf(binding.ydoc).y.seed;
1262
+ // A compacting room admits only whole client keys: an existing one again, or this
1263
+ // tab's document under a fresh one.
1264
+ const me = binding.ydoc.clientID.toString(36);
1265
+ const key = inst.store.room?.compaction?.v === 1
1266
+ ? keys.find((k) => CLIENT_KEY.test(k)) ?? `u${me}-claim-0`
1267
+ : keys.length ? keys[0] : `claim-${me}`;
1268
+ const value = Object.hasOwn(y, key) ? y[key] : code.yValueOf(binding.ydoc).y.seed;
966
1269
  inst.store.handle.set(["y", key], value);
967
1270
  // The session is not admitted until the commit, so its ops stay in the tab: the
968
1271
  // claim alone is framed now, before adoption writes anything, or no election runs.
@@ -985,8 +1288,9 @@ function mountAndAttach(inst, source, loaded) {
985
1288
  return !lane || lane.status.writer === null ? true : lane.isWriter();
986
1289
  },
987
1290
  requestCheckpoint: () => checkpointWithin(0),
988
- snapshot: () => captureForSaveAndComparison().forSave,
1291
+ snapshot: () => captureForSaveAndComparison({ emitForSync: false }).forSave,
989
1292
  mergeTagless: inst.mergeTagless,
1293
+ seeded: source.seeded,
990
1294
  writerReady: awaitWriter,
991
1295
  adopted: (rev) => fastLane()?.adopted?.("page", rev, inst.store.mark),
992
1296
  notice: (counts) => {
@@ -999,7 +1303,10 @@ function mountAndAttach(inst, source, loaded) {
999
1303
  inst.adoption = adoption;
1000
1304
 
1001
1305
  let outcome = await adoption.idle();
1002
- if (!outcome) return { ok: true, outcome: "unchanged" };
1306
+ if (!outcome) {
1307
+ await replayJournal(inst);
1308
+ return { ok: true, outcome: "unchanged" };
1309
+ }
1003
1310
  // Another tab is the room's writer, so this tab's adoption is the one that tab
1004
1311
  // lands: the wait ends when it reaches this tab, or as the timeout a refusal is.
1005
1312
  if (outcome.outcome === "writer-adopts") outcome = await awaitWriterAdoption(inst, found);
@@ -1008,6 +1315,7 @@ function mountAndAttach(inst, source, loaded) {
1008
1315
  // comes back from what the room accepted. A disposed install restores nothing:
1009
1316
  // dispose already did.
1010
1317
  if (!outcome.ok && isCurrent(inst)) abandonEditor(inst);
1318
+ else if (isCurrent(inst)) await replayJournal(inst);
1011
1319
  return outcome;
1012
1320
  }
1013
1321
 
@@ -1070,7 +1378,7 @@ function mountAndAttach(inst, source, loaded) {
1070
1378
  // a `resume` puts the very same session back.
1071
1379
  function closeInput(inst) {
1072
1380
  if (!inst.inputOpen) return false;
1073
- setEditorActive?.(inst.view, false);
1381
+ setEditorActive?.(inst.view, false, { quiesce: true });
1074
1382
  return true;
1075
1383
  }
1076
1384
 
@@ -1123,7 +1431,7 @@ function begin(options = {}, { activate = true, context = null } = {}) {
1123
1431
  options.onPhase?.(result);
1124
1432
  },
1125
1433
  }),
1126
- article: null, view: null, binding: null, adoption: null, source: null, store: null, toolHost: null, aiTool: null, bootHtml: "", bootRev: null, savedRev: null, staged: null, pageStore: null, releaseArticle: null, replaced: false, savesHeld: false, flush: null, listeners: [], retryTimer: null, retryShown: false, statusTimer: null, statusNote: null, notices: null, editorReadyResolve: null, context, inputOpen: false, sessionHeld: false, autosaveSuspended: false, staleRetry: false,
1434
+ article: null, view: null, binding: null, adoption: null, source: null, store: null, toolHost: null, aiTool: null, bootHtml: "", bootRev: null, savedRev: null, staged: null, pageStore: null, releaseArticle: null, replaced: false, savesHeld: false, flush: null, loadFormatGate: null, listeners: [], retryTimer: null, retryShown: false, statusTimer: null, statusNote: null, notices: null, editorReadyResolve: null, context, inputOpen: false, sessionHeld: false, autosaveSuspended: false, staleRetry: false,
1127
1435
  };
1128
1436
  active = inst;
1129
1437
  const boot = run(inst, options, { activate }).catch((error) => {
@@ -1149,10 +1457,7 @@ function endSession(inst) {
1149
1457
  }
1150
1458
  setSaveHeader("Page-Write", null);
1151
1459
  disposeInstall(inst);
1152
- if (inst.retryTimer) {
1153
- clearTimeout(inst.retryTimer);
1154
- inst.retryTimer = null;
1155
- }
1460
+ dismissSaveStateToasts(inst, true);
1156
1461
  inst.titleObserver?.dispose();
1157
1462
  inst.titleObserver = null;
1158
1463
  clearLoadingStatus(inst);
@@ -1285,8 +1590,22 @@ export const page = {
1285
1590
  get view() {
1286
1591
  return active?.view ?? null;
1287
1592
  },
1593
+ // The document's settings, for the panel that switches them.
1594
+ get settings() {
1595
+ return active?.settings ?? null;
1596
+ },
1288
1597
  };
1289
1598
 
1599
+ // The page's accepted bytes are the only thing that clears what this tab owes the file,
1600
+ // and one module-level observer is enough: the hook API has no unsubscribe, so a binding
1601
+ // registers none of its own and the current session's binding answers each save.
1602
+ onSaveAccepted((html) => {
1603
+ const binding = active?.binding;
1604
+ if (!binding?.filePending()) return;
1605
+ const state = pageCode?.acceptedPageState(html);
1606
+ if (state) binding.acceptFile(state);
1607
+ });
1608
+
1290
1609
  // A live entry imports this module while the committed mode is still view: the install
1291
1610
  // belongs to the explicit edit boot alone, and a managed session starts its own.
1292
1611
  page.ready = startsAtImport() ? install() : null;