@jxsuite/studio 0.13.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.
- package/dist/studio.js +428 -617
- package/dist/studio.js.map +36 -36
- package/package.json +1 -1
- package/src/canvas/canvas-live-render.js +25 -0
- package/src/canvas/canvas-render.js +20 -2
- package/src/canvas/canvas-utils.js +1 -2
- package/src/editor/component-inline-edit.js +1 -3
- package/src/editor/content-inline-edit.js +1 -3
- package/src/editor/context-menu.js +26 -25
- package/src/editor/insertion-helper.js +0 -1
- package/src/editor/shortcuts.js +39 -45
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +13 -8
- package/src/panels/activity-bar.js +6 -4
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +45 -7
- package/src/panels/editors.js +14 -16
- package/src/panels/elements-panel.js +13 -13
- package/src/panels/git-panel.js +2 -1
- package/src/panels/layers-panel.js +11 -12
- package/src/panels/left-panel.js +2 -2
- package/src/panels/panel-events.js +19 -25
- package/src/panels/preview-render.js +7 -3
- package/src/panels/pseudo-preview.js +9 -8
- package/src/panels/statusbar.js +8 -16
- package/src/panels/style-inputs.js +2 -3
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +16 -16
- package/src/panels/toolbar.js +2 -2
- package/src/state.js +2 -3
- package/src/store.js +2 -133
- package/src/studio.js +111 -239
- package/src/tabs/tab.js +19 -4
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +1 -0
package/src/files/file-ops.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File Operations — open,
|
|
2
|
+
* File Operations — open, save, export documents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* can assign S and trigger render().
|
|
4
|
+
* All functions read/write directly from/to `activeTab.value` (the reactive tab).
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { unified } from "unified";
|
|
@@ -10,17 +9,13 @@ import remarkStringify from "remark-stringify";
|
|
|
10
9
|
import remarkDirective from "remark-directive";
|
|
11
10
|
import { stringify as stringifyYaml } from "yaml";
|
|
12
11
|
import { jxToMd, jxDocToMd } from "../markdown/md-convert.js";
|
|
13
|
-
import { createState } from "../store.js";
|
|
14
12
|
import { locateDocument } from "../services/code-services.js";
|
|
15
13
|
import { statusMessage } from "../panels/statusbar.js";
|
|
16
14
|
import { getPlatform } from "../platform.js";
|
|
15
|
+
import { activeTab, openTab } from "../workspace/workspace.js";
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
* @param {{ S: any; commit: (s: any) => void; renderToolbar: () => void }} ctx
|
|
22
|
-
*/
|
|
23
|
-
export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar }) {
|
|
17
|
+
/** Open a file via the File System Access API (or fallback input). */
|
|
18
|
+
export async function openFile() {
|
|
24
19
|
try {
|
|
25
20
|
if ("showOpenFilePicker" in window) {
|
|
26
21
|
const [handle] = await /** @type {any} */ (window).showOpenFilePicker({
|
|
@@ -31,18 +26,21 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
31
26
|
});
|
|
32
27
|
const file = await handle.getFile();
|
|
33
28
|
const text = await file.text();
|
|
29
|
+
const documentPath = await locateDocument(handle.name);
|
|
34
30
|
|
|
35
31
|
if (handle.name.endsWith(".md")) {
|
|
36
|
-
const
|
|
37
|
-
|
|
32
|
+
const { document, frontmatter } = await loadMarkdown(text);
|
|
33
|
+
openTab({
|
|
34
|
+
id: handle.name,
|
|
35
|
+
documentPath,
|
|
36
|
+
fileHandle: handle,
|
|
37
|
+
document,
|
|
38
|
+
frontmatter,
|
|
39
|
+
sourceFormat: "md",
|
|
40
|
+
});
|
|
38
41
|
} else {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
newState.fileHandle = handle;
|
|
42
|
-
newState.dirty = false;
|
|
43
|
-
newState.documentPath = await locateDocument(handle.name);
|
|
44
|
-
await loadCompanionJS(handle, newState);
|
|
45
|
-
commit(newState);
|
|
42
|
+
const document = JSON.parse(text);
|
|
43
|
+
openTab({ id: handle.name, documentPath, fileHandle: handle, document });
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
statusMessage(`Opened ${handle.name}`);
|
|
@@ -57,13 +55,11 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
57
55
|
const text = await file.text();
|
|
58
56
|
|
|
59
57
|
if (file.name.endsWith(".md")) {
|
|
60
|
-
const
|
|
61
|
-
|
|
58
|
+
const { document, frontmatter } = await loadMarkdown(text);
|
|
59
|
+
openTab({ id: file.name, document, frontmatter, sourceFormat: "md" });
|
|
62
60
|
} else {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
newState.dirty = false;
|
|
66
|
-
commit(newState);
|
|
61
|
+
const document = JSON.parse(text);
|
|
62
|
+
openTab({ id: file.name, document });
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
statusMessage(`Opened ${file.name}`);
|
|
@@ -76,93 +72,50 @@ export async function openFile({ S: _S, commit, renderToolbar: _renderToolbar })
|
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
/**
|
|
79
|
-
* Parse a markdown string into
|
|
75
|
+
* Parse a markdown string into document + frontmatter (pure — no side effects).
|
|
80
76
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* contract.
|
|
84
|
-
*
|
|
85
|
-
* @param {any} source Markdown text
|
|
86
|
-
* @param {any} fileHandle File handle (or null)
|
|
87
|
-
* @returns {Promise<any>} A new state object ready for commit()
|
|
77
|
+
* @param {string} source Markdown text
|
|
78
|
+
* @returns {Promise<{ document: any; frontmatter: Record<string, any> }>}
|
|
88
79
|
*/
|
|
89
|
-
export async function loadMarkdown(source
|
|
80
|
+
export async function loadMarkdown(source) {
|
|
90
81
|
const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
|
|
91
82
|
const doc = /** @type {any} */ (transpileJxMarkdown(source));
|
|
92
83
|
|
|
93
84
|
const isComponent = doc.tagName && String(doc.tagName).includes("-");
|
|
94
85
|
|
|
95
86
|
if (isComponent) {
|
|
96
|
-
|
|
97
|
-
newState.sourceFormat = "md";
|
|
98
|
-
newState.rawMarkdown = source;
|
|
99
|
-
newState.fileHandle = fileHandle;
|
|
100
|
-
newState.dirty = false;
|
|
101
|
-
return newState;
|
|
87
|
+
return { document: doc, frontmatter: {} };
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
// Content markdown — children form the root-level document body
|
|
105
|
-
const contentDoc = {
|
|
106
|
-
children: doc.children ?? [],
|
|
107
|
-
};
|
|
91
|
+
const contentDoc = { children: doc.children ?? [] };
|
|
108
92
|
|
|
109
|
-
// Extract frontmatter keys (everything except children) as content metadata
|
|
110
93
|
/** @type {Record<string, any>} */
|
|
111
94
|
const frontmatter = {};
|
|
112
95
|
for (const [key, value] of Object.entries(doc)) {
|
|
113
96
|
if (key !== "children") frontmatter[key] = value;
|
|
114
97
|
}
|
|
115
98
|
|
|
116
|
-
|
|
117
|
-
newState.sourceFormat = "md";
|
|
118
|
-
newState.mode = "content";
|
|
119
|
-
newState.content = { frontmatter };
|
|
120
|
-
newState.rawMarkdown = source;
|
|
121
|
-
newState.fileHandle = fileHandle;
|
|
122
|
-
newState.dirty = false;
|
|
123
|
-
return newState;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Load companion JS file metadata into state.
|
|
128
|
-
*
|
|
129
|
-
* @param {any} handle
|
|
130
|
-
* @param {any} state State object to mutate in-place
|
|
131
|
-
*/
|
|
132
|
-
async function loadCompanionJS(handle, state) {
|
|
133
|
-
try {
|
|
134
|
-
if (handle.getParent) {
|
|
135
|
-
// Not yet available in any browser; skip for now
|
|
136
|
-
}
|
|
137
|
-
if (state.document.$handlers) {
|
|
138
|
-
state.handlersSource = `// Companion file: ${state.document.$handlers}\n// (Read-only in builder — edit the JS file directly)`;
|
|
139
|
-
}
|
|
140
|
-
} catch {}
|
|
99
|
+
return { document: contentDoc, frontmatter };
|
|
141
100
|
}
|
|
142
101
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*/
|
|
148
|
-
export async function saveFile({ S, commit, renderToolbar }) {
|
|
102
|
+
/** Save the current document back to its source location. */
|
|
103
|
+
export async function saveFile() {
|
|
104
|
+
const tab = activeTab.value;
|
|
105
|
+
if (!tab) return;
|
|
149
106
|
try {
|
|
150
|
-
const output = serializeDocument(
|
|
107
|
+
const output = serializeDocument(tab);
|
|
151
108
|
|
|
152
|
-
if (
|
|
153
|
-
// Project file — save via platform
|
|
109
|
+
if (tab.documentPath) {
|
|
154
110
|
const platform = getPlatform();
|
|
155
|
-
await platform.writeFile(
|
|
156
|
-
|
|
157
|
-
renderToolbar();
|
|
111
|
+
await platform.writeFile(tab.documentPath, output);
|
|
112
|
+
tab.doc.dirty = false;
|
|
158
113
|
statusMessage("Saved");
|
|
159
|
-
} else if (
|
|
160
|
-
|
|
161
|
-
const writable = await S.fileHandle.createWritable();
|
|
114
|
+
} else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
|
|
115
|
+
const writable = await /** @type {any} */ (tab.fileHandle).createWritable();
|
|
162
116
|
await writable.write(output);
|
|
163
117
|
await writable.close();
|
|
164
|
-
|
|
165
|
-
renderToolbar();
|
|
118
|
+
tab.doc.dirty = false;
|
|
166
119
|
statusMessage("Saved");
|
|
167
120
|
} else {
|
|
168
121
|
statusMessage("No save target — use Export");
|
|
@@ -172,22 +125,20 @@ export async function saveFile({ S, commit, renderToolbar }) {
|
|
|
172
125
|
}
|
|
173
126
|
}
|
|
174
127
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*/
|
|
180
|
-
export async function exportFile({ S, commit, renderToolbar }) {
|
|
128
|
+
/** Export the current document to a new location (Save As / download). */
|
|
129
|
+
export async function exportFile() {
|
|
130
|
+
const tab = activeTab.value;
|
|
131
|
+
if (!tab) return;
|
|
181
132
|
try {
|
|
182
|
-
const isContent =
|
|
183
|
-
const output = serializeDocument(
|
|
133
|
+
const isContent = tab.doc.mode === "content";
|
|
134
|
+
const output = serializeDocument(tab);
|
|
184
135
|
const mimeType = isContent ? "text/markdown" : "application/json";
|
|
185
136
|
const ext = isContent ? ".md" : ".json";
|
|
186
137
|
const description = isContent ? "Markdown Content" : "Jx Component";
|
|
187
138
|
|
|
188
139
|
if ("showSaveFilePicker" in window) {
|
|
189
|
-
const suggestedName =
|
|
190
|
-
?
|
|
140
|
+
const suggestedName = tab.documentPath
|
|
141
|
+
? tab.documentPath.split("/").pop()
|
|
191
142
|
: isContent
|
|
192
143
|
? "content.md"
|
|
193
144
|
: "component.json";
|
|
@@ -198,8 +149,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
198
149
|
const writable = await handle.createWritable();
|
|
199
150
|
await writable.write(output);
|
|
200
151
|
await writable.close();
|
|
201
|
-
|
|
202
|
-
renderToolbar();
|
|
152
|
+
tab.doc.dirty = false;
|
|
203
153
|
statusMessage(`Exported as ${handle.name}`);
|
|
204
154
|
} else {
|
|
205
155
|
// Fallback: download
|
|
@@ -210,8 +160,7 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
210
160
|
a.download = isContent ? "content.md" : "component.json";
|
|
211
161
|
a.click();
|
|
212
162
|
URL.revokeObjectURL(url);
|
|
213
|
-
|
|
214
|
-
renderToolbar();
|
|
163
|
+
tab.doc.dirty = false;
|
|
215
164
|
statusMessage("Downloaded");
|
|
216
165
|
}
|
|
217
166
|
} catch (/** @type {any} */ e) {
|
|
@@ -222,22 +171,22 @@ export async function exportFile({ S, commit, renderToolbar }) {
|
|
|
222
171
|
/**
|
|
223
172
|
* Serialize the current document to its output format (JSON or Markdown).
|
|
224
173
|
*
|
|
225
|
-
* @param {
|
|
174
|
+
* @param {import("../tabs/tab.js").Tab} tab
|
|
226
175
|
* @returns {string}
|
|
227
176
|
*/
|
|
228
|
-
function serializeDocument(
|
|
229
|
-
if (
|
|
230
|
-
return jxDocToMd(
|
|
177
|
+
function serializeDocument(tab) {
|
|
178
|
+
if (tab.doc.sourceFormat === "md") {
|
|
179
|
+
return jxDocToMd(tab.doc.document);
|
|
231
180
|
}
|
|
232
|
-
if (
|
|
233
|
-
const mdast = jxToMd(
|
|
181
|
+
if (tab.doc.mode === "content") {
|
|
182
|
+
const mdast = jxToMd(tab.doc.document);
|
|
234
183
|
const md = unified()
|
|
235
184
|
.use(remarkDirective)
|
|
236
185
|
.use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
|
|
237
186
|
.stringify(mdast);
|
|
238
|
-
const fm =
|
|
187
|
+
const fm = tab.doc.content?.frontmatter;
|
|
239
188
|
const hasFrontmatter = fm && Object.keys(fm).length > 0;
|
|
240
189
|
return hasFrontmatter ? `---\n${stringifyYaml(fm).trim()}\n---\n\n${md}` : md;
|
|
241
190
|
}
|
|
242
|
-
return JSON.stringify(
|
|
191
|
+
return JSON.stringify(tab.doc.document, null, 2);
|
|
243
192
|
}
|
package/src/files/files.js
CHANGED
|
@@ -17,6 +17,7 @@ import { statusMessage } from "../panels/statusbar.js";
|
|
|
17
17
|
import { loadComponentRegistry } from "./components.js";
|
|
18
18
|
import { workspace, openTab, activateTab } from "../workspace/workspace.js";
|
|
19
19
|
import { loadMarkdown } from "./file-ops.js";
|
|
20
|
+
import { view } from "../view.js";
|
|
20
21
|
|
|
21
22
|
// ─── File icon map ────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -79,13 +80,11 @@ export async function loadProject() {
|
|
|
79
80
|
* Open a project via the platform adapter.
|
|
80
81
|
*
|
|
81
82
|
* @param {{
|
|
82
|
-
* S: any;
|
|
83
|
-
* commit: (s: any) => void;
|
|
84
83
|
* renderActivityBar: () => void;
|
|
85
84
|
* renderLeftPanel: () => void;
|
|
86
85
|
* }} ctx
|
|
87
86
|
*/
|
|
88
|
-
export async function openProject({
|
|
87
|
+
export async function openProject({ renderActivityBar, renderLeftPanel }) {
|
|
89
88
|
try {
|
|
90
89
|
const platform = getPlatform();
|
|
91
90
|
const result = await platform.openProject();
|
|
@@ -129,7 +128,7 @@ export async function openProject({ S, commit, renderActivityBar, renderLeftPane
|
|
|
129
128
|
}
|
|
130
129
|
projectState.projectDirs = foundDirs;
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
view.leftTab = "files";
|
|
133
132
|
renderActivityBar();
|
|
134
133
|
renderLeftPanel();
|
|
135
134
|
statusMessage(`Opened project: ${projectState.name}`);
|
|
@@ -616,15 +615,21 @@ export async function openFileInTab(path) {
|
|
|
616
615
|
|
|
617
616
|
let document, frontmatter;
|
|
618
617
|
if (path.endsWith(".md")) {
|
|
619
|
-
const
|
|
620
|
-
document =
|
|
621
|
-
frontmatter =
|
|
618
|
+
const result = await loadMarkdown(content);
|
|
619
|
+
document = result.document;
|
|
620
|
+
frontmatter = result.frontmatter;
|
|
622
621
|
} else {
|
|
623
622
|
document = JSON.parse(content);
|
|
624
623
|
}
|
|
625
624
|
|
|
626
625
|
const id = path;
|
|
627
|
-
openTab({
|
|
626
|
+
openTab({
|
|
627
|
+
id,
|
|
628
|
+
documentPath: path,
|
|
629
|
+
document,
|
|
630
|
+
frontmatter,
|
|
631
|
+
sourceFormat: path.endsWith(".md") ? "md" : null,
|
|
632
|
+
});
|
|
628
633
|
projectState.selectedPath = path;
|
|
629
634
|
statusMessage(`Opened ${path.split("/").pop()}`);
|
|
630
635
|
} catch (/** @type {any} */ e) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Activity bar — tab icons for switching left panel views. */
|
|
2
2
|
|
|
3
3
|
import { html, render as litRender, nothing } from "lit-html";
|
|
4
|
-
import { activityBar } from "../store.js";
|
|
4
|
+
import { activityBar, renderOnly } from "../store.js";
|
|
5
5
|
import { effect, effectScope } from "../reactivity.js";
|
|
6
6
|
import { activeTab } from "../workspace/workspace.js";
|
|
7
|
+
import { view } from "../view.js";
|
|
7
8
|
|
|
8
9
|
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
9
10
|
let _scope = null;
|
|
@@ -14,7 +15,6 @@ export function mount() {
|
|
|
14
15
|
effect(() => {
|
|
15
16
|
const tab = activeTab.value;
|
|
16
17
|
if (!tab) return;
|
|
17
|
-
void tab.session.ui.leftTab;
|
|
18
18
|
renderActivityBar();
|
|
19
19
|
});
|
|
20
20
|
});
|
|
@@ -83,7 +83,7 @@ export function tabIcon(tag, size) {
|
|
|
83
83
|
export function renderActivityBar() {
|
|
84
84
|
const tab = activeTab.value;
|
|
85
85
|
if (!tab) return;
|
|
86
|
-
const leftTab =
|
|
86
|
+
const leftTab = view.leftTab;
|
|
87
87
|
const tabs = [
|
|
88
88
|
{ value: "files", icon: "sp-icon-folder", label: "Files" },
|
|
89
89
|
{ value: "layers", icon: "sp-icon-layers", label: "Layers" },
|
|
@@ -100,7 +100,9 @@ export function renderActivityBar() {
|
|
|
100
100
|
direction="vertical"
|
|
101
101
|
quiet
|
|
102
102
|
@change=${(/** @type {any} */ e) => {
|
|
103
|
-
|
|
103
|
+
view.leftTab = e.target.selected;
|
|
104
|
+
renderOnly("leftPanel");
|
|
105
|
+
renderActivityBar();
|
|
104
106
|
}}
|
|
105
107
|
>
|
|
106
108
|
${tabs.map(
|
package/src/panels/canvas-dnd.js
CHANGED
|
@@ -8,14 +8,8 @@ import {
|
|
|
8
8
|
monitorForElements,
|
|
9
9
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
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
|
|
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(
|
|
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,12 +15,12 @@ import {
|
|
|
15
15
|
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
|
-
getState,
|
|
19
18
|
leftPanel,
|
|
20
19
|
getNodeAtPath,
|
|
21
20
|
parentElementPath,
|
|
22
21
|
childIndex,
|
|
23
22
|
isAncestor,
|
|
23
|
+
renderOnly,
|
|
24
24
|
} from "../store.js";
|
|
25
25
|
import { transact, transactDoc, mutateMoveNode, mutateInsertNode } from "../tabs/transact.js";
|
|
26
26
|
import { activeTab } from "../workspace/workspace.js";
|
|
@@ -42,6 +42,7 @@ export function registerLayersDnD() {
|
|
|
42
42
|
.map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
|
|
43
43
|
const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
|
|
44
44
|
const isVoid = row.hasAttribute("data-dnd-void");
|
|
45
|
+
const isExpanded = row.hasAttribute("data-dnd-expanded");
|
|
45
46
|
|
|
46
47
|
const cleanup = combine(
|
|
47
48
|
draggable({
|
|
@@ -59,9 +60,15 @@ export function registerLayersDnD() {
|
|
|
59
60
|
onDragStart() {
|
|
60
61
|
row.classList.add("dragging");
|
|
61
62
|
view.layerDragSourceHeight = row.offsetHeight;
|
|
63
|
+
if (isExpanded) {
|
|
64
|
+
hideDescendantRows(row, container);
|
|
65
|
+
}
|
|
62
66
|
},
|
|
63
67
|
onDrop() {
|
|
64
68
|
row.classList.remove("dragging");
|
|
69
|
+
if (isExpanded) {
|
|
70
|
+
renderOnly("leftPanel");
|
|
71
|
+
}
|
|
65
72
|
},
|
|
66
73
|
}),
|
|
67
74
|
dropTargetForElements({
|
|
@@ -79,6 +86,7 @@ export function registerLayersDnD() {
|
|
|
79
86
|
element,
|
|
80
87
|
currentLevel: rowDepth,
|
|
81
88
|
indentPerLevel: 16,
|
|
89
|
+
mode: isExpanded ? "expanded" : "standard",
|
|
82
90
|
block: isVoid ? ["make-child"] : [],
|
|
83
91
|
}),
|
|
84
92
|
);
|
|
@@ -111,7 +119,21 @@ export function registerLayersDnD() {
|
|
|
111
119
|
if (!instruction || instruction.type === "instruction-blocked") return;
|
|
112
120
|
const srcData = source.data;
|
|
113
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
|
+
|
|
114
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
|
+
}
|
|
115
137
|
},
|
|
116
138
|
});
|
|
117
139
|
view.dndCleanups.push(monitorCleanup);
|
|
@@ -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
|
|
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));
|
|
@@ -267,7 +305,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
|
267
305
|
);
|
|
268
306
|
break;
|
|
269
307
|
case "make-child": {
|
|
270
|
-
const target = getNodeAtPath(
|
|
308
|
+
const target = getNodeAtPath(document, targetPath);
|
|
271
309
|
const len = target?.children?.length || 0;
|
|
272
310
|
transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
|
|
273
311
|
break;
|
|
@@ -289,7 +327,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
|
289
327
|
);
|
|
290
328
|
break;
|
|
291
329
|
case "make-child": {
|
|
292
|
-
const target = getNodeAtPath(
|
|
330
|
+
const target = getNodeAtPath(document, targetPath);
|
|
293
331
|
const len = target?.children?.length || 0;
|
|
294
332
|
transactDoc(activeTab.value, (t) =>
|
|
295
333
|
mutateInsertNode(t, targetPath, len, structuredClone(srcData.fragment)),
|
|
@@ -303,8 +341,8 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
|
303
341
|
if (tag && tag.includes("-")) {
|
|
304
342
|
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
305
343
|
if (comp) {
|
|
306
|
-
const
|
|
307
|
-
const elements =
|
|
344
|
+
const tab = activeTab.value;
|
|
345
|
+
const elements = tab?.doc.document?.$elements || [];
|
|
308
346
|
if (comp.source === "npm") {
|
|
309
347
|
const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
|
|
310
348
|
const alreadyImported = elements.some(
|
|
@@ -323,7 +361,7 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
|
323
361
|
(e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
|
|
324
362
|
);
|
|
325
363
|
if (!alreadyImported) {
|
|
326
|
-
const relPath = computeRelativePath(
|
|
364
|
+
const relPath = computeRelativePath(tab?.documentPath, comp.path);
|
|
327
365
|
transact(activeTab.value, (/** @type {any} */ doc) => {
|
|
328
366
|
if (!doc.$elements) doc.$elements = [];
|
|
329
367
|
doc.$elements.push({ $ref: relPath });
|
package/src/panels/editors.js
CHANGED
|
@@ -7,26 +7,25 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
|
7
7
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
8
|
import { ref } from "lit-html/directives/ref.js";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { renderOnly, canvasWrap, canvasPanels, getNodeAtPath } from "../store.js";
|
|
11
11
|
import { activeTab } from "../workspace/workspace.js";
|
|
12
12
|
import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "../tabs/transact.js";
|
|
13
13
|
import { view } from "../view.js";
|
|
14
14
|
import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
|
|
15
15
|
|
|
16
16
|
function getFunctionBody(/** @type {any} */ editing) {
|
|
17
|
-
const
|
|
17
|
+
const document = activeTab.value?.doc.document;
|
|
18
18
|
if (editing.type === "def") {
|
|
19
|
-
return
|
|
19
|
+
return document?.state?.[editing.defName]?.body || "";
|
|
20
20
|
} else if (editing.type === "event") {
|
|
21
|
-
const node = getNodeAtPath(
|
|
21
|
+
const node = getNodeAtPath(document, editing.path);
|
|
22
22
|
return node?.[editing.eventKey]?.body || "";
|
|
23
23
|
}
|
|
24
24
|
return "";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function renderFunctionEditor() {
|
|
28
|
-
const
|
|
29
|
-
const editing = S.ui.editingFunction;
|
|
28
|
+
const editing = activeTab.value?.session.ui.editingFunction;
|
|
30
29
|
|
|
31
30
|
// If editor already exists and matches current target, just sync value
|
|
32
31
|
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
@@ -76,7 +75,7 @@ export function renderFunctionEditor() {
|
|
|
76
75
|
);
|
|
77
76
|
|
|
78
77
|
const body = getFunctionBody(editing);
|
|
79
|
-
const args = getFunctionArgs(editing,
|
|
78
|
+
const args = getFunctionArgs(editing, activeTab.value?.doc.document);
|
|
80
79
|
|
|
81
80
|
view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
82
81
|
value: body,
|
|
@@ -120,14 +119,14 @@ export function renderFunctionEditor() {
|
|
|
120
119
|
clearTimeout(syncDebounce);
|
|
121
120
|
syncDebounce = setTimeout(() => {
|
|
122
121
|
const newBody = view.functionEditor.getValue();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const node = getNodeAtPath(
|
|
128
|
-
const current = node?.[
|
|
122
|
+
const ed = /** @type {any} */ (editing);
|
|
123
|
+
if (ed.type === "def") {
|
|
124
|
+
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, ed.defName, { body: newBody }));
|
|
125
|
+
} else if (ed.type === "event") {
|
|
126
|
+
const node = getNodeAtPath(activeTab.value?.doc.document, ed.path);
|
|
127
|
+
const current = node?.[ed.eventKey] || {};
|
|
129
128
|
transactDoc(activeTab.value, (t) =>
|
|
130
|
-
mutateUpdateProperty(t,
|
|
129
|
+
mutateUpdateProperty(t, ed.path, ed.eventKey, {
|
|
131
130
|
...current,
|
|
132
131
|
$prototype: "Function",
|
|
133
132
|
body: newBody,
|
|
@@ -157,8 +156,7 @@ export function registerFunctionCompletions() {
|
|
|
157
156
|
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
158
157
|
triggerCharacters: ["."],
|
|
159
158
|
provideCompletionItems(model, position) {
|
|
160
|
-
const
|
|
161
|
-
const defs = S?.document?.state || {};
|
|
159
|
+
const defs = activeTab.value?.doc.document?.state || {};
|
|
162
160
|
const word = model.getWordUntilPosition(position);
|
|
163
161
|
const range = {
|
|
164
162
|
startLineNumber: position.lineNumber,
|