@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
@@ -2,12 +2,14 @@ import { isEditMode } from "./is-edit-mode.js";
2
2
  import { rowOf, plain } from "./conflict-presentation.js";
3
3
  import { downloadRecovery, downloadStored } from "./conflict-download.js";
4
4
  import { pendingConflicts, listConflicts, dismissConflicts } from "../recovery/conflicts.js";
5
- import { set, make } from "../lib/hostile-css.js";
5
+ import { set, make, style } from "../lib/hostile-css.js";
6
6
  import { isSaveConflicted } from "./save.js";
7
7
  import { gateCaptureToken } from "../lib/dirty-gate.js";
8
8
  import { reloadAfterDiscard } from "./unsaved-warning.js";
9
9
  import { bevelButton, bevelIconButton, bevelSurface, bevelWell, bevelText, protectIcon, paintLook, RUNTIME_ONLY } from "../ui/bevel-controls.js";
10
10
  import { TOKENS, FONT_SANS, FONT_MONO, GLYPHS } from "../ui/bevel.js";
11
+ import { resolveTheme } from "../ui/theme.js";
12
+ import { themed, partRules, markParts } from "../ui/theme-parts.js";
11
13
 
12
14
  // One notice for everything this tab holds that the page no longer shows: edits
13
15
  // another save replaced (the ledger, clay.conflicts) and a save the host refused.
@@ -72,6 +74,9 @@ let copiedId = null;
72
74
  let copiedTimer = null;
73
75
 
74
76
  let root = null;
77
+ // The theme every surface is drawn with: captured once, when the notice draws, and
78
+ // carried into each control it builds.
79
+ let theme = null;
75
80
  let wellEl = null;
76
81
  let keyed = new Map();
77
82
 
@@ -114,6 +119,15 @@ function key(node, name) {
114
119
  return node;
115
120
  }
116
121
 
122
+ // An element built with el() carries no part machinery of its own: mark it, and let the
123
+ // page's explicit declarations land after everything it was drawn with.
124
+ function part(node, names) {
125
+ if (!themed(theme)) return node;
126
+ markParts(node, theme, names);
127
+ style(node, partRules(theme, names, {}));
128
+ return node;
129
+ }
130
+
117
131
  // A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to
118
132
  // the layout viewport, which parks a bottom-anchored bar behind the keyboard at the
119
133
  // exact moment somebody is typing. Lift it by the difference instead.
@@ -147,6 +161,7 @@ export function schedule() {
147
161
  }
148
162
 
