@panphora/clayjs 1.15.2 → 1.17.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 (551) 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 +84291 -57611
  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-ZUE7PSCI.js +1 -0
  21. package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
  22. package/dist/m/c-2AZDZWCK.js +1 -0
  23. package/dist/m/c-2SPVQSHC.js +1 -0
  24. package/dist/m/c-2YYDMLNZ.js +1 -0
  25. package/dist/m/c-336SGU5W.js +1 -0
  26. package/dist/m/c-33JJ6CBX.js +2 -0
  27. package/dist/m/c-3GJ4SDE2.js +1 -0
  28. package/dist/m/c-3H6BZDNJ.js +6 -0
  29. package/dist/m/c-3OWDMNJR.js +2 -0
  30. package/dist/m/c-3RUU4TPX.js +91 -0
  31. package/dist/m/c-3XIRD4NU.js +1 -0
  32. package/dist/m/c-46XYM55L.js +1 -0
  33. package/dist/m/c-4DFEJ2PO.js +3 -0
  34. package/dist/m/c-4JZDZFF6.js +1 -0
  35. package/dist/m/c-4L3BTFRK.js +1 -0
  36. package/dist/m/c-4Q2XTQ2N.js +2 -0
  37. package/dist/m/c-4WKFKEKF.js +1 -0
  38. package/dist/m/c-56QRZTZH.js +1 -0
  39. package/dist/m/c-5J4E7O4Y.js +4 -0
  40. package/dist/m/c-5LKDWQTL.js +19 -0
  41. package/dist/m/c-5QWQ7A5L.js +1 -0
  42. package/dist/m/c-5RJFTLSF.js +1 -0
  43. package/dist/m/c-5V3MOVJT.js +1 -0
  44. package/dist/m/c-5VYOWDTI.js +1 -0
  45. package/dist/m/c-5YIHJ3TJ.js +1 -0
  46. package/dist/m/c-6AZOGAPE.js +1 -0
  47. package/dist/m/c-6CRUUXOX.js +5 -0
  48. package/dist/m/c-6HMB24EV.js +1 -0
  49. package/dist/m/c-6KEVEQ7L.js +1 -0
  50. package/dist/m/c-6NC5I2TV.js +1 -0
  51. package/dist/m/c-74MGYWZF.js +1 -0
  52. package/dist/m/c-7DJDKPGL.js +1 -0
  53. package/dist/m/c-7OLLJPWP.js +1 -0
  54. package/dist/m/c-7SWMYKAI.js +1 -0
  55. package/dist/m/c-A3E3NIMC.js +1 -0
  56. package/dist/m/c-A7LS5UZW.js +1 -0
  57. package/dist/m/c-AMSNPOCX.js +1 -0
  58. package/dist/m/c-AQLFEATB.js +1 -0
  59. package/dist/m/c-B3JM5ZXC.js +1 -0
  60. package/dist/m/c-BBVGTQG7.js +1 -0
  61. package/dist/m/c-BPF2NSPV.js +1 -0
  62. package/dist/m/c-BQ6LOBEE.js +1 -0
  63. package/dist/m/c-BTDI4Y23.js +1 -0
  64. package/dist/m/c-BWFVSBDF.js +1 -0
  65. package/dist/m/c-BYTKL6UX.js +1 -0
  66. package/dist/m/c-CFHW5AMY.js +6 -0
  67. package/dist/m/c-CJ3A7ZKE.js +1 -0
  68. package/dist/m/c-CQHPGKX2.js +1 -0
  69. package/dist/m/c-CYWLPPUS.js +1 -0
  70. package/dist/m/c-CZUSOB7V.js +1 -0
  71. package/dist/m/c-DCFSEJZG.js +1 -0
  72. package/dist/m/c-DCXOXBF3.js +1 -0
  73. package/dist/m/c-DFS4XZJV.js +2 -0
  74. package/dist/m/c-DNENWY4O.js +1 -0
  75. package/dist/m/c-DNZ5IFM6.js +1 -0
  76. package/dist/m/c-E4LIR5X5.js +1 -0
  77. package/dist/m/c-EJTGYSLN.js +1 -0
  78. package/dist/m/c-EVCQ3BKA.js +1 -0
  79. package/dist/m/c-EZU56NQV.js +34 -0
  80. package/dist/m/c-FDNHHZY5.js +1 -0
  81. package/dist/m/c-FILLTJA7.js +1 -0
  82. package/dist/m/c-FRLHSY7E.js +1 -0
  83. package/dist/m/c-FROJUZMV.js +1 -0
  84. package/dist/m/c-G2JPKCTP.js +1 -0
  85. package/dist/m/c-GB6TU7WV.js +1 -0
  86. package/dist/m/c-GFJJEBTU.js +1 -0
  87. package/dist/m/c-GJAEGGXY.js +1 -0
  88. package/dist/m/c-GOWBDM6J.js +1 -0
  89. package/dist/m/c-GQ6F6IDK.js +1 -0
  90. package/dist/m/c-GSFIGKOP.js +1 -0
  91. package/dist/m/c-GY422N3Q.js +1 -0
  92. package/dist/m/c-H4LG442Q.js +1 -0
  93. package/dist/m/c-HA6INT6J.js +1 -0
  94. package/dist/m/c-HAWFNBYG.js +1 -0
  95. package/dist/m/c-HDDEEL77.js +1 -0
  96. package/dist/m/c-HFKUVACH.js +1 -0
  97. package/dist/m/c-HHDFCNHV.js +1 -0
  98. package/dist/m/c-HPURYVZS.js +2 -0
  99. package/dist/m/c-I2ZATQCD.js +2 -0
  100. package/dist/m/c-IEM4NTCS.js +1 -0
  101. package/dist/m/c-IJUK2GPW.js +1 -0
  102. package/dist/m/c-IW6GGOU2.js +1 -0
  103. package/dist/m/c-JAX2667S.js +6 -0
  104. package/dist/m/c-JDCX26UP.js +25 -0
  105. package/dist/m/c-JRKY44DK.js +1 -0
  106. package/dist/m/c-JU3NK25H.js +1 -0
  107. package/dist/m/c-KAU6E43D.js +1 -0
  108. package/dist/m/c-KBS3UV6B.js +1 -0
  109. package/dist/m/c-KGVGROZT.js +1 -0
  110. package/dist/m/c-KI7M4AY3.js +1 -0
  111. package/dist/m/c-KIPQJX4B.js +1 -0
  112. package/dist/m/c-KQ6TYJAP.js +135 -0
  113. package/dist/m/c-KU3ZFADW.js +1 -0
  114. package/dist/m/c-L4WQSC27.js +1 -0
  115. package/dist/m/c-LFPHCXY3.js +1 -0
  116. package/dist/m/c-LRPUO2MX.js +1 -0
  117. package/dist/m/c-LWLBHLVC.js +3 -0
  118. package/dist/m/c-LZJPNR2W.js +1 -0
  119. package/dist/m/c-M2L47S2X.js +7 -0
  120. package/dist/m/c-MFP2ZWD4.js +1 -0
  121. package/dist/m/c-MKSQ4Y57.js +1 -0
  122. package/dist/m/c-MTQHWQC3.js +1 -0
  123. package/dist/m/c-N326R2JD.js +1 -0
  124. package/dist/m/c-N7JPUPGC.js +1 -0
  125. package/dist/m/c-NRPJEVJS.js +1 -0
  126. package/dist/m/c-NUFFJVVJ.js +1 -0
  127. package/dist/m/c-NUHP2XDB.js +1 -0
  128. package/dist/m/c-NUHSPT5P.js +1 -0
  129. package/dist/m/c-O72HVYSA.js +1 -0
  130. package/dist/m/c-OCSZ4QGN.js +1 -0
  131. package/dist/m/c-OIBUTDXI.js +1 -0
  132. package/dist/m/c-OIDRZKAF.js +1 -0
  133. package/dist/m/c-OW5QIGG3.js +1 -0
  134. package/dist/m/c-P7KNDG34.js +1 -0
  135. package/dist/m/c-PGRCK73F.js +1 -0
  136. package/dist/m/c-PQ5LK6QE.js +993 -0
  137. package/dist/m/c-PS5ARVWR.js +9 -0
  138. package/dist/m/c-PZ4XEPUP.js +2 -0
  139. package/dist/m/c-Q622NS2Y.js +1 -0
  140. package/dist/m/c-Q7NQD65V.js +1 -0
  141. package/dist/m/c-QH63LQOS.js +1 -0
  142. package/dist/m/c-QHIWXHRO.js +1 -0
  143. package/dist/m/c-QHWJ5ZIJ.js +1 -0
  144. package/dist/m/c-QICP2YR5.js +1 -0
  145. package/dist/m/c-QJ4OM3G6.js +1 -0
  146. package/dist/m/c-QNKREOL6.js +1 -0
  147. package/dist/m/c-R3L7ZJGB.js +2 -0
  148. package/dist/m/c-R7IWVINQ.js +1 -0
  149. package/dist/m/c-RM7J7BV2.js +1 -0
  150. package/dist/m/c-RSMOODDU.js +1 -0
  151. package/dist/m/c-RVMWGXEJ.js +1 -0
  152. package/dist/m/c-RYUXUUBX.js +1 -0
  153. package/dist/m/c-S4ZEH46B.js +9 -0
  154. package/dist/m/c-S7H2DRCB.js +1 -0
  155. package/dist/m/c-S7LK4KUE.js +1 -0
  156. package/dist/m/c-SDAXDM2D.js +1 -0
  157. package/dist/m/c-SGIA4IEQ.js +188 -0
  158. package/dist/m/c-SMBZG5X4.js +1 -0
  159. package/dist/m/c-SMGU7RT3.js +235 -0
  160. package/dist/m/c-SPIORRYS.js +1 -0
  161. package/dist/m/c-SWLRT66R.js +1 -0
  162. package/dist/m/c-SZGYW5HT.js +1 -0
  163. package/dist/m/c-TCCA5BZN.js +2 -0
  164. package/dist/m/c-TCJTVAFN.js +1 -0
  165. package/dist/m/c-TJ6WOJL3.js +67 -0
  166. package/dist/m/c-TQEMVJCS.js +1 -0
  167. package/dist/m/c-TRS6TM6O.js +1 -0
  168. package/dist/m/c-TYLXX4NF.js +21 -0
  169. package/dist/m/c-U62OB4BT.js +1 -0
  170. package/dist/m/c-U6YWUAR3.js +8 -0
  171. package/dist/m/c-UDCSU5G5.js +2 -0
  172. package/dist/m/c-UDUUJPFZ.js +1 -0
  173. package/dist/m/c-UGHH4MB3.js +1 -0
  174. package/dist/m/c-UL2YUY52.js +1 -0
  175. package/dist/m/c-VAK7SG37.js +1 -0
  176. package/dist/m/c-VDSS4K3Z.js +1 -0
  177. package/dist/m/c-VE6ZMYMC.js +1 -0
  178. package/dist/m/c-VG5XIN4T.js +1 -0
  179. package/dist/m/c-VHM5XQHK.js +1 -0
  180. package/dist/m/c-VHNSBD2M.js +1 -0
  181. package/dist/m/c-VI5RMSVY.js +1 -0
  182. package/dist/m/c-VKEDWYDF.js +1 -0
  183. package/dist/m/c-VRZN56XY.js +1 -0
  184. package/dist/m/c-W5MCHEGA.js +1 -0
  185. package/dist/m/c-WDC5GVKU.js +1 -0
  186. package/dist/m/c-WN5AY3OZ.js +1 -0
  187. package/dist/m/c-WNYBLIOZ.js +1 -0
  188. package/dist/m/c-WRBKEJD4.js +1 -0
  189. package/dist/m/c-WWXIDH4O.js +28 -0
  190. package/dist/m/c-WYFDLITL.js +1 -0
  191. package/dist/m/c-XCNTK76U.js +1 -0
  192. package/dist/m/c-XMS6DPBR.js +1 -0
  193. package/dist/m/c-XN6MBKI6.js +67 -0
  194. package/dist/m/c-XPELB6E5.js +1 -0
  195. package/dist/m/c-XQXZAO7N.js +1 -0
  196. package/dist/m/c-Y5HBBIQ4.js +1 -0
  197. package/dist/m/c-ZE46QDIH.js +1 -0
  198. package/dist/m/c-ZUCOKOHT.js +1 -0
  199. package/dist/m/c-ZVPVXTJF.js +4 -0
  200. package/dist/m/c-ZVRKXKM7.js +1 -0
  201. package/dist/m/c-ZWIOJGMT.js +1 -0
  202. package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
  203. package/dist/m/core/autosave-HRYGUWV6.js +1 -0
  204. package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
  205. package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
  206. package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
  207. package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
  208. package/dist/m/core/persist-JFYRHMRJ.js +3 -0
  209. package/dist/m/core/recovery-FLTCC2OC.js +1 -0
  210. package/dist/m/core/save-HQZ4S667.js +1 -0
  211. package/dist/m/core/save-core-KGQWFUNH.js +1 -0
  212. package/dist/m/core/snapshot-EGQZNT45.js +1 -0
  213. package/dist/m/core/unsaved-warning-IDJLD66H.js +1 -0
  214. package/dist/m/dom/all-APS5ZSKP.js +1 -0
  215. package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
  216. package/dist/m/events/index-RN2JNGBV.js +1 -0
  217. package/dist/m/internals/index-PVXGPUDA.js +1 -0
  218. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  219. package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
  220. package/dist/m/loader-S4I7GKXR.js +1 -0
  221. package/dist/m/manifest-4C419D67.js +1 -0
  222. package/dist/m/manifest-edit-B077F288.js +1 -0
  223. package/dist/m/manifest.json +3988 -0
  224. package/dist/m/options/options-EJMS3K24.js +1 -0
  225. package/dist/m/page/editor-NNJA4OKD.js +202 -0
  226. package/dist/m/plugins/ai-edit-QN4PMMNL.js +6 -0
  227. package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
  228. package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
  229. package/dist/m/plugins/demo-RM5IR6AO.js +1 -0
  230. package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
  231. package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
  232. package/dist/m/plugins/live-HFWD4OIH.js +1 -0
  233. package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
  234. package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
  235. package/dist/m/plugins/page-JOKB6XPR.js +2 -0
  236. package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
  237. package/dist/m/plugins/people-SGPJKANK.js +1 -0
  238. package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
  239. package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
  240. package/dist/m/plugins/richclay-NRQDSWX6.js +517 -0
  241. package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
  242. package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
  243. package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
  244. package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
  245. package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
  246. package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
  247. package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
  248. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
  249. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
  250. package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
  251. package/dist/m/reader-core-DXYCURFF.js +68 -0
  252. package/dist/m/route/route-KOCZSEGN.js +1 -0
  253. package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
  254. package/dist/m/ui/index-3JLVI27O.js +1 -0
  255. package/dist/m/utils/index-4TJCVLT7.js +1 -0
  256. package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
  257. package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
  258. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
  259. package/dist/offline-manifest.json +4034 -0
  260. package/dist/page/themes/light-1.css +430 -0
  261. package/dist/server/core.cjs +23748 -22855
  262. package/dist/server/offline-runtime.cjs +4954 -0
  263. package/dist/server/offline-runtime.mjs +4923 -0
  264. package/dist/server/rooms.mjs +9 -0
  265. package/docs/guide.md +1078 -669
  266. package/entries/all.js +2 -0
  267. package/entries/clay-data.js +1 -1
  268. package/entries/clay-dom.js +2 -0
  269. package/entries/clay-events.js +2 -0
  270. package/entries/clay-internals.js +2 -0
  271. package/entries/clay-options.js +2 -0
  272. package/entries/clay-router.js +2 -0
  273. package/entries/clay-ui.js +2 -0
  274. package/entries/clay-utils.js +2 -0
  275. package/entries/clay.js +2 -0
  276. package/entries/sap.js +6 -6
  277. package/fonts/ibm-plex-mono-LICENSE +93 -0
  278. package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  279. package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  280. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  281. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  282. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  283. package/fonts/ibm-plex-sans-LICENSE +93 -0
  284. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  285. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  286. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  287. package/fonts/ibm-plex.css +71 -0
  288. package/fonts/inter-LICENSE +93 -0
  289. package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  290. package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
  291. package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
  292. package/fonts/inter-greek-wght-normal.woff2 +0 -0
  293. package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  294. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  295. package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
  296. package/fonts/inter.css +62 -0
  297. package/fonts/newsreader-LICENSE +93 -0
  298. package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
  299. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  300. package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
  301. package/fonts/newsreader.css +26 -0
  302. package/fonts/work-sans-LICENSE +93 -0
  303. package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
  304. package/fonts/work-sans.css +8 -0
  305. package/package.json +20 -3
  306. package/packed-contract.json +1 -0
  307. package/src/attrs/add-from.js +27 -4
  308. package/src/attrs/save-freeze.js +7 -52
  309. package/src/core/admin-attrs.js +119 -12
  310. package/src/core/admin-contenteditable.js +12 -10
  311. package/src/core/admin-inputs.js +14 -15
  312. package/src/core/admin-onclick.js +2 -0
  313. package/src/core/autosave.js +74 -21
  314. package/src/core/boot-phase.js +2 -1
  315. package/src/core/conflict-download.js +19 -23
  316. package/src/core/conflict-notice-ui.js +1173 -0
  317. package/src/core/conflict-notice.js +140 -697
  318. package/src/core/download.js +32 -0
  319. package/src/core/edit-mode.js +147 -14
  320. package/src/core/etag.js +30 -12
  321. package/src/core/host-meta.js +90 -8
  322. package/src/core/host-permission.js +29 -0
  323. package/src/core/is-edit-mode.js +145 -12
  324. package/src/core/mode-key.js +125 -0
  325. package/src/core/mode-lifecycle.js +544 -0
  326. package/src/core/mode-receipt.js +30 -0
  327. package/src/core/mode-refusal.js +13 -0
  328. package/src/core/mode-resources.js +52 -0
  329. package/src/core/mode-support.js +82 -0
  330. package/src/core/mode-switch.js +259 -0
  331. package/src/core/mode-transition.js +112 -0
  332. package/src/core/page-data-write.js +305 -0
  333. package/src/core/page-data.js +26 -227
  334. package/src/core/page-format-wire.js +4 -0
  335. package/src/core/page-format.js +58 -0
  336. package/src/core/persist.js +37 -9
  337. package/src/core/preserve-authored.js +114 -0
  338. package/src/core/recovery.js +772 -0
  339. package/src/core/runtime-url.js +105 -0
  340. package/src/core/save-core.js +113 -12
  341. package/src/core/save-replace.js +72 -0
  342. package/src/core/save-size-notice.js +134 -0
  343. package/src/core/save-size.js +140 -0
  344. package/src/core/save.js +806 -119
  345. package/src/core/snapshot.js +159 -11
  346. package/src/core/source-map.js +157 -9
  347. package/src/core/unsaved-warning.js +14 -4
  348. package/src/core/upgrade-run.js +408 -0
  349. package/src/core/version.js +18 -0
  350. package/src/lib/assets.js +30 -0
  351. package/src/lib/autosave-debug-gate.js +50 -0
  352. package/src/lib/autosave-scheduler.js +140 -0
  353. package/src/lib/boot-imports.js +8 -0
  354. package/src/lib/derived.js +3 -1
  355. package/src/lib/dirty-gate.js +15 -0
  356. package/src/lib/doc-storage.js +33 -0
  357. package/src/lib/effect.js +9 -4
  358. package/src/lib/mutation.js +4 -2
  359. package/src/lib/offline-runtime.js +226 -0
  360. package/src/lib/preload.js +48 -0
  361. package/src/lib/root-attrs.js +1 -1
  362. package/src/lib/trash.js +18 -0
  363. package/src/lib/undo/capture-move.js +152 -0
  364. package/src/live/local-channel.js +61 -0
  365. package/src/live/local-lane.js +60 -9
  366. package/src/live/server-lane.js +6 -3
  367. package/src/loader-logic.js +76 -15
  368. package/src/loader.js +218 -30
  369. package/src/page/adopt-boot.js +76 -22
  370. package/src/page/adopt.js +25 -4
  371. package/src/page/awareness.js +230 -0
  372. package/src/page/blocks.js +113 -6
  373. package/src/page/blocksel.js +20 -4
  374. package/src/page/board-face.js +13 -0
  375. package/src/page/board-panel.js +5 -17
  376. package/src/page/board-style.js +2 -13
  377. package/src/page/board-view.js +1 -11
  378. package/src/page/board-views.js +3 -0
  379. package/src/page/board.js +2 -2
  380. package/src/page/boot-pack.js +26 -0
  381. package/src/page/boot.js +237 -0
  382. package/src/page/chart-tables.js +11 -0
  383. package/src/page/chart.js +6 -10
  384. package/src/page/checklist.js +1 -1
  385. package/src/page/checkpoint-proof.js +289 -0
  386. package/src/page/chrome.js +431 -63
  387. package/src/page/csv-import.js +17 -2
  388. package/src/page/document-title-dom.js +52 -0
  389. package/src/page/document-title-runtime.js +90 -0
  390. package/src/page/document-title.js +12 -0
  391. package/src/page/drag.js +48 -13
  392. package/src/page/drop.js +2 -1
  393. package/src/page/editor.js +399 -90
  394. package/src/page/emoji-data.js +162 -0
  395. package/src/page/feature-loader.js +72 -0
  396. package/src/page/features/board.js +21 -0
  397. package/src/page/features/chart.js +11 -0
  398. package/src/page/features/code.js +13 -0
  399. package/src/page/features/dates.js +180 -0
  400. package/src/page/features/emoji.js +187 -0
  401. package/src/page/features/media.js +290 -0
  402. package/src/page/features/mentions.js +188 -0
  403. package/src/page/features/registry.js +28 -0
  404. package/src/page/features/table.js +383 -0
  405. package/src/page/{tool-view.js → features/tool.js} +33 -2
  406. package/src/page/format-codec.js +215 -0
  407. package/src/page/format-command.js +9 -0
  408. package/src/page/format-gate.js +179 -0
  409. package/src/page/format-host.js +54 -0
  410. package/src/page/format-level.js +157 -0
  411. package/src/page/format-native-commands.js +142 -0
  412. package/src/page/format-native.js +42 -0
  413. package/src/page/highlight-slot.js +23 -0
  414. package/src/page/highlight.js +30 -16
  415. package/src/page/history-intent.js +78 -0
  416. package/src/page/inline.js +45 -28
  417. package/src/page/items.js +67 -14
  418. package/src/page/link.js +1 -0
  419. package/src/page/markdown.js +112 -3
  420. package/src/page/media-views.js +3 -0
  421. package/src/page/media.js +33 -245
  422. package/src/page/mention-picker.js +99 -0
  423. package/src/page/merge.js +1 -1
  424. package/src/page/outline-cache.js +11 -0
  425. package/src/page/placeholder.js +27 -7
  426. package/src/page/projection.js +240 -51
  427. package/src/page/reader-patch.js +306 -0
  428. package/src/page/recover.js +1 -1
  429. package/src/page/rich/page-tool.js +1 -4
  430. package/src/page/runtime-owner.js +27 -0
  431. package/src/page/save-projection.js +46 -4
  432. package/src/page/save-states.js +1 -1
  433. package/src/page/schema.js +47 -0
  434. package/src/page/table-rect-key.js +3 -0
  435. package/src/page/table-select.js +4 -3
  436. package/src/page/table-style.js +1 -1
  437. package/src/page/table-views.js +3 -0
  438. package/src/page/table.js +8 -293
  439. package/src/page/themes/content.css +25 -1
  440. package/src/page/themes/light-1.css +163 -2
  441. package/src/page/themes/light-1.source.css +269 -0
  442. package/src/page/tool-capability.js +3 -0
  443. package/src/page/tool-views.js +3 -0
  444. package/src/page/view-hooks.js +32 -0
  445. package/src/page/view.js +390 -107
  446. package/src/page/ybind/awareness.js +172 -0
  447. package/src/page/ybind/binding.js +40 -5
  448. package/src/page/ybind/bridge.js +50 -47
  449. package/src/page/ybind/decode.js +52 -19
  450. package/src/page/ybind/doc.js +27 -2
  451. package/src/page/ybind/group.js +12 -3
  452. package/src/page/ybind/index.js +2 -0
  453. package/src/page/ybind/lineage.js +71 -0
  454. package/src/page/ybind/marks.js +26 -1
  455. package/src/page/ybind/migrate.js +48 -13
  456. package/src/page/ybind/moves.js +32 -21
  457. package/src/page/ybind/positions.js +50 -0
  458. package/src/page/ybind/provider.js +97 -24
  459. package/src/page/ybind/recovery-proof.js +191 -0
  460. package/src/page/ybind/recovery-replay.js +488 -0
  461. package/src/page/ybind/tokens.js +15 -6
  462. package/src/page/ybind/walk.js +10 -3
  463. package/src/page/ybind/writer.js +48 -4
  464. package/src/plugins/ai-edit.js +167 -32
  465. package/src/plugins/bind-text.js +218 -0
  466. package/src/plugins/cms.js +93 -0
  467. package/src/plugins/indicator.js +57 -5
  468. package/src/plugins/live.js +319 -35
  469. package/src/plugins/menu/claygoo.svg.js +1 -0
  470. package/src/plugins/menu/seo-head.js +323 -0
  471. package/src/plugins/menu/seo.js +849 -0
  472. package/src/plugins/menu.js +425 -0
  473. package/src/plugins/page-view.js +131 -0
  474. package/src/plugins/page.js +1131 -270
  475. package/src/plugins/people.js +158 -31
  476. package/src/plugins/richclay.js +119 -9
  477. package/src/plugins/sortable.js +370 -60
  478. package/src/plugins/source.js +189 -366
  479. package/src/plugins/undo.js +289 -19
  480. package/src/plugins/unsent.js +132 -0
  481. package/src/plugins/upgrade-run.js +1 -0
  482. package/src/plugins/upgrade.js +210 -0
  483. package/src/plugins/upload-task.js +571 -0
  484. package/src/plugins/upload-ui.js +265 -0
  485. package/src/plugins/upload.js +34 -153
  486. package/src/plugins/visitor-drafts/inert.js +32 -0
  487. package/src/plugins/visitor-drafts/intro.js +2 -1
  488. package/src/plugins/visitor-drafts/owner-chip.js +3 -0
  489. package/src/plugins/visitor-drafts/prompt.js +2 -2
  490. package/src/plugins/visitor-drafts-owner.js +85 -0
  491. package/src/plugins/visitor-drafts.js +31 -42
  492. package/src/plugins/wire.js +82 -1
  493. package/src/reader-core.js +26 -0
  494. package/src/recovery/conflicts.js +287 -0
  495. package/src/recovery/identity.js +154 -0
  496. package/src/recovery/store.js +792 -0
  497. package/src/recovery/undo.js +257 -0
  498. package/src/server/checkpoint/index.js +68 -4
  499. package/src/server/offline-runtime/index.js +214 -0
  500. package/src/server/rooms/index.js +2 -1
  501. package/src/server/rooms/live-data-rooms.js +2 -2
  502. package/src/server/rooms/page-admission.js +33 -0
  503. package/src/server/rooms/page-lineage.js +56 -0
  504. package/src/server/rooms/page-rebase.js +11 -5
  505. package/src/server/rooms/page-state.js +2 -0
  506. package/src/server/rooms/page-write-admission.js +9 -3
  507. package/src/source/session.js +705 -0
  508. package/src/sync/conflict-revert.js +5 -4
  509. package/src/sync/conflicts-edit.js +119 -0
  510. package/src/sync/conflicts-facade.js +49 -0
  511. package/src/sync/conflicts.js +99 -1
  512. package/src/sync/editing-marker.js +37 -6
  513. package/src/sync/editing-sender.js +8 -1
  514. package/src/sync/live-sync-edit.js +63 -0
  515. package/src/sync/live-sync.js +364 -119
  516. package/src/sync/local-presence.js +187 -0
  517. package/src/sync/merge-declarations.js +10 -0
  518. package/src/sync/merge-engine.js +12 -0
  519. package/src/sync/presence-store.js +412 -0
  520. package/src/sync/presence-target.js +243 -0
  521. package/src/sync/presence.js +72 -41
  522. package/src/sync/saved-handoff.js +200 -0
  523. package/src/sync/saved-stream.js +544 -0
  524. package/src/sync/version-guard.js +92 -0
  525. package/src/sync/wire-profile.js +81 -0
  526. package/src/ui/bevel-controls.js +2 -8
  527. package/src/ui/page-scheme.js +7 -0
  528. package/src/ui/skins/cms.js +1 -1
  529. package/src/ui/theme-config.js +150 -0
  530. package/src/ui/theme.js +6 -138
  531. package/src/ui/toast-lazy.js +11 -0
  532. package/src/vendor/LICENSES-optional.md +253 -0
  533. package/src/vendor/highlight.vendor.js +3 -0
  534. package/src/vendor/hljs-bash.vendor.js +2 -0
  535. package/src/vendor/hljs-css.vendor.js +2 -0
  536. package/src/vendor/hljs-javascript.vendor.js +2 -0
  537. package/src/vendor/hljs-json.vendor.js +2 -0
  538. package/src/vendor/hljs-python.vendor.js +2 -0
  539. package/src/vendor/hljs-xml.vendor.js +2 -0
  540. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  541. package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
  542. package/src/vendor/hyper-morph-identity.vendor.js +2 -0
  543. package/src/vendor/hyper-morph-json.vendor.js +3 -0
  544. package/src/vendor/hyper-morph.vendor.js +6 -6
  545. package/src/vendor/hyper-undo.vendor.js +1 -1
  546. package/src/vendor/hypercms.vendor.js +169 -84
  547. package/src/vendor/marked.vendor.js +68 -0
  548. package/src/vendor/overtype.vendor.js +994 -0
  549. package/src/vendor/richclay.vendor.js +17 -17
  550. package/src/vendor/sap-core.vendor.js +5 -5
  551. package/src/vendor/sap-ownership.vendor.js +11 -7
