@jxsuite/studio 0.9.0 → 0.10.1

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.
@@ -23,6 +23,7 @@ import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-
23
23
  import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
24
24
  import { componentRegistry } from "../files/components.js";
25
25
  import { convertToComponent } from "../editor/convert-to-component.js";
26
+ import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
26
27
 
27
28
  /** @type {any} */
28
29
  let _ctx = null;
@@ -32,8 +33,6 @@ let _ctx = null;
32
33
  *
33
34
  * @param {{
34
35
  * getCanvasMode: () => string;
35
- * findCanvasElement: Function;
36
- * getActivePanel: Function;
37
36
  * navigateToComponent: Function;
38
37
  * createFloatingContainer: Function;
39
38
  * }} ctx
@@ -186,6 +185,11 @@ export function dismissLinkPopover() {
186
185
  if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
187
186
  }
188
187
 
188
+ /** Dismiss the block action bar. */
189
+ export function dismissBlockActionBar() {
190
+ if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
191
+ }
192
+
189
193
  /** @param {any} anchorBtn */
190
194
  function showLinkPopover(anchorBtn) {
191
195
  litRender(nothing, view.linkPopoverHost);
@@ -312,12 +316,12 @@ export function renderBlockActionBar() {
312
316
  return;
313
317
  }
314
318
 
315
- const activePanel = _ctx.getActivePanel();
319
+ const activePanel = getActivePanel();
316
320
  if (!activePanel) {
317
321
  litRender(nothing, view.blockActionBarEl);
318
322
  return;
319
323
  }
320
- const el = _ctx.findCanvasElement(S.selection, activePanel.canvas);
324
+ const el = findCanvasElement(S.selection, activePanel.canvas);
321
325
  const node = el && getNodeAtPath(S.document, S.selection);
322
326
  if (!el || !node) {
323
327
  litRender(nothing, view.blockActionBarEl);
@@ -18,18 +18,7 @@ import {
18
18
  } from "../store.js";
19
19
  import { view } from "../view.js";
20
20
  import { applyDropInstruction } from "../panels/dnd.js";
21
-
22
- /** @type {any} */
23
- let _ctx = null;
24
-
25
- /**
26
- * Initialize the canvas DnD module.
27
- *
28
- * @param {{ effectiveZoom: () => number }} ctx
29
- */
30
- export function initCanvasDnD(ctx) {
31
- _ctx = ctx;
32
- }
21
+ import { effectiveZoom } from "../canvas/canvas-helpers.js";
33
22
 
34
23
  /**
35
24
  * Register all canvas elements in a panel as DnD drop targets.
@@ -133,7 +122,7 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
133
122
  return;
134
123
  }
135
124
 
136
- const scale = _ctx.effectiveZoom();
125
+ const scale = effectiveZoom();
137
126
  const wrapRect = viewport.getBoundingClientRect();
138
127
  const elRect = el.getBoundingClientRect();
139
128
  const left = (elRect.left - wrapRect.left + viewport.scrollLeft) / scale;
@@ -15,7 +15,6 @@ import {
15
15
  applyMutation,
16
16
  updateFrontmatter,
17
17
  } from "../store.js";
18
- import { view } from "../view.js";
19
18
  import { ensureLitState } from "./shared.js";
20
19
  import { renderLayersTemplate } from "./layers-panel.js";
21
20
  import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
@@ -96,7 +95,7 @@ function _render() {
96
95
  updateSession,
97
96
  });
98
97
  else if (tab === "data")
99
- content = _ctx.renderDataExplorerTemplate(S.document.state, view.liveScope, {
98
+ content = _ctx.renderDataExplorerTemplate(S.document.state, S.canvas?.scope ?? null, {
100
99
  renderCanvas: _ctx.renderCanvas,
101
100
  renderLeftPanel: render,
102
101
  defCategory: _ctx.defCategory,
@@ -6,6 +6,11 @@
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
7
  import { getState, canvasPanels, pathsEqual, subscribe } from "../store.js";
8
8
  import { view } from "../view.js";
9
+ import {
10
+ findCanvasElement,
11
+ getActivePanel,
12
+ overlayBoxDescriptor,
13
+ } from "../canvas/canvas-helpers.js";
9
14
 
10
15
  /** @type {any} */
11
16
  let _ctx = null;
@@ -16,8 +21,7 @@ let _unsub = null;
16
21
  /**
17
22
  * Mount the overlays panel.
18
23
  *
19
- * @param {any} ctx — { effectiveZoom, getCanvasMode, isEditing, renderBlockActionBar,
20
- * findCanvasElement, getActivePanel }
24
+ * @param {any} ctx — { getCanvasMode, isEditing, renderBlockActionBar }
21
25
  */
22
26
  export function mount(ctx) {
23
27
  _ctx = ctx;
@@ -32,24 +36,6 @@ export function unmount() {
32
36
  _ctx = null;
33
37
  }
34
38
 
35
- /**
36
- * @param {any} el
37
- * @param {any} type
38
- * @param {any} panel
39
- */
40
- function overlayBoxDescriptor(el, type, panel) {
41
- const vpRect = panel.viewport.getBoundingClientRect();
42
- const elRect = el.getBoundingClientRect();
43
- const scale = _ctx.effectiveZoom();
44
- return {
45
- cls: `overlay-box overlay-${type}`,
46
- top: `${(elRect.top - vpRect.top + panel.viewport.scrollTop) / scale}px`,
47
- left: `${(elRect.left - vpRect.left + panel.viewport.scrollLeft) / scale}px`,
48
- width: `${elRect.width / scale}px`,
49
- height: `${elRect.height / scale}px`,
50
- };
51
- }
52
-
53
39
  export function render() {
54
40
  if (!_ctx) return;
55
41
  const S = getState();
@@ -98,12 +84,12 @@ export function render() {
98
84
  const boxes = [];
99
85
 
100
86
  if (S.hover && !pathsEqual(S.hover, S.selection)) {
101
- const el = _ctx.findCanvasElement(S.hover, p.canvas);
87
+ const el = findCanvasElement(S.hover, p.canvas);
102
88
  if (el) boxes.push(overlayBoxDescriptor(el, "hover", p));
103
89
  }
104
90
 
105
- if (S.selection && p === _ctx.getActivePanel()) {
106
- const el = _ctx.findCanvasElement(S.selection, p.canvas);
91
+ if (S.selection && p === getActivePanel()) {
92
+ const el = findCanvasElement(S.selection, p.canvas);
107
93
  if (el) {
108
94
  const desc = overlayBoxDescriptor(el, "selection", p);
109
95
  if (view.componentInlineEdit || _ctx.isEditing()) /** @type {any} */ (desc).border = "none";
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {
8
8
  update,
9
+ updateUi,
9
10
  selectNode,
10
11
  hoverNode,
11
12
  elToPath,
@@ -21,6 +22,7 @@ import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit.j
21
22
  import { showContextMenu } from "../editor/context-menu.js";
22
23
  import * as insertionHelper from "../editor/insertion-helper.js";
23
24
  import { defaultDef } from "../panels/shared.js";
25
+ import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers.js";
24
26
 
25
27
  /** @type {any} */
26
28
  let _ctx = null;
@@ -32,11 +34,8 @@ let _ctx = null;
32
34
  * getState: () => any;
33
35
  * setState: (s: any) => void;
34
36
  * getCanvasMode: () => string;
35
- * bubbleInlinePath: (doc: any, path: any) => any;
36
- * findCanvasElement: (path: any, canvasEl: any) => any;
37
37
  * enterInlineEdit: (el: any, path: any) => void;
38
38
  * navigateToComponent: (path: any) => void;
39
- * effectiveZoom: () => number;
40
39
  * }} ctx
41
40
  */
42
41
  export function initPanelEvents(ctx) {
@@ -90,12 +89,11 @@ export function registerPanelEvents(panel) {
90
89
  if (canvas.contains(el) && el !== canvas) {
91
90
  const originalPath = elToPath.get(el);
92
91
  if (originalPath) {
93
- let path = _ctx.bubbleInlinePath(S.document, originalPath);
92
+ let path = bubbleInlinePath(S.document, originalPath);
94
93
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
95
94
  const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
96
95
 
97
- const resolvedEl =
98
- path === originalPath ? el : _ctx.findCanvasElement(path, canvas) || el;
96
+ const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
99
97
 
100
98
  if (
101
99
  pathsEqual(path, S.selection) &&
@@ -108,7 +106,7 @@ export function registerPanelEvents(panel) {
108
106
  }
109
107
 
110
108
  if (canvasMode === "design" && S.mode !== "content") {
111
- view.pendingInlineEdit = { path, mediaName };
109
+ updateUi("pendingInlineEdit", { path, mediaName });
112
110
  update(selectNode(withMedia, path));
113
111
  return;
114
112
  }
@@ -149,9 +147,8 @@ export function registerPanelEvents(panel) {
149
147
  if (canvas.contains(el) && el !== canvas) {
150
148
  const originalPath = elToPath.get(el);
151
149
  if (originalPath) {
152
- const path = _ctx.bubbleInlinePath(S.document, originalPath);
153
- const resolvedEl =
154
- path === originalPath ? el : _ctx.findCanvasElement(path, canvas) || el;
150
+ const path = bubbleInlinePath(S.document, originalPath);
151
+ const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
155
152
  if (isEditableBlock(resolvedEl)) {
156
153
  const newMedia = mediaName === "base" ? null : (mediaName ?? null);
157
154
  const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
@@ -188,7 +185,7 @@ export function registerPanelEvents(panel) {
188
185
  if (canvas.contains(el) && el !== canvas) {
189
186
  let path = elToPath.get(el);
190
187
  if (path) {
191
- path = _ctx.bubbleInlinePath(S.document, path);
188
+ path = bubbleInlinePath(S.document, path);
192
189
  showContextMenu(e, path, S, { onEditComponent: _ctx.navigateToComponent });
193
190
  return;
194
191
  }
@@ -218,7 +215,7 @@ export function registerPanelEvents(panel) {
218
215
  if (el && canvas.contains(el) && el !== canvas) {
219
216
  let path = elToPath.get(el);
220
217
  if (path) {
221
- path = _ctx.bubbleInlinePath(S.document, path);
218
+ path = bubbleInlinePath(S.document, path);
222
219
  if (!pathsEqual(path, S.hover)) {
223
220
  _ctx.setState(hoverNode(S, path));
224
221
  renderOnly("overlays");
@@ -249,7 +246,7 @@ export function registerPanelEvents(panel) {
249
246
  update,
250
247
  getCanvasMode: _ctx.getCanvasMode,
251
248
  withPanelPointerEvents,
252
- effectiveZoom: _ctx.effectiveZoom,
249
+ effectiveZoom: effectiveZoom,
253
250
  defaultDef,
254
251
  insertNode,
255
252
  selectNode,
@@ -5,18 +5,11 @@
5
5
 
6
6
  import { getState, getNodeAtPath } from "../store.js";
7
7
  import { view } from "../view.js";
8
+ import { getActivePanel, findCanvasElement } from "../canvas/canvas-helpers.js";
8
9
 
9
- /** @type {any} */
10
- let _ctx = null;
11
-
12
- /**
13
- * Initialize the pseudo-preview module.
14
- *
15
- * @param {{ getActivePanel: Function; findCanvasElement: Function }} ctx
16
- */
17
- export function initPseudoPreview(ctx) {
18
- _ctx = ctx;
19
- }
10
+ const pseudoStyleHost = document.createElement("div");
11
+ pseudoStyleHost.style.display = "contents";
12
+ (document.querySelector("sp-theme") || document.body).appendChild(pseudoStyleHost);
20
13
 
21
14
  export function updateForcedPseudoPreview() {
22
15
  if (view.forcedStyleTag) {
@@ -32,9 +25,9 @@ export function updateForcedPseudoPreview() {
32
25
  const sel = S.ui?.activeSelector;
33
26
  if (!sel || !sel.startsWith(":") || !S.selection) return;
34
27
 
35
- const panel = _ctx.getActivePanel();
28
+ const panel = getActivePanel();
36
29
  if (!panel) return;
37
- const el = _ctx.findCanvasElement(S.selection, panel.canvas);
30
+ const el = findCanvasElement(S.selection, panel.canvas);
38
31
  if (!el) return;
39
32
 
40
33
  const node = getNodeAtPath(S.document, S.selection);
@@ -59,6 +52,6 @@ export function updateForcedPseudoPreview() {
59
52
 
60
53
  const tag = document.createElement("style");
61
54
  tag.textContent = `[data-studio-forced] { ${cssProps} }`;
62
- document.head.appendChild(tag);
55
+ pseudoStyleHost.appendChild(tag);
63
56
  view.forcedStyleTag = tag;
64
57
  }
@@ -31,7 +31,7 @@ export function ensureLitState(container) {
31
31
  const start = part._$startNode;
32
32
  const end = part._$endNode;
33
33
  const startBad = start && start.parentNode !== container;
34
- const endBad = end && end !== container && end.parentNode !== container;
34
+ const endBad = (end && end !== container && end.parentNode !== container) || (!end && start);
35
35
  if (startBad || endBad) {
36
36
  console.warn("ensureLitState: clearing corrupted Lit state on", container.id || container);
37
37
  container.textContent = "";
package/src/platform.js CHANGED
@@ -5,27 +5,30 @@
5
5
  * platform adapter at startup. All file I/O, project loading, and component discovery goes through
6
6
  * this interface.
7
7
  *
8
+ * Uses window.__jxPlatform so the platform can be registered from a separate script bundle (e.g.
9
+ * init.js) before studio.js loads.
10
+ *
8
11
  * See spec/desktop.md §3 for the full StudioPlatform interface.
9
12
  */
10
13
 
11
14
  /** @typedef {Record<string, any>} StudioPlatform */
12
15
 
13
- /** @type {StudioPlatform | null} */
14
- let _platform = null;
16
+ /** @type {any} */
17
+ const g = globalThis;
15
18
 
16
19
  /** @param {StudioPlatform} platform */
17
20
  export function registerPlatform(platform) {
18
- _platform = platform;
21
+ g.__jxPlatform = platform;
19
22
  }
20
23
 
21
24
  /** @returns {StudioPlatform} */
22
25
  export function getPlatform() {
23
- if (!_platform)
26
+ if (!g.__jxPlatform)
24
27
  throw new Error("No platform registered. Call registerPlatform() before starting Studio.");
25
- return _platform;
28
+ return g.__jxPlatform;
26
29
  }
27
30
 
28
31
  /** @returns {boolean} */
29
32
  export function hasPlatform() {
30
- return _platform !== null;
33
+ return g.__jxPlatform != null;
31
34
  }
package/src/state.js CHANGED
@@ -29,6 +29,7 @@
29
29
  * mode: string;
30
30
  * content: { frontmatter: Record<string, any> };
31
31
  * ui: Record<string, any>;
32
+ * canvas: { status: string; scope: any; error: string | null };
32
33
  * }} StudioState
33
34
  */
34
35
 
@@ -234,6 +235,12 @@ export function createState(doc) {
234
235
  gitCommitMessage: "", // commit message input
235
236
  gitLoading: false, // loading indicator during async ops
236
237
  gitError: null, // error message string
238
+ pendingInlineEdit: null, // null | { path, mediaName } — deferred inline edit awaiting canvas readiness
239
+ },
240
+ canvas: {
241
+ status: "idle", // "idle" | "loading" | "ready" | "error"
242
+ scope: null, // $defs scope from runtime buildScope
243
+ error: null, // error message on failure
237
244
  },
238
245
  };
239
246
  }
@@ -272,6 +279,7 @@ export function fromFlat(S) {
272
279
  selection,
273
280
  hover,
274
281
  ui,
282
+ canvas,
275
283
  } = S;
276
284
  return {
277
285
  doc: {
@@ -286,7 +294,7 @@ export function fromFlat(S) {
286
294
  history,
287
295
  historyIndex,
288
296
  },
289
- session: { selection, hover, ui },
297
+ session: { selection, hover, ui, canvas },
290
298
  };
291
299
  }
292
300
 
package/src/store.js CHANGED
@@ -324,6 +324,15 @@ export function updateUi(field, value) {
324
324
  _updateSessionFn({ ui: { [field]: value } });
325
325
  }
326
326
 
327
+ /**
328
+ * Update the canvas async state (status, scope, error).
329
+ *
330
+ * @param {any} patch
331
+ */
332
+ export function updateCanvas(patch) {
333
+ _updateSessionFn({ canvas: patch });
334
+ }
335
+
327
336
  // ─── Subscription system ────────────────────────────────────────────────────
328
337
  // Panels subscribe to state changes and decide when to re-render, rather than
329
338
  // being called unconditionally from _update/_updateSession.