@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
@@ -36,20 +36,23 @@
36
36
  */
37
37
 
38
38
  import { createIdentityStore } from "../vendor/hyper-morph-identity.vendor.js";
39
+ import { pageRelay } from "./page-relay.js";
39
40
  import { loadedEngine, mergeEngine } from "./merge-engine.js";
40
41
  import Mutation from "../lib/mutation.js";
41
42
  import { NO_DIRTY_SELECTOR, LIVE_SELECTOR } from "../lib/region-policy.js";
42
43
  import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from '../lib/extension-noise.js';
43
44
  import { isEditMode, modeState } from "../core/is-edit-mode.js";
44
45
  import { attributeMerge } from "./merge-declarations.js";
45
- import { serializeForSync, captureForComparisonAndDirty, canonicalizeReceived, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
46
+ import { serializeForSync, copyCaptureClone, captureForComparisonAndDirty, canonicalizeReceived, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
46
47
  import { mergeIgnoresAttr, mergeVetoesAttr } from '../lib/root-attrs.js';
47
48
  import { autosaveActive } from '../lib/autosave-state.js';
48
49
  import { hostSupports } from '../core/host-meta.js';
49
50
  import { PAGE_FORMAT, PAGE_FORMAT_HEADER } from '../core/page-format-wire.js';
51
+ import { knownPageFormatFloor, pageFormatFloor, withPageFormatFloor } from '../core/page-format.js';
52
+ import { onSaveAccepted } from '../core/save-accepted.js';
50
53
  import { servedDocumentEtag } from '../core/host-attrs.js';
51
54
  import { recordEtag, seedEtag, lastSeenEtag, conditionalSaves, representedEtag, forgetRepresentedEtag } from '../core/etag.js';
52
- import { pageMaybeDirty, pauseGate, resumeGate, gateCaptureToken, gateClearIfUnchanged } from '../lib/dirty-gate.js';
55
+ import { pageMaybeDirty, pauseGate, resumeGate, gateCaptureToken, gateClearIfUnchanged, gateMarkDirty } from '../lib/dirty-gate.js';
53
56
  import { gestureSeen } from '../lib/user-gesture.js';
54
57
  import { SyncStream } from './stream.js';
55
58
  import { streamOpened, streamClosed, streamEvent, LIVE_EVENTS } from '../live/stream-bridge.js';
@@ -289,6 +292,21 @@ function pairedBaseline() {
289
292
  */
290
293
  import { WIRE_PROFILES, resolveWireProfile } from './wire-profile.js';
291
294
 
295
+ // Above this the relay body is a Blob built from parts, so the snapshot is never joined
296
+ // into one more full-size string on the main thread. The save body's own threshold (5h).
297
+ const RELAY_BODY_BLOB_MIN_BYTES = 1024 * 1024;
298
+
299
+ /**
300
+ * The body of one relay POST: the bytes `JSON.stringify` of this object has always
301
+ * produced, from parts when the snapshot is large. An absent value still leaves its key
302
+ * out, exactly as before.
303
+ */
304
+ function relayBody(profile, html, sender, identityMap, etag) {
305
+ const rest = { sender, identityMap, ...(etag ? { etag } : {}) };
306
+ if (html.length < RELAY_BODY_BLOB_MIN_BYTES) return JSON.stringify({ [profile.snapshotKey]: html, ...rest });
307
+ return new Blob([`{${JSON.stringify(profile.snapshotKey)}:`, JSON.stringify(html), `,${JSON.stringify(rest).slice(1)}`], { type: 'application/json' });
308
+ }
309
+
292
310
  class LiveSync {
293
311
  constructor() {
294
312
  this.sse = null;
@@ -333,6 +351,7 @@ class LiveSync {
333
351
  // Never reconstructed from lastHtml: that is the newest accepted save or
334
352
  // applied frame, which can still lag the save in flight, and pairing a fresh
335
353
  // stamp with older bytes is the one thing spec §10 forbids.
354
+ this._savedSnapshot?.abort?.abort();
336
355
  this._savedSnapshot = null;
337
356
  this._sendInFlight = false;
338
357
  this._queuedSend = null;
@@ -347,6 +366,9 @@ class LiveSync {
347
366
  this._liveData = false;
348
367
  this._editing = null;
349
368
  this._presenceLease = null;
369
+ // Set by the exit barrier: a sender discovered while it runs is built but stays
370
+ // quiet until resume, so the exit publishes nobody.
371
+ this._presenceQuiesced = false;
350
372
  this._ready = null;
351
373
  // Bumped by every start(), so a discovery that resolves after a stop/restart
352
374
  // cannot open a stream for the run that has already been torn down.
@@ -578,6 +600,7 @@ class LiveSync {
578
600
  this._diskBase = null;
579
601
  this._diskBaseTicket = this._ticket();
580
602
  this._saveTicket = 0;
603
+ this._savedSnapshot?.abort?.abort();
581
604
  this._savedSnapshot = null;
582
605
  this._applyGen++;
583
606
  this.lastSeenSeq = 0;
@@ -662,7 +685,7 @@ class LiveSync {
662
685
  // one sender behind it: the generic marker publishes through the store, so a
663
686
  // page target and a caret path never talk over each other.
664
687
  loadPresence().then((store) => this._startPresence(store, gen), () => {});
665
- this._saveSavedHandler = () => {
688
+ this._saveSavedHandler = (event) => {
666
689
  this._saveEpoch++;
667
690
  // The file now holds what this save wrote: the disk lane's new base,
668
691
  // dated at the save's capture, which is when the document held these
@@ -674,7 +697,7 @@ class LiveSync {
674
697
  if (ticket > this._acceptedSaveTicket) this._acceptedSaveTicket = ticket;
675
698
  const bytes = edit ? edit.getLastSavedBytes() : null;
676
699
  if (bytes != null) this._setDiskBase(bytes, ticket);
677
- this._relayCommit();
700
+ this._relayCommit(event?.detail?.captureId, bytes);
678
701
  };
679
702
  document.addEventListener('clay:save-saved', this._saveSavedHandler);
680
703
  // A 412 can arrive after this tab already merged the version that beat it:
@@ -721,10 +744,14 @@ class LiveSync {
721
744
  this._editing.stop();
722
745
  this._editing = null;
723
746
  }
724
- // Only this run's lease: a successor that already attached keeps its roster.
747
+ // Only this run's lease: a successor that already attached keeps its roster. A
748
+ // broadcast session is stopped, not just detached, so its heartbeat, channel and
749
+ // goodbye go with the session that opened them.
725
750
  if (this._presenceLease) {
726
- this._presenceLease.detach();
751
+ const transport = this._presenceLease;
727
752
  this._presenceLease = null;
753
+ if (typeof transport.stop === 'function') transport.stop();
754
+ else transport.detach();
728
755
  }
729
756
  this._lastRoster = null;
730
757
 
@@ -1136,13 +1163,45 @@ class LiveSync {
1136
1163
  // after dispatchEvent returns, so reading outerHTML and walking children
1137
1164
  // must happen now.
1138
1165
  this._log('snapshot-ready received, preparing to send');
1139
- const html = serializeForSync(clone);
1140
1166
  const identityMap = this.identity.exportMap(clone, originalSnapshotNode);
1141
1167
 
1142
1168
  // The save that follows this capture stores these bytes, so this is the
1143
1169
  // content its stamp will describe. Held for _relayCommit, and overwritten
1144
1170
  // by the next capture, so it always names the save currently in flight.
1145
- this._savedSnapshot = { html, identityMap, ticket: this._saveTicket };
1171
+ this._savedSnapshot?.abort?.abort();
1172
+ const held = {
1173
+ html: null,
1174
+ steps: null,
1175
+ copy: null,
1176
+ abort: null,
1177
+ identityMap,
1178
+ ticket: this._saveTicket,
1179
+ captureId: event.detail.captureId ?? null,
1180
+ };
1181
+ if (event.detail.sliced) {
1182
+ // The save strips this clone as soon as the event returns, so a serialization
1183
+ // that runs across tasks reads a private copy. Cloning is much cheaper than
1184
+ // serializing, and the copy is what keeps the bytes exactly a one-call pass's.
1185
+ // The generator records its own result, so whichever finishes it, the background
1186
+ // run or the relay, leaves the bytes on `held`. The step serializers and the slicer
1187
+ // load lazily; the sliced save that raised this event has already fetched them.
1188
+ held.copy = copyCaptureClone(clone);
1189
+ held.abort = new AbortController();
1190
+ const { signal } = held.abort;
1191
+ Promise.all([import('../core/serialize-steps.js'), import('../lib/slice.js')]).then(([{ serializeForSyncSteps }, { runSliced }]) => {
1192
+ if (signal.aborted || typeof held.html === 'string') return;
1193
+ const steps = (function* () {
1194
+ const html = yield* serializeForSyncSteps(held.copy);
1195
+ held.html = html;
1196
+ held.steps = null;
1197
+ })();
1198
+ held.steps = steps;
1199
+ return runSliced(steps, { signal });
1200
+ }).catch(() => {});
1201
+ } else {
1202
+ held.html = serializeForSync(clone);
1203
+ }
1204
+ this._savedSnapshot = held;
1146
1205
  };
1147
1206
 
1148
1207
  document.addEventListener('clay:snapshot-ready', this._snapshotHandler);
@@ -1190,37 +1249,81 @@ class LiveSync {
1190
1249
  * peers only ever see versions the host accepted, each descending from the one
1191
1250
  * before. A host that returns no stamp still gets the relay, without one.
1192
1251
  */
1193
- _relayCommit() {
1252
+ _relayCommit(captureId = null, acceptedHtml = null) {
1194
1253
  if (this.isDestroyed) return;
1195
1254
  const etag = lastSeenEtag();
1196
1255
 
1256
+ const held = this._savedSnapshot;
1257
+ // A capture taken after this save's own is somebody else's content: relay it
1258
+ // under its own save's stamp, never this one's. The legacy null-id shape keeps
1259
+ // matching, so a fixture that names no capture on either side still relays.
1260
+ if (held && (held.captureId ?? null) !== (captureId ?? null)) {
1261
+ this._log('Skipping send - snapshot belongs to a different capture');
1262
+ return;
1263
+ }
1264
+
1197
1265
  // The content this save stored, captured with it. Consumed rather than left
1198
1266
  // behind, so a save-saved with no capture of its own can never reuse an
1199
1267
  // older save's bytes under a newer stamp.
1200
- const pending = this._savedSnapshot;
1268
+ const pending = held;
1201
1269
  this._savedSnapshot = null;
1202
1270
 
1271
+ // A serialization still running in the background: this task finishes it, so the
1272
+ // relayed bytes are the ones the save stored however the two raced.
1273
+ if (pending?.copy && typeof pending.html !== 'string') {
1274
+ pending.abort?.abort();
1275
+ if (pending.steps) for (let step = pending.steps.next(); !step.done; step = pending.steps.next());
1276
+ else pending.html = serializeForSync(pending.copy);
1277
+ }
1278
+
1203
1279
  // No captured snapshot means nothing here knows which bytes this save
1204
1280
  // stored, and §10 is explicit that a stamp must never travel on its own.
1205
1281
  if (!pending || typeof pending.html !== 'string') return;
1206
1282
 
1283
+ // The capture is taken before the document transforms run, and the page
1284
+ // projection is one of them: a page's relay has to carry what the file now
1285
+ // holds, not the editor DOM the capture saw. Everything outside the page's
1286
+ // own elements stays the captured sync domain, `no-save` regions included.
1287
+ let payload = pending;
1288
+ if (typeof acceptedHtml === 'string') {
1289
+ try {
1290
+ payload = pageRelay(pending, acceptedHtml, this.identity);
1291
+ } catch (err) {
1292
+ // Stale page bytes are worse than no relay: the save stays accepted, and
1293
+ // peers get the next one.
1294
+ this._log(`Skipping send - page relay failed: ${err && err.message}`);
1295
+ return;
1296
+ }
1297
+ if (!payload) {
1298
+ this._log('Skipping send - accepted capture has no page article');
1299
+ return;
1300
+ }
1301
+ }
1302
+
1303
+ // The relay cites this save's stamp, so it declares the floor those bytes went out
1304
+ // at: the save adds the format marker on its way out (core/save-core.js runSave),
1305
+ // after the capture this relay was built from. At floor 0 the html is unchanged.
1306
+ const sent = acceptedBytesFor(captureId) ?? acceptedHtml;
1307
+ const floor = typeof sent === 'string' ? pageFormatFloor(sent) : 0;
1308
+ payload = { ...payload, html: asSent(withPageFormatFloor(payload.html, Math.max(knownPageFormatFloor(), floor)), sent) };
1309
+
1207
1310
  // Skip only an unstamped repeat, judged against the base as it stood. A
1208
1311
  // landed save is news even when its bytes equal the last relay: "Keep mine"
1209
1312
  // can restore exactly those bytes after the peers moved on, and they must
1210
1313
  // hear it.
1211
- const repeat = !etag && pending.html === this.lastHtml;
1314
+ const repeat = !etag && payload.html === this.lastHtml;
1212
1315
 
1213
1316
  // The host accepted these bytes, so every later accepted save descends from
1214
1317
  // them: they are the peer lane's base from this moment, not once the relay
1215
1318
  // returns. A frame whose morph ran after this capture installs its own,
1216
1319
  // newer base when its resources settle, and the ticket keeps it there.
1217
- this._setPeerBase(pending.html, pending.identityMap, pending.ticket);
1320
+ this._setPeerBase(payload.html, payload.identityMap, payload.ticket);
1218
1321
 
1219
1322
  if (repeat) {
1220
1323
  this._log('Skipping send - HTML unchanged');
1221
1324
  return;
1222
1325
  }
1223
- this._enqueueSend(pending.html, pending.identityMap, etag || null);
1326
+ this._enqueueSend(payload.html, payload.identityMap, etag || null);
1224
1327
  }
1225
1328
 
1226
1329
  _postUpdate(html, identityMap, etag = null, attempt = 0) {
@@ -1239,12 +1342,7 @@ class LiveSync {
1239
1342
  [profile.documentHeader]: window.location.href,
1240
1343
  [PAGE_FORMAT_HEADER]: String(PAGE_FORMAT),
1241
1344
  },
1242
- body: JSON.stringify({
1243
- [profile.snapshotKey]: html,
1244
- sender: this.clientId,
1245
- identityMap: identityMap,
1246
- ...(etag ? { etag } : {})
1247
- })
1345
+ body: relayBody(profile, html, this.clientId, identityMap, etag)
1248
1346
  }).then(response => {
1249
1347
  if (!response.ok) {
1250
1348
  console.warn('[LiveSync] Save returned status:', response.status);
@@ -1288,7 +1386,8 @@ class LiveSync {
1288
1386
  return store.connect({ send: (body) => this._postPresence(body) })
1289
1387
  .then((transport) => {
1290
1388
  if (this.isDestroyed || this._startGen !== gen) {
1291
- transport?.detach();
1389
+ if (typeof transport?.stop === 'function') transport.stop();
1390
+ else transport?.detach();
1292
1391
  return null;
1293
1392
  }
1294
1393
  this._presenceLease = transport || null;
@@ -1299,8 +1398,11 @@ class LiveSync {
1299
1398
  return loadEdit().then((mod) => {
1300
1399
  if (this.isDestroyed || this._startGen !== gen || this._editing) return;
1301
1400
  this._editing = new mod.EditingSender({});
1302
- this._editing.start();
1303
- this._editing.roster(this._lastRoster);
1401
+ if (this._presenceQuiesced) this._editing.stop();
1402
+ else {
1403
+ this._editing.start();
1404
+ this._editing.roster(this._lastRoster);
1405
+ }
1304
1406
  });
1305
1407
  });
1306
1408
  }
@@ -1917,6 +2019,9 @@ class LiveSync {
1917
2019
  ignoreAttribute: (el, name, merged) => isRootLocalAttr(el, name, merged) || (derivedActive() && isDerivedAttr(merged || el, name)),
1918
2020
  attributeMerge,
1919
2021
  protectFocusedValue: true,
2022
+ // Focus inside ClayJS's own UI (a shadow root) has nothing on the page to put back,
2023
+ // and restoring the stale page selection would pull focus out of the field.
2024
+ restoreFocus: document.activeElement?.localName !== 'clay-editor-ui',
1920
2025
  keepLiveOnly: source === 'peer',
1921
2026
  scripts: { mergeTags: loadedEngine().mergeTagRecognizers },
1922
2027
  hooks: { beforeAttributeUpdated },
@@ -2020,7 +2125,9 @@ class LiveSync {
2020
2125
  } finally {
2021
2126
  lineageCapture.finish(conflicts.list());
2022
2127
  }
2023
- if (dirty && !report.localDiverged) {
2128
+ if (this.lane === 'live' && report.localDiverged) {
2129
+ gateMarkDirty();
2130
+ } else if (dirty && !report.localDiverged) {
2024
2131
  gateClearIfUnchanged(token);
2025
2132
  }
2026
2133
  // Synthetic ids converge: every live element the frame named takes the
@@ -2499,6 +2606,26 @@ class LiveSync {
2499
2606
  }
2500
2607
  }
2501
2608
 
2609
+ // The bytes the host just accepted, exactly as they went out, and the capture they came
2610
+ // from: a save adds the format marker on its way out, so only these say the floor.
2611
+ let lastAccepted = null;
2612
+ onSaveAccepted((html, accepted) => {
2613
+ lastAccepted = typeof html === 'string' ? { html, captureId: accepted?.captureId ?? null } : null;
2614
+ });
2615
+
2616
+ const acceptedBytesFor = (captureId) =>
2617
+ lastAccepted && (lastAccepted.captureId ?? null) === (captureId ?? null) ? lastAccepted.html : null;
2618
+
2619
+ // A relay re-serializes the DOM, and that spelling is not the file's: a bare empty
2620
+ // attribute comes back as `=""`. When the relay describes the same document the host
2621
+ // stored, it carries the stored bytes themselves, so a peer reads what a reload would.
2622
+ // One that holds regions the file leaves out (`no-save`) keeps its own serialization.
2623
+ function asSent(html, sent) {
2624
+ if (typeof sent !== 'string' || sent === html) return html;
2625
+ const root = (source) => new DOMParser().parseFromString(source, 'text/html').documentElement;
2626
+ return root(html).isEqualNode(root(sent)) ? sent : html;
2627
+ }
2628
+
2502
2629
  // Singleton instance
2503
2630
  const liveSync = new LiveSync();
2504
2631
 
@@ -0,0 +1,52 @@
1
+ import { importMap } from "../vendor/hyper-morph-identity.vendor.js";
2
+ import { serializeForSync } from "../core/snapshot.js";
3
+
4
+ const PAGE = /^page(\.[0-9]+)?$/;
5
+ const tags = (root) => [...root.querySelectorAll('script[merge][clay~="live"]')]
6
+ .filter((el) => PAGE.test(el.getAttribute("merge")));
7
+
8
+ export function pageRelay(pending, acceptedHtml, identity) {
9
+ // A capture with no page article has nothing to overlay, and a document without
10
+ // one is not parsed at all: nothing outside the article would change.
11
+ if (!pending.html.includes("data-page")) return pending;
12
+ const parse = (html) => new DOMParser().parseFromString(html, "text/html");
13
+ const outgoing = parse(pending.html);
14
+ const article = outgoing.querySelector("article[data-page]");
15
+ if (!article) return pending;
16
+ const accepted = parse(acceptedHtml);
17
+ const savedArticle = accepted.querySelector("article[data-page]");
18
+ if (!savedArticle) return null;
19
+ const oldIds = importMap(outgoing.documentElement, pending.identityMap);
20
+
21
+ const replace = (old, source) => {
22
+ const next = outgoing.importNode(source, true);
23
+ const id = oldIds.get(old);
24
+ if (id) oldIds.set(next, id);
25
+ old.replaceWith(next);
26
+ return next;
27
+ };
28
+ const nextArticle = replace(article, savedArticle);
29
+ const prior = new Map(tags(outgoing).map((el) => [el.getAttribute("merge"), el]));
30
+ for (const saved of tags(accepted)) {
31
+ const name = saved.getAttribute("merge");
32
+ const old = prior.get(name);
33
+ if (old) replace(old, saved);
34
+ else nextArticle.before(outgoing.importNode(saved, true));
35
+ prior.delete(name);
36
+ }
37
+ for (const old of prior.values()) old.remove();
38
+ const title = outgoing.querySelector("head > title");
39
+ const savedTitle = accepted.querySelector("head > title");
40
+ if (title && savedTitle) replace(title, savedTitle);
41
+ else if (savedTitle) outgoing.head.append(outgoing.importNode(savedTitle, true));
42
+ else title?.remove();
43
+
44
+ const root = outgoing.documentElement;
45
+ const identityMap = identity.exportMap(root, (node) => {
46
+ const id = oldIds.get(node);
47
+ if (!id) return null;
48
+ identity.adopt(node, id);
49
+ return node;
50
+ });
51
+ return { ...pending, html: serializeForSync(root), identityMap };
52
+ }
@@ -60,6 +60,11 @@ export class PresenceStore {
60
60
  this.timer = null;
61
61
  this.timerKind = null;
62
62
  this.inflight = false;
63
+ // The lease the request in flight belongs to, and the target it carried. A
64
+ // request a lease left behind is its own: giving the lease back resets these, so
65
+ // the next one starts unblocked and a late answer changes nothing.
66
+ this.inflightOwner = null;
67
+ this.inflightKey = null;
63
68
  }
64
69
 
65
70
  /**
@@ -253,21 +258,37 @@ export class PresenceStore {
253
258
  },
254
259
  detach() {
255
260
  if (store.owner !== lease) return;
261
+ // A clear this lease asked for and the host never heard: the last word it
262
+ // holds is a target, and nothing after this lease will take it back.
263
+ const unsent = store.heldKey === '' && typeof store.inflightKey === 'string' && store.inflightKey !== '';
256
264
  store.owner = null;
257
265
  store.session = null;
258
266
  store.n = 0;
259
267
  store.sentKey = null;
268
+ // The request this lease left behind is its own: the next lease starts
269
+ // unblocked, and the answer that arrives late releases nothing.
270
+ store.inflight = false;
271
+ store.inflightOwner = null;
272
+ store.inflightKey = null;
260
273
  store.cancel();
261
274
  store.target = null;
262
275
  store.heldKey = '';
263
276
  store.explicit = false;
264
277
  store.clear();
278
+ if (unsent) {
279
+ try {
280
+ Promise.resolve(lease.send({ editing: null })).catch(() => {});
281
+ } catch {}
282
+ }
265
283
  },
266
284
  };
267
285
  this.owner = lease;
268
286
  this.session = null;
269
287
  this.n = 0;
270
288
  this.sentKey = null;
289
+ this.inflight = false;
290
+ this.inflightOwner = null;
291
+ this.inflightKey = null;
271
292
  this.cancel();
272
293
  // A fresh connection starts from nothing, so the target this tab holds is sent
273
294
  // again rather than assumed delivered.
@@ -284,7 +305,7 @@ export class PresenceStore {
284
305
  *
285
306
  * @param {{send?: ?function(Object): any}} [options] - the relay's send, when
286
307
  * this tab may publish at all. A reader passes none.
287
- * @returns {Promise<?{active: boolean, detach: function(): void}>}
308
+ * @returns {Promise<?{active: boolean, detach: function(): void, stop?: function(): void}>}
288
309
  */
289
310
  connect({ send = null } = {}) {
290
311
  if (this.connecting) return this.connecting;
@@ -297,8 +318,15 @@ export class PresenceStore {
297
318
  (extensions.includes('page-awareness-v1') || extensions.includes('record-awareness-v1'))
298
319
  ) {
299
320
  const scope = typeof awareness.scope === 'string' && awareness.scope ? awareness.scope : null;
300
- return import('./local-presence.js')
301
- .then(({ localPresence }) => localPresence({ scope, store: this }).start() ?? null);
321
+ return import('./local-presence.js').then(({ localPresence }) => {
322
+ const session = localPresence({ scope, store: this });
323
+ const lease = session.start();
324
+ if (!lease) return null;
325
+ // The lease stays the transport this document holds, so it is still the
326
+ // store's one owner; `stop` is the session's, and stopping it is what
327
+ // takes the heartbeat, the channel and the goodbye with the session.
328
+ return Object.defineProperty(lease, 'stop', { value: () => session.stop() });
329
+ });
302
330
  }
303
331
  if (typeof send === 'function' && extensions.includes('presence-editing')) {
304
332
  return this.attachTransport({ send });
@@ -382,10 +410,17 @@ export class PresenceStore {
382
410
  this.cancel();
383
411
  this.sentKey = this.heldKey;
384
412
  this.inflight = true;
413
+ this.inflightOwner = owner;
414
+ this.inflightKey = this.sentKey;
385
415
  if (body.v === PRESENCE_VERSION) this.n = n;
386
416
 
387
417
  const settle = (ok) => {
418
+ // A request a lease that has since let the tab go is not this store's to
419
+ // answer for: the newer owner's request, or the final clear, is what stands.
420
+ if (this.inflightOwner !== owner) return;
388
421
  this.inflight = false;
422
+ this.inflightOwner = null;
423
+ this.inflightKey = null;
389
424
  if (this.owner !== owner) return;
390
425
  if (this.heldKey !== this.sentKey) {
391
426
  // A newer target arrived while this one was in flight: the latest wins.
@@ -408,5 +443,10 @@ export default presence;
408
443
  // sender an editor starts with — and both find the same store here.
409
444
  if (typeof window !== 'undefined') {
410
445
  window.clay = window.clay || {};
411
- if (!window.clay.presence) window.clay.presence = presence;
446
+ if (!window.clay.presence) {
447
+ window.clay.presence = presence;
448
+ try {
449
+ document.dispatchEvent(new CustomEvent('clay:presence-ready', { detail: { presence } }));
450
+ } catch {}
451
+ }
412
452
  }
@@ -62,6 +62,7 @@ function loadUI() {
62
62
  uiLoad ??= Promise.all([import('../ui/bevel-controls.js'), import('../ui/bevel.js')])
63
63
  .then(([controls, bevel]) => (ui = {
64
64
  pageScheme: controls.pageScheme,
65
+ paintLook: controls.paintLook,
65
66
  RUNTIME_ONLY: controls.RUNTIME_ONLY,
66
67
  TOKENS: bevel.TOKENS,
67
68
  SHADOW: bevel.SHADOW,
@@ -149,7 +150,7 @@ class Presence {
149
150
  this.roster = next;
150
151
  if (!(await this.supported())) return;
151
152
  if (same) {
152
- set(this.root, 'color-scheme', ui.pageScheme());
153
+ ui.paintLook(this.root);
153
154
  return;
154
155
  }
155
156
  await this.render();
@@ -174,7 +175,8 @@ class Presence {
174
175
  // total either way: a recipient the host would not name gets people: [] and
175
176
  // everyone in `anonymous`, so this sum is the same participant count in both
176
177
  // directions.
177
- if (people.length + anonymous <= 1) {
178
+ // The page menu draws who is here inside its own dock, so the corner stays empty.
179
+ if (people.length + anonymous <= 1 || window.clay?.menu) {
178
180
  this.hide();
179
181
  return;
180
182
  }
@@ -182,7 +184,6 @@ class Presence {
182
184
  if (!this.build()) return;
183
185
 
184
186
  this.hideTip();
185
- set(this.root, 'color-scheme', ui.pageScheme());
186
187
  this.stack.textContent = '';
187
188
  for (const person of people) {
188
189
  this.stack.appendChild(this.avatar(person));
@@ -199,6 +200,7 @@ class Presence {
199
200
  set(this.count, 'display', 'none');
200
201
  }
201
202
  set(this.root, 'display', 'flex');
203
+ ui.paintLook(this.root);
202
204
  }
203
205
 
204
206
  hide() {
@@ -307,6 +309,7 @@ class Presence {
307
309
  }
308
310
 
309
311
  const presence = new Presence();
312
+ if (typeof document !== 'undefined') document.addEventListener('clay:menu-ready', () => presence.render());
310
313
 
311
314
  export { presence, Presence };
312
315
  export default presence;
@@ -177,6 +177,10 @@ export function createSavedStream({ documentURL, servedEtag = null, represented
177
177
  let presenceStore = null;
178
178
  let presenceLease = null;
179
179
  let presenceLoad = null;
180
+ // The corner the roster is drawn in, downloaded beside the store on the same first
181
+ // frame. It is the chrome alone: the block marks an editor draws are edit-only and
182
+ // a reader never imports them.
183
+ let presenceRoster = null;
180
184
  const presenceQueue = [];
181
185
 
182
186
  const active = (gen) => !stopped && gen === generation;
@@ -188,29 +192,37 @@ export function createSavedStream({ documentURL, servedEtag = null, represented
188
192
 
189
193
  function leavePresence() {
190
194
  presenceQueue.length = 0;
195
+ // A roster a newer owner has taken over is that owner's to clear: giving this
196
+ // lease back must not remove the squares it is drawing.
197
+ if (presenceRoster && (!presenceLease || presenceLease.active)) presenceRoster.clear();
191
198
  if (presenceLease) {
192
199
  presenceLease.detach();
193
200
  presenceLease = null;
194
201
  }
195
202
  presenceStore = null;
203
+ presenceRoster = null;
196
204
  presenceLoad = null;
197
205
  }
198
206
 
199
207
  // A frame that lands while the download is in flight waits in order, and a
200
208
  // download that failed keeps the queue so the next frame asks again.
201
- function takePresence(data) {
202
- if (presenceStore) {
203
- presenceStore.receive(data);
209
+ function takePresence(data, gen) {
210
+ if (presenceRoster) {
211
+ presenceRoster.update(data);
204
212
  return;
205
213
  }
206
214
  presenceQueue.push(data);
207
215
  if (presenceLoad) return;
208
- presenceLoad = import('./presence-store.js')
209
- .then(({ presence }) => {
210
- presenceStore = presence;
211
- presenceLease = presence.attachTransport({ send: () => Promise.resolve(false) });
212
- for (const frame of presenceQueue.splice(0)) presence.receive(frame);
213
- return presence;
216
+ presenceLoad = Promise.all([import('./presence-store.js'), import('./presence.js')])
217
+ .then(([storeMod, rosterMod]) => {
218
+ // The download outlived the reader that asked for it: a stop (or a newer start)
219
+ // took the stream away, so this load attaches no lease and draws no roster.
220
+ if (!active(gen)) return null;
221
+ presenceStore = storeMod.presence;
222
+ presenceRoster = rosterMod.presence;
223
+ presenceLease = presenceStore.attachTransport({ send: () => Promise.resolve(false) });
224
+ for (const frame of presenceQueue.splice(0)) presenceRoster.update(frame);
225
+ return presenceRoster;
214
226
  })
215
227
  .catch(() => {
216
228
  presenceLoad = null;
@@ -336,10 +348,12 @@ export function createSavedStream({ documentURL, servedEtag = null, represented
336
348
  }
337
349
 
338
350
  function acceptBody(html, reason, responseEtag) {
339
- // The response's own stamp names the body when the host sends one, otherwise the
340
- // served root attribute does. A discovery stamp is never attached, and identical
341
- // bytes already named by a stamp keep that name.
342
- const bodyStamp = stampOf(responseEtag) ?? readBodyStamp(html);
351
+ // The served root attribute names the body; the response's own stamp only when the
352
+ // body carries none, since a server or proxy may add an HTTP ETag of its own. A weak
353
+ // ETag is never a document stamp (spec §4): it is how Express names any response. A
354
+ // discovery stamp is never attached, and identical bytes already named by a stamp
355
+ // keep that name.
356
+ const bodyStamp = readBodyStamp(html) ?? (/^W\//.test(responseEtag ?? '') ? null : stampOf(responseEtag));
343
357
  const etag = bodyStamp ?? (html === receipt.bytes ? receipt.etag : null);
344
358
  const seq = reason.kind === 'startup' ? receipt.seq : reason.seq ?? receipt.seq;
345
359
  if (!accept({ bytes: html, etag, seq })) return;
@@ -514,7 +528,7 @@ export function createSavedStream({ documentURL, servedEtag = null, represented
514
528
  } catch {
515
529
  return;
516
530
  }
517
- if (data && typeof data === 'object' && !Array.isArray(data)) takePresence(data);
531
+ if (data && typeof data === 'object' && !Array.isArray(data)) takePresence(data, gen);
518
532
  });
519
533
  return handle;
520
534
  });
@@ -335,3 +335,4 @@ export function setShown(el, shown, display = 'block') {
335
335
  }
336
336
 
337
337
  export { pageScheme, protectIcon };
338
+ export { paintLook, uiLook } from './page-colour.js';
@@ -2,7 +2,7 @@
2
2
  // close. It holds no behaviour. The modal and the crop adapter own focus, Escape,
3
3
  // backdrop clicks and settling, so each keeps the rules it already had.
4
4
  import { style, set } from "../lib/hostile-css.js";
5
- import { bevelBox, bevelSurface, bevelText, pageScheme, protectIcon, restyleBox } from "./bevel-controls.js";
5
+ import { bevelBox, bevelSurface, bevelText, uiLook, protectIcon, restyleBox } from "./bevel-controls.js";
6
6
  import { TOKENS, FONT_SANS, RULES, GLYPHS } from "./bevel.js";
7
7
  import { themed, tokenRulesFor, partRules, markParts } from "./theme-parts.js";
8
8
 
@@ -103,7 +103,10 @@ export function bevelDialog({ zIndex = "100", width = "600px", closable = false,
103
103
  root.setAttribute("data-clay-modal", "");
104
104
  if (on) root.setAttribute("data-clay-ui", surface);
105
105
  root.setAttribute("aria-hidden", "true");
106
- set(root, "color-scheme", on ? theme.scheme : pageScheme());
106
+ const known = on && theme.scheme !== "light dark";
107
+ const panelWidth = Math.min(parseFloat(width) || 600, innerWidth - 32);
108
+ const area = { x: (innerWidth - panelWidth) / 2, y: Math.min(96, innerHeight * 0.1), width: panelWidth, height: 160 };
109
+ set(root, "color-scheme", known ? theme.scheme : uiLook(area).scheme);
107
110
 
108
111
  const overlay = bevelBox("div", [
109
112
  "position:fixed", "inset:0", `z-index:${zIndex}`, "overflow:auto",