@jxsuite/studio 0.13.0 → 0.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -389,6 +389,31 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
389
389
  } else {
390
390
  canvasEl.removeAttribute("data-content-mode");
391
391
  }
392
+
393
+ // Resolve relative asset URLs through the PAL for environments that can't
394
+ // serve project files natively (e.g. ElectroBun's views:// scheme).
395
+ const platform = /** @type {any} */ (globalThis).__jxPlatform;
396
+ if (platform?.resolveAssetUrl && docBase) {
397
+ const mediaEls = el.querySelectorAll("img[src], video[src], source[src], video[poster]");
398
+ for (const node of mediaEls) {
399
+ for (const attr of ["src", "poster"]) {
400
+ const val = node.getAttribute(attr);
401
+ if (
402
+ val &&
403
+ !val.startsWith("data:") &&
404
+ !val.startsWith("blob:") &&
405
+ !val.startsWith("http")
406
+ ) {
407
+ try {
408
+ const resolved = new URL(val, docBase).pathname.replace(/^\//, "");
409
+ const dataUrl = await platform.resolveAssetUrl(resolved);
410
+ if (dataUrl) node.setAttribute(attr, dataUrl);
411
+ } catch {}
412
+ }
413
+ }
414
+ }
415
+ }
416
+
392
417
  canvasEl.appendChild(el);
393
418
  if (canvasMode === "design" || canvasMode === "edit") {
394
419
  // Custom element connectedCallbacks render children asynchronously —
@@ -32,6 +32,8 @@ import { registerPanelDnD } from "../panels/canvas-dnd.js";
32
32
  import { registerPanelEvents } from "../panels/panel-events.js";
33
33
  import { computeDocumentDiff } from "./canvas-diff.js";
34
34
  import { updateForcedPseudoPreview } from "../panels/pseudo-preview.js";
35
+ import { findCanvasElement } from "./canvas-helpers.js";
36
+ import { enterComponentInlineEdit } from "../editor/component-inline-edit.js";
35
37
  import { renderStylebookMode } from "../panels/stylebook-panel.js";
36
38
  import { dismissLinkPopover, dismissBlockActionBar } from "../panels/block-action-bar.js";
37
39
  import { dismissContextMenu } from "../editor/context-menu.js";
@@ -481,8 +483,9 @@ function renderCanvasIntoPanel(
481
483
  if (gitDiffState && docOverride) {
482
484
  // Determine which document is original and which is current
483
485
  const isOriginal = docOverride === (gitDiffState.originalDoc || gitDiffState.original);
484
- const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || tab?.doc.document;
485
- const currDoc = isOriginal ? gitDiffState.currentDoc || tab?.doc.document : docOverride;
486
+ const _tab = activeTab.value;
487
+ const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || _tab?.doc.document;
488
+ const currDoc = isOriginal ? gitDiffState.currentDoc || _tab?.doc.document : docOverride;
486
489
 
487
490
  const { byPath: diffMap } = computeDocumentDiff(origDoc, currDoc);
488
491
 
@@ -501,6 +504,21 @@ function renderCanvasIntoPanel(
501
504
  registerPanelEvents(panel);
502
505
  renderOverlays();
503
506
  updateForcedPseudoPreview();
507
+
508
+ // Process pending inline edit when canvas becomes ready
509
+ const currentTab = activeTab.value;
510
+ if (currentTab?.session.ui?.pendingInlineEdit) {
511
+ const { path, mediaName: mn } = /** @type {{ path: any; mediaName: string }} */ (
512
+ currentTab.session.ui.pendingInlineEdit
513
+ );
514
+ currentTab.session.ui.pendingInlineEdit = null;
515
+ const targetPanel =
516
+ canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
517
+ if (targetPanel) {
518
+ const el = findCanvasElement(path, targetPanel.canvas);
519
+ if (el) enterComponentInlineEdit(el, path);
520
+ }
521
+ }
504
522
  });
505
523
  }
506
524
 
@@ -168,8 +168,7 @@ export function applyTransform() {
168
168
  if (!view.panzoomWrap) return;
169
169
  const zoom = _ctx.getZoom();
170
170
  view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`;
171
- const label = document.querySelector(".zoom-indicator-label");
172
- if (label) label.textContent = `${Math.round(zoom * 100)}%`;
171
+ renderZoomIndicator();
173
172
  renderOnly("overlays");
174
173
  if (_ctx.getCanvasMode() === "settings") _ctx.renderStylebookOverlays();
175
174
  }
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import {
7
- getState,
8
7
  updateUi,
9
8
  renderOnly,
10
9
  getNodeAtPath,
@@ -48,8 +47,7 @@ export function enterComponentInlineEdit(el, path) {
48
47
  return;
49
48
  }
50
49
 
51
- const S = getState();
52
- const node = getNodeAtPath(S.document, path);
50
+ const node = getNodeAtPath(activeTab.value?.doc.document, path);
53
51
  if (!node) return;
54
52
 
55
53
  const tc = node.textContent;
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import {
7
- getState,
8
7
  renderOnly,
9
8
  getNodeAtPath,
10
9
  parentElementPath,
@@ -44,8 +43,7 @@ export function enterInlineEdit(el, path) {
44
43
  /** @type {any} */ children,
45
44
  /** @type {any} */ textContent,
46
45
  ) {
47
- const S = getState();
48
- const node = getNodeAtPath(S.document, commitPath);
46
+ const node = getNodeAtPath(activeTab.value?.doc.document, commitPath);
49
47
  if (children) {
50
48
  if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
51
49
  transactDoc(activeTab.value, (t) => {
@@ -26,41 +26,42 @@ let clipboard = null;
26
26
 
27
27
  // ─── Clipboard ────────────────────────────────────────────────────────────────
28
28
 
29
- /** @param {StudioState} S */
30
- export function copyNode(S) {
31
- if (!S.selection) return;
32
- const node = getNodeAtPath(S.document, S.selection);
29
+ export function copyNode() {
30
+ const tab = activeTab.value;
31
+ if (!tab?.session.selection) return;
32
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
33
33
  if (!node) return;
34
34
  clipboard = structuredClone(node);
35
35
  statusMessage("Copied");
36
36
  }
37
37
 
38
- /** @param {StudioState} S */
39
- export function cutNode(S) {
40
- if (!S.selection || S.selection.length < 2) return;
41
- const sel = S.selection;
42
- const node = getNodeAtPath(S.document, sel);
38
+ export function cutNode() {
39
+ const tab = activeTab.value;
40
+ if (!tab?.session.selection || tab.session.selection.length < 2) return;
41
+ const sel = tab.session.selection;
42
+ const node = getNodeAtPath(tab.doc.document, sel);
43
43
  if (!node) return;
44
44
  clipboard = structuredClone(node);
45
- transactDoc(activeTab.value, (t) => mutateRemoveNode(t, sel));
45
+ transactDoc(tab, (t) => mutateRemoveNode(t, sel));
46
46
  statusMessage("Cut");
47
47
  }
48
48
 
49
- /** @param {StudioState} S */
50
- export function pasteNode(S) {
49
+ export function pasteNode() {
51
50
  if (!clipboard) return;
51
+ const tab = activeTab.value;
52
+ if (!tab) return;
52
53
  const clip = clipboard;
53
- const pPath = S.selection || [];
54
- const parent = getNodeAtPath(S.document, pPath);
54
+ const pPath = tab.session.selection || [];
55
+ const parent = getNodeAtPath(tab.doc.document, pPath);
55
56
  if (!parent) return;
56
57
 
57
- if (S.selection && S.selection.length >= 2) {
58
- const pp = /** @type {JxPath} */ (parentElementPath(S.selection));
59
- const idx = /** @type {number} */ (childIndex(S.selection));
60
- transactDoc(activeTab.value, (t) => mutateInsertNode(t, pp, idx + 1, structuredClone(clip)));
58
+ if (tab.session.selection && tab.session.selection.length >= 2) {
59
+ const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
60
+ const idx = /** @type {number} */ (childIndex(tab.session.selection));
61
+ transactDoc(tab, (t) => mutateInsertNode(t, pp, idx + 1, structuredClone(clip)));
61
62
  } else {
62
63
  const idx = parent.children ? parent.children.length : 0;
63
- transactDoc(activeTab.value, (t) => mutateInsertNode(t, pPath, idx, structuredClone(clip)));
64
+ transactDoc(tab, (t) => mutateInsertNode(t, pPath, idx, structuredClone(clip)));
64
65
  }
65
66
  statusMessage("Pasted");
66
67
  }
@@ -85,25 +86,25 @@ export function dismissContextMenu() {
85
86
  /**
86
87
  * @param {MouseEvent} e
87
88
  * @param {JxPath} path
88
- * @param {StudioState} S
89
89
  * @param {{ onEditComponent?: (path: string) => void }} [opts]
90
90
  */
91
- export function showContextMenu(e, path, S, opts = {}) {
91
+ export function showContextMenu(e, path, opts = {}) {
92
92
  e.preventDefault();
93
93
  ctxMenu.removeAttribute("open");
94
94
 
95
- const node = getNodeAtPath(S.document, path);
95
+ const tab = activeTab.value;
96
+ const node = getNodeAtPath(tab?.doc.document, path);
96
97
  if (!node) return;
97
98
 
98
99
  // Select the node
99
- activeTab.value.session.selection = path;
100
+ tab.session.selection = path;
100
101
 
101
102
  /** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
102
103
  const items = [];
103
104
 
104
- items.push({ label: "Copy", action: () => copyNode(S) });
105
+ items.push({ label: "Copy", action: () => copyNode() });
105
106
  if (path.length >= 2) {
106
- items.push({ label: "Cut", action: () => cutNode(S) });
107
+ items.push({ label: "Cut", action: () => cutNode() });
107
108
  items.push({
108
109
  label: "Duplicate",
109
110
  action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
@@ -33,7 +33,6 @@ import { transactDoc, mutateInsertNode } from "../tabs/transact.js";
33
33
 
34
34
  /**
35
35
  * @typedef {Object} InsertionHelperContext
36
- * @property {() => any} getState - Returns the current editor state.
37
36
  * @property {() => string} getCanvasMode - Returns the active canvas mode.
38
37
  * @property {(fn: Function) => any} withPanelPointerEvents - Executes fn with pointer-events
39
38
  * temporarily enabled on the canvas.
@@ -18,18 +18,12 @@ import {
18
18
  import { isEditing } from "./inline-edit.js";
19
19
  import { copyNode, cutNode, pasteNode } from "./context-menu.js";
20
20
 
21
- /**
22
- * @typedef {import("../state.js").StudioState} StudioState
23
- *
24
- * @typedef {import("../state.js").JxPath} JxPath
25
- */
21
+ /** @typedef {import("../state.js").JxPath} JxPath */
26
22
 
27
23
  /**
28
24
  * Initialise all keyboard (and wheel/pointer) shortcuts.
29
25
  *
30
26
  * @param {() => {
31
- * S: StudioState;
32
- * setS: (s: StudioState) => void;
33
27
  * canvasMode: string;
34
28
  * panX: number;
35
29
  * panY: number;
@@ -47,7 +41,7 @@ export function initShortcuts(getContext) {
47
41
  canvasWrap.addEventListener(
48
42
  "wheel",
49
43
  (/** @type {WheelEvent} */ e) => {
50
- const { S, setS, canvasMode, panX, panY, setPan, applyTransform } = getContext();
44
+ const { canvasMode, panX, panY, setPan, applyTransform } = getContext();
51
45
  // Edit (content) mode: let the scroll container handle scrolling natively
52
46
  if (canvasMode === "edit") return;
53
47
  e.preventDefault();
@@ -56,13 +50,13 @@ export function initShortcuts(getContext) {
56
50
  const rect = canvasWrap.getBoundingClientRect();
57
51
  const cursorX = e.clientX - rect.left;
58
52
  const cursorY = e.clientY - rect.top;
59
- const oldZoom = S.ui.zoom;
53
+ const oldZoom = activeTab.value?.session.ui.zoom ?? 1;
60
54
  const delta = -e.deltaY * 0.005;
61
55
  const newZoom = Math.min(5.0, Math.max(0.05, oldZoom * (1 + delta)));
62
56
  const ratio = newZoom / oldZoom;
63
57
  // Adjust pan so the point under cursor stays stationary
64
58
  setPan(cursorX - (cursorX - panX) * ratio, cursorY - (cursorY - panY) * ratio);
65
- setS({ ...S, ui: { ...S.ui, zoom: newZoom } });
59
+ activeTab.value.session.ui.zoom = newZoom;
66
60
  } else if (e.shiftKey) {
67
61
  // Shift+scroll = horizontal pan
68
62
  setPan(panX - e.deltaY, panY);
@@ -105,8 +99,6 @@ export function initShortcuts(getContext) {
105
99
 
106
100
  document.addEventListener("keydown", (e) => {
107
101
  const {
108
- S,
109
- setS,
110
102
  canvasMode,
111
103
  setPan,
112
104
  applyTransform,
@@ -115,6 +107,7 @@ export function initShortcuts(getContext) {
115
107
  openProject,
116
108
  enterEditOnPath,
117
109
  } = getContext();
110
+ const tab = activeTab.value;
118
111
  const mod = e.ctrlKey || e.metaKey;
119
112
 
120
113
  // Don't intercept when typing in inputs or contenteditable
@@ -182,27 +175,27 @@ export function initShortcuts(getContext) {
182
175
  break;
183
176
  case "d":
184
177
  e.preventDefault();
185
- if (S.selection) {
186
- const sel = S.selection;
187
- transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, sel));
178
+ if (tab?.session.selection) {
179
+ const sel = tab.session.selection;
180
+ transactDoc(tab, (t) => mutateDuplicateNode(t, sel));
188
181
  }
189
182
  break;
190
183
  case "c":
191
184
  e.preventDefault();
192
- copyNode(S);
185
+ copyNode();
193
186
  break;
194
187
  case "x":
195
188
  e.preventDefault();
196
- cutNode(S);
189
+ cutNode();
197
190
  break;
198
191
  case "v":
199
192
  e.preventDefault();
200
- pasteNode(S);
193
+ pasteNode();
201
194
  break;
202
195
  case "0":
203
196
  if (canvasMode === "edit") break;
204
197
  e.preventDefault();
205
- setS({ ...S, ui: { ...S.ui, zoom: 1 } });
198
+ activeTab.value.session.ui.zoom = 1;
206
199
  setPan(16, 16);
207
200
  applyTransform();
208
201
  break;
@@ -210,13 +203,13 @@ export function initShortcuts(getContext) {
210
203
  case "+":
211
204
  if (canvasMode === "edit") break;
212
205
  e.preventDefault();
213
- setS({ ...S, ui: { ...S.ui, zoom: Math.min(5.0, S.ui.zoom * 1.2) } });
206
+ activeTab.value.session.ui.zoom = Math.min(5.0, (tab?.session.ui.zoom ?? 1) * 1.2);
214
207
  applyTransform();
215
208
  break;
216
209
  case "-":
217
210
  if (canvasMode === "edit") break;
218
211
  e.preventDefault();
219
- setS({ ...S, ui: { ...S.ui, zoom: Math.max(0.05, S.ui.zoom / 1.2) } });
212
+ activeTab.value.session.ui.zoom = Math.max(0.05, (tab?.session.ui.zoom ?? 1) / 1.2);
220
213
  applyTransform();
221
214
  break;
222
215
  }
@@ -226,22 +219,22 @@ export function initShortcuts(getContext) {
226
219
  switch (e.key) {
227
220
  case "Delete":
228
221
  case "Backspace":
229
- if (S.selection && S.selection.length >= 2) {
222
+ if (tab?.session.selection && tab.session.selection.length >= 2) {
230
223
  e.preventDefault();
231
- const sel = S.selection;
232
- transactDoc(activeTab.value, (t) => mutateRemoveNode(t, sel));
224
+ const sel = tab.session.selection;
225
+ transactDoc(tab, (t) => mutateRemoveNode(t, sel));
233
226
  }
234
227
  break;
235
228
  case "Escape":
236
229
  activeTab.value.session.selection = null;
237
230
  break;
238
231
  case "Enter":
239
- if (S.selection && S.selection.length >= 2) {
232
+ if (tab?.session.selection && tab.session.selection.length >= 2) {
240
233
  e.preventDefault();
241
- const pp = /** @type {JxPath} */ (parentElementPath(S.selection));
242
- const idx = /** @type {number} */ (childIndex(S.selection));
234
+ const pp = /** @type {JxPath} */ (parentElementPath(tab.session.selection));
235
+ const idx = /** @type {number} */ (childIndex(tab.session.selection));
243
236
  const newPath = [...pp, "children", idx + 1];
244
- transactDoc(activeTab.value, (t) => {
237
+ transactDoc(tab, (t) => {
245
238
  mutateInsertNode(t, pp, idx + 1, { tagName: "p", textContent: "" });
246
239
  t.session.selection = newPath;
247
240
  });
@@ -250,24 +243,24 @@ export function initShortcuts(getContext) {
250
243
  break;
251
244
  case "ArrowUp":
252
245
  e.preventDefault();
253
- navigateSelection(S);
246
+ navigateSelection(-1);
254
247
  break;
255
248
  case "ArrowDown":
256
249
  e.preventDefault();
257
- navigateSelection(S, 1);
250
+ navigateSelection(1);
258
251
  break;
259
252
  case "ArrowLeft":
260
253
  e.preventDefault();
261
- if (S.selection && S.selection.length >= 2) {
262
- activeTab.value.session.selection = parentElementPath(S.selection);
254
+ if (tab?.session.selection && tab.session.selection.length >= 2) {
255
+ activeTab.value.session.selection = parentElementPath(tab.session.selection);
263
256
  }
264
257
  break;
265
258
  case "ArrowRight":
266
259
  e.preventDefault();
267
- if (S.selection) {
268
- const node = getNodeAtPath(S.document, S.selection);
260
+ if (tab?.session.selection) {
261
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
269
262
  if (node?.children?.length > 0) {
270
- activeTab.value.session.selection = [...S.selection, "children", 0];
263
+ activeTab.value.session.selection = [...tab.session.selection, "children", 0];
271
264
  }
272
265
  }
273
266
  break;
@@ -286,23 +279,24 @@ export function initShortcuts(getContext) {
286
279
  );
287
280
  }
288
281
 
289
- /**
290
- * @param {StudioState} S
291
- * @param {number} [direction]
292
- */
293
- function navigateSelection(S, direction = -1) {
294
- if (!S.selection) {
282
+ /** @param {number} [direction] */
283
+ function navigateSelection(direction = -1) {
284
+ const tab = activeTab.value;
285
+ if (!tab?.session.selection) {
295
286
  activeTab.value.session.selection = [];
296
287
  return;
297
288
  }
298
- if (S.selection.length < 2) return;
289
+ if (tab.session.selection.length < 2) return;
299
290
 
300
- const parent = getNodeAtPath(S.document, /** @type {JxPath} */ (parentElementPath(S.selection)));
301
- const idx = /** @type {number} */ (childIndex(S.selection));
291
+ const parent = getNodeAtPath(
292
+ tab.doc.document,
293
+ /** @type {JxPath} */ (parentElementPath(tab.session.selection)),
294
+ );
295
+ const idx = /** @type {number} */ (childIndex(tab.session.selection));
302
296
  const newIdx = idx + direction;
303
297
  if (parent?.children && newIdx >= 0 && newIdx < parent.children.length) {
304
298
  activeTab.value.session.selection = [
305
- .../** @type {JxPath} */ (parentElementPath(S.selection)),
299
+ .../** @type {JxPath} */ (parentElementPath(tab.session.selection)),
306
300
  "children",
307
301
  newIdx,
308
302
  ];