@panphora/clayjs 1.23.0 → 1.24.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 (471) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/blockeditor/themes/light-1.css +431 -0
  3. package/dist/clay-dom.js +1 -1
  4. package/dist/clay-events.js +1 -1
  5. package/dist/clay-internals.js +1 -1
  6. package/dist/clay-options.js +1 -1
  7. package/dist/clay-router.js +1 -1
  8. package/dist/clay-ui.js +1 -1
  9. package/dist/clay-utils.js +1 -1
  10. package/dist/clay.js +1 -1
  11. package/dist/clay.standalone.js +30285 -26613
  12. package/dist/m/c-277QP5ET.js +1 -0
  13. package/dist/m/c-2BLVA6IN.js +5 -0
  14. package/dist/m/c-2OJMOUPC.js +1 -0
  15. package/dist/m/c-2PVGJFI5.js +1 -0
  16. package/dist/m/{c-AYVGD6A6.js → c-3UW5EJ77.js} +1 -1
  17. package/dist/m/{c-T5NK2TVY.js → c-42ZNV7LI.js} +1 -1
  18. package/dist/m/{c-3TLG3OLG.js → c-46LJAZZJ.js} +1 -1
  19. package/dist/m/c-47IBJGPQ.js +1 -0
  20. package/dist/m/c-5FXEKU4Y.js +1 -0
  21. package/dist/m/c-5KXDKC5G.js +1 -0
  22. package/dist/m/{c-EUGJVHDD.js → c-5RZNFTWD.js} +1 -1
  23. package/dist/m/c-65LJU2H2.js +72 -0
  24. package/dist/m/{c-NQJRVJ56.js → c-6IJEVE6B.js} +1 -1
  25. package/dist/m/{c-NW54NHKE.js → c-6LEYVQJK.js} +1 -1
  26. package/dist/m/{c-GSRLXYWL.js → c-6Q6RGFBF.js} +1 -1
  27. package/dist/m/c-6RGEQNVV.js +1 -0
  28. package/dist/m/c-7CM4GMTS.js +6 -0
  29. package/dist/m/c-7NCWENHV.js +1 -0
  30. package/dist/m/c-7V6MGMX2.js +1 -0
  31. package/dist/m/{c-LKSL4AUJ.js → c-7WZSUTYP.js} +1 -1
  32. package/dist/m/c-A4L7APIU.js +1 -0
  33. package/dist/m/c-APGHGDZM.js +1 -0
  34. package/dist/m/c-AT7GLQGV.js +34 -0
  35. package/dist/m/{c-ZRXJD4G6.js → c-B7GOFGQR.js} +1 -1
  36. package/dist/m/{c-KDXIJEEF.js → c-BGN7RCTJ.js} +1 -1
  37. package/dist/m/c-BKM46LUJ.js +1 -0
  38. package/dist/m/c-BOMQIQ3V.js +1 -0
  39. package/dist/m/{c-62HZW5DH.js → c-C5AKJVRH.js} +1 -1
  40. package/dist/m/{c-BXYSDKAN.js → c-CJ7QYEWI.js} +1 -1
  41. package/dist/m/c-CTGRDT6E.js +1 -0
  42. package/dist/m/c-CV4T65JE.js +6 -0
  43. package/dist/m/c-CVSWLX3X.js +1 -0
  44. package/dist/m/c-CZHTXGJS.js +1 -0
  45. package/dist/m/c-D7NT5S4F.js +1 -0
  46. package/dist/m/c-DD26VXAP.js +1 -0
  47. package/dist/m/{c-ANNFZFLN.js → c-DDS23EY6.js} +1 -1
  48. package/dist/m/{c-ALDVFPWL.js → c-DFKBGSN2.js} +1 -1
  49. package/dist/m/c-DIKR7H57.js +147 -0
  50. package/dist/m/c-DXPOFLEJ.js +1 -0
  51. package/dist/m/c-EDE45NM7.js +1 -0
  52. package/dist/m/c-EILLN2H2.js +1 -0
  53. package/dist/m/c-EODTTNVA.js +1 -0
  54. package/dist/m/{c-G6COVIZB.js → c-ESK4S67I.js} +1 -1
  55. package/dist/m/c-FDWR3FL7.js +1 -0
  56. package/dist/m/{c-WQZTI5P2.js → c-FEC6LGFF.js} +1 -1
  57. package/dist/m/c-FLEIU6CG.js +1 -0
  58. package/dist/m/c-FQAQRXGK.js +1 -0
  59. package/dist/m/{c-W4JCVLAB.js → c-FSQXFG5T.js} +1 -1
  60. package/dist/m/c-FVPNX2GO.js +135 -0
  61. package/dist/m/c-FYDKYW6E.js +1 -0
  62. package/dist/m/c-GNI7WHKF.js +32 -0
  63. package/dist/m/c-GO635CRU.js +3 -0
  64. package/dist/m/c-H3FNLBHN.js +2 -0
  65. package/dist/m/c-HDVH3MVC.js +72 -0
  66. package/dist/m/c-HIX5H5L7.js +1 -0
  67. package/dist/m/c-HKSLOVU4.js +1 -0
  68. package/dist/m/{c-SGRIYOAJ.js → c-HL6BLJNA.js} +1 -1
  69. package/dist/m/c-HTLYXCUH.js +1 -0
  70. package/dist/m/{c-QIR5CAKM.js → c-IUL7PU4P.js} +1 -1
  71. package/dist/m/c-IXO7EI2Q.js +1 -0
  72. package/dist/m/{c-W5E5P2TZ.js → c-JQF2EUCG.js} +1 -1
  73. package/dist/m/{c-NEL7HQW3.js → c-JWDCQL6J.js} +1 -1
  74. package/dist/m/c-KQKKIVLW.js +20 -0
  75. package/dist/m/c-LATNQ6Q5.js +1 -0
  76. package/dist/m/c-LD6UVV5R.js +1 -0
  77. package/dist/m/c-LS7IVQVW.js +1 -0
  78. package/dist/m/{c-JCOQVZYS.js → c-LSVN3OLG.js} +1 -1
  79. package/dist/m/{c-JDZDF2QM.js → c-LTSPHVAK.js} +1 -1
  80. package/dist/m/c-MFOSMAVM.js +3 -0
  81. package/dist/m/c-MJ7GVE46.js +1 -0
  82. package/dist/m/c-MP6S4AZU.js +6 -0
  83. package/dist/m/c-MQKSKQHG.js +2 -0
  84. package/dist/m/{c-UHUNXWKN.js → c-MQYEDF3Q.js} +1 -1
  85. package/dist/m/{c-IHUWUKIU.js → c-MR75YPRT.js} +1 -1
  86. package/dist/m/c-ND3Q2G2I.js +1 -0
  87. package/dist/m/c-NEQRFC4O.js +1 -0
  88. package/dist/m/{c-5FKRZNJV.js → c-NFQFV7OP.js} +1 -1
  89. package/dist/m/c-NHXOBD6A.js +1 -0
  90. package/dist/m/{c-QIKT6SKM.js → c-NICICT4P.js} +1 -1
  91. package/dist/m/c-NQAPWM7O.js +1 -0
  92. package/dist/m/{c-QUGCKMWF.js → c-NVU3DORS.js} +1 -1
  93. package/dist/m/{c-LWIDDWAR.js → c-OCYG4NBQ.js} +22 -22
  94. package/dist/m/c-OH2FPYSY.js +19 -0
  95. package/dist/m/c-OYHI67B6.js +1 -0
  96. package/dist/m/{c-R2XUODVY.js → c-OYW5MFPZ.js} +1 -1
  97. package/dist/m/{c-5B4YBZXC.js → c-P2HKMJAW.js} +1 -1
  98. package/dist/m/{c-TSBVMWAH.js → c-PCNJETUA.js} +1 -1
  99. package/dist/m/c-PDBNCJCT.js +1 -0
  100. package/dist/m/c-PGSBXHUE.js +1 -0
  101. package/dist/m/{c-5TIZ2I64.js → c-PYG2NGWU.js} +1 -1
  102. package/dist/m/c-Q26XHBLQ.js +1 -0
  103. package/dist/m/c-Q6BG3ZLO.js +1 -0
  104. package/dist/m/c-QENKN7QI.js +1 -0
  105. package/dist/m/c-QQFHEODI.js +1 -0
  106. package/dist/m/c-QTEUO2K6.js +1 -0
  107. package/dist/m/c-R23UHMXG.js +1 -0
  108. package/dist/m/c-RETTCOWO.js +10 -0
  109. package/dist/m/c-SE3SIY7Y.js +7 -0
  110. package/dist/m/{c-XX75FFNL.js → c-SGGGCIVD.js} +1 -1
  111. package/dist/m/c-SGH3UTF3.js +1 -0
  112. package/dist/m/c-SIM7Z3U7.js +1 -0
  113. package/dist/m/{c-X5LNHET5.js → c-SKMIR5HQ.js} +1 -1
  114. package/dist/m/{c-R27DWQKX.js → c-STRI73GQ.js} +1 -1
  115. package/dist/m/c-SUXHUL7U.js +1 -0
  116. package/dist/m/c-SXGEWPFC.js +10 -0
  117. package/dist/m/c-SXNSSHPP.js +1 -0
  118. package/dist/m/c-T3P3AXEW.js +1 -0
  119. package/dist/m/{c-PXLTUYZC.js → c-TFLRT5JX.js} +1 -1
  120. package/dist/m/{c-YCLU4UNT.js → c-TGIMBAWE.js} +1 -1
  121. package/dist/m/c-TGSHHFO6.js +1 -0
  122. package/dist/m/c-U4NTUVDZ.js +1 -0
  123. package/dist/m/c-UOOIF2AL.js +1 -0
  124. package/dist/m/{c-6TYGD5AB.js → c-UOUUI265.js} +1 -1
  125. package/dist/m/c-V2U3F4QX.js +1 -0
  126. package/dist/m/c-V2V5FKXX.js +1 -0
  127. package/dist/m/c-VBXRXTVS.js +26 -0
  128. package/dist/m/c-VHSMPITY.js +1 -0
  129. package/dist/m/c-VPXCXBDT.js +1 -0
  130. package/dist/m/c-WCG6AKA7.js +1 -0
  131. package/dist/m/c-WNOLMHI2.js +1 -0
  132. package/dist/m/{c-CRPIDGXC.js → c-X6MOYATI.js} +1 -1
  133. package/dist/m/{c-FQGHR32O.js → c-X75G43SZ.js} +1 -1
  134. package/dist/m/c-XB6EAIAR.js +13 -0
  135. package/dist/m/c-XMIDHEKF.js +1 -0
  136. package/dist/m/c-XUZMRQ7O.js +4 -0
  137. package/dist/m/{c-2Z22FCFK.js → c-XW5TIYYB.js} +1 -1
  138. package/dist/m/c-YDW4VCHP.js +1 -0
  139. package/dist/m/{c-QGAPG7OM.js → c-YLHTD3KX.js} +1 -1
  140. package/dist/m/c-YNIEIPBS.js +1 -0
  141. package/dist/m/c-Z4G2UK6T.js +4 -0
  142. package/dist/m/c-Z6MROEQ4.js +1 -0
  143. package/dist/m/c-ZFO5RW6G.js +2 -0
  144. package/dist/m/c-ZGCO4J3H.js +8 -0
  145. package/dist/m/c-ZKKAF4UT.js +3 -0
  146. package/dist/m/c-ZRGEN6QQ.js +4 -0
  147. package/dist/m/core/{admin-attrs-U7IJNYBG.js → admin-attrs-ONRWVXPK.js} +1 -1
  148. package/dist/m/core/{autosave-F5YQCKVL.js → autosave-TBKAAAFB.js} +1 -1
  149. package/dist/m/core/conflict-notice-FOEE6I7E.js +1 -0
  150. package/dist/m/core/draft-mode-4MCL3LYI.js +1 -0
  151. package/dist/m/core/{host-permission-6EVJGNPL.js → host-permission-X4BENZEU.js} +1 -1
  152. package/dist/m/core/{is-edit-mode-GCX6767E.js → is-edit-mode-FFHCQ5DR.js} +1 -1
  153. package/dist/m/core/{persist-CJXIL7D4.js → persist-6ILUR34S.js} +1 -1
  154. package/dist/m/core/{recovery-DRFZRJED.js → recovery-APWADU4K.js} +1 -1
  155. package/dist/m/core/save-6HT2LABE.js +1 -0
  156. package/dist/m/core/save-core-EVMNZQ4V.js +1 -0
  157. package/dist/m/core/site-edits-G4BOZMV4.js +1 -0
  158. package/dist/m/core/{unsaved-warning-HNQVTA2D.js → unsaved-warning-ITZDIZ2I.js} +1 -1
  159. package/dist/m/internals/{index-NRPNDTSA.js → index-HS3CQDFW.js} +1 -1
  160. package/dist/m/loader-GLLVDUZA.js +1 -0
  161. package/dist/m/manifest-50FF1C45.js +1 -0
  162. package/dist/m/manifest-edit-BE1A621D.js +1 -0
  163. package/dist/m/manifest.json +2072 -1811
  164. package/dist/m/page/editor-KAZDJNND.js +307 -0
  165. package/dist/m/plugins/ai-edit-LABECCSN.js +6 -0
  166. package/dist/m/plugins/{cms-H7DTMUQX.js → cms-T2HLI7JG.js} +16 -16
  167. package/dist/m/plugins/{demo-42BSYQ7T.js → demo-T7B4RIKL.js} +1 -1
  168. package/dist/m/plugins/{derive-QVI3LI3A.js → derive-6LPJOCAV.js} +1 -1
  169. package/dist/m/plugins/indicator-J4ABFARX.js +1 -0
  170. package/dist/m/plugins/{live-DCUBKXY4.js → live-PGBIEMGB.js} +1 -1
  171. package/dist/m/plugins/menu-YZL3X2AJ.js +1 -0
  172. package/dist/m/plugins/page-WOCNX2IN.js +2 -0
  173. package/dist/m/plugins/page-view-SJODOOGE.js +1 -0
  174. package/dist/m/plugins/{people-5XRNRP5U.js → people-C4TCWFOQ.js} +1 -1
  175. package/dist/m/plugins/{quickcrop-WWF5I422.js → quickcrop-X5MRIBAP.js} +2 -2
  176. package/dist/m/plugins/richclay-Y2NV44NA.js +549 -0
  177. package/dist/m/plugins/sortable-XCHQG65Z.js +1 -0
  178. package/dist/m/plugins/{source-6BKMK4PO.js → source-YK7MO23W.js} +1 -1
  179. package/dist/m/plugins/{undo-3JEYK5QY.js → undo-VLRBQNPN.js} +1 -1
  180. package/dist/m/plugins/upgrade-NNTJBSK2.js +1 -0
  181. package/dist/m/plugins/{upload-TTCQRIM5.js → upload-ZKKJV76X.js} +1 -1
  182. package/dist/m/plugins/visitor-drafts-RTZNONHT.js +20 -0
  183. package/dist/m/plugins/{visitor-drafts-owner-Z5B2OQBR.js → visitor-drafts-owner-LHUHFU3L.js} +1 -1
  184. package/dist/m/plugins/{visitor-menu-4LQMKLPP.js → visitor-menu-FY4KM2SW.js} +1 -1
  185. package/dist/m/plugins/wire-M2NR354P.js +1 -0
  186. package/dist/m/{reader-core-POFQCKRV.js → reader-core-T2S3JC4E.js} +2 -2
  187. package/dist/m/sync/live-sync-Z64YREWQ.js +1 -0
  188. package/dist/m/ui/index-MGTWOW2C.js +1 -0
  189. package/dist/m/vendor/{sap-core.vendor-OA5SKL7Q.js → sap-core.vendor-QAEPMETX.js} +1 -1
  190. package/dist/offline-manifest.json +1250 -1082
  191. package/dist/server/core.cjs +132 -109
  192. package/dist/server/page-compaction-worker.cjs +5 -5
  193. package/dist/server/rooms.mjs +3 -1
  194. package/docs/guide.md +89 -44
  195. package/entries/clay-data.js +1 -1
  196. package/entries/sap.js +1 -1
  197. package/package.json +5 -3
  198. package/src/blockeditor/themes/content.css +70 -0
  199. package/src/blockeditor/themes/light-1.css +431 -0
  200. package/src/blockeditor/themes/light-1.source.css +270 -0
  201. package/src/blockeditor/themes/light-2.css +211 -0
  202. package/src/blockeditor/themes/light-3.css +130 -0
  203. package/src/blockeditor/themes/light-4.css +299 -0
  204. package/src/core/conflict-notice.js +28 -0
  205. package/src/core/draft-mode.js +6 -0
  206. package/src/core/host-meta.js +6 -6
  207. package/src/core/is-edit-mode.js +9 -3
  208. package/src/core/mode-support.js +3 -0
  209. package/src/core/mode-switch.js +4 -4
  210. package/src/core/mode-transition.js +3 -1
  211. package/src/core/page-format-wire.js +1 -1
  212. package/src/core/page-format.js +8 -8
  213. package/src/core/save-accepted.js +6 -7
  214. package/src/core/save-core.js +52 -23
  215. package/src/core/save-failure-notice.js +37 -0
  216. package/src/core/save-feedback.js +6 -0
  217. package/src/core/save-replace.js +1 -1
  218. package/src/core/save-size.js +2 -2
  219. package/src/core/save.js +78 -15
  220. package/src/core/site-edits.js +166 -0
  221. package/src/core/unsaved-warning.js +2 -2
  222. package/src/core/upgrade-run.js +4 -4
  223. package/src/lib/ai-comment.js +23 -0
  224. package/src/lib/effect.js +8 -0
  225. package/src/lib/idb-error-code.js +20 -0
  226. package/src/live/fast-lane.js +10 -10
  227. package/src/live/local-lane.js +10 -10
  228. package/src/live/page-compaction.js +8 -8
  229. package/src/live/server-lane.js +1 -1
  230. package/src/loader-logic.js +48 -18
  231. package/src/loader.js +11 -6
  232. package/src/page/adopt-boot.js +1 -1
  233. package/src/page/adopt.js +2 -2
  234. package/src/page/ai-area.js +182 -0
  235. package/src/page/ai-preview.js +80 -0
  236. package/src/page/ai-surface.js +257 -0
  237. package/src/page/ai-tool.js +308 -145
  238. package/src/page/announce.js +2 -2
  239. package/src/page/awareness.js +18 -18
  240. package/src/page/blocks.js +35 -34
  241. package/src/page/blocksel.js +2 -2
  242. package/src/page/board-drag.js +11 -11
  243. package/src/page/board-face.js +1 -1
  244. package/src/page/board-panel.js +26 -26
  245. package/src/page/board-style.js +127 -127
  246. package/src/page/board-view.js +48 -48
  247. package/src/page/chart.js +29 -29
  248. package/src/page/checkpoint-proof.js +19 -19
  249. package/src/page/chrome.js +74 -65
  250. package/src/page/clipboard-shape.js +1 -1
  251. package/src/page/csv-import.js +4 -4
  252. package/src/page/drag.js +5 -5
  253. package/src/page/editor.js +301 -296
  254. package/src/page/export-html.js +8 -8
  255. package/src/page/features/checklist.js +36 -36
  256. package/src/page/features/export-html.js +19 -19
  257. package/src/page/features/export-markdown.js +10 -10
  258. package/src/page/features/import-markdown.js +13 -13
  259. package/src/page/features/media.js +50 -50
  260. package/src/page/features/table.js +17 -17
  261. package/src/page/features/title-cover.js +17 -17
  262. package/src/page/features/title-icon.js +7 -7
  263. package/src/page/features/toc.js +1 -1
  264. package/src/page/features/tool.js +42 -18
  265. package/src/page/format-command.js +4 -4
  266. package/src/page/format-gate.js +6 -6
  267. package/src/page/format-host.js +6 -6
  268. package/src/page/format-level.js +3 -3
  269. package/src/page/format-native-commands.js +2 -2
  270. package/src/page/highlight-slot.js +1 -1
  271. package/src/page/highlight.js +4 -4
  272. package/src/page/history-intent.js +2 -2
  273. package/src/page/image-actions.js +4 -4
  274. package/src/page/link.js +10 -10
  275. package/src/page/media.js +3 -3
  276. package/src/page/mention-picker.js +7 -7
  277. package/src/page/merge.js +1 -1
  278. package/src/page/placeholder.js +3 -3
  279. package/src/page/projection.js +11 -11
  280. package/src/page/reader-patch.js +2 -2
  281. package/src/page/recover.js +26 -26
  282. package/src/page/recovery-ui.js +28 -28
  283. package/src/page/rich/chart.js +1 -1
  284. package/src/page/rich/page-tool.js +21 -6
  285. package/src/page/rich/tool-runtime.js +3 -2
  286. package/src/page/save-projection.js +2 -2
  287. package/src/page/save-states.js +9 -9
  288. package/src/page/schema.js +9 -9
  289. package/src/page/table-style.js +21 -21
  290. package/src/page/toc-link.js +1 -1
  291. package/src/page/tool-capability.js +8 -3
  292. package/src/page/tool-transport.js +52 -0
  293. package/src/page/tool-views.js +4 -0
  294. package/src/page/tools.js +6 -1
  295. package/src/page/view.js +6 -6
  296. package/src/page/ybind/binding.js +1 -1
  297. package/src/page/ybind/conversion.js +1 -1
  298. package/src/page/ybind/layout.js +4 -4
  299. package/src/page/ybind/migrate.js +1 -1
  300. package/src/page/ybind/origins.js +2 -2
  301. package/src/page/ybind/provider.js +2 -2
  302. package/src/page/ybind/recovery-proof.js +3 -3
  303. package/src/page/ybind/recovery-replay.js +2 -2
  304. package/src/plugins/ai-edit/picker.js +235 -0
  305. package/src/plugins/ai-edit.js +220 -34
  306. package/src/plugins/indicator-chip.js +39 -34
  307. package/src/plugins/indicator.js +13 -4
  308. package/src/plugins/live.js +21 -13
  309. package/src/plugins/menu/actions.js +89 -17
  310. package/src/plugins/menu/seo.js +32 -6
  311. package/src/plugins/menu/shell-style.js +26 -7
  312. package/src/plugins/menu/shell.js +12 -7
  313. package/src/plugins/menu/variants.js +6 -10
  314. package/src/plugins/menu/visitor.js +44 -34
  315. package/src/plugins/menu.js +3 -2
  316. package/src/plugins/page-view.js +2 -2
  317. package/src/plugins/page.js +257 -98
  318. package/src/plugins/sortable.js +7 -0
  319. package/src/plugins/upgrade.js +2 -1
  320. package/src/plugins/visitor-drafts/commit.js +303 -0
  321. package/src/plugins/visitor-drafts/idb.js +221 -0
  322. package/src/plugins/visitor-drafts/intro-style.js +1 -1
  323. package/src/plugins/visitor-drafts/intro.js +8 -25
  324. package/src/plugins/visitor-drafts/prompt.js +30 -4
  325. package/src/plugins/visitor-drafts/session.js +345 -0
  326. package/src/plugins/visitor-drafts/store.js +351 -32
  327. package/src/plugins/visitor-drafts.js +158 -27
  328. package/src/server/checkpoint/index.js +8 -8
  329. package/src/server/rooms/index.js +2 -2
  330. package/src/server/rooms/live-data-rooms.js +35 -35
  331. package/src/server/rooms/live-page-history.js +2 -2
  332. package/src/server/rooms/page-admission.js +34 -6
  333. package/src/server/rooms/page-conversion.js +1 -1
  334. package/src/server/rooms/page-lineage.js +4 -4
  335. package/src/server/rooms/page-rebase.js +4 -4
  336. package/src/server/rooms/page-room-conformance.js +14 -14
  337. package/src/server/rooms/page-state.js +2 -2
  338. package/src/server/rooms/page-write-admission.js +7 -7
  339. package/src/server/rooms/page-write-guard.js +3 -3
  340. package/src/sync/data-guard-notice.js +265 -0
  341. package/src/sync/live-sync.js +54 -11
  342. package/src/sync/page-relay.js +4 -4
  343. package/src/sync/presence-store.js +1 -1
  344. package/src/sync/presence-target.js +3 -3
  345. package/src/sync/presence.js +1 -1
  346. package/src/sync/stream.js +54 -13
  347. package/src/sync/wire-profile.js +8 -3
  348. package/src/ui/bevel.js +1 -1
  349. package/src/ui/index.js +9 -10
  350. package/src/ui/skins/cms.js +1 -1
  351. package/src/ui/skins/quickcrop.js +1 -1
  352. package/src/ui/skins/richclay.js +2 -2
  353. package/src/ui/toast-lazy.js +12 -3
  354. package/src/ui/toast.js +9 -3
  355. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  356. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  357. package/src/vendor/hypercms.vendor.js +16 -16
  358. package/src/vendor/richclay.vendor.js +48 -20
  359. package/src/vendor/sap-core.vendor.js +1 -1
  360. package/tool-generation/context.md +44 -0
  361. package/tool-generation/system.md +23 -0
  362. package/dist/m/c-24PMUJIV.js +0 -2
  363. package/dist/m/c-2AYVYEI4.js +0 -1
  364. package/dist/m/c-2AZDZWCK.js +0 -1
  365. package/dist/m/c-2L6O5PDS.js +0 -6
  366. package/dist/m/c-2RYOGUQM.js +0 -2
  367. package/dist/m/c-2ZVOEU4M.js +0 -1
  368. package/dist/m/c-37NAONZA.js +0 -1
  369. package/dist/m/c-3GJ4SDE2.js +0 -1
  370. package/dist/m/c-3O7OBDCM.js +0 -1
  371. package/dist/m/c-5OJ7YF2N.js +0 -1
  372. package/dist/m/c-5WMXEKFH.js +0 -135
  373. package/dist/m/c-6RDOKLN7.js +0 -1
  374. package/dist/m/c-A4IP6MBC.js +0 -1
  375. package/dist/m/c-A6L3FMUS.js +0 -1
  376. package/dist/m/c-ATKAOH23.js +0 -1
  377. package/dist/m/c-B6O7AJYG.js +0 -32
  378. package/dist/m/c-BCGWJYXW.js +0 -1
  379. package/dist/m/c-BGKAT2FL.js +0 -7
  380. package/dist/m/c-BYTKL6UX.js +0 -1
  381. package/dist/m/c-DHBRA6DL.js +0 -1
  382. package/dist/m/c-DTMZ3ZHI.js +0 -1
  383. package/dist/m/c-E3ZXARWA.js +0 -1
  384. package/dist/m/c-EK63G3CG.js +0 -1
  385. package/dist/m/c-ELYIIYRS.js +0 -1
  386. package/dist/m/c-EXKHPOVC.js +0 -3
  387. package/dist/m/c-FCSZ3DC5.js +0 -20
  388. package/dist/m/c-FTWGYI5E.js +0 -1
  389. package/dist/m/c-G4PWUDD5.js +0 -1
  390. package/dist/m/c-GBKB5WDL.js +0 -2
  391. package/dist/m/c-GBTVCJXC.js +0 -1
  392. package/dist/m/c-GC2HICFJ.js +0 -1
  393. package/dist/m/c-GRWMD6O5.js +0 -1
  394. package/dist/m/c-HCNWJE4A.js +0 -4
  395. package/dist/m/c-HG2C77BM.js +0 -4
  396. package/dist/m/c-JGHEACGK.js +0 -1
  397. package/dist/m/c-JGLX6TBK.js +0 -1
  398. package/dist/m/c-JNRKHYWO.js +0 -1
  399. package/dist/m/c-KBS3UV6B.js +0 -1
  400. package/dist/m/c-KQ7E3PYY.js +0 -1
  401. package/dist/m/c-L3F5ZLIV.js +0 -1
  402. package/dist/m/c-L4KMT7FO.js +0 -6
  403. package/dist/m/c-L5VFATT6.js +0 -1
  404. package/dist/m/c-LCYPMULG.js +0 -1
  405. package/dist/m/c-LIGQYZ7M.js +0 -6
  406. package/dist/m/c-LKIWEPAV.js +0 -1
  407. package/dist/m/c-MPSKIU2I.js +0 -1
  408. package/dist/m/c-N7JPUPGC.js +0 -1
  409. package/dist/m/c-NK33MMYF.js +0 -1
  410. package/dist/m/c-NQLWOHLH.js +0 -10
  411. package/dist/m/c-NTHUTABN.js +0 -1
  412. package/dist/m/c-NWX3MFT4.js +0 -1
  413. package/dist/m/c-OKXI52N7.js +0 -1
  414. package/dist/m/c-OPONJ2UK.js +0 -128
  415. package/dist/m/c-OWQWOGYI.js +0 -1
  416. package/dist/m/c-P6Q2ZTFQ.js +0 -1
  417. package/dist/m/c-QEN6HZCA.js +0 -1
  418. package/dist/m/c-QIXT3Q7C.js +0 -28
  419. package/dist/m/c-QM5LXRDT.js +0 -9
  420. package/dist/m/c-QYYUVLVG.js +0 -1
  421. package/dist/m/c-RQRMPZXD.js +0 -1
  422. package/dist/m/c-SBCBXKOZ.js +0 -26
  423. package/dist/m/c-SKF5HL4I.js +0 -4
  424. package/dist/m/c-SRPC3VBE.js +0 -1
  425. package/dist/m/c-TANBH7FI.js +0 -1
  426. package/dist/m/c-THGQQDYY.js +0 -1
  427. package/dist/m/c-UD5JZTDL.js +0 -1
  428. package/dist/m/c-UDGEFWNU.js +0 -1
  429. package/dist/m/c-UGNV5MFB.js +0 -1
  430. package/dist/m/c-VIRFF5HG.js +0 -8
  431. package/dist/m/c-VJYEVRYK.js +0 -1
  432. package/dist/m/c-VQCEOGKH.js +0 -13
  433. package/dist/m/c-VYJ7O4RI.js +0 -34
  434. package/dist/m/c-W7IPVCAA.js +0 -1
  435. package/dist/m/c-WBGWGOM7.js +0 -1
  436. package/dist/m/c-WJ5DRBHT.js +0 -72
  437. package/dist/m/c-X5WC4PX5.js +0 -1
  438. package/dist/m/c-XCFQEWW6.js +0 -3
  439. package/dist/m/c-XGH4GS2T.js +0 -1
  440. package/dist/m/c-XLQ3BDMM.js +0 -1
  441. package/dist/m/c-ZQXNHZZT.js +0 -72
  442. package/dist/m/core/conflict-notice-42H4ASLA.js +0 -1
  443. package/dist/m/core/draft-mode-WSQAT67S.js +0 -1
  444. package/dist/m/core/save-ESPSQPLV.js +0 -1
  445. package/dist/m/core/save-core-6SQHOR6W.js +0 -1
  446. package/dist/m/loader-FBDE2UAB.js +0 -1
  447. package/dist/m/manifest-1D2F5CA5.js +0 -1
  448. package/dist/m/manifest-edit-F7CC5CFD.js +0 -1
  449. package/dist/m/page/editor-OSKOW6RS.js +0 -304
  450. package/dist/m/plugins/ai-edit-VV4QTINH.js +0 -6
  451. package/dist/m/plugins/indicator-WVPGMOZ7.js +0 -1
  452. package/dist/m/plugins/menu-Q4MFFQ6E.js +0 -1
  453. package/dist/m/plugins/page-OAWUJQEL.js +0 -2
  454. package/dist/m/plugins/page-view-DZ26ZDDQ.js +0 -1
  455. package/dist/m/plugins/richclay-PGOOJ4OK.js +0 -517
  456. package/dist/m/plugins/sortable-266HY6TR.js +0 -1
  457. package/dist/m/plugins/upgrade-BNZVFHMZ.js +0 -1
  458. package/dist/m/plugins/visitor-drafts-2AUYKAIJ.js +0 -20
  459. package/dist/m/plugins/wire-W2EV36KZ.js +0 -1
  460. package/dist/m/sync/live-sync-XGUPHI3I.js +0 -1
  461. package/dist/m/ui/index-VGYAKAKI.js +0 -1
  462. package/dist/page/themes/light-1.css +0 -431
  463. package/src/page/themes/content.css +0 -70
  464. package/src/page/themes/light-1.css +0 -431
  465. package/src/page/themes/light-1.source.css +0 -270
  466. package/src/page/themes/light-2.css +0 -211
  467. package/src/page/themes/light-3.css +0 -130
  468. package/src/page/themes/light-4.css +0 -299
  469. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  470. /package/dist/m/plugins/{quickcrop-WWF5I422.js.LEGAL.txt → quickcrop-X5MRIBAP.js.LEGAL.txt} +0 -0
  471. /package/dist/m/plugins/{richclay-PGOOJ4OK.js.LEGAL.txt → richclay-Y2NV44NA.js.LEGAL.txt} +0 -0
