@jxsuite/studio 0.9.0 → 0.10.2

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;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * When editing project.json: shows Class Imports, Dependencies (add/remove packages), and
5
5
  * per-package component toggles for cherry-picking individual elements. When editing a
6
- * page/layout/component/collection: shows Component Imports ($ref picker) and per-package component
7
- * toggles.
6
+ * page/layout/component/content type: shows Component Imports ($ref picker) and per-package
7
+ * component toggles.
8
8
  */
9
9
 
10
10
  import { html, nothing } from "lit-html";
@@ -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,
@@ -27,7 +27,7 @@ import { renderFieldRow } from "../ui/field-row.js";
27
27
  import {
28
28
  attrLabel,
29
29
  inferInputType,
30
- findCollectionSchema,
30
+ findContentTypeSchema,
31
31
  friendlyNameToVar,
32
32
  camelToLabel,
33
33
  parseCemType,
@@ -210,7 +210,7 @@ function kvRow(
210
210
  function renderFrontmatterOnlyPanel() {
211
211
  const S = getState();
212
212
  const fm = S.content?.frontmatter || {};
213
- const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
213
+ const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
214
214
  const schemaProps = col?.schema?.properties;
215
215
  const requiredFields = new Set(col?.schema?.required || []);
216
216
 
@@ -1276,7 +1276,7 @@ export function renderPropertiesPanelTemplate(ctx) {
1276
1276
  S.mode === "content"
1277
1277
  ? (() => {
1278
1278
  const fm = S.content?.frontmatter || {};
1279
- const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
1279
+ const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
1280
1280
  const schemaProps = col?.schema?.properties;
1281
1281
  const requiredFields = new Set(col?.schema?.required || []);
1282
1282
 
@@ -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 = "";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
3
- * definitions, collections). Extracted from studio.js Phase 4e.
3
+ * definitions, content types). Extracted from studio.js Phase 4e.
4
4
  */
5
5
 
6
6
  import { html, render as litRender, nothing } from "lit-html";
@@ -26,7 +26,7 @@ import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-me
26
26
  import { mediaDisplayName } from "./shared.js";
27
27
  import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
28
28
  import { renderDefsEditor } from "../settings/defs-editor.js";
29
- import { renderCollectionsEditor } from "../settings/collections-editor.js";
29
+ import { renderContentTypesEditor } from "../settings/content-types-editor.js";
30
30
  import stylebookMeta from "../../data/stylebook-meta.json";
31
31
 
32
32
  export { stylebookMeta };
@@ -67,12 +67,12 @@ export function renderStylebookMode(ctx) {
67
67
  >
68
68
  <sp-tab label="Stylebook" value="stylebook"></sp-tab>
69
69
  <sp-tab label="Definitions" value="definitions"></sp-tab>
70
- <sp-tab label="Collections" value="collections"></sp-tab>
70
+ <sp-tab label="Content Types" value="contentTypes"></sp-tab>
71
71
  </sp-tabs>
72
72
  </div>
73
73
  `;
74
74
 
75
- if (settingsTab === "definitions" || settingsTab === "collections") {
75
+ if (settingsTab === "definitions" || settingsTab === "contentTypes") {
76
76
  /** @type {any} */ (canvasWrap).style.overflow = "hidden";
77
77
 
78
78
  litRender(
@@ -88,7 +88,7 @@ export function renderStylebookMode(ctx) {
88
88
  canvasWrap.querySelector(".settings-editor-container")
89
89
  );
90
90
  if (settingsTab === "definitions") renderDefsEditor(container);
91
- else renderCollectionsEditor(container);
91
+ else renderContentTypesEditor(container);
92
92
  return;
93
93
  }
94
94
 
@@ -261,12 +261,6 @@ export function selectStylebookTag(tag, media) {
261
261
  },
262
262
  });
263
263
  renderStylebookOverlays();
264
- requestAnimationFrame(() => {
265
- if (canvasPanels.length > 0) {
266
- const el = findStylebookEl(canvasPanels[0].canvas, tag);
267
- if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
268
- }
269
- });
270
264
  }
271
265
 
272
266
  /** Draw selection + hover overlays for stylebook elements */
@@ -324,7 +318,7 @@ export function renderStylebookOverlays() {
324
318
  }
325
319
  }
326
320
 
327
- // ─── Internal helpers ─────────────────────────────────────────────────────────
321
+ // ─── Internal helpers (exported for testing) ─────────────────────────────────
328
322
 
329
323
  /**
330
324
  * Build a DOM element tree from a stylebook-meta.json entry.
@@ -332,8 +326,9 @@ export function renderStylebookOverlays() {
332
326
  * @param {any} entry
333
327
  * @param {any} rootStyle
334
328
  * @param {any} activeBreakpoints
329
+ * @param {string | null} [parentTag]
335
330
  */
336
- function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
331
+ export function buildStylebookElement(entry, rootStyle, activeBreakpoints, parentTag = null) {
337
332
  const el = document.createElement(entry.tag);
338
333
  if (entry.text) el.textContent = entry.text;
339
334
  if (entry.attributes) {
@@ -344,7 +339,9 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
344
339
  }
345
340
  }
346
341
  if (entry.style) el.style.cssText = entry.style;
347
- const tagStyle = rootStyle[`& ${entry.tag}`];
342
+ const compoundSelector =
343
+ parentTag && parentTag !== entry.tag ? `& ${parentTag} ${entry.tag}` : null;
344
+ const tagStyle = (compoundSelector && rootStyle[compoundSelector]) || rootStyle[`& ${entry.tag}`];
348
345
  if (tagStyle) {
349
346
  for (const [prop, val] of Object.entries(tagStyle)) {
350
347
  if (typeof val === "string" || typeof val === "number") {
@@ -372,7 +369,7 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
372
369
  }
373
370
  if (entry.children) {
374
371
  for (const child of entry.children) {
375
- el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
372
+ el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
376
373
  }
377
374
  }
378
375
  return el;
@@ -429,13 +426,19 @@ function hasTagStyle(rootStyle, tag) {
429
426
  * @param {any} customizedOnly
430
427
  * @param {any} activeBreakpoints
431
428
  */
432
- function renderStylebookElementsIntoCanvas(
429
+ export function renderStylebookElementsIntoCanvas(
433
430
  canvasEl,
434
431
  rootStyle,
435
432
  filter,
436
433
  customizedOnly,
437
434
  activeBreakpoints,
438
435
  ) {
436
+ for (const [k, v] of Object.entries(rootStyle)) {
437
+ if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
438
+ canvasEl.style.setProperty(k, String(v));
439
+ }
440
+ }
441
+
439
442
  /** @type {import("lit-html").TemplateResult[]} */
440
443
  const sectionTemplates = [];
441
444
 
@@ -462,10 +465,11 @@ function renderStylebookElementsIntoCanvas(
462
465
  view.stylebookElToTag.set(card, entry.tag);
463
466
  elToPath.set(card, ["__sb", entry.tag]);
464
467
  for (const child of el.querySelectorAll("*")) {
465
- const tag = child.tagName.toLowerCase();
468
+ const childTag = child.tagName.toLowerCase();
466
469
  if (!view.stylebookElToTag.has(child)) {
467
- view.stylebookElToTag.set(child, tag);
468
- elToPath.set(child, ["__sb", tag]);
470
+ const compound = childTag === entry.tag ? entry.tag : `${entry.tag} ${childTag}`;
471
+ view.stylebookElToTag.set(child, compound);
472
+ elToPath.set(child, ["__sb", compound]);
469
473
  }
470
474
  }
471
475
  })}
@@ -473,7 +477,10 @@ function renderStylebookElementsIntoCanvas(
473
477
  <div
474
478
  class="element-card-preview"
475
479
  ${ref((c) => {
476
- if (c && !c.firstChild) c.appendChild(el);
480
+ if (c) {
481
+ c.textContent = "";
482
+ c.appendChild(el);
483
+ }
477
484
  })}
478
485
  ></div>
479
486
  <div class="element-card-label">&lt;${entry.tag}&gt;</div>
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
  }