@jxsuite/studio 0.11.0 → 0.14.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 +6517 -5870
- package/dist/studio.js.map +57 -51
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +28 -2
- package/src/canvas/canvas-render.js +170 -20
- package/src/canvas/canvas-utils.js +22 -25
- package/src/editor/component-inline-edit.js +55 -44
- package/src/editor/content-inline-edit.js +47 -50
- package/src/editor/context-menu.js +78 -53
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -11
- package/src/editor/shortcuts.js +88 -64
- package/src/files/components.js +15 -4
- package/src/files/file-ops.js +57 -108
- package/src/files/files.js +81 -28
- package/src/panels/activity-bar.js +31 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +4 -10
- package/src/panels/dnd.js +77 -35
- package/src/panels/editors.js +17 -26
- package/src/panels/elements-panel.js +17 -11
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +47 -4
- package/src/panels/layers-panel.js +25 -21
- package/src/panels/left-panel.js +109 -44
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +28 -37
- package/src/panels/preview-render.js +7 -3
- package/src/panels/properties-panel.js +179 -104
- package/src/panels/pseudo-preview.js +9 -8
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +26 -19
- package/src/panels/style-inputs.js +5 -4
- package/src/panels/style-panel.js +128 -105
- package/src/panels/style-utils.js +8 -6
- package/src/panels/stylebook-layers-panel.js +5 -5
- package/src/panels/stylebook-panel.js +27 -31
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +40 -10
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +2 -459
- package/src/store.js +61 -219
- package/src/studio.js +163 -300
- package/src/tabs/tab.js +168 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/color-selector.js +4 -5
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +90 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
33
|
-
"@jxsuite/runtime": "^0.
|
|
33
|
+
"@jxsuite/runtime": "^0.12.0",
|
|
34
34
|
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
35
|
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
36
|
"@spectrum-web-components/action-button": "^1.12.1",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
61
|
"@spectrum-web-components/toast": "^1.12.1",
|
|
62
62
|
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
63
|
+
"@vue/reactivity": "3.5.34",
|
|
63
64
|
"lit-html": "^3.3.3",
|
|
64
65
|
"monaco-editor": "^0.55.1",
|
|
65
66
|
"remark-directive": "^4.0.0",
|
|
@@ -75,5 +76,6 @@
|
|
|
75
76
|
"@webref/css": "^8.5.6",
|
|
76
77
|
"@webref/elements": "^2.7.1",
|
|
77
78
|
"@webref/idl": "^3.78.0"
|
|
78
|
-
}
|
|
79
|
+
},
|
|
80
|
+
"//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
|
|
79
81
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Diff — computes visual diff between original and current Jx documents.
|
|
3
|
+
*
|
|
4
|
+
* Compares two document trees recursively, marking nodes as added/removed/modified, and propagates
|
|
5
|
+
* "modified" status up the tree for structural changes (children added/removed/reordered).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Record<string, any>} JxNode
|
|
10
|
+
*
|
|
11
|
+
* @typedef {"added" | "removed" | "modified"} DiffStatus
|
|
12
|
+
*
|
|
13
|
+
* @typedef {{ byPath: Map<string, DiffStatus>; allPaths: Set<string> }} DiffResult
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deep equality check for two values. Ignores function and ref properties.
|
|
18
|
+
*
|
|
19
|
+
* @param {any} a
|
|
20
|
+
* @param {any} b
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function valuesEqual(a, b) {
|
|
24
|
+
if (a === b) return true;
|
|
25
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
26
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(a)) {
|
|
29
|
+
if (a.length !== b.length) return false;
|
|
30
|
+
return a.every((/** @type {any} */ v, /** @type {number} */ i) => valuesEqual(v, b[i]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const keysA = Object.keys(a).filter(
|
|
34
|
+
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
35
|
+
);
|
|
36
|
+
const keysB = Object.keys(b).filter(
|
|
37
|
+
(/** @type {string} */ k) => !k.startsWith("on") && k !== "$ref",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (keysA.length !== keysB.length) return false;
|
|
41
|
+
if (!keysA.every((/** @type {string} */ k) => keysB.includes(k))) return false;
|
|
42
|
+
|
|
43
|
+
return keysA.every((/** @type {string} */ k) => valuesEqual(a[k], b[k]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Filter element children from a node's children array.
|
|
48
|
+
*
|
|
49
|
+
* @param {any} node
|
|
50
|
+
* @returns {any[]}
|
|
51
|
+
*/
|
|
52
|
+
function elementChildren(node) {
|
|
53
|
+
if (!node?.children || !Array.isArray(node.children)) return [];
|
|
54
|
+
return node.children.filter((/** @type {any} */ c) => c != null && typeof c === "object");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Mark an entire subtree with a diff status.
|
|
59
|
+
*
|
|
60
|
+
* @param {any} node
|
|
61
|
+
* @param {DiffStatus} status
|
|
62
|
+
* @param {string} path
|
|
63
|
+
* @param {Map<string, DiffStatus>} diffMap
|
|
64
|
+
* @param {Set<string>} allPaths
|
|
65
|
+
*/
|
|
66
|
+
function markRecursive(node, status, path, diffMap, allPaths) {
|
|
67
|
+
allPaths.add(path);
|
|
68
|
+
diffMap.set(path, status);
|
|
69
|
+
const children = elementChildren(node);
|
|
70
|
+
for (let i = 0; i < children.length; i++) {
|
|
71
|
+
const childPath = `${path}/children/${i}`;
|
|
72
|
+
markRecursive(children[i], status, childPath, diffMap, allPaths);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compute structural diff between original and current documents.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} originalDoc - Original document (from git)
|
|
80
|
+
* @param {any} currentDoc - Current document (in memory/on disk)
|
|
81
|
+
* @returns {DiffResult}
|
|
82
|
+
*/
|
|
83
|
+
export function computeDocumentDiff(originalDoc, currentDoc) {
|
|
84
|
+
/** @type {Map<string, DiffStatus>} */
|
|
85
|
+
const diffMap = new Map();
|
|
86
|
+
/** @type {Set<string>} */
|
|
87
|
+
const allPaths = new Set();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk both trees in parallel and mark differences.
|
|
91
|
+
*
|
|
92
|
+
* @param {any} origNode
|
|
93
|
+
* @param {any} currNode
|
|
94
|
+
* @param {string} path
|
|
95
|
+
*/
|
|
96
|
+
const walk = (origNode, currNode, path = "") => {
|
|
97
|
+
const pathKey = path || "/";
|
|
98
|
+
allPaths.add(pathKey);
|
|
99
|
+
|
|
100
|
+
// Both exist but differ
|
|
101
|
+
if (origNode != null && currNode != null) {
|
|
102
|
+
if (!valuesEqual(origNode, currNode)) {
|
|
103
|
+
diffMap.set(pathKey, "modified");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Walk children
|
|
107
|
+
if (origNode.children && currNode.children) {
|
|
108
|
+
const origChildren = elementChildren(origNode);
|
|
109
|
+
const currChildren = elementChildren(currNode);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < Math.max(origChildren.length, currChildren.length); i++) {
|
|
112
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
113
|
+
walk(origChildren[i], currChildren[i], childPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// In current only (added)
|
|
120
|
+
if (currNode != null && origNode == null) {
|
|
121
|
+
diffMap.set(pathKey, "added");
|
|
122
|
+
const children = elementChildren(currNode);
|
|
123
|
+
for (let i = 0; i < children.length; i++) {
|
|
124
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
125
|
+
markRecursive(children[i], "added", childPath, diffMap, allPaths);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// In original only (removed)
|
|
131
|
+
if (origNode != null && currNode == null) {
|
|
132
|
+
diffMap.set(pathKey, "removed");
|
|
133
|
+
const children = elementChildren(origNode);
|
|
134
|
+
for (let i = 0; i < children.length; i++) {
|
|
135
|
+
const childPath = pathKey === "/" ? `children/${i}` : `${pathKey}/children/${i}`;
|
|
136
|
+
markRecursive(children[i], "removed", childPath, diffMap, allPaths);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Start comparison
|
|
143
|
+
walk(originalDoc, currentDoc, "");
|
|
144
|
+
|
|
145
|
+
// Propagate "modified" status to parents of added/removed children
|
|
146
|
+
const propagateModified = () => {
|
|
147
|
+
let changed = false;
|
|
148
|
+
for (const [path, status] of diffMap.entries()) {
|
|
149
|
+
if ((status === "added" || status === "removed") && path !== "/") {
|
|
150
|
+
const parentPath = path.split("/").slice(0, -2).join("/") || "/";
|
|
151
|
+
if (parentPath !== "/") {
|
|
152
|
+
const parentStatus = diffMap.get(parentPath);
|
|
153
|
+
if (
|
|
154
|
+
parentStatus !== "modified" &&
|
|
155
|
+
parentStatus !== "added" &&
|
|
156
|
+
parentStatus !== "removed"
|
|
157
|
+
) {
|
|
158
|
+
diffMap.set(parentPath, "modified");
|
|
159
|
+
changed = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return changed;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Propagate upwards until no more changes
|
|
168
|
+
while (propagateModified()) {}
|
|
169
|
+
|
|
170
|
+
return { byPath: diffMap, allPaths };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear diff highlighting from all elements in a canvas.
|
|
175
|
+
*
|
|
176
|
+
* @param {HTMLElement} canvas
|
|
177
|
+
*/
|
|
178
|
+
export function clearDiffHighlight(canvas) {
|
|
179
|
+
canvas
|
|
180
|
+
.querySelectorAll(".element-diff-added, .element-diff-removed, .element-diff-modified")
|
|
181
|
+
.forEach((/** @type {Element} */ el) => {
|
|
182
|
+
el.classList.remove("element-diff-added", "element-diff-removed", "element-diff-modified");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -3,14 +3,8 @@
|
|
|
3
3
|
* multiple canvas-related modules: element lookup, zoom, panel resolution, inline bubbling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
canvasPanels,
|
|
9
|
-
elToPath,
|
|
10
|
-
pathsEqual,
|
|
11
|
-
getNodeAtPath,
|
|
12
|
-
parentElementPath,
|
|
13
|
-
} from "../store.js";
|
|
6
|
+
import { canvasPanels, elToPath, pathsEqual, getNodeAtPath, parentElementPath } from "../store.js";
|
|
7
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
14
8
|
import { isInlineInContext } from "../editor/inline-edit.js";
|
|
15
9
|
|
|
16
10
|
/** @type {any} */
|
|
@@ -19,25 +13,27 @@ let _ctx = null;
|
|
|
19
13
|
/**
|
|
20
14
|
* Initialize the canvas helpers module.
|
|
21
15
|
*
|
|
22
|
-
* @param {{ getCanvasMode: () => string }} ctx
|
|
16
|
+
* @param {{ getCanvasMode: () => string; getZoom: () => number }} ctx
|
|
23
17
|
*/
|
|
24
18
|
export function initCanvasHelpers(ctx) {
|
|
25
19
|
_ctx = ctx;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
/** Effective zoom scale — always 1 in edit (content) mode,
|
|
22
|
+
/** Effective zoom scale — always 1 in edit (content) mode, actual zoom otherwise. */
|
|
29
23
|
export function effectiveZoom() {
|
|
30
|
-
return _ctx.getCanvasMode() === "edit"
|
|
24
|
+
return _ctx.getCanvasMode() === "edit"
|
|
25
|
+
? 1
|
|
26
|
+
: (_ctx.getZoom?.() ?? activeTab.value?.session.ui.zoom ?? 1);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/** Return the active canvas panel based on the current activeMedia setting. */
|
|
34
30
|
export function getActivePanel() {
|
|
35
31
|
if (canvasPanels.length === 0) return null;
|
|
36
32
|
if (canvasPanels.length === 1) return canvasPanels[0];
|
|
37
|
-
const
|
|
33
|
+
const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
|
|
38
34
|
for (const p of canvasPanels) {
|
|
39
|
-
if (
|
|
40
|
-
if (p.mediaName ===
|
|
35
|
+
if (activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
|
|
36
|
+
if (p.mediaName === activeMedia) return p;
|
|
41
37
|
}
|
|
42
38
|
return canvasPanels[0];
|
|
43
39
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { elToPath, stripEventHandlers, projectState } from "../store.js";
|
|
8
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
8
9
|
import { view } from "../view.js";
|
|
9
10
|
import {
|
|
10
11
|
renderNode as runtimeRenderNode,
|
|
@@ -82,7 +83,6 @@ function markLayoutNodes(/** @type {any} */ node) {
|
|
|
82
83
|
* Initialize the canvas live render module.
|
|
83
84
|
*
|
|
84
85
|
* @param {{
|
|
85
|
-
* getState: () => any;
|
|
86
86
|
* getCanvasMode: () => string;
|
|
87
87
|
* }} ctx
|
|
88
88
|
*/
|
|
@@ -100,7 +100,8 @@ export function initCanvasLiveRender(ctx) {
|
|
|
100
100
|
* @param {any} canvasEl
|
|
101
101
|
*/
|
|
102
102
|
export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
103
|
-
const
|
|
103
|
+
const tab = activeTab.value;
|
|
104
|
+
const S = { documentPath: tab?.documentPath, mode: tab?.doc.mode, document: tab?.doc.document };
|
|
104
105
|
const canvasMode = _ctx.getCanvasMode();
|
|
105
106
|
|
|
106
107
|
// Suppress server function resolution in non-preview modes to avoid
|
|
@@ -388,6 +389,31 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
388
389
|
} else {
|
|
389
390
|
canvasEl.removeAttribute("data-content-mode");
|
|
390
391
|
}
|
|
392
|
+
|
|
393
|
+
// Resolve relative asset URLs through the PAL for environments that can't
|
|
394
|
+
// serve project files natively (e.g. ElectroBun's views:// scheme).
|
|
395
|
+
const platform = /** @type {any} */ (globalThis).__jxPlatform;
|
|
396
|
+
if (platform?.resolveAssetUrl && docBase) {
|
|
397
|
+
const mediaEls = el.querySelectorAll("img[src], video[src], source[src], video[poster]");
|
|
398
|
+
for (const node of mediaEls) {
|
|
399
|
+
for (const attr of ["src", "poster"]) {
|
|
400
|
+
const val = node.getAttribute(attr);
|
|
401
|
+
if (
|
|
402
|
+
val &&
|
|
403
|
+
!val.startsWith("data:") &&
|
|
404
|
+
!val.startsWith("blob:") &&
|
|
405
|
+
!val.startsWith("http")
|
|
406
|
+
) {
|
|
407
|
+
try {
|
|
408
|
+
const resolved = new URL(val, docBase).pathname.replace(/^\//, "");
|
|
409
|
+
const dataUrl = await platform.resolveAssetUrl(resolved);
|
|
410
|
+
if (dataUrl) node.setAttribute(attr, dataUrl);
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
391
417
|
canvasEl.appendChild(el);
|
|
392
418
|
if (canvasMode === "design" || canvasMode === "edit") {
|
|
393
419
|
// Custom element connectedCallbacks render children asynchronously —
|
|
@@ -8,6 +8,7 @@ import { ref } from "lit-html/directives/ref.js";
|
|
|
8
8
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
9
9
|
|
|
10
10
|
import { canvasWrap, canvasPanels, updateCanvas } from "../store.js";
|
|
11
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
11
12
|
import { view } from "../view.js";
|
|
12
13
|
import {
|
|
13
14
|
canvasPanelTemplate,
|
|
@@ -29,7 +30,10 @@ import { renderCanvasLive } from "./canvas-live-render.js";
|
|
|
29
30
|
import { renderCanvasNode } from "../panels/preview-render.js";
|
|
30
31
|
import { registerPanelDnD } from "../panels/canvas-dnd.js";
|
|
31
32
|
import { registerPanelEvents } from "../panels/panel-events.js";
|
|
33
|
+
import { computeDocumentDiff } from "./canvas-diff.js";
|
|
32
34
|
import { updateForcedPseudoPreview } from "../panels/pseudo-preview.js";
|
|
35
|
+
import { findCanvasElement } from "./canvas-helpers.js";
|
|
36
|
+
import { enterComponentInlineEdit } from "../editor/component-inline-edit.js";
|
|
33
37
|
import { renderStylebookMode } from "../panels/stylebook-panel.js";
|
|
34
38
|
import { dismissLinkPopover, dismissBlockActionBar } from "../panels/block-action-bar.js";
|
|
35
39
|
import { dismissContextMenu } from "../editor/context-menu.js";
|
|
@@ -49,10 +53,10 @@ let _ctx = null;
|
|
|
49
53
|
* @param {{
|
|
50
54
|
* getCanvasMode: () => string;
|
|
51
55
|
* setCanvasMode: (mode: string) => void;
|
|
52
|
-
* getState: () => any;
|
|
53
|
-
* update: (s: any) => void;
|
|
54
56
|
* openFileFromTree: (path: string) => void;
|
|
55
57
|
* exportFile: () => void;
|
|
58
|
+
* gitDiffState: any;
|
|
59
|
+
* setGitDiffState: (state: any) => void;
|
|
56
60
|
* }} ctx
|
|
57
61
|
*/
|
|
58
62
|
export function initCanvasRender(ctx) {
|
|
@@ -60,7 +64,9 @@ export function initCanvasRender(ctx) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export function renderCanvas() {
|
|
63
|
-
const
|
|
67
|
+
const tab = activeTab.value;
|
|
68
|
+
if (!tab) return;
|
|
69
|
+
const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
|
|
64
70
|
const canvasMode = _ctx.getCanvasMode();
|
|
65
71
|
|
|
66
72
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
@@ -230,7 +236,9 @@ export function renderCanvas() {
|
|
|
230
236
|
debounce = setTimeout(() => {
|
|
231
237
|
try {
|
|
232
238
|
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
233
|
-
|
|
239
|
+
const tab = activeTab.value;
|
|
240
|
+
tab.doc.document = parsed;
|
|
241
|
+
tab.doc.dirty = true;
|
|
234
242
|
} catch {
|
|
235
243
|
// Invalid JSON — don't update state
|
|
236
244
|
}
|
|
@@ -239,6 +247,75 @@ export function renderCanvas() {
|
|
|
239
247
|
return;
|
|
240
248
|
}
|
|
241
249
|
|
|
250
|
+
// Git diff mode — render original (left) and current (right) side-by-side
|
|
251
|
+
if (canvasMode === "git-diff") {
|
|
252
|
+
if (!_ctx.gitDiffState) {
|
|
253
|
+
// Fallback to design mode if diff state is missing
|
|
254
|
+
_ctx.setCanvasMode("design");
|
|
255
|
+
renderCanvas();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
canvasWrap.style.padding = "0";
|
|
260
|
+
canvasWrap.style.overflow = "hidden";
|
|
261
|
+
|
|
262
|
+
const gitDiffState = _ctx.gitDiffState;
|
|
263
|
+
const { tpl: origPanelTpl, panel: origPanel } = canvasPanelTemplate(
|
|
264
|
+
"git-diff-original",
|
|
265
|
+
"Original",
|
|
266
|
+
false,
|
|
267
|
+
"50%",
|
|
268
|
+
);
|
|
269
|
+
const { tpl: currPanelTpl, panel: currPanel } = canvasPanelTemplate(
|
|
270
|
+
"git-diff-current",
|
|
271
|
+
"Current",
|
|
272
|
+
false,
|
|
273
|
+
"50%",
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const diffTpl = html`
|
|
277
|
+
<div style="display: flex; height: 100%; gap: 0;">
|
|
278
|
+
<div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
279
|
+
${origPanelTpl}
|
|
280
|
+
</div>
|
|
281
|
+
<div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
282
|
+
${currPanelTpl}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
|
|
287
|
+
litRender(diffTpl, canvasWrap);
|
|
288
|
+
canvasPanels.push(/** @type {any} */ (origPanel));
|
|
289
|
+
canvasPanels.push(/** @type {any} */ (currPanel));
|
|
290
|
+
|
|
291
|
+
// Parse original document from git content
|
|
292
|
+
let originalDoc = null;
|
|
293
|
+
try {
|
|
294
|
+
if (gitDiffState.isMarkdown) {
|
|
295
|
+
// For markdown, we need to convert it to Jx format
|
|
296
|
+
// For now, use a simple placeholder until we have markdown parsing
|
|
297
|
+
originalDoc = {
|
|
298
|
+
tagName: "div",
|
|
299
|
+
children: [{ tagName: "p", textContent: "Original (markdown)" }],
|
|
300
|
+
};
|
|
301
|
+
} else {
|
|
302
|
+
// Parse as JSON
|
|
303
|
+
originalDoc = JSON.parse(gitDiffState.originalContent);
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
originalDoc = {
|
|
307
|
+
tagName: "div",
|
|
308
|
+
children: [{ tagName: "p", textContent: "Failed to parse original" }],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const currDoc = S.document;
|
|
313
|
+
|
|
314
|
+
renderCanvasIntoPanel(origPanel, new Set(), S.ui.featureToggles, originalDoc, gitDiffState);
|
|
315
|
+
renderCanvasIntoPanel(currPanel, new Set(), S.ui.featureToggles, currDoc, gitDiffState);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
242
319
|
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
243
320
|
if (canvasMode === "edit") {
|
|
244
321
|
if (modeChanged) {
|
|
@@ -257,7 +334,7 @@ export function renderCanvas() {
|
|
|
257
334
|
</div>
|
|
258
335
|
`;
|
|
259
336
|
litRender(editTpl, canvasWrap);
|
|
260
|
-
canvasPanels.push(panel);
|
|
337
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
261
338
|
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
262
339
|
return;
|
|
263
340
|
}
|
|
@@ -302,7 +379,7 @@ export function renderCanvas() {
|
|
|
302
379
|
`,
|
|
303
380
|
canvasWrap,
|
|
304
381
|
);
|
|
305
|
-
canvasPanels.push(panel);
|
|
382
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
306
383
|
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
307
384
|
applyTransform();
|
|
308
385
|
if (modeChanged) {
|
|
@@ -354,7 +431,7 @@ export function renderCanvas() {
|
|
|
354
431
|
);
|
|
355
432
|
|
|
356
433
|
for (const { panel, activeSet } of panelEntries) {
|
|
357
|
-
canvasPanels.push(panel);
|
|
434
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
358
435
|
renderCanvasIntoPanel(panel, activeSet, featureToggles);
|
|
359
436
|
}
|
|
360
437
|
|
|
@@ -376,37 +453,109 @@ export function renderCanvas() {
|
|
|
376
453
|
* structural preview.
|
|
377
454
|
*
|
|
378
455
|
* @param {any} panel
|
|
379
|
-
* @param {
|
|
456
|
+
* @param {Set<string>} activeBreakpoints
|
|
380
457
|
* @param {any} featureToggles
|
|
458
|
+
* @param {any} [docOverride] - Optional document to render (for diff mode). Uses active tab doc if
|
|
459
|
+
* not provided.
|
|
460
|
+
* @param {any} [gitDiffState] - Optional diff state. If provided, computes and applies diff
|
|
461
|
+
* highlighting.
|
|
381
462
|
*/
|
|
382
|
-
function renderCanvasIntoPanel(
|
|
463
|
+
function renderCanvasIntoPanel(
|
|
464
|
+
panel,
|
|
465
|
+
activeBreakpoints,
|
|
466
|
+
featureToggles,
|
|
467
|
+
docOverride = null,
|
|
468
|
+
gitDiffState = null,
|
|
469
|
+
) {
|
|
383
470
|
const gen = view.renderGeneration;
|
|
384
|
-
const
|
|
385
|
-
|
|
471
|
+
const tab = activeTab.value;
|
|
472
|
+
const docToRender = docOverride || tab?.doc.document;
|
|
473
|
+
|
|
474
|
+
renderCanvasLive(gen, docToRender, panel.canvas).then((/** @type {any} */ scope) => {
|
|
386
475
|
// Skip post-render setup if a newer render has started
|
|
387
476
|
if (gen !== view.renderGeneration) return;
|
|
388
477
|
if (scope) {
|
|
389
478
|
updateCanvas({ status: "ready", scope, error: null });
|
|
390
479
|
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
391
480
|
statusMessage("Runtime render OK", 1500);
|
|
481
|
+
|
|
482
|
+
// Apply diff highlighting if in git-diff mode
|
|
483
|
+
if (gitDiffState && docOverride) {
|
|
484
|
+
// Determine which document is original and which is current
|
|
485
|
+
const isOriginal = docOverride === (gitDiffState.originalDoc || gitDiffState.original);
|
|
486
|
+
const _tab = activeTab.value;
|
|
487
|
+
const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || _tab?.doc.document;
|
|
488
|
+
const currDoc = isOriginal ? gitDiffState.currentDoc || _tab?.doc.document : docOverride;
|
|
489
|
+
|
|
490
|
+
const { byPath: diffMap } = computeDocumentDiff(origDoc, currDoc);
|
|
491
|
+
|
|
492
|
+
// Can't iterate WeakMap, so apply styling by walking the canvas
|
|
493
|
+
const { elToPath } = scope;
|
|
494
|
+
if (elToPath instanceof WeakMap) {
|
|
495
|
+
applyDiffHighlightToCanvas(panel.canvas, diffMap);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
392
498
|
} else {
|
|
393
499
|
// Fallback to structural preview
|
|
394
500
|
updateCanvas({ status: "ready", scope: null, error: null });
|
|
395
|
-
renderCanvasNode(
|
|
396
|
-
_ctx.getState().document,
|
|
397
|
-
[],
|
|
398
|
-
panel.canvas,
|
|
399
|
-
activeBreakpoints,
|
|
400
|
-
featureToggles,
|
|
401
|
-
);
|
|
501
|
+
renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
|
|
402
502
|
}
|
|
403
503
|
registerPanelDnD(panel);
|
|
404
504
|
registerPanelEvents(panel);
|
|
405
505
|
renderOverlays();
|
|
406
506
|
updateForcedPseudoPreview();
|
|
507
|
+
|
|
508
|
+
// Process pending inline edit when canvas becomes ready
|
|
509
|
+
const currentTab = activeTab.value;
|
|
510
|
+
if (currentTab?.session.ui?.pendingInlineEdit) {
|
|
511
|
+
const { path, mediaName: mn } = /** @type {{ path: any; mediaName: string }} */ (
|
|
512
|
+
currentTab.session.ui.pendingInlineEdit
|
|
513
|
+
);
|
|
514
|
+
currentTab.session.ui.pendingInlineEdit = null;
|
|
515
|
+
const targetPanel =
|
|
516
|
+
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
517
|
+
if (targetPanel) {
|
|
518
|
+
const el = findCanvasElement(path, targetPanel.canvas);
|
|
519
|
+
if (el) enterComponentInlineEdit(el, path);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
407
522
|
});
|
|
408
523
|
}
|
|
409
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Apply diff highlighting to canvas elements based on elToPath mapping. Walks the canvas DOM and
|
|
527
|
+
* applies classes based on diff status.
|
|
528
|
+
*
|
|
529
|
+
* @param {HTMLElement} canvas
|
|
530
|
+
* @param {Map<string, "added" | "removed" | "modified">} diffMap
|
|
531
|
+
*/
|
|
532
|
+
function applyDiffHighlightToCanvas(canvas, diffMap) {
|
|
533
|
+
if (!diffMap || diffMap.size === 0) return;
|
|
534
|
+
|
|
535
|
+
// Walk all elements in canvas and check their data attributes or other markers
|
|
536
|
+
const walkCanvas = (/** @type {HTMLElement} */ el, /** @type {string} */ path = "") => {
|
|
537
|
+
const pathKey = path || "/";
|
|
538
|
+
|
|
539
|
+
if (diffMap.has(pathKey)) {
|
|
540
|
+
const status = diffMap.get(pathKey);
|
|
541
|
+
if (status === "added") el.classList.add("element-diff-added");
|
|
542
|
+
else if (status === "removed") el.classList.add("element-diff-removed");
|
|
543
|
+
else if (status === "modified") el.classList.add("element-diff-modified");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check for child elements (heuristic: children array markers)
|
|
547
|
+
let childIdx = 0;
|
|
548
|
+
for (const child of el.children) {
|
|
549
|
+
const childPath =
|
|
550
|
+
pathKey === "/" ? `children/${childIdx}` : `${pathKey}/children/${childIdx}`;
|
|
551
|
+
walkCanvas(/** @type {HTMLElement} */ (child), childPath);
|
|
552
|
+
childIdx++;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
walkCanvas(canvas, "");
|
|
557
|
+
}
|
|
558
|
+
|
|
410
559
|
/**
|
|
411
560
|
* Apply media query overrides as inline styles on matching canvas elements. Needed because the
|
|
412
561
|
* runtime renders base styles as inline — @media CSS rules in the injected stylesheet can't win
|
|
@@ -417,8 +566,9 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
|
417
566
|
*/
|
|
418
567
|
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
419
568
|
if (!activeBreakpoints.size) return;
|
|
420
|
-
const
|
|
421
|
-
|
|
569
|
+
const tab = activeTab.value;
|
|
570
|
+
if (!tab) return;
|
|
571
|
+
const docMedia = getEffectiveMedia(tab.doc.document.$media || {});
|
|
422
572
|
const validBreakpoints = new Set();
|
|
423
573
|
for (const name of activeBreakpoints) {
|
|
424
574
|
if (docMedia[name]) validBreakpoints.add(name);
|