@jxsuite/studio 0.10.2 → 0.11.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.10.2",
3
+ "version": "0.11.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.10.2",
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,6 +58,7 @@
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",
62
63
  "lit-html": "^3.3.3",
63
64
  "monaco-editor": "^0.55.1",
@@ -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
  `,
@@ -6,12 +6,20 @@
6
6
 
7
7
  import { elToPath, stripEventHandlers, projectState } from "../store.js";
8
8
  import { view } from "../view.js";
9
- import { renderNode as runtimeRenderNode, buildScope, defineElement } from "@jxsuite/runtime";
9
+ import {
10
+ renderNode as runtimeRenderNode,
11
+ buildScope,
12
+ defineElement,
13
+ setSkipServerFunctions,
14
+ } from "@jxsuite/runtime";
10
15
  import {
11
16
  getEffectiveElements,
12
17
  getEffectiveImports,
13
18
  getEffectiveMedia,
14
19
  getEffectiveHead,
20
+ getEffectiveLayoutPath,
21
+ resolveLayoutDoc,
22
+ distributePageIntoLayout,
15
23
  } from "../site-context.js";
16
24
  import { componentRegistry, computeRelativePath } from "../files/components.js";
17
25
  import { prepareForEditMode } from "../utils/edit-display.js";
@@ -20,6 +28,56 @@ import { getActiveElement } from "../editor/inline-edit.js";
20
28
  /** @type {any} */
21
29
  let _ctx = null;
22
30
 
31
+ /** Set of DOM elements that originated from the layout (not page content). */
32
+ export const layoutElements = new WeakSet();
33
+
34
+ /**
35
+ * Walk the merged document tree to find the path prefix where page children were distributed into
36
+ * the layout slot. Returns the path to the container whose children are the page content (first
37
+ * non-$__layout children array).
38
+ *
39
+ * @param {any} node
40
+ * @param {any[]} [path]
41
+ * @returns {any[] | null}
42
+ */
43
+ function findPageContentPrefix(node, path = []) {
44
+ if (!node || typeof node !== "object") return null;
45
+ if (Array.isArray(node.children)) {
46
+ for (let i = 0; i < node.children.length; i++) {
47
+ const child = node.children[i];
48
+ if (child && typeof child === "object" && !child.$__layout) {
49
+ return [...path, "children"];
50
+ }
51
+ }
52
+ for (let i = 0; i < node.children.length; i++) {
53
+ const child = node.children[i];
54
+ if (child && typeof child === "object" && child.$__layout) {
55
+ const found = findPageContentPrefix(child, [...path, "children", i]);
56
+ if (found) return found;
57
+ }
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /** The path of the currently active layout file, or null. */
64
+ export let activeLayoutPath = /** @type {string | null} */ (null);
65
+
66
+ /**
67
+ * Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
68
+ * rendered DOM elements came from the layout vs page content.
69
+ */
70
+ function markLayoutNodes(/** @type {any} */ node) {
71
+ if (!node || typeof node !== "object") return;
72
+ node.$__layout = true;
73
+ if (Array.isArray(node.children)) {
74
+ for (const child of node.children) markLayoutNodes(child);
75
+ }
76
+ if (node.$elements) {
77
+ for (const el of node.$elements) markLayoutNodes(el);
78
+ }
79
+ }
80
+
23
81
  /**
24
82
  * Initialize the canvas live render module.
25
83
  *
@@ -45,9 +103,46 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
45
103
  const S = _ctx.getState();
46
104
  const canvasMode = _ctx.getCanvasMode();
47
105
 
48
- const renderDoc =
106
+ // Suppress server function resolution in non-preview modes to avoid
107
+ // failed proxy calls and infinite reactive retries (also covers
108
+ // async custom element connectedCallbacks that run after this function returns)
109
+ setSkipServerFunctions(canvasMode !== "preview");
110
+
111
+ let renderDoc =
49
112
  canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
50
113
 
114
+ // ─── Layout wrapping ────────────────────────────────────────────────────
115
+ // For page documents, resolve the layout and wrap content in the layout shell.
116
+ // Layout-originated nodes are marked with $__layout so we can distinguish them.
117
+ let layoutWrapped = false;
118
+ activeLayoutPath = null;
119
+
120
+ const isPage =
121
+ S.documentPath &&
122
+ projectState?.isSiteProject &&
123
+ (S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
124
+
125
+ /** @type {any[] | null} Path prefix in merged doc where page children live */
126
+ let pageContentPrefix = null;
127
+
128
+ if (isPage) {
129
+ const layoutPath = getEffectiveLayoutPath(doc.$layout);
130
+ if (layoutPath) {
131
+ const layoutDoc = await resolveLayoutDoc(layoutPath);
132
+ if (layoutDoc) {
133
+ if (gen !== view.renderGeneration) return null;
134
+ activeLayoutPath = layoutPath.replace(/^\.\//, "");
135
+ markLayoutNodes(layoutDoc);
136
+ const pageForSlots = canvasMode === "preview" ? structuredClone(doc) : renderDoc;
137
+ const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
138
+ renderDoc =
139
+ canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
140
+ layoutWrapped = true;
141
+ pageContentPrefix = findPageContentPrefix(merged);
142
+ }
143
+ }
144
+ }
145
+
51
146
  // In edit mode, collect paths where $map templates were inlined as children[0]
52
147
  // so we can remap runtime paths (children,0,...) → (children,map,...)
53
148
  const mapParentPaths = new Set();
@@ -224,26 +319,49 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
224
319
  if (gen !== view.renderGeneration) return null;
225
320
  const el = /** @type {HTMLElement} */ (
226
321
  runtimeRenderNode(renderDoc, $defs, {
227
- onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
322
+ onNodeCreated(/** @type {any} */ el, /** @type {any} */ path, /** @type {any} */ def) {
323
+ // Track layout-originated elements — don't store in elToPath to avoid
324
+ // path collisions with remapped page content paths
325
+ if (layoutWrapped && def?.$__layout) {
326
+ layoutElements.add(el);
327
+ if (el.setAttribute) el.setAttribute("data-jx-layout", "");
328
+ return;
329
+ }
330
+
331
+ // Remap layout-wrapped paths: strip the layout prefix so paths are
332
+ // relative to the original page document (which is what S.document holds)
333
+ let mappedPath = path;
334
+ if (layoutWrapped && pageContentPrefix) {
335
+ const pfx = pageContentPrefix;
336
+ if (
337
+ path.length >= pfx.length &&
338
+ pfx.every((/** @type {any} */ seg, /** @type {number} */ i) => path[i] === seg)
339
+ ) {
340
+ mappedPath = ["children", ...path.slice(pfx.length)];
341
+ }
342
+ }
343
+
228
344
  // Remap $map paths: wrapper and template children → real document paths
229
345
  // prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
230
346
  // Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
231
- let mappedPath = path;
232
347
  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("/");
348
+ for (let i = 0; i < mappedPath.length - 1; i++) {
349
+ if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
350
+ const parentKey = mappedPath.slice(0, i).join("/");
236
351
  if (mapParentPaths.has(parentKey)) {
237
- if (path.length === i + 2) {
238
- // Wrapper div itself $map container path
239
- mappedPath = path.slice(0, i + 1);
352
+ if (mappedPath.length === i + 2) {
353
+ mappedPath = mappedPath.slice(0, i + 1);
240
354
  } else if (
241
- path.length >= i + 4 &&
242
- path[i + 2] === "children" &&
243
- path[i + 3] === 0
355
+ mappedPath.length >= i + 4 &&
356
+ mappedPath[i + 2] === "children" &&
357
+ mappedPath[i + 3] === 0
244
358
  ) {
245
- // Template or its descendants → children/map/...rest
246
- mappedPath = [...path.slice(0, i), "children", "map", ...path.slice(i + 4)];
359
+ mappedPath = [
360
+ ...mappedPath.slice(0, i),
361
+ "children",
362
+ "map",
363
+ ...mappedPath.slice(i + 4),
364
+ ];
247
365
  }
248
366
  break;
249
367
  }
@@ -249,10 +249,11 @@ export function renderCanvas() {
249
249
  resetZoomIndicator();
250
250
  }
251
251
 
252
+ const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
252
253
  const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
253
254
  const editTpl = html`