@@ -85,6 +85,20 @@ export function setSaveHeader(name, value) {
85
85
  else extraSaveHeaders.set(name, String(value));
86
86
  }
87
87
 
88
+ // Headers that depend on the bytes of one save. Each source gets the exact string being
89
+ // sent and returns `{ name: value }` or null. A source that throws is skipped: a save
90
+ // must not fail because a header could not be computed.
91
+ const saveHeaderSources = new Set();
92
+
93
+ /**
94
+ * @param {(content: string) => (Object<string, string>|null)} source
95
+ * @returns {() => void} removes the source
96
+ */
97
+ export function addSaveHeaderSource(source) {
98
+ saveHeaderSources.add(source);
99
+ return () => saveHeaderSources.delete(source);
100
+ }
101
+
88
102
  /**
89
103
  * Send the next save with `keepalive`, so a request whose page is going away (a tab
90
104
  * closing, a navigation) is still finished by the transport.
@@ -229,7 +243,10 @@ function successResult(data) {
229
243
  msg: data.msg ?? 'Saved',
230
244
  msgType: data.msgType || 'success',
231
245
  code: data.code ?? null,
232
- etag: data.etag ?? null
246
+ etag: data.etag ?? null,
247
+ // A multi-page host's answer about shared values: which declared keys it stored,
248
+ // which it refused, and which values this tab is behind on. Absent on every other save.
249
+ ...(data.siteData ? { siteData: data.siteData } : {})
233
250
  };
234
251
  }
235
252
 
@@ -359,7 +376,7 @@ function errorResult(err) {
359
376
  // An attempt of this save timed out, so it may have landed whatever the
360
377
  // final answer says.
361
378
  hadTimeout: timedOut || err.hadTimeout === true,
362
- // The typed refusal behind `code`, kept for the caller: `page-capacity` carries
379
+ // The typed refusal behind `code`, kept for the caller: `blockeditor-capacity` carries
363
380
  // which limit was hit, the conversion codes carry which condition failed.
364
381
  reason: err.reason ?? null,
365
382
  // Only meaningful while a save can still retry; a caller sees how long the
@@ -428,7 +445,7 @@ function invalidHeaderResult() {
428
445
  }
429
446
 
430
447
  // How many extra sends one logical save may make while the host is still
431
- // converting the page. `page-converting` is a pre-commit refusal, so the same
448
+ // converting the page. `blockeditor-converting` is a pre-commit refusal, so the same
432
449
  // body is worth offering again; anything else is surfaced on the first answer.
433
450
  const PAGE_CONVERTING_MAX_RETRIES = 3;
434
451
  const HTTP_DATE = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2} \d{2}:\d{2}:\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \d|\d{2}) \d{2}:\d{2}:\d{2} \d{4})$/;
@@ -452,7 +469,7 @@ function parseRetryAfter(value) {
452
469
  return Math.max(0, Math.min(3600, Math.round((when - Date.now()) / 1000)));
453
470
  }
454
471
 
455
- // The host grammar for `Page-Conversion`, from `server-lib/page-conversion.js`:
472
+ // The host grammar for `Blockeditor-Conversion`, from `server-lib/page-conversion.js`:
456
473
  // `file` takes a 64-lowercase-hex baseline and no epoch; `room` takes the same
457
474
  // baseline plus an epoch of [A-Za-z0-9-]{1,64}. Validating here means a caller
458
475
  // with bad metadata gets a typed answer instead of a wasted round trip.
@@ -518,10 +535,11 @@ function formatConversionHeader(conversion) {
518
535
  * @param {string} saveId - this attempt's §6 receipt id
519
536
  * @param {?string} etag - the stamp to send as If-Match, or null
520
537
  * @param {boolean} keepalive - whether to ask the transport to see the request through
521
- * @param {?string} conversionHeader - the `Page-Conversion` value for this logical save, or null
538
+ * @param {?string} conversionHeader - the `Blockeditor-Conversion` value for this logical save, or null
539
+ * @param {boolean} perSaveHeaders - false for a lane that sends bytes this tab did not edit (replace), which declares nothing about them
522
540
  * @returns {{url: string, options: Object}}
523
541
  */
