@panphora/clayjs 1.24.1 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +6060 -4662
  11. package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
  12. package/dist/m/attrs/save-freeze-PUYX32AK.js +1 -0
  13. package/dist/m/{c-HIX5H5L7.js → c-2A2ATCIH.js} +1 -1
  14. package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
  15. package/dist/m/{c-MP6S4AZU.js → c-2GB34XL4.js} +2 -2
  16. package/dist/m/c-2TJCFPX4.js +4 -0
  17. package/dist/m/c-33SS5A4Y.js +1 -0
  18. package/dist/m/{c-PCNJETUA.js → c-34J5HC7N.js} +1 -1
  19. package/dist/m/{c-AT7GLQGV.js → c-3AAQJAKJ.js} +1 -1
  20. package/dist/m/c-3F3AADKG.js +1 -0
  21. package/dist/m/{c-HL6BLJNA.js → c-3WX5FABT.js} +1 -1
  22. package/dist/m/c-3XNTQQEJ.js +1 -0
  23. package/dist/m/{c-ZRGEN6QQ.js → c-3Y234AUB.js} +2 -2
  24. package/dist/m/{c-SUXHUL7U.js → c-42RLKGEY.js} +1 -1
  25. package/dist/m/{c-2BLVA6IN.js → c-44WITKWQ.js} +1 -1
  26. package/dist/m/c-47M4QHCK.js +1 -0
  27. package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
  28. package/dist/m/{c-SXGEWPFC.js → c-4NAYDOWH.js} +1 -1
  29. package/dist/m/{c-OCYG4NBQ.js → c-4OZZ7ENT.js} +2 -2
  30. package/dist/m/{c-UOOIF2AL.js → c-5IIEOPIV.js} +1 -1
  31. package/dist/m/{c-YDW4VCHP.js → c-755ZOHOM.js} +1 -1
  32. package/dist/m/c-7Q4XYDF4.js +1 -0
  33. package/dist/m/c-7RHOMAMR.js +1 -0
  34. package/dist/m/{c-7V6MGMX2.js → c-7Z2625LX.js} +1 -1
  35. package/dist/m/{c-CTGRDT6E.js → c-A3OKSYRI.js} +1 -1
  36. package/dist/m/{c-GNI7WHKF.js → c-A5IYL7JI.js} +1 -1
  37. package/dist/m/c-A5LASUEZ.js +1 -0
  38. package/dist/m/c-AQVQKFKB.js +1 -0
  39. package/dist/m/{c-FLEIU6CG.js → c-ASGHHLFX.js} +1 -1
  40. package/dist/m/c-ASMLERFA.js +7 -0
  41. package/dist/m/{c-LUXAB2XI.js → c-AZM34SPO.js} +1 -1
  42. package/dist/m/{c-ZFO5RW6G.js → c-BHLYQGTA.js} +1 -1
  43. package/dist/m/{c-C5AKJVRH.js → c-BJ2RRQBQ.js} +1 -1
  44. package/dist/m/{c-FVPNX2GO.js → c-C27EVUQ6.js} +1 -1
  45. package/dist/m/{c-DFKBGSN2.js → c-C5UGNOC2.js} +1 -1
  46. package/dist/m/c-C6DXZIXD.js +1 -0
  47. package/dist/m/{c-EDE45NM7.js → c-C7SUMNMI.js} +1 -1
  48. package/dist/m/{c-STRI73GQ.js → c-CL5LSATX.js} +1 -1
  49. package/dist/m/c-CPRGR2II.js +1 -0
  50. package/dist/m/{c-NFQFV7OP.js → c-D2OPWOWO.js} +1 -1
  51. package/dist/m/{c-ZKKAF4UT.js → c-D4N4UJCQ.js} +2 -2
  52. package/dist/m/{c-6LEYVQJK.js → c-DAUQ6JZ3.js} +1 -1
  53. package/dist/m/{c-ZGCO4J3H.js → c-DDCYVLGG.js} +1 -1
  54. package/dist/m/{c-KQKKIVLW.js → c-DVMDYRAE.js} +1 -1
  55. package/dist/m/{c-OG6GFSUX.js → c-DYTBSJBI.js} +1 -1
  56. package/dist/m/{c-P2HKMJAW.js → c-EAV7A6BT.js} +1 -1
  57. package/dist/m/{c-X75G43SZ.js → c-EEWPNUM2.js} +1 -1
  58. package/dist/m/c-EHAR4RJJ.js +3 -0
  59. package/dist/m/c-EIDZHQHZ.js +1 -0
  60. package/dist/m/c-ELXMSEN5.js +1 -0
  61. package/dist/m/{c-QQFHEODI.js → c-EQ5NBOGA.js} +1 -1
  62. package/dist/m/c-FC5RUFPR.js +1 -0
  63. package/dist/m/{c-ND3Q2G2I.js → c-GFASNR55.js} +1 -1
  64. package/dist/m/{c-A4L7APIU.js → c-GNFDX6SU.js} +1 -1
  65. package/dist/m/{c-LATNQ6Q5.js → c-H3M6UDNV.js} +1 -1
  66. package/dist/m/{c-YLHTD3KX.js → c-H4M2T4A6.js} +1 -1
  67. package/dist/m/{c-DDS23EY6.js → c-HXGNH6GY.js} +1 -1
  68. package/dist/m/{c-PDBNCJCT.js → c-HYNM5U6B.js} +1 -1
  69. package/dist/m/c-J2YYLEVM.js +1 -0
  70. package/dist/m/c-J2ZW4HMR.js +1 -0
  71. package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
  72. package/dist/m/c-K2ZHQMPF.js +1 -0
  73. package/dist/m/{c-6IJEVE6B.js → c-KU3PX7LL.js} +1 -1
  74. package/dist/m/{c-YYMZWQLR.js → c-L6KVXPBO.js} +1 -1
  75. package/dist/m/{c-CV4T65JE.js → c-LUBZU4AZ.js} +4 -4
  76. package/dist/m/{c-5FXEKU4Y.js → c-LYNLY3MR.js} +1 -1
  77. package/dist/m/{c-NQAPWM7O.js → c-MHTO3QAD.js} +1 -1
  78. package/dist/m/{c-HDVH3MVC.js → c-MKAUC6DB.js} +1 -1
  79. package/dist/m/c-MLMJUNRV.js +1 -0
  80. package/dist/m/c-MMEVDN6D.js +1 -0
  81. package/dist/m/c-MOP5KBWZ.js +1 -0
  82. package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
  83. package/dist/m/{c-XW5TIYYB.js → c-N4GEJMXW.js} +1 -1
  84. package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
  85. package/dist/m/{c-2PVGJFI5.js → c-NW4ZFKCY.js} +1 -1
  86. package/dist/m/{c-PYG2NGWU.js → c-O3DPFZCE.js} +1 -1
  87. package/dist/m/c-O4YIDULY.js +1 -0
  88. package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
  89. package/dist/m/c-OZOACVUD.js +1 -0
  90. package/dist/m/{c-YK2L436K.js → c-OZVK3TFI.js} +1 -1
  91. package/dist/m/c-P73XPAOA.js +1 -0
  92. package/dist/m/c-PP3CUF3U.js +1 -0
  93. package/dist/m/{c-B7GOFGQR.js → c-Q2CB57UG.js} +1 -1
  94. package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
  95. package/dist/m/{c-LSVN3OLG.js → c-QL4NYM2L.js} +1 -1
  96. package/dist/m/c-RES2S5Q3.js +1 -0
  97. package/dist/m/c-RFGELYEU.js +2 -0
  98. package/dist/m/c-RGFPLQGV.js +1 -0
  99. package/dist/m/c-SMLAJASY.js +1 -0
  100. package/dist/m/{c-MQKSKQHG.js → c-STAPQ2X6.js} +1 -1
  101. package/dist/m/c-SYVF6ZMF.js +1 -0
  102. package/dist/m/{c-SE3SIY7Y.js → c-T5IWJOMW.js} +1 -1
  103. package/dist/m/{c-DD26VXAP.js → c-T7LOTEWO.js} +1 -1
  104. package/dist/m/{c-47IBJGPQ.js → c-TWB5ZQU4.js} +1 -1
  105. package/dist/m/c-U35T5NAY.js +1 -0
  106. package/dist/m/c-U4TWEIKZ.js +1 -0
  107. package/dist/m/c-UW6MMZ4V.js +1 -0
  108. package/dist/m/{c-JWDCQL6J.js → c-V5L6HWVL.js} +1 -1
  109. package/dist/m/c-VGG3UR7I.js +1 -0
  110. package/dist/m/{c-CZHTXGJS.js → c-VO6RSFCO.js} +1 -1
  111. package/dist/m/c-VU4WEWPH.js +4 -0
  112. package/dist/m/c-W7LWCJHD.js +1 -0
  113. package/dist/m/c-WOJ2RTV4.js +1 -0
  114. package/dist/m/{c-T3P3AXEW.js → c-WOWGGNCJ.js} +1 -1
  115. package/dist/m/{c-6RGEQNVV.js → c-XCOQNISP.js} +1 -1
  116. package/dist/m/{c-SGGGCIVD.js → c-XS7G6U3Z.js} +1 -1
  117. package/dist/m/{c-4PO3YGDC.js → c-Y3J5LDKB.js} +1 -1
  118. package/dist/m/c-Y3MPVURA.js +1 -0
  119. package/dist/m/c-Y6HH7LTM.js +1 -0
  120. package/dist/m/{c-RO73MKKR.js → c-YRDKVEGR.js} +1 -1
  121. package/dist/m/{c-FEC6LGFF.js → c-YZGWSCMT.js} +1 -1
  122. package/dist/m/{c-7CM4GMTS.js → c-ZJ7726FO.js} +1 -1
  123. package/dist/m/{c-SKMIR5HQ.js → c-ZPI7M5U5.js} +1 -1
  124. package/dist/m/core/{admin-attrs-ONRWVXPK.js → admin-attrs-43JBA6JB.js} +1 -1
  125. package/dist/m/core/{autosave-TBKAAAFB.js → autosave-HD7Y7UNV.js} +1 -1
  126. package/dist/m/core/{conflict-notice-FOEE6I7E.js → conflict-notice-Y2NTMDGJ.js} +1 -1
  127. package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
  128. package/dist/m/core/{host-permission-X4BENZEU.js → host-permission-5BYP24GC.js} +1 -1
  129. package/dist/m/core/{is-edit-mode-FFHCQ5DR.js → is-edit-mode-73OXKK5H.js} +1 -1
  130. package/dist/m/core/{persist-6ILUR34S.js → persist-MX3VYVBN.js} +1 -1
  131. package/dist/m/core/{recovery-APWADU4K.js → recovery-VZF7RJZF.js} +1 -1
  132. package/dist/m/core/{save-6HT2LABE.js → save-JZ7R4ET2.js} +1 -1
  133. package/dist/m/core/{save-core-EVMNZQ4V.js → save-core-I4N6S4ED.js} +1 -1
  134. package/dist/m/core/{site-edits-G4BOZMV4.js → site-edits-TIJDPG6H.js} +1 -1
  135. package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-N5MKDGNI.js} +1 -1
  136. package/dist/m/core/unsaved-warning-BP6ZY3PG.js +1 -0
  137. package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
  138. package/dist/m/internals/index-3LAVREAU.js +1 -0
  139. package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
  140. package/dist/m/{loader-GLLVDUZA.js → loader-7ORQVRCK.js} +1 -1
  141. package/dist/m/manifest-A27A4278.js +1 -0
  142. package/dist/m/manifest-edit-E8A61EB4.js +1 -0
  143. package/dist/m/manifest.json +1608 -1498
  144. package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
  145. package/dist/m/page/{editor-KAZDJNND.js → editor-HS2CAV3W.js} +5 -5
  146. package/dist/m/plugins/{ai-edit-LABECCSN.js → ai-edit-WDV2GLDU.js} +2 -2
  147. package/dist/m/plugins/{cms-T2HLI7JG.js → cms-4KET6RZT.js} +9 -9
  148. package/dist/m/plugins/{demo-T7B4RIKL.js → demo-LMCMBILC.js} +1 -1
  149. package/dist/m/plugins/{derive-6LPJOCAV.js → derive-R4USHZ7P.js} +1 -1
  150. package/dist/m/plugins/{indicator-J4ABFARX.js → indicator-OLPZ4WFI.js} +1 -1
  151. package/dist/m/plugins/{live-PGBIEMGB.js → live-CO4VJS46.js} +1 -1
  152. package/dist/m/plugins/{menu-YZL3X2AJ.js → menu-ESEFQARM.js} +1 -1
  153. package/dist/m/plugins/{page-WOCNX2IN.js → page-L3VKIQU2.js} +2 -2
  154. package/dist/m/plugins/{page-view-SJODOOGE.js → page-view-6KNTXABH.js} +1 -1
  155. package/dist/m/plugins/{people-C4TCWFOQ.js → people-EBWE337I.js} +1 -1
  156. package/dist/m/plugins/quickcrop-VNRXGRPC.js +26 -0
  157. package/dist/m/plugins/{richclay-Y2NV44NA.js → richclay-V6WKJTUF.js} +17 -17
  158. package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
  159. package/dist/m/plugins/{source-YK7MO23W.js → source-OYHDXKTB.js} +1 -1
  160. package/dist/m/plugins/undo-WUK2NLBA.js +1 -0
  161. package/dist/m/plugins/{upgrade-NNTJBSK2.js → upgrade-SZQJYEQQ.js} +1 -1
  162. package/dist/m/plugins/{upload-ZKKJV76X.js → upload-NZET3DNQ.js} +1 -1
  163. package/dist/m/plugins/visitor-drafts-ZE6PCAFP.js +20 -0
  164. package/dist/m/plugins/{visitor-drafts-owner-LHUHFU3L.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
  165. package/dist/m/plugins/{visitor-menu-FY4KM2SW.js → visitor-menu-5XY57JQA.js} +1 -1
  166. package/dist/m/plugins/wire-S5ATC4QI.js +1 -0
  167. package/dist/m/reader-core-AHOAP5DH.js +77 -0
  168. package/dist/m/sync/live-sync-DX4K5GDB.js +1 -0
  169. package/dist/m/ui/index-G7ZJXNMG.js +1 -0
  170. package/dist/m/vendor/sap-core.vendor-5TWOJYVM.js +7 -0
  171. package/dist/offline-manifest.json +844 -784
  172. package/dist/server/core.cjs +4245 -2614
  173. package/dist/server/markdown.cjs +1 -0
  174. package/dist/server/markdown.mjs +7 -0
  175. package/docs/guide.md +27 -37
  176. package/entries/clay-data.js +1 -1
  177. package/entries/sap.js +6 -5
  178. package/package.json +6 -2
  179. package/packed-contract.json +3 -0
  180. package/src/attrs/save-freeze.js +1 -1
  181. package/src/core/admin-contenteditable.js +1 -1
  182. package/src/core/admin-inputs.js +1 -1
  183. package/src/core/admin-onclick.js +1 -1
  184. package/src/core/admin-resources.js +5 -2
  185. package/src/core/conflict-notice-ui.js +59 -34
  186. package/src/core/draft-mode.js +5 -0
  187. package/src/core/save-core.js +10 -10
  188. package/src/core/save.js +10 -10
  189. package/src/core/snapshot.js +56 -43
  190. package/src/core/stale-host-notice.js +20 -7
  191. package/src/lib/dirty-gate.js +19 -0
  192. package/src/page/editor.js +10 -4
  193. package/src/page/import-markdown.js +22 -8
  194. package/src/page/save-projection.js +1 -1
  195. package/src/plugins/menu/visitor.js +33 -1
  196. package/src/plugins/quickcrop.js +128 -69
  197. package/src/plugins/sortable.js +25 -9
  198. package/src/plugins/visitor-drafts/commit.js +50 -3
  199. package/src/plugins/visitor-drafts/idb.js +8 -2
  200. package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
  201. package/src/plugins/visitor-drafts/recovery.js +64 -0
  202. package/src/plugins/visitor-drafts/session.js +61 -13
  203. package/src/plugins/visitor-drafts/store.js +44 -8
  204. package/src/plugins/visitor-drafts.js +34 -5
  205. package/src/server/markdown/index.js +23 -0
  206. package/src/sync/conflicts.js +3 -1
  207. package/src/sync/data-guard-notice.js +137 -18
  208. package/src/sync/live-sync.js +1 -1
  209. package/src/sync/wire-profile.js +9 -4
  210. package/src/ui/bevel-controls.js +4 -1
  211. package/src/ui/dialogs.js +11 -7
  212. package/src/ui/modal.js +9 -7
  213. package/src/ui/toast.js +67 -32
  214. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  215. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  216. package/src/vendor/hyper-undo.vendor.js +1 -1
  217. package/src/vendor/hypercms.vendor.js +7 -7
  218. package/src/vendor/richclay.vendor.js +16 -16
  219. package/src/vendor/sap-core.vendor.js +6 -5
  220. package/src/vendor/sap-ownership.vendor.js +181 -11
  221. package/tool-generation/context.md +69 -0
  222. package/tool-generation/system.md +2 -2
  223. package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
  224. package/dist/m/c-277QP5ET.js +0 -1
  225. package/dist/m/c-3UW5EJ77.js +0 -1
  226. package/dist/m/c-42ZNV7LI.js +0 -7
  227. package/dist/m/c-6Q6RGFBF.js +0 -1
  228. package/dist/m/c-APGHGDZM.js +0 -1
  229. package/dist/m/c-BGN7RCTJ.js +0 -1
  230. package/dist/m/c-CJ7QYEWI.js +0 -1
  231. package/dist/m/c-CVSWLX3X.js +0 -1
  232. package/dist/m/c-DXPOFLEJ.js +0 -1
  233. package/dist/m/c-EILLN2H2.js +0 -1
  234. package/dist/m/c-EODTTNVA.js +0 -1
  235. package/dist/m/c-ESK4S67I.js +0 -1
  236. package/dist/m/c-FDWR3FL7.js +0 -1
  237. package/dist/m/c-FYDKYW6E.js +0 -1
  238. package/dist/m/c-HKSLOVU4.js +0 -1
  239. package/dist/m/c-HTLYXCUH.js +0 -1
  240. package/dist/m/c-IUL7PU4P.js +0 -1
  241. package/dist/m/c-JQF2EUCG.js +0 -1
  242. package/dist/m/c-MFOSMAVM.js +0 -3
  243. package/dist/m/c-MQYEDF3Q.js +0 -1
  244. package/dist/m/c-MR75YPRT.js +0 -1
  245. package/dist/m/c-NHXOBD6A.js +0 -1
  246. package/dist/m/c-NVU3DORS.js +0 -1
  247. package/dist/m/c-OYHI67B6.js +0 -1
  248. package/dist/m/c-PWJSJEJU.js +0 -1
  249. package/dist/m/c-QENKN7QI.js +0 -1
  250. package/dist/m/c-R23UHMXG.js +0 -1
  251. package/dist/m/c-SGH3UTF3.js +0 -1
  252. package/dist/m/c-SXNSSHPP.js +0 -1
  253. package/dist/m/c-TFLRT5JX.js +0 -1
  254. package/dist/m/c-U4NTUVDZ.js +0 -1
  255. package/dist/m/c-V2U3F4QX.js +0 -1
  256. package/dist/m/c-VG5XIN4T.js +0 -1
  257. package/dist/m/c-VHSMPITY.js +0 -1
  258. package/dist/m/c-Z4G2UK6T.js +0 -4
  259. package/dist/m/core/draft-mode-4MCL3LYI.js +0 -1
  260. package/dist/m/core/unsaved-warning-ITZDIZ2I.js +0 -1
  261. package/dist/m/internals/index-HS3CQDFW.js +0 -1
  262. package/dist/m/manifest-50FF1C45.js +0 -1
  263. package/dist/m/manifest-edit-BE1A621D.js +0 -1
  264. package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
  265. package/dist/m/plugins/sortable-XCHQG65Z.js +0 -1
  266. package/dist/m/plugins/undo-VLRBQNPN.js +0 -1
  267. package/dist/m/plugins/visitor-drafts-RTZNONHT.js +0 -20
  268. package/dist/m/plugins/wire-M2NR354P.js +0 -1
  269. package/dist/m/reader-core-T2S3JC4E.js +0 -77
  270. package/dist/m/sync/live-sync-Z64YREWQ.js +0 -1
  271. package/dist/m/ui/index-MGTWOW2C.js +0 -1
  272. package/dist/m/vendor/sap-core.vendor-QAEPMETX.js +0 -6
  273. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  274. /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-VNRXGRPC.js.LEGAL.txt} +0 -0
  275. /package/dist/m/plugins/{richclay-Y2NV44NA.js.LEGAL.txt → richclay-V6WKJTUF.js.LEGAL.txt} +0 -0
