@jxsuite/studio 0.14.0 → 0.15.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.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,13 +1,14 @@
1
1
  // ─── Clipboard & Context Menu ─────────────────────────────────────────────────
2
2
  import { html, render as litRender } from "lit-html";
3
3
  import { getNodeAtPath, parentElementPath, childIndex } from "../store.js";
4
- import { activeTab } from "../workspace/workspace.js";
4
+ import { activeTab, workspace } from "../workspace/workspace.js";
5
5
  import {
6
6
  transactDoc,
7
7
  mutateInsertNode,
8
8
  mutateRemoveNode,
9
9
  mutateDuplicateNode,
10
10
  mutateWrapNode,
11
+ mutateReplaceStyle,
11
12
  } from "../tabs/transact.js";
12
13
  import { statusMessage } from "../panels/statusbar.js";
13
14
  import { convertToComponent } from "./convert-to-component.js";
@@ -21,9 +22,6 @@ import { componentRegistry } from "../files/components.js";
21
22
  * @typedef {import("../state.js").JxNode} JxNode
22
23
  */
23
24
 
24
- /** @type {JxNode | null} */
25
- let clipboard = null;
26
-
27
25
  // ─── Clipboard ────────────────────────────────────────────────────────────────
28
26
 
29
27
  export function copyNode() {
@@ -31,7 +29,7 @@ export function copyNode() {
31
29
  if (!tab?.session.selection) return;
32
30
  const node = getNodeAtPath(tab.doc.document, tab.session.selection);
33
31
  if (!node) return;
34
- clipboard = structuredClone(node);
32
+ workspace.clipboard = structuredClone(node);
35
33
  statusMessage("Copied");
36
34
  }
37
35
 
@@ -41,16 +39,16 @@ export function cutNode() {
41
39
  const sel = tab.session.selection;
42
40
  const node = getNodeAtPath(tab.doc.document, sel);
43
41
  if (!node) return;
44
- clipboard = structuredClone(node);
42
+ workspace.clipboard = structuredClone(node);
45
43
  transactDoc(tab, (t) => mutateRemoveNode(t, sel));
46
44
  statusMessage("Cut");
47
45
  }
48
46
 
49
47
  export function pasteNode() {
50
- if (!clipboard) return;
48
+ if (!workspace.clipboard) return;
51
49
  const tab = activeTab.value;
52
50
  if (!tab) return;
53
- const clip = clipboard;
51
+ const clip = workspace.clipboard;
54
52
  const pPath = tab.session.selection || [];
55
53
  const parent = getNodeAtPath(tab.doc.document, pPath);
56
54
  if (!parent) return;
@@ -66,6 +64,25 @@ export function pasteNode() {
66
64
  statusMessage("Pasted");
67
65
  }
68
66
 
67
+ export function copyStyles() {
68
+ const tab = activeTab.value;
69
+ if (!tab?.session.selection) return;
70
+ const node = getNodeAtPath(tab.doc.document, tab.session.selection);
71
+ if (!node?.style) return;
72
+ workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
73
+ statusMessage("Styles copied");
74
+ }
75
+
76
+ export function pasteStyles() {
77
+ if (!workspace.styleClipboard) return;
78
+ const tab = activeTab.value;
79
+ if (!tab?.session.selection) return;
80
+ const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
81
+ const sel = /** @type {JxPath} */ (tab.session.selection);
82
+ transactDoc(tab, (t) => mutateReplaceStyle(t, sel, style));
83
+ statusMessage("Styles pasted");
84
+ }
85
+
69
86
  // ─── Context menu ─────────────────────────────────────────────────────────────
70
87
 
71
88
  const ctxMenu = document.createElement("sp-popover");
@@ -109,6 +126,26 @@ export function showContextMenu(e, path, opts = {}) {
109
126
  label: "Duplicate",
110
127
  action: () => transactDoc(activeTab.value, (t) => mutateDuplicateNode(t, path)),
111
128
  });
129
+ if (node.style) {
130
+ items.push({
131
+ label: "Copy styles",
132
+ action: () => {
133
+ workspace.styleClipboard = JSON.parse(JSON.stringify(node.style));
134
+ statusMessage("Styles copied");
135
+ },
136
+ });
137
+ }
138
+ if (workspace.styleClipboard) {
139
+ items.push({
140
+ label: "Paste styles",
141
+ action: () => {
142
+ if (!workspace.styleClipboard) return;
143
+ const style = JSON.parse(JSON.stringify(workspace.styleClipboard));
144
+ transactDoc(activeTab.value, (t) => mutateReplaceStyle(t, path, style));
145
+ statusMessage("Styles pasted");
146
+ },
147
+ });
148
+ }
112
149
  items.push({ label: "—" }); // separator
113
150
  items.push({
114
151
  label: "Insert before",
@@ -162,8 +199,8 @@ export function showContextMenu(e, path, opts = {}) {
162
199
  danger: true,
163
200
  });
164
201
  }
165
- if (clipboard) {
166
- const clip = clipboard;
202
+ if (workspace.clipboard) {
203
+ const clip = workspace.clipboard;
167
204
  items.push({ label: "—" });
168
205
  items.push({
169
206
  label: "Paste inside",
@@ -26,8 +26,12 @@ const HISTORY_LIMIT = 100;
26
26
  export function transactDoc(tab, mutationFn, { skipHistory = false } = {}) {
27
27
  mutationFn(tab);
28
28
 
29
+ // Replace the document root reference so effects tracking tab.doc.document re-trigger.
30
+ // Nested objects are shared — the mutation already modified them in place.
31
+ const raw = toRaw(tab.doc.document);
32
+ tab.doc.document = { ...raw };
33
+
29
34
  if (!skipHistory) {
30
- const raw = toRaw(tab.doc.document);
31
35
  const snapshot = {
32
36
  document: structuredClone(raw),
33
37
  selection: tab.session.selection ? [...tab.session.selection] : null,
@@ -270,6 +274,20 @@ export function mutateUpdateMediaNestedStyle(tab, path, mediaName, selector, pro
270
274
  if (Object.keys(node.style).length === 0) delete node.style;
271
275
  }
272
276
 
277
+ /**
278
+ * @param {Tab} tab
279
+ * @param {JxPath} path
280
+ * @param {Record<string, any> | undefined} style
281
+ */
282
+ export function mutateReplaceStyle(tab, path, style) {
283
+ const node = getNodeAtPath(tab.doc.document, path);
284
+ if (style && Object.keys(style).length > 0) {
285
+ node.style = style;
286
+ } else {
287
+ delete node.style;
288
+ }
289
+ }
290
+
273
291
  /**
274
292
  * @param {Tab} tab
275
293
  * @param {string} name
@@ -16,6 +16,10 @@ export const workspace = reactive({
16
16
  projectConfig: null,
17
17
  /** @type {ComponentEntry[]} */
18
18
  componentRegistry: [],
19
+ /** @type {import("../state.js").JxNode | null} */
20
+ clipboard: null,
21
+ /** @type {Record<string, any> | null} */
22
+ styleClipboard: null,
19
23
  fileTree: {
20
24
  /** @type {Map<string, FileEntry[]>} */
21
25
  dirs: new Map(),