@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
@@ -149,6 +149,24 @@ export async function hostPageFormat() {
149
149
  return meta.extensions.includes("page-format-v1") ? 1 : 0;
150
150
  }
151
151
 
152
+ /**
153
+ * Where this host serves the page state of one accepted version, or null.
154
+ *
155
+ * Two things have to be true: the host announced `page-recovery-v1` by name, and its
156
+ * document block named the resource as `pageState.href`. A host that has the resource
157
+ * but has not announced the extension is still launching it, and a client that inferred
158
+ * either one from the other would retire journaled work against a pair nobody promised.
159
+ * Absent, empty or not a string is null, which means no conditional read and no proof
160
+ * from one: the records stay.
161
+ *
162
+ * @returns {Promise<?string>}
163
+ */
164
+ export async function hostPageStateHref() {
165
+ const meta = await hostMeta();
166
+ if (!meta.extensions.includes("page-recovery-v1")) return null;
167
+ return meta.document?.pageState?.href || null;
168
+ }
169
+
152
170
  /**
153
171
  * Whether this host keeps a version history a person can go back to.
154
172
  *
@@ -163,6 +181,44 @@ export async function hostVersions() {
163
181
  return meta.document?.versions === true;
164
182
  }
165
183
 
184
+ const HOST_ACTIONS = ["codeEditor", "versions", "duplicate", "reveal", "export"];
185
+
186
+ function httpUrl(value) {
187
+ if (typeof value !== "string" || !value) return null;
188
+ try {
189
+ const url = new URL(value, location.href);
190
+ return url.protocol === "http:" || url.protocol === "https:" ? url : null;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * What the host offers this caller for the document, from the `document.actions` block:
198
+ * `{ codeEditor, versions, duplicate, reveal, export }`, each present only when the host
199
+ * named it. An action is a page to open (`{ href }`, any http(s) URL) or something the
200
+ * host does itself when the page asks (`{ post }`, a same-origin URL). A host without
201
+ * the block offers none of them.
202
+ *
203
+ * @returns {Promise<Record<string, {href: string} | {post: string}>>}
204
+ */
205
+ export async function hostActions() {
206
+ const meta = await hostMeta();
207
+ const block = meta.document?.actions;
208
+ const actions = {};
209
+ if (!block || typeof block !== "object") return actions;
210
+ for (const name of HOST_ACTIONS) {
211
+ const href = httpUrl(block[name]?.href);
212
+ if (href) {
213
+ actions[name] = { href: href.href };
214
+ continue;
215
+ }
216
+ const post = httpUrl(block[name]?.post);
217
+ if (post && post.origin === location.origin) actions[name] = { post: post.href };
218
+ }
219
+ return actions;
220
+ }
221
+
166
222
  /** Drop the memoized answer. Tests only. */