@@ -0,0 +1,32 @@
1
+ // Saving a page as a file the browser downloads. Kept apart from the conflict copy
2
+ // builders so an editor that only offers "download this page" loads none of them.
3
+
4
+ export function documentName(pathname = location.pathname) {
5
+ const last = pathname.split("/").filter(Boolean).pop();
6
+ if (!last) return pathname.endsWith("/") || pathname === "" ? "index" : "page";
7
+ let name = last;
8
+ try { name = decodeURIComponent(last); } catch {}
9
+ return name.replace(/\.(html?|htmlclay)$/i, "") || "page";
10
+ }
11
+
12
+ export function downloadHtml({ html, name }) {
13
+ let url = null;
14
+ try {
15
+ url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
16
+ const a = document.createElement("a");
17
+ a.setAttribute("clay", "no-save no-watch no-snapshot");
18
+ a.href = url;
19
+ a.download = name;
20
+ a.style.setProperty("display", "none", "important");
21
+ document.body.appendChild(a);
22
+ a.click();
23
+ a.remove();
24
+ // Revoked late: Safari cancels a download whose URL goes away too soon.
25
+ setTimeout(() => URL.revokeObjectURL(url), 40000);
26
+ return true;
27
+ } catch (err) {
28
+ console.error("clayjs: could not build the download", err);
29
+ if (url) URL.revokeObjectURL(url);
30
+ return false;
31
+ }
32
+ }
@@ -1,28 +1,161 @@
1
1
  // Edit mode system - combines the editmode toggle with page type setting.
