@panphora/clayjs 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +5570 -4173
  11. package/dist/m/attrs/{save-freeze-MH3AOD56.js → save-freeze-PUYX32AK.js} +1 -1
  12. package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
  13. package/dist/m/c-2EPHRF47.js +1 -0
  14. package/dist/m/{c-JD2VFAYG.js → c-2SUFMUZX.js} +2 -2
  15. package/dist/m/{c-CITLTVQ4.js → c-2TJCFPX4.js} +1 -1
  16. package/dist/m/c-33SS5A4Y.js +1 -0
  17. package/dist/m/{c-V2FPBRO6.js → c-34J5HC7N.js} +1 -1
  18. package/dist/m/c-3F3AADKG.js +1 -0
  19. package/dist/m/{c-TCFVG3I4.js → c-3S3647J6.js} +1 -1
  20. package/dist/m/{c-GY4BREDT.js → c-3S5TNRZA.js} +1 -1
  21. package/dist/m/{c-GDNZEOOZ.js → c-3WX5FABT.js} +1 -1
  22. package/dist/m/{c-AI3H6ETU.js → c-3XGLJAS7.js} +2 -2
  23. package/dist/m/c-42WBOKFM.js +1 -0
  24. package/dist/m/c-47M4QHCK.js +1 -0
  25. package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
  26. package/dist/m/{c-GEAG5XAK.js → c-4TP3UXBT.js} +1 -1
  27. package/dist/m/{c-IESDKFO6.js → c-5ELELG2F.js} +1 -1
  28. package/dist/m/{c-X3V4RWMA.js → c-5ZYLTN3Q.js} +1 -1
  29. package/dist/m/{c-5PZYDWTW.js → c-67TBU56I.js} +1 -1
  30. package/dist/m/{c-WNOLMHI2.js → c-6BGJAJ67.js} +1 -1
  31. package/dist/m/{c-4Q6QIODW.js → c-755ZOHOM.js} +1 -1
  32. package/dist/m/c-7Q4XYDF4.js +1 -0
  33. package/dist/m/c-7RHOMAMR.js +1 -0
  34. package/dist/m/c-A5LASUEZ.js +1 -0
  35. package/dist/m/{c-IL5IVTOG.js → c-ABOBD7F3.js} +1 -1
  36. package/dist/m/c-AQVQKFKB.js +1 -0
  37. package/dist/m/{c-QDVG4NAZ.js → c-ARO6CPMA.js} +1 -1
  38. package/dist/m/{c-ORLPQFNU.js → c-AZM34SPO.js} +1 -1
  39. package/dist/m/{c-XUZMRQ7O.js → c-BCS3AZQZ.js} +1 -1
  40. package/dist/m/c-BOS7GONO.js +1 -0
  41. package/dist/m/{c-SUAC5RSG.js → c-C5UGNOC2.js} +1 -1
  42. package/dist/m/c-C6DXZIXD.js +1 -0
  43. package/dist/m/{c-WSTUCBHV.js → c-CPRGR2II.js} +1 -1
  44. package/dist/m/{c-NICICT4P.js → c-DEYHCYJB.js} +1 -1
  45. package/dist/m/{c-WGQVSKKC.js → c-DYTBSJBI.js} +1 -1
  46. package/dist/m/{c-DT7E76Z2.js → c-DZT6W2NG.js} +1 -1
  47. package/dist/m/{c-O2QZDVZ2.js → c-EEWPNUM2.js} +1 -1
  48. package/dist/m/{c-MFB74XVN.js → c-EHAR4RJJ.js} +2 -2
  49. package/dist/m/c-EHJWIGWN.js +1 -0
  50. package/dist/m/c-EIDZHQHZ.js +1 -0
  51. package/dist/m/{c-V2V5FKXX.js → c-EIUKME3W.js} +1 -1
  52. package/dist/m/c-ELXMSEN5.js +1 -0
  53. package/dist/m/c-EXZOA6XY.js +7 -0
  54. package/dist/m/{c-FKTALXNV.js → c-EYFMAR3M.js} +1 -1
  55. package/dist/m/c-FC5RUFPR.js +1 -0
  56. package/dist/m/{c-FBOO2UY3.js → c-FUN6AKFE.js} +1 -1
  57. package/dist/m/{c-V4B3RUWB.js → c-GFASNR55.js} +1 -1
  58. package/dist/m/{c-XMIDHEKF.js → c-GY36NO7C.js} +1 -1
  59. package/dist/m/{c-UD4VDBGJ.js → c-H3M6UDNV.js} +1 -1
  60. package/dist/m/{c-SFZRY6RT.js → c-H47C6PJZ.js} +1 -1
  61. package/dist/m/{c-MUM53J5J.js → c-H4M2T4A6.js} +1 -1
  62. package/dist/m/{c-U7F66I64.js → c-HYNM5U6B.js} +1 -1
  63. package/dist/m/c-J2YYLEVM.js +1 -0
  64. package/dist/m/c-J2ZW4HMR.js +1 -0
  65. package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
  66. package/dist/m/c-K2ZHQMPF.js +1 -0
  67. package/dist/m/c-KD2ME5UN.js +2 -0
  68. package/dist/m/{c-Z2OZFX3N.js → c-L6KVXPBO.js} +1 -1
  69. package/dist/m/{c-GO635CRU.js → c-LAKBBJOU.js} +2 -2
  70. package/dist/m/{c-4AE2A2LK.js → c-LETPPG2F.js} +1 -1
  71. package/dist/m/{c-SFYTKRU7.js → c-MKAUC6DB.js} +1 -1
  72. package/dist/m/{c-WDFQ7PBU.js → c-MMEVDN6D.js} +1 -1
  73. package/dist/m/{c-LS7IVQVW.js → c-MMV5JULA.js} +1 -1
  74. package/dist/m/{c-NEQRFC4O.js → c-MVVEIJUU.js} +1 -1
  75. package/dist/m/{c-SXZYVXD6.js → c-N362HNGO.js} +1 -1
  76. package/dist/m/{c-7K4SCQB7.js → c-ND3W7D5U.js} +1 -1
  77. package/dist/m/{c-3E2IK5HU.js → c-NNHNXD6M.js} +1 -1
  78. package/dist/m/{c-D62HRSZK.js → c-NYJ577NL.js} +1 -1
  79. package/dist/m/{c-YFO6ZJLQ.js → c-O3DPFZCE.js} +1 -1
  80. package/dist/m/c-OZOACVUD.js +1 -0
  81. package/dist/m/{c-MV7TEUTD.js → c-OZVK3TFI.js} +1 -1
  82. package/dist/m/{c-ZEA3DCG3.js → c-PWILUN4T.js} +1 -1
  83. package/dist/m/{c-BCWYEG6P.js → c-QA3Q3MDI.js} +1 -1
  84. package/dist/m/{c-XHZIKRCV.js → c-QKOKMQXA.js} +1 -1
  85. package/dist/m/{c-TGIMBAWE.js → c-RBWVF4N5.js} +1 -1
  86. package/dist/m/c-RES2S5Q3.js +1 -0
  87. package/dist/m/c-RFGELYEU.js +2 -0
  88. package/dist/m/c-RGFPLQGV.js +1 -0
  89. package/dist/m/{c-RGAALCDU.js → c-SPOEDIMP.js} +1 -1
  90. package/dist/m/{c-YX2MC7YL.js → c-SRW5OBCF.js} +1 -1
  91. package/dist/m/{c-C6YZNU6E.js → c-SYOA4T7V.js} +1 -1
  92. package/dist/m/c-SYVF6ZMF.js +1 -0
  93. package/dist/m/{c-NADLYVAH.js → c-TJ5PFOJ2.js} +1 -1
  94. package/dist/m/{c-KYTVZT4D.js → c-TKV273YU.js} +1 -1
  95. package/dist/m/c-U35T5NAY.js +1 -0
  96. package/dist/m/{c-PRZNJUYZ.js → c-UEKJWXCB.js} +2 -2
  97. package/dist/m/c-UW6MMZ4V.js +1 -0
  98. package/dist/m/{c-XQTERKQA.js → c-V4DBV5ZV.js} +1 -1
  99. package/dist/m/c-VGG3UR7I.js +1 -0
  100. package/dist/m/c-VU4WEWPH.js +4 -0
  101. package/dist/m/{c-5Q5XBN56.js → c-W7LWCJHD.js} +1 -1
  102. package/dist/m/c-WHMZCRQD.js +6 -0
  103. package/dist/m/{c-WMRJKQ2V.js → c-WHQUZM2V.js} +1 -1
  104. package/dist/m/c-WOJ2RTV4.js +1 -0
  105. package/dist/m/{c-ZJG6C2WL.js → c-WOWGGNCJ.js} +1 -1
  106. package/dist/m/{c-ZORVIXUR.js → c-XHD7XJTE.js} +1 -1
  107. package/dist/m/c-XONFK3WC.js +1 -0
  108. package/dist/m/{c-HGG7M2S3.js → c-Y3J5LDKB.js} +1 -1
  109. package/dist/m/{c-JB5YU57S.js → c-Y3MPVURA.js} +1 -1
  110. package/dist/m/c-Y6HH7LTM.js +1 -0
  111. package/dist/m/{c-52OC3OH3.js → c-YRDKVEGR.js} +1 -1
  112. package/dist/m/c-YYIN4NAF.js +30 -0
  113. package/dist/m/{c-WVCT3ITX.js → c-YZGWSCMT.js} +1 -1
  114. package/dist/m/c-ZFAPQQIB.js +1 -0
  115. package/dist/m/core/{admin-attrs-ZI6K6UGM.js → admin-attrs-43JBA6JB.js} +1 -1
  116. package/dist/m/core/{autosave-BSM2ABJL.js → autosave-HD7Y7UNV.js} +1 -1
  117. package/dist/m/core/{conflict-notice-7EQLDYPA.js → conflict-notice-UNDOD4BL.js} +1 -1
  118. package/dist/m/core/{persist-2SZO3PCE.js → persist-MX3VYVBN.js} +1 -1
  119. package/dist/m/core/{save-K6Q47J2X.js → save-JZ7R4ET2.js} +1 -1
  120. package/dist/m/core/{save-core-U3Z44XBO.js → save-core-I4N6S4ED.js} +1 -1
  121. package/dist/m/core/site-edits-C22O53GS.js +1 -0
  122. package/dist/m/core/{snapshot-PWCHYFL6.js → snapshot-N5MKDGNI.js} +1 -1
  123. package/dist/m/core/{unsaved-warning-2WRZLJH5.js → unsaved-warning-BP6ZY3PG.js} +1 -1
  124. package/dist/m/internals/{index-FSLC4M7J.js → index-3LAVREAU.js} +1 -1
  125. package/dist/m/loader-DCKCZKY6.js +1 -0
  126. package/dist/m/manifest-7954CBE3.js +1 -0
  127. package/dist/m/manifest-edit-93AA79D7.js +1 -0
  128. package/dist/m/manifest.json +1240 -1179
  129. package/dist/m/page/{editor-PKSPJ2XJ.js → editor-UF3CIA6V.js} +5 -5
  130. package/dist/m/plugins/{ai-edit-3LG6MGS3.js → ai-edit-WDV2GLDU.js} +2 -2
  131. package/dist/m/plugins/{cms-T2HLI7JG.js → cms-KG6TCTR3.js} +9 -9
  132. package/dist/m/plugins/{derive-7DR5TUDH.js → derive-R4USHZ7P.js} +1 -1
  133. package/dist/m/plugins/{indicator-CTKX4EY7.js → indicator-OLPZ4WFI.js} +1 -1
  134. package/dist/m/plugins/{live-IFLL6YN5.js → live-DF5YS5AH.js} +1 -1
  135. package/dist/m/plugins/{menu-YJZ3JWBC.js → menu-ESEFQARM.js} +1 -1
  136. package/dist/m/plugins/page-GZSU77JS.js +2 -0
  137. package/dist/m/plugins/{page-view-NMCCU6MF.js → page-view-7MM3FEAR.js} +1 -1
  138. package/dist/m/plugins/{people-6WIWCZEG.js → people-EBWE337I.js} +1 -1
  139. package/dist/m/plugins/quickcrop-UHURKA2F.js +26 -0
  140. package/dist/m/plugins/richclay-MXH2GFAZ.js +549 -0
  141. package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
  142. package/dist/m/plugins/{source-7LSGOV46.js → source-OYHDXKTB.js} +1 -1
  143. package/dist/m/plugins/undo-KLHAGV75.js +1 -0
  144. package/dist/m/plugins/{upgrade-MMPZUCDM.js → upgrade-SZQJYEQQ.js} +1 -1
  145. package/dist/m/plugins/{visitor-drafts-V5YB7XDH.js → visitor-drafts-ZE6PCAFP.js} +2 -2
  146. package/dist/m/plugins/{visitor-menu-QDV2MYJN.js → visitor-menu-5XY57JQA.js} +1 -1
  147. package/dist/m/plugins/{wire-3YGUO3GE.js → wire-S5ATC4QI.js} +1 -1
  148. package/dist/m/reader-core-AHOAP5DH.js +77 -0
  149. package/dist/m/sync/{live-sync-XMXXZS3G.js → live-sync-RZQSDMZW.js} +1 -1
  150. package/dist/m/ui/index-G7ZJXNMG.js +1 -0
  151. package/dist/m/vendor/{sap-core.vendor-JANUQULT.js → sap-core.vendor-5TWOJYVM.js} +4 -4
  152. package/dist/offline-manifest.json +780 -750
  153. package/dist/server/core.cjs +4245 -2614
  154. package/dist/server/markdown.cjs +1 -0
  155. package/dist/server/markdown.mjs +7 -0
  156. package/docs/guide.md +26 -37
  157. package/entries/clay-data.js +1 -1
  158. package/entries/sap.js +5 -5
  159. package/package.json +6 -2
  160. package/packed-contract.json +3 -0
  161. package/src/attrs/save-freeze.js +1 -1
  162. package/src/core/admin-contenteditable.js +1 -1
  163. package/src/core/admin-inputs.js +1 -1
  164. package/src/core/admin-onclick.js +1 -1
  165. package/src/core/admin-resources.js +5 -2
  166. package/src/core/conflict-notice-ui.js +59 -34
  167. package/src/core/save.js +9 -9
  168. package/src/core/site-edits.js +1 -1
  169. package/src/core/snapshot.js +56 -43
  170. package/src/core/stale-host-notice.js +20 -7
  171. package/src/loader-logic.js +0 -13
  172. package/src/page/boot-pack.js +1 -0
  173. package/src/page/editor.js +10 -4
  174. package/src/page/history-intent.js +111 -4
  175. package/src/page/import-markdown.js +22 -8
  176. package/src/page/save-projection.js +1 -1
  177. package/src/page/ybind/bridge.js +14 -7
  178. package/src/page/ybind/moves.js +437 -3
  179. package/src/page/ybind/origins.js +1 -1
  180. package/src/page/ybind/recovery.js +1 -1
  181. package/src/plugins/page.js +48 -4
  182. package/src/plugins/quickcrop.js +128 -69
  183. package/src/plugins/richclay.js +136 -0
  184. package/src/plugins/sortable.js +25 -9
  185. package/src/plugins/undo.js +169 -7
  186. package/src/recovery/undo.js +14 -7
  187. package/src/server/markdown/index.js +23 -0
  188. package/src/sync/conflicts.js +3 -1
  189. package/src/sync/data-guard-notice.js +137 -18
  190. package/src/sync/live-sync.js +24 -0
  191. package/src/ui/bevel-controls.js +4 -1
  192. package/src/ui/dialogs.js +11 -7
  193. package/src/ui/modal.js +9 -7
  194. package/src/ui/toast.js +67 -32
  195. package/src/ui/vendor-skin.js +9 -4
  196. package/src/vendor/hyper-html-api-data.vendor.js +1 -1
  197. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  198. package/src/vendor/hyper-undo.vendor.js +1 -1
  199. package/src/vendor/hypercms.vendor.js +7 -7
  200. package/src/vendor/richclay.vendor.js +16 -16
  201. package/src/vendor/sap-core.vendor.js +4 -4
  202. package/tool-generation/context.md +69 -0
  203. package/tool-generation/system.md +1 -1
  204. package/dist/m/c-277QP5ET.js +0 -1
  205. package/dist/m/c-5SZ6KFR5.js +0 -1
  206. package/dist/m/c-CU33V6RW.js +0 -4
  207. package/dist/m/c-CVSWLX3X.js +0 -1
  208. package/dist/m/c-DXPOFLEJ.js +0 -1
  209. package/dist/m/c-EODTTNVA.js +0 -1
  210. package/dist/m/c-ESK4S67I.js +0 -1
  211. package/dist/m/c-FDWR3FL7.js +0 -1
  212. package/dist/m/c-FJDTDGDA.js +0 -1
  213. package/dist/m/c-FYDKYW6E.js +0 -1
  214. package/dist/m/c-HKSLOVU4.js +0 -1
  215. package/dist/m/c-IJHF4IO6.js +0 -1
  216. package/dist/m/c-IUL7PU4P.js +0 -1
  217. package/dist/m/c-JWW6BHWD.js +0 -6
  218. package/dist/m/c-K4M76VPM.js +0 -7
  219. package/dist/m/c-M7ITYKMF.js +0 -1
  220. package/dist/m/c-MR75YPRT.js +0 -1
  221. package/dist/m/c-NFQFV7OP.js +0 -2
  222. package/dist/m/c-NHXOBD6A.js +0 -1
  223. package/dist/m/c-NVU3DORS.js +0 -1
  224. package/dist/m/c-PNZIJFG7.js +0 -1
  225. package/dist/m/c-QWB53KRW.js +0 -1
  226. package/dist/m/c-RHPHMTF2.js +0 -2
  227. package/dist/m/c-RWJ3OSCP.js +0 -1
  228. package/dist/m/c-SEVOS4UE.js +0 -1
  229. package/dist/m/c-SGG6MUXW.js +0 -1
  230. package/dist/m/c-SXNSSHPP.js +0 -1
  231. package/dist/m/c-TFLRT5JX.js +0 -1
  232. package/dist/m/c-TLQINES7.js +0 -30
  233. package/dist/m/c-U65PREMC.js +0 -1
  234. package/dist/m/c-V5L6HWVL.js +0 -1
  235. package/dist/m/c-VMXCX52Q.js +0 -1
  236. package/dist/m/c-YYGUHSZR.js +0 -1
  237. package/dist/m/core/site-edits-CFMF6GHD.js +0 -1
  238. package/dist/m/loader-RC54UV7H.js +0 -1
  239. package/dist/m/manifest-3B84C829.js +0 -1
  240. package/dist/m/manifest-edit-1CB79AD4.js +0 -1
  241. package/dist/m/plugins/page-GTDPYGE4.js +0 -2
  242. package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
  243. package/dist/m/plugins/richclay-L3BZMX4I.js +0 -549
  244. package/dist/m/plugins/sortable-365SRRL3.js +0 -1
  245. package/dist/m/plugins/undo-UES4KS7B.js +0 -1
  246. package/dist/m/reader-core-VR7EL4WV.js +0 -77
  247. package/dist/m/ui/index-MGTWOW2C.js +0 -1
  248. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  249. /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-UHURKA2F.js.LEGAL.txt} +0 -0
  250. /package/dist/m/plugins/{richclay-L3BZMX4I.js.LEGAL.txt → richclay-MXH2GFAZ.js.LEGAL.txt} +0 -0
