@jxsuite/studio 10.2.0 → 10.4.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 (272) hide show
  1. package/dist/chunks/{2020-bx702wqa.js → 2020-906vx2dq.js} +3 -3
  2. package/dist/chunks/adapter-hnvad61x.js +4 -0
  3. package/dist/chunks/add-new-flow-v0pm96mr.js +4 -0
  4. package/dist/chunks/cf-account-picker-xb8vvt2h.js +4 -0
  5. package/dist/chunks/cf-settings-mkbcz4z2.js +4 -0
  6. package/dist/chunks/components-w5zqwva1.js +4 -0
  7. package/dist/chunks/content-source-g38w0d3f.js +4 -0
  8. package/dist/chunks/convert-to-repeater-r8sfxjjm.js +4 -0
  9. package/dist/chunks/editor-wvf5e6j9.js +4 -0
  10. package/dist/chunks/entry-commands-t13dyw3c.js +4 -0
  11. package/dist/chunks/extension-sections-vjzw46xb.js +4 -0
  12. package/dist/chunks/file-ops-1sghzgcr.js +4 -0
  13. package/dist/chunks/files-zrqtgt41.js +4 -0
  14. package/dist/chunks/format-host-9gy76erh.js +4 -0
  15. package/dist/chunks/fs-events-45rhg3me.js +4 -0
  16. package/dist/chunks/grid-open-t4fq6bbm.js +4 -0
  17. package/dist/chunks/{grid-view-tghcwrrt.js → grid-view-2f03wcgw.js} +16 -16
  18. package/dist/chunks/icons-gb967qc8.js +4 -0
  19. package/dist/chunks/iframe-host-9aq8ze3q.js +4 -0
  20. package/dist/chunks/image-bake-4afr4y6a.js +4 -0
  21. package/dist/chunks/image-editor-x7t8jbjn.js +4 -0
  22. package/dist/chunks/index-f9qr7kzh.js +4 -0
  23. package/dist/chunks/{index-vr006kbt.js → index-yjrfeks8.js} +3 -3
  24. package/dist/chunks/{javascript-z1g3s9fd.js → javascript-hy2mws8v.js} +3 -3
  25. package/dist/chunks/{jsonMode-7szwg8c4.js → jsonMode-vj30te2h.js} +6 -6
  26. package/dist/chunks/jx-validate-r4ax11e3.js +4 -0
  27. package/dist/chunks/layers-m49nmwvx.js +4 -0
  28. package/dist/chunks/layers-panel-zr92pwdp.js +4 -0
  29. package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
  30. package/dist/chunks/media-library-j74rt0w4.js +4 -0
  31. package/dist/chunks/media-open-mypr0k6b.js +4 -0
  32. package/dist/chunks/media-picker-pemtbsjv.js +4 -0
  33. package/dist/chunks/{monaco-binding-v42w9c1v.js → monaco-binding-a51py33c.js} +3 -3
  34. package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
  35. package/dist/chunks/{monaco-setup-7bffxd6v.js → monaco-setup-0hw9f835.js} +3 -3
  36. package/dist/chunks/notify-bwehrp7q.js +4 -0
  37. package/dist/chunks/parse-hee7ne9b.js +4 -0
  38. package/dist/chunks/platform-kv0k65cy.js +4 -0
  39. package/dist/chunks/publish-panel-vg1n880b.js +4 -0
  40. package/dist/chunks/publish-quick-4fwngwrw.js +4 -0
  41. package/dist/chunks/publish-workspace-jv4w2bx2.js +4 -0
  42. package/dist/chunks/quick-search-nh29rt8a.js +4 -0
  43. package/dist/chunks/references-vqyr5s9x.js +4 -0
  44. package/dist/chunks/right-panel-3ws9f2w0.js +4 -0
  45. package/dist/chunks/source-control-h3hcs5gy.js +4 -0
  46. package/dist/chunks/{studio-j3d2fjqz.js → studio-0939t1q1.js} +4 -4
  47. package/dist/chunks/studio-0gvvqj3h.js +5 -0
  48. package/dist/chunks/studio-0khd8xtx.js +4 -0
  49. package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
  50. package/dist/chunks/studio-1410q5dw.js +5 -0
  51. package/dist/chunks/studio-1ctnw3xv.js +5 -0
  52. package/dist/chunks/studio-1ks5d9y4.js +5 -0
  53. package/dist/chunks/{studio-awxwjawq.js → studio-1y9mab9m.js} +4 -4
  54. package/dist/chunks/studio-2e4hry72.js +6 -0
  55. package/dist/chunks/studio-2kpxm0eh.js +7 -0
  56. package/dist/chunks/studio-2pk7kc7z.js +5 -0
  57. package/dist/chunks/studio-2x972v58.js +5 -0
  58. package/dist/chunks/studio-2zz4qbv9.js +5 -0
  59. package/dist/chunks/{studio-bvs9kpst.js → studio-3na1r48j.js} +8 -8
  60. package/dist/chunks/studio-3pz4t5a2.js +5 -0
  61. package/dist/chunks/studio-3qr08sxq.js +4 -0
  62. package/dist/chunks/{studio-75kcdbsk.js → studio-45mj4jqe.js} +4 -4
  63. package/dist/chunks/studio-4bgvvzhv.js +5 -0
  64. package/dist/chunks/studio-4f7thq0p.js +5 -0
  65. package/dist/chunks/studio-4fnb5648.js +5 -0
  66. package/dist/chunks/studio-4zd6an2b.js +5 -0
  67. package/dist/chunks/{studio-9rbe2xpw.js → studio-5tet77de.js} +3 -3
  68. package/dist/chunks/studio-61rg2cz5.js +5 -0
  69. package/dist/chunks/{studio-ybnwymxw.js → studio-7rrdbvex.js} +4 -4
  70. package/dist/chunks/{studio-sxdb9h3q.js → studio-90zkgne5.js} +7 -7
  71. package/dist/chunks/studio-9c6cbq2c.js +4 -0
  72. package/dist/chunks/studio-9dq7rwc7.js +5 -0
  73. package/dist/chunks/studio-9gr7xs9r.js +6 -0
  74. package/dist/chunks/studio-a88ej93p.js +7 -0
  75. package/dist/chunks/{studio-er5f383a.js → studio-abh9kfdh.js} +4 -4
  76. package/dist/chunks/studio-acer0g8w.js +5 -0
  77. package/dist/chunks/{studio-qh8berdz.js → studio-amaz545p.js} +4 -4
  78. package/dist/chunks/studio-az3wvyh2.js +5 -0
  79. package/dist/chunks/studio-bg8pzprg.js +5 -0
  80. package/dist/chunks/{studio-399a25za.js → studio-bnc50f9h.js} +4 -4
  81. package/dist/chunks/studio-brh8d2pr.js +5 -0
  82. package/dist/chunks/{studio-646nn2ys.js → studio-c2fa783p.js} +5 -5
  83. package/dist/chunks/studio-cfnn70h1.js +5 -0
  84. package/dist/chunks/{studio-rzqaz21g.js → studio-cqxj4fcy.js} +4 -4
  85. package/dist/chunks/studio-cw11h078.js +5 -0
  86. package/dist/chunks/studio-daksw8kh.js +5 -0
  87. package/dist/chunks/studio-dn56z7sn.js +5 -0
  88. package/dist/chunks/studio-egnjpyde.js +9 -0
  89. package/dist/chunks/studio-ejrypvmq.js +5 -0
  90. package/dist/chunks/studio-eqcddvc2.js +928 -0
  91. package/dist/chunks/studio-evq47j2z.js +5 -0
  92. package/dist/chunks/{studio-r7pf0rkv.js → studio-frc16j8v.js} +7 -7
  93. package/dist/chunks/studio-g0hxtcga.js +5 -0
  94. package/dist/chunks/{studio-th7z3fg7.js → studio-g9brbewz.js} +4 -4
  95. package/dist/chunks/studio-gnazwtv3.js +5 -0
  96. package/dist/chunks/studio-hfqpfmpk.js +5 -0
  97. package/dist/chunks/studio-hh394phe.js +5 -0
  98. package/dist/chunks/{studio-pjtntksy.js → studio-j0zf5x2x.js} +4 -4
  99. package/dist/chunks/studio-j5fcp6gj.js +5 -0
  100. package/dist/chunks/studio-jd1qb1st.js +7 -0
  101. package/dist/chunks/studio-jma2q8r9.js +5 -0
  102. package/dist/chunks/studio-k0xzrse2.js +64 -0
  103. package/dist/chunks/studio-k1xqrgm5.js +5 -0
  104. package/dist/chunks/studio-k2evngrj.js +7 -0
  105. package/dist/chunks/studio-k3we3kqb.js +5 -0
  106. package/dist/chunks/studio-knsjyx7n.js +5 -0
  107. package/dist/chunks/studio-mrjtnm62.js +5 -0
  108. package/dist/chunks/{studio-sevbc3xv.js → studio-n9qba0gx.js} +4 -4
  109. package/dist/chunks/{studio-ea74s8c1.js → studio-nvcn2ykc.js} +4 -4
  110. package/dist/chunks/studio-nwf0c32p.js +5 -0
  111. package/dist/chunks/studio-nzemfbkk.js +14 -0
  112. package/dist/chunks/{studio-43y04qck.js → studio-pj216fmn.js} +4 -4
  113. package/dist/chunks/{studio-4g8yb6jn.js → studio-pm17wdc4.js} +4 -4
  114. package/dist/chunks/studio-pwctzka3.js +16 -0
  115. package/dist/chunks/{studio-71m92v9s.js → studio-q80fg1d6.js} +7 -7
  116. package/dist/chunks/studio-q9hx2qp0.js +5 -0
  117. package/dist/chunks/studio-qrzmfaf6.js +5 -0
  118. package/dist/chunks/{studio-86r8hp7r.js → studio-r6mx7vm5.js} +5 -5
  119. package/dist/chunks/studio-rr00ex23.js +7 -0
  120. package/dist/chunks/{studio-4jhw38b2.js → studio-s24mhq0z.js} +4 -4
  121. package/dist/chunks/studio-sce30r7b.js +4 -0
  122. package/dist/chunks/studio-swedpd82.js +5 -0
  123. package/dist/chunks/studio-tmfcn347.js +6 -0
  124. package/dist/chunks/studio-tpxcryf2.js +5 -0
  125. package/dist/chunks/{studio-wc6za3zs.js → studio-v5wktj6f.js} +4 -4
  126. package/dist/chunks/studio-v7a7yb5w.js +9 -0
  127. package/dist/chunks/{studio-kg1abnz0.js → studio-vpevkfxw.js} +4 -4
  128. package/dist/chunks/{studio-881z1xrx.js → studio-wmjmsw0p.js} +4 -4
  129. package/dist/chunks/studio-wqfaqw4m.js +5 -0
  130. package/dist/chunks/{studio-hhp4x8xf.js → studio-xpx91fby.js} +5 -5
  131. package/dist/chunks/studio-xva3hzkv.js +5 -0
  132. package/dist/chunks/{studio-6hzdjfv0.js → studio-xx3r3sb0.js} +4 -4
  133. package/dist/chunks/{studio-cg1bfyhf.js → studio-z3fztxrp.js} +8 -8
  134. package/dist/chunks/studio-z9453ypn.js +6 -0
  135. package/dist/chunks/{tsMode-f54j66ym.js → tsMode-jg1dtphj.js} +7 -7
  136. package/dist/chunks/workspace-6ztqcrsh.js +4 -0
  137. package/dist/chunks/workspace-home-rse9h8a0.js +4 -0
  138. package/dist/chunks/{ws-client-e8kj684q.js → ws-client-arz2qsy9.js} +3 -3
  139. package/dist/iframe-entry.js +13 -13
  140. package/dist/manifest.json +83 -78
  141. package/dist/studio.js +84 -84
  142. package/package.json +11 -11
  143. package/src/browse/library-pane.ts +44 -29
  144. package/src/browse/library-preview.ts +6 -2
  145. package/src/canvas/asset-refs.ts +13 -4
  146. package/src/canvas/asset-resolve.ts +54 -11
  147. package/src/canvas/canvas-render.ts +25 -0
  148. package/src/canvas/iframe-entry.ts +5 -1
  149. package/src/canvas/iframe-host.ts +16 -6
  150. package/src/canvas/iframe-protocol.ts +9 -1
  151. package/src/commands/context.ts +5 -0
  152. package/src/commands/live-context.ts +5 -2
  153. package/src/editor/shortcuts.ts +3 -0
  154. package/src/files/asset-versions.ts +108 -2
  155. package/src/files/file-ops.ts +11 -1
  156. package/src/files/files.ts +111 -48
  157. package/src/files/media-paths.ts +12 -4
  158. package/src/files/media-upload.ts +195 -21
  159. package/src/files/new-file-name.ts +39 -1
  160. package/src/media/image-bake.ts +164 -0
  161. package/src/media/image-editor.ts +846 -0
  162. package/src/media/image-format.ts +262 -0
  163. package/src/media/image-recipe.ts +651 -0
  164. package/src/media/media-commands.ts +65 -1
  165. package/src/media/media-library-model.ts +1 -1
  166. package/src/media/media-library.ts +175 -15
  167. package/src/media/media-open.ts +156 -3
  168. package/src/media/media-pane.ts +8 -4
  169. package/src/panels/pane-context.ts +6 -1
  170. package/src/panels/tab-strip.ts +12 -3
  171. package/src/studio.ts +13 -2
  172. package/src/surfaces/image-editor.json +1390 -0
  173. package/src/surfaces/image-editor.ts +272 -0
  174. package/src/surfaces/inspector-dock.json +3 -3
  175. package/src/surfaces/media-library.json +129 -1
  176. package/src/surfaces/media-library.ts +17 -2
  177. package/src/types.ts +2 -1
  178. package/src/workspace/tab-workspace.ts +2 -1
  179. package/src/workspace/workspace-switch.ts +3 -1
  180. package/src/workspace/workspace.ts +22 -1
  181. package/dist/chunks/adapter-pp011m5x.js +0 -4
  182. package/dist/chunks/add-new-flow-ecqmzz76.js +0 -4
  183. package/dist/chunks/cf-account-picker-c07znkqt.js +0 -4
  184. package/dist/chunks/cf-settings-0v2bmqy0.js +0 -4
  185. package/dist/chunks/components-yn0dn2pq.js +0 -4
  186. package/dist/chunks/content-source-g0j41yy6.js +0 -4
  187. package/dist/chunks/convert-to-repeater-sbjk7tgn.js +0 -4
  188. package/dist/chunks/editor-xz02jeh1.js +0 -4
  189. package/dist/chunks/entry-commands-kmt02crp.js +0 -4
  190. package/dist/chunks/extension-sections-0rprf12d.js +0 -4
  191. package/dist/chunks/file-ops-mexj5qed.js +0 -4
  192. package/dist/chunks/files-b2tcppdd.js +0 -4
  193. package/dist/chunks/format-host-btsevv0c.js +0 -4
  194. package/dist/chunks/fs-events-r0apejva.js +0 -4
  195. package/dist/chunks/grid-open-n2a8xkht.js +0 -4
  196. package/dist/chunks/icons-853n6qt3.js +0 -4
  197. package/dist/chunks/iframe-host-gx0ypyh3.js +0 -4
  198. package/dist/chunks/index-bs4k9tr6.js +0 -4
  199. package/dist/chunks/jx-validate-ymf10teh.js +0 -4
  200. package/dist/chunks/layers-p9nfya38.js +0 -4
  201. package/dist/chunks/layers-panel-tt1hzhhf.js +0 -4
  202. package/dist/chunks/lit-html-x18n2sfz.js +0 -4
  203. package/dist/chunks/media-library-0n45gv8r.js +0 -4
  204. package/dist/chunks/media-picker-s09at95z.js +0 -4
  205. package/dist/chunks/monaco-buffer-een2ea0v.js +0 -4
  206. package/dist/chunks/notify-qwb8radt.js +0 -4
  207. package/dist/chunks/parse-v77xtwv7.js +0 -4
  208. package/dist/chunks/platform-8jnez0wr.js +0 -4
  209. package/dist/chunks/publish-panel-aysnj611.js +0 -4
  210. package/dist/chunks/publish-quick-kpbqs9mz.js +0 -4
  211. package/dist/chunks/publish-workspace-nd0a8q99.js +0 -4
  212. package/dist/chunks/quick-search-vg0jkhk2.js +0 -4
  213. package/dist/chunks/references-a7v6a0e2.js +0 -4
  214. package/dist/chunks/right-panel-cvk1vxx5.js +0 -4
  215. package/dist/chunks/source-control-fsqzc6he.js +0 -4
  216. package/dist/chunks/studio-05jfwbmf.js +0 -5
  217. package/dist/chunks/studio-0q5jwddh.js +0 -9
  218. package/dist/chunks/studio-0w2dr0q8.js +0 -5
  219. package/dist/chunks/studio-1tzpds97.js +0 -9
  220. package/dist/chunks/studio-216b27sv.js +0 -4
  221. package/dist/chunks/studio-2ejhqns4.js +0 -5
  222. package/dist/chunks/studio-2r90cts3.js +0 -16
  223. package/dist/chunks/studio-399xknkb.js +0 -6
  224. package/dist/chunks/studio-4ncmwqw5.js +0 -5
  225. package/dist/chunks/studio-5atwjgkj.js +0 -7
  226. package/dist/chunks/studio-5frqk7c0.js +0 -5
  227. package/dist/chunks/studio-6cwj6xrx.js +0 -5
  228. package/dist/chunks/studio-78pywnp2.js +0 -6
  229. package/dist/chunks/studio-886t6ztg.js +0 -5
  230. package/dist/chunks/studio-8wnvtr4z.js +0 -5
  231. package/dist/chunks/studio-8z66jk4z.js +0 -5
  232. package/dist/chunks/studio-b2qpt0xy.js +0 -14
  233. package/dist/chunks/studio-c71806xz.js +0 -7
  234. package/dist/chunks/studio-cbwks69x.js +0 -928
  235. package/dist/chunks/studio-cg710sk9.js +0 -5
  236. package/dist/chunks/studio-cw0wb9qk.js +0 -7
  237. package/dist/chunks/studio-cyheys7k.js +0 -5
  238. package/dist/chunks/studio-ea5m9kg5.js +0 -5
  239. package/dist/chunks/studio-efdxjwyp.js +0 -64
  240. package/dist/chunks/studio-egmw2837.js +0 -5
  241. package/dist/chunks/studio-f746039f.js +0 -5
  242. package/dist/chunks/studio-fmdymd9v.js +0 -5
  243. package/dist/chunks/studio-gcm12z00.js +0 -6
  244. package/dist/chunks/studio-gj98m8gy.js +0 -7
  245. package/dist/chunks/studio-hasbgjah.js +0 -5
  246. package/dist/chunks/studio-hz5n7x27.js +0 -5
  247. package/dist/chunks/studio-jeraxdgv.js +0 -7
  248. package/dist/chunks/studio-jvgek9xt.js +0 -5
  249. package/dist/chunks/studio-jws1a87n.js +0 -5
  250. package/dist/chunks/studio-k2sz13zg.js +0 -5
  251. package/dist/chunks/studio-k3m4jkdk.js +0 -5
  252. package/dist/chunks/studio-kg5ya5r2.js +0 -5
  253. package/dist/chunks/studio-khwbgrtg.js +0 -5
  254. package/dist/chunks/studio-mx6wzh25.js +0 -5
  255. package/dist/chunks/studio-mxs3ar9t.js +0 -5
  256. package/dist/chunks/studio-nm4szey4.js +0 -5
  257. package/dist/chunks/studio-nqxqf8p2.js +0 -4
  258. package/dist/chunks/studio-qn7z4krm.js +0 -5
  259. package/dist/chunks/studio-qyed0ah6.js +0 -5
  260. package/dist/chunks/studio-r4bcdwhq.js +0 -5
  261. package/dist/chunks/studio-rtfnhrdg.js +0 -5
  262. package/dist/chunks/studio-s5tzw84t.js +0 -5
  263. package/dist/chunks/studio-t51y8v1d.js +0 -5
  264. package/dist/chunks/studio-t98vq4r6.js +0 -5
  265. package/dist/chunks/studio-tsxpqvh0.js +0 -6
  266. package/dist/chunks/studio-v1yascv2.js +0 -5
  267. package/dist/chunks/studio-wxq6v5sv.js +0 -5
  268. package/dist/chunks/studio-y60sts62.js +0 -4
  269. package/dist/chunks/studio-zndrqawb.js +0 -5
  270. package/dist/chunks/studio-zwaxn5s2.js +0 -5
  271. package/dist/chunks/workspace-home-x1787wpv.js +0 -4
  272. package/dist/chunks/workspace-zpe1np43.js +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,16 +60,16 @@
