@jxsuite/studio 0.1.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +47638 -33445
- package/dist/studio.js.map +449 -344
- package/package.json +45 -34
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +125 -0
- package/src/panels/right-panel.js +104 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/toolbar.js +217 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +77 -41
- package/src/studio.js +1523 -1375
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +45 -0
package/src/studio.js
CHANGED
|
@@ -9,11 +9,8 @@ import {
|
|
|
9
9
|
createState,
|
|
10
10
|
selectNode,
|
|
11
11
|
hoverNode,
|
|
12
|
-
undo,
|
|
13
|
-
redo,
|
|
14
12
|
insertNode,
|
|
15
13
|
removeNode,
|
|
16
|
-
duplicateNode,
|
|
17
14
|
moveNode,
|
|
18
15
|
updateProperty,
|
|
19
16
|
updateStyle,
|
|
@@ -60,9 +57,19 @@ import {
|
|
|
60
57
|
runPostRenderHooks,
|
|
61
58
|
projectState,
|
|
62
59
|
setProjectState,
|
|
60
|
+
updateFrontmatter,
|
|
61
|
+
updateUi,
|
|
62
|
+
updateSession,
|
|
63
|
+
setUpdateSessionFn,
|
|
64
|
+
setGetDocFn,
|
|
65
|
+
setGetSessionFn,
|
|
66
|
+
toFlat,
|
|
67
|
+
fromFlat,
|
|
63
68
|
} from "./store.js";
|
|
64
69
|
|
|
65
|
-
import {
|
|
70
|
+
import { view } from "./view.js";
|
|
71
|
+
|
|
72
|
+
import { renderNode as runtimeRenderNode, buildScope, defineElement } from "@jxsuite/runtime";
|
|
66
73
|
|
|
67
74
|
import {
|
|
68
75
|
startEditing,
|
|
@@ -86,8 +93,8 @@ import {
|
|
|
86
93
|
kebabToLabel,
|
|
87
94
|
propLabel,
|
|
88
95
|
attrLabel,
|
|
89
|
-
abbreviateValue,
|
|
90
96
|
inferInputType,
|
|
97
|
+
findCollectionSchema,
|
|
91
98
|
friendlyNameToVar,
|
|
92
99
|
varDisplayName,
|
|
93
100
|
parseCemType,
|
|
@@ -97,6 +104,7 @@ import {
|
|
|
97
104
|
openFile as _openFile,
|
|
98
105
|
loadMarkdown as _loadMarkdown,
|
|
99
106
|
saveFile as _saveFile,
|
|
107
|
+
exportFile as _exportFile,
|
|
100
108
|
} from "./files/file-ops.js";
|
|
101
109
|
import {
|
|
102
110
|
loadProject as _loadProject,
|
|
@@ -107,6 +115,7 @@ import {
|
|
|
107
115
|
} from "./files/files.js";
|
|
108
116
|
import { eventsSidebarTemplate as _eventsSidebarTemplate } from "./panels/events-panel.js";
|
|
109
117
|
import { renderImportsTemplate } from "./panels/imports-panel.js";
|
|
118
|
+
import { renderHeadTemplate } from "./panels/head-panel.js";
|
|
110
119
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
|
|
111
120
|
|
|
112
121
|
import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
|
|
@@ -146,7 +155,6 @@ import {
|
|
|
146
155
|
|
|
147
156
|
import { html, render as litRender, nothing } from "lit-html";
|
|
148
157
|
import { live } from "lit-html/directives/live.js";
|
|
149
|
-
import { classMap } from "lit-html/directives/class-map.js";
|
|
150
158
|
import { ref } from "lit-html/directives/ref.js";
|
|
151
159
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
152
160
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
@@ -161,10 +169,22 @@ import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
|
161
169
|
// Explicit class imports + registration — bare side-effect imports are tree-shaken
|
|
162
170
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
163
171
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
164
|
-
import
|
|
165
|
-
import {
|
|
172
|
+
import { renderFieldRow } from "./ui/field-row.js";
|
|
173
|
+
import { isColorPopoverOpen } from "./ui/color-selector.js";
|
|
174
|
+
import { widgetForType as _widgetForType } from "./ui/widgets.js";
|
|
175
|
+
import { computeInheritedStyle } from "./utils/inherited-style.js";
|
|
176
|
+
import "./ui/panel-resize.js";
|
|
177
|
+
import { showContextMenu, dismissContextMenu } from "./editor/context-menu.js";
|
|
178
|
+
import { convertToComponent } from "./editor/convert-to-component.js";
|
|
166
179
|
import { initShortcuts } from "./editor/shortcuts.js";
|
|
167
|
-
import { renderActivityBar
|
|
180
|
+
import { renderActivityBar } from "./panels/activity-bar.js";
|
|
181
|
+
import { renderBrowse } from "./browse/browse.js";
|
|
182
|
+
import { renderCollectionsEditor } from "./settings/collections-editor.js";
|
|
183
|
+
import { renderDefsEditor } from "./settings/defs-editor.js";
|
|
184
|
+
import * as toolbarPanel from "./panels/toolbar.js";
|
|
185
|
+
import * as overlaysPanel from "./panels/overlays.js";
|
|
186
|
+
import * as rightPanelMod from "./panels/right-panel.js";
|
|
187
|
+
import { mediaDisplayName, ensureLitState } from "./panels/shared.js";
|
|
168
188
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
169
189
|
|
|
170
190
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
@@ -172,7 +192,11 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
|
172
192
|
// into their own modules, they will migrate to ctx in store.js.
|
|
173
193
|
|
|
174
194
|
/** @type {any} */
|
|
175
|
-
let S; // current state
|
|
195
|
+
let S; // current state (flat compatibility view)
|
|
196
|
+
/** @type {any} */
|
|
197
|
+
let doc = null; // doc slice (persisted, history, autosave)
|
|
198
|
+
/** @type {any} */
|
|
199
|
+
let session = null; // session slice (selection, hover, ui)
|
|
176
200
|
|
|
177
201
|
/** Creates a display:contents container appended to sp-theme or body, for floating popovers/menus. */
|
|
178
202
|
function createFloatingContainer() {
|
|
@@ -182,32 +206,7 @@ function createFloatingContainer() {
|
|
|
182
206
|
return el;
|
|
183
207
|
}
|
|
184
208
|
|
|
185
|
-
const toolbar = toolbarEl;
|
|
186
|
-
|
|
187
209
|
let canvasMode = "design";
|
|
188
|
-
let panX = 0;
|
|
189
|
-
let panY = 0;
|
|
190
|
-
let needsCenter = true;
|
|
191
|
-
/** @type {ResizeObserver | null} */
|
|
192
|
-
let centerObserver = null;
|
|
193
|
-
/** @type {any} */
|
|
194
|
-
let panzoomWrap = null;
|
|
195
|
-
/** @type {any} */
|
|
196
|
-
let componentInlineEdit = null;
|
|
197
|
-
/** @type {any} */
|
|
198
|
-
let pendingInlineEdit = null;
|
|
199
|
-
/** @type {any} */
|
|
200
|
-
let monacoEditor = null;
|
|
201
|
-
/** @type {any} */
|
|
202
|
-
let functionEditor = null;
|
|
203
|
-
/** @type {any} */
|
|
204
|
-
let liveScope = null;
|
|
205
|
-
/** @type {any} */
|
|
206
|
-
let blockActionBarEl = null;
|
|
207
|
-
/** @type {any} */
|
|
208
|
-
let _inlineEditCleanup = null;
|
|
209
|
-
/** @type {any} */
|
|
210
|
-
let selDragCleanup = null;
|
|
211
210
|
|
|
212
211
|
// ─── Component registry ───────────────────────────────────────────────────────
|
|
213
212
|
|
|
@@ -247,8 +246,8 @@ async function navigateBack() {
|
|
|
247
246
|
async function closeFunctionEditor() {
|
|
248
247
|
const editing = S.ui.editingFunction;
|
|
249
248
|
if (!editing) return;
|
|
250
|
-
if (functionEditor) {
|
|
251
|
-
const currentCode = functionEditor.getValue();
|
|
249
|
+
if (view.functionEditor) {
|
|
250
|
+
const currentCode = view.functionEditor.getValue();
|
|
252
251
|
const minResult = await codeService("minify", { code: currentCode });
|
|
253
252
|
const bodyToStore = minResult?.code ?? currentCode;
|
|
254
253
|
if (editing.type === "def") {
|
|
@@ -264,27 +263,12 @@ async function closeFunctionEditor() {
|
|
|
264
263
|
}),
|
|
265
264
|
);
|
|
266
265
|
}
|
|
267
|
-
functionEditor.dispose();
|
|
268
|
-
functionEditor = null;
|
|
266
|
+
view.functionEditor.dispose();
|
|
267
|
+
view.functionEditor = null;
|
|
269
268
|
}
|
|
270
|
-
|
|
271
|
-
renderCanvas();
|
|
272
|
-
renderToolbar();
|
|
269
|
+
updateUi("editingFunction", null);
|
|
273
270
|
}
|
|
274
271
|
|
|
275
|
-
/**
|
|
276
|
-
* DnD cleanup functions from previous render — called on re-render
|
|
277
|
-
*
|
|
278
|
-
* @type {any[]}
|
|
279
|
-
*/
|
|
280
|
-
let dndCleanups = [];
|
|
281
|
-
/**
|
|
282
|
-
* Canvas DnD cleanup functions — separate from layer panel
|
|
283
|
-
*
|
|
284
|
-
* @type {any[]}
|
|
285
|
-
*/
|
|
286
|
-
let canvasDndCleanups = [];
|
|
287
|
-
|
|
288
272
|
/**
|
|
289
273
|
* Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
|
|
290
274
|
* ❯ so the runtime renders it as literal text.
|
|
@@ -482,10 +466,11 @@ function prepareForEditMode(node) {
|
|
|
482
466
|
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
483
467
|
* failure.
|
|
484
468
|
*
|
|
469
|
+
* @param {number} gen - Render generation for staleness detection
|
|
485
470
|
* @param {any} doc
|
|
486
471
|
* @param {any} canvasEl
|
|
487
472
|
*/
|
|
488
|
-
async function renderCanvasLive(doc, canvasEl) {
|
|
473
|
+
async function renderCanvasLive(gen, doc, canvasEl) {
|
|
489
474
|
canvasEl.innerHTML = "";
|
|
490
475
|
|
|
491
476
|
// Apply content mode typography styling
|
|
@@ -525,10 +510,44 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
525
510
|
}
|
|
526
511
|
|
|
527
512
|
try {
|
|
528
|
-
const
|
|
513
|
+
const root = projectState?.projectRoot || "";
|
|
514
|
+
const docPrefix = root ? `${root}/` : "";
|
|
515
|
+
const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
|
|
529
516
|
|
|
530
517
|
// Register custom elements so the runtime can render them
|
|
531
|
-
|
|
518
|
+
let effectiveElements = getEffectiveElements(renderDoc.$elements);
|
|
519
|
+
|
|
520
|
+
// In content mode (markdown), auto-discover components for directive-based
|
|
521
|
+
// custom elements that have no explicit $elements registration.
|
|
522
|
+
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
523
|
+
const existingRefs = new Set(
|
|
524
|
+
effectiveElements.map((/** @type {any} */ e) => (typeof e === "string" ? e : e?.$ref)),
|
|
525
|
+
);
|
|
526
|
+
/** @param {any} node */
|
|
527
|
+
const collectTags = (node) => {
|
|
528
|
+
/** @type {Set<string>} */
|
|
529
|
+
const tags = new Set();
|
|
530
|
+
if (!node || typeof node !== "object") return tags;
|
|
531
|
+
if (node.tagName) tags.add(node.tagName);
|
|
532
|
+
if (Array.isArray(node.children)) {
|
|
533
|
+
for (const child of node.children) {
|
|
534
|
+
for (const t of collectTags(child)) tags.add(t);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return tags;
|
|
538
|
+
};
|
|
539
|
+
for (const tag of collectTags(renderDoc)) {
|
|
540
|
+
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
541
|
+
if (comp && comp.source !== "npm") {
|
|
542
|
+
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
543
|
+
if (!existingRefs.has(relPath)) {
|
|
544
|
+
effectiveElements.push({ $ref: relPath });
|
|
545
|
+
existingRefs.add(relPath);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
532
551
|
if (effectiveElements.length) {
|
|
533
552
|
renderDoc.$elements = effectiveElements;
|
|
534
553
|
for (const entry of effectiveElements) {
|
|
@@ -553,9 +572,55 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
553
572
|
}
|
|
554
573
|
}
|
|
555
574
|
|
|
575
|
+
// Bail out if a newer render started while we were importing elements
|
|
576
|
+
if (gen !== view.renderGeneration) return null;
|
|
577
|
+
|
|
556
578
|
// Inject site-level imports so buildScope can resolve $prototype names
|
|
557
579
|
renderDoc.imports = getEffectiveImports(renderDoc.imports);
|
|
558
580
|
|
|
581
|
+
// Apply project-level styles mirroring the compiler convention:
|
|
582
|
+
// viewport ≈ :root → CSS custom properties (they inherit down)
|
|
583
|
+
// canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
|
|
584
|
+
// This ensures project font-family, color, etc. override the
|
|
585
|
+
// content-mode fallback typography rules in the stylesheet.
|
|
586
|
+
// In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
|
|
587
|
+
const viewport = canvasEl.closest(".canvas-panel-viewport");
|
|
588
|
+
const editSurface = canvasMode === "edit" ? canvasEl.closest(".content-edit-canvas") : null;
|
|
589
|
+
const siteStyle = projectState?.projectConfig?.style;
|
|
590
|
+
if (viewport) {
|
|
591
|
+
viewport.style.cssText = "";
|
|
592
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
593
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
594
|
+
if (k.startsWith("--")) {
|
|
595
|
+
viewport.style.setProperty(k, String(v));
|
|
596
|
+
} else {
|
|
597
|
+
/** @type {any} */ (viewport.style)[k] = v;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (editSurface) {
|
|
603
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
604
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
605
|
+
if (k.startsWith("--")) {
|
|
606
|
+
/** @type {any} */ (editSurface).style.setProperty(k, String(v));
|
|
607
|
+
} else {
|
|
608
|
+
/** @type {any} */ (editSurface.style)[k] = v;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
614
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
615
|
+
if (!k.startsWith("--")) {
|
|
616
|
+
/** @type {any} */ (canvasEl.style)[k] = v;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Inject site-level $media so runtime can resolve media queries in styles
|
|
622
|
+
renderDoc.$media = getEffectiveMedia(renderDoc.$media);
|
|
623
|
+
|
|
559
624
|
// Inject $head elements (link/meta/script) into document.head
|
|
560
625
|
const effectiveHead = getEffectiveHead(renderDoc.$head);
|
|
561
626
|
if (effectiveHead.length) {
|
|
@@ -584,6 +649,8 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
584
649
|
}
|
|
585
650
|
|
|
586
651
|
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
652
|
+
// Bail out if a newer render started while buildScope was running
|
|
653
|
+
if (gen !== view.renderGeneration) return null;
|
|
587
654
|
const el = /** @type {HTMLElement} */ (
|
|
588
655
|
runtimeRenderNode(renderDoc, $defs, {
|
|
589
656
|
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
@@ -632,7 +699,7 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
632
699
|
const editingEl = getActiveElement();
|
|
633
700
|
for (const child of canvasEl.querySelectorAll("*")) {
|
|
634
701
|
// Preserve pointer-events on the actively-edited element
|
|
635
|
-
if (componentInlineEdit && child === componentInlineEdit.el) continue;
|
|
702
|
+
if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
|
|
636
703
|
if (editingEl && child === editingEl) continue;
|
|
637
704
|
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
638
705
|
}
|
|
@@ -640,7 +707,7 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
640
707
|
}
|
|
641
708
|
return $defs;
|
|
642
709
|
} catch (/** @type {any} */ err) {
|
|
643
|
-
console.warn("
|
|
710
|
+
console.warn("renderCanvasLive failed:", err.message, err);
|
|
644
711
|
return null;
|
|
645
712
|
}
|
|
646
713
|
}
|
|
@@ -666,40 +733,11 @@ litRender(
|
|
|
666
733
|
const cssInitialMap = new Map(/** @type {any} */ (webdata.cssProps));
|
|
667
734
|
|
|
668
735
|
// Persistent render hosts for lit-html (must be before bootstrap/render)
|
|
669
|
-
|
|
736
|
+
let zoomIndicatorHost = document.createElement("div");
|
|
670
737
|
zoomIndicatorHost.style.display = "contents";
|
|
671
738
|
document.body.appendChild(zoomIndicatorHost);
|
|
672
739
|
|
|
673
|
-
// ───
|
|
674
|
-
|
|
675
|
-
const toolbarIconMap = /** @type {Record<string, any>} */ ({
|
|
676
|
-
"sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
|
|
677
|
-
"sp-icon-save-floppy": html`<sp-icon-save-floppy slot="icon"></sp-icon-save-floppy>`,
|
|
678
|
-
"sp-icon-back": html`<sp-icon-back slot="icon"></sp-icon-back>`,
|
|
679
|
-
"sp-icon-undo": html`<sp-icon-undo slot="icon"></sp-icon-undo>`,
|
|
680
|
-
"sp-icon-redo": html`<sp-icon-redo slot="icon"></sp-icon-redo>`,
|
|
681
|
-
"sp-icon-duplicate": html`<sp-icon-duplicate slot="icon"></sp-icon-duplicate>`,
|
|
682
|
-
"sp-icon-delete": html`<sp-icon-delete slot="icon"></sp-icon-delete>`,
|
|
683
|
-
"sp-icon-edit": html`<sp-icon-edit slot="icon"></sp-icon-edit>`,
|
|
684
|
-
"sp-icon-artboard": html`<sp-icon-artboard slot="icon"></sp-icon-artboard>`,
|
|
685
|
-
"sp-icon-preview": html`<sp-icon-preview slot="icon"></sp-icon-preview>`,
|
|
686
|
-
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
687
|
-
"sp-icon-brush": html`<sp-icon-brush slot="icon"></sp-icon-brush>`,
|
|
688
|
-
"sp-icon-document": html`<sp-icon-document slot="icon"></sp-icon-document>`,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* @param {any} label
|
|
693
|
-
* @param {any} onClick
|
|
694
|
-
* @param {any} iconTag
|
|
695
|
-
*/
|
|
696
|
-
function tbBtnTpl(label, onClick, iconTag) {
|
|
697
|
-
return html`
|
|
698
|
-
<sp-action-button size="s" @click=${onClick}>
|
|
699
|
-
${iconTag ? toolbarIconMap[iconTag] : nothing} ${label}
|
|
700
|
-
</sp-action-button>
|
|
701
|
-
`;
|
|
702
|
-
}
|
|
740
|
+
// ─── Module-level UI state (must be before render() call) ─────────────────────
|
|
703
741
|
|
|
704
742
|
let elementsCollapsed = new Set();
|
|
705
743
|
let elementsFilter = "";
|
|
@@ -721,43 +759,125 @@ const EMPTY_DOC = {
|
|
|
721
759
|
};
|
|
722
760
|
|
|
723
761
|
S = createState(structuredClone(EMPTY_DOC));
|
|
762
|
+
({ doc, session } = fromFlat(S));
|
|
724
763
|
|
|
725
764
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
726
765
|
|
|
766
|
+
// Mount extracted panel modules
|
|
767
|
+
toolbarPanel.mount(toolbarEl, {
|
|
768
|
+
navigateBack: () => navigateBack(),
|
|
769
|
+
closeFunctionEditor: () => closeFunctionEditor(),
|
|
770
|
+
openProject: () => openProject(),
|
|
771
|
+
openFile: () => openFile(),
|
|
772
|
+
saveFile: () => saveFile(),
|
|
773
|
+
parseMediaEntries,
|
|
774
|
+
getCanvasMode: () => canvasMode,
|
|
775
|
+
setCanvasMode: (/** @type {any} */ m) => {
|
|
776
|
+
canvasMode = m;
|
|
777
|
+
},
|
|
778
|
+
renderCanvas: () => renderCanvas(),
|
|
779
|
+
safeRenderRightPanel: () => safeRenderRightPanel(),
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
overlaysPanel.mount({
|
|
783
|
+
effectiveZoom,
|
|
784
|
+
getCanvasMode: () => canvasMode,
|
|
785
|
+
isEditing,
|
|
786
|
+
renderBlockActionBar,
|
|
787
|
+
findCanvasElement,
|
|
788
|
+
getActivePanel,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
rightPanelMod.mount({
|
|
792
|
+
propertiesSidebarTemplate,
|
|
793
|
+
renderStylePanelTemplate,
|
|
794
|
+
renderCanvas: () => renderCanvas(),
|
|
795
|
+
updateForcedPseudoPreview,
|
|
796
|
+
});
|
|
797
|
+
|
|
727
798
|
// Register all renderers with the store so render()/renderOnly() work
|
|
728
|
-
registerRenderer("toolbar", () =>
|
|
799
|
+
registerRenderer("toolbar", () => toolbarPanel.render());
|
|
729
800
|
registerRenderer("activityBar", () => renderActivityBar(S));
|
|
730
801
|
registerRenderer("leftPanel", () => renderLeftPanel());
|
|
731
802
|
registerRenderer("canvas", () => renderCanvas());
|
|
732
|
-
registerRenderer("rightPanel", () =>
|
|
733
|
-
registerRenderer("overlays", () =>
|
|
803
|
+
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
804
|
+
registerRenderer("overlays", () => overlaysPanel.render());
|
|
734
805
|
registerRenderer("statusbar", () => renderStatusbar(S));
|
|
735
806
|
setStatusbarRenderer(() => renderStatusbar(S));
|
|
736
807
|
|
|
808
|
+
function safeRenderLeftPanel() {
|
|
809
|
+
try {
|
|
810
|
+
ensureLitState(leftPanel);
|
|
811
|
+
renderLeftPanel();
|
|
812
|
+
} catch (e) {
|
|
813
|
+
console.error("renderLeftPanel error:", e);
|
|
814
|
+
try {
|
|
815
|
+
leftPanel.textContent = "";
|
|
816
|
+
// @ts-ignore
|
|
817
|
+
delete leftPanel["_$litPart$"];
|
|
818
|
+
renderLeftPanel();
|
|
819
|
+
} catch (e2) {
|
|
820
|
+
console.error("renderLeftPanel retry failed:", e2);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function safeRenderRightPanel() {
|
|
826
|
+
rightPanelMod.render();
|
|
827
|
+
}
|
|
828
|
+
|
|
737
829
|
// Register the update implementation with the store
|
|
738
830
|
setGetStateFn(() => S);
|
|
739
831
|
setUpdateFn(function _update(/** @type {any} */ newState) {
|
|
832
|
+
const prev = S;
|
|
740
833
|
const prevDoc = S.document;
|
|
741
834
|
const prevSel = S.selection;
|
|
742
835
|
S = newState;
|
|
743
836
|
|
|
744
|
-
|
|
837
|
+
// Keep doc/session slices in sync with flat S
|
|
838
|
+
({ doc, session } = fromFlat(S));
|
|
839
|
+
|
|
840
|
+
const docChanged = prevDoc !== S.document;
|
|
841
|
+
const selChanged = !pathsEqual(prevSel, S.selection);
|
|
842
|
+
const modeChanged = prev.mode !== S.mode;
|
|
843
|
+
const uiChanged = prev.ui !== S.ui;
|
|
844
|
+
|
|
845
|
+
const canvasUiChanged =
|
|
846
|
+
uiChanged &&
|
|
847
|
+
(prev.ui?.editingFunction !== S.ui?.editingFunction ||
|
|
848
|
+
prev.ui?.settingsTab !== S.ui?.settingsTab ||
|
|
849
|
+
prev.ui?.stylebookTab !== S.ui?.stylebookTab ||
|
|
850
|
+
prev.ui?.stylebookFilter !== S.ui?.stylebookFilter ||
|
|
851
|
+
prev.ui?.stylebookCustomizedOnly !== S.ui?.stylebookCustomizedOnly ||
|
|
852
|
+
prev.ui?.featureToggles !== S.ui?.featureToggles);
|
|
853
|
+
const leftUiChanged =
|
|
854
|
+
uiChanged && (prev.ui?.leftTab !== S.ui?.leftTab || prev.ui?.settingsTab !== S.ui?.settingsTab);
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
renderToolbar();
|
|
858
|
+
} catch (e) {
|
|
859
|
+
console.error("renderToolbar error:", e);
|
|
860
|
+
}
|
|
745
861
|
|
|
746
|
-
if (
|
|
862
|
+
if (docChanged || modeChanged || canvasUiChanged) {
|
|
747
863
|
try {
|
|
748
864
|
renderCanvas();
|
|
749
865
|
} catch (e) {
|
|
750
|
-
console.
|
|
866
|
+
console.error("renderCanvas error:", e);
|
|
751
867
|
}
|
|
752
|
-
|
|
753
|
-
} else if (
|
|
754
|
-
|
|
868
|
+
safeRenderLeftPanel();
|
|
869
|
+
} else if (selChanged || leftUiChanged) {
|
|
870
|
+
safeRenderLeftPanel();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (uiChanged && prev.ui?.activeMedia !== S.ui?.activeMedia) {
|
|
874
|
+
updateActivePanelHeaders();
|
|
755
875
|
}
|
|
756
876
|
|
|
757
877
|
// Skip right-panel rebuild when an input inside it is focused (user is typing)
|
|
758
878
|
// unless the selection changed — that always needs a full re-render
|
|
759
879
|
// Also re-render when color popover is open (changes come from outside rightPanel)
|
|
760
|
-
const colorPopoverOpen =
|
|
880
|
+
const colorPopoverOpen = isColorPopoverOpen();
|
|
761
881
|
const activeTag = document.activeElement?.tagName;
|
|
762
882
|
const rightHasFocus =
|
|
763
883
|
!colorPopoverOpen &&
|
|
@@ -769,27 +889,98 @@ setUpdateFn(function _update(/** @type {any} */ newState) {
|
|
|
769
889
|
activeTag === "SP-PICKER" ||
|
|
770
890
|
activeTag === "SP-COMBOBOX" ||
|
|
771
891
|
activeTag === "SP-SEARCH");
|
|
772
|
-
if (!rightHasFocus ||
|
|
773
|
-
|
|
892
|
+
if (!rightHasFocus || selChanged || uiChanged) {
|
|
893
|
+
safeRenderRightPanel();
|
|
774
894
|
}
|
|
775
|
-
renderOverlays();
|
|
776
|
-
renderStatusbar(S);
|
|
777
895
|
|
|
778
|
-
|
|
779
|
-
|
|
896
|
+
try {
|
|
897
|
+
renderOverlays();
|
|
898
|
+
} catch (e) {
|
|
899
|
+
console.error("renderOverlays error:", e);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
renderStatusbar(S);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
console.error("renderStatusbar error:", e);
|
|
905
|
+
}
|
|
780
906
|
|
|
781
|
-
|
|
907
|
+
runPostRenderHooks(prevDoc, prevSel);
|
|
782
908
|
runUpdateMiddleware(S);
|
|
783
909
|
});
|
|
784
910
|
|
|
911
|
+
// Register session dispatch — lightweight path for selection/hover/ui changes
|
|
912
|
+
setGetDocFn(() => doc);
|
|
913
|
+
setGetSessionFn(() => session);
|
|
914
|
+
setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
915
|
+
const prev = session;
|
|
916
|
+
session = { ...session, ...patch };
|
|
917
|
+
if (patch.ui) {
|
|
918
|
+
session.ui = { ...prev.ui, ...patch.ui };
|
|
919
|
+
}
|
|
920
|
+
S = toFlat(doc, session);
|
|
921
|
+
|
|
922
|
+
const selChanged = !pathsEqual(prev.selection, session.selection);
|
|
923
|
+
const uiChanged = prev.ui !== session.ui;
|
|
924
|
+
|
|
925
|
+
const canvasUiChanged =
|
|
926
|
+
uiChanged &&
|
|
927
|
+
(prev.ui?.editingFunction !== session.ui?.editingFunction ||
|
|
928
|
+
prev.ui?.settingsTab !== session.ui?.settingsTab ||
|
|
929
|
+
prev.ui?.stylebookTab !== session.ui?.stylebookTab ||
|
|
930
|
+
prev.ui?.stylebookFilter !== session.ui?.stylebookFilter ||
|
|
931
|
+
prev.ui?.stylebookCustomizedOnly !== session.ui?.stylebookCustomizedOnly ||
|
|
932
|
+
prev.ui?.featureToggles !== session.ui?.featureToggles);
|
|
933
|
+
const leftUiChanged =
|
|
934
|
+
uiChanged &&
|
|
935
|
+
(prev.ui?.leftTab !== session.ui?.leftTab || prev.ui?.settingsTab !== session.ui?.settingsTab);
|
|
936
|
+
|
|
937
|
+
try {
|
|
938
|
+
renderToolbar();
|
|
939
|
+
} catch (e) {
|
|
940
|
+
console.error("renderToolbar error:", e);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (canvasUiChanged) {
|
|
944
|
+
try {
|
|
945
|
+
renderCanvas();
|
|
946
|
+
} catch (e) {
|
|
947
|
+
console.error("renderCanvas error:", e);
|
|
948
|
+
}
|
|
949
|
+
safeRenderLeftPanel();
|
|
950
|
+
} else if (selChanged || leftUiChanged) {
|
|
951
|
+
safeRenderLeftPanel();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (uiChanged && prev.ui?.activeMedia !== session.ui?.activeMedia) {
|
|
955
|
+
updateActivePanelHeaders();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (selChanged || uiChanged) {
|
|
959
|
+
safeRenderRightPanel();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
try {
|
|
963
|
+
renderOverlays();
|
|
964
|
+
} catch (e) {
|
|
965
|
+
console.error("renderOverlays error:", e);
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
renderStatusbar(S);
|
|
969
|
+
} catch (e) {
|
|
970
|
+
console.error("renderStatusbar error:", e);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
runPostRenderHooks(doc.document, prev.selection);
|
|
974
|
+
});
|
|
975
|
+
|
|
785
976
|
// Register post-render hook for pseudo-state preview
|
|
786
977
|
addPostRenderHook(() => updateForcedPseudoPreview());
|
|
787
978
|
|
|
788
979
|
// Register post-render hook for pending inline edit
|
|
789
980
|
addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
790
|
-
if (pendingInlineEdit && prevDoc === S.document) {
|
|
791
|
-
const { path, mediaName: mn } = pendingInlineEdit;
|
|
792
|
-
pendingInlineEdit = null;
|
|
981
|
+
if (view.pendingInlineEdit && prevDoc === S.document) {
|
|
982
|
+
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
983
|
+
view.pendingInlineEdit = null;
|
|
793
984
|
const targetPanel =
|
|
794
985
|
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
795
986
|
if (targetPanel) {
|
|
@@ -806,7 +997,9 @@ const _openParam = new URLSearchParams(location.search).get("open");
|
|
|
806
997
|
|
|
807
998
|
if (_openParam) {
|
|
808
999
|
// ?open= mode: skip normal loadProject, set up site context from the path
|
|
809
|
-
|
|
1000
|
+
const isAbsPath =
|
|
1001
|
+
_openParam.startsWith("/") || _openParam.startsWith("~") || /^[A-Za-z]:[/\\]/.test(_openParam);
|
|
1002
|
+
if (!isAbsPath) {
|
|
810
1003
|
statusMessage(`Error: ?open= requires an absolute path (got "${_openParam}")`);
|
|
811
1004
|
render();
|
|
812
1005
|
} else {
|
|
@@ -819,13 +1012,17 @@ if (_openParam) {
|
|
|
819
1012
|
: { sitePath: null };
|
|
820
1013
|
|
|
821
1014
|
if (siteCtx.sitePath) {
|
|
822
|
-
// Set PAL project root to
|
|
823
|
-
if (siteCtx.
|
|
1015
|
+
// Set PAL project root to absolute path so file ops work
|
|
1016
|
+
if (siteCtx.sitePath) {
|
|
1017
|
+
platform.projectRoot = siteCtx.sitePath;
|
|
1018
|
+
// Await activation so the server resolves project-relative static files
|
|
1019
|
+
if (platform.activate) await platform.activate();
|
|
1020
|
+
}
|
|
824
1021
|
|
|
825
1022
|
setProjectState({
|
|
826
1023
|
root: siteCtx.sitePath,
|
|
827
1024
|
name: siteCtx.projectConfig?.name || "Project",
|
|
828
|
-
projectRoot: siteCtx.
|
|
1025
|
+
projectRoot: siteCtx.sitePath,
|
|
829
1026
|
isSiteProject: true,
|
|
830
1027
|
projectConfig: siteCtx.projectConfig,
|
|
831
1028
|
projectDirs: [],
|
|
@@ -837,27 +1034,40 @@ if (_openParam) {
|
|
|
837
1034
|
|
|
838
1035
|
await loadComponentRegistry();
|
|
839
1036
|
|
|
840
|
-
// Load directory tree
|
|
1037
|
+
// Load directory tree and populate projectDirs from conventional dirs found
|
|
1038
|
+
const conventionalDirs = [
|
|
1039
|
+
"pages",
|
|
1040
|
+
"layouts",
|
|
1041
|
+
"components",
|
|
1042
|
+
"content",
|
|
1043
|
+
"data",
|
|
1044
|
+
"public",
|
|
1045
|
+
"styles",
|
|
1046
|
+
];
|
|
841
1047
|
const dirEntries = await platform.listDirectory(".");
|
|
842
1048
|
projectState.dirs.set(".", dirEntries);
|
|
1049
|
+
const foundDirs = [];
|
|
843
1050
|
for (const e of dirEntries) {
|
|
844
|
-
if (e.type === "directory" &&
|
|
1051
|
+
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
1052
|
+
foundDirs.push(e.name);
|
|
845
1053
|
projectState.expanded.add(e.path || e.name);
|
|
846
1054
|
const sub = await platform.listDirectory(e.path || e.name);
|
|
847
1055
|
projectState.dirs.set(e.path || e.name, sub);
|
|
848
1056
|
}
|
|
849
1057
|
}
|
|
1058
|
+
projectState.projectDirs = foundDirs;
|
|
850
1059
|
}
|
|
851
1060
|
|
|
852
1061
|
// Read and open the file
|
|
853
1062
|
const fileRelPath = siteCtx.fileRelPath || _openParam;
|
|
854
1063
|
const content = await platform.readFile(fileRelPath);
|
|
855
1064
|
if (content) {
|
|
856
|
-
const
|
|
857
|
-
S = createState(
|
|
1065
|
+
const parsed = JSON.parse(content);
|
|
1066
|
+
S = createState(parsed);
|
|
858
1067
|
S.dirty = false;
|
|
859
1068
|
S.documentPath = fileRelPath;
|
|
860
1069
|
S.ui = { ...S.ui, leftTab: "files" };
|
|
1070
|
+
({ doc, session } = fromFlat(S));
|
|
861
1071
|
render();
|
|
862
1072
|
statusMessage(`Opened ${_openParam}`);
|
|
863
1073
|
}
|
|
@@ -954,9 +1164,55 @@ function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles) {
|
|
|
954
1164
|
}
|
|
955
1165
|
}
|
|
956
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* After a runtime render, apply active media overrides as inline styles so they beat the base
|
|
1169
|
+
* inline styles the runtime already set. The runtime uses @media CSS rules for overrides, but those
|
|
1170
|
+
* can never beat inline base styles.
|
|
1171
|
+
*
|
|
1172
|
+
* @param {Element} canvasEl
|
|
1173
|
+
* @param {Set<string>} activeBreakpoints
|
|
1174
|
+
*/
|
|
1175
|
+
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
1176
|
+
if (!activeBreakpoints.size) return;
|
|
1177
|
+
for (const el of /** @type {NodeListOf<HTMLElement>} */ (canvasEl.querySelectorAll("*"))) {
|
|
1178
|
+
const path = elToPath.get(el);
|
|
1179
|
+
if (!path) continue;
|
|
1180
|
+
const node = getNodeAtPath(S.document, path);
|
|
1181
|
+
if (!node?.style) continue;
|
|
1182
|
+
for (const [key, val] of Object.entries(node.style)) {
|
|
1183
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
1184
|
+
const mediaName = key.slice(1);
|
|
1185
|
+
if (mediaName === "--") continue;
|
|
1186
|
+
if (!activeBreakpoints.has(mediaName)) continue;
|
|
1187
|
+
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
1188
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
1189
|
+
try {
|
|
1190
|
+
if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
|
|
1191
|
+
else /** @type {any} */ (el.style)[prop] = v;
|
|
1192
|
+
} catch {}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
957
1199
|
// ─── Canvas ───────────────────────────────────────────────────────────────────
|
|
958
1200
|
|
|
959
1201
|
function renderCanvas() {
|
|
1202
|
+
// Advance render generation so stale async renders from the previous cycle bail out
|
|
1203
|
+
++view.renderGeneration;
|
|
1204
|
+
|
|
1205
|
+
// Always clear Lit's internal state so it builds fresh DOM. Stale async
|
|
1206
|
+
// renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
|
|
1207
|
+
// markers (Comment nodes inside panzoom-wrap) in ways the root-only
|
|
1208
|
+
// ensureLitState check cannot detect.
|
|
1209
|
+
// @ts-ignore
|
|
1210
|
+
if (canvasWrap["_$litPart$"]) {
|
|
1211
|
+
canvasWrap.textContent = "";
|
|
1212
|
+
// @ts-ignore
|
|
1213
|
+
delete canvasWrap["_$litPart$"];
|
|
1214
|
+
}
|
|
1215
|
+
|
|
960
1216
|
// Function editor mode: editing a function body in Monaco (JS)
|
|
961
1217
|
if (S.ui.editingFunction) {
|
|
962
1218
|
renderFunctionEditor();
|
|
@@ -964,68 +1220,125 @@ function renderCanvas() {
|
|
|
964
1220
|
}
|
|
965
1221
|
|
|
966
1222
|
// Dispose function editor if switching away
|
|
967
|
-
if (functionEditor) {
|
|
968
|
-
functionEditor.dispose();
|
|
969
|
-
functionEditor = null;
|
|
1223
|
+
if (view.functionEditor) {
|
|
1224
|
+
view.functionEditor.dispose();
|
|
1225
|
+
view.functionEditor = null;
|
|
970
1226
|
}
|
|
971
1227
|
|
|
972
1228
|
// Source mode: update existing Monaco editor without recreating
|
|
973
|
-
if (canvasMode === "source" && monacoEditor) {
|
|
1229
|
+
if (canvasMode === "source" && view.monacoEditor) {
|
|
974
1230
|
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
975
|
-
const currentVal = monacoEditor.getValue();
|
|
1231
|
+
const currentVal = view.monacoEditor.getValue();
|
|
976
1232
|
if (currentVal !== jsonStr) {
|
|
977
1233
|
// Prevent triggering the onChange handler for this programmatic update
|
|
978
|
-
monacoEditor._ignoreNextChange = true;
|
|
979
|
-
monacoEditor.setValue(jsonStr);
|
|
1234
|
+
view.monacoEditor._ignoreNextChange = true;
|
|
1235
|
+
view.monacoEditor.setValue(jsonStr);
|
|
980
1236
|
}
|
|
981
1237
|
return;
|
|
982
1238
|
}
|
|
983
1239
|
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
canvasDndCleanups
|
|
1240
|
+
// Detect whether this is a mode transition or a content-only re-render
|
|
1241
|
+
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
1242
|
+
view.prevCanvasMode = canvasMode;
|
|
1243
|
+
|
|
1244
|
+
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
1245
|
+
// content render, so always clean them up.
|
|
1246
|
+
for (const fn of view.canvasDndCleanups) fn();
|
|
1247
|
+
view.canvasDndCleanups = [];
|
|
1248
|
+
|
|
1249
|
+
// Panel event handlers (click, dblclick, etc.) capture closures over panel references.
|
|
1250
|
+
// Always re-register to keep closures fresh across document switches.
|
|
1251
|
+
for (const fn of view.canvasEventCleanups) fn();
|
|
1252
|
+
view.canvasEventCleanups = [];
|
|
1253
|
+
|
|
1254
|
+
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
1255
|
+
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
991
1256
|
canvasPanels.length = 0;
|
|
992
1257
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1258
|
+
if (modeChanged) {
|
|
1259
|
+
// Full teardown on mode transitions — new panel structure needed
|
|
1260
|
+
if (view.centerObserver) {
|
|
1261
|
+
view.centerObserver.disconnect();
|
|
1262
|
+
view.centerObserver = null;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Dispose Monaco editor if switching away from source mode
|
|
1266
|
+
if (view.monacoEditor) {
|
|
1267
|
+
view.monacoEditor.dispose();
|
|
1268
|
+
view.monacoEditor = null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
litRender(nothing, canvasWrap);
|
|
1272
|
+
view.panzoomWrap = null;
|
|
1273
|
+
// Reset inline style overrides from other modes
|
|
1274
|
+
canvasWrap.style.padding = "";
|
|
1275
|
+
canvasWrap.style.alignItems = "";
|
|
1276
|
+
canvasWrap.style.display = "";
|
|
1277
|
+
canvasWrap.style.overflow = "";
|
|
1278
|
+
canvasWrap.style.overflow = "";
|
|
1279
|
+
|
|
1280
|
+
// Clear zoom indicator (only re-rendered by design/preview/stylebook)
|
|
1281
|
+
try {
|
|
1282
|
+
litRender(nothing, zoomIndicatorHost);
|
|
1283
|
+
} catch {
|
|
1284
|
+
const newHost = document.createElement("div");
|
|
1285
|
+
newHost.style.display = "contents";
|
|
1286
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1287
|
+
zoomIndicatorHost = newHost;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Dismiss open popovers/toolbars that are no longer relevant
|
|
1291
|
+
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
1292
|
+
dismissLinkPopover();
|
|
1293
|
+
dismissContextMenu();
|
|
1294
|
+
sharedDismissSlashMenu();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Manage mode: project-level file browser table
|
|
1298
|
+
if (canvasMode === "manage") {
|
|
1299
|
+
canvasWrap.style.padding = "0";
|
|
1300
|
+
canvasWrap.style.overflow = "auto";
|
|
1301
|
+
renderBrowse(canvasWrap, {
|
|
1302
|
+
openFile: (/** @type {string} */ path) => {
|
|
1303
|
+
canvasMode = "edit";
|
|
1304
|
+
openFileFromTree(path);
|
|
1305
|
+
},
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
997
1308
|
}
|
|
998
1309
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
canvasWrap.style.padding = "";
|
|
1003
|
-
canvasWrap.style.alignItems = "";
|
|
1004
|
-
canvasWrap.style.overflow = "";
|
|
1005
|
-
|
|
1006
|
-
// Stylebook mode: render element catalog with panzoom surface
|
|
1007
|
-
if (canvasMode === "stylebook") {
|
|
1008
|
-
renderStylebook();
|
|
1310
|
+
// Settings mode: render element catalog with panzoom surface
|
|
1311
|
+
if (canvasMode === "settings") {
|
|
1312
|
+
renderSettings();
|
|
1009
1313
|
return;
|
|
1010
1314
|
}
|
|
1011
1315
|
|
|
1012
1316
|
// Source mode: create Monaco editor instead of canvas
|
|
1013
1317
|
if (canvasMode === "source") {
|
|
1014
1318
|
canvasWrap.style.padding = "0";
|
|
1319
|
+
canvasWrap.style.display = "block";
|
|
1015
1320
|
/** @type {HTMLDivElement | null} */
|
|
1016
1321
|
let editorContainer = null;
|
|
1017
1322
|
litRender(
|
|
1018
|
-
html`<div
|
|
1019
|
-
class="source-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1323
|
+
html`<div class="source-wrap">
|
|
1324
|
+
<div class="source-toolbar">
|
|
1325
|
+
<sp-action-button size="s" @click=${exportFile}>
|
|
1326
|
+
<sp-icon-export slot="icon"></sp-icon-export>
|
|
1327
|
+
Export
|
|
1328
|
+
</sp-action-button>
|
|
1329
|
+
</div>
|
|
1330
|
+
<div
|
|
1331
|
+
class="source-editor"
|
|
1332
|
+
${ref((el) => {
|
|
1333
|
+
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
1334
|
+
})}
|
|
1335
|
+
></div>
|
|
1336
|
+
</div>`,
|
|
1024
1337
|
canvasWrap,
|
|
1025
1338
|
);
|
|
1026
1339
|
|
|
1027
1340
|
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
1028
|
-
monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1341
|
+
view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1029
1342
|
value: jsonStr,
|
|
1030
1343
|
language: "json",
|
|
1031
1344
|
theme: "vs-dark",
|
|
@@ -1042,19 +1355,16 @@ function renderCanvas() {
|
|
|
1042
1355
|
// Debounced sync back to state
|
|
1043
1356
|
/** @type {any} */
|
|
1044
1357
|
let debounce;
|
|
1045
|
-
monacoEditor.onDidChangeModelContent(() => {
|
|
1046
|
-
if (monacoEditor._ignoreNextChange) {
|
|
1047
|
-
monacoEditor._ignoreNextChange = false;
|
|
1358
|
+
view.monacoEditor.onDidChangeModelContent(() => {
|
|
1359
|
+
if (view.monacoEditor._ignoreNextChange) {
|
|
1360
|
+
view.monacoEditor._ignoreNextChange = false;
|
|
1048
1361
|
return;
|
|
1049
1362
|
}
|
|
1050
1363
|
clearTimeout(debounce);
|
|
1051
1364
|
debounce = setTimeout(() => {
|
|
1052
1365
|
try {
|
|
1053
|
-
const parsed = JSON.parse(monacoEditor.getValue());
|
|
1054
|
-
|
|
1055
|
-
renderToolbar();
|
|
1056
|
-
renderLeftPanel();
|
|
1057
|
-
renderRightPanel();
|
|
1366
|
+
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
1367
|
+
update({ ...S, document: parsed, dirty: true });
|
|
1058
1368
|
} catch {
|
|
1059
1369
|
// Invalid JSON — don't update state
|
|
1060
1370
|
}
|
|
@@ -1065,33 +1375,38 @@ function renderCanvas() {
|
|
|
1065
1375
|
|
|
1066
1376
|
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
1067
1377
|
if (canvasMode === "edit") {
|
|
1068
|
-
|
|
1069
|
-
|
|
1378
|
+
if (modeChanged) {
|
|
1379
|
+
canvasWrap.style.padding = "0";
|
|
1380
|
+
canvasWrap.style.overflow = "hidden";
|
|
1070
1381
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1382
|
+
// Remove zoom indicator left over from design/preview mode
|
|
1383
|
+
try {
|
|
1384
|
+
litRender(nothing, zoomIndicatorHost);
|
|
1385
|
+
} catch {
|
|
1386
|
+
const newHost = document.createElement("div");
|
|
1387
|
+
newHost.style.display = "contents";
|
|
1388
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1389
|
+
zoomIndicatorHost = newHost;
|
|
1390
|
+
}
|
|
1076
1391
|
}
|
|
1077
1392
|
|
|
1078
1393
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
<div class="content-edit-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
canvasWrap,
|
|
1086
|
-
);
|
|
1394
|
+
const editTpl = html`
|
|
1395
|
+
<div class="content-edit-canvas">
|
|
1396
|
+
<div class="content-edit-column">${panelTpl}</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
`;
|
|
1399
|
+
litRender(editTpl, canvasWrap);
|
|
1087
1400
|
canvasPanels.push(panel);
|
|
1088
1401
|
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
1089
1402
|
return;
|
|
1090
1403
|
}
|
|
1091
1404
|
|
|
1092
1405
|
// Normal canvas mode (design / preview) — set up panzoom surface
|
|
1093
|
-
|
|
1094
|
-
|
|
1406
|
+
if (modeChanged) {
|
|
1407
|
+
canvasWrap.style.padding = "0";
|
|
1408
|
+
canvasWrap.style.overflow = "hidden";
|
|
1409
|
+
}
|
|
1095
1410
|
|
|
1096
1411
|
const {
|
|
1097
1412
|
sizeBreakpoints,
|
|
@@ -1119,7 +1434,7 @@ function renderCanvas() {
|
|
|
1119
1434
|
class="panzoom-wrap"
|
|
1120
1435
|
style="transform-origin:0 0"
|
|
1121
1436
|
${ref((el) => {
|
|
1122
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1437
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1123
1438
|
})}
|
|
1124
1439
|
>
|
|
1125
1440
|
${panelTpl}
|
|
@@ -1130,12 +1445,15 @@ function renderCanvas() {
|
|
|
1130
1445
|
canvasPanels.push(panel);
|
|
1131
1446
|
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
1132
1447
|
applyTransform();
|
|
1133
|
-
|
|
1448
|
+
if (modeChanged) {
|
|
1449
|
+
observeCenterUntilStable();
|
|
1450
|
+
}
|
|
1134
1451
|
renderZoomIndicator();
|
|
1135
1452
|
return;
|
|
1136
1453
|
}
|
|
1137
1454
|
|
|
1138
|
-
// Build all panels
|
|
1455
|
+
// Build all panels: base first, then breakpoints in declared order (ascending for min-width,
|
|
1456
|
+
// descending for max-width — matching the direction of the design's media queries).
|
|
1139
1457
|
const allPanelDefs = [
|
|
1140
1458
|
{
|
|
1141
1459
|
name: "base",
|
|
@@ -1152,7 +1470,6 @@ function renderCanvas() {
|
|
|
1152
1470
|
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
1153
1471
|
});
|
|
1154
1472
|
}
|
|
1155
|
-
allPanelDefs.sort((a, b) => b.width - a.width);
|
|
1156
1473
|
|
|
1157
1474
|
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
1158
1475
|
const panelEntries = allPanelDefs.map((def) => {
|
|
@@ -1167,7 +1484,7 @@ function renderCanvas() {
|
|
|
1167
1484
|
class="panzoom-wrap"
|
|
1168
1485
|
style="transform-origin:0 0"
|
|
1169
1486
|
${ref((el) => {
|
|
1170
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1487
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1171
1488
|
})}
|
|
1172
1489
|
>
|
|
1173
1490
|
${panelEntries.map((e) => e.tpl)}
|
|
@@ -1186,7 +1503,9 @@ function renderCanvas() {
|
|
|
1186
1503
|
|
|
1187
1504
|
// Apply current zoom + pan transform
|
|
1188
1505
|
applyTransform();
|
|
1189
|
-
|
|
1506
|
+
if (modeChanged) {
|
|
1507
|
+
observeCenterUntilStable();
|
|
1508
|
+
}
|
|
1190
1509
|
|
|
1191
1510
|
// Floating zoom indicator
|
|
1192
1511
|
renderZoomIndicator();
|
|
@@ -1201,9 +1520,13 @@ function renderCanvas() {
|
|
|
1201
1520
|
* @param {any} featureToggles
|
|
1202
1521
|
*/
|
|
1203
1522
|
function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
1204
|
-
|
|
1523
|
+
const gen = view.renderGeneration;
|
|
1524
|
+
renderCanvasLive(gen, S.document, panel.canvas).then((scope) => {
|
|
1525
|
+
// Skip post-render setup if a newer render has started
|
|
1526
|
+
if (gen !== view.renderGeneration) return;
|
|
1205
1527
|
if (scope) {
|
|
1206
|
-
liveScope = scope;
|
|
1528
|
+
view.liveScope = scope;
|
|
1529
|
+
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
1207
1530
|
statusMessage("Runtime render OK", 1500);
|
|
1208
1531
|
} else {
|
|
1209
1532
|
// Fallback to structural preview
|
|
@@ -1214,9 +1537,9 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
|
1214
1537
|
renderOverlays();
|
|
1215
1538
|
|
|
1216
1539
|
// Process pending inline edit now that the canvas is populated
|
|
1217
|
-
if (pendingInlineEdit) {
|
|
1218
|
-
const { path, mediaName: mn } = pendingInlineEdit;
|
|
1219
|
-
pendingInlineEdit = null;
|
|
1540
|
+
if (view.pendingInlineEdit) {
|
|
1541
|
+
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
1542
|
+
view.pendingInlineEdit = null;
|
|
1220
1543
|
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
1221
1544
|
if (targetPanel) {
|
|
1222
1545
|
const el = findCanvasElement(path, targetPanel.canvas);
|
|
@@ -1271,9 +1594,7 @@ function canvasPanelTemplate(mediaName, label, fullWidth, width) {
|
|
|
1271
1594
|
<div
|
|
1272
1595
|
class="canvas-panel-header"
|
|
1273
1596
|
@click=${() => {
|
|
1274
|
-
|
|
1275
|
-
updateActivePanelHeaders();
|
|
1276
|
-
renderRightPanel();
|
|
1597
|
+
updateUi("activeMedia", mediaName === "base" ? null : mediaName);
|
|
1277
1598
|
}}
|
|
1278
1599
|
>
|
|
1279
1600
|
${label}
|
|
@@ -1322,52 +1643,52 @@ function canvasPanelTemplate(mediaName, label, fullWidth, width) {
|
|
|
1322
1643
|
|
|
1323
1644
|
/** Center canvas in viewport. */
|
|
1324
1645
|
function centerCanvas() {
|
|
1325
|
-
if (!panzoomWrap) return;
|
|
1646
|
+
if (!view.panzoomWrap) return;
|
|
1326
1647
|
const wrapWidth = canvasWrap.clientWidth;
|
|
1327
1648
|
const wrapHeight = canvasWrap.clientHeight;
|
|
1328
|
-
const contentWidth = panzoomWrap.scrollWidth;
|
|
1329
|
-
const contentHeight = panzoomWrap.scrollHeight;
|
|
1649
|
+
const contentWidth = view.panzoomWrap.scrollWidth;
|
|
1650
|
+
const contentHeight = view.panzoomWrap.scrollHeight;
|
|
1330
1651
|
const scaledWidth = contentWidth * S.ui.zoom;
|
|
1331
1652
|
const scaledHeight = contentHeight * S.ui.zoom;
|
|
1332
|
-
panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
1653
|
+
view.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
1333
1654
|
// Center vertically only when content fits; top-align with margin when taller
|
|
1334
1655
|
const verticalCenter = (wrapHeight - scaledHeight) / 2;
|
|
1335
|
-
panY = verticalCenter > 16 ? verticalCenter : 16;
|
|
1656
|
+
view.panY = verticalCenter > 16 ? verticalCenter : 16;
|
|
1336
1657
|
}
|
|
1337
1658
|
|
|
1338
1659
|
/**
|
|
1339
|
-
* Attach a ResizeObserver to panzoomWrap that re-centers until the user pans. Handles async
|
|
1340
|
-
* (runtime rendering, data fetching) that changes layout after initial paint.
|
|
1660
|
+
* Attach a ResizeObserver to view.panzoomWrap that re-centers until the user pans. Handles async
|
|
1661
|
+
* content (runtime rendering, data fetching) that changes layout after initial paint.
|
|
1341
1662
|
*/
|
|
1342
1663
|
function observeCenterUntilStable() {
|
|
1343
|
-
if (centerObserver) {
|
|
1344
|
-
centerObserver.disconnect();
|
|
1345
|
-
centerObserver = null;
|
|
1346
|
-
}
|
|
1347
|
-
if (!panzoomWrap) return;
|
|
1348
|
-
needsCenter = true;
|
|
1349
|
-
centerObserver = new ResizeObserver(() => {
|
|
1350
|
-
if (!needsCenter) {
|
|
1351
|
-
centerObserver?.disconnect();
|
|
1352
|
-
centerObserver = null;
|
|
1664
|
+
if (view.centerObserver) {
|
|
1665
|
+
view.centerObserver.disconnect();
|
|
1666
|
+
view.centerObserver = null;
|
|
1667
|
+
}
|
|
1668
|
+
if (!view.panzoomWrap) return;
|
|
1669
|
+
view.needsCenter = true;
|
|
1670
|
+
view.centerObserver = new ResizeObserver(() => {
|
|
1671
|
+
if (!view.needsCenter) {
|
|
1672
|
+
view.centerObserver?.disconnect();
|
|
1673
|
+
view.centerObserver = null;
|
|
1353
1674
|
return;
|
|
1354
1675
|
}
|
|
1355
1676
|
centerCanvas();
|
|
1356
1677
|
applyTransform();
|
|
1357
1678
|
});
|
|
1358
|
-
centerObserver.observe(panzoomWrap);
|
|
1679
|
+
view.centerObserver.observe(view.panzoomWrap);
|
|
1359
1680
|
// Also center immediately for synchronous content
|
|
1360
1681
|
centerCanvas();
|
|
1361
1682
|
}
|
|
1362
1683
|
|
|
1363
1684
|
/** Apply the current zoom + pan transform to the panzoom wrapper. */
|
|
1364
1685
|
function applyTransform() {
|
|
1365
|
-
if (!panzoomWrap) return;
|
|
1366
|
-
panzoomWrap.style.transform = `translate(${panX}px, ${panY}px) scale(${S.ui.zoom})`;
|
|
1686
|
+
if (!view.panzoomWrap) return;
|
|
1687
|
+
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${S.ui.zoom})`;
|
|
1367
1688
|
const label = document.querySelector(".zoom-indicator-label");
|
|
1368
1689
|
if (label) label.textContent = `${Math.round(S.ui.zoom * 100)}%`;
|
|
1369
1690
|
renderOverlays();
|
|
1370
|
-
if (canvasMode === "
|
|
1691
|
+
if (canvasMode === "settings") renderStylebookOverlays();
|
|
1371
1692
|
}
|
|
1372
1693
|
|
|
1373
1694
|
/** Lightweight in-place zoom update — no full re-render. */
|
|
@@ -1377,7 +1698,7 @@ function _applyZoom() {
|
|
|
1377
1698
|
|
|
1378
1699
|
/** Calculate zoom + pan to fit all panels within the viewport. */
|
|
1379
1700
|
function fitToScreen() {
|
|
1380
|
-
if (!panzoomWrap) return;
|
|
1701
|
+
if (!view.panzoomWrap) return;
|
|
1381
1702
|
const wrapWidth = canvasWrap.clientWidth;
|
|
1382
1703
|
const wrapHeight = canvasWrap.clientHeight;
|
|
1383
1704
|
const gap = 24;
|
|
@@ -1390,7 +1711,7 @@ function fitToScreen() {
|
|
|
1390
1711
|
totalPanelWidth += gap * Math.max(0, canvasPanels.length - 1) + padding;
|
|
1391
1712
|
|
|
1392
1713
|
// Get actual content height from rendered panels
|
|
1393
|
-
const wrapRect = panzoomWrap.getBoundingClientRect();
|
|
1714
|
+
const wrapRect = view.panzoomWrap.getBoundingClientRect();
|
|
1394
1715
|
const unscaledHeight = wrapRect.height / S.ui.zoom;
|
|
1395
1716
|
maxPanelHeight = unscaledHeight + padding;
|
|
1396
1717
|
|
|
@@ -1398,12 +1719,13 @@ function fitToScreen() {
|
|
|
1398
1719
|
const fitZoomH = wrapHeight / maxPanelHeight;
|
|
1399
1720
|
const fitZoom = Math.min(5.0, Math.max(0.05, Math.min(fitZoomW, fitZoomH)));
|
|
1400
1721
|
|
|
1401
|
-
|
|
1722
|
+
session = { ...session, ui: { ...session.ui, zoom: fitZoom } };
|
|
1723
|
+
S = toFlat(doc, session);
|
|
1402
1724
|
// Center the content
|
|
1403
1725
|
const scaledWidth = totalPanelWidth * fitZoom;
|
|
1404
1726
|
const scaledHeight = maxPanelHeight * fitZoom;
|
|
1405
|
-
panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
1406
|
-
panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
1727
|
+
view.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
1728
|
+
view.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
1407
1729
|
applyTransform();
|
|
1408
1730
|
}
|
|
1409
1731
|
|
|
@@ -1443,7 +1765,11 @@ function renderZoomIndicator() {
|
|
|
1443
1765
|
zoomIndicatorHost,
|
|
1444
1766
|
);
|
|
1445
1767
|
} catch {
|
|
1446
|
-
|
|
1768
|
+
// Lit markers were corrupted — replace the host element to fully reset Lit state
|
|
1769
|
+
const newHost = document.createElement("div");
|
|
1770
|
+
newHost.style.display = "contents";
|
|
1771
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1772
|
+
zoomIndicatorHost = newHost;
|
|
1447
1773
|
litRender(
|
|
1448
1774
|
html`
|
|
1449
1775
|
<div class="zoom-indicator">
|
|
@@ -1632,7 +1958,7 @@ function registerPanelDnD(panel) {
|
|
|
1632
1958
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
|
|
1633
1959
|
},
|
|
1634
1960
|
});
|
|
1635
|
-
canvasDndCleanups.push(monitorCleanup);
|
|
1961
|
+
view.canvasDndCleanups.push(monitorCleanup);
|
|
1636
1962
|
|
|
1637
1963
|
for (const el of allEls) {
|
|
1638
1964
|
const elPath = elToPath.get(el);
|
|
@@ -1669,7 +1995,7 @@ function registerPanelDnD(panel) {
|
|
|
1669
1995
|
applyDropInstruction(instruction, source.data, elPath);
|
|
1670
1996
|
},
|
|
1671
1997
|
});
|
|
1672
|
-
canvasDndCleanups.push(cleanup);
|
|
1998
|
+
view.canvasDndCleanups.push(cleanup);
|
|
1673
1999
|
}
|
|
1674
2000
|
}
|
|
1675
2001
|
|
|
@@ -1739,83 +2065,7 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
|
|
|
1739
2065
|
// ─── Overlay system ───────────────────────────────────────────────────────────
|
|
1740
2066
|
|
|
1741
2067
|
function renderOverlays() {
|
|
1742
|
-
|
|
1743
|
-
if (canvasMode !== "design" && canvasMode !== "edit" && canvasMode !== "stylebook") {
|
|
1744
|
-
for (const p of canvasPanels) {
|
|
1745
|
-
litRender(nothing, p.overlay);
|
|
1746
|
-
p.overlayClk.style.pointerEvents = "none";
|
|
1747
|
-
}
|
|
1748
|
-
if (selDragCleanup) {
|
|
1749
|
-
selDragCleanup();
|
|
1750
|
-
selDragCleanup = null;
|
|
1751
|
-
}
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
// Stylebook manages its own overlays
|
|
1755
|
-
if (canvasMode === "stylebook") {
|
|
1756
|
-
const enable = S.ui.stylebookTab === "elements";
|
|
1757
|
-
for (const p of canvasPanels) {
|
|
1758
|
-
p.overlayClk.style.pointerEvents = enable ? "" : "none";
|
|
1759
|
-
}
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
for (const p of canvasPanels) {
|
|
1763
|
-
p.overlayClk.style.pointerEvents = componentInlineEdit || isEditing() ? "none" : "";
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (selDragCleanup) {
|
|
1767
|
-
selDragCleanup();
|
|
1768
|
-
selDragCleanup = null;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
// Collect overlay boxes per panel, then render in batch
|
|
1772
|
-
for (const p of canvasPanels) {
|
|
1773
|
-
/**
|
|
1774
|
-
* @type {{
|
|
1775
|
-
* cls: string;
|
|
1776
|
-
* top: string;
|
|
1777
|
-
* left: string;
|
|
1778
|
-
* width: string;
|
|
1779
|
-
* height: string;
|
|
1780
|
-
* border?: string;
|
|
1781
|
-
* }[]}
|
|
1782
|
-
*/
|
|
1783
|
-
const boxes = [];
|
|
1784
|
-
|
|
1785
|
-
// Hover overlay
|
|
1786
|
-
if (S.hover && !pathsEqual(S.hover, S.selection)) {
|
|
1787
|
-
const el = findCanvasElement(S.hover, p.canvas);
|
|
1788
|
-
if (el) boxes.push(overlayBoxDescriptor(el, "hover", p));
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// Selection overlay (only on active panel)
|
|
1792
|
-
if (S.selection && p === getActivePanel()) {
|
|
1793
|
-
const el = findCanvasElement(S.selection, p.canvas);
|
|
1794
|
-
if (el) {
|
|
1795
|
-
const desc = overlayBoxDescriptor(el, "selection", p);
|
|
1796
|
-
if (componentInlineEdit || isEditing()) /** @type {any} */ (desc).border = "none";
|
|
1797
|
-
boxes.push(desc);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
litRender(
|
|
1802
|
-
html`
|
|
1803
|
-
${p.dropLine}
|
|
1804
|
-
${boxes.map(
|
|
1805
|
-
(b) => html`
|
|
1806
|
-
<div
|
|
1807
|
-
class=${b.cls}
|
|
1808
|
-
style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}${b.border
|
|
1809
|
-
? `;border:${b.border}`
|
|
1810
|
-
: ""}"
|
|
1811
|
-
></div>
|
|
1812
|
-
`,
|
|
1813
|
-
)}
|
|
1814
|
-
`,
|
|
1815
|
-
p.overlay,
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
renderBlockActionBar();
|
|
2068
|
+
overlaysPanel.render();
|
|
1819
2069
|
}
|
|
1820
2070
|
|
|
1821
2071
|
/**
|
|
@@ -1990,14 +2240,19 @@ function applyInlineFormat(action) {
|
|
|
1990
2240
|
}
|
|
1991
2241
|
|
|
1992
2242
|
/** Show a link URL popover anchored to a toolbar button. */
|
|
1993
|
-
|
|
1994
|
-
linkPopoverHost.style.display = "contents";
|
|
1995
|
-
(document.querySelector("sp-theme") || document.body).appendChild(linkPopoverHost);
|
|
2243
|
+
view.linkPopoverHost = document.createElement("div");
|
|
2244
|
+
view.linkPopoverHost.style.display = "contents";
|
|
2245
|
+
(document.querySelector("sp-theme") || document.body).appendChild(view.linkPopoverHost);
|
|
2246
|
+
|
|
2247
|
+
/** Dismiss the link popover if open. */
|
|
2248
|
+
function dismissLinkPopover() {
|
|
2249
|
+
if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
|
|
2250
|
+
}
|
|
1996
2251
|
|
|
1997
2252
|
/** @param {any} anchorBtn */
|
|
1998
2253
|
function showLinkPopover(anchorBtn) {
|
|
1999
2254
|
// Dismiss existing
|
|
2000
|
-
litRender(nothing, linkPopoverHost);
|
|
2255
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2001
2256
|
|
|
2002
2257
|
const sel = window.getSelection();
|
|
2003
2258
|
/** @type {any} */
|
|
@@ -2017,14 +2272,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2017
2272
|
const rect = anchorBtn.getBoundingClientRect();
|
|
2018
2273
|
|
|
2019
2274
|
const onApply = () => {
|
|
2020
|
-
const field = linkPopoverHost.querySelector("sp-textfield");
|
|
2275
|
+
const field = view.linkPopoverHost.querySelector("sp-textfield");
|
|
2021
2276
|
const url = /** @type {any} */ (field)?.value;
|
|
2022
2277
|
if (existingLink) {
|
|
2023
2278
|
existingLink.setAttribute("href", url);
|
|
2024
2279
|
} else if (url) {
|
|
2025
2280
|
document.execCommand("createLink", false, url);
|
|
2026
2281
|
}
|
|
2027
|
-
litRender(nothing, linkPopoverHost);
|
|
2282
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2028
2283
|
renderBlockActionBar();
|
|
2029
2284
|
};
|
|
2030
2285
|
|
|
@@ -2032,14 +2287,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2032
2287
|
const frag = document.createDocumentFragment();
|
|
2033
2288
|
while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
|
|
2034
2289
|
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
2035
|
-
litRender(nothing, linkPopoverHost);
|
|
2290
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2036
2291
|
renderBlockActionBar();
|
|
2037
2292
|
};
|
|
2038
2293
|
|
|
2039
2294
|
const onKeydown = (/** @type {any} */ e) => {
|
|
2040
2295
|
if (e.key === "Enter") onApply();
|
|
2041
2296
|
else if (e.key === "Escape") {
|
|
2042
|
-
litRender(nothing, linkPopoverHost);
|
|
2297
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2043
2298
|
}
|
|
2044
2299
|
};
|
|
2045
2300
|
|
|
@@ -2065,12 +2320,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2065
2320
|
: nothing}
|
|
2066
2321
|
</sp-popover>
|
|
2067
2322
|
`,
|
|
2068
|
-
linkPopoverHost,
|
|
2323
|
+
view.linkPopoverHost,
|
|
2069
2324
|
);
|
|
2070
2325
|
|
|
2071
2326
|
requestAnimationFrame(
|
|
2072
2327
|
() =>
|
|
2073
|
-
/** @type {HTMLElement | null} */ (
|
|
2328
|
+
/** @type {HTMLElement | null} */ (
|
|
2329
|
+
view.linkPopoverHost?.querySelector("sp-textfield")
|
|
2330
|
+
)?.focus(),
|
|
2074
2331
|
);
|
|
2075
2332
|
}
|
|
2076
2333
|
|
|
@@ -2081,7 +2338,8 @@ function moveSelectionUp() {
|
|
|
2081
2338
|
if (idx <= 0) return;
|
|
2082
2339
|
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
2083
2340
|
update(moveNode(S, S.selection, pPath, idx - 1));
|
|
2084
|
-
|
|
2341
|
+
session = { ...session, selection: [...pPath, "children", idx - 1] };
|
|
2342
|
+
S = toFlat(doc, session);
|
|
2085
2343
|
renderOverlays();
|
|
2086
2344
|
}
|
|
2087
2345
|
|
|
@@ -2094,7 +2352,8 @@ function moveSelectionDown() {
|
|
|
2094
2352
|
const siblings = parentNode?.children;
|
|
2095
2353
|
if (!siblings || idx >= siblings.length - 1) return;
|
|
2096
2354
|
update(moveNode(S, S.selection, pPath, idx + 2));
|
|
2097
|
-
|
|
2355
|
+
session = { ...session, selection: [...pPath, "children", idx + 1] };
|
|
2356
|
+
S = toFlat(doc, session);
|
|
2098
2357
|
renderOverlays();
|
|
2099
2358
|
}
|
|
2100
2359
|
|
|
@@ -2104,30 +2363,30 @@ function moveSelectionDown() {
|
|
|
2104
2363
|
*/
|
|
2105
2364
|
function renderBlockActionBar() {
|
|
2106
2365
|
// Ensure persistent render container exists
|
|
2107
|
-
if (!blockActionBarEl) {
|
|
2108
|
-
blockActionBarEl = createFloatingContainer();
|
|
2366
|
+
if (!view.blockActionBarEl) {
|
|
2367
|
+
view.blockActionBarEl = createFloatingContainer();
|
|
2109
2368
|
}
|
|
2110
2369
|
|
|
2111
2370
|
// Tear down drag if it was active
|
|
2112
|
-
if (selDragCleanup) {
|
|
2113
|
-
selDragCleanup();
|
|
2114
|
-
selDragCleanup = null;
|
|
2371
|
+
if (view.selDragCleanup) {
|
|
2372
|
+
view.selDragCleanup();
|
|
2373
|
+
view.selDragCleanup = null;
|
|
2115
2374
|
}
|
|
2116
2375
|
|
|
2117
2376
|
if (!S.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
2118
|
-
litRender(nothing, blockActionBarEl);
|
|
2377
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2119
2378
|
return;
|
|
2120
2379
|
}
|
|
2121
2380
|
|
|
2122
2381
|
const activePanel = getActivePanel();
|
|
2123
2382
|
if (!activePanel) {
|
|
2124
|
-
litRender(nothing, blockActionBarEl);
|
|
2383
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2125
2384
|
return;
|
|
2126
2385
|
}
|
|
2127
2386
|
const el = findCanvasElement(S.selection, activePanel.canvas);
|
|
2128
2387
|
const node = el && getNodeAtPath(S.document, S.selection);
|
|
2129
2388
|
if (!el || !node) {
|
|
2130
|
-
litRender(nothing, blockActionBarEl);
|
|
2389
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2131
2390
|
return;
|
|
2132
2391
|
}
|
|
2133
2392
|
|
|
@@ -2158,6 +2417,32 @@ function renderBlockActionBar() {
|
|
|
2158
2417
|
? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
|
|
2159
2418
|
: nothing}
|
|
2160
2419
|
${S.selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
2420
|
+
${S.selection.length >= 2 && node.tagName
|
|
2421
|
+
? (() => {
|
|
2422
|
+
const isComp =
|
|
2423
|
+
node.tagName.includes("-") &&
|
|
2424
|
+
componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
2425
|
+
if (isComp) {
|
|
2426
|
+
const comp = componentRegistry.find(
|
|
2427
|
+
(/** @type {any} */ c) => c.tagName === node.tagName,
|
|
2428
|
+
);
|
|
2429
|
+
return html`<sp-action-button
|
|
2430
|
+
size="xs"
|
|
2431
|
+
quiet
|
|
2432
|
+
title="Edit Component"
|
|
2433
|
+
@click=${() => navigateToComponent(comp.path)}
|
|
2434
|
+
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
2435
|
+
></sp-action-button>`;
|
|
2436
|
+
}
|
|
2437
|
+
return html`<sp-action-button
|
|
2438
|
+
size="xs"
|
|
2439
|
+
quiet
|
|
2440
|
+
title="Convert to Component"
|
|
2441
|
+
@click=${() => convertToComponent(S)}
|
|
2442
|
+
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
2443
|
+
></sp-action-button>`;
|
|
2444
|
+
})()
|
|
2445
|
+
: nothing}
|
|
2161
2446
|
${showFormat
|
|
2162
2447
|
? html`
|
|
2163
2448
|
<sp-divider size="s" vertical></sp-divider>
|
|
@@ -2186,12 +2471,12 @@ function renderBlockActionBar() {
|
|
|
2186
2471
|
: nothing}
|
|
2187
2472
|
</div>
|
|
2188
2473
|
`,
|
|
2189
|
-
blockActionBarEl,
|
|
2474
|
+
view.blockActionBarEl,
|
|
2190
2475
|
);
|
|
2191
2476
|
|
|
2192
2477
|
// Post-render side effects
|
|
2193
2478
|
requestAnimationFrame(() => {
|
|
2194
|
-
const bar = blockActionBarEl?.firstElementChild;
|
|
2479
|
+
const bar = view.blockActionBarEl?.firstElementChild;
|
|
2195
2480
|
if (!bar) return;
|
|
2196
2481
|
// Clamp to window
|
|
2197
2482
|
const barRect = bar.getBoundingClientRect();
|
|
@@ -2202,7 +2487,11 @@ function renderBlockActionBar() {
|
|
|
2202
2487
|
if (S.selection.length >= 2) {
|
|
2203
2488
|
const handle = bar.querySelector(".bar-drag-handle");
|
|
2204
2489
|
if (handle) {
|
|
2205
|
-
selDragCleanup
|
|
2490
|
+
if (view.selDragCleanup) {
|
|
2491
|
+
view.selDragCleanup();
|
|
2492
|
+
view.selDragCleanup = null;
|
|
2493
|
+
}
|
|
2494
|
+
view.selDragCleanup = draggable({
|
|
2206
2495
|
element: handle,
|
|
2207
2496
|
getInitialData: () => ({ type: "tree-node", path: S.selection }),
|
|
2208
2497
|
});
|
|
@@ -2215,20 +2504,15 @@ function renderBlockActionBar() {
|
|
|
2215
2504
|
// When a pseudo-selector (:hover, :focus, etc.) is active in the style sidebar,
|
|
2216
2505
|
// force those styles onto the selected element so the user can see the result.
|
|
2217
2506
|
|
|
2218
|
-
/** @type {any} */
|
|
2219
|
-
let _forcedStyleTag = null;
|
|
2220
|
-
/** @type {any} */
|
|
2221
|
-
let _forcedAttrEl = null;
|
|
2222
|
-
|
|
2223
2507
|
function updateForcedPseudoPreview() {
|
|
2224
2508
|
// Clean up previous
|
|
2225
|
-
if (
|
|
2226
|
-
|
|
2227
|
-
|
|
2509
|
+
if (view.forcedStyleTag) {
|
|
2510
|
+
view.forcedStyleTag.remove();
|
|
2511
|
+
view.forcedStyleTag = null;
|
|
2228
2512
|
}
|
|
2229
|
-
if (
|
|
2230
|
-
|
|
2231
|
-
|
|
2513
|
+
if (view.forcedAttrEl) {
|
|
2514
|
+
view.forcedAttrEl.removeAttribute("data-studio-forced");
|
|
2515
|
+
view.forcedAttrEl = null;
|
|
2232
2516
|
}
|
|
2233
2517
|
|
|
2234
2518
|
const sel = S.ui?.activeSelector;
|
|
@@ -2259,12 +2543,12 @@ function updateForcedPseudoPreview() {
|
|
|
2259
2543
|
if (!cssProps) return;
|
|
2260
2544
|
|
|
2261
2545
|
el.setAttribute("data-studio-forced", "1");
|
|
2262
|
-
|
|
2546
|
+
view.forcedAttrEl = el;
|
|
2263
2547
|
|
|
2264
2548
|
const tag = document.createElement("style");
|
|
2265
2549
|
tag.textContent = `[data-studio-forced] { ${cssProps} }`;
|
|
2266
2550
|
document.head.appendChild(tag);
|
|
2267
|
-
|
|
2551
|
+
view.forcedStyleTag = tag;
|
|
2268
2552
|
}
|
|
2269
2553
|
|
|
2270
2554
|
/**
|
|
@@ -2316,9 +2600,24 @@ function findCanvasElement(path, canvasEl) {
|
|
|
2316
2600
|
} else {
|
|
2317
2601
|
el = el.children[idx];
|
|
2318
2602
|
}
|
|
2319
|
-
if (!el)
|
|
2603
|
+
if (!el) break;
|
|
2320
2604
|
}
|
|
2321
|
-
|
|
2605
|
+
|
|
2606
|
+
// Verify the result: if DOM traversal landed on the wrong element
|
|
2607
|
+
// (e.g. a custom element template child instead of the intended node),
|
|
2608
|
+
// fall back to scanning elToPath.
|
|
2609
|
+
if (el) {
|
|
2610
|
+
const elPath = elToPath.get(el);
|
|
2611
|
+
if (elPath && pathsEqual(elPath, path)) return el;
|
|
2612
|
+
// el has no path or wrong path — it's a template element, not the target
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// Fall back: scan all descendants for an element with matching elToPath
|
|
2616
|
+
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
2617
|
+
const p = elToPath.get(candidate);
|
|
2618
|
+
if (p && pathsEqual(p, path)) return candidate;
|
|
2619
|
+
}
|
|
2620
|
+
return null;
|
|
2322
2621
|
}
|
|
2323
2622
|
|
|
2324
2623
|
// ─── Per-panel click-to-select ────────────────────────────────────────────────
|
|
@@ -2326,6 +2625,9 @@ function findCanvasElement(path, canvasEl) {
|
|
|
2326
2625
|
/** @param {any} panel */
|
|
2327
2626
|
function registerPanelEvents(panel) {
|
|
2328
2627
|
const { canvas, overlayClk, mediaName } = panel;
|
|
2628
|
+
const ac = new AbortController();
|
|
2629
|
+
const opts = { signal: ac.signal };
|
|
2630
|
+
view.canvasEventCleanups.push(() => ac.abort());
|
|
2329
2631
|
|
|
2330
2632
|
/** @param {any} fn */
|
|
2331
2633
|
function withPanelPointerEvents(fn) {
|
|
@@ -2341,162 +2643,191 @@ function registerPanelEvents(panel) {
|
|
|
2341
2643
|
// During component inline edit, the overlayClk is disabled (see enterComponentInlineEdit).
|
|
2342
2644
|
// No mousedown passthrough needed — native events reach the contenteditable directly.
|
|
2343
2645
|
|
|
2344
|
-
overlayClk.addEventListener(
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
const
|
|
2349
|
-
if (
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
// (see enterComponentInlineEdit), so nothing to do here — just fall through.
|
|
2646
|
+
overlayClk.addEventListener(
|
|
2647
|
+
"click",
|
|
2648
|
+
(/** @type {any} */ e) => {
|
|
2649
|
+
// Don't intercept clicks meant for the block action bar
|
|
2650
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2651
|
+
if (barInner) {
|
|
2652
|
+
const r = barInner.getBoundingClientRect();
|
|
2653
|
+
if (
|
|
2654
|
+
e.clientX >= r.left &&
|
|
2655
|
+
e.clientX <= r.right &&
|
|
2656
|
+
e.clientY >= r.top &&
|
|
2657
|
+
e.clientY <= r.bottom
|
|
2658
|
+
)
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
// If content-mode inline editing is active, treat click outside as blur
|
|
2662
|
+
if (isEditing()) {
|
|
2663
|
+
stopEditing();
|
|
2664
|
+
}
|
|
2364
2665
|
|
|
2365
|
-
|
|
2666
|
+
// Component-mode inline editing is handled by its own document-level listener
|
|
2667
|
+
// (see enterComponentInlineEdit), so nothing to do here — just fall through.
|
|
2366
2668
|
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
if (path) {
|
|
2371
|
-
path = bubbleInlinePath(S.document, path);
|
|
2372
|
-
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2373
|
-
S = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2669
|
+
const elements = withPanelPointerEvents(() =>
|
|
2670
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2671
|
+
);
|
|
2374
2672
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2673
|
+
for (const el of elements) {
|
|
2674
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2675
|
+
const originalPath = elToPath.get(el);
|
|
2676
|
+
if (originalPath) {
|
|
2677
|
+
let path = bubbleInlinePath(S.document, originalPath);
|
|
2678
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2679
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2680
|
+
|
|
2681
|
+
// Find the DOM element for the bubbled path (may differ from hit element)
|
|
2682
|
+
// When path didn't change (no inline bubbling), prefer the hit element directly
|
|
2683
|
+
// since findCanvasElement can't navigate into custom element template DOM.
|
|
2684
|
+
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2685
|
+
|
|
2686
|
+
// Re-click on selected editable block: enter inline editing
|
|
2687
|
+
// Edit mode / content mode → rich text editing (enterInlineEdit)
|
|
2688
|
+
// Design mode → plaintext component editing (enterComponentInlineEdit via view.pendingInlineEdit)
|
|
2689
|
+
if (
|
|
2690
|
+
pathsEqual(path, S.selection) &&
|
|
2691
|
+
isEditableBlock(resolvedEl) &&
|
|
2692
|
+
(canvasMode === "edit" || S.mode === "content")
|
|
2693
|
+
) {
|
|
2694
|
+
S = withMedia;
|
|
2695
|
+
enterInlineEdit(resolvedEl, path);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2377
2698
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
(canvasMode === "edit" || S.mode === "content")
|
|
2385
|
-
) {
|
|
2386
|
-
enterInlineEdit(resolvedEl, path);
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2699
|
+
// Design mode or first click: select and schedule component inline editing
|
|
2700
|
+
if (canvasMode === "design" && S.mode !== "content") {
|
|
2701
|
+
view.pendingInlineEdit = { path, mediaName };
|
|
2702
|
+
update(selectNode(withMedia, path));
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2389
2705
|
|
|
2390
|
-
|
|
2391
|
-
if (canvasMode === "design" && S.mode !== "content") {
|
|
2392
|
-
pendingInlineEdit = { path, mediaName };
|
|
2393
|
-
update(selectNode(S, path));
|
|
2706
|
+
update(selectNode(withMedia, path));
|
|
2394
2707
|
return;
|
|
2395
2708
|
}
|
|
2396
|
-
|
|
2397
|
-
update(selectNode(S, path));
|
|
2398
|
-
return;
|
|
2399
2709
|
}
|
|
2400
2710
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2711
|
+
update(selectNode(S, null));
|
|
2712
|
+
},
|
|
2713
|
+
opts,
|
|
2714
|
+
);
|
|
2404
2715
|
|
|
2405
2716
|
// Double-click shortcut for immediate inline editing
|
|
2406
|
-
overlayClk.addEventListener(
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
const
|
|
2410
|
-
if (
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2717
|
+
overlayClk.addEventListener(
|
|
2718
|
+
"dblclick",
|
|
2719
|
+
(/** @type {any} */ e) => {
|
|
2720
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2721
|
+
if (barInner) {
|
|
2722
|
+
const r = barInner.getBoundingClientRect();
|
|
2723
|
+
if (
|
|
2724
|
+
e.clientX >= r.left &&
|
|
2725
|
+
e.clientX <= r.right &&
|
|
2726
|
+
e.clientY >= r.top &&
|
|
2727
|
+
e.clientY <= r.bottom
|
|
2728
|
+
)
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (canvasMode !== "edit" && canvasMode !== "design") return;
|
|
2419
2732
|
|
|
2420
|
-
|
|
2733
|
+
const elements = withPanelPointerEvents(() =>
|
|
2734
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2735
|
+
);
|
|
2421
2736
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2737
|
+
for (const el of elements) {
|
|
2738
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2739
|
+
const originalPath = elToPath.get(el);
|
|
2740
|
+
if (originalPath) {
|
|
2741
|
+
const path = bubbleInlinePath(S.document, originalPath);
|
|
2742
|
+
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2743
|
+
if (isEditableBlock(resolvedEl)) {
|
|
2744
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2745
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2746
|
+
update(selectNode(withMedia, path));
|
|
2747
|
+
enterInlineEdit(resolvedEl, path);
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
},
|
|
2754
|
+
opts,
|
|
2755
|
+
);
|
|
2756
|
+
|
|
2757
|
+
overlayClk.addEventListener(
|
|
2758
|
+
"contextmenu",
|
|
2759
|
+
(/** @type {any} */ e) => {
|
|
2760
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2761
|
+
if (barInner) {
|
|
2762
|
+
const r = barInner.getBoundingClientRect();
|
|
2763
|
+
if (
|
|
2764
|
+
e.clientX >= r.left &&
|
|
2765
|
+
e.clientX <= r.right &&
|
|
2766
|
+
e.clientY >= r.top &&
|
|
2767
|
+
e.clientY <= r.bottom
|
|
2768
|
+
)
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
const elements = withPanelPointerEvents(() =>
|
|
2772
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2773
|
+
);
|
|
2774
|
+
for (const el of elements) {
|
|
2775
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2776
|
+
let path = elToPath.get(el);
|
|
2777
|
+
if (path) {
|
|
2778
|
+
path = bubbleInlinePath(S.document, path);
|
|
2779
|
+
showContextMenu(e, path, S, { onEditComponent: navigateToComponent });
|
|
2433
2780
|
return;
|
|
2434
2781
|
}
|
|
2435
2782
|
}
|
|
2436
2783
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2784
|
+
e.preventDefault();
|
|
2785
|
+
},
|
|
2786
|
+
opts,
|
|
2787
|
+
);
|
|
2439
2788
|
|
|
2440
|
-
overlayClk.addEventListener(
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2789
|
+
overlayClk.addEventListener(
|
|
2790
|
+
"mousemove",
|
|
2791
|
+
(/** @type {any} */ e) => {
|
|
2792
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2793
|
+
if (barInner) {
|
|
2794
|
+
const r = barInner.getBoundingClientRect();
|
|
2795
|
+
if (
|
|
2796
|
+
e.clientX >= r.left &&
|
|
2797
|
+
e.clientX <= r.right &&
|
|
2798
|
+
e.clientY >= r.top &&
|
|
2799
|
+
e.clientY <= r.bottom
|
|
2800
|
+
)
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
2804
|
+
if (el && canvas.contains(el) && el !== canvas) {
|
|
2455
2805
|
let path = elToPath.get(el);
|
|
2456
2806
|
if (path) {
|
|
2457
2807
|
path = bubbleInlinePath(S.document, path);
|
|
2458
|
-
|
|
2459
|
-
|
|
2808
|
+
if (!pathsEqual(path, S.hover)) {
|
|
2809
|
+
S = hoverNode(S, path);
|
|
2810
|
+
renderOverlays();
|
|
2811
|
+
}
|
|
2460
2812
|
}
|
|
2813
|
+
} else if (S.hover) {
|
|
2814
|
+
S = hoverNode(S, null);
|
|
2815
|
+
renderOverlays();
|
|
2461
2816
|
}
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2817
|
+
},
|
|
2818
|
+
opts,
|
|
2819
|
+
);
|
|
2465
2820
|
|
|
2466
|
-
overlayClk.addEventListener(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
e.clientX <= r.right &&
|
|
2473
|
-
e.clientY >= r.top &&
|
|
2474
|
-
e.clientY <= r.bottom
|
|
2475
|
-
)
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
2479
|
-
if (el && canvas.contains(el) && el !== canvas) {
|
|
2480
|
-
let path = elToPath.get(el);
|
|
2481
|
-
if (path) {
|
|
2482
|
-
path = bubbleInlinePath(S.document, path);
|
|
2483
|
-
if (!pathsEqual(path, S.hover)) {
|
|
2484
|
-
S = hoverNode(S, path);
|
|
2485
|
-
renderOverlays();
|
|
2486
|
-
}
|
|
2821
|
+
overlayClk.addEventListener(
|
|
2822
|
+
"mouseleave",
|
|
2823
|
+
() => {
|
|
2824
|
+
if (S.hover) {
|
|
2825
|
+
S = hoverNode(S, null);
|
|
2826
|
+
renderOverlays();
|
|
2487
2827
|
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
});
|
|
2493
|
-
|
|
2494
|
-
overlayClk.addEventListener("mouseleave", () => {
|
|
2495
|
-
if (S.hover) {
|
|
2496
|
-
S = hoverNode(S, null);
|
|
2497
|
-
renderOverlays();
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
2828
|
+
},
|
|
2829
|
+
opts,
|
|
2830
|
+
);
|
|
2500
2831
|
}
|
|
2501
2832
|
|
|
2502
2833
|
// ─── Inline editing bridge ────────────────────────────────────────────────────
|
|
@@ -2589,12 +2920,63 @@ function enterInlineEdit(el, path) {
|
|
|
2589
2920
|
});
|
|
2590
2921
|
},
|
|
2591
2922
|
|
|
2592
|
-
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd) {
|
|
2923
|
+
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
|
|
2593
2924
|
// cmd comes from the shared slash menu: { label, tag, description }
|
|
2925
|
+
const isEmpty =
|
|
2926
|
+
!commitData ||
|
|
2927
|
+
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
2928
|
+
(commitData.children &&
|
|
2929
|
+
(commitData.children.length === 0 ||
|
|
2930
|
+
(commitData.children.length === 1 &&
|
|
2931
|
+
typeof commitData.children[0] === "string" &&
|
|
2932
|
+
commitData.children[0].trim() === "") ||
|
|
2933
|
+
(commitData.children.length === 1 &&
|
|
2934
|
+
typeof commitData.children[0] === "object" &&
|
|
2935
|
+
commitData.children[0]?.tagName === "br")));
|
|
2936
|
+
|
|
2937
|
+
// If the element is empty, swap its tagName instead of inserting after
|
|
2938
|
+
if (isEmpty) {
|
|
2939
|
+
let s = S;
|
|
2940
|
+
s = updateProperty(s, afterPath, "tagName", cmd.tag);
|
|
2941
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
2942
|
+
const def = defaultDef(cmd.tag);
|
|
2943
|
+
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
2944
|
+
s = updateProperty(s, afterPath, "textContent", def.textContent);
|
|
2945
|
+
} else {
|
|
2946
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2947
|
+
}
|
|
2948
|
+
s = selectNode(s, afterPath);
|
|
2949
|
+
update(s);
|
|
2950
|
+
|
|
2951
|
+
requestAnimationFrame(() => {
|
|
2952
|
+
const activePanel = getActivePanel();
|
|
2953
|
+
if (activePanel) {
|
|
2954
|
+
const el = findCanvasElement(afterPath, activePanel.canvas);
|
|
2955
|
+
if (el && isEditableBlock(el)) {
|
|
2956
|
+
enterInlineEdit(el, afterPath);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2594
2963
|
const elementDef = defaultDef(cmd.tag);
|
|
2595
2964
|
const parentPath = /** @type {any} */ (parentElementPath(afterPath));
|
|
2596
2965
|
const idx = /** @type {number} */ (childIndex(afterPath));
|
|
2597
|
-
|
|
2966
|
+
|
|
2967
|
+
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
2968
|
+
let s = S;
|
|
2969
|
+
if (commitData) {
|
|
2970
|
+
if (commitData.children) {
|
|
2971
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2972
|
+
s = updateProperty(s, afterPath, "children", commitData.children);
|
|
2973
|
+
} else if (commitData.textContent != null) {
|
|
2974
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
2975
|
+
s = updateProperty(s, afterPath, "textContent", commitData.textContent);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
|
|
2598
2980
|
const newPath = [...parentPath, "children", idx + 1];
|
|
2599
2981
|
s = selectNode(s, newPath);
|
|
2600
2982
|
update(s);
|
|
@@ -2613,9 +2995,9 @@ function enterInlineEdit(el, path) {
|
|
|
2613
2995
|
|
|
2614
2996
|
onEnd() {
|
|
2615
2997
|
// Cleanup inline edit listeners
|
|
2616
|
-
if (
|
|
2617
|
-
|
|
2618
|
-
|
|
2998
|
+
if (view.inlineEditCleanup) {
|
|
2999
|
+
view.inlineEditCleanup();
|
|
3000
|
+
view.inlineEditCleanup = null;
|
|
2619
3001
|
}
|
|
2620
3002
|
// Restore overlays after inline editing ends
|
|
2621
3003
|
for (const p of canvasPanels) {
|
|
@@ -2642,7 +3024,7 @@ function enterInlineEdit(el, path) {
|
|
|
2642
3024
|
el.removeEventListener("mouseup", selectionHandler);
|
|
2643
3025
|
el.removeEventListener("keyup", selectionHandler);
|
|
2644
3026
|
};
|
|
2645
|
-
|
|
3027
|
+
view.inlineEditCleanup = inlineEditCleanup;
|
|
2646
3028
|
}
|
|
2647
3029
|
|
|
2648
3030
|
// ─── Component-mode inline text editing ──────────────────────────────────────
|
|
@@ -2653,7 +3035,7 @@ function enterInlineEdit(el, path) {
|
|
|
2653
3035
|
*/
|
|
2654
3036
|
function enterComponentInlineEdit(el, path) {
|
|
2655
3037
|
// Already editing this element
|
|
2656
|
-
if (componentInlineEdit && componentInlineEdit.el === el) {
|
|
3038
|
+
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
2657
3039
|
return;
|
|
2658
3040
|
}
|
|
2659
3041
|
|
|
@@ -2666,7 +3048,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2666
3048
|
if (Array.isArray(node.children) && node.children.length > 0) return;
|
|
2667
3049
|
if (node.children && typeof node.children === "object") return;
|
|
2668
3050
|
if (tc && typeof tc === "object") return;
|
|
2669
|
-
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed"]);
|
|
3051
|
+
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
|
|
2670
3052
|
if (voids.has(node.tagName)) return;
|
|
2671
3053
|
|
|
2672
3054
|
// Keep overlay visible for the label, but hide selection border to not obscure editing outline.
|
|
@@ -2690,7 +3072,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2690
3072
|
const rawText = typeof tc === "string" ? tc : "";
|
|
2691
3073
|
el.textContent = rawText;
|
|
2692
3074
|
|
|
2693
|
-
componentInlineEdit = {
|
|
3075
|
+
view.componentInlineEdit = {
|
|
2694
3076
|
el,
|
|
2695
3077
|
path,
|
|
2696
3078
|
originalText: rawText,
|
|
@@ -2712,15 +3094,15 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2712
3094
|
// Document-level mousedown: clicking outside the editing element commits
|
|
2713
3095
|
// the edit and selects the new target element for inline editing.
|
|
2714
3096
|
const outsideHandler = (/** @type {any} */ evt) => {
|
|
2715
|
-
if (!componentInlineEdit) {
|
|
3097
|
+
if (!view.componentInlineEdit) {
|
|
2716
3098
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2717
3099
|
return;
|
|
2718
3100
|
}
|
|
2719
|
-
if (componentInlineEdit.el.contains(evt.target)) return; // click within editing el — let it through
|
|
3101
|
+
if (view.componentInlineEdit.el.contains(evt.target)) return; // click within editing el — let it through
|
|
2720
3102
|
// Let clicks through when the slash command menu is open
|
|
2721
3103
|
if (isSlashMenuOpen()) return;
|
|
2722
3104
|
// Let clicks inside the block action bar through
|
|
2723
|
-
if (blockActionBarEl && blockActionBarEl.contains(evt.target)) return;
|
|
3105
|
+
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
|
|
2724
3106
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2725
3107
|
|
|
2726
3108
|
// Hit-test BEFORE commit (while the current canvas DOM + elToPath are still valid)
|
|
@@ -2747,7 +3129,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2747
3129
|
}
|
|
2748
3130
|
|
|
2749
3131
|
// Commit + select new element in a single state update if possible
|
|
2750
|
-
const { el: editEl, path: editPath, originalText } = componentInlineEdit;
|
|
3132
|
+
const { el: editEl, path: editPath, originalText } = view.componentInlineEdit;
|
|
2751
3133
|
const newText = (editEl.textContent ?? "").trim();
|
|
2752
3134
|
cleanupComponentInlineEdit(editEl);
|
|
2753
3135
|
|
|
@@ -2757,26 +3139,29 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2757
3139
|
|
|
2758
3140
|
if (hitPath) {
|
|
2759
3141
|
const media = hitMedia === "base" ? null : (hitMedia ?? null);
|
|
2760
|
-
pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2761
|
-
|
|
3142
|
+
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
3143
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
|
|
2762
3144
|
if (isEmpty && pPath) {
|
|
2763
3145
|
// Remove empty node; adjust hitPath if it shifts after removal
|
|
2764
|
-
let s = removeNode(
|
|
3146
|
+
let s = removeNode(withMedia, editPath);
|
|
2765
3147
|
// If hit path is a later sibling in the same parent, adjust index
|
|
2766
3148
|
const removedIdx = /** @type {number} */ (childIndex(editPath));
|
|
2767
3149
|
const hitIdx = /** @type {number} */ (childIndex(hitPath));
|
|
2768
3150
|
const hitParent = parentElementPath(hitPath);
|
|
2769
3151
|
if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
|
|
2770
3152
|
hitPath = [...pPath, "children", hitIdx - 1];
|
|
2771
|
-
pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
3153
|
+
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2772
3154
|
}
|
|
2773
3155
|
update(selectNode(s, hitPath));
|
|
2774
3156
|
} else if (newText !== originalText) {
|
|
2775
3157
|
update(
|
|
2776
|
-
selectNode(
|
|
3158
|
+
selectNode(
|
|
3159
|
+
updateProperty(withMedia, editPath, "textContent", newText || undefined),
|
|
3160
|
+
hitPath,
|
|
3161
|
+
),
|
|
2777
3162
|
);
|
|
2778
3163
|
} else {
|
|
2779
|
-
update(selectNode(
|
|
3164
|
+
update(selectNode(withMedia, hitPath));
|
|
2780
3165
|
}
|
|
2781
3166
|
} else {
|
|
2782
3167
|
// Clicked on empty space — just commit
|
|
@@ -2791,7 +3176,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2791
3176
|
}
|
|
2792
3177
|
};
|
|
2793
3178
|
document.addEventListener("mousedown", outsideHandler, true);
|
|
2794
|
-
componentInlineEdit._outsideHandler = outsideHandler;
|
|
3179
|
+
view.componentInlineEdit._outsideHandler = outsideHandler;
|
|
2795
3180
|
|
|
2796
3181
|
// Re-render block action bar to show inline formatting buttons
|
|
2797
3182
|
renderBlockActionBar();
|
|
@@ -2815,8 +3200,8 @@ function componentInlineKeydown(e) {
|
|
|
2815
3200
|
}
|
|
2816
3201
|
|
|
2817
3202
|
function splitParagraph() {
|
|
2818
|
-
if (!componentInlineEdit) return;
|
|
2819
|
-
const { el, path, mediaName } = componentInlineEdit;
|
|
3203
|
+
if (!view.componentInlineEdit) return;
|
|
3204
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2820
3205
|
|
|
2821
3206
|
// Determine cursor offset within text
|
|
2822
3207
|
const sel = /** @type {any} */ (el.ownerDocument.defaultView?.getSelection());
|
|
@@ -2848,13 +3233,13 @@ function splitParagraph() {
|
|
|
2848
3233
|
s = insertNode(s, pPath, idx + 1, newDef);
|
|
2849
3234
|
s = selectNode(s, newPath);
|
|
2850
3235
|
|
|
2851
|
-
pendingInlineEdit = { path: newPath, mediaName };
|
|
3236
|
+
view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2852
3237
|
update(s);
|
|
2853
3238
|
}
|
|
2854
3239
|
|
|
2855
3240
|
function _commitComponentInlineEdit() {
|
|
2856
|
-
if (!componentInlineEdit) return;
|
|
2857
|
-
const { el, path, originalText } = componentInlineEdit;
|
|
3241
|
+
if (!view.componentInlineEdit) return;
|
|
3242
|
+
const { el, path, originalText } = view.componentInlineEdit;
|
|
2858
3243
|
const newText = (el.textContent ?? "").trim();
|
|
2859
3244
|
|
|
2860
3245
|
cleanupComponentInlineEdit(el);
|
|
@@ -2872,8 +3257,8 @@ function _commitComponentInlineEdit() {
|
|
|
2872
3257
|
}
|
|
2873
3258
|
|
|
2874
3259
|
function cancelComponentInlineEdit() {
|
|
2875
|
-
if (!componentInlineEdit) return;
|
|
2876
|
-
const { el } = componentInlineEdit;
|
|
3260
|
+
if (!view.componentInlineEdit) return;
|
|
3261
|
+
const { el } = view.componentInlineEdit;
|
|
2877
3262
|
cleanupComponentInlineEdit(el);
|
|
2878
3263
|
renderCanvas();
|
|
2879
3264
|
renderOverlays();
|
|
@@ -2892,10 +3277,10 @@ function cleanupComponentInlineEdit(el) {
|
|
|
2892
3277
|
el.style.pointerEvents = "";
|
|
2893
3278
|
|
|
2894
3279
|
// Remove the document-level outside-click handler
|
|
2895
|
-
if (componentInlineEdit?._outsideHandler) {
|
|
2896
|
-
document.removeEventListener("mousedown", componentInlineEdit._outsideHandler, true);
|
|
3280
|
+
if (view.componentInlineEdit?._outsideHandler) {
|
|
3281
|
+
document.removeEventListener("mousedown", view.componentInlineEdit._outsideHandler, true);
|
|
2897
3282
|
}
|
|
2898
|
-
componentInlineEdit = null;
|
|
3283
|
+
view.componentInlineEdit = null;
|
|
2899
3284
|
|
|
2900
3285
|
// Restore overlay and click interceptor
|
|
2901
3286
|
for (const p of canvasPanels) {
|
|
@@ -2907,8 +3292,8 @@ function cleanupComponentInlineEdit(el) {
|
|
|
2907
3292
|
// ─── Component-mode slash commands (delegates to shared slash-menu.js) ────────
|
|
2908
3293
|
|
|
2909
3294
|
function componentInlineInput() {
|
|
2910
|
-
if (!componentInlineEdit) return;
|
|
2911
|
-
const { el, originalText } = componentInlineEdit;
|
|
3295
|
+
if (!view.componentInlineEdit) return;
|
|
3296
|
+
const { el, originalText } = view.componentInlineEdit;
|
|
2912
3297
|
const text = el.textContent || "";
|
|
2913
3298
|
|
|
2914
3299
|
// Only trigger slash menu when the paragraph was originally empty and starts with /
|
|
@@ -2922,8 +3307,8 @@ function componentInlineInput() {
|
|
|
2922
3307
|
|
|
2923
3308
|
/** @param {any} cmd */
|
|
2924
3309
|
function handleComponentSlashSelect(cmd) {
|
|
2925
|
-
if (!componentInlineEdit) return;
|
|
2926
|
-
const { el, path, mediaName } = componentInlineEdit;
|
|
3310
|
+
if (!view.componentInlineEdit) return;
|
|
3311
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2927
3312
|
const pPath = parentElementPath(path);
|
|
2928
3313
|
const idx = /** @type {number} */ (childIndex(path));
|
|
2929
3314
|
if (!pPath) return;
|
|
@@ -2940,7 +3325,7 @@ function handleComponentSlashSelect(cmd) {
|
|
|
2940
3325
|
|
|
2941
3326
|
// If the new element has textContent, enter inline edit on it
|
|
2942
3327
|
const hasText = newDef.textContent != null;
|
|
2943
|
-
if (hasText) pendingInlineEdit = { path: newPath, mediaName };
|
|
3328
|
+
if (hasText) view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2944
3329
|
update(s);
|
|
2945
3330
|
}
|
|
2946
3331
|
|
|
@@ -2952,7 +3337,7 @@ function renderLeftPanel() {
|
|
|
2952
3337
|
/** @type {any} */
|
|
2953
3338
|
let content;
|
|
2954
3339
|
if (tab === "layers")
|
|
2955
|
-
content = canvasMode === "
|
|
3340
|
+
content = canvasMode === "settings" ? renderStylebookLayersTemplate() : renderLayersTemplate();
|
|
2956
3341
|
else if (tab === "imports")
|
|
2957
3342
|
content = renderImportsTemplate({
|
|
2958
3343
|
renderLeftPanel,
|
|
@@ -2965,20 +3350,45 @@ function renderLeftPanel() {
|
|
|
2965
3350
|
});
|
|
2966
3351
|
else if (tab === "files") content = renderFilesTemplate();
|
|
2967
3352
|
else if (tab === "blocks") content = renderElementsTemplate();
|
|
2968
|
-
else if (tab === "state")
|
|
3353
|
+
else if (tab === "state")
|
|
3354
|
+
content = renderSignalsTemplate(S, { renderLeftPanel, renderCanvas, updateSession });
|
|
2969
3355
|
else if (tab === "data")
|
|
2970
|
-
content = renderDataExplorerTemplate(S.document.state, liveScope, {
|
|
3356
|
+
content = renderDataExplorerTemplate(S.document.state, view.liveScope, {
|
|
2971
3357
|
renderCanvas,
|
|
2972
3358
|
renderLeftPanel,
|
|
2973
3359
|
defCategory,
|
|
2974
3360
|
defBadgeLabel,
|
|
2975
3361
|
});
|
|
2976
|
-
else
|
|
3362
|
+
else if (tab === "head") {
|
|
3363
|
+
// In content mode, title/$head live in S.content.frontmatter, not S.document
|
|
3364
|
+
const isContent = S.mode === "content";
|
|
3365
|
+
const fm = S.content?.frontmatter ?? {};
|
|
3366
|
+
const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
|
|
3367
|
+
content = renderHeadTemplate({
|
|
3368
|
+
document: headDoc,
|
|
3369
|
+
applyMutation: isContent
|
|
3370
|
+
? (/** @type {any} */ fn) => {
|
|
3371
|
+
// Apply mutation to a temporary doc, then sync title/$head back to frontmatter
|
|
3372
|
+
const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
|
|
3373
|
+
fn(tmp);
|
|
3374
|
+
if (tmp.title !== fm.title) S = updateFrontmatter(S, "title", tmp.title);
|
|
3375
|
+
// Always sync $head (may have been created, modified, or emptied)
|
|
3376
|
+
const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
|
|
3377
|
+
S = updateFrontmatter(S, "$head", newHead);
|
|
3378
|
+
update(S);
|
|
3379
|
+
}
|
|
3380
|
+
: (/** @type {any} */ fn) => {
|
|
3381
|
+
S = applyMutation(S, fn);
|
|
3382
|
+
update(S);
|
|
3383
|
+
},
|
|
3384
|
+
renderLeftPanel,
|
|
3385
|
+
});
|
|
3386
|
+
} else content = nothing;
|
|
2977
3387
|
|
|
2978
3388
|
litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
|
|
2979
3389
|
|
|
2980
3390
|
// Post-render side effects
|
|
2981
|
-
if (tab === "layers" && canvasMode !== "
|
|
3391
|
+
if (tab === "layers" && canvasMode !== "settings") registerLayersDnD();
|
|
2982
3392
|
else if (tab === "imports") {
|
|
2983
3393
|
/* no post-render DnD needed */
|
|
2984
3394
|
} else if (tab === "blocks") {
|
|
@@ -2993,8 +3403,8 @@ function renderLeftPanel() {
|
|
|
2993
3403
|
/** Returns a TemplateResult — called from renderLeftPanel only when tab=layers & not stylebook */
|
|
2994
3404
|
function renderLayersTemplate() {
|
|
2995
3405
|
// Clean up previous DnD registrations
|
|
2996
|
-
for (const fn of dndCleanups) fn();
|
|
2997
|
-
dndCleanups = [];
|
|
3406
|
+
for (const fn of view.dndCleanups) fn();
|
|
3407
|
+
view.dndCleanups = [];
|
|
2998
3408
|
|
|
2999
3409
|
const rows = flattenTree(S.document);
|
|
3000
3410
|
const collapsed = S._collapsed || (S._collapsed = new Set());
|
|
@@ -3014,6 +3424,9 @@ function renderLayersTemplate() {
|
|
|
3014
3424
|
}
|
|
3015
3425
|
if (hidden) continue;
|
|
3016
3426
|
|
|
3427
|
+
// In content mode, skip the document root row (it's not a real element)
|
|
3428
|
+
if (S.mode === "content" && path.length === 0) continue;
|
|
3429
|
+
|
|
3017
3430
|
// Text node children: display-only row with truncated preview
|
|
3018
3431
|
if (nodeType === "text") {
|
|
3019
3432
|
const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
|
|
@@ -3084,7 +3497,7 @@ function renderLayersTemplate() {
|
|
|
3084
3497
|
|
|
3085
3498
|
// Compute move-button availability for element nodes
|
|
3086
3499
|
const isElement = nodeType === "element";
|
|
3087
|
-
const isRoot = path.length < 2;
|
|
3500
|
+
const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
|
|
3088
3501
|
const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
|
|
3089
3502
|
const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
|
|
3090
3503
|
const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
|
|
@@ -3113,7 +3526,10 @@ function renderLayersTemplate() {
|
|
|
3113
3526
|
data-dnd-depth=${isElement ? depth : nothing}
|
|
3114
3527
|
data-dnd-void=${isElement && isVoidEl ? "" : nothing}
|
|
3115
3528
|
@click=${() => update(selectNode(S, path))}
|
|
3116
|
-
@contextmenu=${isElement
|
|
3529
|
+
@contextmenu=${isElement
|
|
3530
|
+
? (/** @type {any} */ e) =>
|
|
3531
|
+
showContextMenu(e, path, S, { onEditComponent: navigateToComponent })
|
|
3532
|
+
: nothing}
|
|
3117
3533
|
>
|
|
3118
3534
|
<span class="layer-indent" style="width:${depth * 16}px"></span>
|
|
3119
3535
|
<span class="layer-toggle"
|
|
@@ -3306,7 +3722,7 @@ function registerLayersDnD() {
|
|
|
3306
3722
|
},
|
|
3307
3723
|
}),
|
|
3308
3724
|
);
|
|
3309
|
-
dndCleanups.push(cleanup);
|
|
3725
|
+
view.dndCleanups.push(cleanup);
|
|
3310
3726
|
},
|
|
3311
3727
|
);
|
|
3312
3728
|
|
|
@@ -3323,7 +3739,7 @@ function registerLayersDnD() {
|
|
|
3323
3739
|
applyDropInstruction(instruction, srcData, targetPath);
|
|
3324
3740
|
},
|
|
3325
3741
|
});
|
|
3326
|
-
dndCleanups.push(monitorCleanup);
|
|
3742
|
+
view.dndCleanups.push(monitorCleanup);
|
|
3327
3743
|
});
|
|
3328
3744
|
}
|
|
3329
3745
|
|
|
@@ -3364,7 +3780,7 @@ function registerComponentsDnD() {
|
|
|
3364
3780
|
return { type: "block", fragment: structuredClone(instanceDef) };
|
|
3365
3781
|
},
|
|
3366
3782
|
});
|
|
3367
|
-
dndCleanups.push(cleanup);
|
|
3783
|
+
view.dndCleanups.push(cleanup);
|
|
3368
3784
|
},
|
|
3369
3785
|
);
|
|
3370
3786
|
});
|
|
@@ -3858,7 +4274,7 @@ function registerElementsDnD() {
|
|
|
3858
4274
|
return { type: "block", fragment: structuredClone(def) };
|
|
3859
4275
|
},
|
|
3860
4276
|
});
|
|
3861
|
-
dndCleanups.push(cleanup);
|
|
4277
|
+
view.dndCleanups.push(cleanup);
|
|
3862
4278
|
},
|
|
3863
4279
|
);
|
|
3864
4280
|
});
|
|
@@ -3970,7 +4386,51 @@ function hasTagStyle(rootStyle, tag) {
|
|
|
3970
4386
|
return s && typeof s === "object" && Object.keys(s).length > 0;
|
|
3971
4387
|
}
|
|
3972
4388
|
|
|
3973
|
-
function
|
|
4389
|
+
function renderSettings() {
|
|
4390
|
+
const settingsTab = S.ui.settingsTab || "stylebook";
|
|
4391
|
+
|
|
4392
|
+
// Top-level settings tabs chrome bar
|
|
4393
|
+
const settingsChromeBarTpl = html`
|
|
4394
|
+
<div
|
|
4395
|
+
class="sb-chrome settings-top-chrome"
|
|
4396
|
+
style="position:absolute;top:0;left:0;right:0;z-index:16;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
4397
|
+
>
|
|
4398
|
+
<sp-tabs
|
|
4399
|
+
size="s"
|
|
4400
|
+
selected=${settingsTab}
|
|
4401
|
+
@change=${(/** @type {any} */ e) => {
|
|
4402
|
+
updateUi("settingsTab", e.target.selected);
|
|
4403
|
+
}}
|
|
4404
|
+
>
|
|
4405
|
+
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
4406
|
+
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
4407
|
+
<sp-tab label="Collections" value="collections"></sp-tab>
|
|
4408
|
+
</sp-tabs>
|
|
4409
|
+
</div>
|
|
4410
|
+
`;
|
|
4411
|
+
|
|
4412
|
+
// Non-stylebook tabs: render editor into canvasWrap with offset for chrome bar
|
|
4413
|
+
if (settingsTab === "definitions" || settingsTab === "collections") {
|
|
4414
|
+
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
4415
|
+
|
|
4416
|
+
litRender(
|
|
4417
|
+
html`${settingsChromeBarTpl}
|
|
4418
|
+
<div
|
|
4419
|
+
class="settings-editor-container"
|
|
4420
|
+
style="position:absolute;inset:40px 0 0 0;overflow:auto"
|
|
4421
|
+
></div>`,
|
|
4422
|
+
/** @type {any} */ (canvasWrap),
|
|
4423
|
+
);
|
|
4424
|
+
|
|
4425
|
+
const container = /** @type {HTMLElement} */ (
|
|
4426
|
+
canvasWrap.querySelector(".settings-editor-container")
|
|
4427
|
+
);
|
|
4428
|
+
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
4429
|
+
else renderCollectionsEditor(container);
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
// Stylebook tab — existing behavior
|
|
3974
4434
|
stylebookElToTag = new WeakMap();
|
|
3975
4435
|
const rootStyle = getEffectiveStyle(S.document.style);
|
|
3976
4436
|
const filter = (S.ui.stylebookFilter || "").toLowerCase();
|
|
@@ -3981,38 +4441,33 @@ function renderStylebook() {
|
|
|
3981
4441
|
|
|
3982
4442
|
// Chrome bar (tabs + filter) — positioned absolutely above the panzoom surface
|
|
3983
4443
|
const onTabClick = (/** @type {string} */ t) => {
|
|
3984
|
-
|
|
3985
|
-
renderCanvas();
|
|
3986
|
-
renderOverlays();
|
|
3987
|
-
renderLeftPanel();
|
|
4444
|
+
updateUi("stylebookTab", t);
|
|
3988
4445
|
};
|
|
3989
4446
|
|
|
3990
4447
|
const onFilterInput = (/** @type {any} */ e) => {
|
|
3991
|
-
|
|
3992
|
-
renderCanvas();
|
|
3993
|
-
renderOverlays();
|
|
4448
|
+
updateUi("stylebookFilter", e.target.value);
|
|
3994
4449
|
};
|
|
3995
4450
|
|
|
3996
4451
|
const onCustomizedToggle = () => {
|
|
3997
|
-
|
|
3998
|
-
renderCanvas();
|
|
3999
|
-
renderOverlays();
|
|
4452
|
+
updateUi("stylebookCustomizedOnly", !S.ui.stylebookCustomizedOnly);
|
|
4000
4453
|
};
|
|
4001
4454
|
|
|
4002
4455
|
const chromeBarTpl = html`
|
|
4456
|
+
${settingsChromeBarTpl}
|
|
4003
4457
|
<div
|
|
4004
4458
|
class="sb-chrome"
|
|
4005
|
-
style="position:absolute;top:
|
|
4459
|
+
style="position:absolute;top:36px;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
4006
4460
|
>
|
|
4007
|
-
<sp-tabs
|
|
4461
|
+
<sp-tabs
|
|
4462
|
+
size="s"
|
|
4463
|
+
selected=${S.ui.stylebookTab || "elements"}
|
|
4464
|
+
@change=${(/** @type {any} */ e) => {
|
|
4465
|
+
onTabClick(e.target.selected);
|
|
4466
|
+
}}
|
|
4467
|
+
>
|
|
4008
4468
|
${["elements", "variables"].map(
|
|
4009
4469
|
(t) => html`
|
|
4010
|
-
<sp-tab
|
|
4011
|
-
label=${t.charAt(0).toUpperCase() + t.slice(1)}
|
|
4012
|
-
value=${t}
|
|
4013
|
-
?selected=${S.ui.stylebookTab === t}
|
|
4014
|
-
@click=${() => onTabClick(t)}
|
|
4015
|
-
></sp-tab>
|
|
4470
|
+
<sp-tab label=${t.charAt(0).toUpperCase() + t.slice(1)} value=${t}></sp-tab>
|
|
4016
4471
|
`,
|
|
4017
4472
|
)}
|
|
4018
4473
|
</sp-tabs>
|
|
@@ -4058,7 +4513,6 @@ function renderStylebook() {
|
|
|
4058
4513
|
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
4059
4514
|
});
|
|
4060
4515
|
}
|
|
4061
|
-
allPanelDefs.sort((a, b) => b.width - a.width);
|
|
4062
4516
|
}
|
|
4063
4517
|
|
|
4064
4518
|
// Render content into panels
|
|
@@ -4110,9 +4564,9 @@ function renderStylebook() {
|
|
|
4110
4564
|
${chromeBarTpl}
|
|
4111
4565
|
<div
|
|
4112
4566
|
class="panzoom-wrap"
|
|
4113
|
-
style="transform-origin:0 0;padding-top:
|
|
4567
|
+
style="transform-origin:0 0;padding-top:72px"
|
|
4114
4568
|
${ref((el) => {
|
|
4115
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
4569
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
4116
4570
|
})}
|
|
4117
4571
|
>
|
|
4118
4572
|
${panelEntries.map((e) => e.tpl)}
|
|
@@ -4586,21 +5040,6 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
4586
5040
|
|
|
4587
5041
|
// varDisplayName, friendlyNameToVar — imported from studio-utils.js
|
|
4588
5042
|
|
|
4589
|
-
/**
|
|
4590
|
-
* Convert a $media key like "--tablet" to a friendly display name "Tablet". "--" returns "Base".
|
|
4591
|
-
*
|
|
4592
|
-
* @param {any} name
|
|
4593
|
-
*/
|
|
4594
|
-
function mediaDisplayName(name) {
|
|
4595
|
-
if (name === "--") return "Base";
|
|
4596
|
-
return (
|
|
4597
|
-
name
|
|
4598
|
-
.replace(/^--/, "")
|
|
4599
|
-
.replace(/-/g, " ")
|
|
4600
|
-
.replace(/\b\w/g, (/** @type {any} */ c) => c.toUpperCase()) || name
|
|
4601
|
-
);
|
|
4602
|
-
}
|
|
4603
|
-
|
|
4604
5043
|
/**
|
|
4605
5044
|
* Convert a human-friendly name like "Tablet" to a $media key "--tablet"
|
|
4606
5045
|
*
|
|
@@ -4747,9 +5186,8 @@ function registerStylebookPanelEvents(panel) {
|
|
|
4747
5186
|
}
|
|
4748
5187
|
}
|
|
4749
5188
|
// Clicked empty area — deselect
|
|
4750
|
-
|
|
5189
|
+
updateSession({ ui: { stylebookSelection: null, activeSelector: null } });
|
|
4751
5190
|
renderStylebookOverlays();
|
|
4752
|
-
renderRightPanel();
|
|
4753
5191
|
});
|
|
4754
5192
|
|
|
4755
5193
|
overlayClk.addEventListener("mousemove", (/** @type {any} */ e) => {
|
|
@@ -4843,73 +5281,191 @@ function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
|
|
|
4843
5281
|
// ─── Right panel: Inspector ───────────────────────────────────────────────────
|
|
4844
5282
|
|
|
4845
5283
|
function renderRightPanel() {
|
|
4846
|
-
|
|
5284
|
+
rightPanelMod.render();
|
|
5285
|
+
}
|
|
4847
5286
|
|
|
4848
|
-
|
|
4849
|
-
const panelTabs = [
|
|
4850
|
-
{ value: "properties", icon: "sp-icon-properties", label: "Properties" },
|
|
4851
|
-
{ value: "events", icon: "sp-icon-event", label: "Events" },
|
|
4852
|
-
{ value: "style", icon: "sp-icon-brush", label: "Style" },
|
|
4853
|
-
];
|
|
5287
|
+
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
4854
5288
|
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
5289
|
+
/** Frontmatter-only panel shown in content mode when no element is selected */
|
|
5290
|
+
function renderFrontmatterOnlyPanel() {
|
|
5291
|
+
const fm = S.content?.frontmatter || {};
|
|
5292
|
+
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
5293
|
+
const schemaProps = col?.schema?.properties;
|
|
5294
|
+
const requiredFields = new Set(col?.schema?.required || []);
|
|
5295
|
+
|
|
5296
|
+
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
5297
|
+
const fields = [];
|
|
5298
|
+
if (schemaProps) {
|
|
5299
|
+
for (const [field, fieldSchema] of Object.entries(
|
|
5300
|
+
/** @type {Record<string, any>} */ (schemaProps),
|
|
5301
|
+
)) {
|
|
5302
|
+
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
5303
|
+
}
|
|
5304
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5305
|
+
if (!schemaProps[field]) {
|
|
5306
|
+
fields.push({
|
|
5307
|
+
field,
|
|
5308
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5309
|
+
value,
|
|
5310
|
+
});
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
} else {
|
|
5314
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5315
|
+
fields.push({
|
|
5316
|
+
field,
|
|
5317
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5318
|
+
value,
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
|
|
5323
|
+
if (fields.length === 0 && !schemaProps) {
|
|
5324
|
+
return html`<div class="empty-state">No frontmatter. Select an element to inspect.</div>`;
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
return html`
|
|
5328
|
+
<div class="style-sidebar">
|
|
5329
|
+
<sp-accordion allow-multiple size="s">
|
|
5330
|
+
<sp-accordion-item label=${col ? `Frontmatter (${col.name})` : "Frontmatter"} open>
|
|
5331
|
+
<div class="style-section-body">
|
|
5332
|
+
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
5333
|
+
</div>
|
|
5334
|
+
</sp-accordion-item>
|
|
5335
|
+
</sp-accordion>
|
|
4877
5336
|
</div>
|
|
4878
5337
|
`;
|
|
5338
|
+
}
|
|
4879
5339
|
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5340
|
+
/** Render a single frontmatter field row (shared between both panels) */
|
|
5341
|
+
function renderFmFieldRow(
|
|
5342
|
+
/** @type {string} */ field,
|
|
5343
|
+
/** @type {any} */ entry,
|
|
5344
|
+
/** @type {any} */ value,
|
|
5345
|
+
/** @type {Set<string>} */ requiredFields,
|
|
5346
|
+
) {
|
|
5347
|
+
const isRequired = requiredFields.has(field);
|
|
5348
|
+
const label = field.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
5349
|
+
const displayLabel = label + (isRequired ? " *" : "");
|
|
5350
|
+
const hasVal = value !== undefined && value !== "" && value !== false;
|
|
5351
|
+
const onClear = () => update(updateFrontmatter(S, field, undefined));
|
|
5352
|
+
|
|
5353
|
+
// Boolean → checkbox
|
|
5354
|
+
if (entry.type === "boolean") {
|
|
5355
|
+
return renderFieldRow({
|
|
5356
|
+
prop: field,
|
|
5357
|
+
label: displayLabel,
|
|
5358
|
+
hasValue: hasVal,
|
|
5359
|
+
onClear,
|
|
5360
|
+
widget: html`
|
|
5361
|
+
<sp-checkbox
|
|
5362
|
+
size="s"
|
|
5363
|
+
.checked=${live(!!value)}
|
|
5364
|
+
@change=${(/** @type {any} */ e) =>
|
|
5365
|
+
update(updateFrontmatter(S, field, e.target.checked || undefined))}
|
|
5366
|
+
></sp-checkbox>
|
|
5367
|
+
`,
|
|
4889
5368
|
});
|
|
4890
|
-
} else if (tab === "style") {
|
|
4891
|
-
try {
|
|
4892
|
-
bodyT = renderStylePanelTemplate();
|
|
4893
|
-
} catch (/** @type {any} */ e) {
|
|
4894
|
-
console.error("[renderStylePanelTemplate]", e);
|
|
4895
|
-
}
|
|
4896
5369
|
}
|
|
4897
5370
|
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5371
|
+
// Array of strings → comma-separated text
|
|
5372
|
+
if (entry.type === "array") {
|
|
5373
|
+
const display = Array.isArray(value) ? value.join(", ") : value || "";
|
|
5374
|
+
return renderFieldRow({
|
|
5375
|
+
prop: field,
|
|
5376
|
+
label: displayLabel,
|
|
5377
|
+
hasValue: hasVal,
|
|
5378
|
+
onClear,
|
|
5379
|
+
widget: html`
|
|
5380
|
+
<sp-textfield
|
|
5381
|
+
size="s"
|
|
5382
|
+
placeholder="comma, separated"
|
|
5383
|
+
.value=${live(display)}
|
|
5384
|
+
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5385
|
+
const arr = e.target.value
|
|
5386
|
+
? e.target.value
|
|
5387
|
+
.split(",")
|
|
5388
|
+
.map((/** @type {string} */ s) => s.trim())
|
|
5389
|
+
.filter(Boolean)
|
|
5390
|
+
: undefined;
|
|
5391
|
+
update(updateFrontmatter(S, field, arr));
|
|
5392
|
+
})}
|
|
5393
|
+
></sp-textfield>
|
|
5394
|
+
`,
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
4902
5397
|
|
|
4903
|
-
|
|
5398
|
+
// Enum → select
|
|
5399
|
+
if (Array.isArray(entry.enum)) {
|
|
5400
|
+
return renderFieldRow({
|
|
5401
|
+
prop: field,
|
|
5402
|
+
label: displayLabel,
|
|
5403
|
+
hasValue: hasVal,
|
|
5404
|
+
onClear,
|
|
5405
|
+
widget: html`
|
|
5406
|
+
<sp-picker
|
|
5407
|
+
size="s"
|
|
5408
|
+
.value=${live(value || "")}
|
|
5409
|
+
@change=${(/** @type {any} */ e) =>
|
|
5410
|
+
update(updateFrontmatter(S, field, e.target.value || undefined))}
|
|
5411
|
+
>
|
|
5412
|
+
${entry.enum.map(
|
|
5413
|
+
(/** @type {string} */ opt) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`,
|
|
5414
|
+
)}
|
|
5415
|
+
</sp-picker>
|
|
5416
|
+
`,
|
|
5417
|
+
});
|
|
5418
|
+
}
|
|
4904
5419
|
|
|
4905
|
-
|
|
4906
|
-
|
|
5420
|
+
// Number
|
|
5421
|
+
if (entry.type === "number") {
|
|
5422
|
+
return renderFieldRow({
|
|
5423
|
+
prop: field,
|
|
5424
|
+
label: displayLabel,
|
|
5425
|
+
hasValue: hasVal,
|
|
5426
|
+
onClear,
|
|
5427
|
+
widget: html`
|
|
5428
|
+
<sp-number-field
|
|
5429
|
+
size="s"
|
|
5430
|
+
hide-stepper
|
|
5431
|
+
.value=${live(value !== undefined ? Number(value) : undefined)}
|
|
5432
|
+
@change=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5433
|
+
const v = e.target.value;
|
|
5434
|
+
update(updateFrontmatter(S, field, isNaN(v) ? undefined : Number(v)));
|
|
5435
|
+
})}
|
|
5436
|
+
></sp-number-field>
|
|
5437
|
+
`,
|
|
5438
|
+
});
|
|
5439
|
+
}
|
|
4907
5440
|
|
|
4908
|
-
//
|
|
5441
|
+
// Default: text (handles string, date, etc.)
|
|
5442
|
+
return renderFieldRow({
|
|
5443
|
+
prop: field,
|
|
5444
|
+
label: displayLabel,
|
|
5445
|
+
hasValue: hasVal,
|
|
5446
|
+
onClear,
|
|
5447
|
+
widget: html`
|
|
5448
|
+
<sp-textfield
|
|
5449
|
+
size="s"
|
|
5450
|
+
placeholder=${entry.format === "date" ? "YYYY-MM-DD" : ""}
|
|
5451
|
+
.value=${live(value || "")}
|
|
5452
|
+
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5453
|
+
update(updateFrontmatter(S, field, e.target.value || undefined));
|
|
5454
|
+
})}
|
|
5455
|
+
></sp-textfield>
|
|
5456
|
+
`,
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
4909
5459
|
|
|
4910
5460
|
/** Properties panel — lit-html template with accordion sections */
|
|
4911
5461
|
function propertiesSidebarTemplate() {
|
|
4912
|
-
|
|
5462
|
+
// In content mode with no selection, still show frontmatter fields
|
|
5463
|
+
if (!S.selection) {
|
|
5464
|
+
if (S.mode === "content") {
|
|
5465
|
+
return renderFrontmatterOnlyPanel();
|
|
5466
|
+
}
|
|
5467
|
+
return html`<div class="empty-state">Select an element to inspect</div>`;
|
|
5468
|
+
}
|
|
4913
5469
|
const node = getNodeAtPath(S.document, S.selection);
|
|
4914
5470
|
if (!node) return html`<div class="empty-state">Node not found</div>`;
|
|
4915
5471
|
|
|
@@ -4941,21 +5497,12 @@ function propertiesSidebarTemplate() {
|
|
|
4941
5497
|
|
|
4942
5498
|
// Boolean attributes render as checkboxes
|
|
4943
5499
|
if (entry.type === "boolean") {
|
|
4944
|
-
return
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
title="Clear ${attr}"
|
|
4951
|
-
@click=${(/** @type {any} */ e) => {
|
|
4952
|
-
e.stopPropagation();
|
|
4953
|
-
update(updateAttribute(S, path, attr, undefined));
|
|
4954
|
-
}}
|
|
4955
|
-
></span>`
|
|
4956
|
-
: nothing}
|
|
4957
|
-
<sp-field-label size="s" title=${attr}>${attrLabel(entry, attr)}</sp-field-label>
|
|
4958
|
-
</div>
|
|
5500
|
+
return renderFieldRow({
|
|
5501
|
+
prop: attr,
|
|
5502
|
+
label: attrLabel(entry, attr),
|
|
5503
|
+
hasValue: hasVal,
|
|
5504
|
+
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
5505
|
+
widget: html`
|
|
4959
5506
|
<sp-checkbox
|
|
4960
5507
|
size="s"
|
|
4961
5508
|
.checked=${live(!!value)}
|
|
@@ -4963,30 +5510,19 @@ function propertiesSidebarTemplate() {
|
|
|
4963
5510
|
update(updateAttribute(S, path, attr, e.target.checked || undefined))}
|
|
4964
5511
|
>
|
|
4965
5512
|
</sp-checkbox>
|
|
4966
|
-
|
|
4967
|
-
|
|
5513
|
+
`,
|
|
5514
|
+
});
|
|
4968
5515
|
}
|
|
4969
5516
|
|
|
4970
|
-
return
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
update(updateAttribute(S, path, attr, undefined));
|
|
4980
|
-
}}
|
|
4981
|
-
></span>`
|
|
4982
|
-
: nothing}
|
|
4983
|
-
<sp-field-label size="s" title=${attr}>${attrLabel(entry, attr)}</sp-field-label>
|
|
4984
|
-
</div>
|
|
4985
|
-
${widgetForType(type, entry, attr, value || "", (/** @type {any} */ v) =>
|
|
4986
|
-
update(updateAttribute(S, path, attr, v || undefined)),
|
|
4987
|
-
)}
|
|
4988
|
-
</div>
|
|
4989
|
-
`;
|
|
5517
|
+
return renderFieldRow({
|
|
5518
|
+
prop: attr,
|
|
5519
|
+
label: attrLabel(entry, attr),
|
|
5520
|
+
hasValue: hasVal,
|
|
5521
|
+
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
5522
|
+
widget: widgetForType(type, entry, attr, value || "", (/** @type {any} */ v) =>
|
|
5523
|
+
update(updateAttribute(S, path, attr, v || undefined)),
|
|
5524
|
+
),
|
|
5525
|
+
});
|
|
4990
5526
|
}
|
|
4991
5527
|
|
|
4992
5528
|
// ── Collect applicable attributes from html-meta ──
|
|
@@ -5364,11 +5900,63 @@ function propertiesSidebarTemplate() {
|
|
|
5364
5900
|
})()
|
|
5365
5901
|
: nothing;
|
|
5366
5902
|
|
|
5903
|
+
// ── Frontmatter section (content mode only) ──
|
|
5904
|
+
const frontmatterT =
|
|
5905
|
+
S.mode === "content"
|
|
5906
|
+
? (() => {
|
|
5907
|
+
const fm = S.content?.frontmatter || {};
|
|
5908
|
+
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
5909
|
+
const schemaProps = col?.schema?.properties;
|
|
5910
|
+
const requiredFields = new Set(col?.schema?.required || []);
|
|
5911
|
+
|
|
5912
|
+
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
5913
|
+
const fields = [];
|
|
5914
|
+
if (schemaProps) {
|
|
5915
|
+
for (const [field, fieldSchema] of Object.entries(
|
|
5916
|
+
/** @type {Record<string, any>} */ (schemaProps),
|
|
5917
|
+
)) {
|
|
5918
|
+
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
5919
|
+
}
|
|
5920
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5921
|
+
if (!schemaProps[field]) {
|
|
5922
|
+
fields.push({
|
|
5923
|
+
field,
|
|
5924
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5925
|
+
value,
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
} else {
|
|
5930
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5931
|
+
fields.push({
|
|
5932
|
+
field,
|
|
5933
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5934
|
+
value,
|
|
5935
|
+
});
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
if (fields.length === 0 && !schemaProps) return nothing;
|
|
5940
|
+
|
|
5941
|
+
return html`
|
|
5942
|
+
<sp-accordion-item
|
|
5943
|
+
label=${col ? `Frontmatter (${col.name})` : "Frontmatter"}
|
|
5944
|
+
?open=${isSectionOpen("__frontmatter") !== false}
|
|
5945
|
+
@sp-accordion-item-toggle=${() => toggleSection("__frontmatter")}
|
|
5946
|
+
>
|
|
5947
|
+
<div class="style-section-body">
|
|
5948
|
+
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
5949
|
+
</div>
|
|
5950
|
+
</sp-accordion-item>
|
|
5951
|
+
`;
|
|
5952
|
+
})()
|
|
5953
|
+
: nothing;
|
|
5954
|
+
|
|
5367
5955
|
// ── Assemble ──
|
|
5368
5956
|
const tpl = html`
|
|
5369
5957
|
<div class="style-sidebar">
|
|
5370
5958
|
<sp-accordion allow-multiple size="s">
|
|
5371
|
-
${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
|
|
5959
|
+
${frontmatterT} ${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
|
|
5372
5960
|
${isMapNode ? nothing : switchT} ${isMapNode ? nothing : compPropsT}
|
|
5373
5961
|
${isMapNode ? nothing : attrSectionTemplates} ${isMapNode ? nothing : customSectionT}
|
|
5374
5962
|
${isMapNode ? nothing : mediaT} ${isMapNode ? nothing : cssPropsT}
|
|
@@ -5603,13 +6191,13 @@ function renderComponentPropsFieldsTemplate(
|
|
|
5603
6191
|
></sp-number-field>`;
|
|
5604
6192
|
} else if (parsed.kind === "combobox") {
|
|
5605
6193
|
const options = /** @type {string[]} */ (/** @type {any} */ (parsed).options);
|
|
5606
|
-
widgetTpl = html`<jx-
|
|
6194
|
+
widgetTpl = html`<jx-value-selector
|
|
5607
6195
|
.value=${String(staticVal)}
|
|
5608
6196
|
size="s"
|
|
5609
6197
|
placeholder="—"
|
|
5610
6198
|
.options=${options.map((o) => ({ value: o, label: camelToLabel(o) }))}
|
|
5611
6199
|
@change=${(/** @type {any} */ e) => onChange(e.detail?.value ?? e.target.value)}
|
|
5612
|
-
></jx-
|
|
6200
|
+
></jx-value-selector>`;
|
|
5613
6201
|
} else {
|
|
5614
6202
|
widgetTpl = html`<sp-textfield
|
|
5615
6203
|
size="s"
|
|
@@ -5839,7 +6427,7 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
|
|
|
5839
6427
|
|
|
5840
6428
|
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
5841
6429
|
|
|
5842
|
-
|
|
6430
|
+
// UNIT_RE — imported from ui/unit-selector.js
|
|
5843
6431
|
|
|
5844
6432
|
// inferInputType — imported from studio-utils.js
|
|
5845
6433
|
|
|
@@ -5867,32 +6455,118 @@ function autoOpenSections(/** @type {any} */ node, /** @type {any} */ currentSec
|
|
|
5867
6455
|
|
|
5868
6456
|
/** Get longhands for a shorthand property from css-meta */
|
|
5869
6457
|
function getLonghands(/** @type {any} */ shorthandProp) {
|
|
6458
|
+
// Check for explicit $longhands array first (used by border-side shorthands)
|
|
6459
|
+
const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[shorthandProp];
|
|
6460
|
+
if (entry?.$longhands) {
|
|
6461
|
+
return entry.$longhands
|
|
6462
|
+
.map((/** @type {string} */ name) => ({
|
|
6463
|
+
name,
|
|
6464
|
+
entry: /** @type {Record<string, any>} */ (cssMeta.$defs)[name] || { $order: 0 },
|
|
6465
|
+
}))
|
|
6466
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order);
|
|
6467
|
+
}
|
|
6468
|
+
// Fallback: reverse-lookup by $shorthand reference
|
|
5870
6469
|
const result = [];
|
|
5871
|
-
for (const [name,
|
|
5872
|
-
if (
|
|
6470
|
+
for (const [name, e] of /** @type {[string, any][]} */ (Object.entries(cssMeta.$defs))) {
|
|
6471
|
+
if (e.$shorthand === shorthandProp) result.push({ name, entry: e });
|
|
5873
6472
|
}
|
|
5874
6473
|
result.sort((a, b) => a.entry.$order - b.entry.$order);
|
|
5875
6474
|
return result;
|
|
5876
6475
|
}
|
|
5877
6476
|
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
6477
|
+
/**
|
|
6478
|
+
* Expand a CSS shorthand value (margin, padding, borderWidth, borderRadius) into individual
|
|
6479
|
+
* longhand values following the standard 1–4 value TRBL pattern. Returns an array matching the
|
|
6480
|
+
* longhand count (always 4 for box properties).
|
|
6481
|
+
*/
|
|
6482
|
+
function expandShorthand(/** @type {string} */ shortVal, /** @type {number} */ count) {
|
|
6483
|
+
if (!shortVal) return Array(count).fill("");
|
|
6484
|
+
const parts = shortVal.trim().split(/\s+/);
|
|
6485
|
+
if (count !== 4 || parts.length === 0) return Array(count).fill("");
|
|
6486
|
+
if (parts.length === 1) return [parts[0], parts[0], parts[0], parts[0]];
|
|
6487
|
+
if (parts.length === 2) return [parts[0], parts[1], parts[0], parts[1]];
|
|
6488
|
+
if (parts.length === 3) return [parts[0], parts[1], parts[2], parts[1]];
|
|
6489
|
+
return [parts[0], parts[1], parts[2], parts[3]];
|
|
6490
|
+
}
|
|
5884
6491
|
|
|
5885
|
-
/**
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
6492
|
+
/**
|
|
6493
|
+
* Compress 4 TRBL values back into the shortest valid CSS shorthand string. e.g.
|
|
6494
|
+
* ["0","auto","3rem","auto"] → "0 auto 3rem"
|
|
6495
|
+
*/
|
|
6496
|
+
function compressShorthand(/** @type {string[]} */ vals) {
|
|
6497
|
+
const [t, r, b, l] = vals;
|
|
6498
|
+
if (t === r && r === b && b === l) return t;
|
|
6499
|
+
if (t === b && r === l) return `${t} ${r}`;
|
|
6500
|
+
if (r === l) return `${t} ${r} ${b}`;
|
|
6501
|
+
return `${t} ${r} ${b} ${l}`;
|
|
6502
|
+
}
|
|
6503
|
+
|
|
6504
|
+
// ─── Border-side shorthand parsing ────────────────────────────────────────────
|
|
6505
|
+
// CSS border-side shorthand: <width> || <style> || <color> (any order, all optional)
|
|
6506
|
+
|
|
6507
|
+
const BORDER_STYLES = new Set([
|
|
6508
|
+
"none",
|
|
6509
|
+
"solid",
|
|
6510
|
+
"dashed",
|
|
6511
|
+
"dotted",
|
|
6512
|
+
"double",
|
|
6513
|
+
"groove",
|
|
6514
|
+
"ridge",
|
|
6515
|
+
"inset",
|
|
6516
|
+
"outset",
|
|
6517
|
+
"hidden",
|
|
6518
|
+
]);
|
|
6519
|
+
|
|
6520
|
+
/**
|
|
6521
|
+
* Parse a border-side shorthand value into [width, style, color].
|
|
6522
|
+
*
|
|
6523
|
+
* @param {string} value — e.g. "1px solid var(--color-border)"
|
|
6524
|
+
* @returns {string[]} — [width, style, color]
|
|
6525
|
+
*/
|
|
6526
|
+
function expandBorderSide(value) {
|
|
6527
|
+
if (!value) return ["", "", ""];
|
|
6528
|
+
// Tokenize respecting parenthesized values like var(...) and rgb(...)
|
|
6529
|
+
const tokens = [];
|
|
6530
|
+
let current = "";
|
|
6531
|
+
let depth = 0;
|
|
6532
|
+
for (const ch of value.trim()) {
|
|
6533
|
+
if (ch === "(") depth++;
|
|
6534
|
+
if (ch === ")") depth--;
|
|
6535
|
+
if (ch === " " && depth === 0) {
|
|
6536
|
+
if (current) tokens.push(current);
|
|
6537
|
+
current = "";
|
|
6538
|
+
} else {
|
|
6539
|
+
current += ch;
|
|
5893
6540
|
}
|
|
5894
6541
|
}
|
|
5895
|
-
|
|
6542
|
+
if (current) tokens.push(current);
|
|
6543
|
+
|
|
6544
|
+
let width = "";
|
|
6545
|
+
let style = "";
|
|
6546
|
+
let color = "";
|
|
6547
|
+
|
|
6548
|
+
for (const tok of tokens) {
|
|
6549
|
+
if (!style && BORDER_STYLES.has(tok)) {
|
|
6550
|
+
style = tok;
|
|
6551
|
+
} else if (!width && /^[\d.]/.test(tok)) {
|
|
6552
|
+
width = tok;
|
|
6553
|
+
} else {
|
|
6554
|
+
// Remaining token(s) are color — join in case color was split (shouldn't be with paren-aware tokenizer)
|
|
6555
|
+
color = color ? `${color} ${tok}` : tok;
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6559
|
+
return [width, style, color];
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
/**
|
|
6563
|
+
* Recompose border-side longhand values into a shorthand string.
|
|
6564
|
+
*
|
|
6565
|
+
* @param {string[]} vals — [width, style, color]
|
|
6566
|
+
* @returns {string}
|
|
6567
|
+
*/
|
|
6568
|
+
function compressBorderSide(/** @type {string[]} */ vals) {
|
|
6569
|
+
return vals.filter((v) => v && v.trim()).join(" ");
|
|
5896
6570
|
}
|
|
5897
6571
|
|
|
5898
6572
|
/** Extract --font-* CSS custom properties from the document root style. */
|
|
@@ -5908,371 +6582,6 @@ function getFontVars() {
|
|
|
5908
6582
|
return vars;
|
|
5909
6583
|
}
|
|
5910
6584
|
|
|
5911
|
-
/** Resolve a color value for display — if it's a var() reference, look up the actual color. */
|
|
5912
|
-
function resolveColorForDisplay(/** @type {any} */ val) {
|
|
5913
|
-
if (!val) return "transparent";
|
|
5914
|
-
const m = val.match(/^var\((--[^)]+)\)$/);
|
|
5915
|
-
if (m) {
|
|
5916
|
-
const style = S.document?.style;
|
|
5917
|
-
const resolved = style?.[m[1]];
|
|
5918
|
-
if (typeof resolved === "string") return resolved;
|
|
5919
|
-
return "transparent";
|
|
5920
|
-
}
|
|
5921
|
-
return val;
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
const _colorPopoverHost = createFloatingContainer();
|
|
5925
|
-
|
|
5926
|
-
function closeColorPopover() {
|
|
5927
|
-
litRender(nothing, _colorPopoverHost);
|
|
5928
|
-
_colorCallback = null;
|
|
5929
|
-
if (_colorDismissHandler) {
|
|
5930
|
-
document.removeEventListener("pointerdown", _colorDismissHandler, true);
|
|
5931
|
-
document.removeEventListener("keydown", _colorDismissHandler, true);
|
|
5932
|
-
_colorDismissHandler = null;
|
|
5933
|
-
}
|
|
5934
|
-
}
|
|
5935
|
-
|
|
5936
|
-
function openColorPopover(
|
|
5937
|
-
/** @type {any} */ anchorEl,
|
|
5938
|
-
/** @type {any} */ currentColor,
|
|
5939
|
-
/** @type {any} */ onChange,
|
|
5940
|
-
) {
|
|
5941
|
-
const colorVars = getColorVars();
|
|
5942
|
-
const rawResolved = resolveColorForDisplay(currentColor) || "#000000";
|
|
5943
|
-
// Ensure # prefix so Spectrum components return #-prefixed hex
|
|
5944
|
-
const resolvedColor =
|
|
5945
|
-
rawResolved.startsWith("#") || rawResolved.startsWith("rgb") || rawResolved.startsWith("hsl")
|
|
5946
|
-
? rawResolved
|
|
5947
|
-
: `#${rawResolved}`;
|
|
5948
|
-
|
|
5949
|
-
const popoverQuery = (/** @type {string} */ sel) => _colorPopoverHost.querySelector(sel);
|
|
5950
|
-
|
|
5951
|
-
/** Ensure hex color always has a # prefix */
|
|
5952
|
-
const normalizeHex = (/** @type {string} */ c) => {
|
|
5953
|
-
if (!c) return c;
|
|
5954
|
-
if (c.startsWith("var(") || c.startsWith("rgb") || c.startsWith("hsl")) return c;
|
|
5955
|
-
const hex = c.replace(/^#?/, "#");
|
|
5956
|
-
return hex;
|
|
5957
|
-
};
|
|
5958
|
-
|
|
5959
|
-
// Render popover content with lit-html
|
|
5960
|
-
const syncFromArea = (/** @type {any} */ _e) => {
|
|
5961
|
-
/** @type {any} */
|
|
5962
|
-
const area = popoverQuery("sp-color-area");
|
|
5963
|
-
/** @type {any} */
|
|
5964
|
-
const slider = popoverQuery("sp-color-slider");
|
|
5965
|
-
/** @type {any} */
|
|
5966
|
-
const tf = popoverQuery(".color-popover-hex");
|
|
5967
|
-
const color = normalizeHex(String(area.color));
|
|
5968
|
-
if (slider) slider.color = color;
|
|
5969
|
-
if (tf) tf.value = color;
|
|
5970
|
-
_colorCallback?.(color);
|
|
5971
|
-
};
|
|
5972
|
-
|
|
5973
|
-
const syncFromSlider = (/** @type {any} */ _e) => {
|
|
5974
|
-
/** @type {any} */
|
|
5975
|
-
const area = popoverQuery("sp-color-area");
|
|
5976
|
-
/** @type {any} */
|
|
5977
|
-
const slider = popoverQuery("sp-color-slider");
|
|
5978
|
-
/** @type {any} */
|
|
5979
|
-
const tf = popoverQuery(".color-popover-hex");
|
|
5980
|
-
const color = normalizeHex(String(slider.color));
|
|
5981
|
-
if (area) area.color = color;
|
|
5982
|
-
if (tf) tf.value = color;
|
|
5983
|
-
_colorCallback?.(color);
|
|
5984
|
-
};
|
|
5985
|
-
|
|
5986
|
-
const syncFromText = (/** @type {any} */ e) => {
|
|
5987
|
-
const val = e.target.value.trim();
|
|
5988
|
-
if (!val) return;
|
|
5989
|
-
/** @type {any} */
|
|
5990
|
-
const area = popoverQuery("sp-color-area");
|
|
5991
|
-
/** @type {any} */
|
|
5992
|
-
const slider = popoverQuery("sp-color-slider");
|
|
5993
|
-
try {
|
|
5994
|
-
if (area) area.color = val;
|
|
5995
|
-
if (slider) slider.color = val;
|
|
5996
|
-
} catch {}
|
|
5997
|
-
_colorCallback?.(val);
|
|
5998
|
-
};
|
|
5999
|
-
|
|
6000
|
-
const r = anchorEl.getBoundingClientRect();
|
|
6001
|
-
|
|
6002
|
-
litRender(
|
|
6003
|
-
html`
|
|
6004
|
-
<sp-popover
|
|
6005
|
-
open
|
|
6006
|
-
tabindex="-1"
|
|
6007
|
-
style="padding:12px;position:fixed;z-index:9999;left:${r.left}px;top:${r.bottom +
|
|
6008
|
-
4}px;overflow:visible"
|
|
6009
|
-
>
|
|
6010
|
-
<div class="color-popover-inner">
|
|
6011
|
-
<sp-color-area
|
|
6012
|
-
style="width:200px; height:150px; --mod-colorarea-width:200px; --mod-colorarea-height:150px"
|
|
6013
|
-
color=${resolvedColor}
|
|
6014
|
-
@input=${syncFromArea}
|
|
6015
|
-
></sp-color-area>
|
|
6016
|
-
<sp-color-slider
|
|
6017
|
-
style="width:200px; --mod-colorslider-length:200px"
|
|
6018
|
-
color=${resolvedColor}
|
|
6019
|
-
@input=${syncFromSlider}
|
|
6020
|
-
></sp-color-slider>
|
|
6021
|
-
<sp-textfield
|
|
6022
|
-
size="s"
|
|
6023
|
-
class="color-popover-hex"
|
|
6024
|
-
style="width:200px"
|
|
6025
|
-
.value=${live(currentColor || "")}
|
|
6026
|
-
placeholder="#000000"
|
|
6027
|
-
@change=${syncFromText}
|
|
6028
|
-
></sp-textfield>
|
|
6029
|
-
${colorVars.length > 0
|
|
6030
|
-
? html`
|
|
6031
|
-
<sp-divider size="s"></sp-divider>
|
|
6032
|
-
<span class="color-popover-swatches-label">Color Tokens</span>
|
|
6033
|
-
<sp-swatch-group size="xs" border="light" rounding="none">
|
|
6034
|
-
${colorVars.map(
|
|
6035
|
-
(cv) => html`
|
|
6036
|
-
<sp-swatch
|
|
6037
|
-
color=${cv.value}
|
|
6038
|
-
.value=${cv.name}
|
|
6039
|
-
title=${cv.name}
|
|
6040
|
-
@click=${(/** @type {any} */ e) => {
|
|
6041
|
-
e.stopPropagation();
|
|
6042
|
-
const varRef = `var(${cv.name})`;
|
|
6043
|
-
_colorCallback?.(varRef);
|
|
6044
|
-
/** @type {any} */
|
|
6045
|
-
const tf = popoverQuery(".color-popover-hex");
|
|
6046
|
-
if (tf) tf.value = varRef;
|
|
6047
|
-
}}
|
|
6048
|
-
></sp-swatch>
|
|
6049
|
-
`,
|
|
6050
|
-
)}
|
|
6051
|
-
</sp-swatch-group>
|
|
6052
|
-
`
|
|
6053
|
-
: nothing}
|
|
6054
|
-
</div>
|
|
6055
|
-
</sp-popover>
|
|
6056
|
-
`,
|
|
6057
|
-
_colorPopoverHost,
|
|
6058
|
-
);
|
|
6059
|
-
|
|
6060
|
-
_colorCallback = onChange;
|
|
6061
|
-
|
|
6062
|
-
// Dismiss on click-outside or Escape
|
|
6063
|
-
if (_colorDismissHandler) {
|
|
6064
|
-
document.removeEventListener("pointerdown", _colorDismissHandler, true);
|
|
6065
|
-
document.removeEventListener("keydown", _colorDismissHandler, true);
|
|
6066
|
-
}
|
|
6067
|
-
_colorDismissHandler = (/** @type {any} */ e) => {
|
|
6068
|
-
if (e.type === "keydown") {
|
|
6069
|
-
if (e.key === "Escape") closeColorPopover();
|
|
6070
|
-
return;
|
|
6071
|
-
}
|
|
6072
|
-
const popover = popoverQuery("sp-popover");
|
|
6073
|
-
if (popover && !popover.contains(e.target) && !anchorEl.contains(e.target)) {
|
|
6074
|
-
closeColorPopover();
|
|
6075
|
-
}
|
|
6076
|
-
};
|
|
6077
|
-
requestAnimationFrame(() => {
|
|
6078
|
-
document.addEventListener("pointerdown", _colorDismissHandler, true);
|
|
6079
|
-
document.addEventListener("keydown", _colorDismissHandler, true);
|
|
6080
|
-
});
|
|
6081
|
-
}
|
|
6082
|
-
|
|
6083
|
-
function safeColor(/** @type {any} */ val) {
|
|
6084
|
-
if (!val) return "transparent";
|
|
6085
|
-
return resolveColorForDisplay(val);
|
|
6086
|
-
}
|
|
6087
|
-
|
|
6088
|
-
function renderColorInput(
|
|
6089
|
-
/** @type {any} */ prop,
|
|
6090
|
-
/** @type {any} */ value,
|
|
6091
|
-
/** @type {any} */ onChange,
|
|
6092
|
-
) {
|
|
6093
|
-
return html`
|
|
6094
|
-
<div class="style-input-color">
|
|
6095
|
-
<sp-swatch
|
|
6096
|
-
size="s"
|
|
6097
|
-
rounding="none"
|
|
6098
|
-
border="light"
|
|
6099
|
-
color=${safeColor(value)}
|
|
6100
|
-
@click=${(/** @type {any} */ e) => {
|
|
6101
|
-
if (_colorPopoverHost.querySelector("sp-popover[open]")) {
|
|
6102
|
-
closeColorPopover();
|
|
6103
|
-
return;
|
|
6104
|
-
}
|
|
6105
|
-
openColorPopover(e.currentTarget, value, (/** @type {any} */ c) => {
|
|
6106
|
-
onChange(c);
|
|
6107
|
-
});
|
|
6108
|
-
}}
|
|
6109
|
-
></sp-swatch>
|
|
6110
|
-
<sp-textfield
|
|
6111
|
-
size="s"
|
|
6112
|
-
style="flex:1; min-width:0"
|
|
6113
|
-
.value=${live(value || "")}
|
|
6114
|
-
@input=${debouncedStyleCommit(`color:${prop}`, 400, (/** @type {any} */ e) => {
|
|
6115
|
-
onChange(e.target.value.trim());
|
|
6116
|
-
})}
|
|
6117
|
-
></sp-textfield>
|
|
6118
|
-
</div>
|
|
6119
|
-
`;
|
|
6120
|
-
}
|
|
6121
|
-
|
|
6122
|
-
function renderNumberUnitInput(
|
|
6123
|
-
/** @type {any} */ entry,
|
|
6124
|
-
/** @type {any} */ prop,
|
|
6125
|
-
/** @type {any} */ value,
|
|
6126
|
-
/** @type {any} */ onChange,
|
|
6127
|
-
) {
|
|
6128
|
-
const units = entry.$units || [];
|
|
6129
|
-
const keywords = entry.$keywords || [];
|
|
6130
|
-
const strVal = String(value ?? "");
|
|
6131
|
-
const match = strVal.match(UNIT_RE);
|
|
6132
|
-
const isKeyword = !match && strVal !== "" && keywords.includes(strVal);
|
|
6133
|
-
const isNumericVal = (/** @type {any} */ v) => /^-?\d*\.?\d*$/.test(v);
|
|
6134
|
-
|
|
6135
|
-
const currentUnit = isKeyword ? units[0] || "" : match ? match[2] || "" : units[0] || "";
|
|
6136
|
-
let displayValue;
|
|
6137
|
-
if (isKeyword) displayValue = strVal;
|
|
6138
|
-
else if (match) displayValue = match[1];
|
|
6139
|
-
else if (strVal !== "") {
|
|
6140
|
-
const num = parseFloat(strVal);
|
|
6141
|
-
displayValue = isNaN(num) ? strVal : String(num);
|
|
6142
|
-
} else displayValue = "";
|
|
6143
|
-
|
|
6144
|
-
const isExpression = isKeyword || (displayValue !== "" && !isNumericVal(displayValue));
|
|
6145
|
-
const hasUnits = units.length > 0 || keywords.length > 0;
|
|
6146
|
-
const btnId = `style-unit-${prop}`;
|
|
6147
|
-
|
|
6148
|
-
return html`
|
|
6149
|
-
<div class="style-input-number-unit">
|
|
6150
|
-
<div class=${classMap({ "input-group": true, "is-expression": isExpression })}>
|
|
6151
|
-
<sp-textfield
|
|
6152
|
-
size="s"
|
|
6153
|
-
placeholder="0"
|
|
6154
|
-
.value=${live(displayValue)}
|
|
6155
|
-
@input=${debouncedStyleCommit(`nui:${prop}`, 400, (/** @type {any} */ e) => {
|
|
6156
|
-
const val = (e.target.value ?? "").trim();
|
|
6157
|
-
if (val === "") {
|
|
6158
|
-
onChange("");
|
|
6159
|
-
return;
|
|
6160
|
-
}
|
|
6161
|
-
if (isNumericVal(val)) onChange(units.length > 0 ? val + currentUnit : val);
|
|
6162
|
-
else onChange(val);
|
|
6163
|
-
})}
|
|
6164
|
-
></sp-textfield>
|
|
6165
|
-
${hasUnits
|
|
6166
|
-
? html`
|
|
6167
|
-
<sp-picker-button id=${btnId} size="s">
|
|
6168
|
-
<span slot="label">${currentUnit || units[0] || ""}</span>
|
|
6169
|
-
</sp-picker-button>
|
|
6170
|
-
<sp-overlay trigger="${btnId}@click" placement="bottom-end" offset="4">
|
|
6171
|
-
<sp-popover style="min-width: var(--spectrum-component-width-900, 64px)">
|
|
6172
|
-
<sp-menu
|
|
6173
|
-
label="CSS unit"
|
|
6174
|
-
@change=${(/** @type {any} */ e) => {
|
|
6175
|
-
const chosen = e.target.value;
|
|
6176
|
-
if (keywords.includes(chosen)) {
|
|
6177
|
-
onChange(chosen);
|
|
6178
|
-
} else if (units.includes(chosen)) {
|
|
6179
|
-
// Re-commit with new unit
|
|
6180
|
-
const curMatch = String(value ?? "").match(UNIT_RE);
|
|
6181
|
-
const numPart = curMatch ? curMatch[1] : "";
|
|
6182
|
-
if (numPart) onChange(numPart + chosen);
|
|
6183
|
-
}
|
|
6184
|
-
}}
|
|
6185
|
-
>
|
|
6186
|
-
${units.map(
|
|
6187
|
-
(/** @type {any} */ u) => html`<sp-menu-item value=${u}>${u}</sp-menu-item>`,
|
|
6188
|
-
)}
|
|
6189
|
-
${keywords.length > 0 && units.length > 0
|
|
6190
|
-
? html`<sp-menu-divider></sp-menu-divider>`
|
|
6191
|
-
: nothing}
|
|
6192
|
-
${keywords.map(
|
|
6193
|
-
(/** @type {any} */ kw) =>
|
|
6194
|
-
html`<sp-menu-item value=${kw}>${kw}</sp-menu-item>`,
|
|
6195
|
-
)}
|
|
6196
|
-
</sp-menu>
|
|
6197
|
-
</sp-popover>
|
|
6198
|
-
</sp-overlay>
|
|
6199
|
-
`
|
|
6200
|
-
: nothing}
|
|
6201
|
-
</div>
|
|
6202
|
-
</div>
|
|
6203
|
-
`;
|
|
6204
|
-
}
|
|
6205
|
-
|
|
6206
|
-
// abbreviateValue — imported from studio-utils.js
|
|
6207
|
-
|
|
6208
|
-
/** @param {any} entry @param {any} prop @param {any} value @param {any} onChange */
|
|
6209
|
-
function renderButtonGroupInput(entry, prop, value, onChange) {
|
|
6210
|
-
const values = entry.$buttonValues || entry.enum || [];
|
|
6211
|
-
/** @type {Record<string, any>} */
|
|
6212
|
-
const iconMap = entry.$icons || {};
|
|
6213
|
-
const extra =
|
|
6214
|
-
entry.$buttonValues && entry.enum && entry.enum.length > entry.$buttonValues.length
|
|
6215
|
-
? entry.enum.filter((/** @type {any} */ v) => !entry.$buttonValues.includes(v))
|
|
6216
|
-
: [];
|
|
6217
|
-
|
|
6218
|
-
const menuId = `style-btngrp-${prop}`;
|
|
6219
|
-
const hasExtra = extra.length > 0;
|
|
6220
|
-
// If the current value is one of the extra (non-button) options, show it selected in the picker
|
|
6221
|
-
const extraSelected = hasExtra && extra.includes(value);
|
|
6222
|
-
|
|
6223
|
-
return html`
|
|
6224
|
-
<div class="button-group-combo ${hasExtra ? "has-overflow" : ""}">
|
|
6225
|
-
<sp-action-group size="s" compact>
|
|
6226
|
-
${values.map(
|
|
6227
|
-
(/** @type {any} */ v) => html`
|
|
6228
|
-
<sp-action-button
|
|
6229
|
-
size="s"
|
|
6230
|
-
title=${v}
|
|
6231
|
-
?selected=${v === value}
|
|
6232
|
-
@click=${() => onChange(v === value ? "" : v)}
|
|
6233
|
-
>
|
|
6234
|
-
${
|
|
6235
|
-
/** @type {any} */ (iconMap)[v] &&
|
|
6236
|
-
/** @type {any} */ (icons)[/** @type {any} */ (iconMap)[v]]
|
|
6237
|
-
? /** @type {any} */ (icons)[/** @type {any} */ (iconMap)[v]]
|
|
6238
|
-
: abbreviateValue(v)
|
|
6239
|
-
}
|
|
6240
|
-
</sp-action-button>
|
|
6241
|
-
`,
|
|
6242
|
-
)}
|
|
6243
|
-
</sp-action-group>
|
|
6244
|
-
${hasExtra
|
|
6245
|
-
? html`
|
|
6246
|
-
<sp-picker-button
|
|
6247
|
-
size="s"
|
|
6248
|
-
id=${menuId}
|
|
6249
|
-
class=${extraSelected ? "has-selection" : ""}
|
|
6250
|
-
></sp-picker-button>
|
|
6251
|
-
<sp-overlay trigger="${menuId}@click" placement="bottom-end" type="auto">
|
|
6252
|
-
<sp-popover>
|
|
6253
|
-
<sp-menu
|
|
6254
|
-
@change=${(/** @type {any} */ e) => {
|
|
6255
|
-
if (e.target.value) onChange(e.target.value);
|
|
6256
|
-
}}
|
|
6257
|
-
>
|
|
6258
|
-
<sp-menu-item value="__none__">—</sp-menu-item>
|
|
6259
|
-
${extra.map((/** @type {any} */ v) => {
|
|
6260
|
-
const label = v.includes("-")
|
|
6261
|
-
? kebabToLabel(v)
|
|
6262
|
-
: v.replace(/^./, (/** @type {any} */ c) => c.toUpperCase());
|
|
6263
|
-
return html`<sp-menu-item value=${v} ?selected=${v === value}
|
|
6264
|
-
>${label}</sp-menu-item
|
|
6265
|
-
>`;
|
|
6266
|
-
})}
|
|
6267
|
-
</sp-menu>
|
|
6268
|
-
</sp-popover>
|
|
6269
|
-
</sp-overlay>
|
|
6270
|
-
`
|
|
6271
|
-
: nothing}
|
|
6272
|
-
</div>
|
|
6273
|
-
`;
|
|
6274
|
-
}
|
|
6275
|
-
|
|
6276
6585
|
/** Typography CSS properties that should preview their values in-menu */
|
|
6277
6586
|
const TYPO_PREVIEW_PROPS = new Set(["fontStyle", "fontVariant", "textTransform", "textDecoration"]);
|
|
6278
6587
|
|
|
@@ -6316,7 +6625,7 @@ function renderKeywordInput(options, prop, value, onChange) {
|
|
|
6316
6625
|
return { value: v, label, style };
|
|
6317
6626
|
});
|
|
6318
6627
|
|
|
6319
|
-
return html`<jx-
|
|
6628
|
+
return html`<jx-value-selector
|
|
6320
6629
|
size="s"
|
|
6321
6630
|
.value=${value || ""}
|
|
6322
6631
|
placeholder=${cssInitialMap.get(prop) || ""}
|
|
@@ -6325,7 +6634,7 @@ function renderKeywordInput(options, prop, value, onChange) {
|
|
|
6325
6634
|
@input=${debouncedStyleCommit(`kw:${prop}`, 400, (/** @type {any} */ e) =>
|
|
6326
6635
|
onChange(e.target.value),
|
|
6327
6636
|
)}
|
|
6328
|
-
></jx-
|
|
6637
|
+
></jx-value-selector>`;
|
|
6329
6638
|
}
|
|
6330
6639
|
|
|
6331
6640
|
function renderSelectInput(
|
|
@@ -6382,7 +6691,7 @@ function handleFontSelection(
|
|
|
6382
6691
|
}
|
|
6383
6692
|
|
|
6384
6693
|
/**
|
|
6385
|
-
* Build font options array for jx-
|
|
6694
|
+
* Build font options array for jx-value-selector. Local font vars first, divider, then unadded
|
|
6386
6695
|
* presets.
|
|
6387
6696
|
*
|
|
6388
6697
|
* @param {any[]} fontVars @param {any[]} presets
|
|
@@ -6420,13 +6729,13 @@ function renderComboboxInput(
|
|
|
6420
6729
|
const presets = entry.presets || [];
|
|
6421
6730
|
const examples = entry.examples || [];
|
|
6422
6731
|
|
|
6423
|
-
// fontFamily: single jx-
|
|
6732
|
+
// fontFamily: single jx-value-selector with font options
|
|
6424
6733
|
if (prop === "fontFamily") {
|
|
6425
6734
|
// Strip var() wrapper so the component can match the option value
|
|
6426
6735
|
const varMatch = typeof value === "string" && value.match(/^var\((--[^)]+)\)$/);
|
|
6427
6736
|
const comboValue = varMatch ? varMatch[1] : value || "";
|
|
6428
6737
|
const fontOptions = buildFontOptions(fontVars, presets);
|
|
6429
|
-
return html`<jx-
|
|
6738
|
+
return html`<jx-value-selector
|
|
6430
6739
|
size="s"
|
|
6431
6740
|
.value=${comboValue}
|
|
6432
6741
|
placeholder=${cssInitialMap.get("fontFamily") || ""}
|
|
@@ -6435,7 +6744,7 @@ function renderComboboxInput(
|
|
|
6435
6744
|
@input=${debouncedStyleCommit("combo:fontFamily", 400, (/** @type {any} */ e) =>
|
|
6436
6745
|
onChange(e.target.value),
|
|
6437
6746
|
)}
|
|
6438
|
-
></jx-
|
|
6747
|
+
></jx-value-selector>`;
|
|
6439
6748
|
}
|
|
6440
6749
|
|
|
6441
6750
|
// All other comboboxes: use the shared keyword dual-mode input
|
|
@@ -6456,45 +6765,7 @@ function renderComboboxInput(
|
|
|
6456
6765
|
`;
|
|
6457
6766
|
}
|
|
6458
6767
|
|
|
6459
|
-
|
|
6460
|
-
/** @type {any} */ entry,
|
|
6461
|
-
/** @type {any} */ prop,
|
|
6462
|
-
/** @type {any} */ value,
|
|
6463
|
-
/** @type {any} */ onChange,
|
|
6464
|
-
) {
|
|
6465
|
-
return html`
|
|
6466
|
-
<sp-number-field
|
|
6467
|
-
size="s"
|
|
6468
|
-
hide-stepper
|
|
6469
|
-
.value=${live(value !== undefined && value !== "" ? Number(value) : undefined)}
|
|
6470
|
-
min=${ifDefined(entry.minimum)}
|
|
6471
|
-
max=${ifDefined(entry.maximum)}
|
|
6472
|
-
step=${ifDefined(entry.maximum !== undefined && entry.maximum <= 1 ? 0.1 : undefined)}
|
|
6473
|
-
@change=${debouncedStyleCommit(`num:${prop}`, 400, (/** @type {any} */ e) => {
|
|
6474
|
-
const v = e.target.value;
|
|
6475
|
-
if (v === undefined || isNaN(v)) onChange("");
|
|
6476
|
-
else onChange(Number(v));
|
|
6477
|
-
})}
|
|
6478
|
-
></sp-number-field>
|
|
6479
|
-
`;
|
|
6480
|
-
}
|
|
6481
|
-
|
|
6482
|
-
function renderTextInput(
|
|
6483
|
-
/** @type {any} */ prop,
|
|
6484
|
-
/** @type {any} */ value,
|
|
6485
|
-
/** @type {any} */ onChange,
|
|
6486
|
-
) {
|
|
6487
|
-
return html`
|
|
6488
|
-
<sp-textfield
|
|
6489
|
-
size="s"
|
|
6490
|
-
placeholder=${cssInitialMap.get(prop) || ""}
|
|
6491
|
-
.value=${live(value || "")}
|
|
6492
|
-
@input=${debouncedStyleCommit(`text:${prop}`, 400, (/** @type {any} */ e) =>
|
|
6493
|
-
onChange(e.target.value),
|
|
6494
|
-
)}
|
|
6495
|
-
></sp-textfield>
|
|
6496
|
-
`;
|
|
6497
|
-
}
|
|
6768
|
+
// renderNumberInput, renderTextInput — imported from ui/widgets.js
|
|
6498
6769
|
|
|
6499
6770
|
// camelToLabel, kebabToLabel, propLabel, attrLabel — imported from studio-utils.js
|
|
6500
6771
|
|
|
@@ -6504,23 +6775,13 @@ function widgetForType(
|
|
|
6504
6775
|
/** @type {any} */ prop,
|
|
6505
6776
|
/** @type {any} */ value,
|
|
6506
6777
|
/** @type {any} */ onCommit,
|
|
6778
|
+
/** @type {any} */ opts = {},
|
|
6507
6779
|
) {
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
case "number-unit":
|
|
6514
|
-
return renderNumberUnitInput(entry, prop, value, onCommit);
|
|
6515
|
-
case "number":
|
|
6516
|
-
return renderNumberInput(entry, prop, value, onCommit);
|
|
6517
|
-
case "select":
|
|
6518
|
-
return renderSelectInput(entry, prop, value, onCommit);
|
|
6519
|
-
case "combobox":
|
|
6520
|
-
return renderComboboxInput(entry, prop, value, onCommit);
|
|
6521
|
-
default:
|
|
6522
|
-
return renderTextInput(prop, value, onCommit);
|
|
6523
|
-
}
|
|
6780
|
+
return _widgetForType(type, entry, prop, value, onCommit, {
|
|
6781
|
+
placeholder: opts.placeholder || cssInitialMap.get(prop) || "",
|
|
6782
|
+
renderSelect: renderSelectInput,
|
|
6783
|
+
renderCombobox: renderComboboxInput,
|
|
6784
|
+
});
|
|
6524
6785
|
}
|
|
6525
6786
|
|
|
6526
6787
|
function renderStyleRow(
|
|
@@ -6531,31 +6792,20 @@ function renderStyleRow(
|
|
|
6531
6792
|
/** @type {any} */ onDelete,
|
|
6532
6793
|
/** @type {any} */ isWarning,
|
|
6533
6794
|
/** @type {any} */ gridMode,
|
|
6795
|
+
/** @type {any} */ inheritedValue,
|
|
6534
6796
|
) {
|
|
6535
6797
|
const type = inferInputType(entry);
|
|
6536
6798
|
const hasVal = value !== undefined && value !== "";
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
title="Clear ${prop}"
|
|
6548
|
-
@click=${(/** @type {any} */ e) => {
|
|
6549
|
-
e.stopPropagation();
|
|
6550
|
-
onDelete();
|
|
6551
|
-
}}
|
|
6552
|
-
></span>`
|
|
6553
|
-
: nothing}
|
|
6554
|
-
<sp-field-label size="s" title=${prop}>${propLabel(entry, prop)}</sp-field-label>
|
|
6555
|
-
</div>
|
|
6556
|
-
${widgetForType(type, entry, prop, value, onCommit)}
|
|
6557
|
-
</div>
|
|
6558
|
-
`;
|
|
6799
|
+
const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
|
|
6800
|
+
return renderFieldRow({
|
|
6801
|
+
prop,
|
|
6802
|
+
label: propLabel(entry, prop),
|
|
6803
|
+
hasValue: hasVal,
|
|
6804
|
+
onClear: onDelete,
|
|
6805
|
+
widget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
|
|
6806
|
+
span: gridMode && entry.$span === 2 ? 2 : undefined,
|
|
6807
|
+
warning: isWarning,
|
|
6808
|
+
});
|
|
6559
6809
|
}
|
|
6560
6810
|
|
|
6561
6811
|
function renderShorthandRow(
|
|
@@ -6564,12 +6814,14 @@ function renderShorthandRow(
|
|
|
6564
6814
|
/** @type {any} */ style,
|
|
6565
6815
|
/** @type {any} */ commitFn,
|
|
6566
6816
|
/** @type {any} */ _deleteFn,
|
|
6817
|
+
/** @type {Record<string, any>} */ inherited = {},
|
|
6567
6818
|
) {
|
|
6568
6819
|
const longhands = getLonghands(shortProp);
|
|
6569
6820
|
const shortVal = style[shortProp];
|
|
6570
|
-
const hasLonghands = longhands.some((l) => style[l.name] !== undefined);
|
|
6821
|
+
const hasLonghands = longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6571
6822
|
const isExpanded = S.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
6572
|
-
const hasAnyVal =
|
|
6823
|
+
const hasAnyVal =
|
|
6824
|
+
shortVal !== undefined || longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6573
6825
|
|
|
6574
6826
|
return html`
|
|
6575
6827
|
<div class="style-row" data-prop=${shortProp}>
|
|
@@ -6596,8 +6848,12 @@ function renderShorthandRow(
|
|
|
6596
6848
|
size="s"
|
|
6597
6849
|
.value=${live(shortVal || "")}
|
|
6598
6850
|
placeholder=${!shortVal && hasLonghands
|
|
6599
|
-
? longhands.map((l) => style[l.name] || "0").join(" ")
|
|
6600
|
-
:
|
|
6851
|
+
? longhands.map((/** @type {any} */ l) => style[l.name] || "0").join(" ")
|
|
6852
|
+
: !shortVal && inherited[shortProp]
|
|
6853
|
+
? inherited[shortProp]
|
|
6854
|
+
: !shortVal && longhands.some((/** @type {any} */ l) => inherited[l.name])
|
|
6855
|
+
? longhands.map((/** @type {any} */ l) => inherited[l.name] || "0").join(" ")
|
|
6856
|
+
: ""}
|
|
6601
6857
|
@input=${debouncedStyleCommit(`short:${shortProp}`, 400, (/** @type {any} */ e) => {
|
|
6602
6858
|
let s = S;
|
|
6603
6859
|
for (const l of longhands) {
|
|
@@ -6629,33 +6885,74 @@ function renderShorthandRow(
|
|
|
6629
6885
|
</div>
|
|
6630
6886
|
</div>
|
|
6631
6887
|
${isExpanded
|
|
6632
|
-
?
|
|
6633
|
-
const
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6888
|
+
? (() => {
|
|
6889
|
+
const isBorderSide = entry.$shorthandType === "border-side";
|
|
6890
|
+
const expanded = shortVal
|
|
6891
|
+
? isBorderSide
|
|
6892
|
+
? expandBorderSide(shortVal)
|
|
6893
|
+
: expandShorthand(shortVal, longhands.length)
|
|
6894
|
+
: null;
|
|
6895
|
+
const compress = isBorderSide ? compressBorderSide : compressShorthand;
|
|
6896
|
+
const emptyVal = isBorderSide ? "" : "0";
|
|
6897
|
+
return longhands.map(
|
|
6898
|
+
(/** @type {any} */ { name, entry: lEntry }, /** @type {any} */ idx) => {
|
|
6899
|
+
const lVal = style[name] ?? (expanded ? expanded[idx] : "");
|
|
6900
|
+
return html`
|
|
6901
|
+
<div class="style-row style-row--child" data-prop=${name}>
|
|
6902
|
+
<div class="style-row-label">
|
|
6903
|
+
${lVal !== undefined && lVal !== ""
|
|
6904
|
+
? html`<span
|
|
6905
|
+
class="set-dot"
|
|
6906
|
+
title="Clear ${name}"
|
|
6907
|
+
@click=${(/** @type {any} */ e) => {
|
|
6908
|
+
e.stopPropagation();
|
|
6909
|
+
// Recompose shorthand with this longhand cleared
|
|
6910
|
+
const vals = longhands.map(
|
|
6911
|
+
(/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6912
|
+
i === idx
|
|
6913
|
+
? emptyVal
|
|
6914
|
+
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6915
|
+
);
|
|
6916
|
+
let s = S;
|
|
6917
|
+
for (const l of longhands) {
|
|
6918
|
+
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6919
|
+
}
|
|
6920
|
+
s = commitFn(s, shortProp, compress(vals));
|
|
6921
|
+
update(s);
|
|
6922
|
+
}}
|
|
6923
|
+
></span>`
|
|
6924
|
+
: nothing}
|
|
6925
|
+
<sp-field-label size="s" title=${name}
|
|
6926
|
+
>${propLabel(lEntry, name)}</sp-field-label
|
|
6927
|
+
>
|
|
6928
|
+
</div>
|
|
6929
|
+
${widgetForType(
|
|
6930
|
+
inferInputType(lEntry),
|
|
6931
|
+
lEntry,
|
|
6932
|
+
name,
|
|
6933
|
+
lVal,
|
|
6934
|
+
(/** @type {any} */ newVal) => {
|
|
6935
|
+
// Recompose shorthand with this longhand updated
|
|
6936
|
+
const vals = longhands.map((/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6937
|
+
i === idx
|
|
6938
|
+
? newVal || emptyVal
|
|
6939
|
+
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6940
|
+
);
|
|
6941
|
+
let s = S;
|
|
6942
|
+
for (const l of longhands) {
|
|
6943
|
+
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6944
|
+
}
|
|
6945
|
+
s = commitFn(s, shortProp, compress(vals));
|
|
6946
|
+
update(s);
|
|
6947
|
+
renderRightPanel();
|
|
6948
|
+
},
|
|
6949
|
+
{ placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
|
|
6950
|
+
)}
|
|
6951
|
+
</div>
|
|
6952
|
+
`;
|
|
6953
|
+
},
|
|
6954
|
+
);
|
|
6955
|
+
})()
|
|
6659
6956
|
: nothing}
|
|
6660
6957
|
`;
|
|
6661
6958
|
}
|
|
@@ -6674,30 +6971,20 @@ function styleSidebarTemplate(
|
|
|
6674
6971
|
const mediaTabsT =
|
|
6675
6972
|
mediaNames.length > 0
|
|
6676
6973
|
? html`
|
|
6677
|
-
<sp-tabs
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6974
|
+
<sp-tabs
|
|
6975
|
+
size="s"
|
|
6976
|
+
selected=${activeTab || "base"}
|
|
6977
|
+
@change=${(/** @type {any} */ e) => {
|
|
6978
|
+
const val = e.target.selected;
|
|
6979
|
+
const newMedia = val === "base" ? null : val;
|
|
6980
|
+
if (newMedia !== S.ui.activeMedia) {
|
|
6981
|
+
updateUi("activeMedia", newMedia);
|
|
6982
|
+
}
|
|
6983
|
+
}}
|
|
6984
|
+
>
|
|
6985
|
+
<sp-tab label="Base" value="base"></sp-tab>
|
|
6688
6986
|
${mediaNames.map(
|
|
6689
|
-
(name) => html`
|
|
6690
|
-
<sp-tab
|
|
6691
|
-
label=${mediaDisplayName(name)}
|
|
6692
|
-
value=${name}
|
|
6693
|
-
?selected=${activeTab === name}
|
|
6694
|
-
@click=${() => {
|
|
6695
|
-
S = { ...S, ui: { ...S.ui, activeMedia: name } };
|
|
6696
|
-
updateActivePanelHeaders();
|
|
6697
|
-
renderRightPanel();
|
|
6698
|
-
}}
|
|
6699
|
-
></sp-tab>
|
|
6700
|
-
`,
|
|
6987
|
+
(name) => html` <sp-tab label=${mediaDisplayName(name)} value=${name}></sp-tab> `,
|
|
6701
6988
|
)}
|
|
6702
6989
|
</sp-tabs>
|
|
6703
6990
|
`
|
|
@@ -6743,8 +7030,7 @@ function styleSidebarTemplate(
|
|
|
6743
7030
|
inp.remove();
|
|
6744
7031
|
picker.style.display = "";
|
|
6745
7032
|
if (accept && v && isNestedSelector(v)) {
|
|
6746
|
-
|
|
6747
|
-
renderRightPanel();
|
|
7033
|
+
updateUi("activeSelector", v);
|
|
6748
7034
|
}
|
|
6749
7035
|
};
|
|
6750
7036
|
inp.addEventListener("keydown", (ev) => {
|
|
@@ -6755,8 +7041,7 @@ function styleSidebarTemplate(
|
|
|
6755
7041
|
return;
|
|
6756
7042
|
}
|
|
6757
7043
|
const newSelector = val === "__base__" ? null : val;
|
|
6758
|
-
|
|
6759
|
-
renderRightPanel();
|
|
7044
|
+
updateUi("activeSelector", newSelector);
|
|
6760
7045
|
}}
|
|
6761
7046
|
>
|
|
6762
7047
|
<sp-menu-item value="__base__">(base)</sp-menu-item>
|
|
@@ -6807,10 +7092,15 @@ function styleSidebarTemplate(
|
|
|
6807
7092
|
updateStyle(s, S.selection, prop, val);
|
|
6808
7093
|
}
|
|
6809
7094
|
|
|
7095
|
+
// ── Compute inherited style from higher breakpoints ──────────────────────
|
|
7096
|
+
/** @type {Record<string, any>} */
|
|
7097
|
+
const inheritedStyle = computeInheritedStyle(style, mediaNames, activeTab, activeSelector);
|
|
7098
|
+
|
|
6810
7099
|
// Auto-open sections that have properties
|
|
6811
7100
|
const newSections = autoOpenSections({ style: activeStyle }, S.ui.styleSections);
|
|
6812
7101
|
if (JSON.stringify(newSections) !== JSON.stringify(S.ui.styleSections)) {
|
|
6813
|
-
|
|
7102
|
+
session = { ...session, ui: { ...session.ui, styleSections: newSections } };
|
|
7103
|
+
S = toFlat(doc, session);
|
|
6814
7104
|
}
|
|
6815
7105
|
|
|
6816
7106
|
// Partition properties into sections
|
|
@@ -6842,7 +7132,9 @@ function styleSidebarTemplate(
|
|
|
6842
7132
|
const sectionActiveProps = entries.filter((/** @type {any} */ { prop, entry }) => {
|
|
6843
7133
|
if (activeStyle[prop] !== undefined) return true;
|
|
6844
7134
|
if (inferInputType(entry) === "shorthand") {
|
|
6845
|
-
return getLonghands(prop).some(
|
|
7135
|
+
return getLonghands(prop).some(
|
|
7136
|
+
(/** @type {any} */ l) => activeStyle[l.name] !== undefined,
|
|
7137
|
+
);
|
|
6846
7138
|
}
|
|
6847
7139
|
return false;
|
|
6848
7140
|
});
|
|
@@ -6857,9 +7149,12 @@ function styleSidebarTemplate(
|
|
|
6857
7149
|
|
|
6858
7150
|
if (type === "shorthand") {
|
|
6859
7151
|
const longhands = getLonghands(prop);
|
|
6860
|
-
const hasAny =
|
|
7152
|
+
const hasAny =
|
|
7153
|
+
hasVal || longhands.some((/** @type {any} */ l) => activeStyle[l.name] !== undefined);
|
|
6861
7154
|
if (!hasAny && !condMet) continue;
|
|
6862
|
-
rows.push(
|
|
7155
|
+
rows.push(
|
|
7156
|
+
renderShorthandRow(prop, entry, activeStyle, commitStyle, () => {}, inheritedStyle),
|
|
7157
|
+
);
|
|
6863
7158
|
} else {
|
|
6864
7159
|
const isWarning = hasVal && !condMet;
|
|
6865
7160
|
if (hasVal || condMet) {
|
|
@@ -6872,6 +7167,7 @@ function styleSidebarTemplate(
|
|
|
6872
7167
|
() => update(commitStyle(S, prop, undefined)),
|
|
6873
7168
|
isWarning,
|
|
6874
7169
|
sec.$layout === "grid",
|
|
7170
|
+
inheritedStyle[prop],
|
|
6875
7171
|
),
|
|
6876
7172
|
);
|
|
6877
7173
|
}
|
|
@@ -7003,7 +7299,7 @@ function styleSidebarTemplate(
|
|
|
7003
7299
|
|
|
7004
7300
|
/** Top-level Style panel — returns a lit-html template */
|
|
7005
7301
|
function renderStylePanelTemplate() {
|
|
7006
|
-
if (canvasMode === "
|
|
7302
|
+
if (canvasMode === "settings" && S.ui.stylebookSelection) {
|
|
7007
7303
|
const node = S.document;
|
|
7008
7304
|
if (!node) return html`<div class="empty-state">No document loaded</div>`;
|
|
7009
7305
|
return html`
|
|
@@ -7227,8 +7523,7 @@ function _renderSourceView(/** @type {any} */ container) {
|
|
|
7227
7523
|
@blur=${(/** @type {any} */ e) => {
|
|
7228
7524
|
try {
|
|
7229
7525
|
const parsed = JSON.parse(e.target.value);
|
|
7230
|
-
|
|
7231
|
-
render();
|
|
7526
|
+
update({ ...S, document: parsed, dirty: true });
|
|
7232
7527
|
} catch {}
|
|
7233
7528
|
}}
|
|
7234
7529
|
></textarea>
|
|
@@ -7253,29 +7548,31 @@ function renderFunctionEditor() {
|
|
|
7253
7548
|
const editing = S.ui.editingFunction;
|
|
7254
7549
|
|
|
7255
7550
|
// If editor already exists and matches current target, just sync value
|
|
7256
|
-
if (functionEditor && functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
7551
|
+
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
7257
7552
|
const body = getFunctionBody(editing);
|
|
7258
|
-
const currentVal = functionEditor.getValue();
|
|
7553
|
+
const currentVal = view.functionEditor.getValue();
|
|
7259
7554
|
if (currentVal !== body) {
|
|
7260
|
-
functionEditor._ignoreNextChange = true;
|
|
7261
|
-
functionEditor.setValue(body);
|
|
7555
|
+
view.functionEditor._ignoreNextChange = true;
|
|
7556
|
+
view.functionEditor.setValue(body);
|
|
7262
7557
|
}
|
|
7263
7558
|
return;
|
|
7264
7559
|
}
|
|
7265
7560
|
|
|
7266
7561
|
// Dispose previous editors
|
|
7267
|
-
if (functionEditor) {
|
|
7268
|
-
functionEditor.dispose();
|
|
7269
|
-
functionEditor = null;
|
|
7562
|
+
if (view.functionEditor) {
|
|
7563
|
+
view.functionEditor.dispose();
|
|
7564
|
+
view.functionEditor = null;
|
|
7270
7565
|
}
|
|
7271
|
-
if (monacoEditor) {
|
|
7272
|
-
monacoEditor.dispose();
|
|
7273
|
-
monacoEditor = null;
|
|
7566
|
+
if (view.monacoEditor) {
|
|
7567
|
+
view.monacoEditor.dispose();
|
|
7568
|
+
view.monacoEditor = null;
|
|
7274
7569
|
}
|
|
7275
7570
|
|
|
7276
|
-
// Clean up canvas DnD
|
|
7277
|
-
for (const fn of canvasDndCleanups) fn();
|
|
7278
|
-
canvasDndCleanups = [];
|
|
7571
|
+
// Clean up canvas DnD and event handlers
|
|
7572
|
+
for (const fn of view.canvasDndCleanups) fn();
|
|
7573
|
+
view.canvasDndCleanups = [];
|
|
7574
|
+
for (const fn of view.canvasEventCleanups) fn();
|
|
7575
|
+
view.canvasEventCleanups = [];
|
|
7279
7576
|
canvasPanels.length = 0;
|
|
7280
7577
|
|
|
7281
7578
|
litRender(nothing, canvasWrap);
|
|
@@ -7300,7 +7597,7 @@ function renderFunctionEditor() {
|
|
|
7300
7597
|
const body = getFunctionBody(editing);
|
|
7301
7598
|
const args = getFunctionArgs(editing, S);
|
|
7302
7599
|
|
|
7303
|
-
functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
7600
|
+
view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
7304
7601
|
value: body,
|
|
7305
7602
|
language: "javascript",
|
|
7306
7603
|
theme: "vs-dark",
|
|
@@ -7313,17 +7610,18 @@ function renderFunctionEditor() {
|
|
|
7313
7610
|
wordWrap: "on",
|
|
7314
7611
|
tabSize: 2,
|
|
7315
7612
|
});
|
|
7316
|
-
functionEditor._editingTarget = JSON.stringify(editing);
|
|
7613
|
+
view.functionEditor._editingTarget = JSON.stringify(editing);
|
|
7317
7614
|
|
|
7318
7615
|
// Format on open — show pretty-printed code, then run initial lint
|
|
7319
7616
|
codeService("format", { code: body, args }).then((result) => {
|
|
7320
|
-
if (result?.code != null && functionEditor) {
|
|
7321
|
-
functionEditor._ignoreNextChange = true;
|
|
7322
|
-
functionEditor.setValue(result.code);
|
|
7617
|
+
if (result?.code != null && view.functionEditor) {
|
|
7618
|
+
view.functionEditor._ignoreNextChange = true;
|
|
7619
|
+
view.functionEditor.setValue(result.code);
|
|
7323
7620
|
}
|
|
7324
7621
|
});
|
|
7325
7622
|
codeService("lint", { code: body, args }).then((result) => {
|
|
7326
|
-
if (result?.diagnostics && functionEditor)
|
|
7623
|
+
if (result?.diagnostics && view.functionEditor)
|
|
7624
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
7327
7625
|
});
|
|
7328
7626
|
|
|
7329
7627
|
// Debounced sync back to state + lint on edit
|
|
@@ -7332,15 +7630,15 @@ function renderFunctionEditor() {
|
|
|
7332
7630
|
/** @type {any} */
|
|
7333
7631
|
let lintDebounce;
|
|
7334
7632
|
let lintGen = 0;
|
|
7335
|
-
functionEditor.onDidChangeModelContent(() => {
|
|
7336
|
-
if (functionEditor._ignoreNextChange) {
|
|
7337
|
-
functionEditor._ignoreNextChange = false;
|
|
7633
|
+
view.functionEditor.onDidChangeModelContent(() => {
|
|
7634
|
+
if (view.functionEditor._ignoreNextChange) {
|
|
7635
|
+
view.functionEditor._ignoreNextChange = false;
|
|
7338
7636
|
return;
|
|
7339
7637
|
}
|
|
7340
7638
|
|
|
7341
7639
|
clearTimeout(syncDebounce);
|
|
7342
7640
|
syncDebounce = setTimeout(() => {
|
|
7343
|
-
const newBody = functionEditor.getValue();
|
|
7641
|
+
const newBody = view.functionEditor.getValue();
|
|
7344
7642
|
if (editing.type === "def") {
|
|
7345
7643
|
update(updateDef(S, editing.defName, { body: newBody }));
|
|
7346
7644
|
} else if (editing.type === "event") {
|
|
@@ -7360,11 +7658,11 @@ function renderFunctionEditor() {
|
|
|
7360
7658
|
clearTimeout(lintDebounce);
|
|
7361
7659
|
lintDebounce = setTimeout(() => {
|
|
7362
7660
|
const gen = ++lintGen;
|
|
7363
|
-
const currentCode = functionEditor.getValue();
|
|
7661
|
+
const currentCode = view.functionEditor.getValue();
|
|
7364
7662
|
codeService("lint", { code: currentCode, args }).then((result) => {
|
|
7365
7663
|
if (gen !== lintGen) return;
|
|
7366
|
-
if (result?.diagnostics && functionEditor)
|
|
7367
|
-
setLintMarkers(functionEditor, result.diagnostics);
|
|
7664
|
+
if (result?.diagnostics && view.functionEditor)
|
|
7665
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
7368
7666
|
});
|
|
7369
7667
|
}, 750);
|
|
7370
7668
|
});
|
|
@@ -7414,166 +7712,10 @@ function registerFunctionCompletions() {
|
|
|
7414
7712
|
});
|
|
7415
7713
|
}
|
|
7416
7714
|
|
|
7417
|
-
// ─── Toolbar
|
|
7715
|
+
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
7418
7716
|
|
|
7419
7717
|
function renderToolbar() {
|
|
7420
|
-
|
|
7421
|
-
const hasFunc = !!S.ui.editingFunction;
|
|
7422
|
-
|
|
7423
|
-
// Breadcrumb template
|
|
7424
|
-
const breadcrumbTpl =
|
|
7425
|
-
hasStack || hasFunc
|
|
7426
|
-
? html`
|
|
7427
|
-
<div class="breadcrumb">
|
|
7428
|
-
<sp-action-button
|
|
7429
|
-
size="s"
|
|
7430
|
-
title=${hasFunc ? "Close function editor" : "Return to parent document"}
|
|
7431
|
-
@click=${hasFunc ? closeFunctionEditor : navigateBack}
|
|
7432
|
-
>
|
|
7433
|
-
${toolbarIconMap["sp-icon-back"]}Back
|
|
7434
|
-
</sp-action-button>
|
|
7435
|
-
${hasStack
|
|
7436
|
-
? S.documentStack.map(
|
|
7437
|
-
(/** @type {any} */ frame) => html`
|
|
7438
|
-
<span class="breadcrumb-item"
|
|
7439
|
-
>${frame.documentPath?.split("/").pop() || "untitled"}</span
|
|
7440
|
-
>
|
|
7441
|
-
<span class="breadcrumb-sep"> › </span>
|
|
7442
|
-
`,
|
|
7443
|
-
)
|
|
7444
|
-
: nothing}
|
|
7445
|
-
<span
|
|
7446
|
-
class="breadcrumb-item${hasFunc ? " clickable" : " current"}"
|
|
7447
|
-
@click=${hasFunc ? closeFunctionEditor : nothing}
|
|
7448
|
-
>
|
|
7449
|
-
${S.documentPath?.split("/").pop() || S.document.tagName || "document"}
|
|
7450
|
-
</span>
|
|
7451
|
-
${hasFunc
|
|
7452
|
-
? html`
|
|
7453
|
-
<span class="breadcrumb-sep"> › </span>
|
|
7454
|
-
<span class="breadcrumb-item current"
|
|
7455
|
-
>${S.ui.editingFunction.type === "def"
|
|
7456
|
-
? `ƒ ${S.ui.editingFunction.defName}`
|
|
7457
|
-
: `ƒ ${S.ui.editingFunction.eventKey}`}</span
|
|
7458
|
-
>
|
|
7459
|
-
`
|
|
7460
|
-
: nothing}
|
|
7461
|
-
</div>
|
|
7462
|
-
`
|
|
7463
|
-
: nothing;
|
|
7464
|
-
|
|
7465
|
-
// Feature toggles
|
|
7466
|
-
const { featureQueries } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
7467
|
-
const togglesTpl =
|
|
7468
|
-
featureQueries.length > 0
|
|
7469
|
-
? html`
|
|
7470
|
-
<sp-action-group compact size="s">
|
|
7471
|
-
${featureQueries.map(
|
|
7472
|
-
({ name, query }) => html`
|
|
7473
|
-
<sp-action-button
|
|
7474
|
-
toggles
|
|
7475
|
-
size="s"
|
|
7476
|
-
title=${query}
|
|
7477
|
-
?selected=${!!S.ui.featureToggles[name]}
|
|
7478
|
-
@click=${() => {
|
|
7479
|
-
const newToggles = {
|
|
7480
|
-
...S.ui.featureToggles,
|
|
7481
|
-
[name]: !S.ui.featureToggles[name],
|
|
7482
|
-
};
|
|
7483
|
-
S = { ...S, ui: { ...S.ui, featureToggles: newToggles } };
|
|
7484
|
-
renderCanvas();
|
|
7485
|
-
renderOverlays();
|
|
7486
|
-
renderToolbar();
|
|
7487
|
-
}}
|
|
7488
|
-
>
|
|
7489
|
-
${mediaDisplayName(name)}
|
|
7490
|
-
</sp-action-button>
|
|
7491
|
-
`,
|
|
7492
|
-
)}
|
|
7493
|
-
</sp-action-group>
|
|
7494
|
-
`
|
|
7495
|
-
: nothing;
|
|
7496
|
-
|
|
7497
|
-
// Mode switcher
|
|
7498
|
-
const modes = [
|
|
7499
|
-
{ key: "edit", label: "Edit", iconTag: "sp-icon-edit" },
|
|
7500
|
-
{ key: "design", label: "Design", iconTag: "sp-icon-artboard" },
|
|
7501
|
-
{ key: "preview", label: "Preview", iconTag: "sp-icon-preview" },
|
|
7502
|
-
{ key: "source", label: "Code", iconTag: "sp-icon-code" },
|
|
7503
|
-
{ key: "stylebook", label: "Stylebook", iconTag: "sp-icon-brush" },
|
|
7504
|
-
];
|
|
7505
|
-
|
|
7506
|
-
const modeSwitcherTpl = html`
|
|
7507
|
-
<sp-action-group selects="single" size="s" compact>
|
|
7508
|
-
${modes.map(
|
|
7509
|
-
(m) => html`
|
|
7510
|
-
<sp-action-button
|
|
7511
|
-
size="s"
|
|
7512
|
-
?selected=${canvasMode === m.key}
|
|
7513
|
-
@click=${() => {
|
|
7514
|
-
if (canvasMode === m.key) return;
|
|
7515
|
-
if (S.ui.editingFunction) {
|
|
7516
|
-
if (functionEditor) {
|
|
7517
|
-
functionEditor.dispose();
|
|
7518
|
-
functionEditor = null;
|
|
7519
|
-
}
|
|
7520
|
-
S = { ...S, ui: { ...S.ui, editingFunction: null } };
|
|
7521
|
-
}
|
|
7522
|
-
canvasMode = m.key;
|
|
7523
|
-
panX = 0;
|
|
7524
|
-
panY = 0;
|
|
7525
|
-
renderCanvas();
|
|
7526
|
-
renderOverlays();
|
|
7527
|
-
renderToolbar();
|
|
7528
|
-
renderLeftPanel();
|
|
7529
|
-
if (m.key === "stylebook") {
|
|
7530
|
-
S = { ...S, ui: { ...S.ui, rightTab: "style" } };
|
|
7531
|
-
renderRightPanel();
|
|
7532
|
-
}
|
|
7533
|
-
}}
|
|
7534
|
-
>
|
|
7535
|
-
${toolbarIconMap[m.iconTag]}${m.label}
|
|
7536
|
-
</sp-action-button>
|
|
7537
|
-
`,
|
|
7538
|
-
)}
|
|
7539
|
-
</sp-action-group>
|
|
7540
|
-
`;
|
|
7541
|
-
|
|
7542
|
-
const tpl = html`
|
|
7543
|
-
<sp-action-group compact size="s">
|
|
7544
|
-
${tbBtnTpl("Open Project", openProject, "sp-icon-folder-open")}
|
|
7545
|
-
${tbBtnTpl("Open File", openFile, "sp-icon-document")}
|
|
7546
|
-
${tbBtnTpl("Save", saveFile, "sp-icon-save-floppy")}
|
|
7547
|
-
${S.fileHandle ? html`<span class="tb-filename">${S.fileHandle.name}</span>` : nothing}
|
|
7548
|
-
${S.dirty ? html`<span class="tb-dirty">●</span>` : nothing}
|
|
7549
|
-
</sp-action-group>
|
|
7550
|
-
${breadcrumbTpl}
|
|
7551
|
-
<sp-action-group compact size="s">
|
|
7552
|
-
${tbBtnTpl("Undo", () => update(undo(S)), "sp-icon-undo")}
|
|
7553
|
-
${tbBtnTpl("Redo", () => update(redo(S)), "sp-icon-redo")}
|
|
7554
|
-
</sp-action-group>
|
|
7555
|
-
<sp-action-group compact size="s">
|
|
7556
|
-
${tbBtnTpl(
|
|
7557
|
-
"Duplicate",
|
|
7558
|
-
() => {
|
|
7559
|
-
if (S.selection) update(duplicateNode(S, S.selection));
|
|
7560
|
-
},
|
|
7561
|
-
"sp-icon-duplicate",
|
|
7562
|
-
)}
|
|
7563
|
-
${tbBtnTpl(
|
|
7564
|
-
"Delete",
|
|
7565
|
-
() => {
|
|
7566
|
-
if (S.selection) update(removeNode(S, S.selection));
|
|
7567
|
-
},
|
|
7568
|
-
"sp-icon-delete",
|
|
7569
|
-
)}
|
|
7570
|
-
</sp-action-group>
|
|
7571
|
-
${togglesTpl}
|
|
7572
|
-
<div class="tb-spacer"></div>
|
|
7573
|
-
${modeSwitcherTpl}
|
|
7574
|
-
`;
|
|
7575
|
-
|
|
7576
|
-
litRender(tpl, toolbar);
|
|
7718
|
+
toolbarPanel.render();
|
|
7577
7719
|
}
|
|
7578
7720
|
|
|
7579
7721
|
// ─── File Operations (delegated to file-ops.js) ─────────────────────────────
|
|
@@ -7591,13 +7733,16 @@ function fileOpsCtx() {
|
|
|
7591
7733
|
function openFile() {
|
|
7592
7734
|
return _openFile(fileOpsCtx());
|
|
7593
7735
|
}
|
|
7594
|
-
function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
7595
|
-
const ns = _loadMarkdown(source, fileHandle);
|
|
7736
|
+
async function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
7737
|
+
const ns = await _loadMarkdown(source, fileHandle);
|
|
7596
7738
|
S = ns;
|
|
7597
7739
|
}
|
|
7598
7740
|
function saveFile() {
|
|
7599
7741
|
return _saveFile(fileOpsCtx());
|
|
7600
7742
|
}
|
|
7743
|
+
function exportFile() {
|
|
7744
|
+
return _exportFile(fileOpsCtx());
|
|
7745
|
+
}
|
|
7601
7746
|
|
|
7602
7747
|
// ─── File tree (delegated to files.js) ───────────────────────────────────────
|
|
7603
7748
|
|
|
@@ -7620,7 +7765,12 @@ function renderFilesTemplate() {
|
|
|
7620
7765
|
function openFileFromTree(/** @type {any} */ path) {
|
|
7621
7766
|
return _openFileFromTree(
|
|
7622
7767
|
{
|
|
7623
|
-
S
|
|
7768
|
+
get S() {
|
|
7769
|
+
return S;
|
|
7770
|
+
},
|
|
7771
|
+
set S(v) {
|
|
7772
|
+
S = v;
|
|
7773
|
+
},
|
|
7624
7774
|
commit: (/** @type {any} */ ns) => {
|
|
7625
7775
|
S = ns;
|
|
7626
7776
|
},
|
|
@@ -7638,16 +7788,16 @@ initShortcuts(() => ({
|
|
|
7638
7788
|
S = ns;
|
|
7639
7789
|
},
|
|
7640
7790
|
canvasMode,
|
|
7641
|
-
panX,
|
|
7642
|
-
panY,
|
|
7791
|
+
panX: view.panX,
|
|
7792
|
+
panY: view.panY,
|
|
7643
7793
|
setPan: (x, y) => {
|
|
7644
|
-
panX = x;
|
|
7645
|
-
panY = y;
|
|
7646
|
-
needsCenter = false;
|
|
7794
|
+
view.panX = x;
|
|
7795
|
+
view.panY = y;
|
|
7796
|
+
view.needsCenter = false;
|
|
7647
7797
|
},
|
|
7648
7798
|
applyTransform,
|
|
7649
7799
|
positionZoomIndicator,
|
|
7650
|
-
componentInlineEdit,
|
|
7800
|
+
componentInlineEdit: view.componentInlineEdit,
|
|
7651
7801
|
saveFile,
|
|
7652
7802
|
openProject,
|
|
7653
7803
|
enterEditOnPath(path) {
|
|
@@ -7678,8 +7828,7 @@ function scheduleAutosave() {
|
|
|
7678
7828
|
const writable = await S.fileHandle.createWritable();
|
|
7679
7829
|
await writable.write(JSON.stringify(S.document, null, 2));
|
|
7680
7830
|
await writable.close();
|
|
7681
|
-
|
|
7682
|
-
renderToolbar();
|
|
7831
|
+
update({ ...S, dirty: false });
|
|
7683
7832
|
statusMessage("Auto-saved");
|
|
7684
7833
|
} catch {}
|
|
7685
7834
|
}
|
|
@@ -7689,4 +7838,3 @@ function scheduleAutosave() {
|
|
|
7689
7838
|
addUpdateMiddleware((/** @type {any} */ state) => {
|
|
7690
7839
|
if (state.dirty) scheduleAutosave();
|
|
7691
7840
|
});
|
|
7692
|
-
// trigger rebuild
|