@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.
- package/dist/studio.js +117584 -117551
- package/dist/studio.js.map +103 -100
- package/package.json +29 -29
- package/src/browse/browse.js +21 -19
- package/src/canvas/canvas-helpers.js +121 -0
- package/src/canvas/canvas-live-render.js +292 -0
- package/src/canvas/canvas-render.js +431 -0
- package/src/canvas/canvas-utils.js +33 -69
- package/src/editor/component-inline-edit.js +5 -4
- package/src/editor/content-inline-edit.js +11 -18
- package/src/files/files.js +2 -0
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +2 -13
- package/src/panels/imports-panel.js +2 -2
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +10 -13
- package/src/panels/properties-panel.js +3 -3
- package/src/panels/pseudo-preview.js +7 -14
- package/src/panels/shared.js +1 -1
- package/src/panels/stylebook-panel.js +27 -20
- package/src/platform.js +9 -6
- package/src/settings/{collections-editor.js → content-types-editor.js} +48 -48
- package/src/settings/schema-field-ui.js +1 -1
- package/src/state.js +10 -2
- package/src/store.js +9 -0
- package/src/studio.js +67 -811
- package/src/utils/studio-utils.js +5 -5
- package/src/view.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,35 +30,35 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
34
|
-
"@spectrum-web-components/accordion": "^1.12.
|
|
35
|
-
"@spectrum-web-components/action-bar": "1.12.
|
|
36
|
-
"@spectrum-web-components/action-button": "^1.12.
|
|
37
|
-
"@spectrum-web-components/action-group": "^1.12.
|
|
38
|
-
"@spectrum-web-components/checkbox": "^1.12.
|
|
39
|
-
"@spectrum-web-components/color-area": "^1.12.
|
|
40
|
-
"@spectrum-web-components/color-handle": "^1.12.
|
|
41
|
-
"@spectrum-web-components/color-slider": "^1.12.
|
|
42
|
-
"@spectrum-web-components/combobox": "^1.12.
|
|
43
|
-
"@spectrum-web-components/dialog": "^1.12.
|
|
44
|
-
"@spectrum-web-components/divider": "^1.12.
|
|
45
|
-
"@spectrum-web-components/icon": "^1.12.
|
|
46
|
-
"@spectrum-web-components/icons-workflow": "^1.12.
|
|
47
|
-
"@spectrum-web-components/menu": "^1.12.
|
|
48
|
-
"@spectrum-web-components/number-field": "^1.12.
|
|
49
|
-
"@spectrum-web-components/overlay": "^1.12.
|
|
33
|
+
"@jxsuite/runtime": "^0.10.1",
|
|
34
|
+
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
|
+
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
|
+
"@spectrum-web-components/action-button": "^1.12.1",
|
|
37
|
+
"@spectrum-web-components/action-group": "^1.12.1",
|
|
38
|
+
"@spectrum-web-components/checkbox": "^1.12.1",
|
|
39
|
+
"@spectrum-web-components/color-area": "^1.12.1",
|
|
40
|
+
"@spectrum-web-components/color-handle": "^1.12.1",
|
|
41
|
+
"@spectrum-web-components/color-slider": "^1.12.1",
|
|
42
|
+
"@spectrum-web-components/combobox": "^1.12.1",
|
|
43
|
+
"@spectrum-web-components/dialog": "^1.12.1",
|
|
44
|
+
"@spectrum-web-components/divider": "^1.12.1",
|
|
45
|
+
"@spectrum-web-components/icon": "^1.12.1",
|
|
46
|
+
"@spectrum-web-components/icons-workflow": "^1.12.1",
|
|
47
|
+
"@spectrum-web-components/menu": "^1.12.1",
|
|
48
|
+
"@spectrum-web-components/number-field": "^1.12.1",
|
|
49
|
+
"@spectrum-web-components/overlay": "^1.12.1",
|
|
50
50
|
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
51
|
-
"@spectrum-web-components/picker": "^1.12.
|
|
52
|
-
"@spectrum-web-components/picker-button": "^1.12.
|
|
53
|
-
"@spectrum-web-components/popover": "^1.12.
|
|
54
|
-
"@spectrum-web-components/search": "^1.12.
|
|
55
|
-
"@spectrum-web-components/swatch": "^1.12.
|
|
56
|
-
"@spectrum-web-components/switch": "^1.12.
|
|
57
|
-
"@spectrum-web-components/table": "^1.12.
|
|
58
|
-
"@spectrum-web-components/tabs": "^1.12.
|
|
59
|
-
"@spectrum-web-components/textfield": "^1.12.
|
|
60
|
-
"@spectrum-web-components/theme": "^1.12.
|
|
61
|
-
"@spectrum-web-components/tooltip": "^1.12.
|
|
51
|
+
"@spectrum-web-components/picker": "^1.12.1",
|
|
52
|
+
"@spectrum-web-components/picker-button": "^1.12.1",
|
|
53
|
+
"@spectrum-web-components/popover": "^1.12.1",
|
|
54
|
+
"@spectrum-web-components/search": "^1.12.1",
|
|
55
|
+
"@spectrum-web-components/swatch": "^1.12.1",
|
|
56
|
+
"@spectrum-web-components/switch": "^1.12.1",
|
|
57
|
+
"@spectrum-web-components/table": "^1.12.1",
|
|
58
|
+
"@spectrum-web-components/tabs": "^1.12.1",
|
|
59
|
+
"@spectrum-web-components/textfield": "^1.12.1",
|
|
60
|
+
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
|
+
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
62
62
|
"lit-html": "^3.3.3",
|
|
63
63
|
"monaco-editor": "^0.55.1",
|
|
64
64
|
"remark-directive": "^4.0.0",
|
package/src/browse/browse.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Displays pages, layouts, components, content, and media in a filterable table grid. Fills the
|
|
5
5
|
* center canvas area as a parallel state to Edit/Design/Preview/Code/Settings. Includes a "New +"
|
|
6
|
-
* button with type-aware entity creation (including
|
|
6
|
+
* button with type-aware entity creation (including content types from project.json).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { html, render as litRender } from "lit-html";
|
|
@@ -153,14 +153,14 @@ const ENTITY_TYPES = [
|
|
|
153
153
|
];
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
|
-
* Build frontmatter YAML from a
|
|
156
|
+
* Build frontmatter YAML from a content type's schema properties.
|
|
157
157
|
*
|
|
158
|
-
* @param {string}
|
|
158
|
+
* @param {string} contentTypeName
|
|
159
159
|
* @returns {string}
|
|
160
160
|
*/
|
|
161
|
-
function buildFrontmatterYaml(
|
|
161
|
+
function buildFrontmatterYaml(contentTypeName) {
|
|
162
162
|
const config = projectState?.projectConfig;
|
|
163
|
-
const col = config?.
|
|
163
|
+
const col = config?.contentTypes?.[contentTypeName];
|
|
164
164
|
if (!col?.schema?.properties) return "title: Untitled\n";
|
|
165
165
|
|
|
166
166
|
let yaml = "";
|
|
@@ -172,22 +172,22 @@ function buildFrontmatterYaml(collectionName) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Get
|
|
175
|
+
* Get content-type-derived entity types from project config.
|
|
176
176
|
*
|
|
177
|
-
* @returns {{ key: string; label: string; dir: string; ext: string;
|
|
177
|
+
* @returns {{ key: string; label: string; dir: string; ext: string; contentTypeName: string }[]}
|
|
178
178
|
*/
|
|
179
|
-
function
|
|
179
|
+
function getContentTypeTypes() {
|
|
180
180
|
const config = projectState?.projectConfig;
|
|
181
|
-
if (!config?.
|
|
182
|
-
return Object.entries(config.
|
|
181
|
+
if (!config?.contentTypes) return [];
|
|
182
|
+
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
183
183
|
const d = /** @type {any} */ (def);
|
|
184
184
|
const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
|
|
185
185
|
return {
|
|
186
|
-
key: `
|
|
186
|
+
key: `contentType:${name}`,
|
|
187
187
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
188
188
|
dir,
|
|
189
189
|
ext: ".md",
|
|
190
|
-
|
|
190
|
+
contentTypeName: name,
|
|
191
191
|
};
|
|
192
192
|
});
|
|
193
193
|
}
|
|
@@ -200,9 +200,9 @@ function getCollectionTypes() {
|
|
|
200
200
|
* @param {{ openFile: (path: string) => void }} ctx
|
|
201
201
|
*/
|
|
202
202
|
async function handleNewEntity(typeKey, container, ctx) {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const allTypes = [...ENTITY_TYPES, ...
|
|
203
|
+
const isContentType = typeKey.startsWith("contentType:");
|
|
204
|
+
const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
|
|
205
|
+
const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
|
|
206
206
|
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
207
207
|
if (!typeInfo) return;
|
|
208
208
|
|
|
@@ -217,7 +217,9 @@ async function handleNewEntity(typeKey, container, ctx) {
|
|
|
217
217
|
|
|
218
218
|
let content;
|
|
219
219
|
if (typeInfo.ext === ".md") {
|
|
220
|
-
const frontmatter =
|
|
220
|
+
const frontmatter = contentTypeName
|
|
221
|
+
? buildFrontmatterYaml(contentTypeName)
|
|
222
|
+
: "title: Untitled\n";
|
|
221
223
|
content = `---\n${frontmatter}---\n\n`;
|
|
222
224
|
} else {
|
|
223
225
|
content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
|
|
@@ -278,7 +280,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
278
280
|
|
|
279
281
|
const files = filteredFiles();
|
|
280
282
|
|
|
281
|
-
const
|
|
283
|
+
const contentTypeTypes = getContentTypeTypes();
|
|
282
284
|
|
|
283
285
|
const filterBar = html`
|
|
284
286
|
<div class="browse-filter-bar">
|
|
@@ -317,8 +319,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
317
319
|
@change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
|
|
318
320
|
>
|
|
319
321
|
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
320
|
-
${
|
|
321
|
-
? html`<sp-menu-divider></sp-menu-divider> ${
|
|
322
|
+
${contentTypeTypes.length
|
|
323
|
+
? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
|
|
322
324
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
323
325
|
)}`
|
|
324
326
|
: ""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas helpers — extracted from studio.js (Phase 4n). Shared query/utility functions used by
|
|
3
|
+
* multiple canvas-related modules: element lookup, zoom, panel resolution, inline bubbling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getState,
|
|
8
|
+
canvasPanels,
|
|
9
|
+
elToPath,
|
|
10
|
+
pathsEqual,
|
|
11
|
+
getNodeAtPath,
|
|
12
|
+
parentElementPath,
|
|
13
|
+
} from "../store.js";
|
|
14
|
+
import { isInlineInContext } from "../editor/inline-edit.js";
|
|
15
|
+
|
|
16
|
+
/** @type {any} */
|
|
17
|
+
let _ctx = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the canvas helpers module.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ getCanvasMode: () => string }} ctx
|
|
23
|
+
*/
|
|
24
|
+
export function initCanvasHelpers(ctx) {
|
|
25
|
+
_ctx = ctx;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Effective zoom scale — always 1 in edit (content) mode, S.ui.zoom otherwise. */
|
|
29
|
+
export function effectiveZoom() {
|
|
30
|
+
return _ctx.getCanvasMode() === "edit" ? 1 : getState().ui.zoom;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Return the active canvas panel based on the current activeMedia setting. */
|
|
34
|
+
export function getActivePanel() {
|
|
35
|
+
if (canvasPanels.length === 0) return null;
|
|
36
|
+
if (canvasPanels.length === 1) return canvasPanels[0];
|
|
37
|
+
const S = getState();
|
|
38
|
+
for (const p of canvasPanels) {
|
|
39
|
+
if (S.ui.activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
|
|
40
|
+
if (p.mediaName === S.ui.activeMedia) return p;
|
|
41
|
+
}
|
|
42
|
+
return canvasPanels[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
|
|
47
|
+
* ancestor. Returns the original path if already non-inline.
|
|
48
|
+
*
|
|
49
|
+
* @param {any} doc
|
|
50
|
+
* @param {any} path
|
|
51
|
+
*/
|
|
52
|
+
export function bubbleInlinePath(doc, path) {
|
|
53
|
+
let currentPath = path;
|
|
54
|
+
while (currentPath.length >= 2) {
|
|
55
|
+
const node = getNodeAtPath(doc, currentPath);
|
|
56
|
+
const pPath = parentElementPath(currentPath);
|
|
57
|
+
const parentNode = pPath ? getNodeAtPath(doc, pPath) : null;
|
|
58
|
+
if (!node || !parentNode) break;
|
|
59
|
+
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
60
|
+
const parentTag = (parentNode.tagName ?? "div").toLowerCase();
|
|
61
|
+
if (!isInlineInContext(childTag, parentTag)) break;
|
|
62
|
+
currentPath = pPath;
|
|
63
|
+
}
|
|
64
|
+
return currentPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find a canvas DOM element by its document path.
|
|
69
|
+
*
|
|
70
|
+
* @param {any} path
|
|
71
|
+
* @param {any} canvasEl
|
|
72
|
+
*/
|
|
73
|
+
export function findCanvasElement(path, canvasEl) {
|
|
74
|
+
let el = canvasEl.firstElementChild;
|
|
75
|
+
if (!el) return null;
|
|
76
|
+
if (path.length === 0) return el;
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < path.length; i += 2) {
|
|
79
|
+
if (path[i] !== "children" && path[i] !== "cases") return null;
|
|
80
|
+
const idx = path[i + 1];
|
|
81
|
+
if (idx === undefined) {
|
|
82
|
+
el = el.children[0];
|
|
83
|
+
} else if (idx === "map") {
|
|
84
|
+
el = el.children[0]?.children[0];
|
|
85
|
+
} else {
|
|
86
|
+
el = el.children[idx];
|
|
87
|
+
}
|
|
88
|
+
if (!el) break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (el) {
|
|
92
|
+
const elPath = elToPath.get(el);
|
|
93
|
+
if (elPath && pathsEqual(elPath, path)) return el;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
97
|
+
const p = elToPath.get(candidate);
|
|
98
|
+
if (p && pathsEqual(p, path)) return candidate;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build an overlay box descriptor (no DOM creation).
|
|
105
|
+
*
|
|
106
|
+
* @param {any} el
|
|
107
|
+
* @param {any} type
|
|
108
|
+
* @param {any} panel
|
|
109
|
+
*/
|
|
110
|
+
export function overlayBoxDescriptor(el, type, panel) {
|
|
111
|
+
const vpRect = panel.viewport.getBoundingClientRect();
|
|
112
|
+
const elRect = el.getBoundingClientRect();
|
|
113
|
+
const scale = effectiveZoom();
|
|
114
|
+
return {
|
|
115
|
+
cls: `overlay-box overlay-${type}`,
|
|
116
|
+
top: `${(elRect.top - vpRect.top + panel.viewport.scrollTop) / scale}px`,
|
|
117
|
+
left: `${(elRect.left - vpRect.left + panel.viewport.scrollLeft) / scale}px`,
|
|
118
|
+
width: `${elRect.width / scale}px`,
|
|
119
|
+
height: `${elRect.height / scale}px`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas live render — extracted from studio.js (Phase 4p). Async runtime rendering pipeline that
|
|
3
|
+
* builds live canvas DOM using @jxsuite/runtime. Handles element registration, scope building, path
|
|
4
|
+
* mapping ($map remapping), site-level style injection, and $head element injection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { elToPath, stripEventHandlers, projectState } from "../store.js";
|
|
8
|
+
import { view } from "../view.js";
|
|
9
|
+
import { renderNode as runtimeRenderNode, buildScope, defineElement } from "@jxsuite/runtime";
|
|
10
|
+
import {
|
|
11
|
+
getEffectiveElements,
|
|
12
|
+
getEffectiveImports,
|
|
13
|
+
getEffectiveMedia,
|
|
14
|
+
getEffectiveHead,
|
|
15
|
+
} from "../site-context.js";
|
|
16
|
+
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
17
|
+
import { prepareForEditMode } from "../utils/edit-display.js";
|
|
18
|
+
import { getActiveElement } from "../editor/inline-edit.js";
|
|
19
|
+
|
|
20
|
+
/** @type {any} */
|
|
21
|
+
let _ctx = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the canvas live render module.
|
|
25
|
+
*
|
|
26
|
+
* @param {{
|
|
27
|
+
* getState: () => any;
|
|
28
|
+
* getCanvasMode: () => string;
|
|
29
|
+
* }} ctx
|
|
30
|
+
*/
|
|
31
|
+
export function initCanvasLiveRender(ctx) {
|
|
32
|
+
_ctx = ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Render a Jx document into a canvas element using the real runtime. Populates elToPath for each
|
|
37
|
+
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
38
|
+
* failure.
|
|
39
|
+
*
|
|
40
|
+
* @param {number} gen - Render generation for staleness detection
|
|
41
|
+
* @param {any} doc
|
|
42
|
+
* @param {any} canvasEl
|
|
43
|
+
*/
|
|
44
|
+
export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
45
|
+
const S = _ctx.getState();
|
|
46
|
+
const canvasMode = _ctx.getCanvasMode();
|
|
47
|
+
|
|
48
|
+
const renderDoc =
|
|
49
|
+
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
50
|
+
|
|
51
|
+
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
52
|
+
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
53
|
+
const mapParentPaths = new Set();
|
|
54
|
+
if (canvasMode === "design" || canvasMode === "edit") {
|
|
55
|
+
(function findMapParents(/** @type {any} */ node, /** @type {any[]} */ path) {
|
|
56
|
+
if (!node || typeof node !== "object") return;
|
|
57
|
+
if (
|
|
58
|
+
node.children &&
|
|
59
|
+
typeof node.children === "object" &&
|
|
60
|
+
node.children.$prototype === "Array"
|
|
61
|
+
) {
|
|
62
|
+
mapParentPaths.add(path.join("/"));
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(node.children)) {
|
|
65
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
66
|
+
findMapParents(node.children[i], [...path, "children", i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (node.$switch && node.cases) {
|
|
70
|
+
for (const [k, v] of Object.entries(node.cases)) {
|
|
71
|
+
findMapParents(v, [...path, "cases", k]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
})(doc, []);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const root = projectState?.projectRoot || "";
|
|
79
|
+
const docPrefix = root ? `${root}/` : "";
|
|
80
|
+
const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
|
|
81
|
+
|
|
82
|
+
// Register custom elements so the runtime can render them
|
|
83
|
+
let effectiveElements = getEffectiveElements(renderDoc.$elements);
|
|
84
|
+
|
|
85
|
+
// In content mode (markdown), auto-discover components for directive-based
|
|
86
|
+
// custom elements that have no explicit $elements registration.
|
|
87
|
+
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
88
|
+
const existingRefs = new Set(
|
|
89
|
+
effectiveElements.map((/** @type {any} */ e) => (typeof e === "string" ? e : e?.$ref)),
|
|
90
|
+
);
|
|
91
|
+
/** @param {any} node */
|
|
92
|
+
const collectTags = (node) => {
|
|
93
|
+
/** @type {Set<string>} */
|
|
94
|
+
const tags = new Set();
|
|
95
|
+
if (!node || typeof node !== "object") return tags;
|
|
96
|
+
if (node.tagName) tags.add(node.tagName);
|
|
97
|
+
if (Array.isArray(node.children)) {
|
|
98
|
+
for (const child of node.children) {
|
|
99
|
+
for (const t of collectTags(child)) tags.add(t);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return tags;
|
|
103
|
+
};
|
|
104
|
+
for (const tag of collectTags(renderDoc)) {
|
|
105
|
+
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
106
|
+
if (comp && comp.source !== "npm") {
|
|
107
|
+
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
108
|
+
if (!existingRefs.has(relPath)) {
|
|
109
|
+
effectiveElements.push({ $ref: relPath });
|
|
110
|
+
existingRefs.add(relPath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (effectiveElements.length) {
|
|
117
|
+
renderDoc.$elements = effectiveElements;
|
|
118
|
+
for (const entry of effectiveElements) {
|
|
119
|
+
if (typeof entry === "string") {
|
|
120
|
+
try {
|
|
121
|
+
const specifier =
|
|
122
|
+
entry.startsWith("/") || entry.startsWith(".")
|
|
123
|
+
? entry
|
|
124
|
+
: `/${projectState?.projectRoot || ""}/node_modules/${entry}`.replace(/\/+/g, "/");
|
|
125
|
+
await import(specifier);
|
|
126
|
+
} catch (/** @type {any} */ e) {
|
|
127
|
+
console.warn("Studio: failed to import package", entry, e);
|
|
128
|
+
}
|
|
129
|
+
} else if (entry?.$ref) {
|
|
130
|
+
let href;
|
|
131
|
+
try {
|
|
132
|
+
href = new URL(entry.$ref, docBase).href;
|
|
133
|
+
} catch (/** @type {any} */ urlErr) {
|
|
134
|
+
console.warn("Studio: invalid element URL", { ref: entry.$ref, docBase }, urlErr);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await defineElement(href);
|
|
139
|
+
} catch (/** @type {any} */ e) {
|
|
140
|
+
console.warn("Studio: failed to register element", entry.$ref, e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Bail out if a newer render started while we were importing elements
|
|
147
|
+
if (gen !== view.renderGeneration) return null;
|
|
148
|
+
|
|
149
|
+
// Inject site-level imports so buildScope can resolve $prototype names
|
|
150
|
+
renderDoc.imports = getEffectiveImports(renderDoc.imports);
|
|
151
|
+
|
|
152
|
+
// Apply project-level styles mirroring the compiler convention:
|
|
153
|
+
// viewport ≈ :root → CSS custom properties (they inherit down)
|
|
154
|
+
// canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
|
|
155
|
+
// This ensures project font-family, color, etc. override the
|
|
156
|
+
// content-mode fallback typography rules in the stylesheet.
|
|
157
|
+
// In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
|
|
158
|
+
const viewport = canvasEl.closest(".canvas-panel-viewport");
|
|
159
|
+
const editSurface = canvasMode === "edit" ? canvasEl.closest(".content-edit-canvas") : null;
|
|
160
|
+
const siteStyle = projectState?.projectConfig?.style;
|
|
161
|
+
if (viewport) {
|
|
162
|
+
viewport.style.cssText = "";
|
|
163
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
164
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
165
|
+
if (k.startsWith("--")) {
|
|
166
|
+
viewport.style.setProperty(k, String(v));
|
|
167
|
+
} else {
|
|
168
|
+
/** @type {any} */ (viewport.style)[k] = v;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (editSurface) {
|
|
174
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
175
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
176
|
+
if (k.startsWith("--")) {
|
|
177
|
+
/** @type {any} */ (editSurface).style.setProperty(k, String(v));
|
|
178
|
+
} else {
|
|
179
|
+
/** @type {any} */ (editSurface.style)[k] = v;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
185
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
186
|
+
if (!k.startsWith("--")) {
|
|
187
|
+
/** @type {any} */ (canvasEl.style)[k] = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Inject site-level $media so runtime can resolve media queries in styles
|
|
193
|
+
renderDoc.$media = getEffectiveMedia(renderDoc.$media);
|
|
194
|
+
|
|
195
|
+
// Inject $head elements (link/meta/script) into document.head
|
|
196
|
+
const effectiveHead = getEffectiveHead(renderDoc.$head);
|
|
197
|
+
if (effectiveHead.length) {
|
|
198
|
+
for (const entry of effectiveHead) {
|
|
199
|
+
if (!entry?.tagName) continue;
|
|
200
|
+
const tag = entry.tagName.toLowerCase();
|
|
201
|
+
const attrs = { ...entry.attributes };
|
|
202
|
+
const headRoot = projectState?.projectRoot || "";
|
|
203
|
+
for (const key of ["href", "src"]) {
|
|
204
|
+
if (
|
|
205
|
+
attrs[key] &&
|
|
206
|
+
!attrs[key].startsWith("/") &&
|
|
207
|
+
!attrs[key].startsWith(".") &&
|
|
208
|
+
!attrs[key].startsWith("http")
|
|
209
|
+
) {
|
|
210
|
+
attrs[key] = `/${headRoot}/node_modules/${attrs[key]}`.replace(/\/+/g, "/");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const selector = `${tag}${attrs.href ? `[href="${attrs.href}"]` : ""}${attrs.src ? `[src="${attrs.src}"]` : ""}`;
|
|
214
|
+
if (selector !== tag && document.head.querySelector(selector)) continue;
|
|
215
|
+
const el = document.createElement(tag);
|
|
216
|
+
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, /** @type {string} */ (v));
|
|
217
|
+
if (entry.textContent) el.textContent = entry.textContent;
|
|
218
|
+
document.head.appendChild(el);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
223
|
+
// Bail out if a newer render started while buildScope was running
|
|
224
|
+
if (gen !== view.renderGeneration) return null;
|
|
225
|
+
const el = /** @type {HTMLElement} */ (
|
|
226
|
+
runtimeRenderNode(renderDoc, $defs, {
|
|
227
|
+
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
228
|
+
// Remap $map paths: wrapper and template children → real document paths
|
|
229
|
+
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
230
|
+
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
231
|
+
let mappedPath = path;
|
|
232
|
+
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
233
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
234
|
+
if (path[i] === "children" && path[i + 1] === 0) {
|
|
235
|
+
const parentKey = path.slice(0, i).join("/");
|
|
236
|
+
if (mapParentPaths.has(parentKey)) {
|
|
237
|
+
if (path.length === i + 2) {
|
|
238
|
+
// Wrapper div itself → $map container path
|
|
239
|
+
mappedPath = path.slice(0, i + 1);
|
|
240
|
+
} else if (
|
|
241
|
+
path.length >= i + 4 &&
|
|
242
|
+
path[i + 2] === "children" &&
|
|
243
|
+
path[i + 3] === 0
|
|
244
|
+
) {
|
|
245
|
+
// Template or its descendants → children/map/...rest
|
|
246
|
+
mappedPath = [...path.slice(0, i), "children", "map", ...path.slice(i + 4)];
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
elToPath.set(el, mappedPath);
|
|
254
|
+
},
|
|
255
|
+
_path: [],
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
if (canvasMode === "design" || canvasMode === "edit") {
|
|
259
|
+
// Disable pointer events on all rendered elements for edit mode
|
|
260
|
+
el.style.pointerEvents = "none";
|
|
261
|
+
for (const child of el.querySelectorAll("*")) {
|
|
262
|
+
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Clear and append atomically — ensures the canvas is never left empty if a
|
|
266
|
+
// newer render starts and this one would have bailed after clearing.
|
|
267
|
+
canvasEl.innerHTML = "";
|
|
268
|
+
if (S.mode === "content") {
|
|
269
|
+
canvasEl.setAttribute("data-content-mode", "");
|
|
270
|
+
} else {
|
|
271
|
+
canvasEl.removeAttribute("data-content-mode");
|
|
272
|
+
}
|
|
273
|
+
canvasEl.appendChild(el);
|
|
274
|
+
if (canvasMode === "design" || canvasMode === "edit") {
|
|
275
|
+
// Custom element connectedCallbacks render children asynchronously —
|
|
276
|
+
// sweep again after they've had a chance to run
|
|
277
|
+
requestAnimationFrame(() => {
|
|
278
|
+
const editingEl = getActiveElement();
|
|
279
|
+
for (const child of canvasEl.querySelectorAll("*")) {
|
|
280
|
+
// Preserve pointer-events on the actively-edited element
|
|
281
|
+
if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
|
|
282
|
+
if (editingEl && child === editingEl) continue;
|
|
283
|
+
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return $defs;
|
|
288
|
+
} catch (/** @type {any} */ err) {
|
|
289
|
+
console.warn("renderCanvasLive failed:", err.message, err);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|