@jxsuite/studio 9.0.0 → 10.1.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 (190) hide show
  1. package/dist/chunks/{2020-89fz3pfy.js → 2020-mkb0qa6p.js} +3 -3
  2. package/dist/chunks/adapter-xcpsmecx.js +4 -0
  3. package/dist/chunks/{add-new-flow-nmkqpjym.js → add-new-flow-ptmhmzr9.js} +3 -3
  4. package/dist/chunks/{cf-account-picker-2e06qxey.js → cf-account-picker-z1sgwpwd.js} +3 -3
  5. package/dist/chunks/cf-settings-xqvhxxe9.js +4 -0
  6. package/dist/chunks/components-whsc81a7.js +4 -0
  7. package/dist/chunks/content-source-jwtxg78w.js +4 -0
  8. package/dist/chunks/{convert-to-repeater-j9hfcj4f.js → convert-to-repeater-f92ey3kp.js} +3 -3
  9. package/dist/chunks/editor-zdqdpq1x.js +4 -0
  10. package/dist/chunks/entry-commands-w8p2fbpt.js +4 -0
  11. package/dist/chunks/{extension-sections-thfke054.js → extension-sections-y2p1te2r.js} +3 -3
  12. package/dist/chunks/file-ops-13ab4swj.js +4 -0
  13. package/dist/chunks/files-zbc8p6v1.js +4 -0
  14. package/dist/chunks/format-host-zjsbktbx.js +4 -0
  15. package/dist/chunks/fs-events-t6x0hch6.js +4 -0
  16. package/dist/chunks/grid-open-fyzdwpkh.js +4 -0
  17. package/dist/chunks/{grid-view-7d2nx83y.js → grid-view-r22jsqht.js} +5 -5
  18. package/dist/chunks/iframe-host-kbyqc7pt.js +4 -0
  19. package/dist/chunks/index-6nvj89vb.js +4 -0
  20. package/dist/chunks/{index-jxmt7vh8.js → index-pxwwf00y.js} +3 -3
  21. package/dist/chunks/{javascript-vmqp56ke.js → javascript-pzqryby6.js} +2 -2
  22. package/dist/chunks/{jsonMode-n3wmpant.js → jsonMode-tcnyxyap.js} +5 -5
  23. package/dist/chunks/jx-validate-5sp8a4rm.js +4 -0
  24. package/dist/chunks/layers-panel-qrxj9vr2.js +4 -0
  25. package/dist/chunks/layers-tpzayggx.js +4 -0
  26. package/dist/chunks/lit-html-x6nrxsx3.js +4 -0
  27. package/dist/chunks/media-library-ywppnjrz.js +4 -0
  28. package/dist/chunks/media-picker-etxgc8qq.js +4 -0
  29. package/dist/chunks/{monaco-binding-wfhmz89d.js → monaco-binding-bqpge64f.js} +3 -3
  30. package/dist/chunks/monaco-buffer-qrnqhk0b.js +4 -0
  31. package/dist/chunks/{monaco-setup-fty7a8vg.js → monaco-setup-1mkab762.js} +3 -3
  32. package/dist/chunks/notify-kcv38wwe.js +4 -0
  33. package/dist/chunks/parse-df2kkmhc.js +4 -0
  34. package/dist/chunks/platform-dtsysmyd.js +4 -0
  35. package/dist/chunks/publish-panel-evb1rcxt.js +4 -0
  36. package/dist/chunks/{publish-quick-12hh8926.js → publish-quick-m9vkhahq.js} +3 -3
  37. package/dist/chunks/publish-workspace-ktk9h536.js +4 -0
  38. package/dist/chunks/quick-search-2f47fxcw.js +4 -0
  39. package/dist/chunks/references-5w6ajjv7.js +4 -0
  40. package/dist/chunks/right-panel-kz0a6byf.js +4 -0
  41. package/dist/chunks/source-control-1phx90zk.js +4 -0
  42. package/dist/chunks/{studio-qevf5z9d.js → studio-07k0geap.js} +4 -4
  43. package/dist/chunks/{studio-k5n4es74.js → studio-0g2sm01j.js} +4 -4
  44. package/dist/chunks/{studio-7q6jdd4h.js → studio-0hr000ba.js} +4 -4
  45. package/dist/chunks/{studio-e4rq6x0q.js → studio-1wyp7dt5.js} +4 -4
  46. package/dist/chunks/{studio-vtey9w98.js → studio-20ap8dds.js} +4 -4
  47. package/dist/chunks/studio-291gqh98.js +5 -0
  48. package/dist/chunks/{studio-32d8wyxq.js → studio-37twdaf7.js} +30 -30
  49. package/dist/chunks/{studio-8pne0rx4.js → studio-4nkm4404.js} +4 -4
  50. package/dist/chunks/{studio-z1p91z5a.js → studio-4smcnv33.js} +4 -4
  51. package/dist/chunks/{studio-s4fq9jht.js → studio-4zqfw5ha.js} +4 -4
  52. package/dist/chunks/{studio-n4qq1n57.js → studio-56egy6vf.js} +4 -4
  53. package/dist/chunks/{studio-t6dqnnft.js → studio-5dmgmyw4.js} +4 -4
  54. package/dist/chunks/{studio-9ff0vqff.js → studio-5e7p1vgd.js} +4 -4
  55. package/dist/chunks/{studio-4qg61g9j.js → studio-5m0w815d.js} +4 -4
  56. package/dist/chunks/{studio-0rg30pvs.js → studio-695k95bw.js} +4 -4
  57. package/dist/chunks/{studio-spza69tb.js → studio-6vwvshw8.js} +5 -5
  58. package/dist/chunks/{studio-ae7a69jc.js → studio-76t560kn.js} +5 -5
  59. package/dist/chunks/{studio-9gzvypw4.js → studio-7823f0g8.js} +4 -4
  60. package/dist/chunks/{studio-gskkgfe3.js → studio-7vb997ff.js} +4 -4
  61. package/dist/chunks/{studio-sc35ttdk.js → studio-8486jn4r.js} +4 -4
  62. package/dist/chunks/{studio-a6e5fzve.js → studio-848rk4p1.js} +4 -4
  63. package/dist/chunks/{studio-tyzgte21.js → studio-8jb31dcf.js} +4 -4
  64. package/dist/chunks/{studio-jr30g4ae.js → studio-8qejv8yf.js} +4 -4
  65. package/dist/chunks/studio-8x8616xd.js +5 -0
  66. package/dist/chunks/{studio-kyw47pet.js → studio-9b7177jq.js} +4 -4
  67. package/dist/chunks/{studio-gk9yk8dj.js → studio-9cp96v90.js} +6 -6
  68. package/dist/chunks/{studio-h4jbgne2.js → studio-9rcbvksg.js} +6 -6
  69. package/dist/chunks/{studio-3hpcawh8.js → studio-a3s6frde.js} +3 -3
  70. package/dist/chunks/{studio-tfmhnwyr.js → studio-ahf3cspq.js} +3 -3
  71. package/dist/chunks/{studio-6rbaedw2.js → studio-bkmv1h3e.js} +6 -6
  72. package/dist/chunks/{studio-4j35hgsw.js → studio-brdp85q7.js} +4 -4
  73. package/dist/chunks/{studio-hcy9zeac.js → studio-cgb97rwv.js} +4 -4
  74. package/dist/chunks/{studio-gczve8zf.js → studio-ch8gt8vt.js} +3 -3
  75. package/dist/chunks/studio-cqf1k4qk.js +5 -0
  76. package/dist/chunks/{studio-myb4mf0h.js → studio-cvz0e00f.js} +4 -4
  77. package/dist/chunks/{studio-nqexty8x.js → studio-dbgv4fv9.js} +14 -14
  78. package/dist/chunks/{studio-8hf8qcm1.js → studio-dy2xtt7a.js} +4 -4
  79. package/dist/chunks/{studio-c88gq88j.js → studio-eesyjg7h.js} +5 -5
  80. package/dist/chunks/studio-et6t8g4m.js +4 -0
  81. package/dist/chunks/{studio-qs5fmhtj.js → studio-f2j9et3h.js} +8 -8
  82. package/dist/chunks/{studio-79r8msnr.js → studio-fep7hr66.js} +4 -4
  83. package/dist/chunks/{studio-1cfyd1ys.js → studio-fyf7j57j.js} +3 -3
  84. package/dist/chunks/{studio-mkw17ngg.js → studio-g5prntab.js} +4 -4
  85. package/dist/chunks/{studio-x2trkhk5.js → studio-ga77mf07.js} +4 -4
  86. package/dist/chunks/{studio-yjh9gddd.js → studio-ggm7zf70.js} +5 -5
  87. package/dist/chunks/studio-h7gy12h2.js +5 -0
  88. package/dist/chunks/{studio-qd0cycm4.js → studio-hsr3k06v.js} +5 -5
  89. package/dist/chunks/{studio-ztppbam0.js → studio-j933k8nr.js} +4 -4
  90. package/dist/chunks/studio-jgbvqcgh.js +5 -0
  91. package/dist/chunks/{studio-xzgp0cp7.js → studio-jh6t5qz4.js} +4 -4
  92. package/dist/chunks/{studio-k6hbx2fc.js → studio-jw0q5j6k.js} +4 -4
  93. package/dist/chunks/studio-k4xc5g6h.js +5 -0
  94. package/dist/chunks/{studio-fg4fj9ec.js → studio-mwnmzx7g.js} +6 -6
  95. package/dist/chunks/{studio-ekzbgnjp.js → studio-nf0pqpbj.js} +4 -4
  96. package/dist/chunks/{studio-sm2f0sc6.js → studio-p0nf2e1f.js} +5 -5
  97. package/dist/chunks/{studio-by8x0jvc.js → studio-pd9dsqpq.js} +4 -4
  98. package/dist/chunks/{studio-cg54vtet.js → studio-pmzg17fj.js} +4 -4
  99. package/dist/chunks/{studio-ht81xjkr.js → studio-pzfa2vmb.js} +4 -4
  100. package/dist/chunks/{studio-9179bsm7.js → studio-qyb5bebs.js} +7 -7
  101. package/dist/chunks/{studio-aghsg0ds.js → studio-rf8d6p9f.js} +6 -6
  102. package/dist/chunks/{studio-9ra9e7m4.js → studio-rt498t2v.js} +4 -4
  103. package/dist/chunks/{studio-trcfer2b.js → studio-s0fg2wav.js} +4 -4
  104. package/dist/chunks/studio-sd07xva5.js +6 -0
  105. package/dist/chunks/{studio-apapy17g.js → studio-skynbac3.js} +2 -2
  106. package/dist/chunks/{studio-nvfa137z.js → studio-t3j8htp7.js} +5 -5
  107. package/dist/chunks/{studio-ry4789ng.js → studio-thj81e98.js} +6 -6
  108. package/dist/chunks/{studio-nfwacw31.js → studio-tpy3rdxn.js} +4 -4
  109. package/dist/chunks/{studio-g45r5vm8.js → studio-va0e63sk.js} +4 -4
  110. package/dist/chunks/{studio-kqh7fxn6.js → studio-vcz63n42.js} +4 -4
  111. package/dist/chunks/studio-vffwe9k7.js +5 -0
  112. package/dist/chunks/{studio-nm86874b.js → studio-vjhej0w5.js} +123 -123
  113. package/dist/chunks/{studio-ws2r1d7e.js → studio-vxjap704.js} +4 -4
  114. package/dist/chunks/{studio-9ndgd55w.js → studio-w67vfgy5.js} +4 -4
  115. package/dist/chunks/{studio-ajw4k8hp.js → studio-x2metr13.js} +3 -3
  116. package/dist/chunks/{studio-14hh60j8.js → studio-xebavr9s.js} +4 -4
  117. package/dist/chunks/{studio-4rgem12q.js → studio-yg5n49za.js} +4 -4
  118. package/dist/chunks/{studio-6k9djnxw.js → studio-yhz5b313.js} +4 -4
  119. package/dist/chunks/{studio-nfmpmkqb.js → studio-zacvfm31.js} +4 -4
  120. package/dist/chunks/{studio-ywf9y1e2.js → studio-zsjpsw08.js} +4 -4
  121. package/dist/chunks/{studio-0nefxhar.js → studio-zxm98z2m.js} +6 -6
  122. package/dist/chunks/{tsMode-byje4a01.js → tsMode-6vwhy6ty.js} +5 -5
  123. package/dist/chunks/workspace-d65bz1qk.js +4 -0
  124. package/dist/chunks/workspace-home-vbg9b3jh.js +4 -0
  125. package/dist/chunks/{ws-client-h09h6sqg.js → ws-client-zcxkfgmf.js} +3 -3
  126. package/dist/iframe-entry.js +8 -8
  127. package/dist/manifest.json +74 -74
  128. package/dist/studio.js +86 -84
  129. package/package.json +11 -11
  130. package/src/canvas/iframe-editable-root.ts +32 -2
  131. package/src/format/format-host.ts +1 -1
  132. package/src/github/github-auth-status.ts +2 -2
  133. package/src/media/media-library.ts +73 -3
  134. package/src/packages/ensure-deps.ts +5 -5
  135. package/src/packages/pull-package-sync.ts +4 -4
  136. package/src/panels/activity-panel.ts +1 -1
  137. package/src/platform-errors.ts +17 -1
  138. package/src/platforms/cloud.ts +62 -34
  139. package/src/publish/publish-changes.ts +136 -4
  140. package/src/settings/extension-commands.ts +1 -1
  141. package/src/settings/extension-rows.ts +1 -6
  142. package/src/settings/extensions-section.ts +3 -6
  143. package/src/surfaces/block-action-bar.json +1 -3
  144. package/src/surfaces/media-library.json +35 -0
  145. package/src/surfaces/media-library.ts +2 -0
  146. package/src/surfaces/menu.ts +62 -1
  147. package/src/surfaces/settings-packages.ts +1 -1
  148. package/src/types.ts +24 -8
  149. package/src/ui/layers.ts +1 -1
  150. package/src/ui/progress-modal.ts +3 -3
  151. package/dist/chunks/adapter-ay13gema.js +0 -4
  152. package/dist/chunks/cf-settings-5y04810c.js +0 -4
  153. package/dist/chunks/components-3vf75n83.js +0 -4
  154. package/dist/chunks/content-source-4f2raf4a.js +0 -4
  155. package/dist/chunks/editor-645krvtz.js +0 -4
  156. package/dist/chunks/entry-commands-3qmv8wkr.js +0 -4
  157. package/dist/chunks/file-ops-kkhp9ma7.js +0 -4
  158. package/dist/chunks/files-yswdhxrf.js +0 -4
  159. package/dist/chunks/format-host-4g8ex6bz.js +0 -4
  160. package/dist/chunks/fs-events-z9gpvc6t.js +0 -4
  161. package/dist/chunks/grid-open-fm4j183c.js +0 -4
  162. package/dist/chunks/iframe-host-q7fedsmr.js +0 -4
  163. package/dist/chunks/index-pt4wh98x.js +0 -4
  164. package/dist/chunks/jx-validate-19y62knt.js +0 -4
  165. package/dist/chunks/layers-panel-1qvc83y9.js +0 -4
  166. package/dist/chunks/layers-qf0bc7ze.js +0 -4
  167. package/dist/chunks/lit-html-wyqw04y2.js +0 -4
  168. package/dist/chunks/media-library-f9d5qcjb.js +0 -4
  169. package/dist/chunks/media-picker-rsb1fmk2.js +0 -4
  170. package/dist/chunks/monaco-buffer-ak0raadw.js +0 -4
  171. package/dist/chunks/notify-4qj70q0x.js +0 -4
  172. package/dist/chunks/parse-n8dfxnqf.js +0 -4
  173. package/dist/chunks/platform-ehg6t9vd.js +0 -4
  174. package/dist/chunks/publish-panel-yhmda3cd.js +0 -4
  175. package/dist/chunks/publish-workspace-hx8qdv9p.js +0 -4
  176. package/dist/chunks/quick-search-r955m3q1.js +0 -4
  177. package/dist/chunks/references-8gxv0x7t.js +0 -4
  178. package/dist/chunks/right-panel-jarwqjpy.js +0 -4
  179. package/dist/chunks/source-control-84qt25m9.js +0 -4
  180. package/dist/chunks/studio-2bq7hheq.js +0 -5
  181. package/dist/chunks/studio-a9k86nq6.js +0 -5
  182. package/dist/chunks/studio-dvhxxxbd.js +0 -5
  183. package/dist/chunks/studio-fj6jzgxw.js +0 -5
  184. package/dist/chunks/studio-ge08zwpv.js +0 -5
  185. package/dist/chunks/studio-nr0kcqc6.js +0 -5
  186. package/dist/chunks/studio-psh8mwnw.js +0 -4
  187. package/dist/chunks/studio-tn7v8gey.js +0 -6
  188. package/dist/chunks/studio-y47090vq.js +0 -5
  189. package/dist/chunks/workspace-7q4arnt3.js +0 -4
  190. package/dist/chunks/workspace-home-ge027kr6.js +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "9.0.0",
