@jxsuite/studio 0.23.2 → 0.25.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 (106) hide show
  1. package/dist/studio.js +2311 -718
  2. package/dist/studio.js.map +121 -104
  3. package/package.json +8 -7
  4. package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
  5. package/src/browse/{browse.js → browse.ts} +109 -84
  6. package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
  7. package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
  8. package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
  9. package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
  10. package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
  11. package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
  12. package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
  13. package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
  14. package/src/editor/{context-menu.js → context-menu.ts} +38 -36
  15. package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
  16. package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
  17. package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
  18. package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
  19. package/src/editor/{inline-format.js → inline-format.ts} +46 -51
  20. package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
  21. package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
  22. package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
  23. package/src/files/{components.js → components.ts} +12 -15
  24. package/src/files/{file-ops.js → file-ops.ts} +36 -29
  25. package/src/files/{files.js → files.ts} +102 -84
  26. package/src/github/{github-auth.js → github-auth.ts} +3 -3
  27. package/src/github/{github-publish.js → github-publish.ts} +20 -18
  28. package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
  29. package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
  30. package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
  31. package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
  32. package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
  33. package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
  34. package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
  35. package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
  36. package/src/panels/dnd.ts +417 -0
  37. package/src/panels/{editors.js → editors.ts} +43 -50
  38. package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
  39. package/src/panels/{events-panel.js → events-panel.ts} +33 -25
  40. package/src/panels/{git-panel.js → git-panel.ts} +97 -83
  41. package/src/panels/{head-panel.js → head-panel.ts} +117 -139
  42. package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
  43. package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
  44. package/src/panels/{left-panel.js → left-panel.ts} +56 -63
  45. package/src/panels/{overlays.js → overlays.ts} +28 -36
  46. package/src/panels/{panel-events.js → panel-events.ts} +53 -51
  47. package/src/panels/{preview-render.js → preview-render.ts} +20 -15
  48. package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
  49. package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
  50. package/src/panels/{quick-search.js → quick-search.ts} +16 -17
  51. package/src/panels/{right-panel.js → right-panel.ts} +41 -59
  52. package/src/panels/{shared.js → shared.ts} +8 -6
  53. package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
  54. package/src/panels/{statusbar.js → statusbar.ts} +14 -14
  55. package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
  56. package/src/panels/{style-panel.js → style-panel.ts} +235 -290
  57. package/src/panels/{style-utils.js → style-utils.ts} +32 -31
  58. package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
  59. package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
  60. package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
  61. package/src/panels/{toolbar.js → toolbar.ts} +68 -70
  62. package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
  63. package/src/{platform.js → platform.ts} +5 -3
  64. package/src/platforms/{devserver.js → devserver.ts} +53 -44
  65. package/src/{recent-projects.js → recent-projects.ts} +17 -12
  66. package/src/{resize-edges.js → resize-edges.ts} +18 -13
  67. package/src/services/{cem-export.js → cem-export.ts} +14 -7
  68. package/src/services/{code-services.js → code-services.ts} +29 -20
  69. package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
  70. package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
  71. package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
  72. package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
  73. package/src/settings/{general-settings.js → general-settings.ts} +29 -28
  74. package/src/settings/{head-editor.js → head-editor.ts} +37 -35
  75. package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
  76. package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
  77. package/src/{site-context.js → site-context.ts} +35 -32
  78. package/src/{state.js → state.ts} +95 -78
  79. package/src/{store.js → store.ts} +60 -63
  80. package/src/{studio.js → studio.ts} +117 -119
  81. package/src/tabs/tab.ts +196 -0
  82. package/src/tabs/{transact.js → transact.ts} +107 -60
  83. package/src/types.ts +185 -0
  84. package/src/ui/{button-group.js → button-group.ts} +21 -21
  85. package/src/ui/{color-selector.js → color-selector.ts} +44 -37
  86. package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
  87. package/src/ui/{field-row.js → field-row.ts} +19 -2
  88. package/src/ui/{icons.js → icons.ts} +2 -2
  89. package/src/ui/{layers.js → layers.ts} +33 -24
  90. package/src/ui/{media-picker.js → media-picker.ts} +13 -9
  91. package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
  92. package/src/ui/{spectrum.js → spectrum.ts} +4 -4
  93. package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
  94. package/src/ui/{value-selector.js → value-selector.ts} +28 -28
  95. package/src/ui/{widgets.js → widgets.ts} +62 -15
  96. package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
  97. package/src/utils/edit-display.ts +298 -0
  98. package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
  99. package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
  100. package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
  101. package/src/{view.js → view.ts} +53 -55
  102. package/src/workspace/{workspace.js → workspace.ts} +44 -33
  103. package/src/panels/dnd.js +0 -426
  104. package/src/tabs/tab.js +0 -192
  105. package/src/utils/edit-display.js +0 -203
  106. /package/src/{reactivity.js → reactivity.ts} +0 -0
