@jxsuite/studio 0.10.2 → 0.13.0

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.
Files changed (53) hide show
  1. package/README.md +82 -0
  2. package/dist/studio.js +5114 -3424
  3. package/dist/studio.js.map +65 -49
  4. package/package.json +6 -3
  5. package/src/browse/browse.js +27 -3
  6. package/src/canvas/canvas-diff.js +184 -0
  7. package/src/canvas/canvas-helpers.js +10 -14
  8. package/src/canvas/canvas-live-render.js +136 -17
  9. package/src/canvas/canvas-render.js +154 -21
  10. package/src/canvas/canvas-utils.js +21 -23
  11. package/src/editor/component-inline-edit.js +54 -41
  12. package/src/editor/content-inline-edit.js +46 -47
  13. package/src/editor/context-menu.js +63 -39
  14. package/src/editor/convert-to-component.js +11 -14
  15. package/src/editor/insertion-helper.js +8 -10
  16. package/src/editor/shortcuts.js +69 -39
  17. package/src/files/components.js +15 -4
  18. package/src/files/files.js +72 -24
  19. package/src/panels/activity-bar.js +29 -7
  20. package/src/panels/block-action-bar.js +104 -80
  21. package/src/panels/canvas-dnd.js +132 -50
  22. package/src/panels/dnd.js +32 -28
  23. package/src/panels/editors.js +7 -14
  24. package/src/panels/elements-panel.js +9 -3
  25. package/src/panels/events-panel.js +44 -39
  26. package/src/panels/git-panel.js +45 -3
  27. package/src/panels/layers-panel.js +16 -11
  28. package/src/panels/left-panel.js +108 -43
  29. package/src/panels/overlays.js +97 -35
  30. package/src/panels/panel-events.js +18 -11
  31. package/src/panels/properties-panel.js +467 -98
  32. package/src/panels/right-panel.js +85 -37
  33. package/src/panels/shared.js +0 -22
  34. package/src/panels/signals-panel.js +125 -54
  35. package/src/panels/statusbar.js +23 -8
  36. package/src/panels/style-inputs.js +4 -2
  37. package/src/panels/style-panel.js +128 -105
  38. package/src/panels/stylebook-panel.js +42 -17
  39. package/src/panels/tab-strip.js +124 -0
  40. package/src/panels/toolbar.js +39 -9
  41. package/src/reactivity.js +28 -0
  42. package/src/settings/content-types-editor.js +78 -8
  43. package/src/settings/defs-editor.js +56 -7
  44. package/src/settings/schema-field-ui.js +99 -11
  45. package/src/site-context.js +105 -0
  46. package/src/state.js +0 -456
  47. package/src/store.js +105 -124
  48. package/src/studio.js +112 -121
  49. package/src/tabs/tab.js +153 -0
  50. package/src/tabs/transact.js +406 -0
  51. package/src/ui/spectrum.js +2 -0
  52. package/src/view.js +3 -0
  53. package/src/workspace/workspace.js +89 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.10.2",
3
+ "version": "0.13.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
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.10.1",
33
+ "@jxsuite/runtime": "^0.12.0",
34
34
  "@spectrum-web-components/accordion": "^1.12.1",
35
35
  "@spectrum-web-components/action-bar": "1.12.1",
36
36
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -58,7 +58,9 @@
58
58
  "@spectrum-web-components/tabs": "^1.12.1",
59
59
  "@spectrum-web-components/textfield": "^1.12.1",
60
60
  "@spectrum-web-components/theme": "^1.12.1",
61
+ "@spectrum-web-components/toast": "^1.12.1",
61
62
  "@spectrum-web-components/tooltip": "^1.12.1",
63
+ "@vue/reactivity": "3.5.34",
62
64
  "lit-html": "^3.3.3",
63
65
  "monaco-editor": "^0.55.1",
64
66
  "remark-directive": "^4.0.0",
@@ -74,5 +76,6 @@
74
76
  "@webref/css": "^8.5.6",
75
77
  "@webref/elements": "^2.7.1",
76
78
  "@webref/idl": "^3.78.0"
77
- }
79
+ },
80
+ "//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
78
81
  }
@@ -95,11 +95,14 @@ async function collectFiles(dir, platform) {
95
95
  results.push(...sub);
96
96
  } else {
97
97
  const ext = extOf(entry.name);
98
+ const category = categoryFor(entry.path, ext);
99
+ const type =
100
+ category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
98
101
  results.push({
99
102
  name: entry.name,
100
103
  path: entry.path,
101
- type: ext || "file",
102
- category: categoryFor(entry.path, ext),
104
+ type,
105
+ category,
103
106
  ext,
104
107
  });
105
108
  }
