@jxsuite/studio 0.7.0 → 0.8.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 +6293 -5966
- package/dist/studio.js.map +23 -17
- package/package.json +2 -2
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/left-panel.js +3 -2
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/right-panel.js +2 -1
- package/src/panels/shared.js +74 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/studio.js +64 -3340
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DnD registration functions — extracted from studio.js (Phase 4). Registers drag-and-drop behavior
|
|
3
|
+
* on layer rows, component cards, and element cards.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
draggable,
|
|
8
|
+
dropTargetForElements,
|
|
9
|
+
monitorForElements,
|
|
10
|
+
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
11
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
12
|
+
import {
|
|
13
|
+
attachInstruction,
|
|
14
|
+
extractInstruction,
|
|
15
|
+
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getState,
|
|
19
|
+
update,
|
|
20
|
+
leftPanel,
|
|
21
|
+
moveNode,
|
|
22
|
+
insertNode,
|
|
23
|
+
applyMutation,
|
|
24
|
+
getNodeAtPath,
|
|
25
|
+
parentElementPath,
|
|
26
|
+
childIndex,
|
|
27
|
+
isAncestor,
|
|
28
|
+
} from "../store.js";
|
|
29
|
+
import { view } from "../view.js";
|
|
30
|
+
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
31
|
+
import { renderComponentPreview } from "./stylebook-panel.js";
|
|
32
|
+
import { defaultDef, unsafeTags } from "./shared.js";
|
|
33
|
+
|
|
34
|
+
/** Register DnD on layer rows — called from left-panel.js after render */
|
|
35
|
+
export function registerLayersDnD() {
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
const container = /** @type {any} */ (leftPanel)?.querySelector(".layers-container");
|
|
38
|
+
if (!container) return;
|
|
39
|
+
|
|
40
|
+
container.querySelectorAll("[data-dnd-row]").forEach(
|
|
41
|
+
/** @param {any} row */ (row) => {
|
|
42
|
+
const rowPath = /** @type {string} */ (row.dataset.path)
|
|
43
|
+
.split("/")
|
|
44
|
+
.map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
|
|
45
|
+
const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
|
|
46
|
+
const isVoid = row.hasAttribute("data-dnd-void");
|
|
47
|
+
|
|
48
|
+
const cleanup = combine(
|
|
49
|
+
draggable({
|
|
50
|
+
element: row,
|
|
51
|
+
canDrag(/** @type {any} */ { element: _el, input }) {
|
|
52
|
+
const target = /** @type {HTMLElement} */ (
|
|
53
|
+
document.elementFromPoint(input.clientX, input.clientY)
|
|
54
|
+
);
|
|
55
|
+
if (target?.closest(".layer-actions")) return false;
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
getInitialData() {
|
|
59
|
+
return { type: "tree-node", path: rowPath };
|
|
60
|
+
},
|
|
61
|
+
onDragStart() {
|
|
62
|
+
row.classList.add("dragging");
|
|
63
|
+
view.layerDragSourceHeight = row.offsetHeight;
|
|
64
|
+
},
|
|
65
|
+
onDrop() {
|
|
66
|
+
row.classList.remove("dragging");
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
dropTargetForElements({
|
|
70
|
+
element: row,
|
|
71
|
+
canDrop(/** @type {any} */ { source }) {
|
|
72
|
+
const srcPath = source.data.path;
|
|
73
|
+
if (srcPath && isAncestor(srcPath, rowPath)) return false;
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
getData(/** @type {any} */ { input, element }) {
|
|
77
|
+
return attachInstruction(
|
|
78
|
+
{ path: rowPath },
|
|
79
|
+
/** @type {any} */ ({
|
|
80
|
+
input,
|
|
81
|
+
element,
|
|
82
|
+
currentLevel: rowDepth,
|
|
83
|
+
indentPerLevel: 16,
|
|
84
|
+
block: isVoid ? ["make-child"] : [],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
onDragEnter(/** @type {any} */ { self }) {
|
|
89
|
+
showLayerDropGap(row, self.data, container);
|
|
90
|
+
},
|
|
91
|
+
onDrag(/** @type {any} */ { self }) {
|
|
92
|
+
showLayerDropGap(row, self.data, container);
|
|
93
|
+
},
|
|
94
|
+
onDragLeave() {
|
|
95
|
+
clearLayerDropGap(container);
|
|
96
|
+
},
|
|
97
|
+
onDrop() {
|
|
98
|
+
clearLayerDropGap(container);
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
view.dndCleanups.push(cleanup);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Global monitor
|
|
107
|
+
const monitorCleanup = monitorForElements({
|
|
108
|
+
onDrop(/** @type {any} */ { source, location }) {
|
|
109
|
+
clearLayerDropGap(container);
|
|
110
|
+
const target = location.current.dropTargets[0];
|
|
111
|
+
if (!target) return;
|
|
112
|
+
const instruction = extractInstruction(target.data);
|
|
113
|
+
if (!instruction || instruction.type === "instruction-blocked") return;
|
|
114
|
+
const srcData = source.data;
|
|
115
|
+
const targetPath = target.data.path;
|
|
116
|
+
applyDropInstruction(instruction, srcData, targetPath);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
view.dndCleanups.push(monitorCleanup);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Register DnD on component rows — called from renderLeftPanel when tab=components */
|
|
124
|
+
export function registerComponentsDnD() {
|
|
125
|
+
requestAnimationFrame(() => {
|
|
126
|
+
const container = /** @type {any} */ (leftPanel)?.querySelector(".components-section");
|
|
127
|
+
if (!container) return;
|
|
128
|
+
|
|
129
|
+
container.querySelectorAll("[data-component-tag]").forEach(
|
|
130
|
+
/** @param {any} row */ (row) => {
|
|
131
|
+
const tagName = row.dataset.componentTag;
|
|
132
|
+
if (!tagName) return;
|
|
133
|
+
const comp = componentRegistry.find(/** @param {any} c */ (c) => c.tagName === tagName);
|
|
134
|
+
if (!comp) return;
|
|
135
|
+
|
|
136
|
+
// Fill preview with live rendered component
|
|
137
|
+
const preview = row.querySelector(".element-card-preview");
|
|
138
|
+
if (preview && !preview.querySelector(tagName)) {
|
|
139
|
+
renderComponentPreview(comp).then((/** @type {any} */ el) => {
|
|
140
|
+
preview.textContent = "";
|
|
141
|
+
preview.appendChild(el);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const instanceDef = {
|
|
146
|
+
tagName: comp.tagName,
|
|
147
|
+
$props: Object.fromEntries(
|
|
148
|
+
comp.props.map((/** @type {any} */ p) => [
|
|
149
|
+
p.name,
|
|
150
|
+
p.default !== undefined ? p.default : "",
|
|
151
|
+
]),
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
const cleanup = draggable({
|
|
155
|
+
element: row,
|
|
156
|
+
getInitialData() {
|
|
157
|
+
return { type: "block", fragment: structuredClone(instanceDef) };
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
view.dndCleanups.push(cleanup);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Register DnD on element (HTML block) rows */
|
|
167
|
+
export function registerElementsDnD() {
|
|
168
|
+
requestAnimationFrame(() => {
|
|
169
|
+
const container = /** @type {any} */ (leftPanel)?.querySelector(".panel-body");
|
|
170
|
+
if (!container) return;
|
|
171
|
+
container.querySelectorAll("[data-block-tag]").forEach(
|
|
172
|
+
/** @param {any} row */ (row) => {
|
|
173
|
+
const tag = row.dataset.blockTag;
|
|
174
|
+
const preview = row.querySelector(".element-card-preview");
|
|
175
|
+
if (preview && !preview.firstChild) {
|
|
176
|
+
const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
|
|
177
|
+
el.textContent = tag;
|
|
178
|
+
preview.appendChild(el);
|
|
179
|
+
}
|
|
180
|
+
const def = defaultDef(tag);
|
|
181
|
+
const cleanup = draggable({
|
|
182
|
+
element: row,
|
|
183
|
+
getInitialData() {
|
|
184
|
+
return { type: "block", fragment: structuredClone(def) };
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
view.dndCleanups.push(cleanup);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {any} rowEl
|
|
195
|
+
* @param {any} data
|
|
196
|
+
* @param {any} container
|
|
197
|
+
*/
|
|
198
|
+
export function showLayerDropGap(rowEl, data, container) {
|
|
199
|
+
const instruction = extractInstruction(data);
|
|
200
|
+
|
|
201
|
+
// Clear previous drop-target highlight
|
|
202
|
+
if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
|
|
203
|
+
view._currentDropTargetRow.classList.remove("drop-target");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!instruction || instruction.type === "instruction-blocked") {
|
|
207
|
+
clearLayerDropGap(container);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (instruction.type === "make-child") {
|
|
212
|
+
clearLayerDropGap(container);
|
|
213
|
+
rowEl.classList.add("drop-target");
|
|
214
|
+
view._currentDropTargetRow = rowEl;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
rowEl.classList.remove("drop-target");
|
|
219
|
+
view._currentDropTargetRow = rowEl;
|
|
220
|
+
|
|
221
|
+
// Shift rows to create gap
|
|
222
|
+
const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
|
|
223
|
+
const targetIdx = rows.indexOf(rowEl);
|
|
224
|
+
const gap = view.layerDragSourceHeight;
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < rows.length; i++) {
|
|
227
|
+
if (/** @type {any} */ (rows[i]).classList.contains("dragging")) continue;
|
|
228
|
+
if (instruction.type === "reorder-above") {
|
|
229
|
+
/** @type {any} */ (rows[i]).style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
|
|
230
|
+
} else {
|
|
231
|
+
/** @type {any} */ (rows[i]).style.transform = i > targetIdx ? `translateY(${gap}px)` : "";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @param {any} container */
|
|
237
|
+
export function clearLayerDropGap(container) {
|
|
238
|
+
if (view._currentDropTargetRow) {
|
|
239
|
+
view._currentDropTargetRow.classList.remove("drop-target");
|
|
240
|
+
view._currentDropTargetRow = null;
|
|
241
|
+
}
|
|
242
|
+
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
243
|
+
for (const r of rows) /** @type {any} */ (r).style.transform = "";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Apply a DnD instruction to the state
|
|
248
|
+
*
|
|
249
|
+
* @param {any} instruction
|
|
250
|
+
* @param {any} srcData
|
|
251
|
+
* @param {any} targetPath
|
|
252
|
+
*/
|
|
253
|
+
export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
254
|
+
const S = getState();
|
|
255
|
+
if (srcData.type === "tree-node") {
|
|
256
|
+
const fromPath = srcData.path;
|
|
257
|
+
const targetParent = /** @type {any} */ (parentElementPath(targetPath));
|
|
258
|
+
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
259
|
+
|
|
260
|
+
switch (instruction.type) {
|
|
261
|
+
case "reorder-above":
|
|
262
|
+
update(moveNode(S, fromPath, targetParent, targetIdx));
|
|
263
|
+
break;
|
|
264
|
+
case "reorder-below":
|
|
265
|
+
update(moveNode(S, fromPath, targetParent, targetIdx + 1));
|
|
266
|
+
break;
|
|
267
|
+
case "make-child": {
|
|
268
|
+
const target = getNodeAtPath(S.document, targetPath);
|
|
269
|
+
const len = target?.children?.length || 0;
|
|
270
|
+
update(moveNode(S, fromPath, targetPath, len));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else if (srcData.type === "block") {
|
|
275
|
+
const targetParent = /** @type {any} */ (parentElementPath(targetPath));
|
|
276
|
+
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
277
|
+
|
|
278
|
+
switch (instruction.type) {
|
|
279
|
+
case "reorder-above":
|
|
280
|
+
update(insertNode(S, targetParent, targetIdx, structuredClone(srcData.fragment)));
|
|
281
|
+
break;
|
|
282
|
+
case "reorder-below":
|
|
283
|
+
update(insertNode(S, targetParent, targetIdx + 1, structuredClone(srcData.fragment)));
|
|
284
|
+
break;
|
|
285
|
+
case "make-child": {
|
|
286
|
+
const target = getNodeAtPath(S.document, targetPath);
|
|
287
|
+
const len = target?.children?.length || 0;
|
|
288
|
+
update(insertNode(S, targetPath, len, structuredClone(srcData.fragment)));
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Auto-import to $elements if the dropped block is a custom component
|
|
294
|
+
const tag = srcData.fragment?.tagName;
|
|
295
|
+
if (tag && tag.includes("-")) {
|
|
296
|
+
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
297
|
+
if (comp) {
|
|
298
|
+
const currentS = getState();
|
|
299
|
+
const elements = currentS.document.$elements || [];
|
|
300
|
+
if (comp.source === "npm") {
|
|
301
|
+
const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
|
|
302
|
+
const alreadyImported = elements.some(
|
|
303
|
+
(/** @type {any} */ e) => e === specifier || e === comp.package,
|
|
304
|
+
);
|
|
305
|
+
if (!alreadyImported) {
|
|
306
|
+
update(
|
|
307
|
+
applyMutation(currentS, (/** @type {any} */ doc) => {
|
|
308
|
+
if (!doc.$elements) doc.$elements = [];
|
|
309
|
+
doc.$elements.push(specifier);
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
const alreadyImported = elements.some(
|
|
315
|
+
(/** @type {any} */ e) =>
|
|
316
|
+
e.$ref &&
|
|
317
|
+
(e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
|
|
318
|
+
);
|
|
319
|
+
if (!alreadyImported) {
|
|
320
|
+
const relPath = computeRelativePath(currentS.documentPath, comp.path);
|
|
321
|
+
update(
|
|
322
|
+
applyMutation(currentS, (/** @type {any} */ doc) => {
|
|
323
|
+
if (!doc.$elements) doc.$elements = [];
|
|
324
|
+
doc.$elements.push({ $ref: relPath });
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor panels — extracted from studio.js (Phase 4g). Monaco-based function editor (JS mode) and
|
|
3
|
+
* completion provider for state scope variables.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
7
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getState,
|
|
12
|
+
update,
|
|
13
|
+
renderOnly,
|
|
14
|
+
canvasWrap,
|
|
15
|
+
canvasPanels,
|
|
16
|
+
updateDef,
|
|
17
|
+
updateProperty,
|
|
18
|
+
getNodeAtPath,
|
|
19
|
+
} from "../store.js";
|
|
20
|
+
import { view } from "../view.js";
|
|
21
|
+
import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
|
|
22
|
+
|
|
23
|
+
function getFunctionBody(/** @type {any} */ editing) {
|
|
24
|
+
const S = getState();
|
|
25
|
+
if (editing.type === "def") {
|
|
26
|
+
return S.document.state?.[editing.defName]?.body || "";
|
|
27
|
+
} else if (editing.type === "event") {
|
|
28
|
+
const node = getNodeAtPath(S.document, editing.path);
|
|
29
|
+
return node?.[editing.eventKey]?.body || "";
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderFunctionEditor() {
|
|
35
|
+
const S = getState();
|
|
36
|
+
const editing = S.ui.editingFunction;
|
|
37
|
+
|
|
38
|
+
// If editor already exists and matches current target, just sync value
|
|
39
|
+
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
40
|
+
const body = getFunctionBody(editing);
|
|
41
|
+
const currentVal = view.functionEditor.getValue();
|
|
42
|
+
if (currentVal !== body) {
|
|
43
|
+
view.functionEditor._ignoreNextChange = true;
|
|
44
|
+
view.functionEditor.setValue(body);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dispose previous editors
|
|
50
|
+
if (view.functionEditor) {
|
|
51
|
+
view.functionEditor.dispose();
|
|
52
|
+
view.functionEditor = null;
|
|
53
|
+
}
|
|
54
|
+
if (view.monacoEditor) {
|
|
55
|
+
view.monacoEditor.dispose();
|
|
56
|
+
view.monacoEditor = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Clean up canvas DnD and event handlers
|
|
60
|
+
for (const fn of view.canvasDndCleanups) fn();
|
|
61
|
+
view.canvasDndCleanups = [];
|
|
62
|
+
for (const fn of view.canvasEventCleanups) fn();
|
|
63
|
+
view.canvasEventCleanups = [];
|
|
64
|
+
canvasPanels.length = 0;
|
|
65
|
+
|
|
66
|
+
litRender(nothing, canvasWrap);
|
|
67
|
+
canvasWrap.style.padding = "0";
|
|
68
|
+
|
|
69
|
+
// Toolbar breadcrumb handles context display — re-render it
|
|
70
|
+
renderOnly("toolbar");
|
|
71
|
+
|
|
72
|
+
// Editor container
|
|
73
|
+
/** @type {HTMLDivElement | null} */
|
|
74
|
+
let editorContainer = null;
|
|
75
|
+
litRender(
|
|
76
|
+
html`<div
|
|
77
|
+
class="source-editor"
|
|
78
|
+
${ref((el) => {
|
|
79
|
+
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
80
|
+
})}
|
|
81
|
+
></div>`,
|
|
82
|
+
canvasWrap,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const body = getFunctionBody(editing);
|
|
86
|
+
const args = getFunctionArgs(editing, S);
|
|
87
|
+
|
|
88
|
+
view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
89
|
+
value: body,
|
|
90
|
+
language: "javascript",
|
|
91
|
+
theme: "vs-dark",
|
|
92
|
+
automaticLayout: true,
|
|
93
|
+
minimap: { enabled: false },
|
|
94
|
+
fontSize: 12,
|
|
95
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
96
|
+
lineNumbers: "on",
|
|
97
|
+
scrollBeyondLastLine: false,
|
|
98
|
+
wordWrap: "on",
|
|
99
|
+
tabSize: 2,
|
|
100
|
+
});
|
|
101
|
+
view.functionEditor._editingTarget = JSON.stringify(editing);
|
|
102
|
+
|
|
103
|
+
// Format on open — show pretty-printed code, then run initial lint
|
|
104
|
+
codeService("format", { code: body, args }).then((result) => {
|
|
105
|
+
if (result?.code != null && view.functionEditor) {
|
|
106
|
+
view.functionEditor._ignoreNextChange = true;
|
|
107
|
+
view.functionEditor.setValue(result.code);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
codeService("lint", { code: body, args }).then((result) => {
|
|
111
|
+
if (result?.diagnostics && view.functionEditor)
|
|
112
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Debounced sync back to state + lint on edit
|
|
116
|
+
/** @type {any} */
|
|
117
|
+
let syncDebounce;
|
|
118
|
+
/** @type {any} */
|
|
119
|
+
let lintDebounce;
|
|
120
|
+
let lintGen = 0;
|
|
121
|
+
view.functionEditor.onDidChangeModelContent(() => {
|
|
122
|
+
if (view.functionEditor._ignoreNextChange) {
|
|
123
|
+
view.functionEditor._ignoreNextChange = false;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clearTimeout(syncDebounce);
|
|
128
|
+
syncDebounce = setTimeout(() => {
|
|
129
|
+
const S = getState();
|
|
130
|
+
const newBody = view.functionEditor.getValue();
|
|
131
|
+
if (editing.type === "def") {
|
|
132
|
+
update(updateDef(S, editing.defName, { body: newBody }));
|
|
133
|
+
} else if (editing.type === "event") {
|
|
134
|
+
const node = getNodeAtPath(S.document, editing.path);
|
|
135
|
+
const current = node?.[editing.eventKey] || {};
|
|
136
|
+
update(
|
|
137
|
+
updateProperty(S, editing.path, editing.eventKey, {
|
|
138
|
+
...current,
|
|
139
|
+
$prototype: "Function",
|
|
140
|
+
body: newBody,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
renderOnly("leftPanel");
|
|
145
|
+
}, 500);
|
|
146
|
+
|
|
147
|
+
clearTimeout(lintDebounce);
|
|
148
|
+
lintDebounce = setTimeout(() => {
|
|
149
|
+
const gen = ++lintGen;
|
|
150
|
+
const currentCode = view.functionEditor.getValue();
|
|
151
|
+
codeService("lint", { code: currentCode, args }).then((result) => {
|
|
152
|
+
if (gen !== lintGen) return;
|
|
153
|
+
if (result?.diagnostics && view.functionEditor)
|
|
154
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
155
|
+
});
|
|
156
|
+
}, 750);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Register Monaco JS completion provider for state scope variables (once)
|
|
161
|
+
export function registerFunctionCompletions() {
|
|
162
|
+
if (view._completionRegistered) return;
|
|
163
|
+
view._completionRegistered = true;
|
|
164
|
+
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
165
|
+
triggerCharacters: ["."],
|
|
166
|
+
provideCompletionItems(model, position) {
|
|
167
|
+
const S = getState();
|
|
168
|
+
const defs = S?.document?.state || {};
|
|
169
|
+
const word = model.getWordUntilPosition(position);
|
|
170
|
+
const range = {
|
|
171
|
+
startLineNumber: position.lineNumber,
|
|
172
|
+
endLineNumber: position.lineNumber,
|
|
173
|
+
startColumn: word.startColumn,
|
|
174
|
+
endColumn: word.endColumn,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const suggestions = Object.entries(defs).map(([key, def]) => {
|
|
178
|
+
let kind = monaco.languages.CompletionItemKind.Variable;
|
|
179
|
+
if (
|
|
180
|
+
/** @type {any} */ (def)?.$prototype === "Function" ||
|
|
181
|
+
/** @type {any} */ (def)?.$handler
|
|
182
|
+
)
|
|
183
|
+
kind = monaco.languages.CompletionItemKind.Function;
|
|
184
|
+
else if (/** @type {any} */ (def)?.$prototype)
|
|
185
|
+
kind = monaco.languages.CompletionItemKind.Property;
|
|
186
|
+
return {
|
|
187
|
+
label: `state.${key}`,
|
|
188
|
+
kind,
|
|
189
|
+
insertText: `state.${key}`,
|
|
190
|
+
range,
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
return { suggestions };
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
package/src/panels/left-panel.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ensureLitState } from "./shared.js";
|
|
|
20
20
|
import { renderLayersTemplate } from "./layers-panel.js";
|
|
21
21
|
import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
|
|
22
22
|
import { renderElementsTemplate } from "./elements-panel.js";
|
|
23
|
+
import { selectStylebookTag, stylebookMeta } from "./stylebook-panel.js";
|
|
23
24
|
|
|
24
25
|
/** @type {any} */
|
|
25
26
|
let _ctx = null;
|
|
@@ -65,8 +66,8 @@ function _render() {
|
|
|
65
66
|
content =
|
|
66
67
|
_ctx.getCanvasMode() === "settings"
|
|
67
68
|
? renderStylebookLayersTemplate({
|
|
68
|
-
selectStylebookTag
|
|
69
|
-
stylebookMeta
|
|
69
|
+
selectStylebookTag,
|
|
70
|
+
stylebookMeta,
|
|
70
71
|
})
|
|
71
72
|
: renderLayersTemplate({
|
|
72
73
|
navigateToComponent: _ctx.navigateToComponent,
|