@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
@@ -5,15 +5,18 @@
5
5
  // --page-* tokens in editor.js (contract: default-page/v1/look/THEME-CONTRACT.md).
6
6
  import { state, commands } from "../vendor/prosemirror.vendor.js";
7
7
  import { schema } from "./schema.js";
8
- import { BlockSelection, outline, siblingSpan, moveBlock, indentBlocks, outdentBlocks } from "./blocks.js";
8
+ import { BlockSelection, siblingSpan, moveBlock, indentBlocks, outdentBlocks } from "./blocks.js";
9
+ import { outlineOf } from "./outline-cache.js";
9
10
  import { currentSpan, selectBlocks, duplicateBlocks, deleteBlocks, asStep, typeIntoBlocks } from "./blocksel.js";
10
11
  import { openAltEditor, setImageAttrs } from "./media.js";
11
12
  import { BlockDrag } from "./drag.js";
12
13
  import { openLinkEditor } from "./link.js";
13
- import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnInto, insertAfter, rankItems, runItem, retype, insertItemsFor } from "./items.js";
14
- import toast from "../ui/toast.js";
15
- import { insertChartAfterTable } from "./chart.js";
14
+ import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnInto, insertAfter, rankItems, runItem, retype, canTurnInto, insertItemsFor, loadChartPart } from "./items.js";
15
+ import toast from "../ui/toast-lazy.js";
16
16
  import { pickCsv } from "./csv-import.js";
17
+ import { HISTORY_INTENT, typingIntent } from "./history-intent.js";
18
+ import { composing } from "./markdown.js";
19
+ import { announce } from "./announce.js";
17
20
 
18
21
  export { BLOCK_TYPES, blockTypeOf, turnInto, insertAfter } from "./items.js";
19
22
 
@@ -24,7 +27,7 @@ const N = schema.nodes;
24
27
  const MOD = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl+";
25
28
 
26
29
  const selectedRoots = (st) => {
27
- const ents = outline(st.doc);
30
+ const ents = outlineOf(st.doc);
28
31
  const { lo, hi } = st.selection.span(st.doc);
29
32
  return ents.slice(lo, hi + 1).filter((e) => e.up === ents[lo].up);
30
33
  };
@@ -32,6 +35,30 @@ const selectedRoots = (st) => {
32
35
  const imageId = (view, index) => view.state.doc.child(index).attrs.id;
33
36
  const blockStart = (doc, index) => { let at = 0; for (let i = 0; i < index; i++) at += doc.child(i).nodeSize; return at; };
34
37
 
38
+ // The editor attributes a menu lends it while it is open. A slash menu puts them
39
+ // back as it found them; every other menu clears them.
40
+ const POPUP_ATTRS = ["aria-haspopup", "aria-controls", "aria-activedescendant"];
41
+ const TARGET_GONE = "That block is no longer available.";
42
+ const CANNOT_CONVERT = "This block holds content from a newer version, so it can't be converted here.";
43
+
44
+ // The part of the window the person can see: the visual viewport when the
45
+ // browser has one, so a keyboard or a pinch does not hide the bar.
46
+ export function phoneViewport(win) {
47
+ const v = win.visualViewport;
48
+ return v ? { left: v.offsetLeft, top: v.offsetTop, width: v.width, height: v.height }
49
+ : { left: 0, top: 0, width: win.innerWidth, height: win.innerHeight };
50
+ }
51
+
52
+ export function phoneBarBox(v, width, height, safeBottom = 0) {
53
+ const inset = PHONE_INSET;
54
+ const w = Math.min(width, Math.max(0, v.width - inset * 2));
55
+ return {
56
+ left: v.left + (v.width - w) / 2,
57
+ top: Math.max(v.top + inset, v.top + v.height - height - safeBottom - inset),
58
+ width: w,
59
+ };
60
+ }
61
+
35
62
  export function blockLink(id, loc = globalThis.location) {
36
63
  return `${loc.origin}${loc.pathname}${loc.search}#${encodeURIComponent(id)}`;
37
64
  }
@@ -58,7 +85,7 @@ export const ACTION_ITEMS = [
58
85
  { key: "small", label: "Small", icon: "▫", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 240 }) },
59
86
  { key: "medium", label: "Medium", icon: "◻", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 480 }) },
60
87
  { key: "full", label: "Full width", icon: "⬜", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: null }) },
61
- { key: "chart", label: "Chart this table", icon: "📊", only: "table", run: (view, chrome, index) => insertChartAfterTable(view, blockStart(view.state.doc, index)) },
88
+ { key: "chart", label: "Chart this table", icon: "📊", only: "table", run: (view, chrome, index) => { const at = blockStart(view.state.doc, index); return loadChartPart(view).then(({ insertChartAfterTable }) => insertChartAfterTable(view, at)); } },
62
89
  { key: "csv", label: "Import CSV…", icon: "⇧", only: "table", focus: false, run: (view, chrome, index) => pickCsv(view, null, view.state.doc.child(index).attrs.id) },
63
90
  ];
64
91
 
@@ -83,6 +110,7 @@ const ICONS = {
83
110
  bullet: svg('<path d="M6.5 4.5h7M6.5 8h7M6.5 11.5h7"/><path stroke-width="2.4" d="M3 4.5h.01M3 8h.01M3 11.5h.01"/>'),
84
111
  ordered: svg('<path d="M7 4.5h6.5M7 8h6.5M7 11.5h6.5M2.5 3.5l1-.5v3M2.5 9.3c.2-.5.6-.8 1.1-.8.6 0 1 .4 1 .9 0 .9-2.1 1.4-2.1 2.6h2.1"/>'),
85
112
  check: svg('<rect x="2.5" y="2.5" width="11" height="11" rx="2.5"/><path d="M5.5 8.2l1.8 1.8 3.4-4"/>'),
113
+ more: svg('<path stroke-width="2.2" d="M3 8h.01M8 8h.01M13 8h.01"/>'),
86
114
  };
