@jxsuite/studio 0.7.0 → 0.9.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 +125373 -124927
- package/dist/studio.js.map +87 -73
- package/package.json +2 -2
- package/src/canvas/canvas-utils.js +313 -0
- package/src/editor/component-inline-edit.js +316 -0
- package/src/editor/content-inline-edit.js +220 -0
- 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/canvas-dnd.js +165 -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/panel-events.js +263 -0
- package/src/panels/preview-render.js +103 -0
- package/src/panels/properties-panel.js +1340 -0
- package/src/panels/pseudo-preview.js +64 -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 +121 -4874
- package/src/utils/edit-display.js +197 -0
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
|
|
3
|
+
* definitions, collections). Extracted from studio.js Phase 4e.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
7
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
8
|
+
import { styleMap } from "lit-html/directives/style-map.js";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getState,
|
|
12
|
+
update,
|
|
13
|
+
updateStyle,
|
|
14
|
+
updateSession,
|
|
15
|
+
updateUi,
|
|
16
|
+
canvasWrap,
|
|
17
|
+
canvasPanels,
|
|
18
|
+
elToPath,
|
|
19
|
+
projectState,
|
|
20
|
+
} from "../store.js";
|
|
21
|
+
import { view } from "../view.js";
|
|
22
|
+
import { defineElement } from "@jxsuite/runtime";
|
|
23
|
+
import { componentRegistry } from "../files/components.js";
|
|
24
|
+
import { getEffectiveStyle, getEffectiveMedia } from "../site-context.js";
|
|
25
|
+
import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-media.js";
|
|
26
|
+
import { mediaDisplayName } from "./shared.js";
|
|
27
|
+
import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
|
|
28
|
+
import { renderDefsEditor } from "../settings/defs-editor.js";
|
|
29
|
+
import { renderCollectionsEditor } from "../settings/collections-editor.js";
|
|
30
|
+
import stylebookMeta from "../../data/stylebook-meta.json";
|
|
31
|
+
|
|
32
|
+
export { stylebookMeta };
|
|
33
|
+
|
|
34
|
+
/** @type {any} */
|
|
35
|
+
let _ctx = null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render the stylebook/settings mode into the canvas.
|
|
39
|
+
*
|
|
40
|
+
* @param {{
|
|
41
|
+
* canvasPanelTemplate: Function;
|
|
42
|
+
* applyTransform: Function;
|
|
43
|
+
* observeCenterUntilStable: Function;
|
|
44
|
+
* renderZoomIndicator: Function;
|
|
45
|
+
* updateActivePanelHeaders: Function;
|
|
46
|
+
* overlayBoxDescriptor: Function;
|
|
47
|
+
* effectiveZoom: Function;
|
|
48
|
+
* }} ctx
|
|
49
|
+
*/
|
|
50
|
+
export function renderStylebookMode(ctx) {
|
|
51
|
+
_ctx = ctx;
|
|
52
|
+
const S = getState();
|
|
53
|
+
|
|
54
|
+
const settingsTab = S.ui.settingsTab || "stylebook";
|
|
55
|
+
|
|
56
|
+
const settingsChromeBarTpl = html`
|
|
57
|
+
<div
|
|
58
|
+
class="sb-chrome settings-top-chrome"
|
|
59
|
+
style="position:absolute;top:0;left:0;right:0;z-index:16;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
60
|
+
>
|
|
61
|
+
<sp-tabs
|
|
62
|
+
size="s"
|
|
63
|
+
selected=${settingsTab}
|
|
64
|
+
@change=${(/** @type {any} */ e) => {
|
|
65
|
+
updateUi("settingsTab", e.target.selected);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
69
|
+
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
70
|
+
<sp-tab label="Collections" value="collections"></sp-tab>
|
|
71
|
+
</sp-tabs>
|
|
72
|
+
</div>
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
if (settingsTab === "definitions" || settingsTab === "collections") {
|
|
76
|
+
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
77
|
+
|
|
78
|
+
litRender(
|
|
79
|
+
html`${settingsChromeBarTpl}
|
|
80
|
+
<div
|
|
81
|
+
class="settings-editor-container"
|
|
82
|
+
style="position:absolute;inset:40px 0 0 0;overflow:auto"
|
|
83
|
+
></div>`,
|
|
84
|
+
/** @type {any} */ (canvasWrap),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const container = /** @type {HTMLElement} */ (
|
|
88
|
+
canvasWrap.querySelector(".settings-editor-container")
|
|
89
|
+
);
|
|
90
|
+
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
91
|
+
else renderCollectionsEditor(container);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stylebook tab — element catalog / variables
|
|
96
|
+
view.stylebookElToTag = new WeakMap();
|
|
97
|
+
const rootStyle = getEffectiveStyle(S.document.style);
|
|
98
|
+
const filter = (S.ui.stylebookFilter || "").toLowerCase();
|
|
99
|
+
const customizedOnly = S.ui.stylebookCustomizedOnly;
|
|
100
|
+
|
|
101
|
+
const { sizeBreakpoints, baseWidth } = parseMediaEntries(getEffectiveMedia(S.document.$media));
|
|
102
|
+
const hasMedia = sizeBreakpoints.length > 0;
|
|
103
|
+
|
|
104
|
+
const onTabClick = (/** @type {string} */ t) => {
|
|
105
|
+
updateUi("stylebookTab", t);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onFilterInput = (/** @type {any} */ e) => {
|
|
109
|
+
updateUi("stylebookFilter", e.target.value);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const onCustomizedToggle = () => {
|
|
113
|
+
updateUi("stylebookCustomizedOnly", !S.ui.stylebookCustomizedOnly);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const chromeBarTpl = html`
|
|
117
|
+
${settingsChromeBarTpl}
|
|
118
|
+
<div
|
|
119
|
+
class="sb-chrome"
|
|
120
|
+
style="position:absolute;top:36px;left:0;right:0;z-index:15;background:var(--bg-panel);border-bottom:1px solid var(--border)"
|
|
121
|
+
>
|
|
122
|
+
<sp-tabs
|
|
123
|
+
size="s"
|
|
124
|
+
selected=${S.ui.stylebookTab || "elements"}
|
|
125
|
+
@change=${(/** @type {any} */ e) => {
|
|
126
|
+
onTabClick(e.target.selected);
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
${["elements", "variables"].map(
|
|
130
|
+
(t) => html`
|
|
131
|
+
<sp-tab label=${t.charAt(0).toUpperCase() + t.slice(1)} value=${t}></sp-tab>
|
|
132
|
+
`,
|
|
133
|
+
)}
|
|
134
|
+
</sp-tabs>
|
|
135
|
+
${S.ui.stylebookTab === "elements"
|
|
136
|
+
? html`
|
|
137
|
+
<input
|
|
138
|
+
class="field-input"
|
|
139
|
+
style="flex:1;max-width:200px;margin-left:8px"
|
|
140
|
+
placeholder="Filter…"
|
|
141
|
+
.value=${S.ui.stylebookFilter}
|
|
142
|
+
@input=${onFilterInput}
|
|
143
|
+
/>
|
|
144
|
+
<button
|
|
145
|
+
class="tb-toggle${S.ui.stylebookCustomizedOnly ? " active" : ""}"
|
|
146
|
+
style="margin-left:4px"
|
|
147
|
+
@click=${onCustomizedToggle}
|
|
148
|
+
>
|
|
149
|
+
Customized
|
|
150
|
+
</button>
|
|
151
|
+
`
|
|
152
|
+
: nothing}
|
|
153
|
+
</div>
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
157
|
+
|
|
158
|
+
/** @type {any[]} */
|
|
159
|
+
const allPanelDefs = [];
|
|
160
|
+
if (hasMedia) {
|
|
161
|
+
allPanelDefs.push({
|
|
162
|
+
name: "base",
|
|
163
|
+
displayName: mediaDisplayName("--"),
|
|
164
|
+
width: baseWidth,
|
|
165
|
+
activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
|
|
166
|
+
});
|
|
167
|
+
for (const bp of sizeBreakpoints) {
|
|
168
|
+
allPanelDefs.push({
|
|
169
|
+
name: bp.name,
|
|
170
|
+
displayName: mediaDisplayName(bp.name),
|
|
171
|
+
width: bp.width,
|
|
172
|
+
activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const renderIntoPanel = (/** @type {any} */ panel, /** @type {any} */ activeBreakpoints) => {
|
|
178
|
+
panel.canvas.classList.add("sb-canvas");
|
|
179
|
+
if (S.ui.stylebookTab === "elements") {
|
|
180
|
+
renderStylebookElementsIntoCanvas(
|
|
181
|
+
panel.canvas,
|
|
182
|
+
rootStyle,
|
|
183
|
+
filter,
|
|
184
|
+
customizedOnly,
|
|
185
|
+
activeBreakpoints,
|
|
186
|
+
);
|
|
187
|
+
for (const child of panel.canvas.querySelectorAll("*")) {
|
|
188
|
+
child.style.pointerEvents = "none";
|
|
189
|
+
}
|
|
190
|
+
registerStylebookPanelEvents(panel);
|
|
191
|
+
} else {
|
|
192
|
+
renderStylebookVarsIntoCanvas(panel.canvas, rootStyle);
|
|
193
|
+
panel.overlayClk.style.pointerEvents = "none";
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** @type {{ tpl: any; panel: any; activeSet: any }[]} */
|
|
198
|
+
let panelEntries;
|
|
199
|
+
if (!hasMedia) {
|
|
200
|
+
const effectiveMedia = getEffectiveMedia(S.document.$media);
|
|
201
|
+
const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
|
|
202
|
+
const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
|
|
203
|
+
const entry = ctx.canvasPanelTemplate(
|
|
204
|
+
hasBaseWidth ? "base" : null,
|
|
205
|
+
label,
|
|
206
|
+
!hasBaseWidth,
|
|
207
|
+
hasBaseWidth ? baseWidth : undefined,
|
|
208
|
+
);
|
|
209
|
+
panelEntries = [{ tpl: entry.tpl, panel: entry.panel, activeSet: new Set() }];
|
|
210
|
+
} else {
|
|
211
|
+
panelEntries = allPanelDefs.map((def) => {
|
|
212
|
+
const label = `${def.displayName} (${def.width}px)`;
|
|
213
|
+
const { tpl, panel } = ctx.canvasPanelTemplate(def.name, label, false, def.width);
|
|
214
|
+
return { tpl, panel, activeSet: def.activeSet };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
litRender(
|
|
219
|
+
html`
|
|
220
|
+
${chromeBarTpl}
|
|
221
|
+
<div
|
|
222
|
+
class="panzoom-wrap"
|
|
223
|
+
style="transform-origin:0 0;padding-top:72px"
|
|
224
|
+
${ref((el) => {
|
|
225
|
+
if (el) view.panzoomWrap = /** @type {HTMLDivElement} */ (el);
|
|
226
|
+
})}
|
|
227
|
+
>
|
|
228
|
+
${panelEntries.map((e) => e.tpl)}
|
|
229
|
+
</div>
|
|
230
|
+
`,
|
|
231
|
+
/** @type {any} */ (canvasWrap),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
for (const { panel, activeSet } of panelEntries) {
|
|
235
|
+
canvasPanels.push(panel);
|
|
236
|
+
renderIntoPanel(panel, activeSet);
|
|
237
|
+
}
|
|
238
|
+
if (hasMedia) {
|
|
239
|
+
ctx.updateActivePanelHeaders();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
ctx.applyTransform();
|
|
243
|
+
ctx.observeCenterUntilStable();
|
|
244
|
+
ctx.renderZoomIndicator();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Select a tag in the stylebook — shared by layers panel click and canvas click.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} tag
|
|
251
|
+
* @param {string | null} [media]
|
|
252
|
+
*/
|
|
253
|
+
export function selectStylebookTag(tag, media) {
|
|
254
|
+
updateSession({
|
|
255
|
+
selection: [],
|
|
256
|
+
ui: {
|
|
257
|
+
stylebookSelection: tag,
|
|
258
|
+
rightTab: "style",
|
|
259
|
+
activeSelector: `& ${tag}`,
|
|
260
|
+
...(media !== undefined ? { activeMedia: media } : {}),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
renderStylebookOverlays();
|
|
264
|
+
requestAnimationFrame(() => {
|
|
265
|
+
if (canvasPanels.length > 0) {
|
|
266
|
+
const el = findStylebookEl(canvasPanels[0].canvas, tag);
|
|
267
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Draw selection + hover overlays for stylebook elements */
|
|
273
|
+
export function renderStylebookOverlays() {
|
|
274
|
+
if (!_ctx) return;
|
|
275
|
+
if (canvasPanels.length === 0) return;
|
|
276
|
+
|
|
277
|
+
const S = getState();
|
|
278
|
+
const selectedTag = S.ui.stylebookSelection;
|
|
279
|
+
|
|
280
|
+
for (const panel of canvasPanels) {
|
|
281
|
+
const hoverTag = panel._lastHoverTag;
|
|
282
|
+
/**
|
|
283
|
+
* @type {{
|
|
284
|
+
* cls: string;
|
|
285
|
+
* top: string;
|
|
286
|
+
* left: string;
|
|
287
|
+
* width: string;
|
|
288
|
+
* height: string;
|
|
289
|
+
* label?: string;
|
|
290
|
+
* }[]}
|
|
291
|
+
*/
|
|
292
|
+
const boxes = [];
|
|
293
|
+
|
|
294
|
+
if (hoverTag && hoverTag !== selectedTag) {
|
|
295
|
+
const el = findStylebookEl(panel.canvas, hoverTag);
|
|
296
|
+
if (el) boxes.push({ ..._ctx.overlayBoxDescriptor(el, "hover", panel), label: undefined });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (selectedTag) {
|
|
300
|
+
const el = findStylebookEl(panel.canvas, selectedTag);
|
|
301
|
+
if (el)
|
|
302
|
+
boxes.push({
|
|
303
|
+
..._ctx.overlayBoxDescriptor(el, "selection", panel),
|
|
304
|
+
label: `<${selectedTag}>`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
litRender(
|
|
309
|
+
html`
|
|
310
|
+
${panel.dropLine}
|
|
311
|
+
${boxes.map(
|
|
312
|
+
(b) => html`
|
|
313
|
+
<div
|
|
314
|
+
class=${b.cls}
|
|
315
|
+
style="top:${b.top};left:${b.left};width:${b.width};height:${b.height}"
|
|
316
|
+
>
|
|
317
|
+
${b.label ? html`<div class="overlay-label">${b.label}</div>` : nothing}
|
|
318
|
+
</div>
|
|
319
|
+
`,
|
|
320
|
+
)}
|
|
321
|
+
`,
|
|
322
|
+
panel.overlay,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Build a DOM element tree from a stylebook-meta.json entry.
|
|
331
|
+
*
|
|
332
|
+
* @param {any} entry
|
|
333
|
+
* @param {any} rootStyle
|
|
334
|
+
* @param {any} activeBreakpoints
|
|
335
|
+
*/
|
|
336
|
+
function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
337
|
+
const el = document.createElement(entry.tag);
|
|
338
|
+
if (entry.text) el.textContent = entry.text;
|
|
339
|
+
if (entry.attributes) {
|
|
340
|
+
for (const [k, v] of Object.entries(entry.attributes)) {
|
|
341
|
+
try {
|
|
342
|
+
el.setAttribute(k, /** @type {string} */ (v));
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (entry.style) el.style.cssText = entry.style;
|
|
347
|
+
const tagStyle = rootStyle[`& ${entry.tag}`];
|
|
348
|
+
if (tagStyle) {
|
|
349
|
+
for (const [prop, val] of Object.entries(tagStyle)) {
|
|
350
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
351
|
+
try {
|
|
352
|
+
/** @type {any} */ (el.style)[prop] = val;
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (activeBreakpoints) {
|
|
357
|
+
for (const [key, val] of Object.entries(tagStyle)) {
|
|
358
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
359
|
+
const mediaName = key.slice(1);
|
|
360
|
+
if (mediaName === "--") continue;
|
|
361
|
+
if (activeBreakpoints.has(mediaName)) {
|
|
362
|
+
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
363
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
364
|
+
try {
|
|
365
|
+
/** @type {any} */ (el.style)[prop] = v;
|
|
366
|
+
} catch {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (entry.children) {
|
|
374
|
+
for (const child of entry.children) {
|
|
375
|
+
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return el;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Render a live component preview by registering its custom element and instantiating it.
|
|
383
|
+
*
|
|
384
|
+
* @param {any} comp
|
|
385
|
+
* @returns {Promise<HTMLElement>}
|
|
386
|
+
*/
|
|
387
|
+
export async function renderComponentPreview(comp) {
|
|
388
|
+
try {
|
|
389
|
+
if (comp.source === "npm") {
|
|
390
|
+
if (!customElements.get(comp.tagName)) {
|
|
391
|
+
throw new Error("not registered");
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
const root = projectState?.projectRoot;
|
|
395
|
+
const url = `${location.origin}/${root ? root + "/" : ""}${comp.path}`;
|
|
396
|
+
await defineElement(url);
|
|
397
|
+
}
|
|
398
|
+
const el = document.createElement(comp.tagName);
|
|
399
|
+
for (const p of comp.props || []) {
|
|
400
|
+
if (p.default !== undefined && p.default !== "false" && p.default !== "''") {
|
|
401
|
+
const val = String(p.default).replace(/^'|'$/g, "");
|
|
402
|
+
el.setAttribute(p.name, val);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return el;
|
|
406
|
+
} catch (/** @type {any} */ e) {
|
|
407
|
+
console.warn("Component preview failed:", comp.tagName, e);
|
|
408
|
+
const fallback = document.createElement("div");
|
|
409
|
+
fallback.style.cssText =
|
|
410
|
+
"padding:12px;border:1px dashed var(--border);border-radius:4px;color:var(--fg-dim)";
|
|
411
|
+
fallback.textContent = `<${comp.tagName}>`;
|
|
412
|
+
return fallback;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {any} rootStyle
|
|
418
|
+
* @param {any} tag
|
|
419
|
+
*/
|
|
420
|
+
function hasTagStyle(rootStyle, tag) {
|
|
421
|
+
const s = rootStyle[`& ${tag}`];
|
|
422
|
+
return s && typeof s === "object" && Object.keys(s).length > 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {any} canvasEl
|
|
427
|
+
* @param {any} rootStyle
|
|
428
|
+
* @param {any} filter
|
|
429
|
+
* @param {any} customizedOnly
|
|
430
|
+
* @param {any} activeBreakpoints
|
|
431
|
+
*/
|
|
432
|
+
function renderStylebookElementsIntoCanvas(
|
|
433
|
+
canvasEl,
|
|
434
|
+
rootStyle,
|
|
435
|
+
filter,
|
|
436
|
+
customizedOnly,
|
|
437
|
+
activeBreakpoints,
|
|
438
|
+
) {
|
|
439
|
+
/** @type {import("lit-html").TemplateResult[]} */
|
|
440
|
+
const sectionTemplates = [];
|
|
441
|
+
|
|
442
|
+
for (const section of stylebookMeta.$sections) {
|
|
443
|
+
let entries = /** @type {any} */ (section.elements);
|
|
444
|
+
if (filter) {
|
|
445
|
+
entries = entries.filter(
|
|
446
|
+
(/** @type {any} */ e) =>
|
|
447
|
+
e.tag.includes(filter) || section.label.toLowerCase().includes(filter),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (customizedOnly) {
|
|
451
|
+
entries = entries.filter((/** @type {any} */ e) => hasTagStyle(rootStyle, e.tag));
|
|
452
|
+
}
|
|
453
|
+
if (entries.length === 0) continue;
|
|
454
|
+
|
|
455
|
+
const cardTemplates = entries.map((/** @type {any} */ entry) => {
|
|
456
|
+
const el = buildStylebookElement(entry, rootStyle, activeBreakpoints);
|
|
457
|
+
return html`
|
|
458
|
+
<div
|
|
459
|
+
class="element-card"
|
|
460
|
+
${ref((card) => {
|
|
461
|
+
if (!card) return;
|
|
462
|
+
view.stylebookElToTag.set(card, entry.tag);
|
|
463
|
+
elToPath.set(card, ["__sb", entry.tag]);
|
|
464
|
+
for (const child of el.querySelectorAll("*")) {
|
|
465
|
+
const tag = child.tagName.toLowerCase();
|
|
466
|
+
if (!view.stylebookElToTag.has(child)) {
|
|
467
|
+
view.stylebookElToTag.set(child, tag);
|
|
468
|
+
elToPath.set(child, ["__sb", tag]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
})}
|
|
472
|
+
>
|
|
473
|
+
<div
|
|
474
|
+
class="element-card-preview"
|
|
475
|
+
${ref((c) => {
|
|
476
|
+
if (c && !c.firstChild) c.appendChild(el);
|
|
477
|
+
})}
|
|
478
|
+
></div>
|
|
479
|
+
<div class="element-card-label"><${entry.tag}></div>
|
|
480
|
+
</div>
|
|
481
|
+
`;
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
sectionTemplates.push(html`
|
|
485
|
+
<div class="sb-section">
|
|
486
|
+
<div class="sb-label">${section.label}</div>
|
|
487
|
+
<div class="sb-body">${cardTemplates}</div>
|
|
488
|
+
</div>
|
|
489
|
+
`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Custom components from registry
|
|
493
|
+
if (componentRegistry.length > 0) {
|
|
494
|
+
let comps = componentRegistry;
|
|
495
|
+
if (filter)
|
|
496
|
+
comps = comps.filter((/** @type {any} */ c) => c.tagName.toLowerCase().includes(filter));
|
|
497
|
+
if (customizedOnly)
|
|
498
|
+
comps = comps.filter((/** @type {any} */ c) => hasTagStyle(rootStyle, c.tagName));
|
|
499
|
+
if (comps.length > 0) {
|
|
500
|
+
const compCards = comps.map((/** @type {any} */ comp) => {
|
|
501
|
+
/** @type {HTMLDivElement | null} */
|
|
502
|
+
let previewEl = null;
|
|
503
|
+
const cardTpl = html`
|
|
504
|
+
<div
|
|
505
|
+
class="element-card"
|
|
506
|
+
style="display:inline-flex;width:auto"
|
|
507
|
+
${ref((card) => {
|
|
508
|
+
if (!card) return;
|
|
509
|
+
view.stylebookElToTag.set(card, comp.tagName);
|
|
510
|
+
elToPath.set(card, ["__sb", comp.tagName]);
|
|
511
|
+
})}
|
|
512
|
+
>
|
|
513
|
+
<div
|
|
514
|
+
class="element-card-preview"
|
|
515
|
+
${ref((c) => {
|
|
516
|
+
if (c) previewEl = /** @type {HTMLDivElement} */ (c);
|
|
517
|
+
})}
|
|
518
|
+
></div>
|
|
519
|
+
<div class="element-card-label"><${comp.tagName}></div>
|
|
520
|
+
</div>
|
|
521
|
+
`;
|
|
522
|
+
renderComponentPreview(comp).then((el) => {
|
|
523
|
+
if (previewEl) previewEl.appendChild(el);
|
|
524
|
+
});
|
|
525
|
+
return cardTpl;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
sectionTemplates.push(html`
|
|
529
|
+
<div class="sb-section">
|
|
530
|
+
<div class="sb-label">Components</div>
|
|
531
|
+
<div class="sb-body">${compCards}</div>
|
|
532
|
+
</div>
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (sectionTemplates.length === 0) {
|
|
538
|
+
litRender(
|
|
539
|
+
html`
|
|
540
|
+
<div style="padding:48px;text-align:center;color:var(--fg-dim);font-size:13px">
|
|
541
|
+
${customizedOnly ? "No customized elements" : "No matching elements"}
|
|
542
|
+
</div>
|
|
543
|
+
`,
|
|
544
|
+
canvasEl,
|
|
545
|
+
);
|
|
546
|
+
} else {
|
|
547
|
+
litRender(html`${sectionTemplates}`, canvasEl);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Render variables into the canvas (card-based layout matching Elements tab)
|
|
553
|
+
*
|
|
554
|
+
* @param {any} canvasEl
|
|
555
|
+
* @param {any} rootStyle
|
|
556
|
+
*/
|
|
557
|
+
function renderStylebookVarsIntoCanvas(canvasEl, rootStyle) {
|
|
558
|
+
const varCats = stylebookMeta.$variables;
|
|
559
|
+
|
|
560
|
+
/** @type {Record<string, any>} */
|
|
561
|
+
const groups = {};
|
|
562
|
+
for (const key of Object.keys(varCats)) groups[key] = [];
|
|
563
|
+
for (const [k, v] of Object.entries(rootStyle)) {
|
|
564
|
+
if (!k.startsWith("--")) continue;
|
|
565
|
+
if (typeof v !== "string" && typeof v !== "number") continue;
|
|
566
|
+
if (k.startsWith("--color")) groups.color.push([k, v]);
|
|
567
|
+
else if (k.startsWith("--font")) groups.font.push([k, v]);
|
|
568
|
+
else if (k.startsWith("--size") || k.startsWith("--spacing") || k.startsWith("--radius"))
|
|
569
|
+
groups.size.push([k, v]);
|
|
570
|
+
else groups.other.push([k, v]);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** @type {Map<string, HTMLElement | null>} */
|
|
574
|
+
const bodyRefs = new Map();
|
|
575
|
+
|
|
576
|
+
const sectionTemplates = Object.entries(varCats).map(([catKey, catMeta]) => {
|
|
577
|
+
const vars = groups[catKey];
|
|
578
|
+
|
|
579
|
+
const onAdd = () => {
|
|
580
|
+
const bodyEl = bodyRefs.get(catKey);
|
|
581
|
+
if (!bodyEl) return;
|
|
582
|
+
const addBtn = bodyEl.querySelector(".sb-var-add-btn");
|
|
583
|
+
const row = renderVarRow(catKey, /** @type {any} */ (catMeta), null, "", true);
|
|
584
|
+
bodyEl.insertBefore(row, addBtn);
|
|
585
|
+
if (addBtn) /** @type {any} */ (addBtn).style.display = "none";
|
|
586
|
+
const nameField = /** @type {any} */ (row.querySelector("sp-textfield"));
|
|
587
|
+
if (nameField) requestAnimationFrame(() => nameField.focus());
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
return html`
|
|
591
|
+
<div class="sb-section">
|
|
592
|
+
<div class="sb-label">${/** @type {any} */ (catMeta).label}</div>
|
|
593
|
+
<div
|
|
594
|
+
class="sb-body"
|
|
595
|
+
${ref((el) => {
|
|
596
|
+
if (el) bodyRefs.set(catKey, /** @type {HTMLElement} */ (el));
|
|
597
|
+
})}
|
|
598
|
+
>
|
|
599
|
+
${vars.length > 0
|
|
600
|
+
? vars.map((/** @type {[string, any]} */ [varName, varVal]) =>
|
|
601
|
+
renderVarRow(catKey, /** @type {any} */ (catMeta), varName, String(varVal), false),
|
|
602
|
+
)
|
|
603
|
+
: html`<div class="sb-var-empty">
|
|
604
|
+
No ${/** @type {any} */ (catMeta).label.toLowerCase()} variables yet.
|
|
605
|
+
</div>`}
|
|
606
|
+
<button class="sb-var-add-btn" @click=${onAdd}>
|
|
607
|
+
<span class="sb-var-add-icon">+</span> Add ${/** @type {any} */ (catMeta).label}
|
|
608
|
+
</button>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
`;
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
litRender(html`${sectionTemplates}`, canvasEl);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Render a single variable row — used for both existing and add-new.
|
|
619
|
+
*
|
|
620
|
+
* @param {string} catKey
|
|
621
|
+
* @param {any} catMeta
|
|
622
|
+
* @param {string | null} varName
|
|
623
|
+
* @param {string} varVal
|
|
624
|
+
* @param {boolean} isNew
|
|
625
|
+
*/
|
|
626
|
+
function renderVarRow(catKey, catMeta, varName, varVal, isNew) {
|
|
627
|
+
const row = document.createElement("div");
|
|
628
|
+
row.className = isNew ? "sb-var-row is-new" : "sb-var-row";
|
|
629
|
+
|
|
630
|
+
/** @type {any} */
|
|
631
|
+
let colorPicker = null;
|
|
632
|
+
/** @type {any} */
|
|
633
|
+
let nameField = null;
|
|
634
|
+
/** @type {any} */
|
|
635
|
+
let getValueFn;
|
|
636
|
+
/** @type {any} */
|
|
637
|
+
let hexField = null;
|
|
638
|
+
|
|
639
|
+
const swatchTpl =
|
|
640
|
+
catKey === "color"
|
|
641
|
+
? html`
|
|
642
|
+
<div
|
|
643
|
+
class="sb-var-swatch"
|
|
644
|
+
style=${styleMap({ backgroundColor: varVal || "var(--accent)" })}
|
|
645
|
+
>
|
|
646
|
+
<input
|
|
647
|
+
type="color"
|
|
648
|
+
.value=${varVal && varVal.startsWith("#") ? varVal : "#007acc"}
|
|
649
|
+
${ref((el) => {
|
|
650
|
+
if (el) colorPicker = el;
|
|
651
|
+
})}
|
|
652
|
+
@input=${() => {
|
|
653
|
+
if (!colorPicker || !hexField) return;
|
|
654
|
+
hexField.value = colorPicker.value;
|
|
655
|
+
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
656
|
+
if (swatch) swatch.style.backgroundColor = colorPicker.value;
|
|
657
|
+
if (!isNew && varName) {
|
|
658
|
+
const S = getState();
|
|
659
|
+
update(updateStyle(S, [], varName, colorPicker.value));
|
|
660
|
+
}
|
|
661
|
+
}}
|
|
662
|
+
/>
|
|
663
|
+
</div>
|
|
664
|
+
`
|
|
665
|
+
: nothing;
|
|
666
|
+
|
|
667
|
+
const namePlaceholder =
|
|
668
|
+
catKey === "color"
|
|
669
|
+
? "Primary Blue"
|
|
670
|
+
: catKey === "font"
|
|
671
|
+
? "Body Serif"
|
|
672
|
+
: catKey === "size"
|
|
673
|
+
? "Spacing Large"
|
|
674
|
+
: "Border Radius";
|
|
675
|
+
|
|
676
|
+
const nameColTpl = isNew
|
|
677
|
+
? html`
|
|
678
|
+
<div class="sb-var-col-name">
|
|
679
|
+
<div class="sb-var-col-label">Name</div>
|
|
680
|
+
<sp-textfield
|
|
681
|
+
size="s"
|
|
682
|
+
placeholder=${namePlaceholder}
|
|
683
|
+
style="pointer-events:auto"
|
|
684
|
+
${ref((el) => {
|
|
685
|
+
if (el) nameField = el;
|
|
686
|
+
})}
|
|
687
|
+
></sp-textfield>
|
|
688
|
+
</div>
|
|
689
|
+
`
|
|
690
|
+
: nothing;
|
|
691
|
+
|
|
692
|
+
/** @type {any} */
|
|
693
|
+
let valueContent;
|
|
694
|
+
|
|
695
|
+
if (catKey === "color") {
|
|
696
|
+
/** @type {any} */
|
|
697
|
+
let debounce;
|
|
698
|
+
valueContent = html`
|
|
699
|
+
<sp-textfield
|
|
700
|
+
size="s"
|
|
701
|
+
.value=${varVal || "#007acc"}
|
|
702
|
+
placeholder="#007acc"
|
|
703
|
+
style="pointer-events:auto"
|
|
704
|
+
${ref((el) => {
|
|
705
|
+
if (el) hexField = el;
|
|
706
|
+
})}
|
|
707
|
+
@input=${() => {
|
|
708
|
+
clearTimeout(debounce);
|
|
709
|
+
debounce = setTimeout(() => {
|
|
710
|
+
if (!hexField) return;
|
|
711
|
+
const v = hexField.value;
|
|
712
|
+
try {
|
|
713
|
+
if (colorPicker) colorPicker.value = v.startsWith("#") ? v : colorPicker.value;
|
|
714
|
+
} catch {}
|
|
715
|
+
const swatch = /** @type {any} */ (row.querySelector(".sb-var-swatch"));
|
|
716
|
+
if (swatch) swatch.style.backgroundColor = v;
|
|
717
|
+
if (!isNew && varName) {
|
|
718
|
+
const S = getState();
|
|
719
|
+
update(updateStyle(S, [], varName, v));
|
|
720
|
+
}
|
|
721
|
+
}, 400);
|
|
722
|
+
}}
|
|
723
|
+
></sp-textfield>
|
|
724
|
+
`;
|
|
725
|
+
getValueFn = () => hexField?.value?.trim() || "";
|
|
726
|
+
} else if (catKey === "size") {
|
|
727
|
+
const ui = createUnitInput(varVal || "16px", {
|
|
728
|
+
onChange: (/** @type {any} */ newVal) => {
|
|
729
|
+
const bar = /** @type {any} */ (row.querySelector(".sb-var-size-bar"));
|
|
730
|
+
if (bar) bar.style.width = newVal;
|
|
731
|
+
if (!isNew && varName) {
|
|
732
|
+
const S = getState();
|
|
733
|
+
update(updateStyle(S, [], varName, newVal));
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
if (isNew) ui.textfield.value = "";
|
|
738
|
+
valueContent = html`<div
|
|
739
|
+
${ref((el) => {
|
|
740
|
+
if (el && !el.firstChild) el.appendChild(ui.wrap);
|
|
741
|
+
})}
|
|
742
|
+
></div>`;
|
|
743
|
+
getValueFn = () => ui.getValue();
|
|
744
|
+
} else {
|
|
745
|
+
/** @type {any} */
|
|
746
|
+
let textFieldEl = null;
|
|
747
|
+
/** @type {any} */
|
|
748
|
+
let debounce;
|
|
749
|
+
valueContent = html`
|
|
750
|
+
<sp-textfield
|
|
751
|
+
size="s"
|
|
752
|
+
.value=${varVal}
|
|
753
|
+
placeholder=${catMeta.placeholder}
|
|
754
|
+
style="pointer-events:auto"
|
|
755
|
+
${ref((el) => {
|
|
756
|
+
if (el) textFieldEl = el;
|
|
757
|
+
})}
|
|
758
|
+
@input=${() => {
|
|
759
|
+
if (!textFieldEl || isNew || !varName) return;
|
|
760
|
+
clearTimeout(debounce);
|
|
761
|
+
debounce = setTimeout(() => {
|
|
762
|
+
const v = textFieldEl.value;
|
|
763
|
+
const fontPrev = /** @type {any} */ (row.querySelector(".sb-var-font-preview"));
|
|
764
|
+
if (fontPrev) fontPrev.style.fontFamily = v;
|
|
765
|
+
const S = getState();
|
|
766
|
+
update(updateStyle(S, [], varName, v));
|
|
767
|
+
}, 400);
|
|
768
|
+
}}
|
|
769
|
+
></sp-textfield>
|
|
770
|
+
`;
|
|
771
|
+
getValueFn = () => textFieldEl?.value?.trim() || "";
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const valColTpl = html`
|
|
775
|
+
<div class="sb-var-col-value">
|
|
776
|
+
${isNew ? html`<div class="sb-var-col-label">Value</div>` : nothing} ${valueContent}
|
|
777
|
+
</div>
|
|
778
|
+
`;
|
|
779
|
+
|
|
780
|
+
const actionsTpl = isNew
|
|
781
|
+
? html`
|
|
782
|
+
<div class="sb-var-add-actions">
|
|
783
|
+
<sp-action-button
|
|
784
|
+
size="s"
|
|
785
|
+
style="pointer-events:auto"
|
|
786
|
+
@click=${() => {
|
|
787
|
+
const name = (nameField?.value || "").trim();
|
|
788
|
+
const val = getValueFn();
|
|
789
|
+
const generatedVar = friendlyNameToVar(name, catMeta.prefix);
|
|
790
|
+
if (!generatedVar || !val) return;
|
|
791
|
+
const S = getState();
|
|
792
|
+
update(updateStyle(S, [], generatedVar, val));
|
|
793
|
+
}}
|
|
794
|
+
>Add</sp-action-button
|
|
795
|
+
>
|
|
796
|
+
<sp-action-button
|
|
797
|
+
size="s"
|
|
798
|
+
quiet
|
|
799
|
+
style="pointer-events:auto"
|
|
800
|
+
@click=${() => {
|
|
801
|
+
const body = row.parentElement;
|
|
802
|
+
row.remove();
|
|
803
|
+
const addBtn = /** @type {any} */ (body?.querySelector(".sb-var-add-btn"));
|
|
804
|
+
if (addBtn) addBtn.style.display = "";
|
|
805
|
+
}}
|
|
806
|
+
>
|
|
807
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
808
|
+
</sp-action-button>
|
|
809
|
+
</div>
|
|
810
|
+
`
|
|
811
|
+
: nothing;
|
|
812
|
+
|
|
813
|
+
const headerTpl =
|
|
814
|
+
!isNew && varName
|
|
815
|
+
? html`
|
|
816
|
+
<div class="sb-var-row-header">
|
|
817
|
+
<span class="sb-var-row-title">${varDisplayName(varName, catMeta.prefix)}</span>
|
|
818
|
+
<span class="sb-var-row-ref">${varName}</span>
|
|
819
|
+
<sp-action-button
|
|
820
|
+
size="s"
|
|
821
|
+
quiet
|
|
822
|
+
class="sb-var-del"
|
|
823
|
+
style="pointer-events:auto"
|
|
824
|
+
@click=${() => {
|
|
825
|
+
const S = getState();
|
|
826
|
+
update(updateStyle(S, [], varName, undefined));
|
|
827
|
+
}}
|
|
828
|
+
>
|
|
829
|
+
<sp-icon-delete slot="icon"></sp-icon-delete>
|
|
830
|
+
</sp-action-button>
|
|
831
|
+
</div>
|
|
832
|
+
`
|
|
833
|
+
: nothing;
|
|
834
|
+
|
|
835
|
+
const addPreviewTpl = isNew
|
|
836
|
+
? html`
|
|
837
|
+
<div
|
|
838
|
+
class="sb-var-add-preview"
|
|
839
|
+
${ref((el) => {
|
|
840
|
+
if (!el || !nameField) return;
|
|
841
|
+
nameField.addEventListener("input", () => {
|
|
842
|
+
el.textContent = friendlyNameToVar(nameField.value || "", catMeta.prefix);
|
|
843
|
+
});
|
|
844
|
+
})}
|
|
845
|
+
></div>
|
|
846
|
+
`
|
|
847
|
+
: nothing;
|
|
848
|
+
|
|
849
|
+
const typePrevTpl =
|
|
850
|
+
catKey === "font" && varVal
|
|
851
|
+
? html`
|
|
852
|
+
<div class="sb-var-preview">
|
|
853
|
+
<div class="sb-var-font-preview" style=${styleMap({ fontFamily: varVal })}>
|
|
854
|
+
The quick brown fox jumps over the lazy dog
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
`
|
|
858
|
+
: catKey === "size" && varVal
|
|
859
|
+
? html`
|
|
860
|
+
<div class="sb-var-preview">
|
|
861
|
+
<div class="sb-var-size-track">
|
|
862
|
+
<div class="sb-var-size-bar" style=${styleMap({ width: varVal })}></div>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
`
|
|
866
|
+
: nothing;
|
|
867
|
+
|
|
868
|
+
litRender(
|
|
869
|
+
html`
|
|
870
|
+
${headerTpl}
|
|
871
|
+
<div class="sb-var-input-row">${swatchTpl} ${nameColTpl} ${valColTpl} ${actionsTpl}</div>
|
|
872
|
+
${addPreviewTpl} ${typePrevTpl}
|
|
873
|
+
`,
|
|
874
|
+
row,
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
return row;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Creates a combined textfield + quiet sp-picker for CSS values with units.
|
|
882
|
+
*
|
|
883
|
+
* @param {any} initialValue
|
|
884
|
+
* @param {any} [options]
|
|
885
|
+
*/
|
|
886
|
+
function createUnitInput(initialValue, { onChange, size = "s" } = {}) {
|
|
887
|
+
const match = String(initialValue).match(
|
|
888
|
+
/^(-?[\d.]+)\s*(px|em|rem|vw|vh|%|ch|ex|vmin|vmax|pt|cm|mm|in)?$/,
|
|
889
|
+
);
|
|
890
|
+
let numVal = match ? match[1] : initialValue;
|
|
891
|
+
let unitVal = match ? match[2] || "px" : "";
|
|
892
|
+
const isNumeric = !!match;
|
|
893
|
+
|
|
894
|
+
const wrap = document.createElement("div");
|
|
895
|
+
wrap.className = "sb-unit-input";
|
|
896
|
+
wrap.style.pointerEvents = "auto";
|
|
897
|
+
|
|
898
|
+
/** @type {any} */
|
|
899
|
+
let textfield = null;
|
|
900
|
+
/** @type {any} */
|
|
901
|
+
let picker = null;
|
|
902
|
+
|
|
903
|
+
const units = [
|
|
904
|
+
{ value: "px", label: "px" },
|
|
905
|
+
{ value: "rem", label: "rem" },
|
|
906
|
+
{ value: "em", label: "em" },
|
|
907
|
+
{ value: "%", label: "%" },
|
|
908
|
+
{ value: "vw", label: "vw" },
|
|
909
|
+
{ value: "vh", label: "vh" },
|
|
910
|
+
{ value: "ch", label: "ch" },
|
|
911
|
+
{ value: "pt", label: "pt" },
|
|
912
|
+
{ divider: true },
|
|
913
|
+
{ value: "auto", label: "auto" },
|
|
914
|
+
{ value: "fit-content", label: "fit-content" },
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
/** @type {any} */
|
|
918
|
+
let debounce;
|
|
919
|
+
|
|
920
|
+
function getValue() {
|
|
921
|
+
const num = textfield?.value;
|
|
922
|
+
const unit = picker?.value;
|
|
923
|
+
if (unit === "auto" || unit === "fit-content") return unit;
|
|
924
|
+
return num ? `${num}${unit}` : "";
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
litRender(
|
|
928
|
+
html`
|
|
929
|
+
<sp-textfield
|
|
930
|
+
.value=${numVal}
|
|
931
|
+
size=${size}
|
|
932
|
+
${ref((el) => {
|
|
933
|
+
if (el) textfield = el;
|
|
934
|
+
})}
|
|
935
|
+
@input=${() => {
|
|
936
|
+
clearTimeout(debounce);
|
|
937
|
+
const raw = textfield?.value?.trim();
|
|
938
|
+
const looksNumeric = /^-?[\d.]+$/.test(raw || "");
|
|
939
|
+
if (picker) picker.style.display = looksNumeric ? "" : "none";
|
|
940
|
+
debounce = setTimeout(() => {
|
|
941
|
+
if (onChange) onChange(looksNumeric ? `${raw}${picker?.value}` : raw);
|
|
942
|
+
}, 400);
|
|
943
|
+
}}
|
|
944
|
+
></sp-textfield>
|
|
945
|
+
<sp-picker
|
|
946
|
+
quiet
|
|
947
|
+
size=${size}
|
|
948
|
+
style=${styleMap({ display: isNumeric ? "" : "none" })}
|
|
949
|
+
${ref((el) => {
|
|
950
|
+
if (el) {
|
|
951
|
+
picker = el;
|
|
952
|
+
requestAnimationFrame(() => {
|
|
953
|
+
/** @type {any} */ (el).value = unitVal || "px";
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
})}
|
|
957
|
+
@change=${() => {
|
|
958
|
+
const unit = picker?.value;
|
|
959
|
+
if (unit === "auto" || unit === "fit-content") {
|
|
960
|
+
if (textfield) textfield.value = unit;
|
|
961
|
+
if (picker) picker.style.display = "none";
|
|
962
|
+
if (onChange) onChange(unit);
|
|
963
|
+
} else {
|
|
964
|
+
unitVal = unit;
|
|
965
|
+
if (onChange) onChange(getValue());
|
|
966
|
+
}
|
|
967
|
+
}}
|
|
968
|
+
>
|
|
969
|
+
${units.map((u) =>
|
|
970
|
+
u.divider
|
|
971
|
+
? html`<sp-menu-divider></sp-menu-divider>`
|
|
972
|
+
: html`<sp-menu-item value=${u.value}>${u.label}</sp-menu-item>`,
|
|
973
|
+
)}
|
|
974
|
+
</sp-picker>
|
|
975
|
+
`,
|
|
976
|
+
wrap,
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
return { wrap, textfield, picker, getValue };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Click handler for stylebook canvas — selects elements via elToPath/view.stylebookElToTag mapping
|
|
984
|
+
*
|
|
985
|
+
* @param {any} panel
|
|
986
|
+
*/
|
|
987
|
+
function registerStylebookPanelEvents(panel) {
|
|
988
|
+
const { canvas, overlayClk } = panel;
|
|
989
|
+
|
|
990
|
+
overlayClk.addEventListener("click", (/** @type {any} */ e) => {
|
|
991
|
+
const els = canvas.querySelectorAll("*");
|
|
992
|
+
for (const el of els) el.style.pointerEvents = "auto";
|
|
993
|
+
overlayClk.style.display = "none";
|
|
994
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
995
|
+
overlayClk.style.display = "";
|
|
996
|
+
for (const el of els) el.style.pointerEvents = "none";
|
|
997
|
+
|
|
998
|
+
for (const el of elements) {
|
|
999
|
+
if (!canvas.contains(el) || el === canvas) continue;
|
|
1000
|
+
let cur = /** @type {any} */ (el);
|
|
1001
|
+
while (cur && cur !== canvas) {
|
|
1002
|
+
const tag = view.stylebookElToTag.get(cur);
|
|
1003
|
+
if (tag) {
|
|
1004
|
+
const newMedia = panel.mediaName === "base" ? null : (panel.mediaName ?? null);
|
|
1005
|
+
selectStylebookTag(tag, newMedia);
|
|
1006
|
+
if (_ctx) _ctx.updateActivePanelHeaders();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
cur = cur.parentElement;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
updateSession({ ui: { stylebookSelection: null, activeSelector: null } });
|
|
1013
|
+
renderStylebookOverlays();
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
overlayClk.addEventListener("mousemove", (/** @type {any} */ e) => {
|
|
1017
|
+
const els = canvas.querySelectorAll("*");
|
|
1018
|
+
for (const el of els) el.style.pointerEvents = "auto";
|
|
1019
|
+
overlayClk.style.display = "none";
|
|
1020
|
+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
|
|
1021
|
+
overlayClk.style.display = "";
|
|
1022
|
+
for (const el of els) el.style.pointerEvents = "none";
|
|
1023
|
+
|
|
1024
|
+
let hoverTag = null;
|
|
1025
|
+
for (const el of elements) {
|
|
1026
|
+
if (!canvas.contains(el) || el === canvas) continue;
|
|
1027
|
+
let cur = /** @type {any} */ (el);
|
|
1028
|
+
while (cur && cur !== canvas) {
|
|
1029
|
+
const tag = view.stylebookElToTag.get(cur);
|
|
1030
|
+
if (tag) {
|
|
1031
|
+
hoverTag = tag;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
cur = cur.parentElement;
|
|
1035
|
+
}
|
|
1036
|
+
if (hoverTag) break;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (hoverTag !== panel._lastHoverTag) {
|
|
1040
|
+
panel._lastHoverTag = hoverTag;
|
|
1041
|
+
renderStylebookOverlays();
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** Find a stylebook element by tag in the canvas */
|
|
1047
|
+
function findStylebookEl(/** @type {any} */ canvasEl, /** @type {any} */ tag) {
|
|
1048
|
+
for (const child of canvasEl.querySelectorAll("*")) {
|
|
1049
|
+
if (view.stylebookElToTag.get(child) === tag) return child;
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|