@@ -13,6 +13,9 @@ import { bevelDialog, dismissOf, keepFocusIn } from "../ui/bevel-dialog.js";
13
13
  import { bevelButton } from "../ui/bevel-controls.js";
14
14
  import { installSkin } from "../ui/vendor-skin.js";
15
15
  import { CSS as SKIN, ROOTS as SKIN_ROOTS } from "../ui/skins/quickcrop.js";
16
+ import { resolveTheme } from "../ui/theme.js";
17
+ import { themed, partRules, markParts } from "../ui/theme-parts.js";
18
+ import { style } from "../lib/hostile-css.js";
16
19
 
17
20
  const WIDTH = 844;
18
21
 
@@ -24,80 +27,136 @@ function isThemodal(m) {
24
27
  typeof m.onYes === "function" && "html" in m;
25
28
  }
26
29
 
27
- export const bevelCropAdapter = {
28
- // The stage has to fit inside the panel: the overlay's 16px side inset, the panel's
29
- // 1px border and the body's 22px padding a side; below it the header, the footer and
30
- // the overlay's top inset. Widths come from clientWidth, which leaves out a classic
31
- // scrollbar as the overlay does.
32
- fit() {
33
- const width = document.documentElement.clientWidth;
34
- return {
35
- width: Math.max(120, Math.min(WIDTH, width - 32) - 2 - 44),
36
- height: Math.max(120, window.innerHeight - Math.min(96, window.innerHeight * 0.1) - 200),
37
- };
38
- },
39
- open({ content, confirmLabel, onConfirm, onCancel }) {
40
- const { root, overlay, panel, heading, body, footer, close } = bevelDialog({
41
- zIndex: "2147483001", width: `${WIDTH}px`, closable: true, titled: true,
42
- });
43
- heading.textContent = "Crop image";
44
- panel.setAttribute("aria-label", "Crop image");
45
- body.append(content);
46
- const confirm = bevelButton(confirmLabel, { variant: "primary", onClick: () => onConfirm() });
47
- footer.append(confirm);
48
-
49
- close.addEventListener("click", () => onCancel());
50
- panel.addEventListener("submit", (event) => event.preventDefault());
51
- let pressedOnBackdrop = false;
52
- overlay.addEventListener("mousedown", (event) => { pressedOnBackdrop = event.target === overlay; });
53
- overlay.addEventListener("click", (event) => {
54
- if (pressedOnBackdrop && event.target === overlay) onCancel();
55
- pressedOnBackdrop = false;
56
- });
57
- // Capture, and stopped there, so a dialog underneath (a CMS confirm through
58
- // clay.modal) does not take the same key as its own: Escape cancels this crop,
59
- // Tab stays inside it.
60
- const onKey = (event) => {
61
- if (event.key === "Tab") {
30
+ const TITLE = "Crop image";
31
+
32
+ // The frame for one crop, in the theme captured for that crop. Measuring and opening
33
+ // build the same frame, so the stage is sized for the padding, borders, heading and
34
+ // button the person will actually see.
35
+ function cropFrame(theme, confirmLabel, onConfirm = null) {
36
+ const frame = bevelDialog({ zIndex: "2147483001", width: `${WIDTH}px`, closable: true, titled: true, theme, surface: "crop" });
37
+ frame.heading.textContent = TITLE;
38
+ frame.panel.setAttribute("aria-label", TITLE);
39
+ const confirm = bevelButton(confirmLabel, {
40
+ variant: "primary", onClick: onConfirm, theme,
41
+ parts: ["dialog.button", "crop.button"], labelParts: ["dialog.buttonLabel", "crop.buttonLabel"],
42
+ });
43
+ frame.footer.append(confirm);
44
+ return { ...frame, confirm };
45
+ }
46
+
47
+ // The stage is quickcrop's; its parts take only a page's explicit crop.* declarations,
48
+ // so dimming and crop lines keep their contrast unless the page asks otherwise.
49
+ function themeStage(stage, theme) {
50
+ if (!themed(theme)) return;
51
+ const paint = (el, names) => {
52
+ if (!el) return;
53
+ markParts(el, theme, names);
54
+ style(el, partRules(theme, names, {}));
55
+ };
56
+ paint(stage, ["crop.stage"]);
57
+ paint(stage.querySelector(":scope > img"), ["crop.image"]);
58
+ paint(stage.querySelector(".qc-dim"), ["crop.dim"]);
59
+ paint(stage.querySelector(".qc-box"), ["crop.selection"]);
60
+ for (const corner of ["nw", "ne", "sw", "se"]) paint(stage.querySelector(`.qc-handle.qc-${corner}`), ["crop.handle", `crop.handle.${corner}`]);
61
+ }
62
+
63
+ // A themed frame is measured: a hidden copy is mounted for one layout read and removed,
64
+ // without touching focus or the scroll lock. The room left for the stage is the body's
65
+ // inner width, and the overlay's inner height less the empty panel. The exact Bevel
66
+ // frame keeps its fixed allowances.
67
+ function measure(theme, confirmLabel) {
68
+ const { root, overlay, panel, body } = cropFrame(theme, confirmLabel);
69
+ root.setAttribute("aria-hidden", "true");
70
+ style(root, ["visibility:hidden", "pointer-events:none"]);
71
+ document.body.append(root);
72
+ try {
73
+ const px = (value) => parseFloat(value) || 0;
74
+ const o = getComputedStyle(overlay), b = getComputedStyle(body);
75
+ const width = body.clientWidth - px(b.paddingLeft) - px(b.paddingRight);
76
+ const height = overlay.clientHeight - px(o.paddingTop) - px(o.paddingBottom) - panel.offsetHeight;
77
+ return { width: Math.max(1, Math.floor(width)), height: Math.max(1, Math.floor(height)) };
78
+ } finally {
79
+ root.remove();
80
+ }
81
+ }
82
+
83
+ export function createCropAdapter({ theme = null, confirmLabel = "Crop" } = {}) {
84
+ return {
85
+ // Unthemed, the stage fits inside the panel by fixed allowances: the overlay's 16px
86
+ // side inset, the panel's 1px border and the body's 22px padding a side; below it the
87
+ // header, the footer and the overlay's top inset. Widths come from clientWidth, which
88
+ // leaves out a classic scrollbar as the overlay does.
89
+ fit() {
90
+ if (themed(theme)) return measure(theme, confirmLabel);
91
+ const width = document.documentElement.clientWidth;
92
+ return {
93
+ width: Math.max(120, Math.min(WIDTH, width - 32) - 2 - 44),
94
+ height: Math.max(120, window.innerHeight - Math.min(96, window.innerHeight * 0.1) - 200),
95
+ };
96
+ },
97
+ open({ content, confirmLabel: label, onConfirm, onCancel }) {
98
+ const { root, overlay, panel, body, close, confirm } = cropFrame(theme, label, () => onConfirm());
99
+ themeStage(content, theme);
100
+ body.append(content);
101
+
102
+ close.addEventListener("click", () => onCancel());
103
+ panel.addEventListener("submit", (event) => event.preventDefault());
104
+ let pressedOnBackdrop = false;
105
+ overlay.addEventListener("mousedown", (event) => { pressedOnBackdrop = event.target === overlay; });
106
+ overlay.addEventListener("click", (event) => {
107
+ if (pressedOnBackdrop && event.target === overlay) onCancel();
108
+ pressedOnBackdrop = false;
109
+ });
110
+ // Capture, and stopped there, so a dialog underneath (a CMS confirm through
111
+ // clay.modal) does not take the same key as its own: Escape cancels this crop,
112
+ // Tab stays inside it.
113
+ const onKey = (event) => {
114
+ if (event.key === "Tab") {
115
+ event.stopPropagation();
116
+ keepFocusIn(panel, event);
117
+ return;
118
+ }
119
+ if (event.key !== "Escape") return;
62
120
  event.stopPropagation();
63
- keepFocusIn(panel, event);
64
- return;
65
- }
66
- if (event.key !== "Escape") return;
67
- event.stopPropagation();
68
- onCancel();
69
- };
70
- document.addEventListener("keydown", onKey, true);
71
-
72
- // The page does not scroll behind the crop, as it did not under themodal. The
73
- // previous value comes back, so a modal underneath keeps its own lock.
74
- const overflow = document.body.style.overflow;
75
- document.body.style.overflow = "hidden";
76
-
77
- let done = false;
78
- const finish = () => {
79
- if (done) return;
80
- done = true;
81
- document.removeEventListener("keydown", onKey, true);
82
- document.body.style.overflow = overflow;
83
- root.remove();
84
- };
85
- // A modal opened on top supersedes this crop the way it supersedes itself: the
86
- // crop is cancelled, so its promise settles and the next crop can open.
87
- dismissOf.set(root, () => { finish(); onCancel(); });
88
-
89
- root.setAttribute("aria-hidden", "false");
90
- document.body.append(root);
91
- confirm.focus();
92
-
93
- return { close: finish };
94
- },
95
- };
121
+ onCancel();
122
+ };
123
+ document.addEventListener("keydown", onKey, true);
124
+
125
+ // The page does not scroll behind the crop, as it did not under themodal. The
126
+ // previous value comes back, so a modal underneath keeps its own lock.
127
+ const overflow = document.body.style.overflow;
128
+ document.body.style.overflow = "hidden";
129
+
130
+ let done = false;
131
+ const finish = () => {
132
+ if (done) return;
133
+ done = true;
134
+ document.removeEventListener("keydown", onKey, true);
135
+ document.body.style.overflow = overflow;
136
+ root.remove();
137
+ };
138
+ // A modal opened on top supersedes this crop the way it supersedes itself: the
139
+ // crop is cancelled, so its promise settles and the next crop can open.
140
+ dismissOf.set(root, () => { finish(); onCancel(); });
141
+
142
+ root.setAttribute("aria-hidden", "false");
143
+ document.body.append(root);
144
+ confirm.focus();
145
+
146
+ return { close: finish };
147
+ },
148
+ };
149
+ }
150
+
151
+ // The exact Bevel frame, for callers that want ClayJS's crop without the page's theme.
152
+ export const bevelCropAdapter = createCropAdapter();
96
153
 
