@jxsuite/studio 0.28.2 → 0.28.4

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 (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -1,16 +1,15 @@
1
1
  /// <reference lib="dom" />
2
- import { renderOnly, getNodeAtPath, parentElementPath, childIndex, canvasPanels } from "../store";
2
+ import { canvasPanels, childIndex, getNodeAtPath, parentElementPath, renderOnly } from "../store";
3
3
  import { activeTab } from "../workspace/workspace";
4
- import { transactDoc, mutateInsertNode, mutateUpdateProperty } from "../tabs/transact";
4
+ import { mutateInsertNode, mutateUpdateProperty, transactDoc } from "../tabs/transact";
5
5
  import { view } from "../view";
6
- import { startEditing, isEditableBlock } from "./inline-edit";
6
+ import { isEditableBlock, startEditing } from "./inline-edit";
7
7
  import { restoreTemplateExpressions } from "../utils/edit-display";
8
8
  import { renderBlockActionBar } from "../panels/block-action-bar";
9
9
  import { defaultDef } from "../panels/shared";
10
10
  import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
11
11
 
12
- import type { JxContentResult } from "./inline-edit";
13
- import type { SlashCommand } from "./inline-edit";
12
+ import type { JxContentResult, SlashCommand } from "./inline-edit";
14
13
  import type { JxPath } from "../state";
15
14
  import type { JxMutableNode } from "@jxsuite/schema/types";
16
15
 
@@ -22,8 +21,8 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
22
21
  */
23
22
  export function enterInlineEdit(el: HTMLElement, path: JxPath) {
24
23
  // Restore raw template expressions before editing.
25
- // prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
26
- // revert so the user edits the real syntax and commits it back intact.
24
+ // PrepareForEditMode renders ${expr} as ❪ expr ❫ for display;
25
+ // Revert so the user edits the real syntax and commits it back intact.
27
26
  restoreTemplateExpressions(el);
28
27
 
29
28
  // Hide overlays while editing
@@ -40,66 +39,34 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
40
39
  ) {
41
40
  const node = getNodeAtPath(activeTab.value!.doc.document, commitPath);
42
41
  if (children) {
43
- if (node && JSON.stringify(node.children) === JSON.stringify(children)) return;
42
+ if (node && JSON.stringify(node.children) === JSON.stringify(children)) {
43
+ return;
44
+ }
44
45
  transactDoc(activeTab.value, (t) => {
45
- mutateUpdateProperty(t, commitPath, "textContent", undefined);
46
+ mutateUpdateProperty(t, commitPath, "textContent");
46
47
  mutateUpdateProperty(t, commitPath, "children", children);
47
48
  });
48
49
  } else if (textContent != null) {
49
- if (node && node.textContent === textContent && !node.children) return;
50
+ if (node && node.textContent === textContent && !node.children) {
51
+ return;
52
+ }
50
53
  transactDoc(activeTab.value, (t) => {
51
- mutateUpdateProperty(t, commitPath, "children", undefined);
54
+ mutateUpdateProperty(t, commitPath, "children");
52
55
  mutateUpdateProperty(t, commitPath, "textContent", textContent);
53
56
  });
54
57
  }
55
58
  },
56
59
 
57
- onSplit(splitPath: JxPath, before: JxContentResult, after: JxContentResult) {
58
- const tag = "p";
59
-
60
- // Insert new element after with "after" content
61
- const parentPath = parentElementPath(splitPath) as JxPath;
62
- const idx = childIndex(splitPath) as number;
63
- const newNode: JxMutableNode = { tagName: tag };
64
- if (after.textContent != null) {
65
- newNode.textContent = after.textContent;
66
- } else if (after.children) {
67
- newNode.children = after.children;
68
- } else {
69
- newNode.textContent = "";
60
+ onEnd() {
61
+ if (view.inlineEditCleanup) {
62
+ view.inlineEditCleanup();
63
+ view.inlineEditCleanup = null;
70
64
  }
71
-
72
- const newPath = [...parentPath, "children", idx + 1];
73
-
74
- transactDoc(activeTab.value, (t) => {
75
- if (before.textContent != null) {
76
- mutateUpdateProperty(t, splitPath, "children", undefined);
77
- mutateUpdateProperty(t, splitPath, "textContent", before.textContent);
78
- } else if (before.children) {
79
- mutateUpdateProperty(t, splitPath, "textContent", undefined);
80
- mutateUpdateProperty(t, splitPath, "children", before.children);
81
- }
82
- mutateInsertNode(t, parentPath, idx + 1, newNode);
83
- t.session.selection = newPath;
84
- });
85
-
86
- // Re-enter editing on the new element after render
87
- requestAnimationFrame(() => {
88
- const activePanel = getActivePanel();
89
- if (activePanel) {
90
- const newEl = findCanvasElement(newPath, activePanel.canvas);
91
- if (newEl && isEditableBlock(newEl)) {
92
- enterInlineEdit(newEl, newPath);
93
- // Place cursor at start of new element
94
- const sel = window.getSelection();
95
- const range = document.createRange();
96
- range.selectNodeContents(newEl);
97
- range.collapse(true);
98
- sel?.removeAllRanges();
99
- sel?.addRange(range);
100
- }
101
- }
102
- });
65
+ for (const p of canvasPanels) {
66
+ p.overlay.style.display = "";
67
+ p.overlayClk.style.pointerEvents = "";
68
+ }
69
+ renderOnly("overlays");
103
70
  },
104
71
 
105
72
  onInsert(afterPath: JxPath, cmd: SlashCommand, commitData: JxContentResult | undefined) {
@@ -119,12 +86,12 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
119
86
  if (isEmpty) {
120
87
  transactDoc(activeTab.value, (t) => {
121
88
  mutateUpdateProperty(t, afterPath, "tagName", cmd.tag);
122
- mutateUpdateProperty(t, afterPath, "children", undefined);
89
+ mutateUpdateProperty(t, afterPath, "children");
123
90
  const def = defaultDef(cmd.tag);
124
91
  if (def.textContent && def.textContent !== "Paragraph text") {
125
92
  mutateUpdateProperty(t, afterPath, "textContent", def.textContent);
126
93
  } else {
127
- mutateUpdateProperty(t, afterPath, "textContent", undefined);
94
+ mutateUpdateProperty(t, afterPath, "textContent");
128
95
  }
129
96
  t.session.selection = afterPath;
130
97
  });
@@ -132,9 +99,9 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
132
99
  requestAnimationFrame(() => {
133
100
  const activePanel = getActivePanel();
134
101
  if (activePanel) {
135
- const el = findCanvasElement(afterPath, activePanel.canvas);
136
- if (el && isEditableBlock(el)) {
137
- enterInlineEdit(el, afterPath);
102
+ const nextEl = findCanvasElement(afterPath, activePanel.canvas);
103
+ if (nextEl && isEditableBlock(nextEl)) {
104
+ enterInlineEdit(nextEl, afterPath);
138
105
  }
139
106
  }
140
107
  });
@@ -150,10 +117,10 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
150
117
  transactDoc(activeTab.value, (t) => {
151
118
  if (commitData) {
152
119
  if (commitData.children) {
153
- mutateUpdateProperty(t, afterPath, "textContent", undefined);
120
+ mutateUpdateProperty(t, afterPath, "textContent");
154
121
  mutateUpdateProperty(t, afterPath, "children", commitData.children);
155
122
  } else if (commitData.textContent != null) {
156
- mutateUpdateProperty(t, afterPath, "children", undefined);
123
+ mutateUpdateProperty(t, afterPath, "children");
157
124
  mutateUpdateProperty(t, afterPath, "textContent", commitData.textContent);
158
125
  }
159
126
  }
@@ -173,16 +140,52 @@ export function enterInlineEdit(el: HTMLElement, path: JxPath) {
173
140
  });
174
141
  },
175
142
 
176
- onEnd() {
177
- if (view.inlineEditCleanup) {
178
- view.inlineEditCleanup();
179
- view.inlineEditCleanup = null;
180
- }
181
- for (const p of canvasPanels) {
182
- p.overlay.style.display = "";
183
- p.overlayClk.style.pointerEvents = "";
143
+ onSplit(splitPath: JxPath, before: JxContentResult, after: JxContentResult) {
144
+ const tag = "p";
145
+
146
+ // Insert new element after with "after" content
147
+ const parentPath = parentElementPath(splitPath) as JxPath;
148
+ const idx = childIndex(splitPath) as number;
149
+ const newNode: JxMutableNode = { tagName: tag };
150
+ if (after.textContent != null) {
151
+ newNode.textContent = after.textContent;
152
+ } else if (after.children) {
153
+ newNode.children = after.children;
154
+ } else {
155
+ newNode.textContent = "";
184
156
  }
185
- renderOnly("overlays");
157
+
158
+ const newPath = [...parentPath, "children", idx + 1];
159
+
160
+ transactDoc(activeTab.value, (t) => {
161
+ if (before.textContent != null) {
162
+ mutateUpdateProperty(t, splitPath, "children");
163
+ mutateUpdateProperty(t, splitPath, "textContent", before.textContent);
164
+ } else if (before.children) {
165
+ mutateUpdateProperty(t, splitPath, "textContent");
166
+ mutateUpdateProperty(t, splitPath, "children", before.children);
167
+ }
168
+ mutateInsertNode(t, parentPath, idx + 1, newNode);
169
+ t.session.selection = newPath;
170
+ });
171
+
172
+ // Re-enter editing on the new element after render
173
+ requestAnimationFrame(() => {
174
+ const activePanel = getActivePanel();
175
+ if (activePanel) {
176
+ const newEl = findCanvasElement(newPath, activePanel.canvas);
177
+ if (newEl && isEditableBlock(newEl)) {
178
+ enterInlineEdit(newEl, newPath);
179
+ // Place cursor at start of new element
180
+ const sel = window.getSelection();
181
+ const range = document.createRange();
182
+ range.selectNodeContents(newEl);
183
+ range.collapse(true);
184
+ sel?.removeAllRanges();
185
+ sel?.addRange(range);
186
+ }
187
+ }
188
+ });
186
189
  },
187
190
  });
188
191
 
@@ -1,24 +1,24 @@
1
1
  /// <reference lib="dom" />
2
2
  // ─── Clipboard & Context Menu ─────────────────────────────────────────────────
3
3
  import { html } from "lit-html";
4
+ import { jsonClone } from "../utils/studio-utils";
4
5
  import { ref } from "lit-html/directives/ref.js";
5
6
  import { htmlToJx } from "@jxsuite/parser/html-to-jx";
6
- import { getNodeAtPath, parentElementPath, childIndex } from "../store";
7
+ import { childIndex, getNodeAtPath, parentElementPath } from "../store";
7
8
  import { activeTab, workspace } from "../workspace/workspace";
8
9
  import {
9
- transactDoc,
10
+ mutateDuplicateNode,
10
11
  mutateInsertNode,
11
12
  mutateRemoveNode,
12
- mutateDuplicateNode,
13
- mutateWrapNode,
14
13
  mutateReplaceStyle,
14
+ mutateWrapNode,
15
+ transactDoc,
15
16
  } from "../tabs/transact";
16
17
  import { statusMessage } from "../panels/statusbar";
17
18
  import { convertToComponent } from "./convert-to-component";
18
19
  import { convertToRepeater } from "./convert-to-repeater";
19
20
  import { componentRegistry } from "../files/components";
20
21
  import { renderPopover } from "../ui/layers";
21
- import { startLayerTitleEdit } from "../panels/layers-panel";
22
22
 
23
23
  import type { JxPath } from "../state";
24
24
  import type { JxMutableNode } from "@jxsuite/schema/types";
@@ -30,25 +30,33 @@ type JxNode = JxMutableNode;
30
30
  const JX_MIME = "web application/jx+json";
31
31
 
32
32
  /** @param {JxNode | string} node */
33
- function nodeToHtml(node: JxNode | string) {
34
- if (typeof node === "string") return node;
33
+ function nodeToHtml(node: JxNode | string): string {
34
+ if (typeof node === "string") {
35
+ return node;
36
+ }
35
37
  const tag = node.tagName || "div";
36
38
  let attrs = "";
37
39
  if (node.attributes) {
38
40
  for (const [k, v] of Object.entries(node.attributes)) {
39
- attrs += v === "" ? ` ${k}` : ` ${k}="${v.replace(/"/g, "&quot;")}"`;
41
+ // Bound ($ref) attribute values have no static HTML representation — skip.
42
+ if (typeof v === "object") {
43
+ continue;
44
+ }
45
+ attrs += v === "" ? ` ${k}` : ` ${k}="${String(v).replaceAll('"', "&quot;")}"`;
40
46
  }
41
47
  }
42
48
  if (node.style) {
43
49
  const css = Object.entries(node.style)
44
50
  .map(([k, v]) => `${k}:${v}`)
45
51
  .join(";");
46
- if (css) attrs += ` style="${css.replace(/"/g, "&quot;")}"`;
52
+ if (css) {
53
+ attrs += ` style="${css.replaceAll('"', "&quot;")}"`;
54
+ }
47
55
  }
48
56
  let inner = "";
49
- if (node.textContent) {
57
+ if (typeof node.textContent === "string") {
50
58
  inner = node.textContent;
51
- } else if (node.children) {
59
+ } else if (Array.isArray(node.children)) {
52
60
  inner = node.children.map((c) => nodeToHtml(c)).join("");
53
61
  }
54
62
  return `<${tag}${attrs}>${inner}</${tag}>`;
@@ -73,7 +81,7 @@ async function writeToClipboard(json: Record<string, unknown>) {
73
81
  try {
74
82
  await navigator.clipboard.writeText(JSON.stringify(json));
75
83
  } catch {
76
- // clipboard API unavailable — workspace.clipboard is the fallback
84
+ // Clipboard API unavailable — workspace.clipboard is the fallback
77
85
  }
78
86
  }
79
87
  }
@@ -99,7 +107,9 @@ async function readFromClipboard() {
99
107
  const jxNodes = nodes.map((n) =>
100
108
  typeof n === "string" ? { tagName: "p", textContent: n } : n,
101
109
  ) as JxNode[];
102
- if (jxNodes.length > 0) return jxNodes;
110
+ if (jxNodes.length > 0) {
111
+ return jxNodes;
112
+ }
103
113
  }
104
114
  if (item.types.includes("text/plain")) {
105
115
  const blob = await item.getType("text/plain");
@@ -107,17 +117,21 @@ async function readFromClipboard() {
107
117
  // Try parsing as Jx JSON
108
118
  try {
109
119
  const parsed = JSON.parse(text);
110
- if (parsed && parsed.tagName) return [parsed];
120
+ if (parsed && parsed.tagName) {
121
+ return [parsed];
122
+ }
111
123
  } catch {
112
- // plain text → paragraph node
124
+ // Plain text → paragraph node
125
+ }
126
+ if (text.trim()) {
127
+ return [{ tagName: "p", textContent: text.trim() }];
113
128
  }
114
- if (text.trim()) return [{ tagName: "p", textContent: text.trim() }];
115
129
  }
116
130
  }
117
131
  } catch {
118
- // clipboard API unavailable — use workspace fallback
132
+ // Clipboard API unavailable — use workspace fallback
119
133
  if (workspace.clipboard) {
120
- return [JSON.parse(JSON.stringify(workspace.clipboard))];
134
+ return [jsonClone(workspace.clipboard)];
121
135
  }
122
136
  }
123
137
  return null;
@@ -127,21 +141,29 @@ async function readFromClipboard() {
127
141
 
128
142
  export async function copyNode() {
129
143
  const tab = activeTab.value;
130
- if (!tab?.session.selection) return;
144
+ if (!tab?.session.selection) {
145
+ return;
146
+ }
131
147
  const node = getNodeAtPath(tab.doc.document, tab.session.selection);
132
- if (!node) return;
133
- const json = JSON.parse(JSON.stringify(node));
148
+ if (!node) {
149
+ return;
150
+ }
151
+ const json = jsonClone(node);
134
152
  await writeToClipboard(json);
135
153
  statusMessage("Copied");
136
154
  }
137
155
 
138
156
  export async function cutNode() {
139
157
  const tab = activeTab.value;
140
- if (!tab?.session.selection || tab.session.selection.length < 2) return;
158
+ if (!tab?.session.selection || tab.session.selection.length < 2) {
159
+ return;
160
+ }
141
161
  const sel = tab.session.selection;
142
162
  const node = getNodeAtPath(tab.doc.document, sel);
143
- if (!node) return;
144
- const json = JSON.parse(JSON.stringify(node));
163
+ if (!node) {
164
+ return;
165
+ }
166
+ const json = jsonClone(node);
145
167
  await writeToClipboard(json);
146
168
  transactDoc(tab, (t) => mutateRemoveNode(t, sel));
147
169
  statusMessage("Cut");
@@ -149,14 +171,20 @@ export async function cutNode() {
149
171
 
150
172
  export async function pasteNode() {
151
173
  const tab = activeTab.value;
152
- if (!tab) return;
174
+ if (!tab) {
175
+ return;
176
+ }
153
177
 
154
178
  const nodes = await readFromClipboard();
155
- if (!nodes || nodes.length === 0) return;
179
+ if (!nodes || nodes.length === 0) {
180
+ return;
181
+ }
156
182
 
157
183
  const pPath = tab.session.selection || [];
158
184
  const parent = getNodeAtPath(tab.doc.document, pPath);
159
- if (!parent) return;
185
+ if (!parent) {
186
+ return;
187
+ }
160
188
 
161
189
  if (tab.session.selection && tab.session.selection.length >= 2) {
162
190
  const pp = parentElementPath(tab.session.selection) as JxPath;
@@ -167,7 +195,7 @@ export async function pasteNode() {
167
195
  }
168
196
  });
169
197
  } else {
170
- const idx = parent.children ? parent.children.length : 0;
198
+ const idx = Array.isArray(parent.children) ? parent.children.length : 0;
171
199
  transactDoc(tab, (t) => {
172
200
  for (let i = 0; i < nodes.length; i++) {
173
201
  mutateInsertNode(t, pPath, idx + i, nodes[i]);
@@ -179,18 +207,26 @@ export async function pasteNode() {
179
207
 
180
208
  export function copyStyles() {
181
209
  const tab = activeTab.value;
182
- if (!tab?.session.selection) return;
210
+ if (!tab?.session.selection) {
211
+ return;
212
+ }
183
213
  const node = getNodeAtPath(tab.doc.document, tab.session.selection);
184
- if (!node?.style) return;
185
- workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
214
+ if (!node?.style) {
215
+ return;
216
+ }
217
+ workspace.styleClipboard = jsonClone(node.style);
186
218
  statusMessage("Styles copied");
187
219
  }
188
220
 
189
221
  export function pasteStyles() {
190
- if (!workspace.styleClipboard) return;
222
+ if (!workspace.styleClipboard) {
223
+ return;
224
+ }
191
225
  const tab = activeTab.value;
192
- if (!tab?.session.selection) return;
193
- const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
226
+ if (!tab?.session.selection) {
227
+ return;
228
+ }
229
+ const style = jsonClone(workspace.styleClipboard);
194
230
  const sel = tab.session.selection as JxPath;
195
231
  transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
196
232
  statusMessage("Styles pasted");
@@ -225,82 +261,91 @@ export function showContextMenu(
225
261
  dismissContextMenu();
226
262
 
227
263
  const tab = activeTab.value;
228
- if (!tab) return;
264
+ if (!tab) {
265
+ return;
266
+ }
229
267
  const node = getNodeAtPath(tab.doc.document, path);
230
- if (!node) return;
268
+ if (!node) {
269
+ return;
270
+ }
231
271
 
232
272
  // Select the node
233
273
  tab.session.selection = path;
234
274
 
235
- /** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
236
- const items = [];
275
+ const items: { label: string; action?: () => void | Promise<void>; danger?: boolean }[] = [
276
+ { action: () => copyNode(), label: "Copy" },
277
+ ];
237
278
 
238
- items.push({ label: "Copy", action: () => copyNode() });
239
279
  if (path.length >= 2) {
240
- items.push({ label: "Cut", action: () => cutNode() });
280
+ items.push({ action: () => cutNode(), label: "Cut" });
241
281
  items.push({
242
- label: "Duplicate",
243
282
  action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
283
+ label: "Duplicate",
244
284
  });
245
285
  if (node.style) {
286
+ const nodeStyle = node.style;
246
287
  items.push({
247
- label: "Copy styles",
248
288
  action: () => {
249
- workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
289
+ workspace.styleClipboard = jsonClone(nodeStyle);
250
290
  statusMessage("Styles copied");
251
291
  },
292
+ label: "Copy styles",
252
293
  });
253
294
  }
254
295
  if (workspace.styleClipboard) {
255
296
  items.push({
256
- label: "Paste styles",
257
297
  action: () => {
258
- if (!workspace.styleClipboard) return;
259
- const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
298
+ if (!workspace.styleClipboard) {
299
+ return;
300
+ }
301
+ const style = jsonClone(workspace.styleClipboard);
260
302
  transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, path, style));
261
303
  statusMessage("Styles pasted");
262
304
  },
305
+ label: "Paste styles",
263
306
  });
264
307
  }
265
- items.push({ label: "—" }); // separator
308
+ items.push({ label: "—" }); // Separator
266
309
  items.push({
267
- label: "Insert before",
268
310
  action: () => {
269
311
  const pp = parentElementPath(path) as JxPath;
270
312
  const idx = childIndex(path) as number;
271
313
  transactDoc(activeTab.value, (t) =>
272
- mutateInsertNode(t, pp, idx, { tagName: "p", children: [] }),
314
+ mutateInsertNode(t, pp, idx, { children: [], tagName: "p" }),
273
315
  );
274
316
  },
317
+ label: "Insert before",
275
318
  });
276
319
  items.push({
277
- label: "Insert after",
278
320
  action: () => {
279
321
  const pp = parentElementPath(path) as JxPath;
280
322
  const idx = childIndex(path) as number;
281
323
  transactDoc(activeTab.value, (t) =>
282
- mutateInsertNode(t, pp, idx + 1, { tagName: "p", children: [] }),
324
+ mutateInsertNode(t, pp, idx + 1, { children: [], tagName: "p" }),
283
325
  );
284
326
  },
327
+ label: "Insert after",
285
328
  });
286
329
  items.push({
287
- label: "Wrap in Div",
288
330
  action: () => transactDoc(activeTab.value, (t) => mutateWrapNode(t, path)),
331
+ label: "Wrap in Div",
289
332
  });
290
333
  // Don't show Repeat if already inside a repeater (path ends with "children", "map")
291
- if (
292
- !(path.length >= 2 && path[path.length - 2] === "children" && path[path.length - 1] === "map")
293
- ) {
334
+ if (!(path.length >= 2 && path.at(-2) === "children" && path.at(-1) === "map")) {
294
335
  items.push({
295
- label: "Repeat...",
296
336
  action: () => convertToRepeater(),
337
+ label: "Repeat...",
297
338
  });
298
339
  }
299
340
  items.push({
300
- label: "Set Title",
301
- action: () => {
302
- if (opts.rerender) startLayerTitleEdit(path, opts.rerender);
341
+ action: async () => {
342
+ if (opts.rerender) {
343
+ // Lazy import breaks the context-menu ↔ layers-panel module cycle
344
+ const { startLayerTitleEdit } = await import("../panels/layers-panel");
345
+ startLayerTitleEdit(path, opts.rerender);
346
+ }
303
347
  },
348
+ label: "Set Title",
304
349
  });
305
350
  if (node.tagName) {
306
351
  const isComponent =
@@ -313,31 +358,32 @@ export function showContextMenu(
313
358
  (/** @type {{ tagName: string; path: string }} */ c) => c.tagName === node.tagName,
314
359
  );
315
360
  items.push({
316
- label: "Edit Component",
317
361
  action: () => opts.onEditComponent?.(comp?.path as string),
362
+ label: "Edit Component",
318
363
  });
319
364
  } else if (!isComponent) {
320
365
  items.push({
321
- label: "Convert to Component",
322
366
  action: () => convertToComponent(),
367
+ label: "Convert to Component",
323
368
  });
324
369
  }
325
370
  }
326
- items.push({ label: "—" }); // separator
371
+ items.push({ label: "—" }); // Separator
327
372
  items.push({
328
- label: "Delete",
329
373
  action: () => transactDoc(activeTab.value, (t) => mutateRemoveNode(t, path)),
330
374
  danger: true,
375
+ label: "Delete",
331
376
  });
332
377
  }
333
378
  if (path.length >= 2) {
334
379
  items.push({ label: "—" });
335
380
  items.push({
336
- label: "Paste inside",
337
381
  action: async () => {
338
382
  const nodes = await readFromClipboard();
339
- if (!nodes || nodes.length === 0) return;
340
- const idx = node.children ? node.children.length : 0;
383
+ if (!nodes || nodes.length === 0) {
384
+ return;
385
+ }
386
+ const idx = Array.isArray(node.children) ? node.children.length : 0;
341
387
  transactDoc(activeTab.value, (t) => {
342
388
  for (let i = 0; i < nodes.length; i++) {
343
389
  mutateInsertNode(t, path, idx + i, nodes[i]);
@@ -345,12 +391,14 @@ export function showContextMenu(
345
391
  });
346
392
  statusMessage("Pasted");
347
393
  },
394
+ label: "Paste inside",
348
395
  });
349
396
  items.push({
350
- label: "Paste after",
351
397
  action: async () => {
352
398
  const nodes = await readFromClipboard();
353
- if (!nodes || nodes.length === 0) return;
399
+ if (!nodes || nodes.length === 0) {
400
+ return;
401
+ }
354
402
  const pp = parentElementPath(path) as JxPath;
355
403
  const idx = childIndex(path) as number;
356
404
  transactDoc(activeTab.value, (t) => {
@@ -360,6 +408,7 @@ export function showContextMenu(
360
408
  });
361
409
  statusMessage("Pasted");
362
410
  },
411
+ label: "Paste after",
363
412
  });
364
413
  }
365
414
 
@@ -371,13 +420,18 @@ export function showContextMenu(
371
420
  open
372
421
  style="position:fixed;z-index:10000;left:${x}px;top:${y}px"
373
422
  ${ref((el) => {
374
- if (!el) return;
423
+ if (!el) {
424
+ return;
425
+ }
375
426
  requestAnimationFrame(() => {
376
427
  const popover = el as HTMLElement;
377
428
  const menuRect = popover.getBoundingClientRect();
378
- if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
379
- if (y + menuRect.height > window.innerHeight)
429
+ if (x + menuRect.width > window.innerWidth) {
430
+ x = window.innerWidth - menuRect.width - 4;
431
+ }
432
+ if (y + menuRect.height > window.innerHeight) {
380
433
  y = window.innerHeight - menuRect.height - 4;
434
+ }
381
435
  popover.style.left = `${x}px`;
382
436
  popover.style.top = `${y}px`;
383
437
  });