167
223
  export function resetHostMeta() {
168
224
  inFlight = null;
@@ -83,19 +83,35 @@ const hintCanEdit = hostCanEdit || keptCanEdit;
83
83
  const authoredRestriction = isDraftMode || servedStaleToken() || forcedEditMode === false ||
84
84
  query.editmode === "false" || (isViewOnlyDocument && query.editmode !== "true");
85
85
  let canEdit = hintCanEdit;
86
+ // The host's own answer as it stands, true or false, and null while it has said nothing
87
+ // either way. Kept beside canEdit because it is the same answer read for a different
88
+ // question: canEdit is what the page may do, and this is what the host has already said it
89
+ // will refuse.
90
+ let hostWritable = null;
86
91
 
87
92
  // The host's own answer to "may this caller save here": false takes edit away, true grants
88
93
  // it unless the document rules it out, and no answer leaves the hints in charge.
89
94
  export function noteHostWritable(writable) {
95
+ hostWritable = typeof writable === "boolean" ? writable : null;
90
96
  const next = writable === false ? false : writable === true ? hintCanEdit || !authoredRestriction : hintCanEdit;
91
97
  if (next === canEdit) return;
92
98
  canEdit = next;
93
99
  if (typeof document !== "undefined") document.dispatchEvent(new Event("clay:capabilitychange"));
94
100
  }
95
101
 
102
+ // What the host last answered about this page being writable, or null while it has said
103
+ // nothing. A denial is the client's own knowledge that a save will be refused, so the save
104
+ // lane reads it before building a request it already knows the host will not take.
105
+ export function learnedHostWritable() {
106
+ return hostWritable;
107
+ }
108
+
96
109
  // Does this tab start editing? Only when something explicit asks for it.
110
+ // A receipt the page left on its own hide records where the tab happened to be, not a
111
+ // choice, so a tool page that now declares opens-in="edit" still opens editing. A switch
112
+ // the person made leaves an internal receipt, and that one is kept.
97
113
  const startsEditing = receipt
98
- ? receipt.mode === "edit"
114
+ ? receipt.mode === "edit" || (opensInEdit && !receipt.internal)
99
115
  : query.editmode
100
116
  ? query.editmode === "true"
101
117
  : forcedEditMode === true || opensInEdit;
@@ -1,6 +1,7 @@
1
1
  import { canEdit, isDraftMode, mode } from "./is-edit-mode.js";
2
2
  import { setMode } from "./edit-mode.js";
3
3
  import onDomReady from "../lib/dom-ready.js";
4
+ import { PERSIST, resolveRegionPolicy } from "../lib/region-policy.js";
4
5
 
5
6
  const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
6
7
  const chord = mac ? "⌘⇧E" : "Ctrl+Shift+E";
@@ -53,7 +54,16 @@ const NOTE_CSS = `.clay-mode-note {
53
54
  opacity: .65;
54
55
  }
55
56
  .clay-mode-note button:hover { opacity: 1; background: rgba(23, 28, 36, .08); }
57
+ .clay-mode-note .clay-view-notice__edit {
58
+ width: auto;
59
+ padding: 0 10px;
60
+ border: 1px solid rgba(23, 28, 36, .2);
61
+ font-size: 13px;
62
+ font-weight: 600;
63
+ opacity: 1;
64
+ }
56
65
  @media (prefers-color-scheme: dark) {
66
+ .clay-mode-note .clay-view-notice__edit { border-color: rgba(233, 233, 236, .22); }
57
67
  .clay-mode-note {
58
68
  border-color: rgba(233, 233, 236, .16);
59
69
  color: var(--clay-ui-fg, #E9E9EC);
@@ -74,48 +84,144 @@ const NOTE_CSS = `.clay-mode-note {
74
84
  }
75
85
  }`;
76
86
 
77
- function styles() {
78
- if (document.getElementById("clay-mode-note-style")) return;
79
- const style = document.createElement("style");
80
- style.id = "clay-mode-note-style";
81
- style.setAttribute("clay", "editor-ui");
82
- style.textContent = NOTE_CSS;
83
- document.head.append(style);
87
+ // A note mounts in its own shadow root, so the page's CSS cannot restyle it. The helper is
88
+ // imported only when a note shows: this module loads on every page, a note rarely.
89
+ const closedNotes = new WeakSet();
90
+
91
+ function mountNote(content) {
92
+ import("../ui/editor-ui.js").then(({ mountEditorUI }) => {
93
+ if (!closedNotes.has(content)) mountEditorUI({ name: "mode-note", content, css: [NOTE_CSS] });
94
+ });
84
95
  }
85
96
 
86
- function onKey(e) {
87
- if (!(canEdit || mode === "edit")) return;
88
- if (!(mac ? e.metaKey : e.ctrlKey) || !e.shiftKey || e.altKey || e.code !== "KeyE" || e.isComposing) return;
89
- e.preventDefault();
90
- setMode(mode === "edit" ? "view" : "edit").then((r) => {
97
+ function closeNote(content) {
98
+ closedNotes.add(content);
99
+ content.getRootNode().host?.remove();
100
+ }
101
+
102
+ function switchTo(next) {
103
+ return setMode(next).then((r) => {
91
104
  if (r.ok) return;
92
105
  import("./mode-refusal.js").then(({ modeRefusalText }) => {
93
106
  const text = modeRefusalText(r);
94
- if (text) window.clay?.toast?.(text, "error");
107
+ if (!text) return;
108
+ // Templates that load no UI satellite still say why the switch did nothing.
109
+ if (typeof window.clay?.toast === "function") window.clay.toast(text, "error");
110
+ else if (typeof window.clay?.menu?.note === "function") window.clay.menu.note(text);
111
+ else showNote(text);
95
112
  });
96
113
  });
97
114
  }
98
115
 
99
- function showNote() {
100
- styles();
116
+ function onKey(e) {
117
+ if (!(canEdit || mode === "edit")) return;
118
+ if (!(mac ? e.metaKey : e.ctrlKey) || !e.shiftKey || e.altKey || e.code !== "KeyE" || e.isComposing) return;
119
+ e.preventDefault();
120
+ switchTo(mode === "edit" ? "view" : "edit");
121
+ }
122
+
123
+ // A tab in view keeps nothing typed into it, and a page whose own script opens an
124
+ // editor at load (an OverType journal) accepts that typing all day. So for a person
125
+ // who can edit, the first edit to a field that saves in edit mode raises a notice
126
+ // that stays until the tab edits. Fields edit mode would unlock (inert
127
+ // contenteditable, viewmode:readonly, viewmode:disabled) raise it on the attempt.
128
+ const VIEW_NOTICE = "In view mode, so edits won't be saved.";
129
+ const SAVING_FIELD = ":is(input, textarea, select):is([persist], [clay-persist])";
130
+ const INERT_FIELD = "[editmode\\:contenteditable], [viewmode\\:readonly], [viewmode\\:disabled]";
131
+
132
+ let viewNotice = null;
133
+
134
+ function savedContent(el) {
135
+ if (!el.isConnected) return false;
136
+ const policy = resolveRegionPolicy(el);
137
+ return policy.persist === PERSIST.FULL && !policy.extension;
138
+ }
139
+
140
+ function savingField(node) {
141
+ const el = node?.nodeType === 1 ? node : node?.parentElement;
142
+ if (!el) return null;
143
+ const field = el.closest(SAVING_FIELD) || el.closest(INERT_FIELD) || (el.isContentEditable ? el : null);
144
+ return field && savedContent(field) ? field : null;
145
+ }
146
+
147
+ const liveField = (el) =>
148
+ el.isContentEditable || (el.matches("input, textarea, select") && !el.readOnly && !el.disabled);
149
+
150
+ const watching = () => canEdit && mode === "view";
151
+
152
+ function onViewInput(e) {
153
+ if (watching() && savingField(e.target)) showViewNotice();
154
+ }
155
+
156
+ // Bubbles on window, after the page's own handlers, so a single-key shortcut the
157
+ // page handled (and prevented) never reads as an attempt to type.
158
+ function onViewKey(e) {
159
+ if (!watching() || e.defaultPrevented || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
160
+ if (!(e.key.length === 1 && e.key !== " ") && e.key !== "Backspace" && e.key !== "Delete") return;
161
+ const target = e.target;
162
+ if (target?.nodeType !== 1 || liveField(target)) return;
163
+ if (target !== document.body && !target.closest(INERT_FIELD)) return;
164
+ const field = savingField(target === document.body ? document.getSelection()?.anchorNode : target);
165
+ if (field?.matches(INERT_FIELD)) showViewNotice();
166
+ }
167
+
168
+ function onViewPointer(e) {
169
+ if (!watching()) return;
170
+ const field = e.target?.closest?.("[viewmode\\:disabled]");
171
+ if (field && savedContent(field)) showViewNotice();
172
+ }
173
+
174
+ function showViewNotice() {
175
+ if (viewNotice) return;
176
+ console.warn('clayjs: this tab is in view mode, so edits made here are not saved. clay.setMode("edit") switches; a page that must edit as soon as it opens declares <html opens-in="edit">.');
177
+ document.querySelector('clay-editor-ui[data-clay-ui="mode-note"]')?.remove();
178
+ viewNotice = document.createElement("div");
179
+ viewNotice.setAttribute("clay", "editor-ui");
180
+ viewNotice.setAttribute("role", "alert");
181
+ viewNotice.className = "clay-mode-note clay-view-notice";
182
+ viewNotice.innerHTML = '<span></span><button type="button" class="clay-view-notice__edit">Switch to edit</button>';
183
+ viewNotice.querySelector("span").textContent = VIEW_NOTICE;
184
+ viewNotice.querySelector("button").addEventListener("click", () => switchTo("edit"));
185
+ mountNote(viewNotice);
186
+ }
187
+
188
+ function onModeChange() {
189
+ if (mode === "view" || !viewNotice) return;
190
+ closeNote(viewNotice);
191
+ viewNotice = null;
192
+ }
193
+
194
+ function showNote(message) {
101
195
  const note = document.createElement("div");
102
196
  note.setAttribute("clay", "editor-ui");
103
197
  note.setAttribute("role", "status");
104
198
  note.className = "clay-mode-note";
105
199
  note.innerHTML = `<span>This page opens in view mode. Press <kbd>${chord}</kbd> to edit.</span><button type="button" aria-label="Dismiss">×</button>`;
106
- const close = () => note.remove();
200
+ if (message) note.querySelector("span").textContent = message;
201
+ const close = () => closeNote(note);
107
202
  note.querySelector("button").addEventListener("click", close);
108
203
  addEventListener("keydown", (e) => { if (e.key === "Escape") close(); }, { once: true });
109
204
  setTimeout(close, 12000);
110
- document.body.append(note);
205
+ mountNote(note);
111
206
  }
112
207
 
113
208
  if (!isDraftMode) {
114
209
  document.addEventListener("keydown", onKey);
210
+ document.addEventListener("input", onViewInput, true);
211
+ document.addEventListener("pointerdown", onViewPointer, true);
212
+ window.addEventListener("keydown", onViewKey);
213
+ document.addEventListener("clay:modechange", onModeChange);
115
214
  onDomReady(() => {
116
215
  if (!canEdit || mode !== "view" || document.querySelector("[clay-mode-toggle]")) return;
117
- queueMicrotask(() => {
118
- if (window.clay?.menu) return;
216
+ // The note needs clay.storage, which the loader attaches after the core wave.
217
+ const whenReady = (run) => {
218
+ const ready = window.clay?.ready;
219
+ if (window.clay?.storage || typeof ready?.then !== "function") queueMicrotask(run);
220
+ else ready.then(run, () => {});
221
+ };
222
+ whenReady(() => {
223
+ // A menu loads after ready and carries its own way in.
224
+ if (window.clay?.menu || window.clay?.plugins?.includes?.("menu")) return;
119
225
  const store = window.clay?.storage?.local;
120
226
  if (!store || store.get("mode-note")) return;
121
227
  store.set("mode-note", true);
@@ -78,5 +78,10 @@ export const MODULE_SUPPORT = {
78
78
  "plugins/cms.js": "export",
79
79
  };
80
80
 
81
+ export const PAGE_FEATURE_SUPPORT = {
82
+ "page/ybind/recovery.js": { owner: "plugins/page.js", lifecycle: "child" },
83
+ "page/recovery-ui.js": { owner: "plugins/page.js", lifecycle: "child" },
84
+ };
85
+
81
86
  export const supportFor = (path) =>
82
87
  Object.prototype.hasOwnProperty.call(MODULE_SUPPORT, path) ? MODULE_SUPPORT[path] : null;
@@ -65,17 +65,30 @@ export async function runSaveBarrier({ generation = saveGeneration(), keepalive
65
65
  if (!operation && !claimed) return refusal("busy");
66
66
  const deadline = Date.now() + timeoutMs;
67
67
  const permit = openSavePermit(generation);
68
+ const abort = new AbortController();
68
69
  try {
69
70
  for (let pass = 0; ; pass++) {
70
71
  if (generation !== saveGeneration()) return refusal("stale-document");
71
72
  if (Date.now() >= deadline) return refusal("timeout");
72
73
 
73
- // Work that finishes on its own — a checkpoint, a queued upload — is asked to. A
74
- // composer waiting on its person has no flush and stays pending: that is a refusal,
75
- // never a blur or a submit on their behalf.
76
- const sources = await bounded(flushUnsaved(), deadline);
77
- if (sources === EXPIRED) return refusal("timeout");
78
- if (!sources.ok) return refusal("pending-input", { pending: sources.pending });
74
+ // Work that finishes on its own — a checkpoint, a queued upload — is asked to, under
75
+ // this exit's own permit and inside its own deadline: the save such a source needs is
76
+ // the one write a closed session still admits. A composer waiting on its person has no
77
+ // flush and stays pending: that is a refusal, never a blur or a submit on their behalf.
78
+ const context = {
79
+ permit,
80
+ deadline,
81
+ keepalive,
82
+ signal: abort.signal,
83
+ timeoutMs: Math.max(0, deadline - Date.now()),
84
+ };
85
+ const sources = await bounded(flushUnsaved(context), deadline);
86
+ if (sources === EXPIRED) return refusal("timeout", { pending: pendingIds() });
87
+ if (!sources.ok) {
88
+ if (isSaveConflicted()) return refusal("conflict", { pending: sources.pending });
89
+ if (saveFateIsUnknown()) return refusal("save-unknown", { pending: sources.pending });
90
+ return refusal("pending-input", { pending: sources.pending });
91
+ }
79
92
  if (sources.pending.length) return refusal("pending-input", { pending: sources.pending });
80
93
 
81
94
  // Neither a conflict nor an unresolved fate is a failure a retry fixes.
@@ -106,6 +119,7 @@ export async function runSaveBarrier({ generation = saveGeneration(), keepalive
106
119
  return { ok: true, state: result.state, etag: result.etag ?? null };
107
120
  }
108
121
  } finally {
122
+ abort.abort();
109
123
  closeSavePermit(permit);
110
124
  if (claimed) releaseDocumentOperation(claimed);
111
125
  }
@@ -37,10 +37,10 @@ export function authoredAttr(el, name) {
37
37
  // An attribute that was removed and written again lands at the end of the list, and
38
38
  // attribute order is what a saved file compares: an untouched page would come back
39
39
  // with `disabled` moved. Rebuilding the list in the recorded order is the only way to
40
- // hand the file back exactly what it served. Runs only when something actually moved,
41
- // and only on the way out of edit.
42
- export function restoreAuthoredOrder(el) {
43
- const seen = authored.get(el);
40
+ // hand the file back exactly what it served. Runs only when something actually moved, on
41
+ // the way out of edit and on the save clone, whose record belongs to its live original.
42
+ export function restoreAuthoredOrder(el, from = el) {
43
+ const seen = authored.get(from);
44
44
  if (!seen) return;
45
45
  const current = el.getAttributeNames();
46
46
  const desired = [
@@ -42,6 +42,28 @@ function pageWriterId() {
42
42
  return `wr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