@@ -1,12 +1,24 @@
1
1
  import { saveToken } from "../core/host-attrs.js";
2
2
  import { storage } from "../lib/doc-storage.js";
3
+ import { isSaveInProgress } from "../core/save-core.js";
3
4
 
4
- const TITLE = "Saved data was overwritten";
5
+ const TITLE = "Some saved data was removed";
6
+ const TITLES = {
7
+ recent: "A change from outside this page removed something you typed",
8
+ "all-typed": "A change from outside this page removed everything you typed",
9
+ emptied: "A change from outside this page emptied this page's data",
10
+ unreadable: "A change from outside this page left its data unreadable",
11
+ };
12
+ const TYPED_SHOWN = 3;
13
+ const HOUR_MS = 3600000;
5
14
  const DONE_KEY = "data-guard-done";
6
15
  const READ_TIMEOUT_MS = 6000;
7
16
  const AFTER_SAVE_MS = 1500;
8
17
  const MIN_READ_GAP_MS = 5000;
9
18
  const VISIBLE_GAP_MS = 30000;
19
+ const EXTERNAL_WINDOW_MS = 10000;
20
+ const SETTLE_CAP_MS = 6000;
21
+ const SAVE_UNSETTLED_EVENTS = ["clay:save-error", "clay:save-offline", "clay:save-conflict"];
10
22
  const UNREACHED = "Could not reach the server. Try again.";
