@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,19 +22,24 @@
|
|
|
22
22
|
"provenance": true
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build
|
|
25
|
+
"build": "bun build ./src/studio.js --outdir dist --target browser --sourcemap=linked && bun build ./node_modules/monaco-editor/esm/vs/editor/editor.worker.js --outdir dist/workers --target browser --entry-naming editor.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/json/json.worker.js --outdir dist/workers --target browser --entry-naming json.worker.js && bun build ./node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js --outdir dist/workers --target browser --entry-naming ts.worker.js",
|
|
26
26
|
"gen:webdata": "bun run scripts/gen-webdata.js",
|
|
27
|
-
"test": "bun test",
|
|
27
|
+
"test": "bun test --isolate",
|
|
28
|
+
"test:coverage": "bun test --isolate --coverage",
|
|
28
29
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
33
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/
|
|
34
|
+
"@jxsuite/parser": "^0.21.0",
|
|
35
|
+
"@jxsuite/runtime": "^0.21.0",
|
|
36
|
+
"@jxsuite/schema": "^0.21.0",
|
|
34
37
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
38
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
39
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
37
40
|
"@spectrum-web-components/action-group": "^1.12.1",
|
|
41
|
+
"@spectrum-web-components/base": "^1.12.1",
|
|
42
|
+
"@spectrum-web-components/button": "^1.12.1",
|
|
38
43
|
"@spectrum-web-components/checkbox": "^1.12.1",
|
|
39
44
|
"@spectrum-web-components/color-area": "^1.12.1",
|
|
40
45
|
"@spectrum-web-components/color-handle": "^1.12.1",
|
|
@@ -42,7 +47,10 @@
|
|
|
42
47
|
"@spectrum-web-components/combobox": "^1.12.1",
|
|
43
48
|
"@spectrum-web-components/dialog": "^1.12.1",
|
|
44
49
|
"@spectrum-web-components/divider": "^1.12.1",
|
|
50
|
+
"@spectrum-web-components/field-label": "^1.12.1",
|
|
51
|
+
"@spectrum-web-components/help-text": "^1.12.1",
|
|
45
52
|
"@spectrum-web-components/icon": "^1.12.1",
|
|
53
|
+
"@spectrum-web-components/icons-ui": "^1.12.1",
|
|
46
54
|
"@spectrum-web-components/icons-workflow": "^1.12.1",
|
|
47
55
|
"@spectrum-web-components/menu": "^1.12.1",
|
|
48
56
|
"@spectrum-web-components/number-field": "^1.12.1",
|
|
@@ -60,9 +68,12 @@
|
|
|
60
68
|
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
69
|
"@spectrum-web-components/toast": "^1.12.1",
|
|
62
70
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
63
|
-
"@
|
|
71
|
+
"@spectrum-web-components/underlay": "^1.12.1",
|
|
72
|
+
"@vue/reactivity": "3.5.35",
|
|
73
|
+
"lit": "^3.3.3",
|
|
64
74
|
"lit-html": "^3.3.3",
|
|
65
75
|
"monaco-editor": "^0.55.1",
|
|
76
|
+
"quikchat": "^1.2.7",
|
|
66
77
|
"remark-directive": "^4.0.0",
|
|
67
78
|
"remark-frontmatter": "^5.0.0",
|
|
68
79
|
"remark-gfm": "^4.0.1",
|
|
@@ -73,9 +84,10 @@
|
|
|
73
84
|
},
|
|
74
85
|
"devDependencies": {
|
|
75
86
|
"@happy-dom/global-registrator": "^20.9.0",
|
|
76
|
-
"@
|
|
87
|
+
"@types/mdast": "^4.0.4",
|
|
88
|
+
"@webref/css": "^8.5.7",
|
|
77
89
|
"@webref/elements": "^2.7.1",
|
|
78
|
-
"@webref/idl": "^3.
|
|
90
|
+
"@webref/idl": "^3.79.0"
|
|
79
91
|
},
|
|
80
92
|
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
81
93
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { html } from "lit-html";
|
|
7
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
7
8
|
import { renderBrowse } from "./browse.js";
|
|
8
9
|
import { openFileInTab } from "../files/files.js";
|
|
9
10
|
import { openModal } from "../ui/layers.js";
|
|
@@ -31,23 +32,25 @@ export function openBrowseModal() {
|
|
|
31
32
|
<sp-icon-close slot="icon"></sp-icon-close>
|
|
32
33
|
</sp-action-button>
|
|
33
34
|
</div>
|
|
34
|
-
<div
|
|
35
|
+
<div
|
|
36
|
+
class="browse-modal-content"
|
|
37
|
+
${ref((el) => {
|
|
38
|
+
if (el) {
|
|
39
|
+
requestAnimationFrame(() => {
|
|
40
|
+
renderBrowse(/** @type {HTMLElement} */ (el), {
|
|
41
|
+
openFile: (/** @type {string} */ path) => {
|
|
42
|
+
closeBrowseModal();
|
|
43
|
+
openFileInTab(path);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
})}
|
|
49
|
+
></div>
|
|
35
50
|
</div>
|
|
36
51
|
`;
|
|
37
52
|
|
|
38
53
|
_handle = openModal(tpl);
|
|
39
|
-
|
|
40
|
-
requestAnimationFrame(() => {
|
|
41
|
-
const container = _handle?.host.querySelector(".browse-modal-content");
|
|
42
|
-
if (container) {
|
|
43
|
-
renderBrowse(/** @type {HTMLElement} */ (container), {
|
|
44
|
-
openFile: (/** @type {string} */ path) => {
|
|
45
|
-
closeBrowseModal();
|
|
46
|
-
openFileInTab(path);
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export function closeBrowseModal() {
|
package/src/browse/browse.js
CHANGED
|
@@ -15,6 +15,8 @@ import { yamlDefault } from "../settings/schema-field-ui.js";
|
|
|
15
15
|
import { invalidateMediaCache } from "../ui/media-picker.js";
|
|
16
16
|
import { statusMessage } from "../panels/statusbar.js";
|
|
17
17
|
import { componentRegistry } from "../files/components.js";
|
|
18
|
+
|
|
19
|
+
/** @typedef {import("../files/components.js").ComponentEntry} ComponentEntry */
|
|
18
20
|
import { showDialog, renderPopover } from "../ui/layers.js";
|
|
19
21
|
import { renderComponentPreview } from "../panels/stylebook-panel.js";
|
|
20
22
|
import { renderNode, buildScope, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
@@ -150,9 +152,9 @@ function contentTypeFor(filePath) {
|
|
|
150
152
|
const config = projectState?.projectConfig;
|
|
151
153
|
if (!config?.contentTypes) return null;
|
|
152
154
|
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
153
|
-
const d = /** @type {
|
|
155
|
+
const d = /** @type {ContentTypeDef} */ (def);
|
|
154
156
|
if (!d.source) continue;
|
|
155
|
-
const prefix = d.source.replace(/^\.\//, "").
|
|
157
|
+
const prefix = d.source.replace(/^\.\//, "").replace(/\/$/, "");
|
|
156
158
|
if (filePath.startsWith(prefix + "/") || filePath === prefix) {
|
|
157
159
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
158
160
|
}
|
|
@@ -215,8 +217,8 @@ function buildFrontmatterYaml(contentTypeName) {
|
|
|
215
217
|
|
|
216
218
|
let yaml = "";
|
|
217
219
|
for (const [field, def] of Object.entries(col.schema.properties)) {
|
|
218
|
-
const d = /** @type {
|
|
219
|
-
yaml += `${field}: ${yamlDefault(d.type, d.format)}\n`;
|
|
220
|
+
const d = /** @type {{ type?: string; format?: string }} */ (def);
|
|
221
|
+
yaml += `${field}: ${yamlDefault(d.type || "", d.format || "")}\n`;
|
|
220
222
|
}
|
|
221
223
|
return yaml || "title: Untitled\n";
|
|
222
224
|
}
|
|
@@ -230,13 +232,13 @@ function getContentTypeTypes() {
|
|
|
230
232
|
const config = projectState?.projectConfig;
|
|
231
233
|
if (!config?.contentTypes) return [];
|
|
232
234
|
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
233
|
-
const d = /** @type {
|
|
234
|
-
const dir = d.source ? d.source.replace(/^\.\//, "").
|
|
235
|
+
const d = /** @type {ContentTypeDef} */ (def);
|
|
236
|
+
const dir = d.source ? d.source.replace(/^\.\//, "").replace(/\/$/, "") : name;
|
|
235
237
|
return {
|
|
236
238
|
key: `contentType:${name}`,
|
|
237
239
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
238
240
|
dir,
|
|
239
|
-
ext: "
|
|
241
|
+
ext: `.${d.format || "md"}`,
|
|
240
242
|
contentTypeName: name,
|
|
241
243
|
};
|
|
242
244
|
});
|
|
@@ -360,7 +362,22 @@ function showBrowseContextMenu(e, file, container, ctx) {
|
|
|
360
362
|
y = e.clientY;
|
|
361
363
|
|
|
362
364
|
_browseCtxHandle = renderPopover(
|
|
363
|
-
html`<sp-popover
|
|
365
|
+
html`<sp-popover
|
|
366
|
+
open
|
|
367
|
+
style="position:fixed;left:${x}px;top:${y}px"
|
|
368
|
+
${ref((el) => {
|
|
369
|
+
if (!el) return;
|
|
370
|
+
requestAnimationFrame(() => {
|
|
371
|
+
const popover = /** @type {HTMLElement} */ (el);
|
|
372
|
+
const menuRect = popover.getBoundingClientRect();
|
|
373
|
+
if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
|
|
374
|
+
if (y + menuRect.height > window.innerHeight)
|
|
375
|
+
y = window.innerHeight - menuRect.height - 4;
|
|
376
|
+
popover.style.left = `${x}px`;
|
|
377
|
+
popover.style.top = `${y}px`;
|
|
378
|
+
});
|
|
379
|
+
})}
|
|
380
|
+
>
|
|
364
381
|
<sp-menu>
|
|
365
382
|
${items.map((item) =>
|
|
366
383
|
item.label === "\u2014"
|
|
@@ -384,18 +401,6 @@ function showBrowseContextMenu(e, file, container, ctx) {
|
|
|
384
401
|
layer: "dialog",
|
|
385
402
|
},
|
|
386
403
|
);
|
|
387
|
-
|
|
388
|
-
requestAnimationFrame(() => {
|
|
389
|
-
const popover = /** @type {HTMLElement | null} */ (
|
|
390
|
-
_browseCtxHandle?.host.querySelector("sp-popover")
|
|
391
|
-
);
|
|
392
|
-
if (!popover) return;
|
|
393
|
-
const menuRect = popover.getBoundingClientRect();
|
|
394
|
-
if (x + menuRect.width > window.innerWidth) x = window.innerWidth - menuRect.width - 4;
|
|
395
|
-
if (y + menuRect.height > window.innerHeight) y = window.innerHeight - menuRect.height - 4;
|
|
396
|
-
popover.style.left = `${x}px`;
|
|
397
|
-
popover.style.top = `${y}px`;
|
|
398
|
-
});
|
|
399
404
|
}
|
|
400
405
|
|
|
401
406
|
/**
|
|
@@ -415,8 +420,8 @@ async function browseRenameFile(file, container, ctx) {
|
|
|
415
420
|
invalidateBrowseCache();
|
|
416
421
|
renderBrowse(container, ctx);
|
|
417
422
|
statusMessage(`Renamed to ${newName}`);
|
|
418
|
-
} catch (/** @type {
|
|
419
|
-
statusMessage(`Error: ${e.message}`);
|
|
423
|
+
} catch (/** @type {unknown} */ e) {
|
|
424
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
420
425
|
}
|
|
421
426
|
}
|
|
422
427
|
|
|
@@ -439,8 +444,8 @@ async function browseDuplicateFile(file, container, ctx) {
|
|
|
439
444
|
invalidateBrowseCache();
|
|
440
445
|
renderBrowse(container, ctx);
|
|
441
446
|
statusMessage(`Duplicated as ${copyName}`);
|
|
442
|
-
} catch (/** @type {
|
|
443
|
-
statusMessage(`Error: ${e.message}`);
|
|
447
|
+
} catch (/** @type {unknown} */ e) {
|
|
448
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
444
449
|
}
|
|
445
450
|
}
|
|
446
451
|
|
|
@@ -458,8 +463,8 @@ async function browseDeleteFile(file, container, ctx) {
|
|
|
458
463
|
invalidateBrowseCache();
|
|
459
464
|
renderBrowse(container, ctx);
|
|
460
465
|
statusMessage(`Deleted ${file.name}`);
|
|
461
|
-
} catch (/** @type {
|
|
462
|
-
statusMessage(`Error: ${e.message}`);
|
|
466
|
+
} catch (/** @type {unknown} */ e) {
|
|
467
|
+
statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
|
|
463
468
|
}
|
|
464
469
|
}
|
|
465
470
|
|
|
@@ -495,7 +500,7 @@ function showRenameDialog(currentName) {
|
|
|
495
500
|
style="width:100%"
|
|
496
501
|
value=${value}
|
|
497
502
|
@input=${(/** @type {Event} */ e) => {
|
|
498
|
-
value = /** @type {
|
|
503
|
+
value = /** @type {HTMLInputElement} */ (e.target).value || "";
|
|
499
504
|
}}
|
|
500
505
|
@keydown=${(/** @type {KeyboardEvent} */ e) => {
|
|
501
506
|
if (e.key === "Enter") confirm();
|
|
@@ -506,7 +511,7 @@ function showRenameDialog(currentName) {
|
|
|
506
511
|
|
|
507
512
|
requestAnimationFrame(() => {
|
|
508
513
|
const layer = document.getElementById("layer-dialog");
|
|
509
|
-
const tf = /** @type {
|
|
514
|
+
const tf = /** @type {HTMLElement | null} */ (layer?.querySelector("sp-textfield"));
|
|
510
515
|
if (tf) {
|
|
511
516
|
tf.focus();
|
|
512
517
|
const input = tf.shadowRoot?.querySelector("input");
|
|
@@ -587,7 +592,9 @@ async function loadPreview(el, file) {
|
|
|
587
592
|
let preview = _previewCache.get(file.path);
|
|
588
593
|
if (!preview) {
|
|
589
594
|
try {
|
|
590
|
-
const comp = componentRegistry.find(
|
|
595
|
+
const comp = componentRegistry.find(
|
|
596
|
+
(/** @type {ComponentEntry} */ c) => c.path === file.path,
|
|
597
|
+
);
|
|
591
598
|
if (comp) {
|
|
592
599
|
preview = /** @type {HTMLElement | undefined} */ (await renderComponentPreview(comp));
|
|
593
600
|
} else {
|
|
@@ -686,8 +693,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
686
693
|
size="s"
|
|
687
694
|
placeholder="Filter files..."
|
|
688
695
|
.value=${searchQuery}
|
|
689
|
-
@input=${(/** @type {
|
|
690
|
-
searchQuery = e.target.value;
|
|
696
|
+
@input=${(/** @type {Event} */ e) => {
|
|
697
|
+
searchQuery = /** @type {HTMLInputElement} */ (e.target).value;
|
|
691
698
|
renderBrowse(container, ctx);
|
|
692
699
|
}}
|
|
693
700
|
@submit=${(/** @type {Event} */ e) => e.preventDefault()}
|
|
@@ -698,7 +705,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
698
705
|
</sp-action-button>
|
|
699
706
|
<sp-popover slot="click-content" tip>
|
|
700
707
|
<sp-menu
|
|
701
|
-
@change=${(/** @type {
|
|
708
|
+
@change=${(/** @type {Event} */ e) =>
|
|
709
|
+
handleNewEntity(/** @type {HTMLSelectElement} */ (e.target).value, container, ctx)}
|
|
702
710
|
>
|
|
703
711
|
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
704
712
|
${contentTypeTypes.length
|
|
@@ -726,9 +734,10 @@ export async function renderBrowse(container, ctx) {
|
|
|
726
734
|
accept=${UPLOAD_ACCEPT}
|
|
727
735
|
class="browse-upload-input"
|
|
728
736
|
style="display:none"
|
|
729
|
-
@change=${(/** @type {
|
|
730
|
-
|
|
731
|
-
|
|
737
|
+
@change=${(/** @type {Event} */ e) => {
|
|
738
|
+
const input = /** @type {HTMLInputElement} */ (e.target);
|
|
739
|
+
if (input.files?.length) handleUpload(input.files, container, ctx);
|
|
740
|
+
input.value = "";
|
|
732
741
|
}}
|
|
733
742
|
/>
|
|
734
743
|
<div class="browse-view-switcher">
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {
|
|
9
|
+
* @typedef {JxMutableNode} JxNode
|
|
10
10
|
*
|
|
11
11
|
* @typedef {"added" | "removed" | "modified"} DiffStatus
|
|
12
12
|
*
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
/**
|
|
17
17
|
* Deep equality check for two values. Ignores function and ref properties.
|
|
18
18
|
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
19
|
+
* @param {unknown} a
|
|
20
|
+
* @param {unknown} b
|
|
21
21
|
* @returns {boolean}
|
|
22
22
|
*/
|
|
23
23
|
function valuesEqual(a, b) {
|
|
@@ -26,38 +26,46 @@ function valuesEqual(a, b) {
|
|
|
26
26
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
27
27
|
|
|
28
28
|
if (Array.isArray(a)) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const bArr = /** @type {unknown[]} */ (b);
|
|
30
|
+
if (a.length !== bArr.length) return false;
|
|
31
|
+
return a.every((/** @type {unknown} */ v, /** @type {number} */ i) => valuesEqual(v, bArr[i]));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const
|
|
34
|
+
const aObj = /** @type {Record<string, unknown>} */ (a);
|
|
35
|
+
const bObj = /** @type {Record<string, unknown>} */ (b);
|
|
36
|
+
|
|
37
|
+
const keysA = Object.keys(aObj).filter(
|
|
34
38
|
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
35
39
|
);
|
|
36
|
-
const keysB = Object.keys(
|
|
40
|
+
const keysB = Object.keys(bObj).filter(
|
|
37
41
|
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
38
42
|
);
|
|
39
43
|
|
|
40
44
|
if (keysA.length !== keysB.length) return false;
|
|
41
45
|
if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
|
|
42
46
|
|
|
43
|
-
return keysA.every((/** @type {string} */ k) => valuesEqual(
|
|
47
|
+
return keysA.every((/** @type {string} */ k) => valuesEqual(aObj[k], bObj[k]));
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Filter element children from a node's children array.
|
|
48
52
|
*
|
|
49
|
-
* @param {
|
|
50
|
-
* @returns {
|
|
53
|
+
* @param {JxMutableNode | undefined} node
|
|
54
|
+
* @returns {JxMutableNode[]}
|
|
51
55
|
*/
|
|
52
56
|
function elementChildren(node) {
|
|
53
57
|
if (!node?.children || !Array.isArray(node.children)) return [];
|
|
54
|
-
return
|
|
58
|
+
return /** @type {JxMutableNode[]} */ (
|
|
59
|
+
node.children.filter(
|
|
60
|
+
(/** @type {JxMutableNode | string} */ c) => c != null && typeof c === "object",
|
|
61
|
+
)
|
|
62
|
+
);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/**
|
|
58
66
|
* Mark an entire subtree with a diff status.
|
|
59
67
|
*
|
|
60
|
-
* @param {
|
|
68
|
+
* @param {JxMutableNode} node
|
|
61
69
|
* @param {DiffStatus} status
|
|
62
70
|
* @param {string} path
|
|
63
71
|
* @param {Map<string, DiffStatus>} diffMap
|
|
@@ -76,8 +84,8 @@ function markRecursive(node, status, path, diffMap, allPaths) {
|
|
|
76
84
|
/**
|
|
77
85
|
* Compute structural diff between original and current documents.
|
|
78
86
|
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {
|
|
87
|
+
* @param {JxMutableNode | undefined} originalDoc - Original document (from git)
|
|
88
|
+
* @param {JxMutableNode | undefined} currentDoc - Current document (in memory/on disk)
|
|
81
89
|
* @returns {DiffResult}
|
|
82
90
|
*/
|
|
83
91
|
export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
@@ -89,8 +97,8 @@ export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
|
89
97
|
/**
|
|
90
98
|
* Walk both trees in parallel and mark differences.
|
|
91
99
|
*
|
|
92
|
-
* @param {
|
|
93
|
-
* @param {
|
|
100
|
+
* @param {JxMutableNode | undefined} origNode
|
|
101
|
+
* @param {JxMutableNode | undefined} currNode
|
|
94
102
|
* @param {string} path
|
|
95
103
|
*/
|
|
96
104
|
const walk = (origNode, currNode, path = "") => {
|
|
@@ -7,7 +7,7 @@ import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath }
|
|
|
7
7
|
import { activeTab } from "../workspace/workspace.js";
|
|
8
8
|
import { isInlineInContext } from "../editor/inline-edit.js";
|
|
9
9
|
|
|
10
|
-
/** @type {
|
|
10
|
+
/** @type {{ getCanvasMode: () => string; getZoom: () => number } | null} */
|
|
11
11
|
let _ctx = null;
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -21,9 +21,10 @@ export function initCanvasHelpers(ctx) {
|
|
|
21
21
|
|
|
22
22
|
/** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
|
|
23
23
|
export function effectiveZoom() {
|
|
24
|
-
|
|
24
|
+
const ctx = /** @type {{ getCanvasMode: () => string; getZoom: () => number }} */ (_ctx);
|
|
25
|
+
return ctx.getCanvasMode() === "edit"
|
|
25
26
|
? 1
|
|
26
|
-
: (
|
|
27
|
+
: (ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Return the active canvas panel based on the current activeMedia setting. */
|
|
@@ -42,10 +43,11 @@ export function getActivePanel() {
|
|
|
42
43
|
* Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
|
|
43
44
|
* ancestor. Returns the original path if already non-inline.
|
|
44
45
|
*
|
|
45
|
-
* @param {
|
|
46
|
-
* @param {
|
|
46
|
+
* @param {JxMutableNode | undefined} doc
|
|
47
|
+
* @param {JxPath} path
|
|
47
48
|
*/
|
|
48
49
|
export function bubbleInlinePath(doc, path) {
|
|
50
|
+
if (!doc) return path;
|
|
49
51
|
let currentPath = path;
|
|
50
52
|
while (currentPath.length >= 2) {
|
|
51
53
|
const node = getNodeAtPath(doc, currentPath);
|
|
@@ -55,7 +57,7 @@ export function bubbleInlinePath(doc, path) {
|
|
|
55
57
|
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
56
58
|
const parentTag = (parentNode.tagName ?? "div").toLowerCase();
|
|
57
59
|
if (!isInlineInContext(childTag, parentTag)) break;
|
|
58
|
-
currentPath = pPath;
|
|
60
|
+
currentPath = /** @type {JxPath} */ (pPath);
|
|
59
61
|
}
|
|
60
62
|
return currentPath;
|
|
61
63
|
}
|
|
@@ -63,11 +65,13 @@ export function bubbleInlinePath(doc, path) {
|
|
|
63
65
|
/**
|
|
64
66
|
* Find a canvas DOM element by its document path.
|
|
65
67
|
*
|
|
66
|
-
* @param {
|
|
67
|
-
* @param {
|
|
68
|
+
* @param {JxPath} path
|
|
69
|
+
* @param {HTMLElement} canvasEl
|
|
70
|
+
* @returns {HTMLElement | null}
|
|
68
71
|
*/
|
|
69
72
|
export function findCanvasElement(path, canvasEl) {
|
|
70
|
-
|
|
73
|
+
/** @type {HTMLElement | null | undefined} */
|
|
74
|
+
let el = /** @type {HTMLElement | null} */ (canvasEl.firstElementChild);
|
|
71
75
|
if (!el) return null;
|
|
72
76
|
if (path.length === 0) return el;
|
|
73
77
|
|
|
@@ -75,11 +79,11 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
75
79
|
if (path[i] !== "children" && path[i] !== "cases") return null;
|
|
76
80
|
const idx = path[i + 1];
|
|
77
81
|
if (idx === undefined) {
|
|
78
|
-
el = el.children[0];
|
|
82
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[0]);
|
|
79
83
|
} else if (idx === "map") {
|
|
80
|
-
el = el.children[0]?.children[0];
|
|
84
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[0]?.children[0]);
|
|
81
85
|
} else {
|
|
82
|
-
el = el.children[idx];
|
|
86
|
+
el = /** @type {HTMLElement | undefined} */ (el.children[/** @type {number} */ (idx)]);
|
|
83
87
|
}
|
|
84
88
|
if (!el) break;
|
|
85
89
|
}
|
|
@@ -91,7 +95,7 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
91
95
|
|
|
92
96
|
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
93
97
|
const p = elToPath.get(candidate);
|
|
94
|
-
if (p && pathsEqual(p, path)) return candidate;
|
|
98
|
+
if (p && pathsEqual(p, path)) return /** @type {HTMLElement} */ (candidate);
|
|
95
99
|
}
|
|
96
100
|
return null;
|
|
97
101
|
}
|
|
@@ -99,18 +103,19 @@ export function findCanvasElement(path, canvasEl) {
|
|
|
99
103
|
/**
|
|
100
104
|
* Build an overlay box descriptor (no DOM creation).
|
|
101
105
|
*
|
|
102
|
-
* @param {
|
|
103
|
-
* @param {
|
|
104
|
-
* @param {
|
|
106
|
+
* @param {Element} el
|
|
107
|
+
* @param {string} type
|
|
108
|
+
* @param {import("./canvas-render.js").CanvasPanel} panel
|
|
105
109
|
*/
|
|
106
110
|
export function overlayBoxDescriptor(el, type, panel) {
|
|
107
|
-
const
|
|
111
|
+
const viewport = /** @type {HTMLElement} */ (panel.viewport);
|
|
112
|
+
const vpRect = viewport.getBoundingClientRect();
|
|
108
113
|
const elRect = el.getBoundingClientRect();
|
|
109
114
|
const scale = effectiveZoom();
|
|
110
115
|
return {
|
|
111
116
|
cls: `overlay-box overlay-${type}`,
|
|
112
|
-
top: `${(elRect.top - vpRect.top +
|
|
113
|
-
left: `${(elRect.left - vpRect.left +
|
|
117
|
+
top: `${(elRect.top - vpRect.top + viewport.scrollTop) / scale}px`,
|
|
118
|
+
left: `${(elRect.left - vpRect.left + viewport.scrollLeft) / scale}px`,
|
|
114
119
|
width: `${elRect.width / scale}px`,
|
|
115
120
|
height: `${elRect.height / scale}px`,
|
|
116
121
|
};
|