@jxsuite/studio 0.10.1 → 0.11.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/README.md +82 -0
- package/dist/studio.js +1105 -235
- package/dist/studio.js.map +35 -25
- package/package.json +30 -29
- package/src/browse/browse.js +48 -22
- package/src/canvas/canvas-live-render.js +140 -16
- package/src/canvas/canvas-render.js +10 -7
- package/src/files/files.js +2 -0
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/imports-panel.js +2 -2
- package/src/panels/overlays.js +15 -3
- package/src/panels/panel-events.js +10 -0
- package/src/panels/properties-panel.js +302 -8
- package/src/panels/stylebook-panel.js +58 -22
- package/src/settings/{collections-editor.js → content-types-editor.js} +70 -49
- package/src/settings/schema-field-ui.js +58 -5
- package/src/site-context.js +105 -0
- package/src/state.js +1 -1
- package/src/store.js +9 -1
- package/src/studio.js +10 -4
- package/src/ui/spectrum.js +2 -0
- package/src/utils/studio-utils.js +5 -5
- package/src/view.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Jx Studio — visual builder for Jx documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,35 +30,36 @@
|
|
|
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.10.
|
|
34
|
-
"@spectrum-web-components/accordion": "^1.12.
|
|
35
|
-
"@spectrum-web-components/action-bar": "1.12.
|
|
36
|
-
"@spectrum-web-components/action-button": "^1.12.
|
|
37
|
-
"@spectrum-web-components/action-group": "^1.12.
|
|
38
|
-
"@spectrum-web-components/checkbox": "^1.12.
|
|
39
|
-
"@spectrum-web-components/color-area": "^1.12.
|
|
40
|
-
"@spectrum-web-components/color-handle": "^1.12.
|
|
41
|
-
"@spectrum-web-components/color-slider": "^1.12.
|
|
42
|
-
"@spectrum-web-components/combobox": "^1.12.
|
|
43
|
-
"@spectrum-web-components/dialog": "^1.12.
|
|
44
|
-
"@spectrum-web-components/divider": "^1.12.
|
|
45
|
-
"@spectrum-web-components/icon": "^1.12.
|
|
46
|
-
"@spectrum-web-components/icons-workflow": "^1.12.
|
|
47
|
-
"@spectrum-web-components/menu": "^1.12.
|
|
48
|
-
"@spectrum-web-components/number-field": "^1.12.
|
|
49
|
-
"@spectrum-web-components/overlay": "^1.12.
|
|
33
|
+
"@jxsuite/runtime": "^0.10.2",
|
|
34
|
+
"@spectrum-web-components/accordion": "^1.12.1",
|
|
35
|
+
"@spectrum-web-components/action-bar": "1.12.1",
|
|
36
|
+
"@spectrum-web-components/action-button": "^1.12.1",
|
|
37
|
+
"@spectrum-web-components/action-group": "^1.12.1",
|
|
38
|
+
"@spectrum-web-components/checkbox": "^1.12.1",
|
|
39
|
+
"@spectrum-web-components/color-area": "^1.12.1",
|
|
40
|
+
"@spectrum-web-components/color-handle": "^1.12.1",
|
|
41
|
+
"@spectrum-web-components/color-slider": "^1.12.1",
|
|
42
|
+
"@spectrum-web-components/combobox": "^1.12.1",
|
|
43
|
+
"@spectrum-web-components/dialog": "^1.12.1",
|
|
44
|
+
"@spectrum-web-components/divider": "^1.12.1",
|
|
45
|
+
"@spectrum-web-components/icon": "^1.12.1",
|
|
46
|
+
"@spectrum-web-components/icons-workflow": "^1.12.1",
|
|
47
|
+
"@spectrum-web-components/menu": "^1.12.1",
|
|
48
|
+
"@spectrum-web-components/number-field": "^1.12.1",
|
|
49
|
+
"@spectrum-web-components/overlay": "^1.12.1",
|
|
50
50
|
"@spectrum-web-components/overlay-trigger": "^0.1.7",
|
|
51
|
-
"@spectrum-web-components/picker": "^1.12.
|
|
52
|
-
"@spectrum-web-components/picker-button": "^1.12.
|
|
53
|
-
"@spectrum-web-components/popover": "^1.12.
|
|
54
|
-
"@spectrum-web-components/search": "^1.12.
|
|
55
|
-
"@spectrum-web-components/swatch": "^1.12.
|
|
56
|
-
"@spectrum-web-components/switch": "^1.12.
|
|
57
|
-
"@spectrum-web-components/table": "^1.12.
|
|
58
|
-
"@spectrum-web-components/tabs": "^1.12.
|
|
59
|
-
"@spectrum-web-components/textfield": "^1.12.
|
|
60
|
-
"@spectrum-web-components/theme": "^1.12.
|
|
61
|
-
"@spectrum-web-components/
|
|
51
|
+
"@spectrum-web-components/picker": "^1.12.1",
|
|
52
|
+
"@spectrum-web-components/picker-button": "^1.12.1",
|
|
53
|
+
"@spectrum-web-components/popover": "^1.12.1",
|
|
54
|
+
"@spectrum-web-components/search": "^1.12.1",
|
|
55
|
+
"@spectrum-web-components/swatch": "^1.12.1",
|
|
56
|
+
"@spectrum-web-components/switch": "^1.12.1",
|
|
57
|
+
"@spectrum-web-components/table": "^1.12.1",
|
|
58
|
+
"@spectrum-web-components/tabs": "^1.12.1",
|
|
59
|
+
"@spectrum-web-components/textfield": "^1.12.1",
|
|
60
|
+
"@spectrum-web-components/theme": "^1.12.1",
|
|
61
|
+
"@spectrum-web-components/toast": "^1.12.1",
|
|
62
|
+
"@spectrum-web-components/tooltip": "^1.12.1",
|
|
62
63
|
"lit-html": "^3.3.3",
|
|
63
64
|
"monaco-editor": "^0.55.1",
|
|
64
65
|
"remark-directive": "^4.0.0",
|
package/src/browse/browse.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Displays pages, layouts, components, content, and media in a filterable table grid. Fills the
|
|
5
5
|
* center canvas area as a parallel state to Edit/Design/Preview/Code/Settings. Includes a "New +"
|
|
6
|
-
* button with type-aware entity creation (including
|
|
6
|
+
* button with type-aware entity creation (including content types from project.json).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { html, render as litRender } from "lit-html";
|
|
@@ -95,11 +95,14 @@ async function collectFiles(dir, platform) {
|
|
|
95
95
|
results.push(...sub);
|
|
96
96
|
} else {
|
|
97
97
|
const ext = extOf(entry.name);
|
|
98
|
+
const category = categoryFor(entry.path, ext);
|
|
99
|
+
const type =
|
|
100
|
+
category === "Content" ? contentTypeFor(entry.path) || ext || "file" : ext || "file";
|
|
98
101
|
results.push({
|
|
99
102
|
name: entry.name,
|
|
100
103
|
path: entry.path,
|
|
101
|
-
type
|
|
102
|
-
category
|
|
104
|
+
type,
|
|
105
|
+
category,
|
|
103
106
|
ext,
|
|
104
107
|
});
|
|
105
108
|
}
|
|
@@ -110,6 +113,27 @@ async function collectFiles(dir, platform) {
|
|
|
110
113
|
return results;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Match a file path against project contentTypes source globs to find its content type name.
|
|
118
|
+
* Returns the content type name (capitalized) or null if no match.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} filePath
|
|
121
|
+
* @returns {string | null}
|
|
122
|
+
*/
|
|
123
|
+
function contentTypeFor(filePath) {
|
|
124
|
+
const config = projectState?.projectConfig;
|
|
125
|
+
if (!config?.contentTypes) return null;
|
|
126
|
+
for (const [name, def] of Object.entries(config.contentTypes)) {
|
|
127
|
+
const d = /** @type {any} */ (def);
|
|
128
|
+
if (!d.source) continue;
|
|
129
|
+
const prefix = d.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
|
|
130
|
+
if (filePath.startsWith(prefix + "/") || filePath === prefix) {
|
|
131
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
// ─── Data loading ────────────────────────────────────────────────────────────
|
|
114
138
|
|
|
115
139
|
async function loadFiles() {
|
|
@@ -153,14 +177,14 @@ const ENTITY_TYPES = [
|
|
|
153
177
|
];
|
|
154
178
|
|
|
155
179
|
/**
|
|
156
|
-
* Build frontmatter YAML from a
|
|
180
|
+
* Build frontmatter YAML from a content type's schema properties.
|
|
157
181
|
*
|
|
158
|
-
* @param {string}
|
|
182
|
+
* @param {string} contentTypeName
|
|
159
183
|
* @returns {string}
|
|
160
184
|
*/
|
|
161
|
-
function buildFrontmatterYaml(
|
|
185
|
+
function buildFrontmatterYaml(contentTypeName) {
|
|
162
186
|
const config = projectState?.projectConfig;
|
|
163
|
-
const col = config?.
|
|
187
|
+
const col = config?.contentTypes?.[contentTypeName];
|
|
164
188
|
if (!col?.schema?.properties) return "title: Untitled\n";
|
|
165
189
|
|
|
166
190
|
let yaml = "";
|
|
@@ -172,22 +196,22 @@ function buildFrontmatterYaml(collectionName) {
|
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
/**
|
|
175
|
-
* Get
|
|
199
|
+
* Get content-type-derived entity types from project config.
|
|
176
200
|
*
|
|
177
|
-
* @returns {{ key: string; label: string; dir: string; ext: string;
|
|
201
|
+
* @returns {{ key: string; label: string; dir: string; ext: string; contentTypeName: string }[]}
|
|
178
202
|
*/
|
|
179
|
-
function
|
|
203
|
+
function getContentTypeTypes() {
|
|
180
204
|
const config = projectState?.projectConfig;
|
|
181
|
-
if (!config?.
|
|
182
|
-
return Object.entries(config.
|
|
205
|
+
if (!config?.contentTypes) return [];
|
|
206
|
+
return Object.entries(config.contentTypes).map(([name, def]) => {
|
|
183
207
|
const d = /** @type {any} */ (def);
|
|
184
208
|
const dir = d.source ? d.source.replace(/^\.\//, "").split("/")[0] : name;
|
|
185
209
|
return {
|
|
186
|
-
key: `
|
|
210
|
+
key: `contentType:${name}`,
|
|
187
211
|
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
188
212
|
dir,
|
|
189
213
|
ext: ".md",
|
|
190
|
-
|
|
214
|
+
contentTypeName: name,
|
|
191
215
|
};
|
|
192
216
|
});
|
|
193
217
|
}
|
|
@@ -200,9 +224,9 @@ function getCollectionTypes() {
|
|
|
200
224
|
* @param {{ openFile: (path: string) => void }} ctx
|
|
201
225
|
*/
|
|
202
226
|
async function handleNewEntity(typeKey, container, ctx) {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const allTypes = [...ENTITY_TYPES, ...
|
|
227
|
+
const isContentType = typeKey.startsWith("contentType:");
|
|
228
|
+
const contentTypeName = isContentType ? typeKey.slice("contentType:".length) : null;
|
|
229
|
+
const allTypes = [...ENTITY_TYPES, ...getContentTypeTypes()];
|
|
206
230
|
const typeInfo = allTypes.find((t) => t.key === typeKey);
|
|
207
231
|
if (!typeInfo) return;
|
|
208
232
|
|
|
@@ -217,7 +241,9 @@ async function handleNewEntity(typeKey, container, ctx) {
|
|
|
217
241
|
|
|
218
242
|
let content;
|
|
219
243
|
if (typeInfo.ext === ".md") {
|
|
220
|
-
const frontmatter =
|
|
244
|
+
const frontmatter = contentTypeName
|
|
245
|
+
? buildFrontmatterYaml(contentTypeName)
|
|
246
|
+
: "title: Untitled\n";
|
|
221
247
|
content = `---\n${frontmatter}---\n\n`;
|
|
222
248
|
} else {
|
|
223
249
|
content = JSON.stringify({ tagName: "div", children: [] }, null, "\t");
|
|
@@ -278,7 +304,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
278
304
|
|
|
279
305
|
const files = filteredFiles();
|
|
280
306
|
|
|
281
|
-
const
|
|
307
|
+
const contentTypeTypes = getContentTypeTypes();
|
|
282
308
|
|
|
283
309
|
const filterBar = html`
|
|
284
310
|
<div class="browse-filter-bar">
|
|
@@ -317,8 +343,8 @@ export async function renderBrowse(container, ctx) {
|
|
|
317
343
|
@change=${(/** @type {any} */ e) => handleNewEntity(e.target.value, container, ctx)}
|
|
318
344
|
>
|
|
319
345
|
${ENTITY_TYPES.map((t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`)}
|
|
320
|
-
${
|
|
321
|
-
? html`<sp-menu-divider></sp-menu-divider> ${
|
|
346
|
+
${contentTypeTypes.length
|
|
347
|
+
? html`<sp-menu-divider></sp-menu-divider> ${contentTypeTypes.map(
|
|
322
348
|
(t) => html`<sp-menu-item value=${t.key}>${t.label}</sp-menu-item>`,
|
|
323
349
|
)}`
|
|
324
350
|
: ""}
|
|
@@ -374,7 +400,7 @@ export async function renderBrowse(container, ctx) {
|
|
|
374
400
|
>
|
|
375
401
|
<sp-table-cell class="browse-name-cell">${f.name}</sp-table-cell>
|
|
376
402
|
<sp-table-cell>${f.category}</sp-table-cell>
|
|
377
|
-
<sp-table-cell>${f.
|
|
403
|
+
<sp-table-cell>${f.type}</sp-table-cell>
|
|
378
404
|
<sp-table-cell class="browse-path-cell">${f.path}</sp-table-cell>
|
|
379
405
|
</sp-table-row>
|
|
380
406
|
`,
|
|
@@ -6,12 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { elToPath, stripEventHandlers, projectState } from "../store.js";
|
|
8
8
|
import { view } from "../view.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
renderNode as runtimeRenderNode,
|
|
11
|
+
buildScope,
|
|
12
|
+
defineElement,
|
|
13
|
+
setSkipServerFunctions,
|
|
14
|
+
} from "@jxsuite/runtime";
|
|
10
15
|
import {
|
|
11
16
|
getEffectiveElements,
|
|
12
17
|
getEffectiveImports,
|
|
13
18
|
getEffectiveMedia,
|
|
14
19
|
getEffectiveHead,
|
|
20
|
+
getEffectiveLayoutPath,
|
|
21
|
+
resolveLayoutDoc,
|
|
22
|
+
distributePageIntoLayout,
|
|
15
23
|
} from "../site-context.js";
|
|
16
24
|
import { componentRegistry, computeRelativePath } from "../files/components.js";
|
|
17
25
|
import { prepareForEditMode } from "../utils/edit-display.js";
|
|
@@ -20,6 +28,56 @@ import { getActiveElement } from "../editor/inline-edit.js";
|
|
|
20
28
|
/** @type {any} */
|
|
21
29
|
let _ctx = null;
|
|
22
30
|
|
|
31
|
+
/** Set of DOM elements that originated from the layout (not page content). */
|
|
32
|
+
export const layoutElements = new WeakSet();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk the merged document tree to find the path prefix where page children were distributed into
|
|
36
|
+
* the layout slot. Returns the path to the container whose children are the page content (first
|
|
37
|
+
* non-$__layout children array).
|
|
38
|
+
*
|
|
39
|
+
* @param {any} node
|
|
40
|
+
* @param {any[]} [path]
|
|
41
|
+
* @returns {any[] | null}
|
|
42
|
+
*/
|
|
43
|
+
function findPageContentPrefix(node, path = []) {
|
|
44
|
+
if (!node || typeof node !== "object") return null;
|
|
45
|
+
if (Array.isArray(node.children)) {
|
|
46
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
47
|
+
const child = node.children[i];
|
|
48
|
+
if (child && typeof child === "object" && !child.$__layout) {
|
|
49
|
+
return [...path, "children"];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
53
|
+
const child = node.children[i];
|
|
54
|
+
if (child && typeof child === "object" && child.$__layout) {
|
|
55
|
+
const found = findPageContentPrefix(child, [...path, "children", i]);
|
|
56
|
+
if (found) return found;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The path of the currently active layout file, or null. */
|
|
64
|
+
export let activeLayoutPath = /** @type {string | null} */ (null);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursively mark all nodes in a layout doc tree with $__layout: true so we can identify which
|
|
68
|
+
* rendered DOM elements came from the layout vs page content.
|
|
69
|
+
*/
|
|
70
|
+
function markLayoutNodes(/** @type {any} */ node) {
|
|
71
|
+
if (!node || typeof node !== "object") return;
|
|
72
|
+
node.$__layout = true;
|
|
73
|
+
if (Array.isArray(node.children)) {
|
|
74
|
+
for (const child of node.children) markLayoutNodes(child);
|
|
75
|
+
}
|
|
76
|
+
if (node.$elements) {
|
|
77
|
+
for (const el of node.$elements) markLayoutNodes(el);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
23
81
|
/**
|
|
24
82
|
* Initialize the canvas live render module.
|
|
25
83
|
*
|
|
@@ -45,9 +103,46 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
45
103
|
const S = _ctx.getState();
|
|
46
104
|
const canvasMode = _ctx.getCanvasMode();
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
// Suppress server function resolution in non-preview modes to avoid
|
|
107
|
+
// failed proxy calls and infinite reactive retries (also covers
|
|
108
|
+
// async custom element connectedCallbacks that run after this function returns)
|
|
109
|
+
setSkipServerFunctions(canvasMode !== "preview");
|
|
110
|
+
|
|
111
|
+
let renderDoc =
|
|
49
112
|
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
50
113
|
|
|
114
|
+
// ─── Layout wrapping ────────────────────────────────────────────────────
|
|
115
|
+
// For page documents, resolve the layout and wrap content in the layout shell.
|
|
116
|
+
// Layout-originated nodes are marked with $__layout so we can distinguish them.
|
|
117
|
+
let layoutWrapped = false;
|
|
118
|
+
activeLayoutPath = null;
|
|
119
|
+
|
|
120
|
+
const isPage =
|
|
121
|
+
S.documentPath &&
|
|
122
|
+
projectState?.isSiteProject &&
|
|
123
|
+
(S.documentPath.startsWith("pages/") || S.documentPath.startsWith("./pages/"));
|
|
124
|
+
|
|
125
|
+
/** @type {any[] | null} Path prefix in merged doc where page children live */
|
|
126
|
+
let pageContentPrefix = null;
|
|
127
|
+
|
|
128
|
+
if (isPage) {
|
|
129
|
+
const layoutPath = getEffectiveLayoutPath(doc.$layout);
|
|
130
|
+
if (layoutPath) {
|
|
131
|
+
const layoutDoc = await resolveLayoutDoc(layoutPath);
|
|
132
|
+
if (layoutDoc) {
|
|
133
|
+
if (gen !== view.renderGeneration) return null;
|
|
134
|
+
activeLayoutPath = layoutPath.replace(/^\.\//, "");
|
|
135
|
+
markLayoutNodes(layoutDoc);
|
|
136
|
+
const pageForSlots = canvasMode === "preview" ? structuredClone(doc) : renderDoc;
|
|
137
|
+
const merged = distributePageIntoLayout(layoutDoc, pageForSlots);
|
|
138
|
+
renderDoc =
|
|
139
|
+
canvasMode === "preview" ? merged : prepareForEditMode(stripEventHandlers(merged));
|
|
140
|
+
layoutWrapped = true;
|
|
141
|
+
pageContentPrefix = findPageContentPrefix(merged);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
51
146
|
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
52
147
|
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
53
148
|
const mapParentPaths = new Set();
|
|
@@ -127,7 +222,13 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
127
222
|
console.warn("Studio: failed to import package", entry, e);
|
|
128
223
|
}
|
|
129
224
|
} else if (entry?.$ref) {
|
|
130
|
-
|
|
225
|
+
let href;
|
|
226
|
+
try {
|
|
227
|
+
href = new URL(entry.$ref, docBase).href;
|
|
228
|
+
} catch (/** @type {any} */ urlErr) {
|
|
229
|
+
console.warn("Studio: invalid element URL", { ref: entry.$ref, docBase }, urlErr);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
131
232
|
try {
|
|
132
233
|
await defineElement(href);
|
|
133
234
|
} catch (/** @type {any} */ e) {
|
|
@@ -218,26 +319,49 @@ export async function renderCanvasLive(gen, doc, canvasEl) {
|
|
|
218
319
|
if (gen !== view.renderGeneration) return null;
|
|
219
320
|
const el = /** @type {HTMLElement} */ (
|
|
220
321
|
runtimeRenderNode(renderDoc, $defs, {
|
|
221
|
-
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
322
|
+
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path, /** @type {any} */ def) {
|
|
323
|
+
// Track layout-originated elements — don't store in elToPath to avoid
|
|
324
|
+
// path collisions with remapped page content paths
|
|
325
|
+
if (layoutWrapped && def?.$__layout) {
|
|
326
|
+
layoutElements.add(el);
|
|
327
|
+
if (el.setAttribute) el.setAttribute("data-jx-layout", "");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Remap layout-wrapped paths: strip the layout prefix so paths are
|
|
332
|
+
// relative to the original page document (which is what S.document holds)
|
|
333
|
+
let mappedPath = path;
|
|
334
|
+
if (layoutWrapped && pageContentPrefix) {
|
|
335
|
+
const pfx = pageContentPrefix;
|
|
336
|
+
if (
|
|
337
|
+
path.length >= pfx.length &&
|
|
338
|
+
pfx.every((/** @type {any} */ seg, /** @type {number} */ i) => path[i] === seg)
|
|
339
|
+
) {
|
|
340
|
+
mappedPath = ["children", ...path.slice(pfx.length)];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
222
344
|
// Remap $map paths: wrapper and template children → real document paths
|
|
223
345
|
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
224
346
|
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
225
|
-
let mappedPath = path;
|
|
226
347
|
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
227
|
-
for (let i = 0; i <
|
|
228
|
-
if (
|
|
229
|
-
const parentKey =
|
|
348
|
+
for (let i = 0; i < mappedPath.length - 1; i++) {
|
|
349
|
+
if (mappedPath[i] === "children" && mappedPath[i + 1] === 0) {
|
|
350
|
+
const parentKey = mappedPath.slice(0, i).join("/");
|
|
230
351
|
if (mapParentPaths.has(parentKey)) {
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
mappedPath = path.slice(0, i + 1);
|
|
352
|
+
if (mappedPath.length === i + 2) {
|
|
353
|
+
mappedPath = mappedPath.slice(0, i + 1);
|
|
234
354
|
} else if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
355
|
+
mappedPath.length >= i + 4 &&
|
|
356
|
+
mappedPath[i + 2] === "children" &&
|
|
357
|
+
mappedPath[i + 3] === 0
|
|
238
358
|
) {
|
|
239
|
-
|
|
240
|
-
|
|
359
|
+
mappedPath = [
|
|
360
|
+
...mappedPath.slice(0, i),
|
|
361
|
+
"children",
|
|
362
|
+
"map",
|
|
363
|
+
...mappedPath.slice(i + 4),
|
|
364
|
+
];
|
|
241
365
|
}
|
|
242
366
|
break;
|
|
243
367
|
}
|
|
@@ -66,12 +66,15 @@ export function renderCanvas() {
|
|
|
66
66
|
// Advance render generation so stale async renders from the previous cycle bail out
|
|
67
67
|
++view.renderGeneration;
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
69
|
+
// Detect whether this is a mode transition or a content-only re-render
|
|
70
|
+
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
71
|
+
|
|
72
|
+
// Only clear Lit's internal state on mode transitions (structural panel changes).
|
|
73
|
+
// For content re-renders in the same mode, Lit's template diffing preserves
|
|
74
|
+
// the panel structure. Bailed async renders can't corrupt the DOM because
|
|
75
|
+
// renderCanvasLive uses atomic clear (innerHTML = "" right before appendChild).
|
|
73
76
|
// @ts-ignore
|
|
74
|
-
if (canvasWrap["_$litPart$"]) {
|
|
77
|
+
if (modeChanged && canvasWrap["_$litPart$"]) {
|
|
75
78
|
canvasWrap.textContent = "";
|
|
76
79
|
// @ts-ignore
|
|
77
80
|
delete canvasWrap["_$litPart$"];
|
|
@@ -101,7 +104,6 @@ export function renderCanvas() {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
// Detect whether this is a mode transition or a content-only re-render
|
|
104
|
-
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
105
107
|
view.prevCanvasMode = canvasMode;
|
|
106
108
|
|
|
107
109
|
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
@@ -247,10 +249,11 @@ export function renderCanvas() {
|
|
|
247
249
|
resetZoomIndicator();
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
const { baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
250
253
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
251
254
|
const editTpl = html`
|
|
252
255
|
<div class="content-edit-canvas">
|
|
253
|
-
<div class="content-edit-column">${panelTpl}</div>
|
|
256
|
+
<div class="content-edit-column" style="max-width:${baseWidth}px">${panelTpl}</div>
|
|
254
257
|
</div>
|
|
255
258
|
`;
|
|
256
259
|
litRender(editTpl, canvasWrap);
|
package/src/files/files.js
CHANGED
|
@@ -564,6 +564,8 @@ export async function openFileFromTree(ctx, path) {
|
|
|
564
564
|
if (path.endsWith(".md")) {
|
|
565
565
|
await ctx.loadMarkdown(content, null);
|
|
566
566
|
ctx.S.documentPath = path;
|
|
567
|
+
ctx.S.dirty = false;
|
|
568
|
+
ctx.commit(ctx.S);
|
|
567
569
|
} else {
|
|
568
570
|
const doc = JSON.parse(content);
|
|
569
571
|
const newS = createState(doc);
|