@panphora/clayjs 1.15.2 → 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
@@ -1,715 +1,158 @@
1
- import { isEditMode } from "./is-edit-mode.js";
2
- import { rowOf } from "./conflict-presentation.js";
3
- import { downloadRecovery } from "./conflict-download.js";
4
1
  import onDomReady from "../lib/dom-ready.js";
5
- import { set, make } from "../lib/hostile-css.js";
2
+ import { isEditMode } from "./is-edit-mode.js";
6
3
  import { isSaveConflicted } from "./save.js";
7
- import { gateCaptureToken } from "../lib/dirty-gate.js";
8
- import { reloadAfterDiscard } from "./unsaved-warning.js";
9
- import { bevelButton, bevelIconButton, bevelSurface, bevelWell, bevelText, protectIcon, RUNTIME_ONLY } from "../ui/bevel-controls.js";
10
- import { TOKENS, FONT_SANS, FONT_MONO, GLYPHS } from "../ui/bevel.js";
11
-
12
- // One notice for everything this tab holds that the page no longer shows: edits
13
- // another save replaced (the ledger, clay.conflicts) and a save the host refused.
14
- // It never decides for the person. Minimize, Escape, a save, a reconnect or a later
15
- // frame leave every loss where it is; only Accept theirs (or Writer's own merge)
16
- // acknowledges one, and only the ids the panel actually drew.
17
- //
18
- // Ledger changes draw on the next frame, not at once: a listener that claims its
19
- // own records inside clay:sync-applied (Writer) must get there before the first
20
- // draw, or the bar flashes a count that is not the person's to deal with.
21
-
22
- export const WARN = "light-dark(#D08A1E, #E3A33F)";
23
- const ARM_MS = 5000;
24
- const TOAST_MS = 4000;
25
- const DOUBLE_PRESS_MS = 500;
26
-
27
- // The host names what moved the file when it knows. Anything unrecognised falls
28
- // back to the phrase that is true in every case.
29
- const SOURCES = {
30
- "another-tab": "in another tab",
31
- "another-person": "by someone else",
32
- "an-agent": "by an agent",
33
- };
34
-
35
- const MINIMIZE_SVG = '<svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true"><path d="M2.5 9.5h7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square"/></svg>';
36
-
37
- const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
38
-
39
- const BLOCKED_ROW = "This spot changed again. Review the latest edit or download your copy.";
40
- const DATA_BLOCKED_ROW = "A data value can't be put back from here. Download your copy to keep yours.";
41
-
42
- let view = "hidden";
43
- let listOpen = false;
44
- let renderedIds = [];
45
- const seenIds = new Set();
46
- const freshIds = new Set();
47
- let refused = null;
48
- let refusalGeneration = 0;
49
- let armed = null;
50
- let armTimer = null;
51
- let busy = null;
52
- let keepFailed = false;
53
- let downloaded = false;
54
- let downloadFailed = false;
55
- const blockedRows = new Map();
56
- let ledgerRevision = 0;
57
- let frameQueued = false;
58
- let checkRefusal = false;
59
- let toastText = null;
60
- let toastTimer = null;
61
- let pendingFocus = null;
62
- let armedAt = 0;
63
- let armLost = false;
64
- let keepAttempt = 0;
65
- let lastAnnounced = null;
66
- let panelEl = null;
67
-
68
- let root = null;
69
- let wellEl = null;
70
- let keyed = new Map();
71
-
4
+ import { storage } from "../lib/doc-storage.js";
5
+
6
+ // The notice itself lives in conflict-notice-ui.js and is 6.8 KB Brotli: most edit
7
+ // boots have nothing for it to show, so this shell is what every one of them downloads
8
+ // instead. It watches the events that can create something to show and, on the first
9
+ // one, fetches the notice once and hands it that event, which it replays as if it had
10
+ // been listening all along. From then on the notice owns every listener; this file
11
+ // adds no state and draws nothing. Conflicts a reload left in the recovery store have
12
+ // no event to announce them, so a boot that holds some asks for the notice at once.
72
13
  const ledger = () => window.clay?.conflicts || null;
