@jxsuite/studio 0.33.0 → 0.34.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.
Files changed (85) hide show
  1. package/dist/iframe-entry.js +6230 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +10754 -11060
  4. package/dist/studio.js.map +83 -72
  5. package/package.json +7 -7
  6. package/src/browse/browse.ts +11 -4
  7. package/src/canvas/canvas-helpers.ts +2 -56
  8. package/src/canvas/canvas-live-render.ts +102 -435
  9. package/src/canvas/canvas-origin.ts +66 -0
  10. package/src/canvas/canvas-patcher.ts +63 -403
  11. package/src/canvas/canvas-render.ts +70 -212
  12. package/src/canvas/canvas-utils.ts +37 -65
  13. package/src/canvas/iframe-channel.ts +154 -0
  14. package/src/canvas/iframe-drop.ts +484 -0
  15. package/src/canvas/iframe-entry.ts +600 -0
  16. package/src/canvas/iframe-host.ts +1373 -0
  17. package/src/canvas/iframe-inline-edit.ts +367 -0
  18. package/src/canvas/iframe-insert.ts +164 -0
  19. package/src/canvas/iframe-interaction.ts +176 -0
  20. package/src/canvas/iframe-keys.ts +85 -0
  21. package/src/canvas/iframe-overlay.ts +218 -0
  22. package/src/canvas/iframe-patch.ts +363 -0
  23. package/src/canvas/iframe-protocol.ts +361 -0
  24. package/src/canvas/iframe-render.ts +458 -0
  25. package/src/canvas/iframe-slash.ts +114 -0
  26. package/src/canvas/iframe-subtree.ts +113 -0
  27. package/src/canvas/path-mapping.ts +86 -0
  28. package/src/canvas/serialize-scope.ts +65 -0
  29. package/src/editor/canvas-context-menu.ts +40 -0
  30. package/src/editor/canvas-slash-bridge.ts +21 -0
  31. package/src/editor/context-menu.ts +2 -1
  32. package/src/editor/inline-edit-apply.ts +183 -0
  33. package/src/editor/inline-edit.ts +99 -21
  34. package/src/editor/inline-link.ts +89 -0
  35. package/src/editor/insert-zone-action.ts +35 -0
  36. package/src/editor/merge-tags.ts +26 -2
  37. package/src/editor/repeater-scope.ts +144 -0
  38. package/src/editor/shortcuts.ts +14 -28
  39. package/src/editor/slash-menu.ts +73 -42
  40. package/src/files/files.ts +2 -1
  41. package/src/page-params.ts +383 -0
  42. package/src/panels/ai-panel.ts +5 -7
  43. package/src/panels/block-action-bar.ts +296 -138
  44. package/src/panels/canvas-dnd-bridge.ts +397 -0
  45. package/src/panels/component-preview.ts +56 -0
  46. package/src/panels/dnd.ts +41 -17
  47. package/src/panels/drag-ghost.ts +62 -0
  48. package/src/panels/editors.ts +1 -1
  49. package/src/panels/overlays.ts +10 -125
  50. package/src/panels/properties-panel.ts +210 -0
  51. package/src/panels/right-panel.ts +0 -2
  52. package/src/panels/signals-panel.ts +136 -22
  53. package/src/panels/stylebook-doc.ts +373 -0
  54. package/src/panels/stylebook-panel.ts +46 -689
  55. package/src/panels/tab-bar.ts +159 -13
  56. package/src/panels/toolbar.ts +3 -2
  57. package/src/platforms/devserver.ts +15 -0
  58. package/src/services/monaco-setup.ts +12 -0
  59. package/src/services/render-critic.ts +9 -9
  60. package/src/settings/css-vars-editor.ts +2 -2
  61. package/src/store.ts +4 -62
  62. package/src/studio.ts +90 -40
  63. package/src/tabs/doc-op-apply.ts +89 -0
  64. package/src/tabs/patch-ops.ts +6 -2
  65. package/src/tabs/tab.ts +23 -4
  66. package/src/tabs/transact.ts +2 -74
  67. package/src/types.ts +14 -18
  68. package/src/ui/jx-theme.ts +63 -0
  69. package/src/ui/media-picker.ts +6 -4
  70. package/src/ui/spectrum.ts +5 -0
  71. package/src/utils/canvas-media.ts +0 -137
  72. package/src/utils/edit-display.ts +23 -3
  73. package/src/utils/geometry.ts +43 -0
  74. package/src/utils/link-target.ts +93 -0
  75. package/src/utils/strip-events.ts +54 -0
  76. package/src/view.ts +0 -23
  77. package/src/workspace/workspace.ts +14 -1
  78. package/src/canvas/canvas-subtree-render.ts +0 -113
  79. package/src/editor/component-inline-edit.ts +0 -349
  80. package/src/editor/content-inline-edit.ts +0 -207
  81. package/src/editor/insertion-helper.ts +0 -308
  82. package/src/panels/canvas-dnd.ts +0 -329
  83. package/src/panels/panel-events.ts +0 -306
  84. package/src/panels/preview-render.ts +0 -132
  85. package/src/panels/pseudo-preview.ts +0 -75