149
163
  function render() {
164
+ theme = resolveTheme();
150
165
  if (checkRefusal) {
151
166
  checkRefusal = false;
152
167
  if (refused && !isSaveConflicted()) refused = null;
@@ -196,11 +211,18 @@ function render() {
196
211
  // A request for a control the toast is standing in for waits until the toast goes.
197
212
  if (!toastText) pendingFocus = null;
198
213
  place();
199
- paintLook(root);
214
+ if (themed(theme)) {
215
+ markParts(root, theme, ["conflict.root"]);
216
+ style(root, partRules(theme, ["conflict.root"], {}));
217
+ set(root, "color-scheme", theme.scheme === "light dark" ? "light" : theme.scheme);
218
+ } else {
219
+ root.removeAttribute("data-clay-part");
220
+ paintLook(root);
221
+ }
200
222
  }
201
223
 
202
224
  function dot() {
203
- return el("span", ["all:initial", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${WARN}`]);
225
+ return part(el("span", ["all:initial", "display:inline-block", "flex:none", "width:8px", "height:8px", "border-radius:50%", `background:${WARN}`]), ["conflict.dot"]);
204
226
  }
205
227
 
206
228
  function refusedSource() {
@@ -230,7 +252,8 @@ function titleOf(m, n) {
230
252
  }
231
253
 
232
254
  function drawToast(text) {
233
- const t = bevelSurface("div", ["display:block", `border-left:3px solid ${TOKENS.teal}`, "padding:10px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
255
+ const t = bevelSurface("div", ["display:block", `background:${TOKENS["teal-soft"]}`, "padding:10px 14px", "width:max-content", "max-width:calc(100vw - 24px)"], { theme, parts: ["conflict.panel"] });
256
+ if (themed(theme)) style(t, [`background:${TOKENS["teal-soft"]}`]);
234
257
  t.setAttribute("role", "status");
235
258
  t.textContent = text;
236
259
  return t;
@@ -238,19 +261,19 @@ function drawToast(text) {
238
261
 
239
262
  function drawBar(m) {
240
263
  const n = m === "failed" ? failures().length : m === "recovered" ? storedCount() : losses().length;
241
- const bar = bevelSurface("div", ["display:flex", "flex-wrap:wrap", "align-items:center", "gap:10px 12px", "padding:8px 8px 8px 14px", "width:max-content", "max-width:calc(100vw - 24px)"]);
264
+ const bar = bevelSurface("div", ["display:flex", "flex-wrap:wrap", "align-items:center", "gap:10px 12px", "padding:8px 8px 8px 14px", "width:max-content", "max-width:calc(100vw - 24px)"], { theme, parts: ["conflict.panel"] });
242
265
  // A redraw rebuilds the bar; only a new message is announced again.
243
266
  const message = barMessage(m, n);
244
267
  if (message !== lastAnnounced) {
245
268
  bar.setAttribute("role", m === "refused" ? "alert" : "status");
246
269
  lastAnnounced = message;
247
270
  }
248
- bar.append(dot(), bevelText("span", [], message));
271
+ bar.append(dot(), bevelText("span", [], message, { theme, parts: ["conflict.text"] }));
249
272
  if (m === "upgraded") {
250
- bar.append(key(bevelButton("Reload", { variant: "primary", onClick: () => window.location.reload() }), "reload"));
273
+ bar.append(key(bevelButton("Reload", { variant: "primary", onClick: () => window.location.reload(), theme, parts: ["conflict.button"], labelParts: ["conflict.buttonLabel"] }), "reload"));
251
274
  return bar;
252
275
  }
253
- bar.append(key(bevelButton("Review", { onClick: openPanel }), "review"));
276
+ bar.append(key(bevelButton("Review", { onClick: openPanel, theme, parts: ["conflict.button"], labelParts: ["conflict.buttonLabel"] }), "review"));
254
277
  return bar;
255
278
  }
256
279
 
@@ -259,11 +282,11 @@ function action(label, name, { variant = "default", onClick, push = false } = {}
259
282
  const extra = phone
260
283
  ? ["flex:1 1 100%", ...(variant === "primary" ? ["order:-1"] : [])]
261
284
  : (push ? ["margin-left:auto"] : []);
262
- return key(bevelButton(label, { variant, onClick, extra }), name);
285
+ return key(bevelButton(label, { variant, onClick, extra, theme, parts: ["conflict.button"], labelParts: ["conflict.buttonLabel"] }), name);
263
286
  }
264
287
 
265
288
  function lede(text) {
266
- return bevelText("div", ["display:block", "padding:0 16px 12px", `color:${TOKENS["ink-2"]}`, "font-size:13.5px"], text);
289
+ return bevelText("div", ["display:block", "padding:0 16px 12px", `color:${TOKENS["ink-2"]}`, "font-size:13.5px"], text, { theme, parts: ["conflict.text"] });
267
290
  }
268
291
 
269
292
  function drawPanel(m) {
@@ -275,13 +298,13 @@ function drawPanel(m) {
275
298
 
276
299
  const title = titleOf(m, count);
277
300
  const maxHeight = Math.max(160, (window.visualViewport?.height || window.innerHeight) - 32);
278
- const panel = bevelSurface("div", ["display:flex", "flex-direction:column", "width:520px", "max-width:calc(100vw - 24px)", `max-height:${maxHeight}px`]);
301
+ const panel = bevelSurface("div", ["display:flex", "flex-direction:column", "width:520px", "max-width:calc(100vw - 24px)", `max-height:${maxHeight}px`], { theme, parts: ["conflict.panel"] });
279
302
  panelEl = panel;
280
303
  panel.setAttribute("role", "dialog");
281
304
  panel.setAttribute("aria-label", title);
282
305
 
283
- const head = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", "padding:12px 12px 8px 16px", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`]);
284
- const t = key(bevelText("b", ["font-weight:600", "font-size:14.5px", "outline:none"], title), "title");
306
+ const head = part(el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", "padding:12px 12px 8px 16px", `font:14px/1.5 ${FONT_SANS}`, `color:${TOKENS.ink}`]), ["conflict.header"]);
307
+ const t = key(bevelText("b", ["font-weight:600", "font-size:14.5px", "outline:none"], title, { theme, parts: ["conflict.title"] }), "title");
285
308
  t.setAttribute("tabindex", "-1");
286
309
  head.append(dot(), t);
287
310
  if (m === "replaced" && freshIds.size) {
@@ -291,7 +314,7 @@ function drawPanel(m) {
291
314
  ], `${freshIds.size} new`));
292
315
  }
293
316
  const minimizeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
294
- const minimize = key(bevelIconButton(MINIMIZE_SVG, { label: "Minimize", onClick: minimizePanel }), "minimize");
317
+ const minimize = key(bevelIconButton(MINIMIZE_SVG, { label: "Minimize", onClick: minimizePanel, theme, parts: ["close", "conflict.close"] }), "minimize");
295
318
  minimize.title = "Minimize to the bar. Nothing is accepted until you choose.";
296
319
  minimizeWrap.append(minimize);
297
320
  head.append(minimizeWrap);
@@ -310,11 +333,11 @@ function drawPanel(m) {
310
333
  if (keepFailed) panel.append(lede("That did not save either. Your edits here are still here."));
311
334
  if (armLost) panel.append(lede("Something changed on the page after your first press, so nothing was dropped. Press Accept theirs again if you still want their version."));
312
335
  if (armed) {
313
- const warn = el("div", [
336
+ const warn = part(el("div", [
314
337
  "all:initial", "display:block", "margin:0 16px 12px", "padding:8px 10px",
315
- `border-left:3px solid ${WARN}`, `background:color-mix(in srgb, ${WARN} 14%, transparent)`,
338
+ `background:color-mix(in srgb, ${WARN} 14%, transparent)`,
316
339
  `color:${TOKENS.ink}`, `font:13px/1.5 ${FONT_SANS}`,
317
- ]);
340
+ ]), ["conflict.warning"]);
318
341
  warn.append(el("b", ["all:initial", "font:inherit", "font-weight:700", "color:inherit"], "This drops your unsaved edits."), document.createTextNode(" Download your copy first if you might want them. Press again to confirm."));
319
342
  panel.append(warn);
320
343
  }
@@ -326,7 +349,7 @@ function drawPanel(m) {
326
349
  panel.append(lede("The page is showing the other edit. This stays until you choose. You can also save a copy of the page as you had it."));
327
350
  }
328
351
 
329
- const actions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]);
352
+ const actions = part(el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]), ["conflict.actions"]);
330
353
  if (m === "upgraded") {
331
354
  actions.append(action("Reload", "reload", { variant: "primary", onClick: () => window.location.reload(), push: true }));
332
355
  panel.append(actions);
@@ -354,7 +377,7 @@ function drawPanel(m) {
354
377
  if (!listOpen) {
355
378
  const more = el("div", ["all:initial", "display:flex", "align-items:center", "gap:8px", `border-top:1px solid ${TOKENS.line}`, "padding:8px 10px 8px 16px", `color:${TOKENS.muted}`, `font:13px/1.5 ${FONT_SANS}`]);
356
379
  const seeWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
357
- seeWrap.append(key(bevelButton("See the edits", { small: true, onClick: toggleList }), "list"));
380
+ seeWrap.append(key(bevelButton("See the edits", { small: true, onClick: toggleList, theme, parts: ["conflict.button"], labelParts: ["conflict.buttonLabel"] }), "list"));
358
381
  more.append(el("span", ["all:initial", "font:inherit", "color:inherit"], plural(rows.length, "edit")), seeWrap);
359
382
  panel.append(more);
360
383
  } else {
@@ -369,10 +392,10 @@ function drawPanel(m) {
369
392
 
370
393
  function drawWell(rows) {
371
394
  // The sunk ground, not the recess: the recess's lighter blue glares against the panel in dark mode.
372
- const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`]);
395
+ const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`], { theme, parts: ["conflict.well", "conflict.list"] });
373
396
  const head = el("div", ["all:initial", "position:sticky", "top:0", "z-index:1", "display:flex", "align-items:center", "gap:8px", "padding:8px 12px", `background:${TOKENS.sunk}`, `border-bottom:1px solid ${TOKENS["line-2"]}`, `color:${TOKENS.muted}`, `font:600 10.5px/1 ${FONT_MONO}`, "letter-spacing:.12em", "text-transform:uppercase"]);
374
397
  const hideWrap = el("span", ["all:initial", "display:inline-flex", "margin-left:auto"]);
375
- hideWrap.append(key(bevelButton("Hide", { small: true, variant: "quiet", onClick: toggleList }), "list"));
398
+ hideWrap.append(key(bevelButton("Hide", { small: true, variant: "quiet", onClick: toggleList, theme, parts: ["conflict.button"], labelParts: ["conflict.buttonLabel"] }), "list"));
376
399
  head.append(el("span", ["all:initial", "font:inherit", "color:inherit", "letter-spacing:inherit", "text-transform:inherit"], "Replaced edits"), hideWrap);
377
400
  well.append(head);
378
401
  rows.forEach((r, i) => well.append(buildRow(r, i)));
@@ -483,8 +506,8 @@ function undoActionsOf(entry) {
483
506
  const storedSide = (text) => ({ before: "", hit: text, after: "", cutBefore: false, cutAfter: false });
484
507
 
485
508
  function storedRow(entry) {
486
- const box = bevelText("div", ["display:block", "padding:8px 12px 10px"]);
487
- const where = bevelText("div", ["display:flex", "align-items:center", "gap:8px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"]);
509
+ const box = bevelText("div", ["display:block", "padding:8px 12px 10px"], undefined, { theme, parts: ["conflict.row"], role: null });
510
+ const where = bevelText("div", ["display:flex", "align-items:center", "gap:8px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"], undefined, { theme, parts: ["conflict.location"], role: null });
488
511
  // No eye: there is no live element to ring, and an old recovery path may name a
489
512
  // place today's page no longer has.
490
513
  where.append(bevelText("span", [], storedName(entry)));
@@ -514,44 +537,44 @@ function drawRecovered(panel) {
514
537
  ? "These copies were replaced before this page loaded. The page never puts them back on its own. Copy or download the ones you want; dismiss one when you no longer need it."
515
538
  : "These copies were kept because an undo could not be put back or the page set them aside. The page never puts them back on its own. Copy or download the ones you want; dismiss one when you no longer need it."));
516
539
  if (actionFailed) panel.append(lede("That action did not run. The copy is still here."));
517
- const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`]);
540
+ const well = bevelWell(["flex:1 1 auto", "margin:0 16px 12px", "min-height:0", "overflow:auto", `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`], { theme, parts: ["conflict.well", "conflict.list"] });
518
541
  for (const record of recovered) for (const entry of record.conflicts) well.append(storedRow(entry));
519
542
  for (const entry of recoveredUndo) well.append(storedRow(entry));
520
543
  wellEl = well;
521
544
  panel.append(well);
522
- const actions = el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]);
545
+ const actions = part(el("div", ["all:initial", "display:flex", "flex-wrap:wrap", "gap:8px", "padding:0 16px 14px"]), ["conflict.actions"]);
523
546
  if (recovered.some((record) => record.root) || recoveredUndo.length) actions.append(action(downloaded ? "Downloaded" : "Download my copy", "download", { onClick: onDownloadRecovered }));
524
547
  panel.append(actions);
525
548
  renderedIds = [];
526
549
  }
527
550
 
528
551
  function valueLine(label, value, { mine }) {
529
- const cells = [bevelText("span", [`color:${TOKENS.faint}`, `font-family:${FONT_SANS}`], label)];
552
+ const cells = [bevelText("span", [`color:${TOKENS.faint}`, `font-family:${FONT_SANS}`], label, { theme, parts: ["conflict.valueLabel"], role: null })];
530
553
  const box = bevelText("span", [
531
554
  "display:-webkit-box", "-webkit-line-clamp:2", "-webkit-box-orient:vertical", "overflow:hidden",
532
555
  "overflow-wrap:anywhere",
533
556
  ...(mine
534
557
  ? [`color:${TOKENS.ink}`]
535
558
  : [`color:${TOKENS.muted}`]),
536
- ]);
559
+ ], undefined, { theme, parts: [mine ? "conflict.mine" : "conflict.theirs", "conflict.value"], role: null });
537
560
  const full = (value.cutBefore ? "…" : "") + value.before + value.hit + value.after + (value.cutAfter ? "…" : "");
538
561
  box.title = full;
539
- if (value.before) box.append(bevelText("span", [`color:${TOKENS.faint}`], (value.cutBefore ? "…" : "") + value.before));
562
+ if (value.before) box.append(bevelText("span", [`color:${TOKENS.faint}`], (value.cutBefore ? "…" : "") + value.before, { theme, parts: ["conflict.before"], role: null }));
540
563
  const hit = bevelText("span", mine
541
564
  ? [`background:color-mix(in srgb, ${WARN} 18%, transparent)`, "padding:0 4px"]
542
565
  : (value.before || value.after
543
566
  ? ["text-decoration:underline", `text-decoration-color:color-mix(in srgb, ${WARN} 70%, transparent)`, "text-underline-offset:3px"]
544
- : []), value.hit || "(empty)");
567
+ : []), value.hit || "(empty)", { theme, parts: ["conflict.value"], role: null });
545
568
  box.append(hit);
546
- if (value.after) box.append(bevelText("span", [`color:${TOKENS.faint}`], value.after + (value.cutAfter ? "…" : "")));
569
+ if (value.after) box.append(bevelText("span", [`color:${TOKENS.faint}`], value.after + (value.cutAfter ? "…" : ""), { theme, parts: ["conflict.after"], role: null }));
547
570
  cells.push(box);
548
571
  return cells;
549
572
  }
550
573
 
551
574
  function buildRow(record, index) {
552
575
  const row = rowOf(record);
553
- const box = bevelText("div", ["display:block", "padding:8px 12px 10px", ...(index ? [`border-top:1px solid ${TOKENS.line}`] : [])]);
554
- const where = bevelText("div", ["display:flex", "align-items:center", "gap:4px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"]);
576
+ const box = bevelText("div", ["display:block", "padding:8px 12px 10px", ...(index ? [`border-top:1px solid ${TOKENS.line}`] : [])], undefined, { theme, parts: ["conflict.row"], role: null });
577
+ const where = bevelText("div", ["display:flex", "align-items:center", "gap:4px", "font-size:12.5px", "font-weight:600", `color:${TOKENS["ink-2"]}`, "margin-bottom:4px"], undefined, { theme, parts: ["conflict.location"], role: null });
555
578
  where.append(bevelText("span", [], row.name));
556
579
  if (row.target && row.target.isConnected) where.append(key(eyeButton(row), `eye:${record.id}`));
557
580
  box.append(where);
@@ -574,7 +597,9 @@ function eyeButton(row) {
574
597
  protectIcon(b.firstElementChild, 16);
575
598
  b.setAttribute("aria-label", `Show ${row.name} on page`);
576
599
  b.title = "Show on page";
577
- const paint = (extra) => { b.style.cssText = ""; for (const r of [...base, ...extra]) { const i = r.indexOf(":"); b.style.setProperty(r.slice(0, i), r.slice(i + 1), "important"); } };
600
+ const names = ["button", "conflict.button"];
601
+ markParts(b, theme, names);
602
+ const paint = (extra) => { b.style.cssText = ""; const rules = themed(theme) ? [...base, ...partRules(theme, names, {}), ...extra] : [...base, ...extra]; for (const r of rules) { const i = r.indexOf(":"); b.style.setProperty(r.slice(0, i), r.slice(i + 1), "important"); } };
578
603
  b.addEventListener("pointerenter", () => paint([`background:color-mix(in srgb, ${WARN} 16%, transparent)`]));
579
604
  b.addEventListener("pointerleave", () => paint([]));
580
605
  b.addEventListener("focus", () => paint([`outline:2px solid ${TOKENS.brass}`, "outline-offset:1px"]));
@@ -630,7 +655,7 @@ function ring(target) {
630
655
  window.scrollBy?.({ top: shift, behavior });
631
656
  }
632
657
  ringTarget = target;
633
- ringEl = el("div", ["all:initial", "color-scheme:inherit", "position:fixed", "pointer-events:none", "z-index:2147483001", `outline:2px solid ${WARN}`, "outline-offset:3px", `background:color-mix(in srgb, ${WARN} 14%, transparent)`]);
658
+ ringEl = part(el("div", ["all:initial", "color-scheme:inherit", "position:fixed", "pointer-events:none", "z-index:2147483001", `outline:2px solid ${WARN}`, "outline-offset:3px", `background:color-mix(in srgb, ${WARN} 14%, transparent)`]), ["conflict.previewRing"]);
634
659
  document.body.appendChild(ringEl);
635
660
  placeRing();
636
661
  window.addEventListener("scroll", placeRing, true);
@@ -45,6 +45,11 @@ export function draftEligible(doc = document, loc = window.location) {
45
45
  && rulesReady(doc);
46
46
  }
47
47
 
48
+ // The dirty gate once it counts this page's edits: `token()` for the page as it is now,
49
+ // and `unchangedSince(token)`. Null until then, and a store that cannot ask assumes edits.
50
+ let gate = null;
51
+ export function setDraftGate(value) { gate = value; }
52
+ export function draftGate() { return gate; }
48
53
  export function setDraftTransport(fn) { transport = fn; }
49
54
  export function draftTransport() { return transport; }
50
55
  let flushHook = null;
@@ -665,12 +665,12 @@ function draftResponse(status, body) {
665
665
  return { ok: status >= 200 && status < 300, status, statusText: status === 200 ? "OK" : "Insufficient Storage", text: () => Promise.resolve(text) };
666
666
  }
667
667
 
668
- function sendToDraftStore(content) {
668
+ function sendToDraftStore(content, gateToken) {
669
669
  const send = draftTransport();
670
670
  if (!send) return Promise.resolve(draftResponse(503, { msg: "Saving in this browser is not ready yet", msgType: "error", code: "draft-not-ready" }));
671
671
  // The transport's promise settles once the draft database holds the save.
672
672
  return Promise.resolve()
673
- .then(() => send(content))
673
+ .then(() => send(content, gateToken))
674
674
  .then(
675
675
  () => draftResponse(200, { msg: "Saved in this browser", msgType: "success" }),
676
676
  (error) => draftResponse(507, { msg: "Couldn't save in this browser. Storage is full or turned off.", msgType: "error", code: error?.code || "draft-storage" }),
@@ -698,7 +698,7 @@ function sendToDraftStore(content) {
698
698
  * @param {Array} [context.attempts] - collects each unique actual send
699
699
  * @returns {Promise<Object>} The server's response body
700
700
  */
701
- function sendOnce(content, saveId, etag, { captureId = null, conversionHeader = null, attempts = [] } = {}) {
701
+ function sendOnce(content, saveId, etag, { captureId = null, conversionHeader = null, attempts = [], gateToken = null } = {}) {
702
702
  const controller = new AbortController();
703
703
  const timeoutId = setTimeout(() => controller.abort(), SAVE_TIMEOUT_MS);
704
704
 
@@ -727,7 +727,7 @@ function sendOnce(content, saveId, etag, { captureId = null, conversionHeader =
727
727
  notifySaveAttempt({ captureId, saveId, html: content, etag: etag ?? null });
728
728
  }
729
729
 
730
- return (isDraftMode ? sendToDraftStore(content) : fetch(url, options))
730
+ return (isDraftMode ? sendToDraftStore(content, gateToken) : fetch(url, options))
731
731
  .then(res => res.text().then(text => {
732
732
  let data = {};
733
733
  if (text) {
@@ -891,7 +891,7 @@ async function recoverUnknownSave() {
891
891
  * @param {Array} [context.attempts] - collects each unique actual send
892
892
  * @returns {Promise<Object>} The server's response body
893
893
  */
894
- async function sendSave(content, { captureId = null, conversionHeader = null, attempts = [] } = {}) {
894
+ async function sendSave(content, { captureId = null, conversionHeader = null, attempts = [], gateToken = null } = {}) {
895
895
  // The re-send below is a RETRY of this attempt, so it carries this same id:
896
896
  // whichever of the two requests the host ends up storing, its receipt then
897
897
  // answers for this save. A fresh id on the re-send would leave the first
@@ -916,7 +916,7 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
916
916
  const retryGuard = captureId ?? currentCaptureId();
917
917
  for (;;) {
918
918
  try {
919
- return await sendOnce(content, saveId, etag, { captureId, conversionHeader, attempts });
919
+ return await sendOnce(content, saveId, etag, { captureId, conversionHeader, attempts, gateToken });
920
920
  } catch (err) {
921
921
  if (reSent) err.hadTimeout = true;
922
922
  if (err.name === 'AbortError') {
@@ -971,7 +971,7 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
971
971
  // `blockeditorFormatFloor` is internal: one save may carry a floor this tab has not
972
972
  // noted yet, which is how the format gate's activation writes the marker. It
973
973
  // raises these bytes only, never what the tab knows the document at.
974
- function runSave(html, captureId, callback, conversion, capture = null, blockeditorFormatFloor = 0, lane = null) {
974
+ function runSave(html, captureId, callback, conversion, capture = null, blockeditorFormatFloor = 0, lane = null, gateToken = null) {
975
975
  // A capture taken before the floor rose cannot drop the marker by omitting it:
976
976
  // the bytes that go out carry the floor this tab last saw. At floor 0 this is
977
977
  // the string itself, unchanged.
@@ -1039,7 +1039,7 @@ function runSave(html, captureId, callback, conversion, capture = null, blockedi
1039
1039
  return finished(tooLargeResult(size));
1040
1040
  }
1041
1041
 
1042
- sendSave(html, { captureId, conversionHeader: formatted.header, attempts })
1042
+ sendSave(html, { captureId, conversionHeader: formatted.header, attempts, gateToken })
1043
1043
  .then(successResult, errorResult)
1044
1044
  // Clear the flag BEFORE handing the result back, so a caller that queues a
1045
1045
  // follow-up save can start it immediately rather than being told the lane
@@ -1088,8 +1088,8 @@ export function saveHtml(html, callback = () => {}) {
1088
1088
  * @param {Object} [options.conversion] - {source:'file', base} or {source:'room', base, epoch}
1089
1089
  * @returns {Promise<Object>}
1090
1090
  */
1091
- export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion, capture = null, blockeditorFormatFloor, lane = null } = {}) {
1092
- return runSave(html, captureId, callback, conversion, capture, blockeditorFormatFloor, lane);
1091
+ export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion, capture = null, blockeditorFormatFloor, lane = null, gateToken = null } = {}) {
1092
+ return runSave(html, captureId, callback, conversion, capture, blockeditorFormatFloor, lane, gateToken);
1093
1093
  }
1094
1094
 
1095
1095
  /**
package/src/core/save.js CHANGED
@@ -63,7 +63,7 @@ import { beginRecoveryCapture, finishRecoveryCapture, noteRecoveryCapture, noteR
63
63
  // content. Both clones get this, so the dirty comparison sees no difference.
64
64
  addDocumentTransform(clone => {
65
65
  for (const name of ROOT_LIBRARY_ATTRS) clone.removeAttribute(name);
66
- });
66
+ }, { required: true });
67
67
 
68
68
  // Keep the host's save token out of the saved bytes, both spellings, and the response
69
69
  // metadata with it.
@@ -83,7 +83,7 @@ addDocumentTransform(clone => {
83
83
  // delete page content.
84
84
  addDocumentTransform(clone => {
85
85
  for (const name of [...SAVE_TOKEN_ATTRS, ...LEGACY_SAVE_TOKEN_ATTRS, ...HOST_RESPONSE_ATTRS]) clone.removeAttribute(name);
86
- });
86
+ }, { required: true });
87
87
 
88
88
  // ============================================
89
89
  // SAVE STATE MANAGEMENT
@@ -826,9 +826,9 @@ function applySaveResult(result, forComparison, forDirty, label, gateToken, forS
826
826
  captureId,
827
827
  };
828
828
  if (!navigator.onLine) {
829
- setSaveState('offline', result.msg, '', extra);
829
+ setSaveState('offline', result.msg, result.msgType || 'error', extra);
830
830
  } else {
831
- setSaveState('error', result.msg, '', extra);
831
+ setSaveState('error', result.msg, result.msgType || 'error', extra);
832
832
  }
833
833
  }
834
834
  }
@@ -1106,7 +1106,7 @@ async function settleCaptured({
1106
1106
  // lane busy and waits that request out, which is what its 'clean' answer describes.
1107
1107
  // A prepared save holds the lane itself, so its caller does the draining instead.
1108
1108
  if (!laneHeld) drainPendingSave();
1109
- }, { capture, blockeditorFormatFloor, lane });
1109
+ }, { capture, blockeditorFormatFloor, lane, gateToken });
1110
1110
  });
1111
1111
  }
1112
1112
 
@@ -1432,11 +1432,11 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
1432
1432
  console.error(`${explicit ? 'savePage' : 'savePageThrottled'}: captureForSaveAndComparison failed`, err);
1433
1433
  if (explicit) {
1434
1434
  clearExplicitSave();
1435
- setSaveState('error', err.message, '', { sent: false, code: err.code ?? null });
1435
+ setSaveState('error', err.message, 'error', { sent: false, code: err.code ?? null });
1436
1436
  } else {
1437
- setSaveState('error', err.message);
1437
+ setSaveState('error', err.message, 'error');
1438
1438
  }
1439
- const result = { msg: err.message, msgType: 'error', code: explicit ? (err.code ?? null) : null, etag: null };
1439
+ const result = { ok: false, msg: err.message, msgType: 'error', code: explicit ? (err.code ?? null) : null, etag: null };
1440
1440
  if (typeof callback === 'function') {
1441
1441
  callback(result);
1442
1442
  }
@@ -1590,8 +1590,8 @@ export function savePageForce(callback = () => {}, { quietConflict = false, bloc
1590
1590
  ({ forSave, forComparison, forDirty, captureId } = capture);
1591
1591
  } catch (err) {
1592
1592
  console.error('savePageForce: captureForSaveAndComparison failed', err);
1593
- setSaveState('error', err.message, '', { sent: false, code: err.code ?? null });
1594
- const result = { msg: err.message, msgType: 'error', code: err.code ?? null, etag: null };
1593
+ setSaveState('error', err.message, 'error', { sent: false, code: err.code ?? null });
1594
+ const result = { ok: false, msg: err.message, msgType: 'error', code: err.code ?? null, etag: null };
1595
1595
  if (typeof callback === 'function') {
1596
1596
  callback(result);
1597
1597
  }
@@ -196,6 +196,45 @@ function runAuthoredHandlers(clone, attr) {
196
196
  }
197
197
  }
198
198
 
199
+ // A registered callback that throws is skipped, so one broken hook cannot stop every
200
+ // save. It is reported once per hook, not on every autosave. Two kinds still stop the
201
+ // save: ClayJS's own required steps (skipping one, such as making admin-only scripts
202
+ // inert, would write the wrong bytes), and an error with a `code`, which is a hook
203
+ // refusing the capture on purpose (clay.live does this).
204
+ const reportedHooks = new WeakSet();
205
+ const requiredHooks = new WeakSet();
206
+ function reportHookError(kind, hook, err) {
207
+ if (requiredHooks.has(hook) || typeof err?.code === "string") throw err;
208
+ if (reportedHooks.has(hook)) return;
209
+ reportedHooks.add(hook);
210
+ console.error(`clayjs: a ${kind} callback threw; the save goes on without it`, err);
211
+ }
212
+
213
+ // Skipping a callback that threw is only safe when it left the clone as it found it.
214
+ // One that changed the clone first stops the capture, because its half-done changes
215
+ // (rows removed but not yet rebuilt) would otherwise be saved.
216
+ function runHook(kind, hook, clone, call) {
217
+ const watch = typeof MutationObserver === 'function' ? new MutationObserver(() => {}) : null;
218
+ watch?.observe(clone, { subtree: true, childList: true, attributes: true, characterData: true });
219
+ try {
220
+ call();
221
+ } catch (err) {
222
+ if (!watch || watch.takeRecords().length > 0) throw err;
223
+ reportHookError(kind, hook, err);
224
+ } finally {
225
+ watch?.disconnect();
226
+ }
227
+ }
228
+
229
+ // Every registered transform, each on a clone with the strip-on-save regions removed.
230
+ // One that throws is skipped and reported once.
231
+ function runDocumentTransforms(clone) {
232
+ for (const hook of documentTransforms) {
233
+ stripSnapshotRegions(clone);
234
+ runHook('addDocumentTransform', hook, clone, () => hook(clone));
235
+ }
236
+ }
237
+
199
238
  let cloneCanonicalizer = null;
200
239
 
201
240
  export function setCloneCanonicalizer(canonicalize) {
@@ -214,9 +253,11 @@ export function canonicalizeReceived(doc) {
214
253
  * Use for: syncing form values to the clone.
215
254
  *
216
255
  * @param {Function} callback - Receives the cloned document element
256
+ * @param {{required?: boolean}} [options] - required: a throw stops the save instead of being skipped
217
257
  */
218
- export function onSnapshot(callback) {
258
+ export function onSnapshot(callback, { required = false } = {}) {
219
259
  snapshotHooks.push(callback);
260
+ if (required) requiredHooks.add(callback);
220
261
  }
221
262
 
222
263
  /**
@@ -381,9 +422,11 @@ function serializeSaveClone(clone, context = null) {
381
422
  * different visibility rules, which is why neither run can be skipped.
382
423
  *
383
424
  * @param {Function} callback - Receives the cloned document element
425
+ * @param {{required?: boolean}} [options] - required: a throw stops the save instead of being skipped
384
426
  */
385
- export function addDocumentTransform(callback) {
427
+ export function addDocumentTransform(callback, { required = false } = {}) {
386
428
  documentTransforms.push(callback);
429
+ if (required) requiredHooks.add(callback);
387
430
  }
388
431
 
389
432
  // =============================================================================
@@ -439,7 +482,7 @@ export function captureSnapshot({ flushUndo = true, authored = true, forSave = f
439
482
 
440
483
  for (const hook of snapshotHooks) {
441
484
  stripSnapshotRegions(clone);
442
- hook(clone, { original: originalSnapshotNode, forSave, requestRefresh });
485
+ runHook('onSnapshot', hook, clone, () => hook(clone, { original: originalSnapshotNode, forSave, requestRefresh }));
443
486
  }
444
487
 
445
488
  // Put back any URL clay rewrote at runtime (cache-bust, refetch-on-save) so
@@ -489,10 +532,7 @@ function prepareCloneForSave(clone, { authored = true } = {}) {
489
532
  if (authored) runAuthoredHandlers(clone, 'onbeforesave');
490
533
 
491
534
  // Run registered prepare hooks ([freeze]/[save-freeze] innerHTML restore lives here)
492
- for (const hook of documentTransforms) {
493
- stripSnapshotRegions(clone);
494
- hook(clone);
495
- }
535
+ runDocumentTransforms(clone);
496
536
 
497
537
  stripSnapshotRegions(clone);
498
538
 
@@ -575,10 +615,7 @@ export function captureForComparison({ flushUndo = true } = {}) {
575
615
  }
576
616
 
577
617
  // Run registered prepare hooks
578
- for (const hook of documentTransforms) {
579
- stripSnapshotRegions(clone);
580
- hook(clone);
581
- }
618
+ runDocumentTransforms(clone);
582
619
 
583
620
  stripSnapshotRegions(clone);
584
621
 
@@ -608,10 +645,7 @@ export function captureForDirtyCheck({ flushUndo = true } = {}) {
608
645
  el.remove();
609
646
  }
610
647
 
611
- for (const hook of documentTransforms) {
612
- stripSnapshotRegions(clone);
613
- hook(clone);
614
- }
648
+ runDocumentTransforms(clone);
615
649
 
616
650
  stripSnapshotRegions(clone);
617
651
 
@@ -642,10 +676,7 @@ export function captureForComparisonAndDirty({ flushUndo = true } = {}) {
642
676
  for (const el of clone.querySelectorAll(STRIP_FROM_COMPARISON)) {
643
677
  el.remove();
644
678
  }
645
- for (const hook of documentTransforms) {
646
- stripSnapshotRegions(clone);
647
- hook(clone);
648
- }
679
+ runDocumentTransforms(clone);
649
680
  stripSnapshotRegions(clone);
650
681
  const forComparison = serializeForCompare(clone);
651
682
 
@@ -654,10 +685,7 @@ export function captureForComparisonAndDirty({ flushUndo = true } = {}) {
654
685
  for (const el of dirtyClone.querySelectorAll(STRIP_FROM_DIRTY_CHECK)) {
655
686
  el.remove();
656
687
  }
657
- for (const hook of documentTransforms) {
658
- stripSnapshotRegions(dirtyClone);
659
- hook(dirtyClone);
660
- }
688
+ runDocumentTransforms(dirtyClone);
661
689
  stripSnapshotRegions(dirtyClone);
662
690
  forDirty = serializeForCompare(dirtyClone);
663
691
  }
@@ -726,10 +754,7 @@ export function prepareSaveCapture({ emitForSync = true, flushUndo = true, reque
726
754
 
727
755
  // Save clone: run hooks (freeze restore lives here), THEN strip [no-save]/[save-remove]
728
756
  // LAST (snapshot-algorithm step 7) so freeze-restored [no-save] content can't leak to disk.
729
- for (const hook of documentTransforms) {
730
- stripSnapshotRegions(clone);
731
- hook(clone);
732
- }
757
+ runDocumentTransforms(clone);
733
758
  stripSnapshotRegions(clone);
734
759
  for (const el of clone.querySelectorAll(STRIP_FROM_SAVE)) {
735
760
  el.remove();
@@ -740,10 +765,7 @@ export function prepareSaveCapture({ emitForSync = true, flushUndo = true, reque
740
765
  for (const el of compareClone.querySelectorAll(STRIP_FROM_COMPARISON)) {
741
766
  el.remove();
742
767
  }
743
- for (const hook of documentTransforms) {
744
- stripSnapshotRegions(compareClone);
745
- hook(compareClone);
746
- }
768
+ runDocumentTransforms(compareClone);
747
769
  stripSnapshotRegions(compareClone);
748
770
 
749
771
  // Dirty clone: same shape as the compare clone, one selector weaker.
@@ -751,10 +773,7 @@ export function prepareSaveCapture({ emitForSync = true, flushUndo = true, reque
751
773
  for (const el of dirtyClone.querySelectorAll(STRIP_FROM_DIRTY_CHECK)) {
752
774
  el.remove();
753
775
  }
754
- for (const hook of documentTransforms) {
755
- stripSnapshotRegions(dirtyClone);
756
- hook(dirtyClone);
757
- }
776
+ runDocumentTransforms(dirtyClone);
758
777
  stripSnapshotRegions(dirtyClone);
759
778
  }
760
779
 
@@ -882,10 +901,7 @@ export function captureForMerge() {
882
901
  }
883
902
  })(compareClone, clone);
884
903
 
885
- for (const hook of documentTransforms) {
886
- stripSnapshotRegions(clone);
887
- hook(clone);
888
- }
904
+ runDocumentTransforms(clone);
889
905
  stripSnapshotRegions(clone);
890
906
  for (const el of clone.querySelectorAll(STRIP_FROM_SAVE)) {
891
907
  el.remove();
@@ -895,10 +911,7 @@ export function captureForMerge() {
895
911
  for (const el of compareClone.querySelectorAll(STRIP_FROM_DIRTY_CHECK)) {
896
912
  el.remove();
897
913
  }
898
- for (const hook of documentTransforms) {
899
- stripSnapshotRegions(compareClone);
900
- hook(compareClone);
901
- }
914
+ runDocumentTransforms(compareClone);
902
915
  stripSnapshotRegions(compareClone);
903
916
 
904
917
  return { saveClone: clone, compareClone, pairMap };