@jxsuite/studio 0.11.0 → 0.14.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 (55) hide show
  1. package/dist/studio.js +6517 -5870
  2. package/dist/studio.js.map +57 -51
  3. package/package.json +5 -3
  4. package/src/canvas/canvas-diff.js +184 -0
  5. package/src/canvas/canvas-helpers.js +10 -14
  6. package/src/canvas/canvas-live-render.js +28 -2
  7. package/src/canvas/canvas-render.js +170 -20
  8. package/src/canvas/canvas-utils.js +22 -25
  9. package/src/editor/component-inline-edit.js +55 -44
  10. package/src/editor/content-inline-edit.js +47 -50
  11. package/src/editor/context-menu.js +78 -53
  12. package/src/editor/convert-to-component.js +11 -14
  13. package/src/editor/insertion-helper.js +8 -11
  14. package/src/editor/shortcuts.js +88 -64
  15. package/src/files/components.js +15 -4
  16. package/src/files/file-ops.js +57 -108
  17. package/src/files/files.js +81 -28
  18. package/src/panels/activity-bar.js +31 -7
  19. package/src/panels/block-action-bar.js +104 -80
  20. package/src/panels/canvas-dnd.js +4 -10
  21. package/src/panels/dnd.js +77 -35
  22. package/src/panels/editors.js +17 -26
  23. package/src/panels/elements-panel.js +17 -11
  24. package/src/panels/events-panel.js +44 -39
  25. package/src/panels/git-panel.js +47 -4
  26. package/src/panels/layers-panel.js +25 -21
  27. package/src/panels/left-panel.js +109 -44
  28. package/src/panels/overlays.js +91 -41
  29. package/src/panels/panel-events.js +28 -37
  30. package/src/panels/preview-render.js +7 -3
  31. package/src/panels/properties-panel.js +179 -104
  32. package/src/panels/pseudo-preview.js +9 -8
  33. package/src/panels/right-panel.js +85 -37
  34. package/src/panels/shared.js +0 -22
  35. package/src/panels/signals-panel.js +125 -54
  36. package/src/panels/statusbar.js +26 -19
  37. package/src/panels/style-inputs.js +5 -4
  38. package/src/panels/style-panel.js +128 -105
  39. package/src/panels/style-utils.js +8 -6
  40. package/src/panels/stylebook-layers-panel.js +5 -5
  41. package/src/panels/stylebook-panel.js +27 -31
  42. package/src/panels/tab-strip.js +124 -0
  43. package/src/panels/toolbar.js +40 -10
  44. package/src/reactivity.js +28 -0
  45. package/src/settings/content-types-editor.js +56 -7
  46. package/src/settings/defs-editor.js +56 -7
  47. package/src/settings/schema-field-ui.js +60 -25
  48. package/src/state.js +2 -459
  49. package/src/store.js +61 -219
  50. package/src/studio.js +163 -300
  51. package/src/tabs/tab.js +168 -0
  52. package/src/tabs/transact.js +406 -0
  53. package/src/ui/color-selector.js +4 -5
  54. package/src/view.js +3 -0
  55. package/src/workspace/workspace.js +90 -0
@@ -6,18 +6,9 @@
6
6
  import { html, render as litRender, nothing } from "lit-html";
7
7
  import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
8
8
 
9
- import {
10
- getState,
11
- update,
12
- updateSession,
13
- renderOnly,
14
- selectNode,
15
- moveNode,
16
- getNodeAtPath,
17
- nodeLabel,
18
- parentElementPath,
19
- childIndex,
20
- } from "../store.js";
9
+ import { getNodeAtPath, nodeLabel, parentElementPath, childIndex } from "../store.js";
10
+ import { activeTab } from "../workspace/workspace.js";
11
+ import { transactDoc, mutateMoveNode } from "../tabs/transact.js";
21
12
  import { view } from "../view.js";
22
13
  import { isEditing, getActiveElement, getInlineActions } from "../editor/inline-edit.js";
