@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.
- package/dist/studio.js +156878 -156742
- package/dist/studio.js.map +156 -145
- package/package.json +2 -2
- package/src/canvas/canvas-helpers.js +121 -0
- package/src/canvas/canvas-live-render.js +286 -0
- package/src/canvas/canvas-render.js +429 -0
- package/src/canvas/canvas-utils.js +277 -0
- package/src/editor/component-inline-edit.js +317 -0
- package/src/editor/content-inline-edit.js +213 -0
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +154 -0
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +260 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/pseudo-preview.js +57 -0
- package/src/panels/shared.js +1 -1
- package/src/platform.js +9 -6
- package/src/state.js +9 -1
- package/src/store.js +9 -0
- package/src/studio.js +122 -2349
- package/src/utils/edit-display.js +197 -0
- package/src/view.js +0 -2
|
@@ -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
|
+
}
|
package/src/panels/left-panel.js
CHANGED
|
@@ -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,
|
|
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,
|
package/src/panels/overlays.js
CHANGED
|
@@ -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 — {
|
|
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 =
|
|
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 ===
|
|
106
|
-
const el =
|
|
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
|
+
}
|
package/src/panels/shared.js
CHANGED
|
@@ -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 {
|
|
14
|
-
|
|
16
|
+
/** @type {any} */
|
|
17
|
+
const g = globalThis;
|
|
15
18
|
|
|
16
19
|
/** @param {StudioPlatform} platform */
|
|
17
20
|
export function registerPlatform(platform) {
|
|
18
|
-
|
|
21
|
+
g.__jxPlatform = platform;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/** @returns {StudioPlatform} */
|
|
22
25
|
export function getPlatform() {
|
|
23
|
-
if (!
|
|
26
|
+
if (!g.__jxPlatform)
|
|
24
27
|
throw new Error("No platform registered. Call registerPlatform() before starting Studio.");
|
|
25
|
-
return
|
|
28
|
+
return g.__jxPlatform;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/** @returns {boolean} */
|
|
29
32
|
export function hasPlatform() {
|
|
30
|
-
return
|
|
33
|
+
return g.__jxPlatform != null;
|
|
31
34
|
}
|