@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.
- package/dist/studio.js +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
34
34
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
|
|
35
|
-
"@jxsuite/parser": "^0.28.
|
|
36
|
-
"@jxsuite/runtime": "^0.28.
|
|
37
|
-
"@jxsuite/schema": "^0.28.
|
|
35
|
+
"@jxsuite/parser": "^0.28.4",
|
|
36
|
+
"@jxsuite/runtime": "^0.28.4",
|
|
37
|
+
"@jxsuite/schema": "^0.28.4",
|
|
38
38
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
39
39
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
40
40
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@spectrum-web-components/toast": "^1.12.1",
|
|
71
71
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
72
72
|
"@spectrum-web-components/underlay": "^1.12.1",
|
|
73
|
-
"@vue/reactivity": "3.5.
|
|
73
|
+
"@vue/reactivity": "3.5.38",
|
|
74
74
|
"lit": "^3.3.3",
|
|
75
75
|
"lit-html": "^3.3.3",
|
|
76
76
|
"monaco-editor": "^0.55.1",
|
|
@@ -15,10 +15,14 @@ let _handle: ReturnType<typeof openModal> | null = null;
|
|
|
15
15
|
let _escHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
16
16
|
|
|
17
17
|
export function openBrowseModal() {
|
|
18
|
-
if (_handle)
|
|
18
|
+
if (_handle) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
19
21
|
|
|
20
22
|
_escHandler = (e: KeyboardEvent) => {
|
|
21
|
-
if (e.key === "Escape")
|
|
23
|
+
if (e.key === "Escape") {
|
|
24
|
+
closeBrowseModal();
|
|
25
|
+
}
|
|
22
26
|
};
|
|
23
27
|
document.addEventListener("keydown", _escHandler, true);
|
|
24
28
|
|
|
@@ -53,7 +57,9 @@ export function openBrowseModal() {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export function closeBrowseModal() {
|
|
56
|
-
if (!_handle)
|
|
60
|
+
if (!_handle) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
57
63
|
if (_escHandler) {
|
|
58
64
|
document.removeEventListener("keydown", _escHandler, true);
|
|
59
65
|
_escHandler = null;
|
package/src/browse/browse.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, render as litRender, nothing } from "lit-html";
|
|
11
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
11
12
|
import { ref } from "lit-html/directives/ref.js";
|
|
12
13
|
import { repeat } from "lit-html/directives/repeat.js";
|
|
13
14
|
import { getPlatform } from "../platform";
|
|
@@ -17,16 +18,16 @@ import { invalidateMediaCache } from "../ui/media-picker";
|
|
|
17
18
|
import { statusMessage } from "../panels/statusbar";
|
|
18
19
|
import { componentRegistry } from "../files/components";
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { renderPopover, showDialog } from "../ui/layers";
|
|
21
22
|
import { renderComponentPreview } from "../panels/stylebook-panel";
|
|
22
|
-
import {
|
|
23
|
+
import { buildScope, renderNode, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
23
24
|
import { parseSourceForPath } from "../files/file-ops";
|
|
24
25
|
import {
|
|
25
|
-
loadFormats,
|
|
26
|
-
formatForPath,
|
|
27
|
-
formatByName,
|
|
28
|
-
documentExtensions,
|
|
29
26
|
defaultContentFormat,
|
|
27
|
+
documentExtensions,
|
|
28
|
+
formatByName,
|
|
29
|
+
formatForPath,
|
|
30
|
+
loadFormats,
|
|
30
31
|
} from "../format/format-host";
|
|
31
32
|
|
|
32
33
|
import type { ComponentEntry } from "../files/components";
|
|
@@ -36,11 +37,11 @@ import type { ContentTypeDef } from "@jxsuite/schema/types";
|
|
|
36
37
|
|
|
37
38
|
const CATEGORIES = [
|
|
38
39
|
{ key: "all", label: "All" },
|
|
39
|
-
{
|
|
40
|
-
{
|
|
41
|
-
{
|
|
42
|
-
{
|
|
43
|
-
{
|
|
40
|
+
{ dir: "pages", key: "pages", label: "Pages" },
|
|
41
|
+
{ dir: "layouts", key: "layouts", label: "Layouts" },
|
|
42
|
+
{ dir: "components", key: "components", label: "Components" },
|
|
43
|
+
{ dir: "content", key: "content", label: "Content" },
|
|
44
|
+
{ dir: "public", key: "media", label: "Media" },
|
|
44
45
|
];
|
|
45
46
|
|
|
46
47
|
const MEDIA_EXTENSIONS = new Set([
|
|
@@ -106,14 +107,30 @@ function isImage(ext: string) {
|
|
|
106
107
|
|
|
107
108
|
/** Map a file path to a display category. Media files override by extension. */
|
|
108
109
|
function categoryFor(dir: string, ext: string) {
|
|
109
|
-
if (ext && MEDIA_EXTENSIONS.has(ext))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (dir.startsWith("
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (dir.startsWith("
|
|
116
|
-
|
|
110
|
+
if (ext && MEDIA_EXTENSIONS.has(ext)) {
|
|
111
|
+
return "Media";
|
|
112
|
+
}
|
|
113
|
+
if (dir.startsWith("pages")) {
|
|
114
|
+
return "Pages";
|
|
115
|
+
}
|
|
116
|
+
if (dir.startsWith("layouts")) {
|
|
117
|
+
return "Layouts";
|
|
118
|
+
}
|
|
119
|
+
if (dir.startsWith("components")) {
|
|
120
|
+
return "Components";
|
|
121
|
+
}
|
|
122
|
+
if (dir.startsWith("content")) {
|
|
123
|
+
return "Content";
|
|
124
|
+
}
|
|
125
|
+
if (dir.startsWith("public")) {
|
|
126
|
+
return "Media";
|
|
127
|
+
}
|
|
128
|
+
if (dir.startsWith("data")) {
|
|
129
|
+
return "Content";
|
|
130
|
+
}
|
|
131
|
+
if (dir.startsWith("styles")) {
|
|
132
|
+
return "Components";
|
|
133
|
+
}
|
|
117
134
|
return "Other";
|
|
118
135
|
}
|
|
119
136
|
|
|
@@ -149,11 +166,11 @@ async function collectFiles(
|
|
|
149
166
|
const type =
|
|
150
167
|
category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
|
|
151
168
|
results.push({
|
|
169
|
+
category,
|
|
170
|
+
ext,
|
|
152
171
|
name: entry.name,
|
|
153
172
|
path: entry.path,
|
|
154
173
|
type,
|
|
155
|
-
category,
|
|
156
|
-
ext,
|
|
157
174
|
});
|
|
158
175
|
}
|
|
159
176
|
}
|
|
@@ -172,12 +189,16 @@ async function collectFiles(
|
|
|
172
189
|
*/
|
|
173
190
|
function contentTypeFor(filePath: string) {
|
|
174
191
|
const config = projectState?.projectConfig;
|
|
175
|
-
if (!config?.contentTypes)
|
|
192
|
+
if (!config?.contentTypes) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
176
195
|
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
177
196
|
const d = def as ContentTypeDef;
|
|
178
|
-
if (!d.source)
|
|
197
|
+
if (!d.source) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
179
200
|
const prefix = d.source.replace(/^\.\//, "").replace(/\/$/, "");
|
|
180
|
-
if (filePath.startsWith(prefix
|
|
201
|
+
if (filePath.startsWith(`${prefix}/`) || filePath === prefix) {
|
|
181
202
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
182
203
|
}
|
|
183
204
|
}
|
|
@@ -187,7 +208,9 @@ function contentTypeFor(filePath: string) {
|
|
|
187
208
|
// ─── Data loading ────────────────────────────────────────────────────────────
|
|
188
209
|
|
|
189
210
|
async function loadFiles() {
|
|
190
|
-
if (!projectState)
|
|
211
|
+
if (!projectState) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
191
214
|
loading = true;
|
|
192
215
|
const platform = getPlatform();
|
|
193
216
|
const dirs = projectState.projectDirs || [];
|
|
@@ -223,10 +246,10 @@ function getEntityTypes() {
|
|
|
223
246
|
const pageExt = documentExtensions("page")[0] ?? ".json";
|
|
224
247
|
const contentExt = defaultContentFormat()?.extensions[0] ?? ".json";
|
|
225
248
|
return [
|
|
226
|
-
{
|
|
227
|
-
{
|
|
228
|
-
{
|
|
229
|
-
{
|
|
249
|
+
{ dir: "pages", ext: pageExt, key: "page", label: "Page" },
|
|
250
|
+
{ dir: "layouts", ext: ".json", key: "layout", label: "Layout" },
|
|
251
|
+
{ dir: "components", ext: ".json", key: "component", label: "Component" },
|
|
252
|
+
{ dir: "content", ext: contentExt, key: "content", label: "Content" },
|
|
230
253
|
];
|
|
231
254
|
}
|
|
232
255
|
|
|
@@ -239,7 +262,9 @@ function getEntityTypes() {
|
|
|
239
262
|
function buildFrontmatterYaml(contentTypeName: string) {
|
|
240
263
|
const config = projectState?.projectConfig;
|
|
241
264
|
const col = config?.contentTypes?.[contentTypeName];
|
|
242
|
-
if (!col?.schema?.properties)
|
|
265
|
+
if (!col?.schema?.properties) {
|
|
266
|
+
return "title: Untitled\n";
|
|
267
|
+
}
|
|
243
268
|
|
|
244
269
|
let yaml = "";
|
|
245
270
|
for (const [field, def] of Object.entries(col.schema.properties)) {
|
|
@@ -256,13 +281,14 @@ function buildFrontmatterYaml(contentTypeName: string) {
|
|
|
256
281
|
*/
|
|
257
282
|
function getContentTypeTypes() {
|
|
258
283
|
const config = projectState?.projectConfig;
|
|
259
|
-
if (!config?.contentTypes)
|
|
284
|
+
if (!config?.contentTypes) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
260
287
|
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
261
288
|
const d = def as ContentTypeDef;
|
|
262
289
|
const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
|
|
263
290
|
return {
|
|
264
|
-
|
|
265
|
-
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
291
|
+
contentTypeName: name,
|
|
266
292
|
dir,
|
|
267
293
|
ext:
|
|
268
294
|
d.format === "json"
|
|
@@ -270,7 +296,8 @@ function getContentTypeTypes() {
|
|
|
270
296
|
: (formatByName(d.format)?.extensions[0] ??
|
|
271
297
|
defaultContentFormat()?.extensions[0] ??
|
|
272
298
|
".json"),
|
|
273
|
-
|
|
299
|
+
key: `contentType:${name}`,
|
|
300
|
+
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
274
301
|
};
|
|
275
302
|
});
|
|
276
303
|
}
|
|
@@ -292,27 +319,30 @@ async function handleNewEntity(
|
|
|
292
319
|
await loadFormats();
|
|
293
320
|
const allTypes = [...getEntityTypes(), ...getContentTypeTypes()];
|
|
294
321
|
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
295
|
-
if (!typeInfo)
|
|
322
|
+
if (!typeInfo) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
296
325
|
|
|
326
|
+
// oxlint-disable-next-line no-alert -- native prompt is the intended quick-input UX here
|
|
297
327
|
const name = prompt(`${typeInfo.label} name:`, "untitled");
|
|
298
|
-
if (!name)
|
|
328
|
+
if (!name) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
299
331
|
|
|
300
332
|
const slug = name
|
|
301
333
|
.toLowerCase()
|
|
302
|
-
.
|
|
303
|
-
.
|
|
334
|
+
.replaceAll(/\s+/g, "-")
|
|
335
|
+
.replaceAll(/[^a-z0-9-]/g, "");
|
|
304
336
|
const filePath = `${typeInfo.dir}/${slug}${typeInfo.ext}`;
|
|
305
337
|
|
|
306
338
|
let content;
|
|
307
339
|
const entityFormat = formatForPath(filePath);
|
|
308
340
|
if (entityFormat) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
content = entityFormat.studio?.newFileTemplate ?? "";
|
|
313
|
-
}
|
|
341
|
+
content = contentTypeName
|
|
342
|
+
? `---\n${buildFrontmatterYaml(contentTypeName)}---\n\n`
|
|
343
|
+
: (entityFormat.studio?.newFileTemplate ?? "");
|
|
314
344
|
} else {
|
|
315
|
-
content = JSON.stringify({ tagName: "div"
|
|
345
|
+
content = JSON.stringify({ children: [], tagName: "div" }, null, "\t");
|
|
316
346
|
}
|
|
317
347
|
|
|
318
348
|
const platform = getPlatform();
|
|
@@ -386,21 +416,21 @@ function showBrowseContextMenu(
|
|
|
386
416
|
|
|
387
417
|
/** @type {{ label: string; action?: () => void; danger?: boolean }[]} */
|
|
388
418
|
const items = [
|
|
389
|
-
{
|
|
419
|
+
{ action: () => ctx.openFile(file.path), label: "Open" },
|
|
390
420
|
{ label: "\u2014" },
|
|
391
421
|
{
|
|
392
|
-
label: "Rename\u2026",
|
|
393
422
|
action: () => browseRenameFile(file, container, ctx),
|
|
423
|
+
label: "Rename\u2026",
|
|
394
424
|
},
|
|
395
425
|
{
|
|
396
|
-
label: "Duplicate",
|
|
397
426
|
action: () => browseDuplicateFile(file, container, ctx),
|
|
427
|
+
label: "Duplicate",
|
|
398
428
|
},
|
|
399
429
|
{ label: "\u2014" },
|
|
400
430
|
{
|
|
401
|
-
label: "Delete",
|
|
402
431
|
action: () => browseDeleteFile(file, container, ctx),
|
|
403
432
|
danger: true,
|
|
433
|
+
label: "Delete",
|
|
404
434
|
},
|
|
405
435
|
];
|
|
406
436
|
|
|
@@ -412,13 +442,18 @@ function showBrowseContextMenu(
|
|
|
412
442
|
open
|
|
413
443
|
style="position:fixed;left:${x}px;top:${y}px"
|
|
414
444
|
${ref((el) => {
|
|
415
|
-
if (!el)
|
|
445
|
+
if (!el) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
416
448
|
requestAnimationFrame(() => {
|
|
417
449
|
const popover = el as HTMLElement;
|
|
418
450
|
const menuRect = popover.getBoundingClientRect();
|
|
419
|
-
if (x + menuRect.width > window.innerWidth)
|
|
420
|
-
|
|
451
|
+
if (x + menuRect.width > window.innerWidth) {
|
|
452
|
+
x = window.innerWidth - menuRect.width - 4;
|
|
453
|
+
}
|
|
454
|
+
if (y + menuRect.height > window.innerHeight) {
|
|
421
455
|
y = window.innerHeight - menuRect.height - 4;
|
|
456
|
+
}
|
|
422
457
|
popover.style.left = `${x}px`;
|
|
423
458
|
popover.style.top = `${y}px`;
|
|
424
459
|
});
|
|
@@ -441,10 +476,10 @@ function showBrowseContextMenu(
|
|
|
441
476
|
</sp-popover>`,
|
|
442
477
|
{
|
|
443
478
|
dismissOnOutsideClick: true,
|
|
479
|
+
layer: "dialog",
|
|
444
480
|
onDismiss: () => {
|
|
445
481
|
_browseCtxHandle = null;
|
|
446
482
|
},
|
|
447
|
-
layer: "dialog",
|
|
448
483
|
},
|
|
449
484
|
);
|
|
450
485
|
}
|
|
@@ -460,9 +495,11 @@ async function browseRenameFile(
|
|
|
460
495
|
ctx: { openFile: (path: string) => void },
|
|
461
496
|
) {
|
|
462
497
|
const newName = await showRenameDialog(file.name);
|
|
463
|
-
if (!newName || newName === file.name)
|
|
498
|
+
if (!newName || newName === file.name) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
464
501
|
const filePath = file.path.replaceAll("\\", "/");
|
|
465
|
-
const parentDir = filePath.includes("/") ? filePath.
|
|
502
|
+
const parentDir = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".";
|
|
466
503
|
const newPath = parentDir === "." ? newName : `${parentDir}/${newName}`;
|
|
467
504
|
try {
|
|
468
505
|
const platform = getPlatform();
|
|
@@ -470,8 +507,8 @@ async function browseRenameFile(
|
|
|
470
507
|
invalidateBrowseCache();
|
|
471
508
|
renderBrowse(container, ctx);
|
|
472
509
|
statusMessage(`Renamed to ${newName}`);
|
|
473
|
-
} catch (
|
|
474
|
-
statusMessage(`Error: ${(
|
|
510
|
+
} catch (error) {
|
|
511
|
+
statusMessage(`Error: ${errorMessage(error)}`);
|
|
475
512
|
}
|
|
476
513
|
}
|
|
477
514
|
|
|
@@ -486,7 +523,7 @@ async function browseDuplicateFile(
|
|
|
486
523
|
ctx: { openFile: (path: string) => void },
|
|
487
524
|
) {
|
|
488
525
|
const filePath = file.path.replaceAll("\\", "/");
|
|
489
|
-
const parentDir = filePath.includes("/") ? filePath.
|
|
526
|
+
const parentDir = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ".";
|
|
490
527
|
const baseName = file.name.replace(/(\.[^.]+)$/, "");
|
|
491
528
|
const ext = file.ext || "";
|
|
492
529
|
const copyName = `${baseName}-copy${ext}`;
|
|
@@ -498,8 +535,8 @@ async function browseDuplicateFile(
|
|
|
498
535
|
invalidateBrowseCache();
|
|
499
536
|
renderBrowse(container, ctx);
|
|
500
537
|
statusMessage(`Duplicated as ${copyName}`);
|
|
501
|
-
} catch (
|
|
502
|
-
statusMessage(`Error: ${(
|
|
538
|
+
} catch (error) {
|
|
539
|
+
statusMessage(`Error: ${errorMessage(error)}`);
|
|
503
540
|
}
|
|
504
541
|
}
|
|
505
542
|
|
|
@@ -514,15 +551,17 @@ async function browseDeleteFile(
|
|
|
514
551
|
ctx: { openFile: (path: string) => void },
|
|
515
552
|
) {
|
|
516
553
|
const confirmed = await showDeleteDialog(file.name);
|
|
517
|
-
if (!confirmed)
|
|
554
|
+
if (!confirmed) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
518
557
|
try {
|
|
519
558
|
const platform = getPlatform();
|
|
520
559
|
await platform.deleteFile(file.path);
|
|
521
560
|
invalidateBrowseCache();
|
|
522
561
|
renderBrowse(container, ctx);
|
|
523
562
|
statusMessage(`Deleted ${file.name}`);
|
|
524
|
-
} catch (
|
|
525
|
-
statusMessage(`Error: ${(
|
|
563
|
+
} catch (error) {
|
|
564
|
+
statusMessage(`Error: ${errorMessage(error)}`);
|
|
526
565
|
}
|
|
527
566
|
}
|
|
528
567
|
|
|
@@ -538,7 +577,9 @@ function showRenameDialog(currentName: string): Promise<string | null> {
|
|
|
538
577
|
return showDialog<string | null>((done) => {
|
|
539
578
|
function confirm() {
|
|
540
579
|
const trimmed = value.trim();
|
|
541
|
-
if (!trimmed)
|
|
580
|
+
if (!trimmed) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
542
583
|
done(trimmed);
|
|
543
584
|
}
|
|
544
585
|
|
|
@@ -561,19 +602,23 @@ function showRenameDialog(currentName: string): Promise<string | null> {
|
|
|
561
602
|
value = (e.target as HTMLInputElement).value || "";
|
|
562
603
|
}}
|
|
563
604
|
@keydown=${(e: KeyboardEvent) => {
|
|
564
|
-
if (e.key === "Enter")
|
|
605
|
+
if (e.key === "Enter") {
|
|
606
|
+
confirm();
|
|
607
|
+
}
|
|
565
608
|
}}
|
|
566
609
|
></sp-textfield>
|
|
567
610
|
</sp-dialog-wrapper>
|
|
568
611
|
`;
|
|
569
612
|
|
|
570
613
|
requestAnimationFrame(() => {
|
|
571
|
-
const layer = document.
|
|
614
|
+
const layer = document.querySelector("#layer-dialog");
|
|
572
615
|
const tf = layer?.querySelector("sp-textfield") as HTMLElement | null;
|
|
573
616
|
if (tf) {
|
|
574
617
|
tf.focus();
|
|
575
618
|
const input = tf.shadowRoot?.querySelector("input");
|
|
576
|
-
if (input)
|
|
619
|
+
if (input) {
|
|
620
|
+
input.select();
|
|
621
|
+
}
|
|
577
622
|
}
|
|
578
623
|
});
|
|
579
624
|
|
|
@@ -607,7 +652,7 @@ function showDeleteDialog(fileName: string) {
|
|
|
607
652
|
|
|
608
653
|
// ─── Grid view helpers ──────────────────────────────────────────────────────
|
|
609
654
|
|
|
610
|
-
const _previewCache
|
|
655
|
+
const _previewCache = new Map<string, HTMLElement>();
|
|
611
656
|
|
|
612
657
|
/**
|
|
613
658
|
* Render a live preview for a page or layout file (JSON or Markdown).
|
|
@@ -628,9 +673,9 @@ async function renderDocPreview(filePath: string) {
|
|
|
628
673
|
} else {
|
|
629
674
|
doc = JSON.parse(content);
|
|
630
675
|
}
|
|
631
|
-
const scope = buildScope(doc
|
|
676
|
+
const scope = await buildScope(doc, {}, location.href);
|
|
632
677
|
const el = renderNode(doc, scope);
|
|
633
|
-
return el
|
|
678
|
+
return el instanceof HTMLElement ? el : null;
|
|
634
679
|
} catch {
|
|
635
680
|
return null;
|
|
636
681
|
}
|
|
@@ -644,23 +689,27 @@ async function renderDocPreview(filePath: string) {
|
|
|
644
689
|
*/
|
|
645
690
|
async function loadPreview(el: Element, file: { path: string; category: string }) {
|
|
646
691
|
// Already populated
|
|
647
|
-
if (el.firstElementChild)
|
|
692
|
+
if (el.firstElementChild) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
648
695
|
|
|
649
696
|
let preview: HTMLElement | undefined = _previewCache.get(file.path);
|
|
650
697
|
if (!preview) {
|
|
651
698
|
try {
|
|
652
699
|
const comp = componentRegistry.find((c: ComponentEntry) => c.path === file.path);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
700
|
+
preview = comp
|
|
701
|
+
? ((await renderComponentPreview(comp)) as HTMLElement | undefined)
|
|
702
|
+
: ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
|
|
703
|
+
if (preview) {
|
|
704
|
+
_previewCache.set(file.path, /** @type {HTMLElement} */ preview);
|
|
657
705
|
}
|
|
658
|
-
if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ preview);
|
|
659
706
|
} catch {
|
|
660
707
|
return;
|
|
661
708
|
}
|
|
662
709
|
}
|
|
663
|
-
if (preview)
|
|
710
|
+
if (preview) {
|
|
711
|
+
el.append(preview);
|
|
712
|
+
}
|
|
664
713
|
}
|
|
665
714
|
|
|
666
715
|
/**
|
|
@@ -698,7 +747,9 @@ function renderCard(
|
|
|
698
747
|
class="element-card-preview"
|
|
699
748
|
${needsPreview
|
|
700
749
|
? ref((el: Element | undefined) => {
|
|
701
|
-
if (el)
|
|
750
|
+
if (el) {
|
|
751
|
+
loadPreview(el, file);
|
|
752
|
+
}
|
|
702
753
|
})
|
|
703
754
|
: nothing}
|
|
704
755
|
>
|
|
@@ -730,7 +781,7 @@ export async function renderBrowse(
|
|
|
730
781
|
) {
|
|
731
782
|
// Re-load when projectDirs changed (e.g. project opened after initial render)
|
|
732
783
|
const currentKey = (projectState?.projectDirs || []).join(",");
|
|
733
|
-
if ((
|
|
784
|
+
if ((fileCache.length === 0 && !loading) || currentKey !== lastProjectDirsKey) {
|
|
734
785
|
await loadFiles();
|
|
735
786
|
}
|
|
736
787
|
|
|
@@ -777,7 +828,7 @@ export async function renderBrowse(
|
|
|
777
828
|
${getEntityTypes().map(
|
|
778
829
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
779
830
|
)}
|
|
780
|
-
${contentTypeTypes.length
|
|
831
|
+
${contentTypeTypes.length > 0
|
|
781
832
|
? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
|
|
782
833
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
783
834
|
)}`
|
|
@@ -789,7 +840,9 @@ export async function renderBrowse(
|
|
|
789
840
|
size="s"
|
|
790
841
|
@click=${() => {
|
|
791
842
|
const input = container.querySelector(".browse-upload-input") as HTMLInputElement;
|
|
792
|
-
if (input)
|
|
843
|
+
if (input) {
|
|
844
|
+
input.click();
|
|
845
|
+
}
|
|
793
846
|
}}
|
|
794
847
|
>
|
|
795
848
|
<sp-icon-upload slot="icon"></sp-icon-upload> Upload
|
|
@@ -802,7 +855,9 @@ export async function renderBrowse(
|
|
|
802
855
|
style="display:none"
|
|
803
856
|
@change=${(e: Event) => {
|
|
804
857
|
const input = e.target as HTMLInputElement;
|
|
805
|
-
if (input.files?.length)
|
|
858
|
+
if (input.files?.length) {
|
|
859
|
+
handleUpload(input.files, container, ctx);
|
|
860
|
+
}
|
|
806
861
|
input.value = "";
|
|
807
862
|
}}
|
|
808
863
|
/>
|
|
@@ -900,7 +955,9 @@ export async function renderBrowse(
|
|
|
900
955
|
e.preventDefault();
|
|
901
956
|
(e.currentTarget as HTMLElement).classList.remove("browse-drop-active");
|
|
902
957
|
const droppedFiles = e.dataTransfer?.files;
|
|
903
|
-
if (droppedFiles?.length)
|
|
958
|
+
if (droppedFiles?.length) {
|
|
959
|
+
handleUpload(droppedFiles, container, ctx);
|
|
960
|
+
}
|
|
904
961
|
}}
|
|
905
962
|
>
|
|
906
963
|
<div class="browse-filter-bar">${filterBar}</div>
|
|
@@ -23,13 +23,21 @@ interface DiffResult {
|
|
|
23
23
|
* @returns {boolean}
|
|
24
24
|
*/
|
|
25
25
|
function valuesEqual(a: unknown, b: unknown): boolean {
|
|
26
|
-
if (a === b)
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (a === b) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
if (Array.isArray(a)) {
|
|
31
37
|
const bArr = b as unknown[];
|
|
32
|
-
if (a.length !== bArr.length)
|
|
38
|
+
if (a.length !== bArr.length) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
33
41
|
return a.every((v: unknown, i: number) => valuesEqual(v, bArr[i]));
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -39,8 +47,12 @@ function valuesEqual(a: unknown, b: unknown): boolean {
|
|
|
39
47
|
const keysA = Object.keys(aObj).filter((k: string) => !k.startsWith("on") && k !== "$ref");
|
|
40
48
|
const keysB = Object.keys(bObj).filter((k: string) => !k.startsWith("on") && k !== "$ref");
|
|
41
49
|
|
|
42
|
-
if (keysA.length !== keysB.length)
|
|
43
|
-
|
|
50
|
+
if (keysA.length !== keysB.length) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (!keysA.every((k: string) => keysB.includes(k))) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
44
56
|
|
|
45
57
|
return keysA.every((k: string) => valuesEqual(aObj[k], bObj[k]));
|
|
46
58
|
}
|
|
@@ -52,7 +64,9 @@ function valuesEqual(a: unknown, b: unknown): boolean {
|
|
|
52
64
|
* @returns {JxMutableNode[]}
|
|
53
65
|
*/
|
|
54
66
|
function elementChildren(node: JxMutableNode | undefined) {
|
|
55
|
-
if (!node?.children || !Array.isArray(node.children))
|
|
67
|
+
if (!node?.children || !Array.isArray(node.children)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
56
70
|
return node.children.filter(
|
|
57
71
|
(c: JxMutableNode | string) => c != null && typeof c === "object",
|
|
58
72
|
) as JxMutableNode[];
|
|
@@ -94,8 +108,8 @@ export function computeDocumentDiff(
|
|
|
94
108
|
originalDoc: JxMutableNode | undefined,
|
|
95
109
|
currentDoc: JxMutableNode | undefined,
|
|
96
110
|
): DiffResult {
|
|
97
|
-
const diffMap
|
|
98
|
-
const allPaths
|
|
111
|
+
const diffMap = new Map<string, DiffStatus>();
|
|
112
|
+
const allPaths = new Set<string>();
|
|
99
113
|
|
|
100
114
|
/**
|
|
101
115
|
* Walk both trees in parallel and mark differences.
|
|
@@ -107,7 +121,7 @@ export function computeDocumentDiff(
|
|
|
107
121
|
const walk = (
|
|
108
122
|
origNode: JxMutableNode | undefined,
|
|
109
123
|
currNode: JxMutableNode | undefined,
|
|
110
|
-
path
|
|
124
|
+
path = "",
|
|
111
125
|
) => {
|
|
112
126
|
const pathKey = path || "/";
|
|
113
127
|
allPaths.add(pathKey);
|
|
@@ -150,7 +164,6 @@ export function computeDocumentDiff(
|
|
|
150
164
|
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
151
165
|
markRecursive(children[i], "removed", childPath, diffMap, allPaths);
|
|
152
166
|
}
|
|
153
|
-
return;
|
|
154
167
|
}
|
|
155
168
|
};
|
|
156
169
|
|
|
@@ -180,9 +193,12 @@ export function computeDocumentDiff(
|
|
|
180
193
|
};
|
|
181
194
|
|
|
182
195
|
// Propagate upwards until no more changes
|
|
183
|
-
|
|
196
|
+
let propagating = true;
|
|
197
|
+
while (propagating) {
|
|
198
|
+
propagating = propagateModified();
|
|
199
|
+
}
|
|
184
200
|
|
|
185
|
-
return { byPath: diffMap
|
|
201
|
+
return { allPaths, byPath: diffMap };
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/**
|
|
@@ -191,9 +207,10 @@ export function computeDocumentDiff(
|
|
|
191
207
|
* @param {HTMLElement} canvas
|
|
192
208
|
*/
|
|
193
209
|
export function clearDiffHighlight(canvas: HTMLElement) {
|
|
194
|
-
canvas
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
210
|
+
const marked = canvas.querySelectorAll(
|
|
211
|
+
".element-diff-added, .element-diff-removed, .element-diff-modified",
|
|
212
|
+
);
|
|
213
|
+
for (const el of marked) {
|
|
214
|
+
el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
|
|
215
|
+
}
|
|
199
216
|
}
|