23
14
  import { toggleInlineFormat, isTagActiveInSelection } from "../editor/inline-format.js";
@@ -25,7 +16,21 @@ import { componentRegistry } from "../files/components.js";
25
16
  import { convertToComponent } from "../editor/convert-to-component.js";
26
17
  import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers.js";
27
18
 
28
- /** @type {any} */
19
+ /**
20
+ * @typedef {import("../state.js").StudioState} StudioState
21
+ *
22
+ * @typedef {import("../state.js").JxPath} JxPath
23
+ *
24
+ * @typedef {{ command: string; tag: string; label: string; icon: string; shortcut?: string }} InlineAction
25
+ */
26
+
27
+ /**
28
+ * @type {{
29
+ * getCanvasMode: () => string;
30
+ * navigateToComponent: (path: string) => void;
31
+ * createFloatingContainer: () => HTMLElement;
32
+ * } | null}
33
+ */
29
34
  let _ctx = null;
30
35
 
31
36
  /**
@@ -33,8 +38,8 @@ let _ctx = null;
33
38
  *
34
39
  * @param {{
35
40
  * getCanvasMode: () => string;
36
- * navigateToComponent: Function;
37
- * createFloatingContainer: Function;
41
+ * navigateToComponent: (path: string) => void;
42
+ * createFloatingContainer: () => HTMLElement;
38
43
  * }} ctx
39
44
  */