73
- const losses = () => ledger()?.list().filter((r) => !r.claimedBy && r.kind !== "apply-incomplete") ?? [];
74
- const failures = () => ledger()?.list().filter((r) => !r.claimedBy && r.kind === "apply-incomplete") ?? [];
75
-
76
- // A refused save goes first: saving is blocked until it is answered.
77
- function mode() {
78
- if (refused) return "refused";
79
- if (losses().length) return "replaced";
80
- if (failures().length) return "failed";
81
- return null;
82
- }
83
-
84
- function el(tag, rules, text) {
85
- const node = make(tag, rules, text);
86
- if (rules[0] === "all:initial") set(node, "color-scheme", "inherit");
87
- node.setAttribute("clay", RUNTIME_ONLY);
88
- return node;
89
- }
90
-
91
- function key(node, name) {
92
- keyed.set(name, node);
93
- return node;
94
- }
95
-
96
- function schemeOf() {
97
- const s = getComputedStyle(document.documentElement).colorScheme;
98
- return s === "light" || s === "dark" ? s : "light dark";
99
- }
100
-
101
- // A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to
102
- // the layout viewport, which parks a bottom-anchored bar behind the keyboard at the
103
- // exact moment somebody is typing. Lift it by the difference instead.
104
- function place() {
105
- if (!root) return;
106
- const vv = window.visualViewport;
107
- const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
108
- set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
109
- if (panelEl) set(panelEl, "max-height", `${Math.max(160, (vv?.height || window.innerHeight) - 32)}px`);
110
- }
111
-
112
- function build() {
113
- root = el("div", [
114
- "all:initial", "position:fixed", "left:50%", "transform:translateX(-50%)",
115
- "z-index:2147483001", "display:none", "flex-direction:column", "align-items:center",
116
- "max-width:calc(100vw - 24px)", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`,
117
- "text-align:left",
118
- ]);
119
- root.setAttribute("data-clay-conflict", "");
120
- document.body.appendChild(root);
121
- window.visualViewport?.addEventListener("resize", place);
122
- window.visualViewport?.addEventListener("scroll", place);
123
- }
124
-
125
- export function schedule() {
126
- if (frameQueued) return;
127
- frameQueued = true;
128
- const run = () => { frameQueued = false; render(); };
129
- if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
130
- else setTimeout(run, 16);
131
- }
132
-
133
- function render() {
134
- if (checkRefusal) {
135
- checkRefusal = false;
136
- if (refused && !isSaveConflicted()) refused = null;
137
- }
138
- const m = mode();
139
- if (!m) {
140
- view = "hidden";
141
- listOpen = false;
142
- seenIds.clear();
143
- freshIds.clear();
144
- renderedIds = [];
145
- disarm();
146
- lastAnnounced = null;
147
- armLost = false;
148
- } else if (view === "hidden") {
149
- view = "bar";
150
- }
151
- if (!root) {
152
- if (view === "hidden" && !toastText) return;
153
- if (!document.body) return;
154
- build();
155
- }
156
- // A body replaced wholesale takes the root with it.
157
- if (!root.isConnected && document.body) document.body.appendChild(root);
158
-
159
- const active = document.activeElement;
160
- let focusKey = null;
161
- for (const [name, node] of keyed) if (node === active) focusKey = name;
162
- const scroll = wellEl ? wellEl.scrollTop : 0;
163
- keyed = new Map();
164
- wellEl = null;
165
- panelEl = null;
166
- root.replaceChildren();
167
- set(root, "color-scheme", schemeOf());
168
-
169
- if (toastText) {
170
- root.append(drawToast(toastText));
171
- } else if (view === "bar") {
172
- root.append(drawBar(m));
173
- } else if (view === "panel") {
174
- root.append(drawPanel(m));
175
- }
176
- set(root, "display", toastText || view !== "hidden" ? "flex" : "none");
177
- if (wellEl) wellEl.scrollTop = scroll;
178
-
179
- const want = pendingFocus || focusKey;
180
- if (want && keyed.has(want)) keyed.get(want).focus({ preventScroll: true });
181
- // A request for a control the toast is standing in for waits until the toast goes.
182
- if (!toastText) pendingFocus = null;
183
- place();
184
- }
185
-
186
- function dot() {
187
- return el("span", ["all:initial", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${WARN}`]);
188
- }
189
-
190
- function refusedSource() {
191
- return SOURCES[refused?.changedBy] || null;
192
- }
193
-
194
- function barMessage(m, n) {
195
- if (m === "refused") {
196
- const named = refusedSource();
197
- if (!named && refused.afterTimeout) {
198
- return "This page changed elsewhere, possibly by your own save that timed out. Your edits here are safe.";
199
- }
200
- return `This page changed ${named || "elsewhere"}. Your edits here are safe.`;
201
- }
202
- if (m === "failed") return "A sync update did not finish. Your edits here are safe.";
203
- return `Another edit replaced ${plural(n, "change")}.`;
204
- }
205
-
206
- function titleOf(m, n) {
207
- if (m === "refused") return `This page changed ${refusedSource() || "elsewhere"}`;
208
- if (m === "failed") return "A sync update did not finish";
209
- return `Another edit replaced ${plural(n, "change")}`;
210
- }
211
-
212
- function drawToast(text) {
213
- const t = bevelSurface("div", ["display:block", `border-left:3px solid ${TOKENS.teal}`, "padding:10px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
214
- t.setAttribute("role", "status");
215
- t.textContent = text;
216
- return t;
217
- }
218
-
219
- function drawBar(m) {
220
- const n = m === "failed" ? failures().length : losses().length;
221
- const bar = bevelSurface("div", ["display:flex", "flex-wrap:wrap", "align-items:center", "gap:10px 12px", "padding:8px 8px 8px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
222
- // A redraw rebuilds the bar; only a new message is announced again.
223
- const message = barMessage(m, n);
224
- if (message !== lastAnnounced) {
225
- bar.setAttribute("role", m === "refused" ? "alert" : "status");
226
- lastAnnounced = message;
227
- }
228
- bar.append(dot(), bevelText("span", [], message), key(bevelButton("Review", { onClick: openPanel }), "review"));
229
- return bar;
230
- }
231
-
232
- function action(label, name, { variant = "default", onClick, push = false } = {}) {
233
- const phone = window.innerWidth < 480;
234
- const extra = phone
235
- ? ["flex:1 1 100%", ...(variant === "primary" ? ["order:-1"] : [])]
236
- : (push ? ["margin-left:auto"] : []);
237
- return key(bevelButton(label, { variant, onClick, extra }), name);
238
- }
239
-
240
- function lede(text) {
241
- return bevelText("div", ["display:block", "padding:0 16px 12px", `color:${TOKENS["ink-2"]}`, "font-size:13.5px"], text);
242
- }
243
-
244
- function drawPanel(m) {
245
- const rows = m === "replaced" ? losses() : m === "failed" ? failures() : [];
246
- const held = m === "refused" ? losses() : rows;
247
- if (seenIds.size) for (const r of rows) if (!seenIds.has(r.id)) freshIds.add(r.id);
248
- for (const id of [...freshIds]) if (!rows.some((r) => r.id === id)) freshIds.delete(id);
249
-
250
- const title = titleOf(m, rows.length);
251
- const maxHeight = Math.max(160, (window.visualViewport?.height || window.innerHeight) - 32);
252
- const panel = bevelSurface("div", ["display:flex", "flex-direction:column", "width:520px", "max-width:calc(100vw - 24px)", `max-height:${maxHeight}px`]);
253
- panelEl = panel;
254
- panel.setAttribute("role", "dialog");
255
- panel.setAttribute("aria-label", title);
256
-
257
- const head = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", "padding:12px 12px 8px 16px", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`]);
258
- const t = key(bevelText("b", ["font-weight:600", "font-size:14.5px", "outline:none"], title), "title");
259
- t.setAttribute("tabindex", "-1");
260
- head.append(dot(), t);
261
- if (m === "replaced" && freshIds.size) {
262
- head.append(bevelText("span", [
263
- `font:600 10.5px/1 ${FONT_MONO}`, "letter-spacing:.12em", "text-transform:uppercase", "padding:3px 6px",
264
- `color:color-mix(in srgb, ${TOKENS.brass} 80%, ${TOKENS.ink})`, `background:${TOKENS["brass-soft"]}`,
265
- ], `${freshIds.size} new`));
266
- }
267
- const minimizeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
268
- const minimize = key(bevelIconButton(MINIMIZE_SVG, { label: "Minimize", onClick: minimizePanel }), "minimize");
269
- minimize.title = "Minimize to the bar. Nothing is accepted until you choose.";
270
- minimizeWrap.append(minimize);
271
- head.append(minimizeWrap);
272
- panel.append(head);
273
-
274
- if (m === "refused") {
275
- panel.append(lede("Your edits here are safe and not saved yet; nothing will be overwritten until you choose. Their version is not on this page, so there is no list of edits to compare. Keep yours, which saves over theirs, or accept theirs, which reloads the page and drops your unsaved edits."));
276
- if (held.length) {
277
- panel.append(lede(`${plural(held.length, "earlier replaced edit")} ${held.length === 1 ? "is" : "are"} also held in this tab. Download my copy includes them.`));
278
- }
279
- if (keepFailed) panel.append(lede("That did not save either. Your edits here are still here."));
280
- if (armLost) panel.append(lede("Something changed on the page after your first press, so nothing was dropped. Press Accept theirs again if you still want their version."));
281
- if (armed) {
282
- const warn = el("div", [
283
- "all:initial", "display:block", "margin:0 16px 12px", "padding:8px 10px",
284
- `border-left:3px solid ${WARN}`, `background:color-mix(in srgb, ${WARN} 14%, transparent)`,
285
- `color:${TOKENS.ink}`, `font:13px/1.5 ${FONT_SANS}`,
286
- ]);
287
- warn.append(el("b", ["all:initial", "font:inherit", "font-weight:700", "color:inherit"], "This drops your unsaved edits."), document.createTextNode(" Download your copy first if you might want them. Press again to confirm."));
288
- panel.append(warn);
289
- }
290
- } else if (m === "failed") {
291
- panel.append(lede("Part of an update from elsewhere could not be applied, so this page may be missing some of it. Your edits here are safe. Download a copy of the page as you had it, or accept to keep the page as it is."));
292
- } else {
293
- panel.append(lede("The page is showing the other edit. This stays until you choose. You can also save a copy of the page as you had it."));
294
- }
295
-
296
- const actions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]);
297
- const dl = action(downloadFailed ? "Try again" : downloaded ? "Downloaded" : "Download my copy", "download", { onClick: onDownload });
298
- actions.append(dl);
299
- if (m === "refused") {
300
- const keep = action(busy === "overwrite" ? "Saving…" : keepFailed ? "Try again" : "Keep mine", "keep", { onClick: onKeep });
301
- keep.setDisabled(busy === "overwrite");
302
- actions.append(keep, action(armed ? "Yes, drop my edits" : "Accept theirs", "accept", { variant: "primary", onClick: onRefusedAccept, push: true }));
303
- } else {
304
- if (m === "replaced") {
305
- const pending = ledger().hasPendingApply();
306
- const put = action(pending ? "Finishing the incoming edit…" : busy === "revert" ? "Putting back…" : "Revert to mine", "revert", { onClick: onRevert });
307
- put.setDisabled(pending || busy === "revert");
308
- actions.append(put);
309
- }
310
- actions.append(action("Accept theirs", "accept", { variant: "primary", onClick: onAccept, push: true }));
311
- }
312
- panel.append(actions);
313
-
314
- if (m === "replaced") {
315
- if (!listOpen) {
316
- const more = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", `border-top:1px solid ${TOKENS.line}`, "padding:8px 10px 8px 16px", `color:${TOKENS.muted}`, `font:13px/1.5 ${FONT_SANS}`]);
317
- const seeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
318
- seeWrap.append(key(bevelButton("See the edits", { small: true, onClick: toggleList }), "list"));
319
- more.append(el("span", ["all:initial", "font:inherit", "color:inherit"], plural(rows.length, "edit")), seeWrap);
320
- panel.append(more);
321
- } else {
322
- panel.append(drawWell(rows));
323
- }
324
- }
325
14
 
326
- renderedIds = held.map((r) => r.id);
327
- for (const r of rows) seenIds.add(r.id);
328
- return panel;
329
- }
330
-
331
- function drawWell(rows) {
332
- // The sunk ground, not the recess: the recess's lighter blue glares against the panel in dark mode.
333
- const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`]);
334
- const head = el("div", ["all:initial", "position:sticky", "top:0", "z-index:1", "display:flex", "align-items:center", "gap:8px", "padding:8px 12px", `background:${TOKENS.sunk}`, `border-bottom:1px solid ${TOKENS["line-2"]}`, `color:${TOKENS.muted}`, `font:600 10.5px/1 ${FONT_MONO}`, "letter-spacing:.12em", "text-transform:uppercase"]);
335
- const hideWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
336
- hideWrap.append(key(bevelButton("Hide", { small: true, variant: "quiet", onClick: toggleList }), "list"));
337
- head.append(el("span", ["all:initial", "font:inherit", "color:inherit", "letter-spacing:inherit", "text-transform:inherit"], "Replaced edits"), hideWrap);
338
- well.append(head);
339
- rows.forEach((r, i) => well.append(buildRow(r, i)));
340
- wellEl = well;
341
- return well;
342
- }
343
-
344
- function valueLine(label, value, { mine }) {
345
- const cells = [bevelText("span", [`color:${TOKENS.faint}`, `font-family:${FONT_SANS}`], label)];
346
- const box = bevelText("span", [
347
- "display:-webkit-box", "-webkit-line-clamp:2", "-webkit-box-orient:vertical", "overflow:hidden",
348
- "overflow-wrap:anywhere",
349
- ...(mine
350
- ? [`color:${TOKENS.ink}`]
351
- : [`color:${TOKENS.muted}`]),
352
- ]);
353
- const full = (value.cutBefore ? "…" : "") + value.before + value.hit + value.after + (value.cutAfter ? "…" : "");
354
- box.title = full;
355
- if (value.before) box.append(bevelText("span", [`color:${TOKENS.faint}`], (value.cutBefore ? "…" : "") + value.before));
356
- const hit = bevelText("span", mine
357
- ? [`background:color-mix(in srgb, ${WARN} 18%, transparent)`, "padding:0 4px"]
358
- : (value.before || value.after
359
- ? ["text-decoration:underline", `text-decoration-color:color-mix(in srgb, ${WARN} 70%, transparent)`, "text-underline-offset:3px"]
360
- : []), value.hit || "(empty)");
361
- box.append(hit);
362
- if (value.after) box.append(bevelText("span", [`color:${TOKENS.faint}`], value.after + (value.cutAfter ? "…" : "")));
363
- cells.push(box);
364
- return cells;
365
- }
366
-
367
- function buildRow(record, index) {
368
- const row = rowOf(record);
369
- const box = bevelText("div", ["display:block", "padding:8px 12px 10px", ...(index ? [`border-top:1px solid ${TOKENS.line}`] : [])]);
370
- const where = bevelText("div", ["display:flex", "align-items:center", "gap:4px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"]);
371
- where.append(bevelText("span", [], row.name));
372
- if (row.target && row.target.isConnected) where.append(key(eyeButton(row), `eye:${record.id}`));
373
- box.append(where);
374
- if (row.sentence) box.append(bevelText("div", ["display:block", "font-size:13px", `color:${TOKENS["ink-2"]}`], row.sentence));
375
- if (blockedRows.has(record.id)) box.append(bevelText("div", ["display:block", "font-size:13px", "margin-top:4px", `color:${WARN}`], blockedRows.get(record.id)));
376
- if (row.yours) {
377
- const grid = bevelText("div", ["display:grid", "grid-template-columns:44px 1fr", "gap:3px 10px", "align-items:start", `font:12.5px/1.5 ${FONT_MONO}`, ...(row.sentence ? ["margin-top:4px"] : [])]);
378
- grid.append(...valueLine("Yours", row.yours, { mine: true }));
379
- if (row.now) grid.append(...valueLine("Now", row.now, { mine: false }));
380
- box.append(grid);
381
- }
382
- return box;
383
- }
384
-
385
- function eyeButton(row) {
386
- const base = ["all:initial", "color-scheme:inherit", "box-sizing:border-box", "cursor:pointer", "display:inline-grid", "place-items:center", "width:22px", "height:20px", `color:${TOKENS.ink}`];
387
- const b = el("button", base);
388
- b.type = "button";
389
- b.innerHTML = GLYPHS.conflictEye;
390
- protectIcon(b.firstElementChild, 16);
391
- b.setAttribute("aria-label", `Show ${row.name} on page`);
392
- b.title = "Show on page";
393
- const paint = (extra) => { b.style.cssText = ""; for (const r of [...base, ...extra]) { const i = r.indexOf(":"); b.style.setProperty(r.slice(0, i), r.slice(i + 1), "important"); } };
394
- b.addEventListener("pointerenter", () => paint([`background:color-mix(in srgb, ${WARN} 16%, transparent)`]));
395
- b.addEventListener("pointerleave", () => paint([]));
396
- b.addEventListener("focus", () => paint([`outline:2px solid ${TOKENS.brass}`, "outline-offset:1px"]));
397
- b.addEventListener("blur", () => paint([]));
398
- b.addEventListener("click", () => ring(row.target));
399
- return b;
400
- }
401
-
402
- // The eye: scroll the element into view and draw a ring over it. The ring is ours
403
- // and runtime-only; the page's element gets no attribute, class or style, so the
404
- // dirty gate and the saved bytes see nothing.
405
- let ringEl = null;
406
- let ringTimer = null;
407
- let ringTarget = null;
408
-
409
- function dropRing() {
410
- clearTimeout(ringTimer);
411
- window.removeEventListener("scroll", placeRing, true);
412
- window.removeEventListener("resize", placeRing);
413
- ringEl?.remove();
414
- ringEl = null;
415
- ringTarget = null;
416
- }
417
-
418
- function placeRing() {
419
- if (!ringEl || !ringTarget) return;
420
- const r = ringTarget.getBoundingClientRect();
421
- set(ringEl, "left", `${r.left}px`);
422
- set(ringEl, "top", `${r.top}px`);
423
- set(ringEl, "width", `${r.width}px`);
424
- set(ringEl, "height", `${r.height}px`);
425
- }
15
+ let loading = false;
16
+ let notice = null; // the notice module, once it has been fetched
17
+ let listeners = null; // the armed handlers, all of them on one controller
426
18
 
427
- function ring(target) {
428
- dropRing();
429
- if (!target || !target.isConnected) return;
430
- const still = !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
431
- const behavior = still ? "instant" : "smooth";
432
- // The notice sits at the bottom of the viewport: centre the target in what is left
433
- // above it, or the outline lands on the panel instead of the text.
434
- const top = root?.getBoundingClientRect().top ?? 0;
435
- const cover = root && top > 0 ? Math.max(0, window.innerHeight - top) : 0;
436
- if (!cover) {
437
- target.scrollIntoView?.({ block: "center", behavior });
438
- } else {
439
- // Centre first, so a page whose own scroller holds the target still brings it
440
- // up; then move the window the rest of the way. A target taller than the free
441
- // space shows its top instead.
442
- target.scrollIntoView?.({ block: "center", behavior: "instant" });
443
- const r = target.getBoundingClientRect();
444
- const free = window.innerHeight - cover;
445
- const shift = r.height > free - 24 ? r.top - 12 : r.top + r.height / 2 - free / 2;
446
- window.scrollBy?.({ top: shift, behavior });
447
- }
448
- ringTarget = target;
449
- ringEl = el("div", ["all:initial", "color-scheme:inherit", "position:fixed", "pointer-events:none", "z-index:2147483001", `outline:2px solid ${WARN}`, "outline-offset:3px", `background:color-mix(in srgb, ${WARN} 14%, transparent)`]);
450
- document.body.appendChild(ringEl);
451
- placeRing();
452
- window.addEventListener("scroll", placeRing, true);
453
- window.addEventListener("resize", placeRing);
454
- ringTimer = setTimeout(dropRing, 2500);
455
- }
456
-
457
- function openPanel() {
458
- view = "panel";
459
- pendingFocus = "title";
460
- render();
461
- }
462
-
463
- function minimizePanel() {
464
- dropRing();
465
- view = "bar";
19
+ function load(trigger) {
20
+ if (loading) return;
21
+ loading = true;
466
22
  disarm();
467
- pendingFocus = "review";
468
- render();
469
- }
470
-
471
- function toggleList() {
472
- listOpen = !listOpen;
473
- if (listOpen) freshIds.clear();
474
- render();
475
- }
476
-
477
- // The ids copied before anything else runs: a record that landed after the draw was never shown, so this choice does not cover it, and a record something claimed since then is its owner's to settle.
478
- function onAccept() {
479
- const l = ledger();
480
- const ids = renderedIds.filter((id) => { const r = l?.get(id); return r && !r.claimedBy; });
481
- if (l && ids.length) l.acknowledge(ids, { reason: "accepted" });
482
- view = mode() ? "bar" : "hidden";
483
- pendingFocus = "review";
484
- schedule();
485
- }
486
-
487
- // Revert to mine. The ids are the rendered ones, copied before anything runs, as
488
- // Accept reads them. The module that does the work loads on first use, and only
489
- // here, where the sync plugin has published the ledger. What it could not put back
490
- // keeps its row, with the reason under it, until the ledger moves on.
491
- async function onRevert() {
492
- const l = ledger();
493
- if (busy || !l || l.hasPendingApply()) return;
494
- const ids = renderedIds.filter((id) => { const r = l.get(id); return r && !r.claimedBy; });
495
- if (!ids.length) return;
496
- busy = "revert";
497
- render();
498
- // Focus that lands outside the notice while the revert and its save run is the
499
- // person moving on; Review takes focus afterwards only if it stayed here.
500
- let stayed = true;
501
- const track = (e) => { stayed = !!root?.contains(e.target); };
502
- document.addEventListener("focus", track, true);
503
- let result;
504
- try {
505
- const { revertConflicts } = await import("../sync/conflict-revert.js");
506
- result = await revertConflicts(ids);
507
- } catch (err) {
508
- console.error("[clay] Revert to mine did not finish", err);
509
- result = { revertedIds: [], blockedIds: ids, saveResult: null };
510
- } finally {
511
- document.removeEventListener("focus", track, true);
512
- }
513
- busy = null;
514
- for (const id of result.blockedIds) blockedRows.set(id, ledger()?.get(id)?.kind === "json" ? DATA_BLOCKED_ROW : BLOCKED_ROW);
515
- view = mode() ? "bar" : "hidden";
516
- pendingFocus = stayed ? "review" : null;
517
- const saving = result.saveResult && (result.saveResult.ok || result.saveResult.msg === "Save already in progress");
518
- if (result.revertedIds.length && saving) toast(`Put back ${plural(result.revertedIds.length, "change")}. Saving.`);
519
- else render();
23
+ import("./conflict-notice-ui.js").then(
24
+ (module) => {
25
+ notice = module;
26
+ module.start(trigger);
27
+ },
28
+ (error) => {
29
+ console.error("clayjs: the conflict notice could not be downloaded; it will try again on the next conflict", error);
30
+ loading = false;
31
+ arm();
32
+ }
33
+ );
520
34
  }
521
35
 
522
- let downloadedTimer = null;
523
-
524
- // Download my copy. Nothing here acknowledges or releases a record.
525
- function onDownload() {
526
- const l = ledger();
527
- // A refusal ends in a reload that takes every record with it, so its copy carries
528
- // all of them; a replaced notice covers the rows it drew.
529
- const records = refused ? (l?.list() ?? []) : renderedIds.map((id) => l?.get(id)).filter(Boolean);
530
- const ok = downloadRecovery(records, { refused: !!refused });
531
- downloadFailed = !ok;
532
- downloaded = ok;
533
- clearTimeout(downloadedTimer);
534
- if (ok) downloadedTimer = setTimeout(() => { downloaded = false; schedule(); }, 1200);
535
- render();
36
+ // Arming twice is arming once, so a boot, an event that arrived while the notice was
37
+ // being fetched and a mode lease can all ask for the same handlers.
38
+ function arm() {
39
+ listeners ||= new AbortController();
40
+ const { signal } = listeners;
41
+ document.addEventListener("clay:sync-conflicts-changed", onLedgerChanged, { signal });
42
+ document.addEventListener("clay:save-conflict", onSaveConflict, { signal });
43
+ document.addEventListener("clay:document-upgraded", onUpgraded, { signal });
44
+ document.addEventListener("clay:undo-kept", onUndoKept, { signal });
536
45
  }
537
46
 
538
47
  function disarm() {
539
- clearTimeout(armTimer);
540
- armTimer = null;
541
- armed = null;
48
+ listeners?.abort();
49
+ listeners = null;
542
50
  }
543
51
 
544
- async function onKeep() {
545
- if (busy) return;
546
- const attempt = ++keepAttempt;
547
- busy = "overwrite";
548
- keepFailed = false;
549
- disarm();
550
- render();
551
- let result;
552
- try {
553
- result = await window.clay?.save?.overwrite?.();
554
- } catch {
555
- result = { ok: false };
556
- }
557
- // A newer attempt, a new refusal or a landed save already settled this one.
558
- if (attempt !== keepAttempt || busy !== "overwrite") return;
559
- if (!result?.ok && isSaveConflicted()) {
560
- // Left showing: the save still has not happened.
561
- busy = null;
562
- keepFailed = true;
563
- }
564
- schedule();
565
- }
566
-
567
- // Two presses. The first is a choice, not a confirmation, and this is the one
568
- // control here that throws away work nothing can bring back. The second only
569
- // counts if nothing changed in between: a new refusal, a new loss or new typing
570
- // each disarm it.
571
- function onRefusedAccept() {
572
- if (!armed) {
573
- armed = { refusalGeneration, ledgerRevision, dirtyGeneration: gateCaptureToken().gen };
574
- armedAt = Date.now();
575
- armLost = false;
576
- clearTimeout(armTimer);
577
- armTimer = setTimeout(() => { disarm(); schedule(); }, ARM_MS);
578
- render();
579
- return;
580
- }
581
- // The second half of a double click is not a second decision.
582
- if (Date.now() - armedAt < DOUBLE_PRESS_MS) return;
583
- const token = armed;
584
- const current = () =>
585
- armed === token &&
586
- token.refusalGeneration === refusalGeneration &&
587
- token.ledgerRevision === ledgerRevision &&
588
- token.dirtyGeneration === gateCaptureToken().gen;
589
- if (!reloadAfterDiscard({ isCurrent: current, reload: () => window.location.reload() })) {
590
- disarm();
591
- armLost = true;
592
- render();
593
- }
594
- }
595
-
596
- export function toast(text) {
597
- toastText = text;
598
- clearTimeout(toastTimer);
599
- toastTimer = setTimeout(() => { toastText = null; schedule(); }, TOAST_MS);
600
- render();
601
- }
602
-
603
- function onLedgerChanged() {
604
- ledgerRevision++;
605
- if (armed) disarm();
606
- for (const id of [...blockedRows.keys()]) if (!ledger()?.get(id)) blockedRows.delete(id);
607
- schedule();
52
+ function onLedgerChanged(e) {
53
+ if (!ledger()?.size) return;
54
+ load(e);
608
55
  }
609
56
 
610
57
  function onSaveConflict(e) {
611
- const d = e?.detail || {};
612
- refused = { generation: ++refusalGeneration, changedBy: d.changedBy, afterTimeout: !!d.afterTimeout, etag: d.etag ?? null };
613
- // The refusal answers the save a toast may still be announcing; it replaces the
614
- // toast at once.
615
- toastText = null;
616
- clearTimeout(toastTimer);
617
- disarm();
618
- busy = null;
619
- keepFailed = false;
620
- keepAttempt++;
621
- armLost = false;
622
- schedule();
623
- }
624
-
625
- // An old save event cannot erase a newer refusal: only a tab that is no longer
626
- // holding one clears it. Losses are untouched; a save never acknowledges.
627
- function onSaveSettled(e) {
628
- if (isSaveConflicted()) {
629
- // save.js fires clay:save-saved just before it releases the hold, in the same
630
- // task; look again once it has.
631
- if (e.type === "clay:save-saved") queueMicrotask(() => { if (!isSaveConflicted()) onSaveSettled(e); });
632
- return;
633
- }
634
- const keptMine = busy === "overwrite" && e.type === "clay:save-saved";
635
- if (refused || busy) {
636
- refused = null;
637
- busy = null;
638
- keepFailed = false;
639
- armLost = false;
640
- disarm();
641
- }
642
- if (keptMine) toast("Saved your version over theirs.");
643
- else schedule();
644
- }
645
-
646
- // On a manual page, conflictResolvedBySync can release the hold without firing
647
- // clay:save-conflict-resolved. Checked a frame later, after the apply finished.
648
- function onSyncApplied() {
649
- if (!refused) return;
650
- checkRefusal = true;
651
- schedule();
652
- }
653
-
654
- // conflictResolvedBySync released the hold on a manual page with unsaved work:
655
- // no save happened and no clay:save-conflict-resolved fired, but their version
656
- // is on this page now, so the refusal is over.
657
- function onConflictReleased() {
658
- if (!refused || isSaveConflicted()) return;
659
- refused = null;
660
- busy = null;
661
- keepFailed = false;
662
- armLost = false;
663
- disarm();
664
- schedule();
665
- }
666
-
667
- function onKeydown(e) {
668
- if (e.key !== "Escape") return;
669
- if (armed) {
670
- e.preventDefault();
671
- disarm();
672
- render();
673
- return;
674
- }
675
- if (view !== "panel") return;
676
- const a = document.activeElement;
677
- if (a && a !== document.body && !root?.contains(a)) return;
678
- e.preventDefault();
679
- minimizePanel();
680
- }
681
-
682
- function onInput(e) {
683
- if (!armed || root?.contains(e.target)) return;
684
- disarm();
685
- schedule();
686
- }
687
-
688
- // Focus that leaves the notice while a toast stands in for it is the person moving
689
- // on: the control the toast replaced does not take focus back afterwards.
690
- function onFocus(e) {
691
- if (toastText && pendingFocus && !root?.contains(e.target)) pendingFocus = null;
692
- }
693
-
694
- function init() {
58
+ load(e);
59
+ }
60
+
61
+ // The document itself was upgraded somewhere else: the notice is drawn for this
62
+ // too, because it is the one thing on the page that can offer a reload.
63
+ function onUpgraded(e) {
64
+ load(e);
65
+ }
66
+
67
+ // A refused step this tab kept is offered the way a replaced edit is: the bar with
68
+ // Review, and the panel behind it lists the copy. A keep the store refused stays in
69
+ // memory, so the notice is not raised for it.
70
+ function onUndoKept(e) {
71
+ if (e?.detail?.ok !== true) return;
72
+ load(e);
73
+ }
74
+
75
+ // A conflict this tab held before a reload lives in the recovery store, not in the
76
+ // ledger. That store keeps one small count key for exactly this question: it is read
77
+ // here, and the module behind it is fetched only when the count says something is
78
+ // stored, so a boot with nothing stored downloads no recovery code. A kept undo step
79
+ // keeps its own count the same way, and the notice it asks for reads both.
80
+ const HINT_KEY = "conflicts";
81
+ const UNDO_HINT_KEY = "undo";
82
+
83
+ const hinted = (name) => storage.local.get(name)?.count > 0;
84
+
85
+ function keptFromEarlier() {
86
+ if (hinted(UNDO_HINT_KEY)) load(null);
87
+ if (!hinted(HINT_KEY)) return;
88
+ import("../recovery/conflicts.js").then(
89
+ (module) => {
90
+ if (!loading && module.pendingConflicts() > 0) load(null);
91
+ },
92
+ () => {}
93
+ );
94
+ }
95
+
96
+ // A conflict still unresolved when the page leaves edit would go with the session:
97
+ // saving is blocked until it is answered, and the answer needs the editor. A save the
98
+ // host refused is one of those, and so is a loss the ledger is still holding for this
99
+ // tab. A change the recovery store kept from an earlier session is a copy this page can
100
+ // still hand back, not input it is holding, so it never stands in the way.
101
+ function unresolved() {
102
+ const held = (ledger()?.list?.() ?? []).filter((record) => !record.claimedBy).map((record) => record.id);
103
+ if (held.length) return held;
104
+ return isSaveConflicted() ? ["save"] : [];
105
+ }
106
+
107
+ /**
108
+ * The lazy notice as a mode lease.
109
+ *
110
+ * The shell and the notice it fetches are editor-only: they exist to answer a conflict,
111
+ * and the answer needs the editing lane. `quiesce` refuses an exit over a conflict that
112
+ * is still unresolved and leaves the notice standing exactly as it is, which is what a
113
+ * refusal has to keep showing; `resume` has nothing to reopen. `initial: true` adopts the
114
+ * shell and the notice the boot already armed, and arms nothing again. `dispose` (after
115
+ * the barrier) takes back the shell's handlers and stops the notice, so a later frame in
116
+ * view draws nothing.
117
+ */
118
+ export function conflictNoticeParticipant() {
119
+ return {
120
+ id: "clay:conflict-notice",
121
+ order: 960, // last: it reads the save lane's refusal and every peer's losses
122
+ prepare(context = {}) {
123
+ return {
124
+ enable() {
125
+ if (context.initial === true) return;
126
+ if (!notice && !loading) arm();
127
+ },
128
+ quiesce() {
129
+ const pending = unresolved();
130
+ if (!pending.length) return;
131
+ throw Object.assign(new Error("clay.conflict-notice: a conflict here is still unresolved"), {
132
+ code: "conflict",
133
+ pending,
134
+ });
135
+ },
136
+ resume() {},
137
+ dispose() {
138
+ disarm();
139
+ loading = false;
140
+ notice?.stop?.();
141
+ notice = null;
142
+ },
143
+ };
144
+ },
145
+ };
146
+ }
147
+
148
+ onDomReady(() => {
695
149
  if (!isEditMode) return;
696
- document.addEventListener("clay:sync-conflicts-changed", onLedgerChanged);
697
- // An apply beginning or ending changes only whether Revert may run.
698
- document.addEventListener("clay:unsaved-state-changed", schedule);
699
- document.addEventListener("clay:save-conflict", onSaveConflict);
700
- document.addEventListener("clay:save-saved", onSaveSettled);
701
- document.addEventListener("clay:save-conflict-resolved", onSaveSettled);
702
- document.addEventListener("clay:sync-applied", onSyncApplied);
703
- document.addEventListener("clay:save-conflict-released", onConflictReleased);
704
- document.addEventListener("keydown", onKeydown);
705
- document.addEventListener("input", onInput, true);
706
- document.addEventListener("focus", onFocus, true);
707
- if (ledger()?.size) schedule();
708
- }
709
-
710
- onDomReady(init);
150
+ if (ledger()?.size) load(null);
151
+ else {
152
+ // Armed first, always: a conflict can arrive while the hint is still being read.
153
+ arm();
154
+ keptFromEarlier();
155
+ }
156
+ });
711
157
 
712
- export const _test = {
713
- state: () => ({ view, listOpen, renderedIds: renderedIds.slice(), refused, armed, busy, ledgerRevision }),
714
- render,
715
- };
158
+ export { conflictNoticeParticipant as modeParticipant };