43
43
  }
44
44
 
45
+ // Each page holds a lock named for its writer for as long as it lives: a record whose
46
+ // writer still holds one belongs to an open tab that will save it, not to lost work.
47
+ const WRITER_LOCK = "clay-recovery-writer:";
48
+
49
+ function holdWriterLock(writerId) {
50
+ const locks = globalThis.navigator?.locks;
51
+ if (typeof locks?.request !== "function") return;
52
+ locks.request(WRITER_LOCK + writerId, () => new Promise(() => {})).catch(() => {});
53
+ }
54
+
55
+ async function liveWriters(own) {
56
+ const locks = globalThis.navigator?.locks;
57
+ if (typeof locks?.query !== "function") return new Set();
58
+ try {
59
+ const { held = [] } = await locks.query();
60
+ return new Set(held.map((lock) => lock.name).filter((name) => typeof name === "string" && name.startsWith(WRITER_LOCK))
61
+ .map((name) => name.slice(WRITER_LOCK.length)).filter((id) => id !== own));
62
+ } catch {
63
+ return new Set();
64
+ }
65
+ }
66
+
45
67
  function recordKey(id) {
46
68
  return Array.isArray(id) ? id.join("\u0000") : String(id);
47
69
  }
@@ -112,6 +134,34 @@ export function noteRecoveryCapture({ captureId, html } = {}) {
112
134
  for (const service of services) service.capture(captureId, html);
113
135
  }
