@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.9.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.7.0",
34
- "@spectrum-web-components/accordion": "^1.12.0",
35
- "@spectrum-web-components/action-bar": "1.12.0",
36
- "@spectrum-web-components/action-button": "^1.12.0",
37
- "@spectrum-web-components/action-group": "^1.12.0",
38
- "@spectrum-web-components/checkbox": "^1.12.0",
39
- "@spectrum-web-components/color-area": "^1.12.0",
40
- "@spectrum-web-components/color-handle": "^1.12.0",
41
- "@spectrum-web-components/color-slider": "^1.12.0",
42
- "@spectrum-web-components/combobox": "^1.12.0",
43
- "@spectrum-web-components/dialog": "^1.12.0",
44
- "@spectrum-web-components/divider": "^1.12.0",
45
- "@spectrum-web-components/icon": "^1.12.0",
46
- "@spectrum-web-components/icons-workflow": "^1.12.0",
47
- "@spectrum-web-components/menu": "^1.12.0",
48
- "@spectrum-web-components/number-field": "^1.12.0",
49
- "@spectrum-web-components/overlay": "^1.12.0",
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.0",
52
- "@spectrum-web-components/picker-button": "^1.12.0",
53
- "@spectrum-web-components/popover": "^1.12.0",
54
- "@spectrum-web-components/search": "^1.12.0",
55
- "@spectrum-web-components/swatch": "^1.12.0",
56
- "@spectrum-web-components/switch": "^1.12.0",
57
- "@spectrum-web-components/table": "^1.12.0",
58
- "@spectrum-web-components/tabs": "^1.12.0",
59
- "@spectrum-web-components/textfield": "^1.12.0",
60
- "@spectrum-web-components/theme": "^1.12.0",
61
- "@spectrum-web-components/tooltip": "^1.12.0",
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",
@@ -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 collections from project.json).
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 collection's schema properties.
156
+ * Build frontmatter YAML from a content type's schema properties.
157
157
  *
158
- * @param {string} collectionName
158
+ * @param {string} contentTypeName
159
159
  * @returns {string}
160
160
  */
161
- function buildFrontmatterYaml(collectionName) {
161
+ function buildFrontmatterYaml(contentTypeName) {
162
162
  const config = projectState?.projectConfig;
163
- const col = config?.collections?.[collectionName];
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 collection-derived entity types from project config.
175
+ * Get content-type-derived entity types from project config.
176
176
  *
177
- * @returns {{ key: string; label: string; dir: string; ext: string; collectionName: string }[]}
177
+ * @returns {{ key: string; label: string; dir: string; ext: string; contentTypeName: string }[]}
178
178
  */
179
- function getCollectionTypes() {
179
+ function getContentTypeTypes() {
180
180
  const config = projectState?.projectConfig;
181
- if (!config?.collections) return [];
182
- return Object.entries(config.collections).map(([name, def]) => {
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: `collection:${name}`,
186
+ key: `contentType:${name}`,
187
187
  label: name.charAt(0).toUpperCase() + name.slice(1),
188
188
  dir,
189
189
  ext: ".md",
190
- collectionName: name,
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 isCollection = typeKey.startsWith("collection:");
204
- const collectionName = isCollection ? typeKey.slice("collection:".length) : null;
205
- const allTypes = [...ENTITY_TYPES, ...getCollectionTypes()];
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 = collectionName ? buildFrontmatterYaml(collectionName) : "title: Untitled\n";
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 collectionTypes = getCollectionTypes();
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
- ${collectionTypes.length
321
- ? html`<sp-menu-divider></sp-menu-divider> ${collectionTypes.map(
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
+ }