@panphora/clayjs 1.24.1 → 1.25.1

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 (275) 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 +6060 -4662
  11. package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
  12. package/dist/m/attrs/save-freeze-PUYX32AK.js +1 -0
  13. package/dist/m/{c-HIX5H5L7.js → c-2A2ATCIH.js} +1 -1
  14. package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
  15. package/dist/m/{c-MP6S4AZU.js → c-2GB34XL4.js} +2 -2
  16. package/dist/m/c-2TJCFPX4.js +4 -0
  17. package/dist/m/c-33SS5A4Y.js +1 -0
  18. package/dist/m/{c-PCNJETUA.js → c-34J5HC7N.js} +1 -1
  19. package/dist/m/{c-AT7GLQGV.js → c-3AAQJAKJ.js} +1 -1
  20. package/dist/m/c-3F3AADKG.js +1 -0
  21. package/dist/m/{c-HL6BLJNA.js → c-3WX5FABT.js} +1 -1
  22. package/dist/m/c-3XNTQQEJ.js +1 -0
  23. package/dist/m/{c-ZRGEN6QQ.js → c-3Y234AUB.js} +2 -2
  24. package/dist/m/{c-SUXHUL7U.js → c-42RLKGEY.js} +1 -1
  25. package/dist/m/{c-2BLVA6IN.js → c-44WITKWQ.js} +1 -1
  26. package/dist/m/c-47M4QHCK.js +1 -0
  27. package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
  28. package/dist/m/{c-SXGEWPFC.js → c-4NAYDOWH.js} +1 -1
  29. package/dist/m/{c-OCYG4NBQ.js → c-4OZZ7ENT.js} +2 -2
  30. package/dist/m/{c-UOOIF2AL.js → c-5IIEOPIV.js} +1 -1
  31. package/dist/m/{c-YDW4VCHP.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-7V6MGMX2.js → c-7Z2625LX.js} +1 -1
  35. package/dist/m/{c-CTGRDT6E.js → c-A3OKSYRI.js} +1 -1
  36. package/dist/m/{c-GNI7WHKF.js → c-A5IYL7JI.js} +1 -1
  37. package/dist/m/c-A5LASUEZ.js +1 -0
  38. package/dist/m/c-AQVQKFKB.js +1 -0
  39. package/dist/m/{c-FLEIU6CG.js → c-ASGHHLFX.js} +1 -1
  40. package/dist/m/c-ASMLERFA.js +7 -0
  41. package/dist/m/{c-LUXAB2XI.js → c-AZM34SPO.js} +1 -1
  42. package/dist/m/{c-ZFO5RW6G.js → c-BHLYQGTA.js} +1 -1
  43. package/dist/m/{c-C5AKJVRH.js → c-BJ2RRQBQ.js} +1 -1
  44. package/dist/m/{c-FVPNX2GO.js → c-C27EVUQ6.js} +1 -1
  45. package/dist/m/{c-DFKBGSN2.js → c-C5UGNOC2.js} +1 -1
  46. package/dist/m/c-C6DXZIXD.js +1 -0
  47. package/dist/m/{c-EDE45NM7.js → c-C7SUMNMI.js} +1 -1
  48. package/dist/m/{c-STRI73GQ.js → c-CL5LSATX.js} +1 -1
  49. package/dist/m/c-CPRGR2II.js +1 -0
  50. package/dist/m/{c-NFQFV7OP.js → c-D2OPWOWO.js} +1 -1
  51. package/dist/m/{c-ZKKAF4UT.js → c-D4N4UJCQ.js} +2 -2
  52. package/dist/m/{c-6LEYVQJK.js → c-DAUQ6JZ3.js} +1 -1
  53. package/dist/m/{c-ZGCO4J3H.js → c-DDCYVLGG.js} +1 -1
  54. package/dist/m/{c-KQKKIVLW.js → c-DVMDYRAE.js} +1 -1
  55. package/dist/m/{c-OG6GFSUX.js → c-DYTBSJBI.js} +1 -1
  56. package/dist/m/{c-P2HKMJAW.js → c-EAV7A6BT.js} +1 -1
  57. package/dist/m/{c-X75G43SZ.js → c-EEWPNUM2.js} +1 -1
  58. package/dist/m/c-EHAR4RJJ.js +3 -0
  59. package/dist/m/c-EIDZHQHZ.js +1 -0
  60. package/dist/m/c-ELXMSEN5.js +1 -0
  61. package/dist/m/{c-QQFHEODI.js → c-EQ5NBOGA.js} +1 -1
  62. package/dist/m/c-FC5RUFPR.js +1 -0
  63. package/dist/m/{c-ND3Q2G2I.js → c-GFASNR55.js} +1 -1
  64. package/dist/m/{c-A4L7APIU.js → c-GNFDX6SU.js} +1 -1
  65. package/dist/m/{c-LATNQ6Q5.js → c-H3M6UDNV.js} +1 -1
  66. package/dist/m/{c-YLHTD3KX.js → c-H4M2T4A6.js} +1 -1
  67. package/dist/m/{c-DDS23EY6.js → c-HXGNH6GY.js} +1 -1
  68. package/dist/m/{c-PDBNCJCT.js → c-HYNM5U6B.js} +1 -1
  69. package/dist/m/c-J2YYLEVM.js +1 -0
  70. package/dist/m/c-J2ZW4HMR.js +1 -0
  71. package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
  72. package/dist/m/c-K2ZHQMPF.js +1 -0
  73. package/dist/m/{c-6IJEVE6B.js → c-KU3PX7LL.js} +1 -1
  74. package/dist/m/{c-YYMZWQLR.js → c-L6KVXPBO.js} +1 -1
  75. package/dist/m/{c-CV4T65JE.js → c-LUBZU4AZ.js} +4 -4
  76. package/dist/m/{c-5FXEKU4Y.js → c-LYNLY3MR.js} +1 -1
  77. package/dist/m/{c-NQAPWM7O.js → c-MHTO3QAD.js} +1 -1
  78. package/dist/m/{c-HDVH3MVC.js → c-MKAUC6DB.js} +1 -1
  79. package/dist/m/c-MLMJUNRV.js +1 -0
  80. package/dist/m/c-MMEVDN6D.js +1 -0
  81. package/dist/m/c-MOP5KBWZ.js +1 -0
  82. package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
  83. package/dist/m/{c-XW5TIYYB.js → c-N4GEJMXW.js} +1 -1
  84. package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
  85. package/dist/m/{c-2PVGJFI5.js → c-NW4ZFKCY.js} +1 -1
  86. package/dist/m/{c-PYG2NGWU.js → c-O3DPFZCE.js} +1 -1
  87. package/dist/m/c-O4YIDULY.js +1 -0
  88. package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
  89. package/dist/m/c-OZOACVUD.js +1 -0
  90. package/dist/m/{c-YK2L436K.js → c-OZVK3TFI.js} +1 -1
  91. package/dist/m/c-P73XPAOA.js +1 -0
  92. package/dist/m/c-PP3CUF3U.js +1 -0
  93. package/dist/m/{c-B7GOFGQR.js → c-Q2CB57UG.js} +1 -1
  94. package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
  95. package/dist/m/{c-LSVN3OLG.js → c-QL4NYM2L.js} +1 -1
  96. package/dist/m/c-RES2S5Q3.js +1 -0
  97. package/dist/m/c-RFGELYEU.js +2 -0
  98. package/dist/m/c-RGFPLQGV.js +1 -0
  99. package/dist/m/c-SMLAJASY.js +1 -0
  100. package/dist/m/{c-MQKSKQHG.js → c-STAPQ2X6.js} +1 -1
  101. package/dist/m/c-SYVF6ZMF.js +1 -0
  102. package/dist/m/{c-SE3SIY7Y.js → c-T5IWJOMW.js} +1 -1
  103. package/dist/m/{c-DD26VXAP.js → c-T7LOTEWO.js} +1 -1
  104. package/dist/m/{c-47IBJGPQ.js → c-TWB5ZQU4.js} +1 -1
  105. package/dist/m/c-U35T5NAY.js +1 -0
  106. package/dist/m/c-U4TWEIKZ.js +1 -0
  107. package/dist/m/c-UW6MMZ4V.js +1 -0
  108. package/dist/m/{c-JWDCQL6J.js → c-V5L6HWVL.js} +1 -1
  109. package/dist/m/c-VGG3UR7I.js +1 -0
  110. package/dist/m/{c-CZHTXGJS.js → c-VO6RSFCO.js} +1 -1
  111. package/dist/m/c-VU4WEWPH.js +4 -0
  112. package/dist/m/c-W7LWCJHD.js +1 -0
  113. package/dist/m/c-WOJ2RTV4.js +1 -0
  114. package/dist/m/{c-T3P3AXEW.js → c-WOWGGNCJ.js} +1 -1
  115. package/dist/m/{c-6RGEQNVV.js → c-XCOQNISP.js} +1 -1
  116. package/dist/m/{c-SGGGCIVD.js → c-XS7G6U3Z.js} +1 -1
  117. package/dist/m/{c-4PO3YGDC.js → c-Y3J5LDKB.js} +1 -1
  118. package/dist/m/c-Y3MPVURA.js +1 -0
  119. package/dist/m/c-Y6HH7LTM.js +1 -0
  120. package/dist/m/{c-RO73MKKR.js → c-YRDKVEGR.js} +1 -1
  121. package/dist/m/{c-FEC6LGFF.js → c-YZGWSCMT.js} +1 -1
  122. package/dist/m/{c-7CM4GMTS.js → c-ZJ7726FO.js} +1 -1
  123. package/dist/m/{c-SKMIR5HQ.js → c-ZPI7M5U5.js} +1 -1
  124. package/dist/m/core/{admin-attrs-ONRWVXPK.js → admin-attrs-43JBA6JB.js} +1 -1
  125. package/dist/m/core/{autosave-TBKAAAFB.js → autosave-HD7Y7UNV.js} +1 -1
  126. package/dist/m/core/{conflict-notice-FOEE6I7E.js → conflict-notice-Y2NTMDGJ.js} +1 -1
  127. package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
  128. package/dist/m/core/{host-permission-X4BENZEU.js → host-permission-5BYP24GC.js} +1 -1
  129. package/dist/m/core/{is-edit-mode-FFHCQ5DR.js → is-edit-mode-73OXKK5H.js} +1 -1
  130. package/dist/m/core/{persist-6ILUR34S.js → persist-MX3VYVBN.js} +1 -1
  131. package/dist/m/core/{recovery-APWADU4K.js → recovery-VZF7RJZF.js} +1 -1
  132. package/dist/m/core/{save-6HT2LABE.js → save-JZ7R4ET2.js} +1 -1
  133. package/dist/m/core/{save-core-EVMNZQ4V.js → save-core-I4N6S4ED.js} +1 -1
  134. package/dist/m/core/{site-edits-G4BOZMV4.js → site-edits-TIJDPG6H.js} +1 -1
  135. package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-N5MKDGNI.js} +1 -1
  136. package/dist/m/core/unsaved-warning-BP6ZY3PG.js +1 -0
  137. package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
  138. package/dist/m/internals/index-3LAVREAU.js +1 -0
  139. package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
  140. package/dist/m/{loader-GLLVDUZA.js → loader-7ORQVRCK.js} +1 -1
  141. package/dist/m/manifest-A27A4278.js +1 -0
  142. package/dist/m/manifest-edit-E8A61EB4.js +1 -0
  143. package/dist/m/manifest.json +1608 -1498
  144. package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
  145. package/dist/m/page/{editor-KAZDJNND.js → editor-HS2CAV3W.js} +5 -5
  146. package/dist/m/plugins/{ai-edit-LABECCSN.js → ai-edit-WDV2GLDU.js} +2 -2
  147. package/dist/m/plugins/{cms-T2HLI7JG.js → cms-4KET6RZT.js} +9 -9
  148. package/dist/m/plugins/{demo-T7B4RIKL.js → demo-LMCMBILC.js} +1 -1
  149. package/dist/m/plugins/{derive-6LPJOCAV.js → derive-R4USHZ7P.js} +1 -1
  150. package/dist/m/plugins/{indicator-J4ABFARX.js → indicator-OLPZ4WFI.js} +1 -1
  151. package/dist/m/plugins/{live-PGBIEMGB.js → live-CO4VJS46.js} +1 -1
  152. package/dist/m/plugins/{menu-YZL3X2AJ.js → menu-ESEFQARM.js} +1 -1
  153. package/dist/m/plugins/{page-WOCNX2IN.js → page-L3VKIQU2.js} +2 -2
  154. package/dist/m/plugins/{page-view-SJODOOGE.js → page-view-6KNTXABH.js} +1 -1
  155. package/dist/m/plugins/{people-C4TCWFOQ.js → people-EBWE337I.js} +1 -1
  156. package/dist/m/plugins/quickcrop-VNRXGRPC.js +26 -0
  157. package/dist/m/plugins/{richclay-Y2NV44NA.js → richclay-V6WKJTUF.js} +17 -17
  158. package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
  159. package/dist/m/plugins/{source-YK7MO23W.js → source-OYHDXKTB.js} +1 -1
  160. package/dist/m/plugins/undo-WUK2NLBA.js +1 -0
  161. package/dist/m/plugins/{upgrade-NNTJBSK2.js → upgrade-SZQJYEQQ.js} +1 -1
  162. package/dist/m/plugins/{upload-ZKKJV76X.js → upload-NZET3DNQ.js} +1 -1
  163. package/dist/m/plugins/visitor-drafts-ZE6PCAFP.js +20 -0
  164. package/dist/m/plugins/{visitor-drafts-owner-LHUHFU3L.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
  165. package/dist/m/plugins/{visitor-menu-FY4KM2SW.js → visitor-menu-5XY57JQA.js} +1 -1
  166. package/dist/m/plugins/wire-S5ATC4QI.js +1 -0
  167. package/dist/m/reader-core-AHOAP5DH.js +77 -0
  168. package/dist/m/sync/live-sync-DX4K5GDB.js +1 -0
  169. package/dist/m/ui/index-G7ZJXNMG.js +1 -0
  170. package/dist/m/vendor/sap-core.vendor-5TWOJYVM.js +7 -0
  171. package/dist/offline-manifest.json +844 -784
  172. package/dist/server/core.cjs +4245 -2614
  173. package/dist/server/markdown.cjs +1 -0
  174. package/dist/server/markdown.mjs +7 -0
  175. package/docs/guide.md +27 -37
  176. package/entries/clay-data.js +1 -1
  177. package/entries/sap.js +6 -5
  178. package/package.json +6 -2
  179. package/packed-contract.json +3 -0
  180. package/src/attrs/save-freeze.js +1 -1
  181. package/src/core/admin-contenteditable.js +1 -1
  182. package/src/core/admin-inputs.js +1 -1
  183. package/src/core/admin-onclick.js +1 -1
  184. package/src/core/admin-resources.js +5 -2
  185. package/src/core/conflict-notice-ui.js +59 -34
  186. package/src/core/draft-mode.js +5 -0
  187. package/src/core/save-core.js +10 -10
  188. package/src/core/save.js +10 -10
  189. package/src/core/snapshot.js +56 -43
  190. package/src/core/stale-host-notice.js +20 -7
  191. package/src/lib/dirty-gate.js +19 -0
  192. package/src/page/editor.js +10 -4
  193. package/src/page/import-markdown.js +22 -8
  194. package/src/page/save-projection.js +1 -1
  195. package/src/plugins/menu/visitor.js +33 -1
  196. package/src/plugins/quickcrop.js +128 -69
  197. package/src/plugins/sortable.js +25 -9
  198. package/src/plugins/visitor-drafts/commit.js +50 -3
  199. package/src/plugins/visitor-drafts/idb.js +8 -2
  200. package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
  201. package/src/plugins/visitor-drafts/recovery.js +64 -0
  202. package/src/plugins/visitor-drafts/session.js +61 -13
  203. package/src/plugins/visitor-drafts/store.js +44 -8
  204. package/src/plugins/visitor-drafts.js +34 -5
  205. package/src/server/markdown/index.js +23 -0
  206. package/src/sync/conflicts.js +3 -1
  207. package/src/sync/data-guard-notice.js +137 -18
  208. package/src/sync/live-sync.js +1 -1
  209. package/src/sync/wire-profile.js +9 -4
  210. package/src/ui/bevel-controls.js +4 -1
  211. package/src/ui/dialogs.js +11 -7
  212. package/src/ui/modal.js +9 -7
  213. package/src/ui/toast.js +67 -32
  214. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  215. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  216. package/src/vendor/hyper-undo.vendor.js +1 -1
  217. package/src/vendor/hypercms.vendor.js +7 -7
  218. package/src/vendor/richclay.vendor.js +16 -16
  219. package/src/vendor/sap-core.vendor.js +6 -5
  220. package/src/vendor/sap-ownership.vendor.js +181 -11
  221. package/tool-generation/context.md +69 -0
  222. package/tool-generation/system.md +2 -2
  223. package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
  224. package/dist/m/c-277QP5ET.js +0 -1
  225. package/dist/m/c-3UW5EJ77.js +0 -1
  226. package/dist/m/c-42ZNV7LI.js +0 -7
  227. package/dist/m/c-6Q6RGFBF.js +0 -1
  228. package/dist/m/c-APGHGDZM.js +0 -1
  229. package/dist/m/c-BGN7RCTJ.js +0 -1
  230. package/dist/m/c-CJ7QYEWI.js +0 -1
  231. package/dist/m/c-CVSWLX3X.js +0 -1
  232. package/dist/m/c-DXPOFLEJ.js +0 -1
  233. package/dist/m/c-EILLN2H2.js +0 -1
  234. package/dist/m/c-EODTTNVA.js +0 -1
  235. package/dist/m/c-ESK4S67I.js +0 -1
  236. package/dist/m/c-FDWR3FL7.js +0 -1
  237. package/dist/m/c-FYDKYW6E.js +0 -1
  238. package/dist/m/c-HKSLOVU4.js +0 -1
  239. package/dist/m/c-HTLYXCUH.js +0 -1
  240. package/dist/m/c-IUL7PU4P.js +0 -1
  241. package/dist/m/c-JQF2EUCG.js +0 -1
  242. package/dist/m/c-MFOSMAVM.js +0 -3
  243. package/dist/m/c-MQYEDF3Q.js +0 -1
  244. package/dist/m/c-MR75YPRT.js +0 -1
  245. package/dist/m/c-NHXOBD6A.js +0 -1
  246. package/dist/m/c-NVU3DORS.js +0 -1
  247. package/dist/m/c-OYHI67B6.js +0 -1
  248. package/dist/m/c-PWJSJEJU.js +0 -1
  249. package/dist/m/c-QENKN7QI.js +0 -1
  250. package/dist/m/c-R23UHMXG.js +0 -1
  251. package/dist/m/c-SGH3UTF3.js +0 -1
  252. package/dist/m/c-SXNSSHPP.js +0 -1
  253. package/dist/m/c-TFLRT5JX.js +0 -1
  254. package/dist/m/c-U4NTUVDZ.js +0 -1
  255. package/dist/m/c-V2U3F4QX.js +0 -1
  256. package/dist/m/c-VG5XIN4T.js +0 -1
  257. package/dist/m/c-VHSMPITY.js +0 -1
  258. package/dist/m/c-Z4G2UK6T.js +0 -4
  259. package/dist/m/core/draft-mode-4MCL3LYI.js +0 -1
  260. package/dist/m/core/unsaved-warning-ITZDIZ2I.js +0 -1
  261. package/dist/m/internals/index-HS3CQDFW.js +0 -1
  262. package/dist/m/manifest-50FF1C45.js +0 -1
  263. package/dist/m/manifest-edit-BE1A621D.js +0 -1
  264. package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
  265. package/dist/m/plugins/sortable-XCHQG65Z.js +0 -1
  266. package/dist/m/plugins/undo-VLRBQNPN.js +0 -1
  267. package/dist/m/plugins/visitor-drafts-RTZNONHT.js +0 -20
  268. package/dist/m/plugins/wire-M2NR354P.js +0 -1
  269. package/dist/m/reader-core-T2S3JC4E.js +0 -77
  270. package/dist/m/sync/live-sync-Z64YREWQ.js +0 -1
  271. package/dist/m/ui/index-MGTWOW2C.js +0 -1
  272. package/dist/m/vendor/sap-core.vendor-QAEPMETX.js +0 -6
  273. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  274. /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-VNRXGRPC.js.LEGAL.txt} +0 -0
  275. /package/dist/m/plugins/{richclay-Y2NV44NA.js.LEGAL.txt → richclay-V6WKJTUF.js.LEGAL.txt} +0 -0