87
115
  const TOOLS = [
88
116
  ["strong", "Bold"],
@@ -94,6 +122,11 @@ const TOOLS = [
94
122
  ["check", "Checklist"],
95
123
  ["code", "Code"],
96
124
  ];
125
+ // The four controls a phone toolbar keeps: bold, italic, link and the menu that
126
+ // holds everything else. Wider screens show every tool.
127
+ const PHONE_TOOLS = new Set(["strong", "em", "link"]);
128
+ const MORE_ACTIONS = new Set(["up", "down", "indent", "outdent"]);
129
+ const PHONE_BAR_WIDTH = 204, PHONE_BAR_HEIGHT = 52, PHONE_INSET = 8;
97
130
  const LIST_KINDS = new Set(["bullet", "ordered", "check"]);
98
131
  const LINE_TYPES = new Set(["paragraph", "heading", "list_item"]);
99
132
 
@@ -173,11 +206,21 @@ function runList(view, kind) {
173
206
  const st = view.state;
174
207
  const off = listActive(st, kind);
175
208
  const item = BLOCK_TYPES.find((b) => b.key === (off ? "text" : kind));
209
+ const idx = listable(st).filter((i) => off || !isKind(st.doc.child(i), kind));
210
+ if (idx.some((i) => !canTurnInto(st.doc.child(i), item))) {
211
+ announce(view, CANNOT_CONVERT);
212
+ return;
213
+ }
176
214
  const tr = st.tr;
177
- for (const i of listable(st)) if (off || !isKind(tr.doc.child(i), kind)) retype(tr, i, item);
215
+ for (const i of idx) retype(tr, i, item);
178
216
  if (tr.docChanged) view.dispatch(asStep(tr));
179
217
  }
180
218
 
219
+ // The four formats this release adds are commands the format gate may refuse, and
220
+ // a refusal is a notice rather than a change. The menu that offers them, and the
221
+ // commands behind it, load the first time one is chosen (format-native-commands.js).
222
+ const nativeMenu = (view, chrome, name) => import("./format-native-commands.js").then((menu) => menu.nativeMenu(view, name, chrome.more));
223
+
181
224
  function runMark(view, name) {
182
225
  if (name === "link") return openLinkEditor(view);
183
226
  const st = view.state;
@@ -185,6 +228,7 @@ function runMark(view, name) {
185
228
  const lines = name === "code" && !code ? wholeLines(st) : null;
186
229
  if (LIST_KINDS.has(name)) runList(view, name);
187
230
  else if (code) view.dispatch(asStep(codeLinesTr(st, code)).scrollIntoView());
231
+ else if (lines && lines.some((i) => !canTurnInto(st.doc.child(i), BLOCK_TYPES.find((b) => b.key === "code")))) announce(view, CANNOT_CONVERT);
188
232
  else if (lines) view.dispatch(asStep(codeBlockTr(st, lines)).scrollIntoView());
189
233
  else commands.toggleMark(M[name])(st, view.dispatch);
190
234
  view.focus();
@@ -198,7 +242,7 @@ const el = (tag, cls, attrs = {}) => {
198
242
  };
199
243
 
200
244
  function toolButton(name, label) {
201
- const b = el("button", "page-tool", { type: "button", "data-cmd": name, "aria-label": label, title: label });
245
+ const b = el("button", `page-tool${PHONE_TOOLS.has(name) ? " page-tool-primary" : " page-tool-desk"}`, { type: "button", "data-cmd": name, "aria-label": label, title: label });
202
246
  b.innerHTML = ICONS[name];
203
247
  return b;
204
248
  }
@@ -208,6 +252,35 @@ let menuCount = 0;
208
252
  export const chromeKey = new PluginKey("page-chrome");
209
253
  const chromeFor = new WeakMap();
210
254
 
255
+ // A range a slash menu chose, held in plugin state so it follows the document
256
+ // while the action it belongs to waits for code to load.
257
+ export const slashRangeKey = new PluginKey("page-slash-range");
258
+ const slashRangeState = {
259
+ init: () => null,
260
+ apply(tr, value) {
261
+ let held = value;
262
+ if (held && tr.docChanged) {
263
+ const from = tr.mapping.map(held.from, 1), to = tr.mapping.map(held.to, -1);
264
+ held = from <= to && tr.doc.textBetween(from, to) === held.text ? { ...held, from, to } : null;
265
+ }
266
+ const m = tr.getMeta(slashRangeKey);
267
+ if (m !== undefined) held = m;
268
+ return held;
269
+ },
270
+ };
271
+
272
+ export const slashRangePlugin = () => new Plugin({ key: slashRangeKey, state: slashRangeState });
273
+
274
+ export const holdSlashRange = (view, range) => {
275
+ const text = range ? view.state.doc.textBetween(range.from, range.to) : null;
276
+ view.dispatch(view.state.tr.setMeta(slashRangeKey, range ? { ...range, text } : null));
277
+ };
278
+
279
+ export const heldSlashRange = (st) => {
280
+ const held = slashRangeKey.getState(st);
281
+ return held ? { from: held.from, to: held.to } : null;
282
+ };
283
+
211
284
  export function openPageMenu(view, anchor, items) {
212
285
  const chrome = chromeFor.get(view);
213
286
  if (!chrome) return false;
@@ -223,12 +296,21 @@ class Chrome {
223
296
  this.onAiTool = onAiTool;
224
297
  this.shell = view.dom.parentElement;
225
298
  this.menu = null;
299
+ this.priorAria = null;
226
300
  this.movedTimer = 0;
227
301
  this.hover = null;
302
+ this.pendingHover = null;
303
+ this.win = view.dom.ownerDocument.defaultView ?? globalThis;
304
+ this.frame = null;
305
+ this.destroyed = false;
306
+ this.probe = null;
307
+ this.roving = null;
228
308
 
229
309
  this.dock = el("div", "page-dock", { role: "toolbar", "aria-label": "Formatting" });
230
- this.turn = el("button", "page-tool page-tool-turn", { type: "button", "aria-haspopup": "menu" });
231
- this.dock.append(this.turn, el("span", "page-tool-sep"), ...TOOLS.map(([n, l]) => toolButton(n, l)));
310
+ this.turn = el("button", "page-tool page-tool-turn page-tool-desk", { type: "button", "aria-haspopup": "menu" });
311
+ this.more = el("button", "page-tool page-tool-primary page-tool-more", { type: "button", "aria-label": "More", title: "More", "aria-haspopup": "menu" });
312
+ this.more.innerHTML = ICONS.more;
313
+ this.dock.append(this.turn, el("span", "page-tool-sep page-tool-desk"), ...TOOLS.map(([n, l]) => toolButton(n, l)), this.more);
232
314
  this.plus = el("button", "page-plus", { type: "button", "aria-label": "Add a block", title: "Add a block" });
233
315
  this.plus.innerHTML = ICONS.plus;
234
316
  this.grip = el("button", "page-grip", { type: "button", "aria-label": "Block actions", title: "Drag to move, click for actions", "aria-haspopup": "menu" });
@@ -250,6 +332,8 @@ class Chrome {
250
332
  });
251
333
  }
252
334
  this.turn.addEventListener("click", () => this.toggleMenu("turn", this.turn));
335
+ this.more.addEventListener("click", () => this.toggleMenu("more", this.more));
336
+ this.dock.addEventListener("keydown", (e) => this.dockKey(e));
253
337
  this.plus.addEventListener("click", () => this.fromPlus());
254
338
  this.grip.addEventListener("click", (e) => {
255
339
  if (e.detail === 0) this.gripAction(this.gutterIndex());
@@ -272,10 +356,11 @@ class Chrome {
272
356
  this.onKey = () => {
273
357
  clearTimeout(this.movedTimer);
274
358
  this.shell.classList.remove("is-moved");
359
+ this.pendingHover = null;
275
360
  this.setHover(null);
276
361
  };
277
- this.onLeave = () => this.setHover(null);
278
- this.onResize = () => this.place();
362
+ this.onLeave = () => { this.pendingHover = null; this.setHover(null); };
363
+ this.onResize = () => this.scheduleLayout();
279
364
  this.onDocDown = (e) => {
280
365
  if (!this.menu) return;
281
366
  if ([this.menuEl, this.plus, this.grip, this.turn].some((part) => part.contains(e.target))) return;
@@ -285,7 +370,10 @@ class Chrome {
285
370
  this.shell.addEventListener("mousemove", this.onMove);
286
371
  this.shell.addEventListener("mouseleave", this.onLeave);
287
372
  this.shell.addEventListener("keydown", this.onKey);
288
- window.addEventListener("resize", this.onResize);
373
+ this.win.addEventListener("resize", this.onResize);
374
+ this.win.addEventListener("scroll", this.onResize, true);
375
+ this.win.visualViewport?.addEventListener("resize", this.onResize);
376
+ this.win.visualViewport?.addEventListener("scroll", this.onResize);
289
377
  this.update(view, null);
290
378
  }
291
379
 
@@ -295,11 +383,107 @@ class Chrome {
295
383
  return Math.min(st.doc.resolve(pos).index(0), st.doc.childCount - 1);
296
384
  }
297
385
 
386
+ isPhone() {
387
+ return !!this.win?.matchMedia?.("(max-width: 560px)").matches;
388
+ }
389
+
298
390
  gutterIndex() {
299
- if (this.view.dom.ownerDocument.defaultView.matchMedia?.("(max-width: 560px)").matches) return this.blockIndex();
391
+ if (this.isPhone()) return this.blockIndex();
392
+ this.settleHover();
300
393
  return Math.min(this.hover ?? this.blockIndex(), this.view.state.doc.childCount - 1);
301
394
  }
302
395
 
396
+ dockControls() {
397
+ const all = [...this.dock.querySelectorAll("button")].filter((b) => this.rendered(b));
398
+ return this.isPhone() ? all.filter((b) => b.classList.contains("page-tool-primary")) : all;
399
+ }
400
+
401
+ // Every transaction asks whether the roving control is still usable: one style read for
402
+ // that control, not one for every button in the dock.
403
+ inRoving(control) {
404
+ if (!control || !this.dock.contains(control)) return false;
405
+ if (this.isPhone() && !control.classList.contains("page-tool-primary")) return false;
406
+ return this.rendered(control);
407
+ }
408
+
409
+ rendered(control) {
410
+ if (control.hidden) return false;
411
+ const style = this.win?.getComputedStyle?.(control);
412
+ return !style || style.display !== "none";
413
+ }
414
+
415
+ setRoving(target) {
416
+ this.roving = target;
417
+ const roving = new Set(this.dockControls());
418
+ for (const b of this.dock.querySelectorAll("button")) b.tabIndex = b === target && roving.has(b) ? 0 : -1;
419
+ }
420
+
421
+ // The block selection's own Escape still runs first, and then the keyboard
422
+ // leaves the writing surface for the toolbar.
423
+ leaveToToolbar() {
424
+ queueMicrotask(() => {
425
+ if (!this.view.isDestroyed) this.focusToolbar();
426
+ });
427
+ }
428
+
429
+ focusToolbar() {
430
+ const [first] = this.dockControls();
431
+ if (!first) return false;
432
+ this.setRoving(first);
433
+ first.focus();
434
+ return true;
435
+ }
436
+
437
+ // Left and Right walk the toolbar; Escape hands the keyboard back to the
438
+ // editor. Tab is left alone, so it leaves the toolbar.
439
+ dockKey(event) {
440
+ if (event.key === "Escape") {
441
+ event.preventDefault();
442
+ this.view.focus();
443
+ return;
444
+ }
445
+ if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
446
+ const list = this.dockControls();
447
+ const at = list.indexOf(event.target);
448
+ if (at < 0) return;
449
+ event.preventDefault();
450
+ const next = list[(at + (event.key === "ArrowRight" ? 1 : -1) + list.length) % list.length];
451
+ this.setRoving(next);
452
+ next.focus();
453
+ }
454
+
455
+ // The blocks a menu's commands act on, by id, so a peer's edit between opening
456
+ // the menu and running the command cannot retarget them.
457
+ selectionIds() {
458
+ const st = this.view.state;
459
+ return blocksIn(st).map((i) => st.doc.child(i).attrs.id);
460
+ }
461
+
462
+ idIndex(id) {
463
+ let found = -1;
464
+ this.view.state.doc.forEach((n, _, i) => { if (n.attrs.id === id) found = i; });
465
+ return found;
466
+ }
467
+
468
+ // Where Escape sends focus: the control that opened the menu when it can hold
469
+ // focus, otherwise the editor.
470
+ invoker() {
471
+ const m = this.menu;
472
+ if (!m || m.slashAt !== null) return this.view.dom;
473
+ const a = m.anchor;
474
+ return a && a !== this.view.dom && (a.tagName === "BUTTON" || a.hasAttribute("tabindex")) ? a : this.view.dom;
475
+ }
476
+
477
+ // A block action from the phone menu acts on the caret's block when nothing
478
+ // more is selected, as the desktop command does.
479
+ selectCaretBlock(ids = null) {
480
+ const st = this.view.state;
481
+ if (st.selection instanceof BlockSelection) return;
482
+ const found = (ids ?? []).map((id) => this.idIndex(id)).filter((i) => i >= 0);
483
+ const first = found.length ? found[0] : this.blockIndex();
484
+ this.view.dispatch(st.tr.setSelection(selectBlocks(st.doc, first, found.length ? found[found.length - 1] : first)));
485
+ }
486
+
303
487
  gripAction(index) {
304
488
  if (this.menu) this.closeMenu();
305
489
  else this.openActions(this.grip, index);
@@ -307,25 +491,39 @@ class Chrome {
307
491
 
308
492
  trackHover(event) {
309
493
  if (this.menu || this.drag.state || [this.plus, this.grip, this.dock, this.bubble].some((part) => part.contains(event.target))) return;
494
+ this.pendingHover = { clientY: event.clientY };
495
+ this.scheduleLayout();
496
+ }
497
+
498
+ // The pointer's block, read once per frame from the newest move.
499
+ hoverAt(clientY) {
310
500
  const view = this.view;
311
- const hit = view.posAtCoords({ left: view.dom.getBoundingClientRect().left + 2, top: event.clientY });
501
+ const hit = view.posAtCoords({ left: view.dom.getBoundingClientRect().left + 2, top: clientY });
312
502
  if (!hit) return;
313
503
  const doc = view.state.doc;
314
- const ents = outline(doc);
504
+ const ents = outlineOf(doc);
315
505
  let i = Math.min(doc.resolve(hit.inside >= 0 ? hit.inside : hit.pos).index(0), ents.length - 1);
316
506
  while (i > 0 && ents[i].hidden) i--;
317
- this.setHover(i);
507
+ this.hover = i;
318
508
  }
319
509
 
320
510
  setHover(i) {
321
511
  if (i === this.hover) return;
322
512
  this.hover = i;
323
- this.place();
513
+ this.scheduleLayout();
514
+ }
515
+
516
+ settleHover() {
517
+ if (!this.pendingHover) return;
518
+ const { clientY } = this.pendingHover;
519
+ this.pendingHover = null;
520
+ this.hoverAt(clientY);
324
521
  }
325
522
 
326
523
  fromPlus() {
327
524
  this.view.focus();
328
- this.openMenu("insert", this.plus, null, this.gutterIndex());
525
+ const i = this.gutterIndex();
526
+ this.openMenu("insert", this.plus, null, null, this.view.state.doc.child(i).attrs.id);
329
527
  }
330
528
 
331
529
  toggleMenu(mode, anchor) {
@@ -333,10 +531,13 @@ class Chrome {
333
531
  else this.openMenu(mode, anchor);
334
532
  }
335
533
 
336
- openMenu(mode, anchor, slashAt = null, index = null) {
337
- this.menu = { mode, anchor, slashAt, index, query: "", active: 0, items: [] };
534
+ openMenu(mode, anchor, slashAt = null, ids = null, anchorId = null) {
535
+ this.menu = { mode, anchor, slashAt, anchorId, ids: ids ?? (mode === "insert" ? [] : this.selectionIds()), query: "", active: 0, items: [] };
338
536
  anchor.setAttribute("aria-expanded", "true");
339
- this.view.dom.setAttribute("aria-haspopup", "menu");
537
+ if (slashAt !== null) {
538
+ if (!this.priorAria) this.priorAria = POPUP_ATTRS.map((name) => [name, this.view.dom.getAttribute(name)]);
539
+ this.view.dom.setAttribute("aria-haspopup", "listbox");
540
+ } else this.view.dom.setAttribute("aria-haspopup", "menu");
340
541
  this.view.dom.setAttribute("aria-controls", this.menuId);
341
542
  this.renderMenu();
342
543
  }
@@ -346,32 +547,68 @@ class Chrome {
346
547
  this.menu = null;
347
548
  this.menuEl.hidden = true;
348
549
  this.scrim.hidden = true;
349
- for (const name of ["aria-haspopup", "aria-controls", "aria-activedescendant"]) this.view.dom.removeAttribute(name);
550
+ if (this.priorAria) {
551
+ for (const [name, value] of this.priorAria) {
552
+ if (value === null) this.view.dom.removeAttribute(name);
553
+ else this.view.dom.setAttribute(name, value);
554
+ }
555
+ this.priorAria = null;
556
+ } else for (const name of POPUP_ATTRS) this.view.dom.removeAttribute(name);
557
+ }
558
+
559
+ // Everything the phone toolbar folds away: the rest of the formatting, and the
560
+ // block actions the desktop menu carries. The move commands are the same ones.
561
+ moreItems() {
562
+ const tools = TOOLS.filter(([name]) => !PHONE_TOOLS.has(name))
563
+ .map(([name, label]) => ({ key: name, label, icon: ICONS[name], run: (view) => runMark(view, name) }));
564
+ const actions = ACTION_ITEMS.filter((a) => MORE_ACTIONS.has(a.key)).map((a) => ({ ...a, blocks: true }));
565
+ return [
566
+ ...tools,
567
+ { key: "underline", label: "Underline", icon: "U", hint: `${MOD}u`, run: (view, chrome) => nativeMenu(view, chrome, "underline") },
568
+ { key: "text_color", label: "Text color", icon: "A", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "text_color") },
569
+ { key: "highlight", label: "Highlight", icon: "\u25A0", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "highlight") },
570
+ { key: "turn", label: "Turn into", icon: "\u21bb", run: (view, chrome) => chrome.openMenu("turn", chrome.more) },
571
+ { key: "insert", label: "Insert", icon: "+", run: (view, chrome) => chrome.openMenu("insert", chrome.more, null, null, view.state.doc.child(chrome.blockIndex()).attrs.id) },
572
+ ...actions,
573
+ ];
350
574
  }
351
575
 
352
576
  menuItems(m) {
353
577
  if (m.mode === "external") return m.externalItems || [];
354
578
  if (m.mode === "actions") return actionsFor(this.view.state);
579
+ if (m.mode === "more") return this.moreItems();
355
580
  const items = m.mode === "turn" ? BLOCK_TYPES : insertItemsFor(this.view.state, this.onAiTool
356
581
  ? [...MENU_ITEMS, { key: "ai", label: "Make a tool with AI…", icon: "✦", aliases: ["tool", "generate"], insert: "ai" }]
357
582
  : MENU_ITEMS);
358
583
  return rankItems(items, m.query);
359
584
  }
360
585
 
586
+ // The types the blocks in the selection are now. A mixed selection has more
587
+ // than one and so checks no row.
588
+ currentTypes() {
589
+ const st = this.view.state;
590
+ return new Set(blocksIn(st).map((i) => blockTypeOf(st.doc.child(i)).key));
591
+ }
592
+
361
593
  openActions(anchor = this.plus, at = null) {
362
594
  const st = this.view.state;
363
- const ents = outline(st.doc);
595
+ const ents = outlineOf(st.doc);
364
596
  const sel = st.selection;
365
597
  const inside = at !== null && sel instanceof BlockSelection && sel.span(st.doc).lo <= at && at <= sel.span(st.doc).hi;
366
598
  const { lo, hi } = at === null || inside ? currentSpan(st, ents) : siblingSpan(ents, at, at);
367
599
  this.view.dispatch(st.tr.setSelection(selectBlocks(st.doc, lo, hi, ents)));
368
- this.openMenu("actions", anchor, null, lo);
600
+ const ids = [];
601
+ for (let i = lo; i <= hi; i++) ids.push(st.doc.child(i).attrs.id);
602
+ this.openMenu("actions", anchor, null, ids);
369
603
  }
370
604
 
371
605
  renderMenu() {
372
606
  const m = this.menu;
373
607
  m.items = this.menuItems(m);
374
608
  m.active = Math.min(m.active, Math.max(0, m.items.length - 1));
609
+ const slash = m.slashAt !== null;
610
+ const current = m.mode === "turn" ? this.currentTypes() : null;
611
+ this.menuEl.setAttribute("role", slash ? "listbox" : "menu");
375
612
  const nodes = [];
376
613
  if (m.mode === "turn") {
377
614
  const title = el("div", "page-menu-title");
@@ -385,7 +622,9 @@ class Chrome {
385
622
  }
386
623
  m.items.forEach((b, i) => {
387
624
  const active = i === m.active;
388
- const row = el("button", `page-menu-item${active ? " is-active" : ""}`, { type: "button", role: typeof b.checked === "boolean" ? "menuitemcheckbox" : "menuitem", tabindex: m.mode === "external" ? "0" : "-1", id: `${this.menuId}-${i}`, "data-index": String(i), "aria-selected": active ? "true" : "false", ...(typeof b.checked === "boolean" ? { "aria-checked": String(b.checked) } : {}) });
625
+ const checked = current ? current.size === 1 && b.key === [...current][0] : (typeof b.checked === "boolean" ? b.checked : null);
626
+ const role = m.mode === "turn" ? "menuitemradio" : typeof checked === "boolean" ? "menuitemcheckbox" : slash ? "option" : "menuitem";
627
+ const row = el("button", `page-menu-item${active ? " is-active" : ""}`, { type: "button", role, tabindex: !slash && active ? "0" : "-1", id: `${this.menuId}-${i}`, "data-index": String(i), ...(role === "menuitemradio" ? {} : { "aria-selected": active ? "true" : "false" }), ...(typeof checked === "boolean" ? { "aria-checked": String(checked) } : {}) });
389
628
  const icon = el("span", "page-menu-icon");
390
629
  icon.textContent = b.icon || "";
391
630
  const label = el("span", "page-menu-label");
@@ -405,12 +644,12 @@ class Chrome {
405
644
  nodes.push(row);
406
645
  });
407
646
  this.menuEl.replaceChildren(...nodes);
408
- if (m.items.length) this.view.dom.setAttribute("aria-activedescendant", `${this.menuId}-${m.active}`);
647
+ if (slash && m.items.length) this.view.dom.setAttribute("aria-activedescendant", `${this.menuId}-${m.active}`);
409
648
  else this.view.dom.removeAttribute("aria-activedescendant");
410
649
  this.menuEl.hidden = false;
411
650
  this.scrim.hidden = false;
412
- this.place();
413
- if (m.mode === "external") this.menuEl.querySelector("button")?.focus();
651
+ this.scheduleLayout();
652
+ if (!slash) this.menuEl.querySelectorAll("button")[m.active]?.focus();
414
653
  }
415
654
 
416
655
  pick(i) {
@@ -419,34 +658,68 @@ class Chrome {
419
658
  if (!item) return;
420
659
  const st = this.view.state;
421
660
  const remove = m.slashAt !== null && st.selection.head >= m.slashAt ? { from: m.slashAt - 1, to: st.selection.head } : null;
661
+ // Resolve what the menu chose against the current document: a block a peer
662
+ // removed cancels the command instead of handing it whatever took its place.
663
+ const wanted = m.mode === "insert" ? (m.anchorId ? [m.anchorId] : []) : m.ids;
664
+ const stale = wanted.some((id) => !id || this.idIndex(id) < 0);
665
+ const index = wanted.length ? this.idIndex(wanted[0]) : -1;
422
666
  this.closeMenu();
667
+ if (stale && m.mode !== "external") {
668
+ announce(this.view, TARGET_GONE);
669
+ this.view.focus();
670
+ return;
671
+ }
423
672
  if (m.mode === "turn") {
424
- let tr = null;
425
- if (st.selection instanceof BlockSelection) {
426
- tr = st.tr;
427
- for (const root of selectedRoots(st)) retype(tr, root.i, item);
428
- } else tr = turnInto(st, this.blockIndex(), item);
429
- if (tr) this.view.dispatch(asStep(tr).scrollIntoView());
673
+ if (wanted.some((id) => !canTurnInto(st.doc.child(this.idIndex(id)), item))) {
674
+ announce(this.view, CANNOT_CONVERT);
675
+ this.view.focus();
676
+ return;
677
+ }
678
+ const turn = () => {
679
+ const tr = this.view.state.tr;
680
+ for (const id of wanted) {
681
+ const at = this.idIndex(id);
682
+ if (at >= 0) retype(tr, at, item);
683
+ }
684
+ if (tr.docChanged) this.view.dispatch(asStep(tr).scrollIntoView());
685
+ return true;
686
+ };
687
+ // A block type from a later format asks the gate before it writes.
688
+ if (item.type === "quote") {
689
+ const view = this.view;
690
+ import("./format-native-commands.js").then((menu) => menu.gateTurn(view, turn));
691
+ } else turn();
430
692
  this.view.focus();
431
- } else if (m.mode === "actions" || m.mode === "external") {
432
- item.run(this.view, this, m.index);
693
+ } else if (m.mode === "actions" || m.mode === "more" || m.mode === "external") {
694
+ if (item.blocks) this.selectCaretBlock(wanted);
695
+ item.run(this.view, this, index >= 0 ? index : this.blockIndex());
433
696
  if (!this.menu && item.focus !== false) this.view.focus();
434
697
  } else {
435
698
  if (item.insert === "ai") this.onAiTool(this.view, remove);
436
- else runItem(this.view, item, m.slashAt !== null ? this.blockIndex() : m.index, remove);
699
+ else runItem(this.view, item, index >= 0 ? index : this.blockIndex(), remove);
437
700
  }
438
701
  }
439
702
 
440
703
  // Keys while the menu is open; anything else falls through to the editor.
441
704
  key(event) {
442
- if (event.isComposing || this.view.composing) return false;
705
+ if (composing(event, this.view)) return false;
443
706
  const m = this.menu;
444
- if (!m) return false;
707
+ if (!m) {
708
+ if (event.key === "Escape" && !event.metaKey && !event.ctrlKey && !event.altKey
709
+ && this.view.state.selection instanceof BlockSelection) this.leaveToToolbar();
710
+ return false;
711
+ }
712
+ if ((event.key === "Backspace" || event.key === "Delete") && m.mode === "actions"
713
+ && this.view.state.selection instanceof BlockSelection) {
714
+ this.closeMenu();
715
+ this.view.focus();
716
+ deleteBlocks(this.view.state, this.view.dispatch);
717
+ return true;
718
+ }
445
719
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
446
720
  if (m.items.length) {
447
721
  m.active = (m.active + (event.key === "ArrowDown" ? 1 : -1) + m.items.length) % m.items.length;
448
722
  this.renderMenu();
449
- if (m.mode === "external") this.menuEl.querySelectorAll("button")[m.active]?.focus();
450
723
  }
451
724
  return true;
452
725
  }
@@ -454,7 +727,13 @@ class Chrome {
454
727
  if (m.items.length) this.pick(m.active);
455
728
  return true;
456
729
  }
457
- if (event.key === "Escape") { this.closeMenu(); return true; }
730
+ if (event.key === "Escape") {
731
+ const target = this.invoker();
732
+ this.closeMenu();
733
+ if (target === this.view.dom) this.view.focus();
734
+ else target?.focus?.();
735
+ return true;
736
+ }
458
737
  return false;
459
738
  }
460
739
 
@@ -473,33 +752,78 @@ class Chrome {
473
752
  this.renderMenu();
474
753
  }
475
754
 
476
- place() {
755
+ scheduleLayout() {
756
+ if (this.frame !== null || this.destroyed) return;
757
+ this.frame = this.win.requestAnimationFrame(() => {
758
+ this.frame = null;
759
+ if (this.destroyed) return;
760
+ const layout = this.measureLayout();
761
+ this.paintLayout(layout);
762
+ });
763
+ }
764
+
765
+ // The safe-area inset the browser reports, read through one hidden probe.
766
+ safeBottom() {
767
+ if (!this.probe) {
768
+ this.probe = el("div", "page-safe-probe");
769
+ this.shell.append(this.probe);
770
+ }
771
+ return parseFloat(this.win.getComputedStyle(this.probe).paddingBottom) || 0;
772
+ }
773
+
774
+ // A menu stays inside the visible viewport, above the bar when there is room.
775
+ menuBox(bar) {
776
+ const v = phoneViewport(this.win);
777
+ const maxHeight = Math.max(0, v.height - PHONE_INSET * 2);
778
+ const width = Math.min(280, Math.max(0, v.width - PHONE_INSET * 2));
779
+ const applied = this.menuEl.style.getPropertyValue("--page-pop-max");
780
+ this.menuEl.style.removeProperty("--page-pop-max");
781
+ const natural = this.menuEl.offsetHeight || 0;
782
+ if (applied) this.menuEl.style.setProperty("--page-pop-max", applied);
783
+ const height = Math.min(natural, maxHeight);
784
+ const left = Math.max(v.left + PHONE_INSET, Math.min(bar.left, v.left + v.width - PHONE_INSET - width));
785
+ let top = bar.top - 6 - height;
786
+ if (top < v.top + PHONE_INSET) top = Math.min(bar.top + bar.height + 6, v.top + v.height - PHONE_INSET - height);
787
+ return { left, top, width, maxHeight };
788
+ }
789
+
790
+ // Every geometry read the chrome needs, taken in one pass for the frame.
791
+ measureLayout() {
477
792
  const view = this.view;
478
793
  const st = view.state;
794
+ this.settleHover();
479
795
  const shellBox = this.shell.getBoundingClientRect();
480
796
  const edBox = view.dom.getBoundingClientRect();
481
797
  const i = this.gutterIndex();
482
798
  let at = 0;
483
799
  for (let k = 0; k < i; k++) at += st.doc.child(k).nodeSize;
484
800
  const dom = view.nodeDOM(at);
801
+ const out = { textLeft: edBox.left - shellBox.left, gutter: null, bubble: null, menu: null, bar: null, desk: false };
485
802
  if (dom && dom.getBoundingClientRect) {
486
803
  const r = dom.getBoundingClientRect();
487
804
  const lineH = parseFloat(getComputedStyle(dom).lineHeight) || r.height;
488
- for (const part of [this.plus, this.grip]) {
489
- part.style.setProperty("--page-plus-top", `${r.top - shellBox.top + Math.min(r.height, lineH) / 2}px`);
490
- part.style.setProperty("--page-plus-left", `${r.left - shellBox.left}px`);
491
- }
805
+ out.gutter = { top: r.top - shellBox.top + Math.min(r.height, lineH) / 2, left: r.left - shellBox.left };
806
+ }
807
+ const phone = this.isPhone();
808
+ // The fixed toolbar sits inside the visible viewport, so an on-screen keyboard
809
+ // or a pan cannot carry it off screen.
810
+ if (phone) {
811
+ const v = phoneViewport(this.win);
812
+ const height = this.dock.offsetHeight || PHONE_BAR_HEIGHT;
813
+ out.bar = { ...phoneBarBox(v, this.dock.offsetWidth || PHONE_BAR_WIDTH, height, this.safeBottom()), height };
814
+ } else {
815
+ out.desk = !!this.dock.style.left;
492
816
  }
493
- this.plus.style.setProperty("--page-text-left", `${edBox.left - shellBox.left}px`);
494
-
495
817
  const sel = st.selection;
496
- if (!this.bubble.hidden) {
818
+ if (!this.bubble.hidden && !phone) {
497
819
  const a = view.coordsAtPos(sel.from), b = view.coordsAtPos(sel.to, -1);
498
820
  const mid = (Math.min(a.left, b.left) + Math.max(a.right, b.right)) / 2;
499
- this.bubble.style.setProperty("--page-pop-x", `${mid - shellBox.left}px`);
500
- this.bubble.style.setProperty("--page-pop-y", `${Math.min(a.top, b.top) - shellBox.top}px`);
821
+ out.bubble = { x: mid - shellBox.left, y: Math.min(a.top, b.top) - shellBox.top };
501
822
  }
502
- if (this.menu) {
823
+ if (this.menu && phone) {
824
+ const box = this.menuBox(out.bar);
825
+ out.menu = { x: box.left, y: box.top, width: box.width, maxHeight: box.maxHeight };
826
+ } else if (this.menu) {
503
827
  const m = this.menu;
504
828
  let left, top;
505
829
  if (m.slashAt !== null) {
@@ -507,20 +831,56 @@ class Chrome {
507
831
  left = c.left;
508
832
  top = c.bottom + 6;
509
833
  const height = this.menuEl.offsetHeight;
510
- if (top + height > window.innerHeight - 8 && c.top - 6 - height >= 8) top = c.top - 6 - height;
834
+ if (top + height > this.win.innerHeight - 8 && c.top - 6 - height >= 8) top = c.top - 6 - height;
511
835
  } else {
512
836
  const r = m.anchor === this.plus ? (dom?.getBoundingClientRect?.() ?? edBox) : m.anchor.getBoundingClientRect();
513
837
  left = r.left;
514
838
  top = r.bottom + 6;
515
839
  }
516
- this.menuEl.style.setProperty("--page-pop-x", `${Math.max(8, Math.min(left - shellBox.left, shellBox.width - 280))}px`);
517
- this.menuEl.style.setProperty("--page-pop-y", `${top - shellBox.top}px`);
840
+ out.menu = { x: Math.max(8, Math.min(left - shellBox.left, shellBox.width - 280)), y: top - shellBox.top };
841
+ }
842
+ return out;
843
+ }
844
+
845
+ // The placement style writes, from the frame's measurements alone.
846
+ paintLayout(layout) {
847
+ if (layout.bar) {
848
+ this.dock.style.left = `${layout.bar.left}px`;
849
+ this.dock.style.top = `${layout.bar.top}px`;
850
+ this.dock.style.width = `${layout.bar.width}px`;
851
+ this.grip.style.setProperty("--page-grip-lift", `${layout.bar.height}px`);
852
+ } else {
853
+ if (layout.desk) for (const side of ["left", "top", "width"]) this.dock.style.removeProperty(side);
854
+ this.grip.style.removeProperty("--page-grip-lift");
855
+ }
856
+ if (layout.gutter) {
857
+ for (const part of [this.plus, this.grip]) {
858
+ part.style.setProperty("--page-plus-top", `${layout.gutter.top}px`);
859
+ part.style.setProperty("--page-plus-left", `${layout.gutter.left}px`);
860
+ }
861
+ }
862
+ this.plus.style.setProperty("--page-text-left", `${layout.textLeft}px`);
863
+ if (layout.bubble) {
864
+ this.bubble.style.setProperty("--page-pop-x", `${layout.bubble.x}px`);
865
+ this.bubble.style.setProperty("--page-pop-y", `${layout.bubble.y}px`);
866
+ }
867
+ if (layout.menu) {
868
+ this.menuEl.style.setProperty("--page-pop-x", `${layout.menu.x}px`);
869
+ this.menuEl.style.setProperty("--page-pop-y", `${layout.menu.y}px`);
870
+ if (layout.menu.width !== undefined) {
871
+ this.menuEl.style.setProperty("--page-pop-width", `${layout.menu.width}px`);
872
+ this.menuEl.style.setProperty("--page-pop-max", `${layout.menu.maxHeight}px`);
873
+ }
518
874
  }
519
875
  }
520
876
 
521
877
  update(view, prev) {
522
878
  this.view = view;
523
- if (this.menu && this.menu.slashAt === null && prev && prev.doc !== view.state.doc) this.closeMenu();
879
+ if (this.menu && this.menu.slashAt === null && prev && prev.doc !== view.state.doc) {
880
+ const hadFocus = this.menuEl.contains(document.activeElement);
881
+ this.closeMenu();
882
+ if (hadFocus) view.focus();
883
+ }
524
884
  const st = view.state;
525
885
  const node = st.doc.child(this.blockIndex());
526
886
  this.turn.textContent = `${blockTypeOf(node).label} ▾`;
@@ -529,19 +889,26 @@ class Chrome {
529
889
  this.bubble.hidden = !showBubble;
530
890
  if (showBubble) for (const b of this.bubble.querySelectorAll("[data-cmd]")) b.setAttribute("aria-pressed", String(toolActive(st, b.dataset.cmd)));
531
891
  this.grip.classList.toggle("is-on", st.selection instanceof BlockSelection);
892
+ if (!this.inRoving(this.roving)) this.setRoving(this.dockControls()[0]);
532
893
  this.slashQuery();
533
- this.place();
894
+ this.scheduleLayout();
534
895
  }
535
896
 
536
897
  destroy() {
898
+ this.destroyed = true;
899
+ if (this.frame !== null) this.win.cancelAnimationFrame(this.frame);
900
+ this.frame = null;
537
901
  clearTimeout(this.movedTimer);
538
902
  this.drag.destroy();
539
903
  this.shell.removeEventListener("mousemove", this.onMove);
540
904
  this.shell.removeEventListener("mouseleave", this.onLeave);
541
905
  this.shell.removeEventListener("keydown", this.onKey);
542
- window.removeEventListener("resize", this.onResize);
906
+ this.win.removeEventListener("resize", this.onResize);
907
+ this.win.removeEventListener("scroll", this.onResize, true);
908
+ this.win.visualViewport?.removeEventListener("resize", this.onResize);
909
+ this.win.visualViewport?.removeEventListener("scroll", this.onResize);
543
910
  document.removeEventListener("mousedown", this.onDocDown, true);
544
- for (const part of [this.dock, this.plus, this.grip, this.bubble, this.scrim, this.menuEl]) part.remove();
911
+ for (const part of [this.dock, this.plus, this.grip, this.bubble, this.scrim, this.menuEl, this.probe]) part?.remove();
545
912
  }
546
913
  }
547
914
 
@@ -571,12 +938,13 @@ export function chromePlugin({ onAiTool = null } = {}) {
571
938
  const $from = view.state.doc.resolve(from);
572
939
  if ($from.depth !== 1 || !$from.parent.isTextblock || $from.parent.type.spec.code) return inserted;
573
940
  if ($from.parentOffset > 0 && !/\s/.test(view.state.doc.textBetween(from - 1, from, "", "\n"))) return inserted;
574
- if (!inserted) view.dispatch(view.state.tr.insertText("/", from, to));
575
- chrome.openMenu("insert", chrome.plus, from + 1);
941
+ if (!inserted) view.dispatch(view.state.tr.insertText("/", from, to).setMeta(HISTORY_INTENT, typingIntent(view.state, from, to, "/")));
942
+ const at = Math.min(view.state.doc.resolve(from).index(0), view.state.doc.childCount - 1);
943
+ chrome.openMenu("insert", chrome.plus, from + 1, null, view.state.doc.child(at).attrs.id);
576
944
  return true;
577
945
  },
578
946
  handleDOMEvents: {
579
- keydown(view, event) { return event.isComposing || view.composing; },
947
+ keydown(view, event) { return composing(event, view); },
580
948
  blur(view) {
581
949
  if (chrome) chrome.bubble.hidden = true;
582
950
  return false;