@jxsuite/studio 0.8.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 +156878 -156742
- package/dist/studio.js.map +156 -145
- 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 +277 -0
- package/src/editor/component-inline-edit.js +317 -0
- package/src/editor/content-inline-edit.js +213 -0
- package/src/panels/block-action-bar.js +8 -4
- package/src/panels/canvas-dnd.js +154 -0
- package/src/panels/left-panel.js +1 -2
- package/src/panels/overlays.js +9 -23
- package/src/panels/panel-events.js +260 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/pseudo-preview.js +57 -0
- 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 +122 -2349
- package/src/utils/edit-display.js +197 -0
- package/src/view.js +0 -2
package/src/studio.js
CHANGED
|
@@ -8,24 +8,15 @@
|
|
|
8
8
|
import {
|
|
9
9
|
createState,
|
|
10
10
|
selectNode,
|
|
11
|
-
hoverNode,
|
|
12
|
-
insertNode,
|
|
13
|
-
removeNode,
|
|
14
11
|
updateProperty,
|
|
15
12
|
updateDef,
|
|
16
13
|
pushDocument,
|
|
17
14
|
popDocument,
|
|
18
15
|
getNodeAtPath,
|
|
19
16
|
pathsEqual,
|
|
20
|
-
parentElementPath,
|
|
21
|
-
childIndex,
|
|
22
|
-
isAncestor,
|
|
23
17
|
canvasWrap,
|
|
24
18
|
toolbarEl,
|
|
25
|
-
elToPath,
|
|
26
19
|
canvasPanels,
|
|
27
|
-
VOID_ELEMENTS,
|
|
28
|
-
stripEventHandlers,
|
|
29
20
|
registerRenderer,
|
|
30
21
|
render,
|
|
31
22
|
update,
|
|
@@ -48,21 +39,21 @@ import {
|
|
|
48
39
|
|
|
49
40
|
import { view } from "./view.js";
|
|
50
41
|
|
|
51
|
-
import {
|
|
52
|
-
|
|
42
|
+
import { isEditing, isEditableBlock } from "./editor/inline-edit.js";
|
|
53
43
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isEditableBlock,
|
|
59
|
-
isInlineInContext,
|
|
60
|
-
} from "./editor/inline-edit.js";
|
|
44
|
+
enterComponentInlineEdit,
|
|
45
|
+
initComponentInlineEdit,
|
|
46
|
+
} from "./editor/component-inline-edit.js";
|
|
47
|
+
import { enterInlineEdit } from "./editor/content-inline-edit.js";
|
|
61
48
|
import {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
initCanvasUtils,
|
|
50
|
+
applyTransform,
|
|
51
|
+
positionZoomIndicator,
|
|
52
|
+
updateActivePanelHeaders,
|
|
53
|
+
} from "./canvas/canvas-utils.js";
|
|
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";
|
|
66
57
|
import {
|
|
67
58
|
renderStatusbar,
|
|
68
59
|
statusMessage,
|
|
@@ -88,42 +79,13 @@ import { renderHeadTemplate } from "./panels/head-panel.js";
|
|
|
88
79
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
|
|
89
80
|
|
|
90
81
|
import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
|
|
91
|
-
import {
|
|
92
|
-
parseMediaEntries,
|
|
93
|
-
activeBreakpointsForWidth,
|
|
94
|
-
applyCanvasStyle,
|
|
95
|
-
collectMediaOverrides,
|
|
96
|
-
applyOverridesToCanvas,
|
|
97
|
-
} from "./utils/canvas-media.js";
|
|
82
|
+
import { parseMediaEntries } from "./utils/canvas-media.js";
|
|
98
83
|
import { createDevServerPlatform } from "./platforms/devserver.js";
|
|
99
84
|
import { codeService } from "./services/code-services.js";
|
|
100
|
-
import {
|
|
101
|
-
|
|
102
|
-
getEffectiveImports,
|
|
103
|
-
getEffectiveElements,
|
|
104
|
-
getEffectiveHead,
|
|
105
|
-
} from "./site-context.js";
|
|
106
|
-
import {
|
|
107
|
-
defCategory,
|
|
108
|
-
defBadgeLabel,
|
|
109
|
-
resolveDefaultForCanvas,
|
|
110
|
-
renderSignalsTemplate,
|
|
111
|
-
} from "./panels/signals-panel.js";
|
|
112
|
-
import {
|
|
113
|
-
componentRegistry,
|
|
114
|
-
loadComponentRegistry,
|
|
115
|
-
computeRelativePath,
|
|
116
|
-
} from "./files/components.js";
|
|
85
|
+
import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel.js";
|
|
86
|
+
import { loadComponentRegistry } from "./files/components.js";
|
|
117
87
|
|
|
118
|
-
import {
|
|
119
|
-
dropTargetForElements,
|
|
120
|
-
monitorForElements,
|
|
121
|
-
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
122
|
-
|
|
123
|
-
import { html, render as litRender, nothing } from "lit-html";
|
|
124
|
-
import { ref } from "lit-html/directives/ref.js";
|
|
125
|
-
import { styleMap } from "lit-html/directives/style-map.js";
|
|
126
|
-
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
88
|
+
import { html, render as litRender } from "lit-html";
|
|
127
89
|
|
|
128
90
|
import webdata from "../data/webdata.json";
|
|
129
91
|
import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
@@ -134,31 +96,20 @@ import { renderGitPanel } from "./panels/git-panel.js";
|
|
|
134
96
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
135
97
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
136
98
|
import "./ui/panel-resize.js";
|
|
137
|
-
import { showContextMenu, dismissContextMenu } from "./editor/context-menu.js";
|
|
138
99
|
import { initShortcuts } from "./editor/shortcuts.js";
|
|
139
|
-
import * as insertionHelper from "./editor/insertion-helper.js";
|
|
140
100
|
import { renderActivityBar } from "./panels/activity-bar.js";
|
|
141
|
-
import { renderBrowse } from "./browse/browse.js";
|
|
142
101
|
import * as toolbarPanel from "./panels/toolbar.js";
|
|
143
102
|
import * as overlaysPanel from "./panels/overlays.js";
|
|
144
103
|
import * as rightPanelMod from "./panels/right-panel.js";
|
|
145
104
|
import * as leftPanelMod from "./panels/left-panel.js";
|
|
146
|
-
import {
|
|
147
|
-
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
applyDropInstruction,
|
|
152
|
-
} from "./panels/dnd.js";
|
|
153
|
-
import { mediaDisplayName, defaultDef } from "./panels/shared.js";
|
|
154
|
-
import { renderFunctionEditor, registerFunctionCompletions } from "./panels/editors.js";
|
|
155
|
-
import {
|
|
156
|
-
renderBlockActionBar,
|
|
157
|
-
dismissLinkPopover,
|
|
158
|
-
initBlockActionBar,
|
|
159
|
-
} from "./panels/block-action-bar.js";
|
|
105
|
+
import { renderStylebookOverlays } from "./panels/stylebook-panel.js";
|
|
106
|
+
import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd.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";
|
|
160
110
|
import { initCssData } from "./panels/style-utils.js";
|
|
161
|
-
import
|
|
111
|
+
import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
|
|
112
|
+
import { initPanelEvents } from "./panels/panel-events.js";
|
|
162
113
|
|
|
163
114
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
164
115
|
// These mutable variables are local to studio.js for now. As sections are extracted
|
|
@@ -189,11 +140,12 @@ async function navigateToComponent(componentPath) {
|
|
|
189
140
|
const platform = getPlatform();
|
|
190
141
|
const content = await platform.readFile(componentPath);
|
|
191
142
|
if (!content) return;
|
|
192
|
-
const
|
|
193
|
-
S = pushDocument(S,
|
|
143
|
+
const parsed = JSON.parse(content);
|
|
144
|
+
S = pushDocument(S, parsed, componentPath);
|
|
194
145
|
S.dirty = false;
|
|
146
|
+
({ doc, session } = fromFlat(S));
|
|
195
147
|
render();
|
|
196
|
-
statusMessage(`Editing component: ${
|
|
148
|
+
statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
|
|
197
149
|
} catch (/** @type {any} */ e) {
|
|
198
150
|
const err = /** @type {any} */ (e);
|
|
199
151
|
statusMessage(`Error: ${err.message}`);
|
|
@@ -212,6 +164,7 @@ async function navigateBack() {
|
|
|
212
164
|
}
|
|
213
165
|
}
|
|
214
166
|
S = popDocument(S);
|
|
167
|
+
({ doc, session } = fromFlat(S));
|
|
215
168
|
render();
|
|
216
169
|
statusMessage("Returned to parent document");
|
|
217
170
|
}
|
|
@@ -242,449 +195,6 @@ async function closeFunctionEditor() {
|
|
|
242
195
|
updateUi("editingFunction", null);
|
|
243
196
|
}
|
|
244
197
|
|
|
245
|
-
/**
|
|
246
|
-
* Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
|
|
247
|
-
* ❯ so the runtime renders it as literal text.
|
|
248
|
-
*
|
|
249
|
-
* @param {any} str
|
|
250
|
-
*/
|
|
251
|
-
function templateToEditDisplay(str) {
|
|
252
|
-
return str.replace(/\$\{([^}]+)\}/g, "\u276A $1 \u276B");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Reverse templateToEditDisplay: walk all text nodes in `el` and replace ❪ expr ❫ back to ${expr}
|
|
257
|
-
* so the user edits raw template syntax.
|
|
258
|
-
*
|
|
259
|
-
* @param {any} el
|
|
260
|
-
*/
|
|
261
|
-
function restoreTemplateExpressions(el) {
|
|
262
|
-
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
263
|
-
while (walker.nextNode()) {
|
|
264
|
-
const node = /** @type {any} */ (walker.currentNode);
|
|
265
|
-
if (node.textContent.includes("\u276A")) {
|
|
266
|
-
node.textContent = node.textContent.replace(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Prepare a document for edit-mode rendering. Replaces template strings with readable literal text,
|
|
273
|
-
* $prototype:Array with placeholders, and $ref bindings with display labels. Preserves state so the
|
|
274
|
-
* runtime can still initialise scope.
|
|
275
|
-
*
|
|
276
|
-
* @param {any} node
|
|
277
|
-
* @returns {any}
|
|
278
|
-
*/
|
|
279
|
-
function prepareForEditMode(node) {
|
|
280
|
-
if (!node || typeof node !== "object") return node;
|
|
281
|
-
if (Array.isArray(node)) return node.map(prepareForEditMode);
|
|
282
|
-
|
|
283
|
-
/** @type {Record<string, any>} */
|
|
284
|
-
const out = {};
|
|
285
|
-
for (const [k, v] of Object.entries(node)) {
|
|
286
|
-
if (k === "state" || k === "$media" || k === "$props" || k === "$elements") {
|
|
287
|
-
out[k] = v; // preserve as-is for runtime resolution
|
|
288
|
-
} else if (k === "children") {
|
|
289
|
-
if (Array.isArray(v)) {
|
|
290
|
-
out.children = v.map(prepareForEditMode);
|
|
291
|
-
} else if (v && typeof v === "object" && v.$prototype === "Array") {
|
|
292
|
-
// Wrap the map template in a visual repeater perimeter
|
|
293
|
-
const template = v.map;
|
|
294
|
-
if (template && typeof template === "object") {
|
|
295
|
-
out.children = [
|
|
296
|
-
{
|
|
297
|
-
tagName: "div",
|
|
298
|
-
className: "repeater-perimeter",
|
|
299
|
-
state: {
|
|
300
|
-
$map: { item: {}, index: 0 },
|
|
301
|
-
"$map/item": {},
|
|
302
|
-
"$map/index": 0,
|
|
303
|
-
},
|
|
304
|
-
children: [prepareForEditMode(template)],
|
|
305
|
-
},
|
|
306
|
-
];
|
|
307
|
-
} else {
|
|
308
|
-
out.children = [];
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
out.children = prepareForEditMode(v);
|
|
312
|
-
}
|
|
313
|
-
} else if (k === "cases" && node.$switch && v && typeof v === "object") {
|
|
314
|
-
// Replace $switch cases with a placeholder showing the first case or a label
|
|
315
|
-
const caseKeys = Object.keys(v);
|
|
316
|
-
if (caseKeys.length > 0) {
|
|
317
|
-
const firstCase = v[caseKeys[0]];
|
|
318
|
-
if (firstCase && typeof firstCase === "object" && !firstCase.$ref) {
|
|
319
|
-
out.children = [prepareForEditMode(firstCase)];
|
|
320
|
-
} else {
|
|
321
|
-
out.children = [
|
|
322
|
-
{
|
|
323
|
-
tagName: "div",
|
|
324
|
-
textContent: `[$switch: ${caseKeys.join(" | ")}]`,
|
|
325
|
-
style: {
|
|
326
|
-
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
|
327
|
-
fontSize: "11px",
|
|
328
|
-
padding: "6px 10px",
|
|
329
|
-
background: "color-mix(in srgb, var(--danger) 8%, transparent)",
|
|
330
|
-
border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
|
|
331
|
-
borderRadius: "4px",
|
|
332
|
-
color: "var(--danger)",
|
|
333
|
-
fontStyle: "italic",
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
];
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
} else if (k === "style") {
|
|
340
|
-
// Replace template strings in style values with empty strings
|
|
341
|
-
if (v && typeof v === "object") {
|
|
342
|
-
/** @type {Record<string, any>} */
|
|
343
|
-
const s = {};
|
|
344
|
-
for (const [sk, sv] of Object.entries(v)) {
|
|
345
|
-
s[sk] = typeof sv === "string" && sv.includes("${") ? "" : sv;
|
|
346
|
-
}
|
|
347
|
-
out.style = s;
|
|
348
|
-
} else {
|
|
349
|
-
out.style = v;
|
|
350
|
-
}
|
|
351
|
-
} else if (typeof v === "string" && v.includes("${")) {
|
|
352
|
-
// Template string in a display property → show raw expression
|
|
353
|
-
out[k] = templateToEditDisplay(v);
|
|
354
|
-
} else if (v && typeof v === "object" && v.$ref) {
|
|
355
|
-
// $ref binding → show ref path as literal text
|
|
356
|
-
const ref = v.$ref;
|
|
357
|
-
const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
|
|
358
|
-
out[k] = `{${label}}`;
|
|
359
|
-
} else {
|
|
360
|
-
out[k] = prepareForEditMode(v);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Mark empty elements with placeholder classes for design-mode visibility
|
|
365
|
-
if (out.tagName && !out.textContent && !out.innerHTML) {
|
|
366
|
-
const hasChildren = Array.isArray(out.children) && out.children.length > 0;
|
|
367
|
-
if (!hasChildren) {
|
|
368
|
-
const tag = out.tagName;
|
|
369
|
-
const textTags = new Set([
|
|
370
|
-
"p",
|
|
371
|
-
"h1",
|
|
372
|
-
"h2",
|
|
373
|
-
"h3",
|
|
374
|
-
"h4",
|
|
375
|
-
"h5",
|
|
376
|
-
"h6",
|
|
377
|
-
"blockquote",
|
|
378
|
-
"li",
|
|
379
|
-
"dt",
|
|
380
|
-
"dd",
|
|
381
|
-
"th",
|
|
382
|
-
"td",
|
|
383
|
-
"span",
|
|
384
|
-
"strong",
|
|
385
|
-
"em",
|
|
386
|
-
"small",
|
|
387
|
-
"mark",
|
|
388
|
-
"code",
|
|
389
|
-
"abbr",
|
|
390
|
-
"q",
|
|
391
|
-
"sub",
|
|
392
|
-
"sup",
|
|
393
|
-
"time",
|
|
394
|
-
"a",
|
|
395
|
-
"button",
|
|
396
|
-
"label",
|
|
397
|
-
"legend",
|
|
398
|
-
"caption",
|
|
399
|
-
"summary",
|
|
400
|
-
"pre",
|
|
401
|
-
"option",
|
|
402
|
-
]);
|
|
403
|
-
const containerTags = new Set([
|
|
404
|
-
"div",
|
|
405
|
-
"section",
|
|
406
|
-
"article",
|
|
407
|
-
"aside",
|
|
408
|
-
"header",
|
|
409
|
-
"footer",
|
|
410
|
-
"main",
|
|
411
|
-
"nav",
|
|
412
|
-
"figure",
|
|
413
|
-
"figcaption",
|
|
414
|
-
"details",
|
|
415
|
-
"fieldset",
|
|
416
|
-
"form",
|
|
417
|
-
"ul",
|
|
418
|
-
"ol",
|
|
419
|
-
"dl",
|
|
420
|
-
"table",
|
|
421
|
-
]);
|
|
422
|
-
if (textTags.has(tag)) {
|
|
423
|
-
out.className = out.className
|
|
424
|
-
? out.className + " empty-text-placeholder"
|
|
425
|
-
: "empty-text-placeholder";
|
|
426
|
-
} else if (containerTags.has(tag)) {
|
|
427
|
-
out.className = out.className
|
|
428
|
-
? out.className + " empty-container-placeholder"
|
|
429
|
-
: "empty-container-placeholder";
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return out;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Render a Jx document into a canvas element using the real runtime. Populates elToPath for each
|
|
439
|
-
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
440
|
-
* failure.
|
|
441
|
-
*
|
|
442
|
-
* @param {number} gen - Render generation for staleness detection
|
|
443
|
-
* @param {any} doc
|
|
444
|
-
* @param {any} canvasEl
|
|
445
|
-
*/
|
|
446
|
-
async function renderCanvasLive(gen, doc, canvasEl) {
|
|
447
|
-
canvasEl.innerHTML = "";
|
|
448
|
-
|
|
449
|
-
// Apply content mode typography styling
|
|
450
|
-
if (S.mode === "content") {
|
|
451
|
-
canvasEl.setAttribute("data-content-mode", "");
|
|
452
|
-
} else {
|
|
453
|
-
canvasEl.removeAttribute("data-content-mode");
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const renderDoc =
|
|
457
|
-
canvasMode === "preview" ? structuredClone(doc) : prepareForEditMode(stripEventHandlers(doc));
|
|
458
|
-
|
|
459
|
-
// In edit mode, collect paths where $map templates were inlined as children[0]
|
|
460
|
-
// so we can remap runtime paths (children,0,...) → (children,map,...)
|
|
461
|
-
const mapParentPaths = new Set();
|
|
462
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
463
|
-
(function findMapParents(/** @type {any} */ node, /** @type {any[]} */ path) {
|
|
464
|
-
if (!node || typeof node !== "object") return;
|
|
465
|
-
if (
|
|
466
|
-
node.children &&
|
|
467
|
-
typeof node.children === "object" &&
|
|
468
|
-
node.children.$prototype === "Array"
|
|
469
|
-
) {
|
|
470
|
-
mapParentPaths.add(path.join("/"));
|
|
471
|
-
}
|
|
472
|
-
if (Array.isArray(node.children)) {
|
|
473
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
474
|
-
findMapParents(node.children[i], [...path, "children", i]);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
if (node.$switch && node.cases) {
|
|
478
|
-
for (const [k, v] of Object.entries(node.cases)) {
|
|
479
|
-
findMapParents(v, [...path, "cases", k]);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
})(doc, []);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
const root = projectState?.projectRoot || "";
|
|
487
|
-
const docPrefix = root ? `${root}/` : "";
|
|
488
|
-
const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
|
|
489
|
-
|
|
490
|
-
// Register custom elements so the runtime can render them
|
|
491
|
-
let effectiveElements = getEffectiveElements(renderDoc.$elements);
|
|
492
|
-
|
|
493
|
-
// In content mode (markdown), auto-discover components for directive-based
|
|
494
|
-
// custom elements that have no explicit $elements registration.
|
|
495
|
-
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
496
|
-
const existingRefs = new Set(
|
|
497
|
-
effectiveElements.map((/** @type {any} */ e) => (typeof e === "string" ? e : e?.$ref)),
|
|
498
|
-
);
|
|
499
|
-
/** @param {any} node */
|
|
500
|
-
const collectTags = (node) => {
|
|
501
|
-
/** @type {Set<string>} */
|
|
502
|
-
const tags = new Set();
|
|
503
|
-
if (!node || typeof node !== "object") return tags;
|
|
504
|
-
if (node.tagName) tags.add(node.tagName);
|
|
505
|
-
if (Array.isArray(node.children)) {
|
|
506
|
-
for (const child of node.children) {
|
|
507
|
-
for (const t of collectTags(child)) tags.add(t);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return tags;
|
|
511
|
-
};
|
|
512
|
-
for (const tag of collectTags(renderDoc)) {
|
|
513
|
-
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
514
|
-
if (comp && comp.source !== "npm") {
|
|
515
|
-
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
516
|
-
if (!existingRefs.has(relPath)) {
|
|
517
|
-
effectiveElements.push({ $ref: relPath });
|
|
518
|
-
existingRefs.add(relPath);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (effectiveElements.length) {
|
|
525
|
-
renderDoc.$elements = effectiveElements;
|
|
526
|
-
for (const entry of effectiveElements) {
|
|
527
|
-
if (typeof entry === "string") {
|
|
528
|
-
try {
|
|
529
|
-
const specifier =
|
|
530
|
-
entry.startsWith("/") || entry.startsWith(".")
|
|
531
|
-
? entry
|
|
532
|
-
: `/${projectState?.projectRoot || ""}/node_modules/${entry}`.replace(/\/+/g, "/");
|
|
533
|
-
await import(specifier);
|
|
534
|
-
} catch (/** @type {any} */ e) {
|
|
535
|
-
console.warn("Studio: failed to import package", entry, e);
|
|
536
|
-
}
|
|
537
|
-
} else if (entry?.$ref) {
|
|
538
|
-
const href = new URL(entry.$ref, docBase).href;
|
|
539
|
-
try {
|
|
540
|
-
await defineElement(href);
|
|
541
|
-
} catch (/** @type {any} */ e) {
|
|
542
|
-
console.warn("Studio: failed to register element", entry.$ref, e);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Bail out if a newer render started while we were importing elements
|
|
549
|
-
if (gen !== view.renderGeneration) return null;
|
|
550
|
-
|
|
551
|
-
// Inject site-level imports so buildScope can resolve $prototype names
|
|
552
|
-
renderDoc.imports = getEffectiveImports(renderDoc.imports);
|
|
553
|
-
|
|
554
|
-
// Apply project-level styles mirroring the compiler convention:
|
|
555
|
-
// viewport ≈ :root → CSS custom properties (they inherit down)
|
|
556
|
-
// canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
|
|
557
|
-
// This ensures project font-family, color, etc. override the
|
|
558
|
-
// content-mode fallback typography rules in the stylesheet.
|
|
559
|
-
// In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
|
|
560
|
-
const viewport = canvasEl.closest(".canvas-panel-viewport");
|
|
561
|
-
const editSurface = canvasMode === "edit" ? canvasEl.closest(".content-edit-canvas") : null;
|
|
562
|
-
const siteStyle = projectState?.projectConfig?.style;
|
|
563
|
-
if (viewport) {
|
|
564
|
-
viewport.style.cssText = "";
|
|
565
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
566
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
567
|
-
if (k.startsWith("--")) {
|
|
568
|
-
viewport.style.setProperty(k, String(v));
|
|
569
|
-
} else {
|
|
570
|
-
/** @type {any} */ (viewport.style)[k] = v;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (editSurface) {
|
|
576
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
577
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
578
|
-
if (k.startsWith("--")) {
|
|
579
|
-
/** @type {any} */ (editSurface).style.setProperty(k, String(v));
|
|
580
|
-
} else {
|
|
581
|
-
/** @type {any} */ (editSurface.style)[k] = v;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
if (siteStyle && typeof siteStyle === "object") {
|
|
587
|
-
for (const [k, v] of Object.entries(siteStyle)) {
|
|
588
|
-
if (!k.startsWith("--")) {
|
|
589
|
-
/** @type {any} */ (canvasEl.style)[k] = v;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Inject site-level $media so runtime can resolve media queries in styles
|
|
595
|
-
renderDoc.$media = getEffectiveMedia(renderDoc.$media);
|
|
596
|
-
|
|
597
|
-
// Inject $head elements (link/meta/script) into document.head
|
|
598
|
-
const effectiveHead = getEffectiveHead(renderDoc.$head);
|
|
599
|
-
if (effectiveHead.length) {
|
|
600
|
-
for (const entry of effectiveHead) {
|
|
601
|
-
if (!entry?.tagName) continue;
|
|
602
|
-
const tag = entry.tagName.toLowerCase();
|
|
603
|
-
const attrs = { ...entry.attributes };
|
|
604
|
-
const root = projectState?.projectRoot || "";
|
|
605
|
-
for (const key of ["href", "src"]) {
|
|
606
|
-
if (
|
|
607
|
-
attrs[key] &&
|
|
608
|
-
!attrs[key].startsWith("/") &&
|
|
609
|
-
!attrs[key].startsWith(".") &&
|
|
610
|
-
!attrs[key].startsWith("http")
|
|
611
|
-
) {
|
|
612
|
-
attrs[key] = `/${root}/node_modules/${attrs[key]}`.replace(/\/+/g, "/");
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
const selector = `${tag}${attrs.href ? `[href="${attrs.href}"]` : ""}${attrs.src ? `[src="${attrs.src}"]` : ""}`;
|
|
616
|
-
if (selector !== tag && document.head.querySelector(selector)) continue;
|
|
617
|
-
const el = document.createElement(tag);
|
|
618
|
-
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, /** @type {string} */ (v));
|
|
619
|
-
if (entry.textContent) el.textContent = entry.textContent;
|
|
620
|
-
document.head.appendChild(el);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
625
|
-
// Bail out if a newer render started while buildScope was running
|
|
626
|
-
if (gen !== view.renderGeneration) return null;
|
|
627
|
-
const el = /** @type {HTMLElement} */ (
|
|
628
|
-
runtimeRenderNode(renderDoc, $defs, {
|
|
629
|
-
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
630
|
-
// Remap $map paths: wrapper and template children → real document paths
|
|
631
|
-
// prepareForEditMode wraps $map template in: children[0] (wrapper) > children[0] (template)
|
|
632
|
-
// Real paths: wrapper → ['children'] ($map container), template → ['children', 'map']
|
|
633
|
-
let mappedPath = path;
|
|
634
|
-
if ((canvasMode === "design" || canvasMode === "edit") && mapParentPaths.size > 0) {
|
|
635
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
636
|
-
if (path[i] === "children" && path[i + 1] === 0) {
|
|
637
|
-
const parentKey = path.slice(0, i).join("/");
|
|
638
|
-
if (mapParentPaths.has(parentKey)) {
|
|
639
|
-
if (path.length === i + 2) {
|
|
640
|
-
// Wrapper div itself → $map container path
|
|
641
|
-
mappedPath = path.slice(0, i + 1);
|
|
642
|
-
} else if (
|
|
643
|
-
path.length >= i + 4 &&
|
|
644
|
-
path[i + 2] === "children" &&
|
|
645
|
-
path[i + 3] === 0
|
|
646
|
-
) {
|
|
647
|
-
// Template or its descendants → children/map/...rest
|
|
648
|
-
mappedPath = [...path.slice(0, i), "children", "map", ...path.slice(i + 4)];
|
|
649
|
-
}
|
|
650
|
-
break;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
elToPath.set(el, mappedPath);
|
|
656
|
-
},
|
|
657
|
-
_path: [],
|
|
658
|
-
})
|
|
659
|
-
);
|
|
660
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
661
|
-
// Disable pointer events on all rendered elements for edit mode
|
|
662
|
-
el.style.pointerEvents = "none";
|
|
663
|
-
for (const child of el.querySelectorAll("*")) {
|
|
664
|
-
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
canvasEl.appendChild(el);
|
|
668
|
-
if (canvasMode === "design" || canvasMode === "edit") {
|
|
669
|
-
// Custom element connectedCallbacks render children asynchronously —
|
|
670
|
-
// sweep again after they've had a chance to run
|
|
671
|
-
requestAnimationFrame(() => {
|
|
672
|
-
const editingEl = getActiveElement();
|
|
673
|
-
for (const child of canvasEl.querySelectorAll("*")) {
|
|
674
|
-
// Preserve pointer-events on the actively-edited element
|
|
675
|
-
if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
|
|
676
|
-
if (editingEl && child === editingEl) continue;
|
|
677
|
-
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
return $defs;
|
|
682
|
-
} catch (/** @type {any} */ err) {
|
|
683
|
-
console.warn("renderCanvasLive failed:", err.message, err);
|
|
684
|
-
return null;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
198
|
// ─── Webdata: datalists for autocomplete ──────────────────────────────────────
|
|
689
199
|
|
|
690
200
|
const datalistHost = document.createElement("div");
|
|
@@ -704,11 +214,6 @@ litRender(
|
|
|
704
214
|
|
|
705
215
|
initCssData(webdata);
|
|
706
216
|
|
|
707
|
-
// Persistent render hosts for lit-html (must be before bootstrap/render)
|
|
708
|
-
let zoomIndicatorHost = document.createElement("div");
|
|
709
|
-
zoomIndicatorHost.style.display = "contents";
|
|
710
|
-
document.body.appendChild(zoomIndicatorHost);
|
|
711
|
-
|
|
712
217
|
// ─── Module-level UI state (must be before render() call) ─────────────────────
|
|
713
218
|
|
|
714
219
|
// ─── Bootstrap ────────────────────────────────────────────────────────────────
|
|
@@ -749,22 +254,53 @@ toolbarPanel.mount(toolbarEl, {
|
|
|
749
254
|
});
|
|
750
255
|
|
|
751
256
|
overlaysPanel.mount({
|
|
752
|
-
effectiveZoom,
|
|
753
257
|
getCanvasMode: () => canvasMode,
|
|
754
258
|
isEditing,
|
|
755
259
|
renderBlockActionBar,
|
|
756
|
-
findCanvasElement,
|
|
757
|
-
getActivePanel,
|
|
758
260
|
});
|
|
759
261
|
|
|
760
262
|
initBlockActionBar({
|
|
761
263
|
getCanvasMode: () => canvasMode,
|
|
762
|
-
findCanvasElement,
|
|
763
|
-
getActivePanel,
|
|
764
264
|
navigateToComponent,
|
|
765
265
|
createFloatingContainer,
|
|
766
266
|
});
|
|
767
267
|
|
|
268
|
+
initComponentInlineEdit({ findCanvasElement });
|
|
269
|
+
initCanvasHelpers({ getCanvasMode: () => canvasMode });
|
|
270
|
+
initCanvasUtils({
|
|
271
|
+
getCanvasMode: () => canvasMode,
|
|
272
|
+
getZoom: () => S.ui.zoom,
|
|
273
|
+
setZoomDirect: (zoom) => {
|
|
274
|
+
session = { ...session, ui: { ...session.ui, zoom } };
|
|
275
|
+
S = toFlat(doc, session);
|
|
276
|
+
},
|
|
277
|
+
renderStylebookOverlays,
|
|
278
|
+
});
|
|
279
|
+
initPanelEvents({
|
|
280
|
+
getState: () => S,
|
|
281
|
+
setState: (s) => {
|
|
282
|
+
S = s;
|
|
283
|
+
({ doc, session } = fromFlat(S));
|
|
284
|
+
},
|
|
285
|
+
getCanvasMode: () => canvasMode,
|
|
286
|
+
enterInlineEdit,
|
|
287
|
+
navigateToComponent,
|
|
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,
|
|
302
|
+
});
|
|
303
|
+
|
|
768
304
|
rightPanelMod.mount({
|
|
769
305
|
navigateToComponent,
|
|
770
306
|
getCanvasMode: () => canvasMode,
|
|
@@ -803,6 +339,13 @@ registerRenderer("statusbar", () => renderStatusbar(S));
|
|
|
803
339
|
setStatusbarRenderer(() => renderStatusbar(S));
|
|
804
340
|
mountStatusbar();
|
|
805
341
|
|
|
342
|
+
// Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
|
|
343
|
+
canvasWrap.addEventListener("click", (/** @type {any} */ e) => {
|
|
344
|
+
if (e.target !== canvasWrap && e.target !== view.panzoomWrap) return;
|
|
345
|
+
if (!S.selection) return;
|
|
346
|
+
update(selectNode(S, null));
|
|
347
|
+
});
|
|
348
|
+
|
|
806
349
|
function safeRenderLeftPanel() {
|
|
807
350
|
leftPanelMod.render();
|
|
808
351
|
}
|
|
@@ -874,10 +417,14 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
874
417
|
if (patch.ui) {
|
|
875
418
|
session.ui = { ...prev.ui, ...patch.ui };
|
|
876
419
|
}
|
|
420
|
+
if (patch.canvas) {
|
|
421
|
+
session.canvas = { ...prev.canvas, ...patch.canvas };
|
|
422
|
+
}
|
|
877
423
|
S = toFlat(doc, session);
|
|
878
424
|
|
|
879
425
|
const selChanged = !pathsEqual(prev.selection, session.selection);
|
|
880
426
|
const uiChanged = prev.ui !== session.ui;
|
|
427
|
+
const canvasChanged = prev.canvas !== session.canvas;
|
|
881
428
|
|
|
882
429
|
const canvasUiChanged =
|
|
883
430
|
uiChanged &&
|
|
@@ -898,7 +445,7 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
898
445
|
console.error("renderCanvas error:", e);
|
|
899
446
|
}
|
|
900
447
|
safeRenderLeftPanel();
|
|
901
|
-
} else if (selChanged || leftUiChanged) {
|
|
448
|
+
} else if (selChanged || leftUiChanged || canvasChanged) {
|
|
902
449
|
safeRenderLeftPanel();
|
|
903
450
|
}
|
|
904
451
|
|
|
@@ -906,20 +453,10 @@ setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
|
906
453
|
updateActivePanelHeaders();
|
|
907
454
|
}
|
|
908
455
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
// Register post-render hook for pseudo-state preview
|
|
916
|
-
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
917
|
-
|
|
918
|
-
// Register post-render hook for pending inline edit
|
|
919
|
-
addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
920
|
-
if (view.pendingInlineEdit && prevDoc === S.document) {
|
|
921
|
-
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
922
|
-
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);
|
|
923
460
|
const targetPanel =
|
|
924
461
|
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
925
462
|
if (targetPanel) {
|
|
@@ -927,8 +464,16 @@ addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
|
927
464
|
if (el) enterComponentInlineEdit(el, path);
|
|
928
465
|
}
|
|
929
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 });
|
|
930
472
|
});
|
|
931
473
|
|
|
474
|
+
// Register post-render hook for pseudo-state preview
|
|
475
|
+
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
476
|
+
|
|
932
477
|
// Now that renderers and update are registered, bootstrap
|
|
933
478
|
registerFunctionCompletions();
|
|
934
479
|
|
|
@@ -1021,1832 +566,57 @@ if (_openParam) {
|
|
|
1021
566
|
render();
|
|
1022
567
|
}
|
|
1023
568
|
|
|
1024
|
-
// ───
|
|
569
|
+
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
1025
570
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
* inline styles the runtime already set. The runtime uses @media CSS rules for overrides, but those
|
|
1029
|
-
* can never beat inline base styles.
|
|
1030
|
-
*
|
|
1031
|
-
* @param {Element} canvasEl
|
|
1032
|
-
* @param {Set<string>} activeBreakpoints
|
|
1033
|
-
*/
|
|
1034
|
-
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
1035
|
-
if (!activeBreakpoints.size) return;
|
|
1036
|
-
const docMedia = getEffectiveMedia(S.document.$media || {});
|
|
1037
|
-
const validBreakpoints = new Set();
|
|
1038
|
-
for (const name of activeBreakpoints) {
|
|
1039
|
-
if (docMedia[name]) validBreakpoints.add(name);
|
|
1040
|
-
}
|
|
1041
|
-
const overrides = collectMediaOverrides(document.styleSheets, validBreakpoints);
|
|
1042
|
-
applyOverridesToCanvas(canvasEl, overrides);
|
|
571
|
+
function renderLeftPanel() {
|
|
572
|
+
leftPanelMod.render();
|
|
1043
573
|
}
|
|
1044
574
|
|
|
1045
|
-
// ───
|
|
1046
|
-
|
|
1047
|
-
function renderCanvas() {
|
|
1048
|
-
// Advance render generation so stale async renders from the previous cycle bail out
|
|
1049
|
-
++view.renderGeneration;
|
|
1050
|
-
|
|
1051
|
-
// Always clear Lit's internal state so it builds fresh DOM. Stale async
|
|
1052
|
-
// renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
|
|
1053
|
-
// markers (Comment nodes inside panzoom-wrap) in ways the root-only
|
|
1054
|
-
// ensureLitState check cannot detect.
|
|
1055
|
-
// @ts-ignore
|
|
1056
|
-
if (canvasWrap["_$litPart$"]) {
|
|
1057
|
-
canvasWrap.textContent = "";
|
|
1058
|
-
// @ts-ignore
|
|
1059
|
-
delete canvasWrap["_$litPart$"];
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Function editor mode: editing a function body in Monaco (JS)
|
|
1063
|
-
if (S.ui.editingFunction) {
|
|
1064
|
-
renderFunctionEditor();
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Dispose function editor if switching away
|
|
1069
|
-
if (view.functionEditor) {
|
|
1070
|
-
view.functionEditor.dispose();
|
|
1071
|
-
view.functionEditor = null;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// Source mode: update existing Monaco editor without recreating
|
|
1075
|
-
if (canvasMode === "source" && view.monacoEditor) {
|
|
1076
|
-
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
1077
|
-
const currentVal = view.monacoEditor.getValue();
|
|
1078
|
-
if (currentVal !== jsonStr) {
|
|
1079
|
-
// Prevent triggering the onChange handler for this programmatic update
|
|
1080
|
-
view.monacoEditor._ignoreNextChange = true;
|
|
1081
|
-
view.monacoEditor.setValue(jsonStr);
|
|
1082
|
-
}
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Detect whether this is a mode transition or a content-only re-render
|
|
1087
|
-
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
1088
|
-
view.prevCanvasMode = canvasMode;
|
|
1089
|
-
|
|
1090
|
-
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
1091
|
-
// content render, so always clean them up.
|
|
1092
|
-
for (const fn of view.canvasDndCleanups) fn();
|
|
1093
|
-
view.canvasDndCleanups = [];
|
|
1094
|
-
|
|
1095
|
-
// Panel event handlers (click, dblclick, etc.) capture closures over panel references.
|
|
1096
|
-
// Always re-register to keep closures fresh across document switches.
|
|
1097
|
-
for (const fn of view.canvasEventCleanups) fn();
|
|
1098
|
-
view.canvasEventCleanups = [];
|
|
1099
|
-
|
|
1100
|
-
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
1101
|
-
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
1102
|
-
canvasPanels.length = 0;
|
|
1103
|
-
|
|
1104
|
-
if (modeChanged) {
|
|
1105
|
-
// Full teardown on mode transitions — new panel structure needed
|
|
1106
|
-
if (view.centerObserver) {
|
|
1107
|
-
view.centerObserver.disconnect();
|
|
1108
|
-
view.centerObserver = null;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Dispose Monaco editor if switching away from source mode
|
|
1112
|
-
if (view.monacoEditor) {
|
|
1113
|
-
view.monacoEditor.dispose();
|
|
1114
|
-
view.monacoEditor = null;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
litRender(nothing, canvasWrap);
|
|
1118
|
-
view.panzoomWrap = null;
|
|
1119
|
-
// Reset inline style overrides from other modes
|
|
1120
|
-
canvasWrap.style.padding = "";
|
|
1121
|
-
canvasWrap.style.alignItems = "";
|
|
1122
|
-
canvasWrap.style.display = "";
|
|
1123
|
-
canvasWrap.style.overflow = "";
|
|
1124
|
-
canvasWrap.style.overflow = "";
|
|
1125
|
-
|
|
1126
|
-
// Clear zoom indicator (only re-rendered by design/preview/stylebook)
|
|
1127
|
-
try {
|
|
1128
|
-
litRender(nothing, zoomIndicatorHost);
|
|
1129
|
-
} catch {
|
|
1130
|
-
const newHost = document.createElement("div");
|
|
1131
|
-
newHost.style.display = "contents";
|
|
1132
|
-
zoomIndicatorHost.replaceWith(newHost);
|
|
1133
|
-
zoomIndicatorHost = newHost;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Dismiss open popovers/toolbars that are no longer relevant
|
|
1137
|
-
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
1138
|
-
dismissLinkPopover();
|
|
1139
|
-
dismissContextMenu();
|
|
1140
|
-
sharedDismissSlashMenu();
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// Manage mode: project-level file browser table
|
|
1144
|
-
if (canvasMode === "manage") {
|
|
1145
|
-
canvasWrap.style.padding = "0";
|
|
1146
|
-
canvasWrap.style.overflow = "auto";
|
|
1147
|
-
renderBrowse(canvasWrap, {
|
|
1148
|
-
openFile: (/** @type {string} */ path) => {
|
|
1149
|
-
canvasMode = "edit";
|
|
1150
|
-
openFileFromTree(path);
|
|
1151
|
-
},
|
|
1152
|
-
});
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// Settings mode: render element catalog with panzoom surface
|
|
1157
|
-
if (canvasMode === "settings") {
|
|
1158
|
-
renderStylebookMode({
|
|
1159
|
-
canvasPanelTemplate,
|
|
1160
|
-
applyTransform,
|
|
1161
|
-
observeCenterUntilStable,
|
|
1162
|
-
renderZoomIndicator,
|
|
1163
|
-
updateActivePanelHeaders,
|
|
1164
|
-
overlayBoxDescriptor,
|
|
1165
|
-
effectiveZoom,
|
|
1166
|
-
});
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// Source mode: create Monaco editor instead of canvas
|
|
1171
|
-
if (canvasMode === "source") {
|
|
1172
|
-
canvasWrap.style.padding = "0";
|
|
1173
|
-
canvasWrap.style.display = "block";
|
|
1174
|
-
/** @type {HTMLDivElement | null} */
|
|
1175
|
-
let editorContainer = null;
|
|
1176
|
-
litRender(
|
|
1177
|
-
html`<div class="source-wrap">
|
|
1178
|
-
<div class="source-toolbar">
|
|
1179
|
-
<sp-action-button size="s" @click=${exportFile}>
|
|
1180
|
-
<sp-icon-export slot="icon"></sp-icon-export>
|
|
1181
|
-
Export
|
|
1182
|
-
</sp-action-button>
|
|
1183
|
-
</div>
|
|
1184
|
-
<div
|
|
1185
|
-
class="source-editor"
|
|
1186
|
-
${ref((el) => {
|
|
1187
|
-
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
1188
|
-
})}
|
|
1189
|
-
></div>
|
|
1190
|
-
</div>`,
|
|
1191
|
-
canvasWrap,
|
|
1192
|
-
);
|
|
1193
|
-
|
|
1194
|
-
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
1195
|
-
view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1196
|
-
value: jsonStr,
|
|
1197
|
-
language: "json",
|
|
1198
|
-
theme: "vs-dark",
|
|
1199
|
-
automaticLayout: true,
|
|
1200
|
-
minimap: { enabled: false },
|
|
1201
|
-
fontSize: 12,
|
|
1202
|
-
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
1203
|
-
lineNumbers: "on",
|
|
1204
|
-
scrollBeyondLastLine: false,
|
|
1205
|
-
wordWrap: "on",
|
|
1206
|
-
tabSize: 2,
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
// Debounced sync back to state
|
|
1210
|
-
/** @type {any} */
|
|
1211
|
-
let debounce;
|
|
1212
|
-
view.monacoEditor.onDidChangeModelContent(() => {
|
|
1213
|
-
if (view.monacoEditor._ignoreNextChange) {
|
|
1214
|
-
view.monacoEditor._ignoreNextChange = false;
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
clearTimeout(debounce);
|
|
1218
|
-
debounce = setTimeout(() => {
|
|
1219
|
-
try {
|
|
1220
|
-
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
1221
|
-
update({ ...S, document: parsed, dirty: true });
|
|
1222
|
-
} catch {
|
|
1223
|
-
// Invalid JSON — don't update state
|
|
1224
|
-
}
|
|
1225
|
-
}, 600);
|
|
1226
|
-
});
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
1231
|
-
if (canvasMode === "edit") {
|
|
1232
|
-
if (modeChanged) {
|
|
1233
|
-
canvasWrap.style.padding = "0";
|
|
1234
|
-
canvasWrap.style.overflow = "hidden";
|
|
1235
|
-
|
|
1236
|
-
// Remove zoom indicator left over from design/preview mode
|
|
1237
|
-
try {
|
|
1238
|
-
litRender(nothing, zoomIndicatorHost);
|
|
1239
|
-
} catch {
|
|
1240
|
-
const newHost = document.createElement("div");
|
|
1241
|
-
newHost.style.display = "contents";
|
|
1242
|
-
zoomIndicatorHost.replaceWith(newHost);
|
|
1243
|
-
zoomIndicatorHost = newHost;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
1248
|
-
const editTpl = html`
|
|
1249
|
-
<div class="content-edit-canvas">
|
|
1250
|
-
<div class="content-edit-column">${panelTpl}</div>
|
|
1251
|
-
</div>
|
|
1252
|
-
`;
|
|
1253
|
-
litRender(editTpl, canvasWrap);
|
|
1254
|
-
canvasPanels.push(panel);
|
|
1255
|
-
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
575
|
+
// ─── DnD registration: delegated to panels/dnd.js ───────────────────────────
|
|
1258
576
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
canvasWrap.style.padding = "0";
|
|
1262
|
-
canvasWrap.style.overflow = "hidden";
|
|
1263
|
-
}
|
|
577
|
+
// ─── Stylebook ───────────────────────────────────────────────────────────────
|
|
578
|
+
// Extracted to panels/stylebook-panel.js
|
|
1264
579
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
featureQueries: _featureQueries,
|
|
1268
|
-
baseWidth,
|
|
1269
|
-
} = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
1270
|
-
const hasMedia = sizeBreakpoints.length > 0;
|
|
1271
|
-
const featureToggles = S.ui.featureToggles;
|
|
1272
|
-
|
|
1273
|
-
// Create panzoom wrapper (the element that gets transformed)
|
|
1274
|
-
if (!hasMedia) {
|
|
1275
|
-
// Single panel — use baseWidth if a custom one is defined, otherwise full-width
|
|
1276
|
-
const effectiveMedia = getEffectiveMedia(S.document.$media);
|
|
1277
|
-
const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
|
|
1278
|
-
const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
|
|
1279
|
-
const { tpl: panelTpl, panel } = canvasPanelTemplate(
|
|
1280
|
-
hasBaseWidth ? "base" : null,
|
|
1281
|
-
label,
|
|
1282
|
-
!hasBaseWidth,
|
|
1283
|
-
hasBaseWidth ? baseWidth : undefined,
|
|
1284
|
-
);
|
|
1285
|
-
litRender(
|
|
1286
|
-
html`
|
|
1287
|
-
<div
|
|
1288
|
-
class="panzoom-wrap"
|
|
1289
|
-
style="transform-origin:0 0"
|
|
1290
|
-
${ref((el) => {
|
|
1291
|
-
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1292
|
-
})}
|
|
1293
|
-
>
|
|
1294
|
-
${panelTpl}
|
|
1295
|
-
</div>
|
|
1296
|
-
`,
|
|
1297
|
-
canvasWrap,
|
|
1298
|
-
);
|
|
1299
|
-
canvasPanels.push(panel);
|
|
1300
|
-
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
1301
|
-
applyTransform();
|
|
1302
|
-
if (modeChanged) {
|
|
1303
|
-
observeCenterUntilStable();
|
|
1304
|
-
}
|
|
1305
|
-
renderZoomIndicator();
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
580
|
+
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
581
|
+
// Extracted to panels/properties-panel.js
|
|
1308
582
|
|
|
1309
|
-
|
|
1310
|
-
// descending for max-width — matching the direction of the design's media queries).
|
|
1311
|
-
const allPanelDefs = [
|
|
1312
|
-
{
|
|
1313
|
-
name: "base",
|
|
1314
|
-
displayName: mediaDisplayName("--"),
|
|
1315
|
-
width: baseWidth,
|
|
1316
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
|
|
1317
|
-
},
|
|
1318
|
-
];
|
|
1319
|
-
for (const bp of sizeBreakpoints) {
|
|
1320
|
-
allPanelDefs.push({
|
|
1321
|
-
name: bp.name,
|
|
1322
|
-
displayName: mediaDisplayName(bp.name),
|
|
1323
|
-
width: bp.width,
|
|
1324
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
583
|
+
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
1327
584
|
|
|
1328
|
-
|
|
1329
|
-
const panelEntries = allPanelDefs.map((def) => {
|
|
1330
|
-
const label = `${def.displayName} (${def.width}px)`;
|
|
1331
|
-
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
1332
|
-
return { tpl, panel, activeSet: def.activeSet };
|
|
1333
|
-
});
|
|
585
|
+
// UNIT_RE — imported from ui/unit-selector.js
|
|
1334
586
|
|
|
1335
|
-
|
|
1336
|
-
html`
|
|
1337
|
-
<div
|
|
1338
|
-
class="panzoom-wrap"
|
|
1339
|
-
style="transform-origin:0 0"
|
|
1340
|
-
${ref((el) => {
|
|
1341
|
-
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1342
|
-
})}
|
|
1343
|
-
>
|
|
1344
|
-
${panelEntries.map((e) => e.tpl)}
|
|
1345
|
-
</div>
|
|
1346
|
-
`,
|
|
1347
|
-
canvasWrap,
|
|
1348
|
-
);
|
|
587
|
+
// inferInputType — imported from studio-utils.js
|
|
1349
588
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
renderCanvasIntoPanel(panel, activeSet, featureToggles);
|
|
1353
|
-
}
|
|
589
|
+
// ─── Style panel ────────────────────────────────────────────────────────────
|
|
590
|
+
// Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
|
|
1354
591
|
|
|
1355
|
-
|
|
1356
|
-
updateActivePanelHeaders();
|
|
592
|
+
// ─── Source/Function editors: delegated to panels/editors.js ─────────────────
|
|
1357
593
|
|
|
1358
|
-
|
|
1359
|
-
applyTransform();
|
|
1360
|
-
if (modeChanged) {
|
|
1361
|
-
observeCenterUntilStable();
|
|
1362
|
-
}
|
|
594
|
+
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
1363
595
|
|
|
1364
|
-
|
|
1365
|
-
|
|
596
|
+
function renderToolbar() {
|
|
597
|
+
toolbarPanel.render();
|
|
1366
598
|
}
|
|
1367
599
|
|
|
1368
|
-
|
|
1369
|
-
* Render document into a single canvas panel. Tries runtime rendering first, falls back to
|
|
1370
|
-
* structural preview.
|
|
1371
|
-
*
|
|
1372
|
-
* @param {any} panel
|
|
1373
|
-
* @param {any} activeBreakpoints
|
|
1374
|
-
* @param {any} featureToggles
|
|
1375
|
-
*/
|
|
1376
|
-
function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
1377
|
-
const gen = view.renderGeneration;
|
|
1378
|
-
renderCanvasLive(gen, S.document, panel.canvas).then((scope) => {
|
|
1379
|
-
// Skip post-render setup if a newer render has started
|
|
1380
|
-
if (gen !== view.renderGeneration) return;
|
|
1381
|
-
if (scope) {
|
|
1382
|
-
view.liveScope = scope;
|
|
1383
|
-
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
1384
|
-
statusMessage("Runtime render OK", 1500);
|
|
1385
|
-
} else {
|
|
1386
|
-
// Fallback to structural preview
|
|
1387
|
-
renderCanvasNode(S.document, [], panel.canvas, activeBreakpoints, featureToggles);
|
|
1388
|
-
}
|
|
1389
|
-
registerPanelDnD(panel);
|
|
1390
|
-
registerPanelEvents(panel);
|
|
1391
|
-
renderOverlays();
|
|
1392
|
-
|
|
1393
|
-
// Process pending inline edit now that the canvas is populated
|
|
1394
|
-
if (view.pendingInlineEdit) {
|
|
1395
|
-
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
1396
|
-
view.pendingInlineEdit = null;
|
|
1397
|
-
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
1398
|
-
if (targetPanel) {
|
|
1399
|
-
const el = findCanvasElement(path, targetPanel.canvas);
|
|
1400
|
-
if (el) enterComponentInlineEdit(el, path);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
600
|
+
// ─── File Operations (delegated to file-ops.js) ─────────────────────────────
|
|
1405
601
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
function canvasPanelTemplate(mediaName, label, fullWidth, width) {
|
|
1416
|
-
/**
|
|
1417
|
-
* @type {{
|
|
1418
|
-
* mediaName: any;
|
|
1419
|
-
* element: Element | null;
|
|
1420
|
-
* canvas: Element | null;
|
|
1421
|
-
* overlay: Element | null;
|
|
1422
|
-
* overlayClk: Element | null;
|
|
1423
|
-
* viewport: Element | null;
|
|
1424
|
-
* dropLine: Element | null;
|
|
1425
|
-
* _width: any;
|
|
1426
|
-
* }}
|
|
1427
|
-
*/
|
|
1428
|
-
const panel = {
|
|
1429
|
-
mediaName,
|
|
1430
|
-
element: null,
|
|
1431
|
-
canvas: null,
|
|
1432
|
-
overlay: null,
|
|
1433
|
-
overlayClk: null,
|
|
1434
|
-
viewport: null,
|
|
1435
|
-
dropLine: null,
|
|
1436
|
-
_width: width || null,
|
|
602
|
+
function fileOpsCtx() {
|
|
603
|
+
return {
|
|
604
|
+
S,
|
|
605
|
+
commit: (/** @type {any} */ ns) => {
|
|
606
|
+
S = ns;
|
|
607
|
+
({ doc, session } = fromFlat(S));
|
|
608
|
+
render();
|
|
609
|
+
},
|
|
610
|
+
renderToolbar,
|
|
1437
611
|
};
|
|
1438
|
-
const tpl = html`
|
|
1439
|
-
<div
|
|
1440
|
-
class=${`canvas-panel${fullWidth ? " full-width" : ""}`}
|
|
1441
|
-
data-media=${ifDefined(mediaName !== null ? mediaName : undefined)}
|
|
1442
|
-
${ref((el) => {
|
|
1443
|
-
if (el) panel.element = el;
|
|
1444
|
-
})}
|
|
1445
|
-
>
|
|
1446
|
-
${label
|
|
1447
|
-
? html`
|
|
1448
|
-
<div
|
|
1449
|
-
class="canvas-panel-header"
|
|
1450
|
-
@click=${() => {
|
|
1451
|
-
updateUi("activeMedia", mediaName === "base" ? null : mediaName);
|
|
1452
|
-
}}
|
|
1453
|
-
>
|
|
1454
|
-
${label}
|
|
1455
|
-
</div>
|
|
1456
|
-
`
|
|
1457
|
-
: nothing}
|
|
1458
|
-
<div
|
|
1459
|
-
class="canvas-panel-viewport"
|
|
1460
|
-
style=${styleMap({ width: width && !fullWidth ? `${width}px` : "" })}
|
|
1461
|
-
${ref((el) => {
|
|
1462
|
-
if (el) panel.viewport = el;
|
|
1463
|
-
})}
|
|
1464
|
-
>
|
|
1465
|
-
<div
|
|
1466
|
-
class="canvas-panel-canvas"
|
|
1467
|
-
style=${styleMap({ width: width ? `${width}px` : "" })}
|
|
1468
|
-
${ref((el) => {
|
|
1469
|
-
if (el) panel.canvas = el;
|
|
1470
|
-
})}
|
|
1471
|
-
></div>
|
|
1472
|
-
<div
|
|
1473
|
-
class="canvas-panel-overlay"
|
|
1474
|
-
${ref((el) => {
|
|
1475
|
-
if (el) panel.overlay = el;
|
|
1476
|
-
})}
|
|
1477
|
-
>
|
|
1478
|
-
<div
|
|
1479
|
-
class="canvas-drop-indicator"
|
|
1480
|
-
style="display:none"
|
|
1481
|
-
${ref((el) => {
|
|
1482
|
-
if (el) panel.dropLine = el;
|
|
1483
|
-
})}
|
|
1484
|
-
></div>
|
|
1485
|
-
</div>
|
|
1486
|
-
<div
|
|
1487
|
-
class="canvas-panel-click"
|
|
1488
|
-
${ref((el) => {
|
|
1489
|
-
if (el) panel.overlayClk = el;
|
|
1490
|
-
})}
|
|
1491
|
-
></div>
|
|
1492
|
-
</div>
|
|
1493
|
-
</div>
|
|
1494
|
-
`;
|
|
1495
|
-
return { tpl, panel };
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
/** Center canvas in viewport. */
|
|
1499
|
-
function centerCanvas() {
|
|
1500
|
-
if (!view.panzoomWrap) return;
|
|
1501
|
-
const wrapWidth = canvasWrap.clientWidth;
|
|
1502
|
-
const wrapHeight = canvasWrap.clientHeight;
|
|
1503
|
-
const contentWidth = view.panzoomWrap.scrollWidth;
|
|
1504
|
-
const contentHeight = view.panzoomWrap.scrollHeight;
|
|
1505
|
-
const scaledWidth = contentWidth * S.ui.zoom;
|
|
1506
|
-
const scaledHeight = contentHeight * S.ui.zoom;
|
|
1507
|
-
view.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
1508
|
-
// Center vertically only when content fits; top-align with margin when taller
|
|
1509
|
-
const verticalCenter = (wrapHeight - scaledHeight) / 2;
|
|
1510
|
-
view.panY = verticalCenter > 16 ? verticalCenter : 16;
|
|
1511
612
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
* Attach a ResizeObserver to view.panzoomWrap that re-centers until the user pans. Handles async
|
|
1515
|
-
* content (runtime rendering, data fetching) that changes layout after initial paint.
|
|
1516
|
-
*/
|
|
1517
|
-
function observeCenterUntilStable() {
|
|
1518
|
-
if (view.centerObserver) {
|
|
1519
|
-
view.centerObserver.disconnect();
|
|
1520
|
-
view.centerObserver = null;
|
|
1521
|
-
}
|
|
1522
|
-
if (!view.panzoomWrap) return;
|
|
1523
|
-
view.needsCenter = true;
|
|
1524
|
-
view.centerObserver = new ResizeObserver(() => {
|
|
1525
|
-
if (!view.needsCenter) {
|
|
1526
|
-
view.centerObserver?.disconnect();
|
|
1527
|
-
view.centerObserver = null;
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
centerCanvas();
|
|
1531
|
-
applyTransform();
|
|
1532
|
-
});
|
|
1533
|
-
view.centerObserver.observe(view.panzoomWrap);
|
|
1534
|
-
// Also center immediately for synchronous content
|
|
1535
|
-
centerCanvas();
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
/** Apply the current zoom + pan transform to the panzoom wrapper. */
|
|
1539
|
-
function applyTransform() {
|
|
1540
|
-
if (!view.panzoomWrap) return;
|
|
1541
|
-
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${S.ui.zoom})`;
|
|
1542
|
-
const label = document.querySelector(".zoom-indicator-label");
|
|
1543
|
-
if (label) label.textContent = `${Math.round(S.ui.zoom * 100)}%`;
|
|
1544
|
-
renderOverlays();
|
|
1545
|
-
if (canvasMode === "settings") renderStylebookOverlays();
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
/** Lightweight in-place zoom update — no full re-render. */
|
|
1549
|
-
function _applyZoom() {
|
|
1550
|
-
applyTransform();
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
/** Calculate zoom + pan to fit all panels within the viewport. */
|
|
1554
|
-
function fitToScreen() {
|
|
1555
|
-
if (!view.panzoomWrap) return;
|
|
1556
|
-
const wrapWidth = canvasWrap.clientWidth;
|
|
1557
|
-
const wrapHeight = canvasWrap.clientHeight;
|
|
1558
|
-
const gap = 24;
|
|
1559
|
-
const padding = 32;
|
|
1560
|
-
let totalPanelWidth = 0;
|
|
1561
|
-
let maxPanelHeight = 0;
|
|
1562
|
-
for (const p of canvasPanels) {
|
|
1563
|
-
totalPanelWidth += p._width || 800;
|
|
1564
|
-
}
|
|
1565
|
-
totalPanelWidth += gap * Math.max(0, canvasPanels.length - 1) + padding;
|
|
1566
|
-
|
|
1567
|
-
// Get actual content height from rendered panels
|
|
1568
|
-
const wrapRect = view.panzoomWrap.getBoundingClientRect();
|
|
1569
|
-
const unscaledHeight = wrapRect.height / S.ui.zoom;
|
|
1570
|
-
maxPanelHeight = unscaledHeight + padding;
|
|
1571
|
-
|
|
1572
|
-
const fitZoomW = wrapWidth / totalPanelWidth;
|
|
1573
|
-
const fitZoomH = wrapHeight / maxPanelHeight;
|
|
1574
|
-
const fitZoom = Math.min(5.0, Math.max(0.05, Math.min(fitZoomW, fitZoomH)));
|
|
1575
|
-
|
|
1576
|
-
session = { ...session, ui: { ...session.ui, zoom: fitZoom } };
|
|
1577
|
-
S = toFlat(doc, session);
|
|
1578
|
-
// Center the content
|
|
1579
|
-
const scaledWidth = totalPanelWidth * fitZoom;
|
|
1580
|
-
const scaledHeight = maxPanelHeight * fitZoom;
|
|
1581
|
-
view.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
1582
|
-
view.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
1583
|
-
applyTransform();
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
/**
|
|
1587
|
-
* Render the floating zoom indicator at the bottom center of canvas-wrap. Uses position: fixed,
|
|
1588
|
-
* computed from canvas-wrap bounds.
|
|
1589
|
-
*/
|
|
1590
|
-
function renderZoomIndicator() {
|
|
1591
|
-
// Reset lit-html state if the host was disconnected or markers were ejected
|
|
1592
|
-
if (!zoomIndicatorHost.isConnected) document.body.appendChild(zoomIndicatorHost);
|
|
1593
|
-
try {
|
|
1594
|
-
litRender(
|
|
1595
|
-
html`
|
|
1596
|
-
<div class="zoom-indicator">
|
|
1597
|
-
<span class="zoom-indicator-label">${Math.round(S.ui.zoom * 100)}%</span>
|
|
1598
|
-
<sp-action-button
|
|
1599
|
-
quiet
|
|
1600
|
-
size="s"
|
|
1601
|
-
class="zoom-fit-btn"
|
|
1602
|
-
title="Fit to screen"
|
|
1603
|
-
@click=${fitToScreen}
|
|
1604
|
-
>
|
|
1605
|
-
<svg
|
|
1606
|
-
width="14"
|
|
1607
|
-
height="14"
|
|
1608
|
-
viewBox="0 0 16 16"
|
|
1609
|
-
fill="none"
|
|
1610
|
-
stroke="currentColor"
|
|
1611
|
-
stroke-width="1.5"
|
|
1612
|
-
>
|
|
1613
|
-
<rect x="2" y="2" width="12" height="12" rx="1" />
|
|
1614
|
-
<path d="M2 6h12M6 2v12" />
|
|
1615
|
-
</svg>
|
|
1616
|
-
</sp-action-button>
|
|
1617
|
-
</div>
|
|
1618
|
-
`,
|
|
1619
|
-
zoomIndicatorHost,
|
|
1620
|
-
);
|
|
1621
|
-
} catch {
|
|
1622
|
-
// Lit markers were corrupted — replace the host element to fully reset Lit state
|
|
1623
|
-
const newHost = document.createElement("div");
|
|
1624
|
-
newHost.style.display = "contents";
|
|
1625
|
-
zoomIndicatorHost.replaceWith(newHost);
|
|
1626
|
-
zoomIndicatorHost = newHost;
|
|
1627
|
-
litRender(
|
|
1628
|
-
html`
|
|
1629
|
-
<div class="zoom-indicator">
|
|
1630
|
-
<span class="zoom-indicator-label">${Math.round(S.ui.zoom * 100)}%</span>
|
|
1631
|
-
<sp-action-button
|
|
1632
|
-
quiet
|
|
1633
|
-
size="s"
|
|
1634
|
-
class="zoom-fit-btn"
|
|
1635
|
-
title="Fit to screen"
|
|
1636
|
-
@click=${fitToScreen}
|
|
1637
|
-
>
|
|
1638
|
-
<svg
|
|
1639
|
-
width="14"
|
|
1640
|
-
height="14"
|
|
1641
|
-
viewBox="0 0 16 16"
|
|
1642
|
-
fill="none"
|
|
1643
|
-
stroke="currentColor"
|
|
1644
|
-
stroke-width="1.5"
|
|
1645
|
-
>
|
|
1646
|
-
<rect x="2" y="2" width="12" height="12" rx="1" />
|
|
1647
|
-
<path d="M2 6h12M6 2v12" />
|
|
1648
|
-
</svg>
|
|
1649
|
-
</sp-action-button>
|
|
1650
|
-
</div>
|
|
1651
|
-
`,
|
|
1652
|
-
zoomIndicatorHost,
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1655
|
-
positionZoomIndicator();
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
function positionZoomIndicator() {
|
|
1659
|
-
const indicator = /** @type {HTMLElement | null} */ (document.querySelector(".zoom-indicator"));
|
|
1660
|
-
if (!indicator) return;
|
|
1661
|
-
const rect = canvasWrap.getBoundingClientRect();
|
|
1662
|
-
indicator.style.left = `${rect.left + rect.width / 2}px`;
|
|
1663
|
-
indicator.style.top = `${rect.bottom - 32}px`;
|
|
1664
|
-
indicator.style.transform = "translateX(-50%)";
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function updateActivePanelHeaders() {
|
|
1668
|
-
for (const p of canvasPanels) {
|
|
1669
|
-
const header = p.element.querySelector(".canvas-panel-header");
|
|
1670
|
-
if (header) {
|
|
1671
|
-
const isActive =
|
|
1672
|
-
(S.ui.activeMedia === null && p.mediaName === "base") ||
|
|
1673
|
-
(S.ui.activeMedia === null && p.mediaName === null) ||
|
|
1674
|
-
S.ui.activeMedia === p.mediaName;
|
|
1675
|
-
header.classList.toggle("active", isActive);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
/**
|
|
1681
|
-
* Recursively render a Jx node to the canvas DOM. Media-aware: applies base styles + active
|
|
1682
|
-
* breakpoint/feature overrides.
|
|
1683
|
-
*
|
|
1684
|
-
* @param {any} node
|
|
1685
|
-
* @param {any} path
|
|
1686
|
-
* @param {any} parent
|
|
1687
|
-
* @param {any} activeBreakpoints
|
|
1688
|
-
* @param {any} featureToggles
|
|
1689
|
-
*/
|
|
1690
|
-
function renderCanvasNode(node, path, parent, activeBreakpoints, featureToggles) {
|
|
1691
|
-
// Text node children: bare strings/numbers/booleans → DOM Text nodes
|
|
1692
|
-
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
|
1693
|
-
parent.appendChild(document.createTextNode(String(node)));
|
|
1694
|
-
return;
|
|
1695
|
-
}
|
|
1696
|
-
if (!node || typeof node !== "object") return;
|
|
1697
|
-
|
|
1698
|
-
const tag = node.tagName || "div";
|
|
1699
|
-
const el = document.createElement(tag);
|
|
1700
|
-
|
|
1701
|
-
elToPath.set(el, path);
|
|
1702
|
-
|
|
1703
|
-
if (typeof node.textContent === "string") {
|
|
1704
|
-
el.textContent = node.textContent;
|
|
1705
|
-
} else if (typeof node.textContent === "object" && node.textContent?.$ref) {
|
|
1706
|
-
const resolved = resolveDefaultForCanvas(node.textContent, S.document.state);
|
|
1707
|
-
el.textContent = resolved;
|
|
1708
|
-
el.style.opacity = "0.7";
|
|
1709
|
-
el.style.fontStyle = "italic";
|
|
1710
|
-
el.title = `Bound: ${node.textContent.$ref}`;
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
if (node.id) el.id = node.id;
|
|
1714
|
-
if (node.className) el.className = node.className;
|
|
1715
|
-
|
|
1716
|
-
applyCanvasStyle(el, node.style, activeBreakpoints, featureToggles);
|
|
1717
|
-
|
|
1718
|
-
if (node.attributes && typeof node.attributes === "object") {
|
|
1719
|
-
for (const [attr, val] of Object.entries(node.attributes)) {
|
|
1720
|
-
try {
|
|
1721
|
-
if (typeof val === "object" && val?.$ref) {
|
|
1722
|
-
const resolved = resolveDefaultForCanvas(val, S.document.state);
|
|
1723
|
-
el.setAttribute(attr, resolved);
|
|
1724
|
-
} else {
|
|
1725
|
-
el.setAttribute(attr, val);
|
|
1726
|
-
}
|
|
1727
|
-
} catch {}
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
if (Array.isArray(node.children)) {
|
|
1732
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
1733
|
-
renderCanvasNode(
|
|
1734
|
-
node.children[i],
|
|
1735
|
-
[...path, "children", i],
|
|
1736
|
-
el,
|
|
1737
|
-
activeBreakpoints,
|
|
1738
|
-
featureToggles,
|
|
1739
|
-
);
|
|
1740
|
-
}
|
|
1741
|
-
} else if (
|
|
1742
|
-
node.children &&
|
|
1743
|
-
typeof node.children === "object" &&
|
|
1744
|
-
node.children.$prototype === "Array"
|
|
1745
|
-
) {
|
|
1746
|
-
// Wrap the map template in a visual repeater perimeter
|
|
1747
|
-
const template = node.children.map;
|
|
1748
|
-
if (template && typeof template === "object") {
|
|
1749
|
-
const wrapper = document.createElement("div");
|
|
1750
|
-
wrapper.className = "repeater-perimeter";
|
|
1751
|
-
elToPath.set(wrapper, [...path, "children"]);
|
|
1752
|
-
renderCanvasNode(
|
|
1753
|
-
template,
|
|
1754
|
-
[...path, "children", "map"],
|
|
1755
|
-
wrapper,
|
|
1756
|
-
activeBreakpoints,
|
|
1757
|
-
featureToggles,
|
|
1758
|
-
);
|
|
1759
|
-
el.appendChild(wrapper);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
if (node.$switch && node.cases && typeof node.cases === "object") {
|
|
1764
|
-
// $switch placeholder in structural preview
|
|
1765
|
-
const keys = Object.keys(node.cases);
|
|
1766
|
-
const placeholder = document.createElement("div");
|
|
1767
|
-
placeholder.textContent = `[$switch: ${keys.join(" | ")}]`;
|
|
1768
|
-
placeholder.style.cssText =
|
|
1769
|
-
"font-family:monospace;font-size:11px;padding:6px 10px;background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px dashed color-mix(in srgb, var(--danger) 40%, transparent);border-radius:4px;color:var(--danger);font-style:italic";
|
|
1770
|
-
el.appendChild(placeholder);
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
el.style.pointerEvents = "none";
|
|
1774
|
-
parent.appendChild(el);
|
|
1775
|
-
return el;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
/**
|
|
1779
|
-
* Track the last drag pointer position for canvas drop calculations
|
|
1780
|
-
*
|
|
1781
|
-
* @type {any}
|
|
1782
|
-
*/
|
|
1783
|
-
|
|
1784
|
-
/**
|
|
1785
|
-
* Register all canvas elements in a panel as DnD drop targets.
|
|
1786
|
-
*
|
|
1787
|
-
* @param {any} panel
|
|
1788
|
-
*/
|
|
1789
|
-
function registerPanelDnD(panel) {
|
|
1790
|
-
const { canvas, overlayClk: _overlayClk, dropLine } = panel;
|
|
1791
|
-
const allEls = canvas.querySelectorAll("*");
|
|
1792
|
-
|
|
1793
|
-
const monitorCleanup = monitorForElements({
|
|
1794
|
-
onDragStart() {
|
|
1795
|
-
for (const el of canvas.querySelectorAll("*")) {
|
|
1796
|
-
/** @type {any} */ (el).style.pointerEvents = "auto";
|
|
1797
|
-
}
|
|
1798
|
-
// Disable click layers on ALL panels during drag
|
|
1799
|
-
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
|
|
1800
|
-
},
|
|
1801
|
-
onDrag({ location }) {
|
|
1802
|
-
view.lastDragInput = location.current.input;
|
|
1803
|
-
},
|
|
1804
|
-
onDrop() {
|
|
1805
|
-
// Hide all drop lines
|
|
1806
|
-
for (const p of canvasPanels) p.dropLine.style.display = "none";
|
|
1807
|
-
view.lastDragInput = null;
|
|
1808
|
-
for (const el of canvas.querySelectorAll("*")) {
|
|
1809
|
-
/** @type {any} */ (el).style.pointerEvents = "none";
|
|
1810
|
-
}
|
|
1811
|
-
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
|
|
1812
|
-
},
|
|
1813
|
-
});
|
|
1814
|
-
view.canvasDndCleanups.push(monitorCleanup);
|
|
1815
|
-
|
|
1816
|
-
for (const el of allEls) {
|
|
1817
|
-
const elPath = elToPath.get(el);
|
|
1818
|
-
if (!elPath) continue;
|
|
1819
|
-
|
|
1820
|
-
const node = getNodeAtPath(S.document, elPath);
|
|
1821
|
-
const isVoid = VOID_ELEMENTS.has((node?.tagName || "div").toLowerCase());
|
|
1822
|
-
|
|
1823
|
-
const cleanup = dropTargetForElements({
|
|
1824
|
-
element: el,
|
|
1825
|
-
canDrop({ source }) {
|
|
1826
|
-
const srcPath = source.data.path;
|
|
1827
|
-
if (srcPath && isAncestor(/** @type {any} */ (srcPath), elPath)) return false;
|
|
1828
|
-
return true;
|
|
1829
|
-
},
|
|
1830
|
-
getData() {
|
|
1831
|
-
return { path: elPath, _isVoid: isVoid };
|
|
1832
|
-
},
|
|
1833
|
-
onDragEnter() {
|
|
1834
|
-
showCanvasDropIndicator(el, elPath, isVoid, panel);
|
|
1835
|
-
},
|
|
1836
|
-
onDrag() {
|
|
1837
|
-
showCanvasDropIndicator(el, elPath, isVoid, panel);
|
|
1838
|
-
},
|
|
1839
|
-
onDragLeave() {
|
|
1840
|
-
dropLine.style.display = "none";
|
|
1841
|
-
el.classList.remove("canvas-drop-target");
|
|
1842
|
-
},
|
|
1843
|
-
onDrop({ source }) {
|
|
1844
|
-
dropLine.style.display = "none";
|
|
1845
|
-
el.classList.remove("canvas-drop-target");
|
|
1846
|
-
const instruction = getCanvasDropInstruction(el, elPath, isVoid);
|
|
1847
|
-
if (!instruction) return;
|
|
1848
|
-
applyDropInstruction(instruction, source.data, elPath);
|
|
1849
|
-
},
|
|
1850
|
-
});
|
|
1851
|
-
view.canvasDndCleanups.push(cleanup);
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
/**
|
|
1856
|
-
* @param {any} el
|
|
1857
|
-
* @param {any} elPath
|
|
1858
|
-
* @param {any} isVoid
|
|
1859
|
-
*/
|
|
1860
|
-
function getCanvasDropInstruction(el, elPath, isVoid) {
|
|
1861
|
-
const rect = el.getBoundingClientRect();
|
|
1862
|
-
if (!view.lastDragInput) return null;
|
|
1863
|
-
const y = view.lastDragInput.clientY;
|
|
1864
|
-
const relY = (y - rect.top) / rect.height;
|
|
1865
|
-
|
|
1866
|
-
if (elPath.length === 0) return { type: "make-child" };
|
|
1867
|
-
if (isVoid) return relY < 0.5 ? { type: "reorder-above" } : { type: "reorder-below" };
|
|
1868
|
-
if (relY < 0.25) return { type: "reorder-above" };
|
|
1869
|
-
if (relY > 0.75) return { type: "reorder-below" };
|
|
1870
|
-
return { type: "make-child" };
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
/**
|
|
1874
|
-
* @param {any} el
|
|
1875
|
-
* @param {any} elPath
|
|
1876
|
-
* @param {any} isVoid
|
|
1877
|
-
* @param {any} panel
|
|
1878
|
-
*/
|
|
1879
|
-
function showCanvasDropIndicator(el, elPath, isVoid, panel) {
|
|
1880
|
-
const instruction = getCanvasDropInstruction(el, elPath, isVoid);
|
|
1881
|
-
const { dropLine, viewport } = panel;
|
|
1882
|
-
if (!instruction) {
|
|
1883
|
-
dropLine.style.display = "none";
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
const scale = effectiveZoom();
|
|
1888
|
-
const wrapRect = viewport.getBoundingClientRect();
|
|
1889
|
-
const elRect = el.getBoundingClientRect();
|
|
1890
|
-
const left = (elRect.left - wrapRect.left + viewport.scrollLeft) / scale;
|
|
1891
|
-
const width = elRect.width / scale;
|
|
1892
|
-
|
|
1893
|
-
if (instruction.type === "make-child") {
|
|
1894
|
-
dropLine.style.display = "block";
|
|
1895
|
-
dropLine.style.top = `${(elRect.top - wrapRect.top + viewport.scrollTop) / scale}px`;
|
|
1896
|
-
dropLine.style.left = `${left}px`;
|
|
1897
|
-
dropLine.style.width = `${width}px`;
|
|
1898
|
-
dropLine.style.height = `${elRect.height / scale}px`;
|
|
1899
|
-
dropLine.className = "canvas-drop-indicator inside";
|
|
1900
|
-
el.classList.add("canvas-drop-target");
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
el.classList.remove("canvas-drop-target");
|
|
1905
|
-
const top =
|
|
1906
|
-
instruction.type === "reorder-above"
|
|
1907
|
-
? (elRect.top - wrapRect.top + viewport.scrollTop) / scale
|
|
1908
|
-
: (elRect.bottom - wrapRect.top + viewport.scrollTop) / scale;
|
|
1909
|
-
|
|
1910
|
-
dropLine.style.display = "block";
|
|
1911
|
-
dropLine.style.top = `${top}px`;
|
|
1912
|
-
dropLine.style.left = `${left}px`;
|
|
1913
|
-
dropLine.style.width = `${width}px`;
|
|
1914
|
-
dropLine.style.height = "2px";
|
|
1915
|
-
dropLine.className = "canvas-drop-indicator line";
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
// ─── Overlay system ───────────────────────────────────────────────────────────
|
|
1919
|
-
|
|
1920
|
-
function renderOverlays() {
|
|
1921
|
-
overlaysPanel.render();
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
/**
|
|
1925
|
-
* Build an overlay box descriptor (no DOM creation).
|
|
1926
|
-
*
|
|
1927
|
-
* @param {any} el
|
|
1928
|
-
* @param {any} type
|
|
1929
|
-
* @param {any} panel
|
|
1930
|
-
*/
|
|
1931
|
-
function overlayBoxDescriptor(el, type, panel) {
|
|
1932
|
-
const vpRect = panel.viewport.getBoundingClientRect();
|
|
1933
|
-
const elRect = el.getBoundingClientRect();
|
|
1934
|
-
const scale = effectiveZoom();
|
|
1935
|
-
return {
|
|
1936
|
-
cls: `overlay-box overlay-${type}`,
|
|
1937
|
-
top: `${(elRect.top - vpRect.top + panel.viewport.scrollTop) / scale}px`,
|
|
1938
|
-
left: `${(elRect.left - vpRect.left + panel.viewport.scrollLeft) / scale}px`,
|
|
1939
|
-
width: `${elRect.width / scale}px`,
|
|
1940
|
-
height: `${elRect.height / scale}px`,
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
function getActivePanel() {
|
|
1945
|
-
if (canvasPanels.length === 0) return null;
|
|
1946
|
-
if (canvasPanels.length === 1) return canvasPanels[0];
|
|
1947
|
-
for (const p of canvasPanels) {
|
|
1948
|
-
if (S.ui.activeMedia === null && (p.mediaName === "base" || p.mediaName === null)) return p;
|
|
1949
|
-
if (p.mediaName === S.ui.activeMedia) return p;
|
|
1950
|
-
}
|
|
1951
|
-
return canvasPanels[0];
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
// ── Pseudo-state preview ──────────────────────────────────────────────────────
|
|
1955
|
-
// When a pseudo-selector (:hover, :focus, etc.) is active in the style sidebar,
|
|
1956
|
-
// force those styles onto the selected element so the user can see the result.
|
|
1957
|
-
|
|
1958
|
-
function updateForcedPseudoPreview() {
|
|
1959
|
-
// Clean up previous
|
|
1960
|
-
if (view.forcedStyleTag) {
|
|
1961
|
-
view.forcedStyleTag.remove();
|
|
1962
|
-
view.forcedStyleTag = null;
|
|
1963
|
-
}
|
|
1964
|
-
if (view.forcedAttrEl) {
|
|
1965
|
-
view.forcedAttrEl.removeAttribute("data-studio-forced");
|
|
1966
|
-
view.forcedAttrEl = null;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
const sel = S.ui?.activeSelector;
|
|
1970
|
-
if (!sel || !sel.startsWith(":") || !S.selection) return;
|
|
1971
|
-
|
|
1972
|
-
const panel = getActivePanel();
|
|
1973
|
-
if (!panel) return;
|
|
1974
|
-
const el = findCanvasElement(S.selection, panel.canvas);
|
|
1975
|
-
if (!el) return;
|
|
1976
|
-
|
|
1977
|
-
// Read the nested style object for this selector
|
|
1978
|
-
const node = getNodeAtPath(S.document, S.selection);
|
|
1979
|
-
if (!node?.style) return;
|
|
1980
|
-
const activeTab = S.ui.activeMedia;
|
|
1981
|
-
/** @type {any} */
|
|
1982
|
-
const ctx = activeTab ? node.style[`@${activeTab}`] || {} : node.style;
|
|
1983
|
-
const rules = ctx[sel];
|
|
1984
|
-
if (!rules || typeof rules !== "object") return;
|
|
1985
|
-
|
|
1986
|
-
// Build CSS text from the rules
|
|
1987
|
-
const cssProps = Object.entries(rules)
|
|
1988
|
-
.filter(([k]) => typeof rules[k] === "string" || typeof rules[k] === "number")
|
|
1989
|
-
.map(
|
|
1990
|
-
([k, v]) =>
|
|
1991
|
-
`${k.replace(/[A-Z]/g, (/** @type {any} */ c) => `-${c.toLowerCase()}`)}: ${v} !important`,
|
|
1992
|
-
)
|
|
1993
|
-
.join("; ");
|
|
1994
|
-
if (!cssProps) return;
|
|
1995
|
-
|
|
1996
|
-
el.setAttribute("data-studio-forced", "1");
|
|
1997
|
-
view.forcedAttrEl = el;
|
|
1998
|
-
|
|
1999
|
-
const tag = document.createElement("style");
|
|
2000
|
-
tag.textContent = `[data-studio-forced] { ${cssProps} }`;
|
|
2001
|
-
document.head.appendChild(tag);
|
|
2002
|
-
view.forcedStyleTag = tag;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
/**
|
|
2006
|
-
* Walk up the tree from a path, bubbling past inline elements until we find the nearest non-inline
|
|
2007
|
-
* ancestor. Returns the original path if already non-inline.
|
|
2008
|
-
*
|
|
2009
|
-
* @param {any} doc
|
|
2010
|
-
* @param {any} path
|
|
2011
|
-
*/
|
|
2012
|
-
function bubbleInlinePath(doc, path) {
|
|
2013
|
-
let currentPath = path;
|
|
2014
|
-
while (currentPath.length >= 2) {
|
|
2015
|
-
const node = getNodeAtPath(doc, currentPath);
|
|
2016
|
-
const pPath = parentElementPath(currentPath);
|
|
2017
|
-
const parentNode = pPath ? getNodeAtPath(doc, pPath) : null;
|
|
2018
|
-
if (!node || !parentNode) break;
|
|
2019
|
-
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
2020
|
-
const parentTag = (parentNode.tagName ?? "div").toLowerCase();
|
|
2021
|
-
if (!isInlineInContext(childTag, parentTag)) break;
|
|
2022
|
-
currentPath = pPath;
|
|
2023
|
-
}
|
|
2024
|
-
return currentPath;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
/** Effective zoom scale — always 1 in edit (content) mode, S.ui.zoom otherwise. */
|
|
2028
|
-
function effectiveZoom() {
|
|
2029
|
-
return canvasMode === "edit" ? 1 : S.ui.zoom;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
/**
|
|
2033
|
-
* @param {any} path
|
|
2034
|
-
* @param {any} canvasEl
|
|
2035
|
-
*/
|
|
2036
|
-
function findCanvasElement(path, canvasEl) {
|
|
2037
|
-
let el = canvasEl.firstElementChild;
|
|
2038
|
-
if (!el) return null;
|
|
2039
|
-
if (path.length === 0) return el;
|
|
2040
|
-
|
|
2041
|
-
for (let i = 0; i < path.length; i += 2) {
|
|
2042
|
-
if (path[i] !== "children" && path[i] !== "cases") return null;
|
|
2043
|
-
const idx = path[i + 1];
|
|
2044
|
-
if (idx === undefined) {
|
|
2045
|
-
// Odd-length path like ['children', 2, 'children'] — $map container
|
|
2046
|
-
// The wrapper div is children[0] of the current element
|
|
2047
|
-
el = el.children[0];
|
|
2048
|
-
} else if (idx === "map") {
|
|
2049
|
-
// $map template: wrapper is children[0], template is wrapper.children[0]
|
|
2050
|
-
el = el.children[0]?.children[0];
|
|
2051
|
-
} else {
|
|
2052
|
-
el = el.children[idx];
|
|
2053
|
-
}
|
|
2054
|
-
if (!el) break;
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
// Verify the result: if DOM traversal landed on the wrong element
|
|
2058
|
-
// (e.g. a custom element template child instead of the intended node),
|
|
2059
|
-
// fall back to scanning elToPath.
|
|
2060
|
-
if (el) {
|
|
2061
|
-
const elPath = elToPath.get(el);
|
|
2062
|
-
if (elPath && pathsEqual(elPath, path)) return el;
|
|
2063
|
-
// el has no path or wrong path — it's a template element, not the target
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
// Fall back: scan all descendants for an element with matching elToPath
|
|
2067
|
-
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
2068
|
-
const p = elToPath.get(candidate);
|
|
2069
|
-
if (p && pathsEqual(p, path)) return candidate;
|
|
2070
|
-
}
|
|
2071
|
-
return null;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
// ─── Per-panel click-to-select ────────────────────────────────────────────────
|
|
2075
|
-
|
|
2076
|
-
/** @param {any} panel */
|
|
2077
|
-
function registerPanelEvents(panel) {
|
|
2078
|
-
const { canvas, overlayClk, mediaName } = panel;
|
|
2079
|
-
const ac = new AbortController();
|
|
2080
|
-
const opts = { signal: ac.signal };
|
|
2081
|
-
view.canvasEventCleanups.push(() => ac.abort());
|
|
2082
|
-
|
|
2083
|
-
/** @param {any} fn */
|
|
2084
|
-
function withPanelPointerEvents(fn) {
|
|
2085
|
-
const els = canvas.querySelectorAll("*");
|
|
2086
|
-
for (const el of els) el.style.pointerEvents = "auto";
|
|
2087
|
-
overlayClk.style.display = "none";
|
|
2088
|
-
const result = fn();
|
|
2089
|
-
overlayClk.style.display = "";
|
|
2090
|
-
for (const el of els) el.style.pointerEvents = "none";
|
|
2091
|
-
return result;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// During component inline edit, the overlayClk is disabled (see enterComponentInlineEdit).
|
|
2095
|
-
// No mousedown passthrough needed — native events reach the contenteditable directly.
|
|
2096
|
-
|
|
2097
|
-
overlayClk.addEventListener(
|
|
2098
|
-
"click",
|
|
2099
|
-
(/** @type {any} */ e) => {
|
|
2100
|
-
// Don't intercept clicks meant for the block action bar
|
|
2101
|
-
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2102
|
-
if (barInner) {
|
|
2103
|
-
const r = barInner.getBoundingClientRect();
|
|
2104
|
-
if (
|
|
2105
|
-
e.clientX >= r.left &&
|
|
2106
|
-
e.clientX <= r.right &&
|
|
2107
|
-
e.clientY >= r.top &&
|
|
2108
|
-
e.clientY <= r.bottom
|
|
2109
|
-
)
|
|
2110
|
-
return;
|
|
2111
|
-
}
|
|
2112
|
-
// If content-mode inline editing is active, treat click outside as blur
|
|
2113
|
-
if (isEditing()) {
|
|
2114
|
-
stopEditing();
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
// Component-mode inline editing is handled by its own document-level listener
|
|
2118
|
-
// (see enterComponentInlineEdit), so nothing to do here — just fall through.
|
|
2119
|
-
|
|
2120
|
-
const elements = withPanelPointerEvents(() =>
|
|
2121
|
-
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2122
|
-
);
|
|
2123
|
-
|
|
2124
|
-
for (const el of elements) {
|
|
2125
|
-
if (canvas.contains(el) && el !== canvas) {
|
|
2126
|
-
const originalPath = elToPath.get(el);
|
|
2127
|
-
if (originalPath) {
|
|
2128
|
-
let path = bubbleInlinePath(S.document, originalPath);
|
|
2129
|
-
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2130
|
-
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2131
|
-
|
|
2132
|
-
// Find the DOM element for the bubbled path (may differ from hit element)
|
|
2133
|
-
// When path didn't change (no inline bubbling), prefer the hit element directly
|
|
2134
|
-
// since findCanvasElement can't navigate into custom element template DOM.
|
|
2135
|
-
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2136
|
-
|
|
2137
|
-
// Re-click on selected editable block: enter inline editing
|
|
2138
|
-
// Edit mode / content mode → rich text editing (enterInlineEdit)
|
|
2139
|
-
// Design mode → plaintext component editing (enterComponentInlineEdit via view.pendingInlineEdit)
|
|
2140
|
-
if (
|
|
2141
|
-
pathsEqual(path, S.selection) &&
|
|
2142
|
-
isEditableBlock(resolvedEl) &&
|
|
2143
|
-
(canvasMode === "edit" || S.mode === "content")
|
|
2144
|
-
) {
|
|
2145
|
-
S = withMedia;
|
|
2146
|
-
enterInlineEdit(resolvedEl, path);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// Design mode or first click: select and schedule component inline editing
|
|
2151
|
-
if (canvasMode === "design" && S.mode !== "content") {
|
|
2152
|
-
view.pendingInlineEdit = { path, mediaName };
|
|
2153
|
-
update(selectNode(withMedia, path));
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
update(selectNode(withMedia, path));
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
update(selectNode(S, null));
|
|
2163
|
-
},
|
|
2164
|
-
opts,
|
|
2165
|
-
);
|
|
2166
|
-
|
|
2167
|
-
// Double-click shortcut for immediate inline editing
|
|
2168
|
-
overlayClk.addEventListener(
|
|
2169
|
-
"dblclick",
|
|
2170
|
-
(/** @type {any} */ e) => {
|
|
2171
|
-
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2172
|
-
if (barInner) {
|
|
2173
|
-
const r = barInner.getBoundingClientRect();
|
|
2174
|
-
if (
|
|
2175
|
-
e.clientX >= r.left &&
|
|
2176
|
-
e.clientX <= r.right &&
|
|
2177
|
-
e.clientY >= r.top &&
|
|
2178
|
-
e.clientY <= r.bottom
|
|
2179
|
-
)
|
|
2180
|
-
return;
|
|
2181
|
-
}
|
|
2182
|
-
if (canvasMode !== "edit" && canvasMode !== "design") return;
|
|
2183
|
-
|
|
2184
|
-
const elements = withPanelPointerEvents(() =>
|
|
2185
|
-
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2186
|
-
);
|
|
2187
|
-
|
|
2188
|
-
for (const el of elements) {
|
|
2189
|
-
if (canvas.contains(el) && el !== canvas) {
|
|
2190
|
-
const originalPath = elToPath.get(el);
|
|
2191
|
-
if (originalPath) {
|
|
2192
|
-
const path = bubbleInlinePath(S.document, originalPath);
|
|
2193
|
-
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2194
|
-
if (isEditableBlock(resolvedEl)) {
|
|
2195
|
-
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2196
|
-
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2197
|
-
update(selectNode(withMedia, path));
|
|
2198
|
-
enterInlineEdit(resolvedEl, path);
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
},
|
|
2205
|
-
opts,
|
|
2206
|
-
);
|
|
2207
|
-
|
|
2208
|
-
overlayClk.addEventListener(
|
|
2209
|
-
"contextmenu",
|
|
2210
|
-
(/** @type {any} */ e) => {
|
|
2211
|
-
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2212
|
-
if (barInner) {
|
|
2213
|
-
const r = barInner.getBoundingClientRect();
|
|
2214
|
-
if (
|
|
2215
|
-
e.clientX >= r.left &&
|
|
2216
|
-
e.clientX <= r.right &&
|
|
2217
|
-
e.clientY >= r.top &&
|
|
2218
|
-
e.clientY <= r.bottom
|
|
2219
|
-
)
|
|
2220
|
-
return;
|
|
2221
|
-
}
|
|
2222
|
-
const elements = withPanelPointerEvents(() =>
|
|
2223
|
-
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2224
|
-
);
|
|
2225
|
-
for (const el of elements) {
|
|
2226
|
-
if (canvas.contains(el) && el !== canvas) {
|
|
2227
|
-
let path = elToPath.get(el);
|
|
2228
|
-
if (path) {
|
|
2229
|
-
path = bubbleInlinePath(S.document, path);
|
|
2230
|
-
showContextMenu(e, path, S, { onEditComponent: navigateToComponent });
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
e.preventDefault();
|
|
2236
|
-
},
|
|
2237
|
-
opts,
|
|
2238
|
-
);
|
|
2239
|
-
|
|
2240
|
-
overlayClk.addEventListener(
|
|
2241
|
-
"mousemove",
|
|
2242
|
-
(/** @type {any} */ e) => {
|
|
2243
|
-
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2244
|
-
if (barInner) {
|
|
2245
|
-
const r = barInner.getBoundingClientRect();
|
|
2246
|
-
if (
|
|
2247
|
-
e.clientX >= r.left &&
|
|
2248
|
-
e.clientX <= r.right &&
|
|
2249
|
-
e.clientY >= r.top &&
|
|
2250
|
-
e.clientY <= r.bottom
|
|
2251
|
-
)
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
2255
|
-
if (el && canvas.contains(el) && el !== canvas) {
|
|
2256
|
-
let path = elToPath.get(el);
|
|
2257
|
-
if (path) {
|
|
2258
|
-
path = bubbleInlinePath(S.document, path);
|
|
2259
|
-
if (!pathsEqual(path, S.hover)) {
|
|
2260
|
-
S = hoverNode(S, path);
|
|
2261
|
-
renderOverlays();
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
} else if (S.hover) {
|
|
2265
|
-
S = hoverNode(S, null);
|
|
2266
|
-
renderOverlays();
|
|
2267
|
-
}
|
|
2268
|
-
},
|
|
2269
|
-
opts,
|
|
2270
|
-
);
|
|
2271
|
-
|
|
2272
|
-
overlayClk.addEventListener(
|
|
2273
|
-
"mouseleave",
|
|
2274
|
-
() => {
|
|
2275
|
-
if (S.hover) {
|
|
2276
|
-
S = hoverNode(S, null);
|
|
2277
|
-
renderOverlays();
|
|
2278
|
-
}
|
|
2279
|
-
},
|
|
2280
|
-
opts,
|
|
2281
|
-
);
|
|
2282
|
-
|
|
2283
|
-
// Mount insertion helper — positioned via CSS Anchor Positioning
|
|
2284
|
-
insertionHelper.mount({
|
|
2285
|
-
getState: () => S,
|
|
2286
|
-
update,
|
|
2287
|
-
getCanvasMode: () => canvasMode,
|
|
2288
|
-
withPanelPointerEvents,
|
|
2289
|
-
effectiveZoom,
|
|
2290
|
-
defaultDef,
|
|
2291
|
-
insertNode,
|
|
2292
|
-
selectNode,
|
|
2293
|
-
parentElementPath,
|
|
2294
|
-
childIndex,
|
|
2295
|
-
getNodeAtPath,
|
|
2296
|
-
elToPath,
|
|
2297
|
-
panel,
|
|
2298
|
-
});
|
|
2299
|
-
view.canvasEventCleanups.push(() => insertionHelper.unmount());
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
// ─── Inline editing bridge ────────────────────────────────────────────────────
|
|
2303
|
-
|
|
2304
|
-
/**
|
|
2305
|
-
* Enter inline editing mode on a canvas element. Hides the overlay for the element and makes it
|
|
2306
|
-
* contenteditable.
|
|
2307
|
-
*
|
|
2308
|
-
* @param {any} el
|
|
2309
|
-
* @param {any} path
|
|
2310
|
-
*/
|
|
2311
|
-
function enterInlineEdit(el, path) {
|
|
2312
|
-
// Restore raw template expressions before editing.
|
|
2313
|
-
// prepareForEditMode renders ${expr} as ❪ expr ❫ for display;
|
|
2314
|
-
// revert so the user edits the real syntax and commits it back intact.
|
|
2315
|
-
restoreTemplateExpressions(el);
|
|
2316
|
-
|
|
2317
|
-
// Hide overlays while editing
|
|
2318
|
-
for (const p of canvasPanels) {
|
|
2319
|
-
p.overlay.style.display = "none";
|
|
2320
|
-
p.overlayClk.style.pointerEvents = "none";
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
startEditing(el, path, {
|
|
2324
|
-
onCommit(
|
|
2325
|
-
/** @type {any} */ commitPath,
|
|
2326
|
-
/** @type {any} */ children,
|
|
2327
|
-
/** @type {any} */ textContent,
|
|
2328
|
-
) {
|
|
2329
|
-
// Update the Jx node with the edited content
|
|
2330
|
-
if (children) {
|
|
2331
|
-
let s = updateProperty(S, commitPath, "textContent", undefined);
|
|
2332
|
-
s = updateProperty(s, commitPath, "children", children);
|
|
2333
|
-
update(s);
|
|
2334
|
-
} else if (textContent != null) {
|
|
2335
|
-
let s = updateProperty(S, commitPath, "children", undefined);
|
|
2336
|
-
s = updateProperty(s, commitPath, "textContent", textContent);
|
|
2337
|
-
update(s);
|
|
2338
|
-
}
|
|
2339
|
-
},
|
|
2340
|
-
|
|
2341
|
-
onSplit(/** @type {any} */ splitPath, /** @type {any} */ before, /** @type {any} */ after) {
|
|
2342
|
-
// Update current element with "before" content
|
|
2343
|
-
const tag = "p";
|
|
2344
|
-
let s = S;
|
|
2345
|
-
|
|
2346
|
-
if (before.textContent != null) {
|
|
2347
|
-
s = updateProperty(s, splitPath, "children", undefined);
|
|
2348
|
-
s = updateProperty(s, splitPath, "textContent", before.textContent);
|
|
2349
|
-
} else if (before.children) {
|
|
2350
|
-
s = updateProperty(s, splitPath, "textContent", undefined);
|
|
2351
|
-
s = updateProperty(s, splitPath, "children", before.children);
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
// Insert new element after with "after" content
|
|
2355
|
-
const parentPath = /** @type {any} */ (parentElementPath(splitPath));
|
|
2356
|
-
const idx = /** @type {number} */ (childIndex(splitPath));
|
|
2357
|
-
/** @type {any} */
|
|
2358
|
-
const newNode = { tagName: tag };
|
|
2359
|
-
if (after.textContent != null) {
|
|
2360
|
-
newNode.textContent = after.textContent;
|
|
2361
|
-
} else if (after.children) {
|
|
2362
|
-
newNode.children = after.children;
|
|
2363
|
-
} else {
|
|
2364
|
-
newNode.textContent = "";
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
s = insertNode(s, parentPath, idx + 1, newNode);
|
|
2368
|
-
// Select the new element
|
|
2369
|
-
const newPath = [...parentPath, "children", idx + 1];
|
|
2370
|
-
s = selectNode(s, newPath);
|
|
2371
|
-
update(s);
|
|
2372
|
-
|
|
2373
|
-
// Re-enter editing on the new element after render
|
|
2374
|
-
requestAnimationFrame(() => {
|
|
2375
|
-
const activePanel = getActivePanel();
|
|
2376
|
-
if (activePanel) {
|
|
2377
|
-
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
2378
|
-
if (newEl && isEditableBlock(newEl)) {
|
|
2379
|
-
enterInlineEdit(newEl, newPath);
|
|
2380
|
-
// Place cursor at start of new element
|
|
2381
|
-
const sel = window.getSelection();
|
|
2382
|
-
const range = document.createRange();
|
|
2383
|
-
range.selectNodeContents(newEl);
|
|
2384
|
-
range.collapse(true); // collapse to start
|
|
2385
|
-
sel?.removeAllRanges();
|
|
2386
|
-
sel?.addRange(range);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
});
|
|
2390
|
-
},
|
|
2391
|
-
|
|
2392
|
-
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
|
|
2393
|
-
// cmd comes from the shared slash menu: { label, tag, description }
|
|
2394
|
-
const isEmpty =
|
|
2395
|
-
!commitData ||
|
|
2396
|
-
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
2397
|
-
(commitData.children &&
|
|
2398
|
-
(commitData.children.length === 0 ||
|
|
2399
|
-
(commitData.children.length === 1 &&
|
|
2400
|
-
typeof commitData.children[0] === "string" &&
|
|
2401
|
-
commitData.children[0].trim() === "") ||
|
|
2402
|
-
(commitData.children.length === 1 &&
|
|
2403
|
-
typeof commitData.children[0] === "object" &&
|
|
2404
|
-
commitData.children[0]?.tagName === "br")));
|
|
2405
|
-
|
|
2406
|
-
// If the element is empty, swap its tagName instead of inserting after
|
|
2407
|
-
if (isEmpty) {
|
|
2408
|
-
let s = S;
|
|
2409
|
-
s = updateProperty(s, afterPath, "tagName", cmd.tag);
|
|
2410
|
-
s = updateProperty(s, afterPath, "children", undefined);
|
|
2411
|
-
const def = defaultDef(cmd.tag);
|
|
2412
|
-
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
2413
|
-
s = updateProperty(s, afterPath, "textContent", def.textContent);
|
|
2414
|
-
} else {
|
|
2415
|
-
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2416
|
-
}
|
|
2417
|
-
s = selectNode(s, afterPath);
|
|
2418
|
-
update(s);
|
|
2419
|
-
|
|
2420
|
-
requestAnimationFrame(() => {
|
|
2421
|
-
const activePanel = getActivePanel();
|
|
2422
|
-
if (activePanel) {
|
|
2423
|
-
const el = findCanvasElement(afterPath, activePanel.canvas);
|
|
2424
|
-
if (el && isEditableBlock(el)) {
|
|
2425
|
-
enterInlineEdit(el, afterPath);
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
});
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
const elementDef = defaultDef(cmd.tag);
|
|
2433
|
-
const parentPath = /** @type {any} */ (parentElementPath(afterPath));
|
|
2434
|
-
const idx = /** @type {number} */ (childIndex(afterPath));
|
|
2435
|
-
|
|
2436
|
-
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
2437
|
-
let s = S;
|
|
2438
|
-
if (commitData) {
|
|
2439
|
-
if (commitData.children) {
|
|
2440
|
-
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2441
|
-
s = updateProperty(s, afterPath, "children", commitData.children);
|
|
2442
|
-
} else if (commitData.textContent != null) {
|
|
2443
|
-
s = updateProperty(s, afterPath, "children", undefined);
|
|
2444
|
-
s = updateProperty(s, afterPath, "textContent", commitData.textContent);
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
|
|
2449
|
-
const newPath = [...parentPath, "children", idx + 1];
|
|
2450
|
-
s = selectNode(s, newPath);
|
|
2451
|
-
update(s);
|
|
2452
|
-
|
|
2453
|
-
// If the inserted element is editable, enter editing
|
|
2454
|
-
requestAnimationFrame(() => {
|
|
2455
|
-
const activePanel = getActivePanel();
|
|
2456
|
-
if (activePanel) {
|
|
2457
|
-
const newEl = findCanvasElement(newPath, activePanel.canvas);
|
|
2458
|
-
if (newEl && isEditableBlock(newEl)) {
|
|
2459
|
-
enterInlineEdit(newEl, newPath);
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
});
|
|
2463
|
-
},
|
|
2464
|
-
|
|
2465
|
-
onEnd() {
|
|
2466
|
-
// Cleanup inline edit listeners
|
|
2467
|
-
if (view.inlineEditCleanup) {
|
|
2468
|
-
view.inlineEditCleanup();
|
|
2469
|
-
view.inlineEditCleanup = null;
|
|
2470
|
-
}
|
|
2471
|
-
// Restore overlays after inline editing ends
|
|
2472
|
-
for (const p of canvasPanels) {
|
|
2473
|
-
p.overlay.style.display = "";
|
|
2474
|
-
p.overlayClk.style.pointerEvents = "";
|
|
2475
|
-
}
|
|
2476
|
-
renderOverlays();
|
|
2477
|
-
},
|
|
2478
|
-
});
|
|
2479
|
-
|
|
2480
|
-
// Show the block action bar (with inline formatting buttons) on the viewport
|
|
2481
|
-
// Defer to ensure this runs after any synchronous renderOverlays() from update()
|
|
2482
|
-
requestAnimationFrame(() => renderBlockActionBar());
|
|
2483
|
-
|
|
2484
|
-
// Re-render action bar when selection changes inside contenteditable
|
|
2485
|
-
const selectionHandler = () => renderBlockActionBar();
|
|
2486
|
-
document.addEventListener("selectionchange", selectionHandler);
|
|
2487
|
-
el.addEventListener("mouseup", selectionHandler);
|
|
2488
|
-
el.addEventListener("keyup", selectionHandler);
|
|
2489
|
-
|
|
2490
|
-
// Store listeners for cleanup
|
|
2491
|
-
const inlineEditCleanup = () => {
|
|
2492
|
-
document.removeEventListener("selectionchange", selectionHandler);
|
|
2493
|
-
el.removeEventListener("mouseup", selectionHandler);
|
|
2494
|
-
el.removeEventListener("keyup", selectionHandler);
|
|
2495
|
-
};
|
|
2496
|
-
view.inlineEditCleanup = inlineEditCleanup;
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
// ─── Component-mode inline text editing ──────────────────────────────────────
|
|
2500
|
-
|
|
2501
|
-
/**
|
|
2502
|
-
* @param {any} el
|
|
2503
|
-
* @param {any} path
|
|
2504
|
-
*/
|
|
2505
|
-
function enterComponentInlineEdit(el, path) {
|
|
2506
|
-
// Already editing this element
|
|
2507
|
-
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
const node = getNodeAtPath(S.document, path);
|
|
2512
|
-
if (!node) return;
|
|
2513
|
-
|
|
2514
|
-
// Skip nodes that shouldn't be inline-edited
|
|
2515
|
-
const tc = node.textContent;
|
|
2516
|
-
if (node.$props && (node.tagName || "").includes("-")) return;
|
|
2517
|
-
if (Array.isArray(node.children) && node.children.length > 0) return;
|
|
2518
|
-
if (node.children && typeof node.children === "object") return;
|
|
2519
|
-
if (tc && typeof tc === "object") return;
|
|
2520
|
-
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
|
|
2521
|
-
if (voids.has(node.tagName)) return;
|
|
2522
|
-
|
|
2523
|
-
// Keep overlay visible for the label, but hide selection border to not obscure editing outline.
|
|
2524
|
-
// Disable click interceptor so native contenteditable handles all mouse interaction.
|
|
2525
|
-
for (const p of canvasPanels) {
|
|
2526
|
-
const boxes = p.overlay.querySelectorAll(".overlay-box");
|
|
2527
|
-
for (const box of boxes) {
|
|
2528
|
-
box.style.border = "none";
|
|
2529
|
-
}
|
|
2530
|
-
p.overlayClk.style.pointerEvents = "none";
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
el.contentEditable = "plaintext-only";
|
|
2534
|
-
el.style.pointerEvents = "auto"; // required for caretRangeFromPoint hit-testing
|
|
2535
|
-
el.style.cursor = "text";
|
|
2536
|
-
el.style.outline = "1px solid var(--accent, #4f8bc7)";
|
|
2537
|
-
el.style.outlineOffset = "-1px";
|
|
2538
|
-
el.style.minHeight = "1em";
|
|
2539
|
-
|
|
2540
|
-
// Show raw textContent (not the ❮...❯ display transform)
|
|
2541
|
-
const rawText = typeof tc === "string" ? tc : "";
|
|
2542
|
-
el.textContent = rawText;
|
|
2543
|
-
|
|
2544
|
-
view.componentInlineEdit = {
|
|
2545
|
-
el,
|
|
2546
|
-
path,
|
|
2547
|
-
originalText: rawText,
|
|
2548
|
-
mediaName: canvasPanels.find((p) => p.canvas.contains(el))?.mediaName || null,
|
|
2549
|
-
};
|
|
2550
|
-
|
|
2551
|
-
// Focus and place cursor at end
|
|
2552
|
-
el.focus();
|
|
2553
|
-
const sel = window.getSelection();
|
|
2554
|
-
const range = document.createRange();
|
|
2555
|
-
range.selectNodeContents(el);
|
|
2556
|
-
range.collapse(false);
|
|
2557
|
-
sel?.removeAllRanges();
|
|
2558
|
-
sel?.addRange(range);
|
|
2559
|
-
|
|
2560
|
-
el.addEventListener("keydown", componentInlineKeydown);
|
|
2561
|
-
el.addEventListener("input", componentInlineInput);
|
|
2562
|
-
|
|
2563
|
-
// Document-level mousedown: clicking outside the editing element commits
|
|
2564
|
-
// the edit and selects the new target element for inline editing.
|
|
2565
|
-
const outsideHandler = (/** @type {any} */ evt) => {
|
|
2566
|
-
if (!view.componentInlineEdit) {
|
|
2567
|
-
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
if (view.componentInlineEdit.el.contains(evt.target)) return; // click within editing el — let it through
|
|
2571
|
-
// Let clicks through when the slash command menu is open
|
|
2572
|
-
if (isSlashMenuOpen()) return;
|
|
2573
|
-
// Let clicks inside the block action bar through
|
|
2574
|
-
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
|
|
2575
|
-
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2576
|
-
|
|
2577
|
-
// Hit-test BEFORE commit (while the current canvas DOM + elToPath are still valid)
|
|
2578
|
-
let hitPath = null,
|
|
2579
|
-
hitMedia = null;
|
|
2580
|
-
for (const p of canvasPanels) {
|
|
2581
|
-
const els = p.canvas.querySelectorAll("*");
|
|
2582
|
-
for (const el of els) el.style.pointerEvents = "auto";
|
|
2583
|
-
p.overlayClk.style.display = "none";
|
|
2584
|
-
const found = document.elementsFromPoint(evt.clientX, evt.clientY);
|
|
2585
|
-
p.overlayClk.style.display = "";
|
|
2586
|
-
for (const el of els) el.style.pointerEvents = "none";
|
|
2587
|
-
for (const hit of found) {
|
|
2588
|
-
if (p.canvas.contains(hit) && hit !== p.canvas) {
|
|
2589
|
-
const path = elToPath.get(hit);
|
|
2590
|
-
if (path) {
|
|
2591
|
-
hitPath = path;
|
|
2592
|
-
hitMedia = p.mediaName;
|
|
2593
|
-
break;
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
if (hitPath) break;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
// Commit + select new element in a single state update if possible
|
|
2601
|
-
const { el: editEl, path: editPath, originalText } = view.componentInlineEdit;
|
|
2602
|
-
const newText = (editEl.textContent ?? "").trim();
|
|
2603
|
-
cleanupComponentInlineEdit(editEl);
|
|
2604
|
-
|
|
2605
|
-
// If empty, remove the node entirely
|
|
2606
|
-
const isEmpty = !newText;
|
|
2607
|
-
const pPath = parentElementPath(editPath);
|
|
2608
|
-
|
|
2609
|
-
if (hitPath) {
|
|
2610
|
-
const media = hitMedia === "base" ? null : (hitMedia ?? null);
|
|
2611
|
-
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2612
|
-
const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
|
|
2613
|
-
if (isEmpty && pPath) {
|
|
2614
|
-
// Remove empty node; adjust hitPath if it shifts after removal
|
|
2615
|
-
let s = removeNode(withMedia, editPath);
|
|
2616
|
-
// If hit path is a later sibling in the same parent, adjust index
|
|
2617
|
-
const removedIdx = /** @type {number} */ (childIndex(editPath));
|
|
2618
|
-
const hitIdx = /** @type {number} */ (childIndex(hitPath));
|
|
2619
|
-
const hitParent = parentElementPath(hitPath);
|
|
2620
|
-
if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
|
|
2621
|
-
hitPath = [...pPath, "children", hitIdx - 1];
|
|
2622
|
-
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2623
|
-
}
|
|
2624
|
-
update(selectNode(s, hitPath));
|
|
2625
|
-
} else if (newText !== originalText) {
|
|
2626
|
-
update(
|
|
2627
|
-
selectNode(
|
|
2628
|
-
updateProperty(withMedia, editPath, "textContent", newText || undefined),
|
|
2629
|
-
hitPath,
|
|
2630
|
-
),
|
|
2631
|
-
);
|
|
2632
|
-
} else {
|
|
2633
|
-
update(selectNode(withMedia, hitPath));
|
|
2634
|
-
}
|
|
2635
|
-
} else {
|
|
2636
|
-
// Clicked on empty space — just commit
|
|
2637
|
-
if (isEmpty && pPath) {
|
|
2638
|
-
update(removeNode(S, editPath));
|
|
2639
|
-
} else if (newText !== originalText) {
|
|
2640
|
-
update(updateProperty(S, editPath, "textContent", newText || undefined));
|
|
2641
|
-
} else {
|
|
2642
|
-
renderCanvas();
|
|
2643
|
-
renderOverlays();
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
};
|
|
2647
|
-
document.addEventListener("mousedown", outsideHandler, true);
|
|
2648
|
-
view.componentInlineEdit._outsideHandler = outsideHandler;
|
|
2649
|
-
|
|
2650
|
-
// Re-render block action bar to show inline formatting buttons
|
|
2651
|
-
renderBlockActionBar();
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
/** @param {any} e */
|
|
2655
|
-
function componentInlineKeydown(e) {
|
|
2656
|
-
// When slash menu is open, let the shared module's capturing handler deal with it
|
|
2657
|
-
if (isSlashMenuOpen()) {
|
|
2658
|
-
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.key)) return;
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
2662
|
-
e.preventDefault();
|
|
2663
|
-
splitParagraph();
|
|
2664
|
-
} else if (e.key === "Escape") {
|
|
2665
|
-
e.preventDefault();
|
|
2666
|
-
cancelComponentInlineEdit();
|
|
2667
|
-
}
|
|
2668
|
-
e.stopPropagation(); // prevent studio keyboard shortcuts
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
function splitParagraph() {
|
|
2672
|
-
if (!view.componentInlineEdit) return;
|
|
2673
|
-
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2674
|
-
|
|
2675
|
-
// Determine cursor offset within text
|
|
2676
|
-
const sel = /** @type {any} */ (el.ownerDocument.defaultView?.getSelection());
|
|
2677
|
-
const fullText = el.textContent || "";
|
|
2678
|
-
let offset = fullText.length;
|
|
2679
|
-
if (sel.rangeCount) {
|
|
2680
|
-
const range = sel.getRangeAt(0);
|
|
2681
|
-
const preRange = document.createRange();
|
|
2682
|
-
preRange.selectNodeContents(el);
|
|
2683
|
-
preRange.setEnd(range.startContainer, range.startOffset);
|
|
2684
|
-
offset = preRange.toString().length;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
const textBefore = fullText.slice(0, offset);
|
|
2688
|
-
const textAfter = fullText.slice(offset);
|
|
2689
|
-
|
|
2690
|
-
const tag = "p";
|
|
2691
|
-
const pPath = /** @type {any} */ (parentElementPath(path));
|
|
2692
|
-
const idx = /** @type {number} */ (childIndex(path));
|
|
2693
|
-
if (!pPath) return; // can't split root
|
|
2694
|
-
|
|
2695
|
-
const newDef = { tagName: tag, textContent: textAfter };
|
|
2696
|
-
const newPath = [...pPath, "children", idx + 1];
|
|
2697
|
-
|
|
2698
|
-
cleanupComponentInlineEdit(el);
|
|
2699
|
-
|
|
2700
|
-
// Compound mutation: update current text + insert sibling + select new
|
|
2701
|
-
let s = updateProperty(S, path, "textContent", textBefore || undefined);
|
|
2702
|
-
s = insertNode(s, pPath, idx + 1, newDef);
|
|
2703
|
-
s = selectNode(s, newPath);
|
|
2704
|
-
|
|
2705
|
-
view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2706
|
-
update(s);
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
function _commitComponentInlineEdit() {
|
|
2710
|
-
if (!view.componentInlineEdit) return;
|
|
2711
|
-
const { el, path, originalText } = view.componentInlineEdit;
|
|
2712
|
-
const newText = (el.textContent ?? "").trim();
|
|
2713
|
-
|
|
2714
|
-
cleanupComponentInlineEdit(el);
|
|
2715
|
-
|
|
2716
|
-
// If empty, remove the node entirely
|
|
2717
|
-
const pPath = parentElementPath(path);
|
|
2718
|
-
if (!newText && pPath) {
|
|
2719
|
-
update(removeNode(S, path));
|
|
2720
|
-
} else if (newText !== originalText) {
|
|
2721
|
-
update(updateProperty(S, path, "textContent", newText || undefined));
|
|
2722
|
-
} else {
|
|
2723
|
-
renderCanvas();
|
|
2724
|
-
renderOverlays();
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
function cancelComponentInlineEdit() {
|
|
2729
|
-
if (!view.componentInlineEdit) return;
|
|
2730
|
-
const { el } = view.componentInlineEdit;
|
|
2731
|
-
cleanupComponentInlineEdit(el);
|
|
2732
|
-
renderCanvas();
|
|
2733
|
-
renderOverlays();
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
/** @param {any} el */
|
|
2737
|
-
function cleanupComponentInlineEdit(el) {
|
|
2738
|
-
el.removeEventListener("keydown", componentInlineKeydown);
|
|
2739
|
-
el.removeEventListener("input", componentInlineInput);
|
|
2740
|
-
sharedDismissSlashMenu();
|
|
2741
|
-
el.removeAttribute("contenteditable");
|
|
2742
|
-
el.style.cursor = "";
|
|
2743
|
-
el.style.outline = "";
|
|
2744
|
-
el.style.outlineOffset = "";
|
|
2745
|
-
el.style.minHeight = "";
|
|
2746
|
-
el.style.pointerEvents = "";
|
|
2747
|
-
|
|
2748
|
-
// Remove the document-level outside-click handler
|
|
2749
|
-
if (view.componentInlineEdit?._outsideHandler) {
|
|
2750
|
-
document.removeEventListener("mousedown", view.componentInlineEdit._outsideHandler, true);
|
|
2751
|
-
}
|
|
2752
|
-
view.componentInlineEdit = null;
|
|
2753
|
-
|
|
2754
|
-
// Restore overlay and click interceptor
|
|
2755
|
-
for (const p of canvasPanels) {
|
|
2756
|
-
p.overlay.style.display = "";
|
|
2757
|
-
p.overlayClk.style.pointerEvents = "";
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
// ─── Component-mode slash commands (delegates to shared slash-menu.js) ────────
|
|
2762
|
-
|
|
2763
|
-
function componentInlineInput() {
|
|
2764
|
-
if (!view.componentInlineEdit) return;
|
|
2765
|
-
const { el, originalText } = view.componentInlineEdit;
|
|
2766
|
-
const text = el.textContent || "";
|
|
2767
|
-
|
|
2768
|
-
// Only trigger slash menu when the paragraph was originally empty and starts with /
|
|
2769
|
-
if (originalText === "" && text.startsWith("/")) {
|
|
2770
|
-
const filter = text.slice(1).toLowerCase();
|
|
2771
|
-
sharedShowSlashMenu(el, filter, { onSelect: handleComponentSlashSelect });
|
|
2772
|
-
} else {
|
|
2773
|
-
sharedDismissSlashMenu();
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
/** @param {any} cmd */
|
|
2778
|
-
function handleComponentSlashSelect(cmd) {
|
|
2779
|
-
if (!view.componentInlineEdit) return;
|
|
2780
|
-
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2781
|
-
const pPath = parentElementPath(path);
|
|
2782
|
-
const idx = /** @type {number} */ (childIndex(path));
|
|
2783
|
-
if (!pPath) return;
|
|
2784
|
-
|
|
2785
|
-
cleanupComponentInlineEdit(el);
|
|
2786
|
-
|
|
2787
|
-
const newDef = defaultDef(cmd.tag);
|
|
2788
|
-
const newPath = [...pPath, "children", idx];
|
|
2789
|
-
|
|
2790
|
-
// Replace current empty paragraph with the chosen element
|
|
2791
|
-
let s = removeNode(S, path);
|
|
2792
|
-
s = insertNode(s, pPath, idx, newDef);
|
|
2793
|
-
s = selectNode(s, newPath);
|
|
2794
|
-
|
|
2795
|
-
// If the new element has textContent, enter inline edit on it
|
|
2796
|
-
const hasText = newDef.textContent != null;
|
|
2797
|
-
if (hasText) view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2798
|
-
update(s);
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
2802
|
-
|
|
2803
|
-
function renderLeftPanel() {
|
|
2804
|
-
leftPanelMod.render();
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
// ─── DnD registration: delegated to panels/dnd.js ───────────────────────────
|
|
2808
|
-
|
|
2809
|
-
// ─── Stylebook ───────────────────────────────────────────────────────────────
|
|
2810
|
-
// Extracted to panels/stylebook-panel.js
|
|
2811
|
-
|
|
2812
|
-
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
2813
|
-
// Extracted to panels/properties-panel.js
|
|
2814
|
-
|
|
2815
|
-
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
2816
|
-
|
|
2817
|
-
// UNIT_RE — imported from ui/unit-selector.js
|
|
2818
|
-
|
|
2819
|
-
// inferInputType — imported from studio-utils.js
|
|
2820
|
-
|
|
2821
|
-
// ─── Style panel ────────────────────────────────────────────────────────────
|
|
2822
|
-
// Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
|
|
2823
|
-
|
|
2824
|
-
// ─── Source/Function editors: delegated to panels/editors.js ─────────────────
|
|
2825
|
-
|
|
2826
|
-
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
2827
|
-
|
|
2828
|
-
function renderToolbar() {
|
|
2829
|
-
toolbarPanel.render();
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
// ─── File Operations (delegated to file-ops.js) ─────────────────────────────
|
|
2833
|
-
|
|
2834
|
-
function fileOpsCtx() {
|
|
2835
|
-
return {
|
|
2836
|
-
S,
|
|
2837
|
-
commit: (/** @type {any} */ ns) => {
|
|
2838
|
-
S = ns;
|
|
2839
|
-
render();
|
|
2840
|
-
},
|
|
2841
|
-
renderToolbar,
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
function openFile() {
|
|
2845
|
-
return _openFile(fileOpsCtx());
|
|
613
|
+
function openFile() {
|
|
614
|
+
return _openFile(fileOpsCtx());
|
|
2846
615
|
}
|
|
2847
616
|
async function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
2848
617
|
const ns = await _loadMarkdown(source, fileHandle);
|
|
2849
618
|
S = ns;
|
|
619
|
+
({ doc, session } = fromFlat(S));
|
|
2850
620
|
}
|
|
2851
621
|
function saveFile() {
|
|
2852
622
|
return _saveFile(fileOpsCtx());
|
|
@@ -2865,6 +635,7 @@ function openProject() {
|
|
|
2865
635
|
S,
|
|
2866
636
|
commit: (/** @type {any} */ ns) => {
|
|
2867
637
|
S = ns;
|
|
638
|
+
({ doc, session } = fromFlat(S));
|
|
2868
639
|
},
|
|
2869
640
|
renderActivityBar: () => renderActivityBar(S),
|
|
2870
641
|
renderLeftPanel,
|
|
@@ -2884,6 +655,7 @@ function openFileFromTree(/** @type {any} */ path) {
|
|
|
2884
655
|
},
|
|
2885
656
|
commit: (/** @type {any} */ ns) => {
|
|
2886
657
|
S = ns;
|
|
658
|
+
({ doc, session } = fromFlat(S));
|
|
2887
659
|
},
|
|
2888
660
|
render,
|
|
2889
661
|
loadMarkdown,
|
|
@@ -2897,6 +669,7 @@ initShortcuts(() => ({
|
|
|
2897
669
|
S,
|
|
2898
670
|
setS: (ns) => {
|
|
2899
671
|
S = ns;
|
|
672
|
+
({ doc, session } = fromFlat(S));
|
|
2900
673
|
},
|
|
2901
674
|
canvasMode,
|
|
2902
675
|
panX: view.panX,
|