@@ -138,7 +138,7 @@ function wrap(db, keyRange) {
138
138
  // `key`, hands them to `build` inside the transaction, and writes the `puts` and
139
139
  // `deletes` it returns before the transaction can complete. A `build` or a write that
140
140
  // throws aborts it, so nothing lands. Resolves with `build`'s `result` on `complete`.
141
- function change(key, build, { timeout = 0 } = {}) {
141
+ function change(key, build, { timeout = 0, recovery = false } = {}) {
142
142
  return new Promise((resolve, reject) => {
143
143
  let tx;
144
144
  try {
@@ -167,9 +167,14 @@ function wrap(db, keyRange) {
167
167
  const store = tx.objectStore(RECORDS);
168
168
  const rows = keyRange.bound(key + ":op:", key + ":op:\uffff");
169
169
  const reads = { record: store.get(key), meta: store.get(key + ":meta"), v1base: store.get(key + ":v1base"), names: store.getAllKeys(rows), values: store.getAll(rows) };
170
+ const copies = recovery ? keyRange.bound(key + ":recovery:", key + ":recovery:\uffff") : null;
171
+ if (copies) {
172
+ reads.copyNames = store.getAllKeys(copies);
173
+ reads.copyValues = store.getAll(copies);
174
+ }
170
175
  // Requests in one transaction answer in the order they were made: the last one in
171
176
  // means every read is.
172
- reads.values.onsuccess = () => {
177
+ (reads.copyValues ?? reads.values).onsuccess = () => {
173
178
  if (settled) return;
174
179
  try {
175
180
  const raw = {
@@ -177,6 +182,7 @@ function wrap(db, keyRange) {
177
182
  meta: reads.meta.result ?? null,
178
183
  v1base: reads.v1base.result ?? null,
179
184
  rows: reads.names.result.map((name, i) => [name, reads.values.result[i]]),
185
+ ...(copies && { recovery: reads.copyNames.result.map((name, i) => [name, reads.copyValues.result[i]]) }),
180
186
  };
181
187
  const planned = build(raw);
182
188
  for (const [name, value] of planned.puts) store.put(value, name);
@@ -0,0 +1,118 @@
1
+ import { COPY } from "./recovery.js";
2
+ import { bevelBox, bevelButton, bevelText, setShown } from "../../ui/bevel-controls.js";
3
+ import { FONT_MONO } from "../../ui/bevel.js";
4
+ import { resolveTheme } from "../../ui/theme.js";
5
+ import modal, { SURFACE_THEME } from "../../ui/modal.js";
6
+
7
+ export function attachRecoveryMenu(shell, load) {
8
+ let request = 0;
9
+ const refresh = async () => {
10
+ const id = ++request;
11
+ try {
12
+ const recovery = await load();
13
+ const rows = recovery ? await recovery.list() : [];
14
+ if (id !== request) return;
15
+ if (!rows.length) return shell.removeItem("clay.earlier-drafts");
16
+ shell.addItem({ id: "clay.earlier-drafts", label: COPY.label, run: () => openRecovery(recovery, { refresh, anchor: shell.goo }).catch(() => shell.note("The drafts could not be opened. Try again.")) });
17
+ } catch {
18
+ if (id === request) shell.removeItem("clay.earlier-drafts");
19
+ }
20
+ };
21
+ shell.beforeOpen.push(refresh);
22
+ refresh();
23
+ return refresh;
24
+ }
25
+
26
+ export async function openRecovery(recovery, { refresh = () => {}, anchor = document.activeElement } = {}) {
27
+ const theme = resolveTheme();
28
+ const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:16px"], { theme, parts: ["dialog.content"] });
29
+ box.dataset.draftCopies = "";
30
+ box.append(bevelText("p", ["display:block", "margin:0"], COPY.intro, { theme }));
31
+ const status = bevelText("p", ["display:block", "margin:0"], "", { theme });
32
+ status.setAttribute("role", "status");
33
+ const list = bevelBox("ol", ["display:flex", "flex-direction:column", "gap:16px", "padding:0", "margin:0", "list-style:none"], { theme });
34
+ box.append(status, list);
35
+ let busy = false;
36
+
37
+ async function draw() {
38
+ const rows = await recovery.list();
39
+ list.replaceChildren();
40
+ if (!rows.length) status.textContent = "No earlier drafts are kept.";
41
+ for (const row of rows) {
42
+ const item = bevelBox("li", ["display:flex", "flex-direction:column", "gap:8px"], { theme });
43
+ item.dataset.draftMark = row.mark;
44
+ const date = new Date(row.savedAt);
45
+ const when = Number.isNaN(date.getTime()) ? "Time unavailable" : date.toLocaleString();
46
+ const time = bevelText("time", ["display:block", `font-family:${FONT_MONO}`], when, { theme });
47
+ if (row.savedAt) time.dateTime = row.savedAt;
48
+ item.append(time, bevelText("p", ["display:block", "margin:0"], `${row.fields} saved ${row.fields === 1 ? "field" : "fields"}`, { theme, role: "mutedText" }));
49
+ const actions = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px"], { theme });
50
+ const preview = bevelBox("div", ["display:block"], { theme });
51
+ setShown(preview, false);
52
+ const confirm = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", "align-items:center"], { theme });
53
+ setShown(confirm, false);
54
+ const action = (label, fn, options = {}) => {
55
+ const button = bevelButton(label, { small: true, theme, ...options, onClick: async () => {
56
+ if (busy) return;
57
+ busy = true;
58
+ button.setDisabled(true);
59
+ status.textContent = "";
60
+ try { await fn(button); }
61
+ catch (error) { status.textContent = error.message || "That did not work. Try again."; }
62
+ finally { busy = false; button.setDisabled(false); }
63
+ } });
64
+ button.setAttribute("aria-label", `${label}: ${when}`);
65
+ return button;
66
+ };
67
+ const open = action("Preview", () => {
68
+ if (!preview.hidden) { setShown(preview, false); open.setAttribute("aria-expanded", "false"); return; }
69
+ const doc = new DOMParser().parseFromString(recovery.html(row.copy), "text/html");
70
+ for (const el of doc.querySelectorAll('script, meta[http-equiv="refresh" i]')) el.remove();
71
+ for (const el of doc.querySelectorAll("input, textarea, select, button")) el.disabled = true;
72
+ for (const el of doc.querySelectorAll("[contenteditable]")) el.removeAttribute("contenteditable");
73
+ for (const el of doc.querySelectorAll("a, area")) {
74
+ el.removeAttribute("href");
75
+ el.removeAttributeNS("http://www.w3.org/1999/xlink", "href");
76
+ el.removeAttribute("xlink:href");
77
+ }
78
+ const policy = doc.createElement("meta");
79
+ policy.httpEquiv = "Content-Security-Policy";
80
+ policy.content = "default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; form-action 'none'";
81
+ doc.head.prepend(policy);
82
+ const frame = bevelBox("iframe", ["display:block", "width:100%", "height:45vh", "border:0"], { theme });
83
+ frame.title = `Draft preview: ${when}`;
84
+ frame.setAttribute("sandbox", "");
85
+ frame.srcdoc = "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
86
+ preview.replaceChildren(frame);
87
+ setShown(preview, true);
88
+ open.setAttribute("aria-expanded", "true");
89
+ });
90
+ open.setAttribute("aria-expanded", "false");
91
+ const restore = action("Restore", async () => { await recovery.restore(row.mark); status.textContent = "Draft restored. Reloading the page."; await refresh(); });
92
+ const download = action("Download", () => recovery.download(row.copy));
93
+ for (const button of [open, restore, download]) button.setDisabled(!row.available);
94
+ const remove = action("Remove", () => { setShown(confirm, true, "flex"); cancel.focus(); });
95
+ const cancel = bevelButton("Keep draft", { small: true, theme, onClick: () => { setShown(confirm, false); remove.focus(); } });
96
+ confirm.append(bevelText("p", ["margin:0"], "Remove this saved draft?", { theme }), cancel, action("Remove draft", async () => {
97
+ await recovery.remove(row.mark);
98
+ await draw();
99
+ await refresh();
100
+ list.querySelector("button")?.focus();
101
+ }, { variant: "danger" }));
102
+ actions.append(open, restore, download, remove);
103
+ item.append(actions, confirm, preview);
104
+ list.append(item);
105
+ }
106
+ }
107
+
108
+ await draw();
109
+ modal.title = COPY.label;
110
+ modal.html = box;
111
+ modal.width = "720px";
112
+ modal[SURFACE_THEME] = theme;
113
+ modal.closeHtml = "x";
114
+ modal.no = "Close";
115
+ modal.onNo(() => anchor?.focus());
116
+ modal.open();
117
+ return box;
118
+ }
@@ -0,0 +1,64 @@
1
+ import { applyData, extractData } from "../../vendor/hyper-html-api-data.vendor.js";
2
+ import { finalizeControlForSave } from "../../vendor/control-serialize.vendor.js";
3
+ import { downloadHtml, documentName } from "../../core/download.js";
4
+
5
+ export const COPY = {
6
+ label: "Earlier drafts",
7
+ intro: "This page kept your earlier drafts aside, listed by time.",
8
+ };
9
+
10
+ export function createRecovery({ store, session, getHTML = () => window.clay.getHTML(), download = downloadHtml }) {
11
+ const recordOf = (copy) => session.recoveryRecord(copy);
12
+ const savedAt = (copy) => copy?.savedAt ?? copy?.ops?.at(-1)?.savedAt ?? (copy?.at ? new Date(copy.at).toISOString() : null);
13
+
14
+ async function list() {
15
+ const rows = await session.listRecovery();
16
+ return rows.map(({ mark, copy }) => {
17
+ const record = recordOf(copy);
18
+ const fields = copy?.ops ? new Set(copy.ops.flatMap((op) => Object.keys(op.keys ?? {}))).size : Object.keys(record?.data ?? {}).length;
19
+ return { mark, copy, savedAt: savedAt(copy), fields, available: Boolean(record) };
20
+ }).sort((a, b) => (Date.parse(b.savedAt) || 0) - (Date.parse(a.savedAt) || 0));
21
+ }
22
+
23
+ function html(copy) {
24
+ const record = recordOf(copy);
25
+ if (!record) throw new Error("This draft cannot be opened by this version.");
26
+ const doc = new DOMParser().parseFromString(getHTML(), "text/html");
27
+ applyData(doc, store.seed(), store.rules);
28
+ applyData(doc, record.data, store.rules);
29
+ if (!store.fitsInto(record.data, extractData(doc, store.rules))) throw new Error("This draft no longer fits the page. Its saved copy is still kept.");
30
+ const unit = record.blockeditor ? record : store.servedUnit();
31
+ const article = doc.querySelector("article[data-blockeditor]");
32
+ if (unit) {
33
+ if (!article) throw new Error("This draft's page editor is no longer available.");
34
+ for (const tag of doc.querySelectorAll('script[type="application/json"][merge][clay~="live"]')) tag.remove();
35
+ for (const [name, value] of Object.entries(unit.live)) {
36
+ const tag = doc.createElement("script");
37
+ tag.type = "application/json";
38
+ tag.setAttribute("merge", name);
39
+ tag.setAttribute("clay", "live");
40
+ tag.textContent = JSON.stringify(value).replace(/</g, "\\u003c");
41
+ article.before(tag);
42
+ }
43
+ article.setAttribute("data-rev", unit.blockeditor.rev);
44
+ article.innerHTML = unit.blockeditor.html;
45
+ }
46
+ for (const control of doc.querySelectorAll('input:not([type="password"]):not([type="file"]), textarea, select')) finalizeControlForSave(control);
47
+ return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
48
+ }
49
+
50
+ async function downloadCopy(copy) {
51
+ const ok = await download({ html: html(copy), name: documentName() + ".htmlclay" });
52
+ if (!ok) throw new Error("The download could not be made. Try again.");
53
+ return true;
54
+ }
55
+
56
+ async function restore(mark) {
57
+ const row = (await list()).find((row) => row.mark === mark);
58
+ if (!row) throw new Error("That draft is no longer available. Open the list again.");
59
+ html(row.copy);
60
+ return session.restoreRecovery(mark);
61
+ }
62
+
63
+ return { list, html, download: downloadCopy, restore, remove: (mark) => session.removeRecovery(mark) };
64
+ }
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { openDrafts } from "./idb.js";
23
- import { emptyMeta, foreignMark, isForeign, markerFor, markerGen, recoveryKey, recoveryMark, rowKey, settled, transact } from "./commit.js";
23
+ import { emptyMeta, foreignMark, isChain, isForeign, markerFor, markerGen, recoveryKey, recoveryMark, recoveryRecord, rowKey, settled, transact } from "./commit.js";
24
24
  import { setDraftStoreFailed } from "../../core/draft-mode.js";
25
25
 
26
26
  export const FULL_COPY_LIMIT = 256 * 1024;
@@ -91,6 +91,7 @@ export async function openSession({ store, storage, doc = document, now = () =>
91
91
  let sent = null;
92
92
  let carried = 0;
93
93
  let retiring = false;
94
+ let restoringCopy = false;
94
95
  let shownId = store.held()?.id;
95
96
  // The latest full cache copy this tab saw at the key, and the text of another writer's
96
97
  // record a storage event says replaced it: such a record merges from that copy.
@@ -127,6 +128,7 @@ export async function openSession({ store, storage, doc = document, now = () =>
127
128
  seed: store.seed(),
128
129
  newId: store.newId,
129
130
  merge3: store.merge3,
131
+ same: store.same,
130
132
  unitMark: store.unitMark,
131
133
  now: now(),
132
134
  marker: held,
@@ -139,12 +141,12 @@ export async function openSession({ store, storage, doc = document, now = () =>
139
141
  }
140
142
 
141
143
  // Another tab's slot is removed once nothing more can come of it and its tab has gone quiet.
142
- function sweep(meta) {
144
+ function sweep(meta, removedMark) {
143
145
  const at = now();
144
146
  for (const name of store.journalSlots()) {
145
147
  if (name === store.slotKey) continue;
146
148
  const chain = parse(read(name));
147
- if (settled(chain, meta) && at - (chain?.at ?? 0) > SLOT_AGE_MS) remove(name);
149
+ if ((removedMark && isChain(chain) && recoveryMark(chain) === removedMark) || (settled(chain, meta) && at - (chain?.at ?? 0) > SLOT_AGE_MS)) remove(name);
148
150
  }
149
151
  }
150
152
 
@@ -169,20 +171,26 @@ export async function openSession({ store, storage, doc = document, now = () =>
169
171
  try { storage.setItem(key, headOf(record, w)); } catch {}
170
172
  }
171
173
 
174
+ async function stopWithPending() {
175
+ store.stop({ keepPending: true });
176
+ if (store.unsafe()) {
177
+ try { await drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())); }
178
+ catch (error) { throw store.failed(error); }
179
+ store.acknowledge(store.chain().ops.at(-1)?.seq ?? 0);
180
+ }
181
+ }
182
+
172
183
  // Operations captured after the transaction read stay journaled for the next load.
173
184
  async function ended(result) {
185
+ try { store.captureLate(); }
186
+ catch (error) { throw store.failed(error); }
174
187
  retiring = true;
175
188
  const chain = store.chain();
176
189
  const mark = recoveryMark(chain);
177
190
  const kept = chain.ops.some((op) => op.seq > carried) || result.meta.recovery?.includes(mark)
178
191
  ? mark : result.meta.recovery?.includes(shownId) ? shownId : null;
179
192
  store.acknowledge(carried);
180
- store.stop({ keepPending: true });
181
- if (store.unsafe()) {
182
- try { await drafts.putRow(rowKey(key, store.tab), JSON.stringify(store.chain())); }
183
- catch (error) { throw store.failed(error); }
184
- store.acknowledge(store.chain().ops.at(-1)?.seq ?? 0);
185
- }
193
+ await stopWithPending();
186
194
  cache(result);
187
195
  if (kept) rememberRecovery(kept);
188
196
  const held = store.held();
@@ -277,13 +285,14 @@ export async function openSession({ store, storage, doc = document, now = () =>
277
285
  gen = meta.gen;
278
286
  rememberGeneration(meta.reset);
279
287
  const saved = store.acknowledge(meta.tabs[store.tab]?.seq ?? 0);
280
- sweep(meta);
288
+ sweep(meta, result.removedMark);
281
289
  cache(result);
282
290
  if (store.pending()) return result;
283
291
  if (saved) {
284
292
  setDraftStoreFailed(false);
285
293
  doc.dispatchEvent(new CustomEvent("clay:drafts-saved", { detail: { data: record.data } }));
286
294
  }
295
+ if (restoringCopy) return result;
287
296
  if (!store.shown()) {
288
297
  rev = meta.rev;
289
298
  return result;
@@ -303,8 +312,15 @@ export async function openSession({ store, storage, doc = document, now = () =>
303
312
  // so it carries every operation made before then.
304
313
  let active = null;
305
314
  let waiting = null;
306
- function run(options) {
307
- active = transact(drafts, key, input, options).then(took, (error) => {
315
+ function run(options, recovery = null) {
316
+ const given = () => {
317
+ if (recovery?.type === "restore") {
318
+ if (store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
319
+ store.captureNow();
320
+ }
321
+ return { ...input(), recovery };
322
+ };
323
+ active = transact(drafts, key, given, options).then(took, (error) => {
308
324
  throw store.pending() ? store.failed(error) : error;
309
325
  }).finally(() => { active = null; });
310
326
  return active;
@@ -318,6 +334,38 @@ export async function openSession({ store, storage, doc = document, now = () =>
318
334
  });
319
335
  return waiting;
320
336
  }
337
+ async function changeRecovery(recovery) {
338
+ while (active || waiting) await (waiting ?? active).catch(() => {});
339
+ return run({ recovery: true }, recovery);
340
+ }
341
+
342
+ async function restoreRecovery(mark) {
343
+ if (restoringCopy) throw new Error("A draft is already being restored.");
344
+ restoringCopy = true;
345
+ try {
346
+ if (store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
347
+ store.captureNow();
348
+ let result;
349
+ do {
350
+ result = await changeRecovery({ type: "restore", mark, reset: store.generation() });
351
+ if (!result.restoredCopy || store.stopped()) throw new Error("This page has started over. Reload it before restoring a draft.");
352
+ } while (store.pending());
353
+ // This tab's pre-restore page is retired without ending the other tabs' generation.
354
+ try { store.captureLate("restored:" + store.tab); }
355
+ catch (error) { throw store.failed(error); }
356
+ await stopWithPending();
357
+ reload();
358
+ return result;
359
+ } finally { restoringCopy = false; }
360
+ }
361
+
362
+ async function listRecovery() {
363
+ await first.catch(() => {});
364
+ const prefix = key + ":recovery:";
365
+ const rows = await drafts.readAll(key);
366
+ return [...rows].filter(([name]) => name.startsWith(prefix)).map(([name, raw]) => ({ mark: name.slice(prefix.length), copy: parse(raw) }));
367
+ }
368
+
321
369
  const quietly = () => { sync().catch(() => {}); };
322
370
 
323
371
  // At a hide, an operation too large for the journal slot goes into the database blind, with
@@ -341,5 +389,5 @@ export async function openSession({ store, storage, doc = document, now = () =>
341
389
 
342
390
  const first = run({ timeout: FIRST_TRANSACTION_TIMEOUT_MS });
343
391
  first.catch(() => {});
344
- return { first, sync, hide, startOver };
392
+ return { first, sync, hide, startOver, listRecovery, restoreRecovery, recoveryRecord: (copy) => recoveryRecord(copy, store.seed()), removeRecovery: (mark) => changeRecovery({ type: "remove", mark }) };
345
393
  }
@@ -1,5 +1,5 @@
1
1
  import { extractData, applyData, engine } from "../../vendor/hyper-html-api-data.vendor.js";
2
- import { draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
2
+ import { draftGate, draftSlot, setDraftStoreFailed } from "../../core/draft-mode.js";
3
3
 
4
4
  export const KEY_PREFIX = "clay:visitor-drafts:";
5
5
  export const RECORD_VERSION = 1;
@@ -73,6 +73,7 @@ export function same(a, b) {
73
73
 
74
74
  const STORE_SEL = 'script[type="application/json"][merge][clay~="live"]';
75
75
  const ARTICLE_SEL = "article[data-blockeditor]";
76
+ const HANDLER_SEL = "[onbeforesave], [onbeforesnapshot]";
76
77
  const isPagePart = (name) => name === "blockeditor" || /^blockeditor\.\d+$/.test(name);
77
78
  const canonical = (live) => JSON.stringify(Object.keys(live).sort().map((name) => [name, live[name]]));
78
79
 
@@ -269,7 +270,7 @@ function withoutSiteKey(value) {
269
270
  return rest;
270
271
  }
271
272
 
272
- export function createStore({ doc = document, storage = safeStorage(), now = () => new Date(), path = draftPath(), fallback = null, slotLimit = SLOT_LIMIT, pendingLimit = PENDING_LIMIT } = {}) {
273
+ export function createStore({ doc = document, storage = safeStorage(), gate = draftGate, now = () => new Date(), path = draftPath(), fallback = null, slotLimit = SLOT_LIMIT, pendingLimit = PENDING_LIMIT } = {}) {
273
274
  const slot = draftSlot(doc);
274
275
  const key = draftKey(slot, path);
275
276
  const rules = withoutSiteKey(engine.findRulesIn(doc, "api").rules);
@@ -610,16 +611,46 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
610
611
  // to `localStorage` after it made some.
611
612
  let before = null;
612
613
 
613
- function capture(html) {
614
+ // The dirty gate's token for the snapshot `base` was last compared with: taken before
615
+ // that snapshot, in its task. Null while that is not known.
616
+ let capturedAt = null;
617
+
618
+ const pageOf = (html) => (html === undefined ? doc : new DOMParser().parseFromString(html, "text/html"));
619
+
620
+ // `at` is the gate token of the snapshot `html` was serialized from.
621
+ function capture(html, { reset = tab.reset, at = null } = {}) {
622
+ if (stopped) return null;
623
+ const parsed = pageOf(html);
624
+ const op = captureData(extractData(parsed, rules), recordable(readUnit(parsed)), reset);
625
+ capturedAt = at;
626
+ return op;
627
+ }
628
+
629
+ // The page as it is now.
630
+ function captureNow(reset = tab.reset) {
631
+ if (stopped) return null;
632
+ const at = gate()?.token() ?? null;
633
+ return capture(window.clay.getHTML(), { reset, at });
634
+ }
635
+
636
+ // The last capture of a generation that has ended. A capture runs the page's save
637
+ // handlers, and a handler that writes a changing value makes a field differ with no
638
+ // edit behind it. So on a page with a save handler that has recorded no edit since
639
+ // `base` was captured, the fields are what `base` holds, and only the page editor's
640
+ // part, which no handler writes, is compared.
641
+ function captureLate(reset = tab.reset) {
614
642
  if (stopped) return null;
643
+ const settled = capturedAt !== null && doc.querySelector(HANDLER_SEL) !== null && gate()?.unchangedSince(capturedAt) === true;
644
+ const parsed = pageOf(window.clay?.getHTML?.());
645
+ return captureData(settled ? base : extractData(parsed, rules), recordable(readUnit(parsed)), reset);
646
+ }
647
+
648
+ function captureData(mine, mineUnit, reset = tab.reset) {
615
649
  if (unparked != null && park(unparked, { keepSlot: true }) === null) {
616
650
  throw storageFailure(doc, new Error("an earlier saved copy could not be kept aside"));
617
651
  }
618
652
  // Too much is waiting: nothing more is taken until it drains, and the page keeps it.
619
653
  if (tab.sizes.reduce((sum, size) => sum + size, 0) >= pendingLimit) throw storageFailure(doc, new Error("too many changes are waiting to be saved"));
620
- const parsed = new DOMParser().parseFromString(html, "text/html");
621
- const mine = extractData(parsed, rules);
622
- const mineUnit = recordable(readUnit(parsed));
623
654
  const unitChanged = !sameUnit(mineUnit, editorUnit);
624
655
  const keys = {};
625
656
  for (const name of Object.keys(mine)) {
@@ -644,6 +675,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
644
675
  }
645
676
  if (unitChanged) editorUnit = seenUnit = mineUnit;
646
677
  fresh = false;
678
+ tab.reset = reset;
647
679
  tab.ops.push(op);
648
680
  tab.sizes.push(JSON.stringify(op).length);
649
681
  journal();
@@ -655,7 +687,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
655
687
  function keep() {
656
688
  if (stopped) return true;
657
689
  try {
658
- if (changed()) capture(window.clay.getHTML());
690
+ if (changed()) captureNow();
659
691
  } catch { return false; }
660
692
  return tab.journaled === tab.seq;
661
693
  }
@@ -680,6 +712,7 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
680
712
  // unsaved changes, for `write` to save.
681
713
  function release() {
682
714
  if (tab.ops.length) ({ base, editorUnit, seenUnit } = before);
715
+ capturedAt = null;
683
716
  drop();
684
717
  }
685
718
 
@@ -815,8 +848,11 @@ export function createStore({ doc = document, storage = safeStorage(), now = ()
815
848
 
816
849
  return {
817
850
  slot, key, rules, read, restore, write, writeLive, flush, changed, adoptExternal, externalUnit, adoptedUnit, park, clear, snapshot,
818
- tab: tab.id, slotKey, chain, capture, keep, acknowledge, release, take, stop, journalSlots,
851
+ tab: tab.id, slotKey, chain, capture, captureNow, captureLate, keep, acknowledge, release, take, stop, journalSlots,
819
852
  seed: seedData,
853
+ servedUnit: () => { readServed(); return served; },
854
+ fitsInto,
855
+ same,
820
856
  // For the session, which is fetched after the boot and imports nothing from here.
821
857
  merge3,
822
858
  unitMark,
@@ -1,6 +1,6 @@
1
1
  import { isDraftMode, isEditMode, isOwner } from "../core/is-edit-mode.js";
2
2
  import { hasSaveToken } from "../core/host-attrs.js";
3
- import { draftSlot, draftStoreFailed, setDraftTransport, setDraftFlush, setDraftUnconfirmed } from "../core/draft-mode.js";
3
+ import { draftGate, draftSlot, draftStoreFailed, setDraftTransport, setDraftFlush, setDraftUnconfirmed } from "../core/draft-mode.js";
4
4
  import { reloadAfterDiscard } from "../core/unsaved-warning.js";
5
5
  import { createStore, adoptLegacy, draftPath, legacyKey, safeStorage, storageFailure } from "./visitor-drafts/store.js";
6
6
  import { createUiMemory } from "./visitor-drafts/ui-memory.js";
@@ -72,13 +72,13 @@ if (isDraftMode && slot && rulesTag) {
72
72
  .then((m) => m.openSession({ store, storage: local, reload: () => reloadAfterDiscard({ isCurrent: () => true, reload: () => window.location.reload() }) }))
73
73
  .then((opened) => { session = opened; }, () => { direct = true; });
74
74
 
75
- setDraftTransport((html) => {
75
+ setDraftTransport((html, at = null) => {
76
76
  const recapture = !restoredDetail;
77
77
  return restoring.then(() => {
78
78
  if (refused) throw storageFailure(document, new Error("the draft database did not open"));
79
- if (recapture) html = window.clay.getHTML();
79
+ if (recapture) [at, html] = [draftGate()?.token() ?? null, window.clay.getHTML()];
80
80
  if (direct) return void store.write(html);
81
- store.capture(html);
81
+ store.capture(html, { at });
82
82
  return opening.then(() => {
83
83
  if (session) return session.sync().then(() => {});
84
84
  store.write(html);
@@ -131,6 +131,26 @@ if (isDraftMode && slot && rulesTag) {
131
131
  };
132
132
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") keep(); });
133
133
  window.addEventListener("pagehide", keep);
134
+ // A link marked clay-wait-drafts (the host's download and host actions) waits for this
135
+ // page's last changes to reach the download page before it navigates, like the prompt's
136
+ // Download button. A new tab or a modified click leaves this page open, so it does not wait.
137
+ let leaving = false;
138
+ document.addEventListener("click", async (event) => {
139
+ // composedPath reaches into open shadow roots such as Hyperclay's trial bar.
140
+ const link = event.composedPath().find((node) => node instanceof Element && node.matches("a[clay-wait-drafts][href]"));
141
+ if (!link || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
142
+ if (link.target && link.target !== "_self") return;
143
+ if (leaving) { event.preventDefault(); return; }
144
+ const waiting = settle();
145
+ if (!waiting) return;
146
+ event.preventDefault();
147
+ leaving = true;
148
+ let timer;
149
+ await Promise.race([waiting, new Promise((resolve) => { timer = setTimeout(resolve, DOWNLOAD_WAIT_MS); })]);
150
+ clearTimeout(timer);
151
+ leaving = false;
152
+ window.location.assign(link.href);
153
+ }, true);
134
154
 
135
155
  // Another tab's save reaches a page on the database through its session. With
136
156
  // `localStorage` as the record it arrives as that key's storage event, once the page
@@ -197,7 +217,7 @@ export const visitorDrafts = store ? {
197
217
  saveNow() {
198
218
  if (refused || !restoredDetail) return false;
199
219
  if (direct) return store.writeLive();
200
- try { store.capture(window.clay.getHTML()); } catch { return false; }
220
+ try { store.captureNow(); } catch { return false; }
201
221
  session?.sync().catch(() => {});
202
222
  return !store.unsafe();
203
223
  },
@@ -232,3 +252,12 @@ export const visitorDrafts = store ? {
232
252
  }),
233
253
  source,
234
254
  } : inertVisitorDrafts();
255
+
256
+ // Internal to the visitor menu; owner saving and the public visitorDrafts API stay separate.
257
+ export async function recoveryDrafts() {
258
+ await restoring;
259
+ await opening;
260
+ if (!store || !session || refused) return null;
261
+ const { createRecovery } = await import("./visitor-drafts/recovery.js");
262
+ return createRecovery({ store, session });
263
+ }
@@ -0,0 +1,23 @@
1
+ import { Marked } from "../../vendor/marked.vendor.js";
2
+ import { importMarkdownTokens, MarkdownImportError, MAX_BYTES } from "../../page/import-markdown.js";
3
+ import { fragmentCleaner, project, renderModel, revOf } from "../../page/projection.js";
4
+ import { documentTitle } from "../../page/document-title.js";
5
+
6
+ export { MarkdownImportError, MAX_BYTES };
7
+
8
+ /**
9
+ * Markdown text as a Block editor article: the inner HTML of `<article data-blockeditor>`,
10
+ * the `data-rev` that article carries, and the title a save would write. The same codec
11
+ * and projection the editor uses, so the editor opens the result as its own page.
12
+ * `document` is any DOM document (jsdom on a server); raw HTML blocks are cleaned with it.
13
+ * `resolveImage(src)` may return a replacement address for an image, or null; `resolveLink(href)`
14
+ * does the same for a link (a link to another local file is otherwise kept as text).
15
+ */
16
+ export function markdownArticle(text, { document, resolveImage = null, resolveLink = null } = {}) {
17
+ if (!document) throw new TypeError("markdownArticle needs a DOM document");
18
+ const tokens = new Marked().lexer(String(text ?? ""));
19
+ const { blocks, unsupported, refused } = importMarkdownTokens(tokens, { document, resolveImage, resolveLink });
20
+ const model = project(blocks);
21
+ const html = renderModel(model, { fallbackOf: () => "", clean: fragmentCleaner(document) });
22
+ return { html, rev: revOf(model), title: documentTitle(model), unsupported, refused };
23
+ }
@@ -62,8 +62,10 @@ function changed(detail) {
62
62
  // A report is a loss unless the engine says this tab's operation survived, or a
63
63
  // hook kept the incoming change off the page. One the engine could not map to the
64
64
  // live page ("missing-output") still replaced this tab's work: it shows, without
65
- // a place to point at.
65
+ // a place to point at. A region whose incoming version always wins (no-dirty,
66
+ // no-watch) held nothing to keep, so replacing it is never a loss.
66
67
  function isLoss(c) {
68
+ if (c.kind === 'structure' && c.detail === 'remote-wins') return false;
67
69
  const r = c.recovery;
68
70
  if (!r) return true;
69
71
  if (r.applied === false && r.unavailable === 'hook-veto') return false;