3
+ "version": "10.1.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.0",
64
- "@jxsuite/collab": "^0.11.3",
65
- "@jxsuite/create": "^1.4.1",
66
- "@jxsuite/formulas": "^0.0.25",
67
- "@jxsuite/markup": "^1.0.1",
68
- "@jxsuite/protocol": "^5.0.0",
69
- "@jxsuite/runtime": "^6.0.0",
70
- "@jxsuite/schema": "^4.1.0",
71
- "@jxsuite/site": "^2.2.1",
72
- "@jxsuite/ui": "^0.8.1",
63
+ "@jxsuite/ai": "^0.41.2",
64
+ "@jxsuite/collab": "^0.11.4",
65
+ "@jxsuite/create": "^1.5.1",
66
+ "@jxsuite/formulas": "^0.0.26",
67
+ "@jxsuite/markup": "^1.0.2",
68
+ "@jxsuite/protocol": "^6.1.0",
69
+ "@jxsuite/runtime": "^6.0.1",
70
+ "@jxsuite/schema": "^5.0.0",
71
+ "@jxsuite/site": "^2.2.2",
72
+ "@jxsuite/ui": "^0.8.2",
73
73
  "@vue/reactivity": "3.5.44",
74
74
  "ajv": "^8.20.0",
75
75
  "ajv-formats": "^3.0.1",
