@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.
@@ -27,7 +27,7 @@ import { renderFieldRow } from "../ui/field-row.js";
27
27
  import {
28
28
  attrLabel,
29
29
  inferInputType,
30
- findCollectionSchema,
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 = findCollectionSchema(S.documentPath, projectState?.projectConfig);
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 &lt;slot&gt; 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">&lt;${tagName}&gt;</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 = findCollectionSchema(S.documentPath, projectState?.projectConfig);
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
- ${frontmatterT} ${isMapNode ? repeaterT : elemT} ${isMapNode ? nothing : observedAttrsT}
1331
- ${isMapNode ? nothing : switchT} ${isMapNode ? nothing : compPropsT}
1332
- ${isMapNode ? nothing : attrSectionTemplates} ${isMapNode ? nothing : customSectionT}
1333
- ${isMapNode ? nothing : mediaT} ${isMapNode ? nothing : cssPropsT}
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, collections). Extracted from studio.js Phase 4e.
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 { renderCollectionsEditor } from "../settings/collections-editor.js";
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="Collections" value="collections"></sp-tab>
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 === "collections") {
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 renderCollectionsEditor(container);
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 tagStyle = rootStyle[`& ${entry.tag}`];
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
- return s && typeof s === "object" && Object.keys(s).length > 0;
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 tag = child.tagName.toLowerCase();
497
+ const childTag = child.tagName.toLowerCase();
466
498
  if (!view.stylebookElToTag.has(child)) {
467
- view.stylebookElToTag.set(child, tag);
468
- elToPath.set(child, ["__sb", tag]);
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 && !c.firstChild) c.appendChild(el);
509
+ if (c) {
510
+ c.textContent = "";
511
+ c.appendChild(el);
512
+ }
477
513
  })}
478
514
  ></div>
479
515
  <div class="element-card-label">&lt;${entry.tag}&gt;</div>