@panphora/clayjs 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +5570 -4173
  11. package/dist/m/attrs/{save-freeze-MH3AOD56.js → save-freeze-PUYX32AK.js} +1 -1
  12. package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
  13. package/dist/m/c-2EPHRF47.js +1 -0
  14. package/dist/m/{c-JD2VFAYG.js → c-2SUFMUZX.js} +2 -2
  15. package/dist/m/{c-CITLTVQ4.js → c-2TJCFPX4.js} +1 -1
  16. package/dist/m/c-33SS5A4Y.js +1 -0
  17. package/dist/m/{c-V2FPBRO6.js → c-34J5HC7N.js} +1 -1
  18. package/dist/m/c-3F3AADKG.js +1 -0
  19. package/dist/m/{c-TCFVG3I4.js → c-3S3647J6.js} +1 -1
  20. package/dist/m/{c-GY4BREDT.js → c-3S5TNRZA.js} +1 -1
  21. package/dist/m/{c-GDNZEOOZ.js → c-3WX5FABT.js} +1 -1
  22. package/dist/m/{c-AI3H6ETU.js → c-3XGLJAS7.js} +2 -2
  23. package/dist/m/c-42WBOKFM.js +1 -0
  24. package/dist/m/c-47M4QHCK.js +1 -0
  25. package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
  26. package/dist/m/{c-GEAG5XAK.js → c-4TP3UXBT.js} +1 -1
  27. package/dist/m/{c-IESDKFO6.js → c-5ELELG2F.js} +1 -1
  28. package/dist/m/{c-X3V4RWMA.js → c-5ZYLTN3Q.js} +1 -1
  29. package/dist/m/{c-5PZYDWTW.js → c-67TBU56I.js} +1 -1
  30. package/dist/m/{c-WNOLMHI2.js → c-6BGJAJ67.js} +1 -1
  31. package/dist/m/{c-4Q6QIODW.js → c-755ZOHOM.js} +1 -1
  32. package/dist/m/c-7Q4XYDF4.js +1 -0
  33. package/dist/m/c-7RHOMAMR.js +1 -0
  34. package/dist/m/c-A5LASUEZ.js +1 -0
  35. package/dist/m/{c-IL5IVTOG.js → c-ABOBD7F3.js} +1 -1
  36. package/dist/m/c-AQVQKFKB.js +1 -0
  37. package/dist/m/{c-QDVG4NAZ.js → c-ARO6CPMA.js} +1 -1
  38. package/dist/m/{c-ORLPQFNU.js → c-AZM34SPO.js} +1 -1
  39. package/dist/m/{c-XUZMRQ7O.js → c-BCS3AZQZ.js} +1 -1
  40. package/dist/m/c-BOS7GONO.js +1 -0
  41. package/dist/m/{c-SUAC5RSG.js → c-C5UGNOC2.js} +1 -1
  42. package/dist/m/c-C6DXZIXD.js +1 -0
  43. package/dist/m/{c-WSTUCBHV.js → c-CPRGR2II.js} +1 -1
  44. package/dist/m/{c-NICICT4P.js → c-DEYHCYJB.js} +1 -1
  45. package/dist/m/{c-WGQVSKKC.js → c-DYTBSJBI.js} +1 -1
  46. package/dist/m/{c-DT7E76Z2.js → c-DZT6W2NG.js} +1 -1
  47. package/dist/m/{c-O2QZDVZ2.js → c-EEWPNUM2.js} +1 -1
  48. package/dist/m/{c-MFB74XVN.js → c-EHAR4RJJ.js} +2 -2
  49. package/dist/m/c-EHJWIGWN.js +1 -0
  50. package/dist/m/c-EIDZHQHZ.js +1 -0
  51. package/dist/m/{c-V2V5FKXX.js → c-EIUKME3W.js} +1 -1
  52. package/dist/m/c-ELXMSEN5.js +1 -0
  53. package/dist/m/c-EXZOA6XY.js +7 -0
  54. package/dist/m/{c-FKTALXNV.js → c-EYFMAR3M.js} +1 -1
  55. package/dist/m/c-FC5RUFPR.js +1 -0
  56. package/dist/m/{c-FBOO2UY3.js → c-FUN6AKFE.js} +1 -1
  57. package/dist/m/{c-V4B3RUWB.js → c-GFASNR55.js} +1 -1
  58. package/dist/m/{c-XMIDHEKF.js → c-GY36NO7C.js} +1 -1
  59. package/dist/m/{c-UD4VDBGJ.js → c-H3M6UDNV.js} +1 -1
  60. package/dist/m/{c-SFZRY6RT.js → c-H47C6PJZ.js} +1 -1
  61. package/dist/m/{c-MUM53J5J.js → c-H4M2T4A6.js} +1 -1
  62. package/dist/m/{c-U7F66I64.js → c-HYNM5U6B.js} +1 -1
  63. package/dist/m/c-J2YYLEVM.js +1 -0
  64. package/dist/m/c-J2ZW4HMR.js +1 -0
  65. package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
  66. package/dist/m/c-K2ZHQMPF.js +1 -0
  67. package/dist/m/c-KD2ME5UN.js +2 -0
  68. package/dist/m/{c-Z2OZFX3N.js → c-L6KVXPBO.js} +1 -1
  69. package/dist/m/{c-GO635CRU.js → c-LAKBBJOU.js} +2 -2
  70. package/dist/m/{c-4AE2A2LK.js → c-LETPPG2F.js} +1 -1
  71. package/dist/m/{c-SFYTKRU7.js → c-MKAUC6DB.js} +1 -1
  72. package/dist/m/{c-WDFQ7PBU.js → c-MMEVDN6D.js} +1 -1
  73. package/dist/m/{c-LS7IVQVW.js → c-MMV5JULA.js} +1 -1
  74. package/dist/m/{c-NEQRFC4O.js → c-MVVEIJUU.js} +1 -1
  75. package/dist/m/{c-SXZYVXD6.js → c-N362HNGO.js} +1 -1
  76. package/dist/m/{c-7K4SCQB7.js → c-ND3W7D5U.js} +1 -1
  77. package/dist/m/{c-3E2IK5HU.js → c-NNHNXD6M.js} +1 -1
  78. package/dist/m/{c-D62HRSZK.js → c-NYJ577NL.js} +1 -1
  79. package/dist/m/{c-YFO6ZJLQ.js → c-O3DPFZCE.js} +1 -1
  80. package/dist/m/c-OZOACVUD.js +1 -0
  81. package/dist/m/{c-MV7TEUTD.js → c-OZVK3TFI.js} +1 -1
  82. package/dist/m/{c-ZEA3DCG3.js → c-PWILUN4T.js} +1 -1
  83. package/dist/m/{c-BCWYEG6P.js → c-QA3Q3MDI.js} +1 -1
  84. package/dist/m/{c-XHZIKRCV.js → c-QKOKMQXA.js} +1 -1
  85. package/dist/m/{c-TGIMBAWE.js → c-RBWVF4N5.js} +1 -1
  86. package/dist/m/c-RES2S5Q3.js +1 -0
  87. package/dist/m/c-RFGELYEU.js +2 -0
  88. package/dist/m/c-RGFPLQGV.js +1 -0
  89. package/dist/m/{c-RGAALCDU.js → c-SPOEDIMP.js} +1 -1
  90. package/dist/m/{c-YX2MC7YL.js → c-SRW5OBCF.js} +1 -1
  91. package/dist/m/{c-C6YZNU6E.js → c-SYOA4T7V.js} +1 -1
  92. package/dist/m/c-SYVF6ZMF.js +1 -0
  93. package/dist/m/{c-NADLYVAH.js → c-TJ5PFOJ2.js} +1 -1
  94. package/dist/m/{c-KYTVZT4D.js → c-TKV273YU.js} +1 -1
  95. package/dist/m/c-U35T5NAY.js +1 -0
  96. package/dist/m/{c-PRZNJUYZ.js → c-UEKJWXCB.js} +2 -2
  97. package/dist/m/c-UW6MMZ4V.js +1 -0
  98. package/dist/m/{c-XQTERKQA.js → c-V4DBV5ZV.js} +1 -1
  99. package/dist/m/c-VGG3UR7I.js +1 -0
  100. package/dist/m/c-VU4WEWPH.js +4 -0
  101. package/dist/m/{c-5Q5XBN56.js → c-W7LWCJHD.js} +1 -1
  102. package/dist/m/c-WHMZCRQD.js +6 -0
  103. package/dist/m/{c-WMRJKQ2V.js → c-WHQUZM2V.js} +1 -1
  104. package/dist/m/c-WOJ2RTV4.js +1 -0
  105. package/dist/m/{c-ZJG6C2WL.js → c-WOWGGNCJ.js} +1 -1
  106. package/dist/m/{c-ZORVIXUR.js → c-XHD7XJTE.js} +1 -1
  107. package/dist/m/c-XONFK3WC.js +1 -0
  108. package/dist/m/{c-HGG7M2S3.js → c-Y3J5LDKB.js} +1 -1
  109. package/dist/m/{c-JB5YU57S.js → c-Y3MPVURA.js} +1 -1
  110. package/dist/m/c-Y6HH7LTM.js +1 -0
  111. package/dist/m/{c-52OC3OH3.js → c-YRDKVEGR.js} +1 -1
  112. package/dist/m/c-YYIN4NAF.js +30 -0
  113. package/dist/m/{c-WVCT3ITX.js → c-YZGWSCMT.js} +1 -1
  114. package/dist/m/c-ZFAPQQIB.js +1 -0
  115. package/dist/m/core/{admin-attrs-ZI6K6UGM.js → admin-attrs-43JBA6JB.js} +1 -1
  116. package/dist/m/core/{autosave-BSM2ABJL.js → autosave-HD7Y7UNV.js} +1 -1
  117. package/dist/m/core/{conflict-notice-7EQLDYPA.js → conflict-notice-UNDOD4BL.js} +1 -1
  118. package/dist/m/core/{persist-2SZO3PCE.js → persist-MX3VYVBN.js} +1 -1
  119. package/dist/m/core/{save-K6Q47J2X.js → save-JZ7R4ET2.js} +1 -1
  120. package/dist/m/core/{save-core-U3Z44XBO.js → save-core-I4N6S4ED.js} +1 -1
  121. package/dist/m/core/site-edits-C22O53GS.js +1 -0
  122. package/dist/m/core/{snapshot-PWCHYFL6.js → snapshot-N5MKDGNI.js} +1 -1
  123. package/dist/m/core/{unsaved-warning-2WRZLJH5.js → unsaved-warning-BP6ZY3PG.js} +1 -1
  124. package/dist/m/internals/{index-FSLC4M7J.js → index-3LAVREAU.js} +1 -1
  125. package/dist/m/loader-DCKCZKY6.js +1 -0
  126. package/dist/m/manifest-7954CBE3.js +1 -0
  127. package/dist/m/manifest-edit-93AA79D7.js +1 -0
  128. package/dist/m/manifest.json +1240 -1179
  129. package/dist/m/page/{editor-PKSPJ2XJ.js → editor-UF3CIA6V.js} +5 -5
  130. package/dist/m/plugins/{ai-edit-3LG6MGS3.js → ai-edit-WDV2GLDU.js} +2 -2
  131. package/dist/m/plugins/{cms-T2HLI7JG.js → cms-KG6TCTR3.js} +9 -9
  132. package/dist/m/plugins/{derive-7DR5TUDH.js → derive-R4USHZ7P.js} +1 -1
  133. package/dist/m/plugins/{indicator-CTKX4EY7.js → indicator-OLPZ4WFI.js} +1 -1
  134. package/dist/m/plugins/{live-IFLL6YN5.js → live-DF5YS5AH.js} +1 -1
  135. package/dist/m/plugins/{menu-YJZ3JWBC.js → menu-ESEFQARM.js} +1 -1
  136. package/dist/m/plugins/page-GZSU77JS.js +2 -0
  137. package/dist/m/plugins/{page-view-NMCCU6MF.js → page-view-7MM3FEAR.js} +1 -1
  138. package/dist/m/plugins/{people-6WIWCZEG.js → people-EBWE337I.js} +1 -1
  139. package/dist/m/plugins/quickcrop-UHURKA2F.js +26 -0
  140. package/dist/m/plugins/richclay-MXH2GFAZ.js +549 -0
  141. package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
  142. package/dist/m/plugins/{source-7LSGOV46.js → source-OYHDXKTB.js} +1 -1
  143. package/dist/m/plugins/undo-KLHAGV75.js +1 -0
  144. package/dist/m/plugins/{upgrade-MMPZUCDM.js → upgrade-SZQJYEQQ.js} +1 -1
  145. package/dist/m/plugins/{visitor-drafts-V5YB7XDH.js → visitor-drafts-ZE6PCAFP.js} +2 -2
  146. package/dist/m/plugins/{visitor-menu-QDV2MYJN.js → visitor-menu-5XY57JQA.js} +1 -1
  147. package/dist/m/plugins/{wire-3YGUO3GE.js → wire-S5ATC4QI.js} +1 -1
  148. package/dist/m/reader-core-AHOAP5DH.js +77 -0
  149. package/dist/m/sync/{live-sync-XMXXZS3G.js → live-sync-RZQSDMZW.js} +1 -1
  150. package/dist/m/ui/index-G7ZJXNMG.js +1 -0
  151. package/dist/m/vendor/{sap-core.vendor-JANUQULT.js → sap-core.vendor-5TWOJYVM.js} +4 -4
  152. package/dist/offline-manifest.json +780 -750
  153. package/dist/server/core.cjs +4245 -2614
  154. package/dist/server/markdown.cjs +1 -0
  155. package/dist/server/markdown.mjs +7 -0
  156. package/docs/guide.md +26 -37
  157. package/entries/clay-data.js +1 -1
  158. package/entries/sap.js +5 -5
  159. package/package.json +6 -2
  160. package/packed-contract.json +3 -0
  161. package/src/attrs/save-freeze.js +1 -1
  162. package/src/core/admin-contenteditable.js +1 -1
  163. package/src/core/admin-inputs.js +1 -1
  164. package/src/core/admin-onclick.js +1 -1
  165. package/src/core/admin-resources.js +5 -2
  166. package/src/core/conflict-notice-ui.js +59 -34
  167. package/src/core/save.js +9 -9
  168. package/src/core/site-edits.js +1 -1
  169. package/src/core/snapshot.js +56 -43
  170. package/src/core/stale-host-notice.js +20 -7
  171. package/src/loader-logic.js +0 -13
  172. package/src/page/boot-pack.js +1 -0
  173. package/src/page/editor.js +10 -4
  174. package/src/page/history-intent.js +111 -4
  175. package/src/page/import-markdown.js +22 -8
  176. package/src/page/save-projection.js +1 -1
  177. package/src/page/ybind/bridge.js +14 -7
  178. package/src/page/ybind/moves.js +437 -3
  179. package/src/page/ybind/origins.js +1 -1
  180. package/src/page/ybind/recovery.js +1 -1
  181. package/src/plugins/page.js +48 -4
  182. package/src/plugins/quickcrop.js +128 -69
  183. package/src/plugins/richclay.js +136 -0
  184. package/src/plugins/sortable.js +25 -9
  185. package/src/plugins/undo.js +169 -7
  186. package/src/recovery/undo.js +14 -7
  187. package/src/server/markdown/index.js +23 -0
  188. package/src/sync/conflicts.js +3 -1
  189. package/src/sync/data-guard-notice.js +137 -18
  190. package/src/sync/live-sync.js +24 -0
  191. package/src/ui/bevel-controls.js +4 -1
  192. package/src/ui/dialogs.js +11 -7
  193. package/src/ui/modal.js +9 -7
  194. package/src/ui/toast.js +67 -32
  195. package/src/ui/vendor-skin.js +9 -4
  196. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  197. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  198. package/src/vendor/hyper-undo.vendor.js +1 -1
  199. package/src/vendor/hypercms.vendor.js +7 -7
  200. package/src/vendor/richclay.vendor.js +16 -16
  201. package/src/vendor/sap-core.vendor.js +4 -4
  202. package/tool-generation/context.md +69 -0
  203. package/tool-generation/system.md +1 -1
  204. package/dist/m/c-277QP5ET.js +0 -1
  205. package/dist/m/c-5SZ6KFR5.js +0 -1
  206. package/dist/m/c-CU33V6RW.js +0 -4
  207. package/dist/m/c-CVSWLX3X.js +0 -1
  208. package/dist/m/c-DXPOFLEJ.js +0 -1
  209. package/dist/m/c-EODTTNVA.js +0 -1
  210. package/dist/m/c-ESK4S67I.js +0 -1
  211. package/dist/m/c-FDWR3FL7.js +0 -1
  212. package/dist/m/c-FJDTDGDA.js +0 -1
  213. package/dist/m/c-FYDKYW6E.js +0 -1
  214. package/dist/m/c-HKSLOVU4.js +0 -1
  215. package/dist/m/c-IJHF4IO6.js +0 -1
  216. package/dist/m/c-IUL7PU4P.js +0 -1
  217. package/dist/m/c-JWW6BHWD.js +0 -6
  218. package/dist/m/c-K4M76VPM.js +0 -7
  219. package/dist/m/c-M7ITYKMF.js +0 -1
  220. package/dist/m/c-MR75YPRT.js +0 -1
  221. package/dist/m/c-NFQFV7OP.js +0 -2
  222. package/dist/m/c-NHXOBD6A.js +0 -1
  223. package/dist/m/c-NVU3DORS.js +0 -1
  224. package/dist/m/c-PNZIJFG7.js +0 -1
  225. package/dist/m/c-QWB53KRW.js +0 -1
  226. package/dist/m/c-RHPHMTF2.js +0 -2
  227. package/dist/m/c-RWJ3OSCP.js +0 -1
  228. package/dist/m/c-SEVOS4UE.js +0 -1
  229. package/dist/m/c-SGG6MUXW.js +0 -1
  230. package/dist/m/c-SXNSSHPP.js +0 -1
  231. package/dist/m/c-TFLRT5JX.js +0 -1
  232. package/dist/m/c-TLQINES7.js +0 -30
  233. package/dist/m/c-U65PREMC.js +0 -1
  234. package/dist/m/c-V5L6HWVL.js +0 -1
  235. package/dist/m/c-VMXCX52Q.js +0 -1
  236. package/dist/m/c-YYGUHSZR.js +0 -1
  237. package/dist/m/core/site-edits-CFMF6GHD.js +0 -1
  238. package/dist/m/loader-RC54UV7H.js +0 -1
  239. package/dist/m/manifest-3B84C829.js +0 -1
  240. package/dist/m/manifest-edit-1CB79AD4.js +0 -1
  241. package/dist/m/plugins/page-GTDPYGE4.js +0 -2
  242. package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
  243. package/dist/m/plugins/richclay-L3BZMX4I.js +0 -549
  244. package/dist/m/plugins/sortable-365SRRL3.js +0 -1
  245. package/dist/m/plugins/undo-UES4KS7B.js +0 -1
  246. package/dist/m/reader-core-VR7EL4WV.js +0 -77
  247. package/dist/m/ui/index-MGTWOW2C.js +0 -1
  248. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  249. /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-UHURKA2F.js.LEGAL.txt} +0 -0
  250. /package/dist/m/plugins/{richclay-L3BZMX4I.js.LEGAL.txt → richclay-MXH2GFAZ.js.LEGAL.txt} +0 -0