114
136
 
137
+ /**
138
+ * Freeze what every service holds at this capture's boundary.
139
+ *
140
+ * A save that prepares across tasks cannot read the services after it, so the copies that
141
+ * could be covered are pinned here, synchronously, in the task the snapshot was taken in.
142
+ * `finishRecoveryCapture` then judges exactly those against the bytes the save verifies:
143
+ * a copy written after this capture is neither covered by it nor acknowledged by it.
144
+ *
145
+ * @param {*} captureId
146
+ */
147
+ export function beginRecoveryCapture(captureId) {
148
+ if (!services.size || captureId == null) return;
149
+ for (const service of services) service.pinCapture(captureId);
150
+ }
151
+
152
+ /**
153
+ * Record what this capture's final bytes cover, among the copies pinned at its boundary.
154
+ *
155
+ * Called with the verified bytes, before anything is sent. Nothing is acknowledged here.
156
+ *
157
+ * @param {*} captureId
158
+ * @param {string} html
159
+ */
160
+ export function finishRecoveryCapture(captureId, html) {
161
+ if (!services.size || captureId == null) return;
162
+ for (const service of services) service.coverCapture(captureId, html);
163
+ }
164
+
115
165
  /**
116
166
  * Report how a capture settled.
117
167
  *
@@ -153,10 +203,14 @@ export function createRecovery({
153
203
  const channels = new Map();
154
204
  const listeners = new Set();
155
205
  const captures = new Map();
206
+ // What each preparing capture found held at its boundary, before it could read the
207
+ // final bytes. Cleared however the capture settles.
208
+ const pinned = new Map();
156
209
  let prepared = null;
157
210
  let identity = null;
158
211
  let store = null;
159
212
  let status = { ok: true, code: null, durable: false };
213
+ holdWriterLock(writerId);
160
214
 
161
215
  function snapshot() {
162
216
  return {
@@ -248,6 +302,56 @@ export function createRecovery({
248
302
  return covered;
249
303
  }
250
304
 
305
+ // The copies a capture taken now could cover, without reading its bytes: they do not
306
+ // exist yet, and by the time they do a newer copy may have replaced one of these.
307
+ function pinCapture(captureId) {
308
+ const list = [];
309
+ for (const entry of channels.values()) {
310
+ if (typeof entry.readSaved !== 'function' || !entry.held.size) continue;
311
+ for (const record of entry.held.values()) {
312
+ if (record.kind !== TEXT) continue;
313
+ list.push({
314
+ channel: entry.channel,
315
+ key: record.key,
316
+ id: record.id,
317
+ revision: record.revision,
318
+ payload: record.payload,
319
+ });
320
+ }
321
+ }
322
+ if (list.length) pinned.set(captureId, list);
323
+ }
324
+
325
+ // The same coverage question `capture` asks, over the copies pinned at the capture and
326
+ // the bytes the save finally holds. The saved map is read once per channel, and only
327
+ // for a channel that had something it could cover.
328
+ function coverCapture(captureId, html) {
329
+ const list = pinned.get(captureId);
330
+ pinned.delete(captureId);
331
+ if (!list || typeof html !== 'string') return;
332
+ const saved = new Map();
333
+ const covered = new Map();
334
+ for (const candidate of list) {
335
+ if (!saved.has(candidate.channel)) {
336
+ const entry = channels.get(candidate.channel);
337
+ let read = null;
338
+ try {
339
+ read = typeof entry?.readSaved === 'function' ? entry.readSaved(html) : null;
340
+ } catch (_) {
341
+ read = null;
342
+ }
343
+ saved.set(candidate.channel, read && typeof read.get === 'function' ? read : null);
344
+ }
345
+ const read = saved.get(candidate.channel);
346
+ if (!read || !read.has(candidate.key)) continue;
347
+ if (!payloadsMatch(read.get(candidate.key), candidate.payload)) continue;
348
+ const coveredHere = covered.get(candidate.channel) ?? [];
349
+ coveredHere.push({ id: candidate.id, revision: candidate.revision });
350
+ covered.set(candidate.channel, coveredHere);
351
+ }
352
+ if (covered.size) captures.set(captureId, covered);
353
+ }
354
+
251
355
  function capture(captureId, html) {
252
356
  if (captureId == null || typeof html !== "string") return;
253
357
  const covered = new Map();
@@ -277,6 +381,7 @@ export function createRecovery({
277
381
  // acknowledgement is ordered against every put this service has already queued:
278
382
  // a copy that reaches the database late meets the watermark it was refused by.
279
383
  function settle(captureId, ok, etag) {
384
+ pinned.delete(captureId);
280
385
  const covered = captures.get(captureId);
281
386
  if (!covered) return Promise.resolve();
282
387
  captures.delete(captureId);
@@ -471,7 +576,6 @@ export function createRecovery({
471
576
  entry.batches += 1;
472
577
  entry.batchBytes += record.payload.byteLength;
473
578
  if (receipt.skipped) return { ok: true, id: record.id, revision, skipped: true };
474
- planCompaction(entry);
475
579
  return { ok: true, id: record.id, revision };
476
580
  }
477
581
 
@@ -543,9 +647,9 @@ export function createRecovery({
543
647
  * A swap that lost the head re-reads everything and tries again, so a compaction
544
648
  * that raced an append or another compaction never deletes input it did not merge.
545
649
  */
