@panphora/clayjs 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/README.md +4 -5
  2. package/THIRD-PARTY-NOTICES.md +16 -0
  3. package/compat/highlight-11.9.0.min.js +1213 -0
  4. package/compat/highlight-LICENSE +29 -0
  5. package/compat/journal-assets.lock.json +22 -0
  6. package/compat/overtype-2.6.1.min.js +116 -0
  7. package/compat/overtype-LICENSE +21 -0
  8. package/dist/all.js +90 -0
  9. package/dist/clay-dom.js +84 -0
  10. package/dist/clay-events.js +103 -0
  11. package/dist/clay-internals.js +85 -0
  12. package/dist/clay-options.js +93 -0
  13. package/dist/clay-router.js +98 -0
  14. package/dist/clay-ui.js +95 -0
  15. package/dist/clay-utils.js +84 -0
  16. package/dist/clay.js +146 -0
  17. package/dist/clay.standalone.js +83090 -58706
  18. package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
  19. package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
  20. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +1 -0
  21. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2JL6E75N.js +1 -0
  24. package/dist/m/c-2M73252O.js +1 -0
  25. package/dist/m/c-2SPVQSHC.js +1 -0
  26. package/dist/m/c-33HUYEDI.js +1 -0
  27. package/dist/m/c-33JJ6CBX.js +2 -0
  28. package/dist/m/c-35NBGMZB.js +1 -0
  29. package/dist/m/c-3GJ4SDE2.js +1 -0
  30. package/dist/m/c-3IQRECYS.js +1 -0
  31. package/dist/m/c-3OWDMNJR.js +2 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4B6H4G5V.js +1 -0
  34. package/dist/m/c-4DRMS4QG.js +9 -0
  35. package/dist/m/c-4FFHZM32.js +1 -0
  36. package/dist/m/c-4JZDZFF6.js +1 -0
  37. package/dist/m/c-4L3BTFRK.js +1 -0
  38. package/dist/m/c-4Q2XTQ2N.js +2 -0
  39. package/dist/m/c-4WKFKEKF.js +1 -0
  40. package/dist/m/c-4YSCRICO.js +1 -0
  41. package/dist/m/c-54RCDCBU.js +1 -0
  42. package/dist/m/c-5M4YEKCX.js +4 -0
  43. package/dist/m/c-5R4PTRG2.js +1 -0
  44. package/dist/m/c-5V3MOVJT.js +1 -0
  45. package/dist/m/c-5VYOWDTI.js +1 -0
  46. package/dist/m/c-5Y6TNX5W.js +34 -0
  47. package/dist/m/c-5YIHJ3TJ.js +1 -0
  48. package/dist/m/c-66O32MV3.js +28 -0
  49. package/dist/m/c-6HMB24EV.js +1 -0
  50. package/dist/m/c-7DJDKPGL.js +1 -0
  51. package/dist/m/c-7O6AFR65.js +1 -0
  52. package/dist/m/c-7SWMYKAI.js +1 -0
  53. package/dist/m/c-7VGZ7RU5.js +1 -0
  54. package/dist/m/c-A7LS5UZW.js +1 -0
  55. package/dist/m/c-ANQTJ3B5.js +2 -0
  56. package/dist/m/c-AQLFEATB.js +1 -0
  57. package/dist/m/c-AYVOREBE.js +1 -0
  58. package/dist/m/c-B2ZXPOS3.js +3 -0
  59. package/dist/m/c-BBVGTQG7.js +1 -0
  60. package/dist/m/c-BMIZKMPV.js +1 -0
  61. package/dist/m/c-BTDI4Y23.js +1 -0
  62. package/dist/m/c-BWFVSBDF.js +1 -0
  63. package/dist/m/c-BYTKL6UX.js +1 -0
  64. package/dist/m/c-CFHW5AMY.js +6 -0
  65. package/dist/m/c-CJZSLMVQ.js +1 -0
  66. package/dist/m/c-CYWLPPUS.js +1 -0
  67. package/dist/m/c-CZUSOB7V.js +1 -0
  68. package/dist/m/c-DBE6ORSI.js +1 -0
  69. package/dist/m/c-DCFSEJZG.js +1 -0
  70. package/dist/m/c-DNZ5IFM6.js +1 -0
  71. package/dist/m/c-EDHUR4MX.js +1 -0
  72. package/dist/m/c-EJTGYSLN.js +1 -0
  73. package/dist/m/c-EY6UOM54.js +1 -0
  74. package/dist/m/c-F3CKJTJS.js +1 -0
  75. package/dist/m/c-F7Y6FLPY.js +1 -0
  76. package/dist/m/c-FA4DZVTC.js +1 -0
  77. package/dist/m/c-FILLTJA7.js +1 -0
  78. package/dist/m/c-G2BUZITR.js +1 -0
  79. package/dist/m/c-GB3JTV6E.js +1 -0
  80. package/dist/m/c-GBGWXTX4.js +1 -0
  81. package/dist/m/c-GHBCCVG4.js +1 -0
  82. package/dist/m/c-GJAEGGXY.js +1 -0
  83. package/dist/m/c-GOWBDM6J.js +1 -0
  84. package/dist/m/c-GQ6F6IDK.js +1 -0
  85. package/dist/m/c-H4LG442Q.js +1 -0
  86. package/dist/m/c-HA6INT6J.js +1 -0
  87. package/dist/m/c-HPURYVZS.js +2 -0
  88. package/dist/m/c-HRDCD3WN.js +67 -0
  89. package/dist/m/c-HT33EQRN.js +1 -0
  90. package/dist/m/c-HVYXIS72.js +1 -0
  91. package/dist/m/c-IB52R237.js +1 -0
  92. package/dist/m/c-IJUK2GPW.js +1 -0
  93. package/dist/m/c-IKWNE4R4.js +1 -0
  94. package/dist/m/c-IU3IYEU6.js +1 -0
  95. package/dist/m/c-IZ2EBRAJ.js +1 -0
  96. package/dist/m/c-J7CA54IP.js +1 -0
  97. package/dist/m/c-JAX2667S.js +6 -0
  98. package/dist/m/c-KI7M4AY3.js +1 -0
  99. package/dist/m/c-KIPQJX4B.js +1 -0
  100. package/dist/m/c-KKWLDOPM.js +1 -0
  101. package/dist/m/c-KR4UW4KW.js +1 -0
  102. package/dist/m/c-KY3VJGUA.js +8 -0
  103. package/dist/m/c-L4WQSC27.js +1 -0
  104. package/dist/m/c-LFPHCXY3.js +1 -0
  105. package/dist/m/c-LR4CGEPT.js +91 -0
  106. package/dist/m/c-LRPUO2MX.js +1 -0
  107. package/dist/m/c-LTGMXXKO.js +1 -0
  108. package/dist/m/c-LXENEBV7.js +1 -0
  109. package/dist/m/c-LZJPNR2W.js +1 -0
  110. package/dist/m/c-MMUIUUZG.js +2 -0
  111. package/dist/m/c-MTQHWQC3.js +1 -0
  112. package/dist/m/c-N326R2JD.js +1 -0
  113. package/dist/m/c-N7JPUPGC.js +1 -0
  114. package/dist/m/c-NMJRXKPC.js +8 -0
  115. package/dist/m/c-NRPJEVJS.js +1 -0
  116. package/dist/m/c-NUHP2XDB.js +1 -0
  117. package/dist/m/c-NZMNXVF6.js +1 -0
  118. package/dist/m/c-OCSZ4QGN.js +1 -0
  119. package/dist/m/c-OIBUTDXI.js +1 -0
  120. package/dist/m/c-OIDRZKAF.js +1 -0
  121. package/dist/m/c-OYTFP5QN.js +1 -0
  122. package/dist/m/c-P7KNDG34.js +1 -0
  123. package/dist/m/c-PPOSCAQD.js +1 -0
  124. package/dist/m/c-PQ5LK6QE.js +993 -0
  125. package/dist/m/c-PZ4XEPUP.js +2 -0
  126. package/dist/m/c-QHIWXHRO.js +1 -0
  127. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  128. package/dist/m/c-QIX5BJYN.js +1 -0
  129. package/dist/m/c-QNKREOL6.js +1 -0
  130. package/dist/m/c-QQHEX6HA.js +1 -0
  131. package/dist/m/c-R7RMANJI.js +9 -0
  132. package/dist/m/c-RI4NT75Y.js +1 -0
  133. package/dist/m/c-RM7J7BV2.js +1 -0
  134. package/dist/m/c-RUJKSUQW.js +1 -0
  135. package/dist/m/c-RVMWGXEJ.js +1 -0
  136. package/dist/m/c-S7H2DRCB.js +1 -0
  137. package/dist/m/c-S7LK4KUE.js +1 -0
  138. package/dist/m/c-SGCCC4FW.js +25 -0
  139. package/dist/m/c-SGIA4IEQ.js +188 -0
  140. package/dist/m/c-SWRJFCNN.js +1 -0
  141. package/dist/m/c-T4VO4OCB.js +1 -0
  142. package/dist/m/c-TCCA5BZN.js +2 -0
  143. package/dist/m/c-TQEMVJCS.js +1 -0
  144. package/dist/m/c-TQZ2JYHD.js +1 -0
  145. package/dist/m/c-TR6RMDQ7.js +3 -0
  146. package/dist/m/c-TRS6TM6O.js +1 -0
  147. package/dist/m/c-UGHH4MB3.js +1 -0
  148. package/dist/m/c-UKHTLWUW.js +1 -0
  149. package/dist/m/c-UL2YUY52.js +1 -0
  150. package/dist/m/c-UNN7LWCT.js +1 -0
  151. package/dist/m/c-UXBMRDZB.js +235 -0
  152. package/dist/m/c-V7PZZRG7.js +1 -0
  153. package/dist/m/c-VAK7SG37.js +1 -0
  154. package/dist/m/c-VDSS4K3Z.js +1 -0
  155. package/dist/m/c-VFX45IZQ.js +1 -0
  156. package/dist/m/c-VHNSBD2M.js +1 -0
  157. package/dist/m/c-VKEDWYDF.js +1 -0
  158. package/dist/m/c-VL52ADDR.js +1 -0
  159. package/dist/m/c-WN5AY3OZ.js +1 -0
  160. package/dist/m/c-WNYBLIOZ.js +1 -0
  161. package/dist/m/c-WRBKEJD4.js +1 -0
  162. package/dist/m/c-WWDBYXLK.js +1 -0
  163. package/dist/m/c-WYFDLITL.js +1 -0
  164. package/dist/m/c-XN6MBKI6.js +67 -0
  165. package/dist/m/c-XNNW2CWK.js +1 -0
  166. package/dist/m/c-XPELB6E5.js +1 -0
  167. package/dist/m/c-XQXZAO7N.js +1 -0
  168. package/dist/m/c-Y6USGTS5.js +5 -0
  169. package/dist/m/c-YDH7Q3NY.js +1 -0
  170. package/dist/m/c-YG2NJYS6.js +135 -0
  171. package/dist/m/c-Z36J63PP.js +1 -0
  172. package/dist/m/c-Z4VRC7GU.js +7 -0
  173. package/dist/m/c-Z6K52OLP.js +19 -0
  174. package/dist/m/c-ZDY2MDAA.js +21 -0
  175. package/dist/m/c-ZE46QDIH.js +1 -0
  176. package/dist/m/c-ZUCOKOHT.js +1 -0
  177. package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
  178. package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
  179. package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
  180. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  181. package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
  182. package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
  183. package/dist/m/core/persist-HKHA66IY.js +3 -0
  184. package/dist/m/core/recovery-TBBSICE2.js +1 -0
  185. package/dist/m/core/save-WUCHTQBE.js +1 -0
  186. package/dist/m/core/save-core-FJVHT5M3.js +1 -0
  187. package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
  188. package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
  189. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  190. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  191. package/dist/m/events/index-23DPTUT4.js +1 -0
  192. package/dist/m/internals/index-WOVAP3RU.js +1 -0
  193. package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
  194. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  195. package/dist/m/loader-HXX6YNH5.js +1 -0
  196. package/dist/m/manifest-F22DEDAC.js +1 -0
  197. package/dist/m/manifest-edit-C3C7376B.js +1 -0
  198. package/dist/m/manifest.json +3491 -0
  199. package/dist/m/options/options-2LH76JQ3.js +1 -0
  200. package/dist/m/page/editor-XUTB2HB2.js +189 -0
  201. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
  202. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  203. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  204. package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
  205. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  206. package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
  207. package/dist/m/plugins/live-NUTTXSBS.js +1 -0
  208. package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
  209. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  210. package/dist/m/plugins/page-KSUH7G6J.js +2 -0
  211. package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
  212. package/dist/m/plugins/people-AJJ32YCG.js +1 -0
  213. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  214. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  215. package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
  216. package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
  217. package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
  218. package/dist/m/plugins/source-KHK5UUSK.js +1 -0
  219. package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
  220. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  221. package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
  222. package/dist/m/plugins/upload-XSGNILUY.js +1 -0
  223. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
  224. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
  225. package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
  226. package/dist/m/reader-core-ILWLXVMP.js +68 -0
  227. package/dist/m/route/route-KOCZSEGN.js +1 -0
  228. package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
  229. package/dist/m/ui/index-EVHKMKBL.js +1 -0
  230. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  231. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  232. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  233. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  234. package/dist/offline-manifest.json +3800 -0
  235. package/dist/page/themes/light-1.css +406 -0
  236. package/dist/server/core.cjs +23662 -22860
  237. package/dist/server/offline-runtime.cjs +4954 -0
  238. package/dist/server/offline-runtime.mjs +4923 -0
  239. package/dist/server/rooms.mjs +9 -0
  240. package/docs/guide.md +1078 -669
  241. package/entries/all.js +2 -0
  242. package/entries/clay-data.js +1 -1
  243. package/entries/clay-dom.js +2 -0
  244. package/entries/clay-events.js +2 -0
  245. package/entries/clay-internals.js +2 -0
  246. package/entries/clay-options.js +2 -0
  247. package/entries/clay-router.js +2 -0
  248. package/entries/clay-ui.js +2 -0
  249. package/entries/clay-utils.js +2 -0
  250. package/entries/clay.js +2 -0
  251. package/entries/sap.js +6 -6
  252. package/fonts/ibm-plex-mono-LICENSE +93 -0
  253. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  254. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  255. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  256. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  257. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  258. package/fonts/ibm-plex-sans-LICENSE +93 -0
  259. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  260. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  261. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  262. package/fonts/ibm-plex.css +71 -0
  263. package/fonts/inter-LICENSE +93 -0
  264. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  265. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  266. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  267. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  268. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  269. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  270. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  271. package/fonts/inter.css +62 -0
  272. package/fonts/newsreader-LICENSE +93 -0
  273. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  274. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  275. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  276. package/fonts/newsreader.css +26 -0
  277. package/fonts/work-sans-LICENSE +93 -0
  278. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  279. package/fonts/work-sans.css +8 -0
  280. package/package.json +20 -3
  281. package/packed-contract.json +1 -0
  282. package/src/attrs/add-from.js +27 -4
  283. package/src/attrs/save-freeze.js +7 -52
  284. package/src/core/admin-attrs.js +119 -12
  285. package/src/core/admin-contenteditable.js +12 -10
  286. package/src/core/admin-inputs.js +14 -15
  287. package/src/core/admin-onclick.js +2 -0
  288. package/src/core/autosave.js +74 -21
  289. package/src/core/conflict-download.js +22 -0
  290. package/src/core/conflict-notice-ui.js +1173 -0
  291. package/src/core/conflict-notice.js +140 -697
  292. package/src/core/edit-mode.js +147 -14
  293. package/src/core/etag.js +30 -12
  294. package/src/core/host-meta.js +72 -8
  295. package/src/core/host-permission.js +29 -0
  296. package/src/core/is-edit-mode.js +145 -12
  297. package/src/core/mode-key.js +125 -0
  298. package/src/core/mode-lifecycle.js +544 -0
  299. package/src/core/mode-receipt.js +30 -0
  300. package/src/core/mode-refusal.js +13 -0
  301. package/src/core/mode-resources.js +52 -0
  302. package/src/core/mode-support.js +82 -0
  303. package/src/core/mode-switch.js +259 -0
  304. package/src/core/mode-transition.js +112 -0
  305. package/src/core/page-data-write.js +305 -0
  306. package/src/core/page-data.js +26 -227
  307. package/src/core/page-format-wire.js +4 -0
  308. package/src/core/page-format.js +58 -0
  309. package/src/core/persist.js +37 -9
  310. package/src/core/preserve-authored.js +114 -0
  311. package/src/core/recovery.js +772 -0
  312. package/src/core/runtime-url.js +105 -0
  313. package/src/core/save-core.js +109 -12
  314. package/src/core/save-replace.js +72 -0
  315. package/src/core/save-size-notice.js +134 -0
  316. package/src/core/save-size.js +140 -0
  317. package/src/core/save.js +805 -118
  318. package/src/core/snapshot.js +121 -11
  319. package/src/core/source-map.js +157 -9
  320. package/src/core/unsaved-warning.js +14 -4
  321. package/src/core/upgrade-run.js +408 -0
  322. package/src/core/version.js +18 -0
  323. package/src/lib/assets.js +30 -0
  324. package/src/lib/autosave-debug-gate.js +50 -0
  325. package/src/lib/autosave-scheduler.js +141 -0
  326. package/src/lib/boot-imports.js +8 -0
  327. package/src/lib/derived.js +3 -1
  328. package/src/lib/dirty-gate.js +15 -0
  329. package/src/lib/doc-storage.js +33 -0
  330. package/src/lib/effect.js +9 -4
  331. package/src/lib/mutation.js +4 -2
  332. package/src/lib/offline-runtime.js +226 -0
  333. package/src/lib/preload.js +48 -0
  334. package/src/lib/root-attrs.js +1 -1
  335. package/src/lib/trash.js +18 -0
  336. package/src/lib/undo/capture-move.js +152 -0
  337. package/src/live/local-channel.js +61 -0
  338. package/src/live/local-lane.js +60 -9
  339. package/src/live/server-lane.js +6 -3
  340. package/src/loader-logic.js +76 -15
  341. package/src/loader.js +218 -30
  342. package/src/page/adopt-boot.js +76 -22
  343. package/src/page/adopt.js +25 -4
  344. package/src/page/awareness.js +230 -0
  345. package/src/page/blocks.js +79 -5
  346. package/src/page/blocksel.js +20 -4
  347. package/src/page/board-face.js +13 -0
  348. package/src/page/board-panel.js +5 -17
  349. package/src/page/board-style.js +2 -13
  350. package/src/page/board-view.js +1 -11
  351. package/src/page/board-views.js +17 -0
  352. package/src/page/board.js +2 -2
  353. package/src/page/boot-pack.js +26 -0
  354. package/src/page/boot.js +237 -0
  355. package/src/page/checklist.js +1 -1
  356. package/src/page/chrome.js +411 -61
  357. package/src/page/csv-import.js +17 -2
  358. package/src/page/document-title-dom.js +40 -0
  359. package/src/page/document-title-runtime.js +76 -0
  360. package/src/page/document-title.js +9 -0
  361. package/src/page/drag.js +48 -13
  362. package/src/page/drop.js +2 -1
  363. package/src/page/editor.js +330 -86
  364. package/src/page/feature-loader.js +72 -0
  365. package/src/page/features/board.js +12 -0
  366. package/src/page/features/code.js +13 -0
  367. package/src/page/features/media.js +290 -0
  368. package/src/page/features/registry.js +23 -0
  369. package/src/page/features/table.js +383 -0
  370. package/src/page/{tool-view.js → features/tool.js} +33 -2
  371. package/src/page/format-codec.js +215 -0
  372. package/src/page/format-gate.js +179 -0
  373. package/src/page/format-level.js +157 -0
  374. package/src/page/highlight-slot.js +23 -0
  375. package/src/page/highlight.js +30 -16
  376. package/src/page/history-intent.js +78 -0
  377. package/src/page/inline.js +18 -25
  378. package/src/page/items.js +52 -11
  379. package/src/page/link.js +1 -0
  380. package/src/page/markdown.js +86 -1
  381. package/src/page/media-views.js +3 -0
  382. package/src/page/media.js +33 -245
  383. package/src/page/outline-cache.js +11 -0
  384. package/src/page/placeholder.js +27 -7
  385. package/src/page/projection.js +196 -50
  386. package/src/page/reader-patch.js +306 -0
  387. package/src/page/recover.js +1 -1
  388. package/src/page/rich/page-tool.js +1 -4
  389. package/src/page/runtime-owner.js +27 -0
  390. package/src/page/save-projection.js +7 -4
  391. package/src/page/schema.js +14 -0
  392. package/src/page/table-rect-key.js +3 -0
  393. package/src/page/table-select.js +4 -3
  394. package/src/page/table-style.js +1 -1
  395. package/src/page/table-views.js +3 -0
  396. package/src/page/table.js +8 -293
  397. package/src/page/themes/light-1.css +139 -2
  398. package/src/page/themes/light-1.source.css +269 -0
  399. package/src/page/tool-capability.js +3 -0
  400. package/src/page/tool-views.js +3 -0
  401. package/src/page/view.js +390 -107
  402. package/src/page/ybind/awareness.js +172 -0
  403. package/src/page/ybind/binding.js +9 -1
  404. package/src/page/ybind/bridge.js +50 -47
  405. package/src/page/ybind/decode.js +52 -19
  406. package/src/page/ybind/doc.js +27 -2
  407. package/src/page/ybind/group.js +12 -3
  408. package/src/page/ybind/index.js +2 -0
  409. package/src/page/ybind/lineage.js +71 -0
  410. package/src/page/ybind/marks.js +25 -1
  411. package/src/page/ybind/migrate.js +48 -13
  412. package/src/page/ybind/moves.js +32 -21
  413. package/src/page/ybind/positions.js +50 -0
  414. package/src/page/ybind/provider.js +84 -22
  415. package/src/page/ybind/recovery-proof.js +191 -0
  416. package/src/page/ybind/recovery-replay.js +130 -0
  417. package/src/page/ybind/tokens.js +15 -6
  418. package/src/page/ybind/walk.js +10 -3
  419. package/src/page/ybind/writer.js +48 -4
  420. package/src/plugins/ai-edit.js +167 -32
  421. package/src/plugins/bind-text.js +218 -0
  422. package/src/plugins/cms.js +93 -0
  423. package/src/plugins/indicator.js +57 -5
  424. package/src/plugins/live.js +282 -34
  425. package/src/plugins/menu/claygoo.svg.js +1 -0
  426. package/src/plugins/menu/seo-head.js +323 -0
  427. package/src/plugins/menu/seo.js +849 -0
  428. package/src/plugins/menu.js +425 -0
  429. package/src/plugins/page-view.js +131 -0
  430. package/src/plugins/page.js +1022 -275
  431. package/src/plugins/people.js +158 -31
  432. package/src/plugins/richclay.js +119 -9
  433. package/src/plugins/sortable.js +370 -60
  434. package/src/plugins/source.js +189 -366
  435. package/src/plugins/undo.js +289 -19
  436. package/src/plugins/unsent.js +132 -0
  437. package/src/plugins/upgrade-run.js +1 -0
  438. package/src/plugins/upgrade.js +210 -0
  439. package/src/plugins/upload-task.js +571 -0
  440. package/src/plugins/upload-ui.js +265 -0
  441. package/src/plugins/upload.js +34 -153
  442. package/src/plugins/visitor-drafts/inert.js +32 -0
  443. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  444. package/src/plugins/visitor-drafts/prompt.js +2 -2
  445. package/src/plugins/visitor-drafts-owner.js +85 -0
  446. package/src/plugins/visitor-drafts.js +31 -42
  447. package/src/plugins/wire.js +82 -1
  448. package/src/reader-core.js +26 -0
  449. package/src/recovery/conflicts.js +287 -0
  450. package/src/recovery/identity.js +154 -0
  451. package/src/recovery/store.js +792 -0
  452. package/src/recovery/undo.js +257 -0
  453. package/src/server/checkpoint/index.js +68 -4
  454. package/src/server/offline-runtime/index.js +214 -0
  455. package/src/server/rooms/index.js +2 -1
  456. package/src/server/rooms/live-data-rooms.js +2 -2
  457. package/src/server/rooms/page-admission.js +33 -0
  458. package/src/server/rooms/page-lineage.js +56 -0
  459. package/src/server/rooms/page-rebase.js +11 -5
  460. package/src/server/rooms/page-state.js +2 -0
  461. package/src/server/rooms/page-write-admission.js +9 -3
  462. package/src/source/session.js +705 -0
  463. package/src/sync/conflict-revert.js +5 -4
  464. package/src/sync/conflicts-edit.js +119 -0
  465. package/src/sync/conflicts-facade.js +49 -0
  466. package/src/sync/conflicts.js +99 -1
  467. package/src/sync/editing-marker.js +37 -6
  468. package/src/sync/editing-sender.js +8 -1
  469. package/src/sync/live-sync-edit.js +63 -0
  470. package/src/sync/live-sync.js +364 -119
  471. package/src/sync/local-presence.js +187 -0
  472. package/src/sync/merge-declarations.js +10 -0
  473. package/src/sync/merge-engine.js +12 -0
  474. package/src/sync/presence-store.js +412 -0
  475. package/src/sync/presence-target.js +243 -0
  476. package/src/sync/presence.js +72 -41
  477. package/src/sync/saved-handoff.js +200 -0
  478. package/src/sync/saved-stream.js +535 -0
  479. package/src/sync/version-guard.js +92 -0
  480. package/src/sync/wire-profile.js +81 -0
  481. package/src/ui/bevel-controls.js +2 -8
  482. package/src/ui/page-scheme.js +7 -0
  483. package/src/ui/skins/cms.js +1 -1
  484. package/src/ui/theme-config.js +150 -0
  485. package/src/ui/theme.js +6 -138
  486. package/src/ui/toast-lazy.js +11 -0
  487. package/src/vendor/LICENSES-optional.md +253 -0
  488. package/src/vendor/highlight.vendor.js +3 -0
  489. package/src/vendor/hljs-bash.vendor.js +2 -0
  490. package/src/vendor/hljs-css.vendor.js +2 -0
  491. package/src/vendor/hljs-javascript.vendor.js +2 -0
  492. package/src/vendor/hljs-json.vendor.js +2 -0
  493. package/src/vendor/hljs-python.vendor.js +2 -0
  494. package/src/vendor/hljs-xml.vendor.js +2 -0
  495. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  496. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  497. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  498. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  499. package/src/vendor/hyper-morph.vendor.js +6 -6
  500. package/src/vendor/hyper-undo.vendor.js +1 -1
  501. package/src/vendor/hypercms.vendor.js +169 -84
  502. package/src/vendor/marked.vendor.js +68 -0
  503. package/src/vendor/overtype.vendor.js +994 -0
  504. package/src/vendor/richclay.vendor.js +17 -17
  505. package/src/vendor/sap-core.vendor.js +5 -5
  506. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,243 @@