@@ -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);
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
  }
@@ -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
  }
@@ -104,7 +104,7 @@ function takeCurrent(current, sent, refused) {
104
104
  if (refused.has(key) || !rule || !Object.hasOwn(rule, key) || live[key] !== sent[key]) continue;
105
105
  // Outside undo history and the edit observers, the way a sync frame lands: an undo
106
106
  // must not bring back the value this tab was behind on, and then declare it.
107
- const fields = new Map([...root.querySelectorAll("[editable]")].map((field) => [field, field.innerHTML]));
107
+ const fields = new Map([...root.querySelectorAll("[data-richclay], [richclay], [editable], [clay-editable]")].map((field) => [field, field.innerHTML]));
108
108
  Mutation.pause();
109
109
  try {
110
110
  engine.applyData(root, { site: { [key]: value } }, { site: { [key]: rule[key] } });
@@ -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 };
@@ -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
 
@@ -195,10 +195,6 @@ const IMPLIES = { cms: ["quickcrop", "upload", "menu"], "ai-edit": ["wire"], blo
195
195
  // A Document's editor owns its article's rich text, so richclay (for [editable]) has
196
196
  // nothing to do there; plugins=blockeditor,richclay still loads it.
197
197
  const DISPLACES = { blockeditor: ["richclay"] };
198
- // A Document's editor keeps its own history, the same one Cmd+Z in the editor uses, and
199
- // hyper-undo takes Cmd+Z for the whole window: loaded together the two would fight over
200
- // every press. This is the one pair where a name the page asked for by hand still loses.
201
- const CANNOT_RUN_WITH = { blockeditor: ["undo"] };
202
198
  // No first paint or keystroke waits for these. source keeps the file's bytes outside
203
199
  // the edited parts stable; a save before it installs is the ordinary full serialization.
204
200
  // upgrade only asks the host for a newer version once `clay.ready` resolves.
@@ -296,15 +292,6 @@ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap
296
292
  if (enabled.has(owner)) for (const name of names) if (!explicit.has(name)) enabled.delete(name);
297
293
  }
298
294
 
299
- for (const [owner, names] of Object.entries(CANNOT_RUN_WITH)) {
300
- if (!enabled.has(owner)) continue;
301
- for (const name of names) {
302
- if (!enabled.has(name)) continue;
303
- enabled.delete(name);
304
- console.warn(`clayjs: plugin "${name}" cannot run with "${owner}" and was left out`);
305
- }
306
- }
307
-
308
295
  const plugins = [];
309
296
  const late = [];
310
297
  for (const name of PLUGIN_ORDER) {
@@ -20,6 +20,7 @@ export { createYPageBinding } from "./ybind/binding.js";
20
20
  export { yValueOf } from "./ybind/provider.js";
21
21
  export { seedPageFromArticle } from "./ybind/migrate.js";
22
22
  export { holdSlashRange, heldSlashRange } from "./chrome.js";
23
+ export { createHistoryBridge } from "./history-intent.js";
23
24
  export { createRecoverList, focusBlock } from "./recover.js";
24
25
  export { observeDocumentTitle } from "./document-title-runtime.js";
25
26
  export { createFeatureLoader } from "./feature-loader.js";