@@ -1,33 +1,27 @@
1
1
  /// <reference lib="dom" />
2
- /// <reference lib="dom.iterable" />
3
2
  /**
4
3
  * Stylebook panel — renders the Stylebook mode canvas (element catalog with per-file style
5
- * defaults). Extracted from studio.js Phase 4e.
4
+ * defaults) through the IFRAME canvas pipeline: a specimen document is generated parent-side
5
+ * ({@link file://./stylebook-doc.ts}) and mounted per breakpoint panel via `mountStylebookCanvas`,
6
+ * so each panel is a real width-sized viewport and `@media` blocks evaluate for real (no JS
7
+ * flatten). Hits decode to tags in the host and route back here through the injected stylebook-hit
8
+ * handler (`setStylebookHitHandler` in studio.ts).
6
9
  */
7
10
 
8
- import { html, render as litRender, nothing } from "lit-html";
11
+ import { html, render as litRender } from "lit-html";
9
12
  import { ref } from "lit-html/directives/ref.js";
10
13
  import { classMap } from "lit-html/directives/class-map.js";
11
- import { styleMap } from "lit-html/directives/style-map.js";
12
14
  import { live } from "lit-html/directives/live.js";
13
15
 
14
- import {
15
- canvasPanels,
16
- canvasWrap,
17
- elToPath,
18
- projectState,
19
- updateSession,
20
- updateUi,
21
- } from "../store";
16
+ import { canvasPanels, canvasWrap, projectState, updateSession, updateUi } from "../store";
22
17
  import { activeTab } from "../workspace/workspace";
23
18
  import { view } from "../view";
24
- import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
25
19
  import { componentRegistry } from "../files/components";
26
- import type { ComponentEntry } from "../files/components";
27
20
  import { getEffectiveMedia, getEffectiveStyle } from "../site-context";
28
- import { activeBreakpointsForWidth, parseMediaEntries } from "../utils/canvas-media";
21
+ import { parseMediaEntries } from "../utils/canvas-media";
29
22
  import { mediaDisplayName } from "./shared";
30
- import { panToCanvasEl } from "../canvas/canvas-utils";
23
+ import { buildStylebookDoc } from "./stylebook-doc";
24
+ import { mountStylebookCanvas, panToStylebookTag } from "../canvas/iframe-host";
31
25
  import stylebookMeta from "../../data/stylebook-meta.json";
32
26
  import type { TemplateResult } from "lit-html";
33
27
  import type { CanvasPanel } from "../types";
@@ -40,19 +34,6 @@ export interface StylebookEntry {
40
34
  children?: StylebookEntry[];
41
35
  }
42
36
 