2
- // Loads in BOTH view and edit mode: toggleEditMode must exist in view mode
2
+ // Loads in BOTH view and edit mode: setMode must exist in view mode
3
3
  // (it's the way IN), and setPageTypeOnPageLoad marks <html> in either mode.
4
- import { isDraftMode, isEditMode, isOwner } from "./is-edit-mode.js";
4
+ import { canEdit, isDraftMode, isEditMode, isOwner, mode, modeState, registerModeParticipant } from "./is-edit-mode.js";
5
+ import { writeReceipt } from "./mode-receipt.js";
5
6
  import onDomReady from "../lib/dom-ready.js";
6
- import { addDocumentTransform } from "./snapshot.js";
7
7
 
8
- export function toggleEditMode() {
8
+ let leaving = false;
9
+
10
+ // The public registration. The built-ins own the `clay:` prefix (`clay:resources` and
11
+ // every module participant), so an application may not take an id from that space.
12
+ const register = (participant) => {
13
+ if (typeof participant?.id === "string" && participant.id.startsWith("clay:")) {
14
+ throw new TypeError(`clay.modeLifecycle: the id "${participant.id}" is reserved`);
15
+ }
16
+ return registerModeParticipant(participant);
17
+ };
18
+
19
+ export const modeLifecycle = {
20
+ register,
21
+ canWrite: () => modeState().phase === "idle" && isEditMode,
22
+ get state() {
23
+ return Object.freeze(modeState());
24
+ },
25
+ };
26
+
27
+ // The lazy dispatcher. The controller is a dynamic target, fetched by an explicit
28
+ // initial edit or the first live switch — never because a live declaration, a menu or
29
+ // a participant exists — and a rejected fetch is retried on the next ask.
30
+ let controller = null;
31
+
32
+ const fetchController = () => (controller ||= import("./mode-lifecycle.js").catch(() => (controller = null)));
33
+
34
+ // A live switch is a contract the document signs, not a hint. The loader records both
35
+ // resolved plans here only when `<html clay-mode-switch="live">` asked for one; the
36
+ // decision behind it and the controller that runs the transaction are a dynamic target,
37
+ // fetched by the first switch and never by the declaration itself.
38
+ let liveSwitch = null;
39
+
40
+ export const noteLiveSwitch = (boot) => {
41
+ liveSwitch = boot;
42
+ };
43
+
44
+ export async function activateInitialMode() {
45
+ if (!isEditMode) return;
46
+ try {
47
+ await (await fetchController()).activateInitial();
48
+ } catch (err) {
49
+ console.error("clayjs: the mode lifecycle failed to start:", err);
50
+ }
51
+ }
52
+
53
+ // One mode request at a time, whoever ends up handling it (the workspace shell, the live
54
+ // controller or a reload): asking for the same mode joins it, and asking for the other one
55
+ // is busy until it settles.
56
+ let pending = null;
57
+
58
+ export async function setMode(next) {
59
+ if (next !== "edit" && next !== "view") throw new TypeError(`clay.setMode: unknown mode "${next}"`);
60
+ if (pending) return pending.target === next ? pending.promise : { ok: false, mode, code: "busy" };
61
+ if (next === mode && !controller) return { ok: true, mode, changed: false };
62
+ if (isDraftMode) return { ok: false, mode, code: "not-allowed" };
63
+ const promise = request(next);
64
+ pending = { target: next, promise };
65
+ try {
66
+ return await promise;
67
+ } finally {
68
+ if (pending?.promise === promise) pending = null;
69
+ }
70
+ }
71
+
72
+ async function request(next) {
73
+ // In a workspace frame the shell owns the frame and decides permission, so a mode change
74
+ // is its reload, not ours: hand it over before anything local starts. Only a shell that
75
+ // does not take part ("host-unsupported") leaves the switch to this document.
76
+ const host = typeof window !== "undefined" ? window.clay?._requestHostMode : undefined;
77
+ if (typeof host === "function" && next !== mode) {
78
+ let answer;
79
+ try {
80
+ answer = await host(next);
81
+ } catch {
82
+ return { ok: false, mode, code: "host-failed" };
83
+ }
84
+ if (answer?.reason !== "host-unsupported") {
85
+ if (!answer?.ok) return { ok: false, mode, code: answer?.reason || "host-refused" };
86
+ leaving = true;
87
+ return { ok: true, mode: next, changed: true, reloading: true };
88
+ }
89
+ }
90
+ if (next === "edit" && mode !== "edit") return enterEdit();
91
+ if (!canEdit && !(next === "view" && mode === "edit")) return { ok: false, mode, code: "not-allowed" };
92
+ return switchTo(next);
93
+ }
94
+
95
+ async function enterEdit() {
96
+ const asked = location.pathname;
97
+ const { checkHostPermission } = await import("./host-permission.js");
98
+ const writable = await checkHostPermission({ fresh: true });
99
+ // The page moved to another document while the host answered: this request was for the old one.
100
+ if (location.pathname !== asked) return { ok: false, mode, code: "cancelled" };
101
+ if (writable === false) return { ok: false, mode, code: "permission-denied" };
102
+ if (!canEdit) return { ok: false, mode, code: "not-allowed" };
103
+ return switchTo("edit");
104
+ }
105
+
106
+ async function switchTo(next) {
107
+ if (liveSwitch) {
108
+ // A fetch that fails keeps the old way in: the guarded reload below.
109
+ const loaded = await fetchController();
110
+ const live = loaded && (await loaded.switchMode(next, liveSwitch));
111
+ if (live) return live;
112
+ }
113
+ if (next === mode) return { ok: true, mode, changed: false };
114
+ if (mode === "edit") {
115
+ let result;
116
+ try {
117
+ result = await window.clay?.save?.flush?.();
118
+ } catch (err) {
119
+ return { ok: false, mode, code: "save-failed", state: err?.state };
120
+ }
121
+ if (result && result.state && result.state !== "saved" && result.state !== "clean") {
122
+ return { ok: false, mode, code: "save-failed", state: result.state };
123
+ }
124
+ }
125
+ leaving = true;
126
+ writeReceipt({ mode: next, canEdit, internal: true });
127
+ location.reload();
128
+ return { ok: true, mode: next, changed: true, reloading: true };
129
+ }
130
+
131
+ function openEditParam() {
9
132
  const url = new URL(window.location.href);
10
- const newMode = isEditMode && !isDraftMode ? "false" : "true";
11
- url.searchParams.set('editmode', newMode);
133
+ url.searchParams.set('editmode', 'true');
12
134
  window.location.href = url.toString();
13
135
  }
14
136
 
15
- export function setViewerPageTypeBeforeSave () {
16
- addDocumentTransform(docElem => {
17
- docElem.setAttribute("editmode", "false");
18
- docElem.setAttribute("pageowner", "false");
19
- });
137
+ export const toggleEditMode = () =>
138
+ isDraftMode ? openEditParam() : setMode(mode === "edit" ? "view" : "edit");
139
+
140
+ function keepModeOnReload() {
141
+ if (!canEdit || isDraftMode) return;
142
+ window.addEventListener("pagehide", () => { if (!leaving) writeReceipt({ mode, canEdit, internal: false }); });
143
+ }
144
+
145
+ function markCanEdit() {
146
+ if (canEdit) {
147
+ document.documentElement.setAttribute("canedit", "true");
148
+ } else {
149
+ document.documentElement.removeAttribute("canedit");
150
+ }
20
151
  }
21
152
 
22
153
  export function setPageTypeOnPageLoad () {
23
154
  onDomReady(() => {
24
155
  document.documentElement.setAttribute("editmode", isEditMode ? "true" : "false");
25
156
  document.documentElement.setAttribute("pageowner", isOwner ? "true" : "false");
157
+ markCanEdit();
158
+ document.addEventListener("clay:capabilitychange", markCanEdit);
26
159
  if (isDraftMode) {
27
160
  document.documentElement.setAttribute("draftmode", "true");
28
161
  } else {
@@ -32,12 +165,12 @@ export function setPageTypeOnPageLoad () {
32
165
  }
33
166
 
34
167
  function init() {
35
- setViewerPageTypeBeforeSave();
36
168
  setPageTypeOnPageLoad();
169
+ keepModeOnReload();
37
170
  }
38
171
 
39
172
  // Auto-init when module is imported
40
173
  init();
41
174
 
42
- export { init, isEditMode, isOwner };
43
- export default { init, toggleEditMode, isEditMode, isOwner };
175
+ export { init, isEditMode, isOwner, canEdit, mode };
176
+ export default { init, setMode, toggleEditMode, isEditMode, isOwner, canEdit, mode };
package/src/core/etag.js CHANGED
@@ -103,6 +103,25 @@ export function forgetEtag() {
103
103
  recordEtag(null);
104
104
  }
105
105
 
106
+ // Only a real answer (spec is a number) says what the host supports: a refresh
107
+ // that failed comes back as a bare host with no extensions, and must not switch
108
+ // conditional saves off for the rest of the page's life.
109
+ function adoptHostExtensions(meta) {
110
+ if (typeof meta.spec === "number") conditional = meta.extensions.includes("conditional");
111
+ }
112
+
113
+ /**
114
+ * Learn from discovery whether this host offers conditional saves. The stamp is
115
+ * left alone: a session that only wants the capability must not move the version
116
+ * it is about to save against.
117
+ *
118
+ * @returns {Promise<boolean>}
119
+ */
120
+ export async function learnConditionalSaves() {
121
+ adoptHostExtensions(await hostMeta());
122
+ return conditional;
123
+ }
124
+
106
125
  /**
107
126
  * Learn from discovery whether this host offers conditional saves, and what the
108
127
  * document's current stamp is.
@@ -124,10 +143,7 @@ export function forgetEtag() {
124
143
  export async function seedEtag({ fresh = false, clearIfMissing = fresh } = {}) {
125
144
  const at = generation;
126
145
  const meta = await hostMeta({ fresh });
127
- // Only a real answer (spec is a number) says what the host supports: a refresh
128
- // that failed comes back as a bare host with no extensions, and must not switch
129
- // conditional saves off for the rest of the page's life.
130
- if (typeof meta.spec === "number") conditional = meta.extensions.includes("conditional");
146
+ adoptHostExtensions(meta);
131
147
 
132
148
  if (generation !== at) return lastSeen;
133
149
 
@@ -184,11 +200,13 @@ document.addEventListener("clay:sync-applied", (event) => {
184
200
  // Edit mode goes further: the stamp it SAVES with has to move too, and the host is the
185
201
  // only source for the value, so it forgets the one it holds and asks again with the
186
202
  // overwrite flags. A view-mode tab never saves, so its save stamp can stay where it is.
187
- if (isEditMode) {
188
- document.addEventListener("clay:sync-applied", (event) => {
189
- if (event.detail?.source !== "disk") return;
190
- if (typeof event.detail.etag === "string" && event.detail.etag) return;
191
- forgetEtag();
192
- seedEtag({ fresh: true });
193
- });
194
- }
203
+ // The mode is read when the frame arrives rather than when the listener is installed: a
204
+ // live entry imports this module in view, and this listener has to move the stamp once
205
+ // the edit session it belongs to has committed.
206
+ document.addEventListener("clay:sync-applied", (event) => {
207
+ if (!isEditMode) return;
208
+ if (event.detail?.source !== "disk") return;
209
+ if (typeof event.detail.etag === "string" && event.detail.etag) return;
210
+ forgetEtag();
211
+ seedEtag({ fresh: true });
212
+ });
@@ -27,8 +27,20 @@ const META_TIMEOUT_MS = 6000;
27
27
  // a host that failed to say who you are has not said you are nobody.
28
28
  const outcomes = new WeakMap();
29
29
 
30
- function settle(outcome, meta = bareHost()) {
30
+ // The path each answer was fetched for, captured when that fetch started. A document block
31
+ // describes one document, and a reply that lands after a route change answers for the one
32
+ // that asked.
33
+ const paths = new WeakMap();
34
+
35
+ // Which fetch produced each answer, counted from 1 in the order the fetches started. An
36
+ // answer that lands late is still older than one that started later.
37
+ const orders = new WeakMap();
38
+ let fetches = 0;
39
+
40
+ function settle(outcome, meta = bareHost(), asked = null, order = null) {
31
41
  outcomes.set(meta, outcome);
42
+ if (asked !== null) paths.set(meta, asked);
43
+ if (order !== null) orders.set(meta, order);
32
44
  return meta;
33
45
  }
34
46
 
@@ -42,6 +54,26 @@ export function hostMetaOutcome(meta) {
42
54
  return outcomes.get(meta) ?? "none";
43
55
  }
44
56
 
57
+ /**
58
+ * The path the fetch behind `meta` started on, or undefined when there is none.
59
+ *
60
+ * @param {Object} meta - an answer from hostMeta()
61
+ * @returns {string|undefined}
62
+ */
63
+ export function hostMetaPath(meta) {
64
+ return paths.get(meta);
65
+ }
66
+
67
+ /**
68
+ * Which fetch produced `meta`, counting from 1 in the order the fetches started, or 0 when none did.
69
+ *
70
+ * @param {Object} meta - an answer from hostMeta()
71
+ * @returns {number}
72
+ */
73
+ export function hostMetaOrder(meta) {
74
+ return orders.get(meta) ?? 0;
75
+ }
76
+
45
77
  // The bare-core-host answer, which is also every failure's answer.
46
78
  function bareHost() {
47
79
  return { spec: null, extensions: [], document: null };
@@ -101,6 +133,54 @@ export async function hostSupports(name) {
101
133
  return meta.extensions.includes(name);
102
134
  }
103
135
 
136
+ /**
137
+ * The page format this host enforces: 1 when it announced `page-format-v1`,
138
+ * otherwise 0.
139
+ *
140
+ * A host announces that extension only once its floor enforcement and tolerant
141
+ * checkpoint renderer are installed, so a host that cannot hold the floor keeps
142
+ * the newer writers off. The answer comes from the memoized discovery: one
143
+ * answer serves every asker under a loaded document.
144
+ *
145
+ * @returns {Promise<number>}
146
+ */
147
+ export async function hostPageFormat() {
148
+ const meta = await hostMeta();
149
+ return meta.extensions.includes("page-format-v1") ? 1 : 0;
150
+ }
151
+
152
+ /**
153
+ * Where this host serves the page state of one accepted version, or null.
154
+ *
155
+ * Two things have to be true: the host announced `page-recovery-v1` by name, and its
156
+ * document block named the resource as `pageState.href`. A host that has the resource
157
+ * but has not announced the extension is still launching it, and a client that inferred
158
+ * either one from the other would retire journaled work against a pair nobody promised.
159
+ * Absent, empty or not a string is null, which means no conditional read and no proof
160
+ * from one: the records stay.
161
+ *
162
+ * @returns {Promise<?string>}
163
+ */
164
+ export async function hostPageStateHref() {
165
+ const meta = await hostMeta();
166
+ if (!meta.extensions.includes("page-recovery-v1")) return null;
167
+ return meta.document?.pageState?.href || null;
168
+ }
169
+
170
+ /**
171
+ * Whether this host keeps a version history a person can go back to.
172
+ *
173
+ * The `document` block names it: absent means no history, which is the answer every
174
+ * host gave before the field existed. Read through discovery like every other
175
+ * capability, never inferred from the hostname or a probed route.
176
+ *
177
+ * @returns {Promise<boolean>}
178
+ */
179
+ export async function hostVersions() {
180
+ const meta = await hostMeta();
181
+ return meta.document?.versions === true;
182
+ }
183
+
104
184
  /** Drop the memoized answer. Tests only. */
105
185
  export function resetHostMeta() {
106
186
  inFlight = null;
@@ -183,8 +263,10 @@ export async function querySaveOutcome({ saveId, requestHash, signal } = {}) {
183
263
  }
184
264
 
185
265
  async function fetchMeta() {
266
+ const asked = location.pathname;
267
+ const order = ++fetches;
186
268
  // A file opened from disk has no host to ask, and fetch would only throw.
187
- if (!/^https?:$/.test(window.location.protocol)) return settle("none");
269
+ if (!/^https?:$/.test(window.location.protocol)) return settle("none", bareHost(), asked, order);
188
270
 
189
271
  const token = saveToken();
190
272
  // A host that mints tokens answers discovery per token, because on a sandboxed
@@ -211,25 +293,25 @@ async function fetchMeta() {
211
293
  redirect: "manual",
212
294
  signal: controller.signal
213
295
  });
214
- if (!res.ok) return settle(res.status >= 500 ? "failed" : "none");
296
+ if (!res.ok) return settle(res.status >= 500 ? "failed" : "none", bareHost(), asked, order);
215
297
  const text = await res.text();
216
- if (!text) return settle("none");
298
+ if (!text) return settle("none", bareHost(), asked, order);
217
299
 
218
300
  let body;
219
301
  try {
220
302
  body = JSON.parse(text);
221
303
  } catch (_) {
222
- return settle("none");
304
+ return settle("none", bareHost(), asked, order);
223
305
  }
224
- if (!body || typeof body !== "object" || typeof body.spec !== "number") return settle("none");
306
+ if (!body || typeof body !== "object" || typeof body.spec !== "number") return settle("none", bareHost(), asked, order);
225
307
 
226
308
  return settle("ok", {
227
309
  spec: body.spec,
228
310
  extensions: Array.isArray(body.extensions) ? body.extensions : [],
229
311
  document: body.document && typeof body.document === "object" ? body.document : null
230
- });
312
+ }, asked, order);
231
313
  } catch (_) {
232
- return settle("failed");
314
+ return settle("failed", bareHost(), asked, order);
233
315
  } finally {
234
316
  clearTimeout(timeoutId);
235
317
  }
@@ -0,0 +1,29 @@
1
+ import { hostMeta, hostMetaOrder, hostMetaOutcome, hostMetaPath } from "./host-meta.js";
2
+ import { noteHostWritable } from "./is-edit-mode.js";
3
+
4
+ let accepted = null;
5
+ let committed = 0;
6
+
7
+ // A permission answer counts only for the document it was fetched for and only when discovery
8
+ // answered: a failed refresh, or an answer with no document block, is not evidence either way,
9
+ // and leaves the last accepted answer standing. A host that offers no discovery at all ("none")
10
+ // is no answer, so the hints apply again. An older answer never overwrites a newer one.
11
+ export async function checkHostPermission({ fresh = false } = {}) {
12
+ const asked = location.pathname;
13
+ let meta = await hostMeta({ fresh });
14
+ if (location.pathname !== asked) return accepted;
15
+ if (hostMetaPath(meta) !== asked) {
16
+ meta = await hostMeta({ fresh: true });
17
+ if (location.pathname !== asked || hostMetaPath(meta) !== asked) return accepted;
18
+ }
19
+ const outcome = hostMetaOutcome(meta);
20
+ if (outcome === "failed" || (outcome === "ok" && !meta.document)) return accepted;
21
+ const writable = outcome === "ok" ? meta.document.writable : undefined;
22
+ // An answer from a fetch older than the one already committed is out of date.
23
+ const order = hostMetaOrder(meta);
24
+ if (order < committed) return accepted;
25
+ committed = order;
26
+ accepted = typeof writable === "boolean" ? writable : null;
27
+ noteHostWritable(accepted);
28
+ return accepted;
29
+ }
@@ -2,10 +2,14 @@ import cookie from "../lib/cookie.js";
2
2
  import query from "../lib/query.js";
3
3
  import { hasSaveToken, servedStaleToken, servedSiteVisibility } from "./host-attrs.js";
4
4
  import { draftEligible } from "./draft-mode.js";
5
+ import { takeReceipt } from "./mode-receipt.js";
5
6
 
6
- // Edit-mode precedence: an explicit ?editmode=true|false URL param wins, then an
7
- // authored `viewonly` attribute on <html> (a tool page that must never save its
8
- // runtime state, even on a host that hands it a save token), then an
7
+ // Two answers come out of here, both decided once, at module evaluation: may this
8
+ // person edit the document (`canEdit`), and does this tab start in edit (`mode`).
9
+ //
10
+ // canEdit is the edit-mode ladder, read as permission. An explicit ?editmode=true|false
11
+ // URL param wins, then an authored `viewonly` attribute on <html> (a tool page that must
12
+ // never save its runtime state, even on a host that hands it a save token), then an
9
13
  // opt-in window.clayEditMode global (with the legacy window.__hyperclayEditMode
10
14
  // still honored as a fallback for older standalone embedders — htmlclay itself
11
15
  // uses its injected save token plus the admin cookie, not this global), then
@@ -16,6 +20,13 @@ import { draftEligible } from "./draft-mode.js";
16
20
  // comes before ?editmode=true unless the owner cookie is present, and before the
17
21
  // owner cookie otherwise.
18
22
  //
23
+ // mode is the narrower question: permission alone no longer opens a document for
24
+ // editing, so an owner reads every document the way a visitor does. A tab starts
25
+ // editing only when something explicit asks for it: a receipt this tab left for its
26
+ // own reload (the person's choice in this tab, newer than the URL it was served with),
27
+ // an ?editmode=true param, window.clayEditMode = true, or an authored
28
+ // <html opens-in="edit"> for a tool page. clay.setMode('edit') is the way in.
29
+ //
19
30
  // The token sits above the cookie because it is the stronger claim and often the
20
31
  // only one visible: the host minted it for this response and nothing else, while
21
32
  // the cookie is ambient. A sandboxed document cannot read the cookie at all, so
@@ -52,23 +63,145 @@ const isDraftMode =
52
63
  forcedEditMode == null &&
53
64
  !hasSaveToken();
54
65
 
55
- const isEditMode = isDraftMode
56
- ? true
66
+ const receipt = typeof window !== "undefined" ? takeReceipt() : null;
67
+ const opensInEdit =
68
+ typeof document !== "undefined" && document.documentElement.getAttribute("opens-in") === "edit";
69
+
70
+ // May this person edit here? The old rule, read as permission.
71
+ const hostCanEdit = query.editmode
72
+ ? query.editmode === "true" || (!isViewOnlyDocument && !servedStaleToken() && (forcedEditMode ?? (hasSaveToken() || ownerCookie)))
73
+ : isViewOnlyDocument || servedStaleToken()
74
+ ? false
75
+ : forcedEditMode != null
76
+ ? forcedEditMode
77
+ : hasSaveToken() || ownerCookie;
78
+ const keptCanEdit =
79
+ receipt?.canEdit === true && forcedEditMode !== false && !servedStaleToken() &&
80
+ !(isViewOnlyDocument && query.editmode !== "true");
81
+ const hintCanEdit = hostCanEdit || keptCanEdit;
82
+ // What the document or the URL itself rules out; a host's yes never overrides these.
83
+ const authoredRestriction = isDraftMode || servedStaleToken() || forcedEditMode === false ||
84
+ query.editmode === "false" || (isViewOnlyDocument && query.editmode !== "true");
85
+ let canEdit = hintCanEdit;
86
+
87
+ // The host's own answer to "may this caller save here": false takes edit away, true grants
88
+ // it unless the document rules it out, and no answer leaves the hints in charge.
89
+ export function noteHostWritable(writable) {
90
+ const next = writable === false ? false : writable === true ? hintCanEdit || !authoredRestriction : hintCanEdit;
91
+ if (next === canEdit) return;
92
+ canEdit = next;
93
+ if (typeof document !== "undefined") document.dispatchEvent(new Event("clay:capabilitychange"));
94
+ }
95
+
96
+ // Does this tab start editing? Only when something explicit asks for it.
97
+ const startsEditing = receipt
98
+ ? receipt.mode === "edit"
57
99
  : query.editmode
58
- ? query.editmode === "true" // takes precedence over viewonly, the global, token and cookie
59
- : isViewOnlyDocument || servedStaleToken()
60
- ? false
61
- : forcedEditMode != null
62
- ? forcedEditMode
63
- : hasSaveToken() || ownerCookie;
100
+ ? query.editmode === "true"
101
+ : forcedEditMode === true || opensInEdit;
64
102
 
103
+ // Live bindings, not constants: the lifecycle controller commits a transition by
104
+ // rewriting them here, and every facade reads them through a getter, so a boolean
105
+ // copied onto `clay` at boot can never go stale.
106
+ let isEditMode = isDraftMode ? true : canEdit && startsEditing;
107
+ let mode = isDraftMode ? "draft" : isEditMode ? "edit" : "view";
65
108
  const isOwner = ownerCookie;
66
109
 
110
+ // The rest of `clay.modeLifecycle.state`: where edits land, which transition is
111
+ // running, and why the last one refused. The controller is the only writer.
112
+ const lifecycle = {
113
+ phase: "idle",
114
+ destination: isDraftMode ? "draft" : "host",
115
+ generation: 0,
116
+ reason: null,
117
+ };
118
+
119
+ export function commitModeFacts(next) {
120
+ if (next.mode !== undefined) mode = next.mode;
121
+ if (next.isEditMode !== undefined) {
122
+ isEditMode = next.isEditMode;
123
+ if (typeof window !== "undefined") window.__hyperclayEditMode = isEditMode;
124
+ }
125
+ if (next.canEdit !== undefined) canEdit = next.canEdit;
126
+ for (const key in lifecycle) if (next[key] !== undefined) lifecycle[key] = next[key];
127
+ }
128
+
129
+ export const modeState = () => ({ mode, ...lifecycle });
130
+
131
+ // A module that starts a session when it evaluates belongs to the session that was idle when
132
+ // it landed: during any transition the committed mode is still the one being left, so a late
133
+ // import would start an editor no exit holds a lease over.
134
+ export function startsAtImport() {
135
+ return isEditMode && modeState().phase === "idle";
136
+ }
137
+
138
+ // The one registration queue, on `window.clay`: the API, a preboot push and a later raw
139
+ // push all land in the array the controller drains. Non-enumerable, because it is a
140
+ // handoff and not a member of the facade a page boots with.
141
+ const queue = (() => {
142
+ if (typeof window === "undefined" || !window.clay) return [];
143
+ if (window.clay.modeParticipants) return window.clay.modeParticipants;
144
+ Object.defineProperty(window.clay, "modeParticipants", { value: [], writable: true, configurable: true });
145
+ return window.clay.modeParticipants;
146
+ })();
147
+ const registered = new Set();
148
+
149
+ // The readiness check reads the queue without draining it. A registration taken back also
150
+ // leaves the controller's roster, but only when the controller is already loaded.
151
+ let forgetFromRoster = () => {};
152
+ export const noteParticipantForget = (fn) => (forgetFromRoster = fn);
153
+ export const peekModeParticipants = () => queue.slice();
154
+
155
+ const refuse = (message) => {
156
+ throw new TypeError(`clay.modeLifecycle: ${message}`);
157
+ };
158
+
159
+ const refuseUnlessIdleInView = () => {
160
+ if (lifecycle.phase !== "idle" || isEditMode) refuse("the page is not idle in view");
161
+ };
162
+
163
+ // A blocked page is a view with a reason: the transition that refused is over and the page
164
+ // is not idle only so the refusal can be read. A participant that arrives then is recorded
165
+ // for the next entry, which is the answer the block already gave, so it is not a second
166
+ // refusal on top of it.
167
+ const refuseUnlessAnEntryCanTakeIt = () => {
168
+ if (lifecycle.phase === "blocked" && !isEditMode) return;
169
+ refuseUnlessIdleInView();
170
+ };
171
+
172
+ export function takeModeParticipants() {
173
+ registered.clear();
174
+ return queue.splice(0);
175
+ }
176
+
177
+ export function registerModeParticipant(participant) {
178
+ const id = participant && participant.id;
179
+ if (typeof id !== "string" || !id || typeof participant.prepare !== "function") refuse("a participant needs an id and a prepare()");
180
+ if (registered.has(id)) refuse(`duplicate participant id "${id}"`);
181
+ refuseUnlessAnEntryCanTakeIt();
182
+ registered.add(id);
183
+ queue.push(participant);
184
+ // `live`, not `registered`: a drain clears that set while the roster keeps the
185
+ // participant, so an unregister after the first drain still has to reach the roster.
186
+ let live = true;
187
+ return () => {
188
+ if (!live) return;
189
+ refuseUnlessAnEntryCanTakeIt();
190
+ live = false;
191
+ registered.delete(id);
192
+ const index = queue.indexOf(participant);
193
+ if (index !== -1) queue.splice(index, 1);
194
+ forgetFromRoster(id);
195
+ };
196
+ }
197
+
67
198
  const siteVisibility = servedSiteVisibility;
68
199
 
69
200
  export {
70
201
  isDraftMode,
71
202
  isEditMode,
72
203
  isOwner,
73
- siteVisibility
204
+ siteVisibility,
205
+ canEdit,
206
+ mode
74
207
  }