@jxsuite/studio 0.27.0 → 0.28.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 +33900 -56705
- package/dist/studio.js.map +128 -396
- package/package.json +4 -10
- package/src/browse/browse.ts +62 -22
- package/src/canvas/canvas-live-render.ts +57 -55
- package/src/canvas/canvas-render.ts +50 -21
- package/src/editor/context-menu.ts +4 -1
- package/src/editor/convert-to-component.ts +8 -2
- package/src/editor/inline-edit.ts +7 -2
- package/src/editor/insertion-helper.ts +12 -3
- package/src/editor/slash-menu.ts +5 -1
- package/src/files/file-ops.ts +121 -102
- package/src/files/files.ts +53 -35
- package/src/format/constraints.ts +55 -0
- package/src/format/format-host.ts +212 -0
- package/src/new-project/new-project-modal.ts +18 -3
- package/src/panels/ai-panel.ts +3 -1
- package/src/panels/block-action-bar.ts +4 -2
- package/src/panels/canvas-dnd.ts +37 -8
- package/src/panels/data-explorer.ts +9 -2
- package/src/panels/editors.ts +1 -1
- package/src/panels/git-panel.ts +18 -8
- package/src/panels/head-panel.ts +12 -5
- package/src/panels/left-panel.ts +7 -4
- package/src/panels/panel-events.ts +3 -1
- package/src/panels/properties-panel.ts +9 -3
- package/src/panels/quick-search.ts +10 -10
- package/src/panels/right-panel.ts +6 -2
- package/src/panels/shared.ts +6 -1
- package/src/panels/signals-panel.ts +35 -10
- package/src/panels/style-inputs.ts +5 -1
- package/src/panels/style-panel.ts +6 -2
- package/src/panels/stylebook-layers-panel.ts +12 -3
- package/src/panels/stylebook-panel.ts +24 -8
- package/src/panels/toolbar.ts +16 -3
- package/src/platforms/devserver.ts +32 -3
- package/src/resize-edges.ts +12 -2
- package/src/services/cem-export.ts +9 -3
- package/src/settings/content-types-editor.ts +7 -2
- package/src/settings/defs-editor.ts +6 -1
- package/src/settings/general-settings.ts +3 -1
- package/src/settings/schema-field-ui.ts +11 -3
- package/src/settings/settings-modal.ts +4 -1
- package/src/state.ts +5 -1
- package/src/studio.ts +27 -16
- package/src/tabs/tab.ts +29 -6
- package/src/tabs/transact.ts +4 -1
- package/src/types.ts +15 -6
- package/src/ui/button-group.ts +6 -1
- package/src/ui/expression-editor.ts +14 -3
- package/src/ui/layers.ts +5 -1
- package/src/ui/unit-selector.ts +6 -1
- package/src/utils/canvas-media.ts +1 -1
- package/src/utils/google-fonts.ts +4 -1
- package/src/utils/studio-utils.ts +7 -1
- package/src/view.ts +8 -2
- package/src/markdown/md-allowlist.ts +0 -104
- package/src/markdown/md-convert.ts +0 -846
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
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.
|
|
36
|
-
"@jxsuite/runtime": "^0.
|
|
37
|
-
"@jxsuite/schema": "^0.
|
|
35
|
+
"@jxsuite/parser": "^0.28.0",
|
|
36
|
+
"@jxsuite/runtime": "^0.28.0",
|
|
37
|
+
"@jxsuite/schema": "^0.28.0",
|
|
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",
|
|
@@ -75,12 +75,6 @@
|
|
|
75
75
|
"lit-html": "^3.3.3",
|
|
76
76
|
"monaco-editor": "^0.55.1",
|
|
77
77
|
"quikchat": "^1.2.7",
|
|
78
|
-
"remark-directive": "^4.0.0",
|
|
79
|
-
"remark-frontmatter": "^5.0.0",
|
|
80
|
-
"remark-gfm": "^4.0.1",
|
|
81
|
-
"remark-parse": "^11.0.0",
|
|
82
|
-
"remark-stringify": "^11.0.0",
|
|
83
|
-
"unified": "^11.0.5",
|
|
84
78
|
"yaml": "^2.9.0"
|
|
85
79
|
},
|
|
86
80
|
"devDependencies": {
|
package/src/browse/browse.ts
CHANGED
|
@@ -20,7 +20,14 @@ import { componentRegistry } from "../files/components";
|
|
|
20
20
|
import { showDialog, renderPopover } from "../ui/layers";
|
|
21
21
|
import { renderComponentPreview } from "../panels/stylebook-panel";
|
|
22
22
|
import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
23
|
-
import {
|
|
23
|
+
import { parseSourceForPath } from "../files/file-ops";
|
|
24
|
+
import {
|
|
25
|
+
loadFormats,
|
|
26
|
+
formatForPath,
|
|
27
|
+
formatByName,
|
|
28
|
+
documentExtensions,
|
|
29
|
+
defaultContentFormat,
|
|
30
|
+
} from "../format/format-host";
|
|
24
31
|
|
|
25
32
|
import type { ComponentEntry } from "../files/components";
|
|
26
33
|
import type { ContentTypeDef } from "@jxsuite/schema/types";
|
|
@@ -62,7 +69,13 @@ const MEDIA_EXTENSIONS = new Set([
|
|
|
62
69
|
let activeCategory = "all";
|
|
63
70
|
let searchQuery = "";
|
|
64
71
|
let viewMode: "grid" | "table" = "grid";
|
|
65
|
-
let fileCache: {
|
|
72
|
+
let fileCache: {
|
|
73
|
+
name: string;
|
|
74
|
+
path: string;
|
|
75
|
+
type: string;
|
|
76
|
+
category: string;
|
|
77
|
+
ext: string;
|
|
78
|
+
}[] = [];
|
|
66
79
|
let loading = false;
|
|
67
80
|
/** Track which projectDirs were used for the last load, so we re-scan when they change. */
|
|
68
81
|
let lastProjectDirsKey = "";
|
|
@@ -117,7 +130,13 @@ async function collectFiles(
|
|
|
117
130
|
dir: string,
|
|
118
131
|
platform: ReturnType<typeof getPlatform>,
|
|
119
132
|
): Promise<{ name: string; path: string; type: string; category: string; ext: string }[]> {
|
|
120
|
-
const results: {
|
|
133
|
+
const results: {
|
|
134
|
+
name: string;
|
|
135
|
+
path: string;
|
|
136
|
+
type: string;
|
|
137
|
+
category: string;
|
|
138
|
+
ext: string;
|
|
139
|
+
}[] = [];
|
|
121
140
|
try {
|
|
122
141
|
const entries = await platform.listDirectory(dir);
|
|
123
142
|
for (const entry of entries) {
|
|
@@ -200,12 +219,16 @@ function filteredFiles() {
|
|
|
200
219
|
|
|
201
220
|
// ─── Entity types for "New +" button ────────────────────────────────────────
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
function getEntityTypes() {
|
|
223
|
+
const pageExt = documentExtensions("page")[0] ?? ".json";
|
|
224
|
+
const contentExt = defaultContentFormat()?.extensions[0] ?? ".json";
|
|
225
|
+
return [
|
|
226
|
+
{ key: "page", label: "Page", dir: "pages", ext: pageExt },
|
|
227
|
+
{ key: "layout", label: "Layout", dir: "layouts", ext: ".json" },
|
|
228
|
+
{ key: "component", label: "Component", dir: "components", ext: ".json" },
|
|
229
|
+
{ key: "content", label: "Content", dir: "content", ext: contentExt },
|
|
230
|
+
];
|
|
231
|
+
}
|
|
209
232
|
|
|
210
233
|
/**
|
|
211
234
|
* Build frontmatter YAML from a content type's schema properties.
|
|
@@ -220,7 +243,7 @@ function buildFrontmatterYaml(contentTypeName: string) {
|
|
|
220
243
|
|
|
221
244
|
let yaml = "";
|
|
222
245
|
for (const [field, def] of Object.entries(col.schema.properties)) {
|
|
223
|
-
const d = /** @type {{ type?: string; format?: string }} */
|
|
246
|
+
const d = /** @type {{ type?: string; format?: string }} */ def;
|
|
224
247
|
yaml += `${field}: ${yamlDefault(d.type || "", d.format || "")}\n`;
|
|
225
248
|
}
|
|
226
249
|
return yaml || "title: Untitled\n";
|
|
@@ -241,7 +264,12 @@ function getContentTypeTypes() {
|
|
|
241
264
|
key: `contentType:${name}`,
|
|
242
265
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
243
266
|
dir,
|
|
244
|
-
ext:
|
|
267
|
+
ext:
|
|
268
|
+
d.format === "json"
|
|
269
|
+
? ".json"
|
|
270
|
+
: (formatByName(d.format)?.extensions[0] ??
|
|
271
|
+
defaultContentFormat()?.extensions[0] ??
|
|
272
|
+
".json"),
|
|
245
273
|
contentTypeName: name,
|
|
246
274
|
};
|
|
247
275
|
});
|
|
@@ -261,7 +289,8 @@ async function handleNewEntity(
|
|
|
261
289
|
) {
|
|
262
290
|
const isContentType = typeKey.startsWith("contentType:");
|
|
263
291
|
const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
|
|
264
|
-
|
|
292
|
+
await loadFormats();
|
|
293
|
+
const allTypes = [...getEntityTypes(), ...getContentTypeTypes()];
|
|
265
294
|
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
266
295
|
if (!typeInfo) return;
|
|
267
296
|
|
|
@@ -275,11 +304,13 @@ async function handleNewEntity(
|
|
|
275
304
|
const filePath = `${typeInfo.dir}/${slug}${typeInfo.ext}`;
|
|
276
305
|
|
|
277
306
|
let content;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
307
|
+
const entityFormat = formatForPath(filePath);
|
|
308
|
+
if (entityFormat) {
|
|
309
|
+
if (contentTypeName) {
|
|
310
|
+
content = `---\n${buildFrontmatterYaml(contentTypeName)}---\n\n`;
|
|
311
|
+
} else {
|
|
312
|
+
content = entityFormat.studio?.newFileTemplate ?? "";
|
|
313
|
+
}
|
|
283
314
|
} else {
|
|
284
315
|
content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
|
|
285
316
|
}
|
|
@@ -589,9 +620,10 @@ async function renderDocPreview(filePath: string) {
|
|
|
589
620
|
const platform = getPlatform();
|
|
590
621
|
const content = await platform.readFile(filePath);
|
|
591
622
|
setSkipServerFunctions(true);
|
|
623
|
+
await loadFormats();
|
|
592
624
|
let doc;
|
|
593
|
-
if (filePath
|
|
594
|
-
const result = await
|
|
625
|
+
if (formatForPath(filePath)) {
|
|
626
|
+
const result = await parseSourceForPath(filePath, content);
|
|
595
627
|
doc = result.document;
|
|
596
628
|
} else {
|
|
597
629
|
doc = JSON.parse(content);
|
|
@@ -623,7 +655,7 @@ async function loadPreview(el: Element, file: { path: string; category: string }
|
|
|
623
655
|
} else {
|
|
624
656
|
preview = ((await renderDocPreview(file.path)) as HTMLElement | undefined) || undefined;
|
|
625
657
|
}
|
|
626
|
-
if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */
|
|
658
|
+
if (preview) _previewCache.set(file.path, /** @type {HTMLElement} */ preview);
|
|
627
659
|
} catch {
|
|
628
660
|
return;
|
|
629
661
|
}
|
|
@@ -639,7 +671,13 @@ async function loadPreview(el: Element, file: { path: string; category: string }
|
|
|
639
671
|
* @param {{ openFile: (path: string) => void }} ctx
|
|
640
672
|
*/
|
|
641
673
|
function renderCard(
|
|
642
|
-
file: {
|
|
674
|
+
file: {
|
|
675
|
+
name: string;
|
|
676
|
+
path: string;
|
|
677
|
+
type: string;
|
|
678
|
+
category: string;
|
|
679
|
+
ext: string;
|
|
680
|
+
},
|
|
643
681
|
container: HTMLElement,
|
|
644
682
|
ctx: { openFile: (path: string) => void },
|
|
645
683
|
) {
|
|
@@ -736,7 +774,9 @@ export async function renderBrowse(
|
|
|
736
774
|
@change=${(e: Event) =>
|
|
737
775
|
handleNewEntity((e.target as HTMLSelectElement).value, container, ctx)}
|
|
738
776
|
>
|
|
739
|
-
${
|
|
777
|
+
${getEntityTypes().map(
|
|
778
|
+
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
779
|
+
)}
|
|
740
780
|
${contentTypeTypes.length
|
|
741
781
|
? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
|
|
742
782
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
@@ -131,7 +131,11 @@ export function initCanvasLiveRender(ctx: { getCanvasMode: () => string }) {
|
|
|
131
131
|
*/
|
|
132
132
|
export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl: HTMLElement) {
|
|
133
133
|
const tab = activeTab.value;
|
|
134
|
-
const S = {
|
|
134
|
+
const S = {
|
|
135
|
+
documentPath: tab?.documentPath,
|
|
136
|
+
mode: tab?.doc.mode,
|
|
137
|
+
document: tab?.doc.document,
|
|
138
|
+
};
|
|
135
139
|
const canvasMode = _ctx!.getCanvasMode();
|
|
136
140
|
|
|
137
141
|
if (S.documentPath !== _failedElementsDocPath) {
|
|
@@ -384,67 +388,65 @@ export async function renderCanvasLive(gen: number, doc: JxMutableNode, canvasEl
|
|
|
384
388
|
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
385
389
|
// Bail out if a newer render started while buildScope was running
|
|
386
390
|
if (gen !== view.renderGeneration) return null;
|
|
387
|
-
const el = /** @type {HTMLElement} */ (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
391
|
+
const el = /** @type {HTMLElement} */ runtimeRenderNode(renderDoc, $defs, {
|
|
392
|
+
onNodeCreated(
|
|
393
|
+
el: HTMLElement | Text,
|
|
394
|
+
path: (string | number)[],
|
|
395
|
+
def: Record<string, unknown>,
|
|
396
|
+
) {
|
|
397
|
+
if (!(el instanceof HTMLElement)) return;
|
|
398
|
+
// Track layout-originated elements — don't store in elToPath to avoid
|
|
399
|
+
// path collisions with remapped page content paths
|
|
400
|
+
if (layoutWrapped && def?.$__layout) {
|
|
401
|
+
layoutElements.add(el);
|
|
402
|
+
if (el.setAttribute) el.setAttribute("data-jx-layout", "");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
402
405
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
406
|
+
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
407
|
+
// relative to the original page document (which is what S.document holds)
|
|
408
|
+
let mappedPath = path;
|
|
409
|
+
if (layoutWrapped && pageContentPrefix) {
|
|
410
|
+
const pfx = pageContentPrefix;
|
|
411
|
+
if (
|
|
412
|
+
path.length >= pfx.length &&
|
|
413
|
+
pfx.every((seg: string | number, i: number) => path[i] === seg)
|
|
414
|
+
) {
|
|
415
|
+
mappedPath = ["children", ...path.slice(pfx.length)];
|
|
414
416
|
}
|
|
417
|
+
}
|
|
415
418
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
break;
|
|
419
|
+
// Remap $map paths: wrapper and template children → real document paths
|
|
420
|
+
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
421
|
+
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
422
|
+
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
423
|
+
for (let i = 0; i < mappedPath.length - 1; i++) {
|
|
424
|
+
if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
|
|
425
|
+
const parentKey = mappedPath.slice(0, i).join("/");
|
|
426
|
+
if (mapParentPaths.has(parentKey)) {
|
|
427
|
+
if (mappedPath.length === i + 2) {
|
|
428
|
+
mappedPath = mappedPath.slice(0, i + 1);
|
|
429
|
+
} else if (
|
|
430
|
+
mappedPath.length >= i + 4 &&
|
|
431
|
+
mappedPath[i + 2] === "children" &&
|
|
432
|
+
mappedPath[i + 3] === 0
|
|
433
|
+
) {
|
|
434
|
+
mappedPath = [
|
|
435
|
+
...mappedPath.slice(0, i),
|
|
436
|
+
"children",
|
|
437
|
+
"map",
|
|
438
|
+
...mappedPath.slice(i + 4),
|
|
439
|
+
];
|
|
439
440
|
}
|
|
441
|
+
break;
|
|
440
442
|
}
|
|
441
443
|
}
|
|
442
444
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
);
|
|
445
|
+
}
|
|
446
|
+
elToPath.set(el, mappedPath);
|
|
447
|
+
},
|
|
448
|
+
_path: [],
|
|
449
|
+
});
|
|
448
450
|
if (canvasMode === "design" || canvasMode === "edit") {
|
|
449
451
|
// Disable pointer events on all rendered elements for edit mode
|
|
450
452
|
el.style.pointerEvents = "none";
|
|
@@ -11,7 +11,8 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
|
11
11
|
import { canvasWrap, canvasPanels, updateCanvas } from "../store";
|
|
12
12
|
import { activeTab } from "../workspace/workspace";
|
|
13
13
|
import { view } from "../view";
|
|
14
|
-
import {
|
|
14
|
+
import { parseSourceForPath, serializeDocument } from "../files/file-ops";
|
|
15
|
+
import { formatByName, formatForPath } from "../format/format-host";
|
|
15
16
|
import { renderWelcome } from "../panels/welcome-screen";
|
|
16
17
|
import { projectState } from "../state";
|
|
17
18
|
import {
|
|
@@ -77,16 +78,17 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
|
|
|
77
78
|
|
|
78
79
|
/** Monaco language for the source view of a tab's document. */
|
|
79
80
|
function sourceLang(tab: import("../tabs/tab.js").Tab) {
|
|
80
|
-
|
|
81
|
+
const format = formatByName(tab.doc.sourceFormat);
|
|
82
|
+
if (format) return format.mediaType?.split("/").pop() ?? "plaintext";
|
|
81
83
|
return (tab.documentPath || "").endsWith(".js") ? "javascript" : "json";
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
|
-
* The full source text for the source view.
|
|
86
|
-
* (frontmatter YAML
|
|
87
|
+
* The full source text for the source view. Format-class files serialize to their on-disk form
|
|
88
|
+
* (e.g. frontmatter YAML plus the body for markdown), not just the JSON of the body tree.
|
|
87
89
|
*/
|
|
88
|
-
function sourceContent(tab: import("../tabs/tab.js").Tab, lang: string) {
|
|
89
|
-
if (
|
|
90
|
+
async function sourceContent(tab: import("../tabs/tab.js").Tab, lang: string) {
|
|
91
|
+
if (formatByName(tab.doc.sourceFormat)) return serializeDocument(tab);
|
|
90
92
|
if (lang === "javascript") return tab.doc.document?.toString?.() || "";
|
|
91
93
|
return JSON.stringify(tab.doc.document, null, 2);
|
|
92
94
|
}
|
|
@@ -102,7 +104,11 @@ export function renderCanvas() {
|
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
104
106
|
const ctx = _ctx as CanvasRenderCtx;
|
|
105
|
-
const S = {
|
|
107
|
+
const S = {
|
|
108
|
+
document: tab.doc.document,
|
|
109
|
+
ui: tab.session.ui,
|
|
110
|
+
mode: tab.doc.mode,
|
|
111
|
+
};
|
|
106
112
|
const canvasMode = ctx.getCanvasMode();
|
|
107
113
|
|
|
108
114
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
@@ -139,11 +145,17 @@ export function renderCanvas() {
|
|
|
139
145
|
// the editing surface here, mirroring the panel draft-state behaviour).
|
|
140
146
|
if (canvasMode === "source" && view.monacoEditor) {
|
|
141
147
|
const editor = view.monacoEditor;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
sourceContent(tab, sourceLang(tab))
|
|
149
|
+
.then((newVal) => {
|
|
150
|
+
if (view.monacoEditor !== editor) return;
|
|
151
|
+
if (!editor.hasTextFocus() && editor.getValue() !== newVal) {
|
|
152
|
+
editor._ignoreNextChange = true;
|
|
153
|
+
editor.setValue(newVal);
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.catch(() => {
|
|
157
|
+
// Serialization unavailable (e.g. format service unreachable) — keep the current buffer
|
|
158
|
+
});
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -251,9 +263,19 @@ export function renderCanvas() {
|
|
|
251
263
|
|
|
252
264
|
const filePath = tab.documentPath || "document.json";
|
|
253
265
|
const lang = sourceLang(tab);
|
|
254
|
-
const content = sourceContent(tab, lang);
|
|
255
266
|
const modelUri = monaco.Uri.parse("file:///" + filePath);
|
|
256
|
-
const model = monaco.editor.createModel(
|
|
267
|
+
const model = monaco.editor.createModel("", lang, modelUri);
|
|
268
|
+
sourceContent(tab, lang)
|
|
269
|
+
.then((content) => {
|
|
270
|
+
const editor = view.monacoEditor;
|
|
271
|
+
if (editor && editor.getModel() === model) {
|
|
272
|
+
editor._ignoreNextChange = true;
|
|
273
|
+
model.setValue(content);
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
.catch(() => {
|
|
277
|
+
// Serialization unavailable — leave the buffer empty rather than crash the render
|
|
278
|
+
});
|
|
257
279
|
view.monacoEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
|
|
258
280
|
model,
|
|
259
281
|
theme: "vs-dark",
|
|
@@ -280,15 +302,18 @@ export function renderCanvas() {
|
|
|
280
302
|
debounce = setTimeout(async () => {
|
|
281
303
|
const tab = activeTab.value;
|
|
282
304
|
if (!tab) return;
|
|
283
|
-
if (
|
|
305
|
+
if (formatByName(tab.doc.sourceFormat) && tab.documentPath) {
|
|
284
306
|
try {
|
|
285
|
-
// Parse the full
|
|
286
|
-
const { document, frontmatter } = await
|
|
307
|
+
// Parse the full source back into body + frontmatter (title, $head, etc.).
|
|
308
|
+
const { document, frontmatter } = await parseSourceForPath(
|
|
309
|
+
tab.documentPath,
|
|
310
|
+
editor.getValue(),
|
|
311
|
+
);
|
|
287
312
|
tab.doc.document = document as JxMutableNode;
|
|
288
313
|
tab.doc.content.frontmatter = frontmatter;
|
|
289
314
|
tab.doc.dirty = true;
|
|
290
315
|
} catch {
|
|
291
|
-
// Unparseable
|
|
316
|
+
// Unparseable source — don't update state
|
|
292
317
|
}
|
|
293
318
|
} else if (lang === "json") {
|
|
294
319
|
try {
|
|
@@ -354,14 +379,18 @@ export function renderCanvas() {
|
|
|
354
379
|
|
|
355
380
|
/** @param {string} content */
|
|
356
381
|
const parseContent = (content: string) => {
|
|
357
|
-
|
|
358
|
-
|
|
382
|
+
const fmtPath = gitDiffState.filePath ?? "";
|
|
383
|
+
if (formatForPath(fmtPath)) {
|
|
384
|
+
return parseSourceForPath(fmtPath, content).then((r) => r.document);
|
|
359
385
|
}
|
|
360
386
|
return Promise.resolve().then(() => {
|
|
361
387
|
try {
|
|
362
388
|
return JSON.parse(content);
|
|
363
389
|
} catch {
|
|
364
|
-
return {
|
|
390
|
+
return {
|
|
391
|
+
tagName: "div",
|
|
392
|
+
children: [{ tagName: "p", textContent: "Failed to parse" }],
|
|
393
|
+
};
|
|
365
394
|
}
|
|
366
395
|
});
|
|
367
396
|
};
|
|
@@ -216,7 +216,10 @@ export function dismissContextMenu() {
|
|
|
216
216
|
export function showContextMenu(
|
|
217
217
|
e: MouseEvent,
|
|
218
218
|
path: JxPath,
|
|
219
|
-
opts: {
|
|
219
|
+
opts: {
|
|
220
|
+
onEditComponent?: (path: string) => void;
|
|
221
|
+
rerender?: () => void;
|
|
222
|
+
} = {},
|
|
220
223
|
) {
|
|
221
224
|
e.preventDefault();
|
|
222
225
|
dismissContextMenu();
|
|
@@ -102,10 +102,16 @@ function extractComponentDef(node: JxMutableNode) {
|
|
|
102
102
|
function validateName(val: string) {
|
|
103
103
|
val = val.trim().toLowerCase();
|
|
104
104
|
if (!val.includes("-")) {
|
|
105
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
valid: false,
|
|
107
|
+
error: "Name must contain a hyphen (e.g. my-component)",
|
|
108
|
+
};
|
|
106
109
|
}
|
|
107
110
|
if (!VALID_NAME.test(val)) {
|
|
108
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
error: "Lowercase letters, digits, and hyphens only",
|
|
114
|
+
};
|
|
109
115
|
}
|
|
110
116
|
const exists = componentRegistry.some((c: JxMutableNode) => c.tagName === val);
|
|
111
117
|
if (exists) {
|
|
@@ -526,7 +526,12 @@ function domNodeToJx(node: Node) {
|
|
|
526
526
|
const result: JxMutableNode = { tagName: tag };
|
|
527
527
|
|
|
528
528
|
// Map browser execCommand output to our tag conventions
|
|
529
|
-
const tagMap: Record<string, string> = {
|
|
529
|
+
const tagMap: Record<string, string> = {
|
|
530
|
+
b: "strong",
|
|
531
|
+
i: "em",
|
|
532
|
+
s: "del",
|
|
533
|
+
strike: "del",
|
|
534
|
+
};
|
|
530
535
|
if (tagMap[tag]) result.tagName = tagMap[tag];
|
|
531
536
|
|
|
532
537
|
// Attributes
|
|
@@ -549,7 +554,7 @@ function domNodeToJx(node: Node) {
|
|
|
549
554
|
result.children = [];
|
|
550
555
|
for (const child of childNodes) {
|
|
551
556
|
const jsx = domNodeToJx(child);
|
|
552
|
-
if (jsx) result.children.push(/** @type {JxMutableNode} */
|
|
557
|
+
if (jsx) result.children.push(/** @type {JxMutableNode} */ jsx);
|
|
553
558
|
}
|
|
554
559
|
}
|
|
555
560
|
|
|
@@ -44,7 +44,12 @@ let _helper: HTMLElement | null = null;
|
|
|
44
44
|
|
|
45
45
|
let _currentAnchor: HTMLElement | null = null;
|
|
46
46
|
|
|
47
|
-
let _insertionPoint: {
|
|
47
|
+
let _insertionPoint: {
|
|
48
|
+
edge: string;
|
|
49
|
+
path: JxPath;
|
|
50
|
+
parentPath: JxPath;
|
|
51
|
+
idx: number;
|
|
52
|
+
} | null = null;
|
|
48
53
|
|
|
49
54
|
let _abort: AbortController | null = null;
|
|
50
55
|
|
|
@@ -87,8 +92,12 @@ export function mount(ctx: InsertionHelperContext) {
|
|
|
87
92
|
viewport.on("mousemove", { signal: _abort.signal }).subscribe({ next: onMouseMove });
|
|
88
93
|
viewport.on("mouseleave", { signal: _abort.signal }).subscribe({ next: hide });
|
|
89
94
|
} else {
|
|
90
|
-
panel.viewport.addEventListener("mousemove", onMouseMove, {
|
|
91
|
-
|
|
95
|
+
panel.viewport.addEventListener("mousemove", onMouseMove, {
|
|
96
|
+
signal: _abort.signal,
|
|
97
|
+
});
|
|
98
|
+
panel.viewport.addEventListener("mouseleave", hide, {
|
|
99
|
+
signal: _abort.signal,
|
|
100
|
+
});
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
package/src/editor/slash-menu.ts
CHANGED
|
@@ -79,7 +79,11 @@ export function isSlashMenuOpen() {
|
|
|
79
79
|
export function showSlashMenu(
|
|
80
80
|
anchorEl: HTMLElement,
|
|
81
81
|
filter: string,
|
|
82
|
-
cbs: {
|
|
82
|
+
cbs: {
|
|
83
|
+
onSelect: (cmd: SlashCommand) => void;
|
|
84
|
+
showFilter?: boolean;
|
|
85
|
+
commands?: SlashCommand[];
|
|
86
|
+
},
|
|
83
87
|
) {
|
|
84
88
|
callbacks = cbs;
|
|
85
89
|
_anchorEl = anchorEl;
|