40
45
  export function initBlockActionBar(ctx) {
@@ -45,7 +50,7 @@ export function initBlockActionBar(ctx) {
45
50
  }
46
51
 
47
52
  /** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
48
- const formatIconMap = /** @type {Record<string, any>} */ ({
53
+ const formatIconMap = /** @type {Record<string, import("lit-html").TemplateResult>} */ ({
49
54
  "sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
50
55
  "sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
51
56
  "sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
@@ -63,11 +68,11 @@ const formatIconMap = /** @type {Record<string, any>} */ ({
63
68
  /**
64
69
  * Prevent the bar from stealing focus from contenteditable
65
70
  *
66
- * @param {any} e
71
+ * @param {MouseEvent} e
67
72
  */
68
73
  function onBarMousedown(e) {
69
- if (e.target.closest("sp-textfield")) return;
70
- if (e.target.closest(".bar-drag-handle")) return;
74
+ if (/** @type {HTMLElement} */ (e.target).closest("sp-textfield")) return;
75
+ if (/** @type {HTMLElement} */ (e.target).closest(".bar-drag-handle")) return;
71
76
  e.preventDefault();
72
77
  }
73
78
 
@@ -78,42 +83,48 @@ function captureSelectionRange() {
78
83
  }
79
84
 
80
85
  /**
81
- * @param {any} e
82
- * @param {any} action
86
+ * @param {MouseEvent} e
87
+ * @param {InlineAction} action
83
88
  */
84
89
  function onFormatClick(e, action) {
85
90
  e.stopPropagation();
86
91
  if (action.command === "link") {
87
- showLinkPopover(e.target.closest("sp-action-button"));
92
+ showLinkPopover(
93
+ /** @type {HTMLElement} */ (
94
+ /** @type {HTMLElement} */ (e.target).closest("sp-action-button")
95
+ ),
96
+ );
88
97
  } else if (view.savedRange) {
89
- const sel = /** @type {any} */ (window.getSelection());
98
+ const sel = window.getSelection();
90
99
  const anchor = view.savedRange.startContainer;
91
100
  const editableRoot = (
92
- anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement
101
+ anchor?.nodeType === Node.ELEMENT_NODE
102
+ ? /** @type {Element} */ (anchor)
103
+ : anchor?.parentElement
93
104
  )?.closest("[contenteditable]");
94
105
  if (editableRoot) {
95
- editableRoot.focus();
96
- sel.removeAllRanges();
97
- sel.addRange(view.savedRange);
106
+ /** @type {HTMLElement} */ (editableRoot).focus();
107
+ sel?.removeAllRanges();
108
+ sel?.addRange(view.savedRange);
98
109
  applyInlineFormat(action);
99
110
  }
100
111
  }
101
112
  }
102
113
 
103
114
  function renderParentSelector() {
104
- const S = getState();
105
- const pPath = parentElementPath(S.selection);
115
+ const tab = activeTab.value;
116
+ if (!tab?.session.selection) return nothing;
117
+ const pPath = parentElementPath(tab.session.selection);
106
118
  if (!pPath) return nothing;
107
- const parentNode = getNodeAtPath(S.document, pPath);
119
+ const parentNode = getNodeAtPath(tab.doc.document, pPath);
108
120
  return html`
109
121
  <sp-action-button
110
122
  size="xs"
111
123
  quiet
112
124
  title="Select parent: ${nodeLabel(parentNode)}"
113
- @click=${(/** @type {any} */ e) => {
125
+ @click=${(/** @type {MouseEvent} */ e) => {
114
126
  e.stopPropagation();
115
- const S = getState();
116
- update(selectNode(S, pPath));
127
+ activeTab.value.session.selection = pPath;
117
128
  }}
118
129
  >
119
130
  <sp-icon-back slot="icon"></sp-icon-back>
@@ -122,10 +133,12 @@ function renderParentSelector() {
122
133
  }
123
134
 
124
135
  function renderMoveArrows() {
125
- const S = getState();
126
- const idx = /** @type {number} */ (childIndex(S.selection));
127
- const pPath = parentElementPath(S.selection);
128
- const parentNode = getNodeAtPath(S.document, /** @type {any} */ (pPath));
136
+ const tab = activeTab.value;
137
+ if (!tab?.session.selection) return nothing;
138
+ const sel = tab.session.selection;
139
+ const idx = /** @type {number} */ (childIndex(sel));
140
+ const pPath = parentElementPath(sel);
141
+ const parentNode = getNodeAtPath(tab.doc.document, /** @type {JxPath} */ (pPath));
129
142
  const siblings = parentNode?.children;
130
143
  return html`
131
144
  <sp-action-button
@@ -133,7 +146,7 @@ function renderMoveArrows() {
133
146
  quiet
134
147
  title="Move up"
135
148
  ?disabled=${idx <= 0}
136
- @click=${(/** @type {any} */ e) => {
149
+ @click=${(/** @type {MouseEvent} */ e) => {
137
150
  e.stopPropagation();
138
151
  moveSelectionUp();
139
152
  }}
@@ -145,7 +158,7 @@ function renderMoveArrows() {
145
158
  quiet
146
159
  title="Move down"
147
160
  ?disabled=${!siblings || idx >= siblings.length - 1}
148
- @click=${(/** @type {any} */ e) => {
161
+ @click=${(/** @type {MouseEvent} */ e) => {
149
162
  e.stopPropagation();
150
163
  moveSelectionDown();
151
164
  }}
@@ -158,10 +171,10 @@ function renderMoveArrows() {
158
171
  /**
159
172
  * Apply an inline format action.
160
173
  *
161
- * @param {any} action
174
+ * @param {InlineAction} action
162
175
  */
163
176
  function applyInlineFormat(action) {
164
- /** @type {Record<string, any>} */
177
+ /** @type {Record<string, string>} */
165
178
  const cmdToTag = {
166
179
  bold: "strong",
167
180
  italic: "em",
@@ -190,19 +203,22 @@ export function dismissBlockActionBar() {
190
203
  if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
191
204
  }
192
205
 
193
- /** @param {any} anchorBtn */
206
+ /** @param {HTMLElement} anchorBtn */
194
207
  function showLinkPopover(anchorBtn) {
195
208
  litRender(nothing, view.linkPopoverHost);
196
209
 
197
210
  const sel = window.getSelection();
198
- /** @type {any} */
211
+ /** @type {HTMLAnchorElement | null} */
199
212
  let existingLink = null;
200
213
  if (sel?.rangeCount) {
201
- /** @type {any} */
214
+ /** @type {Node | null} */
202
215
  let node = sel.anchorNode;
203
216
  while (node && node !== document.body) {
204
- if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "a") {
205
- existingLink = node;
217
+ if (
218
+ node.nodeType === Node.ELEMENT_NODE &&
219
+ /** @type {Element} */ (node).tagName.toLowerCase() === "a"
220
+ ) {
221
+ existingLink = /** @type {HTMLAnchorElement} */ (node);
206
222
  break;
207
223
  }
208
224
  node = node.parentNode;
@@ -212,8 +228,10 @@ function showLinkPopover(anchorBtn) {
212
228
  const rect = anchorBtn.getBoundingClientRect();
213
229
 
214
230
  const onApply = () => {
215
- const field = view.linkPopoverHost.querySelector("sp-textfield");
216
- const url = /** @type {any} */ (field)?.value;
231
+ const field = /** @type {HTMLInputElement | null} */ (
232
+ view.linkPopoverHost.querySelector("sp-textfield")
233
+ );
234
+ const url = field?.value || "";
217
235
  if (existingLink) {
218
236
  existingLink.setAttribute("href", url);
219
237
  } else if (url) {
@@ -224,6 +242,7 @@ function showLinkPopover(anchorBtn) {
224
242
  };
225
243
 
226
244
  const onRemove = () => {
245
+ if (!existingLink?.parentNode) return;
227
246
  const frag = document.createDocumentFragment();
228
247
  while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
229
248
  existingLink.parentNode.replaceChild(frag, existingLink);
@@ -231,7 +250,7 @@ function showLinkPopover(anchorBtn) {
231
250
  renderBlockActionBar();
232
251
  };
233
252
 
234
- const onKeydown = (/** @type {any} */ e) => {
253
+ const onKeydown = (/** @type {KeyboardEvent} */ e) => {
235
254
  if (e.key === "Enter") onApply();
236
255
  else if (e.key === "Escape") {
237
256
  litRender(nothing, view.linkPopoverHost);
@@ -273,32 +292,33 @@ function showLinkPopover(anchorBtn) {
273
292
 
274
293
  /** Move the selected node up (swap with previous sibling). */
275
294
  function moveSelectionUp() {
276
- const S = getState();
277
- if (!S.selection || S.selection.length < 2) return;
278
- const idx = /** @type {number} */ (childIndex(S.selection));
295
+ const tab = activeTab.value;
296
+ if (!tab?.session.selection || tab.session.selection.length < 2) return;
297
+ const sel = tab.session.selection;
298
+ const idx = /** @type {number} */ (childIndex(sel));
279
299
  if (idx <= 0) return;
280
- const pPath = /** @type {any} */ (parentElementPath(S.selection));
281
- update(moveNode(S, S.selection, pPath, idx - 1));
282
- updateSession({ selection: [...pPath, "children", idx - 1] });
283
- renderOnly("overlays");
300
+ const pPath = /** @type {JxPath} */ (parentElementPath(sel));
301
+ transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx - 1));
302
+ tab.session.selection = [...pPath, "children", idx - 1];
284
303
  }
285
304
 
286
305
  /** Move the selected node down (swap with next sibling). */
287
306
  function moveSelectionDown() {
288
- const S = getState();
289
- if (!S.selection || S.selection.length < 2) return;
290
- const idx = /** @type {number} */ (childIndex(S.selection));
291
- const pPath = /** @type {any} */ (parentElementPath(S.selection));
292
- const parentNode = getNodeAtPath(S.document, pPath);
307
+ const tab = activeTab.value;
308
+ if (!tab?.session.selection || tab.session.selection.length < 2) return;
309
+ const sel = tab.session.selection;
310
+ const idx = /** @type {number} */ (childIndex(sel));
311
+ const pPath = /** @type {JxPath} */ (parentElementPath(sel));
312
+ const parentNode = getNodeAtPath(tab.doc.document, pPath);
293
313
  const siblings = parentNode?.children;
294
314
  if (!siblings || idx >= siblings.length - 1) return;
295
- update(moveNode(S, S.selection, pPath, idx + 2));
296
- updateSession({ selection: [...pPath, "children", idx + 1] });
297
- renderOnly("overlays");
315
+ transactDoc(tab, (t) => mutateMoveNode(t, sel, pPath, idx + 2));
316
+ tab.session.selection = [...pPath, "children", idx + 1];
298
317
  }
299
318
 
300
319
  /** Render the unified block action bar above the selected element. */
301
320
  export function renderBlockActionBar() {
321
+ if (!_ctx) return;
302
322
  if (!view.blockActionBarEl) {
303
323
  view.blockActionBarEl = _ctx.createFloatingContainer();
304
324
  }
@@ -308,21 +328,22 @@ export function renderBlockActionBar() {
308
328
  view.selDragCleanup = null;
309
329
  }
310
330
 
311
- const S = getState();
331
+ const tab = activeTab.value;
312
332
  const canvasMode = _ctx.getCanvasMode();
313
333
 
314
- if (!S.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
334
+ if (!tab?.session.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
315
335
  litRender(nothing, view.blockActionBarEl);
316
336
  return;
317
337
  }
318
338
 
339
+ const selection = tab.session.selection;
319
340
  const activePanel = getActivePanel();
320
341
  if (!activePanel) {
321
342
  litRender(nothing, view.blockActionBarEl);
322
343
  return;
323
344
  }
324
- const el = findCanvasElement(S.selection, activePanel.canvas);
325
- const node = el && getNodeAtPath(S.document, S.selection);
345
+ const el = findCanvasElement(selection, activePanel.canvas);
346
+ const node = el && getNodeAtPath(tab.doc.document, selection);
326
347
  if (!el || !node) {
327
348
  litRender(nothing, view.blockActionBarEl);
328
349
  return;
@@ -347,28 +368,31 @@ export function renderBlockActionBar() {
347
368
  style="left:${elRect.left}px; top:${topPos}px"
348
369
  @mousedown=${onBarMousedown}
349
370
  >
350
- ${S.selection.length >= 2 ? renderParentSelector() : nothing}
371
+ ${selection.length >= 2 ? renderParentSelector() : nothing}
351
372
 
352
373
  <span class="bar-tag">${node.$id || (node.tagName ?? "div")}</span>
353
374
 
354
- ${S.selection.length >= 2
375
+ ${selection.length >= 2
355
376
  ? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
356
377
  : nothing}
357
- ${S.selection.length >= 2 ? renderMoveArrows() : nothing}
358
- ${S.selection.length >= 2 && node.tagName
378
+ ${selection.length >= 2 ? renderMoveArrows() : nothing}
379
+ ${selection.length >= 2 && node.tagName
359
380
  ? (() => {
360
381
  const isComp =
361
382
  node.tagName.includes("-") &&
362
- componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
383
+ componentRegistry.some(
384
+ (/** @type {{ tagName: string }} */ c) => c.tagName === node.tagName,
385
+ );
363
386
  if (isComp) {
364
387
  const comp = componentRegistry.find(
365
- (/** @type {any} */ c) => c.tagName === node.tagName,
388
+ (/** @type {{ tagName: string; path: string }} */ c) =>
389
+ c.tagName === node.tagName,
366
390
  );
367
391
  return html`<sp-action-button
368
392
  size="xs"
369
393
  quiet
370
394
  title="Edit Component"
371
- @click=${() => _ctx.navigateToComponent(comp.path)}
395
+ @click=${() => _ctx?.navigateToComponent(/** @type {string} */ (comp?.path))}
372
396
  ><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
373
397
  ></sp-action-button>`;
374
398
  }
@@ -376,7 +400,7 @@ export function renderBlockActionBar() {
376
400
  size="xs"
377
401
  quiet
378
402
  title="Convert to Component"
379
- @click=${() => convertToComponent(S)}
403
+ @click=${() => convertToComponent()}
380
404
  ><sp-icon-box slot="icon" size="xs"></sp-icon-box
381
405
  ></sp-action-button>`;
382
406
  })()
@@ -398,7 +422,7 @@ export function renderBlockActionBar() {
398
422
  value=${action.tag}
399
423
  title="${action.label}${action.shortcut ? ` (${action.shortcut})` : ""}"
400
424
  @mousedown=${captureSelectionRange}
401
- @click=${(/** @type {any} */ e) => onFormatClick(e, action)}
425
+ @click=${(/** @type {MouseEvent} */ e) => onFormatClick(e, action)}
402
426
  >
403
427
  ${formatIconMap[action.icon] ?? nothing}
404
428
  </sp-action-button>
@@ -422,8 +446,8 @@ export function renderBlockActionBar() {
422
446
  bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
423
447
  }
424
448
  // Attach drag handle
425
- const currentS = getState();
426
- if (currentS.selection?.length >= 2) {
449
+ const currentTab = activeTab.value;
450
+ if (currentTab?.session.selection && currentTab.session.selection.length >= 2) {
427
451
  const handle = bar.querySelector(".bar-drag-handle");
428
452
  if (handle) {
429
453
  if (view.selDragCleanup) {
@@ -432,7 +456,7 @@ export function renderBlockActionBar() {
432
456
  }
433
457
  view.selDragCleanup = draggable({
434
458
  element: handle,
435
- getInitialData: () => ({ type: "tree-node", path: getState().selection }),
459
+ getInitialData: () => ({ type: "tree-node", path: activeTab.value?.session.selection }),
436
460
  });
437
461
  }
438
462
  }
@@ -8,14 +8,8 @@ import {
8
8
  monitorForElements,
9
9
  } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
10
10
 
11
- import {
12
- getState,
13
- elToPath,
14
- canvasPanels,
15
- getNodeAtPath,
16
- VOID_ELEMENTS,
17
- isAncestor,
18
- } from "../store.js";
11
+ import { elToPath, canvasPanels, getNodeAtPath, VOID_ELEMENTS, isAncestor } from "../store.js";
12
+ import { activeTab } from "../workspace/workspace.js";
19
13
  import { view } from "../view.js";
20
14
  import { applyDropInstruction } from "../panels/dnd.js";
21
15
  import { effectiveZoom } from "../canvas/canvas-helpers.js";
@@ -72,12 +66,12 @@ export function registerPanelDnD(panel) {
72
66
  });
73
67
  view.canvasDndCleanups.push(monitorCleanup);
74
68
 
75
- const S = getState();
69
+ const document = activeTab.value?.doc.document;
76
70
  for (const el of allEls) {
77
71
  const elPath = elToPath.get(el);
78
72
  if (!elPath) continue;
79
73
 
80
- const node = getNodeAtPath(S.document, elPath);
74
+ const node = getNodeAtPath(document, elPath);
81
75
  const tag = (node?.tagName || "div").toLowerCase();
82
76
  const hasElementChildren = node?.children?.some(
83
77
  (/** @type {unknown} */ c) => c != null && typeof c === "object",
package/src/panels/dnd.js CHANGED
@@ -15,17 +15,15 @@ import {
15
15
  } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
16
16
 
17
17
  import {
18
- getState,
19
- update,
20
18
  leftPanel,
21
- moveNode,
22
- insertNode,
23
- applyMutation,
24
19
  getNodeAtPath,
25
20
  parentElementPath,
26
21
  childIndex,
27
22
  isAncestor,
23
+ renderOnly,
28
24
  } from "../store.js";
25
+ import { transact, transactDoc, mutateMoveNode, mutateInsertNode } from "../tabs/transact.js";
26
+ import { activeTab } from "../workspace/workspace.js";
29
27
  import { view } from "../view.js";
30
28
  import { componentRegistry, computeRelativePath } from "../files/components.js";
31
29
  import { renderComponentPreview } from "./stylebook-panel.js";
@@ -44,6 +42,7 @@ export function registerLayersDnD() {
44
42
  .map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
45
43
  const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
46
44
  const isVoid = row.hasAttribute("data-dnd-void");
45
+ const isExpanded = row.hasAttribute("data-dnd-expanded");
47
46
 
48
47
  const cleanup = combine(
49
48
  draggable({
@@ -61,9 +60,15 @@ export function registerLayersDnD() {
61
60
  onDragStart() {
62
61
  row.classList.add("dragging");
63
62
  view.layerDragSourceHeight = row.offsetHeight;
63
+ if (isExpanded) {
64
+ hideDescendantRows(row, container);
65
+ }
64
66
  },
65
67
  onDrop() {
66
68
  row.classList.remove("dragging");
69
+ if (isExpanded) {
70
+ renderOnly("leftPanel");
71
+ }
67
72
  },
68
73
  }),
69
74
  dropTargetForElements({
@@ -81,6 +86,7 @@ export function registerLayersDnD() {
81
86
  element,
82
87
  currentLevel: rowDepth,
83
88
  indentPerLevel: 16,
89
+ mode: isExpanded ? "expanded" : "standard",
84
90
  block: isVoid ? ["make-child"] : [],
85
91
  }),
86
92
  );
@@ -113,7 +119,21 @@ export function registerLayersDnD() {
113
119
  if (!instruction || instruction.type === "instruction-blocked") return;
114
120
  const srcData = source.data;
115
121
  const targetPath = target.data.path;
122
+
123
+ // If the source had children, persist collapse at the new location
124
+ const srcRow = srcData.type === "tree-node" && source.element;
125
+ const wasExpanded = srcRow && srcRow.hasAttribute("data-dnd-expanded");
126
+
116
127
  applyDropInstruction(instruction, srcData, targetPath);
128
+
129
+ if (wasExpanded) {
130
+ const tab = activeTab.value;
131
+ const newPath = tab?.session.selection;
132
+ if (newPath) {
133
+ const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
134
+ collapsed.add(newPath.join("/"));
135
+ }
136
+ }
117
137
  },
118
138
  });
119
139
  view.dndCleanups.push(monitorCleanup);
@@ -144,12 +164,14 @@ export function registerComponentsDnD() {
144
164
 
145
165
  const instanceDef = {
146
166
  tagName: comp.tagName,
147
- $props: Object.fromEntries(
148
- comp.props.map((/** @type {any} */ p) => [
149
- p.name,
150
- p.default !== undefined ? p.default : "",
151
- ]),
152
- ),
167
+ $props: comp.props
168
+ ? Object.fromEntries(
169
+ comp.props.map((/** @type {any} */ p) => [
170
+ p.name,
171
+ p.default !== undefined ? p.default : "",
172
+ ]),
173
+ )
174
+ : {},
153
175
  };
154
176
  const cleanup = draggable({
155
177
  element: row,
@@ -190,6 +212,22 @@ export function registerElementsDnD() {
190
212
  });
191
213
  }
192
214
 
215
+ /**
216
+ * Hide descendant rows of the dragged item so it appears collapsed during drag.
217
+ *
218
+ * @param {any} parentRow
219
+ * @param {any} container
220
+ */
221
+ function hideDescendantRows(parentRow, container) {
222
+ const prefix = parentRow.dataset.path + "/";
223
+ const rows = container.querySelectorAll(".layers-tree .layer-row");
224
+ for (const r of rows) {
225
+ if (/** @type {any} */ (r).dataset.path?.startsWith(prefix)) {
226
+ /** @type {any} */ (r).style.display = "none";
227
+ }
228
+ }
229
+ }
230
+
193
231
  /**
194
232
  * @param {any} rowEl
195
233
  * @param {any} data
@@ -251,7 +289,7 @@ export function clearLayerDropGap(container) {
251
289
  * @param {any} targetPath
252
290
  */
253
291
  export function applyDropInstruction(instruction, srcData, targetPath) {
254
- const S = getState();
292
+ const document = activeTab.value?.doc.document;
255
293
  if (srcData.type === "tree-node") {
256
294
  const fromPath = srcData.path;
257
295
  const targetParent = /** @type {any} */ (parentElementPath(targetPath));
@@ -259,15 +297,17 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
259
297
 
260
298
  switch (instruction.type) {
261
299
  case "reorder-above":
262
- update(moveNode(S, fromPath, targetParent, targetIdx));
300
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
263
301
  break;
264
302
  case "reorder-below":
265
- update(moveNode(S, fromPath, targetParent, targetIdx + 1));
303
+ transactDoc(activeTab.value, (t) =>
304
+ mutateMoveNode(t, fromPath, targetParent, targetIdx + 1),
305
+ );
266
306
  break;
267
307
  case "make-child": {
268
- const target = getNodeAtPath(S.document, targetPath);
308
+ const target = getNodeAtPath(document, targetPath);
269
309
  const len = target?.children?.length || 0;
270
- update(moveNode(S, fromPath, targetPath, len));
310
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
271
311
  break;
272
312
  }
273
313
  }
@@ -277,15 +317,21 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
277
317
 
278
318
  switch (instruction.type) {
279
319
  case "reorder-above":
280
- update(insertNode(S, targetParent, targetIdx, structuredClone(srcData.fragment)));
320
+ transactDoc(activeTab.value, (t) =>
321
+ mutateInsertNode(t, targetParent, targetIdx, structuredClone(srcData.fragment)),
322
+ );
281
323
  break;
282
324
  case "reorder-below":
283
- update(insertNode(S, targetParent, targetIdx + 1, structuredClone(srcData.fragment)));
325
+ transactDoc(activeTab.value, (t) =>
326
+ mutateInsertNode(t, targetParent, targetIdx + 1, structuredClone(srcData.fragment)),
327
+ );
284
328
  break;
285
329
  case "make-child": {
286
- const target = getNodeAtPath(S.document, targetPath);
330
+ const target = getNodeAtPath(document, targetPath);
287
331
  const len = target?.children?.length || 0;
288
- update(insertNode(S, targetPath, len, structuredClone(srcData.fragment)));
332
+ transactDoc(activeTab.value, (t) =>
333
+ mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment)),
334
+ );
289
335
  break;
290
336
  }
291
337
  }
@@ -295,20 +341,18 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
295
341
  if (tag && tag.includes("-")) {
296
342
  const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
297
343
  if (comp) {
298
- const currentS = getState();
299
- const elements = currentS.document.$elements || [];
344
+ const tab = activeTab.value;
345
+ const elements = tab?.doc.document?.$elements || [];
300
346
  if (comp.source === "npm") {
301
347
  const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
302
348
  const alreadyImported = elements.some(
303
349
  (/** @type {any} */ e) => e === specifier || e === comp.package,
304
350
  );
305
351
  if (!alreadyImported) {
306
- update(
307
- applyMutation(currentS, (/** @type {any} */ doc) => {
308
- if (!doc.$elements) doc.$elements = [];
309
- doc.$elements.push(specifier);
310
- }),
311
- );
352
+ transact(activeTab.value, (/** @type {any} */ doc) => {
353
+ if (!doc.$elements) doc.$elements = [];
354
+ doc.$elements.push(specifier);
355
+ });
312
356
  }
313
357
  } else {
314
358
  const alreadyImported = elements.some(
@@ -317,13 +361,11 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
317
361
  (e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
318
362
  );
319
363
  if (!alreadyImported) {
320
- const relPath = computeRelativePath(currentS.documentPath, comp.path);
321
- update(
322
- applyMutation(currentS, (/** @type {any} */ doc) => {
323
- if (!doc.$elements) doc.$elements = [];
324
- doc.$elements.push({ $ref: relPath });
325
- }),
326
- );
364
+ const relPath = computeRelativePath(tab?.documentPath, comp.path);
365
+ transact(activeTab.value, (/** @type {any} */ doc) => {
366
+ if (!doc.$elements) doc.$elements = [];
367
+ doc.$elements.push({ $ref: relPath });
368
+ });
327
369
  }
328
370
  }
329
371
  }