546
- async function swapBatches(entry, observed) {
650
+ async function swapBatches(entry, observed, rebase = null) {
547
651
  for (let tries = 0; tries < 3; tries++) {
548
- let capsuleId = null;
652
+ let capsuleId = rebase?.capsuleId ?? null;
549
653
  const inputs = [];
550
654
  for (const { id, revision } of observed) {
551
655
  const read = await store.get(id);
@@ -556,7 +660,7 @@ export function createRecovery({
556
660
  if (!sameId(capsuleId, record.capsuleId)) continue;
557
661
  inputs.push(record);
558
662
  }
559
- if (!capsuleId || !inputs.length) return { ok: true, retired: [] };
663
+ if (!capsuleId || !(inputs.length || rebase)) return { ok: true, retired: [] };
560
664
  const head = await store.get(capsuleId);
561
665
  if (!head.ok || !head.record) return { ok: false, code: "missing" };
562
666
  const capsule = head.record;
@@ -567,6 +671,8 @@ export function createRecovery({
567
671
  ].filter(Boolean);
568
672
  const mergedOwned = owned.length ? copyPayload(await entry.merge(owned)) : new Uint8Array(0);
569
673
  const mergedDependencies = dependencies.length ? copyPayload(await entry.merge(dependencies)) : new Uint8Array(0);
674
+ let payload = { base: capsule.payload?.base ?? new Uint8Array(0), dependencies: mergedDependencies, owned: mergedOwned };
675
+ if (rebase) payload = await rebase.rebuild(payload);
570
676
  const revision = Math.max(entry.revision, capsule.revision) + 1;
571
677
  entry.revision = revision;
572
678
  const swapped = await store.compactBinary({
@@ -574,20 +680,11 @@ export function createRecovery({
574
680
  version: entry.version,
575
681
  capsuleId,
576
682
  expectedRevision: capsule.revision,
577
- replacement: {
578
- revision,
579
- updated: now?.() ?? Date.now(),
580
- payload: {
581
- base: capsule.payload?.base ?? new Uint8Array(0),
582
- dependencies: mergedDependencies,
583
- owned: mergedOwned,
584
- },
585
- },
683
+ replacement: { revision, updated: now?.() ?? Date.now(), context: rebase?.context, payload },
586
684
  inputs: inputs.map((record) => ({ id: record.id, revision: record.revision })),
587
685
  });
588
686
  if (swapped.ok) {
589
- entry.batches = 0;
590
- entry.batchBytes = 0;
687
+ if (inputs.length) entry.batches = entry.batchBytes = 0;
591
688
  return { ok: true, retired: swapped.retired, revision: swapped.revision };
592
689
  }
593
690
  if (swapped.code !== "stale") return { ok: false, code: swapped.code };
@@ -607,22 +704,11 @@ export function createRecovery({
607
704
  return receipt.ok ? { ok: true, retired: receipt.retired } : { ok: false, code: receipt.code, retired: [] };
608
705
  }
609
706
 
610
- function planCompaction(entry) {
611
- if (typeof entry.merge !== "function" || entry.compaction) return;
612
- if (entry.batches < COMPACT_BATCHES && entry.batchBytes < COMPACT_BYTES) return;
613
- entry.compaction = (async () => {
614
- const listed = await store.list();
615
- if (!listed.ok) return;
616
- const mine = listed.records.filter(
617
- (record) => record.channel === entry.channel && record.kind === "binary-batch" && record.writerId === writerId
618
- );
619
- if (!mine.length) return;
620
- await compact(entry, { records: mine.map(({ id, revision }) => ({ id, revision })) });
621
- })()
622
- .catch(() => {})
623
- .finally(() => {
624
- entry.compaction = null;
625
- });
707
+ async function rebase(entry, { key, context, rebuild } = {}) {
708
+ const opened = await ensurePrepared();
709
+ const receipt = opened.ok ? await swapBatches(entry, [], { capsuleId: binaryOf(entry, String(key)), context, rebuild }) : opened;
710
+ note(receipt);
711
+ return receipt;
626
712
  }
627
713
 
628
714
  function flush(entry) {
@@ -644,8 +730,9 @@ export function createRecovery({
644
730
  const result = await store.list();
645
731
  note(result);
646
732
  if (!result.ok) return { ok: false, code: result.code, records: [], status: snapshot() };
733
+ const live = await liveWriters(writerId);
647
734
  const records = result.records
648
- .filter((record) => record.channel === entry.channel)
735
+ .filter((record) => record.channel === entry.channel && !live.has(record.writerId))
649
736
  .map((record) => ({ ...record, supported: record.version === entry.version }));
650
737
  return { ok: true, records, status: snapshot() };
651
738
  }
@@ -684,7 +771,7 @@ export function createRecovery({
684
771
  return flushed;
685
772
  }
686
773
 
687
- const handle = { capture, settle };
774
+ const handle = { capture, settle, pinCapture, coverCapture };
688
775
 
689
776
  function channelApi(entry) {
690
777
  return {
@@ -698,6 +785,7 @@ export function createRecovery({
698
785
  markAcknowledged: (spec) => markAcknowledged(entry, spec),
699
786
  checkpoint: (spec) => checkpoint(entry, spec),
700
787
  compact: (spec) => compact(entry, spec),
788
+ rebase: (spec) => rebase(entry, spec),
701
789
  subscribe: (fn) => subscribe(entry, fn),
702
790
  dispose: () => dispose(entry),
703
791
  };
@@ -0,0 +1,33 @@
1
+ // The hook every accepted save reports to, in a module of its own so a reader can
2
+ // listen for accepted bytes without loading the save code.
3
+ const saveAcceptedHooks = [];
4
+
5
+ /**
6
+ * Run a callback with the bytes the host just accepted.
7
+ *
8
+ * Called once per accepted save, after the response, with the exact body that was
9
+ * sent. Never called for a refused, failed or skipped save, and never in test mode,
10
+ * where nothing reached a host.
11
+ *
12
+ * @param {Function} callback - Receives the accepted HTML string
13
+ */
14
+ export function onSaveAccepted(callback) {
15
+ saveAcceptedHooks.push(callback);
16
+ }
17
+
18
+ export function notifySaveAccepted(html, captureId, artifact = null) {
19
+ for (const hook of saveAcceptedHooks) {
20
+ try {
21
+ // The artifact is the capture's own private model of these bytes, and it is
22
+ // offered only when it exists: a raw save and a capture without one keep the
23
+ // shape they had.
24
+ const accepted = artifact
25
+ ? { captureId: captureId ?? null, artifact }
26
+ : { captureId: captureId ?? null };
27
+ hook(html, accepted);
28
+ } catch (err) {
29
+ console.error('clayjs: onSaveAccepted hook failed:', err);
30
+ }
31
+ }
32
+ }
33
+