43
- interface StylebookPanel {
44
- mediaName: string | null;
45
- element: HTMLElement | null;
46
- canvas: HTMLElement;
47
- overlay: HTMLElement;
48
- overlayClk: HTMLElement;
49
- viewport: HTMLElement | null;
50
- scrollContainer: HTMLElement | null;
51
- dropLine: HTMLElement | null;
52
- _width: number | null;
53
- _lastHoverTag?: string | null;
54
- }
55
-
56
37
  interface StylebookCtx {
57
38
  canvasPanelTemplate: (
58
39
  mediaName: string | null,
@@ -64,69 +45,23 @@ interface StylebookCtx {
64
45
  observeCenterUntilStable: () => void;
65
46
  renderZoomIndicator: () => void;
66
47
  updateActivePanelHeaders: () => void;
67
- overlayBoxDescriptor: (
68
- el: Element,
69
- type: string,
70
- panel: CanvasPanel,
71
- ) => {
72
- cls: string;
73
- top: string;
74
- left: string;
75
- width: string;
76
- height: string;
77
- };
78
- effectiveZoom: () => number;
79
48
  }
80
49
 
81
50
  export { default as stylebookMeta } from "../../data/stylebook-meta.json";
82
51
 
83
52
  /**
84
- * Resolve a nested tag path in a style object. e.g., "table th" style["table"]["th"]
85
- *
86
- * @param {Record<string, unknown>} style
87
- * @param {string} tagPath
88
- * @returns {Record<string, unknown> | null}
89
- */
90
- function _resolveNestedStyle(style: Record<string, unknown>, tagPath: string) {
91
- const parts = tagPath.split(" ");
92
- let obj = style;
93
- for (const part of parts) {
94
- if (!obj || typeof obj !== "object") {
95
- return null;
96
- }
97
- obj = obj[part] as Record<string, unknown>;
98
- }
99
- return obj && typeof obj === "object" ? (obj as Record<string, unknown>) : null;
100
- }
101
-
102
- let _ctx: StylebookCtx | null = null;
103
-
104
- /** Lookup: tag → entry from stylebookMeta (built once) */
105
- const _entryByTag = new Map<string, StylebookEntry>();
106
- for (const section of stylebookMeta.$sections) {
107
- for (const entry of section.elements as StylebookEntry[]) {
108
- _entryByTag.set(entry.tag, entry);
109
- }
110
- }
111
-
112
- /**
113
- * Render the stylebook/settings mode into the canvas.
53
+ * Render the stylebook mode into the canvas: chrome bar + one iframe panel per breakpoint, all
54
+ * mounting the SAME generated specimen document.
114
55
  *
115
56
  * @param {StylebookCtx} ctx
116
57
  */
117
58
  export function renderStylebookMode(ctx: StylebookCtx) {
118
- _ctx = ctx;
119
-
120
- // Stylebook mode — element catalog only
121
- view.stylebookElToTag = new WeakMap();
122
59
  const tab = activeTab.value;
123
- const rootStyle = getEffectiveStyle(tab?.doc.document?.style);
124
60
  const filter = (tab?.session.ui.stylebookFilter || "").toLowerCase();
125
61
  const customizedOnly = tab?.session.ui.stylebookCustomizedOnly;
126
62
 
127
- const { sizeBreakpoints, baseWidth } = parseMediaEntries(
128
- getEffectiveMedia(tab?.doc.document?.$media),
129
- );
63
+ const effectiveMedia = getEffectiveMedia(tab?.doc.document?.$media);
64
+ const { sizeBreakpoints, baseWidth } = parseMediaEntries(effectiveMedia);
130
65
  const hasMedia = sizeBreakpoints.length > 0;
131
66
 
132
67
  const onFilterInput = (e: Event) => {
@@ -165,18 +100,16 @@ export function renderStylebookMode(ctx: StylebookCtx) {
165
100
 
166
101
  (canvasWrap as HTMLElement).style.overflow = "hidden";
167
102
 
168
- /** @type {{ name: string; displayName: string; width: number; activeSet: Set<string> }[]} */
103
+ /** @type {{ name: string; displayName: string; width: number }[]} */
169
104
  const allPanelDefs = [];
170
105
  if (hasMedia) {
171
106
  allPanelDefs.push({
172
- activeSet: activeBreakpointsForWidth(sizeBreakpoints, baseWidth),
173
107
  displayName: mediaDisplayName("--"),
174
108
  name: "base",
175
109
  width: baseWidth,
176
110
  });
177
111
  for (const bp of sizeBreakpoints) {
178
112
  allPanelDefs.push({
179
- activeSet: activeBreakpointsForWidth(sizeBreakpoints, bp.width),
180
113
  displayName: mediaDisplayName(bp.name),
181
114
  name: bp.name,
182
115
  width: bp.width,
@@ -184,31 +117,9 @@ export function renderStylebookMode(ctx: StylebookCtx) {
184
117
  }
185
118
  }
186
119
 
187
- const renderIntoPanel = (panel: StylebookPanel, activeBreakpoints: Set<string>) => {
188
- panel.canvas.classList.add("sb-canvas");
189
- renderStylebookElementsIntoCanvas(
190
- panel.canvas,
191
- rootStyle,
192
- filter,
193
- customizedOnly,
194
- activeBreakpoints,
195
- );
196
- for (const child of panel.canvas.querySelectorAll("*")) {
197
- (child as HTMLElement).style.pointerEvents = "none";
198
- }
199
- registerStylebookPanelEvents(panel);
200
- };
201
-
202
- /**
203
- * @type {{
204
- * tpl: import("lit-html").TemplateResult;
205
- * panel: StylebookPanel;
206
- * activeSet: Set<string>;
207
- * }[]}
208
- */
120
+ /** @type {{ tpl: import("lit-html").TemplateResult; panel: CanvasPanel }[]} */
209
121
  let panelEntries;
210
122
  if (!hasMedia) {
211
- const effectiveMedia = getEffectiveMedia(tab?.doc.document?.$media);
212
123
  const hasBaseWidth = effectiveMedia && effectiveMedia["--"];
213
124
  const label = hasBaseWidth ? `${mediaDisplayName("--")} (${baseWidth}px)` : null;
214
125
  const entry = ctx.canvasPanelTemplate(
@@ -217,12 +128,12 @@ export function renderStylebookMode(ctx: StylebookCtx) {
217
128
  !hasBaseWidth,
218
129
  hasBaseWidth ? baseWidth : undefined,
219
130
  );
220
- panelEntries = [{ activeSet: new Set<string>(), panel: entry.panel, tpl: entry.tpl }];
131
+ panelEntries = [{ panel: entry.panel, tpl: entry.tpl }];
221
132
  } else {
222
133
  panelEntries = allPanelDefs.map((def) => {
223
134
  const label = `${def.displayName} (${def.width}px)`;
224
135
  const { tpl, panel } = ctx.canvasPanelTemplate(def.name, label, false, def.width);
225
- return { activeSet: def.activeSet, panel, tpl };
136
+ return { panel, tpl };
226
137
  });
227
138
  }
228
139
 
@@ -244,9 +155,26 @@ export function renderStylebookMode(ctx: StylebookCtx) {
244
155
  /** @type {HTMLElement} */ canvasWrap,
245
156
  );
246
157
 
247
- for (const { panel, activeSet } of panelEntries) {
248
- canvasPanels.push(/** @type {import("./canvas-dnd.js").CanvasPanel} */ panel);
249
- renderIntoPanel(panel, activeSet);
158
+ // ONE generated doc shared by every panel per-panel @media differentiation comes from each
159
+ // Iframe's real viewport width, not from a per-panel style flatten.
160
+ const generated = buildStylebookDoc({
161
+ components: componentRegistry,
162
+ customizedOnly: Boolean(customizedOnly),
163
+ effectiveMedia: effectiveMedia ?? {},
164
+ effectiveStyle: getEffectiveStyle(tab?.doc.document?.style),
165
+ filter,
166
+ meta: stylebookMeta as { $sections: { label: string; elements: StylebookEntry[] }[] },
167
+ projectRoot: projectState?.projectRoot ?? null,
168
+ });
169
+
170
+ for (const { panel } of panelEntries) {
171
+ canvasPanels.push(panel);
172
+ mountStylebookCanvas(
173
+ view.renderGeneration,
174
+ generated,
175
+ panel.canvas as HTMLElement,
176
+ panel._width,
177
+ );
250
178
  }
251
179
  if (hasMedia) {
252
180
  ctx.updateActivePanelHeaders();
@@ -257,111 +185,11 @@ export function renderStylebookMode(ctx: StylebookCtx) {
257
185
  ctx.renderZoomIndicator();
258
186
  }
259
187
 
260
- /** Fast-path: re-apply styles to existing stylebook elements without rebuilding the DOM. */
261
- export function refreshStylebookStyles() {
262
- const tab = activeTab.value;
263
- if (!tab) {
264
- return;
265
- }
266
- const rootStyle = getEffectiveStyle(tab.doc.document?.style);
267
-
268
- for (const panel of canvasPanels) {
269
- const { canvas } = panel;
270
- // Re-apply CSS custom properties
271
- for (const [k, v] of Object.entries(rootStyle)) {
272
- if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
273
- canvas.style.setProperty(k, String(v));
274
- }
275
- }
276
-
277
- const { sizeBreakpoints } = parseMediaEntries(getEffectiveMedia(tab.doc.document?.$media));
278
- const activeBreakpoints = panel.mediaName
279
- ? activeBreakpointsForWidth(sizeBreakpoints, panel._width as number)
280
- : new Set<string>();
281
-
282
- // Re-apply styles to each element in the canvas
283
- const allEls = canvas.querySelectorAll("*");
284
- for (const el of allEls) {
285
- const tag = view.stylebookElToTag.get(el);
286
- if (!tag) {
287
- continue;
288
- }
289
- const htmlEl = el as HTMLElement;
290
- // Determine if it's a compound selector (e.g. "ul li") or simple tag
291
- const parts = tag.split(" ");
292
- const leafTag = parts.at(-1)!;
293
- const entry = _entryByTag.get(leafTag);
294
-
295
- // Reset to base style
296
- htmlEl.style.cssText = entry?.style || "";
297
-
298
- // Apply root style for this tag (nested path)
299
- const tagStyle = _resolveNestedStyle(rootStyle, tag);
300
- if (tagStyle) {
301
- for (const [prop, val] of Object.entries(tagStyle)) {
302
- if (typeof val === "string" || typeof val === "number") {
303
- try {
304
- (htmlEl.style as unknown as Record<string, string | number>)[prop] = val;
305
- } catch {}
306
- }
307
- }
308
- // Media overrides nested in tag style
309
- if (activeBreakpoints.size > 0) {
310
- for (const [key, val] of Object.entries(tagStyle)) {
311
- if (!key.startsWith("@") || typeof val !== "object" || val === null) {
312
- continue;
313
- }
314
- const mediaName = key.slice(1);
315
- if (mediaName === "--") {
316
- continue;
317
- }
318
- if (activeBreakpoints.has(mediaName)) {
319
- for (const [prop, v] of Object.entries(val as Record<string, unknown>)) {
320
- if (typeof v === "string" || typeof v === "number") {
321
- try {
322
- (htmlEl.style as unknown as Record<string, string | number>)[prop] = v;
323
- } catch {}
324
- }
325
- }
326
- }
327
- }
328
- }
329
- }
330
- // Top-level @media keys
331
- if (activeBreakpoints.size > 0) {
332
- for (const [key, val] of Object.entries(rootStyle)) {
333
- if (!key.startsWith("@") || typeof val !== "object" || val === null) {
334
- continue;
335
- }
336
- const mediaName = key.slice(1);
337
- if (mediaName === "--") {
338
- continue;
339
- }
340
- if (activeBreakpoints.has(mediaName)) {
341
- const mediaTagStyle = _resolveNestedStyle(
342
- /** @type {Record<string, unknown>} */ val,
343
- tag,
344
- );
345
- if (mediaTagStyle && typeof mediaTagStyle === "object") {
346
- for (const [prop, v] of Object.entries(mediaTagStyle)) {
347
- if (typeof v === "string" || typeof v === "number") {
348
- try {
349
- (htmlEl.style as unknown as Record<string, string | number>)[prop] = v;
350
- } catch {}
351
- }
352
- }
353
- }
354
- }
355
- }
356
- }
357
- }
358
- }
359
-
360
- renderStylebookOverlays();
361
- }
362
-
363
188
  /**
364
- * Select a tag in the stylebook — shared by layers panel click and canvas click.
189
+ * Select a tag in the stylebook — shared by the canvas hit handler (via studio's
190
+ * setStylebookHitHandler wiring), the stylebook layers panel, and the style panel. The host's
191
+ * selection watcher tracks `ui.stylebookSelection` and measures the selected tag's card, so no
192
+ * direct overlay drawing happens here.
365
193
  *
366
194
  * @param {string} tag
367
195
  * @param {string | null} [media]
@@ -376,482 +204,11 @@ export function selectStylebookTag(tag: string, media?: string | null, { panCanv
376
204
  ...(media !== undefined ? { activeMedia: media } : {}),
377
205
  },
378
206
  });
379
- renderStylebookOverlays();
380
-
381
- if (tag && panCanvas && canvasPanels.length > 0) {
382
- const el = findStylebookEl(canvasPanels[0]!.canvas, tag);
383
- if (el) {
384
- panToCanvasEl(el);
385
- }
386
- }
387
- }
388
-
389
- /** Draw selection + hover overlays for stylebook elements */
390
- export function renderStylebookOverlays() {
391
- if (!_ctx) {
392
- return;
393
- }
394
- if (canvasPanels.length === 0) {
395
- return;
396
- }
397
-
398
- const selectedTag = activeTab.value?.session.ui.stylebookSelection;
399
207
 
400
- for (const panel of canvasPanels) {
401
- const hoverTag = (panel as StylebookPanel)._lastHoverTag;
402
- /**
403
- * @type {{
404
- * cls: string;
405
- * top: string;
406
- * left: string;
407
- * width: string;
408
- * height: string;
409
- * label?: string;
410
- * }[]}
411
- */
412
- const boxes = [];
413
-
414
- if (hoverTag && hoverTag !== selectedTag) {
415
- const el = findStylebookEl(panel.canvas, hoverTag);
416
- if (el) {
417
- boxes.push({
418
- ..._ctx.overlayBoxDescriptor(el, "hover", panel),
419
- label: undefined,
420
- });
421
- }
422
- }
423
-
424
- if (selectedTag) {
425
- const el = findStylebookEl(panel.canvas, selectedTag);
426
- if (el) {
427
- boxes.push({
428
- ..._ctx.overlayBoxDescriptor(el, "selection", panel),
429
- label: `<${selectedTag}>`,
430
- });
431
- }
432
- }
433
-
434
- litRender(
435
- html`
436
- ${panel.dropLine}
437
- ${boxes.map(
438
- (b) => html`
439
- <div
440
- class=${b.cls}
441
- style=${styleMap({
442
- height: b.height,
443
- left: b.left,
444
- top: b.top,
445
- width: b.width,
446
- })}
447
- >
448
- ${b.label ? html`<div class="overlay-label">${b.label}</div>` : nothing}
449
- </div>
450
- `,
451
- )}
452
- `,
453
- panel.overlay,
454
- );
208
+ if (tag && panCanvas) {
209
+ panToStylebookTag(tag);
455
210
  }
456
211
  }
457
212
 
458
- // ─── Internal helpers (exported for testing) ─────────────────────────────────
459
-
460
- /**
461
- * Build a DOM element tree from a stylebook-meta.json entry.
462
- *
463
- * @param {StylebookEntry} entry
464
- * @param {Record<string, unknown>} rootStyle
465
- * @param {Set<string> | null} activeBreakpoints
466
- * @param {string | null} [parentTag]
467
- */
468
- export function buildStylebookElement(
469
- entry: StylebookEntry,
470
- rootStyle: Record<string, unknown>,
471
- activeBreakpoints: Set<string> | null,
472
- parentTag: string | null = null,
473
- ) {
474
- const el = document.createElement(entry.tag);
475
- if (entry.text) {
476
- el.textContent = entry.text;
477
- }
478
- if (entry.attributes) {
479
- for (const [k, v] of Object.entries(entry.attributes)) {
480
- try {
481
- el.setAttribute(k, /** @type {string} */ v);
482
- } catch {}
483
- }
484
- }
485
- if (entry.style) {
486
- el.style.cssText = entry.style;
487
- }
488
- const compoundTag = parentTag && parentTag !== entry.tag ? `${parentTag} ${entry.tag}` : null;
489
- const tagStyle =
490
- (compoundTag && _resolveNestedStyle(rootStyle, compoundTag)) ||
491
- _resolveNestedStyle(rootStyle, entry.tag);
492
- if (tagStyle) {
493
- for (const [prop, val] of Object.entries(tagStyle)) {
494
- if (typeof val === "string" || typeof val === "number") {
495
- try {
496
- (el.style as unknown as Record<string, string | number>)[prop] = val;
497
- } catch {}
498
- }
499
- }
500
- if (activeBreakpoints) {
501
- // Check media overrides nested inside the tag style (selector wraps media)
502
- for (const [key, val] of Object.entries(tagStyle)) {
503
- if (!key.startsWith("@") || typeof val !== "object" || val === null) {
504
- continue;
505
- }
506
- const mediaName = key.slice(1);
507
- if (mediaName === "--") {
508
- continue;
509
- }
510
- if (activeBreakpoints.has(mediaName)) {
511
- for (const [prop, v] of Object.entries(val as Record<string, unknown>)) {
512
- if (typeof v === "string" || typeof v === "number") {
513
- try {
514
- (el.style as unknown as Record<string, string | number>)[prop] = v;
515
- } catch {}
516
- }
517
- }
518
- }
519
- }
520
- }
521
- }
522
- // Check top-level @media keys for tag-specific overrides (media wraps selector)
523
- if (activeBreakpoints) {
524
- const tagPath = compoundTag || entry.tag;
525
- for (const [key, val] of Object.entries(rootStyle)) {
526
- if (!key.startsWith("@") || typeof val !== "object" || val === null) {
527
- continue;
528
- }
529
- const mediaName = key.slice(1);
530
- if (mediaName === "--") {
531
- continue;
532
- }
533
- if (activeBreakpoints.has(mediaName)) {
534
- const mediaTagStyle = _resolveNestedStyle(val as Record<string, unknown>, tagPath);
535
- if (mediaTagStyle && typeof mediaTagStyle === "object") {
536
- for (const [prop, v] of Object.entries(mediaTagStyle)) {
537
- if (typeof v === "string" || typeof v === "number") {
538
- try {
539
- (el.style as unknown as Record<string, string | number>)[prop] = v;
540
- } catch {}
541
- }
542
- }
543
- }
544
- }
545
- }
546
- }
547
- if (entry.children) {
548
- for (const child of entry.children) {
549
- el.append(buildStylebookElement(child, rootStyle, activeBreakpoints, entry.tag));
550
- }
551
- }
552
- return el;
553
- }
554
-
555
- /**
556
- * Render a live component preview by registering its custom element and instantiating it.
557
- *
558
- * @param {ComponentEntry} comp
559
- * @returns {Promise<HTMLElement>}
560
- */
561
- export async function renderComponentPreview(comp: ComponentEntry) {
562
- setSkipServerFunctions(true);
563
- try {
564
- if (comp.source === "npm") {
565
- if (!customElements.get(comp.tagName)) {
566
- return _componentFallback(comp.tagName);
567
- }
568
- } else {
569
- if (comp.path && !comp.path.endsWith(".json")) {
570
- // Format-class component sources (e.g. markdown) can't be imported as modules
571
- return _componentFallback(comp.tagName);
572
- }
573
- const root = projectState?.projectRoot;
574
- const url = `${location.origin}/${root ? `${root}/` : ""}${comp.path}`;
575
- await defineElement(url);
576
- }
577
- const el = document.createElement(comp.tagName);
578
- for (const p of comp.props || []) {
579
- if (p.default !== undefined && p.default !== "false" && p.default !== "''") {
580
- const val = String(p.default).replaceAll(/^'|'$/g, "");
581
- el.setAttribute(p.name, val);
582
- }
583
- }
584
- return el;
585
- } catch (error) {
586
- console.warn("Component preview failed:", comp.tagName, error);
587
- return _componentFallback(comp.tagName);
588
- }
589
- }
590
-
591
- /** @param {string} tagName */
592
- function _componentFallback(tagName: string) {
593
- const fallback = document.createElement("div");
594
- fallback.style.cssText =
595
- "padding:12px;border:1px dashed var(--border);border-radius:var(--radius);color:var(--fg-dim)";
596
- fallback.textContent = `<${tagName}>`;
597
- return fallback;
598
- }
599
-
600
- /**
601
- * @param {Record<string, unknown>} rootStyle
602
- * @param {string} tag
603
- */
604
- function hasTagStyle(rootStyle: Record<string, unknown>, tag: string) {
605
- const s = _resolveNestedStyle(rootStyle, tag);
606
- if (s && typeof s === "object" && Object.keys(s).length > 0) {
607
- return true;
608
- }
609
- for (const [key, val] of Object.entries(rootStyle)) {
610
- if (!key.startsWith("@") || typeof val !== "object" || val === null) {
611
- continue;
612
- }
613
- const ms = _resolveNestedStyle(val as Record<string, unknown>, tag);
614
- if (ms && typeof ms === "object" && Object.keys(ms).length > 0) {
615
- return true;
616
- }
617
- }
618
- return false;
619
- }
620
-
621
- /**
622
- * @param {HTMLElement} canvasEl
623
- * @param {Record<string, unknown>} rootStyle
624
- * @param {string} filter
625
- * @param {boolean | undefined} customizedOnly
626
- * @param {Set<string> | null} activeBreakpoints
627
- */
628
- export function renderStylebookElementsIntoCanvas(
629
- canvasEl: HTMLElement,
630
- rootStyle: Record<string, unknown>,
631
- filter: string,
632
- customizedOnly: boolean | undefined,
633
- activeBreakpoints: Set<string> | null,
634
- ) {
635
- for (const [k, v] of Object.entries(rootStyle)) {
636
- if (k.startsWith("--") && (typeof v === "string" || typeof v === "number")) {
637
- canvasEl.style.setProperty(k, String(v));
638
- }
639
- }
640
-
641
- const sectionTemplates: TemplateResult[] = [];
642
-
643
- for (const section of stylebookMeta.$sections) {
644
- let entries = section.elements as StylebookEntry[];
645
- if (filter) {
646
- entries = entries.filter(
647
- (e: StylebookEntry) =>
648
- e.tag.includes(filter) || section.label.toLowerCase().includes(filter),
649
- );
650
- }
651
- if (customizedOnly) {
652
- entries = entries.filter((e: StylebookEntry) => hasTagStyle(rootStyle, e.tag));
653
- }
654
- if (entries.length === 0) {
655
- continue;
656
- }
657
-
658
- const cardTemplates = entries.map((entry: StylebookEntry) => {
659
- const el = buildStylebookElement(entry, rootStyle, activeBreakpoints);
660
- return html`
661
- <div
662
- class="element-card"
663
- ${ref((card) => {
664
- if (!card) {
665
- return;
666
- }
667
- view.stylebookElToTag.set(card, entry.tag);
668
- elToPath.set(card, ["__sb", entry.tag]);
669
- for (const child of el.querySelectorAll("*")) {
670
- const childTag = child.tagName.toLowerCase();
671
- if (!view.stylebookElToTag.has(child)) {
672
- const compound = childTag === entry.tag ? entry.tag : `${entry.tag} ${childTag}`;
673
- view.stylebookElToTag.set(child, compound);
674
- elToPath.set(child, ["__sb", compound]);
675
- }
676
- }
677
- })}
678
- >
679
- <div
680
- class="element-card-preview"
681
- ${ref((c) => {
682
- if (c) {
683
- c.textContent = "";
684
- c.append(el);
685
- }
686
- })}
687
- ></div>
688
- <div class="element-card-label">&lt;${entry.tag}&gt;</div>
689
- </div>
690
- `;
691
- });
692
-
693
- sectionTemplates.push(html`
694
- <div class="sb-section">
695
- <div class="sb-label">${section.label}</div>
696
- <div class="sb-body">${cardTemplates}</div>
697
- </div>
698
- `);
699
- }
700
-
701
- // Custom components from registry
702
- if (componentRegistry.length > 0) {
703
- let comps = componentRegistry;
704
- if (filter) {
705
- comps = comps.filter((c: ComponentEntry) => c.tagName.toLowerCase().includes(filter));
706
- }
707
- if (customizedOnly) {
708
- comps = comps.filter((c: ComponentEntry) => hasTagStyle(rootStyle, c.tagName));
709
- }
710
- if (comps.length > 0) {
711
- const compCards = comps.map((comp: ComponentEntry) => {
712
- let previewEl: HTMLDivElement | null = null;
713
- const cardTpl = html`
714
- <div
715
- class="element-card"
716
- style="display:inline-flex;width:auto"
717
- ${ref((card) => {
718
- if (!card) {
719
- return;
720
- }
721
- view.stylebookElToTag.set(card, comp.tagName);
722
- elToPath.set(card, ["__sb", comp.tagName]);
723
- })}
724
- >
725
- <div
726
- class="element-card-preview"
727
- ${ref((c) => {
728
- if (c) {
729
- previewEl = c as HTMLDivElement;
730
- }
731
- })}
732
- ></div>
733
- <div class="element-card-label">&lt;${comp.tagName}&gt;</div>
734
- </div>
735
- `;
736
- renderComponentPreview(comp)
737
- .then((el) => {
738
- if (previewEl) {
739
- previewEl.append(el);
740
- }
741
- })
742
- .catch(() => {});
743
- return cardTpl;
744
- });
745
-
746
- sectionTemplates.push(html`
747
- <div class="sb-section">
748
- <div class="sb-label">Components</div>
749
- <div class="sb-body">${compCards}</div>
750
- </div>
751
- `);
752
- }
753
- }
754
-
755
- if (sectionTemplates.length === 0) {
756
- litRender(
757
- html`
758
- <div style="padding:48px;text-align:center;color:var(--fg-dim);font-size:13px">
759
- ${customizedOnly ? "No customized elements" : "No matching elements"}
760
- </div>
761
- `,
762
- canvasEl,
763
- );
764
- } else {
765
- litRender(html`${sectionTemplates}`, canvasEl);
766
- }
767
- }
768
-
769
- /**
770
- * Click handler for stylebook canvas — selects elements via elToPath/view.stylebookElToTag mapping
771
- *
772
- * @param {StylebookPanel} panel
773
- */
774
- function registerStylebookPanelEvents(panel: StylebookPanel) {
775
- const { canvas, overlayClk } = panel;
776
-
777
- overlayClk.addEventListener("click", (e: MouseEvent) => {
778
- const els = canvas.querySelectorAll("*");
779
- for (const el of els) {
780
- (el as HTMLElement).style.pointerEvents = "auto";
781
- }
782
- overlayClk.style.display = "none";
783
- const elements = document.elementsFromPoint(e.clientX, e.clientY);
784
- overlayClk.style.display = "";
785
- for (const el of els) {
786
- (el as HTMLElement).style.pointerEvents = "none";
787
- }
788
-
789
- for (const el of elements) {
790
- if (!canvas.contains(el) || el === canvas) {
791
- continue;
792
- }
793
- let cur = el as Element | null;
794
- while (cur && cur !== canvas) {
795
- const tag = view.stylebookElToTag.get(cur);
796
- if (tag) {
797
- const newMedia = panel.mediaName === "base" ? null : (panel.mediaName ?? null);
798
- selectStylebookTag(tag, newMedia);
799
- if (_ctx) {
800
- _ctx.updateActivePanelHeaders();
801
- }
802
- return;
803
- }
804
- cur = cur.parentElement;
805
- }
806
- }
807
- updateSession({ ui: { activeSelector: null, stylebookSelection: null } });
808
- renderStylebookOverlays();
809
- });
810
-
811
- overlayClk.addEventListener("mousemove", (e: MouseEvent) => {
812
- const els = canvas.querySelectorAll("*");
813
- for (const el of els) {
814
- (el as HTMLElement).style.pointerEvents = "auto";
815
- }
816
- overlayClk.style.display = "none";
817
- const elements = document.elementsFromPoint(e.clientX, e.clientY);
818
- overlayClk.style.display = "";
819
- for (const el of els) {
820
- (el as HTMLElement).style.pointerEvents = "none";
821
- }
822
-
823
- let hoverTag = null;
824
- for (const el of elements) {
825
- if (!canvas.contains(el) || el === canvas) {
826
- continue;
827
- }
828
- let cur = el as Element | null;
829
- while (cur && cur !== canvas) {
830
- const tag = view.stylebookElToTag.get(cur);
831
- if (tag) {
832
- hoverTag = tag;
833
- break;
834
- }
835
- cur = cur.parentElement;
836
- }
837
- if (hoverTag) {
838
- break;
839
- }
840
- }
841
-
842
- if (hoverTag !== panel._lastHoverTag) {
843
- panel._lastHoverTag = hoverTag;
844
- renderStylebookOverlays();
845
- }
846
- });
847
- }
848
-
849
- /** Find a stylebook element by tag in the canvas */
850
- function findStylebookEl(canvasEl: HTMLElement, tag: string) {
851
- for (const child of canvasEl.querySelectorAll("*")) {
852
- if (view.stylebookElToTag.get(child) === tag) {
853
- return child as HTMLElement;
854
- }
855
- }
856
- return null;
857
- }
213
+ /** Re-exported for legacy consumers (the preview now lives in component-preview.ts). */
214
+ export { renderComponentPreview } from "./component-preview";