@jxsuite/studio 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +6293 -5966
- package/dist/studio.js.map +23 -17
- package/package.json +2 -2
- package/src/editor/insertion-helper.js +301 -0
- package/src/editor/slash-menu.js +111 -37
- package/src/panels/block-action-bar.js +436 -0
- package/src/panels/dnd.js +332 -0
- package/src/panels/editors.js +196 -0
- package/src/panels/left-panel.js +3 -2
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/right-panel.js +2 -1
- package/src/panels/shared.js +74 -0
- package/src/panels/stylebook-panel.js +1052 -0
- package/src/studio.js +64 -3340
package/src/studio.js
CHANGED
|
@@ -11,32 +11,20 @@ import {
|
|
|
11
11
|
hoverNode,
|
|
12
12
|
insertNode,
|
|
13
13
|
removeNode,
|
|
14
|
-
moveNode,
|
|
15
14
|
updateProperty,
|
|
16
|
-
updateStyle,
|
|
17
|
-
updateAttribute,
|
|
18
15
|
updateDef,
|
|
19
|
-
updateMedia,
|
|
20
16
|
pushDocument,
|
|
21
17
|
popDocument,
|
|
22
|
-
updateProp,
|
|
23
|
-
addSwitchCase,
|
|
24
|
-
removeSwitchCase,
|
|
25
|
-
renameSwitchCase,
|
|
26
|
-
applyMutation,
|
|
27
18
|
getNodeAtPath,
|
|
28
|
-
nodeLabel,
|
|
29
19
|
pathsEqual,
|
|
30
20
|
parentElementPath,
|
|
31
21
|
childIndex,
|
|
32
22
|
isAncestor,
|
|
33
23
|
canvasWrap,
|
|
34
|
-
leftPanel,
|
|
35
24
|
toolbarEl,
|
|
36
25
|
elToPath,
|
|
37
26
|
canvasPanels,
|
|
38
27
|
VOID_ELEMENTS,
|
|
39
|
-
debouncedStyleCommit,
|
|
40
28
|
stripEventHandlers,
|
|
41
29
|
registerRenderer,
|
|
42
30
|
render,
|
|
@@ -50,9 +38,7 @@ import {
|
|
|
50
38
|
notify,
|
|
51
39
|
projectState,
|
|
52
40
|
setProjectState,
|
|
53
|
-
updateFrontmatter,
|
|
54
41
|
updateUi,
|
|
55
|
-
updateSession,
|
|
56
42
|
setUpdateSessionFn,
|
|
57
43
|
setGetDocFn,
|
|
58
44
|
setGetSessionFn,
|
|
@@ -71,23 +57,12 @@ import {
|
|
|
71
57
|
getActiveElement,
|
|
72
58
|
isEditableBlock,
|
|
73
59
|
isInlineInContext,
|
|
74
|
-
getInlineActions,
|
|
75
60
|
} from "./editor/inline-edit.js";
|
|
76
61
|
import {
|
|
77
62
|
showSlashMenu as sharedShowSlashMenu,
|
|
78
63
|
dismissSlashMenu as sharedDismissSlashMenu,
|
|
79
64
|
isSlashMenuOpen,
|
|
80
65
|
} from "./editor/slash-menu.js";
|
|
81
|
-
import { toggleInlineFormat, isTagActiveInSelection } from "./editor/inline-format.js";
|
|
82
|
-
import {
|
|
83
|
-
camelToLabel,
|
|
84
|
-
attrLabel,
|
|
85
|
-
inferInputType,
|
|
86
|
-
findCollectionSchema,
|
|
87
|
-
friendlyNameToVar,
|
|
88
|
-
varDisplayName,
|
|
89
|
-
parseCemType,
|
|
90
|
-
} from "./utils/studio-utils.js";
|
|
91
66
|
import {
|
|
92
67
|
renderStatusbar,
|
|
93
68
|
statusMessage,
|
|
@@ -121,10 +96,9 @@ import {
|
|
|
121
96
|
applyOverridesToCanvas,
|
|
122
97
|
} from "./utils/canvas-media.js";
|
|
123
98
|
import { createDevServerPlatform } from "./platforms/devserver.js";
|
|
124
|
-
import { codeService
|
|
99
|
+
import { codeService } from "./services/code-services.js";
|
|
125
100
|
import {
|
|
126
101
|
getEffectiveMedia,
|
|
127
|
-
getEffectiveStyle,
|
|
128
102
|
getEffectiveImports,
|
|
129
103
|
getEffectiveElements,
|
|
130
104
|
getEffectiveHead,
|
|
@@ -132,8 +106,6 @@ import {
|
|
|
132
106
|
import {
|
|
133
107
|
defCategory,
|
|
134
108
|
defBadgeLabel,
|
|
135
|
-
isCustomElementDoc,
|
|
136
|
-
collectCssParts,
|
|
137
109
|
resolveDefaultForCanvas,
|
|
138
110
|
renderSignalsTemplate,
|
|
139
111
|
} from "./panels/signals-panel.js";
|
|
@@ -144,25 +116,16 @@ import {
|
|
|
144
116
|
} from "./files/components.js";
|
|
145
117
|
|
|
146
118
|
import {
|
|
147
|
-
draggable,
|
|
148
119
|
dropTargetForElements,
|
|
149
120
|
monitorForElements,
|
|
150
121
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
151
|
-
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
152
|
-
import {
|
|
153
|
-
attachInstruction,
|
|
154
|
-
extractInstruction,
|
|
155
|
-
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
|
156
122
|
|
|
157
123
|
import { html, render as litRender, nothing } from "lit-html";
|
|
158
|
-
import { live } from "lit-html/directives/live.js";
|
|
159
124
|
import { ref } from "lit-html/directives/ref.js";
|
|
160
125
|
import { styleMap } from "lit-html/directives/style-map.js";
|
|
161
126
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
162
127
|
|
|
163
128
|
import webdata from "../data/webdata.json";
|
|
164
|
-
import htmlMeta from "../data/html-meta.json";
|
|
165
|
-
import stylebookMeta from "../data/stylebook-meta.json";
|
|
166
129
|
import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
167
130
|
import { renderGitPanel } from "./panels/git-panel.js";
|
|
168
131
|
|
|
@@ -170,22 +133,31 @@ import { renderGitPanel } from "./panels/git-panel.js";
|
|
|
170
133
|
// Explicit class imports + registration — bare side-effect imports are tree-shaken
|
|
171
134
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
172
135
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
173
|
-
import { renderFieldRow } from "./ui/field-row.js";
|
|
174
136
|
import "./ui/panel-resize.js";
|
|
175
137
|
import { showContextMenu, dismissContextMenu } from "./editor/context-menu.js";
|
|
176
|
-
import { convertToComponent } from "./editor/convert-to-component.js";
|
|
177
138
|
import { initShortcuts } from "./editor/shortcuts.js";
|
|
139
|
+
import * as insertionHelper from "./editor/insertion-helper.js";
|
|
178
140
|
import { renderActivityBar } from "./panels/activity-bar.js";
|
|
179
141
|
import { renderBrowse } from "./browse/browse.js";
|
|
180
|
-
import { renderCollectionsEditor } from "./settings/collections-editor.js";
|
|
181
|
-
import { renderDefsEditor } from "./settings/defs-editor.js";
|
|
182
142
|
import * as toolbarPanel from "./panels/toolbar.js";
|
|
183
143
|
import * as overlaysPanel from "./panels/overlays.js";
|
|
184
144
|
import * as rightPanelMod from "./panels/right-panel.js";
|
|
185
145
|
import * as leftPanelMod from "./panels/left-panel.js";
|
|
186
|
-
import {
|
|
187
|
-
import {
|
|
188
|
-
|
|
146
|
+
import { renderStylebookMode, renderStylebookOverlays } from "./panels/stylebook-panel.js";
|
|
147
|
+
import {
|
|
148
|
+
registerLayersDnD,
|
|
149
|
+
registerComponentsDnD,
|
|
150
|
+
registerElementsDnD,
|
|
151
|
+
applyDropInstruction,
|
|
152
|
+
} from "./panels/dnd.js";
|
|
153
|
+
import { mediaDisplayName, defaultDef } from "./panels/shared.js";
|
|
154
|
+
import { renderFunctionEditor, registerFunctionCompletions } from "./panels/editors.js";
|
|
155
|
+
import {
|
|
156
|
+
renderBlockActionBar,
|
|
157
|
+
dismissLinkPopover,
|
|
158
|
+
initBlockActionBar,
|
|
159
|
+
} from "./panels/block-action-bar.js";
|
|
160
|
+
import { initCssData } from "./panels/style-utils.js";
|
|
189
161
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
190
162
|
|
|
191
163
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
@@ -785,8 +757,16 @@ overlaysPanel.mount({
|
|
|
785
757
|
getActivePanel,
|
|
786
758
|
});
|
|
787
759
|
|
|
760
|
+
initBlockActionBar({
|
|
761
|
+
getCanvasMode: () => canvasMode,
|
|
762
|
+
findCanvasElement,
|
|
763
|
+
getActivePanel,
|
|
764
|
+
navigateToComponent,
|
|
765
|
+
createFloatingContainer,
|
|
766
|
+
});
|
|
767
|
+
|
|
788
768
|
rightPanelMod.mount({
|
|
789
|
-
|
|
769
|
+
navigateToComponent,
|
|
790
770
|
getCanvasMode: () => canvasMode,
|
|
791
771
|
renderCanvas: () => renderCanvas(),
|
|
792
772
|
updateForcedPseudoPreview,
|
|
@@ -804,8 +784,6 @@ leftPanelMod.mount({
|
|
|
804
784
|
defCategory,
|
|
805
785
|
defBadgeLabel,
|
|
806
786
|
navigateToComponent,
|
|
807
|
-
selectStylebookTag,
|
|
808
|
-
stylebookMeta,
|
|
809
787
|
webdata,
|
|
810
788
|
defaultDef,
|
|
811
789
|
registerLayersDnD,
|
|
@@ -1177,7 +1155,15 @@ function renderCanvas() {
|
|
|
1177
1155
|
|
|
1178
1156
|
// Settings mode: render element catalog with panzoom surface
|
|
1179
1157
|
if (canvasMode === "settings") {
|
|
1180
|
-
|
|
1158
|
+
renderStylebookMode({
|
|
1159
|
+
canvasPanelTemplate,
|
|
1160
|
+
applyTransform,
|
|
1161
|
+
observeCenterUntilStable,
|
|
1162
|
+
renderZoomIndicator,
|
|
1163
|
+
updateActivePanelHeaders,
|
|
1164
|
+
overlayBoxDescriptor,
|
|
1165
|
+
effectiveZoom,
|
|
1166
|
+
});
|
|
1181
1167
|
return;
|
|
1182
1168
|
}
|
|
1183
1169
|
|
|
@@ -1965,403 +1951,6 @@ function getActivePanel() {
|
|
|
1965
1951
|
return canvasPanels[0];
|
|
1966
1952
|
}
|
|
1967
1953
|
|
|
1968
|
-
// ── Floating inline toolbar ────────────────────────────────────────────────
|
|
1969
|
-
|
|
1970
|
-
/** Pre-built icon templates for inline format buttons (avoids unsafeStatic) */
|
|
1971
|
-
const formatIconMap = /** @type {Record<string, any>} */ ({
|
|
1972
|
-
"sp-icon-text-bold": html`<sp-icon-text-bold slot="icon"></sp-icon-text-bold>`,
|
|
1973
|
-
"sp-icon-text-italic": html`<sp-icon-text-italic slot="icon"></sp-icon-text-italic>`,
|
|
1974
|
-
"sp-icon-text-underline": html`<sp-icon-text-underline slot="icon"></sp-icon-text-underline>`,
|
|
1975
|
-
"sp-icon-text-strikethrough": html`<sp-icon-text-strikethrough
|
|
1976
|
-
slot="icon"
|
|
1977
|
-
></sp-icon-text-strikethrough>`,
|
|
1978
|
-
"sp-icon-text-superscript": html`<sp-icon-text-superscript
|
|
1979
|
-
slot="icon"
|
|
1980
|
-
></sp-icon-text-superscript>`,
|
|
1981
|
-
"sp-icon-text-subscript": html`<sp-icon-text-subscript slot="icon"></sp-icon-text-subscript>`,
|
|
1982
|
-
"sp-icon-code": html`<sp-icon-code slot="icon"></sp-icon-code>`,
|
|
1983
|
-
"sp-icon-link": html`<sp-icon-link slot="icon"></sp-icon-link>`,
|
|
1984
|
-
});
|
|
1985
|
-
|
|
1986
|
-
/**
|
|
1987
|
-
* Prevent the bar from stealing focus from contenteditable
|
|
1988
|
-
*
|
|
1989
|
-
* @param {any} e
|
|
1990
|
-
*/
|
|
1991
|
-
function onBarMousedown(e) {
|
|
1992
|
-
if (e.target.closest("sp-textfield")) return;
|
|
1993
|
-
if (e.target.closest(".bar-drag-handle")) return;
|
|
1994
|
-
e.preventDefault();
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
/** Saved selection range for format button mousedown→click flow */
|
|
1998
|
-
function captureSelectionRange() {
|
|
1999
|
-
const sel = window.getSelection();
|
|
2000
|
-
if (sel && sel.rangeCount) view.savedRange = sel.getRangeAt(0).cloneRange();
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
/**
|
|
2004
|
-
* @param {any} e
|
|
2005
|
-
* @param {any} action
|
|
2006
|
-
*/
|
|
2007
|
-
function onFormatClick(e, action) {
|
|
2008
|
-
e.stopPropagation();
|
|
2009
|
-
if (action.command === "link") {
|
|
2010
|
-
showLinkPopover(e.target.closest("sp-action-button"));
|
|
2011
|
-
} else if (view.savedRange) {
|
|
2012
|
-
const sel = /** @type {any} */ (window.getSelection());
|
|
2013
|
-
const anchor = view.savedRange.startContainer;
|
|
2014
|
-
const editableRoot = (
|
|
2015
|
-
anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement
|
|
2016
|
-
)?.closest("[contenteditable]");
|
|
2017
|
-
if (editableRoot) {
|
|
2018
|
-
editableRoot.focus();
|
|
2019
|
-
sel.removeAllRanges();
|
|
2020
|
-
sel.addRange(view.savedRange);
|
|
2021
|
-
applyInlineFormat(action);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
function renderParentSelector() {
|
|
2027
|
-
const pPath = parentElementPath(S.selection);
|
|
2028
|
-
if (!pPath) return nothing;
|
|
2029
|
-
const parentNode = getNodeAtPath(S.document, pPath);
|
|
2030
|
-
return html`
|
|
2031
|
-
<sp-action-button
|
|
2032
|
-
size="xs"
|
|
2033
|
-
quiet
|
|
2034
|
-
title="Select parent: ${nodeLabel(parentNode)}"
|
|
2035
|
-
@click=${(/** @type {any} */ e) => {
|
|
2036
|
-
e.stopPropagation();
|
|
2037
|
-
update(selectNode(S, pPath));
|
|
2038
|
-
}}
|
|
2039
|
-
>
|
|
2040
|
-
<sp-icon-back slot="icon"></sp-icon-back>
|
|
2041
|
-
</sp-action-button>
|
|
2042
|
-
`;
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
function renderMoveArrows() {
|
|
2046
|
-
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
2047
|
-
const pPath = parentElementPath(S.selection);
|
|
2048
|
-
const parentNode = getNodeAtPath(S.document, /** @type {any} */ (pPath));
|
|
2049
|
-
const siblings = parentNode?.children;
|
|
2050
|
-
return html`
|
|
2051
|
-
<sp-action-button
|
|
2052
|
-
size="xs"
|
|
2053
|
-
quiet
|
|
2054
|
-
title="Move up"
|
|
2055
|
-
?disabled=${idx <= 0}
|
|
2056
|
-
@click=${(/** @type {any} */ e) => {
|
|
2057
|
-
e.stopPropagation();
|
|
2058
|
-
moveSelectionUp();
|
|
2059
|
-
}}
|
|
2060
|
-
>
|
|
2061
|
-
<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
|
|
2062
|
-
</sp-action-button>
|
|
2063
|
-
<sp-action-button
|
|
2064
|
-
size="xs"
|
|
2065
|
-
quiet
|
|
2066
|
-
title="Move down"
|
|
2067
|
-
?disabled=${!siblings || idx >= siblings.length - 1}
|
|
2068
|
-
@click=${(/** @type {any} */ e) => {
|
|
2069
|
-
e.stopPropagation();
|
|
2070
|
-
moveSelectionDown();
|
|
2071
|
-
}}
|
|
2072
|
-
>
|
|
2073
|
-
<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
|
|
2074
|
-
</sp-action-button>
|
|
2075
|
-
`;
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
/**
|
|
2079
|
-
* Apply an inline format action.
|
|
2080
|
-
*
|
|
2081
|
-
* @param {any} action
|
|
2082
|
-
*/
|
|
2083
|
-
function applyInlineFormat(action) {
|
|
2084
|
-
// Map commands to semantic tags
|
|
2085
|
-
/** @type {Record<string, any>} */
|
|
2086
|
-
const cmdToTag = {
|
|
2087
|
-
bold: "strong",
|
|
2088
|
-
italic: "em",
|
|
2089
|
-
underline: "u",
|
|
2090
|
-
strikethrough: "del",
|
|
2091
|
-
superscript: "sup",
|
|
2092
|
-
subscript: "sub",
|
|
2093
|
-
code: "code",
|
|
2094
|
-
};
|
|
2095
|
-
|
|
2096
|
-
const tag = cmdToTag[action.command];
|
|
2097
|
-
if (tag) {
|
|
2098
|
-
const editableRoot = getActiveElement();
|
|
2099
|
-
toggleInlineFormat(tag, editableRoot);
|
|
2100
|
-
}
|
|
2101
|
-
requestAnimationFrame(() => renderBlockActionBar());
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
/** Show a link URL popover anchored to a toolbar button. */
|
|
2105
|
-
view.linkPopoverHost = document.createElement("div");
|
|
2106
|
-
view.linkPopoverHost.style.display = "contents";
|
|
2107
|
-
(document.querySelector("sp-theme") || document.body).appendChild(view.linkPopoverHost);
|
|
2108
|
-
|
|
2109
|
-
/** Dismiss the link popover if open. */
|
|
2110
|
-
function dismissLinkPopover() {
|
|
2111
|
-
if (view.linkPopoverHost) litRender(nothing, view.linkPopoverHost);
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
/** @param {any} anchorBtn */
|
|
2115
|
-
function showLinkPopover(anchorBtn) {
|
|
2116
|
-
// Dismiss existing
|
|
2117
|
-
litRender(nothing, view.linkPopoverHost);
|
|
2118
|
-
|
|
2119
|
-
const sel = window.getSelection();
|
|
2120
|
-
/** @type {any} */
|
|
2121
|
-
let existingLink = null;
|
|
2122
|
-
if (sel?.rangeCount) {
|
|
2123
|
-
/** @type {any} */
|
|
2124
|
-
let node = sel.anchorNode;
|
|
2125
|
-
while (node && node !== document.body) {
|
|
2126
|
-
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "a") {
|
|
2127
|
-
existingLink = node;
|
|
2128
|
-
break;
|
|
2129
|
-
}
|
|
2130
|
-
node = node.parentNode;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
const rect = anchorBtn.getBoundingClientRect();
|
|
2135
|
-
|
|
2136
|
-
const onApply = () => {
|
|
2137
|
-
const field = view.linkPopoverHost.querySelector("sp-textfield");
|
|
2138
|
-
const url = /** @type {any} */ (field)?.value;
|
|
2139
|
-
if (existingLink) {
|
|
2140
|
-
existingLink.setAttribute("href", url);
|
|
2141
|
-
} else if (url) {
|
|
2142
|
-
document.execCommand("createLink", false, url);
|
|
2143
|
-
}
|
|
2144
|
-
litRender(nothing, view.linkPopoverHost);
|
|
2145
|
-
renderBlockActionBar();
|
|
2146
|
-
};
|
|
2147
|
-
|
|
2148
|
-
const onRemove = () => {
|
|
2149
|
-
const frag = document.createDocumentFragment();
|
|
2150
|
-
while (existingLink.firstChild) frag.appendChild(existingLink.firstChild);
|
|
2151
|
-
existingLink.parentNode.replaceChild(frag, existingLink);
|
|
2152
|
-
litRender(nothing, view.linkPopoverHost);
|
|
2153
|
-
renderBlockActionBar();
|
|
2154
|
-
};
|
|
2155
|
-
|
|
2156
|
-
const onKeydown = (/** @type {any} */ e) => {
|
|
2157
|
-
if (e.key === "Enter") onApply();
|
|
2158
|
-
else if (e.key === "Escape") {
|
|
2159
|
-
litRender(nothing, view.linkPopoverHost);
|
|
2160
|
-
}
|
|
2161
|
-
};
|
|
2162
|
-
|
|
2163
|
-
litRender(
|
|
2164
|
-
html`
|
|
2165
|
-
<sp-popover
|
|
2166
|
-
class="link-popover"
|
|
2167
|
-
open
|
|
2168
|
-
style="position:fixed; left:${rect.left}px; top:${rect.bottom + 4}px; z-index:30"
|
|
2169
|
-
>
|
|
2170
|
-
<sp-textfield
|
|
2171
|
-
placeholder="https://..."
|
|
2172
|
-
size="s"
|
|
2173
|
-
style="width:200px"
|
|
2174
|
-
value=${existingLink?.getAttribute("href") || ""}
|
|
2175
|
-
@keydown=${onKeydown}
|
|
2176
|
-
></sp-textfield>
|
|
2177
|
-
<sp-action-button size="xs" @click=${onApply}>
|
|
2178
|
-
${existingLink ? "Update" : "Apply"}
|
|
2179
|
-
</sp-action-button>
|
|
2180
|
-
${existingLink
|
|
2181
|
-
? html` <sp-action-button size="xs" @click=${onRemove}>Remove</sp-action-button> `
|
|
2182
|
-
: nothing}
|
|
2183
|
-
</sp-popover>
|
|
2184
|
-
`,
|
|
2185
|
-
view.linkPopoverHost,
|
|
2186
|
-
);
|
|
2187
|
-
|
|
2188
|
-
requestAnimationFrame(
|
|
2189
|
-
() =>
|
|
2190
|
-
/** @type {HTMLElement | null} */ (
|
|
2191
|
-
view.linkPopoverHost?.querySelector("sp-textfield")
|
|
2192
|
-
)?.focus(),
|
|
2193
|
-
);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
/** Move the selected node up (swap with previous sibling). */
|
|
2197
|
-
function moveSelectionUp() {
|
|
2198
|
-
if (!S.selection || S.selection.length < 2) return;
|
|
2199
|
-
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
2200
|
-
if (idx <= 0) return;
|
|
2201
|
-
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
2202
|
-
update(moveNode(S, S.selection, pPath, idx - 1));
|
|
2203
|
-
session = { ...session, selection: [...pPath, "children", idx - 1] };
|
|
2204
|
-
S = toFlat(doc, session);
|
|
2205
|
-
renderOverlays();
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
/** Move the selected node down (swap with next sibling). */
|
|
2209
|
-
function moveSelectionDown() {
|
|
2210
|
-
if (!S.selection || S.selection.length < 2) return;
|
|
2211
|
-
const idx = /** @type {number} */ (childIndex(S.selection));
|
|
2212
|
-
const pPath = /** @type {any} */ (parentElementPath(S.selection));
|
|
2213
|
-
const parentNode = getNodeAtPath(S.document, pPath);
|
|
2214
|
-
const siblings = parentNode?.children;
|
|
2215
|
-
if (!siblings || idx >= siblings.length - 1) return;
|
|
2216
|
-
update(moveNode(S, S.selection, pPath, idx + 2));
|
|
2217
|
-
session = { ...session, selection: [...pPath, "children", idx + 1] };
|
|
2218
|
-
S = toFlat(doc, session);
|
|
2219
|
-
renderOverlays();
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
/**
|
|
2223
|
-
* Render the unified block action bar above the selected element. Combines tag indicator, drag
|
|
2224
|
-
* handle, move arrows, and inline formatting.
|
|
2225
|
-
*/
|
|
2226
|
-
function renderBlockActionBar() {
|
|
2227
|
-
// Ensure persistent render container exists
|
|
2228
|
-
if (!view.blockActionBarEl) {
|
|
2229
|
-
view.blockActionBarEl = createFloatingContainer();
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
// Tear down drag if it was active
|
|
2233
|
-
if (view.selDragCleanup) {
|
|
2234
|
-
view.selDragCleanup();
|
|
2235
|
-
view.selDragCleanup = null;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
if (!S.selection || (canvasMode !== "design" && canvasMode !== "edit")) {
|
|
2239
|
-
litRender(nothing, view.blockActionBarEl);
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
const activePanel = getActivePanel();
|
|
2244
|
-
if (!activePanel) {
|
|
2245
|
-
litRender(nothing, view.blockActionBarEl);
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
const el = findCanvasElement(S.selection, activePanel.canvas);
|
|
2249
|
-
const node = el && getNodeAtPath(S.document, S.selection);
|
|
2250
|
-
if (!el || !node) {
|
|
2251
|
-
litRender(nothing, view.blockActionBarEl);
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
const tag = (node.tagName ?? "div").toLowerCase();
|
|
2256
|
-
const elRect = el.getBoundingClientRect();
|
|
2257
|
-
const topPos = elRect.top < 80 ? elRect.bottom + 4 : elRect.top - 38;
|
|
2258
|
-
|
|
2259
|
-
// Inline format state
|
|
2260
|
-
const inlineEditing = isEditing() || el.contentEditable === "true";
|
|
2261
|
-
const actions = getInlineActions(tag) || [];
|
|
2262
|
-
const showFormat = inlineEditing && actions.length > 0;
|
|
2263
|
-
const activeValues = showFormat
|
|
2264
|
-
? actions.filter((a) => isTagActiveInSelection(a.tag, el)).map((a) => a.tag)
|
|
2265
|
-
: [];
|
|
2266
|
-
|
|
2267
|
-
litRender(
|
|
2268
|
-
html`
|
|
2269
|
-
<div
|
|
2270
|
-
class="block-action-bar"
|
|
2271
|
-
style="left:${elRect.left}px; top:${topPos}px"
|
|
2272
|
-
@mousedown=${onBarMousedown}
|
|
2273
|
-
>
|
|
2274
|
-
${S.selection.length >= 2 ? renderParentSelector() : nothing}
|
|
2275
|
-
|
|
2276
|
-
<span class="bar-tag">${node.$id || (node.tagName ?? "div")}</span>
|
|
2277
|
-
|
|
2278
|
-
${S.selection.length >= 2
|
|
2279
|
-
? html`<span class="bar-drag-handle" title="Drag to reorder">⡇</span>`
|
|
2280
|
-
: nothing}
|
|
2281
|
-
${S.selection.length >= 2 ? renderMoveArrows() : nothing}
|
|
2282
|
-
${S.selection.length >= 2 && node.tagName
|
|
2283
|
-
? (() => {
|
|
2284
|
-
const isComp =
|
|
2285
|
-
node.tagName.includes("-") &&
|
|
2286
|
-
componentRegistry.some((/** @type {any} */ c) => c.tagName === node.tagName);
|
|
2287
|
-
if (isComp) {
|
|
2288
|
-
const comp = componentRegistry.find(
|
|
2289
|
-
(/** @type {any} */ c) => c.tagName === node.tagName,
|
|
2290
|
-
);
|
|
2291
|
-
return html`<sp-action-button
|
|
2292
|
-
size="xs"
|
|
2293
|
-
quiet
|
|
2294
|
-
title="Edit Component"
|
|
2295
|
-
@click=${() => navigateToComponent(comp.path)}
|
|
2296
|
-
><sp-icon-edit slot="icon" size="xs"></sp-icon-edit
|
|
2297
|
-
></sp-action-button>`;
|
|
2298
|
-
}
|
|
2299
|
-
return html`<sp-action-button
|
|
2300
|
-
size="xs"
|
|
2301
|
-
quiet
|
|
2302
|
-
title="Convert to Component"
|
|
2303
|
-
@click=${() => convertToComponent(S)}
|
|
2304
|
-
><sp-icon-box slot="icon" size="xs"></sp-icon-box
|
|
2305
|
-
></sp-action-button>`;
|
|
2306
|
-
})()
|
|
2307
|
-
: nothing}
|
|
2308
|
-
${showFormat
|
|
2309
|
-
? html`
|
|
2310
|
-
<sp-divider size="s" vertical></sp-divider>
|
|
2311
|
-
<sp-action-group
|
|
2312
|
-
size="xs"
|
|
2313
|
-
compact
|
|
2314
|
-
emphasized
|
|
2315
|
-
selects="multiple"
|
|
2316
|
-
selected=${activeValues.length ? JSON.stringify(activeValues) : nothing}
|
|
2317
|
-
>
|
|
2318
|
-
${actions.map(
|
|
2319
|
-
(action) => html`
|
|
2320
|
-
<sp-action-button
|
|
2321
|
-
size="xs"
|
|
2322
|
-
value=${action.tag}
|
|
2323
|
-
title="${action.label}${action.shortcut ? ` (${action.shortcut})` : ""}"
|
|
2324
|
-
@mousedown=${captureSelectionRange}
|
|
2325
|
-
@click=${(/** @type {any} */ e) => onFormatClick(e, action)}
|
|
2326
|
-
>
|
|
2327
|
-
${formatIconMap[action.icon] ?? nothing}
|
|
2328
|
-
</sp-action-button>
|
|
2329
|
-
`,
|
|
2330
|
-
)}
|
|
2331
|
-
</sp-action-group>
|
|
2332
|
-
`
|
|
2333
|
-
: nothing}
|
|
2334
|
-
</div>
|
|
2335
|
-
`,
|
|
2336
|
-
view.blockActionBarEl,
|
|
2337
|
-
);
|
|
2338
|
-
|
|
2339
|
-
// Post-render side effects
|
|
2340
|
-
requestAnimationFrame(() => {
|
|
2341
|
-
const bar = view.blockActionBarEl?.firstElementChild;
|
|
2342
|
-
if (!bar) return;
|
|
2343
|
-
// Clamp to window
|
|
2344
|
-
const barRect = bar.getBoundingClientRect();
|
|
2345
|
-
if (barRect.right > window.innerWidth) {
|
|
2346
|
-
bar.style.left = `${Math.max(0, window.innerWidth - barRect.width)}px`;
|
|
2347
|
-
}
|
|
2348
|
-
// Attach drag handle
|
|
2349
|
-
if (S.selection.length >= 2) {
|
|
2350
|
-
const handle = bar.querySelector(".bar-drag-handle");
|
|
2351
|
-
if (handle) {
|
|
2352
|
-
if (view.selDragCleanup) {
|
|
2353
|
-
view.selDragCleanup();
|
|
2354
|
-
view.selDragCleanup = null;
|
|
2355
|
-
}
|
|
2356
|
-
view.selDragCleanup = draggable({
|
|
2357
|
-
element: handle,
|
|
2358
|
-
getInitialData: () => ({ type: "tree-node", path: S.selection }),
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
1954
|
// ── Pseudo-state preview ──────────────────────────────────────────────────────
|
|
2366
1955
|
// When a pseudo-selector (:hover, :focus, etc.) is active in the style sidebar,
|
|
2367
1956
|
// force those styles onto the selected element so the user can see the result.
|
|
@@ -2690,6 +2279,24 @@ function registerPanelEvents(panel) {
|
|
|
2690
2279
|
},
|
|
2691
2280
|
opts,
|
|
2692
2281
|
);
|
|
2282
|
+
|
|
2283
|
+
// Mount insertion helper — positioned via CSS Anchor Positioning
|
|
2284
|
+
insertionHelper.mount({
|
|
2285
|
+
getState: () => S,
|
|
2286
|
+
update,
|
|
2287
|
+
getCanvasMode: () => canvasMode,
|
|
2288
|
+
withPanelPointerEvents,
|
|
2289
|
+
effectiveZoom,
|
|
2290
|
+
defaultDef,
|
|
2291
|
+
insertNode,
|
|
2292
|
+
selectNode,
|
|
2293
|
+
parentElementPath,
|
|
2294
|
+
childIndex,
|
|
2295
|
+
getNodeAtPath,
|
|
2296
|
+
elToPath,
|
|
2297
|
+
panel,
|
|
2298
|
+
});
|
|
2299
|
+
view.canvasEventCleanups.push(() => insertionHelper.unmount());
|
|
2693
2300
|
}
|
|
2694
2301
|
|
|
2695
2302
|
// ─── Inline editing bridge ────────────────────────────────────────────────────
|
|
@@ -3197,2907 +2804,24 @@ function renderLeftPanel() {
|
|
|
3197
2804
|
leftPanelMod.render();
|
|
3198
2805
|
}
|
|
3199
2806
|
|
|
3200
|
-
|
|
3201
|
-
function registerLayersDnD() {
|
|
3202
|
-
requestAnimationFrame(() => {
|
|
3203
|
-
const container = /** @type {any} */ (leftPanel)?.querySelector(".layers-container");
|
|
3204
|
-
if (!container) return;
|
|
3205
|
-
|
|
3206
|
-
container.querySelectorAll("[data-dnd-row]").forEach(
|
|
3207
|
-
/** @param {any} row */ (row) => {
|
|
3208
|
-
const rowPath = /** @type {string} */ (row.dataset.path)
|
|
3209
|
-
.split("/")
|
|
3210
|
-
.map((/** @type {any} */ s) => (/^\d+$/.test(s) ? parseInt(s) : s));
|
|
3211
|
-
const rowDepth = parseInt(/** @type {string} */ (row.dataset.dndDepth)) || 0;
|
|
3212
|
-
const isVoid = row.hasAttribute("data-dnd-void");
|
|
3213
|
-
|
|
3214
|
-
const cleanup = combine(
|
|
3215
|
-
draggable({
|
|
3216
|
-
element: row,
|
|
3217
|
-
canDrag(/** @type {any} */ { element: _el, input }) {
|
|
3218
|
-
// Prevent drag when clicking action buttons
|
|
3219
|
-
const target = /** @type {HTMLElement} */ (
|
|
3220
|
-
document.elementFromPoint(input.clientX, input.clientY)
|
|
3221
|
-
);
|
|
3222
|
-
if (target?.closest(".layer-actions")) return false;
|
|
3223
|
-
return true;
|
|
3224
|
-
},
|
|
3225
|
-
getInitialData() {
|
|
3226
|
-
return { type: "tree-node", path: rowPath };
|
|
3227
|
-
},
|
|
3228
|
-
onDragStart() {
|
|
3229
|
-
row.classList.add("dragging");
|
|
3230
|
-
view.layerDragSourceHeight = row.offsetHeight;
|
|
3231
|
-
},
|
|
3232
|
-
onDrop() {
|
|
3233
|
-
row.classList.remove("dragging");
|
|
3234
|
-
},
|
|
3235
|
-
}),
|
|
3236
|
-
dropTargetForElements({
|
|
3237
|
-
element: row,
|
|
3238
|
-
canDrop(/** @type {any} */ { source }) {
|
|
3239
|
-
const srcPath = source.data.path;
|
|
3240
|
-
if (srcPath && isAncestor(srcPath, rowPath)) return false;
|
|
3241
|
-
return true;
|
|
3242
|
-
},
|
|
3243
|
-
getData(/** @type {any} */ { input, element }) {
|
|
3244
|
-
return attachInstruction(
|
|
3245
|
-
{ path: rowPath },
|
|
3246
|
-
/** @type {any} */ ({
|
|
3247
|
-
input,
|
|
3248
|
-
element,
|
|
3249
|
-
currentLevel: rowDepth,
|
|
3250
|
-
indentPerLevel: 16,
|
|
3251
|
-
block: isVoid ? ["make-child"] : [],
|
|
3252
|
-
}),
|
|
3253
|
-
);
|
|
3254
|
-
},
|
|
3255
|
-
onDragEnter(/** @type {any} */ { self }) {
|
|
3256
|
-
showLayerDropGap(row, self.data, container);
|
|
3257
|
-
},
|
|
3258
|
-
onDrag(/** @type {any} */ { self }) {
|
|
3259
|
-
showLayerDropGap(row, self.data, container);
|
|
3260
|
-
},
|
|
3261
|
-
onDragLeave() {
|
|
3262
|
-
clearLayerDropGap(container);
|
|
3263
|
-
},
|
|
3264
|
-
onDrop() {
|
|
3265
|
-
clearLayerDropGap(container);
|
|
3266
|
-
},
|
|
3267
|
-
}),
|
|
3268
|
-
);
|
|
3269
|
-
view.dndCleanups.push(cleanup);
|
|
3270
|
-
},
|
|
3271
|
-
);
|
|
3272
|
-
|
|
3273
|
-
// Global monitor
|
|
3274
|
-
const monitorCleanup = monitorForElements({
|
|
3275
|
-
onDrop(/** @type {any} */ { source, location }) {
|
|
3276
|
-
clearLayerDropGap(container);
|
|
3277
|
-
const target = location.current.dropTargets[0];
|
|
3278
|
-
if (!target) return;
|
|
3279
|
-
const instruction = extractInstruction(target.data);
|
|
3280
|
-
if (!instruction || instruction.type === "instruction-blocked") return;
|
|
3281
|
-
const srcData = source.data;
|
|
3282
|
-
const targetPath = target.data.path;
|
|
3283
|
-
applyDropInstruction(instruction, srcData, targetPath);
|
|
3284
|
-
},
|
|
3285
|
-
});
|
|
3286
|
-
view.dndCleanups.push(monitorCleanup);
|
|
3287
|
-
});
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
/** Register DnD on component rows — called from renderLeftPanel when tab=components */
|
|
3291
|
-
function registerComponentsDnD() {
|
|
3292
|
-
requestAnimationFrame(() => {
|
|
3293
|
-
const container = /** @type {any} */ (leftPanel)?.querySelector(".components-section");
|
|
3294
|
-
if (!container) return;
|
|
3295
|
-
|
|
3296
|
-
container.querySelectorAll("[data-component-tag]").forEach(
|
|
3297
|
-
/** @param {any} row */ (row) => {
|
|
3298
|
-
const tagName = row.dataset.componentTag;
|
|
3299
|
-
if (!tagName) return;
|
|
3300
|
-
const comp = componentRegistry.find(/** @param {any} c */ (c) => c.tagName === tagName);
|
|
3301
|
-
if (!comp) return;
|
|
3302
|
-
|
|
3303
|
-
// Fill preview with live rendered component
|
|
3304
|
-
const preview = row.querySelector(".element-card-preview");
|
|
3305
|
-
if (preview && !preview.querySelector(tagName)) {
|
|
3306
|
-
renderComponentPreview(comp).then((el) => {
|
|
3307
|
-
preview.textContent = "";
|
|
3308
|
-
preview.appendChild(el);
|
|
3309
|
-
});
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
const instanceDef = {
|
|
3313
|
-
tagName: comp.tagName,
|
|
3314
|
-
$props: Object.fromEntries(
|
|
3315
|
-
comp.props.map((/** @type {any} */ p) => [
|
|
3316
|
-
p.name,
|
|
3317
|
-
p.default !== undefined ? p.default : "",
|
|
3318
|
-
]),
|
|
3319
|
-
),
|
|
3320
|
-
};
|
|
3321
|
-
const cleanup = draggable({
|
|
3322
|
-
element: row,
|
|
3323
|
-
getInitialData() {
|
|
3324
|
-
return { type: "block", fragment: structuredClone(instanceDef) };
|
|
3325
|
-
},
|
|
3326
|
-
});
|
|
3327
|
-
view.dndCleanups.push(cleanup);
|
|
3328
|
-
},
|
|
3329
|
-
);
|
|
3330
|
-
});
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
/** @type {any} */
|
|
3334
|
-
|
|
3335
|
-
/**
|
|
3336
|
-
* @param {any} rowEl
|
|
3337
|
-
* @param {any} data
|
|
3338
|
-
* @param {any} container
|
|
3339
|
-
*/
|
|
3340
|
-
function showLayerDropGap(rowEl, data, container) {
|
|
3341
|
-
const instruction = extractInstruction(data);
|
|
3342
|
-
|
|
3343
|
-
// Clear previous drop-target highlight
|
|
3344
|
-
if (view._currentDropTargetRow && view._currentDropTargetRow !== rowEl) {
|
|
3345
|
-
view._currentDropTargetRow.classList.remove("drop-target");
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
if (!instruction || instruction.type === "instruction-blocked") {
|
|
3349
|
-
clearLayerDropGap(container);
|
|
3350
|
-
return;
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
if (instruction.type === "make-child") {
|
|
3354
|
-
clearLayerDropGap(container);
|
|
3355
|
-
rowEl.classList.add("drop-target");
|
|
3356
|
-
view._currentDropTargetRow = rowEl;
|
|
3357
|
-
return;
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
rowEl.classList.remove("drop-target");
|
|
3361
|
-
view._currentDropTargetRow = rowEl;
|
|
3362
|
-
|
|
3363
|
-
// Shift rows to create gap
|
|
3364
|
-
const rows = Array.from(container.querySelectorAll(".layers-tree .layer-row"));
|
|
3365
|
-
const targetIdx = rows.indexOf(rowEl);
|
|
3366
|
-
const gap = view.layerDragSourceHeight;
|
|
3367
|
-
|
|
3368
|
-
for (let i = 0; i < rows.length; i++) {
|
|
3369
|
-
if (rows[i].classList.contains("dragging")) continue;
|
|
3370
|
-
if (instruction.type === "reorder-above") {
|
|
3371
|
-
rows[i].style.transform = i >= targetIdx ? `translateY(${gap}px)` : "";
|
|
3372
|
-
} else {
|
|
3373
|
-
rows[i].style.transform = i > targetIdx ? `translateY(${gap}px)` : "";
|
|
3374
|
-
}
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
/** @param {any} container */
|
|
3379
|
-
function clearLayerDropGap(container) {
|
|
3380
|
-
if (view._currentDropTargetRow) {
|
|
3381
|
-
view._currentDropTargetRow.classList.remove("drop-target");
|
|
3382
|
-
view._currentDropTargetRow = null;
|
|
3383
|
-
}
|
|
3384
|
-
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
3385
|
-
for (const r of rows) r.style.transform = "";
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
/**
|
|
3389
|
-
* Select a tag in the stylebook — shared by layers panel click and canvas click.
|
|
3390
|
-
*
|
|
3391
|
-
* @param {string} tag
|
|
3392
|
-
* @param {string | null} [media]
|
|
3393
|
-
*/
|
|
3394
|
-
function selectStylebookTag(tag, media) {
|
|
3395
|
-
updateSession({
|
|
3396
|
-
selection: [],
|
|
3397
|
-
ui: {
|
|
3398
|
-
stylebookSelection: tag,
|
|
3399
|
-
rightTab: "style",
|
|
3400
|
-
activeSelector: `& ${tag}`,
|
|
3401
|
-
...(media !== undefined ? { activeMedia: media } : {}),
|
|
3402
|
-
},
|
|
3403
|
-
});
|
|
3404
|
-
renderStylebookOverlays();
|
|
3405
|
-
requestAnimationFrame(() => {
|
|
3406
|
-
if (canvasPanels.length > 0) {
|
|
3407
|
-
const el = findStylebookEl(canvasPanels[0].canvas, tag);
|
|
3408
|
-
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3409
|
-
}
|
|
3410
|
-
});
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
/**
|
|
3414
|
-
* Apply a DnD instruction to the state
|
|
3415
|
-
*
|
|
3416
|
-
* @param {any} instruction
|
|
3417
|
-
* @param {any} srcData
|
|
3418
|
-
* @param {any} targetPath
|
|
3419
|
-
*/
|
|
3420
|
-
function applyDropInstruction(instruction, srcData, targetPath) {
|
|
3421
|
-
if (srcData.type === "tree-node") {
|
|
3422
|
-
const fromPath = srcData.path;
|
|
3423
|
-
const targetParent = /** @type {any} */ (parentElementPath(targetPath));
|
|
3424
|
-
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
3425
|
-
|
|
3426
|
-
switch (instruction.type) {
|
|
3427
|
-
case "reorder-above":
|
|
3428
|
-
update(moveNode(S, fromPath, targetParent, targetIdx));
|
|
3429
|
-
break;
|
|
3430
|
-
case "reorder-below":
|
|
3431
|
-
update(moveNode(S, fromPath, targetParent, targetIdx + 1));
|
|
3432
|
-
break;
|
|
3433
|
-
case "make-child": {
|
|
3434
|
-
const target = getNodeAtPath(S.document, targetPath);
|
|
3435
|
-
const len = target?.children?.length || 0;
|
|
3436
|
-
update(moveNode(S, fromPath, targetPath, len));
|
|
3437
|
-
break;
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
} else if (srcData.type === "block") {
|
|
3441
|
-
const targetParent = /** @type {any} */ (parentElementPath(targetPath));
|
|
3442
|
-
const targetIdx = /** @type {number} */ (childIndex(targetPath));
|
|
3443
|
-
|
|
3444
|
-
switch (instruction.type) {
|
|
3445
|
-
case "reorder-above":
|
|
3446
|
-
update(insertNode(S, targetParent, targetIdx, structuredClone(srcData.fragment)));
|
|
3447
|
-
break;
|
|
3448
|
-
case "reorder-below":
|
|
3449
|
-
update(insertNode(S, targetParent, targetIdx + 1, structuredClone(srcData.fragment)));
|
|
3450
|
-
break;
|
|
3451
|
-
case "make-child": {
|
|
3452
|
-
const target = getNodeAtPath(S.document, targetPath);
|
|
3453
|
-
const len = target?.children?.length || 0;
|
|
3454
|
-
update(insertNode(S, targetPath, len, structuredClone(srcData.fragment)));
|
|
3455
|
-
break;
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
|
|
3459
|
-
// Auto-import to $elements if the dropped block is a custom component
|
|
3460
|
-
const tag = srcData.fragment?.tagName;
|
|
3461
|
-
if (tag && tag.includes("-")) {
|
|
3462
|
-
const comp = componentRegistry.find((/** @type {any} */ c) => c.tagName === tag);
|
|
3463
|
-
if (comp) {
|
|
3464
|
-
const elements = S.document.$elements || [];
|
|
3465
|
-
if (comp.source === "npm") {
|
|
3466
|
-
// npm web component: add cherry-picked subpath specifier
|
|
3467
|
-
const specifier = comp.modulePath ? `${comp.package}/${comp.modulePath}` : comp.package;
|
|
3468
|
-
const alreadyImported = elements.some(
|
|
3469
|
-
(/** @type {any} */ e) => e === specifier || e === comp.package,
|
|
3470
|
-
);
|
|
3471
|
-
if (!alreadyImported) {
|
|
3472
|
-
S = applyMutation(S, (/** @type {any} */ doc) => {
|
|
3473
|
-
if (!doc.$elements) doc.$elements = [];
|
|
3474
|
-
doc.$elements.push(specifier);
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
|
-
} else {
|
|
3478
|
-
// JX component: add $ref object
|
|
3479
|
-
const alreadyImported = elements.some(
|
|
3480
|
-
(/** @type {any} */ e) =>
|
|
3481
|
-
e.$ref &&
|
|
3482
|
-
(e.$ref === `./${comp.path}` || e.$ref.endsWith(comp.path.split("/").pop())),
|
|
3483
|
-
);
|
|
3484
|
-
if (!alreadyImported) {
|
|
3485
|
-
const relPath = computeRelativePath(S.documentPath, comp.path);
|
|
3486
|
-
S = applyMutation(S, (/** @type {any} */ doc) => {
|
|
3487
|
-
if (!doc.$elements) doc.$elements = [];
|
|
3488
|
-
doc.$elements.push({ $ref: relPath });
|
|
3489
|
-
});
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
2807
|
+
// ─── DnD registration: delegated to panels/dnd.js ───────────────────────────
|
|
3496
2808
|
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
*
|
|
3500
|
-
* @param {any} tag
|
|
3501
|
-
*/
|
|
3502
|
-
function defaultDef(tag) {
|
|
3503
|
-
/** @type {any} */
|
|
3504
|
-
const def = { tagName: tag };
|
|
3505
|
-
if (/^h[1-6]$/.test(tag)) def.textContent = "Heading";
|
|
3506
|
-
else if (tag === "p") def.textContent = "Paragraph text";
|
|
3507
|
-
else if (
|
|
3508
|
-
tag === "span" ||
|
|
3509
|
-
tag === "strong" ||
|
|
3510
|
-
tag === "em" ||
|
|
3511
|
-
tag === "small" ||
|
|
3512
|
-
tag === "mark" ||
|
|
3513
|
-
tag === "code" ||
|
|
3514
|
-
tag === "abbr" ||
|
|
3515
|
-
tag === "q" ||
|
|
3516
|
-
tag === "sub" ||
|
|
3517
|
-
tag === "sup" ||
|
|
3518
|
-
tag === "time"
|
|
3519
|
-
)
|
|
3520
|
-
def.textContent = "Text";
|
|
3521
|
-
else if (tag === "a") {
|
|
3522
|
-
def.textContent = "Link";
|
|
3523
|
-
def.attributes = { href: "#" };
|
|
3524
|
-
} else if (tag === "button") def.textContent = "Button";
|
|
3525
|
-
else if (tag === "label") def.textContent = "Label";
|
|
3526
|
-
else if (tag === "legend") def.textContent = "Legend";
|
|
3527
|
-
else if (tag === "caption") def.textContent = "Caption";
|
|
3528
|
-
else if (tag === "summary") def.textContent = "Summary";
|
|
3529
|
-
else if (
|
|
3530
|
-
tag === "li" ||
|
|
3531
|
-
tag === "dt" ||
|
|
3532
|
-
tag === "dd" ||
|
|
3533
|
-
tag === "th" ||
|
|
3534
|
-
tag === "td" ||
|
|
3535
|
-
tag === "option"
|
|
3536
|
-
)
|
|
3537
|
-
def.textContent = "Item";
|
|
3538
|
-
else if (tag === "blockquote") def.textContent = "Quote";
|
|
3539
|
-
else if (tag === "pre") def.textContent = "Preformatted text";
|
|
3540
|
-
else if (tag === "input") def.attributes = { type: "text", placeholder: "Enter text..." };
|
|
3541
|
-
else if (tag === "img") def.attributes = { src: "", alt: "Image" };
|
|
3542
|
-
else if (tag === "iframe") def.attributes = { src: "" };
|
|
3543
|
-
else if (tag === "select") def.children = [{ tagName: "option", textContent: "Option 1" }];
|
|
3544
|
-
else if (tag === "ul" || tag === "ol") def.children = [{ tagName: "li", textContent: "Item" }];
|
|
3545
|
-
else if (tag === "dl")
|
|
3546
|
-
def.children = [
|
|
3547
|
-
{ tagName: "dt", textContent: "Term" },
|
|
3548
|
-
{ tagName: "dd", textContent: "Definition" },
|
|
3549
|
-
];
|
|
3550
|
-
else if (tag === "table")
|
|
3551
|
-
def.children = [
|
|
3552
|
-
{
|
|
3553
|
-
tagName: "thead",
|
|
3554
|
-
children: [{ tagName: "tr", children: [{ tagName: "th", textContent: "Header" }] }],
|
|
3555
|
-
},
|
|
3556
|
-
{
|
|
3557
|
-
tagName: "tbody",
|
|
3558
|
-
children: [{ tagName: "tr", children: [{ tagName: "td", textContent: "Cell" }] }],
|
|
3559
|
-
},
|
|
3560
|
-
];
|
|
3561
|
-
else if (tag === "details")
|
|
3562
|
-
def.children = [
|
|
3563
|
-
{ tagName: "summary", textContent: "Summary" },
|
|
3564
|
-
{ tagName: "p", textContent: "Detail content" },
|
|
3565
|
-
];
|
|
3566
|
-
return def;
|
|
3567
|
-
}
|
|
2809
|
+
// ─── Stylebook ───────────────────────────────────────────────────────────────
|
|
2810
|
+
// Extracted to panels/stylebook-panel.js
|
|
3568
2811
|
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
function registerElementsDnD() {
|
|
3572
|
-
requestAnimationFrame(() => {
|
|
3573
|
-
const container = /** @type {any} */ (leftPanel)?.querySelector(".panel-body");
|
|
3574
|
-
if (!container) return;
|
|
3575
|
-
container.querySelectorAll("[data-block-tag]").forEach(
|
|
3576
|
-
/** @param {any} row */ (row) => {
|
|
3577
|
-
const tag = row.dataset.blockTag;
|
|
3578
|
-
const preview = row.querySelector(".element-card-preview");
|
|
3579
|
-
if (preview && !preview.firstChild) {
|
|
3580
|
-
const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
|
|
3581
|
-
el.textContent = tag;
|
|
3582
|
-
preview.appendChild(el);
|
|
3583
|
-
}
|
|
3584
|
-
const def = defaultDef(tag);
|
|
3585
|
-
const cleanup = draggable({
|
|
3586
|
-
element: row,
|
|
3587
|
-
getInitialData() {
|
|
3588
|
-
return { type: "block", fragment: structuredClone(def) };
|
|
3589
|
-
},
|
|
3590
|
-
});
|
|
3591
|
-
view.dndCleanups.push(cleanup);
|
|
3592
|
-
},
|
|
3593
|
-
);
|
|
3594
|
-
});
|
|
3595
|
-
}
|
|
2812
|
+
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
2813
|
+
// Extracted to panels/properties-panel.js
|
|
3596
2814
|
|
|
3597
|
-
// ───
|
|
2815
|
+
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
3598
2816
|
|
|
3599
|
-
|
|
2817
|
+
// UNIT_RE — imported from ui/unit-selector.js
|
|
3600
2818
|
|
|
3601
|
-
|
|
3602
|
-
* Build a DOM element tree from a stylebook-meta.json entry. Applies any existing tag-scoped styles
|
|
3603
|
-
* from rootStyle["& tag"].
|
|
3604
|
-
*
|
|
3605
|
-
* @param {any} entry
|
|
3606
|
-
* @param {any} rootStyle
|
|
3607
|
-
* @param {any} activeBreakpoints
|
|
3608
|
-
*/
|
|
3609
|
-
function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
3610
|
-
const el = document.createElement(entry.tag);
|
|
3611
|
-
if (entry.text) el.textContent = entry.text;
|
|
3612
|
-
if (entry.attributes) {
|
|
3613
|
-
for (const [k, v] of Object.entries(entry.attributes)) {
|
|
3614
|
-
try {
|
|
3615
|
-
el.setAttribute(k, /** @type {string} */ (v));
|
|
3616
|
-
} catch {}
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
if (entry.style) el.style.cssText = entry.style;
|
|
3620
|
-
// Apply custom styles from document root
|
|
3621
|
-
const tagStyle = rootStyle[`& ${entry.tag}`];
|
|
3622
|
-
if (tagStyle) {
|
|
3623
|
-
for (const [prop, val] of Object.entries(tagStyle)) {
|
|
3624
|
-
if (typeof val === "string" || typeof val === "number") {
|
|
3625
|
-
try {
|
|
3626
|
-
/** @type {any} */ (el.style)[prop] = val;
|
|
3627
|
-
} catch {}
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
// Apply media overrides for active breakpoints
|
|
3631
|
-
if (activeBreakpoints) {
|
|
3632
|
-
for (const [key, val] of Object.entries(tagStyle)) {
|
|
3633
|
-
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
3634
|
-
const mediaName = key.slice(1);
|
|
3635
|
-
if (mediaName === "--") continue;
|
|
3636
|
-
if (activeBreakpoints.has(mediaName)) {
|
|
3637
|
-
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
3638
|
-
if (typeof v === "string" || typeof v === "number") {
|
|
3639
|
-
try {
|
|
3640
|
-
/** @type {any} */ (el.style)[prop] = v;
|
|
3641
|
-
} catch {}
|
|
3642
|
-
}
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
if (entry.children) {
|
|
3649
|
-
for (const child of entry.children) {
|
|
3650
|
-
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3653
|
-
return el;
|
|
3654
|
-
}
|
|
2819
|
+
// inferInputType — imported from studio-utils.js
|
|
3655
2820
|
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
* back to a placeholder if registration fails.
|
|
3659
|
-
*
|
|
3660
|
-
* @param {any} comp - Entry from componentRegistry ({ tagName, path, props })
|
|
3661
|
-
* @returns {Promise<HTMLElement>}
|
|
3662
|
-
*/
|
|
3663
|
-
async function renderComponentPreview(comp) {
|
|
3664
|
-
try {
|
|
3665
|
-
if (comp.source === "npm") {
|
|
3666
|
-
// npm components are already imported via $elements — check if registered
|
|
3667
|
-
if (!customElements.get(comp.tagName)) {
|
|
3668
|
-
throw new Error("not registered");
|
|
3669
|
-
}
|
|
3670
|
-
} else {
|
|
3671
|
-
const root = projectState?.projectRoot;
|
|
3672
|
-
const url = `${location.origin}/${root ? root + "/" : ""}${comp.path}`;
|
|
3673
|
-
await defineElement(url);
|
|
3674
|
-
}
|
|
3675
|
-
const el = document.createElement(comp.tagName);
|
|
3676
|
-
for (const p of comp.props || []) {
|
|
3677
|
-
if (p.default !== undefined && p.default !== "false" && p.default !== "''") {
|
|
3678
|
-
const val = String(p.default).replace(/^'|'$/g, "");
|
|
3679
|
-
el.setAttribute(p.name, val);
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
return el;
|
|
3683
|
-
} catch (/** @type {any} */ e) {
|
|
3684
|
-
console.warn("Component preview failed:", comp.tagName, e);
|
|
3685
|
-
const fallback = document.createElement("div");
|
|
3686
|
-
fallback.style.cssText =
|
|
3687
|
-
"padding:12px;border:1px dashed var(--border);border-radius:4px;color:var(--fg-dim)";
|
|
3688
|
-
fallback.textContent = `<${comp.tagName}>`;
|
|
3689
|
-
return fallback;
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
2821
|
+
// ─── Style panel ────────────────────────────────────────────────────────────
|
|
2822
|
+
// Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
|
|
3692
2823
|
|
|
3693
|
-
|
|
3694
|
-
* @param {any} rootStyle
|
|
3695
|
-
* @param {any} tag
|
|
3696
|
-
*/
|
|
3697
|
-
function hasTagStyle(rootStyle, tag) {
|
|
3698
|
-
const s = rootStyle[`& ${tag}`];
|
|
3699
|
-
return s && typeof s === "object" && Object.keys(s).length > 0;
|
|
3700
|
-
}
|
|
3701
|
-
|
|
3702
|
-
function renderSettings() {
|
|
3703
|
-
const settingsTab = S.ui.settingsTab || "stylebook";
|
|
3704
|
-
|
|
3705
|
-
// Top-level settings tabs chrome bar
|
|
3706
|
-
const settingsChromeBarTpl = html`
|
|
3707
|
-
<div
|
|
3708
|
-
class="sb-chrome settings-top-chrome"
|
|
3709
|
-
style="position:absolute;top:0;left:0;right:0;z-index:16;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
3710
|
-
>
|
|
3711
|
-
<sp-tabs
|
|
3712
|
-
size="s"
|
|
3713
|
-
selected=${settingsTab}
|
|
3714
|
-
@change=${(/** @type {any} */ e) => {
|
|
3715
|
-
updateUi("settingsTab", e.target.selected);
|
|
3716
|
-
}}
|
|
3717
|
-
>
|
|
3718
|
-
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
3719
|
-
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
3720
|
-
<sp-tab label="Collections" value="collections"></sp-tab>
|
|
3721
|
-
</sp-tabs>
|
|
3722
|
-
</div>
|
|
3723
|
-
`;
|
|
3724
|
-
|
|
3725
|
-
// Non-stylebook tabs: render editor into canvasWrap with offset for chrome bar
|
|
3726
|
-
if (settingsTab === "definitions" || settingsTab === "collections") {
|
|
3727
|
-
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
3728
|
-
|
|
3729
|
-
litRender(
|
|
3730
|
-
html`${settingsChromeBarTpl}
|
|
3731
|
-
<div
|
|
3732
|
-
class="settings-editor-container"
|
|
3733
|
-
style="position:absolute;inset:40px 0 0 0;overflow:auto"
|
|
3734
|
-
></div>`,
|
|
3735
|
-
/** @type {any} */ (canvasWrap),
|
|
3736
|
-
);
|
|
3737
|
-
|
|
3738
|
-
const container = /** @type {HTMLElement} */ (
|
|
3739
|
-
canvasWrap.querySelector(".settings-editor-container")
|
|
3740
|
-
);
|
|
3741
|
-
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
3742
|
-
else renderCollectionsEditor(container);
|
|
3743
|
-
return;
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
// Stylebook tab — existing behavior
|
|
3747
|
-
view.stylebookElToTag = new WeakMap();
|
|
3748
|
-
const rootStyle = getEffectiveStyle(S.document.style);
|
|
3749
|
-
const filter = (S.ui.stylebookFilter || "").toLowerCase();
|
|
3750
|
-
const customizedOnly = S.ui.stylebookCustomizedOnly;
|
|
3751
|
-
|
|
3752
|
-
const { sizeBreakpoints, baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
3753
|
-
const hasMedia = sizeBreakpoints.length > 0;
|
|
3754
|
-
|
|
3755
|
-
// Chrome bar (tabs + filter) — positioned absolutely above the panzoom surface
|
|
3756
|
-
const onTabClick = (/** @type {string} */ t) => {
|
|
3757
|
-
updateUi("stylebookTab", t);
|
|
3758
|
-
};
|
|
3759
|
-
|
|
3760
|
-
const onFilterInput = (/** @type {any} */ e) => {
|
|
3761
|
-
updateUi("stylebookFilter", e.target.value);
|
|
3762
|
-
};
|
|
3763
|
-
|
|
3764
|
-
const onCustomizedToggle = () => {
|
|
3765
|
-
updateUi("stylebookCustomizedOnly", !S.ui.stylebookCustomizedOnly);
|
|
3766
|
-
};
|
|
3767
|
-
|
|
3768
|
-
const chromeBarTpl = html`
|
|
3769
|
-
${settingsChromeBarTpl}
|
|
3770
|
-
<div
|
|
3771
|
-
class="sb-chrome"
|
|
3772
|
-
style="position:absolute;top:36px;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
3773
|
-
>
|
|
3774
|
-
<sp-tabs
|
|
3775
|
-
size="s"
|
|
3776
|
-
selected=${S.ui.stylebookTab || "elements"}
|
|
3777
|
-
@change=${(/** @type {any} */ e) => {
|
|
3778
|
-
onTabClick(e.target.selected);
|
|
3779
|
-
}}
|
|
3780
|
-
>
|
|
3781
|
-
${["elements", "variables"].map(
|
|
3782
|
-
(t) => html`
|
|
3783
|
-
<sp-tab label=${t.charAt(0).toUpperCase() + t.slice(1)} value=${t}></sp-tab>
|
|
3784
|
-
`,
|
|
3785
|
-
)}
|
|
3786
|
-
</sp-tabs>
|
|
3787
|
-
${S.ui.stylebookTab === "elements"
|
|
3788
|
-
? html`
|
|
3789
|
-
<input
|
|
3790
|
-
class="field-input"
|
|
3791
|
-
style="flex:1;max-width:200px;margin-left:8px"
|
|
3792
|
-
placeholder="Filter…"
|
|
3793
|
-
.value=${S.ui.stylebookFilter}
|
|
3794
|
-
@input=${onFilterInput}
|
|
3795
|
-
/>
|
|
3796
|
-
<button
|
|
3797
|
-
class="tb-toggle${S.ui.stylebookCustomizedOnly ? " active" : ""}"
|
|
3798
|
-
style="margin-left:4px"
|
|
3799
|
-
@click=${onCustomizedToggle}
|
|
3800
|
-
>
|
|
3801
|
-
Customized
|
|
3802
|
-
</button>
|
|
3803
|
-
`
|
|
3804
|
-
: nothing}
|
|
3805
|
-
</div>
|
|
3806
|
-
`;
|
|
3807
|
-
|
|
3808
|
-
// Set up panzoom surface — same as normal canvas mode
|
|
3809
|
-
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
3810
|
-
|
|
3811
|
-
// Build panel definitions
|
|
3812
|
-
/** @type {any[]} */
|
|
3813
|
-
const allPanelDefs = [];
|
|
3814
|
-
if (hasMedia) {
|
|
3815
|
-
allPanelDefs.push({
|
|
3816
|
-
name: "base",
|
|
3817
|
-
displayName: mediaDisplayName("--"),
|
|
3818
|
-
width: baseWidth,
|
|
3819
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
|
|
3820
|
-
});
|
|
3821
|
-
for (const bp of sizeBreakpoints) {
|
|
3822
|
-
allPanelDefs.push({
|
|
3823
|
-
name: bp.name,
|
|
3824
|
-
displayName: mediaDisplayName(bp.name),
|
|
3825
|
-
width: bp.width,
|
|
3826
|
-
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
3827
|
-
});
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
|
|
3831
|
-
// Render content into panels
|
|
3832
|
-
const renderIntoPanel = (/** @type {any} */ panel, /** @type {any} */ activeBreakpoints) => {
|
|
3833
|
-
panel.canvas.classList.add("sb-canvas");
|
|
3834
|
-
if (S.ui.stylebookTab === "elements") {
|
|
3835
|
-
renderStylebookElementsIntoCanvas(
|
|
3836
|
-
panel.canvas,
|
|
3837
|
-
rootStyle,
|
|
3838
|
-
filter,
|
|
3839
|
-
customizedOnly,
|
|
3840
|
-
activeBreakpoints,
|
|
3841
|
-
);
|
|
3842
|
-
for (const child of panel.canvas.querySelectorAll("*")) {
|
|
3843
|
-
child.style.pointerEvents = "none";
|
|
3844
|
-
}
|
|
3845
|
-
registerStylebookPanelEvents(panel);
|
|
3846
|
-
} else {
|
|
3847
|
-
renderStylebookVarsIntoCanvas(panel.canvas, rootStyle);
|
|
3848
|
-
panel.overlayClk.style.pointerEvents = "none";
|
|
3849
|
-
}
|
|
3850
|
-
};
|
|
3851
|
-
|
|
3852
|
-
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
3853
|
-
let panelEntries;
|
|
3854
|
-
if (!hasMedia) {
|
|
3855
|
-
// Single panel
|
|
3856
|
-
const effectiveMedia = getEffectiveMedia(S.document.$media);
|
|
3857
|
-
const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
|
|
3858
|
-
const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
|
|
3859
|
-
const entry = canvasPanelTemplate(
|
|
3860
|
-
hasBaseWidth ? "base" : null,
|
|
3861
|
-
label,
|
|
3862
|
-
!hasBaseWidth,
|
|
3863
|
-
hasBaseWidth ? baseWidth : undefined,
|
|
3864
|
-
);
|
|
3865
|
-
panelEntries = [{ tpl: entry.tpl, panel: entry.panel, activeSet: new Set() }];
|
|
3866
|
-
} else {
|
|
3867
|
-
// Multi-panel: one per breakpoint, sorted widest first
|
|
3868
|
-
panelEntries = allPanelDefs.map((def) => {
|
|
3869
|
-
const label = `${def.displayName} (${def.width}px)`;
|
|
3870
|
-
const { tpl, panel } = canvasPanelTemplate(def.name, label, false, def.width);
|
|
3871
|
-
return { tpl, panel, activeSet: def.activeSet };
|
|
3872
|
-
});
|
|
3873
|
-
}
|
|
3874
|
-
|
|
3875
|
-
litRender(
|
|
3876
|
-
html`
|
|
3877
|
-
${chromeBarTpl}
|
|
3878
|
-
<div
|
|
3879
|
-
class="panzoom-wrap"
|
|
3880
|
-
style="transform-origin:0 0;padding-top:72px"
|
|
3881
|
-
${ref((el) => {
|
|
3882
|
-
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
3883
|
-
})}
|
|
3884
|
-
>
|
|
3885
|
-
${panelEntries.map((e) => e.tpl)}
|
|
3886
|
-
</div>
|
|
3887
|
-
`,
|
|
3888
|
-
/** @type {any} */ (canvasWrap),
|
|
3889
|
-
);
|
|
3890
|
-
|
|
3891
|
-
for (const { panel, activeSet } of panelEntries) {
|
|
3892
|
-
canvasPanels.push(panel);
|
|
3893
|
-
renderIntoPanel(panel, activeSet);
|
|
3894
|
-
}
|
|
3895
|
-
if (hasMedia) {
|
|
3896
|
-
updateActivePanelHeaders();
|
|
3897
|
-
}
|
|
3898
|
-
|
|
3899
|
-
applyTransform();
|
|
3900
|
-
observeCenterUntilStable();
|
|
3901
|
-
renderZoomIndicator();
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
/**
|
|
3905
|
-
* @param {any} canvasEl
|
|
3906
|
-
* @param {any} rootStyle
|
|
3907
|
-
* @param {any} filter
|
|
3908
|
-
* @param {any} customizedOnly
|
|
3909
|
-
* @param {any} activeBreakpoints
|
|
3910
|
-
*/
|
|
3911
|
-
function renderStylebookElementsIntoCanvas(
|
|
3912
|
-
canvasEl,
|
|
3913
|
-
rootStyle,
|
|
3914
|
-
filter,
|
|
3915
|
-
customizedOnly,
|
|
3916
|
-
activeBreakpoints,
|
|
3917
|
-
) {
|
|
3918
|
-
/** @type {import("lit-html").TemplateResult[]} */
|
|
3919
|
-
const sectionTemplates = [];
|
|
3920
|
-
|
|
3921
|
-
for (const section of stylebookMeta.$sections) {
|
|
3922
|
-
// Filter elements
|
|
3923
|
-
let entries = /** @type {any} */ (section.elements);
|
|
3924
|
-
if (filter) {
|
|
3925
|
-
entries = entries.filter(
|
|
3926
|
-
(/** @type {any} */ e) =>
|
|
3927
|
-
e.tag.includes(filter) || section.label.toLowerCase().includes(filter),
|
|
3928
|
-
);
|
|
3929
|
-
}
|
|
3930
|
-
if (customizedOnly) {
|
|
3931
|
-
entries = entries.filter((/** @type {any} */ e) => hasTagStyle(rootStyle, e.tag));
|
|
3932
|
-
}
|
|
3933
|
-
if (entries.length === 0) continue;
|
|
3934
|
-
|
|
3935
|
-
const cardTemplates = entries.map((/** @type {any} */ entry) => {
|
|
3936
|
-
const el = buildStylebookElement(entry, rootStyle, activeBreakpoints);
|
|
3937
|
-
return html`
|
|
3938
|
-
<div
|
|
3939
|
-
class="element-card"
|
|
3940
|
-
${ref((card) => {
|
|
3941
|
-
if (!card) return;
|
|
3942
|
-
view.stylebookElToTag.set(card, entry.tag);
|
|
3943
|
-
elToPath.set(card, ["__sb", entry.tag]);
|
|
3944
|
-
for (const child of el.querySelectorAll("*")) {
|
|
3945
|
-
const tag = child.tagName.toLowerCase();
|
|
3946
|
-
if (!view.stylebookElToTag.has(child)) {
|
|
3947
|
-
view.stylebookElToTag.set(child, tag);
|
|
3948
|
-
elToPath.set(child, ["__sb", tag]);
|
|
3949
|
-
}
|
|
3950
|
-
}
|
|
3951
|
-
})}
|
|
3952
|
-
>
|
|
3953
|
-
<div
|
|
3954
|
-
class="element-card-preview"
|
|
3955
|
-
${ref((c) => {
|
|
3956
|
-
if (c && !c.firstChild) c.appendChild(el);
|
|
3957
|
-
})}
|
|
3958
|
-
></div>
|
|
3959
|
-
<div class="element-card-label"><${entry.tag}></div>
|
|
3960
|
-
</div>
|
|
3961
|
-
`;
|
|
3962
|
-
});
|
|
3963
|
-
|
|
3964
|
-
sectionTemplates.push(html`
|
|
3965
|
-
<div class="sb-section">
|
|
3966
|
-
<div class="sb-label">${section.label}</div>
|
|
3967
|
-
<div class="sb-body">${cardTemplates}</div>
|
|
3968
|
-
</div>
|
|
3969
|
-
`);
|
|
3970
|
-
}
|
|
3971
|
-
|
|
3972
|
-
// Custom components from registry
|
|
3973
|
-
if (componentRegistry.length > 0) {
|
|
3974
|
-
let comps = componentRegistry;
|
|
3975
|
-
if (filter)
|
|
3976
|
-
comps = comps.filter((/** @type {any} */ c) => c.tagName.toLowerCase().includes(filter));
|
|
3977
|
-
if (customizedOnly)
|
|
3978
|
-
comps = comps.filter((/** @type {any} */ c) => hasTagStyle(rootStyle, c.tagName));
|
|
3979
|
-
if (comps.length > 0) {
|
|
3980
|
-
const compCards = comps.map((/** @type {any} */ comp) => {
|
|
3981
|
-
/** @type {HTMLDivElement | null} */
|
|
3982
|
-
let previewEl = null;
|
|
3983
|
-
const cardTpl = html`
|
|
3984
|
-
<div
|
|
3985
|
-
class="element-card"
|
|
3986
|
-
style="display:inline-flex;width:auto"
|
|
3987
|
-
${ref((card) => {
|
|
3988
|
-
if (!card) return;
|
|
3989
|
-
view.stylebookElToTag.set(card, comp.tagName);
|
|
3990
|
-
elToPath.set(card, ["__sb", comp.tagName]);
|
|
3991
|
-
})}
|
|
3992
|
-
>
|
|
3993
|
-
<div
|
|
3994
|
-
class="element-card-preview"
|
|
3995
|
-
${ref((c) => {
|
|
3996
|
-
if (c) previewEl = /** @type {HTMLDivElement} */ (c);
|
|
3997
|
-
})}
|
|
3998
|
-
></div>
|
|
3999
|
-
<div class="element-card-label"><${comp.tagName}></div>
|
|
4000
|
-
</div>
|
|
4001
|
-
`;
|
|
4002
|
-
// Fill preview asynchronously with live rendered component
|
|
4003
|
-
renderComponentPreview(comp).then((el) => {
|
|
4004
|
-
if (previewEl) previewEl.appendChild(el);
|
|
4005
|
-
});
|
|
4006
|
-
return cardTpl;
|
|
4007
|
-
});
|
|
4008
|
-
|
|
4009
|
-
sectionTemplates.push(html`
|
|
4010
|
-
<div class="sb-section">
|
|
4011
|
-
<div class="sb-label">Components</div>
|
|
4012
|
-
<div class="sb-body">${compCards}</div>
|
|
4013
|
-
</div>
|
|
4014
|
-
`);
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
|
|
4018
|
-
if (sectionTemplates.length === 0) {
|
|
4019
|
-
litRender(
|
|
4020
|
-
html`
|
|
4021
|
-
<div style="padding:48px;text-align:center;color:var(--fg-dim);font-size:13px">
|
|
4022
|
-
${customizedOnly ? "No customized elements" : "No matching elements"}
|
|
4023
|
-
</div>
|
|
4024
|
-
`,
|
|
4025
|
-
canvasEl,
|
|
4026
|
-
);
|
|
4027
|
-
} else {
|
|
4028
|
-
litRender(html`${sectionTemplates}`, canvasEl);
|
|
4029
|
-
}
|
|
4030
|
-
}
|
|
4031
|
-
|
|
4032
|
-
/**
|
|
4033
|
-
* Render variables into the canvas (card-based layout matching Elements tab)
|
|
4034
|
-
*
|
|
4035
|
-
* @param {any} canvasEl
|
|
4036
|
-
* @param {any} rootStyle
|
|
4037
|
-
*/
|
|
4038
|
-
function renderStylebookVarsIntoCanvas(canvasEl, rootStyle) {
|
|
4039
|
-
const varCats = stylebookMeta.$variables;
|
|
4040
|
-
|
|
4041
|
-
/** @type {Record<string, any>} */
|
|
4042
|
-
const groups = {};
|
|
4043
|
-
for (const key of Object.keys(varCats)) groups[key] = [];
|
|
4044
|
-
for (const [k, v] of Object.entries(rootStyle)) {
|
|
4045
|
-
if (!k.startsWith("--")) continue;
|
|
4046
|
-
if (typeof v !== "string" && typeof v !== "number") continue;
|
|
4047
|
-
if (k.startsWith("--color")) groups.color.push([k, v]);
|
|
4048
|
-
else if (k.startsWith("--font")) groups.font.push([k, v]);
|
|
4049
|
-
else if (k.startsWith("--size") || k.startsWith("--spacing") || k.startsWith("--radius"))
|
|
4050
|
-
groups.size.push([k, v]);
|
|
4051
|
-
else groups.other.push([k, v]);
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
|
-
/** @type {Map<string, HTMLElement | null>} */
|
|
4055
|
-
const bodyRefs = new Map();
|
|
4056
|
-
|
|
4057
|
-
const sectionTemplates = Object.entries(varCats).map(([catKey, catMeta]) => {
|
|
4058
|
-
const vars = groups[catKey];
|
|
4059
|
-
|
|
4060
|
-
const onAdd = () => {
|
|
4061
|
-
const bodyEl = bodyRefs.get(catKey);
|
|
4062
|
-
if (!bodyEl) return;
|
|
4063
|
-
const addBtn = bodyEl.querySelector(".sb-var-add-btn");
|
|
4064
|
-
const row = renderVarRow(catKey, /** @type {any} */ (catMeta), null, "", true);
|
|
4065
|
-
bodyEl.insertBefore(row, addBtn);
|
|
4066
|
-
if (addBtn) /** @type {any} */ (addBtn).style.display = "none";
|
|
4067
|
-
const nameField = /** @type {any} */ (row.querySelector("sp-textfield"));
|
|
4068
|
-
if (nameField) requestAnimationFrame(() => nameField.focus());
|
|
4069
|
-
};
|
|
4070
|
-
|
|
4071
|
-
return html`
|
|
4072
|
-
<div class="sb-section">
|
|
4073
|
-
<div class="sb-label">${/** @type {any} */ (catMeta).label}</div>
|
|
4074
|
-
<div
|
|
4075
|
-
class="sb-body"
|
|
4076
|
-
${ref((el) => {
|
|
4077
|
-
if (el) bodyRefs.set(catKey, /** @type {HTMLElement} */ (el));
|
|
4078
|
-
})}
|
|
4079
|
-
>
|
|
4080
|
-
${vars.length > 0
|
|
4081
|
-
? vars.map((/** @type {[string, any]} */ [varName, varVal]) =>
|
|
4082
|
-
renderVarRow(catKey, /** @type {any} */ (catMeta), varName, String(varVal), false),
|
|
4083
|
-
)
|
|
4084
|
-
: html`<div class="sb-var-empty">
|
|
4085
|
-
No ${/** @type {any} */ (catMeta).label.toLowerCase()} variables yet.
|
|
4086
|
-
</div>`}
|
|
4087
|
-
<button class="sb-var-add-btn" @click=${onAdd}>
|
|
4088
|
-
<span class="sb-var-add-icon">+</span> Add ${/** @type {any} */ (catMeta).label}
|
|
4089
|
-
</button>
|
|
4090
|
-
</div>
|
|
4091
|
-
</div>
|
|
4092
|
-
`;
|
|
4093
|
-
});
|
|
4094
|
-
|
|
4095
|
-
litRender(html`${sectionTemplates}`, canvasEl);
|
|
4096
|
-
}
|
|
4097
|
-
|
|
4098
|
-
/**
|
|
4099
|
-
* Render a single variable row — used for both existing and add-new.
|
|
4100
|
-
*
|
|
4101
|
-
* @param {string} catKey - "color"|"font"|"size"|"other"
|
|
4102
|
-
* @param {any} catMeta - { label, prefix, placeholder }
|
|
4103
|
-
* @param {string | null} varName - Existing var name, or null for add-new
|
|
4104
|
-
* @param {string} varVal - Current value, or "" for add-new
|
|
4105
|
-
* @param {boolean} isNew - True if this is an add-new row
|
|
4106
|
-
*/
|
|
4107
|
-
function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
4108
|
-
const row = document.createElement("div");
|
|
4109
|
-
row.className = isNew ? "sb-var-row is-new" : "sb-var-row";
|
|
4110
|
-
|
|
4111
|
-
/** @type {any} */
|
|
4112
|
-
let colorPicker = null;
|
|
4113
|
-
/** @type {any} */
|
|
4114
|
-
let nameField = null;
|
|
4115
|
-
/** @type {any} */
|
|
4116
|
-
let getValueFn;
|
|
4117
|
-
/** @type {any} */
|
|
4118
|
-
let hexField = null;
|
|
4119
|
-
|
|
4120
|
-
// ─── Color swatch setup ───
|
|
4121
|
-
const swatchTpl =
|
|
4122
|
-
catKey === "color"
|
|
4123
|
-
? html`
|
|
4124
|
-
<div
|
|
4125
|
-
class="sb-var-swatch"
|
|
4126
|
-
style=${styleMap({ backgroundColor: varVal || "var(--accent)" })}
|
|
4127
|
-
>
|
|
4128
|
-
<input
|
|
4129
|
-
type="color"
|
|
4130
|
-
.value=${varVal && varVal.startsWith("#") ? varVal : "#007acc"}
|
|
4131
|
-
${ref((el) => {
|
|
4132
|
-
if (el) colorPicker = el;
|
|
4133
|
-
})}
|
|
4134
|
-
@input=${() => {
|
|
4135
|
-
if (!colorPicker || !hexField) return;
|
|
4136
|
-
hexField.value = colorPicker.value;
|
|
4137
|
-
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
4138
|
-
if (swatch) swatch.style.backgroundColor = colorPicker.value;
|
|
4139
|
-
if (!isNew && varName) update(updateStyle(S, [], varName, colorPicker.value));
|
|
4140
|
-
}}
|
|
4141
|
-
/>
|
|
4142
|
-
</div>
|
|
4143
|
-
`
|
|
4144
|
-
: nothing;
|
|
4145
|
-
|
|
4146
|
-
// ─── Name column (add-new only) ───
|
|
4147
|
-
const namePlaceholder =
|
|
4148
|
-
catKey === "color"
|
|
4149
|
-
? "Primary Blue"
|
|
4150
|
-
: catKey === "font"
|
|
4151
|
-
? "Body Serif"
|
|
4152
|
-
: catKey === "size"
|
|
4153
|
-
? "Spacing Large"
|
|
4154
|
-
: "Border Radius";
|
|
4155
|
-
|
|
4156
|
-
const nameColTpl = isNew
|
|
4157
|
-
? html`
|
|
4158
|
-
<div class="sb-var-col-name">
|
|
4159
|
-
<div class="sb-var-col-label">Name</div>
|
|
4160
|
-
<sp-textfield
|
|
4161
|
-
size="s"
|
|
4162
|
-
placeholder=${namePlaceholder}
|
|
4163
|
-
style="pointer-events:auto"
|
|
4164
|
-
${ref((el) => {
|
|
4165
|
-
if (el) nameField = el;
|
|
4166
|
-
})}
|
|
4167
|
-
></sp-textfield>
|
|
4168
|
-
</div>
|
|
4169
|
-
`
|
|
4170
|
-
: nothing;
|
|
4171
|
-
|
|
4172
|
-
// ─── Value column ───
|
|
4173
|
-
/** @type {any} */
|
|
4174
|
-
let valueContent;
|
|
4175
|
-
|
|
4176
|
-
if (catKey === "color") {
|
|
4177
|
-
/** @type {any} */
|
|
4178
|
-
let debounce;
|
|
4179
|
-
valueContent = html`
|
|
4180
|
-
<sp-textfield
|
|
4181
|
-
size="s"
|
|
4182
|
-
.value=${varVal || "#007acc"}
|
|
4183
|
-
placeholder="#007acc"
|
|
4184
|
-
style="pointer-events:auto"
|
|
4185
|
-
${ref((el) => {
|
|
4186
|
-
if (el) hexField = el;
|
|
4187
|
-
})}
|
|
4188
|
-
@input=${() => {
|
|
4189
|
-
clearTimeout(debounce);
|
|
4190
|
-
debounce = setTimeout(() => {
|
|
4191
|
-
if (!hexField) return;
|
|
4192
|
-
const v = hexField.value;
|
|
4193
|
-
try {
|
|
4194
|
-
if (colorPicker) colorPicker.value = v.startsWith("#") ? v : colorPicker.value;
|
|
4195
|
-
} catch {}
|
|
4196
|
-
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
4197
|
-
if (swatch) swatch.style.backgroundColor = v;
|
|
4198
|
-
if (!isNew && varName) update(updateStyle(S, [], varName, v));
|
|
4199
|
-
}, 400);
|
|
4200
|
-
}}
|
|
4201
|
-
></sp-textfield>
|
|
4202
|
-
`;
|
|
4203
|
-
getValueFn = () => hexField?.value?.trim() || "";
|
|
4204
|
-
} else if (catKey === "size") {
|
|
4205
|
-
const ui = createUnitInput(varVal || "16px", {
|
|
4206
|
-
onChange: (/** @type {any} */ newVal) => {
|
|
4207
|
-
const bar = /** @type {any} */ (row.querySelector(".sb-var-size-bar"));
|
|
4208
|
-
if (bar) bar.style.width = newVal;
|
|
4209
|
-
if (!isNew && varName) update(updateStyle(S, [], varName, newVal));
|
|
4210
|
-
},
|
|
4211
|
-
});
|
|
4212
|
-
if (isNew) ui.textfield.value = "";
|
|
4213
|
-
valueContent = html`<div
|
|
4214
|
-
${ref((el) => {
|
|
4215
|
-
if (el && !el.firstChild) el.appendChild(ui.wrap);
|
|
4216
|
-
})}
|
|
4217
|
-
></div>`;
|
|
4218
|
-
getValueFn = () => ui.getValue();
|
|
4219
|
-
} else {
|
|
4220
|
-
/** @type {any} */
|
|
4221
|
-
let textFieldEl = null;
|
|
4222
|
-
/** @type {any} */
|
|
4223
|
-
let debounce;
|
|
4224
|
-
valueContent = html`
|
|
4225
|
-
<sp-textfield
|
|
4226
|
-
size="s"
|
|
4227
|
-
.value=${varVal}
|
|
4228
|
-
placeholder=${catMeta.placeholder}
|
|
4229
|
-
style="pointer-events:auto"
|
|
4230
|
-
${ref((el) => {
|
|
4231
|
-
if (el) textFieldEl = el;
|
|
4232
|
-
})}
|
|
4233
|
-
@input=${() => {
|
|
4234
|
-
if (!textFieldEl || isNew || !varName) return;
|
|
4235
|
-
clearTimeout(debounce);
|
|
4236
|
-
debounce = setTimeout(() => {
|
|
4237
|
-
const v = textFieldEl.value;
|
|
4238
|
-
const fontPrev = /** @type {any} */ (row.querySelector(".sb-var-font-preview"));
|
|
4239
|
-
if (fontPrev) fontPrev.style.fontFamily = v;
|
|
4240
|
-
update(updateStyle(S, [], varName, v));
|
|
4241
|
-
}, 400);
|
|
4242
|
-
}}
|
|
4243
|
-
></sp-textfield>
|
|
4244
|
-
`;
|
|
4245
|
-
getValueFn = () => textFieldEl?.value?.trim() || "";
|
|
4246
|
-
}
|
|
4247
|
-
|
|
4248
|
-
const valColTpl = html`
|
|
4249
|
-
<div class="sb-var-col-value">
|
|
4250
|
-
${isNew ? html`<div class="sb-var-col-label">Value</div>` : nothing} ${valueContent}
|
|
4251
|
-
</div>
|
|
4252
|
-
`;
|
|
4253
|
-
|
|
4254
|
-
// ─── Action buttons (add-new only) ───
|
|
4255
|
-
const actionsTpl = isNew
|
|
4256
|
-
? html`
|
|
4257
|
-
<div class="sb-var-add-actions">
|
|
4258
|
-
<sp-action-button
|
|
4259
|
-
size="s"
|
|
4260
|
-
style="pointer-events:auto"
|
|
4261
|
-
@click=${() => {
|
|
4262
|
-
const name = (nameField?.value || "").trim();
|
|
4263
|
-
const val = getValueFn();
|
|
4264
|
-
const generatedVar = friendlyNameToVar(name, catMeta.prefix);
|
|
4265
|
-
if (!generatedVar || !val) return;
|
|
4266
|
-
update(updateStyle(S, [], generatedVar, val));
|
|
4267
|
-
}}
|
|
4268
|
-
>Add</sp-action-button
|
|
4269
|
-
>
|
|
4270
|
-
<sp-action-button
|
|
4271
|
-
size="s"
|
|
4272
|
-
quiet
|
|
4273
|
-
style="pointer-events:auto"
|
|
4274
|
-
@click=${() => {
|
|
4275
|
-
const body = row.parentElement;
|
|
4276
|
-
row.remove();
|
|
4277
|
-
const addBtn = /** @type {any} */ (body?.querySelector(".sb-var-add-btn"));
|
|
4278
|
-
if (addBtn) addBtn.style.display = "";
|
|
4279
|
-
}}
|
|
4280
|
-
>
|
|
4281
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
4282
|
-
</sp-action-button>
|
|
4283
|
-
</div>
|
|
4284
|
-
`
|
|
4285
|
-
: nothing;
|
|
4286
|
-
|
|
4287
|
-
// ─── Header ───
|
|
4288
|
-
const headerTpl =
|
|
4289
|
-
!isNew && varName
|
|
4290
|
-
? html`
|
|
4291
|
-
<div class="sb-var-row-header">
|
|
4292
|
-
<span class="sb-var-row-title">${varDisplayName(varName, catMeta.prefix)}</span>
|
|
4293
|
-
<span class="sb-var-row-ref">${varName}</span>
|
|
4294
|
-
<sp-action-button
|
|
4295
|
-
size="s"
|
|
4296
|
-
quiet
|
|
4297
|
-
class="sb-var-del"
|
|
4298
|
-
style="pointer-events:auto"
|
|
4299
|
-
@click=${() => update(updateStyle(S, [], varName, undefined))}
|
|
4300
|
-
>
|
|
4301
|
-
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
4302
|
-
</sp-action-button>
|
|
4303
|
-
</div>
|
|
4304
|
-
`
|
|
4305
|
-
: nothing;
|
|
4306
|
-
|
|
4307
|
-
// ─── Live preview of generated var name (add-new) ───
|
|
4308
|
-
const addPreviewTpl = isNew
|
|
4309
|
-
? html`
|
|
4310
|
-
<div
|
|
4311
|
-
class="sb-var-add-preview"
|
|
4312
|
-
${ref((el) => {
|
|
4313
|
-
if (!el || !nameField) return;
|
|
4314
|
-
nameField.addEventListener("input", () => {
|
|
4315
|
-
el.textContent = friendlyNameToVar(nameField.value || "", catMeta.prefix);
|
|
4316
|
-
});
|
|
4317
|
-
})}
|
|
4318
|
-
></div>
|
|
4319
|
-
`
|
|
4320
|
-
: nothing;
|
|
4321
|
-
|
|
4322
|
-
// ─── Type-specific preview ───
|
|
4323
|
-
const typePrevTpl =
|
|
4324
|
-
catKey === "font" && varVal
|
|
4325
|
-
? html`
|
|
4326
|
-
<div class="sb-var-preview">
|
|
4327
|
-
<div class="sb-var-font-preview" style=${styleMap({ fontFamily: varVal })}>
|
|
4328
|
-
The quick brown fox jumps over the lazy dog
|
|
4329
|
-
</div>
|
|
4330
|
-
</div>
|
|
4331
|
-
`
|
|
4332
|
-
: catKey === "size" && varVal
|
|
4333
|
-
? html`
|
|
4334
|
-
<div class="sb-var-preview">
|
|
4335
|
-
<div class="sb-var-size-track">
|
|
4336
|
-
<div class="sb-var-size-bar" style=${styleMap({ width: varVal })}></div>
|
|
4337
|
-
</div>
|
|
4338
|
-
</div>
|
|
4339
|
-
`
|
|
4340
|
-
: nothing;
|
|
4341
|
-
|
|
4342
|
-
litRender(
|
|
4343
|
-
html`
|
|
4344
|
-
${headerTpl}
|
|
4345
|
-
<div class="sb-var-input-row">${swatchTpl} ${nameColTpl} ${valColTpl} ${actionsTpl}</div>
|
|
4346
|
-
${addPreviewTpl} ${typePrevTpl}
|
|
4347
|
-
`,
|
|
4348
|
-
row,
|
|
4349
|
-
);
|
|
4350
|
-
|
|
4351
|
-
return row;
|
|
4352
|
-
}
|
|
4353
|
-
|
|
4354
|
-
// varDisplayName, friendlyNameToVar — imported from studio-utils.js
|
|
4355
|
-
|
|
4356
|
-
/**
|
|
4357
|
-
* Convert a human-friendly name like "Tablet" to a $media key "--tablet"
|
|
4358
|
-
*
|
|
4359
|
-
* @param {any} name
|
|
4360
|
-
*/
|
|
4361
|
-
function friendlyNameToMedia(name) {
|
|
4362
|
-
return friendlyNameToVar(name, "--");
|
|
4363
|
-
}
|
|
4364
|
-
|
|
4365
|
-
/**
|
|
4366
|
-
* Creates a combined textfield + quiet sp-picker for CSS values with units. Returns { wrap,
|
|
4367
|
-
* textfield, picker, getValue, setValue }. The picker hides automatically when the textfield value
|
|
4368
|
-
* is non-numeric (e.g. "auto", "inherit").
|
|
4369
|
-
*
|
|
4370
|
-
* @param {any} initialValue
|
|
4371
|
-
* @param {any} [options]
|
|
4372
|
-
*/
|
|
4373
|
-
function createUnitInput(initialValue, { onChange, size = "s" } = {}) {
|
|
4374
|
-
const match = String(initialValue).match(
|
|
4375
|
-
/^(-?[\d.]+)\s*(px|em|rem|vw|vh|%|ch|ex|vmin|vmax|pt|cm|mm|in)?$/,
|
|
4376
|
-
);
|
|
4377
|
-
let numVal = match ? match[1] : initialValue;
|
|
4378
|
-
let unitVal = match ? match[2] || "px" : "";
|
|
4379
|
-
const isNumeric = !!match;
|
|
4380
|
-
|
|
4381
|
-
const wrap = document.createElement("div");
|
|
4382
|
-
wrap.className = "sb-unit-input";
|
|
4383
|
-
wrap.style.pointerEvents = "auto";
|
|
4384
|
-
|
|
4385
|
-
/** @type {any} */
|
|
4386
|
-
let textfield = null;
|
|
4387
|
-
/** @type {any} */
|
|
4388
|
-
let picker = null;
|
|
4389
|
-
|
|
4390
|
-
const units = [
|
|
4391
|
-
{ value: "px", label: "px" },
|
|
4392
|
-
{ value: "rem", label: "rem" },
|
|
4393
|
-
{ value: "em", label: "em" },
|
|
4394
|
-
{ value: "%", label: "%" },
|
|
4395
|
-
{ value: "vw", label: "vw" },
|
|
4396
|
-
{ value: "vh", label: "vh" },
|
|
4397
|
-
{ value: "ch", label: "ch" },
|
|
4398
|
-
{ value: "pt", label: "pt" },
|
|
4399
|
-
{ divider: true },
|
|
4400
|
-
{ value: "auto", label: "auto" },
|
|
4401
|
-
{ value: "fit-content", label: "fit-content" },
|
|
4402
|
-
];
|
|
4403
|
-
|
|
4404
|
-
/** @type {any} */
|
|
4405
|
-
let debounce;
|
|
4406
|
-
|
|
4407
|
-
function getValue() {
|
|
4408
|
-
const num = textfield?.value;
|
|
4409
|
-
const unit = picker?.value;
|
|
4410
|
-
if (unit === "auto" || unit === "fit-content") return unit;
|
|
4411
|
-
return num ? `${num}${unit}` : "";
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
litRender(
|
|
4415
|
-
html`
|
|
4416
|
-
<sp-textfield
|
|
4417
|
-
.value=${numVal}
|
|
4418
|
-
size=${size}
|
|
4419
|
-
${ref((el) => {
|
|
4420
|
-
if (el) textfield = el;
|
|
4421
|
-
})}
|
|
4422
|
-
@input=${() => {
|
|
4423
|
-
clearTimeout(debounce);
|
|
4424
|
-
const raw = textfield?.value?.trim();
|
|
4425
|
-
const looksNumeric = /^-?[\d.]+$/.test(raw || "");
|
|
4426
|
-
if (picker) picker.style.display = looksNumeric ? "" : "none";
|
|
4427
|
-
debounce = setTimeout(() => {
|
|
4428
|
-
if (onChange) onChange(looksNumeric ? `${raw}${picker?.value}` : raw);
|
|
4429
|
-
}, 400);
|
|
4430
|
-
}}
|
|
4431
|
-
></sp-textfield>
|
|
4432
|
-
<sp-picker
|
|
4433
|
-
quiet
|
|
4434
|
-
size=${size}
|
|
4435
|
-
style=${styleMap({ display: isNumeric ? "" : "none" })}
|
|
4436
|
-
${ref((el) => {
|
|
4437
|
-
if (el) {
|
|
4438
|
-
picker = el;
|
|
4439
|
-
requestAnimationFrame(() => {
|
|
4440
|
-
/** @type {any} */ (el).value = unitVal || "px";
|
|
4441
|
-
});
|
|
4442
|
-
}
|
|
4443
|
-
})}
|
|
4444
|
-
@change=${() => {
|
|
4445
|
-
const unit = picker?.value;
|
|
4446
|
-
if (unit === "auto" || unit === "fit-content") {
|
|
4447
|
-
if (textfield) textfield.value = unit;
|
|
4448
|
-
if (picker) picker.style.display = "none";
|
|
4449
|
-
if (onChange) onChange(unit);
|
|
4450
|
-
} else {
|
|
4451
|
-
unitVal = unit;
|
|
4452
|
-
if (onChange) onChange(getValue());
|
|
4453
|
-
}
|
|
4454
|
-
}}
|
|
4455
|
-
>
|
|
4456
|
-
${units.map((u) =>
|
|
4457
|
-
u.divider
|
|
4458
|
-
? html`<sp-menu-divider></sp-menu-divider>`
|
|
4459
|
-
: html`<sp-menu-item value=${u.value}>${u.label}</sp-menu-item>`,
|
|
4460
|
-
)}
|
|
4461
|
-
</sp-picker>
|
|
4462
|
-
`,
|
|
4463
|
-
wrap,
|
|
4464
|
-
);
|
|
4465
|
-
|
|
4466
|
-
return { wrap, textfield, picker, getValue };
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
/**
|
|
4470
|
-
* Click handler for stylebook canvas — selects elements via the elToPath/view.stylebookElToTag
|
|
4471
|
-
* mapping
|
|
4472
|
-
*
|
|
4473
|
-
* @param {any} panel
|
|
4474
|
-
*/
|
|
4475
|
-
function registerStylebookPanelEvents(panel) {
|
|
4476
|
-
const { canvas, overlayClk } = panel;
|
|
4477
|
-
|
|
4478
|
-
overlayClk.addEventListener("click", (/** @type {any} */ e) => {
|
|
4479
|
-
// Temporarily enable pointer events to hit-test
|
|
4480
|
-
const els = canvas.querySelectorAll("*");
|
|
4481
|
-
for (const el of els) el.style.pointerEvents = "auto";
|
|
4482
|
-
overlayClk.style.display = "none";
|
|
4483
|
-
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
4484
|
-
overlayClk.style.display = "";
|
|
4485
|
-
for (const el of els) el.style.pointerEvents = "none";
|
|
4486
|
-
|
|
4487
|
-
// Find the closest element with a stylebook tag mapping (walk up parents for nested elements)
|
|
4488
|
-
for (const el of elements) {
|
|
4489
|
-
if (!canvas.contains(el) || el === canvas) continue;
|
|
4490
|
-
let cur = /** @type {any} */ (el);
|
|
4491
|
-
while (cur && cur !== canvas) {
|
|
4492
|
-
const tag = view.stylebookElToTag.get(cur);
|
|
4493
|
-
if (tag) {
|
|
4494
|
-
const newMedia = panel.mediaName === "base" ? null : (panel.mediaName ?? null);
|
|
4495
|
-
selectStylebookTag(tag, newMedia);
|
|
4496
|
-
updateActivePanelHeaders();
|
|
4497
|
-
return;
|
|
4498
|
-
}
|
|
4499
|
-
cur = cur.parentElement;
|
|
4500
|
-
}
|
|
4501
|
-
}
|
|
4502
|
-
// Clicked empty area — deselect
|
|
4503
|
-
updateSession({ ui: { stylebookSelection: null, activeSelector: null } });
|
|
4504
|
-
renderStylebookOverlays();
|
|
4505
|
-
});
|
|
4506
|
-
|
|
4507
|
-
overlayClk.addEventListener("mousemove", (/** @type {any} */ e) => {
|
|
4508
|
-
const els = canvas.querySelectorAll("*");
|
|
4509
|
-
for (const el of els) el.style.pointerEvents = "auto";
|
|
4510
|
-
overlayClk.style.display = "none";
|
|
4511
|
-
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
4512
|
-
overlayClk.style.display = "";
|
|
4513
|
-
for (const el of els) el.style.pointerEvents = "none";
|
|
4514
|
-
|
|
4515
|
-
let hoverTag = null;
|
|
4516
|
-
for (const el of elements) {
|
|
4517
|
-
if (!canvas.contains(el) || el === canvas) continue;
|
|
4518
|
-
let cur = /** @type {any} */ (el);
|
|
4519
|
-
while (cur && cur !== canvas) {
|
|
4520
|
-
const tag = view.stylebookElToTag.get(cur);
|
|
4521
|
-
if (tag) {
|
|
4522
|
-
hoverTag = tag;
|
|
4523
|
-
break;
|
|
4524
|
-
}
|
|
4525
|
-
cur = cur.parentElement;
|
|
4526
|
-
}
|
|
4527
|
-
if (hoverTag) break;
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4530
|
-
if (hoverTag !== panel._lastHoverTag) {
|
|
4531
|
-
panel._lastHoverTag = hoverTag;
|
|
4532
|
-
renderStylebookOverlays();
|
|
4533
|
-
}
|
|
4534
|
-
});
|
|
4535
|
-
}
|
|
4536
|
-
|
|
4537
|
-
/** Draw selection + hover overlays for stylebook elements */
|
|
4538
|
-
function renderStylebookOverlays() {
|
|
4539
|
-
if (canvasPanels.length === 0) return;
|
|
4540
|
-
|
|
4541
|
-
const selectedTag = S.ui.stylebookSelection;
|
|
4542
|
-
|
|
4543
|
-
for (const panel of canvasPanels) {
|
|
4544
|
-
const hoverTag = panel._lastHoverTag;
|
|
4545
|
-
/**
|
|
4546
|
-
* @type {{
|
|
4547
|
-
* cls: string;
|
|
4548
|
-
* top: string;
|
|
4549
|
-
* left: string;
|
|
4550
|
-
* width: string;
|
|
4551
|
-
* height: string;
|
|
4552
|
-
* label?: string;
|
|
4553
|
-
* }[]}
|
|
4554
|
-
*/
|
|
4555
|
-
const boxes = [];
|
|
4556
|
-
|
|
4557
|
-
if (hoverTag && hoverTag !== selectedTag) {
|
|
4558
|
-
const el = findStylebookEl(panel.canvas, hoverTag);
|
|
4559
|
-
if (el) boxes.push({ ...overlayBoxDescriptor(el, "hover", panel), label: undefined });
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
if (selectedTag) {
|
|
4563
|
-
const el = findStylebookEl(panel.canvas, selectedTag);
|
|
4564
|
-
if (el)
|
|
4565
|
-
boxes.push({ ...overlayBoxDescriptor(el, "selection", panel), label: `<${selectedTag}>` });
|
|
4566
|
-
}
|
|
4567
|
-
|
|
4568
|
-
litRender(
|
|
4569
|
-
html`
|
|
4570
|
-
${panel.dropLine}
|
|
4571
|
-
${boxes.map(
|
|
4572
|
-
(b) => html`
|
|
4573
|
-
<div
|
|
4574
|
-
class=${b.cls}
|
|
4575
|
-
style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}"
|
|
4576
|
-
>
|
|
4577
|
-
${b.label ? html`<div class="overlay-label">${b.label}</div>` : nothing}
|
|
4578
|
-
</div>
|
|
4579
|
-
`,
|
|
4580
|
-
)}
|
|
4581
|
-
`,
|
|
4582
|
-
panel.overlay,
|
|
4583
|
-
);
|
|
4584
|
-
}
|
|
4585
|
-
}
|
|
4586
|
-
|
|
4587
|
-
/** Find a stylebook element by tag in the canvas */
|
|
4588
|
-
function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
|
|
4589
|
-
for (const child of canvasEl.querySelectorAll("*")) {
|
|
4590
|
-
if (view.stylebookElToTag.get(child) === tag) return child;
|
|
4591
|
-
}
|
|
4592
|
-
return null;
|
|
4593
|
-
}
|
|
4594
|
-
|
|
4595
|
-
// ─── Right panel: Inspector ───────────────────────────────────────────────────
|
|
4596
|
-
|
|
4597
|
-
function renderRightPanel() {
|
|
4598
|
-
rightPanelMod.render();
|
|
4599
|
-
}
|
|
4600
|
-
|
|
4601
|
-
// ─── Inspector ────────────────────────────────────────────────────────────────
|
|
4602
|
-
|
|
4603
|
-
/** Frontmatter-only panel shown in content mode when no element is selected */
|
|
4604
|
-
function renderFrontmatterOnlyPanel() {
|
|
4605
|
-
const fm = S.content?.frontmatter || {};
|
|
4606
|
-
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
4607
|
-
const schemaProps = col?.schema?.properties;
|
|
4608
|
-
const requiredFields = new Set(col?.schema?.required || []);
|
|
4609
|
-
|
|
4610
|
-
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
4611
|
-
const fields = [];
|
|
4612
|
-
if (schemaProps) {
|
|
4613
|
-
for (const [field, fieldSchema] of Object.entries(
|
|
4614
|
-
/** @type {Record<string, any>} */ (schemaProps),
|
|
4615
|
-
)) {
|
|
4616
|
-
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
4617
|
-
}
|
|
4618
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
4619
|
-
if (!schemaProps[field]) {
|
|
4620
|
-
fields.push({
|
|
4621
|
-
field,
|
|
4622
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
4623
|
-
value,
|
|
4624
|
-
});
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
} else {
|
|
4628
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
4629
|
-
fields.push({
|
|
4630
|
-
field,
|
|
4631
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
4632
|
-
value,
|
|
4633
|
-
});
|
|
4634
|
-
}
|
|
4635
|
-
}
|
|
4636
|
-
|
|
4637
|
-
if (fields.length === 0 && !schemaProps) {
|
|
4638
|
-
return html`<div class="empty-state">No frontmatter. Select an element to inspect.</div>`;
|
|
4639
|
-
}
|
|
4640
|
-
|
|
4641
|
-
return html`
|
|
4642
|
-
<div class="style-sidebar">
|
|
4643
|
-
<sp-accordion allow-multiple size="s">
|
|
4644
|
-
<sp-accordion-item label=${col ? `Frontmatter (${col.name})` : "Frontmatter"} open>
|
|
4645
|
-
<div class="style-section-body">
|
|
4646
|
-
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
4647
|
-
</div>
|
|
4648
|
-
</sp-accordion-item>
|
|
4649
|
-
</sp-accordion>
|
|
4650
|
-
</div>
|
|
4651
|
-
`;
|
|
4652
|
-
}
|
|
4653
|
-
|
|
4654
|
-
/** Render a single frontmatter field row (shared between both panels) */
|
|
4655
|
-
function renderFmFieldRow(
|
|
4656
|
-
/** @type {string} */ field,
|
|
4657
|
-
/** @type {any} */ entry,
|
|
4658
|
-
/** @type {any} */ value,
|
|
4659
|
-
/** @type {Set<string>} */ requiredFields,
|
|
4660
|
-
) {
|
|
4661
|
-
const isRequired = requiredFields.has(field);
|
|
4662
|
-
const label = field.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
4663
|
-
const displayLabel = label + (isRequired ? " *" : "");
|
|
4664
|
-
const hasVal = value !== undefined && value !== "" && value !== false;
|
|
4665
|
-
const onClear = () => update(updateFrontmatter(S, field, undefined));
|
|
4666
|
-
|
|
4667
|
-
// Boolean → checkbox
|
|
4668
|
-
if (entry.type === "boolean") {
|
|
4669
|
-
return renderFieldRow({
|
|
4670
|
-
prop: field,
|
|
4671
|
-
label: displayLabel,
|
|
4672
|
-
hasValue: hasVal,
|
|
4673
|
-
onClear,
|
|
4674
|
-
widget: html`
|
|
4675
|
-
<sp-checkbox
|
|
4676
|
-
size="s"
|
|
4677
|
-
.checked=${live(!!value)}
|
|
4678
|
-
@change=${(/** @type {any} */ e) =>
|
|
4679
|
-
update(updateFrontmatter(S, field, e.target.checked || undefined))}
|
|
4680
|
-
></sp-checkbox>
|
|
4681
|
-
`,
|
|
4682
|
-
});
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
|
-
// Array of strings → comma-separated text
|
|
4686
|
-
if (entry.type === "array") {
|
|
4687
|
-
const display = Array.isArray(value) ? value.join(", ") : value || "";
|
|
4688
|
-
return renderFieldRow({
|
|
4689
|
-
prop: field,
|
|
4690
|
-
label: displayLabel,
|
|
4691
|
-
hasValue: hasVal,
|
|
4692
|
-
onClear,
|
|
4693
|
-
widget: html`
|
|
4694
|
-
<sp-textfield
|
|
4695
|
-
size="s"
|
|
4696
|
-
placeholder="comma, separated"
|
|
4697
|
-
.value=${live(display)}
|
|
4698
|
-
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
4699
|
-
const arr = e.target.value
|
|
4700
|
-
? e.target.value
|
|
4701
|
-
.split(",")
|
|
4702
|
-
.map((/** @type {string} */ s) => s.trim())
|
|
4703
|
-
.filter(Boolean)
|
|
4704
|
-
: undefined;
|
|
4705
|
-
update(updateFrontmatter(S, field, arr));
|
|
4706
|
-
})}
|
|
4707
|
-
></sp-textfield>
|
|
4708
|
-
`,
|
|
4709
|
-
});
|
|
4710
|
-
}
|
|
4711
|
-
|
|
4712
|
-
// Enum → select
|
|
4713
|
-
if (Array.isArray(entry.enum)) {
|
|
4714
|
-
return renderFieldRow({
|
|
4715
|
-
prop: field,
|
|
4716
|
-
label: displayLabel,
|
|
4717
|
-
hasValue: hasVal,
|
|
4718
|
-
onClear,
|
|
4719
|
-
widget: html`
|
|
4720
|
-
<sp-picker
|
|
4721
|
-
size="s"
|
|
4722
|
-
.value=${live(value || "")}
|
|
4723
|
-
@change=${(/** @type {any} */ e) =>
|
|
4724
|
-
update(updateFrontmatter(S, field, e.target.value || undefined))}
|
|
4725
|
-
>
|
|
4726
|
-
${entry.enum.map(
|
|
4727
|
-
(/** @type {string} */ opt) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`,
|
|
4728
|
-
)}
|
|
4729
|
-
</sp-picker>
|
|
4730
|
-
`,
|
|
4731
|
-
});
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
// Number
|
|
4735
|
-
if (entry.type === "number") {
|
|
4736
|
-
return renderFieldRow({
|
|
4737
|
-
prop: field,
|
|
4738
|
-
label: displayLabel,
|
|
4739
|
-
hasValue: hasVal,
|
|
4740
|
-
onClear,
|
|
4741
|
-
widget: html`
|
|
4742
|
-
<sp-number-field
|
|
4743
|
-
size="s"
|
|
4744
|
-
hide-stepper
|
|
4745
|
-
.value=${live(value !== undefined ? Number(value) : undefined)}
|
|
4746
|
-
@change=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
4747
|
-
const v = e.target.value;
|
|
4748
|
-
update(updateFrontmatter(S, field, isNaN(v) ? undefined : Number(v)));
|
|
4749
|
-
})}
|
|
4750
|
-
></sp-number-field>
|
|
4751
|
-
`,
|
|
4752
|
-
});
|
|
4753
|
-
}
|
|
4754
|
-
|
|
4755
|
-
// Default: text (handles string, date, etc.)
|
|
4756
|
-
return renderFieldRow({
|
|
4757
|
-
prop: field,
|
|
4758
|
-
label: displayLabel,
|
|
4759
|
-
hasValue: hasVal,
|
|
4760
|
-
onClear,
|
|
4761
|
-
widget: html`
|
|
4762
|
-
<sp-textfield
|
|
4763
|
-
size="s"
|
|
4764
|
-
placeholder=${entry.format === "date" ? "YYYY-MM-DD" : ""}
|
|
4765
|
-
.value=${live(value || "")}
|
|
4766
|
-
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) => {
|
|
4767
|
-
update(updateFrontmatter(S, field, e.target.value || undefined));
|
|
4768
|
-
})}
|
|
4769
|
-
></sp-textfield>
|
|
4770
|
-
`,
|
|
4771
|
-
});
|
|
4772
|
-
}
|
|
4773
|
-
|
|
4774
|
-
/** Properties panel — lit-html template with accordion sections */
|
|
4775
|
-
function propertiesSidebarTemplate() {
|
|
4776
|
-
// In content mode with no selection, still show frontmatter fields
|
|
4777
|
-
if (!S.selection) {
|
|
4778
|
-
if (S.mode === "content") {
|
|
4779
|
-
return renderFrontmatterOnlyPanel();
|
|
4780
|
-
}
|
|
4781
|
-
return html`<div class="empty-state">Select an element to inspect</div>`;
|
|
4782
|
-
}
|
|
4783
|
-
const node = getNodeAtPath(S.document, S.selection);
|
|
4784
|
-
if (!node) return html`<div class="empty-state">Node not found</div>`;
|
|
4785
|
-
|
|
4786
|
-
const path = S.selection;
|
|
4787
|
-
const isMapNode = node.$prototype === "Array";
|
|
4788
|
-
const isMapParent =
|
|
4789
|
-
node.children && typeof node.children === "object" && node.children.$prototype === "Array";
|
|
4790
|
-
const isSwitchNode = !!node.$switch;
|
|
4791
|
-
const isCustomInstance = (node.tagName || "").includes("-");
|
|
4792
|
-
const isRoot = path.length === 0;
|
|
4793
|
-
const tagName = node.tagName || "div";
|
|
4794
|
-
const attrs = node.attributes || {};
|
|
4795
|
-
|
|
4796
|
-
const mapSignals = isInsideMapTemplate(path)
|
|
4797
|
-
? [
|
|
4798
|
-
{ value: "$map/item", label: "$map/item" },
|
|
4799
|
-
{ value: "$map/index", label: "$map/index" },
|
|
4800
|
-
]
|
|
4801
|
-
: null;
|
|
4802
|
-
|
|
4803
|
-
// Helper: render an attribute row using the style-row pattern
|
|
4804
|
-
function renderAttrRow(
|
|
4805
|
-
/** @type {any} */ attr,
|
|
4806
|
-
/** @type {any} */ entry,
|
|
4807
|
-
/** @type {any} */ value,
|
|
4808
|
-
) {
|
|
4809
|
-
const type = inferInputType(entry);
|
|
4810
|
-
const hasVal = value !== undefined && value !== "";
|
|
4811
|
-
|
|
4812
|
-
// Boolean attributes render as checkboxes
|
|
4813
|
-
if (entry.type === "boolean") {
|
|
4814
|
-
return renderFieldRow({
|
|
4815
|
-
prop: attr,
|
|
4816
|
-
label: attrLabel(entry, attr),
|
|
4817
|
-
hasValue: hasVal,
|
|
4818
|
-
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
4819
|
-
widget: html`
|
|
4820
|
-
<sp-checkbox
|
|
4821
|
-
size="s"
|
|
4822
|
-
.checked=${live(!!value)}
|
|
4823
|
-
@change=${(/** @type {any} */ e) =>
|
|
4824
|
-
update(updateAttribute(S, path, attr, e.target.checked || undefined))}
|
|
4825
|
-
>
|
|
4826
|
-
</sp-checkbox>
|
|
4827
|
-
`,
|
|
4828
|
-
});
|
|
4829
|
-
}
|
|
4830
|
-
|
|
4831
|
-
return renderFieldRow({
|
|
4832
|
-
prop: attr,
|
|
4833
|
-
label: attrLabel(entry, attr),
|
|
4834
|
-
hasValue: hasVal,
|
|
4835
|
-
onClear: () => update(updateAttribute(S, path, attr, undefined)),
|
|
4836
|
-
widget: widgetForType(type, entry, attr, value || "", (/** @type {any} */ v) =>
|
|
4837
|
-
update(updateAttribute(S, path, attr, v || undefined)),
|
|
4838
|
-
),
|
|
4839
|
-
});
|
|
4840
|
-
}
|
|
4841
|
-
|
|
4842
|
-
// ── Collect applicable attributes from html-meta ──
|
|
4843
|
-
const applicableAttrs = /** @type {Record<string, any>} */ ({});
|
|
4844
|
-
for (const [attr, entry] of /** @type {[string, any][]} */ (Object.entries(htmlMeta.$defs))) {
|
|
4845
|
-
if (!entry.$elements || entry.$elements.includes(tagName)) {
|
|
4846
|
-
applicableAttrs[attr] = entry;
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
|
-
|
|
4850
|
-
// Partition into sections
|
|
4851
|
-
const attrSections = /** @type {Record<string, any[]>} */ ({});
|
|
4852
|
-
for (const sec of htmlMeta.$sections) attrSections[sec.key] = [];
|
|
4853
|
-
for (const [attr, entry] of Object.entries(applicableAttrs)) {
|
|
4854
|
-
const secKey = entry.$section;
|
|
4855
|
-
if (attrSections[secKey]) attrSections[secKey].push({ name: attr, entry });
|
|
4856
|
-
}
|
|
4857
|
-
for (const sec of htmlMeta.$sections) {
|
|
4858
|
-
attrSections[sec.key].sort(
|
|
4859
|
-
(/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order,
|
|
4860
|
-
);
|
|
4861
|
-
}
|
|
4862
|
-
|
|
4863
|
-
// Collect "custom" attributes (not in html-meta and not CEM-defined props)
|
|
4864
|
-
const knownAttrNames = new Set(Object.keys(applicableAttrs));
|
|
4865
|
-
// For npm web components, CEM-defined props are rendered in "Component Props"
|
|
4866
|
-
if (isCustomInstance) {
|
|
4867
|
-
const comp = componentRegistry.find((c) => c.tagName === node.tagName);
|
|
4868
|
-
if (comp) for (const p of comp.props) knownAttrNames.add(p.name);
|
|
4869
|
-
}
|
|
4870
|
-
const customAttrs = Object.entries(attrs).filter(([k]) => !knownAttrNames.has(k));
|
|
4871
|
-
|
|
4872
|
-
// Auto-open sections that have set values
|
|
4873
|
-
const autoOpen = new Set();
|
|
4874
|
-
for (const [attr] of Object.entries(attrs)) {
|
|
4875
|
-
const entry = applicableAttrs[attr];
|
|
4876
|
-
if (entry) autoOpen.add(entry.$section);
|
|
4877
|
-
}
|
|
4878
|
-
// Also auto-open if there are custom attrs
|
|
4879
|
-
if (customAttrs.length > 0) autoOpen.add("__custom");
|
|
4880
|
-
|
|
4881
|
-
function isSectionOpen(/** @type {any} */ key) {
|
|
4882
|
-
if (S.ui.inspectorSections[key] !== undefined) return S.ui.inspectorSections[key];
|
|
4883
|
-
return autoOpen.has(key);
|
|
4884
|
-
}
|
|
4885
|
-
|
|
4886
|
-
function toggleSection(/** @type {any} */ key) {
|
|
4887
|
-
const current = isSectionOpen(key);
|
|
4888
|
-
updateUi("inspectorSections", { ...S.ui.inspectorSections, [key]: !current });
|
|
4889
|
-
}
|
|
4890
|
-
|
|
4891
|
-
// ── Build section templates ─────────────────────────────────────────
|
|
4892
|
-
|
|
4893
|
-
// "Element" section — tagName, textContent, hidden
|
|
4894
|
-
const elemT = html`
|
|
4895
|
-
<sp-accordion-item
|
|
4896
|
-
label="Element"
|
|
4897
|
-
?open=${isSectionOpen("__element") !== false}
|
|
4898
|
-
@sp-accordion-item-toggle=${() => toggleSection("__element")}
|
|
4899
|
-
>
|
|
4900
|
-
<div class="style-section-body">
|
|
4901
|
-
<div class="style-row" data-prop="tagName">
|
|
4902
|
-
<div class="style-row-label">
|
|
4903
|
-
<sp-field-label size="s">Tag</sp-field-label>
|
|
4904
|
-
</div>
|
|
4905
|
-
<sp-textfield
|
|
4906
|
-
size="s"
|
|
4907
|
-
.value=${live(tagName)}
|
|
4908
|
-
autocomplete="off"
|
|
4909
|
-
list="tag-names"
|
|
4910
|
-
@input=${debouncedStyleCommit("prop:tagName", 400, (/** @type {any} */ e) => {
|
|
4911
|
-
update(updateProperty(S, path, "tagName", e.target.value || undefined));
|
|
4912
|
-
})}
|
|
4913
|
-
></sp-textfield>
|
|
4914
|
-
</div>
|
|
4915
|
-
<div class="style-row" data-prop="$id">
|
|
4916
|
-
<div class="style-row-label">
|
|
4917
|
-
${node.$id
|
|
4918
|
-
? html`<span
|
|
4919
|
-
class="set-dot"
|
|
4920
|
-
title="Clear $id"
|
|
4921
|
-
@click=${(/** @type {any} */ e) => {
|
|
4922
|
-
e.stopPropagation();
|
|
4923
|
-
update(updateProperty(S, path, "$id", undefined));
|
|
4924
|
-
}}
|
|
4925
|
-
></span>`
|
|
4926
|
-
: nothing}
|
|
4927
|
-
<sp-field-label size="s">ID</sp-field-label>
|
|
4928
|
-
</div>
|
|
4929
|
-
<sp-textfield
|
|
4930
|
-
size="s"
|
|
4931
|
-
.value=${live(node.$id || "")}
|
|
4932
|
-
@input=${debouncedStyleCommit("prop:$id", 400, (/** @type {any} */ e) => {
|
|
4933
|
-
update(updateProperty(S, path, "$id", e.target.value || undefined));
|
|
4934
|
-
})}
|
|
4935
|
-
></sp-textfield>
|
|
4936
|
-
</div>
|
|
4937
|
-
<div class="style-row" data-prop="className">
|
|
4938
|
-
<div class="style-row-label">
|
|
4939
|
-
${node.className
|
|
4940
|
-
? html`<span
|
|
4941
|
-
class="set-dot"
|
|
4942
|
-
title="Clear class"
|
|
4943
|
-
@click=${(/** @type {any} */ e) => {
|
|
4944
|
-
e.stopPropagation();
|
|
4945
|
-
update(updateProperty(S, path, "className", undefined));
|
|
4946
|
-
}}
|
|
4947
|
-
></span>`
|
|
4948
|
-
: nothing}
|
|
4949
|
-
<sp-field-label size="s">Class</sp-field-label>
|
|
4950
|
-
</div>
|
|
4951
|
-
<sp-textfield
|
|
4952
|
-
size="s"
|
|
4953
|
-
.value=${live(node.className || "")}
|
|
4954
|
-
@input=${debouncedStyleCommit("prop:className", 400, (/** @type {any} */ e) => {
|
|
4955
|
-
update(updateProperty(S, path, "className", e.target.value || undefined));
|
|
4956
|
-
})}
|
|
4957
|
-
></sp-textfield>
|
|
4958
|
-
</div>
|
|
4959
|
-
${!Array.isArray(node.children) || node.children.length === 0
|
|
4960
|
-
? html`
|
|
4961
|
-
<div class="style-row" data-prop="textContent">
|
|
4962
|
-
<div class="style-row-label">
|
|
4963
|
-
${node.textContent !== undefined
|
|
4964
|
-
? html`<span
|
|
4965
|
-
class="set-dot"
|
|
4966
|
-
title="Clear text"
|
|
4967
|
-
@click=${(/** @type {any} */ e) => {
|
|
4968
|
-
e.stopPropagation();
|
|
4969
|
-
update(updateProperty(S, path, "textContent", undefined));
|
|
4970
|
-
}}
|
|
4971
|
-
></span>`
|
|
4972
|
-
: nothing}
|
|
4973
|
-
<sp-field-label size="s">Text Content</sp-field-label>
|
|
4974
|
-
</div>
|
|
4975
|
-
<sp-textfield
|
|
4976
|
-
size="s"
|
|
4977
|
-
multiline
|
|
4978
|
-
.value=${live(
|
|
4979
|
-
typeof node.textContent === "string"
|
|
4980
|
-
? node.textContent
|
|
4981
|
-
: (node.textContent ?? ""),
|
|
4982
|
-
)}
|
|
4983
|
-
@input=${debouncedStyleCommit("prop:textContent", 400, (/** @type {any} */ e) => {
|
|
4984
|
-
update(updateProperty(S, path, "textContent", e.target.value || undefined));
|
|
4985
|
-
})}
|
|
4986
|
-
></sp-textfield>
|
|
4987
|
-
</div>
|
|
4988
|
-
`
|
|
4989
|
-
: nothing}
|
|
4990
|
-
<div class="style-row" data-prop="hidden">
|
|
4991
|
-
<div class="style-row-label">
|
|
4992
|
-
${node.hidden
|
|
4993
|
-
? html`<span
|
|
4994
|
-
class="set-dot"
|
|
4995
|
-
title="Clear hidden"
|
|
4996
|
-
@click=${(/** @type {any} */ e) => {
|
|
4997
|
-
e.stopPropagation();
|
|
4998
|
-
update(updateProperty(S, path, "hidden", undefined));
|
|
4999
|
-
}}
|
|
5000
|
-
></span>`
|
|
5001
|
-
: nothing}
|
|
5002
|
-
<sp-field-label size="s">Hidden</sp-field-label>
|
|
5003
|
-
</div>
|
|
5004
|
-
<sp-checkbox
|
|
5005
|
-
size="s"
|
|
5006
|
-
.checked=${live(!!node.hidden)}
|
|
5007
|
-
@change=${(/** @type {any} */ e) =>
|
|
5008
|
-
update(updateProperty(S, path, "hidden", e.target.checked || undefined))}
|
|
5009
|
-
>
|
|
5010
|
-
</sp-checkbox>
|
|
5011
|
-
</div>
|
|
5012
|
-
${isMapParent
|
|
5013
|
-
? html`
|
|
5014
|
-
<div style="font-size:10px;color:var(--fg-dim);padding:4px 0;font-style:italic">
|
|
5015
|
-
Children: Repeater (select in layers to configure)
|
|
5016
|
-
</div>
|
|
5017
|
-
`
|
|
5018
|
-
: nothing}
|
|
5019
|
-
</div>
|
|
5020
|
-
</sp-accordion-item>
|
|
5021
|
-
`;
|
|
5022
|
-
|
|
5023
|
-
// "Repeater" section (if $map node)
|
|
5024
|
-
const repeaterT = isMapNode
|
|
5025
|
-
? html`
|
|
5026
|
-
<sp-accordion-item label="Repeater" open>
|
|
5027
|
-
<div class="style-section-body">
|
|
5028
|
-
${renderRepeaterFieldsTemplate(node, path, mapSignals)}
|
|
5029
|
-
</div>
|
|
5030
|
-
</sp-accordion-item>
|
|
5031
|
-
`
|
|
5032
|
-
: nothing;
|
|
5033
|
-
|
|
5034
|
-
// "$switch" section
|
|
5035
|
-
const switchT = isSwitchNode
|
|
5036
|
-
? html`
|
|
5037
|
-
<sp-accordion-item label="Switch" open>
|
|
5038
|
-
<div class="style-section-body">
|
|
5039
|
-
${renderSwitchFieldsTemplate(node, path, mapSignals)}
|
|
5040
|
-
</div>
|
|
5041
|
-
</sp-accordion-item>
|
|
5042
|
-
`
|
|
5043
|
-
: nothing;
|
|
5044
|
-
|
|
5045
|
-
// "Observed Attributes" section (custom element doc root)
|
|
5046
|
-
const observedAttrsT =
|
|
5047
|
-
isCustomElementDoc(S) && isRoot
|
|
5048
|
-
? (() => {
|
|
5049
|
-
const state = S.document.state || {};
|
|
5050
|
-
const entries = Object.entries(state).filter(([, d]) => d.attribute);
|
|
5051
|
-
return html`
|
|
5052
|
-
<sp-accordion-item label="Observed Attributes" ?open=${isSectionOpen("__observed")}>
|
|
5053
|
-
<div class="style-section-body">
|
|
5054
|
-
${entries.length === 0
|
|
5055
|
-
? html`<div class="empty-state">
|
|
5056
|
-
No attributes declared. Set "attribute" on a state entry.
|
|
5057
|
-
</div>`
|
|
5058
|
-
: entries.map(
|
|
5059
|
-
([key, d]) => html`
|
|
5060
|
-
<div
|
|
5061
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
|
|
5062
|
-
>
|
|
5063
|
-
<code style="font-family:monospace;color:var(--accent)"
|
|
5064
|
-
>${d.attribute}</code
|
|
5065
|
-
>
|
|
5066
|
-
<span style="color:var(--fg-dim)"> → </span>
|
|
5067
|
-
<span>${key}</span>
|
|
5068
|
-
${d.type
|
|
5069
|
-
? html`<span style="margin-left:auto;color:var(--fg-dim);font-size:10px"
|
|
5070
|
-
>${d.type}</span
|
|
5071
|
-
>`
|
|
5072
|
-
: nothing}
|
|
5073
|
-
${d.reflects
|
|
5074
|
-
? html`<span
|
|
5075
|
-
style="font-size:9px;background:var(--bg-hover);padding:1px 4px;border-radius:3px"
|
|
5076
|
-
>reflects</span
|
|
5077
|
-
>`
|
|
5078
|
-
: nothing}
|
|
5079
|
-
</div>
|
|
5080
|
-
`,
|
|
5081
|
-
)}
|
|
5082
|
-
</div>
|
|
5083
|
-
</sp-accordion-item>
|
|
5084
|
-
`;
|
|
5085
|
-
})()
|
|
5086
|
-
: nothing;
|
|
5087
|
-
|
|
5088
|
-
// "Component Props" section
|
|
5089
|
-
const compPropsT = isCustomInstance
|
|
5090
|
-
? html`
|
|
5091
|
-
<sp-accordion-item label="Component Props" open>
|
|
5092
|
-
<div class="style-section-body">
|
|
5093
|
-
${renderComponentPropsFieldsTemplate(node, path, mapSignals)}
|
|
5094
|
-
</div>
|
|
5095
|
-
</sp-accordion-item>
|
|
5096
|
-
`
|
|
5097
|
-
: nothing;
|
|
5098
|
-
|
|
5099
|
-
// HTML-meta attribute sections
|
|
5100
|
-
const attrSectionTemplates = htmlMeta.$sections
|
|
5101
|
-
.filter((sec) => attrSections[sec.key].length > 0)
|
|
5102
|
-
.map((sec) => {
|
|
5103
|
-
const sectionAttrs = attrSections[sec.key];
|
|
5104
|
-
const hasAnySet = sectionAttrs.some((/** @type {any} */ a) => attrs[a.name] !== undefined);
|
|
5105
|
-
return html`
|
|
5106
|
-
<sp-accordion-item
|
|
5107
|
-
label=${sec.label}
|
|
5108
|
-
?open=${isSectionOpen(sec.key)}
|
|
5109
|
-
@sp-accordion-item-toggle=${() => toggleSection(sec.key)}
|
|
5110
|
-
>
|
|
5111
|
-
${hasAnySet
|
|
5112
|
-
? html`<span slot="heading" class="set-dot set-dot--section"></span>`
|
|
5113
|
-
: nothing}
|
|
5114
|
-
<div class="style-section-body">
|
|
5115
|
-
${sectionAttrs.map((/** @type {any} */ a) =>
|
|
5116
|
-
renderAttrRow(a.name, a.entry, attrs[a.name]),
|
|
5117
|
-
)}
|
|
5118
|
-
</div>
|
|
5119
|
-
</sp-accordion-item>
|
|
5120
|
-
`;
|
|
5121
|
-
});
|
|
5122
|
-
|
|
5123
|
-
// "Custom" attributes section (not in html-meta)
|
|
5124
|
-
const customSectionT =
|
|
5125
|
-
customAttrs.length > 0 || Object.keys(attrs).length > 0
|
|
5126
|
-
? html`
|
|
5127
|
-
<sp-accordion-item
|
|
5128
|
-
label="Custom"
|
|
5129
|
-
?open=${isSectionOpen("__custom")}
|
|
5130
|
-
@sp-accordion-item-toggle=${() => toggleSection("__custom")}
|
|
5131
|
-
>
|
|
5132
|
-
${customAttrs.length > 0
|
|
5133
|
-
? html`<span slot="heading" class="set-dot set-dot--section"></span>`
|
|
5134
|
-
: nothing}
|
|
5135
|
-
<div class="style-section-body">
|
|
5136
|
-
${renderCustomAttrsFieldsTemplate(node, path, attrs, knownAttrNames)}
|
|
5137
|
-
</div>
|
|
5138
|
-
</sp-accordion-item>
|
|
5139
|
-
`
|
|
5140
|
-
: nothing;
|
|
5141
|
-
|
|
5142
|
-
// Media section (root only)
|
|
5143
|
-
const mediaT = isRoot
|
|
5144
|
-
? html`
|
|
5145
|
-
<sp-accordion-item
|
|
5146
|
-
label="Media"
|
|
5147
|
-
?open=${isSectionOpen("__media")}
|
|
5148
|
-
@sp-accordion-item-toggle=${() => toggleSection("__media")}
|
|
5149
|
-
>
|
|
5150
|
-
<div class="style-section-body">${renderMediaFieldsTemplate(node)}</div>
|
|
5151
|
-
</sp-accordion-item>
|
|
5152
|
-
`
|
|
5153
|
-
: nothing;
|
|
5154
|
-
|
|
5155
|
-
// CSS Properties + CSS Parts (custom element doc root)
|
|
5156
|
-
const cssPropsT =
|
|
5157
|
-
isCustomElementDoc(S) && isRoot
|
|
5158
|
-
? (() => {
|
|
5159
|
-
const style = node.style || {};
|
|
5160
|
-
const cssProps = Object.entries(style).filter(([k]) => k.startsWith("--"));
|
|
5161
|
-
if (cssProps.length === 0) return nothing;
|
|
5162
|
-
return html`
|
|
5163
|
-
<sp-accordion-item
|
|
5164
|
-
label="CSS Properties"
|
|
5165
|
-
?open=${isSectionOpen("__cssprops")}
|
|
5166
|
-
@sp-accordion-item-toggle=${() => toggleSection("__cssprops")}
|
|
5167
|
-
>
|
|
5168
|
-
<div class="style-section-body">
|
|
5169
|
-
${cssProps.map(
|
|
5170
|
-
([prop, val]) => html`
|
|
5171
|
-
<div
|
|
5172
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
|
|
5173
|
-
>
|
|
5174
|
-
<code style="font-family:monospace;color:var(--accent)">${prop}</code>
|
|
5175
|
-
<span style="margin-left:auto;color:var(--fg-dim)">${String(val)}</span>
|
|
5176
|
-
</div>
|
|
5177
|
-
`,
|
|
5178
|
-
)}
|
|
5179
|
-
</div>
|
|
5180
|
-
</sp-accordion-item>
|
|
5181
|
-
`;
|
|
5182
|
-
})()
|
|
5183
|
-
: nothing;
|
|
5184
|
-
|
|
5185
|
-
const cssPartsT =
|
|
5186
|
-
isCustomElementDoc(S) && isRoot
|
|
5187
|
-
? (() => {
|
|
5188
|
-
const parts = collectCssParts(S.document);
|
|
5189
|
-
if (parts.length === 0) return nothing;
|
|
5190
|
-
return html`
|
|
5191
|
-
<sp-accordion-item
|
|
5192
|
-
label="CSS Parts"
|
|
5193
|
-
?open=${isSectionOpen("__cssparts")}
|
|
5194
|
-
@sp-accordion-item-toggle=${() => toggleSection("__cssparts")}
|
|
5195
|
-
>
|
|
5196
|
-
<div class="style-section-body">
|
|
5197
|
-
${parts.map(
|
|
5198
|
-
(p) => html`
|
|
5199
|
-
<div
|
|
5200
|
-
style="display:flex;gap:6px;align-items:center;padding:2px 0;font-size:11px"
|
|
5201
|
-
>
|
|
5202
|
-
<code style="font-family:monospace;color:var(--accent)">${p.name}</code>
|
|
5203
|
-
<span style="color:var(--fg-dim)"><${p.tag}></span>
|
|
5204
|
-
</div>
|
|
5205
|
-
`,
|
|
5206
|
-
)}
|
|
5207
|
-
</div>
|
|
5208
|
-
</sp-accordion-item>
|
|
5209
|
-
`;
|
|
5210
|
-
})()
|
|
5211
|
-
: nothing;
|
|
5212
|
-
|
|
5213
|
-
// ── Frontmatter section (content mode only) ──
|
|
5214
|
-
const frontmatterT =
|
|
5215
|
-
S.mode === "content"
|
|
5216
|
-
? (() => {
|
|
5217
|
-
const fm = S.content?.frontmatter || {};
|
|
5218
|
-
const col = findCollectionSchema(S.documentPath, projectState?.projectConfig);
|
|
5219
|
-
const schemaProps = col?.schema?.properties;
|
|
5220
|
-
const requiredFields = new Set(col?.schema?.required || []);
|
|
5221
|
-
|
|
5222
|
-
/** @type {{ field: string; entry: any; value: any }[]} */
|
|
5223
|
-
const fields = [];
|
|
5224
|
-
if (schemaProps) {
|
|
5225
|
-
for (const [field, fieldSchema] of Object.entries(
|
|
5226
|
-
/** @type {Record<string, any>} */ (schemaProps),
|
|
5227
|
-
)) {
|
|
5228
|
-
fields.push({ field, entry: fieldSchema, value: fm[field] });
|
|
5229
|
-
}
|
|
5230
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
5231
|
-
if (!schemaProps[field]) {
|
|
5232
|
-
fields.push({
|
|
5233
|
-
field,
|
|
5234
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5235
|
-
value,
|
|
5236
|
-
});
|
|
5237
|
-
}
|
|
5238
|
-
}
|
|
5239
|
-
} else {
|
|
5240
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
5241
|
-
fields.push({
|
|
5242
|
-
field,
|
|
5243
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
5244
|
-
value,
|
|
5245
|
-
});
|
|
5246
|
-
}
|
|
5247
|
-
}
|
|
5248
|
-
|
|
5249
|
-
if (fields.length === 0 && !schemaProps) return nothing;
|
|
5250
|
-
|
|
5251
|
-
return html`
|
|
5252
|
-
<sp-accordion-item
|
|
5253
|
-
label=${col ? `Frontmatter (${col.name})` : "Frontmatter"}
|
|
5254
|
-
?open=${isSectionOpen("__frontmatter") !== false}
|
|
5255
|
-
@sp-accordion-item-toggle=${() => toggleSection("__frontmatter")}
|
|
5256
|
-
>
|
|
5257
|
-
<div class="style-section-body">
|
|
5258
|
-
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
5259
|
-
</div>
|
|
5260
|
-
</sp-accordion-item>
|
|
5261
|
-
`;
|
|
5262
|
-
})()
|
|
5263
|
-
: nothing;
|
|
5264
|
-
|
|
5265
|
-
// ── Assemble ──
|
|
5266
|
-
const tpl = html`
|
|
5267
|
-
<div class="style-sidebar">
|
|
5268
|
-
<sp-accordion allow-multiple size="s">
|
|
5269
|
-
${frontmatterT} ${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
|
|
5270
|
-
${isMapNode ? nothing : switchT} ${isMapNode ? nothing : compPropsT}
|
|
5271
|
-
${isMapNode ? nothing : attrSectionTemplates} ${isMapNode ? nothing : customSectionT}
|
|
5272
|
-
${isMapNode ? nothing : mediaT} ${isMapNode ? nothing : cssPropsT}
|
|
5273
|
-
${isMapNode ? nothing : cssPartsT}
|
|
5274
|
-
</sp-accordion>
|
|
5275
|
-
</div>
|
|
5276
|
-
`;
|
|
5277
|
-
|
|
5278
|
-
return tpl;
|
|
5279
|
-
}
|
|
5280
|
-
|
|
5281
|
-
/** Repeater fields template */
|
|
5282
|
-
function renderRepeaterFieldsTemplate(
|
|
5283
|
-
/** @type {any} */ node,
|
|
5284
|
-
/** @type {any} */ path,
|
|
5285
|
-
/** @type {any} */ _mapSignals,
|
|
5286
|
-
) {
|
|
5287
|
-
return html`
|
|
5288
|
-
${bindableFieldRow("Items", "text", node.items, (/** @type {any} */ v) =>
|
|
5289
|
-
update(updateProperty(S, path, "items", v)),
|
|
5290
|
-
)}
|
|
5291
|
-
${node.filter
|
|
5292
|
-
? bindableFieldRow("Filter", "text", node.filter, (/** @type {any} */ v) =>
|
|
5293
|
-
update(updateProperty(S, path, "filter", v || undefined)),
|
|
5294
|
-
)
|
|
5295
|
-
: nothing}
|
|
5296
|
-
${node.sort
|
|
5297
|
-
? bindableFieldRow("Sort", "text", node.sort, (/** @type {any} */ v) =>
|
|
5298
|
-
update(updateProperty(S, path, "sort", v || undefined)),
|
|
5299
|
-
)
|
|
5300
|
-
: nothing}
|
|
5301
|
-
<div style="display:flex;gap:8px;margin-top:4px">
|
|
5302
|
-
${!node.filter
|
|
5303
|
-
? html`<span
|
|
5304
|
-
class="kv-add"
|
|
5305
|
-
@click=${() => update(updateProperty(S, path, "filter", { $ref: "#/state/" }))}
|
|
5306
|
-
>+ Add filter</span
|
|
5307
|
-
>`
|
|
5308
|
-
: nothing}
|
|
5309
|
-
${!node.sort
|
|
5310
|
-
? html`<span
|
|
5311
|
-
class="kv-add"
|
|
5312
|
-
@click=${() => update(updateProperty(S, path, "sort", { $ref: "#/state/" }))}
|
|
5313
|
-
>+ Add sort</span
|
|
5314
|
-
>`
|
|
5315
|
-
: nothing}
|
|
5316
|
-
</div>
|
|
5317
|
-
${node.map
|
|
5318
|
-
? html`
|
|
5319
|
-
<sp-action-button
|
|
5320
|
-
size="s"
|
|
5321
|
-
style="margin-top:8px;width:100%"
|
|
5322
|
-
@click=${() => update(selectNode(S, [...path, "map"]))}
|
|
5323
|
-
>Edit template →</sp-action-button
|
|
5324
|
-
>
|
|
5325
|
-
`
|
|
5326
|
-
: nothing}
|
|
5327
|
-
`;
|
|
5328
|
-
}
|
|
5329
|
-
|
|
5330
|
-
/** Switch fields template */
|
|
5331
|
-
function renderSwitchFieldsTemplate(
|
|
5332
|
-
/** @type {any} */ node,
|
|
5333
|
-
/** @type {any} */ path,
|
|
5334
|
-
/** @type {any} */ mapSignals,
|
|
5335
|
-
) {
|
|
5336
|
-
const caseNames = Object.keys(node.cases || {});
|
|
5337
|
-
return html`
|
|
5338
|
-
${bindableFieldRow(
|
|
5339
|
-
"Expression",
|
|
5340
|
-
"text",
|
|
5341
|
-
node.$switch,
|
|
5342
|
-
(/** @type {any} */ v) => update(updateProperty(S, path, "$switch", v)),
|
|
5343
|
-
null,
|
|
5344
|
-
mapSignals,
|
|
5345
|
-
)}
|
|
5346
|
-
<div
|
|
5347
|
-
style="font-size:11px;font-weight:600;color:var(--fg-dim);margin:8px 0 4px;text-transform:uppercase;letter-spacing:0.05em"
|
|
5348
|
-
>
|
|
5349
|
-
Cases
|
|
5350
|
-
</div>
|
|
5351
|
-
${caseNames.map((caseName) => {
|
|
5352
|
-
/** @type {any} */
|
|
5353
|
-
let debounce;
|
|
5354
|
-
return html`
|
|
5355
|
-
<div class="field-row" style="display:flex;align-items:center;gap:4px;margin-bottom:3px">
|
|
5356
|
-
<input
|
|
5357
|
-
class="field-input"
|
|
5358
|
-
value=${caseName}
|
|
5359
|
-
style="flex:1"
|
|
5360
|
-
@input=${(/** @type {any} */ e) => {
|
|
5361
|
-
clearTimeout(debounce);
|
|
5362
|
-
debounce = setTimeout(() => {
|
|
5363
|
-
if (e.target.value && e.target.value !== caseName)
|
|
5364
|
-
update(renameSwitchCase(S, path, caseName, e.target.value));
|
|
5365
|
-
}, 500);
|
|
5366
|
-
}}
|
|
5367
|
-
/>
|
|
5368
|
-
<span
|
|
5369
|
-
class="bind-toggle"
|
|
5370
|
-
title="Edit case"
|
|
5371
|
-
style="cursor:pointer"
|
|
5372
|
-
@click=${(/** @type {any} */ e) => {
|
|
5373
|
-
e.stopPropagation();
|
|
5374
|
-
update(selectNode(S, [...path, "cases", caseName]));
|
|
5375
|
-
}}
|
|
5376
|
-
>→</span
|
|
5377
|
-
>
|
|
5378
|
-
<span
|
|
5379
|
-
style="cursor:pointer;color:var(--danger);font-size:11px"
|
|
5380
|
-
@click=${(/** @type {any} */ e) => {
|
|
5381
|
-
e.stopPropagation();
|
|
5382
|
-
update(removeSwitchCase(S, path, caseName));
|
|
5383
|
-
}}
|
|
5384
|
-
>✕</span
|
|
5385
|
-
>
|
|
5386
|
-
</div>
|
|
5387
|
-
`;
|
|
5388
|
-
})}
|
|
5389
|
-
<span
|
|
5390
|
-
class="kv-add"
|
|
5391
|
-
@click=${() => {
|
|
5392
|
-
update(addSwitchCase(S, path, `case${caseNames.length + 1}`));
|
|
5393
|
-
}}
|
|
5394
|
-
>+ Add case</span
|
|
5395
|
-
>
|
|
5396
|
-
`;
|
|
5397
|
-
}
|
|
5398
|
-
|
|
5399
|
-
/** Component props fields template */
|
|
5400
|
-
function renderComponentPropsFieldsTemplate(
|
|
5401
|
-
/** @type {any} */ node,
|
|
5402
|
-
/** @type {any} */ path,
|
|
5403
|
-
/** @type {any} */ mapSignals,
|
|
5404
|
-
) {
|
|
5405
|
-
const comp = componentRegistry.find((c) => c.tagName === node.tagName);
|
|
5406
|
-
if (!comp) return html`<div class="empty-state">Component not found</div>`;
|
|
5407
|
-
// For npm web components, props map to attributes; for JX components, use $props
|
|
5408
|
-
const isNpm = comp.source === "npm";
|
|
5409
|
-
const currentVals = isNpm ? node.attributes || {} : node.$props || {};
|
|
5410
|
-
const updateFn = isNpm
|
|
5411
|
-
? (/** @type {string} */ name, /** @type {any} */ v) =>
|
|
5412
|
-
update(updateAttribute(S, path, name, v === "" ? undefined : v))
|
|
5413
|
-
: (/** @type {string} */ name, /** @type {any} */ v) => update(updateProp(S, path, name, v));
|
|
5414
|
-
|
|
5415
|
-
const defs = S.document.state || {};
|
|
5416
|
-
const signalDefs = Object.entries(defs).filter(
|
|
5417
|
-
([, d]) => !d.$handler && d.$prototype !== "Function",
|
|
5418
|
-
);
|
|
5419
|
-
const extraSignals = mapSignals;
|
|
5420
|
-
|
|
5421
|
-
return html`
|
|
5422
|
-
${comp.props.map((/** @type {any} */ prop) => {
|
|
5423
|
-
const rawValue = currentVals[prop.name];
|
|
5424
|
-
const isBound = typeof rawValue === "object" && rawValue !== null && rawValue.$ref;
|
|
5425
|
-
const hasVal = rawValue !== undefined && rawValue !== null;
|
|
5426
|
-
const parsed = parseCemType(prop.type);
|
|
5427
|
-
const onChange = (/** @type {any} */ v) => updateFn(prop.name, v);
|
|
5428
|
-
|
|
5429
|
-
const clearProp = (/** @type {any} */ e) => {
|
|
5430
|
-
e.stopPropagation();
|
|
5431
|
-
updateFn(prop.name, undefined);
|
|
5432
|
-
};
|
|
5433
|
-
|
|
5434
|
-
const onToggleBind = () => {
|
|
5435
|
-
if (isBound) {
|
|
5436
|
-
const ref = rawValue.$ref;
|
|
5437
|
-
const defName = ref.startsWith("#/state/") ? ref.slice(8) : ref;
|
|
5438
|
-
const def = defs[defName];
|
|
5439
|
-
let staticVal = "";
|
|
5440
|
-
if (def && def.default !== undefined)
|
|
5441
|
-
staticVal =
|
|
5442
|
-
typeof def.default === "object" ? JSON.stringify(def.default) : String(def.default);
|
|
5443
|
-
onChange(staticVal || undefined);
|
|
5444
|
-
} else {
|
|
5445
|
-
if (signalDefs.length > 0) {
|
|
5446
|
-
onChange({ $ref: `#/state/${signalDefs[0][0]}` });
|
|
5447
|
-
} else if (extraSignals?.length > 0) {
|
|
5448
|
-
onChange({ $ref: extraSignals[0].value });
|
|
5449
|
-
}
|
|
5450
|
-
}
|
|
5451
|
-
};
|
|
5452
|
-
|
|
5453
|
-
// Signal picker for bound mode
|
|
5454
|
-
const boundTpl = html`
|
|
5455
|
-
<sp-picker
|
|
5456
|
-
size="s"
|
|
5457
|
-
quiet
|
|
5458
|
-
placeholder="— select signal —"
|
|
5459
|
-
value=${isBound && rawValue.$ref ? rawValue.$ref : nothing}
|
|
5460
|
-
@change=${(/** @type {any} */ e) => {
|
|
5461
|
-
if (e.target.value) onChange({ $ref: e.target.value });
|
|
5462
|
-
else onChange(undefined);
|
|
5463
|
-
}}
|
|
5464
|
-
>
|
|
5465
|
-
${signalDefs.map(
|
|
5466
|
-
([defName]) =>
|
|
5467
|
-
html`<sp-menu-item value=${`#/state/${defName}`}>${defName}</sp-menu-item>`,
|
|
5468
|
-
)}
|
|
5469
|
-
${extraSignals
|
|
5470
|
-
? html`
|
|
5471
|
-
<sp-menu-divider></sp-menu-divider>
|
|
5472
|
-
${extraSignals.map(
|
|
5473
|
-
(/** @type {any} */ sig) =>
|
|
5474
|
-
html`<sp-menu-item value=${sig.value}>${sig.label}</sp-menu-item>`,
|
|
5475
|
-
)}
|
|
5476
|
-
`
|
|
5477
|
-
: nothing}
|
|
5478
|
-
</sp-picker>
|
|
5479
|
-
`;
|
|
5480
|
-
|
|
5481
|
-
// Widget based on CEM type
|
|
5482
|
-
/** @type {any} */
|
|
5483
|
-
let debounce;
|
|
5484
|
-
const staticVal = isBound ? "" : (rawValue ?? "");
|
|
5485
|
-
/** @type {any} */
|
|
5486
|
-
let widgetTpl;
|
|
5487
|
-
if (parsed.kind === "boolean") {
|
|
5488
|
-
widgetTpl = html`<sp-checkbox
|
|
5489
|
-
size="s"
|
|
5490
|
-
.checked=${live(!!staticVal)}
|
|
5491
|
-
@change=${(/** @type {any} */ e) => onChange(e.target.checked || undefined)}
|
|
5492
|
-
></sp-checkbox>`;
|
|
5493
|
-
} else if (parsed.kind === "number") {
|
|
5494
|
-
widgetTpl = html`<sp-number-field
|
|
5495
|
-
size="s"
|
|
5496
|
-
value=${staticVal}
|
|
5497
|
-
@input=${(/** @type {any} */ e) => {
|
|
5498
|
-
clearTimeout(debounce);
|
|
5499
|
-
debounce = setTimeout(() => onChange(e.target.value), 400);
|
|
5500
|
-
}}
|
|
5501
|
-
></sp-number-field>`;
|
|
5502
|
-
} else if (parsed.kind === "combobox") {
|
|
5503
|
-
const options = /** @type {string[]} */ (/** @type {any} */ (parsed).options);
|
|
5504
|
-
widgetTpl = html`<jx-value-selector
|
|
5505
|
-
.value=${String(staticVal)}
|
|
5506
|
-
size="s"
|
|
5507
|
-
placeholder="—"
|
|
5508
|
-
.options=${options.map((o) => ({ value: o, label: camelToLabel(o) }))}
|
|
5509
|
-
@change=${(/** @type {any} */ e) => onChange(e.detail?.value ?? e.target.value)}
|
|
5510
|
-
></jx-value-selector>`;
|
|
5511
|
-
} else {
|
|
5512
|
-
widgetTpl = html`<sp-textfield
|
|
5513
|
-
size="s"
|
|
5514
|
-
value=${staticVal}
|
|
5515
|
-
@input=${(/** @type {any} */ e) => {
|
|
5516
|
-
clearTimeout(debounce);
|
|
5517
|
-
debounce = setTimeout(() => onChange(e.target.value), 400);
|
|
5518
|
-
}}
|
|
5519
|
-
></sp-textfield>`;
|
|
5520
|
-
}
|
|
5521
|
-
|
|
5522
|
-
return html`
|
|
5523
|
-
<div class="style-row" data-prop=${prop.name}>
|
|
5524
|
-
<div class="style-row-label">
|
|
5525
|
-
${hasVal
|
|
5526
|
-
? html`<span class="set-dot" title="Clear ${prop.name}" @click=${clearProp}></span>`
|
|
5527
|
-
: nothing}
|
|
5528
|
-
<sp-field-label size="s" title=${prop.description || prop.name}
|
|
5529
|
-
>${camelToLabel(prop.name)}</sp-field-label
|
|
5530
|
-
>
|
|
5531
|
-
<sp-action-button
|
|
5532
|
-
size="xs"
|
|
5533
|
-
quiet
|
|
5534
|
-
title=${isBound ? "Unbind (switch to static)" : "Bind to signal"}
|
|
5535
|
-
@click=${onToggleBind}
|
|
5536
|
-
>${isBound ? "\u26A1" : "\u2194"}</sp-action-button
|
|
5537
|
-
>
|
|
5538
|
-
</div>
|
|
5539
|
-
${isBound ? boundTpl : widgetTpl}
|
|
5540
|
-
</div>
|
|
5541
|
-
`;
|
|
5542
|
-
})}
|
|
5543
|
-
${comp.props.length === 0 ? html`<div class="empty-state">No props defined</div>` : nothing}
|
|
5544
|
-
${comp.path
|
|
5545
|
-
? html`<span class="kv-add" @click=${() => navigateToComponent(comp.path)}
|
|
5546
|
-
>→ Edit definition</span
|
|
5547
|
-
>`
|
|
5548
|
-
: nothing}
|
|
5549
|
-
`;
|
|
5550
|
-
}
|
|
5551
|
-
|
|
5552
|
-
/** Custom attrs fields template */
|
|
5553
|
-
function renderCustomAttrsFieldsTemplate(
|
|
5554
|
-
/** @type {any} */ node,
|
|
5555
|
-
/** @type {any} */ path,
|
|
5556
|
-
/** @type {any} */ attrs,
|
|
5557
|
-
/** @type {any} */ knownAttrNames,
|
|
5558
|
-
) {
|
|
5559
|
-
const customAttrs = Object.entries(attrs).filter(([k]) => !knownAttrNames.has(k));
|
|
5560
|
-
return html`
|
|
5561
|
-
${customAttrs.map(([attr, val]) =>
|
|
5562
|
-
kvRow(
|
|
5563
|
-
attr,
|
|
5564
|
-
String(val),
|
|
5565
|
-
(/** @type {any} */ newAttr, /** @type {any} */ newVal) => {
|
|
5566
|
-
if (newAttr !== attr) {
|
|
5567
|
-
let s = updateAttribute(S, path, attr, undefined);
|
|
5568
|
-
s = updateAttribute(s, path, newAttr, newVal);
|
|
5569
|
-
update(s);
|
|
5570
|
-
} else {
|
|
5571
|
-
update(updateAttribute(S, path, attr, newVal));
|
|
5572
|
-
}
|
|
5573
|
-
},
|
|
5574
|
-
() => update(updateAttribute(S, path, attr, undefined)),
|
|
5575
|
-
),
|
|
5576
|
-
)}
|
|
5577
|
-
<span class="kv-add" @click=${() => update(updateAttribute(S, path, "data-", ""))}
|
|
5578
|
-
>+ Add attribute</span
|
|
5579
|
-
>
|
|
5580
|
-
`;
|
|
5581
|
-
}
|
|
5582
|
-
|
|
5583
|
-
/** Media breakpoint fields template */
|
|
5584
|
-
|
|
5585
|
-
function renderMediaFieldsTemplate(/** @type {any} */ node) {
|
|
5586
|
-
const media = node.$media || {};
|
|
5587
|
-
/** @type {any} */
|
|
5588
|
-
let baseDebounce;
|
|
5589
|
-
const breakpoints = Object.entries(media).filter(([k]) => k !== "--");
|
|
5590
|
-
|
|
5591
|
-
return html`
|
|
5592
|
-
<div class="kv-row" style="align-items:center">
|
|
5593
|
-
<span class="field-label" style="width:auto;margin-right:4px">Base width</span>
|
|
5594
|
-
<input
|
|
5595
|
-
class="field-input"
|
|
5596
|
-
style="width:70px;flex:none"
|
|
5597
|
-
placeholder="320px"
|
|
5598
|
-
value=${media["--"] || ""}
|
|
5599
|
-
@input=${(/** @type {any} */ e) => {
|
|
5600
|
-
clearTimeout(baseDebounce);
|
|
5601
|
-
baseDebounce = setTimeout(() => {
|
|
5602
|
-
const val = e.target.value.trim();
|
|
5603
|
-
update(updateMedia(S, "--", val || undefined));
|
|
5604
|
-
}, 400);
|
|
5605
|
-
}}
|
|
5606
|
-
/>
|
|
5607
|
-
${media["--"]
|
|
5608
|
-
? html`<span class="kv-del" @click=${() => update(updateMedia(S, "--", undefined))}
|
|
5609
|
-
>✕</span
|
|
5610
|
-
>`
|
|
5611
|
-
: nothing}
|
|
5612
|
-
</div>
|
|
5613
|
-
|
|
5614
|
-
${breakpoints.map(([name, query]) => mediaBreakpointRowTemplate(name, query))}
|
|
5615
|
-
|
|
5616
|
-
<div>
|
|
5617
|
-
<span
|
|
5618
|
-
class="kv-add"
|
|
5619
|
-
style=${view.showAddBreakpointForm ? "display:none" : ""}
|
|
5620
|
-
@click=${(/** @type {any} */ _e) => {
|
|
5621
|
-
view.showAddBreakpointForm = true;
|
|
5622
|
-
renderRightPanel();
|
|
5623
|
-
}}
|
|
5624
|
-
>+ Add breakpoint</span
|
|
5625
|
-
>
|
|
5626
|
-
${view.showAddBreakpointForm
|
|
5627
|
-
? html`
|
|
5628
|
-
<div style="margin-top:4px">
|
|
5629
|
-
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
5630
|
-
<input
|
|
5631
|
-
class="field-input"
|
|
5632
|
-
placeholder="Name (e.g. Tablet)"
|
|
5633
|
-
style="flex:1"
|
|
5634
|
-
@input=${(/** @type {any} */ e) => {
|
|
5635
|
-
view.addBreakpointPreview = friendlyNameToMedia(e.target.value) || "";
|
|
5636
|
-
renderRightPanel();
|
|
5637
|
-
}}
|
|
5638
|
-
/>
|
|
5639
|
-
<span
|
|
5640
|
-
style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
|
|
5641
|
-
>${view.addBreakpointPreview}</span
|
|
5642
|
-
>
|
|
5643
|
-
</div>
|
|
5644
|
-
<div style="display:flex;gap:4px;margin-bottom:3px;align-items:center">
|
|
5645
|
-
<input class="field-input add-bp-query" value="(min-width: 768px)" style="flex:1" />
|
|
5646
|
-
</div>
|
|
5647
|
-
<div style="display:flex;gap:4px">
|
|
5648
|
-
<button
|
|
5649
|
-
class="kv-add"
|
|
5650
|
-
style="padding:2px 10px;cursor:pointer"
|
|
5651
|
-
@click=${(/** @type {any} */ e) => {
|
|
5652
|
-
const wrap = e.target.closest("div").parentElement;
|
|
5653
|
-
const nameVal = wrap.querySelector("input")?.value;
|
|
5654
|
-
const queryVal = wrap.querySelector(".add-bp-query")?.value?.trim();
|
|
5655
|
-
const key = friendlyNameToMedia(nameVal);
|
|
5656
|
-
if (key && queryVal) {
|
|
5657
|
-
view.showAddBreakpointForm = false;
|
|
5658
|
-
view.addBreakpointPreview = "";
|
|
5659
|
-
update(updateMedia(S, key, queryVal));
|
|
5660
|
-
}
|
|
5661
|
-
}}
|
|
5662
|
-
>
|
|
5663
|
-
Add
|
|
5664
|
-
</button>
|
|
5665
|
-
<button
|
|
5666
|
-
class="kv-add"
|
|
5667
|
-
style="padding:2px 10px;cursor:pointer;color:var(--fg-dim)"
|
|
5668
|
-
@click=${() => {
|
|
5669
|
-
view.showAddBreakpointForm = false;
|
|
5670
|
-
view.addBreakpointPreview = "";
|
|
5671
|
-
renderRightPanel();
|
|
5672
|
-
}}
|
|
5673
|
-
>
|
|
5674
|
-
Cancel
|
|
5675
|
-
</button>
|
|
5676
|
-
</div>
|
|
5677
|
-
</div>
|
|
5678
|
-
`
|
|
5679
|
-
: nothing}
|
|
5680
|
-
</div>
|
|
5681
|
-
`;
|
|
5682
|
-
}
|
|
5683
|
-
|
|
5684
|
-
/** Single media breakpoint row template */
|
|
5685
|
-
function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */ query) {
|
|
5686
|
-
/** @type {any} */
|
|
5687
|
-
let debounceTimer;
|
|
5688
|
-
let currentRawLabel = name;
|
|
5689
|
-
return html`
|
|
5690
|
-
<div style="margin-bottom:6px;padding:4px 0;border-bottom:1px solid var(--border)">
|
|
5691
|
-
<div style="display:flex;align-items:center;gap:4px;margin-bottom:2px">
|
|
5692
|
-
<input
|
|
5693
|
-
class="field-input"
|
|
5694
|
-
value=${mediaDisplayName(name)}
|
|
5695
|
-
style="flex:1;font-weight:600;font-size:12px"
|
|
5696
|
-
@input=${(/** @type {any} */ e) => {
|
|
5697
|
-
const newKey = friendlyNameToMedia(e.target.value);
|
|
5698
|
-
currentRawLabel = newKey || "";
|
|
5699
|
-
const rawEl = e.target.parentElement?.querySelector(".bp-raw-label");
|
|
5700
|
-
if (rawEl) rawEl.textContent = currentRawLabel;
|
|
5701
|
-
clearTimeout(debounceTimer);
|
|
5702
|
-
debounceTimer = setTimeout(() => {
|
|
5703
|
-
if (newKey && newKey !== name) {
|
|
5704
|
-
const queryEl = e.target
|
|
5705
|
-
.closest("div[style]")
|
|
5706
|
-
?.parentElement?.querySelector(".bp-query-input");
|
|
5707
|
-
let s = updateMedia(S, name, undefined);
|
|
5708
|
-
s = updateMedia(s, newKey, queryEl?.value || query);
|
|
5709
|
-
update(s);
|
|
5710
|
-
}
|
|
5711
|
-
}, 600);
|
|
5712
|
-
}}
|
|
5713
|
-
/>
|
|
5714
|
-
<span
|
|
5715
|
-
class="bp-raw-label"
|
|
5716
|
-
style="font-size:10px;color:var(--fg-dim);font-family:'SF Mono','Fira Code',monospace;white-space:nowrap"
|
|
5717
|
-
>${name}</span
|
|
5718
|
-
>
|
|
5719
|
-
<span class="kv-del" @click=${() => update(updateMedia(S, name, undefined))}>✕</span>
|
|
5720
|
-
</div>
|
|
5721
|
-
<div style="display:flex;gap:4px;align-items:center">
|
|
5722
|
-
<input
|
|
5723
|
-
class="field-input bp-query-input"
|
|
5724
|
-
value=${query}
|
|
5725
|
-
style="flex:1"
|
|
5726
|
-
@input=${(/** @type {any} */ e) => {
|
|
5727
|
-
clearTimeout(debounceTimer);
|
|
5728
|
-
debounceTimer = setTimeout(() => update(updateMedia(S, name, e.target.value)), 400);
|
|
5729
|
-
}}
|
|
5730
|
-
/>
|
|
5731
|
-
</div>
|
|
5732
|
-
</div>
|
|
5733
|
-
`;
|
|
5734
|
-
}
|
|
5735
|
-
|
|
5736
|
-
// ─── Style Sidebar (metadata-driven) ───────────────────────────────────────────
|
|
5737
|
-
|
|
5738
|
-
// UNIT_RE — imported from ui/unit-selector.js
|
|
5739
|
-
|
|
5740
|
-
// inferInputType — imported from studio-utils.js
|
|
5741
|
-
|
|
5742
|
-
// ─── Style panel ────────────────────────────────────────────────────────────
|
|
5743
|
-
// Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
|
|
5744
|
-
|
|
5745
|
-
/** Check if a selection path is inside a $map template (contains [..., "children", "map", ...]). */
|
|
5746
|
-
function isInsideMapTemplate(/** @type {any} */ path) {
|
|
5747
|
-
if (!path) return false;
|
|
5748
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
5749
|
-
if (path[i] === "children" && path[i + 1] === "map") return true;
|
|
5750
|
-
}
|
|
5751
|
-
return false;
|
|
5752
|
-
}
|
|
5753
|
-
|
|
5754
|
-
/**
|
|
5755
|
-
* Field row with binding toggle — allows switching between static value and signal binding.
|
|
5756
|
-
* rawValue can be a string/bool (static) or { $ref: "..." } (bound).
|
|
5757
|
-
*/
|
|
5758
|
-
function bindableFieldRow(
|
|
5759
|
-
/** @type {any} */ label,
|
|
5760
|
-
/** @type {any} */ type,
|
|
5761
|
-
/** @type {any} */ rawValue,
|
|
5762
|
-
/** @type {any} */ onChange,
|
|
5763
|
-
/** @type {any} */ filterFn = null,
|
|
5764
|
-
/** @type {any} */ extraSignals = null,
|
|
5765
|
-
) {
|
|
5766
|
-
const defs = S.document.state || {};
|
|
5767
|
-
const isBound = typeof rawValue === "object" && rawValue !== null && rawValue.$ref;
|
|
5768
|
-
|
|
5769
|
-
const signalDefs = Object.entries(defs).filter(([, d]) =>
|
|
5770
|
-
filterFn ? filterFn(d) : !d.$handler && d.$prototype !== "Function",
|
|
5771
|
-
);
|
|
5772
|
-
|
|
5773
|
-
/** @type {any} */
|
|
5774
|
-
let debounce;
|
|
5775
|
-
const onInput = (/** @type {any} */ e) => {
|
|
5776
|
-
clearTimeout(debounce);
|
|
5777
|
-
debounce = setTimeout(() => onChange(e.target.value), 400);
|
|
5778
|
-
};
|
|
5779
|
-
|
|
5780
|
-
const staticVal = isBound ? "" : (rawValue ?? "");
|
|
5781
|
-
const staticTpl =
|
|
5782
|
-
type === "textarea"
|
|
5783
|
-
? html`<sp-textfield multiline size="s" value=${staticVal} @input=${onInput}></sp-textfield>`
|
|
5784
|
-
: type === "checkbox"
|
|
5785
|
-
? html`<sp-checkbox
|
|
5786
|
-
?checked=${!!staticVal}
|
|
5787
|
-
@change=${(/** @type {any} */ e) => onChange(e.target.checked)}
|
|
5788
|
-
></sp-checkbox>`
|
|
5789
|
-
: html`<sp-textfield size="s" value=${staticVal} @input=${onInput}></sp-textfield>`;
|
|
5790
|
-
|
|
5791
|
-
const boundTpl = html`
|
|
5792
|
-
<sp-picker
|
|
5793
|
-
size="s"
|
|
5794
|
-
quiet
|
|
5795
|
-
placeholder="— select signal —"
|
|
5796
|
-
value=${isBound && rawValue.$ref ? rawValue.$ref : nothing}
|
|
5797
|
-
@change=${(/** @type {any} */ e) => {
|
|
5798
|
-
if (e.target.value) onChange({ $ref: e.target.value });
|
|
5799
|
-
else onChange(undefined);
|
|
5800
|
-
}}
|
|
5801
|
-
>
|
|
5802
|
-
${signalDefs.map(
|
|
5803
|
-
([defName]) => html`<sp-menu-item value=${`#/state/${defName}`}>${defName}</sp-menu-item>`,
|
|
5804
|
-
)}
|
|
5805
|
-
${extraSignals
|
|
5806
|
-
? html`
|
|
5807
|
-
<sp-menu-divider></sp-menu-divider>
|
|
5808
|
-
${extraSignals.map(
|
|
5809
|
-
(/** @type {any} */ sig) =>
|
|
5810
|
-
html`<sp-menu-item value=${sig.value}>${sig.label}</sp-menu-item>`,
|
|
5811
|
-
)}
|
|
5812
|
-
`
|
|
5813
|
-
: nothing}
|
|
5814
|
-
</sp-picker>
|
|
5815
|
-
`;
|
|
5816
|
-
|
|
5817
|
-
const onToggle = () => {
|
|
5818
|
-
if (isBound) {
|
|
5819
|
-
const ref = rawValue.$ref;
|
|
5820
|
-
const defName = ref.startsWith("#/state/") ? ref.slice(8) : ref;
|
|
5821
|
-
const def = defs[defName];
|
|
5822
|
-
let staticVal = "";
|
|
5823
|
-
if (def && def.default !== undefined)
|
|
5824
|
-
staticVal =
|
|
5825
|
-
typeof def.default === "object" ? JSON.stringify(def.default) : String(def.default);
|
|
5826
|
-
onChange(staticVal || undefined);
|
|
5827
|
-
} else {
|
|
5828
|
-
if (signalDefs.length > 0) {
|
|
5829
|
-
onChange({ $ref: `#/state/${signalDefs[0][0]}` });
|
|
5830
|
-
} else if (extraSignals?.length > 0) {
|
|
5831
|
-
onChange({ $ref: extraSignals[0].value });
|
|
5832
|
-
}
|
|
5833
|
-
}
|
|
5834
|
-
};
|
|
5835
|
-
|
|
5836
|
-
return html`
|
|
5837
|
-
<div class="field-row">
|
|
5838
|
-
<sp-field-label size="s">${label}</sp-field-label>
|
|
5839
|
-
${isBound ? boundTpl : staticTpl}
|
|
5840
|
-
<sp-action-button
|
|
5841
|
-
size="xs"
|
|
5842
|
-
quiet
|
|
5843
|
-
title=${isBound ? "Unbind (switch to static)" : "Bind to signal"}
|
|
5844
|
-
@click=${onToggle}
|
|
5845
|
-
>${isBound ? "\u26A1" : "\u2194"}</sp-action-button
|
|
5846
|
-
>
|
|
5847
|
-
</div>
|
|
5848
|
-
`;
|
|
5849
|
-
}
|
|
5850
|
-
|
|
5851
|
-
/** Key-value pair row for styles / attributes */
|
|
5852
|
-
function kvRow(
|
|
5853
|
-
/** @type {any} */ key,
|
|
5854
|
-
/** @type {any} */ value,
|
|
5855
|
-
/** @type {any} */ onChange,
|
|
5856
|
-
/** @type {any} */ onDelete,
|
|
5857
|
-
/** @type {any} */ datalistId = null,
|
|
5858
|
-
) {
|
|
5859
|
-
/** @type {any} */
|
|
5860
|
-
let debounceTimer;
|
|
5861
|
-
let currentKey = key;
|
|
5862
|
-
let currentVal = value;
|
|
5863
|
-
const commit = () => {
|
|
5864
|
-
clearTimeout(debounceTimer);
|
|
5865
|
-
debounceTimer = setTimeout(() => onChange(currentKey, currentVal), 400);
|
|
5866
|
-
};
|
|
5867
|
-
const placeholder = datalistId === "css-props" ? getCssInitialMap().get(key) || "" : "";
|
|
5868
|
-
return html`
|
|
5869
|
-
<div class="kv-row">
|
|
5870
|
-
<sp-textfield
|
|
5871
|
-
size="s"
|
|
5872
|
-
class="kv-key"
|
|
5873
|
-
value=${key}
|
|
5874
|
-
@input=${(/** @type {any} */ e) => {
|
|
5875
|
-
currentKey = e.target.value;
|
|
5876
|
-
commit();
|
|
5877
|
-
}}
|
|
5878
|
-
@change=${datalistId === "css-props"
|
|
5879
|
-
? (/** @type {any} */ e) => {
|
|
5880
|
-
const el = e.target.closest(".kv-row")?.querySelector(".kv-val");
|
|
5881
|
-
if (el) el.setAttribute("placeholder", getCssInitialMap().get(e.target.value) || "");
|
|
5882
|
-
}
|
|
5883
|
-
: nothing}
|
|
5884
|
-
></sp-textfield>
|
|
5885
|
-
<sp-textfield
|
|
5886
|
-
size="s"
|
|
5887
|
-
class="kv-val"
|
|
5888
|
-
value=${value}
|
|
5889
|
-
placeholder=${placeholder}
|
|
5890
|
-
@input=${(/** @type {any} */ e) => {
|
|
5891
|
-
currentVal = e.target.value;
|
|
5892
|
-
commit();
|
|
5893
|
-
}}
|
|
5894
|
-
></sp-textfield>
|
|
5895
|
-
<sp-action-button size="xs" quiet @click=${onDelete}>
|
|
5896
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
5897
|
-
</sp-action-button>
|
|
5898
|
-
</div>
|
|
5899
|
-
`;
|
|
5900
|
-
}
|
|
5901
|
-
|
|
5902
|
-
// ─── Source view ──────────────────────────────────────────────────────────────
|
|
5903
|
-
|
|
5904
|
-
function _renderSourceView(/** @type {any} */ container) {
|
|
5905
|
-
if (!S.selection) {
|
|
5906
|
-
litRender(
|
|
5907
|
-
html`
|
|
5908
|
-
<textarea
|
|
5909
|
-
id="source-view"
|
|
5910
|
-
.value=${live(JSON.stringify(S.document, null, 2))}
|
|
5911
|
-
@blur=${(/** @type {any} */ e) => {
|
|
5912
|
-
try {
|
|
5913
|
-
const parsed = JSON.parse(e.target.value);
|
|
5914
|
-
update({ ...S, document: parsed, dirty: true });
|
|
5915
|
-
} catch {}
|
|
5916
|
-
}}
|
|
5917
|
-
></textarea>
|
|
5918
|
-
`,
|
|
5919
|
-
container,
|
|
5920
|
-
);
|
|
5921
|
-
return;
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
const node = getNodeAtPath(S.document, S.selection);
|
|
5925
|
-
litRender(
|
|
5926
|
-
html`
|
|
5927
|
-
<textarea id="source-view" readonly .value=${live(JSON.stringify(node, null, 2))}></textarea>
|
|
5928
|
-
`,
|
|
5929
|
-
container,
|
|
5930
|
-
);
|
|
5931
|
-
}
|
|
5932
|
-
|
|
5933
|
-
// ─── Function editor (Monaco JS mode) ─────────────────────────────────────────
|
|
5934
|
-
|
|
5935
|
-
function renderFunctionEditor() {
|
|
5936
|
-
const editing = S.ui.editingFunction;
|
|
5937
|
-
|
|
5938
|
-
// If editor already exists and matches current target, just sync value
|
|
5939
|
-
if (view.functionEditor && view.functionEditor._editingTarget === JSON.stringify(editing)) {
|
|
5940
|
-
const body = getFunctionBody(editing);
|
|
5941
|
-
const currentVal = view.functionEditor.getValue();
|
|
5942
|
-
if (currentVal !== body) {
|
|
5943
|
-
view.functionEditor._ignoreNextChange = true;
|
|
5944
|
-
view.functionEditor.setValue(body);
|
|
5945
|
-
}
|
|
5946
|
-
return;
|
|
5947
|
-
}
|
|
5948
|
-
|
|
5949
|
-
// Dispose previous editors
|
|
5950
|
-
if (view.functionEditor) {
|
|
5951
|
-
view.functionEditor.dispose();
|
|
5952
|
-
view.functionEditor = null;
|
|
5953
|
-
}
|
|
5954
|
-
if (view.monacoEditor) {
|
|
5955
|
-
view.monacoEditor.dispose();
|
|
5956
|
-
view.monacoEditor = null;
|
|
5957
|
-
}
|
|
5958
|
-
|
|
5959
|
-
// Clean up canvas DnD and event handlers
|
|
5960
|
-
for (const fn of view.canvasDndCleanups) fn();
|
|
5961
|
-
view.canvasDndCleanups = [];
|
|
5962
|
-
for (const fn of view.canvasEventCleanups) fn();
|
|
5963
|
-
view.canvasEventCleanups = [];
|
|
5964
|
-
canvasPanels.length = 0;
|
|
5965
|
-
|
|
5966
|
-
litRender(nothing, canvasWrap);
|
|
5967
|
-
canvasWrap.style.padding = "0";
|
|
5968
|
-
|
|
5969
|
-
// Toolbar breadcrumb handles context display — re-render it
|
|
5970
|
-
renderToolbar();
|
|
5971
|
-
|
|
5972
|
-
// Editor container
|
|
5973
|
-
/** @type {HTMLDivElement | null} */
|
|
5974
|
-
let editorContainer = null;
|
|
5975
|
-
litRender(
|
|
5976
|
-
html`<div
|
|
5977
|
-
class="source-editor"
|
|
5978
|
-
${ref((el) => {
|
|
5979
|
-
if (el) editorContainer = /** @type {HTMLDivElement} */ (el);
|
|
5980
|
-
})}
|
|
5981
|
-
></div>`,
|
|
5982
|
-
canvasWrap,
|
|
5983
|
-
);
|
|
5984
|
-
|
|
5985
|
-
const body = getFunctionBody(editing);
|
|
5986
|
-
const args = getFunctionArgs(editing, S);
|
|
5987
|
-
|
|
5988
|
-
view.functionEditor = monaco.editor.create(/** @type {any} */ (editorContainer), {
|
|
5989
|
-
value: body,
|
|
5990
|
-
language: "javascript",
|
|
5991
|
-
theme: "vs-dark",
|
|
5992
|
-
automaticLayout: true,
|
|
5993
|
-
minimap: { enabled: false },
|
|
5994
|
-
fontSize: 12,
|
|
5995
|
-
fontFamily: "'SF Mono', 'Fira Code', 'Consolas', monospace",
|
|
5996
|
-
lineNumbers: "on",
|
|
5997
|
-
scrollBeyondLastLine: false,
|
|
5998
|
-
wordWrap: "on",
|
|
5999
|
-
tabSize: 2,
|
|
6000
|
-
});
|
|
6001
|
-
view.functionEditor._editingTarget = JSON.stringify(editing);
|
|
6002
|
-
|
|
6003
|
-
// Format on open — show pretty-printed code, then run initial lint
|
|
6004
|
-
codeService("format", { code: body, args }).then((result) => {
|
|
6005
|
-
if (result?.code != null && view.functionEditor) {
|
|
6006
|
-
view.functionEditor._ignoreNextChange = true;
|
|
6007
|
-
view.functionEditor.setValue(result.code);
|
|
6008
|
-
}
|
|
6009
|
-
});
|
|
6010
|
-
codeService("lint", { code: body, args }).then((result) => {
|
|
6011
|
-
if (result?.diagnostics && view.functionEditor)
|
|
6012
|
-
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
6013
|
-
});
|
|
6014
|
-
|
|
6015
|
-
// Debounced sync back to state + lint on edit
|
|
6016
|
-
/** @type {any} */
|
|
6017
|
-
let syncDebounce;
|
|
6018
|
-
/** @type {any} */
|
|
6019
|
-
let lintDebounce;
|
|
6020
|
-
let lintGen = 0;
|
|
6021
|
-
view.functionEditor.onDidChangeModelContent(() => {
|
|
6022
|
-
if (view.functionEditor._ignoreNextChange) {
|
|
6023
|
-
view.functionEditor._ignoreNextChange = false;
|
|
6024
|
-
return;
|
|
6025
|
-
}
|
|
6026
|
-
|
|
6027
|
-
clearTimeout(syncDebounce);
|
|
6028
|
-
syncDebounce = setTimeout(() => {
|
|
6029
|
-
const newBody = view.functionEditor.getValue();
|
|
6030
|
-
if (editing.type === "def") {
|
|
6031
|
-
update(updateDef(S, editing.defName, { body: newBody }));
|
|
6032
|
-
} else if (editing.type === "event") {
|
|
6033
|
-
const node = getNodeAtPath(S.document, editing.path);
|
|
6034
|
-
const current = node?.[editing.eventKey] || {};
|
|
6035
|
-
update(
|
|
6036
|
-
updateProperty(S, editing.path, editing.eventKey, {
|
|
6037
|
-
...current,
|
|
6038
|
-
$prototype: "Function",
|
|
6039
|
-
body: newBody,
|
|
6040
|
-
}),
|
|
6041
|
-
);
|
|
6042
|
-
}
|
|
6043
|
-
renderLeftPanel();
|
|
6044
|
-
}, 500);
|
|
6045
|
-
|
|
6046
|
-
clearTimeout(lintDebounce);
|
|
6047
|
-
lintDebounce = setTimeout(() => {
|
|
6048
|
-
const gen = ++lintGen;
|
|
6049
|
-
const currentCode = view.functionEditor.getValue();
|
|
6050
|
-
codeService("lint", { code: currentCode, args }).then((result) => {
|
|
6051
|
-
if (gen !== lintGen) return;
|
|
6052
|
-
if (result?.diagnostics && view.functionEditor)
|
|
6053
|
-
setLintMarkers(view.functionEditor, result.diagnostics);
|
|
6054
|
-
});
|
|
6055
|
-
}, 750);
|
|
6056
|
-
});
|
|
6057
|
-
}
|
|
6058
|
-
|
|
6059
|
-
function getFunctionBody(/** @type {any} */ editing) {
|
|
6060
|
-
if (editing.type === "def") {
|
|
6061
|
-
return S.document.state?.[editing.defName]?.body || "";
|
|
6062
|
-
} else if (editing.type === "event") {
|
|
6063
|
-
const node = getNodeAtPath(S.document, editing.path);
|
|
6064
|
-
return node?.[editing.eventKey]?.body || "";
|
|
6065
|
-
}
|
|
6066
|
-
return "";
|
|
6067
|
-
}
|
|
6068
|
-
|
|
6069
|
-
// Register Monaco JS completion provider for state scope variables (once)
|
|
6070
|
-
function registerFunctionCompletions() {
|
|
6071
|
-
if (view._completionRegistered) return;
|
|
6072
|
-
view._completionRegistered = true;
|
|
6073
|
-
monaco.languages.registerCompletionItemProvider("javascript", {
|
|
6074
|
-
triggerCharacters: ["."],
|
|
6075
|
-
provideCompletionItems(model, position) {
|
|
6076
|
-
const defs = S?.document?.state || {};
|
|
6077
|
-
const word = model.getWordUntilPosition(position);
|
|
6078
|
-
const range = {
|
|
6079
|
-
startLineNumber: position.lineNumber,
|
|
6080
|
-
endLineNumber: position.lineNumber,
|
|
6081
|
-
startColumn: word.startColumn,
|
|
6082
|
-
endColumn: word.endColumn,
|
|
6083
|
-
};
|
|
6084
|
-
|
|
6085
|
-
const suggestions = Object.entries(defs).map(([key, def]) => {
|
|
6086
|
-
let kind = monaco.languages.CompletionItemKind.Variable;
|
|
6087
|
-
if (def?.$prototype === "Function" || def?.$handler)
|
|
6088
|
-
kind = monaco.languages.CompletionItemKind.Function;
|
|
6089
|
-
else if (def?.$prototype) kind = monaco.languages.CompletionItemKind.Property;
|
|
6090
|
-
return {
|
|
6091
|
-
label: `state.${key}`,
|
|
6092
|
-
kind,
|
|
6093
|
-
insertText: `state.${key}`,
|
|
6094
|
-
range,
|
|
6095
|
-
};
|
|
6096
|
-
});
|
|
6097
|
-
return { suggestions };
|
|
6098
|
-
},
|
|
6099
|
-
});
|
|
6100
|
-
}
|
|
2824
|
+
// ─── Source/Function editors: delegated to panels/editors.js ─────────────────
|
|
6101
2825
|
|
|
6102
2826
|
// ─── Toolbar (delegated to panels/toolbar.js) ────────────────────────────────
|
|
6103
2827
|
|