11
23
  const REFUSED = "That did not go through. Try again.";
12
24
  const SAVE_FIRST = "Save your changes first, then try again.";
@@ -20,7 +32,15 @@ const isPending = (event) => Boolean(event) && typeof event.id === "string" && e
20
32
  const allowed = (current, event, choice, flag) => flag === true && !current.refused.has(`${event.id}:${choice}`);
21
33
  const canRestore = (current, event) => allowed(current, event, "restore", event.restorable);
22
34
  const canRevert = (current, event) => allowed(current, event, "revert", event.canRevert);
35
+ const canPutBack = (current, event) => allowed(current, event, "put-back", event.canPutBack);
36
+ const typed = (event) => (Array.isArray(event.typed) ? event.typed : []);
37
+ const reasoned = (event) => Boolean(event.reason);
38
+ const titleFor = (event) => TITLES[event.reason] ?? TITLE;
39
+ const typedLoss = (event) => event.reason === "recent" || event.reason === "all-typed";
40
+ const toneFor = (event) => (typedLoss(event) ? "info" : "warning");
23
41
  const credentials = () => (saveToken() ? "omit" : "same-origin");
42
+ const settling = (current) =>
43
+ isSaveInProgress() || (current.lastExternal > 0 && Date.now() - current.lastExternal < EXTERNAL_WINDOW_MS);
24
44
  const headers = (extra = {}) => ({ "Document-URL": location.href, ...extra });