60
60
  "dependencies": {
61
61
  "@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
62
62
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
63
- "@jxsuite/ai": "^0.42.0",
64
- "@jxsuite/collab": "^0.11.4",
65
- "@jxsuite/create": "^1.5.2",
66
- "@jxsuite/formulas": "^0.0.26",
67
- "@jxsuite/markup": "^1.0.2",
68
- "@jxsuite/protocol": "^6.2.0",
69
- "@jxsuite/runtime": "^6.0.1",
70
- "@jxsuite/schema": "^5.0.0",
71
- "@jxsuite/site": "^2.2.2",
72
- "@jxsuite/ui": "^0.8.2",
63
+ "@jxsuite/ai": "^0.42.1",
64
+ "@jxsuite/collab": "^0.11.5",
65
+ "@jxsuite/create": "^1.5.3",
66
+ "@jxsuite/formulas": "^0.0.27",
67
+ "@jxsuite/markup": "^1.0.3",
68
+ "@jxsuite/protocol": "^6.2.1",
69
+ "@jxsuite/runtime": "^6.0.2",
70
+ "@jxsuite/schema": "^5.1.0",
71
+ "@jxsuite/site": "^2.3.0",
72
+ "@jxsuite/ui": "^0.10.0",
73
73
  "@vue/reactivity": "3.5.44",
74
74
  "ajv": "^8.20.0",
75
75
  "ajv-formats": "^3.0.1",
@@ -44,7 +44,16 @@ import { getPlatform } from "../platform";
44
44
  import { projectState, renderOnly } from "../store";
45
45
  import { invalidateUsages } from "../services/references";
46
46
  import { confirmFileDelete, renamePromptMessage } from "../files/file-ops";
47
- import { openFileInTab } from "../files/files";
47
+ import { duplicateAsset } from "../files/media-upload";
48
+ import { renameRefusal } from "../files/new-file-name";
49
+ import {
50
+ commitRename,
51
+ listRenameSiblings,
52
+ notifyMoveOutcome,
53
+ openFileInTab,
54
+ parentDirOf,
55
+ renameStatus,
56
+ } from "../files/files";
48
57
  import { runActiveReported } from "../commands/run-reported";
49
58
  import {
50
59
  PAGES_COLLECTION,
@@ -680,9 +689,15 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
680
689
  },
681
690
  { dividerAbove: true },
682
691
  ),