@@ -1,3 +1,5 @@
1
+ /// <reference lib="dom" />
2
+ /// <reference lib="dom.iterable" />
1
3
  /**
2
4
  * Canvas DnD — extracted from studio.js (Phase 4m). Registers canvas elements as drag-and-drop
3
5
  * targets using @atlaskit/pragmatic-drag-and-drop.
@@ -8,41 +10,35 @@ import {
8
10
  monitorForElements,
9
11
  } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
10
12
 
11
- import { elToPath, canvasPanels, getNodeAtPath, VOID_ELEMENTS, isAncestor } from "../store.js";
12
- import { activeTab } from "../workspace/workspace.js";
13
- import { view } from "../view.js";
14
- import { applyDropInstruction } from "../panels/dnd.js";
15
- import { effectiveZoom } from "../canvas/canvas-helpers.js";
13
+ import { elToPath, canvasPanels, getNodeAtPath, VOID_ELEMENTS, isAncestor } from "../store";
14
+ import { activeTab } from "../workspace/workspace";
15
+ import { view } from "../view";
16
+ import { applyDropInstruction } from "../panels/dnd";
17
+ import { effectiveZoom } from "../canvas/canvas-helpers";
16
18
 
17
- /**
18
- * @typedef {{
19
- * mediaName: string | null;
20
- * element: HTMLElement | null;
21
- * canvas: HTMLElement;
22
- * overlay: HTMLElement;
23
- * overlayClk: HTMLElement;
24
- * viewport: HTMLElement;
25
- * scrollContainer: HTMLElement | null;
26
- * dropLine: HTMLElement;
27
- * _width: number | null;
28
- * }} CanvasPanel
29
- *
30
- * @typedef {(string | number)[]} JxPath
31
- *
32
- * @typedef {{ type: "reorder-above" | "reorder-below" | "make-child" }} DropInstruction
33
- *
34
- * @typedef {{ instruction: DropInstruction; referenceEl: HTMLElement; targetPath: JxPath }} DropResult
35
- */
19
+ import type { CanvasPanel } from "../types";
20
+ export type { CanvasPanel };
21
+
22
+ import type { JxPath } from "../state";
23
+
24
+ interface DropInstruction {
25
+ type: "reorder-above" | "reorder-below" | "make-child";
26
+ }
27
+
28
+ interface DropResult {
29
+ instruction: DropInstruction;
30
+ referenceEl: HTMLElement;
31
+ targetPath: JxPath;
32
+ }
36
33
 
37
- /** @type {HTMLElement | null} */
38
- let _activeDropEl = null;
34
+ let _activeDropEl: HTMLElement | null = null;
39
35
 
40
36
  /**
41
37
  * Register all canvas elements in a panel as DnD drop targets.
42
38
  *
43
39
  * @param {CanvasPanel} panel
44
40
  */
