@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
@@ -0,0 +1,272 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The Image editor surface: one image asset, edited where it is opened (studio.md §9.5).
4
+ *
5
+ * This is the adapter. `media/image-editor.ts` keeps the recipe, its undo stack, the decoded source
6
+ * and the one write door, and hands this module a flat projection: pre-formatted geometry strings,
7
+ * the values the fields show, and the words the `$switch`es read. The document renders that and
8
+ * decides nothing.
9
+ *
10
+ * **One island.** A drag converts screen pixels into the image's own frame, which needs the frame's
11
+ * drawn size, so the frame element is captured as it is created (`onNodeCreated`,
12
+ * studio-ui-guidelines.md §9.4) and the flow asks for its rect. The handles are captured the same
13
+ * way, so moving the roving tab stop to the next handle focuses an element this module was handed
14
+ * rather than one it searched for.
15
+ *
16
+ * **The container is CLEARED, because this is a pane and not a panel** — the reason
17
+ * `surfaces/media-pane.ts` gives: the canvas stage is handed to whichever mode owns it whole.
18
+ *
19
+ * @docs studio/projects/media
20
+ */
21
+
22
+ import { isNonNullObject, isString } from "@jxsuite/schema/guards";
23
+ import { reactive } from "../reactivity";
24
+ import { mountSurface, registerSurface } from "../ui/surface";
25
+ import { rectOf } from "../utils/geometry";
26
+ import imageEditorDoc from "./image-editor.json";
27
+ import type { SurfaceHandle } from "../ui/surface";
28
+ import type { JxScope } from "@jxsuite/runtime/types";
29
+ import type { JxElement } from "@jxsuite/schema/types";
30
+
31
+ registerSurface("image-editor", imageEditorDoc);
32
+
33
+ /** One crop handle as the document draws it. */
34
+ export interface ImageHandleRow {
35
+ id: string;
36
+ /** Where it sits on screen, as a reader hears it. */
37
+ label: string;
38
+ /** `"0"` for the one handle in the tab order, `"-1"` for the other seven. */
39
+ tabindex: string;
40
+ }
41
+
42
+ /** Everything the document draws, as one value. */
43
+ export interface ImageEditorView {
44
+ region: string;
45
+ /** `loading` while the bytes are read, `refused` with the reason, `ready` with a picture. */
46
+ status: "loading" | "ready" | "refused";
47
+ /** What the loading or refused box says. */
48
+ message: string;
49
+ name: string;
50
+ alt: string;
51
+ /** A same-origin `blob:` URL for the bytes the editor holds. */
52
+ src: string;
53
+ /** The oriented aspect, width over height, as a number string. */
54
+ ratio: string;
55
+ /** The oriented width in CSS pixels, so the preview is never upscaled. */
56
+ maxWidth: string;
57
+ imageWidth: string;
58
+ imageHeight: string;
59
+ imageTransform: string;
60
+ /** The tone pass, the same string the bake sets on its context; `none` when neutral. */
61
+ filter: string;
62
+ cropX: string;
63
+ cropY: string;
64
+ cropW: string;
65
+ cropH: string;
66
+ handles: ImageHandleRow[];
67
+ handleHelpId: string;
68
+ /** The ratio the crop is held to, and the picker's rows. */
69
+ aspect: string;
70
+ aspectOptions: { label: string; value: string }[];
71
+ /** The crop area's cursor: `move`, or `default` when there is nowhere for it to go. */
72
+ moveCursor: string;
73
+ facts: string;
74
+ width: string;
75
+ maxOutputWidth: string;
76
+ fitLabel: string;
77
+ fitHint: string;
78
+ quality: "shown" | "hidden";
79
+ qualityValue: string;
80
+ brightness: string;
81
+ contrast: string;
82
+ saturation: string;
83
+ warning: string;
84
+ /** Disabled flags, each named for the state that disables. */
85
+ busy: boolean;
86
+ undecoded: boolean;
87
+ cannotUndo: boolean;
88
+ cannotRedo: boolean;
89
+ cannotFit: boolean;
90
+ clean: boolean;
91
+ cannotSave: boolean;
92
+ saving: boolean;
93
+ }
94
+
95
+ /** What a control can ask the editor to do. Every one is a decision the flow owns. */
96
+ export interface ImageEditorActions {
97
+ imageLoaded: (width: number, height: number) => void;
98
+ imageFailed: (src: string) => void;
99
+ rotate: (direction: number) => void;
100
+ flip: (axis: string) => void;
101
+ undo: () => void;
102
+ redo: () => void;
103
+ reset: () => void;
104
+ save: () => void;
105
+ fieldEdited: () => void;
106
+ fitForWeb: () => void;
107
+ setWidth: (value: string) => void;
108
+ setAspect: (value: string) => void;
109
+ setValue: (key: string, value: string) => void;
110
+ handleDown: (
111
+ handle: string,
112
+ pointerId: number,
113
+ clientX: number,
114
+ clientY: number,
115
+ target: Element,
116
+ ) => void;
117
+ handleMove: (pointerId: number, clientX: number, clientY: number) => void;
118
+ handleUp: (pointerId: number) => void;
119
+ handleFocused: (handle: string) => void;
120
+ nudge: (handle: string, dx: number, dy: number, large: boolean) => void;
121
+ cycleHandle: (handle: string, step: number) => void;
122
+ }
123
+
124
+ export interface ImageEditorSurfaceHandle {
125
+ ready: Promise<HTMLElement>;
126
+ /** Whether what was mounted is still inside the host; true while the mount is in flight. */
127
+ attached: () => boolean;
128
+ update: (view: ImageEditorView) => void;
129
+ /** The drawn frame's rect, or null before it exists. */
130
+ frameRect: () => DOMRect | null;
131
+ /** Move focus to one handle, when it has been drawn. */
132
+ focusHandle: (handle: string) => void;
133
+ /** Commit a number still being typed, so a save writes what is on screen. Keeps the focus. */
134
+ flushFields: () => void;
135
+ /** Put the view's values back in the number fields, whatever was typed into them. */
136
+ syncFields: (view: ImageEditorView) => void;
137
+ dispose: () => void;
138
+ }
139
+
140
+ type ImageEditorScope = ImageEditorView & ImageEditorActions;
141
+
142
+ /** Each number field's `part`, which is also the name of the view value it shows. */
143
+ const FIELD_VALUES = {
144
+ brightness: "brightness",
145
+ contrast: "contrast",
146
+ quality: "qualityValue",
147
+ saturation: "saturation",
148
+ width: "width",
149
+ } as const;
150
+
151
+ function isFieldPart(part: string): part is keyof typeof FIELD_VALUES {
152
+ return Object.hasOwn(FIELD_VALUES, part);
153
+ }
154
+
155
+ /**
156
+ * The `part` a definition declares, or `""`. Read off the DEFINITION, because a node is announced
157
+ * as it is created, before its attributes are applied.
158
+ */
159
+ function partOf(def: JxElement | string): string {
160
+ const part = isString(def) ? undefined : def.attributes?.["part"];
161
+ return isString(part) ? part : "";
162
+ }
163
+
164
+ /** The handle a mapped row stands for, read off its `$map` item. */
165
+ function mappedHandle(state: JxScope | undefined): string | null {
166
+ const map = state?.["$map"];
167
+ const item = isNonNullObject(map) && "item" in map ? map.item : undefined;
168
+ return isNonNullObject(item) && "id" in item && isString(item.id) ? item.id : null;
169
+ }
170
+
171
+ /**
172
+ * Write a projection into the scope. Each key is assigned through the reactive proxy, which
173
+ * notifies only when a value moved, so a drag that changes four crop strings redraws those four.
174
+ */
175
+ function project(scope: ImageEditorScope, view: ImageEditorView): void {
176
+ Object.assign(scope, view);
177
+ }
178
+
179
+ /**
180
+ * Mount the Image editor document into `host`, wired to `actions`.
181
+ *
182
+ * @param {HTMLElement} host - The pane's canvas stage
183
+ * @param {ImageEditorView} view - What to draw right now
184
+ * @param {ImageEditorActions} actions - What each control does
185
+ * @returns {ImageEditorSurfaceHandle}
186
+ */
187
+ export function mountImageEditorSurface(
188
+ host: HTMLElement,
189
+ view: ImageEditorView,
190
+ actions: ImageEditorActions,
191
+ ): ImageEditorSurfaceHandle {
192
+ host.replaceChildren();
193
+ const scope = reactive<ImageEditorScope>({ ...view, ...actions });
194
+
195
+ let frame: Element | null = null;
196
+ const handles = new Map<string, HTMLElement>();
197
+ const fields = new Map<keyof typeof FIELD_VALUES, HTMLElement>();
198
+ let mounted: SurfaceHandle | null = null;
199
+ let disposed = false;
200
+ const ready = mountSurface("image-editor", scope, host, {
201
+ onNodeCreated: (element, _path, def, state) => {
202
+ // `disposed` first: nodes are still created while a mount that was taken down runs out.
203
+ if (disposed || !(element instanceof HTMLElement)) {
204
+ return;
205
+ }
206
+ const part = partOf(def);
207
+ if (part === "frame") {
208
+ frame = element;
209
+ } else if (part === "handle") {
210
+ const id = mappedHandle(state);
211
+ if (id !== null) {
212
+ handles.set(id, element);
213
+ }
214
+ } else if (isFieldPart(part)) {
215
+ fields.set(part, element);
216
+ }
217
+ },
218
+ }).then((surface) => {
219
+ if (disposed) {
220
+ surface.dispose();
221
+ } else {
222
+ mounted = surface;
223
+ }
224
+ /* SAFETY: `image-editor.json`'s root definition is a `div` element, so the runtime mounts an
225
+ HTMLElement; a `Text` root only comes from a document whose root is a bare string. */
226
+ return surface.root as HTMLElement;
227
+ });
228
+
229
+ return {
230
+ attached: () => {
231
+ if (disposed) {
232
+ return false;
233
+ }
234
+ const root = mounted?.root;
235
+ return root === undefined ? true : host.contains(root);
236
+ },
237
+ dispose() {
238
+ disposed = true;
239
+ mounted?.dispose();
240
+ mounted = null;
241
+ frame = null;
242
+ handles.clear();
243
+ fields.clear();
244
+ },
245
+ focusHandle: (handle) => {
246
+ const element = handles.get(handle);
247
+ if (element?.isConnected) {
248
+ element.focus();
249
+ }
250
+ },
251
+ frameRect: () => (frame?.isConnected ? rectOf(frame) : null),
252
+ flushFields: () => {
253
+ /* A number input commits on blur, which fires its change event synchronously, and the
254
+ document's handler then takes the value. Focused straight back, so the author's place is
255
+ not lost to a keystroke that only meant "save". */
256
+ const active = document.activeElement;
257
+ if (active instanceof HTMLInputElement && host.contains(active)) {
258
+ active.blur();
259
+ active.focus();
260
+ }
261
+ },
262
+ ready,
263
+ syncFields: (next) => {
264
+ for (const [part, element] of fields) {
265
+ if (element.isConnected && "value" in element) {
266
+ element.value = next[FIELD_VALUES[part]];
267
+ }
268
+ }
269
+ },
270
+ update: (next) => project(scope, next),
271
+ };
272
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioMediaLibrary",
4
- "$description": "The Media workspace's library (`specs/studio.md` §22.6): the project's media as filters, a grid and the selected file's detail. ONE document in two modes. In `manage` it is the Media home tab, with Replace, Open in viewer and Delete under the detail; in `pick` it is the full-size picker inside a `jx-dialog` (`media-pick-dialog.json`), and the dialog's own footer answers Cancel or Use <file>. `media/media-library.ts` keeps every decision (which files a filter keeps, the sort, what a count may claim, where an arrow lands, what Replace writes) and hands this document strings and discriminants. **Unused is drawn with no number until the usage sweep has settled**, and Missing alt text and tags are not drawn at all: each needs a store that does not exist, and a filter that cannot be answered is not shipped as one that always answers nothing.",
4
+ "$description": "The Media workspace's library (`specs/studio.md` §22.6): the project's media as filters, a grid and the selected file's detail. ONE document in two modes. In `manage` it is the Media home tab, with Replace, Open in viewer, Rename, Duplicate and Delete under the detail; in `pick` it is the full-size picker inside a `jx-dialog` (`media-pick-dialog.json`), and the dialog's own footer answers Cancel or Use <file>. `media/media-library.ts` keeps every decision (which files a filter keeps, the sort, what a count may claim, where an arrow lands, what Replace writes) and hands this document strings and discriminants. **Unused is drawn with no number until the usage sweep has settled**, and Missing alt text and tags are not drawn at all: each needs a store that does not exist, and a filter that cannot be answered is not shipped as one that always answers nothing.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "media-library",
@@ -1578,6 +1578,134 @@
1578
1578
  }