683
- row("duplicate", "Duplicate", () => {
684
- void duplicateLibraryFile(file);
685
- }),
692
+ /* Not offered without raw bytes, as in the Media workspace (studio.md §9.3): a copy made as
693
+ text would be a damaged copy of anything that is not UTF-8. */
694
+ ...(getPlatform().readFileBytes !== undefined
695
+ ? [
696
+ row("duplicate", "Duplicate", () => {
697
+ void duplicateLibraryFile(file);
698
+ }),
699
+ ]
700
+ : []),
686
701
  row(
687
702
  "delete",
688
703
  "Delete",
@@ -696,17 +711,23 @@ function showLibraryContextMenu(path: string, x: number, y: number): void {
696
711
  }
697
712
 
698
713
  /**
699
- * The rename dialog — the same copy, from the same helper, as the Files panel's.
714
+ * The rename dialog: the same copy, the same refusals and the same commit as the Files panel's.
700
715
  *
701
- * A rename here rewrites references exactly as one in the sidebar does, and two dialogs that
702
- * disagreed about that is the divergence the shared helper exists to prevent.
716
+ * A rename here rewrites references exactly as one in the sidebar does and replaces a destination
717
+ * just as silently, and an open tab has to follow the file. Two flows that disagreed about any of
718
+ * that is the divergence the shared helpers exist to prevent.
703
719
  */
704
720
  async function renameLibraryFile(file: LibraryFile) {
721
+ const path = file.path.replaceAll("\\", "/");
722
+ const taken = await listRenameSiblings(path, SOURCE);
723
+ if (taken === null) {
724
+ return;
725
+ }
705
726
  const message = await renamePromptMessage(file.path);
706
727
  const prompt: PromptDialogOptions = {
707
728
  confirmLabel: "Rename",
708
729
  select: "stem",
709
- validate: (value) => (value.trim() ? "" : "Enter a file name."),
730
+ validate: (value) => renameRefusal(path, value, taken),
710
731
  value: file.name,
711
732
  };
712
733
  if (message !== undefined) {
@@ -716,15 +737,13 @@ async function renameLibraryFile(file: LibraryFile) {
716
737
  if (!newName || newName === file.name) {
717
738
  return;
718
739
  }
719
- const normalized = file.path.replaceAll("\\", "/");
720
- const parent = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
740
+ const parent = parentDirOf(path);
721
741
  const newPath = parent === "." ? newName : `${parent}/${newName}`;
722
742
  try {
723
- await getPlatform().renameFile(file.path, newPath);
724
- invalidateUsages();
743
+ const report = await commitRename(path, newPath);
725
744
  invalidateLibrary();
726
745
  await loadLibrary();
727
- notify.success(`Renamed to ${newName}`);
746
+ notifyMoveOutcome(renameStatus(newName, report), report, newPath);
728
747
  } catch (error) {
729
748
  notify.error(`Could not rename ${file.name}.`, {
730
749
  detail: errorMessage(error),
@@ -734,25 +753,21 @@ async function renameLibraryFile(file: LibraryFile) {
734
753
  }
735
754
  }
736
755
 
756
+ /**
757
+ * Copy the file beside itself, through the one copy Studio has (`duplicateAsset`): byte for byte,
758
+ * under a name nothing else has. A file listed here is usually text, but not provably so. A
759
+ * collection that declares no format lists whatever its folder holds, and a declared Markdown
760
+ * collection can hold a Latin-1 entry, which a read as UTF-8 would have damaged. `document` is what
761
+ * keeps a copy that IS text openable on a backend that files an upload as a binary.
762
+ */
737
763
  async function duplicateLibraryFile(file: LibraryFile) {
738
- const normalized = file.path.replaceAll("\\", "/");
739
- const parent = normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
740
- const stem = file.name.replace(/(\.[^.]+)$/, "");
741
- const copyName = `${stem}-copy${file.ext || ""}`;
742
- const copyPath = parent === "." ? copyName : `${parent}/${copyName}`;
743
- try {
744
- const platform = getPlatform();
745
- const content = await platform.readFile(file.path);
746
- await platform.writeFile(copyPath, content);
764
+ const copy = await duplicateAsset(file.path.replaceAll("\\", "/"), {
765
+ document: true,
766
+ source: SOURCE,
767
+ });
768
+ if (copy !== null) {
747
769
  invalidateLibrary();
748
770
  await loadLibrary();
749
- notify.success(`Duplicated as ${copyName}`);
750
- } catch (error) {
751
- notify.error(`Could not duplicate ${file.name}.`, {
752
- detail: errorMessage(error),
753
- path: file.path,
754
- source: SOURCE,
755
- });
756
771
  }
757
772
  }
758
773
 
@@ -34,7 +34,7 @@ import { assetContextFor, hostAssetDeclarations, resolveAssetRef } from "../canv
34
34
  import { canvasBaseOrigin, documentBase, hostResolveToken } from "../canvas/canvas-origin";
35
35
  import { npmModuleUrl } from "../canvas/npm-modules";
36
36
  import { projectState } from "../store";
37
- import { assetVersionsSnapshot } from "../files/asset-versions";
37
+ import { assetVersionsSnapshot, localRevisionsSnapshot } from "../files/asset-versions";
38
38
  import { componentRegistry } from "../files/components";
39
39
  import { getPlatform } from "../platform";
40
40
  import { loadFormats, formatForPath } from "../format/format-host";
@@ -184,7 +184,11 @@ async function renderDocument(
184
184
  `documentDir` would resolve its relative refs against the wrong entry. The version map is
185
185
  read as the render starts, after the reads above: the Library is the many-images screen,
186
186
  and a versioned src is what lets its cards repaint from the browser cache. */
187
- const assets = context && { ...context, versions: assetVersionsSnapshot() };
187
+ const assets = context && {
188
+ ...context,
189
+ revisions: localRevisionsSnapshot(),
190
+ versions: assetVersionsSnapshot(),
191
+ };
188
192
  setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
189
193
  try {
190
194
  const element = renderNode(document_, scope);
@@ -18,7 +18,11 @@ import { contentMountFor, resolveAssetRef } from "./asset-resolve";
18
18
  import { documentBase, loopbackAssetSrc } from "./canvas-origin";
19
19
  import { getPlatform, hasPlatform } from "../platform";
20
20
  import { projectState } from "../store";
21
- import { assetVersionsSnapshot } from "../files/asset-versions";
21
+ import {
22
+ assetVersionsSnapshot,
23
+ hasLocalRevisions,
24
+ localRevisionsSnapshot,
25
+ } from "../files/asset-versions";
22
26
  import { activeTab } from "../workspace/workspace";
23
27
  import type { AssetContext } from "./asset-resolve";
24
28
  import type { ContentSectionEntry } from "../types";
@@ -59,9 +63,10 @@ export function assetContextFor(
59
63
  projectState?.projectConfig?.content as Record<string, ContentSectionEntry> | undefined,
60
64
  projectLocales(),
61
65
  );
62
- /* In site space the mount is the ONLY thing that needs doing, so no mount means no work. In repo
66
+ /* In site space the mount is the ONLY thing that needs doing, so no mount means no work — unless
67
+ Studio has just written a file in place, whose references need its repaint key (§3.4). In repo
63
68
  space every reference needs rebasing, mount or not. */
64
- if (space === "site" && !mount) {
69
+ if (space === "site" && !mount && !hasLocalRevisions()) {
65
70
  return null;
66
71
  }
67
72
  const context: AssetContext = {
@@ -116,7 +121,11 @@ export function previewAssetSrcFor(value: string, documentPath: string | null |
116
121
  return value;
117
122
  }
118
123
  const ctx = assetContextFor(documentPath, hostAssetDeclarations());
119
- const versioned = ctx && { ...ctx, versions: assetVersionsSnapshot() };
124
+ const versioned = ctx && {
125
+ ...ctx,
126
+ revisions: localRevisionsSnapshot(),
127
+ versions: assetVersionsSnapshot(),
128
+ };
120
129
  return loopbackAssetSrc(resolveAssetRef(value, versioned) ?? value);
121
130
  }
122
131
 
@@ -99,6 +99,11 @@ export interface AssetContext {
99
99
  * unversioned, exactly as before versions existed.
100
100
  */
101
101
  versions?: Readonly<Record<string, string>> | undefined;
102
+ /**
103
+ * Studio's own repaint keys by normalized project path, appended as `?r=<key>` in BOTH spaces
104
+ * (see {@link withRevision}). A path appears only after Studio itself wrote its bytes.
105
+ */
106
+ revisions?: Readonly<Record<string, string>> | undefined;
102
107
  }
103
108
 
104
109
  /**
@@ -122,7 +127,27 @@ export interface AssetContext {
122
127
  * @returns {string} The versioned URL
123
128
  */
124
129
  export function withVersion(url: string, version: string | undefined): string {
125
- if (!isString(version) || version === "") {
130
+ return withParam(url, "v", version);
131
+ }
132
+
133
+ /**
134
+ * `url` with `r=<revision>` in its query: Studio's own repaint key for a path it has just written
135
+ * (specs/studio.md §3.4). Same placement rules as {@link withVersion}, and a different name on
136
+ * purpose: `v` is a version the HOST issued and may answer immutably, and this is a token Studio
137
+ * minted, which never may be. It exists because an unversioned URL is byte-identical before and
138
+ * after an in-place overwrite, so without it a decoded `<img>` keeps the old pixels.
139
+ *
140
+ * @param {string} url - A URL, absolute or relative
141
+ * @param {string | undefined} revision - The path's repaint key, if Studio holds one
142
+ * @returns {string} The URL carrying the key
143
+ */
144
+ export function withRevision(url: string, revision?: string): string {
145
+ return withParam(url, "r", revision);
146
+ }
147
+
148
+ /** `url` with one `name=value` query parameter, replacing every `name` it already had. */
149
+ function withParam(url: string, name: "v" | "r", value: string | undefined): string {
150
+ if (!isString(value) || value === "") {
126
151
  return url;
127
152
  }
128
153
  const hashAt = url.indexOf("#");
@@ -131,23 +156,23 @@ export function withVersion(url: string, version: string | undefined): string {
131
156
  const queryAt = beforeHash.indexOf("?");
132
157
  const base = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
133
158
  const parts = queryAt === -1 ? [] : beforeHash.slice(queryAt + 1).split("&");
134
- const kept = parts.filter((part) => part !== "" && !namesV(part));
135
- kept.push(`v=${encodeURIComponent(version)}`);
159
+ const kept = parts.filter((part) => part !== "" && !namesKey(part, name));
160
+ kept.push(`${name}=${encodeURIComponent(value)}`);
136
161
  return `${base}?${kept.join("&")}${fragment}`;
137
162
  }
138
163
 
139
- /** Whether one `key[=value]` query part's key decodes to `v`, the way `URLSearchParams` reads it. */
140
- function namesV(part: string): boolean {
164
+ /** Whether one `key[=value]` query part's key decodes to `name`, the way `URLSearchParams` reads it. */
165
+ function namesKey(part: string, name: string): boolean {
141
166
  const eq = part.indexOf("=");
142
167
  const key = eq === -1 ? part : part.slice(0, eq);
143
- if (key === "v") {
168
+ if (key === name) {
144
169
  return true;
145
170
  }
146
171
  if (!key.includes("%")) {
147
172
  return false;
148
173
  }
149
174
  try {
150
- return decodeURIComponent(key) === "v";
175
+ return decodeURIComponent(key) === name;
151
176
  } catch {
152
177
  // A malformed escape decodes to itself under `URLSearchParams`, which is never a bare `v`.
153
178
  return false;
@@ -292,13 +317,31 @@ export function resolveAssetRef(value: string, ctx: AssetContext | null): string
292
317
  return null;
293
318
  }
294
319
  const { suffix } = splitRefSuffix(value);
295
- return withVersion(
296
- `${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`,
297
- ownVersion(ctx.versions, path),
320
+ return withRevision(
321
+ withVersion(
322
+ `${ctx.fileBaseUrl}${encodeProjectPath(path)}${suffix}`,
323
+ ownVersion(ctx.versions, path),
324
+ ),
325
+ ownVersion(ctx.revisions, path),
298
326
  );
299
327
  }
300
328
  const [mount] = ctx.mounts;
301
- return mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
329
+ const mounted = mount ? mountedRefFor(value, ctx.documentDir, mount) : null;
330
+ /* In site space a reference is otherwise left as written, which after an in-place overwrite is
331
+ the same string naming new bytes. So a path Studio has just written gets its repaint key here
332
+ too, on the mounted URL or on the reference as written. The lane lookup only runs when Studio
333
+ holds a key at all, which on most sessions is never. */
334
+ const revision = siteRevision(value, ctx);
335
+ return revision === undefined ? mounted : withRevision(mounted ?? value, revision);
336
+ }
337
+
338
+ /** The repaint key for the project file a site-space reference names, or undefined. */
339
+ function siteRevision(value: string, ctx: AssetContext): string | undefined {
340
+ if (!ctx.revisions || Object.keys(ctx.revisions).length === 0) {
341
+ return undefined;
342
+ }
343
+ const path = projectPathForRef(value, ctx.documentDir, ctx.mounts, ctx.lanes);
344
+ return path === null ? undefined : ownVersion(ctx.revisions, path);
302
345
  }
303
346
 
304
347
  /**
@@ -51,6 +51,12 @@ import { parseJsonDocument, serializeJson } from "@jxsuite/schema/json-layout";
51
51
  import { detachGridPanel, gridPanelMounted, renderGridMode } from "../grid/grid-panel";
52
52
  import { detachEntryPane, entryPaneMounted, renderEntryMode } from "../content/entry-editor";
53
53
  import { detachMediaPane, mediaPaneMounted, renderMediaMode } from "../media/media-pane";
54
+ import {
55
+ IMAGE_MODE,
56
+ detachImageEditor,
57
+ imageEditorMounted,
58
+ renderImageMode,
59
+ } from "../media/image-editor";
54
60
  import {
55
61
  detachVariablesView,
56
62
  renderVariablesMode,
@@ -629,6 +635,7 @@ function resetCanvasView(surface: CanvasSurface) {
629
635
  detachEntryPane(paneId);
630
636
  detachWritingBanner(paneId);
631
637
  detachMediaPane(paneId);
638
+ detachImageEditor(paneId);
632
639
  detachVariablesView(paneId);
633
640
  detachWorkspaceHome(paneId);
634
641
  disposeSourceCollab(surface);
@@ -1182,6 +1189,11 @@ function renderCanvasImpl(surface: CanvasSurface) {
1182
1189
  return;
1183
1190
  }
1184
1191
 
1192
+ // Image-editor fast-path, same shape: a gesture repaints the editor through its own effect.
1193
+ if (canvasMode === IMAGE_MODE && imageEditorMounted(surface.paneId, tab) && !modeChanged) {
1194
+ return;
1195
+ }
1196
+
1185
1197
  // Variables fast-path, same shape: the view follows the document through its own effect.
1186
1198
  if (
1187
1199
  canvasMode === PROJECT_VARIABLES_VIEW &&
@@ -1267,6 +1279,9 @@ function renderCanvasImpl(surface: CanvasSurface) {
1267
1279
  // And for the media viewer, whose effect scope subscribes to the tab's path
1268
1280
  detachMediaPane(surface.paneId);
1269
1281
 
1282
+ // And for the Image editor, whose effect scope subscribes to the tab's recipe
1283
+ detachImageEditor(surface.paneId);
1284
+
1270
1285
  // And for the Variables view, whose effect scope subscribes to the document's style
1271
1286
  detachVariablesView(surface.paneId);
1272
1287
 
@@ -1404,6 +1419,15 @@ function renderCanvasImpl(surface: CanvasSurface) {
1404
1419
  return;
1405
1420
  }
1406
1421
 
1422
+ /* Image mode: the media viewer's editing sibling (`media/image-editor.ts`, §9.5). Same pattern;
1423
+ its recipe lives with the tab, so a teardown here loses no unsaved work. */
1424
+ if (canvasMode === IMAGE_MODE) {
1425
+ canvasWrap.style.padding = "0";
1426
+ canvasWrap.style.display = "block";
1427
+ renderImageMode(surface, tab);
1428
+ return;
1429
+ }
1430
+
1407
1431
  /* The Styles workspace's Variables view: `project.json`'s CSS custom properties, edited in a
1408
1432
  form (`style/variables-view.ts`). Like a home, it writes nothing onto the stage: its own style
1409
1433
  block fills it (`surfaces/styles-variables.json`). */
@@ -2003,6 +2027,7 @@ setSurfaceTeardown((surface) => {
2003
2027
  detachGridPanel(surface.paneId);
2004
2028
  detachEntryPane(surface.paneId);
2005
2029
  detachMediaPane(surface.paneId);
2030
+ detachImageEditor(surface.paneId);
2006
2031
  detachVariablesView(surface.paneId);
2007
2032
  detachWorkspaceHome(surface.paneId);
2008
2033
  detachWritingBanner(surface.paneId);
@@ -145,6 +145,8 @@ const FILE_DRAG_SRC: DragSrcKind = { type: "block" };
145
145
  * every reference unversioned — the behaviour from before versions existed.
146
146
  */
147
147
  let assetVersions: Readonly<Record<string, string>> = {};
148
+ /** Studio's repaint keys, posted beside the versions in the same message (studio.md §3.4). */
149
+ let assetRevisions: Readonly<Record<string, string>> = {};
148
150
  /**
149
151
  * The asset context of the newest render accepted, WITHOUT versions — kept so a later
150
152
  * `assetVersions` can rebuild the resolver around the new map. Null when that render resolves
@@ -154,7 +156,7 @@ let liveAssets: AssetContext | null = null;
154
156
 
155
157
  /** A render's asset context with the current versions folded in, or null for no resolution. */
156
158
  function versionedAssets(assets: AssetContext | null): AssetContext | null {
157
- return assets ? { ...assets, versions: assetVersions } : null;
159
+ return assets ? { ...assets, revisions: assetRevisions, versions: assetVersions } : null;
158
160
  }
159
161
 
160
162
  /**
@@ -307,6 +309,7 @@ export function startCanvasIframe(opts: {
307
309
  const { channel, container } = opts;
308
310
  // A frame starts knowing no versions; the host posts the map before the first render.
309
311
  assetVersions = {};
312
+ assetRevisions = {};
310
313
  liveAssets = null;
311
314
  let handle: RenderHandle | null = null;
312
315
  // Disposer for the live render's dataScope re-post effect (see the render handler); stopped
@@ -923,6 +926,7 @@ export function startCanvasIframe(opts: {
923
926
  old map, and a patch draws through that resolver — so it is rebuilt around the new one
924
927
  here, not left for the next render. */
925
928
  assetVersions = msg.versions;
929
+ assetRevisions = msg.revisions ?? {};
926
930
  const assets = versionedAssets(liveAssets);
927
931
  if (assets) {
928
932
  setCanvasAssetResolver((value) => resolveAssetRef(value, assets));
@@ -50,7 +50,11 @@ import { isNodeObject } from "@jxsuite/schema/guards";
50
50
  import { isPopover } from "@jxsuite/schema/overlays";
51
51
  import { localeDirection } from "@jxsuite/schema/locale";
52
52
  import { getPlatform, hasPlatform } from "../platform";
53
- import { assetVersionsEpoch, assetVersionsSnapshot } from "../files/asset-versions";
53
+ import {
54
+ assetVersionsEpoch,
55
+ assetVersionsSnapshot,
56
+ localRevisionsSnapshot,
57
+ } from "../files/asset-versions";
54
58
  import type {
55
59
  ApplyFormatIntent,
56
60
  CanvasMode,
@@ -150,6 +154,8 @@ export interface HostState {
150
154
  * changes identity only when a media version moves.
151
155
  */
152
156
  postedVersions: Readonly<Record<string, string>> | null;
157
+ /** The repaint-key snapshot posted beside {@link HostState.postedVersions}, compared the same way. */
158
+ postedRevisions: Readonly<Record<string, string>> | null;
153
159
  overlay: OverlayLayer;
154
160
  /** Primary selected path (mirrors `session.selection`'s last entry), for hover de-dupe. */
155
161
  selectionPath: (string | number)[] | null;
@@ -1606,15 +1612,17 @@ function syncAssetVersions(host: HostState): void {
1606
1612
  return;
1607
1613
  }
1608
1614
  const versions = assetVersionsSnapshot();
1609
- if (versions === host.postedVersions) {
1615
+ const revisions = localRevisionsSnapshot();
1616
+ if (versions === host.postedVersions && revisions === host.postedRevisions) {
1610
1617
  return;
1611
1618
  }
1612
- if (host.postedVersions === null && Object.keys(versions).length === 0) {
1613
- host.postedVersions = versions;
1619
+ const first = host.postedVersions === null;
1620
+ host.postedVersions = versions;
1621
+ host.postedRevisions = revisions;
1622
+ if (first && Object.keys(versions).length === 0 && Object.keys(revisions).length === 0) {
1614
1623
  return;
1615
1624
  }
1616
- host.postedVersions = versions;
1617
- host.channel.post({ kind: "assetVersions", versions });
1625
+ host.channel.post({ kind: "assetVersions", revisions, versions });
1618
1626
  }
1619
1627
 
1620
1628
  let assetVersionsWatchStarted = false;
@@ -1990,6 +1998,7 @@ function ensureHost(canvasEl: HTMLElement): HostState {
1990
1998
  panReqId: -1,
1991
1999
  pending: null,
1992
2000
  postedVersions: null,
2001
+ postedRevisions: null,
1993
2002
  idle: null,
1994
2003
  pendingEnterEdit: null,
1995
2004
  pendingMeasures: new Map(),
@@ -2143,6 +2152,7 @@ function handleMessage(state: HostState, msg: IframeToParent): void {
2143
2152
  renders, and a FIFO channel makes "posted first" mean "known first". A frame that said
2144
2153
  `ready` again has rebooted and lost what it was sent before. */
2145
2154
  state.postedVersions = null;
2155
+ state.postedRevisions = null;
2146
2156
  syncAssetVersions(state);
2147
2157
  if (state.pending) {
2148
2158
  state.channel.post(state.pending);
@@ -164,7 +164,15 @@ export type ParentToIframe =
164
164
  * latest map and re-installs its asset resolver around it, so the next node a patch draws already
165
165
  * uses it. A frame built before this message existed ignores it and keeps resolving unversioned.
166
166
  */
167
- | { kind: "assetVersions"; versions: Readonly<Record<string, string>> }
167
+ | {
168
+ kind: "assetVersions";
169
+ versions: Readonly<Record<string, string>>;
170
+ /**
171
+ * Studio's own repaint keys (studio.md §3.4), for paths Studio itself just wrote in place.
172
+ * Optional so a host that predates them still speaks the message; absent reads as none.
173
+ */
174
+ revisions?: Readonly<Record<string, string>>;
175
+ }
168
176
  // Flip the forced color-scheme preview on the iframe root without re-rendering — a document-level
169
177
  // Idempotent attribute write, deliberately gen-less (like endEdit).
170
178
  | { kind: "setColorScheme"; scheme: "light" | "dark" | null }
@@ -69,6 +69,7 @@ export type EditorKind =
69
69
  | "tokens"
70
70
  | "entry"
71
71
  | "media"
72
+ | "image"
72
73
  | "home"
73
74
  | "none";
74
75
 
@@ -112,6 +113,9 @@ const EDITOR_KIND_BY_MODE = new Map<string, EditorKind>([
112
113
  can SHOW but has no document model for, which is why clicking one used to end in "No format
113
114
  class imported for …", advice that means nothing about a PNG. */
114
115
  ["media", "media"],
116
+ /* The Image editor — `media/image-editor.ts`'s IMAGE_MODE, the media viewer's editing sibling
117
+ (§9.5). A recipe over one image: nothing to paste into, so not the Canvas key scope. */
118
+ ["image", "image"],
115
119
  ["preview", "canvas"],
116
120
  ["source", "code"],
117
121
  ["stylebook", "config"],
@@ -143,6 +147,7 @@ export const EDITOR_KIND_LABELS: Readonly<Record<EditorKind, string>> = {
143
147
  entry: "Entry",
144
148
  grid: "Grid",
145
149
  home: "Home",
150
+ image: "Image",
146
151
  media: "Media",
147
152
  none: "None",
148
153
  tokens: "Variables",
@@ -30,7 +30,7 @@ import { resolveI18n } from "@jxsuite/schema/locale";
30
30
  import { componentRegistry } from "../files/components";
31
31
  import { getNodeAtPath, projectState } from "../store";
32
32
  import { shell } from "../shell";
33
- import { activeTab, workspace } from "../workspace/workspace";
33
+ import { activeTab, tabHoldsEditBesideDocument, workspace } from "../workspace/workspace";
34
34
  import { primarySelection } from "../tabs/selection";
35
35
  import { canRedo, canUndo } from "../tabs/transact";
36
36
  import { hasTyping } from "../tabs/typing";
@@ -140,7 +140,10 @@ export function createLiveContext(sources: LiveContextSources): () => CommandCon
140
140
 
141
141
  ctx.document.open = tab !== null;
142
142
  // Typing still in a source buffer is unsaved work too, though the document has not seen it.
143
- ctx.document.dirty = tab ? tab.doc.dirty === true || hasTyping(tab.id) : false;
143
+ // So is an image edit, which is held beside the document rather than in it (§9.5).
144
+ ctx.document.dirty = tab
145
+ ? tab.doc.dirty === true || hasTyping(tab.id) || tabHoldsEditBesideDocument(tab)
146
+ : false;
144
147
  ctx.document.mode = tab?.doc.mode ?? "";
145
148
  ctx.document.canUndo = tab ? canUndo(tab) : false;
146
149
  ctx.document.canRedo = tab ? canRedo(tab) : false;
@@ -1006,6 +1006,9 @@ const SELF_SCROLLING_MODES: ReadonlySet<string> = new Set([
1006
1006
  /* `media` — the Media viewer's own scrolling column (`media/media-pane.ts`). There is no artboard
1007
1007
  here either: a wheel over an image is a scroll down the page it sits on, not a zoom. */
1008
1008
  "media",
1009
+ /* `image` — the Image editor (`media/image-editor.ts`). Its picture fits the stage and its
1010
+ controls scroll in their own column, so a wheel has no artboard to pan or zoom. */
1011
+ "image",
1009
1012
  /* A workspace's home — `workspace/workspace-home.ts`. A centred column with nothing to zoom. */
1010
1013
  "media-home",
1011
1014
  "preview",