@jxsuite/studio 0.19.0 → 0.21.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.css +98 -98
- package/dist/studio.js +13476 -12330
- package/dist/studio.js.map +152 -148
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +19 -7
- package/src/browse/browse-modal.js +16 -13
- package/src/browse/browse.js +44 -35
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +113 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +51 -21
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +28 -14
- package/src/editor/convert-targets.js +60 -0
- package/src/editor/convert-to-component.js +20 -19
- package/src/editor/convert-to-repeater.js +226 -0
- package/src/editor/inline-edit.js +69 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +14 -4
- package/src/editor/slash-menu.js +40 -19
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +322 -68
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +126 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +27 -25
- package/src/panels/ai-panel.js +406 -0
- package/src/panels/block-action-bar.js +77 -28
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +475 -99
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +94 -65
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +105 -48
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +378 -257
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +11 -9
- package/src/panels/right-panel.js +42 -16
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +415 -166
- package/src/panels/statusbar.js +10 -6
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +239 -134
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/tab-strip.js +5 -2
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +8 -8
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/settings/settings-modal.js +12 -9
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +36 -35
- package/src/studio.js +120 -90
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/layers.js +31 -1
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +32 -26
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +18 -18
- package/src/view.js +58 -1
- package/src/workspace/workspace.js +29 -7
|
@@ -7,13 +7,18 @@ import { classMap } from "lit-html/directives/class-map.js";
|
|
|
7
7
|
const expandedDataKeys = new Set();
|
|
8
8
|
|
|
9
9
|
/** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
|
|
10
|
-
function unwrapSignal(/** @type {
|
|
11
|
-
if (
|
|
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;
|
|
12
17
|
return value;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/** Type label for a signal value in the data explorer. */
|
|
16
|
-
function dataTypeLabel(/** @type {
|
|
21
|
+
function dataTypeLabel(/** @type {unknown} */ value) {
|
|
17
22
|
const v = unwrapSignal(value);
|
|
18
23
|
if (v === null) return "null";
|
|
19
24
|
if (v === undefined) return "pending";
|
|
@@ -25,15 +30,15 @@ function dataTypeLabel(/** @type {any} */ value) {
|
|
|
25
30
|
/**
|
|
26
31
|
* Render the data explorer tab showing live resolved values.
|
|
27
32
|
*
|
|
28
|
-
* @param {Record<string,
|
|
29
|
-
* @param {Record<string,
|
|
33
|
+
* @param {Record<string, unknown>} state - S.document.state (the $defs definitions)
|
|
34
|
+
* @param {Record<string, unknown> | null} liveScope - Cached live scope from runtime rendering
|
|
30
35
|
* @param {{
|
|
31
36
|
* renderCanvas: () => void;
|
|
32
37
|
* renderLeftPanel: () => void;
|
|
33
|
-
* defCategory: (def:
|
|
34
|
-
* defBadgeLabel: (def:
|
|
38
|
+
* defCategory: (def: unknown) => string;
|
|
39
|
+
* defBadgeLabel: (def: unknown) => string;
|
|
35
40
|
* }} callbacks
|
|
36
|
-
* @returns {
|
|
41
|
+
* @returns {import("lit-html").TemplateResult}
|
|
37
42
|
*/
|
|
38
43
|
export function renderDataExplorerTemplate(state, liveScope, callbacks) {
|
|
39
44
|
const { renderCanvas, renderLeftPanel, defCategory, defBadgeLabel } = callbacks;
|
|
@@ -91,11 +96,11 @@ export function renderDataExplorerTemplate(state, liveScope, callbacks) {
|
|
|
91
96
|
/**
|
|
92
97
|
* Recursively render a JSON value as a tree view (Lit template).
|
|
93
98
|
*
|
|
94
|
-
* @returns {
|
|
99
|
+
* @returns {import("lit-html").TemplateResult}
|
|
95
100
|
*/
|
|
96
101
|
export function renderDataTreeTemplate(
|
|
97
|
-
/** @type {
|
|
98
|
-
/** @type {
|
|
102
|
+
/** @type {unknown} */ value,
|
|
103
|
+
/** @type {number} */ depth,
|
|
99
104
|
maxDepth = 5,
|
|
100
105
|
) {
|
|
101
106
|
const indent = `${(depth + 1) * 12}px`;
|
|
@@ -150,10 +155,11 @@ export function renderDataTreeTemplate(
|
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
// Object
|
|
153
|
-
const
|
|
158
|
+
const obj = /** @type {Record<string, unknown>} */ (value);
|
|
159
|
+
const keys = Object.keys(obj);
|
|
154
160
|
const cap = 30;
|
|
155
161
|
const items = keys.slice(0, cap).map((key) => {
|
|
156
|
-
const v =
|
|
162
|
+
const v = obj[key];
|
|
157
163
|
if (v === null || v === undefined || typeof v !== "object") {
|
|
158
164
|
const valText =
|
|
159
165
|
typeof v === "string" && v.length > 80 ? `"${v.slice(0, 80)}\u2026"` : JSON.stringify(v);
|
package/src/panels/dnd.js
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* on layer rows, component cards, and element cards.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{ element: HTMLElement; input: { clientX: number; clientY: number } }} DragCanDragArgs
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{ source: { data: Record<string, unknown>; element: HTMLElement } }} DragDropSourceArgs
|
|
10
|
+
*
|
|
11
|
+
* @typedef {{ self: { data: Record<string, unknown> } }} DragSelfArgs
|
|
12
|
+
*
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* source: { data: Record<string, unknown>; element: HTMLElement };
|
|
15
|
+
* location: { current: { dropTargets: { data: Record<string, unknown> }[] } };
|
|
16
|
+
* }} DragMonitorDropArgs
|
|
17
|
+
*/
|
|
18
|
+
|
|
6
19
|
import {
|
|
7
20
|
draggable,
|
|
8
21
|
dropTargetForElements,
|
|
@@ -32,14 +45,18 @@ import { defaultDef, unsafeTags } from "./shared.js";
|
|
|
32
45
|
/** Register DnD on layer rows — called from left-panel.js after render */
|
|
33
46
|
export function registerLayersDnD() {
|
|
34
47
|
requestAnimationFrame(() => {
|
|
35
|
-
const container = /** @type {
|
|
48
|
+
const container = /** @type {HTMLElement | null} */ (
|
|
49
|
+
leftPanel?.querySelector(".layers-container")
|
|
50
|
+
);
|
|
36
51
|
if (!container) return;
|
|
37
52
|
|
|
38
|
-
container.querySelectorAll("[data-dnd-row]").forEach(
|
|
39
|
-
|
|
40
|
-
const rowPath = /** @type {
|
|
41
|
-
.
|
|
42
|
-
|
|
53
|
+
/** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll("[data-dnd-row]")).forEach(
|
|
54
|
+
(row) => {
|
|
55
|
+
const rowPath = /** @type {JxPath} */ (
|
|
56
|
+
/** @type {string} */ (row.dataset.path)
|
|
57
|
+
.split("/")
|
|
58
|
+
.map((/** @type {string} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s))
|
|
59
|
+
);
|
|
43
60
|
const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
|
|
44
61
|
const isVoid = row.hasAttribute("data-dnd-void");
|
|
45
62
|
const isExpanded = row.hasAttribute("data-dnd-expanded");
|
|
@@ -47,7 +64,7 @@ export function registerLayersDnD() {
|
|
|
47
64
|
const cleanup = combine(
|
|
48
65
|
draggable({
|
|
49
66
|
element: row,
|
|
50
|
-
canDrag(/** @type {
|
|
67
|
+
canDrag(/** @type {DragCanDragArgs} */ { element: _el, input }) {
|
|
51
68
|
const target = /** @type {HTMLElement} */ (
|
|
52
69
|
document.elementFromPoint(input.clientX, input.clientY)
|
|
53
70
|
);
|
|
@@ -73,28 +90,30 @@ export function registerLayersDnD() {
|
|
|
73
90
|
}),
|
|
74
91
|
dropTargetForElements({
|
|
75
92
|
element: row,
|
|
76
|
-
canDrop(/** @type {
|
|
77
|
-
const srcPath = source.data.path;
|
|
93
|
+
canDrop(/** @type {DragDropSourceArgs} */ { source }) {
|
|
94
|
+
const srcPath = /** @type {JxPath | undefined} */ (source.data.path);
|
|
78
95
|
if (srcPath && isAncestor(srcPath, rowPath)) return false;
|
|
79
96
|
return true;
|
|
80
97
|
},
|
|
81
98
|
getData(/** @type {any} */ { input, element }) {
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
return /** @type {Record<string | symbol, unknown>} */ (
|
|
100
|
+
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
|
+
)
|
|
92
111
|
);
|
|
93
112
|
},
|
|
94
|
-
onDragEnter(/** @type {
|
|
113
|
+
onDragEnter(/** @type {DragSelfArgs} */ { self }) {
|
|
95
114
|
showLayerDropGap(row, self.data, container);
|
|
96
115
|
},
|
|
97
|
-
onDrag(/** @type {
|
|
116
|
+
onDrag(/** @type {DragSelfArgs} */ { self }) {
|
|
98
117
|
showLayerDropGap(row, self.data, container);
|
|
99
118
|
},
|
|
100
119
|
onDragLeave() {
|
|
@@ -111,14 +130,14 @@ export function registerLayersDnD() {
|
|
|
111
130
|
|
|
112
131
|
// Global monitor
|
|
113
132
|
const monitorCleanup = monitorForElements({
|
|
114
|
-
onDrop(/** @type {
|
|
133
|
+
onDrop(/** @type {DragMonitorDropArgs} */ { source, location }) {
|
|
115
134
|
clearLayerDropGap(container);
|
|
116
135
|
const target = location.current.dropTargets[0];
|
|
117
136
|
if (!target) return;
|
|
118
137
|
const instruction = extractInstruction(target.data);
|
|
119
138
|
if (!instruction || instruction.type === "instruction-blocked") return;
|
|
120
139
|
const srcData = source.data;
|
|
121
|
-
const targetPath = target.data.path;
|
|
140
|
+
const targetPath = /** @type {JxPath} */ (target.data.path);
|
|
122
141
|
|
|
123
142
|
// If the source had children, persist collapse at the new location
|
|
124
143
|
const srcRow = srcData.type === "tree-node" && source.element;
|
|
@@ -143,56 +162,62 @@ export function registerLayersDnD() {
|
|
|
143
162
|
/** Register DnD on component rows — called from renderLeftPanel when tab=components */
|
|
144
163
|
export function registerComponentsDnD() {
|
|
145
164
|
requestAnimationFrame(() => {
|
|
146
|
-
const container = /** @type {
|
|
165
|
+
const container = /** @type {HTMLElement | null} */ (
|
|
166
|
+
leftPanel?.querySelector(".components-section")
|
|
167
|
+
);
|
|
147
168
|
if (!container) return;
|
|
148
169
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
/** @type {NodeListOf<HTMLElement>} */ (
|
|
171
|
+
container.querySelectorAll("[data-component-tag]")
|
|
172
|
+
).forEach((row) => {
|
|
173
|
+
const tagName = row.dataset.componentTag;
|
|
174
|
+
if (!tagName) return;
|
|
175
|
+
const comp = componentRegistry.find(
|
|
176
|
+
(/** @type {import("../files/components.js").ComponentEntry} */ c) => c.tagName === tagName,
|
|
177
|
+
);
|
|
178
|
+
if (!comp) return;
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
180
|
+
// Fill preview with live rendered component
|
|
181
|
+
const preview = row.querySelector(".element-card-preview");
|
|
182
|
+
if (preview && !preview.querySelector(tagName)) {
|
|
183
|
+
renderComponentPreview(comp).then((/** @type {HTMLElement} */ el) => {
|
|
184
|
+
preview.textContent = "";
|
|
185
|
+
preview.appendChild(el);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
164
188
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
189
|
+
const instanceDef = {
|
|
190
|
+
tagName: comp.tagName,
|
|
191
|
+
$props: comp.props
|
|
192
|
+
? Object.fromEntries(
|
|
193
|
+
comp.props.map(
|
|
194
|
+
(/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
|
|
170
195
|
p.name,
|
|
171
196
|
p.default !== undefined ? p.default : "",
|
|
172
|
-
]
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
197
|
+
],
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
: {},
|
|
201
|
+
};
|
|
202
|
+
const cleanup = draggable({
|
|
203
|
+
element: row,
|
|
204
|
+
getInitialData() {
|
|
205
|
+
return { type: "block", fragment: structuredClone(instanceDef) };
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
view.dndCleanups.push(cleanup);
|
|
209
|
+
});
|
|
185
210
|
});
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
/** Register DnD on element (HTML block) rows */
|
|
189
214
|
export function registerElementsDnD() {
|
|
190
215
|
requestAnimationFrame(() => {
|
|
191
|
-
const container = /** @type {
|
|
216
|
+
const container = /** @type {HTMLElement | null} */ (leftPanel?.querySelector(".panel-body"));
|
|
192
217
|
if (!container) return;
|
|
193
|
-
container.querySelectorAll("[data-block-tag]").forEach(
|
|
194
|
-
|
|
195
|
-
const tag = row.dataset.blockTag;
|
|
218
|
+
/** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll("[data-block-tag]")).forEach(
|
|
219
|
+
(row) => {
|
|
220
|
+
const tag = /** @type {string} */ (row.dataset.blockTag);
|
|
196
221
|
const preview = row.querySelector(".element-card-preview");
|
|
197
222
|
if (preview && !preview.firstChild) {
|
|
198
223
|
const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
|
|
@@ -215,23 +240,23 @@ export function registerElementsDnD() {
|
|
|
215
240
|
/**
|
|
216
241
|
* Hide descendant rows of the dragged item so it appears collapsed during drag.
|
|
217
242
|
*
|
|
218
|
-
* @param {
|
|
219
|
-
* @param {
|
|
243
|
+
* @param {HTMLElement} parentRow
|
|
244
|
+
* @param {HTMLElement} container
|
|
220
245
|
*/
|
|
221
246
|
function hideDescendantRows(parentRow, container) {
|
|
222
247
|
const prefix = parentRow.dataset.path + "/";
|
|
223
248
|
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
224
249
|
for (const r of rows) {
|
|
225
|
-
if (/** @type {
|
|
226
|
-
/** @type {
|
|
250
|
+
if (/** @type {HTMLElement} */ (r).dataset.path?.startsWith(prefix)) {
|
|
251
|
+
/** @type {HTMLElement} */ (r).style.display = "none";
|
|
227
252
|
}
|
|
228
253
|
}
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
/**
|
|
232
|
-
* @param {
|
|
233
|
-
* @param {
|
|
234
|
-
* @param {
|
|
257
|
+
* @param {HTMLElement} rowEl
|
|
258
|
+
* @param {Record<string, unknown>} data
|
|
259
|
+
* @param {HTMLElement} container
|
|
235
260
|
*/
|
|
236
261
|
export function showLayerDropGap(rowEl, data, container) {
|
|
237
262
|
const instruction = extractInstruction(data);
|
|
@@ -262,37 +287,39 @@ export function showLayerDropGap(rowEl, data, container) {
|
|
|
262
287
|
const gap = view.layerDragSourceHeight;
|
|
263
288
|
|
|
264
289
|
for (let i = 0; i < rows.length; i++) {
|
|
265
|
-
if (/** @type {
|
|
290
|
+
if (/** @type {HTMLElement} */ (rows[i]).classList.contains("dragging")) continue;
|
|
266
291
|
if (instruction.type === "reorder-above") {
|
|
267
|
-
/** @type {
|
|
292
|
+
/** @type {HTMLElement} */ (rows[i]).style.transform =
|
|
293
|
+
i >= targetIdx ? `translateY(${gap}px)` : "";
|
|
268
294
|
} else {
|
|
269
|
-
/** @type {
|
|
295
|
+
/** @type {HTMLElement} */ (rows[i]).style.transform =
|
|
296
|
+
i > targetIdx ? `translateY(${gap}px)` : "";
|
|
270
297
|
}
|
|
271
298
|
}
|
|
272
299
|
}
|
|
273
300
|
|
|
274
|
-
/** @param {
|
|
301
|
+
/** @param {HTMLElement} container */
|
|
275
302
|
export function clearLayerDropGap(container) {
|
|
276
303
|
if (view._currentDropTargetRow) {
|
|
277
304
|
view._currentDropTargetRow.classList.remove("drop-target");
|
|
278
305
|
view._currentDropTargetRow = null;
|
|
279
306
|
}
|
|
280
307
|
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
281
|
-
for (const r of rows) /** @type {
|
|
308
|
+
for (const r of rows) /** @type {HTMLElement} */ (r).style.transform = "";
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
/**
|
|
285
312
|
* Apply a DnD instruction to the state
|
|
286
313
|
*
|
|
287
|
-
* @param {
|
|
288
|
-
* @param {
|
|
289
|
-
* @param {
|
|
314
|
+
* @param {{ type: string }} instruction
|
|
315
|
+
* @param {Record<string, unknown>} srcData
|
|
316
|
+
* @param {JxPath} targetPath
|
|
290
317
|
*/
|
|
291
318
|
export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
292
|
-
const
|
|
319
|
+
const doc = /** @type {JxMutableNode} */ (activeTab.value?.doc.document);
|
|
293
320
|
if (srcData.type === "tree-node") {
|
|
294
|
-
const fromPath = srcData.path;
|
|
295
|
-
const targetParent = /** @type {
|
|
321
|
+
const fromPath = /** @type {JxPath} */ (srcData.path);
|
|
322
|
+
const targetParent = /** @type {JxPath} */ (parentElementPath(targetPath));
|
|
296
323
|
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
297
324
|
|
|
298
325
|
switch (instruction.type) {
|
|
@@ -305,64 +332,89 @@ export function applyDropInstruction(instruction, srcData, targetPath) {
|
|
|
305
332
|
);
|
|
306
333
|
break;
|
|
307
334
|
case "make-child": {
|
|
308
|
-
const target = getNodeAtPath(
|
|
335
|
+
const target = getNodeAtPath(doc, targetPath);
|
|
309
336
|
const len = target?.children?.length || 0;
|
|
310
337
|
transactDoc(activeTab.value, (t) => mutateMoveNode(t, fromPath, targetPath, len));
|
|
311
338
|
break;
|
|
312
339
|
}
|
|
313
340
|
}
|
|
314
341
|
} else if (srcData.type === "block") {
|
|
315
|
-
const targetParent = /** @type {
|
|
342
|
+
const targetParent = /** @type {JxPath} */ (parentElementPath(targetPath));
|
|
316
343
|
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
317
344
|
|
|
318
345
|
switch (instruction.type) {
|
|
319
346
|
case "reorder-above":
|
|
320
347
|
transactDoc(activeTab.value, (t) =>
|
|
321
|
-
mutateInsertNode(
|
|
348
|
+
mutateInsertNode(
|
|
349
|
+
t,
|
|
350
|
+
targetParent,
|
|
351
|
+
targetIdx,
|
|
352
|
+
structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
|
|
353
|
+
),
|
|
322
354
|
);
|
|
323
355
|
break;
|
|
324
356
|
case "reorder-below":
|
|
325
357
|
transactDoc(activeTab.value, (t) =>
|
|
326
|
-
mutateInsertNode(
|
|
358
|
+
mutateInsertNode(
|
|
359
|
+
t,
|
|
360
|
+
targetParent,
|
|
361
|
+
targetIdx + 1,
|
|
362
|
+
structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
|
|
363
|
+
),
|
|
327
364
|
);
|
|
328
365
|
break;
|
|
329
366
|
case "make-child": {
|
|
330
|
-
const target = getNodeAtPath(
|
|
367
|
+
const target = getNodeAtPath(doc, targetPath);
|
|
331
368
|
const len = target?.children?.length || 0;
|
|
332
369
|
transactDoc(activeTab.value, (t) =>
|
|
333
|
-
mutateInsertNode(
|
|
370
|
+
mutateInsertNode(
|
|
371
|
+
t,
|
|
372
|
+
targetPath,
|
|
373
|
+
len,
|
|
374
|
+
structuredClone(/** @type {JxMutableNode} */ (srcData.fragment)),
|
|
375
|
+
),
|
|
334
376
|
);
|
|
335
377
|
break;
|
|
336
378
|
}
|
|
337
379
|
}
|
|
338
380
|
|
|
339
381
|
// Auto-import to $elements if the dropped block is a custom component
|
|
340
|
-
const
|
|
382
|
+
const fragment = /** @type {JxMutableNode | undefined} */ (srcData.fragment);
|
|
383
|
+
const tag = fragment?.tagName;
|
|
341
384
|
if (tag && tag.includes("-")) {
|
|
342
|
-
const comp = componentRegistry.find(
|
|
385
|
+
const comp = componentRegistry.find(
|
|
386
|
+
(/** @type {import("../files/components.js").ComponentEntry} */ c) => c.tagName === tag,
|
|
387
|
+
);
|
|
343
388
|
if (comp) {
|
|
344
389
|
const tab = activeTab.value;
|
|
345
390
|
const elements = tab?.doc.document?.$elements || [];
|
|
346
391
|
if (comp.source === "npm") {
|
|
347
392
|
const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
|
|
393
|
+
if (!specifier) return;
|
|
348
394
|
const alreadyImported = elements.some(
|
|
349
|
-
(/** @type {
|
|
395
|
+
(/** @type {JxMutableNode | string | { $ref: string }} */ e) =>
|
|
396
|
+
e === specifier || e === comp.package,
|
|
350
397
|
);
|
|
351
398
|
if (!alreadyImported) {
|
|
352
|
-
transact(activeTab.value, (/** @type {
|
|
399
|
+
transact(activeTab.value, (/** @type {JxMutableNode} */ doc) => {
|
|
353
400
|
if (!doc.$elements) doc.$elements = [];
|
|
354
401
|
doc.$elements.push(specifier);
|
|
355
402
|
});
|
|
356
403
|
}
|
|
357
404
|
} else {
|
|
358
405
|
const alreadyImported = elements.some(
|
|
359
|
-
(/** @type {
|
|
360
|
-
e.$ref
|
|
361
|
-
|
|
406
|
+
(/** @type {JxMutableNode | string | { $ref: string }} */ e) => {
|
|
407
|
+
const ref = typeof e === "object" && e !== null ? e.$ref : undefined;
|
|
408
|
+
return (
|
|
409
|
+
ref &&
|
|
410
|
+
(ref === `./${comp.path}` ||
|
|
411
|
+
ref.endsWith(/** @type {string} */ (comp.path.split("/").pop())))
|
|
412
|
+
);
|
|
413
|
+
},
|
|
362
414
|
);
|
|
363
415
|
if (!alreadyImported) {
|
|
364
416
|
const relPath = computeRelativePath(tab?.documentPath, comp.path);
|
|
365
|
-
transact(activeTab.value, (/** @type {
|
|
417
|
+
transact(activeTab.value, (/** @type {JxMutableNode} */ doc) => {
|
|
366
418
|
if (!doc.$elements) doc.$elements = [];
|
|
367
419
|
doc.$elements.push({ $ref: relPath });
|
|
368
420
|
});
|
package/src/panels/editors.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* completion provider for state scope variables.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** @typedef {import("../services/code-services.js").OxLintDiagnostic} OxLintDiagnostic */
|
|
7
|
+
|
|
6
8
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
7
9
|
import { html, render as litRender, nothing } from "lit-html";
|
|
8
10
|
import { ref } from "lit-html/directives/ref.js";
|
|
@@ -13,12 +15,15 @@ import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "../tabs/tran
|
|
|
13
15
|
import { view } from "../view.js";
|
|
14
16
|
import { codeService, setLintMarkers, getFunctionArgs } from "../services/code-services.js";
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
/** @typedef {{ type: "def"; defName: string } | { type: "event"; path: JxPath; eventKey: string }} EditingTarget */
|
|
19
|
+
|
|
20
|
+
/** @param {EditingTarget | null | undefined} editing */
|
|
21
|
+
function getFunctionBody(editing) {
|
|
17
22
|
const document = activeTab.value?.doc.document;
|
|
18
|
-
if (editing
|
|
23
|
+
if (editing?.type === "def") {
|
|
19
24
|
return document?.state?.[editing.defName]?.body || "";
|
|
20
|
-
} else if (editing
|
|
21
|
-
const node = getNodeAtPath(document, editing.path);
|
|
25
|
+
} else if (editing?.type === "event") {
|
|
26
|
+
const node = getNodeAtPath(/** @type {JxMutableNode} */ (document), editing.path);
|
|
22
27
|
return node?.[editing.eventKey]?.body || "";
|
|
23
28
|
}
|
|
24
29
|
return "";
|
|
@@ -26,7 +31,9 @@ function getFunctionBody(/** @type {any} */ editing) {
|
|
|
26
31
|
|
|
27
32
|
/** @param {() => void} closeFunctionEditor */
|
|
28
33
|
export function renderFunctionEditor(closeFunctionEditor) {
|
|
29
|
-
const editing =
|
|
34
|
+
const editing = /** @type {EditingTarget | null | undefined} */ (
|
|
35
|
+
activeTab.value?.session.ui.editingFunction
|
|
36
|
+
);
|
|
30
37
|
|
|
31
38
|
// If editor already exists and matches current target, just sync value
|
|
32
39
|
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
@@ -66,7 +73,7 @@ export function renderFunctionEditor(closeFunctionEditor) {
|
|
|
66
73
|
|
|
67
74
|
const tab = activeTab.value;
|
|
68
75
|
const docName = tab?.documentPath?.split("/").pop() || tab?.doc.document?.tagName || "document";
|
|
69
|
-
const ed = /** @type {
|
|
76
|
+
const ed = /** @type {EditingTarget} */ (editing);
|
|
70
77
|
const funcLabel = ed.type === "def" ? `ƒ ${ed.defName}` : `ƒ ${ed.eventKey}`;
|
|
71
78
|
|
|
72
79
|
// Editor container
|
|
@@ -94,22 +101,29 @@ export function renderFunctionEditor(closeFunctionEditor) {
|
|
|
94
101
|
);
|
|
95
102
|
|
|
96
103
|
const body = getFunctionBody(editing);
|
|
97
|
-
const args = getFunctionArgs(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
const args = getFunctionArgs(
|
|
105
|
+
/** @type {EditingTarget} */ (editing),
|
|
106
|
+
activeTab.value?.doc.document,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
view.functionEditor = monaco.editor.create(
|
|
110
|
+
/** @type {HTMLElement} */ (/** @type {unknown} */ (editorContainer)),
|
|
111
|
+
{
|
|
112
|
+
value: body,
|
|
113
|
+
language: "javascript",
|
|
114
|
+
theme: "vs-dark",
|
|
115
|
+
automaticLayout: true,
|
|
116
|
+
minimap: { enabled: false },
|
|
117
|
+
fontSize: 12,
|
|
118
|
+
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
119
|
+
lineNumbers: "on",
|
|
120
|
+
scrollBeyondLastLine: false,
|
|
121
|
+
wordWrap: "on",
|
|
122
|
+
tabSize: 2,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
112
125
|
view.functionEditor._editingTarget = JSON.stringify(editing);
|
|
126
|
+
const editor = view.functionEditor;
|
|
113
127
|
|
|
114
128
|
// Format on open — show pretty-printed code, then run initial lint
|
|
115
129
|
codeService("format", { code: body, args }).then((result) => {
|
|
@@ -120,29 +134,32 @@ export function renderFunctionEditor(closeFunctionEditor) {
|
|
|
120
134
|
});
|
|
121
135
|
codeService("lint", { code: body, args }).then((result) => {
|
|
122
136
|
if (result?.diagnostics && view.functionEditor)
|
|
123
|
-
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
137
|
+
setLintMarkers(view.functionEditor, /** @type {OxLintDiagnostic[]} */ (result.diagnostics));
|
|
124
138
|
});
|
|
125
139
|
|
|
126
140
|
// Debounced sync back to state + lint on edit
|
|
127
|
-
/** @type {
|
|
141
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
128
142
|
let syncDebounce;
|
|
129
|
-
/** @type {
|
|
143
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
130
144
|
let lintDebounce;
|
|
131
145
|
let lintGen = 0;
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
146
|
+
editor.onDidChangeModelContent(() => {
|
|
147
|
+
if (editor._ignoreNextChange) {
|
|
148
|
+
editor._ignoreNextChange = false;
|
|
135
149
|
return;
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
clearTimeout(syncDebounce);
|
|
139
153
|
syncDebounce = setTimeout(() => {
|
|
140
|
-
const newBody =
|
|
141
|
-
const ed = /** @type {
|
|
154
|
+
const newBody = editor.getValue();
|
|
155
|
+
const ed = /** @type {EditingTarget} */ (editing);
|
|
142
156
|
if (ed.type === "def") {
|
|
143
157
|
transactDoc(activeTab.value, (t) => mutateUpdateDef(t, ed.defName, { body: newBody }));
|
|
144
158
|
} else if (ed.type === "event") {
|
|
145
|
-
const node = getNodeAtPath(
|
|
159
|
+
const node = getNodeAtPath(
|
|
160
|
+
/** @type {JxMutableNode} */ (activeTab.value?.doc.document),
|
|
161
|
+
ed.path,
|
|
162
|
+
);
|
|
146
163
|
const current = node?.[ed.eventKey] || {};
|
|
147
164
|
transactDoc(activeTab.value, (t) =>
|
|
148
165
|
mutateUpdateProperty(t, ed.path, ed.eventKey, {
|
|
@@ -158,11 +175,14 @@ export function renderFunctionEditor(closeFunctionEditor) {
|
|
|
158
175
|
clearTimeout(lintDebounce);
|
|
159
176
|
lintDebounce = setTimeout(() => {
|
|
160
177
|
const gen = ++lintGen;
|
|
161
|
-
const currentCode =
|
|
178
|
+
const currentCode = editor.getValue();
|
|
162
179
|
codeService("lint", { code: currentCode, args }).then((result) => {
|
|
163
180
|
if (gen !== lintGen) return;
|
|
164
181
|
if (result?.diagnostics && view.functionEditor)
|
|
165
|
-
setLintMarkers(
|
|
182
|
+
setLintMarkers(
|
|
183
|
+
view.functionEditor,
|
|
184
|
+
/** @type {OxLintDiagnostic[]} */ (result.diagnostics),
|
|
185
|
+
);
|
|
166
186
|
});
|
|
167
187
|
}, 750);
|
|
168
188
|
});
|
|
@@ -187,11 +207,11 @@ export function registerFunctionCompletions() {
|
|
|
187
207
|
const suggestions = Object.entries(defs).map(([key, def]) => {
|
|
188
208
|
let kind = monaco.languages.CompletionItemKind.Variable;
|
|
189
209
|
if (
|
|
190
|
-
/** @type {
|
|
191
|
-
/** @type {
|
|
210
|
+
/** @type {JxPrototypeDef} */ (def)?.$prototype === "Function" ||
|
|
211
|
+
/** @type {{ $handler?: unknown }} */ (def)?.$handler
|
|
192
212
|
)
|
|
193
213
|
kind = monaco.languages.CompletionItemKind.Function;
|
|
194
|
-
else if (/** @type {
|
|
214
|
+
else if (/** @type {JxPrototypeDef} */ (def)?.$prototype)
|
|
195
215
|
kind = monaco.languages.CompletionItemKind.Property;
|
|
196
216
|
return {
|
|
197
217
|
label: `state.${key}`,
|