@jxsuite/studio 0.10.1 → 0.11.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/README.md +82 -0
- package/dist/studio.js +1105 -235
- package/dist/studio.js.map +35 -25
- package/package.json +30 -29
- package/src/browse/browse.js +48 -22
- package/src/canvas/canvas-live-render.js +140 -16
- package/src/canvas/canvas-render.js +10 -7
- package/src/files/files.js +2 -0
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/imports-panel.js +2 -2
- package/src/panels/overlays.js +15 -3
- package/src/panels/panel-events.js +10 -0
- package/src/panels/properties-panel.js +302 -8
- package/src/panels/stylebook-panel.js +58 -22
- package/src/settings/{collections-editor.js → content-types-editor.js} +70 -49
- package/src/settings/schema-field-ui.js +58 -5
- package/src/site-context.js +105 -0
- package/src/state.js +1 -1
- package/src/store.js +9 -1
- package/src/studio.js +10 -4
- package/src/ui/spectrum.js +2 -0
- package/src/utils/studio-utils.js +5 -5
- package/src/view.js +3 -0
|
@@ -27,7 +27,7 @@ import { renderFieldRow } from "../ui/field-row.js";
|
|
|
27
27
|
import {
|
|
28
28
|
attrLabel,
|
|
29
29
|
inferInputType,
|
|
30
|
-
|
|
30
|
+
findContentTypeSchema,
|
|
31
31
|
friendlyNameToVar,
|
|
32
32
|
camelToLabel,
|
|
33
33
|
parseCemType,
|
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
import { isCustomElementDoc, collectCssParts } from "./signals-panel.js";
|
|
36
36
|
import { mediaDisplayName } from "./shared.js";
|
|
37
37
|
import { getCssInitialMap } from "./style-utils.js";
|
|
38
|
+
import { renderMediaPicker } from "../ui/media-picker.js";
|
|
39
|
+
import { getEffectiveLayoutPath, invalidateLayoutCache } from "../site-context.js";
|
|
40
|
+
import { getPlatform } from "../platform.js";
|
|
38
41
|
import htmlMeta from "../../data/html-meta.json";
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -210,7 +213,7 @@ function kvRow(
|
|
|
210
213
|
function renderFrontmatterOnlyPanel() {
|
|
211
214
|
const S = getState();
|
|
212
215
|
const fm = S.content?.frontmatter || {};
|
|
213
|
-
const col =
|
|
216
|
+
const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
|
|
214
217
|
const schemaProps = col?.schema?.properties;
|
|
215
218
|
const requiredFields = new Set(col?.schema?.required || []);
|
|
216
219
|
|
|
@@ -245,9 +248,12 @@ function renderFrontmatterOnlyPanel() {
|
|
|
245
248
|
return html`<div class="empty-state">No frontmatter. Select an element to inspect.</div>`;
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
const pageT = renderPageSection(S.document || {});
|
|
252
|
+
|
|
248
253
|
return html`
|
|
249
254
|
<div class="style-sidebar">
|
|
250
255
|
<sp-accordion allow-multiple size="s">
|
|
256
|
+
${pageT}
|
|
251
257
|
<sp-accordion-item label=${col ? `Frontmatter (${col.name})` : "Frontmatter"} open>
|
|
252
258
|
<div class="style-section-body">
|
|
253
259
|
${fields.map((f) => renderFmFieldRow(f.field, f.entry, f.value, requiredFields))}
|
|
@@ -336,6 +342,72 @@ function renderFmFieldRow(
|
|
|
336
342
|
});
|
|
337
343
|
}
|
|
338
344
|
|
|
345
|
+
if (entry.format === "image") {
|
|
346
|
+
return renderFieldRow({
|
|
347
|
+
prop: field,
|
|
348
|
+
label: displayLabel,
|
|
349
|
+
hasValue: hasVal,
|
|
350
|
+
onClear,
|
|
351
|
+
widget: renderMediaPicker(field, value, (v) =>
|
|
352
|
+
update(updateFrontmatter(getState(), field, v || undefined)),
|
|
353
|
+
),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (entry.type === "array" && entry.items?.format === "image") {
|
|
358
|
+
const images = Array.isArray(value) ? value : [];
|
|
359
|
+
return renderFieldRow({
|
|
360
|
+
prop: field,
|
|
361
|
+
label: displayLabel,
|
|
362
|
+
hasValue: hasVal,
|
|
363
|
+
onClear,
|
|
364
|
+
widget: html`
|
|
365
|
+
<div class="gallery-picker">
|
|
366
|
+
<div class="gallery-picker-strip">
|
|
367
|
+
${images.map(
|
|
368
|
+
(img, i) => html`
|
|
369
|
+
<div class="gallery-picker-item">
|
|
370
|
+
<img src=${img} alt="" class="gallery-picker-thumb" />
|
|
371
|
+
<sp-action-button
|
|
372
|
+
size="xs"
|
|
373
|
+
quiet
|
|
374
|
+
title="Remove"
|
|
375
|
+
@click=${() => {
|
|
376
|
+
const next = images.filter((_, idx) => idx !== i);
|
|
377
|
+
update(updateFrontmatter(getState(), field, next.length ? next : undefined));
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
<sp-icon-close slot="icon"></sp-icon-close>
|
|
381
|
+
</sp-action-button>
|
|
382
|
+
</div>
|
|
383
|
+
`,
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
${renderMediaPicker(`${field}:add`, "", (v) => {
|
|
387
|
+
if (!v) return;
|
|
388
|
+
const next = [...images, v];
|
|
389
|
+
update(updateFrontmatter(getState(), field, next));
|
|
390
|
+
})}
|
|
391
|
+
</div>
|
|
392
|
+
`,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (entry.$ref) {
|
|
397
|
+
const targetName = entry.$ref.replace("#/contentTypes/", "");
|
|
398
|
+
const targetDef = projectState?.projectConfig?.contentTypes?.[targetName];
|
|
399
|
+
const picker = renderReferencePicker(field, value, targetName, targetDef, (v) =>
|
|
400
|
+
update(updateFrontmatter(getState(), field, v || undefined)),
|
|
401
|
+
);
|
|
402
|
+
return renderFieldRow({
|
|
403
|
+
prop: field,
|
|
404
|
+
label: displayLabel,
|
|
405
|
+
hasValue: hasVal,
|
|
406
|
+
onClear,
|
|
407
|
+
widget: picker,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
339
411
|
if (entry.type === "number") {
|
|
340
412
|
return renderFieldRow({
|
|
341
413
|
prop: field,
|
|
@@ -374,6 +446,78 @@ function renderFmFieldRow(
|
|
|
374
446
|
});
|
|
375
447
|
}
|
|
376
448
|
|
|
449
|
+
// ─── Reference picker ────────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
/** @type {Map<string, { slug: string; title: string }[]>} */
|
|
452
|
+
const refEntriesCache = new Map();
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Render a reference field as a picker of entries from the target content type.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} field
|
|
458
|
+
* @param {any} value
|
|
459
|
+
* @param {string} targetName
|
|
460
|
+
* @param {any} targetDef
|
|
461
|
+
* @param {(val: any) => void} onCommit
|
|
462
|
+
*/
|
|
463
|
+
function renderReferencePicker(field, value, targetName, targetDef, onCommit) {
|
|
464
|
+
if (!targetDef?.source) {
|
|
465
|
+
return html`<sp-textfield
|
|
466
|
+
size="s"
|
|
467
|
+
placeholder="slug"
|
|
468
|
+
.value=${live(value || "")}
|
|
469
|
+
@input=${debouncedStyleCommit(`fm:${field}`, 400, (/** @type {any} */ e) =>
|
|
470
|
+
onCommit(e.target.value),
|
|
471
|
+
)}
|
|
472
|
+
></sp-textfield>`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const cacheKey = targetName;
|
|
476
|
+
if (!refEntriesCache.has(cacheKey)) {
|
|
477
|
+
loadRefEntries(targetName, targetDef);
|
|
478
|
+
}
|
|
479
|
+
const entries = refEntriesCache.get(cacheKey) || [];
|
|
480
|
+
|
|
481
|
+
return html`
|
|
482
|
+
<sp-picker
|
|
483
|
+
size="s"
|
|
484
|
+
label=${targetName}
|
|
485
|
+
.value=${live(value || "")}
|
|
486
|
+
@change=${(/** @type {any} */ e) => onCommit(e.target.value || undefined)}
|
|
487
|
+
>
|
|
488
|
+
<sp-menu-item value="">— none —</sp-menu-item>
|
|
489
|
+
${entries.map(
|
|
490
|
+
(ent) => html`<sp-menu-item value=${ent.slug}>${ent.title || ent.slug}</sp-menu-item>`,
|
|
491
|
+
)}
|
|
492
|
+
</sp-picker>
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function loadRefEntries(/** @type {string} */ targetName, /** @type {any} */ targetDef) {
|
|
497
|
+
const platform = getPlatform();
|
|
498
|
+
const sourceDir = targetDef.source.replace(/^\.\//, "").split("/**")[0].split("/*")[0];
|
|
499
|
+
try {
|
|
500
|
+
const listing = await platform.listDirectory(sourceDir);
|
|
501
|
+
const entries = [];
|
|
502
|
+
for (const item of listing) {
|
|
503
|
+
if (item.type === "directory") continue;
|
|
504
|
+
const slug = item.name.replace(/\.[^.]+$/, "");
|
|
505
|
+
let title = slug;
|
|
506
|
+
try {
|
|
507
|
+
const content = await platform.readFile(item.path);
|
|
508
|
+
const match = content.match(/^---[\s\S]*?title:\s*(.+?)[\r\n]/m);
|
|
509
|
+
if (match) title = match[1].trim().replace(/^["']|["']$/g, "");
|
|
510
|
+
} catch {}
|
|
511
|
+
entries.push({ slug, title });
|
|
512
|
+
}
|
|
513
|
+
refEntriesCache.set(targetName, entries);
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function invalidateRefCache() {
|
|
518
|
+
refEntriesCache.clear();
|
|
519
|
+
}
|
|
520
|
+
|
|
377
521
|
// ─── Sub-templates ──────────────────────────────────────────────────────────
|
|
378
522
|
|
|
379
523
|
/** Repeater fields template */
|
|
@@ -840,6 +984,149 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
|
|
|
840
984
|
`;
|
|
841
985
|
}
|
|
842
986
|
|
|
987
|
+
// ─── Layout picker ──────────────────────────────────────────────────────────
|
|
988
|
+
|
|
989
|
+
/** @type {{ name: string; path: string }[] | null} */
|
|
990
|
+
let layoutEntries = null;
|
|
991
|
+
|
|
992
|
+
async function loadLayoutEntries() {
|
|
993
|
+
try {
|
|
994
|
+
const platform = getPlatform();
|
|
995
|
+
const listing = await platform.listDirectory("layouts");
|
|
996
|
+
layoutEntries = listing
|
|
997
|
+
.filter((/** @type {any} */ f) => f.type === "file" && f.name.endsWith(".json"))
|
|
998
|
+
.map((/** @type {any} */ f) => ({
|
|
999
|
+
name: f.name.replace(/\.json$/, ""),
|
|
1000
|
+
path: `./layouts/${f.name}`,
|
|
1001
|
+
}));
|
|
1002
|
+
} catch {
|
|
1003
|
+
layoutEntries = [];
|
|
1004
|
+
}
|
|
1005
|
+
renderOnly("rightPanel");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function invalidateLayoutPickerCache() {
|
|
1009
|
+
layoutEntries = null;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function isPageDocument(/** @type {any} */ documentPath) {
|
|
1013
|
+
if (!documentPath || !projectState?.isSiteProject) return false;
|
|
1014
|
+
return documentPath.startsWith("pages/") || documentPath.startsWith("./pages/");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function renderPageSection(/** @type {any} */ node) {
|
|
1018
|
+
const S = getState();
|
|
1019
|
+
if (!isPageDocument(S.documentPath)) return nothing;
|
|
1020
|
+
|
|
1021
|
+
if (layoutEntries === null) {
|
|
1022
|
+
loadLayoutEntries();
|
|
1023
|
+
return nothing;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const currentLayout = node.$layout;
|
|
1027
|
+
const defaultLayout = projectState?.projectConfig?.defaults?.layout;
|
|
1028
|
+
const effectivePath = getEffectiveLayoutPath(currentLayout);
|
|
1029
|
+
const displayValue =
|
|
1030
|
+
currentLayout === false ? "__none__" : currentLayout ? currentLayout : "__default__";
|
|
1031
|
+
|
|
1032
|
+
return html`
|
|
1033
|
+
<sp-accordion-item label="Page" open>
|
|
1034
|
+
<div class="style-section-body">
|
|
1035
|
+
<div class="style-row" data-prop="$layout">
|
|
1036
|
+
<div class="style-row-label">
|
|
1037
|
+
${currentLayout !== undefined
|
|
1038
|
+
? html`<span
|
|
1039
|
+
class="set-dot"
|
|
1040
|
+
title="Reset to default"
|
|
1041
|
+
@click=${(/** @type {any} */ e) => {
|
|
1042
|
+
e.stopPropagation();
|
|
1043
|
+
update(updateProperty(getState(), [], "$layout", undefined));
|
|
1044
|
+
}}
|
|
1045
|
+
></span>`
|
|
1046
|
+
: nothing}
|
|
1047
|
+
<sp-field-label size="s">Layout</sp-field-label>
|
|
1048
|
+
</div>
|
|
1049
|
+
<sp-picker
|
|
1050
|
+
size="s"
|
|
1051
|
+
value=${displayValue}
|
|
1052
|
+
@change=${(/** @type {any} */ e) => {
|
|
1053
|
+
const val = e.target.value;
|
|
1054
|
+
if (val === "__default__") {
|
|
1055
|
+
update(updateProperty(getState(), [], "$layout", undefined));
|
|
1056
|
+
} else if (val === "__none__") {
|
|
1057
|
+
update(updateProperty(getState(), [], "$layout", false));
|
|
1058
|
+
} else {
|
|
1059
|
+
update(updateProperty(getState(), [], "$layout", val));
|
|
1060
|
+
}
|
|
1061
|
+
invalidateLayoutCache();
|
|
1062
|
+
}}
|
|
1063
|
+
>
|
|
1064
|
+
<sp-menu-item value="__default__"
|
|
1065
|
+
>Default${defaultLayout
|
|
1066
|
+
? ` (${defaultLayout.replace(/^\.\/layouts\//, "").replace(/\.json$/, "")})`
|
|
1067
|
+
: ""}</sp-menu-item
|
|
1068
|
+
>
|
|
1069
|
+
<sp-menu-item value="__none__">None</sp-menu-item>
|
|
1070
|
+
<sp-menu-divider></sp-menu-divider>
|
|
1071
|
+
${layoutEntries.map(
|
|
1072
|
+
(/** @type {any} */ l) =>
|
|
1073
|
+
html`<sp-menu-item value=${l.path}>${l.name}</sp-menu-item>`,
|
|
1074
|
+
)}
|
|
1075
|
+
</sp-picker>
|
|
1076
|
+
</div>
|
|
1077
|
+
${effectivePath
|
|
1078
|
+
? html`<div style="font-size:10px;color:var(--fg-dim);padding:2px 0;font-style:italic">
|
|
1079
|
+
Wraps page content via <slot> distribution
|
|
1080
|
+
</div>`
|
|
1081
|
+
: nothing}
|
|
1082
|
+
</div>
|
|
1083
|
+
</sp-accordion-item>
|
|
1084
|
+
`;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ─── Layout selection panel ─────────────────────────────────────────────────
|
|
1088
|
+
|
|
1089
|
+
function renderLayoutSelectionPanel(/** @type {any} */ ctx) {
|
|
1090
|
+
const { el, layoutPath } = view.layoutSelection;
|
|
1091
|
+
const tagName = el?.tagName?.toLowerCase() || "element";
|
|
1092
|
+
const className = el?.className || "";
|
|
1093
|
+
const displayPath = layoutPath || "layout";
|
|
1094
|
+
|
|
1095
|
+
return html`
|
|
1096
|
+
<div class="style-sidebar">
|
|
1097
|
+
<sp-accordion allow-multiple size="s">
|
|
1098
|
+
<sp-accordion-item label="Layout Element" open>
|
|
1099
|
+
<div class="style-section-body">
|
|
1100
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px">
|
|
1101
|
+
<span
|
|
1102
|
+
style="font-size:9px;padding:2px 6px;background:var(--spectrum-purple-600);color:white;border-radius:3px;text-transform:uppercase;letter-spacing:0.5px"
|
|
1103
|
+
>Layout</span
|
|
1104
|
+
>
|
|
1105
|
+
<code style="font-size:12px;font-family:monospace"><${tagName}></code>
|
|
1106
|
+
</div>
|
|
1107
|
+
${className
|
|
1108
|
+
? html`<div class="style-row">
|
|
1109
|
+
<div class="style-row-label">
|
|
1110
|
+
<sp-field-label size="s">Class</sp-field-label>
|
|
1111
|
+
</div>
|
|
1112
|
+
<span style="font-size:11px;color:var(--fg-dim);word-break:break-all"
|
|
1113
|
+
>${className}</span
|
|
1114
|
+
>
|
|
1115
|
+
</div>`
|
|
1116
|
+
: nothing}
|
|
1117
|
+
<div style="font-size:10px;color:var(--fg-dim);padding:4px 0;font-style:italic">
|
|
1118
|
+
This element is part of the page layout. Edit it by opening the layout file.
|
|
1119
|
+
</div>
|
|
1120
|
+
<span class="kv-add" @click=${() => ctx.navigateToComponent(displayPath)}
|
|
1121
|
+
>Open Layout →</span
|
|
1122
|
+
>
|
|
1123
|
+
</div>
|
|
1124
|
+
</sp-accordion-item>
|
|
1125
|
+
</sp-accordion>
|
|
1126
|
+
</div>
|
|
1127
|
+
`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
843
1130
|
// ─── Main entry point ───────────────────────────────────────────────────────
|
|
844
1131
|
|
|
845
1132
|
/**
|
|
@@ -850,6 +1137,11 @@ function mediaBreakpointRowTemplate(/** @type {any} */ name, /** @type {any} */
|
|
|
850
1137
|
export function renderPropertiesPanelTemplate(ctx) {
|
|
851
1138
|
const S = getState();
|
|
852
1139
|
|
|
1140
|
+
// Layout element selected — show read-only info with link to open layout
|
|
1141
|
+
if (view.layoutSelection) {
|
|
1142
|
+
return renderLayoutSelectionPanel(ctx);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
853
1145
|
if (!S.selection) {
|
|
854
1146
|
if (S.mode === "content") {
|
|
855
1147
|
return renderFrontmatterOnlyPanel();
|
|
@@ -1276,7 +1568,7 @@ export function renderPropertiesPanelTemplate(ctx) {
|
|
|
1276
1568
|
S.mode === "content"
|
|
1277
1569
|
? (() => {
|
|
1278
1570
|
const fm = S.content?.frontmatter || {};
|
|
1279
|
-
const col =
|
|
1571
|
+
const col = findContentTypeSchema(S.documentPath, projectState?.projectConfig);
|
|
1280
1572
|
const schemaProps = col?.schema?.properties;
|
|
1281
1573
|
const requiredFields = new Set(col?.schema?.required || []);
|
|
1282
1574
|
|
|
@@ -1323,15 +1615,17 @@ export function renderPropertiesPanelTemplate(ctx) {
|
|
|
1323
1615
|
})()
|
|
1324
1616
|
: nothing;
|
|
1325
1617
|
|
|
1618
|
+
const pageT = isRoot ? renderPageSection(node) : nothing;
|
|
1619
|
+
|
|
1326
1620
|
// ── Assemble ──
|
|
1327
1621
|
const tpl = html`
|
|
1328
1622
|
<div class="style-sidebar">
|
|
1329
1623
|
<sp-accordion allow-multiple size="s">
|
|
1330
|
-
${
|
|
1331
|
-
${isMapNode ? nothing :
|
|
1332
|
-
${isMapNode ? nothing :
|
|
1333
|
-
${isMapNode ? nothing :
|
|
1334
|
-
${isMapNode ? nothing : cssPartsT}
|
|
1624
|
+
${pageT} ${frontmatterT} ${isMapNode ? repeaterT : elemT}
|
|
1625
|
+
${isMapNode ? nothing : observedAttrsT} ${isMapNode ? nothing : switchT}
|
|
1626
|
+
${isMapNode ? nothing : compPropsT} ${isMapNode ? nothing : attrSectionTemplates}
|
|
1627
|
+
${isMapNode ? nothing : customSectionT} ${isMapNode ? nothing : mediaT}
|
|
1628
|
+
${isMapNode ? nothing : cssPropsT} ${isMapNode ? nothing : cssPartsT}
|
|
1335
1629
|
</sp-accordion>
|
|
1336
1630
|
</div>
|
|
1337
1631
|
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stylebook panel — renders the Settings mode canvas (element catalog, design variables,
|
|
3
|
-
* definitions,
|
|
3
|
+
* definitions, content types). Extracted from studio.js Phase 4e.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
@@ -19,14 +19,14 @@ import {
|
|
|
19
19
|
projectState,
|
|
20
20
|
} from "../store.js";
|
|
21
21
|
import { view } from "../view.js";
|
|
22
|
-
import { defineElement } from "@jxsuite/runtime";
|
|
22
|
+
import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
|
|
23
23
|
import { componentRegistry } from "../files/components.js";
|
|
24
24
|
import { getEffectiveStyle, getEffectiveMedia } from "../site-context.js";
|
|
25
25
|
import { parseMediaEntries, activeBreakpointsForWidth } from "../utils/canvas-media.js";
|
|
26
26
|
import { mediaDisplayName } from "./shared.js";
|
|
27
27
|
import { friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
|
|
28
28
|
import { renderDefsEditor } from "../settings/defs-editor.js";
|
|
29
|
-
import {
|
|
29
|
+
import { renderContentTypesEditor } from "../settings/content-types-editor.js";
|
|
30
30
|
import stylebookMeta from "../../data/stylebook-meta.json";
|
|
31
31
|
|
|
32
32
|
export { stylebookMeta };
|
|
@@ -67,12 +67,12 @@ export function renderStylebookMode(ctx) {
|
|
|
67
67
|
>
|
|
68
68
|
<sp-tab label="Stylebook" value="stylebook"></sp-tab>
|
|
69
69
|
<sp-tab label="Definitions" value="definitions"></sp-tab>
|
|
70
|
-
<sp-tab label="
|
|
70
|
+
<sp-tab label="Content Types" value="contentTypes"></sp-tab>
|
|
71
71
|
</sp-tabs>
|
|
72
72
|
</div>
|
|
73
73
|
`;
|
|
74
74
|
|
|
75
|
-
if (settingsTab === "definitions" || settingsTab === "
|
|
75
|
+
if (settingsTab === "definitions" || settingsTab === "contentTypes") {
|
|
76
76
|
/** @type {any} */ (canvasWrap).style.overflow = "hidden";
|
|
77
77
|
|
|
78
78
|
litRender(
|
|
@@ -88,7 +88,7 @@ export function renderStylebookMode(ctx) {
|
|
|
88
88
|
canvasWrap.querySelector(".settings-editor-container")
|
|
89
89
|
);
|
|
90
90
|
if (settingsTab === "definitions") renderDefsEditor(container);
|
|
91
|
-
else
|
|
91
|
+
else renderContentTypesEditor(container);
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -261,12 +261,6 @@ export function selectStylebookTag(tag, media) {
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
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
264
|
}
|
|
271
265
|
|
|
272
266
|
/** Draw selection + hover overlays for stylebook elements */
|
|
@@ -324,7 +318,7 @@ export function renderStylebookOverlays() {
|
|
|
324
318
|
}
|
|
325
319
|
}
|
|
326
320
|
|
|
327
|
-
// ─── Internal helpers
|
|
321
|
+
// ─── Internal helpers (exported for testing) ─────────────────────────────────
|
|
328
322
|
|
|
329
323
|
/**
|
|
330
324
|
* Build a DOM element tree from a stylebook-meta.json entry.
|
|
@@ -332,8 +326,9 @@ export function renderStylebookOverlays() {
|
|
|
332
326
|
* @param {any} entry
|
|
333
327
|
* @param {any} rootStyle
|
|
334
328
|
* @param {any} activeBreakpoints
|
|
329
|
+
* @param {string | null} [parentTag]
|
|
335
330
|
*/
|
|
336
|
-
function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
331
|
+
export function buildStylebookElement(entry, rootStyle, activeBreakpoints, parentTag = null) {
|
|
337
332
|
const el = document.createElement(entry.tag);
|
|
338
333
|
if (entry.text) el.textContent = entry.text;
|
|
339
334
|
if (entry.attributes) {
|
|
@@ -344,7 +339,9 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
344
339
|
}
|
|
345
340
|
}
|
|
346
341
|
if (entry.style) el.style.cssText = entry.style;
|
|
347
|
-
const
|
|
342
|
+
const compoundSelector =
|
|
343
|
+
parentTag && parentTag !== entry.tag ? `& ${parentTag} ${entry.tag}` : null;
|
|
344
|
+
const tagStyle = (compoundSelector && rootStyle[compoundSelector]) || rootStyle[`& ${entry.tag}`];
|
|
348
345
|
if (tagStyle) {
|
|
349
346
|
for (const [prop, val] of Object.entries(tagStyle)) {
|
|
350
347
|
if (typeof val === "string" || typeof val === "number") {
|
|
@@ -354,6 +351,7 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
354
351
|
}
|
|
355
352
|
}
|
|
356
353
|
if (activeBreakpoints) {
|
|
354
|
+
// Check media overrides nested inside the tag style (selector wraps media)
|
|
357
355
|
for (const [key, val] of Object.entries(tagStyle)) {
|
|
358
356
|
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
359
357
|
const mediaName = key.slice(1);
|
|
@@ -370,9 +368,30 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
370
368
|
}
|
|
371
369
|
}
|
|
372
370
|
}
|
|
371
|
+
// Check top-level @media keys for tag-specific overrides (media wraps selector)
|
|
372
|
+
if (activeBreakpoints) {
|
|
373
|
+
const selector = compoundSelector || `& ${entry.tag}`;
|
|
374
|
+
for (const [key, val] of Object.entries(rootStyle)) {
|
|
375
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
376
|
+
const mediaName = key.slice(1);
|
|
377
|
+
if (mediaName === "--") continue;
|
|
378
|
+
if (activeBreakpoints.has(mediaName)) {
|
|
379
|
+
const mediaTagStyle = /** @type {any} */ (val)[selector];
|
|
380
|
+
if (mediaTagStyle && typeof mediaTagStyle === "object") {
|
|
381
|
+
for (const [prop, v] of Object.entries(mediaTagStyle)) {
|
|
382
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
383
|
+
try {
|
|
384
|
+
/** @type {any} */ (el.style)[prop] = v;
|
|
385
|
+
} catch {}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
373
392
|
if (entry.children) {
|
|
374
393
|
for (const child of entry.children) {
|
|
375
|
-
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints));
|
|
394
|
+
el.appendChild(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
|
|
376
395
|
}
|
|
377
396
|
}
|
|
378
397
|
return el;
|
|
@@ -385,6 +404,7 @@ function buildStylebookElement(entry, rootStyle, activeBreakpoints) {
|
|
|
385
404
|
* @returns {Promise<HTMLElement>}
|
|
386
405
|
*/
|
|
387
406
|
export async function renderComponentPreview(comp) {
|
|
407
|
+
setSkipServerFunctions(true);
|
|
388
408
|
try {
|
|
389
409
|
if (comp.source === "npm") {
|
|
390
410
|
if (!customElements.get(comp.tagName)) {
|
|
@@ -419,7 +439,13 @@ export async function renderComponentPreview(comp) {
|
|
|
419
439
|
*/
|
|
420
440
|
function hasTagStyle(rootStyle, tag) {
|
|
421
441
|
const s = rootStyle[`& ${tag}`];
|
|
422
|
-
|
|
442
|
+
if (s && typeof s === "object" && Object.keys(s).length > 0) return true;
|
|
443
|
+
const selector = `& ${tag}`;
|
|
444
|
+
for (const [key, val] of Object.entries(rootStyle)) {
|
|
445
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
446
|
+
if (/** @type {any} */ (val)[selector]) return true;
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
423
449
|
}
|
|
424
450
|
|
|
425
451
|
/**
|
|
@@ -429,13 +455,19 @@ function hasTagStyle(rootStyle, tag) {
|
|
|
429
455
|
* @param {any} customizedOnly
|
|
430
456
|
* @param {any} activeBreakpoints
|
|
431
457
|
*/
|
|
432
|
-
function renderStylebookElementsIntoCanvas(
|
|
458
|
+
export function renderStylebookElementsIntoCanvas(
|
|
433
459
|
canvasEl,
|
|
434
460
|
rootStyle,
|
|
435
461
|
filter,
|
|
436
462
|
customizedOnly,
|
|
437
463
|
activeBreakpoints,
|
|
438
464
|
) {
|
|
465
|
+
for (const [k, v] of Object.entries(rootStyle)) {
|
|
466
|
+
if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
|
|
467
|
+
canvasEl.style.setProperty(k, String(v));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
439
471
|
/** @type {import("lit-html").TemplateResult[]} */
|
|
440
472
|
const sectionTemplates = [];
|
|
441
473
|
|
|
@@ -462,10 +494,11 @@ function renderStylebookElementsIntoCanvas(
|
|
|
462
494
|
view.stylebookElToTag.set(card, entry.tag);
|
|
463
495
|
elToPath.set(card, ["__sb", entry.tag]);
|
|
464
496
|
for (const child of el.querySelectorAll("*")) {
|
|
465
|
-
const
|
|
497
|
+
const childTag = child.tagName.toLowerCase();
|
|
466
498
|
if (!view.stylebookElToTag.has(child)) {
|
|
467
|
-
|
|
468
|
-
|
|
499
|
+
const compound = childTag === entry.tag ? entry.tag : `${entry.tag} ${childTag}`;
|
|
500
|
+
view.stylebookElToTag.set(child, compound);
|
|
501
|
+
elToPath.set(child, ["__sb", compound]);
|
|
469
502
|
}
|
|
470
503
|
}
|
|
471
504
|
})}
|
|
@@ -473,7 +506,10 @@ function renderStylebookElementsIntoCanvas(
|
|
|
473
506
|
<div
|
|
474
507
|
class="element-card-preview"
|
|
475
508
|
${ref((c) => {
|
|
476
|
-
if (c
|
|
509
|
+
if (c) {
|
|
510
|
+
c.textContent = "";
|
|
511
|
+
c.appendChild(el);
|
|
512
|
+
}
|
|
477
513
|
})}
|
|
478
514
|
></div>
|
|
479
515
|
<div class="element-card-label"><${entry.tag}></div>
|