25
45
 
26
46
  function removeServerBar() {
@@ -34,7 +54,7 @@ function say(message, type) {
34
54
  }, () => {});
35
55
  }
36
56
 
37
- function describe(current, event) {
57
+ function sourceOf(event) {
38
58
  const summary = event.lossSummary || {};
39
59
  const source = summary.provenance === "external" ? "A change from outside this page" : "A save from this page";
40
60
  const loss = summary.removedItems > 0
@@ -43,12 +63,54 @@ function describe(current, event) {
43
63
  const row = Array.isArray(event.preview) ? event.preview[0] : null;
44
64
  const parts = [`${source} ${loss}.`];
45
65
  if (row) parts.push(`${row.key}: ${row.yours} before, ${row.now} now.`);
46
- if (canRestore(current, event)) {
47
- if (event.droppedAdditions > 0) parts.push(`Restoring removes ${plural(event.droppedAdditions, "item")} added or changed since.`);
48
- } else if (canRevert(current, event)) {
49
- parts.push("This data no longer fits the page, but the whole change can be undone.");
66
+ return parts;
67
+ }
68
+
69
+ function labelFor(path) {
70
+ const parts = String(path ?? "").split(".").filter((part) => part && !/^\[[^\]]*\]$/.test(part));
71
+ const last = (parts[parts.length - 1] ?? "").replace(/\[[^\]]*\]/g, "").trim();
72
+ return last.replace(/[_-]+/g, " ") || "value";
73
+ }
74
+
75
+ function typedLine(event) {
76
+ const rows = typed(event);
77
+ if (!rows.length) return "";
78
+ const shown = rows.slice(0, TYPED_SHOWN).map((row) => `${labelFor(row.path)}: "${row.yours}"`);
79
+ const more = rows.length - shown.length;
80
+ return `${shown.join("; ")}${more > 0 ? ` and ${more} more.` : "."}`;
81
+ }
82
+
83
+ function typedWhen(event) {
84
+ if (event.reason === "all-typed") return "";
85
+ const row = typed(event)[0];
86
+ const at = typeof row?.at === "number" ? row.at : Date.parse(row?.at);
87
+ if (!Number.isFinite(at)) return "";
88
+ const ago = Date.now() - at;
89
+ if (ago < 0 || ago >= HOUR_MS) return "";
90
+ const minutes = Math.max(1, Math.floor(ago / 60000));
91
+ return `You typed ${typed(event).length === 1 ? "it" : "them"} ${plural(minutes, "minute")} ago.`;
92
+ }
93
+
94
+ function detailFor(current, event) {
95
+ if (!reasoned(event)) {
96
+ const parts = sourceOf(event);
97
+ if (canRestore(current, event)) {
98
+ if (event.droppedAdditions > 0) parts.push(`Restoring removes ${plural(event.droppedAdditions, "item")} added or changed since.`);
99
+ } else if (canRevert(current, event)) {
100
+ parts.push("This data no longer fits the page, but the whole change can be undone.");
101
+ } else {
102
+ parts.push("It cannot be restored from here.");
103
+ }
104
+ if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
105
+ return parts.join(" ");
106
+ }
107
+ const parts = [];
108
+ if (typedLoss(event)) {
109
+ parts.push(typedLine(event) || sourceOf(event).join(" "));
110
+ const when = typedWhen(event);
111
+ if (when) parts.push(when);
50
112
  } else {
51
- parts.push("It cannot be restored from here.");
113
+ parts.push(sourceOf(event).join(" "));
52
114
  }
53
115
  if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
54
116
  return parts.join(" ");
@@ -56,14 +118,26 @@ function describe(current, event) {
56
118
 
57
119
  function actionsFor(current, event) {
58
120
  const actions = [];
59
- if (canRestore(current, event)) actions.push({ label: "Restore", onClick: () => choose(current, event, "restore") });
121
+ if (!reasoned(event)) {
122
+ if (canRestore(current, event)) actions.push({ label: "Restore", onClick: () => choose(current, event, "restore") });
123
+ if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
124
+ actions.push({ label: "Keep as is", onClick: () => choose(current, event, "dismiss") });
125
+ return actions;
126
+ }
127
+ if (canPutBack(current, event)) actions.push({ label: "Put it back", onClick: () => choose(current, event, "put-back") });
60
128
  if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
61
- actions.push({ label: "Keep as is", onClick: () => choose(current, event, "dismiss") });
129
+ actions.push({ label: "Keep the change", onClick: () => choose(current, event, "dismiss") });
62
130
  return actions;
63
131
  }
64
132
 
65
133
  async function draw(current, event) {
66
- const key = [event.id, canRestore(current, event), canRevert(current, event), event.lastWriteAt, describe(current, event)].join("|");
134
+ const title = titleFor(event);
135
+ const detail = detailFor(current, event);
136
+ const key = [
137
+ event.id, title, event.reason ?? "", JSON.stringify(typed(event)),
138
+ canRestore(current, event), canPutBack(current, event), canRevert(current, event),
139
+ event.lastWriteAt, detail,
140
+ ].join("|");
67
141
  if (current.drawn === key) return true;
68
142
  current.drawn = key;
69
143
  current.eventId = event.id;
@@ -77,15 +151,20 @@ async function draw(current, event) {
77
151
  return false;
78
152
  }
79
153
  if (session !== current || current.drawn !== key) return false;
80
- ui.dismissPersistent(TITLE, { now: true });
81
- ui.toastPersistent(TITLE, "warning", { detail: describe(current, event), actions: actionsFor(current, event) });
154
+ const previous = current.title;
155
+ current.title = title;
156
+ if (previous && previous !== title) ui.dismissPersistent(previous, { now: true });
157
+ ui.dismissPersistent(title, { now: true });
158
+ ui.toastPersistent(title, toneFor(event), { detail, actions: actionsFor(current, event) });
82
159
  return true;
83
160
  }
84
161
 
85
162
  function clearNotice(current) {
163
+ const title = current.title;
86
164
  current.drawn = null;
87
165
  current.eventId = null;
88
- ui?.dismissPersistent(TITLE, { now: true });
166
+ current.title = null;
167
+ if (title) ui?.dismissPersistent(title, { now: true });
89
168
  }
90
169
 
91
170
  async function read(current) {
@@ -113,7 +192,11 @@ async function read(current) {
113
192
  if (session !== current || turn !== current.turn) return;
114
193
  if (!body || typeof body !== "object" || !("event" in body)) return;
115
194
  if (isPending(body.event)) {
116
- if (current.resolved.has(body.event.id)) return;
195
+ if (current.resolved.has(body.event.id) || current.waiting === body.event.id) return;
196
+ if (!current.waited.has(body.event.id) && settling(current)) {
197
+ waitForSettle(current, body.event.id);
198
+ return;
199
+ }
117
200
  if (await draw(current, body.event)) removeServerBar();
118
201
  return;
119
202
  }
@@ -150,7 +233,7 @@ function settle(current, event, choice, status, body) {
150
233
  } else if (status === 422) {
151
234
  current.refused.add(`${event.id}:${choice}`);
152
235
  if (current.eventId === event.id && !current.resolved.has(event.id)) draw(current, event);
153
- say(choice === "restore" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
236
+ say(choice === "restore" || choice === "put-back" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
154
237
  } else if (status === 401 || status === 403) {
155
238
  say(RELOAD_FIRST, "error");
156
239
  } else {
@@ -202,17 +285,44 @@ function onNotification(current, e) {
202
285
  if (typeof data?.id !== "string") return;
203
286
  current.turn++;
204
287
  current.resolved.add(data.id);
288
+ if (current.waiting === data.id) stopWaiting(current);
205
289
  if (current.eventId === data.id) clearNotice(current);
206
290
  return;
207
291
  }
208
292
  if (action === "raised" && isPending(data) && !current.resolved.has(data.id)) {
209
293
  current.turn++;
294
+ if (current.waiting === data.id) return;
295
+ if (!current.waited.has(data.id) && settling(current)) {
296
+ waitForSettle(current, data.id);
297
+ return;
298
+ }
210
299
  draw(current, data).then((drawn) => { if (drawn) removeServerBar(); }, () => {});
211
300
  }
212
301
  }
213
302
 
214
303
  const readAt = (current) => Math.max(current.lastSave + AFTER_SAVE_MS, current.lastRead + MIN_READ_GAP_MS);
215
304
 
305
+ function stopWaiting(current) {
306
+ if (!current.waiting) return false;
307
+ current.waiting = null;
308
+ clearTimeout(current.waitTimer);
309
+ current.waitTimer = null;
310
+ return true;
311
+ }
312
+
313
+ function endWait(current, { now = false } = {}) {
314
+ if (!stopWaiting(current) || session !== current) return;
315
+ if (now) read(current);
316
+ else armRead(current);
317
+ }
318
+
319
+ function waitForSettle(current, id) {
320
+ current.waited.add(id);
321
+ current.waiting = id;
322
+ clearTimeout(current.waitTimer);
323
+ current.waitTimer = setTimeout(() => endWait(current, { now: true }), SETTLE_CAP_MS);
324
+ }
325
+
216
326
  function armRead(current) {
217
327
  if (current.timer) return;
218
328
  current.timer = setTimeout(() => {
@@ -235,22 +345,30 @@ export function start({ href, reload = () => window.location.reload() }) {
235
345
  const listeners = new AbortController();
236
346
  const current = {
237
347
  url: url.href, reload, listeners, turn: 0, lastRead: 0, lastSave: 0, timer: null,
238
- busy: false, drawn: null, eventId: null, resolved: new Set(), refused: new Set(),
348
+ lastExternal: 0, waiting: null, waitTimer: null, waited: new Set(),
349
+ busy: false, drawn: null, eventId: null, title: null, resolved: new Set(), refused: new Set(),
239
350
  };
240
351
  session = current;
241
352
  const { signal } = listeners;
242
353
  document.addEventListener("clay:notification", (e) => onNotification(current, e), { signal });
354
+ document.addEventListener("clay:sync-applied", (e) => {
355
+ if (e.detail?.source === "disk") current.lastExternal = Date.now();
356
+ }, { signal });
243
357
  document.addEventListener("clay:save-saved", () => {
244
358
  current.lastSave = Date.now();
245
- armRead(current);
359
+ if (current.waiting) endWait(current);
360
+ else armRead(current);
246
361
  }, { signal });
362
+ for (const name of SAVE_UNSETTLED_EVENTS) {
363
+ document.addEventListener(name, () => endWait(current, { now: true }), { signal });
364
+ }
247
365
  document.addEventListener("visibilitychange", () => {
248
366
  if (document.visibilityState === "visible" && Date.now() - current.lastRead >= VISIBLE_GAP_MS) read(current);
249
367
  }, { signal });
250
368
  const done = storage.session.get(DONE_KEY);
251
369
  if (done) {
252
370
  storage.session.remove(DONE_KEY);
253
- say(done === "restore" ? "Your saved data is back." : "The earlier version is back.", "success");
371
+ say(done === "restore" || done === "put-back" ? "Your saved data is back." : "The earlier version is back.", "success");
254
372
  }
255
373
  read(current);
256
374
  }
@@ -261,5 +379,6 @@ export function stop() {
261
379
  session = null;
262
380
  current.listeners.abort();
263
381
  clearTimeout(current.timer);
382
+ clearTimeout(current.waitTimer);
264
383
  clearNotice(current);
265
384
  }
@@ -933,7 +933,7 @@ class LiveSync {
933
933
  // Whichever wire discovery selected. start() does not call connect() until the
934
934
  // profile is known, so this is never null on the normal path.
935
935
  const profile = this._profile || WIRE_PROFILES.legacy;
936
- const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId);
936
+ const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId, PAGE_FORMAT);
937
937
  // A frame scheduled while the page showed waits for an animation frame, which a
938
938
  // page hidden since then never runs. Move it to the hidden schedule.
939
939
  if (!this._hiddenHandler) {
@@ -25,14 +25,19 @@
25
25
 
26
26
  import { hostMeta } from '../core/host-meta.js';
27
27
 
28
+ // A live tab declares the page format it can read on its own stream, because a tab whose
29
+ // store boots staged never sends /_/sync/data, and the host would otherwise count it as an
30
+ // older editor and refuse every save that raises the page floor.
31
+ const formatParam = (format) => (format == null ? '' : `&blockeditor-format=${format}`);
32
+
28
33
  export const WIRE_PROFILES = {
29
34
  spec: {
30
35
  name: 'spec',
31
36
  relayPath: '/_/sync',
32
37
  documentHeader: 'Document-URL',
33
38
  snapshotKey: 'snapshot',
34
- streamPath: (href, lane, resumeId, clientId) =>
35
- `/_/sync?document-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
39
+ streamPath: (href, lane, resumeId, clientId, format) =>
40
+ `/_/sync?document-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}${formatParam(format)}`,
36
41
  },
37
42
  // What every published clayjs speaks, and what hyperclayjs and the inline script in
38
43
  // every Collection dashboard hardcode. Hosts keep these addresses permanently.
@@ -41,8 +46,8 @@ export const WIRE_PROFILES = {
41
46
  relayPath: '/_/live-sync/save',
42
47
  documentHeader: 'Page-URL',
43
48
  snapshotKey: 'html',
44
- streamPath: (href, lane, resumeId, clientId) =>
45
- `/_/live-sync/stream?page-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
49
+ streamPath: (href, lane, resumeId, clientId, format) =>
50
+ `/_/live-sync/stream?page-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}${formatParam(format)}`,
46
51
  },
47
52
  };
48
53
 
@@ -186,10 +186,13 @@ export function bevelButton(label, { variant = 'default', small = false, onClick
186
186
  return b;
187
187
  }
188
188
 
189
- export function bevelIconButton(svg, { label, onClick = null }) {
189
+ export function bevelIconButton(svg, { label, onClick = null, theme = null, parts = [], labelParts = [] }) {
190
190
  const b = bevelButton('', {
191
191
  small: true,
192
192
  onClick,
193
+ theme,
194
+ parts,
195
+ labelParts,
193
196
  extra: ['width:26px', 'height:26px', 'padding:0', 'display:inline-grid', 'place-items:center'],
194
197
  labelExtra: ['display:inline-grid', 'width:12px', 'height:12px'],
195
198
  });
package/src/ui/dialogs.js CHANGED
@@ -6,9 +6,10 @@ import { bevelBox, bevelButton, bevelInput, bevelText } from "./bevel-controls.j
6
6
  import { TOKENS, FONT_SANS, FONT_MONO } from "./bevel.js";
7
7
  import { resolveTheme } from "./theme.js";
8
8
 
9
- // Caller markup (promptText, extraContent, tell's paragraphs) goes in as it is: those
10
- // are markup by design, and callers pass elements through them. Only the frame around
11
- // it is ClayJS's to style. The prompt is the dialog's title, as in the dashboard.
9
+ // Prompts, titles, paragraphs and labels are text (or a node placed as it is), so a
10
+ // record or a visitor cannot run markup. extraContent is markup (or a node). Only the
11
+ // frame around it is ClayJS's to style. The prompt is the dialog's title, as in the
12
+ // dashboard.
12
13
  function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "", yesLabel = "OK") {
13
14
  const theme = resolveTheme();
14
15
  const content = bevelBox("div", ["display:flex", "flex-direction:column", "gap:14px", `font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`], { theme, parts: ["dialog.content"], role: "text" });
@@ -19,7 +20,8 @@ function createModal(promptText, yesCallback, extraContent = "", includeInput =
19
20
  input.required = true;
20
21
  content.append(input);
21
22
  }
22
- content.insertAdjacentHTML("beforeend", extraContent);
23
+ if (extraContent instanceof Node) content.append(extraContent);
24
+ else if (extraContent) content.insertAdjacentHTML("beforeend", extraContent);
23
25
 
24
26
  themodal.title = promptText;
25
27
  themodal.html = includeInput || extraContent ? content : "";
@@ -102,7 +104,8 @@ export function tell(promptText, ...content) {
102
104
  const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:12px"], { theme, parts: ["dialog.content"] });
103
105
  for (const c of content) {
104
106
  const paragraph = bevelText("div", [`font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`, "overflow-wrap:anywhere"], undefined, { theme });
105
- paragraph.innerHTML = c;
107
+ if (c instanceof Node) paragraph.append(c);
108
+ else paragraph.textContent = c;
106
109
  box.append(paragraph);
107
110
  }
108
111
 
@@ -148,7 +151,7 @@ export function snippet(title, content, extraContent = '') {
148
151
  `background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`,
149
152
  ], { theme, parts: ["dialog.well"], role: "well" });
150
153
  const pre = bevelText("pre", ["display:block", "margin:0", "white-space:nowrap", `font:13px/1.6 ${FONT_MONO}`], undefined, { theme, parts: ["dialog.code"], role: "code" });
151
- pre.innerHTML = content;
154
+ pre.textContent = content;
152
155
  well.append(pre);
153
156
  const copy = bevelButton("Copy", {
154
157
  small: true,
@@ -160,7 +163,8 @@ export function snippet(title, content, extraContent = '') {
160
163
  },
161
164
  });
162
165
  box.append(well, copy);
163
- box.insertAdjacentHTML("beforeend", extraContent || "");
166
+ if (extraContent instanceof Node) box.append(extraContent);
167
+ else if (extraContent) box.insertAdjacentHTML("beforeend", extraContent);
164
168
 
165
169
  themodal.title = title;
166
170
  themodal.html = box;
package/src/ui/modal.js CHANGED
@@ -18,12 +18,12 @@ export const SURFACE_THEME = Symbol("clay.surfaceTheme");
18
18
  - user confirms
19
19
  - everything about the modal resets
20
20
 
21
- themodal.html = content; // html, yes and no each take markup, or a node placed as it is
22
- themodal.yes = content;
21
+ themodal.html = content; // the body: markup, or a node placed as it is
22
+ themodal.yes = content; // text, or a node placed as it is
23
23
  themodal.no = content;
24
24
 
25
25
  themodal.closeHtml = "x"; // any non-empty value shows the corner close
26
- themodal.title = content; // markup or a node: a header with the title (optional)
26
+ themodal.title = content; // text or a node: a header with the title (optional)
27
27
  themodal.width = "440px"; // the panel's width (default 600px)
28
28
 
29
29
  themodal.disableFocus = true;
@@ -367,12 +367,14 @@ const themodal = (() => {
367
367
  modalYesElem.type = 'submit';
368
368
  modalButtonsElem.append(modalNoElem, modalYesElem);
369
369
 
370
- // Markup is parsed in; a node is placed as it is, listeners and all.
371
- const place = (target, value) => {
370
+ // A node is placed as it is. A string is text, except in the body, which is
371
+ // the one slot that takes markup.
372
+ const place = (target, value, markup = false) => {
372
373
  if (value instanceof Node) target.append(value);
373
- else target.innerHTML = value;
374
+ else if (markup) target.innerHTML = value;
375
+ else target.textContent = value;
374
376
  };
375
- place(modalContentElem, html);
377
+ place(modalContentElem, html, true);
376
378
  place(modalYesElem.firstChild, yes);
377
379
  place(modalNoElem.firstChild, no);
378
380
  if (title) place(shell.heading, title);
package/src/ui/toast.js CHANGED
@@ -3,9 +3,11 @@
3
3
  //
4
4
  // The Hyperclay dashboard's toast (Bevel's React toast with the dashboard's overrides),
5
5
  // drawn inline: a 3px tone edge, a 16px Bevel glyph, 13.5px semibold text, a quiet close.
6
- import { set } from "../lib/hostile-css.js";
7
- import { bevelBox, bevelSurface, bevelText, bevelButton, protectIcon, paintLook } from "./bevel-controls.js";
6
+ import { set, style } from "../lib/hostile-css.js";
7
+ import { bevelBox, bevelSurface, bevelText, bevelButton, protectIcon, paintLook, restyleBox } from "./bevel-controls.js";
8
8
  import { TOKENS, RULES, GLYPHS } from "./bevel.js";
9
+ import { resolveTheme } from "./theme.js";
10
+ import { themed, tokenRulesFor, partRules, markParts } from "./theme-parts.js";
9
11
  import { mountEditorUI } from "./editor-ui.js";
10
12
 
11
13
  const TONES = {
@@ -34,48 +36,49 @@ function motion() {
34
36
  // body, and a detached stack would swallow every toast after it. The stack sits in its own
35
37
  // shadow root: the inline !important rules already beat the page's stylesheets, and the
36
38
  // root also keeps out what they cannot, such as a page's ::before content.
37
- function stack() {
39
+ function stack(theme) {
38
40
  const existing = document.querySelector('clay-editor-ui[data-clay-ui="toasts"]')?.shadowRoot.querySelector("[data-clay-toasts]");
39
41
  if (existing) return existing;
40
42
  const box = bevelBox("div", [
41
43
  "position:fixed", "top:18px", "right:18px", "z-index:9999",
42
44
  "display:flex", "flex-direction:column", "align-items:flex-end", "gap:10px",
43
45
  "width:min(330px, calc(100vw - 36px))", "pointer-events:none",
44
- ]);
46
+ ], { theme, parts: ["toast.stack"] });
45
47
  box.setAttribute("data-clay-toasts", "");
46
48
  box.setAttribute("aria-live", "polite");
47
49
  mountEditorUI({ name: "toasts", content: box });
48
50
  return box;
49
51
  }
50
52
 
51
- function build(message, type, { detail = "", actions = [] } = {}) {
53
+ function build(message, type, { detail = "", actions = [] } = {}, theme) {
52
54
  const tone = TONES[type];
53
55
  const el = bevelSurface("div", [
54
56
  "display:flex", "align-items:flex-start", ...RULES.toast, "width:100%",
55
57
  `border-inline-start:3px solid ${tone.edge}`,
56
58
  "cursor:pointer", "pointer-events:auto",
57
59
  ...HIDDEN, `transition:${motion()}`,
58
- ]);
60
+ ], { theme, parts: ["toast.panel"] });
61
+ if (themed(theme)) style(el, [`border-inline-start-color:${tone.edge}`, ...partRules(theme, ["toast.panel"], {})]);
59
62
  el.setAttribute("data-clay-toast", type);
60
- const body = bevelBox("div", ["flex:1 1 auto", "min-width:0", "display:flex", "flex-direction:column", "gap:8px"]);
61
- const head = bevelBox("div", ["display:flex", "align-items:flex-start", "gap:8px"]);
62
- const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`]);
63
+ const body = bevelBox("div", ["flex:1 1 auto", "min-width:0", "display:flex", "flex-direction:column", "gap:8px"], { theme });
64
+ const head = bevelBox("div", ["display:flex", "align-items:flex-start", "gap:8px"], { theme });
65
+ const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`], undefined, { theme, parts: ["toast.icon"], role: null });
63
66
  glyph.innerHTML = tone.icon;
64
67
  protectIcon(glyph.firstElementChild, 16);
65
68
  // A message can carry a filename, and a filename is whatever whoever wrote the
66
69
  // file chose. It goes in as text so markup in a name can never become markup here.
67
- const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message);
70
+ const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message, { theme, parts: ["toast.text"] });
68
71
  head.append(glyph, text);
69
72
  body.append(head);
70
73
  // Detail and actions are the same untrusted text as the title: textContent for
71
74
  // both, so a reason string can never become markup in the toast either.
72
75
  if (detail) {
73
- body.append(bevelText("span", ["display:block", `padding-inline-start:${TEXT_INSET}px`, `color:${TOKENS.muted}`, "overflow-wrap:anywhere"], detail));
76
+ body.append(bevelText("span", ["display:block", `padding-inline-start:${TEXT_INSET}px`, `color:${TOKENS.muted}`, "overflow-wrap:anywhere"], detail, { theme, parts: ["toast.text"], role: "mutedText" }));
74
77
  }
75
78
  if (actions.length) {
76
- const row = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", `padding-inline-start:${TEXT_INSET - BUTTON_LABEL_INSET}px`]);
79
+ const row = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", `padding-inline-start:${TEXT_INSET - BUTTON_LABEL_INSET}px`], { theme });
77
80
  for (const { label, onClick } of actions) {
78
- row.append(bevelButton(label, { small: true, variant: "quiet", onClick, extra: ["padding:4px 8px"] }));
81
+ row.append(bevelButton(label, { small: true, variant: "quiet", onClick, extra: ["padding:4px 8px"], theme }));
79
82
  }
80
83
  body.append(row);
81
84
  }
@@ -84,31 +87,61 @@ function build(message, type, { detail = "", actions = [] } = {}) {
84
87
  }
85
88
 
86
89
  // The dashboard's toast close: a bare 24px square in the muted ink, the sunk ground on hover.
87
- function closeButton(onClick) {
88
- const b = bevelBox("button", [
90
+ function closeButton(onClick, theme) {
91
+ const base = [
89
92
  "flex:none", "display:inline-grid", "place-items:center", ...RULES.toastClose,
90
93
  "cursor:pointer",
91
- ]);
94
+ ];
95
+ const b = bevelBox("button", base);
92
96
  b.type = "button";
93
97
  b.setAttribute("aria-label", "Dismiss");
94
98
  b.title = "Dismiss";
95
99
  b.innerHTML = CLOSE_ICON;
96
- protectIcon(b.firstElementChild, 14);
97
- const paint = (lit) => {
98
- set(b, "color", lit ? TOKENS.ink : TOKENS.muted);
99
- set(b, "background", lit ? TOKENS.sunk : "transparent");
100
+ if (!themed(theme)) {
101
+ protectIcon(b.firstElementChild, 14);
102
+ const paint = (lit) => {
103
+ set(b, "color", lit ? TOKENS.ink : TOKENS.muted);
104
+ set(b, "background", lit ? TOKENS.sunk : "transparent");
105
+ };
106
+ b.addEventListener("pointerenter", () => paint(true));
107
+ b.addEventListener("pointerleave", () => paint(false));
108
+ b.addEventListener("focus", () => { paint(true); set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "1px"); });
109
+ b.addEventListener("blur", () => { paint(false); set(b, "outline", "none"); });
110
+ b.addEventListener("click", onClick);
111
+ return b;
112
+ }
113
+ const names = ["close", "toast.close"];
114
+ const icon = ["icon", "toast.closeIcon"];
115
+ markParts(b, theme, names);
116
+ markParts(b.firstElementChild, theme, icon);
117
+ const flags = { hover: false, focus: false, pressed: false };
118
+ const paint = () => {
119
+ const act = { hover: flags.hover, active: flags.pressed, focus: flags.focus };
120
+ const svg = b.firstElementChild;
121
+ svg.style.cssText = "";
122
+ protectIcon(svg, 14);
123
+ style(svg, partRules(theme, icon, act));
124
+ restyleBox(b, [...base, ...tokenRulesFor(theme, "close", act), ...partRules(theme, names, act)]);
100
125
  };
101
- b.addEventListener("pointerenter", () => paint(true));
102
- b.addEventListener("pointerleave", () => paint(false));
103
- b.addEventListener("focus", () => { paint(true); set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "1px"); });
104
- b.addEventListener("blur", () => { paint(false); set(b, "outline", "none"); });
126
+ b.addEventListener("pointerenter", () => { flags.hover = true; paint(); });
127
+ b.addEventListener("pointerleave", () => { flags.hover = false; flags.pressed = false; paint(); });
128
+ b.addEventListener("pointerdown", (e) => {
129
+ if (e.button !== undefined && e.button !== 0) return;
130
+ flags.pressed = true;
131
+ paint();
132
+ });
133
+ b.addEventListener("pointerup", () => { flags.pressed = false; paint(); });
134
+ b.addEventListener("focus", () => { flags.focus = true; paint(); });
135
+ b.addEventListener("blur", () => { flags.focus = false; flags.pressed = false; paint(); });
105
136
  b.addEventListener("click", onClick);
137
+ paint();
106
138
  return b;
107
139
  }
108
140
 
109
- function reveal(el) {
110
- stack().append(el);
111
- paintLook(el, { background: TOKENS.surface });
141
+ function reveal(el, theme) {
142
+ stack(theme).append(el);
143
+ if (themed(theme)) set(el, "color-scheme", theme.scheme === "light dark" ? "light" : theme.scheme);
144
+ else paintLook(el, { background: TOKENS.surface });
112
145
  setTimeout(() => {
113
146
  set(el, "opacity", "1");
114
147
  set(el, "transform", "none");
@@ -125,9 +158,10 @@ function dismiss(el, after) {
125
158
  }
126
159
 
127
160
  function toast(message, messageType = "success") {
128
- const el = build(message, TONES[messageType] ? messageType : "success");
161
+ const theme = resolveTheme();
162
+ const el = build(message, TONES[messageType] ? messageType : "success", {}, theme);
129
163
  el.addEventListener("click", () => dismiss(el));
130
- reveal(el);
164
+ reveal(el, theme);
131
165
  setTimeout(() => dismiss(el), 6600);
132
166
  }
133
167
 
@@ -142,17 +176,18 @@ function toastPersistent(message, messageType = "warning", { detail = "", action
142
176
  activePersistentToasts.delete(message);
143
177
  }
144
178
 
145
- const el = build(message, TONES[messageType] ? messageType : "warning", { detail, actions });
179
+ const theme = resolveTheme();
180
+ const el = build(message, TONES[messageType] ? messageType : "warning", { detail, actions }, theme);
146
181
  set(el, "cursor", "default");
147
182
  const close = closeButton((e) => {
148
183
  e.stopPropagation();
149
184
  // A newer toast with the same message may have taken this entry since.
150
185
  dismiss(el, () => { if (activePersistentToasts.get(message) === el) activePersistentToasts.delete(message); });
151
- });
186
+ }, theme);
152
187
  el.append(close);
153
188
 
154
189
  activePersistentToasts.set(message, el);
155
- reveal(el);
190
+ reveal(el, theme);
156
191
  }
157
192
 
158
193
  // Takes down the persistent toast showing this message, if one is up. `now` takes the