@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
@@ -1,7 +1,7 @@
1
1
  import { servedStaleToken } from "./host-attrs.js";
2
2
  import { isEditMode } from "./is-edit-mode.js";
3
3
  import onDomReady from "../lib/dom-ready.js";
4
- import { set, make } from "../lib/hostile-css.js";
4
+ import { set, style, make } from "../lib/hostile-css.js";
5
5
 
6
6
  // The page's only visible word on a host too old to save to.
7
7
  //
@@ -47,12 +47,13 @@ function dismiss() {
47
47
  }
48
48
 
49
49
  function build() {
50
+ const theme = ui.resolveTheme();
50
51
  root = ui.bevelSurface("div", [
51
52
  "position:fixed", "left:50%", "transform:translateX(-50%)",
52
53
  "z-index:2147483001", "display:flex", "align-items:center", "gap:10px 12px",
53
54
  "width:max-content", "max-width:calc(100vw - 24px)", "flex-wrap:wrap",
54
55
  "padding:8px 8px 8px 14px", "text-align:left", `color-scheme:${ui.pageScheme()}`,
55
- ]);
56
+ ], { theme, parts: ["notice.root", "notice.panel", "staleHost.root", "staleHost.panel"] });
56
57
  // Three markers, and each is load-bearing on a page that CAN save: this element is
57
58
  // injected, so it is in no document on disk and must never reach one, never wake the
58
59
  // watcher, and never ride out in a snapshot to somebody else's browser.