97
154
  export function quickcrop(file, options = {}) {
98
155
  const modal = options.modal ?? "auto";
99
156
  const framed = modal === "auto" || isThemodal(modal);
100
- return vendorQuickcrop(file, framed ? { ...options, modal: bevelCropAdapter } : options);
157
+ if (!framed) return vendorQuickcrop(file, options);
158
+ const adapter = createCropAdapter({ theme: resolveTheme(), confirmLabel: options.labels?.confirm ?? "Crop" });
159
+ return vendorQuickcrop(file, { ...options, modal: adapter });
101
160
  }
102
161
 
103
162
  export default quickcrop;
@@ -5,9 +5,145 @@ import { installSkin } from "../ui/vendor-skin.js";
5
5
  import { CSS, ROOTS } from "../ui/skins/richclay.js";
6
6
  import { RichClay } from "../vendor/richclay.vendor.js";
7
7
  import { startsAtImport } from "../core/is-edit-mode.js";
8
+ import { stripEphemeral } from "../lib/ephemeral.js";
9
+ import { loadedEngine, mergeEngine } from "../sync/merge-engine.js";
8
10
 
9
11
  installSkin("richclay", CSS, { roots: ROOTS });
10
12
 
13
+ // A field the author kept out of page undo: its own literal no-undo (not the one RichClay
14
+ // adds while it runs), a clay="no-undo" token on it, or an ancestor region with no-undo.
15
+ const authoredNoUndo = (node) => {
16
+ if (node.hasAttribute("no-undo") && !node.hasAttribute("data-richclay-runtime-no-undo")) return true;
17
+ if ((node.getAttribute("clay") || "").split(/\s+/).includes("no-undo")) return true;
18
+ const parent = node.parentElement;
19
+ return !!parent && window.clay?.region?.resolveRegionPolicy?.(parent)?.undoable === false;
20
+ };
21
+ // The editor tidies a field's source whitespace the first time it is typed in. Whitespace
22
+ // that sits beside a block, or at the start or end of one, shows nothing, so it never
23
+ // counts as a change: a person's first letter is one step, and the tidy is not a step of
24
+ // its own or part of another field's.
25
+ const BLOCK = /^(P|DIV|UL|OL|LI|H[1-6]|BLOCKQUOTE|TABLE|THEAD|TBODY|TR|TD|TH|FIGURE|FIGCAPTION|HR|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|DL|DT|DD)$/;
26
+ const isBlock = (node) => node?.nodeType === 1 && BLOCK.test(node.nodeName);
27
+ const dropLayoutWhitespace = (root) => {
28
+ const walker = root.ownerDocument.createTreeWalker(root, 4);
29
+ const blank = [];
30
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
31
+ if (/\S/.test(text.data) || text.parentElement?.closest("pre")) continue;
32
+ const parent = text.parentNode;
33
+ const edge = !text.previousSibling || !text.nextSibling;
34
+ if ((edge && (parent === root || isBlock(parent))) || isBlock(text.previousSibling) || isBlock(text.nextSibling)) blank.push(text);
35
+ }
36
+ blank.forEach((text) => text.remove());
37
+ };
38
+ // Ephemeral attributes (selection marks and the like) never save, so they come off a
39
+ // copy before the comparison, as they do for every other node undo certifies.
40
+ const savedForm = (node) => {
41
+ const copy = node.cloneNode(true);
42
+ stripEphemeral(copy);
43
+ dropLayoutWhitespace(copy);
44
+ return RichClay.savedContent(copy);
45
+ };
46
+
47
+ // Parts of a field that history does not own (editor chrome, a widget the author marked
48
+ // no-undo) never count as a change: the sample holds a placeholder for each, and a replay
49
+ // puts back what each one holds now.
50
+ const UNOWNED = '[editor-ui], [clay~="editor-ui"], [no-undo]:not([data-richclay-runtime-no-undo]), [clay~="no-undo"]';
51
+ const outermost = (root) => [...root.querySelectorAll(UNOWNED)].filter((el) => {
52
+ for (let at = el.parentElement; at && at !== root; at = at.parentElement) if (at.matches(UNOWNED)) return false;
53
+ return true;
54
+ });
55
+ const historyForm = (node) => {
56
+ const copy = node.cloneNode(true);
57
+ outermost(copy).forEach((el) => el.replaceWith(copy.ownerDocument.createElement("clay-kept")));
58
+ return savedForm(copy);
59
+ };
60
+ const withUnowned = (element, html) => {
61
+ if (!html.includes("<clay-kept")) return html;
62
+ const template = element.ownerDocument.createElement("template");
63
+ template.innerHTML = html;
64
+ const live = outermost(element);
65
+ [...template.content.querySelectorAll("clay-kept")].forEach((slot, at) => {
66
+ if (live[at]) slot.replaceWith(live[at].cloneNode(true));
67
+ else slot.remove();
68
+ });
69
+ return template.innerHTML;
70
+ };
71
+
72
+ // Undo certifies a rich field by the content it would save, so editor runtime state
73
+ // never refuses a step and a real text change always does. A field the author marked
74
+ // no-undo keeps its protection.
75
+ const richForm = (node) => (!node.matches?.(RichClay.selector) || authoredNoUndo(node) ? undefined : savedForm(node));
76
+
77
+ // The field's own typing is a step of the page history: the engine reads the content the
78
+ // field would save and, on a replay, hands it back through the host write. The editor's own
79
+ // history is off while the page owns it, so a field's Cmd+Z and its toolbar reach the same
80
+ // stack as everything else.
81
+ // The engine rebaselines the field right after the write, so the write must land before
82
+ // it returns. A loaded merge engine morphs at once. The first undo asks for it, so a boot
83
+ // pays nothing; until it lands a plain innerHTML write is exact for saved content (checked
84
+ // in Chrome), except in a field holding editor chrome, which only the morph keeps.
85
+ const writeInner = (root, html) => { root.innerHTML = html; };
86
+ const morphNow = (field) => {
87
+ const engine = loadedEngine();
88
+ if (engine) return engine.morph;
89
+ mergeEngine().catch(() => {});
90
+ return field.querySelector('[editor-ui], [clay~="editor-ui"]') ? undefined : writeInner;
91
+ };
92
+
93
+ // Rich fields belong to the page history only when the page records typing: in commands
94
+ // mode the page's history holds commands alone, and a field's own history is left to the
95
+ // editor, which is then the only history that has the typing.
96
+ const pageOwnsTyping = () => window.clay?.undo?.pageHistory === true && window.clay.undo.recordingMode !== "commands";
97
+
98
+ const tracked = new Map();
99
+
100
+ const track = (field) => {
101
+ const undo = window.clay?.undo;
102
+ if (!pageOwnsTyping() || authoredNoUndo(field)) return;
103
+ tracked.set(field, undo.trackContent(field, { read: historyForm, write: (element, html) => { RichClay.setSavedContent(element, withUnowned(element, html), { morph: morphNow(element) }); } }));
104
+ if (!loadedEngine() && field.querySelector('[editor-ui], [clay~="editor-ui"]') && typeof undo.hold === "function") {
105
+ const release = undo.hold();
106
+ mergeEngine().catch(() => {}).finally(release);
107
+ }
108
+ };
109
+
110
+ const untrackAll = () => {
111
+ for (const dispose of tracked.values()) dispose();
112
+ tracked.clear();
113
+ };
114
+
115
+ document.addEventListener("richclay:mount", (event) => track(event.target));
116
+
117
+ // The rich form is handed to the page's undo when its session starts, rather than through
118
+ // a shared module, so a page without undo loads nothing for it. Each undo engine is asked
119
+ // once.
120
+ let formedFor = null;
121
+ const followHistory = () => {
122
+ const undo = window.clay?.undo;
123
+ if (undo?.addDurableForm && formedFor !== undo) {
124
+ undo.addDurableForm(richForm);
125
+ formedFor = undo;
126
+ }
127
+ RichClay.hostHistory = pageOwnsTyping();
128
+ RichClay.keepsOwnHistory = authoredNoUndo;
129
+ if (!RichClay.hostHistory) untrackAll();
130
+ else document.querySelectorAll("[data-richclay-active]").forEach(track);
131
+ };
132
+
133
+ document.addEventListener("clay:undo-session", followHistory);
134
+ document.addEventListener("clay:undo-recording", followHistory);
135
+ // A boot starts the undo session while its own module is still being imported, so the
136
+ // session event can land before the loader has put `clay.undo` on the page: once every
137
+ // plugin is attached the same answer is asked for again.
138
+ document.addEventListener("clay:ready", followHistory);
139
+
140
+ document.addEventListener("richclay:history-exhausted", (event) => {
141
+ const undo = window.clay?.undo;
142
+ if (!undo || window.clay.modeLifecycle?.canWrite?.() === false) return;
143
+ if (event.detail?.direction === "redo") undo.redo();
144
+ else undo.undo();
145
+ });
146
+
11
147
  let session = null;
