@panphora/clayjs 1.17.0 → 1.18.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 (467) 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 +72474 -62090
  11. package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
  12. package/dist/m/c-2GRUM77Q.js +30 -0
  13. package/dist/m/c-2KIEGNC4.js +1 -0
  14. package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
  15. package/dist/m/c-2ZZBC45O.js +1 -0
  16. package/dist/m/c-372N6H3B.js +1 -0
  17. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  18. package/dist/m/c-3LDGIBMN.js +1 -0
  19. package/dist/m/c-3O7OBDCM.js +1 -0
  20. package/dist/m/c-4ASQPRQL.js +1 -0
  21. package/dist/m/c-4TXNLX4K.js +6 -0
  22. package/dist/m/c-4YU3BR4D.js +6 -0
  23. package/dist/m/c-5734MF2P.js +1 -0
  24. package/dist/m/c-57XDY6HC.js +1 -0
  25. package/dist/m/{c-KAU6E43D.js → c-5B4YBZXC.js} +1 -1
  26. package/dist/m/c-5ONTYIUI.js +1 -0
  27. package/dist/m/c-5OOVFCJM.js +1 -0
  28. package/dist/m/{c-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
  29. package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
  30. package/dist/m/{c-7OLLJPWP.js → c-6DB2OLRJ.js} +1 -1
  31. package/dist/m/c-6DNRCPNM.js +1 -0
  32. package/dist/m/c-6UGWFMQC.js +2 -0
  33. package/dist/m/c-7K4SCQB7.js +1 -0
  34. package/dist/m/c-7MG5RU63.js +1 -0
  35. package/dist/m/c-7PRJCA3E.js +1 -0
  36. package/dist/m/c-A6L3FMUS.js +1 -0
  37. package/dist/m/c-AMU3BPPR.js +4 -0
  38. package/dist/m/{c-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
  39. package/dist/m/c-AQYCG7QW.js +1 -0
  40. package/dist/m/c-ATKAOH23.js +1 -0
  41. package/dist/m/c-B5CS55WR.js +1 -0
  42. package/dist/m/c-B5UJQT7W.js +1 -0
  43. package/dist/m/c-B7J4K2AE.js +3 -0
  44. package/dist/m/c-BHZYSHW4.js +1 -0
  45. package/dist/m/{c-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
  46. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  47. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  48. package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
  49. package/dist/m/c-C6IPRG34.js +1 -0
  50. package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
  51. package/dist/m/{c-PS5ARVWR.js → c-CCQ25GFY.js} +1 -1
  52. package/dist/m/c-CJFDLD43.js +1 -0
  53. package/dist/m/c-COPTL5WB.js +1 -0
  54. package/dist/m/c-D3E4WJ4M.js +166 -0
  55. package/dist/m/c-D5IKEKXW.js +7 -0
  56. package/dist/m/c-DHBRA6DL.js +1 -0
  57. package/dist/m/c-DKLS74XB.js +72 -0
  58. package/dist/m/c-DVYGFTSK.js +3 -0
  59. package/dist/m/c-E3ZXARWA.js +1 -0
  60. package/dist/m/c-EK63G3CG.js +1 -0
  61. package/dist/m/c-ERQSNR6K.js +13 -0
  62. package/dist/m/c-EVYAISP3.js +1 -0
  63. package/dist/m/c-EXKHPOVC.js +3 -0
  64. package/dist/m/{c-QJ4OM3G6.js → c-FWKHM4E7.js} +1 -1
  65. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  66. package/dist/m/c-GNDL3TKB.js +1 -0
  67. package/dist/m/c-GRWMD6O5.js +1 -0
  68. package/dist/m/c-GRXHHFPO.js +1 -0
  69. package/dist/m/c-HG2C77BM.js +4 -0
  70. package/dist/m/c-HHGJBU4D.js +1 -0
  71. package/dist/m/c-HMV6P4RX.js +1 -0
  72. package/dist/m/c-HMYWIHFG.js +1 -0
  73. package/dist/m/c-HT2DKBV2.js +1 -0
  74. package/dist/m/c-I2SCCUEW.js +1 -0
  75. package/dist/m/{c-TJ6WOJL3.js → c-ICJYHABF.js} +2 -2
  76. package/dist/m/c-IMZCWYHR.js +7 -0
  77. package/dist/m/c-IVMD3FEM.js +1 -0
  78. package/dist/m/{c-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
  79. package/dist/m/c-JKZJIUIP.js +1 -0
  80. package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
  81. package/dist/m/c-K3ADYNLU.js +1 -0
  82. package/dist/m/c-KGHVFRR3.js +1 -0
  83. package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
  84. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  85. package/dist/m/{c-EZU56NQV.js → c-KP64LT2A.js} +2 -2
  86. package/dist/m/c-KQ7E3PYY.js +1 -0
  87. package/dist/m/c-L3F5ZLIV.js +1 -0
  88. package/dist/m/{c-3RUU4TPX.js → c-L42OLXWM.js} +3 -2
  89. package/dist/m/c-L5VFATT6.js +1 -0
  90. package/dist/m/c-L7JL5TL3.js +3 -0
  91. package/dist/m/c-LKSL4AUJ.js +1 -0
  92. package/dist/m/c-M6FNQCTQ.js +1 -0
  93. package/dist/m/c-MCOOHCSG.js +1 -0
  94. package/dist/m/c-MHI3GYKF.js +1 -0
  95. package/dist/m/c-MWXFLW4J.js +1 -0
  96. package/dist/m/{c-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
  97. package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
  98. package/dist/m/c-NWX3MFT4.js +1 -0
  99. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  100. package/dist/m/c-OKXI52N7.js +1 -0
  101. package/dist/m/{c-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
  102. package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
  103. package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
  104. package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
  105. package/dist/m/c-PLN76GF3.js +6 -0
  106. package/dist/m/c-PLP4JKFC.js +1 -0
  107. package/dist/m/c-Q26MME6N.js +1 -0
  108. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  109. package/dist/m/{c-FRLHSY7E.js → c-QIKT6SKM.js} +1 -1
  110. package/dist/m/c-QIXT3Q7C.js +28 -0
  111. package/dist/m/c-QO4HNGCQ.js +1 -0
  112. package/dist/m/{c-JU3NK25H.js → c-R27DWQKX.js} +1 -1
  113. package/dist/m/{c-RSMOODDU.js → c-R2XUODVY.js} +1 -1
  114. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  115. package/dist/m/c-RND3G2S2.js +1 -0
  116. package/dist/m/{c-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
  117. package/dist/m/c-SPZPGD3R.js +1 -0
  118. package/dist/m/c-SW4YEG2D.js +8 -0
  119. package/dist/m/c-T637T7SN.js +1 -0
  120. package/dist/m/c-TJAXGIRV.js +1 -0
  121. package/dist/m/{c-RYUXUUBX.js → c-TNEM3RNM.js} +1 -1
  122. package/dist/m/c-TTB5EK4S.js +32 -0
  123. package/dist/m/c-U7SHNWDH.js +1 -0
  124. package/dist/m/c-UDHIVLYH.js +20 -0
  125. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  126. package/dist/m/{c-QICP2YR5.js → c-UORXLSD3.js} +1 -1
  127. package/dist/m/c-UPD33ZPE.js +1 -0
  128. package/dist/m/c-URSMSFCB.js +1 -0
  129. package/dist/m/c-UY4QX42B.js +1 -0
  130. package/dist/m/c-V44KWI2L.js +4 -0
  131. package/dist/m/c-VILENZTD.js +1 -0
  132. package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
  133. package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
  134. package/dist/m/c-W4JCVLAB.js +1 -0
  135. package/dist/m/c-W6DQSHXI.js +135 -0
  136. package/dist/m/c-WBGWGOM7.js +1 -0
  137. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  138. package/dist/m/c-WGW764WX.js +1 -0
  139. package/dist/m/c-WMRJKQ2V.js +5 -0
  140. package/dist/m/c-WNTYKCDG.js +1 -0
  141. package/dist/m/c-WQWIRWO3.js +72 -0
  142. package/dist/m/c-X5LNHET5.js +1 -0
  143. package/dist/m/{c-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
  144. package/dist/m/c-XGH4GS2T.js +1 -0
  145. package/dist/m/c-Y6F2FORL.js +1 -0
  146. package/dist/m/c-YBKHUAK7.js +10 -0
  147. package/dist/m/c-YBSS55KU.js +20 -0
  148. package/dist/m/c-YCLU4UNT.js +2 -0
  149. package/dist/m/c-YVZ2L7CE.js +1 -0
  150. package/dist/m/c-ZK4GIZH2.js +1 -0
  151. package/dist/m/c-ZRXJD4G6.js +1 -0
  152. package/dist/m/c-ZWBI4HYK.js +1 -0
  153. package/dist/m/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
  154. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  155. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  156. package/dist/m/core/{host-permission-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
  157. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  158. package/dist/m/core/{persist-JFYRHMRJ.js → persist-UKXV5KLA.js} +1 -1
  159. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  160. package/dist/m/core/save-LYY7K6U7.js +1 -0
  161. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  162. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  163. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  164. package/dist/m/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
  165. package/dist/m/loader-3BBNZZGM.js +1 -0
  166. package/dist/m/manifest-26C55578.js +1 -0
  167. package/dist/m/manifest-edit-4396A009.js +1 -0
  168. package/dist/m/manifest.json +2111 -1573
  169. package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
  170. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  171. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  172. package/dist/m/plugins/{demo-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
  173. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  174. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  175. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  176. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  177. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  178. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  179. package/dist/m/plugins/{people-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
  180. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  181. package/dist/m/plugins/{richclay-NRQDSWX6.js → richclay-KLGETK77.js} +2 -2
  182. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  183. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  184. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  185. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  186. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  187. package/dist/m/plugins/{upload-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
  188. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  189. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  190. package/dist/m/plugins/{wire-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
  191. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  192. package/dist/m/route/route-62CHXL5B.js +1 -0
  193. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  194. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  195. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  196. package/dist/offline-manifest.json +1367 -930
  197. package/dist/page/themes/light-1.css +8 -7
  198. package/dist/server/core.cjs +1440 -288
  199. package/dist/server/offline-runtime.cjs +63 -63
  200. package/dist/server/offline-runtime.mjs +63 -63
  201. package/dist/server/page-compaction-worker.cjs +3710 -0
  202. package/dist/server/rooms.mjs +5 -0
  203. package/docs/guide.md +653 -101
  204. package/entries/sap.js +5 -5
  205. package/package.json +1 -1
  206. package/src/core/admin-contenteditable.js +5 -2
  207. package/src/core/admin-inputs.js +1 -0
  208. package/src/core/admin-onclick.js +2 -1
  209. package/src/core/conflict-notice-ui.js +4 -7
  210. package/src/core/download.js +2 -2
  211. package/src/core/host-meta.js +38 -0
  212. package/src/core/is-edit-mode.js +17 -1
  213. package/src/core/mode-key.js +125 -19
  214. package/src/core/mode-support.js +5 -0
  215. package/src/core/mode-transition.js +20 -6
  216. package/src/core/preserve-authored.js +4 -4
  217. package/src/core/recovery.js +121 -33
  218. package/src/core/save-accepted.js +33 -0
  219. package/src/core/save-core.js +106 -31
  220. package/src/core/save-job.js +108 -0
  221. package/src/core/save-prepare.js +53 -0
  222. package/src/core/save-size-notice.js +3 -1
  223. package/src/core/save-slices.js +15 -0
  224. package/src/core/save.js +557 -127
  225. package/src/core/serialize-steps.js +109 -0
  226. package/src/core/snapshot.js +170 -39
  227. package/src/core/source-map.js +83 -38
  228. package/src/core/stale-host-notice.js +1 -0
  229. package/src/core/upgrade-run.js +59 -8
  230. package/src/dom/active-element.js +5 -0
  231. package/src/lib/assets.js +1 -0
  232. package/src/lib/slice.js +65 -0
  233. package/src/lib/uimotion.js +774 -0
  234. package/src/lib/unsaved-state.js +2 -2
  235. package/src/live/fast-lane.js +10 -3
  236. package/src/live/local-lane.js +202 -11
  237. package/src/live/ops.js +16 -0
  238. package/src/live/page-compaction.js +162 -0
  239. package/src/live/server-lane.js +1 -0
  240. package/src/live/store.js +11 -3
  241. package/src/loader-logic.js +3 -1
  242. package/src/loader.js +14 -4
  243. package/src/page/adopt-boot.js +2 -1
  244. package/src/page/awareness.js +33 -2
  245. package/src/page/blocks.js +89 -27
  246. package/src/page/boot-pack.js +1 -1
  247. package/src/page/chart-tables.js +21 -3
  248. package/src/page/chart.js +406 -63
  249. package/src/page/checklist.js +10 -135
  250. package/src/page/checkpoint-proof.js +7 -1
  251. package/src/page/chrome.js +250 -51
  252. package/src/page/clipboard-shape.js +126 -0
  253. package/src/page/drag.js +33 -3
  254. package/src/page/editor.js +136 -25
  255. package/src/page/export-assets.js +357 -0
  256. package/src/page/export-html.js +209 -0
  257. package/src/page/export-markdown.js +361 -0
  258. package/src/page/features/checklist.js +428 -0
  259. package/src/page/features/export-html.js +189 -0
  260. package/src/page/features/export-markdown.js +169 -0
  261. package/src/page/features/import-markdown.js +349 -0
  262. package/src/page/features/media.js +61 -5
  263. package/src/page/features/registry.js +18 -0
  264. package/src/page/features/table.js +373 -83
  265. package/src/page/features/title-cover.js +331 -0
  266. package/src/page/features/title-icon.js +213 -0
  267. package/src/page/features/toc.js +87 -0
  268. package/src/page/format-command.js +25 -1
  269. package/src/page/format-gate.js +68 -18
  270. package/src/page/format-host.js +17 -0
  271. package/src/page/format-native-commands.js +21 -5
  272. package/src/page/image-actions.js +64 -0
  273. package/src/page/import-markdown.js +262 -0
  274. package/src/page/items.js +135 -17
  275. package/src/page/markdown.js +6 -2
  276. package/src/page/media.js +35 -119
  277. package/src/page/placeholder.js +14 -1
  278. package/src/page/projection.js +98 -10
  279. package/src/page/recover.js +10 -12
  280. package/src/page/recovery-ui.js +464 -0
  281. package/src/page/rich/chart.js +63 -7
  282. package/src/page/save-projection.js +28 -0
  283. package/src/page/save-states.js +13 -3
  284. package/src/page/schema.js +19 -3
  285. package/src/page/table-paste.js +3 -1
  286. package/src/page/table-style.js +4 -4
  287. package/src/page/table.js +44 -2
  288. package/src/page/themes/light-1.css +8 -7
  289. package/src/page/themes/light-1.source.css +8 -7
  290. package/src/page/toc-link.js +31 -0
  291. package/src/page/toc.js +5 -0
  292. package/src/page/view-hooks.js +38 -12
  293. package/src/page/view.js +13 -0
  294. package/src/page/ybind/binding.js +88 -5
  295. package/src/page/ybind/bridge.js +187 -20
  296. package/src/page/ybind/containment.js +162 -0
  297. package/src/page/ybind/doc.js +17 -9
  298. package/src/page/ybind/file-debt.js +63 -0
  299. package/src/page/ybind/layout.js +7 -2
  300. package/src/page/ybind/marks.js +1 -1
  301. package/src/page/ybind/moves.js +5 -4
  302. package/src/page/ybind/origins.js +106 -4
  303. package/src/page/ybind/parts.js +8 -1
  304. package/src/page/ybind/provider.js +113 -17
  305. package/src/page/ybind/recovery-proof.js +32 -9
  306. package/src/page/ybind/recovery-replay.js +558 -15
  307. package/src/page/ybind/recovery.js +174 -45
  308. package/src/page/ybind/table.js +67 -11
  309. package/src/page/ybind/walk-delta.js +258 -0
  310. package/src/page/ybind/walk.js +54 -29
  311. package/src/page/ybind/writer.js +1 -1
  312. package/src/plugins/ai-edit.js +13 -9
  313. package/src/plugins/indicator-chip.js +91 -0
  314. package/src/plugins/indicator.js +13 -80
  315. package/src/plugins/live.js +89 -14
  316. package/src/plugins/menu/actions.js +196 -0
  317. package/src/plugins/menu/people.js +279 -0
  318. package/src/plugins/menu/placement.js +121 -0
  319. package/src/plugins/menu/preferences.js +9 -0
  320. package/src/plugins/menu/seo.js +316 -397
  321. package/src/plugins/menu/style.js +171 -0
  322. package/src/plugins/menu/variants.js +296 -0
  323. package/src/plugins/menu.js +186 -183
  324. package/src/plugins/page-view.js +3 -2
  325. package/src/plugins/page.js +249 -44
  326. package/src/plugins/sortable.js +8 -1
  327. package/src/plugins/source.js +6 -0
  328. package/src/plugins/upgrade.js +51 -17
  329. package/src/plugins/upload-ui.js +12 -17
  330. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  331. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  332. package/src/plugins/visitor-drafts/intro.js +5 -3
  333. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  334. package/src/plugins/visitor-drafts/prompt.js +11 -9
  335. package/src/plugins/visitor-drafts/source.js +4 -3
  336. package/src/plugins/visitor-drafts/store.js +138 -9
  337. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  338. package/src/plugins/visitor-drafts-owner.js +4 -6
  339. package/src/plugins/visitor-drafts.js +5 -3
  340. package/src/recovery/store.js +1 -1
  341. package/src/route/route.js +4 -0
  342. package/src/server/rooms/index.js +5 -1
  343. package/src/server/rooms/live-data-rooms.js +302 -50
  344. package/src/server/rooms/page-compaction-worker.js +12 -0
  345. package/src/server/rooms/page-compactor.js +92 -0
  346. package/src/server/rooms/page-room-conformance.js +461 -0
  347. package/src/source/parse-steps.js +94 -0
  348. package/src/source/session.js +233 -57
  349. package/src/sync/editing-sender.js +12 -0
  350. package/src/sync/live-sync-edit.js +22 -2
  351. package/src/sync/live-sync.js +150 -23
  352. package/src/sync/page-relay.js +52 -0
  353. package/src/sync/presence-store.js +44 -4
  354. package/src/sync/presence.js +6 -3
  355. package/src/sync/saved-stream.js +11 -6
  356. package/src/ui/bevel-controls.js +1 -0
  357. package/src/ui/bevel-dialog.js +5 -2
  358. package/src/ui/editor-ui.js +77 -0
  359. package/src/ui/page-colour.js +281 -0
  360. package/src/ui/toast.js +16 -7
  361. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  362. package/src/vendor/hyper-undo.vendor.js +1 -1
  363. package/src/vendor/hypercms.vendor.js +9 -9
  364. package/src/vendor/parse5.vendor.js +2 -2
  365. package/src/vendor/sap-core.vendor.js +5 -5
  366. package/dist/m/c-2SPVQSHC.js +0 -1
  367. package/dist/m/c-336SGU5W.js +0 -1
  368. package/dist/m/c-3H6BZDNJ.js +0 -6
  369. package/dist/m/c-4DFEJ2PO.js +0 -3
  370. package/dist/m/c-56QRZTZH.js +0 -1
  371. package/dist/m/c-5J4E7O4Y.js +0 -4
  372. package/dist/m/c-5LKDWQTL.js +0 -19
  373. package/dist/m/c-5V3MOVJT.js +0 -1
  374. package/dist/m/c-6AZOGAPE.js +0 -1
  375. package/dist/m/c-6CRUUXOX.js +0 -5
  376. package/dist/m/c-6HMB24EV.js +0 -1
  377. package/dist/m/c-6KEVEQ7L.js +0 -1
  378. package/dist/m/c-A3E3NIMC.js +0 -1
  379. package/dist/m/c-BQ6LOBEE.js +0 -1
  380. package/dist/m/c-BTDI4Y23.js +0 -1
  381. package/dist/m/c-CJ3A7ZKE.js +0 -1
  382. package/dist/m/c-CQHPGKX2.js +0 -1
  383. package/dist/m/c-DFS4XZJV.js +0 -2
  384. package/dist/m/c-DNENWY4O.js +0 -1
  385. package/dist/m/c-E4LIR5X5.js +0 -1
  386. package/dist/m/c-FDNHHZY5.js +0 -1
  387. package/dist/m/c-FROJUZMV.js +0 -1
  388. package/dist/m/c-G2JPKCTP.js +0 -1
  389. package/dist/m/c-GB6TU7WV.js +0 -1
  390. package/dist/m/c-GFJJEBTU.js +0 -1
  391. package/dist/m/c-GJAEGGXY.js +0 -1
  392. package/dist/m/c-GOWBDM6J.js +0 -1
  393. package/dist/m/c-HFKUVACH.js +0 -1
  394. package/dist/m/c-HHDFCNHV.js +0 -1
  395. package/dist/m/c-IEM4NTCS.js +0 -1
  396. package/dist/m/c-JDCX26UP.js +0 -25
  397. package/dist/m/c-KGVGROZT.js +0 -1
  398. package/dist/m/c-KQ6TYJAP.js +0 -135
  399. package/dist/m/c-LFPHCXY3.js +0 -1
  400. package/dist/m/c-LRPUO2MX.js +0 -1
  401. package/dist/m/c-LWLBHLVC.js +0 -3
  402. package/dist/m/c-M2L47S2X.js +0 -7
  403. package/dist/m/c-MFP2ZWD4.js +0 -1
  404. package/dist/m/c-MKSQ4Y57.js +0 -1
  405. package/dist/m/c-NUFFJVVJ.js +0 -1
  406. package/dist/m/c-NUHSPT5P.js +0 -1
  407. package/dist/m/c-O72HVYSA.js +0 -1
  408. package/dist/m/c-OIBUTDXI.js +0 -1
  409. package/dist/m/c-OW5QIGG3.js +0 -1
  410. package/dist/m/c-PGRCK73F.js +0 -1
  411. package/dist/m/c-Q7NQD65V.js +0 -1
  412. package/dist/m/c-QH63LQOS.js +0 -1
  413. package/dist/m/c-R7IWVINQ.js +0 -1
  414. package/dist/m/c-RM7J7BV2.js +0 -1
  415. package/dist/m/c-S4ZEH46B.js +0 -9
  416. package/dist/m/c-SDAXDM2D.js +0 -1
  417. package/dist/m/c-SMGU7RT3.js +0 -235
  418. package/dist/m/c-SPIORRYS.js +0 -1
  419. package/dist/m/c-SWLRT66R.js +0 -1
  420. package/dist/m/c-TCJTVAFN.js +0 -1
  421. package/dist/m/c-TQEMVJCS.js +0 -1
  422. package/dist/m/c-TYLXX4NF.js +0 -21
  423. package/dist/m/c-U62OB4BT.js +0 -1
  424. package/dist/m/c-U6YWUAR3.js +0 -8
  425. package/dist/m/c-UDCSU5G5.js +0 -2
  426. package/dist/m/c-VI5RMSVY.js +0 -1
  427. package/dist/m/c-VRZN56XY.js +0 -1
  428. package/dist/m/c-W5MCHEGA.js +0 -1
  429. package/dist/m/c-WNYBLIOZ.js +0 -1
  430. package/dist/m/c-WWXIDH4O.js +0 -28
  431. package/dist/m/c-XCNTK76U.js +0 -1
  432. package/dist/m/c-XMS6DPBR.js +0 -1
  433. package/dist/m/c-ZUCOKOHT.js +0 -1
  434. package/dist/m/c-ZVPVXTJF.js +0 -4
  435. package/dist/m/c-ZVRKXKM7.js +0 -1
  436. package/dist/m/core/autosave-HRYGUWV6.js +0 -1
  437. package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
  438. package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
  439. package/dist/m/core/recovery-FLTCC2OC.js +0 -1
  440. package/dist/m/core/save-HQZ4S667.js +0 -1
  441. package/dist/m/core/save-core-KGQWFUNH.js +0 -1
  442. package/dist/m/core/snapshot-EGQZNT45.js +0 -1
  443. package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
  444. package/dist/m/loader-S4I7GKXR.js +0 -1
  445. package/dist/m/manifest-4C419D67.js +0 -1
  446. package/dist/m/manifest-edit-B077F288.js +0 -1
  447. package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
  448. package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
  449. package/dist/m/plugins/live-HFWD4OIH.js +0 -1
  450. package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
  451. package/dist/m/plugins/page-JOKB6XPR.js +0 -2
  452. package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
  453. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  454. package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
  455. package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
  456. package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
  457. package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
  458. package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
  459. package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
  460. package/dist/m/reader-core-DXYCURFF.js +0 -68
  461. package/dist/m/route/route-KOCZSEGN.js +0 -1
  462. package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
  463. package/dist/m/ui/index-3JLVI27O.js +0 -1
  464. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  465. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  466. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  467. /package/dist/m/plugins/{richclay-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * recovery-ui.js — the review a page shows for journaled work it may not replay.
3
+ *
4
+ * A capsule this page cannot apply (another authority, another generation, a newer
5
+ * external file, a foreign lineage) is still the person's work, so it is drawn here as
6
+ * the page it reconstructs, can be downloaded, and can be recovered as a copy: fresh
7
+ * block ids, inserted through the page's own document command, never a blind merge.
8
+ * Text typed during a composition that never became a transaction is offered the same
9
+ * way, as text.
10
+ *
11
+ * Loaded lazily, only when there is something to review: a page with nothing held aside
12
+ * pays for none of this, and a reader pays for it never.
13
+ */
14
+
15
+ import { model, state as pmState } from "../vendor/prosemirror.vendor.js";
16
+ import { mountEditorUI } from "../ui/editor-ui.js";
17
+ import { schema } from "./schema.js";
18
+ import { keysBetween, newId, nodeFromBlock } from "./model.js";
19
+ import { downloadStored } from "../core/conflict-download.js";
20
+ import {
21
+ imeEntries,
22
+ openImeShadow,
23
+ pageCopyChannel,
24
+ pageImeChannel,
25
+ reconstructPage,
26
+ recoverAsCopy,
27
+ } from "./ybind/recovery-replay.js";
28
+
29
+ export const REVIEW_TEXT = "Work from an earlier session could not be put back. Read it, download it, or recover it as a copy.";
30
+
31
+ // The control and its panel sit in one shadow root inside the page shell, so the page's
32
+ // CSS cannot restyle them. The page's --page-* tokens still reach them, and they keep
33
+ // the page's font and colour.
34
+ const STYLES = `
35
+ :host { font: inherit !important; color: inherit !important; }
36
+ .page-recovery-root { display: contents; }
37
+ .page-recovery { position: fixed; right: 16px; bottom: 16px; z-index: 20; min-height: 36px; padding: 0 14px; font: inherit; font-size: 14px; color: var(--page-ink); background: var(--page-pop-bg); border: 1px solid var(--page-chrome-line-rest); border-radius: 999px; cursor: pointer; }
38
+ .page-recovery[hidden], .page-recovery-panel[hidden] { display: none; }
39
+ .page-recovery-panel { position: fixed; right: 16px; bottom: 64px; z-index: 21; width: min(420px, calc(100vw - 32px)); max-height: 60vh; overflow-y: auto; color: var(--page-ink); background: var(--page-pop-bg); border: 1px solid var(--page-chrome-line-rest); border-radius: var(--page-chrome-radius); padding: 8px; box-sizing: border-box; }
40
+ .page-recovery-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 6px 8px; font-weight: 600; }
41
+ .page-recovery-note { margin: 0 6px 8px; font-size: 13px; opacity: .72; }
42
+ .page-recovery-failure { margin: 0 6px 8px; font-size: 13px; color: var(--page-warn, #b25b00); }
43
+ .page-recovery-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; align-items: center; min-height: 44px; padding: 8px 6px; border-top: 1px solid var(--page-chrome-line-rest); }
44
+ .page-recovery-meta { grid-column: 1; font-size: 12px; opacity: .72; }
45
+ .page-recovery-text { grid-column: 1; overflow-wrap: anywhere; white-space: pre-wrap; }
46
+ .page-recovery-row button, .page-recovery-head button { grid-column: 2; grid-row: 1 / span 2; min-height: 36px; padding: 0 12px; font: inherit; font-size: 14px; color: inherit; background: transparent; border: 1px solid var(--page-chrome-line-rest); border-radius: var(--page-chrome-radius); cursor: pointer; }
47
+ .page-recovery-row.is-busy button { opacity: .6; cursor: progress; }
48
+ @media (max-width: 560px) {
49
+ .page-recovery { top: 12px; bottom: auto; min-height: 44px; }
50
+ .page-recovery-panel { left: 0; right: 0; bottom: 0; width: 100%; max-height: 70vh; border-radius: var(--page-chrome-radius) var(--page-chrome-radius) 0 0; }
51
+ .page-recovery-row button, .page-recovery-head button { min-height: 44px; }
52
+ }
53
+ `;
54
+
55
+ const PREVIEW_LIMIT = 280;
56
+
57
+ const el = (doc, tag, className, attrs = {}) => {
58
+ const node = doc.createElement(tag);
59
+ if (className) node.className = className;
60
+ for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
61
+ return node;
62
+ };
63
+
64
+
65
+ const ordered = (blocks) =>
66
+ Object.keys(blocks).sort((a, b) => {
67
+ const left = typeof blocks[a]?.pos === "string" ? blocks[a].pos : "";
68
+ const right = typeof blocks[b]?.pos === "string" ? blocks[b].pos : "";
69
+ if (left !== right) return left < right ? -1 : 1;
70
+ return a < b ? -1 : a > b ? 1 : 0;
71
+ });
72
+
73
+ /**
74
+ * One reconstructed page as a copy: fresh block ids, and every parent remapped to its
75
+ * parent's new id, so a toggle's body arrives with its toggle.
76
+ *
77
+ * @param {Object} blocks - a reconstructed page, by block id
78
+ * @returns {Array<Object>} block records carrying their own id and order key
79
+ */
80
+ export function copyBlocks(blocks) {
81
+ const order = ordered(blocks ?? {});
82
+ const ids = new Map(order.map((id) => [id, newId()]));
83
+ const keys = keysBetween(null, null, order.length);
84
+ return order.map((id, at) => {
85
+ const block = blocks[id];
86
+ const parent = typeof block?.parent === "string" ? ids.get(block.parent) ?? null : null;
87
+ return { ...block, id: ids.get(id), pos: keys[at], parent };
88
+ });
89
+ }
90
+
91
+ /**
92
+ * The nodes a copy of one reconstructed page is inserted as.
93
+ *
94
+ * A block this runtime cannot build keeps its text as a paragraph, so no writing is
95
+ * lost to a newer format.
96
+ *
97
+ * @param {Object} blocks - a reconstructed page, by block id
98
+ * @returns {Array<Object>} ProseMirror nodes
99
+ */
100
+ export function copyNodes(blocks) {
101
+ const nodes = [];
102
+ for (const block of copyBlocks(blocks)) {
103
+ let node = null;
104
+ try {
105
+ node = nodeFromBlock(schema, block.id, block);
106
+ } catch (_) {
107
+ node = null;
108
+ }
109
+ if (!node && typeof block.c === "string" && block.c) {
110
+ node = schema.nodes.paragraph.create({ id: block.id, pos: block.pos, parent: block.parent }, schema.text(block.c));
111
+ }
112
+ if (node) nodes.push(node);
113
+ }
114
+ return nodes;
115
+ }
116
+
117
+ /**
118
+ * Insert one reconstructed page at the end of the document, through the normal command.
119
+ *
120
+ * A single transaction the page turns into its own ops: journaled, offered to the room
121
+ * and undoable, exactly as typing it would be.
122
+ *
123
+ * @param {Object} view - the live editor view
124
+ * @param {Object} blocks - a reconstructed page, by block id
125
+ * @returns {{ok: boolean, code?: string, ids?: Array<string>}}
126
+ */
127
+ export function insertCopy(view, blocks) {
128
+ if (!view || !view.state) return { ok: false, code: "no-editor" };
129
+ const nodes = copyNodes(blocks ?? {});
130
+ if (!nodes.length) return { ok: false, code: "nothing-to-copy" };
131
+ const state = view.state;
132
+ const tr = state.tr.insert(state.doc.content.size, model.Fragment.from(nodes));
133
+ tr.setSelection(pmState.Selection.near(tr.doc.resolve(tr.doc.content.size - 1)));
134
+ view.dispatch(tr.scrollIntoView());
135
+ return { ok: true, ids: nodes.map((node) => node.attrs.id) };
136
+ }
137
+
138
+ const preview = (doc, text) => {
139
+ const box = el(doc, "div", "page-recovery-text");
140
+ box.textContent = text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT)}…` : text;
141
+ return box;
142
+ };
143
+
144
+ async function copyText(text) {
145
+ try {
146
+ await navigator.clipboard.writeText(text);
147
+ return true;
148
+ } catch (_) {}
149
+ const field = globalThis.document?.createElement("textarea");
150
+ if (!field) return false;
151
+ field.value = text;
152
+ field.style.setProperty("position", "fixed", "important");
153
+ field.style.setProperty("opacity", "0", "important");
154
+ globalThis.document.body?.append(field);
155
+ field.select();
156
+ let ok = false;
157
+ try {
158
+ ok = globalThis.document.execCommand?.("copy") === true;
159
+ } catch (_) {}
160
+ field.remove();
161
+ return ok;
162
+ }
163
+
164
+ /**
165
+ * The review panel for journaled work this page may not replay.
166
+ *
167
+ * @param {Object} options
168
+ * @param {Element} options.mount - where the button and panel live
169
+ * @param {Function} [options.getView] - the live editor view, or null
170
+ * @param {Function} [options.getJournal] - this session's page journal, or null
171
+ * @param {?Object} [options.service] - `clay.recovery`
172
+ * @param {Function} [options.isCurrent] - whether this session still owns the page
173
+ * @param {Function} [options.onCopied] - (row, receipt) after a copy reached this device
174
+ * @param {Function} [options.onCovered] - (row) when a save proved the copy is in the page
175
+ * @returns {Object}
176
+ */
177
+ export function createRecoveryReview({
178
+ mount,
179
+ getView = () => null,
180
+ getJournal = () => null,
181
+ service = globalThis.window?.clay?.recovery ?? null,
182
+ isCurrent = () => true,
183
+ onCopied = () => {},
184
+ onCovered = () => {},
185
+ } = {}) {
186
+ const doc = mount.ownerDocument;
187
+ const button = el(doc, "button", "page-recovery", { type: "button", "aria-haspopup": "dialog", "aria-expanded": "false" });
188
+ const panel = el(doc, "div", "page-recovery-panel", { role: "dialog", "aria-label": "Recover work" });
189
+ button.hidden = true;
190
+ panel.hidden = true;
191
+ const content = el(doc, "div", "page-recovery-root");
192
+ content.append(button, panel);
193
+ const ui = mountEditorUI({ name: "page-recovery", content, css: [STYLES], parent: mount });
194
+
195
+ let rows = [];
196
+ let journaled = [];
197
+ let failure = null;
198
+ let copied = new Set();
199
+ const busy = new Set();
200
+ let copies = null;
201
+ let shadow = null;
202
+ let destroyed = false;
203
+
204
+ const total = () => rows.length + journaled.length;
205
+
206
+ const ensureCopyChannel = () => {
207
+ if (copies) return copies;
208
+ try {
209
+ copies = pageCopyChannel(service);
210
+ } catch (_) {
211
+ copies = null;
212
+ }
213
+ return copies;
214
+ };
215
+
216
+ const ensureShadow = () => {
217
+ if (shadow) return shadow;
218
+ try {
219
+ shadow = openImeShadow({ channel: pageImeChannel(service), key: getJournal()?.key ?? "" });
220
+ } catch (_) {
221
+ shadow = null;
222
+ }
223
+ return shadow;
224
+ };
225
+
226
+ const close = () => {
227
+ panel.hidden = true;
228
+ button.setAttribute("aria-expanded", "false");
229
+ };
230
+
231
+ const download = () => {
232
+ const edits = [];
233
+ for (const row of rows) edits.push({ id: row.id, kind: "recovered-page", name: row.message, yours: row.text, now: "", sentence: row.message });
234
+ const composing = "Text typed while composing never reached the page.";
235
+ for (const row of journaled) edits.push({ id: row.id, kind: "composing", name: "Typed text", yours: row.text, now: "", sentence: composing });
236
+ if (!downloadStored({ html: null, edits })) failure = "That download could not be made.";
237
+ draw();
238
+ };
239
+
240
+ // Recover as a copy. Nothing of the capsule's own operations is merged: the page is
241
+ // inserted as a local edit with fresh ids, and the destination record the journal
242
+ // commits is what the transfer marker names.
243
+ async function recover(row) {
244
+ if (!row || busy.has(row.id) || copied.has(row.id)) return { ok: false, code: "busy" };
245
+ busy.add(row.id);
246
+ failure = null;
247
+ draw();
248
+ try {
249
+ if (!isCurrent()) return { ok: false, code: "stale" };
250
+ const journal = getJournal();
251
+ const channel = journal?.channel ?? null;
252
+ const marks = ensureCopyChannel();
253
+ const result = await recoverAsCopy({
254
+ channel,
255
+ copies: marks,
256
+ source: { key: journal?.key ?? "", id: row.id, revision: row.revision },
257
+ insert: async () => {
258
+ if (!isCurrent()) throw new Error("stale");
259
+ const view = getView();
260
+ if (!view) throw new Error("no-editor");
261
+ const waiting = journal?.next ? journal.next() : null;
262
+ const blocks = row.blocks ?? reconstructPage({ capsule: row.capsule, batches: row.batches ?? [] });
263
+ const inserted = insertCopy(view, blocks);
264
+ if (!inserted.ok) throw new Error(inserted.code);
265
+ if (!waiting) throw new Error("no-journal");
266
+ const record = await waiting;
267
+ if (!record) throw new Error("not-stored");
268
+ return { key: journal?.key ?? "", id: record.id, revision: record.revision };
269
+ },
270
+ });
271
+ if (!result.ok && !isCurrent()) return { ok: false, code: "stale" };
272
+ if (result.ok) {
273
+ copied.add(row.id);
274
+ onCopied(row, result);
275
+ } else if (result.code === "insert-failed") {
276
+ failure = "That copy could not be stored on this device. It is still here to try again.";
277
+ } else if (result.code === "already-copied") {
278
+ copied.add(row.id);
279
+ failure = "This work was already recovered as a copy.";
280
+ } else if (result.code === "covered") {
281
+ onCovered(row);
282
+ } else if (result.code !== "stale" && result.code !== "busy") {
283
+ failure = "Recovery is unavailable here. Download the copy instead.";
284
+ }
285
+ return result;
286
+ } finally {
287
+ busy.delete(row.id);
288
+ if (!destroyed) draw();
289
+ }
290
+ }
291
+
292
+ async function dismiss(row) {
293
+ if (!row || !shadow) return { ok: false, code: "unavailable" };
294
+ const record = journaled.find((entry) => entry.id === row.id);
295
+ if (!record) return { ok: false, code: "unknown" };
296
+ try {
297
+ const receipt = await shadow.channel.dismiss(record.id, { revision: record.revision });
298
+ if (receipt?.ok) journaled = journaled.filter((entry) => entry.id !== record.id);
299
+ draw();
300
+ return receipt;
301
+ } catch (_) {
302
+ return { ok: false, code: "error" };
303
+ }
304
+ }
305
+
306
+ const rowButton = (label, onClick) => {
307
+ const node = el(doc, "button", "", { type: "button" });
308
+ node.textContent = label;
309
+ node.addEventListener("click", onClick);
310
+ return node;
311
+ };
312
+
313
+ function draw() {
314
+ panel.replaceChildren();
315
+ const head = el(doc, "div", "page-recovery-head");
316
+ head.append(doc.createTextNode("Recover work"));
317
+ head.append(rowButton("Download", download));
318
+ const done = rowButton("Close", () => { close(); button.focus(); });
319
+ head.append(done);
320
+ const note = el(doc, "p", "page-recovery-note");
321
+ note.textContent = REVIEW_TEXT;
322
+ panel.append(head, note);
323
+ if (failure) {
324
+ const line = el(doc, "p", "page-recovery-failure");
325
+ line.textContent = failure;
326
+ panel.append(line);
327
+ }
328
+ for (const row of rows) {
329
+ const item = el(doc, "div", "page-recovery-row", { "data-capsule": String(row.id) });
330
+ if (busy.has(row.id)) item.classList.add("is-busy");
331
+ const meta = el(doc, "div", "page-recovery-meta");
332
+ meta.textContent = `${row.message}${copied.has(row.id) ? " · Recovered as a copy" : ""}`;
333
+ const action = rowButton("Recover as a copy", () => recover(row));
334
+ action.disabled = busy.has(row.id) || copied.has(row.id);
335
+ item.append(meta, preview(doc, row.text || "(no text)"), action);
336
+ panel.append(item);
337
+ }
338
+ for (const row of journaled) {
339
+ const item = el(doc, "div", "page-recovery-row", { "data-composing": row.blockId });
340
+ const meta = el(doc, "div", "page-recovery-meta");
341
+ meta.textContent = "Text typed while composing. It never reached the page.";
342
+ item.append(meta, preview(doc, row.text));
343
+ const actions = el(doc, "div", "");
344
+ actions.append(
345
+ rowButton("Copy", async () => { if (!(await copyText(row.text))) failure = "Could not copy. Select the text instead."; draw(); }),
346
+ rowButton("Dismiss", () => dismiss(row)),
347
+ );
348
+ item.append(actions);
349
+ panel.append(item);
350
+ }
351
+ }
352
+
353
+ const open = () => {
354
+ if (!total()) return false;
355
+ panel.hidden = false;
356
+ button.setAttribute("aria-expanded", "true");
357
+ draw();
358
+ panel.querySelector("button")?.focus();
359
+ return true;
360
+ };
361
+
362
+ button.addEventListener("click", () => (panel.hidden ? open() : close()));
363
+ panel.addEventListener("keydown", (event) => {
364
+ if (event.key !== "Escape") return;
365
+ event.stopPropagation();
366
+ close();
367
+ button.focus();
368
+ });
369
+
370
+ // A composition is never forced into a transaction: its block's visible text is
371
+ // shadowed while it is active, and retired on composition end only when a journaled
372
+ // update already carries that text.
373
+ const composing = new Map();
374
+ const blockOf = (event) => {
375
+ const target = event?.target;
376
+ const element = target?.closest?.("[data-id]") ?? null;
377
+ const id = element?.getAttribute?.("data-id") ?? null;
378
+ return { blockId: id, text: element?.textContent ?? "" };
379
+ };
380
+ const onCompose = (event) => {
381
+ const found = blockOf(event);
382
+ if (!found.blockId) return;
383
+ composing.set(found.blockId, found.text);
384
+ ensureShadow()?.keep(found);
385
+ };
386
+ const onComposed = (event) => {
387
+ const found = blockOf(event);
388
+ const kept = composing.get(found.blockId);
389
+ composing.delete(found.blockId);
390
+ const holder = ensureShadow();
391
+ if (!holder) return;
392
+ const journal = getJournal();
393
+ const text = found.text || kept || "";
394
+ const covered = journal?.covers ? journal.covers(found.blockId, text) : false;
395
+ holder.clear({ blockId: found.blockId, text, covered }).then((receipt) => {
396
+ if (receipt?.ok) refresh();
397
+ }, () => {});
398
+ };
399
+
400
+ /**
401
+ * Attach the composition listeners to the editing surface.
402
+ *
403
+ * @param {Element} root
404
+ * @returns {Function} detach
405
+ */
406
+ function watch(root) {
407
+ if (!root) return () => {};
408
+ root.addEventListener("compositionstart", onCompose);
409
+ root.addEventListener("compositionupdate", onCompose);
410
+ root.addEventListener("compositionend", onComposed);
411
+ return () => {
412
+ root.removeEventListener("compositionstart", onCompose);
413
+ root.removeEventListener("compositionupdate", onCompose);
414
+ root.removeEventListener("compositionend", onComposed);
415
+ };
416
+ }
417
+
418
+ // What the store holds for this page: composition shadows it kept, offered as text.
419
+ async function refresh() {
420
+ const holder = ensureShadow();
421
+ if (!holder || destroyed) return journaled;
422
+ let listed = null;
423
+ try {
424
+ listed = await holder.list();
425
+ } catch (_) {
426
+ listed = null;
427
+ }
428
+ if (destroyed) return journaled;
429
+ journaled = listed?.ok ? imeEntries(listed.records) : [];
430
+ update({ rows, refreshText: false });
431
+ return journaled;
432
+ }
433
+
434
+ function update({ rows: next = rows, refreshText = true } = {}) {
435
+ rows = (next || []).map((row) => ({ ...row }));
436
+ const count = total();
437
+ button.hidden = count === 0;
438
+ button.textContent = `Recover work (${count})`;
439
+ if (!count) close();
440
+ else if (!panel.hidden) draw();
441
+ if (refreshText) refresh();
442
+ return rows;
443
+ }
444
+
445
+ return {
446
+ open,
447
+ close,
448
+ download,
449
+ recover,
450
+ dismiss,
451
+ refresh,
452
+ watch,
453
+ update,
454
+ get count() { return total(); },
455
+ get rows() { return rows; },
456
+ get journaled() { return journaled; },
457
+ destroy() {
458
+ destroyed = true;
459
+ ui.remove();
460
+ shadow = null;
461
+ copies = null;
462
+ },
463
+ };
464
+ }
@@ -16,13 +16,16 @@ function ticks(min, max, count) {
16
16
  return out.length ? out : [Number(min.toFixed(2)), Number(max.toFixed(2))];
17
17
  }
18
18
 
19
+ const missing = (chart, reason) => ({ state: "missing", reason, sourceId: chart.source ?? null, title: chart.title ?? "" });
20
+
19
21
  export function chartModel(chart, table) {
20
- if (!table) return { state: "missing", reason: "table" };
22
+ if (!table) return missing(chart, "table");
21
23
  const cols = table.cols ?? [];
22
24
  const rows = table.rows ?? [];
23
25
  const x = cols.find((col) => col.id === chart.x);
24
- if (!x || !chart.series?.length || chart.series.some((id) => !cols.some((col) => col.id === id))) return { state: "missing", reason: "column" };
25
- const series = chart.series.map((id, index) => {
26
+ if (!x || !chart.series?.length || chart.series.some((id) => !cols.some((col) => col.id === id))) return missing(chart, "column");
27
+ const plotted = chart.type === "pie" ? chart.series.slice(0, 1) : chart.series;
28
+ const series = plotted.map((id, index) => {
26
29
  const col = cols.find((item) => item.id === id);
27
30
  const points = rows.map((row, rowIndex) => ({
28
31
  id: `${chart.id}:${row.id}:${id}`, rowId: row.id, colId: id, rowIndex,
@@ -45,8 +48,64 @@ export function chartModel(chart, table) {
45
48
  };
46
49
  }
47
50
 
51
+ const PIE = { width: 560, height: 232, cx: 150, cy: 116, radius: 84 };
52
+ const PIE_COLOURS = 6;
53
+ const pieHeight = (rows) => Math.max(PIE.height, 54 + (rows - 1) * 36 + 24);
54
+
55
+ function chartCaption(series) {
56
+ const valid = series.points.filter((point) => point.value !== null);
57
+ return `${series.name}: ${valid.length ? `${fmt(valid[0].value)} to ${fmt(valid.at(-1).value)}` : "no values"}`;
58
+ }
59
+
60
+ function pieSlices(model) {
61
+ const series = model.series[0];
62
+ if (!series) return [];
63
+ const positive = series.points.filter((point) => point.value !== null && point.value > 0);
64
+ const total = positive.reduce((sum, point) => sum + point.value, 0);
65
+ if (!total) return [];
66
+ const points = positive.length <= PIE_COLOURS ? positive : [
67
+ ...positive.slice(0, PIE_COLOURS - 1),
68
+ { id: `${positive[0].id.split(":")[0]}:other`, xLabel: "Other", value: positive.slice(PIE_COLOURS - 1).reduce((sum, point) => sum + point.value, 0), highlight: positive.slice(PIE_COLOURS - 1).some((point) => point.highlight) },
69
+ ];
70
+ let angle = -Math.PI / 2;
71
+ return points.map((point, index) => {
72
+ const fraction = point.value / total;
73
+ const slice = { point, color: `var(--chart-${index % 6 + 1})`, fraction, percent: Math.round(fraction * 100), from: angle, to: angle + fraction * Math.PI * 2 };
74
+ angle = slice.to;
75
+ return slice;
76
+ });
77
+ }
78
+
79
+ function renderPie(model, slices, width) {
80
+ const box = pieHeight(slices.length);
81
+ const height = fmt(width * box / PIE.width);
82
+ const caption = model.series[0] ? chartCaption(model.series[0]) : "no values";
83
+ const parts = [`<svg xmlns="http://www.w3.org/2000/svg" role="img" width="${width}" height="${height}" viewBox="0 0 ${PIE.width} ${box}">`, `<title>${esc(model.title)}</title>`, `<desc>${esc(caption)}</desc>`];
84
+ if (!slices.length) parts.push(`<text x="${PIE.width / 2}" y="${PIE.cy}" text-anchor="middle">No positive values to chart</text>`);
85
+ slices.forEach((slice) => {
86
+ const point = ` data-point="${esc(slice.point.id)}"${slice.point.highlight ? ' data-highlight="true"' : ""}`;
87
+ if (slice.fraction >= .999) {
88
+ parts.push(`<circle${point} cx="${PIE.cx}" cy="${PIE.cy}" r="${PIE.radius}" fill="${slice.color}"/>`);
89
+ return;
90
+ }
91
+ const x1 = fmt(PIE.cx + PIE.radius * Math.cos(slice.from));
92
+ const y1 = fmt(PIE.cy + PIE.radius * Math.sin(slice.from));
93
+ const x2 = fmt(PIE.cx + PIE.radius * Math.cos(slice.to));
94
+ const y2 = fmt(PIE.cy + PIE.radius * Math.sin(slice.to));
95
+ parts.push(`<path${point} d="M ${PIE.cx} ${PIE.cy} L ${x1} ${y1} A ${PIE.radius} ${PIE.radius} 0 ${slice.fraction > .5 ? 1 : 0} 1 ${x2} ${y2} Z" fill="${slice.color}" stroke="var(--page-bg)" stroke-width="2"/>`);
96
+ });
97
+ slices.forEach((slice, index) => {
98
+ const yy = fmt(54 + index * 36);
99
+ const point = ` data-point="${esc(slice.point.id)}"${slice.point.highlight ? ' data-highlight="true"' : ""}`;
100
+ parts.push(`<g${point}><rect x="290" y="${fmt(54 + index * 36 - 10)}" width="11" height="11" fill="${slice.color}"/><text x="310" y="${yy}">${esc(slice.point.xLabel)}</text><text x="530" y="${yy}" text-anchor="end">${fmt(slice.point.value)} (${slice.percent}%)</text></g>`);
101
+ });
102
+ parts.push("</svg>");
103
+ return parts.join("");
104
+ }
105
+
48
106
  export function renderChartSvg(model, { width = 640, height = 320, narrow = false } = {}) {
49
107
  if (model.state === "missing") return `<svg xmlns="http://www.w3.org/2000/svg" role="img" width="${width}" height="80" viewBox="0 0 ${width} 80"><title>Chart source missing</title><text x="12" y="44">Chart source missing</text></svg>`;
108
+ if (model.type === "pie") return renderPie(model, pieSlices(model), width);
50
109
  const margin = { left: 52, right: 72, top: 28, bottom: 48 };
51
110
  const plotW = Math.max(1, width - margin.left - margin.right);
52
111
  const plotH = Math.max(1, height - margin.top - margin.bottom);
@@ -55,10 +114,7 @@ export function renderChartSvg(model, { width = 640, height = 320, narrow = fals
55
114
  const n = model.xLabels.length;
56
115
  const x = (index) => margin.left + (n <= 1 ? plotW / 2 : index * plotW / (n - 1));
57
116
  const parts = [`<svg xmlns="http://www.w3.org/2000/svg" role="img" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`, `<title>${esc(model.title)}</title>`];
58
- const caption = model.series.map((item) => {
59
- const valid = item.points.filter((point) => point.value !== null);
60
- return `${item.name}: ${valid.length ? `${fmt(valid[0].value)} to ${fmt(valid.at(-1).value)}` : "no values"}`;
61
- }).join("; ");
117
+ const caption = model.series.map(chartCaption).join("; ");
62
118
  parts.push(`<desc>${esc(caption)}</desc>`);
63
119
  for (const tick of narrow ? model.narrowTicks : model.yTicks) {
64
120
  const yy = fmt(y(tick));
@@ -92,4 +92,32 @@ export function articleOfPageState(state) {
92
92
  return renderFromTags([typeof state === "string" ? state : JSON.stringify(state)]).html;
93
93
  }
94
94
 
95
+ /**
96
+ * The page state a save the host accepted carries, or null.
97
+ *
98
+ * A request body is not a capture: whoever answers it may have been a host that rewrote the
99
+ * bytes, an older save answering after this tab typed again, or something that is not a page
100
+ * at all. So the state has to be there, has to decode, and has to be the one the accepted
101
+ * article projects — otherwise these bytes prove nothing about the input this tab holds.
102
+ *
103
+ * @param {string} html - the exact body the host accepted
104
+ * @returns {?Object}
105
+ */
106
+ export function acceptedPageState(html) {
107
+ if (typeof html !== "string") return null;
108
+ try {
109
+ const root = new DOMParser().parseFromString(html, "text/html");
110
+ const state = pageStateOfClone(root);
111
+ const article = root.querySelector(ARTICLE);
112
+ if (!state || !article) return null;
113
+ // Compare as parsed markup: the projection writes some empty attributes bare,
114
+ // and a parsed article serializes every attribute with a value.
115
+ const rendered = root.createElement("article");
116
+ rendered.innerHTML = articleOfPageState(state);
117
+ return article.innerHTML === rendered.innerHTML ? state : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
95
123
  addDocumentTransform(projectInto);
@@ -27,8 +27,18 @@ const SAVE_STATE_BY_CODE = Object.freeze({
27
27
  "save-history-refresh-required": "blocked",
28
28
  "provenance-unsupported": "blocked",
29
29
  "receipt-expired": "blocked",
30
+ "page-full": "blocked",
31
+ "compaction-failed": "blocked",
30
32
  });
31
33
 
34
+ // Where a person's typing lives, never a stronger claim than the evidence behind it.
35
+ const pageStateFor = ({ unavailable, pending, journaled, acknowledged, checkpointed } = {}) =>
36
+ unavailable ? "unavailable"
37
+ : pending > 0 ? "pending"
38
+ : journaled > 0 ? "journaled"
39
+ : acknowledged > 0 ? "acknowledged"
40
+ : checkpointed ? "checkpointed" : null;
41
+
32
42
  const BLOCKED_TEXT = "Can't save here. Your edits are still in this tab.";
33
43
  const RETRY_TEXT = "Still trying to save. Your edits are safe in this tab.";
34
44
 
@@ -58,7 +68,7 @@ function withoutPageTag(html) {
58
68
  // page projection already applied by the live store's snapshot listener, and
59
69
  // without the page editor's merge state, which no saved file or download carries.
60
70
  function downloadPageCopy() {
61
- const { forSave } = captureForSaveAndComparison();
71
+ const { forSave } = captureForSaveAndComparison({ emitForSync: false });
62
72
  return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
63
73
  }
64
74
 
@@ -71,7 +81,7 @@ function downloadSnapshot(forSave) {
71
81
  // the projected article, and only its text is asked for.
72
82
  async function copyPageText() {
73
83
  try {
74
- const { forSave } = captureForSaveAndComparison();
84
+ const { forSave } = captureForSaveAndComparison({ emitForSync: false });
75
85
  const doc = new DOMParser().parseFromString(forSave, "text/html");
76
86
  const articles = doc.querySelectorAll("article[data-page]");
77
87
  if (articles.length !== 1) return false;
@@ -82,4 +92,4 @@ async function copyPageText() {
82
92
  }
83
93
  }
84
94
 
85
- export { SAVE_STATE_BY_CODE, BLOCKED_TEXT, RETRY_TEXT, saveStateFor, downloadPageCopy, downloadSnapshot, copyPageText };
95
+ export { SAVE_STATE_BY_CODE, BLOCKED_TEXT, RETRY_TEXT, pageStateFor, saveStateFor, downloadPageCopy, downloadSnapshot, copyPageText };
@@ -29,7 +29,15 @@ export const schema = new model.Schema({
29
29
  group: "block",
30
30
  content: "inline*",
31
31
  defining: true,
32
- attrs: { ...block, level: { default: 2 } },
32
+ attrs: {
33
+ ...block,
34
+ level: { default: 2 },
35
+ pageIcon: { default: null },
36
+ pageCoverSrc: { default: null },
37
+ pageCoverAlt: { default: "" },
38
+ pageCoverX: { default: 50 },
39
+ pageCoverY: { default: 50 },
40
+ },
33
41
  toDOM: (n) => [`h${n.attrs.level}`, ref(n), 0],
34
42
  },
35
43
  list_item: {
@@ -74,8 +82,8 @@ export const schema = new model.Schema({
74
82
  toggle: {
75
83
  group: "block",
76
84
  content: "inline*",
77
- attrs: { ...block, open: { default: false } },
78
- toDOM: (n) => ["div", ref(n, { class: "page-toggle", "data-open": n.attrs.open ? "true" : "false" }), 0],
85
+ attrs: { ...block, open: { default: false }, level: { default: null } },
86
+ toDOM: (n) => ["div", ref(n, { class: "page-toggle", "data-open": n.attrs.open ? "true" : "false", ...(n.attrs.level ? { "data-level": String(n.attrs.level) } : {}) }), 0],
79
87
  },
80
88
  divider: {
81
89
  group: "block",
@@ -83,6 +91,14 @@ export const schema = new model.Schema({
83
91
  attrs: block,
84
92
  toDOM: (n) => ["hr", ref(n)],
85
93
  },
94
+ // A table of contents holds no headings: its entries are derived from the document
95
+ // whenever it is drawn, so a rename, a move or a deletion needs no repair here.
96
+ toc: {
97
+ group: "block",
98
+ atom: true,
99
+ attrs: { ...block, maxLevel: { default: null } },
100
+ toDOM: (n) => ["div", ref(n, { class: "page-toc", "data-type": "toc", ...(n.attrs.maxLevel ? { "data-max-level": String(n.attrs.maxLevel) } : {}) })],
101
+ },
86
102
  image: {
87
103
  group: "block",
88
104
  atom: true,