45
- export function registerPanelDnD(panel) {
41
+ export function registerPanelDnD(panel: CanvasPanel) {
46
42
  const { canvas, dropLine } = panel;
47
43
  const allEls = canvas.querySelectorAll("*");
48
44
 
@@ -50,7 +46,7 @@ export function registerPanelDnD(panel) {
50
46
  onDragStart({ location }) {
51
47
  view.lastDragInput = location.current.input;
52
48
  for (const el of canvas.querySelectorAll("*")) {
53
- /** @type {HTMLElement} */ (el).style.pointerEvents = "auto";
49
+ (el as HTMLElement).style.pointerEvents = "auto";
54
50
  }
55
51
  for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
56
52
  },
@@ -65,7 +61,7 @@ export function registerPanelDnD(panel) {
65
61
  }
66
62
  view.lastDragInput = null;
67
63
  for (const el of canvas.querySelectorAll("*")) {
68
- /** @type {HTMLElement} */ (el).style.pointerEvents = "none";
64
+ (el as HTMLElement).style.pointerEvents = "none";
69
65
  }
70
66
  for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
71
67
  },
@@ -77,17 +73,17 @@ export function registerPanelDnD(panel) {
77
73
  const elPath = elToPath.get(el);
78
74
  if (!elPath) continue;
79
75
 
80
- const node = getNodeAtPath(document, elPath);
76
+ const node = getNodeAtPath(document!, elPath);
81
77
  const tag = (node?.tagName || "div").toLowerCase();
82
78
  const hasElementChildren =
83
79
  Array.isArray(node?.children) &&
84
- node.children.some((/** @type {unknown} */ c) => c != null && typeof c === "object");
80
+ node.children.some((c: unknown) => c != null && typeof c === "object");
85
81
  const isLeaf = VOID_ELEMENTS.has(tag) || !hasElementChildren;
86
82
 
87
83
  const cleanup = dropTargetForElements({
88
84
  element: /** @type {HTMLElement} */ (el),
89
85
  canDrop({ source }) {
90
- const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
86
+ const srcPath = source.data.path as JxPath | undefined;
91
87
  if (srcPath && isAncestor(srcPath, elPath)) return false;
92
88
  return true;
93
89
  },
@@ -99,23 +95,19 @@ export function registerPanelDnD(panel) {
99
95
  if (_activeDropEl && _activeDropEl !== el) {
100
96
  _activeDropEl.classList.remove("canvas-drop-target");
101
97
  }
102
- _activeDropEl = /** @type {HTMLElement} */ (el);
103
- showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
98
+ _activeDropEl = el as HTMLElement;
99
+ showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
104
100
  },
105
101
  onDrag({ location }) {
106
102
  view.lastDragInput = location.current.input;
107
- showCanvasDropIndicator(/** @type {HTMLElement} */ (el), elPath, isLeaf, panel);
103
+ showCanvasDropIndicator(el as HTMLElement, elPath, isLeaf, panel);
108
104
  },
109
105
  onDragLeave() {},
110
106
  onDrop({ source }) {
111
107
  dropLine.style.display = "none";
112
- /** @type {HTMLElement} */ (el).classList.remove("canvas-drop-target");
108
+ (el as HTMLElement).classList.remove("canvas-drop-target");
113
109
  _activeDropEl = null;
114
- const { instruction, targetPath } = getCanvasDropResult(
115
- /** @type {HTMLElement} */ (el),
116
- elPath,
117
- isLeaf,
118
- );
110
+ const { instruction, targetPath } = getCanvasDropResult(el as HTMLElement, elPath, isLeaf);
119
111
  applyDropInstruction(instruction, source.data, targetPath);
120
112
  },
121
113
  });
@@ -129,13 +121,13 @@ export function registerPanelDnD(panel) {
129
121
  * @param {boolean} isLeaf
130
122
  * @returns {DropResult}
131
123
  */
132
- function getCanvasDropResult(el, elPath, isLeaf) {
124
+ function getCanvasDropResult(el: HTMLElement, elPath: JxPath, isLeaf: boolean): DropResult {
133
125
  if (!view.lastDragInput)
134
126
  return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
135
127
  const y = view.lastDragInput.clientY;
136
128
 
137
129
  if (elPath.length === 0) {
138
- const children = /** @type {HTMLElement[]} */ (Array.from(el.children));
130
+ const children = Array.from(el.children) as HTMLElement[];
139
131
  if (children.length === 0)
140
132
  return { instruction: { type: "make-child" }, referenceEl: el, targetPath: elPath };
141
133
  return nearestChildEdge(children, y, elPath);
@@ -146,9 +138,7 @@ function getCanvasDropResult(el, elPath, isLeaf) {
146
138
 
147
139
  if (isLeaf) {
148
140
  const instruction =
149
- relY < 0.5
150
- ? { type: /** @type {const} */ ("reorder-above") }
151
- : { type: /** @type {const} */ ("reorder-below") };
141
+ relY < 0.5 ? { type: "reorder-above" as const } : { type: "reorder-below" as const };
152
142
  return { instruction, referenceEl: el, targetPath: elPath };
153
143
  }
154
144
 
@@ -168,9 +158,9 @@ function getCanvasDropResult(el, elPath, isLeaf) {
168
158
  * @param {JxPath} parentPath
169
159
  * @returns {DropResult}
170
160
  */
171
- function nearestChildEdge(children, cursorY, parentPath) {
161
+ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath: JxPath) {
172
162
  let closestDist = Infinity;
173
- let instruction = /** @type {DropInstruction} */ ({ type: "reorder-below" });
163
+ let instruction = { type: "reorder-below" } as DropInstruction;
174
164
  let closestIdx = children.length - 1;
175
165
 
176
166
  for (let i = 0; i < children.length; i++) {
@@ -200,7 +190,12 @@ function nearestChildEdge(children, cursorY, parentPath) {
200
190
  * @param {boolean} isLeaf
201
191
  * @param {CanvasPanel} panel
202
192
  */
203
- function showCanvasDropIndicator(el, elPath, isLeaf, panel) {
193
+ function showCanvasDropIndicator(
194
+ el: HTMLElement,
195
+ elPath: JxPath,
196
+ isLeaf: boolean,
197
+ panel: CanvasPanel,
198
+ ) {
204
199
  const { instruction, referenceEl } = getCanvasDropResult(el, elPath, isLeaf);
205
200
  const { dropLine, viewport } = panel;
206
201
 
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  // ─── Data Explorer ──────────────────────────────────────────────────────────
2
3
 
3
4
  import { html, nothing } from "lit-html";
@@ -7,18 +8,14 @@ import { classMap } from "lit-html/directives/class-map.js";
7
8
  const expandedDataKeys = new Set();
8
9
 
9
10
  /** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
10
- function unwrapSignal(/** @type {unknown} */ value) {
11
- if (
12
- value &&
13
- typeof value === "object" &&
14
- /** @type {Record<string, unknown>} */ (value).__v_isRef
15
- )
16
- return /** @type {Record<string, unknown>} */ (value).value;
11
+ function unwrapSignal(value: unknown) {
12
+ if (value && typeof value === "object" && (value as Record<string, unknown>).__v_isRef)
13
+ return (value as Record<string, unknown>).value;
17
14
  return value;
18
15
  }
19
16
 
20
17
  /** Type label for a signal value in the data explorer. */
21
- function dataTypeLabel(/** @type {unknown} */ value) {
18
+ function dataTypeLabel(value: unknown) {
22
19
  const v = unwrapSignal(value);
23
20
  if (v === null) return "null";
24
21
  if (v === undefined) return "pending";
@@ -40,7 +37,16 @@ function dataTypeLabel(/** @type {unknown} */ value) {
40
37
  * }} callbacks
41
38
  * @returns {import("lit-html").TemplateResult}
42
39
  */
43
- export function renderDataExplorerTemplate(state, liveScope, callbacks) {
40
+ export function renderDataExplorerTemplate(
41
+ state: Record<string, unknown>,
42
+ liveScope: Record<string, unknown> | null,
43
+ callbacks: {
44
+ renderCanvas: () => void;
45
+ renderLeftPanel: () => void;
46
+ defCategory: (def: unknown) => string;
47
+ defBadgeLabel: (def: unknown) => string;
48
+ },
49
+ ) {
44
50
  const { renderCanvas, renderLeftPanel, defCategory, defBadgeLabel } = callbacks;
45
51
 
46
52
  const defs = state || {};
@@ -99,10 +105,10 @@ export function renderDataExplorerTemplate(state, liveScope, callbacks) {
99
105
  * @returns {import("lit-html").TemplateResult}
100
106
  */
101
107
  export function renderDataTreeTemplate(
102
- /** @type {unknown} */ value,
103
- /** @type {number} */ depth,
108
+ value: unknown,
109
+ depth: number,
104
110
  maxDepth = 5,
105
- ) {
111
+ ): import("lit-html").TemplateResult {
106
112
  const indent = `${(depth + 1) * 12}px`;
107
113
 
108
114
  if (depth > maxDepth) {
@@ -127,7 +133,7 @@ export function renderDataTreeTemplate(
127
133
 
128
134
  if (Array.isArray(value)) {
129
135
  const cap = 20;
130
- const items = value.slice(0, cap).map((item, i) => {
136
+ const items: import("lit-html").TemplateResult[] = value.slice(0, cap).map((item, i) => {
131
137
  if (item === null || item === undefined || typeof item !== "object") {
132
138
  const valText =
133
139
  typeof item === "string" && item.length > 80
@@ -155,10 +161,10 @@ export function renderDataTreeTemplate(
155
161
  }
156
162
 
157
163
  // Object
158
- const obj = /** @type {Record<string, unknown>} */ (value);
164
+ const obj = value as Record<string, unknown>;
159
165
  const keys = Object.keys(obj);
160
166
  const cap = 30;
161
- const items = keys.slice(0, cap).map((key) => {
167
+ const items: import("lit-html").TemplateResult[] = keys.slice(0, cap).map((key) => {
162
168
  const v = obj[key];
163
169
  if (v === null || v === undefined || typeof v !== "object") {
164
170
  const valText =
@@ -0,0 +1,417 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * DnD registration functions — extracted from studio.js (Phase 4). Registers drag-and-drop behavior
4
+ * on layer rows, component cards, and element cards.
5
+ */
6
+
7
+ import {
8
+ draggable,
9
+ dropTargetForElements,
10
+ monitorForElements,
11
+ } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
12
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
13
+ import {
14
+ attachInstruction,
15
+ extractInstruction,
16
+ } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
17
+
18
+ import {
19
+ leftPanel,
20
+ getNodeAtPath,
21
+ parentElementPath,
22
+ childIndex,
23
+ isAncestor,
24
+ renderOnly,
25
+ } from "../store";
26
+ import { transact, transactDoc, mutateMoveNode, mutateInsertNode } from "../tabs/transact";
27
+ import { activeTab } from "../workspace/workspace";
28
+ import { view } from "../view";
29
+ import { componentRegistry, computeRelativePath } from "../files/components";
30
+ import { renderComponentPreview } from "./stylebook-panel";
31
+ import { defaultDef, unsafeTags } from "./shared";
32
+ import type { JxPath } from "../state";
33
+ import type { JxMutableNode } from "@jxsuite/schema/types";
34
+
35
+ interface DragCanDragArgs {
36
+ element: HTMLElement;
37
+ input: { clientX: number; clientY: number };
38
+ }
39
+
40
+ interface DragDropSourceArgs {
41
+ source: { data: Record<string, unknown>; element: HTMLElement };
42
+ }
43
+
44
+ interface DragSelfArgs {
45
+ self: { data: Record<string, unknown> };
46
+ }
47
+
48
+ interface DragMonitorDropArgs {
49
+ source: { data: Record<string, unknown>; element: HTMLElement };
50
+ location: { current: { dropTargets: { data: Record<string, unknown> }[] } };
51
+ }
52
+
53
+ /** Register DnD on layer rows — called from left-panel.js after render */
54
+ export function registerLayersDnD() {
55
+ requestAnimationFrame(() => {
56
+ const container = leftPanel?.querySelector(".layers-container") as HTMLElement | null;
57
+ if (!container) return;
58
+
59
+ (container.querySelectorAll("[data-dnd-row]") as NodeListOf<HTMLElement>).forEach((row) => {
60
+ const rowPath = (row.dataset.path as string)
61
+ .split("/")
62
+ .map((s: string) => (/^\d+$/.test(s) ? parseInt(s) : s)) as JxPath;
63
+ const rowDepth = parseInt(row.dataset.dndDepth as string) || 0;
64
+ const isVoid = row.hasAttribute("data-dnd-void");
65
+ const isExpanded = row.hasAttribute("data-dnd-expanded");
66
+
67
+ const cleanup = combine(
68
+ draggable({
69
+ element: row,
70
+ canDrag({ element: _el, input }: DragCanDragArgs) {
71
+ const target = document.elementFromPoint(input.clientX, input.clientY) as HTMLElement;
72
+ if (target?.closest(".layer-actions")) return false;
73
+ return true;
74
+ },
75
+ getInitialData() {
76
+ return { type: "tree-node", path: rowPath };
77
+ },
78
+ onDragStart() {
79
+ row.classList.add("dragging");
80
+ view.layerDragSourceHeight = row.offsetHeight;
81
+ if (isExpanded) {
82
+ hideDescendantRows(row, container);
83
+ }
84
+ },
85
+ onDrop() {
86
+ row.classList.remove("dragging");
87
+ if (isExpanded) {
88
+ renderOnly("leftPanel");
89
+ }
90
+ },
91
+ }),
92
+ dropTargetForElements({
93
+ element: row,
94
+ canDrop({ source }: DragDropSourceArgs) {
95
+ const srcPath = source.data.path as JxPath | undefined;
96
+ if (srcPath && isAncestor(srcPath, rowPath)) return false;
97
+ return true;
98
+ },
99
+ getData({ input, element }: any) {
100
+ return attachInstruction(
101
+ { path: rowPath },
102
+ {
103
+ input,
104
+ element,
105
+ currentLevel: rowDepth,
106
+ indentPerLevel: 16,
107
+ mode: isExpanded ? "expanded" : "standard",
108
+ block: isVoid ? ["make-child"] : [],
109
+ },
110
+ ) as Record<string | symbol, unknown>;
111
+ },
112
+ onDragEnter({ self }: DragSelfArgs) {
113
+ showLayerDropGap(row, self.data, container);
114
+ },
115
+ onDrag({ self }: DragSelfArgs) {
116
+ showLayerDropGap(row, self.data, container);
117
+ },
118
+ onDragLeave() {
119
+ clearLayerDropGap(container);
120
+ },
121
+ onDrop() {
122
+ clearLayerDropGap(container);
123
+ },
124
+ }),
125
+ );
126
+ view.dndCleanups.push(cleanup);
127
+ });
128
+
129
+ // Global monitor
130
+ const monitorCleanup = monitorForElements({
131
+ onDrop({ source, location }: DragMonitorDropArgs) {
132
+ clearLayerDropGap(container);
133
+ const target = location.current.dropTargets[0];
134
+ if (!target) return;
135
+ const instruction = extractInstruction(target.data);
136
+ if (!instruction || instruction.type === "instruction-blocked") return;
137
+ const srcData = source.data;
138
+ const targetPath = target.data.path as JxPath;
139
+
140
+ // If the source had children, persist collapse at the new location
141
+ const srcRow = srcData.type === "tree-node" && source.element;
142
+ const wasExpanded = srcRow && srcRow.hasAttribute("data-dnd-expanded");
143
+
144
+ applyDropInstruction(instruction, srcData, targetPath);
145
+
146
+ if (wasExpanded) {
147
+ const tab = activeTab.value;
148
+ const newPath = tab?.session.selection;
149
+ if (newPath) {
150
+ const collapsed = view._layersCollapsed || (view._layersCollapsed = new Set());
151
+ collapsed.add(newPath.join("/"));
152
+ }
153
+ }
154
+ },
155
+ });
156
+ view.dndCleanups.push(monitorCleanup);
157
+ });
158
+ }
159
+
160
+ /** Register DnD on component rows — called from renderLeftPanel when tab=components */
161
+ export function registerComponentsDnD() {
162
+ requestAnimationFrame(() => {
163
+ const container = leftPanel?.querySelector(".components-section") as HTMLElement | null;
164
+ if (!container) return;
165
+
166
+ (container.querySelectorAll("[data-component-tag]") as NodeListOf<HTMLElement>).forEach(
167
+ (row) => {
168
+ const tagName = row.dataset.componentTag;
169
+ if (!tagName) return;
170
+ const comp = componentRegistry.find(
171
+ (c: import("../files/components.js").ComponentEntry) => c.tagName === tagName,
172
+ );
173
+ if (!comp) return;
174
+
175
+ // Fill preview with live rendered component
176
+ const preview = row.querySelector(".element-card-preview");
177
+ if (preview && !preview.querySelector(tagName)) {
178
+ renderComponentPreview(comp).then((el: HTMLElement) => {
179
+ preview.textContent = "";
180
+ preview.appendChild(el);
181
+ });
182
+ }
183
+
184
+ const instanceDef = {
185
+ tagName: comp.tagName,
186
+ $props: comp.props
187
+ ? Object.fromEntries(
188
+ comp.props.map(
189
+ (/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
190
+ p.name,
191
+ p.default !== undefined ? p.default : "",
192
+ ],
193
+ ),
194
+ )
195
+ : {},
196
+ };
197
+ const cleanup = draggable({
198
+ element: row,
199
+ getInitialData() {
200
+ return { type: "block", fragment: structuredClone(instanceDef) };
201
+ },
202
+ });
203
+ view.dndCleanups.push(cleanup);
204
+ },
205
+ );
206
+ });
207
+ }
208
+
209
+ /** Register DnD on element (HTML block) rows */
210
+ export function registerElementsDnD() {
211
+ requestAnimationFrame(() => {
212
+ const container = leftPanel?.querySelector(".panel-body") as HTMLElement | null;
213
+ if (!container) return;
214
+ (container.querySelectorAll("[data-block-tag]") as NodeListOf<HTMLElement>).forEach((row) => {
215
+ const tag = row.dataset.blockTag as string;
216
+ const preview = row.querySelector(".element-card-preview");
217
+ if (preview && !preview.firstChild) {
218
+ const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
219
+ el.textContent = tag;
220
+ preview.appendChild(el);
221
+ }
222
+ const def = defaultDef(tag);
223
+ const cleanup = draggable({
224
+ element: row,
225
+ getInitialData() {
226
+ return { type: "block", fragment: structuredClone(def) };
227
+ },
228
+ });
229
+ view.dndCleanups.push(cleanup);
230
+ });
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Hide descendant rows of the dragged item so it appears collapsed during drag.
236
+ *
237
+ * @param {HTMLElement} parentRow
238
+ * @param {HTMLElement} container
239
+ */
240
+ function hideDescendantRows(parentRow: HTMLElement, container: HTMLElement) {
241
+ const prefix = parentRow.dataset.path + "/";
242
+ const rows = container.querySelectorAll(".layers-tree .layer-row");
243
+ for (const r of rows) {
244
+ if ((r as HTMLElement).dataset.path?.startsWith(prefix)) {
245
+ (r as HTMLElement).style.display = "none";
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * @param {HTMLElement} rowEl
252
+ * @param {Record<string, unknown>} data
253
+ * @param {HTMLElement} container
254
+ */
255
+ export function showLayerDropGap(
256
+ rowEl: HTMLElement,
257
+ data: Record<string, unknown>,
258
+ container: HTMLElement,
259
+ ) {
260
+ const instruction = extractInstruction(data);
261
+
262
+ // Clear previous drop-target highlight
263
+ if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
264
+ view._currentDropTargetRow.classList.remove("drop-target");
265
+ }
266
+
267
+ if (!instruction || instruction.type === "instruction-blocked") {
268
+ clearLayerDropGap(container);
269
+ return;
270
+ }
271
+
272
+ if (instruction.type === "make-child") {
273
+ clearLayerDropGap(container);
274
+ rowEl.classList.add("drop-target");
275
+ view._currentDropTargetRow = rowEl;
276
+ return;
277
+ }
278
+
279
+ rowEl.classList.remove("drop-target");
280
+ view._currentDropTargetRow = rowEl;
281
+
282
+ // Shift rows to create gap
283
+ const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
284
+ const targetIdx = rows.indexOf(rowEl);
285
+ const gap = view.layerDragSourceHeight;
286
+
287
+ for (let i = 0; i < rows.length; i++) {
288
+ if ((rows[i] as HTMLElement).classList.contains("dragging")) continue;
289
+ if (instruction.type === "reorder-above") {
290
+ (rows[i] as HTMLElement).style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
291
+ } else {
292
+ (rows[i] as HTMLElement).style.transform = i > targetIdx ? `translateY(${gap}px)` : "";
293
+ }
294
+ }
295
+ }
296
+
297
+ /** @param {HTMLElement} container */
298
+ export function clearLayerDropGap(container: HTMLElement) {
299
+ if (view._currentDropTargetRow) {
300
+ view._currentDropTargetRow.classList.remove("drop-target");
301
+ view._currentDropTargetRow = null;
302
+ }
303
+ const rows = container.querySelectorAll(".layers-tree .layer-row");
304
+ for (const r of rows) (r as HTMLElement).style.transform = "";
305
+ }
306
+
307
+ /**
308
+ * Apply a DnD instruction to the state
309
+ *
310
+ * @param {{ type: string }} instruction
311
+ * @param {Record<string, unknown>} srcData
312
+ * @param {JxPath} targetPath
313
+ */
314
+ export function applyDropInstruction(
315
+ instruction: { type: string },
316
+ srcData: Record<string, unknown>,
317
+ targetPath: JxPath,
318
+ ) {
319
+ const doc = activeTab.value?.doc.document as JxMutableNode;
320
+ if (srcData.type === "tree-node") {
321
+ const fromPath = srcData.path as JxPath;
322
+ const targetParent = parentElementPath(targetPath) as JxPath;
323
+ const targetIdx = childIndex(targetPath) as number;
324
+
325
+ switch (instruction.type) {
326
+ case "reorder-above":
327
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetParent, targetIdx));
328
+ break;
329
+ case "reorder-below":
330
+ transactDoc(activeTab.value, (t) =>
331
+ mutateMoveNode(t, fromPath, targetParent, targetIdx + 1),
332
+ );
333
+ break;
334
+ case "make-child": {
335
+ const target = getNodeAtPath(doc, targetPath);
336
+ const len = target?.children?.length || 0;
337
+ transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
338
+ break;
339
+ }
340
+ }
341
+ } else if (srcData.type === "block") {
342
+ const targetParent = parentElementPath(targetPath) as JxPath;
343
+ const targetIdx = childIndex(targetPath) as number;
344
+
345
+ switch (instruction.type) {
346
+ case "reorder-above":
347
+ transactDoc(activeTab.value, (t) =>
348
+ mutateInsertNode(
349
+ t,
350
+ targetParent,
351
+ targetIdx,
352
+ structuredClone(srcData.fragment as JxMutableNode),
353
+ ),
354
+ );
355
+ break;
356
+ case "reorder-below":
357
+ transactDoc(activeTab.value, (t) =>
358
+ mutateInsertNode(
359
+ t,
360
+ targetParent,
361
+ targetIdx + 1,
362
+ structuredClone(srcData.fragment as JxMutableNode),
363
+ ),
364
+ );
365
+ break;
366
+ case "make-child": {
367
+ const target = getNodeAtPath(doc, targetPath);
368
+ const len = target?.children?.length || 0;
369
+ transactDoc(activeTab.value, (t) =>
370
+ mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment as JxMutableNode)),
371
+ );
372
+ break;
373
+ }
374
+ }
375
+
376
+ // Auto-import to $elements if the dropped block is a custom component
377
+ const fragment = srcData.fragment as JxMutableNode | undefined;
378
+ const tag = fragment?.tagName;
379
+ if (tag && tag.includes("-")) {
380
+ const comp = componentRegistry.find(
381
+ (c: import("../files/components.js").ComponentEntry) => c.tagName === tag,
382
+ );
383
+ if (comp) {
384
+ const tab = activeTab.value;
385
+ const elements = tab?.doc.document?.$elements || [];
386
+ if (comp.source === "npm") {
387
+ const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
388
+ if (!specifier) return;
389
+ const alreadyImported = elements.some(
390
+ (e: JxMutableNode | string | { $ref: string }) => e === specifier || e === comp.package,
391
+ );
392
+ if (!alreadyImported) {
393
+ transact(activeTab.value, (doc: JxMutableNode) => {
394
+ if (!doc.$elements) doc.$elements = [];
395
+ doc.$elements.push(specifier);
396
+ });
397
+ }
398
+ } else {
399
+ const alreadyImported = elements.some((e: JxMutableNode | string | { $ref: string }) => {
400
+ const ref = typeof e === "object" && e !== null ? e.$ref : undefined;
401
+ return (
402
+ ref &&
403
+ (ref === `./${comp.path}` || ref.endsWith(comp.path.split("/").pop() as string))
404
+ );
405
+ });
406
+ if (!alreadyImported) {
407
+ const relPath = computeRelativePath(tab?.documentPath ?? null, comp.path);
408
+ transact(activeTab.value, (doc: JxMutableNode) => {
409
+ if (!doc.$elements) doc.$elements = [];
410
+ doc.$elements.push({ $ref: relPath });
411
+ });
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }