@panphora/clayjs 1.25.0 → 1.26.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 (250) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +5570 -4173
  11. package/dist/m/attrs/{save-freeze-MH3AOD56.js → save-freeze-PUYX32AK.js} +1 -1
  12. package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
  13. package/dist/m/c-2EPHRF47.js +1 -0
  14. package/dist/m/{c-JD2VFAYG.js → c-2SUFMUZX.js} +2 -2
  15. package/dist/m/{c-CITLTVQ4.js → c-2TJCFPX4.js} +1 -1
  16. package/dist/m/c-33SS5A4Y.js +1 -0
  17. package/dist/m/{c-V2FPBRO6.js → c-34J5HC7N.js} +1 -1
  18. package/dist/m/c-3F3AADKG.js +1 -0
  19. package/dist/m/{c-TCFVG3I4.js → c-3S3647J6.js} +1 -1
  20. package/dist/m/{c-GY4BREDT.js → c-3S5TNRZA.js} +1 -1
  21. package/dist/m/{c-GDNZEOOZ.js → c-3WX5FABT.js} +1 -1
  22. package/dist/m/{c-AI3H6ETU.js → c-3XGLJAS7.js} +2 -2
  23. package/dist/m/c-42WBOKFM.js +1 -0
  24. package/dist/m/c-47M4QHCK.js +1 -0
  25. package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
  26. package/dist/m/{c-GEAG5XAK.js → c-4TP3UXBT.js} +1 -1
  27. package/dist/m/{c-IESDKFO6.js → c-5ELELG2F.js} +1 -1
  28. package/dist/m/{c-X3V4RWMA.js → c-5ZYLTN3Q.js} +1 -1
  29. package/dist/m/{c-5PZYDWTW.js → c-67TBU56I.js} +1 -1
  30. package/dist/m/{c-WNOLMHI2.js → c-6BGJAJ67.js} +1 -1
  31. package/dist/m/{c-4Q6QIODW.js → c-755ZOHOM.js} +1 -1
  32. package/dist/m/c-7Q4XYDF4.js +1 -0
  33. package/dist/m/c-7RHOMAMR.js +1 -0
  34. package/dist/m/c-A5LASUEZ.js +1 -0
  35. package/dist/m/{c-IL5IVTOG.js → c-ABOBD7F3.js} +1 -1
  36. package/dist/m/c-AQVQKFKB.js +1 -0
  37. package/dist/m/{c-QDVG4NAZ.js → c-ARO6CPMA.js} +1 -1
  38. package/dist/m/{c-ORLPQFNU.js → c-AZM34SPO.js} +1 -1
  39. package/dist/m/{c-XUZMRQ7O.js → c-BCS3AZQZ.js} +1 -1
  40. package/dist/m/c-BOS7GONO.js +1 -0
  41. package/dist/m/{c-SUAC5RSG.js → c-C5UGNOC2.js} +1 -1
  42. package/dist/m/c-C6DXZIXD.js +1 -0
  43. package/dist/m/{c-WSTUCBHV.js → c-CPRGR2II.js} +1 -1
  44. package/dist/m/{c-NICICT4P.js → c-DEYHCYJB.js} +1 -1
  45. package/dist/m/{c-WGQVSKKC.js → c-DYTBSJBI.js} +1 -1
  46. package/dist/m/{c-DT7E76Z2.js → c-DZT6W2NG.js} +1 -1
  47. package/dist/m/{c-O2QZDVZ2.js → c-EEWPNUM2.js} +1 -1
  48. package/dist/m/{c-MFB74XVN.js → c-EHAR4RJJ.js} +2 -2
  49. package/dist/m/c-EHJWIGWN.js +1 -0
  50. package/dist/m/c-EIDZHQHZ.js +1 -0
  51. package/dist/m/{c-V2V5FKXX.js → c-EIUKME3W.js} +1 -1
  52. package/dist/m/c-ELXMSEN5.js +1 -0
  53. package/dist/m/c-EXZOA6XY.js +7 -0
  54. package/dist/m/{c-FKTALXNV.js → c-EYFMAR3M.js} +1 -1
  55. package/dist/m/c-FC5RUFPR.js +1 -0
  56. package/dist/m/{c-FBOO2UY3.js → c-FUN6AKFE.js} +1 -1
  57. package/dist/m/{c-V4B3RUWB.js → c-GFASNR55.js} +1 -1
  58. package/dist/m/{c-XMIDHEKF.js → c-GY36NO7C.js} +1 -1
  59. package/dist/m/{c-UD4VDBGJ.js → c-H3M6UDNV.js} +1 -1
  60. package/dist/m/{c-SFZRY6RT.js → c-H47C6PJZ.js} +1 -1
  61. package/dist/m/{c-MUM53J5J.js → c-H4M2T4A6.js} +1 -1
  62. package/dist/m/{c-U7F66I64.js → c-HYNM5U6B.js} +1 -1
  63. package/dist/m/c-J2YYLEVM.js +1 -0
  64. package/dist/m/c-J2ZW4HMR.js +1 -0
  65. package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
  66. package/dist/m/c-K2ZHQMPF.js +1 -0
  67. package/dist/m/c-KD2ME5UN.js +2 -0
  68. package/dist/m/{c-Z2OZFX3N.js → c-L6KVXPBO.js} +1 -1
  69. package/dist/m/{c-GO635CRU.js → c-LAKBBJOU.js} +2 -2
  70. package/dist/m/{c-4AE2A2LK.js → c-LETPPG2F.js} +1 -1
  71. package/dist/m/{c-SFYTKRU7.js → c-MKAUC6DB.js} +1 -1
  72. package/dist/m/{c-WDFQ7PBU.js → c-MMEVDN6D.js} +1 -1
  73. package/dist/m/{c-LS7IVQVW.js → c-MMV5JULA.js} +1 -1
  74. package/dist/m/{c-NEQRFC4O.js → c-MVVEIJUU.js} +1 -1
  75. package/dist/m/{c-SXZYVXD6.js → c-N362HNGO.js} +1 -1
  76. package/dist/m/{c-7K4SCQB7.js → c-ND3W7D5U.js} +1 -1
  77. package/dist/m/{c-3E2IK5HU.js → c-NNHNXD6M.js} +1 -1
  78. package/dist/m/{c-D62HRSZK.js → c-NYJ577NL.js} +1 -1
  79. package/dist/m/{c-YFO6ZJLQ.js → c-O3DPFZCE.js} +1 -1
  80. package/dist/m/c-OZOACVUD.js +1 -0
  81. package/dist/m/{c-MV7TEUTD.js → c-OZVK3TFI.js} +1 -1
  82. package/dist/m/{c-ZEA3DCG3.js → c-PWILUN4T.js} +1 -1
  83. package/dist/m/{c-BCWYEG6P.js → c-QA3Q3MDI.js} +1 -1
  84. package/dist/m/{c-XHZIKRCV.js → c-QKOKMQXA.js} +1 -1
  85. package/dist/m/{c-TGIMBAWE.js → c-RBWVF4N5.js} +1 -1
  86. package/dist/m/c-RES2S5Q3.js +1 -0
  87. package/dist/m/c-RFGELYEU.js +2 -0
  88. package/dist/m/c-RGFPLQGV.js +1 -0
  89. package/dist/m/{c-RGAALCDU.js → c-SPOEDIMP.js} +1 -1
  90. package/dist/m/{c-YX2MC7YL.js → c-SRW5OBCF.js} +1 -1
  91. package/dist/m/{c-C6YZNU6E.js → c-SYOA4T7V.js} +1 -1
  92. package/dist/m/c-SYVF6ZMF.js +1 -0
  93. package/dist/m/{c-NADLYVAH.js → c-TJ5PFOJ2.js} +1 -1
  94. package/dist/m/{c-KYTVZT4D.js → c-TKV273YU.js} +1 -1
  95. package/dist/m/c-U35T5NAY.js +1 -0
  96. package/dist/m/{c-PRZNJUYZ.js → c-UEKJWXCB.js} +2 -2
  97. package/dist/m/c-UW6MMZ4V.js +1 -0
  98. package/dist/m/{c-XQTERKQA.js → c-V4DBV5ZV.js} +1 -1
  99. package/dist/m/c-VGG3UR7I.js +1 -0
  100. package/dist/m/c-VU4WEWPH.js +4 -0
  101. package/dist/m/{c-5Q5XBN56.js → c-W7LWCJHD.js} +1 -1
  102. package/dist/m/c-WHMZCRQD.js +6 -0
  103. package/dist/m/{c-WMRJKQ2V.js → c-WHQUZM2V.js} +1 -1
  104. package/dist/m/c-WOJ2RTV4.js +1 -0
  105. package/dist/m/{c-ZJG6C2WL.js → c-WOWGGNCJ.js} +1 -1
  106. package/dist/m/{c-ZORVIXUR.js → c-XHD7XJTE.js} +1 -1
  107. package/dist/m/c-XONFK3WC.js +1 -0
  108. package/dist/m/{c-HGG7M2S3.js → c-Y3J5LDKB.js} +1 -1
  109. package/dist/m/{c-JB5YU57S.js → c-Y3MPVURA.js} +1 -1
  110. package/dist/m/c-Y6HH7LTM.js +1 -0
  111. package/dist/m/{c-52OC3OH3.js → c-YRDKVEGR.js} +1 -1
  112. package/dist/m/c-YYIN4NAF.js +30 -0
  113. package/dist/m/{c-WVCT3ITX.js → c-YZGWSCMT.js} +1 -1
  114. package/dist/m/c-ZFAPQQIB.js +1 -0
  115. package/dist/m/core/{admin-attrs-ZI6K6UGM.js → admin-attrs-43JBA6JB.js} +1 -1
  116. package/dist/m/core/{autosave-BSM2ABJL.js → autosave-HD7Y7UNV.js} +1 -1
  117. package/dist/m/core/{conflict-notice-7EQLDYPA.js → conflict-notice-UNDOD4BL.js} +1 -1
  118. package/dist/m/core/{persist-2SZO3PCE.js → persist-MX3VYVBN.js} +1 -1
  119. package/dist/m/core/{save-K6Q47J2X.js → save-JZ7R4ET2.js} +1 -1
  120. package/dist/m/core/{save-core-U3Z44XBO.js → save-core-I4N6S4ED.js} +1 -1
  121. package/dist/m/core/site-edits-C22O53GS.js +1 -0
  122. package/dist/m/core/{snapshot-PWCHYFL6.js → snapshot-N5MKDGNI.js} +1 -1
  123. package/dist/m/core/{unsaved-warning-2WRZLJH5.js → unsaved-warning-BP6ZY3PG.js} +1 -1
  124. package/dist/m/internals/{index-FSLC4M7J.js → index-3LAVREAU.js} +1 -1
  125. package/dist/m/loader-DCKCZKY6.js +1 -0
  126. package/dist/m/manifest-7954CBE3.js +1 -0
  127. package/dist/m/manifest-edit-93AA79D7.js +1 -0
  128. package/dist/m/manifest.json +1240 -1179
  129. package/dist/m/page/{editor-PKSPJ2XJ.js → editor-UF3CIA6V.js} +5 -5
  130. package/dist/m/plugins/{ai-edit-3LG6MGS3.js → ai-edit-WDV2GLDU.js} +2 -2
  131. package/dist/m/plugins/{cms-T2HLI7JG.js → cms-KG6TCTR3.js} +9 -9
  132. package/dist/m/plugins/{derive-7DR5TUDH.js → derive-R4USHZ7P.js} +1 -1
  133. package/dist/m/plugins/{indicator-CTKX4EY7.js → indicator-OLPZ4WFI.js} +1 -1
  134. package/dist/m/plugins/{live-IFLL6YN5.js → live-DF5YS5AH.js} +1 -1
  135. package/dist/m/plugins/{menu-YJZ3JWBC.js → menu-ESEFQARM.js} +1 -1
  136. package/dist/m/plugins/page-GZSU77JS.js +2 -0
  137. package/dist/m/plugins/{page-view-NMCCU6MF.js → page-view-7MM3FEAR.js} +1 -1
  138. package/dist/m/plugins/{people-6WIWCZEG.js → people-EBWE337I.js} +1 -1
  139. package/dist/m/plugins/quickcrop-UHURKA2F.js +26 -0
  140. package/dist/m/plugins/richclay-MXH2GFAZ.js +549 -0
  141. package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
  142. package/dist/m/plugins/{source-7LSGOV46.js → source-OYHDXKTB.js} +1 -1
  143. package/dist/m/plugins/undo-KLHAGV75.js +1 -0
  144. package/dist/m/plugins/{upgrade-MMPZUCDM.js → upgrade-SZQJYEQQ.js} +1 -1
  145. package/dist/m/plugins/{visitor-drafts-V5YB7XDH.js → visitor-drafts-ZE6PCAFP.js} +2 -2
  146. package/dist/m/plugins/{visitor-menu-QDV2MYJN.js → visitor-menu-5XY57JQA.js} +1 -1
  147. package/dist/m/plugins/{wire-3YGUO3GE.js → wire-S5ATC4QI.js} +1 -1
  148. package/dist/m/reader-core-AHOAP5DH.js +77 -0
  149. package/dist/m/sync/{live-sync-XMXXZS3G.js → live-sync-RZQSDMZW.js} +1 -1
  150. package/dist/m/ui/index-G7ZJXNMG.js +1 -0
  151. package/dist/m/vendor/{sap-core.vendor-JANUQULT.js → sap-core.vendor-5TWOJYVM.js} +4 -4
  152. package/dist/offline-manifest.json +780 -750
  153. package/dist/server/core.cjs +4245 -2614
  154. package/dist/server/markdown.cjs +1 -0
  155. package/dist/server/markdown.mjs +7 -0
  156. package/docs/guide.md +26 -37
  157. package/entries/clay-data.js +1 -1
  158. package/entries/sap.js +5 -5
  159. package/package.json +6 -2
  160. package/packed-contract.json +3 -0
  161. package/src/attrs/save-freeze.js +1 -1
  162. package/src/core/admin-contenteditable.js +1 -1
  163. package/src/core/admin-inputs.js +1 -1
  164. package/src/core/admin-onclick.js +1 -1
  165. package/src/core/admin-resources.js +5 -2
  166. package/src/core/conflict-notice-ui.js +59 -34
  167. package/src/core/save.js +9 -9
  168. package/src/core/site-edits.js +1 -1
  169. package/src/core/snapshot.js +56 -43
  170. package/src/core/stale-host-notice.js +20 -7
  171. package/src/loader-logic.js +0 -13
  172. package/src/page/boot-pack.js +1 -0
  173. package/src/page/editor.js +10 -4
  174. package/src/page/history-intent.js +111 -4
  175. package/src/page/import-markdown.js +22 -8
  176. package/src/page/save-projection.js +1 -1
  177. package/src/page/ybind/bridge.js +14 -7
  178. package/src/page/ybind/moves.js +437 -3
  179. package/src/page/ybind/origins.js +1 -1
  180. package/src/page/ybind/recovery.js +1 -1
  181. package/src/plugins/page.js +48 -4
  182. package/src/plugins/quickcrop.js +128 -69
  183. package/src/plugins/richclay.js +136 -0
  184. package/src/plugins/sortable.js +25 -9
  185. package/src/plugins/undo.js +169 -7
  186. package/src/recovery/undo.js +14 -7
  187. package/src/server/markdown/index.js +23 -0
  188. package/src/sync/conflicts.js +3 -1
  189. package/src/sync/data-guard-notice.js +137 -18
  190. package/src/sync/live-sync.js +24 -0
  191. package/src/ui/bevel-controls.js +4 -1
  192. package/src/ui/dialogs.js +11 -7
  193. package/src/ui/modal.js +9 -7
  194. package/src/ui/toast.js +67 -32
  195. package/src/ui/vendor-skin.js +9 -4
  196. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  197. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  198. package/src/vendor/hyper-undo.vendor.js +1 -1
  199. package/src/vendor/hypercms.vendor.js +7 -7
  200. package/src/vendor/richclay.vendor.js +16 -16
  201. package/src/vendor/sap-core.vendor.js +4 -4
  202. package/tool-generation/context.md +69 -0
  203. package/tool-generation/system.md +1 -1
  204. package/dist/m/c-277QP5ET.js +0 -1
  205. package/dist/m/c-5SZ6KFR5.js +0 -1
  206. package/dist/m/c-CU33V6RW.js +0 -4
  207. package/dist/m/c-CVSWLX3X.js +0 -1
  208. package/dist/m/c-DXPOFLEJ.js +0 -1
  209. package/dist/m/c-EODTTNVA.js +0 -1
  210. package/dist/m/c-ESK4S67I.js +0 -1
  211. package/dist/m/c-FDWR3FL7.js +0 -1
  212. package/dist/m/c-FJDTDGDA.js +0 -1
  213. package/dist/m/c-FYDKYW6E.js +0 -1
  214. package/dist/m/c-HKSLOVU4.js +0 -1
  215. package/dist/m/c-IJHF4IO6.js +0 -1
  216. package/dist/m/c-IUL7PU4P.js +0 -1
  217. package/dist/m/c-JWW6BHWD.js +0 -6
  218. package/dist/m/c-K4M76VPM.js +0 -7
  219. package/dist/m/c-M7ITYKMF.js +0 -1
  220. package/dist/m/c-MR75YPRT.js +0 -1
  221. package/dist/m/c-NFQFV7OP.js +0 -2
  222. package/dist/m/c-NHXOBD6A.js +0 -1
  223. package/dist/m/c-NVU3DORS.js +0 -1
  224. package/dist/m/c-PNZIJFG7.js +0 -1
  225. package/dist/m/c-QWB53KRW.js +0 -1
  226. package/dist/m/c-RHPHMTF2.js +0 -2
  227. package/dist/m/c-RWJ3OSCP.js +0 -1
  228. package/dist/m/c-SEVOS4UE.js +0 -1
  229. package/dist/m/c-SGG6MUXW.js +0 -1
  230. package/dist/m/c-SXNSSHPP.js +0 -1
  231. package/dist/m/c-TFLRT5JX.js +0 -1
  232. package/dist/m/c-TLQINES7.js +0 -30
  233. package/dist/m/c-U65PREMC.js +0 -1
  234. package/dist/m/c-V5L6HWVL.js +0 -1
  235. package/dist/m/c-VMXCX52Q.js +0 -1
  236. package/dist/m/c-YYGUHSZR.js +0 -1
  237. package/dist/m/core/site-edits-CFMF6GHD.js +0 -1
  238. package/dist/m/loader-RC54UV7H.js +0 -1
  239. package/dist/m/manifest-3B84C829.js +0 -1
  240. package/dist/m/manifest-edit-1CB79AD4.js +0 -1
  241. package/dist/m/plugins/page-GTDPYGE4.js +0 -2
  242. package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
  243. package/dist/m/plugins/richclay-L3BZMX4I.js +0 -549
  244. package/dist/m/plugins/sortable-365SRRL3.js +0 -1
  245. package/dist/m/plugins/undo-UES4KS7B.js +0 -1
  246. package/dist/m/reader-core-VR7EL4WV.js +0 -77
  247. package/dist/m/ui/index-MGTWOW2C.js +0 -1
  248. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  249. /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-UHURKA2F.js.LEGAL.txt} +0 -0
  250. /package/dist/m/plugins/{richclay-L3BZMX4I.js.LEGAL.txt → richclay-MXH2GFAZ.js.LEGAL.txt} +0 -0
@@ -707,6 +707,11 @@ function checkboxClick(view, event) {
707
707
  return true;
708
708
  }
709
709
 
710
+ function pageTopInset(doc, least) {
711
+ const padding = parseFloat(doc.defaultView?.getComputedStyle(doc.documentElement).scrollPaddingTop);
712
+ return Number.isFinite(padding) ? Math.max(least, padding) : least;
713
+ }
714
+
710
715
  // Mounts the editor in `mount`, inside a .blockeditor-shell wrapper (the surface a
711
716
  // theme styles). `onChange(record)` gets the block record (blockMap) after
712
717
  // every transaction that changed the document.
@@ -743,10 +748,11 @@ export function createPageEditor(mount, { blocks = null, onChange = null, upload
743
748
  view = new pmView.EditorView(shell, {
744
749
  state: blockeditorState(blocks, { upload, highlight, doc: initialDoc, history: useHistory, undo, redo, onAiTool, mediaNote, toolHost, features, commandHost }),
745
750
  attributes: { class: "blockeditor-editor" },
746
- // The phone keeps a fixed bar and the plus and grip at the bottom: a caret scrolled into
747
- // view stops clear of them.
748
- scrollMargin: { top: 16, bottom: 96, left: 5, right: 5 },
749
- scrollThreshold: { top: 16, bottom: 96, left: 0, right: 0 },
751
+ // The phone keeps a fixed bar and the plus and grip at the bottom, and a page with a
752
+ // sticky header declares its height as scroll-padding-top: a caret scrolled into view
753
+ // stops clear of both.
754
+ scrollMargin: { get top() { return pageTopInset(doc, 16); }, bottom: 96, left: 5, right: 5 },
755
+ scrollThreshold: { get top() { return pageTopInset(doc, 16); }, bottom: 96, left: 0, right: 0 },
750
756
  editable: () => editable,
751
757
  nodeViews,
752
758
  handleDOMEvents: {
@@ -1,5 +1,5 @@
1
- // Typing joins one undo item the way it does in other editors: Mod-z removes the last
2
- // word typed, not the last letter. Only the editor's own input path marks a
1
+ // Typing joins one undo item per sentence, the same as every other field on the page:
2
+ // Mod-z removes the last sentence typed, not the last letter or word. Only the editor's own input path marks a
3
3
  // transaction; a command or a programmatic dispatch stays one undo item by itself.
4
4
  import { state } from "../vendor/prosemirror.vendor.js";
5
5
  import { STEP } from "./blocks.js";
@@ -14,15 +14,28 @@ const blockIdAt = (doc, pos) => {
14
14
 
15
15
  const markKey = (st) => (st.storedMarks ?? st.selection.$from.marks()).map((m) => `${m.type.name}:${JSON.stringify(m.attrs)}`).sort().join(",");
16
16
 
17
+ // A sentence ends at a space typed right after ., ? or ! (closing quotes and brackets
18
+ // allowed between). The space belongs to the sentence it ends, so the step closes after it.
19
+ // A second space, or a space after a space, is not a new sentence. The rule is hyper-undo's
20
+ // `endsSentence`, kept here so the block editor does not load the engine.
21
+ const SENTENCE_END = /[.?!]["'\u201D\u2019)\]]*$/;
22
+
23
+ const endsSentence = (st, from, text) => {
24
+ if (!/\s$/.test(text)) return false;
25
+ const $from = st.doc.resolve(from);
26
+ const head = $from.parent.textBetween(0, $from.parentOffset) + text.replace(/\s+$/, "");
27
+ return !/\s$/.test(head) && SENTENCE_END.test(head);
28
+ };
29
+
17
30
  export function typingIntent(st, from, to, text) {
18
- return { kind: "insert", blockId: blockIdAt(st.doc, from), from, end: from + text.length, replaced: from !== to, text, marks: markKey(st), at: Date.now() };
31
+ return { kind: "insert", blockId: blockIdAt(st.doc, from), from, end: from + text.length, replaced: from !== to, text, marks: markKey(st), breakAfter: endsSentence(st, from, text), at: Date.now() };
19
32
  }
20
33
 
21
34
  export function continuesGroup(prev, next) {
22
35
  if (!prev || !next || prev.kind !== next.kind) return false;
23
36
  if (next.kind === "compose") return prev.id === next.id;
24
37
  if (prev.blockId !== next.blockId || next.at - prev.at > GROUP_MS) return false;
25
- if (next.kind === "insert") return !next.replaced && next.from === prev.end && next.marks === prev.marks && !(/\s$/.test(prev.text) && /^\S/.test(next.text));
38
+ if (next.kind === "insert") return !prev.breakAfter && !next.replaced && next.from === prev.end && next.marks === prev.marks;
26
39
  return next.direction === prev.direction && (next.direction < 0 ? next.to === prev.from : next.from === prev.from);
27
40
  }
28
41
 
@@ -76,3 +89,97 @@ export function historyIntentPlugin() {
76
89
  },
77
90
  });
78
91
  }
92
+
93
+ // Each page entry owns one current Yjs StackItem, replaced by its inverse after replay.
94
+ export function createHistoryBridge({ um, undo, redo, article, focus }) {
95
+ const page = () => window.clay?.undo
96
+ const epoch = Math.random().toString(36).slice(2)
97
+ let next = 0
98
+ let running = false
99
+ let recording = false
100
+ let disposed = false
101
+
102
+ const replay = (entry, direction) => {
103
+ const fromKey = direction === 'undo' ? 'undoStack' : 'redoStack'
104
+ const toKey = direction === 'undo' ? 'redoStack' : 'undoStack'
105
+ const source = um[fromKey], destination = um[toKey]
106
+ const index = source.indexOf(entry.item)
107
+ if (index < 0) throw new Error('blockeditor: history item is no longer available')
108
+ const remaining = source.filter((_, at) => at !== index)
109
+ // Yjs scans BOTH stacks' deletion sets when checking map conflicts. Keep those
110
+ // sets visible on the opposite stack while limiting the pop loop to one item.
111
+ const temporary = [...destination, ...remaining]
112
+ const boundary = temporary.length
113
+ um[fromKey] = [entry.item]
114
+ um[toKey] = temporary
115
+ running = true
116
+ let changed
117
+ let inverse
118
+ try {
119
+ changed = direction === 'undo' ? undo() : redo()
120
+ } finally {
121
+ inverse = temporary.slice(boundary)
122
+ um[fromKey] = remaining
123
+ um[toKey] = [...destination, ...inverse]
124
+ running = false
125
+ um.stopCapturing()
126
+ }
127
+ if (!changed || !inverse.length) {
128
+ entry.item = null
129
+ throw new Error('blockeditor: history item was emptied by another edit')
130
+ }
131
+ entry.item = inverse[0]
132
+ entry.applied = direction === 'redo'
133
+ }
134
+
135
+ const onAdded = (event) => {
136
+ if (running || event.origin === um || event.type !== 'undo' || page()?.pageHistory !== true) return
137
+ const entry = { item: event.stackItem, applied: true }
138
+ const id = `${epoch}:${++next}`
139
+ const oldValue = `${id}:undone`, newValue = `${id}:applied`
140
+ const primitive = {
141
+ kind: 'value',
142
+ target: article,
143
+ prop: `blockeditor-step:${id}`,
144
+ oldValue,
145
+ newValue,
146
+ read: () => !disposed && entry.item && (entry.applied ? um.undoStack : um.redoStack).includes(entry.item)
147
+ ? entry.applied ? newValue : oldValue
148
+ : null,
149
+ write: (target, wanted) => {
150
+ const typing = !!document.activeElement?.closest?.('[contenteditable]:not([contenteditable="false"])')
151
+ const before = article.textContent
152
+ replay(entry, wanted === oldValue ? 'undo' : 'redo')
153
+ if (typing && article.textContent !== before && typeof focus === 'function') focus()
154
+ },
155
+ }
156
+ recording = true
157
+ try {
158
+ try { page().recordStep('Edit', primitive) } catch { page().record(primitive) }
159
+ } finally {
160
+ recording = false
161
+ }
162
+ }
163
+
164
+ const onCommit = () => { if (!recording) um.stopCapturing() }
165
+ um.on('stack-item-added', onAdded)
166
+ let subscribed = null
167
+ const subscribe = () => {
168
+ const current = page()?.pageHistory === true ? page() : null
169
+ if (current === subscribed) return
170
+ subscribed?.off?.('commit', onCommit)
171
+ subscribed = current
172
+ subscribed?.on?.('commit', onCommit)
173
+ }
174
+ subscribe()
175
+ document.addEventListener('clay:undo-session', subscribe)
176
+
177
+ return {
178
+ dispose() {
179
+ disposed = true
180
+ um.off('stack-item-added', onAdded)
181
+ subscribed?.off?.('commit', onCommit)
182
+ document.removeEventListener('clay:undo-session', subscribe)
183
+ },
184
+ }
185
+ }
@@ -17,8 +17,8 @@ import { safeHref, safeSrc } from "./address.js";
17
17
  const EMBEDS = "iframe, frame, frameset, object, embed, applet, meta, base, link, form, style, animate, set, animateMotion, animateTransform, portal";
18
18
  const LOADERS = /^(srcdoc|data|formaction|codebase|archive|ping|background|poster|lowsrc|dynsrc)$/i;
19
19
  const SCRIPTED = /^\s*(javascript|vbscript|data):/i;
20
- function cleanImported(html) {
21
- const t = document.createElement("template");
20
+ function cleanImported(html, doc) {
21
+ const t = doc.createElement("template");
22
22
  t.innerHTML = html;
23
23
  for (const el of t.content.querySelectorAll(EMBEDS)) el.remove();
24
24
  for (const el of t.content.querySelectorAll("*")) {
@@ -57,7 +57,19 @@ function added(marks, name, attrs) {
57
57
  // One text block's inline content as the record keeps it: c, and m as runs (one
58
58
  // key per UTF-16 unit, packed), exactly as flatten and runsOf write it; `parts`
59
59
  // splits around an image, which the schema only carries as a block of its own.
60
- function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}) {
60
+ // A link or image written as a relative path (`other.md`, `setup.sh`, `shot.png`) names a
61
+ // file beside the Markdown it came from. It never becomes a web address: `setup.sh` read
62
+ // as a host name is someone else's site, and an image there would make every reader's
63
+ // browser ask that site for it. The host may map it (`resolveLink`, `resolveImage`).
64
+ const RELATIVE = /^(?![a-z][a-z0-9+.-]*:|[/#?])\S/i;
65
+
66
+ function linkHref(raw, resolveLink) {
67
+ const resolved = typeof resolveLink === "function" ? resolveLink(raw) : null;
68
+ if (typeof resolved === "string" && resolved) return safeHref(resolved);
69
+ return RELATIVE.test(raw.trim()) ? null : safeHref(raw);
70
+ }
71
+
72
+ function inlineOf(tokens, { images = true, refused = [], unsupported = [], resolveLink = null } = {}) {
61
73
  const parts = [];
62
74
  let c = "";
63
75
  let m = [];
@@ -91,7 +103,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
91
103
  case "codespan": text(token.text, added(marks, "code")); break;
92
104
  case "br": atom(BREAK); break;
93
105
  case "link": {
94
- const href = safeHref(token.href);
106
+ const href = linkHref(String(token.href ?? ""), resolveLink);
95
107
  if (href) walk(token.tokens, added(marks, "link", { href }));
96
108
  else {
97
109
  refused.push({ type: "link", href: String(token.href ?? "") });
@@ -121,7 +133,7 @@ function inlineOf(tokens, { images = true, refused = [], unsupported = [] } = {}
121
133
  return { parts };
122
134
  }
123
135
 
124
- export function importMarkdownTokens(tokens) {
136
+ export function importMarkdownTokens(tokens, { document: doc = globalThis.document, resolveImage = null, resolveLink = null } = {}) {
125
137
  if (!Array.isArray(tokens)) throw new MarkdownImportError("malformed-input", "Markdown import needs the lexer's token list.");
126
138
  let size = 0;
127
139
  for (const token of tokens) {
@@ -155,7 +167,9 @@ export function importMarkdownTokens(tokens) {
155
167
  return value;
156
168
  };
157
169
  const imageOf = (image) => {
158
- const src = safeSrc(image.src);
170
+ const resolved = typeof resolveImage === "function" ? resolveImage(image.src) : null;
171
+ const mapped = typeof resolved === "string" && resolved ? resolved : null;
172
+ const src = mapped ? safeSrc(mapped) : RELATIVE.test(String(image.src).trim()) ? null : safeSrc(image.src);
159
173
  if (src) return { t: "image", src, ...(image.alt ? { alt: image.alt } : {}), ...(image.title ? { caption: image.title } : {}) };
160
174
  refused.push({ type: "image", href: image.src });
161
175
  return image.alt ? block("paragraph", {}, { c: image.alt, m: [] }) : null;
@@ -168,7 +182,7 @@ export function importMarkdownTokens(tokens) {
168
182
  }
169
183
  };
170
184
  const paragraph = (inline, ctx) => textBlocks(ctx.quote ? "quote" : "paragraph", {}, inline);
171
- const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, ...options });
185
+ const inline = (tokens, options = {}) => inlineOf(tokens, { refused, unsupported, resolveLink, ...options });
172
186
  const literal = (token) => {
173
187
  const raw = String(token.raw ?? token.text ?? "");
174
188
  unsupported.push({ type: token.type, text: raw });
@@ -193,7 +207,7 @@ export function importMarkdownTokens(tokens) {
193
207
  case "html": {
194
208
  const raw = String(token.text ?? token.raw ?? "");
195
209
  unsupported.push({ type: "HTML block cleaned", text: raw });
196
- push({ t: "preserved", html: cleanImported(fragmentCleaner(document).fallback(raw)) });
210
+ push({ t: "preserved", html: cleanImported(fragmentCleaner(doc).fallback(raw), doc) });
197
211
  break;
198
212
  }
199
213
  case "blockquote": {
@@ -120,4 +120,4 @@ export function acceptedPageState(html) {
120
120
  }
121
121
  }
122
122
 
123
- addDocumentTransform(projectInto);
123
+ addDocumentTransform(projectInto, { required: true });
@@ -10,7 +10,7 @@ import { state as pmState } from "../../vendor/prosemirror.vendor.js";
10
10
  import { repairSiblings, isKey } from "../model.js";
11
11
  import { createOriginIndex, installUndoOrigins } from "./origins.js";
12
12
  import { reduceRecovery, reduceRecoverable, canonicalRevision, restoreKey, physicalKeyOf, authorKey, RECOVERED_PREFIX, parsePhysicalKey, parseGoneIntents } from "./recovery.js";
13
- import { moveBlocks } from "./moves.js";
13
+ import { moveBlocks, createMoveCarry } from "./moves.js";
14
14
  import { createBoardCommands } from "./board-commands.js";
15
15
  import { continuesGroup, intentOf } from "../history-intent.js";
16
16
  import { MOVE, BlockSelection } from "../blocks.js";
@@ -320,7 +320,9 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
320
320
  if (!d) { adopt(view.state.doc); return; }
321
321
  const tr = view.state.tr.replace(d.start, d.endA, derived.slice(d.start, d.endB)).setMeta(REMOTE_META, true).setMeta("addToHistory", false);
322
322
  if (own) tr.setMeta("yundo", true);
323
- if (keep) {
323
+ if (own && view.state.doc.textContent !== derived.textContent && document.activeElement?.closest?.('[contenteditable]:not([contenteditable="false"])')) {
324
+ tr.setSelection(pmState.Selection.near(tr.doc.resolve(Math.min(d.endB, tr.doc.content.size)), -1));
325
+ } else if (keep) {
324
326
  const hiddenUnit = (y) => y >= 0 && w.holes.some((hole) => y >= yBefore(w.holes, hole.at) && y < yAfter(w.holes, hole.at));
325
327
  const placeIn = (id, offset) => {
326
328
  let found = null;
@@ -359,12 +361,14 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
359
361
  return true;
360
362
  };
361
363
  const stale = (t) => { began.set(t, docVersion); docVersion++; };
364
+ const carry = createMoveCarry({ pd, origins, um, walked: () => currentWalk() ?? walk(pd, origins) });
362
365
  const onTransaction = (t) => {
363
366
  if (unchanged(t)) {
364
367
  idle.add(t);
365
368
  if (walkedAt === began.get(t) && docVersion === walkedAt + 1) walkedAt = docVersion;
366
369
  return;
367
370
  }
371
+ if (t.origin === um) carry.note(t);
368
372
  deltas++;
369
373
  delta = deltas === 1 ? captureDelta(pd, t, w) : null;
370
374
  };
@@ -385,7 +389,9 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
385
389
  saved = { anchor: end(sel.$anchor), head: end(sel.$head) };
386
390
  };
387
391
  const onAfter = (t) => {
388
- if (t.origin !== LOCAL) { lastIntent = null; groupOpen = false; }
392
+ // A transaction that wrote nothing (Yjs's cleanup after an undo) is no peer's edit, so
393
+ // it leaves an open typing run open.
394
+ if (t.origin !== LOCAL && !unchanged(t)) { lastIntent = null; groupOpen = false; }
389
395
  };
390
396
  // One render per batch Yjs settles (a transaction and those its observers queued), after
391
397
  // its garbage collection and merges, so the walk it leaves is current for the next keystroke.
@@ -398,9 +404,10 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
398
404
  else if (t.origin === um) own = true;
399
405
  }
400
406
  if (!changed) { saved = null; return; }
401
- if (!view) { bumpRecovery(); return; }
407
+ if (!view) { bumpRecovery(); carry.received(transactions); return; }
402
408
  if (local) { saved = null; return; }
403
409
  render(own);
410
+ carry.received(transactions);
404
411
  };
405
412
  const portMove = (tr, move) => {
406
413
  const result = moveBlocks({ pd, view, um, origins, render, walked: currentWalk() }, move);
@@ -486,7 +493,7 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
486
493
  registerAuthor = null;
487
494
  const intent = history ? intentOf(local, view) : null;
488
495
  const join = groupOpen && continuesGroup(lastIntent, intent);
489
- lastIntent = intent;
496
+ lastIntent = !join && intent?.breakAfter && !intent.text.trim() ? { ...intent, breakAfter: false } : intent;
490
497
  joining = join;
491
498
  try {
492
499
  localGroup(pd, local, w, stats, um, history, register, { join, open: intent !== null });
@@ -546,7 +553,7 @@ export function createBridge({ pd, onNotice = () => {}, ownedTokens = [], author
546
553
  moveBlocks(args) {
547
554
  return moveBlocks({ pd, view, um, origins, render, walked: currentWalk() }, args);
548
555
  },
549
- undo() { return um.undo() !== null; },
550
- redo() { return um.redo() !== null; },
556
+ undo() { return carry.replay("undo"); },
557
+ redo() { return carry.replay("redo"); },
551
558
  };
552
559
  }