@@ -62,13 +63,23 @@ function build() {
62
63
 
63
64
  const dot = make("span", ["all:initial", "color-scheme:inherit", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${ui.TOKENS.ox}`]);
64
65
  dot.setAttribute("clay", ui.RUNTIME_ONLY);
65
- const close = ui.bevelButton("Dismiss", { small: true, variant: "quiet", onClick: dismiss });
66
+ const dotNames = ["notice.dot", "staleHost.dot"];
67
+ if (ui.themed(theme)) {
68
+ ui.markParts(dot, theme, dotNames);
69
+ style(dot, ui.partRules(theme, dotNames, {}));
70
+ }
71
+ const close = ui.bevelButton("Dismiss", {
72
+ small: true, variant: "quiet", onClick: dismiss, theme,
73
+ parts: ["notice.button", "staleHost.button"],
74
+ labelParts: ["notice.buttonLabel", "staleHost.buttonLabel"],
75
+ });
66
76
  close.setAttribute("aria-label", "Dismiss this message");
67
- root.append(dot, ui.bevelText("span", [], MESSAGE), close);
77
+ root.append(dot, ui.bevelText("span", [], MESSAGE, { theme, parts: ["notice.text", "staleHost.text"] }), close);
68
78
 
69
79
  document.body.appendChild(root);
70
80
  place();
71
- ui.paintLook(root, { background: ui.TOKENS.surface });
81
+ if (ui.themed(theme)) set(root, "color-scheme", theme.scheme === "light dark" ? "light" : theme.scheme);
82
+ else ui.paintLook(root, { background: ui.TOKENS.surface });
72
83
  window.visualViewport?.addEventListener("resize", place);
73
84
  window.visualViewport?.addEventListener("scroll", place);
74
85
  }
@@ -81,8 +92,10 @@ async function init() {
81
92
  // told its reader the opposite. The message is only true while editing is actually
82
93
  // off, so it is shown only then.
83
94
  if (isEditMode) return;
84
- const [controls, bevel] = await Promise.all([import("../ui/bevel-controls.js"), import("../ui/bevel.js")]);
85
- ui = { ...controls, TOKENS: bevel.TOKENS };
95
+ const [controls, bevel, themeConfig, parts] = await Promise.all([
96
+ import("../ui/bevel-controls.js"), import("../ui/bevel.js"), import("../ui/theme.js"), import("../ui/theme-parts.js"),
97
+ ]);
98
+ ui = { ...controls, ...parts, TOKENS: bevel.TOKENS, resolveTheme: themeConfig.resolveTheme };
86
99
  build();
87
100
  }
88
101
 
@@ -29,6 +29,7 @@ import Mutation from './mutation.js';
29
29
  import { isEditMode } from '../core/is-edit-mode.js';
30
30
  import { STRIP_FROM_DIRTY_CHECK, SNAPSHOT_REMOVE_SELECTOR } from './region-policy.js';
31
31
  import { PERSIST } from './attr-aliases.js';
32
+ import { setDraftGate } from '../core/draft-mode.js';
32
33
 
33
34
  let changes = 0;
34
35
  let clearedAt = 0;
@@ -121,6 +122,7 @@ export function startDirtyGate() {
121
122
  );
122
123
  document.addEventListener('input', onUserInput, true);
123
124
  document.addEventListener('change', onUserInput, true);
125
+ setDraftGate({ token: gateTokenNow, unchangedSince: gateUnchangedSince });
124
126
  }
125
127
 
126
128
  /** The morph-apply window: nothing that happens inside it is a user edit. */
@@ -210,6 +212,23 @@ export function gateCaptureToken() {
210
212
  return { gen: changes, probe };
211
213
  }
212
214
 
215
+ /** A token for the page as it is now: what the browser has queued is counted first. */
216
+ export function gateTokenNow() {
217
+ Mutation._deliverBeforePause();
218
+ return gateCaptureToken();
219
+ }
220
+
221
+ /**
222
+ * True when nothing was recorded since the token was taken and every [persist] control
223
+ * still holds what it held then. Records the browser has queued are delivered first, so
224
+ * the answer holds in the task that made a change too.
225
+ */
226
+ export function gateUnchangedSince(token) {
227
+ if (!token) return false;
228
+ Mutation._deliverBeforePause();
229
+ return changes === token.gen && token.probe.every(([el, sig]) => controlSignature(el) === sig);
230
+ }
231
+
213
232
  /** A change nothing in the DOM recorded: the page holds text a save has not written. */
214
233
  export function gateMarkDirty() {
215
234
  changes++;
@@ -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: {
@@ -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 });
@@ -72,17 +72,49 @@ function copyUrl(loc = window.location) {
72
72
  return `/_/copy${path === "/" ? "" : path}`;
73
73
  }
74
74
 
75
+ // Hyperclay's trial bar, once the visitor closes it, leaves its Host action and a way back to the
76
+ // page (hyperclay server-lib/drop/trial-bar.js): `window.hyperclayBar` holds its state, set before
77
+ // this menu loads, and `hyperclay:bar` reports each change. Download copy is already in the menu.
78
+ function attachBar({ addItem, removeItem }) {
79
+ const draw = (bar) => {
80
+ if (bar?.kind !== "trial" || !bar.dismissed) {
81
+ removeItem("clay.bar.host");
82
+ removeItem("clay.bar.show");
83
+ return;
84
+ }
85
+ const host = Array.isArray(bar.actions) ? bar.actions.find((action) => action?.id === "host") : null;
86
+ if (typeof host?.href === "string") addItem({ id: "clay.bar.host", label: "Host and share online", href: host.href });
87
+ if (typeof bar.show === "function") addItem({ id: "clay.bar.show", label: "Show draft bar", run: () => bar.show() });
88
+ };
89
+ document.addEventListener("hyperclay:bar", (event) => draw(event.detail));
90
+ draw(window.hyperclayBar);
91
+ }
92
+
75
93
  // A visitor's menu holds exactly Undo, Redo, Download copy and Make your own copy (David, Nov 11):
76
- // no theme row, no page items, no Start over.
94
+ // no theme row, no page items, no Start over. While Hyperclay's trial bar is closed it also holds
95
+ // Host and share online and Show draft bar (David, Nov 11, sheet hosts review), and Earlier
96
+ // drafts when this page has recovery copies (David, Nov 13).
77
97
  export function start() {
78
98
  if (document.querySelector('clay-editor-ui[data-clay-ui="menu"]')) return;
79
99
  const shell = createShell({ sections: [["clay.undo"], null], icon: GEAR, label: "Settings" });
80
100
  const { embedded, skin, addItem, removeItem, open, close, note, goo, beforeOpen } = shell;
81
101
  shell.mount(embedded ? [MENU_SAFETY, SHELL_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_SAFETY, SHELL_BASE, SHELL_FLOATING, MENU_SKIN, MENU_TOUCH]);
102
+ let recoveryMenu;
103
+ beforeOpen.push(() => {
104
+ if (typeof globalThis.indexedDB?.open !== "function" || !globalThis.IDBKeyRange) return;
105
+ recoveryMenu ??= import("../visitor-drafts/recovery-menu.js").then(async ({ attachRecoveryMenu }) => {
106
+ const { recoveryDrafts } = await import("../visitor-drafts.js");
107
+ attachRecoveryMenu(shell, recoveryDrafts);
108
+ }).catch((error) => {
109
+ recoveryMenu = null;
110
+ console.warn("visitor-menu: earlier drafts did not load", error);
111
+ });
112
+ });
82
113
  publishLooks();
83
114
  attachUndo({ addItem, beforeOpen });
84
115
  addItem({ id: "clay.download", label: "Download copy", run: () => window.clay.visitorDrafts.download() });
85
116
  addItem({ id: "clay.copy", label: "Make your own copy", href: copyUrl() });
117
+ attachBar({ addItem, removeItem });
86
118
  window.clay.menu = { addItem, removeItem, open, close, note, embedded, button: goo };
87
119
  document.dispatchEvent(new CustomEvent("clay:menu-ready"));
88
120
  }
@@ -13,6 +13,9 @@ import { bevelDialog, dismissOf, keepFocusIn } from "../ui/bevel-dialog.js";
13
13
  import { bevelButton } from "../ui/bevel-controls.js";
14
14
  import { installSkin } from "../ui/vendor-skin.js";
15
15
  import { CSS as SKIN, ROOTS as SKIN_ROOTS } from "../ui/skins/quickcrop.js";
16
+ import { resolveTheme } from "../ui/theme.js";
17
+ import { themed, partRules, markParts } from "../ui/theme-parts.js";
18
+ import { style } from "../lib/hostile-css.js";
16
19
 
17
20
  const WIDTH = 844;
18
21
 
@@ -24,80 +27,136 @@ function isThemodal(m) {
24
27
  typeof m.onYes === "function" && "html" in m;
25
28
  }
26
29
 
27
- export const bevelCropAdapter = {
28
- // The stage has to fit inside the panel: the overlay's 16px side inset, the panel's
29
- // 1px border and the body's 22px padding a side; below it the header, the footer and
30
- // the overlay's top inset. Widths come from clientWidth, which leaves out a classic
31
- // scrollbar as the overlay does.
32
- fit() {
33
- const width = document.documentElement.clientWidth;
34
- return {
35
- width: Math.max(120, Math.min(WIDTH, width - 32) - 2 - 44),
36
- height: Math.max(120, window.innerHeight - Math.min(96, window.innerHeight * 0.1) - 200),
37
- };
38
- },
39
- open({ content, confirmLabel, onConfirm, onCancel }) {
40
- const { root, overlay, panel, heading, body, footer, close } = bevelDialog({
41
- zIndex: "2147483001", width: `${WIDTH}px`, closable: true, titled: true,
42
- });
43
- heading.textContent = "Crop image";
44
- panel.setAttribute("aria-label", "Crop image");
45
- body.append(content);
46
- const confirm = bevelButton(confirmLabel, { variant: "primary", onClick: () => onConfirm() });
47
- footer.append(confirm);
48
-
49
- close.addEventListener("click", () => onCancel());
50
- panel.addEventListener("submit", (event) => event.preventDefault());
51
- let pressedOnBackdrop = false;
52
- overlay.addEventListener("mousedown", (event) => { pressedOnBackdrop = event.target === overlay; });
53
- overlay.addEventListener("click", (event) => {
54
- if (pressedOnBackdrop && event.target === overlay) onCancel();
55
- pressedOnBackdrop = false;
56
- });
57
- // Capture, and stopped there, so a dialog underneath (a CMS confirm through
58
- // clay.modal) does not take the same key as its own: Escape cancels this crop,
59
- // Tab stays inside it.
60
- const onKey = (event) => {
61
- if (event.key === "Tab") {
30
+ const TITLE = "Crop image";
31
+
32
+ // The frame for one crop, in the theme captured for that crop. Measuring and opening
33
+ // build the same frame, so the stage is sized for the padding, borders, heading and
34
+ // button the person will actually see.
35
+ function cropFrame(theme, confirmLabel, onConfirm = null) {
36
+ const frame = bevelDialog({ zIndex: "2147483001", width: `${WIDTH}px`, closable: true, titled: true, theme, surface: "crop" });
37
+ frame.heading.textContent = TITLE;
38
+ frame.panel.setAttribute("aria-label", TITLE);
39
+ const confirm = bevelButton(confirmLabel, {
40
+ variant: "primary", onClick: onConfirm, theme,
41
+ parts: ["dialog.button", "crop.button"], labelParts: ["dialog.buttonLabel", "crop.buttonLabel"],
42
+ });
43
+ frame.footer.append(confirm);
44
+ return { ...frame, confirm };
45
+ }
46
+
47
+ // The stage is quickcrop's; its parts take only a page's explicit crop.* declarations,
48
+ // so dimming and crop lines keep their contrast unless the page asks otherwise.
49
+ function themeStage(stage, theme) {
50
+ if (!themed(theme)) return;
51
+ const paint = (el, names) => {
52
+ if (!el) return;
53
+ markParts(el, theme, names);
54
+ style(el, partRules(theme, names, {}));
55
+ };
56
+ paint(stage, ["crop.stage"]);
57
+ paint(stage.querySelector(":scope > img"), ["crop.image"]);
58
+ paint(stage.querySelector(".qc-dim"), ["crop.dim"]);
59
+ paint(stage.querySelector(".qc-box"), ["crop.selection"]);
60
+ for (const corner of ["nw", "ne", "sw", "se"]) paint(stage.querySelector(`.qc-handle.qc-${corner}`), ["crop.handle", `crop.handle.${corner}`]);
61
+ }
62
+
63
+ // A themed frame is measured: a hidden copy is mounted for one layout read and removed,
64
+ // without touching focus or the scroll lock. The room left for the stage is the body's
65
+ // inner width, and the overlay's inner height less the empty panel. The exact Bevel
66
+ // frame keeps its fixed allowances.
67
+ function measure(theme, confirmLabel) {
68
+ const { root, overlay, panel, body } = cropFrame(theme, confirmLabel);
69
+ root.setAttribute("aria-hidden", "true");
70
+ style(root, ["visibility:hidden", "pointer-events:none"]);
71
+ document.body.append(root);
72
+ try {
73
+ const px = (value) => parseFloat(value) || 0;
74
+ const o = getComputedStyle(overlay), b = getComputedStyle(body);
75
+ const width = body.clientWidth - px(b.paddingLeft) - px(b.paddingRight);
76
+ const height = overlay.clientHeight - px(o.paddingTop) - px(o.paddingBottom) - panel.offsetHeight;
77
+ return { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)) };
78
+ } finally {
79
+ root.remove();
80
+ }
81
+ }
82
+
83
+ export function createCropAdapter({ theme = null, confirmLabel = "Crop" } = {}) {
84
+ return {
85
+ // Unthemed, the stage fits inside the panel by fixed allowances: the overlay's 16px
86
+ // side inset, the panel's 1px border and the body's 22px padding a side; below it the
87
+ // header, the footer and the overlay's top inset. Widths come from clientWidth, which
88
+ // leaves out a classic scrollbar as the overlay does.
89
+ fit() {
90
+ if (themed(theme)) return measure(theme, confirmLabel);
91
+ const width = document.documentElement.clientWidth;
92
+ return {
93
+ width: Math.max(120, Math.min(WIDTH, width - 32) - 2 - 44),
94
+ height: Math.max(120, window.innerHeight - Math.min(96, window.innerHeight * 0.1) - 200),
95
+ };
96
+ },
97
+ open({ content, confirmLabel: label, onConfirm, onCancel }) {
98
+ const { root, overlay, panel, body, close, confirm } = cropFrame(theme, label, () => onConfirm());
99
+ themeStage(content, theme);
100
+ body.append(content);
101
+
102
+ close.addEventListener("click", () => onCancel());
103
+ panel.addEventListener("submit", (event) => event.preventDefault());
104
+ let pressedOnBackdrop = false;
105
+ overlay.addEventListener("mousedown", (event) => { pressedOnBackdrop = event.target === overlay; });
106
+ overlay.addEventListener("click", (event) => {
107
+ if (pressedOnBackdrop && event.target === overlay) onCancel();
108
+ pressedOnBackdrop = false;
109
+ });
110
+ // Capture, and stopped there, so a dialog underneath (a CMS confirm through
111
+ // clay.modal) does not take the same key as its own: Escape cancels this crop,
112
+ // Tab stays inside it.
113
+ const onKey = (event) => {
114
+ if (event.key === "Tab") {
115
+ event.stopPropagation();
116
+ keepFocusIn(panel, event);
117
+ return;
118
+ }
119
+ if (event.key !== "Escape") return;
62
120
  event.stopPropagation();
63
- keepFocusIn(panel, event);
64
- return;
65
- }
66
- if (event.key !== "Escape") return;
67
- event.stopPropagation();
68
- onCancel();
69
- };
70
- document.addEventListener("keydown", onKey, true);
71
-
72
- // The page does not scroll behind the crop, as it did not under themodal. The
73
- // previous value comes back, so a modal underneath keeps its own lock.
74
- const overflow = document.body.style.overflow;
75
- document.body.style.overflow = "hidden";
76
-
77
- let done = false;
78
- const finish = () => {
79
- if (done) return;
80
- done = true;
81
- document.removeEventListener("keydown", onKey, true);
82
- document.body.style.overflow = overflow;
83
- root.remove();
84
- };
85
- // A modal opened on top supersedes this crop the way it supersedes itself: the
86
- // crop is cancelled, so its promise settles and the next crop can open.
87
- dismissOf.set(root, () => { finish(); onCancel(); });
88
-
89
- root.setAttribute("aria-hidden", "false");
90
- document.body.append(root);
91
- confirm.focus();
92
-
93
- return { close: finish };
94
- },
95
- };
121
+ onCancel();
122
+ };
123
+ document.addEventListener("keydown", onKey, true);
124
+
125
+ // The page does not scroll behind the crop, as it did not under themodal. The
126
+ // previous value comes back, so a modal underneath keeps its own lock.
127
+ const overflow = document.body.style.overflow;
128
+ document.body.style.overflow = "hidden";
129
+
130
+ let done = false;
131
+ const finish = () => {
132
+ if (done) return;
133
+ done = true;
134
+ document.removeEventListener("keydown", onKey, true);
135
+ document.body.style.overflow = overflow;
136
+ root.remove();
137
+ };
138
+ // A modal opened on top supersedes this crop the way it supersedes itself: the
139
+ // crop is cancelled, so its promise settles and the next crop can open.
140
+ dismissOf.set(root, () => { finish(); onCancel(); });
141
+
142
+ root.setAttribute("aria-hidden", "false");
143
+ document.body.append(root);
144
+ confirm.focus();
145
+
146
+ return { close: finish };
147
+ },
148
+ };
149
+ }
150
+
151
+ // The exact Bevel frame, for callers that want ClayJS's crop without the page's theme.
152
+ export const bevelCropAdapter = createCropAdapter();
96
153
 
97
154
  export function quickcrop(file, options = {}) {
98
155
  const modal = options.modal ?? "auto";
99
156
  const framed = modal === "auto" || isThemodal(modal);
100
- return vendorQuickcrop(file, framed ? { ...options, modal: bevelCropAdapter } : options);
157
+ if (!framed) return vendorQuickcrop(file, options);
158
+ const adapter = createCropAdapter({ theme: resolveTheme(), confirmLabel: options.labels?.confirm ?? "Crop" });
159
+ return vendorQuickcrop(file, { ...options, modal: adapter });
101
160
  }
102
161
 
103
162
  export default quickcrop;
@@ -217,15 +217,31 @@ function makeSortable(sortableElem, Sortable, record = loose) {
217
217
  // Add onsorting callback if attribute exists (fires during drag)
218
218
  const onsortingCode = sortableElem.getAttribute('onsorting');
219
219
  if (onsortingCode) {
220
- options.onMove = function(evt) {
221
- if (!record.admitted) return;
222
- try {
223
- const asyncFn = new Function(`return (async function(evt) { ${onsortingCode} })`)();
224
- asyncFn.call(sortableElem, evt);
225
- } catch (error) {
226
- console.error('Error in onsorting execution:', error);
227
- }
228
- };
220
+ // Sortable reads onMove's return value (false cancels the move, 1 or -1 picks a
221
+ // side), so the handler runs synchronously and its value goes back. A handler that
222
+ // awaits cannot answer in time; it still runs, as an async function, and Sortable
223
+ // gets no answer from it.
224
+ // Code that awaits compiles as async: a plain function would read `await (x)` as a
225
+ // call to a function named await and throw on every move.
226
+ let onsorting = null;
227
+ try {
228
+ onsorting = /\bawait\b/.test(onsortingCode)
229
+ ? new Function(`return (async function(evt) { ${onsortingCode} })`)()
230
+ : new Function('evt', onsortingCode);
231
+ } catch (error) {
232
+ console.error('Error in onsorting compilation:', error);
233
+ }
234
+ if (onsorting) {
235
+ options.onMove = function(evt) {
236
+ if (!record.admitted) return;
237
+ try {
238
+ const answer = onsorting.call(sortableElem, evt);
239
+ return answer instanceof Promise ? undefined : answer;
240
+ } catch (error) {
241
+ console.error('Error in onsorting execution:', error);
242
+ }
243
+ };
244
+ }
229
245
  }
230
246
 
231
247
  // After a drop, run any author onsorted code, then notify reactive libraries.
@@ -76,6 +76,22 @@ export function applyKeys(data, keys, merge3) {
76
76
  export const isChain = (value) => isPlain(value) && typeof value.tab === "string" && typeof value.reset === "string" && Array.isArray(value.ops);
77
77
  export const recoveryMark = (copy) => isChain(copy) ? `op:${copy.tab}:${copy.ops.at(-1)?.seq ?? copy.acked ?? 0}` : foreignMark(copy);
78
78
 
79
+ // Ended journals carry only the fields they changed; other fields use the served page.
80
+ export function recoveryRecord(copy, seed) {
81
+ if (isPlain(copy) && copy.v === 1 && isPlain(copy.data)) return clone(copy);
82
+ if (!isChain(copy) || !copy.ops.length) return null;
83
+ const data = clone(seed);
84
+ let unit = null;
85
+ for (const op of copy.ops) {
86
+ for (const [name, pair] of Object.entries(op.keys ?? {})) {
87
+ if ("m" in pair) Object.defineProperty(data, name, { value: clone(pair.m), enumerable: true, writable: true, configurable: true });
88
+ else delete data[name];
89
+ }
90
+ if (op.unit !== undefined) unit = op.unit.to;
91
+ }
92
+ return { v: 1, savedAt: copy.ops.at(-1).savedAt, data, ...(unit && { live: clone(unit.live), blockeditor: clone(unit.blockeditor) }) };
93
+ }
94
+
79
95
  // Cleanup needs proof for this exact chain, even when its generation has ended.
80
96
  export function settled(chain, meta) {
81
97
  if (isPlain(chain) && meta.recovery?.includes(recoveryMark(chain))) return true;
@@ -123,7 +139,7 @@ function foreignBase(line, { record, seen, direct, v1base, meta }) {
123
139
  * @param {?Object} [input.seen] - the latest cache copy the caller saw at the draft's key
124
140
  * @param {boolean} [input.direct] - a storage event says `local` replaced exactly that copy
125
141
  */
126
- export function commit(stored, { seed, newId, now, merge3, unitMark, marker = null, resets = [], chains = [], local = null, seen = null, direct = false }) {
142
+ export function commit(stored, { seed, newId, now, merge3, same, unitMark, marker = null, resets = [], chains = [], local = null, seen = null, direct = false, recovery = null, copies = [] }) {
127
143
  const meta = stored.meta ? clone(stored.meta) : emptyMeta(marker);
128
144
  // A database the browser cleared counts on from this ClayJS's cache, so the cache never
129
145
  // reads as newer than the record.
@@ -260,6 +276,36 @@ export function commit(stored, { seed, newId, now, merge3, unitMark, marker = nu
260
276
  out.applied[chain.tab] = at;
261
277
  }
262
278
 
279
+ if (recovery) {
280
+ const found = copies.find(([mark]) => mark === recovery.mark);
281
+ if (!found) throw new Error("That draft is no longer available. Open the list again.");
282
+ if (recovery.type === "remove") {
283
+ meta.recovery = (meta.recovery ?? []).filter((mark) => mark !== recovery.mark);
284
+ out.dropped.push(recovery.mark);
285
+ out.removedMark = recovery.mark;
286
+ out.changed = true;
287
+ } else if (recovery.type === "restore" && recovery.reset === meta.reset) {
288
+ const chosen = recoveryRecord(found[1], seed);
289
+ if (!chosen) throw new Error("This draft cannot be restored by this version.");
290
+ if (meta.recovery?.includes(recovery.mark)) meta.recovery = [recovery.mark, ...meta.recovery.filter((mark) => mark !== recovery.mark)];
291
+ const alreadyKept = record && copies.some(([mark, copy]) => {
292
+ if (!meta.recovery?.includes(mark)) return false;
293
+ const kept = recoveryRecord(copy, seed);
294
+ return kept && same(record.data, kept.data) && same(unitOf(record), unitOf(kept));
295
+ });
296
+ if (record && !alreadyKept) recover(record);
297
+ record = {
298
+ v: 1, savedAt: new Date(now).toISOString(), id: newId(),
299
+ ancestry: lineage([], record, meta), data: chosen.data,
300
+ ...(chosen.blockeditor && { live: chosen.live, blockeditor: chosen.blockeditor }),
301
+ };
302
+ meta.unit = unitMark(unitOf(record));
303
+ meta.rev++;
304
+ out.changed = true;
305
+ out.restoredCopy = true;
306
+ }
307
+ }
308
+
263
309
  for (const [tab, entry] of Object.entries(meta.tabs)) {
264
310
  if (now - (entry.at ?? 0) > TAB_TTL_MS) {
265
311
  delete meta.tabs[tab];
@@ -285,7 +331,8 @@ export function transact(drafts, key, input, options) {
285
331
  const stored = { meta: parse(raw.meta), record: parse(raw.record), v1base: parse(raw.v1base) };
286
332
  const rows = raw.rows.map(([name, text]) => [name, parse(text)]);
287
333
  const given = input(stored);
288
- const result = commit(stored, { ...given, chains: [...rows.map(([, chain]) => chain), ...(given.chains ?? [])] });
334
+ const copies = (raw.recovery ?? []).map(([name, text]) => [name.slice((key + ":recovery:").length), parse(text)]);
335
+ const result = commit(stored, { ...given, copies, chains: [...rows.map(([, chain]) => chain), ...(given.chains ?? [])] });
289
336
  const puts = [];
290
337
  const deletes = [];
291
338
  if (result.changed) {
@@ -297,7 +344,7 @@ export function transact(drafts, key, input, options) {
297
344
  if (result.conflicts.length) puts.push([conflictKey(key), JSON.stringify(result.conflicts[result.conflicts.length - 1])]);
298
345
  for (const copy of result.recovered) puts.push([recoveryKey(key, recoveryMark(copy)), JSON.stringify(copy)]);
299
346
  for (const mark of result.dropped) deletes.push(recoveryKey(key, mark));
300
- for (const [name, chain] of rows) if (settled(chain, result.meta)) deletes.push(name);
347
+ for (const [name, chain] of rows) if (settled(chain, result.meta) || (result.removedMark && isChain(chain) && recoveryMark(chain) === result.removedMark)) deletes.push(name);
301
348
  return { puts, deletes, result };
302
349
  }, options);
303
350
  }