12
148
 
13
149
  const fault = (message, code) => Object.assign(new Error(`clay:richclay: ${message}`), { code });
@@ -217,15 +217,31 @@ function makeSortable(sortableElem, Sortable, record = loose) {
217
217
  // Add onsorting callback if attribute exists (fires during drag)
218
218
  const onsortingCode = sortableElem.getAttribute('onsorting');
219
219
  if (onsortingCode) {
220
- options.onMove = function(evt) {
221
- if (!record.admitted) return;
222
- try {
223
- const asyncFn = new Function(`return (async function(evt) { ${onsortingCode} })`)();
224
- asyncFn.call(sortableElem, evt);
225
- } catch (error) {
226
- console.error('Error in onsorting execution:', error);
227
- }
228
- };
220
+ // Sortable reads onMove's return value (false cancels the move, 1 or -1 picks a
221
+ // side), so the handler runs synchronously and its value goes back. A handler that
222
+ // awaits cannot answer in time; it still runs, as an async function, and Sortable
223
+ // gets no answer from it.
224
+ // Code that awaits compiles as async: a plain function would read `await (x)` as a
225
+ // call to a function named await and throw on every move.
226
+ let onsorting = null;
227
+ try {
228
+ onsorting = /\bawait\b/.test(onsortingCode)
229
+ ? new Function(`return (async function(evt) { ${onsortingCode} })`)()
230
+ : new Function('evt', onsortingCode);
231
+ } catch (error) {
232
+ console.error('Error in onsorting compilation:', error);
233
+ }
234
+ if (onsorting) {
235
+ options.onMove = function(evt) {
236
+ if (!record.admitted) return;
237
+ try {
238
+ const answer = onsorting.call(sortableElem, evt);
239
+ return answer instanceof Promise ? undefined : answer;
240
+ } catch (error) {
241
+ console.error('Error in onsorting execution:', error);
242
+ }
243
+ };
244
+ }
229
245
  }
230
246
 
231
247
  // After a drop, run any author onsorted code, then notify reactive libraries.