@@ -445,8 +445,9 @@ export function startEditableRoot(
445
445
  * something no caret can enter, so the caret went on blinking in the block clicked BEFORE, with
446
446
  * the component now selected in the editor and every keystroke still typed into that block.
447
447
  */
448
- const onPointerDownCapture = (e: Event) => {
449
- if (!editingAllowed()) {
448
+ const onPointerDownCapture = (e: PointerEvent) => {
449
+ // A secondary press asks for the element menu, not an edit: see `onMouseDownCapture`.
450
+ if (!editingAllowed() || e.button === 2) {
450
451
  return;
451
452
  }
452
453
  const target = e.target instanceof Element ? e.target : null;
@@ -478,6 +479,33 @@ export function startEditableRoot(
478
479
  }
479
480
  };
480
481
 
482
+ /**
483
+ * A secondary press outside the active block selects, it never starts an edit.
484
+ *
485
+ * The whole canvas is one `contenteditable`, and a right-button press places the caret where it
486
+ * lands, as a left one does. That caret activated the block through `selectionchange`, so every
487
+ * right-click on a block also entered inline editing: the bridge's edit start ran ahead of the
488
+ * menu's mount in the same task, which held the element menu back by hundreds of milliseconds on
489
+ * a real page, and the block was left in edit mode after the menu closed. Cancelling the press's
490
+ * default keeps the caret where it was; `contextmenu` still fires, so the menu is unaffected. The
491
+ * block being edited keeps its native press, because the caret placed there is what the native
492
+ * spelling and edit menu reads. A right-click elsewhere leaves that block, as a left click
493
+ * would.
494
+ */
495
+ const onMouseDownCapture = (e: MouseEvent) => {
496
+ if (e.button !== 2 || !editingAllowed()) {
497
+ return;
498
+ }
499
+ if (activeEl && e.target instanceof Node && activeEl.contains(e.target)) {
500
+ return;
501
+ }
502
+ e.preventDefault();
503
+ if (activeEl) {
504
+ deactivate();
505
+ doc.getSelection()?.removeAllRanges();
506
+ }
507
+ };
508
+
481
509
  /**
482
510
  * Reordering is the block action bar's drag handle, full stop. A `contenteditable` region is
483
511
  * natively draggable by its text, which would both fight text selection and move content behind
@@ -501,6 +529,7 @@ export function startEditableRoot(
501
529
  container.addEventListener("compositionend", onCompositionEnd);
502
530
  container.addEventListener("dragstart", onDragStart);
503
531
  doc.addEventListener("pointerdown", onPointerDownCapture, true);
532
+ doc.addEventListener("mousedown", onMouseDownCapture, true);
504
533
 
505
534
  return {
506
535
  capture: () => captureDocSelection(container, deps.isEditableBlock),
@@ -542,6 +571,7 @@ export function startEditableRoot(
542
571
  container.removeEventListener("compositionend", onCompositionEnd);
543
572
  container.removeEventListener("dragstart", onDragStart);
544
573
  doc.removeEventListener("pointerdown", onPointerDownCapture, true);
574
+ doc.removeEventListener("mousedown", onMouseDownCapture, true);
545
575
  // Deactivating commits, so the pending tick is cancelled rather than fired.
546
576
  deactivate();
547
577
  },
@@ -217,7 +217,7 @@ async function shareProjectSchemas(
217
217
  */
218
218
  export function refreshExtensionUi(platform: Pick<StudioPlatform, "fetchProjectSchemas">): void {
219
219
  void shareProjectSchemas(platform);
220
- // `installed` and `bundled` are per-project facts, so an install or a project switch can move
220
+ // `installed` is a per-project fact, so an install or a project switch can move
221
221
  // Them even when the shipped half has not changed.
222
222
  void loadExtensionCatalog();
223
223
  void import("../settings/extension-sections")
@@ -21,8 +21,8 @@ const STORAGE_KEY = "jx_github_token";
21
21
  /**
22
22
  * The GitHub OAuth app every Jx sign-in authorizes against: the browser's device flow in
23
23
  * `github-auth.ts`, and the desktop launcher's RFC 8252 loopback redirect
24
- * (`packages/desktop/src/github-signin.ts`). Here, in the module with no UI dependency, because the
25
- * launcher's main process imports it too.
24
+ * (`packages/desktop/src/backend/github-signin.ts`). Here, in the module with no UI dependency,
25
+ * because the launcher's main process imports it too.
26
26
  */
27
27
  export const GITHUB_OAUTH_CLIENT_ID = "Ov23liYVlMFpgjOEPXJH";
28
28
 
@@ -67,6 +67,7 @@ import { dirName, previewFileSrc } from "../files/media-paths";
67
67
  import { forgetVersions } from "../files/asset-versions";
68
68
  import { loadMediaUsages, peekMediaUsages } from "../files/media-usage";
69
69
  import { onProjectMediaChange, projectMediaFiles } from "../ui/media-picker";
70
+ import { openMenu } from "../surfaces/menu";
70
71
  import { openMediaTab } from "./media-open";
71
72
  import { factsLine, mediaStageFor, specimenOf, usageOf } from "./media-pane";
72
73
  import {
@@ -93,6 +94,7 @@ import type {
93
94
  MediaTileRow,
94
95
  } from "../surfaces/media-library";
95
96
  import type { MediaPickDialogHandle } from "../surfaces/media-pick-dialog";
97
+ import type { MenuHandle, MenuRowProjection } from "../surfaces/menu";
96
98
  import type { PreviewObserver } from "../browse/library-preview";
97
99
  import type { Tab } from "../tabs/tab";
98
100
  import type { CanvasSurface } from "../canvas/canvas-surface";
@@ -788,11 +790,11 @@ function actionsFor(mount: LibraryMount) {
788
790
  activate(mount, selected);
789
791
  }
790
792
  },
793
+ contextMenu: (file: string, x: number, y: number) => showTileMenu(mount, file, x, y),
791
794
  copyRef: () => {
792
795
  const entry = selectedEntry(mount);
793
- // `navigator.clipboard` is [SecureContext]: a plain-http host has none, whatever lib.dom says.
794
- if (entry && "clipboard" in navigator) {
795
- void navigator.clipboard.writeText(entry.ref);
796
+ if (entry) {
797
+ copySiteUrl(entry);
796
798
  }
797
799
  },
798
800
  dragOut: () => {
@@ -913,6 +915,74 @@ function actionsFor(mount: LibraryMount) {
913
915
  }
914
916
 
915
917
  /** Enter or a double-click: open the viewer, or in pick mode, use the file. */
918
+ /** Put a file's site URL on the clipboard. */
919
+ function copySiteUrl(entry: ProjectMediaFile): void {
920
+ // `navigator.clipboard` is [SecureContext]: a plain-http host has none, whatever lib.dom says.
921
+ if ("clipboard" in navigator) {
922
+ void navigator.clipboard.writeText(entry.ref);
923
+ }
924
+ }
925
+
926
+ /**
927
+ * The tile menu that is up, if any. One handle, because only one can be open: a second kit menu
928
+ * opened over the first is how two of them light-dismiss each other.
929
+ */
930
+ let _tileMenu: MenuHandle | null = null;
931
+
932
+ /**
933
+ * A right-click on a tile: select it, then offer the detail pane's actions at the pointer.
934
+ *
935
+ * The same four verbs as the detail pane's (studio.md §22.6), in the order the pane draws them, and
936
+ * each calls what the pane's control calls, so the menu cannot drift from the pane. Selecting first
937
+ * is what makes the pane describe the file the menu acts on, as a right-click on a row does in the
938
+ * Content workspace.
939
+ */
940
+ function showTileMenu(mount: LibraryMount, file: string, x: number, y: number): void {
941
+ select(mount, file);
942
+ const entry = selectedEntry(mount);
943
+ if (!entry) {
944
+ return;
945
+ }
946
+ _tileMenu?.close();
947
+ const row = (
948
+ id: string,
949
+ title: string,
950
+ run: () => void,
951
+ extra: Partial<MenuRowProjection> = {},
952
+ ): MenuRowProjection => ({
953
+ destructive: false,
954
+ disabled: false,
955
+ dividerAbove: false,
956
+ id,
957
+ run,
958
+ title,
959
+ ...extra,
960
+ });
961
+ _tileMenu = openMenu({
962
+ label: `${entry.name} actions`,
963
+ onClosed: (handle) => {
964
+ if (_tileMenu === handle) {
965
+ _tileMenu = null;
966
+ }
967
+ },
968
+ origin: { x, y },
969
+ region: "media-tile",
970
+ rows: [
971
+ row("open", "Open in Viewer", () => openMediaTab(file)),
972
+ row("copy-url", "Copy Site URL", () => copySiteUrl(entry)),
973
+ row("replace", "Replace…", () => requestMediaReplace(file), { dividerAbove: true }),
974
+ row(
975
+ "delete",
976
+ "Delete",
977
+ () => {
978
+ void deleteMediaFile(file);
979
+ },
980
+ { destructive: true, dividerAbove: true },
981
+ ),
982
+ ],
983
+ });
984
+ }
985
+
916
986
  function activate(mount: LibraryMount, file: string): void {
917
987
  if (mount.mode === "pick") {
918
988
  const entry = selectedEntry(mount);
@@ -1,8 +1,8 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
3
  * Ensure the active project's dependencies are installed before the editor loads. When the backend
4
- * reports node_modules is missing, run `bun install` behind a blocking progress modal so component
5
- * and format resolution see the real dependency tree. No-op on platforms without package support.
4
+ * reports node_modules is missing, install them behind a blocking progress modal so component and
5
+ * format resolution see the real dependency tree. No-op on platforms without package support.
6
6
  *
7
7
  * It REPORTS whether an install actually ran, because "format resolution sees the real dependency
8
8
  * tree" was only half true: project activation fetches the format registry before calling this, and
@@ -17,7 +17,7 @@ import { shouldInstallAutomation } from "../services/automation";
17
17
 
18
18
  export async function ensureDependenciesInstalled(): Promise<boolean> {
19
19
  // Automation/screenshot runs open projects read-only to drive the canvas — they must never mutate
20
- // The project on disk (a `bun install` would litter node_modules/bun.lock into starter templates).
20
+ // The project on disk (an install would litter node_modules and a lockfile into starter templates).
21
21
  if (shouldInstallAutomation(location.search)) {
22
22
  return false;
23
23
  }
@@ -35,7 +35,7 @@ export async function ensureDependenciesInstalled(): Promise<boolean> {
35
35
  return false;
36
36
  }
37
37
  const progress = showProgressModal({
38
- status: "Running bun install…",
38
+ status: "Installing dependencies…",
39
39
  title: "Installing dependencies",
40
40
  });
41
41
  try {
@@ -44,7 +44,7 @@ export async function ensureDependenciesInstalled(): Promise<boolean> {
44
44
  progress.done();
45
45
  return true;
46
46
  }
47
- progress.fail(result.log ?? "bun install failed");
47
+ progress.fail(result.log ?? "Install failed");
48
48
  } catch (error) {
49
49
  progress.fail(error instanceof Error ? error.message : String(error));
50
50
  }
@@ -4,7 +4,7 @@
4
4
  * updates. The on-open @jxsuite update rewrites package.json and bun.lock without committing, so a
5
5
  * teammate pushing the same update makes a later `git pull` refuse to merge. Since those local
6
6
  * changes are machine-generated and reproducible, we discard them, pull, and re-run the package
7
- * sync (re-apply the update if the pulled project is still behind, or `bun install` if the pull
7
+ * sync (re-apply the update if the pulled project is still behind, or an install if the pull
8
8
  * brought it). Detection is state-based (git status + content comparison against `@{u}`), never git
9
9
  * stderr parsing, so it works identically across backends and locales.
10
10
  */
@@ -188,7 +188,7 @@ async function applyDiscard(plan: PackagePullPlan): Promise<void> {
188
188
 
189
189
  /**
190
190
  * Bring packages back in line after a pull that touched (or followed a discard of) package files:
191
- * re-apply the @jxsuite update if the pulled project is still behind, otherwise `bun install` so
191
+ * re-apply the @jxsuite update if the pulled project is still behind, otherwise an install so
192
192
  * node_modules matches the pulled lockfile. Never throws — a successful pull must not turn into an
193
193
  * error banner; failures surface through the progress modal.
194
194
  */
@@ -209,7 +209,7 @@ async function syncPackagesAfterPull(): Promise<void> {
209
209
  return;
210
210
  }
211
211
  const progress = showProgressModal({
212
- status: "Running bun install…",
212
+ status: "Installing dependencies…",
213
213
  title: "Syncing dependencies",
214
214
  });
215
215
  try {
@@ -217,7 +217,7 @@ async function syncPackagesAfterPull(): Promise<void> {
217
217
  if (result.ok) {
218
218
  progress.done();
219
219
  } else {
220
- progress.fail(result.log ?? "bun install failed");
220
+ progress.fail(result.log ?? "Install failed");
221
221
  }
222
222
  } catch (error) {
223
223
  progress.fail(errorMessage(error));
@@ -5,7 +5,7 @@
5
5
  * What this replaces: `ui/progress-modal.ts` was **the only surface in Studio with a real error
6
6
  * view** — a red headline, the captured log in a `<pre>`, an explicit Close — and it had four call
7
7
  * sites, all package operations. It blocked the whole app, offered no cancel, and vanished
8
- * completely on success, so a `bun install` that hung had no exit and one that failed had a log you
8
+ * completely on success, so an install that hung had no exit and one that failed had a log you
9
9
  * could read exactly once. Every OTHER long operation — git clone, publish, media upload, site
10
10
  * import, project open — got a three-second grey line or nothing at all.
11
11
  *
@@ -26,6 +26,8 @@ export interface PlatformErrorInfo {
26
26
  heading?: string;
27
27
  /** What to do once one of its actions is done. */
28
28
  retry?: RetryHint;
29
+ /** The paths a `conflict` names: the files the remote changed that this side changed too. */
30
+ conflicts?: string[];
29
31
  }
30
32
 
31
33
  const RETRY_HINTS = new Set<string>(["reload", "repeat", "none"]);
@@ -90,7 +92,7 @@ export function platformErrorInfo(cause: unknown): PlatformErrorInfo {
90
92
  if (!isJsonObject(cause)) {
91
93
  return {};
92
94
  }
93
- const { actions, code, heading, installUrl, retry, type } = cause;
95
+ const { actions, code, conflicts, heading, installUrl, retry, type } = cause;
94
96
  const info: PlatformErrorInfo = {};
95
97
  // A problem type wins over a legacy code: it is the shape the backend is migrating toward.
96
98
  const derived = problemSlug(type) ?? (isString(code) ? normalizeCode(code) : null);
@@ -109,9 +111,23 @@ export function platformErrorInfo(cause: unknown): PlatformErrorInfo {
109
111
  if (isRetryHint(retry)) {
110
112
  info.retry = retry;
111
113
  }
114
+ if (Array.isArray(conflicts)) {
115
+ info.conflicts = conflicts.filter((path) => isString(path));
116
+ }
112
117
  return info;
113
118
  }
114
119
 
120
+ /**
121
+ * The files a refused publish overlaps on the remote, when the error is that refusal: a pull or a
122
+ * commit the remote has moved under, whose changes touch the same paths (`pull_conflict`, or the
123
+ * `conflict` problem type carrying `conflicts`). Null for every other failure.
124
+ */
125
+ export function remoteConflictsOf(cause: unknown): string[] | null {
126
+ const info = platformErrorInfo(cause);
127
+ const conflicting = info.code === "pull-conflict" || info.code === "conflict";
128
+ return conflicting && info.conflicts ? info.conflicts : null;
129
+ }
130
+
115
131
  /** The GitHub-App install link when the error is the structured needs-installation 403. */
116
132
  export function installUrlOf(cause: unknown): string | null {
117
133
  const info = platformErrorInfo(cause);
@@ -7,8 +7,9 @@
7
7
  * before Studio boots — while project-less mode (null) powers the /studio welcome screen.
8
8
  *
9
9
  * Cloud omissions (Studio degrades per @jxsuite/protocol's route table): pickDirectory, package
10
- * install/outdated/set-versions (package ops are manifest-only edits), multi-window, gitClone,
11
- * resolveClass, component discovery, code services.
10
+ * install and the install-on-open check (a cloud session has no `node_modules`; add, remove and
11
+ * set-versions update `package.json` and `package-lock.json` through the session's package
12
+ * service), multi-window, gitClone, resolveClass, component discovery, code services.
12
13
  */
13
14
 
14
15
  import { negotiateCollab } from "@jxsuite/collab/negotiate";
@@ -36,6 +37,7 @@ import type {
36
37
  FsEvent,
37
38
  FsResyncReason,
38
39
  GitBranchesResult,
40
+ GitCommitBranchResult,
39
41
  GitLogEntry,
40
42
  GitStatusResult,
41
43
  ImportProgressEvent,
@@ -43,6 +45,8 @@ import type {
43
45
  ImportSiteOptions,
44
46
  OfferedAction,
45
47
  PackageInfo,
48
+ PackageOpResult,
49
+ PackageVersionInfo,
46
50
  ProjectListEntry,
47
51
  SiteBuildResult,
48
52
  ProjectSchemasResponse,
@@ -217,6 +221,7 @@ interface StructuredBody extends ErrorBody {
217
221
  actions?: JsonValue;
218
222
  heading?: JsonValue;
219
223
  retry?: JsonValue;
224
+ conflicts?: JsonValue;
220
225
  }
221
226
 
222
227
  /** What {@link platformError} carries on the Error it returns: the HTTP status, plus what it found. */
@@ -268,6 +273,10 @@ async function platformError(res: Response, fallback: string): Promise<Error> {
268
273
  if (retry !== undefined) {
269
274
  details.retry = retry;
270
275
  }
276
+ const conflicts = body?.conflicts;
277
+ if (Array.isArray(conflicts)) {
278
+ details.conflicts = conflicts.filter((path) => isString(path));
279
+ }
271
280
  return Object.assign(new Error(problemDetail(body) ?? fallback), details);
272
281
  }
273
282
 
@@ -947,17 +956,6 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
947
956
  }
948
957
  }
949
958
 
950
- async function writePackageJson(pkg: JsonObject): Promise<void> {
951
- const res = await api("/file", {
952
- method: "PUT",
953
- headers: { "Content-Type": "application/json" },
954
- body: JSON.stringify({ path: "package.json", content: `${JSON.stringify(pkg, null, 2)}\n` }),
955
- });
956
- if (!res.ok) {
957
- throw new Error(await errorMessage(res, "Failed to update package.json"));
958
- }
959
- }
960
-
961
959
  // The bound session's /raw tree; see `documentBaseUrl` below for why both members ride together.
962
960
  const repoAssets: Pick<StudioPlatform, "assetSpace" | "documentBaseUrl"> = {};
963
961
  if (base) {
@@ -1284,10 +1282,12 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1284
1282
  *
1285
1283
  * A Worker ships a fixed set of extension packages (specs/extensions.md §5.5), decided by the
1286
1284
  * platform build rather than by this repository's `extensions/` tree, so the gateway is the
1287
- * only thing that knows. Everything it returns is therefore `bundled: true`: enabling one is a
1288
- * `project.json` write alone, and an extension the Worker does not bundle is dropped from the
1289
- * registry before composition — advertising it would promise a toggle that silently does
1290
- * nothing.
1285
+ * only thing that knows. An extension the Worker does not bundle is dropped from the registry
1286
+ * before composition, so advertising it would promise a toggle that silently does nothing.
1287
+ *
1288
+ * What the Worker bundles is what it can EDIT with, not what the project gets: the site is
1289
+ * built in Pages CI from the project's own install, so an entry the project has not declared is
1290
+ * install-then-enable here exactly as on every other backend (specs/extensions.md §3).
1291
1291
  *
1292
1292
  * `installed` here means DECLARED. Nothing resolves a module in a Worker: `addPackage` is a
1293
1293
  * manifest edit and resolution happens later in Pages CI, so the manifest is the only fact this
@@ -1309,7 +1309,6 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1309
1309
  ...Object.keys(dependencyBlock(pkg, "devDependencies")),
1310
1310
  ]);
1311
1311
  for (const entry of entries) {
1312
- entry.bundled = true;
1313
1312
  entry.installed = declared.has(entry.name);
1314
1313
  }
1315
1314
  return entries;
@@ -1435,28 +1434,49 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1435
1434
  return null;
1436
1435
  },
1437
1436
 
1438
- // ─── Packages (manifest-only edits; resolution happens in Pages CI) ───
1437
+ // ─── Packages (the session's lockfile service; nothing installs) ───
1439
1438
 
1439
+ /* Each mutation runs npm's installer lockfile-only on the platform, which writes
1440
+ `package.json` and `package-lock.json` back into the shared tree as one change, so a commit
1441
+ carries a lockfile a build can install from frozen (server.md §4.1). A failure changes
1442
+ nothing and its message names the cause. */
1440
1443
  async addPackage(name: string) {
1441
- const pkg = await readPackageJson();
1442
- const deps = dependencyBlock(pkg, "dependencies");
1443
- const at = name.lastIndexOf("@");
1444
- const [pkgName, version] =
1445
- at > 0 ? [name.slice(0, at), name.slice(at + 1)] : [name, "latest"];
1446
- deps[pkgName] = version;
1447
- pkg["dependencies"] = deps;
1448
- await writePackageJson(pkg);
1444
+ const res = await api("/packages/add", {
1445
+ body: JSON.stringify({ name }),
1446
+ headers: { "Content-Type": "application/json" },
1447
+ method: "POST",
1448
+ });
1449
+ if (!res.ok) {
1450
+ throw new Error(await errorMessage(res, `Failed to add ${name}`));
1451
+ }
1449
1452
  },
1450
1453
 
1451
1454
  async removePackage(name: string) {
1452
- const pkg = await readPackageJson();
1453
- for (const key of ["dependencies", "devDependencies"]) {
1454
- const deps = pkg[key];
1455
- if (isJsonObject(deps) && name in deps) {
1456
- pkg[key] = Object.fromEntries(Object.entries(deps).filter(([dep]) => dep !== name));
1457
- }
1455
+ const res = await api("/packages/remove", {
1456
+ body: JSON.stringify({ name }),
1457
+ headers: { "Content-Type": "application/json" },
1458
+ method: "POST",
1459
+ });
1460
+ if (!res.ok) {
1461
+ throw new Error(await errorMessage(res, `Failed to remove ${name}`));
1462
+ }
1463
+ },
1464
+
1465
+ async setPackageVersions(updates: { name: string; version: string; dev?: boolean }[]) {
1466
+ const res = await api("/packages/set-versions", {
1467
+ body: JSON.stringify({ updates }),
1468
+ headers: { "Content-Type": "application/json" },
1469
+ method: "POST",
1470
+ });
1471
+ if (!res.ok) {
1472
+ return { log: await errorMessage(res, "Failed to update versions"), ok: false };
1458
1473
  }
1459
- await writePackageJson(pkg);
1474
+ return readJson<PackageOpResult>(res);
1475
+ },
1476
+
1477
+ async packageVersions() {
1478
+ const res = await api("/packages/versions");
1479
+ return res.ok ? readJson<PackageVersionInfo[]>(res) : [];
1460
1480
  },
1461
1481
 
1462
1482
  async listPackages(): Promise<PackageInfo[]> {
@@ -1576,6 +1596,14 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1576
1596
  await okJson(await postJson("/git/create-branch", { name }), "Failed to create branch");
1577
1597
  },
1578
1598
 
1599
+ /** One commit, parented on the session's base, onto a branch the backend creates for it. */
1600
+ async gitCommitBranch(opts: { branch: string; message: string }) {
1601
+ return okJson<GitCommitBranchResult>(
1602
+ await postJson("/git/commit-branch", { branch: opts.branch, message: opts.message }),
1603
+ `Could not publish to ${opts.branch}`,
1604
+ );
1605
+ },
1606
+
1579
1607
  async gitDiff(path?: string) {
1580
1608
  const data = await okJson<{ diff: string }>(
1581
1609
  await api(`/git/diff?path=${encodeURIComponent(path ?? "")}`),