1579
1579
  ]
1580
1580
  },
1581
+ {
1582
+ "tagName": "div",
1583
+ "attributes": {
1584
+ "part": "edit-slot",
1585
+ "role": "none"
1586
+ },
1587
+ "style": {
1588
+ "display": "contents"
1589
+ },
1590
+ "$switch": {
1591
+ "$ref": "#/state/editable"
1592
+ },
1593
+ "cases": {
1594
+ "yes": {
1595
+ "tagName": "jx-button",
1596
+ "attributes": {
1597
+ "part": "edit-image"
1598
+ },
1599
+ "$props": {
1600
+ "size": "sm",
1601
+ "variant": "secondary"
1602
+ },
1603
+ "onclick": {
1604
+ "$prototype": "Function",
1605
+ "body": [
1606
+ {
1607
+ "operator": "call",
1608
+ "target": {
1609
+ "$ref": "#/state/editImage"
1610
+ },
1611
+ "value": []
1612
+ }
1613
+ ],
1614
+ "description": "Withheld, not disabled, for a file the editor cannot write: a refused format is not offered rather than offered and then refused (studio.md §9.5)."
1615
+ },
1616
+ "children": [
1617
+ {
1618
+ "tagName": "span",
1619
+ "textContent": "Edit image"
1620
+ }
1621
+ ]
1622
+ },
1623
+ "no": {
1624
+ "tagName": "span",
1625
+ "attributes": {
1626
+ "hidden": true
1627
+ }
1628
+ }
1629
+ }
1630
+ },
1631
+ {
1632
+ "tagName": "jx-button",
1633
+ "attributes": {
1634
+ "part": "rename"
1635
+ },
1636
+ "$props": {
1637
+ "size": "sm",
1638
+ "variant": "secondary"
1639
+ },
1640
+ "onclick": {
1641
+ "$prototype": "Function",
1642
+ "body": [
1643
+ {
1644
+ "operator": "call",
1645
+ "target": {
1646
+ "$ref": "#/state/rename"
1647
+ },
1648
+ "value": []
1649
+ }
1650
+ ]
1651
+ },
1652
+ "children": [
1653
+ {
1654
+ "tagName": "span",
1655
+ "textContent": "Rename"
1656
+ }
1657
+ ]
1658
+ },
1659
+ {
1660
+ "tagName": "div",
1661
+ "attributes": {
1662
+ "part": "duplicate-slot",
1663
+ "role": "none"
1664
+ },
1665
+ "style": {
1666
+ "display": "contents"
1667
+ },
1668
+ "$switch": {
1669
+ "$ref": "#/state/duplicable"
1670
+ },
1671
+ "cases": {
1672
+ "yes": {
1673
+ "tagName": "jx-button",
1674
+ "attributes": {
1675
+ "part": "duplicate"
1676
+ },
1677
+ "$props": {
1678
+ "size": "sm",
1679
+ "variant": "secondary"
1680
+ },
1681
+ "onclick": {
1682
+ "$prototype": "Function",
1683
+ "body": [
1684
+ {
1685
+ "operator": "call",
1686
+ "target": {
1687
+ "$ref": "#/state/duplicate"
1688
+ },
1689
+ "value": []
1690
+ }
1691
+ ],
1692
+ "description": "Withheld, not disabled, on a backend that cannot read a file's raw bytes: a copy read as text would be a broken file (studio.md §9.3)."
1693
+ },
1694
+ "children": [
1695
+ {
1696
+ "tagName": "span",
1697
+ "textContent": "Duplicate"
1698
+ }
1699
+ ]
1700
+ },
1701
+ "no": {
1702
+ "tagName": "span",
1703
+ "attributes": {
1704
+ "hidden": true
1705
+ }
1706
+ }
1707
+ }
1708
+ },
1581
1709
  {
1582
1710
  "tagName": "jx-button",
1583
1711
  "attributes": {
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * This is the adapter. `media/media-library.ts` is the flow: which files a filter keeps, in what
6
6
  * order, what each count may claim, which tile is selected, where an arrow lands and what Replace,
7
- * Delete and an upload write. It hands this module one projection, and this module hands the
8
- * document a scope and reports the nodes the flow needs to hold.
7
+ * Rename, Duplicate, Delete and an upload write. It hands this module one projection, and this
8
+ * module hands the document a scope and reports the nodes the flow needs to hold.
9
9
  *
10
10
  * **Four islands, all through `onNodeCreated`.** The flow needs four nodes the document creates:
11
11
  * each tile (watched for viewport entry, so a thumbnail is requested only when it is looked at, and
@@ -97,6 +97,13 @@ export interface MediaLibraryView {
97
97
  previewSpecimen: string;
98
98
  /** The Replace input's `accept`: the selected file's own format only. */
99
99
  replaceAccept: string;
100
+ /**
101
+ * Whether the selected file opens in the Image editor (studio.md §9.5): withheld on exactly the
102
+ * conditions `media.edit` gates on, so the palette and this pane cannot give two answers.
103
+ */
104
+ editable: "yes" | "no";
105
+ /** Whether the host can copy the selected file: `media.duplicate` gates on the same capability. */
106
+ duplicable: "yes" | "no";
100
107
  usageState: "hidden" | "shown";
101
108
  usageHeadline: string;
102
109
  usageFiles: MediaUsageRow[];
@@ -126,7 +133,13 @@ export interface MediaLibraryActions {
126
133
  openDocument: (path: string) => void;
127
134
  replace: (input: HTMLInputElement) => void;
128
135
  remove: () => void;
136
+ /** Rename the selected file, through the Rename dialog. */
137
+ rename: () => void;
138
+ /** Copy the selected file to a new name beside it, and select the copy. */
139
+ duplicate: () => void;
129
140
  openViewer: () => void;
141
+ /** Open the selected image in its Image editor. */
142
+ editImage: () => void;
130
143
  reset: () => void;
131
144
  /** A tile node was created for `file`. */
132
145
  tileHost: (element: HTMLElement, file: string) => void;
@@ -174,6 +187,8 @@ export function emptyMediaLibraryView(): MediaLibraryView {
174
187
  query: "",
175
188
  region: "",
176
189
  replaceAccept: "",
190
+ editable: "no",
191
+ duplicable: "no",
177
192
  selectedFile: "",
178
193
  sort: "",
179
194
  sortOptions: [],
@@ -24,7 +24,7 @@
24
24
  "marginBottom": "12px"
25
25
  },
26
26
  "& [part=\"columns\"]": {
27
- "$description": "The 1px gap over the border colour is the divider between the two columns: two panels on `--bg-panel` with `--border` showing through the gap, so nothing draws a rule of its own.",
27
+ "$description": "The 1px gap over the border colour is the divider between the two columns: two panels on the page's own `--bg` with `--border` showing through the gap, so nothing draws a rule of its own. The panels are `--bg`, not `--bg-panel`, because the section is drawn on the Settings home, whose background is `--bg`: a second shade under one section of the form reads as a box nobody asked for.",
28
28
  "display": "flex",
29
29
  "height": "100%",
30
30
  "gap": "1px",
@@ -37,7 +37,7 @@
37
37
  "padding": "12px",
38
38
  "width": "200px",
39
39
  "minWidth": "200px",
40
- "background": "var(--bg-panel)",
40
+ "background": "var(--bg)",
41
41
  "overflowY": "auto"
42
42
  },
43
43
  "& [part=\"entry\"], & [part=\"new-open\"]": {
@@ -62,7 +62,7 @@
62
62
  "display": "flex",
63
63
  "alignItems": "center",
64
64
  "justifyContent": "center",
65
- "background": "var(--bg-panel)",
65
+ "background": "var(--bg)",
66
66
  "color": "var(--fg-dim)",
67
67
  "fontSize": "13px"
68
68
  },
@@ -70,7 +70,7 @@
70
70
  "$description": "`min-width: 0` is the one line `.settings-editor-panel` did not have and needed: a flex child's floor is its content, and the schema form inside this one is a whole form rather than the short cards Data Shapes puts here — so a long pointer or a wide row pushed the column past the right edge of the pane instead of scrolling inside it.",
71
71
  "flex": "1",
72
72
  "padding": "16px",
73
- "background": "var(--bg-panel)",
73
+ "background": "var(--bg)",
74
74
  "overflowY": "auto",
75
75
  "minWidth": "0"
76
76
  },
@@ -41,7 +41,7 @@ export interface ContributedEntryView {
41
41
  /** The key in the section's map on disk — the row's identity and its `data-entry`. */
42
42
  key: string;
43
43
  selected: boolean;
44
- /** `overlay.dialog:settings/entry:<key>`, so a shot can name the row without a selector. */
44
+ /** `pane.<id>/settings/entry:<key>`, so a shot can name the row without a selector. */
45
45
  region: string;
46
46
  }
47
47
 
@@ -61,7 +61,7 @@ export interface ContributedView {
61
61
  editorState: "empty" | "editing";
62
62
  /** What the rename field holds — the key on disk, or what the reader has just typed. */
63
63
  entryName: string;
64
- /** `overlay.dialog:settings/editor`. */
64
+ /** `pane.<id>/settings/editor`. */
65
65
  editorRegion: string;
66
66
  /** The new-entry button: "New content type", or "New entry" when the section names none. */
67
67
  newLabel: string;