1
+ /**
2
+ * presence-target.js — the one target union behind `clay.presence`.
3
+ *
4
+ * Kanban's record targets and a Document's page targets travel on the same wire
5
+ * field (`editing`) and through the same facade. This module is the contract
6
+ * alone: what a target is, what bounds it, how a roster session is read, and the
7
+ * exact request body a target turns into. It imports no Yjs, no ProseMirror, no
8
+ * page code and no visual code, so a reader can validate a frame without any of
9
+ * them, and the host can validate the same value without decoding Yjs.
10
+ *
11
+ * Three shapes are accepted for `editing`:
12
+ *
13
+ * null nobody is anywhere
14
+ * "1.2" the legacy dotted block path, unchanged
15
+ * {kind:"record", id, field?} a Kanban/Journal record (fieldless journals too)
16
+ * {kind:"page", v:1, epoch, generation, selection} a Document's page target
17
+ *
18
+ * Anything else is refused rather than repaired: a frame this module cannot read
19
+ * draws nothing, and a target it cannot read is never published.
20
+ */
21
+
22
+ import { EDITING_PATH } from './editing-path.js';
23
+
24
+ // The extended envelope's version. A body without `v` is the legacy shape.
25
+ export const PRESENCE_VERSION = 1;
26
+
27
+ // Bounds, shared by this client and the server contract it speaks to.
28
+ export const MAX_TARGET_BYTES = 8 * 1024;
29
+ export const MAX_PAGE_ID_BYTES = 256;
30
+ export const MAX_REL_BYTES = 1024;
31
+ export const MAX_RECORD_ID = 128;
32
+ export const MAX_SELECTION_IDS = 128;
33
+
34
+ // Kanban's documented field enum. A record target without a field is valid.
35
+ export const RECORD_FIELDS = Object.freeze([
36
+ 'title', 'description', 'labels', 'members', 'dates', 'checklist', 'comment', 'attachments',
37
+ ]);
38
+
39
+ // Coalesce selection moves to one every 250 ms, renew a standing target every 5 s,
40
+ // and let it expire 15 s after the server received it. The server accepts at most
41
+ // 10 updates per second, with a burst of 20, per admitted session and document.
42
+ export const COALESCE_MS = 250;
43
+ export const RENEW_MS = 5000;
44
+ export const EXPIRE_MS = 15000;
45
+ export const RATE_PER_SECOND = 10;
46
+ export const RATE_BURST = 20;
47
+
48
+ const RECORD_KEYS = new Set(['kind', 'id', 'field']);
49
+ const PAGE_KEYS = new Set(['kind', 'v', 'epoch', 'generation', 'selection']);
50
+ const TEXT_KEYS = new Set(['kind', 'anchor', 'head']);
51
+ const BLOCKS_KEYS = new Set(['kind', 'ids']);
52
+ const CELL_KEYS = new Set(['kind', 'block', 'row', 'col']);
53
+ const POSITION_KEYS = new Set(['block', 'rel']);
54
+
55
+ // Kanban's ids are ASCII and at most 128 characters. Nothing else is an id.
56
+ const RECORD_ID = /^[\x20-\x7e]+$/;
57
+ // A base64 relative position: valid alphabet, valid padding, decodable length.
58
+ const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
59
+ const CONTROL = /[\u0000-\u001f\u007f]/;
60
+
61
+ const refuse = (reason) => ({ ok: false, reason });
62
+ const accept = (target) => ({ ok: true, target });
63
+
64
+ const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
65
+
66
+ const onlyKeys = (value, allowed) => {
67
+ for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
68
+ return true;
69
+ };
70
+
71
+ // UTF-8 length by hand: the host bounds bytes, and neither a test environment nor an
72
+ // old browser is guaranteed a TextEncoder.
73
+ function utf8Length(text) {
74
+ let length = 0;
75
+ for (const character of text) {
76
+ const code = character.codePointAt(0);
77
+ length += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
78
+ }
79
+ return length;
80
+ }
81
+
82
+ function pageId(value) {
83
+ return typeof value === 'string'
84
+ && value.length > 0
85
+ && !CONTROL.test(value)
86
+ && utf8Length(value) <= MAX_PAGE_ID_BYTES;
87
+ }
88
+
89
+ function relativePosition(value) {
90
+ if (typeof value !== 'string' || !value.length || value.length % 4 !== 0 || !BASE64.test(value)) return false;
91
+ const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
92
+ return (value.length / 4) * 3 - padding <= MAX_REL_BYTES;
93
+ }
94
+
95
+ function positionOf(value) {
96
+ if (!isObject(value) || !onlyKeys(value, POSITION_KEYS)) return null;
97
+ if (!pageId(value.block) || !relativePosition(value.rel)) return null;
98
+ return { block: value.block, rel: value.rel };
99
+ }
100
+
101
+ function selectionOf(value) {
102
+ if (!isObject(value)) return null;
103
+
104
+ if (value.kind === 'text') {
105
+ if (!onlyKeys(value, TEXT_KEYS)) return null;
106
+ const anchor = positionOf(value.anchor);
107
+ const head = positionOf(value.head);
108
+ return anchor && head ? { kind: 'text', anchor, head } : null;
109
+ }
110
+
111
+ if (value.kind === 'blocks') {
112
+ if (!onlyKeys(value, BLOCKS_KEYS) || !Array.isArray(value.ids)) return null;
113
+ if (!value.ids.length || value.ids.length > MAX_SELECTION_IDS) return null;
114
+ for (const id of value.ids) if (!pageId(id)) return null;
115
+ return { kind: 'blocks', ids: [...value.ids] };
116
+ }
117
+
118
+ if (value.kind === 'cell') {
119
+ if (!onlyKeys(value, CELL_KEYS)) return null;
120
+ if (!pageId(value.block) || !pageId(value.row) || !pageId(value.col)) return null;
121
+ return { kind: 'cell', block: value.block, row: value.row, col: value.col };
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ function withinSize(target) {
128
+ return utf8Length(JSON.stringify(target)) <= MAX_TARGET_BYTES;
129
+ }
130
+
131
+ function recordTarget(value) {
132
+ if (!onlyKeys(value, RECORD_KEYS)) return refuse('key');
133
+ const id = value.id;
134
+ if (typeof id !== 'string' || !id.length || id.length > MAX_RECORD_ID || !RECORD_ID.test(id)) return refuse('id');
135
+ if (value.field === undefined) {
136
+ const target = { kind: 'record', id };
137
+ return withinSize(target) ? accept(target) : refuse('size');
138
+ }
139
+ if (typeof value.field !== 'string' || !RECORD_FIELDS.includes(value.field)) return refuse('field');
140
+ const target = { kind: 'record', id, field: value.field };
141
+ return withinSize(target) ? accept(target) : refuse('size');
142
+ }
143
+
144
+ function pageTarget(value) {
145
+ if (!onlyKeys(value, PAGE_KEYS)) return refuse('key');
146
+ if (value.v !== PRESENCE_VERSION) return refuse('version');
147
+ if (!pageId(value.epoch)) return refuse('epoch');
148
+ if (!Number.isSafeInteger(value.generation) || value.generation < 0) return refuse('generation');
149
+ const selection = selectionOf(value.selection);
150
+ if (!selection) return refuse('selection');
151
+ const target = { kind: 'page', v: PRESENCE_VERSION, epoch: value.epoch, generation: value.generation, selection };
152
+ return withinSize(target) ? accept(target) : refuse('size');
153
+ }
154
+
155
+ /**
156
+ * Read one `editing` value.
157
+ *
158
+ * @param {*} value
159
+ * @returns {{ok: true, target: null|string|Object}|{ok: false, reason: string}}
160
+ */
161
+ export function validateTarget(value) {
162
+ if (value === null) return accept(null);
163
+ if (typeof value === 'string') return EDITING_PATH.test(value) ? accept(value) : refuse('path');
164
+ if (!isObject(value)) return refuse('shape');
165
+ if (value.kind === 'record') return recordTarget(value);
166
+ if (value.kind === 'page') return pageTarget(value);
167
+ return refuse('kind');
168
+ }
169
+
170
+ /**
171
+ * The person-level `editing` value: the legacy string or a record target. Page
172
+ * coordinates live only in `sessions`, never on a person.
173
+ */
174
+ export function normalizeEditing(value) {
175
+ const result = validateTarget(value === undefined ? null : value);
176
+ if (!result.ok) return null;
177
+ if (result.target !== null && typeof result.target === 'object' && result.target.kind !== 'record') return null;
178
+ return result.target;
179
+ }
180
+
181
+ /**
182
+ * A stable key for a validated target, so the sender can tell whether the target
183
+ * it holds is the one it sent. Expects `validateTarget`'s output.
184
+ */
185
+ export function targetKey(target) {
186
+ if (target === null || target === undefined) return '';
187
+ if (typeof target === 'string') return `path:${target}`;
188
+ return `target:${JSON.stringify(target)}`;
189
+ }
190
+
191
+ /**
192
+ * Read one public session. `n` is monotonic within a session: a value below the
193
+ * one already seen for that session id cannot overwrite the newer one.
194
+ *
195
+ * @param {Array} value
196
+ * @param {Map<string, Object>} [previous]
197
+ */
198
+ export function normalizeSessions(value, previous = null) {
199
+ const list = Array.isArray(value) ? value : [];
200
+ const sessions = [];
201
+ for (const item of list) {
202
+ if (!isObject(item)) continue;
203
+ const id = pageId(item.id) ? item.id : null;
204
+ if (!id) continue;
205
+ const n = Number.isSafeInteger(item.n) && item.n >= 0 ? item.n : 0;
206
+ const seen = previous && previous.get(id);
207
+ if (seen && n < seen.n) {
208
+ sessions.push(seen);
209
+ continue;
210
+ }
211
+ const editing = validateTarget(item.editing === undefined ? null : item.editing);
212
+ sessions.push({ id, you: item.you === true, n, editing: editing.ok ? editing.target : null });
213
+ }
214
+ return sessions;
215
+ }
216
+
217
+ /** The private recipient-specific transport block. Never published in a snapshot. */
218
+ export function normalizeSelfSession(value) {
219
+ if (!isObject(value)) return null;
220
+ const id = pageId(value.id) ? value.id : null;
221
+ const token = typeof value.token === 'string' && value.token.length ? value.token : null;
222
+ return id && token ? { id, token } : null;
223
+ }
224
+
225
+ /**
226
+ * The exact body a target is sent as.
227
+ *
228
+ * The legacy dotted path and a clear keep the single-field shape every host
229
+ * already accepts, character for character. A record or page target uses the
230
+ * extended envelope once the connection has issued a session token; before that a
231
+ * record may still travel in the single-field shape, and a page target — whose
232
+ * coordinates need the envelope — has no body at all.
233
+ *
234
+ * @param {{target: *, session?: ?{token: string}, n?: number}} request
235
+ * @returns {?Object}
236
+ */
237
+ export function requestBody({ target = null, session = null, n = 0 }) {
238
+ if (target === null || target === undefined || typeof target === 'string') return { editing: target ?? null };
239
+ const token = session && typeof session.token === 'string' && session.token.length ? session.token : null;
240
+ if (!token) return target.kind === 'record' ? { editing: target } : null;
241
+ if (!Number.isSafeInteger(n) || n < 0) return null;
242
+ return { v: PRESENCE_VERSION, session: token, n, editing: target };
243
+ }
@@ -6,6 +6,12 @@
6
6
  * recipient, from that recipient's own access; nothing here asks for a name and
7
7
  * nothing here can widen what arrived.
8
8
  *
9
+ * The roster itself is not here: it lives in `presence-store.js`, which is what
10
+ * `clay.presence` hands to a page and what a caret adapter reads. This module is
11
+ * the chrome a reader sees — the squares, the count and the tip — and the visual
12
+ * library behind it stays a first-use download, so a reader who draws nothing
13
+ * still has a roster and still fetches none of it.
14
+ *
9
15
  * THE RULE THIS FILE EXISTS TO GET RIGHT.
10
16
  *
11
17
  * captureForSaveAndComparison() clones the document, hands that clone to peers
@@ -39,15 +45,39 @@
39
45
  import { hostSupports } from '../core/host-meta.js';
40
46
  import { make, set } from '../lib/hostile-css.js';
41
47
  import { initialsOf, personColor } from '../lib/person-face.js';
42
- import { pageScheme, RUNTIME_ONLY } from '../ui/bevel-controls.js';
43
- import { TOKENS, FONT_MONO, SHADOW } from '../ui/bevel.js';
48
+ import { presence as store } from './presence-store.js';
44
49
 
45
50
  // Two initials at 11px need about 15px of the square, so the overlap has to
46
51
  // leave that much clear or the neighbouring square eats the second letter.
47
52
  const SIZE = 30;
48
53
  const OVERLAP = 6;
49
- const FONT = `600 11px/1 ${FONT_MONO}`;
50
- const LABEL_FONT = `500 11px/1.4 ${FONT_MONO}`;
54
+
55
+ // The palette, the marking token and the two faces are the Bevel subset, which is a
56
+ // first-use download: a reader alone on a document never draws a square, so it never
57
+ // fetches them. Everything here is read after loadUI() resolves.
58
+ let ui = null;
59
+ let uiLoad = null;
60
+
61
+ function loadUI() {
62
+ uiLoad ??= Promise.all([import('../ui/bevel-controls.js'), import('../ui/bevel.js')])
63
+ .then(([controls, bevel]) => (ui = {
64
+ pageScheme: controls.pageScheme,
65
+ RUNTIME_ONLY: controls.RUNTIME_ONLY,
66
+ TOKENS: bevel.TOKENS,
67
+ SHADOW: bevel.SHADOW,
68
+ FONT: `600 11px/1 ${bevel.FONT_MONO}`,
69
+ LABEL_FONT: `500 11px/1.4 ${bevel.FONT_MONO}`,
70
+ }))
71
+ .catch((error) => {
72
+ uiLoad = null;
73
+ throw error;
74
+ });
75
+ return uiLoad;
76
+ }
77
+
78
+ // A roster frame never rejects its way back to the stream: a chunk that failed to
79
+ // arrive leaves the corner empty and the next frame asks again.
80
+ const uiReady = () => loadUI().then(() => true, () => false);
51
81
 
52
82
  // all:initial first on every element, so a page rule has nothing left to reach; it
53
83
  // resets color-scheme and direction too, which are put back straight after.
@@ -60,30 +90,18 @@ const FACE = '#FFFCF6';
60
90
 
61
91
  /** Every element this module creates, marked the same way. */
62
92
  function runtimeRoot(el) {
63
- el.setAttribute('clay', RUNTIME_ONLY);
93
+ el.setAttribute('clay', ui.RUNTIME_ONLY);
64
94
  return el;
65
95
  }
66
96
 
67
97
  /**
68
- * Read the roster frame defensively. Everything here arrived over a wire, and a
69
- * frame this function cannot understand must draw nothing rather than draw a
70
- * guess: `[object Object]` in a square is worse than an empty corner.
98
+ * What this module draws from a snapshot. The store keeps the rest — the target
99
+ * each person holds and their sessions — because none of it is chrome: a frame that
100
+ * only moved somebody's caret must leave every square where it is.
71
101
  */
72
- function normalize(data) {
73
- const list = data && Array.isArray(data.people) ? data.people : [];
74
- const people = [];
75
- for (const person of list) {
76
- if (!person || typeof person.id !== 'string' || !person.id) continue;
77
- const name = typeof person.name === 'string' && person.name.trim() ? person.name.trim() : 'Someone';
78
- people.push({
79
- id: person.id,
80
- name,
81
- you: person.you === true,
82
- });
83
- }
84
- const count = data && data.anonymous;
85
- const anonymous = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
86
- return { people, anonymous };
102
+ function faces(snapshot) {
103
+ const people = snapshot.people.map((person) => ({ id: person.id, name: person.name, you: person.you }));
104
+ return { people, anonymous: snapshot.anonymous };
87
105
  }
88
106
 
89
107
  class Presence {
@@ -99,10 +117,21 @@ class Presence {
99
117
  /**
100
118
  * Does this host run presence at all? Asked once, of /_/meta, and never
101
119
  * inferred from a frame arriving: the answer decides whether the chrome is
102
- * built, so a host that says nothing draws nothing forever.
120
+ * built, so a host that says nothing draws nothing forever. The two awareness
121
+ * capabilities travel with it, because the store publishes them in the same
122
+ * snapshot and a page reads them before it publishes a target.
103
123
  */
104
124
  supported() {
105
- if (!this._gate) this._gate = hostSupports('presence').then((ok) => ok === true, () => false);
125
+ if (!this._gate) {
126
+ this._gate = Promise.all([
127
+ hostSupports('presence'),
128
+ hostSupports('record-awareness-v1'),
129
+ hostSupports('page-awareness-v1'),
130
+ ]).then(([supported, recordAwareness, pageAwareness]) => {
131
+ store.setCapabilities({ supported, recordAwareness, pageAwareness });
132
+ return supported === true;
133
+ }, () => false);
134
+ }
106
135
  return this._gate;
107
136
  }
108
137
 
@@ -115,19 +144,20 @@ class Presence {
115
144
  * @returns {Promise<void>}
116
145
  */
117
146
  async update(data) {
118
- const next = normalize(data);
147
+ const next = faces(store.receive(data));
119
148
  const same = this.root && JSON.stringify(next) === JSON.stringify(this.roster);
120
149
  this.roster = next;
121
150
  if (!(await this.supported())) return;
122
151
  if (same) {
123
- set(this.root, 'color-scheme', pageScheme());
152
+ set(this.root, 'color-scheme', ui.pageScheme());
124
153
  return;
125
154
  }
126
- this.render();
155
+ await this.render();
127
156
  }
128
157
 
129
158
  /** The stream went away, so the roster is no longer true about anything. */
130
159
  clear() {
160
+ store.clear();
131
161
  this.roster = { people: [], anonymous: 0 };
132
162
  if (!this.root) return;
133
163
  this.root.remove();
@@ -137,7 +167,7 @@ class Presence {
137
167
  this.tip = null;
138
168
  }
139
169
 
140
- render() {
170
+ async render() {
141
171
  const { people, anonymous } = this.roster;
142
172
 
143
173
  // A person alone on a document is told nothing at all. The count is the
@@ -148,10 +178,11 @@ class Presence {
148
178
  this.hide();
149
179
  return;
150
180
  }
181
+ if (!(await uiReady())) return;
151
182
  if (!this.build()) return;
152
183
 
153
184
  this.hideTip();
154
- set(this.root, 'color-scheme', pageScheme());
185
+ set(this.root, 'color-scheme', ui.pageScheme());
155
186
  this.stack.textContent = '';
156
187
  for (const person of people) {
157
188
  this.stack.appendChild(this.avatar(person));
@@ -184,14 +215,14 @@ class Presence {
184
215
 
185
216
  this.root = runtimeRoot(make('div', [
186
217
  ...RESET,
187
- `color-scheme:${pageScheme()}`,
218
+ `color-scheme:${ui.pageScheme()}`,
188
219
  'position:fixed',
189
220
  'top:calc(12px + env(safe-area-inset-top,0px))',
190
221
  'right:calc(12px + env(safe-area-inset-right,0px))',
191
222
  'z-index:2147483000',
192
223
  'display:flex', 'align-items:center', 'gap:8px',
193
224
  'max-width:calc(100vw - 24px)',
194
- `font:${LABEL_FONT}`, `color:${TOKENS.ink}`,
225
+ `font:${ui.LABEL_FONT}`, `color:${ui.TOKENS.ink}`,
195
226
  // Only the squares take a pointer. A fixed corner that swallowed clicks
196
227
  // would take a bite out of whatever the page put underneath it.
197
228
  'pointer-events:none',
@@ -202,10 +233,10 @@ class Presence {
202
233
  ...RESET,
203
234
  'box-sizing:border-box', 'flex:none', 'white-space:nowrap',
204
235
  'padding:3px 8px', 'border-radius:0',
205
- `background:${TOKENS.surface}`, `color:${TOKENS.muted}`,
206
- `border:1px solid ${TOKENS['line-2']}`,
207
- `font:${LABEL_FONT}`, 'letter-spacing:.02em',
208
- `box-shadow:${SHADOW}`,
236
+ `background:${ui.TOKENS.surface}`, `color:${ui.TOKENS.muted}`,
237
+ `border:1px solid ${ui.TOKENS['line-2']}`,
238
+ `font:${ui.LABEL_FONT}`, 'letter-spacing:.02em',
239
+ `box-shadow:${ui.SHADOW}`,
209
240
  'display:none',
210
241
  'pointer-events:none',
211
242
  ]));
@@ -224,9 +255,9 @@ class Presence {
224
255
  'position:absolute', 'top:100%', 'right:0', 'margin-top:6px',
225
256
  'box-sizing:border-box', 'white-space:nowrap', 'pointer-events:none',
226
257
  'padding:5px 9px', 'border-radius:0',
227
- `background:${TOKENS.ink}`, `color:${TOKENS.ground}`,
228
- `font:${LABEL_FONT}`, 'letter-spacing:.02em',
229
- `box-shadow:${SHADOW}`,
258
+ `background:${ui.TOKENS.ink}`, `color:${ui.TOKENS.ground}`,
259
+ `font:${ui.LABEL_FONT}`, 'letter-spacing:.02em',
260
+ `box-shadow:${ui.SHADOW}`,
230
261
  'display:none',
231
262
  ]));
232
263
  this.tip.setAttribute('data-clay-presence-tip', '');
@@ -245,10 +276,10 @@ class Presence {
245
276
  'box-sizing:border-box', 'flex:none',
246
277
  `width:${SIZE}px`, `height:${SIZE}px`, 'border-radius:0',
247
278
  'display:flex', 'align-items:center', 'justify-content:center',
248
- `font:${FONT}`, 'letter-spacing:.02em',
279
+ `font:${ui.FONT}`, 'letter-spacing:.02em',
249
280
  `margin-left:-${OVERLAP}px`,
250
281
  'border-width:2px', 'border-style:solid', 'background-image:none',
251
- `box-shadow:${SHADOW}`,
282
+ `box-shadow:${ui.SHADOW}`,
252
283
  'user-select:none', 'cursor:default', 'pointer-events:auto',
253
284
  ...skin,
254
285
  ], initialsOf(person.name)));
@@ -0,0 +1,200 @@
1
+ // The reader's half of a live switch: what the saved lane hands to an entry without
2
+ // stopping, and what it takes back when the entry is refused.
3
+ //
4
+ // A reader owns the saved lane — the transport it opened, or the frames generic sync
5
+ // applies to the page — and a switch needs that one lane, never a second stream. So
6
+ // preparation holds the queue instead of closing it: an apply already on the wire
7
+ // finishes, a file that arrives meanwhile keeps its place, and a repair the host asked
8
+ // for stays owed. `reconcile` applies what was held, `resume` gives the lane back
9
+ // exactly as it was, and `commit` retires it only onto a receipt a successor can build
10
+ // from. Nothing here clears a buffer, aborts a request or stops a stream before commit.
11
+ //
12
+ // This is a dynamic target: a reader that never enters edit never fetches it.
13
+
14
+ const fail = (message, code) => Object.assign(new Error(`clayjs: ${message}`), { code });
15
+
16
+ const newer = (staged, candidate) =>
17
+ !staged || candidate.seq === null || staged.seq === null || candidate.seq > staged.seq ? candidate : staged;
18
+
19
+ /**
20
+ * A transport's handoff, built out of the transport's own takeover points: `adopt`
21
+ * moves the acknowledged sink here, so nothing is applied and the receipt still moves
22
+ * only onto bytes a sink took. `stop` is never called before commit.
23
+ *
24
+ * `yieldArticle` gives the article up to a switch that mounts over it and
25
+ * `reclaimArticle` takes it back when that switch is refused; both are the reader's
26
+ * own, so the queue and the receipt never move while the article is not its.
27
+ *
28
+ * @param {Object} lane
29
+ * @param {Object} lane.stream - the transport this reader opened
30
+ * @param {function(Object): boolean} lane.apply - apply one file to the article
31
+ * @param {function(): boolean} lane.retire - give up the article and its claim
32
+ * @param {function(): boolean} lane.yieldArticle - release the article, keep the queue
33
+ * @param {function(): boolean} lane.reclaimArticle - take the article back and repaint
34
+ */
35
+ function streamHandoff({ stream, apply, retire, yieldArticle = () => false, reclaimArticle = () => false }) {
36
+ let staged = null;
37
+ let yielded = false;
38
+ stream.adopt({
39
+ onSnapshot: (candidate) => {
40
+ staged = newer(staged, candidate);
41
+ return true;
42
+ },
43
+ });
44
+ const take = () => {
45
+ const candidate = staged;
46
+ staged = null;
47
+ return candidate;
48
+ };
49
+ return {
50
+ get represented() {
51
+ return stream.represented;
52
+ },
53
+ // The newest file this reader holds: a snapshot the transport took while the
54
+ // switch prepared is a file the session was not built from.
55
+ get held() {
56
+ return staged ?? stream.represented;
57
+ },
58
+ yieldArticle() {
59
+ if (yielded || !yieldArticle()) return false;
60
+ yielded = true;
61
+ return true;
62
+ },
63
+ reclaimArticle() {
64
+ if (!yielded || !reclaimArticle()) return false;
65
+ yielded = false;
66
+ return true;
67
+ },
68
+ reconcile() {
69
+ const candidate = take();
70
+ if (candidate && !apply(candidate)) return Promise.reject(fail("the switch could not apply the saved file", "load-failed"));
71
+ return Promise.resolve();
72
+ },
73
+ resume() {
74
+ stream.adopt({ onSnapshot: apply });
75
+ const candidate = take();
76
+ if (candidate) apply(candidate);
77
+ },
78
+ commit() {
79
+ // Whatever the old owner was never shown goes on screen first: the receipt a
80
+ // successor is built from and the article it replaces must name one file. An
81
+ // article already yielded belongs to the switch: it reconciled the queue and
82
+ // refused anything newer before asking for this commit, so nothing staged is
83
+ // its to paint, and the read-only half only stops the stream and retires.
84
+ if (!yielded) {
85
+ const candidate = take();
86
+ if (candidate && !apply(candidate)) return false;
87
+ }
88
+ if (stream.represented.bytes === null) return false;
89
+ stream.stop();
90
+ return retire();
91
+ },
92
+ };
93
+ }
94
+
95
+ /**
96
+ * A reader's handoff: its own transport, or the engine that feeds the page.
97
+ *
98
+ * @param {Object} reader
99
+ * @param {?Object} reader.stream - the transport this reader owns, if any
100
+ * @param {Object} reader.options - `{ signal, deadline }` for the lane
101
+ * @param {function(Object): boolean} reader.apply - apply one file to the article
102
+ * @param {function(): boolean} reader.retire - give up the article and its claim
103
+ * @param {function(): boolean} reader.yieldArticle - release the article, keep the queue
104
+ * @param {function(): boolean} reader.reclaimArticle - take the article back and repaint
105
+ * @returns {Promise<Object>}
106
+ */
107
+ export async function readerHandoff({ stream, options, apply, retire, yieldArticle = () => false, reclaimArticle = () => false }) {
108
+ if (stream) return streamHandoff({ stream, apply, retire, yieldArticle, reclaimArticle });
109
+ const lane = await import("./live-sync.js").then(({ liveSync }) => liveSync.prepareHandoff(options));
110
+ return {
111
+ get represented() {
112
+ return lane.represented;
113
+ },
114
+ get held() {
115
+ return lane.held;
116
+ },
117
+ yieldArticle,
118
+ reclaimArticle,
119
+ reconcile: () => lane.reconcile(),
120
+ resume: () => lane.resume(),
121
+ commit: () => lane.commit() && retire(),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * A sync engine's handoff: hold the saved lane's frames and the repair it owes, and
127
+ * answer with the file it last applied.
128
+ *
129
+ * @param {Object} lane - the engine instance
130
+ * @param {Object} options - `{ signal, deadline }`
131
+ * @returns {Promise<Object>}
132
+ */
133
+ export async function engineHandoff(lane, { signal, deadline = 15000 } = {}) {
134
+ const until = Date.now() + deadline;
135
+ // An apply already in flight is the one thing preparation waits for: its bytes are
136
+ // the file the switch will be built from.
137
+ while (lane._morphInFlight) {
138
+ if (signal?.aborted) {
139
+ lane._handoffHeld = false;
140
+ throw fail("the switch was cancelled", "aborted");
141
+ }
142
+ if (Date.now() >= until) {
143
+ lane._handoffHeld = false;
144
+ throw fail("the apply did not settle", "timeout");
145
+ }
146
+ await new Promise((resolve) => setTimeout(resolve, 1));
147
+ }
148
+ const queued = () => lane._pendingHtml != null || lane._pendingExternal != null;
149
+ return {
150
+ get represented() {
151
+ return lane._represented;
152
+ },
153
+ // The newest file the lane holds, queued or applied: a frame the engine took
154
+ // while the switch prepared is newer than the one on the page, and a session
155
+ // built from the older one must refuse rather than stop the lane over it.
156
+ get held() {
157
+ let newest = lane._represented;
158
+ if (lane._pendingHtml != null) {
159
+ newest = newer(newest, {
160
+ bytes: lane._pendingHtml,
161
+ etag: lane._pendingEtag ?? null,
162
+ seq: lane._pendingSeq ?? null,
163
+ });
164
+ }
165
+ if (lane._pendingExternal) {
166
+ newest = newer(newest, {
167
+ bytes: lane._pendingExternal.html,
168
+ etag: lane._pendingExternal.etag ?? null,
169
+ seq: lane._pendingExternal.seq ?? null,
170
+ });
171
+ }
172
+ return newest;
173
+ },
174
+ async reconcile() {
175
+ if (!lane._handoff || lane.isDestroyed) return;
176
+ for (let pass = 0; pass < 8; pass++) {
177
+ if (lane._morphInFlight) {
178
+ await new Promise((resolve) => setTimeout(resolve, 1));
179
+ continue;
180
+ }
181
+ if (!queued()) return;
182
+ await lane._runPending();
183
+ }
184
+ if (queued()) throw fail("the held frame could not be applied", "load-failed");
185
+ },
186
+ resume() {
187
+ if (!lane._handoff) return;
188
+ lane._handoff = null;
189
+ lane._handoffHeld = false;
190
+ lane._scheduleNextFrame();
191
+ },
192
+ commit() {
193
+ if (!lane._represented.bytes) return false;
194
+ lane._handoff = null;
195
+ lane._handoffHeld = false;
196
+ lane.stop();
197
+ return true;
198
+ },
199
+ };
200
+ }