524
- export function buildSaveRequest(content, userDriven, signal, saveId, etag, keepalive = false, conversionHeader = null) {
542
+ export function buildSaveRequest(content, userDriven, signal, saveId, etag, keepalive = false, conversionHeader = null, perSaveHeaders = true) {
525
543
  const token = saveToken();
526
544
  const path = token ? `${SAVE_PATH}/${token}` : SAVE_PATH;
527
545
  const options = {
@@ -552,6 +570,18 @@ export function buildSaveRequest(content, userDriven, signal, saveId, etag, keep
552
570
 
553
571
  for (const [name, value] of extraSaveHeaders) options.headers[name] = value;
554
572
 
573
+ if (perSaveHeaders) {
574
+ for (const source of saveHeaderSources) {
575
+ let extra = null;
576
+ try {
577
+ extra = source(content);
578
+ } catch (err) {
579
+ console.error('clayjs: a save header source failed:', err);
580
+ }
581
+ if (extra) for (const [name, value] of Object.entries(extra)) options.headers[name] = String(value);
582
+ }
583
+ }
584
+
555
585
  // Set after the process-global headers so the type describing THESE bytes is the one
556
586
  // that goes out, the same rule the conversion header below follows.
557
587
  if (typeof options.body !== 'string') options.headers['Content-Type'] = SAVE_BODY_TYPE;
@@ -559,7 +589,7 @@ export function buildSaveRequest(content, userDriven, signal, saveId, etag, keep
559
589
  // The conversion request belongs to ONE logical save, and goes on every attempt
560
590
  // of it. Set after the process-global headers so the capture-bound value is the
561
591
  // one that describes these bytes.
562
- if (conversionHeader) options.headers['Page-Conversion'] = conversionHeader;
592
+ if (conversionHeader) options.headers['Blockeditor-Conversion'] = conversionHeader;
563
593
 
564
594
  if (etag) options.headers['If-Match'] = etag;
565
595
 
@@ -638,14 +668,13 @@ function draftResponse(status, body) {
638
668
  function sendToDraftStore(content) {
639
669
  const send = draftTransport();
640
670
  if (!send) return Promise.resolve(draftResponse(503, { msg: "Saving in this browser is not ready yet", msgType: "error", code: "draft-not-ready" }));
641
- return Promise.resolve().then(() => {
642
- try {
643
- send(content);
644
- return draftResponse(200, { msg: "Saved in this browser", msgType: "success" });
645
- } catch (error) {
646
- return draftResponse(507, { msg: "Couldn't save in this browser. Storage is full or turned off.", msgType: "error", code: error.code || "draft-storage" });
647
- }
648
- });
671
+ // The transport's promise settles once the draft database holds the save.
672
+ return Promise.resolve()
673
+ .then(() => send(content))
674
+ .then(
675
+ () => draftResponse(200, { msg: "Saved in this browser", msgType: "success" }),
676
+ (error) => draftResponse(507, { msg: "Couldn't save in this browser. Storage is full or turned off.", msgType: "error", code: error?.code || "draft-storage" }),
677
+ );
649
678
  }
650
679
 
651
680
  /**
@@ -876,7 +905,7 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
876
905
  // is fast and conclusive, so it gets its own turn regardless.
877
906
  let reSent = false;
878
907
  let duplicateResolved = false;
879
- // `page-converting` is the one refusal that is worth offering the same bytes
908
+ // `blockeditor-converting` is the one refusal that is worth offering the same bytes
880
909
  // again: the host has written nothing and is telling us when it expects to be
881
910
  // ready. The capture these bytes came from is pinned here, so a capture made
882
911
  // while we wait — newer edits, a disk frame — cancels the retry instead of
@@ -916,7 +945,7 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
916
945
  continue;
917
946
  }
918
947
 
919
- if (err.code === 'page-converting' && convertingRetries < PAGE_CONVERTING_MAX_RETRIES) {
948
+ if (err.code === 'blockeditor-converting' && convertingRetries < PAGE_CONVERTING_MAX_RETRIES) {
920
949
  if (currentCaptureId() !== retryGuard) throw err;
921
950
  convertingRetries += 1;
922
951
  // Default a missing or unparseable header to one second, so a host that
@@ -939,14 +968,14 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
939
968
  // thing whether the bytes came from a bound capture or a raw caller. `captureId`
940
969
  // is null for a raw save: it belongs to no capture and can confirm none.
941
970
  //
942
- // `pageFormatFloor` is internal: one save may carry a floor this tab has not
971
+ // `blockeditorFormatFloor` is internal: one save may carry a floor this tab has not
943
972
  // noted yet, which is how the format gate's activation writes the marker. It
944
973
  // raises these bytes only, never what the tab knows the document at.
945
- function runSave(html, captureId, callback, conversion, capture = null, pageFormatFloor = 0, lane = null) {
974
+ function runSave(html, captureId, callback, conversion, capture = null, blockeditorFormatFloor = 0, lane = null) {
946
975
  // A capture taken before the floor rose cannot drop the marker by omitting it:
947
976
  // the bytes that go out carry the floor this tab last saw. At floor 0 this is
948
977
  // the string itself, unchanged.
949
- html = withPageFormatFloor(html, Math.max(knownPageFormatFloor(), pageFormatFloor));
978
+ html = withPageFormatFloor(html, Math.max(knownPageFormatFloor(), blockeditorFormatFloor));
950
979
  return new Promise((resolve) => {
951
980
  const attempts = [];
952
981
  // Read once, before the first request: the artifact belongs to the capture that
@@ -1021,7 +1050,7 @@ function runSave(html, captureId, callback, conversion, capture = null, pageForm
1021
1050
  // same measurement. Its own answer about the size wins over nothing: the bytes
1022
1051
  // are this attempt's, and the limit is still only what discovery advertised.
1023
1052
  if (result.code === 'save-too-large' && !result.size) result.size = size;
1024
- if (result.ok) notifySaveAccepted(html, captureId ?? null, artifact);
1053
+ if (result.ok) notifySaveAccepted(html, captureId ?? null, artifact, result.siteData ?? null);
1025
1054
  finished(result);
1026
1055
  });
1027
1056
  });
@@ -1059,8 +1088,8 @@ export function saveHtml(html, callback = () => {}) {
1059
1088
  * @param {Object} [options.conversion] - {source:'file', base} or {source:'room', base, epoch}
1060
1089
  * @returns {Promise<Object>}
1061
1090
  */
1062
- export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion, capture = null, pageFormatFloor, lane = null } = {}) {
1063
- return runSave(html, captureId, callback, conversion, capture, pageFormatFloor, lane);
1091
+ export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion, capture = null, blockeditorFormatFloor, lane = null } = {}) {
1092
+ return runSave(html, captureId, callback, conversion, capture, blockeditorFormatFloor, lane);
1064
1093
  }
1065
1094
 
1066
1095
  /**
@@ -0,0 +1,37 @@
1
+ import { saveFeedback } from "./save-feedback.js";
2
+ import { toastPersistent, dismissPersistent, preload } from "../ui/toast-lazy.js";
3
+
4
+ // A failed save is never silent. core/save.js fetches this file at the person's first
5
+ // input and hands it every failed save no listener claimed. A page with the indicator chip, clay-ui or its own
6
+ // notices (`clay.saveFeedback = "own"`) already reports failures, so nothing is added
7
+ // there. Everywhere else the toast stack shows what clay-ui would have shown. Core keeps
8
+ // calling in for every later save, so a failure the block editor claims stays its own,
9
+ // and the next save that lands takes the toast down. The toast stack is the block
10
+ // editor's too, so its dismissals reach these toasts.
11
+ const TOASTS = {
12
+ error: ["Couldn't save", "error"],
13
+ offline: ["Offline, will retry", "warning"],
14
+ };
15
+
16
+ let shown = false;
17
+
18
+ const unheard = () => !(saveFeedback.chip || saveFeedback.toasts || window.clay?.saveFeedback === "own");
19
+
20
+ // core/save.js calls this at the person's first input, so the toast can still show offline.
21
+ export function warm() {
22
+ if (unheard()) preload();
23
+ }
24
+
25
+ export function reportFailure(state) {
26
+ if (!TOASTS[state] || !unheard()) return;
27
+ shown = true;
28
+ // One failure notice at a time: a connection that comes and goes swaps them.
29
+ for (const [other, [message]] of Object.entries(TOASTS)) if (other !== state) dismissPersistent(message);
30
+ toastPersistent(...TOASTS[state]);
31
+ }
32
+
33
+ export function clearFailures() {
34
+ if (!shown) return;
35
+ shown = false;
36
+ for (const [message] of Object.values(TOASTS)) dismissPersistent(message);
37
+ }
@@ -4,3 +4,9 @@
4
4
  // read them only when an event arrives, so load order does not matter. savedToast is
5
5
  // the page's `clay.saveToast` switch: Saved goes to a toast instead of the chip.
6
6
  export const saveFeedback = { chip: false, toasts: false, savedToast: false };
7
+
8
+ // Set on a failed save's event detail by a listener that put its own notice up (the block
9
+ // editor's retry and blocked notices), so the chip and the fallback toast stay out of it.
10
+ // core/save.js writes the same key without importing this file, which the edit boot does
11
+ // not otherwise fetch.
12
+ export const FAILURE_CLAIMED = Symbol.for("clay.saveFailureClaimed");
@@ -46,7 +46,7 @@ export async function replace(html, { etag } = {}) {
46
46
  const controller = new AbortController();
47
47
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
48
48
  try {
49
- const { url, options } = buildSaveRequest(html, true, controller.signal, mintSaveId(), stamp);
49
+ const { url, options } = buildSaveRequest(html, true, controller.signal, mintSaveId(), stamp, false, null, false);
50
50
  const res = await fetch(url, options);
51
51
  const text = await res.text();
52
52
  let data = {};
@@ -32,13 +32,13 @@ function adopt(meta) {
32
32
  return limit;
33
33
  }
34
34
 
35
- // A Document's live state rides in one <script merge="page"> tag that Hyperclay
35
+ // A Document's live state rides in one <script merge="blockeditor"> tag that Hyperclay
36
36
  // stores apart from the file, and its limit (`maxBytesMeasures: 'file'`) is judged on
37
37
  // the body without that tag, the way the host's splitPage() cuts it: the whole line
38
38
  // when the tag stands alone on it, otherwise the tag itself. Where the host would not
39
39
  // split (no tag, two tags), the cut here can only make the count smaller, so the
40
40
  // host's own 413 stays the answer and nothing is refused that the host would store.
41
- const PAGE_TAG = /<script\b[^>]*\bmerge=(["'])page\1[^>]*>[\s\S]*?<\/script\s*>/i;
41
+ const PAGE_TAG = /<script\b[^>]*\bmerge=(["'])blockeditor\1[^>]*>[\s\S]*?<\/script\s*>/i;
42
42
 
43
43
  export function storedFile(html, measure = measures) {
44
44
  if (measure !== 'file' || typeof html !== 'string') return html;
package/src/core/save.js CHANGED
@@ -111,6 +111,43 @@ function setSaveState(state, msg = '', msgType = '', extra = null) {
111
111
  detail: { msg, msgType, timestamp: Date.now(), ...(extra || {}) }
112
112
  });
113
113
  document.dispatchEvent(event);
114
+ reportIfUnheard(state, event.detail);
115
+ }
116
+
117
+ // A failed save is never silent. Every listener has seen the event by now; a listener
118
+ // that put its own notice up (the block editor's retry and blocked notices) claimed it.
119
+ // The rest goes to save-failure-notice.js, which shows a toast when nothing else on the
120
+ // page reports failures and takes it down when a later save lands. The claim key is
121
+ // save-feedback.js's FAILURE_CLAIMED.
122
+ const FAILURE_CLAIMED = Symbol.for('clay.saveFailureClaimed');
123
+ export const claimFailure = (event) => { if (event?.detail) event.detail[FAILURE_CLAIMED] = true; };
124
+
125
+ let failureNotice = null;
126
+ const loadFailureNotice = () => {
127
+ failureNotice ??= import('./save-failure-notice.js').catch((error) => {
128
+ failureNotice = null;
129
+ throw error;
130
+ });
131
+ return failureNotice;
132
+ };
133
+
134
+ // A save fails offline most of all, when nothing more can be downloaded. So the notice and
135
+ // the toast stack it needs arrive at the person's first input, as the indicator's chip
136
+ // does, never at boot.
137
+ const warmFailureNotice = () => { loadFailureNotice().then((m) => m.warm(), () => {}); };
138
+ for (const type of ['beforeinput', 'keydown', 'pointerdown']) {
139
+ document.addEventListener(type, warmFailureNotice, { once: true, capture: true });
140
+ }
141
+
142
+ function reportIfUnheard(state, detail) {
143
+ if (state === 'saved') {
144
+ failureNotice?.then((m) => m.clearFailures(), () => {});
145
+ return;
146
+ }
147
+ if ((state !== 'error' && state !== 'offline') || detail?.[FAILURE_CLAIMED]) return;
148
+ loadFailureNotice().then((m) => m.reportFailure(state), (error) => {
149
+ console.error('clayjs: a save failed and the notice for it could not be downloaded', error);
150
+ });
114
151
  }
115
152
 
116
153
  /**
@@ -931,7 +968,7 @@ function prepareInvalidation(ctx, epoch, morph) {
931
968
  if (!savesAdmitted(ctx.permit)) return 'session';
932
969
  if (refusesSave()) return 'refused';
933
970
  if (baselineRevision !== epoch.baselines) return 'newer save';
934
- if (Math.max(knownPageFormatFloor(), ctx.pageFormatFloor) > epoch.floor) return 'floor';
971
+ if (Math.max(knownPageFormatFloor(), ctx.blockeditorFormatFloor) > epoch.floor) return 'floor';
935
972
  return null;
936
973
  }
937
974
 
@@ -969,7 +1006,7 @@ async function settleCaptured({
969
1006
  captureId,
970
1007
  gateToken,
971
1008
  costMs,
972
- pageFormatFloor = 0,
1009
+ blockeditorFormatFloor = 0,
973
1010
  quietConflict = false,
974
1011
  laneHeld = false,
975
1012
  lane = null,
@@ -1069,7 +1106,7 @@ async function settleCaptured({
1069
1106
  // lane busy and waits that request out, which is what its 'clean' answer describes.
1070
1107
  // A prepared save holds the lane itself, so its caller does the draining instead.
1071
1108
  if (!laneHeld) drainPendingSave();
1072
- }, { capture, pageFormatFloor, lane });
1109
+ }, { capture, blockeditorFormatFloor, lane });
1073
1110
  });
1074
1111
  }
1075
1112
 
@@ -1092,7 +1129,7 @@ async function runSlicedSave(ctx, lane) {
1092
1129
  const epoch = {
1093
1130
  generation: saveGenerationAt,
1094
1131
  baselines: baselineRevision,
1095
- floor: Math.max(knownPageFormatFloor(), ctx.pageFormatFloor),
1132
+ floor: Math.max(knownPageFormatFloor(), ctx.blockeditorFormatFloor),
1096
1133
  };
1097
1134
  const job = prepDriver.beginSaveCapture({
1098
1135
  requestRefresh: true,
@@ -1135,7 +1172,7 @@ async function runSlicedSave(ctx, lane) {
1135
1172
  captureId: captured.captureId,
1136
1173
  gateToken,
1137
1174
  costMs: prepared.activeMs,
1138
- pageFormatFloor: ctx.pageFormatFloor,
1175
+ blockeditorFormatFloor: ctx.blockeditorFormatFloor,
1139
1176
  quietConflict: ctx.quietConflict,
1140
1177
  laneHeld: true,
1141
1178
  lane,
@@ -1225,6 +1262,32 @@ function attemptSave(options, callback = () => {}) {
1225
1262
  return adoptionQueue.then(() => attemptSaveNow(options, callback));
1226
1263
  }
1227
1264
 
1265
+ // A save waiting on something it needs before its capture: the source map's install, or
1266
+ // the preparer loaded on demand. The chip moves only on save events, so without this it
1267
+ // kept its last word, usually "Saved", for the whole wait, which on a host whose every
1268
+ // connection was held open lasted until a tab closed. Past the usual 500 ms it says
1269
+ // 'saving'; if the attempt then sends nothing and the page has nothing left to save, it
1270
+ // goes back to 'saved' through `clay:save-status`, never `clay:save-saved`, which would
1271
+ // run every [onaftersave] handler as though a save had happened.
1272
+ function afterWait(wait, attempt) {
1273
+ let shown = false;
1274
+ const timer = setTimeout(() => {
1275
+ shown = true;
1276
+ setSaveState('saving');
1277
+ }, 500);
1278
+ return wait.then(() => {
1279
+ clearTimeout(timer);
1280
+ return attempt();
1281
+ }).then((result) => {
1282
+ if (shown && !isSaveInProgress() && !pageMaybeDirty()
1283
+ && document.documentElement.getAttribute('savestatus') === 'saving') {
1284
+ document.documentElement.setAttribute('savestatus', 'saved');
1285
+ document.dispatchEvent(new CustomEvent('clay:save-status', { detail: { state: 'saved' } }));
1286
+ }
1287
+ return result;
1288
+ });
1289
+ }
1290
+
1228
1291
  function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
1229
1292
  const explicit = kind === 'explicit';
1230
1293
 
@@ -1321,10 +1384,10 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
1321
1384
 
1322
1385
  // The source map asks a save to wait here: after the gates, before the capture.
1323
1386
  const prepare = window.clay?.source?.ensureForSave?.();
1324
- if (prepare) return prepare.then(() => resolve(attemptSaveNow({ kind, permit }, callback)));
1387
+ if (prepare) return resolve(afterWait(prepare, () => attemptSaveNow({ kind, permit }, callback)));
1325
1388
 
1326
1389
  if (preparesSliced() && !prepDriver) {
1327
- loadPreparer().then(() => resolve(attemptSaveNow({ kind, permit }, callback)));
1390
+ resolve(afterWait(loadPreparer(), () => attemptSaveNow({ kind, permit }, callback)));
1328
1391
  return;
1329
1392
  }
1330
1393
 
@@ -1348,7 +1411,7 @@ function attemptSaveNow({ kind, permit = null }, callback = () => {}) {
1348
1411
  explicit,
1349
1412
  forced: false,
1350
1413
  permit,
1351
- pageFormatFloor: 0,
1414
+ blockeditorFormatFloor: 0,
1352
1415
  quietConflict: false,
1353
1416
  settleSoonQueued,
1354
1417
  callback,
@@ -1433,8 +1496,8 @@ function savePagePermitted(permit, callback = () => {}) {
1433
1496
  * @param {Function} callback - Optional callback for custom handling
1434
1497
  * @returns {Promise<{msg: string, msgType: string}>}
1435
1498
  */
1436
- export function savePageForce(callback = () => {}, { quietConflict = false, pageFormatFloor = 0, permit = null } = {}) {
1437
- if (capabilityPending) return capabilityKnown.then(() => savePageForce(callback, { quietConflict, pageFormatFloor, permit }));
1499
+ export function savePageForce(callback = () => {}, { quietConflict = false, blockeditorFormatFloor = 0, permit = null } = {}) {
1500
+ if (capabilityPending) return capabilityKnown.then(() => savePageForce(callback, { quietConflict, blockeditorFormatFloor, permit }));
1438
1501
  return new Promise((resolve) => {
1439
1502
  if (!isEditMode && !window.clay?.testMode) {
1440
1503
  const skipped = skipped_('Not in edit mode');
@@ -1488,7 +1551,7 @@ export function savePageForce(callback = () => {}, { quietConflict = false, page
1488
1551
  }
1489
1552
 
1490
1553
  const prepare = window.clay?.source?.ensureForSave?.();
1491
- if (prepare) return prepare.then(() => resolve(savePageForce(callback, { quietConflict, pageFormatFloor, permit })));
1554
+ if (prepare) return resolve(afterWait(prepare, () => savePageForce(callback, { quietConflict, blockeditorFormatFloor, permit })));
1492
1555
 
1493
1556
  const wasOffline = !navigator.onLine;
1494
1557
  if (wasOffline) {
@@ -1498,7 +1561,7 @@ export function savePageForce(callback = () => {}, { quietConflict = false, page
1498
1561
  // A forced save prepares like an ordinary one when the document is this big; it only
1499
1562
  // skips the dirty question, which `forced` says here.
1500
1563
  if (preparesSliced() && !prepDriver) {
1501
- loadPreparer().then(() => resolve(savePageForce(callback, { quietConflict, pageFormatFloor, permit })));
1564
+ resolve(afterWait(loadPreparer(), () => savePageForce(callback, { quietConflict, blockeditorFormatFloor, permit })));
1502
1565
  return;
1503
1566
  }
1504
1567
 
@@ -1512,7 +1575,7 @@ export function savePageForce(callback = () => {}, { quietConflict = false, page
1512
1575
  explicit: true,
1513
1576
  forced: true,
1514
1577
  permit,
1515
- pageFormatFloor,
1578
+ blockeditorFormatFloor,
1516
1579
  quietConflict,
1517
1580
  settleSoonQueued,
1518
1581
  callback,
@@ -1546,7 +1609,7 @@ export function savePageForce(callback = () => {}, { quietConflict = false, page
1546
1609
  captureId,
1547
1610
  gateToken,
1548
1611
  costMs: 0,
1549
- pageFormatFloor,
1612
+ blockeditorFormatFloor,
1550
1613
  quietConflict,
1551
1614
  capture,
1552
1615
  noteRecovery: () => noteRecoveryCapture({ captureId, html: forSave }),
@@ -1627,7 +1690,7 @@ export async function flushSave({ keepalive = false, timeoutMs = 15000, permit =
1627
1690
  const run = { over: false };
1628
1691
  let timer = null;
1629
1692
  const timeout = new Promise((_, reject) => {
1630
- timer = setTimeout(() => reject(flushError('Save timed out.', 'failed')), Math.max(0, timeoutMs));
1693
+ timer = setTimeout(() => reject(Object.assign(flushError('Save timed out.', 'failed'), { timedOut: true })), Math.max(0, timeoutMs));
1631
1694
  });
1632
1695
  try {
1633
1696
  return await Promise.race([flushBytes(keepalive, deadline, held, run), timeout]);
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Shared site values edited on this page.
3
+ *
4
+ * On a multi-page host a page's `site` rule names values every page of the site shows:
5
+ * a nav, a footer. The host overwrites them with the site store's on every save, which
6
+ * keeps a stale tab from putting old values back and would also undo an edit made right
7
+ * here. So each save says which top-level `site` keys the person changed in this tab
8
+ * (`Site-Data: keys=nav,brand`), and the host writes exactly those to the store before
9
+ * it overwrites the rest.
10
+ *
11
+ * Both sides of the comparison are read from captured bytes, never from the live DOM:
12
+ * the baseline from the save pipeline's own record of what the file held at boot (which
13
+ * leaves out anything typed while the editor was still loading), moved on by this tab's
14
+ * accepted saves and by any other change to that record (a clean live-sync frame), and
15
+ * each save from the string being sent. Whatever the save pipeline normalizes is then normalized on both sides,
16
+ * so a value nobody touched cannot read as edited. That matters most in a stale tab,
17
+ * where a wrongly declared key would write old content over newer.
18
+ */
19
+ import { addSaveHeaderSource } from "./save-core.js";
20
+ import { onSaveAccepted } from "./save-accepted.js";
21
+ import { getLastSavedContents, savePageThrottled } from "./save.js";
22
+ import Mutation from "../lib/mutation.js";
23
+
24
+ export const SITE_DATA_HEADER = "Site-Data";
25
+
26
+ const RULES_TAG = 'script[data-rules-name~="api"]';
27
+ // A top-level key: it opens the object or follows a comma, so `.site:first-child` in a
28
+ // selector string is not a declaration.
29
+ const DECLARES_SITE = /(?:^|[{,])\s*["']?site["']?\s*:/;
30
+ const EMPTY = { html: null, now: null, keys: [] };
31
+
32
+ let engine = null;
33
+ let baseline = null;
34
+ let last = EMPTY;
35
+ // The save pipeline's record the baseline was last taken from. Whenever it changes (this
36
+ // tab's accepted save, a clean live-sync frame), every key the host did not refuse takes
37
+ // the record's value: for this tab's own save that is the value it sent, so an edit typed
38
+ // while the save was in flight still differs, and for a frame it is what the file now holds.
39
+ let knownSaved = null;
40
+ const refusedKeys = new Set();
41
+ // Values the host refreshed, with the value this tab had sent. The accepted hook runs before
42
+ // the save pipeline installs that save's record, so the record then still holds the sent
43
+ // value: the refreshed one stands. A record holding anything else is newer, and wins.
44
+ const refreshed = new Map();
45
+
46
+ // False while there is no record yet: no baseline, so no header.
47
+ function followSavedRecord() {
48
+ const record = getLastSavedContents();
49
+ if (typeof record !== "string" || !record) return false;
50
+ if (record === knownSaved) return true;
51
+ const values = valuesOf(parse(record));
52
+ if (!baseline) baseline = values;
53
+ else {
54
+ for (const [key, value] of Object.entries(values)) {
55
+ if (refusedKeys.has(key)) continue;
56
+ const host = refreshed.get(key);
57
+ baseline[key] = host && host.sent === value ? host.value : value;
58
+ }
59
+ }
60
+ refreshed.clear();
61
+ knownSaved = record;
62
+ last = EMPTY;
63
+ return true;
64
+ }
65
+
66
+ function siteRule(root) {
67
+ const rule = engine.engine.findRulesIn(root, "api")?.rules?.site;
68
+ return rule !== null && typeof rule === "object" && !Array.isArray(rule) ? rule : null;
69
+ }
70
+
71
+ function valuesOf(root) {
72
+ const rule = siteRule(root);
73
+ if (!rule) return {};
74
+ const site = engine.extractData(root, { site: rule }).site ?? {};
75
+ return Object.fromEntries(Object.entries(site).map(([key, value]) => [key, JSON.stringify(value)]));
76
+ }
77
+
78
+ const parse = (html) => new DOMParser().parseFromString(html, "text/html").documentElement;
79
+
80
+ function read(html) {
81
+ if (last.html !== html) {
82
+ const now = valuesOf(parse(html));
83
+ last = { html, now, keys: Object.keys(now).filter((key) => now[key] !== baseline[key]) };
84
+ }
85
+ return last;
86
+ }
87
+
88
+ // No header until the baseline exists: without one this tab cannot tell an edit from a
89
+ // stale value, and a host that sees no header treats every shared value as stale.
90
+ function siteHeader(html) {
91
+ if (!engine || typeof html !== "string" || !followSavedRecord()) return null;
92
+ return { [SITE_DATA_HEADER]: `keys=${read(html).keys.map(encodeURIComponent).join(",")}` };
93
+ }
94
+
95
+ // Values the host stored that this tab was behind on. Taken only for a key the person has
96
+ // not touched since the save was captured, and never for a key the host refused: that
97
+ // one still shows the edit that was not stored, and the page says so.
98
+ function takeCurrent(current, sent, refused) {
99
+ const root = document.documentElement;
100
+ const rule = siteRule(root);
101
+ const live = valuesOf(root);
102
+ const applied = [];
103
+ for (const [key, value] of Object.entries(current)) {
104
+ if (refused.has(key) || !rule || !Object.hasOwn(rule, key) || live[key] !== sent[key]) continue;
105
+ // Outside undo history and the edit observers, the way a sync frame lands: an undo
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]));
108
+ Mutation.pause();
109
+ try {
110
+ engine.applyData(root, { site: { [key]: value } }, { site: { [key]: rule[key] } });
111
+ baseline[key] = valuesOf(root)[key];
112
+ refreshed.set(key, { sent: sent[key], value: baseline[key] });
113
+ applied.push(key);
114
+ } catch (error) {
115
+ console.error(`clayjs: the shared value "${key}" could not be refreshed on this page`, error);
116
+ } finally {
117
+ Mutation.resume();
118
+ }
119
+ // An editor that keeps its own undo history (RichClay) is told its field was rewritten,
120
+ // so its undo starts from the new value rather than stepping back to the old one.
121
+ for (const [field, before] of fields) {
122
+ if (field.isConnected && field.innerHTML !== before) field.dispatchEvent(new CustomEvent("clay:external-write"));
123
+ }
124
+ }
125
+ // The writes above ran with the edit observers paused, so the save pipeline still
126
+ // records the older value: typing that value back would then look like no change and
127
+ // never be saved. Ask for the same automatic save live sync asks for after it applies a
128
+ // frame, which stores what the page now shows and moves the record to it.
129
+ if (applied.length) savePageThrottled();
130
+ return applied;
131
+ }
132
+
133
+ function accept(html, answer) {
134
+ if (!engine || !followSavedRecord()) return;
135
+ refreshed.clear();
136
+ const sent = read(html).now;
137
+ // A `stale` key was edited on a value older than the store's: the host keeps the newer one
138
+ // and sends it as `current`, which this tab takes instead of declaring its edit again.
139
+ const refused = new Set((answer?.refused ?? []).filter((entry) => entry.code !== "stale").map((entry) => entry.key));
140
+ for (const key of Object.keys(sent)) {
141
+ if (refused.has(key)) refusedKeys.add(key);
142
+ else {
143
+ baseline[key] = sent[key];
144
+ refusedKeys.delete(key);
145
+ }
146
+ }
147
+ last = EMPTY;
148
+ if (!answer) return;
149
+ const applied = answer.current ? takeCurrent(answer.current, sent, refused) : [];
150
+ document.dispatchEvent(new CustomEvent("clay:site-data", {
151
+ detail: { written: answer.written ?? [], refused: answer.refused ?? [], applied },
152
+ }));
153
+ }
154
+
155
+ // Resolves once the engine has loaded, or at once on a page that declares no `site` key. The
156
+ // baseline itself is read from the save pipeline's record when it is first needed.
157
+ export const ready = DECLARES_SITE.test(document.querySelector(RULES_TAG)?.textContent ?? "")
158
+ ? (() => {
159
+ const loading = import("../vendor/hyper-html-api-data.vendor.js");
160
+ addSaveHeaderSource(siteHeader);
161
+ onSaveAccepted((html, accepted) => accept(html, accepted.siteData ?? null));
162
+ return loading.then((loaded) => {
163
+ engine = loaded;
164
+ }).catch((error) => console.error("clayjs: shared site edits are not tracked on this page", error));
165
+ })()
166
+ : Promise.resolve();
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { isEditMode, isDraftMode } from "./is-edit-mode.js";
19
- import { draftStoreFailed, flushDraft } from "./draft-mode.js";
19
+ import { draftStoreFailed, draftUnconfirmed, flushDraft } from "./draft-mode.js";
20
20
  import { captureForDirtyCheck } from "./snapshot.js";
21
21
  import { getLastSavedDirty, sessionParked, styleAdoptionPending } from "./save.js";
22
22
  import { logUnloadDiffSync, preloadIfEnabled } from "../lib/autosave-debug-gate.js";
@@ -57,7 +57,7 @@ export function hasUnsavedChanges() {
57
57
  // Work outside the DOM first: it needs no capture, and a capture that throws
58
58
  // must not hide it. The demo plugin does not save it either, so it warns there too.
59
59
  if (hasUnsavedState()) return true;
60
- if (isDraftMode) return draftStoreFailed();
60
+ if (isDraftMode) return draftStoreFailed() || draftUnconfirmed();
61
61
 
62
62
  // The demo plugin saves the page's bytes into this browser's own storage, so
63
63
  // leaving loses none of them.