@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
@@ -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
+ }
@@ -54,8 +54,8 @@
54
54
  "maxWidth": "none",
55
55
  "padding": "var(--jx-space-1) var(--jx-space-2)"
56
56
  },
57
- "& [part=\"panel\"]": {
58
- "$description": "`#right-panel > .panel-body { flex: 1; min-height: 0; overflow-y: auto }`, now a flex column: Element's sub-switch sits above its leaves and must not scroll away with them. The kit's tab panel pads itself for a dialog; a dock's tab is the whole column and each body pads its own content.",
57
+ "& > [part=\"panel\"]": {
58
+ "$description": "`#right-panel > .panel-body { flex: 1; min-height: 0; overflow-y: auto }`, now a flex column: Element's sub-switch sits above its leaves and must not scroll away with them. The kit's tab panel pads itself for a dialog; a dock's tab is the whole column and each body pads its own content. The child combinator is load-bearing: the leaves mount in this same light DOM, the Style leaf's root and every `jx-color-field`'s picker are also `part=\"panel\"`, and a descendant selector put this `overflow: hidden` on them. The Style root then clipped its own rows, so `panel-body` below saw nothing to scroll and every property past the fold was unreachable.",
59
59
  "display": "flex",
60
60
  "flexDirection": "column",
61
61
  "flex": "1",
@@ -63,7 +63,7 @@
63
63
  "padding": "0",
64
64
  "overflow": "hidden"
65
65
  },
66
- "& [part=\"panel\"][hidden]": {
66
+ "& > [part=\"panel\"][hidden]": {
67
67
  "$description": "Out of sight, out of the layout and out of the accessibility tree, but not `display: none`: a panel that left the box tree is restyled whole when it comes back, and the Inspector shows its Page panel on every deselect and on every close of the Assistant (§6), which made each of those a 300-element style recalculation. `content-visibility: hidden` skips the subtree while keeping its computed style, so a return restyles only what changed while it was away; `visibility: hidden` keeps it unfocusable and unexposed, as `hidden` means.",
68
68
  "display": "flex",
69
69
  "flex": "0 0 0",
@@ -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: [],
package/src/types.ts CHANGED
@@ -406,7 +406,8 @@ export interface StudioPlatform {
406
406
  * tainting stops being a thing that can happen rather than a thing that is worked around.
407
407
  *
408
408
  * Optional, and feature-detected like every optional member: a backend without it simply does not
409
- * offer the image editor, which is the honest degradation `specs/desktop.md` §3.1 asks for.
409
+ * offer the image editor or a file's Duplicate, which is the honest degradation
410
+ * `specs/desktop.md` §3.1 asks for.
410
411
  */
411
412
  readFileBytes?: (path: string) => Promise<ArrayBuffer>;
412
413
  writeFile: (path: string, content: string) => Promise<void>;
@@ -47,7 +47,8 @@ export function workspaceOfTab(tab: Tab): WorkspaceId {
47
47
  if (mode === "git-diff") {
48
48
  return "publish";
49
49
  }
50
- if (mode === "media") {
50
+ // The viewer and its editing sibling (`media/image-editor.ts`, §9.5): one file, one workspace.
51
+ if (mode === "media" || mode === "image") {
51
52
  return "media";
52
53
  }
53
54
  if (CONTENT_MODES.has(mode)) {
@@ -46,6 +46,7 @@ import {
46
46
  setCloseLanding,
47
47
  setEmptyPaneFallback,
48
48
  setTabScope,
49
+ tabHoldsEditBesideDocument,
49
50
  workspace,
50
51
  } from "./workspace";
51
52
  import { homeWorkspaceOfTabId, isHomeWorkspace, workspaceHomeTabId } from "./workspace-ids";
@@ -119,7 +120,8 @@ export function viewsReplacedBy(
119
120
  homeWorkspaceOfTabId(other) !== id &&
120
121
  workspaceOfTab(tab) === id &&
121
122
  !tab.pinned &&
122
- !tab.doc.dirty
123
+ !tab.doc.dirty &&
124
+ !tabHoldsEditBesideDocument(tab)
123
125
  );
124
126
  });
125
127
  }
@@ -1063,12 +1063,33 @@ export function promoteTab(tabId: string) {
1063
1063
  */
1064
1064
  export function promoteDirtyPreviewTabs() {
1065
1065
  for (const [id, tab] of workspace.tabs) {
1066
- if (tab.preview && tab.doc.dirty) {
1066
+ if (tab.preview && (tab.doc.dirty || tabHoldsEditBesideDocument(tab))) {
1067
1067
  promoteTab(id);
1068
1068
  }
1069
1069
  }
1070
1070
  }
1071
1071
 
1072
+ /** The answer to {@link tabHoldsEditBesideDocument}, injected by the module that holds the edit. */
1073
+ let editBesideDocument: ((tab: Tab) => boolean) | null = null;
1074
+
1075
+ /**
1076
+ * Register who answers {@link tabHoldsEditBesideDocument}. Injected rather than imported, because
1077
+ * the Image editor (`media/image-editor.ts`, studio.md §9.5) reaches the upload and collab layers,
1078
+ * every one of which imports this module.
1079
+ */
1080
+ export function setEditBesideDocument(answer: (tab: Tab) => boolean): void {
1081
+ editBesideDocument = answer;
1082
+ }
1083
+
1084
+ /**
1085
+ * Whether this tab holds unsaved work that is NOT in its document: a media tab's document is a
1086
+ * stub, and an image edit is a recipe held beside it, so `tab.doc.dirty` cannot say. Every gate
1087
+ * that discards a tab without asking has to ask this too, or a pending crop goes in silence.
1088
+ */
1089
+ export function tabHoldsEditBesideDocument(tab: Tab): boolean {
1090
+ return editBesideDocument?.(tab) ?? false;
1091
+ }
1092
+
1072
1093
  /**
1073
1094
  * Pin or unpin a tab, moving it to the boundary of the pinned prefix so the head of the strip is
1074
1095
  * always exactly the pinned set.
@@ -1,4 +0,0 @@
1
- import{Gs,Ks,Wr}from"./studio-sxdb9h3q.js";export{Wr as draggable,Gs as dropTargetForElements,Ks as monitorForElements};
2
-
3
- //# debugId=12250D3D5E1C3ED864756E2164756E21
4
- //# sourceMappingURL=adapter-pp011m5x.js.map