@@ -110,6 +113,27 @@ async function collectFiles(dir, platform) {
110
113
  return results;
111
114
  }
112
115
 
116
+ /**
117
+ * Match a file path against project contentTypes source globs to find its content type name.
118
+ * Returns the content type name (capitalized) or null if no match.
119
+ *
120
+ * @param {string} filePath
121
+ * @returns {string | null}
122
+ */
123
+ function contentTypeFor(filePath) {
124
+ const config = projectState?.projectConfig;
125
+ if (!config?.contentTypes) return null;
126
+ for (const [name, def] of Object.entries(config.contentTypes)) {
127
+ const d = /** @type {any} */ (def);
128
+ if (!d.source) continue;
129
+ const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
130
+ if (filePath.startsWith(prefix + "/") || filePath === prefix) {
131
+ return name.charAt(0).toUpperCase() + name.slice(1);
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
113
137
  // ─── Data loading ────────────────────────────────────────────────────────────
114
138
 
115
139
  async function loadFiles() {
@@ -376,7 +400,7 @@ export async function renderBrowse(container, ctx) {
376
400
  >
377
401
  <sp-table-cell class="browse-name-cell">${f.name}</sp-table-cell>
378
402
  <sp-table-cell>${f.category}</sp-table-cell>
379
- <sp-table-cell>${f.ext || "—"}</sp-table-cell>
403
+ <sp-table-cell>${f.type}</sp-table-cell>
380
404
  <sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
381
405
  </sp-table-row>
382
406
  `,
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Canvas Diff — computes visual diff between original and current Jx documents.
3
+ *
4
+ * Compares two document trees recursively, marking nodes as added/removed/modified, and propagates
5
+ * "modified" status up the tree for structural changes (children added/removed/reordered).
6
+ */
7
+
8
+ /**
9
+ * @typedef {Record<string, any>} JxNode
10
+ *
11
+ * @typedef {"added" | "removed" | "modified"} DiffStatus
12
+ *
13
+ * @typedef {{ byPath: Map<string, DiffStatus>; allPaths: Set<string> }} DiffResult
14
+ */
15
+
16
+ /**
17
+ * Deep equality check for two values. Ignores function and ref properties.
18
+ *
19
+ * @param {any} a
20
+ * @param {any} b
21
+ * @returns {boolean}
22
+ */
23
+ function valuesEqual(a, b) {
24
+ if (a === b) return true;
25
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
26
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
27
+
28
+ if (Array.isArray(a)) {
29
+ if (a.length !== b.length) return false;
30
+ return a.every((/** @type {any} */ v, /** @type {number} */ i) => valuesEqual(v, b[i]));
31
+ }
32
+
33
+ const keysA = Object.keys(a).filter(
34
+ (/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
35
+ );
36
+ const keysB = Object.keys(b).filter(
37
+ (/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
38
+ );
39
+
40
+ if (keysA.length !== keysB.length) return false;
41
+ if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
42
+
43
+ return keysA.every((/** @type {string} */ k) => valuesEqual(a[k], b[k]));
44
+ }
45
+
46
+ /**
47
+ * Filter element children from a node's children array.
48
+ *
49
+ * @param {any} node
50
+ * @returns {any[]}
51
+ */
52
+ function elementChildren(node) {
53
+ if (!node?.children || !Array.isArray(node.children)) return [];
54
+ return node.children.filter((/** @type {any} */ c) => c != null && typeof c === "object");
55
+ }
56
+
57
+ /**
58
+ * Mark an entire subtree with a diff status.
59
+ *
60
+ * @param {any} node
61
+ * @param {DiffStatus} status
62
+ * @param {string} path
63
+ * @param {Map<string, DiffStatus>} diffMap
64
+ * @param {Set<string>} allPaths
65
+ */
66
+ function markRecursive(node, status, path, diffMap, allPaths) {
67
+ allPaths.add(path);
68
+ diffMap.set(path, status);
69
+ const children = elementChildren(node);
70
+ for (let i = 0; i < children.length; i++) {
71
+ const childPath = `${path}/children/${i}`;
72
+ markRecursive(children[i], status, childPath, diffMap, allPaths);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Compute structural diff between original and current documents.
78
+ *
79
+ * @param {any} originalDoc - Original document (from git)
80
+ * @param {any} currentDoc - Current document (in memory/on disk)
81
+ * @returns {DiffResult}
82
+ */
83
+ export function computeDocumentDiff(originalDoc, currentDoc) {
84
+ /** @type {Map<string, DiffStatus>} */
85
+ const diffMap = new Map();
86
+ /** @type {Set<string>} */
87
+ const allPaths = new Set();
88
+
89
+ /**
90
+ * Walk both trees in parallel and mark differences.
91
+ *
92
+ * @param {any} origNode
93
+ * @param {any} currNode
94
+ * @param {string} path
95
+ */
96
+ const walk = (origNode, currNode, path = "") => {
97
+ const pathKey = path || "/";
98
+ allPaths.add(pathKey);
99
+
100
+ // Both exist but differ
101
+ if (origNode != null && currNode != null) {
102
+ if (!valuesEqual(origNode, currNode)) {
103
+ diffMap.set(pathKey, "modified");
104
+ }
105
+
106
+ // Walk children
107
+ if (origNode.children && currNode.children) {
108
+ const origChildren = elementChildren(origNode);
109
+ const currChildren = elementChildren(currNode);
110
+
111
+ for (let i = 0; i < Math.max(origChildren.length, currChildren.length); i++) {
112
+ const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
113
+ walk(origChildren[i], currChildren[i], childPath);
114
+ }
115
+ }
116
+ return;
117
+ }
118
+
119
+ // In current only (added)
120
+ if (currNode != null && origNode == null) {
121
+ diffMap.set(pathKey, "added");
122
+ const children = elementChildren(currNode);
123
+ for (let i = 0; i < children.length; i++) {
124
+ const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
125
+ markRecursive(children[i], "added", childPath, diffMap, allPaths);
126
+ }
127
+ return;
128
+ }
129
+
130
+ // In original only (removed)
131
+ if (origNode != null && currNode == null) {
132
+ diffMap.set(pathKey, "removed");
133
+ const children = elementChildren(origNode);
134
+ for (let i = 0; i < children.length; i++) {
135
+ const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
136
+ markRecursive(children[i], "removed", childPath, diffMap, allPaths);
137
+ }
138
+ return;
139
+ }
140
+ };
141
+
142
+ // Start comparison
143
+ walk(originalDoc, currentDoc, "");
144
+
145
+ // Propagate "modified" status to parents of added/removed children
146
+ const propagateModified = () => {
147
+ let changed = false;
148
+ for (const [path, status] of diffMap.entries()) {
149
+ if ((status === "added" || status === "removed") && path !== "/") {
150
+ const parentPath = path.split("/").slice(0, -2).join("/") || "/";
151
+ if (parentPath !== "/") {
152
+ const parentStatus = diffMap.get(parentPath);
153
+ if (
154
+ parentStatus !== "modified" &&
155
+ parentStatus !== "added" &&
156
+ parentStatus !== "removed"
157
+ ) {
158
+ diffMap.set(parentPath, "modified");
159
+ changed = true;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ return changed;
165
+ };
166
+
167
+ // Propagate upwards until no more changes
168
+ while (propagateModified()) {}
169
+
170
+ return { byPath: diffMap, allPaths };
171
+ }
172
+
173
+ /**
174
+ * Clear diff highlighting from all elements in a canvas.
175
+ *
176
+ * @param {HTMLElement} canvas
177
+ */
178
+ export function clearDiffHighlight(canvas) {
179
+ canvas
180
+ .querySelectorAll(".element-diff-added, .element-diff-removed, .element-diff-modified")
181
+ .forEach((/** @type {Element} */ el) => {
182
+ el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
183
+ });
184
+ }
@@ -3,14 +3,8 @@
3
3
  * multiple canvas-related modules: element lookup, zoom, panel resolution, inline bubbling.
4
4
  */
5
5
 
6
- import {
7
- getState,
8
- canvasPanels,
9
- elToPath,
10
- pathsEqual,
11
- getNodeAtPath,
12
- parentElementPath,
13
- } from "../store.js";
6
+ import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath } from "../store.js";
7
+ import { activeTab } from "../workspace/workspace.js";
14
8
  import { isInlineInContext } from "../editor/inline-edit.js";
15
9
 
16
10
  /** @type {any} */
@@ -19,25 +13,27 @@ let _ctx = null;
19
13
  /**
20
14
  * Initialize the canvas helpers module.
21
15
  *
22
- * @param {{ getCanvasMode: () => string }} ctx
16
+ * @param {{ getCanvasMode: () => string; getZoom: () => number }} ctx
23
17
  */
24
18
  export function initCanvasHelpers(ctx) {
25
19
  _ctx = ctx;
26
20
  }
27
21
 
28
- /** Effective zoom scale — always 1 in edit (content) mode, S.ui.zoom otherwise. */
22
+ /** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
29
23
  export function effectiveZoom() {
30
- return _ctx.getCanvasMode() === "edit" ? 1 : getState().ui.zoom;
24
+ return _ctx.getCanvasMode() === "edit"
25
+ ? 1
26
+ : (_ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
31
27
  }
32
28
 
33
29
  /** Return the active canvas panel based on the current activeMedia setting. */
34
30
  export function getActivePanel() {
35
31
  if (canvasPanels.length === 0) return null;
36
32
  if (canvasPanels.length === 1) return canvasPanels[0];
37
- const S = getState();
33
+ const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
38
34
  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;
35
+ if (activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
36
+ if (p.mediaName === activeMedia) return p;
41
37
  }
42
38
  return canvasPanels[0];
43
39
  }
@@ -5,13 +5,22 @@
5
5
  */
6
6
 
7
7
  import { elToPath, stripEventHandlers, projectState } from "../store.js";
8
+ import { activeTab } from "../workspace/workspace.js";
8
9
  import { view } from "../view.js";
9
- import { renderNode as runtimeRenderNode, buildScope, defineElement } from "@jxsuite/runtime";
10
+ import {
11
+ renderNode as runtimeRenderNode,
12
+ buildScope,
13
+ defineElement,
14
+ setSkipServerFunctions,
15
+ } from "@jxsuite/runtime";
10
16
  import {
11
17
  getEffectiveElements,
12
18
  getEffectiveImports,
13
19
  getEffectiveMedia,
14
20
  getEffectiveHead,
21
+ getEffectiveLayoutPath,
22
+ resolveLayoutDoc,
23
+ distributePageIntoLayout,
15
24
  } from "../site-context.js";
16
25
  import { componentRegistry, computeRelativePath } from "../files/components.js";
17
26
  import { prepareForEditMode } from "../utils/edit-display.js";
@@ -20,11 +29,60 @@ import { getActiveElement } from "../editor/inline-edit.js";
20
29
  /** @type {any} */
21
30
  let _ctx = null;
22
31
 
32
+ /** Set of DOM elements that originated from the layout (not page content). */
33
+ export const layoutElements = new WeakSet();
34
+
35
+ /**
36
+ * Walk the merged document tree to find the path prefix where page children were distributed into
37
+ * the layout slot. Returns the path to the container whose children are the page content (first
38
+ * non-$__layout children array).
39
+ *
40
+ * @param {any} node
41
+ * @param {any[]} [path]
42
+ * @returns {any[] | null}
43
+ */
44
+ function findPageContentPrefix(node, path = []) {
45
+ if (!node || typeof node !== "object") return null;
46
+ if (Array.isArray(node.children)) {
47
+ for (let i = 0; i < node.children.length; i++) {
48
+ const child = node.children[i];
49
+ if (child && typeof child === "object" && !child.$__layout) {
50
+ return [...path, "children"];
51
+ }
52
+ }
53
+ for (let i = 0; i < node.children.length; i++) {
54
+ const child = node.children[i];
55
+ if (child && typeof child === "object" && child.$__layout) {
56
+ const found = findPageContentPrefix(child, [...path, "children", i]);
57
+ if (found) return found;
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /** The path of the currently active layout file, or null. */
65
+ export let activeLayoutPath = /** @type {string | null} */ (null);
66
+
67
+ /**
68
+ * Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
69
+ * rendered DOM elements came from the layout vs page content.
70
+ */
71
+ function markLayoutNodes(/** @type {any} */ node) {
72
+ if (!node || typeof node !== "object") return;
73
+ node.$__layout = true;
74
+ if (Array.isArray(node.children)) {
75
+ for (const child of node.children) markLayoutNodes(child);
76
+ }
77
+ if (node.$elements) {
78
+ for (const el of node.$elements) markLayoutNodes(el);
79
+ }
80
+ }
81
+
23
82
  /**
24
83
  * Initialize the canvas live render module.
25
84
  *
26
85
  * @param {{
27
- * getState: () => any;
28
86
  * getCanvasMode: () => string;
29
87
  * }} ctx
30
88
  */
@@ -42,12 +100,50 @@ export function initCanvasLiveRender(ctx) {
42
100
  * @param {any} canvasEl
43
101
  */
44
102
  export async function renderCanvasLive(gen, doc, canvasEl) {
45
- const S = _ctx.getState();
103
+ const tab = activeTab.value;
104
+ const S = { documentPath: tab?.documentPath, mode: tab?.doc.mode, document: tab?.doc.document };
46
105
  const canvasMode = _ctx.getCanvasMode();
47
106
 
48
- const renderDoc =
107
+ // Suppress server function resolution in non-preview modes to avoid
108
+ // failed proxy calls and infinite reactive retries (also covers
109
+ // async custom element connectedCallbacks that run after this function returns)
110
+ setSkipServerFunctions(canvasMode !== "preview");
111
+
112
+ let renderDoc =
49
113
  canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
50
114
 
115
+ // ─── Layout wrapping ────────────────────────────────────────────────────
116
+ // For page documents, resolve the layout and wrap content in the layout shell.
117
+ // Layout-originated nodes are marked with $__layout so we can distinguish them.
118
+ let layoutWrapped = false;
119
+ activeLayoutPath = null;
120
+
121
+ const isPage =
122
+ S.documentPath &&
123
+ projectState?.isSiteProject &&
124
+ (S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
125
+
126
+ /** @type {any[] | null} Path prefix in merged doc where page children live */
127
+ let pageContentPrefix = null;
128
+
129
+ if (isPage) {
130
+ const layoutPath = getEffectiveLayoutPath(doc.$layout);
131
+ if (layoutPath) {
132
+ const layoutDoc = await resolveLayoutDoc(layoutPath);
133
+ if (layoutDoc) {
134
+ if (gen !== view.renderGeneration) return null;
135
+ activeLayoutPath = layoutPath.replace(/^\.\//, "");
136
+ markLayoutNodes(layoutDoc);
137
+ const pageForSlots = canvasMode === "preview" ? structuredClone(doc) : renderDoc;
138
+ const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
139
+ renderDoc =
140
+ canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
141
+ layoutWrapped = true;
142
+ pageContentPrefix = findPageContentPrefix(merged);
143
+ }
144
+ }
145
+ }
146
+
51
147
  // In edit mode, collect paths where $map templates were inlined as children[0]
52
148
  // so we can remap runtime paths (children,0,...) → (children,map,...)
53
149
  const mapParentPaths = new Set();
@@ -224,26 +320,49 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
224
320
  if (gen !== view.renderGeneration) return null;
225
321
  const el = /** @type {HTMLElement} */ (
226
322
  runtimeRenderNode(renderDoc, $defs, {
227
- onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
323
+ onNodeCreated(/** @type {any} */ el, /** @type {any} */ path, /** @type {any} */ def) {
324
+ // Track layout-originated elements — don't store in elToPath to avoid
325
+ // path collisions with remapped page content paths
326
+ if (layoutWrapped && def?.$__layout) {
327
+ layoutElements.add(el);
328
+ if (el.setAttribute) el.setAttribute("data-jx-layout", "");
329
+ return;
330
+ }
331
+
332
+ // Remap layout-wrapped paths: strip the layout prefix so paths are
333
+ // relative to the original page document (which is what S.document holds)
334
+ let mappedPath = path;
335
+ if (layoutWrapped && pageContentPrefix) {
336
+ const pfx = pageContentPrefix;
337
+ if (
338
+ path.length >= pfx.length &&
339
+ pfx.every((/** @type {any} */ seg, /** @type {number} */ i) => path[i] === seg)
340
+ ) {
341
+ mappedPath = ["children", ...path.slice(pfx.length)];
342
+ }
343
+ }
344
+
228
345
  // Remap $map paths: wrapper and template children → real document paths
229
346
  // prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
230
347
  // Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
231
- let mappedPath = path;
232
348
  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("/");
349
+ for (let i = 0; i < mappedPath.length - 1; i++) {
350
+ if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
351
+ const parentKey = mappedPath.slice(0, i).join("/");
236
352
  if (mapParentPaths.has(parentKey)) {
237
- if (path.length === i + 2) {
238
- // Wrapper div itself $map container path
239
- mappedPath = path.slice(0, i + 1);
353
+ if (mappedPath.length === i + 2) {
354
+ mappedPath = mappedPath.slice(0, i + 1);
240
355
  } else if (
241
- path.length >= i + 4 &&
242
- path[i + 2] === "children" &&
243
- path[i + 3] === 0
356
+ mappedPath.length >= i + 4 &&
357
+ mappedPath[i + 2] === "children" &&
358
+ mappedPath[i + 3] === 0
244
359
  ) {
245
- // Template or its descendants → children/map/...rest
246
- mappedPath = [...path.slice(0, i), "children", "map", ...path.slice(i + 4)];
360
+ mappedPath = [
361
+ ...mappedPath.slice(0, i),
362
+ "children",
363
+ "map",
364
+ ...mappedPath.slice(i + 4),
365
+ ];
247
366
  }
248
367
  break;
249
368
  }