@jxsuite/studio 0.11.0 → 0.13.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 +4248 -3412
- package/dist/studio.js.map +49 -43
- 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 +3 -2
- package/src/canvas/canvas-render.js +152 -20
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +91 -41
- package/src/panels/panel-events.js +9 -12
- package/src/panels/properties-panel.js +179 -104
- 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 +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +11 -15
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- 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 +0 -456
- package/src/store.js +97 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/workspace/workspace.js +89 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
|
@@ -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,6 +30,7 @@ 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";
|
|
33
35
|
import { renderStylebookMode } from "../panels/stylebook-panel.js";
|
|
34
36
|
import { dismissLinkPopover, dismissBlockActionBar } from "../panels/block-action-bar.js";
|
|
@@ -49,10 +51,10 @@ let _ctx = null;
|
|
|
49
51
|
* @param {{
|
|
50
52
|
* getCanvasMode: () => string;
|
|
51
53
|
* setCanvasMode: (mode: string) => void;
|
|
52
|
-
* getState: () => any;
|
|
53
|
-
* update: (s: any) => void;
|
|
54
54
|
* openFileFromTree: (path: string) => void;
|
|
55
55
|
* exportFile: () => void;
|
|
56
|
+
* gitDiffState: any;
|
|
57
|
+
* setGitDiffState: (state: any) => void;
|
|
56
58
|
* }} ctx
|
|
57
59
|
*/
|
|
58
60
|
export function initCanvasRender(ctx) {
|
|
@@ -60,7 +62,9 @@ export function initCanvasRender(ctx) {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function renderCanvas() {
|
|
63
|
-
const
|
|
65
|
+
const tab = activeTab.value;
|
|
66
|
+
if (!tab) return;
|
|
67
|
+
const S = { document: tab.doc.document, ui: tab.session.ui, mode: tab.doc.mode };
|
|
64
68
|
const canvasMode = _ctx.getCanvasMode();
|
|
65
69
|
|
|
66
70
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
@@ -230,7 +234,9 @@ export function renderCanvas() {
|
|
|
230
234
|
debounce = setTimeout(() => {
|
|
231
235
|
try {
|
|
232
236
|
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
233
|
-
|
|
237
|
+
const tab = activeTab.value;
|
|
238
|
+
tab.doc.document = parsed;
|
|
239
|
+
tab.doc.dirty = true;
|
|
234
240
|
} catch {
|
|
235
241
|
// Invalid JSON — don't update state
|
|
236
242
|
}
|
|
@@ -239,6 +245,75 @@ export function renderCanvas() {
|
|
|
239
245
|
return;
|
|
240
246
|
}
|
|
241
247
|
|
|
248
|
+
// Git diff mode — render original (left) and current (right) side-by-side
|
|
249
|
+
if (canvasMode === "git-diff") {
|
|
250
|
+
if (!_ctx.gitDiffState) {
|
|
251
|
+
// Fallback to design mode if diff state is missing
|
|
252
|
+
_ctx.setCanvasMode("design");
|
|
253
|
+
renderCanvas();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
canvasWrap.style.padding = "0";
|
|
258
|
+
canvasWrap.style.overflow = "hidden";
|
|
259
|
+
|
|
260
|
+
const gitDiffState = _ctx.gitDiffState;
|
|
261
|
+
const { tpl: origPanelTpl, panel: origPanel } = canvasPanelTemplate(
|
|
262
|
+
"git-diff-original",
|
|
263
|
+
"Original",
|
|
264
|
+
false,
|
|
265
|
+
"50%",
|
|
266
|
+
);
|
|
267
|
+
const { tpl: currPanelTpl, panel: currPanel } = canvasPanelTemplate(
|
|
268
|
+
"git-diff-current",
|
|
269
|
+
"Current",
|
|
270
|
+
false,
|
|
271
|
+
"50%",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const diffTpl = html`
|
|
275
|
+
<div style="display: flex; height: 100%; gap: 0;">
|
|
276
|
+
<div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
277
|
+
${origPanelTpl}
|
|
278
|
+
</div>
|
|
279
|
+
<div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
|
|
280
|
+
${currPanelTpl}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
litRender(diffTpl, canvasWrap);
|
|
286
|
+
canvasPanels.push(/** @type {any} */ (origPanel));
|
|
287
|
+
canvasPanels.push(/** @type {any} */ (currPanel));
|
|
288
|
+
|
|
289
|
+
// Parse original document from git content
|
|
290
|
+
let originalDoc = null;
|
|
291
|
+
try {
|
|
292
|
+
if (gitDiffState.isMarkdown) {
|
|
293
|
+
// For markdown, we need to convert it to Jx format
|
|
294
|
+
// For now, use a simple placeholder until we have markdown parsing
|
|
295
|
+
originalDoc = {
|
|
296
|
+
tagName: "div",
|
|
297
|
+
children: [{ tagName: "p", textContent: "Original (markdown)" }],
|
|
298
|
+
};
|
|
299
|
+
} else {
|
|
300
|
+
// Parse as JSON
|
|
301
|
+
originalDoc = JSON.parse(gitDiffState.originalContent);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
originalDoc = {
|
|
305
|
+
tagName: "div",
|
|
306
|
+
children: [{ tagName: "p", textContent: "Failed to parse original" }],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const currDoc = S.document;
|
|
311
|
+
|
|
312
|
+
renderCanvasIntoPanel(origPanel, new Set(), S.ui.featureToggles, originalDoc, gitDiffState);
|
|
313
|
+
renderCanvasIntoPanel(currPanel, new Set(), S.ui.featureToggles, currDoc, gitDiffState);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
242
317
|
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
243
318
|
if (canvasMode === "edit") {
|
|
244
319
|
if (modeChanged) {
|
|
@@ -257,7 +332,7 @@ export function renderCanvas() {
|
|
|
257
332
|
</div>
|
|
258
333
|
`;
|
|
259
334
|
litRender(editTpl, canvasWrap);
|
|
260
|
-
canvasPanels.push(panel);
|
|
335
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
261
336
|
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
262
337
|
return;
|
|
263
338
|
}
|
|
@@ -302,7 +377,7 @@ export function renderCanvas() {
|
|
|
302
377
|
`,
|
|
303
378
|
canvasWrap,
|
|
304
379
|
);
|
|
305
|
-
canvasPanels.push(panel);
|
|
380
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
306
381
|
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
307
382
|
applyTransform();
|
|
308
383
|
if (modeChanged) {
|
|
@@ -354,7 +429,7 @@ export function renderCanvas() {
|
|
|
354
429
|
);
|
|
355
430
|
|
|
356
431
|
for (const { panel, activeSet } of panelEntries) {
|
|
357
|
-
canvasPanels.push(panel);
|
|
432
|
+
canvasPanels.push(/** @type {any} */ (panel));
|
|
358
433
|
renderCanvasIntoPanel(panel, activeSet, featureToggles);
|
|
359
434
|
}
|
|
360
435
|
|
|
@@ -376,29 +451,51 @@ export function renderCanvas() {
|
|
|
376
451
|
* structural preview.
|
|
377
452
|
*
|
|
378
453
|
* @param {any} panel
|
|
379
|
-
* @param {
|
|
454
|
+
* @param {Set<string>} activeBreakpoints
|
|
380
455
|
* @param {any} featureToggles
|
|
456
|
+
* @param {any} [docOverride] - Optional document to render (for diff mode). Uses active tab doc if
|
|
457
|
+
* not provided.
|
|
458
|
+
* @param {any} [gitDiffState] - Optional diff state. If provided, computes and applies diff
|
|
459
|
+
* highlighting.
|
|
381
460
|
*/
|
|
382
|
-
function renderCanvasIntoPanel(
|
|
461
|
+
function renderCanvasIntoPanel(
|
|
462
|
+
panel,
|
|
463
|
+
activeBreakpoints,
|
|
464
|
+
featureToggles,
|
|
465
|
+
docOverride = null,
|
|
466
|
+
gitDiffState = null,
|
|
467
|
+
) {
|
|
383
468
|
const gen = view.renderGeneration;
|
|
384
|
-
const
|
|
385
|
-
|
|
469
|
+
const tab = activeTab.value;
|
|
470
|
+
const docToRender = docOverride || tab?.doc.document;
|
|
471
|
+
|
|
472
|
+
renderCanvasLive(gen, docToRender, panel.canvas).then((/** @type {any} */ scope) => {
|
|
386
473
|
// Skip post-render setup if a newer render has started
|
|
387
474
|
if (gen !== view.renderGeneration) return;
|
|
388
475
|
if (scope) {
|
|
389
476
|
updateCanvas({ status: "ready", scope, error: null });
|
|
390
477
|
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
391
478
|
statusMessage("Runtime render OK", 1500);
|
|
479
|
+
|
|
480
|
+
// Apply diff highlighting if in git-diff mode
|
|
481
|
+
if (gitDiffState && docOverride) {
|
|
482
|
+
// Determine which document is original and which is current
|
|
483
|
+
const isOriginal = docOverride === (gitDiffState.originalDoc || gitDiffState.original);
|
|
484
|
+
const origDoc = isOriginal ? docOverride : gitDiffState.currentDoc || tab?.doc.document;
|
|
485
|
+
const currDoc = isOriginal ? gitDiffState.currentDoc || tab?.doc.document : docOverride;
|
|
486
|
+
|
|
487
|
+
const { byPath: diffMap } = computeDocumentDiff(origDoc, currDoc);
|
|
488
|
+
|
|
489
|
+
// Can't iterate WeakMap, so apply styling by walking the canvas
|
|
490
|
+
const { elToPath } = scope;
|
|
491
|
+
if (elToPath instanceof WeakMap) {
|
|
492
|
+
applyDiffHighlightToCanvas(panel.canvas, diffMap);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
392
495
|
} else {
|
|
393
496
|
// Fallback to structural preview
|
|
394
497
|
updateCanvas({ status: "ready", scope: null, error: null });
|
|
395
|
-
renderCanvasNode(
|
|
396
|
-
_ctx.getState().document,
|
|
397
|
-
[],
|
|
398
|
-
panel.canvas,
|
|
399
|
-
activeBreakpoints,
|
|
400
|
-
featureToggles,
|
|
401
|
-
);
|
|
498
|
+
renderCanvasNode(docToRender, [], panel.canvas, activeBreakpoints, featureToggles);
|
|
402
499
|
}
|
|
403
500
|
registerPanelDnD(panel);
|
|
404
501
|
registerPanelEvents(panel);
|
|
@@ -407,6 +504,40 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
|
407
504
|
});
|
|
408
505
|
}
|
|
409
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Apply diff highlighting to canvas elements based on elToPath mapping. Walks the canvas DOM and
|
|
509
|
+
* applies classes based on diff status.
|
|
510
|
+
*
|
|
511
|
+
* @param {HTMLElement} canvas
|
|
512
|
+
* @param {Map<string, "added" | "removed" | "modified">} diffMap
|
|
513
|
+
*/
|
|
514
|
+
function applyDiffHighlightToCanvas(canvas, diffMap) {
|
|
515
|
+
if (!diffMap || diffMap.size === 0) return;
|
|
516
|
+
|
|
517
|
+
// Walk all elements in canvas and check their data attributes or other markers
|
|
518
|
+
const walkCanvas = (/** @type {HTMLElement} */ el, /** @type {string} */ path = "") => {
|
|
519
|
+
const pathKey = path || "/";
|
|
520
|
+
|
|
521
|
+
if (diffMap.has(pathKey)) {
|
|
522
|
+
const status = diffMap.get(pathKey);
|
|
523
|
+
if (status === "added") el.classList.add("element-diff-added");
|
|
524
|
+
else if (status === "removed") el.classList.add("element-diff-removed");
|
|
525
|
+
else if (status === "modified") el.classList.add("element-diff-modified");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Check for child elements (heuristic: children array markers)
|
|
529
|
+
let childIdx = 0;
|
|
530
|
+
for (const child of el.children) {
|
|
531
|
+
const childPath =
|
|
532
|
+
pathKey === "/" ? `children/${childIdx}` : `${pathKey}/children/${childIdx}`;
|
|
533
|
+
walkCanvas(/** @type {HTMLElement} */ (child), childPath);
|
|
534
|
+
childIdx++;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
walkCanvas(canvas, "");
|
|
539
|
+
}
|
|
540
|
+
|
|
410
541
|
/**
|
|
411
542
|
* Apply media query overrides as inline styles on matching canvas elements. Needed because the
|
|
412
543
|
* runtime renders base styles as inline — @media CSS rules in the injected stylesheet can't win
|
|
@@ -417,8 +548,9 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
|
417
548
|
*/
|
|
418
549
|
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
419
550
|
if (!activeBreakpoints.size) return;
|
|
420
|
-
const
|
|
421
|
-
|
|
551
|
+
const tab = activeTab.value;
|
|
552
|
+
if (!tab) return;
|
|
553
|
+
const docMedia = getEffectiveMedia(tab.doc.document.$media || {});
|
|
422
554
|
const validBreakpoints = new Set();
|
|
423
555
|
for (const name of activeBreakpoints) {
|
|
424
556
|
if (docMedia[name]) validBreakpoints.add(name);
|
|
@@ -8,8 +8,8 @@ import { ref } from "lit-html/directives/ref.js";
|
|
|
8
8
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
9
9
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { renderOnly, updateUi, canvasWrap, canvasPanels } from "../store.js";
|
|
12
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
13
13
|
import { view } from "../view.js";
|
|
14
14
|
|
|
15
15
|
/** @type {any} */
|
|
@@ -44,14 +44,14 @@ export function initCanvasUtils(ctx) {
|
|
|
44
44
|
export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
|
|
45
45
|
/**
|
|
46
46
|
* @type {{
|
|
47
|
-
* mediaName:
|
|
48
|
-
* element:
|
|
49
|
-
* canvas:
|
|
50
|
-
* overlay:
|
|
51
|
-
* overlayClk:
|
|
52
|
-
* viewport:
|
|
53
|
-
* dropLine:
|
|
54
|
-
* _width:
|
|
47
|
+
* mediaName: string;
|
|
48
|
+
* element: HTMLElement | null;
|
|
49
|
+
* canvas: HTMLElement | null;
|
|
50
|
+
* overlay: HTMLElement | null;
|
|
51
|
+
* overlayClk: HTMLElement | null;
|
|
52
|
+
* viewport: HTMLElement | null;
|
|
53
|
+
* dropLine: HTMLElement | null;
|
|
54
|
+
* _width: number | null;
|
|
55
55
|
* }}
|
|
56
56
|
*/
|
|
57
57
|
const panel = {
|
|
@@ -69,7 +69,7 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
|
|
|
69
69
|
class=${`canvas-panel${fullWidth ? " full-width" : ""}`}
|
|
70
70
|
data-media=${ifDefined(mediaName !== null ? mediaName : undefined)}
|
|
71
71
|
${ref((el) => {
|
|
72
|
-
if (el) panel.element = el;
|
|
72
|
+
if (el) panel.element = /** @type {HTMLElement} */ (el);
|
|
73
73
|
})}
|
|
74
74
|
>
|
|
75
75
|
${label
|
|
@@ -88,34 +88,34 @@ export function canvasPanelTemplate(mediaName, label, fullWidth, width = null) {
|
|
|
88
88
|
class="canvas-panel-viewport"
|
|
89
89
|
style=${styleMap({ width: width && !fullWidth ? `${width}px` : "" })}
|
|
90
90
|
${ref((el) => {
|
|
91
|
-
if (el) panel.viewport = el;
|
|
91
|
+
if (el) panel.viewport = /** @type {HTMLElement} */ (el);
|
|
92
92
|
})}
|
|
93
93
|
>
|
|
94
94
|
<div
|
|
95
95
|
class="canvas-panel-canvas"
|
|
96
96
|
style=${styleMap({ width: width ? `${width}px` : "" })}
|
|
97
97
|
${ref((el) => {
|
|
98
|
-
if (el) panel.canvas = el;
|
|
98
|
+
if (el) panel.canvas = /** @type {HTMLElement} */ (el);
|
|
99
99
|
})}
|
|
100
100
|
></div>
|
|
101
101
|
<div
|
|
102
102
|
class="canvas-panel-overlay"
|
|
103
103
|
${ref((el) => {
|
|
104
|
-
if (el) panel.overlay = el;
|
|
104
|
+
if (el) panel.overlay = /** @type {HTMLElement} */ (el);
|
|
105
105
|
})}
|
|
106
106
|
>
|
|
107
107
|
<div
|
|
108
108
|
class="canvas-drop-indicator"
|
|
109
109
|
style="display:none"
|
|
110
110
|
${ref((el) => {
|
|
111
|
-
if (el) panel.dropLine = el;
|
|
111
|
+
if (el) panel.dropLine = /** @type {HTMLElement} */ (el);
|
|
112
112
|
})}
|
|
113
113
|
></div>
|
|
114
114
|
</div>
|
|
115
115
|
<div
|
|
116
116
|
class="canvas-panel-click"
|
|
117
117
|
${ref((el) => {
|
|
118
|
-
if (el) panel.overlayClk = el;
|
|
118
|
+
if (el) panel.overlayClk = /** @type {HTMLElement} */ (el);
|
|
119
119
|
})}
|
|
120
120
|
></div>
|
|
121
121
|
</div>
|
|
@@ -207,7 +207,6 @@ export function fitToScreen() {
|
|
|
207
207
|
|
|
208
208
|
/** Reset the zoom indicator (clear its content). Called when switching to non-panzoom modes. */
|
|
209
209
|
export function resetZoomIndicator() {
|
|
210
|
-
ensureLitState(zoomIndicatorHost);
|
|
211
210
|
litRender(nothing, zoomIndicatorHost);
|
|
212
211
|
}
|
|
213
212
|
|
|
@@ -220,7 +219,6 @@ export function renderZoomIndicator() {
|
|
|
220
219
|
if (!zoomIndicatorHost.isConnected) {
|
|
221
220
|
document.body.appendChild(zoomIndicatorHost);
|
|
222
221
|
}
|
|
223
|
-
ensureLitState(zoomIndicatorHost);
|
|
224
222
|
litRender(
|
|
225
223
|
html`
|
|
226
224
|
<div class="zoom-indicator">
|
|
@@ -263,14 +261,14 @@ export function positionZoomIndicator() {
|
|
|
263
261
|
|
|
264
262
|
/** Toggle "active" class on canvas panel headers based on activeMedia. */
|
|
265
263
|
export function updateActivePanelHeaders() {
|
|
266
|
-
const
|
|
264
|
+
const activeMedia = activeTab.value?.session.ui.activeMedia ?? null;
|
|
267
265
|
for (const p of canvasPanels) {
|
|
268
|
-
const header = p.element
|
|
266
|
+
const header = p.element?.querySelector(".canvas-panel-header");
|
|
269
267
|
if (header) {
|
|
270
268
|
const isActive =
|
|
271
|
-
(
|
|
272
|
-
(
|
|
273
|
-
|
|
269
|
+
(activeMedia === null && p.mediaName === "base") ||
|
|
270
|
+
(activeMedia === null && p.mediaName === null) ||
|
|
271
|
+
activeMedia === p.mediaName;
|
|
274
272
|
header.classList.toggle("active", isActive);
|
|
275
273
|
}
|
|
276
274
|
}
|