@jxsuite/studio 0.8.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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Canvas DnD — extracted from studio.js (Phase 4m). Registers canvas elements as drag-and-drop
3
+ * targets using @atlaskit/pragmatic-drag-and-drop.
4
+ */
5
+
6
+ import {
7
+ dropTargetForElements,
8
+ monitorForElements,
9
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
10
+
11
+ import {
12
+ getState,
13
+ elToPath,
14
+ canvasPanels,
15
+ getNodeAtPath,
16
+ VOID_ELEMENTS,
17
+ isAncestor,
18
+ } from "../store.js";
19
+ import { view } from "../view.js";
20
+ import { applyDropInstruction } from "../panels/dnd.js";
21
+ import { effectiveZoom } from "../canvas/canvas-helpers.js";
22
+
23
+ /**
24
+ * Register all canvas elements in a panel as DnD drop targets.
25
+ *
26
+ * @param {any} panel
27
+ */
28
+ export function registerPanelDnD(panel) {
29
+ const { canvas, dropLine } = panel;
30
+ const allEls = canvas.querySelectorAll("*");
31
+
32
+ const monitorCleanup = monitorForElements({
33
+ onDragStart() {
34
+ for (const el of canvas.querySelectorAll("*")) {
35
+ /** @type {any} */ (el).style.pointerEvents = "auto";
36
+ }
37
+ for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
38
+ },
39
+ onDrag({ location }) {
40
+ view.lastDragInput = location.current.input;
41
+ },
42
+ onDrop() {
43
+ for (const p of canvasPanels) p.dropLine.style.display = "none";
44
+ view.lastDragInput = null;
45
+ for (const el of canvas.querySelectorAll("*")) {
46
+ /** @type {any} */ (el).style.pointerEvents = "none";
47
+ }
48
+ for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
49
+ },
50
+ });
51
+ view.canvasDndCleanups.push(monitorCleanup);
52
+
53
+ const S = getState();
54
+ for (const el of allEls) {
55
+ const elPath = elToPath.get(el);
56
+ if (!elPath) continue;
57
+
58
+ const node = getNodeAtPath(S.document, elPath);
59
+ const isVoid = VOID_ELEMENTS.has((node?.tagName || "div").toLowerCase());
60
+
61
+ const cleanup = dropTargetForElements({
62
+ element: el,
63
+ canDrop({ source }) {
64
+ const srcPath = source.data.path;
65
+ if (srcPath && isAncestor(/** @type {any} */ (srcPath), elPath)) return false;
66
+ return true;
67
+ },
68
+ getData() {
69
+ return { path: elPath, _isVoid: isVoid };
70
+ },
71
+ onDragEnter() {
72
+ showCanvasDropIndicator(el, elPath, isVoid, panel);
73
+ },
74
+ onDrag() {
75
+ showCanvasDropIndicator(el, elPath, isVoid, panel);
76
+ },
77
+ onDragLeave() {
78
+ dropLine.style.display = "none";
79
+ el.classList.remove("canvas-drop-target");
80
+ },
81
+ onDrop({ source }) {
82
+ dropLine.style.display = "none";
83
+ el.classList.remove("canvas-drop-target");
84
+ const instruction = getCanvasDropInstruction(el, elPath, isVoid);
85
+ if (!instruction) return;
86
+ applyDropInstruction(instruction, source.data, elPath);
87
+ },
88
+ });
89
+ view.canvasDndCleanups.push(cleanup);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * @param {any} el
95
+ * @param {any} elPath
96
+ * @param {any} isVoid
97
+ */
98
+ function getCanvasDropInstruction(el, elPath, isVoid) {
99
+ const rect = el.getBoundingClientRect();
100
+ if (!view.lastDragInput) return null;
101
+ const y = view.lastDragInput.clientY;
102
+ const relY = (y - rect.top) / rect.height;
103
+
104
+ if (elPath.length === 0) return { type: "make-child" };
105
+ if (isVoid) return relY < 0.5 ? { type: "reorder-above" } : { type: "reorder-below" };
106
+ if (relY < 0.25) return { type: "reorder-above" };
107
+ if (relY > 0.75) return { type: "reorder-below" };
108
+ return { type: "make-child" };
109
+ }
110
+
111
+ /**
112
+ * @param {any} el
113
+ * @param {any} elPath
114
+ * @param {any} isVoid
115
+ * @param {any} panel
116
+ */
117
+ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
118
+ const instruction = getCanvasDropInstruction(el, elPath, isVoid);
119
+ const { dropLine, viewport } = panel;
120
+ if (!instruction) {
121
+ dropLine.style.display = "none";
122
+ return;
123
+ }
124
+
125
+ const scale = effectiveZoom();
126
+ const wrapRect = viewport.getBoundingClientRect();
127
+ const elRect = el.getBoundingClientRect();
128
+ const left = (elRect.left - wrapRect.left + viewport.scrollLeft) / scale;
129
+ const width = elRect.width / scale;
130
+
131
+ if (instruction.type === "make-child") {
132
+ dropLine.style.display = "block";
133
+ dropLine.style.top = `${(elRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
134
+ dropLine.style.left = `${left}px`;
135
+ dropLine.style.width = `${width}px`;
136
+ dropLine.style.height = `${elRect.height / scale}px`;
137
+ dropLine.className = "canvas-drop-indicator inside";
138
+ el.classList.add("canvas-drop-target");
139
+ return;
140
+ }
141
+
142
+ el.classList.remove("canvas-drop-target");
143
+ const top =
144
+ instruction.type === "reorder-above"
145
+ ? (elRect.top - wrapRect.top + viewport.scrollTop) / scale
146
+ : (elRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
147
+
148
+ dropLine.style.display = "block";
149
+ dropLine.style.top = `${top}px`;
150
+ dropLine.style.left = `${left}px`;
151
+ dropLine.style.width = `${width}px`;
152
+ dropLine.style.height = "2px";
153
+ dropLine.className = "canvas-drop-indicator line";
154
+ }
@@ -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";
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Panel events — extracted from studio.js (Phase 4m). Unified event handler system for canvas
3
+ * panels: click-to-select, double-click inline edit, context menu, hover tracking, insertion
4
+ * helper.
5
+ */
6
+
7
+ import {
8
+ update,
9
+ updateUi,
10
+ selectNode,
11
+ hoverNode,
12
+ elToPath,
13
+ pathsEqual,
14
+ insertNode,
15
+ parentElementPath,
16
+ childIndex,
17
+ getNodeAtPath,
18
+ renderOnly,
19
+ } from "../store.js";
20
+ import { view } from "../view.js";
21
+ import { stopEditing, isEditing, isEditableBlock } from "../editor/inline-edit.js";
22
+ import { showContextMenu } from "../editor/context-menu.js";
23
+ import * as insertionHelper from "../editor/insertion-helper.js";
24
+ import { defaultDef } from "../panels/shared.js";
25
+ import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers.js";
26
+
27
+ /** @type {any} */
28
+ let _ctx = null;
29
+
30
+ /**
31
+ * Initialize the panel events module.
32
+ *
33
+ * @param {{
34
+ * getState: () => any;
35
+ * setState: (s: any) => void;
36
+ * getCanvasMode: () => string;
37
+ * enterInlineEdit: (el: any, path: any) => void;
38
+ * navigateToComponent: (path: any) => void;
39
+ * }} ctx
40
+ */
41
+ export function initPanelEvents(ctx) {
42
+ _ctx = ctx;
43
+ }
44
+
45
+ /** @param {any} panel */
46
+ export function registerPanelEvents(panel) {
47
+ const { canvas, overlayClk, mediaName } = panel;
48
+ const ac = new AbortController();
49
+ const opts = { signal: ac.signal };
50
+ view.canvasEventCleanups.push(() => ac.abort());
51
+
52
+ /** @param {any} fn */
53
+ function withPanelPointerEvents(fn) {
54
+ const els = canvas.querySelectorAll("*");
55
+ for (const el of els) el.style.pointerEvents = "auto";
56
+ overlayClk.style.display = "none";
57
+ const result = fn();
58
+ overlayClk.style.display = "";
59
+ for (const el of els) el.style.pointerEvents = "none";
60
+ return result;
61
+ }
62
+
63
+ overlayClk.addEventListener(
64
+ "click",
65
+ (/** @type {any} */ e) => {
66
+ const barInner = view.blockActionBarEl?.firstElementChild;
67
+ if (barInner) {
68
+ const r = barInner.getBoundingClientRect();
69
+ if (
70
+ e.clientX >= r.left &&
71
+ e.clientX <= r.right &&
72
+ e.clientY >= r.top &&
73
+ e.clientY <= r.bottom
74
+ )
75
+ return;
76
+ }
77
+ if (isEditing()) {
78
+ stopEditing();
79
+ }
80
+
81
+ const S = _ctx.getState();
82
+ const canvasMode = _ctx.getCanvasMode();
83
+
84
+ const elements = withPanelPointerEvents(() =>
85
+ document.elementsFromPoint(e.clientX, e.clientY),
86
+ );
87
+
88
+ for (const el of elements) {
89
+ if (canvas.contains(el) && el !== canvas) {
90
+ const originalPath = elToPath.get(el);
91
+ if (originalPath) {
92
+ let path = bubbleInlinePath(S.document, originalPath);
93
+ const newMedia = mediaName === "base" ? null : (mediaName ?? null);
94
+ const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
95
+
96
+ const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
97
+
98
+ if (
99
+ pathsEqual(path, S.selection) &&
100
+ isEditableBlock(resolvedEl) &&
101
+ (canvasMode === "edit" || S.mode === "content")
102
+ ) {
103
+ _ctx.setState(withMedia);
104
+ _ctx.enterInlineEdit(resolvedEl, path);
105
+ return;
106
+ }
107
+
108
+ if (canvasMode === "design" && S.mode !== "content") {
109
+ updateUi("pendingInlineEdit", { path, mediaName });
110
+ update(selectNode(withMedia, path));
111
+ return;
112
+ }
113
+
114
+ update(selectNode(withMedia, path));
115
+ return;
116
+ }
117
+ }
118
+ }
119
+ update(selectNode(S, null));
120
+ },
121
+ opts,
122
+ );
123
+
124
+ overlayClk.addEventListener(
125
+ "dblclick",
126
+ (/** @type {any} */ e) => {
127
+ const barInner = view.blockActionBarEl?.firstElementChild;
128
+ if (barInner) {
129
+ const r = barInner.getBoundingClientRect();
130
+ if (
131
+ e.clientX >= r.left &&
132
+ e.clientX <= r.right &&
133
+ e.clientY >= r.top &&
134
+ e.clientY <= r.bottom
135
+ )
136
+ return;
137
+ }
138
+ const canvasMode = _ctx.getCanvasMode();
139
+ if (canvasMode !== "edit" && canvasMode !== "design") return;
140
+
141
+ const S = _ctx.getState();
142
+ const elements = withPanelPointerEvents(() =>
143
+ document.elementsFromPoint(e.clientX, e.clientY),
144
+ );
145
+
146
+ for (const el of elements) {
147
+ if (canvas.contains(el) && el !== canvas) {
148
+ const originalPath = elToPath.get(el);
149
+ if (originalPath) {
150
+ const path = bubbleInlinePath(S.document, originalPath);
151
+ const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
152
+ if (isEditableBlock(resolvedEl)) {
153
+ const newMedia = mediaName === "base" ? null : (mediaName ?? null);
154
+ const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
155
+ update(selectNode(withMedia, path));
156
+ _ctx.enterInlineEdit(resolvedEl, path);
157
+ return;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ },
163
+ opts,
164
+ );
165
+
166
+ overlayClk.addEventListener(
167
+ "contextmenu",
168
+ (/** @type {any} */ e) => {
169
+ const barInner = view.blockActionBarEl?.firstElementChild;
170
+ if (barInner) {
171
+ const r = barInner.getBoundingClientRect();
172
+ if (
173
+ e.clientX >= r.left &&
174
+ e.clientX <= r.right &&
175
+ e.clientY >= r.top &&
176
+ e.clientY <= r.bottom
177
+ )
178
+ return;
179
+ }
180
+ const S = _ctx.getState();
181
+ const elements = withPanelPointerEvents(() =>
182
+ document.elementsFromPoint(e.clientX, e.clientY),
183
+ );
184
+ for (const el of elements) {
185
+ if (canvas.contains(el) && el !== canvas) {
186
+ let path = elToPath.get(el);
187
+ if (path) {
188
+ path = bubbleInlinePath(S.document, path);
189
+ showContextMenu(e, path, S, { onEditComponent: _ctx.navigateToComponent });
190
+ return;
191
+ }
192
+ }
193
+ }
194
+ e.preventDefault();
195
+ },
196
+ opts,
197
+ );
198
+
199
+ overlayClk.addEventListener(
200
+ "mousemove",
201
+ (/** @type {any} */ e) => {
202
+ const barInner = view.blockActionBarEl?.firstElementChild;
203
+ if (barInner) {
204
+ const r = barInner.getBoundingClientRect();
205
+ if (
206
+ e.clientX >= r.left &&
207
+ e.clientX <= r.right &&
208
+ e.clientY >= r.top &&
209
+ e.clientY <= r.bottom
210
+ )
211
+ return;
212
+ }
213
+ let S = _ctx.getState();
214
+ const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
215
+ if (el && canvas.contains(el) && el !== canvas) {
216
+ let path = elToPath.get(el);
217
+ if (path) {
218
+ path = bubbleInlinePath(S.document, path);
219
+ if (!pathsEqual(path, S.hover)) {
220
+ _ctx.setState(hoverNode(S, path));
221
+ renderOnly("overlays");
222
+ }
223
+ }
224
+ } else if (S.hover) {
225
+ _ctx.setState(hoverNode(S, null));
226
+ renderOnly("overlays");
227
+ }
228
+ },
229
+ opts,
230
+ );
231
+
232
+ overlayClk.addEventListener(
233
+ "mouseleave",
234
+ () => {
235
+ const S = _ctx.getState();
236
+ if (S.hover) {
237
+ _ctx.setState(hoverNode(S, null));
238
+ renderOnly("overlays");
239
+ }
240
+ },
241
+ opts,
242
+ );
243
+
244
+ insertionHelper.mount({
245
+ getState: _ctx.getState,
246
+ update,
247
+ getCanvasMode: _ctx.getCanvasMode,
248
+ withPanelPointerEvents,
249
+ effectiveZoom: effectiveZoom,
250
+ defaultDef,
251
+ insertNode,
252
+ selectNode,
253
+ parentElementPath,
254
+ childIndex,
255
+ getNodeAtPath,
256
+ elToPath,
257
+ panel,
258
+ });
259
+ view.canvasEventCleanups.push(() => insertionHelper.unmount());
260
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Preview render — extracted from studio.js (Phase 4m). Structural preview renderer that creates
3
+ * DOM from Jx node trees as a fallback when runtime rendering fails.
4
+ */
5
+
6
+ import { getState, elToPath } from "../store.js";
7
+ import { applyCanvasStyle } from "../utils/canvas-media.js";
8
+ import { resolveDefaultForCanvas } from "../panels/signals-panel.js";
9
+
10
+ /**
11
+ * Recursively render a Jx node to the canvas DOM. Media-aware: applies base styles + active
12
+ * breakpoint/feature overrides.
13
+ *
14
+ * @param {any} node
15
+ * @param {any} path
16
+ * @param {any} parent
17
+ * @param {any} activeBreakpoints
18
+ * @param {any} featureToggles
19
+ */
20
+ export function renderCanvasNode(node, path, parent, activeBreakpoints, featureToggles) {
21
+ if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
22
+ parent.appendChild(document.createTextNode(String(node)));
23
+ return;
24
+ }
25
+ if (!node || typeof node !== "object") return;
26
+
27
+ const tag = node.tagName || "div";
28
+ const el = document.createElement(tag);
29
+
30
+ elToPath.set(el, path);
31
+
32
+ if (typeof node.textContent === "string") {
33
+ el.textContent = node.textContent;
34
+ } else if (typeof node.textContent === "object" && node.textContent?.$ref) {
35
+ const resolved = resolveDefaultForCanvas(node.textContent, getState().document.state);
36
+ el.textContent = resolved;
37
+ el.style.opacity = "0.7";
38
+ el.style.fontStyle = "italic";
39
+ el.title = `Bound: ${node.textContent.$ref}`;
40
+ }
41
+
42
+ if (node.id) el.id = node.id;
43
+ if (node.className) el.className = node.className;
44
+
45
+ applyCanvasStyle(el, node.style, activeBreakpoints, featureToggles);
46
+
47
+ if (node.attributes && typeof node.attributes === "object") {
48
+ for (const [attr, val] of Object.entries(node.attributes)) {
49
+ try {
50
+ if (typeof val === "object" && val?.$ref) {
51
+ const resolved = resolveDefaultForCanvas(val, getState().document.state);
52
+ el.setAttribute(attr, resolved);
53
+ } else {
54
+ el.setAttribute(attr, val);
55
+ }
56
+ } catch {}
57
+ }
58
+ }
59
+
60
+ if (Array.isArray(node.children)) {
61
+ for (let i = 0; i < node.children.length; i++) {
62
+ renderCanvasNode(
63
+ node.children[i],
64
+ [...path, "children", i],
65
+ el,
66
+ activeBreakpoints,
67
+ featureToggles,
68
+ );
69
+ }
70
+ } else if (
71
+ node.children &&
72
+ typeof node.children === "object" &&
73
+ node.children.$prototype === "Array"
74
+ ) {
75
+ const template = node.children.map;
76
+ if (template && typeof template === "object") {
77
+ const wrapper = document.createElement("div");
78
+ wrapper.className = "repeater-perimeter";
79
+ elToPath.set(wrapper, [...path, "children"]);
80
+ renderCanvasNode(
81
+ template,
82
+ [...path, "children", "map"],
83
+ wrapper,
84
+ activeBreakpoints,
85
+ featureToggles,
86
+ );
87
+ el.appendChild(wrapper);
88
+ }
89
+ }
90
+
91
+ if (node.$switch && node.cases && typeof node.cases === "object") {
92
+ const keys = Object.keys(node.cases);
93
+ const placeholder = document.createElement("div");
94
+ placeholder.textContent = `[$switch: ${keys.join(" | ")}]`;
95
+ placeholder.style.cssText =
96
+ "font-family:monospace;font-size:11px;padding:6px 10px;background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px dashed color-mix(in srgb, var(--danger) 40%, transparent);border-radius:4px;color:var(--danger);font-style:italic";
97
+ el.appendChild(placeholder);
98
+ }
99
+
100
+ el.style.pointerEvents = "none";
101
+ parent.appendChild(el);
102
+ return el;
103
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pseudo-state preview — extracted from studio.js (Phase 4m). When a pseudo-selector (:hover,
3
+ * :focus, etc.) is active in the style sidebar, force those styles onto the selected element.
4
+ */
5
+
6
+ import { getState, getNodeAtPath } from "../store.js";
7
+ import { view } from "../view.js";
8
+ import { getActivePanel, findCanvasElement } from "../canvas/canvas-helpers.js";
9
+
10
+ const pseudoStyleHost = document.createElement("div");
11
+ pseudoStyleHost.style.display = "contents";
12
+ (document.querySelector("sp-theme") || document.body).appendChild(pseudoStyleHost);
13
+
14
+ export function updateForcedPseudoPreview() {
15
+ if (view.forcedStyleTag) {
16
+ view.forcedStyleTag.remove();
17
+ view.forcedStyleTag = null;
18
+ }
19
+ if (view.forcedAttrEl) {
20
+ view.forcedAttrEl.removeAttribute("data-studio-forced");
21
+ view.forcedAttrEl = null;
22
+ }
23
+
24
+ const S = getState();
25
+ const sel = S.ui?.activeSelector;
26
+ if (!sel || !sel.startsWith(":") || !S.selection) return;
27
+
28
+ const panel = getActivePanel();
29
+ if (!panel) return;
30
+ const el = findCanvasElement(S.selection, panel.canvas);
31
+ if (!el) return;
32
+
33
+ const node = getNodeAtPath(S.document, S.selection);
34
+ if (!node?.style) return;
35
+ const activeTab = S.ui.activeMedia;
36
+ /** @type {any} */
37
+ const ctx = activeTab ? node.style[`@${activeTab}`] || {} : node.style;
38
+ const rules = ctx[sel];
39
+ if (!rules || typeof rules !== "object") return;
40
+
41
+ const cssProps = Object.entries(rules)
42
+ .filter(([k]) => typeof rules[k] === "string" || typeof rules[k] === "number")
43
+ .map(
44
+ ([k, v]) =>
45
+ `${k.replace(/[A-Z]/g, (/** @type {any} */ c) => `-${c.toLowerCase()}`)}: ${v} !important`,
46
+ )
47
+ .join("; ");
48
+ if (!cssProps) return;
49
+
50
+ el.setAttribute("data-studio-forced", "1");
51
+ view.forcedAttrEl = el;
52
+
53
+ const tag = document.createElement("style");
54
+ tag.textContent = `[data-studio-forced] { ${cssProps} }`;
55
+ pseudoStyleHost.appendChild(tag);
56
+ view.forcedStyleTag = tag;
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
  }