@jxsuite/studio 10.1.0 → 10.3.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 (303) hide show
  1. package/dist/chunks/{2020-mkb0qa6p.js → 2020-906vx2dq.js} +3 -3
  2. package/dist/chunks/adapter-hnvad61x.js +4 -0
  3. package/dist/chunks/add-new-flow-3tkn72ey.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-1vzxq1ba.js +4 -0
  8. package/dist/chunks/convert-to-repeater-gwzh9gar.js +4 -0
  9. package/dist/chunks/editor-wvf5e6j9.js +4 -0
  10. package/dist/chunks/entry-commands-mx2whrjx.js +4 -0
  11. package/dist/chunks/extension-sections-338ee35t.js +4 -0
  12. package/dist/chunks/file-ops-9bxvk59j.js +4 -0
  13. package/dist/chunks/files-t07yqqej.js +4 -0
  14. package/dist/chunks/format-host-5ja5gxyk.js +4 -0
  15. package/dist/chunks/fs-events-3424ezp3.js +4 -0
  16. package/dist/chunks/grid-open-1mpq9mzb.js +4 -0
  17. package/dist/chunks/{grid-view-r22jsqht.js → grid-view-w9nfy203.js} +15 -15
  18. package/dist/chunks/icons-gb967qc8.js +4 -0
  19. package/dist/chunks/iframe-host-wsg0bn73.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-pxwwf00y.js → index-yjrfeks8.js} +3 -3
  24. package/dist/chunks/{javascript-pzqryby6.js → javascript-hy2mws8v.js} +3 -3
  25. package/dist/chunks/{jsonMode-tcnyxyap.js → jsonMode-vj30te2h.js} +6 -6
  26. package/dist/chunks/jx-validate-ttr5ewf0.js +4 -0
  27. package/dist/chunks/layers-m49nmwvx.js +4 -0
  28. package/dist/chunks/layers-panel-hvy2e549.js +4 -0
  29. package/dist/chunks/lit-html-k2mb9k0r.js +4 -0
  30. package/dist/chunks/media-library-ae67q4at.js +4 -0
  31. package/dist/chunks/media-open-yp9jm834.js +4 -0
  32. package/dist/chunks/media-picker-s41ngdfr.js +4 -0
  33. package/dist/chunks/{monaco-binding-bqpge64f.js → monaco-binding-a51py33c.js} +3 -3
  34. package/dist/chunks/monaco-buffer-pa0g6ax7.js +4 -0
  35. package/dist/chunks/{monaco-setup-1mkab762.js → monaco-setup-g9kterhx.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-gzfxcm0t.js +4 -0
  40. package/dist/chunks/publish-quick-w3nxd7q4.js +4 -0
  41. package/dist/chunks/publish-workspace-dppd82tc.js +4 -0
  42. package/dist/chunks/quick-search-tdxaam55.js +4 -0
  43. package/dist/chunks/references-vqyr5s9x.js +4 -0
  44. package/dist/chunks/right-panel-0yrw4cgz.js +4 -0
  45. package/dist/chunks/source-control-6fk72me2.js +4 -0
  46. package/dist/chunks/{studio-0g2sm01j.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-rf8d6p9f.js → studio-0w3541dg.js} +6 -6
  50. package/dist/chunks/{studio-9b7177jq.js → studio-0x6f3nk9.js} +4 -4
  51. package/dist/chunks/studio-1410q5dw.js +5 -0
  52. package/dist/chunks/{studio-8qejv8yf.js → studio-15hwqgd8.js} +4 -4
  53. package/dist/chunks/studio-1ks5d9y4.js +5 -0
  54. package/dist/chunks/{studio-881z1xrx.js → studio-2ggfzrrd.js} +4 -4
  55. package/dist/chunks/{studio-jw0q5j6k.js → studio-321ym1tt.js} +4 -4
  56. package/dist/chunks/studio-3c10v2q1.js +5 -0
  57. package/dist/chunks/{studio-9rcbvksg.js → studio-3na1r48j.js} +8 -8
  58. package/dist/chunks/{studio-ggm7zf70.js → studio-3q7mhbgx.js} +5 -5
  59. package/dist/chunks/studio-3qr08sxq.js +4 -0
  60. package/dist/chunks/studio-3xk9zdgb.js +5 -0
  61. package/dist/chunks/studio-423tfdte.js +5 -0
  62. package/dist/chunks/{studio-brdp85q7.js → studio-45mj4jqe.js} +4 -4
  63. package/dist/chunks/studio-4f7thq0p.js +5 -0
  64. package/dist/chunks/{studio-w67vfgy5.js → studio-4fnb5648.js} +4 -4
  65. package/dist/chunks/studio-4zd6an2b.js +5 -0
  66. package/dist/chunks/studio-53wy9ek3.js +5 -0
  67. package/dist/chunks/studio-5n193zqd.js +5 -0
  68. package/dist/chunks/{studio-x2metr13.js → studio-5tet77de.js} +3 -3
  69. package/dist/chunks/studio-7rrdbvex.js +5 -0
  70. package/dist/chunks/studio-8kx6m0rg.js +5 -0
  71. package/dist/chunks/{studio-va0e63sk.js → studio-90zkgne5.js} +7 -7
  72. package/dist/chunks/studio-9c6cbq2c.js +4 -0
  73. package/dist/chunks/studio-9dq7rwc7.js +5 -0
  74. package/dist/chunks/studio-9qx1ywbv.js +9 -0
  75. package/dist/chunks/studio-a65kbs2e.js +9 -0
  76. package/dist/chunks/studio-aam8zt8z.js +5 -0
  77. package/dist/chunks/{studio-8jb31dcf.js → studio-abh9kfdh.js} +4 -4
  78. package/dist/chunks/studio-b4e0ftkt.js +5 -0
  79. package/dist/chunks/studio-b68k0z00.js +5 -0
  80. package/dist/chunks/{studio-dy2xtt7a.js → studio-bct64zmj.js} +4 -4
  81. package/dist/chunks/studio-bz0b5brj.js +6 -0
  82. package/dist/chunks/studio-cfnn70h1.js +5 -0
  83. package/dist/chunks/studio-daksw8kh.js +5 -0
  84. package/dist/chunks/{studio-mwnmzx7g.js → studio-e7vjyp04.js} +7 -7
  85. package/dist/chunks/studio-ea6r1zyr.js +5 -0
  86. package/dist/chunks/studio-egnjpyde.js +9 -0
  87. package/dist/chunks/studio-ejrypvmq.js +5 -0
  88. package/dist/chunks/studio-eqcddvc2.js +928 -0
  89. package/dist/chunks/studio-f8tyeyzt.js +5 -0
  90. package/dist/chunks/studio-fj6c905s.js +5 -0
  91. package/dist/chunks/{studio-hsr3k06v.js → studio-fkrr7kgg.js} +5 -5
  92. package/dist/chunks/studio-fspe7m9t.js +5 -0
  93. package/dist/chunks/{studio-4zqfw5ha.js → studio-g0hxtcga.js} +4 -4
  94. package/dist/chunks/studio-g9brbewz.js +5 -0
  95. package/dist/chunks/studio-ggx3j7nd.js +5 -0
  96. package/dist/chunks/{studio-jh6t5qz4.js → studio-hfxttkyr.js} +4 -4
  97. package/dist/chunks/studio-hh394phe.js +5 -0
  98. package/dist/chunks/{studio-07k0geap.js → studio-j0zf5x2x.js} +4 -4
  99. package/dist/chunks/studio-jeb7hth3.js +5 -0
  100. package/dist/chunks/studio-k0xzrse2.js +64 -0
  101. package/dist/chunks/studio-k1xqrgm5.js +5 -0
  102. package/dist/chunks/studio-k2evngrj.js +7 -0
  103. package/dist/chunks/studio-k3we3kqb.js +5 -0
  104. package/dist/chunks/{studio-nf0pqpbj.js → studio-n9qba0gx.js} +4 -4
  105. package/dist/chunks/studio-nf8esxxc.js +5 -0
  106. package/dist/chunks/studio-nn4f9m9c.js +5 -0
  107. package/dist/chunks/studio-nskxe9q1.js +6 -0
  108. package/dist/chunks/{studio-4nkm4404.js → studio-nvcn2ykc.js} +4 -4
  109. package/dist/chunks/studio-nwynxqtb.js +5 -0
  110. package/dist/chunks/{studio-f2j9et3h.js → studio-nzemfbkk.js} +8 -8
  111. package/dist/chunks/studio-p2a03pgm.js +5 -0
  112. package/dist/chunks/{studio-skynbac3.js → studio-pm17wdc4.js} +4 -4
  113. package/dist/chunks/{studio-9cp96v90.js → studio-q80fg1d6.js} +7 -7
  114. package/dist/chunks/studio-r050sez0.js +6 -0
  115. package/dist/chunks/{studio-sd07xva5.js → studio-s7t4vp1d.js} +5 -5
  116. package/dist/chunks/studio-s9fng7m8.js +7 -0
  117. package/dist/chunks/studio-sce30r7b.js +4 -0
  118. package/dist/chunks/{studio-zxm98z2m.js → studio-stahswgb.js} +6 -6
  119. package/dist/chunks/studio-swedpd82.js +5 -0
  120. package/dist/chunks/studio-thz5t120.js +5 -0
  121. package/dist/chunks/studio-tpxcryf2.js +5 -0
  122. package/dist/chunks/{studio-tpy3rdxn.js → studio-v5wktj6f.js} +4 -4
  123. package/dist/chunks/{studio-7vb997ff.js → studio-vpevkfxw.js} +4 -4
  124. package/dist/chunks/studio-waekn290.js +5 -0
  125. package/dist/chunks/studio-wfct4h6n.js +5 -0
  126. package/dist/chunks/studio-wgc54sxf.js +5 -0
  127. package/dist/chunks/studio-wqfaqw4m.js +5 -0
  128. package/dist/chunks/studio-wse1rs8t.js +7 -0
  129. package/dist/chunks/studio-x0gsfr1d.js +16 -0
  130. package/dist/chunks/studio-xpx91fby.js +6 -0
  131. package/dist/chunks/studio-y1b86ast.js +5 -0
  132. package/dist/chunks/studio-yrcrncev.js +5 -0
  133. package/dist/chunks/{studio-zacvfm31.js → studio-yzpskb0j.js} +4 -4
  134. package/dist/chunks/studio-zfdbmhp3.js +5 -0
  135. package/dist/chunks/{tsMode-6vwhy6ty.js → tsMode-jg1dtphj.js} +7 -7
  136. package/dist/chunks/workspace-9btkc8tk.js +4 -0
  137. package/dist/chunks/workspace-home-qcc4z5ag.js +4 -0
  138. package/dist/chunks/{ws-client-zcxkfgmf.js → ws-client-arz2qsy9.js} +3 -3
  139. package/dist/iframe-entry.js +13 -13
  140. package/dist/manifest.json +83 -77
  141. package/dist/studio.js +74 -75
  142. package/package.json +5 -5
  143. package/src/browse/library-model.ts +56 -23
  144. package/src/browse/library-pane.ts +44 -29
  145. package/src/browse/library-preview.ts +6 -2
  146. package/src/canvas/asset-refs.ts +13 -4
  147. package/src/canvas/asset-resolve.ts +54 -11
  148. package/src/canvas/canvas-render.ts +25 -0
  149. package/src/canvas/iframe-entry.ts +5 -1
  150. package/src/canvas/iframe-host.ts +16 -6
  151. package/src/canvas/iframe-protocol.ts +9 -1
  152. package/src/commands/context.ts +6 -0
  153. package/src/commands/live-context.ts +5 -2
  154. package/src/editor/shortcuts.ts +6 -0
  155. package/src/files/asset-versions.ts +108 -2
  156. package/src/files/file-ops.ts +11 -1
  157. package/src/files/files.ts +111 -48
  158. package/src/files/media-paths.ts +12 -4
  159. package/src/files/media-upload.ts +195 -21
  160. package/src/files/new-file-name.ts +39 -1
  161. package/src/media/image-bake.ts +164 -0
  162. package/src/media/image-editor.ts +846 -0
  163. package/src/media/image-format.ts +262 -0
  164. package/src/media/image-recipe.ts +651 -0
  165. package/src/media/media-commands.ts +65 -1
  166. package/src/media/media-library-model.ts +1 -1
  167. package/src/media/media-library.ts +175 -15
  168. package/src/media/media-open.ts +156 -3
  169. package/src/media/media-pane.ts +8 -4
  170. package/src/panels/data-grid.ts +0 -4
  171. package/src/panels/pane-context.ts +6 -1
  172. package/src/panels/settings-menu.ts +3 -3
  173. package/src/panels/settings-pane.ts +42 -56
  174. package/src/panels/tab-strip.ts +12 -3
  175. package/src/services/ai-import-tools.ts +6 -3
  176. package/src/services/ai-models.ts +65 -11
  177. package/src/services/ai-settings.ts +45 -5
  178. package/src/services/document-assistant.ts +5 -2
  179. package/src/services/settings/definitions.ts +19 -0
  180. package/src/settings/contributed-section.ts +16 -7
  181. package/src/settings/css-vars-editor.ts +4 -4
  182. package/src/settings/general-settings.ts +1 -4
  183. package/src/settings/preferences-accounts.ts +2 -2
  184. package/src/settings/preferences-dialog.ts +4 -2
  185. package/src/settings/project-sections.ts +1 -1
  186. package/src/settings/section-registry.ts +5 -4
  187. package/src/settings/settings-document.ts +33 -24
  188. package/src/settings/settings-workspace.ts +205 -0
  189. package/src/shell.ts +31 -6
  190. package/src/studio.ts +13 -2
  191. package/src/surfaces/ai-credentials-form.json +59 -5
  192. package/src/surfaces/ai-credentials-form.ts +15 -2
  193. package/src/surfaces/commandbar.ts +7 -3
  194. package/src/surfaces/image-editor.json +1390 -0
  195. package/src/surfaces/image-editor.ts +272 -0
  196. package/src/surfaces/media-library.json +129 -1
  197. package/src/surfaces/media-library.ts +17 -2
  198. package/src/surfaces/settings-contributed.json +4 -4
  199. package/src/surfaces/settings-contributed.ts +2 -2
  200. package/src/surfaces/settings-pane.json +262 -180
  201. package/src/surfaces/settings-pane.ts +63 -14
  202. package/src/types.ts +2 -1
  203. package/src/ui/ai-credentials-form.ts +128 -11
  204. package/src/ui/regions.ts +5 -5
  205. package/src/workspace/tab-workspace.ts +5 -3
  206. package/src/workspace/workspace-home.ts +18 -4
  207. package/src/workspace/workspace-ids.ts +40 -7
  208. package/src/workspace/workspace-switch.ts +3 -1
  209. package/src/workspace/workspace.ts +22 -1
  210. package/dist/chunks/adapter-xcpsmecx.js +0 -4
  211. package/dist/chunks/add-new-flow-ptmhmzr9.js +0 -4
  212. package/dist/chunks/cf-account-picker-z1sgwpwd.js +0 -4
  213. package/dist/chunks/cf-settings-xqvhxxe9.js +0 -4
  214. package/dist/chunks/components-whsc81a7.js +0 -4
  215. package/dist/chunks/content-source-jwtxg78w.js +0 -4
  216. package/dist/chunks/convert-to-repeater-f92ey3kp.js +0 -4
  217. package/dist/chunks/editor-zdqdpq1x.js +0 -4
  218. package/dist/chunks/entry-commands-w8p2fbpt.js +0 -4
  219. package/dist/chunks/extension-sections-y2p1te2r.js +0 -4
  220. package/dist/chunks/file-ops-13ab4swj.js +0 -4
  221. package/dist/chunks/files-zbc8p6v1.js +0 -4
  222. package/dist/chunks/format-host-zjsbktbx.js +0 -4
  223. package/dist/chunks/fs-events-t6x0hch6.js +0 -4
  224. package/dist/chunks/grid-open-fyzdwpkh.js +0 -4
  225. package/dist/chunks/icons-853n6qt3.js +0 -4
  226. package/dist/chunks/iframe-host-kbyqc7pt.js +0 -4
  227. package/dist/chunks/index-6nvj89vb.js +0 -4
  228. package/dist/chunks/jx-validate-5sp8a4rm.js +0 -4
  229. package/dist/chunks/layers-panel-qrxj9vr2.js +0 -4
  230. package/dist/chunks/layers-tpzayggx.js +0 -4
  231. package/dist/chunks/lit-html-x6nrxsx3.js +0 -4
  232. package/dist/chunks/media-library-ywppnjrz.js +0 -4
  233. package/dist/chunks/media-picker-etxgc8qq.js +0 -4
  234. package/dist/chunks/monaco-buffer-qrnqhk0b.js +0 -4
  235. package/dist/chunks/notify-kcv38wwe.js +0 -4
  236. package/dist/chunks/parse-df2kkmhc.js +0 -4
  237. package/dist/chunks/platform-dtsysmyd.js +0 -4
  238. package/dist/chunks/publish-panel-evb1rcxt.js +0 -4
  239. package/dist/chunks/publish-quick-m9vkhahq.js +0 -4
  240. package/dist/chunks/publish-workspace-ktk9h536.js +0 -4
  241. package/dist/chunks/quick-search-2f47fxcw.js +0 -4
  242. package/dist/chunks/references-5w6ajjv7.js +0 -4
  243. package/dist/chunks/right-panel-kz0a6byf.js +0 -4
  244. package/dist/chunks/source-control-1phx90zk.js +0 -4
  245. package/dist/chunks/studio-0hr000ba.js +0 -5
  246. package/dist/chunks/studio-1wyp7dt5.js +0 -5
  247. package/dist/chunks/studio-20ap8dds.js +0 -5
  248. package/dist/chunks/studio-291gqh98.js +0 -5
  249. package/dist/chunks/studio-37twdaf7.js +0 -64
  250. package/dist/chunks/studio-4smcnv33.js +0 -5
  251. package/dist/chunks/studio-56egy6vf.js +0 -5
  252. package/dist/chunks/studio-5dmgmyw4.js +0 -5
  253. package/dist/chunks/studio-5e7p1vgd.js +0 -5
  254. package/dist/chunks/studio-5m0w815d.js +0 -5
  255. package/dist/chunks/studio-695k95bw.js +0 -5
  256. package/dist/chunks/studio-6vwvshw8.js +0 -6
  257. package/dist/chunks/studio-732mw0g8.js +0 -5
  258. package/dist/chunks/studio-76t560kn.js +0 -6
  259. package/dist/chunks/studio-7823f0g8.js +0 -5
  260. package/dist/chunks/studio-8486jn4r.js +0 -5
  261. package/dist/chunks/studio-848rk4p1.js +0 -5
  262. package/dist/chunks/studio-8x8616xd.js +0 -5
  263. package/dist/chunks/studio-a3s6frde.js +0 -4
  264. package/dist/chunks/studio-ahf3cspq.js +0 -4
  265. package/dist/chunks/studio-bkmv1h3e.js +0 -9
  266. package/dist/chunks/studio-cgb97rwv.js +0 -5
  267. package/dist/chunks/studio-ch8gt8vt.js +0 -5
  268. package/dist/chunks/studio-cqf1k4qk.js +0 -5
  269. package/dist/chunks/studio-cvz0e00f.js +0 -5
  270. package/dist/chunks/studio-dbgv4fv9.js +0 -16
  271. package/dist/chunks/studio-eesyjg7h.js +0 -9
  272. package/dist/chunks/studio-et6t8g4m.js +0 -4
  273. package/dist/chunks/studio-fep7hr66.js +0 -7
  274. package/dist/chunks/studio-fyf7j57j.js +0 -5
  275. package/dist/chunks/studio-g5prntab.js +0 -5
  276. package/dist/chunks/studio-ga77mf07.js +0 -5
  277. package/dist/chunks/studio-h7gy12h2.js +0 -5
  278. package/dist/chunks/studio-j933k8nr.js +0 -5
  279. package/dist/chunks/studio-jgbvqcgh.js +0 -5
  280. package/dist/chunks/studio-k4xc5g6h.js +0 -5
  281. package/dist/chunks/studio-p0nf2e1f.js +0 -6
  282. package/dist/chunks/studio-pd9dsqpq.js +0 -5
  283. package/dist/chunks/studio-pmzg17fj.js +0 -5
  284. package/dist/chunks/studio-pzfa2vmb.js +0 -5
  285. package/dist/chunks/studio-qyb5bebs.js +0 -9
  286. package/dist/chunks/studio-rt498t2v.js +0 -5
  287. package/dist/chunks/studio-s0fg2wav.js +0 -5
  288. package/dist/chunks/studio-t3j8htp7.js +0 -7
  289. package/dist/chunks/studio-thj81e98.js +0 -7
  290. package/dist/chunks/studio-tyw7tk0x.js +0 -5
  291. package/dist/chunks/studio-vcz63n42.js +0 -5
  292. package/dist/chunks/studio-vffwe9k7.js +0 -5
  293. package/dist/chunks/studio-vjhej0w5.js +0 -928
  294. package/dist/chunks/studio-vxjap704.js +0 -5
  295. package/dist/chunks/studio-xebavr9s.js +0 -5
  296. package/dist/chunks/studio-yg5n49za.js +0 -5
  297. package/dist/chunks/studio-yhz5b313.js +0 -5
  298. package/dist/chunks/studio-zsjpsw08.js +0 -5
  299. package/dist/chunks/workspace-d65bz1qk.js +0 -4
  300. package/dist/chunks/workspace-home-vbg9b3jh.js +0 -4
  301. package/src/settings/settings-dialog.ts +0 -192
  302. package/src/surfaces/settings-dialog.json +0 -51
  303. package/src/surfaces/settings-dialog.ts +0 -169
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "10.1.0",
3
+ "version": "10.3.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.41.2",
63
+ "@jxsuite/ai": "^0.42.0",
64
64
  "@jxsuite/collab": "^0.11.4",
65
- "@jxsuite/create": "^1.5.1",
65
+ "@jxsuite/create": "^1.5.2",
66
66
  "@jxsuite/formulas": "^0.0.26",
67
67
  "@jxsuite/markup": "^1.0.2",
68
- "@jxsuite/protocol": "^6.1.0",
68
+ "@jxsuite/protocol": "^6.2.0",
69
69
  "@jxsuite/runtime": "^6.0.1",
70
70
  "@jxsuite/schema": "^5.0.0",
71
71
  "@jxsuite/site": "^2.2.2",
72
- "@jxsuite/ui": "^0.8.2",
72
+ "@jxsuite/ui": "^0.9.0",
73
73
  "@vue/reactivity": "3.5.44",
74
74
  "ajv": "^8.20.0",
75
75
  "ajv-formats": "^3.0.1",
@@ -200,44 +200,64 @@ export function entryCollectionOf(path: string, pageExtensions?: readonly string
200
200
  */
201
201
  const SKIP_DIRS = new Set(["node_modules", ".git", ".jx", ".cache"]);
202
202
 
203
+ /** What every branch of one scan shares. */
204
+ interface ScanRun {
205
+ platform: Pick<StudioPlatform, "listDirectory">;
206
+ out: LibraryFile[];
207
+ failures: ScanFailure[];
208
+ i18n: ResolvedI18n | null;
209
+ pageExtensions: readonly string[] | undefined;
210
+ /**
211
+ * Every directory some branch has claimed, the roots included. A collection may live in an
212
+ * ANCESTOR of the project (a site inside the vault it publishes declares `source: "../.."`), and
213
+ * that walk reaches the project's own directories under the same project-relative paths the roots
214
+ * walk: every page was listed twice. A directory is walked by whichever branch claims it first.
215
+ */
216
+ claimed: Set<string>;
217
+ }
218
+
219
+ /**
220
+ * The project root as a listing names it from outside: `""` (relative to itself) or `"."`. A walk
221
+ * from an ancestor stops there; the project's own directories are roots of the scan already, and
222
+ * the rest of the root (`dist/`, `scripts/`) holds no collection's entries.
223
+ */
224
+ function isProjectRoot(path: string): boolean {
225
+ const normalized = normalizeDir(path);
226
+ return normalized === "" || normalized === ".";
227
+ }
228
+
203
229
  /**
204
230
  * Walk one directory tree, collecting files and recording — never swallowing — what it could not
205
231
  * read. A failure at depth is reported against the directory that failed, not the root, so the
206
232
  * Problem names the path the author can act on.
207
233
  */
208
- async function walk(
209
- dir: string,
210
- platform: Pick<StudioPlatform, "listDirectory">,
211
- out: LibraryFile[],
212
- failures: ScanFailure[],
213
- i18n: ResolvedI18n | null,
214
- optional = false,
215
- pageExtensions?: readonly string[],
216
- ): Promise<void> {
234
+ async function walk(dir: string, run: ScanRun, optional = false): Promise<void> {
217
235
  let entries: DirEntry[];
218
236
  try {
219
237
  // Noted: the Library walks every media directory, so it is where most versions are learned.
220
- entries = await notedListing(() => platform.listDirectory(dir));
238
+ entries = await notedListing(() => run.platform.listDirectory(dir));
221
239
  } catch (error) {
222
240
  // A declared collection's folder that does not exist yet is an empty collection, not a fault.
223
241
  if (!optional) {
224
- failures.push({ dir, error: errorMessage(error) });
242
+ run.failures.push({ dir, error: errorMessage(error) });
225
243
  }
226
244
  return;
227
245
  }
228
246
  const subdirectories: string[] = [];
229
247
  for (const entry of entries) {
230
248
  if (entry.type === "directory") {
231
- if (!SKIP_DIRS.has(entry.name)) {
249
+ const key = normalizeDir(entry.path);
250
+ if (!SKIP_DIRS.has(entry.name) && !isProjectRoot(key) && !run.claimed.has(key)) {
251
+ run.claimed.add(key);
232
252
  subdirectories.push(entry.path);
233
253
  }
234
254
  continue;
235
255
  }
236
256
  const ext = extensionOf(entry.name);
237
- out.push({
238
- collection: entryCollectionOf(entry.path, pageExtensions),
257
+ run.out.push({
258
+ collection: entryCollectionOf(entry.path, run.pageExtensions),
239
259
  ext,
240
- locale: servedLocaleOfPath(entry.path, i18n) ?? undefined,
260
+ locale: servedLocaleOfPath(entry.path, run.i18n) ?? undefined,
241
261
  media: MEDIA_EXTENSIONS.has(ext),
242
262
  modified: entry.modified,
243
263
  name: entry.name,
@@ -248,9 +268,7 @@ async function walk(
248
268
  }
249
269
  // Siblings in parallel, depth serialized per branch: the platform counts every call as in-flight,
250
270
  // So `probe.idle()` already knows the scan is running without the Library declaring a source.
251
- await Promise.all(
252
- subdirectories.map((sub) => walk(sub, platform, out, failures, i18n, false, pageExtensions)),
253
- );
271
+ await Promise.all(subdirectories.map((sub) => walk(sub, run)));
254
272
  }
255
273
 
256
274
  /**
@@ -265,16 +283,31 @@ export async function scanLibrary(
265
283
  optional: ReadonlySet<string> = new Set(),
266
284
  pageExtensions?: readonly string[],
267
285
  ): Promise<LibraryScan> {
268
- const files: LibraryFile[] = [];
269
- const failures: ScanFailure[] = [];
270
286
  // Resolved ONCE for the whole walk. `resolveI18n` canonicalizes every declared tag through
271
287
  // `Intl.Locale`, and a project with 300 pages would otherwise pay for that per file.
272
288
  const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
289
+ const run: ScanRun = {
290
+ claimed: new Set(),
291
+ failures: [],
292
+ i18n,
293
+ out: [],
294
+ pageExtensions,
295
+ platform,
296
+ };
297
+ /* Every root is claimed before any walk descends: `map` runs to the end synchronously, and a walk
298
+ claims its subdirectories only after its own listing has been awaited. A root named twice is
299
+ walked once. */
273
300
  await Promise.all(
274
- dirs.map((dir) =>
275
- walk(dir, platform, files, failures, i18n, optional.has(dir), pageExtensions),
276
- ),
301
+ dirs.map((dir) => {
302
+ const key = normalizeDir(dir);
303
+ if (run.claimed.has(key)) {
304
+ return Promise.resolve();
305
+ }
306
+ run.claimed.add(key);
307
+ return walk(dir, run, optional.has(dir));
308
+ }),
277
309
  );
310
+ const { failures, out: files } = run;
278
311
  files.sort((a, b) => a.path.localeCompare(b.path));
279
312
  return { failures, files };
280
313
  }
@@ -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
 
@@ -107,10 +108,14 @@ const EDITOR_KIND_BY_MODE = new Map<string, EditorKind>([
107
108
  ["content-home", "home"],
108
109
  ["media-home", "home"],
109
110
  ["publish-home", "home"],
111
+ ["settings-home", "home"],
110
112
  /* The Media viewer — `media/media-pane.ts`. An image, a video, a font specimen: files the studio
111
113
  can SHOW but has no document model for, which is why clicking one used to end in "No format
112
114
  class imported for …", advice that means nothing about a PNG. */
113
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"],
114
119
  ["preview", "canvas"],
115
120
  ["source", "code"],
116
121
  ["stylebook", "config"],
@@ -142,6 +147,7 @@ export const EDITOR_KIND_LABELS: Readonly<Record<EditorKind, string>> = {
142
147
  entry: "Entry",
143
148
  grid: "Grid",
144
149
  home: "Home",
150
+ image: "Image",
145
151
  media: "Media",
146
152
  none: "None",
147
153
  tokens: "Variables",