@jxsuite/studio 0.1.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +50941 -34749
- package/dist/studio.js.map +461 -345
- package/package.json +46 -35
- 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 +133 -0
- package/src/panels/right-panel.js +130 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/statusbar.js +15 -1
- package/src/panels/toolbar.js +223 -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 +112 -41
- package/src/studio.js +1551 -1565
- 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/canvas-media.js +151 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +68 -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,
|
|
@@ -40,7 +37,6 @@ import {
|
|
|
40
37
|
isAncestor,
|
|
41
38
|
canvasWrap,
|
|
42
39
|
leftPanel,
|
|
43
|
-
rightPanel,
|
|
44
40
|
toolbarEl,
|
|
45
41
|
elToPath,
|
|
46
42
|
canvasPanels,
|
|
@@ -58,11 +54,22 @@ import {
|
|
|
58
54
|
runUpdateMiddleware,
|
|
59
55
|
addPostRenderHook,
|
|
60
56
|
runPostRenderHooks,
|
|
57
|
+
notify,
|
|
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,17 +93,23 @@ 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,
|
|
94
101
|
} from "./utils/studio-utils.js";
|
|
95
|
-
import {
|
|
102
|
+
import {
|
|
103
|
+
renderStatusbar,
|
|
104
|
+
statusMessage,
|
|
105
|
+
setStatusbarRenderer,
|
|
106
|
+
mountStatusbar,
|
|
107
|
+
} from "./panels/statusbar.js";
|
|
96
108
|
import {
|
|
97
109
|
openFile as _openFile,
|
|
98
110
|
loadMarkdown as _loadMarkdown,
|
|
99
111
|
saveFile as _saveFile,
|
|
112
|
+
exportFile as _exportFile,
|
|
100
113
|
} from "./files/file-ops.js";
|
|
101
114
|
import {
|
|
102
115
|
loadProject as _loadProject,
|
|
@@ -107,9 +120,17 @@ import {
|
|
|
107
120
|
} from "./files/files.js";
|
|
108
121
|
import { eventsSidebarTemplate as _eventsSidebarTemplate } from "./panels/events-panel.js";
|
|
109
122
|
import { renderImportsTemplate } from "./panels/imports-panel.js";
|
|
123
|
+
import { renderHeadTemplate } from "./panels/head-panel.js";
|
|
110
124
|
import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
|
|
111
125
|
|
|
112
126
|
import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
|
|
127
|
+
import {
|
|
128
|
+
parseMediaEntries,
|
|
129
|
+
activeBreakpointsForWidth,
|
|
130
|
+
applyCanvasStyle,
|
|
131
|
+
collectMediaOverrides,
|
|
132
|
+
applyOverridesToCanvas,
|
|
133
|
+
} from "./utils/canvas-media.js";
|
|
113
134
|
import { createDevServerPlatform } from "./platforms/devserver.js";
|
|
114
135
|
import { codeService, setLintMarkers, getFunctionArgs } from "./services/code-services.js";
|
|
115
136
|
import {
|
|
@@ -146,7 +167,6 @@ import {
|
|
|
146
167
|
|
|
147
168
|
import { html, render as litRender, nothing } from "lit-html";
|
|
148
169
|
import { live } from "lit-html/directives/live.js";
|
|
149
|
-
import { classMap } from "lit-html/directives/class-map.js";
|
|
150
170
|
import { ref } from "lit-html/directives/ref.js";
|
|
151
171
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
152
172
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
@@ -161,10 +181,21 @@ import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
|
161
181
|
// Explicit class imports + registration — bare side-effect imports are tree-shaken
|
|
162
182
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
163
183
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
164
|
-
import
|
|
165
|
-
import {
|
|
184
|
+
import { renderFieldRow } from "./ui/field-row.js";
|
|
185
|
+
import { widgetForType as _widgetForType } from "./ui/widgets.js";
|
|
186
|
+
import { computeInheritedStyle } from "./utils/inherited-style.js";
|
|
187
|
+
import "./ui/panel-resize.js";
|
|
188
|
+
import { showContextMenu, dismissContextMenu } from "./editor/context-menu.js";
|
|
189
|
+
import { convertToComponent } from "./editor/convert-to-component.js";
|
|
166
190
|
import { initShortcuts } from "./editor/shortcuts.js";
|
|
167
|
-
import { renderActivityBar
|
|
191
|
+
import { renderActivityBar } from "./panels/activity-bar.js";
|
|
192
|
+
import { renderBrowse } from "./browse/browse.js";
|
|
193
|
+
import { renderCollectionsEditor } from "./settings/collections-editor.js";
|
|
194
|
+
import { renderDefsEditor } from "./settings/defs-editor.js";
|
|
195
|
+
import * as toolbarPanel from "./panels/toolbar.js";
|
|
196
|
+
import * as overlaysPanel from "./panels/overlays.js";
|
|
197
|
+
import * as rightPanelMod from "./panels/right-panel.js";
|
|
198
|
+
import { mediaDisplayName, ensureLitState } from "./panels/shared.js";
|
|
168
199
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
169
200
|
|
|
170
201
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
@@ -172,7 +203,11 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
|
172
203
|
// into their own modules, they will migrate to ctx in store.js.
|
|
173
204
|
|
|
174
205
|
/** @type {any} */
|
|
175
|
-
let S; // current state
|
|
206
|
+
let S; // current state (flat compatibility view)
|
|
207
|
+
/** @type {any} */
|
|
208
|
+
let doc = null; // doc slice (persisted, history, autosave)
|
|
209
|
+
/** @type {any} */
|
|
210
|
+
let session = null; // session slice (selection, hover, ui)
|
|
176
211
|
|
|
177
212
|
/** Creates a display:contents container appended to sp-theme or body, for floating popovers/menus. */
|
|
178
213
|
function createFloatingContainer() {
|
|
@@ -182,32 +217,7 @@ function createFloatingContainer() {
|
|
|
182
217
|
return el;
|
|
183
218
|
}
|
|
184
219
|
|
|
185
|
-
const toolbar = toolbarEl;
|
|
186
|
-
|
|
187
220
|
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
221
|
|
|
212
222
|
// ─── Component registry ───────────────────────────────────────────────────────
|
|
213
223
|
|
|
@@ -247,8 +257,8 @@ async function navigateBack() {
|
|
|
247
257
|
async function closeFunctionEditor() {
|
|
248
258
|
const editing = S.ui.editingFunction;
|
|
249
259
|
if (!editing) return;
|
|
250
|
-
if (functionEditor) {
|
|
251
|
-
const currentCode = functionEditor.getValue();
|
|
260
|
+
if (view.functionEditor) {
|
|
261
|
+
const currentCode = view.functionEditor.getValue();
|
|
252
262
|
const minResult = await codeService("minify", { code: currentCode });
|
|
253
263
|
const bodyToStore = minResult?.code ?? currentCode;
|
|
254
264
|
if (editing.type === "def") {
|
|
@@ -264,27 +274,12 @@ async function closeFunctionEditor() {
|
|
|
264
274
|
}),
|
|
265
275
|
);
|
|
266
276
|
}
|
|
267
|
-
functionEditor.dispose();
|
|
268
|
-
functionEditor = null;
|
|
277
|
+
view.functionEditor.dispose();
|
|
278
|
+
view.functionEditor = null;
|
|
269
279
|
}
|
|
270
|
-
|
|
271
|
-
renderCanvas();
|
|
272
|
-
renderToolbar();
|
|
280
|
+
updateUi("editingFunction", null);
|
|
273
281
|
}
|
|
274
282
|
|
|
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
283
|
/**
|
|
289
284
|
* Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
|
|
290
285
|
* ❯ so the runtime renders it as literal text.
|
|
@@ -482,10 +477,11 @@ function prepareForEditMode(node) {
|
|
|
482
477
|
* created element via onNodeCreated callback. Returns the live state scope on success, null on
|
|
483
478
|
* failure.
|
|
484
479
|
*
|
|
480
|
+
* @param {number} gen - Render generation for staleness detection
|
|
485
481
|
* @param {any} doc
|
|
486
482
|
* @param {any} canvasEl
|
|
487
483
|
*/
|
|
488
|
-
async function renderCanvasLive(doc, canvasEl) {
|
|
484
|
+
async function renderCanvasLive(gen, doc, canvasEl) {
|
|
489
485
|
canvasEl.innerHTML = "";
|
|
490
486
|
|
|
491
487
|
// Apply content mode typography styling
|
|
@@ -525,10 +521,44 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
525
521
|
}
|
|
526
522
|
|
|
527
523
|
try {
|
|
528
|
-
const
|
|
524
|
+
const root = projectState?.projectRoot || "";
|
|
525
|
+
const docPrefix = root ? `${root}/` : "";
|
|
526
|
+
const docBase = S.documentPath ? `${location.origin}/${docPrefix}${S.documentPath}` : undefined;
|
|
529
527
|
|
|
530
528
|
// Register custom elements so the runtime can render them
|
|
531
|
-
|
|
529
|
+
let effectiveElements = getEffectiveElements(renderDoc.$elements);
|
|
530
|
+
|
|
531
|
+
// In content mode (markdown), auto-discover components for directive-based
|
|
532
|
+
// custom elements that have no explicit $elements registration.
|
|
533
|
+
if (S.mode === "content" && componentRegistry.length > 0) {
|
|
534
|
+
const existingRefs = new Set(
|
|
535
|
+
effectiveElements.map((/** @type {any} */ e) => (typeof e === "string" ? e : e?.$ref)),
|
|
536
|
+
);
|
|
537
|
+
/** @param {any} node */
|
|
538
|
+
const collectTags = (node) => {
|
|
539
|
+
/** @type {Set<string>} */
|
|
540
|
+
const tags = new Set();
|
|
541
|
+
if (!node || typeof node !== "object") return tags;
|
|
542
|
+
if (node.tagName) tags.add(node.tagName);
|
|
543
|
+
if (Array.isArray(node.children)) {
|
|
544
|
+
for (const child of node.children) {
|
|
545
|
+
for (const t of collectTags(child)) tags.add(t);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return tags;
|
|
549
|
+
};
|
|
550
|
+
for (const tag of collectTags(renderDoc)) {
|
|
551
|
+
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
552
|
+
if (comp && comp.source !== "npm") {
|
|
553
|
+
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
554
|
+
if (!existingRefs.has(relPath)) {
|
|
555
|
+
effectiveElements.push({ $ref: relPath });
|
|
556
|
+
existingRefs.add(relPath);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
532
562
|
if (effectiveElements.length) {
|
|
533
563
|
renderDoc.$elements = effectiveElements;
|
|
534
564
|
for (const entry of effectiveElements) {
|
|
@@ -553,9 +583,55 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
553
583
|
}
|
|
554
584
|
}
|
|
555
585
|
|
|
586
|
+
// Bail out if a newer render started while we were importing elements
|
|
587
|
+
if (gen !== view.renderGeneration) return null;
|
|
588
|
+
|
|
556
589
|
// Inject site-level imports so buildScope can resolve $prototype names
|
|
557
590
|
renderDoc.imports = getEffectiveImports(renderDoc.imports);
|
|
558
591
|
|
|
592
|
+
// Apply project-level styles mirroring the compiler convention:
|
|
593
|
+
// viewport ≈ :root → CSS custom properties (they inherit down)
|
|
594
|
+
// canvasEl ≈ body → regular CSS properties (inline beats CSS defaults)
|
|
595
|
+
// This ensures project font-family, color, etc. override the
|
|
596
|
+
// content-mode fallback typography rules in the stylesheet.
|
|
597
|
+
// In edit mode, propagate to the .content-edit-canvas wrapper for seamless appearance.
|
|
598
|
+
const viewport = canvasEl.closest(".canvas-panel-viewport");
|
|
599
|
+
const editSurface = canvasMode === "edit" ? canvasEl.closest(".content-edit-canvas") : null;
|
|
600
|
+
const siteStyle = projectState?.projectConfig?.style;
|
|
601
|
+
if (viewport) {
|
|
602
|
+
viewport.style.cssText = "";
|
|
603
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
604
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
605
|
+
if (k.startsWith("--")) {
|
|
606
|
+
viewport.style.setProperty(k, String(v));
|
|
607
|
+
} else {
|
|
608
|
+
/** @type {any} */ (viewport.style)[k] = v;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (editSurface) {
|
|
614
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
615
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
616
|
+
if (k.startsWith("--")) {
|
|
617
|
+
/** @type {any} */ (editSurface).style.setProperty(k, String(v));
|
|
618
|
+
} else {
|
|
619
|
+
/** @type {any} */ (editSurface.style)[k] = v;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (siteStyle && typeof siteStyle === "object") {
|
|
625
|
+
for (const [k, v] of Object.entries(siteStyle)) {
|
|
626
|
+
if (!k.startsWith("--")) {
|
|
627
|
+
/** @type {any} */ (canvasEl.style)[k] = v;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Inject site-level $media so runtime can resolve media queries in styles
|
|
633
|
+
renderDoc.$media = getEffectiveMedia(renderDoc.$media);
|
|
634
|
+
|
|
559
635
|
// Inject $head elements (link/meta/script) into document.head
|
|
560
636
|
const effectiveHead = getEffectiveHead(renderDoc.$head);
|
|
561
637
|
if (effectiveHead.length) {
|
|
@@ -584,6 +660,8 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
584
660
|
}
|
|
585
661
|
|
|
586
662
|
const $defs = await buildScope(renderDoc, {}, docBase);
|
|
663
|
+
// Bail out if a newer render started while buildScope was running
|
|
664
|
+
if (gen !== view.renderGeneration) return null;
|
|
587
665
|
const el = /** @type {HTMLElement} */ (
|
|
588
666
|
runtimeRenderNode(renderDoc, $defs, {
|
|
589
667
|
onNodeCreated(/** @type {any} */ el, /** @type {any} */ path) {
|
|
@@ -632,7 +710,7 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
632
710
|
const editingEl = getActiveElement();
|
|
633
711
|
for (const child of canvasEl.querySelectorAll("*")) {
|
|
634
712
|
// Preserve pointer-events on the actively-edited element
|
|
635
|
-
if (componentInlineEdit && child === componentInlineEdit.el) continue;
|
|
713
|
+
if (view.componentInlineEdit && child === view.componentInlineEdit.el) continue;
|
|
636
714
|
if (editingEl && child === editingEl) continue;
|
|
637
715
|
/** @type {any} */ (child).style.pointerEvents = "none";
|
|
638
716
|
}
|
|
@@ -640,7 +718,7 @@ async function renderCanvasLive(doc, canvasEl) {
|
|
|
640
718
|
}
|
|
641
719
|
return $defs;
|
|
642
720
|
} catch (/** @type {any} */ err) {
|
|
643
|
-
console.warn("
|
|
721
|
+
console.warn("renderCanvasLive failed:", err.message, err);
|
|
644
722
|
return null;
|
|
645
723
|
}
|
|
646
724
|
}
|
|
@@ -666,43 +744,11 @@ litRender(
|
|
|
666
744
|
const cssInitialMap = new Map(/** @type {any} */ (webdata.cssProps));
|
|
667
745
|
|
|
668
746
|
// Persistent render hosts for lit-html (must be before bootstrap/render)
|
|
669
|
-
|
|
747
|
+
let zoomIndicatorHost = document.createElement("div");
|
|
670
748
|
zoomIndicatorHost.style.display = "contents";
|
|
671
749
|
document.body.appendChild(zoomIndicatorHost);
|
|
672
750
|
|
|
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
|
-
}
|
|
703
|
-
|
|
704
|
-
let elementsCollapsed = new Set();
|
|
705
|
-
let elementsFilter = "";
|
|
751
|
+
// ─── Module-level UI state (must be before render() call) ─────────────────────
|
|
706
752
|
|
|
707
753
|
// ─── Bootstrap ────────────────────────────────────────────────────────────────
|
|
708
754
|
|
|
@@ -721,65 +767,173 @@ const EMPTY_DOC = {
|
|
|
721
767
|
};
|
|
722
768
|
|
|
723
769
|
S = createState(structuredClone(EMPTY_DOC));
|
|
770
|
+
({ doc, session } = fromFlat(S));
|
|
724
771
|
|
|
725
772
|
// ─── Render loop ──────────────────────────────────────────────────────────────
|
|
726
773
|
|
|
774
|
+
// Mount extracted panel modules
|
|
775
|
+
toolbarPanel.mount(toolbarEl, {
|
|
776
|
+
navigateBack: () => navigateBack(),
|
|
777
|
+
closeFunctionEditor: () => closeFunctionEditor(),
|
|
778
|
+
openProject: () => openProject(),
|
|
779
|
+
openFile: () => openFile(),
|
|
780
|
+
saveFile: () => saveFile(),
|
|
781
|
+
parseMediaEntries,
|
|
782
|
+
getCanvasMode: () => canvasMode,
|
|
783
|
+
setCanvasMode: (/** @type {any} */ m) => {
|
|
784
|
+
canvasMode = m;
|
|
785
|
+
},
|
|
786
|
+
renderCanvas: () => renderCanvas(),
|
|
787
|
+
safeRenderRightPanel: () => safeRenderRightPanel(),
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
overlaysPanel.mount({
|
|
791
|
+
effectiveZoom,
|
|
792
|
+
getCanvasMode: () => canvasMode,
|
|
793
|
+
isEditing,
|
|
794
|
+
renderBlockActionBar,
|
|
795
|
+
findCanvasElement,
|
|
796
|
+
getActivePanel,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
rightPanelMod.mount({
|
|
800
|
+
propertiesSidebarTemplate,
|
|
801
|
+
renderStylePanelTemplate,
|
|
802
|
+
renderCanvas: () => renderCanvas(),
|
|
803
|
+
updateForcedPseudoPreview,
|
|
804
|
+
});
|
|
805
|
+
|
|
727
806
|
// Register all renderers with the store so render()/renderOnly() work
|
|
728
|
-
registerRenderer("toolbar", () =>
|
|
807
|
+
registerRenderer("toolbar", () => toolbarPanel.render());
|
|
729
808
|
registerRenderer("activityBar", () => renderActivityBar(S));
|
|
730
809
|
registerRenderer("leftPanel", () => renderLeftPanel());
|
|
731
810
|
registerRenderer("canvas", () => renderCanvas());
|
|
732
|
-
registerRenderer("rightPanel", () =>
|
|
733
|
-
registerRenderer("overlays", () =>
|
|
811
|
+
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
812
|
+
registerRenderer("overlays", () => overlaysPanel.render());
|
|
734
813
|
registerRenderer("statusbar", () => renderStatusbar(S));
|
|
735
814
|
setStatusbarRenderer(() => renderStatusbar(S));
|
|
815
|
+
mountStatusbar();
|
|
816
|
+
|
|
817
|
+
function safeRenderLeftPanel() {
|
|
818
|
+
try {
|
|
819
|
+
ensureLitState(leftPanel);
|
|
820
|
+
renderLeftPanel();
|
|
821
|
+
} catch (e) {
|
|
822
|
+
console.error("renderLeftPanel error:", e);
|
|
823
|
+
try {
|
|
824
|
+
leftPanel.textContent = "";
|
|
825
|
+
// @ts-ignore
|
|
826
|
+
delete leftPanel["_$litPart$"];
|
|
827
|
+
renderLeftPanel();
|
|
828
|
+
} catch (e2) {
|
|
829
|
+
console.error("renderLeftPanel retry failed:", e2);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function safeRenderRightPanel() {
|
|
835
|
+
rightPanelMod.render();
|
|
836
|
+
}
|
|
736
837
|
|
|
737
838
|
// Register the update implementation with the store
|
|
738
839
|
setGetStateFn(() => S);
|
|
739
840
|
setUpdateFn(function _update(/** @type {any} */ newState) {
|
|
841
|
+
const prev = S;
|
|
740
842
|
const prevDoc = S.document;
|
|
741
843
|
const prevSel = S.selection;
|
|
742
844
|
S = newState;
|
|
743
845
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
846
|
+
// Keep doc/session slices in sync with flat S
|
|
847
|
+
({ doc, session } = fromFlat(S));
|
|
848
|
+
|
|
849
|
+
const docChanged = prevDoc !== S.document;
|
|
850
|
+
const selChanged = !pathsEqual(prevSel, S.selection);
|
|
851
|
+
const modeChanged = prev.mode !== S.mode;
|
|
852
|
+
const uiChanged = prev.ui !== S.ui;
|
|
853
|
+
|
|
854
|
+
const canvasUiChanged =
|
|
855
|
+
uiChanged &&
|
|
856
|
+
(prev.ui?.editingFunction !== S.ui?.editingFunction ||
|
|
857
|
+
prev.ui?.settingsTab !== S.ui?.settingsTab ||
|
|
858
|
+
prev.ui?.stylebookTab !== S.ui?.stylebookTab ||
|
|
859
|
+
prev.ui?.stylebookFilter !== S.ui?.stylebookFilter ||
|
|
860
|
+
prev.ui?.stylebookCustomizedOnly !== S.ui?.stylebookCustomizedOnly ||
|
|
861
|
+
prev.ui?.featureToggles !== S.ui?.featureToggles);
|
|
862
|
+
const leftUiChanged =
|
|
863
|
+
uiChanged && (prev.ui?.leftTab !== S.ui?.leftTab || prev.ui?.settingsTab !== S.ui?.settingsTab);
|
|
864
|
+
|
|
865
|
+
if (docChanged || modeChanged || canvasUiChanged) {
|
|
747
866
|
try {
|
|
748
867
|
renderCanvas();
|
|
749
868
|
} catch (e) {
|
|
750
|
-
console.
|
|
869
|
+
console.error("renderCanvas error:", e);
|
|
751
870
|
}
|
|
752
|
-
|
|
753
|
-
} else if (
|
|
754
|
-
|
|
871
|
+
safeRenderLeftPanel();
|
|
872
|
+
} else if (selChanged || leftUiChanged) {
|
|
873
|
+
safeRenderLeftPanel();
|
|
755
874
|
}
|
|
756
875
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
// Also re-render when color popover is open (changes come from outside rightPanel)
|
|
760
|
-
const colorPopoverOpen = !!_colorPopoverHost.querySelector("sp-popover[open]");
|
|
761
|
-
const activeTag = document.activeElement?.tagName;
|
|
762
|
-
const rightHasFocus =
|
|
763
|
-
!colorPopoverOpen &&
|
|
764
|
-
rightPanel.contains(document.activeElement) &&
|
|
765
|
-
(activeTag === "INPUT" ||
|
|
766
|
-
activeTag === "TEXTAREA" ||
|
|
767
|
-
activeTag === "SP-TEXTFIELD" ||
|
|
768
|
-
activeTag === "SP-NUMBER-FIELD" ||
|
|
769
|
-
activeTag === "SP-PICKER" ||
|
|
770
|
-
activeTag === "SP-COMBOBOX" ||
|
|
771
|
-
activeTag === "SP-SEARCH");
|
|
772
|
-
if (!rightHasFocus || !pathsEqual(prevSel, S.selection)) {
|
|
773
|
-
renderRightPanel();
|
|
876
|
+
if (uiChanged && prev.ui?.activeMedia !== S.ui?.activeMedia) {
|
|
877
|
+
updateActivePanelHeaders();
|
|
774
878
|
}
|
|
775
|
-
renderOverlays();
|
|
776
|
-
renderStatusbar(S);
|
|
777
879
|
|
|
778
|
-
// Post-render hooks (pseudo-state preview, pending inline edit, etc.)
|
|
779
880
|
runPostRenderHooks(prevDoc, prevSel);
|
|
780
|
-
|
|
781
|
-
// Update middleware (autosave, etc.)
|
|
782
881
|
runUpdateMiddleware(S);
|
|
882
|
+
|
|
883
|
+
notify({
|
|
884
|
+
doc: docChanged,
|
|
885
|
+
selection: selChanged,
|
|
886
|
+
hover: false,
|
|
887
|
+
ui: uiChanged,
|
|
888
|
+
mode: modeChanged,
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Register session dispatch — lightweight path for selection/hover/ui changes
|
|
893
|
+
setGetDocFn(() => doc);
|
|
894
|
+
setGetSessionFn(() => session);
|
|
895
|
+
setUpdateSessionFn(function _updateSession(/** @type {any} */ patch) {
|
|
896
|
+
const prev = session;
|
|
897
|
+
session = { ...session, ...patch };
|
|
898
|
+
if (patch.ui) {
|
|
899
|
+
session.ui = { ...prev.ui, ...patch.ui };
|
|
900
|
+
}
|
|
901
|
+
S = toFlat(doc, session);
|
|
902
|
+
|
|
903
|
+
const selChanged = !pathsEqual(prev.selection, session.selection);
|
|
904
|
+
const uiChanged = prev.ui !== session.ui;
|
|
905
|
+
|
|
906
|
+
const canvasUiChanged =
|
|
907
|
+
uiChanged &&
|
|
908
|
+
(prev.ui?.editingFunction !== session.ui?.editingFunction ||
|
|
909
|
+
prev.ui?.settingsTab !== session.ui?.settingsTab ||
|
|
910
|
+
prev.ui?.stylebookTab !== session.ui?.stylebookTab ||
|
|
911
|
+
prev.ui?.stylebookFilter !== session.ui?.stylebookFilter ||
|
|
912
|
+
prev.ui?.stylebookCustomizedOnly !== session.ui?.stylebookCustomizedOnly ||
|
|
913
|
+
prev.ui?.featureToggles !== session.ui?.featureToggles);
|
|
914
|
+
const leftUiChanged =
|
|
915
|
+
uiChanged &&
|
|
916
|
+
(prev.ui?.leftTab !== session.ui?.leftTab || prev.ui?.settingsTab !== session.ui?.settingsTab);
|
|
917
|
+
|
|
918
|
+
if (canvasUiChanged) {
|
|
919
|
+
try {
|
|
920
|
+
renderCanvas();
|
|
921
|
+
} catch (e) {
|
|
922
|
+
console.error("renderCanvas error:", e);
|
|
923
|
+
}
|
|
924
|
+
safeRenderLeftPanel();
|
|
925
|
+
} else if (selChanged || leftUiChanged) {
|
|
926
|
+
safeRenderLeftPanel();
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (uiChanged && prev.ui?.activeMedia !== session.ui?.activeMedia) {
|
|
930
|
+
updateActivePanelHeaders();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
runPostRenderHooks(doc.document, prev.selection);
|
|
934
|
+
|
|
935
|
+
const hoverChanged = prev.hover !== session.hover;
|
|
936
|
+
notify({ doc: false, selection: selChanged, hover: hoverChanged, ui: uiChanged, mode: false });
|
|
783
937
|
});
|
|
784
938
|
|
|
785
939
|
// Register post-render hook for pseudo-state preview
|
|
@@ -787,9 +941,9 @@ addPostRenderHook(() => updateForcedPseudoPreview());
|
|
|
787
941
|
|
|
788
942
|
// Register post-render hook for pending inline edit
|
|
789
943
|
addPostRenderHook((/** @type {any} */ prevDoc) => {
|
|
790
|
-
if (pendingInlineEdit && prevDoc === S.document) {
|
|
791
|
-
const { path, mediaName: mn } = pendingInlineEdit;
|
|
792
|
-
pendingInlineEdit = null;
|
|
944
|
+
if (view.pendingInlineEdit && prevDoc === S.document) {
|
|
945
|
+
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
946
|
+
view.pendingInlineEdit = null;
|
|
793
947
|
const targetPanel =
|
|
794
948
|
canvasPanels.find((/** @type {any} */ p) => p.mediaName === mn) || canvasPanels[0];
|
|
795
949
|
if (targetPanel) {
|
|
@@ -806,7 +960,9 @@ const _openParam = new URLSearchParams(location.search).get("open");
|
|
|
806
960
|
|
|
807
961
|
if (_openParam) {
|
|
808
962
|
// ?open= mode: skip normal loadProject, set up site context from the path
|
|
809
|
-
|
|
963
|
+
const isAbsPath =
|
|
964
|
+
_openParam.startsWith("/") || _openParam.startsWith("~") || /^[A-Za-z]:[/\\]/.test(_openParam);
|
|
965
|
+
if (!isAbsPath) {
|
|
810
966
|
statusMessage(`Error: ?open= requires an absolute path (got "${_openParam}")`);
|
|
811
967
|
render();
|
|
812
968
|
} else {
|
|
@@ -819,13 +975,17 @@ if (_openParam) {
|
|
|
819
975
|
: { sitePath: null };
|
|
820
976
|
|
|
821
977
|
if (siteCtx.sitePath) {
|
|
822
|
-
// Set PAL project root to
|
|
823
|
-
if (siteCtx.
|
|
978
|
+
// Set PAL project root to absolute path so file ops work
|
|
979
|
+
if (siteCtx.sitePath) {
|
|
980
|
+
platform.projectRoot = siteCtx.sitePath;
|
|
981
|
+
// Await activation so the server resolves project-relative static files
|
|
982
|
+
if (platform.activate) await platform.activate();
|
|
983
|
+
}
|
|
824
984
|
|
|
825
985
|
setProjectState({
|
|
826
986
|
root: siteCtx.sitePath,
|
|
827
987
|
name: siteCtx.projectConfig?.name || "Project",
|
|
828
|
-
projectRoot: siteCtx.
|
|
988
|
+
projectRoot: siteCtx.sitePath,
|
|
829
989
|
isSiteProject: true,
|
|
830
990
|
projectConfig: siteCtx.projectConfig,
|
|
831
991
|
projectDirs: [],
|
|
@@ -837,27 +997,40 @@ if (_openParam) {
|
|
|
837
997
|
|
|
838
998
|
await loadComponentRegistry();
|
|
839
999
|
|
|
840
|
-
// Load directory tree
|
|
1000
|
+
// Load directory tree and populate projectDirs from conventional dirs found
|
|
1001
|
+
const conventionalDirs = [
|
|
1002
|
+
"pages",
|
|
1003
|
+
"layouts",
|
|
1004
|
+
"components",
|
|
1005
|
+
"content",
|
|
1006
|
+
"data",
|
|
1007
|
+
"public",
|
|
1008
|
+
"styles",
|
|
1009
|
+
];
|
|
841
1010
|
const dirEntries = await platform.listDirectory(".");
|
|
842
1011
|
projectState.dirs.set(".", dirEntries);
|
|
1012
|
+
const foundDirs = [];
|
|
843
1013
|
for (const e of dirEntries) {
|
|
844
|
-
if (e.type === "directory" &&
|
|
1014
|
+
if (e.type === "directory" && conventionalDirs.includes(e.name)) {
|
|
1015
|
+
foundDirs.push(e.name);
|
|
845
1016
|
projectState.expanded.add(e.path || e.name);
|
|
846
1017
|
const sub = await platform.listDirectory(e.path || e.name);
|
|
847
1018
|
projectState.dirs.set(e.path || e.name, sub);
|
|
848
1019
|
}
|
|
849
1020
|
}
|
|
1021
|
+
projectState.projectDirs = foundDirs;
|
|
850
1022
|
}
|
|
851
1023
|
|
|
852
1024
|
// Read and open the file
|
|
853
1025
|
const fileRelPath = siteCtx.fileRelPath || _openParam;
|
|
854
1026
|
const content = await platform.readFile(fileRelPath);
|
|
855
1027
|
if (content) {
|
|
856
|
-
const
|
|
857
|
-
S = createState(
|
|
1028
|
+
const parsed = JSON.parse(content);
|
|
1029
|
+
S = createState(parsed);
|
|
858
1030
|
S.dirty = false;
|
|
859
1031
|
S.documentPath = fileRelPath;
|
|
860
1032
|
S.ui = { ...S.ui, leftTab: "files" };
|
|
1033
|
+
({ doc, session } = fromFlat(S));
|
|
861
1034
|
render();
|
|
862
1035
|
statusMessage(`Opened ${_openParam}`);
|
|
863
1036
|
}
|
|
@@ -875,88 +1048,41 @@ if (_openParam) {
|
|
|
875
1048
|
// ─── Media helpers ────────────────────────────────────────────────────────────
|
|
876
1049
|
|
|
877
1050
|
/**
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* @param {any} mediaDef
|
|
882
|
-
*/
|
|
883
|
-
function parseMediaEntries(mediaDef) {
|
|
884
|
-
if (!mediaDef) return { sizeBreakpoints: [], featureQueries: [], baseWidth: 320 };
|
|
885
|
-
const sizes = [],
|
|
886
|
-
features = [];
|
|
887
|
-
let baseWidth = 320;
|
|
888
|
-
for (const [name, query] of Object.entries(mediaDef)) {
|
|
889
|
-
if (name === "--") {
|
|
890
|
-
const wm = String(query).match(/^(\d+)\s*px$/);
|
|
891
|
-
baseWidth = wm ? parseFloat(wm[1]) : 320;
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
const minMatch = query.match(/min-width:\s*([\d.]+)px/);
|
|
895
|
-
const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
|
|
896
|
-
if (minMatch) sizes.push({ name, query, width: parseFloat(minMatch[1]), type: "min" });
|
|
897
|
-
else if (maxMatch) sizes.push({ name, query, width: parseFloat(maxMatch[1]), type: "max" });
|
|
898
|
-
else features.push({ name, query });
|
|
899
|
-
}
|
|
900
|
-
sizes.sort((a, b) => (a.type === "min" ? a.width - b.width : b.width - a.width));
|
|
901
|
-
return { sizeBreakpoints: sizes, featureQueries: features, baseWidth };
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Compute which named breakpoints are active at a given canvas width. For min-width canvases: all
|
|
906
|
-
* breakpoints with min-width <= canvasWidth are active. For max-width canvases: all breakpoints
|
|
907
|
-
* with max-width >= canvasWidth are active.
|
|
908
|
-
*
|
|
909
|
-
* @param {any} sizeBreakpoints
|
|
910
|
-
* @param {any} canvasWidth
|
|
911
|
-
*/
|
|
912
|
-
function activeBreakpointsForWidth(sizeBreakpoints, canvasWidth) {
|
|
913
|
-
const active = new Set();
|
|
914
|
-
for (const bp of sizeBreakpoints) {
|
|
915
|
-
if (bp.type === "min" && canvasWidth >= bp.width) active.add(bp.name);
|
|
916
|
-
else if (bp.type === "max" && canvasWidth <= bp.width) active.add(bp.name);
|
|
917
|
-
}
|
|
918
|
-
return active;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Apply styles to a canvas element, including active media overrides. Base (flat) styles applied
|
|
923
|
-
* first, then matching media overrides in source order.
|
|
1051
|
+
* After a runtime render, apply active media overrides as inline styles so they beat the base
|
|
1052
|
+
* inline styles the runtime already set. The runtime uses @media CSS rules for overrides, but those
|
|
1053
|
+
* can never beat inline base styles.
|
|
924
1054
|
*
|
|
925
|
-
* @param {
|
|
926
|
-
* @param {
|
|
927
|
-
* @param {any} activeBreakpoints
|
|
928
|
-
* @param {any} featureToggles
|
|
1055
|
+
* @param {Element} canvasEl
|
|
1056
|
+
* @param {Set<string>} activeBreakpoints
|
|
929
1057
|
*/
|
|
930
|
-
function
|
|
931
|
-
if (!
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
else /** @type {any} */ (el.style)[prop] = val;
|
|
937
|
-
} catch {}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
for (const [key, val] of Object.entries(styleDef)) {
|
|
941
|
-
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
942
|
-
const mediaName = key.slice(1);
|
|
943
|
-
if (mediaName === "--") continue; // skip base canvas width key
|
|
944
|
-
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
945
|
-
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
946
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
947
|
-
try {
|
|
948
|
-
if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
|
|
949
|
-
else /** @type {any} */ (el.style)[prop] = v;
|
|
950
|
-
} catch {}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
1058
|
+
function applyCanvasMediaOverrides(canvasEl, activeBreakpoints) {
|
|
1059
|
+
if (!activeBreakpoints.size) return;
|
|
1060
|
+
const docMedia = getEffectiveMedia(S.document.$media || {});
|
|
1061
|
+
const validBreakpoints = new Set();
|
|
1062
|
+
for (const name of activeBreakpoints) {
|
|
1063
|
+
if (docMedia[name]) validBreakpoints.add(name);
|
|
954
1064
|
}
|
|
1065
|
+
const overrides = collectMediaOverrides(document.styleSheets, validBreakpoints);
|
|
1066
|
+
applyOverridesToCanvas(canvasEl, overrides);
|
|
955
1067
|
}
|
|
956
1068
|
|
|
957
1069
|
// ─── Canvas ───────────────────────────────────────────────────────────────────
|
|
958
1070
|
|
|
959
1071
|
function renderCanvas() {
|
|
1072
|
+
// Advance render generation so stale async renders from the previous cycle bail out
|
|
1073
|
+
++view.renderGeneration;
|
|
1074
|
+
|
|
1075
|
+
// Always clear Lit's internal state so it builds fresh DOM. Stale async
|
|
1076
|
+
// renderCanvasLive calls from a previous cycle can corrupt nested ChildPart
|
|
1077
|
+
// markers (Comment nodes inside panzoom-wrap) in ways the root-only
|
|
1078
|
+
// ensureLitState check cannot detect.
|
|
1079
|
+
// @ts-ignore
|
|
1080
|
+
if (canvasWrap["_$litPart$"]) {
|
|
1081
|
+
canvasWrap.textContent = "";
|
|
1082
|
+
// @ts-ignore
|
|
1083
|
+
delete canvasWrap["_$litPart$"];
|
|
1084
|
+
}
|
|
1085
|
+
|
|
960
1086
|
// Function editor mode: editing a function body in Monaco (JS)
|
|
961
1087
|
if (S.ui.editingFunction) {
|
|
962
1088
|
renderFunctionEditor();
|
|
@@ -964,68 +1090,125 @@ function renderCanvas() {
|
|
|
964
1090
|
}
|
|
965
1091
|
|
|
966
1092
|
// Dispose function editor if switching away
|
|
967
|
-
if (functionEditor) {
|
|
968
|
-
functionEditor.dispose();
|
|
969
|
-
functionEditor = null;
|
|
1093
|
+
if (view.functionEditor) {
|
|
1094
|
+
view.functionEditor.dispose();
|
|
1095
|
+
view.functionEditor = null;
|
|
970
1096
|
}
|
|
971
1097
|
|
|
972
1098
|
// Source mode: update existing Monaco editor without recreating
|
|
973
|
-
if (canvasMode === "source" && monacoEditor) {
|
|
1099
|
+
if (canvasMode === "source" && view.monacoEditor) {
|
|
974
1100
|
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
975
|
-
const currentVal = monacoEditor.getValue();
|
|
1101
|
+
const currentVal = view.monacoEditor.getValue();
|
|
976
1102
|
if (currentVal !== jsonStr) {
|
|
977
1103
|
// Prevent triggering the onChange handler for this programmatic update
|
|
978
|
-
monacoEditor._ignoreNextChange = true;
|
|
979
|
-
monacoEditor.setValue(jsonStr);
|
|
1104
|
+
view.monacoEditor._ignoreNextChange = true;
|
|
1105
|
+
view.monacoEditor.setValue(jsonStr);
|
|
980
1106
|
}
|
|
981
1107
|
return;
|
|
982
1108
|
}
|
|
983
1109
|
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
canvasDndCleanups
|
|
1110
|
+
// Detect whether this is a mode transition or a content-only re-render
|
|
1111
|
+
const modeChanged = canvasMode !== view.prevCanvasMode;
|
|
1112
|
+
view.prevCanvasMode = canvasMode;
|
|
1113
|
+
|
|
1114
|
+
// DnD handlers are registered on inner canvas elements that get replaced on every
|
|
1115
|
+
// content render, so always clean them up.
|
|
1116
|
+
for (const fn of view.canvasDndCleanups) fn();
|
|
1117
|
+
view.canvasDndCleanups = [];
|
|
1118
|
+
|
|
1119
|
+
// Panel event handlers (click, dblclick, etc.) capture closures over panel references.
|
|
1120
|
+
// Always re-register to keep closures fresh across document switches.
|
|
1121
|
+
for (const fn of view.canvasEventCleanups) fn();
|
|
1122
|
+
view.canvasEventCleanups = [];
|
|
1123
|
+
|
|
1124
|
+
// Panel JS objects are cheap — always clear and repopulate from templates.
|
|
1125
|
+
// The actual DOM elements are preserved by Lit's diffing on content-only re-renders.
|
|
991
1126
|
canvasPanels.length = 0;
|
|
992
1127
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1128
|
+
if (modeChanged) {
|
|
1129
|
+
// Full teardown on mode transitions — new panel structure needed
|
|
1130
|
+
if (view.centerObserver) {
|
|
1131
|
+
view.centerObserver.disconnect();
|
|
1132
|
+
view.centerObserver = null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Dispose Monaco editor if switching away from source mode
|
|
1136
|
+
if (view.monacoEditor) {
|
|
1137
|
+
view.monacoEditor.dispose();
|
|
1138
|
+
view.monacoEditor = null;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
litRender(nothing, canvasWrap);
|
|
1142
|
+
view.panzoomWrap = null;
|
|
1143
|
+
// Reset inline style overrides from other modes
|
|
1144
|
+
canvasWrap.style.padding = "";
|
|
1145
|
+
canvasWrap.style.alignItems = "";
|
|
1146
|
+
canvasWrap.style.display = "";
|
|
1147
|
+
canvasWrap.style.overflow = "";
|
|
1148
|
+
canvasWrap.style.overflow = "";
|
|
1149
|
+
|
|
1150
|
+
// Clear zoom indicator (only re-rendered by design/preview/stylebook)
|
|
1151
|
+
try {
|
|
1152
|
+
litRender(nothing, zoomIndicatorHost);
|
|
1153
|
+
} catch {
|
|
1154
|
+
const newHost = document.createElement("div");
|
|
1155
|
+
newHost.style.display = "contents";
|
|
1156
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1157
|
+
zoomIndicatorHost = newHost;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Dismiss open popovers/toolbars that are no longer relevant
|
|
1161
|
+
if (view.blockActionBarEl) litRender(nothing, view.blockActionBarEl);
|
|
1162
|
+
dismissLinkPopover();
|
|
1163
|
+
dismissContextMenu();
|
|
1164
|
+
sharedDismissSlashMenu();
|
|
997
1165
|
}
|
|
998
1166
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1167
|
+
// Manage mode: project-level file browser table
|
|
1168
|
+
if (canvasMode === "manage") {
|
|
1169
|
+
canvasWrap.style.padding = "0";
|
|
1170
|
+
canvasWrap.style.overflow = "auto";
|
|
1171
|
+
renderBrowse(canvasWrap, {
|
|
1172
|
+
openFile: (/** @type {string} */ path) => {
|
|
1173
|
+
canvasMode = "edit";
|
|
1174
|
+
openFileFromTree(path);
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Settings mode: render element catalog with panzoom surface
|
|
1181
|
+
if (canvasMode === "settings") {
|
|
1182
|
+
renderSettings();
|
|
1009
1183
|
return;
|
|
1010
1184
|
}
|
|
1011
1185
|
|
|
1012
1186
|
// Source mode: create Monaco editor instead of canvas
|
|
1013
1187
|
if (canvasMode === "source") {
|
|
1014
1188
|
canvasWrap.style.padding = "0";
|
|
1189
|
+
canvasWrap.style.display = "block";
|
|
1015
1190
|
/** @type {HTMLDivElement | null} */
|
|
1016
1191
|
let editorContainer = null;
|
|
1017
1192
|
litRender(
|
|
1018
|
-
html`<div
|
|
1019
|
-
class="source-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1193
|
+
html`<div class="source-wrap">
|
|
1194
|
+
<div class="source-toolbar">
|
|
1195
|
+
<sp-action-button size="s" @click=${exportFile}>
|
|
1196
|
+
<sp-icon-export slot="icon"></sp-icon-export>
|
|
1197
|
+
Export
|
|
1198
|
+
</sp-action-button>
|
|
1199
|
+
</div>
|
|
1200
|
+
<div
|
|
1201
|
+
class="source-editor"
|
|
1202
|
+
${ref((el) => {
|
|
1203
|
+
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
1204
|
+
})}
|
|
1205
|
+
></div>
|
|
1206
|
+
</div>`,
|
|
1024
1207
|
canvasWrap,
|
|
1025
1208
|
);
|
|
1026
1209
|
|
|
1027
1210
|
const jsonStr = JSON.stringify(S.document, null, 2);
|
|
1028
|
-
monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1211
|
+
view.monacoEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
1029
1212
|
value: jsonStr,
|
|
1030
1213
|
language: "json",
|
|
1031
1214
|
theme: "vs-dark",
|
|
@@ -1042,19 +1225,16 @@ function renderCanvas() {
|
|
|
1042
1225
|
// Debounced sync back to state
|
|
1043
1226
|
/** @type {any} */
|
|
1044
1227
|
let debounce;
|
|
1045
|
-
monacoEditor.onDidChangeModelContent(() => {
|
|
1046
|
-
if (monacoEditor._ignoreNextChange) {
|
|
1047
|
-
monacoEditor._ignoreNextChange = false;
|
|
1228
|
+
view.monacoEditor.onDidChangeModelContent(() => {
|
|
1229
|
+
if (view.monacoEditor._ignoreNextChange) {
|
|
1230
|
+
view.monacoEditor._ignoreNextChange = false;
|
|
1048
1231
|
return;
|
|
1049
1232
|
}
|
|
1050
1233
|
clearTimeout(debounce);
|
|
1051
1234
|
debounce = setTimeout(() => {
|
|
1052
1235
|
try {
|
|
1053
|
-
const parsed = JSON.parse(monacoEditor.getValue());
|
|
1054
|
-
|
|
1055
|
-
renderToolbar();
|
|
1056
|
-
renderLeftPanel();
|
|
1057
|
-
renderRightPanel();
|
|
1236
|
+
const parsed = JSON.parse(view.monacoEditor.getValue());
|
|
1237
|
+
update({ ...S, document: parsed, dirty: true });
|
|
1058
1238
|
} catch {
|
|
1059
1239
|
// Invalid JSON — don't update state
|
|
1060
1240
|
}
|
|
@@ -1065,33 +1245,38 @@ function renderCanvas() {
|
|
|
1065
1245
|
|
|
1066
1246
|
// Edit (content) mode — centered column, no panzoom, always 100%
|
|
1067
1247
|
if (canvasMode === "edit") {
|
|
1068
|
-
|
|
1069
|
-
|
|
1248
|
+
if (modeChanged) {
|
|
1249
|
+
canvasWrap.style.padding = "0";
|
|
1250
|
+
canvasWrap.style.overflow = "hidden";
|
|
1070
1251
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1252
|
+
// Remove zoom indicator left over from design/preview mode
|
|
1253
|
+
try {
|
|
1254
|
+
litRender(nothing, zoomIndicatorHost);
|
|
1255
|
+
} catch {
|
|
1256
|
+
const newHost = document.createElement("div");
|
|
1257
|
+
newHost.style.display = "contents";
|
|
1258
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1259
|
+
zoomIndicatorHost = newHost;
|
|
1260
|
+
}
|
|
1076
1261
|
}
|
|
1077
1262
|
|
|
1078
1263
|
const { tpl: panelTpl, panel } = canvasPanelTemplate(null, null, true);
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
<div class="content-edit-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
canvasWrap,
|
|
1086
|
-
);
|
|
1264
|
+
const editTpl = html`
|
|
1265
|
+
<div class="content-edit-canvas">
|
|
1266
|
+
<div class="content-edit-column">${panelTpl}</div>
|
|
1267
|
+
</div>
|
|
1268
|
+
`;
|
|
1269
|
+
litRender(editTpl, canvasWrap);
|
|
1087
1270
|
canvasPanels.push(panel);
|
|
1088
1271
|
renderCanvasIntoPanel(panel, new Set(), S.ui.featureToggles);
|
|
1089
1272
|
return;
|
|
1090
1273
|
}
|
|
1091
1274
|
|
|
1092
1275
|
// Normal canvas mode (design / preview) — set up panzoom surface
|
|
1093
|
-
|
|
1094
|
-
|
|
1276
|
+
if (modeChanged) {
|
|
1277
|
+
canvasWrap.style.padding = "0";
|
|
1278
|
+
canvasWrap.style.overflow = "hidden";
|
|
1279
|
+
}
|
|
1095
1280
|
|
|
1096
1281
|
const {
|
|
1097
1282
|
sizeBreakpoints,
|
|
@@ -1119,7 +1304,7 @@ function renderCanvas() {
|
|
|
1119
1304
|
class="panzoom-wrap"
|
|
1120
1305
|
style="transform-origin:0 0"
|
|
1121
1306
|
${ref((el) => {
|
|
1122
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1307
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1123
1308
|
})}
|
|
1124
1309
|
>
|
|
1125
1310
|
${panelTpl}
|
|
@@ -1130,12 +1315,15 @@ function renderCanvas() {
|
|
|
1130
1315
|
canvasPanels.push(panel);
|
|
1131
1316
|
renderCanvasIntoPanel(panel, new Set(), featureToggles);
|
|
1132
1317
|
applyTransform();
|
|
1133
|
-
|
|
1318
|
+
if (modeChanged) {
|
|
1319
|
+
observeCenterUntilStable();
|
|
1320
|
+
}
|
|
1134
1321
|
renderZoomIndicator();
|
|
1135
1322
|
return;
|
|
1136
1323
|
}
|
|
1137
1324
|
|
|
1138
|
-
// Build all panels
|
|
1325
|
+
// Build all panels: base first, then breakpoints in declared order (ascending for min-width,
|
|
1326
|
+
// descending for max-width — matching the direction of the design's media queries).
|
|
1139
1327
|
const allPanelDefs = [
|
|
1140
1328
|
{
|
|
1141
1329
|
name: "base",
|
|
@@ -1152,7 +1340,6 @@ function renderCanvas() {
|
|
|
1152
1340
|
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
1153
1341
|
});
|
|
1154
1342
|
}
|
|
1155
|
-
allPanelDefs.sort((a, b) => b.width - a.width);
|
|
1156
1343
|
|
|
1157
1344
|
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
1158
1345
|
const panelEntries = allPanelDefs.map((def) => {
|
|
@@ -1167,7 +1354,7 @@ function renderCanvas() {
|
|
|
1167
1354
|
class="panzoom-wrap"
|
|
1168
1355
|
style="transform-origin:0 0"
|
|
1169
1356
|
${ref((el) => {
|
|
1170
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1357
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
1171
1358
|
})}
|
|
1172
1359
|
>
|
|
1173
1360
|
${panelEntries.map((e) => e.tpl)}
|
|
@@ -1186,7 +1373,9 @@ function renderCanvas() {
|
|
|
1186
1373
|
|
|
1187
1374
|
// Apply current zoom + pan transform
|
|
1188
1375
|
applyTransform();
|
|
1189
|
-
|
|
1376
|
+
if (modeChanged) {
|
|
1377
|
+
observeCenterUntilStable();
|
|
1378
|
+
}
|
|
1190
1379
|
|
|
1191
1380
|
// Floating zoom indicator
|
|
1192
1381
|
renderZoomIndicator();
|
|
@@ -1201,9 +1390,13 @@ function renderCanvas() {
|
|
|
1201
1390
|
* @param {any} featureToggles
|
|
1202
1391
|
*/
|
|
1203
1392
|
function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
1204
|
-
|
|
1393
|
+
const gen = view.renderGeneration;
|
|
1394
|
+
renderCanvasLive(gen, S.document, panel.canvas).then((scope) => {
|
|
1395
|
+
// Skip post-render setup if a newer render has started
|
|
1396
|
+
if (gen !== view.renderGeneration) return;
|
|
1205
1397
|
if (scope) {
|
|
1206
|
-
liveScope = scope;
|
|
1398
|
+
view.liveScope = scope;
|
|
1399
|
+
applyCanvasMediaOverrides(panel.canvas, activeBreakpoints);
|
|
1207
1400
|
statusMessage("Runtime render OK", 1500);
|
|
1208
1401
|
} else {
|
|
1209
1402
|
// Fallback to structural preview
|
|
@@ -1214,9 +1407,9 @@ function renderCanvasIntoPanel(panel, activeBreakpoints, featureToggles) {
|
|
|
1214
1407
|
renderOverlays();
|
|
1215
1408
|
|
|
1216
1409
|
// Process pending inline edit now that the canvas is populated
|
|
1217
|
-
if (pendingInlineEdit) {
|
|
1218
|
-
const { path, mediaName: mn } = pendingInlineEdit;
|
|
1219
|
-
pendingInlineEdit = null;
|
|
1410
|
+
if (view.pendingInlineEdit) {
|
|
1411
|
+
const { path, mediaName: mn } = view.pendingInlineEdit;
|
|
1412
|
+
view.pendingInlineEdit = null;
|
|
1220
1413
|
const targetPanel = canvasPanels.find((p) => p.mediaName === mn) || canvasPanels[0];
|
|
1221
1414
|
if (targetPanel) {
|
|
1222
1415
|
const el = findCanvasElement(path, targetPanel.canvas);
|
|
@@ -1271,9 +1464,7 @@ function canvasPanelTemplate(mediaName, label, fullWidth, width) {
|
|
|
1271
1464
|
<div
|
|
1272
1465
|
class="canvas-panel-header"
|
|
1273
1466
|
@click=${() => {
|
|
1274
|
-
|
|
1275
|
-
updateActivePanelHeaders();
|
|
1276
|
-
renderRightPanel();
|
|
1467
|
+
updateUi("activeMedia", mediaName === "base" ? null : mediaName);
|
|
1277
1468
|
}}
|
|
1278
1469
|
>
|
|
1279
1470
|
${label}
|
|
@@ -1322,52 +1513,52 @@ function canvasPanelTemplate(mediaName, label, fullWidth, width) {
|
|
|
1322
1513
|
|
|
1323
1514
|
/** Center canvas in viewport. */
|
|
1324
1515
|
function centerCanvas() {
|
|
1325
|
-
if (!panzoomWrap) return;
|
|
1516
|
+
if (!view.panzoomWrap) return;
|
|
1326
1517
|
const wrapWidth = canvasWrap.clientWidth;
|
|
1327
1518
|
const wrapHeight = canvasWrap.clientHeight;
|
|
1328
|
-
const contentWidth = panzoomWrap.scrollWidth;
|
|
1329
|
-
const contentHeight = panzoomWrap.scrollHeight;
|
|
1519
|
+
const contentWidth = view.panzoomWrap.scrollWidth;
|
|
1520
|
+
const contentHeight = view.panzoomWrap.scrollHeight;
|
|
1330
1521
|
const scaledWidth = contentWidth * S.ui.zoom;
|
|
1331
1522
|
const scaledHeight = contentHeight * S.ui.zoom;
|
|
1332
|
-
panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
1523
|
+
view.panX = Math.max(16, (wrapWidth - scaledWidth) / 2);
|
|
1333
1524
|
// Center vertically only when content fits; top-align with margin when taller
|
|
1334
1525
|
const verticalCenter = (wrapHeight - scaledHeight) / 2;
|
|
1335
|
-
panY = verticalCenter > 16 ? verticalCenter : 16;
|
|
1526
|
+
view.panY = verticalCenter > 16 ? verticalCenter : 16;
|
|
1336
1527
|
}
|
|
1337
1528
|
|
|
1338
1529
|
/**
|
|
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.
|
|
1530
|
+
* Attach a ResizeObserver to view.panzoomWrap that re-centers until the user pans. Handles async
|
|
1531
|
+
* content (runtime rendering, data fetching) that changes layout after initial paint.
|
|
1341
1532
|
*/
|
|
1342
1533
|
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;
|
|
1534
|
+
if (view.centerObserver) {
|
|
1535
|
+
view.centerObserver.disconnect();
|
|
1536
|
+
view.centerObserver = null;
|
|
1537
|
+
}
|
|
1538
|
+
if (!view.panzoomWrap) return;
|
|
1539
|
+
view.needsCenter = true;
|
|
1540
|
+
view.centerObserver = new ResizeObserver(() => {
|
|
1541
|
+
if (!view.needsCenter) {
|
|
1542
|
+
view.centerObserver?.disconnect();
|
|
1543
|
+
view.centerObserver = null;
|
|
1353
1544
|
return;
|
|
1354
1545
|
}
|
|
1355
1546
|
centerCanvas();
|
|
1356
1547
|
applyTransform();
|
|
1357
1548
|
});
|
|
1358
|
-
centerObserver.observe(panzoomWrap);
|
|
1549
|
+
view.centerObserver.observe(view.panzoomWrap);
|
|
1359
1550
|
// Also center immediately for synchronous content
|
|
1360
1551
|
centerCanvas();
|
|
1361
1552
|
}
|
|
1362
1553
|
|
|
1363
1554
|
/** Apply the current zoom + pan transform to the panzoom wrapper. */
|
|
1364
1555
|
function applyTransform() {
|
|
1365
|
-
if (!panzoomWrap) return;
|
|
1366
|
-
panzoomWrap.style.transform = `translate(${panX}px, ${panY}px) scale(${S.ui.zoom})`;
|
|
1556
|
+
if (!view.panzoomWrap) return;
|
|
1557
|
+
view.panzoomWrap.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${S.ui.zoom})`;
|
|
1367
1558
|
const label = document.querySelector(".zoom-indicator-label");
|
|
1368
1559
|
if (label) label.textContent = `${Math.round(S.ui.zoom * 100)}%`;
|
|
1369
1560
|
renderOverlays();
|
|
1370
|
-
if (canvasMode === "
|
|
1561
|
+
if (canvasMode === "settings") renderStylebookOverlays();
|
|
1371
1562
|
}
|
|
1372
1563
|
|
|
1373
1564
|
/** Lightweight in-place zoom update — no full re-render. */
|
|
@@ -1377,7 +1568,7 @@ function _applyZoom() {
|
|
|
1377
1568
|
|
|
1378
1569
|
/** Calculate zoom + pan to fit all panels within the viewport. */
|
|
1379
1570
|
function fitToScreen() {
|
|
1380
|
-
if (!panzoomWrap) return;
|
|
1571
|
+
if (!view.panzoomWrap) return;
|
|
1381
1572
|
const wrapWidth = canvasWrap.clientWidth;
|
|
1382
1573
|
const wrapHeight = canvasWrap.clientHeight;
|
|
1383
1574
|
const gap = 24;
|
|
@@ -1390,7 +1581,7 @@ function fitToScreen() {
|
|
|
1390
1581
|
totalPanelWidth += gap * Math.max(0, canvasPanels.length - 1) + padding;
|
|
1391
1582
|
|
|
1392
1583
|
// Get actual content height from rendered panels
|
|
1393
|
-
const wrapRect = panzoomWrap.getBoundingClientRect();
|
|
1584
|
+
const wrapRect = view.panzoomWrap.getBoundingClientRect();
|
|
1394
1585
|
const unscaledHeight = wrapRect.height / S.ui.zoom;
|
|
1395
1586
|
maxPanelHeight = unscaledHeight + padding;
|
|
1396
1587
|
|
|
@@ -1398,12 +1589,13 @@ function fitToScreen() {
|
|
|
1398
1589
|
const fitZoomH = wrapHeight / maxPanelHeight;
|
|
1399
1590
|
const fitZoom = Math.min(5.0, Math.max(0.05, Math.min(fitZoomW, fitZoomH)));
|
|
1400
1591
|
|
|
1401
|
-
|
|
1592
|
+
session = { ...session, ui: { ...session.ui, zoom: fitZoom } };
|
|
1593
|
+
S = toFlat(doc, session);
|
|
1402
1594
|
// Center the content
|
|
1403
1595
|
const scaledWidth = totalPanelWidth * fitZoom;
|
|
1404
1596
|
const scaledHeight = maxPanelHeight * fitZoom;
|
|
1405
|
-
panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
1406
|
-
panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
1597
|
+
view.panX = Math.max(0, (wrapWidth - scaledWidth) / 2);
|
|
1598
|
+
view.panY = Math.max(0, (wrapHeight - scaledHeight) / 2);
|
|
1407
1599
|
applyTransform();
|
|
1408
1600
|
}
|
|
1409
1601
|
|
|
@@ -1443,7 +1635,11 @@ function renderZoomIndicator() {
|
|
|
1443
1635
|
zoomIndicatorHost,
|
|
1444
1636
|
);
|
|
1445
1637
|
} catch {
|
|
1446
|
-
|
|
1638
|
+
// Lit markers were corrupted — replace the host element to fully reset Lit state
|
|
1639
|
+
const newHost = document.createElement("div");
|
|
1640
|
+
newHost.style.display = "contents";
|
|
1641
|
+
zoomIndicatorHost.replaceWith(newHost);
|
|
1642
|
+
zoomIndicatorHost = newHost;
|
|
1447
1643
|
litRender(
|
|
1448
1644
|
html`
|
|
1449
1645
|
<div class="zoom-indicator">
|
|
@@ -1600,7 +1796,6 @@ function renderCanvasNode(node, path, parent, activeBreakpoints, featureToggles)
|
|
|
1600
1796
|
*
|
|
1601
1797
|
* @type {any}
|
|
1602
1798
|
*/
|
|
1603
|
-
let lastDragInput = null;
|
|
1604
1799
|
|
|
1605
1800
|
/**
|
|
1606
1801
|
* Register all canvas elements in a panel as DnD drop targets.
|
|
@@ -1620,19 +1815,19 @@ function registerPanelDnD(panel) {
|
|
|
1620
1815
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "none";
|
|
1621
1816
|
},
|
|
1622
1817
|
onDrag({ location }) {
|
|
1623
|
-
lastDragInput = location.current.input;
|
|
1818
|
+
view.lastDragInput = location.current.input;
|
|
1624
1819
|
},
|
|
1625
1820
|
onDrop() {
|
|
1626
1821
|
// Hide all drop lines
|
|
1627
1822
|
for (const p of canvasPanels) p.dropLine.style.display = "none";
|
|
1628
|
-
lastDragInput = null;
|
|
1823
|
+
view.lastDragInput = null;
|
|
1629
1824
|
for (const el of canvas.querySelectorAll("*")) {
|
|
1630
1825
|
/** @type {any} */ (el).style.pointerEvents = "none";
|
|
1631
1826
|
}
|
|
1632
1827
|
for (const p of canvasPanels) p.overlayClk.style.pointerEvents = "";
|
|
1633
1828
|
},
|
|
1634
1829
|
});
|
|
1635
|
-
canvasDndCleanups.push(monitorCleanup);
|
|
1830
|
+
view.canvasDndCleanups.push(monitorCleanup);
|
|
1636
1831
|
|
|
1637
1832
|
for (const el of allEls) {
|
|
1638
1833
|
const elPath = elToPath.get(el);
|
|
@@ -1669,7 +1864,7 @@ function registerPanelDnD(panel) {
|
|
|
1669
1864
|
applyDropInstruction(instruction, source.data, elPath);
|
|
1670
1865
|
},
|
|
1671
1866
|
});
|
|
1672
|
-
canvasDndCleanups.push(cleanup);
|
|
1867
|
+
view.canvasDndCleanups.push(cleanup);
|
|
1673
1868
|
}
|
|
1674
1869
|
}
|
|
1675
1870
|
|
|
@@ -1680,8 +1875,8 @@ function registerPanelDnD(panel) {
|
|
|
1680
1875
|
*/
|
|
1681
1876
|
function getCanvasDropInstruction(el, elPath, isVoid) {
|
|
1682
1877
|
const rect = el.getBoundingClientRect();
|
|
1683
|
-
if (!lastDragInput) return null;
|
|
1684
|
-
const y = lastDragInput.clientY;
|
|
1878
|
+
if (!view.lastDragInput) return null;
|
|
1879
|
+
const y = view.lastDragInput.clientY;
|
|
1685
1880
|
const relY = (y - rect.top) / rect.height;
|
|
1686
1881
|
|
|
1687
1882
|
if (elPath.length === 0) return { type: "make-child" };
|
|
@@ -1739,83 +1934,7 @@ function showCanvasDropIndicator(el, elPath, isVoid, panel) {
|
|
|
1739
1934
|
// ─── Overlay system ───────────────────────────────────────────────────────────
|
|
1740
1935
|
|
|
1741
1936
|
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();
|
|
1937
|
+
overlaysPanel.render();
|
|
1819
1938
|
}
|
|
1820
1939
|
|
|
1821
1940
|
/**
|
|
@@ -1877,15 +1996,10 @@ function onBarMousedown(e) {
|
|
|
1877
1996
|
e.preventDefault();
|
|
1878
1997
|
}
|
|
1879
1998
|
|
|
1880
|
-
/**
|
|
1881
|
-
* Saved selection range for format button mousedown→click flow
|
|
1882
|
-
*
|
|
1883
|
-
* @type {any}
|
|
1884
|
-
*/
|
|
1885
|
-
let savedRange = null;
|
|
1999
|
+
/** Saved selection range for format button mousedown→click flow */
|
|
1886
2000
|
function captureSelectionRange() {
|
|
1887
2001
|
const sel = window.getSelection();
|
|
1888
|
-
if (sel && sel.rangeCount) savedRange = sel.getRangeAt(0).cloneRange();
|
|
2002
|
+
if (sel && sel.rangeCount) view.savedRange = sel.getRangeAt(0).cloneRange();
|
|
1889
2003
|
}
|
|
1890
2004
|
|
|
1891
2005
|
/**
|
|
@@ -1896,16 +2010,16 @@ function onFormatClick(e, action) {
|
|
|
1896
2010
|
e.stopPropagation();
|
|
1897
2011
|
if (action.command === "link") {
|
|
1898
2012
|
showLinkPopover(e.target.closest("sp-action-button"));
|
|
1899
|
-
} else if (savedRange) {
|
|
2013
|
+
} else if (view.savedRange) {
|
|
1900
2014
|
const sel = /** @type {any} */ (window.getSelection());
|
|
1901
|
-
const anchor = savedRange.startContainer;
|
|
2015
|
+
const anchor = view.savedRange.startContainer;
|
|
1902
2016
|
const editableRoot = (
|
|
1903
2017
|
anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement
|
|
1904
2018
|
)?.closest("[contenteditable]");
|
|
1905
2019
|
if (editableRoot) {
|
|
1906
2020
|
editableRoot.focus();
|
|
1907
2021
|
sel.removeAllRanges();
|
|
1908
|
-
sel.addRange(savedRange);
|
|
2022
|
+
sel.addRange(view.savedRange);
|
|
1909
2023
|
applyInlineFormat(action);
|
|
1910
2024
|
}
|
|
1911
2025
|
}
|
|
@@ -1990,14 +2104,19 @@ function applyInlineFormat(action) {
|
|
|
1990
2104
|
}
|
|
1991
2105
|
|
|
1992
2106
|
/** 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);
|
|
2107
|
+
view.linkPopoverHost = document.createElement("div");
|
|
2108
|
+
view.linkPopoverHost.style.display = "contents";
|
|
2109
|
+
(document.querySelector("sp-theme") || document.body).appendChild(view.linkPopoverHost);
|
|
2110
|
+
|
|
2111
|
+
/** Dismiss the link popover if open. */
|
|
2112
|
+
function dismissLinkPopover() {
|
|
2113
|
+
if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
|
|
2114
|
+
}
|
|
1996
2115
|
|
|
1997
2116
|
/** @param {any} anchorBtn */
|
|
1998
2117
|
function showLinkPopover(anchorBtn) {
|
|
1999
2118
|
// Dismiss existing
|
|
2000
|
-
litRender(nothing, linkPopoverHost);
|
|
2119
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2001
2120
|
|
|
2002
2121
|
const sel = window.getSelection();
|
|
2003
2122
|
/** @type {any} */
|
|
@@ -2017,14 +2136,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2017
2136
|
const rect = anchorBtn.getBoundingClientRect();
|
|
2018
2137
|
|
|
2019
2138
|
const onApply = () => {
|
|
2020
|
-
const field = linkPopoverHost.querySelector("sp-textfield");
|
|
2139
|
+
const field = view.linkPopoverHost.querySelector("sp-textfield");
|
|
2021
2140
|
const url = /** @type {any} */ (field)?.value;
|
|
2022
2141
|
if (existingLink) {
|
|
2023
2142
|
existingLink.setAttribute("href", url);
|
|
2024
2143
|
} else if (url) {
|
|
2025
2144
|
document.execCommand("createLink", false, url);
|
|
2026
2145
|
}
|
|
2027
|
-
litRender(nothing, linkPopoverHost);
|
|
2146
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2028
2147
|
renderBlockActionBar();
|
|
2029
2148
|
};
|
|
2030
2149
|
|
|
@@ -2032,14 +2151,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2032
2151
|
const frag = document.createDocumentFragment();
|
|
2033
2152
|
while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
|
|
2034
2153
|
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
2035
|
-
litRender(nothing, linkPopoverHost);
|
|
2154
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2036
2155
|
renderBlockActionBar();
|
|
2037
2156
|
};
|
|
2038
2157
|
|
|
2039
2158
|
const onKeydown = (/** @type {any} */ e) => {
|
|
2040
2159
|
if (e.key === "Enter") onApply();
|
|
2041
2160
|
else if (e.key === "Escape") {
|
|
2042
|
-
litRender(nothing, linkPopoverHost);
|
|
2161
|
+
litRender(nothing, view.linkPopoverHost);
|
|
2043
2162
|
}
|
|
2044
2163
|
};
|
|
2045
2164
|
|
|
@@ -2065,12 +2184,14 @@ function showLinkPopover(anchorBtn) {
|
|
|
2065
2184
|
: nothing}
|
|
2066
2185
|
</sp-popover>
|
|
2067
2186
|
`,
|
|
2068
|
-
linkPopoverHost,
|
|
2187
|
+
view.linkPopoverHost,
|
|
2069
2188
|
);
|
|
2070
2189
|
|
|
2071
2190
|
requestAnimationFrame(
|
|
2072
2191
|
() =>
|
|
2073
|
-
/** @type {HTMLElement | null} */ (
|
|
2192
|
+
/** @type {HTMLElement | null} */ (
|
|
2193
|
+
view.linkPopoverHost?.querySelector("sp-textfield")
|
|
2194
|
+
)?.focus(),
|
|
2074
2195
|
);
|
|
2075
2196
|
}
|
|
2076
2197
|
|
|
@@ -2081,7 +2202,8 @@ function moveSelectionUp() {
|
|
|
2081
2202
|
if (idx <= 0) return;
|
|
2082
2203
|
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
2083
2204
|
update(moveNode(S, S.selection, pPath, idx - 1));
|
|
2084
|
-
|
|
2205
|
+
session = { ...session, selection: [...pPath, "children", idx - 1] };
|
|
2206
|
+
S = toFlat(doc, session);
|
|
2085
2207
|
renderOverlays();
|
|
2086
2208
|
}
|
|
2087
2209
|
|
|
@@ -2094,7 +2216,8 @@ function moveSelectionDown() {
|
|
|
2094
2216
|
const siblings = parentNode?.children;
|
|
2095
2217
|
if (!siblings || idx >= siblings.length - 1) return;
|
|
2096
2218
|
update(moveNode(S, S.selection, pPath, idx + 2));
|
|
2097
|
-
|
|
2219
|
+
session = { ...session, selection: [...pPath, "children", idx + 1] };
|
|
2220
|
+
S = toFlat(doc, session);
|
|
2098
2221
|
renderOverlays();
|
|
2099
2222
|
}
|
|
2100
2223
|
|
|
@@ -2104,30 +2227,30 @@ function moveSelectionDown() {
|
|
|
2104
2227
|
*/
|
|
2105
2228
|
function renderBlockActionBar() {
|
|
2106
2229
|
// Ensure persistent render container exists
|
|
2107
|
-
if (!blockActionBarEl) {
|
|
2108
|
-
blockActionBarEl = createFloatingContainer();
|
|
2230
|
+
if (!view.blockActionBarEl) {
|
|
2231
|
+
view.blockActionBarEl = createFloatingContainer();
|
|
2109
2232
|
}
|
|
2110
2233
|
|
|
2111
2234
|
// Tear down drag if it was active
|
|
2112
|
-
if (selDragCleanup) {
|
|
2113
|
-
selDragCleanup();
|
|
2114
|
-
selDragCleanup = null;
|
|
2235
|
+
if (view.selDragCleanup) {
|
|
2236
|
+
view.selDragCleanup();
|
|
2237
|
+
view.selDragCleanup = null;
|
|
2115
2238
|
}
|
|
2116
2239
|
|
|
2117
2240
|
if (!S.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
2118
|
-
litRender(nothing, blockActionBarEl);
|
|
2241
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2119
2242
|
return;
|
|
2120
2243
|
}
|
|
2121
2244
|
|
|
2122
2245
|
const activePanel = getActivePanel();
|
|
2123
2246
|
if (!activePanel) {
|
|
2124
|
-
litRender(nothing, blockActionBarEl);
|
|
2247
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2125
2248
|
return;
|
|
2126
2249
|
}
|
|
2127
2250
|
const el = findCanvasElement(S.selection, activePanel.canvas);
|
|
2128
2251
|
const node = el && getNodeAtPath(S.document, S.selection);
|
|
2129
2252
|
if (!el || !node) {
|
|
2130
|
-
litRender(nothing, blockActionBarEl);
|
|
2253
|
+
litRender(nothing, view.blockActionBarEl);
|
|
2131
2254
|
return;
|
|
2132
2255
|
}
|
|
2133
2256
|
|
|
@@ -2158,6 +2281,32 @@ function renderBlockActionBar() {
|
|
|
2158
2281
|
? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
|
|
2159
2282
|
: nothing}
|
|
2160
2283
|
${S.selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
2284
|
+
${S.selection.length >= 2 && node.tagName
|
|
2285
|
+
? (() => {
|
|
2286
|
+
const isComp =
|
|
2287
|
+
node.tagName.includes("-") &&
|
|
2288
|
+
componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
2289
|
+
if (isComp) {
|
|
2290
|
+
const comp = componentRegistry.find(
|
|
2291
|
+
(/** @type {any} */ c) => c.tagName === node.tagName,
|
|
2292
|
+
);
|
|
2293
|
+
return html`<sp-action-button
|
|
2294
|
+
size="xs"
|
|
2295
|
+
quiet
|
|
2296
|
+
title="Edit Component"
|
|
2297
|
+
@click=${() => navigateToComponent(comp.path)}
|
|
2298
|
+
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
2299
|
+
></sp-action-button>`;
|
|
2300
|
+
}
|
|
2301
|
+
return html`<sp-action-button
|
|
2302
|
+
size="xs"
|
|
2303
|
+
quiet
|
|
2304
|
+
title="Convert to Component"
|
|
2305
|
+
@click=${() => convertToComponent(S)}
|
|
2306
|
+
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
2307
|
+
></sp-action-button>`;
|
|
2308
|
+
})()
|
|
2309
|
+
: nothing}
|
|
2161
2310
|
${showFormat
|
|
2162
2311
|
? html`
|
|
2163
2312
|
<sp-divider size="s" vertical></sp-divider>
|
|
@@ -2186,12 +2335,12 @@ function renderBlockActionBar() {
|
|
|
2186
2335
|
: nothing}
|
|
2187
2336
|
</div>
|
|
2188
2337
|
`,
|
|
2189
|
-
blockActionBarEl,
|
|
2338
|
+
view.blockActionBarEl,
|
|
2190
2339
|
);
|
|
2191
2340
|
|
|
2192
2341
|
// Post-render side effects
|
|
2193
2342
|
requestAnimationFrame(() => {
|
|
2194
|
-
const bar = blockActionBarEl?.firstElementChild;
|
|
2343
|
+
const bar = view.blockActionBarEl?.firstElementChild;
|
|
2195
2344
|
if (!bar) return;
|
|
2196
2345
|
// Clamp to window
|
|
2197
2346
|
const barRect = bar.getBoundingClientRect();
|
|
@@ -2202,7 +2351,11 @@ function renderBlockActionBar() {
|
|
|
2202
2351
|
if (S.selection.length >= 2) {
|
|
2203
2352
|
const handle = bar.querySelector(".bar-drag-handle");
|
|
2204
2353
|
if (handle) {
|
|
2205
|
-
selDragCleanup
|
|
2354
|
+
if (view.selDragCleanup) {
|
|
2355
|
+
view.selDragCleanup();
|
|
2356
|
+
view.selDragCleanup = null;
|
|
2357
|
+
}
|
|
2358
|
+
view.selDragCleanup = draggable({
|
|
2206
2359
|
element: handle,
|
|
2207
2360
|
getInitialData: () => ({ type: "tree-node", path: S.selection }),
|
|
2208
2361
|
});
|
|
@@ -2215,20 +2368,15 @@ function renderBlockActionBar() {
|
|
|
2215
2368
|
// When a pseudo-selector (:hover, :focus, etc.) is active in the style sidebar,
|
|
2216
2369
|
// force those styles onto the selected element so the user can see the result.
|
|
2217
2370
|
|
|
2218
|
-
/** @type {any} */
|
|
2219
|
-
let _forcedStyleTag = null;
|
|
2220
|
-
/** @type {any} */
|
|
2221
|
-
let _forcedAttrEl = null;
|
|
2222
|
-
|
|
2223
2371
|
function updateForcedPseudoPreview() {
|
|
2224
2372
|
// Clean up previous
|
|
2225
|
-
if (
|
|
2226
|
-
|
|
2227
|
-
|
|
2373
|
+
if (view.forcedStyleTag) {
|
|
2374
|
+
view.forcedStyleTag.remove();
|
|
2375
|
+
view.forcedStyleTag = null;
|
|
2228
2376
|
}
|
|
2229
|
-
if (
|
|
2230
|
-
|
|
2231
|
-
|
|
2377
|
+
if (view.forcedAttrEl) {
|
|
2378
|
+
view.forcedAttrEl.removeAttribute("data-studio-forced");
|
|
2379
|
+
view.forcedAttrEl = null;
|
|
2232
2380
|
}
|
|
2233
2381
|
|
|
2234
2382
|
const sel = S.ui?.activeSelector;
|
|
@@ -2259,12 +2407,12 @@ function updateForcedPseudoPreview() {
|
|
|
2259
2407
|
if (!cssProps) return;
|
|
2260
2408
|
|
|
2261
2409
|
el.setAttribute("data-studio-forced", "1");
|
|
2262
|
-
|
|
2410
|
+
view.forcedAttrEl = el;
|
|
2263
2411
|
|
|
2264
2412
|
const tag = document.createElement("style");
|
|
2265
2413
|
tag.textContent = `[data-studio-forced] { ${cssProps} }`;
|
|
2266
2414
|
document.head.appendChild(tag);
|
|
2267
|
-
|
|
2415
|
+
view.forcedStyleTag = tag;
|
|
2268
2416
|
}
|
|
2269
2417
|
|
|
2270
2418
|
/**
|
|
@@ -2316,9 +2464,24 @@ function findCanvasElement(path, canvasEl) {
|
|
|
2316
2464
|
} else {
|
|
2317
2465
|
el = el.children[idx];
|
|
2318
2466
|
}
|
|
2319
|
-
if (!el)
|
|
2467
|
+
if (!el) break;
|
|
2320
2468
|
}
|
|
2321
|
-
|
|
2469
|
+
|
|
2470
|
+
// Verify the result: if DOM traversal landed on the wrong element
|
|
2471
|
+
// (e.g. a custom element template child instead of the intended node),
|
|
2472
|
+
// fall back to scanning elToPath.
|
|
2473
|
+
if (el) {
|
|
2474
|
+
const elPath = elToPath.get(el);
|
|
2475
|
+
if (elPath && pathsEqual(elPath, path)) return el;
|
|
2476
|
+
// el has no path or wrong path — it's a template element, not the target
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// Fall back: scan all descendants for an element with matching elToPath
|
|
2480
|
+
for (const candidate of canvasEl.querySelectorAll("*")) {
|
|
2481
|
+
const p = elToPath.get(candidate);
|
|
2482
|
+
if (p && pathsEqual(p, path)) return candidate;
|
|
2483
|
+
}
|
|
2484
|
+
return null;
|
|
2322
2485
|
}
|
|
2323
2486
|
|
|
2324
2487
|
// ─── Per-panel click-to-select ────────────────────────────────────────────────
|
|
@@ -2326,6 +2489,9 @@ function findCanvasElement(path, canvasEl) {
|
|
|
2326
2489
|
/** @param {any} panel */
|
|
2327
2490
|
function registerPanelEvents(panel) {
|
|
2328
2491
|
const { canvas, overlayClk, mediaName } = panel;
|
|
2492
|
+
const ac = new AbortController();
|
|
2493
|
+
const opts = { signal: ac.signal };
|
|
2494
|
+
view.canvasEventCleanups.push(() => ac.abort());
|
|
2329
2495
|
|
|
2330
2496
|
/** @param {any} fn */
|
|
2331
2497
|
function withPanelPointerEvents(fn) {
|
|
@@ -2341,162 +2507,191 @@ function registerPanelEvents(panel) {
|
|
|
2341
2507
|
// During component inline edit, the overlayClk is disabled (see enterComponentInlineEdit).
|
|
2342
2508
|
// No mousedown passthrough needed — native events reach the contenteditable directly.
|
|
2343
2509
|
|
|
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.
|
|
2510
|
+
overlayClk.addEventListener(
|
|
2511
|
+
"click",
|
|
2512
|
+
(/** @type {any} */ e) => {
|
|
2513
|
+
// Don't intercept clicks meant for the block action bar
|
|
2514
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2515
|
+
if (barInner) {
|
|
2516
|
+
const r = barInner.getBoundingClientRect();
|
|
2517
|
+
if (
|
|
2518
|
+
e.clientX >= r.left &&
|
|
2519
|
+
e.clientX <= r.right &&
|
|
2520
|
+
e.clientY >= r.top &&
|
|
2521
|
+
e.clientY <= r.bottom
|
|
2522
|
+
)
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
// If content-mode inline editing is active, treat click outside as blur
|
|
2526
|
+
if (isEditing()) {
|
|
2527
|
+
stopEditing();
|
|
2528
|
+
}
|
|
2364
2529
|
|
|
2365
|
-
|
|
2530
|
+
// Component-mode inline editing is handled by its own document-level listener
|
|
2531
|
+
// (see enterComponentInlineEdit), so nothing to do here — just fall through.
|
|
2366
2532
|
|
|
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 } };
|
|
2533
|
+
const elements = withPanelPointerEvents(() =>
|
|
2534
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2535
|
+
);
|
|
2374
2536
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2537
|
+
for (const el of elements) {
|
|
2538
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2539
|
+
const originalPath = elToPath.get(el);
|
|
2540
|
+
if (originalPath) {
|
|
2541
|
+
let path = bubbleInlinePath(S.document, originalPath);
|
|
2542
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2543
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2544
|
+
|
|
2545
|
+
// Find the DOM element for the bubbled path (may differ from hit element)
|
|
2546
|
+
// When path didn't change (no inline bubbling), prefer the hit element directly
|
|
2547
|
+
// since findCanvasElement can't navigate into custom element template DOM.
|
|
2548
|
+
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2549
|
+
|
|
2550
|
+
// Re-click on selected editable block: enter inline editing
|
|
2551
|
+
// Edit mode / content mode → rich text editing (enterInlineEdit)
|
|
2552
|
+
// Design mode → plaintext component editing (enterComponentInlineEdit via view.pendingInlineEdit)
|
|
2553
|
+
if (
|
|
2554
|
+
pathsEqual(path, S.selection) &&
|
|
2555
|
+
isEditableBlock(resolvedEl) &&
|
|
2556
|
+
(canvasMode === "edit" || S.mode === "content")
|
|
2557
|
+
) {
|
|
2558
|
+
S = withMedia;
|
|
2559
|
+
enterInlineEdit(resolvedEl, path);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2377
2562
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
(canvasMode === "edit" || S.mode === "content")
|
|
2385
|
-
) {
|
|
2386
|
-
enterInlineEdit(resolvedEl, path);
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2563
|
+
// Design mode or first click: select and schedule component inline editing
|
|
2564
|
+
if (canvasMode === "design" && S.mode !== "content") {
|
|
2565
|
+
view.pendingInlineEdit = { path, mediaName };
|
|
2566
|
+
update(selectNode(withMedia, path));
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2389
2569
|
|
|
2390
|
-
|
|
2391
|
-
if (canvasMode === "design" && S.mode !== "content") {
|
|
2392
|
-
pendingInlineEdit = { path, mediaName };
|
|
2393
|
-
update(selectNode(S, path));
|
|
2570
|
+
update(selectNode(withMedia, path));
|
|
2394
2571
|
return;
|
|
2395
2572
|
}
|
|
2396
|
-
|
|
2397
|
-
update(selectNode(S, path));
|
|
2398
|
-
return;
|
|
2399
2573
|
}
|
|
2400
2574
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2575
|
+
update(selectNode(S, null));
|
|
2576
|
+
},
|
|
2577
|
+
opts,
|
|
2578
|
+
);
|
|
2404
2579
|
|
|
2405
2580
|
// Double-click shortcut for immediate inline editing
|
|
2406
|
-
overlayClk.addEventListener(
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
const
|
|
2410
|
-
if (
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2581
|
+
overlayClk.addEventListener(
|
|
2582
|
+
"dblclick",
|
|
2583
|
+
(/** @type {any} */ e) => {
|
|
2584
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2585
|
+
if (barInner) {
|
|
2586
|
+
const r = barInner.getBoundingClientRect();
|
|
2587
|
+
if (
|
|
2588
|
+
e.clientX >= r.left &&
|
|
2589
|
+
e.clientX <= r.right &&
|
|
2590
|
+
e.clientY >= r.top &&
|
|
2591
|
+
e.clientY <= r.bottom
|
|
2592
|
+
)
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
if (canvasMode !== "edit" && canvasMode !== "design") return;
|
|
2419
2596
|
|
|
2420
|
-
|
|
2597
|
+
const elements = withPanelPointerEvents(() =>
|
|
2598
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2599
|
+
);
|
|
2421
2600
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2601
|
+
for (const el of elements) {
|
|
2602
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2603
|
+
const originalPath = elToPath.get(el);
|
|
2604
|
+
if (originalPath) {
|
|
2605
|
+
const path = bubbleInlinePath(S.document, originalPath);
|
|
2606
|
+
const resolvedEl = path === originalPath ? el : findCanvasElement(path, canvas) || el;
|
|
2607
|
+
if (isEditableBlock(resolvedEl)) {
|
|
2608
|
+
const newMedia = mediaName === "base" ? null : (mediaName ?? null);
|
|
2609
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: newMedia } };
|
|
2610
|
+
update(selectNode(withMedia, path));
|
|
2611
|
+
enterInlineEdit(resolvedEl, path);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
},
|
|
2618
|
+
opts,
|
|
2619
|
+
);
|
|
2620
|
+
|
|
2621
|
+
overlayClk.addEventListener(
|
|
2622
|
+
"contextmenu",
|
|
2623
|
+
(/** @type {any} */ e) => {
|
|
2624
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2625
|
+
if (barInner) {
|
|
2626
|
+
const r = barInner.getBoundingClientRect();
|
|
2627
|
+
if (
|
|
2628
|
+
e.clientX >= r.left &&
|
|
2629
|
+
e.clientX <= r.right &&
|
|
2630
|
+
e.clientY >= r.top &&
|
|
2631
|
+
e.clientY <= r.bottom
|
|
2632
|
+
)
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const elements = withPanelPointerEvents(() =>
|
|
2636
|
+
document.elementsFromPoint(e.clientX, e.clientY),
|
|
2637
|
+
);
|
|
2638
|
+
for (const el of elements) {
|
|
2639
|
+
if (canvas.contains(el) && el !== canvas) {
|
|
2640
|
+
let path = elToPath.get(el);
|
|
2641
|
+
if (path) {
|
|
2642
|
+
path = bubbleInlinePath(S.document, path);
|
|
2643
|
+
showContextMenu(e, path, S, { onEditComponent: navigateToComponent });
|
|
2433
2644
|
return;
|
|
2434
2645
|
}
|
|
2435
2646
|
}
|
|
2436
2647
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2648
|
+
e.preventDefault();
|
|
2649
|
+
},
|
|
2650
|
+
opts,
|
|
2651
|
+
);
|
|
2439
2652
|
|
|
2440
|
-
overlayClk.addEventListener(
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
const
|
|
2444
|
-
if (
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2653
|
+
overlayClk.addEventListener(
|
|
2654
|
+
"mousemove",
|
|
2655
|
+
(/** @type {any} */ e) => {
|
|
2656
|
+
const barInner = view.blockActionBarEl?.firstElementChild;
|
|
2657
|
+
if (barInner) {
|
|
2658
|
+
const r = barInner.getBoundingClientRect();
|
|
2659
|
+
if (
|
|
2660
|
+
e.clientX >= r.left &&
|
|
2661
|
+
e.clientX <= r.right &&
|
|
2662
|
+
e.clientY >= r.top &&
|
|
2663
|
+
e.clientY <= r.bottom
|
|
2664
|
+
)
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const el = withPanelPointerEvents(() => document.elementFromPoint(e.clientX, e.clientY));
|
|
2668
|
+
if (el && canvas.contains(el) && el !== canvas) {
|
|
2455
2669
|
let path = elToPath.get(el);
|
|
2456
2670
|
if (path) {
|
|
2457
2671
|
path = bubbleInlinePath(S.document, path);
|
|
2458
|
-
|
|
2459
|
-
|
|
2672
|
+
if (!pathsEqual(path, S.hover)) {
|
|
2673
|
+
S = hoverNode(S, path);
|
|
2674
|
+
renderOverlays();
|
|
2675
|
+
}
|
|
2460
2676
|
}
|
|
2677
|
+
} else if (S.hover) {
|
|
2678
|
+
S = hoverNode(S, null);
|
|
2679
|
+
renderOverlays();
|
|
2461
2680
|
}
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
|
|
2681
|
+
},
|
|
2682
|
+
opts,
|
|
2683
|
+
);
|
|
2465
2684
|
|
|
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
|
-
}
|
|
2685
|
+
overlayClk.addEventListener(
|
|
2686
|
+
"mouseleave",
|
|
2687
|
+
() => {
|
|
2688
|
+
if (S.hover) {
|
|
2689
|
+
S = hoverNode(S, null);
|
|
2690
|
+
renderOverlays();
|
|
2487
2691
|
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
});
|
|
2493
|
-
|
|
2494
|
-
overlayClk.addEventListener("mouseleave", () => {
|
|
2495
|
-
if (S.hover) {
|
|
2496
|
-
S = hoverNode(S, null);
|
|
2497
|
-
renderOverlays();
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
2692
|
+
},
|
|
2693
|
+
opts,
|
|
2694
|
+
);
|
|
2500
2695
|
}
|
|
2501
2696
|
|
|
2502
2697
|
// ─── Inline editing bridge ────────────────────────────────────────────────────
|
|
@@ -2589,12 +2784,63 @@ function enterInlineEdit(el, path) {
|
|
|
2589
2784
|
});
|
|
2590
2785
|
},
|
|
2591
2786
|
|
|
2592
|
-
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd) {
|
|
2787
|
+
onInsert(/** @type {any} */ afterPath, /** @type {any} */ cmd, /** @type {any} */ commitData) {
|
|
2593
2788
|
// cmd comes from the shared slash menu: { label, tag, description }
|
|
2789
|
+
const isEmpty =
|
|
2790
|
+
!commitData ||
|
|
2791
|
+
(commitData.textContent != null && commitData.textContent.trim() === "") ||
|
|
2792
|
+
(commitData.children &&
|
|
2793
|
+
(commitData.children.length === 0 ||
|
|
2794
|
+
(commitData.children.length === 1 &&
|
|
2795
|
+
typeof commitData.children[0] === "string" &&
|
|
2796
|
+
commitData.children[0].trim() === "") ||
|
|
2797
|
+
(commitData.children.length === 1 &&
|
|
2798
|
+
typeof commitData.children[0] === "object" &&
|
|
2799
|
+
commitData.children[0]?.tagName === "br")));
|
|
2800
|
+
|
|
2801
|
+
// If the element is empty, swap its tagName instead of inserting after
|
|
2802
|
+
if (isEmpty) {
|
|
2803
|
+
let s = S;
|
|
2804
|
+
s = updateProperty(s, afterPath, "tagName", cmd.tag);
|
|
2805
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
2806
|
+
const def = defaultDef(cmd.tag);
|
|
2807
|
+
if (def.textContent && def.textContent !== "Paragraph text") {
|
|
2808
|
+
s = updateProperty(s, afterPath, "textContent", def.textContent);
|
|
2809
|
+
} else {
|
|
2810
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2811
|
+
}
|
|
2812
|
+
s = selectNode(s, afterPath);
|
|
2813
|
+
update(s);
|
|
2814
|
+
|
|
2815
|
+
requestAnimationFrame(() => {
|
|
2816
|
+
const activePanel = getActivePanel();
|
|
2817
|
+
if (activePanel) {
|
|
2818
|
+
const el = findCanvasElement(afterPath, activePanel.canvas);
|
|
2819
|
+
if (el && isEditableBlock(el)) {
|
|
2820
|
+
enterInlineEdit(el, afterPath);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
});
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2594
2827
|
const elementDef = defaultDef(cmd.tag);
|
|
2595
2828
|
const parentPath = /** @type {any} */ (parentElementPath(afterPath));
|
|
2596
2829
|
const idx = /** @type {number} */ (childIndex(afterPath));
|
|
2597
|
-
|
|
2830
|
+
|
|
2831
|
+
// Apply pending commit from inline edit first (batched to avoid double render)
|
|
2832
|
+
let s = S;
|
|
2833
|
+
if (commitData) {
|
|
2834
|
+
if (commitData.children) {
|
|
2835
|
+
s = updateProperty(s, afterPath, "textContent", undefined);
|
|
2836
|
+
s = updateProperty(s, afterPath, "children", commitData.children);
|
|
2837
|
+
} else if (commitData.textContent != null) {
|
|
2838
|
+
s = updateProperty(s, afterPath, "children", undefined);
|
|
2839
|
+
s = updateProperty(s, afterPath, "textContent", commitData.textContent);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
s = insertNode(s, parentPath, idx + 1, structuredClone(elementDef));
|
|
2598
2844
|
const newPath = [...parentPath, "children", idx + 1];
|
|
2599
2845
|
s = selectNode(s, newPath);
|
|
2600
2846
|
update(s);
|
|
@@ -2613,9 +2859,9 @@ function enterInlineEdit(el, path) {
|
|
|
2613
2859
|
|
|
2614
2860
|
onEnd() {
|
|
2615
2861
|
// Cleanup inline edit listeners
|
|
2616
|
-
if (
|
|
2617
|
-
|
|
2618
|
-
|
|
2862
|
+
if (view.inlineEditCleanup) {
|
|
2863
|
+
view.inlineEditCleanup();
|
|
2864
|
+
view.inlineEditCleanup = null;
|
|
2619
2865
|
}
|
|
2620
2866
|
// Restore overlays after inline editing ends
|
|
2621
2867
|
for (const p of canvasPanels) {
|
|
@@ -2642,7 +2888,7 @@ function enterInlineEdit(el, path) {
|
|
|
2642
2888
|
el.removeEventListener("mouseup", selectionHandler);
|
|
2643
2889
|
el.removeEventListener("keyup", selectionHandler);
|
|
2644
2890
|
};
|
|
2645
|
-
|
|
2891
|
+
view.inlineEditCleanup = inlineEditCleanup;
|
|
2646
2892
|
}
|
|
2647
2893
|
|
|
2648
2894
|
// ─── Component-mode inline text editing ──────────────────────────────────────
|
|
@@ -2653,7 +2899,7 @@ function enterInlineEdit(el, path) {
|
|
|
2653
2899
|
*/
|
|
2654
2900
|
function enterComponentInlineEdit(el, path) {
|
|
2655
2901
|
// Already editing this element
|
|
2656
|
-
if (componentInlineEdit && componentInlineEdit.el === el) {
|
|
2902
|
+
if (view.componentInlineEdit && view.componentInlineEdit.el === el) {
|
|
2657
2903
|
return;
|
|
2658
2904
|
}
|
|
2659
2905
|
|
|
@@ -2666,7 +2912,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2666
2912
|
if (Array.isArray(node.children) && node.children.length > 0) return;
|
|
2667
2913
|
if (node.children && typeof node.children === "object") return;
|
|
2668
2914
|
if (tc && typeof tc === "object") return;
|
|
2669
|
-
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed"]);
|
|
2915
|
+
const voids = new Set(["img", "input", "br", "hr", "video", "audio", "source", "embed", "slot"]);
|
|
2670
2916
|
if (voids.has(node.tagName)) return;
|
|
2671
2917
|
|
|
2672
2918
|
// Keep overlay visible for the label, but hide selection border to not obscure editing outline.
|
|
@@ -2690,7 +2936,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2690
2936
|
const rawText = typeof tc === "string" ? tc : "";
|
|
2691
2937
|
el.textContent = rawText;
|
|
2692
2938
|
|
|
2693
|
-
componentInlineEdit = {
|
|
2939
|
+
view.componentInlineEdit = {
|
|
2694
2940
|
el,
|
|
2695
2941
|
path,
|
|
2696
2942
|
originalText: rawText,
|
|
@@ -2712,15 +2958,15 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2712
2958
|
// Document-level mousedown: clicking outside the editing element commits
|
|
2713
2959
|
// the edit and selects the new target element for inline editing.
|
|
2714
2960
|
const outsideHandler = (/** @type {any} */ evt) => {
|
|
2715
|
-
if (!componentInlineEdit) {
|
|
2961
|
+
if (!view.componentInlineEdit) {
|
|
2716
2962
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2717
2963
|
return;
|
|
2718
2964
|
}
|
|
2719
|
-
if (componentInlineEdit.el.contains(evt.target)) return; // click within editing el — let it through
|
|
2965
|
+
if (view.componentInlineEdit.el.contains(evt.target)) return; // click within editing el — let it through
|
|
2720
2966
|
// Let clicks through when the slash command menu is open
|
|
2721
2967
|
if (isSlashMenuOpen()) return;
|
|
2722
2968
|
// Let clicks inside the block action bar through
|
|
2723
|
-
if (blockActionBarEl && blockActionBarEl.contains(evt.target)) return;
|
|
2969
|
+
if (view.blockActionBarEl && view.blockActionBarEl.contains(evt.target)) return;
|
|
2724
2970
|
document.removeEventListener("mousedown", outsideHandler, true);
|
|
2725
2971
|
|
|
2726
2972
|
// Hit-test BEFORE commit (while the current canvas DOM + elToPath are still valid)
|
|
@@ -2747,7 +2993,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2747
2993
|
}
|
|
2748
2994
|
|
|
2749
2995
|
// Commit + select new element in a single state update if possible
|
|
2750
|
-
const { el: editEl, path: editPath, originalText } = componentInlineEdit;
|
|
2996
|
+
const { el: editEl, path: editPath, originalText } = view.componentInlineEdit;
|
|
2751
2997
|
const newText = (editEl.textContent ?? "").trim();
|
|
2752
2998
|
cleanupComponentInlineEdit(editEl);
|
|
2753
2999
|
|
|
@@ -2757,26 +3003,29 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2757
3003
|
|
|
2758
3004
|
if (hitPath) {
|
|
2759
3005
|
const media = hitMedia === "base" ? null : (hitMedia ?? null);
|
|
2760
|
-
pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2761
|
-
|
|
3006
|
+
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
3007
|
+
const withMedia = { ...S, ui: { ...S.ui, activeMedia: media } };
|
|
2762
3008
|
if (isEmpty && pPath) {
|
|
2763
3009
|
// Remove empty node; adjust hitPath if it shifts after removal
|
|
2764
|
-
let s = removeNode(
|
|
3010
|
+
let s = removeNode(withMedia, editPath);
|
|
2765
3011
|
// If hit path is a later sibling in the same parent, adjust index
|
|
2766
3012
|
const removedIdx = /** @type {number} */ (childIndex(editPath));
|
|
2767
3013
|
const hitIdx = /** @type {number} */ (childIndex(hitPath));
|
|
2768
3014
|
const hitParent = parentElementPath(hitPath);
|
|
2769
3015
|
if (hitParent && pPath && hitParent.join("/") === pPath.join("/") && hitIdx > removedIdx) {
|
|
2770
3016
|
hitPath = [...pPath, "children", hitIdx - 1];
|
|
2771
|
-
pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
3017
|
+
view.pendingInlineEdit = { path: hitPath, mediaName: hitMedia };
|
|
2772
3018
|
}
|
|
2773
3019
|
update(selectNode(s, hitPath));
|
|
2774
3020
|
} else if (newText !== originalText) {
|
|
2775
3021
|
update(
|
|
2776
|
-
selectNode(
|
|
3022
|
+
selectNode(
|
|
3023
|
+
updateProperty(withMedia, editPath, "textContent", newText || undefined),
|
|
3024
|
+
hitPath,
|
|
3025
|
+
),
|
|
2777
3026
|
);
|
|
2778
3027
|
} else {
|
|
2779
|
-
update(selectNode(
|
|
3028
|
+
update(selectNode(withMedia, hitPath));
|
|
2780
3029
|
}
|
|
2781
3030
|
} else {
|
|
2782
3031
|
// Clicked on empty space — just commit
|
|
@@ -2791,7 +3040,7 @@ function enterComponentInlineEdit(el, path) {
|
|
|
2791
3040
|
}
|
|
2792
3041
|
};
|
|
2793
3042
|
document.addEventListener("mousedown", outsideHandler, true);
|
|
2794
|
-
componentInlineEdit._outsideHandler = outsideHandler;
|
|
3043
|
+
view.componentInlineEdit._outsideHandler = outsideHandler;
|
|
2795
3044
|
|
|
2796
3045
|
// Re-render block action bar to show inline formatting buttons
|
|
2797
3046
|
renderBlockActionBar();
|
|
@@ -2815,8 +3064,8 @@ function componentInlineKeydown(e) {
|
|
|
2815
3064
|
}
|
|
2816
3065
|
|
|
2817
3066
|
function splitParagraph() {
|
|
2818
|
-
if (!componentInlineEdit) return;
|
|
2819
|
-
const { el, path, mediaName } = componentInlineEdit;
|
|
3067
|
+
if (!view.componentInlineEdit) return;
|
|
3068
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2820
3069
|
|
|
2821
3070
|
// Determine cursor offset within text
|
|
2822
3071
|
const sel = /** @type {any} */ (el.ownerDocument.defaultView?.getSelection());
|
|
@@ -2848,13 +3097,13 @@ function splitParagraph() {
|
|
|
2848
3097
|
s = insertNode(s, pPath, idx + 1, newDef);
|
|
2849
3098
|
s = selectNode(s, newPath);
|
|
2850
3099
|
|
|
2851
|
-
pendingInlineEdit = { path: newPath, mediaName };
|
|
3100
|
+
view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2852
3101
|
update(s);
|
|
2853
3102
|
}
|
|
2854
3103
|
|
|
2855
3104
|
function _commitComponentInlineEdit() {
|
|
2856
|
-
if (!componentInlineEdit) return;
|
|
2857
|
-
const { el, path, originalText } = componentInlineEdit;
|
|
3105
|
+
if (!view.componentInlineEdit) return;
|
|
3106
|
+
const { el, path, originalText } = view.componentInlineEdit;
|
|
2858
3107
|
const newText = (el.textContent ?? "").trim();
|
|
2859
3108
|
|
|
2860
3109
|
cleanupComponentInlineEdit(el);
|
|
@@ -2872,8 +3121,8 @@ function _commitComponentInlineEdit() {
|
|
|
2872
3121
|
}
|
|
2873
3122
|
|
|
2874
3123
|
function cancelComponentInlineEdit() {
|
|
2875
|
-
if (!componentInlineEdit) return;
|
|
2876
|
-
const { el } = componentInlineEdit;
|
|
3124
|
+
if (!view.componentInlineEdit) return;
|
|
3125
|
+
const { el } = view.componentInlineEdit;
|
|
2877
3126
|
cleanupComponentInlineEdit(el);
|
|
2878
3127
|
renderCanvas();
|
|
2879
3128
|
renderOverlays();
|
|
@@ -2892,10 +3141,10 @@ function cleanupComponentInlineEdit(el) {
|
|
|
2892
3141
|
el.style.pointerEvents = "";
|
|
2893
3142
|
|
|
2894
3143
|
// Remove the document-level outside-click handler
|
|
2895
|
-
if (componentInlineEdit?._outsideHandler) {
|
|
2896
|
-
document.removeEventListener("mousedown", componentInlineEdit._outsideHandler, true);
|
|
3144
|
+
if (view.componentInlineEdit?._outsideHandler) {
|
|
3145
|
+
document.removeEventListener("mousedown", view.componentInlineEdit._outsideHandler, true);
|
|
2897
3146
|
}
|
|
2898
|
-
componentInlineEdit = null;
|
|
3147
|
+
view.componentInlineEdit = null;
|
|
2899
3148
|
|
|
2900
3149
|
// Restore overlay and click interceptor
|
|
2901
3150
|
for (const p of canvasPanels) {
|
|
@@ -2907,8 +3156,8 @@ function cleanupComponentInlineEdit(el) {
|
|
|
2907
3156
|
// ─── Component-mode slash commands (delegates to shared slash-menu.js) ────────
|
|
2908
3157
|
|
|
2909
3158
|
function componentInlineInput() {
|
|
2910
|
-
if (!componentInlineEdit) return;
|
|
2911
|
-
const { el, originalText } = componentInlineEdit;
|
|
3159
|
+
if (!view.componentInlineEdit) return;
|
|
3160
|
+
const { el, originalText } = view.componentInlineEdit;
|
|
2912
3161
|
const text = el.textContent || "";
|
|
2913
3162
|
|
|
2914
3163
|
// Only trigger slash menu when the paragraph was originally empty and starts with /
|
|
@@ -2922,8 +3171,8 @@ function componentInlineInput() {
|
|
|
2922
3171
|
|
|
2923
3172
|
/** @param {any} cmd */
|
|
2924
3173
|
function handleComponentSlashSelect(cmd) {
|
|
2925
|
-
if (!componentInlineEdit) return;
|
|
2926
|
-
const { el, path, mediaName } = componentInlineEdit;
|
|
3174
|
+
if (!view.componentInlineEdit) return;
|
|
3175
|
+
const { el, path, mediaName } = view.componentInlineEdit;
|
|
2927
3176
|
const pPath = parentElementPath(path);
|
|
2928
3177
|
const idx = /** @type {number} */ (childIndex(path));
|
|
2929
3178
|
if (!pPath) return;
|
|
@@ -2940,7 +3189,7 @@ function handleComponentSlashSelect(cmd) {
|
|
|
2940
3189
|
|
|
2941
3190
|
// If the new element has textContent, enter inline edit on it
|
|
2942
3191
|
const hasText = newDef.textContent != null;
|
|
2943
|
-
if (hasText) pendingInlineEdit = { path: newPath, mediaName };
|
|
3192
|
+
if (hasText) view.pendingInlineEdit = { path: newPath, mediaName };
|
|
2944
3193
|
update(s);
|
|
2945
3194
|
}
|
|
2946
3195
|
|
|
@@ -2952,7 +3201,7 @@ function renderLeftPanel() {
|
|
|
2952
3201
|
/** @type {any} */
|
|
2953
3202
|
let content;
|
|
2954
3203
|
if (tab === "layers")
|
|
2955
|
-
content = canvasMode === "
|
|
3204
|
+
content = canvasMode === "settings" ? renderStylebookLayersTemplate() : renderLayersTemplate();
|
|
2956
3205
|
else if (tab === "imports")
|
|
2957
3206
|
content = renderImportsTemplate({
|
|
2958
3207
|
renderLeftPanel,
|
|
@@ -2965,20 +3214,45 @@ function renderLeftPanel() {
|
|
|
2965
3214
|
});
|
|
2966
3215
|
else if (tab === "files") content = renderFilesTemplate();
|
|
2967
3216
|
else if (tab === "blocks") content = renderElementsTemplate();
|
|
2968
|
-
else if (tab === "state")
|
|
3217
|
+
else if (tab === "state")
|
|
3218
|
+
content = renderSignalsTemplate(S, { renderLeftPanel, renderCanvas, updateSession });
|
|
2969
3219
|
else if (tab === "data")
|
|
2970
|
-
content = renderDataExplorerTemplate(S.document.state, liveScope, {
|
|
3220
|
+
content = renderDataExplorerTemplate(S.document.state, view.liveScope, {
|
|
2971
3221
|
renderCanvas,
|
|
2972
3222
|
renderLeftPanel,
|
|
2973
3223
|
defCategory,
|
|
2974
3224
|
defBadgeLabel,
|
|
2975
3225
|
});
|
|
2976
|
-
else
|
|
3226
|
+
else if (tab === "head") {
|
|
3227
|
+
// In content mode, title/$head live in S.content.frontmatter, not S.document
|
|
3228
|
+
const isContent = S.mode === "content";
|
|
3229
|
+
const fm = S.content?.frontmatter ?? {};
|
|
3230
|
+
const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
|
|
3231
|
+
content = renderHeadTemplate({
|
|
3232
|
+
document: headDoc,
|
|
3233
|
+
applyMutation: isContent
|
|
3234
|
+
? (/** @type {any} */ fn) => {
|
|
3235
|
+
// Apply mutation to a temporary doc, then sync title/$head back to frontmatter
|
|
3236
|
+
const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
|
|
3237
|
+
fn(tmp);
|
|
3238
|
+
if (tmp.title !== fm.title) S = updateFrontmatter(S, "title", tmp.title);
|
|
3239
|
+
// Always sync $head (may have been created, modified, or emptied)
|
|
3240
|
+
const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
|
|
3241
|
+
S = updateFrontmatter(S, "$head", newHead);
|
|
3242
|
+
update(S);
|
|
3243
|
+
}
|
|
3244
|
+
: (/** @type {any} */ fn) => {
|
|
3245
|
+
S = applyMutation(S, fn);
|
|
3246
|
+
update(S);
|
|
3247
|
+
},
|
|
3248
|
+
renderLeftPanel,
|
|
3249
|
+
});
|
|
3250
|
+
} else content = nothing;
|
|
2977
3251
|
|
|
2978
3252
|
litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
|
|
2979
3253
|
|
|
2980
3254
|
// Post-render side effects
|
|
2981
|
-
if (tab === "layers" && canvasMode !== "
|
|
3255
|
+
if (tab === "layers" && canvasMode !== "settings") registerLayersDnD();
|
|
2982
3256
|
else if (tab === "imports") {
|
|
2983
3257
|
/* no post-render DnD needed */
|
|
2984
3258
|
} else if (tab === "blocks") {
|
|
@@ -2993,8 +3267,8 @@ function renderLeftPanel() {
|
|
|
2993
3267
|
/** Returns a TemplateResult — called from renderLeftPanel only when tab=layers & not stylebook */
|
|
2994
3268
|
function renderLayersTemplate() {
|
|
2995
3269
|
// Clean up previous DnD registrations
|
|
2996
|
-
for (const fn of dndCleanups) fn();
|
|
2997
|
-
dndCleanups = [];
|
|
3270
|
+
for (const fn of view.dndCleanups) fn();
|
|
3271
|
+
view.dndCleanups = [];
|
|
2998
3272
|
|
|
2999
3273
|
const rows = flattenTree(S.document);
|
|
3000
3274
|
const collapsed = S._collapsed || (S._collapsed = new Set());
|
|
@@ -3014,6 +3288,9 @@ function renderLayersTemplate() {
|
|
|
3014
3288
|
}
|
|
3015
3289
|
if (hidden) continue;
|
|
3016
3290
|
|
|
3291
|
+
// In content mode, skip the document root row (it's not a real element)
|
|
3292
|
+
if (S.mode === "content" && path.length === 0) continue;
|
|
3293
|
+
|
|
3017
3294
|
// Text node children: display-only row with truncated preview
|
|
3018
3295
|
if (nodeType === "text") {
|
|
3019
3296
|
const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
|
|
@@ -3084,7 +3361,7 @@ function renderLayersTemplate() {
|
|
|
3084
3361
|
|
|
3085
3362
|
// Compute move-button availability for element nodes
|
|
3086
3363
|
const isElement = nodeType === "element";
|
|
3087
|
-
const isRoot = path.length < 2;
|
|
3364
|
+
const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
|
|
3088
3365
|
const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
|
|
3089
3366
|
const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
|
|
3090
3367
|
const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
|
|
@@ -3113,7 +3390,10 @@ function renderLayersTemplate() {
|
|
|
3113
3390
|
data-dnd-depth=${isElement ? depth : nothing}
|
|
3114
3391
|
data-dnd-void=${isElement && isVoidEl ? "" : nothing}
|
|
3115
3392
|
@click=${() => update(selectNode(S, path))}
|
|
3116
|
-
@contextmenu=${isElement
|
|
3393
|
+
@contextmenu=${isElement
|
|
3394
|
+
? (/** @type {any} */ e) =>
|
|
3395
|
+
showContextMenu(e, path, S, { onEditComponent: navigateToComponent })
|
|
3396
|
+
: nothing}
|
|
3117
3397
|
>
|
|
3118
3398
|
<span class="layer-indent" style="width:${depth * 16}px"></span>
|
|
3119
3399
|
<span class="layer-toggle"
|
|
@@ -3267,7 +3547,7 @@ function registerLayersDnD() {
|
|
|
3267
3547
|
},
|
|
3268
3548
|
onDragStart() {
|
|
3269
3549
|
row.classList.add("dragging");
|
|
3270
|
-
layerDragSourceHeight = row.offsetHeight;
|
|
3550
|
+
view.layerDragSourceHeight = row.offsetHeight;
|
|
3271
3551
|
},
|
|
3272
3552
|
onDrop() {
|
|
3273
3553
|
row.classList.remove("dragging");
|
|
@@ -3306,7 +3586,7 @@ function registerLayersDnD() {
|
|
|
3306
3586
|
},
|
|
3307
3587
|
}),
|
|
3308
3588
|
);
|
|
3309
|
-
dndCleanups.push(cleanup);
|
|
3589
|
+
view.dndCleanups.push(cleanup);
|
|
3310
3590
|
},
|
|
3311
3591
|
);
|
|
3312
3592
|
|
|
@@ -3323,7 +3603,7 @@ function registerLayersDnD() {
|
|
|
3323
3603
|
applyDropInstruction(instruction, srcData, targetPath);
|
|
3324
3604
|
},
|
|
3325
3605
|
});
|
|
3326
|
-
dndCleanups.push(monitorCleanup);
|
|
3606
|
+
view.dndCleanups.push(monitorCleanup);
|
|
3327
3607
|
});
|
|
3328
3608
|
}
|
|
3329
3609
|
|
|
@@ -3364,15 +3644,13 @@ function registerComponentsDnD() {
|
|
|
3364
3644
|
return { type: "block", fragment: structuredClone(instanceDef) };
|
|
3365
3645
|
},
|
|
3366
3646
|
});
|
|
3367
|
-
dndCleanups.push(cleanup);
|
|
3647
|
+
view.dndCleanups.push(cleanup);
|
|
3368
3648
|
},
|
|
3369
3649
|
);
|
|
3370
3650
|
});
|
|
3371
3651
|
}
|
|
3372
3652
|
|
|
3373
3653
|
/** @type {any} */
|
|
3374
|
-
let _currentDropTargetRow = null;
|
|
3375
|
-
let layerDragSourceHeight = 0;
|
|
3376
3654
|
|
|
3377
3655
|
/**
|
|
3378
3656
|
* @param {any} rowEl
|
|
@@ -3383,8 +3661,8 @@ function showLayerDropGap(rowEl, data, container) {
|
|
|
3383
3661
|
const instruction = extractInstruction(data);
|
|
3384
3662
|
|
|
3385
3663
|
// Clear previous drop-target highlight
|
|
3386
|
-
if (_currentDropTargetRow && _currentDropTargetRow !== rowEl) {
|
|
3387
|
-
_currentDropTargetRow.classList.remove("drop-target");
|
|
3664
|
+
if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
|
|
3665
|
+
view._currentDropTargetRow.classList.remove("drop-target");
|
|
3388
3666
|
}
|
|
3389
3667
|
|
|
3390
3668
|
if (!instruction || instruction.type === "instruction-blocked") {
|
|
@@ -3395,17 +3673,17 @@ function showLayerDropGap(rowEl, data, container) {
|
|
|
3395
3673
|
if (instruction.type === "make-child") {
|
|
3396
3674
|
clearLayerDropGap(container);
|
|
3397
3675
|
rowEl.classList.add("drop-target");
|
|
3398
|
-
_currentDropTargetRow = rowEl;
|
|
3676
|
+
view._currentDropTargetRow = rowEl;
|
|
3399
3677
|
return;
|
|
3400
3678
|
}
|
|
3401
3679
|
|
|
3402
3680
|
rowEl.classList.remove("drop-target");
|
|
3403
|
-
_currentDropTargetRow = rowEl;
|
|
3681
|
+
view._currentDropTargetRow = rowEl;
|
|
3404
3682
|
|
|
3405
3683
|
// Shift rows to create gap
|
|
3406
3684
|
const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
|
|
3407
3685
|
const targetIdx = rows.indexOf(rowEl);
|
|
3408
|
-
const gap = layerDragSourceHeight;
|
|
3686
|
+
const gap = view.layerDragSourceHeight;
|
|
3409
3687
|
|
|
3410
3688
|
for (let i = 0; i < rows.length; i++) {
|
|
3411
3689
|
if (rows[i].classList.contains("dragging")) continue;
|
|
@@ -3419,9 +3697,9 @@ function showLayerDropGap(rowEl, data, container) {
|
|
|
3419
3697
|
|
|
3420
3698
|
/** @param {any} container */
|
|
3421
3699
|
function clearLayerDropGap(container) {
|
|
3422
|
-
if (_currentDropTargetRow) {
|
|
3423
|
-
_currentDropTargetRow.classList.remove("drop-target");
|
|
3424
|
-
_currentDropTargetRow = null;
|
|
3700
|
+
if (view._currentDropTargetRow) {
|
|
3701
|
+
view._currentDropTargetRow.classList.remove("drop-target");
|
|
3702
|
+
view._currentDropTargetRow = null;
|
|
3425
3703
|
}
|
|
3426
3704
|
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
3427
3705
|
for (const r of rows) r.style.transform = "";
|
|
@@ -3434,25 +3712,22 @@ function clearLayerDropGap(container) {
|
|
|
3434
3712
|
* @param {string | null} [media]
|
|
3435
3713
|
*/
|
|
3436
3714
|
function selectStylebookTag(tag, media) {
|
|
3437
|
-
|
|
3438
|
-
...S,
|
|
3715
|
+
updateSession({
|
|
3439
3716
|
selection: [],
|
|
3440
3717
|
ui: {
|
|
3441
|
-
...S.ui,
|
|
3442
3718
|
stylebookSelection: tag,
|
|
3443
3719
|
rightTab: "style",
|
|
3444
3720
|
activeSelector: `& ${tag}`,
|
|
3445
3721
|
...(media !== undefined ? { activeMedia: media } : {}),
|
|
3446
3722
|
},
|
|
3447
|
-
};
|
|
3723
|
+
});
|
|
3448
3724
|
renderStylebookOverlays();
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
}
|
|
3725
|
+
requestAnimationFrame(() => {
|
|
3726
|
+
if (canvasPanels.length > 0) {
|
|
3727
|
+
const el = findStylebookEl(canvasPanels[0].canvas, tag);
|
|
3728
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3456
3731
|
}
|
|
3457
3732
|
|
|
3458
3733
|
function renderStylebookLayersTemplate() {
|
|
@@ -3705,18 +3980,18 @@ const unsafeTags = new Set(["script", "style", "link", "iframe", "object", "embe
|
|
|
3705
3980
|
function renderElementsTemplate() {
|
|
3706
3981
|
const categories = Object.entries(webdata.elements).map(
|
|
3707
3982
|
(/** @type {any} */ [category, elements]) => {
|
|
3708
|
-
const filtered = elementsFilter
|
|
3709
|
-
? elements.filter((/** @type {any} */ e) => e.tag.includes(elementsFilter))
|
|
3983
|
+
const filtered = view.elementsFilter
|
|
3984
|
+
? elements.filter((/** @type {any} */ e) => e.tag.includes(view.elementsFilter))
|
|
3710
3985
|
: elements;
|
|
3711
3986
|
if (filtered.length === 0) return nothing;
|
|
3712
3987
|
|
|
3713
3988
|
return html`
|
|
3714
3989
|
<sp-accordion-item
|
|
3715
3990
|
label=${category}
|
|
3716
|
-
?open=${!elementsCollapsed.has(category)}
|
|
3991
|
+
?open=${!view.elementsCollapsed.has(category)}
|
|
3717
3992
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
3718
|
-
if (e.target.open) elementsCollapsed.delete(category);
|
|
3719
|
-
else elementsCollapsed.add(category);
|
|
3993
|
+
if (e.target.open) view.elementsCollapsed.delete(category);
|
|
3994
|
+
else view.elementsCollapsed.add(category);
|
|
3720
3995
|
}}
|
|
3721
3996
|
>
|
|
3722
3997
|
${filtered.map((/** @type {any} */ { tag }) => {
|
|
@@ -3768,7 +4043,7 @@ function renderElementsTemplate() {
|
|
|
3768
4043
|
.filter((/** @type {any} */ c) => c.source !== "npm" || enabledTags.has(c.tagName))
|
|
3769
4044
|
.filter(
|
|
3770
4045
|
(/** @type {any} */ c) =>
|
|
3771
|
-
!elementsFilter || c.tagName.toLowerCase().includes(elementsFilter),
|
|
4046
|
+
!view.elementsFilter || c.tagName.toLowerCase().includes(view.elementsFilter),
|
|
3772
4047
|
)
|
|
3773
4048
|
: [];
|
|
3774
4049
|
|
|
@@ -3777,10 +4052,10 @@ function renderElementsTemplate() {
|
|
|
3777
4052
|
? html`
|
|
3778
4053
|
<sp-accordion-item
|
|
3779
4054
|
label="Components"
|
|
3780
|
-
?open=${!elementsCollapsed.has("Components")}
|
|
4055
|
+
?open=${!view.elementsCollapsed.has("Components")}
|
|
3781
4056
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
3782
|
-
if (e.target.open) elementsCollapsed.delete("Components");
|
|
3783
|
-
else elementsCollapsed.add("Components");
|
|
4057
|
+
if (e.target.open) view.elementsCollapsed.delete("Components");
|
|
4058
|
+
else view.elementsCollapsed.add("Components");
|
|
3784
4059
|
}}
|
|
3785
4060
|
>
|
|
3786
4061
|
<div class="components-section">
|
|
@@ -3826,9 +4101,9 @@ function renderElementsTemplate() {
|
|
|
3826
4101
|
<sp-search
|
|
3827
4102
|
size="s"
|
|
3828
4103
|
placeholder="Filter elements…"
|
|
3829
|
-
value=${elementsFilter}
|
|
4104
|
+
value=${view.elementsFilter}
|
|
3830
4105
|
@input=${(/** @type {any} */ e) => {
|
|
3831
|
-
elementsFilter = e.target.value.toLowerCase();
|
|
4106
|
+
view.elementsFilter = e.target.value.toLowerCase();
|
|
3832
4107
|
renderLeftPanel();
|
|
3833
4108
|
}}
|
|
3834
4109
|
></sp-search>
|
|
@@ -3858,7 +4133,7 @@ function registerElementsDnD() {
|
|
|
3858
4133
|
return { type: "block", fragment: structuredClone(def) };
|
|
3859
4134
|
},
|
|
3860
4135
|
});
|
|
3861
|
-
dndCleanups.push(cleanup);
|
|
4136
|
+
view.dndCleanups.push(cleanup);
|
|
3862
4137
|
},
|
|
3863
4138
|
);
|
|
3864
4139
|
});
|
|
@@ -3867,7 +4142,6 @@ function registerElementsDnD() {
|
|
|
3867
4142
|
// ─── Stylebook ───────────────────────────────────────────────────────────────
|
|
3868
4143
|
|
|
3869
4144
|
/** Map from rendered stylebook DOM elements to their tag names */
|
|
3870
|
-
let stylebookElToTag = new WeakMap();
|
|
3871
4145
|
|
|
3872
4146
|
/**
|
|
3873
4147
|
* Build a DOM element tree from a stylebook-meta.json entry. Applies any existing tag-scoped styles
|
|
@@ -3970,8 +4244,52 @@ function hasTagStyle(rootStyle, tag) {
|
|
|
3970
4244
|
return s && typeof s === "object" && Object.keys(s).length > 0;
|
|
3971
4245
|
}
|
|
3972
4246
|
|
|
3973
|
-
function
|
|
3974
|
-
|
|
4247
|
+
function renderSettings() {
|
|
4248
|
+
const settingsTab = S.ui.settingsTab || "stylebook";
|
|
4249
|
+
|
|
4250
|
+
// Top-level settings tabs chrome bar
|
|
4251
|
+
const settingsChromeBarTpl = html`
|
|
4252
|
+
<div
|
|
4253
|
+
class="sb-chrome settings-top-chrome"
|
|
4254
|
+
style="position:absolute;top:0;left:0;right:0;z-index:16;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
4255
|
+
>
|
|
4256
|
+
<sp-tabs
|
|
4257
|
+
size="s"
|
|
4258
|
+
selected=${settingsTab}
|
|
4259
|
+
@change=${(/** @type {any} */ e) => {
|
|
4260
|
+
updateUi("settingsTab", e.target.selected);
|
|
4261
|
+
}}
|
|
4262
|
+
>
|
|
4263
|
+
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
4264
|
+
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
4265
|
+
<sp-tab label="Collections" value="collections"></sp-tab>
|
|
4266
|
+
</sp-tabs>
|
|
4267
|
+
</div>
|
|
4268
|
+
`;
|
|
4269
|
+
|
|
4270
|
+
// Non-stylebook tabs: render editor into canvasWrap with offset for chrome bar
|
|
4271
|
+
if (settingsTab === "definitions" || settingsTab === "collections") {
|
|
4272
|
+
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
4273
|
+
|
|
4274
|
+
litRender(
|
|
4275
|
+
html`${settingsChromeBarTpl}
|
|
4276
|
+
<div
|
|
4277
|
+
class="settings-editor-container"
|
|
4278
|
+
style="position:absolute;inset:40px 0 0 0;overflow:auto"
|
|
4279
|
+
></div>`,
|
|
4280
|
+
/** @type {any} */ (canvasWrap),
|
|
4281
|
+
);
|
|
4282
|
+
|
|
4283
|
+
const container = /** @type {HTMLElement} */ (
|
|
4284
|
+
canvasWrap.querySelector(".settings-editor-container")
|
|
4285
|
+
);
|
|
4286
|
+
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
4287
|
+
else renderCollectionsEditor(container);
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
// Stylebook tab — existing behavior
|
|
4292
|
+
view.stylebookElToTag = new WeakMap();
|
|
3975
4293
|
const rootStyle = getEffectiveStyle(S.document.style);
|
|
3976
4294
|
const filter = (S.ui.stylebookFilter || "").toLowerCase();
|
|
3977
4295
|
const customizedOnly = S.ui.stylebookCustomizedOnly;
|
|
@@ -3981,38 +4299,33 @@ function renderStylebook() {
|
|
|
3981
4299
|
|
|
3982
4300
|
// Chrome bar (tabs + filter) — positioned absolutely above the panzoom surface
|
|
3983
4301
|
const onTabClick = (/** @type {string} */ t) => {
|
|
3984
|
-
|
|
3985
|
-
renderCanvas();
|
|
3986
|
-
renderOverlays();
|
|
3987
|
-
renderLeftPanel();
|
|
4302
|
+
updateUi("stylebookTab", t);
|
|
3988
4303
|
};
|
|
3989
4304
|
|
|
3990
4305
|
const onFilterInput = (/** @type {any} */ e) => {
|
|
3991
|
-
|
|
3992
|
-
renderCanvas();
|
|
3993
|
-
renderOverlays();
|
|
4306
|
+
updateUi("stylebookFilter", e.target.value);
|
|
3994
4307
|
};
|
|
3995
4308
|
|
|
3996
4309
|
const onCustomizedToggle = () => {
|
|
3997
|
-
|
|
3998
|
-
renderCanvas();
|
|
3999
|
-
renderOverlays();
|
|
4310
|
+
updateUi("stylebookCustomizedOnly", !S.ui.stylebookCustomizedOnly);
|
|
4000
4311
|
};
|
|
4001
4312
|
|
|
4002
4313
|
const chromeBarTpl = html`
|
|
4314
|
+
${settingsChromeBarTpl}
|
|
4003
4315
|
<div
|
|
4004
4316
|
class="sb-chrome"
|
|
4005
|
-
style="position:absolute;top:
|
|
4317
|
+
style="position:absolute;top:36px;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
4006
4318
|
>
|
|
4007
|
-
<sp-tabs
|
|
4319
|
+
<sp-tabs
|
|
4320
|
+
size="s"
|
|
4321
|
+
selected=${S.ui.stylebookTab || "elements"}
|
|
4322
|
+
@change=${(/** @type {any} */ e) => {
|
|
4323
|
+
onTabClick(e.target.selected);
|
|
4324
|
+
}}
|
|
4325
|
+
>
|
|
4008
4326
|
${["elements", "variables"].map(
|
|
4009
4327
|
(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>
|
|
4328
|
+
<sp-tab label=${t.charAt(0).toUpperCase() + t.slice(1)} value=${t}></sp-tab>
|
|
4016
4329
|
`,
|
|
4017
4330
|
)}
|
|
4018
4331
|
</sp-tabs>
|
|
@@ -4058,7 +4371,6 @@ function renderStylebook() {
|
|
|
4058
4371
|
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
4059
4372
|
});
|
|
4060
4373
|
}
|
|
4061
|
-
allPanelDefs.sort((a, b) => b.width - a.width);
|
|
4062
4374
|
}
|
|
4063
4375
|
|
|
4064
4376
|
// Render content into panels
|
|
@@ -4110,9 +4422,9 @@ function renderStylebook() {
|
|
|
4110
4422
|
${chromeBarTpl}
|
|
4111
4423
|
<div
|
|
4112
4424
|
class="panzoom-wrap"
|
|
4113
|
-
style="transform-origin:0 0;padding-top:
|
|
4425
|
+
style="transform-origin:0 0;padding-top:72px"
|
|
4114
4426
|
${ref((el) => {
|
|
4115
|
-
if (el) panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
4427
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
4116
4428
|
})}
|
|
4117
4429
|
>
|
|
4118
4430
|
${panelEntries.map((e) => e.tpl)}
|
|
@@ -4172,12 +4484,12 @@ function renderStylebookElementsIntoCanvas(
|
|
|
4172
4484
|
class="element-card"
|
|
4173
4485
|
${ref((card) => {
|
|
4174
4486
|
if (!card) return;
|
|
4175
|
-
stylebookElToTag.set(card, entry.tag);
|
|
4487
|
+
view.stylebookElToTag.set(card, entry.tag);
|
|
4176
4488
|
elToPath.set(card, ["__sb", entry.tag]);
|
|
4177
4489
|
for (const child of el.querySelectorAll("*")) {
|
|
4178
4490
|
const tag = child.tagName.toLowerCase();
|
|
4179
|
-
if (!stylebookElToTag.has(child)) {
|
|
4180
|
-
stylebookElToTag.set(child, tag);
|
|
4491
|
+
if (!view.stylebookElToTag.has(child)) {
|
|
4492
|
+
view.stylebookElToTag.set(child, tag);
|
|
4181
4493
|
elToPath.set(child, ["__sb", tag]);
|
|
4182
4494
|
}
|
|
4183
4495
|
}
|
|
@@ -4219,7 +4531,7 @@ function renderStylebookElementsIntoCanvas(
|
|
|
4219
4531
|
style="display:inline-flex;width:auto"
|
|
4220
4532
|
${ref((card) => {
|
|
4221
4533
|
if (!card) return;
|
|
4222
|
-
stylebookElToTag.set(card, comp.tagName);
|
|
4534
|
+
view.stylebookElToTag.set(card, comp.tagName);
|
|
4223
4535
|
elToPath.set(card, ["__sb", comp.tagName]);
|
|
4224
4536
|
})}
|
|
4225
4537
|
>
|
|
@@ -4586,21 +4898,6 @@ function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
|
4586
4898
|
|
|
4587
4899
|
// varDisplayName, friendlyNameToVar — imported from studio-utils.js
|
|
4588
4900
|
|
|
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
4901
|
/**
|
|
4605
4902
|
* Convert a human-friendly name like "Tablet" to a $media key "--tablet"
|
|
4606
4903
|
*
|
|
@@ -4715,7 +5012,8 @@ function createUnitInput(initialValue, { onChange, size = "s" } = {}) {
|
|
|
4715
5012
|
}
|
|
4716
5013
|
|
|
4717
5014
|
/**
|
|
4718
|
-
* Click handler for stylebook canvas — selects elements via the elToPath/stylebookElToTag
|
|
5015
|
+
* Click handler for stylebook canvas — selects elements via the elToPath/view.stylebookElToTag
|
|
5016
|
+
* mapping
|
|
4719
5017
|
*
|
|
4720
5018
|
* @param {any} panel
|
|
4721
5019
|
*/
|
|
@@ -4736,7 +5034,7 @@ function registerStylebookPanelEvents(panel) {
|
|
|
4736
5034
|
if (!canvas.contains(el) || el === canvas) continue;
|
|
4737
5035
|
let cur = /** @type {any} */ (el);
|
|
4738
5036
|
while (cur && cur !== canvas) {
|
|
4739
|
-
const tag = stylebookElToTag.get(cur);
|
|
5037
|
+
const tag = view.stylebookElToTag.get(cur);
|
|
4740
5038
|
if (tag) {
|
|
4741
5039
|
const newMedia = panel.mediaName === "base" ? null : (panel.mediaName ?? null);
|
|
4742
5040
|
selectStylebookTag(tag, newMedia);
|
|
@@ -4747,9 +5045,8 @@ function registerStylebookPanelEvents(panel) {
|
|
|
4747
5045
|
}
|
|
4748
5046
|
}
|
|
4749
5047
|
// Clicked empty area — deselect
|
|
4750
|
-
|
|
5048
|
+
updateSession({ ui: { stylebookSelection: null, activeSelector: null } });
|
|
4751
5049
|
renderStylebookOverlays();
|
|
4752
|
-
renderRightPanel();
|
|
4753
5050
|
});
|
|
4754
5051
|
|
|
4755
5052
|
overlayClk.addEventListener("mousemove", (/** @type {any} */ e) => {
|
|
@@ -4765,7 +5062,7 @@ function registerStylebookPanelEvents(panel) {
|
|
|
4765
5062
|
if (!canvas.contains(el) || el === canvas) continue;
|
|
4766
5063
|
let cur = /** @type {any} */ (el);
|
|
4767
5064
|
while (cur && cur !== canvas) {
|
|
4768
|
-
const tag = stylebookElToTag.get(cur);
|
|
5065
|
+
const tag = view.stylebookElToTag.get(cur);
|
|
4769
5066
|
if (tag) {
|
|
4770
5067
|
hoverTag = tag;
|
|
4771
5068
|
break;
|
|
@@ -4835,7 +5132,7 @@ function renderStylebookOverlays() {
|
|
|
4835
5132
|
/** Find a stylebook element by tag in the canvas */
|
|
4836
5133
|
function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
|
|
4837
5134
|
for (const child of canvasEl.querySelectorAll("*")) {
|
|
4838
|
-
if (stylebookElToTag.get(child) === tag) return child;
|
|
5135
|
+
if (view.stylebookElToTag.get(child) === tag) return child;
|
|
4839
5136
|
}
|
|
4840
5137
|
return null;
|
|
4841
5138
|
}
|
|
@@ -4843,73 +5140,191 @@ function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
|
|
|
4843
5140
|
// ─── Right panel: Inspector ───────────────────────────────────────────────────
|
|
4844
5141
|
|
|
4845
5142
|
function renderRightPanel() {
|
|
4846
|
-
|
|
5143
|
+
rightPanelMod.render();
|
|
5144
|
+
}
|
|
4847
5145
|
|
|
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
|
-
];
|
|
5146
|
+
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
4854
5147
|
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
5148
|
+
/** Frontmatter-only panel shown in content mode when no element is selected */
|
|
5149
|
+
function renderFrontmatterOnlyPanel() {
|
|
5150
|
+
const fm = S.content?.frontmatter || {};
|
|
5151
|
+
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
5152
|
+
const schemaProps = col?.schema?.properties;
|
|
5153
|
+
const requiredFields = new Set(col?.schema?.required || []);
|
|
5154
|
+
|
|
5155
|
+
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
5156
|
+
const fields = [];
|
|
5157
|
+
if (schemaProps) {
|
|
5158
|
+
for (const [field, fieldSchema] of Object.entries(
|
|
5159
|
+
/** @type {Record<string, any>} */ (schemaProps),
|
|
5160
|
+
)) {
|
|
5161
|
+
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
5162
|
+
}
|
|
5163
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5164
|
+
if (!schemaProps[field]) {
|
|
5165
|
+
fields.push({
|
|
5166
|
+
field,
|
|
5167
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5168
|
+
value,
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
} else {
|
|
5173
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5174
|
+
fields.push({
|
|
5175
|
+
field,
|
|
5176
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5177
|
+
value,
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
if (fields.length === 0 && !schemaProps) {
|
|
5183
|
+
return html`<div class="empty-state">No frontmatter. Select an element to inspect.</div>`;
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
return html`
|
|
5187
|
+
<div class="style-sidebar">
|
|
5188
|
+
<sp-accordion allow-multiple size="s">
|
|
5189
|
+
<sp-accordion-item label=${col ? `Frontmatter (${col.name})` : "Frontmatter"} open>
|
|
5190
|
+
<div class="style-section-body">
|
|
5191
|
+
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
5192
|
+
</div>
|
|
5193
|
+
</sp-accordion-item>
|
|
5194
|
+
</sp-accordion>
|
|
4877
5195
|
</div>
|
|
4878
5196
|
`;
|
|
5197
|
+
}
|
|
4879
5198
|
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
5199
|
+
/** Render a single frontmatter field row (shared between both panels) */
|
|
5200
|
+
function renderFmFieldRow(
|
|
5201
|
+
/** @type {string} */ field,
|
|
5202
|
+
/** @type {any} */ entry,
|
|
5203
|
+
/** @type {any} */ value,
|
|
5204
|
+
/** @type {Set<string>} */ requiredFields,
|
|
5205
|
+
) {
|
|
5206
|
+
const isRequired = requiredFields.has(field);
|
|
5207
|
+
const label = field.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
5208
|
+
const displayLabel = label + (isRequired ? " *" : "");
|
|
5209
|
+
const hasVal = value !== undefined && value !== "" && value !== false;
|
|
5210
|
+
const onClear = () => update(updateFrontmatter(S, field, undefined));
|
|
5211
|
+
|
|
5212
|
+
// Boolean → checkbox
|
|
5213
|
+
if (entry.type === "boolean") {
|
|
5214
|
+
return renderFieldRow({
|
|
5215
|
+
prop: field,
|
|
5216
|
+
label: displayLabel,
|
|
5217
|
+
hasValue: hasVal,
|
|
5218
|
+
onClear,
|
|
5219
|
+
widget: html`
|
|
5220
|
+
<sp-checkbox
|
|
5221
|
+
size="s"
|
|
5222
|
+
.checked=${live(!!value)}
|
|
5223
|
+
@change=${(/** @type {any} */ e) =>
|
|
5224
|
+
update(updateFrontmatter(S, field, e.target.checked || undefined))}
|
|
5225
|
+
></sp-checkbox>
|
|
5226
|
+
`,
|
|
4889
5227
|
});
|
|
4890
|
-
} else if (tab === "style") {
|
|
4891
|
-
try {
|
|
4892
|
-
bodyT = renderStylePanelTemplate();
|
|
4893
|
-
} catch (/** @type {any} */ e) {
|
|
4894
|
-
console.error("[renderStylePanelTemplate]", e);
|
|
4895
|
-
}
|
|
4896
5228
|
}
|
|
4897
5229
|
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5230
|
+
// Array of strings → comma-separated text
|
|
5231
|
+
if (entry.type === "array") {
|
|
5232
|
+
const display = Array.isArray(value) ? value.join(", ") : value || "";
|
|
5233
|
+
return renderFieldRow({
|
|
5234
|
+
prop: field,
|
|
5235
|
+
label: displayLabel,
|
|
5236
|
+
hasValue: hasVal,
|
|
5237
|
+
onClear,
|
|
5238
|
+
widget: html`
|
|
5239
|
+
<sp-textfield
|
|
5240
|
+
size="s"
|
|
5241
|
+
placeholder="comma, separated"
|
|
5242
|
+
.value=${live(display)}
|
|
5243
|
+
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5244
|
+
const arr = e.target.value
|
|
5245
|
+
? e.target.value
|
|
5246
|
+
.split(",")
|
|
5247
|
+
.map((/** @type {string} */ s) => s.trim())
|
|
5248
|
+
.filter(Boolean)
|
|
5249
|
+
: undefined;
|
|
5250
|
+
update(updateFrontmatter(S, field, arr));
|
|
5251
|
+
})}
|
|
5252
|
+
></sp-textfield>
|
|
5253
|
+
`,
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
4902
5256
|
|
|
4903
|
-
|
|
5257
|
+
// Enum → select
|
|
5258
|
+
if (Array.isArray(entry.enum)) {
|
|
5259
|
+
return renderFieldRow({
|
|
5260
|
+
prop: field,
|
|
5261
|
+
label: displayLabel,
|
|
5262
|
+
hasValue: hasVal,
|
|
5263
|
+
onClear,
|
|
5264
|
+
widget: html`
|
|
5265
|
+
<sp-picker
|
|
5266
|
+
size="s"
|
|
5267
|
+
.value=${live(value || "")}
|
|
5268
|
+
@change=${(/** @type {any} */ e) =>
|
|
5269
|
+
update(updateFrontmatter(S, field, e.target.value || undefined))}
|
|
5270
|
+
>
|
|
5271
|
+
${entry.enum.map(
|
|
5272
|
+
(/** @type {string} */ opt) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`,
|
|
5273
|
+
)}
|
|
5274
|
+
</sp-picker>
|
|
5275
|
+
`,
|
|
5276
|
+
});
|
|
5277
|
+
}
|
|
4904
5278
|
|
|
4905
|
-
|
|
4906
|
-
|
|
5279
|
+
// Number
|
|
5280
|
+
if (entry.type === "number") {
|
|
5281
|
+
return renderFieldRow({
|
|
5282
|
+
prop: field,
|
|
5283
|
+
label: displayLabel,
|
|
5284
|
+
hasValue: hasVal,
|
|
5285
|
+
onClear,
|
|
5286
|
+
widget: html`
|
|
5287
|
+
<sp-number-field
|
|
5288
|
+
size="s"
|
|
5289
|
+
hide-stepper
|
|
5290
|
+
.value=${live(value !== undefined ? Number(value) : undefined)}
|
|
5291
|
+
@change=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5292
|
+
const v = e.target.value;
|
|
5293
|
+
update(updateFrontmatter(S, field, isNaN(v) ? undefined : Number(v)));
|
|
5294
|
+
})}
|
|
5295
|
+
></sp-number-field>
|
|
5296
|
+
`,
|
|
5297
|
+
});
|
|
5298
|
+
}
|
|
4907
5299
|
|
|
4908
|
-
//
|
|
5300
|
+
// Default: text (handles string, date, etc.)
|
|
5301
|
+
return renderFieldRow({
|
|
5302
|
+
prop: field,
|
|
5303
|
+
label: displayLabel,
|
|
5304
|
+
hasValue: hasVal,
|
|
5305
|
+
onClear,
|
|
5306
|
+
widget: html`
|
|
5307
|
+
<sp-textfield
|
|
5308
|
+
size="s"
|
|
5309
|
+
placeholder=${entry.format === "date" ? "YYYY-MM-DD" : ""}
|
|
5310
|
+
.value=${live(value || "")}
|
|
5311
|
+
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
5312
|
+
update(updateFrontmatter(S, field, e.target.value || undefined));
|
|
5313
|
+
})}
|
|
5314
|
+
></sp-textfield>
|
|
5315
|
+
`,
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
4909
5318
|
|
|
4910
5319
|
/** Properties panel — lit-html template with accordion sections */
|
|
4911
5320
|
function propertiesSidebarTemplate() {
|
|
4912
|
-
|
|
5321
|
+
// In content mode with no selection, still show frontmatter fields
|
|
5322
|
+
if (!S.selection) {
|
|
5323
|
+
if (S.mode === "content") {
|
|
5324
|
+
return renderFrontmatterOnlyPanel();
|
|
5325
|
+
}
|
|
5326
|
+
return html`<div class="empty-state">Select an element to inspect</div>`;
|
|
5327
|
+
}
|
|
4913
5328
|
const node = getNodeAtPath(S.document, S.selection);
|
|
4914
5329
|
if (!node) return html`<div class="empty-state">Node not found</div>`;
|
|
4915
5330
|
|
|
@@ -4941,21 +5356,12 @@ function propertiesSidebarTemplate() {
|
|
|
4941
5356
|
|
|
4942
5357
|
// Boolean attributes render as checkboxes
|
|
4943
5358
|
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>
|
|
5359
|
+
return renderFieldRow({
|
|
5360
|
+
prop: attr,
|
|
5361
|
+
label: attrLabel(entry, attr),
|
|
5362
|
+
hasValue: hasVal,
|
|
5363
|
+
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
5364
|
+
widget: html`
|
|
4959
5365
|
<sp-checkbox
|
|
4960
5366
|
size="s"
|
|
4961
5367
|
.checked=${live(!!value)}
|
|
@@ -4963,30 +5369,19 @@ function propertiesSidebarTemplate() {
|
|
|
4963
5369
|
update(updateAttribute(S, path, attr, e.target.checked || undefined))}
|
|
4964
5370
|
>
|
|
4965
5371
|
</sp-checkbox>
|
|
4966
|
-
|
|
4967
|
-
|
|
5372
|
+
`,
|
|
5373
|
+
});
|
|
4968
5374
|
}
|
|
4969
5375
|
|
|
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
|
-
`;
|
|
5376
|
+
return renderFieldRow({
|
|
5377
|
+
prop: attr,
|
|
5378
|
+
label: attrLabel(entry, attr),
|
|
5379
|
+
hasValue: hasVal,
|
|
5380
|
+
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
5381
|
+
widget: widgetForType(type, entry, attr, value || "", (/** @type {any} */ v) =>
|
|
5382
|
+
update(updateAttribute(S, path, attr, v || undefined)),
|
|
5383
|
+
),
|
|
5384
|
+
});
|
|
4990
5385
|
}
|
|
4991
5386
|
|
|
4992
5387
|
// ── Collect applicable attributes from html-meta ──
|
|
@@ -5035,11 +5430,7 @@ function propertiesSidebarTemplate() {
|
|
|
5035
5430
|
|
|
5036
5431
|
function toggleSection(/** @type {any} */ key) {
|
|
5037
5432
|
const current = isSectionOpen(key);
|
|
5038
|
-
S
|
|
5039
|
-
...S,
|
|
5040
|
-
ui: { ...S.ui, inspectorSections: { ...S.ui.inspectorSections, [key]: !current } },
|
|
5041
|
-
};
|
|
5042
|
-
renderRightPanel();
|
|
5433
|
+
updateUi("inspectorSections", { ...S.ui.inspectorSections, [key]: !current });
|
|
5043
5434
|
}
|
|
5044
5435
|
|
|
5045
5436
|
// ── Build section templates ─────────────────────────────────────────
|
|
@@ -5364,11 +5755,63 @@ function propertiesSidebarTemplate() {
|
|
|
5364
5755
|
})()
|
|
5365
5756
|
: nothing;
|
|
5366
5757
|
|
|
5758
|
+
// ── Frontmatter section (content mode only) ──
|
|
5759
|
+
const frontmatterT =
|
|
5760
|
+
S.mode === "content"
|
|
5761
|
+
? (() => {
|
|
5762
|
+
const fm = S.content?.frontmatter || {};
|
|
5763
|
+
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
5764
|
+
const schemaProps = col?.schema?.properties;
|
|
5765
|
+
const requiredFields = new Set(col?.schema?.required || []);
|
|
5766
|
+
|
|
5767
|
+
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
5768
|
+
const fields = [];
|
|
5769
|
+
if (schemaProps) {
|
|
5770
|
+
for (const [field, fieldSchema] of Object.entries(
|
|
5771
|
+
/** @type {Record<string, any>} */ (schemaProps),
|
|
5772
|
+
)) {
|
|
5773
|
+
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
5774
|
+
}
|
|
5775
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5776
|
+
if (!schemaProps[field]) {
|
|
5777
|
+
fields.push({
|
|
5778
|
+
field,
|
|
5779
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5780
|
+
value,
|
|
5781
|
+
});
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
} else {
|
|
5785
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
5786
|
+
fields.push({
|
|
5787
|
+
field,
|
|
5788
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5789
|
+
value,
|
|
5790
|
+
});
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
if (fields.length === 0 && !schemaProps) return nothing;
|
|
5795
|
+
|
|
5796
|
+
return html`
|
|
5797
|
+
<sp-accordion-item
|
|
5798
|
+
label=${col ? `Frontmatter (${col.name})` : "Frontmatter"}
|
|
5799
|
+
?open=${isSectionOpen("__frontmatter") !== false}
|
|
5800
|
+
@sp-accordion-item-toggle=${() => toggleSection("__frontmatter")}
|
|
5801
|
+
>
|
|
5802
|
+
<div class="style-section-body">
|
|
5803
|
+
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
5804
|
+
</div>
|
|
5805
|
+
</sp-accordion-item>
|
|
5806
|
+
`;
|
|
5807
|
+
})()
|
|
5808
|
+
: nothing;
|
|
5809
|
+
|
|
5367
5810
|
// ── Assemble ──
|
|
5368
5811
|
const tpl = html`
|
|
5369
5812
|
<div class="style-sidebar">
|
|
5370
5813
|
<sp-accordion allow-multiple size="s">
|
|
5371
|
-
${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
|
|
5814
|
+
${frontmatterT} ${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
|
|
5372
5815
|
${isMapNode ? nothing : switchT} ${isMapNode ? nothing : compPropsT}
|
|
5373
5816
|
${isMapNode ? nothing : attrSectionTemplates} ${isMapNode ? nothing : customSectionT}
|
|
5374
5817
|
${isMapNode ? nothing : mediaT} ${isMapNode ? nothing : cssPropsT}
|
|
@@ -5603,13 +6046,13 @@ function renderComponentPropsFieldsTemplate(
|
|
|
5603
6046
|
></sp-number-field>`;
|
|
5604
6047
|
} else if (parsed.kind === "combobox") {
|
|
5605
6048
|
const options = /** @type {string[]} */ (/** @type {any} */ (parsed).options);
|
|
5606
|
-
widgetTpl = html`<jx-
|
|
6049
|
+
widgetTpl = html`<jx-value-selector
|
|
5607
6050
|
.value=${String(staticVal)}
|
|
5608
6051
|
size="s"
|
|
5609
6052
|
placeholder="—"
|
|
5610
6053
|
.options=${options.map((o) => ({ value: o, label: camelToLabel(o) }))}
|
|
5611
6054
|
@change=${(/** @type {any} */ e) => onChange(e.detail?.value ?? e.target.value)}
|
|
5612
|
-
></jx-
|
|
6055
|
+
></jx-value-selector>`;
|
|
5613
6056
|
} else {
|
|
5614
6057
|
widgetTpl = html`<sp-textfield
|
|
5615
6058
|
size="s"
|
|
@@ -5683,8 +6126,6 @@ function renderCustomAttrsFieldsTemplate(
|
|
|
5683
6126
|
}
|
|
5684
6127
|
|
|
5685
6128
|
/** Media breakpoint fields template */
|
|
5686
|
-
let showAddBreakpointForm = false;
|
|
5687
|
-
let addBreakpointPreview = "";
|
|
5688
6129
|
|
|
5689
6130
|
function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
5690
6131
|
const media = node.$media || {};
|
|
@@ -5720,14 +6161,14 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
|
5720
6161
|
<div>
|
|
5721
6162
|
<span
|
|
5722
6163
|
class="kv-add"
|
|
5723
|
-
style=${showAddBreakpointForm ? "display:none" : ""}
|
|
6164
|
+
style=${view.showAddBreakpointForm ? "display:none" : ""}
|
|
5724
6165
|
@click=${(/** @type {any} */ _e) => {
|
|
5725
|
-
showAddBreakpointForm = true;
|
|
6166
|
+
view.showAddBreakpointForm = true;
|
|
5726
6167
|
renderRightPanel();
|
|
5727
6168
|
}}
|
|
5728
6169
|
>+ Add breakpoint</span
|
|
5729
6170
|
>
|
|
5730
|
-
${showAddBreakpointForm
|
|
6171
|
+
${view.showAddBreakpointForm
|
|
5731
6172
|
? html`
|
|
5732
6173
|
<div style="margin-top:4px">
|
|
5733
6174
|
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
@@ -5736,13 +6177,13 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
|
5736
6177
|
placeholder="Name (e.g. Tablet)"
|
|
5737
6178
|
style="flex:1"
|
|
5738
6179
|
@input=${(/** @type {any} */ e) => {
|
|
5739
|
-
addBreakpointPreview = friendlyNameToMedia(e.target.value) || "";
|
|
6180
|
+
view.addBreakpointPreview = friendlyNameToMedia(e.target.value) || "";
|
|
5740
6181
|
renderRightPanel();
|
|
5741
6182
|
}}
|
|
5742
6183
|
/>
|
|
5743
6184
|
<span
|
|
5744
6185
|
style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
|
|
5745
|
-
>${addBreakpointPreview}</span
|
|
6186
|
+
>${view.addBreakpointPreview}</span
|
|
5746
6187
|
>
|
|
5747
6188
|
</div>
|
|
5748
6189
|
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
@@ -5758,8 +6199,8 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
|
5758
6199
|
const queryVal = wrap.querySelector(".add-bp-query")?.value?.trim();
|
|
5759
6200
|
const key = friendlyNameToMedia(nameVal);
|
|
5760
6201
|
if (key && queryVal) {
|
|
5761
|
-
showAddBreakpointForm = false;
|
|
5762
|
-
addBreakpointPreview = "";
|
|
6202
|
+
view.showAddBreakpointForm = false;
|
|
6203
|
+
view.addBreakpointPreview = "";
|
|
5763
6204
|
update(updateMedia(S, key, queryVal));
|
|
5764
6205
|
}
|
|
5765
6206
|
}}
|
|
@@ -5770,8 +6211,8 @@ function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
|
5770
6211
|
class="kv-add"
|
|
5771
6212
|
style="padding:2px 10px;cursor:pointer;color:var(--fg-dim)"
|
|
5772
6213
|
@click=${() => {
|
|
5773
|
-
showAddBreakpointForm = false;
|
|
5774
|
-
addBreakpointPreview = "";
|
|
6214
|
+
view.showAddBreakpointForm = false;
|
|
6215
|
+
view.addBreakpointPreview = "";
|
|
5775
6216
|
renderRightPanel();
|
|
5776
6217
|
}}
|
|
5777
6218
|
>
|
|
@@ -5839,7 +6280,7 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
|
|
|
5839
6280
|
|
|
5840
6281
|
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
5841
6282
|
|
|
5842
|
-
|
|
6283
|
+
// UNIT_RE — imported from ui/unit-selector.js
|
|
5843
6284
|
|
|
5844
6285
|
// inferInputType — imported from studio-utils.js
|
|
5845
6286
|
|
|
@@ -5867,32 +6308,118 @@ function autoOpenSections(/** @type {any} */ node, /** @type {any} */ currentSec
|
|
|
5867
6308
|
|
|
5868
6309
|
/** Get longhands for a shorthand property from css-meta */
|
|
5869
6310
|
function getLonghands(/** @type {any} */ shorthandProp) {
|
|
6311
|
+
// Check for explicit $longhands array first (used by border-side shorthands)
|
|
6312
|
+
const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[shorthandProp];
|
|
6313
|
+
if (entry?.$longhands) {
|
|
6314
|
+
return entry.$longhands
|
|
6315
|
+
.map((/** @type {string} */ name) => ({
|
|
6316
|
+
name,
|
|
6317
|
+
entry: /** @type {Record<string, any>} */ (cssMeta.$defs)[name] || { $order: 0 },
|
|
6318
|
+
}))
|
|
6319
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order);
|
|
6320
|
+
}
|
|
6321
|
+
// Fallback: reverse-lookup by $shorthand reference
|
|
5870
6322
|
const result = [];
|
|
5871
|
-
for (const [name,
|
|
5872
|
-
if (
|
|
6323
|
+
for (const [name, e] of /** @type {[string, any][]} */ (Object.entries(cssMeta.$defs))) {
|
|
6324
|
+
if (e.$shorthand === shorthandProp) result.push({ name, entry: e });
|
|
5873
6325
|
}
|
|
5874
6326
|
result.sort((a, b) => a.entry.$order - b.entry.$order);
|
|
5875
6327
|
return result;
|
|
5876
6328
|
}
|
|
5877
6329
|
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
6330
|
+
/**
|
|
6331
|
+
* Expand a CSS shorthand value (margin, padding, borderWidth, borderRadius) into individual
|
|
6332
|
+
* longhand values following the standard 1–4 value TRBL pattern. Returns an array matching the
|
|
6333
|
+
* longhand count (always 4 for box properties).
|
|
6334
|
+
*/
|
|
6335
|
+
function expandShorthand(/** @type {string} */ shortVal, /** @type {number} */ count) {
|
|
6336
|
+
if (!shortVal) return Array(count).fill("");
|
|
6337
|
+
const parts = shortVal.trim().split(/\s+/);
|
|
6338
|
+
if (count !== 4 || parts.length === 0) return Array(count).fill("");
|
|
6339
|
+
if (parts.length === 1) return [parts[0], parts[0], parts[0], parts[0]];
|
|
6340
|
+
if (parts.length === 2) return [parts[0], parts[1], parts[0], parts[1]];
|
|
6341
|
+
if (parts.length === 3) return [parts[0], parts[1], parts[2], parts[1]];
|
|
6342
|
+
return [parts[0], parts[1], parts[2], parts[3]];
|
|
6343
|
+
}
|
|
5884
6344
|
|
|
5885
|
-
/**
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
6345
|
+
/**
|
|
6346
|
+
* Compress 4 TRBL values back into the shortest valid CSS shorthand string. e.g.
|
|
6347
|
+
* ["0","auto","3rem","auto"] → "0 auto 3rem"
|
|
6348
|
+
*/
|
|
6349
|
+
function compressShorthand(/** @type {string[]} */ vals) {
|
|
6350
|
+
const [t, r, b, l] = vals;
|
|
6351
|
+
if (t === r && r === b && b === l) return t;
|
|
6352
|
+
if (t === b && r === l) return `${t} ${r}`;
|
|
6353
|
+
if (r === l) return `${t} ${r} ${b}`;
|
|
6354
|
+
return `${t} ${r} ${b} ${l}`;
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
// ─── Border-side shorthand parsing ────────────────────────────────────────────
|
|
6358
|
+
// CSS border-side shorthand: <width> || <style> || <color> (any order, all optional)
|
|
6359
|
+
|
|
6360
|
+
const BORDER_STYLES = new Set([
|
|
6361
|
+
"none",
|
|
6362
|
+
"solid",
|
|
6363
|
+
"dashed",
|
|
6364
|
+
"dotted",
|
|
6365
|
+
"double",
|
|
6366
|
+
"groove",
|
|
6367
|
+
"ridge",
|
|
6368
|
+
"inset",
|
|
6369
|
+
"outset",
|
|
6370
|
+
"hidden",
|
|
6371
|
+
]);
|
|
6372
|
+
|
|
6373
|
+
/**
|
|
6374
|
+
* Parse a border-side shorthand value into [width, style, color].
|
|
6375
|
+
*
|
|
6376
|
+
* @param {string} value — e.g. "1px solid var(--color-border)"
|
|
6377
|
+
* @returns {string[]} — [width, style, color]
|
|
6378
|
+
*/
|
|
6379
|
+
function expandBorderSide(value) {
|
|
6380
|
+
if (!value) return ["", "", ""];
|
|
6381
|
+
// Tokenize respecting parenthesized values like var(...) and rgb(...)
|
|
6382
|
+
const tokens = [];
|
|
6383
|
+
let current = "";
|
|
6384
|
+
let depth = 0;
|
|
6385
|
+
for (const ch of value.trim()) {
|
|
6386
|
+
if (ch === "(") depth++;
|
|
6387
|
+
if (ch === ")") depth--;
|
|
6388
|
+
if (ch === " " && depth === 0) {
|
|
6389
|
+
if (current) tokens.push(current);
|
|
6390
|
+
current = "";
|
|
6391
|
+
} else {
|
|
6392
|
+
current += ch;
|
|
5893
6393
|
}
|
|
5894
6394
|
}
|
|
5895
|
-
|
|
6395
|
+
if (current) tokens.push(current);
|
|
6396
|
+
|
|
6397
|
+
let width = "";
|
|
6398
|
+
let style = "";
|
|
6399
|
+
let color = "";
|
|
6400
|
+
|
|
6401
|
+
for (const tok of tokens) {
|
|
6402
|
+
if (!style && BORDER_STYLES.has(tok)) {
|
|
6403
|
+
style = tok;
|
|
6404
|
+
} else if (!width && /^[\d.]/.test(tok)) {
|
|
6405
|
+
width = tok;
|
|
6406
|
+
} else {
|
|
6407
|
+
// Remaining token(s) are color — join in case color was split (shouldn't be with paren-aware tokenizer)
|
|
6408
|
+
color = color ? `${color} ${tok}` : tok;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
|
|
6412
|
+
return [width, style, color];
|
|
6413
|
+
}
|
|
6414
|
+
|
|
6415
|
+
/**
|
|
6416
|
+
* Recompose border-side longhand values into a shorthand string.
|
|
6417
|
+
*
|
|
6418
|
+
* @param {string[]} vals — [width, style, color]
|
|
6419
|
+
* @returns {string}
|
|
6420
|
+
*/
|
|
6421
|
+
function compressBorderSide(/** @type {string[]} */ vals) {
|
|
6422
|
+
return vals.filter((v) => v && v.trim()).join(" ");
|
|
5896
6423
|
}
|
|
5897
6424
|
|
|
5898
6425
|
/** Extract --font-* CSS custom properties from the document root style. */
|
|
@@ -5908,371 +6435,6 @@ function getFontVars() {
|
|
|
5908
6435
|
return vars;
|
|
5909
6436
|
}
|
|
5910
6437
|
|
|
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
6438
|
/** Typography CSS properties that should preview their values in-menu */
|
|
6277
6439
|
const TYPO_PREVIEW_PROPS = new Set(["fontStyle", "fontVariant", "textTransform", "textDecoration"]);
|
|
6278
6440
|
|
|
@@ -6316,7 +6478,7 @@ function renderKeywordInput(options, prop, value, onChange) {
|
|
|
6316
6478
|
return { value: v, label, style };
|
|
6317
6479
|
});
|
|
6318
6480
|
|
|
6319
|
-
return html`<jx-
|
|
6481
|
+
return html`<jx-value-selector
|
|
6320
6482
|
size="s"
|
|
6321
6483
|
.value=${value || ""}
|
|
6322
6484
|
placeholder=${cssInitialMap.get(prop) || ""}
|
|
@@ -6325,7 +6487,7 @@ function renderKeywordInput(options, prop, value, onChange) {
|
|
|
6325
6487
|
@input=${debouncedStyleCommit(`kw:${prop}`, 400, (/** @type {any} */ e) =>
|
|
6326
6488
|
onChange(e.target.value),
|
|
6327
6489
|
)}
|
|
6328
|
-
></jx-
|
|
6490
|
+
></jx-value-selector>`;
|
|
6329
6491
|
}
|
|
6330
6492
|
|
|
6331
6493
|
function renderSelectInput(
|
|
@@ -6382,7 +6544,7 @@ function handleFontSelection(
|
|
|
6382
6544
|
}
|
|
6383
6545
|
|
|
6384
6546
|
/**
|
|
6385
|
-
* Build font options array for jx-
|
|
6547
|
+
* Build font options array for jx-value-selector. Local font vars first, divider, then unadded
|
|
6386
6548
|
* presets.
|
|
6387
6549
|
*
|
|
6388
6550
|
* @param {any[]} fontVars @param {any[]} presets
|
|
@@ -6420,13 +6582,13 @@ function renderComboboxInput(
|
|
|
6420
6582
|
const presets = entry.presets || [];
|
|
6421
6583
|
const examples = entry.examples || [];
|
|
6422
6584
|
|
|
6423
|
-
// fontFamily: single jx-
|
|
6585
|
+
// fontFamily: single jx-value-selector with font options
|
|
6424
6586
|
if (prop === "fontFamily") {
|
|
6425
6587
|
// Strip var() wrapper so the component can match the option value
|
|
6426
6588
|
const varMatch = typeof value === "string" && value.match(/^var\((--[^)]+)\)$/);
|
|
6427
6589
|
const comboValue = varMatch ? varMatch[1] : value || "";
|
|
6428
6590
|
const fontOptions = buildFontOptions(fontVars, presets);
|
|
6429
|
-
return html`<jx-
|
|
6591
|
+
return html`<jx-value-selector
|
|
6430
6592
|
size="s"
|
|
6431
6593
|
.value=${comboValue}
|
|
6432
6594
|
placeholder=${cssInitialMap.get("fontFamily") || ""}
|
|
@@ -6435,7 +6597,7 @@ function renderComboboxInput(
|
|
|
6435
6597
|
@input=${debouncedStyleCommit("combo:fontFamily", 400, (/** @type {any} */ e) =>
|
|
6436
6598
|
onChange(e.target.value),
|
|
6437
6599
|
)}
|
|
6438
|
-
></jx-
|
|
6600
|
+
></jx-value-selector>`;
|
|
6439
6601
|
}
|
|
6440
6602
|
|
|
6441
6603
|
// All other comboboxes: use the shared keyword dual-mode input
|
|
@@ -6456,45 +6618,7 @@ function renderComboboxInput(
|
|
|
6456
6618
|
`;
|
|
6457
6619
|
}
|
|
6458
6620
|
|
|
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
|
-
}
|
|
6621
|
+
// renderNumberInput, renderTextInput — imported from ui/widgets.js
|
|
6498
6622
|
|
|
6499
6623
|
// camelToLabel, kebabToLabel, propLabel, attrLabel — imported from studio-utils.js
|
|
6500
6624
|
|
|
@@ -6504,23 +6628,13 @@ function widgetForType(
|
|
|
6504
6628
|
/** @type {any} */ prop,
|
|
6505
6629
|
/** @type {any} */ value,
|
|
6506
6630
|
/** @type {any} */ onCommit,
|
|
6631
|
+
/** @type {any} */ opts = {},
|
|
6507
6632
|
) {
|
|
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
|
-
}
|
|
6633
|
+
return _widgetForType(type, entry, prop, value, onCommit, {
|
|
6634
|
+
placeholder: opts.placeholder || cssInitialMap.get(prop) || "",
|
|
6635
|
+
renderSelect: renderSelectInput,
|
|
6636
|
+
renderCombobox: renderComboboxInput,
|
|
6637
|
+
});
|
|
6524
6638
|
}
|
|
6525
6639
|
|
|
6526
6640
|
function renderStyleRow(
|
|
@@ -6531,31 +6645,20 @@ function renderStyleRow(
|
|
|
6531
6645
|
/** @type {any} */ onDelete,
|
|
6532
6646
|
/** @type {any} */ isWarning,
|
|
6533
6647
|
/** @type {any} */ gridMode,
|
|
6648
|
+
/** @type {any} */ inheritedValue,
|
|
6534
6649
|
) {
|
|
6535
6650
|
const type = inferInputType(entry);
|
|
6536
6651
|
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
|
-
`;
|
|
6652
|
+
const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
|
|
6653
|
+
return renderFieldRow({
|
|
6654
|
+
prop,
|
|
6655
|
+
label: propLabel(entry, prop),
|
|
6656
|
+
hasValue: hasVal,
|
|
6657
|
+
onClear: onDelete,
|
|
6658
|
+
widget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
|
|
6659
|
+
span: gridMode && entry.$span === 2 ? 2 : undefined,
|
|
6660
|
+
warning: isWarning,
|
|
6661
|
+
});
|
|
6559
6662
|
}
|
|
6560
6663
|
|
|
6561
6664
|
function renderShorthandRow(
|
|
@@ -6564,12 +6667,14 @@ function renderShorthandRow(
|
|
|
6564
6667
|
/** @type {any} */ style,
|
|
6565
6668
|
/** @type {any} */ commitFn,
|
|
6566
6669
|
/** @type {any} */ _deleteFn,
|
|
6670
|
+
/** @type {Record<string, any>} */ inherited = {},
|
|
6567
6671
|
) {
|
|
6568
6672
|
const longhands = getLonghands(shortProp);
|
|
6569
6673
|
const shortVal = style[shortProp];
|
|
6570
|
-
const hasLonghands = longhands.some((l) => style[l.name] !== undefined);
|
|
6674
|
+
const hasLonghands = longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6571
6675
|
const isExpanded = S.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
6572
|
-
const hasAnyVal =
|
|
6676
|
+
const hasAnyVal =
|
|
6677
|
+
shortVal !== undefined || longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6573
6678
|
|
|
6574
6679
|
return html`
|
|
6575
6680
|
<div class="style-row" data-prop=${shortProp}>
|
|
@@ -6596,8 +6701,12 @@ function renderShorthandRow(
|
|
|
6596
6701
|
size="s"
|
|
6597
6702
|
.value=${live(shortVal || "")}
|
|
6598
6703
|
placeholder=${!shortVal && hasLonghands
|
|
6599
|
-
? longhands.map((l) => style[l.name] || "0").join(" ")
|
|
6600
|
-
:
|
|
6704
|
+
? longhands.map((/** @type {any} */ l) => style[l.name] || "0").join(" ")
|
|
6705
|
+
: !shortVal && inherited[shortProp]
|
|
6706
|
+
? inherited[shortProp]
|
|
6707
|
+
: !shortVal && longhands.some((/** @type {any} */ l) => inherited[l.name])
|
|
6708
|
+
? longhands.map((/** @type {any} */ l) => inherited[l.name] || "0").join(" ")
|
|
6709
|
+
: ""}
|
|
6601
6710
|
@input=${debouncedStyleCommit(`short:${shortProp}`, 400, (/** @type {any} */ e) => {
|
|
6602
6711
|
let s = S;
|
|
6603
6712
|
for (const l of longhands) {
|
|
@@ -6612,14 +6721,7 @@ function renderShorthandRow(
|
|
|
6612
6721
|
quiet
|
|
6613
6722
|
@click=${(/** @type {any} */ e) => {
|
|
6614
6723
|
e.stopPropagation();
|
|
6615
|
-
S
|
|
6616
|
-
...S,
|
|
6617
|
-
ui: {
|
|
6618
|
-
...S.ui,
|
|
6619
|
-
styleShorthands: { ...S.ui.styleShorthands, [shortProp]: !isExpanded },
|
|
6620
|
-
},
|
|
6621
|
-
};
|
|
6622
|
-
renderRightPanel();
|
|
6724
|
+
updateUi("styleShorthands", { ...S.ui.styleShorthands, [shortProp]: !isExpanded });
|
|
6623
6725
|
}}
|
|
6624
6726
|
>
|
|
6625
6727
|
${isExpanded
|
|
@@ -6629,33 +6731,74 @@ function renderShorthandRow(
|
|
|
6629
6731
|
</div>
|
|
6630
6732
|
</div>
|
|
6631
6733
|
${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
|
-
|
|
6734
|
+
? (() => {
|
|
6735
|
+
const isBorderSide = entry.$shorthandType === "border-side";
|
|
6736
|
+
const expanded = shortVal
|
|
6737
|
+
? isBorderSide
|
|
6738
|
+
? expandBorderSide(shortVal)
|
|
6739
|
+
: expandShorthand(shortVal, longhands.length)
|
|
6740
|
+
: null;
|
|
6741
|
+
const compress = isBorderSide ? compressBorderSide : compressShorthand;
|
|
6742
|
+
const emptyVal = isBorderSide ? "" : "0";
|
|
6743
|
+
return longhands.map(
|
|
6744
|
+
(/** @type {any} */ { name, entry: lEntry }, /** @type {any} */ idx) => {
|
|
6745
|
+
const lVal = style[name] ?? (expanded ? expanded[idx] : "");
|
|
6746
|
+
return html`
|
|
6747
|
+
<div class="style-row style-row--child" data-prop=${name}>
|
|
6748
|
+
<div class="style-row-label">
|
|
6749
|
+
${lVal !== undefined && lVal !== ""
|
|
6750
|
+
? html`<span
|
|
6751
|
+
class="set-dot"
|
|
6752
|
+
title="Clear ${name}"
|
|
6753
|
+
@click=${(/** @type {any} */ e) => {
|
|
6754
|
+
e.stopPropagation();
|
|
6755
|
+
// Recompose shorthand with this longhand cleared
|
|
6756
|
+
const vals = longhands.map(
|
|
6757
|
+
(/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6758
|
+
i === idx
|
|
6759
|
+
? emptyVal
|
|
6760
|
+
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6761
|
+
);
|
|
6762
|
+
let s = S;
|
|
6763
|
+
for (const l of longhands) {
|
|
6764
|
+
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6765
|
+
}
|
|
6766
|
+
s = commitFn(s, shortProp, compress(vals));
|
|
6767
|
+
update(s);
|
|
6768
|
+
}}
|
|
6769
|
+
></span>`
|
|
6770
|
+
: nothing}
|
|
6771
|
+
<sp-field-label size="s" title=${name}
|
|
6772
|
+
>${propLabel(lEntry, name)}</sp-field-label
|
|
6773
|
+
>
|
|
6774
|
+
</div>
|
|
6775
|
+
${widgetForType(
|
|
6776
|
+
inferInputType(lEntry),
|
|
6777
|
+
lEntry,
|
|
6778
|
+
name,
|
|
6779
|
+
lVal,
|
|
6780
|
+
(/** @type {any} */ newVal) => {
|
|
6781
|
+
// Recompose shorthand with this longhand updated
|
|
6782
|
+
const vals = longhands.map((/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6783
|
+
i === idx
|
|
6784
|
+
? newVal || emptyVal
|
|
6785
|
+
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6786
|
+
);
|
|
6787
|
+
let s = S;
|
|
6788
|
+
for (const l of longhands) {
|
|
6789
|
+
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6790
|
+
}
|
|
6791
|
+
s = commitFn(s, shortProp, compress(vals));
|
|
6792
|
+
update(s);
|
|
6793
|
+
renderRightPanel();
|
|
6794
|
+
},
|
|
6795
|
+
{ placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
|
|
6796
|
+
)}
|
|
6797
|
+
</div>
|
|
6798
|
+
`;
|
|
6799
|
+
},
|
|
6800
|
+
);
|
|
6801
|
+
})()
|
|
6659
6802
|
: nothing}
|
|
6660
6803
|
`;
|
|
6661
6804
|
}
|
|
@@ -6674,30 +6817,20 @@ function styleSidebarTemplate(
|
|
|
6674
6817
|
const mediaTabsT =
|
|
6675
6818
|
mediaNames.length > 0
|
|
6676
6819
|
? html`
|
|
6677
|
-
<sp-tabs
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6820
|
+
<sp-tabs
|
|
6821
|
+
size="s"
|
|
6822
|
+
selected=${activeTab || "base"}
|
|
6823
|
+
@change=${(/** @type {any} */ e) => {
|
|
6824
|
+
const val = e.target.selected;
|
|
6825
|
+
const newMedia = val === "base" ? null : val;
|
|
6826
|
+
if (newMedia !== S.ui.activeMedia) {
|
|
6827
|
+
updateUi("activeMedia", newMedia);
|
|
6828
|
+
}
|
|
6829
|
+
}}
|
|
6830
|
+
>
|
|
6831
|
+
<sp-tab label="Base" value="base"></sp-tab>
|
|
6688
6832
|
${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
|
-
`,
|
|
6833
|
+
(name) => html` <sp-tab label=${mediaDisplayName(name)} value=${name}></sp-tab> `,
|
|
6701
6834
|
)}
|
|
6702
6835
|
</sp-tabs>
|
|
6703
6836
|
`
|
|
@@ -6743,8 +6876,7 @@ function styleSidebarTemplate(
|
|
|
6743
6876
|
inp.remove();
|
|
6744
6877
|
picker.style.display = "";
|
|
6745
6878
|
if (accept && v && isNestedSelector(v)) {
|
|
6746
|
-
|
|
6747
|
-
renderRightPanel();
|
|
6879
|
+
updateUi("activeSelector", v);
|
|
6748
6880
|
}
|
|
6749
6881
|
};
|
|
6750
6882
|
inp.addEventListener("keydown", (ev) => {
|
|
@@ -6755,8 +6887,7 @@ function styleSidebarTemplate(
|
|
|
6755
6887
|
return;
|
|
6756
6888
|
}
|
|
6757
6889
|
const newSelector = val === "__base__" ? null : val;
|
|
6758
|
-
|
|
6759
|
-
renderRightPanel();
|
|
6890
|
+
updateUi("activeSelector", newSelector);
|
|
6760
6891
|
}}
|
|
6761
6892
|
>
|
|
6762
6893
|
<sp-menu-item value="__base__">(base)</sp-menu-item>
|
|
@@ -6807,10 +6938,15 @@ function styleSidebarTemplate(
|
|
|
6807
6938
|
updateStyle(s, S.selection, prop, val);
|
|
6808
6939
|
}
|
|
6809
6940
|
|
|
6941
|
+
// ── Compute inherited style from higher breakpoints ──────────────────────
|
|
6942
|
+
/** @type {Record<string, any>} */
|
|
6943
|
+
const inheritedStyle = computeInheritedStyle(style, mediaNames, activeTab, activeSelector);
|
|
6944
|
+
|
|
6810
6945
|
// Auto-open sections that have properties
|
|
6811
6946
|
const newSections = autoOpenSections({ style: activeStyle }, S.ui.styleSections);
|
|
6812
6947
|
if (JSON.stringify(newSections) !== JSON.stringify(S.ui.styleSections)) {
|
|
6813
|
-
|
|
6948
|
+
session = { ...session, ui: { ...session.ui, styleSections: newSections } };
|
|
6949
|
+
S = toFlat(doc, session);
|
|
6814
6950
|
}
|
|
6815
6951
|
|
|
6816
6952
|
// Partition properties into sections
|
|
@@ -6842,7 +6978,9 @@ function styleSidebarTemplate(
|
|
|
6842
6978
|
const sectionActiveProps = entries.filter((/** @type {any} */ { prop, entry }) => {
|
|
6843
6979
|
if (activeStyle[prop] !== undefined) return true;
|
|
6844
6980
|
if (inferInputType(entry) === "shorthand") {
|
|
6845
|
-
return getLonghands(prop).some(
|
|
6981
|
+
return getLonghands(prop).some(
|
|
6982
|
+
(/** @type {any} */ l) => activeStyle[l.name] !== undefined,
|
|
6983
|
+
);
|
|
6846
6984
|
}
|
|
6847
6985
|
return false;
|
|
6848
6986
|
});
|
|
@@ -6857,9 +6995,12 @@ function styleSidebarTemplate(
|
|
|
6857
6995
|
|
|
6858
6996
|
if (type === "shorthand") {
|
|
6859
6997
|
const longhands = getLonghands(prop);
|
|
6860
|
-
const hasAny =
|
|
6998
|
+
const hasAny =
|
|
6999
|
+
hasVal || longhands.some((/** @type {any} */ l) => activeStyle[l.name] !== undefined);
|
|
6861
7000
|
if (!hasAny && !condMet) continue;
|
|
6862
|
-
rows.push(
|
|
7001
|
+
rows.push(
|
|
7002
|
+
renderShorthandRow(prop, entry, activeStyle, commitStyle, () => {}, inheritedStyle),
|
|
7003
|
+
);
|
|
6863
7004
|
} else {
|
|
6864
7005
|
const isWarning = hasVal && !condMet;
|
|
6865
7006
|
if (hasVal || condMet) {
|
|
@@ -6872,6 +7013,7 @@ function styleSidebarTemplate(
|
|
|
6872
7013
|
() => update(commitStyle(S, prop, undefined)),
|
|
6873
7014
|
isWarning,
|
|
6874
7015
|
sec.$layout === "grid",
|
|
7016
|
+
inheritedStyle[prop],
|
|
6875
7017
|
),
|
|
6876
7018
|
);
|
|
6877
7019
|
}
|
|
@@ -6885,10 +7027,7 @@ function styleSidebarTemplate(
|
|
|
6885
7027
|
label=${sec.label}
|
|
6886
7028
|
.open=${isOpen}
|
|
6887
7029
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
6888
|
-
S
|
|
6889
|
-
...S,
|
|
6890
|
-
ui: { ...S.ui, styleSections: { ...S.ui.styleSections, [sec.key]: e.target.open } },
|
|
6891
|
-
};
|
|
7030
|
+
updateUi("styleSections", { ...S.ui.styleSections, [sec.key]: e.target.open });
|
|
6892
7031
|
}}
|
|
6893
7032
|
>
|
|
6894
7033
|
${sectionActiveProps.length > 0
|
|
@@ -6929,10 +7068,7 @@ function styleSidebarTemplate(
|
|
|
6929
7068
|
label="Custom"
|
|
6930
7069
|
.open=${customIsOpen}
|
|
6931
7070
|
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
6932
|
-
S
|
|
6933
|
-
...S,
|
|
6934
|
-
ui: { ...S.ui, styleSections: { ...S.ui.styleSections, other: e.target.open } },
|
|
6935
|
-
};
|
|
7071
|
+
updateUi("styleSections", { ...S.ui.styleSections, other: e.target.open });
|
|
6936
7072
|
}}
|
|
6937
7073
|
>
|
|
6938
7074
|
<div>
|
|
@@ -7003,7 +7139,7 @@ function styleSidebarTemplate(
|
|
|
7003
7139
|
|
|
7004
7140
|
/** Top-level Style panel — returns a lit-html template */
|
|
7005
7141
|
function renderStylePanelTemplate() {
|
|
7006
|
-
if (canvasMode === "
|
|
7142
|
+
if (canvasMode === "settings" && S.ui.stylebookSelection) {
|
|
7007
7143
|
const node = S.document;
|
|
7008
7144
|
if (!node) return html`<div class="empty-state">No document loaded</div>`;
|
|
7009
7145
|
return html`
|
|
@@ -7227,8 +7363,7 @@ function _renderSourceView(/** @type {any} */ container) {
|
|
|
7227
7363
|
@blur=${(/** @type {any} */ e) => {
|
|
7228
7364
|
try {
|
|
7229
7365
|
const parsed = JSON.parse(e.target.value);
|
|
7230
|
-
|
|
7231
|
-
render();
|
|
7366
|
+
update({ ...S, document: parsed, dirty: true });
|
|
7232
7367
|
} catch {}
|
|
7233
7368
|
}}
|
|
7234
7369
|
></textarea>
|
|
@@ -7253,29 +7388,31 @@ function renderFunctionEditor() {
|
|
|
7253
7388
|
const editing = S.ui.editingFunction;
|
|
7254
7389
|
|
|
7255
7390
|
// If editor already exists and matches current target, just sync value
|
|
7256
|
-
if (functionEditor && functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
7391
|
+
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
7257
7392
|
const body = getFunctionBody(editing);
|
|
7258
|
-
const currentVal = functionEditor.getValue();
|
|
7393
|
+
const currentVal = view.functionEditor.getValue();
|
|
7259
7394
|
if (currentVal !== body) {
|
|
7260
|
-
functionEditor._ignoreNextChange = true;
|
|
7261
|
-
functionEditor.setValue(body);
|
|
7395
|
+
view.functionEditor._ignoreNextChange = true;
|
|
7396
|
+
view.functionEditor.setValue(body);
|
|
7262
7397
|
}
|
|
7263
7398
|
return;
|
|
7264
7399
|
}
|
|
7265
7400
|
|
|
7266
7401
|
// Dispose previous editors
|
|
7267
|
-
if (functionEditor) {
|
|
7268
|
-
functionEditor.dispose();
|
|
7269
|
-
functionEditor = null;
|
|
7402
|
+
if (view.functionEditor) {
|
|
7403
|
+
view.functionEditor.dispose();
|
|
7404
|
+
view.functionEditor = null;
|
|
7270
7405
|
}
|
|
7271
|
-
if (monacoEditor) {
|
|
7272
|
-
monacoEditor.dispose();
|
|
7273
|
-
monacoEditor = null;
|
|
7406
|
+
if (view.monacoEditor) {
|
|
7407
|
+
view.monacoEditor.dispose();
|
|
7408
|
+
view.monacoEditor = null;
|
|
7274
7409
|
}
|
|
7275
7410
|
|
|
7276
|
-
// Clean up canvas DnD
|
|
7277
|
-
for (const fn of canvasDndCleanups) fn();
|
|
7278
|
-
canvasDndCleanups = [];
|
|
7411
|
+
// Clean up canvas DnD and event handlers
|
|
7412
|
+
for (const fn of view.canvasDndCleanups) fn();
|
|
7413
|
+
view.canvasDndCleanups = [];
|
|
7414
|
+
for (const fn of view.canvasEventCleanups) fn();
|
|
7415
|
+
view.canvasEventCleanups = [];
|
|
7279
7416
|
canvasPanels.length = 0;
|
|
7280
7417
|
|
|
7281
7418
|
litRender(nothing, canvasWrap);
|
|
@@ -7300,7 +7437,7 @@ function renderFunctionEditor() {
|
|
|
7300
7437
|
const body = getFunctionBody(editing);
|
|
7301
7438
|
const args = getFunctionArgs(editing, S);
|
|
7302
7439
|
|
|
7303
|
-
functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
7440
|
+
view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
7304
7441
|
value: body,
|
|
7305
7442
|
language: "javascript",
|
|
7306
7443
|
theme: "vs-dark",
|
|
@@ -7313,17 +7450,18 @@ function renderFunctionEditor() {
|
|
|
7313
7450
|
wordWrap: "on",
|
|
7314
7451
|
tabSize: 2,
|
|
7315
7452
|
});
|
|
7316
|
-
functionEditor._editingTarget = JSON.stringify(editing);
|
|
7453
|
+
view.functionEditor._editingTarget = JSON.stringify(editing);
|
|
7317
7454
|
|
|
7318
7455
|
// Format on open — show pretty-printed code, then run initial lint
|
|
7319
7456
|
codeService("format", { code: body, args }).then((result) => {
|
|
7320
|
-
if (result?.code != null && functionEditor) {
|
|
7321
|
-
functionEditor._ignoreNextChange = true;
|
|
7322
|
-
functionEditor.setValue(result.code);
|
|
7457
|
+
if (result?.code != null && view.functionEditor) {
|
|
7458
|
+
view.functionEditor._ignoreNextChange = true;
|
|
7459
|
+
view.functionEditor.setValue(result.code);
|
|
7323
7460
|
}
|
|
7324
7461
|
});
|
|
7325
7462
|
codeService("lint", { code: body, args }).then((result) => {
|
|
7326
|
-
if (result?.diagnostics && functionEditor)
|
|
7463
|
+
if (result?.diagnostics && view.functionEditor)
|
|
7464
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
7327
7465
|
});
|
|
7328
7466
|
|
|
7329
7467
|
// Debounced sync back to state + lint on edit
|
|
@@ -7332,15 +7470,15 @@ function renderFunctionEditor() {
|
|
|
7332
7470
|
/** @type {any} */
|
|
7333
7471
|
let lintDebounce;
|
|
7334
7472
|
let lintGen = 0;
|
|
7335
|
-
functionEditor.onDidChangeModelContent(() => {
|
|
7336
|
-
if (functionEditor._ignoreNextChange) {
|
|
7337
|
-
functionEditor._ignoreNextChange = false;
|
|
7473
|
+
view.functionEditor.onDidChangeModelContent(() => {
|
|
7474
|
+
if (view.functionEditor._ignoreNextChange) {
|
|
7475
|
+
view.functionEditor._ignoreNextChange = false;
|
|
7338
7476
|
return;
|
|
7339
7477
|
}
|
|
7340
7478
|
|
|
7341
7479
|
clearTimeout(syncDebounce);
|
|
7342
7480
|
syncDebounce = setTimeout(() => {
|
|
7343
|
-
const newBody = functionEditor.getValue();
|
|
7481
|
+
const newBody = view.functionEditor.getValue();
|
|
7344
7482
|
if (editing.type === "def") {
|
|
7345
7483
|
update(updateDef(S, editing.defName, { body: newBody }));
|
|
7346
7484
|
} else if (editing.type === "event") {
|
|
@@ -7360,11 +7498,11 @@ function renderFunctionEditor() {
|
|
|
7360
7498
|
clearTimeout(lintDebounce);
|
|
7361
7499
|
lintDebounce = setTimeout(() => {
|
|
7362
7500
|
const gen = ++lintGen;
|
|
7363
|
-
const currentCode = functionEditor.getValue();
|
|
7501
|
+
const currentCode = view.functionEditor.getValue();
|
|
7364
7502
|
codeService("lint", { code: currentCode, args }).then((result) => {
|
|
7365
7503
|
if (gen !== lintGen) return;
|
|
7366
|
-
if (result?.diagnostics && functionEditor)
|
|
7367
|
-
setLintMarkers(functionEditor, result.diagnostics);
|
|
7504
|
+
if (result?.diagnostics && view.functionEditor)
|
|
7505
|
+
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
7368
7506
|
});
|
|
7369
7507
|
}, 750);
|
|
7370
7508
|
});
|
|
@@ -7381,10 +7519,9 @@ function getFunctionBody(/** @type {any} */ editing) {
|
|
|
7381
7519
|
}
|
|
7382
7520
|
|
|
7383
7521
|
// Register Monaco JS completion provider for state scope variables (once)
|
|
7384
|
-
let _completionRegistered = false;
|
|
7385
7522
|
function registerFunctionCompletions() {
|
|
7386
|
-
if (_completionRegistered) return;
|
|
7387
|
-
_completionRegistered = true;
|
|
7523
|
+
if (view._completionRegistered) return;
|
|
7524
|
+
view._completionRegistered = true;
|
|
7388
7525
|
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
7389
7526
|
triggerCharacters: ["."],
|
|
7390
7527
|
provideCompletionItems(model, position) {
|
|
@@ -7414,166 +7551,10 @@ function registerFunctionCompletions() {
|
|
|
7414
7551
|
});
|
|
7415
7552
|
}
|
|
7416
7553
|
|
|
7417
|
-
// ─── Toolbar
|
|
7554
|
+
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
7418
7555
|
|
|
7419
7556
|
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);
|
|
7557
|
+
toolbarPanel.render();
|
|
7577
7558
|
}
|
|
7578
7559
|
|
|
7579
7560
|
// ─── File Operations (delegated to file-ops.js) ─────────────────────────────
|
|
@@ -7591,13 +7572,16 @@ function fileOpsCtx() {
|
|
|
7591
7572
|
function openFile() {
|
|
7592
7573
|
return _openFile(fileOpsCtx());
|
|
7593
7574
|
}
|
|
7594
|
-
function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
7595
|
-
const ns = _loadMarkdown(source, fileHandle);
|
|
7575
|
+
async function loadMarkdown(/** @type {any} */ source, /** @type {any} */ fileHandle) {
|
|
7576
|
+
const ns = await _loadMarkdown(source, fileHandle);
|
|
7596
7577
|
S = ns;
|
|
7597
7578
|
}
|
|
7598
7579
|
function saveFile() {
|
|
7599
7580
|
return _saveFile(fileOpsCtx());
|
|
7600
7581
|
}
|
|
7582
|
+
function exportFile() {
|
|
7583
|
+
return _exportFile(fileOpsCtx());
|
|
7584
|
+
}
|
|
7601
7585
|
|
|
7602
7586
|
// ─── File tree (delegated to files.js) ───────────────────────────────────────
|
|
7603
7587
|
|
|
@@ -7620,7 +7604,12 @@ function renderFilesTemplate() {
|
|
|
7620
7604
|
function openFileFromTree(/** @type {any} */ path) {
|
|
7621
7605
|
return _openFileFromTree(
|
|
7622
7606
|
{
|
|
7623
|
-
S
|
|
7607
|
+
get S() {
|
|
7608
|
+
return S;
|
|
7609
|
+
},
|
|
7610
|
+
set S(v) {
|
|
7611
|
+
S = v;
|
|
7612
|
+
},
|
|
7624
7613
|
commit: (/** @type {any} */ ns) => {
|
|
7625
7614
|
S = ns;
|
|
7626
7615
|
},
|
|
@@ -7638,16 +7627,16 @@ initShortcuts(() => ({
|
|
|
7638
7627
|
S = ns;
|
|
7639
7628
|
},
|
|
7640
7629
|
canvasMode,
|
|
7641
|
-
panX,
|
|
7642
|
-
panY,
|
|
7630
|
+
panX: view.panX,
|
|
7631
|
+
panY: view.panY,
|
|
7643
7632
|
setPan: (x, y) => {
|
|
7644
|
-
panX = x;
|
|
7645
|
-
panY = y;
|
|
7646
|
-
needsCenter = false;
|
|
7633
|
+
view.panX = x;
|
|
7634
|
+
view.panY = y;
|
|
7635
|
+
view.needsCenter = false;
|
|
7647
7636
|
},
|
|
7648
7637
|
applyTransform,
|
|
7649
7638
|
positionZoomIndicator,
|
|
7650
|
-
componentInlineEdit,
|
|
7639
|
+
componentInlineEdit: view.componentInlineEdit,
|
|
7651
7640
|
saveFile,
|
|
7652
7641
|
openProject,
|
|
7653
7642
|
enterEditOnPath(path) {
|
|
@@ -7666,20 +7655,18 @@ initShortcuts(() => ({
|
|
|
7666
7655
|
// ─── Autosave (registered as update middleware) ──────────────────────────────
|
|
7667
7656
|
|
|
7668
7657
|
/** @type {any} */
|
|
7669
|
-
let autosaveTimer;
|
|
7670
7658
|
const AUTO_SAVE_DELAY = 2000;
|
|
7671
7659
|
|
|
7672
7660
|
function scheduleAutosave() {
|
|
7673
7661
|
if (!S.fileHandle || !S.dirty) return;
|
|
7674
|
-
clearTimeout(autosaveTimer);
|
|
7675
|
-
autosaveTimer = setTimeout(async () => {
|
|
7662
|
+
clearTimeout(view.autosaveTimer);
|
|
7663
|
+
view.autosaveTimer = setTimeout(async () => {
|
|
7676
7664
|
if (S.fileHandle && S.dirty && "createWritable" in S.fileHandle) {
|
|
7677
7665
|
try {
|
|
7678
7666
|
const writable = await S.fileHandle.createWritable();
|
|
7679
7667
|
await writable.write(JSON.stringify(S.document, null, 2));
|
|
7680
7668
|
await writable.close();
|
|
7681
|
-
|
|
7682
|
-
renderToolbar();
|
|
7669
|
+
update({ ...S, dirty: false });
|
|
7683
7670
|
statusMessage("Auto-saved");
|
|
7684
7671
|
} catch {}
|
|
7685
7672
|
}
|
|
@@ -7689,4 +7676,3 @@ function scheduleAutosave() {
|
|
|
7689
7676
|
addUpdateMiddleware((/** @type {any} */ state) => {
|
|
7690
7677
|
if (state.dirty) scheduleAutosave();
|
|
7691
7678
|
});
|
|
7692
|
-
// trigger rebuild
|