@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,331 @@
1
+ // Page cover: the picture a page may show above its title, and the two things a person
2
+ // does with it — put one there, and choose which part of it stays in view. It is a set of
3
+ // scalar attributes of the title heading, never a second store, so one person's icon and
4
+ // another's cover are different fields and neither overwrites the other. The file travels
5
+ // the media upload transport, and nothing reaches the record until the host answers with an
6
+ // address: one admitted transaction, after the gate, for the title the command was asked
7
+ // for. The preview never writes; one commit lands on release, Enter or Apply.
8
+ import { schema } from "../schema.js";
9
+ import { newId, keyBetween } from "../model.js";
10
+ import { asStep } from "../blocksel.js";
11
+ import { announce } from "../announce.js";
12
+ import { safeFileHref, safeSrc } from "../address.js";
13
+ import { EMBED_MAX, mediaRuntime } from "../media.js";
14
+ import { mountEditorUI } from "../../ui/editor-ui.js";
15
+
16
+ // How far one arrow key moves the focal point, in percent of the image.
17
+ const STEP = 5;
18
+
19
+ // The title is the document's first root H1, and only it carries a cover: a nested H1 is no
20
+ // title, and a second root H1 becomes the title only when it moves first. The rule is read
21
+ // here rather than beside the icon's, so a cover command downloads nothing it does not use.
22
+ function titleOf(doc) {
23
+ let at = 0;
24
+ for (let i = 0; i < doc.childCount; i++) {
25
+ const node = doc.child(i);
26
+ if (node.type === schema.nodes.heading && node.attrs.level === 1 && node.attrs.parent == null) return { index: i, at, node };
27
+ at += node.nodeSize;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ // The bar sits in its own shadow root beside the editor, so the page's CSS cannot restyle
33
+ // it. The page's --page-* tokens still reach it, and it keeps the editor's font and colour.
34
+ const COVER_CSS = `
35
+ :host{font:inherit !important;color:inherit !important}
36
+ .page-cover-panel{display:flex;align-items:center;gap:8px;margin:8px 0;padding:6px 10px;border:1px solid var(--page-pop-line);border-radius:8px;background:var(--page-pop-bg);color:var(--page-pop-ink);box-shadow:var(--page-pop-shadow);font-size:.88em}
37
+ .page-cover-msg{max-width:34ch}
38
+ .page-cover-bar{flex:1;height:2px;border-radius:1px;background:var(--page-rule);overflow:hidden}
39
+ .page-cover-bar>span{display:block;height:100%;background:var(--page-progress, var(--page-ink-soft));transition:width 200ms}
40
+ .page-cover-btn{border:0;padding:4px 8px;border-radius:6px;background:none;color:inherit;font:inherit;cursor:pointer}
41
+ .page-cover-btn:hover{background:var(--page-pop-hover)}
42
+ .page-cover-panel[data-state="error"] .page-cover-msg{color:var(--page-danger, #9E3B2F)}
43
+ `;
44
+
45
+ const clamp = (value) => Math.min(100, Math.max(0, Math.round(value)));
46
+
47
+ // The format gate's own answer, as every command that writes a newer page format asks for
48
+ // it: the command's result when the gate admitted the write, the gate's refusal otherwise.
49
+ async function formatted(view, apply) {
50
+ if (typeof view.runFormatCommand !== "function") return { ok: false, code: "page-format-unsupported" };
51
+ const answer = await view.runFormatCommand(apply);
52
+ return answer?.status === "applied" ? answer.result : { ok: false, code: answer?.code ?? answer?.status ?? "page-format-refused", retry: answer?.retry === true };
53
+ }
54
+ const readDataUrl = (file) => new Promise((ok, no) => { const r = new FileReader(); r.onload = () => ok(r.result); r.onerror = () => no(r.error); r.readAsDataURL(file); });
55
+
56
+ export function createFeature({ view = null, upload = null } = {}) {
57
+ if (!view) return { run: () => ({ ok: false, code: "not-editing" }), dispose() {} };
58
+ const doc = view.dom.ownerDocument;
59
+ let session = null;
60
+
61
+ function close() {
62
+ const s = session;
63
+ if (!s) return;
64
+ session = null;
65
+ for (const [target, type, fn, options] of s.listen) target.removeEventListener(type, fn, options);
66
+ s.abort?.abort();
67
+ s.input?.remove();
68
+ s.ui?.remove();
69
+ s.stopPick?.();
70
+ if (s.state === "position") restore(s);
71
+ }
72
+
73
+ const listen = (s, target, type, fn, options) => {
74
+ target.addEventListener(type, fn, options);
75
+ s.listen.push([target, type, fn, options]);
76
+ };
77
+
78
+ const button = (act, label) => {
79
+ const b = doc.createElement("button");
80
+ b.type = "button";
81
+ b.className = "page-cover-btn";
82
+ b.dataset.act = act;
83
+ b.textContent = label;
84
+ return b;
85
+ };
86
+
87
+ // One bar for the whole session: the upload's progress, its refusal, the embed offer, the
88
+ // reposition's own message. It is runtime only, so nothing of it is in the record.
89
+ function draw(s) {
90
+ const box = s.panel ?? (s.panel = doc.createElement("div"));
91
+ box.className = "page-cover-panel";
92
+ box.dataset.state = s.state;
93
+ box.setAttribute("contenteditable", "false");
94
+ box.setAttribute("role", s.state === "uploading" ? "status" : "alert");
95
+ box.textContent = "";
96
+ const msg = doc.createElement("span");
97
+ msg.className = "page-cover-msg";
98
+ msg.textContent = s.message;
99
+ box.append(msg);
100
+ if (s.state === "uploading") {
101
+ const bar = doc.createElement("span");
102
+ bar.className = "page-cover-bar";
103
+ bar.setAttribute("role", "progressbar");
104
+ bar.setAttribute("aria-valuemin", "0");
105
+ bar.setAttribute("aria-valuemax", "100");
106
+ bar.setAttribute("aria-valuenow", String(s.percent));
107
+ const fill = doc.createElement("span");
108
+ fill.style.width = `${s.percent}%`;
109
+ bar.append(fill);
110
+ box.append(bar, button("cancel", "Cancel"));
111
+ } else if (s.state === "embed-choice") box.append(button("embed", "Keep this image inside the document"), button("cancel", "Close"));
112
+ else if (s.state === "position") box.append(button("apply", "Apply"), button("cancel", "Cancel"));
113
+ else box.append(button("cancel", "Close"));
114
+ if (!s.ui) {
115
+ const host = view.dom.parentElement;
116
+ s.ui = mountEditorUI({ name: "page-cover", content: box, css: [COVER_CSS], parent: host ?? doc.body, before: host ? view.dom : null });
117
+ }
118
+ }
119
+
120
+ function decide(s) {
121
+ listen(s, s.panel, "click", (event) => {
122
+ const b = event.target.closest?.("[data-act]");
123
+ if (!b) return;
124
+ event.preventDefault();
125
+ if (b.dataset.act === "cancel") close();
126
+ else if (b.dataset.act === "apply") apply(s);
127
+ else if (b.dataset.act === "embed") embed(s);
128
+ });
129
+ }
130
+
131
+ // The one place a cover reaches the document. The title the command was asked for is read
132
+ // again here: one that was deleted, or one a heading that moved first displaced, is no
133
+ // longer that title and the command refuses rather than decorating another page. A
134
+ // document with no title grows one heading, whole, in this same transaction.
135
+ function writeCover(owner, fields) {
136
+ const title = titleOf(view.state.doc);
137
+ if ((title ? title.node.attrs.id : null) !== owner) return { ok: false, code: "stale-target" };
138
+ const tr = view.state.tr;
139
+ if (!title) {
140
+ const first = view.state.doc.maybeChild(0);
141
+ tr.insert(0, schema.nodes.heading.create({ id: newId(), pos: keyBetween(null, first?.attrs.pos ?? null), parent: null, level: 1, ...fields }));
142
+ } else tr.setNodeMarkup(title.at, null, { ...title.node.attrs, ...fields });
143
+ view.dispatch(asStep(tr));
144
+ return { ok: true, cover: true };
145
+ }
146
+
147
+ async function commitAndClose(s, fields) {
148
+ const answer = await formatted(view, () => writeCover(s.owner, fields));
149
+ if (session === s) close();
150
+ if (answer?.ok) announce(view, "Cover added");
151
+ return answer;
152
+ }
153
+
154
+ function pick(s) {
155
+ return new Promise((resolve) => {
156
+ const input = doc.createElement("input");
157
+ input.type = "file";
158
+ input.accept = "image/*";
159
+ input.className = "page-cover-input";
160
+ input.hidden = true;
161
+ (view.dom.parentElement ?? doc.body).appendChild(input);
162
+ s.input = input;
163
+ s.stopPick = () => resolve(null);
164
+ let done = false;
165
+ const finish = (file) => { if (done) return; done = true; resolve(file); };
166
+ listen(s, input, "change", () => finish([...(input.files ?? [])][0] ?? null));
167
+ listen(s, input, "cancel", () => finish(null));
168
+ input.click();
169
+ });
170
+ }
171
+
172
+ // The upload is media's own transport: the same host question, the same cancellation, the
173
+ // same progress. Only an `unsupported` host is offered an embed, and only within the size
174
+ // it can carry; every other answer is a refusal and embeds nothing.
175
+ async function send(s, file) {
176
+ const rt = mediaRuntime.get(view);
177
+ const transport = rt?.upload ?? upload;
178
+ if (typeof transport !== "function") {
179
+ close();
180
+ return { ok: false, code: "unavailable" };
181
+ }
182
+ const abort = new AbortController();
183
+ s.abort = abort;
184
+ rt?.aborts.set(s.id, abort);
185
+ s.message = `Uploading ${file.name || "the image"}`;
186
+ draw(s);
187
+ let last = -5;
188
+ const onProgress = ({ percent }) => {
189
+ if (session !== s || (percent - last < 5 && percent !== 100)) return;
190
+ last = percent;
191
+ s.percent = percent;
192
+ draw(s);
193
+ };
194
+ let res;
195
+ try { res = await transport(file, { onProgress, signal: abort.signal }); }
196
+ catch { res = { code: "network", msg: "Upload failed" }; }
197
+ rt?.aborts.delete(s.id);
198
+ if (session !== s || view.isDestroyed) return { ok: false, code: "not-editing" };
199
+ if (res?.ok) {
200
+ const src = safeFileHref(res.uploads?.[0]?.url);
201
+ if (src) return commitAndClose(s, { pageCoverSrc: src, pageCoverAlt: "", pageCoverX: 50, pageCoverY: 50 });
202
+ res = { code: "bad-response", msg: "The upload returned an invalid address" };
203
+ }
204
+ if (!res || typeof res !== "object") res = { code: "bad-response", msg: "The upload returned an invalid response" };
205
+ if (res.code === "aborted") {
206
+ close();
207
+ return { ok: false, code: "cancelled" };
208
+ }
209
+ if (res.code === "unsupported") {
210
+ const keep = /^image\/(png|jpeg|gif|webp)$/i.test(file.type) && file.size <= EMBED_MAX;
211
+ s.state = keep ? "embed-choice" : "error";
212
+ s.file = keep ? file : null;
213
+ s.message = keep
214
+ ? "This host doesn't store files. You can keep this image inside the document, which makes the document larger."
215
+ : "This host doesn't store files, and this image is too large to keep inside the document.";
216
+ draw(s);
217
+ return { ok: false, code: "unsupported" };
218
+ }
219
+ s.state = "error";
220
+ s.message = res.msg || "Upload failed";
221
+ draw(s);
222
+ return { ok: false, code: res.code ?? "error" };
223
+ }
224
+
225
+ async function embed(s) {
226
+ const file = s.file;
227
+ if (!file) return;
228
+ const src = await readDataUrl(file);
229
+ if (session !== s) return;
230
+ const url = safeSrc(src);
231
+ if (typeof url !== "string") {
232
+ s.state = "error";
233
+ s.message = "That image could not be kept inside the document.";
234
+ draw(s);
235
+ return;
236
+ }
237
+ return commitAndClose(s, { pageCoverSrc: url, pageCoverAlt: "", pageCoverX: 50, pageCoverY: 50 });
238
+ }
239
+
240
+ // Add cover: the file, then the host, then one write. The command answers with the file
241
+ // chooser it opened, the way a picker does; nothing is written before the host answers,
242
+ // and nothing at all if the title moved, the format gate refused, or the edit left while
243
+ // the upload was in the air.
244
+ function add() {
245
+ close();
246
+ const s = { id: newId("c"), owner: titleOf(view.state.doc)?.node.attrs.id ?? null, state: "uploading", percent: 0, message: "Choosing a cover", listen: [] };
247
+ session = s;
248
+ listen(s, doc, "keydown", (event) => {
249
+ if (session !== s || event.key !== "Escape") return;
250
+ event.preventDefault();
251
+ event.stopPropagation();
252
+ close();
253
+ }, true);
254
+ draw(s);
255
+ decide(s);
256
+ pick(s).then((file) => {
257
+ if (session !== s) return;
258
+ if (!file) close();
259
+ else send(s, file);
260
+ });
261
+ return { ok: true, picker: true };
262
+ }
263
+
264
+ const coverImg = () => view.dom.querySelector("header[data-page-heading] img[data-page-cover]");
265
+
266
+ function preview(s) {
267
+ const img = coverImg();
268
+ if (img) img.setAttribute("style", `object-position:${s.x}% ${s.y}%`);
269
+ }
270
+
271
+ function restore(s) {
272
+ const title = titleOf(view.state.doc);
273
+ const img = coverImg();
274
+ if (!img || !title || title.node.attrs.id !== s.owner) return;
275
+ img.setAttribute("style", `object-position:${title.node.attrs.pageCoverX}% ${title.node.attrs.pageCoverY}%`);
276
+ }
277
+
278
+ // The reposition commits once, for the block it was opened on, and only after the gate.
279
+ async function apply(s) {
280
+ if (session !== s) return { ok: false, code: "not-editing" };
281
+ const { x, y } = s;
282
+ close();
283
+ return formatted(view, () => writeCover(s.owner, { pageCoverX: x, pageCoverY: y }));
284
+ }
285
+
286
+ // Reposition: the picture moves under the hand that placed it and the record does not,
287
+ // until one commit on release, on Enter or on Apply. Escape puts it back.
288
+ function reposition() {
289
+ close();
290
+ const title = titleOf(view.state.doc);
291
+ if (!title || !title.node.attrs.pageCoverSrc) return { ok: false, code: "no-cover" };
292
+ const s = {
293
+ owner: title.node.attrs.id, state: "position", listen: [],
294
+ x: title.node.attrs.pageCoverX, y: title.node.attrs.pageCoverY,
295
+ message: "Drag the picture or use the arrow keys, then Apply",
296
+ };
297
+ session = s;
298
+ draw(s);
299
+ decide(s);
300
+ listen(s, doc, "keydown", (event) => {
301
+ const stop = () => { event.preventDefault(); event.stopPropagation(); };
302
+ if (event.key === "Escape") { stop(); close(); }
303
+ else if (event.key === "Enter") { stop(); apply(s); }
304
+ else if (event.key === "ArrowLeft") { stop(); s.x = clamp(s.x - STEP); preview(s); }
305
+ else if (event.key === "ArrowRight") { stop(); s.x = clamp(s.x + STEP); preview(s); }
306
+ else if (event.key === "ArrowUp") { stop(); s.y = clamp(s.y - STEP); preview(s); }
307
+ else if (event.key === "ArrowDown") { stop(); s.y = clamp(s.y + STEP); preview(s); }
308
+ }, true);
309
+ const img = coverImg();
310
+ if (img) listen(s, img, "pointerdown", (event) => {
311
+ const rect = img.getBoundingClientRect();
312
+ if (!rect.width || !rect.height) return;
313
+ event.preventDefault();
314
+ const at = { px: event.clientX, py: event.clientY, x: s.x, y: s.y };
315
+ listen(s, doc, "pointermove", (move) => {
316
+ s.x = clamp(at.x + ((move.clientX - at.px) / rect.width) * 100);
317
+ s.y = clamp(at.y + ((move.clientY - at.py) / rect.height) * 100);
318
+ preview(s);
319
+ });
320
+ listen(s, doc, "pointerup", () => apply(s));
321
+ });
322
+ return { ok: true, position: true };
323
+ }
324
+
325
+ return {
326
+ run(args) {
327
+ return args?.act === "position" ? reposition() : add();
328
+ },
329
+ dispose() { close(); },
330
+ };
331
+ }
@@ -0,0 +1,213 @@
1
+ // Page icon: the one glyph a page shows beside its title. It is a scalar attribute of the
2
+ // title heading, never a second store, so a reader draws it as ordinary Unicode text with
3
+ // nothing of this loaded and the title's own words stay the title. The picker is the emoji
4
+ // feature's, fetched the first time it opens; this module loads when a document has an icon
5
+ // or somebody asks for one, and the command writes through the format gate.
6
+ import { schema } from "../schema.js";
7
+ import { newId, keyBetween } from "../model.js";
8
+ import { asStep } from "../blocksel.js";
9
+ import { composing } from "../markdown.js";
10
+ import { createMentionPicker } from "../mention-picker.js";
11
+ import { runFormatted } from "../format-command.js";
12
+
13
+ const TITLE_CSS = `
14
+ [data-page-heading]{display:block}
15
+ [data-page-heading]>span[data-page-icon]{display:inline-block;font-size:34px;line-height:1;margin-block-end:.2em}
16
+ [data-page-heading]>img[data-page-cover]{display:block;width:100%;height:180px;object-fit:cover;border-radius:var(--page-image-radius, 6px);margin-block-end:.6em}
17
+ [data-page-heading]>h1{font-family:var(--page-font-display);font-size:var(--page-h1-size);font-weight:var(--page-h1-weight);letter-spacing:var(--page-h1-tracking);line-height:var(--page-h1-line);color:var(--page-ink-strong);margin-block:0 .6em}
18
+ `;
19
+
20
+ // The emoji feature, fetched on first use: one list of glyphs, one way of finding one and
21
+ // one rule for what a single glyph is. A fetch that failed is not kept, so it is asked for
22
+ // again the next time.
23
+ let emojiParts = null;
24
+ const emojiModule = () => (emojiParts ??= import("./emoji.js").catch((error) => { emojiParts = null; throw error; }));
25
+
26
+ // One glyph, as a person sees it: a joined family is one, a skin tone is one, a keycap is
27
+ // one, and two letters are two. Without a segmenter this refuses rather than cutting a
28
+ // glyph in half, and the stored value is bounded in code points and bytes.
29
+ export function isIconGlyph(value) {
30
+ const text = typeof value === "string" ? value : "";
31
+ if (!text.trim() || typeof Intl === "undefined" || typeof Intl.Segmenter !== "function") return false;
32
+ if ([...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(text)].length !== 1) return false;
33
+ return [...text].length <= 32 && new TextEncoder().encode(text).length <= 128;
34
+ }
35
+
36
+ // The title is the document's first root H1, and only it carries a page icon: a nested H1
37
+ // is no title, and a second root H1 becomes the title only when it moves first.
38
+ export function titleOf(doc) {
39
+ let at = 0;
40
+ for (let i = 0; i < doc.childCount; i++) {
41
+ const node = doc.child(i);
42
+ if (node.type === schema.nodes.heading && node.attrs.level === 1 && node.attrs.parent == null) return { index: i, at, node };
43
+ at += node.nodeSize;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ // The cover the page shows above its title: an ordinary image whose focal point is the
49
+ // part of it a person kept in view, so a reader draws it with nothing of this loaded.
50
+ function coverImg(doc, attrs) {
51
+ const img = doc.createElement("img");
52
+ img.setAttribute("data-page-cover", "");
53
+ img.setAttribute("src", attrs.pageCoverSrc);
54
+ img.setAttribute("alt", attrs.pageCoverAlt ?? "");
55
+ img.setAttribute("style", `object-position:${attrs.pageCoverX}% ${attrs.pageCoverY}%`);
56
+ return img;
57
+ }
58
+
59
+ // The decorated heading: the block's id on the wrapper and each part of the decoration
60
+ // beside the `h1` the text lives in. A heading with no icon draws the plain heading it
61
+ // always did, and one with a different glyph, cover or level is a new decoration, so the
62
+ // view is rebuilt from the node. The cover is drawn here too: it is the same wrapper, one
63
+ // part of it, not a second view.
64
+ class TitleHeadingView {
65
+ constructor(node, view) {
66
+ this.node = node;
67
+ this.decorated = !!(node.attrs.pageIcon || node.attrs.pageCoverSrc);
68
+ const doc = view.dom.ownerDocument;
69
+ const head = doc.createElement(`h${node.attrs.level}`);
70
+ head.setAttribute("data-id", node.attrs.id);
71
+ this.dom = this.contentDOM = head;
72
+ if (!this.decorated) return;
73
+ const box = doc.createElement("header");
74
+ box.setAttribute("data-id", node.attrs.id);
75
+ box.setAttribute("data-page-heading", "1");
76
+ head.removeAttribute("data-id");
77
+ if (node.attrs.pageCoverSrc) box.append(coverImg(doc, node.attrs));
78
+ if (node.attrs.pageIcon) {
79
+ const mark = doc.createElement("span");
80
+ mark.setAttribute("data-page-icon", "");
81
+ mark.setAttribute("aria-hidden", "true");
82
+ mark.textContent = node.attrs.pageIcon;
83
+ box.append(mark);
84
+ }
85
+ box.append(head);
86
+ this.dom = box;
87
+ }
88
+ update(node) {
89
+ if (this.decorated !== !!(node.attrs.pageIcon || node.attrs.pageCoverSrc) || !node.sameMarkup(this.node)) return false;
90
+ this.node = node;
91
+ return true;
92
+ }
93
+ ignoreMutation(mutation) {
94
+ return !this.contentDOM.contains(mutation.target);
95
+ }
96
+ }
97
+
98
+ export function createFeature({ view = null } = {}) {
99
+ if (!view) return { run: () => ({ ok: false, code: "not-editing" }), dispose() {} };
100
+ const doc = view.dom.ownerDocument;
101
+ let library = null;
102
+ let session = null;
103
+
104
+ function place() {
105
+ try {
106
+ const box = view.dom.parentElement?.getBoundingClientRect?.() ?? { left: 0, top: 0 };
107
+ const at = view.coordsAtPos(view.state.selection.from);
108
+ return { x: Math.max(8, at.left - box.left), y: at.bottom + 6 - box.top };
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ function anchor(s) {
115
+ const id = s.picker.activeId();
116
+ if (id) view.dom.setAttribute("aria-activedescendant", id);
117
+ else if (view.dom.getAttribute("aria-activedescendant")?.startsWith(s.picker.el.id)) view.dom.removeAttribute("aria-activedescendant");
118
+ }
119
+
120
+ function close() {
121
+ const s = session;
122
+ if (!s) return;
123
+ session = null;
124
+ for (const [target, type, fn, options] of s.listen) target.removeEventListener(type, fn, options);
125
+ if (view.dom.getAttribute("aria-activedescendant")?.startsWith(`${s.picker.el.id}-`)) view.dom.removeAttribute("aria-activedescendant");
126
+ s.picker.destroy();
127
+ }
128
+
129
+ // The one place an icon reaches the document. The title the command was asked for is read
130
+ // again here: one that was deleted, or one a heading that moved first displaced, is no
131
+ // longer that title and the command refuses rather than decorating another page. A
132
+ // document with no title grows one heading, whole, in this same transaction.
133
+ function writeIcon(s, char) {
134
+ const title = titleOf(view.state.doc);
135
+ const owner = title ? title.node.attrs.id : null;
136
+ if (owner !== s.owner) return { ok: false, code: "stale-target" };
137
+ const tr = view.state.tr;
138
+ if (!title) {
139
+ const first = view.state.doc.maybeChild(0);
140
+ tr.insert(0, schema.nodes.heading.create({ id: newId(), pos: keyBetween(null, first?.attrs.pos ?? null), parent: null, level: 1, pageIcon: char }));
141
+ } else tr.setNodeMarkup(title.at, null, { ...title.node.attrs, pageIcon: char });
142
+ view.dispatch(asStep(tr));
143
+ return { ok: true, icon: char };
144
+ }
145
+
146
+ async function choose(index) {
147
+ const s = session;
148
+ if (!s) return { ok: false, code: "not-editing" };
149
+ const entry = s.picker.personAt(index);
150
+ close();
151
+ if (!entry) return { ok: false, code: "no-emoji" };
152
+ if (!isIconGlyph(entry.char)) return { ok: false, code: "invalid-icon" };
153
+ return runFormatted(view, () => writeIcon(s, entry.char));
154
+ }
155
+
156
+ function key(event) {
157
+ const s = session;
158
+ if (!s || composing(event, view)) return;
159
+ const stop = () => { event.preventDefault(); event.stopPropagation(); };
160
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
161
+ stop();
162
+ s.picker.move(event.key === "ArrowDown" ? 1 : -1);
163
+ anchor(s);
164
+ } else if (event.key === "Enter") {
165
+ stop();
166
+ choose(s.picker.index());
167
+ } else if (event.key === "Escape" || event.key === "Tab") {
168
+ stop();
169
+ close();
170
+ }
171
+ }
172
+
173
+ // The Add icon command: the emoji list, opened for the title. It writes nothing until one
174
+ // is chosen, and nothing at all for a title that changed while the list was open.
175
+ async function open(args) {
176
+ close();
177
+ const s = { owner: titleOf(view.state.doc)?.node.attrs.id ?? null, picker: null, listen: [] };
178
+ s.picker = createMentionPicker({ document: doc, label: "Choose a page icon", empty: "No emoji found", onPick: (index) => choose(index) });
179
+ session = s;
180
+ const listen = (target, type, fn, options) => {
181
+ target.addEventListener(type, fn, options);
182
+ s.listen.push([target, type, fn, options]);
183
+ };
184
+ listen(doc, "keydown", (event) => key(event), true);
185
+ listen(view.dom, "input", () => close());
186
+ listen(doc, "mousedown", (event) => { if (!s.picker.el.contains(event.target)) close(); }, true);
187
+ (view.dom.parentElement ?? doc.body).appendChild(s.picker.el);
188
+
189
+ let list, filter;
190
+ try {
191
+ const mod = await emojiModule();
192
+ filter = mod.filterEmoji;
193
+ library ??= mod.emojiLibrary();
194
+ list = await library();
195
+ } catch (error) {
196
+ close();
197
+ return { ok: false, code: "unavailable", error };
198
+ }
199
+ if (session !== s) return { ok: false, code: "not-editing" };
200
+ s.picker.show(filter(list, args?.query), place());
201
+ anchor(s);
202
+ return { ok: true, picker: true };
203
+ }
204
+
205
+ return {
206
+ nodeViews: { heading: (node, editorView) => new TitleHeadingView(node, editorView) },
207
+ css: TITLE_CSS,
208
+ async run(args) {
209
+ return open(args);
210
+ },
211
+ dispose() { close(); },
212
+ };
213
+ }
@@ -0,0 +1,87 @@
1
+ // The table of contents' node view: one `nav` of links to the document's headings,
2
+ // derived from the document itself. Loaded the first time a document has or takes one.
3
+ //
4
+ // Following a link reveals what a closed toggle hides without writing anything: in the
5
+ // editor the reveal lives in the collapse plugin's own state, so nothing is saved, no
6
+ // undo step is made and no peer is told.
7
+ import { state } from "../../vendor/prosemirror.vendor.js";
8
+ import { schema } from "../schema.js";
9
+ import { blockMap } from "../model.js";
10
+ import { project } from "../projection.js";
11
+ import { outline, revealToggles } from "../blocks.js";
12
+ import { announce } from "../announce.js";
13
+ import { idFromHref, tocHtml } from "../toc.js";
14
+
15
+ const { TextSelection } = state;
16
+
17
+ // Show the block `id` and open the collapsed toggles above it for this reader only. A
18
+ // target the document no longer holds is a no-op that says so.
19
+ export function followTocLink(view, id) {
20
+ const ents = outline(view.state.doc);
21
+ const i = ents.findIndex((e) => e.node.attrs.id === id);
22
+ if (i < 0) {
23
+ announce(view, "That link's block is gone");
24
+ return null;
25
+ }
26
+ const closed = [];
27
+ for (let up = ents[i].up; up >= 0; up = ents[up].up) {
28
+ if (ents[up].node.type === schema.nodes.toggle && !ents[up].open) closed.push(ents[up].node.attrs.id);
29
+ }
30
+ const tr = view.state.tr
31
+ .setSelection(TextSelection.create(view.state.doc, ents[i].at + 1))
32
+ .setMeta("addToHistory", false);
33
+ view.dispatch(revealToggles(tr, closed).scrollIntoView());
34
+ return ents[i];
35
+ }
36
+
37
+ export class TocView {
38
+ constructor(node, view) {
39
+ this.node = node;
40
+ this.view = view;
41
+ this.doc = view.state.doc;
42
+ const doc = view.dom.ownerDocument;
43
+ this.dom = doc.createElement("div");
44
+ this.dom.className = "page-toc";
45
+ this.dom.contentEditable = "false";
46
+ this.dom.setAttribute("role", "group");
47
+ this.dom.setAttribute("aria-label", "Table of contents");
48
+ this.dom.addEventListener("mousedown", (event) => { if (event.target.closest?.("a")) event.preventDefault(); });
49
+ this.dom.addEventListener("click", (event) => {
50
+ const link = event.target.closest?.("a");
51
+ if (!link) return;
52
+ event.preventDefault();
53
+ const id = idFromHref(link.getAttribute("href"));
54
+ if (id !== null) followTocLink(this.view, id);
55
+ });
56
+ this.render();
57
+ }
58
+ // ProseMirror calls this on every state update; the entries are rebuilt only when the
59
+ // document itself changed, and then from one projection of it.
60
+ update(node) {
61
+ this.node = node;
62
+ if (this.view.state.doc !== this.doc) {
63
+ this.doc = this.view.state.doc;
64
+ this.render();
65
+ }
66
+ return true;
67
+ }
68
+ render() {
69
+ const html = tocHtml(this.node.attrs, project(blockMap(this.doc)));
70
+ if (this.dom.innerHTML !== html) this.dom.innerHTML = html;
71
+ }
72
+ stopEvent(event) {
73
+ return !!event.target.closest?.("a");
74
+ }
75
+ ignoreMutation() {
76
+ return true;
77
+ }
78
+ }
79
+
80
+ export function createFeature() {
81
+ return {
82
+ id: "toc",
83
+ nodeViews: { toc: (node, view) => new TocView(node, view) },
84
+ css: null,
85
+ dispose() {},
86
+ };
87
+ }
@@ -1,9 +1,33 @@
1
+ import { announce } from "./announce.js";
2
+ import toast from "../ui/toast-lazy.js";
3
+
4
+ // A refusal the person cannot see is the same as a command that did nothing, so the
5
+ // gate's "no" is said out loud. Only the gate's own refusal: a command the gate let
6
+ // through reports its own result to its caller.
7
+ const UNSUPPORTED = new Set(["page-format-unsupported", "unsupported-host"]);
8
+ const UNSUPPORTED_TEXT = "This host can't store that yet, so nothing was added.";
9
+ const RETRY_TEXT = "That didn't go through. Try again.";
10
+ const REFUSED_TEXT = "That couldn't be added here.";
11
+
12
+ function say(view, answer) {
13
+ const text = answer?.status === "upgrade-required" && answer.message ? answer.message
14
+ : UNSUPPORTED.has(answer?.code) || UNSUPPORTED.has(answer?.status) ? UNSUPPORTED_TEXT
15
+ : answer?.retry === true ? RETRY_TEXT : REFUSED_TEXT;
16
+ if (view.isDestroyed) return;
17
+ announce(view, text);
18
+ toast(text, "error");
19
+ }
20
+
1
21
  // A command that writes a newer page format asks the format gate first. The answer is
2
22
  // the command's own result when the gate let it apply, or the gate's refusal, so the
3
23
  // caller writes nothing on a refusal.
4
24
  export async function runFormatted(view, apply) {
5
- if (typeof view.runFormatCommand !== "function") return { ok: false, code: "page-format-unsupported" };
25
+ if (typeof view.runFormatCommand !== "function") {
26
+ say(view, { code: "page-format-unsupported" });
27
+ return { ok: false, code: "page-format-unsupported" };
28
+ }
6
29
  const answer = await view.runFormatCommand(apply);
7
30
  if (answer?.status === "applied") return answer.result;
31
+ say(view, answer);
8
32
  return { ok: false, code: answer?.code ?? answer?.status ?? "page-format-refused", retry: answer?.retry === true };
9
33
  }