@jxsuite/studio 0.6.1 → 0.7.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 +151438 -141129
- package/dist/studio.js.map +84 -18
- package/package.json +2 -2
- package/src/markdown/md-convert.js +18 -16
- package/src/panels/activity-bar.js +22 -0
- package/src/panels/elements-panel.js +148 -0
- package/src/panels/git-panel.js +280 -0
- package/src/panels/layers-panel.js +270 -0
- package/src/panels/left-panel.js +141 -0
- package/src/panels/right-panel.js +3 -2
- package/src/panels/style-inputs.js +176 -0
- package/src/panels/style-panel.js +651 -0
- package/src/panels/style-utils.js +193 -0
- package/src/panels/stylebook-layers-panel.js +103 -0
- package/src/platforms/devserver.js +113 -0
- package/src/state.js +7 -0
- package/src/studio.js +38 -1490
- package/src/ui/spectrum.js +4 -0
package/src/studio.js
CHANGED
|
@@ -16,10 +16,7 @@ import {
|
|
|
16
16
|
updateStyle,
|
|
17
17
|
updateAttribute,
|
|
18
18
|
updateDef,
|
|
19
|
-
updateMediaStyle,
|
|
20
19
|
updateMedia,
|
|
21
|
-
updateNestedStyle,
|
|
22
|
-
updateMediaNestedStyle,
|
|
23
20
|
pushDocument,
|
|
24
21
|
popDocument,
|
|
25
22
|
updateProp,
|
|
@@ -28,9 +25,7 @@ import {
|
|
|
28
25
|
renameSwitchCase,
|
|
29
26
|
applyMutation,
|
|
30
27
|
getNodeAtPath,
|
|
31
|
-
flattenTree,
|
|
32
28
|
nodeLabel,
|
|
33
|
-
pathKey,
|
|
34
29
|
pathsEqual,
|
|
35
30
|
parentElementPath,
|
|
36
31
|
childIndex,
|
|
@@ -41,8 +36,6 @@ import {
|
|
|
41
36
|
elToPath,
|
|
42
37
|
canvasPanels,
|
|
43
38
|
VOID_ELEMENTS,
|
|
44
|
-
COMMON_SELECTORS,
|
|
45
|
-
isNestedSelector,
|
|
46
39
|
debouncedStyleCommit,
|
|
47
40
|
stripEventHandlers,
|
|
48
41
|
registerRenderer,
|
|
@@ -77,7 +70,6 @@ import {
|
|
|
77
70
|
isEditing,
|
|
78
71
|
getActiveElement,
|
|
79
72
|
isEditableBlock,
|
|
80
|
-
isInlineElement,
|
|
81
73
|
isInlineInContext,
|
|
82
74
|
getInlineActions,
|
|
83
75
|
} from "./editor/inline-edit.js";
|
|
@@ -88,10 +80,7 @@ import {
|
|
|
88
80
|
} from "./editor/slash-menu.js";
|
|
89
81
|
import { toggleInlineFormat, isTagActiveInSelection } from "./editor/inline-format.js";
|
|
90
82
|
import {
|
|
91
|
-
camelToKebab,
|
|
92
83
|
camelToLabel,
|
|
93
|
-
kebabToLabel,
|
|
94
|
-
propLabel,
|
|
95
84
|
attrLabel,
|
|
96
85
|
inferInputType,
|
|
97
86
|
findCollectionSchema,
|
|
@@ -172,18 +161,16 @@ import { styleMap } from "lit-html/directives/style-map.js";
|
|
|
172
161
|
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
173
162
|
|
|
174
163
|
import webdata from "../data/webdata.json";
|
|
175
|
-
import cssMeta from "../data/css-meta.json";
|
|
176
164
|
import htmlMeta from "../data/html-meta.json";
|
|
177
165
|
import stylebookMeta from "../data/stylebook-meta.json";
|
|
178
166
|
import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
|
|
167
|
+
import { renderGitPanel } from "./panels/git-panel.js";
|
|
179
168
|
|
|
180
169
|
// ─── Spectrum Web Components ──────────────────────────────────────────────────
|
|
181
170
|
// Explicit class imports + registration — bare side-effect imports are tree-shaken
|
|
182
171
|
// by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
|
|
183
172
|
import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
|
|
184
173
|
import { renderFieldRow } from "./ui/field-row.js";
|
|
185
|
-
import { widgetForType as _widgetForType } from "./ui/widgets.js";
|
|
186
|
-
import { computeInheritedStyle } from "./utils/inherited-style.js";
|
|
187
174
|
import "./ui/panel-resize.js";
|
|
188
175
|
import { showContextMenu, dismissContextMenu } from "./editor/context-menu.js";
|
|
189
176
|
import { convertToComponent } from "./editor/convert-to-component.js";
|
|
@@ -195,7 +182,10 @@ import { renderDefsEditor } from "./settings/defs-editor.js";
|
|
|
195
182
|
import * as toolbarPanel from "./panels/toolbar.js";
|
|
196
183
|
import * as overlaysPanel from "./panels/overlays.js";
|
|
197
184
|
import * as rightPanelMod from "./panels/right-panel.js";
|
|
198
|
-
import
|
|
185
|
+
import * as leftPanelMod from "./panels/left-panel.js";
|
|
186
|
+
import { mediaDisplayName } from "./panels/shared.js";
|
|
187
|
+
import { initCssData, getCssInitialMap } from "./panels/style-utils.js";
|
|
188
|
+
import { widgetForType } from "./panels/style-inputs.js";
|
|
199
189
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
200
190
|
|
|
201
191
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
@@ -740,8 +730,7 @@ litRender(
|
|
|
740
730
|
datalistHost,
|
|
741
731
|
);
|
|
742
732
|
|
|
743
|
-
|
|
744
|
-
const cssInitialMap = new Map(/** @type {any} */ (webdata.cssProps));
|
|
733
|
+
initCssData(webdata);
|
|
745
734
|
|
|
746
735
|
// Persistent render hosts for lit-html (must be before bootstrap/render)
|
|
747
736
|
let zoomIndicatorHost = document.createElement("div");
|
|
@@ -798,15 +787,37 @@ overlaysPanel.mount({
|
|
|
798
787
|
|
|
799
788
|
rightPanelMod.mount({
|
|
800
789
|
propertiesSidebarTemplate,
|
|
801
|
-
|
|
790
|
+
getCanvasMode: () => canvasMode,
|
|
802
791
|
renderCanvas: () => renderCanvas(),
|
|
803
792
|
updateForcedPseudoPreview,
|
|
804
793
|
});
|
|
805
794
|
|
|
795
|
+
leftPanelMod.mount({
|
|
796
|
+
getCanvasMode: () => canvasMode,
|
|
797
|
+
renderImportsTemplate,
|
|
798
|
+
renderFilesTemplate,
|
|
799
|
+
renderSignalsTemplate,
|
|
800
|
+
renderDataExplorerTemplate,
|
|
801
|
+
renderHeadTemplate,
|
|
802
|
+
renderGitPanel,
|
|
803
|
+
renderCanvas: () => renderCanvas(),
|
|
804
|
+
defCategory,
|
|
805
|
+
defBadgeLabel,
|
|
806
|
+
navigateToComponent,
|
|
807
|
+
selectStylebookTag,
|
|
808
|
+
stylebookMeta,
|
|
809
|
+
webdata,
|
|
810
|
+
defaultDef,
|
|
811
|
+
registerLayersDnD,
|
|
812
|
+
registerElementsDnD,
|
|
813
|
+
registerComponentsDnD,
|
|
814
|
+
setupTreeKeyboard,
|
|
815
|
+
});
|
|
816
|
+
|
|
806
817
|
// Register all renderers with the store so render()/renderOnly() work
|
|
807
818
|
registerRenderer("toolbar", () => toolbarPanel.render());
|
|
808
819
|
registerRenderer("activityBar", () => renderActivityBar(S));
|
|
809
|
-
registerRenderer("leftPanel", () =>
|
|
820
|
+
registerRenderer("leftPanel", () => leftPanelMod.render());
|
|
810
821
|
registerRenderer("canvas", () => renderCanvas());
|
|
811
822
|
registerRenderer("rightPanel", () => rightPanelMod.render());
|
|
812
823
|
registerRenderer("overlays", () => overlaysPanel.render());
|
|
@@ -815,20 +826,7 @@ setStatusbarRenderer(() => renderStatusbar(S));
|
|
|
815
826
|
mountStatusbar();
|
|
816
827
|
|
|
817
828
|
function safeRenderLeftPanel() {
|
|
818
|
-
|
|
819
|
-
ensureLitState(leftPanel);
|
|
820
|
-
renderLeftPanel();
|
|
821
|
-
} catch (e) {
|
|
822
|
-
console.error("renderLeftPanel error:", e);
|
|
823
|
-
try {
|
|
824
|
-
leftPanel.textContent = "";
|
|
825
|
-
// @ts-ignore
|
|
826
|
-
delete leftPanel["_$litPart$"];
|
|
827
|
-
renderLeftPanel();
|
|
828
|
-
} catch (e2) {
|
|
829
|
-
console.error("renderLeftPanel retry failed:", e2);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
829
|
+
leftPanelMod.render();
|
|
832
830
|
}
|
|
833
831
|
|
|
834
832
|
function safeRenderRightPanel() {
|
|
@@ -3193,331 +3191,13 @@ function handleComponentSlashSelect(cmd) {
|
|
|
3193
3191
|
update(s);
|
|
3194
3192
|
}
|
|
3195
3193
|
|
|
3196
|
-
// ─── Left panel:
|
|
3194
|
+
// ─── Left panel: delegated to panels/left-panel.js ───────────────────────────
|
|
3197
3195
|
|
|
3198
3196
|
function renderLeftPanel() {
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
/** @type {any} */
|
|
3202
|
-
let content;
|
|
3203
|
-
if (tab === "layers")
|
|
3204
|
-
content = canvasMode === "settings" ? renderStylebookLayersTemplate() : renderLayersTemplate();
|
|
3205
|
-
else if (tab === "imports")
|
|
3206
|
-
content = renderImportsTemplate({
|
|
3207
|
-
renderLeftPanel,
|
|
3208
|
-
documentPath: S.documentPath,
|
|
3209
|
-
documentElements: S.document.$elements || [],
|
|
3210
|
-
applyMutation: (/** @type {any} */ fn) => {
|
|
3211
|
-
S = applyMutation(S, fn);
|
|
3212
|
-
update(S);
|
|
3213
|
-
},
|
|
3214
|
-
});
|
|
3215
|
-
else if (tab === "files") content = renderFilesTemplate();
|
|
3216
|
-
else if (tab === "blocks") content = renderElementsTemplate();
|
|
3217
|
-
else if (tab === "state")
|
|
3218
|
-
content = renderSignalsTemplate(S, { renderLeftPanel, renderCanvas, updateSession });
|
|
3219
|
-
else if (tab === "data")
|
|
3220
|
-
content = renderDataExplorerTemplate(S.document.state, view.liveScope, {
|
|
3221
|
-
renderCanvas,
|
|
3222
|
-
renderLeftPanel,
|
|
3223
|
-
defCategory,
|
|
3224
|
-
defBadgeLabel,
|
|
3225
|
-
});
|
|
3226
|
-
else if (tab === "head") {
|
|
3227
|
-
// In content mode, title/$head live in S.content.frontmatter, not S.document
|
|
3228
|
-
const isContent = S.mode === "content";
|
|
3229
|
-
const fm = S.content?.frontmatter ?? {};
|
|
3230
|
-
const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
|
|
3231
|
-
content = renderHeadTemplate({
|
|
3232
|
-
document: headDoc,
|
|
3233
|
-
applyMutation: isContent
|
|
3234
|
-
? (/** @type {any} */ fn) => {
|
|
3235
|
-
// Apply mutation to a temporary doc, then sync title/$head back to frontmatter
|
|
3236
|
-
const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
|
|
3237
|
-
fn(tmp);
|
|
3238
|
-
if (tmp.title !== fm.title) S = updateFrontmatter(S, "title", tmp.title);
|
|
3239
|
-
// Always sync $head (may have been created, modified, or emptied)
|
|
3240
|
-
const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
|
|
3241
|
-
S = updateFrontmatter(S, "$head", newHead);
|
|
3242
|
-
update(S);
|
|
3243
|
-
}
|
|
3244
|
-
: (/** @type {any} */ fn) => {
|
|
3245
|
-
S = applyMutation(S, fn);
|
|
3246
|
-
update(S);
|
|
3247
|
-
},
|
|
3248
|
-
renderLeftPanel,
|
|
3249
|
-
});
|
|
3250
|
-
} else content = nothing;
|
|
3251
|
-
|
|
3252
|
-
litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
|
|
3253
|
-
|
|
3254
|
-
// Post-render side effects
|
|
3255
|
-
if (tab === "layers" && canvasMode !== "settings") registerLayersDnD();
|
|
3256
|
-
else if (tab === "imports") {
|
|
3257
|
-
/* no post-render DnD needed */
|
|
3258
|
-
} else if (tab === "blocks") {
|
|
3259
|
-
registerElementsDnD();
|
|
3260
|
-
registerComponentsDnD();
|
|
3261
|
-
} else if (tab === "files") {
|
|
3262
|
-
const tree = /** @type {any} */ (leftPanel)?.querySelector(".file-tree");
|
|
3263
|
-
if (tree) setupTreeKeyboard(tree);
|
|
3264
|
-
}
|
|
3197
|
+
leftPanelMod.render();
|
|
3265
3198
|
}
|
|
3266
3199
|
|
|
3267
|
-
/**
|
|
3268
|
-
function renderLayersTemplate() {
|
|
3269
|
-
// Clean up previous DnD registrations
|
|
3270
|
-
for (const fn of view.dndCleanups) fn();
|
|
3271
|
-
view.dndCleanups = [];
|
|
3272
|
-
|
|
3273
|
-
const rows = flattenTree(S.document);
|
|
3274
|
-
const collapsed = S._collapsed || (S._collapsed = new Set());
|
|
3275
|
-
|
|
3276
|
-
// Build layer rows
|
|
3277
|
-
/** @type {any[]} */
|
|
3278
|
-
const layerRows = [];
|
|
3279
|
-
for (const { node, path, depth, nodeType } of rows) {
|
|
3280
|
-
// Check if any ancestor is collapsed
|
|
3281
|
-
let hidden = false;
|
|
3282
|
-
for (let d = 1; d <= path.length; d++) {
|
|
3283
|
-
const sub = path.slice(0, d);
|
|
3284
|
-
if (d < path.length && collapsed.has(pathKey(sub))) {
|
|
3285
|
-
hidden = true;
|
|
3286
|
-
break;
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
if (hidden) continue;
|
|
3290
|
-
|
|
3291
|
-
// In content mode, skip the document root row (it's not a real element)
|
|
3292
|
-
if (S.mode === "content" && path.length === 0) continue;
|
|
3293
|
-
|
|
3294
|
-
// Text node children: display-only row with truncated preview
|
|
3295
|
-
if (nodeType === "text") {
|
|
3296
|
-
const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
|
|
3297
|
-
layerRows.push(html`
|
|
3298
|
-
<div
|
|
3299
|
-
class="layer-row"
|
|
3300
|
-
style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
|
|
3301
|
-
>
|
|
3302
|
-
<span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
|
|
3303
|
-
<span class="layer-label">${textPreview}</span>
|
|
3304
|
-
</div>
|
|
3305
|
-
`);
|
|
3306
|
-
continue;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
// Skip inline elements
|
|
3310
|
-
if (path.length >= 2 && nodeType === "element") {
|
|
3311
|
-
const pPath = parentElementPath(path);
|
|
3312
|
-
const parentNode = pPath ? getNodeAtPath(S.document, pPath) : null;
|
|
3313
|
-
if (parentNode && isInlineElement(node, parentNode)) continue;
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
const key = pathKey(path);
|
|
3317
|
-
const isSelected = pathsEqual(path, S.selection);
|
|
3318
|
-
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
3319
|
-
const hasMapChildren =
|
|
3320
|
-
node.children && typeof node.children === "object" && node.children.$prototype === "Array";
|
|
3321
|
-
const hasCases =
|
|
3322
|
-
node.$switch &&
|
|
3323
|
-
node.cases &&
|
|
3324
|
-
typeof node.cases === "object" &&
|
|
3325
|
-
Object.keys(node.cases).length > 0;
|
|
3326
|
-
const isExpandable =
|
|
3327
|
-
hasChildren || hasMapChildren || hasCases || (nodeType === "map" && node.map);
|
|
3328
|
-
const isVoidEl = VOID_ELEMENTS.has((node.tagName || "div").toLowerCase());
|
|
3329
|
-
|
|
3330
|
-
// Badge
|
|
3331
|
-
/** @type {any} */
|
|
3332
|
-
let badgeClass, badgeText, badgeTitle;
|
|
3333
|
-
if (nodeType === "map") {
|
|
3334
|
-
badgeClass = "layer-tag map-tag";
|
|
3335
|
-
badgeText = "↻";
|
|
3336
|
-
badgeTitle = "Repeater (mapped array)";
|
|
3337
|
-
} else if (nodeType === "case" || nodeType === "case-ref") {
|
|
3338
|
-
badgeClass = "layer-tag case-tag";
|
|
3339
|
-
badgeText = path[path.length - 1];
|
|
3340
|
-
badgeTitle = `$switch case: ${path[path.length - 1]}`;
|
|
3341
|
-
} else if (node.$switch) {
|
|
3342
|
-
badgeClass = "layer-tag switch-tag";
|
|
3343
|
-
badgeText = "⇄";
|
|
3344
|
-
badgeTitle = "$switch";
|
|
3345
|
-
} else {
|
|
3346
|
-
badgeClass = "layer-tag";
|
|
3347
|
-
badgeText = node.tagName || "div";
|
|
3348
|
-
badgeTitle = undefined;
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
// Label
|
|
3352
|
-
/** @type {any} */
|
|
3353
|
-
let labelText, labelItalic;
|
|
3354
|
-
if (nodeType === "case-ref") {
|
|
3355
|
-
labelText = node.$ref || "external";
|
|
3356
|
-
labelItalic = true;
|
|
3357
|
-
} else {
|
|
3358
|
-
labelText = nodeLabel(node);
|
|
3359
|
-
labelItalic = false;
|
|
3360
|
-
}
|
|
3361
|
-
|
|
3362
|
-
// Compute move-button availability for element nodes
|
|
3363
|
-
const isElement = nodeType === "element";
|
|
3364
|
-
const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
|
|
3365
|
-
const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
|
|
3366
|
-
const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
|
|
3367
|
-
const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
|
|
3368
|
-
const siblingCount = parentNode?.children?.length || 0;
|
|
3369
|
-
const canMoveUp = isElement && !isRoot && idx > 0;
|
|
3370
|
-
const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
|
|
3371
|
-
// "in" = move into the previous sibling (become its last child)
|
|
3372
|
-
const prevSibling = canMoveUp && parentNode ? parentNode.children[idx - 1] : null;
|
|
3373
|
-
const canMoveIn =
|
|
3374
|
-
isElement &&
|
|
3375
|
-
!isRoot &&
|
|
3376
|
-
prevSibling &&
|
|
3377
|
-
!VOID_ELEMENTS.has((prevSibling.tagName || "div").toLowerCase());
|
|
3378
|
-
// "out" = move out of parent to grandparent (after parent)
|
|
3379
|
-
const grandparentPath =
|
|
3380
|
-
isElement && parentPath && parentPath.length >= 2
|
|
3381
|
-
? /** @type {any} */ (parentElementPath(parentPath))
|
|
3382
|
-
: null;
|
|
3383
|
-
const canMoveOut = isElement && !isRoot && !!grandparentPath;
|
|
3384
|
-
|
|
3385
|
-
layerRows.push(html`
|
|
3386
|
-
<div
|
|
3387
|
-
class="layer-row${isSelected ? " selected" : ""}"
|
|
3388
|
-
data-path=${key}
|
|
3389
|
-
data-dnd-row=${isElement ? key : nothing}
|
|
3390
|
-
data-dnd-depth=${isElement ? depth : nothing}
|
|
3391
|
-
data-dnd-void=${isElement && isVoidEl ? "" : nothing}
|
|
3392
|
-
@click=${() => update(selectNode(S, path))}
|
|
3393
|
-
@contextmenu=${isElement
|
|
3394
|
-
? (/** @type {any} */ e) =>
|
|
3395
|
-
showContextMenu(e, path, S, { onEditComponent: navigateToComponent })
|
|
3396
|
-
: nothing}
|
|
3397
|
-
>
|
|
3398
|
-
<span class="layer-indent" style="width:${depth * 16}px"></span>
|
|
3399
|
-
<span class="layer-toggle"
|
|
3400
|
-
>${isExpandable
|
|
3401
|
-
? html`
|
|
3402
|
-
${collapsed.has(key)
|
|
3403
|
-
? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
|
|
3404
|
-
: html`<sp-icon-chevron-down></sp-icon-chevron-down>`}
|
|
3405
|
-
`
|
|
3406
|
-
: nothing}</span
|
|
3407
|
-
>
|
|
3408
|
-
<span class=${badgeClass} title=${badgeTitle ?? nothing}>${badgeText}</span>
|
|
3409
|
-
<span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
|
|
3410
|
-
>${labelText}</span
|
|
3411
|
-
>
|
|
3412
|
-
${isElement && !isRoot
|
|
3413
|
-
? html`
|
|
3414
|
-
<span class="layer-actions">
|
|
3415
|
-
${canMoveUp
|
|
3416
|
-
? html`<sp-action-button
|
|
3417
|
-
quiet
|
|
3418
|
-
size="xs"
|
|
3419
|
-
title="Move up"
|
|
3420
|
-
@click=${(/** @type {any} */ e) => {
|
|
3421
|
-
e.stopPropagation();
|
|
3422
|
-
/** @type {HTMLElement} */ (e.currentTarget).blur();
|
|
3423
|
-
update(moveNode(S, path, parentPath, idx - 1));
|
|
3424
|
-
}}
|
|
3425
|
-
>
|
|
3426
|
-
<sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
|
|
3427
|
-
</sp-action-button>`
|
|
3428
|
-
: nothing}
|
|
3429
|
-
${canMoveDown
|
|
3430
|
-
? html`<sp-action-button
|
|
3431
|
-
quiet
|
|
3432
|
-
size="xs"
|
|
3433
|
-
title="Move down"
|
|
3434
|
-
@click=${(/** @type {any} */ e) => {
|
|
3435
|
-
e.stopPropagation();
|
|
3436
|
-
/** @type {HTMLElement} */ (e.currentTarget).blur();
|
|
3437
|
-
update(moveNode(S, path, parentPath, idx + 1));
|
|
3438
|
-
}}
|
|
3439
|
-
>
|
|
3440
|
-
<sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
|
|
3441
|
-
</sp-action-button>`
|
|
3442
|
-
: nothing}
|
|
3443
|
-
${canMoveIn
|
|
3444
|
-
? html`<sp-action-button
|
|
3445
|
-
quiet
|
|
3446
|
-
size="xs"
|
|
3447
|
-
title="Move into previous sibling"
|
|
3448
|
-
@click=${(/** @type {any} */ e) => {
|
|
3449
|
-
e.stopPropagation();
|
|
3450
|
-
/** @type {HTMLElement} */ (e.currentTarget).blur();
|
|
3451
|
-
const prevPath = [...parentPath, idx - 1];
|
|
3452
|
-
const prev = getNodeAtPath(S.document, prevPath);
|
|
3453
|
-
const len = prev?.children?.length || 0;
|
|
3454
|
-
update(moveNode(S, path, prevPath, len));
|
|
3455
|
-
}}
|
|
3456
|
-
>
|
|
3457
|
-
<sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
|
|
3458
|
-
</sp-action-button>`
|
|
3459
|
-
: nothing}
|
|
3460
|
-
${canMoveOut
|
|
3461
|
-
? html`<sp-action-button
|
|
3462
|
-
quiet
|
|
3463
|
-
size="xs"
|
|
3464
|
-
title="Move out of parent"
|
|
3465
|
-
@click=${(/** @type {any} */ e) => {
|
|
3466
|
-
e.stopPropagation();
|
|
3467
|
-
/** @type {HTMLElement} */ (e.currentTarget).blur();
|
|
3468
|
-
const parentIdx = /** @type {number} */ (childIndex(parentPath));
|
|
3469
|
-
update(moveNode(S, path, grandparentPath, parentIdx + 1));
|
|
3470
|
-
}}
|
|
3471
|
-
>
|
|
3472
|
-
<sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
|
|
3473
|
-
</sp-action-button>`
|
|
3474
|
-
: nothing}
|
|
3475
|
-
<sp-action-button
|
|
3476
|
-
quiet
|
|
3477
|
-
size="xs"
|
|
3478
|
-
class="layer-delete"
|
|
3479
|
-
title="Delete"
|
|
3480
|
-
@click=${(/** @type {any} */ e) => {
|
|
3481
|
-
e.stopPropagation();
|
|
3482
|
-
update(removeNode(S, path));
|
|
3483
|
-
}}
|
|
3484
|
-
>
|
|
3485
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
3486
|
-
</sp-action-button>
|
|
3487
|
-
</span>
|
|
3488
|
-
`
|
|
3489
|
-
: nothing}
|
|
3490
|
-
</div>
|
|
3491
|
-
`);
|
|
3492
|
-
|
|
3493
|
-
// Collapse toggle click handler — we add it via event delegation on the layer-toggle span
|
|
3494
|
-
// It's already in the template above as the toggle span, but we need the click handler
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
return html`
|
|
3498
|
-
<div class="layers-container" style="position:relative">
|
|
3499
|
-
<div
|
|
3500
|
-
class="layers-tree"
|
|
3501
|
-
@click=${(/** @type {any} */ e) => {
|
|
3502
|
-
const toggle = e.target.closest(".layer-toggle");
|
|
3503
|
-
if (!toggle) return;
|
|
3504
|
-
e.stopPropagation();
|
|
3505
|
-
const row = toggle.closest(".layer-row");
|
|
3506
|
-
if (!row) return;
|
|
3507
|
-
const key = row.dataset.path;
|
|
3508
|
-
if (!key) return;
|
|
3509
|
-
if (collapsed.has(key)) collapsed.delete(key);
|
|
3510
|
-
else collapsed.add(key);
|
|
3511
|
-
renderLeftPanel();
|
|
3512
|
-
}}
|
|
3513
|
-
>
|
|
3514
|
-
${layerRows}
|
|
3515
|
-
</div>
|
|
3516
|
-
</div>
|
|
3517
|
-
`;
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
/** Register DnD on layer rows after litRender — called from renderLeftPanel */
|
|
3200
|
+
/** Register DnD on layer rows after litRender — called from left-panel.js */
|
|
3521
3201
|
function registerLayersDnD() {
|
|
3522
3202
|
requestAnimationFrame(() => {
|
|
3523
3203
|
const container = /** @type {any} */ (leftPanel)?.querySelector(".layers-container");
|
|
@@ -3730,95 +3410,6 @@ function selectStylebookTag(tag, media) {
|
|
|
3730
3410
|
});
|
|
3731
3411
|
}
|
|
3732
3412
|
|
|
3733
|
-
function renderStylebookLayersTemplate() {
|
|
3734
|
-
const rootStyle = S.document?.style || {};
|
|
3735
|
-
const selectedTag = S.ui.stylebookSelection;
|
|
3736
|
-
|
|
3737
|
-
if (S.ui.stylebookTab === "elements") {
|
|
3738
|
-
/**
|
|
3739
|
-
* Render a stylebook entry row with recursive children.
|
|
3740
|
-
*
|
|
3741
|
-
* @param {any} entry
|
|
3742
|
-
* @param {number} depth
|
|
3743
|
-
* @returns {any}
|
|
3744
|
-
*/
|
|
3745
|
-
const renderEntryRow = (entry, depth = 0) => {
|
|
3746
|
-
const tag = entry.tag;
|
|
3747
|
-
// Deduplicate children by tag (e.g. multiple <li> → show one "li" row)
|
|
3748
|
-
const uniqueChildren = entry.children
|
|
3749
|
-
? [...new Map(entry.children.map((/** @type {any} */ c) => [c.tag, c])).values()]
|
|
3750
|
-
: [];
|
|
3751
|
-
return html`
|
|
3752
|
-
<div
|
|
3753
|
-
class="layer-row${tag === selectedTag ? " selected" : ""}"
|
|
3754
|
-
style="padding-left:${8 + depth * 16}px"
|
|
3755
|
-
@click=${(/** @type {any} */ e) => {
|
|
3756
|
-
e.stopPropagation();
|
|
3757
|
-
selectStylebookTag(tag);
|
|
3758
|
-
}}
|
|
3759
|
-
>
|
|
3760
|
-
<span class="layer-tag">${tag}</span>
|
|
3761
|
-
<span
|
|
3762
|
-
class="layer-label"
|
|
3763
|
-
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1"
|
|
3764
|
-
>${entry.text || `<${tag}>`}</span
|
|
3765
|
-
>
|
|
3766
|
-
${hasTagStyle(rootStyle, tag)
|
|
3767
|
-
? html`<span
|
|
3768
|
-
style="width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0"
|
|
3769
|
-
></span>`
|
|
3770
|
-
: nothing}
|
|
3771
|
-
</div>
|
|
3772
|
-
${uniqueChildren.map((/** @type {any} */ child) => renderEntryRow(child, depth + 1))}
|
|
3773
|
-
`;
|
|
3774
|
-
};
|
|
3775
|
-
|
|
3776
|
-
/** @type {any[]} */
|
|
3777
|
-
const elementRows = [];
|
|
3778
|
-
for (const section of stylebookMeta.$sections) {
|
|
3779
|
-
for (const entry of /** @type {any[]} */ (section.elements)) {
|
|
3780
|
-
elementRows.push(renderEntryRow(entry, 0));
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
// Custom components
|
|
3784
|
-
const compRows = componentRegistry.map(
|
|
3785
|
-
/** @param {any} comp */ (comp) => html`
|
|
3786
|
-
<div
|
|
3787
|
-
class="layer-row${comp.tagName === selectedTag ? " selected" : ""}"
|
|
3788
|
-
@click=${() => selectStylebookTag(comp.tagName)}
|
|
3789
|
-
>
|
|
3790
|
-
<span class="layer-tag component-tag" style="background:var(--accent)">⬡</span>
|
|
3791
|
-
<span class="layer-label">${comp.tagName}</span>
|
|
3792
|
-
</div>
|
|
3793
|
-
`,
|
|
3794
|
-
);
|
|
3795
|
-
return html`${elementRows}${compRows}`;
|
|
3796
|
-
} else {
|
|
3797
|
-
// Variables tab
|
|
3798
|
-
const style = rootStyle;
|
|
3799
|
-
const vars = Object.entries(style).filter(([k]) => k.startsWith("--"));
|
|
3800
|
-
if (vars.length === 0) {
|
|
3801
|
-
return html`<div style="padding:16px;text-align:center;color:var(--fg-dim);font-size:12px">
|
|
3802
|
-
No variables defined
|
|
3803
|
-
</div>`;
|
|
3804
|
-
}
|
|
3805
|
-
return html`${vars.map(
|
|
3806
|
-
([k, v]) => html`
|
|
3807
|
-
<div class="layer-row">
|
|
3808
|
-
<span class="layer-tag" style="font-size:10px;font-family:'SF Mono','Fira Code',monospace"
|
|
3809
|
-
>var</span
|
|
3810
|
-
>
|
|
3811
|
-
<span class="layer-label">${k}</span>
|
|
3812
|
-
<span
|
|
3813
|
-
style="font-size:11px;color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px"
|
|
3814
|
-
>${String(v)}</span
|
|
3815
|
-
>
|
|
3816
|
-
</div>
|
|
3817
|
-
`,
|
|
3818
|
-
)}`;
|
|
3819
|
-
}
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
3413
|
/**
|
|
3823
3414
|
* Apply a DnD instruction to the state
|
|
3824
3415
|
*
|
|
@@ -3977,142 +3568,6 @@ function defaultDef(tag) {
|
|
|
3977
3568
|
|
|
3978
3569
|
const unsafeTags = new Set(["script", "style", "link", "iframe", "object", "embed"]);
|
|
3979
3570
|
|
|
3980
|
-
function renderElementsTemplate() {
|
|
3981
|
-
const categories = Object.entries(webdata.elements).map(
|
|
3982
|
-
(/** @type {any} */ [category, elements]) => {
|
|
3983
|
-
const filtered = view.elementsFilter
|
|
3984
|
-
? elements.filter((/** @type {any} */ e) => e.tag.includes(view.elementsFilter))
|
|
3985
|
-
: elements;
|
|
3986
|
-
if (filtered.length === 0) return nothing;
|
|
3987
|
-
|
|
3988
|
-
return html`
|
|
3989
|
-
<sp-accordion-item
|
|
3990
|
-
label=${category}
|
|
3991
|
-
?open=${!view.elementsCollapsed.has(category)}
|
|
3992
|
-
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
3993
|
-
if (e.target.open) view.elementsCollapsed.delete(category);
|
|
3994
|
-
else view.elementsCollapsed.add(category);
|
|
3995
|
-
}}
|
|
3996
|
-
>
|
|
3997
|
-
${filtered.map((/** @type {any} */ { tag }) => {
|
|
3998
|
-
const def = defaultDef(tag);
|
|
3999
|
-
return html`
|
|
4000
|
-
<div
|
|
4001
|
-
class="element-card"
|
|
4002
|
-
data-block-tag=${tag}
|
|
4003
|
-
@click=${() => {
|
|
4004
|
-
const parentPath = S.selection || [];
|
|
4005
|
-
const parent = getNodeAtPath(S.document, parentPath);
|
|
4006
|
-
const idx = parent?.children ? parent.children.length : 0;
|
|
4007
|
-
update(insertNode(S, parentPath, idx, structuredClone(def)));
|
|
4008
|
-
}}
|
|
4009
|
-
>
|
|
4010
|
-
<div class="element-card-preview"></div>
|
|
4011
|
-
<div class="element-card-label"><${tag}></div>
|
|
4012
|
-
</div>
|
|
4013
|
-
`;
|
|
4014
|
-
})}
|
|
4015
|
-
</sp-accordion-item>
|
|
4016
|
-
`;
|
|
4017
|
-
},
|
|
4018
|
-
);
|
|
4019
|
-
|
|
4020
|
-
// Components from the component registry — only show enabled (imported) npm components
|
|
4021
|
-
const effectiveEls = getEffectiveElements(S.document?.$elements);
|
|
4022
|
-
/** @type {Set<string>} */
|
|
4023
|
-
const enabledTags = new Set();
|
|
4024
|
-
for (const entry of effectiveEls) {
|
|
4025
|
-
if (typeof entry !== "string") continue;
|
|
4026
|
-
// Cherry-picked subpath: match by package + modulePath
|
|
4027
|
-
const comp = componentRegistry.find(
|
|
4028
|
-
(/** @type {any} */ c) =>
|
|
4029
|
-
c.source === "npm" && c.modulePath && entry === `${c.package}/${c.modulePath}`,
|
|
4030
|
-
);
|
|
4031
|
-
if (comp) {
|
|
4032
|
-
enabledTags.add(comp.tagName);
|
|
4033
|
-
} else {
|
|
4034
|
-
// Legacy full-package import: enable all components from that package
|
|
4035
|
-
for (const c of componentRegistry) {
|
|
4036
|
-
if (c.source === "npm" && c.package === entry) enabledTags.add(c.tagName);
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
const compsFiltered =
|
|
4041
|
-
componentRegistry.length > 0
|
|
4042
|
-
? componentRegistry
|
|
4043
|
-
.filter((/** @type {any} */ c) => c.source !== "npm" || enabledTags.has(c.tagName))
|
|
4044
|
-
.filter(
|
|
4045
|
-
(/** @type {any} */ c) =>
|
|
4046
|
-
!view.elementsFilter || c.tagName.toLowerCase().includes(view.elementsFilter),
|
|
4047
|
-
)
|
|
4048
|
-
: [];
|
|
4049
|
-
|
|
4050
|
-
const componentsAccordion =
|
|
4051
|
-
compsFiltered.length > 0
|
|
4052
|
-
? html`
|
|
4053
|
-
<sp-accordion-item
|
|
4054
|
-
label="Components"
|
|
4055
|
-
?open=${!view.elementsCollapsed.has("Components")}
|
|
4056
|
-
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
4057
|
-
if (e.target.open) view.elementsCollapsed.delete("Components");
|
|
4058
|
-
else view.elementsCollapsed.add("Components");
|
|
4059
|
-
}}
|
|
4060
|
-
>
|
|
4061
|
-
<div class="components-section">
|
|
4062
|
-
${compsFiltered.map(
|
|
4063
|
-
(/** @type {any} */ comp) => html`
|
|
4064
|
-
<div
|
|
4065
|
-
class="element-card"
|
|
4066
|
-
data-component-tag=${comp.tagName}
|
|
4067
|
-
title=${comp.source === "npm"
|
|
4068
|
-
? `${comp.package}: <${comp.tagName}>`
|
|
4069
|
-
: comp.path}
|
|
4070
|
-
@click=${() => {
|
|
4071
|
-
const parentPath = S.selection || [];
|
|
4072
|
-
const parent = getNodeAtPath(S.document, parentPath);
|
|
4073
|
-
const idx = parent?.children ? parent.children.length : 0;
|
|
4074
|
-
const instanceDef = {
|
|
4075
|
-
tagName: comp.tagName,
|
|
4076
|
-
$props: Object.fromEntries(
|
|
4077
|
-
(comp.props || []).map((/** @type {any} */ p) => [
|
|
4078
|
-
p.name,
|
|
4079
|
-
p.default !== undefined ? p.default : "",
|
|
4080
|
-
]),
|
|
4081
|
-
),
|
|
4082
|
-
};
|
|
4083
|
-
update(insertNode(S, parentPath, idx, structuredClone(instanceDef)));
|
|
4084
|
-
}}
|
|
4085
|
-
>
|
|
4086
|
-
<div class="element-card-preview">
|
|
4087
|
-
<span style="color:var(--fg-dim);font-size:11px;font-style:italic"
|
|
4088
|
-
><${comp.tagName}></span
|
|
4089
|
-
>
|
|
4090
|
-
</div>
|
|
4091
|
-
<div class="element-card-label">${comp.tagName}</div>
|
|
4092
|
-
</div>
|
|
4093
|
-
`,
|
|
4094
|
-
)}
|
|
4095
|
-
</div>
|
|
4096
|
-
</sp-accordion-item>
|
|
4097
|
-
`
|
|
4098
|
-
: nothing;
|
|
4099
|
-
|
|
4100
|
-
return html`
|
|
4101
|
-
<sp-search
|
|
4102
|
-
size="s"
|
|
4103
|
-
placeholder="Filter elements…"
|
|
4104
|
-
value=${view.elementsFilter}
|
|
4105
|
-
@input=${(/** @type {any} */ e) => {
|
|
4106
|
-
view.elementsFilter = e.target.value.toLowerCase();
|
|
4107
|
-
renderLeftPanel();
|
|
4108
|
-
}}
|
|
4109
|
-
></sp-search>
|
|
4110
|
-
<sp-accordion class="elements-list" allow-multiple
|
|
4111
|
-
>${componentsAccordion}${categories}</sp-accordion
|
|
4112
|
-
>
|
|
4113
|
-
`;
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
3571
|
function registerElementsDnD() {
|
|
4117
3572
|
requestAnimationFrame(() => {
|
|
4118
3573
|
const container = /** @type {any} */ (leftPanel)?.querySelector(".panel-body");
|
|
@@ -6284,915 +5739,8 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
|
|
|
6284
5739
|
|
|
6285
5740
|
// inferInputType — imported from studio-utils.js
|
|
6286
5741
|
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
if (cond.values.length === 0) return val !== "" && val !== "initial";
|
|
6290
|
-
return cond.values.includes(val);
|
|
6291
|
-
}
|
|
6292
|
-
|
|
6293
|
-
function allConditionsPass(/** @type {any} */ entry, /** @type {any} */ styles) {
|
|
6294
|
-
return (entry.$show ?? []).every((/** @type {any} */ c) => conditionPasses(c, styles));
|
|
6295
|
-
}
|
|
6296
|
-
|
|
6297
|
-
function autoOpenSections(/** @type {any} */ node, /** @type {any} */ currentSections) {
|
|
6298
|
-
const style = node.style || {};
|
|
6299
|
-
const result = { ...currentSections };
|
|
6300
|
-
for (const prop of Object.keys(style)) {
|
|
6301
|
-
if (typeof style[prop] === "object") continue;
|
|
6302
|
-
const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[prop];
|
|
6303
|
-
const section = entry?.$section ?? "other";
|
|
6304
|
-
if (!result[section]) result[section] = true;
|
|
6305
|
-
}
|
|
6306
|
-
return result;
|
|
6307
|
-
}
|
|
6308
|
-
|
|
6309
|
-
/** Get longhands for a shorthand property from css-meta */
|
|
6310
|
-
function getLonghands(/** @type {any} */ shorthandProp) {
|
|
6311
|
-
// Check for explicit $longhands array first (used by border-side shorthands)
|
|
6312
|
-
const entry = /** @type {Record<string, any>} */ (cssMeta.$defs)[shorthandProp];
|
|
6313
|
-
if (entry?.$longhands) {
|
|
6314
|
-
return entry.$longhands
|
|
6315
|
-
.map((/** @type {string} */ name) => ({
|
|
6316
|
-
name,
|
|
6317
|
-
entry: /** @type {Record<string, any>} */ (cssMeta.$defs)[name] || { $order: 0 },
|
|
6318
|
-
}))
|
|
6319
|
-
.sort((/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order);
|
|
6320
|
-
}
|
|
6321
|
-
// Fallback: reverse-lookup by $shorthand reference
|
|
6322
|
-
const result = [];
|
|
6323
|
-
for (const [name, e] of /** @type {[string, any][]} */ (Object.entries(cssMeta.$defs))) {
|
|
6324
|
-
if (e.$shorthand === shorthandProp) result.push({ name, entry: e });
|
|
6325
|
-
}
|
|
6326
|
-
result.sort((a, b) => a.entry.$order - b.entry.$order);
|
|
6327
|
-
return result;
|
|
6328
|
-
}
|
|
6329
|
-
|
|
6330
|
-
/**
|
|
6331
|
-
* Expand a CSS shorthand value (margin, padding, borderWidth, borderRadius) into individual
|
|
6332
|
-
* longhand values following the standard 1–4 value TRBL pattern. Returns an array matching the
|
|
6333
|
-
* longhand count (always 4 for box properties).
|
|
6334
|
-
*/
|
|
6335
|
-
function expandShorthand(/** @type {string} */ shortVal, /** @type {number} */ count) {
|
|
6336
|
-
if (!shortVal) return Array(count).fill("");
|
|
6337
|
-
const parts = shortVal.trim().split(/\s+/);
|
|
6338
|
-
if (count !== 4 || parts.length === 0) return Array(count).fill("");
|
|
6339
|
-
if (parts.length === 1) return [parts[0], parts[0], parts[0], parts[0]];
|
|
6340
|
-
if (parts.length === 2) return [parts[0], parts[1], parts[0], parts[1]];
|
|
6341
|
-
if (parts.length === 3) return [parts[0], parts[1], parts[2], parts[1]];
|
|
6342
|
-
return [parts[0], parts[1], parts[2], parts[3]];
|
|
6343
|
-
}
|
|
6344
|
-
|
|
6345
|
-
/**
|
|
6346
|
-
* Compress 4 TRBL values back into the shortest valid CSS shorthand string. e.g.
|
|
6347
|
-
* ["0","auto","3rem","auto"] → "0 auto 3rem"
|
|
6348
|
-
*/
|
|
6349
|
-
function compressShorthand(/** @type {string[]} */ vals) {
|
|
6350
|
-
const [t, r, b, l] = vals;
|
|
6351
|
-
if (t === r && r === b && b === l) return t;
|
|
6352
|
-
if (t === b && r === l) return `${t} ${r}`;
|
|
6353
|
-
if (r === l) return `${t} ${r} ${b}`;
|
|
6354
|
-
return `${t} ${r} ${b} ${l}`;
|
|
6355
|
-
}
|
|
6356
|
-
|
|
6357
|
-
// ─── Border-side shorthand parsing ────────────────────────────────────────────
|
|
6358
|
-
// CSS border-side shorthand: <width> || <style> || <color> (any order, all optional)
|
|
6359
|
-
|
|
6360
|
-
const BORDER_STYLES = new Set([
|
|
6361
|
-
"none",
|
|
6362
|
-
"solid",
|
|
6363
|
-
"dashed",
|
|
6364
|
-
"dotted",
|
|
6365
|
-
"double",
|
|
6366
|
-
"groove",
|
|
6367
|
-
"ridge",
|
|
6368
|
-
"inset",
|
|
6369
|
-
"outset",
|
|
6370
|
-
"hidden",
|
|
6371
|
-
]);
|
|
6372
|
-
|
|
6373
|
-
/**
|
|
6374
|
-
* Parse a border-side shorthand value into [width, style, color].
|
|
6375
|
-
*
|
|
6376
|
-
* @param {string} value — e.g. "1px solid var(--color-border)"
|
|
6377
|
-
* @returns {string[]} — [width, style, color]
|
|
6378
|
-
*/
|
|
6379
|
-
function expandBorderSide(value) {
|
|
6380
|
-
if (!value) return ["", "", ""];
|
|
6381
|
-
// Tokenize respecting parenthesized values like var(...) and rgb(...)
|
|
6382
|
-
const tokens = [];
|
|
6383
|
-
let current = "";
|
|
6384
|
-
let depth = 0;
|
|
6385
|
-
for (const ch of value.trim()) {
|
|
6386
|
-
if (ch === "(") depth++;
|
|
6387
|
-
if (ch === ")") depth--;
|
|
6388
|
-
if (ch === " " && depth === 0) {
|
|
6389
|
-
if (current) tokens.push(current);
|
|
6390
|
-
current = "";
|
|
6391
|
-
} else {
|
|
6392
|
-
current += ch;
|
|
6393
|
-
}
|
|
6394
|
-
}
|
|
6395
|
-
if (current) tokens.push(current);
|
|
6396
|
-
|
|
6397
|
-
let width = "";
|
|
6398
|
-
let style = "";
|
|
6399
|
-
let color = "";
|
|
6400
|
-
|
|
6401
|
-
for (const tok of tokens) {
|
|
6402
|
-
if (!style && BORDER_STYLES.has(tok)) {
|
|
6403
|
-
style = tok;
|
|
6404
|
-
} else if (!width && /^[\d.]/.test(tok)) {
|
|
6405
|
-
width = tok;
|
|
6406
|
-
} else {
|
|
6407
|
-
// Remaining token(s) are color — join in case color was split (shouldn't be with paren-aware tokenizer)
|
|
6408
|
-
color = color ? `${color} ${tok}` : tok;
|
|
6409
|
-
}
|
|
6410
|
-
}
|
|
6411
|
-
|
|
6412
|
-
return [width, style, color];
|
|
6413
|
-
}
|
|
6414
|
-
|
|
6415
|
-
/**
|
|
6416
|
-
* Recompose border-side longhand values into a shorthand string.
|
|
6417
|
-
*
|
|
6418
|
-
* @param {string[]} vals — [width, style, color]
|
|
6419
|
-
* @returns {string}
|
|
6420
|
-
*/
|
|
6421
|
-
function compressBorderSide(/** @type {string[]} */ vals) {
|
|
6422
|
-
return vals.filter((v) => v && v.trim()).join(" ");
|
|
6423
|
-
}
|
|
6424
|
-
|
|
6425
|
-
/** Extract --font-* CSS custom properties from the document root style. */
|
|
6426
|
-
function getFontVars() {
|
|
6427
|
-
const style = S.document?.style;
|
|
6428
|
-
if (!style) return [];
|
|
6429
|
-
const vars = [];
|
|
6430
|
-
for (const [k, v] of Object.entries(style)) {
|
|
6431
|
-
if (k.startsWith("--font") && (typeof v === "string" || typeof v === "number")) {
|
|
6432
|
-
vars.push({ name: k, value: String(v) });
|
|
6433
|
-
}
|
|
6434
|
-
}
|
|
6435
|
-
return vars;
|
|
6436
|
-
}
|
|
6437
|
-
|
|
6438
|
-
/** Typography CSS properties that should preview their values in-menu */
|
|
6439
|
-
const TYPO_PREVIEW_PROPS = new Set(["fontStyle", "fontVariant", "textTransform", "textDecoration"]);
|
|
6440
|
-
|
|
6441
|
-
// camelToKebab — imported from studio-utils.js
|
|
6442
|
-
|
|
6443
|
-
/** Resolve the current font family for typography preview (handles var() references) */
|
|
6444
|
-
function currentFontFamily() {
|
|
6445
|
-
const node = S.selection ? getNodeAtPath(S.document, S.selection) : null;
|
|
6446
|
-
const raw = node?.style?.fontFamily;
|
|
6447
|
-
if (!raw) return "";
|
|
6448
|
-
const m = typeof raw === "string" && raw.match(/^var\((--[^)]+)\)$/);
|
|
6449
|
-
if (m) return S.document?.style?.[m[1]] || "";
|
|
6450
|
-
return raw;
|
|
6451
|
-
}
|
|
6452
|
-
|
|
6453
|
-
/**
|
|
6454
|
-
* Dual-mode keyword input — shared by select (enum) and combobox (examples) widgets.
|
|
6455
|
-
*
|
|
6456
|
-
* If the current value is one of the predefined options → renders as sp-picker with Title Case
|
|
6457
|
-
* labels (and typography preview when applicable). Selecting "—" clears the value, which flips to
|
|
6458
|
-
* combobox mode.
|
|
6459
|
-
*
|
|
6460
|
-
* If the value is empty or a custom string → renders as sp-combobox with predefined options in its
|
|
6461
|
-
* dropdown. Selecting one flips to picker mode.
|
|
6462
|
-
*
|
|
6463
|
-
* Note: sp-combobox recreates items in shadow DOM as plain text, so typography preview props use a
|
|
6464
|
-
* manual sp-textfield + sp-overlay + sp-menu instead.
|
|
6465
|
-
*
|
|
6466
|
-
* @param {any} options @param {any} prop @param {any} value @param {any} onChange
|
|
6467
|
-
*/
|
|
6468
|
-
function renderKeywordInput(options, prop, value, onChange) {
|
|
6469
|
-
const isTypoPreview = TYPO_PREVIEW_PROPS.has(prop) || prop === "fontWeight";
|
|
6470
|
-
const font = isTypoPreview ? currentFontFamily() : "";
|
|
6471
|
-
const cssProp = isTypoPreview ? camelToKebab(prop) : "";
|
|
6472
|
-
|
|
6473
|
-
const comboOptions = options.map((/** @type {any} */ v) => {
|
|
6474
|
-
const label = v.includes("-")
|
|
6475
|
-
? kebabToLabel(v)
|
|
6476
|
-
: v.replace(/^./, (/** @type {any} */ c) => c.toUpperCase());
|
|
6477
|
-
const style = isTypoPreview ? `${cssProp}: ${v};${font ? ` font-family: ${font}` : ""}` : "";
|
|
6478
|
-
return { value: v, label, style };
|
|
6479
|
-
});
|
|
6480
|
-
|
|
6481
|
-
return html`<jx-value-selector
|
|
6482
|
-
size="s"
|
|
6483
|
-
.value=${value || ""}
|
|
6484
|
-
placeholder=${cssInitialMap.get(prop) || ""}
|
|
6485
|
-
.options=${comboOptions}
|
|
6486
|
-
@change=${(/** @type {any} */ e) => onChange(e.target.value)}
|
|
6487
|
-
@input=${debouncedStyleCommit(`kw:${prop}`, 400, (/** @type {any} */ e) =>
|
|
6488
|
-
onChange(e.target.value),
|
|
6489
|
-
)}
|
|
6490
|
-
></jx-value-selector>`;
|
|
6491
|
-
}
|
|
6492
|
-
|
|
6493
|
-
function renderSelectInput(
|
|
6494
|
-
/** @type {any} */ entry,
|
|
6495
|
-
/** @type {any} */ prop,
|
|
6496
|
-
/** @type {any} */ value,
|
|
6497
|
-
/** @type {any} */ onChange,
|
|
6498
|
-
) {
|
|
6499
|
-
return renderKeywordInput(entry.enum || [], prop, value, onChange);
|
|
6500
|
-
}
|
|
6501
|
-
|
|
6502
|
-
function handleFontPresetSelection(/** @type {any} */ preset, /** @type {any} */ onChange) {
|
|
6503
|
-
const varName = friendlyNameToVar(preset.title, "--font-");
|
|
6504
|
-
if (!S.document?.style?.[varName]) {
|
|
6505
|
-
S = updateStyle(S, [], varName, preset.value);
|
|
6506
|
-
}
|
|
6507
|
-
onChange(`var(${varName})`);
|
|
6508
|
-
}
|
|
6509
|
-
|
|
6510
|
-
function handleFontSelection(
|
|
6511
|
-
/** @type {any} */ val,
|
|
6512
|
-
/** @type {any} */ presets,
|
|
6513
|
-
/** @type {any} */ onChange,
|
|
6514
|
-
) {
|
|
6515
|
-
if (!val) return;
|
|
6516
|
-
// sp-picker returns the option's value attribute (prefixed or var name)
|
|
6517
|
-
if (val.startsWith("__preset__:")) {
|
|
6518
|
-
const title = val.slice("__preset__:".length);
|
|
6519
|
-
const preset = presets.find((/** @type {any} */ p) => p.title === title);
|
|
6520
|
-
if (preset) handleFontPresetSelection(preset, onChange);
|
|
6521
|
-
return;
|
|
6522
|
-
}
|
|
6523
|
-
if (val.startsWith("--")) {
|
|
6524
|
-
onChange(`var(${val})`);
|
|
6525
|
-
return;
|
|
6526
|
-
}
|
|
6527
|
-
// sp-combobox returns display text — match against preset titles and font var
|
|
6528
|
-
// display names before falling through to plain text
|
|
6529
|
-
const preset = presets.find((/** @type {any} */ p) => p.title === val);
|
|
6530
|
-
if (preset) {
|
|
6531
|
-
handleFontPresetSelection(preset, onChange);
|
|
6532
|
-
return;
|
|
6533
|
-
}
|
|
6534
|
-
const fontVars = getFontVars();
|
|
6535
|
-
const matchedVar = fontVars.find(
|
|
6536
|
-
(/** @type {any} */ fv) => varDisplayName(fv.name, "--font-") === val,
|
|
6537
|
-
);
|
|
6538
|
-
if (matchedVar) {
|
|
6539
|
-
onChange(`var(${matchedVar.name})`);
|
|
6540
|
-
return;
|
|
6541
|
-
}
|
|
6542
|
-
// Plain font family string (e.g. "serif", "Arial, sans-serif")
|
|
6543
|
-
onChange(val);
|
|
6544
|
-
}
|
|
6545
|
-
|
|
6546
|
-
/**
|
|
6547
|
-
* Build font options array for jx-value-selector. Local font vars first, divider, then unadded
|
|
6548
|
-
* presets.
|
|
6549
|
-
*
|
|
6550
|
-
* @param {any[]} fontVars @param {any[]} presets
|
|
6551
|
-
* @returns {{ value: string; label: string; style: string }[] | { divider: true }[]}
|
|
6552
|
-
*/
|
|
6553
|
-
function buildFontOptions(fontVars, presets) {
|
|
6554
|
-
/** @type {any[]} */
|
|
6555
|
-
const opts = fontVars.map((/** @type {any} */ fv) => ({
|
|
6556
|
-
value: fv.name,
|
|
6557
|
-
label: varDisplayName(fv.name, "--font-"),
|
|
6558
|
-
style: `font-family: ${fv.value}`,
|
|
6559
|
-
}));
|
|
6560
|
-
const unadded = presets.filter(
|
|
6561
|
-
(/** @type {any} */ p) =>
|
|
6562
|
-
!fontVars.some((/** @type {any} */ fv) => fv.name === friendlyNameToVar(p.title, "--font-")),
|
|
6563
|
-
);
|
|
6564
|
-
if (unadded.length > 0 && opts.length > 0) opts.push({ divider: true });
|
|
6565
|
-
for (const p of unadded) {
|
|
6566
|
-
opts.push({
|
|
6567
|
-
value: "__preset__:" + p.title,
|
|
6568
|
-
label: p.title,
|
|
6569
|
-
style: `font-family: ${p.value}`,
|
|
6570
|
-
});
|
|
6571
|
-
}
|
|
6572
|
-
return opts;
|
|
6573
|
-
}
|
|
6574
|
-
|
|
6575
|
-
function renderComboboxInput(
|
|
6576
|
-
/** @type {any} */ entry,
|
|
6577
|
-
/** @type {any} */ prop,
|
|
6578
|
-
/** @type {any} */ value,
|
|
6579
|
-
/** @type {any} */ onChange,
|
|
6580
|
-
) {
|
|
6581
|
-
const fontVars = prop === "fontFamily" ? getFontVars() : [];
|
|
6582
|
-
const presets = entry.presets || [];
|
|
6583
|
-
const examples = entry.examples || [];
|
|
6584
|
-
|
|
6585
|
-
// fontFamily: single jx-value-selector with font options
|
|
6586
|
-
if (prop === "fontFamily") {
|
|
6587
|
-
// Strip var() wrapper so the component can match the option value
|
|
6588
|
-
const varMatch = typeof value === "string" && value.match(/^var\((--[^)]+)\)$/);
|
|
6589
|
-
const comboValue = varMatch ? varMatch[1] : value || "";
|
|
6590
|
-
const fontOptions = buildFontOptions(fontVars, presets);
|
|
6591
|
-
return html`<jx-value-selector
|
|
6592
|
-
size="s"
|
|
6593
|
-
.value=${comboValue}
|
|
6594
|
-
placeholder=${cssInitialMap.get("fontFamily") || ""}
|
|
6595
|
-
.options=${fontOptions}
|
|
6596
|
-
@change=${(/** @type {any} */ e) => handleFontSelection(e.target.value, presets, onChange)}
|
|
6597
|
-
@input=${debouncedStyleCommit("combo:fontFamily", 400, (/** @type {any} */ e) =>
|
|
6598
|
-
onChange(e.target.value),
|
|
6599
|
-
)}
|
|
6600
|
-
></jx-value-selector>`;
|
|
6601
|
-
}
|
|
6602
|
-
|
|
6603
|
-
// All other comboboxes: use the shared keyword dual-mode input
|
|
6604
|
-
if (examples.length > 0) {
|
|
6605
|
-
return renderKeywordInput(examples, prop, value, onChange);
|
|
6606
|
-
}
|
|
6607
|
-
|
|
6608
|
-
// Fallback: plain textfield (no predefined options)
|
|
6609
|
-
return html`
|
|
6610
|
-
<sp-textfield
|
|
6611
|
-
size="s"
|
|
6612
|
-
placeholder=${cssInitialMap.get(prop) || ""}
|
|
6613
|
-
.value=${live(value || "")}
|
|
6614
|
-
@input=${debouncedStyleCommit(`combo:${prop}`, 400, (/** @type {any} */ e) =>
|
|
6615
|
-
onChange(e.target.value),
|
|
6616
|
-
)}
|
|
6617
|
-
></sp-textfield>
|
|
6618
|
-
`;
|
|
6619
|
-
}
|
|
6620
|
-
|
|
6621
|
-
// renderNumberInput, renderTextInput — imported from ui/widgets.js
|
|
6622
|
-
|
|
6623
|
-
// camelToLabel, kebabToLabel, propLabel, attrLabel — imported from studio-utils.js
|
|
6624
|
-
|
|
6625
|
-
function widgetForType(
|
|
6626
|
-
/** @type {any} */ type,
|
|
6627
|
-
/** @type {any} */ entry,
|
|
6628
|
-
/** @type {any} */ prop,
|
|
6629
|
-
/** @type {any} */ value,
|
|
6630
|
-
/** @type {any} */ onCommit,
|
|
6631
|
-
/** @type {any} */ opts = {},
|
|
6632
|
-
) {
|
|
6633
|
-
return _widgetForType(type, entry, prop, value, onCommit, {
|
|
6634
|
-
placeholder: opts.placeholder || cssInitialMap.get(prop) || "",
|
|
6635
|
-
renderSelect: renderSelectInput,
|
|
6636
|
-
renderCombobox: renderComboboxInput,
|
|
6637
|
-
});
|
|
6638
|
-
}
|
|
6639
|
-
|
|
6640
|
-
function renderStyleRow(
|
|
6641
|
-
/** @type {any} */ entry,
|
|
6642
|
-
/** @type {any} */ prop,
|
|
6643
|
-
/** @type {any} */ value,
|
|
6644
|
-
/** @type {any} */ onCommit,
|
|
6645
|
-
/** @type {any} */ onDelete,
|
|
6646
|
-
/** @type {any} */ isWarning,
|
|
6647
|
-
/** @type {any} */ gridMode,
|
|
6648
|
-
/** @type {any} */ inheritedValue,
|
|
6649
|
-
) {
|
|
6650
|
-
const type = inferInputType(entry);
|
|
6651
|
-
const hasVal = value !== undefined && value !== "";
|
|
6652
|
-
const placeholder = !hasVal && inheritedValue ? String(inheritedValue) : "";
|
|
6653
|
-
return renderFieldRow({
|
|
6654
|
-
prop,
|
|
6655
|
-
label: propLabel(entry, prop),
|
|
6656
|
-
hasValue: hasVal,
|
|
6657
|
-
onClear: onDelete,
|
|
6658
|
-
widget: widgetForType(type, entry, prop, value, onCommit, { placeholder }),
|
|
6659
|
-
span: gridMode && entry.$span === 2 ? 2 : undefined,
|
|
6660
|
-
warning: isWarning,
|
|
6661
|
-
});
|
|
6662
|
-
}
|
|
6663
|
-
|
|
6664
|
-
function renderShorthandRow(
|
|
6665
|
-
/** @type {any} */ shortProp,
|
|
6666
|
-
/** @type {any} */ entry,
|
|
6667
|
-
/** @type {any} */ style,
|
|
6668
|
-
/** @type {any} */ commitFn,
|
|
6669
|
-
/** @type {any} */ _deleteFn,
|
|
6670
|
-
/** @type {Record<string, any>} */ inherited = {},
|
|
6671
|
-
) {
|
|
6672
|
-
const longhands = getLonghands(shortProp);
|
|
6673
|
-
const shortVal = style[shortProp];
|
|
6674
|
-
const hasLonghands = longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6675
|
-
const isExpanded = S.ui.styleShorthands[shortProp] ?? hasLonghands;
|
|
6676
|
-
const hasAnyVal =
|
|
6677
|
-
shortVal !== undefined || longhands.some((/** @type {any} */ l) => style[l.name] !== undefined);
|
|
6678
|
-
|
|
6679
|
-
return html`
|
|
6680
|
-
<div class="style-row" data-prop=${shortProp}>
|
|
6681
|
-
<div class="style-row-label">
|
|
6682
|
-
${hasAnyVal
|
|
6683
|
-
? html`<span
|
|
6684
|
-
class="set-dot"
|
|
6685
|
-
title="Clear ${shortProp}"
|
|
6686
|
-
@click=${(/** @type {any} */ e) => {
|
|
6687
|
-
e.stopPropagation();
|
|
6688
|
-
let s = S;
|
|
6689
|
-
if (shortVal !== undefined) s = commitFn(s, shortProp, undefined);
|
|
6690
|
-
for (const l of longhands) {
|
|
6691
|
-
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6692
|
-
}
|
|
6693
|
-
update(s);
|
|
6694
|
-
}}
|
|
6695
|
-
></span>`
|
|
6696
|
-
: nothing}
|
|
6697
|
-
<sp-field-label size="s" title=${shortProp}>${propLabel(entry, shortProp)}</sp-field-label>
|
|
6698
|
-
</div>
|
|
6699
|
-
<div class="style-shorthand-header">
|
|
6700
|
-
<sp-textfield
|
|
6701
|
-
size="s"
|
|
6702
|
-
.value=${live(shortVal || "")}
|
|
6703
|
-
placeholder=${!shortVal && hasLonghands
|
|
6704
|
-
? longhands.map((/** @type {any} */ l) => style[l.name] || "0").join(" ")
|
|
6705
|
-
: !shortVal && inherited[shortProp]
|
|
6706
|
-
? inherited[shortProp]
|
|
6707
|
-
: !shortVal && longhands.some((/** @type {any} */ l) => inherited[l.name])
|
|
6708
|
-
? longhands.map((/** @type {any} */ l) => inherited[l.name] || "0").join(" ")
|
|
6709
|
-
: ""}
|
|
6710
|
-
@input=${debouncedStyleCommit(`short:${shortProp}`, 400, (/** @type {any} */ e) => {
|
|
6711
|
-
let s = S;
|
|
6712
|
-
for (const l of longhands) {
|
|
6713
|
-
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6714
|
-
}
|
|
6715
|
-
s = commitFn(s, shortProp, e.target.value || undefined);
|
|
6716
|
-
update(s);
|
|
6717
|
-
})}
|
|
6718
|
-
></sp-textfield>
|
|
6719
|
-
<sp-action-button
|
|
6720
|
-
size="xs"
|
|
6721
|
-
quiet
|
|
6722
|
-
@click=${(/** @type {any} */ e) => {
|
|
6723
|
-
e.stopPropagation();
|
|
6724
|
-
updateUi("styleShorthands", { ...S.ui.styleShorthands, [shortProp]: !isExpanded });
|
|
6725
|
-
}}
|
|
6726
|
-
>
|
|
6727
|
-
${isExpanded
|
|
6728
|
-
? html`<sp-icon-chevron-down slot="icon"></sp-icon-chevron-down>`
|
|
6729
|
-
: html`<sp-icon-chevron-right slot="icon"></sp-icon-chevron-right>`}
|
|
6730
|
-
</sp-action-button>
|
|
6731
|
-
</div>
|
|
6732
|
-
</div>
|
|
6733
|
-
${isExpanded
|
|
6734
|
-
? (() => {
|
|
6735
|
-
const isBorderSide = entry.$shorthandType === "border-side";
|
|
6736
|
-
const expanded = shortVal
|
|
6737
|
-
? isBorderSide
|
|
6738
|
-
? expandBorderSide(shortVal)
|
|
6739
|
-
: expandShorthand(shortVal, longhands.length)
|
|
6740
|
-
: null;
|
|
6741
|
-
const compress = isBorderSide ? compressBorderSide : compressShorthand;
|
|
6742
|
-
const emptyVal = isBorderSide ? "" : "0";
|
|
6743
|
-
return longhands.map(
|
|
6744
|
-
(/** @type {any} */ { name, entry: lEntry }, /** @type {any} */ idx) => {
|
|
6745
|
-
const lVal = style[name] ?? (expanded ? expanded[idx] : "");
|
|
6746
|
-
return html`
|
|
6747
|
-
<div class="style-row style-row--child" data-prop=${name}>
|
|
6748
|
-
<div class="style-row-label">
|
|
6749
|
-
${lVal !== undefined && lVal !== ""
|
|
6750
|
-
? html`<span
|
|
6751
|
-
class="set-dot"
|
|
6752
|
-
title="Clear ${name}"
|
|
6753
|
-
@click=${(/** @type {any} */ e) => {
|
|
6754
|
-
e.stopPropagation();
|
|
6755
|
-
// Recompose shorthand with this longhand cleared
|
|
6756
|
-
const vals = longhands.map(
|
|
6757
|
-
(/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6758
|
-
i === idx
|
|
6759
|
-
? emptyVal
|
|
6760
|
-
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6761
|
-
);
|
|
6762
|
-
let s = S;
|
|
6763
|
-
for (const l of longhands) {
|
|
6764
|
-
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6765
|
-
}
|
|
6766
|
-
s = commitFn(s, shortProp, compress(vals));
|
|
6767
|
-
update(s);
|
|
6768
|
-
}}
|
|
6769
|
-
></span>`
|
|
6770
|
-
: nothing}
|
|
6771
|
-
<sp-field-label size="s" title=${name}
|
|
6772
|
-
>${propLabel(lEntry, name)}</sp-field-label
|
|
6773
|
-
>
|
|
6774
|
-
</div>
|
|
6775
|
-
${widgetForType(
|
|
6776
|
-
inferInputType(lEntry),
|
|
6777
|
-
lEntry,
|
|
6778
|
-
name,
|
|
6779
|
-
lVal,
|
|
6780
|
-
(/** @type {any} */ newVal) => {
|
|
6781
|
-
// Recompose shorthand with this longhand updated
|
|
6782
|
-
const vals = longhands.map((/** @type {any} */ l, /** @type {any} */ i) =>
|
|
6783
|
-
i === idx
|
|
6784
|
-
? newVal || emptyVal
|
|
6785
|
-
: (style[l.name] ?? (expanded ? expanded[i] : emptyVal)),
|
|
6786
|
-
);
|
|
6787
|
-
let s = S;
|
|
6788
|
-
for (const l of longhands) {
|
|
6789
|
-
if (style[l.name] !== undefined) s = commitFn(s, l.name, undefined);
|
|
6790
|
-
}
|
|
6791
|
-
s = commitFn(s, shortProp, compress(vals));
|
|
6792
|
-
update(s);
|
|
6793
|
-
renderRightPanel();
|
|
6794
|
-
},
|
|
6795
|
-
{ placeholder: !lVal && inherited[name] ? String(inherited[name]) : "" },
|
|
6796
|
-
)}
|
|
6797
|
-
</div>
|
|
6798
|
-
`;
|
|
6799
|
-
},
|
|
6800
|
-
);
|
|
6801
|
-
})()
|
|
6802
|
-
: nothing}
|
|
6803
|
-
`;
|
|
6804
|
-
}
|
|
6805
|
-
|
|
6806
|
-
function styleSidebarTemplate(
|
|
6807
|
-
/** @type {any} */ node,
|
|
6808
|
-
/** @type {any} */ activeMediaTab,
|
|
6809
|
-
/** @type {any} */ activeSelector,
|
|
6810
|
-
) {
|
|
6811
|
-
const style = node.style || {};
|
|
6812
|
-
const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
6813
|
-
const mediaNames = sizeBreakpoints.map((bp) => bp.name);
|
|
6814
|
-
const activeTab = activeMediaTab;
|
|
6815
|
-
|
|
6816
|
-
// ── Media tabs template ──────────────────────────────────────────────────
|
|
6817
|
-
const mediaTabsT =
|
|
6818
|
-
mediaNames.length > 0
|
|
6819
|
-
? html`
|
|
6820
|
-
<sp-tabs
|
|
6821
|
-
size="s"
|
|
6822
|
-
selected=${activeTab || "base"}
|
|
6823
|
-
@change=${(/** @type {any} */ e) => {
|
|
6824
|
-
const val = e.target.selected;
|
|
6825
|
-
const newMedia = val === "base" ? null : val;
|
|
6826
|
-
if (newMedia !== S.ui.activeMedia) {
|
|
6827
|
-
updateUi("activeMedia", newMedia);
|
|
6828
|
-
}
|
|
6829
|
-
}}
|
|
6830
|
-
>
|
|
6831
|
-
<sp-tab label="Base" value="base"></sp-tab>
|
|
6832
|
-
${mediaNames.map(
|
|
6833
|
-
(name) => html` <sp-tab label=${mediaDisplayName(name)} value=${name}></sp-tab> `,
|
|
6834
|
-
)}
|
|
6835
|
-
</sp-tabs>
|
|
6836
|
-
`
|
|
6837
|
-
: nothing;
|
|
6838
|
-
|
|
6839
|
-
// ── Selector dropdown ──────────────────────────────────────────────────────
|
|
6840
|
-
const contextStyle = activeTab ? style[`@${activeTab}`] || {} : style;
|
|
6841
|
-
const existingSelectors = Object.keys(contextStyle).filter(isNestedSelector);
|
|
6842
|
-
const existingSet = new Set(existingSelectors);
|
|
6843
|
-
const commonSet = new Set(COMMON_SELECTORS);
|
|
6844
|
-
const extraSelectors = existingSelectors.filter((s) => !commonSet.has(s));
|
|
6845
|
-
if (activeSelector && !commonSet.has(activeSelector) && !existingSet.has(activeSelector)) {
|
|
6846
|
-
extraSelectors.unshift(activeSelector);
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
|
-
const _selectorVal = activeSelector || "__base__";
|
|
6850
|
-
const selectorT = html`
|
|
6851
|
-
<div class="selector-bar">
|
|
6852
|
-
<sp-picker
|
|
6853
|
-
class="selector-select"
|
|
6854
|
-
.value=${live(_selectorVal)}
|
|
6855
|
-
@change=${(/** @type {any} */ e) => {
|
|
6856
|
-
const val = e.target.value;
|
|
6857
|
-
if (val === "__add_custom__") {
|
|
6858
|
-
requestAnimationFrame(() => {
|
|
6859
|
-
e.target.value = activeSelector || "__base__";
|
|
6860
|
-
});
|
|
6861
|
-
// Show inline input — imperative since it's a one-off interaction
|
|
6862
|
-
const picker = e.target;
|
|
6863
|
-
const bar = picker.closest(".selector-bar");
|
|
6864
|
-
picker.style.display = "none";
|
|
6865
|
-
const inp = document.createElement("input");
|
|
6866
|
-
inp.type = "text";
|
|
6867
|
-
inp.className = "selector-custom-input";
|
|
6868
|
-
inp.placeholder = ":hover, .child, &.active, [attr]";
|
|
6869
|
-
bar.appendChild(inp);
|
|
6870
|
-
inp.focus();
|
|
6871
|
-
let done = false;
|
|
6872
|
-
const finish = (/** @type {any} */ accept) => {
|
|
6873
|
-
if (done) return;
|
|
6874
|
-
done = true;
|
|
6875
|
-
const v = inp.value.trim();
|
|
6876
|
-
inp.remove();
|
|
6877
|
-
picker.style.display = "";
|
|
6878
|
-
if (accept && v && isNestedSelector(v)) {
|
|
6879
|
-
updateUi("activeSelector", v);
|
|
6880
|
-
}
|
|
6881
|
-
};
|
|
6882
|
-
inp.addEventListener("keydown", (ev) => {
|
|
6883
|
-
if (ev.key === "Enter") finish(true);
|
|
6884
|
-
else if (ev.key === "Escape") finish(false);
|
|
6885
|
-
});
|
|
6886
|
-
inp.addEventListener("blur", () => finish(inp.value.trim().length > 0));
|
|
6887
|
-
return;
|
|
6888
|
-
}
|
|
6889
|
-
const newSelector = val === "__base__" ? null : val;
|
|
6890
|
-
updateUi("activeSelector", newSelector);
|
|
6891
|
-
}}
|
|
6892
|
-
>
|
|
6893
|
-
<sp-menu-item value="__base__">(base)</sp-menu-item>
|
|
6894
|
-
<sp-menu-divider></sp-menu-divider>
|
|
6895
|
-
${COMMON_SELECTORS.map(
|
|
6896
|
-
(s) => html`
|
|
6897
|
-
<sp-menu-item value=${s}>${existingSet.has(s) ? `${s} \u25CF` : s}</sp-menu-item>
|
|
6898
|
-
`,
|
|
6899
|
-
)}
|
|
6900
|
-
${extraSelectors.length > 0
|
|
6901
|
-
? html`
|
|
6902
|
-
<sp-menu-divider></sp-menu-divider>
|
|
6903
|
-
${extraSelectors.map((s) => html` <sp-menu-item value=${s}>${s} ●</sp-menu-item> `)}
|
|
6904
|
-
`
|
|
6905
|
-
: nothing}
|
|
6906
|
-
<sp-menu-divider></sp-menu-divider>
|
|
6907
|
-
<sp-menu-item value="__add_custom__">+ Add custom…</sp-menu-item>
|
|
6908
|
-
</sp-picker>
|
|
6909
|
-
</div>
|
|
6910
|
-
`;
|
|
6911
|
-
|
|
6912
|
-
// ── Determine the active style object ──────────────────────────────────────
|
|
6913
|
-
/** @type {Record<string, any>} */
|
|
6914
|
-
let activeStyle;
|
|
6915
|
-
/** @type {any} */
|
|
6916
|
-
let commitStyle;
|
|
6917
|
-
if (activeSelector && activeTab && mediaNames.length > 0) {
|
|
6918
|
-
activeStyle = (style[`@${activeTab}`] || {})[activeSelector] || {};
|
|
6919
|
-
commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
6920
|
-
updateMediaNestedStyle(s, S.selection, activeTab, activeSelector, prop, val);
|
|
6921
|
-
} else if (activeSelector) {
|
|
6922
|
-
activeStyle = style[activeSelector] || {};
|
|
6923
|
-
commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
6924
|
-
updateNestedStyle(s, S.selection, activeSelector, prop, val);
|
|
6925
|
-
} else if (activeTab !== null && mediaNames.length > 0) {
|
|
6926
|
-
activeStyle = {};
|
|
6927
|
-
for (const [p, v] of Object.entries(style[`@${activeTab}`] || {})) {
|
|
6928
|
-
if (typeof v !== "object") activeStyle[p] = v;
|
|
6929
|
-
}
|
|
6930
|
-
commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
6931
|
-
updateMediaStyle(s, S.selection, activeTab, prop, val);
|
|
6932
|
-
} else {
|
|
6933
|
-
activeStyle = {};
|
|
6934
|
-
for (const [p, v] of Object.entries(style)) {
|
|
6935
|
-
if (typeof v !== "object") activeStyle[p] = v;
|
|
6936
|
-
}
|
|
6937
|
-
commitStyle = (/** @type {any} */ s, /** @type {any} */ prop, /** @type {any} */ val) =>
|
|
6938
|
-
updateStyle(s, S.selection, prop, val);
|
|
6939
|
-
}
|
|
6940
|
-
|
|
6941
|
-
// ── Compute inherited style from higher breakpoints ──────────────────────
|
|
6942
|
-
/** @type {Record<string, any>} */
|
|
6943
|
-
const inheritedStyle = computeInheritedStyle(style, mediaNames, activeTab, activeSelector);
|
|
6944
|
-
|
|
6945
|
-
// Auto-open sections that have properties
|
|
6946
|
-
const newSections = autoOpenSections({ style: activeStyle }, S.ui.styleSections);
|
|
6947
|
-
if (JSON.stringify(newSections) !== JSON.stringify(S.ui.styleSections)) {
|
|
6948
|
-
session = { ...session, ui: { ...session.ui, styleSections: newSections } };
|
|
6949
|
-
S = toFlat(doc, session);
|
|
6950
|
-
}
|
|
6951
|
-
|
|
6952
|
-
// Partition properties into sections
|
|
6953
|
-
const sectionProps = /** @type {Record<string, any[]>} */ ({});
|
|
6954
|
-
for (const sec of cssMeta.$sections) sectionProps[sec.key] = [];
|
|
6955
|
-
|
|
6956
|
-
for (const [prop, entry] of /** @type {[string, any][]} */ (Object.entries(cssMeta.$defs))) {
|
|
6957
|
-
if (typeof entry.$shorthand === "string") continue;
|
|
6958
|
-
const sec = entry.$section || "other";
|
|
6959
|
-
sectionProps[sec].push({ prop, entry });
|
|
6960
|
-
}
|
|
6961
|
-
for (const sec of cssMeta.$sections) {
|
|
6962
|
-
sectionProps[sec.key].sort(
|
|
6963
|
-
(/** @type {any} */ a, /** @type {any} */ b) => a.entry.$order - b.entry.$order,
|
|
6964
|
-
);
|
|
6965
|
-
}
|
|
6966
|
-
|
|
6967
|
-
const otherProps = [];
|
|
6968
|
-
for (const prop of Object.keys(activeStyle)) {
|
|
6969
|
-
if (!(/** @type {Record<string, any>} */ (cssMeta.$defs)[prop])) otherProps.push(prop);
|
|
6970
|
-
}
|
|
6971
|
-
|
|
6972
|
-
// ── Section templates ────────────────────────────────────────────────────
|
|
6973
|
-
const sectionTemplates = cssMeta.$sections
|
|
6974
|
-
.filter((sec) => sec.key !== "other")
|
|
6975
|
-
.map((sec) => {
|
|
6976
|
-
const entries = sectionProps[sec.key];
|
|
6977
|
-
|
|
6978
|
-
const sectionActiveProps = entries.filter((/** @type {any} */ { prop, entry }) => {
|
|
6979
|
-
if (activeStyle[prop] !== undefined) return true;
|
|
6980
|
-
if (inferInputType(entry) === "shorthand") {
|
|
6981
|
-
return getLonghands(prop).some(
|
|
6982
|
-
(/** @type {any} */ l) => activeStyle[l.name] !== undefined,
|
|
6983
|
-
);
|
|
6984
|
-
}
|
|
6985
|
-
return false;
|
|
6986
|
-
});
|
|
6987
|
-
|
|
6988
|
-
const rows = [];
|
|
6989
|
-
for (const { prop, entry } of entries) {
|
|
6990
|
-
const val = activeStyle[prop];
|
|
6991
|
-
const hasVal = val !== undefined;
|
|
6992
|
-
const condMet = allConditionsPass(entry, activeStyle);
|
|
6993
|
-
const type = inferInputType(entry);
|
|
6994
|
-
if (!hasVal && !condMet) continue;
|
|
6995
|
-
|
|
6996
|
-
if (type === "shorthand") {
|
|
6997
|
-
const longhands = getLonghands(prop);
|
|
6998
|
-
const hasAny =
|
|
6999
|
-
hasVal || longhands.some((/** @type {any} */ l) => activeStyle[l.name] !== undefined);
|
|
7000
|
-
if (!hasAny && !condMet) continue;
|
|
7001
|
-
rows.push(
|
|
7002
|
-
renderShorthandRow(prop, entry, activeStyle, commitStyle, () => {}, inheritedStyle),
|
|
7003
|
-
);
|
|
7004
|
-
} else {
|
|
7005
|
-
const isWarning = hasVal && !condMet;
|
|
7006
|
-
if (hasVal || condMet) {
|
|
7007
|
-
rows.push(
|
|
7008
|
-
renderStyleRow(
|
|
7009
|
-
entry,
|
|
7010
|
-
prop,
|
|
7011
|
-
val ?? "",
|
|
7012
|
-
(/** @type {any} */ newVal) => update(commitStyle(S, prop, newVal || undefined)),
|
|
7013
|
-
() => update(commitStyle(S, prop, undefined)),
|
|
7014
|
-
isWarning,
|
|
7015
|
-
sec.$layout === "grid",
|
|
7016
|
-
inheritedStyle[prop],
|
|
7017
|
-
),
|
|
7018
|
-
);
|
|
7019
|
-
}
|
|
7020
|
-
}
|
|
7021
|
-
}
|
|
7022
|
-
|
|
7023
|
-
const isOpen = S.ui.styleSections[sec.key] ?? false;
|
|
7024
|
-
|
|
7025
|
-
return html`
|
|
7026
|
-
<sp-accordion-item
|
|
7027
|
-
label=${sec.label}
|
|
7028
|
-
.open=${isOpen}
|
|
7029
|
-
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
7030
|
-
updateUi("styleSections", { ...S.ui.styleSections, [sec.key]: e.target.open });
|
|
7031
|
-
}}
|
|
7032
|
-
>
|
|
7033
|
-
${sectionActiveProps.length > 0
|
|
7034
|
-
? html`
|
|
7035
|
-
<span slot="heading" style="display:flex;align-items:center;gap:6px">
|
|
7036
|
-
${sec.label}
|
|
7037
|
-
<span
|
|
7038
|
-
class="set-dot set-dot--section"
|
|
7039
|
-
title="Clear all ${sec.label.toLowerCase()} properties"
|
|
7040
|
-
@click=${(/** @type {any} */ e) => {
|
|
7041
|
-
e.stopPropagation();
|
|
7042
|
-
e.preventDefault();
|
|
7043
|
-
let s = S;
|
|
7044
|
-
for (const { prop, entry } of sectionActiveProps) {
|
|
7045
|
-
if (activeStyle[prop] !== undefined) s = commitStyle(s, prop, undefined);
|
|
7046
|
-
if (inferInputType(entry) === "shorthand") {
|
|
7047
|
-
for (const l of getLonghands(prop)) {
|
|
7048
|
-
if (activeStyle[l.name] !== undefined)
|
|
7049
|
-
s = commitStyle(s, l.name, undefined);
|
|
7050
|
-
}
|
|
7051
|
-
}
|
|
7052
|
-
}
|
|
7053
|
-
update(s);
|
|
7054
|
-
}}
|
|
7055
|
-
></span>
|
|
7056
|
-
</span>
|
|
7057
|
-
`
|
|
7058
|
-
: nothing}
|
|
7059
|
-
<div class=${sec.$layout === "grid" ? "style-section-body--grid" : ""}>${rows}</div>
|
|
7060
|
-
</sp-accordion-item>
|
|
7061
|
-
`;
|
|
7062
|
-
});
|
|
7063
|
-
|
|
7064
|
-
// ── Custom section ─────────────────────────────────────────────────────────
|
|
7065
|
-
const customIsOpen = S.ui.styleSections.other ?? otherProps.length > 0;
|
|
7066
|
-
const customSectionT = html`
|
|
7067
|
-
<sp-accordion-item
|
|
7068
|
-
label="Custom"
|
|
7069
|
-
.open=${customIsOpen}
|
|
7070
|
-
@sp-accordion-item-toggle=${(/** @type {any} */ e) => {
|
|
7071
|
-
updateUi("styleSections", { ...S.ui.styleSections, other: e.target.open });
|
|
7072
|
-
}}
|
|
7073
|
-
>
|
|
7074
|
-
<div>
|
|
7075
|
-
${otherProps.map(
|
|
7076
|
-
(prop) => html`
|
|
7077
|
-
<div class="kv-row">
|
|
7078
|
-
<sp-textfield
|
|
7079
|
-
size="s"
|
|
7080
|
-
class="kv-key"
|
|
7081
|
-
.value=${live(prop)}
|
|
7082
|
-
@change=${(/** @type {any} */ e) => {
|
|
7083
|
-
const newProp = e.target.value.trim();
|
|
7084
|
-
if (newProp && newProp !== prop) {
|
|
7085
|
-
let s = commitStyle(S, prop, undefined);
|
|
7086
|
-
s = commitStyle(s, newProp, String(activeStyle[prop]));
|
|
7087
|
-
update(s);
|
|
7088
|
-
}
|
|
7089
|
-
}}
|
|
7090
|
-
></sp-textfield>
|
|
7091
|
-
<sp-textfield
|
|
7092
|
-
size="s"
|
|
7093
|
-
class="kv-val"
|
|
7094
|
-
.value=${live(String(activeStyle[prop]))}
|
|
7095
|
-
placeholder=${ifDefined(cssInitialMap.get(prop))}
|
|
7096
|
-
@input=${debouncedStyleCommit(`custom:${prop}`, 400, (/** @type {any} */ e) => {
|
|
7097
|
-
update(commitStyle(S, prop, e.target.value));
|
|
7098
|
-
})}
|
|
7099
|
-
></sp-textfield>
|
|
7100
|
-
<sp-action-button
|
|
7101
|
-
size="xs"
|
|
7102
|
-
quiet
|
|
7103
|
-
@click=${() => update(commitStyle(S, prop, undefined))}
|
|
7104
|
-
>
|
|
7105
|
-
<sp-icon-close slot="icon"></sp-icon-close>
|
|
7106
|
-
</sp-action-button>
|
|
7107
|
-
</div>
|
|
7108
|
-
`,
|
|
7109
|
-
)}
|
|
7110
|
-
<div style="display:flex;gap:4px;padding-top:4px">
|
|
7111
|
-
<sp-textfield
|
|
7112
|
-
size="s"
|
|
7113
|
-
placeholder="Property name…"
|
|
7114
|
-
style="flex:1"
|
|
7115
|
-
@keydown=${(/** @type {any} */ e) => {
|
|
7116
|
-
if (e.key === "Enter") {
|
|
7117
|
-
e.preventDefault();
|
|
7118
|
-
const prop = e.target.value.trim();
|
|
7119
|
-
if (prop) {
|
|
7120
|
-
const initial = cssInitialMap.get(prop) || "";
|
|
7121
|
-
update(commitStyle(S, prop, initial || ""));
|
|
7122
|
-
e.target.value = "";
|
|
7123
|
-
}
|
|
7124
|
-
}
|
|
7125
|
-
}}
|
|
7126
|
-
></sp-textfield>
|
|
7127
|
-
</div>
|
|
7128
|
-
</div>
|
|
7129
|
-
</sp-accordion-item>
|
|
7130
|
-
`;
|
|
7131
|
-
|
|
7132
|
-
return html`
|
|
7133
|
-
<div class="style-sidebar">
|
|
7134
|
-
${mediaTabsT} ${selectorT}
|
|
7135
|
-
<sp-accordion allow-multiple size="s"> ${sectionTemplates} ${customSectionT} </sp-accordion>
|
|
7136
|
-
</div>
|
|
7137
|
-
`;
|
|
7138
|
-
}
|
|
7139
|
-
|
|
7140
|
-
/** Top-level Style panel — returns a lit-html template */
|
|
7141
|
-
function renderStylePanelTemplate() {
|
|
7142
|
-
if (canvasMode === "settings" && S.ui.stylebookSelection) {
|
|
7143
|
-
const node = S.document;
|
|
7144
|
-
if (!node) return html`<div class="empty-state">No document loaded</div>`;
|
|
7145
|
-
return html`
|
|
7146
|
-
<div class="stylebook-style-header">Styling: <${S.ui.stylebookSelection}></div>
|
|
7147
|
-
${styleSidebarTemplate(node, S.ui.activeMedia, S.ui.activeSelector)}
|
|
7148
|
-
`;
|
|
7149
|
-
}
|
|
7150
|
-
if (!S.selection) return html`<div class="empty-state">Select an element to style</div>`;
|
|
7151
|
-
const node = getNodeAtPath(S.document, S.selection);
|
|
7152
|
-
if (!node) return html`<div class="empty-state">Select an element to style</div>`;
|
|
7153
|
-
return styleSidebarTemplate(node, S.ui.activeMedia, S.ui.activeSelector);
|
|
7154
|
-
}
|
|
7155
|
-
|
|
7156
|
-
/** @deprecated — use renderStylePanelTemplate() for lit-html integration */
|
|
7157
|
-
function _renderStylePanel(/** @type {any} */ container) {
|
|
7158
|
-
litRender(renderStylePanelTemplate(), container);
|
|
7159
|
-
}
|
|
7160
|
-
|
|
7161
|
-
/** Single property input row */
|
|
7162
|
-
function _fieldRow(
|
|
7163
|
-
/** @type {any} */ label,
|
|
7164
|
-
/** @type {any} */ type,
|
|
7165
|
-
/** @type {any} */ value,
|
|
7166
|
-
/** @type {any} */ onChange,
|
|
7167
|
-
/** @type {any} */ _datalistId,
|
|
7168
|
-
) {
|
|
7169
|
-
/** @type {any} */
|
|
7170
|
-
let debounceTimer;
|
|
7171
|
-
const onInput = (/** @type {any} */ e) => {
|
|
7172
|
-
clearTimeout(debounceTimer);
|
|
7173
|
-
debounceTimer = setTimeout(() => onChange(e.target.value), 400);
|
|
7174
|
-
};
|
|
7175
|
-
const inputTpl =
|
|
7176
|
-
type === "textarea"
|
|
7177
|
-
? html`<sp-textfield
|
|
7178
|
-
multiline
|
|
7179
|
-
size="s"
|
|
7180
|
-
value=${value ?? ""}
|
|
7181
|
-
@input=${onInput}
|
|
7182
|
-
></sp-textfield>`
|
|
7183
|
-
: type === "checkbox"
|
|
7184
|
-
? html`<sp-checkbox
|
|
7185
|
-
?checked=${!!value}
|
|
7186
|
-
@change=${(/** @type {any} */ e) => onChange(e.target.checked)}
|
|
7187
|
-
></sp-checkbox>`
|
|
7188
|
-
: html`<sp-textfield size="s" value=${value ?? ""} @input=${onInput}></sp-textfield>`;
|
|
7189
|
-
return html`
|
|
7190
|
-
<div class="field-row">
|
|
7191
|
-
<sp-field-label size="s">${label}</sp-field-label>
|
|
7192
|
-
${inputTpl}
|
|
7193
|
-
</div>
|
|
7194
|
-
`;
|
|
7195
|
-
}
|
|
5742
|
+
// ─── Style panel ────────────────────────────────────────────────────────────
|
|
5743
|
+
// Extracted to panels/style-utils.js, panels/style-inputs.js, panels/style-panel.js
|
|
7196
5744
|
|
|
7197
5745
|
/** Check if a selection path is inside a $map template (contains [..., "children", "map", ...]). */
|
|
7198
5746
|
function isInsideMapTemplate(/** @type {any} */ path) {
|
|
@@ -7316,7 +5864,7 @@ function kvRow(
|
|
|
7316
5864
|
clearTimeout(debounceTimer);
|
|
7317
5865
|
debounceTimer = setTimeout(() => onChange(currentKey, currentVal), 400);
|
|
7318
5866
|
};
|
|
7319
|
-
const placeholder = datalistId === "css-props" ?
|
|
5867
|
+
const placeholder = datalistId === "css-props" ? getCssInitialMap().get(key) || "" : "";
|
|
7320
5868
|
return html`
|
|
7321
5869
|
<div class="kv-row">
|
|
7322
5870
|
<sp-textfield
|
|
@@ -7330,7 +5878,7 @@ function kvRow(
|
|
|
7330
5878
|
@change=${datalistId === "css-props"
|
|
7331
5879
|
? (/** @type {any} */ e) => {
|
|
7332
5880
|
const el = e.target.closest(".kv-row")?.querySelector(".kv-val");
|
|
7333
|
-
if (el) el.setAttribute("placeholder",
|
|
5881
|
+
if (el) el.setAttribute("placeholder", getCssInitialMap().get(e.target.value) || "");
|
|
7334
5882
|
}
|
|
7335
5883
|
: nothing}
|
|
7336
5884
|
></sp-textfield>
|