254
255
  <div class="content-edit-canvas">
255
- <div class="content-edit-column">${panelTpl}</div>
256
+ <div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
256
257
  </div>
257
258
  `;
258
259
  litRender(editTpl, canvasWrap);
@@ -20,19 +20,39 @@ import { view } from "../view.js";
20
20
  import { applyDropInstruction } from "../panels/dnd.js";
21
21
  import { effectiveZoom } from "../canvas/canvas-helpers.js";
22
22
 
23
+ /**
24
+ * @typedef {{
25
+ * canvas: HTMLElement;
26
+ * overlayClk: HTMLElement;
27
+ * overlay: HTMLElement;
28
+ * viewport: HTMLElement;
29
+ * dropLine: HTMLElement;
30
+ * }} CanvasPanel
31
+ *
32
+ * @typedef {(string | number)[]} JxPath
33
+ *
34
+ * @typedef {{ type: "reorder-above" | "reorder-below" | "make-child" }} DropInstruction
35
+ *
36
+ * @typedef {{ instruction: DropInstruction; referenceEl: HTMLElement; targetPath: JxPath }} DropResult
37
+ */
38
+
39
+ /** @type {HTMLElement | null} */
40
+ let _activeDropEl = null;
41
+
23
42
  /**
24
43
  * Register all canvas elements in a panel as DnD drop targets.
25
44
  *
26
- * @param {any} panel
45
+ * @param {CanvasPanel} panel
27
46
  */
28
47
  export function registerPanelDnD(panel) {
29
48
  const { canvas, dropLine } = panel;
30
49
  const allEls = canvas.querySelectorAll("*");
31
50
 
32
51
  const monitorCleanup = monitorForElements({
33
- onDragStart() {
52
+ onDragStart({ location }) {
53
+ view.lastDragInput = location.current.input;
34
54
  for (const el of canvas.querySelectorAll("*")) {
35
- /** @type {any} */ (el).style.pointerEvents = "auto";
55
+ /** @type {HTMLElement} */ (el).style.pointerEvents = "auto";
36
56
  }
37
57
  for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
38
58
  },
@@ -40,10 +60,12 @@ export function registerPanelDnD(panel) {
40
60
  view.lastDragInput = location.current.input;
41
61
  },
42
62
  onDrop() {
63
+ _activeDropEl?.classList.remove("canvas-drop-target");
64
+ _activeDropEl = null;
43
65
  for (const p of canvasPanels) p.dropLine.style.display = "none";
44
66
  view.lastDragInput = null;
45
67
  for (const el of canvas.querySelectorAll("*")) {
46
- /** @type {any} */ (el).style.pointerEvents = "none";
68
+ /** @type {HTMLElement} */ (el).style.pointerEvents = "none";
47
69
  }
48
70
  for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
49
71
  },
@@ -56,34 +78,45 @@ export function registerPanelDnD(panel) {
56
78
  if (!elPath) continue;
57
79
 
58
80
  const node = getNodeAtPath(S.document, elPath);
59
- const isVoid = VOID_ELEMENTS.has((node?.tagName || "div").toLowerCase());
81
+ const tag = (node?.tagName || "div").toLowerCase();
82
+ const hasElementChildren = node?.children?.some(
83
+ (/** @type {unknown} */ c) => c != null && typeof c === "object",
84
+ );
85
+ const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
60
86
 
61
87
  const cleanup = dropTargetForElements({
62
- element: el,
88
+ element: /** @type {HTMLElement} */ (el),
63
89
  canDrop({ source }) {
64
- const srcPath = source.data.path;
65
- if (srcPath && isAncestor(/** @type {any} */ (srcPath), elPath)) return false;
90
+ const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
91
+ if (srcPath && isAncestor(srcPath, elPath)) return false;
66
92
  return true;
67
93
  },
68
94
  getData() {
69
- return { path: elPath, _isVoid: isVoid };
70
- },
71
- onDragEnter() {
72
- showCanvasDropIndicator(el, elPath, isVoid, panel);
95
+ return { path: elPath, _isVoid: isLeaf };
73
96
  },
74
- onDrag() {
75
- showCanvasDropIndicator(el, elPath, isVoid, panel);
97
+ onDragEnter({ location }) {
98
+ view.lastDragInput = location.current.input;
99
+ if (_activeDropEl && _activeDropEl !== el) {
100
+ _activeDropEl.classList.remove("canvas-drop-target");
101
+ }
102
+ _activeDropEl = /** @type {HTMLElement} */ (el);
103
+ showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
76
104
  },
77
- onDragLeave() {
78
- dropLine.style.display = "none";
79
- el.classList.remove("canvas-drop-target");
105
+ onDrag({ location }) {
106
+ view.lastDragInput = location.current.input;
107
+ showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
80
108
  },
109
+ onDragLeave() {},
81
110
  onDrop({ source }) {
82
111
  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);
112
+ /** @type {HTMLElement} */ (el).classList.remove("canvas-drop-target");
113
+ _activeDropEl = null;
114
+ const { instruction, targetPath } = getCanvasDropResult(
115
+ /** @type {HTMLElement} */ (el),
116
+ elPath,
117
+ isLeaf,
118
+ );
119
+ applyDropInstruction(instruction, source.data, targetPath);
87
120
  },
88
121
  });
89
122
  view.canvasDndCleanups.push(cleanup);
@@ -91,49 +124,98 @@ export function registerPanelDnD(panel) {
91
124
  }
92
125
 
93
126
  /**
94
- * @param {any} el
95
- * @param {any} elPath
96
- * @param {any} isVoid
127
+ * @param {HTMLElement} el
128
+ * @param {JxPath} elPath
129
+ * @param {boolean} isLeaf
130
+ * @returns {DropResult}
97
131
  */
98
- function getCanvasDropInstruction(el, elPath, isVoid) {
99
- const rect = el.getBoundingClientRect();
100
- if (!view.lastDragInput) return null;
132
+ function getCanvasDropResult(el, elPath, isLeaf) {
133
+ if (!view.lastDragInput)
134
+ return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
101
135
  const y = view.lastDragInput.clientY;
136
+
137
+ if (elPath.length === 0) {
138
+ const children = /** @type {HTMLElement[]} */ (Array.from(el.children));
139
+ if (children.length === 0)
140
+ return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
141
+ return nearestChildEdge(children, y, elPath);
142
+ }
143
+
144
+ const rect = el.getBoundingClientRect();
102
145
  const relY = (y - rect.top) / rect.height;
103
146
 
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" };
147
+ if (isLeaf) {
148
+ const instruction =
149
+ relY < 0.5
150
+ ? { type: /** @type {const} */ ("reorder-above") }
151
+ : { type: /** @type {const} */ ("reorder-below") };
152
+ return { instruction, referenceEl: el, targetPath: elPath };
153
+ }
154
+
155
+ if (relY < 0.25)
156
+ return { instruction: { type: "reorder-above" }, referenceEl: el, targetPath: elPath };
157
+ if (relY > 0.75)
158
+ return { instruction: { type: "reorder-below" }, referenceEl: el, targetPath: elPath };
159
+ return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
109
160
  }
110
161
 
111
162
  /**
112
- * @param {any} el
113
- * @param {any} elPath
114
- * @param {any} isVoid
115
- * @param {any} panel
163
+ * Find the nearest child edge to the cursor and return the appropriate instruction along with the
164
+ * reference child element and its path.
165
+ *
166
+ * @param {HTMLElement[]} children
167
+ * @param {number} cursorY
168
+ * @param {JxPath} parentPath
169
+ * @returns {DropResult}
116
170
  */
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;
171
+ function nearestChildEdge(children, cursorY, parentPath) {
172
+ let closestDist = Infinity;
173
+ let instruction = /** @type {DropInstruction} */ ({ type: "reorder-below" });
174
+ let closestIdx = children.length - 1;
175
+
176
+ for (let i = 0; i < children.length; i++) {
177
+ const rect = children[i].getBoundingClientRect();
178
+ const topDist = Math.abs(cursorY - rect.top);
179
+ const bottomDist = Math.abs(cursorY - rect.bottom);
180
+
181
+ if (topDist < closestDist) {
182
+ closestDist = topDist;
183
+ instruction = { type: "reorder-above" };
184
+ closestIdx = i;
185
+ }
186
+ if (bottomDist < closestDist) {
187
+ closestDist = bottomDist;
188
+ instruction = { type: "reorder-below" };
189
+ closestIdx = i;
190
+ }
123
191
  }
124
192
 
193
+ const childPath = [...parentPath, "children", closestIdx];
194
+ return { instruction, referenceEl: children[closestIdx], targetPath: childPath };
195
+ }
196
+
197
+ /**
198
+ * @param {HTMLElement} el
199
+ * @param {JxPath} elPath
200
+ * @param {boolean} isLeaf
201
+ * @param {CanvasPanel} panel
202
+ */
203
+ function showCanvasDropIndicator(el, elPath, isLeaf, panel) {
204
+ const { instruction, referenceEl } = getCanvasDropResult(el, elPath, isLeaf);
205
+ const { dropLine, viewport } = panel;
206
+
125
207
  const scale = effectiveZoom();
126
208
  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;
209
+ const refRect = referenceEl.getBoundingClientRect();
210
+ const left = (refRect.left - wrapRect.left + viewport.scrollLeft) / scale;
211
+ const width = refRect.width / scale;
130
212
 
131
213
  if (instruction.type === "make-child") {
132
214
  dropLine.style.display = "block";
133
- dropLine.style.top = `${(elRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
215
+ dropLine.style.top = `${(refRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
134
216
  dropLine.style.left = `${left}px`;
135
217
  dropLine.style.width = `${width}px`;
136
- dropLine.style.height = `${elRect.height / scale}px`;
218
+ dropLine.style.height = `${refRect.height / scale}px`;
137
219
  dropLine.className = "canvas-drop-indicator inside";
138
220
  el.classList.add("canvas-drop-target");
139
221
  return;
@@ -142,13 +224,13 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
142
224
  el.classList.remove("canvas-drop-target");
143
225
  const top =
144
226
  instruction.type === "reorder-above"
145
- ? (elRect.top - wrapRect.top + viewport.scrollTop) / scale
146
- : (elRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
227
+ ? (refRect.top - wrapRect.top + viewport.scrollTop) / scale
228
+ : (refRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
147
229
 
148
230
  dropLine.style.display = "block";
149
231
  dropLine.style.top = `${top}px`;
150
232
  dropLine.style.left = `${left}px`;
151
233
  dropLine.style.width = `${width}px`;
152
- dropLine.style.height = "2px";
234
+ dropLine.style.height = "";
153
235
  dropLine.className = "canvas-drop-indicator line";
154
236
  }
@@ -11,6 +11,7 @@ import {
11
11
  getActivePanel,
12
12
  overlayBoxDescriptor,
13
13
  } from "../canvas/canvas-helpers.js";
14
+ import { layoutElements } from "../canvas/canvas-live-render.js";
14
15
 
15
16
  /** @type {any} */
16
17
  let _ctx = null;
@@ -85,7 +86,11 @@ export function render() {
85
86
 
86
87
  if (S.hover && !pathsEqual(S.hover, S.selection)) {
87
88
  const el = findCanvasElement(S.hover, p.canvas);
88
- if (el) boxes.push(overlayBoxDescriptor(el, "hover", p));
89
+ if (el) {
90
+ const desc = overlayBoxDescriptor(el, "hover", p);
91
+ if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
92
+ boxes.push(desc);
93
+ }
89
94
  }
90
95
 
91
96
  if (S.selection && p === getActivePanel()) {
@@ -93,6 +98,7 @@ export function render() {
93
98
  if (el) {
94
99
  const desc = overlayBoxDescriptor(el, "selection", p);
95
100
  if (view.componentInlineEdit || _ctx.isEditing()) /** @type {any} */ (desc).border = "none";
101
+ if (layoutElements.has(el)) /** @type {any} */ (desc).isLayout = true;
96
102
  boxes.push(desc);
97
103
  }
98
104
  }
@@ -103,11 +109,17 @@ export function render() {
103
109
  ${boxes.map(
104
110
  (b) => html`
105
111
  <div
106
- class=${b.cls}
112
+ class="${b.cls}${/** @type {any} */ (b).isLayout ? " overlay-layout" : ""}"
107
113
  style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}${b.border
108
114
  ? `;border:${b.border}`
109
115
  : ""}"
110
- ></div>
116
+ >
117
+ ${
118
+ /** @type {any} */ (b).isLayout
119
+ ? html`<span class="overlay-layout-badge">Layout</span>`
120
+ : nothing
121
+ }
122
+ </div>
111
123
  `,
112
124
  )}
113
125
  `,
@@ -23,6 +23,7 @@ import { showContextMenu } from "../editor/context-menu.js";
23
23
  import * as insertionHelper from "../editor/insertion-helper.js";
24
24
  import { defaultDef } from "../panels/shared.js";
25
25
  import { bubbleInlinePath, findCanvasElement, effectiveZoom } from "../canvas/canvas-helpers.js";
26
+ import { layoutElements, activeLayoutPath } from "../canvas/canvas-live-render.js";
26
27
 
27
28
  /** @type {any} */
28
29
  let _ctx = null;
@@ -87,6 +88,15 @@ export function registerPanelEvents(panel) {
87
88
 
88
89
  for (const el of elements) {
89
90
  if (canvas.contains(el) && el !== canvas) {
91
+ // Layout element clicked — show layout info instead of selecting in page doc
92
+ if (layoutElements.has(el)) {
93
+ view.layoutSelection = { el, layoutPath: activeLayoutPath };
94
+ update(selectNode(S, null));
95
+ renderOnly("rightPanel");
96
+ return;
97
+ }
98
+ view.layoutSelection = null;
99
+
90
100
  const originalPath = elToPath.get(el);
91
101
  if (originalPath) {
92
102
  let path = bubbleInlinePath(S.document, originalPath);