@jxsuite/studio 0.9.0 → 0.10.1
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 +137161 -137144
- package/dist/studio.js.map +100 -97
- package/package.json +2 -2
- package/src/canvas/canvas-helpers.js +121 -0
- package/src/canvas/canvas-live-render.js +286 -0
- package/src/canvas/canvas-render.js +429 -0
- package/src/canvas/canvas-utils.js +33 -69
- package/src/editor/component-inline-edit.js +5 -4
- package/src/editor/content-inline-edit.js +11 -18
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +2 -13
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +10 -13
- package/src/panels/pseudo-preview.js +7 -14
- package/src/panels/shared.js +1 -1
- package/src/platform.js +9 -6
- package/src/state.js +9 -1
- package/src/store.js +9 -0
- package/src/studio.js +57 -807
- package/src/view.js +0 -2
package/src/studio.js
CHANGED
|
@@ -14,12 +14,9 @@ import {
|
|
|
14
14
|
popDocument,
|
|
15
15
|
getNodeAtPath,
|
|
16
16
|
pathsEqual,
|
|
17
|
-
parentElementPath,
|
|
18
17
|
canvasWrap,
|
|
19
18
|
toolbarEl,
|
|
20
|
-
elToPath,
|
|
21
19
|
canvasPanels,
|
|
22
|
-
stripEventHandlers,
|
|
23
20
|
registerRenderer,
|
|
24
21
|
render,
|
|
25
22
|
update,
|
|
@@ -42,30 +39,21 @@ import {
|
|
|
42
39
|
|
|
43
40
|
import { view } from "./view.js";
|
|
44
41
|
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
import {
|
|
48
|
-
isEditing,
|
|
49
|
-
getActiveElement,
|
|
50
|
-
isEditableBlock,
|
|
51
|
-
isInlineInContext,
|
|
52
|
-
} from "./editor/inline-edit.js";
|
|
42
|
+
import { isEditing, isEditableBlock } from "./editor/inline-edit.js";
|
|
53
43
|
import {
|
|
54
44
|
enterComponentInlineEdit,
|
|
55
45
|
initComponentInlineEdit,
|
|
56
46
|
} from "./editor/component-inline-edit.js";
|
|
57
|
-
import { enterInlineEdit
|
|
47
|
+
import { enterInlineEdit } from "./editor/content-inline-edit.js";
|
|
58
48
|
import {
|
|
59
49
|
initCanvasUtils,
|
|
60
|
-
canvasPanelTemplate,
|
|
61
|
-
observeCenterUntilStable,
|
|
62
50
|
applyTransform,
|
|
63
|
-
renderZoomIndicator,
|
|
64
|
-
resetZoomIndicator,
|
|
65
51
|
positionZoomIndicator,
|
|
66
52
|
updateActivePanelHeaders,
|
|
67
53
|
} from "./canvas/canvas-utils.js";
|
|
68
|
-
import {
|
|
54
|
+
import { initCanvasHelpers, getActivePanel, findCanvasElement } from "./canvas/canvas-helpers.js";
|
|
55
|
+
import { initCanvasRender, renderCanvas } from "./canvas/canvas-render.js";
|
|
56
|
+
import { initCanvasLiveRender } from "./canvas/canvas-live-render.js";
|
|
69
57
|
import {
|
|
70
58
|
renderStatusbar,
|
|
71
59
|
statusMessage,
|
|
@@ -91,29 +79,13 @@ import { renderHeadTemplate } from "./panels/head-panel.js";
|
|
|
91
79
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
|
|
92
80
|
|
|
93
81
|
import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
|
|
94
|
-
import {
|
|
95
|
-
parseMediaEntries,
|
|
96
|
-
activeBreakpointsForWidth,
|
|
97
|
-
collectMediaOverrides,
|
|
98
|
-
applyOverridesToCanvas,
|
|
99
|
-
} from "./utils/canvas-media.js";
|
|
82
|
+
import { parseMediaEntries } from "./utils/canvas-media.js";
|
|
100
83
|
import { createDevServerPlatform } from "./platforms/devserver.js";
|
|
101
84
|
import { codeService } from "./services/code-services.js";
|
|
102
|
-
import {
|
|
103
|
-
getEffectiveMedia,
|
|
104
|
-
getEffectiveImports,
|
|
105
|
-
getEffectiveElements,
|
|
106
|
-
getEffectiveHead,
|
|
107
|
-
} from "./site-context.js";
|
|
108
85
|
import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel.js";
|
|
109
|
-
import {
|
|
110
|
-
componentRegistry,
|
|
111
|
-
loadComponentRegistry,
|
|
112
|
-
computeRelativePath,
|
|
113
|
-
} from "./files/components.js";
|
|
86
|
+
import { loadComponentRegistry } from "./files/components.js";
|
|
114
87
|
|
|
115
|
-
import { html, render as litRender
|
|
116
|
-
import { ref } from "lit-html/directives/ref.js";
|
|
88
|
+
import { html, render as litRender } from "lit-html";
|
|
117
89
|
|
|
118
90
|
import webdata from "../data/webdata.json";
|
|
119
91
|
import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
@@ -124,29 +96,20 @@ import { renderGitPanel } from "./panels/git-panel.js";
|
|
|
124
96
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
125
97
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
126
98
|
import "./ui/panel-resize.js";
|
|
127
|
-
import { dismissContextMenu } from "./editor/context-menu.js";
|
|
128
99
|
import { initShortcuts } from "./editor/shortcuts.js";
|
|
129
100
|
import { renderActivityBar } from "./panels/activity-bar.js";
|
|
130
|
-
import { renderBrowse } from "./browse/browse.js";
|
|
131
101
|
import * as toolbarPanel from "./panels/toolbar.js";
|
|
132
102
|
import * as overlaysPanel from "./panels/overlays.js";
|
|
133
103
|
import * as rightPanelMod from "./panels/right-panel.js";
|
|
134
104
|
import * as leftPanelMod from "./panels/left-panel.js";
|
|
135
|
-
import {
|
|
105
|
+
import { renderStylebookOverlays } from "./panels/stylebook-panel.js";
|
|
136
106
|
import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd.js";
|
|
137
|
-
import {
|
|
138
|
-
import {
|
|
139
|
-
import {
|
|
140
|
-
renderBlockActionBar,
|
|
141
|
-
dismissLinkPopover,
|
|
142
|
-
initBlockActionBar,
|
|
143
|
-
} from "./panels/block-action-bar.js";
|
|
107
|
+
import { defaultDef } from "./panels/shared.js";
|
|
108
|
+
import { registerFunctionCompletions } from "./panels/editors.js";
|
|
109
|
+
import { renderBlockActionBar, initBlockActionBar } from "./panels/block-action-bar.js";
|
|
144
110
|
import { initCssData } from "./panels/style-utils.js";
|
|
145
|
-
import {
|
|
146
|
-
import {
|
|
147
|
-
import { initCanvasDnD, registerPanelDnD } from "./panels/canvas-dnd.js";
|
|
148
|
-
import { initPanelEvents, registerPanelEvents } from "./panels/panel-events.js";
|
|
149
|
-
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
111
|
+
import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
|
|
112
|
+
import { initPanelEvents } from "./panels/panel-events.js";
|
|
150
113
|
|
|
151
114
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
152
115
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
@@ -177,11 +140,12 @@ async function navigateToComponent(componentPath) {
|
|
|
177
140
|
const platform = getPlatform();
|
|
178
141
|
const content = await platform.readFile(componentPath);
|
|
179
142
|
if (!content) return;
|
|
180
|
-
const
|
|
181
|
-
S = pushDocument(S,
|
|
143
|
+
const parsed = JSON.parse(content);
|
|
144
|
+
S = pushDocument(S, parsed, componentPath);
|
|
182
145
|
S.dirty = false;
|
|
146
|
+
({ doc, session } = fromFlat(S));
|
|
183
147
|
render();
|
|
184
|
-
statusMessage(`Editing component: ${
|
|
148
|
+
statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
|
|
185
149
|
} catch (/** @type {any} */ e) {
|
|
186
150
|
const err = /** @type {any} */ (e);
|
|
187
151
|
statusMessage(`Error: ${err.message}`);
|
|
@@ -200,6 +164,7 @@ async function navigateBack() {
|
|
|
200
164
|
}
|
|
201
165
|
}
|
|
202
166
|
S = popDocument(S);
|
|
167
|
+
({ doc, session } = fromFlat(S));
|
|
203
168
|
render();
|
|
204
169
|
statusMessage("Returned to parent document");
|
|
205
170
|
}
|
|
@@ -230,259 +195,6 @@ async function closeFunctionEditor() {
|
|
|
230
195
|
updateUi("editingFunction", null);
|
|
231
196
|
}
|
|
232
197
|
|
|
233
|
-
import { prepareForEditMode } from "./utils/edit-display.js";
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Render a Jx document into a canvas element using the real runtime. Populates elToPath for each
|
|
237
|
-
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
238
|
-
* failure.
|
|
239
|
-
*
|
|
240
|
-
* @param {number} gen - Render generation for staleness detection
|
|
241
|
-
* @param {any} doc
|
|
242
|
-
* @param {any} canvasEl
|
|
243
|
-
*/
|
|
244
|
-
async function renderCanvasLive(gen, doc, canvasEl) {
|
|
245
|
-
canvasEl.innerHTML = "";
|
|
246
|
-
|
|
247
|
-
// Apply content mode typography styling
|
|
248
|
-
if (S.mode === "content") {
|
|
249
|
-
canvasEl.setAttribute("data-content-mode", "");
|
|
250
|
-
} else {
|
|
251
|
-
canvasEl.removeAttribute("data-content-mode");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const renderDoc =
|
|
255
|
-
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
256
|
-
|
|
257
|
-
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
258
|
-
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
259
|
-
const mapParentPaths = new Set();
|
|
260
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
261
|
-
(function findMapParents(/** @type {any} */ node, /** @type {any[]} */ path) {
|
|
262
|
-
if (!node || typeof node !== "object") return;
|
|
263
|
-
if (
|
|
264
|
-
node.children &&
|
|
265
|
-
typeof node.children === "object" &&
|
|
266
|
-
node.children.$prototype === "Array"
|
|
267
|
-
) {
|
|
268
|
-
mapParentPaths.add(path.join("/"));
|
|
269
|
-
}
|
|
270
|
-
if (Array.isArray(node.children)) {
|
|
271
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
272
|
-
findMapParents(node.children[i], [...path, "children", i]);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (node.$switch && node.cases) {
|
|
276
|
-
for (const [k, v] of Object.entries(node.cases)) {
|
|
277
|
-
findMapParents(v, [...path, "cases", k]);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
})(doc, []);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const root = projectState?.projectRoot || "";
|
|
285
|
-
const docPrefix = root ? `${root}/` : "";
|
|
286
|
-
const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
|
|
287
|
-
|
|
288
|
-
// Register custom elements so the runtime can render them
|
|
289
|
-
let effectiveElements = getEffectiveElements(renderDoc.$elements);
|
|
290
|
-
|
|
291
|
-
// In content mode (markdown), auto-discover components for directive-based
|
|
292
|
-
// custom elements that have no explicit $elements registration.
|
|
293
|
-
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
294
|
-
const existingRefs = new Set(
|
|
295
|
-
effectiveElements.map((/** @type {any} */ e) => (typeof e === "string" ? e : e?.$ref)),
|
|
296
|
-
);
|
|
297
|
-
/** @param {any} node */
|
|
298
|
-
const collectTags = (node) => {
|
|
299
|
-
/** @type {Set<string>} */
|
|
300
|
-
const tags = new Set();
|
|
301
|
-
if (!node || typeof node !== "object") return tags;
|
|
302
|
-
if (node.tagName) tags.add(node.tagName);
|
|
303
|
-
if (Array.isArray(node.children)) {
|
|
304
|
-
for (const child of node.children) {
|
|
305
|
-
for (const t of collectTags(child)) tags.add(t);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return tags;
|
|
309
|
-
};
|
|
310
|
-
for (const tag of collectTags(renderDoc)) {
|
|
311
|
-
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
312
|
-
if (comp && comp.source !== "npm") {
|
|
313
|
-
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
314
|
-
if (!existingRefs.has(relPath)) {
|
|
315
|
-
effectiveElements.push({ $ref: relPath });
|
|
316
|
-
existingRefs.add(relPath);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (effectiveElements.length) {
|
|
323
|
-
renderDoc.$elements = effectiveElements;
|
|
324
|
-
for (const entry of effectiveElements) {
|
|
325
|
-
if (typeof entry === "string") {
|
|
326
|
-
try {
|
|
327
|
-
const specifier =
|
|
328
|
-
entry.startsWith("/") || entry.startsWith(".")
|
|
329
|
-
? entry
|
|
330
|
-
: `/${projectState?.projectRoot || ""}/node_modules/${entry}`.replace(/\/+/g, "/");
|
|
331
|
-
await import(specifier);
|
|
332
|
-
} catch (/** @type {any} */ e) {
|
|
333
|
-
console.warn("Studio: failed to import package", entry, e);
|
|
334
|
-
}
|
|
335
|
-
} else if (entry?.$ref) {
|
|
336
|
-
const href = new URL(entry.$ref, docBase).href;
|
|
337
|
-
try {
|
|
338
|
-
await defineElement(href);
|
|
339
|
-
} catch (/** @type {any} */ e) {
|
|
340
|
-
console.warn("Studio: failed to register element", entry.$ref, e);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Bail out if a newer render started while we were importing elements
|
|
347
|
-
if (gen !== view.renderGeneration) return null;
|
|
348
|
-
|
|
349
|
-
// Inject site-level imports so buildScope can resolve $prototype names
|
|
350
|
-
renderDoc.imports = getEffectiveImports(renderDoc.imports);
|
|
351
|
-
|
|
352
|
-
// Apply project-level styles mirroring the compiler convention:
|
|
353
|
-
// viewport ≈ :root → CSS custom properties (they inherit down)
|
|
354
|
-
// canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
|
|
355
|
-
// This ensures project font-family, color, etc. override the
|
|
356
|
-
// content-mode fallback typography rules in the stylesheet.
|
|
357
|
-
// In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
|
|
358
|
-
const viewport = canvasEl.closest(".canvas-panel-viewport");
|
|
359
|
-
const editSurface = canvasMode === "edit" ? canvasEl.closest(".content-edit-canvas") : null;
|
|
360
|
-
const siteStyle = projectState?.projectConfig?.style;
|
|
361
|
-
if (viewport) {
|
|
362
|
-
viewport.style.cssText = "";
|
|
363
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
364
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
365
|
-
if (k.startsWith("--")) {
|
|
366
|
-
viewport.style.setProperty(k, String(v));
|
|
367
|
-
} else {
|
|
368
|
-
/** @type {any} */ (viewport.style)[k] = v;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (editSurface) {
|
|
374
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
375
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
376
|
-
if (k.startsWith("--")) {
|
|
377
|
-
/** @type {any} */ (editSurface).style.setProperty(k, String(v));
|
|
378
|
-
} else {
|
|
379
|
-
/** @type {any} */ (editSurface.style)[k] = v;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
385
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
386
|
-
if (!k.startsWith("--")) {
|
|
387
|
-
/** @type {any} */ (canvasEl.style)[k] = v;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Inject site-level $media so runtime can resolve media queries in styles
|
|
393
|
-
renderDoc.$media = getEffectiveMedia(renderDoc.$media);
|
|
394
|
-
|
|
395
|
-
// Inject $head elements (link/meta/script) into document.head
|
|
396
|
-
const effectiveHead = getEffectiveHead(renderDoc.$head);
|
|
397
|
-
if (effectiveHead.length) {
|
|
398
|
-
for (const entry of effectiveHead) {
|
|
399
|
-
if (!entry?.tagName) continue;
|
|
400
|
-
const tag = entry.tagName.toLowerCase();
|
|
401
|
-
const attrs = { ...entry.attributes };
|
|
402
|
-
const root = projectState?.projectRoot || "";
|
|
403
|
-
for (const key of ["href", "src"]) {
|
|
404
|
-
if (
|
|
405
|
-
attrs[key] &&
|
|
406
|
-
!attrs[key].startsWith("/") &&
|
|
407
|
-
!attrs[key].startsWith(".") &&
|
|
408
|
-
!attrs[key].startsWith("http")
|
|
409
|
-
) {
|
|
410
|
-
attrs[key] = `/${root}/node_modules/${attrs[key]}`.replace(/\/+/g, "/");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
const selector = `${tag}${attrs.href ? `[href="${attrs.href}"]` : ""}${attrs.src ? `[src="${attrs.src}"]` : ""}`;
|
|
414
|
-
if (selector !== tag && document.head.querySelector(selector)) continue;
|
|
415
|
-
const el = document.createElement(tag);
|
|
416
|
-
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, /** @type {string} */ (v));
|
|
417
|
-
if (entry.textContent) el.textContent = entry.textContent;
|
|
418
|
-
document.head.appendChild(el);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
423
|
-
// Bail out if a newer render started while buildScope was running
|
|
424
|
-
if (gen !== view.renderGeneration) return null;
|
|
425
|
-
const el = /** @type {HTMLElement} */ (
|
|
426
|
-
runtimeRenderNode(renderDoc, $defs, {
|
|
427
|
-
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
428
|
-
// Remap $map paths: wrapper and template children → real document paths
|
|
429
|
-
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
430
|
-
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
431
|
-
let mappedPath = path;
|
|
432
|
-
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
433
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
434
|
-
if (path[i] === "children" && path[i + 1] === 0) {
|
|
435
|
-
const parentKey = path.slice(0, i).join("/");
|
|
436
|
-
if (mapParentPaths.has(parentKey)) {
|
|
437
|
-
if (path.length === i + 2) {
|
|
438
|
-
// Wrapper div itself → $map container path
|
|
439
|
-
mappedPath = path.slice(0, i + 1);
|
|
440
|
-
} else if (
|
|
441
|
-
path.length >= i + 4 &&
|
|
442
|
-
path[i + 2] === "children" &&
|
|
443
|
-
path[i + 3] === 0
|
|
444
|
-
) {
|
|
445
|
-
// Template or its descendants → children/map/...rest
|
|
446
|
-
mappedPath = [...path.slice(0, i), "children", "map", ...path.slice(i + 4)];
|
|
447
|
-
}
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
elToPath.set(el, mappedPath);
|
|
454
|
-
},
|
|
455
|
-
_path: [],
|
|
456
|
-
})
|
|
457
|
-
);
|
|
458
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
459
|
-
// Disable pointer events on all rendered elements for edit mode
|
|
460
|
-
el.style.pointerEvents = "none";
|
|
461
|
-
for (const child of el.querySelectorAll("*")) {
|
|
462
|
-
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
canvasEl.appendChild(el);
|
|
466
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
467
|
-
// Custom element connectedCallbacks render children asynchronously —
|
|
468
|
-
// sweep again after they've had a chance to run
|
|
469
|
-
requestAnimationFrame(() => {
|
|
470
|
-
const editingEl = getActiveElement();
|
|
471
|
-
for (const child of canvasEl.querySelectorAll("*")) {
|
|
472
|
-
// Preserve pointer-events on the actively-edited element
|
|
473
|
-
if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
|
|
474
|
-
if (editingEl && child === editingEl) continue;
|
|
475
|
-
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
return $defs;
|
|
480
|
-
} catch (/** @type {any} */ err) {
|
|
481
|
-
console.warn("renderCanvasLive failed:", err.message, err);
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
198
|
// ─── Webdata: datalists for autocomplete ──────────────────────────────────────
|
|
487
199
|
|
|
488
200
|
const datalistHost = document.createElement("div");
|
|
@@ -542,24 +254,19 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
542
254
|
});
|
|
543
255
|
|
|
544
256
|
overlaysPanel.mount({
|
|
545
|
-
effectiveZoom,
|
|
546
257
|
getCanvasMode: () => canvasMode,
|
|
547
258
|
isEditing,
|
|
548
259
|
renderBlockActionBar,
|
|
549
|
-
findCanvasElement,
|
|
550
|
-
getActivePanel,
|
|
551
260
|
});
|
|
552
261
|
|
|
553
262
|
initBlockActionBar({
|
|
554
263
|
getCanvasMode: () => canvasMode,
|
|
555
|
-
findCanvasElement,
|
|
556
|
-
getActivePanel,
|
|
557
264
|
navigateToComponent,
|
|
558
265
|
createFloatingContainer,
|
|
559
266
|
});
|
|
560
267
|
|
|
561
268
|
initComponentInlineEdit({ findCanvasElement });
|
|
562
|
-
|
|
269
|
+
initCanvasHelpers({ getCanvasMode: () => canvasMode });
|
|
563
270
|
initCanvasUtils({
|
|
564
271
|
getCanvasMode: () => canvasMode,
|
|
565
272
|
getZoom: () => S.ui.zoom,
|
|
@@ -569,19 +276,29 @@ initCanvasUtils({
|
|
|
569
276
|
},
|
|
570
277
|
renderStylebookOverlays,
|
|
571
278
|
});
|
|
572
|
-
initPseudoPreview({ getActivePanel, findCanvasElement });
|
|
573
|
-
initCanvasDnD({ effectiveZoom });
|
|
574
279
|
initPanelEvents({
|
|
575
280
|
getState: () => S,
|
|
576
281
|
setState: (s) => {
|
|
577
282
|
S = s;
|
|
283
|
+
({ doc, session } = fromFlat(S));
|
|
578
284
|
},
|
|
579
285
|
getCanvasMode: () => canvasMode,
|
|
580
|
-
bubbleInlinePath,
|
|
581
|
-
findCanvasElement,
|
|
582
286
|
enterInlineEdit,
|
|
583
287
|
navigateToComponent,
|
|
584
|
-
|
|
288
|
+
});
|
|
289
|
+
initCanvasLiveRender({
|
|
290
|
+
getState: () => S,
|
|
291
|
+
getCanvasMode: () => canvasMode,
|
|
292
|
+
});
|
|
293
|
+
initCanvasRender({
|
|
294
|
+
getCanvasMode: () => canvasMode,
|
|
295
|
+
setCanvasMode: (mode) => {
|
|
296
|
+
canvasMode = mode;
|
|
297
|
+
},
|
|
298
|
+
getState: () => S,
|
|
299
|
+
update,
|
|
300
|
+
openFileFromTree,
|
|
301
|
+
exportFile,
|
|
585
302
|
});
|
|
586
303
|
|
|
587
304
|
rightPanelMod.mount({
|
|
@@ -700,10 +417,14 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
700
417
|
if (patch.ui) {
|
|
701
418
|
session.ui = { ...prev.ui, ...patch.ui };
|
|
702
419
|
}
|
|
420
|
+
if (patch.canvas) {
|
|
421
|
+
session.canvas = { ...prev.canvas, ...patch.canvas };
|
|
422
|
+
}
|
|
703
423
|
S = toFlat(doc, session);
|
|
704
424
|
|
|
705
425
|
const selChanged = !pathsEqual(prev.selection, session.selection);
|
|
706
426
|
const uiChanged = prev.ui !== session.ui;
|
|
427
|
+
const canvasChanged = prev.canvas !== session.canvas;
|
|
707
428
|
|
|
708
429
|
const canvasUiChanged =
|
|
709
430
|
uiChanged &&
|
|
@@ -724,7 +445,7 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
724
445
|
console.error("renderCanvas error:", e);
|
|
725
446
|
}
|
|
726
447
|
safeRenderLeftPanel();
|
|
727
|
-
} else if (selChanged || leftUiChanged) {
|
|
448
|
+
} else if (selChanged || leftUiChanged || canvasChanged) {
|
|
728
449
|
safeRenderLeftPanel();
|
|
729
450
|
}
|
|
730
451
|
|
|
@@ -732,20 +453,10 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
732
453
|
updateActivePanelHeaders();
|
|
733
454
|
}
|
|
734
455
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// Register post-render hook for pseudo-state preview
|
|
742
|
-
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
743
|
-
|
|
744
|
-
// Register post-render hook for pending inline edit
|
|
745
|
-
addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
746
|
-
if (view.pendingInlineEdit && prevDoc === S.document) {
|
|
747
|
-
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
748
|
-
view.pendingInlineEdit = null;
|
|
456
|
+
// Process pending inline edit when canvas becomes ready
|
|
457
|
+
if (canvasChanged && session.canvas.status === "ready" && session.ui?.pendingInlineEdit) {
|
|
458
|
+
const { path, mediaName: mn } = session.ui.pendingInlineEdit;
|
|
459
|
+
updateUi("pendingInlineEdit", null);
|
|
749
460
|
const targetPanel =
|
|
750
461
|
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
751
462
|
if (targetPanel) {
|
|
@@ -753,8 +464,16 @@ addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
|
753
464
|
if (el) enterComponentInlineEdit(el, path);
|
|
754
465
|
}
|
|
755
466
|
}
|
|
467
|
+
|
|
468
|
+
runPostRenderHooks(doc.document, prev.selection);
|
|
469
|
+
|
|
470
|
+
const hoverChanged = prev.hover !== session.hover;
|
|
471
|
+
notify({ doc: false, selection: selChanged, hover: hoverChanged, ui: uiChanged, mode: false });
|
|
756
472
|
});
|
|
757
473
|
|
|
474
|
+
// Register post-render hook for pseudo-state preview
|
|
475
|
+
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
476
|
+
|
|
758
477
|
// Now that renderers and update are registered, bootstrap
|
|
759
478
|
registerFunctionCompletions();
|
|
760
479
|
|
|
@@ -847,480 +566,6 @@ if (_openParam) {
|
|
|
847
566
|
render();
|
|
848
567
|
}
|
|
849
568
|
|
|
850
|
-
// ─── Media helpers ────────────────────────────────────────────────────────────
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* After a runtime render, apply active media overrides as inline styles so they beat the base
|
|
854
|
-
* inline styles the runtime already set. The runtime uses @media CSS rules for overrides, but those
|
|
855
|
-
* can never beat inline base styles.
|
|
856
|
-
*
|
|
857
|
-
* @param {Element} canvasEl
|
|
858
|
-
* @param {Set<string>} activeBreakpoints
|
|
859
|
-
*/
|
|
860
|
-
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
861
|
-
if (!activeBreakpoints.size) return;
|
|
862
|
-
const docMedia = getEffectiveMedia(S.document.$media || {});
|
|
863
|
-
const validBreakpoints = new Set();
|
|
864
|
-
for (const name of activeBreakpoints) {
|
|
865
|
-
if (docMedia[name]) validBreakpoints.add(name);
|
|
866
|
-
}
|
|
867
|
-
const overrides = collectMediaOverrides(document.styleSheets, validBreakpoints);
|
|
868
|
-
applyOverridesToCanvas(canvasEl, overrides);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// ─── Canvas ───────────────────────────────────────────────────────────────────
|
|
872
|
-
|
|
873
|
-
function renderCanvas() {
|
|
874
|
-
// Advance render generation so stale async renders from the previous cycle bail out
|
|
875
|
-
++view.renderGeneration;
|
|
876
|
-
|
|
877
|
-
// Always clear Lit's internal state so it builds fresh DOM. Stale async
|
|
878
|
-
// renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
|
|
879
|
-
// markers (Comment nodes inside panzoom-wrap) in ways the root-only
|
|
880
|
-
// ensureLitState check cannot detect.
|
|
881
|
-
// @ts-ignore
|
|
882
|
-
if (canvasWrap["_$litPart$"]) {
|
|
883
|
-
canvasWrap.textContent = "";
|
|
884
|
-
// @ts-ignore
|
|
885
|
-
delete canvasWrap["_$litPart$"];
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Function editor mode: editing a function body in Monaco (JS)
|
|
889
|
-
if (S.ui.editingFunction) {
|
|
890
|
-
renderFunctionEditor();
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Dispose function editor if switching away
|
|
895
|
-
if (view.functionEditor) {
|
|
896
|
-
view.functionEditor.dispose();
|
|
897
|
-
view.functionEditor = null;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Source mode: update existing Monaco editor without recreating
|
|
901
|
-
if (canvasMode === "source" && view.monacoEditor) {
|
|
902
|
-
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
903
|
-
const currentVal = view.monacoEditor.getValue();
|
|
904
|
-
if (currentVal !== jsonStr) {
|
|
905
|
-
// Prevent triggering the onChange handler for this programmatic update
|
|
906
|
-
view.monacoEditor._ignoreNextChange = true;
|
|
907
|
-
view.monacoEditor.setValue(jsonStr);
|
|
908
|
-
}
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Detect whether this is a mode transition or a content-only re-render
|
|
913
|
-
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
914
|
-
view.prevCanvasMode = canvasMode;
|
|
915
|
-
|
|
916
|
-
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
917
|
-
// content render, so always clean them up.
|
|
918
|
-
for (const fn of view.canvasDndCleanups) fn();
|
|
919
|
-
view.canvasDndCleanups = [];
|
|
920
|
-
|
|
921
|
-
// Panel event handlers (click, dblclick, etc.) capture closures over panel references.
|
|
922
|
-
// Always re-register to keep closures fresh across document switches.
|
|
923
|
-
for (const fn of view.canvasEventCleanups) fn();
|
|
924
|
-
view.canvasEventCleanups = [];
|
|
925
|
-
|
|
926
|
-
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
927
|
-
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
928
|
-
canvasPanels.length = 0;
|
|
929
|
-
|
|
930
|
-
if (modeChanged) {
|
|
931
|
-
// Full teardown on mode transitions — new panel structure needed
|
|
932
|
-
if (view.centerObserver) {
|
|
933
|
-
view.centerObserver.disconnect();
|
|
934
|
-
view.centerObserver = null;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Dispose Monaco editor if switching away from source mode
|
|
938
|
-
if (view.monacoEditor) {
|
|
939
|
-
view.monacoEditor.dispose();
|
|
940
|
-
view.monacoEditor = null;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
litRender(nothing, canvasWrap);
|
|
944
|
-
view.panzoomWrap = null;
|
|
945
|
-
// Reset inline style overrides from other modes
|
|
946
|
-
canvasWrap.style.padding = "";
|
|
947
|
-
canvasWrap.style.alignItems = "";
|
|
948
|
-
canvasWrap.style.display = "";
|
|
949
|
-
canvasWrap.style.overflow = "";
|
|
950
|
-
canvasWrap.style.overflow = "";
|
|
951
|
-
|
|
952
|
-
// Clear zoom indicator (only re-rendered by design/preview/stylebook)
|
|
953
|
-
resetZoomIndicator();
|
|
954
|
-
|
|
955
|
-
// Dismiss open popovers/toolbars that are no longer relevant
|
|
956
|
-
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
957
|
-
dismissLinkPopover();
|
|
958
|
-
dismissContextMenu();
|
|
959
|
-
sharedDismissSlashMenu();
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Manage mode: project-level file browser table
|
|
963
|
-
if (canvasMode === "manage") {
|
|
964
|
-
canvasWrap.style.padding = "0";
|
|
965
|
-
canvasWrap.style.overflow = "auto";
|
|
966
|
-
renderBrowse(canvasWrap, {
|
|
967
|
-
openFile: (/** @type {string} */ path) => {
|
|
968
|
-
canvasMode = "edit";
|
|
969
|
-
openFileFromTree(path);
|
|
970
|
-
},
|
|
971
|
-
});
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Settings mode: render element catalog with panzoom surface
|
|
976
|
-
if (canvasMode === "settings") {
|
|
977
|
-
renderStylebookMode({
|
|
978
|
-
canvasPanelTemplate,
|
|
979
|
-
applyTransform,
|
|
980
|
-
observeCenterUntilStable,
|
|
981
|
-
renderZoomIndicator,
|
|
982
|
-
updateActivePanelHeaders,
|
|
983
|
-
overlayBoxDescriptor,
|
|
984
|
-
effectiveZoom,
|
|
985
|
-
});
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Source mode: create Monaco editor instead of canvas
|
|
990
|
-
if (canvasMode === "source") {
|
|
991
|
-
canvasWrap.style.padding = "0";
|
|
992
|
-
canvasWrap.style.display = "block";
|
|
993
|
-
/** @type {HTMLDivElement | null} */
|
|
994
|
-
let editorContainer = null;
|
|
995
|
-
litRender(
|
|
996
|
-
html`<div class="source-wrap">
|
|
997
|
-
<div class="source-toolbar">
|
|
998
|
-
<sp-action-button size="s" @click=${exportFile}>
|
|
999
|
-
<sp-icon-export slot="icon"></sp-icon-export>
|
|
1000
|
-
Export
|
|
1001
|
-
</sp-action-button>
|
|
1002
|
-
</div>
|
|
1003
|
-
<div
|
|
1004
|
-
class="source-editor"
|
|
1005
|
-
${ref((el) => {
|
|
1006
|
-
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
1007
|
-
})}
|
|
1008
|
-
></div>
|
|
1009
|
-
</div>`,
|
|
1010
|
-
canvasWrap,
|
|
1011
|
-
);
|
|
1012
|
-
|
|
1013
|
-
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
1014
|
-
view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1015
|
-
value: jsonStr,
|
|
1016
|
-
language: "json",
|
|
1017
|
-
theme: "vs-dark",
|
|
1018
|
-
automaticLayout: true,
|
|
1019
|
-
minimap: { enabled: false },
|
|
1020
|
-
fontSize: 12,
|
|
1021
|
-
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
1022
|
-
lineNumbers: "on",
|
|
1023
|
-
scrollBeyondLastLine: false,
|
|
1024
|
-
wordWrap: "on",
|
|
1025
|
-
tabSize: 2,
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
// Debounced sync back to state
|
|
1029
|
-
/** @type {any} */
|
|
1030
|
-
let debounce;
|
|
1031
|
-
view.monacoEditor.onDidChangeModelContent(() => {
|
|
1032
|
-
if (view.monacoEditor._ignoreNextChange) {
|
|
1033
|
-
view.monacoEditor._ignoreNextChange = false;
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
clearTimeout(debounce);
|
|
1037
|
-
debounce = setTimeout(() => {
|
|
1038
|
-
try {
|
|
1039
|
-
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
1040
|
-
update({ ...S, document: parsed, dirty: true });
|
|
1041
|
-
} catch {
|
|
1042
|
-
// Invalid JSON — don't update state
|
|
1043
|
-
}
|
|
1044
|
-
}, 600);
|
|
1045
|
-
});
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
1050
|
-
if (canvasMode === "edit") {
|
|
1051
|
-
if (modeChanged) {
|
|
1052
|
-
canvasWrap.style.padding = "0";
|
|
1053
|
-
canvasWrap.style.overflow = "hidden";
|
|
1054
|
-
|
|
1055
|
-
// Remove zoom indicator left over from design/preview mode
|
|
1056
|
-
resetZoomIndicator();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
1060
|
-
const editTpl = html`
|
|
1061
|
-
<div class="content-edit-canvas">
|
|
1062
|
-
<div class="content-edit-column">${panelTpl}</div>
|
|
1063
|
-
</div>
|
|
1064
|
-
`;
|
|
1065
|
-
litRender(editTpl, canvasWrap);
|
|
1066
|
-
canvasPanels.push(panel);
|
|
1067
|
-
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// Normal canvas mode (design / preview) — set up panzoom surface
|
|
1072
|
-
if (modeChanged) {
|
|
1073
|
-
canvasWrap.style.padding = "0";
|
|
1074
|
-
canvasWrap.style.overflow = "hidden";
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
const {
|
|
1078
|
-
sizeBreakpoints,
|
|
1079
|
-
featureQueries: _featureQueries,
|
|
1080
|
-
baseWidth,
|
|
1081
|
-
} = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
1082
|
-
const hasMedia = sizeBreakpoints.length > 0;
|
|
1083
|
-
const featureToggles = S.ui.featureToggles;
|
|
1084
|
-
|
|
1085
|
-
// Create panzoom wrapper (the element that gets transformed)
|
|
1086
|
-
if (!hasMedia) {
|
|
1087
|
-
// Single panel — use baseWidth if a custom one is defined, otherwise full-width
|
|
1088
|
-
const effectiveMedia = getEffectiveMedia(S.document.$media);
|
|
1089
|
-
const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
|
|
1090
|
-
const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
|
|
1091
|
-
const { tpl: panelTpl, panel } = canvasPanelTemplate(
|
|
1092
|
-
hasBaseWidth ? "base" : null,
|
|
1093
|
-
label,
|
|
1094
|
-
!hasBaseWidth,
|
|
1095
|
-
hasBaseWidth ? baseWidth : undefined,
|
|
1096
|
-
);
|
|
1097
|
-
litRender(
|
|
1098
|
-
html`
|
|
1099
|
-
<div
|
|
1100
|
-
class="panzoom-wrap"
|
|
1101
|
-
style="transform-origin:0 0"
|
|
1102
|
-
${ref((el) => {
|
|
1103
|
-
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1104
|
-
})}
|
|
1105
|
-
>
|
|
1106
|
-
${panelTpl}
|
|
1107
|
-
</div>
|
|
1108
|
-
`,
|
|
1109
|
-
canvasWrap,
|
|
1110
|
-
);
|
|
1111
|
-
canvasPanels.push(panel);
|
|
1112
|
-
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
1113
|
-
applyTransform();
|
|
1114
|
-
if (modeChanged) {
|
|
1115
|
-
observeCenterUntilStable();
|
|
1116
|
-
}
|
|
1117
|
-
renderZoomIndicator();
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Build all panels: base first, then breakpoints in declared order (ascending for min-width,
|
|
1122
|
-
// descending for max-width — matching the direction of the design's media queries).
|
|
1123
|
-
const allPanelDefs = [
|
|
1124
|
-
{
|
|
1125
|
-
name: "base",
|
|
1126
|
-
displayName: mediaDisplayName("--"),
|
|
1127
|
-
width: baseWidth,
|
|
1128
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
|
|
1129
|
-
},
|
|
1130
|
-
];
|
|
1131
|
-
for (const bp of sizeBreakpoints) {
|
|
1132
|
-
allPanelDefs.push({
|
|
1133
|
-
name: bp.name,
|
|
1134
|
-
displayName: mediaDisplayName(bp.name),
|
|
1135
|
-
width: bp.width,
|
|
1136
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
1141
|
-
const panelEntries = allPanelDefs.map((def) => {
|
|
1142
|
-
const label = `${def.displayName} (${def.width}px)`;
|
|
1143
|
-
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
1144
|
-
return { tpl, panel, activeSet: def.activeSet };
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
litRender(
|
|
1148
|
-
html`
|
|
1149
|
-
<div
|
|
1150
|
-
class="panzoom-wrap"
|
|
1151
|
-
style="transform-origin:0 0"
|
|
1152
|
-
${ref((el) => {
|
|
1153
|
-
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1154
|
-
})}
|
|
1155
|
-
>
|
|
1156
|
-
${panelEntries.map((e) => e.tpl)}
|
|
1157
|
-
</div>
|
|
1158
|
-
`,
|
|
1159
|
-
canvasWrap,
|
|
1160
|
-
);
|
|
1161
|
-
|
|
1162
|
-
for (const { panel, activeSet } of panelEntries) {
|
|
1163
|
-
canvasPanels.push(panel);
|
|
1164
|
-
renderCanvasIntoPanel(panel, activeSet, featureToggles);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// Highlight active panel header
|
|
1168
|
-
updateActivePanelHeaders();
|
|
1169
|
-
|
|
1170
|
-
// Apply current zoom + pan transform
|
|
1171
|
-
applyTransform();
|
|
1172
|
-
if (modeChanged) {
|
|
1173
|
-
observeCenterUntilStable();
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Floating zoom indicator
|
|
1177
|
-
renderZoomIndicator();
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
/**
|
|
1181
|
-
* Render document into a single canvas panel. Tries runtime rendering first, falls back to
|
|
1182
|
-
* structural preview.
|
|
1183
|
-
*
|
|
1184
|
-
* @param {any} panel
|
|
1185
|
-
* @param {any} activeBreakpoints
|
|
1186
|
-
* @param {any} featureToggles
|
|
1187
|
-
*/
|
|
1188
|
-
function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
1189
|
-
const gen = view.renderGeneration;
|
|
1190
|
-
renderCanvasLive(gen, S.document, panel.canvas).then((scope) => {
|
|
1191
|
-
// Skip post-render setup if a newer render has started
|
|
1192
|
-
if (gen !== view.renderGeneration) return;
|
|
1193
|
-
if (scope) {
|
|
1194
|
-
view.liveScope = scope;
|
|
1195
|
-
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
1196
|
-
statusMessage("Runtime render OK", 1500);
|
|
1197
|
-
} else {
|
|
1198
|
-
// Fallback to structural preview
|
|
1199
|
-
renderCanvasNode(S.document, [], panel.canvas, activeBreakpoints, featureToggles);
|
|
1200
|
-
}
|
|
1201
|
-
registerPanelDnD(panel);
|
|
1202
|
-
registerPanelEvents(panel);
|
|
1203
|
-
renderOverlays();
|
|
1204
|
-
updateForcedPseudoPreview();
|
|
1205
|
-
|
|
1206
|
-
// Process pending inline edit now that the canvas is populated
|
|
1207
|
-
if (view.pendingInlineEdit) {
|
|
1208
|
-
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
1209
|
-
view.pendingInlineEdit = null;
|
|
1210
|
-
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
1211
|
-
if (targetPanel) {
|
|
1212
|
-
const el = findCanvasElement(path, targetPanel.canvas);
|
|
1213
|
-
if (el) enterComponentInlineEdit(el, path);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// ─── Overlay system ───────────────────────────────────────────────────────────
|
|
1220
|
-
|
|
1221
|
-
function renderOverlays() {
|
|
1222
|
-
overlaysPanel.render();
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
/**
|
|
1226
|
-
* Build an overlay box descriptor (no DOM creation).
|
|
1227
|
-
*
|
|
1228
|
-
* @param {any} el
|
|
1229
|
-
* @param {any} type
|
|
1230
|
-
* @param {any} panel
|
|
1231
|
-
*/
|
|
1232
|
-
function overlayBoxDescriptor(el, type, panel) {
|
|
1233
|
-
const vpRect = panel.viewport.getBoundingClientRect();
|
|
1234
|
-
const elRect = el.getBoundingClientRect();
|
|
1235
|
-
const scale = effectiveZoom();
|
|
1236
|
-
return {
|
|
1237
|
-
cls: `overlay-box overlay-${type}`,
|
|
1238
|
-
top: `${(elRect.top - vpRect.top + panel.viewport.scrollTop) / scale}px`,
|
|
1239
|
-
left: `${(elRect.left - vpRect.left + panel.viewport.scrollLeft) / scale}px`,
|
|
1240
|
-
width: `${elRect.width / scale}px`,
|
|
1241
|
-
height: `${elRect.height / scale}px`,
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
function getActivePanel() {
|
|
1246
|
-
if (canvasPanels.length === 0) return null;
|
|
1247
|
-
if (canvasPanels.length === 1) return canvasPanels[0];
|
|
1248
|
-
for (const p of canvasPanels) {
|
|
1249
|
-
if (S.ui.activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
|
|
1250
|
-
if (p.mediaName === S.ui.activeMedia) return p;
|
|
1251
|
-
}
|
|
1252
|
-
return canvasPanels[0];
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
/**
|
|
1256
|
-
* Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
|
|
1257
|
-
* ancestor. Returns the original path if already non-inline.
|
|
1258
|
-
*
|
|
1259
|
-
* @param {any} doc
|
|
1260
|
-
* @param {any} path
|
|
1261
|
-
*/
|
|
1262
|
-
function bubbleInlinePath(doc, path) {
|
|
1263
|
-
let currentPath = path;
|
|
1264
|
-
while (currentPath.length >= 2) {
|
|
1265
|
-
const node = getNodeAtPath(doc, currentPath);
|
|
1266
|
-
const pPath = parentElementPath(currentPath);
|
|
1267
|
-
const parentNode = pPath ? getNodeAtPath(doc, pPath) : null;
|
|
1268
|
-
if (!node || !parentNode) break;
|
|
1269
|
-
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
1270
|
-
const parentTag = (parentNode.tagName ?? "div").toLowerCase();
|
|
1271
|
-
if (!isInlineInContext(childTag, parentTag)) break;
|
|
1272
|
-
currentPath = pPath;
|
|
1273
|
-
}
|
|
1274
|
-
return currentPath;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
/** Effective zoom scale — always 1 in edit (content) mode, S.ui.zoom otherwise. */
|
|
1278
|
-
function effectiveZoom() {
|
|
1279
|
-
return canvasMode === "edit" ? 1 : S.ui.zoom;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
/**
|
|
1283
|
-
* @param {any} path
|
|
1284
|
-
* @param {any} canvasEl
|
|
1285
|
-
*/
|
|
1286
|
-
function findCanvasElement(path, canvasEl) {
|
|
1287
|
-
let el = canvasEl.firstElementChild;
|
|
1288
|
-
if (!el) return null;
|
|
1289
|
-
if (path.length === 0) return el;
|
|
1290
|
-
|
|
1291
|
-
for (let i = 0; i < path.length; i += 2) {
|
|
1292
|
-
if (path[i] !== "children" && path[i] !== "cases") return null;
|
|
1293
|
-
const idx = path[i + 1];
|
|
1294
|
-
if (idx === undefined) {
|
|
1295
|
-
// Odd-length path like ['children', 2, 'children'] — $map container
|
|
1296
|
-
// The wrapper div is children[0] of the current element
|
|
1297
|
-
el = el.children[0];
|
|
1298
|
-
} else if (idx === "map") {
|
|
1299
|
-
// $map template: wrapper is children[0], template is wrapper.children[0]
|
|
1300
|
-
el = el.children[0]?.children[0];
|
|
1301
|
-
} else {
|
|
1302
|
-
el = el.children[idx];
|
|
1303
|
-
}
|
|
1304
|
-
if (!el) break;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// Verify the result: if DOM traversal landed on the wrong element
|
|
1308
|
-
// (e.g. a custom element template child instead of the intended node),
|
|
1309
|
-
// fall back to scanning elToPath.
|
|
1310
|
-
if (el) {
|
|
1311
|
-
const elPath = elToPath.get(el);
|
|
1312
|
-
if (elPath && pathsEqual(elPath, path)) return el;
|
|
1313
|
-
// el has no path or wrong path — it's a template element, not the target
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// Fall back: scan all descendants for an element with matching elToPath
|
|
1317
|
-
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
1318
|
-
const p = elToPath.get(candidate);
|
|
1319
|
-
if (p && pathsEqual(p, path)) return candidate;
|
|
1320
|
-
}
|
|
1321
|
-
return null;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
569
|
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
1325
570
|
|
|
1326
571
|
function renderLeftPanel() {
|
|
@@ -1359,6 +604,7 @@ function fileOpsCtx() {
|
|
|
1359
604
|
S,
|
|
1360
605
|
commit: (/** @type {any} */ ns) => {
|
|
1361
606
|
S = ns;
|
|
607
|
+
({ doc, session } = fromFlat(S));
|
|
1362
608
|
render();
|
|
1363
609
|
},
|
|
1364
610
|
renderToolbar,
|
|
@@ -1370,6 +616,7 @@ function openFile() {
|
|
|
1370
616
|
async function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
1371
617
|
const ns = await _loadMarkdown(source, fileHandle);
|
|
1372
618
|
S = ns;
|
|
619
|
+
({ doc, session } = fromFlat(S));
|
|
1373
620
|
}
|
|
1374
621
|
function saveFile() {
|
|
1375
622
|
return _saveFile(fileOpsCtx());
|
|
@@ -1388,6 +635,7 @@ function openProject() {
|
|
|
1388
635
|
S,
|
|
1389
636
|
commit: (/** @type {any} */ ns) => {
|
|
1390
637
|
S = ns;
|
|
638
|
+
({ doc, session } = fromFlat(S));
|
|
1391
639
|
},
|
|
1392
640
|
renderActivityBar: () => renderActivityBar(S),
|
|
1393
641
|
renderLeftPanel,
|
|
@@ -1407,6 +655,7 @@ function openFileFromTree(/** @type {any} */ path) {
|
|
|
1407
655
|
},
|
|
1408
656
|
commit: (/** @type {any} */ ns) => {
|
|
1409
657
|
S = ns;
|
|
658
|
+
({ doc, session } = fromFlat(S));
|
|
1410
659
|
},
|
|
1411
660
|
render,
|
|
1412
661
|
loadMarkdown,
|
|
@@ -1420,6 +669,7 @@ initShortcuts(() => ({
|
|
|
1420
669
|
S,
|
|
1421
670
|
setS: (ns) => {
|
|
1422
671
|
S = ns;
|
|
672
|
+
({ doc, session } = fromFlat(S));
|
|
1423
673
|
},
|
|
1424
674
|
canvasMode,
|
|
1425
675
|
panX: view.panX,
|